summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJeff Davidson <jpd@google.com>2018-02-08 15:30:06 -0800
committerJeff Davidson <jpd@google.com>2018-02-08 15:30:06 -0800
commita192cc2a132cb0ee8588e2df755563ec7008c179 (patch)
tree380e4db22df19c819bd37df34bf06e7568916a50
parent98fe7819c6d14f4f464a5cac047f9e82dee5da58 (diff)
downloadandroid-28-a192cc2a132cb0ee8588e2df755563ec7008c179.tar.gz
Update fullsdk to 4575844
/google/data/ro/projects/android/fetch_artifact \ --bid 4575844 \ --target sdk_phone_x86_64-sdk \ sdk-repo-linux-sources-4575844.zip Test: TreeHugger Change-Id: I81e0eb157b4ac3b38408d0ef86f9d6286471f87a
-rw-r--r--android/accessibilityservice/AccessibilityService.java5
-rw-r--r--android/annotation/SystemApi.java2
-rw-r--r--android/app/Activity.java109
-rw-r--r--android/app/ActivityManager.java66
-rw-r--r--android/app/ActivityManagerInternal.java25
-rw-r--r--android/app/ActivityManagerNative.java6
-rw-r--r--android/app/ActivityOptions.java61
-rw-r--r--android/app/ActivityThread.java68
-rw-r--r--android/app/ActivityView.java97
-rw-r--r--android/app/AppOpsManager.java248
-rw-r--r--android/app/ApplicationPackageManager.java68
-rw-r--r--android/app/ClientTransactionHandler.java8
-rw-r--r--android/app/ContextImpl.java16
-rw-r--r--android/app/Dialog.java35
-rw-r--r--android/app/Instrumentation.java11
-rw-r--r--android/app/KeyguardManager.java56
-rw-r--r--android/app/Notification.java579
-rw-r--r--android/app/NotificationChannel.java12
-rw-r--r--android/app/NotificationChannelGroup.java8
-rw-r--r--android/app/NotificationManager.java14
-rw-r--r--android/app/PendingIntent.java19
-rw-r--r--android/app/ProfilerInfo.java30
-rw-r--r--android/app/RemoteInput.java52
-rw-r--r--android/app/SharedPreferencesImpl.java170
-rw-r--r--android/app/StatsManager.java238
-rw-r--r--android/app/SystemServiceRegistry.java28
-rw-r--r--android/app/UiAutomation.java15
-rw-r--r--android/app/admin/ConnectEvent.java2
-rw-r--r--android/app/admin/DeviceAdminReceiver.java178
-rw-r--r--android/app/admin/DevicePolicyManager.java657
-rw-r--r--android/app/admin/DevicePolicyManagerInternal.java18
-rw-r--r--android/app/admin/DnsEvent.java2
-rw-r--r--android/app/assist/AssistStructure.java26
-rw-r--r--android/app/backup/BackupManager.java25
-rw-r--r--android/app/backup/BackupManagerMonitor.java6
-rw-r--r--android/app/backup/BackupTransport.java53
-rw-r--r--android/app/job/JobInfo.java27
-rw-r--r--android/app/job/JobParameters.java14
-rw-r--r--android/app/servertransaction/ActivityLifecycleItem.java38
-rw-r--r--android/app/servertransaction/ClientTransaction.java14
-rw-r--r--android/app/servertransaction/DestroyActivityItem.java2
-rw-r--r--android/app/servertransaction/PauseActivityItem.java2
-rw-r--r--android/app/servertransaction/ResumeActivityItem.java2
-rw-r--r--android/app/servertransaction/StopActivityItem.java2
-rw-r--r--android/app/servertransaction/TransactionExecutor.java22
-rw-r--r--android/app/slice/Slice.java115
-rw-r--r--android/app/slice/SliceItem.java12
-rw-r--r--android/app/slice/SliceManager.java240
-rw-r--r--android/app/slice/SliceProvider.java210
-rw-r--r--android/app/timezone/RulesState.java3
-rw-r--r--android/app/trust/TrustManager.java29
-rw-r--r--android/app/usage/NetworkStats.java52
-rw-r--r--android/app/usage/NetworkStatsManager.java114
-rw-r--r--android/app/usage/UsageEvents.java20
-rw-r--r--android/app/usage/UsageStatsManagerInternal.java87
-rw-r--r--android/appwidget/AppWidgetManager.java28
-rw-r--r--android/arch/lifecycle/ComputableLiveData.java139
-rw-r--r--android/arch/lifecycle/GenericLifecycleObserver.java3
-rw-r--r--android/arch/lifecycle/HolderFragment.java5
-rw-r--r--android/arch/lifecycle/LiveData.java410
-rw-r--r--android/arch/lifecycle/LiveDataTest.java35
-rw-r--r--android/arch/lifecycle/ThreadedLiveDataTest.java3
-rw-r--r--android/arch/lifecycle/TransformationsTest.java22
-rw-r--r--android/arch/lifecycle/ViewModelProvider.java56
-rw-r--r--android/arch/lifecycle/ViewModelProviderTest.java2
-rw-r--r--android/arch/lifecycle/ViewModelProviders.java70
-rw-r--r--android/arch/lifecycle/ViewModelStores.java2
-rw-r--r--android/arch/paging/ContiguousPagedList.java3
-rw-r--r--android/arch/paging/DataSource.java277
-rw-r--r--android/arch/paging/LivePagedListProvider.java4
-rw-r--r--android/arch/paging/PagedListAdapter.java2
-rw-r--r--android/arch/paging/PagedListAdapterHelper.java8
-rw-r--r--android/arch/paging/PositionalDataSource.java24
-rw-r--r--android/arch/paging/TiledDataSource.java6
-rw-r--r--android/arch/paging/TiledPagedList.java6
-rw-r--r--android/arch/persistence/db/SimpleSQLiteQuery.java4
-rw-r--r--android/arch/persistence/room/BuilderTest.java109
-rw-r--r--android/arch/persistence/room/ColumnInfo.java16
-rw-r--r--android/arch/persistence/room/DatabaseConfiguration.java27
-rw-r--r--android/arch/persistence/room/RawQuery.java157
-rw-r--r--android/arch/persistence/room/RoomDatabase.java79
-rw-r--r--android/arch/persistence/room/RoomOpenHelper.java26
-rw-r--r--android/arch/persistence/room/integration/testapp/TestDatabase.java2
-rw-r--r--android/arch/persistence/room/integration/testapp/dao/PetDao.java17
-rw-r--r--android/arch/persistence/room/integration/testapp/dao/RawDao.java67
-rw-r--r--android/arch/persistence/room/integration/testapp/dao/UserDao.java14
-rw-r--r--android/arch/persistence/room/integration/testapp/migration/MigrationTest.java94
-rw-r--r--android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java38
-rw-r--r--android/arch/persistence/room/integration/testapp/paging/LimitOffsetDataSourceTest.java13
-rw-r--r--android/arch/persistence/room/integration/testapp/test/CollationTest.java187
-rw-r--r--android/arch/persistence/room/integration/testapp/test/DatabaseCallbackTest.java52
-rw-r--r--android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java18
-rw-r--r--android/arch/persistence/room/integration/testapp/test/PojoTest.java9
-rw-r--r--android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java12
-rw-r--r--android/arch/persistence/room/integration/testapp/test/RawQueryTest.java196
-rw-r--r--android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java42
-rw-r--r--android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java3
-rw-r--r--android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java19
-rw-r--r--android/arch/persistence/room/migration/TableInfoTest.java24
-rw-r--r--android/arch/persistence/room/migration/bundle/DatabaseBundle.java8
-rw-r--r--android/arch/persistence/room/migration/bundle/EntityBundle.java15
-rw-r--r--android/arch/persistence/room/migration/bundle/EntityBundleTest.java168
-rw-r--r--android/arch/persistence/room/migration/bundle/FieldBundle.java12
-rw-r--r--android/arch/persistence/room/migration/bundle/FieldBundleTest.java62
-rw-r--r--android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java24
-rw-r--r--android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java95
-rw-r--r--android/arch/persistence/room/migration/bundle/IndexBundle.java25
-rw-r--r--android/arch/persistence/room/migration/bundle/IndexBundleTest.java83
-rw-r--r--android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java7
-rw-r--r--android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java65
-rw-r--r--android/arch/persistence/room/migration/bundle/SchemaBundle.java8
-rw-r--r--android/arch/persistence/room/migration/bundle/SchemaEquality.java30
-rw-r--r--android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java90
-rw-r--r--android/arch/persistence/room/paging/LimitOffsetDataSource.java47
-rw-r--r--android/arch/persistence/room/testing/MigrationTestHelper.java10
-rw-r--r--android/arch/persistence/room/util/TableInfo.java54
-rw-r--r--android/bluetooth/BluetoothA2dp.java88
-rw-r--r--android/bluetooth/BluetoothAdapter.java147
-rw-r--r--android/bluetooth/BluetoothDevice.java74
-rw-r--r--android/bluetooth/BluetoothHeadset.java122
-rw-r--r--android/bluetooth/BluetoothHeadsetClientCall.java24
-rw-r--r--android/bluetooth/BluetoothProfile.java12
-rw-r--r--android/bluetooth/BluetoothServerSocket.java20
-rw-r--r--android/bluetooth/BluetoothSocket.java19
-rw-r--r--android/bluetooth/client/map/BluetoothMapBmessage.java170
-rw-r--r--android/bluetooth/client/map/BluetoothMapBmessageBuilder.java160
-rw-r--r--android/bluetooth/client/map/BluetoothMapBmessageParser.java459
-rw-r--r--android/bluetooth/client/map/BluetoothMapEventReport.java223
-rw-r--r--android/bluetooth/client/map/BluetoothMapFolderListing.java69
-rw-r--r--android/bluetooth/client/map/BluetoothMapMessage.java338
-rw-r--r--android/bluetooth/client/map/BluetoothMapMessagesListing.java84
-rw-r--r--android/bluetooth/client/map/BluetoothMapRfcommTransport.java92
-rw-r--r--android/bluetooth/client/map/BluetoothMasClient.java1106
-rw-r--r--android/bluetooth/client/map/BluetoothMasObexClientSession.java187
-rw-r--r--android/bluetooth/client/map/BluetoothMasRequest.java162
-rw-r--r--android/bluetooth/client/map/BluetoothMasRequestGetFolderListing.java75
-rw-r--r--android/bluetooth/client/map/BluetoothMasRequestGetFolderListingSize.java56
-rw-r--r--android/bluetooth/client/map/BluetoothMasRequestGetMessage.java101
-rw-r--r--android/bluetooth/client/map/BluetoothMasRequestGetMessagesListing.java155
-rw-r--r--android/bluetooth/client/map/BluetoothMasRequestGetMessagesListingSize.java57
-rw-r--r--android/bluetooth/client/map/BluetoothMasRequestPushMessage.java79
-rw-r--r--android/bluetooth/client/map/BluetoothMasRequestSetMessageStatus.java52
-rw-r--r--android/bluetooth/client/map/BluetoothMasRequestSetNotificationRegistration.java52
-rw-r--r--android/bluetooth/client/map/BluetoothMasRequestSetPath.java71
-rw-r--r--android/bluetooth/client/map/BluetoothMnsObexServer.java136
-rw-r--r--android/bluetooth/client/map/BluetoothMnsService.java195
-rw-r--r--android/bluetooth/client/map/utils/BmsgTokenizer.java108
-rw-r--r--android/bluetooth/client/map/utils/ObexAppParameters.java182
-rw-r--r--android/bluetooth/client/map/utils/ObexTime.java101
-rw-r--r--android/bluetooth/le/BluetoothLeScanner.java2
-rw-r--r--android/content/ClipData.java43
-rw-r--r--android/content/ClipDescription.java23
-rw-r--r--android/content/ComponentName.java4
-rw-r--r--android/content/Context.java394
-rw-r--r--android/content/Intent.java17
-rw-r--r--android/content/ServiceConnection.java5
-rw-r--r--android/content/pm/ApplicationInfo.java113
-rw-r--r--android/content/pm/CrossProfileApps.java (renamed from android/content/pm/crossprofile/CrossProfileApps.java)15
-rw-r--r--android/content/pm/OrgApacheHttpLegacyUpdater.java66
-rw-r--r--android/content/pm/PackageBackwardCompatibility.java148
-rw-r--r--android/content/pm/PackageInfo.java72
-rw-r--r--android/content/pm/PackageInstaller.java26
-rw-r--r--android/content/pm/PackageItemInfo.java21
-rw-r--r--android/content/pm/PackageManager.java193
-rw-r--r--android/content/pm/PackageManagerInternal.java19
-rw-r--r--android/content/pm/PackageParser.java516
-rw-r--r--android/content/pm/PackageSharedLibraryUpdater.java55
-rw-r--r--android/content/pm/PackageUserState.java7
-rw-r--r--android/content/pm/ShortcutInfo.java9
-rw-r--r--android/content/pm/dex/ArtManager.java10
-rw-r--r--android/content/pm/dex/DexMetadataHelper.java230
-rw-r--r--android/content/res/BridgeAssetManager.java1
-rw-r--r--android/content/res/BridgeTypedArray.java8
-rw-r--r--android/content/res/ResourcesImpl.java2
-rw-r--r--android/content/res/Resources_Delegate.java6
-rw-r--r--android/database/sqlite/SQLiteOpenHelper.java8
-rw-r--r--android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java69
-rw-r--r--android/ext/services/autofill/EditDistanceScorer.java (renamed from android/service/autofill/EditDistanceScorer.java)61
-rw-r--r--android/graphics/BaseCanvas.java22
-rw-r--r--android/graphics/BidiRenderer.java5
-rw-r--r--android/graphics/ImageDecoder.java882
-rw-r--r--android/graphics/ImageDecoder_Delegate.java63
-rw-r--r--android/graphics/Paint.java2
-rw-r--r--android/graphics/PostProcessor.java (renamed from android/graphics/PostProcess.java)30
-rw-r--r--android/graphics/Typeface.java6
-rw-r--r--android/graphics/drawable/AnimatedImageDrawable.java191
-rw-r--r--android/graphics/drawable/BitmapDrawable.java4
-rw-r--r--android/graphics/drawable/Icon.java6
-rw-r--r--android/graphics/drawable/RippleBackground.java33
-rw-r--r--android/graphics/drawable/RippleDrawable.java21
-rw-r--r--android/graphics/drawable/RippleForeground.java1
-rw-r--r--android/hardware/HardwareBuffer.java56
-rw-r--r--android/hardware/camera2/CameraCharacteristics.java275
-rw-r--r--android/hardware/camera2/CameraDevice.java96
-rw-r--r--android/hardware/camera2/CameraManager.java7
-rw-r--r--android/hardware/camera2/CameraMetadata.java210
-rw-r--r--android/hardware/camera2/CaptureRequest.java222
-rw-r--r--android/hardware/camera2/CaptureResult.java144
-rw-r--r--android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java7
-rw-r--r--android/hardware/camera2/impl/CameraDeviceImpl.java108
-rw-r--r--android/hardware/camera2/params/OutputConfiguration.java66
-rw-r--r--android/hardware/display/BrightnessChangeEvent.java135
-rw-r--r--android/hardware/display/BrightnessConfiguration.java70
-rw-r--r--android/hardware/display/DisplayManager.java48
-rw-r--r--android/hardware/display/DisplayManagerGlobal.java41
-rw-r--r--android/hardware/display/DisplayManagerInternal.java54
-rw-r--r--android/hardware/fingerprint/FingerprintDialog.java293
-rw-r--r--android/hardware/fingerprint/FingerprintManager.java261
-rw-r--r--android/hardware/hdmi/HdmiTimerRecordSources.java1
-rw-r--r--android/hardware/location/ContextHubClient.java41
-rw-r--r--android/hardware/location/ContextHubClientCallback.java36
-rw-r--r--android/hardware/location/ContextHubInfo.java12
-rw-r--r--android/hardware/location/ContextHubManager.java159
-rw-r--r--android/hardware/location/ContextHubMessage.java36
-rw-r--r--android/hardware/location/ContextHubTransaction.java45
-rw-r--r--android/hardware/location/NanoApp.java5
-rw-r--r--android/hardware/location/NanoAppBinary.java2
-rw-r--r--android/hardware/location/NanoAppFilter.java5
-rw-r--r--android/hardware/location/NanoAppInstanceInfo.java8
-rw-r--r--android/hardware/location/NanoAppMessage.java28
-rw-r--r--android/hardware/location/NanoAppState.java2
-rw-r--r--android/hardware/radio/Announcement.java133
-rw-r--r--android/hardware/radio/ProgramList.java427
-rw-r--r--android/hardware/radio/ProgramSelector.java164
-rw-r--r--android/hardware/radio/RadioManager.java401
-rw-r--r--android/hardware/radio/RadioTuner.java76
-rw-r--r--android/hardware/radio/TunerAdapter.java92
-rw-r--r--android/hardware/radio/TunerCallbackAdapter.java73
-rw-r--r--android/hardware/radio/Utils.java118
-rw-r--r--android/hardware/usb/UsbManager.java26
-rw-r--r--android/inputmethodservice/InputMethodService.java155
-rw-r--r--android/location/LocationManager.java147
-rw-r--r--android/media/AudioAttributes.java7
-rw-r--r--android/media/AudioFocusInfo.java2
-rw-r--r--android/media/AudioFocusRequest.java31
-rw-r--r--android/media/AudioFormat.java73
-rw-r--r--android/media/AudioManager.java40
-rw-r--r--android/media/AudioPort.java3
-rw-r--r--android/media/AudioSystem.java14
-rw-r--r--android/media/AudioTrack.java156
-rw-r--r--android/media/DataSourceDesc.java465
-rw-r--r--android/media/Media2DataSource.java62
-rw-r--r--android/media/Media2HTTPConnection.java385
-rw-r--r--android/media/Media2HTTPService.java98
-rw-r--r--android/media/MediaBrowser2.java176
-rw-r--r--android/media/MediaBrowser2Test.java141
-rw-r--r--android/media/MediaCodecInfo.java5
-rw-r--r--android/media/MediaController2.java616
-rw-r--r--android/media/MediaController2Test.java487
-rw-r--r--android/media/MediaDrm.java301
-rw-r--r--android/media/MediaFormat.java17
-rw-r--r--android/media/MediaItem2.java146
-rw-r--r--android/media/MediaLibraryService2.java350
-rw-r--r--android/media/MediaMetadata2.java815
-rw-r--r--android/media/MediaPlayer2.java2476
-rw-r--r--android/media/MediaPlayer2Impl.java4899
-rw-r--r--android/media/MediaPlayerBase.java72
-rw-r--r--android/media/MediaRecorder.java33
-rw-r--r--android/media/MediaSession2.java1223
-rw-r--r--android/media/MediaSession2Test.java273
-rw-r--r--android/media/MediaSession2TestBase.java210
-rw-r--r--android/media/MediaSessionManager_MediaSession2.java223
-rw-r--r--android/media/MediaSessionService2.java247
-rw-r--r--android/media/MockMediaLibraryService2.java98
-rw-r--r--android/media/MockMediaSessionService2.java102
-rw-r--r--android/media/MockPlayer.java146
-rw-r--r--android/media/PlaybackListenerHolder.java73
-rw-r--r--android/media/PlaybackState2.java216
-rw-r--r--android/media/Rating2.java304
-rw-r--r--android/media/SessionToken2.java225
-rw-r--r--android/media/TestServiceRegistry.java135
-rw-r--r--android/media/TestUtils.java124
-rw-r--r--android/media/session/MediaSessionManager.java102
-rw-r--r--android/media/update/ApiLoader.java60
-rw-r--r--android/media/update/MediaBrowser2Provider.java33
-rw-r--r--android/media/update/MediaControlView2Provider.java47
-rw-r--r--android/media/update/MediaController2Provider.java64
-rw-r--r--android/media/update/MediaLibraryService2Provider.java30
-rw-r--r--android/media/update/MediaSession2Provider.java65
-rw-r--r--android/media/update/MediaSessionService2Provider.java35
-rw-r--r--android/media/update/StaticProvider.java81
-rw-r--r--android/media/update/TransportControlProvider.java39
-rw-r--r--android/media/update/VideoView2Provider.java73
-rw-r--r--android/media/update/ViewProvider.java49
-rw-r--r--android/net/ConnectivityManager.java16
-rw-r--r--android/net/IpSecAlgorithm.java33
-rw-r--r--android/net/IpSecConfig.java225
-rw-r--r--android/net/IpSecManager.java327
-rw-r--r--android/net/IpSecTransform.java169
-rw-r--r--android/net/IpSecTunnelInterfaceResponse.java78
-rw-r--r--android/net/KeepalivePacketData.java (renamed from com/android/server/connectivity/KeepalivePacketData.java)91
-rw-r--r--android/net/LinkProperties.java110
-rw-r--r--android/net/MacAddress.java27
-rw-r--r--android/net/Network.java14
-rw-r--r--android/net/NetworkAgent.java32
-rw-r--r--android/net/NetworkCapabilities.java303
-rw-r--r--android/net/NetworkIdentity.java25
-rw-r--r--android/net/NetworkPolicyManager.java17
-rw-r--r--android/net/NetworkRequest.java30
-rw-r--r--android/net/NetworkStats.java83
-rw-r--r--android/net/NetworkTemplate.java69
-rw-r--r--android/net/NetworkWatchlistManager.java30
-rw-r--r--android/net/TrafficStats.java16
-rw-r--r--android/net/apf/ApfFilter.java24
-rw-r--r--android/net/dhcp/DhcpClient.java30
-rw-r--r--android/net/ip/ConnectivityPacketTracker.java36
-rw-r--r--android/net/ip/IpClient.java30
-rw-r--r--android/net/ip/IpNeighborMonitor.java41
-rw-r--r--android/net/ip/IpReachabilityMonitor.java54
-rw-r--r--android/net/ip/RouterAdvertisementDaemon.java21
-rw-r--r--android/net/metrics/WakeupStats.java2
-rw-r--r--android/net/util/ConnectivityPacketSummary.java14
-rw-r--r--android/net/util/InterfaceParams.java94
-rw-r--r--android/net/util/MultinetworkPolicyTracker.java1
-rw-r--r--android/net/util/NetworkConstants.java18
-rw-r--r--android/net/wifi/ScanResult.java100
-rw-r--r--android/net/wifi/WifiActivityEnergyInfo.java25
-rw-r--r--android/net/wifi/WifiConfiguration.java104
-rw-r--r--android/net/wifi/WifiConnectionStatistics.java158
-rw-r--r--android/net/wifi/WifiManager.java346
-rw-r--r--android/net/wifi/aware/DiscoverySessionCallback.java1
-rw-r--r--android/net/wifi/aware/PublishConfig.java2
-rw-r--r--android/net/wifi/aware/SubscribeConfig.java4
-rw-r--r--android/net/wifi/aware/WifiAwareManager.java12
-rw-r--r--android/net/wifi/rtt/LocationCivic.java118
-rw-r--r--android/net/wifi/rtt/LocationConfigurationInformation.java272
-rw-r--r--android/net/wifi/rtt/RangingRequest.java8
-rw-r--r--android/net/wifi/rtt/RangingResult.java86
-rw-r--r--android/net/wifi/rtt/RangingResultCallback.java5
-rw-r--r--android/net/wifi/rtt/ResponderConfig.java10
-rw-r--r--android/net/wifi/rtt/WifiRttManager.java41
-rw-r--r--android/os/BatteryStats.java232
-rw-r--r--android/os/Binder.java49
-rw-r--r--android/os/Bundle.java18
-rw-r--r--android/os/ConfigUpdate.java8
-rw-r--r--android/os/Debug.java22
-rw-r--r--android/os/Environment.java10
-rw-r--r--android/os/Handler.java31
-rw-r--r--android/os/HidlSupport.java35
-rw-r--r--android/os/HwBinder.java27
-rw-r--r--android/os/HwBlob.java277
-rw-r--r--android/os/HwParcel.java312
-rw-r--r--android/os/IHwBinder.java21
-rw-r--r--android/os/IHwInterface.java6
-rw-r--r--android/os/PackageManagerPerfTest.java114
-rw-r--r--android/os/PersistableBundle.java18
-rw-r--r--android/os/PowerManager.java88
-rw-r--r--android/os/PowerManagerInternal.java18
-rw-r--r--android/os/Process.java25
-rw-r--r--android/os/PssPerfTest.java41
-rw-r--r--android/os/RecoverySystem.java63
-rw-r--r--android/os/StatsDimensionsValue.java353
-rw-r--r--android/os/SystemProperties.java22
-rw-r--r--android/os/SystemUpdateManager.java152
-rw-r--r--android/os/UserHandle.java24
-rw-r--r--android/os/UserManager.java114
-rw-r--r--android/os/VintfObject.java4
-rw-r--r--android/os/WorkSource.java32
-rw-r--r--android/os/connectivity/GpsBatteryStats.java108
-rw-r--r--android/os/connectivity/WifiBatteryStats.java279
-rw-r--r--android/os/storage/StorageManager.java19
-rw-r--r--android/os/storage/StorageVolume.java28
-rw-r--r--android/perftests/utils/BenchmarkState.java46
-rw-r--r--android/perftests/utils/ManualBenchmarkState.java157
-rw-r--r--android/perftests/utils/PerfManualStatusReporter.java73
-rw-r--r--android/perftests/utils/Stats.java76
-rw-r--r--android/privacy/internal/rappor/RapporEncoder.java5
-rw-r--r--android/provider/AlarmClock.java7
-rw-r--r--android/provider/CallLog.java9
-rw-r--r--android/provider/DocumentsContract.java4
-rw-r--r--android/provider/Settings.java1284
-rw-r--r--android/provider/SettingsValidators.java249
-rw-r--r--android/provider/Telephony.java50
-rw-r--r--android/provider/VoicemailContract.java27
-rw-r--r--android/security/KeyStore.java22
-rw-r--r--android/security/keymaster/KeymasterDefs.java4
-rw-r--r--android/security/keystore/AndroidKeyStore3DESCipherSpi.java298
-rw-r--r--android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java10
-rw-r--r--android/security/keystore/AndroidKeyStoreCipherSpiBase.java2
-rw-r--r--android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java6
-rw-r--r--android/security/keystore/AndroidKeyStoreProvider.java5
-rw-r--r--android/security/keystore/AndroidKeyStoreSpi.java30
-rw-r--r--android/security/keystore/AttestationUtils.java89
-rw-r--r--android/security/keystore/BackwardsCompat.java127
-rw-r--r--android/security/keystore/BadCertificateFormatException.java28
-rw-r--r--android/security/keystore/DecryptionFailedException.java30
-rw-r--r--android/security/keystore/InternalRecoveryServiceException.java35
-rw-r--r--android/security/keystore/KeyDerivationParams.java (renamed from android/security/recoverablekeystore/KeyDerivationParameters.java)39
-rw-r--r--android/security/keystore/KeyGenParameterSpec.java25
-rw-r--r--android/security/keystore/KeyProperties.java29
-rw-r--r--android/security/keystore/KeyProtection.java2
-rw-r--r--android/security/keystore/KeychainProtectionParams.java285
-rw-r--r--android/security/keystore/KeychainSnapshot.java290
-rw-r--r--android/security/keystore/LockScreenRequiredException.java (renamed from android/service/autofill/Scorer.java)20
-rw-r--r--android/security/keystore/RecoveryClaim.java54
-rw-r--r--android/security/keystore/RecoveryController.java515
-rw-r--r--android/security/keystore/RecoveryControllerException.java36
-rw-r--r--android/security/keystore/RecoverySession.java71
-rw-r--r--android/security/keystore/SessionExpiredException.java (renamed from android/os/Seccomp.java)12
-rw-r--r--android/security/keystore/StrongBoxUnavailableException.java (renamed from android/arch/lifecycle/LifecycleActivity.java)12
-rw-r--r--android/security/keystore/WrappedApplicationKey.java144
-rw-r--r--android/security/keystore/WrappedKeyEntry.java56
-rw-r--r--android/security/keystore/recovery/BadCertificateFormatException.java29
-rw-r--r--android/security/keystore/recovery/DecryptionFailedException.java34
-rw-r--r--android/security/keystore/recovery/InternalRecoveryServiceException.java39
-rw-r--r--android/security/keystore/recovery/KeyChainProtectionParams.java287
-rw-r--r--android/security/keystore/recovery/KeyChainSnapshot.java299
-rw-r--r--android/security/keystore/recovery/KeyDerivationParams.java119
-rw-r--r--android/security/keystore/recovery/LockScreenRequiredException.java35
-rw-r--r--android/security/keystore/recovery/RecoveryClaim.java55
-rw-r--r--android/security/keystore/recovery/RecoveryController.java460
-rw-r--r--android/security/keystore/recovery/RecoveryControllerException.java (renamed from android/bluetooth/client/map/BluetoothMasRequestUpdateInbox.java)29
-rw-r--r--android/security/keystore/recovery/RecoverySession.java177
-rw-r--r--android/security/keystore/recovery/SessionExpiredException.java (renamed from android/support/design/widget/CircularBorderDrawableLollipop.java)25
-rw-r--r--android/security/keystore/recovery/WrappedApplicationKey.java169
-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/AutofillFieldClassificationService.java235
-rw-r--r--android/service/autofill/CharSequenceTransformation.java28
-rw-r--r--android/service/autofill/FieldClassification.java8
-rw-r--r--android/service/autofill/InternalScorer.java40
-rw-r--r--android/service/autofill/UserData.java66
-rw-r--r--android/service/carrier/CarrierIdentifier.java77
-rw-r--r--android/service/dreams/DreamService.java37
-rw-r--r--android/service/euicc/EuiccProfileInfo.java362
-rw-r--r--android/service/euicc/EuiccService.java42
-rw-r--r--android/service/notification/NotificationListenerService.java28
-rw-r--r--android/service/notification/NotifyingApp.java139
-rw-r--r--android/service/trust/TrustAgentService.java29
-rw-r--r--android/service/wallpaper/WallpaperService.java23
-rw-r--r--android/support/LibraryVersions.java19
-rw-r--r--android/support/Version.java157
-rw-r--r--android/support/VersionFileWriterTask.java109
-rw-r--r--android/support/animation/AnimationHandler.java7
-rw-r--r--android/support/animation/DynamicAnimation.java2
-rw-r--r--android/support/animation/SpringForce.java3
-rw-r--r--android/support/customtabs/CustomTabsClient.java2
-rw-r--r--android/support/design/internal/BaselineLayout.java116
-rw-r--r--android/support/design/internal/BottomNavigationItemView.java258
-rw-r--r--android/support/design/internal/BottomNavigationMenu.java59
-rw-r--r--android/support/design/internal/BottomNavigationMenuView.java343
-rw-r--r--android/support/design/internal/BottomNavigationPresenter.java152
-rw-r--r--android/support/design/internal/ForegroundLinearLayout.java240
-rw-r--r--android/support/design/internal/NavigationMenu.java49
-rw-r--r--android/support/design/internal/NavigationMenuItemView.java272
-rw-r--r--android/support/design/internal/NavigationMenuPresenter.java686
-rw-r--r--android/support/design/internal/NavigationMenuView.java58
-rw-r--r--android/support/design/internal/NavigationSubMenu.java46
-rw-r--r--android/support/design/internal/ParcelableSparseArray.java83
-rw-r--r--android/support/design/internal/ScrimInsetsFrameLayout.java138
-rw-r--r--android/support/design/internal/SnackbarContentLayout.java157
-rw-r--r--android/support/design/internal/TextScale.java85
-rw-r--r--android/support/design/internal/package-info.java9
-rw-r--r--android/support/design/widget/AnimationUtils.java45
-rw-r--r--android/support/design/widget/AppBarLayout.java1474
-rw-r--r--android/support/design/widget/BaseTransientBottomBar.java753
-rw-r--r--android/support/design/widget/BottomNavigationView.java477
-rw-r--r--android/support/design/widget/BottomSheetBehavior.java829
-rw-r--r--android/support/design/widget/BottomSheetDialog.java230
-rw-r--r--android/support/design/widget/BottomSheetDialogFragment.java35
-rw-r--r--android/support/design/widget/CheckableImageButton.java101
-rw-r--r--android/support/design/widget/CircularBorderDrawable.java211
-rw-r--r--android/support/design/widget/CollapsingTextHelper.java723
-rw-r--r--android/support/design/widget/CollapsingToolbarLayout.java1308
-rw-r--r--android/support/design/widget/CoordinatorLayout.java6
-rw-r--r--android/support/design/widget/DrawableUtils.java65
-rw-r--r--android/support/design/widget/FloatingActionButton.java870
-rw-r--r--android/support/design/widget/FloatingActionButtonImpl.java531
-rw-r--r--android/support/design/widget/FloatingActionButtonLollipop.java226
-rw-r--r--android/support/design/widget/HeaderBehavior.java307
-rw-r--r--android/support/design/widget/HeaderScrollingViewBehavior.java182
-rw-r--r--android/support/design/widget/NavigationView.java494
-rw-r--r--android/support/design/widget/ShadowDrawableWrapper.java365
-rw-r--r--android/support/design/widget/Snackbar.java353
-rw-r--r--android/support/design/widget/SnackbarManager.java243
-rw-r--r--android/support/design/widget/StateListAnimator.java116
-rw-r--r--android/support/design/widget/SwipeDismissBehavior.java411
-rw-r--r--android/support/design/widget/TabItem.java57
-rw-r--r--android/support/design/widget/TabLayout.java2217
-rw-r--r--android/support/design/widget/TextInputEditText.java65
-rw-r--r--android/support/design/widget/TextInputLayout.java1530
-rw-r--r--android/support/design/widget/ThemeUtils.java37
-rw-r--r--android/support/design/widget/ViewOffsetBehavior.java91
-rw-r--r--android/support/design/widget/ViewOffsetHelper.java102
-rw-r--r--android/support/design/widget/ViewUtils.java39
-rw-r--r--android/support/design/widget/ViewUtilsLollipop.java79
-rw-r--r--android/support/design/widget/VisibilityAwareImageButton.java55
-rw-r--r--android/support/graphics/drawable/AndroidResources.java4
-rw-r--r--android/support/graphics/drawable/AnimatedVectorDrawableCompat.java3
-rw-r--r--android/support/graphics/drawable/AnimatorInflaterCompat.java1
-rw-r--r--android/support/media/ExifInterface.java5
-rw-r--r--android/support/media/tv/BasePreviewProgram.java2
-rw-r--r--android/support/media/tv/BaseProgram.java2
-rw-r--r--android/support/media/tv/TvContractCompat.java1
-rw-r--r--android/support/multidex/MultiDex.java136
-rw-r--r--android/support/multidex/MultiDexExtractor.java149
-rw-r--r--android/support/transition/TransitionSet.java11
-rw-r--r--android/support/transition/TransitionSetTest.java8
-rw-r--r--android/support/v13/view/DragAndDropPermissionsCompat.java63
-rw-r--r--android/support/v13/view/DragStartHelper.java4
-rw-r--r--android/support/v13/view/inputmethod/EditorInfoCompat.java81
-rw-r--r--android/support/v13/view/inputmethod/InputConnectionCompat.java213
-rw-r--r--android/support/v14/preference/EditTextPreferenceDialogFragment.java6
-rw-r--r--android/support/v14/preference/ListPreferenceDialogFragment.java2
-rw-r--r--android/support/v14/preference/MultiSelectListPreference.java47
-rw-r--r--android/support/v14/preference/MultiSelectListPreferenceDialogFragment.java6
-rw-r--r--android/support/v14/preference/PreferenceDialogFragment.java10
-rw-r--r--android/support/v14/preference/PreferenceFragment.java18
-rw-r--r--android/support/v14/preference/SwitchPreference.java29
-rw-r--r--android/support/v17/leanback/app/PlaybackFragment.java5
-rw-r--r--android/support/v17/leanback/app/PlaybackSupportFragment.java5
-rw-r--r--android/support/v17/leanback/media/PlaybackControlGlue.java2
-rw-r--r--android/support/v17/leanback/media/PlaybackTransportControlGlue.java2
-rw-r--r--android/support/v17/leanback/transition/TransitionEpicenterCallback.java3
-rw-r--r--android/support/v17/leanback/util/MathUtil.java3
-rw-r--r--android/support/v17/leanback/widget/DetailsParallaxDrawable.java2
-rw-r--r--android/support/v17/leanback/widget/GridLayoutManager.java5
-rw-r--r--android/support/v17/leanback/widget/MediaRowFocusView.java4
-rw-r--r--android/support/v17/leanback/widget/ParallaxEffect.java4
-rw-r--r--android/support/v17/leanback/widget/VideoSurfaceView.java2
-rw-r--r--android/support/v4/app/ActivityCompat.java27
-rw-r--r--android/support/v4/app/Fragment.java10
-rw-r--r--android/support/v4/app/FragmentActivity.java1
-rw-r--r--android/support/v4/app/LoaderManager.java124
-rw-r--r--android/support/v4/app/NotificationCompat.java199
-rw-r--r--android/support/v4/app/NotificationCompatBuilder.java9
-rw-r--r--android/support/v4/app/NotificationCompatJellybean.java7
-rw-r--r--android/support/v4/app/NotificationManagerCompat.java2
-rw-r--r--android/support/v4/content/FileProvider.java19
-rw-r--r--android/support/v4/content/Loader.java28
-rw-r--r--android/support/v4/content/WakefulBroadcastReceiver.java5
-rw-r--r--android/support/v4/graphics/TypefaceCompatUtil.java14
-rw-r--r--android/support/v4/graphics/drawable/DrawableCompat.java14
-rw-r--r--android/support/v4/graphics/drawable/WrappedDrawable.java (renamed from android/support/v4/graphics/drawable/DrawableWrapper.java)2
-rw-r--r--android/support/v4/graphics/drawable/WrappedDrawableApi14.java (renamed from android/support/v4/graphics/drawable/DrawableWrapperApi14.java)44
-rw-r--r--android/support/v4/graphics/drawable/WrappedDrawableApi19.java (renamed from android/support/v4/graphics/drawable/DrawableWrapperApi19.java)9
-rw-r--r--android/support/v4/graphics/drawable/WrappedDrawableApi21.java (renamed from android/support/v4/graphics/drawable/DrawableWrapperApi21.java)23
-rw-r--r--android/support/v4/util/ArraySet.java60
-rw-r--r--android/support/v4/util/LongSparseArray.java8
-rw-r--r--android/support/v4/util/ObjectsCompat.java43
-rw-r--r--android/support/v4/util/SparseArrayCompat.java8
-rw-r--r--android/support/v4/view/ViewCompat.java56
-rw-r--r--android/support/v4/view/ViewConfigurationCompat.java12
-rw-r--r--android/support/v4/view/WindowCompat.java27
-rw-r--r--android/support/v4/widget/ContentLoadingProgressBar.java6
-rw-r--r--android/support/v4/widget/CursorAdapter.java18
-rw-r--r--android/support/v4/widget/SimpleCursorAdapter.java4
-rw-r--r--android/support/v4/widget/TextViewCompat.java201
-rw-r--r--android/support/v7/preference/Preference.java3
-rw-r--r--android/support/v7/recyclerview/extensions/ListAdapter.java15
-rw-r--r--android/support/v7/recyclerview/extensions/ListAdapterConfig.java97
-rw-r--r--android/support/v7/recyclerview/extensions/ListAdapterHelper.java91
-rw-r--r--android/support/v7/util/AdapterListUpdateCallback.java63
-rw-r--r--android/support/v7/util/DiffUtil.java32
-rw-r--r--android/support/v7/widget/AppCompatEditText.java16
-rw-r--r--android/support/v7/widget/AppCompatProgressBarHelper.java8
-rw-r--r--android/support/v7/widget/ContentFrameLayout.java2
-rw-r--r--android/support/v7/widget/DrawableUtils.java5
-rw-r--r--android/support/v7/widget/DropDownListView.java437
-rw-r--r--android/support/v7/widget/LinearLayoutCompat.java2
-rw-r--r--android/support/v7/widget/ListViewCompat.java413
-rw-r--r--android/support/v7/widget/RecyclerView.java22
-rw-r--r--android/support/v7/widget/StaggeredGridLayoutManager.java2
-rw-r--r--android/support/v7/widget/TooltipCompatHandler.java38
-rw-r--r--android/support/wear/widget/BoxInsetLayout.java2
-rw-r--r--android/system/Os.java38
-rw-r--r--android/system/OsConstants.java3
-rw-r--r--android/telecom/Call.java12
-rw-r--r--android/telecom/Connection.java24
-rw-r--r--android/telecom/ConnectionService.java62
-rw-r--r--android/telecom/InCallAdapter.java5
-rw-r--r--android/telecom/InCallService.java11
-rw-r--r--android/telecom/Log.java53
-rw-r--r--android/telecom/Phone.java7
-rw-r--r--android/telecom/TelecomManager.java42
-rw-r--r--android/telecom/TransformationInfo.java127
-rw-r--r--android/telephony/AccessNetworkConstants.java (renamed from android/telephony/RadioNetworkConstants.java)29
-rw-r--r--android/telephony/CarrierConfigManager.java80
-rw-r--r--android/telephony/CellIdentity.java173
-rw-r--r--android/telephony/CellIdentityCdma.java79
-rw-r--r--android/telephony/CellIdentityGsm.java120
-rw-r--r--android/telephony/CellIdentityLte.java120
-rw-r--r--android/telephony/CellIdentityTdscdma.java196
-rw-r--r--android/telephony/CellIdentityWcdma.java111
-rw-r--r--android/telephony/DisconnectCause.java9
-rw-r--r--android/telephony/NetworkRegistrationState.java258
-rw-r--r--android/telephony/NetworkScanRequest.java8
-rw-r--r--android/telephony/NetworkService.java314
-rw-r--r--android/telephony/NetworkServiceCallback.java88
-rw-r--r--android/telephony/PhoneStateListener.java47
-rw-r--r--android/telephony/PhysicalChannelConfig.java127
-rw-r--r--android/telephony/RadioAccessSpecifier.java28
-rw-r--r--android/telephony/ServiceState.java149
-rw-r--r--android/telephony/SignalStrength.java351
-rw-r--r--android/telephony/SubscriptionInfo.java42
-rw-r--r--android/telephony/SubscriptionManager.java269
-rw-r--r--android/telephony/SubscriptionPlan.java1
-rw-r--r--android/telephony/TelephonyManager.java317
-rw-r--r--android/telephony/UiccAccessRule.java16
-rw-r--r--android/telephony/UiccSlotInfo.java158
-rw-r--r--android/telephony/VisualVoicemailSmsFilterSettings.java27
-rw-r--r--android/telephony/data/ApnSetting.java1351
-rw-r--r--android/telephony/data/DataCallResponse.java11
-rw-r--r--android/telephony/data/DataService.java567
-rw-r--r--android/telephony/data/DataServiceCallback.java172
-rw-r--r--android/telephony/data/InterfaceAddress.java127
-rw-r--r--android/telephony/euicc/EuiccCardManager.java680
-rw-r--r--android/telephony/euicc/EuiccManager.java33
-rw-r--r--android/telephony/euicc/EuiccNotification.java179
-rw-r--r--android/telephony/euicc/EuiccRulesAuthTable.java260
-rw-r--r--android/telephony/ims/ImsService.java19
-rw-r--r--android/telephony/ims/feature/ImsFeature.java2
-rw-r--r--android/telephony/ims/feature/MMTelFeature.java71
-rw-r--r--android/telephony/ims/internal/ImsService.java4
-rw-r--r--android/telephony/ims/internal/SmsImplBase.java260
-rw-r--r--android/telephony/ims/internal/feature/CapabilityChangeRequest.java2
-rw-r--r--android/telephony/ims/internal/feature/MmTelFeature.java78
-rw-r--r--android/telephony/ims/internal/stub/SmsImplBase.java271
-rw-r--r--android/telephony/ims/stub/ImsRegistrationImplBase.java (renamed from android/telephony/ims/internal/stub/ImsRegistrationImplBase.java)56
-rw-r--r--android/test/IsolatedContext.java79
-rw-r--r--android/test/ProviderTestCase2.java3
-rw-r--r--android/test/RenamingDelegatingContext.java3
-rw-r--r--android/test/ServiceTestCase.java11
-rw-r--r--android/test/mock/MockAccountManager.java119
-rw-r--r--android/test/mock/MockContentProvider.java17
-rw-r--r--android/test/mock/MockPackageManager.java44
-rw-r--r--android/test/mock/MockService.java49
-rw-r--r--android/text/BoringLayout.java9
-rw-r--r--android/text/DynamicLayout.java5
-rw-r--r--android/text/Layout.java4
-rw-r--r--android/text/MeasuredParagraph.java721
-rw-r--r--android/text/MeasuredParagraph_Delegate.java (renamed from android/text/MeasuredText_Delegate.java)35
-rw-r--r--android/text/MeasuredText.java849
-rw-r--r--android/text/PremeasuredText.java272
-rw-r--r--android/text/StaticLayout.java290
-rw-r--r--android/text/StaticLayoutPerfTest.java199
-rw-r--r--android/text/StaticLayout_Delegate.java2
-rw-r--r--android/text/TextLine.java19
-rw-r--r--android/text/TextUtils.java200
-rw-r--r--android/text/format/Formatter.java268
-rw-r--r--android/text/format/Time.java5
-rw-r--r--android/text/style/AbsoluteSizeSpan.java58
-rw-r--r--android/text/style/BackgroundColorSpan.java52
-rw-r--r--android/text/style/BulletSpan.java186
-rw-r--r--android/text/style/ForegroundColorSpan.java47
-rw-r--r--android/text/style/RelativeSizeSpan.java45
-rw-r--r--android/text/style/ScaleXSpan.java46
-rw-r--r--android/text/style/StrikethroughSpan.java37
-rw-r--r--android/text/style/SubscriptSpan.java44
-rw-r--r--android/text/style/SuperscriptSpan.java45
-rw-r--r--android/text/style/UnderlineSpan.java37
-rw-r--r--android/util/DataUnit.java44
-rw-r--r--android/util/FeatureFlagUtils.java12
-rw-r--r--android/util/KeyValueListParser.java23
-rw-r--r--android/util/MutableBoolean.java2
-rw-r--r--android/util/MutableByte.java2
-rw-r--r--android/util/MutableChar.java2
-rw-r--r--android/util/MutableDouble.java2
-rw-r--r--android/util/MutableFloat.java2
-rw-r--r--android/util/MutableInt.java2
-rw-r--r--android/util/MutableLong.java2
-rw-r--r--android/util/MutableShort.java2
-rw-r--r--android/util/PackageUtils.java13
-rw-r--r--android/util/StatsManager.java57
-rw-r--r--android/util/TimeUtils.java98
-rw-r--r--android/util/apk/ApkSignatureSchemeV2Verifier.java79
-rw-r--r--android/util/apk/ApkSignatureSchemeV3Verifier.java45
-rw-r--r--android/util/apk/ApkSignatureVerifier.java118
-rw-r--r--android/util/apk/ApkSigningBlockUtils.java123
-rw-r--r--android/util/apk/ApkVerityBuilder.java207
-rw-r--r--android/util/proto/ProtoUtils.java22
-rw-r--r--android/view/Choreographer.java6
-rw-r--r--android/view/DisplayCutout.java104
-rw-r--r--android/view/DisplayInfo.java11
-rw-r--r--android/view/IWindowManagerImpl.java26
-rw-r--r--android/view/KeyEvent.java7
-rw-r--r--android/view/MotionEvent.java45
-rw-r--r--android/view/NotificationHeaderView.java46
-rw-r--r--android/view/PointerIcon.java4
-rw-r--r--android/view/RecordingCanvas.java21
-rw-r--r--android/view/RemoteAnimationAdapter.java108
-rw-r--r--android/view/RemoteAnimationDefinition.java93
-rw-r--r--android/view/RemoteAnimationTarget.java161
-rw-r--r--android/view/Surface.java6
-rw-r--r--android/view/SurfaceControl.java25
-rw-r--r--android/view/ThreadedRenderer.java6
-rw-r--r--android/view/View.java673
-rw-r--r--android/view/ViewDebug.java102
-rw-r--r--android/view/ViewGroup.java84
-rw-r--r--android/view/ViewRootImpl.java177
-rw-r--r--android/view/ViewStructure.java12
-rw-r--r--android/view/Window.java57
-rw-r--r--android/view/WindowManager.java386
-rw-r--r--android/view/WindowManagerPolicyConstants.java8
-rw-r--r--android/view/accessibility/AccessibilityEvent.java459
-rw-r--r--android/view/accessibility/AccessibilityNodeInfo.java124
-rw-r--r--android/view/accessibility/AccessibilityRecord.java89
-rw-r--r--android/view/accessibility/AccessibilityViewHierarchyState.java61
-rw-r--r--android/view/accessibility/AccessibilityWindowInfo.java59
-rw-r--r--android/view/accessibility/SendViewScrolledAccessibilityEvent.java58
-rw-r--r--android/view/accessibility/SendWindowContentChangedAccessibilityEvent.java111
-rw-r--r--android/view/accessibility/ThrottlingAccessibilityEventSender.java248
-rw-r--r--android/view/animation/AnimationUtils.java2
-rw-r--r--android/view/animation/ClipRectAnimation.java111
-rw-r--r--android/view/autofill/AutofillManager.java211
-rw-r--r--android/view/autofill/AutofillPopupWindow.java4
-rw-r--r--android/view/inputmethod/ExtractedText.java4
-rw-r--r--android/view/inputmethod/InputConnection.java147
-rw-r--r--android/view/inputmethod/InputConnectionWrapper.java61
-rw-r--r--android/view/inputmethod/InputMethodManager.java44
-rw-r--r--android/view/textclassifier/EntityConfidence.java76
-rw-r--r--android/view/textclassifier/TextClassification.java278
-rw-r--r--android/view/textclassifier/TextClassifier.java39
-rw-r--r--android/view/textclassifier/TextClassifierConstants.java12
-rw-r--r--android/view/textclassifier/TextClassifierImpl.java12
-rw-r--r--android/view/textclassifier/TextLinks.java115
-rw-r--r--android/view/textclassifier/TextSelection.java105
-rw-r--r--android/webkit/FindAddress.java478
-rw-r--r--android/webkit/SafeBrowsingResponse.java35
-rw-r--r--android/webkit/WebViewClient.java540
-rw-r--r--android/webkit/WebViewFactory.java3
-rw-r--r--android/webkit/WebViewFactoryProvider.java6
-rw-r--r--android/widget/AbsListView.java19
-rw-r--r--android/widget/AdapterView.java2
-rw-r--r--android/widget/CheckedTextView.java2
-rw-r--r--android/widget/CompoundButton.java2
-rw-r--r--android/widget/EditText.java4
-rw-r--r--android/widget/Editor.java44
-rw-r--r--android/widget/Magnifier.java41
-rw-r--r--android/widget/MediaControlView2.java279
-rw-r--r--android/widget/SelectionActionModeHelper.java34
-rw-r--r--android/widget/TextView.java314
-rw-r--r--android/widget/VideoView2.java602
-rw-r--r--androidx/app/slice/Slice.java44
-rw-r--r--androidx/app/slice/SliceItem.java42
-rw-r--r--androidx/app/slice/SliceManager.java160
-rw-r--r--androidx/app/slice/SliceManagerCompat.java157
-rw-r--r--androidx/app/slice/SliceManagerTest.java194
-rw-r--r--androidx/app/slice/SliceManagerWrapper.java118
-rw-r--r--androidx/app/slice/SliceProvider.java40
-rw-r--r--androidx/app/slice/SliceSpecs.java51
-rw-r--r--androidx/app/slice/SliceTest.java20
-rw-r--r--androidx/app/slice/builders/GridBuilder.java124
-rw-r--r--androidx/app/slice/builders/ListBuilder.java223
-rw-r--r--androidx/app/slice/builders/MessagingSliceBuilder.java64
-rw-r--r--androidx/app/slice/builders/TemplateSliceBuilder.java105
-rw-r--r--androidx/app/slice/builders/impl/GridBuilder.java96
-rw-r--r--androidx/app/slice/builders/impl/GridBuilderBasicImpl.java138
-rw-r--r--androidx/app/slice/builders/impl/GridBuilderListV1Impl.java163
-rw-r--r--androidx/app/slice/builders/impl/ListBuilder.java136
-rw-r--r--androidx/app/slice/builders/impl/ListBuilderBasicImpl.java212
-rw-r--r--androidx/app/slice/builders/impl/ListBuilderV1Impl.java276
-rw-r--r--androidx/app/slice/builders/impl/MessagingBasicImpl.java120
-rw-r--r--androidx/app/slice/builders/impl/MessagingBuilder.java58
-rw-r--r--androidx/app/slice/builders/impl/MessagingListV1Impl.java109
-rw-r--r--androidx/app/slice/builders/impl/MessagingV1Impl.java97
-rw-r--r--androidx/app/slice/builders/impl/TemplateBuilderImpl.java70
-rw-r--r--androidx/app/slice/compat/CompatPinnedList.java179
-rw-r--r--androidx/app/slice/compat/CompatPinnedListTest.java128
-rw-r--r--androidx/app/slice/compat/SlicePermissionActivity.java94
-rw-r--r--androidx/app/slice/compat/SliceProviderCompat.java243
-rw-r--r--androidx/app/slice/compat/SliceProviderWrapper.java65
-rw-r--r--androidx/app/slice/compat/SliceProviderWrapperContainer.java84
-rw-r--r--androidx/app/slice/core/SliceHints.java16
-rw-r--r--androidx/app/slice/core/SliceQuery.java59
-rw-r--r--androidx/app/slice/widget/ActionRow.java13
-rw-r--r--androidx/app/slice/widget/EventInfo.java268
-rw-r--r--androidx/app/slice/widget/GridContent.java202
-rw-r--r--androidx/app/slice/widget/GridRowView.java240
-rw-r--r--androidx/app/slice/widget/LargeSliceAdapter.java73
-rw-r--r--androidx/app/slice/widget/LargeTemplateView.java80
-rw-r--r--androidx/app/slice/widget/ListContent.java187
-rw-r--r--androidx/app/slice/widget/MessageView.java39
-rw-r--r--androidx/app/slice/widget/RowContent.java246
-rw-r--r--androidx/app/slice/widget/RowView.java457
-rw-r--r--androidx/app/slice/widget/ShortcutView.java73
-rw-r--r--androidx/app/slice/widget/SliceChildView.java98
-rw-r--r--androidx/app/slice/widget/SliceLiveData.java37
-rw-r--r--androidx/app/slice/widget/SliceView.java154
-rw-r--r--androidx/app/slice/widget/SliceViewUtil.java2
-rw-r--r--androidx/browser/browseractions/BrowserActionItem.java75
-rw-r--r--androidx/browser/browseractions/BrowserActionsIntent.java391
-rw-r--r--androidx/car/drawer/CarDrawerActivity.java163
-rw-r--r--androidx/car/moderator/ContentRateLimiter.java240
-rw-r--r--androidx/car/moderator/SpeedBumpView.java200
-rw-r--r--androidx/car/moderator/SystemClockTimeProvider.java31
-rw-r--r--androidx/car/widget/ClickThroughToolbar.java78
-rw-r--r--androidx/car/widget/ListItem.java731
-rw-r--r--androidx/car/widget/ListItemAdapter.java161
-rw-r--r--androidx/car/widget/PagedListView.java207
-rw-r--r--androidx/car/widget/PagedSnapHelper.java20
-rw-r--r--androidx/car/widget/SeekbarListItem.java582
-rw-r--r--androidx/car/widget/TextListItem.java827
-rw-r--r--androidx/recyclerview/selection/ActivationCallbacks.java55
-rw-r--r--androidx/recyclerview/selection/BandPredicate.java131
-rw-r--r--androidx/recyclerview/selection/ContentLock.java98
-rw-r--r--androidx/recyclerview/selection/SelectionHelper.java251
-rw-r--r--androidx/recyclerview/selection/SelectionHelperBuilder.java341
-rw-r--r--androidx/recyclerview/selection/SelectionStorage.java181
-rw-r--r--androidx/textclassifier/EntityConfidence.java152
-rw-r--r--androidx/textclassifier/TextClassification.java598
-rw-r--r--androidx/textclassifier/TextClassifier.java184
-rw-r--r--androidx/textclassifier/TextLinks.java527
-rw-r--r--androidx/textclassifier/TextSelection.java274
-rw-r--r--androidx/widget/recyclerview/selection/AutoScroller.java (renamed from androidx/recyclerview/selection/AutoScroller.java)8
-rw-r--r--androidx/widget/recyclerview/selection/BandPredicate.java137
-rw-r--r--androidx/widget/recyclerview/selection/BandSelectionHelper.java (renamed from androidx/recyclerview/selection/BandSelectionHelper.java)110
-rw-r--r--androidx/widget/recyclerview/selection/DefaultBandHost.java (renamed from androidx/recyclerview/selection/DefaultBandHost.java)66
-rw-r--r--androidx/widget/recyclerview/selection/DefaultSelectionTracker.java (renamed from androidx/recyclerview/selection/DefaultSelectionHelper.java)163
-rw-r--r--androidx/widget/recyclerview/selection/EventBridge.java (renamed from androidx/recyclerview/selection/EventBridge.java)73
-rw-r--r--androidx/widget/recyclerview/selection/FocusDelegate.java (renamed from androidx/recyclerview/selection/FocusCallbacks.java)27
-rw-r--r--androidx/widget/recyclerview/selection/GestureRouter.java (renamed from androidx/recyclerview/selection/GestureRouter.java)25
-rw-r--r--androidx/widget/recyclerview/selection/GestureSelectionHelper.java (renamed from androidx/recyclerview/selection/GestureSelectionHelper.java)88
-rw-r--r--androidx/widget/recyclerview/selection/GridModel.java (renamed from androidx/recyclerview/selection/GridModel.java)38
-rw-r--r--androidx/widget/recyclerview/selection/ItemDetailsLookup.java (renamed from androidx/recyclerview/selection/ItemDetailsLookup.java)73
-rw-r--r--androidx/widget/recyclerview/selection/ItemKeyProvider.java (renamed from androidx/recyclerview/selection/ItemKeyProvider.java)34
-rw-r--r--androidx/widget/recyclerview/selection/MotionEvents.java (renamed from androidx/recyclerview/selection/MotionEvents.java)37
-rw-r--r--androidx/widget/recyclerview/selection/MotionInputHandler.java (renamed from androidx/recyclerview/selection/MotionInputHandler.java)55
-rw-r--r--androidx/widget/recyclerview/selection/MouseInputHandler.java (renamed from androidx/recyclerview/selection/MouseInputHandler.java)100
-rw-r--r--androidx/widget/recyclerview/selection/MutableSelection.java (renamed from androidx/recyclerview/selection/MutableSelection.java)24
-rw-r--r--androidx/widget/recyclerview/selection/OnContextClickListener.java (renamed from androidx/recyclerview/selection/MouseCallbacks.java)28
-rw-r--r--androidx/widget/recyclerview/selection/OnDragInitiatedListener.java (renamed from androidx/recyclerview/selection/TouchCallbacks.java)32
-rw-r--r--androidx/widget/recyclerview/selection/OnItemActivatedListener.java43
-rw-r--r--androidx/widget/recyclerview/selection/OperationMonitor.java130
-rw-r--r--androidx/widget/recyclerview/selection/Range.java (renamed from androidx/recyclerview/selection/Range.java)18
-rw-r--r--androidx/widget/recyclerview/selection/Selection.java (renamed from androidx/recyclerview/selection/Selection.java)80
-rw-r--r--androidx/widget/recyclerview/selection/SelectionPredicates.java (renamed from androidx/recyclerview/selection/SelectionPredicates.java)28
-rw-r--r--androidx/widget/recyclerview/selection/SelectionTracker.java814
-rw-r--r--androidx/widget/recyclerview/selection/Shared.java (renamed from androidx/recyclerview/selection/Shared.java)6
-rw-r--r--androidx/widget/recyclerview/selection/StableIdKeyProvider.java (renamed from androidx/recyclerview/selection/StableIdKeyProvider.java)36
-rw-r--r--androidx/widget/recyclerview/selection/StorageStrategy.java227
-rw-r--r--androidx/widget/recyclerview/selection/ToolHandlerRegistry.java (renamed from androidx/recyclerview/selection/ToolHandlerRegistry.java)13
-rw-r--r--androidx/widget/recyclerview/selection/TouchEventRouter.java (renamed from androidx/recyclerview/selection/TouchEventRouter.java)25
-rw-r--r--androidx/widget/recyclerview/selection/TouchInputHandler.java (renamed from androidx/recyclerview/selection/TouchInputHandler.java)63
-rw-r--r--androidx/widget/recyclerview/selection/ViewAutoScroller.java (renamed from androidx/recyclerview/selection/ViewAutoScroller.java)43
-rw-r--r--benchmarks/regression/CharsetUtf8Benchmark.java69
-rw-r--r--com/android/car/setupwizardlib/CarSetupWizardLayout.java149
-rw-r--r--com/android/car/setupwizardlib/util/CarWizardManagerHelper.java16
-rw-r--r--com/android/commands/bmgr/Bmgr.java23
-rw-r--r--com/android/commands/sm/Sm.java16
-rw-r--r--com/android/commands/svc/UsbCommand.java14
-rw-r--r--com/android/externalstorage/ExternalStorageProvider.java29
-rw-r--r--com/android/ims/ImsCallProfile.java2
-rw-r--r--com/android/ims/ImsConnectionStateListener.java35
-rw-r--r--com/android/ims/ImsManager.java205
-rw-r--r--com/android/ims/ImsReasonInfo.java7
-rw-r--r--com/android/ims/ImsServiceProxy.java122
-rw-r--r--com/android/ims/ImsServiceProxyCompat.java32
-rw-r--r--com/android/internal/app/ChooserActivity.java5
-rw-r--r--com/android/internal/app/HarmfulAppWarningActivity.java99
-rw-r--r--com/android/internal/app/IntentForwarderActivity.java2
-rw-r--r--com/android/internal/app/ResolverActivity.java38
-rw-r--r--com/android/internal/app/ResolverComparator.java4
-rw-r--r--com/android/internal/app/SuggestedLocaleAdapter.java2
-rw-r--r--com/android/internal/app/UnlaunchableAppActivity.java2
-rw-r--r--com/android/internal/app/procstats/ProcessState.java19
-rw-r--r--com/android/internal/app/procstats/ProcessStats.java65
-rw-r--r--com/android/internal/colorextraction/types/Tonal.java25
-rw-r--r--com/android/internal/content/PackageHelper.java4
-rw-r--r--com/android/internal/location/gnssmetrics/GnssMetrics.java86
-rw-r--r--com/android/internal/net/NetworkStatsFactory.java70
-rw-r--r--com/android/internal/os/BatteryStatsHelper.java8
-rw-r--r--com/android/internal/os/BatteryStatsImpl.java1138
-rw-r--r--com/android/internal/os/CpuPowerCalculator.java24
-rw-r--r--com/android/internal/os/KernelCpuSpeedReader.java29
-rw-r--r--com/android/internal/os/KernelSingleUidTimeReader.java28
-rw-r--r--com/android/internal/os/KernelUidCpuActiveTimeReader.java146
-rw-r--r--com/android/internal/os/KernelUidCpuClusterTimeReader.java178
-rw-r--r--com/android/internal/os/PowerProfile.java215
-rw-r--r--com/android/internal/os/WakelockPowerCalculator.java2
-rw-r--r--com/android/internal/os/Zygote.java8
-rw-r--r--com/android/internal/os/ZygoteInit.java21
-rw-r--r--com/android/internal/os/logging/MetricsLoggerWrapper.java99
-rw-r--r--com/android/internal/policy/DecorView.java58
-rw-r--r--com/android/internal/policy/KeyguardDismissCallback.java41
-rw-r--r--com/android/internal/policy/PhoneWindow.java14
-rw-r--r--com/android/internal/print/DualDumpOutputStream.java276
-rw-r--r--com/android/internal/print/DumpUtils.java195
-rw-r--r--com/android/internal/telephony/BaseCommands.java18
-rw-r--r--com/android/internal/telephony/CallFailCause.java99
-rw-r--r--com/android/internal/telephony/CarrierActionAgent.java9
-rw-r--r--com/android/internal/telephony/CarrierIdentifier.java26
-rw-r--r--com/android/internal/telephony/CarrierInfoManager.java52
-rw-r--r--com/android/internal/telephony/CarrierKeyDownloadManager.java31
-rw-r--r--com/android/internal/telephony/CarrierServiceBindHelper.java2
-rw-r--r--com/android/internal/telephony/CommandException.java2
-rw-r--r--com/android/internal/telephony/CommandsInterface.java46
-rw-r--r--com/android/internal/telephony/DefaultPhoneNotifier.java10
-rw-r--r--com/android/internal/telephony/GsmCdmaConnection.java3
-rw-r--r--com/android/internal/telephony/GsmCdmaPhone.java175
-rw-r--r--com/android/internal/telephony/IccCard.java14
-rw-r--r--com/android/internal/telephony/IccCardConstants.java14
-rw-r--r--com/android/internal/telephony/IccSmsInterfaceManager.java41
-rw-r--r--com/android/internal/telephony/ImsSMSDispatcher.java408
-rw-r--r--com/android/internal/telephony/ImsSmsDispatcher.java395
-rw-r--r--com/android/internal/telephony/InboundSmsHandler.java10
-rw-r--r--com/android/internal/telephony/NetworkScanRequestTracker.java14
-rw-r--r--com/android/internal/telephony/NitzData.java48
-rw-r--r--com/android/internal/telephony/NitzStateMachine.java673
-rw-r--r--com/android/internal/telephony/Phone.java13
-rw-r--r--com/android/internal/telephony/PhoneFactory.java8
-rw-r--r--com/android/internal/telephony/PhoneInternalInterface.java5
-rw-r--r--com/android/internal/telephony/PhoneNotifier.java2
-rw-r--r--com/android/internal/telephony/PhoneSubInfoController.java36
-rw-r--r--com/android/internal/telephony/RIL.java271
-rw-r--r--com/android/internal/telephony/RILConstants.java5
-rw-r--r--com/android/internal/telephony/RILRequest.java29
-rw-r--r--com/android/internal/telephony/RadioIndication.java55
-rw-r--r--com/android/internal/telephony/RadioResponse.java113
-rw-r--r--com/android/internal/telephony/SMSDispatcher.java303
-rw-r--r--com/android/internal/telephony/ServiceStateTracker.java275
-rw-r--r--com/android/internal/telephony/SmsDispatchersController.java562
-rw-r--r--com/android/internal/telephony/SubscriptionController.java32
-rw-r--r--com/android/internal/telephony/SubscriptionInfoUpdater.java171
-rw-r--r--com/android/internal/telephony/TelephonyComponentFactory.java18
-rw-r--r--com/android/internal/telephony/TelephonyIntents.java6
-rw-r--r--com/android/internal/telephony/TimeServiceHelper.java14
-rw-r--r--com/android/internal/telephony/TimeZoneLookupHelper.java298
-rw-r--r--com/android/internal/telephony/VisualVoicemailSmsFilter.java11
-rw-r--r--com/android/internal/telephony/cat/CatService.java36
-rw-r--r--com/android/internal/telephony/cdma/CdmaSMSDispatcher.java189
-rw-r--r--com/android/internal/telephony/dataconnection/ApnSetting.java111
-rw-r--r--com/android/internal/telephony/dataconnection/DataConnection.java299
-rw-r--r--com/android/internal/telephony/dataconnection/DcController.java93
-rw-r--r--com/android/internal/telephony/dataconnection/DcTracker.java53
-rw-r--r--com/android/internal/telephony/dataconnection/KeepaliveStatus.java105
-rw-r--r--com/android/internal/telephony/euicc/EuiccCardController.java275
-rw-r--r--com/android/internal/telephony/euicc/EuiccConnector.java41
-rw-r--r--com/android/internal/telephony/euicc/EuiccController.java32
-rw-r--r--com/android/internal/telephony/gsm/GsmSMSDispatcher.java176
-rw-r--r--com/android/internal/telephony/ims/ImsResolver.java25
-rw-r--r--com/android/internal/telephony/ims/ImsServiceController.java16
-rw-r--r--com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java270
-rw-r--r--com/android/internal/telephony/imsphone/ImsPhoneCommandInterface.java10
-rw-r--r--com/android/internal/telephony/metrics/TelephonyEventBuilder.java17
-rw-r--r--com/android/internal/telephony/metrics/TelephonyMetrics.java51
-rw-r--r--com/android/internal/telephony/sip/SipCommandInterface.java10
-rw-r--r--com/android/internal/telephony/test/SimulatedCommands.java37
-rw-r--r--com/android/internal/telephony/test/SimulatedCommandsVerifier.java18
-rw-r--r--com/android/internal/telephony/uicc/IccCardProxy.java114
-rw-r--r--com/android/internal/telephony/uicc/IccCardStatus.java2
-rw-r--r--com/android/internal/telephony/uicc/IccRecords.java129
-rw-r--r--com/android/internal/telephony/uicc/IccSlotStatus.java18
-rw-r--r--com/android/internal/telephony/uicc/IccUtils.java7
-rw-r--r--com/android/internal/telephony/uicc/IsimUiccRecords.java82
-rw-r--r--com/android/internal/telephony/uicc/RuimRecords.java79
-rw-r--r--com/android/internal/telephony/uicc/SIMRecords.java82
-rw-r--r--com/android/internal/telephony/uicc/UiccCard.java673
-rw-r--r--com/android/internal/telephony/uicc/UiccCardApplication.java43
-rw-r--r--com/android/internal/telephony/uicc/UiccCarrierPrivilegeRules.java16
-rw-r--r--com/android/internal/telephony/uicc/UiccController.java349
-rw-r--r--com/android/internal/telephony/uicc/UiccPkcs15.java23
-rw-r--r--com/android/internal/telephony/uicc/UiccProfile.java781
-rw-r--r--com/android/internal/telephony/uicc/UiccSlot.java69
-rw-r--r--com/android/internal/telephony/uicc/UiccStateChangedLauncher.java6
-rw-r--r--com/android/internal/telephony/uicc/euicc/EuiccCard.java1101
-rw-r--r--com/android/internal/telephony/uicc/euicc/EuiccCardErrorException.java123
-rw-r--r--com/android/internal/telephony/uicc/euicc/EuiccCardException.java33
-rw-r--r--com/android/internal/telephony/uicc/euicc/EuiccSpecVersion.java144
-rw-r--r--com/android/internal/telephony/uicc/euicc/Tags.java108
-rw-r--r--com/android/internal/telephony/uicc/euicc/apdu/ApduCommand.java62
-rw-r--r--com/android/internal/telephony/uicc/euicc/apdu/ApduException.java63
-rw-r--r--com/android/internal/telephony/uicc/euicc/apdu/ApduSender.java253
-rw-r--r--com/android/internal/telephony/uicc/euicc/apdu/CloseLogicalChannelInvocation.java61
-rw-r--r--com/android/internal/telephony/uicc/euicc/apdu/OpenLogicalChannelInvocation.java93
-rw-r--r--com/android/internal/telephony/uicc/euicc/apdu/RequestBuilder.java101
-rw-r--r--com/android/internal/telephony/uicc/euicc/apdu/RequestProvider.java35
-rw-r--r--com/android/internal/telephony/uicc/euicc/apdu/TransmitApduLogicalChannelInvocation.java73
-rw-r--r--com/android/internal/telephony/uicc/euicc/async/AsyncMessageInvocation.java72
-rw-r--r--com/android/internal/telephony/util/SMSDispatcherUtil.java199
-rw-r--r--com/android/internal/telephony/util/TimeStampedValue.java73
-rw-r--r--com/android/internal/util/ArrayUtils.java11
-rw-r--r--com/android/internal/util/CollectionUtils.java11
-rw-r--r--com/android/internal/util/ConcurrentUtils.java25
-rw-r--r--com/android/internal/util/DumpUtils.java1
-rw-r--r--com/android/internal/util/ObjectUtils.java12
-rw-r--r--com/android/internal/util/RingBuffer.java23
-rw-r--r--com/android/internal/util/ScreenshotHelper.java139
-rw-r--r--com/android/internal/view/IInputConnectionWrapper.java16
-rw-r--r--com/android/internal/view/InputConnectionWrapper.java9
-rw-r--r--com/android/internal/widget/MessagingGroup.java61
-rw-r--r--com/android/internal/widget/MessagingLayout.java102
-rw-r--r--com/android/internal/widget/RecyclerView.java2
-rw-r--r--com/android/internal/widget/ResolverDrawerLayout.java4
-rw-r--r--com/android/keyguard/CarrierText.java12
-rw-r--r--com/android/keyguard/KeyguardAbsKeyInputView.java6
-rw-r--r--com/android/keyguard/KeyguardHostView.java7
-rw-r--r--com/android/keyguard/KeyguardPasswordView.java2
-rw-r--r--com/android/keyguard/KeyguardPatternView.java2
-rw-r--r--com/android/keyguard/KeyguardPinBasedInputView.java2
-rw-r--r--com/android/keyguard/KeyguardSecurityContainer.java3
-rw-r--r--com/android/keyguard/KeyguardSecurityView.java2
-rw-r--r--com/android/keyguard/KeyguardSecurityViewFlipper.java2
-rw-r--r--com/android/keyguard/KeyguardSimPinView.java2
-rw-r--r--com/android/keyguard/KeyguardSimPukView.java2
-rw-r--r--com/android/keyguard/KeyguardSliceView.java80
-rw-r--r--com/android/keyguard/KeyguardStatusView.java23
-rw-r--r--com/android/keyguard/KeyguardUpdateMonitor.java37
-rw-r--r--com/android/keyguard/KeyguardUpdateMonitorCallback.java14
-rw-r--r--com/android/keyguard/ViewMediatorCallback.java6
-rw-r--r--com/android/layoutlib/bridge/android/BridgePackageManager.java12
-rw-r--r--com/android/layoutlib/bridge/android/BridgePowerManager.java10
-rw-r--r--com/android/layoutlib/bridge/android/BridgeWindowSession.java19
-rw-r--r--com/android/media/MediaBrowser2Impl.java101
-rw-r--r--com/android/media/MediaController2Impl.java601
-rw-r--r--com/android/media/MediaLibraryService2Impl.java91
-rw-r--r--com/android/media/MediaSession2Impl.java482
-rw-r--r--com/android/media/MediaSession2Stub.java380
-rw-r--r--com/android/media/MediaSessionService2Impl.java163
-rw-r--r--com/android/media/PlaybackListenerHolder.java74
-rw-r--r--com/android/media/update/ApiFactory.java132
-rw-r--r--com/android/media/update/ApiHelper.java84
-rw-r--r--com/android/printspooler/model/PrintSpoolerService.java60
-rw-r--r--com/android/printspooler/widget/PrintContentView.java3
-rw-r--r--com/android/printspooler/widget/PrintOptionsLayout.java18
-rw-r--r--com/android/providers/settings/SettingsBackupAgent.java118
-rw-r--r--com/android/providers/settings/SettingsHelper.java18
-rw-r--r--com/android/providers/settings/SettingsProtoDumpUtil.java30
-rw-r--r--com/android/providers/settings/SettingsProvider.java99
-rw-r--r--com/android/server/AlarmManagerService.java219
-rw-r--r--com/android/server/AppOpsService.java65
-rw-r--r--com/android/server/BatteryService.java8
-rw-r--r--com/android/server/BluetoothManagerService.java22
-rw-r--r--com/android/server/ConnectivityService.java636
-rw-r--r--com/android/server/DeviceIdleController.java51
-rw-r--r--com/android/server/DiskStatsService.java47
-rw-r--r--com/android/server/EntropyMixer.java7
-rw-r--r--com/android/server/ForceAppStandbyTracker.java258
-rw-r--r--com/android/server/InputMethodManagerService.java57
-rw-r--r--com/android/server/IpSecService.java871
-rw-r--r--com/android/server/LocationManagerService.java378
-rw-r--r--com/android/server/NetworkManagementService.java93
-rw-r--r--com/android/server/NetworkScoreService.java45
-rw-r--r--com/android/server/PersistentDataBlockManagerInternal.java9
-rw-r--r--com/android/server/PersistentDataBlockService.java24
-rw-r--r--com/android/server/StorageManagerService.java58
-rw-r--r--com/android/server/SystemConfig.java5
-rw-r--r--com/android/server/SystemServer.java24
-rw-r--r--com/android/server/SystemUpdateManagerService.java255
-rw-r--r--com/android/server/TelephonyRegistry.java51
-rw-r--r--com/android/server/VibratorService.java71
-rw-r--r--com/android/server/Watchdog.java1
-rw-r--r--com/android/server/accessibility/AccessibilityManagerService.java163
-rw-r--r--com/android/server/accessibility/GestureUtils.java6
-rw-r--r--com/android/server/accessibility/GlobalActionPerformer.java27
-rw-r--r--com/android/server/accessibility/MagnificationGestureHandler.java118
-rw-r--r--com/android/server/accounts/AccountManagerService.java39
-rw-r--r--com/android/server/am/ActiveInstrumentation.java25
-rw-r--r--com/android/server/am/ActivityDisplay.java89
-rw-r--r--com/android/server/am/ActivityLaunchParamsModifier.java (renamed from com/android/server/am/LaunchingActivityPositioner.java)20
-rw-r--r--com/android/server/am/ActivityManagerConstants.java4
-rw-r--r--com/android/server/am/ActivityManagerService.java1262
-rw-r--r--com/android/server/am/ActivityManagerShellCommand.java20
-rw-r--r--com/android/server/am/ActivityMetricsLogger.java55
-rw-r--r--com/android/server/am/ActivityRecord.java39
-rw-r--r--com/android/server/am/ActivityStack.java57
-rw-r--r--com/android/server/am/ActivityStackSupervisor.java104
-rw-r--r--com/android/server/am/ActivityStartController.java37
-rw-r--r--com/android/server/am/ActivityStartInterceptor.java63
-rw-r--r--com/android/server/am/ActivityStarter.java156
-rw-r--r--com/android/server/am/AppErrorDialog.java31
-rw-r--r--com/android/server/am/AppErrors.java99
-rw-r--r--com/android/server/am/AppTaskImpl.java9
-rw-r--r--com/android/server/am/AppTimeTracker.java22
-rw-r--r--com/android/server/am/BatteryExternalStatsWorker.java45
-rw-r--r--com/android/server/am/BatteryStatsService.java94
-rw-r--r--com/android/server/am/ClientLifecycleManager.java3
-rw-r--r--com/android/server/am/ConnectionRecord.java88
-rw-r--r--com/android/server/am/GlobalSettingsToPropertiesMapper.java115
-rw-r--r--com/android/server/am/KeyguardController.java20
-rw-r--r--com/android/server/am/LaunchParamsController.java256
-rw-r--r--com/android/server/am/LaunchingBoundsController.java167
-rw-r--r--com/android/server/am/LockTaskController.java2
-rw-r--r--com/android/server/am/PendingIntentRecord.java35
-rw-r--r--com/android/server/am/ProcessList.java140
-rw-r--r--com/android/server/am/ProcessRecord.java1
-rw-r--r--com/android/server/am/RecentsAnimation.java159
-rw-r--r--com/android/server/am/SafeActivityOptions.java232
-rw-r--r--com/android/server/am/TaskLaunchParamsModifier.java (renamed from com/android/server/am/LaunchingTaskPositioner.java)31
-rw-r--r--com/android/server/am/TaskRecord.java11
-rw-r--r--com/android/server/am/UidRecord.java48
-rw-r--r--com/android/server/am/UserController.java211
-rw-r--r--com/android/server/am/UserState.java21
-rw-r--r--com/android/server/am/UserSwitchingDialog.java17
-rw-r--r--com/android/server/am/VrController.java24
-rw-r--r--com/android/server/appwidget/AppWidgetServiceImpl.java124
-rw-r--r--com/android/server/audio/AudioService.java243
-rw-r--r--com/android/server/audio/FocusRequester.java45
-rw-r--r--com/android/server/audio/MediaFocusControl.java49
-rw-r--r--com/android/server/audio/PlaybackActivityMonitor.java6
-rw-r--r--com/android/server/audio/PlayerFocusEnforcer.java2
-rw-r--r--com/android/server/autofill/AutofillManagerService.java96
-rw-r--r--com/android/server/autofill/AutofillManagerServiceImpl.java107
-rw-r--r--com/android/server/autofill/AutofillManagerServiceShellCommand.java53
-rw-r--r--com/android/server/autofill/FieldClassificationStrategy.java289
-rw-r--r--com/android/server/autofill/Session.java187
-rw-r--r--com/android/server/autofill/ui/SaveUi.java3
-rw-r--r--com/android/server/backup/BackupManagerConstants.java8
-rw-r--r--com/android/server/backup/BackupManagerConstantsTest.java48
-rw-r--r--com/android/server/backup/BackupManagerServiceInterface.java2
-rw-r--r--com/android/server/backup/BackupManagerServiceTest.java679
-rw-r--r--com/android/server/backup/BackupPolicyEnforcer.java25
-rw-r--r--com/android/server/backup/PackageManagerBackupAgent.java321
-rw-r--r--com/android/server/backup/RefactoredBackupManagerService.java361
-rw-r--r--com/android/server/backup/Trampoline.java10
-rw-r--r--com/android/server/backup/TransportManager.java900
-rw-r--r--com/android/server/backup/TransportManagerTest.java926
-rw-r--r--com/android/server/backup/internal/BackupState.java1
-rw-r--r--com/android/server/backup/internal/PerformBackupTask.java216
-rw-r--r--com/android/server/backup/internal/PerformClearTask.java8
-rw-r--r--com/android/server/backup/internal/PerformInitializeTask.java4
-rw-r--r--com/android/server/backup/internal/PerformInitializeTaskTest.java284
-rw-r--r--com/android/server/backup/restore/FullRestoreEngine.java12
-rw-r--r--com/android/server/backup/restore/PerformAdbRestoreTask.java13
-rw-r--r--com/android/server/backup/restore/PerformUnifiedRestoreTask.java8
-rw-r--r--com/android/server/backup/restore/RestoreInstallObserver.java89
-rw-r--r--com/android/server/backup/testing/ShadowAppBackupUtils.java46
-rw-r--r--com/android/server/backup/testing/ShadowBackupPolicyEnforcer.java24
-rw-r--r--com/android/server/backup/testing/TestUtils.java68
-rw-r--r--com/android/server/backup/testing/TransportBoundListenerStub.java64
-rw-r--r--com/android/server/backup/testing/TransportData.java149
-rw-r--r--com/android/server/backup/testing/TransportReadyCallbackStub.java55
-rw-r--r--com/android/server/backup/testing/TransportTestUtils.java201
-rw-r--r--com/android/server/backup/transport/OnTransportRegisteredListener.java33
-rw-r--r--com/android/server/backup/transport/TransportClient.java111
-rw-r--r--com/android/server/backup/transport/TransportClientManager.java41
-rw-r--r--com/android/server/backup/transport/TransportClientTest.java123
-rw-r--r--com/android/server/backup/transport/TransportNotRegisteredException.java5
-rw-r--r--com/android/server/backup/transport/TransportUtils.java23
-rw-r--r--com/android/server/backup/utils/AppBackupUtils.java42
-rw-r--r--com/android/server/backup/utils/RestoreUtils.java139
-rw-r--r--com/android/server/broadcastradio/BroadcastRadioService.java35
-rw-r--r--com/android/server/broadcastradio/hal1/Tuner.java42
-rw-r--r--com/android/server/broadcastradio/hal1/TunerCallback.java38
-rw-r--r--com/android/server/broadcastradio/hal2/AnnouncementAggregator.java128
-rw-r--r--com/android/server/broadcastradio/hal2/BroadcastRadioService.java46
-rw-r--r--com/android/server/broadcastradio/hal2/Convert.java233
-rw-r--r--com/android/server/broadcastradio/hal2/Mutable.java46
-rw-r--r--com/android/server/broadcastradio/hal2/RadioModule.java70
-rw-r--r--com/android/server/broadcastradio/hal2/TunerCallback.java68
-rw-r--r--com/android/server/broadcastradio/hal2/TunerSession.java272
-rw-r--r--com/android/server/broadcastradio/hal2/Utils.java40
-rw-r--r--com/android/server/connectivity/ConnectivityConstants.java52
-rw-r--r--com/android/server/connectivity/DnsManager.java323
-rw-r--r--com/android/server/connectivity/KeepaliveTracker.java6
-rw-r--r--com/android/server/connectivity/MultipathPolicyTracker.java361
-rw-r--r--com/android/server/connectivity/NetworkAgentInfo.java12
-rw-r--r--com/android/server/connectivity/NetworkDiagnostics.java11
-rw-r--r--com/android/server/connectivity/NetworkMonitor.java133
-rw-r--r--com/android/server/connectivity/PacManager.java12
-rw-r--r--com/android/server/connectivity/Tethering.java75
-rw-r--r--com/android/server/connectivity/Vpn.java192
-rw-r--r--com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java35
-rw-r--r--com/android/server/connectivity/tethering/TetheringConfiguration.java3
-rw-r--r--com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java2
-rw-r--r--com/android/server/content/SyncJobService.java84
-rw-r--r--com/android/server/content/SyncLogger.java7
-rw-r--r--com/android/server/content/SyncManager.java2
-rw-r--r--com/android/server/devicepolicy/BaseIDevicePolicyManager.java83
-rw-r--r--com/android/server/devicepolicy/DevicePolicyManagerService.java1240
-rw-r--r--com/android/server/devicepolicy/Owners.java54
-rw-r--r--com/android/server/devicepolicy/TransferOwnershipMetadataManager.java227
-rw-r--r--com/android/server/display/AutomaticBrightnessController.java90
-rw-r--r--com/android/server/display/BrightnessMappingStrategy.java320
-rw-r--r--com/android/server/display/BrightnessTracker.java232
-rw-r--r--com/android/server/display/ColorFade.java12
-rw-r--r--com/android/server/display/DisplayDevice.java3
-rw-r--r--com/android/server/display/DisplayDeviceInfo.java11
-rw-r--r--com/android/server/display/DisplayManagerService.java85
-rw-r--r--com/android/server/display/DisplayPowerController.java430
-rw-r--r--com/android/server/display/LocalDisplayAdapter.java18
-rw-r--r--com/android/server/display/LogicalDisplay.java2
-rw-r--r--com/android/server/display/PersistentDataStore.java100
-rw-r--r--com/android/server/ethernet/EthernetNetworkFactory.java1
-rw-r--r--com/android/server/fingerprint/AuthenticationClient.java149
-rw-r--r--com/android/server/fingerprint/FingerprintService.java34
-rw-r--r--com/android/server/hdmi/DeviceDiscoveryAction.java23
-rw-r--r--com/android/server/hdmi/HdmiCecLocalDeviceTv.java18
-rw-r--r--com/android/server/hdmi/HdmiControlService.java8
-rw-r--r--com/android/server/hdmi/HdmiUtils.java26
-rw-r--r--com/android/server/hdmi/SystemAudioStatusAction.java4
-rw-r--r--com/android/server/hdmi/VolumeControlAction.java4
-rw-r--r--com/android/server/job/GrantedUriPermissions.java18
-rw-r--r--com/android/server/job/JobPackageTracker.java140
-rw-r--r--com/android/server/job/JobSchedulerInternal.java1
-rw-r--r--com/android/server/job/JobSchedulerService.java392
-rw-r--r--com/android/server/job/JobServiceContext.java24
-rw-r--r--com/android/server/job/JobStore.java29
-rw-r--r--com/android/server/job/controllers/AppIdleController.java34
-rw-r--r--com/android/server/job/controllers/BackgroundJobsController.java76
-rw-r--r--com/android/server/job/controllers/BatteryController.java33
-rw-r--r--com/android/server/job/controllers/ConnectivityController.java151
-rw-r--r--com/android/server/job/controllers/ContentObserverController.java102
-rw-r--r--com/android/server/job/controllers/DeviceIdleJobsController.java37
-rw-r--r--com/android/server/job/controllers/IdleController.java25
-rw-r--r--com/android/server/job/controllers/JobStatus.java358
-rw-r--r--com/android/server/job/controllers/StateController.java3
-rw-r--r--com/android/server/job/controllers/StorageController.java36
-rw-r--r--com/android/server/job/controllers/TimeController.java42
-rw-r--r--com/android/server/location/ContextHubClientManager.java4
-rw-r--r--com/android/server/location/ContextHubServiceUtil.java2
-rw-r--r--com/android/server/location/GnssLocationProvider.java10
-rw-r--r--com/android/server/location/RemoteListenerHelper.java8
-rw-r--r--com/android/server/locksettings/LockSettingsService.java224
-rw-r--r--com/android/server/locksettings/LockSettingsStorage.java11
-rw-r--r--com/android/server/locksettings/SyntheticPasswordManager.java6
-rw-r--r--com/android/server/locksettings/recoverablekeystore/AndroidKeyStoreFactory.java34
-rw-r--r--com/android/server/locksettings/recoverablekeystore/KeySyncTask.java178
-rw-r--r--com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java13
-rw-r--r--com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java127
-rw-r--r--com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java1
-rw-r--r--com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java290
-rw-r--r--com/android/server/locksettings/recoverablekeystore/WrappedKey.java29
-rw-r--r--com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java380
-rw-r--r--com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java18
-rw-r--r--com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java13
-rw-r--r--com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java10
-rw-r--r--com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java24
-rw-r--r--com/android/server/media/MediaSession2Record.java181
-rw-r--r--com/android/server/media/MediaSessionService.java148
-rw-r--r--com/android/server/media/MediaSessionService2Record.java65
-rw-r--r--com/android/server/media/MediaUpdateService.java148
-rw-r--r--com/android/server/net/NetworkIdentitySet.java28
-rw-r--r--com/android/server/net/NetworkPolicyLogger.java10
-rw-r--r--com/android/server/net/NetworkPolicyManagerInternal.java43
-rw-r--r--com/android/server/net/NetworkPolicyManagerService.java378
-rw-r--r--com/android/server/net/NetworkStatsCollection.java4
-rw-r--r--com/android/server/net/NetworkStatsService.java57
-rw-r--r--com/android/server/net/watchlist/NetworkWatchlistService.java76
-rw-r--r--com/android/server/net/watchlist/PrivacyUtils.java103
-rw-r--r--com/android/server/net/watchlist/ReportEncoder.java126
-rw-r--r--com/android/server/net/watchlist/WatchlistConfig.java253
-rw-r--r--com/android/server/net/watchlist/WatchlistLoggingHandler.java135
-rw-r--r--com/android/server/net/watchlist/WatchlistReportDbHelper.java43
-rw-r--r--com/android/server/net/watchlist/WatchlistSettings.java277
-rw-r--r--com/android/server/notification/ManagedServices.java41
-rw-r--r--com/android/server/notification/NotificationComparator.java2
-rw-r--r--com/android/server/notification/NotificationManagerService.java168
-rw-r--r--com/android/server/notification/NotificationRecord.java9
-rw-r--r--com/android/server/notification/RankingHelper.java9
-rw-r--r--com/android/server/notification/ValidateNotificationPeople.java16
-rw-r--r--com/android/server/notification/ZenModeHelper.java21
-rw-r--r--com/android/server/oemlock/OemLockService.java11
-rw-r--r--com/android/server/om/OverlayManagerServiceImpl.java22
-rw-r--r--com/android/server/om/OverlayManagerSettings.java4
-rw-r--r--com/android/server/pm/CrossProfileAppsService.java (renamed from com/android/server/pm/crossprofile/CrossProfileAppsService.java)2
-rw-r--r--com/android/server/pm/CrossProfileAppsServiceImpl.java (renamed from com/android/server/pm/crossprofile/CrossProfileAppsServiceImpl.java)7
-rw-r--r--com/android/server/pm/Installer.java33
-rw-r--r--com/android/server/pm/InstantAppRegistry.java18
-rw-r--r--com/android/server/pm/KeySetManagerService.java6
-rw-r--r--com/android/server/pm/LauncherAppsService.java4
-rw-r--r--com/android/server/pm/OtaDexoptService.java8
-rw-r--r--com/android/server/pm/PackageDexOptimizer.java16
-rw-r--r--com/android/server/pm/PackageInstallerService.java7
-rw-r--r--com/android/server/pm/PackageInstallerSession.java133
-rw-r--r--com/android/server/pm/PackageManagerService.java1217
-rw-r--r--com/android/server/pm/PackageManagerServiceUtils.java146
-rw-r--r--com/android/server/pm/PackageManagerShellCommand.java90
-rw-r--r--com/android/server/pm/PackageSetting.java29
-rw-r--r--com/android/server/pm/PackageSettingBase.java16
-rw-r--r--com/android/server/pm/PackageSignatures.java267
-rw-r--r--com/android/server/pm/SELinuxMMAC.java6
-rw-r--r--com/android/server/pm/Settings.java197
-rw-r--r--com/android/server/pm/SharedUserSetting.java5
-rw-r--r--com/android/server/pm/ShortcutPackage.java3
-rw-r--r--com/android/server/pm/ShortcutService.java2
-rw-r--r--com/android/server/pm/UserManagerService.java151
-rw-r--r--com/android/server/pm/UserRestrictionsUtils.java46
-rw-r--r--com/android/server/pm/dex/ArtManagerService.java71
-rw-r--r--com/android/server/pm/permission/DefaultPermissionGrantPolicy.java66
-rw-r--r--com/android/server/pm/permission/PermissionManagerService.java9
-rw-r--r--com/android/server/policy/BarController.java12
-rw-r--r--com/android/server/policy/PhoneWindowManager.java512
-rw-r--r--com/android/server/policy/WindowManagerPolicy.java38
-rw-r--r--com/android/server/policy/WindowOrientationListener.java44
-rw-r--r--com/android/server/policy/keyguard/KeyguardServiceDelegate.java20
-rw-r--r--com/android/server/policy/keyguard/KeyguardServiceWrapper.java4
-rw-r--r--com/android/server/power/BatterySaverPolicy.java18
-rw-r--r--com/android/server/power/Notifier.java61
-rw-r--r--com/android/server/power/PowerManagerService.java238
-rw-r--r--com/android/server/power/ShutdownThread.java49
-rw-r--r--com/android/server/power/batterysaver/BatterySaverLocationPlugin.java4
-rw-r--r--com/android/server/print/PrintManagerService.java74
-rw-r--r--com/android/server/print/RemotePrintService.java46
-rw-r--r--com/android/server/print/RemotePrintSpooler.java36
-rw-r--r--com/android/server/print/UserState.java198
-rw-r--r--com/android/server/security/VerityUtils.java184
-rw-r--r--com/android/server/slice/PinnedSliceState.java92
-rw-r--r--com/android/server/slice/SliceManagerService.java139
-rw-r--r--com/android/server/stats/StatsCompanionService.java93
-rw-r--r--com/android/server/statusbar/StatusBarManagerInternal.java6
-rw-r--r--com/android/server/statusbar/StatusBarManagerService.java69
-rw-r--r--com/android/server/storage/DeviceStorageMonitorService.java5
-rw-r--r--com/android/server/testing/ShadowEventLog.java71
-rw-r--r--com/android/server/testing/shadows/FrameworkShadowContextImpl.java37
-rw-r--r--com/android/server/testing/shadows/FrameworkShadowPackageManager.java (renamed from com/android/server/backup/testing/ShadowPackageManagerForBackup.java)12
-rw-r--r--com/android/server/timezone/RulesManagerService.java41
-rw-r--r--com/android/server/trust/TrustAgentWrapper.java18
-rw-r--r--com/android/server/trust/TrustManagerService.java22
-rw-r--r--com/android/server/updates/CarrierIdInstallReceiver.java39
-rw-r--r--com/android/server/updates/NetworkWatchlistInstallReceiver.java40
-rw-r--r--com/android/server/usage/AppIdleHistory.java186
-rw-r--r--com/android/server/usage/AppStandbyController.java150
-rw-r--r--com/android/server/usage/IntervalStats.java13
-rw-r--r--com/android/server/usage/StorageStatsService.java10
-rw-r--r--com/android/server/usage/UsageStatsService.java57
-rw-r--r--com/android/server/usage/UsageStatsXmlV1.java8
-rw-r--r--com/android/server/usage/UserUsageStatsService.java5
-rw-r--r--com/android/server/usb/UsbDeviceManager.java170
-rw-r--r--com/android/server/usb/UsbService.java29
-rw-r--r--com/android/server/vr/VrManagerService.java36
-rw-r--r--com/android/server/wallpaper/WallpaperManagerService.java6
-rw-r--r--com/android/server/wifi/AvailableNetworkNotifier.java541
-rw-r--r--com/android/server/wifi/BaseWifiDiagnostics.java6
-rw-r--r--com/android/server/wifi/CarrierNetworkNotifier.java74
-rw-r--r--com/android/server/wifi/ConnectToNetworkNotificationBuilder.java65
-rw-r--r--com/android/server/wifi/DeletedEphemeralSsidsStoreData.java4
-rw-r--r--com/android/server/wifi/HalDeviceManager.java13
-rw-r--r--com/android/server/wifi/HostapdHal.java465
-rw-r--r--com/android/server/wifi/NetworkListStoreData.java4
-rw-r--r--com/android/server/wifi/OpenNetworkNotifier.java469
-rw-r--r--com/android/server/wifi/OpenNetworkRecommender.java58
-rw-r--r--com/android/server/wifi/PropertyService.java5
-rw-r--r--com/android/server/wifi/RttService.java4
-rw-r--r--com/android/server/wifi/ScanDetailCache.java10
-rw-r--r--com/android/server/wifi/ScanOnlyModeManager.java228
-rw-r--r--com/android/server/wifi/ScanRequestProxy.java222
-rw-r--r--com/android/server/wifi/SoftApManager.java217
-rw-r--r--com/android/server/wifi/SsidSetStoreData.java4
-rw-r--r--com/android/server/wifi/SupplicantStaIfaceHal.java15
-rw-r--r--com/android/server/wifi/SupplicantStaNetworkHal.java13
-rw-r--r--com/android/server/wifi/SystemPropertyService.java5
-rw-r--r--com/android/server/wifi/WakeupConfigStoreData.java82
-rw-r--r--com/android/server/wifi/WakeupController.java97
-rw-r--r--com/android/server/wifi/WakeupEvaluator.java89
-rw-r--r--com/android/server/wifi/WakeupNotificationFactory.java83
-rw-r--r--com/android/server/wifi/WakeupOnboarding.java170
-rw-r--r--com/android/server/wifi/WifiApConfigStore.java3
-rw-r--r--com/android/server/wifi/WifiBackupDataParser.java45
-rw-r--r--com/android/server/wifi/WifiBackupDataV1Parser.java533
-rw-r--r--com/android/server/wifi/WifiBackupRestore.java161
-rw-r--r--com/android/server/wifi/WifiConfigManager.java64
-rw-r--r--com/android/server/wifi/WifiConfigStore.java43
-rw-r--r--com/android/server/wifi/WifiConfigStoreLegacy.java355
-rw-r--r--com/android/server/wifi/WifiConfigurationUtil.java4
-rw-r--r--com/android/server/wifi/WifiConnectivityHelper.java8
-rw-r--r--com/android/server/wifi/WifiConnectivityManager.java35
-rw-r--r--com/android/server/wifi/WifiController.java380
-rw-r--r--com/android/server/wifi/WifiCountryCode.java2
-rw-r--r--com/android/server/wifi/WifiDiagnostics.java26
-rw-r--r--com/android/server/wifi/WifiInjector.java101
-rw-r--r--com/android/server/wifi/WifiLockManager.java18
-rw-r--r--com/android/server/wifi/WifiMetrics.java47
-rw-r--r--com/android/server/wifi/WifiMonitor.java44
-rw-r--r--com/android/server/wifi/WifiNative.java1196
-rw-r--r--com/android/server/wifi/WifiNetworkHistory.java630
-rw-r--r--com/android/server/wifi/WifiNetworkSelector.java30
-rw-r--r--com/android/server/wifi/WifiServiceImpl.java403
-rw-r--r--com/android/server/wifi/WifiStateMachine.java918
-rw-r--r--com/android/server/wifi/WifiStateMachinePrime.java146
-rw-r--r--com/android/server/wifi/WifiTrafficPoller.java35
-rw-r--r--com/android/server/wifi/WifiVendorHal.java86
-rw-r--r--com/android/server/wifi/WificondControl.java116
-rw-r--r--com/android/server/wifi/aware/WifiAwareDataPathStateManager.java44
-rw-r--r--com/android/server/wifi/aware/WifiAwareNativeApi.java213
-rw-r--r--com/android/server/wifi/aware/WifiAwareNativeCallback.java48
-rw-r--r--com/android/server/wifi/aware/WifiAwareNativeManager.java19
-rw-r--r--com/android/server/wifi/aware/WifiAwareStateManager.java30
-rw-r--r--com/android/server/wifi/hotspot2/LegacyPasspointConfigParser.java513
-rw-r--r--com/android/server/wifi/hotspot2/PasspointConfigStoreData.java4
-rw-r--r--com/android/server/wifi/hotspot2/PasspointEventHandler.java4
-rw-r--r--com/android/server/wifi/rtt/RttNative.java31
-rw-r--r--com/android/server/wifi/rtt/RttServiceImpl.java127
-rw-r--r--com/android/server/wifi/scanner/HalWifiScannerImpl.java24
-rw-r--r--com/android/server/wifi/scanner/WifiScannerImpl.java13
-rw-r--r--com/android/server/wifi/scanner/WifiScanningServiceImpl.java145
-rw-r--r--com/android/server/wifi/scanner/WificondScannerImpl.java44
-rw-r--r--com/android/server/wifi/util/ApConfigUtil.java9
-rw-r--r--com/android/server/wifi/util/TelephonyUtil.java109
-rw-r--r--com/android/server/wifi/util/WifiPermissionsUtil.java18
-rw-r--r--com/android/server/wifi/util/XmlUtil.java2
-rw-r--r--com/android/server/wifi/wificond/NativeScanResult.java5
-rw-r--r--com/android/server/wifi/wificond/RadioChainInfo.java105
-rw-r--r--com/android/server/wifi/wificond/SingleScanSettings.java22
-rw-r--r--com/android/server/wm/AccessibilityController.java12
-rw-r--r--com/android/server/wm/AppTransition.java216
-rw-r--r--com/android/server/wm/AppWindowContainerController.java20
-rw-r--r--com/android/server/wm/AppWindowThumbnail.java36
-rw-r--r--com/android/server/wm/AppWindowToken.java260
-rw-r--r--com/android/server/wm/DisplayContent.java202
-rw-r--r--com/android/server/wm/DisplayFrames.java78
-rw-r--r--com/android/server/wm/DisplayWindowController.java85
-rw-r--r--com/android/server/wm/DockedStackDividerController.java9
-rw-r--r--com/android/server/wm/DragDropController.java150
-rw-r--r--com/android/server/wm/DragState.java27
-rw-r--r--com/android/server/wm/InputMonitor.java28
-rw-r--r--com/android/server/wm/Letterbox.java145
-rw-r--r--com/android/server/wm/PinnedStackController.java75
-rw-r--r--com/android/server/wm/PinnedStackWindowController.java4
-rw-r--r--com/android/server/wm/RecentsAnimationController.java385
-rw-r--r--com/android/server/wm/RemoteAnimationController.java219
-rw-r--r--com/android/server/wm/RemoteSurfaceTrace.java4
-rw-r--r--com/android/server/wm/RootWindowContainer.java12
-rw-r--r--com/android/server/wm/Session.java30
-rw-r--r--com/android/server/wm/SurfaceAnimationRunner.java35
-rw-r--r--com/android/server/wm/SurfaceAnimator.java85
-rw-r--r--com/android/server/wm/SurfaceControlWithBackground.java334
-rw-r--r--com/android/server/wm/TapExcludeRegionHolder.java56
-rw-r--r--com/android/server/wm/Task.java3
-rw-r--r--com/android/server/wm/TaskPositioner.java21
-rw-r--r--com/android/server/wm/TaskPositioningController.java2
-rw-r--r--com/android/server/wm/TaskStack.java44
-rw-r--r--com/android/server/wm/WallpaperController.java33
-rw-r--r--com/android/server/wm/WindowAnimationSpec.java9
-rw-r--r--com/android/server/wm/WindowAnimator.java38
-rw-r--r--com/android/server/wm/WindowContainer.java27
-rw-r--r--com/android/server/wm/WindowManagerService.java134
-rw-r--r--com/android/server/wm/WindowManagerShellCommand.java2
-rw-r--r--com/android/server/wm/WindowState.java195
-rw-r--r--com/android/server/wm/WindowStateAnimator.java7
-rw-r--r--com/android/server/wm/WindowSurfaceController.java10
-rw-r--r--com/android/server/wm/WindowSurfacePlacer.java246
-rw-r--r--com/android/server/wm/WindowToken.java14
-rw-r--r--com/android/server/wm/WindowTracing.java13
-rw-r--r--com/android/server/wm/utils/CoordinateTransforms.java62
-rw-r--r--com/android/settingslib/Utils.java21
-rw-r--r--com/android/settingslib/bluetooth/A2dpProfile.java32
-rw-r--r--com/android/settingslib/bluetooth/BluetoothCallback.java1
-rw-r--r--com/android/settingslib/bluetooth/BluetoothEventManager.java41
-rw-r--r--com/android/settingslib/bluetooth/CachedBluetoothDevice.java83
-rw-r--r--com/android/settingslib/bluetooth/HeadsetProfile.java10
-rw-r--r--com/android/settingslib/bluetooth/LocalBluetoothAdapter.java4
-rw-r--r--com/android/settingslib/core/AbstractPreferenceController.java8
-rw-r--r--com/android/settingslib/core/instrumentation/EventLogWriter.java110
-rw-r--r--com/android/settingslib/core/instrumentation/Instrumentable.java (renamed from android/support/design/widget/ShadowViewDelegate.java)18
-rw-r--r--com/android/settingslib/core/instrumentation/LogWriter.java84
-rw-r--r--com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java159
-rw-r--r--com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java259
-rw-r--r--com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java96
-rw-r--r--com/android/settingslib/notification/ZenRadioLayout.java (renamed from com/android/systemui/volume/ZenRadioLayout.java)6
-rw-r--r--com/android/settingslib/wifi/WifiStatusTracker.java5
-rw-r--r--com/android/settingslib/wrapper/LocationManagerWrapper.java64
-rw-r--r--com/android/setupwizardlib/GlifLayout.java23
-rw-r--r--com/android/setupwizardlib/GlifLayoutTest.java41
-rw-r--r--com/android/setupwizardlib/GlifRecyclerLayout.java4
-rw-r--r--com/android/setupwizardlib/SetupWizardRecyclerLayout.java4
-rw-r--r--com/android/setupwizardlib/TemplateLayout.java2
-rw-r--r--com/android/setupwizardlib/items/ButtonBarItem.java1
-rw-r--r--com/android/setupwizardlib/test/GlifPatternDrawableTest.java2
-rw-r--r--com/android/shell/BugreportProgressService.java13
-rw-r--r--com/android/support/mediarouter/app/MediaRouteActionProvider.java333
-rw-r--r--com/android/support/mediarouter/app/MediaRouteButton.java620
-rw-r--r--com/android/support/mediarouter/app/MediaRouteChooserDialog.java392
-rw-r--r--com/android/support/mediarouter/app/MediaRouteChooserDialogFragment.java126
-rw-r--r--com/android/support/mediarouter/app/MediaRouteControllerDialog.java1481
-rw-r--r--com/android/support/mediarouter/app/MediaRouteControllerDialogFragment.java76
-rw-r--r--com/android/support/mediarouter/app/MediaRouteDialogFactory.java74
-rw-r--r--com/android/support/mediarouter/app/MediaRouteDialogHelper.java152
-rw-r--r--com/android/support/mediarouter/app/MediaRouteDiscoveryFragment.java164
-rw-r--r--com/android/support/mediarouter/app/MediaRouteExpandCollapseButton.java93
-rw-r--r--com/android/support/mediarouter/app/MediaRouteVolumeSlider.java99
-rw-r--r--com/android/support/mediarouter/app/MediaRouterThemeHelper.java216
-rw-r--r--com/android/support/mediarouter/app/OverlayListView.java265
-rw-r--r--com/android/support/mediarouter/media/MediaControlIntent.java1228
-rw-r--r--com/android/support/mediarouter/media/MediaItemMetadata.java138
-rw-r--r--com/android/support/mediarouter/media/MediaItemStatus.java392
-rw-r--r--com/android/support/mediarouter/media/MediaRouteDescriptor.java693
-rw-r--r--com/android/support/mediarouter/media/MediaRouteDiscoveryRequest.java132
-rw-r--r--com/android/support/mediarouter/media/MediaRouteProvider.java447
-rw-r--r--com/android/support/mediarouter/media/MediaRouteProviderDescriptor.java208
-rw-r--r--com/android/support/mediarouter/media/MediaRouteProviderProtocol.java230
-rw-r--r--com/android/support/mediarouter/media/MediaRouteProviderService.java759
-rw-r--r--com/android/support/mediarouter/media/MediaRouteSelector.java308
-rw-r--r--com/android/support/mediarouter/media/MediaRouter.java2999
-rw-r--r--com/android/support/mediarouter/media/MediaRouterApi24.java26
-rw-r--r--com/android/support/mediarouter/media/MediaRouterJellybean.java462
-rw-r--r--com/android/support/mediarouter/media/MediaRouterJellybeanMr1.java185
-rw-r--r--com/android/support/mediarouter/media/MediaRouterJellybeanMr2.java45
-rw-r--r--com/android/support/mediarouter/media/MediaSessionStatus.java244
-rw-r--r--com/android/support/mediarouter/media/RegisteredMediaRouteProvider.java741
-rw-r--r--com/android/support/mediarouter/media/RegisteredMediaRouteProviderWatcher.java157
-rw-r--r--com/android/support/mediarouter/media/RemoteControlClientCompat.java190
-rw-r--r--com/android/support/mediarouter/media/RemotePlaybackClient.java1044
-rw-r--r--com/android/support/mediarouter/media/SystemMediaRouteProvider.java883
-rw-r--r--com/android/systemui/BatteryMeterView.java1
-rw-r--r--com/android/systemui/ChargingView.java126
-rw-r--r--com/android/systemui/Dependency.java4
-rw-r--r--com/android/systemui/EmulatedDisplayCutout.java51
-rw-r--r--com/android/systemui/HardwareUiLayout.java13
-rw-r--r--com/android/systemui/ImageWallpaper.java3
-rw-r--r--com/android/systemui/OverviewProxyService.java32
-rw-r--r--com/android/systemui/Prefs.java4
-rw-r--r--com/android/systemui/RecentsComponent.java2
-rw-r--r--com/android/systemui/RoundedCorners.java3
-rw-r--r--com/android/systemui/SlicePermissionActivity.java88
-rw-r--r--com/android/systemui/SwipeHelper.java16
-rw-r--r--com/android/systemui/analytics/DataCollector.java4
-rw-r--r--com/android/systemui/charging/WirelessChargingAnimation.java213
-rw-r--r--com/android/systemui/charging/WirelessChargingLayout.java81
-rw-r--r--com/android/systemui/charging/WirelessChargingView.java163
-rw-r--r--com/android/systemui/chooser/ChooserActivity.java41
-rw-r--r--com/android/systemui/chooser/ChooserHelper.java45
-rw-r--r--com/android/systemui/classifier/FalsingManager.java3
-rw-r--r--com/android/systemui/doze/DozeFactory.java2
-rw-r--r--com/android/systemui/doze/DozeUi.java7
-rw-r--r--com/android/systemui/doze/DozeWallpaperState.java38
-rw-r--r--com/android/systemui/fingerprint/FingerprintDialogImpl.java220
-rw-r--r--com/android/systemui/fingerprint/FingerprintDialogView.java233
-rw-r--r--com/android/systemui/globalactions/GlobalActionsComponent.java8
-rw-r--r--com/android/systemui/globalactions/GlobalActionsDialog.java65
-rw-r--r--com/android/systemui/keyboard/KeyboardUI.java3
-rw-r--r--com/android/systemui/keyguard/KeyguardService.java4
-rw-r--r--com/android/systemui/keyguard/KeyguardSliceProvider.java7
-rw-r--r--com/android/systemui/keyguard/KeyguardViewMediator.java44
-rw-r--r--com/android/systemui/pip/phone/PipAppOpsListener.java89
-rw-r--r--com/android/systemui/pip/phone/PipManager.java13
-rw-r--r--com/android/systemui/pip/phone/PipMenuActivity.java2
-rw-r--r--com/android/systemui/pip/phone/PipMenuActivityController.java2
-rw-r--r--com/android/systemui/pip/phone/PipTouchHandler.java21
-rw-r--r--com/android/systemui/plugins/qs/QSTile.java4
-rw-r--r--com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java2
-rw-r--r--com/android/systemui/plugins/statusbar/phone/NavGesture.java7
-rw-r--r--com/android/systemui/power/EnhancedEstimates.java26
-rw-r--r--com/android/systemui/power/EnhancedEstimatesImpl.java26
-rw-r--r--com/android/systemui/power/Estimate.java11
-rw-r--r--com/android/systemui/power/PowerNotificationWarnings.java91
-rw-r--r--com/android/systemui/power/PowerUI.java98
-rw-r--r--com/android/systemui/qs/QSContainerImpl.java49
-rw-r--r--com/android/systemui/qs/QSFooterImpl.java14
-rw-r--r--com/android/systemui/qs/QuickStatusBarHeader.java59
-rw-r--r--com/android/systemui/qs/SignalTileView.java10
-rw-r--r--com/android/systemui/qs/car/CarQSFooter.java14
-rw-r--r--com/android/systemui/qs/car/CarQSFragment.java157
-rw-r--r--com/android/systemui/qs/tileimpl/QSIconViewImpl.java2
-rw-r--r--com/android/systemui/qs/tileimpl/QSTileBaseView.java60
-rw-r--r--com/android/systemui/qs/tileimpl/QSTileImpl.java6
-rw-r--r--com/android/systemui/qs/tileimpl/QSTileView.java25
-rw-r--r--com/android/systemui/qs/tiles/AirplaneModeTile.java2
-rw-r--r--com/android/systemui/qs/tiles/BluetoothTile.java91
-rw-r--r--com/android/systemui/qs/tiles/DndTile.java119
-rw-r--r--com/android/systemui/qs/tiles/HotspotTile.java54
-rw-r--r--com/android/systemui/qs/tiles/LocationTile.java3
-rw-r--r--com/android/systemui/qs/tiles/NightDisplayTile.java49
-rw-r--r--com/android/systemui/qs/tiles/RotationLockTile.java40
-rw-r--r--com/android/systemui/qs/tiles/WorkModeTile.java5
-rw-r--r--com/android/systemui/recents/Recents.java7
-rw-r--r--com/android/systemui/recents/RecentsActivity.java20
-rw-r--r--com/android/systemui/recents/RecentsImpl.java18
-rw-r--r--com/android/systemui/recents/RecentsImplProxy.java9
-rw-r--r--com/android/systemui/recents/SwipeUpOnboarding.java252
-rw-r--r--com/android/systemui/recents/misc/SystemServicesProxy.java17
-rw-r--r--com/android/systemui/recents/views/TaskStackView.java15
-rw-r--r--com/android/systemui/screenshot/GlobalScreenshot.java44
-rw-r--r--com/android/systemui/screenshot/TakeScreenshotService.java2
-rw-r--r--com/android/systemui/settings/BrightnessController.java83
-rw-r--r--com/android/systemui/settings/ToggleSlider.java1
-rw-r--r--com/android/systemui/settings/ToggleSliderView.java5
-rw-r--r--com/android/systemui/shared/recents/utilities/Utilities.java35
-rw-r--r--com/android/systemui/shared/system/ActivityCompat.java34
-rw-r--r--com/android/systemui/shared/system/ActivityManagerWrapper.java37
-rw-r--r--com/android/systemui/shared/system/ActivityOptionsCompat.java5
-rw-r--r--com/android/systemui/shared/system/AssistDataReceiver.java (renamed from com/android/systemui/shared/system/AssistDataReceiverCompat.java)6
-rw-r--r--com/android/systemui/shared/system/InputConsumerController.java (renamed from com/android/systemui/pip/phone/InputConsumerController.java)51
-rw-r--r--com/android/systemui/shared/system/RecentsAnimationControllerCompat.java61
-rw-r--r--com/android/systemui/shared/system/RecentsAnimationListener.java31
-rw-r--r--com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java71
-rw-r--r--com/android/systemui/shared/system/RemoteAnimationDefinitionCompat.java35
-rw-r--r--com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java (renamed from android/arch/lifecycle/LifecycleFragment.java)18
-rw-r--r--com/android/systemui/shared/system/RemoteAnimationTargetCompat.java59
-rw-r--r--com/android/systemui/shared/system/SurfaceControlCompat.java27
-rw-r--r--com/android/systemui/shared/system/TransactionCompat.java108
-rw-r--r--com/android/systemui/shared/system/WindowManagerWrapper.java38
-rw-r--r--com/android/systemui/statusbar/ActivatableNotificationView.java7
-rw-r--r--com/android/systemui/statusbar/CommandQueue.java114
-rw-r--r--com/android/systemui/statusbar/ExpandableNotificationRow.java43
-rw-r--r--com/android/systemui/statusbar/ExpandableOutlineView.java61
-rw-r--r--com/android/systemui/statusbar/ExpandableView.java8
-rw-r--r--com/android/systemui/statusbar/KeyguardIndicationController.java49
-rw-r--r--com/android/systemui/statusbar/NotificationBackgroundView.java9
-rw-r--r--com/android/systemui/statusbar/NotificationContentView.java96
-rw-r--r--com/android/systemui/statusbar/NotificationData.java15
-rw-r--r--com/android/systemui/statusbar/NotificationEntryManager.java27
-rw-r--r--com/android/systemui/statusbar/NotificationGuts.java30
-rw-r--r--com/android/systemui/statusbar/NotificationGutsManager.java42
-rw-r--r--com/android/systemui/statusbar/NotificationHeaderUtil.java1
-rw-r--r--com/android/systemui/statusbar/NotificationInfo.java363
-rw-r--r--com/android/systemui/statusbar/NotificationMenuRow.java6
-rw-r--r--com/android/systemui/statusbar/car/CarNavigationBarView.java68
-rw-r--r--com/android/systemui/statusbar/car/FullscreenUserSwitcher.java2
-rw-r--r--com/android/systemui/statusbar/car/UserGridView.java208
-rw-r--r--com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java20
-rw-r--r--com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java14
-rw-r--r--com/android/systemui/statusbar/notification/NotificationMediaTemplateViewWrapper.java2
-rw-r--r--com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java8
-rw-r--r--com/android/systemui/statusbar/notification/NotificationViewWrapper.java2
-rw-r--r--com/android/systemui/statusbar/phone/AutoTileManager.java2
-rw-r--r--com/android/systemui/statusbar/phone/ButtonDispatcher.java38
-rw-r--r--com/android/systemui/statusbar/phone/DozeParameters.java15
-rw-r--r--com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java7
-rw-r--r--com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java60
-rw-r--r--com/android/systemui/statusbar/phone/KeyguardBouncer.java4
-rw-r--r--com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java42
-rw-r--r--com/android/systemui/statusbar/phone/LockIcon.java2
-rw-r--r--com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java2
-rw-r--r--com/android/systemui/statusbar/phone/NavigationBarFragment.java224
-rw-r--r--com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java87
-rw-r--r--com/android/systemui/statusbar/phone/NavigationBarInflaterView.java5
-rw-r--r--com/android/systemui/statusbar/phone/NavigationBarTransitions.java32
-rw-r--r--com/android/systemui/statusbar/phone/NavigationBarView.java92
-rw-r--r--com/android/systemui/statusbar/phone/NotificationPanelView.java21
-rw-r--r--com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java24
-rw-r--r--com/android/systemui/statusbar/phone/PhoneStatusBarView.java133
-rw-r--r--com/android/systemui/statusbar/phone/QuickScrubController.java402
-rw-r--r--com/android/systemui/statusbar/phone/ScrimController.java8
-rw-r--r--com/android/systemui/statusbar/phone/SettingsButton.java4
-rw-r--r--com/android/systemui/statusbar/phone/StatusBar.java68
-rw-r--r--com/android/systemui/statusbar/phone/StatusBarIconController.java2
-rw-r--r--com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java13
-rw-r--r--com/android/systemui/statusbar/phone/StatusIconContainer.java169
-rw-r--r--com/android/systemui/statusbar/policy/BluetoothControllerImpl.java3
-rw-r--r--com/android/systemui/statusbar/policy/DataSaverControllerImpl.java16
-rw-r--r--com/android/systemui/statusbar/policy/HotspotController.java6
-rw-r--r--com/android/systemui/statusbar/policy/HotspotControllerImpl.java99
-rw-r--r--com/android/systemui/statusbar/policy/KeyButtonDrawable.java2
-rw-r--r--com/android/systemui/statusbar/policy/KeyButtonRipple.java56
-rw-r--r--com/android/systemui/statusbar/policy/KeyButtonView.java6
-rw-r--r--com/android/systemui/statusbar/policy/LocationControllerImpl.java31
-rw-r--r--com/android/systemui/statusbar/policy/RemoteInputView.java5
-rw-r--r--com/android/systemui/statusbar/policy/SmartReplyView.java64
-rw-r--r--com/android/systemui/statusbar/policy/TintedKeyButtonDrawable.java57
-rw-r--r--com/android/systemui/statusbar/stack/AmbientState.java2
-rw-r--r--com/android/systemui/statusbar/stack/NotificationChildrenContainer.java5
-rw-r--r--com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java228
-rw-r--r--com/android/systemui/statusbar/stack/StackScrollAlgorithm.java11
-rw-r--r--com/android/systemui/tuner/TunerServiceImpl.java5
-rw-r--r--com/android/systemui/usb/UsbConfirmActivity.java1
-rw-r--r--com/android/systemui/usb/UsbPermissionActivity.java1
-rw-r--r--com/android/systemui/util/NotificationChannels.java50
-rw-r--r--com/android/systemui/volume/MediaRouterWrapper.java51
-rw-r--r--com/android/systemui/volume/OutputChooserDialog.java197
-rw-r--r--com/android/systemui/volume/OutputChooserLayout.java28
-rw-r--r--com/android/systemui/volume/VolumeDialogComponent.java2
-rw-r--r--com/android/systemui/volume/VolumeDialogImpl.java138
-rw-r--r--com/android/systemui/volume/VolumeUiLayout.java275
-rw-r--r--com/android/uiautomator/testrunner/UiAutomatorTestCase.java106
-rw-r--r--com/android/widget/MediaControlView2Impl.java904
-rw-r--r--com/android/widget/SubtitleView.java142
-rw-r--r--com/android/widget/VideoSurfaceView.java198
-rw-r--r--com/android/widget/VideoTextureView.java210
-rw-r--r--com/android/widget/VideoView2Impl.java1022
-rw-r--r--com/android/widget/VideoViewInterface.java65
-rw-r--r--foo/bar/ComplexDatabase.java6
-rw-r--r--foo/bar/UpdateDao.java11
-rw-r--r--java/lang/Daemons.java20
-rw-r--r--java/lang/Math.java50
-rw-r--r--java/lang/Runtime.java44
-rw-r--r--java/lang/StringFactory.java179
-rw-r--r--java/lang/Thread.java2
-rw-r--r--java/lang/invoke/CallSite.java350
-rw-r--r--java/lang/invoke/MethodHandle.java1347
-rw-r--r--java/lang/invoke/MethodHandles.java3430
-rw-r--r--java/lang/invoke/MethodType.java1205
-rw-r--r--java/lang/invoke/VarHandle.java13
-rw-r--r--java/net/URI.java12
-rw-r--r--java/nio/charset/CharsetDecoderICU.java14
-rw-r--r--java/nio/charset/CharsetEncoderICU.java14
-rw-r--r--java/security/AlgorithmParameters.java4
-rw-r--r--java/text/DateFormat.java12
-rw-r--r--java/text/SimpleDateFormat.java67
-rw-r--r--java/time/zone/IcuZoneRulesProvider.java13
-rw-r--r--java/util/TreeMap.java30
-rw-r--r--javax/crypto/Cipher.java13
-rw-r--r--javax/crypto/KeyGenerator.java4
1675 files changed, 140755 insertions, 59200 deletions
diff --git a/android/accessibilityservice/AccessibilityService.java b/android/accessibilityservice/AccessibilityService.java
index 97dcb90b..0a4541ba 100644
--- a/android/accessibilityservice/AccessibilityService.java
+++ b/android/accessibilityservice/AccessibilityService.java
@@ -363,6 +363,11 @@ public abstract class AccessibilityService extends Service {
*/
public static final int GLOBAL_ACTION_LOCK_SCREEN = 8;
+ /**
+ * Action to take a screenshot
+ */
+ public static final int GLOBAL_ACTION_TAKE_SCREENSHOT = 9;
+
private static final String LOG_TAG = "AccessibilityService";
/**
diff --git a/android/annotation/SystemApi.java b/android/annotation/SystemApi.java
index 55028ebf..e96ff01d 100644
--- a/android/annotation/SystemApi.java
+++ b/android/annotation/SystemApi.java
@@ -39,6 +39,6 @@ import java.lang.annotation.Target;
* @hide
*/
@Target({TYPE, FIELD, METHOD, CONSTRUCTOR, ANNOTATION_TYPE, PACKAGE})
-@Retention(RetentionPolicy.SOURCE)
+@Retention(RetentionPolicy.RUNTIME)
public @interface SystemApi {
}
diff --git a/android/app/Activity.java b/android/app/Activity.java
index aa099eb1..cd029c06 100644
--- a/android/app/Activity.java
+++ b/android/app/Activity.java
@@ -16,6 +16,8 @@
package android.app;
+import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
+
import static java.lang.Character.MIN_VALUE;
import android.annotation.CallSuper;
@@ -98,6 +100,7 @@ import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.MotionEvent;
+import android.view.RemoteAnimationDefinition;
import android.view.SearchEvent;
import android.view.View;
import android.view.View.OnCreateContextMenuListener;
@@ -857,6 +860,7 @@ public class Activity extends ContextThemeWrapper
private boolean mHasCurrentPermissionsRequest;
private boolean mAutoFillResetNeeded;
+ private boolean mAutoFillIgnoreFirstResumePause;
/** The last autofill id that was returned from {@link #getNextAutofillId()} */
private int mLastAutofillId = View.LAST_APP_AUTOFILL_ID;
@@ -1253,10 +1257,7 @@ public class Activity extends ContextThemeWrapper
getApplication().dispatchActivityStarted(this);
if (mAutoFillResetNeeded) {
- AutofillManager afm = getAutofillManager();
- if (afm != null) {
- afm.onVisibleForAutofill();
- }
+ getAutofillManager().onVisibleForAutofill();
}
}
@@ -1320,6 +1321,20 @@ public class Activity extends ContextThemeWrapper
if (DEBUG_LIFECYCLE) Slog.v(TAG, "onResume " + this);
getApplication().dispatchActivityResumed(this);
mActivityTransitionState.onResume(this, isTopOfTask());
+ if (mAutoFillResetNeeded) {
+ if (!mAutoFillIgnoreFirstResumePause) {
+ View focus = getCurrentFocus();
+ if (focus != null && focus.canNotifyAutofillEnterExitEvent()) {
+ // TODO: in Activity killed/recreated case, i.e. SessionLifecycleTest#
+ // testDatasetVisibleWhileAutofilledAppIsLifecycled: the View's initial
+ // window visibility after recreation is INVISIBLE in onResume() and next frame
+ // ViewRootImpl.performTraversals() changes window visibility to VISIBLE.
+ // So we cannot call View.notifyEnterOrExited() which will do nothing
+ // when View.isVisibleToUser() is false.
+ getAutofillManager().notifyViewEntered(focus);
+ }
+ }
+ }
mCalled = true;
}
@@ -1681,6 +1696,19 @@ public class Activity extends ContextThemeWrapper
protected void onPause() {
if (DEBUG_LIFECYCLE) Slog.v(TAG, "onPause " + this);
getApplication().dispatchActivityPaused(this);
+ if (mAutoFillResetNeeded) {
+ if (!mAutoFillIgnoreFirstResumePause) {
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "autofill notifyViewExited " + this);
+ View focus = getCurrentFocus();
+ if (focus != null && focus.canNotifyAutofillEnterExitEvent()) {
+ getAutofillManager().notifyViewExited(focus);
+ }
+ } else {
+ // reset after first pause()
+ if (DEBUG_LIFECYCLE) Slog.v(TAG, "autofill got first pause " + this);
+ mAutoFillIgnoreFirstResumePause = false;
+ }
+ }
mCalled = true;
}
@@ -1871,6 +1899,10 @@ public class Activity extends ContextThemeWrapper
mTranslucentCallback = null;
mCalled = true;
+ if (mAutoFillResetNeeded) {
+ getAutofillManager().onInvisibleForAutofill();
+ }
+
if (isFinishing()) {
if (mAutoFillResetNeeded) {
getAutofillManager().onActivityFinished();
@@ -2587,6 +2619,7 @@ public class Activity extends ContextThemeWrapper
* @param id the ID to search for
* @return a view with given ID if found, or {@code null} otherwise
* @see View#findViewById(int)
+ * @see Activity#requireViewById(int)
*/
@Nullable
public <T extends View> T findViewById(@IdRes int id) {
@@ -2594,6 +2627,30 @@ public class Activity extends ContextThemeWrapper
}
/**
+ * Finds a view that was identified by the {@code android:id} XML attribute that was processed
+ * in {@link #onCreate}, or throws an IllegalArgumentException if the ID is invalid, or there is
+ * no matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#requireViewById(int)
+ * @see Activity#findViewById(int)
+ */
+ @NonNull
+ public final <T extends View> T requireViewById(@IdRes int id) {
+ T view = findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this Activity");
+ }
+ return view;
+ }
+
+ /**
* Retrieve a reference to this activity's ActionBar.
*
* @return The Activity's ActionBar, or null if it does not have one.
@@ -4640,6 +4697,7 @@ public class Activity extends ContextThemeWrapper
* their launch had come from the original activity.
* @param intent The Intent to start.
* @param options ActivityOptions or null.
+ * @param permissionToken Token received from the system that permits this call to be made.
* @param ignoreTargetSecurity If true, the activity manager will not check whether the
* caller it is doing the start is, is actually allowed to start the target activity.
* If you set this to true, you must set an explicit component in the Intent and do any
@@ -4648,7 +4706,7 @@ public class Activity extends ContextThemeWrapper
* @hide
*/
public void startActivityAsCaller(Intent intent, @Nullable Bundle options,
- boolean ignoreTargetSecurity, int userId) {
+ IBinder permissionToken, boolean ignoreTargetSecurity, int userId) {
if (mParent != null) {
throw new RuntimeException("Can't be called from a child");
}
@@ -4656,7 +4714,7 @@ public class Activity extends ContextThemeWrapper
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivityAsCaller(
this, mMainThread.getApplicationThread(), mToken, this,
- intent, -1, options, ignoreTargetSecurity, userId);
+ intent, -1, options, permissionToken, ignoreTargetSecurity, userId);
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, -1, ar.getResultCode(),
@@ -6266,7 +6324,7 @@ public class Activity extends ContextThemeWrapper
mHandler.getLooper().dump(new PrintWriterPrinter(writer), prefix);
- final AutofillManager afm = getAutofillManager();
+ final AutofillManager afm = mAutofillManager;
if (afm != null) {
afm.dump(prefix, writer);
} else {
@@ -6616,7 +6674,6 @@ public class Activity extends ContextThemeWrapper
* to run as a {@link android.service.vr.VrListenerService} is not installed, or has
* not been enabled in user settings.
*
- * @see android.content.pm.PackageManager#FEATURE_VR_MODE
* @see android.content.pm.PackageManager#FEATURE_VR_MODE_HIGH_PERFORMANCE
* @see android.service.vr.VrListenerService
* @see android.provider.Settings#ACTION_VR_LISTENER_SETTINGS
@@ -7120,13 +7177,23 @@ public class Activity extends ContextThemeWrapper
}
}
- final void performResume() {
+ final void performResume(boolean followedByPause) {
performRestart(true /* start */);
mFragments.execPendingActions();
mLastNonConfigurationInstances = null;
+ if (mAutoFillResetNeeded) {
+ // When Activity is destroyed in paused state, and relaunch activity, there will be
+ // extra onResume and onPause event, ignore the first onResume and onPause.
+ // see ActivityThread.handleRelaunchActivity()
+ mAutoFillIgnoreFirstResumePause = followedByPause;
+ if (mAutoFillIgnoreFirstResumePause && DEBUG_LIFECYCLE) {
+ Slog.v(TAG, "autofill will ignore first pause when relaunching " + this);
+ }
+ }
+
mCalled = false;
// mResumed is set by the instrumentation
mInstrumentation.callActivityOnResume(this);
@@ -7311,7 +7378,7 @@ public class Activity extends ContextThemeWrapper
}
} else if (who.startsWith(AUTO_FILL_AUTH_WHO_PREFIX)) {
Intent resultData = (resultCode == Activity.RESULT_OK) ? data : null;
- getAutofillManager().onAuthenticationResult(requestCode, resultData);
+ getAutofillManager().onAuthenticationResult(requestCode, resultData, getCurrentFocus());
} else {
Fragment frag = mFragments.findFragmentByWho(who);
if (frag != null) {
@@ -7585,6 +7652,12 @@ public class Activity extends ContextThemeWrapper
return !mStopped;
}
+ /** @hide */
+ @Override
+ public boolean isDisablingEnterExitEventForAutofill() {
+ return mAutoFillIgnoreFirstResumePause || !mResumed;
+ }
+
/**
* If set to true, this indicates to the system that it should never take a
* screenshot of the activity to be used as a representation while it is not in a started state.
@@ -7659,6 +7732,22 @@ public class Activity extends ContextThemeWrapper
}
}
+ /**
+ * Registers remote animations per transition type for this activity.
+ *
+ * @param definition The remote animation definition that defines which transition whould run
+ * which remote animation.
+ * @hide
+ */
+ @RequiresPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS)
+ public void registerRemoteAnimations(RemoteAnimationDefinition definition) {
+ try {
+ ActivityManager.getService().registerRemoteAnimations(mToken, definition);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to call registerRemoteAnimations", e);
+ }
+ }
+
class HostCallbacks extends FragmentHostCallback<Activity> {
public HostCallbacks() {
super(Activity.this /*activity*/);
diff --git a/android/app/ActivityManager.java b/android/app/ActivityManager.java
index 1adae7a8..80350584 100644
--- a/android/app/ActivityManager.java
+++ b/android/app/ActivityManager.java
@@ -60,6 +60,7 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.os.WorkSource;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
@@ -180,7 +181,8 @@ public class ActivityManager {
BUGREPORT_OPTION_INTERACTIVE,
BUGREPORT_OPTION_REMOTE,
BUGREPORT_OPTION_WEAR,
- BUGREPORT_OPTION_TELEPHONY
+ BUGREPORT_OPTION_TELEPHONY,
+ BUGREPORT_OPTION_WIFI
})
public @interface BugreportMode {}
/**
@@ -215,6 +217,12 @@ public class ActivityManager {
public static final int BUGREPORT_OPTION_TELEPHONY = 4;
/**
+ * Takes a lightweight bugreport that only includes a few sections related to Wifi.
+ * @hide
+ */
+ public static final int BUGREPORT_OPTION_WIFI = 5;
+
+ /**
* <a href="{@docRoot}guide/topics/manifest/meta-data-element.html">{@code
* <meta-data>}</a> name for a 'home' Activity that declares a package that is to be
* uninstalled in lieu of the declaring one. The package named here must be
@@ -442,6 +450,31 @@ public class ActivityManager {
*/
public static final int INTENT_SENDER_FOREGROUND_SERVICE = 5;
+ /**
+ * Extra included on intents that are delegating the call to
+ * ActivityManager#startActivityAsCaller to another app. This token is necessary for that call
+ * to succeed. Type is IBinder.
+ * @hide
+ */
+ public static final String EXTRA_PERMISSION_TOKEN = "android.app.extra.PERMISSION_TOKEN";
+
+ /**
+ * Extra included on intents that contain an EXTRA_INTENT, with options that the contained
+ * intent may want to be started with. Type is Bundle.
+ * TODO: remove once the ChooserActivity moves to systemui
+ * @hide
+ */
+ public static final String EXTRA_OPTIONS = "android.app.extra.OPTIONS";
+
+ /**
+ * Extra included on intents that contain an EXTRA_INTENT, use this boolean value for the
+ * parameter of the same name when starting the contained intent.
+ * TODO: remove once the ChooserActivity moves to systemui
+ * @hide
+ */
+ public static final String EXTRA_IGNORE_TARGET_SECURITY =
+ "android.app.extra.EXTRA_IGNORE_TARGET_SECURITY";
+
/** @hide User operation call: success! */
public static final int USER_OP_SUCCESS = 0;
@@ -484,11 +517,11 @@ public class ActivityManager {
* all activities that are visible to the user. */
public static final int PROCESS_STATE_TOP = 2;
- /** @hide Process is hosting a foreground service due to a system binding. */
- public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 3;
-
/** @hide Process is hosting a foreground service. */
- public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4;
+ public static final int PROCESS_STATE_FOREGROUND_SERVICE = 3;
+
+ /** @hide Process is hosting a foreground service due to a system binding. */
+ public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 4;
/** @hide Process is important to the user, and something they are aware of. */
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 5;
@@ -3085,11 +3118,11 @@ public class ActivityManager {
} else if (importance >= IMPORTANCE_VISIBLE) {
return PROCESS_STATE_IMPORTANT_FOREGROUND;
} else if (importance >= IMPORTANCE_TOP_SLEEPING_PRE_28) {
- return PROCESS_STATE_FOREGROUND_SERVICE;
+ return PROCESS_STATE_IMPORTANT_FOREGROUND;
} else if (importance >= IMPORTANCE_FOREGROUND_SERVICE) {
return PROCESS_STATE_FOREGROUND_SERVICE;
} else {
- return PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+ return PROCESS_STATE_TOP;
}
}
@@ -3911,10 +3944,10 @@ public class ActivityManager {
/**
* @hide
*/
- public static void noteWakeupAlarm(PendingIntent ps, int sourceUid, String sourcePkg,
- String tag) {
+ public static void noteWakeupAlarm(PendingIntent ps, WorkSource workSource, int sourceUid,
+ String sourcePkg, String tag) {
try {
- getService().noteWakeupAlarm((ps != null) ? ps.getTarget() : null,
+ getService().noteWakeupAlarm((ps != null) ? ps.getTarget() : null, workSource,
sourceUid, sourcePkg, tag);
} catch (RemoteException ex) {
}
@@ -3923,19 +3956,24 @@ public class ActivityManager {
/**
* @hide
*/
- public static void noteAlarmStart(PendingIntent ps, int sourceUid, String tag) {
+ public static void noteAlarmStart(PendingIntent ps, WorkSource workSource, int sourceUid,
+ String tag) {
try {
- getService().noteAlarmStart((ps != null) ? ps.getTarget() : null, sourceUid, tag);
+ getService().noteAlarmStart((ps != null) ? ps.getTarget() : null, workSource,
+ sourceUid, tag);
} catch (RemoteException ex) {
}
}
+
/**
* @hide
*/
- public static void noteAlarmFinish(PendingIntent ps, int sourceUid, String tag) {
+ public static void noteAlarmFinish(PendingIntent ps, WorkSource workSource, int sourceUid,
+ String tag) {
try {
- getService().noteAlarmFinish((ps != null) ? ps.getTarget() : null, sourceUid, tag);
+ getService().noteAlarmFinish((ps != null) ? ps.getTarget() : null, workSource,
+ sourceUid, tag);
} catch (RemoteException ex) {
}
}
diff --git a/android/app/ActivityManagerInternal.java b/android/app/ActivityManagerInternal.java
index 60a5a110..da9f7285 100644
--- a/android/app/ActivityManagerInternal.java
+++ b/android/app/ActivityManagerInternal.java
@@ -319,4 +319,29 @@ public abstract class ActivityManagerInternal {
}
public abstract void registerScreenObserver(ScreenObserver observer);
+
+ /**
+ * Returns if more users can be started without stopping currently running users.
+ */
+ public abstract boolean canStartMoreUsers();
+
+ /**
+ * Sets the user switcher message for switching from {@link android.os.UserHandle#SYSTEM}.
+ */
+ public abstract void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage);
+
+ /**
+ * Sets the user switcher message for switching to {@link android.os.UserHandle#SYSTEM}.
+ */
+ public abstract void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage);
+
+ /**
+ * Returns maximum number of users that can run simultaneously.
+ */
+ public abstract int getMaxRunningUsers();
+
+ /**
+ * Returns is the caller has the same uid as the Recents component
+ */
+ public abstract boolean isCallerRecents(int callingUid);
}
diff --git a/android/app/ActivityManagerNative.java b/android/app/ActivityManagerNative.java
index c09403c2..4c558f37 100644
--- a/android/app/ActivityManagerNative.java
+++ b/android/app/ActivityManagerNative.java
@@ -75,20 +75,20 @@ public abstract class ActivityManagerNative {
*/
static public void noteWakeupAlarm(PendingIntent ps, int sourceUid, String sourcePkg,
String tag) {
- ActivityManager.noteWakeupAlarm(ps, sourceUid, sourcePkg, tag);
+ ActivityManager.noteWakeupAlarm(ps, null, sourceUid, sourcePkg, tag);
}
/**
* @deprecated use ActivityManager.noteAlarmStart instead.
*/
static public void noteAlarmStart(PendingIntent ps, int sourceUid, String tag) {
- ActivityManager.noteAlarmStart(ps, sourceUid, tag);
+ ActivityManager.noteAlarmStart(ps, null, sourceUid, tag);
}
/**
* @deprecated use ActivityManager.noteAlarmFinish instead.
*/
static public void noteAlarmFinish(PendingIntent ps, int sourceUid, String tag) {
- ActivityManager.noteAlarmFinish(ps, sourceUid, tag);
+ ActivityManager.noteAlarmFinish(ps, null, sourceUid, tag);
}
}
diff --git a/android/app/ActivityOptions.java b/android/app/ActivityOptions.java
index e61c5b7c..fee58274 100644
--- a/android/app/ActivityOptions.java
+++ b/android/app/ActivityOptions.java
@@ -16,12 +16,14 @@
package android.app;
+import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
import static android.app.ActivityManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.view.Display.INVALID_DISPLAY;
import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.annotation.TestApi;
import android.content.ComponentName;
import android.content.Context;
@@ -44,6 +46,7 @@ import android.util.Pair;
import android.util.Slog;
import android.view.AppTransitionAnimationSpec;
import android.view.IAppTransitionAnimationSpecsFuture;
+import android.view.RemoteAnimationAdapter;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
@@ -204,6 +207,12 @@ public class ActivityOptions {
"android.activity.taskOverlayCanResume";
/**
+ * See {@link #setAvoidMoveToFront()}.
+ * @hide
+ */
+ private static final String KEY_AVOID_MOVE_TO_FRONT = "android.activity.avoidMoveToFront";
+
+ /**
* Where the split-screen-primary stack should be positioned.
* @hide
*/
@@ -241,6 +250,8 @@ public class ActivityOptions {
private static final String KEY_INSTANT_APP_VERIFICATION_BUNDLE
= "android:instantapps.installerbundle";
private static final String KEY_SPECS_FUTURE = "android:activity.specsFuture";
+ private static final String KEY_REMOTE_ANIMATION_ADAPTER
+ = "android:activity.remoteAnimationAdapter";
/** @hide */
public static final int ANIM_NONE = 0;
@@ -268,6 +279,8 @@ public class ActivityOptions {
public static final int ANIM_CLIP_REVEAL = 11;
/** @hide */
public static final int ANIM_OPEN_CROSS_PROFILE_APPS = 12;
+ /** @hide */
+ public static final int ANIM_REMOTE_ANIMATION = 13;
private String mPackageName;
private Rect mLaunchBounds;
@@ -300,10 +313,12 @@ public class ActivityOptions {
private boolean mDisallowEnterPictureInPictureWhileLaunching;
private boolean mTaskOverlay;
private boolean mTaskOverlayCanResume;
+ private boolean mAvoidMoveToFront;
private AppTransitionAnimationSpec mAnimSpecs[];
private int mRotationAnimationHint = -1;
private Bundle mAppVerificationBundle;
private IAppTransitionAnimationSpecsFuture mSpecsFuture;
+ private RemoteAnimationAdapter mRemoteAnimationAdapter;
/**
* Create an ActivityOptions specifying a custom animation to run when
@@ -826,6 +841,20 @@ public class ActivityOptions {
return opts;
}
+ /**
+ * Create an {@link ActivityOptions} instance that lets the application control the entire
+ * animation using a {@link RemoteAnimationAdapter}.
+ * @hide
+ */
+ @RequiresPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS)
+ public static ActivityOptions makeRemoteAnimation(
+ RemoteAnimationAdapter remoteAnimationAdapter) {
+ final ActivityOptions opts = new ActivityOptions();
+ opts.mRemoteAnimationAdapter = remoteAnimationAdapter;
+ opts.mAnimationType = ANIM_REMOTE_ANIMATION;
+ return opts;
+ }
+
/** @hide */
public boolean getLaunchTaskBehind() {
return mAnimationType == ANIM_LAUNCH_TASK_BEHIND;
@@ -901,6 +930,7 @@ public class ActivityOptions {
mLaunchTaskId = opts.getInt(KEY_LAUNCH_TASK_ID, -1);
mTaskOverlay = opts.getBoolean(KEY_TASK_OVERLAY, false);
mTaskOverlayCanResume = opts.getBoolean(KEY_TASK_OVERLAY_CAN_RESUME, false);
+ mAvoidMoveToFront = opts.getBoolean(KEY_AVOID_MOVE_TO_FRONT, false);
mSplitScreenCreateMode = opts.getInt(KEY_SPLIT_SCREEN_CREATE_MODE,
SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT);
mDisallowEnterPictureInPictureWhileLaunching = opts.getBoolean(
@@ -922,6 +952,7 @@ public class ActivityOptions {
mSpecsFuture = IAppTransitionAnimationSpecsFuture.Stub.asInterface(opts.getBinder(
KEY_SPECS_FUTURE));
}
+ mRemoteAnimationAdapter = opts.getParcelable(KEY_REMOTE_ANIMATION_ADAPTER);
}
/**
@@ -1070,6 +1101,11 @@ public class ActivityOptions {
}
/** @hide */
+ public RemoteAnimationAdapter getRemoteAnimationAdapter() {
+ return mRemoteAnimationAdapter;
+ }
+
+ /** @hide */
public static ActivityOptions fromBundle(Bundle bOptions) {
return bOptions != null ? new ActivityOptions(bOptions) : null;
}
@@ -1211,6 +1247,25 @@ public class ActivityOptions {
return mTaskOverlayCanResume;
}
+ /**
+ * Sets whether the activity launched should not cause the activity stack it is contained in to
+ * be moved to the front as a part of launching.
+ *
+ * @hide
+ */
+ public void setAvoidMoveToFront() {
+ mAvoidMoveToFront = true;
+ }
+
+ /**
+ * @return whether the activity launch should prevent moving the associated activity stack to
+ * the front.
+ * @hide
+ */
+ public boolean getAvoidMoveToFront() {
+ return mAvoidMoveToFront;
+ }
+
/** @hide */
public int getSplitScreenCreateMode() {
return mSplitScreenCreateMode;
@@ -1309,6 +1364,7 @@ public class ActivityOptions {
mAnimSpecs = otherOptions.mAnimSpecs;
mAnimationFinishedListener = otherOptions.mAnimationFinishedListener;
mSpecsFuture = otherOptions.mSpecsFuture;
+ mRemoteAnimationAdapter = otherOptions.mRemoteAnimationAdapter;
}
/**
@@ -1387,6 +1443,7 @@ public class ActivityOptions {
b.putInt(KEY_LAUNCH_TASK_ID, mLaunchTaskId);
b.putBoolean(KEY_TASK_OVERLAY, mTaskOverlay);
b.putBoolean(KEY_TASK_OVERLAY_CAN_RESUME, mTaskOverlayCanResume);
+ b.putBoolean(KEY_AVOID_MOVE_TO_FRONT, mAvoidMoveToFront);
b.putInt(KEY_SPLIT_SCREEN_CREATE_MODE, mSplitScreenCreateMode);
b.putBoolean(KEY_DISALLOW_ENTER_PICTURE_IN_PICTURE_WHILE_LAUNCHING,
mDisallowEnterPictureInPictureWhileLaunching);
@@ -1403,7 +1460,9 @@ public class ActivityOptions {
if (mAppVerificationBundle != null) {
b.putBundle(KEY_INSTANT_APP_VERIFICATION_BUNDLE, mAppVerificationBundle);
}
-
+ if (mRemoteAnimationAdapter != null) {
+ b.putParcelable(KEY_REMOTE_ANIMATION_ADAPTER, mRemoteAnimationAdapter);
+ }
return b;
}
diff --git a/android/app/ActivityThread.java b/android/app/ActivityThread.java
index aaa6bf03..934b0f3c 100644
--- a/android/app/ActivityThread.java
+++ b/android/app/ActivityThread.java
@@ -166,6 +166,7 @@ import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.text.DateFormat;
+import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -220,6 +221,9 @@ public final class ActivityThread extends ClientTransactionHandler {
// Whether to invoke an activity callback after delivering new configuration.
private static final boolean REPORT_TO_ACTIVITY = true;
+ // Maximum number of recent tokens to maintain for debugging purposes
+ private static final int MAX_RECENT_TOKENS = 10;
+
/**
* Denotes an invalid sequence number corresponding to a process state change.
*/
@@ -252,6 +256,8 @@ public final class ActivityThread extends ClientTransactionHandler {
final H mH = new H();
final Executor mExecutor = new HandlerExecutor(mH);
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();
+ final ArrayDeque<Integer> mRecentTokens = new ArrayDeque<>();
+
// List of new activities (via ActivityRecord.nextIdle) that should
// be reported when next we idle.
ActivityClientRecord mNewActivities = null;
@@ -1752,9 +1758,11 @@ public final class ActivityThread extends ClientTransactionHandler {
handleLocalVoiceInteractionStarted((IBinder) ((SomeArgs) msg.obj).arg1,
(IVoiceInteractor) ((SomeArgs) msg.obj).arg2);
break;
- case ATTACH_AGENT:
- handleAttachAgent((String) msg.obj);
+ case ATTACH_AGENT: {
+ Application app = getApplication();
+ handleAttachAgent((String) msg.obj, app != null ? app.mLoadedApk : null);
break;
+ }
case APPLICATION_INFO_CHANGED:
mUpdatingSystemConfig = true;
try {
@@ -1770,7 +1778,12 @@ public final class ActivityThread extends ClientTransactionHandler {
case EXECUTE_TRANSACTION:
final ClientTransaction transaction = (ClientTransaction) msg.obj;
mTransactionExecutor.execute(transaction);
- transaction.recycle();
+ if (isSystem()) {
+ // Client transactions inside system process are recycled on the client side
+ // instead of ClientLifecycleManager to avoid being cleared before this
+ // message is handled.
+ transaction.recycle();
+ }
break;
}
Object obj = msg.obj;
@@ -2161,6 +2174,18 @@ public final class ActivityThread extends ClientTransactionHandler {
pw.println(String.format(format, objs));
}
+ @Override
+ public void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + "mActivities:");
+
+ for (ArrayMap.Entry<IBinder, ActivityClientRecord> entry : mActivities.entrySet()) {
+ pw.println(prefix + " [token:" + entry.getKey().hashCode() + " record:"
+ + entry.getValue().toString() + "]");
+ }
+
+ pw.println(prefix + "mRecentTokens:" + mRecentTokens);
+ }
+
public static void dumpMemInfoTable(PrintWriter pw, Debug.MemoryInfo memInfo, boolean checkin,
boolean dumpFullInfo, boolean dumpDalvik, boolean dumpSummaryOnly,
int pid, String processName,
@@ -2845,6 +2870,11 @@ public final class ActivityThread extends ClientTransactionHandler {
r.setState(ON_CREATE);
mActivities.put(r.token, r);
+ mRecentTokens.push(r.token.hashCode());
+
+ if (mRecentTokens.size() > MAX_RECENT_TOKENS) {
+ mRecentTokens.removeLast();
+ }
} catch (SuperNotCalledException e) {
throw e;
@@ -3066,7 +3096,7 @@ public final class ActivityThread extends ClientTransactionHandler {
checkAndBlockForNetworkAccess();
deliverNewIntents(r, intents);
if (resumed) {
- r.activity.performResume();
+ r.activity.performResume(false);
r.activity.mTemporaryPause = false;
}
@@ -3241,11 +3271,23 @@ public final class ActivityThread extends ClientTransactionHandler {
}
}
- static final void handleAttachAgent(String agent) {
+ private static boolean attemptAttachAgent(String agent, ClassLoader classLoader) {
try {
- VMDebug.attachAgent(agent);
+ VMDebug.attachAgent(agent, classLoader);
+ return true;
} catch (IOException e) {
- Slog.e(TAG, "Attaching agent failed: " + agent);
+ Slog.e(TAG, "Attaching agent with " + classLoader + " failed: " + agent);
+ return false;
+ }
+ }
+
+ static void handleAttachAgent(String agent, LoadedApk loadedApk) {
+ ClassLoader classLoader = loadedApk != null ? loadedApk.getClassLoader() : null;
+ if (attemptAttachAgent(agent, classLoader)) {
+ return;
+ }
+ if (classLoader != null) {
+ attemptAttachAgent(agent, null);
}
}
@@ -3676,7 +3718,7 @@ public final class ActivityThread extends ClientTransactionHandler {
deliverResults(r, r.pendingResults);
r.pendingResults = null;
}
- r.activity.performResume();
+ r.activity.performResume(r.startsNotResumed);
synchronized (mResourcesManager) {
// If there is a pending local relaunch that was requested when the activity was
@@ -4395,7 +4437,7 @@ public final class ActivityThread extends ClientTransactionHandler {
checkAndBlockForNetworkAccess();
deliverResults(r, results);
if (resumed) {
- r.activity.performResume();
+ r.activity.performResume(false);
r.activity.mTemporaryPause = false;
}
}
@@ -5537,12 +5579,16 @@ public final class ActivityThread extends ClientTransactionHandler {
mCompatConfiguration = new Configuration(data.config);
mProfiler = new Profiler();
+ String agent = null;
if (data.initProfilerInfo != null) {
mProfiler.profileFile = data.initProfilerInfo.profileFile;
mProfiler.profileFd = data.initProfilerInfo.profileFd;
mProfiler.samplingInterval = data.initProfilerInfo.samplingInterval;
mProfiler.autoStopProfiler = data.initProfilerInfo.autoStopProfiler;
mProfiler.streamingOutput = data.initProfilerInfo.streamingOutput;
+ if (data.initProfilerInfo.attachAgentDuringBind) {
+ agent = data.initProfilerInfo.agent;
+ }
}
// send up app name; do this *before* waiting for debugger
@@ -5592,6 +5638,10 @@ public final class ActivityThread extends ClientTransactionHandler {
data.loadedApk = getLoadedApkNoCheck(data.appInfo, data.compatInfo);
+ if (agent != null) {
+ handleAttachAgent(agent, data.loadedApk);
+ }
+
/**
* Switch this process to density compatibility mode if needed.
*/
diff --git a/android/app/ActivityView.java b/android/app/ActivityView.java
index 9f1e9839..5d0143a5 100644
--- a/android/app/ActivityView.java
+++ b/android/app/ActivityView.java
@@ -17,6 +17,7 @@
package android.app;
import android.annotation.NonNull;
+import android.app.ActivityManager.StackInfo;
import android.content.Context;
import android.content.Intent;
import android.hardware.display.DisplayManager;
@@ -34,9 +35,12 @@ import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;
import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
import dalvik.system.CloseGuard;
+import java.util.List;
+
/**
* Activity container that allows launching activities into itself and does input forwarding.
* <p>Creation of this view is only allowed to callers who have
@@ -57,7 +61,12 @@ public class ActivityView extends ViewGroup {
private final SurfaceCallback mSurfaceCallback;
private StateCallback mActivityViewCallback;
+ private IActivityManager mActivityManager;
private IInputForwarder mInputForwarder;
+ // Temp container to store view coordinates on screen.
+ private final int[] mLocationOnScreen = new int[2];
+
+ private TaskStackListener mTaskStackListener;
private final CloseGuard mGuard = CloseGuard.get();
private boolean mOpened; // Protected by mGuard.
@@ -73,6 +82,7 @@ public class ActivityView extends ViewGroup {
public ActivityView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
+ mActivityManager = ActivityManager.getService();
mSurfaceView = new SurfaceView(context);
mSurfaceCallback = new SurfaceCallback();
mSurfaceView.getHolder().addCallback(mSurfaceCallback);
@@ -198,11 +208,30 @@ public class ActivityView extends ViewGroup {
performRelease();
}
+ /**
+ * Triggers an update of {@link ActivityView}'s location on screen to properly set touch exclude
+ * regions and avoid focus switches by touches on this view.
+ */
+ public void onLocationChanged() {
+ updateLocation();
+ }
+
@Override
public void onLayout(boolean changed, int l, int t, int r, int b) {
mSurfaceView.layout(0 /* left */, 0 /* top */, r - l /* right */, b - t /* bottom */);
}
+ /** Send current location and size to the WM to set tap exclude region for this view. */
+ private void updateLocation() {
+ try {
+ getLocationOnScreen(mLocationOnScreen);
+ WindowManagerGlobal.getWindowSession().updateTapExcludeRegion(getWindow(), hashCode(),
+ mLocationOnScreen[0], mLocationOnScreen[1], getWidth(), getHeight());
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
@Override
public boolean onTouchEvent(MotionEvent event) {
return injectInputEvent(event) || super.onTouchEvent(event);
@@ -241,6 +270,7 @@ public class ActivityView extends ViewGroup {
} else {
mVirtualDisplay.setSurface(surfaceHolder.getSurface());
}
+ updateLocation();
}
@Override
@@ -248,6 +278,7 @@ public class ActivityView extends ViewGroup {
if (mVirtualDisplay != null) {
mVirtualDisplay.resize(width, height, getBaseDisplayDensity());
}
+ updateLocation();
}
@Override
@@ -257,6 +288,7 @@ public class ActivityView extends ViewGroup {
if (mVirtualDisplay != null) {
mVirtualDisplay.setSurface(null);
}
+ cleanTapExcludeRegion();
}
}
@@ -278,6 +310,12 @@ public class ActivityView extends ViewGroup {
mInputForwarder = InputManager.getInstance().createInputForwarder(
mVirtualDisplay.getDisplay().getDisplayId());
+ mTaskStackListener = new TaskBackgroundChangeListener();
+ try {
+ mActivityManager.registerTaskStackListener(mTaskStackListener);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to register task stack listener", e);
+ }
}
private void performRelease() {
@@ -290,6 +328,16 @@ public class ActivityView extends ViewGroup {
if (mInputForwarder != null) {
mInputForwarder = null;
}
+ cleanTapExcludeRegion();
+
+ if (mTaskStackListener != null) {
+ try {
+ mActivityManager.unregisterTaskStackListener(mTaskStackListener);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to unregister task stack listener", e);
+ }
+ mTaskStackListener = null;
+ }
final boolean displayReleased;
if (mVirtualDisplay != null) {
@@ -313,6 +361,17 @@ public class ActivityView extends ViewGroup {
mOpened = false;
}
+ /** Report to server that tap exclude region on hosting display should be cleared. */
+ private void cleanTapExcludeRegion() {
+ // Update tap exclude region with an empty rect to clean the state on server.
+ try {
+ WindowManagerGlobal.getWindowSession().updateTapExcludeRegion(getWindow(), hashCode(),
+ 0 /* left */, 0 /* top */, 0 /* width */, 0 /* height */);
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ }
+ }
+
/** Get density of the hosting display. */
private int getBaseDisplayDensity() {
final WindowManager wm = mContext.getSystemService(WindowManager.class);
@@ -332,4 +391,42 @@ public class ActivityView extends ViewGroup {
super.finalize();
}
}
+
+ /**
+ * A task change listener that detects background color change of the topmost stack on our
+ * virtual display and updates the background of the surface view. This background will be shown
+ * when surface view is resized, but the app hasn't drawn its content in new size yet.
+ */
+ private class TaskBackgroundChangeListener extends TaskStackListener {
+
+ @Override
+ public void onTaskDescriptionChanged(int taskId, ActivityManager.TaskDescription td)
+ throws RemoteException {
+ if (mVirtualDisplay == null) {
+ return;
+ }
+
+ // Find the topmost task on our virtual display - it will define the background
+ // color of the surface view during resizing.
+ final int displayId = mVirtualDisplay.getDisplay().getDisplayId();
+ final List<StackInfo> stackInfoList = mActivityManager.getAllStackInfos();
+
+ // Iterate through stacks from top to bottom.
+ final int stackCount = stackInfoList.size();
+ for (int i = 0; i < stackCount; i++) {
+ final StackInfo stackInfo = stackInfoList.get(i);
+ // Only look for stacks on our virtual display.
+ if (stackInfo.displayId != displayId) {
+ continue;
+ }
+ // Found the topmost stack on target display. Now check if the topmost task's
+ // description changed.
+ if (taskId == stackInfo.taskIds[stackInfo.taskIds.length - 1]) {
+ mSurfaceView.setResizeBackgroundColor(td.getBackgroundColor());
+ }
+ break;
+ }
+ }
+ }
+
}
diff --git a/android/app/AppOpsManager.java b/android/app/AppOpsManager.java
index ea22d332..e923fb21 100644
--- a/android/app/AppOpsManager.java
+++ b/android/app/AppOpsManager.java
@@ -20,6 +20,7 @@ import android.Manifest;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.annotation.TestApi;
import android.app.usage.UsageStatsManager;
import android.content.Context;
import android.media.AudioAttributes.AttributeUsage;
@@ -37,6 +38,7 @@ import com.android.internal.app.IAppOpsCallback;
import com.android.internal.app.IAppOpsService;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@@ -106,6 +108,7 @@ public class AppOpsManager {
// when adding one of these:
// - increment _NUM_OP
+ // - define an OPSTR_* constant (marked as @SystemApi)
// - add rows to sOpToSwitch, sOpToString, sOpNames, sOpToPerms, sOpDefault
// - add descriptive strings to Settings/res/values/arrays.xml
// - add the op to the appropriate template in AppOpsState.OpsTemplate (settings app)
@@ -260,8 +263,10 @@ public class AppOpsManager {
public static final int OP_REQUEST_DELETE_PACKAGES = 72;
/** @hide Bind an accessibility service. */
public static final int OP_BIND_ACCESSIBILITY_SERVICE = 73;
+ /** @hide Continue handover of a call from another app */
+ public static final int OP_ACCEPT_HANDOVER = 74;
/** @hide */
- public static final int _NUM_OP = 74;
+ public static final int _NUM_OP = 75;
/** Access to coarse location information. */
public static final String OPSTR_COARSE_LOCATION = "android:coarse_location";
@@ -278,7 +283,7 @@ public class AppOpsManager {
public static final String OPSTR_GET_USAGE_STATS
= "android:get_usage_stats";
/** Activate a VPN connection without user intervention. @hide */
- @SystemApi
+ @SystemApi @TestApi
public static final String OPSTR_ACTIVATE_VPN
= "android:activate_vpn";
/** Allows an application to read the user's contacts data. */
@@ -360,6 +365,7 @@ public class AppOpsManager {
public static final String OPSTR_WRITE_SETTINGS
= "android:write_settings";
/** @hide Get device accounts. */
+ @SystemApi @TestApi
public static final String OPSTR_GET_ACCOUNTS
= "android:get_accounts";
public static final String OPSTR_READ_PHONE_NUMBERS
@@ -368,11 +374,133 @@ public class AppOpsManager {
public static final String OPSTR_PICTURE_IN_PICTURE
= "android:picture_in_picture";
/** @hide */
+ @SystemApi @TestApi
public static final String OPSTR_INSTANT_APP_START_FOREGROUND
= "android:instant_app_start_foreground";
/** Answer incoming phone calls */
public static final String OPSTR_ANSWER_PHONE_CALLS
= "android:answer_phone_calls";
+ /**
+ * Accept call handover
+ * @hide
+ */
+ @SystemApi @TestApi
+ public static final String OPSTR_ACCEPT_HANDOVER
+ = "android:accept_handover";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_GPS = "android:gps";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_VIBRATE = "android:vibrate";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_WIFI_SCAN = "android:wifi_scan";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_POST_NOTIFICATION = "android:post_notification";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_NEIGHBORING_CELLS = "android:neighboring_cells";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_WRITE_SMS = "android:write_sms";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_RECEIVE_EMERGENCY_BROADCAST =
+ "android:receive_emergency_broadcast";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_READ_ICC_SMS = "android:read_icc_sms";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_WRITE_ICC_SMS = "android:write_icc_sms";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_ACCESS_NOTIFICATIONS = "android:access_notifications";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_PLAY_AUDIO = "android:play_audio";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_READ_CLIPBOARD = "android:read_clipboard";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_WRITE_CLIPBOARD = "android:write_clipboard";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_TAKE_MEDIA_BUTTONS = "android:take_media_buttons";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_TAKE_AUDIO_FOCUS = "android:take_audio_focus";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_MASTER_VOLUME = "android:audio_master_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_VOICE_VOLUME = "android:audio_voice_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_RING_VOLUME = "android:audio_ring_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_MEDIA_VOLUME = "android:audio_media_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_ALARM_VOLUME = "android:audio_alarm_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_NOTIFICATION_VOLUME =
+ "android:audio_notification_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_BLUETOOTH_VOLUME = "android:audio_bluetooth_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_WAKE_LOCK = "android:wake_lock";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_MUTE_MICROPHONE = "android:mute_microphone";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_TOAST_WINDOW = "android:toast_window";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_PROJECT_MEDIA = "android:project_media";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_WRITE_WALLPAPER = "android:write_wallpaper";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_ASSIST_STRUCTURE = "android:assist_structure";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_ASSIST_SCREENSHOT = "android:assist_screenshot";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_TURN_SCREEN_ON = "android:turn_screen_on";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_RUN_IN_BACKGROUND = "android:run_in_background";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_AUDIO_ACCESSIBILITY_VOLUME =
+ "android:audio_accessibility_volume";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_REQUEST_INSTALL_PACKAGES = "android:request_install_packages";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_RUN_ANY_IN_BACKGROUND = "android:run_any_in_background";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_CHANGE_WIFI_STATE = "change_wifi_state";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_REQUEST_DELETE_PACKAGES = "request_delete_packages";
+ /** @hide */
+ @SystemApi @TestApi
+ public static final String OPSTR_BIND_ACCESSIBILITY_SERVICE = "bind_accessibility_service";
// Warning: If an permission is added here it also has to be added to
// com.android.packageinstaller.permission.utils.EventLogger
@@ -408,6 +536,7 @@ public class AppOpsManager {
OP_USE_SIP,
OP_PROCESS_OUTGOING_CALLS,
OP_ANSWER_PHONE_CALLS,
+ OP_ACCEPT_HANDOVER,
// Microphone
OP_RECORD_AUDIO,
// Camera
@@ -506,64 +635,64 @@ public class AppOpsManager {
OP_CHANGE_WIFI_STATE,
OP_REQUEST_DELETE_PACKAGES,
OP_BIND_ACCESSIBILITY_SERVICE,
+ OP_ACCEPT_HANDOVER,
};
/**
* This maps each operation to the public string constant for it.
- * If it doesn't have a public string constant, it maps to null.
*/
- private static String[] sOpToString = new String[] {
+ private static String[] sOpToString = new String[]{
OPSTR_COARSE_LOCATION,
OPSTR_FINE_LOCATION,
- null,
- null,
+ OPSTR_GPS,
+ OPSTR_VIBRATE,
OPSTR_READ_CONTACTS,
OPSTR_WRITE_CONTACTS,
OPSTR_READ_CALL_LOG,
OPSTR_WRITE_CALL_LOG,
OPSTR_READ_CALENDAR,
OPSTR_WRITE_CALENDAR,
- null,
- null,
- null,
+ OPSTR_WIFI_SCAN,
+ OPSTR_POST_NOTIFICATION,
+ OPSTR_NEIGHBORING_CELLS,
OPSTR_CALL_PHONE,
OPSTR_READ_SMS,
- null,
+ OPSTR_WRITE_SMS,
OPSTR_RECEIVE_SMS,
- null,
+ OPSTR_RECEIVE_EMERGENCY_BROADCAST,
OPSTR_RECEIVE_MMS,
OPSTR_RECEIVE_WAP_PUSH,
OPSTR_SEND_SMS,
- null,
- null,
+ OPSTR_READ_ICC_SMS,
+ OPSTR_WRITE_ICC_SMS,
OPSTR_WRITE_SETTINGS,
OPSTR_SYSTEM_ALERT_WINDOW,
- null,
+ OPSTR_ACCESS_NOTIFICATIONS,
OPSTR_CAMERA,
OPSTR_RECORD_AUDIO,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
- null,
+ OPSTR_PLAY_AUDIO,
+ OPSTR_READ_CLIPBOARD,
+ OPSTR_WRITE_CLIPBOARD,
+ OPSTR_TAKE_MEDIA_BUTTONS,
+ OPSTR_TAKE_AUDIO_FOCUS,
+ OPSTR_AUDIO_MASTER_VOLUME,
+ OPSTR_AUDIO_VOICE_VOLUME,
+ OPSTR_AUDIO_RING_VOLUME,
+ OPSTR_AUDIO_MEDIA_VOLUME,
+ OPSTR_AUDIO_ALARM_VOLUME,
+ OPSTR_AUDIO_NOTIFICATION_VOLUME,
+ OPSTR_AUDIO_BLUETOOTH_VOLUME,
+ OPSTR_WAKE_LOCK,
OPSTR_MONITOR_LOCATION,
OPSTR_MONITOR_HIGH_POWER_LOCATION,
OPSTR_GET_USAGE_STATS,
- null,
- null,
- null,
+ OPSTR_MUTE_MICROPHONE,
+ OPSTR_TOAST_WINDOW,
+ OPSTR_PROJECT_MEDIA,
OPSTR_ACTIVATE_VPN,
- null,
- null,
- null,
+ OPSTR_WRITE_WALLPAPER,
+ OPSTR_ASSIST_STRUCTURE,
+ OPSTR_ASSIST_SCREENSHOT,
OPSTR_READ_PHONE_STATE,
OPSTR_ADD_VOICEMAIL,
OPSTR_USE_SIP,
@@ -574,19 +703,20 @@ public class AppOpsManager {
OPSTR_MOCK_LOCATION,
OPSTR_READ_EXTERNAL_STORAGE,
OPSTR_WRITE_EXTERNAL_STORAGE,
- null,
+ OPSTR_TURN_SCREEN_ON,
OPSTR_GET_ACCOUNTS,
- null,
- null, // OP_AUDIO_ACCESSIBILITY_VOLUME
+ OPSTR_RUN_IN_BACKGROUND,
+ OPSTR_AUDIO_ACCESSIBILITY_VOLUME,
OPSTR_READ_PHONE_NUMBERS,
- null, // OP_REQUEST_INSTALL_PACKAGES
+ OPSTR_REQUEST_INSTALL_PACKAGES,
OPSTR_PICTURE_IN_PICTURE,
OPSTR_INSTANT_APP_START_FOREGROUND,
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
+ OPSTR_RUN_ANY_IN_BACKGROUND,
+ OPSTR_CHANGE_WIFI_STATE,
+ OPSTR_REQUEST_DELETE_PACKAGES,
+ OPSTR_BIND_ACCESSIBILITY_SERVICE,
+ OPSTR_ACCEPT_HANDOVER,
};
/**
@@ -668,6 +798,7 @@ public class AppOpsManager {
"CHANGE_WIFI_STATE",
"REQUEST_DELETE_PACKAGES",
"BIND_ACCESSIBILITY_SERVICE",
+ "ACCEPT_HANDOVER",
};
/**
@@ -749,6 +880,7 @@ public class AppOpsManager {
Manifest.permission.CHANGE_WIFI_STATE,
Manifest.permission.REQUEST_DELETE_PACKAGES,
Manifest.permission.BIND_ACCESSIBILITY_SERVICE,
+ Manifest.permission.ACCEPT_HANDOVER,
};
/**
@@ -831,6 +963,7 @@ public class AppOpsManager {
null, // OP_CHANGE_WIFI_STATE
null, // REQUEST_DELETE_PACKAGES
null, // OP_BIND_ACCESSIBILITY_SERVICE
+ null, // ACCEPT_HANDOVER
};
/**
@@ -912,6 +1045,7 @@ public class AppOpsManager {
false, // OP_CHANGE_WIFI_STATE
false, // OP_REQUEST_DELETE_PACKAGES
false, // OP_BIND_ACCESSIBILITY_SERVICE
+ false, // ACCEPT_HANDOVER
};
/**
@@ -992,6 +1126,7 @@ public class AppOpsManager {
AppOpsManager.MODE_ALLOWED, // OP_CHANGE_WIFI_STATE
AppOpsManager.MODE_ALLOWED, // REQUEST_DELETE_PACKAGES
AppOpsManager.MODE_ALLOWED, // OP_BIND_ACCESSIBILITY_SERVICE
+ AppOpsManager.MODE_ALLOWED, // ACCEPT_HANDOVER
};
/**
@@ -1076,6 +1211,7 @@ public class AppOpsManager {
false, // OP_CHANGE_WIFI_STATE
false, // OP_REQUEST_DELETE_PACKAGES
false, // OP_BIND_ACCESSIBILITY_SERVICE
+ false, // ACCEPT_HANDOVER
};
/**
@@ -1207,6 +1343,25 @@ public class AppOpsManager {
}
/**
+ * Retrieve the human readable mode.
+ * @hide
+ */
+ public static String modeToString(int mode) {
+ switch (mode) {
+ case MODE_ALLOWED:
+ return "allow";
+ case MODE_IGNORED:
+ return "ignore";
+ case MODE_ERRORED:
+ return "deny";
+ case MODE_DEFAULT:
+ return "default";
+ default:
+ return "mode=" + mode;
+ }
+ }
+
+ /**
* Retrieve whether the op allows itself to be reset.
* @hide
*/
@@ -1482,6 +1637,7 @@ public class AppOpsManager {
}
/** @hide */
+ @TestApi
public void setMode(int code, int uid, String packageName, int mode) {
try {
mService.setMode(code, uid, packageName, mode);
@@ -1997,4 +2153,14 @@ public class AppOpsManager {
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Returns all supported operation names.
+ * @hide
+ */
+ @SystemApi
+ @TestApi
+ public static String[] getOpStrs() {
+ return Arrays.copyOf(sOpToString, sOpToString.length);
+ }
}
diff --git a/android/app/ApplicationPackageManager.java b/android/app/ApplicationPackageManager.java
index 8641a21a..cc68c051 100644
--- a/android/app/ApplicationPackageManager.java
+++ b/android/app/ApplicationPackageManager.java
@@ -64,7 +64,6 @@ import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.LayerDrawable;
-import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
@@ -223,9 +222,18 @@ public class ApplicationPackageManager extends PackageManager {
@Override
public Intent getLeanbackLaunchIntentForPackage(String packageName) {
- // Try to find a main leanback_launcher activity.
+ return getLaunchIntentForPackageAndCategory(packageName, Intent.CATEGORY_LEANBACK_LAUNCHER);
+ }
+
+ @Override
+ public Intent getCarLaunchIntentForPackage(String packageName) {
+ return getLaunchIntentForPackageAndCategory(packageName, Intent.CATEGORY_CAR_LAUNCHER);
+ }
+
+ private Intent getLaunchIntentForPackageAndCategory(String packageName, String category) {
+ // Try to find a main launcher activity for the given categories.
Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
- intentToResolve.addCategory(Intent.CATEGORY_LEANBACK_LAUNCHER);
+ intentToResolve.addCategory(category);
intentToResolve.setPackage(packageName);
List<ResolveInfo> ris = queryIntentActivities(intentToResolve, 0);
@@ -691,6 +699,26 @@ public class ApplicationPackageManager extends PackageManager {
}
@Override
+ public boolean hasSigningCertificate(
+ String packageName, byte[] certificate, @PackageManager.CertificateInputType int type) {
+ try {
+ return mPM.hasSigningCertificate(packageName, certificate, type);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
+ public boolean hasSigningCertificate(
+ int uid, byte[] certificate, @PackageManager.CertificateInputType int type) {
+ try {
+ return mPM.hasUidSigningCertificate(uid, certificate, type);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ @Override
public String[] getPackagesForUid(int uid) {
try {
return mPM.getPackagesForUid(uid);
@@ -1683,22 +1711,6 @@ public class ApplicationPackageManager extends PackageManager {
}
@Override
- public void installPackage(Uri packageURI,
- PackageInstallObserver observer, int flags, String installerPackageName) {
- if (!"file".equals(packageURI.getScheme())) {
- throw new UnsupportedOperationException("Only file:// URIs are supported");
- }
-
- final String originPath = packageURI.getPath();
- try {
- mPM.installPackageAsUser(originPath, observer.getBinder(), flags, installerPackageName,
- mContext.getUserId());
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- @Override
public int installExistingPackage(String packageName) throws NameNotFoundException {
return installExistingPackage(packageName, PackageManager.INSTALL_REASON_UNKNOWN);
}
@@ -2755,6 +2767,24 @@ public class ApplicationPackageManager extends PackageManager {
}
@Override
+ public CharSequence getHarmfulAppWarning(String packageName) {
+ try {
+ return mPM.getHarmfulAppWarning(packageName, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
+ public void setHarmfulAppWarning(String packageName, CharSequence warning) {
+ try {
+ mPM.setHarmfulAppWarning(packageName, warning, mContext.getUserId());
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ @Override
public ArtManager getArtManager() {
synchronized (mLock) {
if (mArtManager == null) {
diff --git a/android/app/ClientTransactionHandler.java b/android/app/ClientTransactionHandler.java
index 45c0e0cd..0f66652a 100644
--- a/android/app/ClientTransactionHandler.java
+++ b/android/app/ClientTransactionHandler.java
@@ -24,6 +24,7 @@ import android.os.IBinder;
import com.android.internal.content.ReferrerIntent;
+import java.io.PrintWriter;
import java.util.List;
/**
@@ -121,4 +122,11 @@ public abstract class ClientTransactionHandler {
* provided token.
*/
public abstract ActivityThread.ActivityClientRecord getActivityClient(IBinder token);
+
+ /**
+ * Debugging output.
+ * @param pw {@link PrintWriter} to write logs to.
+ * @param prefix Prefix to prepend to output.
+ */
+ public abstract void dump(PrintWriter pw, String prefix);
}
diff --git a/android/app/ContextImpl.java b/android/app/ContextImpl.java
index 16534305..4914ffaf 100644
--- a/android/app/ContextImpl.java
+++ b/android/app/ContextImpl.java
@@ -872,13 +872,19 @@ class ContextImpl extends Context {
// Calling start activity from outside an activity without FLAG_ACTIVITY_NEW_TASK is
// generally not allowed, except if the caller specifies the task id the activity should
- // be launched in.
- if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0
- && options != null && ActivityOptions.fromBundle(options).getLaunchTaskId() == -1) {
+ // be launched in. A bug was existed between N and O-MR1 which allowed this to work. We
+ // maintain this for backwards compatibility.
+ final int targetSdkVersion = getApplicationInfo().targetSdkVersion;
+
+ if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
+ && (targetSdkVersion < Build.VERSION_CODES.N
+ || targetSdkVersion >= Build.VERSION_CODES.P)
+ && (options == null
+ || ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
- + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
- + " Is this really what you want?");
+ + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ + " Is this really what you want?");
}
mMainThread.getInstrumentation().execStartActivity(
getOuterContext(), mMainThread.getApplicationThread(), null,
diff --git a/android/app/Dialog.java b/android/app/Dialog.java
index b162cb16..2b648ea6 100644
--- a/android/app/Dialog.java
+++ b/android/app/Dialog.java
@@ -16,10 +16,6 @@
package android.app;
-import com.android.internal.R;
-import com.android.internal.app.WindowDecorActionBar;
-import com.android.internal.policy.PhoneWindow;
-
import android.annotation.CallSuper;
import android.annotation.DrawableRes;
import android.annotation.IdRes;
@@ -32,8 +28,8 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.DialogInterface;
-import android.content.res.Configuration;
import android.content.pm.ApplicationInfo;
+import android.content.res.Configuration;
import android.content.res.ResourceId;
import android.graphics.drawable.Drawable;
import android.net.Uri;
@@ -62,6 +58,10 @@ import android.view.Window;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
+import com.android.internal.R;
+import com.android.internal.app.WindowDecorActionBar;
+import com.android.internal.policy.PhoneWindow;
+
import java.lang.ref.WeakReference;
/**
@@ -512,6 +512,7 @@ public class Dialog implements DialogInterface, Window.Callback,
* @param id the ID to search for
* @return a view with given ID if found, or {@code null} otherwise
* @see View#findViewById(int)
+ * @see Dialog#requireViewById(int)
*/
@Nullable
public <T extends View> T findViewById(@IdRes int id) {
@@ -519,6 +520,30 @@ public class Dialog implements DialogInterface, Window.Callback,
}
/**
+ * Finds the first descendant view with the given ID or throws an IllegalArgumentException if
+ * the ID is invalid (< 0), there is no matching view in the hierarchy, or the dialog has not
+ * yet been fully created (for example, via {@link #show()} or {@link #create()}).
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#requireViewById(int)
+ * @see Dialog#findViewById(int)
+ */
+ @NonNull
+ public final <T extends View> T requireViewById(@IdRes int id) {
+ T view = findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this Dialog");
+ }
+ return view;
+ }
+
+ /**
* Set the screen content from a layout resource. The resource will be
* inflated, adding all top-level views to the screen.
*
diff --git a/android/app/Instrumentation.java b/android/app/Instrumentation.java
index b469de56..3c38a4ec 100644
--- a/android/app/Instrumentation.java
+++ b/android/app/Instrumentation.java
@@ -17,6 +17,7 @@
package android.app;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
@@ -458,7 +459,8 @@ public class Instrumentation {
*
* @see Context#startActivity(Intent, Bundle)
*/
- public Activity startActivitySync(Intent intent, @Nullable Bundle options) {
+ @NonNull
+ public Activity startActivitySync(@NonNull Intent intent, @Nullable Bundle options) {
validateNotAppThread();
synchronized (mSync) {
@@ -1872,8 +1874,8 @@ public class Instrumentation {
*/
public ActivityResult execStartActivityAsCaller(
Context who, IBinder contextThread, IBinder token, Activity target,
- Intent intent, int requestCode, Bundle options, boolean ignoreTargetSecurity,
- int userId) {
+ Intent intent, int requestCode, Bundle options, IBinder permissionToken,
+ boolean ignoreTargetSecurity, int userId) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
if (mActivityMonitors != null) {
synchronized (mSync) {
@@ -1904,7 +1906,8 @@ public class Instrumentation {
.startActivityAsCaller(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
- requestCode, 0, null, options, ignoreTargetSecurity, userId);
+ requestCode, 0, null, options, permissionToken,
+ ignoreTargetSecurity, userId);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
diff --git a/android/app/KeyguardManager.java b/android/app/KeyguardManager.java
index d0f84c8e..553099f2 100644
--- a/android/app/KeyguardManager.java
+++ b/android/app/KeyguardManager.java
@@ -20,6 +20,7 @@ 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.trust.ITrustManager;
import android.content.Context;
@@ -166,24 +167,26 @@ public class KeyguardManager {
* clicking this button, the activity returns
* {@link #RESULT_ALTERNATE}
*
- * @return the intent for launching the activity or null if the credential of the previous
- * owner can not be verified (e.g. because there was none, or the device does not support
- * verifying credentials after a factory reset, or device setup has already been completed).
- *
+ * @return the intent for launching the activity or null if the previous owner of the device
+ * did not set a credential.
+ * @throws UnsupportedOperationException if the device does not support factory reset
+ * credentials
+ * @throws IllegalStateException if the device has already been provisioned
* @hide
*/
+ @SystemApi
public Intent createConfirmFactoryResetCredentialIntent(
CharSequence title, CharSequence description, CharSequence alternateButtonLabel) {
if (!LockPatternUtils.frpCredentialEnabled(mContext)) {
Log.w(TAG, "Factory reset credentials not supported.");
- return null;
+ throw new UnsupportedOperationException("not supported on this device");
}
// Cannot verify credential if the device is provisioned
if (Settings.Global.getInt(mContext.getContentResolver(),
Settings.Global.DEVICE_PROVISIONED, 0) != 0) {
Log.e(TAG, "Factory reset credential cannot be verified after provisioning.");
- return null;
+ throw new IllegalStateException("must not be provisioned yet");
}
// Make sure we have a credential
@@ -192,8 +195,10 @@ public class KeyguardManager {
ServiceManager.getService(Context.PERSISTENT_DATA_BLOCK_SERVICE));
if (pdb == null) {
Log.e(TAG, "No persistent data block service");
- return null;
+ throw new UnsupportedOperationException("not supported on this device");
}
+ // The following will throw an UnsupportedOperationException if the device does not
+ // support factory reset credentials (or something went wrong retrieving it).
if (!pdb.hasFrpCredentialHandle()) {
Log.i(TAG, "The persistent data block does not have a factory reset credential.");
return null;
@@ -475,6 +480,39 @@ public class KeyguardManager {
*/
public void requestDismissKeyguard(@NonNull Activity activity,
@Nullable KeyguardDismissCallback callback) {
+ requestDismissKeyguard(activity, null /* message */, callback);
+ }
+
+ /**
+ * If the device is currently locked (see {@link #isKeyguardLocked()}, requests the Keyguard to
+ * be dismissed.
+ * <p>
+ * If the Keyguard is not secure or the device is currently in a trusted state, calling this
+ * method will immediately dismiss the Keyguard without any user interaction.
+ * <p>
+ * If the Keyguard is secure and the device is not in a trusted state, this will bring up the
+ * UI so the user can enter their credentials.
+ * <p>
+ * If the value set for the {@link Activity} attr {@link android.R.attr#turnScreenOn} is true,
+ * the screen will turn on when the keyguard is dismissed.
+ *
+ * @param activity The activity requesting the dismissal. The activity must be either visible
+ * by using {@link LayoutParams#FLAG_SHOW_WHEN_LOCKED} or must be in a state in
+ * which it would be visible if Keyguard would not be hiding it. If that's not
+ * the case, the request will fail immediately and
+ * {@link KeyguardDismissCallback#onDismissError} will be invoked.
+ * @param message A message that will be shown in the keyguard explaining why the user
+ * would want to dismiss it.
+ * @param callback The callback to be called if the request to dismiss Keyguard was successful
+ * or {@code null} if the caller isn't interested in knowing the result. The
+ * callback will not be invoked if the activity was destroyed before the
+ * callback was received.
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.SHOW_KEYGUARD_MESSAGE)
+ @SystemApi
+ public void requestDismissKeyguard(@NonNull Activity activity, @Nullable CharSequence message,
+ @Nullable KeyguardDismissCallback callback) {
try {
mAm.dismissKeyguard(activity.getActivityToken(), new IKeyguardDismissCallback.Stub() {
@Override
@@ -497,9 +535,9 @@ public class KeyguardManager {
activity.mHandler.post(callback::onDismissCancelled);
}
}
- });
+ }, message);
} catch (RemoteException e) {
- Log.i(TAG, "Failed to dismiss keyguard: " + e);
+ throw e.rethrowFromSystemServer();
}
}
diff --git a/android/app/Notification.java b/android/app/Notification.java
index 85c3be82..d6fddfca 100644
--- a/android/app/Notification.java
+++ b/android/app/Notification.java
@@ -1022,10 +1022,18 @@ public class Notification implements Parcelable
/**
* {@link #extras} key: A String array containing the people that this notification relates to,
* each of which was supplied to {@link Builder#addPerson(String)}.
+ *
+ * @deprecated the actual objects are now in {@link #EXTRA_PEOPLE_LIST}
*/
public static final String EXTRA_PEOPLE = "android.people";
/**
+ * {@link #extras} key: An arrayList of {@link Person} objects containing the people that
+ * this notification relates to.
+ */
+ public static final String EXTRA_PEOPLE_LIST = "android.people.list";
+
+ /**
* Allow certain system-generated notifications to appear before the device is provisioned.
* Only available to notifications coming from the android package.
* @hide
@@ -1063,10 +1071,20 @@ public class Notification implements Parcelable
* direct replies
* {@link android.app.Notification.MessagingStyle} notification. This extra is a
* {@link CharSequence}
+ *
+ * @deprecated use {@link #EXTRA_MESSAGING_PERSON}
*/
public static final String EXTRA_SELF_DISPLAY_NAME = "android.selfDisplayName";
/**
+ * {@link #extras} key: the person to be displayed for all messages sent by the user including
+ * direct replies
+ * {@link android.app.Notification.MessagingStyle} notification. This extra is a
+ * {@link Person}
+ */
+ public static final String EXTRA_MESSAGING_PERSON = "android.messagingUser";
+
+ /**
* {@link #extras} key: a {@link CharSequence} to be displayed as the title to a conversation
* represented by a {@link android.app.Notification.MessagingStyle}
*/
@@ -1250,10 +1268,67 @@ public class Notification implements Parcelable
*/
private static final String EXTRA_DATA_ONLY_INPUTS = "android.extra.DATA_ONLY_INPUTS";
+ /**
+ * {@link }: No semantic action defined.
+ */
+ public static final int SEMANTIC_ACTION_NONE = 0;
+
+ /**
+ * {@code SemanticAction}: Reply to a conversation, chat, group, or wherever replies
+ * may be appropriate.
+ */
+ public static final int SEMANTIC_ACTION_REPLY = 1;
+
+ /**
+ * {@code SemanticAction}: Mark content as read.
+ */
+ public static final int SEMANTIC_ACTION_MARK_AS_READ = 2;
+
+ /**
+ * {@code SemanticAction}: Mark content as unread.
+ */
+ public static final int SEMANTIC_ACTION_MARK_AS_UNREAD = 3;
+
+ /**
+ * {@code SemanticAction}: Delete the content associated with the notification. This
+ * could mean deleting an email, message, etc.
+ */
+ public static final int SEMANTIC_ACTION_DELETE = 4;
+
+ /**
+ * {@code SemanticAction}: Archive the content associated with the notification. This
+ * could mean archiving an email, message, etc.
+ */
+ public static final int SEMANTIC_ACTION_ARCHIVE = 5;
+
+ /**
+ * {@code SemanticAction}: Mute the content associated with the notification. This could
+ * mean silencing a conversation or currently playing media.
+ */
+ public static final int SEMANTIC_ACTION_MUTE = 6;
+
+ /**
+ * {@code SemanticAction}: Unmute the content associated with the notification. This could
+ * mean un-silencing a conversation or currently playing media.
+ */
+ public static final int SEMANTIC_ACTION_UNMUTE = 7;
+
+ /**
+ * {@code SemanticAction}: Mark content with a thumbs up.
+ */
+ public static final int SEMANTIC_ACTION_THUMBS_UP = 8;
+
+ /**
+ * {@code SemanticAction}: Mark content with a thumbs down.
+ */
+ public static final int SEMANTIC_ACTION_THUMBS_DOWN = 9;
+
+
private final Bundle mExtras;
private Icon mIcon;
private final RemoteInput[] mRemoteInputs;
private boolean mAllowGeneratedReplies = true;
+ private final @SemanticAction int mSemanticAction;
/**
* Small icon representing the action.
@@ -1288,6 +1363,7 @@ public class Notification implements Parcelable
mExtras = Bundle.setDefusable(in.readBundle(), true);
mRemoteInputs = in.createTypedArray(RemoteInput.CREATOR);
mAllowGeneratedReplies = in.readInt() == 1;
+ mSemanticAction = in.readInt();
}
/**
@@ -1295,12 +1371,14 @@ public class Notification implements Parcelable
*/
@Deprecated
public Action(int icon, CharSequence title, PendingIntent intent) {
- this(Icon.createWithResource("", icon), title, intent, new Bundle(), null, true);
+ this(Icon.createWithResource("", icon), title, intent, new Bundle(), null, true,
+ SEMANTIC_ACTION_NONE);
}
/** Keep in sync with {@link Notification.Action.Builder#Builder(Action)}! */
private Action(Icon icon, CharSequence title, PendingIntent intent, Bundle extras,
- RemoteInput[] remoteInputs, boolean allowGeneratedReplies) {
+ RemoteInput[] remoteInputs, boolean allowGeneratedReplies,
+ @SemanticAction int semanticAction) {
this.mIcon = icon;
if (icon != null && icon.getType() == Icon.TYPE_RESOURCE) {
this.icon = icon.getResId();
@@ -1310,6 +1388,7 @@ public class Notification implements Parcelable
this.mExtras = extras != null ? extras : new Bundle();
this.mRemoteInputs = remoteInputs;
this.mAllowGeneratedReplies = allowGeneratedReplies;
+ this.mSemanticAction = semanticAction;
}
/**
@@ -1348,6 +1427,15 @@ public class Notification implements Parcelable
}
/**
+ * Returns the {@code SemanticAction} associated with this {@link Action}. A
+ * {@code SemanticAction} denotes what an {@link Action}'s {@link PendingIntent} will do
+ * (eg. reply, mark as read, delete, etc).
+ */
+ public @SemanticAction int getSemanticAction() {
+ return mSemanticAction;
+ }
+
+ /**
* Get the list of inputs to be collected from the user that ONLY accept data when this
* action is sent. These remote inputs are guaranteed to return true on a call to
* {@link RemoteInput#isDataOnly}.
@@ -1371,6 +1459,7 @@ public class Notification implements Parcelable
private boolean mAllowGeneratedReplies = true;
private final Bundle mExtras;
private ArrayList<RemoteInput> mRemoteInputs;
+ private @SemanticAction int mSemanticAction;
/**
* Construct a new builder for {@link Action} object.
@@ -1390,7 +1479,7 @@ public class Notification implements Parcelable
* @param intent the {@link PendingIntent} to fire when users trigger this action
*/
public Builder(Icon icon, CharSequence title, PendingIntent intent) {
- this(icon, title, intent, new Bundle(), null, true);
+ this(icon, title, intent, new Bundle(), null, true, SEMANTIC_ACTION_NONE);
}
/**
@@ -1401,11 +1490,12 @@ public class Notification implements Parcelable
public Builder(Action action) {
this(action.getIcon(), action.title, action.actionIntent,
new Bundle(action.mExtras), action.getRemoteInputs(),
- action.getAllowGeneratedReplies());
+ action.getAllowGeneratedReplies(), action.getSemanticAction());
}
private Builder(Icon icon, CharSequence title, PendingIntent intent, Bundle extras,
- RemoteInput[] remoteInputs, boolean allowGeneratedReplies) {
+ RemoteInput[] remoteInputs, boolean allowGeneratedReplies,
+ @SemanticAction int semanticAction) {
mIcon = icon;
mTitle = title;
mIntent = intent;
@@ -1415,6 +1505,7 @@ public class Notification implements Parcelable
Collections.addAll(mRemoteInputs, remoteInputs);
}
mAllowGeneratedReplies = allowGeneratedReplies;
+ mSemanticAction = semanticAction;
}
/**
@@ -1470,6 +1561,19 @@ public class Notification implements Parcelable
}
/**
+ * Sets the {@code SemanticAction} for this {@link Action}. A
+ * {@code SemanticAction} denotes what an {@link Action}'s
+ * {@link PendingIntent} will do (eg. reply, mark as read, delete, etc).
+ * @param semanticAction a SemanticAction defined within {@link Action} with
+ * {@code SEMANTIC_ACTION_} prefixes
+ * @return this object for method chaining
+ */
+ public Builder setSemanticAction(@SemanticAction int semanticAction) {
+ mSemanticAction = semanticAction;
+ return this;
+ }
+
+ /**
* Apply an extender to this action builder. Extenders may be used to add
* metadata or change options on this builder.
*/
@@ -1510,7 +1614,7 @@ public class Notification implements Parcelable
RemoteInput[] textInputsArr = textInputs.isEmpty()
? null : textInputs.toArray(new RemoteInput[textInputs.size()]);
return new Action(mIcon, mTitle, mIntent, mExtras, textInputsArr,
- mAllowGeneratedReplies);
+ mAllowGeneratedReplies, mSemanticAction);
}
}
@@ -1522,12 +1626,15 @@ public class Notification implements Parcelable
actionIntent, // safe to alias
mExtras == null ? new Bundle() : new Bundle(mExtras),
getRemoteInputs(),
- getAllowGeneratedReplies());
+ getAllowGeneratedReplies(),
+ getSemanticAction());
}
+
@Override
public int describeContents() {
return 0;
}
+
@Override
public void writeToParcel(Parcel out, int flags) {
final Icon ic = getIcon();
@@ -1547,7 +1654,9 @@ public class Notification implements Parcelable
out.writeBundle(mExtras);
out.writeTypedArray(mRemoteInputs, flags);
out.writeInt(mAllowGeneratedReplies ? 1 : 0);
+ out.writeInt(mSemanticAction);
}
+
public static final Parcelable.Creator<Action> CREATOR =
new Parcelable.Creator<Action>() {
public Action createFromParcel(Parcel in) {
@@ -1809,6 +1918,29 @@ public class Notification implements Parcelable
return (mFlags & FLAG_HINT_DISPLAY_INLINE) != 0;
}
}
+
+ /**
+ * Provides meaning to an {@link Action} that hints at what the associated
+ * {@link PendingIntent} will do. For example, an {@link Action} with a
+ * {@link PendingIntent} that replies to a text message notification may have the
+ * {@link #SEMANTIC_ACTION_REPLY} {@code SemanticAction} set within it.
+ *
+ * @hide
+ */
+ @IntDef(prefix = { "SEMANTIC_ACTION_" }, value = {
+ SEMANTIC_ACTION_NONE,
+ SEMANTIC_ACTION_REPLY,
+ SEMANTIC_ACTION_MARK_AS_READ,
+ SEMANTIC_ACTION_MARK_AS_UNREAD,
+ SEMANTIC_ACTION_DELETE,
+ SEMANTIC_ACTION_ARCHIVE,
+ SEMANTIC_ACTION_MUTE,
+ SEMANTIC_ACTION_UNMUTE,
+ SEMANTIC_ACTION_THUMBS_UP,
+ SEMANTIC_ACTION_THUMBS_DOWN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SemanticAction {}
}
/**
@@ -2819,7 +2951,7 @@ public class Notification implements Parcelable
private Bundle mUserExtras = new Bundle();
private Style mStyle;
private ArrayList<Action> mActions = new ArrayList<Action>(MAX_ACTION_BUTTONS);
- private ArrayList<String> mPersonList = new ArrayList<String>();
+ private ArrayList<Person> mPersonList = new ArrayList<>();
private NotificationColorUtil mColorUtil;
private boolean mIsLegacy;
private boolean mIsLegacyInitialized;
@@ -2910,8 +3042,9 @@ public class Notification implements Parcelable
Collections.addAll(mActions, mN.actions);
}
- if (mN.extras.containsKey(EXTRA_PEOPLE)) {
- Collections.addAll(mPersonList, mN.extras.getStringArray(EXTRA_PEOPLE));
+ if (mN.extras.containsKey(EXTRA_PEOPLE_LIST)) {
+ ArrayList<Person> people = mN.extras.getParcelableArrayList(EXTRA_PEOPLE_LIST);
+ mPersonList.addAll(people);
}
if (mN.getSmallIcon() == null && mN.icon != 0) {
@@ -3621,13 +3754,41 @@ public class Notification implements Parcelable
* URIs. The path part of these URIs must exist in the contacts database, in the
* appropriate column, or the reference will be discarded as invalid. Telephone schema
* URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}.
+ * It is also possible to provide a URI with the schema {@code name:} in order to uniquely
+ * identify a person without an entry in the contacts database.
* </P>
*
* @param uri A URI for the person.
* @see Notification#EXTRA_PEOPLE
+ * @deprecated use {@link #addPerson(Person)}
*/
public Builder addPerson(String uri) {
- mPersonList.add(uri);
+ addPerson(new Person().setUri(uri));
+ return this;
+ }
+
+ /**
+ * Add a person that is relevant to this notification.
+ *
+ * <P>
+ * Depending on user preferences, this annotation may allow the notification to pass
+ * through interruption filters, if this notification is of category {@link #CATEGORY_CALL}
+ * or {@link #CATEGORY_MESSAGE}. The addition of people may also cause this notification to
+ * appear more prominently in the user interface.
+ * </P>
+ *
+ * <P>
+ * A person should usually contain a uri in order to benefit from the ranking boost.
+ * However, even if no uri is provided, it's beneficial to provide other people in the
+ * notification, such that listeners and voice only devices can announce and handle them
+ * properly.
+ * </P>
+ *
+ * @param person the person to add.
+ * @see Notification#EXTRA_PEOPLE_LIST
+ */
+ public Builder addPerson(Person person) {
+ mPersonList.add(person);
return this;
}
@@ -3934,7 +4095,10 @@ public class Notification implements Parcelable
contentView.setViewVisibility(R.id.chronometer, View.GONE);
contentView.setViewVisibility(R.id.header_text, View.GONE);
contentView.setTextViewText(R.id.header_text, null);
+ contentView.setViewVisibility(R.id.header_text_secondary, View.GONE);
+ contentView.setTextViewText(R.id.header_text_secondary, null);
contentView.setViewVisibility(R.id.header_text_divider, View.GONE);
+ contentView.setViewVisibility(R.id.header_text_secondary_divider, View.GONE);
contentView.setViewVisibility(R.id.time_divider, View.GONE);
contentView.setViewVisibility(R.id.time, View.GONE);
contentView.setImageViewIcon(R.id.profile_badge, null);
@@ -3965,8 +4129,8 @@ public class Notification implements Parcelable
final Bundle ex = mN.extras;
updateBackgroundColor(contentView);
- bindNotificationHeader(contentView, p.ambient);
- bindLargeIcon(contentView, p.hideLargeIcon, p.alwaysShowReply);
+ bindNotificationHeader(contentView, p.ambient, p.headerTextSecondary);
+ bindLargeIcon(contentView, p.hideLargeIcon || p.ambient, p.alwaysShowReply);
boolean showProgress = handleProgressBar(p.hasProgress, contentView, ex);
if (p.title != null) {
contentView.setViewVisibility(R.id.title, View.VISIBLE);
@@ -4248,12 +4412,14 @@ public class Notification implements Parcelable
return null;
}
- private void bindNotificationHeader(RemoteViews contentView, boolean ambient) {
+ private void bindNotificationHeader(RemoteViews contentView, boolean ambient,
+ CharSequence secondaryHeaderText) {
bindSmallIcon(contentView, ambient);
bindHeaderAppName(contentView, ambient);
if (!ambient) {
// Ambient view does not have these
bindHeaderText(contentView);
+ bindHeaderTextSecondary(contentView, secondaryHeaderText);
bindHeaderChronometerAndTime(contentView);
bindProfileBadge(contentView);
}
@@ -4322,6 +4488,17 @@ public class Notification implements Parcelable
}
}
+ private void bindHeaderTextSecondary(RemoteViews contentView, CharSequence secondaryText) {
+ if (!TextUtils.isEmpty(secondaryText)) {
+ contentView.setTextViewText(R.id.header_text_secondary, processTextSpans(
+ processLegacyText(secondaryText)));
+ setTextViewColorSecondary(contentView, R.id.header_text_secondary);
+ contentView.setViewVisibility(R.id.header_text_secondary, View.VISIBLE);
+ contentView.setViewVisibility(R.id.header_text_secondary_divider, View.VISIBLE);
+ setTextViewColorSecondary(contentView, R.id.header_text_secondary_divider);
+ }
+ }
+
/**
* @hide
*/
@@ -4555,7 +4732,7 @@ public class Notification implements Parcelable
ambient ? R.layout.notification_template_ambient_header
: R.layout.notification_template_header);
resetNotificationHeader(header);
- bindNotificationHeader(header, ambient);
+ bindNotificationHeader(header, ambient, null);
if (colorized != null) {
mN.extras.putBoolean(EXTRA_COLORIZED, colorized);
} else {
@@ -4968,8 +5145,7 @@ public class Notification implements Parcelable
mActions.toArray(mN.actions);
}
if (!mPersonList.isEmpty()) {
- mN.extras.putStringArray(EXTRA_PEOPLE,
- mPersonList.toArray(new String[mPersonList.size()]));
+ mN.extras.putParcelableArrayList(EXTRA_PEOPLE_LIST, mPersonList);
}
if (mN.bigContentView != null || mN.contentView != null
|| mN.headsUpContentView != null) {
@@ -5965,7 +6141,7 @@ public class Notification implements Parcelable
*/
public static final int MAXIMUM_RETAINED_MESSAGES = 25;
- CharSequence mUserDisplayName;
+ @NonNull Person mUser;
@Nullable CharSequence mConversationTitle;
List<Message> mMessages = new ArrayList<>();
List<Message> mHistoricMessages = new ArrayList<>();
@@ -5979,23 +6155,54 @@ public class Notification implements Parcelable
* user before the posting app reposts the notification with those messages after they've
* been actually sent and in previous messages sent by the user added in
* {@link #addMessage(Notification.MessagingStyle.Message)}
+ *
+ * @deprecated use {@code MessagingStyle(Person)}
*/
public MessagingStyle(@NonNull CharSequence userDisplayName) {
- mUserDisplayName = userDisplayName;
+ this(new Person().setName(userDisplayName));
+ }
+
+ /**
+ * @param user Required - The person displayed for any messages that are sent by the
+ * user. Any messages added with {@link #addMessage(Notification.MessagingStyle.Message)}
+ * who don't have a Person associated with it will be displayed as if they were sent
+ * by this user. The user also needs to have a valid name associated with it.
+ */
+ public MessagingStyle(@NonNull Person user) {
+ mUser = user;
+ if (user == null || user.getName() == null) {
+ throw new RuntimeException("user must be valid and have a name");
+ }
+ }
+
+ /**
+ * @return the user to be displayed for any replies sent by the user
+ */
+ public Person getUser() {
+ return mUser;
}
/**
* Returns the name to be displayed for any replies sent by the user
+ *
+ * @deprecated use {@link #getUser()} instead
*/
public CharSequence getUserDisplayName() {
- return mUserDisplayName;
+ return mUser.getName();
}
/**
* 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.
+ * <p>This API's behavior was changed in SDK version {@link Build.VERSION_CODES#P}. If your
+ * application's target version is less than {@link Build.VERSION_CODES#P}, setting a
+ * conversation title to a non-null value will make {@link #isGroupConversation()} return
+ * {@code true} and passing {@code null} will make it return {@code false}. In
+ * {@link Build.VERSION_CODES#P} and beyond, use {@link #setGroupConversation(boolean)}
+ * to set group conversation status.
+ *
+ * @param conversationTitle Title displayed for this conversation
+ * @return this object for method chaining
*/
public MessagingStyle setConversationTitle(@Nullable CharSequence conversationTitle) {
mConversationTitle = conversationTitle;
@@ -6024,8 +6231,28 @@ public class Notification implements Parcelable
* @see Message#Message(CharSequence, long, CharSequence)
*
* @return this object for method chaining
+ *
+ * @deprecated use {@link #addMessage(CharSequence, long, Person)}
*/
public MessagingStyle addMessage(CharSequence text, long timestamp, CharSequence sender) {
+ return addMessage(text, timestamp,
+ sender == null ? null : new Person().setName(sender));
+ }
+
+ /**
+ * Adds a message for display by this notification. Convenience call for a simple
+ * {@link Message} in {@link #addMessage(Notification.MessagingStyle.Message)}.
+ * @param text A {@link CharSequence} to be displayed as the message content
+ * @param timestamp Time at which the message arrived
+ * @param sender The {@link Person} who sent the message.
+ * Should be <code>null</code> for messages by the current user, in which case
+ * the platform will insert the user set in {@code MessagingStyle(Person)}.
+ *
+ * @see Message#Message(CharSequence, long, CharSequence)
+ *
+ * @return this object for method chaining
+ */
+ public MessagingStyle addMessage(CharSequence text, long timestamp, Person sender) {
return addMessage(new Message(text, timestamp, sender));
}
@@ -6083,6 +6310,7 @@ 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
@@ -6093,9 +6321,27 @@ public class Notification implements Parcelable
}
/**
- * Returns {@code true} if this notification represents a group conversation.
+ * Returns {@code true} if this notification represents a group conversation, otherwise
+ * {@code false}.
+ *
+ * <p> If the application that generated this {@link MessagingStyle} targets an SDK version
+ * less than {@link Build.VERSION_CODES#P}, this method becomes dependent on whether or
+ * not the conversation title is set; returning {@code true} if the conversation title is
+ * a non-null value, or {@code false} otherwise. From {@link Build.VERSION_CODES#P} forward,
+ * this method returns what's set by {@link #setGroupConversation(boolean)} allowing for
+ * named, non-group conversations.
+ *
+ * @see #setConversationTitle(CharSequence)
*/
public boolean isGroupConversation() {
+ // When target SDK version is < P, a non-null conversation title dictates if this is
+ // as group conversation.
+ if (mBuilder != null
+ && mBuilder.mContext.getApplicationInfo().targetSdkVersion
+ < Build.VERSION_CODES.P) {
+ return mConversationTitle != null;
+ }
+
return mIsGroupConversation;
}
@@ -6105,8 +6351,10 @@ public class Notification implements Parcelable
@Override
public void addExtras(Bundle extras) {
super.addExtras(extras);
- if (mUserDisplayName != null) {
- extras.putCharSequence(EXTRA_SELF_DISPLAY_NAME, mUserDisplayName);
+ if (mUser != null) {
+ // For legacy usages
+ extras.putCharSequence(EXTRA_SELF_DISPLAY_NAME, mUser.getName());
+ extras.putParcelable(EXTRA_MESSAGING_PERSON, mUser);
}
if (mConversationTitle != null) {
extras.putCharSequence(EXTRA_CONVERSATION_TITLE, mConversationTitle);
@@ -6126,14 +6374,15 @@ public class Notification implements Parcelable
Message m = findLatestIncomingMessage();
CharSequence text = (m == null) ? null : m.mText;
CharSequence sender = m == null ? null
- : TextUtils.isEmpty(m.mSender) ? mUserDisplayName : m.mSender;
+ : m.mSender == null || TextUtils.isEmpty(m.mSender.getName())
+ ? mUser.getName() : m.mSender.getName();
CharSequence title;
if (!TextUtils.isEmpty(mConversationTitle)) {
if (!TextUtils.isEmpty(sender)) {
BidiFormatter bidi = BidiFormatter.getInstance();
title = mBuilder.mContext.getString(
com.android.internal.R.string.notification_messaging_title_template,
- bidi.unicodeWrap(mConversationTitle), bidi.unicodeWrap(m.mSender));
+ bidi.unicodeWrap(mConversationTitle), bidi.unicodeWrap(sender));
} else {
title = mConversationTitle;
}
@@ -6156,7 +6405,11 @@ public class Notification implements Parcelable
protected void restoreFromExtras(Bundle extras) {
super.restoreFromExtras(extras);
- mUserDisplayName = extras.getCharSequence(EXTRA_SELF_DISPLAY_NAME);
+ mUser = extras.getParcelable(EXTRA_MESSAGING_PERSON);
+ if (mUser == null) {
+ CharSequence displayName = extras.getCharSequence(EXTRA_SELF_DISPLAY_NAME);
+ mUser = new Person().setName(displayName);
+ }
mConversationTitle = extras.getCharSequence(EXTRA_CONVERSATION_TITLE);
Parcelable[] messages = extras.getParcelableArray(EXTRA_MESSAGES);
mMessages = Message.getMessagesFromBundleArray(messages);
@@ -6172,7 +6425,7 @@ public class Notification implements Parcelable
public RemoteViews makeContentView(boolean increasedHeight) {
mBuilder.mOriginalActions = mBuilder.mActions;
mBuilder.mActions = new ArrayList<>();
- RemoteViews remoteViews = makeBigContentView();
+ RemoteViews remoteViews = makeBigContentView(true /* showRightIcon */);
mBuilder.mActions = mBuilder.mOriginalActions;
mBuilder.mOriginalActions = null;
return remoteViews;
@@ -6191,7 +6444,7 @@ public class Notification implements Parcelable
for (int i = messages.size() - 1; i >= 0; i--) {
Message m = messages.get(i);
// Incoming messages have a non-empty sender.
- if (!TextUtils.isEmpty(m.mSender)) {
+ if (m.mSender != null && !TextUtils.isEmpty(m.mSender.getName())) {
return m;
}
}
@@ -6207,27 +6460,40 @@ public class Notification implements Parcelable
*/
@Override
public RemoteViews makeBigContentView() {
+ return makeBigContentView(false /* showRightIcon */);
+ }
+
+ @NonNull
+ private RemoteViews makeBigContentView(boolean showRightIcon) {
CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle)
? super.mBigContentTitle
: mConversationTitle;
boolean isOneToOne = TextUtils.isEmpty(conversationTitle);
- if (isOneToOne) {
- // Let's add the conversationTitle in case we didn't have one before and all
- // messages are from the same sender
- conversationTitle = createConversationTitleFromMessages();
- } else if (hasOnlyWhiteSpaceSenders()) {
+ CharSequence nameReplacement = null;
+ if (hasOnlyWhiteSpaceSenders()) {
isOneToOne = true;
+ nameReplacement = conversationTitle;
+ conversationTitle = null;
}
- boolean hasTitle = !TextUtils.isEmpty(conversationTitle);
RemoteViews contentView = mBuilder.applyStandardTemplateWithActions(
mBuilder.getMessagingLayoutResource(),
mBuilder.mParams.reset().hasProgress(false).title(conversationTitle).text(null)
- .hideLargeIcon(isOneToOne).alwaysShowReply(true));
+ .hideLargeIcon(!showRightIcon || isOneToOne)
+ .headerTextSecondary(conversationTitle)
+ .alwaysShowReply(showRightIcon));
addExtras(mBuilder.mN.extras);
+ // also update the end margin if there is an image
+ int endMargin = R.dimen.notification_content_margin_end;
+ if (mBuilder.mN.hasLargeIcon() && showRightIcon) {
+ endMargin = R.dimen.notification_content_plus_picture_margin_end;
+ }
+ contentView.setViewLayoutMarginEndDimen(R.id.notification_main_column, endMargin);
contentView.setInt(R.id.status_bar_latest_event_content, "setLayoutColor",
mBuilder.resolveContrastColor());
contentView.setIcon(R.id.status_bar_latest_event_content, "setLargeIcon",
mBuilder.mN.mLargeIcon);
+ contentView.setCharSequence(R.id.status_bar_latest_event_content, "setNameReplacement",
+ nameReplacement);
contentView.setBoolean(R.id.status_bar_latest_event_content, "setIsOneToOne",
isOneToOne);
contentView.setBundle(R.id.status_bar_latest_event_content, "setData",
@@ -6238,8 +6504,8 @@ public class Notification implements Parcelable
private boolean hasOnlyWhiteSpaceSenders() {
for (int i = 0; i < mMessages.size(); i++) {
Message m = mMessages.get(i);
- CharSequence sender = m.getSender();
- if (!isWhiteSpace(sender)) {
+ Person sender = m.getSenderPerson();
+ if (sender != null && !isWhiteSpace(sender.getName())) {
return false;
}
}
@@ -6268,9 +6534,9 @@ public class Notification implements Parcelable
ArraySet<CharSequence> names = new ArraySet<>();
for (int i = 0; i < mMessages.size(); i++) {
Message m = mMessages.get(i);
- CharSequence sender = m.getSender();
+ Person sender = m.getSenderPerson();
if (sender != null) {
- names.add(sender);
+ names.add(sender.getName());
}
}
SpannableStringBuilder title = new SpannableStringBuilder();
@@ -6290,7 +6556,7 @@ public class Notification implements Parcelable
*/
@Override
public RemoteViews makeHeadsUpContentView(boolean increasedHeight) {
- RemoteViews remoteViews = makeBigContentView();
+ RemoteViews remoteViews = makeBigContentView(true /* showRightIcon */);
remoteViews.setInt(R.id.notification_messaging, "setMaxDisplayedLines", 1);
return remoteViews;
}
@@ -6305,13 +6571,15 @@ public class Notification implements Parcelable
static final String KEY_TEXT = "text";
static final String KEY_TIMESTAMP = "time";
static final String KEY_SENDER = "sender";
+ static final String KEY_SENDER_PERSON = "sender_person";
static final String KEY_DATA_MIME_TYPE = "type";
static final String KEY_DATA_URI= "uri";
static final String KEY_EXTRAS_BUNDLE = "extras";
private final CharSequence mText;
private final long mTimestamp;
- private final CharSequence mSender;
+ @Nullable
+ private final Person mSender;
private Bundle mExtras = new Bundle();
private String mDataMimeType;
@@ -6326,8 +6594,28 @@ public class Notification implements Parcelable
* the platform will insert {@link MessagingStyle#getUserDisplayName()}.
* Should be unique amongst all individuals in the conversation, and should be
* consistent during re-posts of the notification.
+ *
+ * @deprecated use {@code Message(CharSequence, long, Person)}
*/
public Message(CharSequence text, long timestamp, CharSequence sender){
+ this(text, timestamp, sender == null ? null : new Person().setName(sender));
+ }
+
+ /**
+ * Constructor
+ * @param text A {@link CharSequence} to be displayed as the message content
+ * @param timestamp Time at which the message arrived
+ * @param sender The {@link Person} who sent the message.
+ * Should be <code>null</code> for messages by the current user, in which case
+ * the platform will insert the user set in {@code MessagingStyle(Person)}.
+ * <p>
+ * The person provided should contain an Icon, set with {@link Person#setIcon(Icon)}
+ * and also have a name provided with {@link Person#setName(CharSequence)}. If multiple
+ * users have the same name, consider providing a key with {@link Person#setKey(String)}
+ * in order to differentiate between the different users.
+ * </p>
+ */
+ public Message(CharSequence text, long timestamp, @Nullable Person sender){
mText = text;
mTimestamp = timestamp;
mSender = sender;
@@ -6390,8 +6678,18 @@ public class Notification implements Parcelable
/**
* Get the text used to display the contact's name in the messaging experience
+ *
+ * @deprecated use {@link #getSenderPerson()}
*/
public CharSequence getSender() {
+ return mSender == null ? null : mSender.getName();
+ }
+
+ /**
+ * Get the sender associated with this message.
+ */
+ @Nullable
+ public Person getSenderPerson() {
return mSender;
}
@@ -6417,7 +6715,9 @@ public class Notification implements Parcelable
}
bundle.putLong(KEY_TIMESTAMP, mTimestamp);
if (mSender != null) {
- bundle.putCharSequence(KEY_SENDER, mSender);
+ // Legacy listeners need this
+ bundle.putCharSequence(KEY_SENDER, mSender.getName());
+ bundle.putParcelable(KEY_SENDER_PERSON, mSender);
}
if (mDataMimeType != null) {
bundle.putString(KEY_DATA_MIME_TYPE, mDataMimeType);
@@ -6466,8 +6766,20 @@ public class Notification implements Parcelable
if (!bundle.containsKey(KEY_TEXT) || !bundle.containsKey(KEY_TIMESTAMP)) {
return null;
} else {
+
+ Person senderPerson = bundle.getParcelable(KEY_SENDER_PERSON);
+ if (senderPerson == null) {
+ // Legacy apps that use compat don't actually provide the sender objects
+ // We need to fix the compat version to provide people / use
+ // the native api instead
+ CharSequence senderName = bundle.getCharSequence(KEY_SENDER);
+ if (senderName != null) {
+ senderPerson = new Person().setName(senderName);
+ }
+ }
Message message = new Message(bundle.getCharSequence(KEY_TEXT),
- bundle.getLong(KEY_TIMESTAMP), bundle.getCharSequence(KEY_SENDER));
+ bundle.getLong(KEY_TIMESTAMP),
+ senderPerson);
if (bundle.containsKey(KEY_DATA_MIME_TYPE) &&
bundle.containsKey(KEY_DATA_URI)) {
message.setData(bundle.getString(KEY_DATA_MIME_TYPE),
@@ -7102,6 +7414,176 @@ public class Notification implements Parcelable
}
}
+ /**
+ * A Person associated with this Notification.
+ */
+ public static final class Person implements Parcelable {
+ @Nullable private CharSequence mName;
+ @Nullable private Icon mIcon;
+ @Nullable private String mUri;
+ @Nullable private String mKey;
+
+ protected Person(Parcel in) {
+ mName = in.readCharSequence();
+ if (in.readInt() != 0) {
+ mIcon = Icon.CREATOR.createFromParcel(in);
+ }
+ mUri = in.readString();
+ mKey = in.readString();
+ }
+
+ /**
+ * Create a new person.
+ */
+ public Person() {
+ }
+
+ /**
+ * Give this person a name.
+ *
+ * @param name the name of this person
+ */
+ public Person setName(@Nullable CharSequence name) {
+ this.mName = name;
+ return this;
+ }
+
+ /**
+ * Add an icon for this person.
+ * <br />
+ * This is currently only used for {@link MessagingStyle} notifications and should not be
+ * provided otherwise, in order to save memory. The system will prefer this icon over any
+ * images that are resolved from the URI.
+ *
+ * @param icon the icon of the person
+ */
+ public Person setIcon(@Nullable Icon icon) {
+ this.mIcon = icon;
+ return this;
+ }
+
+ /**
+ * Set a URI associated with this person.
+ *
+ * <P>
+ * Depending on user preferences, adding a URI to a Person may allow the notification to
+ * pass through interruption filters, if this notification is of
+ * category {@link #CATEGORY_CALL} or {@link #CATEGORY_MESSAGE}.
+ * The addition of people may also cause this notification to appear more prominently in
+ * the user interface.
+ * </P>
+ *
+ * <P>
+ * The person should be specified by the {@code String} representation of a
+ * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
+ * </P>
+ *
+ * <P>The system will also attempt to resolve {@code mailto:} and {@code tel:} schema
+ * URIs. The path part of these URIs must exist in the contacts database, in the
+ * appropriate column, or the reference will be discarded as invalid. Telephone schema
+ * URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}.
+ * </P>
+ *
+ * @param uri a URI for the person
+ */
+ public Person setUri(@Nullable String uri) {
+ mUri = uri;
+ return this;
+ }
+
+ /**
+ * Add a key to this person in order to uniquely identify it.
+ * This is especially useful if the name doesn't uniquely identify this person or if the
+ * display name is a short handle of the actual name.
+ *
+ * <P>If no key is provided, the name serves as as the key for the purpose of
+ * identification.</P>
+ *
+ * @param key the key that uniquely identifies this person
+ */
+ public Person setKey(@Nullable String key) {
+ mKey = key;
+ return this;
+ }
+
+
+ /**
+ * @return the uri provided for this person or {@code null} if no Uri was provided
+ */
+ @Nullable
+ public String getUri() {
+ return mUri;
+ }
+
+ /**
+ * @return the name provided for this person or {@code null} if no name was provided
+ */
+ @Nullable
+ public CharSequence getName() {
+ return mName;
+ }
+
+ /**
+ * @return the icon provided for this person or {@code null} if no icon was provided
+ */
+ @Nullable
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * @return the key provided for this person or {@code null} if no key was provided
+ */
+ @Nullable
+ public String getKey() {
+ return mKey;
+ }
+
+ /**
+ * @return the URI associated with this person, or "name:mName" otherwise
+ * @hide
+ */
+ public String resolveToLegacyUri() {
+ if (mUri != null) {
+ return mUri;
+ }
+ if (mName != null) {
+ return "name:" + mName;
+ }
+ return "";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, @WriteFlags int flags) {
+ dest.writeCharSequence(mName);
+ if (mIcon != null) {
+ dest.writeInt(1);
+ mIcon.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeString(mUri);
+ dest.writeString(mKey);
+ }
+
+ public static final Creator<Person> CREATOR = new Creator<Person>() {
+ @Override
+ public Person createFromParcel(Parcel in) {
+ return new Person(in);
+ }
+
+ @Override
+ public Person[] newArray(int size) {
+ return new Person[size];
+ }
+ };
+ }
+
// When adding a new Style subclass here, don't forget to update
// Builder.getNotificationStyleClass.
@@ -8541,6 +9023,7 @@ public class Notification implements Parcelable
boolean ambient = false;
CharSequence title;
CharSequence text;
+ CharSequence headerTextSecondary;
boolean hideLargeIcon;
public boolean alwaysShowReply;
@@ -8549,6 +9032,7 @@ public class Notification implements Parcelable
ambient = false;
title = null;
text = null;
+ headerTextSecondary = null;
return this;
}
@@ -8567,6 +9051,11 @@ public class Notification implements Parcelable
return this;
}
+ final StandardTemplateParams headerTextSecondary(CharSequence text) {
+ this.headerTextSecondary = text;
+ return this;
+ }
+
final StandardTemplateParams alwaysShowReply(boolean alwaysShowReply) {
this.alwaysShowReply = alwaysShowReply;
return this;
diff --git a/android/app/NotificationChannel.java b/android/app/NotificationChannel.java
index c06ad3f3..30f2697c 100644
--- a/android/app/NotificationChannel.java
+++ b/android/app/NotificationChannel.java
@@ -32,8 +32,6 @@ import android.util.proto.ProtoOutputStream;
import com.android.internal.util.Preconditions;
-import com.android.internal.util.Preconditions;
-
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
@@ -936,7 +934,9 @@ public final class NotificationChannel implements Parcelable {
}
/** @hide */
- public void toProto(ProtoOutputStream proto) {
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
proto.write(NotificationChannelProto.ID, mId);
proto.write(NotificationChannelProto.NAME, mName);
proto.write(NotificationChannelProto.DESCRIPTION, mDesc);
@@ -959,10 +959,10 @@ public final class NotificationChannel implements Parcelable {
proto.write(NotificationChannelProto.IS_DELETED, mDeleted);
proto.write(NotificationChannelProto.GROUP, mGroup);
if (mAudioAttributes != null) {
- long aToken = proto.start(NotificationChannelProto.AUDIO_ATTRIBUTES);
- mAudioAttributes.toProto(proto);
- proto.end(aToken);
+ mAudioAttributes.writeToProto(proto, NotificationChannelProto.AUDIO_ATTRIBUTES);
}
proto.write(NotificationChannelProto.IS_BLOCKABLE_SYSTEM, mBlockableSystem);
+
+ proto.end(token);
}
}
diff --git a/android/app/NotificationChannelGroup.java b/android/app/NotificationChannelGroup.java
index 5cb7fb7a..16166f7c 100644
--- a/android/app/NotificationChannelGroup.java
+++ b/android/app/NotificationChannelGroup.java
@@ -298,13 +298,17 @@ public final class NotificationChannelGroup implements Parcelable {
}
/** @hide */
- public void toProto(ProtoOutputStream proto) {
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
proto.write(NotificationChannelGroupProto.ID, mId);
proto.write(NotificationChannelGroupProto.NAME, mName.toString());
proto.write(NotificationChannelGroupProto.DESCRIPTION, mDescription);
proto.write(NotificationChannelGroupProto.IS_BLOCKED, mBlocked);
for (NotificationChannel channel : mChannels) {
- channel.toProto(proto);
+ channel.writeToProto(proto, NotificationChannelGroupProto.CHANNELS);
}
+
+ proto.end(token);
}
}
diff --git a/android/app/NotificationManager.java b/android/app/NotificationManager.java
index 659cf169..49c03ab9 100644
--- a/android/app/NotificationManager.java
+++ b/android/app/NotificationManager.java
@@ -93,6 +93,18 @@ public class NotificationManager {
private static boolean localLOGV = false;
/**
+ * Intent that is broadcast when an application is blocked or unblocked.
+ *
+ * This broadcast is only sent to the app whose block state has changed.
+ *
+ * Input: nothing
+ * Output: nothing
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_APP_BLOCK_STATE_CHANGED =
+ "android.app.action.APP_BLOCK_STATE_CHANGED";
+
+ /**
* Intent that is broadcast when a {@link NotificationChannel} is blocked
* (when {@link NotificationChannel#getImportance()} is {@link #IMPORTANCE_NONE}) or unblocked
* (when {@link NotificationChannel#getImportance()} is anything other than
@@ -1133,7 +1145,7 @@ public class NotificationManager {
}
/** @hide */
- public void toProto(ProtoOutputStream proto, long fieldId) {
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
final long pToken = proto.start(fieldId);
bitwiseToProtoEnum(proto, PolicyProto.PRIORITY_CATEGORIES, priorityCategories);
diff --git a/android/app/PendingIntent.java b/android/app/PendingIntent.java
index 8b76cc7c..d6429ae9 100644
--- a/android/app/PendingIntent.java
+++ b/android/app/PendingIntent.java
@@ -867,19 +867,30 @@ public final class PendingIntent implements Parcelable {
@Nullable OnFinished onFinished, @Nullable Handler handler,
@Nullable String requiredPermission, @Nullable Bundle options)
throws CanceledException {
+ if (sendAndReturnResult(context, code, intent, onFinished, handler, requiredPermission,
+ options) < 0) {
+ throw new CanceledException();
+ }
+ }
+
+ /**
+ * Like {@link #send}, but returns the result
+ * @hide
+ */
+ public int sendAndReturnResult(Context context, int code, @Nullable Intent intent,
+ @Nullable OnFinished onFinished, @Nullable Handler handler,
+ @Nullable String requiredPermission, @Nullable Bundle options)
+ throws CanceledException {
try {
String resolvedType = intent != null ?
intent.resolveTypeIfNeeded(context.getContentResolver())
: null;
- int res = ActivityManager.getService().sendIntentSender(
+ return ActivityManager.getService().sendIntentSender(
mTarget, mWhitelistToken, code, intent, resolvedType,
onFinished != null
? new FinishedDispatcher(this, onFinished, handler)
: null,
requiredPermission, options);
- if (res < 0) {
- throw new CanceledException();
- }
} catch (RemoteException e) {
throw new CanceledException(e);
}
diff --git a/android/app/ProfilerInfo.java b/android/app/ProfilerInfo.java
index d5234278..0ed1b082 100644
--- a/android/app/ProfilerInfo.java
+++ b/android/app/ProfilerInfo.java
@@ -20,6 +20,7 @@ import android.os.Parcel;
import android.os.ParcelFileDescriptor;
import android.os.Parcelable;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import java.io.IOException;
import java.util.Objects;
@@ -55,14 +56,24 @@ public class ProfilerInfo implements Parcelable {
*/
public final String agent;
+ /**
+ * Whether the {@link agent} should be attached early (before bind-application) or during
+ * bind-application. Agents attached prior to binding cannot be loaded from the app's APK
+ * directly and must be given as an absolute path (or available in the default LD_LIBRARY_PATH).
+ * Agents attached during bind-application will miss early setup (e.g., resource initialization
+ * and classloader generation), but are searched in the app's library search path.
+ */
+ public final boolean attachAgentDuringBind;
+
public ProfilerInfo(String filename, ParcelFileDescriptor fd, int interval, boolean autoStop,
- boolean streaming, String agent) {
+ boolean streaming, String agent, boolean attachAgentDuringBind) {
profileFile = filename;
profileFd = fd;
samplingInterval = interval;
autoStopProfiler = autoStop;
streamingOutput = streaming;
this.agent = agent;
+ this.attachAgentDuringBind = attachAgentDuringBind;
}
public ProfilerInfo(ProfilerInfo in) {
@@ -72,6 +83,7 @@ public class ProfilerInfo implements Parcelable {
autoStopProfiler = in.autoStopProfiler;
streamingOutput = in.streamingOutput;
agent = in.agent;
+ attachAgentDuringBind = in.attachAgentDuringBind;
}
/**
@@ -110,6 +122,21 @@ public class ProfilerInfo implements Parcelable {
out.writeInt(autoStopProfiler ? 1 : 0);
out.writeInt(streamingOutput ? 1 : 0);
out.writeString(agent);
+ out.writeBoolean(attachAgentDuringBind);
+ }
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(ProfilerInfoProto.PROFILE_FILE, profileFile);
+ if (profileFd != null) {
+ proto.write(ProfilerInfoProto.PROFILE_FD, profileFd.getFd());
+ }
+ proto.write(ProfilerInfoProto.SAMPLING_INTERVAL, samplingInterval);
+ proto.write(ProfilerInfoProto.AUTO_STOP_PROFILER, autoStopProfiler);
+ proto.write(ProfilerInfoProto.STREAMING_OUTPUT, streamingOutput);
+ proto.write(ProfilerInfoProto.AGENT, agent);
+ proto.end(token);
}
public static final Parcelable.Creator<ProfilerInfo> CREATOR =
@@ -132,6 +159,7 @@ public class ProfilerInfo implements Parcelable {
autoStopProfiler = in.readInt() != 0;
streamingOutput = in.readInt() != 0;
agent = in.readString();
+ attachAgentDuringBind = in.readBoolean();
}
@Override
diff --git a/android/app/RemoteInput.java b/android/app/RemoteInput.java
index 02a01242..b7100e6f 100644
--- a/android/app/RemoteInput.java
+++ b/android/app/RemoteInput.java
@@ -24,6 +24,7 @@ import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArraySet;
+
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -73,6 +74,15 @@ public final class RemoteInput implements Parcelable {
private static final String EXTRA_DATA_TYPE_RESULTS_DATA =
"android.remoteinput.dataTypeResultsData";
+ /** Extra added to a clip data intent object identifying the source of the results. */
+ private static final String EXTRA_RESULTS_SOURCE = "android.remoteinput.resultsSource";
+
+ /** The user manually entered the data. */
+ public static final int SOURCE_FREE_FORM_INPUT = 0;
+
+ /** The user selected one of the choices from {@link #getChoices}. */
+ public static final int SOURCE_CHOICE = 1;
+
// Flags bitwise-ored to mFlags
private static final int FLAG_ALLOW_FREE_FORM_INPUT = 0x1;
@@ -416,6 +426,48 @@ public final class RemoteInput implements Parcelable {
intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
}
+ /**
+ * Set the source of the RemoteInput results. This method should only be called by remote
+ * input collection services (e.g.
+ * {@link android.service.notification.NotificationListenerService})
+ * when sending results to a pending intent.
+ *
+ * @see #SOURCE_FREE_FORM_INPUT
+ * @see #SOURCE_CHOICE
+ *
+ * @param intent The intent to add remote input source to. The {@link ClipData}
+ * field of the intent will be modified to contain the source.
+ * field of the intent will be modified to contain the source.
+ * @param source The source of the results.
+ */
+ public static void setResultsSource(Intent intent, int source) {
+ Intent clipDataIntent = getClipDataIntentFromIntent(intent);
+ if (clipDataIntent == null) {
+ clipDataIntent = new Intent(); // First time we've added a result.
+ }
+ clipDataIntent.putExtra(EXTRA_RESULTS_SOURCE, source);
+ intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipDataIntent));
+ }
+
+ /**
+ * Get the source of the RemoteInput results.
+ *
+ * @see #SOURCE_FREE_FORM_INPUT
+ * @see #SOURCE_CHOICE
+ *
+ * @param intent The intent object that fired in response to an action or content intent
+ * which also had one or more remote input requested.
+ * @return The source of the results. If no source was set, {@link #SOURCE_FREE_FORM_INPUT} will
+ * be returned.
+ */
+ public static int getResultsSource(Intent intent) {
+ Intent clipDataIntent = getClipDataIntentFromIntent(intent);
+ if (clipDataIntent == null) {
+ return SOURCE_FREE_FORM_INPUT;
+ }
+ return clipDataIntent.getExtras().getInt(EXTRA_RESULTS_SOURCE, SOURCE_FREE_FORM_INPUT);
+ }
+
private static String getExtraResultsKeyForData(String mimeType) {
return EXTRA_DATA_TYPE_RESULTS_DATA + mimeType;
}
diff --git a/android/app/SharedPreferencesImpl.java b/android/app/SharedPreferencesImpl.java
index 6dca4004..6ac15a5f 100644
--- a/android/app/SharedPreferencesImpl.java
+++ b/android/app/SharedPreferencesImpl.java
@@ -50,11 +50,6 @@ 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";
@@ -74,12 +69,18 @@ final class SharedPreferencesImpl implements SharedPreferences {
private final Object mLock = new Object();
private final Object mWritingToDiskLock = new Object();
- private Future<Map<String, Object>> mMap;
+ @GuardedBy("mLock")
+ private Map<String, Object> mMap;
+ @GuardedBy("mLock")
+ private Throwable mThrowable;
@GuardedBy("mLock")
private int mDiskWritesInFlight = 0;
@GuardedBy("mLock")
+ private boolean mLoaded = false;
+
+ @GuardedBy("mLock")
private StructTimespec mStatTimestamp;
@GuardedBy("mLock")
@@ -106,18 +107,28 @@ final class SharedPreferencesImpl implements SharedPreferences {
mFile = file;
mBackupFile = makeBackupFile(file);
mMode = mode;
+ mLoaded = false;
mMap = null;
+ mThrowable = null;
startLoadFromDisk();
}
private void startLoadFromDisk() {
- FutureTask<Map<String, Object>> futureTask = new FutureTask<>(() -> loadFromDisk());
- mMap = futureTask;
- new Thread(futureTask, "SharedPreferencesImpl-load").start();
+ synchronized (mLock) {
+ mLoaded = false;
+ }
+ new Thread("SharedPreferencesImpl-load") {
+ public void run() {
+ loadFromDisk();
+ }
+ }.start();
}
- private Map<String, Object> loadFromDisk() {
+ private void loadFromDisk() {
synchronized (mLock) {
+ if (mLoaded) {
+ return;
+ }
if (mBackupFile.exists()) {
mFile.delete();
mBackupFile.renameTo(mFile);
@@ -131,13 +142,14 @@ final class SharedPreferencesImpl implements SharedPreferences {
Map<String, Object> map = null;
StructStat stat = null;
+ Throwable thrown = null;
try {
stat = Os.stat(mFile.getPath());
if (mFile.canRead()) {
BufferedInputStream str = null;
try {
str = new BufferedInputStream(
- new FileInputStream(mFile), 16*1024);
+ new FileInputStream(mFile), 16 * 1024);
map = (Map<String, Object>) XmlUtils.readMapXml(str);
} catch (Exception e) {
Log.w(TAG, "Cannot read " + mFile.getAbsolutePath(), e);
@@ -146,18 +158,37 @@ final class SharedPreferencesImpl implements SharedPreferences {
}
}
} catch (ErrnoException e) {
- /* ignore */
+ // An errno exception means the stat failed. Treat as empty/non-existing by
+ // ignoring.
+ } catch (Throwable t) {
+ thrown = t;
}
synchronized (mLock) {
- if (map != null) {
- mStatTimestamp = stat.st_mtim;
- mStatSize = stat.st_size;
- } else {
- map = new HashMap<>();
+ mLoaded = true;
+ mThrowable = thrown;
+
+ // It's important that we always signal waiters, even if we'll make
+ // them fail with an exception. The try-finally is pretty wide, but
+ // better safe than sorry.
+ try {
+ if (thrown == null) {
+ if (map != null) {
+ mMap = map;
+ mStatTimestamp = stat.st_mtim;
+ mStatSize = stat.st_size;
+ } else {
+ mMap = new HashMap<>();
+ }
+ }
+ // In case of a thrown exception, we retain the old map. That allows
+ // any open editors to commit and store updates.
+ } catch (Throwable t) {
+ mThrowable = t;
+ } finally {
+ mLock.notifyAll();
}
}
- return map;
}
static File makeBackupFile(File prefsFile) {
@@ -216,42 +247,40 @@ final class SharedPreferencesImpl implements SharedPreferences {
}
}
- 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()) {
+ @GuardedBy("mLock")
+ private void awaitLoadedLocked() {
+ if (!mLoaded) {
// 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();
}
- return getLoaded();
+ while (!mLoaded) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException unused) {
+ }
+ }
+ if (mThrowable != null) {
+ throw new IllegalStateException(mThrowable);
+ }
}
@Override
public Map<String, ?> getAll() {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- return new HashMap<String, Object>(map);
+ awaitLoadedLocked();
+ //noinspection unchecked
+ return new HashMap<String, Object>(mMap);
}
}
@Override
@Nullable
public String getString(String key, @Nullable String defValue) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- String v = (String) map.get(key);
+ awaitLoadedLocked();
+ String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
@@ -259,65 +288,66 @@ final class SharedPreferencesImpl implements SharedPreferences {
@Override
@Nullable
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- @SuppressWarnings("unchecked")
- Set<String> v = (Set<String>) map.get(key);
+ awaitLoadedLocked();
+ Set<String> v = (Set<String>) mMap.get(key);
return v != null ? v : defValues;
}
}
@Override
public int getInt(String key, int defValue) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- Integer v = (Integer) map.get(key);
+ awaitLoadedLocked();
+ Integer v = (Integer)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public long getLong(String key, long defValue) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- Long v = (Long) map.get(key);
+ awaitLoadedLocked();
+ Long v = (Long)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public float getFloat(String key, float defValue) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- Float v = (Float) map.get(key);
+ awaitLoadedLocked();
+ Float v = (Float)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public boolean getBoolean(String key, boolean defValue) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- Boolean v = (Boolean) map.get(key);
+ awaitLoadedLocked();
+ Boolean v = (Boolean)mMap.get(key);
return v != null ? v : defValue;
}
}
@Override
public boolean contains(String key) {
- Map<String, Object> map = getLoadedWithBlockGuard();
synchronized (mLock) {
- return map.containsKey(key);
+ awaitLoadedLocked();
+ return mMap.containsKey(key);
}
}
@Override
public Editor edit() {
- // TODO: remove the need to call getLoaded() when
+ // TODO: remove the need to call awaitLoadedLocked() 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.
- getLoadedWithBlockGuard();
+ synchronized (mLock) {
+ awaitLoadedLocked();
+ }
return new EditorImpl();
}
@@ -471,43 +501,13 @@ 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 map as a currently
+ // We can't modify our mMap as a currently
// in-flight write owns it. Clone it before
// modifying it.
// noinspection unchecked
- 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;
- }
- };
+ mMap = new HashMap<String, Object>(mMap);
}
- mapToWriteToDisk = getLoaded();
+ mapToWriteToDisk = mMap;
mDiskWritesInFlight++;
boolean hasListeners = mListeners.size() > 0;
diff --git a/android/app/StatsManager.java b/android/app/StatsManager.java
new file mode 100644
index 00000000..963fc776
--- /dev/null
+++ b/android/app/StatsManager.java
@@ -0,0 +1,238 @@
+/*
+ * 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;
+
+import android.Manifest;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.os.IBinder;
+import android.os.IStatsManager;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Slog;
+
+/**
+ * API for statsd clients to send configurations and retrieve data.
+ *
+ * @hide
+ */
+@SystemApi
+public final class StatsManager extends android.util.StatsManager { // TODO: Remove the extends.
+ IStatsManager mService;
+ private static final String TAG = "StatsManager";
+
+ /** Long extra of uid that added the relevant stats config. */
+ public static final String EXTRA_STATS_CONFIG_UID =
+ "android.app.extra.STATS_CONFIG_UID";
+ /** Long extra of the relevant stats config's configKey. */
+ public static final String EXTRA_STATS_CONFIG_KEY =
+ "android.app.extra.STATS_CONFIG_KEY";
+ /** Long extra of the relevant statsd_config.proto's Subscription.id. */
+ public static final String EXTRA_STATS_SUBSCRIPTION_ID =
+ "android.app.extra.STATS_SUBSCRIPTION_ID";
+ /** Long extra of the relevant statsd_config.proto's Subscription.rule_id. */
+ public static final String EXTRA_STATS_SUBSCRIPTION_RULE_ID =
+ "android.app.extra.STATS_SUBSCRIPTION_RULE_ID";
+ /**
+ * Extra of a {@link android.os.StatsDimensionsValue} representing sliced dimension value
+ * information.
+ */
+ public static final String EXTRA_STATS_DIMENSIONS_VALUE =
+ "android.app.extra.STATS_DIMENSIONS_VALUE";
+
+ /**
+ * Constructor for StatsManagerClient.
+ *
+ * @hide
+ */
+ public StatsManager() {
+ }
+
+ /**
+ * Clients can send a configuration and simultaneously registers the name of a broadcast
+ * receiver that listens for when it should request data.
+ *
+ * @param configKey An arbitrary integer that allows clients to track the configuration.
+ * @param config Wire-encoded StatsDConfig proto that specifies metrics (and all
+ * dependencies eg, conditions and matchers).
+ * @param pkg The package name to receive the broadcast.
+ * @param cls The name of the class that receives the broadcast.
+ * @return true if successful
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean addConfiguration(long configKey, byte[] config, String pkg, String cls) {
+ synchronized (this) {
+ try {
+ IStatsManager service = getIStatsManagerLocked();
+ if (service == null) {
+ Slog.d(TAG, "Failed to find statsd when adding configuration");
+ return false;
+ }
+ return service.addConfiguration(configKey, config, pkg, cls);
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Failed to connect to statsd when adding configuration");
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Remove a configuration from logging.
+ *
+ * @param configKey Configuration key to remove.
+ * @return true if successful
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean removeConfiguration(long configKey) {
+ synchronized (this) {
+ try {
+ IStatsManager service = getIStatsManagerLocked();
+ if (service == null) {
+ Slog.d(TAG, "Failed to find statsd when removing configuration");
+ return false;
+ }
+ return service.removeConfiguration(configKey);
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Failed to connect to statsd when removing configuration");
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Set the PendingIntent to be used when broadcasting subscriber information to the given
+ * subscriberId within the given config.
+ *
+ * <p>
+ * Suppose that the calling uid has added a config with key configKey, and that in this config
+ * it is specified that when a particular anomaly is detected, a broadcast should be sent to
+ * a BroadcastSubscriber with id subscriberId. This function links the given pendingIntent with
+ * that subscriberId (for that config), so that this pendingIntent is used to send the broadcast
+ * when the anomaly is detected.
+ *
+ * <p>
+ * When statsd sends the broadcast, the PendingIntent will used to send an intent with
+ * information of
+ * {@link #EXTRA_STATS_CONFIG_UID},
+ * {@link #EXTRA_STATS_CONFIG_KEY},
+ * {@link #EXTRA_STATS_SUBSCRIPTION_ID},
+ * {@link #EXTRA_STATS_SUBSCRIPTION_RULE_ID}, and
+ * {@link #EXTRA_STATS_DIMENSIONS_VALUE}.
+ *
+ * <p>
+ * This function can only be called by the owner (uid) of the config. It must be called each
+ * time statsd starts. The config must have been added first (via addConfiguration()).
+ *
+ * @param configKey The integer naming the config to which this subscriber is attached.
+ * @param subscriberId ID of the subscriber, as used in the config.
+ * @param pendingIntent the PendingIntent to use when broadcasting info to the subscriber
+ * associated with the given subscriberId. May be null, in which case
+ * it undoes any previous setting of this subscriberId.
+ * @return true if successful
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean setBroadcastSubscriber(long configKey,
+ long subscriberId,
+ PendingIntent pendingIntent) {
+ synchronized (this) {
+ try {
+ IStatsManager service = getIStatsManagerLocked();
+ if (service == null) {
+ Slog.w(TAG, "Failed to find statsd when adding broadcast subscriber");
+ return false;
+ }
+ if (pendingIntent != null) {
+ // Extracts IIntentSender from the PendingIntent and turns it into an IBinder.
+ IBinder intentSender = pendingIntent.getTarget().asBinder();
+ return service.setBroadcastSubscriber(configKey, subscriberId, intentSender);
+ } else {
+ return service.unsetBroadcastSubscriber(configKey, subscriberId);
+ }
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to connect to statsd when adding broadcast subscriber", e);
+ return false;
+ }
+ }
+ }
+
+ /**
+ * 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 ConfigMetricsReportList proto. Returns null on failure.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public byte[] getData(long configKey) {
+ synchronized (this) {
+ try {
+ IStatsManager service = getIStatsManagerLocked();
+ if (service == null) {
+ Slog.d(TAG, "Failed to find statsd when getting data");
+ return null;
+ }
+ return service.getData(configKey);
+ } catch (RemoteException e) {
+ Slog.d(TAG, "Failed to connecto statsd when getting data");
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Clients can request metadata for statsd. Will contain stats across all configurations but not
+ * the actual metrics themselves (metrics must be collected via {@link #getData(String)}.
+ * 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() {
+ synchronized (this) {
+ mService = null;
+ }
+ }
+ }
+
+ private IStatsManager getIStatsManagerLocked() throws RemoteException {
+ if (mService != null) {
+ return mService;
+ }
+ mService = IStatsManager.Stub.asInterface(ServiceManager.getService("stats"));
+ if (mService != null) {
+ mService.asBinder().linkToDeath(new StatsdDeathRecipient(), 0);
+ }
+ return mService;
+ }
+}
diff --git a/android/app/SystemServiceRegistry.java b/android/app/SystemServiceRegistry.java
index 66cf9915..4310434c 100644
--- a/android/app/SystemServiceRegistry.java
+++ b/android/app/SystemServiceRegistry.java
@@ -38,12 +38,12 @@ import android.content.ClipboardManager;
import android.content.Context;
import android.content.IRestrictionsManager;
import android.content.RestrictionsManager;
+import android.content.pm.CrossProfileApps;
+import android.content.pm.ICrossProfileApps;
import android.content.pm.IShortcutService;
import android.content.pm.LauncherApps;
import android.content.pm.PackageManager;
import android.content.pm.ShortcutManager;
-import android.content.pm.crossprofile.CrossProfileApps;
-import android.content.pm.crossprofile.ICrossProfileApps;
import android.content.res.Resources;
import android.hardware.ConsumerIrManager;
import android.hardware.ISerialManager;
@@ -112,6 +112,7 @@ import android.os.IBinder;
import android.os.IHardwarePropertiesManager;
import android.os.IPowerManager;
import android.os.IRecoverySystem;
+import android.os.ISystemUpdateManager;
import android.os.IUserManager;
import android.os.IncidentManager;
import android.os.PowerManager;
@@ -119,6 +120,7 @@ import android.os.Process;
import android.os.RecoverySystem;
import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
+import android.os.SystemUpdateManager;
import android.os.SystemVibrator;
import android.os.UserHandle;
import android.os.UserManager;
@@ -136,9 +138,9 @@ import android.telecom.TelecomManager;
import android.telephony.CarrierConfigManager;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
+import android.telephony.euicc.EuiccCardManager;
import android.telephony.euicc.EuiccManager;
import android.util.Log;
-import android.util.StatsManager;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.WindowManager;
@@ -484,6 +486,17 @@ final class SystemServiceRegistry {
return new StorageStatsManager(ctx, service);
}});
+ registerService(Context.SYSTEM_UPDATE_SERVICE, SystemUpdateManager.class,
+ new CachedServiceFetcher<SystemUpdateManager>() {
+ @Override
+ public SystemUpdateManager createService(ContextImpl ctx)
+ throws ServiceNotFoundException {
+ IBinder b = ServiceManager.getServiceOrThrow(
+ Context.SYSTEM_UPDATE_SERVICE);
+ ISystemUpdateManager service = ISystemUpdateManager.Stub.asInterface(b);
+ return new SystemUpdateManager(service);
+ }});
+
registerService(Context.TELEPHONY_SERVICE, TelephonyManager.class,
new CachedServiceFetcher<TelephonyManager>() {
@Override
@@ -494,7 +507,7 @@ final class SystemServiceRegistry {
registerService(Context.TELEPHONY_SUBSCRIPTION_SERVICE, SubscriptionManager.class,
new CachedServiceFetcher<SubscriptionManager>() {
@Override
- public SubscriptionManager createService(ContextImpl ctx) {
+ public SubscriptionManager createService(ContextImpl ctx) throws ServiceNotFoundException {
return new SubscriptionManager(ctx.getOuterContext());
}});
@@ -519,6 +532,13 @@ final class SystemServiceRegistry {
return new EuiccManager(ctx.getOuterContext());
}});
+ registerService(Context.EUICC_CARD_SERVICE, EuiccCardManager.class,
+ new CachedServiceFetcher<EuiccCardManager>() {
+ @Override
+ public EuiccCardManager createService(ContextImpl ctx) {
+ return new EuiccCardManager(ctx.getOuterContext());
+ }});
+
registerService(Context.UI_MODE_SERVICE, UiModeManager.class,
new CachedServiceFetcher<UiModeManager>() {
@Override
diff --git a/android/app/UiAutomation.java b/android/app/UiAutomation.java
index 8f016853..ba39740b 100644
--- a/android/app/UiAutomation.java
+++ b/android/app/UiAutomation.java
@@ -24,7 +24,6 @@ import android.accessibilityservice.IAccessibilityServiceConnection;
import android.annotation.NonNull;
import android.annotation.TestApi;
import android.graphics.Bitmap;
-import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
@@ -47,10 +46,14 @@ import android.view.accessibility.AccessibilityInteractionClient;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
import android.view.accessibility.IAccessibilityInteractionConnection;
+
+import com.android.internal.util.CollectionUtils;
+
import libcore.io.IoUtils;
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeoutException;
@@ -580,6 +583,8 @@ public final class UiAutomation {
// Execute the command *without* the lock being held.
command.run();
+ List<AccessibilityEvent> eventsReceived = Collections.emptyList();
+
// Acquire the lock and wait for the event.
try {
// Wait for the event.
@@ -600,14 +605,14 @@ public final class UiAutomation {
if (filter.accept(event)) {
return event;
}
- event.recycle();
+ eventsReceived = CollectionUtils.add(eventsReceived, event);
}
// Check if timed out and if not wait.
final long elapsedTimeMillis = SystemClock.uptimeMillis() - startTimeMillis;
final long remainingTimeMillis = timeoutMillis - elapsedTimeMillis;
if (remainingTimeMillis <= 0) {
throw new TimeoutException("Expected event not received within: "
- + timeoutMillis + " ms.");
+ + timeoutMillis + " ms, among " + eventsReceived);
}
synchronized (mLock) {
if (mEventQueue.isEmpty()) {
@@ -620,6 +625,10 @@ public final class UiAutomation {
}
}
} finally {
+ for (int i = 0; i < CollectionUtils.size(eventsReceived); i++) {
+ AccessibilityEvent event = eventsReceived.get(i);
+ event.recycle();
+ }
synchronized (mLock) {
mWaitingForEventDelivery = false;
mEventQueue.clear();
diff --git a/android/app/admin/ConnectEvent.java b/android/app/admin/ConnectEvent.java
index f06a9257..d511c57b 100644
--- a/android/app/admin/ConnectEvent.java
+++ b/android/app/admin/ConnectEvent.java
@@ -68,7 +68,7 @@ public final class ConnectEvent extends NetworkEvent implements Parcelable {
@Override
public String toString() {
- return String.format("ConnectEvent(%s, %d, %d, %s)", mIpAddress, mPort, mTimestamp,
+ return String.format("ConnectEvent(%d, %s, %d, %d, %s)", mId, mIpAddress, mPort, mTimestamp,
mPackageName);
}
diff --git a/android/app/admin/DeviceAdminReceiver.java b/android/app/admin/DeviceAdminReceiver.java
index 2e697ac0..28e845a0 100644
--- a/android/app/admin/DeviceAdminReceiver.java
+++ b/android/app/admin/DeviceAdminReceiver.java
@@ -29,10 +29,14 @@ import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
+import android.os.PersistableBundle;
import android.os.Process;
import android.os.UserHandle;
import android.security.KeyChain;
+import libcore.util.NonNull;
+import libcore.util.Nullable;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -335,7 +339,7 @@ public class DeviceAdminReceiver extends BroadcastReceiver {
/**
* Broadcast action: notify the device owner that a user or profile has been removed.
* Carries an extra {@link Intent#EXTRA_USER} that has the {@link UserHandle} of
- * the new user.
+ * the user.
* @hide
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
@@ -343,6 +347,36 @@ public class DeviceAdminReceiver extends BroadcastReceiver {
public static final String ACTION_USER_REMOVED = "android.app.action.USER_REMOVED";
/**
+ * Broadcast action: notify the device owner that a user or profile has been started.
+ * Carries an extra {@link Intent#EXTRA_USER} that has the {@link UserHandle} of
+ * the user.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_USER_STARTED = "android.app.action.USER_STARTED";
+
+ /**
+ * Broadcast action: notify the device owner that a user or profile has been stopped.
+ * Carries an extra {@link Intent#EXTRA_USER} that has the {@link UserHandle} of
+ * the user.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_USER_STOPPED = "android.app.action.USER_STOPPED";
+
+ /**
+ * Broadcast action: notify the device owner that a user or profile has been switched to.
+ * Carries an extra {@link Intent#EXTRA_USER} that has the {@link UserHandle} of
+ * the user.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @BroadcastBehavior(explicitOnly = true)
+ public static final String ACTION_USER_SWITCHED = "android.app.action.USER_SWITCHED";
+
+ /**
* A string containing the SHA-256 hash of the bugreport file.
*
* @see #ACTION_BUGREPORT_SHARE
@@ -438,6 +472,65 @@ public class DeviceAdminReceiver extends BroadcastReceiver {
// TO DO: describe syntax.
public static final String DEVICE_ADMIN_META_DATA = "android.app.device_admin";
+ /**
+ * Broadcast action: notify the newly transferred administrator that the transfer
+ * from the original administrator was successful.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_TRANSFER_OWNERSHIP_COMPLETE =
+ "android.app.action.TRANSFER_OWNERSHIP_COMPLETE";
+
+ /**
+ * Broadcast action: notify the device owner that the ownership of one of its affiliated
+ * profiles is transferred.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_AFFILIATED_PROFILE_TRANSFER_OWNERSHIP_COMPLETE =
+ "android.app.action.AFFILIATED_PROFILE_TRANSFER_OWNERSHIP_COMPLETE";
+
+ /**
+ * A {@link android.os.Parcelable} extra of type {@link android.os.PersistableBundle} that
+ * allows a mobile device management application to pass data to the management application
+ * instance after owner transfer.
+ *
+ * <p>If the transfer is successful, the new owner receives the data in
+ * {@link DeviceAdminReceiver#onTransferOwnershipComplete(Context, PersistableBundle)}.
+ * The bundle is not changed during the ownership transfer.
+ *
+ * @see DevicePolicyManager#transferOwnership(ComponentName, ComponentName, PersistableBundle)
+ */
+ public static final String EXTRA_TRANSFER_OWNERSHIP_ADMIN_EXTRAS_BUNDLE =
+ "android.app.extra.TRANSFER_OWNERSHIP_ADMIN_EXTRAS_BUNDLE";
+
+ /**
+ * Name under which a device administration component indicates whether it supports transfer of
+ * ownership. This meta-data is of type <code>boolean</code>. A value of <code>true</code>
+ * allows this administrator to be used as a target administrator for a transfer. If the value
+ * is <code>false</code>, ownership cannot be transferred to this administrator. The default
+ * value is <code>false</code>.
+ * <p>This metadata is used to avoid ownership transfer migration to an administrator with a
+ * version which does not yet support it.
+ * <p>Usage:
+ * <pre>
+ * &lt;receiver name="..." android:permission="android.permission.BIND_DEVICE_ADMIN"&gt;
+ * &lt;meta-data
+ * android:name="android.app.device_admin"
+ * android:resource="@xml/..." /&gt;
+ * &lt;meta-data
+ * android:name="android.app.support_transfer_ownership"
+ * android:value="true" /&gt;
+ * &lt;/receiver&gt;
+ * </pre>
+ *
+ * @see DevicePolicyManager#transferOwnership(ComponentName, ComponentName, PersistableBundle)
+ */
+ public static final String SUPPORT_TRANSFER_OWNERSHIP_META_DATA =
+ "android.app.support_transfer_ownership";
+
private DevicePolicyManager mManager;
private ComponentName mWho;
@@ -860,6 +953,76 @@ public class DeviceAdminReceiver extends BroadcastReceiver {
}
/**
+ * Called when a user or profile is started.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param startedUser The {@link UserHandle} of the user that has just been started.
+ */
+ public void onUserStarted(Context context, Intent intent, UserHandle startedUser) {
+ }
+
+ /**
+ * Called when a user or profile is stopped.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param stoppedUser The {@link UserHandle} of the user that has just been stopped.
+ */
+ public void onUserStopped(Context context, Intent intent, UserHandle stoppedUser) {
+ }
+
+ /**
+ * Called when a user or profile is switched to.
+ *
+ * <p>This callback is only applicable to device owners.
+ *
+ * @param context The running context as per {@link #onReceive}.
+ * @param intent The received intent as per {@link #onReceive}.
+ * @param switchedUser The {@link UserHandle} of the user that has just been switched to.
+ */
+ public void onUserSwitched(Context context, Intent intent, UserHandle switchedUser) {
+ }
+
+ /**
+ * Called on the newly assigned owner (either device owner or profile owner) when the ownership
+ * transfer has completed successfully.
+ *
+ * <p> The {@code bundle} parameter allows the original owner to pass data
+ * to the new one.
+ *
+ * @param context the running context as per {@link #onReceive}
+ * @param bundle the data to be passed to the new owner
+ */
+ public void onTransferOwnershipComplete(@NonNull Context context,
+ @Nullable PersistableBundle bundle) {
+ }
+
+ /**
+ * Called on the device owner when the ownership of one of its affiliated profiles is
+ * transferred.
+ *
+ * <p>This can be used when transferring both device and profile ownership when using
+ * work profile on a fully managed device. The process would look like this:
+ * <ol>
+ * <li>Transfer profile ownership</li>
+ * <li>The device owner gets notified with this callback</li>
+ * <li>Transfer device ownership</li>
+ * <li>Both profile and device ownerships have been transferred</li>
+ * </ol>
+ *
+ * @param context the running context as per {@link #onReceive}
+ * @param user the {@link UserHandle} of the affiliated user
+ * @see DevicePolicyManager#transferOwnership(ComponentName, ComponentName, PersistableBundle)
+ */
+ public void onTransferAffiliatedProfileOwnershipComplete(Context context, UserHandle user) {
+ }
+
+ /**
* Intercept standard device administrator broadcasts. Implementations
* should not override this method; it is better to implement the
* convenience callbacks for each action.
@@ -921,6 +1084,19 @@ public class DeviceAdminReceiver extends BroadcastReceiver {
onUserAdded(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
} else if (ACTION_USER_REMOVED.equals(action)) {
onUserRemoved(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ } else if (ACTION_USER_STARTED.equals(action)) {
+ onUserStarted(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ } else if (ACTION_USER_STOPPED.equals(action)) {
+ onUserStopped(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ } else if (ACTION_USER_SWITCHED.equals(action)) {
+ onUserSwitched(context, intent, intent.getParcelableExtra(Intent.EXTRA_USER));
+ } else if (ACTION_TRANSFER_OWNERSHIP_COMPLETE.equals(action)) {
+ PersistableBundle bundle =
+ intent.getParcelableExtra(EXTRA_TRANSFER_OWNERSHIP_ADMIN_EXTRAS_BUNDLE);
+ onTransferOwnershipComplete(context, bundle);
+ } else if (ACTION_AFFILIATED_PROFILE_TRANSFER_OWNERSHIP_COMPLETE.equals(action)) {
+ onTransferAffiliatedProfileOwnershipComplete(context,
+ intent.getParcelableExtra(Intent.EXTRA_USER));
}
}
}
diff --git a/android/app/admin/DevicePolicyManager.java b/android/app/admin/DevicePolicyManager.java
index 7e80ac7b..8f76032b 100644
--- a/android/app/admin/DevicePolicyManager.java
+++ b/android/app/admin/DevicePolicyManager.java
@@ -18,7 +18,6 @@ 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;
@@ -50,8 +49,6 @@ import android.graphics.Bitmap;
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;
@@ -71,6 +68,7 @@ import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.ParcelableKeyGenParameterSpec;
import android.service.restrictions.RestrictionsReceiver;
import android.telephony.TelephonyManager;
+import android.telephony.data.ApnSetting;
import android.util.ArraySet;
import android.util.Log;
@@ -1124,6 +1122,7 @@ public class DevicePolicyManager {
*
* This broadcast is sent only to the primary user.
* @see #ACTION_PROVISION_MANAGED_DEVICE
+ * @see DevicePolicyManager#transferOwnership(ComponentName, ComponentName, PersistableBundle)
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public static final String ACTION_DEVICE_OWNER_CHANGED
@@ -1159,9 +1158,17 @@ public class DevicePolicyManager {
public static final String POLICY_DISABLE_SCREEN_CAPTURE = "policy_disable_screen_capture";
/**
+ * Constant to indicate the feature of mandatory backups. Used as argument to
+ * {@link #createAdminSupportIntent(String)}.
+ * @see #setMandatoryBackupTransport(ComponentName, ComponentName)
+ */
+ public static final String POLICY_MANDATORY_BACKUPS = "policy_mandatory_backups";
+
+ /**
* A String indicating a specific restricted feature. Can be a user restriction from the
* {@link UserManager}, e.g. {@link UserManager#DISALLOW_ADJUST_VOLUME}, or one of the values
- * {@link #POLICY_DISABLE_CAMERA} or {@link #POLICY_DISABLE_SCREEN_CAPTURE}.
+ * {@link #POLICY_DISABLE_CAMERA}, {@link #POLICY_DISABLE_SCREEN_CAPTURE} or
+ * {@link #POLICY_MANDATORY_BACKUPS}.
* @see #createAdminSupportIntent(String)
* @hide
*/
@@ -1253,6 +1260,26 @@ public class DevicePolicyManager {
= "android.app.action.SYSTEM_UPDATE_POLICY_CHANGED";
/**
+ * Broadcast action to notify ManagedProvisioning that
+ * {@link UserManager#DISALLOW_SHARE_INTO_MANAGED_PROFILE} restriction has changed.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_DATA_SHARING_RESTRICTION_CHANGED =
+ "android.app.action.DATA_SHARING_RESTRICTION_CHANGED";
+
+ /**
+ * Broadcast action from ManagedProvisioning to notify that the latest change to
+ * {@link UserManager#DISALLOW_SHARE_INTO_MANAGED_PROFILE} restriction has been successfully
+ * applied (cross profile intent filters updated). Only usesd for CTS tests.
+ * @hide
+ */
+ @TestApi
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_DATA_SHARING_RESTRICTION_APPLIED =
+ "android.app.action.DATA_SHARING_RESTRICTION_APPLIED";
+
+ /**
* Permission policy to prompt user for new permission requests for runtime permissions.
* Already granted or denied permissions are not affected by this.
*/
@@ -1668,6 +1695,56 @@ public class DevicePolicyManager {
public static final String ACTION_DEVICE_ADMIN_SERVICE
= "android.app.action.DEVICE_ADMIN_SERVICE";
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = {"ID_TYPE_"}, value = {
+ ID_TYPE_BASE_INFO,
+ ID_TYPE_SERIAL,
+ ID_TYPE_IMEI,
+ ID_TYPE_MEID
+ })
+ public @interface AttestationIdType {}
+
+ /**
+ * Specifies that the device should attest its manufacturer details. For use with
+ * {@link #generateKeyPair}.
+ *
+ * @see #generateKeyPair
+ */
+ public static final int ID_TYPE_BASE_INFO = 1;
+
+ /**
+ * Specifies that the device should attest its serial number. For use with
+ * {@link #generateKeyPair}.
+ *
+ * @see #generateKeyPair
+ */
+ public static final int ID_TYPE_SERIAL = 2;
+
+ /**
+ * Specifies that the device should attest its IMEI. For use with {@link #generateKeyPair}.
+ *
+ * @see #generateKeyPair
+ */
+ public static final int ID_TYPE_IMEI = 4;
+
+ /**
+ * Specifies that the device should attest its MEID. For use with {@link #generateKeyPair}.
+ *
+ * @see #generateKeyPair
+ */
+ public static final int ID_TYPE_MEID = 8;
+
+ /**
+ * Broadcast action: sent when the profile owner is set, changed or cleared.
+ *
+ * This broadcast is sent only to the user managed by the new profile owner.
+ * @see DevicePolicyManager#transferOwnership(ComponentName, ComponentName, PersistableBundle)
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PROFILE_OWNER_CHANGED =
+ "android.app.action.PROFILE_OWNER_CHANGED";
+
/**
* Return true if the given administrator component is currently active (enabled) in the system.
*
@@ -4106,22 +4183,46 @@ public class DevicePolicyManager {
* @param algorithm The key generation algorithm, see {@link java.security.KeyPairGenerator}.
* @param keySpec Specification of the key to generate, see
* {@link java.security.KeyPairGenerator}.
+ * @param idAttestationFlags A bitmask of all the identifiers that should be included in the
+ * attestation record ({@code ID_TYPE_BASE_INFO}, {@code ID_TYPE_SERIAL},
+ * {@code ID_TYPE_IMEI} and {@code ID_TYPE_MEID}), or {@code 0} if no device
+ * identification is required in the attestation record.
+ * Device owner, profile owner and their delegated certificate installer can use
+ * {@link #ID_TYPE_BASE_INFO} to request inclusion of the general device information
+ * including manufacturer, model, brand, device and product in the attestation record.
+ * Only device owner and their delegated certificate installer can use
+ * {@link #ID_TYPE_SERIAL}, {@link #ID_TYPE_IMEI} and {@link #ID_TYPE_MEID} to request
+ * unique device identifiers to be attested.
+ * <p>
+ * If any of {@link #ID_TYPE_SERIAL}, {@link #ID_TYPE_IMEI} and {@link #ID_TYPE_MEID}
+ * is set, it is implicitly assumed that {@link #ID_TYPE_BASE_INFO} is also set.
+ * <p>
+ * If any flag is specified, then an attestation challenge must be included in the
+ * {@code keySpec}.
* @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
+ * owner. If Device ID attestation is requested (using {@link #ID_TYPE_SERIAL},
+ * {@link #ID_TYPE_IMEI} or {@link #ID_TYPE_MEID}), the caller must be the Device Owner
+ * or the Certificate Installer delegate.
+ * @throws IllegalArgumentException if the alias in {@code keySpec} is empty, if the
* algorithm specification in {@code keySpec} is not {@code RSAKeyGenParameterSpec}
- * or {@code ECGenParameterSpec}.
+ * or {@code ECGenParameterSpec}, or if Device ID attestation was requested but the
+ * {@code keySpec} does not contain an attestation challenge.
+ * @see KeyGenParameterSpec.Builder#setAttestationChallenge(byte[])
*/
public AttestedKeyPair generateKeyPair(@Nullable ComponentName admin,
- @NonNull String algorithm, @NonNull KeyGenParameterSpec keySpec) {
+ @NonNull String algorithm, @NonNull KeyGenParameterSpec keySpec,
+ @AttestationIdType int idAttestationFlags) {
throwIfParentInstance("generateKeyPair");
try {
final ParcelableKeyGenParameterSpec parcelableSpec =
new ParcelableKeyGenParameterSpec(keySpec);
KeymasterCertificateChain attestationChain = new KeymasterCertificateChain();
+
+ // Translate ID attestation flags to values used by AttestationUtils
final boolean success = mService.generateKeyPair(
- admin, mContext.getPackageName(), algorithm, parcelableSpec, attestationChain);
+ admin, mContext.getPackageName(), algorithm, parcelableSpec,
+ idAttestationFlags, attestationChain);
if (!success) {
Log.e(TAG, "Error generating key via DevicePolicyManagerService.");
return null;
@@ -5982,6 +6083,13 @@ public class DevicePolicyManager {
* Called by a profile owner of a managed profile to remove the cross-profile intent filters
* that go from the managed profile to the parent, or from the parent to the managed profile.
* Only removes those that have been set by the profile owner.
+ * <p>
+ * <em>Note</em>: A list of default cross profile intent filters are set up by the system when
+ * the profile is created, some of them ensure the proper functioning of the profile, while
+ * others enable sharing of data from the parent to the managed profile for user convenience.
+ * These default intent filters are not cleared when this API is called. If the default cross
+ * profile data sharing is not desired, they can be disabled with
+ * {@link UserManager#DISALLOW_SHARE_INTO_MANAGED_PROFILE}.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @throws SecurityException if {@code admin} is not a device or profile owner.
@@ -6404,12 +6512,6 @@ public class DevicePolicyManager {
public static final int MAKE_USER_DEMO = 0x0004;
/**
- * Flag used by {@link #createAndManageUser} to specify that the newly created user should be
- * started in the background as part of the user creation.
- */
- 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.
*/
@@ -6422,7 +6524,6 @@ public class DevicePolicyManager {
SKIP_SETUP_WIZARD,
MAKE_USER_EPHEMERAL,
MAKE_USER_DEMO,
- START_USER_IN_BACKGROUND,
LEAVE_ALL_SYSTEM_APPS_ENABLED
})
@Retention(RetentionPolicy.SOURCE)
@@ -6451,7 +6552,8 @@ public class DevicePolicyManager {
* IllegalArgumentException is thrown.
* @param adminExtras Extras that will be passed to onEnable of the admin receiver on the new
* user.
- * @param flags {@link #SKIP_SETUP_WIZARD} is supported.
+ * @param flags {@link #SKIP_SETUP_WIZARD}, {@link #MAKE_USER_EPHEMERAL} and
+ * {@link #LEAVE_ALL_SYSTEM_APPS_ENABLED} are supported.
* @see UserHandle
* @return the {@link android.os.UserHandle} object for the created user, or {@code null} if the
* user could not be created.
@@ -6470,8 +6572,8 @@ public class DevicePolicyManager {
}
/**
- * Called by a device owner to remove a user and all associated data. The primary user can not
- * be removed.
+ * Called by a device owner to remove a user/profile and all associated data. The primary user
+ * can not be removed.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param userHandle the user to remove.
@@ -6488,14 +6590,14 @@ 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.
+ * Called by a device owner to switch the specified secondary user to the foreground.
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param userHandle the user to switch to; null will switch to primary.
* @return {@code true} if the switch was successful, {@code false} otherwise.
* @throws SecurityException if {@code admin} is not a device owner.
* @see Intent#ACTION_USER_FOREGROUND
+ * @see #getSecondaryUsers(ComponentName)
*/
public boolean switchUser(@NonNull ComponentName admin, @Nullable UserHandle userHandle) {
throwIfParentInstance("switchUser");
@@ -6507,13 +6609,32 @@ public class DevicePolicyManager {
}
/**
+ * Called by a device owner to start the specified secondary user in background.
+ *
+ * @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 started, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ * @see #getSecondaryUsers(ComponentName)
+ */
+ public boolean startUserInBackground(
+ @NonNull ComponentName admin, @NonNull UserHandle userHandle) {
+ throwIfParentInstance("startUserInBackground");
+ try {
+ return mService.startUserInBackground(admin, userHandle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* 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.
+ * @see #getSecondaryUsers(ComponentName)
*/
public boolean stopUser(@NonNull ComponentName admin, @NonNull UserHandle userHandle) {
throwIfParentInstance("stopUser");
@@ -6525,14 +6646,13 @@ public class DevicePolicyManager {
}
/**
- * 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.
+ * Called by a profile owner of secondary user that is affiliated with the device to stop the
+ * calling user and switch back to primary.
*
* @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
+ * @see #getSecondaryUsers(ComponentName)
*/
public boolean logoutUser(@NonNull ComponentName admin) {
throwIfParentInstance("logoutUser");
@@ -6544,17 +6664,18 @@ public class DevicePolicyManager {
}
/**
- * Called by a device owner to list all secondary users on the device, excluding managed
- * profiles.
+ * Called by a device owner to list all secondary users on the device. Managed profiles are not
+ * considered as secondary users.
* <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
+ * @see #removeUser(ComponentName, UserHandle)
+ * @see #switchUser(ComponentName, UserHandle)
+ * @see #startUserInBackground(ComponentName, UserHandle)
+ * @see #stopUser(ComponentName, UserHandle)
*/
public List<UserHandle> getSecondaryUsers(@NonNull ComponentName admin) {
throwIfParentInstance("getSecondaryUsers");
@@ -6694,7 +6815,8 @@ public class DevicePolicyManager {
* @param restriction Indicates for which feature the dialog should be displayed. Can be a
* user restriction from {@link UserManager}, e.g.
* {@link UserManager#DISALLOW_ADJUST_VOLUME}, or one of the constants
- * {@link #POLICY_DISABLE_CAMERA} or {@link #POLICY_DISABLE_SCREEN_CAPTURE}.
+ * {@link #POLICY_DISABLE_CAMERA}, {@link #POLICY_DISABLE_SCREEN_CAPTURE} or
+ * {@link #POLICY_MANDATORY_BACKUPS}.
* @return Intent An intent to be used to start the dialog-activity if the restriction is
* set by an admin, or null if the restriction does not exist or no admin set it.
*/
@@ -6915,14 +7037,14 @@ public class DevicePolicyManager {
* task. From {@link android.os.Build.VERSION_CODES#M} removing packages from the lock task
* 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. See {@link #isAffiliatedUser}. Any packages
- * set via this method will be cleared if the user becomes unaffiliated.
+ * This function can only be called by the device owner, a profile owner of an affiliated user
+ * or profile, or the profile owner when no device owner is set. See {@link #isAffiliatedUser}.
+ * Any package 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.
+ * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an
+ * affiliated user or profile, or the profile owner when no device owner is set.
* @see #isAffiliatedUser
* @see Activity#startLockTask()
* @see DeviceAdminReceiver#onLockTaskModeEntering(Context, Intent, String)
@@ -6944,8 +7066,8 @@ public class DevicePolicyManager {
/**
* Returns the list of packages allowed to start the lock task mode.
*
- * @throws SecurityException if {@code admin} is not the device owner, or the profile owner of
- * an affiliated user or profile.
+ * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an
+ * affiliated user or profile, or the profile owner when no device owner is set.
* @see #isAffiliatedUser
* @see #setLockTaskPackages
*/
@@ -6985,9 +7107,9 @@ public class DevicePolicyManager {
* is in LockTask mode. If this method is not called, none of the features listed here will be
* 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. See {@link #isAffiliatedUser}. Any features
- * set via this method will be cleared if the user becomes unaffiliated.
+ * This function can only be called by the device owner, a profile owner of an affiliated user
+ * or profile, or the profile owner when no device owner is set. 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.
* @param flags Bitfield of feature flags:
@@ -6998,9 +7120,10 @@ public class DevicePolicyManager {
* {@link #LOCK_TASK_FEATURE_RECENTS},
* {@link #LOCK_TASK_FEATURE_GLOBAL_ACTIONS},
* {@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.
+ * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an
+ * affiliated user or profile, or the profile owner when no device owner is set.
* @see #isAffiliatedUser
+ * @throws SecurityException if {@code admin} is not the device owner or the profile owner.
*/
public void setLockTaskFeatures(@NonNull ComponentName admin, @LockTaskFeature int flags) {
throwIfParentInstance("setLockTaskFeatures");
@@ -7018,8 +7141,8 @@ public class DevicePolicyManager {
*
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @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.
+ * @throws SecurityException if {@code admin} is not the device owner, the profile owner of an
+ * affiliated user or profile, or the profile owner when no device owner is set.
* @see #isAffiliatedUser
* @see #setLockTaskFeatures
*/
@@ -7454,7 +7577,8 @@ public class DevicePolicyManager {
}
/**
- * Called by a device owner to disable the keyguard altogether.
+ * Called by a device owner or profile owner of secondary users that is affiliated with the
+ * device to disable the keyguard altogether.
* <p>
* Setting the keyguard to disabled has the same effect as choosing "None" as the screen lock
* type. However, this call has no effect if a password, pin or pattern is currently set. If a
@@ -7469,7 +7593,10 @@ public class DevicePolicyManager {
* @param disabled {@code true} disables the keyguard, {@code false} reenables it.
* @return {@code false} if attempting to disable the keyguard while a lock password was in
* place. {@code true} otherwise.
- * @throws SecurityException if {@code admin} is not a device owner.
+ * @throws SecurityException if {@code admin} is not the device owner, or a profile owner of
+ * secondary user that is affiliated with the device.
+ * @see #isAffiliatedUser
+ * @see #getSecondaryUsers
*/
public boolean setKeyguardDisabled(@NonNull ComponentName admin, boolean disabled) {
throwIfParentInstance("setKeyguardDisabled");
@@ -7481,9 +7608,9 @@ public class DevicePolicyManager {
}
/**
- * Called by device owner to disable the status bar. Disabling the status bar blocks
- * notifications, quick settings and other screen overlays that allow escaping from a single use
- * device.
+ * Called by device owner or profile owner of secondary users that is affiliated with the
+ * device to disable the status bar. Disabling the status bar blocks notifications, quick
+ * settings and other screen overlays that allow escaping from a single use device.
* <p>
* <strong>Note:</strong> This method has no effect for LockTask mode. The behavior of the
* status bar in LockTask mode can be configured with
@@ -7494,7 +7621,10 @@ public class DevicePolicyManager {
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param disabled {@code true} disables the status bar, {@code false} reenables it.
* @return {@code false} if attempting to disable the status bar failed. {@code true} otherwise.
- * @throws SecurityException if {@code admin} is not a device owner.
+ * @throws SecurityException if {@code admin} is not the device owner, or a profile owner of
+ * secondary user that is affiliated with the device.
+ * @see #isAffiliatedUser
+ * @see #getSecondaryUsers
*/
public boolean setStatusBarDisabled(@NonNull ComponentName admin, boolean disabled) {
throwIfParentInstance("setStatusBarDisabled");
@@ -8100,6 +8230,47 @@ public class DevicePolicyManager {
}
/**
+ * Called by a device or profile owner to restrict packages from accessing metered data.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @param packageNames the list of package names to be restricted.
+ * @return a list of package names which could not be restricted.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public @NonNull List<String> setMeteredDataDisabled(@NonNull ComponentName admin,
+ @NonNull List<String> packageNames) {
+ throwIfParentInstance("setMeteredDataDisabled");
+ if (mService != null) {
+ try {
+ return mService.setMeteredDataDisabled(admin, packageNames);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return packageNames;
+ }
+
+ /**
+ * Called by a device or profile owner to retrieve the list of packages which are restricted
+ * by the admin from accessing metered data.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @return the list of restricted package names.
+ * @throws SecurityException if {@code admin} is not a device or profile owner.
+ */
+ public @NonNull List<String> getMeteredDataDisabled(@NonNull ComponentName admin) {
+ throwIfParentInstance("getMeteredDataDisabled");
+ if (mService != null) {
+ try {
+ return mService.getMeteredDataDisabled(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+ return new ArrayList<>();
+ }
+
+ /**
* Called by device owners to retrieve device logs from before the device's last reboot.
* <p>
* <strong> This API is not supported on all devices. Calling this API on unsupported devices
@@ -8511,6 +8682,13 @@ public class DevicePolicyManager {
*
* <p> Backup service is off by default when device owner is present.
*
+ * <p> If backups are made mandatory by specifying a non-null mandatory backup transport using
+ * the {@link DevicePolicyManager#setMandatoryBackupTransport} method, the backup service is
+ * automatically enabled.
+ *
+ * <p> If the backup service is disabled using this method after the mandatory backup transport
+ * has been set, the mandatory backup transport is cleared.
+ *
* @param admin Which {@link DeviceAdminReceiver} this request is associated with.
* @param enabled {@code true} to enable the backup service, {@code false} to disable it.
* @throws SecurityException if {@code admin} is not a device owner.
@@ -8542,6 +8720,43 @@ public class DevicePolicyManager {
}
/**
+ * Makes backups mandatory and enforces the usage of the specified backup transport.
+ *
+ * <p>When a {@code null} backup transport is specified, backups are made optional again.
+ * <p>Only device owner can call this method.
+ * <p>If backups were disabled and a non-null backup transport {@link ComponentName} is
+ * specified, backups will be enabled.
+ *
+ * @param admin admin Which {@link DeviceAdminReceiver} this request is associated with.
+ * @param backupTransportComponent The backup transport layer to be used for mandatory backups.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setMandatoryBackupTransport(
+ @NonNull ComponentName admin, @Nullable ComponentName backupTransportComponent) {
+ try {
+ mService.setMandatoryBackupTransport(admin, backupTransportComponent);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the backup transport which has to be used for backups if backups are mandatory or
+ * {@code null} if backups are not mandatory.
+ *
+ * @return a {@link ComponentName} of the backup transport layer to be used if backups are
+ * mandatory or {@code null} if backups are not mandatory.
+ */
+ public ComponentName getMandatoryBackupTransport() {
+ try {
+ return mService.getMandatoryBackupTransport();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+
+ /**
* Called by a device owner to control the network logging feature.
*
* <p> Network logs contain DNS lookup and connect() library call events. The following library
@@ -8817,15 +9032,6 @@ 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
@@ -8836,14 +9042,14 @@ 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 executor The executor through which the listener should be invoked.
+ * @param listener A callback object that will inform the caller when the clearing is done.
* @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 @CallbackExecutor Executor executor) {
+ @NonNull String packageName, @NonNull @CallbackExecutor Executor executor,
+ @NonNull OnClearApplicationUserDataListener listener) {
throwIfParentInstance("clearAppData");
Preconditions.checkNotNull(executor);
try {
@@ -8926,41 +9132,312 @@ public class DevicePolicyManager {
}
}
- //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.
+ * Changes the current administrator to another one. 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:
+ * <p>Depending on the current administrator (device owner, 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
+ * <p>Use the {@code bundle} parameter to pass data to the new administrator. The data
+ * will be received in the
+ * {@link DeviceAdminReceiver#onTransferOwnershipComplete(Context, PersistableBundle)}
+ * callback of the new administrator.
+ *
+ * <p>The transfer has failed if the original administrator is still the corresponding owner
+ * after calling this method.
+ *
+ * <p>The incoming target administrator must have the
+ * {@link DeviceAdminReceiver#SUPPORT_TRANSFER_OWNERSHIP_META_DATA} <code>meta-data</code> tag
+ * included in its corresponding <code>receiver</code> component with a value of {@code true}.
+ * Otherwise an {@link IllegalArgumentException} will be thrown.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @param target which {@link DeviceAdminReceiver} we want the new administrator to be
+ * @param bundle data to be sent to the new administrator
+ * @throws SecurityException if {@code admin} is not a device owner nor a profile owner
+ * @throws IllegalArgumentException if {@code admin} or {@code target} is {@code null}, they
+ * are components in the same package or {@code target} is not an active admin
+ */
+ public void transferOwnership(@NonNull ComponentName admin, @NonNull ComponentName target,
+ @Nullable PersistableBundle bundle) {
+ throwIfParentInstance("transferOwnership");
+ try {
+ mService.transferOwnership(admin, target, bundle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a device owner to specify the user session start message. This may be displayed
+ * during a user switch.
+ * <p>
+ * The message should be limited to a short statement or it may be truncated.
+ * <p>
+ * If the message needs to be localized, it is the responsibility of the
+ * {@link DeviceAdminReceiver} to listen to the {@link Intent#ACTION_LOCALE_CHANGED} broadcast
+ * and set a new version of this message accordingly.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @param startUserSessionMessage message for starting user session, or {@code null} to use
+ * system default message.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setStartUserSessionMessage(
+ @NonNull ComponentName admin, @Nullable CharSequence startUserSessionMessage) {
+ throwIfParentInstance("setStartUserSessionMessage");
+ try {
+ mService.setStartUserSessionMessage(admin, startUserSessionMessage);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by a device owner to specify the user session end message. This may be displayed
+ * during a user switch.
+ * <p>
+ * The message should be limited to a short statement or it may be truncated.
+ * <p>
+ * If the message needs to be localized, it is the responsibility of the
+ * {@link DeviceAdminReceiver} to listen to the {@link Intent#ACTION_LOCALE_CHANGED} broadcast
+ * and set a new version of this message accordingly.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @param endUserSessionMessage message for ending user session, or {@code null} to use system
+ * default message.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setEndUserSessionMessage(
+ @NonNull ComponentName admin, @Nullable CharSequence endUserSessionMessage) {
+ throwIfParentInstance("setEndUserSessionMessage");
+ try {
+ mService.setEndUserSessionMessage(admin, endUserSessionMessage);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the user session start message.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public CharSequence getStartUserSessionMessage(@NonNull ComponentName admin) {
+ throwIfParentInstance("getStartUserSessionMessage");
+ try {
+ return mService.getStartUserSessionMessage(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the user session end message.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public CharSequence getEndUserSessionMessage(@NonNull ComponentName admin) {
+ throwIfParentInstance("getEndUserSessionMessage");
+ try {
+ return mService.getEndUserSessionMessage(admin);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Allows/disallows printing.
+ *
+ * Called by a device owner or a profile owner.
+ * Device owner changes policy for all users. Profile owner can override it if present.
+ * Printing is enabled by default. If {@code FEATURE_PRINTING} is absent, the call is ignored.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+ * @param enabled whether printing should be allowed or not.
+ * @throws SecurityException if {@code admin} is neither device, nor profile owner.
+ */
+ public void setPrintingEnabled(@NonNull ComponentName admin, boolean enabled) {
+ try {
+ mService.setPrintingEnabled(admin, enabled);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns whether printing is enabled for this user.
+ *
+ * Always {@code false} if {@code FEATURE_PRINTING} is absent.
+ * Otherwise, {@code true} by default.
+ *
+ * @return {@code true} iff printing is enabled.
+ */
+ public boolean isPrintingEnabled() {
+ try {
+ return mService.isPrintingEnabled();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by device owner to add an override APN.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @param apnSetting the override APN to insert
+ * @return The {@code id} of inserted override APN. Or {@code -1} when failed to insert into
+ * the database.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ *
+ * @see #setOverrideApnsEnabled(ComponentName, boolean)
+ */
+ public int addOverrideApn(@NonNull ComponentName admin, @NonNull ApnSetting apnSetting) {
+ throwIfParentInstance("addOverrideApn");
+ if (mService != null) {
+ try {
+ return mService.addOverrideApn(admin, apnSetting);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Called by device owner to update an override APN.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @param apnId the {@code id} of the override APN to update
+ * @param apnSetting the override APN to update
+ * @return {@code true} if the required override APN is successfully updated,
+ * {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ *
+ * @see #setOverrideApnsEnabled(ComponentName, boolean)
+ */
+ public boolean updateOverrideApn(@NonNull ComponentName admin, int apnId,
+ @NonNull ApnSetting apnSetting) {
+ throwIfParentInstance("updateOverrideApn");
+ if (mService != null) {
+ try {
+ return mService.updateOverrideApn(admin, apnId, apnSetting);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by device owner to remove an override APN.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @param apnId the {@code id} of the override APN to remove
+ * @return {@code true} if the required override APN is successfully removed, {@code false}
+ * otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ *
+ * @see #setOverrideApnsEnabled(ComponentName, boolean)
+ */
+ public boolean removeOverrideApn(@NonNull ComponentName admin, int apnId) {
+ throwIfParentInstance("removeOverrideApn");
+ if (mService != null) {
+ try {
+ return mService.removeOverrideApn(admin, apnId);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by device owner to get all override APNs inserted by device owner.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @return A list of override APNs inserted by device owner.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ *
+ * @see #setOverrideApnsEnabled(ComponentName, boolean)
+ */
+ public List<ApnSetting> getOverrideApns(@NonNull ComponentName admin) {
+ throwIfParentInstance("getOverrideApns");
+ if (mService != null) {
+ try {
+ return mService.getOverrideApns(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Called by device owner to set if override APNs should be enabled.
+ * <p> Override APNs are separated from other APNs on the device, and can only be inserted or
+ * modified by the device owner. When enabled, only override APNs are in use, any other APNs
+ * are ignored.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @param enabled {@code true} if override APNs should be enabled, {@code false} otherwise
+ * @throws SecurityException if {@code admin} is not a device owner.
+ */
+ public void setOverrideApnsEnabled(@NonNull ComponentName admin, boolean enabled) {
+ throwIfParentInstance("setOverrideApnEnabled");
+ if (mService != null) {
+ try {
+ mService.setOverrideApnsEnabled(admin, enabled);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ }
+
+ /**
+ * Called by device owner to check if override APNs are currently enabled.
+ *
+ * @param admin which {@link DeviceAdminReceiver} this request is associated with
+ * @return {@code true} if override APNs are currently enabled, {@code false} otherwise.
+ * @throws SecurityException if {@code admin} is not a device owner.
+ *
+ * @see #setOverrideApnsEnabled(ComponentName, boolean)
+ */
+ public boolean isOverrideApnEnabled(@NonNull ComponentName admin) {
+ throwIfParentInstance("isOverrideApnEnabled");
+ if (mService != null) {
+ try {
+ return mService.isOverrideApnEnabled(admin);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the data passed from the current administrator to the new administrator during an
+ * ownership transfer. This is the same {@code bundle} passed in
+ * {@link #transferOwnership(ComponentName, ComponentName, PersistableBundle)}.
+ *
+ * <p>Returns <code>null</code> if no ownership transfer was started for the calling user.
+ *
+ * @see #transferOwnership
+ * @see DeviceAdminReceiver#onTransferOwnershipComplete(Context, PersistableBundle)
*/
- public void transferOwner(@NonNull ComponentName admin, @NonNull ComponentName target,
- PersistableBundle bundle) {
- throwIfParentInstance("transferOwner");
+ @Nullable
+ public PersistableBundle getTransferOwnershipBundle() {
+ throwIfParentInstance("getTransferOwnershipBundle");
try {
- mService.transferOwner(admin, target, bundle);
+ return mService.getTransferOwnershipBundle();
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
}
diff --git a/android/app/admin/DevicePolicyManagerInternal.java b/android/app/admin/DevicePolicyManagerInternal.java
index b692ffd9..ebaf4648 100644
--- a/android/app/admin/DevicePolicyManagerInternal.java
+++ b/android/app/admin/DevicePolicyManagerInternal.java
@@ -123,4 +123,22 @@ public abstract class DevicePolicyManagerInternal {
* @param userId User ID of the profile.
*/
public abstract void reportSeparateProfileChallengeChanged(@UserIdInt int userId);
+
+ /**
+ * Check whether the user could have their password reset in an untrusted manor due to there
+ * being an admin which can call {@link #resetPassword} to reset the password without knowledge
+ * of the previous password.
+ *
+ * @param userId The user in question
+ */
+ public abstract boolean canUserHaveUntrustedCredentialReset(@UserIdInt int userId);
+
+ /**
+ * Return text of error message if printing is disabled.
+ * Called by Print Service when printing is disabled by PO or DO when printing is attempted.
+ *
+ * @param userId The user in question
+ * @return localized error message
+ */
+ public abstract CharSequence getPrintingDisabledReasonForUser(@UserIdInt int userId);
}
diff --git a/android/app/admin/DnsEvent.java b/android/app/admin/DnsEvent.java
index 4ddf13e0..a2d704b8 100644
--- a/android/app/admin/DnsEvent.java
+++ b/android/app/admin/DnsEvent.java
@@ -96,7 +96,7 @@ public final class DnsEvent extends NetworkEvent implements Parcelable {
@Override
public String toString() {
- return String.format("DnsEvent(%s, %s, %d, %d, %s)", mHostname,
+ return String.format("DnsEvent(%d, %s, %s, %d, %d, %s)", mId, mHostname,
(mIpAddresses == null) ? "NONE" : String.join(" ", mIpAddresses),
mIpAddressesCount, mTimestamp, mPackageName);
}
diff --git a/android/app/assist/AssistStructure.java b/android/app/assist/AssistStructure.java
index 7b549cd5..87f22712 100644
--- a/android/app/assist/AssistStructure.java
+++ b/android/app/assist/AssistStructure.java
@@ -32,6 +32,8 @@ import android.view.WindowManagerGlobal;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
+import com.android.internal.util.Preconditions;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -624,6 +626,7 @@ public class AssistStructure implements Parcelable {
int mMinEms = -1;
int mMaxEms = -1;
int mMaxLength = -1;
+ @Nullable String mTextIdEntry;
// POJO used to override some autofill-related values when the node is parcelized.
// Not written to parcel.
@@ -701,7 +704,7 @@ public class AssistStructure implements Parcelable {
final int flags = mFlags;
if ((flags&FLAGS_HAS_ID) != 0) {
mId = in.readInt();
- if (mId != 0) {
+ if (mId != View.NO_ID) {
mIdEntry = preader.readString();
if (mIdEntry != null) {
mIdType = preader.readString();
@@ -724,6 +727,7 @@ public class AssistStructure implements Parcelable {
mMinEms = in.readInt();
mMaxEms = in.readInt();
mMaxLength = in.readInt();
+ mTextIdEntry = preader.readString();
}
if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) {
mX = in.readInt();
@@ -857,7 +861,7 @@ public class AssistStructure implements Parcelable {
out.writeInt(writtenFlags);
if ((flags&FLAGS_HAS_ID) != 0) {
out.writeInt(mId);
- if (mId != 0) {
+ if (mId != View.NO_ID) {
pwriter.writeString(mIdEntry);
if (mIdEntry != null) {
pwriter.writeString(mIdType);
@@ -890,6 +894,7 @@ public class AssistStructure implements Parcelable {
out.writeInt(mMinEms);
out.writeInt(mMaxEms);
out.writeInt(mMaxLength);
+ pwriter.writeString(mTextIdEntry);
}
if ((flags&FLAGS_HAS_LARGE_COORDS) != 0) {
out.writeInt(mX);
@@ -1430,6 +1435,17 @@ public class AssistStructure implements Parcelable {
}
/**
+ * Gets the identifier used to set the text associated with this view.
+ *
+ * <p>It's only relevant when the {@link AssistStructure} is used for autofill purposes,
+ * not for assist purposes.
+ */
+ @Nullable
+ public String getTextIdEntry() {
+ return mTextIdEntry;
+ }
+
+ /**
* Return additional hint text associated with the node; this is typically used with
* a node that takes user input, describing to the user what the input means.
*/
@@ -1684,6 +1700,11 @@ public class AssistStructure implements Parcelable {
}
@Override
+ public void setTextIdEntry(@NonNull String entryName) {
+ mNode.mTextIdEntry = Preconditions.checkNotNull(entryName);
+ }
+
+ @Override
public void setHint(CharSequence hint) {
getNodeText().mHint = hint != null ? hint.toString() : null;
}
@@ -2082,6 +2103,7 @@ public class AssistStructure implements Parcelable {
Log.i(TAG, prefix + " Text color fg: #" + Integer.toHexString(node.getTextColor())
+ ", bg: #" + Integer.toHexString(node.getTextBackgroundColor()));
Log.i(TAG, prefix + " Input type: " + node.getInputType());
+ Log.i(TAG, prefix + " Resource id: " + node.getTextIdEntry());
}
String webDomain = node.getWebDomain();
if (webDomain != null) {
diff --git a/android/app/backup/BackupManager.java b/android/app/backup/BackupManager.java
index 6512b98c..12f44831 100644
--- a/android/app/backup/BackupManager.java
+++ b/android/app/backup/BackupManager.java
@@ -27,6 +27,7 @@ import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.UserHandle;
import android.util.Log;
import android.util.Pair;
@@ -387,6 +388,29 @@ public class BackupManager {
}
/**
+ * Report whether the backup mechanism is currently active.
+ * When it is inactive, the device will not perform any backup operations, nor will it
+ * deliver data for restore, although clients can still safely call BackupManager methods.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.BACKUP)
+ public boolean isBackupServiceActive(UserHandle user) {
+ mContext.enforceCallingPermission(android.Manifest.permission.BACKUP,
+ "isBackupServiceActive");
+ checkServiceBinder();
+ if (sService != null) {
+ try {
+ return sService.isBackupServiceActive(user.getIdentifier());
+ } catch (RemoteException e) {
+ Log.e(TAG, "isBackupEnabled() couldn't connect");
+ }
+ }
+ return false;
+ }
+
+ /**
* Enable/disable data restore at application install time. When enabled, app
* installation will include an attempt to fetch the app's historical data from
* the archival restore dataset (if any). When disabled, no such attempt will
@@ -707,7 +731,6 @@ public class BackupManager {
* redirects them into main-thread actions. This serializes the backup
* progress callbacks nicely within the usual main-thread lifecycle pattern.
*/
- @SystemApi
private class BackupObserverWrapper extends IBackupObserver.Stub {
final Handler mHandler;
final BackupObserver mObserver;
diff --git a/android/app/backup/BackupManagerMonitor.java b/android/app/backup/BackupManagerMonitor.java
index ae4a98a4..a91aded1 100644
--- a/android/app/backup/BackupManagerMonitor.java
+++ b/android/app/backup/BackupManagerMonitor.java
@@ -172,6 +172,12 @@ public class BackupManagerMonitor {
public static final int LOG_EVENT_ID_NO_PACKAGES = 49;
public static final int LOG_EVENT_ID_TRANSPORT_IS_NULL = 50;
+ /**
+ * The transport returned {@link BackupTransport#TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED}.
+ * @hide
+ */
+ public static final int LOG_EVENT_ID_TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED = 51;
+
diff --git a/android/app/backup/BackupTransport.java b/android/app/backup/BackupTransport.java
index da81d19c..266f58df 100644
--- a/android/app/backup/BackupTransport.java
+++ b/android/app/backup/BackupTransport.java
@@ -51,10 +51,40 @@ public class BackupTransport {
public static final int AGENT_UNKNOWN = -1004;
public static final int TRANSPORT_QUOTA_EXCEEDED = -1005;
+ /**
+ * Indicates that the transport cannot accept a diff backup for this package.
+ *
+ * <p>Backup manager should clear its state for this package and immediately retry a
+ * non-incremental backup. This might be used if the transport no longer has data for this
+ * package in its backing store.
+ *
+ * <p>This is only valid when backup manager called {@link
+ * #performBackup(PackageInfo, ParcelFileDescriptor, int)} with {@link #FLAG_INCREMENTAL}.
+ *
+ * @hide
+ */
+ public static final int TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED = -1006;
+
// Indicates that operation was initiated by user, not a scheduled one.
// Transport should ignore its own moratoriums for call with this flag set.
public static final int FLAG_USER_INITIATED = 1;
+ /**
+ * For key value backup, indicates that the backup data is a diff from a previous backup. The
+ * transport must apply this diff to an existing backup to build the new backup set.
+ *
+ * @hide
+ */
+ public static final int FLAG_INCREMENTAL = 1 << 1;
+
+ /**
+ * For key value backup, indicates that the backup data is a complete set, not a diff from a
+ * previous backup. The transport should clear any previous backup when storing this backup.
+ *
+ * @hide
+ */
+ public static final int FLAG_NON_INCREMENTAL = 1 << 2;
+
IBackupTransport mBinderImpl = new TransportImpl();
public IBinder getBinder() {
@@ -231,18 +261,33 @@ public class BackupTransport {
* {@link #TRANSPORT_OK}, {@link #finishBackup} will then be called to ensure the data
* is sent and recorded successfully.
*
+ * If the backup data is a diff against the previous backup then the flag {@link
+ * BackupTransport#FLAG_INCREMENTAL} will be set. Otherwise, if the data is a complete backup
+ * set then {@link BackupTransport#FLAG_NON_INCREMENTAL} will be set. Before P neither flag will
+ * be set regardless of whether the backup is incremental or not.
+ *
+ * <p>If {@link BackupTransport#FLAG_INCREMENTAL} is set and the transport does not have data
+ * for this package in its storage backend then it cannot apply the incremental diff. Thus it
+ * should return {@link BackupTransport#TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED} to indicate
+ * that backup manager should delete its state and retry the package as a non-incremental
+ * backup. Before P, or if this is a non-incremental backup, then this return code is equivalent
+ * to {@link BackupTransport#TRANSPORT_ERROR}.
+ *
* @param packageInfo The identity of the application whose data is being backed up.
* This specifically includes the signature list for the package.
* @param inFd Descriptor of file with data that resulted from invoking the application's
* BackupService.doBackup() method. This may be a pipe rather than a file on
* persistent media, so it may not be seekable.
- * @param flags {@link BackupTransport#FLAG_USER_INITIATED} or 0.
+ * @param flags a combination of {@link BackupTransport#FLAG_USER_INITIATED}, {@link
+ * BackupTransport#FLAG_NON_INCREMENTAL}, {@link BackupTransport#FLAG_INCREMENTAL}, or 0.
* @return one of {@link BackupTransport#TRANSPORT_OK} (OK so far),
* {@link BackupTransport#TRANSPORT_PACKAGE_REJECTED} (to suppress backup of this
* specific package, but allow others to proceed),
- * {@link BackupTransport#TRANSPORT_ERROR} (on network error or other failure), or
- * {@link BackupTransport#TRANSPORT_NOT_INITIALIZED} (if the backend dataset has
- * become lost due to inactivity purge or some other reason and needs re-initializing)
+ * {@link BackupTransport#TRANSPORT_ERROR} (on network error or other failure), {@link
+ * BackupTransport#TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED} (if the transport cannot accept
+ * an incremental backup for this package), or {@link
+ * BackupTransport#TRANSPORT_NOT_INITIALIZED} (if the backend dataset has become lost due to
+ * inactivity purge or some other reason and needs re-initializing)
*/
public int performBackup(PackageInfo packageInfo, ParcelFileDescriptor inFd, int flags) {
return performBackup(packageInfo, inFd);
diff --git a/android/app/job/JobInfo.java b/android/app/job/JobInfo.java
index 7c40b4ea..cba9dcc3 100644
--- a/android/app/job/JobInfo.java
+++ b/android/app/job/JobInfo.java
@@ -253,6 +253,11 @@ public class JobInfo implements Parcelable {
/**
* @hide
*/
+ public static final int FLAG_IS_PREFETCH = 1 << 2;
+
+ /**
+ * @hide
+ */
public static final int CONSTRAINT_FLAG_CHARGING = 1 << 0;
/**
@@ -1364,6 +1369,28 @@ public class JobInfo implements Parcelable {
}
/**
+ * Setting this to true indicates that this job is designed to prefetch
+ * content that will make a material improvement to the experience of
+ * the specific user of this device. For example, fetching top headlines
+ * of interest to the current user.
+ * <p>
+ * The system may use this signal to relax the network constraints you
+ * originally requested, such as allowing a
+ * {@link JobInfo#NETWORK_TYPE_UNMETERED} job to run over a metered
+ * network when there is a surplus of metered data available. The system
+ * may also use this signal in combination with end user usage patterns
+ * to ensure data is prefetched before the user launches your app.
+ */
+ public Builder setIsPrefetch(boolean isPrefetch) {
+ if (isPrefetch) {
+ mFlags |= FLAG_IS_PREFETCH;
+ } else {
+ mFlags &= (~FLAG_IS_PREFETCH);
+ }
+ return this;
+ }
+
+ /**
* Set whether or not to persist this job across device reboots.
*
* @param isPersisted True to indicate that the job will be written to
diff --git a/android/app/job/JobParameters.java b/android/app/job/JobParameters.java
index 5053dc6f..c71bf2e6 100644
--- a/android/app/job/JobParameters.java
+++ b/android/app/job/JobParameters.java
@@ -70,6 +70,7 @@ public class JobParameters implements Parcelable {
private final Network network;
private int stopReason; // Default value of stopReason is REASON_CANCELED
+ private String debugStopReason; // Human readable stop reason for debugging.
/** @hide */
public JobParameters(IBinder callback, int jobId, PersistableBundle extras,
@@ -104,6 +105,14 @@ public class JobParameters implements Parcelable {
}
/**
+ * Reason onStopJob() was called on this job.
+ * @hide
+ */
+ public String getDebugStopReason() {
+ return debugStopReason;
+ }
+
+ /**
* @return The extras you passed in when constructing this job with
* {@link android.app.job.JobInfo.Builder#setExtras(android.os.PersistableBundle)}. This will
* never be null. If you did not set any extras this will be an empty bundle.
@@ -288,11 +297,13 @@ public class JobParameters implements Parcelable {
network = null;
}
stopReason = in.readInt();
+ debugStopReason = in.readString();
}
/** @hide */
- public void setStopReason(int reason) {
+ public void setStopReason(int reason, String debugStopReason) {
stopReason = reason;
+ this.debugStopReason = debugStopReason;
}
@Override
@@ -323,6 +334,7 @@ public class JobParameters implements Parcelable {
dest.writeInt(0);
}
dest.writeInt(stopReason);
+ dest.writeString(debugStopReason);
}
public static final Creator<JobParameters> CREATOR = new Creator<JobParameters>() {
diff --git a/android/app/servertransaction/ActivityLifecycleItem.java b/android/app/servertransaction/ActivityLifecycleItem.java
index 0fdc7c56..9a50a009 100644
--- a/android/app/servertransaction/ActivityLifecycleItem.java
+++ b/android/app/servertransaction/ActivityLifecycleItem.java
@@ -17,7 +17,9 @@
package android.app.servertransaction;
import android.annotation.IntDef;
+import android.os.Parcel;
+import java.io.PrintWriter;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -26,6 +28,7 @@ import java.lang.annotation.RetentionPolicy;
* @hide
*/
public abstract class ActivityLifecycleItem extends ClientTransactionItem {
+ private String mDescription;
@IntDef(prefix = { "UNDEFINED", "PRE_", "ON_" }, value = {
UNDEFINED,
@@ -53,4 +56,39 @@ public abstract class ActivityLifecycleItem extends ClientTransactionItem {
/** A final lifecycle state that an activity should reach. */
@LifecycleState
public abstract int getTargetState();
+
+
+ protected ActivityLifecycleItem() {
+ }
+
+ protected ActivityLifecycleItem(Parcel in) {
+ mDescription = in.readString();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mDescription);
+ }
+
+ /**
+ * Sets a description that can be retrieved later for debugging purposes.
+ * @param description Description to set.
+ * @return The {@link ActivityLifecycleItem}.
+ */
+ public ActivityLifecycleItem setDescription(String description) {
+ mDescription = description;
+ return this;
+ }
+
+ /**
+ * Retrieves description if set through {@link #setDescription(String)}.
+ */
+ public String getDescription() {
+ return mDescription;
+ }
+
+ void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + "target state:" + getTargetState());
+ pw.println(prefix + "description: " + mDescription);
+ }
}
diff --git a/android/app/servertransaction/ClientTransaction.java b/android/app/servertransaction/ClientTransaction.java
index 3c96f069..fc078798 100644
--- a/android/app/servertransaction/ClientTransaction.java
+++ b/android/app/servertransaction/ClientTransaction.java
@@ -24,6 +24,9 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteException;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -83,7 +86,8 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem {
}
/** Get the target state lifecycle request. */
- ActivityLifecycleItem getLifecycleStateRequest() {
+ @VisibleForTesting
+ public ActivityLifecycleItem getLifecycleStateRequest() {
return mLifecycleStateRequest;
}
@@ -234,4 +238,12 @@ public class ClientTransaction implements Parcelable, ObjectPoolItem {
result = 31 * result + Objects.hashCode(mLifecycleStateRequest);
return result;
}
+
+ void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + "mActivityToken:" + mActivityToken.hashCode());
+ pw.println(prefix + "mLifecycleStateRequest:");
+ if (mLifecycleStateRequest != null) {
+ mLifecycleStateRequest.dump(pw, prefix + " ");
+ }
+ }
}
diff --git a/android/app/servertransaction/DestroyActivityItem.java b/android/app/servertransaction/DestroyActivityItem.java
index 83da5f33..cbcf6c75 100644
--- a/android/app/servertransaction/DestroyActivityItem.java
+++ b/android/app/servertransaction/DestroyActivityItem.java
@@ -76,12 +76,14 @@ public class DestroyActivityItem extends ActivityLifecycleItem {
/** Write to Parcel. */
@Override
public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
dest.writeBoolean(mFinished);
dest.writeInt(mConfigChanges);
}
/** Read from Parcel. */
private DestroyActivityItem(Parcel in) {
+ super(in);
mFinished = in.readBoolean();
mConfigChanges = in.readInt();
}
diff --git a/android/app/servertransaction/PauseActivityItem.java b/android/app/servertransaction/PauseActivityItem.java
index 880fef73..70a4755f 100644
--- a/android/app/servertransaction/PauseActivityItem.java
+++ b/android/app/servertransaction/PauseActivityItem.java
@@ -114,6 +114,7 @@ public class PauseActivityItem extends ActivityLifecycleItem {
/** Write to Parcel. */
@Override
public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
dest.writeBoolean(mFinished);
dest.writeBoolean(mUserLeaving);
dest.writeInt(mConfigChanges);
@@ -122,6 +123,7 @@ public class PauseActivityItem extends ActivityLifecycleItem {
/** Read from Parcel. */
private PauseActivityItem(Parcel in) {
+ super(in);
mFinished = in.readBoolean();
mUserLeaving = in.readBoolean();
mConfigChanges = in.readInt();
diff --git a/android/app/servertransaction/ResumeActivityItem.java b/android/app/servertransaction/ResumeActivityItem.java
index 9249c6e8..ed90f2cb 100644
--- a/android/app/servertransaction/ResumeActivityItem.java
+++ b/android/app/servertransaction/ResumeActivityItem.java
@@ -113,6 +113,7 @@ public class ResumeActivityItem extends ActivityLifecycleItem {
/** Write to Parcel. */
@Override
public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
dest.writeInt(mProcState);
dest.writeBoolean(mUpdateProcState);
dest.writeBoolean(mIsForward);
@@ -120,6 +121,7 @@ public class ResumeActivityItem extends ActivityLifecycleItem {
/** Read from Parcel. */
private ResumeActivityItem(Parcel in) {
+ super(in);
mProcState = in.readInt();
mUpdateProcState = in.readBoolean();
mIsForward = in.readBoolean();
diff --git a/android/app/servertransaction/StopActivityItem.java b/android/app/servertransaction/StopActivityItem.java
index 5c5c3041..b814d1ae 100644
--- a/android/app/servertransaction/StopActivityItem.java
+++ b/android/app/servertransaction/StopActivityItem.java
@@ -83,12 +83,14 @@ public class StopActivityItem extends ActivityLifecycleItem {
/** Write to Parcel. */
@Override
public void writeToParcel(Parcel dest, int flags) {
+ super.writeToParcel(dest, flags);
dest.writeBoolean(mShowWindow);
dest.writeInt(mConfigChanges);
}
/** Read from Parcel. */
private StopActivityItem(Parcel in) {
+ super(in);
mShowWindow = in.readBoolean();
mConfigChanges = in.readInt();
}
diff --git a/android/app/servertransaction/TransactionExecutor.java b/android/app/servertransaction/TransactionExecutor.java
index 5b0ea6b1..78b393a8 100644
--- a/android/app/servertransaction/TransactionExecutor.java
+++ b/android/app/servertransaction/TransactionExecutor.java
@@ -33,6 +33,8 @@ import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
+import java.io.PrintWriter;
+import java.io.StringWriter;
import java.util.List;
/**
@@ -122,6 +124,21 @@ public class TransactionExecutor {
final IBinder token = transaction.getActivityToken();
final ActivityClientRecord r = mTransactionHandler.getActivityClient(token);
+ // TODO(b/71506345): Remove once root cause is found.
+ if (r == null) {
+ final StringWriter stringWriter = new StringWriter();
+ final PrintWriter pw = new PrintWriter(stringWriter);
+ final String prefix = " ";
+
+ pw.println("Lifecycle transaction does not have valid ActivityClientRecord.");
+ pw.println("Transaction:");
+ transaction.dump(pw, prefix);
+ pw.println("Executor:");
+ dump(pw, prefix);
+
+ Slog.wtf(TAG, stringWriter.toString());
+ }
+
// Cycle to the state right before the final requested state.
cycleToPath(r, lifecycleItem.getTargetState(), true /* excludeLastState */);
@@ -245,4 +262,9 @@ public class TransactionExecutor {
private static void log(String message) {
if (DEBUG_RESOLVER) Slog.d(TAG, message);
}
+
+ private void dump(PrintWriter pw, String prefix) {
+ pw.println(prefix + "mTransactionHandler:");
+ mTransactionHandler.dump(pw, prefix + " ");
+ }
}
diff --git a/android/app/slice/Slice.java b/android/app/slice/Slice.java
index 5c7f6741..5808f8b5 100644
--- a/android/app/slice/Slice.java
+++ b/android/app/slice/Slice.java
@@ -21,12 +21,10 @@ import android.annotation.Nullable;
import android.annotation.StringDef;
import android.app.PendingIntent;
import android.app.RemoteInput;
-import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.Context;
import android.content.IContentProvider;
import android.content.Intent;
-import android.content.pm.ResolveInfo;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
@@ -67,6 +65,7 @@ public final class Slice implements Parcelable {
HINT_TOGGLE,
HINT_HORIZONTAL,
HINT_PARTIAL,
+ HINT_SEE_MORE
})
@Retention(RetentionPolicy.SOURCE)
public @interface SliceHint {}
@@ -151,7 +150,19 @@ public final class Slice implements Parcelable {
* Used to indicate the maximum integer value for a {@link #SUBTYPE_SLIDER}.
*/
public static final String HINT_MAX = "max";
-
+ /**
+ * A hint representing that this item should be used to indicate that there's more
+ * content associated with this slice.
+ */
+ public static final String HINT_SEE_MORE = "see_more";
+ /**
+ * A hint used when implementing app-specific slice permissions.
+ * Tells the system that for this slice the return value of
+ * {@link SliceProvider#onBindSlice(Uri, List)} may be different depending on
+ * {@link SliceProvider#getBindingPackage} and should not be cached for multiple
+ * apps.
+ */
+ public static final String HINT_CALLER_NEEDED = "caller_needed";
/**
* Key to retrieve an extra added to an intent when a control is changed.
*/
@@ -184,6 +195,10 @@ public final class Slice implements Parcelable {
* Subtype to tag an item representing priority.
*/
public static final String SUBTYPE_PRIORITY = "priority";
+ /**
+ * Subtype to tag an item to use as a content description.
+ */
+ public static final String SUBTYPE_CONTENT_DESCRIPTION = "content_description";
private final SliceItem[] mItems;
private final @SliceHint String[] mHints;
@@ -415,28 +430,6 @@ 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_INT, subType, hints));
- return this;
- }
-
- /**
- * 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) {
- return addColor(color, subType, hints.toArray(new String[hints.size()]));
- }
-
- /**
- * 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));
@@ -549,16 +542,11 @@ public final class Slice implements Parcelable {
}
/**
- * Turns a slice Uri into slice content.
- *
- * @param resolver ContentResolver to be used.
- * @param uri The URI to a slice provider
- * @param supportedSpecs List of supported specs.
- * @return The Slice provided by the app or null if none is given.
- * @see Slice
- */
- public static @Nullable Slice bindSlice(ContentResolver resolver, @NonNull Uri uri,
- List<SliceSpec> supportedSpecs) {
+ * @deprecated TO BE REMOVED.
+ */
+ @Deprecated
+ public static @Nullable Slice bindSlice(ContentResolver resolver,
+ @NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
Preconditions.checkNotNull(uri, "uri");
IContentProvider provider = resolver.acquireProvider(uri);
if (provider == null) {
@@ -586,60 +574,11 @@ public final class Slice implements Parcelable {
}
/**
- * Turns a slice intent into slice content. Expects an explicit intent. If there is no
- * {@link ContentProvider} associated with the given intent this will throw
- * {@link IllegalArgumentException}.
- *
- * @param context The context to use.
- * @param intent The intent associated with a slice.
- * @param supportedSpecs List of supported specs.
- * @return The Slice provided by the app or null if none is given.
- * @see Slice
- * @see SliceProvider#onMapIntentToUri(Intent)
- * @see Intent
+ * @deprecated TO BE REMOVED.
*/
+ @Deprecated
public static @Nullable Slice bindSlice(Context context, @NonNull Intent intent,
- List<SliceSpec> supportedSpecs) {
- Preconditions.checkNotNull(intent, "intent");
- Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null,
- "Slice intent must be explicit " + intent);
- 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 && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
- return bindSlice(resolver, intentData, supportedSpecs);
- }
- // Otherwise ask the app
- List<ResolveInfo> providers =
- context.getPackageManager().queryIntentContentProviders(intent, 0);
- if (providers == null) {
- throw new IllegalArgumentException("Unable to resolve intent " + intent);
- }
- String authority = providers.get(0).providerInfo.authority;
- Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
- .authority(authority).build();
- IContentProvider provider = resolver.acquireProvider(uri);
- if (provider == null) {
- throw new IllegalArgumentException("Unknown URI " + uri);
- }
- try {
- Bundle extras = new Bundle();
- extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
- extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
- new ArrayList<>(supportedSpecs));
- final Bundle res = provider.call(resolver.getPackageName(),
- SliceProvider.METHOD_MAP_INTENT, null, extras);
- if (res == null) {
- return null;
- }
- return res.getParcelable(SliceProvider.EXTRA_SLICE);
- } catch (RemoteException e) {
- // Arbitrary and not worth documenting, as Activity
- // Manager will kill this process shortly anyway.
- return null;
- } finally {
- resolver.releaseProvider(provider);
- }
+ @NonNull List<SliceSpec> supportedSpecs) {
+ return context.getSystemService(SliceManager.class).bindSlice(intent, supportedSpecs);
}
}
diff --git a/android/app/slice/SliceItem.java b/android/app/slice/SliceItem.java
index bcfd413f..9eb2bb89 100644
--- a/android/app/slice/SliceItem.java
+++ b/android/app/slice/SliceItem.java
@@ -98,11 +98,6 @@ public final class SliceItem implements Parcelable {
*/
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";
- /**
* A {@link SliceItem} that contains a timestamp.
*/
public static final String FORMAT_TIMESTAMP = "timestamp";
@@ -231,13 +226,6 @@ public final class SliceItem implements Parcelable {
}
/**
- * @deprecated to be removed.
- */
- public int getColor() {
- return (Integer) mObj;
- }
-
- /**
* @return The slice held by this {@link #FORMAT_ACTION} or {@link #FORMAT_SLICE} SliceItem
*/
public Slice getSlice() {
diff --git a/android/app/slice/SliceManager.java b/android/app/slice/SliceManager.java
index 0c5f225d..2fa9d8e0 100644
--- a/android/app/slice/SliceManager.java
+++ b/android/app/slice/SliceManager.java
@@ -16,18 +16,31 @@
package android.app.slice;
+import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.SystemService;
+import android.content.ContentResolver;
import android.content.Context;
+import android.content.IContentProvider;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
import android.net.Uri;
+import android.os.Bundle;
import android.os.Handler;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
import android.util.ArrayMap;
+import android.util.Log;
import android.util.Pair;
+import com.android.internal.util.Preconditions;
+
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
@@ -39,12 +52,36 @@ import java.util.concurrent.Executor;
@SystemService(Context.SLICE_SERVICE)
public class SliceManager {
+ private static final String TAG = "SliceManager";
+
+ /**
+ * @hide
+ */
+ public static final String ACTION_REQUEST_SLICE_PERMISSION =
+ "android.intent.action.REQUEST_SLICE_PERMISSION";
+
private final ISliceManager mService;
private final Context mContext;
private final ArrayMap<Pair<Uri, SliceCallback>, ISliceListener> mListenerLookup =
new ArrayMap<>();
/**
+ * Permission denied.
+ * @hide
+ */
+ public static final int PERMISSION_DENIED = -1;
+ /**
+ * Permission granted.
+ * @hide
+ */
+ public static final int PERMISSION_GRANTED = 0;
+ /**
+ * Permission just granted by the user, and should be granted uri permission as well.
+ * @hide
+ */
+ public static final int PERMISSION_USER_GRANTED = 1;
+
+ /**
* @hide
*/
public SliceManager(Context context, Handler handler) throws ServiceNotFoundException {
@@ -54,21 +91,21 @@ public class SliceManager {
}
/**
- * 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)
+ * @deprecated TO BE REMOVED.
*/
+ @Deprecated
public void registerSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback,
@NonNull List<SliceSpec> specs) {
- registerSliceCallback(uri, callback, specs, Handler.getMain());
+ registerSliceCallback(uri, specs, mContext.getMainExecutor(), callback);
+ }
+
+ /**
+ * @deprecated TO BE REMOVED.
+ */
+ @Deprecated
+ public void registerSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback,
+ @NonNull List<SliceSpec> specs, Executor executor) {
+ registerSliceCallback(uri, specs, executor, callback);
}
/**
@@ -84,19 +121,9 @@ public class SliceManager {
* @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();
- }
+ public void registerSliceCallback(@NonNull Uri uri, @NonNull List<SliceSpec> specs,
+ @NonNull SliceCallback callback) {
+ registerSliceCallback(uri, specs, mContext.getMainExecutor(), callback);
}
/**
@@ -112,8 +139,8 @@ public class SliceManager {
* @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) {
+ public void registerSliceCallback(@NonNull Uri uri, @NonNull List<SliceSpec> specs,
+ @NonNull @CallbackExecutor Executor executor, @NonNull SliceCallback callback) {
try {
mService.addSliceListener(uri, mContext.getPackageName(),
getListener(uri, callback, new ISliceListener.Stub() {
@@ -224,6 +251,165 @@ public class SliceManager {
}
/**
+ * Obtains a list of slices that are descendants of the specified Uri.
+ * <p>
+ * Not all slice providers will implement this functionality, in which case,
+ * an empty collection will be returned.
+ *
+ * @param uri The uri to look for descendants under.
+ * @return All slices within the space.
+ * @see SliceProvider#onGetSliceDescendants(Uri)
+ */
+ public @NonNull Collection<Uri> getSliceDescendants(@NonNull Uri uri) {
+ ContentResolver resolver = mContext.getContentResolver();
+ IContentProvider provider = resolver.acquireProvider(uri);
+ try {
+ Bundle extras = new Bundle();
+ extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
+ final Bundle res = provider.call(resolver.getPackageName(),
+ SliceProvider.METHOD_GET_DESCENDANTS, null, extras);
+ return res.getParcelableArrayList(SliceProvider.EXTRA_SLICE_DESCENDANTS);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to get slice descendants", e);
+ } finally {
+ resolver.releaseProvider(provider);
+ }
+ return Collections.emptyList();
+ }
+
+ /**
+ * Turns a slice Uri into slice content.
+ *
+ * @param uri The URI to a slice provider
+ * @param supportedSpecs List of supported specs.
+ * @return The Slice provided by the app or null if none is given.
+ * @see Slice
+ */
+ public @Nullable Slice bindSlice(@NonNull Uri uri, @NonNull List<SliceSpec> supportedSpecs) {
+ Preconditions.checkNotNull(uri, "uri");
+ ContentResolver resolver = mContext.getContentResolver();
+ IContentProvider provider = resolver.acquireProvider(uri);
+ if (provider == null) {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ try {
+ Bundle extras = new Bundle();
+ extras.putParcelable(SliceProvider.EXTRA_BIND_URI, uri);
+ extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
+ new ArrayList<>(supportedSpecs));
+ final Bundle res = provider.call(mContext.getPackageName(), SliceProvider.METHOD_SLICE,
+ null, extras);
+ Bundle.setDefusable(res, true);
+ if (res == null) {
+ return null;
+ }
+ return res.getParcelable(SliceProvider.EXTRA_SLICE);
+ } catch (RemoteException e) {
+ // Arbitrary and not worth documenting, as Activity
+ // Manager will kill this process shortly anyway.
+ return null;
+ } finally {
+ resolver.releaseProvider(provider);
+ }
+ }
+
+ /**
+ * Turns a slice intent into slice content. Expects an explicit intent. If there is no
+ * {@link android.content.ContentProvider} associated with the given intent this will throw
+ * {@link IllegalArgumentException}.
+ *
+ * @param intent The intent associated with a slice.
+ * @param supportedSpecs List of supported specs.
+ * @return The Slice provided by the app or null if none is given.
+ * @see Slice
+ * @see SliceProvider#onMapIntentToUri(Intent)
+ * @see Intent
+ */
+ public @Nullable Slice bindSlice(@NonNull Intent intent,
+ @NonNull List<SliceSpec> supportedSpecs) {
+ Preconditions.checkNotNull(intent, "intent");
+ Preconditions.checkArgument(intent.getComponent() != null || intent.getPackage() != null,
+ "Slice intent must be explicit " + intent);
+ ContentResolver resolver = mContext.getContentResolver();
+
+ // Check if the intent has data for the slice uri on it and use that
+ final Uri intentData = intent.getData();
+ if (intentData != null && SliceProvider.SLICE_TYPE.equals(resolver.getType(intentData))) {
+ return bindSlice(intentData, supportedSpecs);
+ }
+ // Otherwise ask the app
+ List<ResolveInfo> providers =
+ mContext.getPackageManager().queryIntentContentProviders(intent, 0);
+ if (providers == null) {
+ throw new IllegalArgumentException("Unable to resolve intent " + intent);
+ }
+ String authority = providers.get(0).providerInfo.authority;
+ Uri uri = new Uri.Builder().scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(authority).build();
+ IContentProvider provider = resolver.acquireProvider(uri);
+ if (provider == null) {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ try {
+ Bundle extras = new Bundle();
+ extras.putParcelable(SliceProvider.EXTRA_INTENT, intent);
+ extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS,
+ new ArrayList<>(supportedSpecs));
+ final Bundle res = provider.call(mContext.getPackageName(),
+ SliceProvider.METHOD_MAP_INTENT, null, extras);
+ if (res == null) {
+ return null;
+ }
+ return res.getParcelable(SliceProvider.EXTRA_SLICE);
+ } catch (RemoteException e) {
+ // Arbitrary and not worth documenting, as Activity
+ // Manager will kill this process shortly anyway.
+ return null;
+ } finally {
+ resolver.releaseProvider(provider);
+ }
+ }
+
+ /**
+ * Does the permission check to see if a caller has access to a specific slice.
+ * @hide
+ */
+ public void enforceSlicePermission(Uri uri, String pkg, int pid, int uid) {
+ try {
+ if (pkg == null) {
+ throw new SecurityException("No pkg specified");
+ }
+ int result = mService.checkSlicePermission(uri, pkg, pid, uid);
+ if (result == PERMISSION_DENIED) {
+ throw new SecurityException("User " + uid + " does not have slice permission for "
+ + uri + ".");
+ }
+ if (result == PERMISSION_USER_GRANTED) {
+ // We just had a user grant of this permission and need to grant this to the app
+ // permanently.
+ mContext.grantUriPermission(pkg, uri.buildUpon().path("").build(),
+ Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Called by SystemUI to grant a slice permission after a dialog is shown.
+ * @hide
+ */
+ public void grantPermissionFromUser(Uri uri, String pkg, boolean allSlices) {
+ try {
+ mService.grantPermissionFromUser(uri, pkg, mContext.getPackageName(), allSlices);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Class that listens to changes in {@link Slice}s.
*/
public interface SliceCallback {
diff --git a/android/app/slice/SliceProvider.java b/android/app/slice/SliceProvider.java
index 8483931c..00e8ccad 100644
--- a/android/app/slice/SliceProvider.java
+++ b/android/app/slice/SliceProvider.java
@@ -15,13 +15,19 @@
*/
package android.app.slice;
-import android.Manifest.permission;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.ContentResolver;
import android.content.ContentValues;
+import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ProviderInfo;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
@@ -36,6 +42,9 @@ import android.os.StrictMode.ThreadPolicy;
import android.os.UserHandle;
import android.util.Log;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
@@ -80,7 +89,7 @@ import java.util.concurrent.CountDownLatch;
*/
public abstract class SliceProvider extends ContentProvider {
/**
- * This is the Android platform's MIME type for a slice: URI
+ * This is the Android platform's MIME type for a URI
* containing a slice implemented through {@link SliceProvider}.
*/
public static final String SLICE_TYPE = "vnd.android.slice";
@@ -113,14 +122,53 @@ public abstract class SliceProvider extends ContentProvider {
/**
* @hide
*/
+ public static final String METHOD_GET_DESCENDANTS = "get_descendants";
+ /**
+ * @hide
+ */
public static final String EXTRA_INTENT = "slice_intent";
/**
* @hide
*/
public static final String EXTRA_SLICE = "slice";
+ /**
+ * @hide
+ */
+ public static final String EXTRA_SLICE_DESCENDANTS = "slice_descendants";
+ /**
+ * @hide
+ */
+ public static final String EXTRA_PKG = "pkg";
+ /**
+ * @hide
+ */
+ public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
+ /**
+ * @hide
+ */
+ public static final String EXTRA_OVERRIDE_PKG = "override_pkg";
private static final boolean DEBUG = false;
+ private String mBindingPkg;
+ private SliceManager mSliceManager;
+
+ /**
+ * Return the package name of the caller that initiated the binding request
+ * currently happening. The returned package will have been
+ * verified to belong to the calling UID. Returns {@code null} if not
+ * currently performing an {@link #onBindSlice(Uri, List)}.
+ */
+ public final @Nullable String getBindingPackage() {
+ return mBindingPkg;
+ }
+
+ @Override
+ public void attachInfo(Context context, ProviderInfo info) {
+ super.attachInfo(context, info);
+ mSliceManager = context.getSystemService(SliceManager.class);
+ }
+
/**
* Implemented to create a slice. Will be called on the main thread.
* <p>
@@ -139,14 +187,6 @@ public abstract class SliceProvider extends ContentProvider {
* @see {@link Slice#HINT_PARTIAL}
*/
public Slice onBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) {
- return onBindSlice(sliceUri);
- }
-
- /**
- * @deprecated migrating to {@link #onBindSlice(Uri, List)}
- */
- @Deprecated
- public Slice onBindSlice(Uri sliceUri) {
return null;
}
@@ -183,6 +223,20 @@ public abstract class SliceProvider extends ContentProvider {
}
/**
+ * Obtains a list of slices that are descendants of the specified Uri.
+ * <p>
+ * Implementing this is optional for a SliceProvider, but does provide a good
+ * discovery mechanism for finding slice Uris.
+ *
+ * @param uri The uri to look for descendants under.
+ * @return All slices within the space.
+ * @see SliceManager#getSliceDescendants(Uri)
+ */
+ public @NonNull Collection<Uri> onGetSliceDescendants(@NonNull Uri uri) {
+ return Collections.emptyList();
+ }
+
+ /**
* 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}.
@@ -244,56 +298,74 @@ public abstract class SliceProvider extends ContentProvider {
@Override
public Bundle call(String method, String arg, Bundle extras) {
if (method.equals(METHOD_SLICE)) {
- 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");
- }
+ Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_BIND_URI));
List<SliceSpec> supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS);
- Slice s = handleBindSlice(uri, supportedSpecs);
+ String callingPackage = getCallingPackage();
+ if (extras.containsKey(EXTRA_OVERRIDE_PKG)) {
+ if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+ throw new SecurityException("Only the system can override calling pkg");
+ }
+ callingPackage = extras.getString(EXTRA_OVERRIDE_PKG);
+ }
+ Slice s = handleBindSlice(uri, supportedSpecs, callingPackage);
Bundle b = new Bundle();
b.putParcelable(EXTRA_SLICE, s);
return b;
} else if (method.equals(METHOD_MAP_INTENT)) {
- 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();
if (uri != null) {
- Slice s = handleBindSlice(uri, supportedSpecs);
+ Slice s = handleBindSlice(uri, supportedSpecs, getCallingPackage());
b.putParcelable(EXTRA_SLICE, s);
} else {
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");
+ Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_BIND_URI));
+ if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+ throw new SecurityException("Only the system can pin/unpin slices");
}
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");
+ Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_BIND_URI));
+ if (Binder.getCallingUid() != Process.SYSTEM_UID) {
+ throw new SecurityException("Only the system can pin/unpin slices");
}
handleUnpinSlice(uri);
+ } else if (method.equals(METHOD_GET_DESCENDANTS)) {
+ Uri uri = getUriWithoutUserId(extras.getParcelable(EXTRA_BIND_URI));
+ Bundle b = new Bundle();
+ b.putParcelableArrayList(EXTRA_SLICE_DESCENDANTS,
+ new ArrayList<>(handleGetDescendants(uri)));
+ return b;
}
return super.call(method, arg, extras);
}
+ private Collection<Uri> handleGetDescendants(Uri uri) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ return onGetSliceDescendants(uri);
+ } else {
+ CountDownLatch latch = new CountDownLatch(1);
+ Collection<Uri>[] output = new Collection[1];
+ Handler.getMain().post(() -> {
+ output[0] = onGetSliceDescendants(uri);
+ latch.countDown();
+ });
+ try {
+ latch.await();
+ return output[0];
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
private void handlePinSlice(Uri sliceUri) {
if (Looper.myLooper() == Looper.getMainLooper()) {
onSlicePinned(sliceUri);
@@ -328,14 +400,27 @@ public abstract class SliceProvider extends ContentProvider {
}
}
- private Slice handleBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) {
+ private Slice handleBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs,
+ String callingPkg) {
+ // This can be removed once Slice#bindSlice is removed and everyone is using
+ // SliceManager#bindSlice.
+ String pkg = callingPkg != null ? callingPkg
+ : getContext().getPackageManager().getNameForUid(Binder.getCallingUid());
+ if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) {
+ try {
+ mSliceManager.enforceSlicePermission(sliceUri, pkg,
+ Binder.getCallingPid(), Binder.getCallingUid());
+ } catch (SecurityException e) {
+ return createPermissionSlice(getContext(), sliceUri, pkg);
+ }
+ }
if (Looper.myLooper() == Looper.getMainLooper()) {
- return onBindSliceStrict(sliceUri, supportedSpecs);
+ return onBindSliceStrict(sliceUri, supportedSpecs, pkg);
} else {
CountDownLatch latch = new CountDownLatch(1);
Slice[] output = new Slice[1];
Handler.getMain().post(() -> {
- output[0] = onBindSliceStrict(sliceUri, supportedSpecs);
+ output[0] = onBindSliceStrict(sliceUri, supportedSpecs, pkg);
latch.countDown();
});
try {
@@ -347,15 +432,66 @@ public abstract class SliceProvider extends ContentProvider {
}
}
- private Slice onBindSliceStrict(Uri sliceUri, List<SliceSpec> supportedSpecs) {
+ /**
+ * @hide
+ */
+ public static Slice createPermissionSlice(Context context, Uri sliceUri,
+ String callingPackage) {
+ return new Slice.Builder(sliceUri)
+ .addAction(createPermissionIntent(context, sliceUri, callingPackage),
+ new Slice.Builder(sliceUri.buildUpon().appendPath("permission").build())
+ .addText(getPermissionString(context, callingPackage), null)
+ .build())
+ .addHints(Slice.HINT_LIST_ITEM)
+ .build();
+ }
+
+ /**
+ * @hide
+ */
+ public static PendingIntent createPermissionIntent(Context context, Uri sliceUri,
+ String callingPackage) {
+ Intent intent = new Intent(SliceManager.ACTION_REQUEST_SLICE_PERMISSION);
+ intent.setComponent(new ComponentName("com.android.systemui",
+ "com.android.systemui.SlicePermissionActivity"));
+ intent.putExtra(EXTRA_BIND_URI, sliceUri);
+ intent.putExtra(EXTRA_PKG, callingPackage);
+ intent.putExtra(EXTRA_PROVIDER_PKG, context.getPackageName());
+ // Unique pending intent.
+ intent.setData(sliceUri.buildUpon().appendQueryParameter("package", callingPackage)
+ .build());
+
+ return PendingIntent.getActivity(context, 0, intent, 0);
+ }
+
+ /**
+ * @hide
+ */
+ public static CharSequence getPermissionString(Context context, String callingPackage) {
+ PackageManager pm = context.getPackageManager();
+ try {
+ return context.getString(
+ com.android.internal.R.string.slices_permission_request,
+ pm.getApplicationInfo(callingPackage, 0).loadLabel(pm),
+ context.getApplicationInfo().loadLabel(pm));
+ } catch (NameNotFoundException e) {
+ // This shouldn't be possible since the caller is verified.
+ throw new RuntimeException("Unknown calling app", e);
+ }
+ }
+
+ private Slice onBindSliceStrict(Uri sliceUri, List<SliceSpec> supportedSpecs,
+ String callingPackage) {
ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
try {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyDeath()
.build());
+ mBindingPkg = callingPackage;
return onBindSlice(sliceUri, supportedSpecs);
} finally {
+ mBindingPkg = null;
StrictMode.setThreadPolicy(oldPolicy);
}
}
diff --git a/android/app/timezone/RulesState.java b/android/app/timezone/RulesState.java
index 16309fab..e86d348a 100644
--- a/android/app/timezone/RulesState.java
+++ b/android/app/timezone/RulesState.java
@@ -126,9 +126,6 @@ public final class RulesState implements Parcelable {
mStagedOperationType == STAGED_OPERATION_INSTALL /* requireNotNull */,
"stagedDistroRulesVersion", stagedDistroRulesVersion);
- if (operationInProgress && distroStatus != DISTRO_STATUS_UNKNOWN) {
- throw new IllegalArgumentException("distroInstalled != DISTRO_STATUS_UNKNOWN");
- }
this.mDistroStatus = validateDistroStatus(distroStatus);
this.mInstalledDistroRulesVersion = validateConditionalNull(
mDistroStatus == DISTRO_STATUS_INSTALLED/* requireNotNull */,
diff --git a/android/app/trust/TrustManager.java b/android/app/trust/TrustManager.java
index 852cb8e0..8ab0b706 100644
--- a/android/app/trust/TrustManager.java
+++ b/android/app/trust/TrustManager.java
@@ -36,9 +36,11 @@ public class TrustManager {
private static final int MSG_TRUST_CHANGED = 1;
private static final int MSG_TRUST_MANAGED_CHANGED = 2;
+ private static final int MSG_TRUST_ERROR = 3;
private static final String TAG = "TrustManager";
private static final String DATA_FLAGS = "initiatedByUser";
+ private static final String DATA_MESSAGE = "message";
private final ITrustManager mService;
private final ArrayMap<TrustListener, ITrustListener> mTrustListeners;
@@ -148,6 +150,13 @@ public class TrustManager {
mHandler.obtainMessage(MSG_TRUST_MANAGED_CHANGED, (managed ? 1 : 0), userId,
trustListener).sendToTarget();
}
+
+ @Override
+ public void onTrustError(CharSequence message) {
+ Message m = mHandler.obtainMessage(MSG_TRUST_ERROR);
+ m.getData().putCharSequence(DATA_MESSAGE, message);
+ m.sendToTarget();
+ }
};
mService.registerTrustListener(iTrustListener);
mTrustListeners.put(trustListener, iTrustListener);
@@ -221,6 +230,10 @@ public class TrustManager {
break;
case MSG_TRUST_MANAGED_CHANGED:
((TrustListener)msg.obj).onTrustManagedChanged(msg.arg1 != 0, msg.arg2);
+ break;
+ case MSG_TRUST_ERROR:
+ final CharSequence message = msg.peekData().getCharSequence(DATA_MESSAGE);
+ ((TrustListener)msg.obj).onTrustError(message);
}
}
};
@@ -229,9 +242,9 @@ public class TrustManager {
/**
* Reports that the trust state has changed.
- * @param enabled if true, the system believes the environment to be trusted.
- * @param userId the user, for which the trust changed.
- * @param flags flags specified by the trust agent when granting trust. See
+ * @param enabled If true, the system believes the environment to be trusted.
+ * @param userId The user, for which the trust changed.
+ * @param flags Flags specified by the trust agent when granting trust. See
* {@link android.service.trust.TrustAgentService#grantTrust(CharSequence, long, int)
* TrustAgentService.grantTrust(CharSequence, long, int)}.
*/
@@ -239,9 +252,15 @@ public class TrustManager {
/**
* Reports that whether trust is managed has changed
- * @param enabled if true, at least one trust agent is managing trust.
- * @param userId the user, for which the state changed.
+ * @param enabled If true, at least one trust agent is managing trust.
+ * @param userId The user, for which the state changed.
*/
void onTrustManagedChanged(boolean enabled, int userId);
+
+ /**
+ * Reports that an error happened on a TrustAgentService.
+ * @param message A message that should be displayed on the UI.
+ */
+ void onTrustError(CharSequence message);
}
}
diff --git a/android/app/usage/NetworkStats.java b/android/app/usage/NetworkStats.java
index 2e44a630..da36157d 100644
--- a/android/app/usage/NetworkStats.java
+++ b/android/app/usage/NetworkStats.java
@@ -227,6 +227,30 @@ public final class NetworkStats implements AutoCloseable {
*/
public static final int ROAMING_YES = 0x2;
+ /** @hide */
+ @IntDef(prefix = { "DEFAULT_NETWORK_" }, value = {
+ DEFAULT_NETWORK_ALL,
+ DEFAULT_NETWORK_NO,
+ DEFAULT_NETWORK_YES
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DefaultNetwork {}
+
+ /**
+ * Combined usage for this network regardless of whether it was the active default network.
+ */
+ public static final int DEFAULT_NETWORK_ALL = -1;
+
+ /**
+ * Usage that occurs while this network is not the active default network.
+ */
+ public static final int DEFAULT_NETWORK_NO = 0x1;
+
+ /**
+ * Usage that occurs while this network is the active default network.
+ */
+ public static final int DEFAULT_NETWORK_YES = 0x2;
+
/**
* Special TAG value for total data across all tags
*/
@@ -235,6 +259,7 @@ public final class NetworkStats implements AutoCloseable {
private int mUid;
private int mTag;
private int mState;
+ private int mDefaultNetwork;
private int mMetered;
private int mRoaming;
private long mBeginTimeStamp;
@@ -286,6 +311,15 @@ public final class NetworkStats implements AutoCloseable {
return 0;
}
+ private static @DefaultNetwork int convertDefaultNetwork(int defaultNetwork) {
+ switch (defaultNetwork) {
+ case android.net.NetworkStats.DEFAULT_NETWORK_ALL : return DEFAULT_NETWORK_ALL;
+ case android.net.NetworkStats.DEFAULT_NETWORK_NO: return DEFAULT_NETWORK_NO;
+ case android.net.NetworkStats.DEFAULT_NETWORK_YES: return DEFAULT_NETWORK_YES;
+ }
+ return 0;
+ }
+
public Bucket() {
}
@@ -351,6 +385,21 @@ public final class NetworkStats implements AutoCloseable {
}
/**
+ * Default network state. One of the following values:<p/>
+ * <ul>
+ * <li>{@link #DEFAULT_NETWORK_ALL}</li>
+ * <li>{@link #DEFAULT_NETWORK_NO}</li>
+ * <li>{@link #DEFAULT_NETWORK_YES}</li>
+ * </ul>
+ * <p>Indicates whether the network usage occurred on the system default network for this
+ * type of traffic, or whether the application chose to send this traffic on a network that
+ * was not the one selected by the system.
+ */
+ public @DefaultNetwork int getDefaultNetwork() {
+ return mDefaultNetwork;
+ }
+
+ /**
* Start timestamp of the bucket's time interval. Defined in terms of "Unix time", see
* {@link java.lang.System#currentTimeMillis}.
* @return Start of interval.
@@ -551,6 +600,8 @@ public final class NetworkStats implements AutoCloseable {
bucketOut.mUid = Bucket.convertUid(mRecycledSummaryEntry.uid);
bucketOut.mTag = Bucket.convertTag(mRecycledSummaryEntry.tag);
bucketOut.mState = Bucket.convertState(mRecycledSummaryEntry.set);
+ bucketOut.mDefaultNetwork = Bucket.convertDefaultNetwork(
+ mRecycledSummaryEntry.defaultNetwork);
bucketOut.mMetered = Bucket.convertMetered(mRecycledSummaryEntry.metered);
bucketOut.mRoaming = Bucket.convertRoaming(mRecycledSummaryEntry.roaming);
bucketOut.mBeginTimeStamp = mStartTimeStamp;
@@ -600,6 +651,7 @@ public final class NetworkStats implements AutoCloseable {
bucketOut.mUid = Bucket.convertUid(getUid());
bucketOut.mTag = Bucket.convertTag(mTag);
bucketOut.mState = Bucket.STATE_ALL;
+ bucketOut.mDefaultNetwork = Bucket.DEFAULT_NETWORK_ALL;
bucketOut.mMetered = Bucket.METERED_ALL;
bucketOut.mRoaming = Bucket.ROAMING_ALL;
bucketOut.mBeginTimeStamp = mRecycledHistoryEntry.bucketStart;
diff --git a/android/app/usage/NetworkStatsManager.java b/android/app/usage/NetworkStatsManager.java
index 853b0033..5576e86e 100644
--- a/android/app/usage/NetworkStatsManager.java
+++ b/android/app/usage/NetworkStatsManager.java
@@ -60,10 +60,11 @@ import android.util.Log;
* {@link #queryDetailsForUid} <p />
* {@link #queryDetails} <p />
* These queries do not aggregate over time but do aggregate over state, metered and roaming.
- * Therefore there can be multiple buckets for a particular key but all Bucket's state is going to
- * be {@link NetworkStats.Bucket#STATE_ALL}, all Bucket's metered is going to be
- * {@link NetworkStats.Bucket#METERED_ALL}, and all Bucket's roaming is going to be
- * {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * Therefore there can be multiple buckets for a particular key. However, all Buckets will have
+ * {@code state} {@link NetworkStats.Bucket#STATE_ALL},
+ * {@code defaultNetwork} {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * {@code metered } {@link NetworkStats.Bucket#METERED_ALL},
+ * {@code roaming} {@link NetworkStats.Bucket#ROAMING_ALL}.
* <p />
* <b>NOTE:</b> Calling {@link #querySummaryForDevice} or accessing stats for apps other than the
* calling app requires the permission {@link android.Manifest.permission#PACKAGE_USAGE_STATS},
@@ -130,13 +131,26 @@ public class NetworkStatsManager {
}
}
+ /** @hide */
+ public Bucket querySummaryForDevice(NetworkTemplate template,
+ long startTime, long endTime) throws SecurityException, RemoteException {
+ Bucket bucket = null;
+ NetworkStats stats = new NetworkStats(mContext, template, mFlags, startTime, endTime);
+ bucket = stats.getDeviceSummaryForNetwork();
+
+ stats.close();
+ return bucket;
+ }
+
/**
* Query network usage statistics summaries. Result is summarised data usage for the whole
* device. Result is a single Bucket aggregated over time, state, uid, tag, metered, and
* roaming. This means the bucket's start and end timestamp are going to be the same as the
* 'startTime' and 'endTime' parameters. State is going to be
* {@link NetworkStats.Bucket#STATE_ALL}, uid {@link NetworkStats.Bucket#UID_ALL},
- * tag {@link NetworkStats.Bucket#TAG_NONE}, metered {@link NetworkStats.Bucket#METERED_ALL},
+ * tag {@link NetworkStats.Bucket#TAG_NONE},
+ * default network {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * metered {@link NetworkStats.Bucket#METERED_ALL},
* and roaming {@link NetworkStats.Bucket#ROAMING_ALL}.
*
* @param networkType As defined in {@link ConnectivityManager}, e.g.
@@ -160,12 +174,7 @@ public class NetworkStatsManager {
return null;
}
- Bucket bucket = null;
- NetworkStats stats = new NetworkStats(mContext, template, mFlags, startTime, endTime);
- bucket = stats.getDeviceSummaryForNetwork();
-
- stats.close();
- return bucket;
+ return querySummaryForDevice(template, startTime, endTime);
}
/**
@@ -209,10 +218,10 @@ public class NetworkStatsManager {
/**
* Query network usage statistics summaries. Result filtered to include only uids belonging to
* calling user. Result is aggregated over time, hence all buckets will have the same start and
- * end timestamps. Not aggregated over state, uid, metered, or roaming. This means buckets'
- * start and end timestamps are going to be the same as the 'startTime' and 'endTime'
- * parameters. State, uid, metered, and roaming are going to vary, and tag is going to be the
- * same.
+ * end timestamps. Not aggregated over state, uid, default network, metered, or roaming. This
+ * means buckets' start and end timestamps are going to be the same as the 'startTime' and
+ * 'endTime' parameters. State, uid, metered, and roaming are going to vary, and tag is going to
+ * be the same.
*
* @param networkType As defined in {@link ConnectivityManager}, e.g.
* {@link ConnectivityManager#TYPE_MOBILE}, {@link ConnectivityManager#TYPE_WIFI}
@@ -258,9 +267,10 @@ public class NetworkStatsManager {
* belonging to calling user. Result is aggregated over state but not aggregated over time.
* This means buckets' start and end timestamps are going to be between 'startTime' and
* 'endTime' parameters. State is going to be {@link NetworkStats.Bucket#STATE_ALL}, uid the
- * same as the 'uid' parameter and tag the same as 'tag' parameter. metered is going to be
- * {@link NetworkStats.Bucket#METERED_ALL}, and roaming is going to be
- * {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * same as the 'uid' parameter and tag the same as 'tag' parameter.
+ * defaultNetwork is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * metered is going to be {@link NetworkStats.Bucket#METERED_ALL}, and
+ * roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
* <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
* interpolate across partial buckets. Since bucket length is in the order of hours, this
* method cannot be used to measure data usage on a fine grained time scale.
@@ -301,9 +311,10 @@ public class NetworkStatsManager {
* metered, nor roaming. This means buckets' start and end timestamps are going to be between
* 'startTime' and 'endTime' parameters. State is going to be
* {@link NetworkStats.Bucket#STATE_ALL}, uid will vary,
- * tag {@link NetworkStats.Bucket#TAG_NONE}, metered is going to be
- * {@link NetworkStats.Bucket#METERED_ALL}, and roaming is going to be
- * {@link NetworkStats.Bucket#ROAMING_ALL}.
+ * tag {@link NetworkStats.Bucket#TAG_NONE},
+ * default network is going to be {@link NetworkStats.Bucket#DEFAULT_NETWORK_ALL},
+ * metered is going to be {@link NetworkStats.Bucket#METERED_ALL},
+ * and roaming is going to be {@link NetworkStats.Bucket#ROAMING_ALL}.
* <p>Only includes buckets that atomically occur in the inclusive time range. Doesn't
* interpolate across partial buckets. Since bucket length is in the order of hours, this
* method cannot be used to measure data usage on a fine grained time scale.
@@ -335,6 +346,37 @@ public class NetworkStatsManager {
return result;
}
+ /** @hide */
+ public void registerUsageCallback(NetworkTemplate template, int networkType,
+ long thresholdBytes, UsageCallback callback, @Nullable Handler handler) {
+ checkNotNull(callback, "UsageCallback cannot be null");
+
+ final Looper looper;
+ if (handler == null) {
+ looper = Looper.myLooper();
+ } else {
+ looper = handler.getLooper();
+ }
+
+ DataUsageRequest request = new DataUsageRequest(DataUsageRequest.REQUEST_ID_UNSET,
+ template, thresholdBytes);
+ try {
+ CallbackHandler callbackHandler = new CallbackHandler(looper, networkType,
+ template.getSubscriberId(), callback);
+ callback.request = mService.registerUsageCallback(
+ mContext.getOpPackageName(), request, new Messenger(callbackHandler),
+ new Binder());
+ if (DBG) Log.d(TAG, "registerUsageCallback returned " + callback.request);
+
+ if (callback.request == null) {
+ Log.e(TAG, "Request from callback is null; should not happen");
+ }
+ } catch (RemoteException e) {
+ if (DBG) Log.d(TAG, "Remote exception when registering callback");
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
/**
* Registers to receive notifications about data usage on specified networks.
*
@@ -363,15 +405,7 @@ public class NetworkStatsManager {
*/
public void registerUsageCallback(int networkType, String subscriberId, long thresholdBytes,
UsageCallback callback, @Nullable Handler handler) {
- checkNotNull(callback, "UsageCallback cannot be null");
-
- final Looper looper;
- if (handler == null) {
- looper = Looper.myLooper();
- } else {
- looper = handler.getLooper();
- }
-
+ NetworkTemplate template = createTemplate(networkType, subscriberId);
if (DBG) {
Log.d(TAG, "registerUsageCallback called with: {"
+ " networkType=" + networkType
@@ -379,25 +413,7 @@ public class NetworkStatsManager {
+ " thresholdBytes=" + thresholdBytes
+ " }");
}
-
- NetworkTemplate template = createTemplate(networkType, subscriberId);
- DataUsageRequest request = new DataUsageRequest(DataUsageRequest.REQUEST_ID_UNSET,
- template, thresholdBytes);
- try {
- CallbackHandler callbackHandler = new CallbackHandler(looper, networkType,
- subscriberId, callback);
- callback.request = mService.registerUsageCallback(
- mContext.getOpPackageName(), request, new Messenger(callbackHandler),
- new Binder());
- if (DBG) Log.d(TAG, "registerUsageCallback returned " + callback.request);
-
- if (callback.request == null) {
- Log.e(TAG, "Request from callback is null; should not happen");
- }
- } catch (RemoteException e) {
- if (DBG) Log.d(TAG, "Remote exception when registering callback");
- throw e.rethrowFromSystemServer();
- }
+ registerUsageCallback(template, networkType, thresholdBytes, callback, handler);
}
/**
diff --git a/android/app/usage/UsageEvents.java b/android/app/usage/UsageEvents.java
index f04e9074..edb992bd 100644
--- a/android/app/usage/UsageEvents.java
+++ b/android/app/usage/UsageEvents.java
@@ -106,6 +106,12 @@ public final class UsageEvents implements Parcelable {
*/
public static final int NOTIFICATION_SEEN = 10;
+ /**
+ * An event type denoting a change in App Standby Bucket.
+ * @hide
+ */
+ public static final int STANDBY_BUCKET_CHANGED = 11;
+
/** @hide */
public static final int FLAG_IS_PACKAGE_INSTANT_APP = 1 << 0;
@@ -170,6 +176,13 @@ public final class UsageEvents implements Parcelable {
*/
public String[] mContentAnnotations;
+ /**
+ * The app standby bucket assigned.
+ * Only present for {@link #STANDBY_BUCKET_CHANGED} event types
+ * {@hide}
+ */
+ public int mBucket;
+
/** @hide */
@EventFlags
public int mFlags;
@@ -189,6 +202,7 @@ public final class UsageEvents implements Parcelable {
mContentType = orig.mContentType;
mContentAnnotations = orig.mContentAnnotations;
mFlags = orig.mFlags;
+ mBucket = orig.mBucket;
}
/**
@@ -399,6 +413,9 @@ public final class UsageEvents implements Parcelable {
p.writeString(event.mContentType);
p.writeStringArray(event.mContentAnnotations);
break;
+ case Event.STANDBY_BUCKET_CHANGED:
+ p.writeInt(event.mBucket);
+ break;
}
}
@@ -442,6 +459,9 @@ public final class UsageEvents implements Parcelable {
eventOut.mContentType = p.readString();
eventOut.mContentAnnotations = p.createStringArray();
break;
+ case Event.STANDBY_BUCKET_CHANGED:
+ eventOut.mBucket = p.readInt();
+ break;
}
}
diff --git a/android/app/usage/UsageStatsManagerInternal.java b/android/app/usage/UsageStatsManagerInternal.java
index 4b4fe72f..bd978e3d 100644
--- a/android/app/usage/UsageStatsManagerInternal.java
+++ b/android/app/usage/UsageStatsManagerInternal.java
@@ -16,11 +16,13 @@
package android.app.usage;
+import android.annotation.UserIdInt;
import android.app.usage.UsageStatsManager.StandbyBuckets;
import android.content.ComponentName;
import android.content.res.Configuration;
import java.util.List;
+import java.util.Set;
/**
* UsageStatsManager local system service interface.
@@ -37,7 +39,7 @@ public abstract class UsageStatsManagerInternal {
* @param eventType The event that occurred. Valid values can be found at
* {@link UsageEvents}
*/
- public abstract void reportEvent(ComponentName component, int userId, int eventType);
+ public abstract void reportEvent(ComponentName component, @UserIdInt int userId, int eventType);
/**
* Reports an event to the UsageStatsManager.
@@ -47,14 +49,14 @@ public abstract class UsageStatsManagerInternal {
* @param eventType The event that occurred. Valid values can be found at
* {@link UsageEvents}
*/
- public abstract void reportEvent(String packageName, int userId, int eventType);
+ public abstract void reportEvent(String packageName, @UserIdInt int userId, int eventType);
/**
* Reports a configuration change to the UsageStatsManager.
*
* @param config The new device configuration.
*/
- public abstract void reportConfigurationChange(Configuration config, int userId);
+ public abstract void reportConfigurationChange(Configuration config, @UserIdInt int userId);
/**
* Reports that an action equivalent to a ShortcutInfo is taken by the user.
@@ -65,7 +67,8 @@ public abstract class UsageStatsManagerInternal {
*
* @see android.content.pm.ShortcutManager#reportShortcutUsed(String)
*/
- public abstract void reportShortcutUsage(String packageName, String shortcutId, int userId);
+ public abstract void reportShortcutUsage(String packageName, String shortcutId,
+ @UserIdInt int userId);
/**
* Reports that a content provider has been accessed by a foreground app.
@@ -73,7 +76,8 @@ public abstract class UsageStatsManagerInternal {
* @param pkgName The package name of the content provider
* @param userId The user in which the content provider was accessed.
*/
- public abstract void reportContentProviderUsage(String name, String pkgName, int userId);
+ public abstract void reportContentProviderUsage(String name, String pkgName,
+ @UserIdInt int userId);
/**
* Prepares the UsageStatsService for shutdown.
@@ -89,7 +93,7 @@ public abstract class UsageStatsManagerInternal {
* @param userId
* @return
*/
- public abstract boolean isAppIdle(String packageName, int uidForAppId, int userId);
+ public abstract boolean isAppIdle(String packageName, int uidForAppId, @UserIdInt int userId);
/**
* Returns the app standby bucket that the app is currently in. This accessor does
@@ -101,15 +105,15 @@ public abstract class UsageStatsManagerInternal {
* @return the AppStandby bucket code the app currently resides in. If the app is
* unknown in the given user, STANDBY_BUCKET_NEVER is returned.
*/
- @StandbyBuckets public abstract int getAppStandbyBucket(String packageName, int userId,
- long nowElapsed);
+ @StandbyBuckets public abstract int getAppStandbyBucket(String packageName,
+ @UserIdInt int userId, long nowElapsed);
/**
* Returns all of the uids for a given user where all packages associating with that uid
* are in the app idle state -- there are no associated apps that are not idle. This means
* all of the returned uids can be safely considered app idle.
*/
- public abstract int[] getIdleUidsForUser(int userId);
+ public abstract int[] getIdleUidsForUser(@UserIdInt int userId);
/**
* @return True if currently app idle parole mode is on. This means all idle apps are allow to
@@ -134,8 +138,8 @@ public abstract class UsageStatsManagerInternal {
public static abstract class AppIdleStateChangeListener {
/** Callback to inform listeners that the idle state has changed to a new bucket. */
- public abstract void onAppIdleStateChanged(String packageName, int userId, boolean idle,
- int bucket);
+ public abstract void onAppIdleStateChanged(String packageName, @UserIdInt int userId,
+ boolean idle, int bucket);
/**
* Callback to inform listeners that the parole state has changed. This means apps are
@@ -144,10 +148,38 @@ public abstract class UsageStatsManagerInternal {
public abstract void onParoleStateChanged(boolean isParoleOn);
}
- /* Backup/Restore API */
- public abstract byte[] getBackupPayload(int user, String key);
+ /** Backup/Restore API */
+ public abstract byte[] getBackupPayload(@UserIdInt int userId, String key);
- public abstract void applyRestoredPayload(int user, String key, byte[] payload);
+ /**
+ * ?
+ * @param userId
+ * @param key
+ * @param payload
+ */
+ public abstract void applyRestoredPayload(@UserIdInt int userId, String key, byte[] payload);
+
+ /**
+ * Called by DevicePolicyManagerService to inform that a new admin has been added.
+ *
+ * @param packageName the package in which the admin component is part of.
+ * @param userId the userId in which the admin has been added.
+ */
+ public abstract void onActiveAdminAdded(String packageName, int userId);
+
+ /**
+ * Called by DevicePolicyManagerService to inform about the active admins in an user.
+ *
+ * @param adminApps the set of active admins in {@param userId} or null if there are none.
+ * @param userId the userId to which the admin apps belong.
+ */
+ public abstract void setActiveAdminApps(Set<String> adminApps, int userId);
+
+ /**
+ * Called by DevicePolicyManagerService during boot to inform that admin data is loaded and
+ * pushed to UsageStatsService.
+ */
+ public abstract void onAdminDataAvailable();
/**
* Return usage stats.
@@ -155,6 +187,29 @@ public abstract class UsageStatsManagerInternal {
* @param obfuscateInstantApps whether instant app package names need to be obfuscated in the
* result.
*/
- public abstract List<UsageStats> queryUsageStatsForUser(
- int userId, int interval, long beginTime, long endTime, boolean obfuscateInstantApps);
+ public abstract List<UsageStats> queryUsageStatsForUser(@UserIdInt int userId, int interval,
+ long beginTime, long endTime, boolean obfuscateInstantApps);
+
+ /**
+ * Used to persist the last time a job was run for this app, in order to make decisions later
+ * whether a job should be deferred until later. The time passed in should be in elapsed
+ * realtime since boot.
+ * @param packageName the app that executed a job.
+ * @param userId the user associated with the job.
+ * @param elapsedRealtime the time when the job was executed, in elapsed realtime millis since
+ * boot.
+ */
+ public abstract void setLastJobRunTime(String packageName, @UserIdInt int userId,
+ long elapsedRealtime);
+
+ /**
+ * Returns the time in millis since a job was executed for this app, in elapsed realtime
+ * timebase. This value can be larger than the current elapsed realtime if the job was executed
+ * before the device was rebooted. The default value is {@link Long#MAX_VALUE}.
+ * @param packageName the app you're asking about.
+ * @param userId the user associated with the job.
+ * @return the time in millis since a job was last executed for the app, provided it was
+ * indicated here before by a call to {@link #setLastJobRunTime(String, int, long)}.
+ */
+ public abstract long getTimeSinceLastJobRun(String packageName, @UserIdInt int userId);
}
diff --git a/android/appwidget/AppWidgetManager.java b/android/appwidget/AppWidgetManager.java
index 37bb6b05..a55bbdae 100644
--- a/android/appwidget/AppWidgetManager.java
+++ b/android/appwidget/AppWidgetManager.java
@@ -677,6 +677,34 @@ public class AppWidgetManager {
}
/**
+ * Updates the info for the supplied AppWidget provider.
+ *
+ * <p>
+ * The manifest entry of the provider should contain an additional meta-data tag similar to
+ * {@link #META_DATA_APPWIDGET_PROVIDER} which should point to any additional definitions for
+ * the provider.
+ *
+ * <p>
+ * This is persisted across device reboots and app updates. If this meta-data key is not
+ * present in the manifest entry, the info reverts to default.
+ *
+ * @param provider {@link ComponentName} for the {@link
+ * android.content.BroadcastReceiver BroadcastReceiver} provider for your AppWidget.
+ * @param metaDataKey key for the meta-data tag pointing to the new provider info. Use null
+ * to reset any previously set info.
+ */
+ public void updateAppWidgetProviderInfo(ComponentName provider, @Nullable String metaDataKey) {
+ if (mService == null) {
+ return;
+ }
+ try {
+ mService.updateAppWidgetProviderInfo(provider, metaDataKey);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Notifies the specified collection view in all the specified AppWidget instances
* to invalidate their data.
*
diff --git a/android/arch/lifecycle/ComputableLiveData.java b/android/arch/lifecycle/ComputableLiveData.java
index 1ddcb1a9..f1352446 100644
--- a/android/arch/lifecycle/ComputableLiveData.java
+++ b/android/arch/lifecycle/ComputableLiveData.java
@@ -1,136 +1,9 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
+//ComputableLiveData interface for tests
package android.arch.lifecycle;
-
-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)
+import android.arch.lifecycle.LiveData;
public abstract class ComputableLiveData<T> {
-
- 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();
+ public ComputableLiveData(){}
+ abstract protected T compute();
+ public LiveData<T> getLiveData() {return null;}
+ public void invalidate() {}
}
diff --git a/android/arch/lifecycle/GenericLifecycleObserver.java b/android/arch/lifecycle/GenericLifecycleObserver.java
index 59f09c49..4601478b 100644
--- a/android/arch/lifecycle/GenericLifecycleObserver.java
+++ b/android/arch/lifecycle/GenericLifecycleObserver.java
@@ -16,10 +16,13 @@
package android.arch.lifecycle;
+import android.support.annotation.RestrictTo;
+
/**
* Internal class that can receive any lifecycle change and dispatch it to the receiver.
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
@SuppressWarnings({"WeakerAccess", "unused"})
public interface GenericLifecycleObserver extends LifecycleObserver {
/**
diff --git a/android/arch/lifecycle/HolderFragment.java b/android/arch/lifecycle/HolderFragment.java
index 100d10a1..ca5e1819 100644
--- a/android/arch/lifecycle/HolderFragment.java
+++ b/android/arch/lifecycle/HolderFragment.java
@@ -19,6 +19,7 @@ package android.arch.lifecycle;
import android.app.Activity;
import android.app.Application.ActivityLifecycleCallbacks;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.v4.app.Fragment;
@@ -34,7 +35,7 @@ import java.util.Map;
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class HolderFragment extends Fragment {
+public class HolderFragment extends Fragment implements ViewModelStoreOwner {
private static final String LOG_TAG = "ViewModelStores";
private static final HolderFragmentManager sHolderFragmentManager = new HolderFragmentManager();
@@ -69,6 +70,8 @@ public class HolderFragment extends Fragment {
mViewModelStore.clear();
}
+ @NonNull
+ @Override
public ViewModelStore getViewModelStore() {
return mViewModelStore;
}
diff --git a/android/arch/lifecycle/LiveData.java b/android/arch/lifecycle/LiveData.java
index 5b09c32f..3aea6acb 100644
--- a/android/arch/lifecycle/LiveData.java
+++ b/android/arch/lifecycle/LiveData.java
@@ -1,410 +1,4 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
+//LiveData interface for tests
package android.arch.lifecycle;
-
-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");
- }
- }
+public class LiveData<T> {
}
diff --git a/android/arch/lifecycle/LiveDataTest.java b/android/arch/lifecycle/LiveDataTest.java
index c1dc54da..046059bd 100644
--- a/android/arch/lifecycle/LiveDataTest.java
+++ b/android/arch/lifecycle/LiveDataTest.java
@@ -30,6 +30,7 @@ import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.only;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -37,11 +38,12 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import android.arch.core.executor.ArchTaskExecutor;
-import android.arch.lifecycle.util.InstantTaskExecutor;
+import android.arch.core.executor.testing.InstantTaskExecutorRule;
import android.support.annotation.Nullable;
import org.junit.After;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -52,6 +54,10 @@ import org.mockito.Mockito;
@SuppressWarnings({"unchecked"})
@RunWith(JUnit4.class)
public class LiveDataTest {
+
+ @Rule
+ public InstantTaskExecutorRule mInstantTaskExecutorRule = new InstantTaskExecutorRule();
+
private PublicLiveData<String> mLiveData;
private MethodExec mActiveObserversChanged;
@@ -102,11 +108,6 @@ public class LiveDataTest {
when(mOwner4.getLifecycle()).thenReturn(mLifecycle4);
}
- @Before
- public void swapExecutorDelegate() {
- ArchTaskExecutor.getInstance().setDelegate(new InstantTaskExecutor());
- }
-
@After
public void removeExecutorDelegate() {
ArchTaskExecutor.getInstance().setDelegate(null);
@@ -779,6 +780,28 @@ public class LiveDataTest {
verify(mObserver4, never()).onChanged(anyString());
}
+ @Test
+ public void nestedForeverObserver() {
+ mLiveData.setValue(".");
+ mLiveData.observeForever(new Observer<String>() {
+ @Override
+ public void onChanged(@Nullable String s) {
+ mLiveData.observeForever(mock(Observer.class));
+ mLiveData.removeObserver(this);
+ }
+ });
+ verify(mActiveObserversChanged, only()).onCall(true);
+ }
+
+ @Test
+ public void readdForeverObserver() {
+ Observer observer = mock(Observer.class);
+ mLiveData.observeForever(observer);
+ mLiveData.observeForever(observer);
+ mLiveData.removeObserver(observer);
+ assertThat(mLiveData.hasObservers(), is(false));
+ }
+
private GenericLifecycleObserver getGenericLifecycleObserver(Lifecycle lifecycle) {
ArgumentCaptor<GenericLifecycleObserver> captor =
ArgumentCaptor.forClass(GenericLifecycleObserver.class);
diff --git a/android/arch/lifecycle/ThreadedLiveDataTest.java b/android/arch/lifecycle/ThreadedLiveDataTest.java
index ca270675..3366641b 100644
--- a/android/arch/lifecycle/ThreadedLiveDataTest.java
+++ b/android/arch/lifecycle/ThreadedLiveDataTest.java
@@ -30,10 +30,13 @@ import android.support.annotation.Nullable;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+@RunWith(JUnit4.class)
public class ThreadedLiveDataTest {
private static final int TIMEOUT_SECS = 3;
diff --git a/android/arch/lifecycle/TransformationsTest.java b/android/arch/lifecycle/TransformationsTest.java
index 940a3e86..02397da7 100644
--- a/android/arch/lifecycle/TransformationsTest.java
+++ b/android/arch/lifecycle/TransformationsTest.java
@@ -21,6 +21,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Matchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.only;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -191,4 +192,25 @@ public class TransformationsTest {
verify(observer, never()).onChanged(anyString());
assertThat(first.hasObservers(), is(false));
}
+
+ @Test
+ public void noObsoleteValueTest() {
+ MutableLiveData<Integer> numbers = new MutableLiveData<>();
+ LiveData<Integer> squared = Transformations.map(numbers, new Function<Integer, Integer>() {
+ @Override
+ public Integer apply(Integer input) {
+ return input * input;
+ }
+ });
+
+ Observer observer = mock(Observer.class);
+ squared.setValue(1);
+ squared.observeForever(observer);
+ verify(observer).onChanged(1);
+ squared.removeObserver(observer);
+ reset(observer);
+ numbers.setValue(2);
+ squared.observeForever(observer);
+ verify(observer, only()).onChanged(4);
+ }
}
diff --git a/android/arch/lifecycle/ViewModelProvider.java b/android/arch/lifecycle/ViewModelProvider.java
index a7b3aeba..e01aa19a 100644
--- a/android/arch/lifecycle/ViewModelProvider.java
+++ b/android/arch/lifecycle/ViewModelProvider.java
@@ -16,9 +16,12 @@
package android.arch.lifecycle;
+import android.app.Application;
import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
+import java.lang.reflect.InvocationTargetException;
+
/**
* An utility class that provides {@code ViewModels} for a scope.
* <p>
@@ -152,4 +155,57 @@ public class ViewModelProvider {
}
}
}
+
+ /**
+ * {@link Factory} which may create {@link AndroidViewModel} and
+ * {@link ViewModel}, which have an empty constructor.
+ */
+ public static class AndroidViewModelFactory extends ViewModelProvider.NewInstanceFactory {
+
+ private static AndroidViewModelFactory sInstance;
+
+ /**
+ * Retrieve a singleton instance of AndroidViewModelFactory.
+ *
+ * @param application an application to pass in {@link AndroidViewModel}
+ * @return A valid {@link AndroidViewModelFactory}
+ */
+ public static AndroidViewModelFactory getInstance(@NonNull Application application) {
+ if (sInstance == null) {
+ sInstance = new AndroidViewModelFactory(application);
+ }
+ return sInstance;
+ }
+
+ private Application mApplication;
+
+ /**
+ * Creates a {@code AndroidViewModelFactory}
+ *
+ * @param application an application to pass in {@link AndroidViewModel}
+ */
+ public AndroidViewModelFactory(@NonNull Application application) {
+ mApplication = application;
+ }
+
+ @NonNull
+ @Override
+ public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
+ if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
+ //noinspection TryWithIdenticalCatches
+ try {
+ return modelClass.getConstructor(Application.class).newInstance(mApplication);
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("Cannot create an instance of " + modelClass, e);
+ }
+ }
+ return super.create(modelClass);
+ }
+ }
}
diff --git a/android/arch/lifecycle/ViewModelProviderTest.java b/android/arch/lifecycle/ViewModelProviderTest.java
index 37d2020a..142f19a2 100644
--- a/android/arch/lifecycle/ViewModelProviderTest.java
+++ b/android/arch/lifecycle/ViewModelProviderTest.java
@@ -21,6 +21,7 @@ import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.MatcherAssert.assertThat;
import android.arch.lifecycle.ViewModelProvider.NewInstanceFactory;
+import android.support.annotation.NonNull;
import org.junit.Assert;
import org.junit.Before;
@@ -72,6 +73,7 @@ public class ViewModelProviderTest {
public void testOwnedBy() {
final ViewModelStore store = new ViewModelStore();
ViewModelStoreOwner owner = new ViewModelStoreOwner() {
+ @NonNull
@Override
public ViewModelStore getViewModelStore() {
return store;
diff --git a/android/arch/lifecycle/ViewModelProviders.java b/android/arch/lifecycle/ViewModelProviders.java
index b4b20aa4..d9894a8a 100644
--- a/android/arch/lifecycle/ViewModelProviders.java
+++ b/android/arch/lifecycle/ViewModelProviders.java
@@ -16,7 +16,6 @@
package android.arch.lifecycle;
-import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.arch.lifecycle.ViewModelProvider.Factory;
@@ -25,20 +24,16 @@ import android.support.annotation.NonNull;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentActivity;
-import java.lang.reflect.InvocationTargetException;
-
/**
* Utilities methods for {@link ViewModelStore} class.
*/
public class ViewModelProviders {
- @SuppressLint("StaticFieldLeak")
- private static DefaultFactory sDefaultFactory;
-
- private static void initializeFactoryIfNeeded(Application application) {
- if (sDefaultFactory == null) {
- sDefaultFactory = new DefaultFactory(application);
- }
+ /**
+ * @deprecated This class should not be directly instantiated
+ */
+ @Deprecated
+ public ViewModelProviders() {
}
private static Application checkApplication(Activity activity) {
@@ -62,30 +57,36 @@ public class ViewModelProviders {
* Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given
* {@code fragment} is alive. More detailed explanation is in {@link ViewModel}.
* <p>
- * It uses {@link DefaultFactory} to instantiate new ViewModels.
+ * It uses {@link ViewModelProvider.AndroidViewModelFactory} to instantiate new ViewModels.
*
* @param fragment a fragment, in whose scope ViewModels should be retained
* @return a ViewModelProvider instance
*/
+ @NonNull
@MainThread
public static ViewModelProvider of(@NonNull Fragment fragment) {
- initializeFactoryIfNeeded(checkApplication(checkActivity(fragment)));
- return new ViewModelProvider(ViewModelStores.of(fragment), sDefaultFactory);
+ ViewModelProvider.AndroidViewModelFactory factory =
+ ViewModelProvider.AndroidViewModelFactory.getInstance(
+ checkApplication(checkActivity(fragment)));
+ return new ViewModelProvider(ViewModelStores.of(fragment), factory);
}
/**
* Creates a {@link ViewModelProvider}, which retains ViewModels while a scope of given Activity
* is alive. More detailed explanation is in {@link ViewModel}.
* <p>
- * It uses {@link DefaultFactory} to instantiate new ViewModels.
+ * It uses {@link ViewModelProvider.AndroidViewModelFactory} to instantiate new ViewModels.
*
* @param activity an activity, in whose scope ViewModels should be retained
* @return a ViewModelProvider instance
*/
+ @NonNull
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity) {
- initializeFactoryIfNeeded(checkApplication(activity));
- return new ViewModelProvider(ViewModelStores.of(activity), sDefaultFactory);
+ ViewModelProvider.AndroidViewModelFactory factory =
+ ViewModelProvider.AndroidViewModelFactory.getInstance(
+ checkApplication(activity));
+ return new ViewModelProvider(ViewModelStores.of(activity), factory);
}
/**
@@ -98,6 +99,7 @@ public class ViewModelProviders {
* @param factory a {@code Factory} to instantiate new ViewModels
* @return a ViewModelProvider instance
*/
+ @NonNull
@MainThread
public static ViewModelProvider of(@NonNull Fragment fragment, @NonNull Factory factory) {
checkApplication(checkActivity(fragment));
@@ -114,6 +116,7 @@ public class ViewModelProviders {
* @param factory a {@code Factory} to instantiate new ViewModels
* @return a ViewModelProvider instance
*/
+ @NonNull
@MainThread
public static ViewModelProvider of(@NonNull FragmentActivity activity,
@NonNull Factory factory) {
@@ -124,39 +127,22 @@ public class ViewModelProviders {
/**
* {@link Factory} which may create {@link AndroidViewModel} and
* {@link ViewModel}, which have an empty constructor.
+ *
+ * @deprecated Use {@link ViewModelProvider.AndroidViewModelFactory}
*/
@SuppressWarnings("WeakerAccess")
- public static class DefaultFactory extends ViewModelProvider.NewInstanceFactory {
-
- private Application mApplication;
-
+ @Deprecated
+ public static class DefaultFactory extends ViewModelProvider.AndroidViewModelFactory {
/**
- * Creates a {@code DefaultFactory}
+ * Creates a {@code AndroidViewModelFactory}
*
* @param application an application to pass in {@link AndroidViewModel}
+ * @deprecated Use {@link ViewModelProvider.AndroidViewModelFactory} or
+ * {@link ViewModelProvider.AndroidViewModelFactory#getInstance(Application)}.
*/
+ @Deprecated
public DefaultFactory(@NonNull Application application) {
- mApplication = application;
- }
-
- @NonNull
- @Override
- public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
- if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
- //noinspection TryWithIdenticalCatches
- try {
- return modelClass.getConstructor(Application.class).newInstance(mApplication);
- } catch (NoSuchMethodException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- } catch (IllegalAccessException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- } catch (InstantiationException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- } catch (InvocationTargetException e) {
- throw new RuntimeException("Cannot create an instance of " + modelClass, e);
- }
- }
- return super.create(modelClass);
+ super(application);
}
}
}
diff --git a/android/arch/lifecycle/ViewModelStores.java b/android/arch/lifecycle/ViewModelStores.java
index e79c934a..348a06e7 100644
--- a/android/arch/lifecycle/ViewModelStores.java
+++ b/android/arch/lifecycle/ViewModelStores.java
@@ -38,6 +38,7 @@ public class ViewModelStores {
* @param activity an activity whose {@code ViewModelStore} is requested
* @return a {@code ViewModelStore}
*/
+ @NonNull
@MainThread
public static ViewModelStore of(@NonNull FragmentActivity activity) {
if (activity instanceof ViewModelStoreOwner) {
@@ -52,6 +53,7 @@ public class ViewModelStores {
* @param fragment a fragment whose {@code ViewModelStore} is requested
* @return a {@code ViewModelStore}
*/
+ @NonNull
@MainThread
public static ViewModelStore of(@NonNull Fragment fragment) {
if (fragment instanceof ViewModelStoreOwner) {
diff --git a/android/arch/paging/ContiguousPagedList.java b/android/arch/paging/ContiguousPagedList.java
index 42eb320d..c622f65b 100644
--- a/android/arch/paging/ContiguousPagedList.java
+++ b/android/arch/paging/ContiguousPagedList.java
@@ -58,8 +58,11 @@ class ContiguousPagedList<K, V> extends PagedList<V> implements PagedStorage.Cal
mStorage.appendPage(page, ContiguousPagedList.this);
} else if (resultType == PageResult.PREPEND) {
mStorage.prependPage(page, ContiguousPagedList.this);
+ } else {
+ throw new IllegalArgumentException("unexpected resultType " + resultType);
}
+
if (mBoundaryCallback != null) {
boolean deferEmpty = mStorage.size() == 0;
boolean deferBegin = !deferEmpty
diff --git a/android/arch/paging/DataSource.java b/android/arch/paging/DataSource.java
index bbf7ccb3..9f51539a 100644
--- a/android/arch/paging/DataSource.java
+++ b/android/arch/paging/DataSource.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * Copyright 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.
@@ -16,280 +16,7 @@
package android.arch.paging;
-import android.support.annotation.AnyThread;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.WorkerThread;
-
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.Executor;
-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. 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.
- * <p>
- * To control how and when a PagedList queries data from its DataSource, see
- * {@link PagedList.Config}. The Config object defines things like load sizes and prefetch distance.
- * <h4>Updating Paged Data</h4>
- * A PagedList / DataSource pair are a snapshot of the data set. A new pair of
- * PagedList / DataSource must be created if an update occurs, such as a reorder, insert, delete, or
- * content update occurs. A DataSource must detect that it cannot continue loading its
- * snapshot (for instance, when Database query notices a table being invalidated), and call
- * {@link #invalidate()}. Then a new PagedList / DataSource pair would be created to load data from
- * the new state of the Database query.
- * <p>
- * To page in data that doesn't update, you can create a single DataSource, and pass it to a single
- * PagedList. For example, loading from network when the network's paging API doesn't provide
- * updates.
- * <p>
- * To page in data from a source that does provide updates, you can create a
- * {@link DataSource.Factory}, where each DataSource created is invalidated when an update to the
- * data set occurs that makes the current snapshot invalid. For example, when paging a query from
- * the Database, and the table being queried inserts or removes items. You can also use a
- * DataSource.Factory to provide multiple versions of network-paged lists. If reloading all content
- * (e.g. in response to an action like swipe-to-refresh) is required to get a new version of data,
- * you can connect an explicit refresh signal to call {@link #invalidate()} on the current
- * DataSource.
- * <p>
- * If you have more granular update signals, such as a network API signaling an update to a single
- * item in the list, it's recommended to load data from network into memory. Then present that
- * data to the PagedList via a DataSource that wraps an in-memory snapshot. Each time the in-memory
- * 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 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 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 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
- * can differentiate unloaded placeholder items from content that has been paged in.
- *
- * @param <Key> Input used to trigger initial load from the DataSource. Often an Integer position.
- * @param <Value> Value type loaded by the DataSource.
- */
-@SuppressWarnings("unused") // suppress warning to remove Key/Value, needed for subclass type safety
-public abstract class DataSource<Key, Value> {
- /**
- * Factory for DataSources.
- * <p>
- * Data-loading systems of an application or library can implement this interface to allow
- * {@code LiveData<PagedList>}s to be created. For example, Room can provide a
- * DataSource.Factory for a given SQL query:
- *
- * <pre>
- * {@literal @}Dao
- * interface UserDao {
- * {@literal @}Query("SELECT * FROM user ORDER BY lastName ASC")
- * public abstract DataSource.Factory&lt;Integer, User> usersByLastName();
- * }
- * </pre>
- * In the above sample, {@code Integer} is used because it is the {@code Key} type of
- * PositionalDataSource. Currently, Room uses the {@code LIMIT}/{@code OFFSET} SQL keywords to
- * page a large query with a PositionalDataSource.
- *
- * @param <Key> Key identifying items in DataSource.
- * @param <Value> Type of items in the list loaded by the DataSources.
- */
+abstract public class DataSource<K, T> {
public interface Factory<Key, Value> {
- /**
- * Create a DataSource.
- * <p>
- * 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.
- */
- DataSource<Key, Value> create();
- }
-
- // Since we currently rely on implementation details of two implementations,
- // prevent external subclassing, except through exposed subclasses
- DataSource() {
- }
-
- /**
- * Returns true if the data source guaranteed to produce a contiguous set of items,
- * never producing gaps.
- */
- abstract boolean isContiguous();
-
- static class BaseLoadCallback<T> {
- static void validateInitialLoadParams(@NonNull List<?> data, int position, int totalCount) {
- if (position < 0) {
- throw new IllegalArgumentException("Position must be non-negative");
- }
- if (data.size() + position > totalCount) {
- throw new IllegalArgumentException(
- "List size + position too large, last item in list beyond totalCount.");
- }
- if (data.size() == 0 && totalCount > 0) {
- throw new IllegalArgumentException(
- "Initial result cannot be empty if items are present in data set.");
- }
- }
-
- @PageResult.ResultType
- final int mResultType;
- private final DataSource mDataSource;
- private final PageResult.Receiver<T> mReceiver;
-
- // mSignalLock protects mPostExecutor, and mHasSignalled
- private final Object mSignalLock = new Object();
- private Executor mPostExecutor = null;
- private boolean mHasSignalled = false;
-
- BaseLoadCallback(@NonNull DataSource dataSource, @PageResult.ResultType int resultType,
- @Nullable Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
- mDataSource = dataSource;
- mResultType = resultType;
- mPostExecutor = mainThreadExecutor;
- mReceiver = receiver;
- }
-
- void setPostExecutor(Executor postExecutor) {
- synchronized (mSignalLock) {
- mPostExecutor = postExecutor;
- }
- }
-
- /**
- * 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(
- "callback.onResult already called, cannot call again.");
- }
- mHasSignalled = true;
- executor = mPostExecutor;
- }
-
- if (executor != null) {
- executor.execute(new Runnable() {
- @Override
- public void run() {
- mReceiver.onPageResult(mResultType, result);
- }
- });
- } else {
- mReceiver.onPageResult(mResultType, result);
- }
- }
- }
-
- /**
- * Invalidation callback for DataSource.
- * <p>
- * Used to signal when a DataSource a data source has become invalid, and that a new data source
- * is needed to continue loading data.
- */
- public interface InvalidatedCallback {
- /**
- * Called when the data backing the list has become invalid. This callback is typically used
- * to signal that a new data source is needed.
- * <p>
- * This callback will be invoked on the thread that calls {@link #invalidate()}. It is valid
- * for the data source to invalidate itself during its load methods, or for an outside
- * source to invalidate it.
- */
- @AnyThread
- void onInvalidated();
- }
-
- private AtomicBoolean mInvalid = new AtomicBoolean(false);
-
- private CopyOnWriteArrayList<InvalidatedCallback> mOnInvalidatedCallbacks =
- new CopyOnWriteArrayList<>();
-
- /**
- * Add a callback to invoke when the DataSource is first invalidated.
- * <p>
- * Once invalidated, a data source will not become valid again.
- * <p>
- * A data source will only invoke its callbacks once - the first time {@link #invalidate()}
- * is called, on that thread.
- *
- * @param onInvalidatedCallback The callback, will be invoked on thread that
- * {@link #invalidate()} is called on.
- */
- @AnyThread
- @SuppressWarnings("WeakerAccess")
- public void addInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
- mOnInvalidatedCallbacks.add(onInvalidatedCallback);
- }
-
- /**
- * Remove a previously added invalidate callback.
- *
- * @param onInvalidatedCallback The previously added callback.
- */
- @AnyThread
- @SuppressWarnings("WeakerAccess")
- public void removeInvalidatedCallback(@NonNull InvalidatedCallback onInvalidatedCallback) {
- mOnInvalidatedCallbacks.remove(onInvalidatedCallback);
- }
-
- /**
- * Signal the data source to stop loading, and notify its callback.
- * <p>
- * If invalidate has already been called, this method does nothing.
- */
- @AnyThread
- public void invalidate() {
- if (mInvalid.compareAndSet(false, true)) {
- for (InvalidatedCallback callback : mOnInvalidatedCallbacks) {
- callback.onInvalidated();
- }
- }
- }
-
- /**
- * Returns true if the data source is invalid, and can no longer be queried for data.
- *
- * @return True if the data source is invalid, and can no longer return data.
- */
- @WorkerThread
- public boolean isInvalid() {
- return mInvalid.get();
}
}
diff --git a/android/arch/paging/LivePagedListProvider.java b/android/arch/paging/LivePagedListProvider.java
index 44b71a82..74334eee 100644
--- a/android/arch/paging/LivePagedListProvider.java
+++ b/android/arch/paging/LivePagedListProvider.java
@@ -22,8 +22,8 @@ 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
+// NOTE: Room 1.0 depends on this class, so it should not be removed until
+// we can require a version of Room that uses DataSource.Factory directly
/**
* Provides a {@code LiveData<PagedList>}, given a means to construct a DataSource.
* <p>
diff --git a/android/arch/paging/PagedListAdapter.java b/android/arch/paging/PagedListAdapter.java
index a8158c23..be232710 100644
--- a/android/arch/paging/PagedListAdapter.java
+++ b/android/arch/paging/PagedListAdapter.java
@@ -50,7 +50,7 @@ import android.support.v7.widget.RecyclerView;
* class MyViewModel extends ViewModel {
* public final LiveData&lt;PagedList&lt;User>> usersList;
* public MyViewModel(UserDao userDao) {
- * usersList = LivePagedListBuilder&lt;>(
+ * usersList = new LivePagedListBuilder&lt;>(
* userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
* }
* }
diff --git a/android/arch/paging/PagedListAdapterHelper.java b/android/arch/paging/PagedListAdapterHelper.java
index 7a0b81a6..ba8ffabe 100644
--- a/android/arch/paging/PagedListAdapterHelper.java
+++ b/android/arch/paging/PagedListAdapterHelper.java
@@ -54,7 +54,7 @@ import android.support.v7.widget.RecyclerView;
* class MyViewModel extends ViewModel {
* public final LiveData&lt;PagedList&lt;User>> usersList;
* public MyViewModel(UserDao userDao) {
- * usersList = LivePagedListBuilder&lt;>(
+ * usersList = new LivePagedListBuilder&lt;>(
* userDao.usersByLastName(), /* page size {@literal *}/ 20).build();
* }
* }
@@ -72,10 +72,8 @@ import android.support.v7.widget.RecyclerView;
* }
*
* class UserAdapter extends RecyclerView.Adapter&lt;UserViewHolder> {
- * private final PagedListAdapterHelper&lt;User> mHelper;
- * public UserAdapter(PagedListAdapterHelper.Builder&lt;User> builder) {
- * mHelper = new PagedListAdapterHelper(this, DIFF_CALLBACK);
- * }
+ * private final PagedListAdapterHelper&lt;User> mHelper
+ * = new PagedListAdapterHelper(this, DIFF_CALLBACK);
* {@literal @}Override
* public int getItemCount() {
* return mHelper.getItemCount();
diff --git a/android/arch/paging/PositionalDataSource.java b/android/arch/paging/PositionalDataSource.java
index 780bcf6d..7ffce009 100644
--- a/android/arch/paging/PositionalDataSource.java
+++ b/android/arch/paging/PositionalDataSource.java
@@ -172,7 +172,9 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
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.");
+ + " size to be a multiple of page size to support internal tiling."
+ + " loadSize " + data.size() + ", position " + position
+ + ", totalCount " + totalCount + ", pageSize " + mPageSize);
}
if (mCountingEnabled) {
@@ -236,9 +238,10 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
*/
public static class LoadRangeCallback<T> extends BaseLoadCallback<T> {
private final int mPositionOffset;
- LoadRangeCallback(@NonNull PositionalDataSource dataSource, int positionOffset,
+ LoadRangeCallback(@NonNull PositionalDataSource dataSource,
+ @PageResult.ResultType int resultType, int positionOffset,
Executor mainThreadExecutor, PageResult.Receiver<T> receiver) {
- super(dataSource, PageResult.TILE, mainThreadExecutor, receiver);
+ super(dataSource, resultType, mainThreadExecutor, receiver);
mPositionOffset = positionOffset;
}
@@ -272,10 +275,11 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
callback.setPostExecutor(mainThreadExecutor);
}
- final void dispatchLoadRange(int startPosition, int count,
- @NonNull Executor mainThreadExecutor, @NonNull PageResult.Receiver<T> receiver) {
- LoadRangeCallback<T> callback =
- new LoadRangeCallback<>(this, startPosition, mainThreadExecutor, receiver);
+ final void dispatchLoadRange(@PageResult.ResultType int resultType, int startPosition,
+ int count, @NonNull Executor mainThreadExecutor,
+ @NonNull PageResult.Receiver<T> receiver) {
+ LoadRangeCallback<T> callback = new LoadRangeCallback<>(
+ this, resultType, startPosition, mainThreadExecutor, receiver);
if (count == 0) {
callback.onResult(Collections.<T>emptyList());
} else {
@@ -467,7 +471,7 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
@NonNull PageResult.Receiver<Value> receiver) {
int startIndex = currentEndIndex + 1;
mPositionalDataSource.dispatchLoadRange(
- startIndex, pageSize, mainThreadExecutor, receiver);
+ PageResult.APPEND, startIndex, pageSize, mainThreadExecutor, receiver);
}
@Override
@@ -479,12 +483,12 @@ public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
if (startIndex < 0) {
// trigger empty list load
mPositionalDataSource.dispatchLoadRange(
- startIndex, 0, mainThreadExecutor, receiver);
+ PageResult.PREPEND, startIndex, 0, mainThreadExecutor, receiver);
} else {
int loadSize = Math.min(pageSize, startIndex + 1);
startIndex = startIndex - loadSize + 1;
mPositionalDataSource.dispatchLoadRange(
- startIndex, loadSize, mainThreadExecutor, receiver);
+ PageResult.PREPEND, startIndex, loadSize, mainThreadExecutor, receiver);
}
}
diff --git a/android/arch/paging/TiledDataSource.java b/android/arch/paging/TiledDataSource.java
index 77695e5c..7285aa46 100644
--- a/android/arch/paging/TiledDataSource.java
+++ b/android/arch/paging/TiledDataSource.java
@@ -23,6 +23,8 @@ import android.support.annotation.WorkerThread;
import java.util.Collections;
import java.util.List;
+// NOTE: Room 1.0 depends on this class, so it should not be removed until
+// we can require a version of Room that uses PositionalDataSource directly
/**
* @param <T> Type loaded by the TiledDataSource.
*
@@ -60,9 +62,11 @@ public abstract class TiledDataSource<T> extends PositionalDataSource<T> {
// convert from legacy behavior
List<T> list = loadRange(firstLoadPosition, firstLoadSize);
- if (list != null) {
+ if (list != null && list.size() == firstLoadSize) {
callback.onResult(list, firstLoadPosition, totalCount);
} else {
+ // null list, or size doesn't match request
+ // The size check is a WAR for Room 1.0, subsequent versions do the check in Room
invalidate();
}
}
diff --git a/android/arch/paging/TiledPagedList.java b/android/arch/paging/TiledPagedList.java
index f7aae980..9958b8dd 100644
--- a/android/arch/paging/TiledPagedList.java
+++ b/android/arch/paging/TiledPagedList.java
@@ -44,6 +44,10 @@ class TiledPagedList<T> extends PagedList<T>
return;
}
+ if (type != PageResult.INIT && type != PageResult.TILE) {
+ throw new IllegalArgumentException("unexpected resultType" + type);
+ }
+
if (mStorage.getPageCount() == 0) {
mStorage.initAndSplit(
pageResult.leadingNulls, pageResult.page, pageResult.trailingNulls,
@@ -179,7 +183,7 @@ class TiledPagedList<T> extends PagedList<T>
int startPosition = pageIndex * pageSize;
int count = Math.min(pageSize, mStorage.size() - startPosition);
mDataSource.dispatchLoadRange(
- startPosition, count, mMainThreadExecutor, mReceiver);
+ PageResult.TILE, startPosition, count, mMainThreadExecutor, mReceiver);
}
}
});
diff --git a/android/arch/persistence/db/SimpleSQLiteQuery.java b/android/arch/persistence/db/SimpleSQLiteQuery.java
index e2a38294..bcf4f49b 100644
--- a/android/arch/persistence/db/SimpleSQLiteQuery.java
+++ b/android/arch/persistence/db/SimpleSQLiteQuery.java
@@ -17,8 +17,8 @@
package android.arch.persistence.db;
/**
- * A basic implemtation of {@link SupportSQLiteQuery} which receives a query and its args and binds
- * args based on the passed in Object type.
+ * A basic implementation of {@link SupportSQLiteQuery} which receives a query and its args and
+ * binds args based on the passed in Object type.
*/
public final class SimpleSQLiteQuery implements SupportSQLiteQuery {
private final String mQuery;
diff --git a/android/arch/persistence/room/BuilderTest.java b/android/arch/persistence/room/BuilderTest.java
index 0728ccab..2c9b9e79 100644
--- a/android/arch/persistence/room/BuilderTest.java
+++ b/android/arch/persistence/room/BuilderTest.java
@@ -30,6 +30,7 @@ import android.arch.persistence.db.SupportSQLiteOpenHelper;
import android.arch.persistence.db.framework.FrameworkSQLiteOpenHelperFactory;
import android.arch.persistence.room.migration.Migration;
import android.content.Context;
+import android.support.annotation.NonNull;
import org.hamcrest.CoreMatchers;
import org.junit.Test;
@@ -108,15 +109,119 @@ public class BuilderTest {
}
@Test
+ public void migrationDowngrade() {
+ Migration m1_2 = new EmptyMigration(1, 2);
+ Migration m2_3 = new EmptyMigration(2, 3);
+ Migration m3_4 = new EmptyMigration(3, 4);
+ Migration m3_2 = new EmptyMigration(3, 2);
+ Migration m2_1 = new EmptyMigration(2, 1);
+ TestDatabase db = Room.databaseBuilder(mock(Context.class), TestDatabase.class, "foo")
+ .addMigrations(m1_2, m2_3, m3_4, m3_2, m2_1).build();
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ RoomDatabase.MigrationContainer migrations = config.migrationContainer;
+ assertThat(migrations.findMigrationPath(3, 2), is(asList(m3_2)));
+ assertThat(migrations.findMigrationPath(3, 1), is(asList(m3_2, m2_1)));
+ }
+
+ @Test
public void skipMigration() {
Context context = mock(Context.class);
+
TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
- .fallbackToDestructiveMigration().build();
+ .fallbackToDestructiveMigration()
+ .build();
+
DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
assertThat(config.requireMigration, is(false));
}
@Test
+ public void fallbackToDestructiveMigrationFrom_calledOnce_migrationsNotRequiredForValues() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 2).build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(2), is(false));
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_calledTwice_migrationsNotRequiredForValues() {
+ Context context = mock(Context.class);
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 2)
+ .fallbackToDestructiveMigrationFrom(3, 4)
+ .build();
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(2), is(false));
+ assertThat(config.isMigrationRequiredFrom(3), is(false));
+ assertThat(config.isMigrationRequiredFrom(4), is(false));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_fallBackToDestructiveCalled_alwaysReturnsFalse() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigration()
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(0), is(false));
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(5), is(false));
+ assertThat(config.isMigrationRequiredFrom(12), is(false));
+ assertThat(config.isMigrationRequiredFrom(132), is(false));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_byDefault_alwaysReturnsTrue() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(0), is(true));
+ assertThat(config.isMigrationRequiredFrom(1), is(true));
+ assertThat(config.isMigrationRequiredFrom(5), is(true));
+ assertThat(config.isMigrationRequiredFrom(12), is(true));
+ assertThat(config.isMigrationRequiredFrom(132), is(true));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_fallBackToDestFromCalled_falseForProvidedValues() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 4, 81)
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(1), is(false));
+ assertThat(config.isMigrationRequiredFrom(4), is(false));
+ assertThat(config.isMigrationRequiredFrom(81), is(false));
+ }
+
+ @Test
+ public void isMigrationRequiredFrom_fallBackToDestFromCalled_trueForNonProvidedValues() {
+ Context context = mock(Context.class);
+
+ TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class)
+ .fallbackToDestructiveMigrationFrom(1, 4, 81)
+ .build();
+
+ DatabaseConfiguration config = ((BuilderTest_TestDatabase_Impl) db).mConfig;
+ assertThat(config.isMigrationRequiredFrom(2), is(true));
+ assertThat(config.isMigrationRequiredFrom(3), is(true));
+ assertThat(config.isMigrationRequiredFrom(73), is(true));
+ }
+
+ @Test
public void createBasic() {
Context context = mock(Context.class);
TestDatabase db = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();
@@ -163,7 +268,7 @@ public class BuilderTest {
}
@Override
- public void migrate(SupportSQLiteDatabase database) {
+ public void migrate(@NonNull SupportSQLiteDatabase database) {
}
}
diff --git a/android/arch/persistence/room/ColumnInfo.java b/android/arch/persistence/room/ColumnInfo.java
index 65da379c..32b58187 100644
--- a/android/arch/persistence/room/ColumnInfo.java
+++ b/android/arch/persistence/room/ColumnInfo.java
@@ -68,7 +68,7 @@ public @interface ColumnInfo {
* collation sequence to the column, and SQLite treats it like {@link #BINARY}.
*
* @return The collation sequence of the column. This is either {@link #UNSPECIFIED},
- * {@link #BINARY}, {@link #NOCASE}, or {@link #RTRIM}.
+ * {@link #BINARY}, {@link #NOCASE}, {@link #RTRIM}, {@link #LOCALIZED} or {@link #UNICODE}.
*/
@Collate int collate() default UNSPECIFIED;
@@ -141,8 +141,20 @@ public @interface ColumnInfo {
* @see #collate()
*/
int RTRIM = 4;
+ /**
+ * Collation sequence that uses system's current locale.
+ *
+ * @see #collate()
+ */
+ int LOCALIZED = 5;
+ /**
+ * Collation sequence that uses Unicode Collation Algorithm.
+ *
+ * @see #collate()
+ */
+ int UNICODE = 6;
- @IntDef({UNSPECIFIED, BINARY, NOCASE, RTRIM})
+ @IntDef({UNSPECIFIED, BINARY, NOCASE, RTRIM, LOCALIZED, UNICODE})
@interface Collate {
}
}
diff --git a/android/arch/persistence/room/DatabaseConfiguration.java b/android/arch/persistence/room/DatabaseConfiguration.java
index adf5d4df..42acc1d0 100644
--- a/android/arch/persistence/room/DatabaseConfiguration.java
+++ b/android/arch/persistence/room/DatabaseConfiguration.java
@@ -23,6 +23,7 @@ import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import java.util.List;
+import java.util.Set;
/**
* Configuration class for a {@link RoomDatabase}.
@@ -65,6 +66,11 @@ public class DatabaseConfiguration {
public final boolean requireMigration;
/**
+ * The collection of schema versions from which migrations aren't required.
+ */
+ private final Set<Integer> mMigrationNotRequiredFrom;
+
+ /**
* Creates a database configuration with the given values.
*
* @param context The application context.
@@ -75,6 +81,8 @@ public class DatabaseConfiguration {
* @param allowMainThreadQueries Whether to allow main thread reads/writes or not.
* @param requireMigration True if Room should require a valid migration if version changes,
* instead of recreating the tables.
+ * @param migrationNotRequiredFrom The collection of schema versions from which migrations
+ * aren't required.
*
* @hide
*/
@@ -84,7 +92,8 @@ public class DatabaseConfiguration {
@NonNull RoomDatabase.MigrationContainer migrationContainer,
@Nullable List<RoomDatabase.Callback> callbacks,
boolean allowMainThreadQueries,
- boolean requireMigration) {
+ boolean requireMigration,
+ @Nullable Set<Integer> migrationNotRequiredFrom) {
this.sqliteOpenHelperFactory = sqliteOpenHelperFactory;
this.context = context;
this.name = name;
@@ -92,5 +101,21 @@ public class DatabaseConfiguration {
this.callbacks = callbacks;
this.allowMainThreadQueries = allowMainThreadQueries;
this.requireMigration = requireMigration;
+ this.mMigrationNotRequiredFrom = migrationNotRequiredFrom;
+ }
+
+ /**
+ * Returns whether a migration is required from the specified version.
+ *
+ * @param version The schema version.
+ * @return True if a valid migration is required, false otherwise.
+ */
+ public boolean isMigrationRequiredFrom(int version) {
+ // Migrations are required from this version if we generally require migrations AND EITHER
+ // there are no exceptions OR the supplied version is not one of the exceptions.
+ return requireMigration
+ && (mMigrationNotRequiredFrom == null
+ || !mMigrationNotRequiredFrom.contains(version));
+
}
}
diff --git a/android/arch/persistence/room/RawQuery.java b/android/arch/persistence/room/RawQuery.java
new file mode 100644
index 00000000..b41feab9
--- /dev/null
+++ b/android/arch/persistence/room/RawQuery.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 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.arch.persistence.room;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a method in a {@link Dao} annotated class as a raw query method where you can pass the
+ * query as a {@link String} or a
+ * {@link android.arch.persistence.db.SupportSQLiteQuery SupportSQLiteQuery}.
+ * <pre>
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * User getUser(String query);
+ * {@literal @}RawQuery
+ * User getUserViaQuery(SupportSQLiteQuery query);
+ * }
+ * User user = rawDao.getUser("SELECT * FROM User WHERE id = 3 LIMIT 1");
+ * SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE id = ? LIMIT 1",
+ * new Object[]{3});
+ * User user2 = rawDao.getUserViaQuery(query);
+ * </pre>
+ * <p>
+ * Room will generate the code based on the return type of the function and failure to
+ * pass a proper query will result in a runtime failure or an undefined result.
+ * <p>
+ * If you know the query at compile time, you should always prefer {@link Query} since it validates
+ * the query at compile time and also generates more efficient code since Room can compute the
+ * query result at compile time (e.g. it does not need to account for possibly missing columns in
+ * the response).
+ * <p>
+ * On the other hand, {@code RawQuery} serves as an escape hatch where you can build your own
+ * SQL query at runtime but still use Room to convert it into objects.
+ * <p>
+ * {@code RawQuery} methods must return a non-void type. If you want to execute a raw query that
+ * does not return any value, use {@link android.arch.persistence.room.RoomDatabase#query
+ * RoomDatabase#query} methods.
+ * <p>
+ * <b>Observable Queries:</b>
+ * <p>
+ * {@code RawQuery} methods can return observable types but you need to specify which tables are
+ * accessed in the query using the {@link #observedEntities()} field in the annotation.
+ * <pre>
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery(observedEntities = User.class)
+ * LiveData&lt;List&lt;User>> getUsers(String query);
+ * }
+ * LiveData&lt;List&lt;User>> liveUsers = rawDao.getUsers("SELECT * FROM User ORDER BY name DESC");
+ * </pre>
+ * <b>Returning Pojos:</b>
+ * <p>
+ * RawQueries can also return plain old java objects, similar to {@link Query} methods.
+ * <pre>
+ * public class NameAndLastName {
+ * public final String name;
+ * public final String lastName;
+ *
+ * public NameAndLastName(String name, String lastName) {
+ * this.name = name;
+ * this.lastName = lastName;
+ * }
+ * }
+ *
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * NameAndLastName getNameAndLastName(String query);
+ * }
+ * NameAndLastName result = rawDao.getNameAndLastName("SELECT * FROM User WHERE id = 3")
+ * // or
+ * NameAndLastName result = rawDao.getNameAndLastName("SELECT name, lastName FROM User WHERE id =
+ * 3")
+ * </pre>
+ * <p>
+ * <b>Pojos with Embedded Fields:</b>
+ * <p>
+ * {@code RawQuery} methods can return pojos that include {@link Embedded} fields as well.
+ * <pre>
+ * public class UserAndPet {
+ * {@literal @}Embedded
+ * public User user;
+ * {@literal @}Embedded
+ * public Pet pet;
+ * }
+ *
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * UserAndPet getUserAndPet(String query);
+ * }
+ * UserAndPet received = rawDao.getUserAndPet(
+ * "SELECT * FROM User, Pet WHERE User.id = Pet.userId LIMIT 1")
+ * </pre>
+ *
+ * <b>Relations:</b>
+ * <p>
+ * {@code RawQuery} return types can also be objects with {@link Relation Relations}.
+ * <pre>
+ * public class UserAndAllPets {
+ * {@literal @}Embedded
+ * public User user;
+ * {@literal @}Relation(parentColumn = "id", entityColumn = "userId")
+ * public List&lt;Pet> pets;
+ * }
+ *
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery
+ * List&lt;UserAndAllPets> getUsersAndAllPets(String query);
+ * }
+ * List&lt;UserAndAllPets> result = rawDao.getUsersAndAllPets("SELECT * FROM users");
+ * </pre>
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.CLASS)
+public @interface RawQuery {
+ /**
+ * Denotes the list of entities which are accessed in the provided query and should be observed
+ * for invalidation if the query is observable.
+ * <p>
+ * The listed classes should be {@link Entity Entities} that are linked from the containing
+ * {@link Database}.
+ * <p>
+ * Providing this field in a non-observable query has no impact.
+ * <pre>
+ * {@literal @}Dao
+ * interface RawDao {
+ * {@literal @}RawQuery(observedEntities = User.class)
+ * LiveData&lt;List&lt;User>> getUsers(String query);
+ * }
+ * LiveData&lt;List&lt;User>> liveUsers = rawDao.getUsers("select * from User ORDER BY name
+ * DESC");
+ * </pre>
+ *
+ * @return List of entities that should invalidate the query if changed.
+ */
+ Class[] observedEntities() default {};
+}
diff --git a/android/arch/persistence/room/RoomDatabase.java b/android/arch/persistence/room/RoomDatabase.java
index 70d832b0..db7af1d9 100644
--- a/android/arch/persistence/room/RoomDatabase.java
+++ b/android/arch/persistence/room/RoomDatabase.java
@@ -35,7 +35,9 @@ import android.util.Log;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashSet;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
@@ -333,7 +335,14 @@ public abstract class RoomDatabase {
/**
* Migrations, mapped by from-to pairs.
*/
- private MigrationContainer mMigrationContainer;
+ private final MigrationContainer mMigrationContainer;
+ private Set<Integer> mMigrationsNotRequiredFrom;
+ /**
+ * Keeps track of {@link Migration#startVersion}s and {@link Migration#endVersion}s added in
+ * {@link #addMigrations(Migration...)} for later validation that makes those versions don't
+ * match any versions passed to {@link #fallbackToDestructiveMigrationFrom(Integer...)}.
+ */
+ private Set<Integer> mMigrationStartAndEndVersions;
Builder(@NonNull Context context, @NonNull Class<T> klass, @Nullable String name) {
mContext = context;
@@ -376,7 +385,15 @@ public abstract class RoomDatabase {
* @return this
*/
@NonNull
- public Builder<T> addMigrations(@NonNull Migration... migrations) {
+ public Builder<T> addMigrations(@NonNull Migration... migrations) {
+ if (mMigrationStartAndEndVersions == null) {
+ mMigrationStartAndEndVersions = new HashSet<>();
+ }
+ for (Migration migration: migrations) {
+ mMigrationStartAndEndVersions.add(migration.startVersion);
+ mMigrationStartAndEndVersions.add(migration.endVersion);
+ }
+
mMigrationContainer.addMigrations(migrations);
return this;
}
@@ -423,6 +440,36 @@ public abstract class RoomDatabase {
}
/**
+ * Informs Room that it is allowed to destructively recreate database tables from specific
+ * starting schema versions.
+ * <p>
+ * This functionality is the same as that provided by
+ * {@link #fallbackToDestructiveMigration()}, except that this method allows the
+ * specification of a set of schema versions for which destructive recreation is allowed.
+ * <p>
+ * Using this method is preferable to {@link #fallbackToDestructiveMigration()} if you want
+ * to allow destructive migrations from some schema versions while still taking advantage
+ * of exceptions being thrown due to unintentionally missing migrations.
+ * <p>
+ * Note: No versions passed to this method may also exist as either starting or ending
+ * versions in the {@link Migration}s provided to {@link #addMigrations(Migration...)}. If a
+ * version passed to this method is found as a starting or ending version in a Migration, an
+ * exception will be thrown.
+ *
+ * @param startVersions The set of schema versions from which Room should use a destructive
+ * migration.
+ * @return this
+ */
+ @NonNull
+ public Builder<T> fallbackToDestructiveMigrationFrom(Integer... startVersions) {
+ if (mMigrationsNotRequiredFrom == null) {
+ mMigrationsNotRequiredFrom = new HashSet<>();
+ }
+ Collections.addAll(mMigrationsNotRequiredFrom, startVersions);
+ return this;
+ }
+
+ /**
* Adds a {@link Callback} to this database.
*
* @param callback The callback.
@@ -456,12 +503,28 @@ public abstract class RoomDatabase {
throw new IllegalArgumentException("Must provide an abstract class that"
+ " extends RoomDatabase");
}
+
+ if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) {
+ for (Integer version : mMigrationStartAndEndVersions) {
+ if (mMigrationsNotRequiredFrom.contains(version)) {
+ throw new IllegalArgumentException(
+ "Inconsistency detected. A Migration was supplied to "
+ + "addMigration(Migration... migrations) that has a start "
+ + "or end version equal to a start version supplied to "
+ + "fallbackToDestructiveMigrationFrom(Integer ... "
+ + "startVersions). Start version: "
+ + version);
+ }
+ }
+ }
+
if (mFactory == null) {
mFactory = new FrameworkSQLiteOpenHelperFactory();
}
DatabaseConfiguration configuration =
new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer,
- mCallbacks, mAllowMainThreadQueries, mRequireMigration);
+ mCallbacks, mAllowMainThreadQueries, mRequireMigration,
+ mMigrationsNotRequiredFrom);
T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
db.init(configuration);
return db;
@@ -545,8 +608,14 @@ public abstract class RoomDatabase {
}
boolean found = false;
for (int i = firstIndex; i != lastIndex; i += searchDirection) {
- int targetVersion = targetNodes.keyAt(i);
- if (targetVersion <= end && targetVersion > start) {
+ final int targetVersion = targetNodes.keyAt(i);
+ final boolean shouldAddToPath;
+ if (upgrade) {
+ shouldAddToPath = targetVersion <= end && targetVersion > start;
+ } else {
+ shouldAddToPath = targetVersion >= end && targetVersion < start;
+ }
+ if (shouldAddToPath) {
result.add(targetNodes.valueAt(i));
start = targetVersion;
found = true;
diff --git a/android/arch/persistence/room/RoomOpenHelper.java b/android/arch/persistence/room/RoomOpenHelper.java
index 47279d60..aad6895c 100644
--- a/android/arch/persistence/room/RoomOpenHelper.java
+++ b/android/arch/persistence/room/RoomOpenHelper.java
@@ -41,13 +41,20 @@ public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
private final Delegate mDelegate;
@NonNull
private final String mIdentityHash;
+ /**
+ * Room v1 had a bug where the hash was not consistent if fields are reordered.
+ * The new has fixes it but we still need to accept the legacy hash.
+ */
+ @NonNull // b/64290754
+ private final String mLegacyHash;
public RoomOpenHelper(@NonNull DatabaseConfiguration configuration, @NonNull Delegate delegate,
- @NonNull String identityHash) {
+ @NonNull String identityHash, @NonNull String legacyHash) {
super(delegate.version);
mConfiguration = configuration;
mDelegate = delegate;
mIdentityHash = identityHash;
+ mLegacyHash = legacyHash;
}
@Override
@@ -78,14 +85,17 @@ public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
}
}
if (!migrated) {
- if (mConfiguration == null || mConfiguration.requireMigration) {
+ if (mConfiguration != null && !mConfiguration.isMigrationRequiredFrom(oldVersion)) {
+ mDelegate.dropAllTables(db);
+ mDelegate.createAllTables(db);
+ } else {
throw new IllegalStateException("A migration from " + oldVersion + " to "
- + newVersion + " is necessary. Please provide a Migration in the builder or call"
- + " fallbackToDestructiveMigration in the builder in which case Room will"
- + " re-create all of the tables.");
+ + newVersion + " was required but not found. Please provide the "
+ + "necessary Migration path via "
+ + "RoomDatabase.Builder.addMigration(Migration ...) or allow for "
+ + "destructive migrations via one of the "
+ + "RoomDatabase.Builder.fallbackToDestructiveMigration* methods.");
}
- mDelegate.dropAllTables(db);
- mDelegate.createAllTables(db);
}
}
@@ -115,7 +125,7 @@ public class RoomOpenHelper extends SupportSQLiteOpenHelper.Callback {
} finally {
cursor.close();
}
- if (!mIdentityHash.equals(identityHash)) {
+ if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
+ " you've changed schema but forgot to update the version number. You can"
+ " simply fix this by increasing the version number.");
diff --git a/android/arch/persistence/room/integration/testapp/TestDatabase.java b/android/arch/persistence/room/integration/testapp/TestDatabase.java
index 610afb26..98282aba 100644
--- a/android/arch/persistence/room/integration/testapp/TestDatabase.java
+++ b/android/arch/persistence/room/integration/testapp/TestDatabase.java
@@ -25,6 +25,7 @@ import android.arch.persistence.room.integration.testapp.dao.FunnyNamedDao;
import android.arch.persistence.room.integration.testapp.dao.PetCoupleDao;
import android.arch.persistence.room.integration.testapp.dao.PetDao;
import android.arch.persistence.room.integration.testapp.dao.ProductDao;
+import android.arch.persistence.room.integration.testapp.dao.RawDao;
import android.arch.persistence.room.integration.testapp.dao.SchoolDao;
import android.arch.persistence.room.integration.testapp.dao.SpecificDogDao;
import android.arch.persistence.room.integration.testapp.dao.ToyDao;
@@ -61,6 +62,7 @@ public abstract class TestDatabase extends RoomDatabase {
public abstract SpecificDogDao getSpecificDogDao();
public abstract WithClauseDao getWithClauseDao();
public abstract FunnyNamedDao getFunnyNamedDao();
+ public abstract RawDao getRawDao();
@SuppressWarnings("unused")
public static class Converters {
diff --git a/android/arch/persistence/room/integration/testapp/dao/PetDao.java b/android/arch/persistence/room/integration/testapp/dao/PetDao.java
index 5d060f41..e3a45a0f 100644
--- a/android/arch/persistence/room/integration/testapp/dao/PetDao.java
+++ b/android/arch/persistence/room/integration/testapp/dao/PetDao.java
@@ -17,9 +17,11 @@
package android.arch.persistence.room.integration.testapp.dao;
import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Insert;
import android.arch.persistence.room.OnConflictStrategy;
import android.arch.persistence.room.Query;
+import android.arch.persistence.room.Transaction;
import android.arch.persistence.room.integration.testapp.vo.Pet;
import android.arch.persistence.room.integration.testapp.vo.PetWithToyIds;
@@ -38,4 +40,19 @@ public interface PetDao {
@Query("SELECT * FROM Pet ORDER BY Pet.mPetId ASC")
List<PetWithToyIds> allPetsWithToyIds();
+
+ @Delete
+ void delete(Pet pet);
+
+ @Query("SELECT mPetId FROM Pet")
+ int[] allIds();
+
+ @Transaction
+ default void deleteAndInsert(Pet oldPet, Pet newPet, boolean shouldFail) {
+ delete(oldPet);
+ if (shouldFail) {
+ throw new RuntimeException();
+ }
+ insertOrReplace(newPet);
+ }
}
diff --git a/android/arch/persistence/room/integration/testapp/dao/RawDao.java b/android/arch/persistence/room/integration/testapp/dao/RawDao.java
new file mode 100644
index 00000000..b4469c07
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/dao/RawDao.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 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.arch.persistence.room.integration.testapp.dao;
+
+import android.arch.lifecycle.LiveData;
+import android.arch.persistence.db.SupportSQLiteQuery;
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.RawQuery;
+import android.arch.persistence.room.integration.testapp.vo.NameAndLastName;
+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 java.util.Date;
+import java.util.List;
+
+@Dao
+public interface RawDao {
+ @RawQuery
+ User getUser(String query);
+ @RawQuery
+ UserAndAllPets getUserAndAllPets(String query);
+ @RawQuery
+ User getUser(SupportSQLiteQuery query);
+ @RawQuery
+ UserAndPet getUserAndPet(String query);
+ @RawQuery
+ NameAndLastName getUserNameAndLastName(String query);
+ @RawQuery(observedEntities = User.class)
+ NameAndLastName getUserNameAndLastName(SupportSQLiteQuery query);
+ @RawQuery
+ int count(String query);
+ @RawQuery
+ List<User> getUserList(String query);
+ @RawQuery
+ List<UserAndPet> getUserAndPetList(String query);
+ @RawQuery(observedEntities = User.class)
+ LiveData<User> getUserLiveData(String query);
+ @RawQuery
+ UserNameAndBirthday getUserAndBirthday(String query);
+ class UserNameAndBirthday {
+ @ColumnInfo(name = "mName")
+ public final String name;
+ @ColumnInfo(name = "mBirthday")
+ public final Date birthday;
+
+ public UserNameAndBirthday(String name, Date birthday) {
+ this.name = name;
+ this.birthday = birthday;
+ }
+ }
+}
diff --git a/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/android/arch/persistence/room/integration/testapp/dao/UserDao.java
index 1a2a4689..7cb8b608 100644
--- a/android/arch/persistence/room/integration/testapp/dao/UserDao.java
+++ b/android/arch/persistence/room/integration/testapp/dao/UserDao.java
@@ -18,8 +18,6 @@ package android.arch.persistence.room.integration.testapp.dao;
import android.arch.lifecycle.LiveData;
import android.arch.paging.DataSource;
-import android.arch.paging.LivePagedListProvider;
-import android.arch.paging.TiledDataSource;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Delete;
import android.arch.persistence.room.Insert;
@@ -166,6 +164,12 @@ public abstract class UserDao {
@Query("SELECT COUNT(*) from user")
public abstract int count();
+ @Query("SELECT mAdmin from User where mId = :uid")
+ public abstract boolean isAdmin(int uid);
+
+ @Query("SELECT mAdmin from User where mId = :uid")
+ public abstract LiveData<Boolean> isAdminLiveData(int uid);
+
public void insertBothByRunnable(final User a, final User b) {
mDatabase.runInTransaction(new Runnable() {
@Override
@@ -190,16 +194,10 @@ public abstract class UserDao {
@Query("SELECT * FROM user where mAge > :age")
public abstract DataSource.Factory<Integer, User> loadPagedByAge(int age);
- @Query("SELECT * FROM user where mAge > :age")
- public abstract LivePagedListProvider<Integer, User> loadPagedByAge_legacy(int age);
-
// TODO: switch to PositionalDataSource once Room supports it
@Query("SELECT * FROM user ORDER BY mAge DESC")
public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc();
- @Query("SELECT * FROM user ORDER BY mAge DESC")
- public abstract TiledDataSource<User> loadUsersByAgeDesc_legacy();
-
@Query("DELETE FROM User WHERE mId IN (:ids) AND mAge == :age")
public abstract int deleteByAgeAndIds(int age, List<Integer> ids);
diff --git a/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java b/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
index 7fe2bc94..c850a4d7 100644
--- a/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
+++ b/android/arch/persistence/room/integration/testapp/migration/MigrationTest.java
@@ -17,9 +17,13 @@
package android.arch.persistence.room.integration.testapp.migration;
import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.endsWith;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.notNullValue;
import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.CoreMatchers.startsWith;
import static org.hamcrest.MatcherAssert.assertThat;
import android.arch.persistence.db.SupportSQLiteDatabase;
@@ -33,6 +37,7 @@ import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import org.hamcrest.MatcherAssert;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -252,6 +257,95 @@ public class MigrationTest {
db.close();
}
+ @Test
+ public void failWithIdentityCheck() throws IOException {
+ for (int i = 1; i < MigrationDb.LATEST_VERSION; i++) {
+ String name = "test_" + i;
+ helper.createDatabase(name, i).close();
+ IllegalStateException exception = null;
+ try {
+ MigrationDb db = Room.databaseBuilder(
+ InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ MigrationDb.class, name).build();
+ db.runInTransaction(new Runnable() {
+ @Override
+ public void run() {
+ // do nothing
+ }
+ });
+ } catch (IllegalStateException ex) {
+ exception = ex;
+ }
+ MatcherAssert.assertThat("identity detection should've failed",
+ exception, notNullValue());
+ }
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_destructiveMigrationOccursForSuppliedVersion()
+ throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 6);
+ final MigrationDb.Dao_V1 dao = new MigrationDb.Dao_V1(database);
+ dao.insertIntoEntity1(2, "foo");
+ dao.insertIntoEntity1(3, "bar");
+ database.close();
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+
+ MigrationDb db = Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .fallbackToDestructiveMigrationFrom(6)
+ .build();
+
+ assertThat(db.dao().loadAllEntity1s().size(), is(0));
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_suppliedValueIsMigrationStartVersion_exception()
+ throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 6);
+ database.close();
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+
+ Throwable throwable = null;
+ try {
+ Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .addMigrations(MIGRATION_6_7)
+ .fallbackToDestructiveMigrationFrom(6)
+ .build();
+ } catch (Throwable t) {
+ throwable = t;
+ }
+
+ assertThat(throwable, is(not(nullValue())));
+ //noinspection ConstantConditions
+ assertThat(throwable.getMessage(),
+ startsWith("Inconsistency detected. A Migration was supplied to"));
+ assertThat(throwable.getMessage(), endsWith("6"));
+ }
+
+ @Test
+ public void fallbackToDestructiveMigrationFrom_suppliedValueIsMigrationEndVersion_exception()
+ throws IOException {
+ SupportSQLiteDatabase database = helper.createDatabase(TEST_DB, 5);
+ database.close();
+ Context targetContext = InstrumentationRegistry.getTargetContext();
+
+ Throwable throwable = null;
+ try {
+ Room.databaseBuilder(targetContext, MigrationDb.class, TEST_DB)
+ .addMigrations(MIGRATION_5_6)
+ .fallbackToDestructiveMigrationFrom(6)
+ .build();
+ } catch (Throwable t) {
+ throwable = t;
+ }
+
+ assertThat(throwable, is(not(nullValue())));
+ //noinspection ConstantConditions
+ assertThat(throwable.getMessage(),
+ startsWith("Inconsistency detected. A Migration was supplied to"));
+ assertThat(throwable.getMessage(), endsWith("6"));
+ }
+
private void testFailure(int startVersion, int endVersion) throws IOException {
final SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, startVersion);
db.close();
diff --git a/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java b/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
index b54abe8e..0f686563 100644
--- a/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
+++ b/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
@@ -60,37 +60,13 @@ public class DataSourceFactoryTest extends TestDatabaseTest {
@Test
public void getUsersAsPagedList()
throws InterruptedException, ExecutionException, TimeoutException {
- validateUsersAsPagedList(new LivePagedListFactory() {
- @Override
- public LiveData<PagedList<User>> create() {
- return new LivePagedListBuilder<>(
- mUserDao.loadPagedByAge(3),
- new PagedList.Config.Builder()
- .setPageSize(10)
- .setPrefetchDistance(1)
- .setInitialLoadSizeHint(10).build())
- .build();
- }
- });
- }
-
-
- // TODO: delete this and factory abstraction when LivePagedListProvider is removed
- @Test
- public void getUsersAsPagedList_legacyLivePagedListProvider()
- throws InterruptedException, ExecutionException, TimeoutException {
- validateUsersAsPagedList(new LivePagedListFactory() {
- @Override
- public LiveData<PagedList<User>> create() {
- return mUserDao.loadPagedByAge_legacy(3).create(
- 0,
- new PagedList.Config.Builder()
- .setPageSize(10)
- .setPrefetchDistance(1)
- .setInitialLoadSizeHint(10)
- .build());
- }
- });
+ validateUsersAsPagedList(() -> new LivePagedListBuilder<>(
+ mUserDao.loadPagedByAge(3),
+ new PagedList.Config.Builder()
+ .setPageSize(10)
+ .setPrefetchDistance(1)
+ .setInitialLoadSizeHint(10).build())
+ .build());
}
private void validateUsersAsPagedList(LivePagedListFactory factory)
diff --git a/android/arch/persistence/room/integration/testapp/paging/LimitOffsetDataSourceTest.java b/android/arch/persistence/room/integration/testapp/paging/LimitOffsetDataSourceTest.java
index f0285a04..89359bef 100644
--- a/android/arch/persistence/room/integration/testapp/paging/LimitOffsetDataSourceTest.java
+++ b/android/arch/persistence/room/integration/testapp/paging/LimitOffsetDataSourceTest.java
@@ -16,7 +16,7 @@
package android.arch.persistence.room.integration.testapp.paging;
-import static android.test.MoreAsserts.assertEmpty;
+import static junit.framework.Assert.assertFalse;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
@@ -45,15 +45,6 @@ public class LimitOffsetDataSourceTest extends TestDatabaseTest {
mUserDao.deleteEverything();
}
- // TODO: delete this and factory abstraction when LivePagedListProvider is removed
- @Test
- public void limitOffsetDataSource_legacyTiledDataSource() {
- // Simple verification that loading a TiledDataSource still works.
- LimitOffsetDataSource<User> dataSource =
- (LimitOffsetDataSource<User>) mUserDao.loadUsersByAgeDesc_legacy();
- assertThat(dataSource.countItems(), is(0));
- }
-
private LimitOffsetDataSource<User> loadUsersByAgeDesc() {
return (LimitOffsetDataSource<User>) mUserDao.loadUsersByAgeDesc().create();
}
@@ -79,7 +70,7 @@ public class LimitOffsetDataSourceTest extends TestDatabaseTest {
List<User> initial = dataSource.loadRange(0, 10);
assertThat(initial.get(0), is(users.get(0)));
- assertEmpty(dataSource.loadRange(1, 10));
+ assertFalse(dataSource.loadRange(1, 10).iterator().hasNext());
}
@Test
diff --git a/android/arch/persistence/room/integration/testapp/test/CollationTest.java b/android/arch/persistence/room/integration/testapp/test/CollationTest.java
new file mode 100644
index 00000000..7b0e9337
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/test/CollationTest.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright 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.arch.persistence.room.integration.testapp.test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.arch.persistence.room.ColumnInfo;
+import android.arch.persistence.room.Dao;
+import android.arch.persistence.room.Database;
+import android.arch.persistence.room.Insert;
+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.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class CollationTest {
+ private CollateDb mDb;
+ private CollateDao mDao;
+ private Locale mDefaultLocale;
+ private final CollateEntity mItem1 = new CollateEntity(1, "abı");
+ private final CollateEntity mItem2 = new CollateEntity(2, "abi");
+ private final CollateEntity mItem3 = new CollateEntity(3, "abj");
+ private final CollateEntity mItem4 = new CollateEntity(4, "abç");
+
+ @Before
+ public void init() {
+ mDefaultLocale = Locale.getDefault();
+ }
+
+ private void initDao(Locale systemLocale) {
+ Locale.setDefault(systemLocale);
+ mDb = Room.inMemoryDatabaseBuilder(InstrumentationRegistry.getTargetContext(),
+ CollateDb.class).build();
+ mDao = mDb.dao();
+ mDao.insert(mItem1);
+ mDao.insert(mItem2);
+ mDao.insert(mItem3);
+ mDao.insert(mItem4);
+ }
+
+ @After
+ public void closeDb() {
+ mDb.close();
+ Locale.setDefault(mDefaultLocale);
+ }
+
+ @Test
+ public void localized() {
+ initDao(new Locale("tr", "TR"));
+ List<CollateEntity> result = mDao.sortedByLocalized();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem1, mItem2, mItem3
+ )));
+ }
+
+ @Test
+ public void localized_asUnicode() {
+ initDao(Locale.getDefault());
+ List<CollateEntity> result = mDao.sortedByLocalizedAsUnicode();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem2, mItem1, mItem3
+ )));
+ }
+
+ @Test
+ public void unicode_asLocalized() {
+ initDao(new Locale("tr", "TR"));
+ List<CollateEntity> result = mDao.sortedByUnicodeAsLocalized();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem1, mItem2, mItem3
+ )));
+ }
+
+ @Test
+ public void unicode() {
+ initDao(Locale.getDefault());
+ List<CollateEntity> result = mDao.sortedByUnicode();
+ assertThat(result, CoreMatchers.is(Arrays.asList(
+ mItem4, mItem2, mItem1, mItem3
+ )));
+ }
+
+ @SuppressWarnings("WeakerAccess")
+ @android.arch.persistence.room.Entity
+ static class CollateEntity {
+ @PrimaryKey
+ public final int id;
+ @ColumnInfo(collate = ColumnInfo.LOCALIZED)
+ public final String localizedName;
+ @ColumnInfo(collate = ColumnInfo.UNICODE)
+ public final String unicodeName;
+
+ CollateEntity(int id, String name) {
+ this.id = id;
+ this.localizedName = name;
+ this.unicodeName = name;
+ }
+
+ CollateEntity(int id, String localizedName, String unicodeName) {
+ this.id = id;
+ this.localizedName = localizedName;
+ this.unicodeName = unicodeName;
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ CollateEntity that = (CollateEntity) o;
+
+ if (id != that.id) return false;
+ if (!localizedName.equals(that.localizedName)) return false;
+ return unicodeName.equals(that.unicodeName);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = id;
+ result = 31 * result + localizedName.hashCode();
+ result = 31 * result + unicodeName.hashCode();
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "CollateEntity{"
+ + "id=" + id
+ + ", localizedName='" + localizedName + '\''
+ + ", unicodeName='" + unicodeName + '\''
+ + '}';
+ }
+ }
+
+ @Dao
+ interface CollateDao {
+ @Query("SELECT * FROM CollateEntity ORDER BY localizedName ASC")
+ List<CollateEntity> sortedByLocalized();
+
+ @Query("SELECT * FROM CollateEntity ORDER BY localizedName COLLATE UNICODE ASC")
+ List<CollateEntity> sortedByLocalizedAsUnicode();
+
+ @Query("SELECT * FROM CollateEntity ORDER BY unicodeName ASC")
+ List<CollateEntity> sortedByUnicode();
+
+ @Query("SELECT * FROM CollateEntity ORDER BY unicodeName COLLATE LOCALIZED ASC")
+ List<CollateEntity> sortedByUnicodeAsLocalized();
+
+ @Insert
+ void insert(CollateEntity... entities);
+ }
+
+ @Database(entities = CollateEntity.class, version = 1, exportSchema = false)
+ abstract static class CollateDb extends RoomDatabase {
+ abstract CollateDao dao();
+ }
+}
diff --git a/android/arch/persistence/room/integration/testapp/test/DatabaseCallbackTest.java b/android/arch/persistence/room/integration/testapp/test/DatabaseCallbackTest.java
index 579b3e41..fafcc2c1 100644
--- a/android/arch/persistence/room/integration/testapp/test/DatabaseCallbackTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/DatabaseCallbackTest.java
@@ -50,26 +50,38 @@ public class DatabaseCallbackTest {
public void createAndOpen() {
Context context = InstrumentationRegistry.getTargetContext();
TestDatabaseCallback callback1 = new TestDatabaseCallback();
- TestDatabase db1 = Room.databaseBuilder(context, TestDatabase.class, "test")
- .addCallback(callback1)
- .build();
- assertFalse(callback1.mCreated);
- assertFalse(callback1.mOpened);
- User user1 = TestUtil.createUser(3);
- user1.setName("george");
- db1.getUserDao().insert(user1);
- assertTrue(callback1.mCreated);
- assertTrue(callback1.mOpened);
- TestDatabaseCallback callback2 = new TestDatabaseCallback();
- TestDatabase db2 = Room.databaseBuilder(context, TestDatabase.class, "test")
- .addCallback(callback2)
- .build();
- assertFalse(callback2.mCreated);
- assertFalse(callback2.mOpened);
- User user2 = db2.getUserDao().load(3);
- assertThat(user2.getName(), is("george"));
- assertFalse(callback2.mCreated); // Not called; already created by db1
- assertTrue(callback2.mOpened);
+ TestDatabase db1 = null;
+ TestDatabase db2 = null;
+ try {
+ db1 = Room.databaseBuilder(context, TestDatabase.class, "test")
+ .addCallback(callback1)
+ .build();
+ assertFalse(callback1.mCreated);
+ assertFalse(callback1.mOpened);
+ User user1 = TestUtil.createUser(3);
+ user1.setName("george");
+ db1.getUserDao().insert(user1);
+ assertTrue(callback1.mCreated);
+ assertTrue(callback1.mOpened);
+ TestDatabaseCallback callback2 = new TestDatabaseCallback();
+ db2 = Room.databaseBuilder(context, TestDatabase.class, "test")
+ .addCallback(callback2)
+ .build();
+ assertFalse(callback2.mCreated);
+ assertFalse(callback2.mOpened);
+ User user2 = db2.getUserDao().load(3);
+ assertThat(user2.getName(), is("george"));
+ assertFalse(callback2.mCreated); // Not called; already created by db1
+ assertTrue(callback2.mOpened);
+ } finally {
+ if (db1 != null) {
+ db1.close();
+ }
+ if (db2 != null) {
+ db2.close();
+ }
+ assertTrue(context.deleteDatabase("test"));
+ }
}
@Test
diff --git a/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java b/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java
index d78411f8..d0735980 100644
--- a/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/LiveDataQueryTest.java
@@ -315,6 +315,24 @@ public class LiveDataQueryTest extends TestDatabaseTest {
assertThat(weakLiveData.get(), nullValue());
}
+ @Test
+ public void booleanLiveData() throws ExecutionException, InterruptedException,
+ TimeoutException {
+ User user = TestUtil.createUser(3);
+ user.setAdmin(false);
+ LiveData<Boolean> adminLiveData = mUserDao.isAdminLiveData(3);
+ final TestLifecycleOwner lifecycleOwner = new TestLifecycleOwner();
+ lifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
+ final TestObserver<Boolean> observer = new TestObserver<>();
+ observe(adminLiveData, lifecycleOwner, observer);
+ assertThat(observer.get(), is(nullValue()));
+ mUserDao.insert(user);
+ assertThat(observer.get(), is(false));
+ user.setAdmin(true);
+ mUserDao.insertOrReplace(user);
+ assertThat(observer.get(), is(true));
+ }
+
private void observe(final LiveData liveData, final LifecycleOwner provider,
final Observer observer) throws ExecutionException, InterruptedException {
FutureTask<Void> futureTask = new FutureTask<>(new Callable<Void>() {
diff --git a/android/arch/persistence/room/integration/testapp/test/PojoTest.java b/android/arch/persistence/room/integration/testapp/test/PojoTest.java
index b43e2748..b1579fcb 100644
--- a/android/arch/persistence/room/integration/testapp/test/PojoTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/PojoTest.java
@@ -19,15 +19,15 @@ package android.arch.persistence.room.integration.testapp.test;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;
-import android.content.Context;
-import android.support.test.InstrumentationRegistry;
-import android.support.test.runner.AndroidJUnit4;
-
import android.arch.persistence.room.Room;
import android.arch.persistence.room.integration.testapp.TestDatabase;
import android.arch.persistence.room.integration.testapp.dao.UserDao;
import android.arch.persistence.room.integration.testapp.vo.AvgWeightByAge;
import android.arch.persistence.room.integration.testapp.vo.User;
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
import org.junit.Before;
import org.junit.Test;
@@ -35,6 +35,7 @@ import org.junit.runner.RunWith;
import java.util.Arrays;
+@LargeTest
@RunWith(AndroidJUnit4.class)
public class PojoTest {
private UserDao mUserDao;
diff --git a/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java b/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
index f076cf13..292e588d 100644
--- a/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/QueryTransactionTest.java
@@ -28,7 +28,7 @@ import android.arch.lifecycle.Observer;
import android.arch.paging.DataSource;
import android.arch.paging.LivePagedListBuilder;
import android.arch.paging.PagedList;
-import android.arch.paging.TiledDataSource;
+import android.arch.paging.PositionalDataSource;
import android.arch.persistence.room.Dao;
import android.arch.persistence.room.Database;
import android.arch.persistence.room.Entity;
@@ -41,6 +41,7 @@ import android.arch.persistence.room.Room;
import android.arch.persistence.room.RoomDatabase;
import android.arch.persistence.room.RoomWarnings;
import android.arch.persistence.room.Transaction;
+import android.arch.persistence.room.paging.LimitOffsetDataSource;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.test.InstrumentationRegistry;
@@ -227,7 +228,8 @@ public class QueryTransactionTest {
drain();
resetTransactionCount();
@SuppressWarnings("deprecation")
- TiledDataSource<Entity1> dataSource = mDao.dataSource();
+ LimitOffsetDataSource<Entity1> dataSource =
+ (LimitOffsetDataSource<Entity1>) mDao.dataSource();
dataSource.loadRange(0, 10);
assertThat(sStartedTransactionCount.get(), is(mUseTransactionDao ? 1 : 0));
}
@@ -371,7 +373,7 @@ public class QueryTransactionTest {
DataSource.Factory<Integer, Entity1> pagedList();
- TiledDataSource<Entity1> dataSource();
+ PositionalDataSource<Entity1> dataSource();
@Insert
void insert(Entity1 entity1);
@@ -413,7 +415,7 @@ public class QueryTransactionTest {
@Override
@Query(SELECT_ALL)
- TiledDataSource<Entity1> dataSource();
+ PositionalDataSource<Entity1> dataSource();
}
@Dao
@@ -456,7 +458,7 @@ public class QueryTransactionTest {
@Override
@Transaction
@Query(SELECT_ALL)
- TiledDataSource<Entity1> dataSource();
+ PositionalDataSource<Entity1> dataSource();
}
@Database(version = 1, entities = {Entity1.class, Child.class}, exportSchema = false)
diff --git a/android/arch/persistence/room/integration/testapp/test/RawQueryTest.java b/android/arch/persistence/room/integration/testapp/test/RawQueryTest.java
new file mode 100644
index 00000000..4aae4ea6
--- /dev/null
+++ b/android/arch/persistence/room/integration/testapp/test/RawQueryTest.java
@@ -0,0 +1,196 @@
+/*
+ * Copyright 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.arch.persistence.room.integration.testapp.test;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.nullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import android.arch.core.executor.testing.CountingTaskExecutorRule;
+import android.arch.lifecycle.LiveData;
+import android.arch.persistence.db.SimpleSQLiteQuery;
+import android.arch.persistence.room.integration.testapp.dao.RawDao;
+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.User;
+import android.arch.persistence.room.integration.testapp.vo.UserAndAllPets;
+import android.arch.persistence.room.integration.testapp.vo.UserAndPet;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class RawQueryTest extends TestDatabaseTest {
+ @Rule
+ public CountingTaskExecutorRule mExecutorRule = new CountingTaskExecutorRule();
+
+ @Test
+ public void entity_null() {
+ User user = mRawDao.getUser("SELECT * FROM User WHERE mId = 0");
+ assertThat(user, is(nullValue()));
+ }
+
+ @Test
+ public void entity_one() {
+ User expected = TestUtil.createUser(3);
+ mUserDao.insert(expected);
+ User received = mRawDao.getUser("SELECT * FROM User WHERE mId = 3");
+ assertThat(received, is(expected));
+ }
+
+ @Test
+ public void entity_list() {
+ List<User> expected = TestUtil.createUsersList(1, 2, 3, 4);
+ mUserDao.insertAll(expected.toArray(new User[4]));
+ List<User> received = mRawDao.getUserList("SELECT * FROM User ORDER BY mId ASC");
+ assertThat(received, is(expected));
+ }
+
+ @Test
+ public void entity_liveData() throws TimeoutException, InterruptedException {
+ LiveData<User> liveData = mRawDao.getUserLiveData("SELECT * FROM User WHERE mId = 3");
+ liveData.observeForever(user -> {
+ });
+ drain();
+ assertThat(liveData.getValue(), is(nullValue()));
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ drain();
+ assertThat(liveData.getValue(), is(user));
+ user.setLastName("cxZ");
+ mUserDao.insertOrReplace(user);
+ drain();
+ assertThat(liveData.getValue(), is(user));
+ }
+
+ @Test
+ public void entity_supportSql() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE mId = ?",
+ new Object[]{3});
+ User received = mRawDao.getUser(query);
+ assertThat(received, is(user));
+ }
+
+ @Test
+ public void embedded() {
+ User user = TestUtil.createUser(3);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 1);
+ mUserDao.insert(user);
+ mPetDao.insertAll(pets);
+ UserAndPet received = mRawDao.getUserAndPet(
+ "SELECT * FROM User, Pet WHERE User.mId = Pet.mUserId LIMIT 1");
+ assertThat(received.getUser(), is(user));
+ assertThat(received.getPet(), is(pets[0]));
+ }
+
+ @Test
+ public void relation() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 10);
+ mPetDao.insertAll(pets);
+ UserAndAllPets result = mRawDao
+ .getUserAndAllPets("SELECT * FROM User WHERE mId = 3");
+ assertThat(result.user, is(user));
+ assertThat(result.pets, is(Arrays.asList(pets)));
+ }
+
+ @Test
+ public void pojo() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ NameAndLastName result =
+ mRawDao.getUserNameAndLastName("SELECT * FROM User");
+ assertThat(result, is(new NameAndLastName(user.getName(), user.getLastName())));
+ }
+
+ @Test
+ public void pojo_supportSql() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ NameAndLastName result =
+ mRawDao.getUserNameAndLastName(new SimpleSQLiteQuery(
+ "SELECT * FROM User WHERE mId = ?",
+ new Object[] {3}
+ ));
+ assertThat(result, is(new NameAndLastName(user.getName(), user.getLastName())));
+ }
+
+ @Test
+ public void pojo_typeConverter() {
+ User user = TestUtil.createUser(3);
+ mUserDao.insert(user);
+ RawDao.UserNameAndBirthday result = mRawDao.getUserAndBirthday(
+ "SELECT mName, mBirthday FROM user LIMIT 1");
+ assertThat(result.name, is(user.getName()));
+ assertThat(result.birthday, is(user.getBirthday()));
+ }
+
+ @Test
+ public void embedded_nullField() {
+ User user = TestUtil.createUser(3);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 1);
+ mUserDao.insert(user);
+ mPetDao.insertAll(pets);
+ UserAndPet received = mRawDao.getUserAndPet("SELECT * FROM User LIMIT 1");
+ assertThat(received.getUser(), is(user));
+ assertThat(received.getPet(), is(nullValue()));
+ }
+
+ @Test
+ public void embedded_list() {
+ User[] users = TestUtil.createUsersArray(3, 5);
+ Pet[] pets = TestUtil.createPetsForUser(3, 1, 2);
+ mUserDao.insertAll(users);
+ mPetDao.insertAll(pets);
+ List<UserAndPet> received = mRawDao.getUserAndPetList(
+ "SELECT * FROM User LEFT JOIN Pet ON (User.mId = Pet.mUserId)"
+ + " ORDER BY mId ASC, mPetId ASC");
+ assertThat(received.size(), is(3));
+ // row 0
+ assertThat(received.get(0).getUser(), is(users[0]));
+ assertThat(received.get(0).getPet(), is(pets[0]));
+ // row 1
+ assertThat(received.get(1).getUser(), is(users[0]));
+ assertThat(received.get(1).getPet(), is(pets[1]));
+ // row 2
+ assertThat(received.get(2).getUser(), is(users[1]));
+ assertThat(received.get(2).getPet(), is(nullValue()));
+ }
+
+ @Test
+ public void count() {
+ mUserDao.insertAll(TestUtil.createUsersArray(3, 5, 7, 10));
+ int count = mRawDao.count("SELECT COUNT(*) FROM User");
+ assertThat(count, is(4));
+ }
+
+ private void drain() throws TimeoutException, InterruptedException {
+ mExecutorRule.drainTasks(1, TimeUnit.MINUTES);
+ }
+}
diff --git a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
index de45ebb3..793523c9 100644
--- a/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/SimpleEntityReadWriteTest.java
@@ -267,6 +267,18 @@ public class SimpleEntityReadWriteTest {
}
@Test
+ public void returnBoolean() {
+ User user1 = TestUtil.createUser(1);
+ User user2 = TestUtil.createUser(2);
+ user1.setAdmin(true);
+ user2.setAdmin(false);
+ mUserDao.insert(user1);
+ mUserDao.insert(user2);
+ assertThat(mUserDao.isAdmin(1), is(true));
+ assertThat(mUserDao.isAdmin(2), is(false));
+ }
+
+ @Test
public void findByCollateNoCase() {
User user = TestUtil.createUser(3);
user.setCustomField("abc");
@@ -483,6 +495,36 @@ public class SimpleEntityReadWriteTest {
}
@Test
+ public void transactionByDefaultImplementation() {
+ Pet pet1 = TestUtil.createPet(1);
+ mPetDao.insertOrReplace(pet1);
+ assertThat(mPetDao.count(), is(1));
+ assertThat(mPetDao.allIds()[0], is(1));
+ Pet pet2 = TestUtil.createPet(2);
+ mPetDao.deleteAndInsert(pet1, pet2, false);
+ assertThat(mPetDao.count(), is(1));
+ assertThat(mPetDao.allIds()[0], is(2));
+ }
+
+ @Test
+ public void transactionByDefaultImplementation_failure() {
+ Pet pet1 = TestUtil.createPet(1);
+ mPetDao.insertOrReplace(pet1);
+ assertThat(mPetDao.count(), is(1));
+ assertThat(mPetDao.allIds()[0], is(1));
+ Pet pet2 = TestUtil.createPet(2);
+ Throwable throwable = null;
+ try {
+ mPetDao.deleteAndInsert(pet1, pet2, true);
+ } catch (Throwable t) {
+ throwable = t;
+ }
+ assertNotNull("Was expecting an exception", throwable);
+ assertThat(mPetDao.count(), is(1));
+ assertThat(mPetDao.allIds()[0], is(1));
+ }
+
+ @Test
public void multipleInParamsFollowedByASingleParam_delete() {
User user = TestUtil.createUser(3);
user.setAge(30);
diff --git a/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java b/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
index ec775617..e2525c4b 100644
--- a/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
+++ b/android/arch/persistence/room/integration/testapp/test/TestDatabaseTest.java
@@ -21,6 +21,7 @@ import android.arch.persistence.room.integration.testapp.TestDatabase;
import android.arch.persistence.room.integration.testapp.dao.FunnyNamedDao;
import android.arch.persistence.room.integration.testapp.dao.PetCoupleDao;
import android.arch.persistence.room.integration.testapp.dao.PetDao;
+import android.arch.persistence.room.integration.testapp.dao.RawDao;
import android.arch.persistence.room.integration.testapp.dao.SchoolDao;
import android.arch.persistence.room.integration.testapp.dao.SpecificDogDao;
import android.arch.persistence.room.integration.testapp.dao.ToyDao;
@@ -44,6 +45,7 @@ public abstract class TestDatabaseTest {
protected SpecificDogDao mSpecificDogDao;
protected WithClauseDao mWithClauseDao;
protected FunnyNamedDao mFunnyNamedDao;
+ protected RawDao mRawDao;
@Before
public void createDb() {
@@ -58,5 +60,6 @@ public abstract class TestDatabaseTest {
mSpecificDogDao = mDatabase.getSpecificDogDao();
mWithClauseDao = mDatabase.getWithClauseDao();
mFunnyNamedDao = mDatabase.getFunnyNamedDao();
+ mRawDao = mDatabase.getRawDao();
}
}
diff --git a/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java b/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
index 29e25548..a6e82230 100644
--- a/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
+++ b/android/arch/persistence/room/integration/testapp/vo/NameAndLastName.java
@@ -33,4 +33,23 @@ public class NameAndLastName {
public String getLastName() {
return mLastName;
}
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ NameAndLastName that = (NameAndLastName) o;
+
+ if (mName != null ? !mName.equals(that.mName) : that.mName != null) return false;
+ return mLastName != null ? mLastName.equals(that.mLastName) : that.mLastName == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mName != null ? mName.hashCode() : 0;
+ result = 31 * result + (mLastName != null ? mLastName.hashCode() : 0);
+ return result;
+ }
}
diff --git a/android/arch/persistence/room/migration/TableInfoTest.java b/android/arch/persistence/room/migration/TableInfoTest.java
index d88c02fd..0eb35f6e 100644
--- a/android/arch/persistence/room/migration/TableInfoTest.java
+++ b/android/arch/persistence/room/migration/TableInfoTest.java
@@ -31,6 +31,7 @@ import android.arch.persistence.room.util.TableInfo;
import android.support.test.InstrumentationRegistry;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
+import android.util.Pair;
import org.junit.After;
import org.junit.Test;
@@ -41,6 +42,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -209,6 +211,28 @@ public class TableInfoTest {
));
}
+ @Test
+ public void compatColumnTypes() {
+ // see:https://www.sqlite.org/datatype3.html 3.1
+ List<Pair<String, String>> testCases = Arrays.asList(
+ new Pair<>("TINYINT", "integer"),
+ new Pair<>("VARCHAR", "text"),
+ new Pair<>("DOUBLE", "real"),
+ new Pair<>("BOOLEAN", "numeric"),
+ new Pair<>("FLOATING POINT", "integer")
+ );
+ for (Pair<String, String> testCase : testCases) {
+ mDb = createDatabase(
+ "CREATE TABLE foo (id INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + "name " + testCase.first + ")");
+ TableInfo info = TableInfo.read(mDb, "foo");
+ assertThat(info, is(new TableInfo("foo",
+ toMap(new TableInfo.Column("id", "INTEGER", false, 1),
+ new TableInfo.Column("name", testCase.second, false, 0)),
+ Collections.<TableInfo.ForeignKey>emptySet())));
+ }
+ }
+
private static Map<String, TableInfo.Column> toMap(TableInfo.Column... columns) {
Map<String, TableInfo.Column> result = new HashMap<>();
for (TableInfo.Column column : columns) {
diff --git a/android/arch/persistence/room/migration/bundle/DatabaseBundle.java b/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
index 4ac9029b..f131838d 100644
--- a/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
+++ b/android/arch/persistence/room/migration/bundle/DatabaseBundle.java
@@ -32,7 +32,7 @@ import java.util.Map;
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class DatabaseBundle {
+public class DatabaseBundle implements SchemaEquality<DatabaseBundle> {
@SerializedName("version")
private int mVersion;
@SerializedName("identityHash")
@@ -104,4 +104,10 @@ public class DatabaseBundle {
result.addAll(mSetupQueries);
return result;
}
+
+ @Override
+ public boolean isSchemaEqual(DatabaseBundle other) {
+ return SchemaEqualityUtil.checkSchemaEquality(getEntitiesByTableName(),
+ other.getEntitiesByTableName());
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/EntityBundle.java b/android/arch/persistence/room/migration/bundle/EntityBundle.java
index 8980a3b6..d78ac358 100644
--- a/android/arch/persistence/room/migration/bundle/EntityBundle.java
+++ b/android/arch/persistence/room/migration/bundle/EntityBundle.java
@@ -16,6 +16,8 @@
package android.arch.persistence.room.migration.bundle;
+import static android.arch.persistence.room.migration.bundle.SchemaEqualityUtil.checkSchemaEquality;
+
import android.support.annotation.RestrictTo;
import com.google.gson.annotations.SerializedName;
@@ -35,7 +37,7 @@ import java.util.Map;
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class EntityBundle {
+public class EntityBundle implements SchemaEquality<EntityBundle> {
static final String NEW_TABLE_PREFIX = "_new_";
@@ -176,4 +178,15 @@ public class EntityBundle {
}
return result;
}
+
+ @Override
+ public boolean isSchemaEqual(EntityBundle other) {
+ if (!mTableName.equals(other.mTableName)) {
+ return false;
+ }
+ return checkSchemaEquality(getFieldsByColumnName(), other.getFieldsByColumnName())
+ && checkSchemaEquality(mPrimaryKey, other.mPrimaryKey)
+ && checkSchemaEquality(mIndices, other.mIndices)
+ && checkSchemaEquality(mForeignKeys, other.mForeignKeys);
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/EntityBundleTest.java b/android/arch/persistence/room/migration/bundle/EntityBundleTest.java
new file mode 100644
index 00000000..4b4df8bf
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/EntityBundleTest.java
@@ -0,0 +1,168 @@
+/*
+ * 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.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import static java.util.Arrays.asList;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Collections;
+
+@SuppressWarnings("ArraysAsListWithZeroOrOneArgument")
+@RunWith(JUnit4.class)
+public class EntityBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo")),
+ asList(createForeignKeyBundle("bar", "foo")));
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo")),
+ asList(createForeignKeyBundle("bar", "foo")));
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_reorderedFields_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("bar"), createFieldBundle("foo")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffFields_notEqual() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ asList(createFieldBundle("foo2"), createFieldBundle("bar")),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_reorderedForeignKeys_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("x", "y"),
+ createForeignKeyBundle("bar", "foo")));
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("bar", "foo"),
+ createForeignKeyBundle("x", "y")));
+
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffForeignKeys_notEqual() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("bar", "foo")));
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ Collections.<IndexBundle>emptyList(),
+ asList(createForeignKeyBundle("bar2", "foo")));
+
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_reorderedIndices_equal() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo"), createIndexBundle("baz")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("baz"), createIndexBundle("foo")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffIndices_notEqual() {
+ EntityBundle bundle = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ EntityBundle other = new EntityBundle("foo", "sq",
+ Collections.<FieldBundle>emptyList(),
+ new PrimaryKeyBundle(false, asList("foo")),
+ asList(createIndexBundle("foo2")),
+ Collections.<ForeignKeyBundle>emptyList());
+
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ private FieldBundle createFieldBundle(String name) {
+ return new FieldBundle("foo", name, "text", false);
+ }
+
+ private IndexBundle createIndexBundle(String colName) {
+ return new IndexBundle("ind_" + colName, false,
+ asList(colName), "create");
+ }
+
+ private ForeignKeyBundle createForeignKeyBundle(String targetTable, String column) {
+ return new ForeignKeyBundle(targetTable, "CASCADE", "CASCADE",
+ asList(column), asList(column));
+ }
+}
diff --git a/android/arch/persistence/room/migration/bundle/FieldBundle.java b/android/arch/persistence/room/migration/bundle/FieldBundle.java
index eb73d814..5f740872 100644
--- a/android/arch/persistence/room/migration/bundle/FieldBundle.java
+++ b/android/arch/persistence/room/migration/bundle/FieldBundle.java
@@ -27,7 +27,7 @@ import com.google.gson.annotations.SerializedName;
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class FieldBundle {
+public class FieldBundle implements SchemaEquality<FieldBundle> {
@SerializedName("fieldPath")
private String mFieldPath;
@SerializedName("columnName")
@@ -59,4 +59,14 @@ public class FieldBundle {
public boolean isNonNull() {
return mNonNull;
}
+
+ @Override
+ public boolean isSchemaEqual(FieldBundle other) {
+ if (mNonNull != other.mNonNull) return false;
+ if (mColumnName != null ? !mColumnName.equals(other.mColumnName)
+ : other.mColumnName != null) {
+ return false;
+ }
+ return mAffinity != null ? mAffinity.equals(other.mAffinity) : other.mAffinity == null;
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/FieldBundleTest.java b/android/arch/persistence/room/migration/bundle/FieldBundleTest.java
new file mode 100644
index 00000000..eac44775
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/FieldBundleTest.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 android.arch.persistence.room.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+@RunWith(JUnit4.class)
+public class FieldBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo", "text", false);
+ assertThat(bundle.isSchemaEqual(copy), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffNonNull_notEqual() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo", "text", true);
+ assertThat(bundle.isSchemaEqual(copy), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumnName_notEqual() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo2", "text", true);
+ assertThat(bundle.isSchemaEqual(copy), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffAffinity_notEqual() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo", "foo2", "int", false);
+ assertThat(bundle.isSchemaEqual(copy), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffPath_equal() {
+ FieldBundle bundle = new FieldBundle("foo", "foo", "text", false);
+ FieldBundle copy = new FieldBundle("foo>bar", "foo", "text", false);
+ assertThat(bundle.isSchemaEqual(copy), is(true));
+ }
+}
diff --git a/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java b/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
index d72cf8cb..367dd746 100644
--- a/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
+++ b/android/arch/persistence/room/migration/bundle/ForeignKeyBundle.java
@@ -28,7 +28,7 @@ import java.util.List;
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class ForeignKeyBundle {
+public class ForeignKeyBundle implements SchemaEquality<ForeignKeyBundle> {
@SerializedName("table")
private String mTable;
@SerializedName("onDelete")
@@ -43,10 +43,10 @@ public class ForeignKeyBundle {
/**
* Creates a foreign key bundle with the given parameters.
*
- * @param table The target table
- * @param onDelete OnDelete action
- * @param onUpdate OnUpdate action
- * @param columns The list of columns in the current table
+ * @param table The target table
+ * @param onDelete OnDelete action
+ * @param onUpdate OnUpdate action
+ * @param columns The list of columns in the current table
* @param referencedColumns The list of columns in the referenced table
*/
public ForeignKeyBundle(String table, String onDelete, String onUpdate,
@@ -102,4 +102,18 @@ public class ForeignKeyBundle {
public List<String> getReferencedColumns() {
return mReferencedColumns;
}
+
+ @Override
+ public boolean isSchemaEqual(ForeignKeyBundle other) {
+ if (mTable != null ? !mTable.equals(other.mTable) : other.mTable != null) return false;
+ if (mOnDelete != null ? !mOnDelete.equals(other.mOnDelete) : other.mOnDelete != null) {
+ return false;
+ }
+ if (mOnUpdate != null ? !mOnUpdate.equals(other.mOnUpdate) : other.mOnUpdate != null) {
+ return false;
+ }
+ // order matters
+ return mColumns.equals(other.mColumns) && mReferencedColumns.equals(
+ other.mReferencedColumns);
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java b/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java
new file mode 100644
index 00000000..be1b81e9
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/ForeignKeyBundleTest.java
@@ -0,0 +1,95 @@
+/*
+ * 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.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class ForeignKeyBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffTable_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table2", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffOnDelete_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete2",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffOnUpdate_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate2", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffSrcOrder_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col2", "col1"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffTargetOrder_notEqual() {
+ ForeignKeyBundle bundle = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target1", "target2"));
+ ForeignKeyBundle other = new ForeignKeyBundle("table", "onDelete",
+ "onUpdate", Arrays.asList("col1", "col2"),
+ Arrays.asList("target2", "target1"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+}
diff --git a/android/arch/persistence/room/migration/bundle/IndexBundle.java b/android/arch/persistence/room/migration/bundle/IndexBundle.java
index ba406186..e991316e 100644
--- a/android/arch/persistence/room/migration/bundle/IndexBundle.java
+++ b/android/arch/persistence/room/migration/bundle/IndexBundle.java
@@ -28,7 +28,9 @@ import java.util.List;
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class IndexBundle {
+public class IndexBundle implements SchemaEquality<IndexBundle> {
+ // should match Index.kt
+ public static final String DEFAULT_PREFIX = "index_";
@SerializedName("name")
private String mName;
@SerializedName("unique")
@@ -65,4 +67,25 @@ public class IndexBundle {
public String create(String tableName) {
return BundleUtil.replaceTableName(mCreateSql, tableName);
}
+
+ @Override
+ public boolean isSchemaEqual(IndexBundle other) {
+ if (mUnique != other.mUnique) return false;
+ if (mName.startsWith(DEFAULT_PREFIX)) {
+ if (!other.mName.startsWith(DEFAULT_PREFIX)) {
+ return false;
+ }
+ } else if (other.mName.startsWith(DEFAULT_PREFIX)) {
+ return false;
+ } else if (!mName.equals(other.mName)) {
+ return false;
+ }
+
+ // order matters
+ if (mColumnNames != null ? !mColumnNames.equals(other.mColumnNames)
+ : other.mColumnNames != null) {
+ return false;
+ }
+ return true;
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/IndexBundleTest.java b/android/arch/persistence/room/migration/bundle/IndexBundleTest.java
new file mode 100644
index 00000000..aa7230f7
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/IndexBundleTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class IndexBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffName_notEqual() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index3", false,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffGenericName_equal() {
+ IndexBundle bundle = new IndexBundle(IndexBundle.DEFAULT_PREFIX + "x", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle(IndexBundle.DEFAULT_PREFIX + "y", false,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffUnique_notEqual() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", true,
+ Arrays.asList("col1", "col2"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumns_notEqual() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", false,
+ Arrays.asList("col2", "col1"), "sql");
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffSql_equal() {
+ IndexBundle bundle = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql");
+ IndexBundle other = new IndexBundle("index1", false,
+ Arrays.asList("col1", "col2"), "sql22");
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+}
diff --git a/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java b/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
index c16f9670..820aa7e5 100644
--- a/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
+++ b/android/arch/persistence/room/migration/bundle/PrimaryKeyBundle.java
@@ -28,7 +28,7 @@ import java.util.List;
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class PrimaryKeyBundle {
+public class PrimaryKeyBundle implements SchemaEquality<PrimaryKeyBundle> {
@SerializedName("columnNames")
private List<String> mColumnNames;
@SerializedName("autoGenerate")
@@ -46,4 +46,9 @@ public class PrimaryKeyBundle {
public boolean isAutoGenerate() {
return mAutoGenerate;
}
+
+ @Override
+ public boolean isSchemaEqual(PrimaryKeyBundle other) {
+ return mColumnNames.equals(other.mColumnNames) && mAutoGenerate == other.mAutoGenerate;
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java b/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java
new file mode 100644
index 00000000..3b9e4643
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/PrimaryKeyBundleTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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.migration.bundle;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.util.Arrays;
+
+@RunWith(JUnit4.class)
+public class PrimaryKeyBundleTest {
+ @Test
+ public void schemaEquality_same_equal() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ assertThat(bundle.isSchemaEqual(other), is(true));
+ }
+
+ @Test
+ public void schemaEquality_diffAutoGen_notEqual() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(false,
+ Arrays.asList("foo", "bar"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumns_notEqual() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "baz"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+
+ @Test
+ public void schemaEquality_diffColumnOrder_notEqual() {
+ PrimaryKeyBundle bundle = new PrimaryKeyBundle(true,
+ Arrays.asList("foo", "bar"));
+ PrimaryKeyBundle other = new PrimaryKeyBundle(true,
+ Arrays.asList("bar", "foo"));
+ assertThat(bundle.isSchemaEqual(other), is(false));
+ }
+}
diff --git a/android/arch/persistence/room/migration/bundle/SchemaBundle.java b/android/arch/persistence/room/migration/bundle/SchemaBundle.java
index d6171aa4..af35e6f1 100644
--- a/android/arch/persistence/room/migration/bundle/SchemaBundle.java
+++ b/android/arch/persistence/room/migration/bundle/SchemaBundle.java
@@ -37,7 +37,7 @@ import java.io.UnsupportedEncodingException;
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public class SchemaBundle {
+public class SchemaBundle implements SchemaEquality<SchemaBundle> {
@SerializedName("formatVersion")
private int mFormatVersion;
@@ -47,6 +47,7 @@ public class SchemaBundle {
private static final Gson GSON;
private static final String CHARSET = "UTF-8";
public static final int LATEST_FORMAT = 1;
+
static {
GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
}
@@ -104,4 +105,9 @@ public class SchemaBundle {
}
}
+ @Override
+ public boolean isSchemaEqual(SchemaBundle other) {
+ return SchemaEqualityUtil.checkSchemaEquality(mDatabase, other.mDatabase)
+ && mFormatVersion == other.mFormatVersion;
+ }
}
diff --git a/android/arch/persistence/room/migration/bundle/SchemaEquality.java b/android/arch/persistence/room/migration/bundle/SchemaEquality.java
new file mode 100644
index 00000000..59ea4b0d
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/SchemaEquality.java
@@ -0,0 +1,30 @@
+/*
+ * 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.migration.bundle;
+
+import android.support.annotation.RestrictTo;
+
+/**
+ * A loose equals check which checks schema equality instead of 100% equality (e.g. order of
+ * columns in an entity does not have to match)
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+interface SchemaEquality<T> {
+ boolean isSchemaEqual(T other);
+}
diff --git a/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java b/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java
new file mode 100644
index 00000000..65a75720
--- /dev/null
+++ b/android/arch/persistence/room/migration/bundle/SchemaEqualityUtil.java
@@ -0,0 +1,90 @@
+/*
+ * 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.migration.bundle;
+
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * utility class to run schema equality on collections.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class SchemaEqualityUtil {
+ static <T, K extends SchemaEquality<K>> boolean checkSchemaEquality(
+ @Nullable Map<T, K> map1, @Nullable Map<T, K> map2) {
+ if (map1 == null) {
+ return map2 == null;
+ }
+ if (map2 == null) {
+ return false;
+ }
+ if (map1.size() != map2.size()) {
+ return false;
+ }
+ for (Map.Entry<T, K> pair : map1.entrySet()) {
+ if (!checkSchemaEquality(pair.getValue(), map2.get(pair.getKey()))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ static <K extends SchemaEquality<K>> boolean checkSchemaEquality(
+ @Nullable List<K> list1, @Nullable List<K> list2) {
+ if (list1 == null) {
+ return list2 == null;
+ }
+ if (list2 == null) {
+ return false;
+ }
+ if (list1.size() != list2.size()) {
+ return false;
+ }
+ // we don't care this is n^2, small list + only used for testing.
+ for (K item1 : list1) {
+ // find matching item
+ boolean matched = false;
+ for (K item2 : list2) {
+ if (checkSchemaEquality(item1, item2)) {
+ matched = true;
+ break;
+ }
+ }
+ if (!matched) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @SuppressWarnings("SimplifiableIfStatement")
+ static <K extends SchemaEquality<K>> boolean checkSchemaEquality(
+ @Nullable K item1, @Nullable K item2) {
+ if (item1 == null) {
+ return item2 == null;
+ }
+ if (item2 == null) {
+ return false;
+ }
+ return item1.isSchemaEqual(item2);
+ }
+}
diff --git a/android/arch/persistence/room/paging/LimitOffsetDataSource.java b/android/arch/persistence/room/paging/LimitOffsetDataSource.java
index 2f9a8882..baa5b43c 100644
--- a/android/arch/persistence/room/paging/LimitOffsetDataSource.java
+++ b/android/arch/persistence/room/paging/LimitOffsetDataSource.java
@@ -16,7 +16,7 @@
package android.arch.persistence.room.paging;
-import android.arch.paging.TiledDataSource;
+import android.arch.paging.PositionalDataSource;
import android.arch.persistence.room.InvalidationTracker;
import android.arch.persistence.room.RoomDatabase;
import android.arch.persistence.room.RoomSQLiteQuery;
@@ -25,6 +25,7 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
@@ -42,7 +43,7 @@ import java.util.Set;
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-public abstract class LimitOffsetDataSource<T> extends TiledDataSource<T> {
+public abstract class LimitOffsetDataSource<T> extends PositionalDataSource<T> {
private final RoomSQLiteQuery mSourceQuery;
private final String mCountQuery;
private final String mLimitOffsetQuery;
@@ -67,7 +68,10 @@ public abstract class LimitOffsetDataSource<T> extends TiledDataSource<T> {
db.getInvalidationTracker().addWeakObserver(mObserver);
}
- @Override
+ /**
+ * Count number of rows query can return
+ */
+ @SuppressWarnings("WeakerAccess")
public int countItems() {
final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mCountQuery,
mSourceQuery.getArgCount());
@@ -93,8 +97,43 @@ public abstract class LimitOffsetDataSource<T> extends TiledDataSource<T> {
@SuppressWarnings("WeakerAccess")
protected abstract List<T> convertRows(Cursor cursor);
- @Nullable
@Override
+ public void loadInitial(@NonNull LoadInitialParams params,
+ @NonNull LoadInitialCallback<T> callback) {
+ int totalCount = countItems();
+ if (totalCount == 0) {
+ callback.onResult(Collections.<T>emptyList(), 0, 0);
+ return;
+ }
+
+ // bound the size requested, based on known count
+ final int firstLoadPosition = computeInitialLoadPosition(params, totalCount);
+ final int firstLoadSize = computeInitialLoadSize(params, firstLoadPosition, totalCount);
+
+ List<T> list = loadRange(firstLoadPosition, firstLoadSize);
+ if (list != null && list.size() == firstLoadSize) {
+ callback.onResult(list, firstLoadPosition, totalCount);
+ } else {
+ // null list, or size doesn't match request - DB modified between count and load
+ invalidate();
+ }
+ }
+
+ @Override
+ 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 {
+ invalidate();
+ }
+ }
+
+ /**
+ * Return the rows from startPos to startPos + loadCount
+ */
+ @Nullable
public List<T> loadRange(int startPosition, int loadCount) {
final RoomSQLiteQuery sqLiteQuery = RoomSQLiteQuery.acquire(mLimitOffsetQuery,
mSourceQuery.getArgCount() + 2);
diff --git a/android/arch/persistence/room/testing/MigrationTestHelper.java b/android/arch/persistence/room/testing/MigrationTestHelper.java
index 2e93bbe4..013dd379 100644
--- a/android/arch/persistence/room/testing/MigrationTestHelper.java
+++ b/android/arch/persistence/room/testing/MigrationTestHelper.java
@@ -143,9 +143,12 @@ public class MigrationTestHelper extends TestWatcher {
RoomDatabase.MigrationContainer container = new RoomDatabase.MigrationContainer();
DatabaseConfiguration configuration = new DatabaseConfiguration(
mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true,
- true);
+ true, Collections.<Integer>emptySet());
RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
new CreatingDelegate(schemaBundle.getDatabase()),
+ schemaBundle.getDatabase().getIdentityHash(),
+ // we pass the same hash twice since an old schema does not necessarily have
+ // a legacy hash and we would not even persist it.
schemaBundle.getDatabase().getIdentityHash());
return openDatabase(name, roomOpenHelper);
}
@@ -186,9 +189,12 @@ public class MigrationTestHelper extends TestWatcher {
container.addMigrations(migrations);
DatabaseConfiguration configuration = new DatabaseConfiguration(
mInstrumentation.getTargetContext(), name, mOpenFactory, container, null, true,
- true);
+ true, Collections.<Integer>emptySet());
RoomOpenHelper roomOpenHelper = new RoomOpenHelper(configuration,
new MigratingDelegate(schemaBundle.getDatabase(), validateDroppedTables),
+ // we pass the same hash twice since an old schema does not necessarily have
+ // a legacy hash and we would not even persist it.
+ schemaBundle.getDatabase().getIdentityHash(),
schemaBundle.getDatabase().getIdentityHash());
return openDatabase(name, roomOpenHelper);
}
diff --git a/android/arch/persistence/room/util/TableInfo.java b/android/arch/persistence/room/util/TableInfo.java
index a115147d..19d9853e 100644
--- a/android/arch/persistence/room/util/TableInfo.java
+++ b/android/arch/persistence/room/util/TableInfo.java
@@ -17,6 +17,7 @@
package android.arch.persistence.room.util;
import android.arch.persistence.db.SupportSQLiteDatabase;
+import android.arch.persistence.room.ColumnInfo;
import android.database.Cursor;
import android.os.Build;
import android.support.annotation.NonNull;
@@ -28,6 +29,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
@@ -44,7 +46,8 @@ import java.util.TreeMap;
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources"})
+@SuppressWarnings({"WeakerAccess", "unused", "TryFinallyCanBeTryWithResources",
+ "SimplifiableIfStatement"})
// if you change this class, you must change TableInfoWriter.kt
public class TableInfo {
/**
@@ -313,6 +316,14 @@ public class TableInfo {
*/
public final String type;
/**
+ * The column type after it is normalized to one of the basic types according to
+ * https://www.sqlite.org/datatype3.html Section 3.1.
+ * <p>
+ * This is the value Room uses for equality check.
+ */
+ @ColumnInfo.SQLiteTypeAffinity
+ public final int affinity;
+ /**
* Whether or not the column can be NULL.
*/
public final boolean notNull;
@@ -337,6 +348,40 @@ public class TableInfo {
this.type = type;
this.notNull = notNull;
this.primaryKeyPosition = primaryKeyPosition;
+ this.affinity = findAffinity(type);
+ }
+
+ /**
+ * Implements https://www.sqlite.org/datatype3.html section 3.1
+ *
+ * @param type The type that was given to the sqlite
+ * @return The normalized type which is one of the 5 known affinities
+ */
+ @ColumnInfo.SQLiteTypeAffinity
+ private static int findAffinity(@Nullable String type) {
+ if (type == null) {
+ return ColumnInfo.BLOB;
+ }
+ String uppercaseType = type.toUpperCase(Locale.US);
+ if (uppercaseType.contains("INT")) {
+ return ColumnInfo.INTEGER;
+ }
+ if (uppercaseType.contains("CHAR")
+ || uppercaseType.contains("CLOB")
+ || uppercaseType.contains("TEXT")) {
+ return ColumnInfo.TEXT;
+ }
+ if (uppercaseType.contains("BLOB")) {
+ return ColumnInfo.BLOB;
+ }
+ if (uppercaseType.contains("REAL")
+ || uppercaseType.contains("FLOA")
+ || uppercaseType.contains("DOUB")) {
+ return ColumnInfo.REAL;
+ }
+ // sqlite returns NUMERIC here but it is like a catch all. We already
+ // have UNDEFINED so it is better to use UNDEFINED for consistency.
+ return ColumnInfo.UNDEFINED;
}
@Override
@@ -354,7 +399,7 @@ public class TableInfo {
if (!name.equals(column.name)) return false;
//noinspection SimplifiableIfStatement
if (notNull != column.notNull) return false;
- return type != null ? type.equalsIgnoreCase(column.type) : column.type == null;
+ return affinity == column.affinity;
}
/**
@@ -369,7 +414,7 @@ public class TableInfo {
@Override
public int hashCode() {
int result = name.hashCode();
- result = 31 * result + (type != null ? type.hashCode() : 0);
+ result = 31 * result + affinity;
result = 31 * result + (notNull ? 1231 : 1237);
result = 31 * result + primaryKeyPosition;
return result;
@@ -380,6 +425,7 @@ public class TableInfo {
return "Column{"
+ "name='" + name + '\''
+ ", type='" + type + '\''
+ + ", affinity='" + affinity + '\''
+ ", notNull=" + notNull
+ ", primaryKeyPosition=" + primaryKeyPosition
+ '}';
@@ -472,7 +518,7 @@ public class TableInfo {
}
@Override
- public int compareTo(ForeignKeyWithSequence o) {
+ public int compareTo(@NonNull ForeignKeyWithSequence o) {
final int idCmp = mId - o.mId;
if (idCmp == 0) {
return mSequence - o.mSequence;
diff --git a/android/bluetooth/BluetoothA2dp.java b/android/bluetooth/BluetoothA2dp.java
index 7841b83c..35a21a4e 100644
--- a/android/bluetooth/BluetoothA2dp.java
+++ b/android/bluetooth/BluetoothA2dp.java
@@ -17,6 +17,7 @@
package android.bluetooth;
import android.Manifest;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
@@ -103,6 +104,24 @@ public final class BluetoothA2dp implements BluetoothProfile {
"android.bluetooth.a2dp.profile.action.AVRCP_CONNECTION_STATE_CHANGED";
/**
+ * Intent used to broadcast the selection of a connected device as active.
+ *
+ * <p>This intent will have one extra:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
+ * be null if no device is active. </li>
+ * </ul>
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
+ * receive.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ACTIVE_DEVICE_CHANGED =
+ "android.bluetooth.a2dp.profile.action.ACTIVE_DEVICE_CHANGED";
+
+ /**
* Intent used to broadcast the change in the Audio Codec state of the
* A2DP Source profile.
*
@@ -425,6 +444,75 @@ public final class BluetoothA2dp implements BluetoothProfile {
}
/**
+ * Select a connected device as active.
+ *
+ * The active device selection is per profile. An active device's
+ * purpose is profile-specific. For example, A2DP audio streaming
+ * is to the active A2DP Sink device. If a remote device is not
+ * connected, it cannot be selected as active.
+ *
+ * <p> This API returns false in scenarios like the profile on the
+ * device is not connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that the
+ * {@link #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
+ * with the active device.
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
+ * permission.
+ *
+ * @param device the remote Bluetooth device. Could be null to clear
+ * the active device and stop streaming audio to a Bluetooth device.
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ public boolean setActiveDevice(@Nullable BluetoothDevice device) {
+ if (DBG) log("setActiveDevice(" + device + ")");
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null && isEnabled()
+ && ((device == null) || isValidDevice(device))) {
+ return mService.setActiveDevice(device);
+ }
+ if (mService == null) Log.w(TAG, "Proxy not attached to service");
+ return false;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return false;
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Get the connected device that is active.
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH}
+ * permission.
+ *
+ * @return the connected device that is active or null if no device
+ * is active
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ @Nullable
+ public BluetoothDevice getActiveDevice() {
+ if (VDBG) log("getActiveDevice()");
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null && isEnabled()) {
+ return mService.getActiveDevice();
+ }
+ if (mService == null) Log.w(TAG, "Proxy not attached to service");
+ return null;
+ } catch (RemoteException e) {
+ Log.e(TAG, "Stack:" + Log.getStackTraceString(new Throwable()));
+ return null;
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ }
+
+ /**
* Set priority of the profile
*
* <p> The device should already be paired.
diff --git a/android/bluetooth/BluetoothAdapter.java b/android/bluetooth/BluetoothAdapter.java
index 3290d57f..9f11d6e9 100644
--- a/android/bluetooth/BluetoothAdapter.java
+++ b/android/bluetooth/BluetoothAdapter.java
@@ -79,8 +79,9 @@ import java.util.concurrent.locks.ReentrantReadWriteLock;
* {@link BluetoothDevice} objects representing all paired devices with
* {@link #getBondedDevices()}; start device discovery with
* {@link #startDiscovery()}; or create a {@link BluetoothServerSocket} to
- * listen for incoming connection requests with
- * {@link #listenUsingRfcommWithServiceRecord(String, UUID)}; or start a scan for
+ * listen for incoming RFComm connection requests with {@link
+ * #listenUsingRfcommWithServiceRecord(String, UUID)}; listen for incoming L2CAP Connection-oriented
+ * Channels (CoC) connection requests with listenUsingL2capCoc(int)}; or start a scan for
* Bluetooth LE devices with {@link #startLeScan(LeScanCallback callback)}.
* </p>
* <p>This class is thread safe.</p>
@@ -210,6 +211,14 @@ public final class BluetoothAdapter {
public static final int STATE_BLE_TURNING_OFF = 16;
/**
+ * UUID of the GATT Read Characteristics for LE_PSM value.
+ *
+ * @hide
+ */
+ public static final UUID LE_PSM_CHARACTERISTIC_UUID =
+ UUID.fromString("2d410339-82b6-42aa-b34e-e2e01df8cc1a");
+
+ /**
* Human-readable string helper for AdapterState
*
* @hide
@@ -1675,6 +1684,27 @@ public final class BluetoothAdapter {
}
/**
+ * Get the maximum number of connected audio devices.
+ *
+ * @return the maximum number of connected audio devices
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public int getMaxConnectedAudioDevices() {
+ try {
+ mServiceLock.readLock().lock();
+ if (mService != null) {
+ return mService.getMaxConnectedAudioDevices();
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "failed to get getMaxConnectedAudioDevices, error: ", e);
+ } finally {
+ mServiceLock.readLock().unlock();
+ }
+ return 1;
+ }
+
+ /**
* Return true if hardware has entries available for matching beacons
*
* @return true if there are hw entries available for matching beacons
@@ -2139,7 +2169,9 @@ public final class BluetoothAdapter {
min16DigitPin);
int errno = socket.mSocket.bindListen();
if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
- socket.setChannel(socket.mSocket.getPort());
+ int assignedChannel = socket.mSocket.getPort();
+ if (DBG) Log.d(TAG, "listenUsingL2capOn: set assigned channel to " + assignedChannel);
+ socket.setChannel(assignedChannel);
}
if (errno != 0) {
//TODO(BT): Throw the same exception error code
@@ -2180,12 +2212,18 @@ public final class BluetoothAdapter {
* @hide
*/
public BluetoothServerSocket listenUsingInsecureL2capOn(int port) throws IOException {
+ Log.d(TAG, "listenUsingInsecureL2capOn: port=" + port);
BluetoothServerSocket socket =
new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP, false, false, port, false,
- false);
+ false);
int errno = socket.mSocket.bindListen();
if (port == SOCKET_CHANNEL_AUTO_STATIC_NO_SDP) {
- socket.setChannel(socket.mSocket.getPort());
+ int assignedChannel = socket.mSocket.getPort();
+ if (DBG) {
+ Log.d(TAG, "listenUsingInsecureL2capOn: set assigned channel to "
+ + assignedChannel);
+ }
+ socket.setChannel(assignedChannel);
}
if (errno != 0) {
//TODO(BT): Throw the same exception error code
@@ -2744,4 +2782,103 @@ public final class BluetoothAdapter {
scanner.stopScan(scanCallback);
}
}
+
+ /**
+ * Create a secure L2CAP Connection-oriented Channel (CoC) {@link BluetoothServerSocket} and
+ * assign a dynamic protocol/service multiplexer (PSM) value. This socket can be used to listen
+ * for incoming connections.
+ * <p>A remote device connecting to this socket will be authenticated and communication on this
+ * socket will be encrypted.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming connections from a listening
+ * {@link BluetoothServerSocket}.
+ * <p>The system will assign a dynamic PSM value. This PSM value can be read from the {#link
+ * BluetoothServerSocket#getPsm()} and this value will be released when this server socket is
+ * closed, Bluetooth is turned off, or the application exits unexpectedly.
+ * <p>The mechanism of disclosing the assigned dynamic PSM value to the initiating peer is
+ * defined and performed by the application.
+ * <p>Use {@link BluetoothDevice#createL2capCocSocket(int, int)} to connect to this server
+ * socket from another Android device that is given the PSM value.
+ *
+ * @param transport Bluetooth transport to use, must be {@link BluetoothDevice#TRANSPORT_LE}
+ * @return an L2CAP CoC BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or unable to start this CoC
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public BluetoothServerSocket listenUsingL2capCoc(int transport)
+ throws IOException {
+ if (transport != BluetoothDevice.TRANSPORT_LE) {
+ throw new IllegalArgumentException("Unsupported transport: " + transport);
+ }
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP_LE, true, true,
+ SOCKET_CHANNEL_AUTO_STATIC_NO_SDP, false, false);
+ int errno = socket.mSocket.bindListen();
+ if (errno != 0) {
+ throw new IOException("Error: " + errno);
+ }
+
+ int assignedPsm = socket.mSocket.getPort();
+ if (assignedPsm == 0) {
+ throw new IOException("Error: Unable to assign PSM value");
+ }
+ if (DBG) {
+ Log.d(TAG, "listenUsingL2capCoc: set assigned PSM to "
+ + assignedPsm);
+ }
+ socket.setChannel(assignedPsm);
+
+ return socket;
+ }
+
+ /**
+ * Create an insecure L2CAP Connection-oriented Channel (CoC) {@link BluetoothServerSocket} and
+ * assign a dynamic PSM value. This socket can be used to listen for incoming connections.
+ * <p>The link key is not required to be authenticated, i.e the communication may be vulnerable
+ * to man-in-the-middle attacks. Use {@link #listenUsingL2capCoc}, if an encrypted and
+ * authenticated communication channel is desired.
+ * <p>Use {@link BluetoothServerSocket#accept} to retrieve incoming connections from a listening
+ * {@link BluetoothServerSocket}.
+ * <p>The system will assign a dynamic protocol/service multiplexer (PSM) value. This PSM value
+ * can be read from the {#link BluetoothServerSocket#getPsm()} and this value will be released
+ * when this server socket is closed, Bluetooth is turned off, or the application exits
+ * unexpectedly.
+ * <p>The mechanism of disclosing the assigned dynamic PSM value to the initiating peer is
+ * defined and performed by the application.
+ * <p>Use {@link BluetoothDevice#createInsecureL2capCocSocket(int, int)} to connect to this
+ * server socket from another Android device that is given the PSM value.
+ *
+ * @param transport Bluetooth transport to use, must be {@link BluetoothDevice#TRANSPORT_LE}
+ * @return an L2CAP CoC BluetoothServerSocket
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions, or unable to start this CoC
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public BluetoothServerSocket listenUsingInsecureL2capCoc(int transport)
+ throws IOException {
+ if (transport != BluetoothDevice.TRANSPORT_LE) {
+ throw new IllegalArgumentException("Unsupported transport: " + transport);
+ }
+ BluetoothServerSocket socket =
+ new BluetoothServerSocket(BluetoothSocket.TYPE_L2CAP_LE, false, false,
+ SOCKET_CHANNEL_AUTO_STATIC_NO_SDP, false, false);
+ int errno = socket.mSocket.bindListen();
+ if (errno != 0) {
+ throw new IOException("Error: " + errno);
+ }
+
+ int assignedPsm = socket.mSocket.getPort();
+ if (assignedPsm == 0) {
+ throw new IOException("Error: Unable to assign PSM value");
+ }
+ if (DBG) {
+ Log.d(TAG, "listenUsingInsecureL2capOn: set assigned PSM to "
+ + assignedPsm);
+ }
+ socket.setChannel(assignedPsm);
+
+ return socket;
+ }
}
diff --git a/android/bluetooth/BluetoothDevice.java b/android/bluetooth/BluetoothDevice.java
index ad7a93cd..ac21395c 100644
--- a/android/bluetooth/BluetoothDevice.java
+++ b/android/bluetooth/BluetoothDevice.java
@@ -618,6 +618,7 @@ public final class BluetoothDevice implements Parcelable {
*
* @hide
*/
+ @SystemApi
public static final int ACCESS_UNKNOWN = 0;
/**
@@ -626,6 +627,7 @@ public final class BluetoothDevice implements Parcelable {
*
* @hide
*/
+ @SystemApi
public static final int ACCESS_ALLOWED = 1;
/**
@@ -634,6 +636,7 @@ public final class BluetoothDevice implements Parcelable {
*
* @hide
*/
+ @SystemApi
public static final int ACCESS_REJECTED = 2;
/**
@@ -1918,4 +1921,75 @@ public final class BluetoothDevice implements Parcelable {
}
return null;
}
+
+ /**
+ * Create a Bluetooth L2CAP Connection-oriented Channel (CoC) {@link BluetoothSocket} that can
+ * be used to start a secure outgoing connection to the remote device with the same dynamic
+ * protocol/service multiplexer (PSM) value.
+ * <p>This is designed to be used with {@link BluetoothAdapter#listenUsingL2capCoc(int)} for
+ * peer-peer Bluetooth applications.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing connection.
+ * <p>Application using this API is responsible for obtaining PSM value from remote device.
+ * <p>The remote device will be authenticated and communication on this socket will be
+ * encrypted.
+ * <p> Use this socket if an authenticated socket link is possible. Authentication refers
+ * to the authentication of the link key to prevent man-in-the-middle type of attacks. When a
+ * secure socket connection is not possible, use {#link createInsecureLeL2capCocSocket(int,
+ * int)}.
+ *
+ * @param transport Bluetooth transport to use, must be {@link #TRANSPORT_LE}
+ * @param psm dynamic PSM value from remote device
+ * @return a CoC #BluetoothSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public BluetoothSocket createL2capCocSocket(int transport, int psm) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "createL2capCocSocket: Bluetooth is not enabled");
+ throw new IOException();
+ }
+ if (transport != BluetoothDevice.TRANSPORT_LE) {
+ throw new IllegalArgumentException("Unsupported transport: " + transport);
+ }
+ if (DBG) Log.d(TAG, "createL2capCocSocket: transport=" + transport + ", psm=" + psm);
+ return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP_LE, -1, true, true, this, psm,
+ null);
+ }
+
+ /**
+ * Create a Bluetooth L2CAP Connection-oriented Channel (CoC) {@link BluetoothSocket} that can
+ * be used to start a secure outgoing connection to the remote device with the same dynamic
+ * protocol/service multiplexer (PSM) value.
+ * <p>This is designed to be used with {@link BluetoothAdapter#listenUsingInsecureL2capCoc(int)}
+ * for peer-peer Bluetooth applications.
+ * <p>Use {@link BluetoothSocket#connect} to initiate the outgoing connection.
+ * <p>Application using this API is responsible for obtaining PSM value from remote device.
+ * <p> The communication channel may not have an authenticated link key, i.e. it may be subject
+ * to man-in-the-middle attacks. Use {@link #createL2capCocSocket(int, int)} if an encrypted and
+ * authenticated communication channel is possible.
+ *
+ * @param transport Bluetooth transport to use, must be {@link #TRANSPORT_LE}
+ * @param psm dynamic PSM value from remote device
+ * @return a CoC #BluetoothSocket ready for an outgoing connection
+ * @throws IOException on error, for example Bluetooth not available, or insufficient
+ * permissions
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.BLUETOOTH)
+ public BluetoothSocket createInsecureL2capCocSocket(int transport, int psm) throws IOException {
+ if (!isBluetoothEnabled()) {
+ Log.e(TAG, "createInsecureL2capCocSocket: Bluetooth is not enabled");
+ throw new IOException();
+ }
+ if (transport != BluetoothDevice.TRANSPORT_LE) {
+ throw new IllegalArgumentException("Unsupported transport: " + transport);
+ }
+ if (DBG) {
+ Log.d(TAG, "createInsecureL2capCocSocket: transport=" + transport + ", psm=" + psm);
+ }
+ return new BluetoothSocket(BluetoothSocket.TYPE_L2CAP_LE, -1, false, false, this, psm,
+ null);
+ }
}
diff --git a/android/bluetooth/BluetoothHeadset.java b/android/bluetooth/BluetoothHeadset.java
index 838d3153..a68f485f 100644
--- a/android/bluetooth/BluetoothHeadset.java
+++ b/android/bluetooth/BluetoothHeadset.java
@@ -16,6 +16,7 @@
package android.bluetooth;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
@@ -93,6 +94,23 @@ public final class BluetoothHeadset implements BluetoothProfile {
public static final String ACTION_AUDIO_STATE_CHANGED =
"android.bluetooth.headset.profile.action.AUDIO_STATE_CHANGED";
+ /**
+ * Intent used to broadcast the selection of a connected device as active.
+ *
+ * <p>This intent will have one extra:
+ * <ul>
+ * <li> {@link BluetoothDevice#EXTRA_DEVICE} - The remote device. It can
+ * be null if no device is active. </li>
+ * </ul>
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH} permission to
+ * receive.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_ACTIVE_DEVICE_CHANGED =
+ "android.bluetooth.headset.profile.action.ACTIVE_DEVICE_CHANGED";
/**
* Intent used to broadcast that the headset has posted a
@@ -538,8 +556,8 @@ public final class BluetoothHeadset implements BluetoothProfile {
* Set priority of the profile
*
* <p> The device should already be paired.
- * Priority can be one of {@link #PRIORITY_ON} or
- * {@link #PRIORITY_OFF},
+ * Priority can be one of {@link BluetoothProfile#PRIORITY_ON} or
+ * {@link BluetoothProfile#PRIORITY_OFF},
*
* <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
* permission.
@@ -983,9 +1001,105 @@ public final class BluetoothHeadset implements BluetoothProfile {
}
/**
- * check if in-band ringing is supported for this platform.
+ * Select a connected device as active.
+ *
+ * The active device selection is per profile. An active device's
+ * purpose is profile-specific. For example, in HFP and HSP profiles,
+ * it is the device used for phone call audio. If a remote device is not
+ * connected, it cannot be selected as active.
+ *
+ * <p> This API returns false in scenarios like the profile on the
+ * device is not connected or Bluetooth is not turned on.
+ * When this API returns true, it is guaranteed that the
+ * {@link #ACTION_ACTIVE_DEVICE_CHANGED} intent will be broadcasted
+ * with the active device.
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH_ADMIN}
+ * permission.
+ *
+ * @param device Remote Bluetooth Device, could be null if phone call audio should not be
+ * streamed to a headset
+ * @return false on immediate error, true otherwise
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH_ADMIN)
+ public boolean setActiveDevice(@Nullable BluetoothDevice device) {
+ if (DBG) {
+ Log.d(TAG, "setActiveDevice: " + device);
+ }
+ final IBluetoothHeadset service = mService;
+ if (service != null && isEnabled() && (device == null || isValidDevice(device))) {
+ try {
+ return service.setActiveDevice(device);
+ } catch (RemoteException e) {
+ Log.e(TAG, Log.getStackTraceString(new Throwable()));
+ }
+ }
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ return false;
+ }
+
+ /**
+ * Get the connected device that is active.
+ *
+ * <p>Requires {@link android.Manifest.permission#BLUETOOTH}
+ * permission.
+ *
+ * @return the connected device that is active or null if no device
+ * is active.
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH)
+ public BluetoothDevice getActiveDevice() {
+ if (VDBG) {
+ Log.d(TAG, "getActiveDevice");
+ }
+ final IBluetoothHeadset service = mService;
+ if (service != null && isEnabled()) {
+ try {
+ return service.getActiveDevice();
+ } catch (RemoteException e) {
+ Log.e(TAG, Log.getStackTraceString(new Throwable()));
+ }
+ }
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ return null;
+ }
+
+ /**
+ * Check if in-band ringing is currently enabled. In-band ringing could be disabled during an
+ * active connection.
+ *
+ * @return true if in-band ringing is enabled, false if in-band ringing is disabled
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.BLUETOOTH)
+ public boolean isInbandRingingEnabled() {
+ if (DBG) {
+ log("isInbandRingingEnabled()");
+ }
+ final IBluetoothHeadset service = mService;
+ if (service != null && isEnabled()) {
+ try {
+ return service.isInbandRingingEnabled();
+ } catch (RemoteException e) {
+ Log.e(TAG, Log.getStackTraceString(new Throwable()));
+ }
+ }
+ if (service == null) {
+ Log.w(TAG, "Proxy not attached to service");
+ }
+ 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
+ * @return true if in-band ringing is supported, false if in-band ringing is not supported
* @hide
*/
public static boolean isInbandRingingSupported(Context context) {
diff --git a/android/bluetooth/BluetoothHeadsetClientCall.java b/android/bluetooth/BluetoothHeadsetClientCall.java
index dc00d630..d46b2e37 100644
--- a/android/bluetooth/BluetoothHeadsetClientCall.java
+++ b/android/bluetooth/BluetoothHeadsetClientCall.java
@@ -73,17 +73,18 @@ public final class BluetoothHeadsetClientCall implements Parcelable {
private final boolean mOutgoing;
private final UUID mUUID;
private final long mCreationElapsedMilli;
+ private final boolean mInBandRing;
/**
* Creates BluetoothHeadsetClientCall instance.
*/
public BluetoothHeadsetClientCall(BluetoothDevice device, int id, int state, String number,
- boolean multiParty, boolean outgoing) {
- this(device, id, UUID.randomUUID(), state, number, multiParty, outgoing);
+ boolean multiParty, boolean outgoing, boolean inBandRing) {
+ this(device, id, UUID.randomUUID(), state, number, multiParty, outgoing, inBandRing);
}
public BluetoothHeadsetClientCall(BluetoothDevice device, int id, UUID uuid, int state,
- String number, boolean multiParty, boolean outgoing) {
+ String number, boolean multiParty, boolean outgoing, boolean inBandRing) {
mDevice = device;
mId = id;
mUUID = uuid;
@@ -91,6 +92,7 @@ public final class BluetoothHeadsetClientCall implements Parcelable {
mNumber = number != null ? number : "";
mMultiParty = multiParty;
mOutgoing = outgoing;
+ mInBandRing = inBandRing;
mCreationElapsedMilli = SystemClock.elapsedRealtime();
}
@@ -200,6 +202,16 @@ public final class BluetoothHeadsetClientCall implements Parcelable {
return mOutgoing;
}
+ /**
+ * Checks if the ringtone will be generated by the connected phone
+ *
+ * @return <code>true</code> if in band ring is enabled, <code>false</code> otherwise.
+ */
+ public boolean isInBandRing() {
+ return mInBandRing;
+ }
+
+
@Override
public String toString() {
return toString(false);
@@ -253,6 +265,8 @@ public final class BluetoothHeadsetClientCall implements Parcelable {
builder.append(mMultiParty);
builder.append(", mOutgoing: ");
builder.append(mOutgoing);
+ builder.append(", mInBandRing: ");
+ builder.append(mInBandRing);
builder.append("}");
return builder.toString();
}
@@ -266,7 +280,8 @@ public final class BluetoothHeadsetClientCall implements Parcelable {
public BluetoothHeadsetClientCall createFromParcel(Parcel in) {
return new BluetoothHeadsetClientCall((BluetoothDevice) in.readParcelable(null),
in.readInt(), UUID.fromString(in.readString()), in.readInt(),
- in.readString(), in.readInt() == 1, in.readInt() == 1);
+ in.readString(), in.readInt() == 1, in.readInt() == 1,
+ in.readInt() == 1);
}
@Override
@@ -284,6 +299,7 @@ public final class BluetoothHeadsetClientCall implements Parcelable {
out.writeString(mNumber);
out.writeInt(mMultiParty ? 1 : 0);
out.writeInt(mOutgoing ? 1 : 0);
+ out.writeInt(mInBandRing ? 1 : 0);
}
@Override
diff --git a/android/bluetooth/BluetoothProfile.java b/android/bluetooth/BluetoothProfile.java
index df2028a5..0e2263f7 100644
--- a/android/bluetooth/BluetoothProfile.java
+++ b/android/bluetooth/BluetoothProfile.java
@@ -19,6 +19,7 @@ package android.bluetooth;
import android.Manifest;
import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import java.util.List;
@@ -157,12 +158,19 @@ public interface BluetoothProfile {
public static final int HID_DEVICE = 19;
/**
+ * Object Push Profile (OPP)
+ *
+ * @hide
+ */
+ public static final int OPP = 20;
+
+ /**
* Max profile ID. This value should be updated whenever a new profile is added to match
* the largest value assigned to a profile.
*
* @hide
*/
- public static final int MAX_PROFILE_ID = 19;
+ public static final int MAX_PROFILE_ID = 20;
/**
* Default priority for devices that we try to auto-connect to and
@@ -178,6 +186,7 @@ public interface BluetoothProfile {
*
* @hide
**/
+ @SystemApi
public static final int PRIORITY_ON = 100;
/**
@@ -186,6 +195,7 @@ public interface BluetoothProfile {
*
* @hide
**/
+ @SystemApi
public static final int PRIORITY_OFF = 0;
/**
diff --git a/android/bluetooth/BluetoothServerSocket.java b/android/bluetooth/BluetoothServerSocket.java
index 58d090dc..ebb7f187 100644
--- a/android/bluetooth/BluetoothServerSocket.java
+++ b/android/bluetooth/BluetoothServerSocket.java
@@ -68,6 +68,7 @@ import java.io.IOException;
public final class BluetoothServerSocket implements Closeable {
private static final String TAG = "BluetoothServerSocket";
+ private static final boolean DBG = false;
/*package*/ final BluetoothSocket mSocket;
private Handler mHandler;
private int mMessage;
@@ -169,6 +170,7 @@ public final class BluetoothServerSocket implements Closeable {
* close any {@link BluetoothSocket} received from {@link #accept()}.
*/
public void close() throws IOException {
+ if (DBG) Log.d(TAG, "BluetoothServerSocket:close() called. mChannel=" + mChannel);
synchronized (this) {
if (mHandler != null) {
mHandler.obtainMessage(mMessage).sendToTarget();
@@ -197,6 +199,20 @@ public final class BluetoothServerSocket implements Closeable {
}
/**
+ * Returns the assigned dynamic protocol/service multiplexer (PSM) value for the listening L2CAP
+ * Connection-oriented Channel (CoC) server socket. This server socket must be returned by the
+ * {#link BluetoothAdapter.listenUsingL2capCoc(int)} or {#link
+ * BluetoothAdapter.listenUsingInsecureL2capCoc(int)}. The returned value is undefined if this
+ * method is called on non-L2CAP server sockets.
+ *
+ * @return the assigned PSM or LE_PSM value depending on transport
+ * @hide
+ */
+ public int getPsm() {
+ return mChannel;
+ }
+
+ /**
* Sets the channel on which future sockets are bound.
* Currently used only when a channel is auto generated.
*/
@@ -227,6 +243,10 @@ public final class BluetoothServerSocket implements Closeable {
sb.append("TYPE_L2CAP");
break;
}
+ case BluetoothSocket.TYPE_L2CAP_LE: {
+ sb.append("TYPE_L2CAP_LE");
+ break;
+ }
case BluetoothSocket.TYPE_SCO: {
sb.append("TYPE_SCO");
break;
diff --git a/android/bluetooth/BluetoothSocket.java b/android/bluetooth/BluetoothSocket.java
index 05699134..09f96840 100644
--- a/android/bluetooth/BluetoothSocket.java
+++ b/android/bluetooth/BluetoothSocket.java
@@ -99,6 +99,16 @@ public final class BluetoothSocket implements Closeable {
/** L2CAP socket */
public static final int TYPE_L2CAP = 3;
+ /** L2CAP socket on BR/EDR transport
+ * @hide
+ */
+ public static final int TYPE_L2CAP_BREDR = TYPE_L2CAP;
+
+ /** L2CAP socket on LE transport
+ * @hide
+ */
+ public static final int TYPE_L2CAP_LE = 4;
+
/*package*/ static final int EBADFD = 77;
/*package*/ static final int EADDRINUSE = 98;
@@ -417,6 +427,7 @@ public final class BluetoothSocket implements Closeable {
return -1;
}
try {
+ if (DBG) Log.d(TAG, "bindListen(): mPort=" + mPort + ", mType=" + mType);
mPfd = bluetoothProxy.getSocketManager().createSocketChannel(mType, mServiceName,
mUuid, mPort, getSecurityFlags());
} catch (RemoteException e) {
@@ -451,7 +462,7 @@ public final class BluetoothSocket implements Closeable {
mSocketState = SocketState.LISTENING;
}
}
- if (DBG) Log.d(TAG, "channel: " + channel);
+ if (DBG) Log.d(TAG, "bindListen(): channel=" + channel + ", mPort=" + mPort);
if (mPort <= -1) {
mPort = channel;
} // else ASSERT(mPort == channel)
@@ -515,7 +526,7 @@ public final class BluetoothSocket implements Closeable {
/*package*/ int read(byte[] b, int offset, int length) throws IOException {
int ret = 0;
if (VDBG) Log.d(TAG, "read in: " + mSocketIS + " len: " + length);
- if (mType == TYPE_L2CAP) {
+ if ((mType == TYPE_L2CAP) || (mType == TYPE_L2CAP_LE)) {
int bytesToRead = length;
if (VDBG) {
Log.v(TAG, "l2cap: read(): offset: " + offset + " length:" + length
@@ -558,7 +569,7 @@ public final class BluetoothSocket implements Closeable {
// Rfcomm uses dynamic allocation, and should not have any bindings
// to the actual message length.
if (VDBG) Log.d(TAG, "write: " + mSocketOS + " length: " + length);
- if (mType == TYPE_L2CAP) {
+ if ((mType == TYPE_L2CAP) || (mType == TYPE_L2CAP_LE)) {
if (length <= mMaxTxPacketSize) {
mSocketOS.write(b, offset, length);
} else {
@@ -702,7 +713,7 @@ public final class BluetoothSocket implements Closeable {
}
private void createL2capRxBuffer() {
- if (mType == TYPE_L2CAP) {
+ if ((mType == TYPE_L2CAP) || (mType == TYPE_L2CAP_LE)) {
// Allocate the buffer to use for reads.
if (VDBG) Log.v(TAG, " Creating mL2capBuffer: mMaxPacketSize: " + mMaxRxPacketSize);
mL2capBuffer = ByteBuffer.wrap(new byte[mMaxRxPacketSize]);
diff --git a/android/bluetooth/client/map/BluetoothMapBmessage.java b/android/bluetooth/client/map/BluetoothMapBmessage.java
deleted file mode 100644
index e06b0332..00000000
--- a/android/bluetooth/client/map/BluetoothMapBmessage.java
+++ /dev/null
@@ -1,170 +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.bluetooth.client.map;
-
-import com.android.vcard.VCardEntry;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.util.ArrayList;
-
-/**
- * Object representation of message in bMessage format
- * <p>
- * This object will be received in {@link BluetoothMasClient#EVENT_GET_MESSAGE}
- * callback message.
- */
-public class BluetoothMapBmessage {
-
- String mBmsgVersion;
- Status mBmsgStatus;
- Type mBmsgType;
- String mBmsgFolder;
-
- String mBbodyEncoding;
- String mBbodyCharset;
- String mBbodyLanguage;
- int mBbodyLength;
-
- String mMessage;
-
- ArrayList<VCardEntry> mOriginators;
- ArrayList<VCardEntry> mRecipients;
-
- public enum Status {
- READ, UNREAD
- }
-
- public enum Type {
- EMAIL, SMS_GSM, SMS_CDMA, MMS
- }
-
- /**
- * Constructs empty message object
- */
- public BluetoothMapBmessage() {
- mOriginators = new ArrayList<VCardEntry>();
- mRecipients = new ArrayList<VCardEntry>();
- }
-
- public VCardEntry getOriginator() {
- if (mOriginators.size() > 0) {
- return mOriginators.get(0);
- } else {
- return null;
- }
- }
-
- public ArrayList<VCardEntry> getOriginators() {
- return mOriginators;
- }
-
- public BluetoothMapBmessage addOriginator(VCardEntry vcard) {
- mOriginators.add(vcard);
- return this;
- }
-
- public ArrayList<VCardEntry> getRecipients() {
- return mRecipients;
- }
-
- public BluetoothMapBmessage addRecipient(VCardEntry vcard) {
- mRecipients.add(vcard);
- return this;
- }
-
- public Status getStatus() {
- return mBmsgStatus;
- }
-
- public BluetoothMapBmessage setStatus(Status status) {
- mBmsgStatus = status;
- return this;
- }
-
- public Type getType() {
- return mBmsgType;
- }
-
- public BluetoothMapBmessage setType(Type type) {
- mBmsgType = type;
- return this;
- }
-
- public String getFolder() {
- return mBmsgFolder;
- }
-
- public BluetoothMapBmessage setFolder(String folder) {
- mBmsgFolder = folder;
- return this;
- }
-
- public String getEncoding() {
- return mBbodyEncoding;
- }
-
- public BluetoothMapBmessage setEncoding(String encoding) {
- mBbodyEncoding = encoding;
- return this;
- }
-
- public String getCharset() {
- return mBbodyCharset;
- }
-
- public BluetoothMapBmessage setCharset(String charset) {
- mBbodyCharset = charset;
- return this;
- }
-
- public String getLanguage() {
- return mBbodyLanguage;
- }
-
- public BluetoothMapBmessage setLanguage(String language) {
- mBbodyLanguage = language;
- return this;
- }
-
- public String getBodyContent() {
- return mMessage;
- }
-
- public BluetoothMapBmessage setBodyContent(String body) {
- mMessage = body;
- return this;
- }
-
- @Override
- public String toString() {
- JSONObject json = new JSONObject();
-
- try {
- json.put("status", mBmsgStatus);
- json.put("type", mBmsgType);
- json.put("folder", mBmsgFolder);
- json.put("charset", mBbodyCharset);
- json.put("message", mMessage);
- } catch (JSONException e) {
- // do nothing
- }
-
- return json.toString();
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapBmessageBuilder.java b/android/bluetooth/client/map/BluetoothMapBmessageBuilder.java
deleted file mode 100644
index 8629423a..00000000
--- a/android/bluetooth/client/map/BluetoothMapBmessageBuilder.java
+++ /dev/null
@@ -1,160 +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.bluetooth.client.map;
-import com.android.vcard.VCardEntry;
-import com.android.vcard.VCardEntry.EmailData;
-import com.android.vcard.VCardEntry.NameData;
-import com.android.vcard.VCardEntry.PhoneData;
-
-import java.util.List;
-
-class BluetoothMapBmessageBuilder {
-
- private final static String CRLF = "\r\n";
-
- private final static String BMSG_BEGIN = "BEGIN:BMSG";
- private final static String BMSG_VERSION = "VERSION:1.0";
- private final static String BMSG_STATUS = "STATUS:";
- private final static String BMSG_TYPE = "TYPE:";
- private final static String BMSG_FOLDER = "FOLDER:";
- private final static String BMSG_END = "END:BMSG";
-
- private final static String BENV_BEGIN = "BEGIN:BENV";
- private final static String BENV_END = "END:BENV";
-
- private final static String BBODY_BEGIN = "BEGIN:BBODY";
- private final static String BBODY_ENCODING = "ENCODING:";
- private final static String BBODY_CHARSET = "CHARSET:";
- private final static String BBODY_LANGUAGE = "LANGUAGE:";
- private final static String BBODY_LENGTH = "LENGTH:";
- private final static String BBODY_END = "END:BBODY";
-
- private final static String MSG_BEGIN = "BEGIN:MSG";
- private final static String MSG_END = "END:MSG";
-
- private final static String VCARD_BEGIN = "BEGIN:VCARD";
- private final static String VCARD_VERSION = "VERSION:2.1";
- private final static String VCARD_N = "N:";
- private final static String VCARD_EMAIL = "EMAIL:";
- private final static String VCARD_TEL = "TEL:";
- private final static String VCARD_END = "END:VCARD";
-
- private final StringBuilder mBmsg;
-
- private BluetoothMapBmessageBuilder() {
- mBmsg = new StringBuilder();
- }
-
- static public String createBmessage(BluetoothMapBmessage bmsg) {
- BluetoothMapBmessageBuilder b = new BluetoothMapBmessageBuilder();
-
- b.build(bmsg);
-
- return b.mBmsg.toString();
- }
-
- private void build(BluetoothMapBmessage bmsg) {
- int bodyLen = MSG_BEGIN.length() + MSG_END.length() + 3 * CRLF.length()
- + bmsg.mMessage.getBytes().length;
-
- mBmsg.append(BMSG_BEGIN).append(CRLF);
-
- mBmsg.append(BMSG_VERSION).append(CRLF);
- mBmsg.append(BMSG_STATUS).append(bmsg.mBmsgStatus).append(CRLF);
- mBmsg.append(BMSG_TYPE).append(bmsg.mBmsgType).append(CRLF);
- mBmsg.append(BMSG_FOLDER).append(bmsg.mBmsgFolder).append(CRLF);
-
- for (VCardEntry vcard : bmsg.mOriginators) {
- buildVcard(vcard);
- }
-
- {
- mBmsg.append(BENV_BEGIN).append(CRLF);
-
- for (VCardEntry vcard : bmsg.mRecipients) {
- buildVcard(vcard);
- }
-
- {
- mBmsg.append(BBODY_BEGIN).append(CRLF);
-
- if (bmsg.mBbodyEncoding != null) {
- mBmsg.append(BBODY_ENCODING).append(bmsg.mBbodyEncoding).append(CRLF);
- }
-
- if (bmsg.mBbodyCharset != null) {
- mBmsg.append(BBODY_CHARSET).append(bmsg.mBbodyCharset).append(CRLF);
- }
-
- if (bmsg.mBbodyLanguage != null) {
- mBmsg.append(BBODY_LANGUAGE).append(bmsg.mBbodyLanguage).append(CRLF);
- }
-
- mBmsg.append(BBODY_LENGTH).append(bodyLen).append(CRLF);
-
- {
- mBmsg.append(MSG_BEGIN).append(CRLF);
-
- mBmsg.append(bmsg.mMessage).append(CRLF);
-
- mBmsg.append(MSG_END).append(CRLF);
- }
-
- mBmsg.append(BBODY_END).append(CRLF);
- }
-
- mBmsg.append(BENV_END).append(CRLF);
- }
-
- mBmsg.append(BMSG_END).append(CRLF);
- }
-
- private void buildVcard(VCardEntry vcard) {
- String n = buildVcardN(vcard);
- List<PhoneData> tel = vcard.getPhoneList();
- List<EmailData> email = vcard.getEmailList();
-
- mBmsg.append(VCARD_BEGIN).append(CRLF);
-
- mBmsg.append(VCARD_VERSION).append(CRLF);
-
- mBmsg.append(VCARD_N).append(n).append(CRLF);
-
- if (tel != null && tel.size() > 0) {
- mBmsg.append(VCARD_TEL).append(tel.get(0).getNumber()).append(CRLF);
- }
-
- if (email != null && email.size() > 0) {
- mBmsg.append(VCARD_EMAIL).append(email.get(0).getAddress()).append(CRLF);
- }
-
- mBmsg.append(VCARD_END).append(CRLF);
- }
-
- private String buildVcardN(VCardEntry vcard) {
- NameData nd = vcard.getNameData();
- StringBuilder sb = new StringBuilder();
-
- sb.append(nd.getFamily()).append(";");
- sb.append(nd.getGiven() == null ? "" : nd.getGiven()).append(";");
- sb.append(nd.getMiddle() == null ? "" : nd.getMiddle()).append(";");
- sb.append(nd.getPrefix() == null ? "" : nd.getPrefix()).append(";");
- sb.append(nd.getSuffix() == null ? "" : nd.getSuffix());
-
- return sb.toString();
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapBmessageParser.java b/android/bluetooth/client/map/BluetoothMapBmessageParser.java
deleted file mode 100644
index ea9bc7f8..00000000
--- a/android/bluetooth/client/map/BluetoothMapBmessageParser.java
+++ /dev/null
@@ -1,459 +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.bluetooth.client.map;
-
-import android.util.Log;
-
-import com.android.vcard.VCardEntry;
-import com.android.vcard.VCardEntryConstructor;
-import com.android.vcard.VCardEntryHandler;
-import com.android.vcard.VCardParser;
-import com.android.vcard.VCardParser_V21;
-import com.android.vcard.VCardParser_V30;
-import com.android.vcard.exception.VCardException;
-import com.android.vcard.exception.VCardVersionException;
-import android.bluetooth.client.map.BluetoothMapBmessage.Status;
-import android.bluetooth.client.map.BluetoothMapBmessage.Type;
-import android.bluetooth.client.map.utils.BmsgTokenizer;
-import android.bluetooth.client.map.utils.BmsgTokenizer.Property;
-
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.text.ParseException;
-
-class BluetoothMapBmessageParser {
-
- private final static String TAG = "BluetoothMapBmessageParser";
- private final static boolean DBG = false;
-
- private final static String CRLF = "\r\n";
-
- private final static Property BEGIN_BMSG = new Property("BEGIN", "BMSG");
- private final static Property END_BMSG = new Property("END", "BMSG");
-
- private final static Property BEGIN_VCARD = new Property("BEGIN", "VCARD");
- private final static Property END_VCARD = new Property("END", "VCARD");
-
- private final static Property BEGIN_BENV = new Property("BEGIN", "BENV");
- private final static Property END_BENV = new Property("END", "BENV");
-
- private final static Property BEGIN_BBODY = new Property("BEGIN", "BBODY");
- private final static Property END_BBODY = new Property("END", "BBODY");
-
- private final static Property BEGIN_MSG = new Property("BEGIN", "MSG");
- private final static Property END_MSG = new Property("END", "MSG");
-
- private final static int CRLF_LEN = 2;
-
- /*
- * length of "container" for 'message' in bmessage-body-content:
- * BEGIN:MSG<CRLF> + <CRLF> + END:MSG<CRFL>
- */
- private final static int MSG_CONTAINER_LEN = 22;
-
- private BmsgTokenizer mParser;
-
- private final BluetoothMapBmessage mBmsg;
-
- private BluetoothMapBmessageParser() {
- mBmsg = new BluetoothMapBmessage();
- }
-
- static public BluetoothMapBmessage createBmessage(String str) {
- BluetoothMapBmessageParser p = new BluetoothMapBmessageParser();
-
- if (DBG) {
- Log.d(TAG, "actual wired contents: " + str);
- }
-
- try {
- p.parse(str);
- } catch (IOException e) {
- Log.e(TAG, "I/O exception when parsing bMessage", e);
- return null;
- } catch (ParseException e) {
- Log.e(TAG, "Cannot parse bMessage", e);
- return null;
- }
-
- return p.mBmsg;
- }
-
- private ParseException expected(Property... props) {
- boolean first = true;
- StringBuilder sb = new StringBuilder();
-
- for (Property prop : props) {
- if (!first) {
- sb.append(" or ");
- }
- sb.append(prop);
- first = false;
- }
-
- return new ParseException("Expected: " + sb.toString(), mParser.pos());
- }
-
- private void parse(String str) throws IOException, ParseException {
-
- Property prop;
-
- /*
- * <bmessage-object>::= { "BEGIN:BMSG" <CRLF> <bmessage-property>
- * [<bmessage-originator>]* <bmessage-envelope> "END:BMSG" <CRLF> }
- */
-
- mParser = new BmsgTokenizer(str + CRLF);
-
- prop = mParser.next();
- if (!prop.equals(BEGIN_BMSG)) {
- throw expected(BEGIN_BMSG);
- }
-
- prop = parseProperties();
-
- while (prop.equals(BEGIN_VCARD)) {
-
- /* <bmessage-originator>::= <vcard> <CRLF> */
-
- StringBuilder vcard = new StringBuilder();
- prop = extractVcard(vcard);
-
- VCardEntry entry = parseVcard(vcard.toString());
- mBmsg.mOriginators.add(entry);
- }
-
- if (!prop.equals(BEGIN_BENV)) {
- throw expected(BEGIN_BENV);
- }
-
- prop = parseEnvelope(1);
-
- if (!prop.equals(END_BMSG)) {
- throw expected(END_BENV);
- }
-
- /*
- * there should be no meaningful data left in stream here so we just
- * ignore whatever is left
- */
-
- mParser = null;
- }
-
- private Property parseProperties() throws ParseException {
-
- Property prop;
-
- /*
- * <bmessage-property>::=<bmessage-version-property>
- * <bmessage-readstatus-property> <bmessage-type-property>
- * <bmessage-folder-property> <bmessage-version-property>::="VERSION:"
- * <common-digit>*"."<common-digit>* <CRLF>
- * <bmessage-readstatus-property>::="STATUS:" 'readstatus' <CRLF>
- * <bmessage-type-property>::="TYPE:" 'type' <CRLF>
- * <bmessage-folder-property>::="FOLDER:" 'foldername' <CRLF>
- */
-
- do {
- prop = mParser.next();
-
- if (prop.name.equals("VERSION")) {
- mBmsg.mBmsgVersion = prop.value;
-
- } else if (prop.name.equals("STATUS")) {
- for (Status s : Status.values()) {
- if (prop.value.equals(s.toString())) {
- mBmsg.mBmsgStatus = s;
- break;
- }
- }
-
- } else if (prop.name.equals("TYPE")) {
- for (Type t : Type.values()) {
- if (prop.value.equals(t.toString())) {
- mBmsg.mBmsgType = t;
- break;
- }
- }
-
- } else if (prop.name.equals("FOLDER")) {
- mBmsg.mBmsgFolder = prop.value;
-
- }
-
- } while (!prop.equals(BEGIN_VCARD) && !prop.equals(BEGIN_BENV));
-
- return prop;
- }
-
- private Property parseEnvelope(int level) throws IOException, ParseException {
-
- Property prop;
-
- /*
- * we can support as many nesting level as we want, but MAP spec clearly
- * defines that there should be no more than 3 levels. so we verify it
- * here.
- */
-
- if (level > 3) {
- throw new ParseException("bEnvelope is nested more than 3 times", mParser.pos());
- }
-
- /*
- * <bmessage-envelope> ::= { "BEGIN:BENV" <CRLF> [<bmessage-recipient>]*
- * <bmessage-envelope> | <bmessage-content> "END:BENV" <CRLF> }
- */
-
- prop = mParser.next();
-
- while (prop.equals(BEGIN_VCARD)) {
-
- /* <bmessage-originator>::= <vcard> <CRLF> */
-
- StringBuilder vcard = new StringBuilder();
- prop = extractVcard(vcard);
-
- if (level == 1) {
- VCardEntry entry = parseVcard(vcard.toString());
- mBmsg.mRecipients.add(entry);
- }
- }
-
- if (prop.equals(BEGIN_BENV)) {
- prop = parseEnvelope(level + 1);
-
- } else if (prop.equals(BEGIN_BBODY)) {
- prop = parseBody();
-
- } else {
- throw expected(BEGIN_BENV, BEGIN_BBODY);
- }
-
- if (!prop.equals(END_BENV)) {
- throw expected(END_BENV);
- }
-
- return mParser.next();
- }
-
- private Property parseBody() throws IOException, ParseException {
-
- Property prop;
-
- /*
- * <bmessage-content>::= { "BEGIN:BBODY"<CRLF> [<bmessage-body-part-ID>
- * <CRLF>] <bmessage-body-property> <bmessage-body-content>* <CRLF>
- * "END:BBODY"<CRLF> } <bmessage-body-part-ID>::="PARTID:" 'Part-ID'
- * <bmessage-body-property>::=[<bmessage-body-encoding-property>]
- * [<bmessage-body-charset-property>]
- * [<bmessage-body-language-property>]
- * <bmessage-body-content-length-property>
- * <bmessage-body-encoding-property>::="ENCODING:"'encoding' <CRLF>
- * <bmessage-body-charset-property>::="CHARSET:"'charset' <CRLF>
- * <bmessage-body-language-property>::="LANGUAGE:"'language' <CRLF>
- * <bmessage-body-content-length-property>::= "LENGTH:" <common-digit>*
- * <CRLF>
- */
-
- do {
- prop = mParser.next();
-
- if (prop.name.equals("PARTID")) {
- } else if (prop.name.equals("ENCODING")) {
- mBmsg.mBbodyEncoding = prop.value;
-
- } else if (prop.name.equals("CHARSET")) {
- mBmsg.mBbodyCharset = prop.value;
-
- } else if (prop.name.equals("LANGUAGE")) {
- mBmsg.mBbodyLanguage = prop.value;
-
- } else if (prop.name.equals("LENGTH")) {
- try {
- mBmsg.mBbodyLength = Integer.parseInt(prop.value);
- } catch (NumberFormatException e) {
- throw new ParseException("Invalid LENGTH value", mParser.pos());
- }
-
- }
-
- } while (!prop.equals(BEGIN_MSG));
-
- /*
- * check that the charset is always set to UTF-8. We expect only text transfer (in lieu with
- * the MAPv12 specifying only RFC2822 (text only) for MMS/EMAIL and SMS do not support
- * non-text content. If the charset is not set to UTF-8, it is safe to set the message as
- * empty. We force the getMessage (see BluetoothMasClient) to only call getMessage with
- * UTF-8 as the MCE is not obliged to support native charset.
- */
- if (!mBmsg.mBbodyCharset.equals("UTF-8")) {
- Log.e(TAG, "The charset was not set to charset UTF-8: " + mBmsg.mBbodyCharset);
- }
-
- /*
- * <bmessage-body-content>::={ "BEGIN:MSG"<CRLF> 'message'<CRLF>
- * "END:MSG"<CRLF> }
- */
-
- int messageLen = mBmsg.mBbodyLength - MSG_CONTAINER_LEN;
- int offset = messageLen + CRLF_LEN;
- int restartPos = mParser.pos() + offset;
-
- /*
- * length is specified in bytes so we need to convert from unicode
- * string back to bytes array
- */
-
- String remng = mParser.remaining();
- byte[] data = remng.getBytes();
-
- /* restart parsing from after 'message'<CRLF> */
- mParser = new BmsgTokenizer(new String(data, offset, data.length - offset), restartPos);
-
- prop = mParser.next(true);
-
- if (prop != null) {
- if (prop.equals(END_MSG)) {
- if (mBmsg.mBbodyCharset.equals("UTF-8")) {
- mBmsg.mMessage = new String(data, 0, messageLen, StandardCharsets.UTF_8);
- } else {
- mBmsg.mMessage = null;
- }
- } else {
- /* Handle possible exception for incorrect LENGTH value
- * from MSE while parsing GET Message response */
- Log.e(TAG, "Prop Invalid: "+ prop.toString());
- Log.e(TAG, "Possible Invalid LENGTH value");
- throw expected(END_MSG);
- }
- } else {
-
- data = null;
-
- /*
- * now we check if bMessage can be parsed if LENGTH is handled as
- * number of characters instead of number of bytes
- */
- if (offset < 0 || offset > remng.length()) {
- /* Handle possible exception for incorrect LENGTH value
- * from MSE while parsing GET Message response */
- throw new ParseException("Invalid LENGTH value", mParser.pos());
- }
-
- Log.w(TAG, "byte LENGTH seems to be invalid, trying with char length");
-
- mParser = new BmsgTokenizer(remng.substring(offset));
-
- prop = mParser.next();
-
- if (!prop.equals(END_MSG)) {
- throw expected(END_MSG);
- }
-
- if (mBmsg.mBbodyCharset.equals("UTF-8")) {
- mBmsg.mMessage = remng.substring(0, messageLen);
- } else {
- mBmsg.mMessage = null;
- }
- }
-
- prop = mParser.next();
-
- if (!prop.equals(END_BBODY)) {
- throw expected(END_BBODY);
- }
-
- return mParser.next();
- }
-
- private Property extractVcard(StringBuilder out) throws IOException, ParseException {
- Property prop;
-
- out.append(BEGIN_VCARD).append(CRLF);
-
- do {
- prop = mParser.next();
- out.append(prop).append(CRLF);
- } while (!prop.equals(END_VCARD));
-
- return mParser.next();
- }
-
- private class VcardHandler implements VCardEntryHandler {
-
- VCardEntry vcard;
-
- @Override
- public void onStart() {
- }
-
- @Override
- public void onEntryCreated(VCardEntry entry) {
- vcard = entry;
- }
-
- @Override
- public void onEnd() {
- }
- };
-
- private VCardEntry parseVcard(String str) throws IOException, ParseException {
- VCardEntry vcard = null;
-
- try {
- VCardParser p = new VCardParser_V21();
- VCardEntryConstructor c = new VCardEntryConstructor();
- VcardHandler handler = new VcardHandler();
- c.addEntryHandler(handler);
- p.addInterpreter(c);
- p.parse(new ByteArrayInputStream(str.getBytes()));
-
- vcard = handler.vcard;
-
- } catch (VCardVersionException e1) {
-
- try {
- VCardParser p = new VCardParser_V30();
- VCardEntryConstructor c = new VCardEntryConstructor();
- VcardHandler handler = new VcardHandler();
- c.addEntryHandler(handler);
- p.addInterpreter(c);
- p.parse(new ByteArrayInputStream(str.getBytes()));
-
- vcard = handler.vcard;
-
- } catch (VCardVersionException e2) {
- // will throw below
- } catch (VCardException e2) {
- // will throw below
- }
-
- } catch (VCardException e1) {
- // will throw below
- }
-
- if (vcard == null) {
- throw new ParseException("Cannot parse vCard object (neither 2.1 nor 3.0?)",
- mParser.pos());
- }
-
- return vcard;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapEventReport.java b/android/bluetooth/client/map/BluetoothMapEventReport.java
deleted file mode 100644
index 5963db45..00000000
--- a/android/bluetooth/client/map/BluetoothMapEventReport.java
+++ /dev/null
@@ -1,223 +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.bluetooth.client.map;
-import android.util.Log;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlPullParserFactory;
-
-import java.io.DataInputStream;
-import java.io.IOException;
-import java.math.BigInteger;
-import java.util.HashMap;
-
-/**
- * Object representation of event report received by MNS
- * <p>
- * This object will be received in {@link BluetoothMasClient#EVENT_EVENT_REPORT}
- * callback message.
- */
-public class BluetoothMapEventReport {
-
- private final static String TAG = "BluetoothMapEventReport";
-
- public enum Type {
- NEW_MESSAGE("NewMessage"), DELIVERY_SUCCESS("DeliverySuccess"),
- SENDING_SUCCESS("SendingSuccess"), DELIVERY_FAILURE("DeliveryFailure"),
- SENDING_FAILURE("SendingFailure"), MEMORY_FULL("MemoryFull"),
- MEMORY_AVAILABLE("MemoryAvailable"), MESSAGE_DELETED("MessageDeleted"),
- MESSAGE_SHIFT("MessageShift");
-
- private final String mSpecName;
-
- private Type(String specName) {
- mSpecName = specName;
- }
-
- @Override
- public String toString() {
- return mSpecName;
- }
- }
-
- private final Type mType;
-
- private final String mHandle;
-
- private final String mFolder;
-
- private final String mOldFolder;
-
- private final BluetoothMapBmessage.Type mMsgType;
-
- private BluetoothMapEventReport(HashMap<String, String> attrs) throws IllegalArgumentException {
- mType = parseType(attrs.get("type"));
-
- if (mType != Type.MEMORY_FULL && mType != Type.MEMORY_AVAILABLE) {
- String handle = attrs.get("handle");
- try {
- /* just to validate */
- new BigInteger(attrs.get("handle"), 16);
-
- mHandle = attrs.get("handle");
- } catch (NumberFormatException e) {
- throw new IllegalArgumentException("Invalid value for handle:" + handle);
- }
- } else {
- mHandle = null;
- }
-
- mFolder = attrs.get("folder");
-
- mOldFolder = attrs.get("old_folder");
-
- if (mType != Type.MEMORY_FULL && mType != Type.MEMORY_AVAILABLE) {
- String s = attrs.get("msg_type");
-
- if ("".equals(s)) {
- // Some phones (e.g. SGS3 for MessageDeleted) send empty
- // msg_type, in such case leave it as null rather than throw
- // parse exception
- mMsgType = null;
- } else {
- mMsgType = parseMsgType(s);
- }
- } else {
- mMsgType = null;
- }
- }
-
- private Type parseType(String type) throws IllegalArgumentException {
- for (Type t : Type.values()) {
- if (t.toString().equals(type)) {
- return t;
- }
- }
-
- throw new IllegalArgumentException("Invalid value for type: " + type);
- }
-
- private BluetoothMapBmessage.Type parseMsgType(String msgType) throws IllegalArgumentException {
- for (BluetoothMapBmessage.Type t : BluetoothMapBmessage.Type.values()) {
- if (t.name().equals(msgType)) {
- return t;
- }
- }
-
- throw new IllegalArgumentException("Invalid value for msg_type: " + msgType);
- }
-
- /**
- * @return {@link BluetoothMapEventReport.Type} object corresponding to
- * <code>type</code> application parameter in MAP specification
- */
- public Type getType() {
- return mType;
- }
-
- /**
- * @return value corresponding to <code>handle</code> parameter in MAP
- * specification
- */
- public String getHandle() {
- return mHandle;
- }
-
- /**
- * @return value corresponding to <code>folder</code> parameter in MAP
- * specification
- */
- public String getFolder() {
- return mFolder;
- }
-
- /**
- * @return value corresponding to <code>old_folder</code> parameter in MAP
- * specification
- */
- public String getOldFolder() {
- return mOldFolder;
- }
-
- /**
- * @return {@link BluetoothMapBmessage.Type} object corresponding to
- * <code>msg_type</code> application parameter in MAP specification
- */
- public BluetoothMapBmessage.Type getMsgType() {
- return mMsgType;
- }
-
- @Override
- public String toString() {
- JSONObject json = new JSONObject();
-
- try {
- json.put("type", mType);
- json.put("handle", mHandle);
- json.put("folder", mFolder);
- json.put("old_folder", mOldFolder);
- json.put("msg_type", mMsgType);
- } catch (JSONException e) {
- // do nothing
- }
-
- return json.toString();
- }
-
- static BluetoothMapEventReport fromStream(DataInputStream in) {
- BluetoothMapEventReport ev = null;
-
- try {
- XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
- xpp.setInput(in, "utf-8");
-
- int event = xpp.getEventType();
- while (event != XmlPullParser.END_DOCUMENT) {
- switch (event) {
- case XmlPullParser.START_TAG:
- if (xpp.getName().equals("event")) {
- HashMap<String, String> attrs = new HashMap<String, String>();
-
- for (int i = 0; i < xpp.getAttributeCount(); i++) {
- attrs.put(xpp.getAttributeName(i), xpp.getAttributeValue(i));
- }
-
- ev = new BluetoothMapEventReport(attrs);
-
- // return immediately, only one event should be here
- return ev;
- }
- break;
- }
-
- event = xpp.next();
- }
-
- } catch (XmlPullParserException e) {
- Log.e(TAG, "XML parser error when parsing XML", e);
- } catch (IOException e) {
- Log.e(TAG, "I/O error when parsing XML", e);
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Invalid event received", e);
- }
-
- return ev;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapFolderListing.java b/android/bluetooth/client/map/BluetoothMapFolderListing.java
deleted file mode 100644
index f0494b3c..00000000
--- a/android/bluetooth/client/map/BluetoothMapFolderListing.java
+++ /dev/null
@@ -1,69 +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.bluetooth.client.map;
-import android.util.Log;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlPullParserFactory;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-
-class BluetoothMapFolderListing {
-
- private static final String TAG = "BluetoothMasFolderListing";
-
- private final ArrayList<String> mFolders;
-
- public BluetoothMapFolderListing(InputStream in) {
- mFolders = new ArrayList<String>();
-
- parse(in);
- }
-
- public void parse(InputStream in) {
-
- try {
- XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
- xpp.setInput(in, "utf-8");
-
- int event = xpp.getEventType();
- while (event != XmlPullParser.END_DOCUMENT) {
- switch (event) {
- case XmlPullParser.START_TAG:
- if (xpp.getName().equals("folder")) {
- mFolders.add(xpp.getAttributeValue(null, "name"));
- }
- break;
- }
-
- event = xpp.next();
- }
-
- } catch (XmlPullParserException e) {
- Log.e(TAG, "XML parser error when parsing XML", e);
- } catch (IOException e) {
- Log.e(TAG, "I/O error when parsing XML", e);
- }
- }
-
- public ArrayList<String> getList() {
- return mFolders;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapMessage.java b/android/bluetooth/client/map/BluetoothMapMessage.java
deleted file mode 100644
index 5ce6c4be..00000000
--- a/android/bluetooth/client/map/BluetoothMapMessage.java
+++ /dev/null
@@ -1,338 +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.bluetooth.client.map;
-import android.bluetooth.client.map.utils.ObexTime;
-
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.math.BigInteger;
-import java.util.Date;
-import java.util.HashMap;
-
-/**
- * Object representation of message received in messages listing
- * <p>
- * This object will be received in
- * {@link BluetoothMasClient#EVENT_GET_MESSAGES_LISTING} callback message.
- */
-public class BluetoothMapMessage {
-
- private final String mHandle;
-
- private final String mSubject;
-
- private final Date mDateTime;
-
- private final String mSenderName;
-
- private final String mSenderAddressing;
-
- private final String mReplytoAddressing;
-
- private final String mRecipientName;
-
- private final String mRecipientAddressing;
-
- private final Type mType;
-
- private final int mSize;
-
- private final boolean mText;
-
- private final ReceptionStatus mReceptionStatus;
-
- private final int mAttachmentSize;
-
- private final boolean mPriority;
-
- private final boolean mRead;
-
- private final boolean mSent;
-
- private final boolean mProtected;
-
- public enum Type {
- UNKNOWN, EMAIL, SMS_GSM, SMS_CDMA, MMS
- };
-
- public enum ReceptionStatus {
- UNKNOWN, COMPLETE, FRACTIONED, NOTIFICATION
- }
-
- BluetoothMapMessage(HashMap<String, String> attrs) throws IllegalArgumentException {
- int size;
-
- try {
- /* just to validate */
- new BigInteger(attrs.get("handle"), 16);
-
- mHandle = attrs.get("handle");
- } catch (NumberFormatException e) {
- /*
- * handle MUST have proper value, if it does not then throw
- * something here
- */
- throw new IllegalArgumentException(e);
- }
-
- mSubject = attrs.get("subject");
- String dateTime = attrs.get("datetime");
- //Handle possible NPE when not able to retreive datetime attribute
- if(dateTime != null){
- mDateTime = (new ObexTime(dateTime)).getTime();
- } else {
- mDateTime = null;
- }
-
-
- mSenderName = attrs.get("sender_name");
-
- mSenderAddressing = attrs.get("sender_addressing");
-
- mReplytoAddressing = attrs.get("replyto_addressing");
-
- mRecipientName = attrs.get("recipient_name");
-
- mRecipientAddressing = attrs.get("recipient_addressing");
-
- mType = strToType(attrs.get("type"));
-
- try {
- size = Integer.parseInt(attrs.get("size"));
- } catch (NumberFormatException e) {
- size = 0;
- }
-
- mSize = size;
-
- mText = yesnoToBoolean(attrs.get("text"));
-
- mReceptionStatus = strToReceptionStatus(attrs.get("reception_status"));
-
- try {
- size = Integer.parseInt(attrs.get("attachment_size"));
- } catch (NumberFormatException e) {
- size = 0;
- }
-
- mAttachmentSize = size;
-
- mPriority = yesnoToBoolean(attrs.get("priority"));
-
- mRead = yesnoToBoolean(attrs.get("read"));
-
- mSent = yesnoToBoolean(attrs.get("sent"));
-
- mProtected = yesnoToBoolean(attrs.get("protected"));
- }
-
- private boolean yesnoToBoolean(String yesno) {
- return "yes".equals(yesno);
- }
-
- private Type strToType(String s) {
- if ("EMAIL".equals(s)) {
- return Type.EMAIL;
- } else if ("SMS_GSM".equals(s)) {
- return Type.SMS_GSM;
- } else if ("SMS_CDMA".equals(s)) {
- return Type.SMS_CDMA;
- } else if ("MMS".equals(s)) {
- return Type.MMS;
- }
-
- return Type.UNKNOWN;
- }
-
- private ReceptionStatus strToReceptionStatus(String s) {
- if ("complete".equals(s)) {
- return ReceptionStatus.COMPLETE;
- } else if ("fractioned".equals(s)) {
- return ReceptionStatus.FRACTIONED;
- } else if ("notification".equals(s)) {
- return ReceptionStatus.NOTIFICATION;
- }
-
- return ReceptionStatus.UNKNOWN;
- }
-
- @Override
- public String toString() {
- JSONObject json = new JSONObject();
-
- try {
- json.put("handle", mHandle);
- json.put("subject", mSubject);
- json.put("datetime", mDateTime);
- json.put("sender_name", mSenderName);
- json.put("sender_addressing", mSenderAddressing);
- json.put("replyto_addressing", mReplytoAddressing);
- json.put("recipient_name", mRecipientName);
- json.put("recipient_addressing", mRecipientAddressing);
- json.put("type", mType);
- json.put("size", mSize);
- json.put("text", mText);
- json.put("reception_status", mReceptionStatus);
- json.put("attachment_size", mAttachmentSize);
- json.put("priority", mPriority);
- json.put("read", mRead);
- json.put("sent", mSent);
- json.put("protected", mProtected);
- } catch (JSONException e) {
- // do nothing
- }
-
- return json.toString();
- }
-
- /**
- * @return value corresponding to <code>handle</code> parameter in MAP
- * specification
- */
- public String getHandle() {
- return mHandle;
- }
-
- /**
- * @return value corresponding to <code>subject</code> parameter in MAP
- * specification
- */
- public String getSubject() {
- return mSubject;
- }
-
- /**
- * @return <code>Date</code> object corresponding to <code>datetime</code>
- * parameter in MAP specification
- */
- public Date getDateTime() {
- return mDateTime;
- }
-
- /**
- * @return value corresponding to <code>sender_name</code> parameter in MAP
- * specification
- */
- public String getSenderName() {
- return mSenderName;
- }
-
- /**
- * @return value corresponding to <code>sender_addressing</code> parameter
- * in MAP specification
- */
- public String getSenderAddressing() {
- return mSenderAddressing;
- }
-
- /**
- * @return value corresponding to <code>replyto_addressing</code> parameter
- * in MAP specification
- */
- public String getReplytoAddressing() {
- return mReplytoAddressing;
- }
-
- /**
- * @return value corresponding to <code>recipient_name</code> parameter in
- * MAP specification
- */
- public String getRecipientName() {
- return mRecipientName;
- }
-
- /**
- * @return value corresponding to <code>recipient_addressing</code>
- * parameter in MAP specification
- */
- public String getRecipientAddressing() {
- return mRecipientAddressing;
- }
-
- /**
- * @return {@link Type} object corresponding to <code>type</code> parameter
- * in MAP specification
- */
- public Type getType() {
- return mType;
- }
-
- /**
- * @return value corresponding to <code>size</code> parameter in MAP
- * specification
- */
- public int getSize() {
- return mSize;
- }
-
- /**
- * @return {@link .ReceptionStatus} object corresponding to
- * <code>reception_status</code> parameter in MAP specification
- */
- public ReceptionStatus getReceptionStatus() {
- return mReceptionStatus;
- }
-
- /**
- * @return value corresponding to <code>attachment_size</code> parameter in
- * MAP specification
- */
- public int getAttachmentSize() {
- return mAttachmentSize;
- }
-
- /**
- * @return value corresponding to <code>text</code> parameter in MAP
- * specification
- */
- public boolean isText() {
- return mText;
- }
-
- /**
- * @return value corresponding to <code>priority</code> parameter in MAP
- * specification
- */
- public boolean isPriority() {
- return mPriority;
- }
-
- /**
- * @return value corresponding to <code>read</code> parameter in MAP
- * specification
- */
- public boolean isRead() {
- return mRead;
- }
-
- /**
- * @return value corresponding to <code>sent</code> parameter in MAP
- * specification
- */
- public boolean isSent() {
- return mSent;
- }
-
- /**
- * @return value corresponding to <code>protected</code> parameter in MAP
- * specification
- */
- public boolean isProtected() {
- return mProtected;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapMessagesListing.java b/android/bluetooth/client/map/BluetoothMapMessagesListing.java
deleted file mode 100644
index 2fb3dea7..00000000
--- a/android/bluetooth/client/map/BluetoothMapMessagesListing.java
+++ /dev/null
@@ -1,84 +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.bluetooth.client.map;
-
-import android.util.Log;
-
-import org.xmlpull.v1.XmlPullParser;
-import org.xmlpull.v1.XmlPullParserException;
-import org.xmlpull.v1.XmlPullParserFactory;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.HashMap;
-
-class BluetoothMapMessagesListing {
-
- private static final String TAG = "BluetoothMapMessagesListing";
-
- private final ArrayList<BluetoothMapMessage> mMessages;
-
- public BluetoothMapMessagesListing(InputStream in) {
- mMessages = new ArrayList<BluetoothMapMessage>();
-
- parse(in);
- }
-
- public void parse(InputStream in) {
-
- try {
- XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser();
- xpp.setInput(in, "utf-8");
-
- int event = xpp.getEventType();
- while (event != XmlPullParser.END_DOCUMENT) {
- switch (event) {
- case XmlPullParser.START_TAG:
- if (xpp.getName().equals("msg")) {
-
- HashMap<String, String> attrs = new HashMap<String, String>();
-
- for (int i = 0; i < xpp.getAttributeCount(); i++) {
- attrs.put(xpp.getAttributeName(i), xpp.getAttributeValue(i));
- }
-
- try {
- BluetoothMapMessage msg = new BluetoothMapMessage(attrs);
- mMessages.add(msg);
- } catch (IllegalArgumentException e) {
- /* TODO: provide something more useful here */
- Log.w(TAG, "Invalid <msg/>");
- }
- }
- break;
- }
-
- event = xpp.next();
- }
-
- } catch (XmlPullParserException e) {
- Log.e(TAG, "XML parser error when parsing XML", e);
- } catch (IOException e) {
- Log.e(TAG, "I/O error when parsing XML", e);
- }
- }
-
- public ArrayList<BluetoothMapMessage> getList() {
- return mMessages;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMapRfcommTransport.java b/android/bluetooth/client/map/BluetoothMapRfcommTransport.java
deleted file mode 100644
index 5bec982b..00000000
--- a/android/bluetooth/client/map/BluetoothMapRfcommTransport.java
+++ /dev/null
@@ -1,92 +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.bluetooth.client.map;
-
-import android.bluetooth.BluetoothSocket;
-
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-
-import javax.obex.ObexTransport;
-
-class BluetoothMapRfcommTransport implements ObexTransport {
- private final BluetoothSocket mSocket;
-
- public BluetoothMapRfcommTransport(BluetoothSocket socket) {
- super();
- mSocket = socket;
- }
-
- @Override
- public void create() throws IOException {
- }
-
- @Override
- public void listen() throws IOException {
- }
-
- @Override
- public void close() throws IOException {
- mSocket.close();
- }
-
- @Override
- public void connect() throws IOException {
- }
-
- @Override
- public void disconnect() throws IOException {
- }
-
- @Override
- public InputStream openInputStream() throws IOException {
- return mSocket.getInputStream();
- }
-
- @Override
- public OutputStream openOutputStream() throws IOException {
- return mSocket.getOutputStream();
- }
-
- @Override
- public DataInputStream openDataInputStream() throws IOException {
- return new DataInputStream(openInputStream());
- }
-
- @Override
- public DataOutputStream openDataOutputStream() throws IOException {
- return new DataOutputStream(openOutputStream());
- }
-
- @Override
- public int getMaxTransmitPacketSize() {
- return -1;
- }
-
- @Override
- public int getMaxReceivePacketSize() {
- return -1;
- }
-
- @Override
- public boolean isSrmSupported() {
- return false;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasClient.java b/android/bluetooth/client/map/BluetoothMasClient.java
deleted file mode 100644
index 87f5a385..00000000
--- a/android/bluetooth/client/map/BluetoothMasClient.java
+++ /dev/null
@@ -1,1106 +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.bluetooth.client.map;
-
-import android.bluetooth.BluetoothDevice;
-import android.bluetooth.BluetoothMasInstance;
-import android.bluetooth.BluetoothSocket;
-import android.bluetooth.SdpMasRecord;
-import android.os.Handler;
-import android.os.Message;
-import android.util.Log;
-
-import android.bluetooth.client.map.BluetoothMasRequestSetMessageStatus.StatusIndicator;
-import android.bluetooth.client.map.utils.ObexTime;
-
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-import java.math.BigInteger;
-import java.util.ArrayDeque;
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.Iterator;
-
-import javax.obex.ObexTransport;
-
-public class BluetoothMasClient {
-
- private final static String TAG = "BluetoothMasClient";
-
- private static final int SOCKET_CONNECTED = 10;
-
- private static final int SOCKET_ERROR = 11;
-
- /**
- * Callback message sent when connection state changes
- * <p>
- * <code>arg1</code> is set to {@link #STATUS_OK} when connection is
- * established successfully and {@link #STATUS_FAILED} when connection
- * either failed or was disconnected (depends on request from application)
- *
- * @see #connect()
- * @see #disconnect()
- */
- public static final int EVENT_CONNECT = 1;
-
- /**
- * Callback message sent when MSE accepted update inbox request
- *
- * @see #updateInbox()
- */
- public static final int EVENT_UPDATE_INBOX = 2;
-
- /**
- * Callback message sent when path is changed
- * <p>
- * <code>obj</code> is set to path currently set on MSE
- *
- * @see #setFolderRoot()
- * @see #setFolderUp()
- * @see #setFolderDown(String)
- */
- public static final int EVENT_SET_PATH = 3;
-
- /**
- * Callback message sent when folder listing is received
- * <p>
- * <code>obj</code> contains ArrayList of sub-folder names
- *
- * @see #getFolderListing()
- * @see #getFolderListing(int, int)
- */
- public static final int EVENT_GET_FOLDER_LISTING = 4;
-
- /**
- * Callback message sent when folder listing size is received
- * <p>
- * <code>obj</code> contains number of items in folder listing
- *
- * @see #getFolderListingSize()
- */
- public static final int EVENT_GET_FOLDER_LISTING_SIZE = 5;
-
- /**
- * Callback message sent when messages listing is received
- * <p>
- * <code>obj</code> contains ArrayList of {@link BluetoothMapBmessage}
- *
- * @see #getMessagesListing(String, int)
- * @see #getMessagesListing(String, int, MessagesFilter, int)
- * @see #getMessagesListing(String, int, MessagesFilter, int, int, int)
- */
- public static final int EVENT_GET_MESSAGES_LISTING = 6;
-
- /**
- * Callback message sent when message is received
- * <p>
- * <code>obj</code> contains {@link BluetoothMapBmessage}
- *
- * @see #getMessage(String, CharsetType, boolean)
- */
- public static final int EVENT_GET_MESSAGE = 7;
-
- /**
- * Callback message sent when message status is changed
- *
- * @see #setMessageDeletedStatus(String, boolean)
- * @see #setMessageReadStatus(String, boolean)
- */
- public static final int EVENT_SET_MESSAGE_STATUS = 8;
-
- /**
- * Callback message sent when message is pushed to MSE
- * <p>
- * <code>obj</code> contains handle of message as allocated by MSE
- *
- * @see #pushMessage(String, BluetoothMapBmessage, CharsetType)
- * @see #pushMessage(String, BluetoothMapBmessage, CharsetType, boolean,
- * boolean)
- */
- public static final int EVENT_PUSH_MESSAGE = 9;
-
- /**
- * Callback message sent when notification status is changed
- * <p>
- * <code>obj</code> contains <code>1</code> if notifications are enabled and
- * <code>0</code> otherwise
- *
- * @see #setNotificationRegistration(boolean)
- */
- public static final int EVENT_SET_NOTIFICATION_REGISTRATION = 10;
-
- /**
- * Callback message sent when event report is received from MSE to MNS
- * <p>
- * <code>obj</code> contains {@link BluetoothMapEventReport}
- *
- * @see #setNotificationRegistration(boolean)
- */
- public static final int EVENT_EVENT_REPORT = 11;
-
- /**
- * Callback message sent when messages listing size is received
- * <p>
- * <code>obj</code> contains number of items in messages listing
- *
- * @see #getMessagesListingSize()
- */
- public static final int EVENT_GET_MESSAGES_LISTING_SIZE = 12;
-
- /**
- * Status for callback message when request is successful
- */
- public static final int STATUS_OK = 0;
-
- /**
- * Status for callback message when request is not successful
- */
- public static final int STATUS_FAILED = 1;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_DEFAULT = 0x00000000;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_SUBJECT = 0x00000001;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_DATETIME = 0x00000002;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_SENDER_NAME = 0x00000004;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_SENDER_ADDRESSING = 0x00000008;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_RECIPIENT_NAME = 0x00000010;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_RECIPIENT_ADDRESSING = 0x00000020;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_TYPE = 0x00000040;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_SIZE = 0x00000080;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_RECEPTION_STATUS = 0x00000100;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_TEXT = 0x00000200;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_ATTACHMENT_SIZE = 0x00000400;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_PRIORITY = 0x00000800;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_READ = 0x00001000;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_SENT = 0x00002000;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_PROTECTED = 0x00004000;
-
- /**
- * Constant corresponding to <code>ParameterMask</code> application
- * parameter value in MAP specification
- */
- public static final int PARAMETER_REPLYTO_ADDRESSING = 0x00008000;
-
- public enum ConnectionState {
- DISCONNECTED, CONNECTING, CONNECTED, DISCONNECTING;
- }
-
- public enum CharsetType {
- NATIVE, UTF_8;
- }
-
- /** device associated with client */
- private final BluetoothDevice mDevice;
-
- /** MAS instance associated with client */
- private final SdpMasRecord mMas;
-
- /** callback handler to application */
- private final Handler mCallback;
-
- private ConnectionState mConnectionState = ConnectionState.DISCONNECTED;
-
- private boolean mNotificationEnabled = false;
-
- private SocketConnectThread mConnectThread = null;
-
- private ObexTransport mObexTransport = null;
-
- private BluetoothMasObexClientSession mObexSession = null;
-
- private SessionHandler mSessionHandler = null;
-
- private BluetoothMnsService mMnsService = null;
-
- private ArrayDeque<String> mPath = null;
-
- private static class SessionHandler extends Handler {
-
- private final WeakReference<BluetoothMasClient> mClient;
-
- public SessionHandler(BluetoothMasClient client) {
- super();
-
- mClient = new WeakReference<BluetoothMasClient>(client);
- }
-
- @Override
- public void handleMessage(Message msg) {
-
- BluetoothMasClient client = mClient.get();
- if (client == null) {
- return;
- }
- Log.v(TAG, "handleMessage "+msg.what);
-
- switch (msg.what) {
- case SOCKET_ERROR:
- client.mConnectThread = null;
- client.sendToClient(EVENT_CONNECT, false);
- break;
-
- case SOCKET_CONNECTED:
- client.mConnectThread = null;
-
- client.mObexTransport = (ObexTransport) msg.obj;
-
- client.mObexSession = new BluetoothMasObexClientSession(client.mObexTransport,
- client.mSessionHandler);
- client.mObexSession.start();
- break;
-
- case BluetoothMasObexClientSession.MSG_OBEX_CONNECTED:
- client.mPath.clear(); // we're in root after connected
- client.mConnectionState = ConnectionState.CONNECTED;
- client.sendToClient(EVENT_CONNECT, true);
- break;
-
- case BluetoothMasObexClientSession.MSG_OBEX_DISCONNECTED:
- client.mConnectionState = ConnectionState.DISCONNECTED;
- client.mNotificationEnabled = false;
- client.mObexSession = null;
- client.sendToClient(EVENT_CONNECT, false);
- break;
-
- case BluetoothMasObexClientSession.MSG_REQUEST_COMPLETED:
- BluetoothMasRequest request = (BluetoothMasRequest) msg.obj;
- int status = request.isSuccess() ? STATUS_OK : STATUS_FAILED;
-
- Log.v(TAG, "MSG_REQUEST_COMPLETED (" + status + ") for "
- + request.getClass().getName());
-
- if (request instanceof BluetoothMasRequestUpdateInbox) {
- client.sendToClient(EVENT_UPDATE_INBOX, request.isSuccess());
-
- } else if (request instanceof BluetoothMasRequestSetPath) {
- if (request.isSuccess()) {
- BluetoothMasRequestSetPath req = (BluetoothMasRequestSetPath) request;
- switch (req.mDir) {
- case UP:
- if (client.mPath.size() > 0) {
- client.mPath.removeLast();
- }
- break;
-
- case ROOT:
- client.mPath.clear();
- break;
-
- case DOWN:
- client.mPath.addLast(req.mName);
- break;
- }
- }
-
- client.sendToClient(EVENT_SET_PATH, request.isSuccess(),
- client.getCurrentPath());
-
- } else if (request instanceof BluetoothMasRequestGetFolderListing) {
- BluetoothMasRequestGetFolderListing req = (BluetoothMasRequestGetFolderListing) request;
- ArrayList<String> folders = req.getList();
-
- client.sendToClient(EVENT_GET_FOLDER_LISTING, request.isSuccess(), folders);
-
- } else if (request instanceof BluetoothMasRequestGetFolderListingSize) {
- int size = ((BluetoothMasRequestGetFolderListingSize) request).getSize();
-
- client.sendToClient(EVENT_GET_FOLDER_LISTING_SIZE, request.isSuccess(),
- size);
-
- } else if (request instanceof BluetoothMasRequestGetMessagesListing) {
- BluetoothMasRequestGetMessagesListing req = (BluetoothMasRequestGetMessagesListing) request;
- ArrayList<BluetoothMapMessage> msgs = req.getList();
-
- client.sendToClient(EVENT_GET_MESSAGES_LISTING, request.isSuccess(), msgs);
-
- } else if (request instanceof BluetoothMasRequestGetMessage) {
- BluetoothMasRequestGetMessage req = (BluetoothMasRequestGetMessage) request;
- BluetoothMapBmessage bmsg = req.getMessage();
-
- client.sendToClient(EVENT_GET_MESSAGE, request.isSuccess(), bmsg);
-
- } else if (request instanceof BluetoothMasRequestSetMessageStatus) {
- client.sendToClient(EVENT_SET_MESSAGE_STATUS, request.isSuccess());
-
- } else if (request instanceof BluetoothMasRequestPushMessage) {
- BluetoothMasRequestPushMessage req = (BluetoothMasRequestPushMessage) request;
- String handle = req.getMsgHandle();
-
- client.sendToClient(EVENT_PUSH_MESSAGE, request.isSuccess(), handle);
-
- } else if (request instanceof BluetoothMasRequestSetNotificationRegistration) {
- BluetoothMasRequestSetNotificationRegistration req = (BluetoothMasRequestSetNotificationRegistration) request;
-
- client.mNotificationEnabled = req.isSuccess() ? req.getStatus()
- : client.mNotificationEnabled;
-
- client.sendToClient(EVENT_SET_NOTIFICATION_REGISTRATION,
- request.isSuccess(),
- client.mNotificationEnabled ? 1 : 0);
- } else if (request instanceof BluetoothMasRequestGetMessagesListingSize) {
- int size = ((BluetoothMasRequestGetMessagesListingSize) request).getSize();
- client.sendToClient(EVENT_GET_MESSAGES_LISTING_SIZE, request.isSuccess(),
- size);
- }
- break;
-
- case BluetoothMnsService.EVENT_REPORT:
- /* pass event report directly to app */
- client.sendToClient(EVENT_EVENT_REPORT, true, msg.obj);
- break;
- }
- }
- }
-
- private void sendToClient(int event, boolean success) {
- sendToClient(event, success, null);
- }
-
- private void sendToClient(int event, boolean success, int param) {
- sendToClient(event, success, Integer.valueOf(param));
- }
-
- private void sendToClient(int event, boolean success, Object param) {
- // Send event, status and notification state for both sucess and failure case.
- mCallback.obtainMessage(event, success ? STATUS_OK : STATUS_FAILED, mMas.getMasInstanceId(),
- param).sendToTarget();
- }
-
- private class SocketConnectThread extends Thread {
- private BluetoothSocket socket = null;
-
- public SocketConnectThread() {
- super("SocketConnectThread");
- }
-
- @Override
- public void run() {
- try {
- socket = mDevice.createRfcommSocket(mMas.getRfcommCannelNumber());
- socket.connect();
-
- BluetoothMapRfcommTransport transport;
- transport = new BluetoothMapRfcommTransport(socket);
-
- mSessionHandler.obtainMessage(SOCKET_CONNECTED, transport).sendToTarget();
- } catch (IOException e) {
- Log.e(TAG, "Error when creating/connecting socket", e);
-
- closeSocket();
- mSessionHandler.obtainMessage(SOCKET_ERROR).sendToTarget();
- }
- }
-
- @Override
- public void interrupt() {
- closeSocket();
- }
-
- private void closeSocket() {
- try {
- if (socket != null) {
- socket.close();
- }
- } catch (IOException e) {
- Log.e(TAG, "Error when closing socket", e);
- }
- }
- }
-
- /**
- * Object representation of filters to be applied on message listing
- *
- * @see #getMessagesListing(String, int, MessagesFilter, int)
- * @see #getMessagesListing(String, int, MessagesFilter, int, int, int)
- */
- public static final class MessagesFilter {
-
- public final static byte MESSAGE_TYPE_ALL = 0x00;
- public final static byte MESSAGE_TYPE_SMS_GSM = 0x01;
- public final static byte MESSAGE_TYPE_SMS_CDMA = 0x02;
- public final static byte MESSAGE_TYPE_EMAIL = 0x04;
- public final static byte MESSAGE_TYPE_MMS = 0x08;
-
- public final static byte READ_STATUS_ANY = 0x00;
- public final static byte READ_STATUS_UNREAD = 0x01;
- public final static byte READ_STATUS_READ = 0x02;
-
- public final static byte PRIORITY_ANY = 0x00;
- public final static byte PRIORITY_HIGH = 0x01;
- public final static byte PRIORITY_NON_HIGH = 0x02;
-
- byte messageType = MESSAGE_TYPE_ALL;
-
- String periodBegin = null;
-
- String periodEnd = null;
-
- byte readStatus = READ_STATUS_ANY;
-
- String recipient = null;
-
- String originator = null;
-
- byte priority = PRIORITY_ANY;
-
- public MessagesFilter() {
- }
-
- public void setMessageType(byte filter) {
- messageType = filter;
- }
-
- public void setPeriod(Date filterBegin, Date filterEnd) {
- //Handle possible NPE for obexTime constructor utility
- if(filterBegin != null )
- periodBegin = (new ObexTime(filterBegin)).toString();
- if(filterEnd != null)
- periodEnd = (new ObexTime(filterEnd)).toString();
- }
-
- public void setReadStatus(byte readfilter) {
- readStatus = readfilter;
- }
-
- public void setRecipient(String filter) {
- if ("".equals(filter)) {
- recipient = null;
- } else {
- recipient = filter;
- }
- }
-
- public void setOriginator(String filter) {
- if ("".equals(filter)) {
- originator = null;
- } else {
- originator = filter;
- }
- }
-
- public void setPriority(byte filter) {
- priority = filter;
- }
- }
-
- /**
- * Constructs client object to communicate with single MAS instance on MSE
- *
- * @param device {@link BluetoothDevice} corresponding to remote device
- * acting as MSE
- * @param mas {@link BluetoothMasInstance} object describing MAS instance on
- * remote device
- * @param callback {@link Handler} object to which callback messages will be
- * sent Each message will have <code>arg1</code> set to either
- * {@link #STATUS_OK} or {@link #STATUS_FAILED} and
- * <code>arg2</code> to MAS instance ID. <code>obj</code> in
- * message is event specific.
- */
- public BluetoothMasClient(BluetoothDevice device, SdpMasRecord mas,
- Handler callback) {
- mDevice = device;
- mMas = mas;
- mCallback = callback;
-
- mPath = new ArrayDeque<String>();
- }
-
- /**
- * Retrieves MAS instance data associated with client
- *
- * @return instance data object
- */
- public SdpMasRecord getInstanceData() {
- return mMas;
- }
-
- /**
- * Connects to MAS instance
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_CONNECT}
- */
- public void connect() {
- if (mSessionHandler == null) {
- mSessionHandler = new SessionHandler(this);
- }
-
- if (mConnectThread == null && mObexSession == null) {
- mConnectionState = ConnectionState.CONNECTING;
-
- mConnectThread = new SocketConnectThread();
- mConnectThread.start();
- }
- }
-
- /**
- * Disconnects from MAS instance
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_CONNECT}
- */
- public void disconnect() {
- if (mConnectThread == null && mObexSession == null) {
- return;
- }
-
- mConnectionState = ConnectionState.DISCONNECTING;
-
- if (mConnectThread != null) {
- mConnectThread.interrupt();
- }
-
- if (mObexSession != null) {
- mObexSession.stop();
- }
- }
-
- @Override
- public void finalize() {
- disconnect();
- }
-
- /**
- * Gets current connection state
- *
- * @return current connection state
- * @see ConnectionState
- */
- public ConnectionState getState() {
- return mConnectionState;
- }
-
- private boolean enableNotifications() {
- Log.v(TAG, "enableNotifications()");
-
- if (mMnsService == null) {
- mMnsService = new BluetoothMnsService();
- }
-
- mMnsService.registerCallback(mMas.getMasInstanceId(), mSessionHandler);
-
- BluetoothMasRequest request = new BluetoothMasRequestSetNotificationRegistration(true);
- return mObexSession.makeRequest(request);
- }
-
- private boolean disableNotifications() {
- Log.v(TAG, "enableNotifications()");
-
- if (mMnsService != null) {
- mMnsService.unregisterCallback(mMas.getMasInstanceId());
- }
-
- mMnsService = null;
-
- BluetoothMasRequest request = new BluetoothMasRequestSetNotificationRegistration(false);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Sets state of notifications for MAS instance
- * <p>
- * Once notifications are enabled, callback handler will receive
- * {@link #EVENT_EVENT_REPORT} when new notification is received
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_SET_NOTIFICATION_REGISTRATION}
- *
- * @param status <code>true</code> if notifications shall be enabled,
- * <code>false</code> otherwise
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean setNotificationRegistration(boolean status) {
- if (mObexSession == null) {
- return false;
- }
-
- if (status) {
- return enableNotifications();
- } else {
- return disableNotifications();
- }
- }
-
- /**
- * Gets current state of notifications for MAS instance
- *
- * @return <code>true</code> if notifications are enabled,
- * <code>false</code> otherwise
- */
- public boolean getNotificationRegistration() {
- return mNotificationEnabled;
- }
-
- /**
- * Goes back to root of folder hierarchy
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_SET_PATH}
- *
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean setFolderRoot() {
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestSetPath(true);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Goes back to parent folder in folder hierarchy
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_SET_PATH}
- *
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean setFolderUp() {
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestSetPath(false);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Goes down to specified sub-folder in folder hierarchy
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_SET_PATH}
- *
- * @param name name of sub-folder
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean setFolderDown(String name) {
- if (mObexSession == null) {
- return false;
- }
-
- if (name == null || name.isEmpty() || name.contains("/")) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestSetPath(name);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Gets current path in folder hierarchy
- *
- * @return current path
- */
- public String getCurrentPath() {
- if (mPath.size() == 0) {
- return "";
- }
-
- Iterator<String> iter = mPath.iterator();
-
- StringBuilder sb = new StringBuilder(iter.next());
-
- while (iter.hasNext()) {
- sb.append("/").append(iter.next());
- }
-
- return sb.toString();
- }
-
- /**
- * Gets list of sub-folders in current folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_FOLDER_LISTING}
- *
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean getFolderListing() {
- return getFolderListing((short) 0, (short) 0);
- }
-
- /**
- * Gets list of sub-folders in current folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_FOLDER_LISTING}
- *
- * @param maxListCount maximum number of items returned or <code>0</code>
- * for default value
- * @param listStartOffset index of first item returned or <code>0</code> for
- * default value
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- * @throws IllegalArgumentException if either maxListCount or
- * listStartOffset are outside allowed range [0..65535]
- */
- public boolean getFolderListing(int maxListCount, int listStartOffset) {
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestGetFolderListing(maxListCount,
- listStartOffset);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Gets number of sub-folders in current folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_FOLDER_LISTING_SIZE}
- *
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean getFolderListingSize() {
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestGetFolderListingSize();
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Gets list of messages in specified sub-folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_MESSAGES_LISTING}
- *
- * @param folder name of sub-folder or <code>null</code> for current folder
- * @param parameters bit-mask specifying requested parameters in listing or
- * <code>0</code> for default value
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean getMessagesListing(String folder, int parameters) {
- return getMessagesListing(folder, parameters, null, (byte) 0, 0, 0);
- }
-
- /**
- * Gets list of messages in specified sub-folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_MESSAGES_LISTING}
- *
- * @param folder name of sub-folder or <code>null</code> for current folder
- * @param parameters corresponds to <code>ParameterMask</code> application
- * parameter in MAP specification
- * @param filter {@link MessagesFilter} object describing filters to be
- * applied on listing by MSE
- * @param subjectLength maximum length of message subject in returned
- * listing or <code>0</code> for default value
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- * @throws IllegalArgumentException if subjectLength is outside allowed
- * range [0..255]
- */
- public boolean getMessagesListing(String folder, int parameters, MessagesFilter filter,
- int subjectLength) {
-
- return getMessagesListing(folder, parameters, filter, subjectLength, 0, 0);
- }
-
- /**
- * Gets list of messages in specified sub-folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_MESSAGES_LISTING}
- *
- * @param folder name of sub-folder or <code>null</code> for current folder
- * @param parameters corresponds to <code>ParameterMask</code> application
- * parameter in MAP specification
- * @param filter {@link MessagesFilter} object describing filters to be
- * applied on listing by MSE
- * @param subjectLength maximum length of message subject in returned
- * listing or <code>0</code> for default value
- * @param maxListCount maximum number of items returned or <code>0</code>
- * for default value
- * @param listStartOffset index of first item returned or <code>0</code> for
- * default value
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- * @throws IllegalArgumentException if subjectLength is outside allowed
- * range [0..255] or either maxListCount or listStartOffset are
- * outside allowed range [0..65535]
- */
- public boolean getMessagesListing(String folder, int parameters, MessagesFilter filter,
- int subjectLength, int maxListCount, int listStartOffset) {
-
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestGetMessagesListing(folder,
- parameters, filter, subjectLength, maxListCount, listStartOffset);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Gets number of messages in current folder
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_GET_MESSAGES_LISTING_SIZE}
- *
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean getMessagesListingSize() {
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestGetMessagesListingSize();
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Retrieves message from MSE
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_GET_MESSAGE}
- *
- * @param handle handle of message to retrieve
- * @param charset {@link CharsetType} object corresponding to
- * <code>Charset</code> application parameter in MAP
- * specification
- * @param attachment corresponds to <code>Attachment</code> application
- * parameter in MAP specification
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean getMessage(String handle, boolean attachment) {
- if (mObexSession == null) {
- return false;
- }
-
- try {
- /* just to validate */
- new BigInteger(handle, 16);
- } catch (NumberFormatException e) {
- return false;
- }
-
- // Since we support only text messaging via Bluetooth, it is OK to restrict the requests to
- // force conversion to UTF-8.
- BluetoothMasRequest request =
- new BluetoothMasRequestGetMessage(handle, CharsetType.UTF_8, attachment);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Sets read status of message on MSE
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_SET_MESSAGE_STATUS}
- *
- * @param handle handle of message
- * @param read <code>true</code> for "read", <code>false</code> for "unread"
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean setMessageReadStatus(String handle, boolean read) {
- if (mObexSession == null) {
- return false;
- }
-
- try {
- /* just to validate */
- new BigInteger(handle, 16);
- } catch (NumberFormatException e) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestSetMessageStatus(handle,
- StatusIndicator.READ, read);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Sets deleted status of message on MSE
- * <p>
- * Upon completion callback handler will receive
- * {@link #EVENT_SET_MESSAGE_STATUS}
- *
- * @param handle handle of message
- * @param deleted <code>true</code> for "deleted", <code>false</code> for
- * "undeleted"
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean setMessageDeletedStatus(String handle, boolean deleted) {
- if (mObexSession == null) {
- return false;
- }
-
- try {
- /* just to validate */
- new BigInteger(handle, 16);
- } catch (NumberFormatException e) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestSetMessageStatus(handle,
- StatusIndicator.DELETED, deleted);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Pushes new message to MSE
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_PUSH_MESSAGE}
- *
- * @param folder name of sub-folder to push to or <code>null</code> for
- * current folder
- * @param charset {@link CharsetType} object corresponding to
- * <code>Charset</code> application parameter in MAP
- * specification
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean pushMessage(String folder, BluetoothMapBmessage bmsg, CharsetType charset) {
- return pushMessage(folder, bmsg, charset, false, false);
- }
-
- /**
- * Pushes new message to MSE
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_PUSH_MESSAGE}
- *
- * @param folder name of sub-folder to push to or <code>null</code> for
- * current folder
- * @param bmsg {@link BluetoothMapBmessage} object representing message to
- * be pushed
- * @param charset {@link CharsetType} object corresponding to
- * <code>Charset</code> application parameter in MAP
- * specification
- * @param transparent corresponds to <code>Transparent</code> application
- * parameter in MAP specification
- * @param retry corresponds to <code>Transparent</code> application
- * parameter in MAP specification
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean pushMessage(String folder, BluetoothMapBmessage bmsg, CharsetType charset,
- boolean transparent, boolean retry) {
- if (mObexSession == null) {
- return false;
- }
-
- String bmsgString = BluetoothMapBmessageBuilder.createBmessage(bmsg);
-
- BluetoothMasRequest request =
- new BluetoothMasRequestPushMessage(folder, bmsgString, charset, transparent, retry);
- return mObexSession.makeRequest(request);
- }
-
- /**
- * Requests MSE to initiate ubdate of inbox
- * <p>
- * Upon completion callback handler will receive {@link #EVENT_UPDATE_INBOX}
- *
- * @return <code>true</code> if request has been sent, <code>false</code>
- * otherwise
- */
- public boolean updateInbox() {
- if (mObexSession == null) {
- return false;
- }
-
- BluetoothMasRequest request = new BluetoothMasRequestUpdateInbox();
- return mObexSession.makeRequest(request);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasObexClientSession.java b/android/bluetooth/client/map/BluetoothMasObexClientSession.java
deleted file mode 100644
index 9bf75d40..00000000
--- a/android/bluetooth/client/map/BluetoothMasObexClientSession.java
+++ /dev/null
@@ -1,187 +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.bluetooth.client.map;
-
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.Message;
-import android.os.Process;
-import android.util.Log;
-
-import java.io.IOException;
-import java.lang.ref.WeakReference;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-import javax.obex.ObexTransport;
-import javax.obex.ResponseCodes;
-
-class BluetoothMasObexClientSession {
- private static final String TAG = "BluetoothMasObexClientSession";
-
- private static final byte[] MAS_TARGET = new byte[] {
- (byte) 0xbb, 0x58, 0x2b, 0x40, 0x42, 0x0c, 0x11, (byte) 0xdb, (byte) 0xb0, (byte) 0xde,
- 0x08, 0x00, 0x20, 0x0c, (byte) 0x9a, 0x66
- };
-
- private boolean DBG = true;
-
- static final int MSG_OBEX_CONNECTED = 100;
- static final int MSG_OBEX_DISCONNECTED = 101;
- static final int MSG_REQUEST_COMPLETED = 102;
-
- private static final int CONNECT = 0;
- private static final int DISCONNECT = 1;
- private static final int REQUEST = 2;
-
- private final ObexTransport mTransport;
-
- private final Handler mSessionHandler;
-
- private ClientSession mSession;
-
- private HandlerThread mThread;
- private Handler mHandler;
-
- private boolean mConnected;
-
- private static class ObexClientHandler extends Handler {
- WeakReference<BluetoothMasObexClientSession> mInst;
-
- ObexClientHandler(Looper looper, BluetoothMasObexClientSession inst) {
- super(looper);
- mInst = new WeakReference<BluetoothMasObexClientSession>(inst);
- }
-
- @Override
- public void handleMessage(Message msg) {
- BluetoothMasObexClientSession inst = mInst.get();
- if (!inst.connected() && msg.what != CONNECT) {
- Log.w(TAG, "Cannot execute " + msg + " when not CONNECTED.");
- return;
- }
-
- switch (msg.what) {
- case CONNECT:
- inst.connect();
- break;
-
- case DISCONNECT:
- inst.disconnect();
- break;
-
- case REQUEST:
- inst.executeRequest((BluetoothMasRequest) msg.obj);
- break;
- }
- }
- }
-
- public BluetoothMasObexClientSession(ObexTransport transport, Handler handler) {
- mTransport = transport;
- mSessionHandler = handler;
- }
-
- public void start() {
- if (DBG) Log.d(TAG, "start called.");
- if (mConnected) {
- if (DBG) Log.d(TAG, "Already connected, nothing to do.");
- return;
- }
-
- // Start a thread to handle messages here.
- mThread = new HandlerThread("BluetoothMasObexClientSessionThread");
- mThread.start();
- mHandler = new ObexClientHandler(mThread.getLooper(), this);
-
- // Connect it to the target device via OBEX.
- mHandler.obtainMessage(CONNECT).sendToTarget();
- }
-
- public boolean makeRequest(BluetoothMasRequest request) {
- if (DBG) Log.d(TAG, "makeRequest called with: " + request);
-
- boolean status = mHandler.sendMessage(mHandler.obtainMessage(REQUEST, request));
- if (!status) {
- Log.e(TAG, "Adding messages failed, state: " + mConnected);
- return false;
- }
- return true;
- }
-
- public void stop() {
- if (DBG) Log.d(TAG, "stop called...");
-
- mThread.quit();
- disconnect();
- }
-
- private void connect() {
- try {
- mSession = new ClientSession(mTransport);
-
- HeaderSet headerset = new HeaderSet();
- headerset.setHeader(HeaderSet.TARGET, MAS_TARGET);
-
- headerset = mSession.connect(headerset);
-
- if (headerset.getResponseCode() == ResponseCodes.OBEX_HTTP_OK) {
- mConnected = true;
- mSessionHandler.obtainMessage(MSG_OBEX_CONNECTED).sendToTarget();
- } else {
- disconnect();
- }
- } catch (IOException e) {
- disconnect();
- }
- }
-
- private void disconnect() {
- if (mSession != null) {
- try {
- mSession.disconnect(null);
- } catch (IOException e) {
- }
-
- try {
- mSession.close();
- } catch (IOException e) {
- }
- }
-
- mConnected = false;
- mSessionHandler.obtainMessage(MSG_OBEX_DISCONNECTED).sendToTarget();
- }
-
- private void executeRequest(BluetoothMasRequest request) {
- try {
- request.execute(mSession);
- mSessionHandler.obtainMessage(MSG_REQUEST_COMPLETED, request).sendToTarget();
- } catch (IOException e) {
- if (DBG) Log.d(TAG, "Request failed: " + request);
-
- // Disconnect to cleanup.
- disconnect();
- }
- }
-
-
- private boolean connected() {
- return mConnected;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequest.java b/android/bluetooth/client/map/BluetoothMasRequest.java
deleted file mode 100644
index 0c9c29c0..00000000
--- a/android/bluetooth/client/map/BluetoothMasRequest.java
+++ /dev/null
@@ -1,162 +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.bluetooth.client.map;
-
-import java.io.DataOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-
-import javax.obex.ClientOperation;
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-import javax.obex.Operation;
-import javax.obex.ResponseCodes;
-
-abstract class BluetoothMasRequest {
-
- protected static final byte OAP_TAGID_MAX_LIST_COUNT = 0x01;
- protected static final byte OAP_TAGID_START_OFFSET = 0x02;
- protected static final byte OAP_TAGID_FILTER_MESSAGE_TYPE = 0x03;
- protected static final byte OAP_TAGID_FILTER_PERIOD_BEGIN = 0x04;
- protected static final byte OAP_TAGID_FILTER_PERIOD_END = 0x05;
- protected static final byte OAP_TAGID_FILTER_READ_STATUS = 0x06;
- protected static final byte OAP_TAGID_FILTER_RECIPIENT = 0x07;
- protected static final byte OAP_TAGID_FILTER_ORIGINATOR = 0x08;
- protected static final byte OAP_TAGID_FILTER_PRIORITY = 0x09;
- protected static final byte OAP_TAGID_ATTACHMENT = 0x0a;
- protected static final byte OAP_TAGID_TRANSPARENT = 0xb;
- protected static final byte OAP_TAGID_RETRY = 0xc;
- protected static final byte OAP_TAGID_NEW_MESSAGE = 0x0d;
- protected static final byte OAP_TAGID_NOTIFICATION_STATUS = 0x0e;
- protected static final byte OAP_TAGID_MAS_INSTANCE_ID = 0x0f;
- protected static final byte OAP_TAGID_PARAMETER_MASK = 0x10;
- protected static final byte OAP_TAGID_FOLDER_LISTING_SIZE = 0x11;
- protected static final byte OAP_TAGID_MESSAGES_LISTING_SIZE = 0x12;
- protected static final byte OAP_TAGID_SUBJECT_LENGTH = 0x13;
- protected static final byte OAP_TAGID_CHARSET = 0x14;
- protected static final byte OAP_TAGID_STATUS_INDICATOR = 0x17;
- protected static final byte OAP_TAGID_STATUS_VALUE = 0x18;
- protected static final byte OAP_TAGID_MSE_TIME = 0x19;
-
- protected static byte NOTIFICATION_ON = 0x01;
- protected static byte NOTIFICATION_OFF = 0x00;
-
- protected static byte ATTACHMENT_ON = 0x01;
- protected static byte ATTACHMENT_OFF = 0x00;
-
- protected static byte CHARSET_NATIVE = 0x00;
- protected static byte CHARSET_UTF8 = 0x01;
-
- protected static byte STATUS_INDICATOR_READ = 0x00;
- protected static byte STATUS_INDICATOR_DELETED = 0x01;
-
- protected static byte STATUS_NO = 0x00;
- protected static byte STATUS_YES = 0x01;
-
- protected static byte TRANSPARENT_OFF = 0x00;
- protected static byte TRANSPARENT_ON = 0x01;
-
- protected static byte RETRY_OFF = 0x00;
- protected static byte RETRY_ON = 0x01;
-
- /* used for PUT requests which require filler byte */
- protected static final byte[] FILLER_BYTE = {
- 0x30
- };
-
- protected HeaderSet mHeaderSet;
-
- protected int mResponseCode;
-
- public BluetoothMasRequest() {
- mHeaderSet = new HeaderSet();
- }
-
- abstract public void execute(ClientSession session) throws IOException;
-
- protected void executeGet(ClientSession session) throws IOException {
- ClientOperation op = null;
-
- try {
- op = (ClientOperation) session.get(mHeaderSet);
-
- /*
- * MAP spec does not explicitly require that GET request should be
- * sent in single packet but for some reason PTS complains when
- * final GET packet with no headers follows non-final GET with all
- * headers. So this is workaround, at least temporary. TODO: check
- * with PTS
- */
- op.setGetFinalFlag(true);
-
- /*
- * this will trigger ClientOperation to use non-buffered stream so
- * we can abort operation
- */
- op.continueOperation(true, false);
-
- readResponseHeaders(op.getReceivedHeader());
-
- InputStream is = op.openInputStream();
- readResponse(is);
- is.close();
-
- op.close();
-
- mResponseCode = op.getResponseCode();
- } catch (IOException e) {
- mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
-
- throw e;
- }
- }
-
- protected void executePut(ClientSession session, byte[] body) throws IOException {
- Operation op = null;
-
- mHeaderSet.setHeader(HeaderSet.LENGTH, Long.valueOf(body.length));
-
- try {
- op = session.put(mHeaderSet);
-
- DataOutputStream out = op.openDataOutputStream();
- out.write(body);
- out.close();
-
- readResponseHeaders(op.getReceivedHeader());
-
- op.close();
- mResponseCode = op.getResponseCode();
- } catch (IOException e) {
- mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
-
- throw e;
- }
- }
-
- final public boolean isSuccess() {
- return (mResponseCode == ResponseCodes.OBEX_HTTP_OK);
- }
-
- protected void readResponse(InputStream stream) throws IOException {
- /* nothing here by default */
- }
-
- protected void readResponseHeaders(HeaderSet headerset) {
- /* nothing here by default */
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestGetFolderListing.java b/android/bluetooth/client/map/BluetoothMasRequestGetFolderListing.java
deleted file mode 100644
index db22ada9..00000000
--- a/android/bluetooth/client/map/BluetoothMasRequestGetFolderListing.java
+++ /dev/null
@@ -1,75 +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.bluetooth.client.map;
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestGetFolderListing extends BluetoothMasRequest {
-
- private static final String TYPE = "x-obex/folder-listing";
-
- private BluetoothMapFolderListing mResponse = null;
-
- public BluetoothMasRequestGetFolderListing(int maxListCount, int listStartOffset) {
-
- if (maxListCount < 0 || maxListCount > 65535) {
- throw new IllegalArgumentException("maxListCount should be [0..65535]");
- }
-
- if (listStartOffset < 0 || listStartOffset > 65535) {
- throw new IllegalArgumentException("listStartOffset should be [0..65535]");
- }
-
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
-
- ObexAppParameters oap = new ObexAppParameters();
- // Allow GetFolderListing for maxListCount value 0 also.
- if (maxListCount >= 0) {
- oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) maxListCount);
- }
-
- if (listStartOffset > 0) {
- oap.add(OAP_TAGID_START_OFFSET, (short) listStartOffset);
- }
-
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- protected void readResponse(InputStream stream) {
- mResponse = new BluetoothMapFolderListing(stream);
- }
-
- public ArrayList<String> getList() {
- if (mResponse == null) {
- return null;
- }
-
- return mResponse.getList();
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executeGet(session);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestGetFolderListingSize.java b/android/bluetooth/client/map/BluetoothMasRequestGetFolderListingSize.java
deleted file mode 100644
index 910c0362..00000000
--- a/android/bluetooth/client/map/BluetoothMasRequestGetFolderListingSize.java
+++ /dev/null
@@ -1,56 +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.bluetooth.client.map;
-
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.IOException;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestGetFolderListingSize extends BluetoothMasRequest {
-
- private static final String TYPE = "x-obex/folder-listing";
-
- private int mSize;
-
- public BluetoothMasRequestGetFolderListingSize() {
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
-
- ObexAppParameters oap = new ObexAppParameters();
- oap.add(OAP_TAGID_MAX_LIST_COUNT, 0);
-
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- protected void readResponseHeaders(HeaderSet headerset) {
- ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
-
- mSize = oap.getShort(OAP_TAGID_FOLDER_LISTING_SIZE);
- }
-
- public int getSize() {
- return mSize;
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executeGet(session);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestGetMessage.java b/android/bluetooth/client/map/BluetoothMasRequestGetMessage.java
deleted file mode 100644
index 923bff08..00000000
--- a/android/bluetooth/client/map/BluetoothMasRequestGetMessage.java
+++ /dev/null
@@ -1,101 +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.bluetooth.client.map;
-
-import android.util.Log;
-
-
-import android.bluetooth.client.map.BluetoothMasClient.CharsetType;
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.UnsupportedEncodingException;
-import java.nio.charset.StandardCharsets;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-import javax.obex.ResponseCodes;
-
-final class BluetoothMasRequestGetMessage extends BluetoothMasRequest {
-
- private static final String TAG = "BluetoothMasRequestGetMessage";
-
- private static final String TYPE = "x-bt/message";
-
- private BluetoothMapBmessage mBmessage;
-
- public BluetoothMasRequestGetMessage(String handle, CharsetType charset, boolean attachment) {
-
- mHeaderSet.setHeader(HeaderSet.NAME, handle);
-
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
-
- ObexAppParameters oap = new ObexAppParameters();
-
- oap.add(OAP_TAGID_CHARSET, CharsetType.UTF_8.equals(charset) ? CHARSET_UTF8
- : CHARSET_NATIVE);
-
- oap.add(OAP_TAGID_ATTACHMENT, attachment ? ATTACHMENT_ON : ATTACHMENT_OFF);
-
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- protected void readResponse(InputStream stream) {
-
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- byte[] buf = new byte[1024];
-
- try {
- int len;
- while ((len = stream.read(buf)) != -1) {
- baos.write(buf, 0, len);
- }
- } catch (IOException e) {
- Log.e(TAG, "I/O exception while reading response", e);
- }
-
- // Convert the input stream using UTF-8 since the attributes in the payload are all encoded
- // according to it. The actual message body may need to be transcoded depending on
- // charset/encoding defined for body-content.
- String bmsg;
- try {
- bmsg = baos.toString(StandardCharsets.UTF_8.name());
- } catch (UnsupportedEncodingException ex) {
- Log.e(TAG,
- "Coudn't decode the bmessage with UTF-8. Something must be really messed up.");
- return;
- }
-
- mBmessage = BluetoothMapBmessageParser.createBmessage(bmsg);
-
- if (mBmessage == null) {
- mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
- }
- }
-
- public BluetoothMapBmessage getMessage() {
- return mBmessage;
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executeGet(session);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListing.java b/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListing.java
deleted file mode 100644
index 2ad167de..00000000
--- a/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListing.java
+++ /dev/null
@@ -1,155 +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.bluetooth.client.map;
-
-import android.bluetooth.client.map.BluetoothMasClient.MessagesFilter;
-import android.bluetooth.client.map.utils.ObexAppParameters;
-import android.bluetooth.client.map.utils.ObexTime;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Date;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestGetMessagesListing extends BluetoothMasRequest {
-
- private static final String TYPE = "x-bt/MAP-msg-listing";
-
- private BluetoothMapMessagesListing mResponse = null;
-
- private boolean mNewMessage = false;
-
- private Date mServerTime = null;
-
- public BluetoothMasRequestGetMessagesListing(String folderName, int parameters,
- BluetoothMasClient.MessagesFilter filter, int subjectLength, int maxListCount,
- int listStartOffset) {
- if (subjectLength < 0 || subjectLength > 255) {
- throw new IllegalArgumentException("subjectLength should be [0..255]");
- }
-
- if (maxListCount < 0 || maxListCount > 65535) {
- throw new IllegalArgumentException("maxListCount should be [0..65535]");
- }
-
- if (listStartOffset < 0 || listStartOffset > 65535) {
- throw new IllegalArgumentException("listStartOffset should be [0..65535]");
- }
-
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
-
- if (folderName == null) {
- mHeaderSet.setHeader(HeaderSet.NAME, "");
- } else {
- mHeaderSet.setHeader(HeaderSet.NAME, folderName);
- }
-
- ObexAppParameters oap = new ObexAppParameters();
-
- if (filter != null) {
- if (filter.messageType != MessagesFilter.MESSAGE_TYPE_ALL) {
- oap.add(OAP_TAGID_FILTER_MESSAGE_TYPE, filter.messageType);
- }
-
- if (filter.periodBegin != null) {
- oap.add(OAP_TAGID_FILTER_PERIOD_BEGIN, filter.periodBegin);
- }
-
- if (filter.periodEnd != null) {
- oap.add(OAP_TAGID_FILTER_PERIOD_END, filter.periodEnd);
- }
-
- if (filter.readStatus != MessagesFilter.READ_STATUS_ANY) {
- oap.add(OAP_TAGID_FILTER_READ_STATUS, filter.readStatus);
- }
-
- if (filter.recipient != null) {
- oap.add(OAP_TAGID_FILTER_RECIPIENT, filter.recipient);
- }
-
- if (filter.originator != null) {
- oap.add(OAP_TAGID_FILTER_ORIGINATOR, filter.originator);
- }
-
- if (filter.priority != MessagesFilter.PRIORITY_ANY) {
- oap.add(OAP_TAGID_FILTER_PRIORITY, filter.priority);
- }
- }
-
- if (subjectLength != 0) {
- oap.add(OAP_TAGID_SUBJECT_LENGTH, (byte) subjectLength);
- }
- /* Include parameterMask only when specific values are selected,
- * to avoid IOT specific issue with no paramterMask header support.
- */
- if (parameters > 0 ) {
- oap.add(OAP_TAGID_PARAMETER_MASK, parameters);
- }
- // Allow GetMessageListing for maxlistcount value 0 also.
- if (maxListCount >= 0) {
- oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) maxListCount);
- }
-
- if (listStartOffset != 0) {
- oap.add(OAP_TAGID_START_OFFSET, (short) listStartOffset);
- }
-
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- protected void readResponse(InputStream stream) {
- mResponse = new BluetoothMapMessagesListing(stream);
- }
-
- @Override
- protected void readResponseHeaders(HeaderSet headerset) {
- ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
-
- mNewMessage = ((oap.getByte(OAP_TAGID_NEW_MESSAGE) & 0x01) == 1);
-
- if (oap.exists(OAP_TAGID_MSE_TIME)) {
- String mseTime = oap.getString(OAP_TAGID_MSE_TIME);
- if(mseTime != null )
- mServerTime = (new ObexTime(mseTime)).getTime();
- }
- }
-
- public ArrayList<BluetoothMapMessage> getList() {
- if (mResponse == null) {
- return null;
- }
-
- return mResponse.getList();
- }
-
- public boolean getNewMessageStatus() {
- return mNewMessage;
- }
-
- public Date getMseTime() {
- return mServerTime;
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executeGet(session);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListingSize.java b/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListingSize.java
deleted file mode 100644
index cdadb2ec..00000000
--- a/android/bluetooth/client/map/BluetoothMasRequestGetMessagesListingSize.java
+++ /dev/null
@@ -1,57 +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.bluetooth.client.map;
-
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.IOException;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestGetMessagesListingSize extends BluetoothMasRequest {
-
- private static final String TYPE = "x-bt/MAP-msg-listing";
-
- private int mSize;
-
- public BluetoothMasRequestGetMessagesListingSize() {
- mHeaderSet.setHeader(HeaderSet.NAME, "");
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
-
- ObexAppParameters oap = new ObexAppParameters();
- oap.add(OAP_TAGID_MAX_LIST_COUNT, (short) 0);
-
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- protected void readResponseHeaders(HeaderSet headerset) {
- ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
-
- mSize = oap.getShort(OAP_TAGID_MESSAGES_LISTING_SIZE);
- }
-
- public int getSize() {
- return mSize;
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executeGet(session);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestPushMessage.java b/android/bluetooth/client/map/BluetoothMasRequestPushMessage.java
deleted file mode 100644
index 8fc9bd41..00000000
--- a/android/bluetooth/client/map/BluetoothMasRequestPushMessage.java
+++ /dev/null
@@ -1,79 +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.bluetooth.client.map;
-
-import java.io.IOException;
-import java.math.BigInteger;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-import javax.obex.ResponseCodes;
-
-import android.bluetooth.client.map.BluetoothMasClient.CharsetType;
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-final class BluetoothMasRequestPushMessage extends BluetoothMasRequest {
-
- private static final String TYPE = "x-bt/message";
- private String mMsg;
- private String mMsgHandle;
-
- private BluetoothMasRequestPushMessage(String folder) {
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
- if (folder == null) {
- folder = "";
- }
- mHeaderSet.setHeader(HeaderSet.NAME, folder);
- }
-
- public BluetoothMasRequestPushMessage(String folder, String msg, CharsetType charset,
- boolean transparent, boolean retry) {
- this(folder);
- mMsg = msg;
- ObexAppParameters oap = new ObexAppParameters();
- oap.add(OAP_TAGID_TRANSPARENT, transparent ? TRANSPARENT_ON : TRANSPARENT_OFF);
- oap.add(OAP_TAGID_RETRY, retry ? RETRY_ON : RETRY_OFF);
- oap.add(OAP_TAGID_CHARSET, charset == CharsetType.NATIVE ? CHARSET_NATIVE : CHARSET_UTF8);
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- protected void readResponseHeaders(HeaderSet headerset) {
- try {
- String handle = (String) headerset.getHeader(HeaderSet.NAME);
- if (handle != null) {
- /* just to validate */
- new BigInteger(handle, 16);
-
- mMsgHandle = handle;
- }
- } catch (NumberFormatException e) {
- mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
- } catch (IOException e) {
- mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
- }
- }
-
- public String getMsgHandle() {
- return mMsgHandle;
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executePut(session, mMsg.getBytes());
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestSetMessageStatus.java b/android/bluetooth/client/map/BluetoothMasRequestSetMessageStatus.java
deleted file mode 100644
index 140312e1..00000000
--- a/android/bluetooth/client/map/BluetoothMasRequestSetMessageStatus.java
+++ /dev/null
@@ -1,52 +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.bluetooth.client.map;
-
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.IOException;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestSetMessageStatus extends BluetoothMasRequest {
-
- public enum StatusIndicator {
- READ, DELETED;
- }
-
- private static final String TYPE = "x-bt/messageStatus";
-
- public BluetoothMasRequestSetMessageStatus(String handle, StatusIndicator statusInd,
- boolean statusValue) {
-
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
- mHeaderSet.setHeader(HeaderSet.NAME, handle);
-
- ObexAppParameters oap = new ObexAppParameters();
- oap.add(OAP_TAGID_STATUS_INDICATOR,
- statusInd == StatusIndicator.READ ? STATUS_INDICATOR_READ
- : STATUS_INDICATOR_DELETED);
- oap.add(OAP_TAGID_STATUS_VALUE, statusValue ? STATUS_YES : STATUS_NO);
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executePut(session, FILLER_BYTE);
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestSetNotificationRegistration.java b/android/bluetooth/client/map/BluetoothMasRequestSetNotificationRegistration.java
deleted file mode 100644
index debb5089..00000000
--- a/android/bluetooth/client/map/BluetoothMasRequestSetNotificationRegistration.java
+++ /dev/null
@@ -1,52 +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.bluetooth.client.map;
-
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.IOException;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestSetNotificationRegistration extends BluetoothMasRequest {
-
- private static final String TYPE = "x-bt/MAP-NotificationRegistration";
-
- private final boolean mStatus;
-
- public BluetoothMasRequestSetNotificationRegistration(boolean status) {
- mStatus = status;
-
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
-
- ObexAppParameters oap = new ObexAppParameters();
-
- oap.add(OAP_TAGID_NOTIFICATION_STATUS, status ? NOTIFICATION_ON : NOTIFICATION_OFF);
-
- oap.addToHeaderSet(mHeaderSet);
- }
-
- @Override
- public void execute(ClientSession session) throws IOException {
- executePut(session, FILLER_BYTE);
- }
-
- public boolean getStatus() {
- return mStatus;
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestSetPath.java b/android/bluetooth/client/map/BluetoothMasRequestSetPath.java
deleted file mode 100644
index 71e2dbe4..00000000
--- a/android/bluetooth/client/map/BluetoothMasRequestSetPath.java
+++ /dev/null
@@ -1,71 +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.bluetooth.client.map;
-
-import java.io.IOException;
-
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-import javax.obex.ResponseCodes;
-
-class BluetoothMasRequestSetPath extends BluetoothMasRequest {
-
- enum SetPathDir {
- ROOT, UP, DOWN
- };
-
- SetPathDir mDir;
-
- String mName;
-
- public BluetoothMasRequestSetPath(String name) {
- mDir = SetPathDir.DOWN;
- mName = name;
-
- mHeaderSet.setHeader(HeaderSet.NAME, name);
- }
-
- public BluetoothMasRequestSetPath(boolean goRoot) {
- mHeaderSet.setEmptyNameHeader();
- if (goRoot) {
- mDir = SetPathDir.ROOT;
- } else {
- mDir = SetPathDir.UP;
- }
- }
-
- @Override
- public void execute(ClientSession session) {
- HeaderSet hs = null;
-
- try {
- switch (mDir) {
- case ROOT:
- case DOWN:
- hs = session.setPath(mHeaderSet, false, false);
- break;
- case UP:
- hs = session.setPath(mHeaderSet, true, false);
- break;
- }
-
- mResponseCode = hs.getResponseCode();
- } catch (IOException e) {
- mResponseCode = ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
- }
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMnsObexServer.java b/android/bluetooth/client/map/BluetoothMnsObexServer.java
deleted file mode 100644
index 672e9cf6..00000000
--- a/android/bluetooth/client/map/BluetoothMnsObexServer.java
+++ /dev/null
@@ -1,136 +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.bluetooth.client.map;
-
-import android.os.Handler;
-import android.util.Log;
-
-import android.bluetooth.client.map.utils.ObexAppParameters;
-
-import java.io.IOException;
-import java.util.Arrays;
-
-import javax.obex.HeaderSet;
-import javax.obex.Operation;
-import javax.obex.ResponseCodes;
-import javax.obex.ServerRequestHandler;
-
-class BluetoothMnsObexServer extends ServerRequestHandler {
-
- private final static String TAG = "BluetoothMnsObexServer";
-
- private static final byte[] MNS_TARGET = new byte[] {
- (byte) 0xbb, 0x58, 0x2b, 0x41, 0x42, 0x0c, 0x11, (byte) 0xdb, (byte) 0xb0, (byte) 0xde,
- 0x08, 0x00, 0x20, 0x0c, (byte) 0x9a, 0x66
- };
-
- private final static String TYPE = "x-bt/MAP-event-report";
-
- private final Handler mCallback;
-
- public BluetoothMnsObexServer(Handler callback) {
- super();
-
- mCallback = callback;
- }
-
- @Override
- public int onConnect(final HeaderSet request, HeaderSet reply) {
- Log.v(TAG, "onConnect");
-
- try {
- byte[] uuid = (byte[]) request.getHeader(HeaderSet.TARGET);
-
- if (!Arrays.equals(uuid, MNS_TARGET)) {
- return ResponseCodes.OBEX_HTTP_NOT_ACCEPTABLE;
- }
-
- } catch (IOException e) {
- // this should never happen since getHeader won't throw exception it
- // declares to throw
- return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
- }
-
- reply.setHeader(HeaderSet.WHO, MNS_TARGET);
- return ResponseCodes.OBEX_HTTP_OK;
- }
-
- @Override
- public void onDisconnect(final HeaderSet request, HeaderSet reply) {
- Log.v(TAG, "onDisconnect");
- }
-
- @Override
- public int onGet(final Operation op) {
- Log.v(TAG, "onGet");
-
- return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
- }
-
- @Override
- public int onPut(final Operation op) {
- Log.v(TAG, "onPut");
-
- try {
- HeaderSet headerset;
- headerset = op.getReceivedHeader();
-
- String type = (String) headerset.getHeader(HeaderSet.TYPE);
- ObexAppParameters oap = ObexAppParameters.fromHeaderSet(headerset);
-
- if (!TYPE.equals(type) || !oap.exists(BluetoothMasRequest.OAP_TAGID_MAS_INSTANCE_ID)) {
- return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
- }
-
- Byte inst = oap.getByte(BluetoothMasRequest.OAP_TAGID_MAS_INSTANCE_ID);
-
- BluetoothMapEventReport ev = BluetoothMapEventReport.fromStream(op
- .openDataInputStream());
-
- op.close();
-
- mCallback.obtainMessage(BluetoothMnsService.MSG_EVENT, inst, 0, ev).sendToTarget();
- } catch (IOException e) {
- Log.e(TAG, "I/O exception when handling PUT request", e);
- return ResponseCodes.OBEX_HTTP_INTERNAL_ERROR;
- }
-
- return ResponseCodes.OBEX_HTTP_OK;
- }
-
- @Override
- public int onAbort(final HeaderSet request, HeaderSet reply) {
- Log.v(TAG, "onAbort");
-
- return ResponseCodes.OBEX_HTTP_NOT_IMPLEMENTED;
- }
-
- @Override
- public int onSetPath(final HeaderSet request, HeaderSet reply,
- final boolean backup, final boolean create) {
- Log.v(TAG, "onSetPath");
-
- return ResponseCodes.OBEX_HTTP_BAD_REQUEST;
- }
-
- @Override
- public void onClose() {
- Log.v(TAG, "onClose");
-
- // TODO: call session handler so it can disconnect
- }
-}
diff --git a/android/bluetooth/client/map/BluetoothMnsService.java b/android/bluetooth/client/map/BluetoothMnsService.java
deleted file mode 100644
index 42175e00..00000000
--- a/android/bluetooth/client/map/BluetoothMnsService.java
+++ /dev/null
@@ -1,195 +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.bluetooth.client.map;
-
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.BluetoothServerSocket;
-import android.bluetooth.BluetoothSocket;
-import android.os.Handler;
-import android.os.Message;
-import android.os.ParcelUuid;
-import android.util.Log;
-import android.util.SparseArray;
-
-import java.io.IOException;
-import java.io.InterruptedIOException;
-import java.lang.ref.WeakReference;
-
-import javax.obex.ServerSession;
-
-class BluetoothMnsService {
-
- private static final String TAG = "BluetoothMnsService";
-
- private static final ParcelUuid MAP_MNS =
- ParcelUuid.fromString("00001133-0000-1000-8000-00805F9B34FB");
-
- static final int MSG_EVENT = 1;
-
- /* for BluetoothMasClient */
- static final int EVENT_REPORT = 1001;
-
- /* these are shared across instances */
- static private SparseArray<Handler> mCallbacks = null;
- static private SocketAcceptThread mAcceptThread = null;
- static private Handler mSessionHandler = null;
- static private BluetoothServerSocket mServerSocket = null;
-
- private static class SessionHandler extends Handler {
-
- private final WeakReference<BluetoothMnsService> mService;
-
- SessionHandler(BluetoothMnsService service) {
- mService = new WeakReference<BluetoothMnsService>(service);
- }
-
- @Override
- public void handleMessage(Message msg) {
- Log.d(TAG, "Handler: msg: " + msg.what);
-
- switch (msg.what) {
- case MSG_EVENT:
- int instanceId = msg.arg1;
-
- synchronized (mCallbacks) {
- Handler cb = mCallbacks.get(instanceId);
-
- if (cb != null) {
- BluetoothMapEventReport ev = (BluetoothMapEventReport) msg.obj;
- cb.obtainMessage(EVENT_REPORT, ev).sendToTarget();
- } else {
- Log.w(TAG, "Got event for instance which is not registered: "
- + instanceId);
- }
- }
- break;
- }
- }
- }
-
- private static class SocketAcceptThread extends Thread {
-
- private boolean mInterrupted = false;
-
- @Override
- public void run() {
-
- if (mServerSocket != null) {
- Log.w(TAG, "Socket already created, exiting");
- return;
- }
-
- try {
- BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
- mServerSocket = adapter.listenUsingEncryptedRfcommWithServiceRecord(
- "MAP Message Notification Service", MAP_MNS.getUuid());
- } catch (IOException e) {
- mInterrupted = true;
- Log.e(TAG, "I/O exception when trying to create server socket", e);
- }
-
- while (!mInterrupted) {
- try {
- Log.v(TAG, "waiting to accept connection...");
-
- BluetoothSocket sock = mServerSocket.accept();
-
- Log.v(TAG, "new incoming connection from "
- + sock.getRemoteDevice().getName());
-
- // session will live until closed by remote
- BluetoothMnsObexServer srv = new BluetoothMnsObexServer(mSessionHandler);
- BluetoothMapRfcommTransport transport = new BluetoothMapRfcommTransport(
- sock);
- new ServerSession(transport, srv, null);
- } catch (IOException ex) {
- Log.v(TAG, "I/O exception when waiting to accept (aborted?)");
- mInterrupted = true;
- }
- }
-
- if (mServerSocket != null) {
- try {
- mServerSocket.close();
- } catch (IOException e) {
- // do nothing
- }
-
- mServerSocket = null;
- }
- }
- }
-
- BluetoothMnsService() {
- Log.v(TAG, "BluetoothMnsService()");
-
- if (mCallbacks == null) {
- Log.v(TAG, "BluetoothMnsService(): allocating callbacks");
- mCallbacks = new SparseArray<Handler>();
- }
-
- if (mSessionHandler == null) {
- Log.v(TAG, "BluetoothMnsService(): allocating session handler");
- mSessionHandler = new SessionHandler(this);
- }
- }
-
- public void registerCallback(int instanceId, Handler callback) {
- Log.v(TAG, "registerCallback()");
-
- synchronized (mCallbacks) {
- mCallbacks.put(instanceId, callback);
-
- if (mAcceptThread == null) {
- Log.v(TAG, "registerCallback(): starting MNS server");
- mAcceptThread = new SocketAcceptThread();
- mAcceptThread.setName("BluetoothMnsAcceptThread");
- mAcceptThread.start();
- }
- }
- }
-
- public void unregisterCallback(int instanceId) {
- Log.v(TAG, "unregisterCallback()");
-
- synchronized (mCallbacks) {
- mCallbacks.remove(instanceId);
-
- if (mCallbacks.size() == 0) {
- Log.v(TAG, "unregisterCallback(): shutting down MNS server");
-
- if (mServerSocket != null) {
- try {
- mServerSocket.close();
- } catch (IOException e) {
- }
-
- mServerSocket = null;
- }
-
- mAcceptThread.interrupt();
-
- try {
- mAcceptThread.join(5000);
- } catch (InterruptedException e) {
- }
-
- mAcceptThread = null;
- }
- }
- }
-}
diff --git a/android/bluetooth/client/map/utils/BmsgTokenizer.java b/android/bluetooth/client/map/utils/BmsgTokenizer.java
deleted file mode 100644
index 9f239618..00000000
--- a/android/bluetooth/client/map/utils/BmsgTokenizer.java
+++ /dev/null
@@ -1,108 +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.bluetooth.client.map.utils;
-
-import android.util.Log;
-
-import java.text.ParseException;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public final class BmsgTokenizer {
-
- private final String mStr;
-
- private final Matcher mMatcher;
-
- private int mPos = 0;
-
- private final int mOffset;
-
- static public class Property {
- public final String name;
- public final String value;
-
- public Property(String name, String value) {
- if (name == null || value == null) {
- throw new IllegalArgumentException();
- }
-
- this.name = name;
- this.value = value;
-
- Log.v("BMSG >> ", toString());
- }
-
- @Override
- public String toString() {
- return name + ":" + value;
- }
-
- @Override
- public boolean equals(Object o) {
- return ((o instanceof Property) && ((Property) o).name.equals(name) && ((Property) o).value
- .equals(value));
- }
- };
-
- public BmsgTokenizer(String str) {
- this(str, 0);
- }
-
- public BmsgTokenizer(String str, int offset) {
- mStr = str;
- mOffset = offset;
- mMatcher = Pattern.compile("(([^:]*):(.*))?\r\n").matcher(str);
- mPos = mMatcher.regionStart();
- }
-
- public Property next(boolean alwaysReturn) throws ParseException {
- boolean found = false;
-
- do {
- mMatcher.region(mPos, mMatcher.regionEnd());
-
- if (!mMatcher.lookingAt()) {
- if (alwaysReturn) {
- return null;
- }
-
- throw new ParseException("Property or empty line expected", pos());
- }
-
- mPos = mMatcher.end();
-
- if (mMatcher.group(1) != null) {
- found = true;
- }
- } while (!found);
-
- return new Property(mMatcher.group(2), mMatcher.group(3));
- }
-
- public Property next() throws ParseException {
- return next(false);
- }
-
- public String remaining() {
- return mStr.substring(mPos);
- }
-
- public int pos() {
- return mPos + mOffset;
- }
-}
diff --git a/android/bluetooth/client/map/utils/ObexAppParameters.java b/android/bluetooth/client/map/utils/ObexAppParameters.java
deleted file mode 100644
index cae379b7..00000000
--- a/android/bluetooth/client/map/utils/ObexAppParameters.java
+++ /dev/null
@@ -1,182 +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.bluetooth.client.map.utils;
-
-import java.io.IOException;
-import java.nio.ByteBuffer;
-import java.util.HashMap;
-import java.util.Map;
-
-import javax.obex.HeaderSet;
-
-public final class ObexAppParameters {
-
- private final HashMap<Byte, byte[]> mParams;
-
- public ObexAppParameters() {
- mParams = new HashMap<Byte, byte[]>();
- }
-
- public ObexAppParameters(byte[] raw) {
- mParams = new HashMap<Byte, byte[]>();
-
- if (raw != null) {
- for (int i = 0; i < raw.length;) {
- if (raw.length - i < 2) {
- break;
- }
-
- byte tag = raw[i++];
- byte len = raw[i++];
-
- if (raw.length - i - len < 0) {
- break;
- }
-
- byte[] val = new byte[len];
-
- System.arraycopy(raw, i, val, 0, len);
- this.add(tag, val);
-
- i += len;
- }
- }
- }
-
- public static ObexAppParameters fromHeaderSet(HeaderSet headerset) {
- try {
- byte[] raw = (byte[]) headerset.getHeader(HeaderSet.APPLICATION_PARAMETER);
- return new ObexAppParameters(raw);
- } catch (IOException e) {
- // won't happen
- }
-
- return null;
- }
-
- public byte[] getHeader() {
- int length = 0;
-
- for (Map.Entry<Byte, byte[]> entry : mParams.entrySet()) {
- length += (entry.getValue().length + 2);
- }
-
- byte[] ret = new byte[length];
-
- int idx = 0;
- for (Map.Entry<Byte, byte[]> entry : mParams.entrySet()) {
- length = entry.getValue().length;
-
- ret[idx++] = entry.getKey();
- ret[idx++] = (byte) length;
- System.arraycopy(entry.getValue(), 0, ret, idx, length);
- idx += length;
- }
-
- return ret;
- }
-
- public void addToHeaderSet(HeaderSet headerset) {
- if (mParams.size() > 0) {
- headerset.setHeader(HeaderSet.APPLICATION_PARAMETER, getHeader());
- }
- }
-
- public boolean exists(byte tag) {
- return mParams.containsKey(tag);
- }
-
- public void add(byte tag, byte val) {
- byte[] bval = ByteBuffer.allocate(1).put(val).array();
- mParams.put(tag, bval);
- }
-
- public void add(byte tag, short val) {
- byte[] bval = ByteBuffer.allocate(2).putShort(val).array();
- mParams.put(tag, bval);
- }
-
- public void add(byte tag, int val) {
- byte[] bval = ByteBuffer.allocate(4).putInt(val).array();
- mParams.put(tag, bval);
- }
-
- public void add(byte tag, long val) {
- byte[] bval = ByteBuffer.allocate(8).putLong(val).array();
- mParams.put(tag, bval);
- }
-
- public void add(byte tag, String val) {
- byte[] bval = val.getBytes();
- mParams.put(tag, bval);
- }
-
- public void add(byte tag, byte[] bval) {
- mParams.put(tag, bval);
- }
-
- public byte getByte(byte tag) {
- byte[] bval = mParams.get(tag);
-
- if (bval == null || bval.length < 1) {
- return 0;
- }
-
- return ByteBuffer.wrap(bval).get();
- }
-
- public short getShort(byte tag) {
- byte[] bval = mParams.get(tag);
-
- if (bval == null || bval.length < 2) {
- return 0;
- }
-
- return ByteBuffer.wrap(bval).getShort();
- }
-
- public int getInt(byte tag) {
- byte[] bval = mParams.get(tag);
-
- if (bval == null || bval.length < 4) {
- return 0;
- }
-
- return ByteBuffer.wrap(bval).getInt();
- }
-
- public String getString(byte tag) {
- byte[] bval = mParams.get(tag);
-
- if (bval == null) {
- return null;
- }
-
- return new String(bval);
- }
-
- public byte[] getByteArray(byte tag) {
- byte[] bval = mParams.get(tag);
-
- return bval;
- }
-
- @Override
- public String toString() {
- return mParams.toString();
- }
-}
diff --git a/android/bluetooth/client/map/utils/ObexTime.java b/android/bluetooth/client/map/utils/ObexTime.java
deleted file mode 100644
index b35ce816..00000000
--- a/android/bluetooth/client/map/utils/ObexTime.java
+++ /dev/null
@@ -1,101 +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.bluetooth.client.map.utils;
-
-import java.util.Calendar;
-import java.util.Date;
-import java.util.Locale;
-import java.util.TimeZone;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public final class ObexTime {
-
- private Date mDate;
-
- public ObexTime(String time) {
- /*
- * match OBEX time string: YYYYMMDDTHHMMSS with optional UTF offset
- * +/-hhmm
- */
- Pattern p = Pattern
- .compile("(\\d{4})(\\d{2})(\\d{2})T(\\d{2})(\\d{2})(\\d{2})(([+-])(\\d{2})(\\d{2}))?");
- Matcher m = p.matcher(time);
-
- if (m.matches()) {
-
- /*
- * matched groups are numberes as follows: YYYY MM DD T HH MM SS +
- * hh mm ^^^^ ^^ ^^ ^^ ^^ ^^ ^ ^^ ^^ 1 2 3 4 5 6 8 9 10 all groups
- * are guaranteed to be numeric so conversion will always succeed
- * (except group 8 which is either + or -)
- */
-
- Calendar cal = Calendar.getInstance();
- cal.set(Integer.parseInt(m.group(1)), Integer.parseInt(m.group(2)) - 1,
- Integer.parseInt(m.group(3)), Integer.parseInt(m.group(4)),
- Integer.parseInt(m.group(5)), Integer.parseInt(m.group(6)));
-
- /*
- * if 7th group is matched then we have UTC offset information
- * included
- */
- if (m.group(7) != null) {
- int ohh = Integer.parseInt(m.group(9));
- int omm = Integer.parseInt(m.group(10));
-
- /* time zone offset is specified in miliseconds */
- int offset = (ohh * 60 + omm) * 60 * 1000;
-
- if (m.group(8).equals("-")) {
- offset = -offset;
- }
-
- TimeZone tz = TimeZone.getTimeZone("UTC");
- tz.setRawOffset(offset);
-
- cal.setTimeZone(tz);
- }
-
- mDate = cal.getTime();
- }
- }
-
- public ObexTime(Date date) {
- mDate = date;
- }
-
- public Date getTime() {
- return mDate;
- }
-
- @Override
- public String toString() {
- if (mDate == null) {
- return null;
- }
-
- Calendar cal = Calendar.getInstance();
- cal.setTime(mDate);
-
- /* note that months are numbered stating from 0 */
- return String.format(Locale.US, "%04d%02d%02dT%02d%02d%02d",
- cal.get(Calendar.YEAR), cal.get(Calendar.MONTH) + 1,
- cal.get(Calendar.DATE), cal.get(Calendar.HOUR_OF_DAY),
- cal.get(Calendar.MINUTE), cal.get(Calendar.SECOND));
- }
-}
diff --git a/android/bluetooth/le/BluetoothLeScanner.java b/android/bluetooth/le/BluetoothLeScanner.java
index a189e271..347fc4df 100644
--- a/android/bluetooth/le/BluetoothLeScanner.java
+++ b/android/bluetooth/le/BluetoothLeScanner.java
@@ -387,7 +387,7 @@ public final class BluetoothLeScanner {
if (mScannerId > 0) {
mLeScanClients.put(mScanCallback, this);
} else {
- // Registration timed out or got exception, reset scannerId to -1 so no
+ // Registration timed out or got exception, reset RscannerId to -1 so no
// subsequent operations can proceed.
if (mScannerId == 0) mScannerId = -1;
diff --git a/android/content/ClipData.java b/android/content/ClipData.java
index 9323261f..94e1e2df 100644
--- a/android/content/ClipData.java
+++ b/android/content/ClipData.java
@@ -34,16 +34,18 @@ import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.URLSpan;
import android.util.Log;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.util.ArrayUtils;
+import libcore.io.IoUtils;
+
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;
-import libcore.io.IoUtils;
/**
* Representation of a clipped data on the clipboard.
@@ -665,6 +667,25 @@ public class ClipData implements Parcelable {
b.append("NULL");
}
}
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ if (mHtmlText != null) {
+ proto.write(ClipDataProto.Item.HTML_TEXT, mHtmlText);
+ } else if (mText != null) {
+ proto.write(ClipDataProto.Item.TEXT, mText.toString());
+ } else if (mUri != null) {
+ proto.write(ClipDataProto.Item.URI, mUri.toString());
+ } else if (mIntent != null) {
+ mIntent.writeToProto(proto, ClipDataProto.Item.INTENT, true, true, true, true);
+ } else {
+ proto.write(ClipDataProto.Item.NOTHING, true);
+ }
+
+ proto.end(token);
+ }
}
/**
@@ -1048,6 +1069,26 @@ public class ClipData implements Parcelable {
}
/** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ if (mClipDescription != null) {
+ mClipDescription.writeToProto(proto, ClipDataProto.DESCRIPTION);
+ }
+ if (mIcon != null) {
+ final long iToken = proto.start(ClipDataProto.ICON);
+ proto.write(ClipDataProto.Icon.WIDTH, mIcon.getWidth());
+ proto.write(ClipDataProto.Icon.HEIGHT, mIcon.getHeight());
+ proto.end(iToken);
+ }
+ for (int i = 0; i < mItems.size(); i++) {
+ mItems.get(i).writeToProto(proto, ClipDataProto.ITEMS);
+ }
+
+ proto.end(token);
+ }
+
+ /** @hide */
public void collectUris(List<Uri> out) {
for (int i = 0; i < mItems.size(); ++i) {
ClipData.Item item = getItemAt(i);
diff --git a/android/content/ClipDescription.java b/android/content/ClipDescription.java
index 8e30fd6e..19295fcf 100644
--- a/android/content/ClipDescription.java
+++ b/android/content/ClipDescription.java
@@ -21,6 +21,7 @@ import android.os.Parcelable;
import android.os.PersistableBundle;
import android.text.TextUtils;
import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
@@ -337,6 +338,28 @@ public class ClipDescription implements Parcelable {
return !first;
}
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ final int size = mMimeTypes.size();
+ for (int i = 0; i < size; i++) {
+ proto.write(ClipDescriptionProto.MIME_TYPES, mMimeTypes.get(i));
+ }
+
+ if (mLabel != null) {
+ proto.write(ClipDescriptionProto.LABEL, mLabel.toString());
+ }
+ if (mExtras != null) {
+ mExtras.writeToProto(proto, ClipDescriptionProto.EXTRAS);
+ }
+ if (mTimeStamp > 0) {
+ proto.write(ClipDescriptionProto.TIMESTAMP_MS, mTimeStamp);
+ }
+
+ proto.end(token);
+ }
+
@Override
public int describeContents() {
return 0;
diff --git a/android/content/ComponentName.java b/android/content/ComponentName.java
index 0d36bddc..ead6c259 100644
--- a/android/content/ComponentName.java
+++ b/android/content/ComponentName.java
@@ -284,9 +284,11 @@ public final class ComponentName implements Parcelable, Cloneable, Comparable<Co
}
/** Put this here so that individual services don't have to reimplement this. @hide */
- public void toProto(ProtoOutputStream proto) {
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
proto.write(ComponentNameProto.PACKAGE_NAME, mPackage);
proto.write(ComponentNameProto.CLASS_NAME, mClass);
+ proto.end(token);
}
@Override
diff --git a/android/content/Context.java b/android/content/Context.java
index 4cedeaa0..1b050330 100644
--- a/android/content/Context.java
+++ b/android/content/Context.java
@@ -2851,10 +2851,12 @@ public abstract class Context {
* {@link #BIND_NOT_FOREGROUND}, {@link #BIND_ABOVE_CLIENT},
* {@link #BIND_ALLOW_OOM_MANAGEMENT}, or
* {@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. You should still call {@link #unbindService}
- * to release the connection even if this method returned {@code false}.
+ * @return {@code true} if the system is in the process of bringing up a
+ * service that your client has permission to bind to; {@code false}
+ * if the system couldn't find the service or if your client doesn't
+ * have permission to bind to it. If this value is {@code true}, you
+ * should later call {@link #unbindService} to release the
+ * connection.
*
* @throws SecurityException If the caller does not have permission to access the service
* or the service can not be found.
@@ -3022,7 +3024,8 @@ public abstract class Context {
//@hide: INCIDENT_SERVICE,
//@hide: STATS_COMPANION_SERVICE,
COMPANION_DEVICE_SERVICE,
- CROSS_PROFILE_APPS_SERVICE
+ CROSS_PROFILE_APPS_SERVICE,
+ //@hide: SYSTEM_UPDATE_SERVICE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface ServiceName {}
@@ -3221,7 +3224,7 @@ public abstract class Context {
public abstract @Nullable String getSystemServiceName(@NonNull Class<?> serviceClass);
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.PowerManager} for controlling power management,
* including "wake locks," which let you keep the device on while
* you're running long tasks.
@@ -3229,117 +3232,128 @@ public abstract class Context {
public static final String POWER_SERVICE = "power";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.RecoverySystem} for accessing the recovery system
* service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @hide
*/
public static final String RECOVERY_SERVICE = "recovery";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
+ * {@link android.os.SystemUpdateManager} for accessing the system update
+ * manager service.
+ *
+ * @see #getSystemService(String)
+ * @hide
+ */
+ @SystemApi
+ public static final String SYSTEM_UPDATE_SERVICE = "system_update";
+
+ /**
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.WindowManager} for accessing the system's window
* manager.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.view.WindowManager
*/
public static final String WINDOW_SERVICE = "window";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.LayoutInflater} for inflating layout resources in this
* context.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.view.LayoutInflater
*/
public static final String LAYOUT_INFLATER_SERVICE = "layout_inflater";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.accounts.AccountManager} for receiving intents at a
* time of your choosing.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.accounts.AccountManager
*/
public static final String ACCOUNT_SERVICE = "account";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.ActivityManager} for interacting with the global
* system state.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.ActivityManager
*/
public static final String ACTIVITY_SERVICE = "activity";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.AlarmManager} for receiving intents at a
* time of your choosing.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.AlarmManager
*/
public static final String ALARM_SERVICE = "alarm";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.NotificationManager} for informing the user of
* background events.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.NotificationManager
*/
public static final String NOTIFICATION_SERVICE = "notification";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.accessibility.AccessibilityManager} for giving the user
* feedback for UI events through the registered event listeners.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.view.accessibility.AccessibilityManager
*/
public static final String ACCESSIBILITY_SERVICE = "accessibility";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.accessibility.CaptioningManager} for obtaining
* captioning properties and listening for changes in captioning
* preferences.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.view.accessibility.CaptioningManager
*/
public static final String CAPTIONING_SERVICE = "captioning";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.NotificationManager} for controlling keyguard.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.KeyguardManager
*/
public static final String KEYGUARD_SERVICE = "keyguard";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.location.LocationManager} for controlling location
* updates.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.location.LocationManager
*/
public static final String LOCATION_SERVICE = "location";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.location.CountryDetector} for detecting the country that
* the user is in.
*
@@ -3348,96 +3362,96 @@ public abstract class Context {
public static final String COUNTRY_DETECTOR = "country_detector";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.app.SearchManager} for handling searches.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.SearchManager
*/
public static final String SEARCH_SERVICE = "search";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.hardware.SensorManager} for accessing sensors.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.SensorManager
*/
public static final String SENSOR_SERVICE = "sensor";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.os.storage.StorageManager} for accessing system storage
* functions.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.os.storage.StorageManager
*/
public static final String STORAGE_SERVICE = "storage";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.app.usage.StorageStatsManager} for accessing system storage
* statistics.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.usage.StorageStatsManager
*/
public static final String STORAGE_STATS_SERVICE = "storagestats";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* com.android.server.WallpaperService for accessing wallpapers.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String WALLPAPER_SERVICE = "wallpaper";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.os.Vibrator} for interacting with the vibration hardware.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.os.Vibrator
*/
public static final String VIBRATOR_SERVICE = "vibrator";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.app.StatusBarManager} for interacting with the status bar.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.StatusBarManager
* @hide
*/
public static final String STATUS_BAR_SERVICE = "statusbar";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.ConnectivityManager} for handling management of
* network connections.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.ConnectivityManager
*/
public static final String CONNECTIVITY_SERVICE = "connectivity";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.net.IpSecManager} for encrypting Sockets or Networks with
* IPSec.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String IPSEC_SERVICE = "ipsec";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.os.IUpdateLock} for managing runtime sequences that
* must not be interrupted by headless OTA application or similar.
*
* @hide
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.os.UpdateLock
*/
public static final String UPDATE_LOCK_SERVICE = "updatelock";
@@ -3449,18 +3463,18 @@ public abstract class Context {
public static final String NETWORKMANAGEMENT_SERVICE = "network_management";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link com.android.server.slice.SliceManagerService} for managing slices.
* @hide
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String SLICE_SERVICE = "slice";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.app.usage.NetworkStatsManager} for querying network usage stats.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.usage.NetworkStatsManager
*/
public static final String NETWORK_STATS_SERVICE = "netstats";
@@ -3470,40 +3484,40 @@ public abstract class Context {
public static final String NETWORK_WATCHLIST_SERVICE = "network_watchlist";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.wifi.WifiManager} for handling management of
* Wi-Fi access.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.wifi.WifiManager
*/
public static final String WIFI_SERVICE = "wifi";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.wifi.p2p.WifiP2pManager} for handling management of
* Wi-Fi peer-to-peer connections.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.wifi.p2p.WifiP2pManager
*/
public static final String WIFI_P2P_SERVICE = "wifip2p";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.net.wifi.aware.WifiAwareManager} for handling management of
* Wi-Fi Aware.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.wifi.aware.WifiAwareManager
*/
public static final String WIFI_AWARE_SERVICE = "wifiaware";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.wifi.WifiScanner} for scanning the wifi universe
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.wifi.WifiScanner
* @hide
*/
@@ -3511,10 +3525,10 @@ public abstract class Context {
public static final String WIFI_SCANNING_SERVICE = "wifiscanner";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.wifi.RttManager} for ranging devices with wifi
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.wifi.RttManager
* @hide
*/
@@ -3522,24 +3536,23 @@ public abstract class Context {
public static final String WIFI_RTT_SERVICE = "rttmanager";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.wifi.rtt.WifiRttManager} for ranging devices with wifi
*
* Note: this is a replacement for WIFI_RTT_SERVICE above. It will
* be renamed once final implementation in place.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.wifi.rtt.WifiRttManager
- * @hide
*/
- public static final String WIFI_RTT_RANGING_SERVICE = "rttmanager2";
+ public static final String WIFI_RTT_RANGING_SERVICE = "wifirtt";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.lowpan.LowpanManager} for handling management of
* LoWPAN access.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.lowpan.LowpanManager
*
* @hide
@@ -3547,11 +3560,11 @@ public abstract class Context {
public static final String LOWPAN_SERVICE = "lowpan";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.EthernetManager} for handling management of
* Ethernet access.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.EthernetManager
*
* @hide
@@ -3559,98 +3572,98 @@ public abstract class Context {
public static final String ETHERNET_SERVICE = "ethernet";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.net.nsd.NsdManager} for handling management of network service
* discovery
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.nsd.NsdManager
*/
public static final String NSD_SERVICE = "servicediscovery";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.media.AudioManager} for handling management of volume,
* ringer modes and audio routing.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.media.AudioManager
*/
public static final String AUDIO_SERVICE = "audio";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.fingerprint.FingerprintManager} for handling management
* of fingerprints.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.fingerprint.FingerprintManager
*/
public static final String FINGERPRINT_SERVICE = "fingerprint";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.media.MediaRouter} for controlling and managing
* routing of media.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.media.MediaRouter
*/
public static final String MEDIA_ROUTER_SERVICE = "media_router";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.media.session.MediaSessionManager} for managing media Sessions.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.media.session.MediaSessionManager
*/
public static final String MEDIA_SESSION_SERVICE = "media_session";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.telephony.TelephonyManager} for handling management the
* telephony features of the device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.telephony.TelephonyManager
*/
public static final String TELEPHONY_SERVICE = "phone";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.telephony.SubscriptionManager} for handling management the
* telephony subscriptions of the device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.telephony.SubscriptionManager
*/
public static final String TELEPHONY_SUBSCRIPTION_SERVICE = "telephony_subscription_service";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.telecom.TelecomManager} to manage telecom-related features
* of the device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.telecom.TelecomManager
*/
public static final String TELECOM_SERVICE = "telecom";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.telephony.CarrierConfigManager} for reading carrier configuration values.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.telephony.CarrierConfigManager
*/
public static final String CARRIER_CONFIG_SERVICE = "carrier_config";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.telephony.euicc.EuiccManager} to manage the device eUICC (embedded SIM).
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.telephony.euicc.EuiccManager
* TODO(b/35851809): Unhide this API.
* @hide
@@ -3658,47 +3671,58 @@ public abstract class Context {
public static final String EUICC_SERVICE = "euicc_service";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
+ * {@link android.telephony.euicc.EuiccCardManager} to access the device eUICC (embedded SIM).
+ *
+ * @see #getSystemService(String)
+ * @see android.telephony.euicc.EuiccCardManager
+ * TODO(b/35851809): Make this a SystemApi.
+ * @hide
+ */
+ public static final String EUICC_CARD_SERVICE = "euicc_card_service";
+
+ /**
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.content.ClipboardManager} for accessing and modifying
* the contents of the global clipboard.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.content.ClipboardManager
*/
public static final String CLIPBOARD_SERVICE = "clipboard";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link TextClassificationManager} for text classification services.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see TextClassificationManager
*/
public static final String TEXT_CLASSIFICATION_SERVICE = "textclassification";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.inputmethod.InputMethodManager} for accessing input
* methods.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String INPUT_METHOD_SERVICE = "input_method";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.view.textservice.TextServicesManager} for accessing
* text services.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String TEXT_SERVICES_MANAGER_SERVICE = "textservices";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.appwidget.AppWidgetManager} for accessing AppWidgets.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String APPWIDGET_SERVICE = "appwidget";
@@ -3706,7 +3730,7 @@ public abstract class Context {
* Official published name of the (internal) voice interaction manager service.
*
* @hide
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String VOICE_INTERACTION_MANAGER_SERVICE = "voiceinteraction";
@@ -3714,119 +3738,119 @@ public abstract class Context {
* Official published name of the (internal) autofill service.
*
* @hide
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String AUTOFILL_MANAGER_SERVICE = "autofill";
/**
- * Use with {@link #getSystemService} to access the
+ * Use with {@link #getSystemService(String)} to access the
* {@link com.android.server.voiceinteraction.SoundTriggerService}.
*
* @hide
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String SOUND_TRIGGER_SERVICE = "soundtrigger";
/**
- * Use with {@link #getSystemService} to retrieve an
+ * Use with {@link #getSystemService(String)} to retrieve an
* {@link android.app.backup.IBackupManager IBackupManager} for communicating
* with the backup mechanism.
* @hide
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
@SystemApi
public static final String BACKUP_SERVICE = "backup";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.DropBoxManager} instance for recording
* diagnostic logs.
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String DROPBOX_SERVICE = "dropbox";
/**
* System service name for the DeviceIdleController. There is no Java API for this.
- * @see #getSystemService
+ * @see #getSystemService(String)
* @hide
*/
public static final String DEVICE_IDLE_CONTROLLER = "deviceidle";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.admin.DevicePolicyManager} for working with global
* device policy management.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String DEVICE_POLICY_SERVICE = "device_policy";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.UiModeManager} for controlling UI modes.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String UI_MODE_SERVICE = "uimode";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.DownloadManager} for requesting HTTP downloads.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String DOWNLOAD_SERVICE = "download";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.BatteryManager} for managing battery state.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String BATTERY_SERVICE = "batterymanager";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.nfc.NfcManager} for using NFC.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String NFC_SERVICE = "nfc";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.bluetooth.BluetoothManager} for using Bluetooth.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String BLUETOOTH_SERVICE = "bluetooth";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.net.sip.SipManager} for accessing the SIP related service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
/** @hide */
public static final String SIP_SERVICE = "sip";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.hardware.usb.UsbManager} for access to USB devices (as a USB host)
* and for controlling this device's behavior as a USB device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.usb.UsbManager
*/
public static final String USB_SERVICE = "usb";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.hardware.SerialManager} for access to serial ports.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.SerialManager
*
* @hide
@@ -3834,11 +3858,11 @@ public abstract class Context {
public static final String SERIAL_SERVICE = "serial";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.hdmi.HdmiControlManager} for controlling and managing
* HDMI-CEC protocol.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.hdmi.HdmiControlManager
* @hide
*/
@@ -3846,67 +3870,67 @@ public abstract class Context {
public static final String HDMI_CONTROL_SERVICE = "hdmi_control";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.input.InputManager} for interacting with input devices.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.input.InputManager
*/
public static final String INPUT_SERVICE = "input";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.display.DisplayManager} for interacting with display devices.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.display.DisplayManager
*/
public static final String DISPLAY_SERVICE = "display";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.UserManager} for managing users on devices that support multiple users.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.os.UserManager
*/
public static final String USER_SERVICE = "user";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.content.pm.LauncherApps} for querying and monitoring launchable apps across
* profiles of a user.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.content.pm.LauncherApps
*/
public static final String LAUNCHER_APPS_SERVICE = "launcherapps";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.content.RestrictionsManager} for retrieving application restrictions
* and requesting permissions for restricted operations.
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.content.RestrictionsManager
*/
public static final String RESTRICTIONS_SERVICE = "restrictions";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.app.AppOpsManager} for tracking application operations
* on the device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.AppOpsManager
*/
public static final String APP_OPS_SERVICE = "appops";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.camera2.CameraManager} for interacting with
* camera devices.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.camera2.CameraManager
*/
public static final String CAMERA_SERVICE = "camera";
@@ -3915,51 +3939,51 @@ public abstract class Context {
* {@link android.print.PrintManager} for printing and managing
* printers and print tasks.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.print.PrintManager
*/
public static final String PRINT_SERVICE = "print";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.companion.CompanionDeviceManager} for managing companion devices
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.companion.CompanionDeviceManager
*/
public static final String COMPANION_DEVICE_SERVICE = "companiondevice";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.ConsumerIrManager} for transmitting infrared
* signals from the device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.ConsumerIrManager
*/
public static final String CONSUMER_IR_SERVICE = "consumer_ir";
/**
* {@link android.app.trust.TrustManager} for managing trust agents.
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.trust.TrustManager
* @hide
*/
public static final String TRUST_SERVICE = "trust";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.media.tv.TvInputManager} for interacting with TV inputs
* on the device.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.media.tv.TvInputManager
*/
public static final String TV_INPUT_SERVICE = "tv_input";
/**
* {@link android.net.NetworkScoreManager} for managing network scoring.
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.net.NetworkScoreManager
* @hide
*/
@@ -3967,29 +3991,29 @@ public abstract class Context {
public static final String NETWORK_SCORE_SERVICE = "network_score";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.app.usage.UsageStatsManager} for querying device usage stats.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.usage.UsageStatsManager
*/
public static final String USAGE_STATS_SERVICE = "usagestats";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.app.job.JobScheduler} instance for managing occasional
* background tasks.
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.app.job.JobScheduler
*/
public static final String JOB_SCHEDULER_SERVICE = "jobscheduler";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.service.persistentdata.PersistentDataBlockManager} instance
* for interacting with a storage device that lives across factory resets.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.service.persistentdata.PersistentDataBlockManager
* @hide
*/
@@ -3997,10 +4021,10 @@ public abstract class Context {
public static final String PERSISTENT_DATA_BLOCK_SERVICE = "persistent_data_block";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.service.oemlock.OemLockManager} instance for managing the OEM lock.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.service.oemlock.OemLockManager
* @hide
*/
@@ -4008,54 +4032,54 @@ public abstract class Context {
public static final String OEM_LOCK_SERVICE = "oem_lock";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.media.projection.MediaProjectionManager} instance for managing
* media projection sessions.
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.media.projection.MediaProjectionManager
*/
public static final String MEDIA_PROJECTION_SERVICE = "media_projection";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.media.midi.MidiManager} for accessing the MIDI service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String MIDI_SERVICE = "midi";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.hardware.radio.RadioManager} for accessing the broadcast radio service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @hide
*/
public static final String RADIO_SERVICE = "broadcastradio";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.HardwarePropertiesManager} for accessing the hardware properties service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String HARDWARE_PROPERTIES_SERVICE = "hardware_properties";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.content.pm.ShortcutManager} for accessing the launcher shortcut service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.content.pm.ShortcutManager
*/
public static final String SHORTCUT_SERVICE = "shortcut";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.hardware.location.ContextHubManager} for accessing context hubs.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.hardware.location.ContextHubManager
*
* @hide
@@ -4064,11 +4088,11 @@ public abstract class Context {
public static final String CONTEXTHUB_SERVICE = "contexthub";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.health.SystemHealthManager} for accessing system health (battery, power,
* memory, etc) metrics.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String SYSTEM_HEALTH_SERVICE = "systemhealth";
@@ -4097,46 +4121,46 @@ public abstract class Context {
public static final String STATS_COMPANION_SERVICE = "statscompanion";
/**
- * Use with {@link #getSystemService} to retrieve an {@link android.stats.StatsManager}.
+ * Use with {@link #getSystemService(String)} to retrieve an {@link android.app.StatsManager}.
* @hide
*/
@SystemApi
public static final String STATS_MANAGER = "stats";
/**
- * Use with {@link #getSystemService} to retrieve a {@link
+ * Use with {@link #getSystemService(String)} to retrieve a {@link
* android.content.om.OverlayManager} for managing overlay packages.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @see android.content.om.OverlayManager
* @hide
*/
public static final String OVERLAY_SERVICE = "overlay";
/**
- * Use with {@link #getSystemService} to retrieve a
+ * Use with {@link #getSystemService(String)} to retrieve a
* {@link VrManager} for accessing the VR service.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
* @hide
*/
@SystemApi
public static final String VR_SERVICE = "vrmanager";
/**
- * Use with {@link #getSystemService} to retrieve an
+ * Use with {@link #getSystemService(String)} to retrieve an
* {@link android.app.timezone.ITimeZoneRulesManager}.
* @hide
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String TIME_ZONE_RULES_MANAGER_SERVICE = "timezone";
/**
- * Use with {@link #getSystemService} to retrieve a
- * {@link android.content.pm.crossprofile.CrossProfileApps} for cross profile operations.
+ * Use with {@link #getSystemService(String)} to retrieve a
+ * {@link android.content.pm.CrossProfileApps} for cross profile operations.
*
- * @see #getSystemService
+ * @see #getSystemService(String)
*/
public static final String CROSS_PROFILE_APPS_SERVICE = "crossprofileapps";
diff --git a/android/content/Intent.java b/android/content/Intent.java
index e940769a..acbdf142 100644
--- a/android/content/Intent.java
+++ b/android/content/Intent.java
@@ -3516,7 +3516,10 @@ public class Intent implements Parcelable, Cloneable {
* For more details see TelephonyIntents.ACTION_SIM_STATE_CHANGED. This is here
* because TelephonyIntents is an internal class.
* @hide
+ * @deprecated Use {@link #ACTION_SIM_CARD_STATE_CHANGED} or
+ * {@link #ACTION_SIM_APPLICATION_STATE_CHANGED}
*/
+ @Deprecated
@SystemApi
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public static final String ACTION_SIM_STATE_CHANGED = "android.intent.action.SIM_STATE_CHANGED";
@@ -3927,6 +3930,14 @@ public class Intent implements Parcelable, Cloneable {
@SdkConstant(SdkConstantType.INTENT_CATEGORY)
public static final String CATEGORY_LEANBACK_LAUNCHER = "android.intent.category.LEANBACK_LAUNCHER";
/**
+ * Indicates the preferred entry-point activity when an application is launched from a Car
+ * launcher. If not present, Car launcher can optionally use {@link #CATEGORY_LAUNCHER} as a
+ * fallback, or exclude the application entirely.
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.INTENT_CATEGORY)
+ public static final String CATEGORY_CAR_LAUNCHER = "android.intent.category.CAR_LAUNCHER";
+ /**
* Indicates a Leanback settings activity to be displayed in the Leanback launcher.
* @hide
*/
@@ -9410,6 +9421,12 @@ public class Intent implements Parcelable, Cloneable {
}
/** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ // Same input parameters that toString() gives to toShortString().
+ writeToProto(proto, fieldId, true, true, true, false);
+ }
+
+ /** @hide */
public void writeToProto(ProtoOutputStream proto, long fieldId, boolean secure, boolean comp,
boolean extras, boolean clip) {
long token = proto.start(fieldId);
diff --git a/android/content/ServiceConnection.java b/android/content/ServiceConnection.java
index c16dbbe3..21398f6e 100644
--- a/android/content/ServiceConnection.java
+++ b/android/content/ServiceConnection.java
@@ -31,6 +31,11 @@ public interface ServiceConnection {
* the {@link android.os.IBinder} of the communication channel to the
* Service.
*
+ * <p class="note"><b>Note:</b> If the system has started to bind your
+ * client app to a service, it's possible that your app will never receive
+ * this callback. Your app won't receive a callback if there's an issue with
+ * the service, such as the service crashing while being created.
+ *
* @param name The concrete component name of the service that has
* been connected.
*
diff --git a/android/content/pm/ApplicationInfo.java b/android/content/pm/ApplicationInfo.java
index 15e119b2..746a0902 100644
--- a/android/content/pm/ApplicationInfo.java
+++ b/android/content/pm/ApplicationInfo.java
@@ -26,7 +26,6 @@ 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;
@@ -35,6 +34,7 @@ import android.os.storage.StorageManager;
import android.text.TextUtils;
import android.util.Printer;
import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.util.ArrayUtils;
@@ -1184,6 +1184,105 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
super.dumpBack(pw, prefix);
}
+ /** {@hide} */
+ public void writeToProto(ProtoOutputStream proto, long fieldId, int dumpFlags) {
+ long token = proto.start(fieldId);
+ super.writeToProto(proto, ApplicationInfoProto.PACKAGE);
+ proto.write(ApplicationInfoProto.PERMISSION, permission);
+ proto.write(ApplicationInfoProto.PROCESS_NAME, processName);
+ proto.write(ApplicationInfoProto.UID, uid);
+ proto.write(ApplicationInfoProto.FLAGS, flags);
+ proto.write(ApplicationInfoProto.PRIVATE_FLAGS, privateFlags);
+ proto.write(ApplicationInfoProto.THEME, theme);
+ proto.write(ApplicationInfoProto.SOURCE_DIR, sourceDir);
+ if (!Objects.equals(sourceDir, publicSourceDir)) {
+ proto.write(ApplicationInfoProto.PUBLIC_SOURCE_DIR, publicSourceDir);
+ }
+ if (!ArrayUtils.isEmpty(splitSourceDirs)) {
+ for (String dir : splitSourceDirs) {
+ proto.write(ApplicationInfoProto.SPLIT_SOURCE_DIRS, dir);
+ }
+ }
+ if (!ArrayUtils.isEmpty(splitPublicSourceDirs)
+ && !Arrays.equals(splitSourceDirs, splitPublicSourceDirs)) {
+ for (String dir : splitPublicSourceDirs) {
+ proto.write(ApplicationInfoProto.SPLIT_PUBLIC_SOURCE_DIRS, dir);
+ }
+ }
+ if (resourceDirs != null) {
+ for (String dir : resourceDirs) {
+ proto.write(ApplicationInfoProto.RESOURCE_DIRS, dir);
+ }
+ }
+ proto.write(ApplicationInfoProto.DATA_DIR, dataDir);
+ proto.write(ApplicationInfoProto.CLASS_LOADER_NAME, classLoaderName);
+ if (!ArrayUtils.isEmpty(splitClassLoaderNames)) {
+ for (String name : splitClassLoaderNames) {
+ proto.write(ApplicationInfoProto.SPLIT_CLASS_LOADER_NAMES, name);
+ }
+ }
+
+ long versionToken = proto.start(ApplicationInfoProto.VERSION);
+ proto.write(ApplicationInfoProto.Version.ENABLED, enabled);
+ proto.write(ApplicationInfoProto.Version.MIN_SDK_VERSION, minSdkVersion);
+ proto.write(ApplicationInfoProto.Version.TARGET_SDK_VERSION, targetSdkVersion);
+ proto.write(ApplicationInfoProto.Version.VERSION_CODE, versionCode);
+ proto.write(ApplicationInfoProto.Version.TARGET_SANDBOX_VERSION, targetSandboxVersion);
+ proto.end(versionToken);
+
+ if ((dumpFlags & DUMP_FLAG_DETAILS) != 0) {
+ long detailToken = proto.start(ApplicationInfoProto.DETAIL);
+ if (className != null) {
+ proto.write(ApplicationInfoProto.Detail.CLASS_NAME, className);
+ }
+ proto.write(ApplicationInfoProto.Detail.TASK_AFFINITY, taskAffinity);
+ proto.write(ApplicationInfoProto.Detail.REQUIRES_SMALLEST_WIDTH_DP,
+ requiresSmallestWidthDp);
+ proto.write(ApplicationInfoProto.Detail.COMPATIBLE_WIDTH_LIMIT_DP,
+ compatibleWidthLimitDp);
+ proto.write(ApplicationInfoProto.Detail.LARGEST_WIDTH_LIMIT_DP,
+ largestWidthLimitDp);
+ if (seInfo != null) {
+ proto.write(ApplicationInfoProto.Detail.SEINFO, seInfo);
+ proto.write(ApplicationInfoProto.Detail.SEINFO_USER, seInfoUser);
+ }
+ proto.write(ApplicationInfoProto.Detail.DEVICE_PROTECTED_DATA_DIR,
+ deviceProtectedDataDir);
+ proto.write(ApplicationInfoProto.Detail.CREDENTIAL_PROTECTED_DATA_DIR,
+ credentialProtectedDataDir);
+ if (sharedLibraryFiles != null) {
+ for (String f : sharedLibraryFiles) {
+ proto.write(ApplicationInfoProto.Detail.SHARED_LIBRARY_FILES, f);
+ }
+ }
+ if (manageSpaceActivityName != null) {
+ proto.write(ApplicationInfoProto.Detail.MANAGE_SPACE_ACTIVITY_NAME,
+ manageSpaceActivityName);
+ }
+ if (descriptionRes != 0) {
+ proto.write(ApplicationInfoProto.Detail.DESCRIPTION_RES, descriptionRes);
+ }
+ if (uiOptions != 0) {
+ proto.write(ApplicationInfoProto.Detail.UI_OPTIONS, uiOptions);
+ }
+ proto.write(ApplicationInfoProto.Detail.SUPPORTS_RTL, hasRtlSupport());
+ if (fullBackupContent > 0) {
+ proto.write(ApplicationInfoProto.Detail.CONTENT, "@xml/" + fullBackupContent);
+ } else {
+ proto.write(ApplicationInfoProto.Detail.IS_FULL_BACKUP, fullBackupContent == 0);
+ }
+ if (networkSecurityConfigRes != 0) {
+ proto.write(ApplicationInfoProto.Detail.NETWORK_SECURITY_CONFIG_RES,
+ networkSecurityConfigRes);
+ }
+ if (category != CATEGORY_UNDEFINED) {
+ proto.write(ApplicationInfoProto.Detail.CATEGORY, category);
+ }
+ proto.end(detailToken);
+ }
+ proto.end(token);
+ }
+
/**
* @return true if "supportsRtl" has been set to true in the AndroidManifest
* @hide
@@ -1492,6 +1591,13 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
/**
* @hide
*/
+ public boolean isAllowedToUseHiddenApi() {
+ return isSystemApp();
+ }
+
+ /**
+ * @hide
+ */
@Override
public Drawable loadDefaultIcon(PackageManager pm) {
if ((flags & FLAG_EXTERNAL_STORAGE) != 0
@@ -1593,11 +1699,6 @@ public class ApplicationInfo extends PackageItemInfo implements Parcelable {
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/crossprofile/CrossProfileApps.java b/android/content/pm/CrossProfileApps.java
index 414c1389..7d5d6090 100644
--- a/android/content/pm/crossprofile/CrossProfileApps.java
+++ b/android/content/pm/CrossProfileApps.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.content.pm.crossprofile;
+package android.content.pm;
import android.annotation.NonNull;
import android.content.ComponentName;
@@ -57,13 +57,14 @@ public class CrossProfileApps {
* action {@link android.content.Intent#ACTION_MAIN}, category
* {@link android.content.Intent#CATEGORY_LAUNCHER}. Otherwise, SecurityException will
* be thrown.
- * @param user The UserHandle of the profile, must be one of the users returned by
+ * @param targetUser The UserHandle of the profile, must be one of the users returned by
* {@link #getTargetUserProfiles()}, otherwise a {@link SecurityException} will
* be thrown.
*/
- public void startMainActivity(@NonNull ComponentName component, @NonNull UserHandle user) {
+ public void startMainActivity(@NonNull ComponentName component,
+ @NonNull UserHandle targetUser) {
try {
- mService.startActivityAsUser(mContext.getPackageName(), component, user);
+ mService.startActivityAsUser(mContext.getPackageName(), component, targetUser);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
@@ -114,7 +115,7 @@ public class CrossProfileApps {
}
/**
- * Return an icon that calling app can show to user for the semantic of profile switching --
+ * Return a drawable 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.
*
@@ -124,9 +125,9 @@ public class CrossProfileApps {
* @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)
+ * @see #startMainActivity(ComponentName, UserHandle)
*/
- public @NonNull Drawable getProfileSwitchingIcon(@NonNull UserHandle userHandle) {
+ public @NonNull Drawable getProfileSwitchingIconDrawable(@NonNull UserHandle userHandle) {
verifyCanAccessUser(userHandle);
final boolean isManagedProfile =
diff --git a/android/content/pm/OrgApacheHttpLegacyUpdater.java b/android/content/pm/OrgApacheHttpLegacyUpdater.java
new file mode 100644
index 00000000..81041e9d
--- /dev/null
+++ b/android/content/pm/OrgApacheHttpLegacyUpdater.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.content.pm;
+
+import android.content.pm.PackageParser.Package;
+import android.os.Build;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+
+/**
+ * Updates a package to ensure that if it targets < P that the org.apache.http.legacy library is
+ * included by default.
+ *
+ * <p>This is separated out so that it can be conditionally included at build time depending on
+ * whether org.apache.http.legacy is on the bootclasspath or not. In order to include this at
+ * build time, and remove org.apache.http.legacy from the bootclasspath pass
+ * REMOVE_OAHL_FROM_BCP=true on the build command line, otherwise this class will not be included
+ * and the
+ *
+ * @hide
+ */
+@VisibleForTesting
+public class OrgApacheHttpLegacyUpdater extends PackageSharedLibraryUpdater {
+
+ private static final String APACHE_HTTP_LEGACY = "org.apache.http.legacy";
+
+ @Override
+ public void updatePackage(Package pkg) {
+ ArrayList<String> usesLibraries = pkg.usesLibraries;
+ ArrayList<String> usesOptionalLibraries = pkg.usesOptionalLibraries;
+
+ // Packages targeted at <= O_MR1 expect the classes in the org.apache.http.legacy library
+ // to be accessible so this maintains backward compatibility by adding the
+ // org.apache.http.legacy library to those packages.
+ if (apkTargetsApiLevelLessThanOrEqualToOMR1(pkg)) {
+ boolean apacheHttpLegacyPresent = isLibraryPresent(
+ usesLibraries, usesOptionalLibraries, APACHE_HTTP_LEGACY);
+ if (!apacheHttpLegacyPresent) {
+ usesLibraries = prefix(usesLibraries, APACHE_HTTP_LEGACY);
+ }
+ }
+
+ pkg.usesLibraries = usesLibraries;
+ pkg.usesOptionalLibraries = usesOptionalLibraries;
+ }
+
+ private static boolean apkTargetsApiLevelLessThanOrEqualToOMR1(Package pkg) {
+ int targetSdkVersion = pkg.applicationInfo.targetSdkVersion;
+ return targetSdkVersion <= Build.VERSION_CODES.O_MR1;
+ }
+}
diff --git a/android/content/pm/PackageBackwardCompatibility.java b/android/content/pm/PackageBackwardCompatibility.java
index cee25994..9bdb78be 100644
--- a/android/content/pm/PackageBackwardCompatibility.java
+++ b/android/content/pm/PackageBackwardCompatibility.java
@@ -17,12 +17,13 @@
package android.content.pm;
import android.content.pm.PackageParser.Package;
-import android.os.Build;
+import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import java.util.ArrayList;
+import java.util.List;
/**
* Modifies {@link Package} in order to maintain backwards compatibility.
@@ -30,13 +31,60 @@ import java.util.ArrayList;
* @hide
*/
@VisibleForTesting
-public class PackageBackwardCompatibility {
+public class PackageBackwardCompatibility extends PackageSharedLibraryUpdater {
+
+ private static final String TAG = PackageBackwardCompatibility.class.getSimpleName();
private static final String ANDROID_TEST_MOCK = "android.test.mock";
private static final String ANDROID_TEST_RUNNER = "android.test.runner";
- private static final String APACHE_HTTP_LEGACY = "org.apache.http.legacy";
+ private static final PackageBackwardCompatibility INSTANCE;
+
+ static {
+ String className = "android.content.pm.OrgApacheHttpLegacyUpdater";
+ Class<? extends PackageSharedLibraryUpdater> clazz;
+ try {
+ clazz = (PackageBackwardCompatibility.class.getClassLoader()
+ .loadClass(className)
+ .asSubclass(PackageSharedLibraryUpdater.class));
+ } catch (ClassNotFoundException e) {
+ Log.i(TAG, "Could not find " + className + ", ignoring");
+ clazz = null;
+ }
+
+ boolean hasOrgApacheHttpLegacy = false;
+ final List<PackageSharedLibraryUpdater> packageUpdaters = new ArrayList<>();
+ if (clazz == null) {
+ // Add an updater that will remove any references to org.apache.http.library from the
+ // package so that it does not try and load the library when it is on the
+ // bootclasspath.
+ packageUpdaters.add(new RemoveUnnecessaryOrgApacheHttpLegacyLibrary());
+ } else {
+ try {
+ packageUpdaters.add(clazz.getConstructor().newInstance());
+ hasOrgApacheHttpLegacy = true;
+ } catch (ReflectiveOperationException e) {
+ throw new IllegalStateException("Could not create instance of " + className, e);
+ }
+ }
+
+ packageUpdaters.add(new AndroidTestRunnerSplitUpdater());
+
+ PackageSharedLibraryUpdater[] updaterArray = packageUpdaters
+ .toArray(new PackageSharedLibraryUpdater[0]);
+ INSTANCE = new PackageBackwardCompatibility(hasOrgApacheHttpLegacy, updaterArray);
+ }
+
+ private final boolean mRemovedOAHLFromBCP;
+
+ private final PackageSharedLibraryUpdater[] mPackageUpdaters;
+
+ public PackageBackwardCompatibility(boolean removedOAHLFromBCP,
+ PackageSharedLibraryUpdater[] packageUpdaters) {
+ this.mRemovedOAHLFromBCP = removedOAHLFromBCP;
+ this.mPackageUpdaters = packageUpdaters;
+ }
/**
* Modify the shared libraries in the supplied {@link Package} to maintain backwards
@@ -46,44 +94,74 @@ public class PackageBackwardCompatibility {
*/
@VisibleForTesting
public static void modifySharedLibraries(Package pkg) {
- ArrayList<String> usesLibraries = pkg.usesLibraries;
- ArrayList<String> usesOptionalLibraries = pkg.usesOptionalLibraries;
-
- // Packages targeted at <= O_MR1 expect the classes in the org.apache.http.legacy library
- // to be accessible so this maintains backward compatibility by adding the
- // org.apache.http.legacy library to those packages.
- if (apkTargetsApiLevelLessThanOrEqualToOMR1(pkg)) {
- boolean apacheHttpLegacyPresent = isLibraryPresent(
- usesLibraries, usesOptionalLibraries, APACHE_HTTP_LEGACY);
- if (!apacheHttpLegacyPresent) {
- usesLibraries = ArrayUtils.add(usesLibraries, APACHE_HTTP_LEGACY);
- }
- }
+ INSTANCE.updatePackage(pkg);
+ }
- // android.test.runner has a dependency on android.test.mock so if android.test.runner
- // is present but android.test.mock is not then add android.test.mock.
- boolean androidTestMockPresent = isLibraryPresent(
- usesLibraries, usesOptionalLibraries, ANDROID_TEST_MOCK);
- if (ArrayUtils.contains(usesLibraries, ANDROID_TEST_RUNNER) && !androidTestMockPresent) {
- usesLibraries.add(ANDROID_TEST_MOCK);
- }
- if (ArrayUtils.contains(usesOptionalLibraries, ANDROID_TEST_RUNNER)
- && !androidTestMockPresent) {
- usesOptionalLibraries.add(ANDROID_TEST_MOCK);
+ @Override
+ public void updatePackage(Package pkg) {
+
+ for (PackageSharedLibraryUpdater packageUpdater : mPackageUpdaters) {
+ packageUpdater.updatePackage(pkg);
}
+ }
- pkg.usesLibraries = usesLibraries;
- pkg.usesOptionalLibraries = usesOptionalLibraries;
+ /**
+ * True if the org.apache.http.legacy has been removed the bootclasspath, false otherwise.
+ */
+ public static boolean removeOAHLFromBCP() {
+ return INSTANCE.mRemovedOAHLFromBCP;
}
- private static boolean apkTargetsApiLevelLessThanOrEqualToOMR1(Package pkg) {
- int targetSdkVersion = pkg.applicationInfo.targetSdkVersion;
- return targetSdkVersion <= Build.VERSION_CODES.O_MR1;
+ /**
+ * Add android.test.mock dependency for any APK that depends on android.test.runner.
+ *
+ * <p>This is needed to maintain backwards compatibility as in previous versions of Android the
+ * android.test.runner library included the classes from android.test.mock which have since
+ * been split out into a separate library.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static class AndroidTestRunnerSplitUpdater extends PackageSharedLibraryUpdater {
+
+ @Override
+ public void updatePackage(Package pkg) {
+ ArrayList<String> usesLibraries = pkg.usesLibraries;
+ ArrayList<String> usesOptionalLibraries = pkg.usesOptionalLibraries;
+
+ // android.test.runner has a dependency on android.test.mock so if android.test.runner
+ // is present but android.test.mock is not then add android.test.mock.
+ boolean androidTestMockPresent = isLibraryPresent(
+ usesLibraries, usesOptionalLibraries, ANDROID_TEST_MOCK);
+ if (ArrayUtils.contains(usesLibraries, ANDROID_TEST_RUNNER)
+ && !androidTestMockPresent) {
+ usesLibraries.add(ANDROID_TEST_MOCK);
+ }
+ if (ArrayUtils.contains(usesOptionalLibraries, ANDROID_TEST_RUNNER)
+ && !androidTestMockPresent) {
+ usesOptionalLibraries.add(ANDROID_TEST_MOCK);
+ }
+
+ pkg.usesLibraries = usesLibraries;
+ pkg.usesOptionalLibraries = usesOptionalLibraries;
+ }
}
- private static boolean isLibraryPresent(ArrayList<String> usesLibraries,
- ArrayList<String> usesOptionalLibraries, String apacheHttpLegacy) {
- return ArrayUtils.contains(usesLibraries, apacheHttpLegacy)
- || ArrayUtils.contains(usesOptionalLibraries, apacheHttpLegacy);
+ /**
+ * Remove any usages of org.apache.http.legacy from the shared library as the library is on the
+ * bootclasspath.
+ */
+ @VisibleForTesting
+ public static class RemoveUnnecessaryOrgApacheHttpLegacyLibrary
+ extends PackageSharedLibraryUpdater {
+
+ private static final String APACHE_HTTP_LEGACY = "org.apache.http.legacy";
+
+ @Override
+ public void updatePackage(Package pkg) {
+ pkg.usesLibraries = ArrayUtils.remove(pkg.usesLibraries, APACHE_HTTP_LEGACY);
+ pkg.usesOptionalLibraries =
+ ArrayUtils.remove(pkg.usesOptionalLibraries, APACHE_HTTP_LEGACY);
+ }
}
}
diff --git a/android/content/pm/PackageInfo.java b/android/content/pm/PackageInfo.java
index 5a91e947..09a46b8a 100644
--- a/android/content/pm/PackageInfo.java
+++ b/android/content/pm/PackageInfo.java
@@ -16,14 +16,10 @@
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.
@@ -246,9 +242,44 @@ public class PackageInfo implements Parcelable {
* equivalent to being signed with certificates B and A. This means that
* in case multiple signatures are reported you cannot assume the one at
* the first position to be the same across updates.
+ *
+ * <strong>Deprecated</strong> This has been replaced by the
+ * {@link PackageInfo#signingCertificateHistory} field, which takes into
+ * account signing certificate rotation. For backwards compatibility in
+ * the event of signing certificate rotation, this will return the oldest
+ * reported signing certificate, so that an application will appear to
+ * callers as though no rotation occurred.
+ *
+ * @deprecated use {@code signingCertificateHistory} instead
*/
+ @Deprecated
public Signature[] signatures;
-
+
+ /**
+ * Array of all signatures arrays read from the package file, potentially
+ * including past signing certificates no longer used after signing
+ * certificate rotation. Though signing certificate rotation is only
+ * available for apps with a single signing certificate, this provides an
+ * array of arrays so that packages signed with multiple signing
+ * certificates can still return all signers. This is only filled in if
+ * the flag {@link PackageManager#GET_SIGNING_CERTIFICATES} was set.
+ *
+ * A package must be singed with at least one certificate, which is at
+ * position zero in the array. An application may be signed by multiple
+ * certificates, which would be in the array at position zero in an
+ * indeterminate order. A package may also have a history of certificates
+ * due to signing certificate rotation. In this case, the array will be
+ * populated by a series of single-entry arrays corresponding to a signing
+ * certificate of the package.
+ *
+ * <strong>Note:</strong> Signature ordering is not guaranteed to be
+ * stable which means that a package signed with certificates A and B is
+ * equivalent to being signed with certificates B and A. This means that
+ * in case multiple signatures are reported you cannot assume the one at
+ * the first position will be the same across updates.
+ */
+ public Signature[][] signingCertificateHistory;
+
/**
* Application specified preferred configuration
* {@link android.R.styleable#AndroidManifestUsesConfiguration
@@ -335,28 +366,9 @@ public class PackageInfo implements Parcelable {
public int overlayPriority;
/**
- * 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}.
+ * Whether the overlay is static, meaning it cannot be enabled/disabled at runtime.
*/
- @OverlayFlags int mOverlayFlags;
+ boolean mOverlayIsStatic;
/**
* The user-visible SDK version (ex. 26) of the framework against which the application claims
@@ -389,7 +401,7 @@ public class PackageInfo implements Parcelable {
* @hide
*/
public boolean isOverlayPackage() {
- return overlayTarget != null && (mOverlayFlags & FLAG_OVERLAY_TRUSTED) != 0;
+ return overlayTarget != null;
}
/**
@@ -398,7 +410,7 @@ public class PackageInfo implements Parcelable {
* @hide
*/
public boolean isStaticOverlayPackage() {
- return overlayTarget != null && (mOverlayFlags & FLAG_OVERLAY_STATIC) != 0;
+ return overlayTarget != null && mOverlayIsStatic;
}
@Override
@@ -453,7 +465,7 @@ public class PackageInfo implements Parcelable {
dest.writeString(requiredAccountType);
dest.writeString(overlayTarget);
dest.writeInt(overlayPriority);
- dest.writeInt(mOverlayFlags);
+ dest.writeBoolean(mOverlayIsStatic);
dest.writeInt(compileSdkVersion);
dest.writeString(compileSdkVersionCodename);
}
@@ -508,7 +520,7 @@ public class PackageInfo implements Parcelable {
requiredAccountType = source.readString();
overlayTarget = source.readString();
overlayPriority = source.readInt();
- mOverlayFlags = source.readInt();
+ mOverlayIsStatic = source.readBoolean();
compileSdkVersion = source.readInt();
compileSdkVersionCodename = source.readString();
diff --git a/android/content/pm/PackageInstaller.java b/android/content/pm/PackageInstaller.java
index 77c5743f..df677d20 100644
--- a/android/content/pm/PackageInstaller.java
+++ b/android/content/pm/PackageInstaller.java
@@ -324,7 +324,14 @@ public class PackageInstaller {
*/
public int createSession(@NonNull SessionParams params) throws IOException {
try {
- return mInstaller.createSession(params, mInstallerPackageName, mUserId);
+ final String installerPackage;
+ if (params.installerPackageName == null) {
+ installerPackage = mInstallerPackageName;
+ } else {
+ installerPackage = params.installerPackageName;
+ }
+
+ return mInstaller.createSession(params, installerPackage, mUserId);
} catch (RuntimeException e) {
ExceptionUtils.maybeUnwrapIOException(e);
throw e;
@@ -1081,6 +1088,8 @@ public class PackageInstaller {
public String volumeUuid;
/** {@hide} */
public String[] grantedRuntimePermissions;
+ /** {@hide} */
+ public String installerPackageName;
/**
* Construct parameters for a new package install session.
@@ -1109,6 +1118,7 @@ public class PackageInstaller {
abiOverride = source.readString();
volumeUuid = source.readString();
grantedRuntimePermissions = source.readStringArray();
+ installerPackageName = source.readString();
}
/**
@@ -1304,6 +1314,18 @@ public class PackageInstaller {
}
}
+ /**
+ * Set the installer package for the app.
+ *
+ * By default this is the app that created the {@link PackageInstaller} object.
+ *
+ * @param installerPackageName name of the installer package
+ * {@hide}
+ */
+ public void setInstallerPackageName(String installerPackageName) {
+ this.installerPackageName = installerPackageName;
+ }
+
/** {@hide} */
public void dump(IndentingPrintWriter pw) {
pw.printPair("mode", mode);
@@ -1319,6 +1341,7 @@ public class PackageInstaller {
pw.printPair("abiOverride", abiOverride);
pw.printPair("volumeUuid", volumeUuid);
pw.printPair("grantedRuntimePermissions", grantedRuntimePermissions);
+ pw.printPair("installerPackageName", installerPackageName);
pw.println();
}
@@ -1343,6 +1366,7 @@ public class PackageInstaller {
dest.writeString(abiOverride);
dest.writeString(volumeUuid);
dest.writeStringArray(grantedRuntimePermissions);
+ dest.writeString(installerPackageName);
}
public static final Parcelable.Creator<SessionParams>
diff --git a/android/content/pm/PackageItemInfo.java b/android/content/pm/PackageItemInfo.java
index 11830c29..2c0c6ad0 100644
--- a/android/content/pm/PackageItemInfo.java
+++ b/android/content/pm/PackageItemInfo.java
@@ -19,7 +19,6 @@ package android.content.pm;
import android.annotation.NonNull;
import android.annotation.SystemApi;
import android.content.res.XmlResourceParser;
-
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcel;
@@ -28,6 +27,8 @@ import android.text.Html;
import android.text.TextPaint;
import android.text.TextUtils;
import android.util.Printer;
+import android.util.proto.ProtoOutputStream;
+
import java.text.Collator;
import java.util.Comparator;
@@ -386,6 +387,24 @@ public class PackageItemInfo {
dest.writeInt(showUserIcon);
}
+ /**
+ * @hide
+ */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ long token = proto.start(fieldId);
+ if (name != null) {
+ proto.write(PackageItemInfoProto.NAME, name);
+ }
+ proto.write(PackageItemInfoProto.PACKAGE_NAME, packageName);
+ if (labelRes != 0 || nonLocalizedLabel != null || icon != 0 || banner != 0) {
+ proto.write(PackageItemInfoProto.LABEL_RES, labelRes);
+ proto.write(PackageItemInfoProto.NON_LOCALIZED_LABEL, nonLocalizedLabel.toString());
+ proto.write(PackageItemInfoProto.ICON, icon);
+ proto.write(PackageItemInfoProto.BANNER, banner);
+ }
+ proto.end(token);
+ }
+
protected PackageItemInfo(Parcel source) {
name = source.readString();
packageName = source.readString();
diff --git a/android/content/pm/PackageManager.java b/android/content/pm/PackageManager.java
index 2d726329..df69d803 100644
--- a/android/content/pm/PackageManager.java
+++ b/android/content/pm/PackageManager.java
@@ -47,7 +47,6 @@ import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
-import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Bundle;
@@ -134,6 +133,7 @@ public abstract class PackageManager {
GET_SERVICES,
GET_SHARED_LIBRARY_FILES,
GET_SIGNATURES,
+ GET_SIGNING_CERTIFICATES,
GET_URI_PERMISSION_PATTERNS,
MATCH_UNINSTALLED_PACKAGES,
MATCH_DISABLED_COMPONENTS,
@@ -273,7 +273,10 @@ public abstract class PackageManager {
/**
* {@link PackageInfo} flag: return information about the
* signatures included in the package.
+ *
+ * @deprecated use {@code GET_SIGNING_CERTIFICATES} instead
*/
+ @Deprecated
public static final int GET_SIGNATURES = 0x00000040;
/**
@@ -489,6 +492,14 @@ public abstract class PackageManager {
public static final int MATCH_STATIC_SHARED_LIBRARIES = 0x04000000;
/**
+ * {@link PackageInfo} flag: return the signing certificates associated with
+ * this package. Each entry is a signing certificate that the package
+ * has proven it is authorized to use, usually a past signing certificate from
+ * which it has rotated.
+ */
+ public static final int GET_SIGNING_CERTIFICATES = 0x08000000;
+
+ /**
* Internal flag used to indicate that a system component has done their
* homework and verified that they correctly handle packages and components
* that come and go over time. In particular:
@@ -788,7 +799,8 @@ public abstract class PackageManager {
/**
* Flag parameter for {@link #installPackage} to indicate that this package is an
- * upgrade to a package that refers to the SDK via release letter.
+ * upgrade to a package that refers to the SDK via release letter or is targeting an SDK via
+ * release letter that the current build does not support.
*
* @hide
*/
@@ -1297,6 +1309,15 @@ public abstract class PackageManager {
*/
public static final int INSTALL_FAILED_INSTANT_APP_INVALID = -116;
+ /**
+ * Installation parse return code: this is passed in the
+ * {@link PackageInstaller#EXTRA_LEGACY_STATUS} if the dex metadata file is invalid or
+ * if there was no matching apk file for a dex metadata file.
+ *
+ * @hide
+ */
+ public static final int INSTALL_FAILED_BAD_DEX_METADATA = -117;
+
/** @hide */
@IntDef(flag = true, prefix = { "DELETE_" }, value = {
DELETE_KEEP_DATA,
@@ -1745,6 +1766,16 @@ public abstract class PackageManager {
"android.hardware.camera.capability.raw";
/**
+ * Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}: At least one
+ * of the cameras on the device supports the
+ * {@link android.hardware.camera2.CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING
+ * MOTION_TRACKING} capability level.
+ */
+ @SdkConstant(SdkConstantType.FEATURE)
+ public static final String FEATURE_CAMERA_AR =
+ "android.hardware.camera.ar";
+
+ /**
* Feature for {@link #getSystemAvailableFeatures} and
* {@link #hasSystemFeature}: The device is capable of communicating with
* consumer IR devices.
@@ -2328,8 +2359,6 @@ public abstract class PackageManager {
/**
* Feature for {@link #getSystemAvailableFeatures} and
* {@link #hasSystemFeature}: The device supports Wi-Fi RTT (IEEE 802.11mc).
- *
- * @hide RTT_API
*/
@SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_WIFI_RTT = "android.hardware.wifi.rtt";
@@ -2480,10 +2509,17 @@ public abstract class PackageManager {
= "android.software.securely_removes_users";
/** {@hide} */
+ @TestApi
@SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_FILE_BASED_ENCRYPTION
= "android.software.file_based_encryption";
+ /** {@hide} */
+ @TestApi
+ @SdkConstant(SdkConstantType.FEATURE)
+ public static final String FEATURE_ADOPTABLE_STORAGE
+ = "android.software.adoptable_storage";
+
/**
* Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
* The device has a full implementation of the android.webkit.* APIs. Devices
@@ -2530,31 +2566,22 @@ public abstract class PackageManager {
* Devices declaring this feature must include an application implementing a
* {@link android.service.vr.VrListenerService} that can be targeted by VR applications via
* {@link android.app.Activity#setVrModeEnabled}.
+ * @deprecated use {@link #FEATURE_VR_MODE_HIGH_PERFORMANCE} instead.
*/
+ @Deprecated
@SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_VR_MODE = "android.software.vr.mode";
/**
* Feature for {@link #getSystemAvailableFeatures} and {@link #hasSystemFeature}:
- * The device implements {@link #FEATURE_VR_MODE} but additionally meets extra CDD requirements
- * to provide a high-quality VR experience. In general, devices declaring this feature will
- * additionally:
- * <ul>
- * <li>Deliver consistent performance at a high framerate over an extended period of time
- * for typical VR application CPU/GPU workloads with a minimal number of frame drops for VR
- * applications that have called
- * {@link android.view.Window#setSustainedPerformanceMode}.</li>
- * <li>Implement {@link #FEATURE_HIFI_SENSORS} and have a low sensor latency.</li>
- * <li>Include optimizations to lower display persistence while running VR applications.</li>
- * <li>Implement an optimized render path to minimize latency to draw to the device's main
- * display.</li>
- * <li>Include the following EGL extensions: EGL_ANDROID_create_native_client_buffer,
- * EGL_ANDROID_front_buffer_auto_refresh, EGL_EXT_protected_content,
- * EGL_KHR_mutable_render_buffer, EGL_KHR_reusable_sync, and EGL_KHR_wait_sync.</li>
- * <li>Provide at least one CPU core that is reserved for use solely by the top, foreground
- * VR application process for critical render threads while such an application is
- * running.</li>
- * </ul>
+ * The device implements an optimized mode for virtual reality (VR) applications that handles
+ * stereoscopic rendering of notifications, disables most monocular system UI components
+ * while a VR application has user focus and meets extra CDD requirements to provide a
+ * high-quality VR experience.
+ * Devices declaring this feature must include an application implementing a
+ * {@link android.service.vr.VrListenerService} that can be targeted by VR applications via
+ * {@link android.app.Activity#setVrModeEnabled}.
+ * and must meet CDD requirements to provide a high-quality VR experience.
*/
@SdkConstant(SdkConstantType.FEATURE)
public static final String FEATURE_VR_MODE_HIGH_PERFORMANCE
@@ -3039,6 +3066,21 @@ public abstract class PackageManager {
public abstract @Nullable Intent getLeanbackLaunchIntentForPackage(@NonNull String packageName);
/**
+ * Return a "good" intent to launch a front-door Car activity in a
+ * package, for use for example to implement an "open" button when browsing
+ * through packages. The current implementation will look for a main
+ * activity in the category {@link Intent#CATEGORY_CAR_LAUNCHER}, or
+ * return null if no main car activities are found.
+ *
+ * @param packageName The name of the package to inspect.
+ * @return Returns either a fully-qualified Intent that can be used to launch
+ * the main Car activity in the package, or null if the package
+ * does not contain such an activity.
+ * @hide
+ */
+ public abstract @Nullable Intent getCarLaunchIntentForPackage(@NonNull String packageName);
+
+ /**
* Return an array of all of the POSIX secondary group IDs that have been
* assigned to the given package.
* <p>
@@ -3761,7 +3803,7 @@ public abstract class PackageManager {
public abstract int getInstantAppCookieMaxBytes();
/**
- * @deprecated
+ * deprecated
* @hide
*/
public abstract int getInstantAppCookieMaxSize();
@@ -4718,17 +4760,6 @@ public abstract class PackageManager {
}
/**
- * @deprecated replaced by {@link PackageInstaller}
- * @hide
- */
- @Deprecated
- public abstract void installPackage(
- Uri packageURI,
- PackageInstallObserver observer,
- @InstallFlags int flags,
- String installerPackageName);
-
- /**
* If there is already an application with the given package name installed
* on the system for other users, also install it for the calling user.
* @hide
@@ -5633,6 +5664,8 @@ public abstract class PackageManager {
case INSTALL_FAILED_DUPLICATE_PERMISSION: return "INSTALL_FAILED_DUPLICATE_PERMISSION";
case INSTALL_FAILED_NO_MATCHING_ABIS: return "INSTALL_FAILED_NO_MATCHING_ABIS";
case INSTALL_FAILED_ABORTED: return "INSTALL_FAILED_ABORTED";
+ case INSTALL_FAILED_BAD_DEX_METADATA:
+ return "INSTALL_FAILED_BAD_DEX_METADATA";
default: return Integer.toString(status);
}
}
@@ -5677,6 +5710,7 @@ public abstract class PackageManager {
case INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID: return PackageInstaller.STATUS_FAILURE_INVALID;
case INSTALL_PARSE_FAILED_MANIFEST_MALFORMED: return PackageInstaller.STATUS_FAILURE_INVALID;
case INSTALL_PARSE_FAILED_MANIFEST_EMPTY: return PackageInstaller.STATUS_FAILURE_INVALID;
+ case INSTALL_FAILED_BAD_DEX_METADATA: return PackageInstaller.STATUS_FAILURE_INVALID;
case INSTALL_FAILED_INTERNAL_ERROR: return PackageInstaller.STATUS_FAILURE;
case INSTALL_FAILED_USER_RESTRICTED: return PackageInstaller.STATUS_FAILURE_INCOMPATIBLE;
case INSTALL_FAILED_DUPLICATE_PERMISSION: return PackageInstaller.STATUS_FAILURE_CONFLICT;
@@ -5869,4 +5903,93 @@ public abstract class PackageManager {
public @NonNull ArtManager getArtManager() {
throw new UnsupportedOperationException("getArtManager not implemented in subclass");
}
+
+ /**
+ * Sets or clears the harmful app warning details for the given app.
+ *
+ * When set, any attempt to launch an activity in this package will be intercepted and a
+ * warning dialog will be shown to the user instead, with the given warning. The user
+ * will have the option to proceed with the activity launch, or to uninstall the application.
+ *
+ * @param packageName The full name of the package to warn on.
+ * @param warning A warning string to display to the user describing the threat posed by the
+ * application, or null to clear the warning.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.SET_HARMFUL_APP_WARNINGS)
+ @SystemApi
+ public void setHarmfulAppWarning(@NonNull String packageName, @Nullable CharSequence warning) {
+ throw new UnsupportedOperationException("setHarmfulAppWarning not implemented in subclass");
+ }
+
+ /**
+ * Returns the harmful app warning string for the given app, or null if there is none set.
+ *
+ * @param packageName The full name of the desired package.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.SET_HARMFUL_APP_WARNINGS)
+ @Nullable
+ @SystemApi
+ public CharSequence getHarmfulAppWarning(@NonNull String packageName) {
+ throw new UnsupportedOperationException("getHarmfulAppWarning not implemented in subclass");
+ }
+
+ /** @hide */
+ @IntDef(prefix = { "CERT_INPUT_" }, value = {
+ CERT_INPUT_RAW_X509,
+ CERT_INPUT_SHA256
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface CertificateInputType {}
+
+ /**
+ * Certificate input bytes: the input bytes represent an encoded X.509 Certificate which could
+ * be generated using an {@code CertificateFactory}
+ */
+ public static final int CERT_INPUT_RAW_X509 = 0;
+
+ /**
+ * Certificate input bytes: the input bytes represent the SHA256 output of an encoded X.509
+ * Certificate.
+ */
+ public static final int CERT_INPUT_SHA256 = 1;
+
+ /**
+ * Searches the set of signing certificates by which the given package has proven to have been
+ * signed. This should be used instead of {@code getPackageInfo} with {@code GET_SIGNATURES}
+ * since it takes into account the possibility of signing certificate rotation, except in the
+ * case of packages that are signed by multiple certificates, for which signing certificate
+ * rotation is not supported.
+ *
+ * @param packageName package whose signing certificates to check
+ * @param certificate signing certificate for which to search
+ * @param type representation of the {@code certificate}
+ * @return true if this package was or is signed by exactly the certificate {@code certificate}
+ */
+ public boolean hasSigningCertificate(
+ String packageName, byte[] certificate, @CertificateInputType int type) {
+ throw new UnsupportedOperationException(
+ "hasSigningCertificate not implemented in subclass");
+ }
+
+ /**
+ * Searches the set of signing certificates by which the given uid has proven to have been
+ * signed. This should be used instead of {@code getPackageInfo} with {@code GET_SIGNATURES}
+ * since it takes into account the possibility of signing certificate rotation, except in the
+ * case of packages that are signed by multiple certificates, for which signing certificate
+ * rotation is not supported.
+ *
+ * @param uid package whose signing certificates to check
+ * @param certificate signing certificate for which to search
+ * @param type representation of the {@code certificate}
+ * @return true if this package was or is signed by exactly the certificate {@code certificate}
+ */
+ public boolean hasSigningCertificate(
+ int uid, byte[] certificate, @CertificateInputType int type) {
+ throw new UnsupportedOperationException(
+ "hasSigningCertificate not implemented in subclass");
+ }
}
diff --git a/android/content/pm/PackageManagerInternal.java b/android/content/pm/PackageManagerInternal.java
index 8ee8e102..6f093ba8 100644
--- a/android/content/pm/PackageManagerInternal.java
+++ b/android/content/pm/PackageManagerInternal.java
@@ -119,6 +119,12 @@ public abstract class PackageManagerInternal {
public abstract void setSimCallManagerPackagesProvider(PackagesProvider provider);
/**
+ * Sets the Use Open Wifi packages provider.
+ * @param provider The packages provider.
+ */
+ public abstract void setUseOpenWifiAppPackagesProvider(PackagesProvider provider);
+
+ /**
* Sets the sync adapter packages provider.
* @param provider The provider.
*/
@@ -147,6 +153,14 @@ public abstract class PackageManagerInternal {
int userId);
/**
+ * Requests granting of the default permissions to the current default Use Open Wifi app.
+ * @param packageName The default use open wifi package name.
+ * @param userId The user for which to grant the permissions.
+ */
+ public abstract void grantDefaultPermissionsToDefaultUseOpenWifiApp(String packageName,
+ int userId);
+
+ /**
* Sets a list of apps to keep in PM's internal data structures and as APKs even if no user has
* currently installed it. The apps are not preloaded.
* @param packageList List of package names to keep cached.
@@ -422,6 +436,11 @@ public abstract class PackageManagerInternal {
*/
public abstract int getUidTargetSdkVersion(int uid);
+ /**
+ * Return the taget SDK version for the app with the given package name.
+ */
+ public abstract int getPackageTargetSdkVersion(String packageName);
+
/** Whether the binder caller can access instant apps. */
public abstract boolean canAccessInstantApps(int callingUid, int userId);
diff --git a/android/content/pm/PackageParser.java b/android/content/pm/PackageParser.java
index 77eb57f2..24e3dfa9 100644
--- a/android/content/pm/PackageParser.java
+++ b/android/content/pm/PackageParser.java
@@ -35,7 +35,6 @@ import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE
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;
-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.Build.VERSION_CODES.O;
import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
@@ -85,7 +84,6 @@ import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TypedValue;
-import android.util.apk.ApkSignatureSchemeV2Verifier;
import android.util.apk.ApkSignatureVerifier;
import android.view.Gravity;
@@ -112,8 +110,7 @@ import java.lang.reflect.Constructor;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
import java.security.spec.EncodedKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
@@ -158,10 +155,6 @@ public class PackageParser {
private static final boolean MULTI_PACKAGE_APK_ENABLED = Build.IS_DEBUGGABLE &&
SystemProperties.getBoolean(PROPERTY_CHILD_PACKAGES_ENABLED, false);
- public static final int APK_SIGNING_UNKNOWN = 0;
- public static final int APK_SIGNING_V1 = 1;
- public static final int APK_SIGNING_V2 = 2;
-
private static final float DEFAULT_PRE_O_MAX_ASPECT_RATIO = 1.86f;
// TODO: switch outError users to PackageParserException
@@ -247,6 +240,9 @@ public class PackageParser {
}
/** @hide */
+ public static final String APK_FILE_EXTENSION = ".apk";
+
+ /** @hide */
public static class NewPermissionInfo {
public final String name;
public final int sdkVersion;
@@ -477,8 +473,7 @@ public class PackageParser {
public final int revisionCode;
public final int installLocation;
public final VerifierInfo[] verifiers;
- public final Signature[] signatures;
- public final Certificate[][] certificates;
+ public final SigningDetails signingDetails;
public final boolean coreApp;
public final boolean debuggable;
public final boolean multiArch;
@@ -486,10 +481,11 @@ public class PackageParser {
public final boolean extractNativeLibs;
public final boolean isolatedSplits;
- public ApkLite(String codePath, String packageName, String splitName, boolean isFeatureSplit,
+ public ApkLite(String codePath, String packageName, String splitName,
+ boolean isFeatureSplit,
String configForSplit, String usesSplitName, int versionCode, int versionCodeMajor,
int revisionCode, int installLocation, List<VerifierInfo> verifiers,
- Signature[] signatures, Certificate[][] certificates, boolean coreApp,
+ SigningDetails signingDetails, boolean coreApp,
boolean debuggable, boolean multiArch, boolean use32bitAbi,
boolean extractNativeLibs, boolean isolatedSplits) {
this.codePath = codePath;
@@ -502,9 +498,8 @@ public class PackageParser {
this.versionCodeMajor = versionCodeMajor;
this.revisionCode = revisionCode;
this.installLocation = installLocation;
+ this.signingDetails = signingDetails;
this.verifiers = verifiers.toArray(new VerifierInfo[verifiers.size()]);
- this.signatures = signatures;
- this.certificates = certificates;
this.coreApp = coreApp;
this.debuggable = debuggable;
this.multiArch = multiArch;
@@ -621,7 +616,7 @@ public class PackageParser {
}
public static boolean isApkPath(String path) {
- return path.endsWith(".apk");
+ return path.endsWith(APK_FILE_EXTENSION);
}
/**
@@ -684,15 +679,7 @@ public class PackageParser {
pi.requiredAccountType = p.mRequiredAccountType;
pi.overlayTarget = p.mOverlayTarget;
pi.overlayPriority = p.mOverlayPriority;
-
- if (p.mIsStaticOverlay) {
- pi.mOverlayFlags |= PackageInfo.FLAG_OVERLAY_STATIC;
- }
-
- if (p.mTrustedOverlay) {
- pi.mOverlayFlags |= PackageInfo.FLAG_OVERLAY_TRUSTED;
- }
-
+ pi.mOverlayIsStatic = p.mOverlayIsStatic;
pi.compileSdkVersion = p.mCompileSdkVersion;
pi.compileSdkVersionCodename = p.mCompileSdkVersionCodename;
pi.firstInstallTime = firstInstallTime;
@@ -806,11 +793,38 @@ public class PackageParser {
}
}
}
+ // deprecated method of getting signing certificates
if ((flags&PackageManager.GET_SIGNATURES) != 0) {
- int N = (p.mSignatures != null) ? p.mSignatures.length : 0;
- if (N > 0) {
- pi.signatures = new Signature[N];
- System.arraycopy(p.mSignatures, 0, pi.signatures, 0, N);
+ if (p.mSigningDetails.hasPastSigningCertificates()) {
+ // Package has included signing certificate rotation information. Return the oldest
+ // cert so that programmatic checks keep working even if unaware of key rotation.
+ pi.signatures = new Signature[1];
+ pi.signatures[0] = p.mSigningDetails.pastSigningCertificates[0];
+ } else if (p.mSigningDetails.hasSignatures()) {
+ // otherwise keep old behavior
+ int numberOfSigs = p.mSigningDetails.signatures.length;
+ pi.signatures = new Signature[numberOfSigs];
+ System.arraycopy(p.mSigningDetails.signatures, 0, pi.signatures, 0, numberOfSigs);
+ }
+ }
+
+ // replacement for GET_SIGNATURES
+ if ((flags & PackageManager.GET_SIGNING_CERTIFICATES) != 0) {
+ if (p.mSigningDetails.hasPastSigningCertificates()) {
+ // Package has included signing certificate rotation information. Convert each
+ // entry to an array
+ int numberOfSigs = p.mSigningDetails.pastSigningCertificates.length;
+ pi.signingCertificateHistory = new Signature[numberOfSigs][];
+ for (int i = 0; i < numberOfSigs; i++) {
+ pi.signingCertificateHistory[i] =
+ new Signature[] { p.mSigningDetails.pastSigningCertificates[i] };
+ }
+ } else if (p.mSigningDetails.hasSignatures()) {
+ // otherwise keep old behavior
+ int numberOfSigs = p.mSigningDetails.signatures.length;
+ pi.signingCertificateHistory = new Signature[1][numberOfSigs];
+ System.arraycopy(p.mSigningDetails.signatures, 0,
+ pi.signingCertificateHistory[0], 0, numberOfSigs);
}
}
return pi;
@@ -818,15 +832,14 @@ public class PackageParser {
public static final int PARSE_MUST_BE_APK = 1 << 0;
public static final int PARSE_IGNORE_PROCESSES = 1 << 1;
+ /** @deprecated forward lock no longer functional. remove. */
+ @Deprecated
public static final int PARSE_FORWARD_LOCK = 1 << 2;
public static final int PARSE_EXTERNAL_STORAGE = 1 << 3;
public static final int PARSE_IS_SYSTEM_DIR = 1 << 4;
public static final int PARSE_COLLECT_CERTIFICATES = 1 << 5;
public static final int PARSE_ENFORCE_CODE = 1 << 6;
public static final int PARSE_FORCE_SDK = 1 << 7;
- /** @deprecated remove when fixing b/68860689 */
- @Deprecated
- public static final int PARSE_IS_EPHEMERAL = 1 << 8;
public static final int PARSE_CHATTY = 1 << 31;
@IntDef(flag = true, prefix = { "PARSE_" }, value = {
@@ -837,7 +850,6 @@ public class PackageParser {
PARSE_FORCE_SDK,
PARSE_FORWARD_LOCK,
PARSE_IGNORE_PROCESSES,
- PARSE_IS_EPHEMERAL,
PARSE_IS_SYSTEM_DIR,
PARSE_MUST_BE_APK,
})
@@ -1349,7 +1361,7 @@ public class PackageParser {
pkg.setVolumeUuid(volumeUuid);
pkg.setApplicationVolumeUuid(volumeUuid);
pkg.setBaseCodePath(apkPath);
- pkg.setSignatures(null);
+ pkg.setSigningDetails(SigningDetails.UNKNOWN);
return pkg;
@@ -1469,57 +1481,19 @@ public class PackageParser {
return pkg;
}
- public static int getApkSigningVersion(Package pkg) {
- try {
- if (ApkSignatureSchemeV2Verifier.hasSignature(pkg.baseCodePath)) {
- return APK_SIGNING_V2;
- }
- return APK_SIGNING_V1;
- } catch (IOException e) {
- }
- return APK_SIGNING_UNKNOWN;
- }
-
- /**
- * Populates the correct packages fields with the given certificates.
- * <p>
- * This is useful when we've already processed the certificates [such as during package
- * installation through an installer session]. We don't re-process the archive and
- * simply populate the correct fields.
- */
- public static void populateCertificates(Package pkg, Certificate[][] certificates)
- throws PackageParserException {
- pkg.mCertificates = null;
- pkg.mSignatures = null;
- pkg.mSigningKeys = null;
-
- pkg.mCertificates = certificates;
- try {
- pkg.mSignatures = ApkSignatureVerifier.convertToSignatures(certificates);
- } catch (CertificateEncodingException e) {
- // certificates weren't encoded properly; something went wrong
- throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
- "Failed to collect certificates from " + pkg.baseCodePath, e);
- }
- pkg.mSigningKeys = new ArraySet<>(certificates.length);
- for (int i = 0; i < certificates.length; i++) {
- Certificate[] signerCerts = certificates[i];
- Certificate signerCert = signerCerts[0];
- pkg.mSigningKeys.add(signerCert.getPublicKey());
- }
- // add signatures to child packages
- final int childCount = (pkg.childPackages != null) ? pkg.childPackages.size() : 0;
- for (int i = 0; i < childCount; i++) {
- Package childPkg = pkg.childPackages.get(i);
- childPkg.mCertificates = pkg.mCertificates;
- childPkg.mSignatures = pkg.mSignatures;
- childPkg.mSigningKeys = pkg.mSigningKeys;
+ /** Parses the public keys from the set of signatures. */
+ public static ArraySet<PublicKey> toSigningKeys(Signature[] signatures)
+ throws CertificateException {
+ ArraySet<PublicKey> keys = new ArraySet<>(signatures.length);
+ for (int i = 0; i < signatures.length; i++) {
+ keys.add(signatures[i].getPublicKey());
}
+ return keys;
}
/**
* Collect certificates from all the APKs described in the given package,
- * populating {@link Package#mSignatures}. Also asserts that all APK
+ * populating {@link Package#mSigningDetails}. Also asserts that all APK
* contents are signed correctly and consistently.
*/
public static void collectCertificates(Package pkg, @ParseFlags int parseFlags)
@@ -1528,17 +1502,13 @@ public class PackageParser {
final int childCount = (pkg.childPackages != null) ? pkg.childPackages.size() : 0;
for (int i = 0; i < childCount; i++) {
Package childPkg = pkg.childPackages.get(i);
- childPkg.mCertificates = pkg.mCertificates;
- childPkg.mSignatures = pkg.mSignatures;
- childPkg.mSigningKeys = pkg.mSigningKeys;
+ childPkg.mSigningDetails = pkg.mSigningDetails;
}
}
private static void collectCertificatesInternal(Package pkg, @ParseFlags int parseFlags)
throws PackageParserException {
- pkg.mCertificates = null;
- pkg.mSignatures = null;
- pkg.mSigningKeys = null;
+ pkg.mSigningDetails = SigningDetails.UNKNOWN;
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "collectCertificates");
try {
@@ -1558,12 +1528,12 @@ public class PackageParser {
throws PackageParserException {
final String apkPath = apkFile.getAbsolutePath();
- int minSignatureScheme = ApkSignatureVerifier.VERSION_JAR_SIGNATURE_SCHEME;
+ int minSignatureScheme = SigningDetails.SignatureSchemeVersion.JAR;
if (pkg.applicationInfo.isStaticSharedLibrary()) {
// must use v2 signing scheme
- minSignatureScheme = ApkSignatureVerifier.VERSION_APK_SIGNATURE_SCHEME_V2;
+ minSignatureScheme = SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2;
}
- ApkSignatureVerifier.Result verified;
+ SigningDetails verified;
if ((parseFlags & PARSE_IS_SYSTEM_DIR) != 0) {
// systemDir APKs are already trusted, save time by not verifying
verified = ApkSignatureVerifier.plsCertsNoVerifyOnlyCerts(
@@ -1571,29 +1541,14 @@ public class PackageParser {
} 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,
- "No APK Signature Scheme v2 signature in ephemeral package " + apkPath);
- }
- }
// 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());
- }
+ if (pkg.mSigningDetails == SigningDetails.UNKNOWN) {
+ pkg.mSigningDetails = verified;
} else {
- if (!Signature.areExactMatch(pkg.mSignatures, verified.sigs)) {
+ if (!Signature.areExactMatch(pkg.mSigningDetails.signatures, verified.signatures)) {
throw new PackageParserException(
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
apkPath + " has mismatched certificates");
@@ -1655,8 +1610,7 @@ public class PackageParser {
parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);
- final Signature[] signatures;
- final Certificate[][] certificates;
+ final SigningDetails signingDetails;
if ((flags & PARSE_COLLECT_CERTIFICATES) != 0) {
// TODO: factor signature related items out of Package object
final Package tempPkg = new Package((String) null);
@@ -1666,15 +1620,13 @@ public class PackageParser {
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
- signatures = tempPkg.mSignatures;
- certificates = tempPkg.mCertificates;
+ signingDetails = tempPkg.mSigningDetails;
} else {
- signatures = null;
- certificates = null;
+ signingDetails = SigningDetails.UNKNOWN;
}
final AttributeSet attrs = parser;
- return parseApkLite(apkPath, parser, attrs, signatures, certificates);
+ return parseApkLite(apkPath, parser, attrs, signingDetails);
} catch (XmlPullParserException | IOException | RuntimeException e) {
Slog.w(TAG, "Failed to parse " + apkPath, e);
@@ -1761,7 +1713,7 @@ public class PackageParser {
}
private static ApkLite parseApkLite(String codePath, XmlPullParser parser, AttributeSet attrs,
- Signature[] signatures, Certificate[][] certificates)
+ SigningDetails signingDetails)
throws IOException, XmlPullParserException, PackageParserException {
final Pair<String, String> packageSplit = parsePackageSplitNames(parser, attrs);
@@ -1854,7 +1806,7 @@ public class PackageParser {
return new ApkLite(codePath, packageSplit.first, packageSplit.second, isFeatureSplit,
configForSplit, usesSplitName, versionCode, versionCodeMajor, revisionCode,
- installLocation, verifiers, signatures, certificates, coreApp, debuggable,
+ installLocation, verifiers, signingDetails, coreApp, debuggable,
multiArch, use32bitAbi, extractNativeLibs, isolatedSplits);
}
@@ -2039,11 +1991,6 @@ public class PackageParser {
String str = sa.getNonConfigurationString(
com.android.internal.R.styleable.AndroidManifest_sharedUserId, 0);
if (str != null && str.length() > 0) {
- if ((flags & PARSE_IS_EPHEMERAL) != 0) {
- outError[0] = "sharedUserId not allowed in ephemeral application";
- mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID;
- return null;
- }
String nameError = validateName(str, true, false);
if (nameError != null && !"android".equals(pkg.packageName)) {
outError[0] = "<manifest> specifies bad sharedUserId name \""
@@ -2130,7 +2077,7 @@ public class PackageParser {
pkg.mOverlayPriority = sa.getInt(
com.android.internal.R.styleable.AndroidManifestResourceOverlay_priority,
0);
- pkg.mIsStaticOverlay = sa.getBoolean(
+ pkg.mOverlayIsStatic = sa.getBoolean(
com.android.internal.R.styleable.AndroidManifestResourceOverlay_isStatic,
false);
final String propName = sa.getString(
@@ -2304,8 +2251,9 @@ public class PackageParser {
return null;
}
+ boolean defaultToCurrentDevBranch = (flags & PARSE_FORCE_SDK) != 0;
final int targetSdkVersion = PackageParser.computeTargetSdkVersion(targetVers,
- targetCode, SDK_VERSION, SDK_CODENAMES, outError);
+ targetCode, SDK_CODENAMES, outError, defaultToCurrentDevBranch);
if (targetSdkVersion < 0) {
mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK;
return null;
@@ -2621,19 +2569,19 @@ public class PackageParser {
* application manifest, or 0 otherwise
* @param targetCode targetSdkVersion code, if specified in the application
* manifest, or {@code null} otherwise
- * @param platformSdkVersion platform SDK version number, typically
- * Build.VERSION.SDK_INT
* @param platformSdkCodenames array of allowed pre-release SDK codenames
* for this platform
* @param outError output array to populate with error, if applicable
+ * @param forceCurrentDev if development target code is not available, use the current
+ * development version by default.
* @return the targetSdkVersion to use at runtime, or -1 if the package is
* not compatible with this platform
* @hide Exposed for unit testing only.
*/
@TestApi
public static int computeTargetSdkVersion(@IntRange(from = 0) int targetVers,
- @Nullable String targetCode, @IntRange(from = 1) int platformSdkVersion,
- @NonNull String[] platformSdkCodenames, @NonNull String[] outError) {
+ @Nullable String targetCode, @NonNull String[] platformSdkCodenames,
+ @NonNull String[] outError, boolean forceCurrentDev) {
// If it's a release SDK, return the version number unmodified.
if (targetCode == null) {
return targetVers;
@@ -2641,7 +2589,7 @@ public class PackageParser {
// If it's a pre-release SDK and the codename matches this platform, it
// definitely targets this SDK.
- if (ArrayUtils.contains(platformSdkCodenames, targetCode)) {
+ if (ArrayUtils.contains(platformSdkCodenames, targetCode) || forceCurrentDev) {
return Build.VERSION_CODES.CUR_DEVELOPMENT;
}
@@ -5734,6 +5682,261 @@ public class PackageParser {
return true;
}
+ /** A container for signing-related data of an application package. */
+ public static final class SigningDetails implements Parcelable {
+
+ @IntDef({SigningDetails.SignatureSchemeVersion.UNKNOWN,
+ SigningDetails.SignatureSchemeVersion.JAR,
+ SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V2,
+ SigningDetails.SignatureSchemeVersion.SIGNING_BLOCK_V3})
+ public @interface SignatureSchemeVersion {
+ int UNKNOWN = 0;
+ int JAR = 1;
+ int SIGNING_BLOCK_V2 = 2;
+ int SIGNING_BLOCK_V3 = 3;
+ }
+
+ @Nullable
+ public final Signature[] signatures;
+ @SignatureSchemeVersion
+ public final int signatureSchemeVersion;
+ @Nullable
+ public final ArraySet<PublicKey> publicKeys;
+
+ /**
+ * Collection of {@code Signature} objects, each of which is formed from a former signing
+ * certificate of this APK before it was changed by signing certificate rotation.
+ */
+ @Nullable
+ public final Signature[] pastSigningCertificates;
+
+ /**
+ * Flags for the {@code pastSigningCertificates} collection, which indicate the capabilities
+ * the including APK wishes to grant to its past signing certificates.
+ */
+ @Nullable
+ public final int[] pastSigningCertificatesFlags;
+
+ /** A representation of unknown signing details. Use instead of null. */
+ public static final SigningDetails UNKNOWN =
+ new SigningDetails(null, SignatureSchemeVersion.UNKNOWN, null, null, null);
+
+ @VisibleForTesting
+ public SigningDetails(Signature[] signatures,
+ @SignatureSchemeVersion int signatureSchemeVersion,
+ ArraySet<PublicKey> keys, Signature[] pastSigningCertificates,
+ int[] pastSigningCertificatesFlags) {
+ this.signatures = signatures;
+ this.signatureSchemeVersion = signatureSchemeVersion;
+ this.publicKeys = keys;
+ this.pastSigningCertificates = pastSigningCertificates;
+ this.pastSigningCertificatesFlags = pastSigningCertificatesFlags;
+ }
+
+ public SigningDetails(Signature[] signatures,
+ @SignatureSchemeVersion int signatureSchemeVersion,
+ Signature[] pastSigningCertificates, int[] pastSigningCertificatesFlags)
+ throws CertificateException {
+ this(signatures, signatureSchemeVersion, toSigningKeys(signatures),
+ pastSigningCertificates, pastSigningCertificatesFlags);
+ }
+
+ public SigningDetails(Signature[] signatures,
+ @SignatureSchemeVersion int signatureSchemeVersion)
+ throws CertificateException {
+ this(signatures, signatureSchemeVersion,
+ null, null);
+ }
+
+ public SigningDetails(SigningDetails orig) {
+ if (orig != null) {
+ if (orig.signatures != null) {
+ this.signatures = orig.signatures.clone();
+ } else {
+ this.signatures = null;
+ }
+ this.signatureSchemeVersion = orig.signatureSchemeVersion;
+ this.publicKeys = new ArraySet<>(orig.publicKeys);
+ if (orig.pastSigningCertificates != null) {
+ this.pastSigningCertificates = orig.pastSigningCertificates.clone();
+ this.pastSigningCertificatesFlags = orig.pastSigningCertificatesFlags.clone();
+ } else {
+ this.pastSigningCertificates = null;
+ this.pastSigningCertificatesFlags = null;
+ }
+ } else {
+ this.signatures = null;
+ this.signatureSchemeVersion = SignatureSchemeVersion.UNKNOWN;
+ this.publicKeys = null;
+ this.pastSigningCertificates = null;
+ this.pastSigningCertificatesFlags = null;
+ }
+ }
+
+ /** Returns true if the signing details have one or more signatures. */
+ public boolean hasSignatures() {
+ return signatures != null && signatures.length > 0;
+ }
+
+ /** Returns true if the signing details have past signing certificates. */
+ public boolean hasPastSigningCertificates() {
+ return pastSigningCertificates != null && pastSigningCertificates.length > 0;
+ }
+
+ /** Returns true if the signatures in this and other match exactly. */
+ public boolean signaturesMatchExactly(SigningDetails other) {
+ return Signature.areExactMatch(this.signatures, other.signatures);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ boolean isUnknown = UNKNOWN == this;
+ dest.writeBoolean(isUnknown);
+ if (isUnknown) {
+ return;
+ }
+ dest.writeTypedArray(this.signatures, flags);
+ dest.writeInt(this.signatureSchemeVersion);
+ dest.writeArraySet(this.publicKeys);
+ dest.writeTypedArray(this.pastSigningCertificates, flags);
+ dest.writeIntArray(this.pastSigningCertificatesFlags);
+ }
+
+ protected SigningDetails(Parcel in) {
+ final ClassLoader boot = Object.class.getClassLoader();
+ this.signatures = in.createTypedArray(Signature.CREATOR);
+ this.signatureSchemeVersion = in.readInt();
+ this.publicKeys = (ArraySet<PublicKey>) in.readArraySet(boot);
+ this.pastSigningCertificates = in.createTypedArray(Signature.CREATOR);
+ this.pastSigningCertificatesFlags = in.createIntArray();
+ }
+
+ public static final Creator<SigningDetails> CREATOR = new Creator<SigningDetails>() {
+ @Override
+ public SigningDetails createFromParcel(Parcel source) {
+ if (source.readBoolean()) {
+ return UNKNOWN;
+ }
+ return new SigningDetails(source);
+ }
+
+ @Override
+ public SigningDetails[] newArray(int size) {
+ return new SigningDetails[size];
+ }
+ };
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof SigningDetails)) return false;
+
+ SigningDetails that = (SigningDetails) o;
+
+ if (signatureSchemeVersion != that.signatureSchemeVersion) return false;
+ if (!Signature.areExactMatch(signatures, that.signatures)) return false;
+ if (publicKeys != null) {
+ if (!publicKeys.equals((that.publicKeys))) {
+ return false;
+ }
+ } else if (that.publicKeys != null) {
+ return false;
+ }
+
+ // can't use Signature.areExactMatch() because order matters with the past signing certs
+ if (!Arrays.equals(pastSigningCertificates, that.pastSigningCertificates)) {
+ return false;
+ }
+ if (!Arrays.equals(pastSigningCertificatesFlags, that.pastSigningCertificatesFlags)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = +Arrays.hashCode(signatures);
+ result = 31 * result + signatureSchemeVersion;
+ result = 31 * result + (publicKeys != null ? publicKeys.hashCode() : 0);
+ result = 31 * result + Arrays.hashCode(pastSigningCertificates);
+ result = 31 * result + Arrays.hashCode(pastSigningCertificatesFlags);
+ return result;
+ }
+
+ /**
+ * Builder of {@code SigningDetails} instances.
+ */
+ public static class Builder {
+ private Signature[] mSignatures;
+ private int mSignatureSchemeVersion = SignatureSchemeVersion.UNKNOWN;
+ private Signature[] mPastSigningCertificates;
+ private int[] mPastSigningCertificatesFlags;
+
+ public Builder() {
+ }
+
+ /** get signing certificates used to sign the current APK */
+ public Builder setSignatures(Signature[] signatures) {
+ mSignatures = signatures;
+ return this;
+ }
+
+ /** set the signature scheme version used to sign the APK */
+ public Builder setSignatureSchemeVersion(int signatureSchemeVersion) {
+ mSignatureSchemeVersion = signatureSchemeVersion;
+ return this;
+ }
+
+ /** set the signing certificates by which the APK proved it can be authenticated */
+ public Builder setPastSigningCertificates(Signature[] pastSigningCertificates) {
+ mPastSigningCertificates = pastSigningCertificates;
+ return this;
+ }
+
+ /** set the flags for the {@code pastSigningCertificates} */
+ public Builder setPastSigningCertificatesFlags(int[] pastSigningCertificatesFlags) {
+ mPastSigningCertificatesFlags = pastSigningCertificatesFlags;
+ return this;
+ }
+
+ private void checkInvariants() {
+ // must have signatures and scheme version set
+ if (mSignatures == null) {
+ throw new IllegalStateException("SigningDetails requires the current signing"
+ + " certificates.");
+ }
+
+ // pastSigningCerts and flags must match up
+ boolean pastMismatch = false;
+ if (mPastSigningCertificates != null && mPastSigningCertificatesFlags != null) {
+ if (mPastSigningCertificates.length != mPastSigningCertificatesFlags.length) {
+ pastMismatch = true;
+ }
+ } else if (!(mPastSigningCertificates == null
+ && mPastSigningCertificatesFlags == null)) {
+ pastMismatch = true;
+ }
+ if (pastMismatch) {
+ throw new IllegalStateException("SigningDetails must have a one to one mapping "
+ + "between pastSigningCertificates and pastSigningCertificatesFlags");
+ }
+ }
+ /** build a {@code SigningDetails} object */
+ public SigningDetails build()
+ throws CertificateException {
+ checkInvariants();
+ return new SigningDetails(mSignatures, mSignatureSchemeVersion,
+ mPastSigningCertificates, mPastSigningCertificatesFlags);
+ }
+ }
+ }
+
/**
* Representation of a full package parsed from APK files on disk. A package
* consists of a single base APK, and zero or more split APKs.
@@ -5840,8 +6043,7 @@ public class PackageParser {
public int mSharedUserLabel;
// Signatures that were read from the package.
- public Signature[] mSignatures;
- public Certificate[][] mCertificates;
+ @NonNull public SigningDetails mSigningDetails = SigningDetails.UNKNOWN;
// For use by package manager service for quick lookup of
// preferred up order.
@@ -5884,8 +6086,7 @@ public class PackageParser {
public String mOverlayTarget;
public int mOverlayPriority;
- public boolean mIsStaticOverlay;
- public boolean mTrustedOverlay;
+ public boolean mOverlayIsStatic;
public int mCompileSdkVersion;
public String mCompileSdkVersionCodename;
@@ -5893,7 +6094,6 @@ public class PackageParser {
/**
* Data used to feed the KeySetManagerService
*/
- public ArraySet<PublicKey> mSigningKeys;
public ArraySet<String> mUpgradeKeySets;
public ArrayMap<String, ArraySet<PublicKey>> mKeySetMapping;
@@ -5950,6 +6150,8 @@ public class PackageParser {
}
}
+ /** @deprecated Forward locked apps no longer supported. Resource path not needed. */
+ @Deprecated
public void setApplicationInfoResourcePath(String resourcePath) {
this.applicationInfo.setResourcePath(resourcePath);
if (childPackages != null) {
@@ -5960,6 +6162,8 @@ public class PackageParser {
}
}
+ /** @deprecated Forward locked apps no longer supported. Resource path not needed. */
+ @Deprecated
public void setApplicationInfoBaseResourcePath(String resourcePath) {
this.applicationInfo.setBaseResourcePath(resourcePath);
if (childPackages != null) {
@@ -6008,6 +6212,8 @@ public class PackageParser {
// Children have no splits
}
+ /** @deprecated Forward locked apps no longer supported. Resource path not needed. */
+ @Deprecated
public void setApplicationInfoSplitResourcePaths(String[] resroucePaths) {
this.applicationInfo.setSplitResourcePaths(resroucePaths);
// Children have no splits
@@ -6037,12 +6243,13 @@ public class PackageParser {
}
}
- public void setSignatures(Signature[] signatures) {
- this.mSignatures = signatures;
+ /** Sets signing details on the package and any of its children. */
+ public void setSigningDetails(@NonNull SigningDetails signingDetails) {
+ mSigningDetails = signingDetails;
if (childPackages != null) {
final int packageCount = childPackages.size();
for (int i = 0; i < packageCount; i++) {
- childPackages.get(i).mSignatures = signatures;
+ childPackages.get(i).mSigningDetails = signingDetails;
}
}
}
@@ -6243,6 +6450,31 @@ public class PackageParser {
+ " " + packageName + "}";
}
+ public String dumpState_temp() {
+ String flags = "";
+ flags += ((applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 ? "U" : "");
+ flags += ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0 ? "S" : "");
+ if ("".equals(flags)) {
+ flags = "-";
+ }
+ String privFlags = "";
+ privFlags += ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0 ? "P" : "");
+ privFlags += ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0 ? "O" : "");
+ privFlags += ((applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0 ? "V" : "");
+ if ("".equals(privFlags)) {
+ privFlags = "-";
+ }
+ return "Package{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + packageName
+ + ", ver:" + getLongVersionCode()
+ + ", path: " + codePath
+ + ", flags: " + flags
+ + ", privFlags: " + privFlags
+ + ", extra: " + (mExtras == null ? "<<NULL>>" : Integer.toHexString(System.identityHashCode(mExtras)) + "}")
+ + "}";
+ }
+
@Override
public int describeContents() {
return 0;
@@ -6348,8 +6580,7 @@ public class PackageParser {
}
mSharedUserLabel = dest.readInt();
- mSignatures = (Signature[]) dest.readParcelableArray(boot, Signature.class);
- mCertificates = (Certificate[][]) dest.readSerializable();
+ mSigningDetails = dest.readParcelable(boot);
mPreferredOrder = dest.readInt();
@@ -6385,11 +6616,9 @@ public class PackageParser {
mRequiredAccountType = dest.readString();
mOverlayTarget = dest.readString();
mOverlayPriority = dest.readInt();
- mIsStaticOverlay = (dest.readInt() == 1);
- mTrustedOverlay = (dest.readInt() == 1);
+ mOverlayIsStatic = (dest.readInt() == 1);
mCompileSdkVersion = dest.readInt();
mCompileSdkVersionCodename = dest.readString();
- mSigningKeys = (ArraySet<PublicKey>) dest.readArraySet(boot);
mUpgradeKeySets = (ArraySet<String>) dest.readArraySet(boot);
mKeySetMapping = readKeySetMapping(dest);
@@ -6489,8 +6718,7 @@ public class PackageParser {
dest.writeString(mSharedUserId);
dest.writeInt(mSharedUserLabel);
- dest.writeParcelableArray(mSignatures, flags);
- dest.writeSerializable(mCertificates);
+ dest.writeParcelable(mSigningDetails, flags);
dest.writeInt(mPreferredOrder);
@@ -6511,11 +6739,9 @@ public class PackageParser {
dest.writeString(mRequiredAccountType);
dest.writeString(mOverlayTarget);
dest.writeInt(mOverlayPriority);
- dest.writeInt(mIsStaticOverlay ? 1 : 0);
- dest.writeInt(mTrustedOverlay ? 1 : 0);
+ dest.writeInt(mOverlayIsStatic ? 1 : 0);
dest.writeInt(mCompileSdkVersion);
dest.writeString(mCompileSdkVersionCodename);
- dest.writeArraySet(mSigningKeys);
dest.writeArraySet(mUpgradeKeySets);
writeKeySetMapping(dest, mKeySetMapping);
dest.writeString(cpuAbiOverride);
diff --git a/android/content/pm/PackageSharedLibraryUpdater.java b/android/content/pm/PackageSharedLibraryUpdater.java
new file mode 100644
index 00000000..49d884ca
--- /dev/null
+++ b/android/content/pm/PackageSharedLibraryUpdater.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.content.pm;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import java.util.ArrayList;
+
+/**
+ * Base for classes that update a {@link PackageParser.Package}'s shared libraries.
+ *
+ * @hide
+ */
+@VisibleForTesting
+public abstract class PackageSharedLibraryUpdater {
+
+ /**
+ * Update the package's shared libraries.
+ *
+ * @param pkg the package to update.
+ */
+ public abstract void updatePackage(PackageParser.Package pkg);
+
+ static @NonNull
+ <T> ArrayList<T> prefix(@Nullable ArrayList<T> cur, T val) {
+ if (cur == null) {
+ cur = new ArrayList<>();
+ }
+ cur.add(0, val);
+ return cur;
+ }
+
+ static boolean isLibraryPresent(ArrayList<String> usesLibraries,
+ ArrayList<String> usesOptionalLibraries, String apacheHttpLegacy) {
+ return ArrayUtils.contains(usesLibraries, apacheHttpLegacy)
+ || ArrayUtils.contains(usesOptionalLibraries, apacheHttpLegacy);
+ }
+}
diff --git a/android/content/pm/PackageUserState.java b/android/content/pm/PackageUserState.java
index 069b2d4e..293beb2b 100644
--- a/android/content/pm/PackageUserState.java
+++ b/android/content/pm/PackageUserState.java
@@ -52,6 +52,7 @@ public class PackageUserState {
public int appLinkGeneration;
public int categoryHint = ApplicationInfo.CATEGORY_UNDEFINED;
public int installReason;
+ public String harmfulAppWarning;
public ArraySet<String> disabledComponents;
public ArraySet<String> enabledComponents;
@@ -87,6 +88,7 @@ public class PackageUserState {
enabledComponents = ArrayUtils.cloneOrNull(o.enabledComponents);
overlayPaths =
o.overlayPaths == null ? null : Arrays.copyOf(o.overlayPaths, o.overlayPaths.length);
+ harmfulAppWarning = o.harmfulAppWarning;
}
/**
@@ -247,6 +249,11 @@ public class PackageUserState {
}
}
}
+ if (harmfulAppWarning == null && oldState.harmfulAppWarning != null
+ || (harmfulAppWarning != null
+ && !harmfulAppWarning.equals(oldState.harmfulAppWarning))) {
+ return false;
+ }
return true;
}
}
diff --git a/android/content/pm/ShortcutInfo.java b/android/content/pm/ShortcutInfo.java
index 8839cf9d..ea476b0a 100644
--- a/android/content/pm/ShortcutInfo.java
+++ b/android/content/pm/ShortcutInfo.java
@@ -181,6 +181,11 @@ public final class ShortcutInfo implements Parcelable {
public static final int DISABLED_REASON_APP_CHANGED = 2;
/**
+ * Shortcut is disabled for an unknown reason.
+ */
+ public static final int DISABLED_REASON_UNKNOWN = 3;
+
+ /**
* A disabled reason that's equal to or bigger than this is due to backup and restore issue.
* A shortcut with such a reason wil be visible to the launcher, but not to the publisher.
* ({@link #isVisibleToPublisher()} will be false.)
@@ -214,6 +219,7 @@ public final class ShortcutInfo implements Parcelable {
DISABLED_REASON_NOT_DISABLED,
DISABLED_REASON_BY_APP,
DISABLED_REASON_APP_CHANGED,
+ DISABLED_REASON_UNKNOWN,
DISABLED_REASON_VERSION_LOWER,
DISABLED_REASON_BACKUP_NOT_SUPPORTED,
DISABLED_REASON_SIGNATURE_MISMATCH,
@@ -272,6 +278,9 @@ public final class ShortcutInfo implements Parcelable {
case DISABLED_REASON_OTHER_RESTORE_ISSUE:
return res.getString(
com.android.internal.R.string.shortcut_restore_unknown_issue);
+ case DISABLED_REASON_UNKNOWN:
+ return res.getString(
+ com.android.internal.R.string.shortcut_disabled_reason_unknown);
}
return null;
}
diff --git a/android/content/pm/dex/ArtManager.java b/android/content/pm/dex/ArtManager.java
index 201cd8d3..aa9c46e6 100644
--- a/android/content/pm/dex/ArtManager.java
+++ b/android/content/pm/dex/ArtManager.java
@@ -153,4 +153,14 @@ public class ArtManager {
return true;
}
}
+
+ /**
+ * Return the profile name for the given split. If {@code splitName} is null the
+ * method returns the profile name for the base apk.
+ *
+ * @hide
+ */
+ public static String getProfileName(String splitName) {
+ return splitName == null ? "primary.prof" : splitName + ".split.prof";
+ }
}
diff --git a/android/content/pm/dex/DexMetadataHelper.java b/android/content/pm/dex/DexMetadataHelper.java
new file mode 100644
index 00000000..5d10b882
--- /dev/null
+++ b/android/content/pm/dex/DexMetadataHelper.java
@@ -0,0 +1,230 @@
+/**
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.content.pm.dex;
+
+import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_DEX_METADATA;
+import static android.content.pm.PackageParser.APK_FILE_EXTENSION;
+
+import android.content.pm.PackageParser;
+import android.content.pm.PackageParser.PackageLite;
+import android.content.pm.PackageParser.PackageParserException;
+import android.util.ArrayMap;
+import android.util.jar.StrictJarFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper class used to compute and validate the location of dex metadata files.
+ *
+ * @hide
+ */
+public class DexMetadataHelper {
+ private static final String DEX_METADATA_FILE_EXTENSION = ".dm";
+
+ private DexMetadataHelper() {}
+
+ /** Return true if the given file is a dex metadata file. */
+ public static boolean isDexMetadataFile(File file) {
+ return isDexMetadataPath(file.getName());
+ }
+
+ /** Return true if the given path is a dex metadata path. */
+ private static boolean isDexMetadataPath(String path) {
+ return path.endsWith(DEX_METADATA_FILE_EXTENSION);
+ }
+
+ /**
+ * Return the size (in bytes) of all dex metadata files associated with the given package.
+ */
+ public static long getPackageDexMetadataSize(PackageLite pkg) {
+ long sizeBytes = 0;
+ Collection<String> dexMetadataList = DexMetadataHelper.getPackageDexMetadata(pkg).values();
+ for (String dexMetadata : dexMetadataList) {
+ sizeBytes += new File(dexMetadata).length();
+ }
+ return sizeBytes;
+ }
+
+ /**
+ * Search for the dex metadata file associated with the given target file.
+ * If it exists, the method returns the dex metadata file; otherwise it returns null.
+ *
+ * Note that this performs a loose matching suitable to be used in the InstallerSession logic.
+ * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile}
+ * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk').
+ */
+ public static File findDexMetadataForFile(File targetFile) {
+ String dexMetadataPath = buildDexMetadataPathForFile(targetFile);
+ File dexMetadataFile = new File(dexMetadataPath);
+ return dexMetadataFile.exists() ? dexMetadataFile : null;
+ }
+
+ /**
+ * Return the dex metadata files for the given package as a map
+ * [code path -> dex metadata path].
+ *
+ * NOTE: involves I/O checks.
+ */
+ public static Map<String, String> getPackageDexMetadata(PackageParser.Package pkg) {
+ return buildPackageApkToDexMetadataMap(pkg.getAllCodePaths());
+ }
+
+ /**
+ * Return the dex metadata files for the given package as a map
+ * [code path -> dex metadata path].
+ *
+ * NOTE: involves I/O checks.
+ */
+ private static Map<String, String> getPackageDexMetadata(PackageLite pkg) {
+ return buildPackageApkToDexMetadataMap(pkg.getAllCodePaths());
+ }
+
+ /**
+ * Look up the dex metadata files for the given code paths building the map
+ * [code path -> dex metadata].
+ *
+ * For each code path (.apk) the method checks if a matching dex metadata file (.dm) exists.
+ * If it does it adds the pair to the returned map.
+ *
+ * Note that this method will do a loose
+ * matching based on the extension ('foo.dm' will match 'foo.apk' or 'foo').
+ *
+ * This should only be used for code paths extracted from a package structure after the naming
+ * was enforced in the installer.
+ */
+ private static Map<String, String> buildPackageApkToDexMetadataMap(
+ List<String> codePaths) {
+ ArrayMap<String, String> result = new ArrayMap<>();
+ for (int i = codePaths.size() - 1; i >= 0; i--) {
+ String codePath = codePaths.get(i);
+ String dexMetadataPath = buildDexMetadataPathForFile(new File(codePath));
+
+ if (Files.exists(Paths.get(dexMetadataPath))) {
+ result.put(codePath, dexMetadataPath);
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Return the dex metadata path associated with the given code path.
+ * (replaces '.apk' extension with '.dm')
+ *
+ * @throws IllegalArgumentException if the code path is not an .apk.
+ */
+ public static String buildDexMetadataPathForApk(String codePath) {
+ if (!PackageParser.isApkPath(codePath)) {
+ throw new IllegalStateException(
+ "Corrupted package. Code path is not an apk " + codePath);
+ }
+ return codePath.substring(0, codePath.length() - APK_FILE_EXTENSION.length())
+ + DEX_METADATA_FILE_EXTENSION;
+ }
+
+ /**
+ * Return the dex metadata path corresponding to the given {@code targetFile} using a loose
+ * matching.
+ * i.e. the method will attempt to match the {@code dmFile} regardless of {@code targetFile}
+ * extension (e.g. 'foo.dm' will match 'foo' or 'foo.apk').
+ */
+ private static String buildDexMetadataPathForFile(File targetFile) {
+ return PackageParser.isApkFile(targetFile)
+ ? buildDexMetadataPathForApk(targetFile.getPath())
+ : targetFile.getPath() + DEX_METADATA_FILE_EXTENSION;
+ }
+
+ /**
+ * Validate the dex metadata files installed for the given package.
+ *
+ * @throws PackageParserException in case of errors.
+ */
+ public static void validatePackageDexMetadata(PackageParser.Package pkg)
+ throws PackageParserException {
+ Collection<String> apkToDexMetadataList = getPackageDexMetadata(pkg).values();
+ for (String dexMetadata : apkToDexMetadataList) {
+ validateDexMetadataFile(dexMetadata);
+ }
+ }
+
+ /**
+ * Validate that the given file is a dex metadata archive.
+ * This is just a sanity validation that the file is a zip archive.
+ *
+ * @throws PackageParserException if the file is not a .dm file.
+ */
+ private static void validateDexMetadataFile(String dmaPath) throws PackageParserException {
+ StrictJarFile jarFile = null;
+ try {
+ jarFile = new StrictJarFile(dmaPath, false, false);
+ } catch (IOException e) {
+ throw new PackageParserException(INSTALL_FAILED_BAD_DEX_METADATA,
+ "Error opening " + dmaPath, e);
+ } finally {
+ if (jarFile != null) {
+ try {
+ jarFile.close();
+ } catch (IOException ignored) {
+ }
+ }
+ }
+ }
+
+ /**
+ * Validates that all dex metadata paths in the given list have a matching apk.
+ * (for any foo.dm there should be either a 'foo' of a 'foo.apk' file).
+ * If that's not the case it throws {@code IllegalStateException}.
+ *
+ * This is used to perform a basic sanity check during adb install commands.
+ * (The installer does not support stand alone .dm files)
+ */
+ public static void validateDexPaths(String[] paths) {
+ ArrayList<String> apks = new ArrayList<>();
+ for (int i = 0; i < paths.length; i++) {
+ if (PackageParser.isApkPath(paths[i])) {
+ apks.add(paths[i]);
+ }
+ }
+ ArrayList<String> unmatchedDmFiles = new ArrayList<>();
+ for (int i = 0; i < paths.length; i++) {
+ String dmPath = paths[i];
+ if (isDexMetadataPath(dmPath)) {
+ boolean valid = false;
+ for (int j = apks.size() - 1; j >= 0; j--) {
+ if (dmPath.equals(buildDexMetadataPathForFile(new File(apks.get(j))))) {
+ valid = true;
+ break;
+ }
+ }
+ if (!valid) {
+ unmatchedDmFiles.add(dmPath);
+ }
+ }
+ }
+ if (!unmatchedDmFiles.isEmpty()) {
+ throw new IllegalStateException("Unmatched .dm files: " + unmatchedDmFiles);
+ }
+ }
+
+}
diff --git a/android/content/res/BridgeAssetManager.java b/android/content/res/BridgeAssetManager.java
index 2691e564..a1a4a196 100644
--- a/android/content/res/BridgeAssetManager.java
+++ b/android/content/res/BridgeAssetManager.java
@@ -36,7 +36,6 @@ public class BridgeAssetManager extends AssetManager {
// Note that AssetManager() creates a system AssetManager and we override it
// with our BridgeAssetManager.
AssetManager.sSystem = new BridgeAssetManager();
- AssetManager.sSystem.makeStringBlocks(null);
}
return AssetManager.sSystem;
}
diff --git a/android/content/res/BridgeTypedArray.java b/android/content/res/BridgeTypedArray.java
index 5536c4f6..95059938 100644
--- a/android/content/res/BridgeTypedArray.java
+++ b/android/content/res/BridgeTypedArray.java
@@ -29,7 +29,6 @@ import com.android.layoutlib.bridge.impl.ResourceHelper;
import com.android.resources.ResourceType;
import android.annotation.Nullable;
-import android.content.res.Resources.NotFoundException;
import android.content.res.Resources.Theme;
import android.graphics.Typeface;
import android.graphics.Typeface_Accessor;
@@ -676,6 +675,13 @@ public final class BridgeTypedArray extends TypedArray {
return idValue;
}
+ if ("text".equals(mNames[index])) {
+ // In a TextView, if the text is set from the attribute android:text, the correct
+ // behaviour is not to find a resourceId for the text, and to return the default value.
+ // So in this case, do not log a warning.
+ return defValue;
+ }
+
Bridge.getLog().warning(LayoutLog.TAG_RESOURCES_RESOLVE,
String.format(
"Unable to resolve id \"%1$s\" for attribute \"%2$s\"", value, mNames[index]),
diff --git a/android/content/res/ResourcesImpl.java b/android/content/res/ResourcesImpl.java
index 3239212a..97cb78bc 100644
--- a/android/content/res/ResourcesImpl.java
+++ b/android/content/res/ResourcesImpl.java
@@ -815,7 +815,7 @@ public class ResourcesImpl {
} finally {
stack.pop();
}
- } catch (Exception e) {
+ } catch (Exception | StackOverflowError e) {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
final NotFoundException rnf = new NotFoundException(
"File " + file + " from drawable resource ID #0x" + Integer.toHexString(id));
diff --git a/android/content/res/Resources_Delegate.java b/android/content/res/Resources_Delegate.java
index a32d5282..77ae90fb 100644
--- a/android/content/res/Resources_Delegate.java
+++ b/android/content/res/Resources_Delegate.java
@@ -886,6 +886,12 @@ public class Resources_Delegate {
}
@LayoutlibDelegate
+ static void getValueForDensity(Resources resources, int id, int density, TypedValue outValue,
+ boolean resolveRefs) throws NotFoundException {
+ getValue(resources, id, outValue, resolveRefs);
+ }
+
+ @LayoutlibDelegate
static XmlResourceParser getXml(Resources resources, int id) throws NotFoundException {
Pair<String, ResourceValue> v = getResourceValue(resources, id, mPlatformResourceFlag);
diff --git a/android/database/sqlite/SQLiteOpenHelper.java b/android/database/sqlite/SQLiteOpenHelper.java
index 49f357e6..a2991e6e 100644
--- a/android/database/sqlite/SQLiteOpenHelper.java
+++ b/android/database/sqlite/SQLiteOpenHelper.java
@@ -66,7 +66,7 @@ public abstract class SQLiteOpenHelper {
* created or opened until one of {@link #getWritableDatabase} or
* {@link #getReadableDatabase} is called.
*
- * @param context to use to open or create the database
+ * @param context to use for locating paths to the the database
* @param name of the database file, or null for an in-memory database
* @param factory to use for creating cursor objects, or null for the default
* @param version number of the database (starting at 1); if the database is older,
@@ -86,7 +86,7 @@ public abstract class SQLiteOpenHelper {
* <p>Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be
* used to handle corruption when sqlite reports database corruption.</p>
*
- * @param context to use to open or create the database
+ * @param context to use for locating paths to the the database
* @param name of the database file, or null for an in-memory database
* @param factory to use for creating cursor objects, or null for the default
* @param version number of the database (starting at 1); if the database is older,
@@ -107,7 +107,7 @@ public abstract class SQLiteOpenHelper {
* created or opened until one of {@link #getWritableDatabase} or
* {@link #getReadableDatabase} is called.
*
- * @param context to use to open or create the database
+ * @param context to use for locating paths to the the database
* @param name of the database file, or null for an in-memory database
* @param version number of the database (starting at 1); if the database is older,
* {@link #onUpgrade} will be used to upgrade the database; if the database is
@@ -128,7 +128,7 @@ public abstract class SQLiteOpenHelper {
* minimumSupportedVersion is found, it is simply deleted and a new database is created with the
* given name and version
*
- * @param context to use to open or create the database
+ * @param context to use for locating paths to the the database
* @param name the name of the database file, null for a temporary in-memory database
* @param factory to use for creating cursor objects, null for default
* @param version the required version of the database
diff --git a/android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java b/android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java
new file mode 100644
index 00000000..4709d350
--- /dev/null
+++ b/android/ext/services/autofill/AutofillFieldClassificationServiceImpl.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.ext.services.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.service.autofill.AutofillFieldClassificationService;
+import android.util.Log;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.util.List;
+
+public class AutofillFieldClassificationServiceImpl extends AutofillFieldClassificationService {
+
+ private static final String TAG = "AutofillFieldClassificationServiceImpl";
+ // TODO(b/70291841): set to false before launching
+ private static final boolean DEBUG = true;
+
+ @Nullable
+ @Override
+ public float[][] onGetScores(@Nullable String algorithmName,
+ @Nullable Bundle algorithmArgs, @NonNull List<AutofillValue> actualValues,
+ @NonNull List<String> userDataValues) {
+ if (ArrayUtils.isEmpty(actualValues) || ArrayUtils.isEmpty(userDataValues)) {
+ Log.w(TAG, "getScores(): empty currentvalues (" + actualValues + ") or userValues ("
+ + userDataValues + ")");
+ // TODO(b/70939974): add unit test
+ return null;
+ }
+ if (algorithmName != null && !algorithmName.equals(EditDistanceScorer.NAME)) {
+ Log.w(TAG, "Ignoring invalid algorithm (" + algorithmName + ") and using "
+ + EditDistanceScorer.NAME + " instead");
+ }
+
+ final String actualAlgorithmName = EditDistanceScorer.NAME;
+ final int actualValuesSize = actualValues.size();
+ final int userDataValuesSize = userDataValues.size();
+ if (DEBUG) {
+ Log.d(TAG, "getScores() will return a " + actualValuesSize + "x"
+ + userDataValuesSize + " matrix for " + actualAlgorithmName);
+ }
+ final float[][] scores = new float[actualValuesSize][userDataValuesSize];
+
+ final EditDistanceScorer algorithm = EditDistanceScorer.getInstance();
+ for (int i = 0; i < actualValuesSize; i++) {
+ for (int j = 0; j < userDataValuesSize; j++) {
+ final float score = algorithm.getScore(actualValues.get(i), userDataValues.get(j));
+ scores[i][j] = score;
+ }
+ }
+ return scores;
+ }
+}
diff --git a/android/service/autofill/EditDistanceScorer.java b/android/ext/services/autofill/EditDistanceScorer.java
index 0706b377..d2e804af 100644
--- a/android/service/autofill/EditDistanceScorer.java
+++ b/android/ext/services/autofill/EditDistanceScorer.java
@@ -13,11 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.service.autofill;
+package android.ext.services.autofill;
import android.annotation.NonNull;
-import android.os.Parcel;
-import android.os.Parcelable;
import android.view.autofill.AutofillValue;
/**
@@ -25,10 +23,12 @@ import android.view.autofill.AutofillValue;
* 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 {
+final class EditDistanceScorer {
private static final EditDistanceScorer sInstance = new EditDistanceScorer();
+ public static final String NAME = "EDIT_DISTANCE";
+
/**
* Gets the singleton instance.
*/
@@ -39,59 +39,30 @@ public final class EditDistanceScorer extends InternalScorer implements Scorer,
private EditDistanceScorer() {
}
- /** @hide */
- @Override
- public float getScore(@NonNull AutofillValue actualValue, @NonNull String userData) {
- if (actualValue == null || !actualValue.isText() || userData == null) return 0;
+ /**
+ * 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 float getScore(@NonNull AutofillValue actualValue, @NonNull String userDataValue) {
+ if (actualValue == null || !actualValue.isText() || userDataValue == 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;
+ if (total != userDataValue.length()) return 0F;
int matches = 0;
for (int i = 0; i < total; i++) {
if (Character.toLowerCase(textValue.charAt(i)) == Character
- .toLowerCase(userData.charAt(i))) {
+ .toLowerCase(userDataValue.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/graphics/BaseCanvas.java b/android/graphics/BaseCanvas.java
index 2d8c7179..627d5515 100644
--- a/android/graphics/BaseCanvas.java
+++ b/android/graphics/BaseCanvas.java
@@ -22,6 +22,8 @@ import android.annotation.Nullable;
import android.annotation.Size;
import android.graphics.Canvas.VertexMode;
import android.text.GraphicsOperations;
+import android.text.MeasuredParagraph;
+import android.text.MeasuredText;
import android.text.SpannableString;
import android.text.SpannedString;
import android.text.TextUtils;
@@ -453,7 +455,8 @@ public abstract class BaseCanvas {
throwIfHasHwBitmapInSwMode(paint);
nDrawTextRun(mNativeCanvasWrapper, text, index, count, contextIndex, contextCount,
- x, y, isRtl, paint.getNativeInstance());
+ x, y, isRtl, paint.getNativeInstance(), 0 /* measured text */,
+ 0 /* measured text offset */);
}
public void drawTextRun(@NonNull CharSequence text, int start, int end, int contextStart,
@@ -483,8 +486,20 @@ public abstract class BaseCanvas {
int len = end - start;
char[] buf = TemporaryBuffer.obtain(contextLen);
TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+ long measuredTextPtr = 0;
+ int measuredTextOffset = 0;
+ if (text instanceof MeasuredText) {
+ MeasuredText mt = (MeasuredText) text;
+ int paraIndex = mt.findParaIndex(start);
+ if (end <= mt.getParagraphEnd(paraIndex)) {
+ // Only suppor the same paragraph.
+ measuredTextPtr = mt.getMeasuredParagraph(paraIndex).getNativePtr();
+ measuredTextOffset = start - mt.getParagraphStart(paraIndex);
+ }
+ }
nDrawTextRun(mNativeCanvasWrapper, buf, start - contextStart, len,
- 0, contextLen, x, y, isRtl, paint.getNativeInstance());
+ 0, contextLen, x, y, isRtl, paint.getNativeInstance(),
+ measuredTextPtr, measuredTextOffset);
TemporaryBuffer.recycle(buf);
}
}
@@ -623,7 +638,8 @@ public abstract class BaseCanvas {
int contextStart, int contextEnd, float x, float y, boolean isRtl, long nativePaint);
private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count,
- int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint);
+ int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint,
+ long nativeMeasuredText, int measuredTextOffset);
private static native void nDrawTextOnPath(long nativeCanvas, char[] text, int index, int count,
long nativePath, float hOffset, float vOffset, int bidiFlags, long nativePaint);
diff --git a/android/graphics/BidiRenderer.java b/android/graphics/BidiRenderer.java
index 7b7dfa6c..3dc1d41c 100644
--- a/android/graphics/BidiRenderer.java
+++ b/android/graphics/BidiRenderer.java
@@ -231,6 +231,11 @@ public class BidiRenderer {
int[] ci = gv.getGlyphCharIndices(0, ng, null);
if (advances != null) {
for (int i = 0; i < ng; i++) {
+ if (mText[ci[i]] == '\uFEFF') {
+ // Workaround for bug in JetBrains JDK
+ // where the character \uFEFF is associated a glyph with non-zero width
+ continue;
+ }
int adv_idx = advancesIndex + ci[i];
advances[adv_idx] += gv.getGlyphMetrics(i).getAdvanceX();
}
diff --git a/android/graphics/ImageDecoder.java b/android/graphics/ImageDecoder.java
index 60416a72..3de050b5 100644
--- a/android/graphics/ImageDecoder.java
+++ b/android/graphics/ImageDecoder.java
@@ -16,45 +16,80 @@
package android.graphics;
+import static android.system.OsConstants.SEEK_CUR;
+import static android.system.OsConstants.SEEK_SET;
+
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.RawRes;
+import android.content.ContentResolver;
+import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.content.res.Resources;
+import android.graphics.drawable.AnimatedImageDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.NinePatchDrawable;
+import android.net.Uri;
+import android.util.Size;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+
+import libcore.io.IoUtils;
+import dalvik.system.CloseGuard;
import java.nio.ByteBuffer;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.ArrayIndexOutOfBoundsException;
+import java.lang.AutoCloseable;
import java.lang.NullPointerException;
import java.lang.RuntimeException;
import java.lang.annotation.Retention;
import static java.lang.annotation.RetentionPolicy.SOURCE;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* Class for decoding images as {@link Bitmap}s or {@link Drawable}s.
- * @hide
*/
-public final class ImageDecoder {
+public final class ImageDecoder implements AutoCloseable {
/**
* Source of the encoded image data.
*/
public static abstract class Source {
+ private Source() {}
+
/* @hide */
+ @Nullable
Resources getResources() { return null; }
/* @hide */
- void close() {}
+ int getDensity() { return Bitmap.DENSITY_NONE; }
/* @hide */
- abstract ImageDecoder createImageDecoder();
+ int computeDstDensity() {
+ Resources res = getResources();
+ if (res == null) {
+ return Bitmap.getDefaultDensity();
+ }
+
+ return res.getDisplayMetrics().densityDpi;
+ }
+
+ /* @hide */
+ @NonNull
+ abstract ImageDecoder createImageDecoder() throws IOException;
};
private static class ByteArraySource extends Source {
- ByteArraySource(byte[] data, int offset, int length) {
+ ByteArraySource(@NonNull byte[] data, int offset, int length) {
mData = data;
mOffset = offset;
mLength = length;
@@ -64,19 +99,19 @@ public final class ImageDecoder {
private final int mLength;
@Override
- public ImageDecoder createImageDecoder() {
+ public ImageDecoder createImageDecoder() throws IOException {
return nCreate(mData, mOffset, mLength);
}
}
private static class ByteBufferSource extends Source {
- ByteBufferSource(ByteBuffer buffer) {
+ ByteBufferSource(@NonNull ByteBuffer buffer) {
mBuffer = buffer;
}
private final ByteBuffer mBuffer;
@Override
- public ImageDecoder createImageDecoder() {
+ public ImageDecoder createImageDecoder() throws IOException {
if (!mBuffer.isDirect() && mBuffer.hasArray()) {
int offset = mBuffer.arrayOffset() + mBuffer.position();
int length = mBuffer.limit() - mBuffer.position();
@@ -86,61 +121,194 @@ public final class ImageDecoder {
}
}
- private static class ResourceSource extends Source {
- ResourceSource(Resources res, int resId)
- throws Resources.NotFoundException {
- // Test that the resource can be found.
- InputStream is = null;
+ private static class ContentResolverSource extends Source {
+ ContentResolverSource(@NonNull ContentResolver resolver, @NonNull Uri uri) {
+ mResolver = resolver;
+ mUri = uri;
+ }
+
+ private final ContentResolver mResolver;
+ private final Uri mUri;
+
+ @Override
+ public ImageDecoder createImageDecoder() throws IOException {
+ AssetFileDescriptor assetFd = null;
+ try {
+ if (mUri.getScheme() == ContentResolver.SCHEME_CONTENT) {
+ assetFd = mResolver.openTypedAssetFileDescriptor(mUri,
+ "image/*", null);
+ } else {
+ assetFd = mResolver.openAssetFileDescriptor(mUri, "r");
+ }
+ } catch (FileNotFoundException e) {
+ // Some images cannot be opened as AssetFileDescriptors (e.g.
+ // bmp, ico). Open them as InputStreams.
+ InputStream is = mResolver.openInputStream(mUri);
+ if (is == null) {
+ throw new FileNotFoundException(mUri.toString());
+ }
+
+ return createFromStream(is);
+ }
+
+ final FileDescriptor fd = assetFd.getFileDescriptor();
+ final long offset = assetFd.getStartOffset();
+
+ ImageDecoder decoder = null;
try {
- is = res.openRawResource(resId);
+ try {
+ Os.lseek(fd, offset, SEEK_SET);
+ decoder = nCreate(fd);
+ } catch (ErrnoException e) {
+ decoder = createFromStream(new FileInputStream(fd));
+ }
} finally {
- if (is != null) {
- try {
- is.close();
- } catch (IOException e) {
- }
+ if (decoder == null) {
+ IoUtils.closeQuietly(assetFd);
+ } else {
+ decoder.mAssetFd = assetFd;
}
}
+ return decoder;
+ }
+ }
+ @NonNull
+ private static ImageDecoder createFromFile(@NonNull File file) throws IOException {
+ FileInputStream stream = new FileInputStream(file);
+ FileDescriptor fd = stream.getFD();
+ try {
+ Os.lseek(fd, 0, SEEK_CUR);
+ } catch (ErrnoException e) {
+ return createFromStream(stream);
+ }
+
+ ImageDecoder decoder = null;
+ try {
+ decoder = nCreate(fd);
+ } finally {
+ if (decoder == null) {
+ IoUtils.closeQuietly(stream);
+ } else {
+ decoder.mInputStream = stream;
+ }
+ }
+ return decoder;
+ }
+
+ @NonNull
+ private static ImageDecoder createFromStream(@NonNull InputStream is) throws IOException {
+ // Arbitrary size matches BitmapFactory.
+ byte[] storage = new byte[16 * 1024];
+ ImageDecoder decoder = null;
+ try {
+ decoder = nCreate(is, storage);
+ } finally {
+ if (decoder == null) {
+ IoUtils.closeQuietly(is);
+ } else {
+ decoder.mInputStream = is;
+ decoder.mTempStorage = storage;
+ }
+ }
+
+ return decoder;
+ }
+
+ private static class InputStreamSource extends Source {
+ InputStreamSource(Resources res, InputStream is, int inputDensity) {
+ if (is == null) {
+ throw new IllegalArgumentException("The InputStream cannot be null");
+ }
mResources = res;
- mResId = resId;
+ mInputStream = is;
+ mInputDensity = res != null ? inputDensity : Bitmap.DENSITY_NONE;
}
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;
+ final int mInputDensity;
@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?");
+ public int getDensity() { return mInputDensity; }
+
+ @Override
+ public ImageDecoder createImageDecoder() throws IOException {
+
+ synchronized (this) {
+ if (mInputStream == null) {
+ throw new IOException("Cannot reuse InputStreamSource");
+ }
+ InputStream is = mInputStream;
+ mInputStream = null;
+ return createFromStream(is);
}
- long asset = ((AssetManager.AssetInputStream) mInputStream).getNativeAsset();
- return nCreate(asset);
}
+ }
+
+ private static class ResourceSource extends Source {
+ ResourceSource(@NonNull Resources res, int resId) {
+ mResources = res;
+ mResId = resId;
+ mResDensity = Bitmap.DENSITY_NONE;
+ }
+
+ final Resources mResources;
+ final int mResId;
+ int mResDensity;
@Override
- public void close() {
+ public Resources getResources() { return mResources; }
+
+ @Override
+ public int getDensity() { return mResDensity; }
+
+ @Override
+ public ImageDecoder createImageDecoder() throws IOException {
+ // This is just used in order to access the underlying Asset and
+ // keep it alive. FIXME: Can we skip creating this object?
+ InputStream is = null;
+ ImageDecoder decoder = null;
+ TypedValue value = new TypedValue();
try {
- mInputStream.close();
- } catch (IOException e) {
+ is = mResources.openRawResource(mResId, value);
+
+ if (value.density == TypedValue.DENSITY_DEFAULT) {
+ mResDensity = DisplayMetrics.DENSITY_DEFAULT;
+ } else if (value.density != TypedValue.DENSITY_NONE) {
+ mResDensity = value.density;
+ }
+
+ if (!(is instanceof AssetManager.AssetInputStream)) {
+ // This should never happen.
+ throw new RuntimeException("Resource is not an asset?");
+ }
+ long asset = ((AssetManager.AssetInputStream) is).getNativeAsset();
+ decoder = nCreate(asset);
} finally {
- mInputStream = null;
+ if (decoder == null) {
+ IoUtils.closeQuietly(is);
+ } else {
+ decoder.mInputStream = is;
+ }
}
+ return decoder;
+ }
+ }
+
+ private static class FileSource extends Source {
+ FileSource(@NonNull File file) {
+ mFile = file;
+ }
+
+ private final File mFile;
+
+ @Override
+ public ImageDecoder createImageDecoder() throws IOException {
+ return createFromFile(mFile);
}
}
@@ -148,29 +316,35 @@ public final class ImageDecoder {
* 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?
+ private final Size mSize;
+ private ImageDecoder mDecoder;
- ImageInfo(int width, int height) {
- this.width = width;
- this.height = height;
+ private ImageInfo(@NonNull ImageDecoder decoder) {
+ mSize = new Size(decoder.mWidth, decoder.mHeight);
+ mDecoder = decoder;
}
- };
- /**
- * Used if the provided data is incomplete.
- *
- * There may be a partial image to display.
- */
- public class IncompleteException extends Exception {};
+ /**
+ * Size of the image, without scaling or cropping.
+ */
+ @NonNull
+ public Size getSize() {
+ return mSize;
+ }
+
+ /**
+ * The mimeType of the image.
+ */
+ @NonNull
+ public String getMimeType() {
+ return mDecoder.getMimeType();
+ }
+ };
/**
- * Used if the provided data is corrupt.
- *
- * There may be a partial image to display.
+ * Thrown if the provided data is incomplete.
*/
- public class CorruptException extends Exception {};
+ public static class IncompleteException extends IOException {};
/**
* Optional listener supplied to {@link #decodeDrawable} or
@@ -180,78 +354,145 @@ public final class ImageDecoder {
/**
* 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.
+ * @param info Information about the encoded image.
+ * @param source that created the decoder.
*/
- public void onHeaderDecoded(ImageInfo info, ImageDecoder decoder);
+ public void onHeaderDecoded(@NonNull ImageDecoder decoder,
+ @NonNull ImageInfo info, @NonNull Source source);
};
/**
+ * An Exception was thrown reading the {@link Source}.
+ */
+ public static final int ERROR_SOURCE_EXCEPTION = 1;
+
+ /**
+ * The encoded data was incomplete.
+ */
+ public static final int ERROR_SOURCE_INCOMPLETE = 2;
+
+ /**
+ * The encoded data contained an error.
+ */
+ public static final int ERROR_SOURCE_ERROR = 3;
+
+ @Retention(SOURCE)
+ @IntDef({ ERROR_SOURCE_EXCEPTION, ERROR_SOURCE_INCOMPLETE, ERROR_SOURCE_ERROR })
+ public @interface Error {};
+
+ /**
* Optional listener supplied to the ImageDecoder.
+ *
+ * Without this listener, errors will throw {@link java.io.IOException}.
*/
- public static interface OnExceptionListener {
+ public static interface OnPartialImageListener {
/**
- * 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?
+ * Called when there is only a partial image to display.
+ *
+ * If decoding is interrupted after having decoded a partial image,
+ * this listener lets the client know that and allows them to
+ * optionally finish the rest of the decode/creation process to create
+ * a partial {@link Drawable}/{@link Bitmap}.
*
- * @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.
+ * @param error indicating what interrupted the decode.
+ * @param source that had the error.
+ * @return True to create and return a {@link Drawable}/{@link Bitmap}
+ * with partial data. False (which is the default) to abort the
+ * decode and throw {@link java.io.IOException}.
*/
- public boolean onException(Exception e);
+ public boolean onPartialImage(@Error int error, @NonNull Source source);
};
// Fields
- private long mNativePtr;
- private final int mWidth;
- private final int mHeight;
+ private long mNativePtr;
+ private final int mWidth;
+ private final int mHeight;
+ private final boolean mAnimated;
private int mDesiredWidth;
private int mDesiredHeight;
- private int mAllocator = DEFAULT_ALLOCATOR;
+ private int mAllocator = ALLOCATOR_DEFAULT;
private boolean mRequireUnpremultiplied = false;
private boolean mMutable = false;
private boolean mPreferRamOverQuality = false;
private boolean mAsAlphaMask = false;
private Rect mCropRect;
+ private Source mSource;
- private PostProcess mPostProcess;
- private OnExceptionListener mOnExceptionListener;
+ private PostProcessor mPostProcessor;
+ private OnPartialImageListener mOnPartialImageListener;
+ // Objects for interacting with the input.
+ private InputStream mInputStream;
+ private byte[] mTempStorage;
+ private AssetFileDescriptor mAssetFd;
+ private final AtomicBoolean mClosed = new AtomicBoolean();
+ private final CloseGuard mCloseGuard = CloseGuard.get();
/**
- * Private constructor called by JNI. {@link #recycle} must be
+ * Private constructor called by JNI. {@link #close} must be
* called after decoding to delete native resources.
*/
@SuppressWarnings("unused")
- private ImageDecoder(long nativePtr, int width, int height) {
+ private ImageDecoder(long nativePtr, int width, int height,
+ boolean animated) {
mNativePtr = nativePtr;
mWidth = width;
mHeight = height;
mDesiredWidth = width;
mDesiredHeight = height;
+ mAnimated = animated;
+ mCloseGuard.open("close");
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+
+ close();
+ } finally {
+ super.finalize();
+ }
}
/**
* Create a new {@link Source} from an asset.
+ * @hide
*
* @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.
*/
+ @NonNull
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 {@link android.net.Uri}.
+ *
+ * @param cr to retrieve from.
+ * @param uri of the image file.
+ * @return a new Source object, which can be passed to
+ * {@link #decodeDrawable} or {@link #decodeBitmap}.
+ */
+ @NonNull
+ public static Source createSource(@NonNull ContentResolver cr,
+ @NonNull Uri uri) {
+ return new ContentResolverSource(cr, uri);
+ }
+
+ /**
* 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.
@@ -259,8 +500,9 @@ public final class ImageDecoder {
* @throws NullPointerException if data is null.
* @throws ArrayIndexOutOfBoundsException if offset and length are
* not within data.
+ * @hide
*/
- // TODO: Overloads that don't use offset, length
+ @NonNull
public static Source createSource(@NonNull byte[] data, int offset,
int length) throws ArrayIndexOutOfBoundsException {
if (data == null) {
@@ -275,39 +517,75 @@ public final class ImageDecoder {
}
/**
+ * See {@link #createSource(byte[], int, int).
+ * @hide
+ */
+ @NonNull
+ public static Source createSource(@NonNull byte[] data) {
+ return createSource(data, 0, data.length);
+ }
+
+ /**
* Create a new {@link Source} from a {@link java.nio.ByteBuffer}.
*
- * The returned {@link Source} effectively takes ownership of the
+ * <p>The returned {@link Source} effectively takes ownership of the
* {@link java.nio.ByteBuffer}; i.e. no other code should modify it after
- * this call.
+ * this call.</p>
*
- * Decoding will start from {@link java.nio.ByteBuffer#position()}.
+ * Decoding will start from {@link java.nio.ByteBuffer#position()}. The
+ * position after decoding is undefined.
*/
- public static Source createSource(ByteBuffer buffer) {
+ @NonNull
+ public static Source createSource(@NonNull ByteBuffer buffer) {
return new ByteBufferSource(buffer);
}
/**
+ * Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable)
+ * @hide
+ */
+ public static Source createSource(Resources res, InputStream is) {
+ return new InputStreamSource(res, is, Bitmap.getDefaultDensity());
+ }
+
+ /**
+ * Internal API used to generate bitmaps for use by Drawables (i.e. BitmapDrawable)
+ * @hide
+ */
+ public static Source createSource(Resources res, InputStream is, int density) {
+ return new InputStreamSource(res, is, density);
+ }
+
+ /**
+ * Create a new {@link Source} from a {@link java.io.File}.
+ */
+ @NonNull
+ public static Source createSource(@NonNull File file) {
+ return new FileSource(file);
+ }
+
+ /**
* Return the width and height of a given sample size.
*
- * This takes an input that functions like
+ * <p>This takes an input that functions like
* {@link BitmapFactory.Options#inSampleSize}. It returns a width and
* height that can be acheived by sampling the encoded image. Other widths
* and heights may be supported, but will require an additional (internal)
* scaling step. Such internal scaling is *not* supported with
- * {@link #requireUnpremultiplied}.
+ * {@link #setRequireUnpremultiplied} set to {@code true}.</p>
*
* @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.
+ * @return {@link android.util.Size} of the width and height after
+ * sampling.
*/
- public Point getSampledSize(int sampleSize) {
+ @NonNull
+ public Size getSampledSize(int sampleSize) {
if (sampleSize <= 0) {
throw new IllegalArgumentException("sampleSize must be positive! "
+ "provided " + sampleSize);
}
if (mNativePtr == 0) {
- throw new IllegalStateException("ImageDecoder is recycled!");
+ throw new IllegalStateException("ImageDecoder is closed!");
}
return nGetSampledSize(mNativePtr, sampleSize);
@@ -320,7 +598,7 @@ public final class ImageDecoder {
* @param width must be greater than 0.
* @param height must be greater than 0.
*/
- public void resize(int width, int height) {
+ public void setResize(int width, int height) {
if (width <= 0 || height <= 0) {
throw new IllegalArgumentException("Dimensions must be positive! "
+ "provided (" + width + ", " + height + ")");
@@ -333,14 +611,18 @@ public final class ImageDecoder {
/**
* Resize based on a sample size.
*
- * This has the same effect as passing the result of
- * {@link #getSampledSize} to {@link #resize(int, int)}.
+ * <p>This has the same effect as passing the result of
+ * {@link #getSampledSize} to {@link #setResize(int, int)}.</p>
*
* @param sampleSize Sampling rate of the encoded image.
*/
- public void resize(int sampleSize) {
- Point dimensions = this.getSampledSize(sampleSize);
- this.resize(dimensions.x, dimensions.y);
+ public void setResize(int sampleSize) {
+ Size size = this.getSampledSize(sampleSize);
+ this.setResize(size.getWidth(), size.getHeight());
+ }
+
+ private boolean requestedResize() {
+ return mWidth != mDesiredWidth || mHeight != mDesiredHeight;
}
// These need to stay in sync with ImageDecoder.cpp's Allocator enum.
@@ -352,7 +634,7 @@ public final class ImageDecoder {
* switch to software when HARDWARE is incompatible, e.g.
* {@link #setMutable}, {@link #setAsAlphaMask}.
*/
- public static final int DEFAULT_ALLOCATOR = 0;
+ public static final int ALLOCATOR_DEFAULT = 0;
/**
* Use a software allocation for the pixel memory.
@@ -360,28 +642,29 @@ public final class ImageDecoder {
* Useful for drawing to a software {@link Canvas} or for
* accessing the pixels on the final output.
*/
- public static final int SOFTWARE_ALLOCATOR = 1;
+ public static final int ALLOCATOR_SOFTWARE = 1;
/**
* Use shared memory for the pixel memory.
*
* Useful for sharing across processes.
*/
- public static final int SHARED_MEMORY_ALLOCATOR = 2;
+ public static final int ALLOCATOR_SHARED_MEMORY = 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}.
+ * When this is combined with incompatible options, like
+ * {@link #setMutable} or {@link #setAsAlphaMask}, {@link #decodeDrawable}
+ * / {@link #decodeBitmap} will throw an
+ * {@link java.lang.IllegalStateException}.
*/
- public static final int HARDWARE_ALLOCATOR = 3;
+ public static final int ALLOCATOR_HARDWARE = 3;
/** @hide **/
@Retention(SOURCE)
- @IntDef({ DEFAULT_ALLOCATOR, SOFTWARE_ALLOCATOR, SHARED_MEMORY_ALLOCATOR,
- HARDWARE_ALLOCATOR })
+ @IntDef({ ALLOCATOR_DEFAULT, ALLOCATOR_SOFTWARE, ALLOCATOR_SHARED_MEMORY,
+ ALLOCATOR_HARDWARE })
public @interface Allocator {};
/**
@@ -389,140 +672,147 @@ public final class ImageDecoder {
*
* 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) {
+ if (allocator < ALLOCATOR_DEFAULT || allocator > ALLOCATOR_HARDWARE) {
throw new IllegalArgumentException("invalid allocator " + allocator);
}
mAllocator = allocator;
}
/**
- * Create a {@link Bitmap} with unpremultiplied pixels.
+ * Specify whether the {@link Bitmap} should have 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
+ * this method with a value of {@code true} will result in
+ * {@link #decodeBitmap} returning a {@link Bitmap} with unpremultiplied
+ * pixels. See {@link Bitmap#isPremultiplied}. This is incompatible with
* {@link #decodeDrawable}; attempting to decode an unpremultiplied
* {@link Drawable} will throw an {@link java.lang.IllegalStateException}.
*/
- public void requireUnpremultiplied() {
- mRequireUnpremultiplied = true;
+ public void setRequireUnpremultiplied(boolean requireUnpremultiplied) {
+ mRequireUnpremultiplied = requireUnpremultiplied;
}
/**
* Modify the image after decoding and scaling.
*
- * This allows adding effects prior to returning a {@link Drawable} or
+ * <p>This allows adding effects prior to returning a {@link Drawable} or
* {@link Bitmap}. For a {@code Drawable} or an immutable {@code Bitmap},
- * this is the only way to process the image after decoding.
+ * this is the only way to process the image after decoding.</p>
*
- * If set on a nine-patch image, the nine-patch data is ignored.
+ * <p>If set on a nine-patch image, the nine-patch data is ignored.</p>
*
- * For an animated image, the drawing commands drawn on the {@link Canvas}
- * will be recorded immediately and then applied to each frame.
+ * <p>For an animated image, the drawing commands drawn on the
+ * {@link Canvas} will be recorded immediately and then applied to each
+ * frame.</p>
*/
- public void setPostProcess(PostProcess p) {
- mPostProcess = p;
+ public void setPostProcessor(@Nullable PostProcessor p) {
+ mPostProcessor = p;
}
/**
- * Set (replace) the {@link OnExceptionListener} on this object.
+ * Set (replace) the {@link OnPartialImageListener} on this object.
*
* Will be called if there is an error in the input. Without one, a
* partial {@link Bitmap} will be created.
*/
- public void setOnExceptionListener(OnExceptionListener l) {
- mOnExceptionListener = l;
+ public void setOnPartialImageListener(@Nullable OnPartialImageListener l) {
+ mOnPartialImageListener = 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.
+ * <p>{@code subset} must be contained within the size set by
+ * {@link #setResize} or the bounds of the image if setResize was not
+ * called. Otherwise an {@link IllegalStateException} will be thrown by
+ * {@link #decodeDrawable}/{@link #decodeBitmap}.</p>
*
- * NOT intended as a replacement for
+ * <p>NOT intended as a replacement for
* {@link BitmapRegionDecoder#decodeRegion}. This supports all formats,
- * but merely crops the output.
+ * but merely crops the output.</p>
*/
- public void crop(Rect subset) {
+ public void setCrop(@Nullable Rect subset) {
mCropRect = subset;
}
/**
- * Create a mutable {@link Bitmap}.
+ * Specify whether the {@link Bitmap} should be mutable.
*
- * By default, a {@link Bitmap} created will be immutable, but that can be
- * changed with this call.
+ * <p>By default, a {@link Bitmap} created will be immutable, but that can
+ * be changed with this call.</p>
*
- * 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}.
+ * <p>Mutable Bitmaps are incompatible with {@link #ALLOCATOR_HARDWARE},
+ * because {@link Bitmap.Config#HARDWARE} Bitmaps cannot be mutable.
+ * Attempting to combine them will throw an
+ * {@link java.lang.IllegalStateException}.</p>
*
- * 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}
+ * <p>Mutable Bitmaps are also 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}.</p>
*/
- public void setMutable() {
- mMutable = true;
+ public void setMutable(boolean mutable) {
+ mMutable = mutable;
}
/**
- * Potentially save RAM at the expense of quality.
+ * Specify whether to 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.
+ * Setting this to {@code true} may result in a {@link Bitmap} with a
+ * denser {@link Bitmap.Config}, depending on the image. For example, for
+ * an opaque {@link Bitmap}, this may result in a {@link Bitmap.Config}
+ * with no alpha information.
*/
- public void setPreferRamOverQuality() {
- mPreferRamOverQuality = true;
+ public void setPreferRamOverQuality(boolean preferRamOverQuality) {
+ mPreferRamOverQuality = preferRamOverQuality;
}
/**
- * Potentially treat the output as an alpha mask.
+ * Specify whether to 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.
+ * <p>If this is set to {@code true} and the image is encoded in a format
+ * with only one channel, treat that channel as alpha. Otherwise this call has
+ * no effect.</p>
*
- * Incompatible with {@link #HARDWARE_ALLOCATOR}. Trying to combine them
- * will throw an {@link java.lang.IllegalStateException}.
+ * <p>setAsAlphaMask is incompatible with {@link #ALLOCATOR_HARDWARE}. Trying to
+ * combine them will result in {@link #decodeDrawable}/
+ * {@link #decodeBitmap} throwing an
+ * {@link java.lang.IllegalStateException}.</p>
*/
- public void setAsAlphaMask() {
- mAsAlphaMask = true;
+ public void setAsAlphaMask(boolean asAlphaMask) {
+ mAsAlphaMask = asAlphaMask;
}
- /**
- * 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) {
+ @Override
+ public void close() {
+ mCloseGuard.close();
+ if (!mClosed.compareAndSet(false, true)) {
return;
}
- nRecycle(mNativePtr);
+ nClose(mNativePtr);
mNativePtr = 0;
+
+ IoUtils.closeQuietly(mInputStream);
+ IoUtils.closeQuietly(mAssetFd);
+
+ mInputStream = null;
+ mAssetFd = null;
+ mTempStorage = null;
}
private void checkState() {
if (mNativePtr == 0) {
- throw new IllegalStateException("Cannot reuse ImageDecoder.Source!");
+ throw new IllegalStateException("Cannot use closed ImageDecoder!");
}
checkSubset(mDesiredWidth, mDesiredHeight, mCropRect);
- if (mAllocator == HARDWARE_ALLOCATOR) {
+ if (mAllocator == ALLOCATOR_HARDWARE) {
if (mMutable) {
throw new IllegalStateException("Cannot make mutable HARDWARE Bitmap!");
}
@@ -531,7 +821,7 @@ public final class ImageDecoder {
}
}
- if (mPostProcess != null && mRequireUnpremultiplied) {
+ if (mPostProcessor != null && mRequireUnpremultiplied) {
throw new IllegalStateException("Cannot draw to unpremultiplied pixels!");
}
}
@@ -546,54 +836,88 @@ public final class ImageDecoder {
}
}
- /**
- * Create a {@link Drawable}.
- */
- public static Drawable decodeDrawable(Source src, OnHeaderDecodedListener listener) {
- ImageDecoder decoder = src.createImageDecoder();
- if (decoder == null) {
- return null;
- }
+ @NonNull
+ private Bitmap decodeBitmap() throws IOException {
+ checkState();
+ // nDecodeBitmap calls onPartialImage only if mOnPartialImageListener
+ // exists
+ ImageDecoder partialImagePtr = mOnPartialImageListener == null ? null : this;
+ // nDecodeBitmap calls postProcessAndRelease only if mPostProcessor
+ // exists.
+ ImageDecoder postProcessPtr = mPostProcessor == null ? null : this;
+ return nDecodeBitmap(mNativePtr, partialImagePtr,
+ postProcessPtr, mDesiredWidth, mDesiredHeight, mCropRect,
+ mMutable, mAllocator, mRequireUnpremultiplied,
+ mPreferRamOverQuality, mAsAlphaMask);
- 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!");
+ private void callHeaderDecoded(@Nullable OnHeaderDecodedListener listener,
+ @NonNull Source src) {
+ if (listener != null) {
+ ImageInfo info = new ImageInfo(this);
+ try {
+ listener.onHeaderDecoded(this, info, src);
+ } finally {
+ info.mDecoder = null;
+ }
}
+ }
- if (decoder.mMutable) {
- throw new IllegalStateException("Cannot decode a mutable Drawable!");
- }
+ /**
+ * Create a {@link Drawable} from a {@code Source}.
+ *
+ * @param src representing the encoded image.
+ * @param listener for learning the {@link ImageInfo} and changing any
+ * default settings on the {@code ImageDecoder}. If not {@code null},
+ * this will be called on the same thread as {@code decodeDrawable}
+ * before that method returns.
+ * @return Drawable for displaying the image.
+ * @throws IOException if {@code src} is not found, is an unsupported
+ * format, or cannot be decoded for any reason.
+ */
+ @NonNull
+ public static Drawable decodeDrawable(@NonNull Source src,
+ @Nullable OnHeaderDecodedListener listener) throws IOException {
+ try (ImageDecoder decoder = src.createImageDecoder()) {
+ decoder.mSource = src;
+ decoder.callHeaderDecoded(listener, src);
+
+ 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!");
+ }
- 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;
+ if (decoder.mMutable) {
+ throw new IllegalStateException("Cannot decode a mutable " +
+ "Drawable!");
}
- Resources res = src.getResources();
- if (res == null) {
- bm.setDensity(Bitmap.DENSITY_NONE);
+ // this call potentially manipulates the decoder so it must be performed prior to
+ // decoding the bitmap and after decode set the density on the resulting bitmap
+ final int srcDensity = computeDensity(src, decoder);
+ if (decoder.mAnimated) {
+ // AnimatedImageDrawable calls postProcessAndRelease only if
+ // mPostProcessor exists.
+ ImageDecoder postProcessPtr = decoder.mPostProcessor == null ?
+ null : decoder;
+ Drawable d = new AnimatedImageDrawable(decoder.mNativePtr,
+ postProcessPtr, decoder.mDesiredWidth,
+ decoder.mDesiredHeight, srcDensity,
+ src.computeDstDensity(), decoder.mCropRect,
+ decoder.mInputStream, decoder.mAssetFd);
+ // d has taken ownership of these objects.
+ decoder.mInputStream = null;
+ decoder.mAssetFd = null;
+ return d;
}
+ Bitmap bm = decoder.decodeBitmap();
+ bm.setDensity(srcDensity);
+
+ Resources res = src.getResources();
byte[] np = bm.getNinePatchChunk();
if (np != null && NinePatch.isNinePatchChunk(np)) {
Rect opticalInsets = new Rect();
@@ -604,62 +928,134 @@ public final class ImageDecoder {
opticalInsets, null);
}
- // TODO: Handle animation.
return new BitmapDrawable(res, bm);
- } finally {
- decoder.recycle();
- src.close();
}
}
/**
- * Create a {@link Bitmap}.
+ * See {@link #decodeDrawable(Source, OnHeaderDecodedListener)}.
*/
- public static Bitmap decodeBitmap(Source src, OnHeaderDecodedListener listener) {
- ImageDecoder decoder = src.createImageDecoder();
- if (decoder == null) {
- return null;
+ @NonNull
+ public static Drawable decodeDrawable(@NonNull Source src)
+ throws IOException {
+ return decodeDrawable(src, null);
+ }
+
+ /**
+ * Create a {@link Bitmap} from a {@code Source}.
+ *
+ * @param src representing the encoded image.
+ * @param listener for learning the {@link ImageInfo} and changing any
+ * default settings on the {@code ImageDecoder}. If not {@code null},
+ * this will be called on the same thread as {@code decodeBitmap}
+ * before that method returns.
+ * @return Bitmap containing the image.
+ * @throws IOException if {@code src} is not found, is an unsupported
+ * format, or cannot be decoded for any reason.
+ */
+ @NonNull
+ public static Bitmap decodeBitmap(@NonNull Source src,
+ @Nullable OnHeaderDecodedListener listener) throws IOException {
+ try (ImageDecoder decoder = src.createImageDecoder()) {
+ decoder.mSource = src;
+ decoder.callHeaderDecoded(listener, src);
+
+ // this call potentially manipulates the decoder so it must be performed prior to
+ // decoding the bitmap
+ final int srcDensity = computeDensity(src, decoder);
+ Bitmap bm = decoder.decodeBitmap();
+ bm.setDensity(srcDensity);
+ return bm;
}
+ }
- if (listener != null) {
- ImageInfo info = new ImageInfo(decoder.mWidth, decoder.mHeight);
- listener.onHeaderDecoded(info, decoder);
+ // This method may modify the decoder so it must be called prior to performing the decode
+ private static int computeDensity(@NonNull Source src, @NonNull ImageDecoder decoder) {
+ // if the caller changed the size then we treat the density as unknown
+ if (decoder.requestedResize()) {
+ return Bitmap.DENSITY_NONE;
+ }
+
+ // Special stuff for compatibility mode: if the target density is not
+ // the same as the display density, but the resource -is- the same as
+ // the display density, then don't scale it down to the target density.
+ // This allows us to load the system's density-correct resources into
+ // an application in compatibility mode, without scaling those down
+ // to the compatibility density only to have them scaled back up when
+ // drawn to the screen.
+ Resources res = src.getResources();
+ final int srcDensity = src.getDensity();
+ if (res != null && res.getDisplayMetrics().noncompatDensityDpi == srcDensity) {
+ return srcDensity;
+ }
+
+ // downscale the bitmap if the asset has a higher density than the default
+ final int dstDensity = src.computeDstDensity();
+ if (srcDensity != Bitmap.DENSITY_NONE && srcDensity > dstDensity) {
+ float scale = (float) dstDensity / srcDensity;
+ int scaledWidth = (int) (decoder.mWidth * scale + 0.5f);
+ int scaledHeight = (int) (decoder.mHeight * scale + 0.5f);
+ decoder.setResize(scaledWidth, scaledHeight);
+ return dstDensity;
}
- decoder.checkState();
+ return srcDensity;
+ }
+
+ @NonNull
+ private String getMimeType() {
+ return nGetMimeType(mNativePtr);
+ }
+
+ /**
+ * See {@link #decodeBitmap(Source, OnHeaderDecodedListener)}.
+ */
+ @NonNull
+ public static Bitmap decodeBitmap(@NonNull Source src) throws IOException {
+ return decodeBitmap(src, null);
+ }
+ /**
+ * Private method called by JNI.
+ */
+ @SuppressWarnings("unused")
+ private int postProcessAndRelease(@NonNull Canvas canvas) {
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);
+ return mPostProcessor.onPostProcess(canvas);
} finally {
- decoder.recycle();
- src.close();
+ canvas.release();
}
}
- private static native ImageDecoder nCreate(long asset);
+ /**
+ * Private method called by JNI.
+ */
+ @SuppressWarnings("unused")
+ private boolean onPartialImage(@Error int error) {
+ return mOnPartialImageListener.onPartialImage(error, mSource);
+ }
+
+ private static native ImageDecoder nCreate(long asset) throws IOException;
private static native ImageDecoder nCreate(ByteBuffer buffer,
int position,
- int limit);
+ int limit) throws IOException;
private static native ImageDecoder nCreate(byte[] data, int offset,
- int length);
+ int length) throws IOException;
+ private static native ImageDecoder nCreate(InputStream is, byte[] storage);
+ // The fd must be seekable.
+ private static native ImageDecoder nCreate(FileDescriptor fd) throws IOException;
+ @NonNull
private static native Bitmap nDecodeBitmap(long nativePtr,
- OnExceptionListener listener,
- PostProcess postProcess,
+ @Nullable ImageDecoder partialImageListener,
+ @Nullable ImageDecoder postProcessor,
int width, int height,
- Rect cropRect, boolean mutable,
+ @Nullable 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);
+ boolean preferRamOverQuality, boolean asAlphaMask)
+ throws IOException;
+ private static native Size nGetSampledSize(long nativePtr,
+ int sampleSize);
+ private static native void nGetPadding(long nativePtr, @NonNull Rect outRect);
+ private static native void nClose(long nativePtr);
+ private static native String nGetMimeType(long nativePtr);
}
diff --git a/android/graphics/ImageDecoder_Delegate.java b/android/graphics/ImageDecoder_Delegate.java
new file mode 100644
index 00000000..d9fe9bf2
--- /dev/null
+++ b/android/graphics/ImageDecoder_Delegate.java
@@ -0,0 +1,63 @@
+/*
+ * 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.graphics;
+
+import com.android.tools.layoutlib.annotations.LayoutlibDelegate;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.ImageDecoder.InputStreamSource;
+import android.graphics.ImageDecoder.OnHeaderDecodedListener;
+import android.graphics.ImageDecoder.Source;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+
+import java.io.IOException;
+
+public class ImageDecoder_Delegate {
+ @LayoutlibDelegate
+ static Bitmap decodeBitmap(@NonNull Source src, @Nullable OnHeaderDecodedListener listener)
+ throws IOException {
+ TypedValue value = new TypedValue();
+ value.density = src.getDensity();
+ return BitmapFactory.decodeResourceStream(src.getResources(), value,
+ ((InputStreamSource) src).mInputStream, null, null);
+ }
+
+ @LayoutlibDelegate
+ static Bitmap decodeBitmap(@NonNull Source src) throws IOException {
+ return decodeBitmap(src, null);
+ }
+
+ @LayoutlibDelegate
+ static Bitmap decodeBitmap(ImageDecoder thisDecoder) {
+ return null;
+ }
+
+ @LayoutlibDelegate
+ static Drawable decodeDrawable(@NonNull Source src, @Nullable OnHeaderDecodedListener listener)
+ throws IOException {
+ Bitmap bitmap = decodeBitmap(src, listener);
+ return new BitmapDrawable(src.getResources(), bitmap);
+ }
+
+ @LayoutlibDelegate
+ static Drawable decodeDrawable(@NonNull Source src) throws IOException {
+ return decodeDrawable(src, null);
+ }
+}
diff --git a/android/graphics/Paint.java b/android/graphics/Paint.java
index 317144a2..5a80ee28 100644
--- a/android/graphics/Paint.java
+++ b/android/graphics/Paint.java
@@ -2742,7 +2742,7 @@ public class Paint {
* @param offset index of caret position
* @return width measurement between start and offset
*/
- public float getRunAdvance(@NonNull CharSequence text, int start, int end, int contextStart,
+ public float getRunAdvance(CharSequence text, int start, int end, int contextStart,
int contextEnd, boolean isRtl, int offset) {
if (text == null) {
throw new IllegalArgumentException("text cannot be null");
diff --git a/android/graphics/PostProcess.java b/android/graphics/PostProcessor.java
index c5a31e82..b1712e92 100644
--- a/android/graphics/PostProcess.java
+++ b/android/graphics/PostProcessor.java
@@ -20,38 +20,38 @@ 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
+ * <p>The image being processed may be a {@link Drawable}, {@link Bitmap} or frame
* of an animated image produced by {@link ImageDecoder}. This is called before
- * the requested object is returned.
+ * the requested object is returned.</p>
*
- * This custom processing also applies to image types that are otherwise
- * immutable, such as {@link Bitmap.Config#HARDWARE}.
+ * <p>This custom processing also applies to image types that are otherwise
+ * immutable, such as {@link Bitmap.Config#HARDWARE}.</p>
*
- * On an animated image, the callback will only be called once, but the drawing
+ * <p>On an animated image, the callback will only be called once, but the drawing
* commands will be applied to each frame, as if the {@code Canvas} had been
- * returned by {@link Picture#beginRecording}.
+ * returned by {@link Picture#beginRecording}.<p>
*
- * Supplied to ImageDecoder via {@link ImageDecoder#setPostProcess}.
- * @hide
+ * <p>Supplied to ImageDecoder via {@link ImageDecoder#setPostProcessor}.</p>
*/
-public interface PostProcess {
+public interface PostProcessor {
/**
* Do any processing after (for example) decoding.
*
- * Drawing to the {@link Canvas} will behave as if the initial processing
+ * <p>Drawing to the {@link Canvas} will behave as if the initial processing
* (e.g. decoding) already exists in the Canvas. An implementation can draw
* effects on top of this, or it can even draw behind it using
* {@link PorterDuff.Mode#DST_OVER}. A common effect is to add transparency
* to the corners to achieve rounded corners. That can be done with the
- * following code:
+ * following code:</p>
*
* <code>
* Path path = new Path();
* path.setFillType(Path.FillType.INVERSE_EVEN_ODD);
+ * int width = canvas.getWidth();
+ * int height = canvas.getHeight();
* path.addRoundRect(0, 0, width, height, 20, 20, Path.Direction.CW);
* Paint paint = new Paint();
* paint.setAntiAlias(true);
@@ -63,10 +63,6 @@ public interface PostProcess {
*
*
* @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
@@ -87,5 +83,5 @@ public interface PostProcess {
* {@link java.lang.IllegalArgumentException}.
*/
@PixelFormat.Opacity
- public int postProcess(@NonNull Canvas canvas, int width, int height);
+ public int onPostProcess(@NonNull Canvas canvas);
}
diff --git a/android/graphics/Typeface.java b/android/graphics/Typeface.java
index 3d65bd22..ef415076 100644
--- a/android/graphics/Typeface.java
+++ b/android/graphics/Typeface.java
@@ -429,7 +429,7 @@ public class Typeface {
}
/**
- * Sets an index of the font collection.
+ * Sets an index of the font collection. See {@link android.R.attr#ttcIndex}.
*
* Can not be used for Typeface source. build() method will return null for invalid index.
* @param ttcIndex An index of the font collection. If the font source is not font
@@ -1025,6 +1025,10 @@ public class Typeface {
xmlFamily.getName(), fallback, languageTags, variant, cache, fontDir);
if (family != null) {
fallbackMap.valueAt(i).add(family);
+ } else if (defaultFamily != null) {
+ fallbackMap.valueAt(i).add(defaultFamily);
+ } else {
+ // There is no valid for for default fallback. Ignore.
}
}
}
diff --git a/android/graphics/drawable/AnimatedImageDrawable.java b/android/graphics/drawable/AnimatedImageDrawable.java
new file mode 100644
index 00000000..3034a105
--- /dev/null
+++ b/android/graphics/drawable/AnimatedImageDrawable.java
@@ -0,0 +1,191 @@
+/*
+ * 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.graphics.drawable;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.AssetFileDescriptor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.ImageDecoder;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.util.DisplayMetrics;
+
+import libcore.io.IoUtils;
+import libcore.util.NativeAllocationRegistry;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.Runnable;
+
+/**
+ * @hide
+ */
+public class AnimatedImageDrawable extends Drawable implements Animatable {
+ private final long mNativePtr;
+ private final InputStream mInputStream;
+ private final AssetFileDescriptor mAssetFd;
+
+ private final int mIntrinsicWidth;
+ private final int mIntrinsicHeight;
+
+ private Runnable mRunnable = new Runnable() {
+ @Override
+ public void run() {
+ invalidateSelf();
+ }
+ };
+
+ /**
+ * @hide
+ * This should only be called by ImageDecoder.
+ *
+ * decoder is only non-null if it has a PostProcess
+ */
+ public AnimatedImageDrawable(long nativeImageDecoder,
+ @Nullable ImageDecoder decoder, int width, int height,
+ int srcDensity, int dstDensity, Rect cropRect,
+ InputStream inputStream, AssetFileDescriptor afd)
+ throws IOException {
+ width = Bitmap.scaleFromDensity(width, srcDensity, dstDensity);
+ height = Bitmap.scaleFromDensity(height, srcDensity, dstDensity);
+
+ if (cropRect == null) {
+ mIntrinsicWidth = width;
+ mIntrinsicHeight = height;
+ } else {
+ cropRect.set(Bitmap.scaleFromDensity(cropRect.left, srcDensity, dstDensity),
+ Bitmap.scaleFromDensity(cropRect.top, srcDensity, dstDensity),
+ Bitmap.scaleFromDensity(cropRect.right, srcDensity, dstDensity),
+ Bitmap.scaleFromDensity(cropRect.bottom, srcDensity, dstDensity));
+ mIntrinsicWidth = cropRect.width();
+ mIntrinsicHeight = cropRect.height();
+ }
+
+ mNativePtr = nCreate(nativeImageDecoder, decoder, width, height, cropRect);
+ mInputStream = inputStream;
+ mAssetFd = afd;
+
+ // FIXME: Use the right size for the native allocation.
+ long nativeSize = 200;
+ NativeAllocationRegistry registry = new NativeAllocationRegistry(
+ AnimatedImageDrawable.class.getClassLoader(), nGetNativeFinalizer(), nativeSize);
+ registry.registerNativeAllocation(this, mNativePtr);
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ // FIXME: It's a shame that we have *both* a native finalizer and a Java
+ // one. The native one is necessary to report how much memory is being
+ // used natively, and this one is necessary to close the input. An
+ // alternative might be to read the entire stream ahead of time, so we
+ // can eliminate the Java finalizer.
+ try {
+ IoUtils.closeQuietly(mInputStream);
+ IoUtils.closeQuietly(mAssetFd);
+ } finally {
+ super.finalize();
+ }
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mIntrinsicWidth;
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mIntrinsicHeight;
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ long nextUpdate = nDraw(mNativePtr, canvas.getNativeCanvasWrapper());
+ // a value <= 0 indicates that the drawable is stopped or that renderThread
+ // will manage the animation
+ if (nextUpdate > 0) {
+ scheduleSelf(mRunnable, nextUpdate);
+ }
+ }
+
+ @Override
+ public void setAlpha(@IntRange(from=0,to=255) int alpha) {
+ if (alpha < 0 || alpha > 255) {
+ throw new IllegalArgumentException("Alpha must be between 0 and"
+ + " 255! provided " + alpha);
+ }
+ nSetAlpha(mNativePtr, alpha);
+ invalidateSelf();
+ }
+
+ @Override
+ public int getAlpha() {
+ return nGetAlpha(mNativePtr);
+ }
+
+ @Override
+ public void setColorFilter(@Nullable ColorFilter colorFilter) {
+ long nativeFilter = colorFilter == null ? 0 : colorFilter.getNativeInstance();
+ nSetColorFilter(mNativePtr, nativeFilter);
+ invalidateSelf();
+ }
+
+ @Override
+ public @PixelFormat.Opacity int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ // TODO: Add a Constant State?
+ // @Override
+ // public @Nullable ConstantState getConstantState() {}
+
+
+ // Animatable overrides
+ @Override
+ public boolean isRunning() {
+ return nIsRunning(mNativePtr);
+ }
+
+ @Override
+ public void start() {
+ if (nStart(mNativePtr)) {
+ invalidateSelf();
+ }
+ }
+
+ @Override
+ public void stop() {
+ nStop(mNativePtr);
+ }
+
+ private static native long nCreate(long nativeImageDecoder,
+ @Nullable ImageDecoder decoder, int width, int height, Rect cropRect)
+ throws IOException;
+ private static native long nGetNativeFinalizer();
+ private static native long nDraw(long nativePtr, long canvasNativePtr);
+ private static native void nSetAlpha(long nativePtr, int alpha);
+ private static native int nGetAlpha(long nativePtr);
+ private static native void nSetColorFilter(long nativePtr, long nativeFilter);
+ private static native boolean nIsRunning(long nativePtr);
+ private static native boolean nStart(long nativePtr);
+ private static native void nStop(long nativePtr);
+ private static native long nNativeByteSize(long nativePtr);
+}
diff --git a/android/graphics/drawable/BitmapDrawable.java b/android/graphics/drawable/BitmapDrawable.java
index e3740e3c..7ad062a6 100644
--- a/android/graphics/drawable/BitmapDrawable.java
+++ b/android/graphics/drawable/BitmapDrawable.java
@@ -163,7 +163,7 @@ public class BitmapDrawable extends Drawable {
/**
* Create a drawable by opening a given file path and decoding the bitmap.
*/
- @SuppressWarnings("unused")
+ @SuppressWarnings({ "unused", "ChainingConstructorIgnoresParameter" })
public BitmapDrawable(Resources res, String filepath) {
this(new BitmapState(BitmapFactory.decodeFile(filepath)), null);
mBitmapState.mTargetDensity = mTargetDensity;
@@ -188,7 +188,7 @@ public class BitmapDrawable extends Drawable {
/**
* Create a drawable by decoding a bitmap from the given input stream.
*/
- @SuppressWarnings("unused")
+ @SuppressWarnings({ "unused", "ChainingConstructorIgnoresParameter" })
public BitmapDrawable(Resources res, java.io.InputStream is) {
this(new BitmapState(BitmapFactory.decodeStream(is)), null);
mBitmapState.mTargetDensity = mTargetDensity;
diff --git a/android/graphics/drawable/Icon.java b/android/graphics/drawable/Icon.java
index c329918a..749b7594 100644
--- a/android/graphics/drawable/Icon.java
+++ b/android/graphics/drawable/Icon.java
@@ -819,8 +819,10 @@ public final class Icon implements Parcelable {
if (bitmapWidth > maxWidth || bitmapHeight > maxHeight) {
float scale = Math.min((float) maxWidth / bitmapWidth,
(float) maxHeight / bitmapHeight);
- bitmap = Bitmap.createScaledBitmap(bitmap, (int) (scale * bitmapWidth),
- (int) (scale * bitmapHeight), true /* filter */);
+ bitmap = Bitmap.createScaledBitmap(bitmap,
+ Math.max(1, (int) (scale * bitmapWidth)),
+ Math.max(1, (int) (scale * bitmapHeight)),
+ true /* filter */);
}
return bitmap;
}
diff --git a/android/graphics/drawable/RippleBackground.java b/android/graphics/drawable/RippleBackground.java
index dea194e4..41d36986 100644
--- a/android/graphics/drawable/RippleBackground.java
+++ b/android/graphics/drawable/RippleBackground.java
@@ -16,17 +16,12 @@
package android.graphics.drawable;
-import android.animation.Animator;
-import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.TimeInterpolator;
import android.graphics.Canvas;
-import android.graphics.CanvasProperty;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.FloatProperty;
-import android.view.DisplayListCanvas;
-import android.view.RenderNodeAnimator;
import android.view.animation.LinearInterpolator;
/**
@@ -68,30 +63,32 @@ class RippleBackground extends RippleComponent {
}
}
- public void setState(boolean focused, boolean hovered, boolean animateChanged) {
+ public void setState(boolean focused, boolean hovered, boolean pressed) {
+ if (!mFocused) {
+ focused = focused && !pressed;
+ }
+ if (!mHovered) {
+ hovered = hovered && !pressed;
+ }
if (mHovered != hovered || mFocused != focused) {
mHovered = hovered;
mFocused = focused;
- onStateChanged(animateChanged);
+ onStateChanged();
}
}
- private void onStateChanged(boolean animateChanged) {
+ private void onStateChanged() {
float newOpacity = 0.0f;
- if (mHovered) newOpacity += 1.0f;
- if (mFocused) newOpacity += 1.0f;
+ if (mHovered) newOpacity += .25f;
+ if (mFocused) newOpacity += .75f;
if (mAnimator != null) {
mAnimator.cancel();
mAnimator = null;
}
- if (animateChanged) {
- mAnimator = ObjectAnimator.ofFloat(this, OPACITY, newOpacity);
- mAnimator.setDuration(OPACITY_DURATION);
- mAnimator.setInterpolator(LINEAR_INTERPOLATOR);
- mAnimator.start();
- } else {
- mOpacity = newOpacity;
- }
+ mAnimator = ObjectAnimator.ofFloat(this, OPACITY, newOpacity);
+ mAnimator.setDuration(OPACITY_DURATION);
+ mAnimator.setInterpolator(LINEAR_INTERPOLATOR);
+ mAnimator.start();
}
public void jumpToFinal() {
diff --git a/android/graphics/drawable/RippleDrawable.java b/android/graphics/drawable/RippleDrawable.java
index 734cff54..0da61c29 100644
--- a/android/graphics/drawable/RippleDrawable.java
+++ b/android/graphics/drawable/RippleDrawable.java
@@ -264,8 +264,8 @@ public class RippleDrawable extends LayerDrawable {
}
setRippleActive(enabled && pressed);
+ setBackgroundActive(hovered, focused, pressed);
- setBackgroundActive(hovered, focused);
return changed;
}
@@ -280,13 +280,13 @@ public class RippleDrawable extends LayerDrawable {
}
}
- private void setBackgroundActive(boolean hovered, boolean focused) {
+ private void setBackgroundActive(boolean hovered, boolean focused, boolean pressed) {
if (mBackground == null && (hovered || focused)) {
mBackground = new RippleBackground(this, mHotspotBounds, isBounded());
mBackground.setup(mState.mMaxRadius, mDensity);
}
if (mBackground != null) {
- mBackground.setState(focused, hovered, true);
+ mBackground.setState(focused, hovered, pressed);
}
}
@@ -878,23 +878,22 @@ public class RippleDrawable extends LayerDrawable {
// Grab the color for the current state and cut the alpha channel in
// half so that the ripple and background together yield full alpha.
- final int color = mState.mColor.getColorForState(getState(), Color.BLACK);
- final int halfAlpha = (Color.alpha(color) / 2) << 24;
+ int color = mState.mColor.getColorForState(getState(), Color.BLACK);
+ if (Color.alpha(color) > 128) {
+ color = (color & 0x00FFFFFF) | 0x80000000;
+ }
final Paint p = mRipplePaint;
if (mMaskColorFilter != null) {
// The ripple timing depends on the paint's alpha value, so we need
// to push just the alpha channel into the paint and let the filter
// handle the full-alpha color.
- final int fullAlphaColor = color | (0xFF << 24);
- mMaskColorFilter.setColor(fullAlphaColor);
-
- p.setColor(halfAlpha);
+ mMaskColorFilter.setColor(color | 0xFF000000);
+ p.setColor(color & 0xFF000000);
p.setColorFilter(mMaskColorFilter);
p.setShader(mMaskShader);
} else {
- final int halfAlphaColor = (color & 0xFFFFFF) | halfAlpha;
- p.setColor(halfAlphaColor);
+ p.setColor(color);
p.setColorFilter(null);
p.setShader(null);
}
diff --git a/android/graphics/drawable/RippleForeground.java b/android/graphics/drawable/RippleForeground.java
index ecbf5780..41298680 100644
--- a/android/graphics/drawable/RippleForeground.java
+++ b/android/graphics/drawable/RippleForeground.java
@@ -289,6 +289,7 @@ class RippleForeground extends RippleComponent {
opacity.setInterpolator(LINEAR_INTERPOLATOR);
opacity.addListener(mAnimationListener);
opacity.setStartDelay(computeFadeOutDelay());
+ opacity.setStartValue(mOwner.getRipplePaint().getAlpha());
mPendingHwAnimators.add(opacity);
invalidateSelf();
}
diff --git a/android/hardware/HardwareBuffer.java b/android/hardware/HardwareBuffer.java
index 7866b52c..9aa3f40a 100644
--- a/android/hardware/HardwareBuffer.java
+++ b/android/hardware/HardwareBuffer.java
@@ -25,11 +25,11 @@ import android.os.Parcelable;
import dalvik.annotation.optimization.FastNative;
import dalvik.system.CloseGuard;
+import libcore.util.NativeAllocationRegistry;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
-import libcore.util.NativeAllocationRegistry;
-
/**
* HardwareBuffer wraps a native <code>AHardwareBuffer</code> object, which is a low-level object
* representing a memory buffer accessible by various hardware units. HardwareBuffer allows sharing
@@ -42,18 +42,25 @@ import libcore.util.NativeAllocationRegistry;
public final class HardwareBuffer implements Parcelable, AutoCloseable {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef(prefix = { "RGB", "BLOB" }, value = {
+ @IntDef(prefix = { "RGB", "BLOB", "D_", "DS_", "S_" }, value = {
RGBA_8888,
RGBA_FP16,
RGBA_1010102,
RGBX_8888,
RGB_888,
RGB_565,
- BLOB
+ BLOB,
+ D_16,
+ D_24,
+ DS_24UI8,
+ D_FP32,
+ DS_FP32UI8,
+ S_UI8,
})
public @interface Format {
}
+ @Format
/** Format: 8 bits each red, green, blue, alpha */
public static final int RGBA_8888 = 1;
/** Format: 8 bits each red, green, blue, alpha, alpha is always 0xFF */
@@ -68,6 +75,18 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable {
public static final int RGBA_1010102 = 0x2b;
/** Format: opaque format used for raw data transfer; must have a height of 1 */
public static final int BLOB = 0x21;
+ /** Format: 16 bits depth */
+ public static final int D_16 = 0x30;
+ /** Format: 24 bits depth */
+ public static final int D_24 = 0x31;
+ /** Format: 24 bits depth, 8 bits stencil */
+ public static final int DS_24UI8 = 0x32;
+ /** Format: 32 bits depth */
+ public static final int D_FP32 = 0x33;
+ /** Format: 32 bits depth, 8 bits stencil */
+ public static final int DS_FP32UI8 = 0x34;
+ /** Format: 8 bits stencil */
+ public static final int S_UI8 = 0x35;
// Note: do not rename, this field is used by native code
private long mNativeObject;
@@ -82,9 +101,11 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable {
@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})
+ USAGE_GPU_DATA_BUFFER, USAGE_SENSOR_DIRECT_DATA, USAGE_GPU_CUBE_MAP,
+ USAGE_GPU_MIPMAP_COMPLETE})
public @interface Usage {};
+ @Usage
/** Usage: The buffer will sometimes be read by the CPU */
public static final long USAGE_CPU_READ_RARELY = 2;
/** Usage: The buffer will often be read by the CPU */
@@ -107,6 +128,10 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable {
public static final long USAGE_SENSOR_DIRECT_DATA = 1 << 23;
/** Usage: The buffer will be used as a shader storage or uniform buffer object */
public static final long USAGE_GPU_DATA_BUFFER = 1 << 24;
+ /** Usage: The buffer will be used as a cube map texture */
+ public static final long USAGE_GPU_CUBE_MAP = 1 << 25;
+ /** Usage: The buffer contains a complete mipmap hierarchy */
+ public static final long USAGE_GPU_MIPMAP_COMPLETE = 1 << 26;
// The approximate size of a native AHardwareBuffer object.
private static final long NATIVE_HARDWARE_BUFFER_SIZE = 232;
@@ -118,15 +143,9 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable {
*
* @param width The width in pixels of the buffer
* @param height The height in pixels of the buffer
- * @param format The format of each pixel, one of {@link #RGBA_8888}, {@link #RGBA_FP16},
- * {@link #RGBX_8888}, {@link #RGB_565}, {@link #RGB_888}, {@link #RGBA_1010102}, {@link #BLOB}
+ * @param format The @Format of each pixel
* @param layers The number of layers in the buffer
- * @param usage Flags describing how the buffer will be used, one of
- * {@link #USAGE_CPU_READ_RARELY}, {@link #USAGE_CPU_READ_OFTEN},
- * {@link #USAGE_CPU_WRITE_RARELY}, {@link #USAGE_CPU_WRITE_OFTEN},
- * {@link #USAGE_GPU_SAMPLED_IMAGE}, {@link #USAGE_GPU_COLOR_OUTPUT},
- * {@link #USAGE_GPU_DATA_BUFFER}, {@link #USAGE_PROTECTED_CONTENT},
- * {@link #USAGE_SENSOR_DIRECT_DATA}, {@link #USAGE_VIDEO_ENCODE}
+ * @param usage The @Usage flags describing how the buffer will be used
* @return A <code>HardwareBuffer</code> instance if successful, or throws an
* IllegalArgumentException if the dimensions passed are invalid (either zero, negative, or
* too large to allocate), if the format is not supported, if the requested number of layers
@@ -154,7 +173,7 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable {
if (nativeObject == 0) {
throw new IllegalArgumentException("Unable to create a HardwareBuffer, either the " +
"dimensions passed were too large, too many image layers were requested, " +
- "or an invalid set of usage flags was passed");
+ "or an invalid set of usage flags or invalid format was passed");
}
return new HardwareBuffer(nativeObject);
}
@@ -206,8 +225,7 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable {
}
/**
- * Returns the format of this buffer, one of {@link #RGBA_8888}, {@link #RGBA_FP16},
- * {@link #RGBX_8888}, {@link #RGB_565}, {@link #RGB_888}, {@link #RGBA_1010102}, {@link #BLOB}.
+ * Returns the @Format of this buffer.
*/
@Format
public int getFormat() {
@@ -338,6 +356,12 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable {
case RGB_565:
case RGB_888:
case BLOB:
+ case D_16:
+ case D_24:
+ case DS_24UI8:
+ case D_FP32:
+ case DS_FP32UI8:
+ case S_UI8:
return true;
}
return false;
diff --git a/android/hardware/camera2/CameraCharacteristics.java b/android/hardware/camera2/CameraCharacteristics.java
index 57ab18e2..96d043c2 100644
--- a/android/hardware/camera2/CameraCharacteristics.java
+++ b/android/hardware/camera2/CameraCharacteristics.java
@@ -22,9 +22,11 @@ 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.ArrayUtils;
import android.hardware.camera2.utils.TypeReference;
import android.util.Rational;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@@ -171,6 +173,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
private List<CameraCharacteristics.Key<?>> mKeys;
private List<CaptureRequest.Key<?>> mAvailableRequestKeys;
private List<CaptureRequest.Key<?>> mAvailableSessionKeys;
+ private List<CaptureRequest.Key<?>> mAvailablePhysicalRequestKeys;
private List<CaptureResult.Key<?>> mAvailableResultKeys;
/**
@@ -314,6 +317,45 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
}
/**
+ * <p>Returns a subset of {@link #getAvailableCaptureRequestKeys} keys that can
+ * be overriden for physical devices backing a logical multi-camera.</p>
+ *
+ * <p>This is a subset of android.request.availableRequestKeys which contains a list
+ * of keys that can be overriden using {@link CaptureRequest.Builder#setPhysicalCameraKey }.
+ * The respective value of such request key can be obtained by calling
+ * {@link CaptureRequest.Builder#getPhysicalCameraKey }. Capture requests that contain
+ * individual physical device requests must be built via
+ * {@link android.hardware.camera2.CameraDevice#createCaptureRequest(int, Set)}.
+ * Such extended capture requests can be passed only to
+ * {@link CameraCaptureSession#capture } or {@link CameraCaptureSession#captureBurst } and
+ * not to {@link CameraCaptureSession#setRepeatingRequest } or
+ * {@link CameraCaptureSession#setRepeatingBurst }.</p>
+ *
+ * <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 overriden in individual physical device requests.
+ * In case the camera device doesn't support such keys the list can be null.
+ */
+ @SuppressWarnings({"unchecked"})
+ public List<CaptureRequest.Key<?>> getAvailablePhysicalCameraRequestKeys() {
+ if (mAvailableSessionKeys == null) {
+ Object crKey = CaptureRequest.Key.class;
+ Class<CaptureRequest.Key<?>> crKeyTyped = (Class<CaptureRequest.Key<?>>)crKey;
+
+ int[] filterTags = get(REQUEST_AVAILABLE_PHYSICAL_CAMERA_REQUEST_KEYS);
+ if (filterTags == null) {
+ return null;
+ }
+ mAvailablePhysicalRequestKeys =
+ getAvailableKeyList(CaptureRequest.class, crKeyTyped, filterTags);
+ }
+ return mAvailablePhysicalRequestKeys;
+ }
+
+ /**
* Returns the list of keys supported by this {@link CameraDevice} for querying
* with a {@link CaptureRequest}.
*
@@ -407,6 +449,47 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
return Collections.unmodifiableList(staticKeyList);
}
+ /**
+ * Returns the list of physical camera ids that this logical {@link CameraDevice} is
+ * made up of.
+ *
+ * <p>A camera device is a logical camera if it has
+ * REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA capability. If the camera device
+ * doesn't have the capability, the return value will be an empty list. </p>
+ *
+ * <p>The list returned is not modifiable, so any attempts to modify it will throw
+ * a {@code UnsupportedOperationException}.</p>
+ *
+ * <p>Each physical camera id is only listed once in the list. The order of the keys
+ * is undefined.</p>
+ *
+ * @return List of physical camera ids for this logical camera device.
+ */
+ @NonNull
+ public List<String> getPhysicalCameraIds() {
+ int[] availableCapabilities = get(REQUEST_AVAILABLE_CAPABILITIES);
+ if (availableCapabilities == null) {
+ throw new AssertionError("android.request.availableCapabilities must be non-null "
+ + "in the characteristics");
+ }
+
+ if (!ArrayUtils.contains(availableCapabilities,
+ REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)) {
+ return Collections.emptyList();
+ }
+ byte[] physicalCamIds = get(LOGICAL_MULTI_CAMERA_PHYSICAL_IDS);
+
+ String physicalCamIdString = null;
+ try {
+ physicalCamIdString = new String(physicalCamIds, "UTF-8");
+ } catch (java.io.UnsupportedEncodingException e) {
+ throw new AssertionError("android.logicalCam.physicalIds must be UTF-8 string");
+ }
+ String[] physicalCameraIdList = physicalCamIdString.split("\0");
+
+ return Collections.unmodifiableList(Arrays.asList(physicalCameraIdList));
+ }
+
/*@O~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~
* The key entries below this point are generated from metadata
* definitions in /system/media/camera/docs. Do not modify by hand or
@@ -1149,36 +1232,33 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
/**
* <p>Position of the camera optical center.</p>
* <p>The position of the camera device's lens optical center,
- * as a three-dimensional vector <code>(x,y,z)</code>, relative to the
- * optical center of the largest camera device facing in the
- * same direction as this camera, in the {@link android.hardware.SensorEvent Android sensor coordinate
- * axes}. Note that only the axis definitions are shared with
- * the sensor coordinate system, but not the origin.</p>
- * <p>If this device is the largest or only camera device with a
- * given facing, then this position will be <code>(0, 0, 0)</code>; a
- * camera device with a lens optical center located 3 cm from
- * the main sensor along the +X axis (to the right from the
- * user's perspective) will report <code>(0.03, 0, 0)</code>.</p>
- * <p>To transform a pixel coordinates between two cameras
- * facing the same direction, first the source camera
- * {@link CameraCharacteristics#LENS_RADIAL_DISTORTION android.lens.radialDistortion} must be corrected for. Then
- * the source camera {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration} needs
- * to be applied, followed by the {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation}
- * of the source camera, the translation of the source camera
- * relative to the destination camera, the
- * {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation} of the destination camera, and
- * finally the inverse of {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration}
- * of the destination camera. This obtains a
- * radial-distortion-free coordinate in the destination
- * camera pixel coordinates.</p>
- * <p>To compare this against a real image from the destination
- * camera, the destination camera image then needs to be
- * corrected for radial distortion before comparison or
- * sampling.</p>
+ * as a three-dimensional vector <code>(x,y,z)</code>.</p>
+ * <p>Prior to Android P, or when {@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference} is PRIMARY_CAMERA, this position
+ * is relative to the optical center of the largest camera device facing in the same
+ * direction as this camera, in the {@link android.hardware.SensorEvent Android sensor
+ * coordinate axes}. Note that only the axis definitions are shared with the sensor
+ * coordinate system, but not the origin.</p>
+ * <p>If this device is the largest or only camera device with a given facing, then this
+ * position will be <code>(0, 0, 0)</code>; a camera device with a lens optical center located 3 cm
+ * from the main sensor along the +X axis (to the right from the user's perspective) will
+ * report <code>(0.03, 0, 0)</code>.</p>
+ * <p>To transform a pixel coordinates between two cameras facing the same direction, first
+ * the source camera {@link CameraCharacteristics#LENS_RADIAL_DISTORTION android.lens.radialDistortion} must be corrected for. Then the source
+ * camera {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration} needs to be applied, followed by the
+ * {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation} of the source camera, the translation of the source camera
+ * relative to the destination camera, the {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation} of the destination
+ * camera, and finally the inverse of {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration} of the destination
+ * camera. This obtains a radial-distortion-free coordinate in the destination camera pixel
+ * coordinates.</p>
+ * <p>To compare this against a real image from the destination camera, the destination camera
+ * image then needs to be corrected for radial distortion before comparison or sampling.</p>
+ * <p>When {@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference} is GYROSCOPE, then this position is relative to
+ * the center of the primary gyroscope on the device.</p>
* <p><b>Units</b>: Meters</p>
* <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
*
* @see CameraCharacteristics#LENS_INTRINSIC_CALIBRATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
* @see CameraCharacteristics#LENS_POSE_ROTATION
* @see CameraCharacteristics#LENS_RADIAL_DISTORTION
*/
@@ -1289,6 +1369,28 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
new Key<float[]>("android.lens.radialDistortion", float[].class);
/**
+ * <p>The origin for {@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation}.</p>
+ * <p>Different calibration methods and use cases can produce better or worse results
+ * depending on the selected coordinate origin.</p>
+ * <p>For devices designed to support the MOTION_TRACKING capability, the GYROSCOPE origin
+ * makes device calibration and later usage by applications combining camera and gyroscope
+ * information together simpler.</p>
+ * <p><b>Possible values:</b>
+ * <ul>
+ * <li>{@link #LENS_POSE_REFERENCE_PRIMARY_CAMERA PRIMARY_CAMERA}</li>
+ * <li>{@link #LENS_POSE_REFERENCE_GYROSCOPE GYROSCOPE}</li>
+ * </ul></p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CameraCharacteristics#LENS_POSE_TRANSLATION
+ * @see #LENS_POSE_REFERENCE_PRIMARY_CAMERA
+ * @see #LENS_POSE_REFERENCE_GYROSCOPE
+ */
+ @PublicKey
+ public static final Key<Integer> LENS_POSE_REFERENCE =
+ new Key<Integer>("android.lens.poseReference", int.class);
+
+ /**
* <p>List of noise reduction modes for {@link CaptureRequest#NOISE_REDUCTION_MODE android.noiseReduction.mode} that are supported
* by this camera device.</p>
* <p>Full-capability camera devices will always support OFF and FAST.</p>
@@ -1559,6 +1661,8 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
* <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING YUV_REPROCESSING}</li>
* <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT DEPTH_OUTPUT}</li>
* <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO CONSTRAINED_HIGH_SPEED_VIDEO}</li>
+ * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING MOTION_TRACKING}</li>
+ * <li>{@link #REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA LOGICAL_MULTI_CAMERA}</li>
* </ul></p>
* <p>This key is available on all devices.</p>
*
@@ -1573,6 +1677,8 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
* @see #REQUEST_AVAILABLE_CAPABILITIES_YUV_REPROCESSING
* @see #REQUEST_AVAILABLE_CAPABILITIES_DEPTH_OUTPUT
* @see #REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO
+ * @see #REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING
+ * @see #REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA
*/
@PublicKey
public static final Key<int[]> REQUEST_AVAILABLE_CAPABILITIES =
@@ -1642,8 +1748,9 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
* 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
+ * 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
@@ -1653,15 +1760,18 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
* <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>
+ * 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>
+ * 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>
+ * 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
@@ -1676,6 +1786,30 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
new Key<int[]>("android.request.availableSessionKeys", int[].class);
/**
+ * <p>A subset of the available request keys that can be overriden for
+ * physical devices backing a logical multi-camera.</p>
+ * <p>This is a subset of android.request.availableRequestKeys which contains a list
+ * of keys that can be overriden using {@link CaptureRequest.Builder#setPhysicalCameraKey }.
+ * The respective value of such request key can be obtained by calling
+ * {@link CaptureRequest.Builder#getPhysicalCameraKey }. Capture requests that contain
+ * individual physical device requests must be built via
+ * {@link android.hardware.camera2.CameraDevice#createCaptureRequest(int, Set)}.
+ * Such extended capture requests can be passed only to
+ * {@link CameraCaptureSession#capture } or {@link CameraCaptureSession#captureBurst } and
+ * not to {@link CameraCaptureSession#setRepeatingRequest } or
+ * {@link CameraCaptureSession#setRepeatingBurst }.</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ * <p><b>Limited capability</b> -
+ * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+ * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+ *
+ * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+ * @hide
+ */
+ public static final Key<int[]> REQUEST_AVAILABLE_PHYSICAL_CAMERA_REQUEST_KEYS =
+ new Key<int[]>("android.request.availablePhysicalCameraRequestKeys", 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>
@@ -2845,6 +2979,21 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
new Key<int[]>("android.statistics.info.availableLensShadingMapModes", int[].class);
/**
+ * <p>List of OIS data output modes for {@link CaptureRequest#STATISTICS_OIS_DATA_MODE android.statistics.oisDataMode} that
+ * are supported by this camera device.</p>
+ * <p>If no OIS data output is available for this camera device, this key will
+ * contain only OFF.</p>
+ * <p><b>Range of valid values:</b><br>
+ * Any value listed in {@link CaptureRequest#STATISTICS_OIS_DATA_MODE android.statistics.oisDataMode}</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CaptureRequest#STATISTICS_OIS_DATA_MODE
+ */
+ @PublicKey
+ public static final Key<int[]> STATISTICS_INFO_AVAILABLE_OIS_DATA_MODES =
+ new Key<int[]>("android.statistics.info.availableOisDataModes", int[].class);
+
+ /**
* <p>Maximum number of supported points in the
* tonemap curve that can be used for {@link CaptureRequest#TONEMAP_CURVE android.tonemap.curve}.</p>
* <p>If the actual number of points provided by the application (in {@link CaptureRequest#TONEMAP_CURVE android.tonemap.curve}*) is
@@ -2953,6 +3102,7 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
* <li>{@link #INFO_SUPPORTED_HARDWARE_LEVEL_FULL FULL}</li>
* <li>{@link #INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY LEGACY}</li>
* <li>{@link #INFO_SUPPORTED_HARDWARE_LEVEL_3 3}</li>
+ * <li>{@link #INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL EXTERNAL}</li>
* </ul></p>
* <p>This key is available on all devices.</p>
*
@@ -2966,12 +3116,25 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
* @see #INFO_SUPPORTED_HARDWARE_LEVEL_FULL
* @see #INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY
* @see #INFO_SUPPORTED_HARDWARE_LEVEL_3
+ * @see #INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL
*/
@PublicKey
public static final Key<Integer> INFO_SUPPORTED_HARDWARE_LEVEL =
new Key<Integer>("android.info.supportedHardwareLevel", int.class);
/**
+ * <p>A short string for manufacturer version information about the camera device, such as
+ * ISP hardware, sensors, etc.</p>
+ * <p>This can be used in {@link android.media.ExifInterface#TAG_IMAGE_DESCRIPTION TAG_IMAGE_DESCRIPTION}
+ * in jpeg EXIF. This key may be absent if no version information is available on the
+ * device.</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ */
+ @PublicKey
+ public static final Key<String> INFO_VERSION =
+ new Key<String>("android.info.version", String.class);
+
+ /**
* <p>The maximum number of frames that can occur after a request
* (different than the previous) has been submitted, and before the
* result's state becomes synchronized.</p>
@@ -3130,6 +3293,54 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri
public static final Key<Boolean> DEPTH_DEPTH_IS_EXCLUSIVE =
new Key<Boolean>("android.depth.depthIsExclusive", boolean.class);
+ /**
+ * <p>String containing the ids of the underlying physical cameras.</p>
+ * <p>For a logical camera, this is concatenation of all underlying physical camera ids.
+ * The null terminator for physical camera id must be preserved so that the whole string
+ * can be tokenized using '\0' to generate list of physical camera ids.</p>
+ * <p>For example, if the physical camera ids of the logical camera are "2" and "3", the
+ * value of this tag will be ['2', '\0', '3', '\0'].</p>
+ * <p>The number of physical camera ids must be no less than 2.</p>
+ * <p><b>Units</b>: UTF-8 null-terminated string</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ * <p><b>Limited capability</b> -
+ * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+ * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+ *
+ * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+ * @hide
+ */
+ public static final Key<byte[]> LOGICAL_MULTI_CAMERA_PHYSICAL_IDS =
+ new Key<byte[]>("android.logicalMultiCamera.physicalIds", byte[].class);
+
+ /**
+ * <p>The accuracy of frame timestamp synchronization between physical cameras</p>
+ * <p>The accuracy of the frame timestamp synchronization determines the physical cameras'
+ * ability to start exposure at the same time. If the sensorSyncType is CALIBRATED,
+ * the physical camera sensors usually run in master-slave mode so that their shutter
+ * time is synchronized. For APPROXIMATE sensorSyncType, the camera sensors usually run in
+ * master-master mode, and there could be offset between their start of exposure.</p>
+ * <p>In both cases, all images generated for a particular capture request still carry the same
+ * timestamps, so that they can be used to look up the matching frame number and
+ * onCaptureStarted callback.</p>
+ * <p><b>Possible values:</b>
+ * <ul>
+ * <li>{@link #LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE_APPROXIMATE APPROXIMATE}</li>
+ * <li>{@link #LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE_CALIBRATED CALIBRATED}</li>
+ * </ul></p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ * <p><b>Limited capability</b> -
+ * Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
+ * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
+ *
+ * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+ * @see #LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE_APPROXIMATE
+ * @see #LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE_CALIBRATED
+ */
+ @PublicKey
+ public static final Key<Integer> LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE =
+ new Key<Integer>("android.logicalMultiCamera.sensorSyncType", int.class);
+
/*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~
* End generated code
*~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~@~O@*/
diff --git a/android/hardware/camera2/CameraDevice.java b/android/hardware/camera2/CameraDevice.java
index 87e503de..40ee8348 100644
--- a/android/hardware/camera2/CameraDevice.java
+++ b/android/hardware/camera2/CameraDevice.java
@@ -31,6 +31,7 @@ import android.os.Handler;
import android.view.Surface;
import java.util.List;
+import java.util.Set;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -144,6 +145,37 @@ public abstract class CameraDevice implements AutoCloseable {
*/
public static final int TEMPLATE_MANUAL = 6;
+ /**
+ * A template for selecting camera parameters that match TEMPLATE_PREVIEW as closely as
+ * possible while improving the camera output for motion tracking use cases.
+ *
+ * <p>This template is best used by applications that are frequently switching between motion
+ * tracking use cases and regular still capture use cases, to minimize the IQ changes
+ * when swapping use cases.</p>
+ *
+ * <p>This template is guaranteed to be supported on camera devices that support the
+ * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING MOTION_TRACKING}
+ * capability.</p>
+ *
+ * @see #createCaptureRequest
+ */
+ public static final int TEMPLATE_MOTION_TRACKING_PREVIEW = 7;
+
+ /**
+ * A template for selecting camera parameters that maximize the quality of camera output for
+ * motion tracking use cases.
+ *
+ * <p>This template is best used by applications dedicated to motion tracking applications,
+ * which aren't concerned about fast switches between motion tracking and other use cases.</p>
+ *
+ * <p>This template is guaranteed to be supported on camera devices that support the
+ * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING MOTION_TRACKING}
+ * capability.</p>
+ *
+ * @see #createCaptureRequest
+ */
+ public static final int TEMPLATE_MOTION_TRACKING_BEST = 8;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"TEMPLATE_"}, value =
@@ -152,7 +184,9 @@ public abstract class CameraDevice implements AutoCloseable {
TEMPLATE_RECORD,
TEMPLATE_VIDEO_SNAPSHOT,
TEMPLATE_ZERO_SHUTTER_LAG,
- TEMPLATE_MANUAL })
+ TEMPLATE_MANUAL,
+ TEMPLATE_MOTION_TRACKING_PREVIEW,
+ TEMPLATE_MOTION_TRACKING_BEST})
public @interface RequestTemplate {};
/**
@@ -386,6 +420,27 @@ public abstract class CameraDevice implements AutoCloseable {
* </table><br>
* </p>
*
+ * <p>MOTION_TRACKING-capability ({@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES}
+ * includes
+ * {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING MOTION_TRACKING})
+ * devices support at least the below stream combinations in addition to those for
+ * {@link CameraMetadata#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED LIMITED} devices. The
+ * {@code FULL FOV 640} entry means that the device will support a resolution that's 640 pixels
+ * wide, with the height set so that the resolution aspect ratio matches the MAXIMUM output
+ * aspect ratio, rounded down. So for a device with a 4:3 image sensor, this will be 640x480,
+ * and for a device with a 16:9 sensor, this will be 640x360, and so on. And the
+ * {@code MAX 30FPS} entry means the largest JPEG resolution on the device for which
+ * {@link android.hardware.camera2.params.StreamConfigurationMap#getOutputMinFrameDuration}
+ * returns a value less than or equal to 1/30s.
+ *
+ * <table>
+ * <tr><th colspan="7">MOTION_TRACKING-capability additional guaranteed configurations</th></tr>
+ * <tr><th colspan="2" id="rb">Target 1</th><th colspan="2" id="rb">Target 2</th><th colspan="2" id="rb">Target 3</th><th rowspan="2">Sample use case(s)</th> </tr>
+ * <tr><th>Type</th><th id="rb">Max size</th><th>Type</th><th id="rb">Max size</th><th>Type</th><th id="rb">Max size</th></tr>
+ * <tr> <td>{@code YUV}</td><td id="rb">{@code PREVIEW}</td> <td>{@code YUV }</td><td id="rb">{@code FULL FOV 640}</td> <td>{@code JPEG}</td><td id="rb">{@code MAX 30FPS}</td> <td>Preview with a tracking YUV output and a as-large-as-possible JPEG for still captures.</td> </tr>
+ * </table><br>
+ * </p>
+ *
* <p>BURST-capability ({@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES} includes
* {@link CameraMetadata#REQUEST_AVAILABLE_CAPABILITIES_BURST_CAPTURE BURST_CAPTURE}) devices
* support at least the below stream combinations in addition to those for
@@ -850,16 +905,51 @@ public abstract class CameraDevice implements AutoCloseable {
* @throws CameraAccessException if the camera device is no longer connected or has
* encountered a fatal error
* @throws IllegalStateException if the camera device has been closed
+ */
+ @NonNull
+ public abstract CaptureRequest.Builder createCaptureRequest(@RequestTemplate int templateType)
+ throws CameraAccessException;
+
+ /**
+ * <p>Create a {@link CaptureRequest.Builder} for new capture requests,
+ * initialized with template for a target use case. This methods allows
+ * clients to pass physical camera ids which can be used to customize the
+ * request for a specific physical camera. The settings are chosen
+ * to be the best options for the specific logical camera device. If
+ * additional physical camera ids are passed, then they will also use the
+ * same settings template. Requests containing individual physical camera
+ * settings can be passed only to {@link CameraCaptureSession#capture} or
+ * {@link CameraCaptureSession#captureBurst} and not to
+ * {@link CameraCaptureSession#setRepeatingRequest} or
+ * {@link CameraCaptureSession#setRepeatingBurst}</p>
+ *
+ * @param templateType An enumeration selecting the use case for this request. Not all template
+ * types are supported on every device. See the documentation for each template type for
+ * details.
+ * @param physicalCameraIdSet A set of physical camera ids that can be used to customize
+ * the request for a specific physical camera.
+ * @return a builder for a capture request, initialized with default
+ * settings for that template, and no output streams
+ *
+ * @throws IllegalArgumentException if the templateType is not supported by
+ * this device, or one of the physical id arguments matches with logical camera id.
+ * @throws CameraAccessException if the camera device is no longer connected or has
+ * encountered a fatal error
+ * @throws IllegalStateException if the camera device has been closed
*
* @see #TEMPLATE_PREVIEW
* @see #TEMPLATE_RECORD
* @see #TEMPLATE_STILL_CAPTURE
* @see #TEMPLATE_VIDEO_SNAPSHOT
* @see #TEMPLATE_MANUAL
+ * @see CaptureRequest.Builder#setKey
+ * @see CaptureRequest.Builder#getKey
*/
@NonNull
- public abstract CaptureRequest.Builder createCaptureRequest(@RequestTemplate int templateType)
- throws CameraAccessException;
+ public CaptureRequest.Builder createCaptureRequest(@RequestTemplate int templateType,
+ Set<String> physicalCameraIdSet) throws CameraAccessException {
+ throw new UnsupportedOperationException("Subclasses must override this method");
+ }
/**
* <p>Create a {@link CaptureRequest.Builder} for a new reprocess {@link CaptureRequest} from a
diff --git a/android/hardware/camera2/CameraManager.java b/android/hardware/camera2/CameraManager.java
index 90bf896c..a2bc91e0 100644
--- a/android/hardware/camera2/CameraManager.java
+++ b/android/hardware/camera2/CameraManager.java
@@ -996,7 +996,12 @@ public final class CameraManager {
return;
}
- Integer oldStatus = mDeviceStatus.put(id, status);
+ Integer oldStatus;
+ if (status == ICameraServiceListener.STATUS_NOT_PRESENT) {
+ oldStatus = mDeviceStatus.remove(id);
+ } else {
+ oldStatus = mDeviceStatus.put(id, status);
+ }
if (oldStatus != null && oldStatus == status) {
if (DEBUG) {
diff --git a/android/hardware/camera2/CameraMetadata.java b/android/hardware/camera2/CameraMetadata.java
index cb11d0f5..e7c89611 100644
--- a/android/hardware/camera2/CameraMetadata.java
+++ b/android/hardware/camera2/CameraMetadata.java
@@ -336,6 +336,30 @@ public abstract class CameraMetadata<TKey> {
public static final int LENS_FACING_EXTERNAL = 2;
//
+ // Enumeration values for CameraCharacteristics#LENS_POSE_REFERENCE
+ //
+
+ /**
+ * <p>The value of {@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation} is relative to the optical center of
+ * the largest camera device facing the same direction as this camera.</p>
+ * <p>This default value for API levels before Android P.</p>
+ *
+ * @see CameraCharacteristics#LENS_POSE_TRANSLATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
+ */
+ public static final int LENS_POSE_REFERENCE_PRIMARY_CAMERA = 0;
+
+ /**
+ * <p>The value of {@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation} is relative to the position of the
+ * primary gyroscope of this Android device.</p>
+ * <p>This is the value reported by all devices that support the MOTION_TRACKING capability.</p>
+ *
+ * @see CameraCharacteristics#LENS_POSE_TRANSLATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
+ */
+ public static final int LENS_POSE_REFERENCE_GYROSCOPE = 1;
+
+ //
// Enumeration values for CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES
//
@@ -665,6 +689,7 @@ public abstract class CameraMetadata<TKey> {
* </ul>
* </li>
* <li>The {@link CameraCharacteristics#DEPTH_DEPTH_IS_EXCLUSIVE android.depth.depthIsExclusive} entry is listed by this device.</li>
+ * <li>As of Android P, the {@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference} entry is listed by this device.</li>
* <li>A LIMITED camera with only the DEPTH_OUTPUT capability does not have to support
* normal YUV_420_888, JPEG, and PRIV-format outputs. It only has to support the DEPTH16
* format.</li>
@@ -680,6 +705,7 @@ public abstract class CameraMetadata<TKey> {
* @see CameraCharacteristics#DEPTH_DEPTH_IS_EXCLUSIVE
* @see CameraCharacteristics#LENS_FACING
* @see CameraCharacteristics#LENS_INTRINSIC_CALIBRATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
* @see CameraCharacteristics#LENS_POSE_ROTATION
* @see CameraCharacteristics#LENS_POSE_TRANSLATION
* @see CameraCharacteristics#LENS_RADIAL_DISTORTION
@@ -774,6 +800,98 @@ public abstract class CameraMetadata<TKey> {
*/
public static final int REQUEST_AVAILABLE_CAPABILITIES_CONSTRAINED_HIGH_SPEED_VIDEO = 9;
+ /**
+ * <p>The device supports controls and metadata required for accurate motion tracking for
+ * use cases such as augmented reality, electronic image stabilization, and so on.</p>
+ * <p>This means this camera device has accurate optical calibration and timestamps relative
+ * to the inertial sensors.</p>
+ * <p>This capability requires the camera device to support the following:</p>
+ * <ul>
+ * <li>Capture request templates {@link android.hardware.camera2.CameraDevice#TEMPLATE_MOTION_TRACKING_PREVIEW } and {@link android.hardware.camera2.CameraDevice#TEMPLATE_MOTION_TRACKING_BEST } are defined.</li>
+ * <li>The stream configurations listed in {@link android.hardware.camera2.CameraDevice#createCaptureSession } for MOTION_TRACKING are
+ * supported, either at 30 or 60fps maximum frame rate.</li>
+ * <li>The following camera characteristics and capture result metadata are provided:<ul>
+ * <li>{@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration}</li>
+ * <li>{@link CameraCharacteristics#LENS_RADIAL_DISTORTION android.lens.radialDistortion}</li>
+ * <li>{@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation}</li>
+ * <li>{@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation}</li>
+ * <li>{@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference} with value GYROSCOPE</li>
+ * </ul>
+ * </li>
+ * <li>The {@link CameraCharacteristics#SENSOR_INFO_TIMESTAMP_SOURCE android.sensor.info.timestampSource} field has value <code>REALTIME</code>. When compared to
+ * timestamps from the device's gyroscopes, the clock difference for events occuring at
+ * the same actual time instant will be less than 1 ms.</li>
+ * <li>The value of the {@link CaptureResult#SENSOR_ROLLING_SHUTTER_SKEW android.sensor.rollingShutterSkew} field is accurate to within 1 ms.</li>
+ * <li>The value of {@link CaptureRequest#SENSOR_EXPOSURE_TIME android.sensor.exposureTime} is guaranteed to be available in the
+ * capture result.</li>
+ * <li>The {@link CaptureRequest#CONTROL_CAPTURE_INTENT android.control.captureIntent} control supports MOTION_TRACKING to limit maximum
+ * exposure to 20 milliseconds.</li>
+ * <li>The stream configurations required for MOTION_TRACKING (listed at {@link android.hardware.camera2.CameraDevice#createCaptureSession }) can operate at least at
+ * 30fps; optionally, they can operate at 60fps, and '[60, 60]' is listed in
+ * {@link CameraCharacteristics#CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES android.control.aeAvailableTargetFpsRanges}.</li>
+ * </ul>
+ *
+ * @see CameraCharacteristics#CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES
+ * @see CaptureRequest#CONTROL_CAPTURE_INTENT
+ * @see CameraCharacteristics#LENS_INTRINSIC_CALIBRATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
+ * @see CameraCharacteristics#LENS_POSE_ROTATION
+ * @see CameraCharacteristics#LENS_POSE_TRANSLATION
+ * @see CameraCharacteristics#LENS_RADIAL_DISTORTION
+ * @see CaptureRequest#SENSOR_EXPOSURE_TIME
+ * @see CameraCharacteristics#SENSOR_INFO_TIMESTAMP_SOURCE
+ * @see CaptureResult#SENSOR_ROLLING_SHUTTER_SKEW
+ * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES
+ */
+ public static final int REQUEST_AVAILABLE_CAPABILITIES_MOTION_TRACKING = 10;
+
+ /**
+ * <p>The camera device is a logical camera backed by two or more physical cameras that are
+ * also exposed to the application.</p>
+ * <p>This capability requires the camera device to support the following:</p>
+ * <ul>
+ * <li>This camera device must list the following static metadata entries in {@link android.hardware.camera2.CameraCharacteristics }:<ul>
+ * <li>android.logicalMultiCamera.physicalIds</li>
+ * <li>{@link CameraCharacteristics#LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE android.logicalMultiCamera.sensorSyncType}</li>
+ * </ul>
+ * </li>
+ * <li>The underlying physical cameras' static metadata must list the following entries,
+ * so that the application can correlate pixels from the physical streams:<ul>
+ * <li>{@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference}</li>
+ * <li>{@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation}</li>
+ * <li>{@link CameraCharacteristics#LENS_POSE_TRANSLATION android.lens.poseTranslation}</li>
+ * <li>{@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration}</li>
+ * <li>{@link CameraCharacteristics#LENS_RADIAL_DISTORTION android.lens.radialDistortion}</li>
+ * </ul>
+ * </li>
+ * <li>The logical camera device must be LIMITED or higher device.</li>
+ * </ul>
+ * <p>Both the logical camera device and its underlying physical devices support the
+ * mandatory stream combinations required for their device levels.</p>
+ * <p>Additionally, for each guaranteed stream combination, the logical camera supports:</p>
+ * <ul>
+ * <li>Replacing one logical {@link android.graphics.ImageFormat#YUV_420_888 YUV_420_888}
+ * or raw stream with two physical streams of the same size and format, each from a
+ * separate physical camera, given that the size and format are supported by both
+ * physical cameras.</li>
+ * <li>Adding two raw streams, each from one physical camera, if the logical camera doesn't
+ * advertise RAW capability, but the underlying physical cameras do. This is usually
+ * the case when the physical cameras have different sensor sizes.</li>
+ * </ul>
+ * <p>Using physical streams in place of a logical stream of the same size and format will
+ * not slow down the frame rate of the capture, as long as the minimum frame duration
+ * of the physical and logical streams are the same.</p>
+ *
+ * @see CameraCharacteristics#LENS_INTRINSIC_CALIBRATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
+ * @see CameraCharacteristics#LENS_POSE_ROTATION
+ * @see CameraCharacteristics#LENS_POSE_TRANSLATION
+ * @see CameraCharacteristics#LENS_RADIAL_DISTORTION
+ * @see CameraCharacteristics#LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE
+ * @see CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES
+ */
+ public static final int REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA = 11;
+
//
// Enumeration values for CameraCharacteristics#SCALER_CROPPING_TYPE
//
@@ -1063,6 +1181,38 @@ public abstract class CameraMetadata<TKey> {
*/
public static final int INFO_SUPPORTED_HARDWARE_LEVEL_3 = 3;
+ /**
+ * <p>This camera device is backed by an external camera connected to this Android device.</p>
+ * <p>The device has capability identical to a LIMITED level device, with the following
+ * exceptions:</p>
+ * <ul>
+ * <li>The device may not report lens/sensor related information such as<ul>
+ * <li>{@link CaptureRequest#LENS_FOCAL_LENGTH android.lens.focalLength}</li>
+ * <li>{@link CameraCharacteristics#LENS_INFO_HYPERFOCAL_DISTANCE android.lens.info.hyperfocalDistance}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_INFO_PHYSICAL_SIZE android.sensor.info.physicalSize}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_INFO_WHITE_LEVEL android.sensor.info.whiteLevel}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_BLACK_LEVEL_PATTERN android.sensor.blackLevelPattern}</li>
+ * <li>{@link CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT android.sensor.info.colorFilterArrangement}</li>
+ * <li>{@link CaptureResult#SENSOR_ROLLING_SHUTTER_SKEW android.sensor.rollingShutterSkew}</li>
+ * </ul>
+ * </li>
+ * <li>The device will report 0 for {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}</li>
+ * <li>The device has less guarantee on stable framerate, as the framerate partly depends
+ * on the external camera being used.</li>
+ * </ul>
+ *
+ * @see CaptureRequest#LENS_FOCAL_LENGTH
+ * @see CameraCharacteristics#LENS_INFO_HYPERFOCAL_DISTANCE
+ * @see CameraCharacteristics#SENSOR_BLACK_LEVEL_PATTERN
+ * @see CameraCharacteristics#SENSOR_INFO_COLOR_FILTER_ARRANGEMENT
+ * @see CameraCharacteristics#SENSOR_INFO_PHYSICAL_SIZE
+ * @see CameraCharacteristics#SENSOR_INFO_WHITE_LEVEL
+ * @see CameraCharacteristics#SENSOR_ORIENTATION
+ * @see CaptureResult#SENSOR_ROLLING_SHUTTER_SKEW
+ * @see CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL
+ */
+ public static final int INFO_SUPPORTED_HARDWARE_LEVEL_EXTERNAL = 4;
+
//
// Enumeration values for CameraCharacteristics#SYNC_MAX_LATENCY
//
@@ -1089,6 +1239,26 @@ public abstract class CameraMetadata<TKey> {
public static final int SYNC_MAX_LATENCY_UNKNOWN = -1;
//
+ // Enumeration values for CameraCharacteristics#LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE
+ //
+
+ /**
+ * <p>A software mechanism is used to synchronize between the physical cameras. As a result,
+ * the timestamp of an image from a physical stream is only an approximation of the
+ * image sensor start-of-exposure time.</p>
+ * @see CameraCharacteristics#LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE
+ */
+ public static final int LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE_APPROXIMATE = 0;
+
+ /**
+ * <p>The camera device supports frame timestamp synchronization at the hardware level,
+ * and the timestamp of a physical stream image accurately reflects its
+ * start-of-exposure time.</p>
+ * @see CameraCharacteristics#LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE
+ */
+ public static final int LOGICAL_MULTI_CAMERA_SENSOR_SYNC_TYPE_CALIBRATED = 1;
+
+ //
// Enumeration values for CaptureRequest#COLOR_CORRECTION_MODE
//
@@ -1288,6 +1458,20 @@ public abstract class CameraMetadata<TKey> {
*/
public static final int CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE = 4;
+ /**
+ * <p>An external flash has been turned on.</p>
+ * <p>It informs the camera device that an external flash has been turned on, and that
+ * metering (and continuous focus if active) should be quickly recaculated to account
+ * for the external flash. Otherwise, this mode acts like ON.</p>
+ * <p>When the external flash is turned off, AE mode should be changed to one of the
+ * other available AE modes.</p>
+ * <p>If the camera device supports AE external flash mode, aeState must be
+ * FLASH_REQUIRED after the camera device finishes AE scan and it's too dark without
+ * flash.</p>
+ * @see CaptureRequest#CONTROL_AE_MODE
+ */
+ public static final int CONTROL_AE_MODE_ON_EXTERNAL_FLASH = 5;
+
//
// Enumeration values for CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
//
@@ -1661,6 +1845,16 @@ public abstract class CameraMetadata<TKey> {
*/
public static final int CONTROL_CAPTURE_INTENT_MANUAL = 6;
+ /**
+ * <p>This request is for a motion tracking use case, where
+ * the application will use camera and inertial sensor data to
+ * locate and track objects in the world.</p>
+ * <p>The camera device auto-exposure routine will limit the exposure time
+ * of the camera to no more than 20 milliseconds, to minimize motion blur.</p>
+ * @see CaptureRequest#CONTROL_CAPTURE_INTENT
+ */
+ public static final int CONTROL_CAPTURE_INTENT_MOTION_TRACKING = 7;
+
//
// Enumeration values for CaptureRequest#CONTROL_EFFECT_MODE
//
@@ -2471,6 +2665,22 @@ public abstract class CameraMetadata<TKey> {
public static final int STATISTICS_LENS_SHADING_MAP_MODE_ON = 1;
//
+ // Enumeration values for CaptureRequest#STATISTICS_OIS_DATA_MODE
+ //
+
+ /**
+ * <p>Do not include OIS data in the capture result.</p>
+ * @see CaptureRequest#STATISTICS_OIS_DATA_MODE
+ */
+ public static final int STATISTICS_OIS_DATA_MODE_OFF = 0;
+
+ /**
+ * <p>Include OIS data in the capture result.</p>
+ * @see CaptureRequest#STATISTICS_OIS_DATA_MODE
+ */
+ public static final int STATISTICS_OIS_DATA_MODE_ON = 1;
+
+ //
// Enumeration values for CaptureRequest#TONEMAP_MODE
//
diff --git a/android/hardware/camera2/CaptureRequest.java b/android/hardware/camera2/CaptureRequest.java
index 77da2a51..481b7649 100644
--- a/android/hardware/camera2/CaptureRequest.java
+++ b/android/hardware/camera2/CaptureRequest.java
@@ -33,9 +33,11 @@ import android.view.Surface;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
-
+import java.util.Set;
/**
* <p>An immutable package of settings and outputs needed to capture a single
@@ -219,7 +221,11 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
private static final ArraySet<Surface> mEmptySurfaceSet = new ArraySet<Surface>();
- private final CameraMetadataNative mSettings;
+ private String mLogicalCameraId;
+ private CameraMetadataNative mLogicalCameraSettings;
+ private final HashMap<String, CameraMetadataNative> mPhysicalCameraSettings =
+ new HashMap<String, CameraMetadataNative>();
+
private boolean mIsReprocess;
// If this request is part of constrained high speed request list that was created by
// {@link android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession#createHighSpeedRequestList}
@@ -236,8 +242,6 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* Used by Binder to unparcel this object only.
*/
private CaptureRequest() {
- mSettings = new CameraMetadataNative();
- setNativeInstance(mSettings);
mIsReprocess = false;
mReprocessableSessionId = CameraCaptureSession.SESSION_ID_NONE;
}
@@ -249,8 +253,14 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
*/
@SuppressWarnings("unchecked")
private CaptureRequest(CaptureRequest source) {
- mSettings = new CameraMetadataNative(source.mSettings);
- setNativeInstance(mSettings);
+ mLogicalCameraId = new String(source.mLogicalCameraId);
+ for (Map.Entry<String, CameraMetadataNative> entry :
+ source.mPhysicalCameraSettings.entrySet()) {
+ mPhysicalCameraSettings.put(new String(entry.getKey()),
+ new CameraMetadataNative(entry.getValue()));
+ }
+ mLogicalCameraSettings = mPhysicalCameraSettings.get(mLogicalCameraId);
+ setNativeInstance(mLogicalCameraSettings);
mSurfaceSet.addAll(source.mSurfaceSet);
mIsReprocess = source.mIsReprocess;
mIsPartOfCHSRequestList = source.mIsPartOfCHSRequestList;
@@ -272,16 +282,35 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* reprocess capture request to the same session where
* the {@link TotalCaptureResult}, used to create the reprocess
* capture, came from.
+ * @param logicalCameraId Camera Id of the actively open camera that instantiates the
+ * Builder.
+ *
+ * @param physicalCameraIdSet A set of physical camera ids that can be used to customize
+ * the request for a specific physical camera.
*
* @throws IllegalArgumentException If creating a reprocess capture request with an invalid
- * reprocessableSessionId.
+ * reprocessableSessionId, or multiple physical cameras.
*
* @see CameraDevice#createReprocessCaptureRequest
*/
private CaptureRequest(CameraMetadataNative settings, boolean isReprocess,
- int reprocessableSessionId) {
- mSettings = CameraMetadataNative.move(settings);
- setNativeInstance(mSettings);
+ int reprocessableSessionId, String logicalCameraId, Set<String> physicalCameraIdSet) {
+ if ((physicalCameraIdSet != null) && isReprocess) {
+ throw new IllegalArgumentException("Create a reprocess capture request with " +
+ "with more than one physical camera is not supported!");
+ }
+
+ mLogicalCameraId = logicalCameraId;
+ mLogicalCameraSettings = CameraMetadataNative.move(settings);
+ mPhysicalCameraSettings.put(mLogicalCameraId, mLogicalCameraSettings);
+ if (physicalCameraIdSet != null) {
+ for (String physicalId : physicalCameraIdSet) {
+ mPhysicalCameraSettings.put(physicalId, new CameraMetadataNative(
+ mLogicalCameraSettings));
+ }
+ }
+
+ setNativeInstance(mLogicalCameraSettings);
mIsReprocess = isReprocess;
if (isReprocess) {
if (reprocessableSessionId == CameraCaptureSession.SESSION_ID_NONE) {
@@ -309,7 +338,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
*/
@Nullable
public <T> T get(Key<T> key) {
- return mSettings.get(key);
+ return mLogicalCameraSettings.get(key);
}
/**
@@ -319,7 +348,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
@SuppressWarnings("unchecked")
@Override
protected <T> T getProtected(Key<?> key) {
- return (T) mSettings.get(key);
+ return (T) mLogicalCameraSettings.get(key);
}
/**
@@ -403,7 +432,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* @hide
*/
public CameraMetadataNative getNativeCopy() {
- return new CameraMetadataNative(mSettings);
+ return new CameraMetadataNative(mLogicalCameraSettings);
}
/**
@@ -444,14 +473,16 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
return other != null
&& Objects.equals(mUserTag, other.mUserTag)
&& mSurfaceSet.equals(other.mSurfaceSet)
- && mSettings.equals(other.mSettings)
+ && mPhysicalCameraSettings.equals(other.mPhysicalCameraSettings)
+ && mLogicalCameraId.equals(other.mLogicalCameraId)
+ && mLogicalCameraSettings.equals(other.mLogicalCameraSettings)
&& mIsReprocess == other.mIsReprocess
&& mReprocessableSessionId == other.mReprocessableSessionId;
}
@Override
public int hashCode() {
- return HashCodeHelpers.hashCodeGeneric(mSettings, mSurfaceSet, mUserTag);
+ return HashCodeHelpers.hashCodeGeneric(mPhysicalCameraSettings, mSurfaceSet, mUserTag);
}
public static final Parcelable.Creator<CaptureRequest> CREATOR =
@@ -479,8 +510,25 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* @hide
*/
private void readFromParcel(Parcel in) {
- mSettings.readFromParcel(in);
- setNativeInstance(mSettings);
+ int physicalCameraCount = in.readInt();
+ if (physicalCameraCount <= 0) {
+ throw new RuntimeException("Physical camera count" + physicalCameraCount +
+ " should always be positive");
+ }
+
+ //Always start with the logical camera id
+ mLogicalCameraId = in.readString();
+ mLogicalCameraSettings = new CameraMetadataNative();
+ mLogicalCameraSettings.readFromParcel(in);
+ setNativeInstance(mLogicalCameraSettings);
+ mPhysicalCameraSettings.put(mLogicalCameraId, mLogicalCameraSettings);
+ for (int i = 1; i < physicalCameraCount; i++) {
+ String physicalId = in.readString();
+ CameraMetadataNative physicalCameraSettings = new CameraMetadataNative();
+ physicalCameraSettings.readFromParcel(in);
+ mPhysicalCameraSettings.put(physicalId, physicalCameraSettings);
+ }
+
mIsReprocess = (in.readInt() == 0) ? false : true;
mReprocessableSessionId = CameraCaptureSession.SESSION_ID_NONE;
@@ -509,7 +557,19 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
@Override
public void writeToParcel(Parcel dest, int flags) {
- mSettings.writeToParcel(dest, flags);
+ int physicalCameraCount = mPhysicalCameraSettings.size();
+ dest.writeInt(physicalCameraCount);
+ //Logical camera id and settings always come first.
+ dest.writeString(mLogicalCameraId);
+ mLogicalCameraSettings.writeToParcel(dest, flags);
+ for (Map.Entry<String, CameraMetadataNative> entry : mPhysicalCameraSettings.entrySet()) {
+ if (entry.getKey().equals(mLogicalCameraId)) {
+ continue;
+ }
+ dest.writeString(entry.getKey());
+ entry.getValue().writeToParcel(dest, flags);
+ }
+
dest.writeInt(mIsReprocess ? 1 : 0);
synchronized (mSurfacesLock) {
@@ -542,6 +602,14 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
}
/**
+ * Retrieves the logical camera id.
+ * @hide
+ */
+ public String getLogicalCameraId() {
+ return mLogicalCameraId;
+ }
+
+ /**
* @hide
*/
public void convertSurfaceToStreamId(
@@ -633,14 +701,20 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* submits a reprocess capture request to the same session
* where the {@link TotalCaptureResult}, used to create the
* reprocess capture, came from.
+ * @param logicalCameraId Camera Id of the actively open camera that instantiates the
+ * Builder.
+ * @param physicalCameraIdSet A set of physical camera ids that can be used to customize
+ * the request for a specific physical camera.
*
* @throws IllegalArgumentException If creating a reprocess capture request with an invalid
* reprocessableSessionId.
* @hide
*/
public Builder(CameraMetadataNative template, boolean reprocess,
- int reprocessableSessionId) {
- mRequest = new CaptureRequest(template, reprocess, reprocessableSessionId);
+ int reprocessableSessionId, String logicalCameraId,
+ Set<String> physicalCameraIdSet) {
+ mRequest = new CaptureRequest(template, reprocess, reprocessableSessionId,
+ logicalCameraId, physicalCameraIdSet);
}
/**
@@ -682,7 +756,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* type to the key.
*/
public <T> void set(@NonNull Key<T> key, T value) {
- mRequest.mSettings.set(key, value);
+ mRequest.mLogicalCameraSettings.set(key, value);
}
/**
@@ -696,7 +770,71 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
*/
@Nullable
public <T> T get(Key<T> key) {
- return mRequest.mSettings.get(key);
+ return mRequest.mLogicalCameraSettings.get(key);
+ }
+
+ /**
+ * Set a capture request field to a value. The field definitions can be
+ * found in {@link CaptureRequest}.
+ *
+ * <p>Setting a field to {@code null} will remove that field from the capture request.
+ * Unless the field is optional, removing it will likely produce an error from the camera
+ * device when the request is submitted.</p>
+ *
+ *<p>This method can be called for logical camera devices, which are devices that have
+ * REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA capability and calls to
+ * {@link CameraCharacteristics#getPhysicalCameraIds} return a non-empty list of
+ * physical devices that are backing the logical camera. The camera Id included in the
+ * 'physicalCameraId' argument selects an individual physical device that will receive
+ * the customized capture request field.</p>
+ *
+ * @throws IllegalArgumentException if the physical camera id is not valid
+ *
+ * @param key The metadata field to write.
+ * @param value The value to set the field to, which must be of a matching
+ * @param physicalCameraId A valid physical camera Id. The valid camera Ids can be obtained
+ * via calls to {@link CameraCharacteristics#getPhysicalCameraIds}.
+ * @return The builder object.
+ * type to the key.
+ */
+ public <T> Builder setPhysicalCameraKey(@NonNull Key<T> key, T value,
+ @NonNull String physicalCameraId) {
+ if (!mRequest.mPhysicalCameraSettings.containsKey(physicalCameraId)) {
+ throw new IllegalArgumentException("Physical camera id: " + physicalCameraId +
+ " is not valid!");
+ }
+
+ mRequest.mPhysicalCameraSettings.get(physicalCameraId).set(key, value);
+
+ return this;
+ }
+
+ /**
+ * Get a capture request field value for a specific physical camera Id. The field
+ * definitions can be found in {@link CaptureRequest}.
+ *
+ *<p>This method can be called for logical camera devices, which are devices that have
+ * REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA capability and calls to
+ * {@link CameraCharacteristics#getPhysicalCameraIds} return a non-empty list of
+ * physical devices that are backing the logical camera. The camera Id included in the
+ * 'physicalCameraId' argument selects an individual physical device and returns
+ * its specific capture request field.</p>
+ *
+ * @throws IllegalArgumentException if the key or physical camera id were not valid
+ *
+ * @param key The metadata field to read.
+ * @param physicalCameraId A valid physical camera Id. The valid camera Ids can be obtained
+ * via calls to {@link CameraCharacteristics#getPhysicalCameraIds}.
+ * @return The value of that key, or {@code null} if the field is not set.
+ */
+ @Nullable
+ public <T> T getPhysicalCameraKey(Key<T> key,@NonNull String physicalCameraId) {
+ if (!mRequest.mPhysicalCameraSettings.containsKey(physicalCameraId)) {
+ throw new IllegalArgumentException("Physical camera id: " + physicalCameraId +
+ " is not valid!");
+ }
+
+ return mRequest.mPhysicalCameraSettings.get(physicalCameraId).get(key);
}
/**
@@ -748,7 +886,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* @hide
*/
public boolean isEmpty() {
- return mRequest.mSettings.isEmpty();
+ return mRequest.mLogicalCameraSettings.isEmpty();
}
}
@@ -1076,6 +1214,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* <li>{@link #CONTROL_AE_MODE_ON_AUTO_FLASH ON_AUTO_FLASH}</li>
* <li>{@link #CONTROL_AE_MODE_ON_ALWAYS_FLASH ON_ALWAYS_FLASH}</li>
* <li>{@link #CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE ON_AUTO_FLASH_REDEYE}</li>
+ * <li>{@link #CONTROL_AE_MODE_ON_EXTERNAL_FLASH ON_EXTERNAL_FLASH}</li>
* </ul></p>
* <p><b>Available values for this device:</b><br>
* {@link CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES android.control.aeAvailableModes}</p>
@@ -1093,6 +1232,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* @see #CONTROL_AE_MODE_ON_AUTO_FLASH
* @see #CONTROL_AE_MODE_ON_ALWAYS_FLASH
* @see #CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE
+ * @see #CONTROL_AE_MODE_ON_EXTERNAL_FLASH
*/
@PublicKey
public static final Key<Integer> CONTROL_AE_MODE =
@@ -1487,10 +1627,13 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* strategy.</p>
* <p>This control (except for MANUAL) is only effective if
* <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} != OFF</code> and any 3A routine is active.</p>
- * <p>ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities}
- * contains PRIVATE_REPROCESSING or YUV_REPROCESSING. MANUAL will be supported if
- * {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains MANUAL_SENSOR. Other intent values are
- * always supported.</p>
+ * <p>All intents are supported by all devices, except that:
+ * * ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+ * PRIVATE_REPROCESSING or YUV_REPROCESSING.
+ * * MANUAL will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+ * MANUAL_SENSOR.
+ * * MOTION_TRACKING will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+ * MOTION_TRACKING.</p>
* <p><b>Possible values:</b>
* <ul>
* <li>{@link #CONTROL_CAPTURE_INTENT_CUSTOM CUSTOM}</li>
@@ -1500,6 +1643,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* <li>{@link #CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT VIDEO_SNAPSHOT}</li>
* <li>{@link #CONTROL_CAPTURE_INTENT_ZERO_SHUTTER_LAG ZERO_SHUTTER_LAG}</li>
* <li>{@link #CONTROL_CAPTURE_INTENT_MANUAL MANUAL}</li>
+ * <li>{@link #CONTROL_CAPTURE_INTENT_MOTION_TRACKING MOTION_TRACKING}</li>
* </ul></p>
* <p>This key is available on all devices.</p>
*
@@ -1512,6 +1656,7 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
* @see #CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT
* @see #CONTROL_CAPTURE_INTENT_ZERO_SHUTTER_LAG
* @see #CONTROL_CAPTURE_INTENT_MANUAL
+ * @see #CONTROL_CAPTURE_INTENT_MOTION_TRACKING
*/
@PublicKey
public static final Key<Integer> CONTROL_CAPTURE_INTENT =
@@ -2612,6 +2757,29 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>>
new Key<Integer>("android.statistics.lensShadingMapMode", int.class);
/**
+ * <p>Whether the camera device outputs the OIS data in output
+ * result metadata.</p>
+ * <p>When set to ON,
+ * {@link CaptureResult#STATISTICS_OIS_TIMESTAMPS android.statistics.oisTimestamps}, android.statistics.oisShiftPixelX,
+ * android.statistics.oisShiftPixelY will provide OIS data in the output result metadata.</p>
+ * <p><b>Possible values:</b>
+ * <ul>
+ * <li>{@link #STATISTICS_OIS_DATA_MODE_OFF OFF}</li>
+ * <li>{@link #STATISTICS_OIS_DATA_MODE_ON ON}</li>
+ * </ul></p>
+ * <p><b>Available values for this device:</b><br>
+ * android.Statistics.info.availableOisDataModes</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CaptureResult#STATISTICS_OIS_TIMESTAMPS
+ * @see #STATISTICS_OIS_DATA_MODE_OFF
+ * @see #STATISTICS_OIS_DATA_MODE_ON
+ */
+ @PublicKey
+ public static final Key<Integer> STATISTICS_OIS_DATA_MODE =
+ new Key<Integer>("android.statistics.oisDataMode", int.class);
+
+ /**
* <p>Tonemapping / contrast / gamma curve for the blue
* channel, to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is
* CONTRAST_CURVE.</p>
diff --git a/android/hardware/camera2/CaptureResult.java b/android/hardware/camera2/CaptureResult.java
index 6d7b06fc..d730fa8a 100644
--- a/android/hardware/camera2/CaptureResult.java
+++ b/android/hardware/camera2/CaptureResult.java
@@ -691,6 +691,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
* <li>{@link #CONTROL_AE_MODE_ON_AUTO_FLASH ON_AUTO_FLASH}</li>
* <li>{@link #CONTROL_AE_MODE_ON_ALWAYS_FLASH ON_ALWAYS_FLASH}</li>
* <li>{@link #CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE ON_AUTO_FLASH_REDEYE}</li>
+ * <li>{@link #CONTROL_AE_MODE_ON_EXTERNAL_FLASH ON_EXTERNAL_FLASH}</li>
* </ul></p>
* <p><b>Available values for this device:</b><br>
* {@link CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES android.control.aeAvailableModes}</p>
@@ -708,6 +709,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
* @see #CONTROL_AE_MODE_ON_AUTO_FLASH
* @see #CONTROL_AE_MODE_ON_ALWAYS_FLASH
* @see #CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE
+ * @see #CONTROL_AE_MODE_ON_EXTERNAL_FLASH
*/
@PublicKey
public static final Key<Integer> CONTROL_AE_MODE =
@@ -877,7 +879,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
* </tr>
* </tbody>
* </table>
- * <p>When {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is AE_MODE_ON_*:</p>
+ * <p>When {@link CaptureRequest#CONTROL_AE_MODE android.control.aeMode} is AE_MODE_ON*:</p>
* <table>
* <thead>
* <tr>
@@ -998,10 +1000,13 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
* </tr>
* </tbody>
* </table>
+ * <p>If the camera device supports AE external flash mode (ON_EXTERNAL_FLASH is included in
+ * {@link CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES android.control.aeAvailableModes}), aeState must be FLASH_REQUIRED after the camera device
+ * finishes AE scan and it's too dark without flash.</p>
* <p>For the above table, the camera device may skip reporting any state changes that happen
* without application intervention (i.e. mode switch, trigger, locking). Any state that
* can be skipped in that manner is called a transient state.</p>
- * <p>For example, for above AE modes (AE_MODE_ON_*), in addition to the state transitions
+ * <p>For example, for above AE modes (AE_MODE_ON*), in addition to the state transitions
* listed in above table, it is also legal for the camera device to skip one or more
* transient states between two results. See below table for examples:</p>
* <table>
@@ -1072,6 +1077,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
* Present on all camera devices that report being at least {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_LIMITED HARDWARE_LEVEL_LIMITED} devices in the
* {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p>
*
+ * @see CameraCharacteristics#CONTROL_AE_AVAILABLE_MODES
* @see CaptureRequest#CONTROL_AE_LOCK
* @see CaptureRequest#CONTROL_AE_MODE
* @see CaptureRequest#CONTROL_AE_PRECAPTURE_TRIGGER
@@ -1754,10 +1760,13 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
* strategy.</p>
* <p>This control (except for MANUAL) is only effective if
* <code>{@link CaptureRequest#CONTROL_MODE android.control.mode} != OFF</code> and any 3A routine is active.</p>
- * <p>ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities}
- * contains PRIVATE_REPROCESSING or YUV_REPROCESSING. MANUAL will be supported if
- * {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains MANUAL_SENSOR. Other intent values are
- * always supported.</p>
+ * <p>All intents are supported by all devices, except that:
+ * * ZERO_SHUTTER_LAG will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+ * PRIVATE_REPROCESSING or YUV_REPROCESSING.
+ * * MANUAL will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+ * MANUAL_SENSOR.
+ * * MOTION_TRACKING will be supported if {@link CameraCharacteristics#REQUEST_AVAILABLE_CAPABILITIES android.request.availableCapabilities} contains
+ * MOTION_TRACKING.</p>
* <p><b>Possible values:</b>
* <ul>
* <li>{@link #CONTROL_CAPTURE_INTENT_CUSTOM CUSTOM}</li>
@@ -1767,6 +1776,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
* <li>{@link #CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT VIDEO_SNAPSHOT}</li>
* <li>{@link #CONTROL_CAPTURE_INTENT_ZERO_SHUTTER_LAG ZERO_SHUTTER_LAG}</li>
* <li>{@link #CONTROL_CAPTURE_INTENT_MANUAL MANUAL}</li>
+ * <li>{@link #CONTROL_CAPTURE_INTENT_MOTION_TRACKING MOTION_TRACKING}</li>
* </ul></p>
* <p>This key is available on all devices.</p>
*
@@ -1779,6 +1789,7 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
* @see #CONTROL_CAPTURE_INTENT_VIDEO_SNAPSHOT
* @see #CONTROL_CAPTURE_INTENT_ZERO_SHUTTER_LAG
* @see #CONTROL_CAPTURE_INTENT_MANUAL
+ * @see #CONTROL_CAPTURE_INTENT_MOTION_TRACKING
*/
@PublicKey
public static final Key<Integer> CONTROL_CAPTURE_INTENT =
@@ -2192,8 +2203,6 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
* 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>
@@ -2761,36 +2770,33 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
/**
* <p>Position of the camera optical center.</p>
* <p>The position of the camera device's lens optical center,
- * as a three-dimensional vector <code>(x,y,z)</code>, relative to the
- * optical center of the largest camera device facing in the
- * same direction as this camera, in the {@link android.hardware.SensorEvent Android sensor coordinate
- * axes}. Note that only the axis definitions are shared with
- * the sensor coordinate system, but not the origin.</p>
- * <p>If this device is the largest or only camera device with a
- * given facing, then this position will be <code>(0, 0, 0)</code>; a
- * camera device with a lens optical center located 3 cm from
- * the main sensor along the +X axis (to the right from the
- * user's perspective) will report <code>(0.03, 0, 0)</code>.</p>
- * <p>To transform a pixel coordinates between two cameras
- * facing the same direction, first the source camera
- * {@link CameraCharacteristics#LENS_RADIAL_DISTORTION android.lens.radialDistortion} must be corrected for. Then
- * the source camera {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration} needs
- * to be applied, followed by the {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation}
- * of the source camera, the translation of the source camera
- * relative to the destination camera, the
- * {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation} of the destination camera, and
- * finally the inverse of {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration}
- * of the destination camera. This obtains a
- * radial-distortion-free coordinate in the destination
- * camera pixel coordinates.</p>
- * <p>To compare this against a real image from the destination
- * camera, the destination camera image then needs to be
- * corrected for radial distortion before comparison or
- * sampling.</p>
+ * as a three-dimensional vector <code>(x,y,z)</code>.</p>
+ * <p>Prior to Android P, or when {@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference} is PRIMARY_CAMERA, this position
+ * is relative to the optical center of the largest camera device facing in the same
+ * direction as this camera, in the {@link android.hardware.SensorEvent Android sensor
+ * coordinate axes}. Note that only the axis definitions are shared with the sensor
+ * coordinate system, but not the origin.</p>
+ * <p>If this device is the largest or only camera device with a given facing, then this
+ * position will be <code>(0, 0, 0)</code>; a camera device with a lens optical center located 3 cm
+ * from the main sensor along the +X axis (to the right from the user's perspective) will
+ * report <code>(0.03, 0, 0)</code>.</p>
+ * <p>To transform a pixel coordinates between two cameras facing the same direction, first
+ * the source camera {@link CameraCharacteristics#LENS_RADIAL_DISTORTION android.lens.radialDistortion} must be corrected for. Then the source
+ * camera {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration} needs to be applied, followed by the
+ * {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation} of the source camera, the translation of the source camera
+ * relative to the destination camera, the {@link CameraCharacteristics#LENS_POSE_ROTATION android.lens.poseRotation} of the destination
+ * camera, and finally the inverse of {@link CameraCharacteristics#LENS_INTRINSIC_CALIBRATION android.lens.intrinsicCalibration} of the destination
+ * camera. This obtains a radial-distortion-free coordinate in the destination camera pixel
+ * coordinates.</p>
+ * <p>To compare this against a real image from the destination camera, the destination camera
+ * image then needs to be corrected for radial distortion before comparison or sampling.</p>
+ * <p>When {@link CameraCharacteristics#LENS_POSE_REFERENCE android.lens.poseReference} is GYROSCOPE, then this position is relative to
+ * the center of the primary gyroscope on the device.</p>
* <p><b>Units</b>: Meters</p>
* <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
*
* @see CameraCharacteristics#LENS_INTRINSIC_CALIBRATION
+ * @see CameraCharacteristics#LENS_POSE_REFERENCE
* @see CameraCharacteristics#LENS_POSE_ROTATION
* @see CameraCharacteristics#LENS_RADIAL_DISTORTION
*/
@@ -3903,6 +3909,76 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> {
new Key<Integer>("android.statistics.lensShadingMapMode", int.class);
/**
+ * <p>Whether the camera device outputs the OIS data in output
+ * result metadata.</p>
+ * <p>When set to ON,
+ * {@link CaptureResult#STATISTICS_OIS_TIMESTAMPS android.statistics.oisTimestamps}, android.statistics.oisShiftPixelX,
+ * android.statistics.oisShiftPixelY will provide OIS data in the output result metadata.</p>
+ * <p><b>Possible values:</b>
+ * <ul>
+ * <li>{@link #STATISTICS_OIS_DATA_MODE_OFF OFF}</li>
+ * <li>{@link #STATISTICS_OIS_DATA_MODE_ON ON}</li>
+ * </ul></p>
+ * <p><b>Available values for this device:</b><br>
+ * android.Statistics.info.availableOisDataModes</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CaptureResult#STATISTICS_OIS_TIMESTAMPS
+ * @see #STATISTICS_OIS_DATA_MODE_OFF
+ * @see #STATISTICS_OIS_DATA_MODE_ON
+ */
+ @PublicKey
+ public static final Key<Integer> STATISTICS_OIS_DATA_MODE =
+ new Key<Integer>("android.statistics.oisDataMode", int.class);
+
+ /**
+ * <p>An array of timestamps of OIS samples, in nanoseconds.</p>
+ * <p>The array contains the timestamps of OIS samples. The timestamps are in the same
+ * timebase as and comparable to {@link CaptureResult#SENSOR_TIMESTAMP android.sensor.timestamp}.</p>
+ * <p><b>Units</b>: nanoseconds</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CaptureResult#SENSOR_TIMESTAMP
+ */
+ @PublicKey
+ public static final Key<long[]> STATISTICS_OIS_TIMESTAMPS =
+ new Key<long[]>("android.statistics.oisTimestamps", long[].class);
+
+ /**
+ * <p>An array of shifts of OIS samples, in x direction.</p>
+ * <p>The array contains the amount of shifts in x direction, in pixels, based on OIS samples.
+ * A positive value is a shift from left to right in active array coordinate system. For
+ * example, if the optical center is (1000, 500) in active array coordinates, an shift of
+ * (3, 0) puts the new optical center at (1003, 500).</p>
+ * <p>The number of shifts must match the number of timestamps in
+ * {@link CaptureResult#STATISTICS_OIS_TIMESTAMPS android.statistics.oisTimestamps}.</p>
+ * <p><b>Units</b>: Pixels in active array.</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CaptureResult#STATISTICS_OIS_TIMESTAMPS
+ */
+ @PublicKey
+ public static final Key<float[]> STATISTICS_OIS_X_SHIFTS =
+ new Key<float[]>("android.statistics.oisXShifts", float[].class);
+
+ /**
+ * <p>An array of shifts of OIS samples, in y direction.</p>
+ * <p>The array contains the amount of shifts in y direction, in pixels, based on OIS samples.
+ * A positive value is a shift from top to bottom in active array coordinate system. For
+ * example, if the optical center is (1000, 500) in active array coordinates, an shift of
+ * (0, 5) puts the new optical center at (1000, 505).</p>
+ * <p>The number of shifts must match the number of timestamps in
+ * {@link CaptureResult#STATISTICS_OIS_TIMESTAMPS android.statistics.oisTimestamps}.</p>
+ * <p><b>Units</b>: Pixels in active array.</p>
+ * <p><b>Optional</b> - This value may be {@code null} on some devices.</p>
+ *
+ * @see CaptureResult#STATISTICS_OIS_TIMESTAMPS
+ */
+ @PublicKey
+ public static final Key<float[]> STATISTICS_OIS_Y_SHIFTS =
+ new Key<float[]>("android.statistics.oisYShifts", float[].class);
+
+ /**
* <p>Tonemapping / contrast / gamma curve for the blue
* channel, to use when {@link CaptureRequest#TONEMAP_MODE android.tonemap.mode} is
* CONTRAST_CURVE.</p>
diff --git a/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java b/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java
index 8c4dbfa5..06c2c25a 100644
--- a/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java
+++ b/android/hardware/camera2/impl/CameraConstrainedHighSpeedCaptureSessionImpl.java
@@ -94,8 +94,8 @@ public class CameraConstrainedHighSpeedCaptureSessionImpl
// Note that after this step, the requestMetadata is mutated (swapped) and can not be used
// for next request builder creation.
CaptureRequest.Builder singleTargetRequestBuilder = new CaptureRequest.Builder(
- requestMetadata, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE);
-
+ requestMetadata, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE,
+ request.getLogicalCameraId(), /*physicalCameraIdSet*/ null);
// Carry over userTag, as native metadata doesn't have this field.
singleTargetRequestBuilder.setTag(request.getTag());
@@ -120,7 +120,8 @@ public class CameraConstrainedHighSpeedCaptureSessionImpl
// CaptureRequest.Builder creation.
requestMetadata = new CameraMetadataNative(request.getNativeCopy());
doubleTargetRequestBuilder = new CaptureRequest.Builder(
- requestMetadata, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE);
+ requestMetadata, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE,
+ request.getLogicalCameraId(), /*physicalCameraIdSet*/null);
doubleTargetRequestBuilder.setTag(request.getTag());
doubleTargetRequestBuilder.set(CaptureRequest.CONTROL_CAPTURE_INTENT,
CaptureRequest.CONTROL_CAPTURE_INTENT_VIDEO_RECORD);
diff --git a/android/hardware/camera2/impl/CameraDeviceImpl.java b/android/hardware/camera2/impl/CameraDeviceImpl.java
index f1ffb890..cab9d704 100644
--- a/android/hardware/camera2/impl/CameraDeviceImpl.java
+++ b/android/hardware/camera2/impl/CameraDeviceImpl.java
@@ -16,27 +16,25 @@
package android.hardware.camera2.impl;
-import static android.hardware.camera2.CameraAccessException.CAMERA_IN_USE;
+import static com.android.internal.util.function.pooled.PooledLambda.obtainRunnable;
-import android.graphics.ImageFormat;
+import android.hardware.ICameraService;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.CaptureResult;
-import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.ICameraDeviceCallbacks;
import android.hardware.camera2.ICameraDeviceUser;
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;
-import android.hardware.ICameraService;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
@@ -51,16 +49,15 @@ import android.view.Surface;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collection;
-import java.util.Collections;
-import java.util.concurrent.atomic.AtomicBoolean;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
-import java.util.List;
import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicBoolean;
/**
* HAL2.1+ implementation of CameraDevice. Use CameraManager#open to instantiate
@@ -715,6 +712,38 @@ public class CameraDeviceImpl extends CameraDevice
}
@Override
+ public CaptureRequest.Builder createCaptureRequest(int templateType,
+ Set<String> physicalCameraIdSet)
+ throws CameraAccessException {
+ synchronized(mInterfaceLock) {
+ checkIfCameraClosedOrInError();
+
+ for (String physicalId : physicalCameraIdSet) {
+ if (physicalId == getId()) {
+ throw new IllegalStateException("Physical id matches the logical id!");
+ }
+ }
+
+ CameraMetadataNative templatedRequest = null;
+
+ templatedRequest = mRemoteDevice.createDefaultRequest(templateType);
+
+ // If app target SDK is older than O, or it's not a still capture template, enableZsl
+ // must be false in the default request.
+ if (mAppTargetSdkVersion < Build.VERSION_CODES.O ||
+ templateType != TEMPLATE_STILL_CAPTURE) {
+ overrideEnableZsl(templatedRequest, false);
+ }
+
+ CaptureRequest.Builder builder = new CaptureRequest.Builder(
+ templatedRequest, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE,
+ getId(), physicalCameraIdSet);
+
+ return builder;
+ }
+ }
+
+ @Override
public CaptureRequest.Builder createCaptureRequest(int templateType)
throws CameraAccessException {
synchronized(mInterfaceLock) {
@@ -732,7 +761,8 @@ public class CameraDeviceImpl extends CameraDevice
}
CaptureRequest.Builder builder = new CaptureRequest.Builder(
- templatedRequest, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE);
+ templatedRequest, /*reprocess*/false, CameraCaptureSession.SESSION_ID_NONE,
+ getId(), /*physicalCameraIdSet*/ null);
return builder;
}
@@ -748,7 +778,7 @@ public class CameraDeviceImpl extends CameraDevice
CameraMetadataNative(inputResult.getNativeCopy());
return new CaptureRequest.Builder(resultMetadata, /*reprocess*/true,
- inputResult.getSessionId());
+ inputResult.getSessionId(), getId(), /*physicalCameraIdSet*/ null);
}
}
@@ -958,7 +988,8 @@ public class CameraDeviceImpl extends CameraDevice
// callback is valid
handler = checkHandler(handler, callback);
- // Make sure that there all requests have at least 1 surface; all surfaces are non-null
+ // Make sure that there all requests have at least 1 surface; all surfaces are non-null;
+ // the surface isn't a physical stream surface for reprocessing request
for (CaptureRequest request : requestList) {
if (request.getTargets().isEmpty()) {
throw new IllegalArgumentException(
@@ -969,7 +1000,20 @@ public class CameraDeviceImpl extends CameraDevice
if (surface == null) {
throw new IllegalArgumentException("Null Surface targets are not allowed");
}
+
+ if (!request.isReprocess()) {
+ continue;
+ }
+ for (int i = 0; i < mConfiguredOutputs.size(); i++) {
+ OutputConfiguration configuration = mConfiguredOutputs.valueAt(i);
+ if (configuration.isForPhysicalCamera()
+ && configuration.getSurfaces().contains(surface)) {
+ throw new IllegalArgumentException(
+ "Reprocess request on physical stream is not allowed");
+ }
+ }
}
+
}
synchronized(mInterfaceLock) {
@@ -1798,34 +1842,36 @@ public class CameraDeviceImpl extends CameraDevice
case ERROR_CAMERA_DISCONNECTED:
CameraDeviceImpl.this.mDeviceHandler.post(mCallOnDisconnected);
break;
- default:
- Log.e(TAG, "Unknown error from camera device: " + errorCode);
- // no break
- case ERROR_CAMERA_DEVICE:
- case ERROR_CAMERA_SERVICE:
- mInError = true;
- final int publicErrorCode = (errorCode == ERROR_CAMERA_DEVICE) ?
- StateCallback.ERROR_CAMERA_DEVICE :
- StateCallback.ERROR_CAMERA_SERVICE;
- Runnable r = new Runnable() {
- @Override
- public void run() {
- if (!CameraDeviceImpl.this.isClosed()) {
- mDeviceCallback.onError(CameraDeviceImpl.this, publicErrorCode);
- }
- }
- };
- CameraDeviceImpl.this.mDeviceHandler.post(r);
- break;
case ERROR_CAMERA_REQUEST:
case ERROR_CAMERA_RESULT:
case ERROR_CAMERA_BUFFER:
onCaptureErrorLocked(errorCode, resultExtras);
break;
+ case ERROR_CAMERA_DEVICE:
+ scheduleNotifyError(StateCallback.ERROR_CAMERA_DEVICE);
+ break;
+ case ERROR_CAMERA_DISABLED:
+ scheduleNotifyError(StateCallback.ERROR_CAMERA_DISABLED);
+ break;
+ default:
+ Log.e(TAG, "Unknown error from camera device: " + errorCode);
+ scheduleNotifyError(StateCallback.ERROR_CAMERA_SERVICE);
}
}
}
+ private void scheduleNotifyError(int code) {
+ mInError = true;
+ CameraDeviceImpl.this.mDeviceHandler.post(obtainRunnable(
+ CameraDeviceCallbacks::notifyError, this, code));
+ }
+
+ private void notifyError(int code) {
+ if (!CameraDeviceImpl.this.isClosed()) {
+ mDeviceCallback.onError(CameraDeviceImpl.this, code);
+ }
+ }
+
@Override
public void onRepeatingRequestError(long lastFrameNumber, int repeatingRequestId) {
if (DEBUG) {
diff --git a/android/hardware/camera2/params/OutputConfiguration.java b/android/hardware/camera2/params/OutputConfiguration.java
index a85b5f71..f47cd665 100644
--- a/android/hardware/camera2/params/OutputConfiguration.java
+++ b/android/hardware/camera2/params/OutputConfiguration.java
@@ -31,13 +31,12 @@ import android.util.Log;
import android.util.Size;
import android.view.Surface;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Collections;
-import java.util.ArrayList;
-
import static com.android.internal.util.Preconditions.*;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
/**
* A class for describing camera output, which contains a {@link Surface} and its specific
* configuration for creating capture session.
@@ -266,6 +265,7 @@ public final class OutputConfiguration implements Parcelable {
mConfiguredGenerationId = surface.getGenerationId();
mIsDeferredConfig = false;
mIsShared = false;
+ mPhysicalCameraId = null;
}
/**
@@ -319,6 +319,7 @@ public final class OutputConfiguration implements Parcelable {
mConfiguredGenerationId = 0;
mIsDeferredConfig = true;
mIsShared = false;
+ mPhysicalCameraId = null;
}
/**
@@ -348,8 +349,9 @@ public final class OutputConfiguration implements Parcelable {
* </ol>
*
* <p>To enable surface sharing, this function must be called before {@link
- * CameraDevice#createCaptureSessionByOutputConfigurations}. Calling this function after {@link
- * CameraDevice#createCaptureSessionByOutputConfigurations} has no effect.</p>
+ * CameraDevice#createCaptureSessionByOutputConfigurations} or {@link
+ * CameraDevice#createReprocessableCaptureSessionByConfigurations}. Calling this function after
+ * {@link CameraDevice#createCaptureSessionByOutputConfigurations} has no effect.</p>
*
* <p>Up to {@link #getMaxSharedSurfaceCount} surfaces can be shared for an OutputConfiguration.
* The supported surfaces for sharing must be of type SurfaceTexture, SurfaceView,
@@ -360,6 +362,44 @@ public final class OutputConfiguration implements Parcelable {
}
/**
+ * Set the id of the physical camera for this OutputConfiguration
+ *
+ * <p>In the case one logical camera is made up of multiple physical cameras, it could be
+ * desirable for the camera application to request streams from individual physical cameras.
+ * This call achieves it by mapping the OutputConfiguration to the physical camera id.</p>
+ *
+ * <p>The valid physical camera id can be queried by {@link
+ * android.hardware.camera2.CameraCharacteristics#getPhysicalCameraIds}.
+ * </p>
+ *
+ * <p>Passing in a null physicalCameraId means that the OutputConfiguration is for a logical
+ * stream.</p>
+ *
+ * <p>This function must be called before {@link
+ * CameraDevice#createCaptureSessionByOutputConfigurations} or {@link
+ * CameraDevice#createReprocessableCaptureSessionByConfigurations}. Calling this function
+ * after {@link CameraDevice#createCaptureSessionByOutputConfigurations} or {@link
+ * CameraDevice#createReprocessableCaptureSessionByConfigurations} has no effect.</p>
+ *
+ * <p>The surface belonging to a physical camera OutputConfiguration must not be used as input
+ * or output of a reprocessing request. </p>
+ */
+ public void setPhysicalCameraId(@Nullable String physicalCameraId) {
+ mPhysicalCameraId = physicalCameraId;
+ }
+
+ /**
+ * Check if this configuration is for a physical camera.
+ *
+ * <p>This returns true if the output configuration was for a physical camera making up a
+ * logical multi camera via {@link OutputConfiguration#setPhysicalCameraId}.</p>
+ * @hide
+ */
+ public boolean isForPhysicalCamera() {
+ return (mPhysicalCameraId != null);
+ }
+
+ /**
* Check if this configuration has deferred configuration.
*
* <p>This will return true if the output configuration was constructed with surface deferred by
@@ -487,6 +527,7 @@ public final class OutputConfiguration implements Parcelable {
this.mConfiguredGenerationId = other.mConfiguredGenerationId;
this.mIsDeferredConfig = other.mIsDeferredConfig;
this.mIsShared = other.mIsShared;
+ this.mPhysicalCameraId = other.mPhysicalCameraId;
}
/**
@@ -502,6 +543,7 @@ public final class OutputConfiguration implements Parcelable {
boolean isShared = source.readInt() == 1;
ArrayList<Surface> surfaces = new ArrayList<Surface>();
source.readTypedList(surfaces, Surface.CREATOR);
+ String physicalCameraId = source.readString();
checkArgumentInRange(rotation, ROTATION_0, ROTATION_270, "Rotation constant");
@@ -524,6 +566,7 @@ public final class OutputConfiguration implements Parcelable {
StreamConfigurationMap.imageFormatToDataspace(ImageFormat.PRIVATE);
mConfiguredGenerationId = 0;
}
+ mPhysicalCameraId = physicalCameraId;
}
/**
@@ -622,6 +665,7 @@ public final class OutputConfiguration implements Parcelable {
dest.writeInt(mIsDeferredConfig ? 1 : 0);
dest.writeInt(mIsShared ? 1 : 0);
dest.writeTypedList(mSurfaces);
+ dest.writeString(mPhysicalCameraId);
}
/**
@@ -675,13 +719,15 @@ public final class OutputConfiguration implements Parcelable {
if (mIsDeferredConfig) {
return HashCodeHelpers.hashCode(
mRotation, mConfiguredSize.hashCode(), mConfiguredFormat, mConfiguredDataspace,
- mSurfaceGroupId, mSurfaceType, mIsShared ? 1 : 0);
+ mSurfaceGroupId, mSurfaceType, mIsShared ? 1 : 0,
+ mPhysicalCameraId == null ? 0 : mPhysicalCameraId.hashCode());
}
return HashCodeHelpers.hashCode(
mRotation, mSurfaces.hashCode(), mConfiguredGenerationId,
mConfiguredSize.hashCode(), mConfiguredFormat,
- mConfiguredDataspace, mSurfaceGroupId, mIsShared ? 1 : 0);
+ mConfiguredDataspace, mSurfaceGroupId, mIsShared ? 1 : 0,
+ mPhysicalCameraId == null ? 0 : mPhysicalCameraId.hashCode());
}
private static final String TAG = "OutputConfiguration";
@@ -701,4 +747,6 @@ public final class OutputConfiguration implements Parcelable {
private final boolean mIsDeferredConfig;
// Flag indicating if this config has shared surfaces
private boolean mIsShared;
+ // The physical camera id that this output configuration is for.
+ private String mPhysicalCameraId;
}
diff --git a/android/hardware/display/BrightnessChangeEvent.java b/android/hardware/display/BrightnessChangeEvent.java
index 3003607e..2301824c 100644
--- a/android/hardware/display/BrightnessChangeEvent.java
+++ b/android/hardware/display/BrightnessChangeEvent.java
@@ -16,6 +16,8 @@
package android.hardware.display;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -23,51 +25,65 @@ import android.os.Parcelable;
* Data about a brightness settings change.
*
* {@see DisplayManager.getBrightnessEvents()}
- * TODO make this SystemAPI
* @hide
*/
+@SystemApi
+@TestApi
public final class BrightnessChangeEvent implements Parcelable {
/** Brightness in nits */
- public int brightness;
+ public final float brightness;
/** Timestamp of the change {@see System.currentTimeMillis()} */
- public long timeStamp;
+ public final long timeStamp;
/** 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;
+ public final String packageName;
/** User id of of the user running when brightness was changed.
* @hide */
- public int userId;
+ public final int userId;
/** Lux values of recent sensor data */
- public float[] luxValues;
+ public final float[] luxValues;
/** Timestamps of the lux sensor readings {@see System.currentTimeMillis()} */
- public long[] luxTimestamps;
+ public final long[] luxTimestamps;
/** Most recent battery level when brightness was changed or Float.NaN */
- public float batteryLevel;
+ public final float batteryLevel;
/** Color filter active to provide night mode */
- public boolean nightMode;
+ public final boolean nightMode;
/** If night mode color filter is active this will be the temperature in kelvin */
- public int colorTemperature;
+ public final int colorTemperature;
- /** Brightness level before slider adjustment */
- public int lastBrightness;
+ /** Brightness le vel before slider adjustment */
+ public final float lastBrightness;
- public BrightnessChangeEvent() {
+ /** @hide */
+ private BrightnessChangeEvent(float brightness, long timeStamp, String packageName,
+ int userId, float[] luxValues, long[] luxTimestamps, float batteryLevel,
+ boolean nightMode, int colorTemperature, float lastBrightness) {
+ this.brightness = brightness;
+ this.timeStamp = timeStamp;
+ this.packageName = packageName;
+ this.userId = userId;
+ this.luxValues = luxValues;
+ this.luxTimestamps = luxTimestamps;
+ this.batteryLevel = batteryLevel;
+ this.nightMode = nightMode;
+ this.colorTemperature = colorTemperature;
+ this.lastBrightness = lastBrightness;
}
/** @hide */
- public BrightnessChangeEvent(BrightnessChangeEvent other) {
+ public BrightnessChangeEvent(BrightnessChangeEvent other, boolean redactPackage) {
this.brightness = other.brightness;
this.timeStamp = other.timeStamp;
- this.packageName = other.packageName;
+ this.packageName = redactPackage ? null : other.packageName;
this.userId = other.userId;
this.luxValues = other.luxValues;
this.luxTimestamps = other.luxTimestamps;
@@ -78,7 +94,7 @@ public final class BrightnessChangeEvent implements Parcelable {
}
private BrightnessChangeEvent(Parcel source) {
- brightness = source.readInt();
+ brightness = source.readFloat();
timeStamp = source.readLong();
packageName = source.readString();
userId = source.readInt();
@@ -87,7 +103,7 @@ public final class BrightnessChangeEvent implements Parcelable {
batteryLevel = source.readFloat();
nightMode = source.readBoolean();
colorTemperature = source.readInt();
- lastBrightness = source.readInt();
+ lastBrightness = source.readFloat();
}
public static final Creator<BrightnessChangeEvent> CREATOR =
@@ -107,7 +123,7 @@ public final class BrightnessChangeEvent implements Parcelable {
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(brightness);
+ dest.writeFloat(brightness);
dest.writeLong(timeStamp);
dest.writeString(packageName);
dest.writeInt(userId);
@@ -116,6 +132,87 @@ public final class BrightnessChangeEvent implements Parcelable {
dest.writeFloat(batteryLevel);
dest.writeBoolean(nightMode);
dest.writeInt(colorTemperature);
- dest.writeInt(lastBrightness);
+ dest.writeFloat(lastBrightness);
+ }
+
+ /** @hide */
+ public static class Builder {
+ private float mBrightness;
+ private long mTimeStamp;
+ private String mPackageName;
+ private int mUserId;
+ private float[] mLuxValues;
+ private long[] mLuxTimestamps;
+ private float mBatteryLevel;
+ private boolean mNightMode;
+ private int mColorTemperature;
+ private float mLastBrightness;
+
+ /** {@see BrightnessChangeEvent#brightness} */
+ public Builder setBrightness(float brightness) {
+ mBrightness = brightness;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#timeStamp} */
+ public Builder setTimeStamp(long timeStamp) {
+ mTimeStamp = timeStamp;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#packageName} */
+ public Builder setPackageName(String packageName) {
+ mPackageName = packageName;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#userId} */
+ public Builder setUserId(int userId) {
+ mUserId = userId;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#luxValues} */
+ public Builder setLuxValues(float[] luxValues) {
+ mLuxValues = luxValues;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#luxTimestamps} */
+ public Builder setLuxTimestamps(long[] luxTimestamps) {
+ mLuxTimestamps = luxTimestamps;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#batteryLevel} */
+ public Builder setBatteryLevel(float batteryLevel) {
+ mBatteryLevel = batteryLevel;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#nightMode} */
+ public Builder setNightMode(boolean nightMode) {
+ mNightMode = nightMode;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#colorTemperature} */
+ public Builder setColorTemperature(int colorTemperature) {
+ mColorTemperature = colorTemperature;
+ return this;
+ }
+
+ /** {@see BrightnessChangeEvent#lastBrightness} */
+ public Builder setLastBrightness(float lastBrightness) {
+ mLastBrightness = lastBrightness;
+ return this;
+ }
+
+ /** Builds a BrightnessChangeEvent */
+ public BrightnessChangeEvent build() {
+ return new BrightnessChangeEvent(mBrightness, mTimeStamp,
+ mPackageName, mUserId, mLuxValues, mLuxTimestamps, mBatteryLevel,
+ mNightMode, mColorTemperature, mLastBrightness);
+ }
}
}
diff --git a/android/hardware/display/BrightnessConfiguration.java b/android/hardware/display/BrightnessConfiguration.java
index 6c3be816..67e97bfd 100644
--- a/android/hardware/display/BrightnessConfiguration.java
+++ b/android/hardware/display/BrightnessConfiguration.java
@@ -16,6 +16,9 @@
package android.hardware.display;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Pair;
@@ -23,15 +26,20 @@ import android.util.Pair;
import com.android.internal.util.Preconditions;
import java.util.Arrays;
+import java.util.Objects;
/** @hide */
+@SystemApi
+@TestApi
public final class BrightnessConfiguration implements Parcelable {
private final float[] mLux;
private final float[] mNits;
+ private final String mDescription;
- private BrightnessConfiguration(float[] lux, float[] nits) {
+ private BrightnessConfiguration(float[] lux, float[] nits, String description) {
mLux = lux;
mNits = nits;
+ mDescription = description;
}
/**
@@ -47,10 +55,19 @@ public final class BrightnessConfiguration implements Parcelable {
return Pair.create(Arrays.copyOf(mLux, mLux.length), Arrays.copyOf(mNits, mNits.length));
}
+ /**
+ * Returns description string.
+ * @hide
+ */
+ public String getDescription() {
+ return mDescription;
+ }
+
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeFloatArray(mLux);
dest.writeFloatArray(mNits);
+ dest.writeString(mDescription);
}
@Override
@@ -68,7 +85,9 @@ public final class BrightnessConfiguration implements Parcelable {
}
sb.append("(").append(mLux[i]).append(", ").append(mNits[i]).append(")");
}
- sb.append("]}");
+ sb.append("], '");
+ sb.append(mDescription);
+ sb.append("'}");
return sb.toString();
}
@@ -77,6 +96,7 @@ public final class BrightnessConfiguration implements Parcelable {
int result = 1;
result = result * 31 + Arrays.hashCode(mLux);
result = result * 31 + Arrays.hashCode(mNits);
+ result = result * 31 + mDescription.hashCode();
return result;
}
@@ -89,16 +109,17 @@ public final class BrightnessConfiguration implements Parcelable {
return false;
}
final BrightnessConfiguration other = (BrightnessConfiguration) o;
- return Arrays.equals(mLux, other.mLux) && Arrays.equals(mNits, other.mNits);
+ return Arrays.equals(mLux, other.mLux) && Arrays.equals(mNits, other.mNits)
+ && Objects.equals(mDescription, other.mDescription);
}
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);
+ Builder builder = new Builder(lux, nits);
+ builder.setDescription(in.readString());
return builder.build();
}
@@ -113,6 +134,29 @@ public final class BrightnessConfiguration implements Parcelable {
public static class Builder {
private float[] mCurveLux;
private float[] mCurveNits;
+ private String mDescription;
+
+ /**
+ * STOPSHIP remove when app has stopped using this.
+ * @hide
+ */
+ public Builder() {
+ }
+
+ /**
+ * Constructs the builder with 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(float[] lux, float[] nits) {
+ setCurve(lux, nits);
+ }
/**
* Sets the control points for the brightness curve.
@@ -124,6 +168,9 @@ public final class BrightnessConfiguration implements Parcelable {
* @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.
+ *
+ * STOPSHIP remove when app has stopped using this.
+ * @hide
*/
public Builder setCurve(float[] lux, float[] nits) {
Preconditions.checkNotNull(lux);
@@ -147,6 +194,17 @@ public final class BrightnessConfiguration implements Parcelable {
}
/**
+ * Set description of the brightness curve.
+ *
+ * @param description brief text describing the curve pushed. It maybe truncated
+ * and will not be displayed in the UI
+ */
+ public Builder setDescription(@Nullable String description) {
+ mDescription = description;
+ return this;
+ }
+
+ /**
* Builds the {@link BrightnessConfiguration}.
*
* A brightness curve <b>must</b> be set before calling this.
@@ -155,7 +213,7 @@ public final class BrightnessConfiguration implements Parcelable {
if (mCurveLux == null || mCurveNits == null) {
throw new IllegalStateException("A curve must be set!");
}
- return new BrightnessConfiguration(mCurveLux, mCurveNits);
+ return new BrightnessConfiguration(mCurveLux, mCurveNits, mDescription);
}
private static void checkMonotonic(float[] vals, boolean strictlyIncreasing, String name) {
diff --git a/android/hardware/display/DisplayManager.java b/android/hardware/display/DisplayManager.java
index 7de667dc..4de4880b 100644
--- a/android/hardware/display/DisplayManager.java
+++ b/android/hardware/display/DisplayManager.java
@@ -22,6 +22,7 @@ import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.annotation.TestApi;
import android.app.KeyguardManager;
import android.content.Context;
import android.graphics.Point;
@@ -622,25 +623,23 @@ public final class DisplayManager {
* Fetch {@link BrightnessChangeEvent}s.
* @hide until we make it a system api.
*/
+ @SystemApi
+ @TestApi
@RequiresPermission(Manifest.permission.BRIGHTNESS_SLIDER_USAGE)
public List<BrightnessChangeEvent> getBrightnessEvents() {
return mGlobal.getBrightnessEvents(mContext.getOpPackageName());
}
/**
- * @hide STOPSHIP - remove when adaptive brightness accepts curves.
- */
- public void setBrightness(int brightness) {
- mGlobal.setBrightness(brightness);
- }
-
- /**
* Sets the global display brightness configuration.
*
* @hide
*/
+ @SystemApi
+ @TestApi
+ @RequiresPermission(Manifest.permission.CONFIGURE_DISPLAY_BRIGHTNESS)
public void setBrightnessConfiguration(BrightnessConfiguration c) {
- setBrightnessConfigurationForUser(c, UserHandle.myUserId());
+ setBrightnessConfigurationForUser(c, UserHandle.myUserId(), mContext.getPackageName());
}
/**
@@ -651,8 +650,37 @@ public final class DisplayManager {
*
* @hide
*/
- public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId) {
- mGlobal.setBrightnessConfigurationForUser(c, userId);
+ public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId,
+ String packageName) {
+ mGlobal.setBrightnessConfigurationForUser(c, userId, packageName);
+ }
+
+ /**
+ * Temporarily sets the brightness of the display.
+ * <p>
+ * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS} permission.
+ * </p>
+ *
+ * @param brightness The brightness value from 0 to 255.
+ *
+ * @hide Requires signature permission.
+ */
+ public void setTemporaryBrightness(int brightness) {
+ mGlobal.setTemporaryBrightness(brightness);
+ }
+
+ /**
+ * Temporarily sets the auto brightness adjustment factor.
+ * <p>
+ * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS} permission.
+ * </p>
+ *
+ * @param adjustment The adjustment factor from -1.0 to 1.0.
+ *
+ * @hide Requires signature permission.
+ */
+ public void setTemporaryAutoBrightnessAdjustment(float adjustment) {
+ mGlobal.setTemporaryAutoBrightnessAdjustment(adjustment);
}
/**
diff --git a/android/hardware/display/DisplayManagerGlobal.java b/android/hardware/display/DisplayManagerGlobal.java
index bf4cc1d8..2d5f5e04 100644
--- a/android/hardware/display/DisplayManagerGlobal.java
+++ b/android/hardware/display/DisplayManagerGlobal.java
@@ -476,25 +476,50 @@ public final class DisplayManagerGlobal {
}
/**
- * Set brightness but don't add a BrightnessChangeEvent
- * STOPSHIP remove when adaptive brightness accepts curves.
+ * Sets the global brightness configuration for a given user.
+ *
+ * @hide
*/
- public void setBrightness(int brightness) {
+ public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId,
+ String packageName) {
try {
- mDm.setBrightness(brightness);
+ mDm.setBrightnessConfigurationForUser(c, userId, packageName);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
}
/**
- * Sets the global brightness configuration for a given user.
+ * Temporarily sets the brightness of the display.
+ * <p>
+ * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS} permission.
+ * </p>
*
- * @hide
+ * @param brightness The brightness value from 0 to 255.
+ *
+ * @hide Requires signature permission.
+ */
+ public void setTemporaryBrightness(int brightness) {
+ try {
+ mDm.setTemporaryBrightness(brightness);
+ } catch (RemoteException ex) {
+ throw ex.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Temporarily sets the auto brightness adjustment factor.
+ * <p>
+ * Requires the {@link android.Manifest.permission#CONTROL_DISPLAY_BRIGHTNESS} permission.
+ * </p>
+ *
+ * @param adjustment The adjustment factor from -1.0 to 1.0.
+ *
+ * @hide Requires signature permission.
*/
- public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userId) {
+ public void setTemporaryAutoBrightnessAdjustment(float adjustment) {
try {
- mDm.setBrightnessConfigurationForUser(c, userId);
+ mDm.setTemporaryAutoBrightnessAdjustment(adjustment);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
diff --git a/android/hardware/display/DisplayManagerInternal.java b/android/hardware/display/DisplayManagerInternal.java
index cd551bd4..1cfad4f0 100644
--- a/android/hardware/display/DisplayManagerInternal.java
+++ b/android/hardware/display/DisplayManagerInternal.java
@@ -179,6 +179,11 @@ public abstract class DisplayManagerInternal {
public abstract void persistBrightnessSliderEvents();
/**
+ * Notifies the display manager that resource overlays have changed.
+ */
+ public abstract void onOverlayChanged();
+
+ /**
* Describes the requested power state of the display.
*
* This object is intended to describe the general characteristics of the
@@ -209,18 +214,12 @@ public abstract class DisplayManagerInternal {
// nearby, turning it off temporarily until the object is moved away.
public boolean useProximitySensor;
- // The desired screen brightness in the range 0 (minimum / off) to 255 (brightest).
- // The display power controller may choose to clamp the brightness.
- // When auto-brightness is enabled, this field should specify a nominal default
- // value to use while waiting for the light sensor to report enough data.
- public int screenBrightness;
-
- // The screen auto-brightness adjustment factor in the range -1 (dimmer) to 1 (brighter).
- public float screenAutoBrightnessAdjustment;
+ // An override of the screen brightness. Set to -1 is used if there's no override.
+ public int screenBrightnessOverride;
- // Set to true if screenBrightness and screenAutoBrightnessAdjustment were both
- // set by the user as opposed to being programmatically controlled by apps.
- public boolean brightnessSetByUser;
+ // An override of the screen auto-brightness adjustment factor in the range -1 (dimmer) to
+ // 1 (brighter). Set to Float.NaN if there's no override.
+ public float screenAutoBrightnessAdjustmentOverride;
// If true, enables automatic brightness control.
public boolean useAutoBrightness;
@@ -252,10 +251,10 @@ public abstract class DisplayManagerInternal {
public DisplayPowerRequest() {
policy = POLICY_BRIGHT;
useProximitySensor = false;
- screenBrightness = PowerManager.BRIGHTNESS_ON;
- screenAutoBrightnessAdjustment = 0.0f;
- screenLowPowerBrightnessFactor = 0.5f;
+ screenBrightnessOverride = -1;
useAutoBrightness = false;
+ screenAutoBrightnessAdjustmentOverride = Float.NaN;
+ screenLowPowerBrightnessFactor = 0.5f;
blockScreenOn = false;
dozeScreenBrightness = PowerManager.BRIGHTNESS_DEFAULT;
dozeScreenState = Display.STATE_UNKNOWN;
@@ -276,11 +275,10 @@ public abstract class DisplayManagerInternal {
public void copyFrom(DisplayPowerRequest other) {
policy = other.policy;
useProximitySensor = other.useProximitySensor;
- screenBrightness = other.screenBrightness;
- screenAutoBrightnessAdjustment = other.screenAutoBrightnessAdjustment;
- screenLowPowerBrightnessFactor = other.screenLowPowerBrightnessFactor;
- brightnessSetByUser = other.brightnessSetByUser;
+ screenBrightnessOverride = other.screenBrightnessOverride;
useAutoBrightness = other.useAutoBrightness;
+ screenAutoBrightnessAdjustmentOverride = other.screenAutoBrightnessAdjustmentOverride;
+ screenLowPowerBrightnessFactor = other.screenLowPowerBrightnessFactor;
blockScreenOn = other.blockScreenOn;
lowPowerMode = other.lowPowerMode;
boostScreenBrightness = other.boostScreenBrightness;
@@ -298,12 +296,12 @@ public abstract class DisplayManagerInternal {
return other != null
&& policy == other.policy
&& useProximitySensor == other.useProximitySensor
- && screenBrightness == other.screenBrightness
- && screenAutoBrightnessAdjustment == other.screenAutoBrightnessAdjustment
+ && screenBrightnessOverride == other.screenBrightnessOverride
+ && useAutoBrightness == other.useAutoBrightness
+ && floatEquals(screenAutoBrightnessAdjustmentOverride,
+ other.screenAutoBrightnessAdjustmentOverride)
&& screenLowPowerBrightnessFactor
== other.screenLowPowerBrightnessFactor
- && brightnessSetByUser == other.brightnessSetByUser
- && useAutoBrightness == other.useAutoBrightness
&& blockScreenOn == other.blockScreenOn
&& lowPowerMode == other.lowPowerMode
&& boostScreenBrightness == other.boostScreenBrightness
@@ -311,6 +309,10 @@ public abstract class DisplayManagerInternal {
&& dozeScreenState == other.dozeScreenState;
}
+ private boolean floatEquals(float f1, float f2) {
+ return f1 == f2 || Float.isNaN(f1) && Float.isNaN(f2);
+ }
+
@Override
public int hashCode() {
return 0; // don't care
@@ -320,11 +322,11 @@ public abstract class DisplayManagerInternal {
public String toString() {
return "policy=" + policyToString(policy)
+ ", useProximitySensor=" + useProximitySensor
- + ", screenBrightness=" + screenBrightness
- + ", screenAutoBrightnessAdjustment=" + screenAutoBrightnessAdjustment
- + ", screenLowPowerBrightnessFactor=" + screenLowPowerBrightnessFactor
- + ", brightnessSetByUser=" + brightnessSetByUser
+ + ", screenBrightnessOverride=" + screenBrightnessOverride
+ ", useAutoBrightness=" + useAutoBrightness
+ + ", screenAutoBrightnessAdjustmentOverride="
+ + screenAutoBrightnessAdjustmentOverride
+ + ", screenLowPowerBrightnessFactor=" + screenLowPowerBrightnessFactor
+ ", blockScreenOn=" + blockScreenOn
+ ", lowPowerMode=" + lowPowerMode
+ ", boostScreenBrightness=" + boostScreenBrightness
diff --git a/android/hardware/fingerprint/FingerprintDialog.java b/android/hardware/fingerprint/FingerprintDialog.java
new file mode 100644
index 00000000..6b7fab77
--- /dev/null
+++ b/android/hardware/fingerprint/FingerprintDialog.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.fingerprint;
+
+import static android.Manifest.permission.USE_FINGERPRINT;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback;
+import android.hardware.fingerprint.FingerprintManager.AuthenticationResult;
+import android.hardware.fingerprint.IFingerprintDialogReceiver;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.text.TextUtils;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A class that manages a system-provided fingerprint dialog.
+ */
+public class FingerprintDialog {
+
+ /**
+ * @hide
+ */
+ public static final String KEY_TITLE = "title";
+ /**
+ * @hide
+ */
+ public static final String KEY_SUBTITLE = "subtitle";
+ /**
+ * @hide
+ */
+ public static final String KEY_DESCRIPTION = "description";
+ /**
+ * @hide
+ */
+ public static final String KEY_POSITIVE_TEXT = "positive_text";
+ /**
+ * @hide
+ */
+ public static final String KEY_NEGATIVE_TEXT = "negative_text";
+
+ /**
+ * Error/help message will show for this amount of time.
+ * For error messages, the dialog will also be dismissed after this amount of time.
+ * Error messages will be propagated back to the application via AuthenticationCallback
+ * after this amount of time.
+ * @hide
+ */
+ public static final int HIDE_DIALOG_DELAY = 3000; // ms
+ /**
+ * @hide
+ */
+ public static final int DISMISSED_REASON_POSITIVE = 1;
+
+ /**
+ * @hide
+ */
+ public static final int DISMISSED_REASON_NEGATIVE = 2;
+
+ /**
+ * @hide
+ */
+ public static final int DISMISSED_REASON_USER_CANCEL = 3;
+
+ private static class ButtonInfo {
+ Executor executor;
+ DialogInterface.OnClickListener listener;
+ ButtonInfo(Executor ex, DialogInterface.OnClickListener l) {
+ executor = ex;
+ listener = l;
+ }
+ }
+
+ /**
+ * A builder that collects arguments, to be shown on the system-provided fingerprint dialog.
+ **/
+ public static class Builder {
+ private final Bundle bundle;
+ private ButtonInfo positiveButtonInfo;
+ private ButtonInfo negativeButtonInfo;
+
+ /**
+ * Creates a builder for a fingerprint dialog.
+ */
+ public Builder() {
+ bundle = new Bundle();
+ }
+
+ /**
+ * Required: Set the title to display.
+ * @param title
+ * @return
+ */
+ public Builder setTitle(@NonNull CharSequence title) {
+ bundle.putCharSequence(KEY_TITLE, title);
+ return this;
+ }
+
+ /**
+ * Optional: Set the subtitle to display.
+ * @param subtitle
+ * @return
+ */
+ public Builder setSubtitle(@NonNull CharSequence subtitle) {
+ bundle.putCharSequence(KEY_SUBTITLE, subtitle);
+ return this;
+ }
+
+ /**
+ * Optional: Set the description to display.
+ * @param description
+ * @return
+ */
+ public Builder setDescription(@NonNull CharSequence description) {
+ bundle.putCharSequence(KEY_DESCRIPTION, description);
+ return this;
+ }
+
+ /**
+ * Optional: Set the text for the positive button. If not set, the positive button
+ * will not show.
+ * @param text
+ * @return
+ * @hide
+ */
+ public Builder setPositiveButton(@NonNull CharSequence text,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull DialogInterface.OnClickListener listener) {
+ if (TextUtils.isEmpty(text)) {
+ throw new IllegalArgumentException("Text must be set and non-empty");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Executor must not be null");
+ }
+ if (listener == null) {
+ throw new IllegalArgumentException("Listener must not be null");
+ }
+ bundle.putCharSequence(KEY_POSITIVE_TEXT, text);
+ positiveButtonInfo = new ButtonInfo(executor, listener);
+ return this;
+ }
+
+ /**
+ * Required: Set the text for the negative button.
+ * @param text
+ * @return
+ */
+ public Builder setNegativeButton(@NonNull CharSequence text,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull DialogInterface.OnClickListener listener) {
+ if (TextUtils.isEmpty(text)) {
+ throw new IllegalArgumentException("Text must be set and non-empty");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Executor must not be null");
+ }
+ if (listener == null) {
+ throw new IllegalArgumentException("Listener must not be null");
+ }
+ bundle.putCharSequence(KEY_NEGATIVE_TEXT, text);
+ negativeButtonInfo = new ButtonInfo(executor, listener);
+ return this;
+ }
+
+ /**
+ * Creates a {@link FingerprintDialog} with the arguments supplied to this builder.
+ * @param context
+ * @return a {@link FingerprintDialog}
+ * @throws IllegalArgumentException if any of the required fields are not set.
+ */
+ public FingerprintDialog build(Context context) {
+ final CharSequence title = bundle.getCharSequence(KEY_TITLE);
+ final CharSequence negative = bundle.getCharSequence(KEY_NEGATIVE_TEXT);
+
+ if (TextUtils.isEmpty(title)) {
+ throw new IllegalArgumentException("Title must be set and non-empty");
+ } else if (TextUtils.isEmpty(negative)) {
+ throw new IllegalArgumentException("Negative text must be set and non-empty");
+ }
+ return new FingerprintDialog(context, bundle, positiveButtonInfo, negativeButtonInfo);
+ }
+ }
+
+ private FingerprintManager mFingerprintManager;
+ private Bundle mBundle;
+ private ButtonInfo mPositiveButtonInfo;
+ private ButtonInfo mNegativeButtonInfo;
+
+ IFingerprintDialogReceiver mDialogReceiver = new IFingerprintDialogReceiver.Stub() {
+ @Override
+ public void onDialogDismissed(int reason) {
+ // Check the reason and invoke OnClickListener(s) if necessary
+ if (reason == DISMISSED_REASON_POSITIVE) {
+ mPositiveButtonInfo.executor.execute(() -> {
+ mPositiveButtonInfo.listener.onClick(null, DialogInterface.BUTTON_POSITIVE);
+ });
+ } else if (reason == DISMISSED_REASON_NEGATIVE) {
+ mNegativeButtonInfo.executor.execute(() -> {
+ mNegativeButtonInfo.listener.onClick(null, DialogInterface.BUTTON_NEGATIVE);
+ });
+ }
+ }
+ };
+
+ private FingerprintDialog(Context context, Bundle bundle,
+ ButtonInfo positiveButtonInfo, ButtonInfo negativeButtonInfo) {
+ mBundle = bundle;
+ mPositiveButtonInfo = positiveButtonInfo;
+ mNegativeButtonInfo = negativeButtonInfo;
+ mFingerprintManager = context.getSystemService(FingerprintManager.class);
+ }
+
+ /**
+ * This call warms up the fingerprint hardware, displays a system-provided dialog,
+ * and starts scanning for a fingerprint. It terminates when
+ * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} is called, when
+ * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called,
+ * when {@link AuthenticationCallback#onAuthenticationFailed()} is called or when the user
+ * dismisses the system-provided dialog, at which point the crypto object becomes invalid.
+ * This operation can be canceled by using the provided cancel object. The application will
+ * receive authentication errors through {@link AuthenticationCallback}, and button events
+ * through the corresponding callback set in
+ * {@link Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}.
+ * It is safe to reuse the {@link FingerprintDialog} object, and calling
+ * {@link FingerprintDialog#authenticate(CancellationSignal, Executor, AuthenticationCallback)}
+ * while an existing authentication attempt is occurring will stop the previous client and
+ * start a new authentication. The interrupted client will receive a cancelled notification
+ * through {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)}.
+ *
+ * @throws IllegalArgumentException if any of the arguments are null
+ *
+ * @param crypto object associated with the call
+ * @param cancel an object that can be used to cancel authentication
+ * @param executor an executor to handle callback events
+ * @param callback an object to receive authentication events
+ */
+ @RequiresPermission(USE_FINGERPRINT)
+ public void authenticate(@NonNull FingerprintManager.CryptoObject crypto,
+ @NonNull CancellationSignal cancel,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull FingerprintManager.AuthenticationCallback callback) {
+ mFingerprintManager.authenticate(crypto, cancel, mBundle, executor, mDialogReceiver,
+ callback);
+ }
+
+ /**
+ * This call warms up the fingerprint hardware, displays a system-provided dialog,
+ * and starts scanning for a fingerprint. It terminates when
+ * {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)} is called, when
+ * {@link AuthenticationCallback#onAuthenticationSucceeded(AuthenticationResult)} is called,
+ * when {@link AuthenticationCallback#onAuthenticationFailed()} is called or when the user
+ * dismisses the system-provided dialog. This operation can be canceled by using the provided
+ * cancel object. The application will receive authentication errors through
+ * {@link AuthenticationCallback}, and button events through the corresponding callback set in
+ * {@link Builder#setNegativeButton(CharSequence, Executor, DialogInterface.OnClickListener)}.
+ * It is safe to reuse the {@link FingerprintDialog} object, and calling
+ * {@link FingerprintDialog#authenticate(CancellationSignal, Executor, AuthenticationCallback)}
+ * while an existing authentication attempt is occurring will stop the previous client and
+ * start a new authentication. The interrupted client will receive a cancelled notification
+ * through {@link AuthenticationCallback#onAuthenticationError(int, CharSequence)}.
+ *
+ * @throws IllegalArgumentException if any of the arguments are null
+ *
+ * @param cancel an object that can be used to cancel authentication
+ * @param executor an executor to handle callback events
+ * @param callback an object to receive authentication events
+ */
+ @RequiresPermission(USE_FINGERPRINT)
+ public void authenticate(@NonNull CancellationSignal cancel,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull FingerprintManager.AuthenticationCallback callback) {
+ mFingerprintManager.authenticate(cancel, mBundle, executor, mDialogReceiver, callback);
+ }
+}
diff --git a/android/hardware/fingerprint/FingerprintManager.java b/android/hardware/fingerprint/FingerprintManager.java
index 987718a8..62d92c4a 100644
--- a/android/hardware/fingerprint/FingerprintManager.java
+++ b/android/hardware/fingerprint/FingerprintManager.java
@@ -16,6 +16,11 @@
package android.hardware.fingerprint;
+import static android.Manifest.permission.INTERACT_ACROSS_USERS;
+import static android.Manifest.permission.MANAGE_FINGERPRINT;
+import static android.Manifest.permission.USE_FINGERPRINT;
+
+import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -23,6 +28,7 @@ import android.annotation.SystemService;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Binder;
+import android.os.Bundle;
import android.os.CancellationSignal;
import android.os.CancellationSignal.OnCancelListener;
import android.os.Handler;
@@ -38,14 +44,11 @@ import android.util.Slog;
import java.security.Signature;
import java.util.List;
+import java.util.concurrent.Executor;
import javax.crypto.Cipher;
import javax.crypto.Mac;
-import static android.Manifest.permission.INTERACT_ACROSS_USERS;
-import static android.Manifest.permission.MANAGE_FINGERPRINT;
-import static android.Manifest.permission.USE_FINGERPRINT;
-
/**
* A class that coordinates access to the fingerprint hardware.
*/
@@ -204,6 +207,7 @@ public class FingerprintManager {
private CryptoObject mCryptoObject;
private Fingerprint mRemovalFingerprint;
private Handler mHandler;
+ private Executor mExecutor;
private class OnEnrollCancelListener implements OnCancelListener {
@Override
@@ -505,7 +509,9 @@ public class FingerprintManager {
}
/**
- * Per-user version
+ * Per-user version, see {@link FingerprintManager#authenticate(CryptoObject,
+ * CancellationSignal, int, AuthenticationCallback, Handler)}
+ * @param userId the user ID that the fingerprint hardware will authenticate for.
* @hide
*/
@RequiresPermission(USE_FINGERPRINT)
@@ -530,7 +536,7 @@ public class FingerprintManager {
mCryptoObject = crypto;
long sessionId = crypto != null ? crypto.getOpId() : 0;
mService.authenticate(mToken, sessionId, userId, mServiceReceiver, flags,
- mContext.getOpPackageName());
+ mContext.getOpPackageName(), null /* bundle */, null /* receiver */);
} catch (RemoteException e) {
Log.w(TAG, "Remote exception while authenticating: ", e);
if (callback != null) {
@@ -543,6 +549,111 @@ public class FingerprintManager {
}
/**
+ * Per-user version, see {@link FingerprintManager#authenticate(CryptoObject,
+ * CancellationSignal, Bundle, Executor, IFingerprintDialogReceiver, AuthenticationCallback)}
+ * @param userId the user ID that the fingerprint hardware will authenticate for.
+ */
+ private void authenticate(int userId,
+ @Nullable CryptoObject crypto,
+ @NonNull CancellationSignal cancel,
+ @NonNull Bundle bundle,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull IFingerprintDialogReceiver receiver,
+ @NonNull AuthenticationCallback callback) {
+ mCryptoObject = crypto;
+ if (cancel.isCanceled()) {
+ Log.w(TAG, "authentication already canceled");
+ return;
+ } else {
+ cancel.setOnCancelListener(new OnAuthenticationCancelListener(crypto));
+ }
+
+ if (mService != null) {
+ try {
+ mExecutor = executor;
+ mAuthenticationCallback = callback;
+ final long sessionId = crypto != null ? crypto.getOpId() : 0;
+ mService.authenticate(mToken, sessionId, userId, mServiceReceiver,
+ 0 /* flags */, mContext.getOpPackageName(), bundle, receiver);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Remote exception while authenticating", e);
+ mExecutor.execute(() -> {
+ callback.onAuthenticationError(FINGERPRINT_ERROR_HW_UNAVAILABLE,
+ getErrorString(FINGERPRINT_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */));
+ });
+ }
+ }
+ }
+
+ /**
+ * Private method, see {@link FingerprintDialog#authenticate(CancellationSignal, Executor,
+ * AuthenticationCallback)}
+ * @param cancel
+ * @param executor
+ * @param callback
+ * @hide
+ */
+ public void authenticate(
+ @NonNull CancellationSignal cancel,
+ @NonNull Bundle bundle,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull IFingerprintDialogReceiver receiver,
+ @NonNull AuthenticationCallback callback) {
+ if (cancel == null) {
+ throw new IllegalArgumentException("Must supply a cancellation signal");
+ }
+ if (bundle == null) {
+ throw new IllegalArgumentException("Must supply a bundle");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Must supply an executor");
+ }
+ if (receiver == null) {
+ throw new IllegalArgumentException("Must supply a receiver");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("Must supply a calback");
+ }
+ authenticate(UserHandle.myUserId(), null, cancel, bundle, executor, receiver, callback);
+ }
+
+ /**
+ * Private method, see {@link FingerprintDialog#authenticate(CryptoObject, CancellationSignal,
+ * Executor, AuthenticationCallback)}
+ * @param crypto
+ * @param cancel
+ * @param executor
+ * @param callback
+ * @hide
+ */
+ public void authenticate(@NonNull CryptoObject crypto,
+ @NonNull CancellationSignal cancel,
+ @NonNull Bundle bundle,
+ @NonNull @CallbackExecutor Executor executor,
+ @NonNull IFingerprintDialogReceiver receiver,
+ @NonNull AuthenticationCallback callback) {
+ if (crypto == null) {
+ throw new IllegalArgumentException("Must supply a crypto object");
+ }
+ if (cancel == null) {
+ throw new IllegalArgumentException("Must supply a cancellation signal");
+ }
+ if (bundle == null) {
+ throw new IllegalArgumentException("Must supply a bundle");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Must supply an executor");
+ }
+ if (receiver == null) {
+ throw new IllegalArgumentException("Must supply a receiver");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("Must supply a calback");
+ }
+ authenticate(UserHandle.myUserId(), crypto, cancel, bundle, executor, receiver, callback);
+ }
+
+ /**
* Request fingerprint enrollment. This call warms up the fingerprint hardware
* and starts scanning for fingerprints. Progress will be indicated by callbacks to the
* {@link EnrollmentCallback} object. It terminates when
@@ -929,63 +1040,63 @@ public class FingerprintManager {
}
}
- private void sendErrorResult(long deviceId, int errMsgId, int vendorCode) {
- // emulate HAL 2.1 behavior and send real errMsgId
- final int clientErrMsgId = errMsgId == FINGERPRINT_ERROR_VENDOR
- ? (vendorCode + FINGERPRINT_ERROR_VENDOR_BASE) : errMsgId;
- if (mEnrollmentCallback != null) {
- mEnrollmentCallback.onEnrollmentError(clientErrMsgId,
- getErrorString(errMsgId, vendorCode));
- } else if (mAuthenticationCallback != null) {
- mAuthenticationCallback.onAuthenticationError(clientErrMsgId,
- getErrorString(errMsgId, vendorCode));
- } else if (mRemovalCallback != null) {
- mRemovalCallback.onRemovalError(mRemovalFingerprint, clientErrMsgId,
- getErrorString(errMsgId, vendorCode));
- } else if (mEnumerateCallback != null) {
- mEnumerateCallback.onEnumerateError(clientErrMsgId,
- getErrorString(errMsgId, vendorCode));
- }
- }
-
private void sendEnrollResult(Fingerprint fp, int remaining) {
if (mEnrollmentCallback != null) {
mEnrollmentCallback.onEnrollmentProgress(remaining);
}
}
+ };
- private void sendAuthenticatedSucceeded(Fingerprint fp, int userId) {
- if (mAuthenticationCallback != null) {
- final AuthenticationResult result =
- new AuthenticationResult(mCryptoObject, fp, userId);
- mAuthenticationCallback.onAuthenticationSucceeded(result);
- }
+ private void sendAuthenticatedSucceeded(Fingerprint fp, int userId) {
+ if (mAuthenticationCallback != null) {
+ final AuthenticationResult result =
+ new AuthenticationResult(mCryptoObject, fp, userId);
+ mAuthenticationCallback.onAuthenticationSucceeded(result);
}
+ }
- private void sendAuthenticatedFailed() {
- if (mAuthenticationCallback != null) {
- mAuthenticationCallback.onAuthenticationFailed();
- }
+ private void sendAuthenticatedFailed() {
+ if (mAuthenticationCallback != null) {
+ mAuthenticationCallback.onAuthenticationFailed();
}
+ }
- private void sendAcquiredResult(long deviceId, int acquireInfo, int vendorCode) {
- if (mAuthenticationCallback != null) {
- mAuthenticationCallback.onAuthenticationAcquired(acquireInfo);
- }
- final String msg = getAcquiredString(acquireInfo, vendorCode);
- if (msg == null) {
- return;
- }
- // emulate HAL 2.1 behavior and send real acquiredInfo
- final int clientInfo = acquireInfo == FINGERPRINT_ACQUIRED_VENDOR
- ? (vendorCode + FINGERPRINT_ACQUIRED_VENDOR_BASE) : acquireInfo;
- if (mEnrollmentCallback != null) {
- mEnrollmentCallback.onEnrollmentHelp(clientInfo, msg);
- } else if (mAuthenticationCallback != null) {
- mAuthenticationCallback.onAuthenticationHelp(clientInfo, msg);
- }
+ private void sendAcquiredResult(long deviceId, int acquireInfo, int vendorCode) {
+ if (mAuthenticationCallback != null) {
+ mAuthenticationCallback.onAuthenticationAcquired(acquireInfo);
}
- };
+ final String msg = getAcquiredString(acquireInfo, vendorCode);
+ if (msg == null) {
+ return;
+ }
+ // emulate HAL 2.1 behavior and send real acquiredInfo
+ final int clientInfo = acquireInfo == FINGERPRINT_ACQUIRED_VENDOR
+ ? (vendorCode + FINGERPRINT_ACQUIRED_VENDOR_BASE) : acquireInfo;
+ if (mEnrollmentCallback != null) {
+ mEnrollmentCallback.onEnrollmentHelp(clientInfo, msg);
+ } else if (mAuthenticationCallback != null) {
+ mAuthenticationCallback.onAuthenticationHelp(clientInfo, msg);
+ }
+ }
+
+ private void sendErrorResult(long deviceId, int errMsgId, int vendorCode) {
+ // emulate HAL 2.1 behavior and send real errMsgId
+ final int clientErrMsgId = errMsgId == FINGERPRINT_ERROR_VENDOR
+ ? (vendorCode + FINGERPRINT_ERROR_VENDOR_BASE) : errMsgId;
+ if (mEnrollmentCallback != null) {
+ mEnrollmentCallback.onEnrollmentError(clientErrMsgId,
+ getErrorString(errMsgId, vendorCode));
+ } else if (mAuthenticationCallback != null) {
+ mAuthenticationCallback.onAuthenticationError(clientErrMsgId,
+ getErrorString(errMsgId, vendorCode));
+ } else if (mRemovalCallback != null) {
+ mRemovalCallback.onRemovalError(mRemovalFingerprint, clientErrMsgId,
+ getErrorString(errMsgId, vendorCode));
+ } else if (mEnumerateCallback != null) {
+ mEnumerateCallback.onEnumerateError(clientErrMsgId,
+ getErrorString(errMsgId, vendorCode));
+ }
+ }
/**
* @hide
@@ -1023,7 +1134,10 @@ public class FingerprintManager {
}
}
- private String getErrorString(int errMsg, int vendorCode) {
+ /**
+ * @hide
+ */
+ public String getErrorString(int errMsg, int vendorCode) {
switch (errMsg) {
case FINGERPRINT_ERROR_UNABLE_TO_PROCESS:
return mContext.getString(
@@ -1043,6 +1157,9 @@ public class FingerprintManager {
case FINGERPRINT_ERROR_LOCKOUT_PERMANENT:
return mContext.getString(
com.android.internal.R.string.fingerprint_error_lockout_permanent);
+ case FINGERPRINT_ERROR_USER_CANCELED:
+ return mContext.getString(
+ com.android.internal.R.string.fingerprint_error_user_canceled);
case FINGERPRINT_ERROR_VENDOR: {
String[] msgArray = mContext.getResources().getStringArray(
com.android.internal.R.array.fingerprint_error_vendor);
@@ -1055,7 +1172,10 @@ public class FingerprintManager {
return null;
}
- private String getAcquiredString(int acquireInfo, int vendorCode) {
+ /**
+ * @hide
+ */
+ public String getAcquiredString(int acquireInfo, int vendorCode) {
switch (acquireInfo) {
case FINGERPRINT_ACQUIRED_GOOD:
return null;
@@ -1096,22 +1216,47 @@ public class FingerprintManager {
@Override // binder call
public void onAcquired(long deviceId, int acquireInfo, int vendorCode) {
- mHandler.obtainMessage(MSG_ACQUIRED, acquireInfo, vendorCode, deviceId).sendToTarget();
+ if (mExecutor != null) {
+ mExecutor.execute(() -> {
+ sendAcquiredResult(deviceId, acquireInfo, vendorCode);
+ });
+ } else {
+ mHandler.obtainMessage(MSG_ACQUIRED, acquireInfo, vendorCode,
+ deviceId).sendToTarget();
+ }
}
@Override // binder call
public void onAuthenticationSucceeded(long deviceId, Fingerprint fp, int userId) {
- mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, userId, 0, fp).sendToTarget();
+ if (mExecutor != null) {
+ mExecutor.execute(() -> {
+ sendAuthenticatedSucceeded(fp, userId);
+ });
+ } else {
+ mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, userId, 0, fp).sendToTarget();
+ }
}
@Override // binder call
public void onAuthenticationFailed(long deviceId) {
- mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget();
+ if (mExecutor != null) {
+ mExecutor.execute(() -> {
+ sendAuthenticatedFailed();
+ });
+ } else {
+ mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget();
+ }
}
@Override // binder call
public void onError(long deviceId, int error, int vendorCode) {
- mHandler.obtainMessage(MSG_ERROR, error, vendorCode, deviceId).sendToTarget();
+ if (mExecutor != null) {
+ mExecutor.execute(() -> {
+ sendErrorResult(deviceId, error, vendorCode);
+ });
+ } else {
+ mHandler.obtainMessage(MSG_ERROR, error, vendorCode, deviceId).sendToTarget();
+ }
}
@Override // binder call
diff --git a/android/hardware/hdmi/HdmiTimerRecordSources.java b/android/hardware/hdmi/HdmiTimerRecordSources.java
index 6fe13ca8..d7c2e1b3 100644
--- a/android/hardware/hdmi/HdmiTimerRecordSources.java
+++ b/android/hardware/hdmi/HdmiTimerRecordSources.java
@@ -187,7 +187,6 @@ public class HdmiTimerRecordSources {
* Base class for time-related information.
* @hide
*/
- @SystemApi
/* package */ static class TimeUnit {
/* package */ final int mHour;
/* package */ final int mMinute;
diff --git a/android/hardware/location/ContextHubClient.java b/android/hardware/location/ContextHubClient.java
index 52527ed6..0a21083a 100644
--- a/android/hardware/location/ContextHubClient.java
+++ b/android/hardware/location/ContextHubClient.java
@@ -15,9 +15,13 @@
*/
package android.hardware.location;
+import android.annotation.NonNull;
import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
import android.os.RemoteException;
+import com.android.internal.util.Preconditions;
+
import dalvik.system.CloseGuard;
import java.io.Closeable;
@@ -31,16 +35,12 @@ import java.util.concurrent.atomic.AtomicBoolean;
*
* @hide
*/
+@SystemApi
public class ContextHubClient implements Closeable {
/*
* The proxy to the client interface at the service.
*/
- private final IContextHubClient mClientProxy;
-
- /*
- * The callback interface associated with this client.
- */
- private final IContextHubClientCallback mCallbackInterface;
+ private IContextHubClient mClientProxy = null;
/*
* The Context Hub that this client is attached to.
@@ -51,20 +51,33 @@ public class ContextHubClient implements Closeable {
private final AtomicBoolean mIsClosed = new AtomicBoolean(false);
- /* package */ ContextHubClient(
- IContextHubClient clientProxy, IContextHubClientCallback callback,
- ContextHubInfo hubInfo) {
- mClientProxy = clientProxy;
- mCallbackInterface = callback;
+ /* package */ ContextHubClient(ContextHubInfo hubInfo) {
mAttachedHub = hubInfo;
mCloseGuard.open("close");
}
/**
+ * Sets the proxy interface of the client at the service. This method should always be called
+ * by the ContextHubManager after the client is registered at the service, and should only be
+ * called once.
+ *
+ * @param clientProxy the proxy of the client at the service
+ */
+ /* package */ void setClientProxy(IContextHubClient clientProxy) {
+ Preconditions.checkNotNull(clientProxy, "IContextHubClient cannot be null");
+ if (mClientProxy != null) {
+ throw new IllegalStateException("Cannot change client proxy multiple times");
+ }
+
+ mClientProxy = clientProxy;
+ }
+
+ /**
* Returns the hub that this client is attached to.
*
* @return the ContextHubInfo of the attached hub
*/
+ @NonNull
public ContextHubInfo getAttachedHub() {
return mAttachedHub;
}
@@ -96,12 +109,16 @@ public class ContextHubClient implements Closeable {
*
* @return the result of sending the message defined as in ContextHubTransaction.Result
*
+ * @throws NullPointerException if NanoAppMessage is null
+ *
* @see NanoAppMessage
* @see ContextHubTransaction.Result
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
@ContextHubTransaction.Result
- public int sendMessageToNanoApp(NanoAppMessage message) {
+ public int sendMessageToNanoApp(@NonNull NanoAppMessage message) {
+ Preconditions.checkNotNull(message, "NanoAppMessage cannot be null");
+
try {
return mClientProxy.sendMessageToNanoApp(message);
} catch (RemoteException e) {
diff --git a/android/hardware/location/ContextHubClientCallback.java b/android/hardware/location/ContextHubClientCallback.java
index ab19d547..cc2fe65d 100644
--- a/android/hardware/location/ContextHubClientCallback.java
+++ b/android/hardware/location/ContextHubClientCallback.java
@@ -15,15 +15,20 @@
*/
package android.hardware.location;
+import android.annotation.SystemApi;
+
+import java.util.concurrent.Executor;
+
/**
* A class for {@link android.hardware.location.ContextHubClient ContextHubClient} to
* receive messages and life-cycle events from nanoapps in the Context Hub at which the client is
* attached to.
*
- * This callback is registered through the
- * {@link android.hardware.location.ContextHubManager#createClient() creation} of
- * {@link android.hardware.location.ContextHubClient ContextHubClient}. Callbacks are
- * invoked in the following ways:
+ * This callback is registered through the {@link
+ * android.hardware.location.ContextHubManager#createClient(
+ * ContextHubInfo, ContextHubClientCallback, Executor) creation} of
+ * {@link android.hardware.location.ContextHubClient ContextHubClient}. Callbacks are invoked in
+ * the following ways:
* 1) Messages from nanoapps delivered through onMessageFromNanoApp may either be broadcasted
* or targeted to a specific client.
* 2) Nanoapp or Context Hub events (the remaining callbacks) are broadcasted to all clients, and
@@ -31,6 +36,7 @@ package android.hardware.location;
*
* @hide
*/
+@SystemApi
public class ContextHubClientCallback {
/**
* Callback invoked when receiving a message from a nanoapp.
@@ -38,48 +44,56 @@ public class ContextHubClientCallback {
* The message contents of this callback may either be broadcasted or targeted to the
* client receiving the invocation.
*
+ * @param client the client that is associated with this callback
* @param message the message sent by the nanoapp
*/
- public void onMessageFromNanoApp(NanoAppMessage message) {}
+ public void onMessageFromNanoApp(ContextHubClient client, NanoAppMessage message) {}
/**
* Callback invoked when the attached Context Hub has reset.
+ *
+ * @param client the client that is associated with this callback
*/
- public void onHubReset() {}
+ public void onHubReset(ContextHubClient client) {}
/**
* Callback invoked when a nanoapp aborts at the attached Context Hub.
*
+ * @param client the client that is associated with this callback
* @param nanoAppId the ID of the nanoapp that had aborted
* @param abortCode the reason for nanoapp's abort, specific to each nanoapp
*/
- public void onNanoAppAborted(long nanoAppId, int abortCode) {}
+ public void onNanoAppAborted(ContextHubClient client, long nanoAppId, int abortCode) {}
/**
* Callback invoked when a nanoapp is loaded at the attached Context Hub.
*
+ * @param client the client that is associated with this callback
* @param nanoAppId the ID of the nanoapp that had been loaded
*/
- public void onNanoAppLoaded(long nanoAppId) {}
+ public void onNanoAppLoaded(ContextHubClient client, long nanoAppId) {}
/**
* Callback invoked when a nanoapp is unloaded from the attached Context Hub.
*
+ * @param client the client that is associated with this callback
* @param nanoAppId the ID of the nanoapp that had been unloaded
*/
- public void onNanoAppUnloaded(long nanoAppId) {}
+ public void onNanoAppUnloaded(ContextHubClient client, long nanoAppId) {}
/**
* Callback invoked when a nanoapp is enabled at the attached Context Hub.
*
+ * @param client the client that is associated with this callback
* @param nanoAppId the ID of the nanoapp that had been enabled
*/
- public void onNanoAppEnabled(long nanoAppId) {}
+ public void onNanoAppEnabled(ContextHubClient client, long nanoAppId) {}
/**
* Callback invoked when a nanoapp is disabled at the attached Context Hub.
*
+ * @param client the client that is associated with this callback
* @param nanoAppId the ID of the nanoapp that had been disabled
*/
- public void onNanoAppDisabled(long nanoAppId) {}
+ public void onNanoAppDisabled(ContextHubClient client, long nanoAppId) {}
}
diff --git a/android/hardware/location/ContextHubInfo.java b/android/hardware/location/ContextHubInfo.java
index c2b28001..36123e3d 100644
--- a/android/hardware/location/ContextHubInfo.java
+++ b/android/hardware/location/ContextHubInfo.java
@@ -221,9 +221,6 @@ public class ContextHubInfo implements Parcelable {
/**
* @return the CHRE platform ID as defined in chre/version.h
- *
- * TODO(b/67734082): Expose as public API
- * @hide
*/
public long getChrePlatformId() {
return mChrePlatformId;
@@ -231,9 +228,6 @@ public class ContextHubInfo implements Parcelable {
/**
* @return the CHRE API's major version as defined in chre/version.h
- *
- * TODO(b/67734082): Expose as public API
- * @hide
*/
public byte getChreApiMajorVersion() {
return mChreApiMajorVersion;
@@ -241,9 +235,6 @@ public class ContextHubInfo implements Parcelable {
/**
* @return the CHRE API's minor version as defined in chre/version.h
- *
- * TODO(b/67734082): Expose as public API
- * @hide
*/
public byte getChreApiMinorVersion() {
return mChreApiMinorVersion;
@@ -251,9 +242,6 @@ public class ContextHubInfo implements Parcelable {
/**
* @return the CHRE patch version as defined in chre/version.h
- *
- * TODO(b/67734082): Expose as public API
- * @hide
*/
public short getChrePatchVersion() {
return mChrePatchVersion;
diff --git a/android/hardware/location/ContextHubManager.java b/android/hardware/location/ContextHubManager.java
index 4cea0acd..de13c813 100644
--- a/android/hardware/location/ContextHubManager.java
+++ b/android/hardware/location/ContextHubManager.java
@@ -17,6 +17,7 @@ package android.hardware.location;
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
@@ -30,6 +31,8 @@ import android.os.ServiceManager;
import android.os.ServiceManager.ServiceNotFoundException;
import android.util.Log;
+import com.android.internal.util.Preconditions;
+
import java.util.List;
import java.util.concurrent.Executor;
@@ -59,7 +62,11 @@ public final class ContextHubManager {
/**
* An interface to receive asynchronous communication from the context hub.
+ *
+ * @deprecated Use the more refined {@link android.hardware.location.ContextHubClientCallback}
+ * instead for notification callbacks.
*/
+ @Deprecated
public abstract static class Callback {
protected Callback() {}
@@ -75,7 +82,7 @@ public final class ContextHubManager {
public abstract void onMessageReceipt(
int hubHandle,
int nanoAppHandle,
- ContextHubMessage message);
+ @NonNull ContextHubMessage message);
}
/**
@@ -98,8 +105,13 @@ public final class ContextHubManager {
/**
* Get a handle to all the context hubs in the system
+ *
* @return array of context hub handles
+ *
+ * @deprecated Use {@link #getContextHubs()} instead. The use of handles are deprecated in the
+ * new APIs.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
public int[] getContextHubHandles() {
try {
@@ -116,7 +128,11 @@ public final class ContextHubManager {
* @return ContextHubInfo Information about the requested context hub.
*
* @see ContextHubInfo
+ *
+ * @deprecated Use {@link #getContextHubs()} instead. The use of handles are deprecated in the
+ * new APIs.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
public ContextHubInfo getContextHubInfo(int hubHandle) {
try {
@@ -144,9 +160,12 @@ public final class ContextHubManager {
* -1 otherwise
*
* @see NanoApp
+ *
+ * @deprecated Use {@link #loadNanoApp(ContextHubInfo, NanoAppBinary)} instead.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public int loadNanoApp(int hubHandle, NanoApp app) {
+ public int loadNanoApp(int hubHandle, @NonNull NanoApp app) {
try {
return mService.loadNanoApp(hubHandle, app);
} catch (RemoteException e) {
@@ -168,7 +187,10 @@ public final class ContextHubManager {
*
* @return 0 if the command for unloading was sent to the context hub;
* -1 otherwise
+ *
+ * @deprecated Use {@link #unloadNanoApp(ContextHubInfo, long)} instead.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
public int unloadNanoApp(int nanoAppHandle) {
try {
@@ -199,13 +221,18 @@ public final class ContextHubManager {
* TODO(b/30943489): Have the returned NanoAppInstanceInfo contain the
* correct information.
*
- * @param nanoAppHandle handle of the nanoAppInstance
- * @return NanoAppInstanceInfo Information about the nano app instance.
+ * @param nanoAppHandle handle of the nanoapp instance
+ * @return NanoAppInstanceInfo the NanoAppInstanceInfo of the nanoapp, or null if the nanoapp
+ * does not exist
*
* @see NanoAppInstanceInfo
+ *
+ * @deprecated Use {@link #queryNanoApps(ContextHubInfo)} instead to explicitly query the hub
+ * for loaded nanoapps.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppHandle) {
+ @Nullable public NanoAppInstanceInfo getNanoAppInstanceInfo(int nanoAppHandle) {
try {
return mService.getNanoAppInstanceInfo(nanoAppHandle);
} catch (RemoteException e) {
@@ -222,9 +249,13 @@ public final class ContextHubManager {
* @see NanoAppFilter
*
* @return int[] Array of handles to any found nano apps
+ *
+ * @deprecated Use {@link #queryNanoApps(ContextHubInfo)} instead to explicitly query the hub
+ * for loaded nanoapps.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public int[] findNanoAppOnHub(int hubHandle, NanoAppFilter filter) {
+ @NonNull public int[] findNanoAppOnHub(int hubHandle, @NonNull NanoAppFilter filter) {
try {
return mService.findNanoAppOnHub(hubHandle, filter);
} catch (RemoteException e) {
@@ -250,9 +281,16 @@ public final class ContextHubManager {
* @see ContextHubMessage
*
* @return int 0 on success, -1 otherwise
+ *
+ * @deprecated Use {@link android.hardware.location.ContextHubClient#sendMessageToNanoApp(
+ * NanoAppMessage)} instead, after creating a
+ * {@link android.hardware.location.ContextHubClient} with
+ * {@link #createClient(ContextHubInfo, ContextHubClientCallback, Executor)}
+ * or {@link #createClient(ContextHubInfo, ContextHubClientCallback)}.
*/
+ @Deprecated
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public int sendMessage(int hubHandle, int nanoAppHandle, ContextHubMessage message) {
+ public int sendMessage(int hubHandle, int nanoAppHandle, @NonNull ContextHubMessage message) {
try {
return mService.sendMessage(hubHandle, nanoAppHandle, message);
} catch (RemoteException e) {
@@ -266,11 +304,9 @@ public final class ContextHubManager {
* @return the list of ContextHubInfo objects
*
* @see ContextHubInfo
- *
- * @hide
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public List<ContextHubInfo> getContextHubs() {
+ @NonNull public List<ContextHubInfo> getContextHubs() {
try {
return mService.getContextHubs();
} catch (RemoteException e) {
@@ -342,13 +378,16 @@ public final class ContextHubManager {
*
* @return the ContextHubTransaction of the request
*
- * @see NanoAppBinary
+ * @throws NullPointerException if hubInfo or NanoAppBinary is null
*
- * @hide
+ * @see NanoAppBinary
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public ContextHubTransaction<Void> loadNanoApp(
- ContextHubInfo hubInfo, NanoAppBinary appBinary) {
+ @NonNull public ContextHubTransaction<Void> loadNanoApp(
+ @NonNull ContextHubInfo hubInfo, @NonNull NanoAppBinary appBinary) {
+ Preconditions.checkNotNull(hubInfo, "ContextHubInfo cannot be null");
+ Preconditions.checkNotNull(appBinary, "NanoAppBinary cannot be null");
+
ContextHubTransaction<Void> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_LOAD_NANOAPP);
IContextHubTransactionCallback callback = createTransactionCallback(transaction);
@@ -370,10 +409,13 @@ public final class ContextHubManager {
*
* @return the ContextHubTransaction of the request
*
- * @hide
+ * @throws NullPointerException if hubInfo is null
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public ContextHubTransaction<Void> unloadNanoApp(ContextHubInfo hubInfo, long nanoAppId) {
+ @NonNull public ContextHubTransaction<Void> unloadNanoApp(
+ @NonNull ContextHubInfo hubInfo, long nanoAppId) {
+ Preconditions.checkNotNull(hubInfo, "ContextHubInfo cannot be null");
+
ContextHubTransaction<Void> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_UNLOAD_NANOAPP);
IContextHubTransactionCallback callback = createTransactionCallback(transaction);
@@ -395,10 +437,13 @@ public final class ContextHubManager {
*
* @return the ContextHubTransaction of the request
*
- * @hide
+ * @throws NullPointerException if hubInfo is null
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public ContextHubTransaction<Void> enableNanoApp(ContextHubInfo hubInfo, long nanoAppId) {
+ @NonNull public ContextHubTransaction<Void> enableNanoApp(
+ @NonNull ContextHubInfo hubInfo, long nanoAppId) {
+ Preconditions.checkNotNull(hubInfo, "ContextHubInfo cannot be null");
+
ContextHubTransaction<Void> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_ENABLE_NANOAPP);
IContextHubTransactionCallback callback = createTransactionCallback(transaction);
@@ -420,10 +465,13 @@ public final class ContextHubManager {
*
* @return the ContextHubTransaction of the request
*
- * @hide
+ * @throws NullPointerException if hubInfo is null
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public ContextHubTransaction<Void> disableNanoApp(ContextHubInfo hubInfo, long nanoAppId) {
+ @NonNull public ContextHubTransaction<Void> disableNanoApp(
+ @NonNull ContextHubInfo hubInfo, long nanoAppId) {
+ Preconditions.checkNotNull(hubInfo, "ContextHubInfo cannot be null");
+
ContextHubTransaction<Void> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_DISABLE_NANOAPP);
IContextHubTransactionCallback callback = createTransactionCallback(transaction);
@@ -444,10 +492,13 @@ public final class ContextHubManager {
*
* @return the ContextHubTransaction of the request
*
- * @hide
+ * @throws NullPointerException if hubInfo is null
*/
@RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
- public ContextHubTransaction<List<NanoAppState>> queryNanoApps(ContextHubInfo hubInfo) {
+ @NonNull public ContextHubTransaction<List<NanoAppState>> queryNanoApps(
+ @NonNull ContextHubInfo hubInfo) {
+ Preconditions.checkNotNull(hubInfo, "ContextHubInfo cannot be null");
+
ContextHubTransaction<List<NanoAppState>> transaction =
new ContextHubTransaction<>(ContextHubTransaction.TYPE_QUERY_NANOAPPS);
IContextHubTransactionCallback callback = createQueryCallback(transaction);
@@ -469,9 +520,14 @@ public final class ContextHubManager {
* @see Callback
*
* @return int 0 on success, -1 otherwise
+ *
+ * @deprecated Use {@link #createClient(ContextHubInfo, ContextHubClientCallback, Executor)}
+ * or {@link #createClient(ContextHubInfo, ContextHubClientCallback)} instead to
+ * register a {@link android.hardware.location.ContextHubClientCallback}.
*/
+ @Deprecated
@SuppressLint("Doclava125")
- public int registerCallback(Callback callback) {
+ public int registerCallback(@NonNull Callback callback) {
return registerCallback(callback, null);
}
@@ -498,7 +554,12 @@ public final class ContextHubManager {
* @see Callback
*
* @return int 0 on success, -1 otherwise
+ *
+ * @deprecated Use {@link #createClient(ContextHubInfo, ContextHubClientCallback, Executor)}
+ * or {@link #createClient(ContextHubInfo, ContextHubClientCallback)} instead to
+ * register a {@link android.hardware.location.ContextHubClientCallback}.
*/
+ @Deprecated
@SuppressLint("Doclava125")
public int registerCallback(Callback callback, Handler handler) {
synchronized(this) {
@@ -515,47 +576,48 @@ public final class ContextHubManager {
/**
* Creates an interface to the ContextHubClient to send down to the service.
*
+ * @param client the ContextHubClient object associated with this callback
* @param callback the callback to invoke at the client process
* @param executor the executor to invoke callbacks for this client
*
* @return the callback interface
*/
private IContextHubClientCallback createClientCallback(
- ContextHubClientCallback callback, Executor executor) {
+ ContextHubClient client, ContextHubClientCallback callback, Executor executor) {
return new IContextHubClientCallback.Stub() {
@Override
public void onMessageFromNanoApp(NanoAppMessage message) {
- executor.execute(() -> callback.onMessageFromNanoApp(message));
+ executor.execute(() -> callback.onMessageFromNanoApp(client, message));
}
@Override
public void onHubReset() {
- executor.execute(() -> callback.onHubReset());
+ executor.execute(() -> callback.onHubReset(client));
}
@Override
public void onNanoAppAborted(long nanoAppId, int abortCode) {
- executor.execute(() -> callback.onNanoAppAborted(nanoAppId, abortCode));
+ executor.execute(() -> callback.onNanoAppAborted(client, nanoAppId, abortCode));
}
@Override
public void onNanoAppLoaded(long nanoAppId) {
- executor.execute(() -> callback.onNanoAppLoaded(nanoAppId));
+ executor.execute(() -> callback.onNanoAppLoaded(client, nanoAppId));
}
@Override
public void onNanoAppUnloaded(long nanoAppId) {
- executor.execute(() -> callback.onNanoAppUnloaded(nanoAppId));
+ executor.execute(() -> callback.onNanoAppUnloaded(client, nanoAppId));
}
@Override
public void onNanoAppEnabled(long nanoAppId) {
- executor.execute(() -> callback.onNanoAppEnabled(nanoAppId));
+ executor.execute(() -> callback.onNanoAppEnabled(client, nanoAppId));
}
@Override
public void onNanoAppDisabled(long nanoAppId) {
- executor.execute(() -> callback.onNanoAppDisabled(nanoAppId));
+ executor.execute(() -> callback.onNanoAppDisabled(client, nanoAppId));
}
};
}
@@ -574,31 +636,30 @@ public final class ContextHubManager {
*
* @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
+ * @throws NullPointerException if callback, hubInfo, or executor is null
*
- * @hide
* @see ContextHubClientCallback
*/
@NonNull public ContextHubClient createClient(
@NonNull ContextHubInfo hubInfo, @NonNull ContextHubClientCallback callback,
@NonNull @CallbackExecutor Executor executor) {
- if (callback == null) {
- throw new NullPointerException("Callback cannot be null");
- }
- if (hubInfo == null) {
- throw new NullPointerException("Hub info cannot be null");
- }
+ Preconditions.checkNotNull(callback, "Callback cannot be null");
+ Preconditions.checkNotNull(hubInfo, "ContextHubInfo cannot be null");
+ Preconditions.checkNotNull(executor, "Executor cannot be null");
- IContextHubClientCallback clientInterface = createClientCallback(callback, executor);
+ ContextHubClient client = new ContextHubClient(hubInfo);
+ IContextHubClientCallback clientInterface = createClientCallback(
+ client, callback, executor);
- IContextHubClient client;
+ IContextHubClient clientProxy;
try {
- client = mService.createClient(clientInterface, hubInfo.getId());
+ clientProxy = mService.createClient(clientInterface, hubInfo.getId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
- return new ContextHubClient(client, clientInterface, hubInfo);
+ client.setClientProxy(clientProxy);
+ return client;
}
/**
@@ -612,7 +673,7 @@ public final class ContextHubManager {
* @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(
@@ -628,9 +689,13 @@ public final class ContextHubManager {
* @param callback method to deregister
*
* @return int 0 on success, -1 otherwise
+ *
+ * @deprecated Use {@link android.hardware.location.ContextHubClient#close()} to unregister
+ * a {@link android.hardware.location.ContextHubClientCallback}.
*/
@SuppressLint("Doclava125")
- public int unregisterCallback(Callback callback) {
+ @Deprecated
+ public int unregisterCallback(@NonNull Callback callback) {
synchronized(this) {
if (callback != mCallback) {
Log.w(TAG, "Cannot recognize callback!");
@@ -679,8 +744,6 @@ public final class ContextHubManager {
synchronized (this) {
mLocalCallback.onMessageReceipt(hubId, nanoAppId, message);
}
- } else {
- Log.d(TAG, "Context hub manager client callback is NULL");
}
}
};
@@ -694,7 +757,7 @@ public final class ContextHubManager {
try {
mService.registerCallback(mClientCallback);
} catch (RemoteException e) {
- Log.w(TAG, "Could not register callback:" + e);
+ throw e.rethrowFromSystemServer();
}
}
}
diff --git a/android/hardware/location/ContextHubMessage.java b/android/hardware/location/ContextHubMessage.java
index bca2ae6d..f85ce3ee 100644
--- a/android/hardware/location/ContextHubMessage.java
+++ b/android/hardware/location/ContextHubMessage.java
@@ -19,22 +19,26 @@ package android.hardware.location;
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
-import android.util.Log;
import java.util.Arrays;
/**
+ * @deprecated Use {@link android.hardware.location.NanoAppMessage} instead to send messages with
+ * {@link android.hardware.location.ContextHubClient#sendMessageToNanoApp(
+ * NanoAppMessage)} and receive messages with
+ * {@link android.hardware.location.ContextHubClientCallback#onMessageFromNanoApp(
+ * ContextHubClient, NanoAppMessage)}.
+ *
* @hide
*/
@SystemApi
+@Deprecated
public class ContextHubMessage {
+ private static final int DEBUG_LOG_NUM_BYTES = 16;
private int mType;
private int mVersion;
private byte[]mData;
- private static final String TAG = "ContextHubMessage";
-
-
/**
* Get the message type
*
@@ -131,4 +135,28 @@ public class ContextHubMessage {
return new ContextHubMessage[size];
}
};
+
+ @Override
+ public String toString() {
+ int length = mData.length;
+
+ String ret =
+ "ContextHubMessage[type = " + mType + ", length = " + mData.length + " bytes](";
+ if (length > 0) {
+ ret += "data = 0x";
+ }
+ for (int i = 0; i < Math.min(length, DEBUG_LOG_NUM_BYTES); i++) {
+ ret += Byte.toHexString(mData[i], true /* upperCase */);
+
+ if ((i + 1) % 4 == 0) {
+ ret += " ";
+ }
+ }
+ if (length > DEBUG_LOG_NUM_BYTES) {
+ ret += "...";
+ }
+ ret += ")";
+
+ return ret;
+ }
}
diff --git a/android/hardware/location/ContextHubTransaction.java b/android/hardware/location/ContextHubTransaction.java
index a1b743da..bc7efef5 100644
--- a/android/hardware/location/ContextHubTransaction.java
+++ b/android/hardware/location/ContextHubTransaction.java
@@ -18,9 +18,12 @@ package android.hardware.location;
import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.os.Handler;
import android.os.HandlerExecutor;
+import com.android.internal.util.Preconditions;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.concurrent.CountDownLatch;
@@ -35,17 +38,19 @@ import java.util.concurrent.TimeoutException;
* 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 listener
- * ({@link #setOnCompleteListener(Listener, Executor)} )}).
+ * ({@link #setOnCompleteListener(OnCompleteListener, Executor)} )}).
*
* @param <T> the type of the contents in the transaction response
*
* @hide
*/
+@SystemApi
public class ContextHubTransaction<T> {
private static final String TAG = "ContextHubTransaction";
/**
* Constants describing the type of a transaction through the Context Hub Service.
+ * {@hide}
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "TYPE_" }, value = {
@@ -65,6 +70,7 @@ public class ContextHubTransaction<T> {
/**
* Constants describing the result of a transaction or request through the Context Hub Service.
+ * {@hide}
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = { "RESULT_" }, value = {
@@ -72,7 +78,7 @@ public class ContextHubTransaction<T> {
RESULT_FAILED_UNKNOWN,
RESULT_FAILED_BAD_PARAMS,
RESULT_FAILED_UNINITIALIZED,
- RESULT_FAILED_PENDING,
+ RESULT_FAILED_BUSY,
RESULT_FAILED_AT_HUB,
RESULT_FAILED_TIMEOUT,
RESULT_FAILED_SERVICE_INTERNAL_FAILURE,
@@ -95,7 +101,7 @@ public class ContextHubTransaction<T> {
/**
* Failure mode when there are too many transactions pending.
*/
- public static final int RESULT_FAILED_PENDING = 4;
+ public static final int RESULT_FAILED_BUSY = 4;
/**
* Failure mode when the request went through, but failed asynchronously at the hub.
*/
@@ -151,7 +157,7 @@ public class ContextHubTransaction<T> {
* @param <L> the type of the contents in the transaction response
*/
@FunctionalInterface
- public interface Listener<L> {
+ public interface OnCompleteListener<L> {
/**
* The listener function to invoke when the transaction completes.
*
@@ -181,7 +187,7 @@ public class ContextHubTransaction<T> {
/*
* The listener to be invoked when the transaction completes.
*/
- private ContextHubTransaction.Listener<T> mListener = null;
+ private ContextHubTransaction.OnCompleteListener<T> mListener = null;
/*
* Synchronization latch used to block on response.
@@ -272,8 +278,8 @@ public class ContextHubTransaction<T> {
* 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 #setOnCompleteListener(ContextHubTransaction.Listener)} can only be
- * invoked once, or an IllegalStateException will be thrown.
+ * This method or {@link #setOnCompleteListener(ContextHubTransaction.OnCompleteListener)} can
+ * only be invoked once, or an IllegalStateException will be thrown.
*
* @param listener the listener to be invoked upon completion
* @param executor the executor to invoke the callback
@@ -282,15 +288,11 @@ public class ContextHubTransaction<T> {
* @throws NullPointerException if the callback or handler is null
*/
public void setOnCompleteListener(
- @NonNull ContextHubTransaction.Listener<T> listener,
+ @NonNull ContextHubTransaction.OnCompleteListener<T> listener,
@NonNull @CallbackExecutor Executor executor) {
synchronized (this) {
- if (listener == null) {
- throw new NullPointerException("Listener cannot be null");
- }
- if (executor == null) {
- throw new NullPointerException("Executor cannot be null");
- }
+ Preconditions.checkNotNull(listener, "OnCompleteListener cannot be null");
+ Preconditions.checkNotNull(executor, "Executor cannot be null");
if (mListener != null) {
throw new IllegalStateException(
"Cannot set ContextHubTransaction listener multiple times");
@@ -308,18 +310,19 @@ public class ContextHubTransaction<T> {
/**
* Sets the listener to be invoked invoked when the transaction completes.
*
- * Equivalent to {@link #setOnCompleteListener(ContextHubTransaction.Listener, Executor)}
- * with the executor using the main thread's Looper.
+ * Equivalent to {@link #setOnCompleteListener(ContextHubTransaction.OnCompleteListener,
+ * Executor)} with the executor using the main thread's Looper.
*
- * This method or {@link #setOnCompleteListener(ContextHubTransaction.Listener, Executor)}
- * can only be invoked once, or an IllegalStateException will be thrown.
+ * This method or {@link #setOnCompleteListener(ContextHubTransaction.OnCompleteListener,
+ * Executor)} can only be invoked once, or an IllegalStateException will be thrown.
*
* @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 setOnCompleteListener(@NonNull ContextHubTransaction.Listener<T> listener) {
+ public void setOnCompleteListener(
+ @NonNull ContextHubTransaction.OnCompleteListener<T> listener) {
setOnCompleteListener(listener, new HandlerExecutor(Handler.getMain()));
}
@@ -337,9 +340,7 @@ public class ContextHubTransaction<T> {
*/
/* package */ void setResponse(ContextHubTransaction.Response<T> response) {
synchronized (this) {
- if (response == null) {
- throw new NullPointerException("Response cannot be null");
- }
+ Preconditions.checkNotNull(response, "Response cannot be null");
if (mIsResponseSet) {
throw new IllegalStateException(
"Cannot set response of ContextHubTransaction multiple times");
diff --git a/android/hardware/location/NanoApp.java b/android/hardware/location/NanoApp.java
index 0465defc..b5c01ec2 100644
--- a/android/hardware/location/NanoApp.java
+++ b/android/hardware/location/NanoApp.java
@@ -28,9 +28,14 @@ import android.util.Log;
* Nano apps are expected to be used only by bundled apps only
* at this time.
*
+ * @deprecated Use {@link android.hardware.location.NanoAppBinary} instead to load a nanoapp with
+ * {@link android.hardware.location.ContextHubManager#loadNanoApp(
+ * ContextHubInfo, NanoAppBinary)}.
+ *
* @hide
*/
@SystemApi
+@Deprecated
public class NanoApp {
private final String TAG = "NanoApp";
diff --git a/android/hardware/location/NanoAppBinary.java b/android/hardware/location/NanoAppBinary.java
index 934e9e48..ba01ca25 100644
--- a/android/hardware/location/NanoAppBinary.java
+++ b/android/hardware/location/NanoAppBinary.java
@@ -15,6 +15,7 @@
*/
package android.hardware.location;
+import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
@@ -27,6 +28,7 @@ import java.util.Arrays;
/**
* @hide
*/
+@SystemApi
public final class NanoAppBinary implements Parcelable {
private static final String TAG = "NanoAppBinary";
diff --git a/android/hardware/location/NanoAppFilter.java b/android/hardware/location/NanoAppFilter.java
index 5ccf546a..75a96ee8 100644
--- a/android/hardware/location/NanoAppFilter.java
+++ b/android/hardware/location/NanoAppFilter.java
@@ -16,15 +16,18 @@
package android.hardware.location;
-
import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
/**
+ * @deprecated Use {@link android.hardware.location.ContextHubManager#queryNanoApps(ContextHubInfo)}
+ * to find loaded nanoapps, which doesn't require using this class as a parameter.
+ *
* @hide
*/
@SystemApi
+@Deprecated
public class NanoAppFilter {
private static final String TAG = "NanoAppFilter";
diff --git a/android/hardware/location/NanoAppInstanceInfo.java b/android/hardware/location/NanoAppInstanceInfo.java
index f73fd87b..f1926eaa 100644
--- a/android/hardware/location/NanoAppInstanceInfo.java
+++ b/android/hardware/location/NanoAppInstanceInfo.java
@@ -28,9 +28,12 @@ import libcore.util.EmptyArray;
*
* TODO(b/69270990) Remove this class once the old API is deprecated.
*
+ * @deprecated Use {@link android.hardware.location.NanoAppState} instead.
+ *
* @hide
*/
@SystemApi
+@Deprecated
public class NanoAppInstanceInfo {
private String mPublisher = "Unknown";
private String mName = "Unknown";
@@ -90,11 +93,6 @@ public class NanoAppInstanceInfo {
/**
* Get the application version
*
- * NOTE: There is a race condition where shortly after loading, this
- * may return -1 instead of the correct version.
- *
- * TODO(b/30970527): Fix this race condition.
- *
* @return int - version of the app
*/
public int getAppVersion() {
diff --git a/android/hardware/location/NanoAppMessage.java b/android/hardware/location/NanoAppMessage.java
index 20286749..66352581 100644
--- a/android/hardware/location/NanoAppMessage.java
+++ b/android/hardware/location/NanoAppMessage.java
@@ -15,6 +15,7 @@
*/
package android.hardware.location;
+import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -25,7 +26,9 @@ import android.os.Parcelable;
*
* @hide
*/
+@SystemApi
public final class NanoAppMessage implements Parcelable {
+ private static final int DEBUG_LOG_NUM_BYTES = 16;
private long mNanoAppId;
private int mMessageType;
private byte[] mMessageBody;
@@ -140,4 +143,29 @@ public final class NanoAppMessage implements Parcelable {
return new NanoAppMessage[size];
}
};
+
+ @Override
+ public String toString() {
+ int length = mMessageBody.length;
+
+ String ret = "NanoAppMessage[type = " + mMessageType + ", length = " + mMessageBody.length
+ + " bytes, " + (mIsBroadcasted ? "broadcast" : "unicast") + ", nanoapp = 0x"
+ + Long.toHexString(mNanoAppId) + "](";
+ if (length > 0) {
+ ret += "data = 0x";
+ }
+ for (int i = 0; i < Math.min(length, DEBUG_LOG_NUM_BYTES); i++) {
+ ret += Byte.toHexString(mMessageBody[i], true /* upperCase */);
+
+ if ((i + 1) % 4 == 0) {
+ ret += " ";
+ }
+ }
+ if (length > DEBUG_LOG_NUM_BYTES) {
+ ret += "...";
+ }
+ ret += ")";
+
+ return ret;
+ }
}
diff --git a/android/hardware/location/NanoAppState.java b/android/hardware/location/NanoAppState.java
index 644031b0..d05277d4 100644
--- a/android/hardware/location/NanoAppState.java
+++ b/android/hardware/location/NanoAppState.java
@@ -15,6 +15,7 @@
*/
package android.hardware.location;
+import android.annotation.SystemApi;
import android.os.Parcel;
import android.os.Parcelable;
@@ -23,6 +24,7 @@ import android.os.Parcelable;
*
* @hide
*/
+@SystemApi
public final class NanoAppState implements Parcelable {
private long mNanoAppId;
private int mNanoAppVersion;
diff --git a/android/hardware/radio/Announcement.java b/android/hardware/radio/Announcement.java
new file mode 100644
index 00000000..166fe604
--- /dev/null
+++ b/android/hardware/radio/Announcement.java
@@ -0,0 +1,133 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * @hide
+ */
+@SystemApi
+public final class Announcement implements Parcelable {
+
+ /** DAB alarm, RDS emergency program type (PTY 31). */
+ public static final int TYPE_EMERGENCY = 1;
+ /** DAB warning. */
+ public static final int TYPE_WARNING = 2;
+ /** DAB road traffic, RDS TA, HD Radio transportation. */
+ public static final int TYPE_TRAFFIC = 3;
+ /** Weather. */
+ public static final int TYPE_WEATHER = 4;
+ /** News. */
+ public static final int TYPE_NEWS = 5;
+ /** DAB event, special event. */
+ public static final int TYPE_EVENT = 6;
+ /** DAB sport report, RDS sports. */
+ public static final int TYPE_SPORT = 7;
+ /** All others. */
+ public static final int TYPE_MISC = 8;
+ /** @hide */
+ @IntDef(prefix = { "TYPE_" }, value = {
+ TYPE_EMERGENCY,
+ TYPE_WARNING,
+ TYPE_TRAFFIC,
+ TYPE_WEATHER,
+ TYPE_NEWS,
+ TYPE_EVENT,
+ TYPE_SPORT,
+ TYPE_MISC,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Type {}
+
+ /**
+ * Listener of announcement list events.
+ */
+ public interface OnListUpdatedListener {
+ /**
+ * An event called whenever a list of active announcements change.
+ *
+ * The entire list is sent each time a new announcement appears or any ends broadcasting.
+ *
+ * @param activeAnnouncements a full list of active announcements
+ */
+ void onListUpdated(Collection<Announcement> activeAnnouncements);
+ }
+
+ @NonNull private final ProgramSelector mSelector;
+ @Type private final int mType;
+ @NonNull private final Map<String, String> mVendorInfo;
+
+ /** @hide */
+ public Announcement(@NonNull ProgramSelector selector, @Type int type,
+ @NonNull Map<String, String> vendorInfo) {
+ mSelector = Objects.requireNonNull(selector);
+ mType = Objects.requireNonNull(type);
+ mVendorInfo = Objects.requireNonNull(vendorInfo);
+ }
+
+ private Announcement(@NonNull Parcel in) {
+ mSelector = in.readTypedObject(ProgramSelector.CREATOR);
+ mType = in.readInt();
+ mVendorInfo = Utils.readStringMap(in);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeTypedObject(mSelector, 0);
+ dest.writeInt(mType);
+ Utils.writeStringMap(dest, mVendorInfo);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<Announcement> CREATOR =
+ new Parcelable.Creator<Announcement>() {
+ public Announcement createFromParcel(Parcel in) {
+ return new Announcement(in);
+ }
+
+ public Announcement[] newArray(int size) {
+ return new Announcement[size];
+ }
+ };
+
+ public @NonNull ProgramSelector getSelector() {
+ return mSelector;
+ }
+
+ public @Type int getType() {
+ return mType;
+ }
+
+ public @NonNull Map<String, String> getVendorInfo() {
+ return mVendorInfo;
+ }
+}
diff --git a/android/hardware/radio/ProgramList.java b/android/hardware/radio/ProgramList.java
new file mode 100644
index 00000000..b2aa9ba5
--- /dev/null
+++ b/android/hardware/radio/ProgramList.java
@@ -0,0 +1,427 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+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;
+import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
+
+/**
+ * @hide
+ */
+@SystemApi
+public final class ProgramList implements AutoCloseable {
+
+ private final Object mLock = new Object();
+ private final Map<ProgramSelector.Identifier, RadioManager.ProgramInfo> mPrograms =
+ new HashMap<>();
+
+ private final List<ListCallback> mListCallbacks = new ArrayList<>();
+ private final List<OnCompleteListener> mOnCompleteListeners = new ArrayList<>();
+ private OnCloseListener mOnCloseListener;
+ private boolean mIsClosed = false;
+ private boolean mIsComplete = false;
+
+ ProgramList() {}
+
+ /**
+ * Callback for list change operations.
+ */
+ public abstract static class ListCallback {
+ /**
+ * Called when item was modified or added to the list.
+ */
+ public void onItemChanged(@NonNull ProgramSelector.Identifier id) { }
+
+ /**
+ * Called when item was removed from the list.
+ */
+ public void onItemRemoved(@NonNull ProgramSelector.Identifier id) { }
+ }
+
+ /**
+ * Listener of list complete event.
+ */
+ public interface OnCompleteListener {
+ /**
+ * Called when the list turned complete (i.e. when the scan process
+ * came to an end).
+ */
+ void onComplete();
+ }
+
+ interface OnCloseListener {
+ void onClose();
+ }
+
+ /**
+ * Registers list change callback with executor.
+ */
+ public void registerListCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull ListCallback callback) {
+ registerListCallback(new ListCallback() {
+ public void onItemChanged(@NonNull ProgramSelector.Identifier id) {
+ executor.execute(() -> callback.onItemChanged(id));
+ }
+
+ public void onItemRemoved(@NonNull ProgramSelector.Identifier id) {
+ executor.execute(() -> callback.onItemRemoved(id));
+ }
+ });
+ }
+
+ /**
+ * Registers list change callback.
+ */
+ public void registerListCallback(@NonNull ListCallback callback) {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+ mListCallbacks.add(Objects.requireNonNull(callback));
+ }
+ }
+
+ /**
+ * Unregisters list change callback.
+ */
+ public void unregisterListCallback(@NonNull ListCallback callback) {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+ mListCallbacks.remove(Objects.requireNonNull(callback));
+ }
+ }
+
+ /**
+ * Adds list complete event listener with executor.
+ */
+ public void addOnCompleteListener(@NonNull @CallbackExecutor Executor executor,
+ @NonNull OnCompleteListener listener) {
+ addOnCompleteListener(() -> executor.execute(listener::onComplete));
+ }
+
+ /**
+ * Adds list complete event listener.
+ */
+ public void addOnCompleteListener(@NonNull OnCompleteListener listener) {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+ mOnCompleteListeners.add(Objects.requireNonNull(listener));
+ if (mIsComplete) listener.onComplete();
+ }
+ }
+
+ /**
+ * Removes list complete event listener.
+ */
+ public void removeOnCompleteListener(@NonNull OnCompleteListener listener) {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+ mOnCompleteListeners.remove(Objects.requireNonNull(listener));
+ }
+ }
+
+ void setOnCloseListener(@Nullable OnCloseListener listener) {
+ synchronized (mLock) {
+ if (mOnCloseListener != null) {
+ throw new IllegalStateException("Close callback is already set");
+ }
+ mOnCloseListener = listener;
+ }
+ }
+
+ /**
+ * Disables list updates and releases all resources.
+ */
+ public void close() {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+ mIsClosed = true;
+ mPrograms.clear();
+ mListCallbacks.clear();
+ mOnCompleteListeners.clear();
+ if (mOnCloseListener != null) {
+ mOnCloseListener.onClose();
+ mOnCloseListener = null;
+ }
+ }
+ }
+
+ void apply(@NonNull Chunk chunk) {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+
+ mIsComplete = false;
+
+ if (chunk.isPurge()) {
+ new HashSet<>(mPrograms.keySet()).stream().forEach(id -> removeLocked(id));
+ }
+
+ chunk.getRemoved().stream().forEach(id -> removeLocked(id));
+ chunk.getModified().stream().forEach(info -> putLocked(info));
+
+ if (chunk.isComplete()) {
+ mIsComplete = true;
+ mOnCompleteListeners.forEach(cb -> cb.onComplete());
+ }
+ }
+ }
+
+ private void putLocked(@NonNull RadioManager.ProgramInfo value) {
+ ProgramSelector.Identifier key = value.getSelector().getPrimaryId();
+ mPrograms.put(Objects.requireNonNull(key), value);
+ ProgramSelector.Identifier sel = value.getSelector().getPrimaryId();
+ mListCallbacks.forEach(cb -> cb.onItemChanged(sel));
+ }
+
+ private void removeLocked(@NonNull ProgramSelector.Identifier key) {
+ RadioManager.ProgramInfo removed = mPrograms.remove(Objects.requireNonNull(key));
+ if (removed == null) return;
+ ProgramSelector.Identifier sel = removed.getSelector().getPrimaryId();
+ mListCallbacks.forEach(cb -> cb.onItemRemoved(sel));
+ }
+
+ /**
+ * Converts the program list in its current shape to the static List<>.
+ *
+ * @return the new List<> object; it won't receive any further updates
+ */
+ public @NonNull List<RadioManager.ProgramInfo> toList() {
+ synchronized (mLock) {
+ return mPrograms.values().stream().collect(Collectors.toList());
+ }
+ }
+
+ /**
+ * Returns the program with a specified primary identifier.
+ *
+ * @param id primary identifier of a program to fetch
+ * @return the program info, or null if there is no such program on the list
+ */
+ public @Nullable RadioManager.ProgramInfo get(@NonNull ProgramSelector.Identifier id) {
+ synchronized (mLock) {
+ return mPrograms.get(Objects.requireNonNull(id));
+ }
+ }
+
+ /**
+ * Filter for the program list.
+ */
+ public static final class Filter implements Parcelable {
+ private final @NonNull Set<Integer> mIdentifierTypes;
+ private final @NonNull Set<ProgramSelector.Identifier> mIdentifiers;
+ private final boolean mIncludeCategories;
+ private final boolean mExcludeModifications;
+ private final @Nullable Map<String, String> mVendorFilter;
+
+ /**
+ * Constructor of program list filter.
+ *
+ * Arrays passed to this constructor become owned by this object, do not modify them later.
+ *
+ * @param identifierTypes see getIdentifierTypes()
+ * @param identifiers see getIdentifiers()
+ * @param includeCategories see areCategoriesIncluded()
+ * @param excludeModifications see areModificationsExcluded()
+ */
+ public Filter(@NonNull Set<Integer> identifierTypes,
+ @NonNull Set<ProgramSelector.Identifier> identifiers,
+ boolean includeCategories, boolean excludeModifications) {
+ mIdentifierTypes = Objects.requireNonNull(identifierTypes);
+ mIdentifiers = Objects.requireNonNull(identifiers);
+ mIncludeCategories = includeCategories;
+ mExcludeModifications = excludeModifications;
+ mVendorFilter = null;
+ }
+
+ /**
+ * @hide for framework use only
+ */
+ public Filter(@Nullable Map<String, String> vendorFilter) {
+ mIdentifierTypes = Collections.emptySet();
+ mIdentifiers = Collections.emptySet();
+ mIncludeCategories = false;
+ mExcludeModifications = false;
+ mVendorFilter = vendorFilter;
+ }
+
+ private Filter(@NonNull Parcel in) {
+ mIdentifierTypes = Utils.createIntSet(in);
+ mIdentifiers = Utils.createSet(in, ProgramSelector.Identifier.CREATOR);
+ mIncludeCategories = in.readByte() != 0;
+ mExcludeModifications = in.readByte() != 0;
+ mVendorFilter = Utils.readStringMap(in);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ Utils.writeIntSet(dest, mIdentifierTypes);
+ Utils.writeSet(dest, mIdentifiers);
+ dest.writeByte((byte) (mIncludeCategories ? 1 : 0));
+ dest.writeByte((byte) (mExcludeModifications ? 1 : 0));
+ Utils.writeStringMap(dest, mVendorFilter);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<Filter> CREATOR = new Parcelable.Creator<Filter>() {
+ public Filter createFromParcel(Parcel in) {
+ return new Filter(in);
+ }
+
+ public Filter[] newArray(int size) {
+ return new Filter[size];
+ }
+ };
+
+ /**
+ * @hide for framework use only
+ */
+ public Map<String, String> getVendorFilter() {
+ return mVendorFilter;
+ }
+
+ /**
+ * Returns the list of identifier types that satisfy the filter.
+ *
+ * If the program list entry contains at least one identifier of the type
+ * listed, it satisfies this condition.
+ *
+ * Empty list means no filtering on identifier type.
+ *
+ * @return the list of accepted identifier types, must not be modified
+ */
+ public @NonNull Set<Integer> getIdentifierTypes() {
+ return mIdentifierTypes;
+ }
+
+ /**
+ * Returns the list of identifiers that satisfy the filter.
+ *
+ * If the program list entry contains at least one listed identifier,
+ * it satisfies this condition.
+ *
+ * Empty list means no filtering on identifier.
+ *
+ * @return the list of accepted identifiers, must not be modified
+ */
+ public @NonNull Set<ProgramSelector.Identifier> getIdentifiers() {
+ return mIdentifiers;
+ }
+
+ /**
+ * Checks, if non-tunable entries that define tree structure on the
+ * program list (i.e. DAB ensembles) should be included.
+ */
+ public boolean areCategoriesIncluded() {
+ return mIncludeCategories;
+ }
+
+ /**
+ * Checks, if updates on entry modifications should be disabled.
+ *
+ * If true, 'modified' vector of ProgramListChunk must contain list
+ * additions only. Once the program is added to the list, it's not
+ * updated anymore.
+ */
+ public boolean areModificationsExcluded() {
+ return mExcludeModifications;
+ }
+ }
+
+ /**
+ * @hide This is a transport class used for internal communication between
+ * Broadcast Radio Service and RadioManager.
+ * Do not use it directly.
+ */
+ public static final class Chunk implements Parcelable {
+ private final boolean mPurge;
+ private final boolean mComplete;
+ private final @NonNull Set<RadioManager.ProgramInfo> mModified;
+ private final @NonNull Set<ProgramSelector.Identifier> mRemoved;
+
+ public Chunk(boolean purge, boolean complete,
+ @Nullable Set<RadioManager.ProgramInfo> modified,
+ @Nullable Set<ProgramSelector.Identifier> removed) {
+ mPurge = purge;
+ mComplete = complete;
+ mModified = (modified != null) ? modified : Collections.emptySet();
+ mRemoved = (removed != null) ? removed : Collections.emptySet();
+ }
+
+ private Chunk(@NonNull Parcel in) {
+ mPurge = in.readByte() != 0;
+ mComplete = in.readByte() != 0;
+ mModified = Utils.createSet(in, RadioManager.ProgramInfo.CREATOR);
+ mRemoved = Utils.createSet(in, ProgramSelector.Identifier.CREATOR);
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByte((byte) (mPurge ? 1 : 0));
+ dest.writeByte((byte) (mComplete ? 1 : 0));
+ Utils.writeSet(dest, mModified);
+ Utils.writeSet(dest, mRemoved);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Parcelable.Creator<Chunk> CREATOR = new Parcelable.Creator<Chunk>() {
+ public Chunk createFromParcel(Parcel in) {
+ return new Chunk(in);
+ }
+
+ public Chunk[] newArray(int size) {
+ return new Chunk[size];
+ }
+ };
+
+ public boolean isPurge() {
+ return mPurge;
+ }
+
+ public boolean isComplete() {
+ return mComplete;
+ }
+
+ public @NonNull Set<RadioManager.ProgramInfo> getModified() {
+ return mModified;
+ }
+
+ public @NonNull Set<ProgramSelector.Identifier> getRemoved() {
+ return mRemoved;
+ }
+ }
+}
diff --git a/android/hardware/radio/ProgramSelector.java b/android/hardware/radio/ProgramSelector.java
index 2211cee9..0294a29b 100644
--- a/android/hardware/radio/ProgramSelector.java
+++ b/android/hardware/radio/ProgramSelector.java
@@ -27,6 +27,7 @@ import android.os.Parcelable;
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.Objects;
import java.util.stream.Stream;
@@ -59,24 +60,58 @@ import java.util.stream.Stream;
*/
@SystemApi
public final class ProgramSelector implements Parcelable {
- /** Analogue AM radio (with or without RDS). */
+ /** Invalid program type.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
+ public static final int PROGRAM_TYPE_INVALID = 0;
+ /** Analogue AM radio (with or without RDS).
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_AM = 1;
- /** analogue FM radio (with or without RDS). */
+ /** analogue FM radio (with or without RDS).
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_FM = 2;
- /** AM HD Radio. */
+ /** AM HD Radio.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_AM_HD = 3;
- /** FM HD Radio. */
+ /** FM HD Radio.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_FM_HD = 4;
- /** Digital audio broadcasting. */
+ /** Digital audio broadcasting.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_DAB = 5;
- /** Digital Radio Mondiale. */
+ /** Digital Radio Mondiale.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_DRMO = 6;
- /** SiriusXM Satellite Radio. */
+ /** SiriusXM Satellite Radio.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_SXM = 7;
- /** Vendor-specific, not synced across devices. */
+ /** Vendor-specific, not synced across devices.
+ * @deprecated use {@link ProgramIdentifier} instead
+ */
+ @Deprecated
public static final int PROGRAM_TYPE_VENDOR_START = 1000;
+ /** @deprecated use {@link ProgramIdentifier} instead */
+ @Deprecated
public static final int PROGRAM_TYPE_VENDOR_END = 1999;
+ /** @deprecated use {@link ProgramIdentifier} instead */
+ @Deprecated
@IntDef(prefix = { "PROGRAM_TYPE_" }, value = {
+ PROGRAM_TYPE_INVALID,
PROGRAM_TYPE_AM,
PROGRAM_TYPE_FM,
PROGRAM_TYPE_AM_HD,
@@ -89,6 +124,7 @@ public final class ProgramSelector implements Parcelable {
@Retention(RetentionPolicy.SOURCE)
public @interface ProgramType {}
+ public static final int IDENTIFIER_TYPE_INVALID = 0;
/** kHz */
public static final int IDENTIFIER_TYPE_AMFM_FREQUENCY = 1;
/** 16bit */
@@ -109,18 +145,46 @@ public final class ProgramSelector implements Parcelable {
*
* The subchannel index is 0-based (where 0 is MPS and 1..7 are SPS),
* as opposed to HD Radio standard (where it's 1-based).
+ *
+ * @deprecated use IDENTIFIER_TYPE_HD_STATION_ID_EXT instead
*/
+ @Deprecated
public static final int IDENTIFIER_TYPE_HD_SUBCHANNEL = 4;
/**
- * 24bit compound primary identifier for DAB.
+ * 64bit additional identifier for HD Radio.
+ *
+ * Due to Station ID abuse, some HD_STATION_ID_EXT identifiers may be not
+ * globally unique. To provide a best-effort solution, a short version of
+ * station name may be carried as additional identifier and may be used
+ * by the tuner hardware to double-check tuning.
+ *
+ * The name is limited to the first 8 A-Z0-9 characters (lowercase letters
+ * must be converted to uppercase). Encoded in little-endian ASCII:
+ * the first character of the name is the LSB.
+ *
+ * For example: "Abc" is encoded as 0x434241.
+ */
+ public static final int IDENTIFIER_TYPE_HD_STATION_NAME = 10004;
+ /**
+ * @see {@link IDENTIFIER_TYPE_DAB_SID_EXT}
+ */
+ public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5;
+ /**
+ * 28bit compound primary identifier for Digital Audio Broadcasting.
*
* Consists of (from the LSB):
* - 16bit: SId;
- * - 8bit: ECC code.
+ * - 8bit: ECC code;
+ * - 4bit: SCIdS.
+ *
+ * SCIdS (Service Component Identifier within the Service) value
+ * of 0 represents the main service, while 1 and above represents
+ * secondary services.
+ *
* The remaining bits should be set to zeros when writing on the chip side
* and ignored when read.
*/
- public static final int IDENTIFIER_TYPE_DAB_SIDECC = 5;
+ public static final int IDENTIFIER_TYPE_DAB_SID_EXT = IDENTIFIER_TYPE_DAB_SIDECC;
/** 16bit */
public static final int IDENTIFIER_TYPE_DAB_ENSEMBLE = 6;
/** 12bit */
@@ -131,7 +195,11 @@ public final class ProgramSelector implements Parcelable {
public static final int IDENTIFIER_TYPE_DRMO_SERVICE_ID = 9;
/** kHz */
public static final int IDENTIFIER_TYPE_DRMO_FREQUENCY = 10;
- /** 1: AM, 2:FM */
+ /**
+ * 1: AM, 2:FM
+ * @deprecated use {@link IDENTIFIER_TYPE_DRMO_FREQUENCY} instead
+ */
+ @Deprecated
public static final int IDENTIFIER_TYPE_DRMO_MODULATION = 11;
/** 32bit */
public static final int IDENTIFIER_TYPE_SXM_SERVICE_ID = 12;
@@ -145,13 +213,29 @@ public final class ProgramSelector implements Parcelable {
* type between VENDOR_START and VENDOR_END (eg. identifier type 1015 must
* not be used in any program type other than 1015).
*/
- public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_START = PROGRAM_TYPE_VENDOR_START;
- public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = PROGRAM_TYPE_VENDOR_END;
+ public static final int IDENTIFIER_TYPE_VENDOR_START = PROGRAM_TYPE_VENDOR_START;
+ /**
+ * @see {@link IDENTIFIER_TYPE_VENDOR_START}
+ */
+ public static final int IDENTIFIER_TYPE_VENDOR_END = PROGRAM_TYPE_VENDOR_END;
+ /**
+ * @deprecated use {@link IDENTIFIER_TYPE_VENDOR_START} instead
+ */
+ @Deprecated
+ public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_START = IDENTIFIER_TYPE_VENDOR_START;
+ /**
+ * @deprecated use {@link IDENTIFIER_TYPE_VENDOR_END} instead
+ */
+ @Deprecated
+ public static final int IDENTIFIER_TYPE_VENDOR_PRIMARY_END = IDENTIFIER_TYPE_VENDOR_END;
@IntDef(prefix = { "IDENTIFIER_TYPE_" }, value = {
+ IDENTIFIER_TYPE_INVALID,
IDENTIFIER_TYPE_AMFM_FREQUENCY,
IDENTIFIER_TYPE_RDS_PI,
IDENTIFIER_TYPE_HD_STATION_ID_EXT,
IDENTIFIER_TYPE_HD_SUBCHANNEL,
+ IDENTIFIER_TYPE_HD_STATION_NAME,
+ IDENTIFIER_TYPE_DAB_SID_EXT,
IDENTIFIER_TYPE_DAB_SIDECC,
IDENTIFIER_TYPE_DAB_ENSEMBLE,
IDENTIFIER_TYPE_DAB_SCID,
@@ -162,7 +246,7 @@ public final class ProgramSelector implements Parcelable {
IDENTIFIER_TYPE_SXM_SERVICE_ID,
IDENTIFIER_TYPE_SXM_CHANNEL,
})
- @IntRange(from = IDENTIFIER_TYPE_VENDOR_PRIMARY_START, to = IDENTIFIER_TYPE_VENDOR_PRIMARY_END)
+ @IntRange(from = IDENTIFIER_TYPE_VENDOR_START, to = IDENTIFIER_TYPE_VENDOR_END)
@Retention(RetentionPolicy.SOURCE)
public @interface IdentifierType {}
@@ -201,7 +285,9 @@ public final class ProgramSelector implements Parcelable {
* Type of a radio technology.
*
* @return program type.
+ * @deprecated use {@link getPrimaryId} instead
*/
+ @Deprecated
public @ProgramType int getProgramType() {
return mProgramType;
}
@@ -268,13 +354,48 @@ public final class ProgramSelector implements Parcelable {
* Vendor identifiers are passed as-is to the HAL implementation,
* preserving elements order.
*
- * @return a array of vendor identifiers, must not be modified.
+ * @return an array of vendor identifiers, must not be modified.
+ * @deprecated for HAL 1.x compatibility;
+ * HAL 2.x uses standard primary/secondary lists for vendor IDs
*/
+ @Deprecated
public @NonNull long[] getVendorIds() {
return mVendorIds;
}
/**
+ * Creates an equivalent ProgramSelector with a given secondary identifier preferred.
+ *
+ * Used to point to a specific physical identifier for technologies that may broadcast the same
+ * program on different channels. For example, with a DAB program broadcasted over multiple
+ * ensembles, the radio hardware may select the one with the strongest signal. The UI may select
+ * preferred ensemble though, so the radio hardware may try to use it in the first place.
+ *
+ * This is a best-effort hint for the tuner, not a guaranteed behavior.
+ *
+ * Setting the given secondary identifier as preferred means filtering out other secondary
+ * identifiers of its type and adding it to the list.
+ *
+ * @param preferred preferred secondary identifier
+ * @return a new ProgramSelector with a given secondary identifier preferred
+ */
+ public @NonNull ProgramSelector withSecondaryPreferred(@NonNull Identifier preferred) {
+ int preferredType = preferred.getType();
+ Identifier[] secondaryIds = Stream.concat(
+ // remove other identifiers of that type
+ Arrays.stream(mSecondaryIds).filter(id -> id.getType() != preferredType),
+ // add preferred identifier instead
+ Stream.of(preferred)).toArray(Identifier[]::new);
+
+ return new ProgramSelector(
+ mProgramType,
+ mPrimaryId,
+ secondaryIds,
+ mVendorIds
+ );
+ }
+
+ /**
* Builds new ProgramSelector for AM/FM frequency.
*
* @param band the band.
@@ -423,6 +544,10 @@ public final class ProgramSelector implements Parcelable {
private final long mValue;
public Identifier(@IdentifierType int type, long value) {
+ if (type == IDENTIFIER_TYPE_HD_STATION_NAME) {
+ // see getType
+ type = IDENTIFIER_TYPE_HD_SUBCHANNEL;
+ }
mType = type;
mValue = value;
}
@@ -433,6 +558,13 @@ public final class ProgramSelector implements Parcelable {
* @return type of an identifier.
*/
public @IdentifierType int getType() {
+ if (mType == IDENTIFIER_TYPE_HD_SUBCHANNEL && mValue > 10) {
+ /* HD_SUBCHANNEL and HD_STATION_NAME use the same identifier type, but they differ
+ * in possible values: sub channel is 0-7, station name is greater than ASCII space
+ * code (32).
+ */
+ return IDENTIFIER_TYPE_HD_STATION_NAME;
+ }
return mType;
}
diff --git a/android/hardware/radio/RadioManager.java b/android/hardware/radio/RadioManager.java
index 4d54e31b..b00f6033 100644
--- a/android/hardware/radio/RadioManager.java
+++ b/android/hardware/radio/RadioManager.java
@@ -17,8 +17,10 @@
package android.hardware.radio;
import android.Manifest;
+import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.annotation.SystemService;
@@ -32,13 +34,19 @@ import android.os.ServiceManager.ServiceNotFoundException;
import android.text.TextUtils;
import android.util.Log;
+import com.android.internal.util.Preconditions;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.Executor;
import java.util.stream.Collectors;
/**
@@ -119,24 +127,70 @@ public class RadioManager {
* @see BandDescriptor */
public static final int REGION_KOREA = 4;
- private static void writeStringMap(@NonNull Parcel dest, @NonNull Map<String, String> map) {
- dest.writeInt(map.size());
- for (Map.Entry<String, String> entry : map.entrySet()) {
- dest.writeString(entry.getKey());
- dest.writeString(entry.getValue());
- }
- }
-
- private static @NonNull Map<String, String> readStringMap(@NonNull Parcel in) {
- int size = in.readInt();
- Map<String, String> map = new HashMap<>();
- while (size-- > 0) {
- String key = in.readString();
- String value = in.readString();
- map.put(key, value);
- }
- return map;
- }
+ /**
+ * Forces mono audio stream reception.
+ *
+ * Analog broadcasts can recover poor reception conditions by jointing
+ * stereo channels into one. Mainly for, but not limited to AM/FM.
+ */
+ public static final int CONFIG_FORCE_MONO = 1;
+ /**
+ * Forces the analog playback for the supporting radio technology.
+ *
+ * User may disable digital playback for FM HD Radio or hybrid FM/DAB with
+ * this option. This is purely user choice, ie. does not reflect digital-
+ * analog handover state managed from the HAL implementation side.
+ *
+ * Some radio technologies may not support this, ie. DAB.
+ */
+ public static final int CONFIG_FORCE_ANALOG = 2;
+ /**
+ * Forces the digital playback for the supporting radio technology.
+ *
+ * User may disable digital-analog handover that happens with poor
+ * reception conditions. With digital forced, the radio will remain silent
+ * instead of switching to analog channel if it's available. This is purely
+ * user choice, it does not reflect the actual state of handover.
+ */
+ public static final int CONFIG_FORCE_DIGITAL = 3;
+ /**
+ * RDS Alternative Frequencies.
+ *
+ * If set and the currently tuned RDS station broadcasts on multiple
+ * channels, radio tuner automatically switches to the best available
+ * alternative.
+ */
+ public static final int CONFIG_RDS_AF = 4;
+ /**
+ * RDS region-specific program lock-down.
+ *
+ * Allows user to lock to the current region as they move into the
+ * other region.
+ */
+ public static final int CONFIG_RDS_REG = 5;
+ /** Enables DAB-DAB hard- and implicit-linking (the same content). */
+ public static final int CONFIG_DAB_DAB_LINKING = 6;
+ /** Enables DAB-FM hard- and implicit-linking (the same content). */
+ public static final int CONFIG_DAB_FM_LINKING = 7;
+ /** Enables DAB-DAB soft-linking (related content). */
+ public static final int CONFIG_DAB_DAB_SOFT_LINKING = 8;
+ /** Enables DAB-FM soft-linking (related content). */
+ public static final int CONFIG_DAB_FM_SOFT_LINKING = 9;
+
+ /** @hide */
+ @IntDef(prefix = { "CONFIG_" }, value = {
+ CONFIG_FORCE_MONO,
+ CONFIG_FORCE_ANALOG,
+ CONFIG_FORCE_DIGITAL,
+ CONFIG_RDS_AF,
+ CONFIG_RDS_REG,
+ CONFIG_DAB_DAB_LINKING,
+ CONFIG_DAB_FM_LINKING,
+ CONFIG_DAB_DAB_SOFT_LINKING,
+ CONFIG_DAB_FM_SOFT_LINKING,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ConfigFlag {}
/*****************************************************************************
* Lists properties, options and radio bands supported by a given broadcast radio module.
@@ -349,7 +403,7 @@ public class RadioManager {
mIsBgScanSupported = in.readInt() == 1;
mSupportedProgramTypes = arrayToSet(in.createIntArray());
mSupportedIdentifierTypes = arrayToSet(in.createIntArray());
- mVendorInfo = readStringMap(in);
+ mVendorInfo = Utils.readStringMap(in);
}
public static final Parcelable.Creator<ModuleProperties> CREATOR
@@ -379,7 +433,7 @@ public class RadioManager {
dest.writeInt(mIsBgScanSupported ? 1 : 0);
dest.writeIntArray(setToArray(mSupportedProgramTypes));
dest.writeIntArray(setToArray(mSupportedIdentifierTypes));
- writeStringMap(dest, mVendorInfo);
+ Utils.writeStringMap(dest, mVendorInfo);
}
@Override
@@ -645,7 +699,8 @@ public class RadioManager {
private final boolean mAf;
private final boolean mEa;
- FmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing,
+ /** @hide */
+ public FmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing,
boolean stereo, boolean rds, boolean ta, boolean af, boolean ea) {
super(region, type, lowerLimit, upperLimit, spacing);
mStereo = stereo;
@@ -771,7 +826,8 @@ public class RadioManager {
private final boolean mStereo;
- AmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing,
+ /** @hide */
+ public AmBandDescriptor(int region, int type, int lowerLimit, int upperLimit, int spacing,
boolean stereo) {
super(region, type, lowerLimit, upperLimit, spacing);
mStereo = stereo;
@@ -843,10 +899,10 @@ public class RadioManager {
/** Radio band configuration. */
public static class BandConfig implements Parcelable {
- final BandDescriptor mDescriptor;
+ @NonNull final BandDescriptor mDescriptor;
BandConfig(BandDescriptor descriptor) {
- mDescriptor = descriptor;
+ mDescriptor = Objects.requireNonNull(descriptor);
}
BandConfig(int region, int type, int lowerLimit, int upperLimit, int spacing) {
@@ -968,7 +1024,8 @@ public class RadioManager {
private final boolean mAf;
private final boolean mEa;
- FmBandConfig(FmBandDescriptor descriptor) {
+ /** @hide */
+ public FmBandConfig(FmBandDescriptor descriptor) {
super((BandDescriptor)descriptor);
mStereo = descriptor.isStereoSupported();
mRds = descriptor.isRdsSupported();
@@ -1204,7 +1261,8 @@ public class RadioManager {
public static class AmBandConfig extends BandConfig {
private final boolean mStereo;
- AmBandConfig(AmBandDescriptor descriptor) {
+ /** @hide */
+ public AmBandConfig(AmBandDescriptor descriptor) {
super((BandDescriptor)descriptor);
mStereo = descriptor.isStereoSupported();
}
@@ -1329,34 +1387,44 @@ public class RadioManager {
};
}
- /** Radio program information returned by
- * {@link RadioTuner#getProgramInformation(RadioManager.ProgramInfo[])} */
+ /** Radio program information. */
public static class ProgramInfo implements Parcelable {
- // sourced from hardware/interfaces/broadcastradio/1.1/types.hal
+ // sourced from hardware/interfaces/broadcastradio/2.0/types.hal
private static final int FLAG_LIVE = 1 << 0;
private static final int FLAG_MUTED = 1 << 1;
private static final int FLAG_TRAFFIC_PROGRAM = 1 << 2;
private static final int FLAG_TRAFFIC_ANNOUNCEMENT = 1 << 3;
+ private static final int FLAG_TUNED = 1 << 4;
+ private static final int FLAG_STEREO = 1 << 5;
@NonNull private final ProgramSelector mSelector;
- private final boolean mTuned;
- private final boolean mStereo;
- private final boolean mDigital;
- private final int mFlags;
- private final int mSignalStrength;
- private final RadioMetadata mMetadata;
+ @Nullable private final ProgramSelector.Identifier mLogicallyTunedTo;
+ @Nullable private final ProgramSelector.Identifier mPhysicallyTunedTo;
+ @NonNull private final Collection<ProgramSelector.Identifier> mRelatedContent;
+ private final int mInfoFlags;
+ private final int mSignalQuality;
+ @Nullable private final RadioMetadata mMetadata;
@NonNull private final Map<String, String> mVendorInfo;
- ProgramInfo(@NonNull ProgramSelector selector, boolean tuned, boolean stereo,
- boolean digital, int signalStrength, RadioMetadata metadata, int flags,
- Map<String, String> vendorInfo) {
- mSelector = selector;
- mTuned = tuned;
- mStereo = stereo;
- mDigital = digital;
- mFlags = flags;
- mSignalStrength = signalStrength;
+ /** @hide */
+ public ProgramInfo(@NonNull ProgramSelector selector,
+ @Nullable ProgramSelector.Identifier logicallyTunedTo,
+ @Nullable ProgramSelector.Identifier physicallyTunedTo,
+ @Nullable Collection<ProgramSelector.Identifier> relatedContent,
+ int infoFlags, int signalQuality, @Nullable RadioMetadata metadata,
+ @Nullable Map<String, String> vendorInfo) {
+ mSelector = Objects.requireNonNull(selector);
+ mLogicallyTunedTo = logicallyTunedTo;
+ mPhysicallyTunedTo = physicallyTunedTo;
+ if (relatedContent == null) {
+ mRelatedContent = Collections.emptyList();
+ } else {
+ Preconditions.checkCollectionElementsNotNull(relatedContent, "relatedContent");
+ mRelatedContent = relatedContent;
+ }
+ mInfoFlags = infoFlags;
+ mSignalQuality = signalQuality;
mMetadata = metadata;
mVendorInfo = (vendorInfo == null) ? new HashMap<>() : vendorInfo;
}
@@ -1370,6 +1438,51 @@ public class RadioManager {
return mSelector;
}
+ /**
+ * Identifier currently used for program selection.
+ *
+ * This identifier can be used to determine which technology is
+ * currently being used for reception.
+ *
+ * Some program selectors contain tuning information for different radio
+ * technologies (i.e. FM RDS and DAB). For example, user may tune using
+ * a ProgramSelector with RDS_PI primary identifier, but the tuner hardware
+ * may choose to use DAB technology to make actual tuning. This identifier
+ * must reflect that.
+ */
+ public @Nullable ProgramSelector.Identifier getLogicallyTunedTo() {
+ return mLogicallyTunedTo;
+ }
+
+ /**
+ * Identifier currently used by hardware to physically tune to a channel.
+ *
+ * Some radio technologies broadcast the same program on multiple channels,
+ * i.e. with RDS AF the same program may be broadcasted on multiple
+ * alternative frequencies; the same DAB program may be broadcast on
+ * multiple ensembles. This identifier points to the channel to which the
+ * radio hardware is physically tuned to.
+ */
+ public @Nullable ProgramSelector.Identifier getPhysicallyTunedTo() {
+ return mPhysicallyTunedTo;
+ }
+
+ /**
+ * Primary identifiers of related contents.
+ *
+ * Some radio technologies provide pointers to other programs that carry
+ * related content (i.e. DAB soft-links). This field is a list of pointers
+ * to other programs on the program list.
+ *
+ * Please note, that these identifiers does not have to exist on the program
+ * list - i.e. DAB tuner may provide information on FM RDS alternatives
+ * despite not supporting FM RDS. If the system has multiple tuners, another
+ * one may have it on its list.
+ */
+ public @Nullable Collection<ProgramSelector.Identifier> getRelatedContent() {
+ return mRelatedContent;
+ }
+
/** Main channel expressed in units according to band type.
* Currently all defined band types express channels as frequency in kHz
* @return the program channel
@@ -1404,19 +1517,28 @@ public class RadioManager {
* @return {@code true} if currently tuned, {@code false} otherwise.
*/
public boolean isTuned() {
- return mTuned;
+ return (mInfoFlags & FLAG_TUNED) != 0;
}
+
/** {@code true} if the received program is stereo
* @return {@code true} if stereo, {@code false} otherwise.
*/
public boolean isStereo() {
- return mStereo;
+ return (mInfoFlags & FLAG_STEREO) != 0;
}
+
/** {@code true} if the received program is digital (e.g HD radio)
* @return {@code true} if digital, {@code false} otherwise.
+ * @deprecated Use {@link getLogicallyTunedTo()} instead.
*/
+ @Deprecated
public boolean isDigital() {
- return mDigital;
+ ProgramSelector.Identifier id = mLogicallyTunedTo;
+ if (id == null) id = mSelector.getPrimaryId();
+
+ int type = id.getType();
+ return (type != ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY
+ && type != ProgramSelector.IDENTIFIER_TYPE_RDS_PI);
}
/**
@@ -1425,7 +1547,7 @@ public class RadioManager {
* usually targetted at reduced latency.
*/
public boolean isLive() {
- return (mFlags & FLAG_LIVE) != 0;
+ return (mInfoFlags & FLAG_LIVE) != 0;
}
/**
@@ -1435,7 +1557,7 @@ public class RadioManager {
* It does NOT mean the user has muted audio.
*/
public boolean isMuted() {
- return (mFlags & FLAG_MUTED) != 0;
+ return (mInfoFlags & FLAG_MUTED) != 0;
}
/**
@@ -1443,7 +1565,7 @@ public class RadioManager {
* regularily.
*/
public boolean isTrafficProgram() {
- return (mFlags & FLAG_TRAFFIC_PROGRAM) != 0;
+ return (mInfoFlags & FLAG_TRAFFIC_PROGRAM) != 0;
}
/**
@@ -1451,15 +1573,18 @@ public class RadioManager {
* at the very moment.
*/
public boolean isTrafficAnnouncementActive() {
- return (mFlags & FLAG_TRAFFIC_ANNOUNCEMENT) != 0;
+ return (mInfoFlags & FLAG_TRAFFIC_ANNOUNCEMENT) != 0;
}
- /** Signal strength indicator from 0 (no signal) to 100 (excellent)
- * @return the signal strength indication.
+ /**
+ * Signal quality (as opposed to the name) indication from 0 (no signal)
+ * to 100 (excellent)
+ * @return the signal quality indication.
*/
public int getSignalStrength() {
- return mSignalStrength;
+ return mSignalQuality;
}
+
/** Metadata currently received from this station.
* null if no metadata have been received
* @return current meta data received from this program.
@@ -1483,18 +1608,14 @@ public class RadioManager {
}
private ProgramInfo(Parcel in) {
- mSelector = in.readParcelable(null);
- mTuned = in.readByte() == 1;
- mStereo = in.readByte() == 1;
- mDigital = in.readByte() == 1;
- mSignalStrength = in.readInt();
- if (in.readByte() == 1) {
- mMetadata = RadioMetadata.CREATOR.createFromParcel(in);
- } else {
- mMetadata = null;
- }
- mFlags = in.readInt();
- mVendorInfo = readStringMap(in);
+ mSelector = Objects.requireNonNull(in.readTypedObject(ProgramSelector.CREATOR));
+ mLogicallyTunedTo = in.readTypedObject(ProgramSelector.Identifier.CREATOR);
+ mPhysicallyTunedTo = in.readTypedObject(ProgramSelector.Identifier.CREATOR);
+ mRelatedContent = in.createTypedArrayList(ProgramSelector.Identifier.CREATOR);
+ mInfoFlags = in.readInt();
+ mSignalQuality = in.readInt();
+ mMetadata = in.readTypedObject(RadioMetadata.CREATOR);
+ mVendorInfo = Utils.readStringMap(in);
}
public static final Parcelable.Creator<ProgramInfo> CREATOR
@@ -1510,19 +1631,14 @@ public class RadioManager {
@Override
public void writeToParcel(Parcel dest, int flags) {
- dest.writeParcelable(mSelector, 0);
- dest.writeByte((byte)(mTuned ? 1 : 0));
- dest.writeByte((byte)(mStereo ? 1 : 0));
- dest.writeByte((byte)(mDigital ? 1 : 0));
- dest.writeInt(mSignalStrength);
- if (mMetadata == null) {
- dest.writeByte((byte)0);
- } else {
- dest.writeByte((byte)1);
- mMetadata.writeToParcel(dest, flags);
- }
- dest.writeInt(mFlags);
- writeStringMap(dest, mVendorInfo);
+ dest.writeTypedObject(mSelector, flags);
+ dest.writeTypedObject(mLogicallyTunedTo, flags);
+ dest.writeTypedObject(mPhysicallyTunedTo, flags);
+ Utils.writeTypedCollection(dest, mRelatedContent);
+ dest.writeInt(mInfoFlags);
+ dest.writeInt(mSignalQuality);
+ dest.writeTypedObject(mMetadata, flags);
+ Utils.writeStringMap(dest, mVendorInfo);
}
@Override
@@ -1532,52 +1648,38 @@ public class RadioManager {
@Override
public String toString() {
- return "ProgramInfo [mSelector=" + mSelector
- + ", mTuned=" + mTuned + ", mStereo=" + mStereo + ", mDigital=" + mDigital
- + ", mFlags=" + mFlags + ", mSignalStrength=" + mSignalStrength
- + ((mMetadata == null) ? "" : (", mMetadata=" + mMetadata.toString()))
+ return "ProgramInfo"
+ + " [selector=" + mSelector
+ + ", logicallyTunedTo=" + Objects.toString(mLogicallyTunedTo)
+ + ", physicallyTunedTo=" + Objects.toString(mPhysicallyTunedTo)
+ + ", relatedContent=" + mRelatedContent.size()
+ + ", infoFlags=" + mInfoFlags
+ + ", mSignalQuality=" + mSignalQuality
+ + ", mMetadata=" + Objects.toString(mMetadata)
+ "]";
}
@Override
public int hashCode() {
- final int prime = 31;
- int result = 1;
- result = prime * result + mSelector.hashCode();
- result = prime * result + (mTuned ? 1 : 0);
- result = prime * result + (mStereo ? 1 : 0);
- result = prime * result + (mDigital ? 1 : 0);
- result = prime * result + mFlags;
- result = prime * result + mSignalStrength;
- result = prime * result + ((mMetadata == null) ? 0 : mMetadata.hashCode());
- result = prime * result + mVendorInfo.hashCode();
- return result;
+ return Objects.hash(mSelector, mLogicallyTunedTo, mPhysicallyTunedTo,
+ mRelatedContent, mInfoFlags, mSignalQuality, mMetadata, mVendorInfo);
}
@Override
public boolean equals(Object obj) {
- if (this == obj)
- return true;
- if (!(obj instanceof ProgramInfo))
- return false;
+ if (this == obj) return true;
+ if (!(obj instanceof ProgramInfo)) return false;
ProgramInfo other = (ProgramInfo) obj;
- if (!mSelector.equals(other.getSelector())) return false;
- if (mTuned != other.isTuned())
- return false;
- if (mStereo != other.isStereo())
- return false;
- if (mDigital != other.isDigital())
- return false;
- if (mFlags != other.mFlags)
- return false;
- if (mSignalStrength != other.getSignalStrength())
- return false;
- if (mMetadata == null) {
- if (other.getMetadata() != null)
- return false;
- } else if (!mMetadata.equals(other.getMetadata()))
- return false;
- if (!mVendorInfo.equals(other.mVendorInfo)) return false;
+
+ if (!Objects.equals(mSelector, other.mSelector)) return false;
+ if (!Objects.equals(mLogicallyTunedTo, other.mLogicallyTunedTo)) return false;
+ if (!Objects.equals(mPhysicallyTunedTo, other.mPhysicallyTunedTo)) return false;
+ if (!Objects.equals(mRelatedContent, other.mRelatedContent)) return false;
+ if (mInfoFlags != other.mInfoFlags) return false;
+ if (mSignalQuality != other.mSignalQuality) return false;
+ if (!Objects.equals(mMetadata, other.mMetadata)) return false;
+ if (!Objects.equals(mVendorInfo, other.mVendorInfo)) return false;
+
return true;
}
}
@@ -1649,15 +1751,78 @@ public class RadioManager {
TunerCallbackAdapter halCallback = new TunerCallbackAdapter(callback, handler);
try {
tuner = mService.openTuner(moduleId, config, withAudio, halCallback);
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to open tuner", e);
+ } catch (RemoteException | IllegalArgumentException ex) {
+ Log.e(TAG, "Failed to open tuner", ex);
return null;
}
if (tuner == null) {
Log.e(TAG, "Failed to open tuner");
return null;
}
- return new TunerAdapter(tuner, config != null ? config.getType() : BAND_INVALID);
+ return new TunerAdapter(tuner, halCallback,
+ config != null ? config.getType() : BAND_INVALID);
+ }
+
+ private final Map<Announcement.OnListUpdatedListener, ICloseHandle> mAnnouncementListeners =
+ new HashMap<>();
+
+ /**
+ * Adds new announcement listener.
+ *
+ * @param enabledAnnouncementTypes a set of announcement types to listen to
+ * @param listener announcement listener
+ */
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ public void addAnnouncementListener(@NonNull Set<Integer> enabledAnnouncementTypes,
+ @NonNull Announcement.OnListUpdatedListener listener) {
+ addAnnouncementListener(cmd -> cmd.run(), enabledAnnouncementTypes, listener);
+ }
+
+ /**
+ * Adds new announcement listener with executor.
+ *
+ * @param executor the executor
+ * @param enabledAnnouncementTypes a set of announcement types to listen to
+ * @param listener announcement listener
+ */
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ public void addAnnouncementListener(@NonNull @CallbackExecutor Executor executor,
+ @NonNull Set<Integer> enabledAnnouncementTypes,
+ @NonNull Announcement.OnListUpdatedListener listener) {
+ Objects.requireNonNull(executor);
+ Objects.requireNonNull(listener);
+ int[] types = enabledAnnouncementTypes.stream().mapToInt(Integer::intValue).toArray();
+ IAnnouncementListener listenerIface = new IAnnouncementListener.Stub() {
+ public void onListUpdated(List<Announcement> activeAnnouncements) {
+ executor.execute(() -> listener.onListUpdated(activeAnnouncements));
+ }
+ };
+ synchronized (mAnnouncementListeners) {
+ ICloseHandle closeHandle = null;
+ try {
+ closeHandle = mService.addAnnouncementListener(types, listenerIface);
+ } catch (RemoteException ex) {
+ ex.rethrowFromSystemServer();
+ }
+ Objects.requireNonNull(closeHandle);
+ ICloseHandle oldCloseHandle = mAnnouncementListeners.put(listener, closeHandle);
+ if (oldCloseHandle != null) Utils.close(oldCloseHandle);
+ }
+ }
+
+ /**
+ * Removes previously registered announcement listener.
+ *
+ * @param listener announcement listener, previously registered with
+ * {@link addAnnouncementListener}
+ */
+ @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO)
+ public void removeAnnouncementListener(@NonNull Announcement.OnListUpdatedListener listener) {
+ Objects.requireNonNull(listener);
+ synchronized (mAnnouncementListeners) {
+ ICloseHandle closeHandle = mAnnouncementListeners.remove(listener);
+ if (closeHandle != null) Utils.close(closeHandle);
+ }
}
@NonNull private final Context mContext;
diff --git a/android/hardware/radio/RadioTuner.java b/android/hardware/radio/RadioTuner.java
index e93fd5f1..ed20c4aa 100644
--- a/android/hardware/radio/RadioTuner.java
+++ b/android/hardware/radio/RadioTuner.java
@@ -280,17 +280,37 @@ public abstract class RadioTuner {
* @throws IllegalStateException if the scan is in progress or has not been started,
* startBackgroundScan() call may fix it.
* @throws IllegalArgumentException if the vendorFilter argument is not valid.
+ * @deprecated Use {@link getDynamicProgramList} instead.
*/
+ @Deprecated
public abstract @NonNull List<RadioManager.ProgramInfo>
getProgramList(@Nullable Map<String, String> vendorFilter);
/**
+ * Get the dynamic list of discovered radio stations.
+ *
+ * The list object is updated asynchronously; to get the updates register
+ * with {@link ProgramList#addListCallback}.
+ *
+ * When the returned object is no longer used, it must be closed.
+ *
+ * @param filter filter for the list, or null to get the full list.
+ * @return the dynamic program list object, close it after use
+ * or {@code null} if program list is not supported by the tuner
+ */
+ public @Nullable ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) {
+ return null;
+ }
+
+ /**
* Checks, if the analog playback is forced, see setAnalogForced.
*
* @throws IllegalStateException if the switch is not supported at current
* configuration.
* @return {@code true} if analog is forced, {@code false} otherwise.
+ * @deprecated Use {@link isConfigFlagSet(int)} instead.
*/
+ @Deprecated
public abstract boolean isAnalogForced();
/**
@@ -305,10 +325,50 @@ public abstract class RadioTuner {
* @param isForced {@code true} to force analog, {@code false} for a default behaviour.
* @throws IllegalStateException if the switch is not supported at current
* configuration.
+ * @deprecated Use {@link setConfigFlag(int, boolean)} instead.
*/
+ @Deprecated
public abstract void setAnalogForced(boolean isForced);
/**
+ * Checks, if a given config flag is supported
+ *
+ * @param flag Flag to check.
+ * @return True, if the flag is supported.
+ */
+ public boolean isConfigFlagSupported(@RadioManager.ConfigFlag int flag) {
+ return false;
+ }
+
+ /**
+ * Fetches the current setting of a given config flag.
+ *
+ * The success/failure result is consistent with isConfigFlagSupported.
+ *
+ * @param flag Flag to fetch.
+ * @return The current value of the flag.
+ * @throws IllegalStateException if the flag is not applicable right now.
+ * @throws UnsupportedOperationException if the flag is not supported at all.
+ */
+ public boolean isConfigFlagSet(@RadioManager.ConfigFlag int flag) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Sets the config flag.
+ *
+ * The success/failure result is consistent with isConfigFlagSupported.
+ *
+ * @param flag Flag to set.
+ * @param value The new value of a given flag.
+ * @throws IllegalStateException if the flag is not applicable right now.
+ * @throws UnsupportedOperationException if the flag is not supported at all.
+ */
+ public void setConfigFlag(@RadioManager.ConfigFlag int flag, boolean value) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
* Generic method for setting vendor-specific parameter values.
* The framework does not interpret the parameters, they are passed
* in an opaque manner between a vendor application and HAL.
@@ -316,6 +376,7 @@ public abstract class RadioTuner {
* Framework does not make any assumptions on the keys or values, other than
* ones stated in VendorKeyValue documentation (a requirement of key
* prefixes).
+ * See VendorKeyValue at hardware/interfaces/broadcastradio/2.0/types.hal.
*
* For each pair in the result map, the key will be one of the keys
* contained in the input (possibly with wildcards expanded), and the value
@@ -332,10 +393,11 @@ public abstract class RadioTuner {
*
* @param parameters Vendor-specific key-value pairs.
* @return Operation completion status for parameters being set.
- * @hide FutureFeature
*/
- public abstract @NonNull Map<String, String>
- setParameters(@NonNull Map<String, String> parameters);
+ public @NonNull Map<String, String>
+ setParameters(@NonNull Map<String, String> parameters) {
+ throw new UnsupportedOperationException();
+ }
/**
* Generic method for retrieving vendor-specific parameter values.
@@ -355,10 +417,11 @@ public abstract class RadioTuner {
*
* @param keys Parameter keys to fetch.
* @return Vendor-specific key-value pairs.
- * @hide FutureFeature
*/
- public abstract @NonNull Map<String, String>
- getParameters(@NonNull List<String> keys);
+ public @NonNull Map<String, String>
+ getParameters(@NonNull List<String> keys) {
+ throw new UnsupportedOperationException();
+ }
/**
* Get current antenna connection state for current configuration.
@@ -494,7 +557,6 @@ public abstract class RadioTuner {
* asynchronously.
*
* @param parameters Vendor-specific key-value pairs.
- * @hide FutureFeature
*/
public void onParametersUpdated(@NonNull Map<String, String> parameters) {}
}
diff --git a/android/hardware/radio/TunerAdapter.java b/android/hardware/radio/TunerAdapter.java
index 864d17c2..91944bfd 100644
--- a/android/hardware/radio/TunerAdapter.java
+++ b/android/hardware/radio/TunerAdapter.java
@@ -33,15 +33,18 @@ class TunerAdapter extends RadioTuner {
private static final String TAG = "BroadcastRadio.TunerAdapter";
@NonNull private final ITuner mTuner;
+ @NonNull private final TunerCallbackAdapter mCallback;
private boolean mIsClosed = false;
private @RadioManager.Band int mBand;
- TunerAdapter(ITuner tuner, @RadioManager.Band int band) {
- if (tuner == null) {
- throw new NullPointerException();
- }
- mTuner = tuner;
+ private ProgramList mLegacyListProxy;
+ private Map<String, String> mLegacyListFilter;
+
+ TunerAdapter(@NonNull ITuner tuner, @NonNull TunerCallbackAdapter callback,
+ @RadioManager.Band int band) {
+ mTuner = Objects.requireNonNull(tuner);
+ mCallback = Objects.requireNonNull(callback);
mBand = band;
}
@@ -53,6 +56,10 @@ class TunerAdapter extends RadioTuner {
return;
}
mIsClosed = true;
+ if (mLegacyListProxy != null) {
+ mLegacyListProxy.close();
+ mLegacyListProxy = null;
+ }
}
try {
mTuner.close();
@@ -63,6 +70,7 @@ class TunerAdapter extends RadioTuner {
@Override
public int setConfiguration(RadioManager.BandConfig config) {
+ if (config == null) return RadioManager.STATUS_BAD_VALUE;
try {
mTuner.setConfiguration(config);
mBand = config.getType();
@@ -226,26 +234,90 @@ class TunerAdapter extends RadioTuner {
@Override
public @NonNull List<RadioManager.ProgramInfo>
getProgramList(@Nullable Map<String, String> vendorFilter) {
+ synchronized (mTuner) {
+ if (mLegacyListProxy == null || !Objects.equals(mLegacyListFilter, vendorFilter)) {
+ Log.i(TAG, "Program list filter has changed, requesting new list");
+ mLegacyListProxy = new ProgramList();
+ mLegacyListFilter = vendorFilter;
+
+ mCallback.clearLastCompleteList();
+ mCallback.setProgramListObserver(mLegacyListProxy, () -> { });
+ try {
+ mTuner.startProgramListUpdates(new ProgramList.Filter(vendorFilter));
+ } catch (RemoteException ex) {
+ throw new RuntimeException("service died", ex);
+ }
+ }
+
+ List<RadioManager.ProgramInfo> list = mCallback.getLastCompleteList();
+ if (list == null) throw new IllegalStateException("Program list is not ready yet");
+ return list;
+ }
+ }
+
+ @Override
+ public @Nullable ProgramList getDynamicProgramList(@Nullable ProgramList.Filter filter) {
+ synchronized (mTuner) {
+ if (mLegacyListProxy != null) {
+ mLegacyListProxy.close();
+ mLegacyListProxy = null;
+ }
+ mLegacyListFilter = null;
+
+ ProgramList list = new ProgramList();
+ mCallback.setProgramListObserver(list, () -> {
+ try {
+ mTuner.stopProgramListUpdates();
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Couldn't stop program list updates", ex);
+ }
+ });
+
+ try {
+ mTuner.startProgramListUpdates(filter);
+ } catch (UnsupportedOperationException ex) {
+ return null;
+ } catch (RemoteException ex) {
+ mCallback.setProgramListObserver(null, () -> { });
+ throw new RuntimeException("service died", ex);
+ }
+
+ return list;
+ }
+ }
+
+ @Override
+ public boolean isAnalogForced() {
+ return isConfigFlagSet(RadioManager.CONFIG_FORCE_ANALOG);
+ }
+
+ @Override
+ public void setAnalogForced(boolean isForced) {
+ setConfigFlag(RadioManager.CONFIG_FORCE_ANALOG, isForced);
+ }
+
+ @Override
+ public boolean isConfigFlagSupported(@RadioManager.ConfigFlag int flag) {
try {
- return mTuner.getProgramList(vendorFilter);
+ return mTuner.isConfigFlagSupported(flag);
} catch (RemoteException e) {
throw new RuntimeException("service died", e);
}
}
@Override
- public boolean isAnalogForced() {
+ public boolean isConfigFlagSet(@RadioManager.ConfigFlag int flag) {
try {
- return mTuner.isAnalogForced();
+ return mTuner.isConfigFlagSet(flag);
} catch (RemoteException e) {
throw new RuntimeException("service died", e);
}
}
@Override
- public void setAnalogForced(boolean isForced) {
+ public void setConfigFlag(@RadioManager.ConfigFlag int flag, boolean value) {
try {
- mTuner.setAnalogForced(isForced);
+ mTuner.setConfigFlag(flag, value);
} catch (RemoteException e) {
throw new RuntimeException("service died", e);
}
diff --git a/android/hardware/radio/TunerCallbackAdapter.java b/android/hardware/radio/TunerCallbackAdapter.java
index a01f658e..b299ffe0 100644
--- a/android/hardware/radio/TunerCallbackAdapter.java
+++ b/android/hardware/radio/TunerCallbackAdapter.java
@@ -22,7 +22,9 @@ import android.os.Handler;
import android.os.Looper;
import android.util.Log;
+import java.util.List;
import java.util.Map;
+import java.util.Objects;
/**
* Implements the ITunerCallback interface by forwarding calls to RadioTuner.Callback.
@@ -30,9 +32,14 @@ import java.util.Map;
class TunerCallbackAdapter extends ITunerCallback.Stub {
private static final String TAG = "BroadcastRadio.TunerCallbackAdapter";
+ private final Object mLock = new Object();
@NonNull private final RadioTuner.Callback mCallback;
@NonNull private final Handler mHandler;
+ @Nullable ProgramList mProgramList;
+ @Nullable List<RadioManager.ProgramInfo> mLastCompleteList; // for legacy getProgramList call
+ private boolean mDelayedCompleteCallback = false;
+
TunerCallbackAdapter(@NonNull RadioTuner.Callback callback, @Nullable Handler handler) {
mCallback = callback;
if (handler == null) {
@@ -42,6 +49,49 @@ class TunerCallbackAdapter extends ITunerCallback.Stub {
}
}
+ void setProgramListObserver(@Nullable ProgramList programList,
+ @NonNull ProgramList.OnCloseListener closeListener) {
+ Objects.requireNonNull(closeListener);
+ synchronized (mLock) {
+ if (mProgramList != null) {
+ Log.w(TAG, "Previous program list observer wasn't properly closed, closing it...");
+ mProgramList.close();
+ }
+ mProgramList = programList;
+ if (programList == null) return;
+ programList.setOnCloseListener(() -> {
+ synchronized (mLock) {
+ if (mProgramList != programList) return;
+ mProgramList = null;
+ mLastCompleteList = null;
+ closeListener.onClose();
+ }
+ });
+ programList.addOnCompleteListener(() -> {
+ synchronized (mLock) {
+ if (mProgramList != programList) return;
+ mLastCompleteList = programList.toList();
+ if (mDelayedCompleteCallback) {
+ Log.d(TAG, "Sending delayed onBackgroundScanComplete callback");
+ sendBackgroundScanCompleteLocked();
+ }
+ }
+ });
+ }
+ }
+
+ @Nullable List<RadioManager.ProgramInfo> getLastCompleteList() {
+ synchronized (mLock) {
+ return mLastCompleteList;
+ }
+ }
+
+ void clearLastCompleteList() {
+ synchronized (mLock) {
+ mLastCompleteList = null;
+ }
+ }
+
@Override
public void onError(int status) {
mHandler.post(() -> mCallback.onError(status));
@@ -87,9 +137,22 @@ class TunerCallbackAdapter extends ITunerCallback.Stub {
mHandler.post(() -> mCallback.onBackgroundScanAvailabilityChange(isAvailable));
}
+ private void sendBackgroundScanCompleteLocked() {
+ mDelayedCompleteCallback = false;
+ mHandler.post(() -> mCallback.onBackgroundScanComplete());
+ }
+
@Override
public void onBackgroundScanComplete() {
- mHandler.post(() -> mCallback.onBackgroundScanComplete());
+ synchronized (mLock) {
+ if (mLastCompleteList == null) {
+ Log.i(TAG, "Got onBackgroundScanComplete callback, but the "
+ + "program list didn't get through yet. Delaying it...");
+ mDelayedCompleteCallback = true;
+ return;
+ }
+ sendBackgroundScanCompleteLocked();
+ }
}
@Override
@@ -98,6 +161,14 @@ class TunerCallbackAdapter extends ITunerCallback.Stub {
}
@Override
+ public void onProgramListUpdated(ProgramList.Chunk chunk) {
+ synchronized (mLock) {
+ if (mProgramList == null) return;
+ mProgramList.apply(Objects.requireNonNull(chunk));
+ }
+ }
+
+ @Override
public void onParametersUpdated(Map parameters) {
mHandler.post(() -> mCallback.onParametersUpdated(parameters));
}
diff --git a/android/hardware/radio/Utils.java b/android/hardware/radio/Utils.java
new file mode 100644
index 00000000..f1b58974
--- /dev/null
+++ b/android/hardware/radio/Utils.java
@@ -0,0 +1,118 @@
+/**
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.hardware.radio;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+
+final class Utils {
+ private static final String TAG = "BroadcastRadio.utils";
+
+ static void writeStringMap(@NonNull Parcel dest, @Nullable Map<String, String> map) {
+ if (map == null) {
+ dest.writeInt(0);
+ return;
+ }
+ dest.writeInt(map.size());
+ for (Map.Entry<String, String> entry : map.entrySet()) {
+ dest.writeString(entry.getKey());
+ dest.writeString(entry.getValue());
+ }
+ }
+
+ static @NonNull Map<String, String> readStringMap(@NonNull Parcel in) {
+ int size = in.readInt();
+ Map<String, String> map = new HashMap<>();
+ while (size-- > 0) {
+ String key = in.readString();
+ String value = in.readString();
+ map.put(key, value);
+ }
+ return map;
+ }
+
+ static <T extends Parcelable> void writeSet(@NonNull Parcel dest, @Nullable Set<T> set) {
+ if (set == null) {
+ dest.writeInt(0);
+ return;
+ }
+ dest.writeInt(set.size());
+ set.stream().forEach(elem -> dest.writeTypedObject(elem, 0));
+ }
+
+ static <T> Set<T> createSet(@NonNull Parcel in, Parcelable.Creator<T> c) {
+ int size = in.readInt();
+ Set<T> set = new HashSet<>();
+ while (size-- > 0) {
+ set.add(in.readTypedObject(c));
+ }
+ return set;
+ }
+
+ static void writeIntSet(@NonNull Parcel dest, @Nullable Set<Integer> set) {
+ if (set == null) {
+ dest.writeInt(0);
+ return;
+ }
+ dest.writeInt(set.size());
+ set.stream().forEach(elem -> dest.writeInt(Objects.requireNonNull(elem)));
+ }
+
+ static Set<Integer> createIntSet(@NonNull Parcel in) {
+ return createSet(in, new Parcelable.Creator<Integer>() {
+ public Integer createFromParcel(Parcel in) {
+ return in.readInt();
+ }
+
+ public Integer[] newArray(int size) {
+ return new Integer[size];
+ }
+ });
+ }
+
+ static <T extends Parcelable> void writeTypedCollection(@NonNull Parcel dest,
+ @Nullable Collection<T> coll) {
+ ArrayList<T> list = null;
+ if (coll != null) {
+ if (coll instanceof ArrayList) {
+ list = (ArrayList) coll;
+ } else {
+ list = new ArrayList<>(coll);
+ }
+ }
+ dest.writeTypedList(list);
+ }
+
+ static void close(ICloseHandle handle) {
+ try {
+ handle.close();
+ } catch (RemoteException ex) {
+ ex.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/hardware/usb/UsbManager.java b/android/hardware/usb/UsbManager.java
index bdb90bcc..7617c2bd 100644
--- a/android/hardware/usb/UsbManager.java
+++ b/android/hardware/usb/UsbManager.java
@@ -601,6 +601,32 @@ public class UsbManager {
}
/**
+ * Sets the screen unlocked functions, which are persisted and set as the current functions
+ * whenever the screen is unlocked.
+ * <p>
+ * The allowed values are: {@link #USB_FUNCTION_NONE},
+ * {@link #USB_FUNCTION_MIDI}, {@link #USB_FUNCTION_MTP}, {@link #USB_FUNCTION_PTP},
+ * or {@link #USB_FUNCTION_RNDIS}.
+ * {@link #USB_FUNCTION_NONE} has the effect of switching off this feature, so functions
+ * no longer change on screen unlock.
+ * </p><p>
+ * Note: When the screen is on, this method will apply given functions as current functions,
+ * which is asynchronous and may fail silently without applying the requested changes.
+ * </p>
+ *
+ * @param function function to set as default
+ *
+ * {@hide}
+ */
+ public void setScreenUnlockedFunctions(String function) {
+ try {
+ mService.setScreenUnlockedFunctions(function);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns a list of physical USB ports on the device.
* <p>
* This list is guaranteed to contain all dual-role USB Type C ports but it might
diff --git a/android/inputmethodservice/InputMethodService.java b/android/inputmethodservice/InputMethodService.java
index 02b1c658..7528bc39 100644
--- a/android/inputmethodservice/InputMethodService.java
+++ b/android/inputmethodservice/InputMethodService.java
@@ -18,6 +18,7 @@ package android.inputmethodservice;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
import android.annotation.CallSuper;
import android.annotation.DrawableRes;
@@ -339,42 +340,35 @@ public class InputMethodService extends AbstractInputMethodService {
final Insets mTmpInsets = new Insets();
final int[] mTmpLocation = new int[2];
- final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer =
- new ViewTreeObserver.OnComputeInternalInsetsListener() {
- public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) {
- if (isExtractViewShown()) {
- // In true fullscreen mode, we just say the window isn't covering
- // any content so we don't impact whatever is behind.
- View decor = getWindow().getWindow().getDecorView();
- info.contentInsets.top = info.visibleInsets.top
- = decor.getHeight();
- info.touchableRegion.setEmpty();
- info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
- } else {
- onComputeInsets(mTmpInsets);
- info.contentInsets.top = mTmpInsets.contentTopInsets;
- info.visibleInsets.top = mTmpInsets.visibleTopInsets;
- info.touchableRegion.set(mTmpInsets.touchableRegion);
- info.setTouchableInsets(mTmpInsets.touchableInsets);
- }
+ final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsComputer = info -> {
+ if (isExtractViewShown()) {
+ // In true fullscreen mode, we just say the window isn't covering
+ // any content so we don't impact whatever is behind.
+ View decor = getWindow().getWindow().getDecorView();
+ info.contentInsets.top = info.visibleInsets.top = decor.getHeight();
+ info.touchableRegion.setEmpty();
+ info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
+ } else {
+ onComputeInsets(mTmpInsets);
+ info.contentInsets.top = mTmpInsets.contentTopInsets;
+ info.visibleInsets.top = mTmpInsets.visibleTopInsets;
+ info.touchableRegion.set(mTmpInsets.touchableRegion);
+ info.setTouchableInsets(mTmpInsets.touchableInsets);
}
};
- final View.OnClickListener mActionClickListener = new View.OnClickListener() {
- public void onClick(View v) {
- final EditorInfo ei = getCurrentInputEditorInfo();
- final InputConnection ic = getCurrentInputConnection();
- if (ei != null && ic != null) {
- if (ei.actionId != 0) {
- ic.performEditorAction(ei.actionId);
- } else if ((ei.imeOptions&EditorInfo.IME_MASK_ACTION)
- != EditorInfo.IME_ACTION_NONE) {
- ic.performEditorAction(ei.imeOptions&EditorInfo.IME_MASK_ACTION);
- }
+ final View.OnClickListener mActionClickListener = v -> {
+ final EditorInfo ei = getCurrentInputEditorInfo();
+ final InputConnection ic = getCurrentInputConnection();
+ if (ei != null && ic != null) {
+ if (ei.actionId != 0) {
+ ic.performEditorAction(ei.actionId);
+ } else if ((ei.imeOptions & EditorInfo.IME_MASK_ACTION) != EditorInfo.IME_ACTION_NONE) {
+ ic.performEditorAction(ei.imeOptions & EditorInfo.IME_MASK_ACTION);
}
}
};
-
+
/**
* Concrete implementation of
* {@link AbstractInputMethodService.AbstractInputMethodImpl} that provides
@@ -852,6 +846,11 @@ public class InputMethodService extends AbstractInputMethodService {
Context.LAYOUT_INFLATER_SERVICE);
mWindow = new SoftInputWindow(this, "InputMethod", mTheme, null, null, mDispatcherState,
WindowManager.LayoutParams.TYPE_INPUT_METHOD, Gravity.BOTTOM, false);
+ // For ColorView in DecorView to work, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS needs to be set
+ // by default (but IME developers can opt this out later if they want a new behavior).
+ mWindow.getWindow().setFlags(
+ FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS, FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+
initViews();
mWindow.getWindow().setLayout(MATCH_PARENT, WRAP_CONTENT);
}
@@ -882,8 +881,6 @@ public class InputMethodService extends AbstractInputMethodService {
mThemeAttrs = obtainStyledAttributes(android.R.styleable.InputMethodService);
mRootView = mInflater.inflate(
com.android.internal.R.layout.input_method, null);
- mRootView.setSystemUiVisibility(
- View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
mWindow.setContentView(mRootView);
mRootView.getViewTreeObserver().removeOnComputeInternalInsetsListener(mInsetsComputer);
mRootView.getViewTreeObserver().addOnComputeInternalInsetsListener(mInsetsComputer);
@@ -892,20 +889,20 @@ public class InputMethodService extends AbstractInputMethodService {
mWindow.getWindow().setWindowAnimations(
com.android.internal.R.style.Animation_InputMethodFancy);
}
- mFullscreenArea = (ViewGroup)mRootView.findViewById(com.android.internal.R.id.fullscreenArea);
+ mFullscreenArea = mRootView.findViewById(com.android.internal.R.id.fullscreenArea);
mExtractViewHidden = false;
- mExtractFrame = (FrameLayout)mRootView.findViewById(android.R.id.extractArea);
+ mExtractFrame = mRootView.findViewById(android.R.id.extractArea);
mExtractView = null;
mExtractEditText = null;
mExtractAccessories = null;
mExtractAction = null;
mFullscreenApplied = false;
-
- mCandidatesFrame = (FrameLayout)mRootView.findViewById(android.R.id.candidatesArea);
- mInputFrame = (FrameLayout)mRootView.findViewById(android.R.id.inputArea);
+
+ mCandidatesFrame = mRootView.findViewById(android.R.id.candidatesArea);
+ mInputFrame = mRootView.findViewById(android.R.id.inputArea);
mInputView = null;
mIsInputViewShown = false;
-
+
mExtractFrame.setVisibility(View.GONE);
mCandidatesVisibility = getCandidatesHiddenVisibility();
mCandidatesFrame.setVisibility(mCandidatesVisibility);
@@ -1085,33 +1082,6 @@ public class InputMethodService extends AbstractInputMethodService {
}
/**
- * 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.
@@ -1457,17 +1427,17 @@ public class InputMethodService extends AbstractInputMethodService {
public int getCandidatesHiddenVisibility() {
return isExtractViewShown() ? View.GONE : View.INVISIBLE;
}
-
+
public void showStatusIcon(@DrawableRes int iconResId) {
mStatusIcon = iconResId;
- mImm.showStatusIcon(mToken, getPackageName(), iconResId);
+ mImm.showStatusIconInternal(mToken, getPackageName(), iconResId);
}
-
+
public void hideStatusIcon() {
mStatusIcon = 0;
- mImm.hideStatusIcon(mToken);
+ mImm.hideStatusIconInternal(mToken);
}
-
+
/**
* Force switch to a new input method, as identified by <var>id</var>. This
* input method will be destroyed, and the requested one started on the
@@ -1476,9 +1446,9 @@ public class InputMethodService extends AbstractInputMethodService {
* @param id Unique identifier of the new input method ot start.
*/
public void switchInputMethod(String id) {
- mImm.setInputMethod(mToken, id);
+ mImm.setInputMethodInternal(mToken, id);
}
-
+
public void setExtractView(View view) {
mExtractFrame.removeAllViews();
mExtractFrame.addView(view, new FrameLayout.LayoutParams(
@@ -1486,13 +1456,13 @@ public class InputMethodService extends AbstractInputMethodService {
ViewGroup.LayoutParams.MATCH_PARENT));
mExtractView = view;
if (view != null) {
- mExtractEditText = (ExtractEditText)view.findViewById(
+ mExtractEditText = view.findViewById(
com.android.internal.R.id.inputExtractEditText);
mExtractEditText.setIME(this);
mExtractAction = view.findViewById(
com.android.internal.R.id.inputExtractAction);
if (mExtractAction != null) {
- mExtractAccessories = (ViewGroup)view.findViewById(
+ mExtractAccessories = view.findViewById(
com.android.internal.R.id.inputExtractAccessories);
}
startExtractingText(false);
@@ -1741,7 +1711,7 @@ public class InputMethodService extends AbstractInputMethodService {
// Rethrow the exception to preserve the existing behavior. Some IMEs may have directly
// called this method and relied on this exception for some clean-up tasks.
// TODO: Give developers a clear guideline of whether it's OK to call this method or
- // InputMethodManager#showSoftInputFromInputMethod() should always be used instead.
+ // InputMethodService#requestShowSelf(int) should always be used instead.
throw e;
} finally {
// TODO: Is it OK to set true when we get BadTokenException?
@@ -2063,27 +2033,30 @@ public class InputMethodService extends AbstractInputMethodService {
/**
* Close this input method's soft input area, removing it from the display.
- * The input method will continue running, but the user can no longer use
- * it to generate input by touching the screen.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link InputMethodManager#HIDE_IMPLICIT_ONLY
- * InputMethodManager.HIDE_IMPLICIT_ONLY} bit set.
+ *
+ * The input method will continue running, but the user can no longer use it to generate input
+ * by touching the screen.
+ *
+ * @see InputMethodManager#HIDE_IMPLICIT_ONLY
+ * @see InputMethodManager#HIDE_NOT_ALWAYS
+ * @param flags Provides additional operating flags.
*/
public void requestHideSelf(int flags) {
- mImm.hideSoftInputFromInputMethod(mToken, flags);
+ mImm.hideSoftInputFromInputMethodInternal(mToken, flags);
}
-
+
/**
- * Show the input method. This is a call back to the
- * IMF to handle showing the input method.
- * @param flags Provides additional operating flags. Currently may be
- * 0 or have the {@link InputMethodManager#SHOW_FORCED
- * InputMethodManager.} bit set.
+ * Show the input method's soft input area, so the user sees the input method window and can
+ * interact with it.
+ *
+ * @see InputMethodManager#SHOW_IMPLICIT
+ * @see InputMethodManager#SHOW_FORCED
+ * @param flags Provides additional operating flags.
*/
- private void requestShowSelf(int flags) {
- mImm.showSoftInputFromInputMethod(mToken, flags);
+ public void requestShowSelf(int flags) {
+ mImm.showSoftInputFromInputMethodInternal(mToken, flags);
}
-
+
private boolean handleBack(boolean doIt) {
if (mShowInputRequested) {
// If the soft input area is shown, back closes it and we
@@ -2750,7 +2723,7 @@ public class InputMethodService extends AbstractInputMethodService {
* application.
* This cannot be {@code null}.
* @param inputConnection {@link InputConnection} with which
- * {@link InputConnection#commitContent(InputContentInfo, Bundle)} will be called.
+ * {@link InputConnection#commitContent(InputContentInfo, int, Bundle)} will be called.
* @hide
*/
@Override
diff --git a/android/location/LocationManager.java b/android/location/LocationManager.java
index 4802b235..9db9d332 100644
--- a/android/location/LocationManager.java
+++ b/android/location/LocationManager.java
@@ -16,7 +16,10 @@
package android.location;
-import com.android.internal.location.ProviderProperties;
+import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.Manifest.permission.LOCATION_HARDWARE;
+import static android.Manifest.permission.WRITE_SECURE_SETTINGS;
import android.Manifest;
import android.annotation.NonNull;
@@ -24,7 +27,6 @@ import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
-import android.annotation.TestApi;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
@@ -33,16 +35,15 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.Process;
import android.os.RemoteException;
+import android.os.UserHandle;
import android.util.Log;
-
+import com.android.internal.location.ProviderProperties;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
-import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
-import static android.Manifest.permission.ACCESS_FINE_LOCATION;
-
/**
* This class provides access to the system location services. These
* services allow applications to obtain periodic updates of the
@@ -882,6 +883,34 @@ public class LocationManager {
requestLocationUpdates(request, null, null, intent);
}
+ /**
+ * Set the last known location with a new location.
+ *
+ * <p>A privileged client can inject a {@link Location} if it has a better estimate of what
+ * the recent location is. This is especially useful when the device boots up and the GPS
+ * chipset is in the process of getting the first fix. If the client has cached the location,
+ * it can inject the {@link Location}, so if an app requests for a {@link Location} from {@link
+ * #getLastKnownLocation(String)}, the location information is still useful before getting
+ * the first fix.</p>
+ *
+ * <p> Useful in products like Auto.
+ *
+ * @param newLocation newly available {@link Location} object
+ * @return true if update was successful, false if not
+ *
+ * @throws SecurityException if no suitable permission is present
+ *
+ * @hide
+ */
+ @RequiresPermission(allOf = {LOCATION_HARDWARE, ACCESS_FINE_LOCATION})
+ public boolean injectLocation(Location newLocation) {
+ try {
+ return mService.injectLocation(newLocation);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
private ListenerTransport wrapListener(LocationListener listener, Looper looper) {
if (listener == null) return null;
synchronized (mListeners) {
@@ -1142,13 +1171,57 @@ public class LocationManager {
}
/**
+ * Returns the current enabled/disabled status of location
+ *
+ * @return true if location is enabled. false if location is disabled.
+ */
+ public boolean isLocationEnabled() {
+ return isLocationEnabledForUser(Process.myUserHandle());
+ }
+
+ /**
+ * Method for enabling or disabling location.
+ *
+ * @param enabled true to enable location. false to disable location
+ * @param userHandle the user to set
+ * @return true if the value was set, false on database errors
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(WRITE_SECURE_SETTINGS)
+ public void setLocationEnabledForUser(boolean enabled, UserHandle userHandle) {
+ try {
+ mService.setLocationEnabledForUser(enabled, userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Returns the current enabled/disabled status of location
+ *
+ * @param userHandle the user to query
+ * @return true location is enabled. false if location is disabled.
+ *
+ * @hide
+ */
+ @SystemApi
+ public boolean isLocationEnabledForUser(UserHandle userHandle) {
+ try {
+ return mService.isLocationEnabledForUser(userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns the current enabled/disabled status of the given provider.
*
* <p>If the user has enabled this provider in the Settings menu, true
* is returned otherwise false is returned
*
- * <p>Callers should instead use
- * {@link android.provider.Settings.Secure#LOCATION_MODE}
+ * <p>Callers should instead use {@link #isLocationEnabled()}
* unless they depend on provider-specific APIs such as
* {@link #requestLocationUpdates(String, long, float, LocationListener)}.
*
@@ -1173,6 +1246,64 @@ public class LocationManager {
}
/**
+ * Returns the current enabled/disabled status of the given provider and user.
+ *
+ * <p>If the user has enabled this provider in the Settings menu, true
+ * is returned otherwise false is returned
+ *
+ * <p>Callers should instead use {@link #isLocationEnabled()}
+ * unless they depend on provider-specific APIs such as
+ * {@link #requestLocationUpdates(String, long, float, LocationListener)}.
+ *
+ * <p>
+ * Before API version {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this
+ * method would throw {@link SecurityException} if the location permissions
+ * were not sufficient to use the specified provider.
+ *
+ * @param provider the name of the provider
+ * @param userHandle the user to query
+ * @return true if the provider exists and is enabled
+ *
+ * @throws IllegalArgumentException if provider is null
+ * @hide
+ */
+ @SystemApi
+ public boolean isProviderEnabledForUser(String provider, UserHandle userHandle) {
+ checkProvider(provider);
+
+ try {
+ return mService.isProviderEnabledForUser(provider, userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Method for enabling or disabling a single location provider.
+ *
+ * @param provider the name of the provider
+ * @param enabled true to enable the provider. false to disable the provider
+ * @param userHandle the user to set
+ * @return true if the value was set, false on database errors
+ *
+ * @throws IllegalArgumentException if provider is null
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(WRITE_SECURE_SETTINGS)
+ public boolean setProviderEnabledForUser(
+ String provider, boolean enabled, UserHandle userHandle) {
+ checkProvider(provider);
+
+ try {
+ return mService.setProviderEnabledForUser(
+ provider, enabled, userHandle.getIdentifier());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Get the last known location.
*
* <p>This location could be very old so use
diff --git a/android/media/AudioAttributes.java b/android/media/AudioAttributes.java
index e0289f0b..44a2ff9e 100644
--- a/android/media/AudioAttributes.java
+++ b/android/media/AudioAttributes.java
@@ -180,6 +180,7 @@ public final class AudioAttributes implements Parcelable {
/**
* IMPORTANT: when adding new usage types, add them to SDK_USAGES and update SUPPRESSIBLE_USAGES
* if applicable, as well as audioattributes.proto.
+ * Also consider adding them to <aaudio/AAudio.h> for the NDK.
*/
/**
@@ -879,7 +880,9 @@ public final class AudioAttributes implements Parcelable {
}
/** @hide */
- public void toProto(ProtoOutputStream proto) {
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
proto.write(AudioAttributesProto.USAGE, mUsage);
proto.write(AudioAttributesProto.CONTENT_TYPE, mContentType);
proto.write(AudioAttributesProto.FLAGS, mFlags);
@@ -891,6 +894,8 @@ public final class AudioAttributes implements Parcelable {
}
}
// TODO: is the data in mBundle useful for debugging?
+
+ proto.end(token);
}
/** @hide */
diff --git a/android/media/AudioFocusInfo.java b/android/media/AudioFocusInfo.java
index 6d9c5e2a..5d0c8e23 100644
--- a/android/media/AudioFocusInfo.java
+++ b/android/media/AudioFocusInfo.java
@@ -130,13 +130,11 @@ public final class AudioFocusInfo implements Parcelable {
dest.writeInt(mSdkTarget);
}
- @SystemApi
@Override
public int hashCode() {
return Objects.hash(mAttributes, mClientUid, mClientId, mPackageName, mGainRequest, mFlags);
}
- @SystemApi
@Override
public boolean equals(Object obj) {
if (this == obj)
diff --git a/android/media/AudioFocusRequest.java b/android/media/AudioFocusRequest.java
index de59ac39..7104dad4 100644
--- a/android/media/AudioFocusRequest.java
+++ b/android/media/AudioFocusRequest.java
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.media.AudioManager.OnAudioFocusChangeListener;
+import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -220,6 +221,9 @@ public final class AudioFocusRequest {
private final static AudioAttributes FOCUS_DEFAULT_ATTR = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_MEDIA).build();
+ /** @hide */
+ public static final String KEY_ACCESSIBILITY_FORCE_FOCUS_DUCKING = "a11y_force_ducking";
+
private final OnAudioFocusChangeListener mFocusListener; // may be null
private final Handler mListenerHandler; // may be null
private final AudioAttributes mAttr; // never null
@@ -349,6 +353,7 @@ public final class AudioFocusRequest {
private boolean mPausesOnDuck = false;
private boolean mDelayedFocus = false;
private boolean mFocusLocked = false;
+ private boolean mA11yForceDucking = false;
/**
* Constructs a new {@code Builder}, and specifies how audio focus
@@ -526,6 +531,21 @@ public final class AudioFocusRequest {
}
/**
+ * Marks this focus request as forcing ducking, regardless of the conditions in which
+ * the system would or would not enforce ducking.
+ * Forcing ducking will only be honored when requesting AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+ * with an {@link AudioAttributes} usage of
+ * {@link AudioAttributes#USAGE_ASSISTANCE_ACCESSIBILITY}, coming from an accessibility
+ * service, and will be ignored otherwise.
+ * @param forceDucking {@code true} to force ducking
+ * @return this {@code Builder} instance
+ */
+ public @NonNull Builder setForceDucking(boolean forceDucking) {
+ mA11yForceDucking = forceDucking;
+ return this;
+ }
+
+ /**
* Builds a new {@code AudioFocusRequest} instance combining all the information gathered
* by this {@code Builder}'s configuration methods.
* @return the {@code AudioFocusRequest} instance qualified by all the properties set
@@ -538,6 +558,17 @@ public final class AudioFocusRequest {
throw new IllegalStateException(
"Can't use delayed focus or pause on duck without a listener");
}
+ if (mA11yForceDucking) {
+ final Bundle extraInfo;
+ if (mAttr.getBundle() == null) {
+ extraInfo = new Bundle();
+ } else {
+ extraInfo = mAttr.getBundle();
+ }
+ // checking of usage and focus request is done server side
+ extraInfo.putBoolean(KEY_ACCESSIBILITY_FORCE_FOCUS_DUCKING, true);
+ mAttr = new AudioAttributes.Builder(mAttr).addBundle(extraInfo).build();
+ }
final int flags = 0
| (mDelayedFocus ? AudioManager.AUDIOFOCUS_FLAG_DELAY_OK : 0)
| (mPausesOnDuck ? AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS : 0)
diff --git a/android/media/AudioFormat.java b/android/media/AudioFormat.java
index 93fc3da5..b07d0422 100644
--- a/android/media/AudioFormat.java
+++ b/android/media/AudioFormat.java
@@ -238,22 +238,15 @@ public final class AudioFormat implements Parcelable {
public static final int ENCODING_DTS = 7;
/** Audio data format: DTS HD compressed */
public static final int ENCODING_DTS_HD = 8;
- /** Audio data format: MP3 compressed
- * @hide
- * */
+ /** Audio data format: MP3 compressed */
public static final int ENCODING_MP3 = 9;
- /** Audio data format: AAC LC compressed
- * @hide
- * */
+ /** Audio data format: AAC LC compressed */
public static final int ENCODING_AAC_LC = 10;
- /** Audio data format: AAC HE V1 compressed
- * @hide
- * */
+ /** Audio data format: AAC HE V1 compressed */
public static final int ENCODING_AAC_HE_V1 = 11;
- /** Audio data format: AAC HE V2 compressed
- * @hide
- * */
+ /** Audio data format: AAC HE V2 compressed */
public static final int ENCODING_AAC_HE_V2 = 12;
+
/** Audio data format: compressed audio wrapped in PCM for HDMI
* or S/PDIF passthrough.
* IEC61937 uses a stereo stream of 16-bit samples as the wrapper.
@@ -266,6 +259,12 @@ public final class AudioFormat implements Parcelable {
/** Audio data format: DOLBY TRUEHD compressed
**/
public static final int ENCODING_DOLBY_TRUEHD = 14;
+ /** Audio data format: AAC ELD compressed */
+ public static final int ENCODING_AAC_ELD = 15;
+ /** Audio data format: AAC xHE compressed */
+ public static final int ENCODING_AAC_XHE = 16;
+ /** Audio data format: AC-4 sync frame transport format */
+ public static final int ENCODING_AC4 = 17;
/** @hide */
public static String toLogFriendlyEncoding(int enc) {
@@ -298,6 +297,12 @@ public final class AudioFormat implements Parcelable {
return "ENCODING_IEC61937";
case ENCODING_DOLBY_TRUEHD:
return "ENCODING_DOLBY_TRUEHD";
+ case ENCODING_AAC_ELD:
+ return "ENCODING_AAC_ELD";
+ case ENCODING_AAC_XHE:
+ return "ENCODING_AAC_XHE";
+ case ENCODING_AC4:
+ return "ENCODING_AC4";
default :
return "invalid encoding " + enc;
}
@@ -514,6 +519,9 @@ public final class AudioFormat implements Parcelable {
case ENCODING_AAC_HE_V1:
case ENCODING_AAC_HE_V2:
case ENCODING_IEC61937:
+ case ENCODING_AAC_ELD:
+ case ENCODING_AAC_XHE:
+ case ENCODING_AC4:
return true;
default:
return false;
@@ -532,6 +540,13 @@ public final class AudioFormat implements Parcelable {
case ENCODING_DTS:
case ENCODING_DTS_HD:
case ENCODING_IEC61937:
+ case ENCODING_MP3:
+ case ENCODING_AAC_LC:
+ case ENCODING_AAC_HE_V1:
+ case ENCODING_AAC_HE_V2:
+ case ENCODING_AAC_ELD:
+ case ENCODING_AAC_XHE:
+ case ENCODING_AC4:
return true;
default:
return false;
@@ -556,6 +571,9 @@ public final class AudioFormat implements Parcelable {
case ENCODING_AAC_HE_V1:
case ENCODING_AAC_HE_V2:
case ENCODING_IEC61937: // wrapped in PCM but compressed
+ case ENCODING_AAC_ELD:
+ case ENCODING_AAC_XHE:
+ case ENCODING_AC4:
return false;
case ENCODING_INVALID:
default:
@@ -581,6 +599,9 @@ public final class AudioFormat implements Parcelable {
case ENCODING_AAC_LC:
case ENCODING_AAC_HE_V1:
case ENCODING_AAC_HE_V2:
+ case ENCODING_AAC_ELD:
+ case ENCODING_AAC_XHE:
+ case ENCODING_AC4:
return false;
case ENCODING_INVALID:
default:
@@ -794,14 +815,7 @@ public final class AudioFormat implements Parcelable {
/**
* Sets the data encoding format.
- * @param encoding one of {@link AudioFormat#ENCODING_DEFAULT},
- * {@link AudioFormat#ENCODING_PCM_8BIT},
- * {@link AudioFormat#ENCODING_PCM_16BIT},
- * {@link AudioFormat#ENCODING_PCM_FLOAT},
- * {@link AudioFormat#ENCODING_AC3},
- * {@link AudioFormat#ENCODING_E_AC3}.
- * {@link AudioFormat#ENCODING_DTS},
- * {@link AudioFormat#ENCODING_DTS_HD}.
+ * @param encoding the specified encoding or default.
* @return the same Builder instance.
* @throws java.lang.IllegalArgumentException
*/
@@ -818,6 +832,13 @@ public final class AudioFormat implements Parcelable {
case ENCODING_DTS:
case ENCODING_DTS_HD:
case ENCODING_IEC61937:
+ case ENCODING_MP3:
+ case ENCODING_AAC_LC:
+ case ENCODING_AAC_HE_V1:
+ case ENCODING_AAC_HE_V2:
+ case ENCODING_AAC_ELD:
+ case ENCODING_AAC_XHE:
+ case ENCODING_AC4:
mEncoding = encoding;
break;
case ENCODING_INVALID:
@@ -1016,7 +1037,7 @@ public final class AudioFormat implements Parcelable {
}
/** @hide */
- @IntDef({
+ @IntDef(flag = false, prefix = "ENCODING", value = {
ENCODING_DEFAULT,
ENCODING_PCM_8BIT,
ENCODING_PCM_16BIT,
@@ -1025,8 +1046,14 @@ public final class AudioFormat implements Parcelable {
ENCODING_E_AC3,
ENCODING_DTS,
ENCODING_DTS_HD,
- ENCODING_IEC61937
- })
+ ENCODING_IEC61937,
+ ENCODING_AAC_HE_V1,
+ ENCODING_AAC_HE_V2,
+ ENCODING_AAC_LC,
+ ENCODING_AAC_ELD,
+ ENCODING_AAC_XHE,
+ ENCODING_AC4 }
+ )
@Retention(RetentionPolicy.SOURCE)
public @interface Encoding {}
diff --git a/android/media/AudioManager.java b/android/media/AudioManager.java
index 913b5e84..2ac4063d 100644
--- a/android/media/AudioManager.java
+++ b/android/media/AudioManager.java
@@ -1329,6 +1329,19 @@ public class AudioManager {
}
//====================================================================
+ // Offload query
+ /**
+ * Returns whether offloaded playback of an audio format is supported on the device.
+ * Offloaded playback is where the decoding of an audio stream is not competing with other
+ * software resources. In general, it is supported by dedicated hardware, such as audio DSPs.
+ * @param format the audio format (codec, sample rate, channels) being checked.
+ * @return true if the given audio format can be offloaded.
+ */
+ public boolean isOffloadedPlaybackSupported(@NonNull AudioFormat format) {
+ return AudioSystem.isOffloadSupported(format);
+ }
+
+ //====================================================================
// Bluetooth SCO control
/**
* Sticky broadcast intent action indicating that the Bluetooth SCO audio
@@ -3746,6 +3759,33 @@ public class AudioManager {
}
/**
+ * Indicate A2DP source or sink connection state change and eventually suppress
+ * the {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent.
+ * @param device Bluetooth device connected/disconnected
+ * @param state new connection state (BluetoothProfile.STATE_xxx)
+ * @param profile profile for the A2DP device
+ * (either {@link android.bluetooth.BluetoothProfile.A2DP} or
+ * {@link android.bluetooth.BluetoothProfile.A2DP_SINK})
+ * @param suppressNoisyIntent if true the
+ * {@link AudioManager.ACTION_AUDIO_BECOMING_NOISY} intent will not be sent.
+ * @return a delay in ms that the caller should wait before broadcasting
+ * BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED intent.
+ * {@hide}
+ */
+ public int setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
+ BluetoothDevice device, int state, int profile, boolean suppressNoisyIntent) {
+ final IAudioService service = getService();
+ int delay = 0;
+ try {
+ delay = service.setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(device,
+ state, profile, suppressNoisyIntent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ return delay;
+ }
+
+ /**
* Indicate A2DP device configuration has changed.
* @param device Bluetooth device whose configuration has changed.
* {@hide}
diff --git a/android/media/AudioPort.java b/android/media/AudioPort.java
index 19bf51d9..047db194 100644
--- a/android/media/AudioPort.java
+++ b/android/media/AudioPort.java
@@ -20,7 +20,7 @@ package android.media;
* An audio port is a node of the audio framework or hardware that can be connected to or
* disconnect from another audio node to create a specific audio routing configuration.
* Examples of audio ports are an output device (speaker) or an output mix (see AudioMixPort).
- * All attributes that are relevant for applications to make routing selection are decribed
+ * All attributes that are relevant for applications to make routing selection are described
* in an AudioPort, in particular:
* - possible channel mask configurations.
* - audio format (PCM 16bit, PCM 24bit...)
@@ -173,6 +173,7 @@ public class AudioPort {
/**
* Build a specific configuration of this audio port for use by methods
* like AudioManager.connectAudioPatch().
+ * @param samplingRate
* @param channelMask The desired channel mask. AudioFormat.CHANNEL_OUT_DEFAULT if no change
* from active configuration requested.
* @param format The desired audio format. AudioFormat.ENCODING_DEFAULT if no change
diff --git a/android/media/AudioSystem.java b/android/media/AudioSystem.java
index e56944df..dcd37daf 100644
--- a/android/media/AudioSystem.java
+++ b/android/media/AudioSystem.java
@@ -16,6 +16,7 @@
package android.media;
+import android.annotation.NonNull;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.audiopolicy.AudioMix;
@@ -792,7 +793,7 @@ public class AudioSystem
public static native int getPrimaryOutputFrameCount();
public static native int getOutputLatency(int stream);
- public static native int setLowRamDevice(boolean isLowRamDevice);
+ public static native int setLowRamDevice(boolean isLowRamDevice, long totalMemory);
public static native int checkAudioFlinger();
public static native int listAudioPorts(ArrayList<AudioPort> ports, int[] generation);
@@ -818,6 +819,14 @@ public class AudioSystem
public static native float getStreamVolumeDB(int stream, int index, int device);
+ static boolean isOffloadSupported(@NonNull AudioFormat format) {
+ return native_is_offload_supported(format.getEncoding(), format.getSampleRate(),
+ format.getChannelMask(), format.getChannelIndexMask());
+ }
+
+ private static native boolean native_is_offload_supported(int encoding, int sampleRate,
+ int channelMask, int channelIndexMask);
+
// Items shared with audio service
/**
@@ -914,7 +923,8 @@ public class AudioSystem
(1 << STREAM_MUSIC) |
(1 << STREAM_RING) |
(1 << STREAM_NOTIFICATION) |
- (1 << STREAM_SYSTEM);
+ (1 << STREAM_SYSTEM) |
+ (1 << STREAM_VOICE_CALL);
/**
* Event posted by AudioTrack and AudioRecord JNI (JNIDeviceCallback) when routing changes.
diff --git a/android/media/AudioTrack.java b/android/media/AudioTrack.java
index e535fdf5..5928d03d 100644
--- a/android/media/AudioTrack.java
+++ b/android/media/AudioTrack.java
@@ -24,7 +24,9 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.NioUtils;
import java.util.Collection;
+import java.util.concurrent.Executor;
+import android.annotation.CallbackExecutor;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -185,6 +187,22 @@ public class AudioTrack extends PlayerBase
* Event id denotes when previously set update period has elapsed during playback.
*/
private static final int NATIVE_EVENT_NEW_POS = 4;
+ /**
+ * Callback for more data
+ * TODO only for offload
+ */
+ private static final int NATIVE_EVENT_MORE_DATA = 0;
+ /**
+ * IAudioTrack tear down for offloaded tracks
+ * TODO: when received, java AudioTrack must be released
+ */
+ private static final int NATIVE_EVENT_NEW_IAUDIOTRACK = 6;
+ /**
+ * Event id denotes when all the buffers queued in AF and HW are played
+ * back (after stop is called) for an offloaded track.
+ * TODO: not just for offload
+ */
+ private static final int NATIVE_EVENT_STREAM_END = 7;
private final static String TAG = "android.media.AudioTrack";
@@ -540,6 +558,12 @@ public class AudioTrack extends PlayerBase
public AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
int mode, int sessionId)
throws IllegalArgumentException {
+ this(attributes, format, bufferSizeInBytes, mode, sessionId, false /*offload*/);
+ }
+
+ private AudioTrack(AudioAttributes attributes, AudioFormat format, int bufferSizeInBytes,
+ int mode, int sessionId, boolean offload)
+ throws IllegalArgumentException {
super(attributes, AudioPlaybackConfiguration.PLAYER_TYPE_JAM_AUDIOTRACK);
// mState already == STATE_UNINITIALIZED
@@ -601,7 +625,8 @@ public class AudioTrack extends PlayerBase
// native initialization
int initResult = native_setup(new WeakReference<AudioTrack>(this), mAttributes,
sampleRate, mChannelMask, mChannelIndexMask, mAudioFormat,
- mNativeBufferSizeInBytes, mDataLoadMode, session, 0 /*nativeTrackInJavaObj*/);
+ mNativeBufferSizeInBytes, mDataLoadMode, session, 0 /*nativeTrackInJavaObj*/,
+ offload);
if (initResult != SUCCESS) {
loge("Error code "+initResult+" when initializing AudioTrack.");
return; // with mState == STATE_UNINITIALIZED
@@ -681,7 +706,8 @@ public class AudioTrack extends PlayerBase
0 /*mNativeBufferSizeInBytes - NA*/,
0 /*mDataLoadMode - NA*/,
session,
- nativeTrackInJavaObj);
+ nativeTrackInJavaObj,
+ false /*offload*/);
if (initResult != SUCCESS) {
loge("Error code "+initResult+" when initializing AudioTrack.");
return; // with mState == STATE_UNINITIALIZED
@@ -729,6 +755,7 @@ public class AudioTrack extends PlayerBase
* <code>MODE_STREAM</code> will be used.
* <br>If the session ID is not specified with {@link #setSessionId(int)}, a new one will
* be generated.
+ * <br>Offload is false by default.
*/
public static class Builder {
private AudioAttributes mAttributes;
@@ -737,6 +764,7 @@ public class AudioTrack extends PlayerBase
private int mSessionId = AudioManager.AUDIO_SESSION_ID_GENERATE;
private int mMode = MODE_STREAM;
private int mPerformanceMode = PERFORMANCE_MODE_NONE;
+ private boolean mOffload = false;
/**
* Constructs a new Builder with the default values as described above.
@@ -867,6 +895,21 @@ public class AudioTrack extends PlayerBase
}
/**
+ * Sets whether this track will play through the offloaded audio path.
+ * When set to true, at build time, the audio format will be checked against
+ * {@link AudioManager#isOffloadedPlaybackSupported(AudioFormat)} to verify the audio format
+ * used by this track is supported on the device's offload path (if any).
+ * <br>Offload is only supported for media audio streams, and therefore requires that
+ * the usage be {@link AudioAttributes#USAGE_MEDIA}.
+ * @param offload true to require the offload path for playback.
+ * @return the same Builder instance.
+ */
+ public @NonNull Builder setOffloadedPlayback(boolean offload) {
+ mOffload = offload;
+ return this;
+ }
+
+ /**
* Builds an {@link AudioTrack} instance initialized with all the parameters set
* on this <code>Builder</code>.
* @return a new successfully initialized {@link AudioTrack} instance.
@@ -909,6 +952,19 @@ public class AudioTrack extends PlayerBase
.setEncoding(AudioFormat.ENCODING_DEFAULT)
.build();
}
+
+ //TODO tie offload to PERFORMANCE_MODE_POWER_SAVING?
+ if (mOffload) {
+ if (mAttributes.getUsage() != AudioAttributes.USAGE_MEDIA) {
+ throw new UnsupportedOperationException(
+ "Cannot create AudioTrack, offload requires USAGE_MEDIA");
+ }
+ if (!AudioSystem.isOffloadSupported(mFormat)) {
+ throw new UnsupportedOperationException(
+ "Cannot create AudioTrack, offload format not supported");
+ }
+ }
+
try {
// If the buffer size is not specified in streaming mode,
// use a single frame for the buffer size and let the
@@ -918,7 +974,7 @@ public class AudioTrack extends PlayerBase
* mFormat.getBytesPerSample(mFormat.getEncoding());
}
final AudioTrack track = new AudioTrack(
- mAttributes, mFormat, mBufferSizeInBytes, mMode, mSessionId);
+ mAttributes, mFormat, mBufferSizeInBytes, mMode, mSessionId, mOffload);
if (track.getState() == STATE_UNINITIALIZED) {
// release is not necessary
throw new UnsupportedOperationException("Cannot create AudioTrack");
@@ -2882,6 +2938,69 @@ public class AudioTrack extends PlayerBase
void onPeriodicNotification(AudioTrack track);
}
+ /**
+ * Abstract class to receive event notification about the stream playback.
+ * See {@link AudioTrack#setStreamEventCallback(Executor, StreamEventCallback)} to register
+ * the callback on the given {@link AudioTrack} instance.
+ */
+ public abstract static class StreamEventCallback {
+ /** @hide */ // add hidden empty constructor so it doesn't show in SDK
+ public StreamEventCallback() { }
+ /**
+ * Called when an offloaded track is no longer valid and has been discarded by the system.
+ * An example of this happening is when an offloaded track has been paused too long, and
+ * gets invalidated by the system to prevent any other offload.
+ * @param track the {@link AudioTrack} on which the event happened
+ */
+ public void onTearDown(AudioTrack track) { }
+ /**
+ * Called when all the buffers of an offloaded track that were queued in the audio system
+ * (e.g. the combination of the Android audio framework and the device's audio hardware)
+ * have been played after {@link AudioTrack#stop()} has been called.
+ * @param track the {@link AudioTrack} on which the event happened
+ */
+ public void onStreamPresentationEnd(AudioTrack track) { }
+ /**
+ * Called when more audio data can be written without blocking on an offloaded track.
+ * @param track the {@link AudioTrack} on which the event happened
+ */
+ public void onStreamDataRequest(AudioTrack track) { }
+ }
+
+ private Executor mStreamEventExec;
+ private StreamEventCallback mStreamEventCb;
+ private final Object mStreamEventCbLock = new Object();
+
+ /**
+ * Sets the callback for the notification of stream events.
+ * @param executor {@link Executor} to handle the callbacks
+ * @param eventCallback the callback to receive the stream event notifications
+ */
+ public void setStreamEventCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull StreamEventCallback eventCallback) {
+ if (eventCallback == null) {
+ throw new IllegalArgumentException("Illegal null StreamEventCallback");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Illegal null Executor for the StreamEventCallback");
+ }
+ synchronized (mStreamEventCbLock) {
+ mStreamEventExec = executor;
+ mStreamEventCb = eventCallback;
+ }
+ }
+
+ /**
+ * Unregisters the callback for notification of stream events, previously set
+ * by {@link #setStreamEventCallback(Executor, StreamEventCallback)}.
+ */
+ public void removeStreamEventCallback() {
+ synchronized (mStreamEventCbLock) {
+ mStreamEventExec = null;
+ mStreamEventCb = null;
+ }
+ }
+
//---------------------------------------------------------
// Inner classes
//--------------------
@@ -2965,7 +3084,7 @@ public class AudioTrack extends PlayerBase
private static void postEventFromNative(Object audiotrack_ref,
int what, int arg1, int arg2, Object obj) {
//logd("Event posted from the native side: event="+ what + " args="+ arg1+" "+arg2);
- AudioTrack track = (AudioTrack)((WeakReference)audiotrack_ref).get();
+ final AudioTrack track = (AudioTrack)((WeakReference)audiotrack_ref).get();
if (track == null) {
return;
}
@@ -2974,6 +3093,32 @@ public class AudioTrack extends PlayerBase
track.broadcastRoutingChange();
return;
}
+
+ if (what == NATIVE_EVENT_MORE_DATA || what == NATIVE_EVENT_NEW_IAUDIOTRACK
+ || what == NATIVE_EVENT_STREAM_END) {
+ final Executor exec;
+ final StreamEventCallback cb;
+ synchronized (track.mStreamEventCbLock) {
+ exec = track.mStreamEventExec;
+ cb = track.mStreamEventCb;
+ }
+ if ((exec == null) || (cb == null)) {
+ return;
+ }
+ switch (what) {
+ case NATIVE_EVENT_MORE_DATA:
+ exec.execute(() -> cb.onStreamDataRequest(track));
+ return;
+ case NATIVE_EVENT_NEW_IAUDIOTRACK:
+ // TODO also release track as it's not longer usable
+ exec.execute(() -> cb.onTearDown(track));
+ return;
+ case NATIVE_EVENT_STREAM_END:
+ exec.execute(() -> cb.onStreamPresentationEnd(track));
+ return;
+ }
+ }
+
NativePositionEventHandlerDelegate delegate = track.mEventHandlerDelegate;
if (delegate != null) {
Handler handler = delegate.getHandler();
@@ -2995,7 +3140,8 @@ public class AudioTrack extends PlayerBase
private native final int native_setup(Object /*WeakReference<AudioTrack>*/ audiotrack_this,
Object /*AudioAttributes*/ attributes,
int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat,
- int buffSizeInBytes, int mode, int[] sessionId, long nativeAudioTrack);
+ int buffSizeInBytes, int mode, int[] sessionId, long nativeAudioTrack,
+ boolean offload);
private native final void native_finalize();
diff --git a/android/media/DataSourceDesc.java b/android/media/DataSourceDesc.java
new file mode 100644
index 00000000..73fad7ad
--- /dev/null
+++ b/android/media/DataSourceDesc.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.FileDescriptor;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.HttpCookie;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Structure for data source descriptor.
+ *
+ * Used by {@link MediaPlayer2#setDataSource(DataSourceDesc)}
+ * to set data source for playback.
+ *
+ * <p>Users should use {@link Builder} to change {@link DataSourceDesc}.
+ *
+ */
+public final class DataSourceDesc {
+ /* No data source has been set yet */
+ public static final int TYPE_NONE = 0;
+ /* data source is type of MediaDataSource */
+ public static final int TYPE_CALLBACK = 1;
+ /* data source is type of FileDescriptor */
+ public static final int TYPE_FD = 2;
+ /* data source is type of Uri */
+ public static final int TYPE_URI = 3;
+
+ // intentionally less than long.MAX_VALUE
+ public static final long LONG_MAX = 0x7ffffffffffffffL;
+
+ private int mType = TYPE_NONE;
+
+ private Media2DataSource mMedia2DataSource;
+
+ private FileDescriptor mFD;
+ private long mFDOffset = 0;
+ private long mFDLength = LONG_MAX;
+
+ private Uri mUri;
+ private Map<String, String> mUriHeader;
+ private List<HttpCookie> mUriCookies;
+ private Context mUriContext;
+
+ private long mId = 0;
+ private long mStartPositionMs = 0;
+ private long mEndPositionMs = LONG_MAX;
+
+ private DataSourceDesc() {
+ }
+
+ /**
+ * Return the Id of data source.
+ * @return the Id of data source
+ */
+ public long getId() {
+ return mId;
+ }
+
+ /**
+ * Return the position in milliseconds at which the playback will start.
+ * @return the position in milliseconds at which the playback will start
+ */
+ public long getStartPosition() {
+ return mStartPositionMs;
+ }
+
+ /**
+ * Return the position in milliseconds at which the playback will end.
+ * -1 means ending at the end of source content.
+ * @return the position in milliseconds at which the playback will end
+ */
+ public long getEndPosition() {
+ return mEndPositionMs;
+ }
+
+ /**
+ * Return the type of data source.
+ * @return the type of data source
+ */
+ public int getType() {
+ return mType;
+ }
+
+ /**
+ * Return the Media2DataSource of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_CALLBACK}.
+ * @return the Media2DataSource of this data source
+ */
+ public Media2DataSource getMedia2DataSource() {
+ return mMedia2DataSource;
+ }
+
+ /**
+ * Return the FileDescriptor of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
+ * @return the FileDescriptor of this data source
+ */
+ public FileDescriptor getFileDescriptor() {
+ return mFD;
+ }
+
+ /**
+ * Return the offset associated with the FileDescriptor of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_FD} and it has
+ * been set by the {@link Builder}.
+ * @return the offset associated with the FileDescriptor of this data source
+ */
+ public long getFileDescriptorOffset() {
+ return mFDOffset;
+ }
+
+ /**
+ * Return the content length associated with the FileDescriptor of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_FD}.
+ * -1 means same as the length of source content.
+ * @return the content length associated with the FileDescriptor of this data source
+ */
+ public long getFileDescriptorLength() {
+ return mFDLength;
+ }
+
+ /**
+ * Return the Uri of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+ * @return the Uri of this data source
+ */
+ public Uri getUri() {
+ return mUri;
+ }
+
+ /**
+ * Return the Uri headers of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+ * @return the Uri headers of this data source
+ */
+ public Map<String, String> getUriHeaders() {
+ if (mUriHeader == null) {
+ return null;
+ }
+ return new HashMap<String, String>(mUriHeader);
+ }
+
+ /**
+ * Return the Uri cookies of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+ * @return the Uri cookies of this data source
+ */
+ public List<HttpCookie> getUriCookies() {
+ if (mUriCookies == null) {
+ return null;
+ }
+ return new ArrayList<HttpCookie>(mUriCookies);
+ }
+
+ /**
+ * Return the Context used for resolving the Uri of this data source.
+ * It's meaningful only when {@code getType} returns {@link #TYPE_URI}.
+ * @return the Context used for resolving the Uri of this data source
+ */
+ public Context getUriContext() {
+ return mUriContext;
+ }
+
+ /**
+ * Builder class for {@link DataSourceDesc} objects.
+ * <p> Here is an example where <code>Builder</code> is used to define the
+ * {@link DataSourceDesc} to be used by a {@link MediaPlayer2} instance:
+ *
+ * <pre class="prettyprint">
+ * DataSourceDesc oldDSD = mediaplayer2.getDataSourceDesc();
+ * DataSourceDesc newDSD = new DataSourceDesc.Builder(oldDSD)
+ * .setStartPosition(1000)
+ * .setEndPosition(15000)
+ * .build();
+ * mediaplayer2.setDataSourceDesc(newDSD);
+ * </pre>
+ */
+ public static class Builder {
+ private int mType = TYPE_NONE;
+
+ private Media2DataSource mMedia2DataSource;
+
+ private FileDescriptor mFD;
+ private long mFDOffset = 0;
+ private long mFDLength = LONG_MAX;
+
+ private Uri mUri;
+ private Map<String, String> mUriHeader;
+ private List<HttpCookie> mUriCookies;
+ private Context mUriContext;
+
+ private long mId = 0;
+ private long mStartPositionMs = 0;
+ private long mEndPositionMs = LONG_MAX;
+
+ /**
+ * Constructs a new Builder with the defaults.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Constructs a new Builder from a given {@link DataSourceDesc} instance
+ * @param dsd the {@link DataSourceDesc} object whose data will be reused
+ * in the new Builder.
+ */
+ public Builder(DataSourceDesc dsd) {
+ mType = dsd.mType;
+ mMedia2DataSource = dsd.mMedia2DataSource;
+ mFD = dsd.mFD;
+ mFDOffset = dsd.mFDOffset;
+ mFDLength = dsd.mFDLength;
+ mUri = dsd.mUri;
+ mUriHeader = dsd.mUriHeader;
+ mUriCookies = dsd.mUriCookies;
+ mUriContext = dsd.mUriContext;
+
+ mId = dsd.mId;
+ mStartPositionMs = dsd.mStartPositionMs;
+ mEndPositionMs = dsd.mEndPositionMs;
+ }
+
+ /**
+ * Combines all of the fields that have been set and return a new
+ * {@link DataSourceDesc} object. <code>IllegalStateException</code> will be
+ * thrown if there is conflict between fields.
+ *
+ * @return a new {@link DataSourceDesc} object
+ */
+ public DataSourceDesc build() {
+ if (mType != TYPE_CALLBACK
+ && mType != TYPE_FD
+ && mType != TYPE_URI) {
+ throw new IllegalStateException("Illegal type: " + mType);
+ }
+ if (mStartPositionMs > mEndPositionMs) {
+ throw new IllegalStateException("Illegal start/end position: "
+ + mStartPositionMs + " : " + mEndPositionMs);
+ }
+
+ DataSourceDesc dsd = new DataSourceDesc();
+ dsd.mType = mType;
+ dsd.mMedia2DataSource = mMedia2DataSource;
+ dsd.mFD = mFD;
+ dsd.mFDOffset = mFDOffset;
+ dsd.mFDLength = mFDLength;
+ dsd.mUri = mUri;
+ dsd.mUriHeader = mUriHeader;
+ dsd.mUriCookies = mUriCookies;
+ dsd.mUriContext = mUriContext;
+
+ dsd.mId = mId;
+ dsd.mStartPositionMs = mStartPositionMs;
+ dsd.mEndPositionMs = mEndPositionMs;
+
+ return dsd;
+ }
+
+ /**
+ * Sets the Id of this data source.
+ *
+ * @param id the Id of this data source
+ * @return the same Builder instance.
+ */
+ public Builder setId(long id) {
+ mId = id;
+ return this;
+ }
+
+ /**
+ * Sets the start position in milliseconds at which the playback will start.
+ * Any negative number is treated as 0.
+ *
+ * @param position the start position in milliseconds at which the playback will start
+ * @return the same Builder instance.
+ *
+ */
+ public Builder setStartPosition(long position) {
+ if (position < 0) {
+ position = 0;
+ }
+ mStartPositionMs = position;
+ return this;
+ }
+
+ /**
+ * Sets the end position in milliseconds at which the playback will end.
+ * Any negative number is treated as maximum length of the data source.
+ *
+ * @param position the end position in milliseconds at which the playback will end
+ * @return the same Builder instance.
+ */
+ public Builder setEndPosition(long position) {
+ if (position < 0) {
+ position = LONG_MAX;
+ }
+ mEndPositionMs = position;
+ return this;
+ }
+
+ /**
+ * Sets the data source (Media2DataSource) to use.
+ *
+ * @param m2ds the Media2DataSource for the media you want to play
+ * @return the same Builder instance.
+ * @throws NullPointerException if m2ds is null.
+ */
+ public Builder setDataSource(Media2DataSource m2ds) {
+ Preconditions.checkNotNull(m2ds);
+ resetDataSource();
+ mType = TYPE_CALLBACK;
+ mMedia2DataSource = m2ds;
+ return this;
+ }
+
+ /**
+ * Sets the data source (FileDescriptor) to use. The FileDescriptor must be
+ * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility
+ * to close the file descriptor after the source has been used.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @return the same Builder instance.
+ * @throws NullPointerException if fd is null.
+ */
+ public Builder setDataSource(FileDescriptor fd) {
+ Preconditions.checkNotNull(fd);
+ resetDataSource();
+ mType = TYPE_FD;
+ mFD = fd;
+ return this;
+ }
+
+ /**
+ * Sets the data source (FileDescriptor) to use. The FileDescriptor must be
+ * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility
+ * to close the file descriptor after the source has been used.
+ *
+ * Any negative number for offset is treated as 0.
+ * Any negative number for length is treated as maximum length of the data source.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @param offset the offset into the file where the data to be played starts, in bytes
+ * @param length the length in bytes of the data to be played
+ * @return the same Builder instance.
+ * @throws NullPointerException if fd is null.
+ */
+ public Builder setDataSource(FileDescriptor fd, long offset, long length) {
+ Preconditions.checkNotNull(fd);
+ if (offset < 0) {
+ offset = 0;
+ }
+ if (length < 0) {
+ length = LONG_MAX;
+ }
+ resetDataSource();
+ mType = TYPE_FD;
+ mFD = fd;
+ mFDOffset = offset;
+ mFDLength = length;
+ return this;
+ }
+
+ /**
+ * Sets the data source as a content Uri.
+ *
+ * @param context the Context to use when resolving the Uri
+ * @param uri the Content URI of the data you want to play
+ * @return the same Builder instance.
+ * @throws NullPointerException if context or uri is null.
+ */
+ public Builder setDataSource(@NonNull Context context, @NonNull Uri uri) {
+ Preconditions.checkNotNull(context, "context cannot be null");
+ Preconditions.checkNotNull(uri, "uri cannot be null");
+ resetDataSource();
+ mType = TYPE_URI;
+ mUri = uri;
+ mUriContext = context;
+ return this;
+ }
+
+ /**
+ * Sets the data source as a content Uri.
+ *
+ * To provide cookies for the subsequent HTTP requests, you can install your own default
+ * cookie handler and use other variants of setDataSource APIs instead. Alternatively, you
+ * can use this API to pass the cookies as a list of HttpCookie. If the app has not
+ * installed a CookieHandler already, {@link MediaPlayer2} will create a CookieManager
+ * and populates its CookieStore with the provided cookies when this data source is passed
+ * to {@link MediaPlayer2}. If the app has installed its own handler already, the handler
+ * is required to be of CookieManager type such that {@link MediaPlayer2} can update the
+ * manager’s CookieStore.
+ *
+ * <p><strong>Note</strong> that the cross domain redirection is allowed by default,
+ * but that can be changed with key/value pairs through the headers parameter with
+ * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value to
+ * disallow or allow cross domain redirection.
+ *
+ * @param context the Context to use when resolving the Uri
+ * @param uri the Content URI of the data you want to play
+ * @param headers the headers to be sent together with the request for the data
+ * The headers must not include cookies. Instead, use the cookies param.
+ * @param cookies the cookies to be sent together with the request
+ * @return the same Builder instance.
+ * @throws NullPointerException if context or uri is null.
+ */
+ public Builder setDataSource(@NonNull Context context, @NonNull Uri uri,
+ @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies) {
+ Preconditions.checkNotNull(uri);
+ resetDataSource();
+ mType = TYPE_URI;
+ mUri = uri;
+ if (headers != null) {
+ mUriHeader = new HashMap<String, String>(headers);
+ }
+ if (cookies != null) {
+ mUriCookies = new ArrayList<HttpCookie>(cookies);
+ }
+ mUriContext = context;
+ return this;
+ }
+
+ private void resetDataSource() {
+ mType = TYPE_NONE;
+ mMedia2DataSource = null;
+ mFD = null;
+ mFDOffset = 0;
+ mFDLength = LONG_MAX;
+ mUri = null;
+ mUriHeader = null;
+ mUriCookies = null;
+ mUriContext = null;
+ }
+ }
+}
diff --git a/android/media/Media2DataSource.java b/android/media/Media2DataSource.java
new file mode 100644
index 00000000..8ee4a705
--- /dev/null
+++ b/android/media/Media2DataSource.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 android.media;
+
+import java.io.Closeable;
+import java.io.IOException;
+
+/**
+ * For supplying media data to the framework. Implement this if your app has
+ * special requirements for the way media data is obtained.
+ *
+ * <p class="note">Methods of this interface may be called on multiple different
+ * threads. There will be a thread synchronization point between each call to ensure that
+ * modifications to the state of your Media2DataSource are visible to future calls. This means
+ * you don't need to do your own synchronization unless you're modifying the
+ * Media2DataSource from another thread while it's being used by the framework.</p>
+ *
+ */
+public abstract class Media2DataSource implements Closeable {
+ /**
+ * Called to request data from the given position.
+ *
+ * Implementations should should write up to {@code size} bytes into
+ * {@code buffer}, and return the number of bytes written.
+ *
+ * Return {@code 0} if size is zero (thus no bytes are read).
+ *
+ * Return {@code -1} to indicate that end of stream is reached.
+ *
+ * @param position the position in the data source to read from.
+ * @param buffer the buffer to read the data into.
+ * @param offset the offset within buffer to read the data into.
+ * @param size the number of bytes to read.
+ * @throws IOException on fatal errors.
+ * @return the number of bytes read, or -1 if there was an error.
+ */
+ public abstract int readAt(long position, byte[] buffer, int offset, int size)
+ throws IOException;
+
+ /**
+ * Called to get the size of the data source.
+ *
+ * @throws IOException on fatal errors
+ * @return the size of data source in bytes, or -1 if the size is unknown.
+ */
+ public abstract long getSize() throws IOException;
+}
diff --git a/android/media/Media2HTTPConnection.java b/android/media/Media2HTTPConnection.java
new file mode 100644
index 00000000..0d7825a0
--- /dev/null
+++ b/android/media/Media2HTTPConnection.java
@@ -0,0 +1,385 @@
+/*
+ * 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.media;
+
+import android.net.NetworkUtils;
+import android.os.StrictMode;
+import android.util.Log;
+
+import java.io.BufferedInputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.Proxy;
+import java.net.URL;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.NoRouteToHostException;
+import java.net.ProtocolException;
+import java.net.UnknownServiceException;
+import java.util.HashMap;
+import java.util.Map;
+
+import static android.media.MediaPlayer2.MEDIA_ERROR_UNSUPPORTED;
+
+/** @hide */
+public class Media2HTTPConnection {
+ private static final String TAG = "Media2HTTPConnection";
+ private static final boolean VERBOSE = false;
+
+ // connection timeout - 30 sec
+ private static final int CONNECT_TIMEOUT_MS = 30 * 1000;
+
+ private long mCurrentOffset = -1;
+ private URL mURL = null;
+ private Map<String, String> mHeaders = null;
+ private HttpURLConnection mConnection = null;
+ private long mTotalSize = -1;
+ private InputStream mInputStream = null;
+
+ private boolean mAllowCrossDomainRedirect = true;
+ private boolean mAllowCrossProtocolRedirect = true;
+
+ // from com.squareup.okhttp.internal.http
+ private final static int HTTP_TEMP_REDIRECT = 307;
+ private final static int MAX_REDIRECTS = 20;
+
+ public Media2HTTPConnection() {
+ CookieHandler cookieHandler = CookieHandler.getDefault();
+ if (cookieHandler == null) {
+ Log.w(TAG, "Media2HTTPConnection: Unexpected. No CookieHandler found.");
+ }
+ }
+
+ public boolean connect(String uri, String headers) {
+ if (VERBOSE) {
+ Log.d(TAG, "connect: uri=" + uri + ", headers=" + headers);
+ }
+
+ try {
+ disconnect();
+ mAllowCrossDomainRedirect = true;
+ mURL = new URL(uri);
+ mHeaders = convertHeaderStringToMap(headers);
+ } catch (MalformedURLException e) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private boolean parseBoolean(String val) {
+ try {
+ return Long.parseLong(val) != 0;
+ } catch (NumberFormatException e) {
+ return "true".equalsIgnoreCase(val) ||
+ "yes".equalsIgnoreCase(val);
+ }
+ }
+
+ /* returns true iff header is internal */
+ private boolean filterOutInternalHeaders(String key, String val) {
+ if ("android-allow-cross-domain-redirect".equalsIgnoreCase(key)) {
+ mAllowCrossDomainRedirect = parseBoolean(val);
+ // cross-protocol redirects are also controlled by this flag
+ mAllowCrossProtocolRedirect = mAllowCrossDomainRedirect;
+ } else {
+ return false;
+ }
+ return true;
+ }
+
+ private Map<String, String> convertHeaderStringToMap(String headers) {
+ HashMap<String, String> map = new HashMap<String, String>();
+
+ String[] pairs = headers.split("\r\n");
+ for (String pair : pairs) {
+ int colonPos = pair.indexOf(":");
+ if (colonPos >= 0) {
+ String key = pair.substring(0, colonPos);
+ String val = pair.substring(colonPos + 1);
+
+ if (!filterOutInternalHeaders(key, val)) {
+ map.put(key, val);
+ }
+ }
+ }
+
+ return map;
+ }
+
+ public void disconnect() {
+ teardownConnection();
+ mHeaders = null;
+ mURL = null;
+ }
+
+ private void teardownConnection() {
+ if (mConnection != null) {
+ if (mInputStream != null) {
+ try {
+ mInputStream.close();
+ } catch (IOException e) {
+ }
+ mInputStream = null;
+ }
+
+ mConnection.disconnect();
+ mConnection = null;
+
+ mCurrentOffset = -1;
+ }
+ }
+
+ private static final boolean isLocalHost(URL url) {
+ if (url == null) {
+ return false;
+ }
+
+ String host = url.getHost();
+
+ if (host == null) {
+ return false;
+ }
+
+ try {
+ if (host.equalsIgnoreCase("localhost")) {
+ return true;
+ }
+ if (NetworkUtils.numericToInetAddress(host).isLoopbackAddress()) {
+ return true;
+ }
+ } catch (IllegalArgumentException iex) {
+ }
+ return false;
+ }
+
+ private void seekTo(long offset) throws IOException {
+ teardownConnection();
+
+ try {
+ int response;
+ int redirectCount = 0;
+
+ URL url = mURL;
+
+ // do not use any proxy for localhost (127.0.0.1)
+ boolean noProxy = isLocalHost(url);
+
+ while (true) {
+ if (noProxy) {
+ mConnection = (HttpURLConnection)url.openConnection(Proxy.NO_PROXY);
+ } else {
+ mConnection = (HttpURLConnection)url.openConnection();
+ }
+ mConnection.setConnectTimeout(CONNECT_TIMEOUT_MS);
+
+ // handle redirects ourselves if we do not allow cross-domain redirect
+ mConnection.setInstanceFollowRedirects(mAllowCrossDomainRedirect);
+
+ if (mHeaders != null) {
+ for (Map.Entry<String, String> entry : mHeaders.entrySet()) {
+ mConnection.setRequestProperty(
+ entry.getKey(), entry.getValue());
+ }
+ }
+
+ if (offset > 0) {
+ mConnection.setRequestProperty(
+ "Range", "bytes=" + offset + "-");
+ }
+
+ response = mConnection.getResponseCode();
+ if (response != HttpURLConnection.HTTP_MULT_CHOICE &&
+ response != HttpURLConnection.HTTP_MOVED_PERM &&
+ response != HttpURLConnection.HTTP_MOVED_TEMP &&
+ response != HttpURLConnection.HTTP_SEE_OTHER &&
+ response != HTTP_TEMP_REDIRECT) {
+ // not a redirect, or redirect handled by HttpURLConnection
+ break;
+ }
+
+ if (++redirectCount > MAX_REDIRECTS) {
+ throw new NoRouteToHostException("Too many redirects: " + redirectCount);
+ }
+
+ String method = mConnection.getRequestMethod();
+ if (response == HTTP_TEMP_REDIRECT &&
+ !method.equals("GET") && !method.equals("HEAD")) {
+ // "If the 307 status code is received in response to a
+ // request other than GET or HEAD, the user agent MUST NOT
+ // automatically redirect the request"
+ throw new NoRouteToHostException("Invalid redirect");
+ }
+ String location = mConnection.getHeaderField("Location");
+ if (location == null) {
+ throw new NoRouteToHostException("Invalid redirect");
+ }
+ url = new URL(mURL /* TRICKY: don't use url! */, location);
+ if (!url.getProtocol().equals("https") &&
+ !url.getProtocol().equals("http")) {
+ throw new NoRouteToHostException("Unsupported protocol redirect");
+ }
+ boolean sameProtocol = mURL.getProtocol().equals(url.getProtocol());
+ if (!mAllowCrossProtocolRedirect && !sameProtocol) {
+ throw new NoRouteToHostException("Cross-protocol redirects are disallowed");
+ }
+ boolean sameHost = mURL.getHost().equals(url.getHost());
+ if (!mAllowCrossDomainRedirect && !sameHost) {
+ throw new NoRouteToHostException("Cross-domain redirects are disallowed");
+ }
+
+ if (response != HTTP_TEMP_REDIRECT) {
+ // update effective URL, unless it is a Temporary Redirect
+ mURL = url;
+ }
+ }
+
+ if (mAllowCrossDomainRedirect) {
+ // remember the current, potentially redirected URL if redirects
+ // were handled by HttpURLConnection
+ mURL = mConnection.getURL();
+ }
+
+ if (response == HttpURLConnection.HTTP_PARTIAL) {
+ // Partial content, we cannot just use getContentLength
+ // because what we want is not just the length of the range
+ // returned but the size of the full content if available.
+
+ String contentRange =
+ mConnection.getHeaderField("Content-Range");
+
+ mTotalSize = -1;
+ if (contentRange != null) {
+ // format is "bytes xxx-yyy/zzz
+ // where "zzz" is the total number of bytes of the
+ // content or '*' if unknown.
+
+ int lastSlashPos = contentRange.lastIndexOf('/');
+ if (lastSlashPos >= 0) {
+ String total =
+ contentRange.substring(lastSlashPos + 1);
+
+ try {
+ mTotalSize = Long.parseLong(total);
+ } catch (NumberFormatException e) {
+ }
+ }
+ }
+ } else if (response != HttpURLConnection.HTTP_OK) {
+ throw new IOException();
+ } else {
+ mTotalSize = mConnection.getContentLength();
+ }
+
+ if (offset > 0 && response != HttpURLConnection.HTTP_PARTIAL) {
+ // Some servers simply ignore "Range" requests and serve
+ // data from the start of the content.
+ throw new ProtocolException();
+ }
+
+ mInputStream =
+ new BufferedInputStream(mConnection.getInputStream());
+
+ mCurrentOffset = offset;
+ } catch (IOException e) {
+ mTotalSize = -1;
+ teardownConnection();
+ mCurrentOffset = -1;
+
+ throw e;
+ }
+ }
+
+ public int readAt(long offset, byte[] data, int size) {
+ StrictMode.ThreadPolicy policy =
+ new StrictMode.ThreadPolicy.Builder().permitAll().build();
+
+ StrictMode.setThreadPolicy(policy);
+
+ try {
+ if (offset != mCurrentOffset) {
+ seekTo(offset);
+ }
+
+ int n = mInputStream.read(data, 0, size);
+
+ if (n == -1) {
+ // InputStream signals EOS using a -1 result, our semantics
+ // are to return a 0-length read.
+ n = 0;
+ }
+
+ mCurrentOffset += n;
+
+ if (VERBOSE) {
+ Log.d(TAG, "readAt " + offset + " / " + size + " => " + n);
+ }
+
+ return n;
+ } catch (ProtocolException e) {
+ Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
+ return MEDIA_ERROR_UNSUPPORTED;
+ } catch (NoRouteToHostException e) {
+ Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
+ return MEDIA_ERROR_UNSUPPORTED;
+ } catch (UnknownServiceException e) {
+ Log.w(TAG, "readAt " + offset + " / " + size + " => " + e);
+ return MEDIA_ERROR_UNSUPPORTED;
+ } catch (IOException e) {
+ if (VERBOSE) {
+ Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
+ }
+ return -1;
+ } catch (Exception e) {
+ if (VERBOSE) {
+ Log.d(TAG, "unknown exception " + e);
+ Log.d(TAG, "readAt " + offset + " / " + size + " => -1");
+ }
+ return -1;
+ }
+ }
+
+ public long getSize() {
+ if (mConnection == null) {
+ try {
+ seekTo(0);
+ } catch (IOException e) {
+ return -1;
+ }
+ }
+
+ return mTotalSize;
+ }
+
+ public String getMIMEType() {
+ if (mConnection == null) {
+ try {
+ seekTo(0);
+ } catch (IOException e) {
+ return "application/octet-stream";
+ }
+ }
+
+ return mConnection.getContentType();
+ }
+
+ public String getUri() {
+ return mURL.toString();
+ }
+}
diff --git a/android/media/Media2HTTPService.java b/android/media/Media2HTTPService.java
new file mode 100644
index 00000000..957aceca
--- /dev/null
+++ b/android/media/Media2HTTPService.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 android.media;
+
+import android.util.Log;
+
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.CookieStore;
+import java.net.HttpCookie;
+import java.util.List;
+
+/** @hide */
+public class Media2HTTPService {
+ private static final String TAG = "Media2HTTPService";
+ private List<HttpCookie> mCookies;
+ private Boolean mCookieStoreInitialized = new Boolean(false);
+
+ public Media2HTTPService(List<HttpCookie> cookies) {
+ mCookies = cookies;
+ Log.v(TAG, "Media2HTTPService(" + this + "): Cookies: " + cookies);
+ }
+
+ public Media2HTTPConnection makeHTTPConnection() {
+
+ synchronized (mCookieStoreInitialized) {
+ // Only need to do it once for all connections
+ if ( !mCookieStoreInitialized ) {
+ CookieHandler cookieHandler = CookieHandler.getDefault();
+ if (cookieHandler == null) {
+ cookieHandler = new CookieManager();
+ CookieHandler.setDefault(cookieHandler);
+ Log.v(TAG, "makeHTTPConnection: CookieManager created: " + cookieHandler);
+ } else {
+ Log.v(TAG, "makeHTTPConnection: CookieHandler (" + cookieHandler + ") exists.");
+ }
+
+ // Applying the bootstrapping cookies
+ if ( mCookies != null ) {
+ if ( cookieHandler instanceof CookieManager ) {
+ CookieManager cookieManager = (CookieManager)cookieHandler;
+ CookieStore store = cookieManager.getCookieStore();
+ for ( HttpCookie cookie : mCookies ) {
+ try {
+ store.add(null, cookie);
+ } catch ( Exception e ) {
+ Log.v(TAG, "makeHTTPConnection: CookieStore.add" + e);
+ }
+ //for extended debugging when needed
+ //Log.v(TAG, "MediaHTTPConnection adding Cookie[" + cookie.getName() +
+ // "]: " + cookie);
+ }
+ } else {
+ Log.w(TAG, "makeHTTPConnection: The installed CookieHandler is not a "
+ + "CookieManager. Can’t add the provided cookies to the cookie "
+ + "store.");
+ }
+ } // mCookies
+
+ mCookieStoreInitialized = true;
+
+ Log.v(TAG, "makeHTTPConnection(" + this + "): cookieHandler: " + cookieHandler +
+ " Cookies: " + mCookies);
+ } // mCookieStoreInitialized
+ } // synchronized
+
+ return new Media2HTTPConnection();
+ }
+
+ /* package private */ static Media2HTTPService createHTTPService(String path) {
+ return createHTTPService(path, null);
+ }
+
+ // when cookies are provided
+ static Media2HTTPService createHTTPService(String path, List<HttpCookie> cookies) {
+ if (path.startsWith("http://") || path.startsWith("https://")) {
+ return (new Media2HTTPService(cookies));
+ } else if (path.startsWith("widevine://")) {
+ Log.d(TAG, "Widevine classic is no longer supported");
+ }
+
+ return null;
+ }
+}
diff --git a/android/media/MediaBrowser2.java b/android/media/MediaBrowser2.java
new file mode 100644
index 00000000..be4be3fc
--- /dev/null
+++ b/android/media/MediaBrowser2.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.update.ApiLoader;
+import android.media.update.MediaBrowser2Provider;
+import android.os.Bundle;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Browses media content offered by a {@link MediaLibraryService2}.
+ * @hide
+ */
+public class MediaBrowser2 extends MediaController2 {
+ // Equals to the ((MediaBrowser2Provider) getProvider())
+ private final MediaBrowser2Provider mProvider;
+
+ /**
+ * Callback to listen events from {@link MediaLibraryService2}.
+ */
+ public static class BrowserCallback extends MediaController2.ControllerCallback {
+ /**
+ * Called with the result of {@link #getBrowserRoot(Bundle)}.
+ * <p>
+ * {@code rootMediaId} and {@code rootExtra} can be {@code null} if the browser root isn't
+ * available.
+ *
+ * @param rootHints rootHints that you previously requested.
+ * @param rootMediaId media id of the browser root. Can be {@code null}
+ * @param rootExtra extra of the browser root. Can be {@code null}
+ */
+ public void onGetRootResult(Bundle rootHints, @Nullable String rootMediaId,
+ @Nullable Bundle rootExtra) { }
+
+ /**
+ * Called when the item has been returned by the library service for the previous
+ * {@link MediaBrowser2#getItem} call.
+ * <p>
+ * Result can be null if there had been error.
+ *
+ * @param mediaId media id
+ * @param result result. Can be {@code null}
+ */
+ public void onItemLoaded(@NonNull String mediaId, @Nullable MediaItem2 result) { }
+
+ /**
+ * Called when the list of items has been returned by the library service for the previous
+ * {@link MediaBrowser2#getChildren(String, int, int, Bundle)}.
+ *
+ * @param parentId parent id
+ * @param page page number that you've specified
+ * @param pageSize page size that you've specified
+ * @param options optional bundle that you've specified
+ * @param result result. Can be {@code null}
+ */
+ public void onChildrenLoaded(@NonNull String parentId, int page, int pageSize,
+ @Nullable Bundle options, @Nullable List<MediaItem2> result) { }
+
+ /**
+ * Called when there's change in the parent's children.
+ *
+ * @param parentId parent id that you've specified with subscribe
+ * @param options optional bundle that you've specified with subscribe
+ */
+ public void onChildrenChanged(@NonNull String parentId, @Nullable Bundle options) { }
+
+ /**
+ * Called when the search result has been returned by the library service for the previous
+ * {@link MediaBrowser2#search(String, int, int, Bundle)}.
+ * <p>
+ * Result can be null if there had been error.
+ *
+ * @param query query string that you've specified
+ * @param page page number that you've specified
+ * @param pageSize page size that you've specified
+ * @param options optional bundle that you've specified
+ * @param result result. Can be {@code null}
+ */
+ public void onSearchResult(@NonNull String query, int page, int pageSize,
+ @Nullable Bundle options, @Nullable List<MediaItem2> result) { }
+ }
+
+ public MediaBrowser2(Context context, SessionToken2 token, BrowserCallback callback,
+ Executor executor) {
+ super(context, token, callback, executor);
+ mProvider = (MediaBrowser2Provider) getProvider();
+ }
+
+ @Override
+ MediaBrowser2Provider createProvider(Context context, SessionToken2 token,
+ ControllerCallback callback, Executor executor) {
+ return ApiLoader.getProvider(context)
+ .createMediaBrowser2(this, context, token, (BrowserCallback) callback, executor);
+ }
+
+ public void getBrowserRoot(Bundle rootHints) {
+ mProvider.getBrowserRoot_impl(rootHints);
+ }
+
+ /**
+ * Subscribe to a parent id for the change in its children. When there's a change,
+ * {@link BrowserCallback#onChildrenChanged(String, Bundle)} will be called with the bundle
+ * that you've specified. You should call {@link #getChildren(String, int, int, Bundle)} to get
+ * the actual contents for the parent.
+ *
+ * @param parentId parent id
+ * @param options optional bundle
+ */
+ public void subscribe(String parentId, @Nullable Bundle options) {
+ mProvider.subscribe_impl(parentId, options);
+ }
+
+ /**
+ * Unsubscribe for changes to the children of the parent, which was previously subscribed with
+ * {@link #subscribe(String, Bundle)}.
+ *
+ * @param parentId parent id
+ * @param options optional bundle
+ */
+ public void unsubscribe(String parentId, @Nullable Bundle options) {
+ mProvider.unsubscribe_impl(parentId, options);
+ }
+
+ /**
+ * Get the media item with the given media id. Result would be sent back asynchronously with the
+ * {@link BrowserCallback#onItemLoaded(String, MediaItem2)}.
+ *
+ * @param mediaId media id
+ */
+ public void getItem(String mediaId) {
+ mProvider.getItem_impl(mediaId);
+ }
+
+ /**
+ * Get list of children under the parent. Result would be sent back asynchronously with the
+ * {@link BrowserCallback#onChildrenLoaded(String, int, int, Bundle, List)}.
+ *
+ * @param parentId
+ * @param page
+ * @param pageSize
+ * @param options
+ */
+ public void getChildren(String parentId, int page, int pageSize, @Nullable Bundle options) {
+ mProvider.getChildren_impl(parentId, page, pageSize, options);
+ }
+
+ /**
+ *
+ * @param query search query deliminated by string
+ * @param page page number to get search result. Starts from {@code 1}
+ * @param pageSize page size. Should be greater or equal to {@code 1}
+ * @param extras extra bundle
+ */
+ public void search(String query, int page, int pageSize, Bundle extras) {
+ mProvider.search_impl(query, page, pageSize, extras);
+ }
+}
diff --git a/android/media/MediaBrowser2Test.java b/android/media/MediaBrowser2Test.java
new file mode 100644
index 00000000..5c960c85
--- /dev/null
+++ b/android/media/MediaBrowser2Test.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright 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.media;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.media.MediaBrowser2.BrowserCallback;
+import android.media.MediaSession2.CommandGroup;
+import android.os.Bundle;
+import android.support.annotation.CallSuper;
+import android.support.annotation.NonNull;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Tests {@link MediaBrowser2}.
+ * <p>
+ * This test inherits {@link MediaController2Test} to ensure that inherited APIs from
+ * {@link MediaController2} works cleanly.
+ */
+// TODO(jaewan): Implement host-side test so browser and service can run in different processes.
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MediaBrowser2Test extends MediaController2Test {
+ private static final String TAG = "MediaBrowser2Test";
+
+ @Override
+ TestControllerInterface onCreateController(@NonNull SessionToken2 token,
+ @NonNull TestControllerCallbackInterface callback) {
+ return new TestMediaBrowser(mContext, token, new TestBrowserCallback(callback));
+ }
+
+ @Test
+ public void testGetBrowserRoot() throws InterruptedException {
+ final Bundle param = new Bundle();
+ param.putString(TAG, TAG);
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ final TestControllerCallbackInterface callback = new TestControllerCallbackInterface() {
+ @Override
+ public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
+ assertTrue(TestUtils.equals(param, rootHints));
+ assertEquals(MockMediaLibraryService2.ROOT_ID, rootMediaId);
+ assertTrue(TestUtils.equals(MockMediaLibraryService2.EXTRA, rootExtra));
+ latch.countDown();
+ }
+ };
+
+ final SessionToken2 token = MockMediaLibraryService2.getToken(mContext);
+ MediaBrowser2 browser =
+ (MediaBrowser2) createController(token,true, callback);
+ browser.getBrowserRoot(param);
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ public static class TestBrowserCallback extends BrowserCallback
+ implements WaitForConnectionInterface {
+ private final TestControllerCallbackInterface mCallbackProxy;
+ public final CountDownLatch connectLatch = new CountDownLatch(1);
+ public final CountDownLatch disconnectLatch = new CountDownLatch(1);
+
+ TestBrowserCallback(TestControllerCallbackInterface callbackProxy) {
+ mCallbackProxy = callbackProxy;
+ }
+
+ @CallSuper
+ @Override
+ public void onConnected(CommandGroup commands) {
+ super.onConnected(commands);
+ connectLatch.countDown();
+ }
+
+ @CallSuper
+ @Override
+ public void onDisconnected() {
+ super.onDisconnected();
+ disconnectLatch.countDown();
+ }
+
+ @Override
+ public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {
+ mCallbackProxy.onGetRootResult(rootHints, rootMediaId, rootExtra);
+ }
+
+ @Override
+ public void waitForConnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void waitForDisconnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+ }
+
+ public class TestMediaBrowser extends MediaBrowser2 implements TestControllerInterface {
+ private final BrowserCallback mCallback;
+
+ public TestMediaBrowser(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull ControllerCallback callback) {
+ super(context, token, (BrowserCallback) callback, sHandlerExecutor);
+ mCallback = (BrowserCallback) callback;
+ }
+
+ @Override
+ public BrowserCallback getCallback() {
+ return mCallback;
+ }
+ }
+} \ No newline at end of file
diff --git a/android/media/MediaCodecInfo.java b/android/media/MediaCodecInfo.java
index f41e33f7..44d90997 100644
--- a/android/media/MediaCodecInfo.java
+++ b/android/media/MediaCodecInfo.java
@@ -2639,7 +2639,8 @@ public final class MediaCodecInfo {
/**
* Returns the supported range of quality values.
*
- * @hide
+ * Quality is implementation-specific. As a general rule, a higher quality
+ * setting results in a better image quality and a lower compression ratio.
*/
public Range<Integer> getQualityRange() {
return mQualityRange;
@@ -2751,7 +2752,7 @@ public final class MediaCodecInfo {
}
if (info.containsKey("feature-bitrate-modes")) {
for (String mode: info.getString("feature-bitrate-modes").split(",")) {
- mBitControl |= parseBitrateMode(mode);
+ mBitControl |= (1 << parseBitrateMode(mode));
}
}
diff --git a/android/media/MediaController2.java b/android/media/MediaController2.java
new file mode 100644
index 00000000..d669bc12
--- /dev/null
+++ b/android/media/MediaController2.java
@@ -0,0 +1,616 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.media.MediaSession2.Command;
+import android.media.MediaSession2.CommandButton;
+import android.media.MediaSession2.CommandGroup;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.PlaylistParam;
+import android.media.session.MediaSessionManager;
+import android.media.update.ApiLoader;
+import android.media.update.MediaController2Provider;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows an app to interact with an active {@link MediaSession2} or a
+ * {@link MediaSessionService2} in any status. Media buttons and other commands can be sent to
+ * the session.
+ * <p>
+ * When you're done, use {@link #close()} to clean up resources. This also helps session service
+ * to be destroyed when there's no controller associated with it.
+ * <p>
+ * When controlling {@link MediaSession2}, the controller will be available immediately after
+ * the creation.
+ * <p>
+ * When controlling {@link MediaSessionService2}, the {@link MediaController2} would be
+ * available only if the session service allows this controller by
+ * {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)} for the service. Wait
+ * {@link ControllerCallback#onConnected(CommandGroup)} or
+ * {@link ControllerCallback#onDisconnected()} for the result.
+ * <p>
+ * A controller can be created through token from {@link MediaSessionManager} if you hold the
+ * signature|privileged permission "android.permission.MEDIA_CONTENT_CONTROL" permission or are
+ * an enabled notification listener or by getting a {@link SessionToken2} directly the
+ * the session owner.
+ * <p>
+ * MediaController2 objects are thread-safe.
+ * <p>
+ * @see MediaSession2
+ * @see MediaSessionService2
+ * @hide
+ */
+// TODO(jaewan): Unhide
+// TODO(jaewan): Revisit comments. Currently MediaBrowser case is missing.
+public class MediaController2 implements AutoCloseable {
+ /**
+ * Interface for listening to change in activeness of the {@link MediaSession2}. It's
+ * active if and only if it has set a player.
+ */
+ public abstract static class ControllerCallback {
+ /**
+ * Called when the controller is successfully connected to the session. The controller
+ * becomes available afterwards.
+ *
+ * @param allowedCommands commands that's allowed by the session.
+ */
+ public void onConnected(CommandGroup allowedCommands) { }
+
+ /**
+ * Called when the session refuses the controller or the controller is disconnected from
+ * the session. The controller becomes unavailable afterwards and the callback wouldn't
+ * be called.
+ * <p>
+ * It will be also called after the {@link #close()}, so you can put clean up code here.
+ * You don't need to call {@link #close()} after this.
+ */
+ public void onDisconnected() { }
+
+ /**
+ * Called when the session set the custom layout through the
+ * {@link MediaSession2#setCustomLayout(ControllerInfo, List)}.
+ * <p>
+ * Can be called before {@link #onConnected(CommandGroup)} is called.
+ *
+ * @param layout
+ */
+ public void onCustomLayoutChanged(List<CommandButton> layout) { }
+
+ /**
+ * Called when the session has changed anything related with the {@link PlaybackInfo}.
+ *
+ * @param info new playback info
+ */
+ public void onAudioInfoChanged(PlaybackInfo info) { }
+
+ /**
+ * Called when the allowed commands are changed by session.
+ *
+ * @param commands newly allowed commands
+ */
+ public void onAllowedCommandsChanged(CommandGroup commands) { }
+
+ /**
+ * Called when the session sent a custom command.
+ *
+ * @param command
+ * @param args
+ * @param receiver
+ */
+ public void onCustomCommand(Command command, @Nullable Bundle args,
+ @Nullable ResultReceiver receiver) { }
+
+ /**
+ * Called when the playlist is changed.
+ *
+ * @param list
+ * @param param
+ */
+ public void onPlaylistChanged(
+ @NonNull List<MediaItem2> list, @NonNull PlaylistParam param) { }
+
+ /**
+ * Called when the playback state is changed.
+ *
+ * @param state
+ */
+ public void onPlaybackStateChanged(@NonNull PlaybackState2 state) { }
+ }
+
+ /**
+ * Holds information about the current playback and how audio is handled for
+ * this session.
+ */
+ // The same as MediaController.PlaybackInfo
+ public static final class PlaybackInfo {
+ /**
+ * The session uses remote playback.
+ */
+ public static final int PLAYBACK_TYPE_REMOTE = 2;
+ /**
+ * The session uses local playback.
+ */
+ public static final int PLAYBACK_TYPE_LOCAL = 1;
+
+ private final int mVolumeType;
+ private final int mVolumeControl;
+ private final int mMaxVolume;
+ private final int mCurrentVolume;
+ private final AudioAttributes mAudioAttrs;
+
+ /**
+ * @hide
+ */
+ public PlaybackInfo(int type, AudioAttributes attrs, int control, int max, int current) {
+ mVolumeType = type;
+ mAudioAttrs = attrs;
+ mVolumeControl = control;
+ mMaxVolume = max;
+ mCurrentVolume = current;
+ }
+
+ /**
+ * Get the type of playback which affects volume handling. One of:
+ * <ul>
+ * <li>{@link #PLAYBACK_TYPE_LOCAL}</li>
+ * <li>{@link #PLAYBACK_TYPE_REMOTE}</li>
+ * </ul>
+ *
+ * @return The type of playback this session is using.
+ */
+ public int getPlaybackType() {
+ return mVolumeType;
+ }
+
+ /**
+ * Get the audio attributes for this session. The attributes will affect
+ * volume handling for the session. When the volume type is
+ * {@link PlaybackInfo#PLAYBACK_TYPE_REMOTE} these may be ignored by the
+ * remote volume handler.
+ *
+ * @return The attributes for this session.
+ */
+ public AudioAttributes getAudioAttributes() {
+ return mAudioAttrs;
+ }
+
+ /**
+ * Get the type of volume control that can be used. One of:
+ * <ul>
+ * <li>{@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}</li>
+ * <li>{@link VolumeProvider#VOLUME_CONTROL_RELATIVE}</li>
+ * <li>{@link VolumeProvider#VOLUME_CONTROL_FIXED}</li>
+ * </ul>
+ *
+ * @return The type of volume control that may be used with this
+ * session.
+ */
+ public int getVolumeControl() {
+ return mVolumeControl;
+ }
+
+ /**
+ * Get the maximum volume that may be set for this session.
+ *
+ * @return The maximum allowed volume where this session is playing.
+ */
+ public int getMaxVolume() {
+ return mMaxVolume;
+ }
+
+ /**
+ * Get the current volume for this session.
+ *
+ * @return The current volume where this session is playing.
+ */
+ public int getCurrentVolume() {
+ return mCurrentVolume;
+ }
+ }
+
+ private final MediaController2Provider mProvider;
+
+ /**
+ * Create a {@link MediaController2} from the {@link SessionToken2}. This connects to the session
+ * and may wake up the service if it's not available.
+ *
+ * @param context Context
+ * @param token token to connect to
+ * @param callback controller callback to receive changes in
+ * @param executor executor to run callbacks on.
+ */
+ // TODO(jaewan): Put @CallbackExecutor to the constructor.
+ public MediaController2(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull ControllerCallback callback, @NonNull Executor executor) {
+ super();
+
+ // This also connects to the token.
+ // Explicit connect() isn't added on purpose because retrying connect() is impossible with
+ // session whose session binder is only valid while it's active.
+ // prevent a controller from reusable after the
+ // session is released and recreated.
+ mProvider = createProvider(context, token, callback, executor);
+ }
+
+ MediaController2Provider createProvider(@NonNull Context context,
+ @NonNull SessionToken2 token, @NonNull ControllerCallback callback,
+ @NonNull Executor executor) {
+ return ApiLoader.getProvider(context)
+ .createMediaController2(this, context, token, callback, executor);
+ }
+
+ /**
+ * Release this object, and disconnect from the session. After this, callbacks wouldn't be
+ * received.
+ */
+ @Override
+ public void close() {
+ mProvider.close_impl();
+ }
+
+ /**
+ * @hide
+ */
+ public MediaController2Provider getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * @return token
+ */
+ public @NonNull
+ SessionToken2 getSessionToken() {
+ return mProvider.getSessionToken_impl();
+ }
+
+ /**
+ * Returns whether this class is connected to active {@link MediaSession2} or not.
+ */
+ public boolean isConnected() {
+ return mProvider.isConnected_impl();
+ }
+
+ public void play() {
+ mProvider.play_impl();
+ }
+
+ public void pause() {
+ mProvider.pause_impl();
+ }
+
+ public void stop() {
+ mProvider.stop_impl();
+ }
+
+ public void skipToPrevious() {
+ mProvider.skipToPrevious_impl();
+ }
+
+ public void skipToNext() {
+ mProvider.skipToNext_impl();
+ }
+
+ /**
+ * Request that the player prepare its playback. In other words, other sessions can continue
+ * to play during the preparation of this session. This method can be used to speed up the
+ * start of the playback. Once the preparation is done, the session will change its playback
+ * state to {@link PlaybackState2#STATE_PAUSED}. Afterwards, {@link #play} can be called to
+ * start playback.
+ */
+ public void prepare() {
+ mProvider.prepare_impl();
+ }
+
+ /**
+ * Start fast forwarding. If playback is already fast forwarding this
+ * may increase the rate.
+ */
+ public void fastForward() {
+ mProvider.fastForward_impl();
+ }
+
+ /**
+ * Start rewinding. If playback is already rewinding this may increase
+ * the rate.
+ */
+ public void rewind() {
+ mProvider.rewind_impl();
+ }
+
+ /**
+ * Move to a new location in the media stream.
+ *
+ * @param pos Position to move to, in milliseconds.
+ */
+ public void seekTo(long pos) {
+ mProvider.seekTo_impl(pos);
+ }
+
+ /**
+ * Sets the index of current DataSourceDesc in the play list to be played.
+ *
+ * @param index the index of DataSourceDesc in the play list you want to play
+ * @throws IllegalArgumentException if the play list is null
+ * @throws NullPointerException if index is outside play list range
+ */
+ public void setCurrentPlaylistItem(int index) {
+ mProvider.setCurrentPlaylistItem_impl(index);
+ }
+
+ /**
+ * @hide
+ */
+ public void skipForward() {
+ // To match with KEYCODE_MEDIA_SKIP_FORWARD
+ }
+
+ /**
+ * @hide
+ */
+ public void skipBackward() {
+ // To match with KEYCODE_MEDIA_SKIP_BACKWARD
+ }
+
+ /**
+ * Request that the player start playback for a specific media id.
+ *
+ * @param mediaId The id of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be played.
+ */
+ public void playFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+ mProvider.playFromMediaId_impl(mediaId, extras);
+ }
+
+ /**
+ * Request that the player start playback for a specific search query.
+ * An empty or null query should be treated as a request to play any
+ * music.
+ *
+ * @param query The search query.
+ * @param extras Optional extras that can include extra information
+ * about the query.
+ */
+ public void playFromSearch(@NonNull String query, @Nullable Bundle extras) {
+ mProvider.playFromSearch_impl(query, extras);
+ }
+
+ /**
+ * Request that the player start playback for a specific {@link Uri}.
+ *
+ * @param uri The URI of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be played.
+ */
+ public void playFromUri(@NonNull String uri, @Nullable Bundle extras) {
+ mProvider.playFromUri_impl(uri, extras);
+ }
+
+
+ /**
+ * Request that the player prepare playback for a specific media id. In other words, other
+ * sessions can continue to play during the preparation of this session. This method can be
+ * used to speed up the start of the playback. Once the preparation is done, the session
+ * will change its playback state to {@link PlaybackState2#STATE_PAUSED}. Afterwards,
+ * {@link #play} can be called to start playback. If the preparation is not needed,
+ * {@link #playFromMediaId} can be directly called without this method.
+ *
+ * @param mediaId The id of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be prepared.
+ */
+ public void prepareFromMediaId(@NonNull String mediaId, @Nullable Bundle extras) {
+ mProvider.prepareMediaId_impl(mediaId, extras);
+ }
+
+ /**
+ * Request that the player prepare playback for a specific search query. An empty or null
+ * query should be treated as a request to prepare any music. In other words, other sessions
+ * can continue to play during the preparation of this session. This method can be used to
+ * speed up the start of the playback. Once the preparation is done, the session will
+ * change its playback state to {@link PlaybackState2#STATE_PAUSED}. Afterwards,
+ * {@link #play} can be called to start playback. If the preparation is not needed,
+ * {@link #playFromSearch} can be directly called without this method.
+ *
+ * @param query The search query.
+ * @param extras Optional extras that can include extra information
+ * about the query.
+ */
+ public void prepareFromSearch(@NonNull String query, @Nullable Bundle extras) {
+ mProvider.prepareFromSearch_impl(query, extras);
+ }
+
+ /**
+ * Request that the player prepare playback for a specific {@link Uri}. In other words,
+ * other sessions can continue to play during the preparation of this session. This method
+ * can be used to speed up the start of the playback. Once the preparation is done, the
+ * session will change its playback state to {@link PlaybackState2#STATE_PAUSED}. Afterwards,
+ * {@link #play} can be called to start playback. If the preparation is not needed,
+ * {@link #playFromUri} can be directly called without this method.
+ *
+ * @param uri The URI of the requested media.
+ * @param extras Optional extras that can include extra information about the media item
+ * to be prepared.
+ */
+ public void prepareFromUri(@NonNull Uri uri, @Nullable Bundle extras) {
+ mProvider.prepareFromUri_impl(uri, extras);
+ }
+
+ /**
+ * Set the volume of the output this session is playing on. The command will be ignored if it
+ * does not support {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}.
+ * <p>
+ * If the session is local playback, this changes the device's volume with the stream that
+ * session's player is using. Flags will be specified for the {@link AudioManager}.
+ * <p>
+ * If the session is remote player (i.e. session has set volume provider), its volume provider
+ * will receive this request instead.
+ *
+ * @see #getPlaybackInfo()
+ * @param value The value to set it to, between 0 and the reported max.
+ * @param flags flags from {@link AudioManager} to include with the volume request for local
+ * playback
+ */
+ public void setVolumeTo(int value, int flags) {
+ mProvider.setVolumeTo_impl(value, flags);
+ }
+
+ /**
+ * Adjust the volume of the output this session is playing on. The direction
+ * must be one of {@link AudioManager#ADJUST_LOWER},
+ * {@link AudioManager#ADJUST_RAISE}, or {@link AudioManager#ADJUST_SAME}.
+ * The command will be ignored if the session does not support
+ * {@link VolumeProvider#VOLUME_CONTROL_RELATIVE} or
+ * {@link VolumeProvider#VOLUME_CONTROL_ABSOLUTE}.
+ * <p>
+ * If the session is local playback, this changes the device's volume with the stream that
+ * session's player is using. Flags will be specified for the {@link AudioManager}.
+ * <p>
+ * If the session is remote player (i.e. session has set volume provider), its volume provider
+ * will receive this request instead.
+ *
+ * @see #getPlaybackInfo()
+ * @param direction The direction to adjust the volume in.
+ * @param flags flags from {@link AudioManager} to include with the volume request for local
+ * playback
+ */
+ public void adjustVolume(int direction, int flags) {
+ mProvider.adjustVolume_impl(direction, flags);
+ }
+
+ /**
+ * Get the rating type supported by the session. One of:
+ * <ul>
+ * <li>{@link Rating2#RATING_NONE}</li>
+ * <li>{@link Rating2#RATING_HEART}</li>
+ * <li>{@link Rating2#RATING_THUMB_UP_DOWN}</li>
+ * <li>{@link Rating2#RATING_3_STARS}</li>
+ * <li>{@link Rating2#RATING_4_STARS}</li>
+ * <li>{@link Rating2#RATING_5_STARS}</li>
+ * <li>{@link Rating2#RATING_PERCENTAGE}</li>
+ * </ul>
+ *
+ * @return The supported rating type
+ */
+ public int getRatingType() {
+ return mProvider.getRatingType_impl();
+ }
+
+ /**
+ * Get an intent for launching UI associated with this session if one exists.
+ *
+ * @return A {@link PendingIntent} to launch UI or null.
+ */
+ public @Nullable PendingIntent getSessionActivity() {
+ return mProvider.getSessionActivity_impl();
+ }
+
+ /**
+ * Get the latest {@link PlaybackState2} from the session.
+ *
+ * @return a playback state
+ */
+ public PlaybackState2 getPlaybackState() {
+ return mProvider.getPlaybackState_impl();
+ }
+
+ /**
+ * Get the current playback info for this session.
+ *
+ * @return The current playback info or null.
+ */
+ public @Nullable PlaybackInfo getPlaybackInfo() {
+ return mProvider.getPlaybackInfo_impl();
+ }
+
+ /**
+ * Rate the current content. This will cause the rating to be set for
+ * the current user. The Rating type must match the type returned by
+ * {@link #getRatingType()}.
+ *
+ * @param rating The rating to set for the current content
+ */
+ public void setRating(Rating2 rating) {
+ mProvider.setRating_impl(rating);
+ }
+
+ /**
+ * Send custom command to the session
+ *
+ * @param command custom command
+ * @param args optional argument
+ * @param cb optional result receiver
+ */
+ public void sendCustomCommand(@NonNull Command command, @Nullable Bundle args,
+ @Nullable ResultReceiver cb) {
+ mProvider.sendCustomCommand_impl(command, args, cb);
+ }
+
+ /**
+ * Return playlist from the session.
+ *
+ * @return playlist. Can be {@code null} if the controller doesn't have enough permission.
+ */
+ public @Nullable List<MediaItem2> getPlaylist() {
+ return mProvider.getPlaylist_impl();
+ }
+
+ public @Nullable PlaylistParam getPlaylistParam() {
+ return mProvider.getPlaylistParam_impl();
+ }
+
+ /**
+ * Removes the media item at index in the play list.
+ *<p>
+ * If index is same as the current index of the playlist, current playback
+ * will be stopped and playback moves to next source in the list.
+ *
+ * @return the removed DataSourceDesc at index in the play list
+ * @throws IllegalArgumentException if the play list is null
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ */
+ // TODO(jaewan): Remove with index was previously rejected by council (b/36524925)
+ // TODO(jaewan): Should we also add movePlaylistItem from index to index?
+ public void removePlaylistItem(MediaItem2 item) {
+ mProvider.removePlaylistItem_impl(item);
+ }
+
+ /**
+ * Inserts the media item to the play list at position index.
+ * <p>
+ * This will not change the currently playing media item.
+ * If index is less than or equal to the current index of the play list,
+ * the current index of the play list will be incremented correspondingly.
+ *
+ * @param index the index you want to add dsd to the play list
+ * @param item the media item you want to add to the play list
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ * @throws NullPointerException if dsd is null
+ */
+ public void addPlaylistItem(int index, MediaItem2 item) {
+ mProvider.addPlaylistItem_impl(index, item);
+ }
+}
diff --git a/android/media/MediaController2Test.java b/android/media/MediaController2Test.java
new file mode 100644
index 00000000..ae67a952
--- /dev/null
+++ b/android/media/MediaController2Test.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright 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.media;
+
+import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.TestUtils.SyncHandler;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.support.test.filters.FlakyTest;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static android.media.TestUtils.createPlaybackState;
+import static org.junit.Assert.*;
+
+/**
+ * Tests {@link MediaController2}.
+ */
+// TODO(jaewan): Implement host-side test so controller and session can run in different processes.
+// TODO(jaewan): Fix flaky failure -- see MediaController2Impl.getController()
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@FlakyTest
+public class MediaController2Test extends MediaSession2TestBase {
+ private static final String TAG = "MediaController2Test";
+
+ MediaSession2 mSession;
+ MediaController2 mController;
+ MockPlayer mPlayer;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ // Create this test specific MediaSession2 to use our own Handler.
+ sHandler.postAndSync(()->{
+ mPlayer = new MockPlayer(1);
+ mSession = new MediaSession2.Builder(mContext, mPlayer).setId(TAG).build();
+ });
+
+ mController = createController(mSession.getToken());
+ TestServiceRegistry.getInstance().setHandler(sHandler);
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ sHandler.postAndSync(() -> {
+ if (mSession != null) {
+ mSession.close();
+ }
+ });
+ TestServiceRegistry.getInstance().cleanUp();
+ }
+
+ @Test
+ public void testPlay() throws InterruptedException {
+ mController.play();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPlayCalled);
+ }
+
+ @Test
+ public void testPause() throws InterruptedException {
+ mController.pause();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mPauseCalled);
+ }
+
+
+ @Test
+ public void testSkipToPrevious() throws InterruptedException {
+ mController.skipToPrevious();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mSkipToPreviousCalled);
+ }
+
+ @Test
+ public void testSkipToNext() throws InterruptedException {
+ mController.skipToNext();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mSkipToNextCalled);
+ }
+
+ @Test
+ public void testStop() throws InterruptedException {
+ mController.stop();
+ try {
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } catch (InterruptedException e) {
+ fail(e.getMessage());
+ }
+ assertTrue(mPlayer.mStopCalled);
+ }
+
+ @Test
+ public void testGetPackageName() {
+ assertEquals(mContext.getPackageName(), mController.getSessionToken().getPackageName());
+ }
+
+ @Test
+ public void testGetPlaybackState() throws InterruptedException {
+ // TODO(jaewan): add equivalent test later
+ /*
+ final CountDownLatch latch = new CountDownLatch(1);
+ final MediaPlayerBase.PlaybackListener listener = (state) -> {
+ assertEquals(PlaybackState.STATE_BUFFERING, state.getState());
+ latch.countDown();
+ };
+ assertNull(mController.getPlaybackState());
+ mController.addPlaybackListener(listener, sHandler);
+
+ mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_BUFFERING));
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertEquals(PlaybackState.STATE_BUFFERING, mController.getPlaybackState().getState());
+ */
+ }
+
+ // TODO(jaewan): add equivalent test later
+ /*
+ @Test
+ public void testAddPlaybackListener() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(2);
+ final MediaPlayerBase.PlaybackListener listener = (state) -> {
+ switch ((int) latch.getCount()) {
+ case 2:
+ assertEquals(PlaybackState.STATE_PLAYING, state.getState());
+ break;
+ case 1:
+ assertEquals(PlaybackState.STATE_PAUSED, state.getState());
+ break;
+ }
+ latch.countDown();
+ };
+
+ mController.addPlaybackListener(listener, sHandler);
+ sHandler.postAndSync(()->{
+ mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
+ mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
+ });
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+
+ @Test
+ public void testRemovePlaybackListener() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final MediaPlayerBase.PlaybackListener listener = (state) -> {
+ fail();
+ latch.countDown();
+ };
+ mController.addPlaybackListener(listener, sHandler);
+ mController.removePlaybackListener(listener);
+ mPlayer.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
+ assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ */
+
+ @Test
+ public void testControllerCallback_onConnected() throws InterruptedException {
+ // createController() uses controller callback to wait until the controller becomes
+ // available.
+ MediaController2 controller = createController(mSession.getToken());
+ assertNotNull(controller);
+ }
+
+ @Test
+ public void testControllerCallback_sessionRejects() throws InterruptedException {
+ final MediaSession2.SessionCallback sessionCallback = new SessionCallback() {
+ @Override
+ public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
+ return null;
+ }
+ };
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext, mPlayer)
+ .setSessionCallback(sHandlerExecutor, sessionCallback).build();
+ });
+ MediaController2 controller =
+ createController(mSession.getToken(), false, null);
+ assertNotNull(controller);
+ waitForConnect(controller, false);
+ waitForDisconnect(controller, true);
+ }
+
+ @Test
+ public void testControllerCallback_releaseSession() throws InterruptedException {
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ });
+ waitForDisconnect(mController, true);
+ }
+
+ @Test
+ public void testControllerCallback_release() throws InterruptedException {
+ mController.close();
+ waitForDisconnect(mController, true);
+ }
+
+ @Test
+ public void testIsConnected() throws InterruptedException {
+ assertTrue(mController.isConnected());
+ sHandler.postAndSync(()->{
+ mSession.close();
+ });
+ // postAndSync() to wait until the disconnection is propagated.
+ sHandler.postAndSync(()->{
+ assertFalse(mController.isConnected());
+ });
+ }
+
+ /**
+ * Test potential deadlock for calls between controller and session.
+ */
+ @Test
+ public void testDeadlock() throws InterruptedException {
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mSession = null;
+ });
+
+ // Two more threads are needed not to block test thread nor test wide thread (sHandler).
+ final HandlerThread sessionThread = new HandlerThread("testDeadlock_session");
+ final HandlerThread testThread = new HandlerThread("testDeadlock_test");
+ sessionThread.start();
+ testThread.start();
+ final SyncHandler sessionHandler = new SyncHandler(sessionThread.getLooper());
+ final Handler testHandler = new Handler(testThread.getLooper());
+ final CountDownLatch latch = new CountDownLatch(1);
+ try {
+ final MockPlayer player = new MockPlayer(0);
+ sessionHandler.postAndSync(() -> {
+ mSession = new MediaSession2.Builder(mContext, mPlayer)
+ .setId("testDeadlock").build();
+ });
+ final MediaController2 controller = createController(mSession.getToken());
+ testHandler.post(() -> {
+ final PlaybackState2 state = createPlaybackState(PlaybackState.STATE_ERROR);
+ for (int i = 0; i < 100; i++) {
+ // triggers call from session to controller.
+ player.notifyPlaybackState(state);
+ // triggers call from controller to session.
+ controller.play();
+
+ // Repeat above
+ player.notifyPlaybackState(state);
+ controller.pause();
+ player.notifyPlaybackState(state);
+ controller.stop();
+ player.notifyPlaybackState(state);
+ controller.skipToNext();
+ player.notifyPlaybackState(state);
+ controller.skipToPrevious();
+ }
+ // This may hang if deadlock happens.
+ latch.countDown();
+ });
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } finally {
+ if (mSession != null) {
+ sessionHandler.postAndSync(() -> {
+ // Clean up here because sessionHandler will be removed afterwards.
+ mSession.close();
+ mSession = null;
+ });
+ }
+ if (sessionThread != null) {
+ sessionThread.quitSafely();
+ }
+ if (testThread != null) {
+ testThread.quitSafely();
+ }
+ }
+ }
+
+ @Ignore
+ @Test
+ public void testGetServiceToken() {
+ SessionToken2 token = TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID);
+ assertNotNull(token);
+ assertEquals(mContext.getPackageName(), token.getPackageName());
+ assertEquals(MockMediaSessionService2.ID, token.getId());
+ assertNull(token.getSessionBinder());
+ assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+ }
+
+ private void connectToService(SessionToken2 token) throws InterruptedException {
+ mController = createController(token);
+ mSession = TestServiceRegistry.getInstance().getServiceInstance().getSession();
+ mPlayer = (MockPlayer) mSession.getPlayer();
+ }
+
+ // TODO(jaewan): Reenable when session manager detects app installs
+ @Ignore
+ @Test
+ public void testConnectToService_sessionService() throws InterruptedException {
+ connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+ testConnectToService();
+ }
+
+ // TODO(jaewan): Reenable when session manager detects app installs
+ @Ignore
+ @Test
+ public void testConnectToService_libraryService() throws InterruptedException {
+ connectToService(TestUtils.getServiceToken(mContext, MockMediaLibraryService2.ID));
+ testConnectToService();
+ }
+
+ public void testConnectToService() throws InterruptedException {
+ TestServiceRegistry serviceInfo = TestServiceRegistry.getInstance();
+ ControllerInfo info = serviceInfo.getOnConnectControllerInfo();
+ assertEquals(mContext.getPackageName(), info.getPackageName());
+ assertEquals(Process.myUid(), info.getUid());
+ assertFalse(info.isTrusted());
+
+ // Test command from controller to session service
+ mController.play();
+ assertTrue(mPlayer.mCountDownLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mPlayer.mPlayCalled);
+
+ // Test command from session service to controller
+ // TODO(jaewan): Add equivalent tests again
+ /*
+ final CountDownLatch latch = new CountDownLatch(1);
+ mController.addPlaybackListener((state) -> {
+ assertNotNull(state);
+ assertEquals(PlaybackState.STATE_REWINDING, state.getState());
+ latch.countDown();
+ }, sHandler);
+ mPlayer.notifyPlaybackState(
+ TestUtils.createPlaybackState(PlaybackState.STATE_REWINDING));
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ */
+ }
+
+ @Test
+ public void testControllerAfterSessionIsGone_session() throws InterruptedException {
+ testControllerAfterSessionIsGone(mSession.getToken().getId());
+ }
+
+ @Ignore
+ @Test
+ public void testControllerAfterSessionIsGone_sessionService() throws InterruptedException {
+ connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+ testControllerAfterSessionIsGone(MockMediaSessionService2.ID);
+ }
+
+ @Test
+ public void testClose_beforeConnected() throws InterruptedException {
+ MediaController2 controller =
+ createController(mSession.getToken(), false, null);
+ controller.close();
+ }
+
+ @Test
+ public void testClose_twice() throws InterruptedException {
+ mController.close();
+ mController.close();
+ }
+
+ @Test
+ public void testClose_session() throws InterruptedException {
+ final String id = mSession.getToken().getId();
+ mController.close();
+ // close is done immediately for session.
+ testNoInteraction();
+
+ // Test whether the controller is notified about later close of the session or
+ // re-creation.
+ testControllerAfterSessionIsGone(id);
+ }
+
+ // TODO(jaewan): Reenable when session manager detects app installs
+ @Ignore
+ @Test
+ public void testClose_sessionService() throws InterruptedException {
+ connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+ testCloseFromService();
+ }
+
+ // TODO(jaewan): Reenable when session manager detects app installs
+ @Ignore
+ @Test
+ public void testClose_libraryService() throws InterruptedException {
+ connectToService(TestUtils.getServiceToken(mContext, MockMediaSessionService2.ID));
+ testCloseFromService();
+ }
+
+ private void testCloseFromService() throws InterruptedException {
+ final String id = mController.getSessionToken().getId();
+ final CountDownLatch latch = new CountDownLatch(1);
+ TestServiceRegistry.getInstance().setServiceInstanceChangedCallback((service) -> {
+ if (service == null) {
+ // Destroying..
+ latch.countDown();
+ }
+ });
+ mController.close();
+ // Wait until close triggers onDestroy() of the session service.
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertNull(TestServiceRegistry.getInstance().getServiceInstance());
+ testNoInteraction();
+
+ // Test whether the controller is notified about later close of the session or
+ // re-creation.
+ testControllerAfterSessionIsGone(id);
+ }
+
+ private void testControllerAfterSessionIsGone(final String id) throws InterruptedException {
+ sHandler.postAndSync(() -> {
+ // TODO(jaewan): Use Session.close later when we add the API.
+ mSession.close();
+ });
+ waitForDisconnect(mController, true);
+ testNoInteraction();
+
+ // Test with the newly created session.
+ sHandler.postAndSync(() -> {
+ // Recreated session has different session stub, so previously created controller
+ // shouldn't be available.
+ mSession = new MediaSession2.Builder(mContext, mPlayer).setId(id).build();
+ });
+ testNoInteraction();
+ }
+
+ private void testNoInteraction() throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ final PlaybackListener playbackListener = (state) -> {
+ fail("Controller shouldn't be notified about change in session after the close.");
+ latch.countDown();
+ };
+ // TODO(jaewan): Add equivalent tests again
+ /*
+ mController.addPlaybackListener(playbackListener, sHandler);
+ mPlayer.notifyPlaybackState(TestUtils.createPlaybackState(PlaybackState.STATE_BUFFERING));
+ assertFalse(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ mController.removePlaybackListener(playbackListener);
+ */
+ }
+
+ // TODO(jaewan): Add test for service connect rejection, when we differentiate session
+ // active/inactive and connection accept/refuse
+}
diff --git a/android/media/MediaDrm.java b/android/media/MediaDrm.java
index e2f9b47e..063186d7 100644
--- a/android/media/MediaDrm.java
+++ b/android/media/MediaDrm.java
@@ -16,13 +16,6 @@
package android.media;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.UUID;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -33,7 +26,18 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.Parcel;
+import android.os.PersistableBundle;
import android.util.Log;
+import dalvik.system.CloseGuard;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.atomic.AtomicBoolean;
+
/**
* MediaDrm can be used to obtain keys for decrypting protected media streams, in
@@ -117,10 +121,13 @@ import android.util.Log;
* MediaDrm objects on a thread with its own Looper running (main UI
* thread by default has a Looper running).
*/
-public final class MediaDrm {
+public final class MediaDrm implements AutoCloseable {
private static final String TAG = "MediaDrm";
+ private final AtomicBoolean mClosed = new AtomicBoolean();
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+
private static final String PERMISSION = android.Manifest.permission.ACCESS_DRM_CERTIFICATES;
private EventHandler mEventHandler;
@@ -215,6 +222,8 @@ public final class MediaDrm {
*/
native_setup(new WeakReference<MediaDrm>(this),
getByteArrayFromUUID(uuid), ActivityThread.currentOpPackageName());
+
+ mCloseGuard.open("release");
}
/**
@@ -670,12 +679,14 @@ public final class MediaDrm {
private int mRequestType;
/**
- * Key request type is initial license request
+ * Key request type is initial license request. A license request
+ * is necessary to load keys.
*/
public static final int REQUEST_TYPE_INITIAL = 0;
/**
- * Key request type is license renewal
+ * Key request type is license renewal. A license request is
+ * necessary to prevent the keys from expiring.
*/
public static final int REQUEST_TYPE_RENEWAL = 1;
@@ -684,11 +695,25 @@ public final class MediaDrm {
*/
public static final int REQUEST_TYPE_RELEASE = 2;
+ /**
+ * Keys are already loaded. No license request is necessary, and no
+ * key request data is returned.
+ */
+ public static final int REQUEST_TYPE_NONE = 3;
+
+ /**
+ * Keys have been loaded but an additional license request is needed
+ * to update their values.
+ */
+ public static final int REQUEST_TYPE_UPDATE = 4;
+
/** @hide */
@IntDef({
REQUEST_TYPE_INITIAL,
REQUEST_TYPE_RENEWAL,
REQUEST_TYPE_RELEASE,
+ REQUEST_TYPE_NONE,
+ REQUEST_TYPE_UPDATE,
})
@Retention(RetentionPolicy.SOURCE)
public @interface RequestType {}
@@ -729,7 +754,8 @@ public final class MediaDrm {
/**
* Get the type of the request
* @return one of {@link #REQUEST_TYPE_INITIAL},
- * {@link #REQUEST_TYPE_RENEWAL} or {@link #REQUEST_TYPE_RELEASE}
+ * {@link #REQUEST_TYPE_RENEWAL}, {@link #REQUEST_TYPE_RELEASE},
+ * {@link #REQUEST_TYPE_NONE} or {@link #REQUEST_TYPE_UPDATE}
*/
@RequestType
public int getRequestType() { return mRequestType; }
@@ -954,6 +980,168 @@ public final class MediaDrm {
*/
public native void releaseAllSecureStops();
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({HDCP_LEVEL_UNKNOWN, HDCP_NONE, HDCP_V1, HDCP_V2,
+ HDCP_V2_1, HDCP_V2_2, HDCP_NO_DIGITAL_OUTPUT})
+ public @interface HdcpLevel {}
+
+
+ /**
+ * The DRM plugin did not report an HDCP level, or an error
+ * occurred accessing it
+ */
+ public static final int HDCP_LEVEL_UNKNOWN = 0;
+
+ /**
+ * HDCP is not supported on this device, content is unprotected
+ */
+ public static final int HDCP_NONE = 1;
+
+ /**
+ * HDCP version 1.0
+ */
+ public static final int HDCP_V1 = 2;
+
+ /**
+ * HDCP version 2.0 Type 1.
+ */
+ public static final int HDCP_V2 = 3;
+
+ /**
+ * HDCP version 2.1 Type 1.
+ */
+ public static final int HDCP_V2_1 = 4;
+
+ /**
+ * HDCP version 2.2 Type 1.
+ */
+ public static final int HDCP_V2_2 = 5;
+
+ /**
+ * No digital output, implicitly secure
+ */
+ public static final int HDCP_NO_DIGITAL_OUTPUT = Integer.MAX_VALUE;
+
+ /**
+ * Return the HDCP level negotiated with downstream receivers the
+ * device is connected to. If multiple HDCP-capable displays are
+ * simultaneously connected to separate interfaces, this method
+ * returns the lowest negotiated level of all interfaces.
+ * <p>
+ * This method should only be used for informational purposes, not for
+ * enforcing compliance with HDCP requirements. Trusted enforcement of
+ * HDCP policies must be handled by the DRM system.
+ * <p>
+ * @return one of {@link #HDCP_LEVEL_UNKNOWN}, {@link #HDCP_NONE},
+ * {@link #HDCP_V1}, {@link #HDCP_V2}, {@link #HDCP_V2_1}, {@link #HDCP_V2_2}
+ * or {@link #HDCP_NO_DIGITAL_OUTPUT}.
+ */
+ @HdcpLevel
+ public native int getConnectedHdcpLevel();
+
+ /**
+ * Return the maximum supported HDCP level. The maximum HDCP level is a
+ * constant for a given device, it does not depend on downstream receivers
+ * that may be connected. If multiple HDCP-capable interfaces are present,
+ * it indicates the highest of the maximum HDCP levels of all interfaces.
+ * <p>
+ * @return one of {@link #HDCP_LEVEL_UNKNOWN}, {@link #HDCP_NONE},
+ * {@link #HDCP_V1}, {@link #HDCP_V2}, {@link #HDCP_V2_1}, {@link #HDCP_V2_2}
+ * or {@link #HDCP_NO_DIGITAL_OUTPUT}.
+ */
+ @HdcpLevel
+ public native int getMaxHdcpLevel();
+
+ /**
+ * Return the number of MediaDrm sessions that are currently opened
+ * simultaneously among all MediaDrm instances for the active DRM scheme.
+ * @return the number of open sessions.
+ */
+ public native int getOpenSessionCount();
+
+ /**
+ * Return the maximum number of MediaDrm sessions that may be opened
+ * simultaneosly among all MediaDrm instances for the active DRM
+ * scheme. The maximum number of sessions is not affected by any
+ * sessions that may have already been opened.
+ * @return maximum sessions.
+ */
+ public native int getMaxSessionCount();
+
+ /**
+ * Security level indicates the robustness of the device's DRM
+ * implementation.
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({SECURITY_LEVEL_UNKNOWN, SW_SECURE_CRYPTO, SW_SECURE_DECODE,
+ HW_SECURE_CRYPTO, HW_SECURE_DECODE, HW_SECURE_ALL})
+ public @interface SecurityLevel {}
+
+ /**
+ * The DRM plugin did not report a security level, or an error occurred
+ * accessing it
+ */
+ public static final int SECURITY_LEVEL_UNKNOWN = 0;
+
+ /**
+ * Software-based whitebox crypto
+ */
+ public static final int SW_SECURE_CRYPTO = 1;
+
+ /**
+ * Software-based whitebox crypto and an obfuscated decoder
+ */
+ public static final int SW_SECURE_DECODE = 2;
+
+ /**
+ * DRM key management and crypto operations are performed within a
+ * hardware backed trusted execution environment
+ */
+ public static final int HW_SECURE_CRYPTO = 3;
+
+ /**
+ * DRM key management, crypto operations and decoding of content
+ * are performed within a hardware backed trusted execution environment
+ */
+ public static final int HW_SECURE_DECODE = 4;
+
+ /**
+ * DRM key management, crypto operations, decoding of content and all
+ * handling of the media (compressed and uncompressed) is handled within
+ * a hardware backed trusted execution environment.
+ */
+ public static final int HW_SECURE_ALL = 5;
+
+ /**
+ * Return the current security level of a session. A session
+ * has an initial security level determined by the robustness of
+ * the DRM system's implementation on the device. The security
+ * level may be adjusted using {@link #setSecurityLevel}.
+ * @param sessionId the session to query.
+ * <p>
+ * @return one of {@link #SECURITY_LEVEL_UNKNOWN},
+ * {@link #SW_SECURE_CRYPTO}, {@link #SW_SECURE_DECODE},
+ * {@link #HW_SECURE_CRYPTO}, {@link #HW_SECURE_DECODE} or
+ * {@link #HW_SECURE_ALL}.
+ */
+ @SecurityLevel
+ public native int getSecurityLevel(@NonNull byte[] sessionId);
+
+ /**
+ * Set the security level of a session. This can be useful if specific
+ * attributes of a lower security level are needed by an application,
+ * such as image manipulation or compositing. Reducing the security
+ * level will typically limit decryption to lower content resolutions,
+ * depending on the license policy.
+ * @param sessionId the session to set the security level on.
+ * @param level the new security level, one of
+ * {@link #SW_SECURE_CRYPTO}, {@link #SW_SECURE_DECODE},
+ * {@link #HW_SECURE_CRYPTO}, {@link #HW_SECURE_DECODE} or
+ * {@link #HW_SECURE_ALL}.
+ */
+ public native void setSecurityLevel(@NonNull byte[] sessionId,
+ @SecurityLevel int level);
+
/**
* String property name: identifies the maker of the DRM plugin
*/
@@ -1031,7 +1219,6 @@ public final class MediaDrm {
public native void setPropertyByteArray(@NonNull @ArrayProperty
String propertyName, @NonNull byte[] value);
-
private static final native void setCipherAlgorithmNative(
@NonNull MediaDrm drm, @NonNull byte[] sessionId, @NonNull String algorithm);
@@ -1058,6 +1245,25 @@ public final class MediaDrm {
@NonNull byte[] keyId, @NonNull byte[] message, @NonNull byte[] signature);
/**
+ * Return Metrics data about the current MediaDrm instance.
+ *
+ * @return a {@link PersistableBundle} containing the set of attributes and values
+ * available for this instance of MediaDrm.
+ * The attributes are described in {@link MetricsConstants}.
+ *
+ * Additional vendor-specific fields may also be present in
+ * the return value.
+ *
+ * @hide - not part of the public API at this time
+ */
+ public PersistableBundle getMetrics() {
+ PersistableBundle bundle = getMetricsNative();
+ return bundle;
+ }
+
+ private native PersistableBundle getMetricsNative();
+
+ /**
* In addition to supporting decryption of DASH Common Encrypted Media, the
* MediaDrm APIs provide the ability to securely deliver session keys from
* an operator's session key server to a client device, based on the factory-installed
@@ -1311,20 +1517,81 @@ public final class MediaDrm {
}
@Override
- protected void finalize() {
- native_finalize();
+ protected void finalize() throws Throwable {
+ try {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ release();
+ } finally {
+ super.finalize();
+ }
+ }
+
+ /**
+ * Releases resources associated with the current session of
+ * MediaDrm. It is considered good practice to call this method when
+ * the {@link MediaDrm} object is no longer needed in your
+ * application. After this method is called, {@link MediaDrm} is no
+ * longer usable since it has lost all of its required resource.
+ *
+ * This method was added in API 28. In API versions 18 through 27, release()
+ * should be called instead. There is no need to do anything for API
+ * versions prior to 18.
+ */
+ @Override
+ public void close() {
+ release();
+ }
+
+ /**
+ * @deprecated replaced by {@link #close()}.
+ */
+ @Deprecated
+ public void release() {
+ mCloseGuard.close();
+ if (mClosed.compareAndSet(false, true)) {
+ native_release();
+ }
}
- public native final void release();
+ /** @hide */
+ public native final void native_release();
+
private static native final void native_init();
private native final void native_setup(Object mediadrm_this, byte[] uuid,
String appPackageName);
- private native final void native_finalize();
-
static {
System.loadLibrary("media_jni");
native_init();
}
+
+ /**
+ * Definitions for the metrics that are reported via the
+ * {@link #getMetrics} call.
+ *
+ * @hide - not part of the public API at this time
+ */
+ public final static class MetricsConstants
+ {
+ private MetricsConstants() {}
+
+ /**
+ * Key to extract the number of successful {@link #openSession} calls
+ * from the {@link PersistableBundle} returned by a
+ * {@link #getMetrics} call.
+ */
+ public static final String OPEN_SESSION_OK_COUNT
+ = "/drm/mediadrm/open_session/ok/count";
+
+ /**
+ * Key to extract the number of failed {@link #openSession} calls
+ * from the {@link PersistableBundle} returned by a
+ * {@link #getMetrics} call.
+ */
+ public static final String OPEN_SESSION_ERROR_COUNT
+ = "/drm/mediadrm/open_session/error/count";
+ }
}
diff --git a/android/media/MediaFormat.java b/android/media/MediaFormat.java
index 306ed83c..e9128e4c 100644
--- a/android/media/MediaFormat.java
+++ b/android/media/MediaFormat.java
@@ -601,8 +601,6 @@ public final class MediaFormat {
* codec specific, but lower values generally result in more efficient
* (smaller-sized) encoding.
*
- * @hide
- *
* @see MediaCodecInfo.EncoderCapabilities#getQualityRange()
*/
public static final String KEY_QUALITY = "quality";
@@ -680,6 +678,21 @@ public final class MediaFormat {
public static final String KEY_LATENCY = "latency";
/**
+ * An optional key describing the maximum number of non-display-order coded frames.
+ * This is an optional parameter that applies only to video encoders. Application should
+ * check the value for this key in the output format to see if codec will produce
+ * non-display-order coded frames. If encoder supports it, the output frames' order will be
+ * different from the display order and each frame's display order could be retrived from
+ * {@link MediaCodec.BufferInfo#presentationTimeUs}. Before API level 27, application may
+ * receive non-display-order coded frames even though the application did not request it.
+ * Note: Application should not rearrange the frames to display order before feeding them
+ * to {@link MediaMuxer#writeSampleData}.
+ * <p>
+ * The default value is 0.
+ */
+ public static final String KEY_OUTPUT_REORDER_DEPTH = "output-reorder-depth";
+
+ /**
* A key describing the desired clockwise rotation on an output surface.
* This key is only used when the codec is configured using an output surface.
* The associated value is an integer, representing degrees. Supported values
diff --git a/android/media/MediaItem2.java b/android/media/MediaItem2.java
new file mode 100644
index 00000000..96a87d5d
--- /dev/null
+++ b/android/media/MediaItem2.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class with information on a single media item with the metadata information.
+ * Media item are application dependent so we cannot guarantee that they contain the right values.
+ * <p>
+ * When it's sent to a controller or browser, it's anonymized and data descriptor wouldn't be sent.
+ * <p>
+ * This object isn't a thread safe.
+ *
+ * @hide
+ */
+// TODO(jaewan): Unhide and extends from DataSourceDesc.
+// Note) Feels like an anti-pattern. We should anonymize MediaItem2 to remove *all*
+// information in the DataSourceDesc. Why it should extends from this?
+// TODO(jaewan): Move this to updatable
+// Previously MediaBrowser.MediaItem
+public class MediaItem2 {
+ private final int mFlags;
+ private MediaMetadata2 mMetadata;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag=true, value = { FLAG_BROWSABLE, FLAG_PLAYABLE })
+ public @interface Flags { }
+
+ /**
+ * Flag: Indicates that the item has children of its own.
+ */
+ public static final int FLAG_BROWSABLE = 1 << 0;
+
+ /**
+ * Flag: Indicates that the item is playable.
+ * <p>
+ * The id of this item may be passed to
+ * {@link MediaController2#playFromMediaId(String, Bundle)}
+ */
+ public static final int FLAG_PLAYABLE = 1 << 1;
+
+ /**
+ * Create a new media item.
+ *
+ * @param metadata metadata with the media id.
+ * @param flags The flags for this item.
+ */
+ public MediaItem2(@NonNull MediaMetadata2 metadata, @Flags int flags) {
+ mFlags = flags;
+ setMetadata(metadata);
+ }
+
+ /**
+ * Return this object as a bundle to share between processes.
+ *
+ * @return a new bundle instance
+ */
+ public Bundle toBundle() {
+ // TODO(jaewan): Fill here when we rebase.
+ return new Bundle();
+ }
+
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("MediaItem2{");
+ sb.append("mFlags=").append(mFlags);
+ sb.append(", mMetadata=").append(mMetadata);
+ sb.append('}');
+ return sb.toString();
+ }
+
+ /**
+ * Gets the flags of the item.
+ */
+ public @Flags int getFlags() {
+ return mFlags;
+ }
+
+ /**
+ * Returns whether this item is browsable.
+ * @see #FLAG_BROWSABLE
+ */
+ public boolean isBrowsable() {
+ return (mFlags & FLAG_BROWSABLE) != 0;
+ }
+
+ /**
+ * Returns whether this item is playable.
+ * @see #FLAG_PLAYABLE
+ */
+ public boolean isPlayable() {
+ return (mFlags & FLAG_PLAYABLE) != 0;
+ }
+
+ /**
+ * Set a metadata. Metadata shouldn't be null and should have non-empty media id.
+ *
+ * @param metadata
+ */
+ public void setMetadata(@NonNull MediaMetadata2 metadata) {
+ if (metadata == null) {
+ throw new IllegalArgumentException("metadata cannot be null");
+ }
+ if (TextUtils.isEmpty(metadata.getMediaId())) {
+ throw new IllegalArgumentException("metadata must have a non-empty media id");
+ }
+ mMetadata = metadata;
+ }
+
+ /**
+ * Returns the metadata of the media.
+ */
+ public @NonNull MediaMetadata2 getMetadata() {
+ return mMetadata;
+ }
+
+ /**
+ * Returns the media id in the {@link MediaMetadata2} for this item.
+ * @see MediaMetadata2#METADATA_KEY_MEDIA_ID
+ */
+ public @Nullable String getMediaId() {
+ return mMetadata.getMediaId();
+ }
+}
diff --git a/android/media/MediaLibraryService2.java b/android/media/MediaLibraryService2.java
new file mode 100644
index 00000000..d7e43ec9
--- /dev/null
+++ b/android/media/MediaLibraryService2.java
@@ -0,0 +1,350 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.media.MediaSession2.BuilderBase;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.update.ApiLoader;
+import android.media.update.MediaLibraryService2Provider.MediaLibrarySessionProvider;
+import android.media.update.MediaSession2Provider;
+import android.media.update.MediaSessionService2Provider;
+import android.os.Bundle;
+import android.service.media.MediaBrowserService.BrowserRoot;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Base class for media library services.
+ * <p>
+ * Media library services enable applications to browse media content provided by an application
+ * and ask the application to start playing it. They may also be used to control content that
+ * is already playing by way of a {@link MediaSession2}.
+ * <p>
+ * To extend this class, adding followings directly to your {@code AndroidManifest.xml}.
+ * <pre>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.media.MediaLibraryService2" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;/service&gt;</pre>
+ * <p>
+ * A {@link MediaLibraryService2} is extension of {@link MediaSessionService2}. IDs shouldn't
+ * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By
+ * default, an empty string will be used for ID of the service. If you want to specify an ID,
+ * declare metadata in the manifest as follows.
+ * @hide
+ */
+// TODO(jaewan): Unhide
+public abstract class MediaLibraryService2 extends MediaSessionService2 {
+ /**
+ * This is the interface name that a service implementing a session service should say that it
+ * support -- that is, this is the action it uses for its intent filter.
+ */
+ public static final String SERVICE_INTERFACE = "android.media.MediaLibraryService2";
+
+ /**
+ * Session for the media library service.
+ */
+ public class MediaLibrarySession extends MediaSession2 {
+ private final MediaLibrarySessionProvider mProvider;
+
+ MediaLibrarySession(Context context, MediaPlayerBase player, String id,
+ Executor callbackExecutor, SessionCallback callback, VolumeProvider volumeProvider,
+ int ratingType, PendingIntent sessionActivity) {
+ super(context, player, id, callbackExecutor, callback, volumeProvider, ratingType,
+ sessionActivity);
+ mProvider = (MediaLibrarySessionProvider) getProvider();
+ }
+
+ @Override
+ MediaSession2Provider createProvider(Context context, MediaPlayerBase player, String id,
+ Executor callbackExecutor, SessionCallback callback, VolumeProvider volumeProvider,
+ int ratingType, PendingIntent sessionActivity) {
+ return ApiLoader.getProvider(context)
+ .createMediaLibraryService2MediaLibrarySession(this, context, player, id,
+ callbackExecutor, (MediaLibrarySessionCallback) callback,
+ volumeProvider, ratingType, sessionActivity);
+ }
+
+ /**
+ * Notify subscribed controller about change in a parent's children.
+ *
+ * @param controller controller to notify
+ * @param parentId
+ * @param options
+ */
+ public void notifyChildrenChanged(@NonNull ControllerInfo controller,
+ @NonNull String parentId, @NonNull Bundle options) {
+ mProvider.notifyChildrenChanged_impl(controller, parentId, options);
+ }
+
+ /**
+ * Notify subscribed controller about change in a parent's children.
+ *
+ * @param parentId parent id
+ * @param options optional bundle
+ */
+ // This is for the backward compatibility.
+ public void notifyChildrenChanged(@NonNull String parentId, @Nullable Bundle options) {
+ mProvider.notifyChildrenChanged_impl(parentId, options);
+ }
+ }
+
+ public static class MediaLibrarySessionCallback extends MediaSession2.SessionCallback {
+ /**
+ * Called to get the root information for browsing by a particular client.
+ * <p>
+ * The implementation should verify that the client package has permission
+ * to access browse media information before returning the root id; it
+ * should return null if the client is not allowed to access this
+ * information.
+ *
+ * @param controllerInfo information of the controller requesting access to browse media.
+ * @param rootHints An optional bundle of service-specific arguments to send
+ * to the media browser service when connecting and retrieving the
+ * root id for browsing, or null if none. The contents of this
+ * bundle may affect the information returned when browsing.
+ * @return The {@link BrowserRoot} for accessing this app's content or null.
+ * @see BrowserRoot#EXTRA_RECENT
+ * @see BrowserRoot#EXTRA_OFFLINE
+ * @see BrowserRoot#EXTRA_SUGGESTED
+ */
+ public @Nullable BrowserRoot onGetRoot(@NonNull ControllerInfo controllerInfo,
+ @Nullable Bundle rootHints) {
+ return null;
+ }
+
+ /**
+ * Called to get the search result. Return search result here for the browser.
+ * <p>
+ * Return an empty list for no search result, and return {@code null} for the error.
+ *
+ * @param query The search query sent from the media browser. It contains keywords separated
+ * by space.
+ * @param extras The bundle of service-specific arguments sent from the media browser.
+ * @return search result. {@code null} for error.
+ */
+ public @Nullable List<MediaItem2> onSearch(@NonNull ControllerInfo controllerInfo,
+ @NonNull String query, @Nullable Bundle extras) {
+ return null;
+ }
+
+ /**
+ * Called to get the search result . Return result here for the browser.
+ * <p>
+ * Return an empty list for no search result, and return {@code null} for the error.
+ *
+ * @param itemId item id to get media item.
+ * @return media item2. {@code null} for error.
+ */
+ public @Nullable MediaItem2 onLoadItem(@NonNull ControllerInfo controllerInfo,
+ @NonNull String itemId) {
+ return null;
+ }
+
+ /**
+ * Called to get the search result. Return search result here for the browser.
+ * <p>
+ * Return an empty list for no search result, and return {@code null} for the error.
+ *
+ * @param parentId parent id to get children
+ * @param page number of page
+ * @param pageSize size of the page
+ * @param options
+ * @return list of children. Can be {@code null}.
+ */
+ public @Nullable List<MediaItem2> onLoadChildren(@NonNull ControllerInfo controller,
+ @NonNull String parentId, int page, int pageSize, @Nullable Bundle options) {
+ return null;
+ }
+
+ /**
+ * Called when a controller subscribes to the parent.
+ *
+ * @param controller controller
+ * @param parentId parent id
+ * @param options optional bundle
+ */
+ public void onSubscribed(@NonNull ControllerInfo controller,
+ String parentId, @Nullable Bundle options) {
+ }
+
+ /**
+ * Called when a controller unsubscribes to the parent.
+ *
+ * @param controller controller
+ * @param parentId parent id
+ * @param options optional bundle
+ */
+ public void onUnsubscribed(@NonNull ControllerInfo controller,
+ String parentId, @Nullable Bundle options) {
+ }
+ }
+
+ /**
+ * Builder for {@link MediaLibrarySession}.
+ */
+ // TODO(jaewan): Move this to updatable.
+ public class MediaLibrarySessionBuilder
+ extends BuilderBase<MediaLibrarySessionBuilder, MediaLibrarySessionCallback> {
+ public MediaLibrarySessionBuilder(
+ @NonNull Context context, @NonNull MediaPlayerBase player,
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull MediaLibrarySessionCallback callback) {
+ super(context, player);
+ setSessionCallback(callbackExecutor, callback);
+ }
+
+ @Override
+ public MediaLibrarySessionBuilder setSessionCallback(
+ @NonNull @CallbackExecutor Executor callbackExecutor,
+ @NonNull MediaLibrarySessionCallback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("MediaLibrarySessionCallback cannot be null");
+ }
+ return super.setSessionCallback(callbackExecutor, callback);
+ }
+
+ @Override
+ public MediaLibrarySession build() throws IllegalStateException {
+ return new MediaLibrarySession(mContext, mPlayer, mId, mCallbackExecutor, mCallback,
+ mVolumeProvider, mRatingType, mSessionActivity);
+ }
+ }
+
+ @Override
+ MediaSessionService2Provider createProvider() {
+ return ApiLoader.getProvider(this).createMediaLibraryService2(this);
+ }
+
+ /**
+ * Called when another app requested to start this service.
+ * <p>
+ * Library service will accept or reject the connection with the
+ * {@link MediaLibrarySessionCallback} in the created session.
+ * <p>
+ * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the
+ * expected ID that you've specified through the AndroidManifest.xml.
+ * <p>
+ * This method will be called on the main thread.
+ *
+ * @param sessionId session id written in the AndroidManifest.xml.
+ * @return a new browser session
+ * @see MediaLibrarySessionBuilder
+ * @see #getSession()
+ * @throws RuntimeException if returned session is invalid
+ */
+ @Override
+ public @NonNull abstract MediaLibrarySession onCreateSession(String sessionId);
+
+ /**
+ * Contains information that the browser service needs to send to the client
+ * when first connected.
+ */
+ public static final class BrowserRoot {
+ /**
+ * The lookup key for a boolean that indicates whether the browser service should return a
+ * browser root for recently played media items.
+ *
+ * <p>When creating a media browser for a given media browser service, this key can be
+ * supplied as a root hint for retrieving media items that are recently played.
+ * If the media browser service can provide such media items, the implementation must return
+ * the key in the root hint when
+ * {@link MediaLibrarySessionCallback#onGetRoot(ControllerInfo, Bundle)} is called back.
+ *
+ * <p>The root hint may contain multiple keys.
+ *
+ * @see #EXTRA_OFFLINE
+ * @see #EXTRA_SUGGESTED
+ */
+ public static final String EXTRA_RECENT = "android.service.media.extra.RECENT";
+
+ /**
+ * The lookup key for a boolean that indicates whether the browser service should return a
+ * browser root for offline media items.
+ *
+ * <p>When creating a media browser for a given media browser service, this key can be
+ * supplied as a root hint for retrieving media items that are can be played without an
+ * internet connection.
+ * If the media browser service can provide such media items, the implementation must return
+ * the key in the root hint when
+ * {@link MediaLibrarySessionCallback#onGetRoot(ControllerInfo, Bundle)} is called back.
+ *
+ * <p>The root hint may contain multiple keys.
+ *
+ * @see #EXTRA_RECENT
+ * @see #EXTRA_SUGGESTED
+ */
+ public static final String EXTRA_OFFLINE = "android.service.media.extra.OFFLINE";
+
+ /**
+ * The lookup key for a boolean that indicates whether the browser service should return a
+ * browser root for suggested media items.
+ *
+ * <p>When creating a media browser for a given media browser service, this key can be
+ * supplied as a root hint for retrieving the media items suggested by the media browser
+ * service. The list of media items passed in {@link android.media.browse.MediaBrowser.SubscriptionCallback#onChildrenLoaded(String, List)}
+ * is considered ordered by relevance, first being the top suggestion.
+ * If the media browser service can provide such media items, the implementation must return
+ * the key in the root hint when
+ * {@link MediaLibrarySessionCallback#onGetRoot(ControllerInfo, Bundle)} is called back.
+ *
+ * <p>The root hint may contain multiple keys.
+ *
+ * @see #EXTRA_RECENT
+ * @see #EXTRA_OFFLINE
+ */
+ public static final String EXTRA_SUGGESTED = "android.service.media.extra.SUGGESTED";
+
+ final private String mRootId;
+ final private Bundle mExtras;
+
+ /**
+ * Constructs a browser root.
+ * @param rootId The root id for browsing.
+ * @param extras Any extras about the browser service.
+ */
+ public BrowserRoot(@NonNull String rootId, @Nullable Bundle extras) {
+ if (rootId == null) {
+ throw new IllegalArgumentException("The root id in BrowserRoot cannot be null. " +
+ "Use null for BrowserRoot instead.");
+ }
+ mRootId = rootId;
+ mExtras = extras;
+ }
+
+ /**
+ * Gets the root id for browsing.
+ */
+ public String getRootId() {
+ return mRootId;
+ }
+
+ /**
+ * Gets any extras about the browser service.
+ */
+ public Bundle getExtras() {
+ return mExtras;
+ }
+ }
+}
diff --git a/android/media/MediaMetadata2.java b/android/media/MediaMetadata2.java
new file mode 100644
index 00000000..0e24db65
--- /dev/null
+++ b/android/media/MediaMetadata2.java
@@ -0,0 +1,815 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.Nullable;
+import android.annotation.StringDef;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.ArrayMap;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Set;
+
+/**
+ * Contains metadata about an item, such as the title, artist, etc.
+ * @hide
+ */
+// TODO(jaewan): Move this to updatable
+public final class MediaMetadata2 {
+ private static final String TAG = "MediaMetadata2";
+
+ /**
+ * The title of the media.
+ */
+ public static final String METADATA_KEY_TITLE = "android.media.metadata.TITLE";
+
+ /**
+ * The artist of the media.
+ */
+ public static final String METADATA_KEY_ARTIST = "android.media.metadata.ARTIST";
+
+ /**
+ * The duration of the media in ms. A negative duration indicates that the
+ * duration is unknown (or infinite).
+ */
+ public static final String METADATA_KEY_DURATION = "android.media.metadata.DURATION";
+
+ /**
+ * The album title for the media.
+ */
+ public static final String METADATA_KEY_ALBUM = "android.media.metadata.ALBUM";
+
+ /**
+ * The author of the media.
+ */
+ public static final String METADATA_KEY_AUTHOR = "android.media.metadata.AUTHOR";
+
+ /**
+ * The writer of the media.
+ */
+ public static final String METADATA_KEY_WRITER = "android.media.metadata.WRITER";
+
+ /**
+ * The composer of the media.
+ */
+ public static final String METADATA_KEY_COMPOSER = "android.media.metadata.COMPOSER";
+
+ /**
+ * The compilation status of the media.
+ */
+ public static final String METADATA_KEY_COMPILATION = "android.media.metadata.COMPILATION";
+
+ /**
+ * The date the media was created or published. The format is unspecified
+ * but RFC 3339 is recommended.
+ */
+ public static final String METADATA_KEY_DATE = "android.media.metadata.DATE";
+
+ /**
+ * The year the media was created or published as a long.
+ */
+ public static final String METADATA_KEY_YEAR = "android.media.metadata.YEAR";
+
+ /**
+ * The genre of the media.
+ */
+ public static final String METADATA_KEY_GENRE = "android.media.metadata.GENRE";
+
+ /**
+ * The track number for the media.
+ */
+ public static final String METADATA_KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+
+ /**
+ * The number of tracks in the media's original source.
+ */
+ public static final String METADATA_KEY_NUM_TRACKS = "android.media.metadata.NUM_TRACKS";
+
+ /**
+ * The disc number for the media's original source.
+ */
+ public static final String METADATA_KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+
+ /**
+ * The artist for the album of the media's original source.
+ */
+ public static final String METADATA_KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+
+ /**
+ * The artwork for the media as a {@link Bitmap}.
+ *
+ * The artwork should be relatively small and may be scaled down
+ * if it is too large. For higher resolution artwork
+ * {@link #METADATA_KEY_ART_URI} should be used instead.
+ */
+ public static final String METADATA_KEY_ART = "android.media.metadata.ART";
+
+ /**
+ * The artwork for the media as a Uri style String.
+ */
+ public static final String METADATA_KEY_ART_URI = "android.media.metadata.ART_URI";
+
+ /**
+ * The artwork for the album of the media's original source as a
+ * {@link Bitmap}.
+ * The artwork should be relatively small and may be scaled down
+ * if it is too large. For higher resolution artwork
+ * {@link #METADATA_KEY_ALBUM_ART_URI} should be used instead.
+ */
+ public static final String METADATA_KEY_ALBUM_ART = "android.media.metadata.ALBUM_ART";
+
+ /**
+ * The artwork for the album of the media's original source as a Uri style
+ * String.
+ */
+ public static final String METADATA_KEY_ALBUM_ART_URI = "android.media.metadata.ALBUM_ART_URI";
+
+ /**
+ * The user's rating for the media.
+ *
+ * @see Rating
+ */
+ public static final String METADATA_KEY_USER_RATING = "android.media.metadata.USER_RATING";
+
+ /**
+ * The overall rating for the media.
+ *
+ * @see Rating
+ */
+ public static final String METADATA_KEY_RATING = "android.media.metadata.RATING";
+
+ /**
+ * A title that is suitable for display to the user. This will generally be
+ * the same as {@link #METADATA_KEY_TITLE} but may differ for some formats.
+ * When displaying media described by this metadata this should be preferred
+ * if present.
+ */
+ public static final String METADATA_KEY_DISPLAY_TITLE = "android.media.metadata.DISPLAY_TITLE";
+
+ /**
+ * A subtitle that is suitable for display to the user. When displaying a
+ * second line for media described by this metadata this should be preferred
+ * to other fields if present.
+ */
+ public static final String METADATA_KEY_DISPLAY_SUBTITLE
+ = "android.media.metadata.DISPLAY_SUBTITLE";
+
+ /**
+ * A description that is suitable for display to the user. When displaying
+ * more information for media described by this metadata this should be
+ * preferred to other fields if present.
+ */
+ public static final String METADATA_KEY_DISPLAY_DESCRIPTION
+ = "android.media.metadata.DISPLAY_DESCRIPTION";
+
+ /**
+ * An icon or thumbnail that is suitable for display to the user. When
+ * displaying an icon for media described by this metadata this should be
+ * preferred to other fields if present. This must be a {@link Bitmap}.
+ *
+ * The icon should be relatively small and may be scaled down
+ * if it is too large. For higher resolution artwork
+ * {@link #METADATA_KEY_DISPLAY_ICON_URI} should be used instead.
+ */
+ public static final String METADATA_KEY_DISPLAY_ICON
+ = "android.media.metadata.DISPLAY_ICON";
+
+ /**
+ * An icon or thumbnail that is suitable for display to the user. When
+ * displaying more information for media described by this metadata the
+ * display description should be preferred to other fields when present.
+ * This must be a Uri style String.
+ */
+ public static final String METADATA_KEY_DISPLAY_ICON_URI
+ = "android.media.metadata.DISPLAY_ICON_URI";
+
+ /**
+ * A String key for identifying the content. This value is specific to the
+ * service providing the content. If used, this should be a persistent
+ * unique key for the underlying content. It may be used with
+ * {@link MediaController2#playFromMediaId(String, Bundle)}
+ * to initiate playback when provided by a {@link MediaBrowser2} connected to
+ * the same app.
+ */
+ public static final String METADATA_KEY_MEDIA_ID = "android.media.metadata.MEDIA_ID";
+
+ /**
+ * A Uri formatted String representing the content. This value is specific to the
+ * service providing the content. It may be used with
+ * {@link MediaController2#playFromUri(Uri, Bundle)}
+ * to initiate playback when provided by a {@link MediaBrowser2} connected to
+ * the same app.
+ */
+ public static final String METADATA_KEY_MEDIA_URI = "android.media.metadata.MEDIA_URI";
+
+ /**
+ * The bluetooth folder type of the media specified in the section 6.10.2.2 of the Bluetooth
+ * AVRCP 1.5. It should be one of the following:
+ * <ul>
+ * <li>{@link #BT_FOLDER_TYPE_MIXED}</li>
+ * <li>{@link #BT_FOLDER_TYPE_TITLES}</li>
+ * <li>{@link #BT_FOLDER_TYPE_ALBUMS}</li>
+ * <li>{@link #BT_FOLDER_TYPE_ARTISTS}</li>
+ * <li>{@link #BT_FOLDER_TYPE_GENRES}</li>
+ * <li>{@link #BT_FOLDER_TYPE_PLAYLISTS}</li>
+ * <li>{@link #BT_FOLDER_TYPE_YEARS}</li>
+ * </ul>
+ */
+ public static final String METADATA_KEY_BT_FOLDER_TYPE
+ = "android.media.metadata.BT_FOLDER_TYPE";
+
+ /**
+ * The type of folder that is unknown or contains media elements of mixed types as specified in
+ * the section 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_MIXED = 0;
+
+ /**
+ * The type of folder that contains media elements only as specified in the section 6.10.2.2 of
+ * the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_TITLES = 1;
+
+ /**
+ * The type of folder that contains folders categorized by album as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_ALBUMS = 2;
+
+ /**
+ * The type of folder that contains folders categorized by artist as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_ARTISTS = 3;
+
+ /**
+ * The type of folder that contains folders categorized by genre as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_GENRES = 4;
+
+ /**
+ * The type of folder that contains folders categorized by playlist as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_PLAYLISTS = 5;
+
+ /**
+ * The type of folder that contains folders categorized by year as specified in the section
+ * 6.10.2.2 of the Bluetooth AVRCP 1.5.
+ */
+ public static final long BT_FOLDER_TYPE_YEARS = 6;
+
+ /**
+ * Whether the media is an advertisement. A value of 0 indicates it is not an advertisement. A
+ * value of 1 or non-zero indicates it is an advertisement. If not specified, this value is set
+ * to 0 by default.
+ */
+ public static final String METADATA_KEY_ADVERTISEMENT = "android.media.metadata.ADVERTISEMENT";
+
+ /**
+ * The download status of the media which will be used for later offline playback. It should be
+ * one of the following:
+ *
+ * <ul>
+ * <li>{@link #STATUS_NOT_DOWNLOADED}</li>
+ * <li>{@link #STATUS_DOWNLOADING}</li>
+ * <li>{@link #STATUS_DOWNLOADED}</li>
+ * </ul>
+ */
+ public static final String METADATA_KEY_DOWNLOAD_STATUS =
+ "android.media.metadata.DOWNLOAD_STATUS";
+
+ /**
+ * The status value to indicate the media item is not downloaded.
+ *
+ * @see #METADATA_KEY_DOWNLOAD_STATUS
+ */
+ public static final long STATUS_NOT_DOWNLOADED = 0;
+
+ /**
+ * The status value to indicate the media item is being downloaded.
+ *
+ * @see #METADATA_KEY_DOWNLOAD_STATUS
+ */
+ public static final long STATUS_DOWNLOADING = 1;
+
+ /**
+ * The status value to indicate the media item is downloaded for later offline playback.
+ *
+ * @see #METADATA_KEY_DOWNLOAD_STATUS
+ */
+ public static final long STATUS_DOWNLOADED = 2;
+
+ /**
+ * A {@link Bundle} extra.
+ * @hide
+ */
+ public static final String METADATA_KEY_EXTRA = "android.media.metadata.EXTRA";
+
+ /**
+ * @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})
+ @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,
+ METADATA_KEY_ADVERTISEMENT, METADATA_KEY_DOWNLOAD_STATUS})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LongKey {}
+
+ /**
+ * @hide
+ */
+ @StringDef({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})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RatingKey {}
+
+ static final int METADATA_TYPE_LONG = 0;
+ static final int METADATA_TYPE_TEXT = 1;
+ static final int METADATA_TYPE_BITMAP = 2;
+ static final int METADATA_TYPE_RATING = 3;
+ static final ArrayMap<String, Integer> METADATA_KEYS_TYPE;
+
+ static {
+ METADATA_KEYS_TYPE = new ArrayMap<String, Integer>();
+ METADATA_KEYS_TYPE.put(METADATA_KEY_TITLE, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ARTIST, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DURATION, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_AUTHOR, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_WRITER, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_COMPOSER, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_COMPILATION, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DATE, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_YEAR, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_GENRE, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_TRACK_NUMBER, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_NUM_TRACKS, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DISC_NUMBER, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ARTIST, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ART, METADATA_TYPE_BITMAP);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ART_URI, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART, METADATA_TYPE_BITMAP);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ALBUM_ART_URI, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_USER_RATING, METADATA_TYPE_RATING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_RATING, METADATA_TYPE_RATING);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_TITLE, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_SUBTITLE, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_DESCRIPTION, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON, METADATA_TYPE_BITMAP);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DISPLAY_ICON_URI, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_ID, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_BT_FOLDER_TYPE, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_MEDIA_URI, METADATA_TYPE_TEXT);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_ADVERTISEMENT, METADATA_TYPE_LONG);
+ METADATA_KEYS_TYPE.put(METADATA_KEY_DOWNLOAD_STATUS, METADATA_TYPE_LONG);
+ }
+
+ private static final @TextKey String[] PREFERRED_DESCRIPTION_ORDER = {
+ METADATA_KEY_TITLE,
+ METADATA_KEY_ARTIST,
+ METADATA_KEY_ALBUM,
+ METADATA_KEY_ALBUM_ARTIST,
+ METADATA_KEY_WRITER,
+ METADATA_KEY_AUTHOR,
+ METADATA_KEY_COMPOSER
+ };
+
+ private static final @BitmapKey String[] PREFERRED_BITMAP_ORDER = {
+ METADATA_KEY_DISPLAY_ICON,
+ METADATA_KEY_ART,
+ METADATA_KEY_ALBUM_ART
+ };
+
+ private static final @TextKey String[] PREFERRED_URI_ORDER = {
+ METADATA_KEY_DISPLAY_ICON_URI,
+ METADATA_KEY_ART_URI,
+ METADATA_KEY_ALBUM_ART_URI
+ };
+
+ final Bundle mBundle;
+
+ /**
+ * @hide
+ */
+ public MediaMetadata2(Bundle bundle) {
+ mBundle = new Bundle(bundle);
+ }
+
+ /**
+ * Returns true if the given key is contained in the metadata
+ *
+ * @param key a String key
+ * @return true if the key exists in this metadata, false otherwise
+ */
+ public boolean containsKey(String key) {
+ return mBundle.containsKey(key);
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if no mapping of
+ * the desired type exists for the given key or a null value is explicitly
+ * associated with the key.
+ *
+ * @param key The key the value is stored under
+ * @return a CharSequence value, or null
+ */
+ public CharSequence getText(@TextKey String key) {
+ return mBundle.getCharSequence(key);
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if no mapping of
+ * the desired type exists for the given key or a null value is explicitly
+ * associated with the key.
+ *
+ * @
+ * @return media id. Can be {@code null}
+ */
+ public @Nullable String getMediaId() {
+ return getString(METADATA_KEY_MEDIA_ID);
+ }
+
+ /**
+ * Returns the value associated with the given key, or null if no mapping of
+ * the desired type exists for the given key or a null value is explicitly
+ * associated with the key.
+ *
+ * @param key The key the value is stored under
+ * @return a String value, or null
+ */
+ public String getString(@TextKey String key) {
+ CharSequence text = mBundle.getCharSequence(key);
+ if (text != null) {
+ return text.toString();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the value associated with the given key, or 0L if no long exists
+ * for the given key.
+ *
+ * @param key The key the value is stored under
+ * @return a long value
+ */
+ public long getLong(@LongKey String key) {
+ return mBundle.getLong(key, 0);
+ }
+
+ /**
+ * Return a {@link Rating2} for the given key or null if no rating exists for
+ * the given key.
+ *
+ * @param key The key the value is stored under
+ * @return A {@link Rating2} or null
+ */
+ public Rating2 getRating(@RatingKey String key) {
+ // TODO(jaewan): Add backward compatibility
+ Rating2 rating = null;
+ try {
+ rating = Rating2.fromBundle(mBundle.getBundle(key));
+ } catch (Exception e) {
+ // ignore, value was not a rating
+ Log.w(TAG, "Failed to retrieve a key as Rating.", e);
+ }
+ return rating;
+ }
+
+ /**
+ * Return a {@link Bitmap} for the given key or null if no bitmap exists for
+ * the given key.
+ *
+ * @param key The key the value is stored under
+ * @return A {@link Bitmap} or null
+ */
+ public Bitmap getBitmap(@BitmapKey String key) {
+ Bitmap bmp = null;
+ try {
+ bmp = mBundle.getParcelable(key);
+ } catch (Exception e) {
+ // ignore, value was not a bitmap
+ Log.w(TAG, "Failed to retrieve a key as Bitmap.", e);
+ }
+ return bmp;
+ }
+
+ /**
+ * Get the extra {@link Bundle} from the metadata object.
+ *
+ * @return A {@link Bundle} or {@code null}
+ */
+ public Bundle getExtra() {
+ try {
+ return mBundle.getBundle(METADATA_KEY_EXTRA);
+ } catch (Exception e) {
+ // ignore, value was not an bundle
+ Log.w(TAG, "Failed to retrieve an extra");
+ }
+ return null;
+ }
+
+ /**
+ * Get the number of fields in this metadata.
+ *
+ * @return The number of fields in the metadata.
+ */
+ public int size() {
+ return mBundle.size();
+ }
+
+ /**
+ * Returns a Set containing the Strings used as keys in this metadata.
+ *
+ * @return a Set of String keys
+ */
+ public Set<String> keySet() {
+ return mBundle.keySet();
+ }
+
+ /**
+ * Gets the bundle backing the metadata object. This is available to support
+ * backwards compatibility. Apps should not modify the bundle directly.
+ *
+ * @return The Bundle backing this metadata.
+ */
+ public Bundle getBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Use to build MediaMetadata2 objects. The system defined metadata keys must
+ * use the appropriate data type.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+
+ /**
+ * Create an empty Builder. Any field that should be included in the
+ * {@link MediaMetadata2} must be added.
+ */
+ public Builder() {
+ mBundle = new Bundle();
+ }
+
+ /**
+ * Create a Builder using a {@link MediaMetadata2} instance to set the
+ * initial values. All fields in the source metadata will be included in
+ * the new metadata. Fields can be overwritten by adding the same key.
+ *
+ * @param source
+ */
+ public Builder(MediaMetadata2 source) {
+ mBundle = new Bundle(source.mBundle);
+ }
+
+ /**
+ * Create a Builder using a {@link MediaMetadata2} instance to set
+ * initial values, but replace bitmaps with a scaled down copy if they
+ * are larger than maxBitmapSize.
+ *
+ * @param source The original metadata to copy.
+ * @param maxBitmapSize The maximum height/width for bitmaps contained
+ * in the metadata.
+ * @hide
+ */
+ public Builder(MediaMetadata2 source, int maxBitmapSize) {
+ this(source);
+ for (String key : mBundle.keySet()) {
+ Object value = mBundle.get(key);
+ if (value instanceof Bitmap) {
+ Bitmap bmp = (Bitmap) value;
+ if (bmp.getHeight() > maxBitmapSize || bmp.getWidth() > maxBitmapSize) {
+ putBitmap(key, scaleBitmap(bmp, maxBitmapSize));
+ }
+ }
+ }
+ }
+
+ /**
+ * Put a CharSequence value into the metadata. Custom keys may be used,
+ * but if the METADATA_KEYs defined in this class are used they may only
+ * be one of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ALBUM}</li>
+ * <li>{@link #METADATA_KEY_AUTHOR}</li>
+ * <li>{@link #METADATA_KEY_WRITER}</li>
+ * <li>{@link #METADATA_KEY_COMPOSER}</li>
+ * <li>{@link #METADATA_KEY_DATE}</li>
+ * <li>{@link #METADATA_KEY_GENRE}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ART_URI}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_SUBTITLE}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_DESCRIPTION}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The CharSequence value to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putText(@TextKey String key, CharSequence value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a CharSequence");
+ }
+ }
+ mBundle.putCharSequence(key, value);
+ return this;
+ }
+
+ /**
+ * Put a String value into the metadata. Custom keys may be used, but if
+ * the METADATA_KEYs defined in this class are used they may only be one
+ * of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ALBUM}</li>
+ * <li>{@link #METADATA_KEY_AUTHOR}</li>
+ * <li>{@link #METADATA_KEY_WRITER}</li>
+ * <li>{@link #METADATA_KEY_COMPOSER}</li>
+ * <li>{@link #METADATA_KEY_DATE}</li>
+ * <li>{@link #METADATA_KEY_GENRE}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ARTIST}</li>
+ * <li>{@link #METADATA_KEY_ART_URI}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ART_URI}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_TITLE}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_SUBTITLE}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_DESCRIPTION}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_ICON_URI}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The String value to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putString(@TextKey String key, String value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_TEXT) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a String");
+ }
+ }
+ mBundle.putCharSequence(key, value);
+ return this;
+ }
+
+ /**
+ * Put a long value into the metadata. Custom keys may be used, but if
+ * the METADATA_KEYs defined in this class are used they may only be one
+ * of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_DURATION}</li>
+ * <li>{@link #METADATA_KEY_TRACK_NUMBER}</li>
+ * <li>{@link #METADATA_KEY_NUM_TRACKS}</li>
+ * <li>{@link #METADATA_KEY_DISC_NUMBER}</li>
+ * <li>{@link #METADATA_KEY_YEAR}</li>
+ * <li>{@link #METADATA_KEY_BT_FOLDER_TYPE}</li>
+ * <li>{@link #METADATA_KEY_ADVERTISEMENT}</li>
+ * <li>{@link #METADATA_KEY_DOWNLOAD_STATUS}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The String value to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putLong(@LongKey String key, long value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_LONG) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a long");
+ }
+ }
+ mBundle.putLong(key, value);
+ return this;
+ }
+
+ /**
+ * Put a {@link Rating2} into the metadata. Custom keys may be used, but
+ * if the METADATA_KEYs defined in this class are used they may only be
+ * one of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_RATING}</li>
+ * <li>{@link #METADATA_KEY_USER_RATING}</li>
+ * </ul>
+ *
+ * @param key The key for referencing this value
+ * @param value The String value to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putRating(@RatingKey String key, Rating2 value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_RATING) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a Rating");
+ }
+ }
+ mBundle.putBundle(key, value.toBundle());
+
+ return this;
+ }
+
+ /**
+ * Put a {@link Bitmap} into the metadata. Custom keys may be used, but
+ * if the METADATA_KEYs defined in this class are used they may only be
+ * one of the following:
+ * <ul>
+ * <li>{@link #METADATA_KEY_ART}</li>
+ * <li>{@link #METADATA_KEY_ALBUM_ART}</li>
+ * <li>{@link #METADATA_KEY_DISPLAY_ICON}</li>
+ * </ul>
+ * Large bitmaps may be scaled down by the system when
+ * {@link android.media.session.MediaSession#setMetadata} is called.
+ * To pass full resolution images {@link Uri Uris} should be used with
+ * {@link #putString}.
+ *
+ * @param key The key for referencing this value
+ * @param value The Bitmap to store
+ * @return The Builder to allow chaining
+ */
+ public Builder putBitmap(@BitmapKey String key, Bitmap value) {
+ if (METADATA_KEYS_TYPE.containsKey(key)) {
+ if (METADATA_KEYS_TYPE.get(key) != METADATA_TYPE_BITMAP) {
+ throw new IllegalArgumentException("The " + key
+ + " key cannot be used to put a Bitmap");
+ }
+ }
+ mBundle.putParcelable(key, value);
+ return this;
+ }
+
+ /**
+ * Set an extra {@link Bundle} into the metadata.
+ */
+ public Builder setExtra(Bundle bundle) {
+ mBundle.putBundle(METADATA_KEY_EXTRA, bundle);
+ return this;
+ }
+
+ /**
+ * Creates a {@link MediaMetadata2} instance with the specified fields.
+ *
+ * @return The new MediaMetadata2 instance
+ */
+ public MediaMetadata2 build() {
+ return new MediaMetadata2(mBundle);
+ }
+
+ private Bitmap scaleBitmap(Bitmap bmp, int maxSize) {
+ float maxSizeF = maxSize;
+ float widthScale = maxSizeF / bmp.getWidth();
+ float heightScale = maxSizeF / bmp.getHeight();
+ float scale = Math.min(widthScale, heightScale);
+ int height = (int) (bmp.getHeight() * scale);
+ int width = (int) (bmp.getWidth() * scale);
+ return Bitmap.createScaledBitmap(bmp, width, height, true);
+ }
+ }
+}
+
diff --git a/android/media/MediaPlayer2.java b/android/media/MediaPlayer2.java
new file mode 100644
index 00000000..d36df845
--- /dev/null
+++ b/android/media/MediaPlayer2.java
@@ -0,0 +1,2476 @@
+/*
+ * 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.media;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.PersistableBundle;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.media.MediaDrm;
+import android.media.MediaFormat;
+import android.media.MediaPlayer2Impl;
+import android.media.MediaTimeProvider;
+import android.media.PlaybackParams;
+import android.media.SubtitleController;
+import android.media.SubtitleController.Anchor;
+import android.media.SubtitleData;
+import android.media.SubtitleTrack.RenderingWidget;
+import android.media.SyncParams;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.AutoCloseable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetSocketAddress;
+import java.util.concurrent.Executor;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+
+
+/**
+ * MediaPlayer2 class can be used to control playback
+ * of audio/video files and streams. An example on how to use the methods in
+ * this class can be found in {@link android.widget.VideoView}.
+ *
+ * <p>Topics covered here are:
+ * <ol>
+ * <li><a href="#StateDiagram">State Diagram</a>
+ * <li><a href="#Valid_and_Invalid_States">Valid and Invalid States</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * <li><a href="#Callbacks">Register informational and error callbacks</a>
+ * </ol>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about how to use MediaPlayer2, read the
+ * <a href="{@docRoot}guide/topics/media/mediaplayer.html">Media Playback</a> developer guide.</p>
+ * </div>
+ *
+ * <a name="StateDiagram"></a>
+ * <h3>State Diagram</h3>
+ *
+ * <p>Playback control of audio/video files and streams is managed as a state
+ * machine. The following diagram shows the life cycle and the states of a
+ * MediaPlayer2 object driven by the supported playback control operations.
+ * The ovals represent the states a MediaPlayer2 object may reside
+ * in. The arcs represent the playback control operations that drive the object
+ * state transition. There are two types of arcs. The arcs with a single arrow
+ * head represent synchronous method calls, while those with
+ * a double arrow head represent asynchronous method calls.</p>
+ *
+ * <p><img src="../../../images/mediaplayer_state_diagram.gif"
+ * alt="MediaPlayer State diagram"
+ * border="0" /></p>
+ *
+ * <p>From this state diagram, one can see that a MediaPlayer2 object has the
+ * following states:</p>
+ * <ul>
+ * <li>When a MediaPlayer2 object is just created using <code>new</code> or
+ * after {@link #reset()} is called, it is in the <em>Idle</em> state; and after
+ * {@link #close()} is called, it is in the <em>End</em> state. Between these
+ * two states is the life cycle of the MediaPlayer2 object.
+ * <ul>
+ * <li>There is a subtle but important difference between a newly constructed
+ * MediaPlayer2 object and the MediaPlayer2 object after {@link #reset()}
+ * is called. It is a programming error to invoke methods such
+ * as {@link #getCurrentPosition()},
+ * {@link #getDuration()}, {@link #getVideoHeight()},
+ * {@link #getVideoWidth()}, {@link #setAudioAttributes(AudioAttributes)},
+ * {@link #setVolume(float, float)}, {@link #pause()}, {@link #play()},
+ * {@link #seekTo(long, int)} or
+ * {@link #prepareAsync()} in the <em>Idle</em> state for both cases. If any of these
+ * methods is called right after a MediaPlayer2 object is constructed,
+ * the user supplied callback method OnErrorListener.onError() won't be
+ * called by the internal player engine and the object state remains
+ * unchanged; but if these methods are called right after {@link #reset()},
+ * the user supplied callback method OnErrorListener.onError() will be
+ * invoked by the internal player engine and the object will be
+ * transfered to the <em>Error</em> state. </li>
+ * <li>It is also recommended that once
+ * a MediaPlayer2 object is no longer being used, call {@link #close()} immediately
+ * so that resources used by the internal player engine associated with the
+ * MediaPlayer2 object can be released immediately. Resource may include
+ * singleton resources such as hardware acceleration components and
+ * failure to call {@link #close()} may cause subsequent instances of
+ * MediaPlayer2 objects to fallback to software implementations or fail
+ * altogether. Once the MediaPlayer2
+ * object is in the <em>End</em> state, it can no longer be used and
+ * there is no way to bring it back to any other state. </li>
+ * <li>Furthermore,
+ * the MediaPlayer2 objects created using <code>new</code> is in the
+ * <em>Idle</em> state.
+ * </li>
+ * </ul>
+ * </li>
+ * <li>In general, some playback control operation may fail due to various
+ * reasons, such as unsupported audio/video format, poorly interleaved
+ * audio/video, resolution too high, streaming timeout, and the like.
+ * Thus, error reporting and recovery is an important concern under
+ * these circumstances. Sometimes, due to programming errors, invoking a playback
+ * control operation in an invalid state may also occur. Under all these
+ * error conditions, the internal player engine invokes a user supplied
+ * EventCallback.onError() method if an EventCallback has been
+ * registered beforehand via
+ * {@link #registerEventCallback(Executor, EventCallback)}.
+ * <ul>
+ * <li>It is important to note that once an error occurs, the
+ * MediaPlayer2 object enters the <em>Error</em> state (except as noted
+ * above), even if an error listener has not been registered by the application.</li>
+ * <li>In order to reuse a MediaPlayer2 object that is in the <em>
+ * Error</em> state and recover from the error,
+ * {@link #reset()} can be called to restore the object to its <em>Idle</em>
+ * state.</li>
+ * <li>It is good programming practice to have your application
+ * register a OnErrorListener to look out for error notifications from
+ * the internal player engine.</li>
+ * <li>IllegalStateException is
+ * thrown to prevent programming errors such as calling
+ * {@link #prepareAsync()}, {@link #setDataSource(DataSourceDesc)}, or
+ * {@code setPlaylist} methods in an invalid state. </li>
+ * </ul>
+ * </li>
+ * <li>Calling
+ * {@link #setDataSource(DataSourceDesc)}, or
+ * {@code setPlaylist} transfers a
+ * MediaPlayer2 object in the <em>Idle</em> state to the
+ * <em>Initialized</em> state.
+ * <ul>
+ * <li>An IllegalStateException is thrown if
+ * setDataSource() or setPlaylist() is called in any other state.</li>
+ * <li>It is good programming
+ * practice to always look out for <code>IllegalArgumentException</code>
+ * and <code>IOException</code> that may be thrown from
+ * <code>setDataSource</code> and <code>setPlaylist</code> methods.</li>
+ * </ul>
+ * </li>
+ * <li>A MediaPlayer2 object must first enter the <em>Prepared</em> state
+ * before playback can be started.
+ * <ul>
+ * <li>There are an asynchronous way that the <em>Prepared</em> state can be reached:
+ * a call to {@link #prepareAsync()} (asynchronous) which
+ * first transfers the object to the <em>Preparing</em> state after the
+ * call returns (which occurs almost right way) while the internal
+ * player engine continues working on the rest of preparation work
+ * until the preparation work completes. When the preparation completes,
+ * the internal player engine then calls a user supplied callback method,
+ * onInfo() of the EventCallback interface with {@link #MEDIA_INFO_PREPARED}, if an
+ * EventCallback is registered beforehand via
+ * {@link #registerEventCallback(Executor, EventCallback)}.</li>
+ * <li>It is important to note that
+ * the <em>Preparing</em> state is a transient state, and the behavior
+ * of calling any method with side effect while a MediaPlayer2 object is
+ * in the <em>Preparing</em> state is undefined.</li>
+ * <li>An IllegalStateException is
+ * thrown if {@link #prepareAsync()} is called in
+ * any other state.</li>
+ * <li>While in the <em>Prepared</em> state, properties
+ * such as audio/sound volume, screenOnWhilePlaying, looping can be
+ * adjusted by invoking the corresponding set methods.</li>
+ * </ul>
+ * </li>
+ * <li>To start the playback, {@link #play()} must be called. After
+ * {@link #play()} returns successfully, the MediaPlayer2 object is in the
+ * <em>Started</em> state. {@link #isPlaying()} can be called to test
+ * whether the MediaPlayer2 object is in the <em>Started</em> state.
+ * <ul>
+ * <li>While in the <em>Started</em> state, the internal player engine calls
+ * a user supplied EventCallback.onBufferingUpdate() callback
+ * method if an EventCallback has been registered beforehand
+ * via {@link #registerEventCallback(Executor, EventCallback)}.
+ * This callback allows applications to keep track of the buffering status
+ * while streaming audio/video.</li>
+ * <li>Calling {@link #play()} has not effect
+ * on a MediaPlayer2 object that is already in the <em>Started</em> state.</li>
+ * </ul>
+ * </li>
+ * <li>Playback can be paused and stopped, and the current playback position
+ * can be adjusted. Playback can be paused via {@link #pause()}. When the call to
+ * {@link #pause()} returns, the MediaPlayer2 object enters the
+ * <em>Paused</em> state. Note that the transition from the <em>Started</em>
+ * state to the <em>Paused</em> state and vice versa happens
+ * asynchronously in the player engine. It may take some time before
+ * the state is updated in calls to {@link #isPlaying()}, and it can be
+ * a number of seconds in the case of streamed content.
+ * <ul>
+ * <li>Calling {@link #play()} to resume playback for a paused
+ * MediaPlayer2 object, and the resumed playback
+ * position is the same as where it was paused. When the call to
+ * {@link #play()} returns, the paused MediaPlayer2 object goes back to
+ * the <em>Started</em> state.</li>
+ * <li>Calling {@link #pause()} has no effect on
+ * a MediaPlayer2 object that is already in the <em>Paused</em> state.</li>
+ * </ul>
+ * </li>
+ * <li>The playback position can be adjusted with a call to
+ * {@link #seekTo(long, int)}.
+ * <ul>
+ * <li>Although the asynchronuous {@link #seekTo(long, int)}
+ * call returns right away, the actual seek operation may take a while to
+ * finish, especially for audio/video being streamed. When the actual
+ * seek operation completes, the internal player engine calls a user
+ * supplied EventCallback.onInfo() with {@link #MEDIA_INFO_COMPLETE_CALL_SEEK}
+ * if an EventCallback has been registered beforehand via
+ * {@link #registerEventCallback(Executor, EventCallback)}.</li>
+ * <li>Please
+ * note that {@link #seekTo(long, int)} can also be called in the other states,
+ * such as <em>Prepared</em>, <em>Paused</em> and <em>PlaybackCompleted
+ * </em> state. When {@link #seekTo(long, int)} is called in those states,
+ * one video frame will be displayed if the stream has video and the requested
+ * position is valid.
+ * </li>
+ * <li>Furthermore, the actual current playback position
+ * can be retrieved with a call to {@link #getCurrentPosition()}, which
+ * is helpful for applications such as a Music player that need to keep
+ * track of the playback progress.</li>
+ * </ul>
+ * </li>
+ * <li>When the playback reaches the end of stream, the playback completes.
+ * <ul>
+ * <li>If the looping mode was being set to one of the values of
+ * {@link #LOOPING_MODE_FULL}, {@link #LOOPING_MODE_SINGLE} or
+ * {@link #LOOPING_MODE_SHUFFLE} with
+ * {@link #setLoopingMode(int)}, the MediaPlayer2 object shall remain in
+ * the <em>Started</em> state.</li>
+ * <li>If the looping mode was set to <var>false
+ * </var>, the player engine calls a user supplied callback method,
+ * EventCallback.onCompletion(), if an EventCallback is registered
+ * beforehand via {@link #registerEventCallback(Executor, EventCallback)}.
+ * The invoke of the callback signals that the object is now in the <em>
+ * PlaybackCompleted</em> state.</li>
+ * <li>While in the <em>PlaybackCompleted</em>
+ * state, calling {@link #play()} can restart the playback from the
+ * beginning of the audio/video source.</li>
+ * </ul>
+ *
+ *
+ * <a name="Valid_and_Invalid_States"></a>
+ * <h3>Valid and invalid states</h3>
+ *
+ * <table border="0" cellspacing="0" cellpadding="0">
+ * <tr><td>Method Name </p></td>
+ * <td>Valid Sates </p></td>
+ * <td>Invalid States </p></td>
+ * <td>Comments </p></td></tr>
+ * <tr><td>attachAuxEffect </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ * <td>{Idle, Error} </p></td>
+ * <td>This method must be called after setDataSource or setPlaylist.
+ * Calling it does not change the object state. </p></td></tr>
+ * <tr><td>getAudioSessionId </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>getCurrentPosition </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted} </p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getDuration </p></td>
+ * <td>{Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ * <td>{Idle, Initialized, Error} </p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getVideoHeight </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getVideoWidth </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>isPlaying </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>pause </p></td>
+ * <td>{Started, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Prepared, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Paused</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>prepareAsync </p></td>
+ * <td>{Initialized, Stopped} </p></td>
+ * <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Preparing</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>release </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>After {@link #close()}, the object is no longer available. </p></td></tr>
+ * <tr><td>reset </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted, Error}</p></td>
+ * <td>{}</p></td>
+ * <td>After {@link #reset()}, the object is like being just created.</p></td></tr>
+ * <tr><td>seekTo </p></td>
+ * <td>{Prepared, Started, Paused, PlaybackCompleted} </p></td>
+ * <td>{Idle, Initialized, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>setAudioAttributes </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state. In order for the
+ * target audio attributes type to become effective, this method must be called before
+ * prepareAsync().</p></td></tr>
+ * <tr><td>setAudioSessionId </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>This method must be called in idle state as the audio session ID must be known before
+ * calling setDataSource or setPlaylist. Calling it does not change the object
+ * state. </p></td></tr>
+ * <tr><td>setAudioStreamType (deprecated)</p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state. In order for the
+ * target audio stream type to become effective, this method must be called before
+ * prepareAsync().</p></td></tr>
+ * <tr><td>setAuxEffectSendLevel </p></td>
+ * <td>any</p></td>
+ * <td>{} </p></td>
+ * <td>Calling this method does not change the object state. </p></td></tr>
+ * <tr><td>setDataSource </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Initialized</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>setPlaylist </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Initialized</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>setDisplay </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setSurface </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setLoopingMode </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>isLooping </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>registerDrmEventCallback </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>registerEventCallback </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setPlaybackParams</p></td>
+ * <td>{Initialized, Prepared, Started, Paused, PlaybackCompleted, Error}</p></td>
+ * <td>{Idle, Stopped} </p></td>
+ * <td>This method will change state in some cases, depending on when it's called.
+ * </p></td></tr>
+ * <tr><td>setVolume </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.
+ * <tr><td>play </p></td>
+ * <td>{Prepared, Started, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Started</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>stop </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Stopped</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>getTrackInfo </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>selectTrack </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>deselectTrack </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ *
+ * </table>
+ *
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ * <p>One may need to declare a corresponding WAKE_LOCK permission {@link
+ * android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
+ * element.
+ *
+ * <p>This class requires the {@link android.Manifest.permission#INTERNET} permission
+ * when used with network-based content.
+ *
+ * <a name="Callbacks"></a>
+ * <h3>Callbacks</h3>
+ * <p>Applications may want to register for informational and error
+ * events in order to be informed of some internal state update and
+ * possible runtime errors during playback or streaming. Registration for
+ * these events is done by properly setting the appropriate listeners (via calls
+ * to
+ * {@link #registerEventCallback(Executor, EventCallback)},
+ * {@link #registerDrmEventCallback(Executor, DrmEventCallback)}).
+ * In order to receive the respective callback
+ * associated with these listeners, applications are required to create
+ * MediaPlayer2 objects on a thread with its own Looper running (main UI
+ * thread by default has a Looper running).
+ *
+ */
+public abstract class MediaPlayer2 implements SubtitleController.Listener
+ , AudioRouting
+ , AutoCloseable
+{
+ /**
+ Constant to retrieve only the new metadata since the last
+ call.
+ // FIXME: unhide.
+ // FIXME: add link to getMetadata(boolean, boolean)
+ {@hide}
+ */
+ public static final boolean METADATA_UPDATE_ONLY = true;
+
+ /**
+ Constant to retrieve all the metadata.
+ // FIXME: unhide.
+ // FIXME: add link to getMetadata(boolean, boolean)
+ {@hide}
+ */
+ public static final boolean METADATA_ALL = false;
+
+ /**
+ Constant to enable the metadata filter during retrieval.
+ // FIXME: unhide.
+ // FIXME: add link to getMetadata(boolean, boolean)
+ {@hide}
+ */
+ public static final boolean APPLY_METADATA_FILTER = true;
+
+ /**
+ Constant to disable the metadata filter during retrieval.
+ // FIXME: unhide.
+ // FIXME: add link to getMetadata(boolean, boolean)
+ {@hide}
+ */
+ public static final boolean BYPASS_METADATA_FILTER = false;
+
+ /**
+ * Create a MediaPlayer2 object.
+ *
+ * @return A MediaPlayer2 object created
+ */
+ public static final MediaPlayer2 create() {
+ // TODO: load MediaUpdate APK
+ return new MediaPlayer2Impl();
+ }
+
+ /**
+ * @hide
+ */
+ // add hidden empty constructor so it doesn't show in SDK
+ public MediaPlayer2() { }
+
+ /**
+ * Create a request parcel which can be routed to the native media
+ * player using {@link #invoke(Parcel, Parcel)}. The Parcel
+ * returned has the proper InterfaceToken set. The caller should
+ * not overwrite that token, i.e it can only append data to the
+ * Parcel.
+ *
+ * @return A parcel suitable to hold a request for the native
+ * player.
+ * {@hide}
+ */
+ public Parcel newRequest() {
+ return null;
+ }
+
+ /**
+ * Invoke a generic method on the native player using opaque
+ * parcels for the request and reply. Both payloads' format is a
+ * convention between the java caller and the native player.
+ * Must be called after setDataSource or setPlaylist to make sure a native player
+ * exists. On failure, a RuntimeException is thrown.
+ *
+ * @param request Parcel with the data for the extension. The
+ * caller must use {@link #newRequest()} to get one.
+ *
+ * @param reply Output parcel with the data returned by the
+ * native player.
+ * {@hide}
+ */
+ public void invoke(Parcel request, Parcel reply) { }
+
+ /**
+ * Sets the {@link SurfaceHolder} to use for displaying the video
+ * portion of the media.
+ *
+ * Either a surface holder or surface must be set if a display or video sink
+ * is needed. Not calling this method or {@link #setSurface(Surface)}
+ * when playing back a video will result in only the audio track being played.
+ * A null surface holder or surface will result in only the audio track being
+ * played.
+ *
+ * @param sh the SurfaceHolder to use for video display
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ * @hide
+ */
+ public abstract void setDisplay(SurfaceHolder sh);
+
+ /**
+ * Sets the {@link Surface} to be used as the sink for the video portion of
+ * the media. Setting a
+ * Surface will un-set any Surface or SurfaceHolder that was previously set.
+ * A null surface will result in only the audio track being played.
+ *
+ * If the Surface sends frames to a {@link SurfaceTexture}, the timestamps
+ * returned from {@link SurfaceTexture#getTimestamp()} will have an
+ * unspecified zero point. These timestamps cannot be directly compared
+ * between different media sources, different instances of the same media
+ * source, or multiple runs of the same program. The timestamp is normally
+ * monotonically increasing and is unaffected by time-of-day adjustments,
+ * but it is reset when the position is set.
+ *
+ * @param surface The {@link Surface} to be used for the video portion of
+ * the media.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ */
+ public abstract void setSurface(Surface surface);
+
+ /* Do not change these video scaling mode values below without updating
+ * their counterparts in system/window.h! Please do not forget to update
+ * {@link #isVideoScalingModeSupported} when new video scaling modes
+ * are added.
+ */
+ /**
+ * Specifies a video scaling mode. The content is stretched to the
+ * surface rendering area. When the surface has the same aspect ratio
+ * as the content, the aspect ratio of the content is maintained;
+ * otherwise, the aspect ratio of the content is not maintained when video
+ * is being rendered.
+ * There is no content cropping with this video scaling mode.
+ */
+ public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT = 1;
+
+ /**
+ * Specifies a video scaling mode. The content is scaled, maintaining
+ * its aspect ratio. The whole surface area is always used. When the
+ * aspect ratio of the content is the same as the surface, no content
+ * is cropped; otherwise, content is cropped to fit the surface.
+ * @hide
+ */
+ public static final int VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING = 2;
+
+ /**
+ * Sets video scaling mode. To make the target video scaling mode
+ * effective during playback, this method must be called after
+ * data source is set. If not called, the default video
+ * scaling mode is {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT}.
+ *
+ * <p> The supported video scaling modes are:
+ * <ul>
+ * <li> {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT}
+ * </ul>
+ *
+ * @param mode target video scaling mode. Must be one of the supported
+ * video scaling modes; otherwise, IllegalArgumentException will be thrown.
+ *
+ * @see MediaPlayer2#VIDEO_SCALING_MODE_SCALE_TO_FIT
+ * @hide
+ */
+ public void setVideoScalingMode(int mode) { }
+
+ /**
+ * Discards all pending commands.
+ */
+ public abstract void clearPendingCommands();
+
+ /**
+ * Sets the data source as described by a DataSourceDesc.
+ *
+ * @param dsd the descriptor of data source you want to play
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws NullPointerException if dsd is null
+ */
+ public abstract void setDataSource(@NonNull DataSourceDesc dsd) throws IOException;
+
+ /**
+ * Gets the current data source as described by a DataSourceDesc.
+ *
+ * @return the current DataSourceDesc
+ */
+ public abstract DataSourceDesc getCurrentDataSource();
+
+ /**
+ * Sets the play list.
+ *
+ * If startIndex falls outside play list range, it will be clamped to the nearest index
+ * in the play list.
+ *
+ * @param pl the play list of data source you want to play
+ * @param startIndex the index of the DataSourceDesc in the play list you want to play first
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if pl is null or empty, or pl contains null DataSourceDesc
+ */
+ public abstract void setPlaylist(@NonNull List<DataSourceDesc> pl, int startIndex)
+ throws IOException;
+
+ /**
+ * Gets a copy of the play list.
+ *
+ * @return a copy of the play list used by {@link MediaPlayer2}
+ */
+ public abstract List<DataSourceDesc> getPlaylist();
+
+ /**
+ * Sets the index of current DataSourceDesc in the play list to be played.
+ *
+ * @param index the index of DataSourceDesc in the play list you want to play
+ * @throws IllegalArgumentException if the play list is null
+ * @throws NullPointerException if index is outside play list range
+ */
+ public abstract void setCurrentPlaylistItem(int index);
+
+ /**
+ * Sets the index of next-to-be-played DataSourceDesc in the play list.
+ *
+ * @param index the index of next-to-be-played DataSourceDesc in the play list
+ * @throws IllegalArgumentException if the play list is null
+ * @throws NullPointerException if index is outside play list range
+ */
+ public abstract void setNextPlaylistItem(int index);
+
+ /**
+ * Gets the current index of play list.
+ *
+ * @return the index of the current DataSourceDesc in the play list
+ */
+ public abstract int getCurrentPlaylistItemIndex();
+
+ /**
+ * Specifies a playback looping mode. The source will not be played in looping mode.
+ */
+ public static final int LOOPING_MODE_NONE = 0;
+ /**
+ * Specifies a playback looping mode. The full list of source will be played in looping mode,
+ * and in the order specified in the play list.
+ */
+ public static final int LOOPING_MODE_FULL = 1;
+ /**
+ * Specifies a playback looping mode. The current DataSourceDesc will be played in looping mode.
+ */
+ public static final int LOOPING_MODE_SINGLE = 2;
+ /**
+ * Specifies a playback looping mode. The full list of source will be played in looping mode,
+ * and in a random order.
+ */
+ public static final int LOOPING_MODE_SHUFFLE = 3;
+
+ /** @hide */
+ @IntDef(
+ value = {
+ LOOPING_MODE_NONE,
+ LOOPING_MODE_FULL,
+ LOOPING_MODE_SINGLE,
+ LOOPING_MODE_SHUFFLE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface LoopingMode {}
+
+ /**
+ * Sets the looping mode of the play list.
+ * The mode shall be one of {@link #LOOPING_MODE_NONE}, {@link #LOOPING_MODE_FULL},
+ * {@link #LOOPING_MODE_SINGLE}, {@link #LOOPING_MODE_SHUFFLE}.
+ *
+ * @param mode the mode in which the play list will be played
+ * @throws IllegalArgumentException if mode is not supported
+ */
+ public abstract void setLoopingMode(@LoopingMode int mode);
+
+ /**
+ * Gets the looping mode of play list.
+ *
+ * @return the looping mode of the play list
+ */
+ public abstract int getLoopingMode();
+
+ /**
+ * Moves the DataSourceDesc at indexFrom in the play list to indexTo.
+ *
+ * @throws IllegalArgumentException if the play list is null
+ * @throws IndexOutOfBoundsException if indexFrom or indexTo is outside play list range
+ */
+ public abstract void movePlaylistItem(int indexFrom, int indexTo);
+
+ /**
+ * Removes the DataSourceDesc at index in the play list.
+ *
+ * If index is same as the current index of the play list, current DataSourceDesc
+ * will be stopped and playback moves to next source in the list.
+ *
+ * @return the removed DataSourceDesc at index in the play list
+ * @throws IllegalArgumentException if the play list is null
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ */
+ public abstract DataSourceDesc removePlaylistItem(int index);
+
+ /**
+ * Inserts the DataSourceDesc to the play list at position index.
+ *
+ * This will not change the DataSourceDesc currently being played.
+ * If index is less than or equal to the current index of the play list,
+ * the current index of the play list will be incremented correspondingly.
+ *
+ * @param index the index you want to add dsd to the play list
+ * @param dsd the descriptor of data source you want to add to the play list
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ * @throws NullPointerException if dsd is null
+ */
+ public abstract void addPlaylistItem(int index, DataSourceDesc dsd);
+
+ /**
+ * replaces the DataSourceDesc at index in the play list with given dsd.
+ *
+ * When index is same as the current index of the play list, the current source
+ * will be stopped and the new source will be played, except that if new
+ * and old source only differ on end position and current media position is
+ * smaller then the new end position.
+ *
+ * This will not change the DataSourceDesc currently being played.
+ * If index is less than or equal to the current index of the play list,
+ * the current index of the play list will be incremented correspondingly.
+ *
+ * @param index the index you want to add dsd to the play list
+ * @param dsd the descriptor of data source you want to add to the play list
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ * @throws NullPointerException if dsd is null
+ */
+ public abstract DataSourceDesc editPlaylistItem(int index, DataSourceDesc dsd);
+
+ /**
+ * Prepares the player for playback, synchronously.
+ *
+ * After setting the datasource and the display surface, you need to either
+ * call prepare() or prepareAsync(). For files, it is OK to call prepare(),
+ * which blocks until MediaPlayer2 is ready for playback.
+ *
+ * @throws IOException if source can not be accessed
+ * @throws IllegalStateException if it is called in an invalid state
+ * @hide
+ */
+ public void prepare() throws IOException { }
+
+ /**
+ * Prepares the player for playback, asynchronously.
+ *
+ * After setting the datasource and the display surface, you need to
+ * call prepareAsync().
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ public abstract void prepareAsync();
+
+ /**
+ * Starts or resumes playback. If playback had previously been paused,
+ * playback will continue from where it was paused. If playback had
+ * been stopped, or never started before, playback will start at the
+ * beginning.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ public abstract void play();
+
+ /**
+ * Stops playback after playback has been started or paused.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * @hide
+ */
+ public void stop() { }
+
+ /**
+ * Pauses playback. Call play() to resume.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ public abstract void pause();
+
+ //--------------------------------------------------------------------------
+ // Explicit Routing
+ //--------------------
+
+ /**
+ * Specifies an audio device (via an {@link AudioDeviceInfo} object) to route
+ * the output from this MediaPlayer2.
+ * @param deviceInfo The {@link AudioDeviceInfo} specifying the audio sink or source.
+ * If deviceInfo is null, default routing is restored.
+ * @return true if succesful, false if the specified {@link AudioDeviceInfo} is non-null and
+ * does not correspond to a valid audio device.
+ */
+ @Override
+ public abstract boolean setPreferredDevice(AudioDeviceInfo deviceInfo);
+
+ /**
+ * Returns the selected output specified by {@link #setPreferredDevice}. Note that this
+ * is not guaranteed to correspond to the actual device being used for playback.
+ */
+ @Override
+ public abstract AudioDeviceInfo getPreferredDevice();
+
+ /**
+ * Returns an {@link AudioDeviceInfo} identifying the current routing of this MediaPlayer2
+ * Note: The query is only valid if the MediaPlayer2 is currently playing.
+ * If the player is not playing, the returned device can be null or correspond to previously
+ * selected device when the player was last active.
+ */
+ @Override
+ public abstract AudioDeviceInfo getRoutedDevice();
+
+ /**
+ * Adds an {@link AudioRouting.OnRoutingChangedListener} to receive notifications of routing
+ * changes on this MediaPlayer2.
+ * @param listener The {@link AudioRouting.OnRoutingChangedListener} interface to receive
+ * notifications of rerouting events.
+ * @param handler Specifies the {@link Handler} object for the thread on which to execute
+ * the callback. If <code>null</code>, the handler on the main looper will be used.
+ */
+ @Override
+ public abstract void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener,
+ Handler handler);
+
+ /**
+ * Removes an {@link AudioRouting.OnRoutingChangedListener} which has been previously added
+ * to receive rerouting notifications.
+ * @param listener The previously added {@link AudioRouting.OnRoutingChangedListener} interface
+ * to remove.
+ */
+ @Override
+ public abstract void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener);
+
+ /**
+ * Set the low-level power management behavior for this MediaPlayer2.
+ *
+ * <p>This function has the MediaPlayer2 access the low-level power manager
+ * service to control the device's power usage while playing is occurring.
+ * The parameter is a combination of {@link android.os.PowerManager} wake flags.
+ * Use of this method requires {@link android.Manifest.permission#WAKE_LOCK}
+ * permission.
+ * By default, no attempt is made to keep the device awake during playback.
+ *
+ * @param context the Context to use
+ * @param mode the power/wake mode to set
+ * @see android.os.PowerManager
+ * @hide
+ */
+ public abstract void setWakeMode(Context context, int mode);
+
+ /**
+ * Control whether we should use the attached SurfaceHolder to keep the
+ * screen on while video playback is occurring. This is the preferred
+ * method over {@link #setWakeMode} where possible, since it doesn't
+ * require that the application have permission for low-level wake lock
+ * access.
+ *
+ * @param screenOn Supply true to keep the screen on, false to allow it
+ * to turn off.
+ * @hide
+ */
+ public abstract void setScreenOnWhilePlaying(boolean screenOn);
+
+ /**
+ * Returns the width of the video.
+ *
+ * @return the width of the video, or 0 if there is no video,
+ * no display surface was set, or the width has not been determined
+ * yet. The {@code EventCallback} can be registered via
+ * {@link #registerEventCallback(Executor, EventCallback)} to provide a
+ * notification {@code EventCallback.onVideoSizeChanged} when the width is available.
+ */
+ public abstract int getVideoWidth();
+
+ /**
+ * Returns the height of the video.
+ *
+ * @return the height of the video, or 0 if there is no video,
+ * no display surface was set, or the height has not been determined
+ * yet. The {@code EventCallback} can be registered via
+ * {@link #registerEventCallback(Executor, EventCallback)} to provide a
+ * notification {@code EventCallback.onVideoSizeChanged} when the height is available.
+ */
+ public abstract int getVideoHeight();
+
+ /**
+ * Return Metrics data about the current player.
+ *
+ * @return a {@link PersistableBundle} containing the set of attributes and values
+ * available for the media being handled by this instance of MediaPlayer2
+ * The attributes are descibed in {@link MetricsConstants}.
+ *
+ * Additional vendor-specific fields may also be present in
+ * the return value.
+ */
+ public abstract PersistableBundle getMetrics();
+
+ /**
+ * Checks whether the MediaPlayer2 is playing.
+ *
+ * @return true if currently playing, false otherwise
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ */
+ public abstract boolean isPlaying();
+
+ /**
+ * Gets the current buffering management params used by the source component.
+ * Calling it only after {@code setDataSource} has been called.
+ * Each type of data source might have different set of default params.
+ *
+ * @return the current buffering management params used by the source component.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized, or {@code setDataSource} has not been called.
+ * @hide
+ */
+ @NonNull
+ public BufferingParams getBufferingParams() {
+ return new BufferingParams.Builder().build();
+ }
+
+ /**
+ * Sets buffering management params.
+ * The object sets its internal BufferingParams to the input, except that the input is
+ * invalid or not supported.
+ * Call it only after {@code setDataSource} has been called.
+ * The input is a hint to MediaPlayer2.
+ *
+ * @param params the buffering management params.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released, or {@code setDataSource} has not been called.
+ * @throws IllegalArgumentException if params is invalid or not supported.
+ * @hide
+ */
+ public void setBufferingParams(@NonNull BufferingParams params) { }
+
+ /**
+ * Change playback speed of audio by resampling the audio.
+ * <p>
+ * Specifies resampling as audio mode for variable rate playback, i.e.,
+ * resample the waveform based on the requested playback rate to get
+ * a new waveform, and play back the new waveform at the original sampling
+ * frequency.
+ * When rate is larger than 1.0, pitch becomes higher.
+ * When rate is smaller than 1.0, pitch becomes lower.
+ *
+ * @hide
+ */
+ public static final int PLAYBACK_RATE_AUDIO_MODE_RESAMPLE = 2;
+
+ /**
+ * Change playback speed of audio without changing its pitch.
+ * <p>
+ * Specifies time stretching as audio mode for variable rate playback.
+ * Time stretching changes the duration of the audio samples without
+ * affecting its pitch.
+ * <p>
+ * This mode is only supported for a limited range of playback speed factors,
+ * e.g. between 1/2x and 2x.
+ *
+ * @hide
+ */
+ public static final int PLAYBACK_RATE_AUDIO_MODE_STRETCH = 1;
+
+ /**
+ * Change playback speed of audio without changing its pitch, and
+ * possibly mute audio if time stretching is not supported for the playback
+ * speed.
+ * <p>
+ * Try to keep audio pitch when changing the playback rate, but allow the
+ * system to determine how to change audio playback if the rate is out
+ * of range.
+ *
+ * @hide
+ */
+ public static final int PLAYBACK_RATE_AUDIO_MODE_DEFAULT = 0;
+
+ /** @hide */
+ @IntDef(
+ value = {
+ PLAYBACK_RATE_AUDIO_MODE_DEFAULT,
+ PLAYBACK_RATE_AUDIO_MODE_STRETCH,
+ PLAYBACK_RATE_AUDIO_MODE_RESAMPLE,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PlaybackRateAudioMode {}
+
+ /**
+ * Sets playback rate and audio mode.
+ *
+ * @param rate the ratio between desired playback rate and normal one.
+ * @param audioMode audio playback mode. Must be one of the supported
+ * audio modes.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * @throws IllegalArgumentException if audioMode is not supported.
+ *
+ * @hide
+ */
+ @NonNull
+ public PlaybackParams easyPlaybackParams(float rate, @PlaybackRateAudioMode int audioMode) {
+ return new PlaybackParams();
+ }
+
+ /**
+ * Sets playback rate using {@link PlaybackParams}. The object sets its internal
+ * PlaybackParams to the input, except that the object remembers previous speed
+ * when input speed is zero. This allows the object to resume at previous speed
+ * when play() is called. Calling it before the object is prepared does not change
+ * the object state. After the object is prepared, calling it with zero speed is
+ * equivalent to calling pause(). After the object is prepared, calling it with
+ * non-zero speed is equivalent to calling play().
+ *
+ * @param params the playback params.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ * @throws IllegalArgumentException if params is not supported.
+ */
+ public abstract void setPlaybackParams(@NonNull PlaybackParams params);
+
+ /**
+ * Gets the playback params, containing the current playback rate.
+ *
+ * @return the playback params.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @NonNull
+ public abstract PlaybackParams getPlaybackParams();
+
+ /**
+ * Sets A/V sync mode.
+ *
+ * @param params the A/V sync params to apply
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * @throws IllegalArgumentException if params are not supported.
+ */
+ public abstract void setSyncParams(@NonNull SyncParams params);
+
+ /**
+ * Gets the A/V sync mode.
+ *
+ * @return the A/V sync params
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @NonNull
+ public abstract SyncParams getSyncParams();
+
+ /**
+ * Seek modes used in method seekTo(long, int) to move media position
+ * to a specified location.
+ *
+ * Do not change these mode values without updating their counterparts
+ * in include/media/IMediaSource.h!
+ */
+ /**
+ * This mode is used with {@link #seekTo(long, int)} to move media position to
+ * a sync (or key) frame associated with a data source that is located
+ * right before or at the given time.
+ *
+ * @see #seekTo(long, int)
+ */
+ public static final int SEEK_PREVIOUS_SYNC = 0x00;
+ /**
+ * This mode is used with {@link #seekTo(long, int)} to move media position to
+ * a sync (or key) frame associated with a data source that is located
+ * right after or at the given time.
+ *
+ * @see #seekTo(long, int)
+ */
+ public static final int SEEK_NEXT_SYNC = 0x01;
+ /**
+ * This mode is used with {@link #seekTo(long, int)} to move media position to
+ * a sync (or key) frame associated with a data source that is located
+ * closest to (in time) or at the given time.
+ *
+ * @see #seekTo(long, int)
+ */
+ public static final int SEEK_CLOSEST_SYNC = 0x02;
+ /**
+ * This mode is used with {@link #seekTo(long, int)} to move media position to
+ * a frame (not necessarily a key frame) associated with a data source that
+ * is located closest to or at the given time.
+ *
+ * @see #seekTo(long, int)
+ */
+ public static final int SEEK_CLOSEST = 0x03;
+
+ /** @hide */
+ @IntDef(
+ value = {
+ SEEK_PREVIOUS_SYNC,
+ SEEK_NEXT_SYNC,
+ SEEK_CLOSEST_SYNC,
+ SEEK_CLOSEST,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SeekMode {}
+
+ /**
+ * Moves the media to specified time position by considering the given mode.
+ * <p>
+ * When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user.
+ * There is at most one active seekTo processed at any time. If there is a to-be-completed
+ * seekTo, new seekTo requests will be queued in such a way that only the last request
+ * is kept. When current seekTo is completed, the queued request will be processed if
+ * that request is different from just-finished seekTo operation, i.e., the requested
+ * position or mode is different.
+ *
+ * @param msec the offset in milliseconds from the start to seek to.
+ * When seeking to the given time position, there is no guarantee that the data source
+ * has a frame located at the position. When this happens, a frame nearby will be rendered.
+ * If msec is negative, time position zero will be used.
+ * If msec is larger than duration, duration will be used.
+ * @param mode the mode indicating where exactly to seek to.
+ * Use {@link #SEEK_PREVIOUS_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp earlier than or the same as msec. Use
+ * {@link #SEEK_NEXT_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp later than or the same as msec. Use
+ * {@link #SEEK_CLOSEST_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp closest to or the same as msec. Use
+ * {@link #SEEK_CLOSEST} if one wants to seek to a frame that may
+ * or may not be a sync frame but is closest to or the same as msec.
+ * {@link #SEEK_CLOSEST} often has larger performance overhead compared
+ * to the other options if there is no sync frame located at msec.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized
+ * @throws IllegalArgumentException if the mode is invalid.
+ */
+ public abstract void seekTo(long msec, @SeekMode int mode);
+
+ /**
+ * Get current playback position as a {@link MediaTimestamp}.
+ * <p>
+ * The MediaTimestamp represents how the media time correlates to the system time in
+ * a linear fashion using an anchor and a clock rate. During regular playback, the media
+ * time moves fairly constantly (though the anchor frame may be rebased to a current
+ * system time, the linear correlation stays steady). Therefore, this method does not
+ * need to be called often.
+ * <p>
+ * To help users get current playback position, this method always anchors the timestamp
+ * to the current {@link System#nanoTime system time}, so
+ * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position.
+ *
+ * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp
+ * is available, e.g. because the media player has not been initialized.
+ *
+ * @see MediaTimestamp
+ */
+ @Nullable
+ public abstract MediaTimestamp getTimestamp();
+
+ /**
+ * Gets the current playback position.
+ *
+ * @return the current position in milliseconds
+ */
+ public abstract int getCurrentPosition();
+
+ /**
+ * Gets the duration of the file.
+ *
+ * @return the duration in milliseconds, if no duration is available
+ * (for example, if streaming live content), -1 is returned.
+ */
+ public abstract int getDuration();
+
+ /**
+ * Gets the media metadata.
+ *
+ * @param update_only controls whether the full set of available
+ * metadata is returned or just the set that changed since the
+ * last call. See {@see #METADATA_UPDATE_ONLY} and {@see
+ * #METADATA_ALL}.
+ *
+ * @param apply_filter if true only metadata that matches the
+ * filter is returned. See {@see #APPLY_METADATA_FILTER} and {@see
+ * #BYPASS_METADATA_FILTER}.
+ *
+ * @return The metadata, possibly empty. null if an error occured.
+ // FIXME: unhide.
+ * {@hide}
+ */
+ public Metadata getMetadata(final boolean update_only,
+ final boolean apply_filter) {
+ return null;
+ }
+
+ /**
+ * Set a filter for the metadata update notification and update
+ * retrieval. The caller provides 2 set of metadata keys, allowed
+ * and blocked. The blocked set always takes precedence over the
+ * allowed one.
+ * Metadata.MATCH_ALL and Metadata.MATCH_NONE are 2 sets available as
+ * shorthands to allow/block all or no metadata.
+ *
+ * By default, there is no filter set.
+ *
+ * @param allow Is the set of metadata the client is interested
+ * in receiving new notifications for.
+ * @param block Is the set of metadata the client is not interested
+ * in receiving new notifications for.
+ * @return The call status code.
+ *
+ // FIXME: unhide.
+ * {@hide}
+ */
+ public int setMetadataFilter(Set<Integer> allow, Set<Integer> block) {
+ return 0;
+ }
+
+ /**
+ * Set the MediaPlayer2 to start when this MediaPlayer2 finishes playback
+ * (i.e. reaches the end of the stream).
+ * The media framework will attempt to transition from this player to
+ * the next as seamlessly as possible. The next player can be set at
+ * any time before completion, but shall be after setDataSource has been
+ * called successfully. The next player must be prepared by the
+ * app, and the application should not call play() on it.
+ * The next MediaPlayer2 must be different from 'this'. An exception
+ * will be thrown if next == this.
+ * The application may call setNextMediaPlayer(null) to indicate no
+ * next player should be started at the end of playback.
+ * If the current player is looping, it will keep looping and the next
+ * player will not be started.
+ *
+ * @param next the player to start after this one completes playback.
+ *
+ * @hide
+ */
+ public void setNextMediaPlayer(MediaPlayer2 next) { }
+
+ /**
+ * Resets the MediaPlayer2 to its uninitialized state. After calling
+ * this method, you will have to initialize it again by setting the
+ * data source and calling prepareAsync().
+ */
+ public abstract void reset();
+
+ /**
+ * Set up a timer for {@link #TimeProvider}. {@link #TimeProvider} will be
+ * notified when the presentation time reaches (becomes greater than or equal to)
+ * the value specified.
+ *
+ * @param mediaTimeUs presentation time to get timed event callback at
+ * @hide
+ */
+ public void notifyAt(long mediaTimeUs) { }
+
+ /**
+ * Sets the audio attributes for this MediaPlayer2.
+ * See {@link AudioAttributes} for how to build and configure an instance of this class.
+ * You must call this method before {@link #prepareAsync()} in order
+ * for the audio attributes to become effective thereafter.
+ * @param attributes a non-null set of audio attributes
+ * @throws IllegalArgumentException if the attributes are null or invalid.
+ */
+ public abstract void setAudioAttributes(AudioAttributes attributes);
+
+ /**
+ * Sets the player to be looping or non-looping.
+ *
+ * @param looping whether to loop or not
+ * @hide
+ */
+ public void setLooping(boolean looping) { }
+
+ /**
+ * Checks whether the MediaPlayer2 is looping or non-looping.
+ *
+ * @return true if the MediaPlayer2 is currently looping, false otherwise
+ * @hide
+ */
+ public boolean isLooping() {
+ return false;
+ }
+
+ /**
+ * Sets the volume on this player.
+ * This API is recommended for balancing the output of audio streams
+ * within an application. Unless you are writing an application to
+ * control user settings, this API should be used in preference to
+ * {@link AudioManager#setStreamVolume(int, int, int)} which sets the volume of ALL streams of
+ * a particular type. Note that the passed volume values are raw scalars in range 0.0 to 1.0.
+ * UI controls should be scaled logarithmically.
+ *
+ * @param leftVolume left volume scalar
+ * @param rightVolume right volume scalar
+ */
+ /*
+ * FIXME: Merge this into javadoc comment above when setVolume(float) is not @hide.
+ * The single parameter form below is preferred if the channel volumes don't need
+ * to be set independently.
+ */
+ public abstract void setVolume(float leftVolume, float rightVolume);
+
+ /**
+ * Similar, excepts sets volume of all channels to same value.
+ * @hide
+ */
+ public void setVolume(float volume) { }
+
+ /**
+ * Sets the audio session ID.
+ *
+ * @param sessionId the audio session ID.
+ * The audio session ID is a system wide unique identifier for the audio stream played by
+ * this MediaPlayer2 instance.
+ * The primary use of the audio session ID is to associate audio effects to a particular
+ * instance of MediaPlayer2: if an audio session ID is provided when creating an audio effect,
+ * this effect will be applied only to the audio content of media players within the same
+ * audio session and not to the output mix.
+ * When created, a MediaPlayer2 instance automatically generates its own audio session ID.
+ * However, it is possible to force this player to be part of an already existing audio session
+ * by calling this method.
+ * This method must be called before one of the overloaded <code> setDataSource </code> methods.
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if the sessionId is invalid.
+ */
+ public abstract void setAudioSessionId(int sessionId);
+
+ /**
+ * Returns the audio session ID.
+ *
+ * @return the audio session ID. {@see #setAudioSessionId(int)}
+ * Note that the audio session ID is 0 only if a problem occured when the MediaPlayer2 was contructed.
+ */
+ public abstract int getAudioSessionId();
+
+ /**
+ * Attaches an auxiliary effect to the player. A typical auxiliary effect is a reverberation
+ * effect which can be applied on any sound source that directs a certain amount of its
+ * energy to this effect. This amount is defined by setAuxEffectSendLevel().
+ * See {@link #setAuxEffectSendLevel(float)}.
+ * <p>After creating an auxiliary effect (e.g.
+ * {@link android.media.audiofx.EnvironmentalReverb}), retrieve its ID with
+ * {@link android.media.audiofx.AudioEffect#getId()} and use it when calling this method
+ * to attach the player to the effect.
+ * <p>To detach the effect from the player, call this method with a null effect id.
+ * <p>This method must be called after one of the overloaded <code> setDataSource </code>
+ * methods.
+ * @param effectId system wide unique id of the effect to attach
+ */
+ public abstract void attachAuxEffect(int effectId);
+
+
+ /**
+ * Sets the send level of the player to the attached auxiliary effect.
+ * See {@link #attachAuxEffect(int)}. The level value range is 0 to 1.0.
+ * <p>By default the send level is 0, so even if an effect is attached to the player
+ * this method must be called for the effect to be applied.
+ * <p>Note that the passed level value is a raw scalar. UI controls should be scaled
+ * logarithmically: the gain applied by audio framework ranges from -72dB to 0dB,
+ * so an appropriate conversion from linear UI input x to level is:
+ * x == 0 -> level = 0
+ * 0 < x <= R -> level = 10^(72*(x-R)/20/R)
+ * @param level send level scalar
+ */
+ public abstract void setAuxEffectSendLevel(float level);
+
+ /**
+ * Class for MediaPlayer2 to return each audio/video/subtitle track's metadata.
+ *
+ * @see android.media.MediaPlayer2#getTrackInfo
+ */
+ public abstract static class TrackInfo {
+ /**
+ * Gets the track type.
+ * @return TrackType which indicates if the track is video, audio, timed text.
+ */
+ public abstract int getTrackType();
+
+ /**
+ * Gets the language code of the track.
+ * @return a language code in either way of ISO-639-1 or ISO-639-2.
+ * When the language is unknown or could not be determined,
+ * ISO-639-2 language code, "und", is returned.
+ */
+ public abstract String getLanguage();
+
+ /**
+ * Gets the {@link MediaFormat} of the track. If the format is
+ * unknown or could not be determined, null is returned.
+ */
+ public abstract MediaFormat getFormat();
+
+ public static final int MEDIA_TRACK_TYPE_UNKNOWN = 0;
+ public static final int MEDIA_TRACK_TYPE_VIDEO = 1;
+ public static final int MEDIA_TRACK_TYPE_AUDIO = 2;
+
+ /** @hide */
+ public static final int MEDIA_TRACK_TYPE_TIMEDTEXT = 3;
+
+ public static final int MEDIA_TRACK_TYPE_SUBTITLE = 4;
+ public static final int MEDIA_TRACK_TYPE_METADATA = 5;
+
+ @Override
+ public abstract String toString();
+ };
+
+ /**
+ * Returns a List of track information.
+ *
+ * @return List of track info. The total number of tracks is the array length.
+ * Must be called again if an external timed text source has been added after
+ * addTimedTextSource method is called.
+ * @throws IllegalStateException if it is called in an invalid state.
+ */
+ public abstract List<TrackInfo> getTrackInfo();
+
+ /* Do not change these values without updating their counterparts
+ * in include/media/stagefright/MediaDefs.h and media/libstagefright/MediaDefs.cpp!
+ */
+ /**
+ * MIME type for SubRip (SRT) container. Used in addTimedTextSource APIs.
+ * @hide
+ */
+ public static final String MEDIA_MIMETYPE_TEXT_SUBRIP = "application/x-subrip";
+
+ /**
+ * MIME type for WebVTT subtitle data.
+ * @hide
+ */
+ public static final String MEDIA_MIMETYPE_TEXT_VTT = "text/vtt";
+
+ /**
+ * MIME type for CEA-608 closed caption data.
+ * @hide
+ */
+ public static final String MEDIA_MIMETYPE_TEXT_CEA_608 = "text/cea-608";
+
+ /**
+ * MIME type for CEA-708 closed caption data.
+ * @hide
+ */
+ public static final String MEDIA_MIMETYPE_TEXT_CEA_708 = "text/cea-708";
+
+ /** @hide */
+ public void setSubtitleAnchor(
+ SubtitleController controller,
+ SubtitleController.Anchor anchor) { }
+
+ /** @hide */
+ @Override
+ public void onSubtitleTrackSelected(SubtitleTrack track) { }
+
+ /** @hide */
+ public void addSubtitleSource(InputStream is, MediaFormat format) { }
+
+ /* TODO: Limit the total number of external timed text source to a reasonable number.
+ */
+ /**
+ * Adds an external timed text source file.
+ *
+ * Currently supported format is SubRip with the file extension .srt, case insensitive.
+ * Note that a single external timed text source may contain multiple tracks in it.
+ * One can find the total number of available tracks using {@link #getTrackInfo()} to see what
+ * additional tracks become available after this method call.
+ *
+ * @param path The file path of external timed text source file.
+ * @param mimeType The mime type of the file. Must be one of the mime types listed above.
+ * @throws IOException if the file cannot be accessed or is corrupted.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ public void addTimedTextSource(String path, String mimeType) throws IOException { }
+
+ /**
+ * Adds an external timed text source file (Uri).
+ *
+ * Currently supported format is SubRip with the file extension .srt, case insensitive.
+ * Note that a single external timed text source may contain multiple tracks in it.
+ * One can find the total number of available tracks using {@link #getTrackInfo()} to see what
+ * additional tracks become available after this method call.
+ *
+ * @param context the Context to use when resolving the Uri
+ * @param uri the Content URI of the data you want to play
+ * @param mimeType The mime type of the file. Must be one of the mime types listed above.
+ * @throws IOException if the file cannot be accessed or is corrupted.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ public void addTimedTextSource(Context context, Uri uri, String mimeType) throws IOException { }
+
+ /**
+ * Adds an external timed text source file (FileDescriptor).
+ *
+ * It is the caller's responsibility to close the file descriptor.
+ * It is safe to do so as soon as this call returns.
+ *
+ * Currently supported format is SubRip. Note that a single external timed text source may
+ * contain multiple tracks in it. One can find the total number of available tracks
+ * using {@link #getTrackInfo()} to see what additional tracks become available
+ * after this method call.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @param mimeType The mime type of the file. Must be one of the mime types listed above.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ public void addTimedTextSource(FileDescriptor fd, String mimeType) { }
+
+ /**
+ * Adds an external timed text file (FileDescriptor).
+ *
+ * It is the caller's responsibility to close the file descriptor.
+ * It is safe to do so as soon as this call returns.
+ *
+ * Currently supported format is SubRip. Note that a single external timed text source may
+ * contain multiple tracks in it. One can find the total number of available tracks
+ * using {@link #getTrackInfo()} to see what additional tracks become available
+ * after this method call.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @param offset the offset into the file where the data to be played starts, in bytes
+ * @param length the length in bytes of the data to be played
+ * @param mime The mime type of the file. Must be one of the mime types listed above.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ public abstract void addTimedTextSource(FileDescriptor fd, long offset, long length, String mime);
+
+ /**
+ * Returns the index of the audio, video, or subtitle track currently selected for playback,
+ * The return value is an index into the array returned by {@link #getTrackInfo()}, and can
+ * be used in calls to {@link #selectTrack(int)} or {@link #deselectTrack(int)}.
+ *
+ * @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO},
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, or
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}
+ * @return index of the audio, video, or subtitle track currently selected for playback;
+ * a negative integer is returned when there is no selected track for {@code trackType} or
+ * when {@code trackType} is not one of audio, video, or subtitle.
+ * @throws IllegalStateException if called after {@link #close()}
+ *
+ * @see #getTrackInfo()
+ * @see #selectTrack(int)
+ * @see #deselectTrack(int)
+ */
+ public abstract int getSelectedTrack(int trackType);
+
+ /**
+ * Selects a track.
+ * <p>
+ * If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception.
+ * If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately.
+ * If a MediaPlayer2 is not in Started state, it just marks the track to be played.
+ * </p>
+ * <p>
+ * In any valid state, if it is called multiple times on the same type of track (ie. Video,
+ * Audio, Timed Text), the most recent one will be chosen.
+ * </p>
+ * <p>
+ * The first audio and video tracks are selected by default if available, even though
+ * this method is not called. However, no timed text track will be selected until
+ * this function is called.
+ * </p>
+ * <p>
+ * Currently, only timed text tracks or audio tracks can be selected via this method.
+ * In addition, the support for selecting an audio track at runtime is pretty limited
+ * in that an audio track can only be selected in the <em>Prepared</em> state.
+ * </p>
+ * @param index the index of the track to be selected. The valid range of the index
+ * is 0..total number of track - 1. The total number of tracks as well as the type of
+ * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ *
+ * @see android.media.MediaPlayer2#getTrackInfo
+ */
+ public abstract void selectTrack(int index);
+
+ /**
+ * Deselect a track.
+ * <p>
+ * Currently, the track must be a timed text track and no audio or video tracks can be
+ * deselected. If the timed text track identified by index has not been
+ * selected before, it throws an exception.
+ * </p>
+ * @param index the index of the track to be deselected. The valid range of the index
+ * is 0..total number of tracks - 1. The total number of tracks as well as the type of
+ * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ *
+ * @see android.media.MediaPlayer2#getTrackInfo
+ */
+ public abstract void deselectTrack(int index);
+
+ /**
+ * Sets the target UDP re-transmit endpoint for the low level player.
+ * Generally, the address portion of the endpoint is an IP multicast
+ * address, although a unicast address would be equally valid. When a valid
+ * retransmit endpoint has been set, the media player will not decode and
+ * render the media presentation locally. Instead, the player will attempt
+ * to re-multiplex its media data using the Android@Home RTP profile and
+ * re-transmit to the target endpoint. Receiver devices (which may be
+ * either the same as the transmitting device or different devices) may
+ * instantiate, prepare, and start a receiver player using a setDataSource
+ * URL of the form...
+ *
+ * aahRX://&lt;multicastIP&gt;:&lt;port&gt;
+ *
+ * to receive, decode and render the re-transmitted content.
+ *
+ * setRetransmitEndpoint may only be called before setDataSource has been
+ * called; while the player is in the Idle state.
+ *
+ * @param endpoint the address and UDP port of the re-transmission target or
+ * null if no re-transmission is to be performed.
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if the retransmit endpoint is supplied,
+ * but invalid.
+ *
+ * {@hide} pending API council
+ */
+ public void setRetransmitEndpoint(InetSocketAddress endpoint) { }
+
+ /**
+ * Releases the resources held by this {@code MediaPlayer2} object.
+ *
+ * It is considered good practice to call this method when you're
+ * done using the MediaPlayer2. In particular, whenever an Activity
+ * of an application is paused (its onPause() method is called),
+ * or stopped (its onStop() method is called), this method should be
+ * invoked to release the MediaPlayer2 object, unless the application
+ * has a special need to keep the object around. In addition to
+ * unnecessary resources (such as memory and instances of codecs)
+ * being held, failure to call this method immediately if a
+ * MediaPlayer2 object is no longer needed may also lead to
+ * continuous battery consumption for mobile devices, and playback
+ * failure for other applications if no multiple instances of the
+ * same codec are supported on a device. Even if multiple instances
+ * of the same codec are supported, some performance degradation
+ * may be expected when unnecessary multiple instances are used
+ * at the same time.
+ *
+ * {@code close()} may be safely called after a prior {@code close()}.
+ * This class implements the Java {@code AutoCloseable} interface and
+ * may be used with try-with-resources.
+ */
+ @Override
+ public abstract void close();
+
+ /** @hide */
+ public MediaTimeProvider getMediaTimeProvider() {
+ return null;
+ }
+
+ /**
+ * Interface definition for callbacks to be invoked when the player has the corresponding
+ * events.
+ */
+ public abstract static class EventCallback {
+ /**
+ * Called to update status in buffering a media source received through
+ * progressive downloading. The received buffering percentage
+ * indicates how much of the content has been buffered or played.
+ * For example a buffering update of 80 percent when half the content
+ * has already been played indicates that the next 30 percent of the
+ * content to play has been buffered.
+ *
+ * @param mp the MediaPlayer2 the update pertains to
+ * @param srcId the Id of this data source
+ * @param percent the percentage (0-100) of the content
+ * that has been buffered or played thus far
+ */
+ public void onBufferingUpdate(MediaPlayer2 mp, long srcId, int percent) { }
+
+ /**
+ * Called to indicate the video size
+ *
+ * The video size (width and height) could be 0 if there was no video,
+ * no display surface was set, or the value was not determined yet.
+ *
+ * @param mp the MediaPlayer2 associated with this callback
+ * @param srcId the Id of this data source
+ * @param width the width of the video
+ * @param height the height of the video
+ */
+ public void onVideoSizeChanged(MediaPlayer2 mp, long srcId, int width, int height) { }
+
+ /**
+ * Called to indicate an avaliable timed text
+ *
+ * @param mp the MediaPlayer2 associated with this callback
+ * @param srcId the Id of this data source
+ * @param text the timed text sample which contains the text
+ * needed to be displayed and the display format.
+ * @hide
+ */
+ public void onTimedText(MediaPlayer2 mp, long srcId, TimedText text) { }
+
+ /**
+ * Called to indicate avaliable timed metadata
+ * <p>
+ * This method will be called as timed metadata is extracted from the media,
+ * in the same order as it occurs in the media. The timing of this event is
+ * not controlled by the associated timestamp.
+ * <p>
+ * Currently only HTTP live streaming data URI's embedded with timed ID3 tags generates
+ * {@link TimedMetaData}.
+ *
+ * @see MediaPlayer2#selectTrack(int)
+ * @see MediaPlayer2.OnTimedMetaDataAvailableListener
+ * @see TimedMetaData
+ *
+ * @param mp the MediaPlayer2 associated with this callback
+ * @param srcId the Id of this data source
+ * @param data the timed metadata sample associated with this event
+ */
+ public void onTimedMetaDataAvailable(MediaPlayer2 mp, long srcId, TimedMetaData data) { }
+
+ /**
+ * Called to indicate an error.
+ *
+ * @param mp the MediaPlayer2 the error pertains to
+ * @param srcId the Id of this data source
+ * @param what the type of error that has occurred:
+ * <ul>
+ * <li>{@link #MEDIA_ERROR_UNKNOWN}
+ * </ul>
+ * @param extra an extra code, specific to the error. Typically
+ * implementation dependent.
+ * <ul>
+ * <li>{@link #MEDIA_ERROR_IO}
+ * <li>{@link #MEDIA_ERROR_MALFORMED}
+ * <li>{@link #MEDIA_ERROR_UNSUPPORTED}
+ * <li>{@link #MEDIA_ERROR_TIMED_OUT}
+ * <li><code>MEDIA_ERROR_SYSTEM (-2147483648)</code> - low-level system error.
+ * </ul>
+ */
+ public void onError(MediaPlayer2 mp, long srcId, int what, int extra) { }
+
+ /**
+ * Called to indicate an info or a warning.
+ *
+ * @param mp the MediaPlayer2 the info pertains to.
+ * @param srcId the Id of this data source
+ * @param what the type of info or warning.
+ * <ul>
+ * <li>{@link #MEDIA_INFO_UNKNOWN}
+ * <li>{@link #MEDIA_INFO_STARTED_AS_NEXT}
+ * <li>{@link #MEDIA_INFO_VIDEO_RENDERING_START}
+ * <li>{@link #MEDIA_INFO_AUDIO_RENDERING_START}
+ * <li>{@link #MEDIA_INFO_PLAYBACK_COMPLETE}
+ * <li>{@link #MEDIA_INFO_PLAYLIST_END}
+ * <li>{@link #MEDIA_INFO_PREPARED}
+ * <li>{@link #MEDIA_INFO_COMPLETE_CALL_PLAY}
+ * <li>{@link #MEDIA_INFO_COMPLETE_CALL_PAUSE}
+ * <li>{@link #MEDIA_INFO_COMPLETE_CALL_SEEK}
+ * <li>{@link #MEDIA_INFO_VIDEO_TRACK_LAGGING}
+ * <li>{@link #MEDIA_INFO_BUFFERING_START}
+ * <li>{@link #MEDIA_INFO_BUFFERING_END}
+ * <li><code>MEDIA_INFO_NETWORK_BANDWIDTH (703)</code> -
+ * bandwidth information is available (as <code>extra</code> kbps)
+ * <li>{@link #MEDIA_INFO_BAD_INTERLEAVING}
+ * <li>{@link #MEDIA_INFO_NOT_SEEKABLE}
+ * <li>{@link #MEDIA_INFO_METADATA_UPDATE}
+ * <li>{@link #MEDIA_INFO_UNSUPPORTED_SUBTITLE}
+ * <li>{@link #MEDIA_INFO_SUBTITLE_TIMED_OUT}
+ * </ul>
+ * @param extra an extra code, specific to the info. Typically
+ * implementation dependent.
+ */
+ public void onInfo(MediaPlayer2 mp, long srcId, int what, int extra) { }
+ }
+
+ /**
+ * Register a callback to be invoked when the media source is ready
+ * for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ public abstract void registerEventCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull EventCallback eventCallback);
+
+ /**
+ * Unregisters an {@link EventCallback}.
+ *
+ * @param callback an {@link EventCallback} to unregister
+ */
+ public abstract void unregisterEventCallback(EventCallback callback);
+
+ /**
+ * Interface definition of a callback to be invoked when a
+ * track has data available.
+ *
+ * @hide
+ */
+ public interface OnSubtitleDataListener
+ {
+ public void onSubtitleData(MediaPlayer2 mp, SubtitleData data);
+ }
+
+ /**
+ * Register a callback to be invoked when a track has data available.
+ *
+ * @param listener the callback that will be run
+ *
+ * @hide
+ */
+ public void setOnSubtitleDataListener(OnSubtitleDataListener listener) { }
+
+
+ /* Do not change these values without updating their counterparts
+ * in include/media/mediaplayer2.h!
+ */
+ /** Unspecified media player error.
+ * @see android.media.MediaPlayer2.EventCallback.onError
+ */
+ public static final int MEDIA_ERROR_UNKNOWN = 1;
+
+ /** The video is streamed and its container is not valid for progressive
+ * playback i.e the video's index (e.g moov atom) is not at the start of the
+ * file.
+ * @see android.media.MediaPlayer2.EventCallback.onError
+ */
+ public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 200;
+
+ /** File or network related operation errors. */
+ public static final int MEDIA_ERROR_IO = -1004;
+ /** Bitstream is not conforming to the related coding standard or file spec. */
+ public static final int MEDIA_ERROR_MALFORMED = -1007;
+ /** Bitstream is conforming to the related coding standard or file spec, but
+ * the media framework does not support the feature. */
+ public static final int MEDIA_ERROR_UNSUPPORTED = -1010;
+ /** Some operation takes too long to complete, usually more than 3-5 seconds. */
+ public static final int MEDIA_ERROR_TIMED_OUT = -110;
+
+ /** Unspecified low-level system error. This value originated from UNKNOWN_ERROR in
+ * system/core/include/utils/Errors.h
+ * @see android.media.MediaPlayer2.EventCallback.onError
+ * @hide
+ */
+ public static final int MEDIA_ERROR_SYSTEM = -2147483648;
+
+
+ /* Do not change these values without updating their counterparts
+ * in include/media/mediaplayer2.h!
+ */
+ /** Unspecified media player info.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_UNKNOWN = 1;
+
+ /** The player switched to this datas source because it is the
+ * next-to-be-played in the play list.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_STARTED_AS_NEXT = 2;
+
+ /** The player just pushed the very first video frame for rendering.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_VIDEO_RENDERING_START = 3;
+
+ /** The player just rendered the very first audio sample.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_AUDIO_RENDERING_START = 4;
+
+ /** The player just completed the playback of this data source.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_PLAYBACK_COMPLETE = 5;
+
+ /** The player just completed the playback of the full play list.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_PLAYLIST_END = 6;
+
+ /** The player just prepared a data source.
+ * This also serves as call completion notification for {@link #prepareAsync()}.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_PREPARED = 100;
+
+ /** The player just completed a call {@link #play()}.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_COMPLETE_CALL_PLAY = 101;
+
+ /** The player just completed a call {@link #pause()}.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_COMPLETE_CALL_PAUSE = 102;
+
+ /** The player just completed a call {@link #seekTo(long, int)}.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_COMPLETE_CALL_SEEK = 103;
+
+ /** The video is too complex for the decoder: it can't decode frames fast
+ * enough. Possibly only the audio plays fine at this stage.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_VIDEO_TRACK_LAGGING = 700;
+
+ /** MediaPlayer2 is temporarily pausing playback internally in order to
+ * buffer more data.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_BUFFERING_START = 701;
+
+ /** MediaPlayer2 is resuming playback after filling buffers.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_BUFFERING_END = 702;
+
+ /** Estimated network bandwidth information (kbps) is available; currently this event fires
+ * simultaneously as {@link #MEDIA_INFO_BUFFERING_START} and {@link #MEDIA_INFO_BUFFERING_END}
+ * when playing network files.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ * @hide
+ */
+ public static final int MEDIA_INFO_NETWORK_BANDWIDTH = 703;
+
+ /** Bad interleaving means that a media has been improperly interleaved or
+ * not interleaved at all, e.g has all the video samples first then all the
+ * audio ones. Video is playing but a lot of disk seeks may be happening.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_BAD_INTERLEAVING = 800;
+
+ /** The media cannot be seeked (e.g live stream)
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_NOT_SEEKABLE = 801;
+
+ /** A new set of metadata is available.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_METADATA_UPDATE = 802;
+
+ /** A new set of external-only metadata is available. Used by
+ * JAVA framework to avoid triggering track scanning.
+ * @hide
+ */
+ public static final int MEDIA_INFO_EXTERNAL_METADATA_UPDATE = 803;
+
+ /** Informs that audio is not playing. Note that playback of the video
+ * is not interrupted.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_AUDIO_NOT_PLAYING = 804;
+
+ /** Informs that video is not playing. Note that playback of the audio
+ * is not interrupted.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_VIDEO_NOT_PLAYING = 805;
+
+ /** Failed to handle timed text track properly.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ *
+ * {@hide}
+ */
+ public static final int MEDIA_INFO_TIMED_TEXT_ERROR = 900;
+
+ /** Subtitle track was not supported by the media framework.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_UNSUPPORTED_SUBTITLE = 901;
+
+ /** Reading the subtitle track takes too long.
+ * @see android.media.MediaPlayer2.EventCallback.onInfo
+ */
+ public static final int MEDIA_INFO_SUBTITLE_TIMED_OUT = 902;
+
+
+ // Modular DRM begin
+
+ /**
+ * Interface definition of a callback to be invoked when the app
+ * can do DRM configuration (get/set properties) before the session
+ * is opened. This facilitates configuration of the properties, like
+ * 'securityLevel', which has to be set after DRM scheme creation but
+ * before the DRM session is opened.
+ *
+ * The only allowed DRM calls in this listener are {@code getDrmPropertyString}
+ * and {@code setDrmPropertyString}.
+ */
+ public interface OnDrmConfigHelper
+ {
+ /**
+ * Called to give the app the opportunity to configure DRM before the session is created
+ *
+ * @param mp the {@code MediaPlayer2} associated with this callback
+ */
+ public void onDrmConfig(MediaPlayer2 mp);
+ }
+
+ /**
+ * Register a callback to be invoked for configuration of the DRM object before
+ * the session is created.
+ * The callback will be invoked synchronously during the execution
+ * of {@link #prepareDrm(UUID uuid)}.
+ *
+ * @param listener the callback that will be run
+ */
+ public abstract void setOnDrmConfigHelper(OnDrmConfigHelper listener);
+
+ /**
+ * Interface definition for callbacks to be invoked when the player has the corresponding
+ * DRM events.
+ */
+ public abstract static class DrmEventCallback {
+ /**
+ * Called to indicate DRM info is available
+ *
+ * @param mp the {@code MediaPlayer2} associated with this callback
+ * @param drmInfo DRM info of the source including PSSH, and subset
+ * of crypto schemes supported by this device
+ */
+ public void onDrmInfo(MediaPlayer2 mp, DrmInfo drmInfo) { }
+
+ /**
+ * Called to notify the client that {@code prepareDrm} is finished and ready for key request/response.
+ *
+ * @param mp the {@code MediaPlayer2} associated with this callback
+ * @param status the result of DRM preparation which can be
+ * {@link #PREPARE_DRM_STATUS_SUCCESS},
+ * {@link #PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR},
+ * {@link #PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR}, or
+ * {@link #PREPARE_DRM_STATUS_PREPARATION_ERROR}.
+ */
+ public void onDrmPrepared(MediaPlayer2 mp, @PrepareDrmStatusCode int status) { }
+
+ }
+
+ /**
+ * Register a callback to be invoked when the media source is ready
+ * for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ public abstract void registerDrmEventCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull DrmEventCallback eventCallback);
+
+ /**
+ * Unregisters a {@link DrmEventCallback}.
+ *
+ * @param callback a {@link DrmEventCallback} to unregister
+ */
+ public abstract void unregisterDrmEventCallback(DrmEventCallback callback);
+
+ /**
+ * The status codes for {@link DrmEventCallback#onDrmPrepared} listener.
+ * <p>
+ *
+ * DRM preparation has succeeded.
+ */
+ public static final int PREPARE_DRM_STATUS_SUCCESS = 0;
+
+ /**
+ * The device required DRM provisioning but couldn't reach the provisioning server.
+ */
+ public static final int PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR = 1;
+
+ /**
+ * The device required DRM provisioning but the provisioning server denied the request.
+ */
+ public static final int PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR = 2;
+
+ /**
+ * The DRM preparation has failed .
+ */
+ public static final int PREPARE_DRM_STATUS_PREPARATION_ERROR = 3;
+
+
+ /** @hide */
+ @IntDef({
+ PREPARE_DRM_STATUS_SUCCESS,
+ PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR,
+ PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR,
+ PREPARE_DRM_STATUS_PREPARATION_ERROR,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PrepareDrmStatusCode {}
+
+ /**
+ * Retrieves the DRM Info associated with the current source
+ *
+ * @throws IllegalStateException if called before being prepared
+ */
+ public abstract DrmInfo getDrmInfo();
+
+ /**
+ * Prepares the DRM for the current source
+ * <p>
+ * If {@code OnDrmConfigHelper} is registered, it will be called during
+ * preparation to allow configuration of the DRM properties before opening the
+ * DRM session. Note that the callback is called synchronously in the thread that called
+ * {@code prepareDrm}. It should be used only for a series of {@code getDrmPropertyString}
+ * and {@code setDrmPropertyString} calls and refrain from any lengthy operation.
+ * <p>
+ * If the device has not been provisioned before, this call also provisions the device
+ * which involves accessing the provisioning server and can take a variable time to
+ * complete depending on the network connectivity.
+ * If {@code OnDrmPreparedListener} is registered, prepareDrm() runs in non-blocking
+ * mode by launching the provisioning in the background and returning. The listener
+ * will be called when provisioning and preparation has finished. If a
+ * {@code OnDrmPreparedListener} is not registered, prepareDrm() waits till provisioning
+ * and preparation has finished, i.e., runs in blocking mode.
+ * <p>
+ * If {@code OnDrmPreparedListener} is registered, it is called to indicate the DRM
+ * session being ready. The application should not make any assumption about its call
+ * sequence (e.g., before or after prepareDrm returns), or the thread context that will
+ * execute the listener (unless the listener is registered with a handler thread).
+ * <p>
+ *
+ * @param uuid The UUID of the crypto scheme. If not known beforehand, it can be retrieved
+ * from the source through {@code getDrmInfo} or registering a {@code onDrmInfoListener}.
+ *
+ * @throws IllegalStateException if called before being prepared or the DRM was
+ * prepared already
+ * @throws UnsupportedSchemeException if the crypto scheme is not supported
+ * @throws ResourceBusyException if required DRM resources are in use
+ * @throws ProvisioningNetworkErrorException if provisioning is required but failed due to a
+ * network error
+ * @throws ProvisioningServerErrorException if provisioning is required but failed due to
+ * the request denied by the provisioning server
+ */
+ public abstract void prepareDrm(@NonNull UUID uuid)
+ throws UnsupportedSchemeException, ResourceBusyException,
+ ProvisioningNetworkErrorException, ProvisioningServerErrorException;
+
+ /**
+ * Releases the DRM session
+ * <p>
+ * The player has to have an active DRM session and be in stopped, or prepared
+ * state before this call is made.
+ * A {@code reset()} call will release the DRM session implicitly.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session to release
+ */
+ public abstract void releaseDrm() throws NoDrmSchemeException;
+
+ /**
+ * A key request/response exchange occurs between the app and a license server
+ * to obtain or release keys used to decrypt encrypted content.
+ * <p>
+ * getKeyRequest() is used to obtain an opaque key request byte array that is
+ * delivered to the license server. The opaque key request byte array is returned
+ * in KeyRequest.data. The recommended URL to deliver the key request to is
+ * returned in KeyRequest.defaultUrl.
+ * <p>
+ * After the app has received the key request response from the server,
+ * it should deliver to the response to the DRM engine plugin using the method
+ * {@link #provideKeyResponse}.
+ *
+ * @param keySetId is the key-set identifier of the offline keys being released when keyType is
+ * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when
+ * keyType is {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}.
+ *
+ * @param initData is the container-specific initialization data when the keyType is
+ * {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. Its meaning is
+ * interpreted based on the mime type provided in the mimeType parameter. It could
+ * contain, for example, the content ID, key ID or other data obtained from the content
+ * metadata that is required in generating the key request.
+ * When the keyType is {@link MediaDrm#KEY_TYPE_RELEASE}, it should be set to null.
+ *
+ * @param mimeType identifies the mime type of the content
+ *
+ * @param keyType specifies the type of the request. The request may be to acquire
+ * keys for streaming, {@link MediaDrm#KEY_TYPE_STREAMING}, or for offline content
+ * {@link MediaDrm#KEY_TYPE_OFFLINE}, or to release previously acquired
+ * keys ({@link MediaDrm#KEY_TYPE_RELEASE}), which are identified by a keySetId.
+ *
+ * @param optionalParameters are included in the key request message to
+ * allow a client application to provide additional message parameters to the server.
+ * This may be {@code null} if no additional parameters are to be sent.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ */
+ @NonNull
+ public abstract MediaDrm.KeyRequest getKeyRequest(@Nullable byte[] keySetId, @Nullable byte[] initData,
+ @Nullable String mimeType, @MediaDrm.KeyType int keyType,
+ @Nullable Map<String, String> optionalParameters)
+ throws NoDrmSchemeException;
+
+ /**
+ * A key response is received from the license server by the app, then it is
+ * provided to the DRM engine plugin using provideKeyResponse. When the
+ * response is for an offline key request, a key-set identifier is returned that
+ * can be used to later restore the keys to a new session with the method
+ * {@ link # restoreKeys}.
+ * When the response is for a streaming or release request, null is returned.
+ *
+ * @param keySetId When the response is for a release request, keySetId identifies
+ * the saved key associated with the release request (i.e., the same keySetId
+ * passed to the earlier {@ link # getKeyRequest} call. It MUST be null when the
+ * response is for either streaming or offline key requests.
+ *
+ * @param response the byte array response from the server
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ * @throws DeniedByServerException if the response indicates that the
+ * server rejected the request
+ */
+ public abstract byte[] provideKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
+ throws NoDrmSchemeException, DeniedByServerException;
+
+ /**
+ * Restore persisted offline keys into a new session. keySetId identifies the
+ * keys to load, obtained from a prior call to {@link #provideKeyResponse}.
+ *
+ * @param keySetId identifies the saved key set to restore
+ */
+ public abstract void restoreKeys(@NonNull byte[] keySetId)
+ throws NoDrmSchemeException;
+
+ /**
+ * Read a DRM engine plugin String property value, given the property name string.
+ * <p>
+ * @param propertyName the property name
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ @NonNull
+ public abstract String getDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName)
+ throws NoDrmSchemeException;
+
+ /**
+ * Set a DRM engine plugin String property value.
+ * <p>
+ * @param propertyName the property name
+ * @param value the property value
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ public abstract void setDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName,
+ @NonNull String value)
+ throws NoDrmSchemeException;
+
+ /**
+ * Encapsulates the DRM properties of the source.
+ */
+ public abstract static class DrmInfo {
+ /**
+ * Returns the PSSH info of the data source for each supported DRM scheme.
+ */
+ public abstract Map<UUID, byte[]> getPssh();
+
+ /**
+ * Returns the intersection of the data source and the device DRM schemes.
+ * It effectively identifies the subset of the source's DRM schemes which
+ * are supported by the device too.
+ */
+ public abstract List<UUID> getSupportedSchemes();
+ }; // DrmInfo
+
+ /**
+ * Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm().
+ * Extends MediaDrm.MediaDrmException
+ */
+ public abstract static class NoDrmSchemeException extends MediaDrmException {
+ protected NoDrmSchemeException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to a network error (Internet reachability, timeout, etc.).
+ * Extends MediaDrm.MediaDrmException
+ */
+ public abstract static class ProvisioningNetworkErrorException extends MediaDrmException {
+ protected ProvisioningNetworkErrorException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to the provisioning server denying the request.
+ * Extends MediaDrm.MediaDrmException
+ */
+ public abstract static class ProvisioningServerErrorException extends MediaDrmException {
+ protected ProvisioningServerErrorException(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ public static final class MetricsConstants {
+ private MetricsConstants() {}
+
+ /**
+ * Key to extract the MIME type of the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String MIME_TYPE_VIDEO = "android.media.mediaplayer.video.mime";
+
+ /**
+ * Key to extract the codec being used to decode the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String CODEC_VIDEO = "android.media.mediaplayer.video.codec";
+
+ /**
+ * Key to extract the width (in pixels) of the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String WIDTH = "android.media.mediaplayer.width";
+
+ /**
+ * Key to extract the height (in pixels) of the video track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String HEIGHT = "android.media.mediaplayer.height";
+
+ /**
+ * Key to extract the count of video frames played
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String FRAMES = "android.media.mediaplayer.frames";
+
+ /**
+ * Key to extract the count of video frames dropped
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String FRAMES_DROPPED = "android.media.mediaplayer.dropped";
+
+ /**
+ * Key to extract the MIME type of the audio track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String MIME_TYPE_AUDIO = "android.media.mediaplayer.audio.mime";
+
+ /**
+ * Key to extract the codec being used to decode the audio track
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a String.
+ */
+ public static final String CODEC_AUDIO = "android.media.mediaplayer.audio.codec";
+
+ /**
+ * Key to extract the duration (in milliseconds) of the
+ * media being played
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a long.
+ */
+ public static final String DURATION = "android.media.mediaplayer.durationMs";
+
+ /**
+ * Key to extract the playing time (in milliseconds) of the
+ * media being played
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is a long.
+ */
+ public static final String PLAYING = "android.media.mediaplayer.playingMs";
+
+ /**
+ * Key to extract the count of errors encountered while
+ * playing the media
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String ERRORS = "android.media.mediaplayer.err";
+
+ /**
+ * Key to extract an (optional) error code detected while
+ * playing the media
+ * from the {@link MediaPlayer2#getMetrics} return value.
+ * The value is an integer.
+ */
+ public static final String ERROR_CODE = "android.media.mediaplayer.errcode";
+
+ }
+}
diff --git a/android/media/MediaPlayer2Impl.java b/android/media/MediaPlayer2Impl.java
new file mode 100644
index 00000000..86a285cc
--- /dev/null
+++ b/android/media/MediaPlayer2Impl.java
@@ -0,0 +1,4899 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.content.ContentProvider;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.AssetFileDescriptor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PersistableBundle;
+import android.os.Process;
+import android.os.PowerManager;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.Log;
+import android.util.Pair;
+import android.util.ArrayMap;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.widget.VideoView;
+import android.graphics.SurfaceTexture;
+import android.media.AudioManager;
+import android.media.MediaDrm;
+import android.media.MediaFormat;
+import android.media.MediaPlayer2;
+import android.media.MediaTimeProvider;
+import android.media.PlaybackParams;
+import android.media.SubtitleController;
+import android.media.SubtitleController.Anchor;
+import android.media.SubtitleData;
+import android.media.SubtitleTrack.RenderingWidget;
+import android.media.SyncParams;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import dalvik.system.CloseGuard;
+
+import libcore.io.IoBridge;
+import libcore.io.Streams;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.AutoCloseable;
+import java.lang.Runnable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.net.CookieHandler;
+import java.net.CookieManager;
+import java.net.HttpCookie;
+import java.net.HttpURLConnection;
+import java.net.InetSocketAddress;
+import java.net.URL;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.concurrent.Executor;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Scanner;
+import java.util.Set;
+import java.util.UUID;
+import java.util.Vector;
+
+
+/**
+ * MediaPlayer2 class can be used to control playback
+ * of audio/video files and streams. An example on how to use the methods in
+ * this class can be found in {@link android.widget.VideoView}.
+ *
+ * <p>Topics covered here are:
+ * <ol>
+ * <li><a href="#StateDiagram">State Diagram</a>
+ * <li><a href="#Valid_and_Invalid_States">Valid and Invalid States</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * <li><a href="#Callbacks">Register informational and error callbacks</a>
+ * </ol>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about how to use MediaPlayer2, read the
+ * <a href="{@docRoot}guide/topics/media/mediaplayer.html">Media Playback</a> developer guide.</p>
+ * </div>
+ *
+ * <a name="StateDiagram"></a>
+ * <h3>State Diagram</h3>
+ *
+ * <p>Playback control of audio/video files and streams is managed as a state
+ * machine. The following diagram shows the life cycle and the states of a
+ * MediaPlayer2 object driven by the supported playback control operations.
+ * The ovals represent the states a MediaPlayer2 object may reside
+ * in. The arcs represent the playback control operations that drive the object
+ * state transition. There are two types of arcs. The arcs with a single arrow
+ * head represent synchronous method calls, while those with
+ * a double arrow head represent asynchronous method calls.</p>
+ *
+ * <p><img src="../../../images/mediaplayer_state_diagram.gif"
+ * alt="MediaPlayer State diagram"
+ * border="0" /></p>
+ *
+ * <p>From this state diagram, one can see that a MediaPlayer2 object has the
+ * following states:</p>
+ * <ul>
+ * <li>When a MediaPlayer2 object is just created using <code>new</code> or
+ * after {@link #reset()} is called, it is in the <em>Idle</em> state; and after
+ * {@link #close()} is called, it is in the <em>End</em> state. Between these
+ * two states is the life cycle of the MediaPlayer2 object.
+ * <ul>
+ * <li>There is a subtle but important difference between a newly constructed
+ * MediaPlayer2 object and the MediaPlayer2 object after {@link #reset()}
+ * is called. It is a programming error to invoke methods such
+ * as {@link #getCurrentPosition()},
+ * {@link #getDuration()}, {@link #getVideoHeight()},
+ * {@link #getVideoWidth()}, {@link #setAudioAttributes(AudioAttributes)},
+ * {@link #setLooping(boolean)},
+ * {@link #setVolume(float, float)}, {@link #pause()}, {@link #play()},
+ * {@link #seekTo(long, int)}, {@link #prepare()} or
+ * {@link #prepareAsync()} in the <em>Idle</em> state for both cases. If any of these
+ * methods is called right after a MediaPlayer2 object is constructed,
+ * the user supplied callback method OnErrorListener.onError() won't be
+ * called by the internal player engine and the object state remains
+ * unchanged; but if these methods are called right after {@link #reset()},
+ * the user supplied callback method OnErrorListener.onError() will be
+ * invoked by the internal player engine and the object will be
+ * transfered to the <em>Error</em> state. </li>
+ * <li>It is also recommended that once
+ * a MediaPlayer2 object is no longer being used, call {@link #close()} immediately
+ * so that resources used by the internal player engine associated with the
+ * MediaPlayer2 object can be released immediately. Resource may include
+ * singleton resources such as hardware acceleration components and
+ * failure to call {@link #close()} may cause subsequent instances of
+ * MediaPlayer2 objects to fallback to software implementations or fail
+ * altogether. Once the MediaPlayer2
+ * object is in the <em>End</em> state, it can no longer be used and
+ * there is no way to bring it back to any other state. </li>
+ * <li>Furthermore,
+ * the MediaPlayer2 objects created using <code>new</code> is in the
+ * <em>Idle</em> state.
+ * </li>
+ * </ul>
+ * </li>
+ * <li>In general, some playback control operation may fail due to various
+ * reasons, such as unsupported audio/video format, poorly interleaved
+ * audio/video, resolution too high, streaming timeout, and the like.
+ * Thus, error reporting and recovery is an important concern under
+ * these circumstances. Sometimes, due to programming errors, invoking a playback
+ * control operation in an invalid state may also occur. Under all these
+ * error conditions, the internal player engine invokes a user supplied
+ * EventCallback.onError() method if an EventCallback has been
+ * registered beforehand via
+ * {@link #registerEventCallback(Executor, EventCallback)}.
+ * <ul>
+ * <li>It is important to note that once an error occurs, the
+ * MediaPlayer2 object enters the <em>Error</em> state (except as noted
+ * above), even if an error listener has not been registered by the application.</li>
+ * <li>In order to reuse a MediaPlayer2 object that is in the <em>
+ * Error</em> state and recover from the error,
+ * {@link #reset()} can be called to restore the object to its <em>Idle</em>
+ * state.</li>
+ * <li>It is good programming practice to have your application
+ * register a OnErrorListener to look out for error notifications from
+ * the internal player engine.</li>
+ * <li>IllegalStateException is
+ * thrown to prevent programming errors such as calling {@link #prepare()},
+ * {@link #prepareAsync()}, {@link #setDataSource(DataSourceDesc)}, or
+ * {@code setPlaylist} methods in an invalid state. </li>
+ * </ul>
+ * </li>
+ * <li>Calling
+ * {@link #setDataSource(DataSourceDesc)}, or
+ * {@code setPlaylist} transfers a
+ * MediaPlayer2 object in the <em>Idle</em> state to the
+ * <em>Initialized</em> state.
+ * <ul>
+ * <li>An IllegalStateException is thrown if
+ * setDataSource() or setPlaylist() is called in any other state.</li>
+ * <li>It is good programming
+ * practice to always look out for <code>IllegalArgumentException</code>
+ * and <code>IOException</code> that may be thrown from
+ * <code>setDataSource</code> and <code>setPlaylist</code> methods.</li>
+ * </ul>
+ * </li>
+ * <li>A MediaPlayer2 object must first enter the <em>Prepared</em> state
+ * before playback can be started.
+ * <ul>
+ * <li>There are two ways (synchronous vs.
+ * asynchronous) that the <em>Prepared</em> state can be reached:
+ * either a call to {@link #prepare()} (synchronous) which
+ * transfers the object to the <em>Prepared</em> state once the method call
+ * returns, or a call to {@link #prepareAsync()} (asynchronous) which
+ * first transfers the object to the <em>Preparing</em> state after the
+ * call returns (which occurs almost right way) while the internal
+ * player engine continues working on the rest of preparation work
+ * until the preparation work completes. When the preparation completes or when {@link #prepare()} call returns,
+ * the internal player engine then calls a user supplied callback method,
+ * onPrepared() of the EventCallback interface, if an
+ * EventCallback is registered beforehand via {@link
+ * #registerEventCallback(Executor, EventCallback)}.</li>
+ * <li>It is important to note that
+ * the <em>Preparing</em> state is a transient state, and the behavior
+ * of calling any method with side effect while a MediaPlayer2 object is
+ * in the <em>Preparing</em> state is undefined.</li>
+ * <li>An IllegalStateException is
+ * thrown if {@link #prepare()} or {@link #prepareAsync()} is called in
+ * any other state.</li>
+ * <li>While in the <em>Prepared</em> state, properties
+ * such as audio/sound volume, screenOnWhilePlaying, looping can be
+ * adjusted by invoking the corresponding set methods.</li>
+ * </ul>
+ * </li>
+ * <li>To start the playback, {@link #play()} must be called. After
+ * {@link #play()} returns successfully, the MediaPlayer2 object is in the
+ * <em>Started</em> state. {@link #isPlaying()} can be called to test
+ * whether the MediaPlayer2 object is in the <em>Started</em> state.
+ * <ul>
+ * <li>While in the <em>Started</em> state, the internal player engine calls
+ * a user supplied EventCallback.onBufferingUpdate() callback
+ * method if an EventCallback has been registered beforehand
+ * via {@link #registerEventCallback(Executor, EventCallback)}.
+ * This callback allows applications to keep track of the buffering status
+ * while streaming audio/video.</li>
+ * <li>Calling {@link #play()} has not effect
+ * on a MediaPlayer2 object that is already in the <em>Started</em> state.</li>
+ * </ul>
+ * </li>
+ * <li>Playback can be paused and stopped, and the current playback position
+ * can be adjusted. Playback can be paused via {@link #pause()}. When the call to
+ * {@link #pause()} returns, the MediaPlayer2 object enters the
+ * <em>Paused</em> state. Note that the transition from the <em>Started</em>
+ * state to the <em>Paused</em> state and vice versa happens
+ * asynchronously in the player engine. It may take some time before
+ * the state is updated in calls to {@link #isPlaying()}, and it can be
+ * a number of seconds in the case of streamed content.
+ * <ul>
+ * <li>Calling {@link #play()} to resume playback for a paused
+ * MediaPlayer2 object, and the resumed playback
+ * position is the same as where it was paused. When the call to
+ * {@link #play()} returns, the paused MediaPlayer2 object goes back to
+ * the <em>Started</em> state.</li>
+ * <li>Calling {@link #pause()} has no effect on
+ * a MediaPlayer2 object that is already in the <em>Paused</em> state.</li>
+ * </ul>
+ * </li>
+ * <li>The playback position can be adjusted with a call to
+ * {@link #seekTo(long, int)}.
+ * <ul>
+ * <li>Although the asynchronuous {@link #seekTo(long, int)}
+ * call returns right away, the actual seek operation may take a while to
+ * finish, especially for audio/video being streamed. When the actual
+ * seek operation completes, the internal player engine calls a user
+ * supplied EventCallback.onSeekComplete() if an EventCallback
+ * has been registered beforehand via
+ * {@link #registerEventCallback(Executor, EventCallback)}.</li>
+ * <li>Please
+ * note that {@link #seekTo(long, int)} can also be called in the other states,
+ * such as <em>Prepared</em>, <em>Paused</em> and <em>PlaybackCompleted
+ * </em> state. When {@link #seekTo(long, int)} is called in those states,
+ * one video frame will be displayed if the stream has video and the requested
+ * position is valid.
+ * </li>
+ * <li>Furthermore, the actual current playback position
+ * can be retrieved with a call to {@link #getCurrentPosition()}, which
+ * is helpful for applications such as a Music player that need to keep
+ * track of the playback progress.</li>
+ * </ul>
+ * </li>
+ * <li>When the playback reaches the end of stream, the playback completes.
+ * <ul>
+ * <li>If the looping mode was being set to <var>true</var>with
+ * {@link #setLooping(boolean)}, the MediaPlayer2 object shall remain in
+ * the <em>Started</em> state.</li>
+ * <li>If the looping mode was set to <var>false
+ * </var>, the player engine calls a user supplied callback method,
+ * EventCallback.onCompletion(), if an EventCallback is registered
+ * beforehand via {@link #registerEventCallback(Executor, EventCallback)}.
+ * The invoke of the callback signals that the object is now in the <em>
+ * PlaybackCompleted</em> state.</li>
+ * <li>While in the <em>PlaybackCompleted</em>
+ * state, calling {@link #play()} can restart the playback from the
+ * beginning of the audio/video source.</li>
+ * </ul>
+ *
+ *
+ * <a name="Valid_and_Invalid_States"></a>
+ * <h3>Valid and invalid states</h3>
+ *
+ * <table border="0" cellspacing="0" cellpadding="0">
+ * <tr><td>Method Name </p></td>
+ * <td>Valid Sates </p></td>
+ * <td>Invalid States </p></td>
+ * <td>Comments </p></td></tr>
+ * <tr><td>attachAuxEffect </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ * <td>{Idle, Error} </p></td>
+ * <td>This method must be called after setDataSource or setPlaylist.
+ * Calling it does not change the object state. </p></td></tr>
+ * <tr><td>getAudioSessionId </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>getCurrentPosition </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted} </p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getDuration </p></td>
+ * <td>{Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ * <td>{Idle, Initialized, Error} </p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getVideoHeight </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change the
+ * state. Calling this method in an invalid state transfers the object
+ * to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>getVideoWidth </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>isPlaying </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>pause </p></td>
+ * <td>{Started, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Prepared, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Paused</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>prepare </p></td>
+ * <td>{Initialized, Stopped} </p></td>
+ * <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Prepared</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>prepareAsync </p></td>
+ * <td>{Initialized, Stopped} </p></td>
+ * <td>{Idle, Prepared, Started, Paused, PlaybackCompleted, Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Preparing</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>release </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>After {@link #close()}, the object is no longer available. </p></td></tr>
+ * <tr><td>reset </p></td>
+ * <td>{Idle, Initialized, Prepared, Started, Paused, Stopped,
+ * PlaybackCompleted, Error}</p></td>
+ * <td>{}</p></td>
+ * <td>After {@link #reset()}, the object is like being just created.</p></td></tr>
+ * <tr><td>seekTo </p></td>
+ * <td>{Prepared, Started, Paused, PlaybackCompleted} </p></td>
+ * <td>{Idle, Initialized, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an invalid state transfers the
+ * object to the <em>Error</em> state. </p></td></tr>
+ * <tr><td>setAudioAttributes </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state. In order for the
+ * target audio attributes type to become effective, this method must be called before
+ * prepare() or prepareAsync().</p></td></tr>
+ * <tr><td>setAudioSessionId </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>This method must be called in idle state as the audio session ID must be known before
+ * calling setDataSource or setPlaylist. Calling it does not change the object
+ * state. </p></td></tr>
+ * <tr><td>setAudioStreamType (deprecated)</p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state. In order for the
+ * target audio stream type to become effective, this method must be called before
+ * prepare() or prepareAsync().</p></td></tr>
+ * <tr><td>setAuxEffectSendLevel </p></td>
+ * <td>any</p></td>
+ * <td>{} </p></td>
+ * <td>Calling this method does not change the object state. </p></td></tr>
+ * <tr><td>setDataSource </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Initialized</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>setPlaylist </p></td>
+ * <td>{Idle} </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted,
+ * Error} </p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Initialized</em> state. Calling this method in an
+ * invalid state throws an IllegalStateException.</p></td></tr>
+ * <tr><td>setDisplay </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setSurface </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setVideoScalingMode </p></td>
+ * <td>{Initialized, Prepared, Started, Paused, Stopped, PlaybackCompleted} </p></td>
+ * <td>{Idle, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>setLooping </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method in a valid state does not change
+ * the state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>isLooping </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>registerDrmEventCallback </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>registerEventCallback </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setPlaybackParams</p></td>
+ * <td>{Initialized, Prepared, Started, Paused, PlaybackCompleted, Error}</p></td>
+ * <td>{Idle, Stopped} </p></td>
+ * <td>This method will change state in some cases, depending on when it's called.
+ * </p></td></tr>
+ * <tr><td>setScreenOnWhilePlaying</></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state. </p></td></tr>
+ * <tr><td>setVolume </p></td>
+ * <td>{Idle, Initialized, Stopped, Prepared, Started, Paused,
+ * PlaybackCompleted}</p></td>
+ * <td>{Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.
+ * <tr><td>setWakeMode </p></td>
+ * <td>any </p></td>
+ * <td>{} </p></td>
+ * <td>This method can be called in any state and calling it does not change
+ * the object state.</p></td></tr>
+ * <tr><td>start </p></td>
+ * <td>{Prepared, Started, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Stopped, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Started</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>stop </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method in a valid state transfers the
+ * object to the <em>Stopped</em> state. Calling this method in an
+ * invalid state transfers the object to the <em>Error</em> state.</p></td></tr>
+ * <tr><td>getTrackInfo </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>addTimedTextSource </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>selectTrack </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ * <tr><td>deselectTrack </p></td>
+ * <td>{Prepared, Started, Stopped, Paused, PlaybackCompleted}</p></td>
+ * <td>{Idle, Initialized, Error}</p></td>
+ * <td>Successful invoke of this method does not change the state.</p></td></tr>
+ *
+ * </table>
+ *
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ * <p>One may need to declare a corresponding WAKE_LOCK permission {@link
+ * android.R.styleable#AndroidManifestUsesPermission &lt;uses-permission&gt;}
+ * element.
+ *
+ * <p>This class requires the {@link android.Manifest.permission#INTERNET} permission
+ * when used with network-based content.
+ *
+ * <a name="Callbacks"></a>
+ * <h3>Callbacks</h3>
+ * <p>Applications may want to register for informational and error
+ * events in order to be informed of some internal state update and
+ * possible runtime errors during playback or streaming. Registration for
+ * these events is done by properly setting the appropriate listeners (via calls
+ * to
+ * {@link #registerEventCallback(Executor, EventCallback)},
+ * {@link #registerDrmEventCallback(Executor, DrmEventCallback)}).
+ * In order to receive the respective callback
+ * associated with these listeners, applications are required to create
+ * MediaPlayer2 objects on a thread with its own Looper running (main UI
+ * thread by default has a Looper running).
+ *
+ * @hide
+ */
+public final class MediaPlayer2Impl extends MediaPlayer2 {
+ static {
+ System.loadLibrary("media2_jni");
+ native_init();
+ }
+
+ private final static String TAG = "MediaPlayer2Impl";
+
+ private long mNativeContext; // accessed by native methods
+ private long mNativeSurfaceTexture; // accessed by native methods
+ private int mListenerContext; // accessed by native methods
+ private SurfaceHolder mSurfaceHolder;
+ private EventHandler mEventHandler;
+ private PowerManager.WakeLock mWakeLock = null;
+ private boolean mScreenOnWhilePlaying;
+ private boolean mStayAwake;
+ private int mStreamType = AudioManager.USE_DEFAULT_STREAM_TYPE;
+ private int mUsage = -1;
+ private boolean mBypassInterruptionPolicy;
+ private final CloseGuard mGuard = CloseGuard.get();
+
+ private List<DataSourceDesc> mPlaylist;
+ private int mPLCurrentIndex = 0;
+ private int mPLNextIndex = -1;
+ private int mLoopingMode = LOOPING_MODE_NONE;
+
+ // Modular DRM
+ private UUID mDrmUUID;
+ private final Object mDrmLock = new Object();
+ private DrmInfoImpl mDrmInfoImpl;
+ private MediaDrm mDrmObj;
+ private byte[] mDrmSessionId;
+ private boolean mDrmInfoResolved;
+ private boolean mActiveDrmScheme;
+ private boolean mDrmConfigAllowed;
+ private boolean mDrmProvisioningInProgress;
+ private boolean mPrepareDrmInProgress;
+ private ProvisioningThread mDrmProvisioningThread;
+
+ /**
+ * Default constructor.
+ * <p>When done with the MediaPlayer2Impl, you should call {@link #close()},
+ * to free the resources. If not released, too many MediaPlayer2Impl instances may
+ * result in an exception.</p>
+ */
+ public MediaPlayer2Impl() {
+ Looper looper;
+ if ((looper = Looper.myLooper()) != null) {
+ mEventHandler = new EventHandler(this, looper);
+ } else if ((looper = Looper.getMainLooper()) != null) {
+ mEventHandler = new EventHandler(this, looper);
+ } else {
+ mEventHandler = null;
+ }
+
+ mTimeProvider = new TimeProvider(this);
+ mOpenSubtitleSources = new Vector<InputStream>();
+ mGuard.open("close");
+
+ /* Native setup requires a weak reference to our object.
+ * It's easier to create it here than in C++.
+ */
+ native_setup(new WeakReference<MediaPlayer2Impl>(this));
+ }
+
+ /*
+ * Update the MediaPlayer2Impl SurfaceTexture.
+ * Call after setting a new display surface.
+ */
+ private native void _setVideoSurface(Surface surface);
+
+ /* Do not change these values (starting with INVOKE_ID) without updating
+ * their counterparts in include/media/mediaplayer2.h!
+ */
+ private static final int INVOKE_ID_GET_TRACK_INFO = 1;
+ private static final int INVOKE_ID_ADD_EXTERNAL_SOURCE = 2;
+ private static final int INVOKE_ID_ADD_EXTERNAL_SOURCE_FD = 3;
+ private static final int INVOKE_ID_SELECT_TRACK = 4;
+ private static final int INVOKE_ID_DESELECT_TRACK = 5;
+ private static final int INVOKE_ID_SET_VIDEO_SCALE_MODE = 6;
+ private static final int INVOKE_ID_GET_SELECTED_TRACK = 7;
+
+ /**
+ * Create a request parcel which can be routed to the native media
+ * player using {@link #invoke(Parcel, Parcel)}. The Parcel
+ * returned has the proper InterfaceToken set. The caller should
+ * not overwrite that token, i.e it can only append data to the
+ * Parcel.
+ *
+ * @return A parcel suitable to hold a request for the native
+ * player.
+ * {@hide}
+ */
+ @Override
+ public Parcel newRequest() {
+ Parcel parcel = Parcel.obtain();
+ return parcel;
+ }
+
+ /**
+ * Invoke a generic method on the native player using opaque
+ * parcels for the request and reply. Both payloads' format is a
+ * convention between the java caller and the native player.
+ * Must be called after setDataSource or setPlaylist to make sure a native player
+ * exists. On failure, a RuntimeException is thrown.
+ *
+ * @param request Parcel with the data for the extension. The
+ * caller must use {@link #newRequest()} to get one.
+ *
+ * @param reply Output parcel with the data returned by the
+ * native player.
+ * {@hide}
+ */
+ @Override
+ public void invoke(Parcel request, Parcel reply) {
+ int retcode = native_invoke(request, reply);
+ reply.setDataPosition(0);
+ if (retcode != 0) {
+ throw new RuntimeException("failure code: " + retcode);
+ }
+ }
+
+ /**
+ * Sets the {@link SurfaceHolder} to use for displaying the video
+ * portion of the media.
+ *
+ * Either a surface holder or surface must be set if a display or video sink
+ * is needed. Not calling this method or {@link #setSurface(Surface)}
+ * when playing back a video will result in only the audio track being played.
+ * A null surface holder or surface will result in only the audio track being
+ * played.
+ *
+ * @param sh the SurfaceHolder to use for video display
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ * @hide
+ */
+ @Override
+ public void setDisplay(SurfaceHolder sh) {
+ mSurfaceHolder = sh;
+ Surface surface;
+ if (sh != null) {
+ surface = sh.getSurface();
+ } else {
+ surface = null;
+ }
+ _setVideoSurface(surface);
+ updateSurfaceScreenOn();
+ }
+
+ /**
+ * Sets the {@link Surface} to be used as the sink for the video portion of
+ * the media. This is similar to {@link #setDisplay(SurfaceHolder)}, but
+ * does not support {@link #setScreenOnWhilePlaying(boolean)}. Setting a
+ * Surface will un-set any Surface or SurfaceHolder that was previously set.
+ * A null surface will result in only the audio track being played.
+ *
+ * If the Surface sends frames to a {@link SurfaceTexture}, the timestamps
+ * returned from {@link SurfaceTexture#getTimestamp()} will have an
+ * unspecified zero point. These timestamps cannot be directly compared
+ * between different media sources, different instances of the same media
+ * source, or multiple runs of the same program. The timestamp is normally
+ * monotonically increasing and is unaffected by time-of-day adjustments,
+ * but it is reset when the position is set.
+ *
+ * @param surface The {@link Surface} to be used for the video portion of
+ * the media.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ */
+ @Override
+ public void setSurface(Surface surface) {
+ if (mScreenOnWhilePlaying && surface != null) {
+ Log.w(TAG, "setScreenOnWhilePlaying(true) is ineffective for Surface");
+ }
+ mSurfaceHolder = null;
+ _setVideoSurface(surface);
+ updateSurfaceScreenOn();
+ }
+
+ /**
+ * Sets video scaling mode. To make the target video scaling mode
+ * effective during playback, this method must be called after
+ * data source is set. If not called, the default video
+ * scaling mode is {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT}.
+ *
+ * <p> The supported video scaling modes are:
+ * <ul>
+ * <li> {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT}
+ * <li> {@link #VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING}
+ * </ul>
+ *
+ * @param mode target video scaling mode. Must be one of the supported
+ * video scaling modes; otherwise, IllegalArgumentException will be thrown.
+ *
+ * @see MediaPlayer2#VIDEO_SCALING_MODE_SCALE_TO_FIT
+ * @see MediaPlayer2#VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING
+ * @hide
+ */
+ @Override
+ public void setVideoScalingMode(int mode) {
+ if (!isVideoScalingModeSupported(mode)) {
+ final String msg = "Scaling mode " + mode + " is not supported";
+ throw new IllegalArgumentException(msg);
+ }
+ Parcel request = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ try {
+ request.writeInt(INVOKE_ID_SET_VIDEO_SCALE_MODE);
+ request.writeInt(mode);
+ invoke(request, reply);
+ } finally {
+ request.recycle();
+ reply.recycle();
+ }
+ }
+
+ /**
+ * Discards all pending commands.
+ */
+ @Override
+ public void clearPendingCommands() {
+ }
+
+ /**
+ * Sets the data source as described by a DataSourceDesc.
+ *
+ * @param dsd the descriptor of data source you want to play
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws NullPointerException if dsd is null
+ */
+ @Override
+ public void setDataSource(@NonNull DataSourceDesc dsd) throws IOException {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+ mPlaylist = Collections.synchronizedList(new ArrayList<DataSourceDesc>(1));
+ mPlaylist.add(dsd);
+ mPLCurrentIndex = 0;
+ setDataSourcePriv(dsd);
+ }
+
+ /**
+ * Gets the current data source as described by a DataSourceDesc.
+ *
+ * @return the current DataSourceDesc
+ */
+ @Override
+ public DataSourceDesc getCurrentDataSource() {
+ if (mPlaylist == null) {
+ return null;
+ }
+ return mPlaylist.get(mPLCurrentIndex);
+ }
+
+ /**
+ * Sets the play list.
+ *
+ * If startIndex falls outside play list range, it will be clamped to the nearest index
+ * in the play list.
+ *
+ * @param pl the play list of data source you want to play
+ * @param startIndex the index of the DataSourceDesc in the play list you want to play first
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if pl is null or empty, or pl contains null DataSourceDesc
+ */
+ @Override
+ public void setPlaylist(@NonNull List<DataSourceDesc> pl, int startIndex)
+ throws IOException {
+ if (pl == null || pl.size() == 0) {
+ throw new IllegalArgumentException("play list cannot be null or empty.");
+ }
+ HashSet ids = new HashSet(pl.size());
+ for (DataSourceDesc dsd : pl) {
+ if (dsd == null) {
+ throw new IllegalArgumentException("DataSourceDesc in play list cannot be null.");
+ }
+ if (ids.add(dsd.getId()) == false) {
+ throw new IllegalArgumentException("DataSourceDesc Id in play list should be unique.");
+ }
+ }
+
+ if (startIndex < 0) {
+ startIndex = 0;
+ } else if (startIndex >= pl.size()) {
+ startIndex = pl.size() - 1;
+ }
+
+ mPlaylist = Collections.synchronizedList(new ArrayList(pl));
+ mPLCurrentIndex = startIndex;
+ setDataSourcePriv(mPlaylist.get(startIndex));
+ // TODO: handle the preparation of next source in the play list.
+ // It should be processed after current source is prepared.
+ }
+
+ /**
+ * Gets a copy of the play list.
+ *
+ * @return a copy of the play list used by {@link MediaPlayer2}
+ */
+ @Override
+ public List<DataSourceDesc> getPlaylist() {
+ if (mPlaylist == null) {
+ return null;
+ }
+ return new ArrayList(mPlaylist);
+ }
+
+ /**
+ * Sets the index of current DataSourceDesc in the play list to be played.
+ *
+ * @param index the index of DataSourceDesc in the play list you want to play
+ * @throws IllegalArgumentException if the play list is null
+ * @throws NullPointerException if index is outside play list range
+ */
+ @Override
+ public void setCurrentPlaylistItem(int index) {
+ if (mPlaylist == null) {
+ throw new IllegalArgumentException("play list has not been set yet.");
+ }
+ if (index < 0 || index >= mPlaylist.size()) {
+ throw new IndexOutOfBoundsException("index is out of play list range.");
+ }
+
+ if (index == mPLCurrentIndex) {
+ return;
+ }
+
+ // TODO: in playing state, stop current source and start to play source of index.
+ mPLCurrentIndex = index;
+ }
+
+ /**
+ * Sets the index of next-to-be-played DataSourceDesc in the play list.
+ *
+ * @param index the index of next-to-be-played DataSourceDesc in the play list
+ * @throws IllegalArgumentException if the play list is null
+ * @throws NullPointerException if index is outside play list range
+ */
+ @Override
+ public void setNextPlaylistItem(int index) {
+ if (mPlaylist == null) {
+ throw new IllegalArgumentException("play list has not been set yet.");
+ }
+ if (index < 0 || index >= mPlaylist.size()) {
+ throw new IndexOutOfBoundsException("index is out of play list range.");
+ }
+
+ if (index == mPLNextIndex) {
+ return;
+ }
+
+ // TODO: prepare the new next-to-be-played DataSourceDesc
+ mPLNextIndex = index;
+ }
+
+ /**
+ * Gets the current index of play list.
+ *
+ * @return the index of the current DataSourceDesc in the play list
+ */
+ @Override
+ public int getCurrentPlaylistItemIndex() {
+ return mPLCurrentIndex;
+ }
+
+ /**
+ * Sets the looping mode of the play list.
+ * The mode shall be one of {@link #LOOPING_MODE_NONE}, {@link #LOOPING_MODE_FULL},
+ * {@link #LOOPING_MODE_SINGLE}, {@link #LOOPING_MODE_SHUFFLE}.
+ *
+ * @param mode the mode in which the play list will be played
+ * @throws IllegalArgumentException if mode is not supported
+ */
+ @Override
+ public void setLoopingMode(@LoopingMode int mode) {
+ if (mode != LOOPING_MODE_NONE
+ && mode != LOOPING_MODE_FULL
+ && mode != LOOPING_MODE_SINGLE
+ && mode != LOOPING_MODE_SHUFFLE) {
+ throw new IllegalArgumentException("mode is not supported.");
+ }
+ mLoopingMode = mode;
+ if (mPlaylist == null) {
+ return;
+ }
+
+ // TODO: handle the new mode if necessary.
+ }
+
+ /**
+ * Gets the looping mode of play list.
+ *
+ * @return the looping mode of the play list
+ */
+ @Override
+ public int getLoopingMode() {
+ return mPLCurrentIndex;
+ }
+
+ /**
+ * Moves the DataSourceDesc at indexFrom in the play list to indexTo.
+ *
+ * @throws IllegalArgumentException if the play list is null
+ * @throws IndexOutOfBoundsException if indexFrom or indexTo is outside play list range
+ */
+ @Override
+ public void movePlaylistItem(int indexFrom, int indexTo) {
+ if (mPlaylist == null) {
+ throw new IllegalArgumentException("play list has not been set yet.");
+ }
+ // TODO: move the DataSourceDesc from indexFrom to indexTo.
+ }
+
+ /**
+ * Removes the DataSourceDesc at index in the play list.
+ *
+ * If index is same as the current index of the play list, current DataSourceDesc
+ * will be stopped and playback moves to next source in the list.
+ *
+ * @return the removed DataSourceDesc at index in the play list
+ * @throws IllegalArgumentException if the play list is null
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ */
+ @Override
+ public DataSourceDesc removePlaylistItem(int index) {
+ if (mPlaylist == null) {
+ throw new IllegalArgumentException("play list has not been set yet.");
+ }
+
+ DataSourceDesc oldDsd = mPlaylist.remove(index);
+ // TODO: if index == mPLCurrentIndex, stop current source and move to next one.
+ // if index == mPLNextIndex, prepare the new next-to-be-played source.
+ return oldDsd;
+ }
+
+ /**
+ * Inserts the DataSourceDesc to the play list at position index.
+ *
+ * This will not change the DataSourceDesc currently being played.
+ * If index is less than or equal to the current index of the play list,
+ * the current index of the play list will be incremented correspondingly.
+ *
+ * @param index the index you want to add dsd to the play list
+ * @param dsd the descriptor of data source you want to add to the play list
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ * @throws NullPointerException if dsd is null
+ */
+ @Override
+ public void addPlaylistItem(int index, DataSourceDesc dsd) {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+
+ if (mPlaylist == null) {
+ if (index == 0) {
+ mPlaylist = Collections.synchronizedList(new ArrayList<DataSourceDesc>());
+ mPlaylist.add(dsd);
+ mPLCurrentIndex = 0;
+ return;
+ }
+ throw new IllegalArgumentException("index should be 0 for first DataSourceDesc.");
+ }
+
+ long id = dsd.getId();
+ for (DataSourceDesc pldsd : mPlaylist) {
+ if (id == pldsd.getId()) {
+ throw new IllegalArgumentException("Id of dsd already exists in the play list.");
+ }
+ }
+
+ mPlaylist.add(index, dsd);
+ if (index <= mPLCurrentIndex) {
+ ++mPLCurrentIndex;
+ }
+ }
+
+ /**
+ * replaces the DataSourceDesc at index in the play list with given dsd.
+ *
+ * When index is same as the current index of the play list, the current source
+ * will be stopped and the new source will be played, except that if new
+ * and old source only differ on end position and current media position is
+ * smaller then the new end position.
+ *
+ * This will not change the DataSourceDesc currently being played.
+ * If index is less than or equal to the current index of the play list,
+ * the current index of the play list will be incremented correspondingly.
+ *
+ * @param index the index you want to add dsd to the play list
+ * @param dsd the descriptor of data source you want to add to the play list
+ * @throws IndexOutOfBoundsException if index is outside play list range
+ * @throws NullPointerException if dsd is null
+ */
+ @Override
+ public DataSourceDesc editPlaylistItem(int index, DataSourceDesc dsd) {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+ Preconditions.checkNotNull(mPlaylist, "the play list cannot be null");
+
+ long id = dsd.getId();
+ for (int i = 0; i < mPlaylist.size(); ++i) {
+ if (i == index) {
+ continue;
+ }
+ if (id == mPlaylist.get(i).getId()) {
+ throw new IllegalArgumentException("Id of dsd already exists in the play list.");
+ }
+ }
+
+ // TODO: if needed, stop playback of current source, and start new dsd.
+ DataSourceDesc oldDsd = mPlaylist.set(index, dsd);
+ return mPlaylist.set(index, dsd);
+ }
+
+ private void setDataSourcePriv(@NonNull DataSourceDesc dsd) throws IOException {
+ Preconditions.checkNotNull(dsd, "the DataSourceDesc cannot be null");
+
+ switch (dsd.getType()) {
+ case DataSourceDesc.TYPE_CALLBACK:
+ setDataSourcePriv(dsd.getId(),
+ dsd.getMedia2DataSource());
+ break;
+
+ case DataSourceDesc.TYPE_FD:
+ setDataSourcePriv(dsd.getId(),
+ dsd.getFileDescriptor(),
+ dsd.getFileDescriptorOffset(),
+ dsd.getFileDescriptorLength());
+ break;
+
+ case DataSourceDesc.TYPE_URI:
+ setDataSourcePriv(dsd.getId(),
+ dsd.getUriContext(),
+ dsd.getUri(),
+ dsd.getUriHeaders(),
+ dsd.getUriCookies());
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ /**
+ * To provide cookies for the subsequent HTTP requests, you can install your own default cookie
+ * handler and use other variants of setDataSource APIs instead. Alternatively, you can use
+ * this API to pass the cookies as a list of HttpCookie. If the app has not installed
+ * a CookieHandler already, this API creates a CookieManager and populates its CookieStore with
+ * the provided cookies. If the app has installed its own handler already, this API requires the
+ * handler to be of CookieManager type such that the API can update the manager’s CookieStore.
+ *
+ * <p><strong>Note</strong> that the cross domain redirection is allowed by default,
+ * but that can be changed with key/value pairs through the headers parameter with
+ * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value to
+ * disallow or allow cross domain redirection.
+ *
+ * @throws IllegalArgumentException if cookies are provided and the installed handler is not
+ * a CookieManager
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws NullPointerException if context or uri is null
+ * @throws IOException if uri has a file scheme and an I/O error occurs
+ */
+ private void setDataSourcePriv(long srcId, @NonNull Context context, @NonNull Uri uri,
+ @Nullable Map<String, String> headers, @Nullable List<HttpCookie> cookies)
+ throws IOException {
+ if (context == null) {
+ throw new NullPointerException("context param can not be null.");
+ }
+
+ if (uri == null) {
+ throw new NullPointerException("uri param can not be null.");
+ }
+
+ if (cookies != null) {
+ CookieHandler cookieHandler = CookieHandler.getDefault();
+ if (cookieHandler != null && !(cookieHandler instanceof CookieManager)) {
+ throw new IllegalArgumentException("The cookie handler has to be of CookieManager "
+ + "type when cookies are provided.");
+ }
+ }
+
+ // The context and URI usually belong to the calling user. Get a resolver for that user
+ // and strip out the userId from the URI if present.
+ final ContentResolver resolver = context.getContentResolver();
+ final String scheme = uri.getScheme();
+ final String authority = ContentProvider.getAuthorityWithoutUserId(uri.getAuthority());
+ if (ContentResolver.SCHEME_FILE.equals(scheme)) {
+ setDataSourcePriv(srcId, uri.getPath(), null, null);
+ return;
+ } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)
+ && Settings.AUTHORITY.equals(authority)) {
+ // Try cached ringtone first since the actual provider may not be
+ // encryption aware, or it may be stored on CE media storage
+ final int type = RingtoneManager.getDefaultType(uri);
+ final Uri cacheUri = RingtoneManager.getCacheForType(type, context.getUserId());
+ final Uri actualUri = RingtoneManager.getActualDefaultRingtoneUri(context, type);
+ if (attemptDataSource(srcId, resolver, cacheUri)) {
+ return;
+ } else if (attemptDataSource(srcId, resolver, actualUri)) {
+ return;
+ } else {
+ setDataSourcePriv(srcId, uri.toString(), headers, cookies);
+ }
+ } else {
+ // Try requested Uri locally first, or fallback to media server
+ if (attemptDataSource(srcId, resolver, uri)) {
+ return;
+ } else {
+ setDataSourcePriv(srcId, uri.toString(), headers, cookies);
+ }
+ }
+ }
+
+ private boolean attemptDataSource(long srcId, ContentResolver resolver, Uri uri) {
+ try (AssetFileDescriptor afd = resolver.openAssetFileDescriptor(uri, "r")) {
+ if (afd.getDeclaredLength() < 0) {
+ setDataSourcePriv(srcId, afd.getFileDescriptor(), 0, DataSourceDesc.LONG_MAX);
+ } else {
+ setDataSourcePriv(srcId,
+ afd.getFileDescriptor(),
+ afd.getStartOffset(),
+ afd.getDeclaredLength());
+ }
+ return true;
+ } catch (NullPointerException | SecurityException | IOException ex) {
+ Log.w(TAG, "Couldn't open " + uri + ": " + ex);
+ return false;
+ }
+ }
+
+ private void setDataSourcePriv(
+ long srcId, String path, Map<String, String> headers, List<HttpCookie> cookies)
+ throws IOException, IllegalArgumentException, SecurityException, IllegalStateException
+ {
+ String[] keys = null;
+ String[] values = null;
+
+ if (headers != null) {
+ keys = new String[headers.size()];
+ values = new String[headers.size()];
+
+ int i = 0;
+ for (Map.Entry<String, String> entry: headers.entrySet()) {
+ keys[i] = entry.getKey();
+ values[i] = entry.getValue();
+ ++i;
+ }
+ }
+ setDataSourcePriv(srcId, path, keys, values, cookies);
+ }
+
+ private void setDataSourcePriv(long srcId, String path, String[] keys, String[] values,
+ List<HttpCookie> cookies)
+ throws IOException, IllegalArgumentException, SecurityException, IllegalStateException {
+ final Uri uri = Uri.parse(path);
+ final String scheme = uri.getScheme();
+ if ("file".equals(scheme)) {
+ path = uri.getPath();
+ } else if (scheme != null) {
+ // handle non-file sources
+ nativeSetDataSource(
+ Media2HTTPService.createHTTPService(path, cookies),
+ path,
+ keys,
+ values);
+ return;
+ }
+
+ final File file = new File(path);
+ if (file.exists()) {
+ FileInputStream is = new FileInputStream(file);
+ FileDescriptor fd = is.getFD();
+ setDataSourcePriv(srcId, fd, 0, DataSourceDesc.LONG_MAX);
+ is.close();
+ } else {
+ throw new IOException("setDataSourcePriv failed.");
+ }
+ }
+
+ private native void nativeSetDataSource(
+ Media2HTTPService httpService, String path, String[] keys, String[] values)
+ throws IOException, IllegalArgumentException, SecurityException, IllegalStateException;
+
+ /**
+ * Sets the data source (FileDescriptor) to use. The FileDescriptor must be
+ * seekable (N.B. a LocalSocket is not seekable). It is the caller's responsibility
+ * to close the file descriptor. It is safe to do so as soon as this call returns.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if fd is not a valid FileDescriptor
+ * @throws IOException if fd can not be read
+ */
+ private void setDataSourcePriv(long srcId, FileDescriptor fd, long offset, long length)
+ throws IOException {
+ _setDataSource(fd, offset, length);
+ }
+
+ private native void _setDataSource(FileDescriptor fd, long offset, long length)
+ throws IOException;
+
+ /**
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if dataSource is not a valid Media2DataSource
+ */
+ private void setDataSourcePriv(long srcId, Media2DataSource dataSource) {
+ _setDataSource(dataSource);
+ }
+
+ private native void _setDataSource(Media2DataSource dataSource);
+
+ /**
+ * Prepares the player for playback, synchronously.
+ *
+ * After setting the datasource and the display surface, you need to either
+ * call prepare() or prepareAsync(). For files, it is OK to call prepare(),
+ * which blocks until MediaPlayer2 is ready for playback.
+ *
+ * @throws IOException if source can not be accessed
+ * @throws IllegalStateException if it is called in an invalid state
+ * @hide
+ */
+ @Override
+ public void prepare() throws IOException {
+ _prepare();
+ scanInternalSubtitleTracks();
+
+ // DrmInfo, if any, has been resolved by now.
+ synchronized (mDrmLock) {
+ mDrmInfoResolved = true;
+ }
+ }
+
+ private native void _prepare() throws IOException, IllegalStateException;
+
+ /**
+ * Prepares the player for playback, asynchronously.
+ *
+ * After setting the datasource and the display surface, you need to either
+ * call prepare() or prepareAsync(). For streams, you should call prepareAsync(),
+ * which returns immediately, rather than blocking until enough data has been
+ * buffered.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ @Override
+ public native void prepareAsync();
+
+ /**
+ * Starts or resumes playback. If playback had previously been paused,
+ * playback will continue from where it was paused. If playback had
+ * been stopped, or never started before, playback will start at the
+ * beginning.
+ *
+ * @throws IllegalStateException if it is called in an invalid state
+ */
+ @Override
+ public void play() {
+ stayAwake(true);
+ _start();
+ }
+
+ private native void _start() throws IllegalStateException;
+
+
+ private int getAudioStreamType() {
+ if (mStreamType == AudioManager.USE_DEFAULT_STREAM_TYPE) {
+ mStreamType = _getAudioStreamType();
+ }
+ return mStreamType;
+ }
+
+ private native int _getAudioStreamType() throws IllegalStateException;
+
+ /**
+ * Stops playback after playback has been started or paused.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * #hide
+ */
+ @Override
+ public void stop() {
+ stayAwake(false);
+ _stop();
+ }
+
+ private native void _stop() throws IllegalStateException;
+
+ /**
+ * Pauses playback. Call play() to resume.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @Override
+ public void pause() {
+ stayAwake(false);
+ _pause();
+ }
+
+ private native void _pause() throws IllegalStateException;
+
+ //--------------------------------------------------------------------------
+ // Explicit Routing
+ //--------------------
+ private AudioDeviceInfo mPreferredDevice = null;
+
+ /**
+ * Specifies an audio device (via an {@link AudioDeviceInfo} object) to route
+ * the output from this MediaPlayer2.
+ * @param deviceInfo The {@link AudioDeviceInfo} specifying the audio sink or source.
+ * If deviceInfo is null, default routing is restored.
+ * @return true if succesful, false if the specified {@link AudioDeviceInfo} is non-null and
+ * does not correspond to a valid audio device.
+ */
+ @Override
+ public boolean setPreferredDevice(AudioDeviceInfo deviceInfo) {
+ if (deviceInfo != null && !deviceInfo.isSink()) {
+ return false;
+ }
+ int preferredDeviceId = deviceInfo != null ? deviceInfo.getId() : 0;
+ boolean status = native_setOutputDevice(preferredDeviceId);
+ if (status == true) {
+ synchronized (this) {
+ mPreferredDevice = deviceInfo;
+ }
+ }
+ return status;
+ }
+
+ /**
+ * Returns the selected output specified by {@link #setPreferredDevice}. Note that this
+ * is not guaranteed to correspond to the actual device being used for playback.
+ */
+ @Override
+ public AudioDeviceInfo getPreferredDevice() {
+ synchronized (this) {
+ return mPreferredDevice;
+ }
+ }
+
+ /**
+ * Returns an {@link AudioDeviceInfo} identifying the current routing of this MediaPlayer2
+ * Note: The query is only valid if the MediaPlayer2 is currently playing.
+ * If the player is not playing, the returned device can be null or correspond to previously
+ * selected device when the player was last active.
+ */
+ @Override
+ public AudioDeviceInfo getRoutedDevice() {
+ int deviceId = native_getRoutedDeviceId();
+ if (deviceId == 0) {
+ return null;
+ }
+ AudioDeviceInfo[] devices =
+ AudioManager.getDevicesStatic(AudioManager.GET_DEVICES_OUTPUTS);
+ for (int i = 0; i < devices.length; i++) {
+ if (devices[i].getId() == deviceId) {
+ return devices[i];
+ }
+ }
+ return null;
+ }
+
+ /*
+ * Call BEFORE adding a routing callback handler or AFTER removing a routing callback handler.
+ */
+ private void enableNativeRoutingCallbacksLocked(boolean enabled) {
+ if (mRoutingChangeListeners.size() == 0) {
+ native_enableDeviceCallback(enabled);
+ }
+ }
+
+ /**
+ * The list of AudioRouting.OnRoutingChangedListener interfaces added (with
+ * {@link #addOnRoutingChangedListener(android.media.AudioRouting.OnRoutingChangedListener, Handler)}
+ * by an app to receive (re)routing notifications.
+ */
+ @GuardedBy("mRoutingChangeListeners")
+ private ArrayMap<AudioRouting.OnRoutingChangedListener,
+ NativeRoutingEventHandlerDelegate> mRoutingChangeListeners = new ArrayMap<>();
+
+ /**
+ * Adds an {@link AudioRouting.OnRoutingChangedListener} to receive notifications of routing
+ * changes on this MediaPlayer2.
+ * @param listener The {@link AudioRouting.OnRoutingChangedListener} interface to receive
+ * notifications of rerouting events.
+ * @param handler Specifies the {@link Handler} object for the thread on which to execute
+ * the callback. If <code>null</code>, the handler on the main looper will be used.
+ */
+ @Override
+ public void addOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener,
+ Handler handler) {
+ synchronized (mRoutingChangeListeners) {
+ if (listener != null && !mRoutingChangeListeners.containsKey(listener)) {
+ enableNativeRoutingCallbacksLocked(true);
+ mRoutingChangeListeners.put(
+ listener, new NativeRoutingEventHandlerDelegate(this, listener,
+ handler != null ? handler : mEventHandler));
+ }
+ }
+ }
+
+ /**
+ * Removes an {@link AudioRouting.OnRoutingChangedListener} which has been previously added
+ * to receive rerouting notifications.
+ * @param listener The previously added {@link AudioRouting.OnRoutingChangedListener} interface
+ * to remove.
+ */
+ @Override
+ public void removeOnRoutingChangedListener(AudioRouting.OnRoutingChangedListener listener) {
+ synchronized (mRoutingChangeListeners) {
+ if (mRoutingChangeListeners.containsKey(listener)) {
+ mRoutingChangeListeners.remove(listener);
+ enableNativeRoutingCallbacksLocked(false);
+ }
+ }
+ }
+
+ private native final boolean native_setOutputDevice(int deviceId);
+ private native final int native_getRoutedDeviceId();
+ private native final void native_enableDeviceCallback(boolean enabled);
+
+ /**
+ * Set the low-level power management behavior for this MediaPlayer2. This
+ * can be used when the MediaPlayer2 is not playing through a SurfaceHolder
+ * set with {@link #setDisplay(SurfaceHolder)} and thus can use the
+ * high-level {@link #setScreenOnWhilePlaying(boolean)} feature.
+ *
+ * <p>This function has the MediaPlayer2 access the low-level power manager
+ * service to control the device's power usage while playing is occurring.
+ * The parameter is a combination of {@link android.os.PowerManager} wake flags.
+ * Use of this method requires {@link android.Manifest.permission#WAKE_LOCK}
+ * permission.
+ * By default, no attempt is made to keep the device awake during playback.
+ *
+ * @param context the Context to use
+ * @param mode the power/wake mode to set
+ * @see android.os.PowerManager
+ * @hide
+ */
+ @Override
+ public void setWakeMode(Context context, int mode) {
+ boolean washeld = false;
+
+ /* Disable persistant wakelocks in media player based on property */
+ if (SystemProperties.getBoolean("audio.offload.ignore_setawake", false) == true) {
+ Log.w(TAG, "IGNORING setWakeMode " + mode);
+ return;
+ }
+
+ if (mWakeLock != null) {
+ if (mWakeLock.isHeld()) {
+ washeld = true;
+ mWakeLock.release();
+ }
+ mWakeLock = null;
+ }
+
+ PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
+ mWakeLock = pm.newWakeLock(mode|PowerManager.ON_AFTER_RELEASE, MediaPlayer2Impl.class.getName());
+ mWakeLock.setReferenceCounted(false);
+ if (washeld) {
+ mWakeLock.acquire();
+ }
+ }
+
+ /**
+ * Control whether we should use the attached SurfaceHolder to keep the
+ * screen on while video playback is occurring. This is the preferred
+ * method over {@link #setWakeMode} where possible, since it doesn't
+ * require that the application have permission for low-level wake lock
+ * access.
+ *
+ * @param screenOn Supply true to keep the screen on, false to allow it
+ * to turn off.
+ * @hide
+ */
+ @Override
+ public void setScreenOnWhilePlaying(boolean screenOn) {
+ if (mScreenOnWhilePlaying != screenOn) {
+ if (screenOn && mSurfaceHolder == null) {
+ Log.w(TAG, "setScreenOnWhilePlaying(true) is ineffective without a SurfaceHolder");
+ }
+ mScreenOnWhilePlaying = screenOn;
+ updateSurfaceScreenOn();
+ }
+ }
+
+ private void stayAwake(boolean awake) {
+ if (mWakeLock != null) {
+ if (awake && !mWakeLock.isHeld()) {
+ mWakeLock.acquire();
+ } else if (!awake && mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
+ }
+ mStayAwake = awake;
+ updateSurfaceScreenOn();
+ }
+
+ private void updateSurfaceScreenOn() {
+ if (mSurfaceHolder != null) {
+ mSurfaceHolder.setKeepScreenOn(mScreenOnWhilePlaying && mStayAwake);
+ }
+ }
+
+ /**
+ * Returns the width of the video.
+ *
+ * @return the width of the video, or 0 if there is no video,
+ * no display surface was set, or the width has not been determined
+ * yet. The {@code EventCallback} can be registered via
+ * {@link #registerEventCallback(Executor, EventCallback)} to provide a
+ * notification {@code EventCallback.onVideoSizeChanged} when the width is available.
+ */
+ @Override
+ public native int getVideoWidth();
+
+ /**
+ * Returns the height of the video.
+ *
+ * @return the height of the video, or 0 if there is no video,
+ * no display surface was set, or the height has not been determined
+ * yet. The {@code EventCallback} can be registered via
+ * {@link #registerEventCallback(Executor, EventCallback)} to provide a
+ * notification {@code EventCallback.onVideoSizeChanged} when the height is available.
+ */
+ @Override
+ public native int getVideoHeight();
+
+ /**
+ * Return Metrics data about the current player.
+ *
+ * @return a {@link PersistableBundle} containing the set of attributes and values
+ * available for the media being handled by this instance of MediaPlayer2
+ * The attributes are descibed in {@link MetricsConstants}.
+ *
+ * Additional vendor-specific fields may also be present in
+ * the return value.
+ */
+ @Override
+ public PersistableBundle getMetrics() {
+ PersistableBundle bundle = native_getMetrics();
+ return bundle;
+ }
+
+ private native PersistableBundle native_getMetrics();
+
+ /**
+ * Checks whether the MediaPlayer2 is playing.
+ *
+ * @return true if currently playing, false otherwise
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ */
+ @Override
+ public native boolean isPlaying();
+
+ /**
+ * Gets the current buffering management params used by the source component.
+ * Calling it only after {@code setDataSource} has been called.
+ * Each type of data source might have different set of default params.
+ *
+ * @return the current buffering management params used by the source component.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized, or {@code setDataSource} has not been called.
+ * @hide
+ */
+ @Override
+ @NonNull
+ public native BufferingParams getBufferingParams();
+
+ /**
+ * Sets buffering management params.
+ * The object sets its internal BufferingParams to the input, except that the input is
+ * invalid or not supported.
+ * Call it only after {@code setDataSource} has been called.
+ * The input is a hint to MediaPlayer2.
+ *
+ * @param params the buffering management params.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released, or {@code setDataSource} has not been called.
+ * @throws IllegalArgumentException if params is invalid or not supported.
+ * @hide
+ */
+ @Override
+ public native void setBufferingParams(@NonNull BufferingParams params);
+
+ /**
+ * Sets playback rate and audio mode.
+ *
+ * @param rate the ratio between desired playback rate and normal one.
+ * @param audioMode audio playback mode. Must be one of the supported
+ * audio modes.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * @throws IllegalArgumentException if audioMode is not supported.
+ *
+ * @hide
+ */
+ @Override
+ @NonNull
+ public PlaybackParams easyPlaybackParams(float rate, @PlaybackRateAudioMode int audioMode) {
+ PlaybackParams params = new PlaybackParams();
+ params.allowDefaults();
+ switch (audioMode) {
+ case PLAYBACK_RATE_AUDIO_MODE_DEFAULT:
+ params.setSpeed(rate).setPitch(1.0f);
+ break;
+ case PLAYBACK_RATE_AUDIO_MODE_STRETCH:
+ params.setSpeed(rate).setPitch(1.0f)
+ .setAudioFallbackMode(params.AUDIO_FALLBACK_MODE_FAIL);
+ break;
+ case PLAYBACK_RATE_AUDIO_MODE_RESAMPLE:
+ params.setSpeed(rate).setPitch(rate);
+ break;
+ default:
+ final String msg = "Audio playback mode " + audioMode + " is not supported";
+ throw new IllegalArgumentException(msg);
+ }
+ return params;
+ }
+
+ /**
+ * Sets playback rate using {@link PlaybackParams}. The object sets its internal
+ * PlaybackParams to the input, except that the object remembers previous speed
+ * when input speed is zero. This allows the object to resume at previous speed
+ * when play() is called. Calling it before the object is prepared does not change
+ * the object state. After the object is prepared, calling it with zero speed is
+ * equivalent to calling pause(). After the object is prepared, calling it with
+ * non-zero speed is equivalent to calling play().
+ *
+ * @param params the playback params.
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized or has been released.
+ * @throws IllegalArgumentException if params is not supported.
+ */
+ @Override
+ public native void setPlaybackParams(@NonNull PlaybackParams params);
+
+ /**
+ * Gets the playback params, containing the current playback rate.
+ *
+ * @return the playback params.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @Override
+ @NonNull
+ public native PlaybackParams getPlaybackParams();
+
+ /**
+ * Sets A/V sync mode.
+ *
+ * @param params the A/V sync params to apply
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ * @throws IllegalArgumentException if params are not supported.
+ */
+ @Override
+ public native void setSyncParams(@NonNull SyncParams params);
+
+ /**
+ * Gets the A/V sync mode.
+ *
+ * @return the A/V sync params
+ *
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized.
+ */
+ @Override
+ @NonNull
+ public native SyncParams getSyncParams();
+
+ private native final void _seekTo(long msec, int mode);
+
+ /**
+ * Moves the media to specified time position by considering the given mode.
+ * <p>
+ * When seekTo is finished, the user will be notified via OnSeekComplete supplied by the user.
+ * There is at most one active seekTo processed at any time. If there is a to-be-completed
+ * seekTo, new seekTo requests will be queued in such a way that only the last request
+ * is kept. When current seekTo is completed, the queued request will be processed if
+ * that request is different from just-finished seekTo operation, i.e., the requested
+ * position or mode is different.
+ *
+ * @param msec the offset in milliseconds from the start to seek to.
+ * When seeking to the given time position, there is no guarantee that the data source
+ * has a frame located at the position. When this happens, a frame nearby will be rendered.
+ * If msec is negative, time position zero will be used.
+ * If msec is larger than duration, duration will be used.
+ * @param mode the mode indicating where exactly to seek to.
+ * Use {@link #SEEK_PREVIOUS_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp earlier than or the same as msec. Use
+ * {@link #SEEK_NEXT_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp later than or the same as msec. Use
+ * {@link #SEEK_CLOSEST_SYNC} if one wants to seek to a sync frame
+ * that has a timestamp closest to or the same as msec. Use
+ * {@link #SEEK_CLOSEST} if one wants to seek to a frame that may
+ * or may not be a sync frame but is closest to or the same as msec.
+ * {@link #SEEK_CLOSEST} often has larger performance overhead compared
+ * to the other options if there is no sync frame located at msec.
+ * @throws IllegalStateException if the internal player engine has not been
+ * initialized
+ * @throws IllegalArgumentException if the mode is invalid.
+ */
+ @Override
+ public void seekTo(long msec, @SeekMode int mode) {
+ if (mode < SEEK_PREVIOUS_SYNC || mode > SEEK_CLOSEST) {
+ final String msg = "Illegal seek mode: " + mode;
+ throw new IllegalArgumentException(msg);
+ }
+ // TODO: pass long to native, instead of truncating here.
+ if (msec > Integer.MAX_VALUE) {
+ Log.w(TAG, "seekTo offset " + msec + " is too large, cap to " + Integer.MAX_VALUE);
+ msec = Integer.MAX_VALUE;
+ } else if (msec < Integer.MIN_VALUE) {
+ Log.w(TAG, "seekTo offset " + msec + " is too small, cap to " + Integer.MIN_VALUE);
+ msec = Integer.MIN_VALUE;
+ }
+ _seekTo(msec, mode);
+ }
+
+ /**
+ * Get current playback position as a {@link MediaTimestamp}.
+ * <p>
+ * The MediaTimestamp represents how the media time correlates to the system time in
+ * a linear fashion using an anchor and a clock rate. During regular playback, the media
+ * time moves fairly constantly (though the anchor frame may be rebased to a current
+ * system time, the linear correlation stays steady). Therefore, this method does not
+ * need to be called often.
+ * <p>
+ * To help users get current playback position, this method always anchors the timestamp
+ * to the current {@link System#nanoTime system time}, so
+ * {@link MediaTimestamp#getAnchorMediaTimeUs} can be used as current playback position.
+ *
+ * @return a MediaTimestamp object if a timestamp is available, or {@code null} if no timestamp
+ * is available, e.g. because the media player has not been initialized.
+ *
+ * @see MediaTimestamp
+ */
+ @Override
+ @Nullable
+ public MediaTimestamp getTimestamp()
+ {
+ try {
+ // TODO: get the timestamp from native side
+ return new MediaTimestamp(
+ getCurrentPosition() * 1000L,
+ System.nanoTime(),
+ isPlaying() ? getPlaybackParams().getSpeed() : 0.f);
+ } catch (IllegalStateException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Gets the current playback position.
+ *
+ * @return the current position in milliseconds
+ */
+ @Override
+ public native int getCurrentPosition();
+
+ /**
+ * Gets the duration of the file.
+ *
+ * @return the duration in milliseconds, if no duration is available
+ * (for example, if streaming live content), -1 is returned.
+ */
+ @Override
+ public native int getDuration();
+
+ /**
+ * Gets the media metadata.
+ *
+ * @param update_only controls whether the full set of available
+ * metadata is returned or just the set that changed since the
+ * last call. See {@see #METADATA_UPDATE_ONLY} and {@see
+ * #METADATA_ALL}.
+ *
+ * @param apply_filter if true only metadata that matches the
+ * filter is returned. See {@see #APPLY_METADATA_FILTER} and {@see
+ * #BYPASS_METADATA_FILTER}.
+ *
+ * @return The metadata, possibly empty. null if an error occured.
+ // FIXME: unhide.
+ * {@hide}
+ */
+ @Override
+ public Metadata getMetadata(final boolean update_only,
+ final boolean apply_filter) {
+ Parcel reply = Parcel.obtain();
+ Metadata data = new Metadata();
+
+ if (!native_getMetadata(update_only, apply_filter, reply)) {
+ reply.recycle();
+ return null;
+ }
+
+ // Metadata takes over the parcel, don't recycle it unless
+ // there is an error.
+ if (!data.parse(reply)) {
+ reply.recycle();
+ return null;
+ }
+ return data;
+ }
+
+ /**
+ * Set a filter for the metadata update notification and update
+ * retrieval. The caller provides 2 set of metadata keys, allowed
+ * and blocked. The blocked set always takes precedence over the
+ * allowed one.
+ * Metadata.MATCH_ALL and Metadata.MATCH_NONE are 2 sets available as
+ * shorthands to allow/block all or no metadata.
+ *
+ * By default, there is no filter set.
+ *
+ * @param allow Is the set of metadata the client is interested
+ * in receiving new notifications for.
+ * @param block Is the set of metadata the client is not interested
+ * in receiving new notifications for.
+ * @return The call status code.
+ *
+ // FIXME: unhide.
+ * {@hide}
+ */
+ @Override
+ public int setMetadataFilter(Set<Integer> allow, Set<Integer> block) {
+ // Do our serialization manually instead of calling
+ // Parcel.writeArray since the sets are made of the same type
+ // we avoid paying the price of calling writeValue (used by
+ // writeArray) which burns an extra int per element to encode
+ // the type.
+ Parcel request = newRequest();
+
+ // The parcel starts already with an interface token. There
+ // are 2 filters. Each one starts with a 4bytes number to
+ // store the len followed by a number of int (4 bytes as well)
+ // representing the metadata type.
+ int capacity = request.dataSize() + 4 * (1 + allow.size() + 1 + block.size());
+
+ if (request.dataCapacity() < capacity) {
+ request.setDataCapacity(capacity);
+ }
+
+ request.writeInt(allow.size());
+ for(Integer t: allow) {
+ request.writeInt(t);
+ }
+ request.writeInt(block.size());
+ for(Integer t: block) {
+ request.writeInt(t);
+ }
+ return native_setMetadataFilter(request);
+ }
+
+ /**
+ * Set the MediaPlayer2 to start when this MediaPlayer2 finishes playback
+ * (i.e. reaches the end of the stream).
+ * The media framework will attempt to transition from this player to
+ * the next as seamlessly as possible. The next player can be set at
+ * any time before completion, but shall be after setDataSource has been
+ * called successfully. The next player must be prepared by the
+ * app, and the application should not call play() on it.
+ * The next MediaPlayer2 must be different from 'this'. An exception
+ * will be thrown if next == this.
+ * The application may call setNextMediaPlayer(null) to indicate no
+ * next player should be started at the end of playback.
+ * If the current player is looping, it will keep looping and the next
+ * player will not be started.
+ *
+ * @param next the player to start after this one completes playback.
+ *
+ * @hide
+ */
+ @Override
+ public native void setNextMediaPlayer(MediaPlayer2 next);
+
+ /**
+ * Resets the MediaPlayer2 to its uninitialized state. After calling
+ * this method, you will have to initialize it again by setting the
+ * data source and calling prepare().
+ */
+ @Override
+ public void reset() {
+ mSelectedSubtitleTrackIndex = -1;
+ synchronized(mOpenSubtitleSources) {
+ for (final InputStream is: mOpenSubtitleSources) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ }
+ }
+ mOpenSubtitleSources.clear();
+ }
+ if (mSubtitleController != null) {
+ mSubtitleController.reset();
+ }
+ if (mTimeProvider != null) {
+ mTimeProvider.close();
+ mTimeProvider = null;
+ }
+
+ stayAwake(false);
+ _reset();
+ // make sure none of the listeners get called anymore
+ if (mEventHandler != null) {
+ mEventHandler.removeCallbacksAndMessages(null);
+ }
+
+ synchronized (mIndexTrackPairs) {
+ mIndexTrackPairs.clear();
+ mInbandTrackIndices.clear();
+ };
+
+ resetDrmState();
+ }
+
+ private native void _reset();
+
+ /**
+ * Set up a timer for {@link #TimeProvider}. {@link #TimeProvider} will be
+ * notified when the presentation time reaches (becomes greater than or equal to)
+ * the value specified.
+ *
+ * @param mediaTimeUs presentation time to get timed event callback at
+ * @hide
+ */
+ @Override
+ public void notifyAt(long mediaTimeUs) {
+ _notifyAt(mediaTimeUs);
+ }
+
+ private native void _notifyAt(long mediaTimeUs);
+
+ // Keep KEY_PARAMETER_* in sync with include/media/mediaplayer2.h
+ private final static int KEY_PARAMETER_AUDIO_ATTRIBUTES = 1400;
+ /**
+ * Sets the parameter indicated by key.
+ * @param key key indicates the parameter to be set.
+ * @param value value of the parameter to be set.
+ * @return true if the parameter is set successfully, false otherwise
+ * {@hide}
+ */
+ private native boolean setParameter(int key, Parcel value);
+
+ /**
+ * Sets the audio attributes for this MediaPlayer2.
+ * See {@link AudioAttributes} for how to build and configure an instance of this class.
+ * You must call this method before {@link #prepare()} or {@link #prepareAsync()} in order
+ * for the audio attributes to become effective thereafter.
+ * @param attributes a non-null set of audio attributes
+ * @throws IllegalArgumentException if the attributes are null or invalid.
+ */
+ @Override
+ public void setAudioAttributes(AudioAttributes attributes) {
+ if (attributes == null) {
+ final String msg = "Cannot set AudioAttributes to null";
+ throw new IllegalArgumentException(msg);
+ }
+ mUsage = attributes.getUsage();
+ mBypassInterruptionPolicy = (attributes.getAllFlags()
+ & AudioAttributes.FLAG_BYPASS_INTERRUPTION_POLICY) != 0;
+ Parcel pattributes = Parcel.obtain();
+ attributes.writeToParcel(pattributes, AudioAttributes.FLATTEN_TAGS);
+ setParameter(KEY_PARAMETER_AUDIO_ATTRIBUTES, pattributes);
+ pattributes.recycle();
+ }
+
+ /**
+ * Sets the player to be looping or non-looping.
+ *
+ * @param looping whether to loop or not
+ * @hide
+ */
+ @Override
+ public native void setLooping(boolean looping);
+
+ /**
+ * Checks whether the MediaPlayer2 is looping or non-looping.
+ *
+ * @return true if the MediaPlayer2 is currently looping, false otherwise
+ * @hide
+ */
+ @Override
+ public native boolean isLooping();
+
+ /**
+ * Sets the volume on this player.
+ * This API is recommended for balancing the output of audio streams
+ * within an application. Unless you are writing an application to
+ * control user settings, this API should be used in preference to
+ * {@link AudioManager#setStreamVolume(int, int, int)} which sets the volume of ALL streams of
+ * a particular type. Note that the passed volume values are raw scalars in range 0.0 to 1.0.
+ * UI controls should be scaled logarithmically.
+ *
+ * @param leftVolume left volume scalar
+ * @param rightVolume right volume scalar
+ */
+ /*
+ * FIXME: Merge this into javadoc comment above when setVolume(float) is not @hide.
+ * The single parameter form below is preferred if the channel volumes don't need
+ * to be set independently.
+ */
+ @Override
+ public void setVolume(float leftVolume, float rightVolume) {
+ _setVolume(leftVolume, rightVolume);
+ }
+
+ private native void _setVolume(float leftVolume, float rightVolume);
+
+ /**
+ * Similar, excepts sets volume of all channels to same value.
+ * @hide
+ */
+ @Override
+ public void setVolume(float volume) {
+ setVolume(volume, volume);
+ }
+
+ /**
+ * Sets the audio session ID.
+ *
+ * @param sessionId the audio session ID.
+ * The audio session ID is a system wide unique identifier for the audio stream played by
+ * this MediaPlayer2 instance.
+ * The primary use of the audio session ID is to associate audio effects to a particular
+ * instance of MediaPlayer2: if an audio session ID is provided when creating an audio effect,
+ * this effect will be applied only to the audio content of media players within the same
+ * audio session and not to the output mix.
+ * When created, a MediaPlayer2 instance automatically generates its own audio session ID.
+ * However, it is possible to force this player to be part of an already existing audio session
+ * by calling this method.
+ * This method must be called before one of the overloaded <code> setDataSource </code> methods.
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if the sessionId is invalid.
+ */
+ @Override
+ public native void setAudioSessionId(int sessionId);
+
+ /**
+ * Returns the audio session ID.
+ *
+ * @return the audio session ID. {@see #setAudioSessionId(int)}
+ * Note that the audio session ID is 0 only if a problem occured when the MediaPlayer2 was contructed.
+ */
+ @Override
+ public native int getAudioSessionId();
+
+ /**
+ * Attaches an auxiliary effect to the player. A typical auxiliary effect is a reverberation
+ * effect which can be applied on any sound source that directs a certain amount of its
+ * energy to this effect. This amount is defined by setAuxEffectSendLevel().
+ * See {@link #setAuxEffectSendLevel(float)}.
+ * <p>After creating an auxiliary effect (e.g.
+ * {@link android.media.audiofx.EnvironmentalReverb}), retrieve its ID with
+ * {@link android.media.audiofx.AudioEffect#getId()} and use it when calling this method
+ * to attach the player to the effect.
+ * <p>To detach the effect from the player, call this method with a null effect id.
+ * <p>This method must be called after one of the overloaded <code> setDataSource </code>
+ * methods.
+ * @param effectId system wide unique id of the effect to attach
+ */
+ @Override
+ public native void attachAuxEffect(int effectId);
+
+
+ /**
+ * Sets the send level of the player to the attached auxiliary effect.
+ * See {@link #attachAuxEffect(int)}. The level value range is 0 to 1.0.
+ * <p>By default the send level is 0, so even if an effect is attached to the player
+ * this method must be called for the effect to be applied.
+ * <p>Note that the passed level value is a raw scalar. UI controls should be scaled
+ * logarithmically: the gain applied by audio framework ranges from -72dB to 0dB,
+ * so an appropriate conversion from linear UI input x to level is:
+ * x == 0 -> level = 0
+ * 0 < x <= R -> level = 10^(72*(x-R)/20/R)
+ * @param level send level scalar
+ */
+ @Override
+ public void setAuxEffectSendLevel(float level) {
+ _setAuxEffectSendLevel(level);
+ }
+
+ private native void _setAuxEffectSendLevel(float level);
+
+ /*
+ * @param request Parcel destinated to the media player.
+ * @param reply[out] Parcel that will contain the reply.
+ * @return The status code.
+ */
+ private native final int native_invoke(Parcel request, Parcel reply);
+
+
+ /*
+ * @param update_only If true fetch only the set of metadata that have
+ * changed since the last invocation of getMetadata.
+ * The set is built using the unfiltered
+ * notifications the native player sent to the
+ * MediaPlayer2Manager during that period of
+ * time. If false, all the metadatas are considered.
+ * @param apply_filter If true, once the metadata set has been built based on
+ * the value update_only, the current filter is applied.
+ * @param reply[out] On return contains the serialized
+ * metadata. Valid only if the call was successful.
+ * @return The status code.
+ */
+ private native final boolean native_getMetadata(boolean update_only,
+ boolean apply_filter,
+ Parcel reply);
+
+ /*
+ * @param request Parcel with the 2 serialized lists of allowed
+ * metadata types followed by the one to be
+ * dropped. Each list starts with an integer
+ * indicating the number of metadata type elements.
+ * @return The status code.
+ */
+ private native final int native_setMetadataFilter(Parcel request);
+
+ private static native final void native_init();
+ private native final void native_setup(Object mediaplayer2_this);
+ private native final void native_finalize();
+
+ /**
+ * Class for MediaPlayer2 to return each audio/video/subtitle track's metadata.
+ *
+ * @see android.media.MediaPlayer2#getTrackInfo
+ */
+ public static final class TrackInfoImpl extends TrackInfo {
+ /**
+ * Gets the track type.
+ * @return TrackType which indicates if the track is video, audio, timed text.
+ */
+ @Override
+ public int getTrackType() {
+ return mTrackType;
+ }
+
+ /**
+ * Gets the language code of the track.
+ * @return a language code in either way of ISO-639-1 or ISO-639-2.
+ * When the language is unknown or could not be determined,
+ * ISO-639-2 language code, "und", is returned.
+ */
+ @Override
+ public String getLanguage() {
+ String language = mFormat.getString(MediaFormat.KEY_LANGUAGE);
+ return language == null ? "und" : language;
+ }
+
+ /**
+ * Gets the {@link MediaFormat} of the track. If the format is
+ * unknown or could not be determined, null is returned.
+ */
+ @Override
+ public MediaFormat getFormat() {
+ if (mTrackType == MEDIA_TRACK_TYPE_TIMEDTEXT
+ || mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+ return mFormat;
+ }
+ return null;
+ }
+
+ final int mTrackType;
+ final MediaFormat mFormat;
+
+ TrackInfoImpl(Parcel in) {
+ mTrackType = in.readInt();
+ // TODO: parcel in the full MediaFormat; currently we are using createSubtitleFormat
+ // even for audio/video tracks, meaning we only set the mime and language.
+ String mime = in.readString();
+ String language = in.readString();
+ mFormat = MediaFormat.createSubtitleFormat(mime, language);
+
+ if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+ mFormat.setInteger(MediaFormat.KEY_IS_AUTOSELECT, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_DEFAULT, in.readInt());
+ mFormat.setInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE, in.readInt());
+ }
+ }
+
+ /** @hide */
+ TrackInfoImpl(int type, MediaFormat format) {
+ mTrackType = type;
+ mFormat = format;
+ }
+
+ /**
+ * Flatten this object in to a Parcel.
+ *
+ * @param dest The Parcel in which the object should be written.
+ * @param flags Additional flags about how the object should be written.
+ * May be 0 or {@link android.os.Parcelable#PARCELABLE_WRITE_RETURN_VALUE}.
+ */
+ /* package private */ void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mTrackType);
+ dest.writeString(getLanguage());
+
+ if (mTrackType == MEDIA_TRACK_TYPE_SUBTITLE) {
+ dest.writeString(mFormat.getString(MediaFormat.KEY_MIME));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_AUTOSELECT));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_DEFAULT));
+ dest.writeInt(mFormat.getInteger(MediaFormat.KEY_IS_FORCED_SUBTITLE));
+ }
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder out = new StringBuilder(128);
+ out.append(getClass().getName());
+ out.append('{');
+ switch (mTrackType) {
+ case MEDIA_TRACK_TYPE_VIDEO:
+ out.append("VIDEO");
+ break;
+ case MEDIA_TRACK_TYPE_AUDIO:
+ out.append("AUDIO");
+ break;
+ case MEDIA_TRACK_TYPE_TIMEDTEXT:
+ out.append("TIMEDTEXT");
+ break;
+ case MEDIA_TRACK_TYPE_SUBTITLE:
+ out.append("SUBTITLE");
+ break;
+ default:
+ out.append("UNKNOWN");
+ break;
+ }
+ out.append(", " + mFormat.toString());
+ out.append("}");
+ return out.toString();
+ }
+
+ /**
+ * Used to read a TrackInfoImpl from a Parcel.
+ */
+ /* package private */ static final Parcelable.Creator<TrackInfoImpl> CREATOR
+ = new Parcelable.Creator<TrackInfoImpl>() {
+ @Override
+ public TrackInfoImpl createFromParcel(Parcel in) {
+ return new TrackInfoImpl(in);
+ }
+
+ @Override
+ public TrackInfoImpl[] newArray(int size) {
+ return new TrackInfoImpl[size];
+ }
+ };
+
+ };
+
+ // We would like domain specific classes with more informative names than the `first` and `second`
+ // in generic Pair, but we would also like to avoid creating new/trivial classes. As a compromise
+ // we document the meanings of `first` and `second` here:
+ //
+ // Pair.first - inband track index; non-null iff representing an inband track.
+ // Pair.second - a SubtitleTrack registered with mSubtitleController; non-null iff representing
+ // an inband subtitle track or any out-of-band track (subtitle or timedtext).
+ private Vector<Pair<Integer, SubtitleTrack>> mIndexTrackPairs = new Vector<>();
+ private BitSet mInbandTrackIndices = new BitSet();
+
+ /**
+ * Returns a List of track information.
+ *
+ * @return List of track info. The total number of tracks is the array length.
+ * Must be called again if an external timed text source has been added after
+ * addTimedTextSource method is called.
+ * @throws IllegalStateException if it is called in an invalid state.
+ */
+ @Override
+ public List<TrackInfo> getTrackInfo() {
+ TrackInfoImpl trackInfo[] = getInbandTrackInfoImpl();
+ // add out-of-band tracks
+ synchronized (mIndexTrackPairs) {
+ TrackInfoImpl allTrackInfo[] = new TrackInfoImpl[mIndexTrackPairs.size()];
+ for (int i = 0; i < allTrackInfo.length; i++) {
+ Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i);
+ if (p.first != null) {
+ // inband track
+ allTrackInfo[i] = trackInfo[p.first];
+ } else {
+ SubtitleTrack track = p.second;
+ allTrackInfo[i] = new TrackInfoImpl(track.getTrackType(), track.getFormat());
+ }
+ }
+ return Arrays.asList(allTrackInfo);
+ }
+ }
+
+ private TrackInfoImpl[] getInbandTrackInfoImpl() throws IllegalStateException {
+ Parcel request = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ try {
+ request.writeInt(INVOKE_ID_GET_TRACK_INFO);
+ invoke(request, reply);
+ TrackInfoImpl trackInfo[] = reply.createTypedArray(TrackInfoImpl.CREATOR);
+ return trackInfo;
+ } finally {
+ request.recycle();
+ reply.recycle();
+ }
+ }
+
+ /*
+ * A helper function to check if the mime type is supported by media framework.
+ */
+ private static boolean availableMimeTypeForExternalSource(String mimeType) {
+ if (MEDIA_MIMETYPE_TEXT_SUBRIP.equals(mimeType)) {
+ return true;
+ }
+ return false;
+ }
+
+ private SubtitleController mSubtitleController;
+
+ /** @hide */
+ @Override
+ public void setSubtitleAnchor(
+ SubtitleController controller,
+ SubtitleController.Anchor anchor) {
+ // TODO: create SubtitleController in MediaPlayer2
+ mSubtitleController = controller;
+ mSubtitleController.setAnchor(anchor);
+ }
+
+ /**
+ * The private version of setSubtitleAnchor is used internally to set mSubtitleController if
+ * necessary when clients don't provide their own SubtitleControllers using the public version
+ * {@link #setSubtitleAnchor(SubtitleController, Anchor)} (e.g. {@link VideoView} provides one).
+ */
+ private synchronized void setSubtitleAnchor() {
+ if ((mSubtitleController == null) && (ActivityThread.currentApplication() != null)) {
+ final HandlerThread thread = new HandlerThread("SetSubtitleAnchorThread");
+ thread.start();
+ Handler handler = new Handler(thread.getLooper());
+ handler.post(new Runnable() {
+ @Override
+ public void run() {
+ Context context = ActivityThread.currentApplication();
+ mSubtitleController = new SubtitleController(context, mTimeProvider, MediaPlayer2Impl.this);
+ mSubtitleController.setAnchor(new Anchor() {
+ @Override
+ public void setSubtitleWidget(RenderingWidget subtitleWidget) {
+ }
+
+ @Override
+ public Looper getSubtitleLooper() {
+ return Looper.getMainLooper();
+ }
+ });
+ thread.getLooper().quitSafely();
+ }
+ });
+ try {
+ thread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ Log.w(TAG, "failed to join SetSubtitleAnchorThread");
+ }
+ }
+ }
+
+ private int mSelectedSubtitleTrackIndex = -1;
+ private Vector<InputStream> mOpenSubtitleSources;
+
+ private OnSubtitleDataListener mSubtitleDataListener = new OnSubtitleDataListener() {
+ @Override
+ public void onSubtitleData(MediaPlayer2 mp, SubtitleData data) {
+ int index = data.getTrackIndex();
+ synchronized (mIndexTrackPairs) {
+ for (Pair<Integer, SubtitleTrack> p : mIndexTrackPairs) {
+ if (p.first != null && p.first == index && p.second != null) {
+ // inband subtitle track that owns data
+ SubtitleTrack track = p.second;
+ track.onData(data);
+ }
+ }
+ }
+ }
+ };
+
+ /** @hide */
+ @Override
+ public void onSubtitleTrackSelected(SubtitleTrack track) {
+ if (mSelectedSubtitleTrackIndex >= 0) {
+ try {
+ selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, false);
+ } catch (IllegalStateException e) {
+ }
+ mSelectedSubtitleTrackIndex = -1;
+ }
+ setOnSubtitleDataListener(null);
+ if (track == null) {
+ return;
+ }
+
+ synchronized (mIndexTrackPairs) {
+ for (Pair<Integer, SubtitleTrack> p : mIndexTrackPairs) {
+ if (p.first != null && p.second == track) {
+ // inband subtitle track that is selected
+ mSelectedSubtitleTrackIndex = p.first;
+ break;
+ }
+ }
+ }
+
+ if (mSelectedSubtitleTrackIndex >= 0) {
+ try {
+ selectOrDeselectInbandTrack(mSelectedSubtitleTrackIndex, true);
+ } catch (IllegalStateException e) {
+ }
+ setOnSubtitleDataListener(mSubtitleDataListener);
+ }
+ // no need to select out-of-band tracks
+ }
+
+ /** @hide */
+ @Override
+ public void addSubtitleSource(InputStream is, MediaFormat format)
+ throws IllegalStateException
+ {
+ final InputStream fIs = is;
+ final MediaFormat fFormat = format;
+
+ if (is != null) {
+ // Ensure all input streams are closed. It is also a handy
+ // way to implement timeouts in the future.
+ synchronized(mOpenSubtitleSources) {
+ mOpenSubtitleSources.add(is);
+ }
+ } else {
+ Log.w(TAG, "addSubtitleSource called with null InputStream");
+ }
+
+ getMediaTimeProvider();
+
+ // process each subtitle in its own thread
+ final HandlerThread thread = new HandlerThread("SubtitleReadThread",
+ Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
+ thread.start();
+ Handler handler = new Handler(thread.getLooper());
+ handler.post(new Runnable() {
+ private int addTrack() {
+ if (fIs == null || mSubtitleController == null) {
+ return MEDIA_INFO_UNSUPPORTED_SUBTITLE;
+ }
+
+ SubtitleTrack track = mSubtitleController.addTrack(fFormat);
+ if (track == null) {
+ return MEDIA_INFO_UNSUPPORTED_SUBTITLE;
+ }
+
+ // TODO: do the conversion in the subtitle track
+ Scanner scanner = new Scanner(fIs, "UTF-8");
+ String contents = scanner.useDelimiter("\\A").next();
+ synchronized(mOpenSubtitleSources) {
+ mOpenSubtitleSources.remove(fIs);
+ }
+ scanner.close();
+ synchronized (mIndexTrackPairs) {
+ mIndexTrackPairs.add(Pair.<Integer, SubtitleTrack>create(null, track));
+ }
+ Handler h = mTimeProvider.mEventHandler;
+ int what = TimeProvider.NOTIFY;
+ int arg1 = TimeProvider.NOTIFY_TRACK_DATA;
+ Pair<SubtitleTrack, byte[]> trackData = Pair.create(track, contents.getBytes());
+ Message m = h.obtainMessage(what, arg1, 0, trackData);
+ h.sendMessage(m);
+ return MEDIA_INFO_EXTERNAL_METADATA_UPDATE;
+ }
+
+ public void run() {
+ int res = addTrack();
+ if (mEventHandler != null) {
+ Message m = mEventHandler.obtainMessage(MEDIA_INFO, res, 0, null);
+ mEventHandler.sendMessage(m);
+ }
+ thread.getLooper().quitSafely();
+ }
+ });
+ }
+
+ private void scanInternalSubtitleTracks() {
+ setSubtitleAnchor();
+
+ populateInbandTracks();
+
+ if (mSubtitleController != null) {
+ mSubtitleController.selectDefaultTrack();
+ }
+ }
+
+ private void populateInbandTracks() {
+ TrackInfoImpl[] tracks = getInbandTrackInfoImpl();
+ synchronized (mIndexTrackPairs) {
+ for (int i = 0; i < tracks.length; i++) {
+ if (mInbandTrackIndices.get(i)) {
+ continue;
+ } else {
+ mInbandTrackIndices.set(i);
+ }
+
+ // newly appeared inband track
+ if (tracks[i].getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+ SubtitleTrack track = mSubtitleController.addTrack(
+ tracks[i].getFormat());
+ mIndexTrackPairs.add(Pair.create(i, track));
+ } else {
+ mIndexTrackPairs.add(Pair.<Integer, SubtitleTrack>create(i, null));
+ }
+ }
+ }
+ }
+
+ /* TODO: Limit the total number of external timed text source to a reasonable number.
+ */
+ /**
+ * Adds an external timed text source file.
+ *
+ * Currently supported format is SubRip with the file extension .srt, case insensitive.
+ * Note that a single external timed text source may contain multiple tracks in it.
+ * One can find the total number of available tracks using {@link #getTrackInfo()} to see what
+ * additional tracks become available after this method call.
+ *
+ * @param path The file path of external timed text source file.
+ * @param mimeType The mime type of the file. Must be one of the mime types listed above.
+ * @throws IOException if the file cannot be accessed or is corrupted.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ @Override
+ public void addTimedTextSource(String path, String mimeType)
+ throws IOException {
+ if (!availableMimeTypeForExternalSource(mimeType)) {
+ final String msg = "Illegal mimeType for timed text source: " + mimeType;
+ throw new IllegalArgumentException(msg);
+ }
+
+ File file = new File(path);
+ if (file.exists()) {
+ FileInputStream is = new FileInputStream(file);
+ FileDescriptor fd = is.getFD();
+ addTimedTextSource(fd, mimeType);
+ is.close();
+ } else {
+ // We do not support the case where the path is not a file.
+ throw new IOException(path);
+ }
+ }
+
+
+ /**
+ * Adds an external timed text source file (Uri).
+ *
+ * Currently supported format is SubRip with the file extension .srt, case insensitive.
+ * Note that a single external timed text source may contain multiple tracks in it.
+ * One can find the total number of available tracks using {@link #getTrackInfo()} to see what
+ * additional tracks become available after this method call.
+ *
+ * @param context the Context to use when resolving the Uri
+ * @param uri the Content URI of the data you want to play
+ * @param mimeType The mime type of the file. Must be one of the mime types listed above.
+ * @throws IOException if the file cannot be accessed or is corrupted.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ @Override
+ public void addTimedTextSource(Context context, Uri uri, String mimeType)
+ throws IOException {
+ String scheme = uri.getScheme();
+ if(scheme == null || scheme.equals("file")) {
+ addTimedTextSource(uri.getPath(), mimeType);
+ return;
+ }
+
+ AssetFileDescriptor fd = null;
+ try {
+ ContentResolver resolver = context.getContentResolver();
+ fd = resolver.openAssetFileDescriptor(uri, "r");
+ if (fd == null) {
+ return;
+ }
+ addTimedTextSource(fd.getFileDescriptor(), mimeType);
+ return;
+ } catch (SecurityException ex) {
+ } catch (IOException ex) {
+ } finally {
+ if (fd != null) {
+ fd.close();
+ }
+ }
+ }
+
+ /**
+ * Adds an external timed text source file (FileDescriptor).
+ *
+ * It is the caller's responsibility to close the file descriptor.
+ * It is safe to do so as soon as this call returns.
+ *
+ * Currently supported format is SubRip. Note that a single external timed text source may
+ * contain multiple tracks in it. One can find the total number of available tracks
+ * using {@link #getTrackInfo()} to see what additional tracks become available
+ * after this method call.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @param mimeType The mime type of the file. Must be one of the mime types listed above.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ @Override
+ public void addTimedTextSource(FileDescriptor fd, String mimeType) {
+ // intentionally less than LONG_MAX
+ addTimedTextSource(fd, 0, 0x7ffffffffffffffL, mimeType);
+ }
+
+ /**
+ * Adds an external timed text file (FileDescriptor).
+ *
+ * It is the caller's responsibility to close the file descriptor.
+ * It is safe to do so as soon as this call returns.
+ *
+ * Currently supported format is SubRip. Note that a single external timed text source may
+ * contain multiple tracks in it. One can find the total number of available tracks
+ * using {@link #getTrackInfo()} to see what additional tracks become available
+ * after this method call.
+ *
+ * @param fd the FileDescriptor for the file you want to play
+ * @param offset the offset into the file where the data to be played starts, in bytes
+ * @param length the length in bytes of the data to be played
+ * @param mime The mime type of the file. Must be one of the mime types listed above.
+ * @throws IllegalArgumentException if the mimeType is not supported.
+ * @throws IllegalStateException if called in an invalid state.
+ * @hide
+ */
+ @Override
+ public void addTimedTextSource(FileDescriptor fd, long offset, long length, String mime) {
+ if (!availableMimeTypeForExternalSource(mime)) {
+ throw new IllegalArgumentException("Illegal mimeType for timed text source: " + mime);
+ }
+
+ final FileDescriptor dupedFd;
+ try {
+ dupedFd = Os.dup(fd);
+ } catch (ErrnoException ex) {
+ Log.e(TAG, ex.getMessage(), ex);
+ throw new RuntimeException(ex);
+ }
+
+ final MediaFormat fFormat = new MediaFormat();
+ fFormat.setString(MediaFormat.KEY_MIME, mime);
+ fFormat.setInteger(MediaFormat.KEY_IS_TIMED_TEXT, 1);
+
+ // A MediaPlayer2 created by a VideoView should already have its mSubtitleController set.
+ if (mSubtitleController == null) {
+ setSubtitleAnchor();
+ }
+
+ if (!mSubtitleController.hasRendererFor(fFormat)) {
+ // test and add not atomic
+ Context context = ActivityThread.currentApplication();
+ mSubtitleController.registerRenderer(new SRTRenderer(context, mEventHandler));
+ }
+ final SubtitleTrack track = mSubtitleController.addTrack(fFormat);
+ synchronized (mIndexTrackPairs) {
+ mIndexTrackPairs.add(Pair.<Integer, SubtitleTrack>create(null, track));
+ }
+
+ getMediaTimeProvider();
+
+ final long offset2 = offset;
+ final long length2 = length;
+ final HandlerThread thread = new HandlerThread(
+ "TimedTextReadThread",
+ Process.THREAD_PRIORITY_BACKGROUND + Process.THREAD_PRIORITY_MORE_FAVORABLE);
+ thread.start();
+ Handler handler = new Handler(thread.getLooper());
+ handler.post(new Runnable() {
+ private int addTrack() {
+ final ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ try {
+ Os.lseek(dupedFd, offset2, OsConstants.SEEK_SET);
+ byte[] buffer = new byte[4096];
+ for (long total = 0; total < length2;) {
+ int bytesToRead = (int) Math.min(buffer.length, length2 - total);
+ int bytes = IoBridge.read(dupedFd, buffer, 0, bytesToRead);
+ if (bytes < 0) {
+ break;
+ } else {
+ bos.write(buffer, 0, bytes);
+ total += bytes;
+ }
+ }
+ Handler h = mTimeProvider.mEventHandler;
+ int what = TimeProvider.NOTIFY;
+ int arg1 = TimeProvider.NOTIFY_TRACK_DATA;
+ Pair<SubtitleTrack, byte[]> trackData = Pair.create(track, bos.toByteArray());
+ Message m = h.obtainMessage(what, arg1, 0, trackData);
+ h.sendMessage(m);
+ return MEDIA_INFO_EXTERNAL_METADATA_UPDATE;
+ } catch (Exception e) {
+ Log.e(TAG, e.getMessage(), e);
+ return MEDIA_INFO_TIMED_TEXT_ERROR;
+ } finally {
+ try {
+ Os.close(dupedFd);
+ } catch (ErrnoException e) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ }
+ }
+
+ public void run() {
+ int res = addTrack();
+ if (mEventHandler != null) {
+ Message m = mEventHandler.obtainMessage(MEDIA_INFO, res, 0, null);
+ mEventHandler.sendMessage(m);
+ }
+ thread.getLooper().quitSafely();
+ }
+ });
+ }
+
+ /**
+ * Returns the index of the audio, video, or subtitle track currently selected for playback,
+ * The return value is an index into the array returned by {@link #getTrackInfo()}, and can
+ * be used in calls to {@link #selectTrack(int)} or {@link #deselectTrack(int)}.
+ *
+ * @param trackType should be one of {@link TrackInfo#MEDIA_TRACK_TYPE_VIDEO},
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_AUDIO}, or
+ * {@link TrackInfo#MEDIA_TRACK_TYPE_SUBTITLE}
+ * @return index of the audio, video, or subtitle track currently selected for playback;
+ * a negative integer is returned when there is no selected track for {@code trackType} or
+ * when {@code trackType} is not one of audio, video, or subtitle.
+ * @throws IllegalStateException if called after {@link #close()}
+ *
+ * @see #getTrackInfo()
+ * @see #selectTrack(int)
+ * @see #deselectTrack(int)
+ */
+ @Override
+ public int getSelectedTrack(int trackType) {
+ if (mSubtitleController != null
+ && (trackType == TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE
+ || trackType == TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT)) {
+ SubtitleTrack subtitleTrack = mSubtitleController.getSelectedTrack();
+ if (subtitleTrack != null) {
+ synchronized (mIndexTrackPairs) {
+ for (int i = 0; i < mIndexTrackPairs.size(); i++) {
+ Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i);
+ if (p.second == subtitleTrack && subtitleTrack.getTrackType() == trackType) {
+ return i;
+ }
+ }
+ }
+ }
+ }
+
+ Parcel request = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ try {
+ request.writeInt(INVOKE_ID_GET_SELECTED_TRACK);
+ request.writeInt(trackType);
+ invoke(request, reply);
+ int inbandTrackIndex = reply.readInt();
+ synchronized (mIndexTrackPairs) {
+ for (int i = 0; i < mIndexTrackPairs.size(); i++) {
+ Pair<Integer, SubtitleTrack> p = mIndexTrackPairs.get(i);
+ if (p.first != null && p.first == inbandTrackIndex) {
+ return i;
+ }
+ }
+ }
+ return -1;
+ } finally {
+ request.recycle();
+ reply.recycle();
+ }
+ }
+
+ /**
+ * Selects a track.
+ * <p>
+ * If a MediaPlayer2 is in invalid state, it throws an IllegalStateException exception.
+ * If a MediaPlayer2 is in <em>Started</em> state, the selected track is presented immediately.
+ * If a MediaPlayer2 is not in Started state, it just marks the track to be played.
+ * </p>
+ * <p>
+ * In any valid state, if it is called multiple times on the same type of track (ie. Video,
+ * Audio, Timed Text), the most recent one will be chosen.
+ * </p>
+ * <p>
+ * The first audio and video tracks are selected by default if available, even though
+ * this method is not called. However, no timed text track will be selected until
+ * this function is called.
+ * </p>
+ * <p>
+ * Currently, only timed text tracks or audio tracks can be selected via this method.
+ * In addition, the support for selecting an audio track at runtime is pretty limited
+ * in that an audio track can only be selected in the <em>Prepared</em> state.
+ * </p>
+ * @param index the index of the track to be selected. The valid range of the index
+ * is 0..total number of track - 1. The total number of tracks as well as the type of
+ * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ *
+ * @see android.media.MediaPlayer2#getTrackInfo
+ */
+ @Override
+ public void selectTrack(int index) {
+ selectOrDeselectTrack(index, true /* select */);
+ }
+
+ /**
+ * Deselect a track.
+ * <p>
+ * Currently, the track must be a timed text track and no audio or video tracks can be
+ * deselected. If the timed text track identified by index has not been
+ * selected before, it throws an exception.
+ * </p>
+ * @param index the index of the track to be deselected. The valid range of the index
+ * is 0..total number of tracks - 1. The total number of tracks as well as the type of
+ * each individual track can be found by calling {@link #getTrackInfo()} method.
+ * @throws IllegalStateException if called in an invalid state.
+ *
+ * @see android.media.MediaPlayer2#getTrackInfo
+ */
+ @Override
+ public void deselectTrack(int index) {
+ selectOrDeselectTrack(index, false /* select */);
+ }
+
+ private void selectOrDeselectTrack(int index, boolean select)
+ throws IllegalStateException {
+ // handle subtitle track through subtitle controller
+ populateInbandTracks();
+
+ Pair<Integer,SubtitleTrack> p = null;
+ try {
+ p = mIndexTrackPairs.get(index);
+ } catch (ArrayIndexOutOfBoundsException e) {
+ // ignore bad index
+ return;
+ }
+
+ SubtitleTrack track = p.second;
+ if (track == null) {
+ // inband (de)select
+ selectOrDeselectInbandTrack(p.first, select);
+ return;
+ }
+
+ if (mSubtitleController == null) {
+ return;
+ }
+
+ if (!select) {
+ // out-of-band deselect
+ if (mSubtitleController.getSelectedTrack() == track) {
+ mSubtitleController.selectTrack(null);
+ } else {
+ Log.w(TAG, "trying to deselect track that was not selected");
+ }
+ return;
+ }
+
+ // out-of-band select
+ if (track.getTrackType() == TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT) {
+ int ttIndex = getSelectedTrack(TrackInfo.MEDIA_TRACK_TYPE_TIMEDTEXT);
+ synchronized (mIndexTrackPairs) {
+ if (ttIndex >= 0 && ttIndex < mIndexTrackPairs.size()) {
+ Pair<Integer,SubtitleTrack> p2 = mIndexTrackPairs.get(ttIndex);
+ if (p2.first != null && p2.second == null) {
+ // deselect inband counterpart
+ selectOrDeselectInbandTrack(p2.first, false);
+ }
+ }
+ }
+ }
+ mSubtitleController.selectTrack(track);
+ }
+
+ private void selectOrDeselectInbandTrack(int index, boolean select)
+ throws IllegalStateException {
+ Parcel request = Parcel.obtain();
+ Parcel reply = Parcel.obtain();
+ try {
+ request.writeInt(select? INVOKE_ID_SELECT_TRACK: INVOKE_ID_DESELECT_TRACK);
+ request.writeInt(index);
+ invoke(request, reply);
+ } finally {
+ request.recycle();
+ reply.recycle();
+ }
+ }
+
+ /**
+ * Sets the target UDP re-transmit endpoint for the low level player.
+ * Generally, the address portion of the endpoint is an IP multicast
+ * address, although a unicast address would be equally valid. When a valid
+ * retransmit endpoint has been set, the media player will not decode and
+ * render the media presentation locally. Instead, the player will attempt
+ * to re-multiplex its media data using the Android@Home RTP profile and
+ * re-transmit to the target endpoint. Receiver devices (which may be
+ * either the same as the transmitting device or different devices) may
+ * instantiate, prepare, and start a receiver player using a setDataSource
+ * URL of the form...
+ *
+ * aahRX://&lt;multicastIP&gt;:&lt;port&gt;
+ *
+ * to receive, decode and render the re-transmitted content.
+ *
+ * setRetransmitEndpoint may only be called before setDataSource has been
+ * called; while the player is in the Idle state.
+ *
+ * @param endpoint the address and UDP port of the re-transmission target or
+ * null if no re-transmission is to be performed.
+ * @throws IllegalStateException if it is called in an invalid state
+ * @throws IllegalArgumentException if the retransmit endpoint is supplied,
+ * but invalid.
+ *
+ * {@hide} pending API council
+ */
+ @Override
+ public void setRetransmitEndpoint(InetSocketAddress endpoint)
+ throws IllegalStateException, IllegalArgumentException
+ {
+ String addrString = null;
+ int port = 0;
+
+ if (null != endpoint) {
+ addrString = endpoint.getAddress().getHostAddress();
+ port = endpoint.getPort();
+ }
+
+ int ret = native_setRetransmitEndpoint(addrString, port);
+ if (ret != 0) {
+ throw new IllegalArgumentException("Illegal re-transmit endpoint; native ret " + ret);
+ }
+ }
+
+ private native final int native_setRetransmitEndpoint(String addrString, int port);
+
+ /**
+ * Releases the resources held by this {@code MediaPlayer2} object.
+ *
+ * It is considered good practice to call this method when you're
+ * done using the MediaPlayer2. In particular, whenever an Activity
+ * of an application is paused (its onPause() method is called),
+ * or stopped (its onStop() method is called), this method should be
+ * invoked to release the MediaPlayer2 object, unless the application
+ * has a special need to keep the object around. In addition to
+ * unnecessary resources (such as memory and instances of codecs)
+ * being held, failure to call this method immediately if a
+ * MediaPlayer2 object is no longer needed may also lead to
+ * continuous battery consumption for mobile devices, and playback
+ * failure for other applications if no multiple instances of the
+ * same codec are supported on a device. Even if multiple instances
+ * of the same codec are supported, some performance degradation
+ * may be expected when unnecessary multiple instances are used
+ * at the same time.
+ *
+ * {@code close()} may be safely called after a prior {@code close()}.
+ * This class implements the Java {@code AutoCloseable} interface and
+ * may be used with try-with-resources.
+ */
+ @Override
+ public void close() {
+ synchronized (mGuard) {
+ release();
+ }
+ }
+
+ // Have to declare protected for finalize() since it is protected
+ // in the base class Object.
+ @Override
+ protected void finalize() throws Throwable {
+ if (mGuard != null) {
+ mGuard.warnIfOpen();
+ }
+
+ close();
+ native_finalize();
+ }
+
+ private void release() {
+ stayAwake(false);
+ updateSurfaceScreenOn();
+ synchronized (mEventCbLock) {
+ mEventCb = null;
+ mEventExec = null;
+ }
+ if (mTimeProvider != null) {
+ mTimeProvider.close();
+ mTimeProvider = null;
+ }
+ mOnSubtitleDataListener = null;
+
+ // Modular DRM clean up
+ mOnDrmConfigHelper = null;
+ synchronized (mDrmEventCbLock) {
+ mDrmEventCb = null;
+ mDrmEventExec = null;
+ }
+ resetDrmState();
+
+ _release();
+ }
+
+ private native void _release();
+
+ /* Do not change these values without updating their counterparts
+ * in include/media/mediaplayer2.h!
+ */
+ private static final int MEDIA_NOP = 0; // interface test message
+ private static final int MEDIA_PREPARED = 1;
+ private static final int MEDIA_PLAYBACK_COMPLETE = 2;
+ private static final int MEDIA_BUFFERING_UPDATE = 3;
+ private static final int MEDIA_SEEK_COMPLETE = 4;
+ private static final int MEDIA_SET_VIDEO_SIZE = 5;
+ private static final int MEDIA_STARTED = 6;
+ private static final int MEDIA_PAUSED = 7;
+ private static final int MEDIA_STOPPED = 8;
+ private static final int MEDIA_SKIPPED = 9;
+ private static final int MEDIA_NOTIFY_TIME = 98;
+ private static final int MEDIA_TIMED_TEXT = 99;
+ private static final int MEDIA_ERROR = 100;
+ private static final int MEDIA_INFO = 200;
+ private static final int MEDIA_SUBTITLE_DATA = 201;
+ private static final int MEDIA_META_DATA = 202;
+ private static final int MEDIA_DRM_INFO = 210;
+ private static final int MEDIA_AUDIO_ROUTING_CHANGED = 10000;
+
+ private TimeProvider mTimeProvider;
+
+ /** @hide */
+ @Override
+ public MediaTimeProvider getMediaTimeProvider() {
+ if (mTimeProvider == null) {
+ mTimeProvider = new TimeProvider(this);
+ }
+ return mTimeProvider;
+ }
+
+ private class EventHandler extends Handler {
+ private MediaPlayer2Impl mMediaPlayer;
+
+ public EventHandler(MediaPlayer2Impl mp, Looper looper) {
+ super(looper);
+ mMediaPlayer = mp;
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (mMediaPlayer.mNativeContext == 0) {
+ Log.w(TAG, "mediaplayer2 went away with unhandled events");
+ return;
+ }
+ final Executor eventExec;
+ final EventCallback eventCb;
+ synchronized (mEventCbLock) {
+ eventExec = mEventExec;
+ eventCb = mEventCb;
+ }
+ final Executor drmEventExec;
+ final DrmEventCallback drmEventCb;
+ synchronized (mDrmEventCbLock) {
+ drmEventExec = mDrmEventExec;
+ drmEventCb = mDrmEventCb;
+ }
+ switch(msg.what) {
+ case MEDIA_PREPARED:
+ try {
+ scanInternalSubtitleTracks();
+ } catch (RuntimeException e) {
+ // send error message instead of crashing;
+ // send error message instead of inlining a call to onError
+ // to avoid code duplication.
+ Message msg2 = obtainMessage(
+ MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, null);
+ sendMessage(msg2);
+ }
+
+ if (eventCb != null && eventExec != null) {
+ eventExec.execute(() -> eventCb.onInfo(
+ mMediaPlayer, 0, MEDIA_INFO_PREPARED, 0));
+ }
+ return;
+
+ case MEDIA_DRM_INFO:
+ Log.v(TAG, "MEDIA_DRM_INFO " + mDrmEventCb);
+
+ if (msg.obj == null) {
+ Log.w(TAG, "MEDIA_DRM_INFO msg.obj=NULL");
+ } else if (msg.obj instanceof Parcel) {
+ if (drmEventExec != null && drmEventCb != null) {
+ // The parcel was parsed already in postEventFromNative
+ final DrmInfoImpl drmInfo;
+
+ synchronized (mDrmLock) {
+ if (mDrmInfoImpl != null) {
+ drmInfo = mDrmInfoImpl.makeCopy();
+ } else {
+ drmInfo = null;
+ }
+ }
+
+ // notifying the client outside the lock
+ if (drmInfo != null) {
+ drmEventExec.execute(() -> drmEventCb.onDrmInfo(mMediaPlayer, drmInfo));
+ }
+ }
+ } else {
+ Log.w(TAG, "MEDIA_DRM_INFO msg.obj of unexpected type " + msg.obj);
+ }
+ return;
+
+ case MEDIA_PLAYBACK_COMPLETE:
+ if (eventCb != null && eventExec != null) {
+ eventExec.execute(() -> eventCb.onInfo(
+ mMediaPlayer, 0, MEDIA_INFO_PLAYBACK_COMPLETE, 0));
+ }
+ stayAwake(false);
+ return;
+
+ case MEDIA_STOPPED:
+ {
+ TimeProvider timeProvider = mTimeProvider;
+ if (timeProvider != null) {
+ timeProvider.onStopped();
+ }
+ }
+ break;
+
+ case MEDIA_STARTED:
+ case MEDIA_PAUSED:
+ {
+ TimeProvider timeProvider = mTimeProvider;
+ if (timeProvider != null) {
+ timeProvider.onPaused(msg.what == MEDIA_PAUSED);
+ }
+ }
+ break;
+
+ case MEDIA_BUFFERING_UPDATE:
+ if (eventCb != null && eventExec != null) {
+ final int percent = msg.arg1;
+ eventExec.execute(() -> eventCb.onBufferingUpdate(mMediaPlayer, 0, percent));
+ }
+ return;
+
+ case MEDIA_SEEK_COMPLETE:
+ if (eventCb != null && eventExec != null) {
+ eventExec.execute(() -> eventCb.onInfo(
+ mMediaPlayer, 0, MEDIA_INFO_COMPLETE_CALL_SEEK, 0));
+ }
+ // fall through
+
+ case MEDIA_SKIPPED:
+ {
+ TimeProvider timeProvider = mTimeProvider;
+ if (timeProvider != null) {
+ timeProvider.onSeekComplete(mMediaPlayer);
+ }
+ }
+ return;
+
+ case MEDIA_SET_VIDEO_SIZE:
+ if (eventCb != null && eventExec != null) {
+ final int width = msg.arg1;
+ final int height = msg.arg2;
+ eventExec.execute(() -> eventCb.onVideoSizeChanged(
+ mMediaPlayer, 0, width, height));
+ }
+ return;
+
+ case MEDIA_ERROR:
+ Log.e(TAG, "Error (" + msg.arg1 + "," + msg.arg2 + ")");
+ if (eventCb != null && eventExec != null) {
+ final int what = msg.arg1;
+ final int extra = msg.arg2;
+ eventExec.execute(() -> eventCb.onError(mMediaPlayer, 0, what, extra));
+ eventExec.execute(() -> eventCb.onInfo(
+ mMediaPlayer, 0, MEDIA_INFO_PLAYBACK_COMPLETE, 0));
+ }
+ stayAwake(false);
+ return;
+
+ case MEDIA_INFO:
+ switch (msg.arg1) {
+ case MEDIA_INFO_VIDEO_TRACK_LAGGING:
+ Log.i(TAG, "Info (" + msg.arg1 + "," + msg.arg2 + ")");
+ break;
+ case MEDIA_INFO_METADATA_UPDATE:
+ try {
+ scanInternalSubtitleTracks();
+ } catch (RuntimeException e) {
+ Message msg2 = obtainMessage(
+ MEDIA_ERROR, MEDIA_ERROR_UNKNOWN, MEDIA_ERROR_UNSUPPORTED, null);
+ sendMessage(msg2);
+ }
+ // fall through
+
+ case MEDIA_INFO_EXTERNAL_METADATA_UPDATE:
+ msg.arg1 = MEDIA_INFO_METADATA_UPDATE;
+ // update default track selection
+ if (mSubtitleController != null) {
+ mSubtitleController.selectDefaultTrack();
+ }
+ break;
+ case MEDIA_INFO_BUFFERING_START:
+ case MEDIA_INFO_BUFFERING_END:
+ TimeProvider timeProvider = mTimeProvider;
+ if (timeProvider != null) {
+ timeProvider.onBuffering(msg.arg1 == MEDIA_INFO_BUFFERING_START);
+ }
+ break;
+ }
+
+ if (eventCb != null && eventExec != null) {
+ final int what = msg.arg1;
+ final int extra = msg.arg2;
+ eventExec.execute(() -> eventCb.onInfo(mMediaPlayer, 0, what, extra));
+ }
+ // No real default action so far.
+ return;
+
+ case MEDIA_NOTIFY_TIME:
+ TimeProvider timeProvider = mTimeProvider;
+ if (timeProvider != null) {
+ timeProvider.onNotifyTime();
+ }
+ return;
+
+ case MEDIA_TIMED_TEXT:
+ if (eventCb == null || eventExec == null) {
+ return;
+ }
+ if (msg.obj == null) {
+ eventExec.execute(() -> eventCb.onTimedText(mMediaPlayer, 0, null));
+ } else {
+ if (msg.obj instanceof Parcel) {
+ Parcel parcel = (Parcel)msg.obj;
+ TimedText text = new TimedText(parcel);
+ parcel.recycle();
+ eventExec.execute(() -> eventCb.onTimedText(mMediaPlayer, 0, text));
+ }
+ }
+ return;
+
+ case MEDIA_SUBTITLE_DATA:
+ OnSubtitleDataListener onSubtitleDataListener = mOnSubtitleDataListener;
+ if (onSubtitleDataListener == null) {
+ return;
+ }
+ if (msg.obj instanceof Parcel) {
+ Parcel parcel = (Parcel) msg.obj;
+ SubtitleData data = new SubtitleData(parcel);
+ parcel.recycle();
+ onSubtitleDataListener.onSubtitleData(mMediaPlayer, data);
+ }
+ return;
+
+ case MEDIA_META_DATA:
+ if (eventCb == null || eventExec == null) {
+ return;
+ }
+ if (msg.obj instanceof Parcel) {
+ Parcel parcel = (Parcel) msg.obj;
+ TimedMetaData data = TimedMetaData.createTimedMetaDataFromParcel(parcel);
+ parcel.recycle();
+ eventExec.execute(() -> eventCb.onTimedMetaDataAvailable(
+ mMediaPlayer, 0, data));
+ }
+ return;
+
+ case MEDIA_NOP: // interface test message - ignore
+ break;
+
+ case MEDIA_AUDIO_ROUTING_CHANGED:
+ AudioManager.resetAudioPortGeneration();
+ synchronized (mRoutingChangeListeners) {
+ for (NativeRoutingEventHandlerDelegate delegate
+ : mRoutingChangeListeners.values()) {
+ delegate.notifyClient();
+ }
+ }
+ return;
+
+ default:
+ Log.e(TAG, "Unknown message type " + msg.what);
+ return;
+ }
+ }
+ }
+
+ /*
+ * Called from native code when an interesting event happens. This method
+ * just uses the EventHandler system to post the event back to the main app thread.
+ * We use a weak reference to the original MediaPlayer2 object so that the native
+ * code is safe from the object disappearing from underneath it. (This is
+ * the cookie passed to native_setup().)
+ */
+ private static void postEventFromNative(Object mediaplayer2_ref,
+ int what, int arg1, int arg2, Object obj)
+ {
+ final MediaPlayer2Impl mp = (MediaPlayer2Impl)((WeakReference)mediaplayer2_ref).get();
+ if (mp == null) {
+ return;
+ }
+
+ switch (what) {
+ case MEDIA_INFO:
+ if (arg1 == MEDIA_INFO_STARTED_AS_NEXT) {
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ // this acquires the wakelock if needed, and sets the client side state
+ mp.play();
+ }
+ }).start();
+ Thread.yield();
+ }
+ break;
+
+ case MEDIA_DRM_INFO:
+ // We need to derive mDrmInfoImpl before prepare() returns so processing it here
+ // before the notification is sent to EventHandler below. EventHandler runs in the
+ // notification looper so its handleMessage might process the event after prepare()
+ // has returned.
+ Log.v(TAG, "postEventFromNative MEDIA_DRM_INFO");
+ if (obj instanceof Parcel) {
+ Parcel parcel = (Parcel)obj;
+ DrmInfoImpl drmInfo = new DrmInfoImpl(parcel);
+ synchronized (mp.mDrmLock) {
+ mp.mDrmInfoImpl = drmInfo;
+ }
+ } else {
+ Log.w(TAG, "MEDIA_DRM_INFO msg.obj of unexpected type " + obj);
+ }
+ break;
+
+ case MEDIA_PREPARED:
+ // By this time, we've learned about DrmInfo's presence or absence. This is meant
+ // mainly for prepareAsync() use case. For prepare(), this still can run to a race
+ // condition b/c MediaPlayerNative releases the prepare() lock before calling notify
+ // so we also set mDrmInfoResolved in prepare().
+ synchronized (mp.mDrmLock) {
+ mp.mDrmInfoResolved = true;
+ }
+ break;
+
+ }
+
+ if (mp.mEventHandler != null) {
+ Message m = mp.mEventHandler.obtainMessage(what, arg1, arg2, obj);
+ mp.mEventHandler.sendMessage(m);
+ }
+ }
+
+ private Executor mEventExec;
+ private EventCallback mEventCb;
+ private final Object mEventCbLock = new Object();
+
+ /**
+ * Register a callback to be invoked when the media source is ready
+ * for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ @Override
+ public void registerEventCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull EventCallback eventCallback) {
+ if (eventCallback == null) {
+ throw new IllegalArgumentException("Illegal null EventCallback");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Illegal null Executor for the EventCallback");
+ }
+ synchronized (mEventCbLock) {
+ // TODO: support multiple callbacks.
+ mEventExec = executor;
+ mEventCb = eventCallback;
+ }
+ }
+
+ /**
+ * Unregisters an {@link EventCallback}.
+ *
+ * @param callback an {@link EventCallback} to unregister
+ */
+ @Override
+ public void unregisterEventCallback(EventCallback callback) {
+ synchronized (mEventCbLock) {
+ if (callback == mEventCb) {
+ mEventExec = null;
+ mEventCb = null;
+ }
+ }
+ }
+
+ /**
+ * Register a callback to be invoked when a track has data available.
+ *
+ * @param listener the callback that will be run
+ *
+ * @hide
+ */
+ @Override
+ public void setOnSubtitleDataListener(OnSubtitleDataListener listener) {
+ mOnSubtitleDataListener = listener;
+ }
+
+ private OnSubtitleDataListener mOnSubtitleDataListener;
+
+
+ // Modular DRM begin
+
+ /**
+ * Register a callback to be invoked for configuration of the DRM object before
+ * the session is created.
+ * The callback will be invoked synchronously during the execution
+ * of {@link #prepareDrm(UUID uuid)}.
+ *
+ * @param listener the callback that will be run
+ */
+ @Override
+ public void setOnDrmConfigHelper(OnDrmConfigHelper listener)
+ {
+ synchronized (mDrmLock) {
+ mOnDrmConfigHelper = listener;
+ } // synchronized
+ }
+
+ private OnDrmConfigHelper mOnDrmConfigHelper;
+
+ private Executor mDrmEventExec;
+ private DrmEventCallback mDrmEventCb;
+ private final Object mDrmEventCbLock = new Object();
+
+ /**
+ * Register a callback to be invoked when the media source is ready
+ * for playback.
+ *
+ * @param eventCallback the callback that will be run
+ * @param executor the executor through which the callback should be invoked
+ */
+ @Override
+ public void registerDrmEventCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull DrmEventCallback eventCallback) {
+ if (eventCallback == null) {
+ throw new IllegalArgumentException("Illegal null EventCallback");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Illegal null Executor for the EventCallback");
+ }
+ synchronized (mDrmEventCbLock) {
+ // TODO: support multiple callbacks.
+ mDrmEventExec = executor;
+ mDrmEventCb = eventCallback;
+ }
+ }
+
+ /**
+ * Unregisters a {@link DrmEventCallback}.
+ *
+ * @param callback a {@link DrmEventCallback} to unregister
+ */
+ @Override
+ public void unregisterDrmEventCallback(DrmEventCallback callback) {
+ synchronized (mDrmEventCbLock) {
+ if (callback == mDrmEventCb) {
+ mDrmEventExec = null;
+ mDrmEventCb = null;
+ }
+ }
+ }
+
+
+ /**
+ * Retrieves the DRM Info associated with the current source
+ *
+ * @throws IllegalStateException if called before prepare()
+ */
+ @Override
+ public DrmInfo getDrmInfo() {
+ DrmInfoImpl drmInfo = null;
+
+ // there is not much point if the app calls getDrmInfo within an OnDrmInfoListenet;
+ // regardless below returns drmInfo anyway instead of raising an exception
+ synchronized (mDrmLock) {
+ if (!mDrmInfoResolved && mDrmInfoImpl == null) {
+ final String msg = "The Player has not been prepared yet";
+ Log.v(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+
+ if (mDrmInfoImpl != null) {
+ drmInfo = mDrmInfoImpl.makeCopy();
+ }
+ } // synchronized
+
+ return drmInfo;
+ }
+
+
+ /**
+ * Prepares the DRM for the current source
+ * <p>
+ * If {@code OnDrmConfigHelper} is registered, it will be called during
+ * preparation to allow configuration of the DRM properties before opening the
+ * DRM session. Note that the callback is called synchronously in the thread that called
+ * {@code prepareDrm}. It should be used only for a series of {@code getDrmPropertyString}
+ * and {@code setDrmPropertyString} calls and refrain from any lengthy operation.
+ * <p>
+ * If the device has not been provisioned before, this call also provisions the device
+ * which involves accessing the provisioning server and can take a variable time to
+ * complete depending on the network connectivity.
+ * If {@code OnDrmPreparedListener} is registered, prepareDrm() runs in non-blocking
+ * mode by launching the provisioning in the background and returning. The listener
+ * will be called when provisioning and preparation has finished. If a
+ * {@code OnDrmPreparedListener} is not registered, prepareDrm() waits till provisioning
+ * and preparation has finished, i.e., runs in blocking mode.
+ * <p>
+ * If {@code OnDrmPreparedListener} is registered, it is called to indicate the DRM
+ * session being ready. The application should not make any assumption about its call
+ * sequence (e.g., before or after prepareDrm returns), or the thread context that will
+ * execute the listener (unless the listener is registered with a handler thread).
+ * <p>
+ *
+ * @param uuid The UUID of the crypto scheme. If not known beforehand, it can be retrieved
+ * from the source through {@code getDrmInfo} or registering a {@code onDrmInfoListener}.
+ *
+ * @throws IllegalStateException if called before prepare(), or the DRM was
+ * prepared already
+ * @throws UnsupportedSchemeException if the crypto scheme is not supported
+ * @throws ResourceBusyException if required DRM resources are in use
+ * @throws ProvisioningNetworkErrorException if provisioning is required but failed due to a
+ * network error
+ * @throws ProvisioningServerErrorException if provisioning is required but failed due to
+ * the request denied by the provisioning server
+ */
+ @Override
+ public void prepareDrm(@NonNull UUID uuid)
+ throws UnsupportedSchemeException, ResourceBusyException,
+ ProvisioningNetworkErrorException, ProvisioningServerErrorException
+ {
+ Log.v(TAG, "prepareDrm: uuid: " + uuid + " mOnDrmConfigHelper: " + mOnDrmConfigHelper);
+
+ boolean allDoneWithoutProvisioning = false;
+
+ synchronized (mDrmLock) {
+
+ // only allowing if tied to a protected source; might relax for releasing offline keys
+ if (mDrmInfoImpl == null) {
+ final String msg = "prepareDrm(): Wrong usage: The player must be prepared and " +
+ "DRM info be retrieved before this call.";
+ Log.e(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+
+ if (mActiveDrmScheme) {
+ final String msg = "prepareDrm(): Wrong usage: There is already " +
+ "an active DRM scheme with " + mDrmUUID;
+ Log.e(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+
+ if (mPrepareDrmInProgress) {
+ final String msg = "prepareDrm(): Wrong usage: There is already " +
+ "a pending prepareDrm call.";
+ Log.e(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+
+ if (mDrmProvisioningInProgress) {
+ final String msg = "prepareDrm(): Unexpectd: Provisioning is already in progress.";
+ Log.e(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+
+ // shouldn't need this; just for safeguard
+ cleanDrmObj();
+
+ mPrepareDrmInProgress = true;
+
+ try {
+ // only creating the DRM object to allow pre-openSession configuration
+ prepareDrm_createDrmStep(uuid);
+ } catch (Exception e) {
+ Log.w(TAG, "prepareDrm(): Exception ", e);
+ mPrepareDrmInProgress = false;
+ throw e;
+ }
+
+ mDrmConfigAllowed = true;
+ } // synchronized
+
+
+ // call the callback outside the lock
+ if (mOnDrmConfigHelper != null) {
+ mOnDrmConfigHelper.onDrmConfig(this);
+ }
+
+ synchronized (mDrmLock) {
+ mDrmConfigAllowed = false;
+ boolean earlyExit = false;
+
+ try {
+ prepareDrm_openSessionStep(uuid);
+
+ mDrmUUID = uuid;
+ mActiveDrmScheme = true;
+
+ allDoneWithoutProvisioning = true;
+ } catch (IllegalStateException e) {
+ final String msg = "prepareDrm(): Wrong usage: The player must be " +
+ "in the prepared state to call prepareDrm().";
+ Log.e(TAG, msg);
+ earlyExit = true;
+ throw new IllegalStateException(msg);
+ } catch (NotProvisionedException e) {
+ Log.w(TAG, "prepareDrm: NotProvisionedException");
+
+ // handle provisioning internally; it'll reset mPrepareDrmInProgress
+ int result = HandleProvisioninig(uuid);
+
+ // if blocking mode, we're already done;
+ // if non-blocking mode, we attempted to launch background provisioning
+ if (result != PREPARE_DRM_STATUS_SUCCESS) {
+ earlyExit = true;
+ String msg;
+
+ switch (result) {
+ case PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR:
+ msg = "prepareDrm: Provisioning was required but failed " +
+ "due to a network error.";
+ Log.e(TAG, msg);
+ throw new ProvisioningNetworkErrorExceptionImpl(msg);
+
+ case PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR:
+ msg = "prepareDrm: Provisioning was required but the request " +
+ "was denied by the server.";
+ Log.e(TAG, msg);
+ throw new ProvisioningServerErrorExceptionImpl(msg);
+
+ case PREPARE_DRM_STATUS_PREPARATION_ERROR:
+ default: // default for safeguard
+ msg = "prepareDrm: Post-provisioning preparation failed.";
+ Log.e(TAG, msg);
+ throw new IllegalStateException(msg);
+ }
+ }
+ // nothing else to do;
+ // if blocking or non-blocking, HandleProvisioninig does the re-attempt & cleanup
+ } catch (Exception e) {
+ Log.e(TAG, "prepareDrm: Exception " + e);
+ earlyExit = true;
+ throw e;
+ } finally {
+ if (!mDrmProvisioningInProgress) {// if early exit other than provisioning exception
+ mPrepareDrmInProgress = false;
+ }
+ if (earlyExit) { // cleaning up object if didn't succeed
+ cleanDrmObj();
+ }
+ } // finally
+ } // synchronized
+
+
+ // if finished successfully without provisioning, call the callback outside the lock
+ if (allDoneWithoutProvisioning) {
+ final Executor drmEventExec;
+ final DrmEventCallback drmEventCb;
+ synchronized (mDrmEventCbLock) {
+ drmEventExec = mDrmEventExec;
+ drmEventCb = mDrmEventCb;
+ }
+ if (drmEventExec != null && drmEventCb != null) {
+ drmEventExec.execute(() -> drmEventCb.onDrmPrepared(
+ this, PREPARE_DRM_STATUS_SUCCESS));
+ }
+ }
+
+ }
+
+
+ private native void _releaseDrm();
+
+ /**
+ * Releases the DRM session
+ * <p>
+ * The player has to have an active DRM session and be in stopped, or prepared
+ * state before this call is made.
+ * A {@code reset()} call will release the DRM session implicitly.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session to release
+ */
+ @Override
+ public void releaseDrm()
+ throws NoDrmSchemeException
+ {
+ Log.v(TAG, "releaseDrm:");
+
+ synchronized (mDrmLock) {
+ if (!mActiveDrmScheme) {
+ Log.e(TAG, "releaseDrm(): No active DRM scheme to release.");
+ throw new NoDrmSchemeExceptionImpl("releaseDrm: No active DRM scheme to release.");
+ }
+
+ try {
+ // we don't have the player's state in this layer. The below call raises
+ // exception if we're in a non-stopped/prepared state.
+
+ // for cleaning native/mediaserver crypto object
+ _releaseDrm();
+
+ // for cleaning client-side MediaDrm object; only called if above has succeeded
+ cleanDrmObj();
+
+ mActiveDrmScheme = false;
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "releaseDrm: Exception ", e);
+ throw new IllegalStateException("releaseDrm: The player is not in a valid state.");
+ } catch (Exception e) {
+ Log.e(TAG, "releaseDrm: Exception ", e);
+ }
+ } // synchronized
+ }
+
+
+ /**
+ * A key request/response exchange occurs between the app and a license server
+ * to obtain or release keys used to decrypt encrypted content.
+ * <p>
+ * getKeyRequest() is used to obtain an opaque key request byte array that is
+ * delivered to the license server. The opaque key request byte array is returned
+ * in KeyRequest.data. The recommended URL to deliver the key request to is
+ * returned in KeyRequest.defaultUrl.
+ * <p>
+ * After the app has received the key request response from the server,
+ * it should deliver to the response to the DRM engine plugin using the method
+ * {@link #provideKeyResponse}.
+ *
+ * @param keySetId is the key-set identifier of the offline keys being released when keyType is
+ * {@link MediaDrm#KEY_TYPE_RELEASE}. It should be set to null for other key requests, when
+ * keyType is {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}.
+ *
+ * @param initData is the container-specific initialization data when the keyType is
+ * {@link MediaDrm#KEY_TYPE_STREAMING} or {@link MediaDrm#KEY_TYPE_OFFLINE}. Its meaning is
+ * interpreted based on the mime type provided in the mimeType parameter. It could
+ * contain, for example, the content ID, key ID or other data obtained from the content
+ * metadata that is required in generating the key request.
+ * When the keyType is {@link MediaDrm#KEY_TYPE_RELEASE}, it should be set to null.
+ *
+ * @param mimeType identifies the mime type of the content
+ *
+ * @param keyType specifies the type of the request. The request may be to acquire
+ * keys for streaming, {@link MediaDrm#KEY_TYPE_STREAMING}, or for offline content
+ * {@link MediaDrm#KEY_TYPE_OFFLINE}, or to release previously acquired
+ * keys ({@link MediaDrm#KEY_TYPE_RELEASE}), which are identified by a keySetId.
+ *
+ * @param optionalParameters are included in the key request message to
+ * allow a client application to provide additional message parameters to the server.
+ * This may be {@code null} if no additional parameters are to be sent.
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ */
+ @Override
+ @NonNull
+ public MediaDrm.KeyRequest getKeyRequest(@Nullable byte[] keySetId, @Nullable byte[] initData,
+ @Nullable String mimeType, @MediaDrm.KeyType int keyType,
+ @Nullable Map<String, String> optionalParameters)
+ throws NoDrmSchemeException
+ {
+ Log.v(TAG, "getKeyRequest: " +
+ " keySetId: " + keySetId + " initData:" + initData + " mimeType: " + mimeType +
+ " keyType: " + keyType + " optionalParameters: " + optionalParameters);
+
+ synchronized (mDrmLock) {
+ if (!mActiveDrmScheme) {
+ Log.e(TAG, "getKeyRequest NoDrmSchemeException");
+ throw new NoDrmSchemeExceptionImpl("getKeyRequest: Has to set a DRM scheme first.");
+ }
+
+ try {
+ byte[] scope = (keyType != MediaDrm.KEY_TYPE_RELEASE) ?
+ mDrmSessionId : // sessionId for KEY_TYPE_STREAMING/OFFLINE
+ keySetId; // keySetId for KEY_TYPE_RELEASE
+
+ HashMap<String, String> hmapOptionalParameters =
+ (optionalParameters != null) ?
+ new HashMap<String, String>(optionalParameters) :
+ null;
+
+ MediaDrm.KeyRequest request = mDrmObj.getKeyRequest(scope, initData, mimeType,
+ keyType, hmapOptionalParameters);
+ Log.v(TAG, "getKeyRequest: --> request: " + request);
+
+ return request;
+
+ } catch (NotProvisionedException e) {
+ Log.w(TAG, "getKeyRequest NotProvisionedException: " +
+ "Unexpected. Shouldn't have reached here.");
+ throw new IllegalStateException("getKeyRequest: Unexpected provisioning error.");
+ } catch (Exception e) {
+ Log.w(TAG, "getKeyRequest Exception " + e);
+ throw e;
+ }
+
+ } // synchronized
+ }
+
+
+ /**
+ * A key response is received from the license server by the app, then it is
+ * provided to the DRM engine plugin using provideKeyResponse. When the
+ * response is for an offline key request, a key-set identifier is returned that
+ * can be used to later restore the keys to a new session with the method
+ * {@ link # restoreKeys}.
+ * When the response is for a streaming or release request, null is returned.
+ *
+ * @param keySetId When the response is for a release request, keySetId identifies
+ * the saved key associated with the release request (i.e., the same keySetId
+ * passed to the earlier {@ link # getKeyRequest} call. It MUST be null when the
+ * response is for either streaming or offline key requests.
+ *
+ * @param response the byte array response from the server
+ *
+ * @throws NoDrmSchemeException if there is no active DRM session
+ * @throws DeniedByServerException if the response indicates that the
+ * server rejected the request
+ */
+ @Override
+ public byte[] provideKeyResponse(@Nullable byte[] keySetId, @NonNull byte[] response)
+ throws NoDrmSchemeException, DeniedByServerException
+ {
+ Log.v(TAG, "provideKeyResponse: keySetId: " + keySetId + " response: " + response);
+
+ synchronized (mDrmLock) {
+
+ if (!mActiveDrmScheme) {
+ Log.e(TAG, "getKeyRequest NoDrmSchemeException");
+ throw new NoDrmSchemeExceptionImpl("getKeyRequest: Has to set a DRM scheme first.");
+ }
+
+ try {
+ byte[] scope = (keySetId == null) ?
+ mDrmSessionId : // sessionId for KEY_TYPE_STREAMING/OFFLINE
+ keySetId; // keySetId for KEY_TYPE_RELEASE
+
+ byte[] keySetResult = mDrmObj.provideKeyResponse(scope, response);
+
+ Log.v(TAG, "provideKeyResponse: keySetId: " + keySetId + " response: " + response +
+ " --> " + keySetResult);
+
+
+ return keySetResult;
+
+ } catch (NotProvisionedException e) {
+ Log.w(TAG, "provideKeyResponse NotProvisionedException: " +
+ "Unexpected. Shouldn't have reached here.");
+ throw new IllegalStateException("provideKeyResponse: " +
+ "Unexpected provisioning error.");
+ } catch (Exception e) {
+ Log.w(TAG, "provideKeyResponse Exception " + e);
+ throw e;
+ }
+ } // synchronized
+ }
+
+
+ /**
+ * Restore persisted offline keys into a new session. keySetId identifies the
+ * keys to load, obtained from a prior call to {@link #provideKeyResponse}.
+ *
+ * @param keySetId identifies the saved key set to restore
+ */
+ @Override
+ public void restoreKeys(@NonNull byte[] keySetId)
+ throws NoDrmSchemeException
+ {
+ Log.v(TAG, "restoreKeys: keySetId: " + keySetId);
+
+ synchronized (mDrmLock) {
+
+ if (!mActiveDrmScheme) {
+ Log.w(TAG, "restoreKeys NoDrmSchemeException");
+ throw new NoDrmSchemeExceptionImpl("restoreKeys: Has to set a DRM scheme first.");
+ }
+
+ try {
+ mDrmObj.restoreKeys(mDrmSessionId, keySetId);
+ } catch (Exception e) {
+ Log.w(TAG, "restoreKeys Exception " + e);
+ throw e;
+ }
+
+ } // synchronized
+ }
+
+
+ /**
+ * Read a DRM engine plugin String property value, given the property name string.
+ * <p>
+ * @param propertyName the property name
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ @Override
+ @NonNull
+ public String getDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName)
+ throws NoDrmSchemeException
+ {
+ Log.v(TAG, "getDrmPropertyString: propertyName: " + propertyName);
+
+ String value;
+ synchronized (mDrmLock) {
+
+ if (!mActiveDrmScheme && !mDrmConfigAllowed) {
+ Log.w(TAG, "getDrmPropertyString NoDrmSchemeException");
+ throw new NoDrmSchemeExceptionImpl("getDrmPropertyString: Has to prepareDrm() first.");
+ }
+
+ try {
+ value = mDrmObj.getPropertyString(propertyName);
+ } catch (Exception e) {
+ Log.w(TAG, "getDrmPropertyString Exception " + e);
+ throw e;
+ }
+ } // synchronized
+
+ Log.v(TAG, "getDrmPropertyString: propertyName: " + propertyName + " --> value: " + value);
+
+ return value;
+ }
+
+
+ /**
+ * Set a DRM engine plugin String property value.
+ * <p>
+ * @param propertyName the property name
+ * @param value the property value
+ *
+ * Standard fields names are:
+ * {@link MediaDrm#PROPERTY_VENDOR}, {@link MediaDrm#PROPERTY_VERSION},
+ * {@link MediaDrm#PROPERTY_DESCRIPTION}, {@link MediaDrm#PROPERTY_ALGORITHMS}
+ */
+ @Override
+ public void setDrmPropertyString(@NonNull @MediaDrm.StringProperty String propertyName,
+ @NonNull String value)
+ throws NoDrmSchemeException
+ {
+ Log.v(TAG, "setDrmPropertyString: propertyName: " + propertyName + " value: " + value);
+
+ synchronized (mDrmLock) {
+
+ if ( !mActiveDrmScheme && !mDrmConfigAllowed ) {
+ Log.w(TAG, "setDrmPropertyString NoDrmSchemeException");
+ throw new NoDrmSchemeExceptionImpl("setDrmPropertyString: Has to prepareDrm() first.");
+ }
+
+ try {
+ mDrmObj.setPropertyString(propertyName, value);
+ } catch ( Exception e ) {
+ Log.w(TAG, "setDrmPropertyString Exception " + e);
+ throw e;
+ }
+ } // synchronized
+ }
+
+ /**
+ * Encapsulates the DRM properties of the source.
+ */
+ public static final class DrmInfoImpl extends DrmInfo {
+ private Map<UUID, byte[]> mapPssh;
+ private UUID[] supportedSchemes;
+
+ /**
+ * Returns the PSSH info of the data source for each supported DRM scheme.
+ */
+ @Override
+ public Map<UUID, byte[]> getPssh() {
+ return mapPssh;
+ }
+
+ /**
+ * Returns the intersection of the data source and the device DRM schemes.
+ * It effectively identifies the subset of the source's DRM schemes which
+ * are supported by the device too.
+ */
+ @Override
+ public List<UUID> getSupportedSchemes() {
+ return Arrays.asList(supportedSchemes);
+ }
+
+ private DrmInfoImpl(Map<UUID, byte[]> Pssh, UUID[] SupportedSchemes) {
+ mapPssh = Pssh;
+ supportedSchemes = SupportedSchemes;
+ }
+
+ private DrmInfoImpl(Parcel parcel) {
+ Log.v(TAG, "DrmInfoImpl(" + parcel + ") size " + parcel.dataSize());
+
+ int psshsize = parcel.readInt();
+ byte[] pssh = new byte[psshsize];
+ parcel.readByteArray(pssh);
+
+ Log.v(TAG, "DrmInfoImpl() PSSH: " + arrToHex(pssh));
+ mapPssh = parsePSSH(pssh, psshsize);
+ Log.v(TAG, "DrmInfoImpl() PSSH: " + mapPssh);
+
+ int supportedDRMsCount = parcel.readInt();
+ supportedSchemes = new UUID[supportedDRMsCount];
+ for (int i = 0; i < supportedDRMsCount; i++) {
+ byte[] uuid = new byte[16];
+ parcel.readByteArray(uuid);
+
+ supportedSchemes[i] = bytesToUUID(uuid);
+
+ Log.v(TAG, "DrmInfoImpl() supportedScheme[" + i + "]: " +
+ supportedSchemes[i]);
+ }
+
+ Log.v(TAG, "DrmInfoImpl() Parcel psshsize: " + psshsize +
+ " supportedDRMsCount: " + supportedDRMsCount);
+ }
+
+ private DrmInfoImpl makeCopy() {
+ return new DrmInfoImpl(this.mapPssh, this.supportedSchemes);
+ }
+
+ private String arrToHex(byte[] bytes) {
+ String out = "0x";
+ for (int i = 0; i < bytes.length; i++) {
+ out += String.format("%02x", bytes[i]);
+ }
+
+ return out;
+ }
+
+ private UUID bytesToUUID(byte[] uuid) {
+ long msb = 0, lsb = 0;
+ for (int i = 0; i < 8; i++) {
+ msb |= ( ((long)uuid[i] & 0xff) << (8 * (7 - i)) );
+ lsb |= ( ((long)uuid[i+8] & 0xff) << (8 * (7 - i)) );
+ }
+
+ return new UUID(msb, lsb);
+ }
+
+ private Map<UUID, byte[]> parsePSSH(byte[] pssh, int psshsize) {
+ Map<UUID, byte[]> result = new HashMap<UUID, byte[]>();
+
+ final int UUID_SIZE = 16;
+ final int DATALEN_SIZE = 4;
+
+ int len = psshsize;
+ int numentries = 0;
+ int i = 0;
+
+ while (len > 0) {
+ if (len < UUID_SIZE) {
+ Log.w(TAG, String.format("parsePSSH: len is too short to parse " +
+ "UUID: (%d < 16) pssh: %d", len, psshsize));
+ return null;
+ }
+
+ byte[] subset = Arrays.copyOfRange(pssh, i, i + UUID_SIZE);
+ UUID uuid = bytesToUUID(subset);
+ i += UUID_SIZE;
+ len -= UUID_SIZE;
+
+ // get data length
+ if (len < 4) {
+ Log.w(TAG, String.format("parsePSSH: len is too short to parse " +
+ "datalen: (%d < 4) pssh: %d", len, psshsize));
+ return null;
+ }
+
+ subset = Arrays.copyOfRange(pssh, i, i+DATALEN_SIZE);
+ int datalen = (ByteOrder.nativeOrder() == ByteOrder.LITTLE_ENDIAN) ?
+ ((subset[3] & 0xff) << 24) | ((subset[2] & 0xff) << 16) |
+ ((subset[1] & 0xff) << 8) | (subset[0] & 0xff) :
+ ((subset[0] & 0xff) << 24) | ((subset[1] & 0xff) << 16) |
+ ((subset[2] & 0xff) << 8) | (subset[3] & 0xff) ;
+ i += DATALEN_SIZE;
+ len -= DATALEN_SIZE;
+
+ if (len < datalen) {
+ Log.w(TAG, String.format("parsePSSH: len is too short to parse " +
+ "data: (%d < %d) pssh: %d", len, datalen, psshsize));
+ return null;
+ }
+
+ byte[] data = Arrays.copyOfRange(pssh, i, i+datalen);
+
+ // skip the data
+ i += datalen;
+ len -= datalen;
+
+ Log.v(TAG, String.format("parsePSSH[%d]: <%s, %s> pssh: %d",
+ numentries, uuid, arrToHex(data), psshsize));
+ numentries++;
+ result.put(uuid, data);
+ }
+
+ return result;
+ }
+
+ }; // DrmInfoImpl
+
+ /**
+ * Thrown when a DRM method is called before preparing a DRM scheme through prepareDrm().
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static final class NoDrmSchemeExceptionImpl extends NoDrmSchemeException {
+ public NoDrmSchemeExceptionImpl(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to a network error (Internet reachability, timeout, etc.).
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static final class ProvisioningNetworkErrorExceptionImpl
+ extends ProvisioningNetworkErrorException {
+ public ProvisioningNetworkErrorExceptionImpl(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+ /**
+ * Thrown when the device requires DRM provisioning but the provisioning attempt has
+ * failed due to the provisioning server denying the request.
+ * Extends MediaDrm.MediaDrmException
+ */
+ public static final class ProvisioningServerErrorExceptionImpl
+ extends ProvisioningServerErrorException {
+ public ProvisioningServerErrorExceptionImpl(String detailMessage) {
+ super(detailMessage);
+ }
+ }
+
+
+ private native void _prepareDrm(@NonNull byte[] uuid, @NonNull byte[] drmSessionId);
+
+ // Modular DRM helpers
+
+ private void prepareDrm_createDrmStep(@NonNull UUID uuid)
+ throws UnsupportedSchemeException {
+ Log.v(TAG, "prepareDrm_createDrmStep: UUID: " + uuid);
+
+ try {
+ mDrmObj = new MediaDrm(uuid);
+ Log.v(TAG, "prepareDrm_createDrmStep: Created mDrmObj=" + mDrmObj);
+ } catch (Exception e) { // UnsupportedSchemeException
+ Log.e(TAG, "prepareDrm_createDrmStep: MediaDrm failed with " + e);
+ throw e;
+ }
+ }
+
+ private void prepareDrm_openSessionStep(@NonNull UUID uuid)
+ throws NotProvisionedException, ResourceBusyException {
+ Log.v(TAG, "prepareDrm_openSessionStep: uuid: " + uuid);
+
+ // TODO: don't need an open session for a future specialKeyReleaseDrm mode but we should do
+ // it anyway so it raises provisioning error if needed. We'd rather handle provisioning
+ // at prepareDrm/openSession rather than getKeyRequest/provideKeyResponse
+ try {
+ mDrmSessionId = mDrmObj.openSession();
+ Log.v(TAG, "prepareDrm_openSessionStep: mDrmSessionId=" + mDrmSessionId);
+
+ // Sending it down to native/mediaserver to create the crypto object
+ // This call could simply fail due to bad player state, e.g., after play().
+ _prepareDrm(getByteArrayFromUUID(uuid), mDrmSessionId);
+ Log.v(TAG, "prepareDrm_openSessionStep: _prepareDrm/Crypto succeeded");
+
+ } catch (Exception e) { //ResourceBusyException, NotProvisionedException
+ Log.e(TAG, "prepareDrm_openSessionStep: open/crypto failed with " + e);
+ throw e;
+ }
+
+ }
+
+ private class ProvisioningThread extends Thread {
+ public static final int TIMEOUT_MS = 60000;
+
+ private UUID uuid;
+ private String urlStr;
+ private Object drmLock;
+ private MediaPlayer2Impl mediaPlayer;
+ private int status;
+ private boolean finished;
+ public int status() {
+ return status;
+ }
+
+ public ProvisioningThread initialize(MediaDrm.ProvisionRequest request,
+ UUID uuid, MediaPlayer2Impl mediaPlayer) {
+ // lock is held by the caller
+ drmLock = mediaPlayer.mDrmLock;
+ this.mediaPlayer = mediaPlayer;
+
+ urlStr = request.getDefaultUrl() + "&signedRequest=" + new String(request.getData());
+ this.uuid = uuid;
+
+ status = PREPARE_DRM_STATUS_PREPARATION_ERROR;
+
+ Log.v(TAG, "HandleProvisioninig: Thread is initialised url: " + urlStr);
+ return this;
+ }
+
+ public void run() {
+
+ byte[] response = null;
+ boolean provisioningSucceeded = false;
+ try {
+ URL url = new URL(urlStr);
+ final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ try {
+ connection.setRequestMethod("POST");
+ connection.setDoOutput(false);
+ connection.setDoInput(true);
+ connection.setConnectTimeout(TIMEOUT_MS);
+ connection.setReadTimeout(TIMEOUT_MS);
+
+ connection.connect();
+ response = Streams.readFully(connection.getInputStream());
+
+ Log.v(TAG, "HandleProvisioninig: Thread run: response " +
+ response.length + " " + response);
+ } catch (Exception e) {
+ status = PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR;
+ Log.w(TAG, "HandleProvisioninig: Thread run: connect " + e + " url: " + url);
+ } finally {
+ connection.disconnect();
+ }
+ } catch (Exception e) {
+ status = PREPARE_DRM_STATUS_PROVISIONING_NETWORK_ERROR;
+ Log.w(TAG, "HandleProvisioninig: Thread run: openConnection " + e);
+ }
+
+ if (response != null) {
+ try {
+ mDrmObj.provideProvisionResponse(response);
+ Log.v(TAG, "HandleProvisioninig: Thread run: " +
+ "provideProvisionResponse SUCCEEDED!");
+
+ provisioningSucceeded = true;
+ } catch (Exception e) {
+ status = PREPARE_DRM_STATUS_PROVISIONING_SERVER_ERROR;
+ Log.w(TAG, "HandleProvisioninig: Thread run: " +
+ "provideProvisionResponse " + e);
+ }
+ }
+
+ boolean succeeded = false;
+
+ final Executor drmEventExec;
+ final DrmEventCallback drmEventCb;
+ synchronized (mDrmEventCbLock) {
+ drmEventExec = mDrmEventExec;
+ drmEventCb = mDrmEventCb;
+ }
+ // non-blocking mode needs the lock
+ if (drmEventExec != null && drmEventCb != null) {
+
+ synchronized (drmLock) {
+ // continuing with prepareDrm
+ if (provisioningSucceeded) {
+ succeeded = mediaPlayer.resumePrepareDrm(uuid);
+ status = (succeeded) ?
+ PREPARE_DRM_STATUS_SUCCESS :
+ PREPARE_DRM_STATUS_PREPARATION_ERROR;
+ }
+ mediaPlayer.mDrmProvisioningInProgress = false;
+ mediaPlayer.mPrepareDrmInProgress = false;
+ if (!succeeded) {
+ cleanDrmObj(); // cleaning up if it hasn't gone through while in the lock
+ }
+ } // synchronized
+
+ // calling the callback outside the lock
+ drmEventExec.execute(() -> drmEventCb.onDrmPrepared(mediaPlayer, status));
+ } else { // blocking mode already has the lock
+
+ // continuing with prepareDrm
+ if (provisioningSucceeded) {
+ succeeded = mediaPlayer.resumePrepareDrm(uuid);
+ status = (succeeded) ?
+ PREPARE_DRM_STATUS_SUCCESS :
+ PREPARE_DRM_STATUS_PREPARATION_ERROR;
+ }
+ mediaPlayer.mDrmProvisioningInProgress = false;
+ mediaPlayer.mPrepareDrmInProgress = false;
+ if (!succeeded) {
+ cleanDrmObj(); // cleaning up if it hasn't gone through
+ }
+ }
+
+ finished = true;
+ } // run()
+
+ } // ProvisioningThread
+
+ private int HandleProvisioninig(UUID uuid) {
+ // the lock is already held by the caller
+
+ if (mDrmProvisioningInProgress) {
+ Log.e(TAG, "HandleProvisioninig: Unexpected mDrmProvisioningInProgress");
+ return PREPARE_DRM_STATUS_PREPARATION_ERROR;
+ }
+
+ MediaDrm.ProvisionRequest provReq = mDrmObj.getProvisionRequest();
+ if (provReq == null) {
+ Log.e(TAG, "HandleProvisioninig: getProvisionRequest returned null.");
+ return PREPARE_DRM_STATUS_PREPARATION_ERROR;
+ }
+
+ Log.v(TAG, "HandleProvisioninig provReq " +
+ " data: " + provReq.getData() + " url: " + provReq.getDefaultUrl());
+
+ // networking in a background thread
+ mDrmProvisioningInProgress = true;
+
+ mDrmProvisioningThread = new ProvisioningThread().initialize(provReq, uuid, this);
+ mDrmProvisioningThread.start();
+
+ int result;
+
+ // non-blocking: this is not the final result
+ final Executor drmEventExec;
+ final DrmEventCallback drmEventCb;
+ synchronized (mDrmEventCbLock) {
+ drmEventExec = mDrmEventExec;
+ drmEventCb = mDrmEventCb;
+ }
+ if (drmEventCb != null && drmEventExec != null) {
+ result = PREPARE_DRM_STATUS_SUCCESS;
+ } else {
+ // if blocking mode, wait till provisioning is done
+ try {
+ mDrmProvisioningThread.join();
+ } catch (Exception e) {
+ Log.w(TAG, "HandleProvisioninig: Thread.join Exception " + e);
+ }
+ result = mDrmProvisioningThread.status();
+ // no longer need the thread
+ mDrmProvisioningThread = null;
+ }
+
+ return result;
+ }
+
+ private boolean resumePrepareDrm(UUID uuid) {
+ Log.v(TAG, "resumePrepareDrm: uuid: " + uuid);
+
+ // mDrmLock is guaranteed to be held
+ boolean success = false;
+ try {
+ // resuming
+ prepareDrm_openSessionStep(uuid);
+
+ mDrmUUID = uuid;
+ mActiveDrmScheme = true;
+
+ success = true;
+ } catch (Exception e) {
+ Log.w(TAG, "HandleProvisioninig: Thread run _prepareDrm resume failed with " + e);
+ // mDrmObj clean up is done by the caller
+ }
+
+ return success;
+ }
+
+ private void resetDrmState() {
+ synchronized (mDrmLock) {
+ Log.v(TAG, "resetDrmState: " +
+ " mDrmInfoImpl=" + mDrmInfoImpl +
+ " mDrmProvisioningThread=" + mDrmProvisioningThread +
+ " mPrepareDrmInProgress=" + mPrepareDrmInProgress +
+ " mActiveDrmScheme=" + mActiveDrmScheme);
+
+ mDrmInfoResolved = false;
+ mDrmInfoImpl = null;
+
+ if (mDrmProvisioningThread != null) {
+ // timeout; relying on HttpUrlConnection
+ try {
+ mDrmProvisioningThread.join();
+ }
+ catch (InterruptedException e) {
+ Log.w(TAG, "resetDrmState: ProvThread.join Exception " + e);
+ }
+ mDrmProvisioningThread = null;
+ }
+
+ mPrepareDrmInProgress = false;
+ mActiveDrmScheme = false;
+
+ cleanDrmObj();
+ } // synchronized
+ }
+
+ private void cleanDrmObj() {
+ // the caller holds mDrmLock
+ Log.v(TAG, "cleanDrmObj: mDrmObj=" + mDrmObj + " mDrmSessionId=" + mDrmSessionId);
+
+ if (mDrmSessionId != null) {
+ mDrmObj.closeSession(mDrmSessionId);
+ mDrmSessionId = null;
+ }
+ if (mDrmObj != null) {
+ mDrmObj.release();
+ mDrmObj = null;
+ }
+ }
+
+ private static final byte[] getByteArrayFromUUID(@NonNull UUID uuid) {
+ long msb = uuid.getMostSignificantBits();
+ long lsb = uuid.getLeastSignificantBits();
+
+ byte[] uuidBytes = new byte[16];
+ for (int i = 0; i < 8; ++i) {
+ uuidBytes[i] = (byte)(msb >>> (8 * (7 - i)));
+ uuidBytes[8 + i] = (byte)(lsb >>> (8 * (7 - i)));
+ }
+
+ return uuidBytes;
+ }
+
+ // Modular DRM end
+
+ /*
+ * Test whether a given video scaling mode is supported.
+ */
+ private boolean isVideoScalingModeSupported(int mode) {
+ return (mode == VIDEO_SCALING_MODE_SCALE_TO_FIT ||
+ mode == VIDEO_SCALING_MODE_SCALE_TO_FIT_WITH_CROPPING);
+ }
+
+ /** @hide */
+ static class TimeProvider implements MediaTimeProvider {
+ private static final String TAG = "MTP";
+ private static final long MAX_NS_WITHOUT_POSITION_CHECK = 5000000000L;
+ private static final long MAX_EARLY_CALLBACK_US = 1000;
+ private static final long TIME_ADJUSTMENT_RATE = 2; /* meaning 1/2 */
+ private long mLastTimeUs = 0;
+ private MediaPlayer2Impl mPlayer;
+ private boolean mPaused = true;
+ private boolean mStopped = true;
+ private boolean mBuffering;
+ private long mLastReportedTime;
+ // since we are expecting only a handful listeners per stream, there is
+ // no need for log(N) search performance
+ private MediaTimeProvider.OnMediaTimeListener mListeners[];
+ private long mTimes[];
+ private Handler mEventHandler;
+ private boolean mRefresh = false;
+ private boolean mPausing = false;
+ private boolean mSeeking = false;
+ private static final int NOTIFY = 1;
+ private static final int NOTIFY_TIME = 0;
+ private static final int NOTIFY_STOP = 2;
+ private static final int NOTIFY_SEEK = 3;
+ private static final int NOTIFY_TRACK_DATA = 4;
+ private HandlerThread mHandlerThread;
+
+ /** @hide */
+ public boolean DEBUG = false;
+
+ public TimeProvider(MediaPlayer2Impl mp) {
+ mPlayer = mp;
+ try {
+ getCurrentTimeUs(true, false);
+ } catch (IllegalStateException e) {
+ // we assume starting position
+ mRefresh = true;
+ }
+
+ Looper looper;
+ if ((looper = Looper.myLooper()) == null &&
+ (looper = Looper.getMainLooper()) == null) {
+ // Create our own looper here in case MP was created without one
+ mHandlerThread = new HandlerThread("MediaPlayer2MTPEventThread",
+ Process.THREAD_PRIORITY_FOREGROUND);
+ mHandlerThread.start();
+ looper = mHandlerThread.getLooper();
+ }
+ mEventHandler = new EventHandler(looper);
+
+ mListeners = new MediaTimeProvider.OnMediaTimeListener[0];
+ mTimes = new long[0];
+ mLastTimeUs = 0;
+ }
+
+ private void scheduleNotification(int type, long delayUs) {
+ // ignore time notifications until seek is handled
+ if (mSeeking && type == NOTIFY_TIME) {
+ return;
+ }
+
+ if (DEBUG) Log.v(TAG, "scheduleNotification " + type + " in " + delayUs);
+ mEventHandler.removeMessages(NOTIFY);
+ Message msg = mEventHandler.obtainMessage(NOTIFY, type, 0);
+ mEventHandler.sendMessageDelayed(msg, (int) (delayUs / 1000));
+ }
+
+ /** @hide */
+ public void close() {
+ mEventHandler.removeMessages(NOTIFY);
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ mHandlerThread = null;
+ }
+ }
+
+ /** @hide */
+ protected void finalize() {
+ if (mHandlerThread != null) {
+ mHandlerThread.quitSafely();
+ }
+ }
+
+ /** @hide */
+ public void onNotifyTime() {
+ synchronized (this) {
+ if (DEBUG) Log.d(TAG, "onNotifyTime: ");
+ scheduleNotification(NOTIFY_TIME, 0 /* delay */);
+ }
+ }
+
+ /** @hide */
+ public void onPaused(boolean paused) {
+ synchronized(this) {
+ if (DEBUG) Log.d(TAG, "onPaused: " + paused);
+ if (mStopped) { // handle as seek if we were stopped
+ mStopped = false;
+ mSeeking = true;
+ scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
+ } else {
+ mPausing = paused; // special handling if player disappeared
+ mSeeking = false;
+ scheduleNotification(NOTIFY_TIME, 0 /* delay */);
+ }
+ }
+ }
+
+ /** @hide */
+ public void onBuffering(boolean buffering) {
+ synchronized (this) {
+ if (DEBUG) Log.d(TAG, "onBuffering: " + buffering);
+ mBuffering = buffering;
+ scheduleNotification(NOTIFY_TIME, 0 /* delay */);
+ }
+ }
+
+ /** @hide */
+ public void onStopped() {
+ synchronized(this) {
+ if (DEBUG) Log.d(TAG, "onStopped");
+ mPaused = true;
+ mStopped = true;
+ mSeeking = false;
+ mBuffering = false;
+ scheduleNotification(NOTIFY_STOP, 0 /* delay */);
+ }
+ }
+
+ /** @hide */
+ public void onSeekComplete(MediaPlayer2Impl mp) {
+ synchronized(this) {
+ mStopped = false;
+ mSeeking = true;
+ scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
+ }
+ }
+
+ /** @hide */
+ public void onNewPlayer() {
+ if (mRefresh) {
+ synchronized(this) {
+ mStopped = false;
+ mSeeking = true;
+ mBuffering = false;
+ scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
+ }
+ }
+ }
+
+ private synchronized void notifySeek() {
+ mSeeking = false;
+ try {
+ long timeUs = getCurrentTimeUs(true, false);
+ if (DEBUG) Log.d(TAG, "onSeekComplete at " + timeUs);
+
+ for (MediaTimeProvider.OnMediaTimeListener listener: mListeners) {
+ if (listener == null) {
+ break;
+ }
+ listener.onSeek(timeUs);
+ }
+ } catch (IllegalStateException e) {
+ // we should not be there, but at least signal pause
+ if (DEBUG) Log.d(TAG, "onSeekComplete but no player");
+ mPausing = true; // special handling if player disappeared
+ notifyTimedEvent(false /* refreshTime */);
+ }
+ }
+
+ private synchronized void notifyTrackData(Pair<SubtitleTrack, byte[]> trackData) {
+ SubtitleTrack track = trackData.first;
+ byte[] data = trackData.second;
+ track.onData(data, true /* eos */, ~0 /* runID: keep forever */);
+ }
+
+ private synchronized void notifyStop() {
+ for (MediaTimeProvider.OnMediaTimeListener listener: mListeners) {
+ if (listener == null) {
+ break;
+ }
+ listener.onStop();
+ }
+ }
+
+ private int registerListener(MediaTimeProvider.OnMediaTimeListener listener) {
+ int i = 0;
+ for (; i < mListeners.length; i++) {
+ if (mListeners[i] == listener || mListeners[i] == null) {
+ break;
+ }
+ }
+
+ // new listener
+ if (i >= mListeners.length) {
+ MediaTimeProvider.OnMediaTimeListener[] newListeners =
+ new MediaTimeProvider.OnMediaTimeListener[i + 1];
+ long[] newTimes = new long[i + 1];
+ System.arraycopy(mListeners, 0, newListeners, 0, mListeners.length);
+ System.arraycopy(mTimes, 0, newTimes, 0, mTimes.length);
+ mListeners = newListeners;
+ mTimes = newTimes;
+ }
+
+ if (mListeners[i] == null) {
+ mListeners[i] = listener;
+ mTimes[i] = MediaTimeProvider.NO_TIME;
+ }
+ return i;
+ }
+
+ public void notifyAt(
+ long timeUs, MediaTimeProvider.OnMediaTimeListener listener) {
+ synchronized(this) {
+ if (DEBUG) Log.d(TAG, "notifyAt " + timeUs);
+ mTimes[registerListener(listener)] = timeUs;
+ scheduleNotification(NOTIFY_TIME, 0 /* delay */);
+ }
+ }
+
+ public void scheduleUpdate(MediaTimeProvider.OnMediaTimeListener listener) {
+ synchronized(this) {
+ if (DEBUG) Log.d(TAG, "scheduleUpdate");
+ int i = registerListener(listener);
+
+ if (!mStopped) {
+ mTimes[i] = 0;
+ scheduleNotification(NOTIFY_TIME, 0 /* delay */);
+ }
+ }
+ }
+
+ public void cancelNotifications(
+ MediaTimeProvider.OnMediaTimeListener listener) {
+ synchronized(this) {
+ int i = 0;
+ for (; i < mListeners.length; i++) {
+ if (mListeners[i] == listener) {
+ System.arraycopy(mListeners, i + 1,
+ mListeners, i, mListeners.length - i - 1);
+ System.arraycopy(mTimes, i + 1,
+ mTimes, i, mTimes.length - i - 1);
+ mListeners[mListeners.length - 1] = null;
+ mTimes[mTimes.length - 1] = NO_TIME;
+ break;
+ } else if (mListeners[i] == null) {
+ break;
+ }
+ }
+
+ scheduleNotification(NOTIFY_TIME, 0 /* delay */);
+ }
+ }
+
+ private synchronized void notifyTimedEvent(boolean refreshTime) {
+ // figure out next callback
+ long nowUs;
+ try {
+ nowUs = getCurrentTimeUs(refreshTime, true);
+ } catch (IllegalStateException e) {
+ // assume we paused until new player arrives
+ mRefresh = true;
+ mPausing = true; // this ensures that call succeeds
+ nowUs = getCurrentTimeUs(refreshTime, true);
+ }
+ long nextTimeUs = nowUs;
+
+ if (mSeeking) {
+ // skip timed-event notifications until seek is complete
+ return;
+ }
+
+ if (DEBUG) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("notifyTimedEvent(").append(mLastTimeUs).append(" -> ")
+ .append(nowUs).append(") from {");
+ boolean first = true;
+ for (long time: mTimes) {
+ if (time == NO_TIME) {
+ continue;
+ }
+ if (!first) sb.append(", ");
+ sb.append(time);
+ first = false;
+ }
+ sb.append("}");
+ Log.d(TAG, sb.toString());
+ }
+
+ Vector<MediaTimeProvider.OnMediaTimeListener> activatedListeners =
+ new Vector<MediaTimeProvider.OnMediaTimeListener>();
+ for (int ix = 0; ix < mTimes.length; ix++) {
+ if (mListeners[ix] == null) {
+ break;
+ }
+ if (mTimes[ix] <= NO_TIME) {
+ // ignore, unless we were stopped
+ } else if (mTimes[ix] <= nowUs + MAX_EARLY_CALLBACK_US) {
+ activatedListeners.add(mListeners[ix]);
+ if (DEBUG) Log.d(TAG, "removed");
+ mTimes[ix] = NO_TIME;
+ } else if (nextTimeUs == nowUs || mTimes[ix] < nextTimeUs) {
+ nextTimeUs = mTimes[ix];
+ }
+ }
+
+ if (nextTimeUs > nowUs && !mPaused) {
+ // schedule callback at nextTimeUs
+ if (DEBUG) Log.d(TAG, "scheduling for " + nextTimeUs + " and " + nowUs);
+ mPlayer.notifyAt(nextTimeUs);
+ } else {
+ mEventHandler.removeMessages(NOTIFY);
+ // no more callbacks
+ }
+
+ for (MediaTimeProvider.OnMediaTimeListener listener: activatedListeners) {
+ listener.onTimedEvent(nowUs);
+ }
+ }
+
+ public long getCurrentTimeUs(boolean refreshTime, boolean monotonic)
+ throws IllegalStateException {
+ synchronized (this) {
+ // we always refresh the time when the paused-state changes, because
+ // we expect to have received the pause-change event delayed.
+ if (mPaused && !refreshTime) {
+ return mLastReportedTime;
+ }
+
+ try {
+ mLastTimeUs = mPlayer.getCurrentPosition() * 1000L;
+ mPaused = !mPlayer.isPlaying() || mBuffering;
+ if (DEBUG) Log.v(TAG, (mPaused ? "paused" : "playing") + " at " + mLastTimeUs);
+ } catch (IllegalStateException e) {
+ if (mPausing) {
+ // if we were pausing, get last estimated timestamp
+ mPausing = false;
+ if (!monotonic || mLastReportedTime < mLastTimeUs) {
+ mLastReportedTime = mLastTimeUs;
+ }
+ mPaused = true;
+ if (DEBUG) Log.d(TAG, "illegal state, but pausing: estimating at " + mLastReportedTime);
+ return mLastReportedTime;
+ }
+ // TODO get time when prepared
+ throw e;
+ }
+ if (monotonic && mLastTimeUs < mLastReportedTime) {
+ /* have to adjust time */
+ if (mLastReportedTime - mLastTimeUs > 1000000) {
+ // schedule seeked event if time jumped significantly
+ // TODO: do this properly by introducing an exception
+ mStopped = false;
+ mSeeking = true;
+ scheduleNotification(NOTIFY_SEEK, 0 /* delay */);
+ }
+ } else {
+ mLastReportedTime = mLastTimeUs;
+ }
+
+ return mLastReportedTime;
+ }
+ }
+
+ private class EventHandler extends Handler {
+ public EventHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (msg.what == NOTIFY) {
+ switch (msg.arg1) {
+ case NOTIFY_TIME:
+ notifyTimedEvent(true /* refreshTime */);
+ break;
+ case NOTIFY_STOP:
+ notifyStop();
+ break;
+ case NOTIFY_SEEK:
+ notifySeek();
+ break;
+ case NOTIFY_TRACK_DATA:
+ notifyTrackData((Pair<SubtitleTrack, byte[]>)msg.obj);
+ break;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/android/media/MediaPlayerBase.java b/android/media/MediaPlayerBase.java
new file mode 100644
index 00000000..d638a9f9
--- /dev/null
+++ b/android/media/MediaPlayerBase.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 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.media;
+
+import android.media.MediaSession2.PlaylistParam;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Base interfaces for all media players that want media session.
+ *
+ * @hide
+ */
+public abstract class MediaPlayerBase {
+ /**
+ * Listens change in {@link PlaybackState2}.
+ */
+ public interface PlaybackListener {
+ /**
+ * Called when {@link PlaybackState2} for this player is changed.
+ */
+ void onPlaybackChanged(PlaybackState2 state);
+ }
+
+ public abstract void play();
+ public abstract void prepare();
+ public abstract void pause();
+ public abstract void stop();
+ public abstract void skipToPrevious();
+ public abstract void skipToNext();
+ public abstract void seekTo(long pos);
+ public abstract void fastFoward();
+ public abstract void rewind();
+
+ public abstract PlaybackState2 getPlaybackState();
+ public abstract AudioAttributes getAudioAttributes();
+
+ public abstract void setPlaylist(List<MediaItem2> item, PlaylistParam param);
+ public abstract void setCurrentPlaylistItem(int index);
+
+ /**
+ * Add a {@link PlaybackListener} to be invoked when the playback state is changed.
+ *
+ * @param executor the Handler that will receive the listener
+ * @param listener the listener that will be run
+ */
+ public abstract void addPlaybackListener(Executor executor, PlaybackListener listener);
+
+ /**
+ * Remove previously added {@link PlaybackListener}.
+ *
+ * @param listener the listener to be removed
+ */
+ public abstract void removePlaybackListener(PlaybackListener listener);
+}
diff --git a/android/media/MediaRecorder.java b/android/media/MediaRecorder.java
index 3c49b80b..78477f75 100644
--- a/android/media/MediaRecorder.java
+++ b/android/media/MediaRecorder.java
@@ -1380,7 +1380,8 @@ public class MediaRecorder implements AudioRouting
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));
}
}
}
@@ -1401,36 +1402,6 @@ public class MediaRecorder 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 MediaRecorder mMediaRecorder;
- private AudioRouting.OnRoutingChangedListener mOnRoutingChangedListener;
- private Handler mHandler;
-
- NativeRoutingEventHandlerDelegate(final MediaRecorder mediaRecorder,
- final AudioRouting.OnRoutingChangedListener listener, Handler handler) {
- mMediaRecorder = mediaRecorder;
- 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(mMediaRecorder);
- }
- }
- });
- }
- }
- }
-
private native final boolean native_setInputDevice(int deviceId);
private native final int native_getRoutedDeviceId();
private native final void native_enableDeviceCallback(boolean enabled);
diff --git a/android/media/MediaSession2.java b/android/media/MediaSession2.java
new file mode 100644
index 00000000..0e90040a
--- /dev/null
+++ b/android/media/MediaSession2.java
@@ -0,0 +1,1223 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.session.MediaSession;
+import android.media.session.MediaSession.Callback;
+import android.media.session.PlaybackState;
+import android.media.update.ApiLoader;
+import android.media.update.MediaSession2Provider;
+import android.media.update.MediaSession2Provider.ControllerInfoProvider;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcelable;
+import android.os.ResultReceiver;
+import android.text.TextUtils;
+import android.util.ArraySet;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows a media app to expose its transport controls and playback information in a process to
+ * other processes including the Android framework and other apps. Common use cases are as follows.
+ * <ul>
+ * <li>Bluetooth/wired headset key events support</li>
+ * <li>Android Auto/Wearable support</li>
+ * <li>Separating UI process and playback process</li>
+ * </ul>
+ * <p>
+ * A MediaSession2 should be created when an app wants to publish media playback information or
+ * handle media keys. In general an app only needs one session for all playback, though multiple
+ * sessions can be created to provide finer grain controls of media.
+ * <p>
+ * If you want to support background playback, {@link MediaSessionService2} is preferred
+ * instead. With it, your playback can be revived even after you've finished playback. See
+ * {@link MediaSessionService2} for details.
+ * <p>
+ * A session can be obtained by {@link Builder}. The owner of the session may pass its session token
+ * to other processes to allow them to create a {@link MediaController2} to interact with the
+ * session.
+ * <p>
+ * When a session receive transport control commands, the session sends the commands directly to
+ * the the underlying media player set by {@link Builder} or {@link #setPlayer(MediaPlayerBase)}.
+ * <p>
+ * When an app is finished performing playback it must call {@link #close()} to clean up the session
+ * and notify any controllers.
+ * <p>
+ * {@link MediaSession2} objects should be used on the thread on the looper.
+ *
+ * @see MediaSessionService2
+ * @hide
+ */
+// TODO(jaewan): Unhide
+// TODO(jaewan): Revisit comments. Currently it's borrowed from the MediaSession.
+// TODO(jaewan): Should we support thread safe? It may cause tricky issue such as b/63797089
+// TODO(jaewan): Should we make APIs for MediaSessionService2 public? It's helpful for
+// developers that doesn't want to override from Browser, but user may not use this
+// correctly.
+public class MediaSession2 implements AutoCloseable {
+ private final MediaSession2Provider mProvider;
+
+ // Note: Do not define IntDef because subclass can add more command code on top of these.
+ // TODO(jaewan): Shouldn't we pull out?
+ public static final int COMMAND_CODE_CUSTOM = 0;
+ public static final int COMMAND_CODE_PLAYBACK_START = 1;
+ public static final int COMMAND_CODE_PLAYBACK_PAUSE = 2;
+ public static final int COMMAND_CODE_PLAYBACK_STOP = 3;
+ public static final int COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM = 4;
+ public static final int COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM = 5;
+ public static final int COMMAND_CODE_PLAYBACK_PREPARE = 6;
+ public static final int COMMAND_CODE_PLAYBACK_FAST_FORWARD = 7;
+ public static final int COMMAND_CODE_PLAYBACK_REWIND = 8;
+ public static final int COMMAND_CODE_PLAYBACK_SEEK_TO = 9;
+ public static final int COMMAND_CODE_PLAYBACK_SET_CURRENT_PLAYLIST_ITEM = 10;
+
+ public static final int COMMAND_CODE_PLAYLIST_GET = 11;
+ public static final int COMMAND_CODE_PLAYLIST_ADD = 12;
+ public static final int COMMAND_CODE_PLAYLIST_REMOVE = 13;
+
+ public static final int COMMAND_CODE_PLAY_FROM_MEDIA_ID = 14;
+ public static final int COMMAND_CODE_PLAY_FROM_URI = 15;
+ public static final int COMMAND_CODE_PLAY_FROM_SEARCH = 16;
+
+ public static final int COMMAND_CODE_PREPARE_FROM_MEDIA_ID = 17;
+ public static final int COMMAND_CODE_PREPARE_FROM_URI = 18;
+ public static final int COMMAND_CODE_PREPARE_FROM_SEARCH = 19;
+
+ /**
+ * Define a command that a {@link MediaController2} can send to a {@link MediaSession2}.
+ * <p>
+ * If {@link #getCommandCode()} isn't {@link #COMMAND_CODE_CUSTOM}), it's predefined command.
+ * If {@link #getCommandCode()} is {@link #COMMAND_CODE_CUSTOM}), it's custom command and
+ * {@link #getCustomCommand()} shouldn't be {@code null}.
+ */
+ // TODO(jaewan): Move this into the updatable.
+ public static final class Command {
+ private static final String KEY_COMMAND_CODE
+ = "android.media.media_session2.command.command_code";
+ private static final String KEY_COMMAND_CUSTOM_COMMAND
+ = "android.media.media_session2.command.custom_command";
+ private static final String KEY_COMMAND_EXTRA
+ = "android.media.media_session2.command.extra";
+
+ private final int mCommandCode;
+ // Nonnull if it's custom command
+ private final String mCustomCommand;
+ private final Bundle mExtra;
+
+ public Command(int commandCode) {
+ mCommandCode = commandCode;
+ mCustomCommand = null;
+ mExtra = null;
+ }
+
+ public Command(@NonNull String action, @Nullable Bundle extra) {
+ if (action == null) {
+ throw new IllegalArgumentException("action shouldn't be null");
+ }
+ mCommandCode = COMMAND_CODE_CUSTOM;
+ mCustomCommand = action;
+ mExtra = extra;
+ }
+
+ public int getCommandCode() {
+ return mCommandCode;
+ }
+
+ public @Nullable String getCustomCommand() {
+ return mCustomCommand;
+ }
+
+ public @Nullable Bundle getExtra() {
+ return mExtra;
+ }
+
+ /**
+ * @return a new Bundle instance from the Command
+ * @hide
+ */
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(KEY_COMMAND_CODE, mCommandCode);
+ bundle.putString(KEY_COMMAND_CUSTOM_COMMAND, mCustomCommand);
+ bundle.putBundle(KEY_COMMAND_EXTRA, mExtra);
+ return bundle;
+ }
+
+ /**
+ * @return a new Command instance from the Bundle
+ * @hide
+ */
+ public static Command fromBundle(Bundle command) {
+ int code = command.getInt(KEY_COMMAND_CODE);
+ if (code != COMMAND_CODE_CUSTOM) {
+ return new Command(code);
+ } else {
+ String customCommand = command.getString(KEY_COMMAND_CUSTOM_COMMAND);
+ if (customCommand == null) {
+ return null;
+ }
+ return new Command(customCommand, command.getBundle(KEY_COMMAND_EXTRA));
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Command)) {
+ return false;
+ }
+ Command other = (Command) obj;
+ // TODO(jaewan): Should we also compare contents in bundle?
+ // It may not be possible if the bundle contains private class.
+ return mCommandCode == other.mCommandCode
+ && TextUtils.equals(mCustomCommand, other.mCustomCommand);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ return ((mCustomCommand != null) ? mCustomCommand.hashCode() : 0) * prime + mCommandCode;
+ }
+ }
+
+ /**
+ * Represent set of {@link Command}.
+ */
+ // TODO(jaewan): Move this to updatable
+ public static class CommandGroup {
+ private static final String KEY_COMMANDS =
+ "android.media.mediasession2.commandgroup.commands";
+ private ArraySet<Command> mCommands = new ArraySet<>();
+
+ public CommandGroup() {
+ }
+
+ public CommandGroup(CommandGroup others) {
+ mCommands.addAll(others.mCommands);
+ }
+
+ public void addCommand(Command command) {
+ mCommands.add(command);
+ }
+
+ public void addAllPredefinedCommands() {
+ // TODO(jaewan): Is there any better way than this?
+ mCommands.add(new Command(COMMAND_CODE_PLAYBACK_START));
+ mCommands.add(new Command(COMMAND_CODE_PLAYBACK_PAUSE));
+ mCommands.add(new Command(COMMAND_CODE_PLAYBACK_STOP));
+ mCommands.add(new Command(COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM));
+ mCommands.add(new Command(COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM));
+ }
+
+ public void removeCommand(Command command) {
+ mCommands.remove(command);
+ }
+
+ public boolean hasCommand(Command command) {
+ return mCommands.contains(command);
+ }
+
+ public boolean hasCommand(int code) {
+ if (code == COMMAND_CODE_CUSTOM) {
+ throw new IllegalArgumentException("Use hasCommand(Command) for custom command");
+ }
+ for (int i = 0; i < mCommands.size(); i++) {
+ if (mCommands.valueAt(i).getCommandCode() == code) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @return new bundle from the CommandGroup
+ * @hide
+ */
+ public Bundle toBundle() {
+ ArrayList<Bundle> list = new ArrayList<>();
+ for (int i = 0; i < mCommands.size(); i++) {
+ list.add(mCommands.valueAt(i).toBundle());
+ }
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArrayList(KEY_COMMANDS, list);
+ return bundle;
+ }
+
+ /**
+ * @return new instance of CommandGroup from the bundle
+ * @hide
+ */
+ public static @Nullable CommandGroup fromBundle(Bundle commands) {
+ if (commands == null) {
+ return null;
+ }
+ List<Parcelable> list = commands.getParcelableArrayList(KEY_COMMANDS);
+ if (list == null) {
+ return null;
+ }
+ CommandGroup commandGroup = new CommandGroup();
+ for (int i = 0; i < list.size(); i++) {
+ Parcelable parcelable = list.get(i);
+ if (!(parcelable instanceof Bundle)) {
+ continue;
+ }
+ Bundle commandBundle = (Bundle) parcelable;
+ Command command = Command.fromBundle(commandBundle);
+ if (command != null) {
+ commandGroup.addCommand(command);
+ }
+ }
+ return commandGroup;
+ }
+ }
+
+ /**
+ * Callback to be called for all incoming commands from {@link MediaController2}s.
+ * <p>
+ * If it's not set, the session will accept all controllers and all incoming commands by
+ * default.
+ */
+ // TODO(jaewan): Can we move this inside of the updatable for default implementation.
+ public static class SessionCallback {
+ /**
+ * Called when a controller is created for this session. Return allowed commands for
+ * controller. By default it allows all connection requests and commands.
+ * <p>
+ * You can reject the connection by return {@code null}. In that case, controller receives
+ * {@link MediaController2.ControllerCallback#onDisconnected()} and cannot be usable.
+ *
+ * @param controller controller information.
+ * @return allowed commands. Can be {@code null} to reject coonnection.
+ */
+ // TODO(jaewan): Change return type. Once we do, null is for reject.
+ public @Nullable CommandGroup onConnect(@NonNull ControllerInfo controller) {
+ CommandGroup commands = new CommandGroup();
+ commands.addAllPredefinedCommands();
+ return commands;
+ }
+
+ /**
+ * Called when a controller is disconnected
+ *
+ * @param controller controller information
+ */
+ public void onDisconnected(@NonNull ControllerInfo controller) { }
+
+ /**
+ * Called when a controller sent a command to the session, and the command will be sent to
+ * the player directly unless you reject the request by {@code false}.
+ *
+ * @param controller controller information.
+ * @param command a command. This method will be called for every single command.
+ * @return {@code true} if you want to accept incoming command. {@code false} otherwise.
+ */
+ // TODO(jaewan): Add more documentations (or make it clear) which commands can be filtered
+ // with this.
+ public boolean onCommandRequest(@NonNull ControllerInfo controller,
+ @NonNull Command command) {
+ return true;
+ }
+
+ /**
+ * Called when a controller set rating on the currently playing contents.
+ *
+ * @param
+ */
+ public void onSetRating(@NonNull ControllerInfo controller, @NonNull Rating2 rating) { }
+
+ /**
+ * Called when a controller sent a custom command.
+ *
+ * @param controller controller information
+ * @param customCommand custom command.
+ * @param args optional arguments
+ * @param cb optional result receiver
+ */
+ public void onCustomCommand(@NonNull ControllerInfo controller,
+ @NonNull Command customCommand, @Nullable Bundle args,
+ @Nullable ResultReceiver cb) { }
+
+ /**
+ * Override to handle requests to prepare for playing a specific mediaId.
+ * During the preparation, a session should not hold audio focus in order to allow other
+ * sessions play seamlessly. The state of playback should be updated to
+ * {@link PlaybackState#STATE_PAUSED} after the preparation is done.
+ * <p>
+ * The playback of the prepared content should start in the later calls of
+ * {@link MediaSession2#play()}.
+ * <p>
+ * Override {@link #onPlayFromMediaId} to handle requests for starting
+ * playback without preparation.
+ */
+ public void onPlayFromMediaId(@NonNull ControllerInfo controller,
+ @NonNull String mediaId, @Nullable Bundle extras) { }
+
+ /**
+ * Override to handle requests to prepare playback from a search query. An empty query
+ * indicates that the app may prepare any music. The implementation should attempt to make a
+ * smart choice about what to play. During the preparation, a session should not hold audio
+ * focus in order to allow other sessions play seamlessly. The state of playback should be
+ * updated to {@link PlaybackState#STATE_PAUSED} after the preparation is done.
+ * <p>
+ * The playback of the prepared content should start in the later calls of
+ * {@link MediaSession2#play()}.
+ * <p>
+ * Override {@link #onPlayFromSearch} to handle requests for starting playback without
+ * preparation.
+ */
+ public void onPlayFromSearch(@NonNull ControllerInfo controller,
+ @NonNull String query, @Nullable Bundle extras) { }
+
+ /**
+ * Override to handle requests to prepare a specific media item represented by a URI.
+ * During the preparation, a session should not hold audio focus in order to allow
+ * other sessions play seamlessly. The state of playback should be updated to
+ * {@link PlaybackState#STATE_PAUSED} after the preparation is done.
+ * <p>
+ * The playback of the prepared content should start in the later calls of
+ * {@link MediaSession2#play()}.
+ * <p>
+ * Override {@link #onPlayFromUri} to handle requests for starting playback without
+ * preparation.
+ */
+ public void onPlayFromUri(@NonNull ControllerInfo controller,
+ @NonNull String uri, @Nullable Bundle extras) { }
+
+ /**
+ * Override to handle requests to play a specific mediaId.
+ */
+ public void onPrepareFromMediaId(@NonNull ControllerInfo controller,
+ @NonNull String mediaId, @Nullable Bundle extras) { }
+
+ /**
+ * Override to handle requests to begin playback from a search query. An
+ * empty query indicates that the app may play any music. The
+ * implementation should attempt to make a smart choice about what to
+ * play.
+ */
+ public void onPrepareFromSearch(@NonNull ControllerInfo controller,
+ @NonNull String query, @Nullable Bundle extras) { }
+
+ /**
+ * Override to handle requests to play a specific media item represented by a URI.
+ */
+ public void prepareFromUri(@NonNull ControllerInfo controller,
+ @NonNull Uri uri, @Nullable Bundle extras) { }
+
+ /**
+ * Called when a controller wants to add a {@link MediaItem2} at the specified position
+ * in the play queue.
+ * <p>
+ * The item from the media controller wouldn't have valid data source descriptor because
+ * it would have been anonymized when it's sent to the remote process.
+ *
+ * @param item The media item to be inserted.
+ * @param index The index at which the item is to be inserted.
+ */
+ public void onAddPlaylistItem(@NonNull ControllerInfo controller,
+ @NonNull MediaItem2 item, int index) { }
+
+ /**
+ * Called when a controller wants to remove the {@link MediaItem2}
+ *
+ * @param item
+ */
+ // Can we do this automatically?
+ public void onRemovePlaylistItem(@NonNull MediaItem2 item) { }
+ };
+
+ /**
+ * Base builder class for MediaSession2 and its subclass.
+ *
+ * @hide
+ */
+ static abstract class BuilderBase
+ <T extends MediaSession2.BuilderBase<T, C>, C extends SessionCallback> {
+ final Context mContext;
+ final MediaPlayerBase mPlayer;
+ String mId;
+ Executor mCallbackExecutor;
+ C mCallback;
+ VolumeProvider mVolumeProvider;
+ int mRatingType;
+ PendingIntent mSessionActivity;
+
+ /**
+ * Constructor.
+ *
+ * @param context a context
+ * @param player a player to handle incoming command from any controller.
+ * @throws IllegalArgumentException if any parameter is null, or the player is a
+ * {@link MediaSession2} or {@link MediaController2}.
+ */
+ // TODO(jaewan): Also need executor
+ public BuilderBase(@NonNull Context context, @NonNull MediaPlayerBase player) {
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ if (player == null) {
+ throw new IllegalArgumentException("player shouldn't be null");
+ }
+ mContext = context;
+ mPlayer = player;
+ // Ensure non-null
+ mId = "";
+ }
+
+ /**
+ * Set volume provider to configure this session to use remote volume handling.
+ * This must be called to receive volume button events, otherwise the system
+ * will adjust the appropriate stream volume for this session's player.
+ * <p>
+ * Set {@code null} to reset.
+ *
+ * @param volumeProvider The provider that will handle volume changes. Can be {@code null}
+ */
+ public T setVolumeProvider(@Nullable VolumeProvider volumeProvider) {
+ mVolumeProvider = volumeProvider;
+ return (T) this;
+ }
+
+ /**
+ * Set the style of rating used by this session. Apps trying to set the
+ * rating should use this style. Must be one of the following:
+ * <ul>
+ * <li>{@link Rating2#RATING_NONE}</li>
+ * <li>{@link Rating2#RATING_3_STARS}</li>
+ * <li>{@link Rating2#RATING_4_STARS}</li>
+ * <li>{@link Rating2#RATING_5_STARS}</li>
+ * <li>{@link Rating2#RATING_HEART}</li>
+ * <li>{@link Rating2#RATING_PERCENTAGE}</li>
+ * <li>{@link Rating2#RATING_THUMB_UP_DOWN}</li>
+ * </ul>
+ */
+ public T setRatingType(@Rating2.Style int type) {
+ mRatingType = type;
+ return (T) this;
+ }
+
+ /**
+ * Set an intent for launching UI for this Session. This can be used as a
+ * quick link to an ongoing media screen. The intent should be for an
+ * activity that may be started using {@link Activity#startActivity(Intent)}.
+ *
+ * @param pi The intent to launch to show UI for this session.
+ */
+ public T setSessionActivity(@Nullable PendingIntent pi) {
+ mSessionActivity = pi;
+ return (T) this;
+ }
+
+ /**
+ * Set ID of the session. If it's not set, an empty string with used to create a session.
+ * <p>
+ * Use this if and only if your app supports multiple playback at the same time and also
+ * wants to provide external apps to have finer controls of them.
+ *
+ * @param id id of the session. Must be unique per package.
+ * @throws IllegalArgumentException if id is {@code null}
+ * @return
+ */
+ public T setId(@NonNull String id) {
+ if (id == null) {
+ throw new IllegalArgumentException("id shouldn't be null");
+ }
+ mId = id;
+ return (T) this;
+ }
+
+ /**
+ * Set {@link SessionCallback}.
+ *
+ * @param executor callback executor
+ * @param callback session callback.
+ * @return
+ */
+ public T setSessionCallback(@NonNull @CallbackExecutor Executor executor,
+ @NonNull C callback) {
+ if (executor == null) {
+ throw new IllegalArgumentException("executor shouldn't be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback shouldn't be null");
+ }
+ mCallbackExecutor = executor;
+ mCallback = callback;
+ return (T) this;
+ }
+
+ /**
+ * Build {@link MediaSession2}.
+ *
+ * @return a new session
+ * @throws IllegalStateException if the session with the same id is already exists for the
+ * package.
+ */
+ public abstract MediaSession2 build() throws IllegalStateException;
+ }
+
+ /**
+ * Builder for {@link MediaSession2}.
+ * <p>
+ * Any incoming event from the {@link MediaController2} will be handled on the thread
+ * that created session with the {@link Builder#build()}.
+ */
+ // TODO(jaewan): Move this to updatable
+ // TODO(jaewan): Add setRatingType()
+ // TODO(jaewan): Add setSessionActivity()
+ public static final class Builder extends BuilderBase<Builder, SessionCallback> {
+ public Builder(Context context, @NonNull MediaPlayerBase player) {
+ super(context, player);
+ }
+
+ @Override
+ public MediaSession2 build() throws IllegalStateException {
+ if (mCallback == null) {
+ mCallback = new SessionCallback();
+ }
+ return new MediaSession2(mContext, mPlayer, mId, mCallbackExecutor, mCallback,
+ mVolumeProvider, mRatingType, mSessionActivity);
+ }
+ }
+
+ /**
+ * Information of a controller.
+ */
+ // TODO(jaewan): Move implementation to the updatable.
+ public static final class ControllerInfo {
+ private final ControllerInfoProvider mProvider;
+
+ /**
+ * @hide
+ */
+ // TODO(jaewan): SystemApi
+ // TODO(jaewan): Also accept componentName to check notificaiton listener.
+ public ControllerInfo(Context context, int uid, int pid, String packageName,
+ IMediaSession2Callback callback) {
+ mProvider = ApiLoader.getProvider(context)
+ .createMediaSession2ControllerInfoProvider(
+ this, context, uid, pid, packageName, callback);
+ }
+
+ /**
+ * @return package name of the controller
+ */
+ public String getPackageName() {
+ return mProvider.getPackageName_impl();
+ }
+
+ /**
+ * @return uid of the controller
+ */
+ public int getUid() {
+ return mProvider.getUid_impl();
+ }
+
+ /**
+ * Return if the controller has granted {@code android.permission.MEDIA_CONTENT_CONTROL} or
+ * has a enabled notification listener so can be trusted to accept connection and incoming
+ * command request.
+ *
+ * @return {@code true} if the controller is trusted.
+ */
+ public boolean isTrusted() {
+ return mProvider.isTrusted_impl();
+ }
+
+ /**
+ * @hide
+ * @return
+ */
+ // TODO(jaewan): SystemApi
+ public ControllerInfoProvider getProvider() {
+ return mProvider;
+ }
+
+ @Override
+ public int hashCode() {
+ return mProvider.hashCode_impl();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ControllerInfo)) {
+ return false;
+ }
+ ControllerInfo other = (ControllerInfo) obj;
+ return mProvider.equals_impl(other.mProvider);
+ }
+
+ @Override
+ public String toString() {
+ // TODO(jaewan): Move this to updatable.
+ return "ControllerInfo {pkg=" + getPackageName() + ", uid=" + getUid() + ", trusted="
+ + isTrusted() + "}";
+ }
+ }
+
+ /**
+ * Button for a {@link Command} that will be shown by the controller.
+ * <p>
+ * It's up to the controller's decision to respect or ignore this customization request.
+ */
+ // TODO(jaewan): Move this to updatable.
+ public static class CommandButton {
+ private static final String KEY_COMMAND
+ = "android.media.media_session2.command_button.command";
+ private static final String KEY_ICON_RES_ID
+ = "android.media.media_session2.command_button.icon_res_id";
+ private static final String KEY_DISPLAY_NAME
+ = "android.media.media_session2.command_button.display_name";
+ private static final String KEY_EXTRA
+ = "android.media.media_session2.command_button.extra";
+ private static final String KEY_ENABLED
+ = "android.media.media_session2.command_button.enabled";
+
+ private Command mCommand;
+ private int mIconResId;
+ private String mDisplayName;
+ private Bundle mExtra;
+ private boolean mEnabled;
+
+ private CommandButton(@Nullable Command command, int iconResId,
+ @Nullable String displayName, Bundle extra, boolean enabled) {
+ mCommand = command;
+ mIconResId = iconResId;
+ mDisplayName = displayName;
+ mExtra = extra;
+ mEnabled = enabled;
+ }
+
+ /**
+ * Get command associated with this button. Can be {@code null} if the button isn't enabled
+ * and only providing placeholder.
+ *
+ * @return command or {@code null}
+ */
+ public @Nullable Command getCommand() {
+ return mCommand;
+ }
+
+ /**
+ * Resource id of the button in this package. Can be {@code 0} if the command is predefined
+ * and custom icon isn't needed.
+ *
+ * @return resource id of the icon. Can be {@code 0}.
+ */
+ public int getIconResId() {
+ return mIconResId;
+ }
+
+ /**
+ * Display name of the button. Can be {@code null} or empty if the command is predefined
+ * and custom name isn't needed.
+ *
+ * @return custom display name. Can be {@code null} or empty.
+ */
+ public @Nullable String getDisplayName() {
+ return mDisplayName;
+ }
+
+ /**
+ * Extra information of the button. It's private information between session and controller.
+ *
+ * @return
+ */
+ public @Nullable Bundle getExtra() {
+ return mExtra;
+ }
+
+ /**
+ * Return whether it's enabled
+ *
+ * @return {@code true} if enabled. {@code false} otherwise.
+ */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * @hide
+ */
+ // TODO(jaewan): @SystemApi
+ public @NonNull Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putBundle(KEY_COMMAND, mCommand.toBundle());
+ bundle.putInt(KEY_ICON_RES_ID, mIconResId);
+ bundle.putString(KEY_DISPLAY_NAME, mDisplayName);
+ bundle.putBundle(KEY_EXTRA, mExtra);
+ bundle.putBoolean(KEY_ENABLED, mEnabled);
+ return bundle;
+ }
+
+ /**
+ * @hide
+ */
+ // TODO(jaewan): @SystemApi
+ public static @Nullable CommandButton fromBundle(Bundle bundle) {
+ Builder builder = new Builder();
+ builder.setCommand(Command.fromBundle(bundle.getBundle(KEY_COMMAND)));
+ builder.setIconResId(bundle.getInt(KEY_ICON_RES_ID, 0));
+ builder.setDisplayName(bundle.getString(KEY_DISPLAY_NAME));
+ builder.setExtra(bundle.getBundle(KEY_EXTRA));
+ builder.setEnabled(bundle.getBoolean(KEY_ENABLED));
+ try {
+ return builder.build();
+ } catch (IllegalStateException e) {
+ // Malformed or version mismatch. Return null for now.
+ return null;
+ }
+ }
+
+ /**
+ * Builder for {@link CommandButton}.
+ */
+ public static class Builder {
+ private Command mCommand;
+ private int mIconResId;
+ private String mDisplayName;
+ private Bundle mExtra;
+ private boolean mEnabled;
+
+ public Builder() {
+ mEnabled = true;
+ }
+
+ public Builder setCommand(Command command) {
+ mCommand = command;
+ return this;
+ }
+
+ public Builder setIconResId(int resId) {
+ mIconResId = resId;
+ return this;
+ }
+
+ public Builder setDisplayName(String displayName) {
+ mDisplayName = displayName;
+ return this;
+ }
+
+ public Builder setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ return this;
+ }
+
+ public Builder setExtra(Bundle extra) {
+ mExtra = extra;
+ return this;
+ }
+
+ public CommandButton build() {
+ if (mEnabled && mCommand == null) {
+ throw new IllegalStateException("Enabled button needs Command"
+ + " for controller to invoke the command");
+ }
+ if (mCommand != null && mCommand.getCommandCode() == COMMAND_CODE_CUSTOM
+ && (mIconResId == 0 || TextUtils.isEmpty(mDisplayName))) {
+ throw new IllegalStateException("Custom commands needs icon and"
+ + " and name to display");
+ }
+ return new CommandButton(mCommand, mIconResId, mDisplayName, mExtra, mEnabled);
+ }
+ }
+ }
+
+ /**
+ * Parameter for the playlist.
+ */
+ // TODO(jaewan): add fromBundle()/toBundle()
+ public static class PlaylistParam {
+ /**
+ * @hide
+ */
+ @IntDef({REPEAT_MODE_NONE, REPEAT_MODE_ONE, REPEAT_MODE_ALL,
+ REPEAT_MODE_GROUP})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface RepeatMode {}
+
+ /**
+ * Playback will be stopped at the end of the playing media list.
+ */
+ public static final int REPEAT_MODE_NONE = 0;
+
+ /**
+ * Playback of the current playing media item will be repeated.
+ */
+ public static final int REPEAT_MODE_ONE = 1;
+
+ /**
+ * Playing media list will be repeated.
+ */
+ public static final int REPEAT_MODE_ALL = 2;
+
+ /**
+ * Playback of the playing media group will be repeated.
+ * A group is a logical block of media items which is specified in the section 5.7 of the
+ * Bluetooth AVRCP 1.6.
+ */
+ public static final int REPEAT_MODE_GROUP = 3;
+
+ /**
+ * @hide
+ */
+ @IntDef({SHUFFLE_MODE_NONE, SHUFFLE_MODE_ALL, SHUFFLE_MODE_GROUP})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ShuffleMode {}
+
+ /**
+ * Media list will be played in order.
+ */
+ public static final int SHUFFLE_MODE_NONE = 0;
+
+ /**
+ * Media list will be played in shuffled order.
+ */
+ public static final int SHUFFLE_MODE_ALL = 1;
+
+ /**
+ * Media group will be played in shuffled order.
+ * A group is a logical block of media items which is specified in the section 5.7 of the
+ * Bluetooth AVRCP 1.6.
+ */
+ public static final int SHUFFLE_MODE_GROUP = 2;
+
+ private @RepeatMode int mRepeatMode;
+ private @ShuffleMode int mShuffleMode;
+
+ private MediaMetadata2 mPlaylistMetadata;
+
+ public PlaylistParam(@RepeatMode int repeatMode, @ShuffleMode int shuffleMode,
+ @Nullable MediaMetadata2 playlistMetadata) {
+ mRepeatMode = repeatMode;
+ mShuffleMode = shuffleMode;
+ mPlaylistMetadata = playlistMetadata;
+ }
+
+ public @RepeatMode int getRepeatMode() {
+ return mRepeatMode;
+ }
+
+ public @ShuffleMode int getShuffleMode() {
+ return mShuffleMode;
+ }
+
+ public MediaMetadata2 getPlaylistMetadata() {
+ return mPlaylistMetadata;
+ }
+ }
+
+ /**
+ * Constructor is hidden and apps can only instantiate indirectly through {@link Builder}.
+ * <p>
+ * This intended behavior and here's the reasons.
+ * 1. Prevent multiple sessions with the same tag in a media app.
+ * Whenever it happens only one session was properly setup and others were all dummies.
+ * Android framework couldn't find the right session to dispatch media key event.
+ * 2. Simplify session's lifecycle.
+ * {@link MediaSession} can be available after all of {@link MediaSession#setFlags(int)},
+ * {@link MediaSession#setCallback(Callback)}, and
+ * {@link MediaSession#setActive(boolean)}. It was common for an app to omit one, so
+ * framework had to add heuristics to figure out if an app is
+ * @hide
+ */
+ MediaSession2(Context context, MediaPlayerBase player, String id, Executor callbackExecutor,
+ SessionCallback callback, VolumeProvider volumeProvider, int ratingType,
+ PendingIntent sessionActivity) {
+ super();
+ mProvider = createProvider(context, player, id, callbackExecutor, callback,
+ volumeProvider, ratingType, sessionActivity);
+ }
+
+ MediaSession2Provider createProvider(Context context, MediaPlayerBase player, String id,
+ Executor callbackExecutor, SessionCallback callback, VolumeProvider volumeProvider,
+ int ratingType, PendingIntent sessionActivity) {
+ return ApiLoader.getProvider(context)
+ .createMediaSession2(this, context, player, id, callbackExecutor,
+ callback, volumeProvider, ratingType, sessionActivity);
+ }
+
+ /**
+ * @hide
+ */
+ // TODO(jaewan): SystemApi
+ public MediaSession2Provider getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * Set the underlying {@link MediaPlayerBase} for this session to dispatch incoming event to.
+ * Events from the {@link MediaController2} will be sent directly to the underlying
+ * player on the {@link Handler} where the session is created on.
+ * <p>
+ * If the new player is successfully set, {@link PlaybackListener}
+ * will be called to tell the current playback state of the new player.
+ * <p>
+ * You can also specify a volume provider. If so, playback in the player is considered as
+ * remote playback.
+ *
+ * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
+ * @throws IllegalArgumentException if the player is {@code null}.
+ */
+ public void setPlayer(@NonNull MediaPlayerBase player) {
+ mProvider.setPlayer_impl(player);
+ }
+
+ /**
+ * Set the underlying {@link MediaPlayerBase} with the volume provider for remote playback.
+ *
+ * @param player a {@link MediaPlayerBase} that handles actual media playback in your app.
+ * @param volumeProvider a volume provider
+ * @see #setPlayer(MediaPlayerBase)
+ * @see Builder#setVolumeProvider(VolumeProvider)
+ * @throws IllegalArgumentException if a parameter is {@code null}.
+ */
+ public void setPlayer(@NonNull MediaPlayerBase player, @NonNull VolumeProvider volumeProvider)
+ throws IllegalArgumentException {
+ mProvider.setPlayer_impl(player, volumeProvider);
+ }
+
+ @Override
+ public void close() {
+ mProvider.close_impl();
+ }
+
+ /**
+ * @return player
+ */
+ public @Nullable MediaPlayerBase getPlayer() {
+ return mProvider.getPlayer_impl();
+ }
+
+ /**
+ * Returns the {@link SessionToken2} for creating {@link MediaController2}.
+ */
+ public @NonNull
+ SessionToken2 getToken() {
+ return mProvider.getToken_impl();
+ }
+
+ public @NonNull List<ControllerInfo> getConnectedControllers() {
+ return mProvider.getConnectedControllers_impl();
+ }
+
+ /**
+ * Sets the {@link AudioAttributes} to be used during the playback of the video.
+ *
+ * @param attributes non-null <code>AudioAttributes</code>.
+ */
+ public void setAudioAttributes(@NonNull AudioAttributes attributes) {
+ mProvider.setAudioAttributes_impl(attributes);
+ }
+
+ /**
+ * Sets which type of audio focus will be requested during the playback, or configures playback
+ * to not request audio focus. Valid values for focus requests are
+ * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+ * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+ * requested when playback starts. You can for instance use this when playing a silent animation
+ * through this class, and you don't want to affect other audio applications playing in the
+ * background.
+ *
+ * @param focusGain the type of audio focus gain that will be requested, or
+ * {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during
+ * playback.
+ */
+ public void setAudioFocusRequest(int focusGain) {
+ mProvider.setAudioFocusRequest_impl(focusGain);
+ }
+
+ /**
+ * Sets ordered list of {@link CommandButton} for controllers to build UI with it.
+ * <p>
+ * It's up to controller's decision how to represent the layout in its own UI.
+ * Here's the same way
+ * (layout[i] means a CommandButton at index i in the given list)
+ * For 5 icons row
+ * layout[3] layout[1] layout[0] layout[2] layout[4]
+ * For 3 icons row
+ * layout[1] layout[0] layout[2]
+ * For 5 icons row with overflow icon (can show +5 extra buttons with overflow button)
+ * expanded row: layout[5] layout[6] layout[7] layout[8] layout[9]
+ * main row: layout[3] layout[1] layout[0] layout[2] layout[4]
+ * <p>
+ * This API can be called in the {@link SessionCallback#onConnect(ControllerInfo)}.
+ *
+ * @param controller controller to specify layout.
+ * @param layout oredered list of layout.
+ */
+ public void setCustomLayout(@NonNull ControllerInfo controller,
+ @NonNull List<CommandButton> layout) {
+ mProvider.setCustomLayout_impl(controller, layout);
+ }
+
+ /**
+ * Set the new allowed command group for the controller
+ *
+ * @param controller controller to change allowed commands
+ * @param commands new allowed commands
+ */
+ public void setAllowedCommands(@NonNull ControllerInfo controller,
+ @NonNull CommandGroup commands) {
+ mProvider.setAllowedCommands_impl(controller, commands);
+ }
+
+ /**
+ * Notify changes in metadata of previously set playlist. Controller will get the whole set of
+ * playlist again.
+ */
+ public void notifyMetadataChanged() {
+ mProvider.notifyMetadataChanged_impl();
+ }
+
+ /**
+ * Send custom command to all connected controllers.
+ *
+ * @param command a command
+ * @param args optional argument
+ */
+ public void sendCustomCommand(@NonNull Command command, @Nullable Bundle args) {
+ mProvider.sendCustomCommand_impl(command, args);
+ }
+
+ /**
+ * Send custom command to a specific controller.
+ *
+ * @param command a command
+ * @param args optional argument
+ * @param receiver result receiver for the session
+ */
+ public void sendCustomCommand(@NonNull ControllerInfo controller, @NonNull Command command,
+ @Nullable Bundle args, @Nullable ResultReceiver receiver) {
+ // Equivalent to the MediaController.sendCustomCommand(Action action, ResultReceiver r);
+ mProvider.sendCustomCommand_impl(controller, command, args, receiver);
+ }
+
+ /**
+ * Play playback
+ */
+ public void play() {
+ mProvider.play_impl();
+ }
+
+ /**
+ * Pause playback
+ */
+ public void pause() {
+ mProvider.pause_impl();
+ }
+
+ /**
+ * Stop playback
+ */
+ public void stop() {
+ mProvider.stop_impl();
+ }
+
+ /**
+ * Rewind playback
+ */
+ public void skipToPrevious() {
+ mProvider.skipToPrevious_impl();
+ }
+
+ /**
+ * Rewind playback
+ */
+ public void skipToNext() {
+ mProvider.skipToNext_impl();
+ }
+
+ /**
+ * Request that the player prepare its playback. In other words, other sessions can continue
+ * to play during the preparation of this session. This method can be used to speed up the
+ * start of the playback. Once the preparation is done, the session will change its playback
+ * state to {@link PlaybackState#STATE_PAUSED}. Afterwards, {@link #play} can be called to
+ * start playback.
+ */
+ public void prepare() {
+ mProvider.prepare_impl();
+ }
+
+ /**
+ * Start fast forwarding. If playback is already fast forwarding this may increase the rate.
+ */
+ public void fastForward() {
+ mProvider.fastForward_impl();
+ }
+
+ /**
+ * Start rewinding. If playback is already rewinding this may increase the rate.
+ */
+ public void rewind() {
+ mProvider.rewind_impl();
+ }
+
+ /**
+ * Move to a new location in the media stream.
+ *
+ * @param pos Position to move to, in milliseconds.
+ */
+ public void seekTo(long pos) {
+ mProvider.seekTo_impl(pos);
+ }
+
+ /**
+ * Sets the index of current DataSourceDesc in the play list to be played.
+ *
+ * @param index the index of DataSourceDesc in the play list you want to play
+ * @throws IllegalArgumentException if the play list is null
+ * @throws NullPointerException if index is outside play list range
+ */
+ public void setCurrentPlaylistItem(int index) {
+ mProvider.setCurrentPlaylistItem_impl(index);
+ }
+
+ /**
+ * @hide
+ */
+ public void skipForward() {
+ // To match with KEYCODE_MEDIA_SKIP_FORWARD
+ }
+
+ /**
+ * @hide
+ */
+ public void skipBackward() {
+ // To match with KEYCODE_MEDIA_SKIP_BACKWARD
+ }
+
+ public void setPlaylist(@NonNull List<MediaItem2> playlist, @NonNull PlaylistParam param) {
+ mProvider.setPlaylist_impl(playlist, param);
+ }
+}
diff --git a/android/media/MediaSession2Test.java b/android/media/MediaSession2Test.java
new file mode 100644
index 00000000..045dcd5a
--- /dev/null
+++ b/android/media/MediaSession2Test.java
@@ -0,0 +1,273 @@
+/*
+ * Copyright 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.media;
+
+import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaSession2.Builder;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.session.PlaybackState;
+import android.os.Process;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.util.ArrayList;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static android.media.TestUtils.createPlaybackState;
+import static org.junit.Assert.*;
+
+/**
+ * Tests {@link MediaSession2}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class MediaSession2Test extends MediaSession2TestBase {
+ private static final String TAG = "MediaSession2Test";
+
+ private MediaSession2 mSession;
+ private MockPlayer mPlayer;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ sHandler.postAndSync(() -> {
+ mPlayer = new MockPlayer(0);
+ mSession = new MediaSession2.Builder(mContext, mPlayer).build();
+ });
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ });
+ }
+
+ @Test
+ public void testBuilder() throws Exception {
+ try {
+ MediaSession2.Builder builder = new Builder(mContext, null);
+ fail("null player shouldn't be allowed");
+ } catch (IllegalArgumentException e) {
+ // expected. pass-through
+ }
+ MediaSession2.Builder builder = new Builder(mContext, mPlayer);
+ try {
+ builder.setId(null);
+ fail("null id shouldn't be allowed");
+ } catch (IllegalArgumentException e) {
+ // expected. pass-through
+ }
+ }
+
+ @Test
+ public void testSetPlayer() throws Exception {
+ sHandler.postAndSync(() -> {
+ MockPlayer player = new MockPlayer(0);
+ // Test if setPlayer doesn't crash with various situations.
+ mSession.setPlayer(mPlayer);
+ mSession.setPlayer(player);
+ mSession.close();
+ });
+ }
+
+ @Test
+ public void testPlay() throws Exception {
+ sHandler.postAndSync(() -> {
+ mSession.play();
+ assertTrue(mPlayer.mPlayCalled);
+ });
+ }
+
+ @Test
+ public void testPause() throws Exception {
+ sHandler.postAndSync(() -> {
+ mSession.pause();
+ assertTrue(mPlayer.mPauseCalled);
+ });
+ }
+
+ @Test
+ public void testStop() throws Exception {
+ sHandler.postAndSync(() -> {
+ mSession.stop();
+ assertTrue(mPlayer.mStopCalled);
+ });
+ }
+
+ @Test
+ public void testSkipToNext() throws Exception {
+ sHandler.postAndSync(() -> {
+ mSession.skipToNext();
+ assertTrue(mPlayer.mSkipToNextCalled);
+ });
+ }
+
+ @Test
+ public void testSkipToPrevious() throws Exception {
+ sHandler.postAndSync(() -> {
+ mSession.skipToPrevious();
+ assertTrue(mPlayer.mSkipToPreviousCalled);
+ });
+ }
+
+ @Test
+ public void testPlaybackStateChangedListener() throws InterruptedException {
+ // TODO(jaewan): Add equivalent tests again
+ /*
+ final CountDownLatch latch = new CountDownLatch(2);
+ final MockPlayer player = new MockPlayer(0);
+ final PlaybackListener listener = (state) -> {
+ assertEquals(sHandler.getLooper(), Looper.myLooper());
+ assertNotNull(state);
+ switch ((int) latch.getCount()) {
+ case 2:
+ assertEquals(PlaybackState.STATE_PLAYING, state.getState());
+ break;
+ case 1:
+ assertEquals(PlaybackState.STATE_PAUSED, state.getState());
+ break;
+ case 0:
+ fail();
+ }
+ latch.countDown();
+ };
+ player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PLAYING));
+ sHandler.postAndSync(() -> {
+ mSession.addPlaybackListener(listener, sHandler);
+ // When the player is set, listeners will be notified about the player's current state.
+ mSession.setPlayer(player);
+ });
+ player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
+ assertTrue(latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ */
+ }
+
+ @Test
+ public void testBadPlayer() throws InterruptedException {
+ // TODO(jaewan): Add equivalent tests again
+ /*
+ final CountDownLatch latch = new CountDownLatch(3); // expected call + 1
+ final BadPlayer player = new BadPlayer(0);
+ sHandler.postAndSync(() -> {
+ mSession.addPlaybackListener((state) -> {
+ // This will be called for every setPlayer() calls, but no more.
+ assertNull(state);
+ latch.countDown();
+ }, sHandler);
+ mSession.setPlayer(player);
+ mSession.setPlayer(mPlayer);
+ });
+ player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_PAUSED));
+ assertFalse(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ */
+ }
+
+ private static class BadPlayer extends MockPlayer {
+ public BadPlayer(int count) {
+ super(count);
+ }
+
+ @Override
+ public void removePlaybackListener(@NonNull PlaybackListener listener) {
+ // No-op. This bad player will keep push notification to the listener that is previously
+ // registered by session.setPlayer().
+ }
+ }
+
+ @Test
+ public void testOnCommandCallback() throws InterruptedException {
+ final MockOnCommandCallback callback = new MockOnCommandCallback();
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mPlayer = new MockPlayer(1);
+ mSession = new MediaSession2.Builder(mContext, mPlayer)
+ .setSessionCallback(sHandlerExecutor, callback).build();
+ });
+ MediaController2 controller = createController(mSession.getToken());
+ controller.pause();
+ assertFalse(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertFalse(mPlayer.mPauseCalled);
+ assertEquals(1, callback.commands.size());
+ assertEquals(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE,
+ (long) callback.commands.get(0).getCommandCode());
+ controller.skipToNext();
+ assertTrue(mPlayer.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(mPlayer.mSkipToNextCalled);
+ assertFalse(mPlayer.mPauseCalled);
+ assertEquals(2, callback.commands.size());
+ assertEquals(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM,
+ (long) callback.commands.get(1).getCommandCode());
+ }
+
+ @Test
+ public void testOnConnectCallback() throws InterruptedException {
+ final MockOnConnectCallback sessionCallback = new MockOnConnectCallback();
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext, mPlayer)
+ .setSessionCallback(sHandlerExecutor, sessionCallback).build();
+ });
+ MediaController2 controller =
+ createController(mSession.getToken(), false, null);
+ assertNotNull(controller);
+ waitForConnect(controller, false);
+ waitForDisconnect(controller, true);
+ }
+
+ public class MockOnConnectCallback extends SessionCallback {
+ @Override
+ public MediaSession2.CommandGroup onConnect(ControllerInfo controllerInfo) {
+ if (Process.myUid() != controllerInfo.getUid()) {
+ return null;
+ }
+ assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
+ assertEquals(Process.myUid(), controllerInfo.getUid());
+ assertFalse(controllerInfo.isTrusted());
+ // Reject all
+ return null;
+ }
+ }
+
+ public class MockOnCommandCallback extends SessionCallback {
+ public final ArrayList<MediaSession2.Command> commands = new ArrayList<>();
+
+ @Override
+ public boolean onCommandRequest(ControllerInfo controllerInfo, MediaSession2.Command command) {
+ assertEquals(mContext.getPackageName(), controllerInfo.getPackageName());
+ assertEquals(Process.myUid(), controllerInfo.getUid());
+ assertFalse(controllerInfo.isTrusted());
+ commands.add(command);
+ if (command.getCommandCode() == MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE) {
+ return false;
+ }
+ return true;
+ }
+ }
+}
diff --git a/android/media/MediaSession2TestBase.java b/android/media/MediaSession2TestBase.java
new file mode 100644
index 00000000..96afcb90
--- /dev/null
+++ b/android/media/MediaSession2TestBase.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 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.media;
+
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import android.content.Context;
+import android.media.MediaController2.ControllerCallback;
+import android.media.MediaSession2.CommandGroup;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.support.annotation.CallSuper;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.test.InstrumentationRegistry;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+
+/**
+ * Base class for session test.
+ */
+abstract class MediaSession2TestBase {
+ // Expected success
+ static final int WAIT_TIME_MS = 1000;
+
+ // Expected timeout
+ static final int TIMEOUT_MS = 500;
+
+ static TestUtils.SyncHandler sHandler;
+ static Executor sHandlerExecutor;
+
+ Context mContext;
+ private List<MediaController2> mControllers = new ArrayList<>();
+
+ interface TestControllerInterface {
+ ControllerCallback getCallback();
+ }
+
+ interface TestControllerCallbackInterface {
+ // Currently empty. Add methods in ControllerCallback/BrowserCallback that you want to test.
+
+ // Browser specific callbacks
+ default void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra) {}
+ }
+
+ interface WaitForConnectionInterface {
+ void waitForConnect(boolean expect) throws InterruptedException;
+ void waitForDisconnect(boolean expect) throws InterruptedException;
+ }
+
+ @BeforeClass
+ public static void setUpThread() {
+ if (sHandler == null) {
+ HandlerThread handlerThread = new HandlerThread("MediaSession2TestBase");
+ handlerThread.start();
+ sHandler = new TestUtils.SyncHandler(handlerThread.getLooper());
+ sHandlerExecutor = (runnable) -> {
+ sHandler.post(runnable);
+ };
+ }
+ }
+
+ @AfterClass
+ public static void cleanUpThread() {
+ if (sHandler != null) {
+ sHandler.getLooper().quitSafely();
+ sHandler = null;
+ sHandlerExecutor = null;
+ }
+ }
+
+ @CallSuper
+ public void setUp() throws Exception {
+ mContext = InstrumentationRegistry.getTargetContext();
+ }
+
+ @CallSuper
+ public void cleanUp() throws Exception {
+ for (int i = 0; i < mControllers.size(); i++) {
+ mControllers.get(i).close();
+ }
+ }
+
+ final MediaController2 createController(SessionToken2 token) throws InterruptedException {
+ return createController(token, true, null);
+ }
+
+ final MediaController2 createController(@NonNull SessionToken2 token,
+ boolean waitForConnect, @Nullable TestControllerCallbackInterface callback)
+ throws InterruptedException {
+ TestControllerInterface instance = onCreateController(token, callback);
+ if (!(instance instanceof MediaController2)) {
+ throw new RuntimeException("Test has a bug. Expected MediaController2 but returned "
+ + instance);
+ }
+ MediaController2 controller = (MediaController2) instance;
+ mControllers.add(controller);
+ if (waitForConnect) {
+ waitForConnect(controller, true);
+ }
+ return controller;
+ }
+
+ private static WaitForConnectionInterface getWaitForConnectionInterface(
+ MediaController2 controller) {
+ if (!(controller instanceof TestControllerInterface)) {
+ throw new RuntimeException("Test has a bug. Expected controller implemented"
+ + " TestControllerInterface but got " + controller);
+ }
+ ControllerCallback callback = ((TestControllerInterface) controller).getCallback();
+ if (!(callback instanceof WaitForConnectionInterface)) {
+ throw new RuntimeException("Test has a bug. Expected controller with callback "
+ + " implemented WaitForConnectionInterface but got " + controller);
+ }
+ return (WaitForConnectionInterface) callback;
+ }
+
+ public static void waitForConnect(MediaController2 controller, boolean expected)
+ throws InterruptedException {
+ getWaitForConnectionInterface(controller).waitForConnect(expected);
+ }
+
+ public static void waitForDisconnect(MediaController2 controller, boolean expected)
+ throws InterruptedException {
+ getWaitForConnectionInterface(controller).waitForDisconnect(expected);
+ }
+
+ TestControllerInterface onCreateController(@NonNull SessionToken2 token,
+ @NonNull TestControllerCallbackInterface callback) {
+ return new TestMediaController(mContext, token, new TestControllerCallback(callback));
+ }
+
+ public static class TestControllerCallback extends MediaController2.ControllerCallback
+ implements WaitForConnectionInterface {
+ public final TestControllerCallbackInterface mCallbackProxy;
+ public final CountDownLatch connectLatch = new CountDownLatch(1);
+ public final CountDownLatch disconnectLatch = new CountDownLatch(1);
+
+ TestControllerCallback(TestControllerCallbackInterface callbackProxy) {
+ mCallbackProxy = callbackProxy;
+ }
+
+ @CallSuper
+ @Override
+ public void onConnected(CommandGroup commands) {
+ super.onConnected(commands);
+ connectLatch.countDown();
+ }
+
+ @CallSuper
+ @Override
+ public void onDisconnected() {
+ super.onDisconnected();
+ disconnectLatch.countDown();
+ }
+
+ @Override
+ public void waitForConnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(connectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(connectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+
+ @Override
+ public void waitForDisconnect(boolean expect) throws InterruptedException {
+ if (expect) {
+ assertTrue(disconnectLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ } else {
+ assertFalse(disconnectLatch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+ }
+
+ public class TestMediaController extends MediaController2 implements TestControllerInterface {
+ private final ControllerCallback mCallback;
+
+ public TestMediaController(@NonNull Context context, @NonNull SessionToken2 token,
+ @NonNull ControllerCallback callback) {
+ super(context, token, callback, sHandlerExecutor);
+ mCallback = callback;
+ }
+
+ @Override
+ public ControllerCallback getCallback() {
+ return mCallback;
+ }
+ }
+}
diff --git a/android/media/MediaSessionManager_MediaSession2.java b/android/media/MediaSessionManager_MediaSession2.java
new file mode 100644
index 00000000..192cbc2b
--- /dev/null
+++ b/android/media/MediaSessionManager_MediaSession2.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright 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.media;
+
+import android.content.Context;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static android.media.TestUtils.createPlaybackState;
+import static org.junit.Assert.*;
+
+/**
+ * Tests {@link MediaSessionManager} with {@link MediaSession2} specific APIs.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+@Ignore
+// TODO(jaewan): Reenable test when the media session service detects newly installed sesison
+// service app.
+public class MediaSessionManager_MediaSession2 extends MediaSession2TestBase {
+ private static final String TAG = "MediaSessionManager_MediaSession2";
+
+ private MediaSessionManager mManager;
+ private MediaSession2 mSession;
+
+ @Before
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ mManager = (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
+
+ // Specify TAG here so {@link MediaSession2.getInstance()} doesn't complaint about
+ // per test thread differs across the {@link MediaSession2} with the same TAG.
+ final MockPlayer player = new MockPlayer(1);
+ sHandler.postAndSync(() -> {
+ mSession = new MediaSession2.Builder(mContext, player).setId(TAG).build();
+ });
+ ensureChangeInSession();
+ }
+
+ @After
+ @Override
+ public void cleanUp() throws Exception {
+ super.cleanUp();
+ sHandler.removeCallbacksAndMessages(null);
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ });
+ }
+
+ // TODO(jaewan): Make this host-side test to see per-user behavior.
+ @Test
+ public void testGetMediaSession2Tokens_hasMediaController() throws InterruptedException {
+ final MockPlayer player = (MockPlayer) mSession.getPlayer();
+ player.notifyPlaybackState(createPlaybackState(PlaybackState.STATE_STOPPED));
+
+ MediaController2 controller = null;
+ List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+ assertNotNull(tokens);
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (mContext.getPackageName().equals(token.getPackageName())
+ && TAG.equals(token.getId())) {
+ assertNotNull(token.getSessionBinder());
+ assertNull(controller);
+ controller = createController(token);
+ }
+ }
+ assertNotNull(controller);
+
+ // Test if the found controller is correct one.
+ assertEquals(PlaybackState.STATE_STOPPED, controller.getPlaybackState().getState());
+ controller.play();
+
+ assertTrue(player.mCountDownLatch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ assertTrue(player.mPlayCalled);
+ }
+
+ /**
+ * Test if server recognizes session even if session refuses the connection from server.
+ *
+ * @throws InterruptedException
+ */
+ @Test
+ public void testGetSessionTokens_sessionRejected() throws InterruptedException {
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ mSession = new MediaSession2.Builder(mContext, new MockPlayer(0)).setId(TAG)
+ .setSessionCallback(sHandlerExecutor, new SessionCallback() {
+ @Override
+ public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
+ // Reject all connection request.
+ return null;
+ }
+ }).build();
+ });
+ ensureChangeInSession();
+
+ boolean foundSession = false;
+ List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+ assertNotNull(tokens);
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (mContext.getPackageName().equals(token.getPackageName())
+ && TAG.equals(token.getId())) {
+ assertFalse(foundSession);
+ foundSession = true;
+ }
+ }
+ assertTrue(foundSession);
+ }
+
+ @Test
+ public void testGetMediaSession2Tokens_playerRemoved() throws InterruptedException {
+ // Release
+ sHandler.postAndSync(() -> {
+ mSession.close();
+ });
+ ensureChangeInSession();
+
+ // When the mSession's player becomes null, it should lose binder connection between server.
+ // So server will forget the session.
+ List<SessionToken2> tokens = mManager.getActiveSessionTokens();
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ assertFalse(mContext.getPackageName().equals(token.getPackageName())
+ && TAG.equals(token.getId()));
+ }
+ }
+
+ @Test
+ public void testGetMediaSessionService2Token() throws InterruptedException {
+ boolean foundTestSessionService = false;
+ boolean foundTestLibraryService = false;
+ List<SessionToken2> tokens = mManager.getSessionServiceTokens();
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (mContext.getPackageName().equals(token.getPackageName())
+ && MockMediaSessionService2.ID.equals(token.getId())) {
+ assertFalse(foundTestSessionService);
+ assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+ assertNull(token.getSessionBinder());
+ foundTestSessionService = true;
+ } else if (mContext.getPackageName().equals(token.getPackageName())
+ && MockMediaLibraryService2.ID.equals(token.getId())) {
+ assertFalse(foundTestLibraryService);
+ assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
+ assertNull(token.getSessionBinder());
+ foundTestLibraryService = true;
+ }
+ }
+ assertTrue(foundTestSessionService);
+ assertTrue(foundTestLibraryService);
+ }
+
+ @Test
+ public void testGetAllSessionTokens() throws InterruptedException {
+ boolean foundTestSession = false;
+ boolean foundTestSessionService = false;
+ boolean foundTestLibraryService = false;
+ List<SessionToken2> tokens = mManager.getAllSessionTokens();
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (!mContext.getPackageName().equals(token.getPackageName())) {
+ continue;
+ }
+ switch (token.getId()) {
+ case TAG:
+ assertFalse(foundTestSession);
+ foundTestSession = true;
+ break;
+ case MockMediaSessionService2.ID:
+ assertFalse(foundTestSessionService);
+ foundTestSessionService = true;
+ assertEquals(SessionToken2.TYPE_SESSION_SERVICE, token.getType());
+ break;
+ case MockMediaLibraryService2.ID:
+ assertFalse(foundTestLibraryService);
+ assertEquals(SessionToken2.TYPE_LIBRARY_SERVICE, token.getType());
+ foundTestLibraryService = true;
+ break;
+ default:
+ fail("Unexpected session " + token + " exists in the package");
+ }
+ }
+ assertTrue(foundTestSession);
+ assertTrue(foundTestSessionService);
+ assertTrue(foundTestLibraryService);
+ }
+
+ // Ensures if the session creation/release is notified to the server.
+ private void ensureChangeInSession() throws InterruptedException {
+ // TODO(jaewan): Wait by listener.
+ Thread.sleep(WAIT_TIME_MS);
+ }
+}
diff --git a/android/media/MediaSessionService2.java b/android/media/MediaSessionService2.java
new file mode 100644
index 00000000..19814f04
--- /dev/null
+++ b/android/media/MediaSessionService2.java
@@ -0,0 +1,247 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Notification;
+import android.app.Service;
+import android.content.Intent;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.session.PlaybackState;
+import android.media.update.ApiLoader;
+import android.media.update.MediaSessionService2Provider;
+import android.os.IBinder;
+
+/**
+ * Base class for media session services, which is the service version of the {@link MediaSession2}.
+ * <p>
+ * It's highly recommended for an app to use this instead of {@link MediaSession2} if it wants
+ * to keep media playback in the background.
+ * <p>
+ * Here's the benefits of using {@link MediaSessionService2} instead of
+ * {@link MediaSession2}.
+ * <ul>
+ * <li>Another app can know that your app supports {@link MediaSession2} even when your app
+ * isn't running.
+ * <li>Another app can start playback of your app even when your app isn't running.
+ * </ul>
+ * For example, user's voice command can start playback of your app even when it's not running.
+ * <p>
+ * To extend this class, adding followings directly to your {@code AndroidManifest.xml}.
+ * <pre>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.media.MediaSessionService2" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;/service&gt;</pre>
+ * <p>
+ * A {@link MediaSessionService2} is another form of {@link MediaSession2}. IDs shouldn't
+ * be shared between the {@link MediaSessionService2} and {@link MediaSession2}. By
+ * default, an empty string will be used for ID of the service. If you want to specify an ID,
+ * declare metadata in the manifest as follows.
+ * <pre>
+ * &lt;service android:name="component_name_of_your_implementation" &gt;
+ * &lt;intent-filter&gt;
+ * &lt;action android:name="android.media.MediaSessionService2" /&gt;
+ * &lt;/intent-filter&gt;
+ * &lt;meta-data android:name="android.media.session"
+ * android:value="session_id"/&gt;
+ * &lt;/service&gt;</pre>
+ * <p>
+ * It's recommended for an app to have a single {@link MediaSessionService2} declared in the
+ * manifest. Otherwise, your app might be shown twice in the list of the Auto/Wearable, or another
+ * app fails to pick the right session service when it wants to start the playback this app.
+ * <p>
+ * If there's conflicts with the session ID among the services, services wouldn't be available for
+ * any controllers.
+ * <p>
+ * Topic covered here:
+ * <ol>
+ * <li><a href="#ServiceLifecycle">Service Lifecycle</a>
+ * <li><a href="#Permissions">Permissions</a>
+ * </ol>
+ * <div class="special reference">
+ * <a name="ServiceLifecycle"></a>
+ * <h3>Service Lifecycle</h3>
+ * <p>
+ * Session service is bounded service. When a {@link MediaController2} is created for the
+ * session service, the controller binds to the session service. {@link #onCreateSession(String)}
+ * may be called after the {@link #onCreate} if the service hasn't created yet.
+ * <p>
+ * After the binding, session's {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)}
+ * will be called to accept or reject connection request from a controller. If the connection is
+ * rejected, the controller will unbind. If it's accepted, the controller will be available to use
+ * and keep binding.
+ * <p>
+ * When playback is started for this session service, {@link #onUpdateNotification(PlaybackState)}
+ * is called and service would become a foreground service. It's needed to keep playback after the
+ * controller is destroyed. The session service becomes background service when the playback is
+ * stopped.
+ * <a name="Permissions"></a>
+ * <h3>Permissions</h3>
+ * <p>
+ * Any app can bind to the session service with controller, but the controller can be used only if
+ * the session service accepted the connection request through
+ * {@link MediaSession2.SessionCallback#onConnect(ControllerInfo)}.
+ *
+ * @hide
+ */
+// TODO(jaewan): Unhide
+// TODO(jaewan): Can we clean up sessions in onDestroy() automatically instead?
+// What about currently running SessionCallback when the onDestroy() is called?
+// TODO(jaewan): Protect this with system|privilleged permission - Q.
+// TODO(jaewan): Add permission check for the service to know incoming connection request.
+// Follow-up questions: What about asking a XML for list of white/black packages for
+// allowing enumeration?
+// We can read the information even when the service is started,
+// so SessionManager.getXXXXService() can only return apps
+// TODO(jaewan): Will be the black/white listing persistent?
+// In other words, can we cache the rejection?
+public abstract class MediaSessionService2 extends Service {
+ private final MediaSessionService2Provider mProvider;
+
+ /**
+ * This is the interface name that a service implementing a session service should say that it
+ * support -- that is, this is the action it uses for its intent filter.
+ */
+ public static final String SERVICE_INTERFACE = "android.media.MediaSessionService2";
+
+ /**
+ * Name under which a MediaSessionService2 component publishes information about itself.
+ * This meta-data must provide a string value for the ID.
+ */
+ public static final String SERVICE_META_DATA = "android.media.session";
+
+ public MediaSessionService2() {
+ super();
+ mProvider = createProvider();
+ }
+
+ MediaSessionService2Provider createProvider() {
+ return ApiLoader.getProvider(this).createMediaSessionService2(this);
+ }
+
+ /**
+ * Default implementation for {@link MediaSessionService2} to initialize session service.
+ * <p>
+ * Override this method if you need your own initialization. Derived classes MUST call through
+ * to the super class's implementation of this method.
+ */
+ @CallSuper
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mProvider.onCreate_impl();
+ }
+
+ /**
+ * Called when another app requested to start this service to get {@link MediaSession2}.
+ * <p>
+ * Session service will accept or reject the connection with the
+ * {@link MediaSession2.SessionCallback} in the created session.
+ * <p>
+ * Service wouldn't run if {@code null} is returned or session's ID doesn't match with the
+ * expected ID that you've specified through the AndroidManifest.xml.
+ * <p>
+ * This method will be called on the main thread.
+ *
+ * @param sessionId session id written in the AndroidManifest.xml.
+ * @return a new session
+ * @see MediaSession2.Builder
+ * @see #getSession()
+ */
+ public @NonNull abstract MediaSession2 onCreateSession(String sessionId);
+
+ /**
+ * Called when the playback state of this session is changed, and notification needs update.
+ * Override this method to show your own notification UI.
+ * <p>
+ * With the notification returned here, the service become foreground service when the playback
+ * is started. It becomes background service after the playback is stopped.
+ *
+ * @param state playback state
+ * @return a {@link MediaNotification}. If it's {@code null}, notification wouldn't be shown.
+ */
+ // TODO(jaewan): Also add metadata
+ public MediaNotification onUpdateNotification(PlaybackState2 state) {
+ return mProvider.onUpdateNotification_impl(state);
+ }
+
+ /**
+ * Get instance of the {@link MediaSession2} that you've previously created with the
+ * {@link #onCreateSession} for this service.
+ *
+ * @return created session
+ */
+ public final MediaSession2 getSession() {
+ return mProvider.getSession_impl();
+ }
+
+ /**
+ * Default implementation for {@link MediaSessionService2} to handle incoming binding
+ * request. If the request is for getting the session, the intent will have action
+ * {@link #SERVICE_INTERFACE}.
+ * <p>
+ * Override this method if this service also needs to handle binder requests other than
+ * {@link #SERVICE_INTERFACE}. Derived classes MUST call through to the super class's
+ * implementation of this method.
+ *
+ * @param intent
+ * @return Binder
+ */
+ @CallSuper
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mProvider.onBind_impl(intent);
+ }
+
+ /**
+ * Returned by {@link #onUpdateNotification(PlaybackState)} for making session service
+ * foreground service to keep playback running in the background. It's highly recommended to
+ * show media style notification here.
+ */
+ // TODO(jaewan): Should we also move this to updatable?
+ public static class MediaNotification {
+ public final int id;
+ public final Notification notification;
+
+ private MediaNotification(int id, @NonNull Notification notification) {
+ this.id = id;
+ this.notification = notification;
+ }
+
+ /**
+ * Create a {@link MediaNotification}.
+ *
+ * @param notificationId notification id to be used for
+ * {@link android.app.NotificationManager#notify(int, Notification)}.
+ * @param notification a notification to make session service foreground service. Media
+ * style notification is recommended here.
+ * @return
+ */
+ public static MediaNotification create(int notificationId,
+ @NonNull Notification notification) {
+ if (notification == null) {
+ throw new IllegalArgumentException("Notification cannot be null");
+ }
+ return new MediaNotification(notificationId, notification);
+ }
+ }
+}
diff --git a/android/media/MockMediaLibraryService2.java b/android/media/MockMediaLibraryService2.java
new file mode 100644
index 00000000..14cf2577
--- /dev/null
+++ b/android/media/MockMediaLibraryService2.java
@@ -0,0 +1,98 @@
+/*
+* Copyright 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.media;
+
+import static junit.framework.Assert.fail;
+
+import android.content.Context;
+import android.media.MediaSession2.CommandGroup;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.TestUtils.SyncHandler;
+import android.os.Bundle;
+import android.os.Process;
+
+import javax.annotation.concurrent.GuardedBy;
+
+/**
+ * Mock implementation of {@link MediaLibraryService2} for testing.
+ */
+public class MockMediaLibraryService2 extends MediaLibraryService2 {
+ // Keep in sync with the AndroidManifest.xml
+ public static final String ID = "TestLibrary";
+
+ public static final String ROOT_ID = "rootId";
+ public static final Bundle EXTRA = new Bundle();
+ static {
+ EXTRA.putString(ROOT_ID, ROOT_ID);
+ }
+ @GuardedBy("MockMediaLibraryService2.class")
+ private static SessionToken2 sToken;
+
+ private MediaLibrarySession mSession;
+
+ @Override
+ public MediaLibrarySession onCreateSession(String sessionId) {
+ final MockPlayer player = new MockPlayer(1);
+ final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+ try {
+ handler.postAndSync(() -> {
+ TestLibrarySessionCallback callback = new TestLibrarySessionCallback();
+ mSession = new MediaLibrarySessionBuilder(MockMediaLibraryService2.this,
+ player, (runnable) -> handler.post(runnable), callback)
+ .setId(sessionId).build();
+ });
+ } catch (InterruptedException e) {
+ fail(e.toString());
+ }
+ return mSession;
+ }
+
+ @Override
+ public void onDestroy() {
+ TestServiceRegistry.getInstance().cleanUp();
+ super.onDestroy();
+ }
+
+ public static SessionToken2 getToken(Context context) {
+ synchronized (MockMediaLibraryService2.class) {
+ if (sToken == null) {
+ sToken = new SessionToken2(SessionToken2.TYPE_LIBRARY_SERVICE,
+ context.getPackageName(), ID,
+ MockMediaLibraryService2.class.getName(), null);
+ }
+ return sToken;
+ }
+ }
+
+ private class TestLibrarySessionCallback extends MediaLibrarySessionCallback {
+ @Override
+ public CommandGroup onConnect(ControllerInfo controller) {
+ if (Process.myUid() != controller.getUid()) {
+ // It's system app wants to listen changes. Ignore.
+ return super.onConnect(controller);
+ }
+ TestServiceRegistry.getInstance().setServiceInstance(
+ MockMediaLibraryService2.this, controller);
+ return super.onConnect(controller);
+ }
+
+ @Override
+ public BrowserRoot onGetRoot(ControllerInfo controller, Bundle rootHints) {
+ return new BrowserRoot(ROOT_ID, EXTRA);
+ }
+ }
+} \ No newline at end of file
diff --git a/android/media/MockMediaSessionService2.java b/android/media/MockMediaSessionService2.java
new file mode 100644
index 00000000..b0581170
--- /dev/null
+++ b/android/media/MockMediaSessionService2.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 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.media;
+
+import static junit.framework.Assert.fail;
+
+import android.app.Notification;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.TestUtils.SyncHandler;
+import android.media.session.PlaybackState;
+import android.os.Process;
+
+/**
+ * Mock implementation of {@link android.media.MediaSessionService2} for testing.
+ */
+public class MockMediaSessionService2 extends MediaSessionService2 {
+ // Keep in sync with the AndroidManifest.xml
+ public static final String ID = "TestSession";
+
+ private static final String DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID = "media_session_service";
+ private static final int DEFAULT_MEDIA_NOTIFICATION_ID = 1001;
+
+ private NotificationChannel mDefaultNotificationChannel;
+ private MediaSession2 mSession;
+ private NotificationManager mNotificationManager;
+
+ @Override
+ public MediaSession2 onCreateSession(String sessionId) {
+ final MockPlayer player = new MockPlayer(1);
+ final SyncHandler handler = (SyncHandler) TestServiceRegistry.getInstance().getHandler();
+ try {
+ handler.postAndSync(() -> {
+ mSession = new MediaSession2.Builder(MockMediaSessionService2.this, player)
+ .setId(sessionId).setSessionCallback((runnable)->handler.post(runnable),
+ new MySessionCallback()).build();
+ });
+ } catch (InterruptedException e) {
+ fail(e.toString());
+ }
+ return mSession;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ @Override
+ public void onDestroy() {
+ TestServiceRegistry.getInstance().cleanUp();
+ super.onDestroy();
+ }
+
+ @Override
+ public MediaNotification onUpdateNotification(PlaybackState2 state) {
+ if (mDefaultNotificationChannel == null) {
+ mDefaultNotificationChannel = new NotificationChannel(
+ DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
+ DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID,
+ NotificationManager.IMPORTANCE_DEFAULT);
+ mNotificationManager.createNotificationChannel(mDefaultNotificationChannel);
+ }
+ Notification notification = new Notification.Builder(
+ this, DEFAULT_MEDIA_NOTIFICATION_CHANNEL_ID)
+ .setContentTitle(getPackageName())
+ .setContentText("Playback state: " + state.getState())
+ .setSmallIcon(android.R.drawable.sym_def_app_icon).build();
+ return MediaNotification.create(DEFAULT_MEDIA_NOTIFICATION_ID, notification);
+ }
+
+ private class MySessionCallback extends SessionCallback {
+ @Override
+ public MediaSession2.CommandGroup onConnect(ControllerInfo controller) {
+ if (Process.myUid() != controller.getUid()) {
+ // It's system app wants to listen changes. Ignore.
+ return super.onConnect(controller);
+ }
+ TestServiceRegistry.getInstance().setServiceInstance(
+ MockMediaSessionService2.this, controller);
+ return super.onConnect(controller);
+ }
+ }
+}
diff --git a/android/media/MockPlayer.java b/android/media/MockPlayer.java
new file mode 100644
index 00000000..fd693092
--- /dev/null
+++ b/android/media/MockPlayer.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright 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.media;
+
+import android.media.MediaSession2.PlaylistParam;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+
+/**
+ * A mock implementation of {@link MediaPlayerBase} for testing.
+ */
+public class MockPlayer extends MediaPlayerBase {
+ public final CountDownLatch mCountDownLatch;
+
+ public boolean mPlayCalled;
+ public boolean mPauseCalled;
+ public boolean mStopCalled;
+ public boolean mSkipToPreviousCalled;
+ public boolean mSkipToNextCalled;
+ public List<PlaybackListenerHolder> mListeners = new ArrayList<>();
+ private PlaybackState2 mLastPlaybackState;
+
+ public MockPlayer(int count) {
+ mCountDownLatch = (count > 0) ? new CountDownLatch(count) : null;
+ }
+
+ @Override
+ public void play() {
+ mPlayCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void pause() {
+ mPauseCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void stop() {
+ mStopCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void skipToPrevious() {
+ mSkipToPreviousCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+ @Override
+ public void skipToNext() {
+ mSkipToNextCalled = true;
+ if (mCountDownLatch != null) {
+ mCountDownLatch.countDown();
+ }
+ }
+
+
+
+ @Nullable
+ @Override
+ public PlaybackState2 getPlaybackState() {
+ return mLastPlaybackState;
+ }
+
+ @Override
+ public void addPlaybackListener(@NonNull Executor executor,
+ @NonNull PlaybackListener listener) {
+ mListeners.add(new PlaybackListenerHolder(executor, listener));
+ }
+
+ @Override
+ public void removePlaybackListener(@NonNull PlaybackListener listener) {
+ int index = PlaybackListenerHolder.indexOf(mListeners, listener);
+ if (index >= 0) {
+ mListeners.remove(index);
+ }
+ }
+
+ public void notifyPlaybackState(final PlaybackState2 state) {
+ mLastPlaybackState = state;
+ for (int i = 0; i < mListeners.size(); i++) {
+ mListeners.get(i).postPlaybackChange(state);
+ }
+ }
+
+ // No-op. Should be added for test later.
+ @Override
+ public void prepare() {
+ }
+
+ @Override
+ public void seekTo(long pos) {
+ }
+
+ @Override
+ public void fastFoward() {
+ }
+
+ @Override
+ public void rewind() {
+ }
+
+ @Override
+ public AudioAttributes getAudioAttributes() {
+ return null;
+ }
+
+ @Override
+ public void setPlaylist(List<MediaItem2> item, PlaylistParam param) {
+ }
+
+ @Override
+ public void setCurrentPlaylistItem(int index) {
+ }
+}
diff --git a/android/media/PlaybackListenerHolder.java b/android/media/PlaybackListenerHolder.java
new file mode 100644
index 00000000..4e19d4de
--- /dev/null
+++ b/android/media/PlaybackListenerHolder.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 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.media;
+
+import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.os.Message;
+import android.support.annotation.NonNull;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Holds {@link PlaybackListener} with the {@link Handler}.
+ */
+public class PlaybackListenerHolder {
+ public final Executor executor;
+ public final PlaybackListener listener;
+
+ public PlaybackListenerHolder(Executor executor, @NonNull PlaybackListener listener) {
+ this.executor = executor;
+ this.listener = listener;
+ }
+
+ public void postPlaybackChange(final PlaybackState2 state) {
+ executor.execute(() -> listener.onPlaybackChanged(state));
+ }
+
+ /**
+ * Returns {@code true} if the given list contains a {@link PlaybackListenerHolder} that holds
+ * the given listener.
+ *
+ * @param list list to check
+ * @param listener listener to check
+ * @return {@code true} if the given list contains listener. {@code false} otherwise.
+ */
+ public static <Holder extends PlaybackListenerHolder> boolean contains(
+ @NonNull List<Holder> list, PlaybackListener listener) {
+ return indexOf(list, listener) >= 0;
+ }
+
+ /**
+ * Returns the index of the {@link PlaybackListenerHolder} that contains the given listener.
+ *
+ * @param list list to check
+ * @param listener listener to check
+ * @return {@code index} of item if the given list contains listener. {@code -1} otherwise.
+ */
+ public static <Holder extends PlaybackListenerHolder> int indexOf(
+ @NonNull List<Holder> list, PlaybackListener listener) {
+ for (int i = 0; i < list.size(); i++) {
+ if (list.get(i).listener == listener) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/android/media/PlaybackState2.java b/android/media/PlaybackState2.java
new file mode 100644
index 00000000..46d6f45a
--- /dev/null
+++ b/android/media/PlaybackState2.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.IntDef;
+import android.os.Bundle;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Playback state for a {@link MediaPlayerBase}, to be shared between {@link MediaSession2} and
+ * {@link MediaController2}. This includes a playback state {@link #STATE_PLAYING},
+ * the current playback position and extra.
+ * @hide
+ */
+// TODO(jaewan): Move to updatable
+public final class PlaybackState2 {
+ private static final String TAG = "PlaybackState2";
+
+ private static final String KEY_STATE = "android.media.playbackstate2.state";
+
+ // TODO(jaewan): Replace states from MediaPlayer2
+ /**
+ * @hide
+ */
+ @IntDef({STATE_NONE, STATE_STOPPED, STATE_PREPARED, STATE_PAUSED, STATE_PLAYING,
+ STATE_FINISH, STATE_BUFFERING, STATE_ERROR})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface State {}
+
+ /**
+ * This is the default playback state and indicates that no media has been
+ * added yet, or the performer has been reset and has no content to play.
+ */
+ public final static int STATE_NONE = 0;
+
+ /**
+ * State indicating this item is currently stopped.
+ */
+ public final static int STATE_STOPPED = 1;
+
+ /**
+ * State indicating this item is currently prepared
+ */
+ public final static int STATE_PREPARED = 2;
+
+ /**
+ * State indicating this item is currently paused.
+ */
+ public final static int STATE_PAUSED = 3;
+
+ /**
+ * State indicating this item is currently playing.
+ */
+ public final static int STATE_PLAYING = 4;
+
+ /**
+ * State indicating the playback reaches the end of the item.
+ */
+ public final static int STATE_FINISH = 5;
+
+ /**
+ * State indicating this item is currently buffering and will begin playing
+ * when enough data has buffered.
+ */
+ public final static int STATE_BUFFERING = 6;
+
+ /**
+ * State indicating this item is currently in an error state. The error
+ * message should also be set when entering this state.
+ */
+ public final static int STATE_ERROR = 7;
+
+ /**
+ * Use this value for the position to indicate the position is not known.
+ */
+ public final static long PLAYBACK_POSITION_UNKNOWN = -1;
+
+ private final int mState;
+ private final long mPosition;
+ private final long mBufferedPosition;
+ private final float mSpeed;
+ private final CharSequence mErrorMessage;
+ private final long mUpdateTime;
+ private final long mActiveItemId;
+
+ public PlaybackState2(int state, long position, long updateTime, float speed,
+ long bufferedPosition, long activeItemId, CharSequence error) {
+ mState = state;
+ mPosition = position;
+ mSpeed = speed;
+ mUpdateTime = updateTime;
+ mBufferedPosition = bufferedPosition;
+ mActiveItemId = activeItemId;
+ mErrorMessage = error;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder bob = new StringBuilder("PlaybackState {");
+ bob.append("state=").append(mState);
+ bob.append(", position=").append(mPosition);
+ bob.append(", buffered position=").append(mBufferedPosition);
+ bob.append(", speed=").append(mSpeed);
+ bob.append(", updated=").append(mUpdateTime);
+ bob.append(", active item id=").append(mActiveItemId);
+ bob.append(", error=").append(mErrorMessage);
+ bob.append("}");
+ return bob.toString();
+ }
+
+ /**
+ * Get the current state of playback. One of the following:
+ * <ul>
+ * <li> {@link PlaybackState2#STATE_NONE}</li>
+ * <li> {@link PlaybackState2#STATE_STOPPED}</li>
+ * <li> {@link PlaybackState2#STATE_PLAYING}</li>
+ * <li> {@link PlaybackState2#STATE_PAUSED}</li>
+ * <li> {@link PlaybackState2#STATE_BUFFERING}</li>
+ * <li> {@link PlaybackState2#STATE_ERROR}</li>
+ * </ul>
+ */
+ @State
+ public int getState() {
+ return mState;
+ }
+
+ /**
+ * Get the current playback position in ms.
+ */
+ public long getPosition() {
+ return mPosition;
+ }
+
+ /**
+ * Get the current buffered position in ms. This is the farthest playback
+ * point that can be reached from the current position using only buffered
+ * content.
+ */
+ public long getBufferedPosition() {
+ return mBufferedPosition;
+ }
+
+ /**
+ * Get the current playback speed as a multiple of normal playback. This
+ * should be negative when rewinding. A value of 1 means normal playback and
+ * 0 means paused.
+ *
+ * @return The current speed of playback.
+ */
+ public float getPlaybackSpeed() {
+ return mSpeed;
+ }
+
+ /**
+ * Get a user readable error message. This should be set when the state is
+ * {@link PlaybackState2#STATE_ERROR}.
+ */
+ public CharSequence getErrorMessage() {
+ return mErrorMessage;
+ }
+
+ /**
+ * Get the elapsed real time at which position was last updated. If the
+ * position has never been set this will return 0;
+ *
+ * @return The last time the position was updated.
+ */
+ public long getLastPositionUpdateTime() {
+ return mUpdateTime;
+ }
+
+ /**
+ * Get the id of the currently active item in the playlist.
+ *
+ * @return The id of the currently active item in the queue
+ */
+ public long getCurrentPlaylistItemIndex() {
+ return mActiveItemId;
+ }
+
+ /**
+ * @return Bundle object for this to share between processes.
+ */
+ public Bundle toBundle() {
+ // TODO(jaewan): Include other variables.
+ Bundle bundle = new Bundle();
+ bundle.putInt(KEY_STATE, mState);
+ return bundle;
+ }
+
+ /**
+ * @param bundle input
+ * @return
+ */
+ public static PlaybackState2 fromBundle(Bundle bundle) {
+ // TODO(jaewan): Include other variables.
+ final int state = bundle.getInt(KEY_STATE);
+ return new PlaybackState2(state, 0, 0, 0, 0, 0, null);
+ }
+} \ No newline at end of file
diff --git a/android/media/Rating2.java b/android/media/Rating2.java
new file mode 100644
index 00000000..67e5e728
--- /dev/null
+++ b/android/media/Rating2.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.IntDef;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A class to encapsulate rating information used as content metadata.
+ * A rating is defined by its rating style (see {@link #RATING_HEART},
+ * {@link #RATING_THUMB_UP_DOWN}, {@link #RATING_3_STARS}, {@link #RATING_4_STARS},
+ * {@link #RATING_5_STARS} or {@link #RATING_PERCENTAGE}) and the actual rating value (which may
+ * be defined as "unrated"), both of which are defined when the rating instance is constructed
+ * through one of the factory methods.
+ * @hide
+ */
+// TODO(jaewan): Move this to updatable
+public final class Rating2 {
+ private static final String TAG = "Rating2";
+
+ private static final String KEY_STYLE = "android.media.rating2.style";
+ private static final String KEY_VALUE = "android.media.rating2.value";
+
+ /**
+ * @hide
+ */
+ @IntDef({RATING_NONE, RATING_HEART, RATING_THUMB_UP_DOWN, RATING_3_STARS, RATING_4_STARS,
+ RATING_5_STARS, RATING_PERCENTAGE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Style {}
+
+ /**
+ * @hide
+ */
+ @IntDef({RATING_3_STARS, RATING_4_STARS, RATING_5_STARS})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface StarStyle {}
+
+ /**
+ * Indicates a rating style is not supported. A Rating2 will never have this
+ * type, but can be used by other classes to indicate they do not support
+ * Rating2.
+ */
+ public final static int RATING_NONE = 0;
+
+ /**
+ * A rating style with a single degree of rating, "heart" vs "no heart". Can be used to
+ * indicate the content referred to is a favorite (or not).
+ */
+ public final static int RATING_HEART = 1;
+
+ /**
+ * A rating style for "thumb up" vs "thumb down".
+ */
+ public final static int RATING_THUMB_UP_DOWN = 2;
+
+ /**
+ * A rating style with 0 to 3 stars.
+ */
+ public final static int RATING_3_STARS = 3;
+
+ /**
+ * A rating style with 0 to 4 stars.
+ */
+ public final static int RATING_4_STARS = 4;
+
+ /**
+ * A rating style with 0 to 5 stars.
+ */
+ public final static int RATING_5_STARS = 5;
+
+ /**
+ * A rating style expressed as a percentage.
+ */
+ public final static int RATING_PERCENTAGE = 6;
+
+ private final static float RATING_NOT_RATED = -1.0f;
+
+ private final int mRatingStyle;
+
+ private final float mRatingValue;
+
+ private Rating2(@Style int ratingStyle, float rating) {
+ mRatingStyle = ratingStyle;
+ mRatingValue = rating;
+ }
+
+ @Override
+ public String toString() {
+ return "Rating2:style=" + mRatingStyle + " rating="
+ + (mRatingValue < 0.0f ? "unrated" : String.valueOf(mRatingValue));
+ }
+
+ /**
+ * Create an instance from bundle object, previoulsy created by {@link #toBundle()}
+ *
+ * @param bundle bundle
+ * @return new Rating2 instance
+ */
+ public static Rating2 fromBundle(Bundle bundle) {
+ return new Rating2(bundle.getInt(KEY_STYLE), bundle.getFloat(KEY_VALUE));
+ }
+
+ /**
+ * Return bundle for this object to share across the process.
+ * @return bundle of this object
+ */
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putInt(KEY_STYLE, mRatingStyle);
+ bundle.putFloat(KEY_VALUE, mRatingValue);
+ return bundle;
+ }
+
+ /**
+ * Return a Rating2 instance with no rating.
+ * Create and return a new Rating2 instance with no rating known for the given
+ * rating style.
+ * @param ratingStyle one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN},
+ * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS},
+ * or {@link #RATING_PERCENTAGE}.
+ * @return null if an invalid rating style is passed, a new Rating2 instance otherwise.
+ */
+ public static Rating2 newUnratedRating(@Style int ratingStyle) {
+ switch(ratingStyle) {
+ case RATING_HEART:
+ case RATING_THUMB_UP_DOWN:
+ case RATING_3_STARS:
+ case RATING_4_STARS:
+ case RATING_5_STARS:
+ case RATING_PERCENTAGE:
+ return new Rating2(ratingStyle, RATING_NOT_RATED);
+ default:
+ return null;
+ }
+ }
+
+ /**
+ * Return a Rating2 instance with a heart-based rating.
+ * Create and return a new Rating2 instance with a rating style of {@link #RATING_HEART},
+ * and a heart-based rating.
+ * @param hasHeart true for a "heart selected" rating, false for "heart unselected".
+ * @return a new Rating2 instance.
+ */
+ public static Rating2 newHeartRating(boolean hasHeart) {
+ return new Rating2(RATING_HEART, hasHeart ? 1.0f : 0.0f);
+ }
+
+ /**
+ * Return a Rating2 instance with a thumb-based rating.
+ * Create and return a new Rating2 instance with a {@link #RATING_THUMB_UP_DOWN}
+ * rating style, and a "thumb up" or "thumb down" rating.
+ * @param thumbIsUp true for a "thumb up" rating, false for "thumb down".
+ * @return a new Rating2 instance.
+ */
+ public static Rating2 newThumbRating(boolean thumbIsUp) {
+ return new Rating2(RATING_THUMB_UP_DOWN, thumbIsUp ? 1.0f : 0.0f);
+ }
+
+ /**
+ * Return a Rating2 instance with a star-based rating.
+ * Create and return a new Rating2 instance with one of the star-base rating styles
+ * and the given integer or fractional number of stars. Non integer values can for instance
+ * be used to represent an average rating value, which might not be an integer number of stars.
+ * @param starRatingStyle one of {@link #RATING_3_STARS}, {@link #RATING_4_STARS},
+ * {@link #RATING_5_STARS}.
+ * @param starRating a number ranging from 0.0f to 3.0f, 4.0f or 5.0f according to
+ * the rating style.
+ * @return null if the rating style is invalid, or the rating is out of range,
+ * a new Rating2 instance otherwise.
+ */
+ public static Rating2 newStarRating(@StarStyle int starRatingStyle, float starRating) {
+ float maxRating = -1.0f;
+ switch(starRatingStyle) {
+ case RATING_3_STARS:
+ maxRating = 3.0f;
+ break;
+ case RATING_4_STARS:
+ maxRating = 4.0f;
+ break;
+ case RATING_5_STARS:
+ maxRating = 5.0f;
+ break;
+ default:
+ Log.e(TAG, "Invalid rating style (" + starRatingStyle + ") for a star rating");
+ return null;
+ }
+ if ((starRating < 0.0f) || (starRating > maxRating)) {
+ Log.e(TAG, "Trying to set out of range star-based rating");
+ return null;
+ }
+ return new Rating2(starRatingStyle, starRating);
+ }
+
+ /**
+ * Return a Rating2 instance with a percentage-based rating.
+ * Create and return a new Rating2 instance with a {@link #RATING_PERCENTAGE}
+ * rating style, and a rating of the given percentage.
+ * @param percent the value of the rating
+ * @return null if the rating is out of range, a new Rating2 instance otherwise.
+ */
+ public static Rating2 newPercentageRating(float percent) {
+ if ((percent < 0.0f) || (percent > 100.0f)) {
+ Log.e(TAG, "Invalid percentage-based rating value");
+ return null;
+ } else {
+ return new Rating2(RATING_PERCENTAGE, percent);
+ }
+ }
+
+ /**
+ * Return whether there is a rating value available.
+ * @return true if the instance was not created with {@link #newUnratedRating(int)}.
+ */
+ public boolean isRated() {
+ return mRatingValue >= 0.0f;
+ }
+
+ /**
+ * Return the rating style.
+ * @return one of {@link #RATING_HEART}, {@link #RATING_THUMB_UP_DOWN},
+ * {@link #RATING_3_STARS}, {@link #RATING_4_STARS}, {@link #RATING_5_STARS},
+ * or {@link #RATING_PERCENTAGE}.
+ */
+ @Style
+ public int getRatingStyle() {
+ return mRatingStyle;
+ }
+
+ /**
+ * Return whether the rating is "heart selected".
+ * @return true if the rating is "heart selected", false if the rating is "heart unselected",
+ * if the rating style is not {@link #RATING_HEART} or if it is unrated.
+ */
+ public boolean hasHeart() {
+ if (mRatingStyle != RATING_HEART) {
+ return false;
+ } else {
+ return (mRatingValue == 1.0f);
+ }
+ }
+
+ /**
+ * Return whether the rating is "thumb up".
+ * @return true if the rating is "thumb up", false if the rating is "thumb down",
+ * if the rating style is not {@link #RATING_THUMB_UP_DOWN} or if it is unrated.
+ */
+ public boolean isThumbUp() {
+ if (mRatingStyle != RATING_THUMB_UP_DOWN) {
+ return false;
+ } else {
+ return (mRatingValue == 1.0f);
+ }
+ }
+
+ /**
+ * Return the star-based rating value.
+ * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is
+ * not star-based, or if it is unrated.
+ */
+ public float getStarRating() {
+ switch (mRatingStyle) {
+ case RATING_3_STARS:
+ case RATING_4_STARS:
+ case RATING_5_STARS:
+ if (isRated()) {
+ return mRatingValue;
+ }
+ default:
+ return -1.0f;
+ }
+ }
+
+ /**
+ * Return the percentage-based rating value.
+ * @return a rating value greater or equal to 0.0f, or a negative value if the rating style is
+ * not percentage-based, or if it is unrated.
+ */
+ public float getPercentRating() {
+ if ((mRatingStyle != RATING_PERCENTAGE) || !isRated()) {
+ return -1.0f;
+ } else {
+ return mRatingValue;
+ }
+ }
+}
diff --git a/android/media/SessionToken2.java b/android/media/SessionToken2.java
new file mode 100644
index 00000000..697a5a87
--- /dev/null
+++ b/android/media/SessionToken2.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright 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.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.media.session.MediaSessionManager;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents an ongoing {@link MediaSession2} or a {@link MediaSessionService2}.
+ * If it's representing a session service, it may not be ongoing.
+ * <p>
+ * This may be passed to apps by the session owner to allow them to create a
+ * {@link MediaController2} to communicate with the session.
+ * <p>
+ * It can be also obtained by {@link MediaSessionManager}.
+ * @hide
+ */
+// TODO(jaewan): Unhide. SessionToken2?
+// TODO(jaewan): Move Token to updatable!
+// TODO(jaewan): Find better name for this (SessionToken or Session2Token)
+public final class SessionToken2 {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {TYPE_SESSION, TYPE_SESSION_SERVICE, TYPE_LIBRARY_SERVICE})
+ public @interface TokenType {
+ }
+
+ public static final int TYPE_SESSION = 0;
+ public static final int TYPE_SESSION_SERVICE = 1;
+ public static final int TYPE_LIBRARY_SERVICE = 2;
+
+ private static final String KEY_TYPE = "android.media.token.type";
+ private static final String KEY_PACKAGE_NAME = "android.media.token.package_name";
+ private static final String KEY_SERVICE_NAME = "android.media.token.service_name";
+ private static final String KEY_ID = "android.media.token.id";
+ private static final String KEY_SESSION_BINDER = "android.media.token.session_binder";
+
+ private final @TokenType int mType;
+ private final String mPackageName;
+ private final String mServiceName;
+ private final String mId;
+ private final IMediaSession2 mSessionBinder;
+
+ /**
+ * Constructor for the token.
+ *
+ * @hide
+ * @param type type
+ * @param packageName package name
+ * @param id id
+ * @param serviceName name of service. Can be {@code null} if it's not an service.
+ * @param sessionBinder binder for this session. Can be {@code null} if it's service.
+ * @hide
+ */
+ // TODO(jaewan): UID is also needed.
+ // TODO(jaewan): Unhide
+ public SessionToken2(@TokenType int type, @NonNull String packageName, @NonNull String id,
+ @Nullable String serviceName, @Nullable IMediaSession2 sessionBinder) {
+ // TODO(jaewan): Add sanity check.
+ mType = type;
+ mPackageName = packageName;
+ mId = id;
+ mServiceName = serviceName;
+ mSessionBinder = sessionBinder;
+ }
+
+ public int hashCode() {
+ final int prime = 31;
+ return mType
+ + prime * (mPackageName.hashCode()
+ + prime * (mId.hashCode()
+ + prime * ((mServiceName != null ? mServiceName.hashCode() : 0)
+ + prime * (mSessionBinder != null ? mSessionBinder.asBinder().hashCode() : 0))));
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ SessionToken2 other = (SessionToken2) obj;
+ if (!mPackageName.equals(other.getPackageName())
+ || !mServiceName.equals(other.getServiceName())
+ || !mId.equals(other.getId())
+ || mType != other.getType()) {
+ return false;
+ }
+ if (mSessionBinder == other.getSessionBinder()) {
+ return true;
+ } else if (mSessionBinder == null || other.getSessionBinder() == null) {
+ return false;
+ }
+ return mSessionBinder.asBinder().equals(other.getSessionBinder().asBinder());
+ }
+
+ @Override
+ public String toString() {
+ return "SessionToken {pkg=" + mPackageName + " id=" + mId + " type=" + mType
+ + " service=" + mServiceName + " binder=" + mSessionBinder + "}";
+ }
+
+ /**
+ * @return package name
+ */
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * @return id
+ */
+ public String getId() {
+ return mId;
+ }
+
+ /**
+ * @return type of the token
+ * @see #TYPE_SESSION
+ * @see #TYPE_SESSION_SERVICE
+ */
+ public @TokenType int getType() {
+ return mType;
+ }
+
+ /**
+ * @return session binder.
+ * @hide
+ */
+ public @Nullable IMediaSession2 getSessionBinder() {
+ return mSessionBinder;
+ }
+
+ /**
+ * @return service name if it's session service.
+ * @hide
+ */
+ public @Nullable String getServiceName() {
+ return mServiceName;
+ }
+
+ /**
+ * Create a token from the bundle, exported by {@link #toBundle()}.
+ *
+ * @param bundle
+ * @return
+ */
+ public static SessionToken2 fromBundle(@NonNull Bundle bundle) {
+ if (bundle == null) {
+ return null;
+ }
+ final @TokenType int type = bundle.getInt(KEY_TYPE, -1);
+ final String packageName = bundle.getString(KEY_PACKAGE_NAME);
+ final String serviceName = bundle.getString(KEY_SERVICE_NAME);
+ final String id = bundle.getString(KEY_ID);
+ final IBinder sessionBinder = bundle.getBinder(KEY_SESSION_BINDER);
+
+ // Sanity check.
+ switch (type) {
+ case TYPE_SESSION:
+ if (!(sessionBinder instanceof IMediaSession2)) {
+ throw new IllegalArgumentException("Session needs sessionBinder");
+ }
+ break;
+ case TYPE_SESSION_SERVICE:
+ if (TextUtils.isEmpty(serviceName)) {
+ throw new IllegalArgumentException("Session service needs service name");
+ }
+ if (sessionBinder != null && !(sessionBinder instanceof IMediaSession2)) {
+ throw new IllegalArgumentException("Invalid session binder");
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Invalid type");
+ }
+ if (TextUtils.isEmpty(packageName) || id == null) {
+ throw new IllegalArgumentException("Package name nor ID cannot be null.");
+ }
+ // TODO(jaewan): Revisit here when we add connection callback to the session for individual
+ // controller's permission check. With it, sessionBinder should be available
+ // if and only if for session, not session service.
+ return new SessionToken2(type, packageName, id, serviceName,
+ sessionBinder != null ? IMediaSession2.Stub.asInterface(sessionBinder) : null);
+ }
+
+ /**
+ * Create a {@link Bundle} from this token to share it across processes.
+ *
+ * @return Bundle
+ * @hide
+ */
+ public Bundle toBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_PACKAGE_NAME, mPackageName);
+ bundle.putString(KEY_SERVICE_NAME, mServiceName);
+ bundle.putString(KEY_ID, mId);
+ bundle.putInt(KEY_TYPE, mType);
+ bundle.putBinder(KEY_SESSION_BINDER,
+ mSessionBinder != null ? mSessionBinder.asBinder() : null);
+ return bundle;
+ }
+}
diff --git a/android/media/TestServiceRegistry.java b/android/media/TestServiceRegistry.java
new file mode 100644
index 00000000..6f5512ef
--- /dev/null
+++ b/android/media/TestServiceRegistry.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright 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.media;
+
+import static org.junit.Assert.fail;
+
+import android.media.MediaSession2.ControllerInfo;
+import android.media.TestUtils.SyncHandler;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.GuardedBy;
+
+/**
+ * Keeps the instance of currently running {@link MockMediaSessionService2}. And also provides
+ * a way to control them in one place.
+ * <p>
+ * It only support only one service at a time.
+ */
+public class TestServiceRegistry {
+ public interface ServiceInstanceChangedCallback {
+ void OnServiceInstanceChanged(MediaSessionService2 service);
+ }
+
+ @GuardedBy("TestServiceRegistry.class")
+ private static TestServiceRegistry sInstance;
+ @GuardedBy("TestServiceRegistry.class")
+ private MediaSessionService2 mService;
+ @GuardedBy("TestServiceRegistry.class")
+ private SyncHandler mHandler;
+ @GuardedBy("TestServiceRegistry.class")
+ private ControllerInfo mOnConnectControllerInfo;
+ @GuardedBy("TestServiceRegistry.class")
+ private ServiceInstanceChangedCallback mCallback;
+
+ public static TestServiceRegistry getInstance() {
+ synchronized (TestServiceRegistry.class) {
+ if (sInstance == null) {
+ sInstance = new TestServiceRegistry();
+ }
+ return sInstance;
+ }
+ }
+
+ public void setHandler(Handler handler) {
+ synchronized (TestServiceRegistry.class) {
+ mHandler = new SyncHandler(handler.getLooper());
+ }
+ }
+
+ public void setServiceInstanceChangedCallback(ServiceInstanceChangedCallback callback) {
+ synchronized (TestServiceRegistry.class) {
+ mCallback = callback;
+ }
+ }
+
+ public Handler getHandler() {
+ synchronized (TestServiceRegistry.class) {
+ return mHandler;
+ }
+ }
+
+ public void setServiceInstance(MediaSessionService2 service, ControllerInfo controller) {
+ synchronized (TestServiceRegistry.class) {
+ if (mService != null) {
+ fail("Previous service instance is still running. Clean up manually to ensure"
+ + " previoulsy running service doesn't break current test");
+ }
+ mService = service;
+ mOnConnectControllerInfo = controller;
+ if (mCallback != null) {
+ mCallback.OnServiceInstanceChanged(service);
+ }
+ }
+ }
+
+ public MediaSessionService2 getServiceInstance() {
+ synchronized (TestServiceRegistry.class) {
+ return mService;
+ }
+ }
+
+ public ControllerInfo getOnConnectControllerInfo() {
+ synchronized (TestServiceRegistry.class) {
+ return mOnConnectControllerInfo;
+ }
+ }
+
+
+ public void cleanUp() {
+ synchronized (TestServiceRegistry.class) {
+ final ServiceInstanceChangedCallback callback = mCallback;
+ if (mService != null) {
+ try {
+ if (mHandler.getLooper() == Looper.myLooper()) {
+ mService.getSession().close();
+ } else {
+ mHandler.postAndSync(() -> {
+ mService.getSession().close();
+ });
+ }
+ } catch (InterruptedException e) {
+ // No-op. Service containing session will die, but shouldn't be a huge issue.
+ }
+ // stopSelf() would not kill service while the binder connection established by
+ // bindService() exists, and close() above will do the job instead.
+ // So stopSelf() isn't really needed, but just for sure.
+ mService.stopSelf();
+ mService = null;
+ }
+ if (mHandler != null) {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+ mCallback = null;
+ mOnConnectControllerInfo = null;
+
+ if (callback != null) {
+ callback.OnServiceInstanceChanged(null);
+ }
+ }
+ }
+}
diff --git a/android/media/TestUtils.java b/android/media/TestUtils.java
new file mode 100644
index 00000000..9a1fa100
--- /dev/null
+++ b/android/media/TestUtils.java
@@ -0,0 +1,124 @@
+/*
+ * Copyright 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.media;
+
+import android.content.Context;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.os.Bundle;
+import android.os.Handler;
+
+import android.os.Looper;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * Utilities for tests.
+ */
+public final class TestUtils {
+ private static final int WAIT_TIME_MS = 1000;
+ private static final int WAIT_SERVICE_TIME_MS = 5000;
+
+ /**
+ * Creates a {@link android.media.session.PlaybackState} with the given state.
+ *
+ * @param state one of the PlaybackState.STATE_xxx.
+ * @return a PlaybackState
+ */
+ public static PlaybackState2 createPlaybackState(int state) {
+ return new PlaybackState2(state, 0, 0, 1.0f,
+ 0, 0, null);
+ }
+
+ /**
+ * Finds the session with id in this test package.
+ *
+ * @param context
+ * @param id
+ * @return
+ */
+ // TODO(jaewan): Currently not working.
+ public static SessionToken2 getServiceToken(Context context, String id) {
+ MediaSessionManager manager =
+ (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE);
+ List<SessionToken2> tokens = manager.getSessionServiceTokens();
+ for (int i = 0; i < tokens.size(); i++) {
+ SessionToken2 token = tokens.get(i);
+ if (context.getPackageName().equals(token.getPackageName())
+ && id.equals(token.getId())) {
+ return token;
+ }
+ }
+ fail("Failed to find service");
+ return null;
+ }
+
+ /**
+ * Compares contents of two bundles.
+ *
+ * @param a a bundle
+ * @param b another bundle
+ * @return {@code true} if two bundles are the same. {@code false} otherwise. This may be
+ * incorrect if any bundle contains a bundle.
+ */
+ public static boolean equals(Bundle a, Bundle b) {
+ if (a == b) {
+ return true;
+ }
+ if (a == null || b == null) {
+ return false;
+ }
+ if (!a.keySet().containsAll(b.keySet())
+ || !b.keySet().containsAll(a.keySet())) {
+ return false;
+ }
+ for (String key : a.keySet()) {
+ if (!Objects.equals(a.get(key), b.get(key))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Handler that always waits until the Runnable finishes.
+ */
+ public static class SyncHandler extends Handler {
+ public SyncHandler(Looper looper) {
+ super(looper);
+ }
+
+ public void postAndSync(Runnable runnable) throws InterruptedException {
+ final CountDownLatch latch = new CountDownLatch(1);
+ if (getLooper() == Looper.myLooper()) {
+ runnable.run();
+ } else {
+ post(()->{
+ runnable.run();
+ latch.countDown();
+ });
+ assertTrue(latch.await(WAIT_TIME_MS, TimeUnit.MILLISECONDS));
+ }
+ }
+ }
+}
diff --git a/android/media/session/MediaSessionManager.java b/android/media/session/MediaSessionManager.java
index b215825c..81b4603e 100644
--- a/android/media/session/MediaSessionManager.java
+++ b/android/media/session/MediaSessionManager.java
@@ -24,8 +24,12 @@ import android.annotation.SystemService;
import android.content.ComponentName;
import android.content.Context;
import android.media.AudioManager;
+import android.media.IMediaSession2;
import android.media.IRemoteVolumeController;
-import android.media.session.ISessionManager;
+import android.media.MediaSession2;
+import android.media.MediaSessionService2;
+import android.media.SessionToken2;
+import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
@@ -38,6 +42,7 @@ import android.util.Log;
import android.view.KeyEvent;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -331,6 +336,101 @@ public final class MediaSessionManager {
}
/**
+ * Called when a {@link MediaSession2} is created.
+ *
+ * @hide
+ */
+ // TODO(jaewan): System API
+ public SessionToken2 createSessionToken(@NonNull String callingPackage, @NonNull String id,
+ @NonNull IMediaSession2 binder) {
+ try {
+ Bundle bundle = mService.createSessionToken(callingPackage, id, binder);
+ return SessionToken2.fromBundle(bundle);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Cannot communicate with the service.", e);
+ }
+ return null;
+ }
+
+ /**
+ * Get {@link List} of {@link SessionToken2} whose sessions are active now. This list represents
+ * active sessions regardless of whether they're {@link MediaSession2} or
+ * {@link MediaSessionService2}.
+ *
+ * @return list of Tokens
+ * @hide
+ */
+ // TODO(jaewan): Unhide
+ // TODO(jaewan): Protect this with permission.
+ // TODO(jaewna): Add listener for change in lists.
+ public List<SessionToken2> getActiveSessionTokens() {
+ try {
+ List<Bundle> bundles = mService.getSessionTokens(
+ /* activeSessionOnly */ true, /* sessionServiceOnly */ false);
+ return toTokenList(bundles);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Cannot communicate with the service.", e);
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Get {@link List} of {@link SessionToken2} for {@link MediaSessionService2} regardless of their
+ * activeness. This list represents media apps that support background playback.
+ *
+ * @return list of Tokens
+ * @hide
+ */
+ // TODO(jaewan): Unhide
+ // TODO(jaewna): Add listener for change in lists.
+ public List<SessionToken2> getSessionServiceTokens() {
+ try {
+ List<Bundle> bundles = mService.getSessionTokens(
+ /* activeSessionOnly */ false, /* sessionServiceOnly */ true);
+ return toTokenList(bundles);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Cannot communicate with the service.", e);
+ return Collections.emptyList();
+ }
+ }
+
+ /**
+ * Get all {@link SessionToken2}s. This is the combined list of {@link #getActiveSessionTokens()}
+ * and {@link #getSessionServiceTokens}.
+ *
+ * @return list of Tokens
+ * @see #getActiveSessionTokens
+ * @see #getSessionServiceTokens
+ * @hide
+ */
+ // TODO(jaewan): Unhide
+ // TODO(jaewan): Protect this with permission.
+ // TODO(jaewna): Add listener for change in lists.
+ public List<SessionToken2> getAllSessionTokens() {
+ try {
+ List<Bundle> bundles = mService.getSessionTokens(
+ /* activeSessionOnly */ false, /* sessionServiceOnly */ false);
+ return toTokenList(bundles);
+ } catch (RemoteException e) {
+ Log.wtf(TAG, "Cannot communicate with the service.", e);
+ return Collections.emptyList();
+ }
+ }
+
+ private static List<SessionToken2> toTokenList(List<Bundle> bundles) {
+ List<SessionToken2> tokens = new ArrayList<>();
+ if (bundles != null) {
+ for (int i = 0; i < bundles.size(); i++) {
+ SessionToken2 token = SessionToken2.fromBundle(bundles.get(i));
+ if (token != null) {
+ tokens.add(token);
+ }
+ }
+ }
+ return tokens;
+ }
+
+ /**
* Check if the global priority session is currently active. This can be
* used to decide if media keys should be sent to the session or to the app.
*
diff --git a/android/media/update/ApiLoader.java b/android/media/update/ApiLoader.java
new file mode 100644
index 00000000..b928e931
--- /dev/null
+++ b/android/media/update/ApiLoader.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.media.update;
+
+import android.content.res.Resources;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Build;
+
+/**
+ * @hide
+ */
+public final class ApiLoader {
+ private static Object sMediaLibrary;
+
+ private static final String UPDATE_PACKAGE = "com.android.media.update";
+ private static final String UPDATE_CLASS = "com.android.media.update.ApiFactory";
+ private static final String UPDATE_METHOD = "initialize";
+
+ private ApiLoader() { }
+
+ public static StaticProvider getProvider(Context context) {
+ try {
+ return (StaticProvider) getMediaLibraryImpl(context);
+ } catch (PackageManager.NameNotFoundException | ReflectiveOperationException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ // TODO This method may do I/O; Ensure it does not violate (emit warnings in) strict mode.
+ private static synchronized Object getMediaLibraryImpl(Context context)
+ throws PackageManager.NameNotFoundException, ReflectiveOperationException {
+ if (sMediaLibrary != null) return sMediaLibrary;
+
+ // TODO Figure out when to use which package (query media update service)
+ int flags = Build.IS_DEBUGGABLE ? 0 : PackageManager.MATCH_FACTORY_ONLY;
+ Context libContext = context.createApplicationContext(
+ context.getPackageManager().getPackageInfo(UPDATE_PACKAGE, flags).applicationInfo,
+ Context.CONTEXT_INCLUDE_CODE | Context.CONTEXT_IGNORE_SECURITY);
+ sMediaLibrary = libContext.getClassLoader()
+ .loadClass(UPDATE_CLASS)
+ .getMethod(UPDATE_METHOD, Resources.class, Resources.Theme.class)
+ .invoke(null, libContext.getResources(), libContext.getTheme());
+ return sMediaLibrary;
+ }
+}
diff --git a/android/media/update/MediaBrowser2Provider.java b/android/media/update/MediaBrowser2Provider.java
new file mode 100644
index 00000000..e48711d9
--- /dev/null
+++ b/android/media/update/MediaBrowser2Provider.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 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.media.update;
+
+import android.os.Bundle;
+
+/**
+ * @hide
+ */
+public interface MediaBrowser2Provider extends MediaController2Provider {
+ void getBrowserRoot_impl(Bundle rootHints);
+
+ void subscribe_impl(String parentId, Bundle options);
+ void unsubscribe_impl(String parentId, Bundle options);
+
+ void getItem_impl(String mediaId);
+ void getChildren_impl(String parentId, int page, int pageSize, Bundle options);
+ void search_impl(String query, int page, int pageSize, Bundle extras);
+}
diff --git a/android/media/update/MediaControlView2Provider.java b/android/media/update/MediaControlView2Provider.java
new file mode 100644
index 00000000..6b38c926
--- /dev/null
+++ b/android/media/update/MediaControlView2Provider.java
@@ -0,0 +1,47 @@
+/*
+ * 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.media.update;
+
+import android.annotation.SystemApi;
+import android.media.session.MediaController;
+import android.view.View;
+
+/**
+ * Interface for connecting the public API to an updatable implementation.
+ *
+ * Each instance object is connected to one corresponding updatable object which implements the
+ * runtime behavior of that class. There should a corresponding provider method for all public
+ * methods.
+ *
+ * All methods behave as per their namesake in the public API.
+ *
+ * @see android.widget.MediaControlView2
+ *
+ * @hide
+ */
+// TODO @SystemApi
+public interface MediaControlView2Provider extends ViewProvider {
+ void setController_impl(MediaController controller);
+ void show_impl();
+ void show_impl(int timeout);
+ boolean isShowing_impl();
+ void hide_impl();
+ void showSubtitle_impl();
+ void hideSubtitle_impl();
+ void setPrevNextListeners_impl(View.OnClickListener next, View.OnClickListener prev);
+ void setButtonVisibility_impl(int button, boolean visible);
+}
diff --git a/android/media/update/MediaController2Provider.java b/android/media/update/MediaController2Provider.java
new file mode 100644
index 00000000..c5f6b963
--- /dev/null
+++ b/android/media/update/MediaController2Provider.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright 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.media.update;
+
+import android.app.PendingIntent;
+import android.media.MediaController2.PlaybackInfo;
+import android.media.MediaItem2;
+import android.media.MediaSession2.Command;
+import android.media.MediaSession2.PlaylistParam;
+import android.media.PlaybackState2;
+import android.media.Rating2;
+import android.media.SessionToken2;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import java.util.List;
+
+/**
+ * @hide
+ */
+public interface MediaController2Provider extends TransportControlProvider {
+ void close_impl();
+ SessionToken2 getSessionToken_impl();
+ boolean isConnected_impl();
+
+ PendingIntent getSessionActivity_impl();
+ int getRatingType_impl();
+
+ void setVolumeTo_impl(int value, int flags);
+ void adjustVolume_impl(int direction, int flags);
+ PlaybackInfo getPlaybackInfo_impl();
+
+ void prepareFromUri_impl(Uri uri, Bundle extras);
+ void prepareFromSearch_impl(String query, Bundle extras);
+ void prepareMediaId_impl(String mediaId, Bundle extras);
+ void playFromSearch_impl(String query, Bundle extras);
+ void playFromUri_impl(String uri, Bundle extras);
+ void playFromMediaId_impl(String mediaId, Bundle extras);
+
+ void setRating_impl(Rating2 rating);
+ void sendCustomCommand_impl(Command command, Bundle args, ResultReceiver cb);
+ List<MediaItem2> getPlaylist_impl();
+
+ void removePlaylistItem_impl(MediaItem2 index);
+ void addPlaylistItem_impl(int index, MediaItem2 item);
+
+ PlaylistParam getPlaylistParam_impl();
+ PlaybackState2 getPlaybackState_impl();
+}
diff --git a/android/media/update/MediaLibraryService2Provider.java b/android/media/update/MediaLibraryService2Provider.java
new file mode 100644
index 00000000..dac57841
--- /dev/null
+++ b/android/media/update/MediaLibraryService2Provider.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 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.media.update;
+
+import android.media.MediaSession2.ControllerInfo;
+import android.os.Bundle; /**
+ * @hide
+ */
+public interface MediaLibraryService2Provider extends MediaSessionService2Provider {
+ // Nothing new for now
+
+ interface MediaLibrarySessionProvider extends MediaSession2Provider {
+ void notifyChildrenChanged_impl(ControllerInfo controller, String parentId, Bundle options);
+ void notifyChildrenChanged_impl(String parentId, Bundle options);
+ }
+}
diff --git a/android/media/update/MediaSession2Provider.java b/android/media/update/MediaSession2Provider.java
new file mode 100644
index 00000000..2a68ad1d
--- /dev/null
+++ b/android/media/update/MediaSession2Provider.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 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.media.update;
+
+import android.media.AudioAttributes;
+import android.media.MediaItem2;
+import android.media.MediaPlayerBase;
+import android.media.MediaSession2;
+import android.media.MediaSession2.Command;
+import android.media.MediaSession2.CommandButton;
+import android.media.MediaSession2.CommandGroup;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.SessionToken2;
+import android.media.VolumeProvider;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+
+import java.util.List;
+
+/**
+ * @hide
+ */
+public interface MediaSession2Provider extends TransportControlProvider {
+ void close_impl();
+ void setPlayer_impl(MediaPlayerBase player);
+ void setPlayer_impl(MediaPlayerBase player, VolumeProvider volumeProvider);
+ MediaPlayerBase getPlayer_impl();
+ SessionToken2 getToken_impl();
+ List<ControllerInfo> getConnectedControllers_impl();
+ void setCustomLayout_impl(ControllerInfo controller, List<CommandButton> layout);
+ void setAudioAttributes_impl(AudioAttributes attributes);
+ void setAudioFocusRequest_impl(int focusGain);
+
+ void setAllowedCommands_impl(ControllerInfo controller, CommandGroup commands);
+ void notifyMetadataChanged_impl();
+ void sendCustomCommand_impl(ControllerInfo controller, Command command, Bundle args,
+ ResultReceiver receiver);
+ void sendCustomCommand_impl(Command command, Bundle args);
+ void setPlaylist_impl(List<MediaItem2> playlist, MediaSession2.PlaylistParam param);
+
+ /**
+ * @hide
+ */
+ interface ControllerInfoProvider {
+ String getPackageName_impl();
+ int getUid_impl();
+ boolean isTrusted_impl();
+ int hashCode_impl();
+ boolean equals_impl(ControllerInfoProvider obj);
+ }
+}
diff --git a/android/media/update/MediaSessionService2Provider.java b/android/media/update/MediaSessionService2Provider.java
new file mode 100644
index 00000000..a6b462b8
--- /dev/null
+++ b/android/media/update/MediaSessionService2Provider.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 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.media.update;
+
+import android.content.Intent;
+import android.media.MediaSession2;
+import android.media.MediaSessionService2.MediaNotification;
+import android.media.PlaybackState2;
+import android.os.IBinder;
+
+/**
+ * @hide
+ */
+public interface MediaSessionService2Provider {
+ MediaSession2 getSession_impl();
+ MediaNotification onUpdateNotification_impl(PlaybackState2 state);
+
+ // Service
+ void onCreate_impl();
+ IBinder onBind_impl(Intent intent);
+}
diff --git a/android/media/update/StaticProvider.java b/android/media/update/StaticProvider.java
new file mode 100644
index 00000000..7c222c3c
--- /dev/null
+++ b/android/media/update/StaticProvider.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.update;
+
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.media.IMediaSession2Callback;
+import android.media.MediaBrowser2;
+import android.media.MediaBrowser2.BrowserCallback;
+import android.media.MediaController2;
+import android.media.MediaController2.ControllerCallback;
+import android.media.MediaLibraryService2;
+import android.media.MediaLibraryService2.MediaLibrarySession;
+import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
+import android.media.MediaPlayerBase;
+import android.media.MediaSession2;
+import android.media.MediaSession2.SessionCallback;
+import android.media.MediaSessionService2;
+import android.media.SessionToken2;
+import android.media.VolumeProvider;
+import android.media.update.MediaLibraryService2Provider.MediaLibrarySessionProvider;
+import android.media.update.MediaSession2Provider.ControllerInfoProvider;
+import android.util.AttributeSet;
+import android.widget.MediaControlView2;
+import android.widget.VideoView2;
+
+import java.util.concurrent.Executor;
+
+/**
+ * Interface for connecting the public API to an updatable implementation.
+ *
+ * This interface provides access to constructors and static methods that are otherwise not directly
+ * accessible via an implementation object.
+ *
+ * @hide
+ */
+// TODO @SystemApi
+public interface StaticProvider {
+ MediaControlView2Provider createMediaControlView2(
+ MediaControlView2 instance, ViewProvider superProvider);
+ VideoView2Provider createVideoView2(
+ VideoView2 instance, ViewProvider superProvider,
+ @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes);
+
+ MediaSession2Provider createMediaSession2(MediaSession2 mediaSession2, Context context,
+ MediaPlayerBase player, String id, Executor callbackExecutor, SessionCallback callback,
+ VolumeProvider volumeProvider, int ratingType,
+ PendingIntent sessionActivity);
+ ControllerInfoProvider createMediaSession2ControllerInfoProvider(
+ MediaSession2.ControllerInfo instance, Context context, int uid, int pid,
+ String packageName, IMediaSession2Callback callback);
+ MediaController2Provider createMediaController2(
+ MediaController2 instance, Context context, SessionToken2 token,
+ ControllerCallback callback, Executor executor);
+ MediaBrowser2Provider createMediaBrowser2(
+ MediaBrowser2 instance, Context context, SessionToken2 token,
+ BrowserCallback callback, Executor executor);
+ MediaSessionService2Provider createMediaSessionService2(
+ MediaSessionService2 instance);
+ MediaSessionService2Provider createMediaLibraryService2(
+ MediaLibraryService2 instance);
+ MediaLibrarySessionProvider createMediaLibraryService2MediaLibrarySession(
+ MediaLibrarySession instance, Context context, MediaPlayerBase player, String id,
+ Executor callbackExecutor, MediaLibrarySessionCallback callback,
+ VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity);
+}
diff --git a/android/media/update/TransportControlProvider.java b/android/media/update/TransportControlProvider.java
new file mode 100644
index 00000000..5217a9d9
--- /dev/null
+++ b/android/media/update/TransportControlProvider.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 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.media.update;
+
+import android.media.MediaPlayerBase;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+
+/**
+ * @hide
+ */
+// TODO(jaewan): SystemApi
+public interface TransportControlProvider {
+ void play_impl();
+ void pause_impl();
+ void stop_impl();
+ void skipToPrevious_impl();
+ void skipToNext_impl();
+
+ void prepare_impl();
+ void fastForward_impl();
+ void rewind_impl();
+ void seekTo_impl(long pos);
+ void setCurrentPlaylistItem_impl(int index);
+}
diff --git a/android/media/update/VideoView2Provider.java b/android/media/update/VideoView2Provider.java
new file mode 100644
index 00000000..416ea98d
--- /dev/null
+++ b/android/media/update/VideoView2Provider.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 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.media.update;
+
+import android.media.AudioAttributes;
+import android.media.MediaPlayerBase;
+import android.net.Uri;
+import android.widget.MediaControlView2;
+import android.widget.VideoView2;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Interface for connecting the public API to an updatable implementation.
+ *
+ * Each instance object is connected to one corresponding updatable object which implements the
+ * runtime behavior of that class. There should a corresponding provider method for all public
+ * methods.
+ *
+ * All methods behave as per their namesake in the public API.
+ *
+ * @see android.widget.VideoView2
+ *
+ * @hide
+ */
+// TODO @SystemApi
+public interface VideoView2Provider extends ViewProvider {
+ void setMediaControlView2_impl(MediaControlView2 mediaControlView);
+ MediaControlView2 getMediaControlView2_impl();
+ void start_impl();
+ void pause_impl();
+ int getDuration_impl();
+ int getCurrentPosition_impl();
+ void seekTo_impl(int msec);
+ boolean isPlaying_impl();
+ int getBufferPercentage_impl();
+ int getAudioSessionId_impl();
+ void showSubtitle_impl();
+ void hideSubtitle_impl();
+ void setFullScreen_impl(boolean fullScreen);
+ void setSpeed_impl(float speed);
+ float getSpeed_impl();
+ void setAudioFocusRequest_impl(int focusGain);
+ void setAudioAttributes_impl(AudioAttributes attributes);
+ void setRouteAttributes_impl(List<String> routeCategories, MediaPlayerBase player);
+ void setVideoPath_impl(String path);
+ void setVideoURI_impl(Uri uri);
+ void setVideoURI_impl(Uri uri, Map<String, String> headers);
+ void setViewType_impl(int viewType);
+ int getViewType_impl();
+ void stopPlayback_impl();
+ void setOnPreparedListener_impl(VideoView2.OnPreparedListener l);
+ void setOnCompletionListener_impl(VideoView2.OnCompletionListener l);
+ void setOnErrorListener_impl(VideoView2.OnErrorListener l);
+ void setOnInfoListener_impl(VideoView2.OnInfoListener l);
+ void setOnViewTypeChangedListener_impl(VideoView2.OnViewTypeChangedListener l);
+ void setFullScreenChangedListener_impl(VideoView2.OnFullScreenChangedListener l);
+}
diff --git a/android/media/update/ViewProvider.java b/android/media/update/ViewProvider.java
new file mode 100644
index 00000000..78c5b36f
--- /dev/null
+++ b/android/media/update/ViewProvider.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 android.media.update;
+
+import android.annotation.SystemApi;
+import android.graphics.Canvas;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+/**
+ * Interface for connecting the public API to an updatable implementation.
+ *
+ * Each instance object is connected to one corresponding updatable object which implements the
+ * runtime behavior of that class. There should a corresponding provider method for all public
+ * methods.
+ *
+ * All methods behave as per their namesake in the public API.
+ *
+ * @see android.view.View
+ *
+ * @hide
+ */
+// TODO @SystemApi
+public interface ViewProvider {
+ // TODO Add more (all?) methods from View
+ void onAttachedToWindow_impl();
+ void onDetachedFromWindow_impl();
+ CharSequence getAccessibilityClassName_impl();
+ boolean onTouchEvent_impl(MotionEvent ev);
+ boolean onTrackballEvent_impl(MotionEvent ev);
+ boolean onKeyDown_impl(int keyCode, KeyEvent event);
+ void onFinishInflate_impl();
+ boolean dispatchKeyEvent_impl(KeyEvent event);
+ void setEnabled_impl(boolean enabled);
+}
diff --git a/android/net/ConnectivityManager.java b/android/net/ConnectivityManager.java
index 11d338d0..166342dd 100644
--- a/android/net/ConnectivityManager.java
+++ b/android/net/ConnectivityManager.java
@@ -3763,4 +3763,20 @@ public class ConnectivityManager {
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * The network watchlist is a list of domains and IP addresses that are associated with
+ * potentially harmful apps. This method returns the hash of the watchlist currently
+ * used by the system.
+ *
+ * @return Hash of network watchlist config file. Null if config does not exist.
+ */
+ public byte[] getNetworkWatchlistConfigHash() {
+ try {
+ return mService.getNetworkWatchlistConfigHash();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to get watchlist config hash");
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/android/net/IpSecAlgorithm.java b/android/net/IpSecAlgorithm.java
index f82627b9..c69a4d4c 100644
--- a/android/net/IpSecAlgorithm.java
+++ b/android/net/IpSecAlgorithm.java
@@ -231,13 +231,44 @@ public final class IpSecAlgorithm implements Parcelable {
}
}
+ /** @hide */
+ public boolean isAuthentication() {
+ switch (getName()) {
+ // Fallthrough
+ case AUTH_HMAC_MD5:
+ case AUTH_HMAC_SHA1:
+ case AUTH_HMAC_SHA256:
+ case AUTH_HMAC_SHA384:
+ case AUTH_HMAC_SHA512:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /** @hide */
+ public boolean isEncryption() {
+ return getName().equals(CRYPT_AES_CBC);
+ }
+
+ /** @hide */
+ public boolean isAead() {
+ return getName().equals(AUTH_CRYPT_AES_GCM);
+ }
+
+ // Because encryption keys are sensitive and userdebug builds are used by large user pools
+ // such as beta testers, we only allow sensitive info such as keys on eng builds.
+ private static boolean isUnsafeBuild() {
+ return Build.IS_DEBUGGABLE && Build.IS_ENG;
+ }
+
@Override
public String toString() {
return new StringBuilder()
.append("{mName=")
.append(mName)
.append(", mKey=")
- .append(Build.IS_DEBUGGABLE ? HexDump.toHexString(mKey) : "<hidden>")
+ .append(isUnsafeBuild() ? HexDump.toHexString(mKey) : "<hidden>")
.append(", mTruncLenBits=")
.append(mTruncLenBits)
.append("}")
diff --git a/android/net/IpSecConfig.java b/android/net/IpSecConfig.java
index e6cd3fc1..6a262e2c 100644
--- a/android/net/IpSecConfig.java
+++ b/android/net/IpSecConfig.java
@@ -32,59 +32,29 @@ public final class IpSecConfig implements Parcelable {
// MODE_TRANSPORT or MODE_TUNNEL
private int mMode = IpSecTransform.MODE_TRANSPORT;
- // Needs to be valid only for tunnel mode
// Preventing this from being null simplifies Java->Native binder
- private String mLocalAddress = "";
+ private String mSourceAddress = "";
// Preventing this from being null simplifies Java->Native binder
- private String mRemoteAddress = "";
+ private String mDestinationAddress = "";
// The underlying Network that represents the "gateway" Network
// for outbound packets. It may also be used to select packets.
private Network mNetwork;
- /**
- * This class captures the parameters that specifically apply to inbound or outbound traffic.
- */
- public static class Flow {
- // Minimum requirements for identifying a transform
- // SPI identifying the IPsec flow in packet processing
- // and a remote IP address
- private int mSpiResourceId = IpSecManager.INVALID_RESOURCE_ID;
-
- // Encryption Algorithm
- private IpSecAlgorithm mEncryption;
-
- // Authentication Algorithm
- private IpSecAlgorithm mAuthentication;
-
- // Authenticated Encryption Algorithm
- private IpSecAlgorithm mAuthenticatedEncryption;
-
- @Override
- public String toString() {
- return new StringBuilder()
- .append("{mSpiResourceId=")
- .append(mSpiResourceId)
- .append(", mEncryption=")
- .append(mEncryption)
- .append(", mAuthentication=")
- .append(mAuthentication)
- .append(", mAuthenticatedEncryption=")
- .append(mAuthenticatedEncryption)
- .append("}")
- .toString();
- }
-
- static boolean equals(IpSecConfig.Flow lhs, IpSecConfig.Flow rhs) {
- if (lhs == null || rhs == null) return (lhs == rhs);
- return (lhs.mSpiResourceId == rhs.mSpiResourceId
- && IpSecAlgorithm.equals(lhs.mEncryption, rhs.mEncryption)
- && IpSecAlgorithm.equals(lhs.mAuthentication, rhs.mAuthentication));
- }
- }
-
- private final Flow[] mFlow = new Flow[] {new Flow(), new Flow()};
+ // Minimum requirements for identifying a transform
+ // SPI identifying the IPsec SA in packet processing
+ // and a destination IP address
+ private int mSpiResourceId = IpSecManager.INVALID_RESOURCE_ID;
+
+ // Encryption Algorithm
+ private IpSecAlgorithm mEncryption;
+
+ // Authentication Algorithm
+ private IpSecAlgorithm mAuthentication;
+
+ // Authenticated Encryption Algorithm
+ private IpSecAlgorithm mAuthenticatedEncryption;
// For tunnel mode IPv4 UDP Encapsulation
// IpSecTransform#ENCAP_ESP_*, such as ENCAP_ESP_OVER_UDP_IKE
@@ -95,47 +65,46 @@ public final class IpSecConfig implements Parcelable {
// An interval, in seconds between the NattKeepalive packets
private int mNattKeepaliveInterval;
+ // XFRM mark and mask
+ private int mMarkValue;
+ private int mMarkMask;
+
/** Set the mode for this IPsec transform */
public void setMode(int mode) {
mMode = mode;
}
- /** Set the local IP address for Tunnel mode */
- public void setLocalAddress(String localAddress) {
- if (localAddress == null) {
- throw new IllegalArgumentException("localAddress may not be null!");
- }
- mLocalAddress = localAddress;
+ /** Set the source IP addres for this IPsec transform */
+ public void setSourceAddress(String sourceAddress) {
+ mSourceAddress = sourceAddress;
}
- /** Set the remote IP address for this IPsec transform */
- public void setRemoteAddress(String remoteAddress) {
- if (remoteAddress == null) {
- throw new IllegalArgumentException("remoteAddress may not be null!");
- }
- mRemoteAddress = remoteAddress;
+ /** Set the destination IP address for this IPsec transform */
+ public void setDestinationAddress(String destinationAddress) {
+ mDestinationAddress = destinationAddress;
}
- /** Set the SPI for a given direction by resource ID */
- public void setSpiResourceId(int direction, int resourceId) {
- mFlow[direction].mSpiResourceId = resourceId;
+ /** Set the SPI by resource ID */
+ public void setSpiResourceId(int resourceId) {
+ mSpiResourceId = resourceId;
}
- /** Set the encryption algorithm for a given direction */
- public void setEncryption(int direction, IpSecAlgorithm encryption) {
- mFlow[direction].mEncryption = encryption;
+ /** Set the encryption algorithm */
+ public void setEncryption(IpSecAlgorithm encryption) {
+ mEncryption = encryption;
}
- /** Set the authentication algorithm for a given direction */
- public void setAuthentication(int direction, IpSecAlgorithm authentication) {
- mFlow[direction].mAuthentication = authentication;
+ /** Set the authentication algorithm */
+ public void setAuthentication(IpSecAlgorithm authentication) {
+ mAuthentication = authentication;
}
- /** Set the authenticated encryption algorithm for a given direction */
- public void setAuthenticatedEncryption(int direction, IpSecAlgorithm authenticatedEncryption) {
- mFlow[direction].mAuthenticatedEncryption = authenticatedEncryption;
+ /** Set the authenticated encryption algorithm */
+ public void setAuthenticatedEncryption(IpSecAlgorithm authenticatedEncryption) {
+ mAuthenticatedEncryption = authenticatedEncryption;
}
+ /** Set the underlying network that will carry traffic for this transform */
public void setNetwork(Network network) {
mNetwork = network;
}
@@ -156,33 +125,41 @@ public final class IpSecConfig implements Parcelable {
mNattKeepaliveInterval = interval;
}
+ public void setMarkValue(int mark) {
+ mMarkValue = mark;
+ }
+
+ public void setMarkMask(int mask) {
+ mMarkMask = mask;
+ }
+
// Transport or Tunnel
public int getMode() {
return mMode;
}
- public String getLocalAddress() {
- return mLocalAddress;
+ public String getSourceAddress() {
+ return mSourceAddress;
}
- public int getSpiResourceId(int direction) {
- return mFlow[direction].mSpiResourceId;
+ public int getSpiResourceId() {
+ return mSpiResourceId;
}
- public String getRemoteAddress() {
- return mRemoteAddress;
+ public String getDestinationAddress() {
+ return mDestinationAddress;
}
- public IpSecAlgorithm getEncryption(int direction) {
- return mFlow[direction].mEncryption;
+ public IpSecAlgorithm getEncryption() {
+ return mEncryption;
}
- public IpSecAlgorithm getAuthentication(int direction) {
- return mFlow[direction].mAuthentication;
+ public IpSecAlgorithm getAuthentication() {
+ return mAuthentication;
}
- public IpSecAlgorithm getAuthenticatedEncryption(int direction) {
- return mFlow[direction].mAuthenticatedEncryption;
+ public IpSecAlgorithm getAuthenticatedEncryption() {
+ return mAuthenticatedEncryption;
}
public Network getNetwork() {
@@ -205,6 +182,14 @@ public final class IpSecConfig implements Parcelable {
return mNattKeepaliveInterval;
}
+ public int getMarkValue() {
+ return mMarkValue;
+ }
+
+ public int getMarkMask() {
+ return mMarkMask;
+ }
+
// Parcelable Methods
@Override
@@ -215,21 +200,19 @@ public final class IpSecConfig implements Parcelable {
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(mMode);
- out.writeString(mLocalAddress);
- out.writeString(mRemoteAddress);
+ out.writeString(mSourceAddress);
+ out.writeString(mDestinationAddress);
out.writeParcelable(mNetwork, flags);
- out.writeInt(mFlow[IpSecTransform.DIRECTION_IN].mSpiResourceId);
- out.writeParcelable(mFlow[IpSecTransform.DIRECTION_IN].mEncryption, flags);
- out.writeParcelable(mFlow[IpSecTransform.DIRECTION_IN].mAuthentication, flags);
- out.writeParcelable(mFlow[IpSecTransform.DIRECTION_IN].mAuthenticatedEncryption, flags);
- out.writeInt(mFlow[IpSecTransform.DIRECTION_OUT].mSpiResourceId);
- out.writeParcelable(mFlow[IpSecTransform.DIRECTION_OUT].mEncryption, flags);
- out.writeParcelable(mFlow[IpSecTransform.DIRECTION_OUT].mAuthentication, flags);
- out.writeParcelable(mFlow[IpSecTransform.DIRECTION_OUT].mAuthenticatedEncryption, flags);
+ out.writeInt(mSpiResourceId);
+ out.writeParcelable(mEncryption, flags);
+ out.writeParcelable(mAuthentication, flags);
+ out.writeParcelable(mAuthenticatedEncryption, flags);
out.writeInt(mEncapType);
out.writeInt(mEncapSocketResourceId);
out.writeInt(mEncapRemotePort);
out.writeInt(mNattKeepaliveInterval);
+ out.writeInt(mMarkValue);
+ out.writeInt(mMarkMask);
}
@VisibleForTesting
@@ -237,27 +220,22 @@ public final class IpSecConfig implements Parcelable {
private IpSecConfig(Parcel in) {
mMode = in.readInt();
- mLocalAddress = in.readString();
- mRemoteAddress = in.readString();
+ mSourceAddress = in.readString();
+ mDestinationAddress = in.readString();
mNetwork = (Network) in.readParcelable(Network.class.getClassLoader());
- mFlow[IpSecTransform.DIRECTION_IN].mSpiResourceId = in.readInt();
- mFlow[IpSecTransform.DIRECTION_IN].mEncryption =
- (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
- mFlow[IpSecTransform.DIRECTION_IN].mAuthentication =
- (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
- mFlow[IpSecTransform.DIRECTION_IN].mAuthenticatedEncryption =
- (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
- mFlow[IpSecTransform.DIRECTION_OUT].mSpiResourceId = in.readInt();
- mFlow[IpSecTransform.DIRECTION_OUT].mEncryption =
+ mSpiResourceId = in.readInt();
+ mEncryption =
(IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
- mFlow[IpSecTransform.DIRECTION_OUT].mAuthentication =
+ mAuthentication =
(IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
- mFlow[IpSecTransform.DIRECTION_OUT].mAuthenticatedEncryption =
+ mAuthenticatedEncryption =
(IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
mEncapType = in.readInt();
mEncapSocketResourceId = in.readInt();
mEncapRemotePort = in.readInt();
mNattKeepaliveInterval = in.readInt();
+ mMarkValue = in.readInt();
+ mMarkMask = in.readInt();
}
@Override
@@ -266,10 +244,10 @@ public final class IpSecConfig implements Parcelable {
strBuilder
.append("{mMode=")
.append(mMode == IpSecTransform.MODE_TUNNEL ? "TUNNEL" : "TRANSPORT")
- .append(", mLocalAddress=")
- .append(mLocalAddress)
- .append(", mRemoteAddress=")
- .append(mRemoteAddress)
+ .append(", mSourceAddress=")
+ .append(mSourceAddress)
+ .append(", mDestinationAddress=")
+ .append(mDestinationAddress)
.append(", mNetwork=")
.append(mNetwork)
.append(", mEncapType=")
@@ -280,10 +258,18 @@ public final class IpSecConfig implements Parcelable {
.append(mEncapRemotePort)
.append(", mNattKeepaliveInterval=")
.append(mNattKeepaliveInterval)
- .append(", mFlow[OUT]=")
- .append(mFlow[IpSecTransform.DIRECTION_OUT])
- .append(", mFlow[IN]=")
- .append(mFlow[IpSecTransform.DIRECTION_IN])
+ .append("{mSpiResourceId=")
+ .append(mSpiResourceId)
+ .append(", mEncryption=")
+ .append(mEncryption)
+ .append(", mAuthentication=")
+ .append(mAuthentication)
+ .append(", mAuthenticatedEncryption=")
+ .append(mAuthenticatedEncryption)
+ .append(", mMarkValue=")
+ .append(mMarkValue)
+ .append(", mMarkMask=")
+ .append(mMarkMask)
.append("}");
return strBuilder.toString();
@@ -305,17 +291,20 @@ public final class IpSecConfig implements Parcelable {
public static boolean equals(IpSecConfig lhs, IpSecConfig rhs) {
if (lhs == null || rhs == null) return (lhs == rhs);
return (lhs.mMode == rhs.mMode
- && lhs.mLocalAddress.equals(rhs.mLocalAddress)
- && lhs.mRemoteAddress.equals(rhs.mRemoteAddress)
+ && lhs.mSourceAddress.equals(rhs.mSourceAddress)
+ && lhs.mDestinationAddress.equals(rhs.mDestinationAddress)
&& ((lhs.mNetwork != null && lhs.mNetwork.equals(rhs.mNetwork))
|| (lhs.mNetwork == rhs.mNetwork))
&& lhs.mEncapType == rhs.mEncapType
&& lhs.mEncapSocketResourceId == rhs.mEncapSocketResourceId
&& lhs.mEncapRemotePort == rhs.mEncapRemotePort
&& lhs.mNattKeepaliveInterval == rhs.mNattKeepaliveInterval
- && IpSecConfig.Flow.equals(lhs.mFlow[IpSecTransform.DIRECTION_OUT],
- rhs.mFlow[IpSecTransform.DIRECTION_OUT])
- && IpSecConfig.Flow.equals(lhs.mFlow[IpSecTransform.DIRECTION_IN],
- rhs.mFlow[IpSecTransform.DIRECTION_IN]));
+ && lhs.mSpiResourceId == rhs.mSpiResourceId
+ && IpSecAlgorithm.equals(lhs.mEncryption, rhs.mEncryption)
+ && IpSecAlgorithm.equals(
+ lhs.mAuthenticatedEncryption, rhs.mAuthenticatedEncryption)
+ && IpSecAlgorithm.equals(lhs.mAuthentication, rhs.mAuthentication)
+ && lhs.mMarkValue == rhs.mMarkValue
+ && lhs.mMarkMask == rhs.mMarkMask);
}
}
diff --git a/android/net/IpSecManager.java b/android/net/IpSecManager.java
index 6a4b8914..24a078fc 100644
--- a/android/net/IpSecManager.java
+++ b/android/net/IpSecManager.java
@@ -17,7 +17,9 @@ package android.net;
import static com.android.internal.util.Preconditions.checkNotNull;
+import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.TestApi;
import android.content.Context;
@@ -33,6 +35,8 @@ import dalvik.system.CloseGuard;
import java.io.FileDescriptor;
import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.Socket;
@@ -53,6 +57,23 @@ public final class IpSecManager {
private static final String TAG = "IpSecManager";
/**
+ * For direction-specific attributes of an {@link IpSecTransform}, indicates that an attribute
+ * applies to traffic towards the host.
+ */
+ public static final int DIRECTION_IN = 0;
+
+ /**
+ * For direction-specific attributes of an {@link IpSecTransform}, indicates that an attribute
+ * applies to traffic from the host.
+ */
+ public static final int DIRECTION_OUT = 1;
+
+ /** @hide */
+ @IntDef(value = {DIRECTION_IN, DIRECTION_OUT})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface PolicyDirection {}
+
+ /**
* The Security Parameter Index (SPI) 0 indicates an unknown or invalid index.
*
* <p>No IPsec packet may contain an SPI of 0.
@@ -69,7 +90,7 @@ public final class IpSecManager {
}
/** @hide */
- public static final int INVALID_RESOURCE_ID = 0;
+ public static final int INVALID_RESOURCE_ID = -1;
/**
* Thrown to indicate that a requested SPI is in use.
@@ -125,10 +146,10 @@ public final class IpSecManager {
*/
public static final class SecurityParameterIndex implements AutoCloseable {
private final IIpSecService mService;
- private final InetAddress mRemoteAddress;
+ private final InetAddress mDestinationAddress;
private final CloseGuard mCloseGuard = CloseGuard.get();
private int mSpi = INVALID_SECURITY_PARAMETER_INDEX;
- private int mResourceId;
+ private int mResourceId = INVALID_RESOURCE_ID;
/** Get the underlying SPI held by this object. */
public int getSpi() {
@@ -146,6 +167,7 @@ public final class IpSecManager {
public void close() {
try {
mService.releaseSecurityParameterIndex(mResourceId);
+ mResourceId = INVALID_RESOURCE_ID;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -163,14 +185,14 @@ public final class IpSecManager {
}
private SecurityParameterIndex(
- @NonNull IIpSecService service, int direction, InetAddress remoteAddress, int spi)
+ @NonNull IIpSecService service, InetAddress destinationAddress, int spi)
throws ResourceUnavailableException, SpiUnavailableException {
mService = service;
- mRemoteAddress = remoteAddress;
+ mDestinationAddress = destinationAddress;
try {
IpSecSpiResponse result =
mService.allocateSecurityParameterIndex(
- direction, remoteAddress.getHostAddress(), spi, new Binder());
+ destinationAddress.getHostAddress(), spi, new Binder());
if (result == null) {
throw new NullPointerException("Received null response from IpSecService");
@@ -215,25 +237,23 @@ public final class IpSecManager {
}
/**
- * Reserve a random SPI for traffic bound to or from the specified remote address.
+ * Reserve a random SPI for traffic bound to or from the specified destination address.
*
* <p>If successful, this SPI is guaranteed available until released by a call to {@link
* SecurityParameterIndex#close()}.
*
- * @param direction {@link IpSecTransform#DIRECTION_IN} or {@link IpSecTransform#DIRECTION_OUT}
- * @param remoteAddress address of the remote. SPIs must be unique for each remoteAddress
+ * @param destinationAddress the destination address for traffic bearing the requested SPI.
+ * For inbound traffic, the destination should be an address currently assigned on-device.
* @return the reserved SecurityParameterIndex
- * @throws ResourceUnavailableException indicating that too many SPIs are currently allocated
- * for this user
- * @throws SpiUnavailableException indicating that a particular SPI cannot be reserved
+ * @throws {@link #ResourceUnavailableException} indicating that too many SPIs are
+ * currently allocated for this user
*/
- public SecurityParameterIndex allocateSecurityParameterIndex(
- int direction, InetAddress remoteAddress) throws ResourceUnavailableException {
+ public SecurityParameterIndex allocateSecurityParameterIndex(InetAddress destinationAddress)
+ throws ResourceUnavailableException {
try {
return new SecurityParameterIndex(
mService,
- direction,
- remoteAddress,
+ destinationAddress,
IpSecManager.INVALID_SECURITY_PARAMETER_INDEX);
} catch (SpiUnavailableException unlikely) {
throw new ResourceUnavailableException("No SPIs available");
@@ -241,26 +261,27 @@ public final class IpSecManager {
}
/**
- * Reserve the requested SPI for traffic bound to or from the specified remote address.
+ * Reserve the requested SPI for traffic bound to or from the specified destination address.
*
* <p>If successful, this SPI is guaranteed available until released by a call to {@link
* SecurityParameterIndex#close()}.
*
- * @param direction {@link IpSecTransform#DIRECTION_IN} or {@link IpSecTransform#DIRECTION_OUT}
- * @param remoteAddress address of the remote. SPIs must be unique for each remoteAddress
+ * @param destinationAddress the destination address for traffic bearing the requested SPI.
+ * For inbound traffic, the destination should be an address currently assigned on-device.
* @param requestedSpi the requested SPI, or '0' to allocate a random SPI
* @return the reserved SecurityParameterIndex
- * @throws ResourceUnavailableException indicating that too many SPIs are currently allocated
- * for this user
- * @throws SpiUnavailableException indicating that the requested SPI could not be reserved
+ * @throws {@link #ResourceUnavailableException} indicating that too many SPIs are
+ * currently allocated for this user
+ * @throws {@link #SpiUnavailableException} indicating that the requested SPI could not be
+ * reserved
*/
public SecurityParameterIndex allocateSecurityParameterIndex(
- int direction, InetAddress remoteAddress, int requestedSpi)
+ InetAddress destinationAddress, int requestedSpi)
throws SpiUnavailableException, ResourceUnavailableException {
if (requestedSpi == IpSecManager.INVALID_SECURITY_PARAMETER_INDEX) {
throw new IllegalArgumentException("Requested SPI must be a valid (non-zero) SPI");
}
- return new SecurityParameterIndex(mService, direction, remoteAddress, requestedSpi);
+ return new SecurityParameterIndex(mService, destinationAddress, requestedSpi);
}
/**
@@ -268,14 +289,14 @@ public final class IpSecManager {
*
* <p>This applies transport mode encapsulation to the given socket. Once applied, I/O on the
* socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When
- * the transform is removed from the socket by calling {@link #removeTransportModeTransform},
+ * the transform is removed from the socket by calling {@link #removeTransportModeTransforms},
* 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
* 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}.
+ * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}.
*
* <h4>Rekey Procedure</h4>
*
@@ -286,15 +307,14 @@ public final class IpSecManager {
* in-flight packets have been received.
*
* @param socket a stream socket
+ * @param direction the policy direction either {@link #DIRECTION_IN} or {@link #DIRECTION_OUT}
* @param transform a transport mode {@code IpSecTransform}
* @throws IOException indicating that the transform could not be applied
- * @hide
*/
- public void applyTransportModeTransform(Socket socket, IpSecTransform transform)
+ public void applyTransportModeTransform(
+ Socket socket, int direction, IpSecTransform transform)
throws IOException {
- try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket)) {
- applyTransportModeTransform(pfd, transform);
- }
+ applyTransportModeTransform(socket.getFileDescriptor$(), direction, transform);
}
/**
@@ -302,14 +322,14 @@ public final class IpSecManager {
*
* <p>This applies transport mode encapsulation to the given socket. Once applied, I/O on the
* socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When
- * the transform is removed from the socket by calling {@link #removeTransportModeTransform},
+ * the transform is removed from the socket by calling {@link #removeTransportModeTransforms},
* 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
* 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}.
+ * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}.
*
* <h4>Rekey Procedure</h4>
*
@@ -320,15 +340,13 @@ public final class IpSecManager {
* in-flight packets have been received.
*
* @param socket a datagram socket
+ * @param direction the policy direction either DIRECTION_IN or DIRECTION_OUT
* @param transform a transport mode {@code IpSecTransform}
* @throws IOException indicating that the transform could not be applied
- * @hide
*/
- public void applyTransportModeTransform(DatagramSocket socket, IpSecTransform transform)
- throws IOException {
- try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromDatagramSocket(socket)) {
- applyTransportModeTransform(pfd, transform);
- }
+ public void applyTransportModeTransform(
+ DatagramSocket socket, int direction, IpSecTransform transform) throws IOException {
+ applyTransportModeTransform(socket.getFileDescriptor$(), direction, transform);
}
/**
@@ -336,14 +354,14 @@ public final class IpSecManager {
*
* <p>This applies transport mode encapsulation to the given socket. Once applied, I/O on the
* socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When
- * the transform is removed from the socket by calling {@link #removeTransportModeTransform},
+ * the transform is removed from the socket by calling {@link #removeTransportModeTransforms},
* 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
* 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}.
+ * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}.
*
* <h4>Rekey Procedure</h4>
*
@@ -354,24 +372,17 @@ public final class IpSecManager {
* in-flight packets have been received.
*
* @param socket a socket file descriptor
+ * @param direction the policy direction either DIRECTION_IN or DIRECTION_OUT
* @param transform a transport mode {@code IpSecTransform}
* @throws IOException indicating that the transform could not be applied
*/
- public void applyTransportModeTransform(FileDescriptor socket, IpSecTransform transform)
+ public void applyTransportModeTransform(
+ FileDescriptor socket, int direction, IpSecTransform transform)
throws IOException {
// We dup() the FileDescriptor here because if we don't, then the ParcelFileDescriptor()
- // constructor takes control and closes the user's FD when we exit the method
- // This is behaviorally the same as the other versions, but the PFD constructor does not
- // dup() automatically, whereas PFD.fromSocket() and PDF.fromDatagramSocket() do dup().
+ // constructor takes control and closes the user's FD when we exit the method.
try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(socket)) {
- applyTransportModeTransform(pfd, transform);
- }
- }
-
- /* Call down to activate a transform */
- private void applyTransportModeTransform(ParcelFileDescriptor pfd, IpSecTransform transform) {
- try {
- mService.applyTransportModeTransform(pfd, transform.getResourceId());
+ mService.applyTransportModeTransform(pfd, direction, transform.getResourceId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -395,75 +406,56 @@ public final class IpSecManager {
/**
* Remove an IPsec transform from a stream socket.
*
- * <p>Once removed, traffic on the socket will not be encrypted. This operation will succeed
- * regardless of the state of the transform. Removing a transform from a socket allows the
- * socket to be reused for communication in the clear.
+ * <p>Once removed, traffic on the socket will not be encrypted. Removing transforms from a
+ * socket allows the socket to be reused for communication in the clear.
*
* <p>If an {@code IpSecTransform} object applied to this socket was deallocated by calling
* {@link IpSecTransform#close()}, then communication on the socket will fail until this method
* is called.
*
* @param socket a socket that previously had a transform applied to it
- * @param transform the IPsec Transform that was previously applied to the given socket
* @throws IOException indicating that the transform could not be removed from the socket
- * @hide
*/
- public void removeTransportModeTransform(Socket socket, IpSecTransform transform)
+ public void removeTransportModeTransforms(Socket socket)
throws IOException {
- try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromSocket(socket)) {
- removeTransportModeTransform(pfd, transform);
- }
+ removeTransportModeTransforms(socket.getFileDescriptor$());
}
/**
* Remove an IPsec transform from a datagram socket.
*
- * <p>Once removed, traffic on the socket will not be encrypted. This operation will succeed
- * regardless of the state of the transform. Removing a transform from a socket allows the
- * socket to be reused for communication in the clear.
+ * <p>Once removed, traffic on the socket will not be encrypted. Removing transforms from a
+ * socket allows the socket to be reused for communication in the clear.
*
* <p>If an {@code IpSecTransform} object applied to this socket was deallocated by calling
* {@link IpSecTransform#close()}, then communication on the socket will fail until this method
* is called.
*
* @param socket a socket that previously had a transform applied to it
- * @param transform the IPsec Transform that was previously applied to the given socket
* @throws IOException indicating that the transform could not be removed from the socket
- * @hide
*/
- public void removeTransportModeTransform(DatagramSocket socket, IpSecTransform transform)
+ public void removeTransportModeTransforms(DatagramSocket socket)
throws IOException {
- try (ParcelFileDescriptor pfd = ParcelFileDescriptor.fromDatagramSocket(socket)) {
- removeTransportModeTransform(pfd, transform);
- }
+ removeTransportModeTransforms(socket.getFileDescriptor$());
}
/**
* Remove an IPsec transform from a socket.
*
- * <p>Once removed, traffic on the socket will not be encrypted. This operation will succeed
- * regardless of the state of the transform. Removing a transform from a socket allows the
- * socket to be reused for communication in the clear.
+ * <p>Once removed, traffic on the socket will not be encrypted. Removing transforms from a
+ * socket allows the socket to be reused for communication in the clear.
*
* <p>If an {@code IpSecTransform} object applied to this socket was deallocated by calling
* {@link IpSecTransform#close()}, then communication on the socket will fail until this method
* is called.
*
* @param socket a socket that previously had a transform applied to it
- * @param transform the IPsec Transform that was previously applied to the given socket
* @throws IOException indicating that the transform could not be removed from the socket
*/
- public void removeTransportModeTransform(FileDescriptor socket, IpSecTransform transform)
+ public void removeTransportModeTransforms(FileDescriptor socket)
throws IOException {
try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(socket)) {
- removeTransportModeTransform(pfd, transform);
- }
- }
-
- /* Call down to remove a transform */
- private void removeTransportModeTransform(ParcelFileDescriptor pfd, IpSecTransform transform) {
- try {
- mService.removeTransportModeTransform(pfd, transform.getResourceId());
+ mService.removeTransportModeTransforms(pfd);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -501,7 +493,7 @@ public final class IpSecManager {
public static final class UdpEncapsulationSocket implements AutoCloseable {
private final ParcelFileDescriptor mPfd;
private final IIpSecService mService;
- private final int mResourceId;
+ private int mResourceId = INVALID_RESOURCE_ID;
private final int mPort;
private final CloseGuard mCloseGuard = CloseGuard.get();
@@ -554,6 +546,7 @@ public final class IpSecManager {
public void close() throws IOException {
try {
mService.closeUdpEncapsulationSocket(mResourceId);
+ mResourceId = INVALID_RESOURCE_ID;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -633,6 +626,170 @@ public final class IpSecManager {
}
/**
+ * This class represents an IpSecTunnelInterface
+ *
+ * <p>IpSecTunnelInterface objects track tunnel interfaces that serve as
+ * local endpoints for IPsec tunnels.
+ *
+ * <p>Creating an IpSecTunnelInterface creates a device to which IpSecTransforms may be
+ * applied to provide IPsec security to packets sent through the tunnel. While a tunnel
+ * cannot be used in standalone mode within Android, the higher layers may use the tunnel
+ * to create Network objects which are accessible to the Android system.
+ * @hide
+ */
+ @SystemApi
+ public static final class IpSecTunnelInterface implements AutoCloseable {
+ private final IIpSecService mService;
+ private final InetAddress mRemoteAddress;
+ private final InetAddress mLocalAddress;
+ private final Network mUnderlyingNetwork;
+ private final CloseGuard mCloseGuard = CloseGuard.get();
+ private String mInterfaceName;
+ private int mResourceId = INVALID_RESOURCE_ID;
+
+ /** Get the underlying SPI held by this object. */
+ public String getInterfaceName() {
+ return mInterfaceName;
+ }
+
+ /**
+ * Add an address to the IpSecTunnelInterface
+ *
+ * <p>Add an address which may be used as the local inner address for
+ * tunneled traffic.
+ *
+ * @param address the local address for traffic inside the tunnel
+ * @throws IOException if the address could not be added
+ * @hide
+ */
+ public void addAddress(LinkAddress address) throws IOException {
+ }
+
+ /**
+ * Remove an address from the IpSecTunnelInterface
+ *
+ * <p>Remove an address which was previously added to the IpSecTunnelInterface
+ *
+ * @param address to be removed
+ * @throws IOException if the address could not be removed
+ * @hide
+ */
+ public void removeAddress(LinkAddress address) throws IOException {
+ }
+
+ private IpSecTunnelInterface(@NonNull IIpSecService service,
+ @NonNull InetAddress localAddress, @NonNull InetAddress remoteAddress,
+ @NonNull Network underlyingNetwork)
+ throws ResourceUnavailableException, IOException {
+ mService = service;
+ mLocalAddress = localAddress;
+ mRemoteAddress = remoteAddress;
+ mUnderlyingNetwork = underlyingNetwork;
+
+ try {
+ IpSecTunnelInterfaceResponse result =
+ mService.createTunnelInterface(
+ localAddress.getHostAddress(),
+ remoteAddress.getHostAddress(),
+ underlyingNetwork,
+ new Binder());
+ switch (result.status) {
+ case Status.OK:
+ break;
+ case Status.RESOURCE_UNAVAILABLE:
+ throw new ResourceUnavailableException(
+ "No more tunnel interfaces may be allocated by this requester.");
+ default:
+ throw new RuntimeException(
+ "Unknown status returned by IpSecService: " + result.status);
+ }
+ mResourceId = result.resourceId;
+ mInterfaceName = result.interfaceName;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mCloseGuard.open("constructor");
+ }
+
+ /**
+ * Delete an IpSecTunnelInterface
+ *
+ * <p>Calling close will deallocate the IpSecTunnelInterface and all of its system
+ * resources. Any packets bound for this interface either inbound or outbound will
+ * all be lost.
+ */
+ @Override
+ public void close() {
+ try {
+ mService.deleteTunnelInterface(mResourceId);
+ mResourceId = INVALID_RESOURCE_ID;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ mCloseGuard.close();
+ }
+
+ /** Check that the Interface was closed properly. */
+ @Override
+ protected void finalize() throws Throwable {
+ if (mCloseGuard != null) {
+ mCloseGuard.warnIfOpen();
+ }
+ close();
+ }
+
+ /** @hide */
+ @VisibleForTesting
+ public int getResourceId() {
+ return mResourceId;
+ }
+ }
+
+ /**
+ * Create a new IpSecTunnelInterface as a local endpoint for tunneled IPsec traffic.
+ *
+ * <p>An application that creates tunnels is responsible for cleaning up the tunnel when the
+ * underlying network goes away, and the onLost() callback is received.
+ *
+ * @param localAddress The local addres of the tunnel
+ * @param remoteAddress The local addres of the tunnel
+ * @param underlyingNetwork the {@link Network} that will carry traffic for this tunnel.
+ * This network should almost certainly be a network such as WiFi with an L2 address.
+ * @return a new {@link IpSecManager#IpSecTunnelInterface} with the specified properties
+ * @throws IOException indicating that the socket could not be opened or bound
+ * @throws ResourceUnavailableException indicating that too many encapsulation sockets are open
+ * @hide
+ */
+ @SystemApi
+ public IpSecTunnelInterface createIpSecTunnelInterface(@NonNull InetAddress localAddress,
+ @NonNull InetAddress remoteAddress, @NonNull Network underlyingNetwork)
+ throws ResourceUnavailableException, IOException {
+ return new IpSecTunnelInterface(mService, localAddress, remoteAddress, underlyingNetwork);
+ }
+
+ /**
+ * Apply a transform to the IpSecTunnelInterface
+ *
+ * @param tunnel The {@link IpSecManager#IpSecTunnelInterface} that will use the supplied
+ * transform.
+ * @param direction the direction, {@link DIRECTION_OUT} or {@link #DIRECTION_IN} in which
+ * the transform will be used.
+ * @param transform an {@link IpSecTransform} created in tunnel mode
+ * @throws IOException indicating that the transform could not be applied due to a lower
+ * layer failure.
+ * @hide
+ */
+ @SystemApi
+ public void applyTunnelModeTransform(IpSecTunnelInterface tunnel, int direction,
+ IpSecTransform transform) throws IOException {
+ try {
+ mService.applyTunnelModeTransform(
+ tunnel.getResourceId(), direction, transform.getResourceId());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+ /**
* Construct an instance of IpSecManager within an application context.
*
* @param context the application context for this manager
diff --git a/android/net/IpSecTransform.java b/android/net/IpSecTransform.java
index 7cd742b4..37e2c4fb 100644
--- a/android/net/IpSecTransform.java
+++ b/android/net/IpSecTransform.java
@@ -38,13 +38,11 @@ import java.lang.annotation.RetentionPolicy;
import java.net.InetAddress;
/**
- * This class represents an IPsec transform, which comprises security associations in one or both
- * directions.
+ * This class represents a transform, which roughly corresponds to an IPsec Security Association.
*
* <p>Transforms are created using {@link IpSecTransform.Builder}. Each {@code IpSecTransform}
- * object encapsulates the properties and state of an inbound and outbound IPsec security
- * association. That includes, but is not limited to, algorithm choice, key material, and allocated
- * system resources.
+ * object encapsulates the properties and state of an IPsec security association. That includes,
+ * but is not limited to, algorithm choice, key material, and allocated system resources.
*
* @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the
* Internet Protocol</a>
@@ -52,23 +50,6 @@ import java.net.InetAddress;
public final class IpSecTransform implements AutoCloseable {
private static final String TAG = "IpSecTransform";
- /**
- * For direction-specific attributes of an {@link IpSecTransform}, indicates that an attribute
- * applies to traffic towards the host.
- */
- public static final int DIRECTION_IN = 0;
-
- /**
- * For direction-specific attributes of an {@link IpSecTransform}, indicates that an attribute
- * applies to traffic from the host.
- */
- public static final int DIRECTION_OUT = 1;
-
- /** @hide */
- @IntDef(value = {DIRECTION_IN, DIRECTION_OUT})
- @Retention(RetentionPolicy.SOURCE)
- public @interface TransformDirection {}
-
/** @hide */
public static final int MODE_TRANSPORT = 0;
@@ -143,8 +124,7 @@ public final class IpSecTransform implements AutoCloseable {
synchronized (this) {
try {
IIpSecService svc = getIpSecService();
- IpSecTransformResponse result =
- svc.createTransportModeTransform(mConfig, new Binder());
+ IpSecTransformResponse result = svc.createTransform(mConfig, new Binder());
int status = result.status;
checkResultStatus(status);
mResourceId = result.resourceId;
@@ -170,7 +150,7 @@ public final class IpSecTransform implements AutoCloseable {
*
* <p>Deactivating a transform while it is still applied to a socket will result in errors on
* that socket. Make sure to remove transforms by calling {@link
- * IpSecManager#removeTransportModeTransform}. Note, removing an {@code IpSecTransform} from a
+ * IpSecManager#removeTransportModeTransforms}. Note, removing an {@code IpSecTransform} from a
* socket will not deactivate it (because one transform may be applied to multiple sockets).
*
* <p>It is safe to call this method on a transform that has already been deactivated.
@@ -189,7 +169,7 @@ public final class IpSecTransform implements AutoCloseable {
* still want to clear out the transform.
*/
IIpSecService svc = getIpSecService();
- svc.deleteTransportModeTransform(mResourceId);
+ svc.deleteTransform(mResourceId);
stopKeepalive();
} catch (RemoteException e) {
throw e.rethrowAsRuntimeException();
@@ -272,96 +252,49 @@ public final class IpSecTransform implements AutoCloseable {
private IpSecConfig mConfig;
/**
- * Set the encryption algorithm for the given direction.
- *
- * <p>If encryption is set for a direction without also providing an SPI for that direction,
- * creation of an {@code IpSecTransform} will fail when attempting to build the transform.
+ * Set the encryption algorithm.
*
* <p>Encryption is mutually exclusive with authenticated encryption.
*
- * @param direction either {@link #DIRECTION_IN} or {@link #DIRECTION_OUT}
* @param algo {@link IpSecAlgorithm} specifying the encryption to be applied.
*/
- public IpSecTransform.Builder setEncryption(
- @TransformDirection int direction, IpSecAlgorithm algo) {
+ public IpSecTransform.Builder setEncryption(@NonNull IpSecAlgorithm algo) {
// TODO: throw IllegalArgumentException if algo is not an encryption algorithm.
- mConfig.setEncryption(direction, algo);
+ Preconditions.checkNotNull(algo);
+ mConfig.setEncryption(algo);
return this;
}
/**
- * Set the authentication (integrity) algorithm for the given direction.
- *
- * <p>If authentication is set for a direction without also providing an SPI for that
- * direction, creation of an {@code IpSecTransform} will fail when attempting to build the
- * transform.
+ * Set the authentication (integrity) algorithm.
*
* <p>Authentication is mutually exclusive with authenticated encryption.
*
- * @param direction either {@link #DIRECTION_IN} or {@link #DIRECTION_OUT}
* @param algo {@link IpSecAlgorithm} specifying the authentication to be applied.
*/
- public IpSecTransform.Builder setAuthentication(
- @TransformDirection int direction, IpSecAlgorithm algo) {
+ public IpSecTransform.Builder setAuthentication(@NonNull IpSecAlgorithm algo) {
// TODO: throw IllegalArgumentException if algo is not an authentication algorithm.
- mConfig.setAuthentication(direction, algo);
+ Preconditions.checkNotNull(algo);
+ mConfig.setAuthentication(algo);
return this;
}
/**
- * Set the authenticated encryption algorithm for the given direction.
+ * Set the authenticated encryption algorithm.
*
- * <p>If an authenticated encryption algorithm is set for a given direction without also
- * providing an SPI for that direction, creation of an {@code IpSecTransform} will fail when
- * attempting to build the transform.
- *
- * <p>The Authenticated Encryption (AE) class of algorithms are also known as Authenticated
- * Encryption with Associated Data (AEAD) algorithms, or Combined mode algorithms (as
- * referred to in <a href="https://tools.ietf.org/html/rfc4301">RFC 4301</a>).
+ * <p>The Authenticated Encryption (AE) class of algorithms are also known as
+ * Authenticated Encryption with Associated Data (AEAD) algorithms, or Combined mode
+ * algorithms (as referred to in
+ * <a href="https://tools.ietf.org/html/rfc4301">RFC 4301</a>).
*
* <p>Authenticated encryption is mutually exclusive with encryption and authentication.
*
- * @param direction either {@link #DIRECTION_IN} or {@link #DIRECTION_OUT}
* @param algo {@link IpSecAlgorithm} specifying the authenticated encryption algorithm to
* be applied.
*/
- public IpSecTransform.Builder setAuthenticatedEncryption(
- @TransformDirection int direction, IpSecAlgorithm algo) {
- mConfig.setAuthenticatedEncryption(direction, algo);
- return this;
- }
-
- /**
- * Set the SPI for the given direction.
- *
- * <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#allocateSecurityParameterIndex}.
- *
- * <p>If the SPI and algorithms are omitted for one direction, traffic in that direction
- * will not be encrypted or authenticated.
- *
- * @param direction either {@link #DIRECTION_IN} or {@link #DIRECTION_OUT}
- * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
- * traffic
- */
- public IpSecTransform.Builder setSpi(
- @TransformDirection int direction, IpSecManager.SecurityParameterIndex spi) {
- mConfig.setSpiResourceId(direction, spi.getResourceId());
- return this;
- }
-
- /**
- * Set the {@link Network} which will carry tunneled traffic.
- *
- * <p>Restricts the transformed traffic to a particular {@link Network}. This is required
- * for tunnel mode, otherwise tunneled traffic would be sent on the default network.
- *
- * @hide
- */
- @SystemApi
- public IpSecTransform.Builder setUnderlyingNetwork(Network net) {
- mConfig.setNetwork(net);
+ public IpSecTransform.Builder setAuthenticatedEncryption(@NonNull IpSecAlgorithm algo) {
+ Preconditions.checkNotNull(algo);
+ mConfig.setAuthenticatedEncryption(algo);
return this;
}
@@ -379,8 +312,12 @@ public final class IpSecTransform implements AutoCloseable {
* encapsulated traffic. In the case of IKEv2, this should be port 4500.
*/
public IpSecTransform.Builder setIpv4Encapsulation(
- IpSecManager.UdpEncapsulationSocket localSocket, int remotePort) {
+ @NonNull IpSecManager.UdpEncapsulationSocket localSocket, int remotePort) {
+ Preconditions.checkNotNull(localSocket);
mConfig.setEncapType(ENCAP_ESPINUDP);
+ if (localSocket.getResourceId() == INVALID_RESOURCE_ID) {
+ throw new IllegalArgumentException("Invalid UdpEncapsulationSocket");
+ }
mConfig.setEncapSocketResourceId(localSocket.getResourceId());
mConfig.setEncapRemotePort(remotePort);
return this;
@@ -413,21 +350,33 @@ 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
+ * @param sourceAddress the source {@code InetAddress} of traffic on sockets that will use
+ * this transform; this address must belong to the Network used by all sockets that
+ * utilize this transform; if provided, then only traffic originating from the
+ * specified source address will be processed.
+ * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
+ * traffic
* @throws IllegalArgumentException indicating that a particular combination of transform
* properties is invalid
- * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms are
- * active
+ * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms
+ * are active
* @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI
* collides with an existing transform
* @throws IOException indicating other errors
*/
- public IpSecTransform buildTransportModeTransform(InetAddress remoteAddress)
+ public IpSecTransform buildTransportModeTransform(
+ @NonNull InetAddress sourceAddress,
+ @NonNull IpSecManager.SecurityParameterIndex spi)
throws IpSecManager.ResourceUnavailableException,
IpSecManager.SpiUnavailableException, IOException {
+ Preconditions.checkNotNull(sourceAddress);
+ Preconditions.checkNotNull(spi);
+ if (spi.getResourceId() == INVALID_RESOURCE_ID) {
+ throw new IllegalArgumentException("Invalid SecurityParameterIndex");
+ }
mConfig.setMode(MODE_TRANSPORT);
- mConfig.setRemoteAddress(remoteAddress.getHostAddress());
+ mConfig.setSourceAddress(sourceAddress.getHostAddress());
+ mConfig.setSpiResourceId(spi.getResourceId());
// FIXME: modifying a builder after calling build can change the built transform.
return new IpSecTransform(mContext, mConfig).activate();
}
@@ -436,22 +385,34 @@ public final class IpSecTransform implements AutoCloseable {
* Build and return an {@link IpSecTransform} object as a Tunnel Mode Transform. Some
* parameters have interdependencies that are checked at build time.
*
- * @param localAddress the {@link InetAddress} that provides the local endpoint for this
+ * @param sourceAddress the {@link InetAddress} that provides the source address for this
* IPsec tunnel. This is almost certainly an address belonging to the {@link Network}
* that will originate the traffic, which is set as the {@link #setUnderlyingNetwork}.
- * @param remoteAddress the {@link InetAddress} representing the remote endpoint of this
- * IPsec tunnel.
+ * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
+ * traffic
* @throws IllegalArgumentException indicating that a particular combination of transform
* properties is invalid.
+ * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms
+ * are active
+ * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI
+ * collides with an existing transform
+ * @throws IOException indicating other errors
* @hide
*/
+ @SystemApi
public IpSecTransform buildTunnelModeTransform(
- InetAddress localAddress, InetAddress remoteAddress) {
- // FIXME: argument validation here
- // throw new IllegalArgumentException("Natt Keepalive requires UDP Encapsulation");
- mConfig.setLocalAddress(localAddress.getHostAddress());
- mConfig.setRemoteAddress(remoteAddress.getHostAddress());
+ @NonNull InetAddress sourceAddress,
+ @NonNull IpSecManager.SecurityParameterIndex spi)
+ throws IpSecManager.ResourceUnavailableException,
+ IpSecManager.SpiUnavailableException, IOException {
+ Preconditions.checkNotNull(sourceAddress);
+ Preconditions.checkNotNull(spi);
+ if (spi.getResourceId() == INVALID_RESOURCE_ID) {
+ throw new IllegalArgumentException("Invalid SecurityParameterIndex");
+ }
mConfig.setMode(MODE_TUNNEL);
+ mConfig.setSourceAddress(sourceAddress.getHostAddress());
+ mConfig.setSpiResourceId(spi.getResourceId());
return new IpSecTransform(mContext, mConfig);
}
diff --git a/android/net/IpSecTunnelInterfaceResponse.java b/android/net/IpSecTunnelInterfaceResponse.java
new file mode 100644
index 00000000..c23d831a
--- /dev/null
+++ b/android/net/IpSecTunnelInterfaceResponse.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class is used to return an IpSecTunnelInterface resource Id and and corresponding status
+ * from the IpSecService to an IpSecTunnelInterface object.
+ *
+ * @hide
+ */
+public final class IpSecTunnelInterfaceResponse implements Parcelable {
+ private static final String TAG = "IpSecTunnelInterfaceResponse";
+
+ public final int resourceId;
+ public final String interfaceName;
+ public final int status;
+ // Parcelable Methods
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(status);
+ out.writeInt(resourceId);
+ out.writeString(interfaceName);
+ }
+
+ public IpSecTunnelInterfaceResponse(int inStatus) {
+ if (inStatus == IpSecManager.Status.OK) {
+ throw new IllegalArgumentException("Valid status implies other args must be provided");
+ }
+ status = inStatus;
+ resourceId = IpSecManager.INVALID_RESOURCE_ID;
+ interfaceName = "";
+ }
+
+ public IpSecTunnelInterfaceResponse(int inStatus, int inResourceId, String inInterfaceName) {
+ status = inStatus;
+ resourceId = inResourceId;
+ interfaceName = inInterfaceName;
+ }
+
+ private IpSecTunnelInterfaceResponse(Parcel in) {
+ status = in.readInt();
+ resourceId = in.readInt();
+ interfaceName = in.readString();
+ }
+
+ public static final Parcelable.Creator<IpSecTunnelInterfaceResponse> CREATOR =
+ new Parcelable.Creator<IpSecTunnelInterfaceResponse>() {
+ public IpSecTunnelInterfaceResponse createFromParcel(Parcel in) {
+ return new IpSecTunnelInterfaceResponse(in);
+ }
+
+ public IpSecTunnelInterfaceResponse[] newArray(int size) {
+ return new IpSecTunnelInterfaceResponse[size];
+ }
+ };
+}
diff --git a/com/android/server/connectivity/KeepalivePacketData.java b/android/net/KeepalivePacketData.java
index 2ccfdd1f..08d4ff5d 100644
--- a/com/android/server/connectivity/KeepalivePacketData.java
+++ b/android/net/KeepalivePacketData.java
@@ -14,12 +14,15 @@
* limitations under the License.
*/
-package com.android.server.connectivity;
+package android.net;
import android.system.OsConstants;
import android.net.ConnectivityManager;
-import android.net.NetworkUtils;
import android.net.util.IpUtils;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.system.OsConstants;
+import android.util.Log;
import java.net.Inet4Address;
import java.net.Inet6Address;
@@ -35,9 +38,8 @@ import static android.net.ConnectivityManager.PacketKeepalive.*;
*
* @hide
*/
-public class KeepalivePacketData {
- /** Protocol of the packet to send; one of the OsConstants.ETH_P_* values. */
- public final int protocol;
+public class KeepalivePacketData implements Parcelable {
+ private static final String TAG = "KeepalivePacketData";
/** Source IP address */
public final InetAddress srcAddress;
@@ -51,57 +53,57 @@ public class KeepalivePacketData {
/** Destination port */
public final int dstPort;
- /** Destination MAC address. Can change if routing changes. */
- public byte[] dstMac;
-
/** Packet data. A raw byte string of packet data, not including the link-layer header. */
- public final byte[] data;
+ private final byte[] mPacket;
private static final int IPV4_HEADER_LENGTH = 20;
private static final int UDP_HEADER_LENGTH = 8;
+ // This should only be constructed via static factory methods, such as
+ // nattKeepalivePacket
protected KeepalivePacketData(InetAddress srcAddress, int srcPort,
InetAddress dstAddress, int dstPort, byte[] data) throws InvalidPacketException {
this.srcAddress = srcAddress;
this.dstAddress = dstAddress;
this.srcPort = srcPort;
this.dstPort = dstPort;
- this.data = data;
+ this.mPacket = data;
// Check we have two IP addresses of the same family.
- if (srcAddress == null || dstAddress == null ||
- !srcAddress.getClass().getName().equals(dstAddress.getClass().getName())) {
- throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
- }
-
- // Set the protocol.
- if (this.dstAddress instanceof Inet4Address) {
- this.protocol = OsConstants.ETH_P_IP;
- } else if (this.dstAddress instanceof Inet6Address) {
- this.protocol = OsConstants.ETH_P_IPV6;
- } else {
+ if (srcAddress == null || dstAddress == null || !srcAddress.getClass().getName()
+ .equals(dstAddress.getClass().getName())) {
+ Log.e(TAG, "Invalid or mismatched InetAddresses in KeepalivePacketData");
throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
}
// Check the ports.
if (!IpUtils.isValidUdpOrTcpPort(srcPort) || !IpUtils.isValidUdpOrTcpPort(dstPort)) {
+ Log.e(TAG, "Invalid ports in KeepalivePacketData");
throw new InvalidPacketException(ERROR_INVALID_PORT);
}
}
public static class InvalidPacketException extends Exception {
- final public int error;
+ public final int error;
public InvalidPacketException(int error) {
this.error = error;
}
}
- /**
- * Creates an IPsec NAT-T keepalive packet with the specified parameters.
- */
+ public byte[] getPacket() {
+ return mPacket.clone();
+ }
+
public static KeepalivePacketData nattKeepalivePacket(
- InetAddress srcAddress, int srcPort,
- InetAddress dstAddress, int dstPort) throws InvalidPacketException {
+ InetAddress srcAddress, int srcPort, InetAddress dstAddress, int dstPort)
+ throws InvalidPacketException {
+
+ // FIXME: remove this and actually support IPv6 keepalives
+ if (srcAddress instanceof Inet6Address && dstAddress instanceof Inet6Address) {
+ // Optimistically returning an IPv6 Keepalive Packet with no data,
+ // which currently only works on cellular
+ return new KeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, new byte[0]);
+ }
if (!(srcAddress instanceof Inet4Address) || !(dstAddress instanceof Inet4Address)) {
throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
@@ -134,4 +136,39 @@ public class KeepalivePacketData {
return new KeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, buf.array());
}
+
+ /* Parcelable Implementation */
+ public int describeContents() {
+ return 0;
+ }
+
+ /** Write to parcel */
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(srcAddress.getHostAddress());
+ out.writeString(dstAddress.getHostAddress());
+ out.writeInt(srcPort);
+ out.writeInt(dstPort);
+ out.writeByteArray(mPacket);
+ }
+
+ private KeepalivePacketData(Parcel in) {
+ srcAddress = NetworkUtils.numericToInetAddress(in.readString());
+ dstAddress = NetworkUtils.numericToInetAddress(in.readString());
+ srcPort = in.readInt();
+ dstPort = in.readInt();
+ mPacket = in.createByteArray();
+ }
+
+ /** Parcelable Creator */
+ public static final Parcelable.Creator<KeepalivePacketData> CREATOR =
+ new Parcelable.Creator<KeepalivePacketData>() {
+ public KeepalivePacketData createFromParcel(Parcel in) {
+ return new KeepalivePacketData(in);
+ }
+
+ public KeepalivePacketData[] newArray(int size) {
+ return new KeepalivePacketData[size];
+ }
+ };
+
}
diff --git a/android/net/LinkProperties.java b/android/net/LinkProperties.java
index 4e474c8e..f525b1f3 100644
--- a/android/net/LinkProperties.java
+++ b/android/net/LinkProperties.java
@@ -50,6 +50,8 @@ public final class LinkProperties implements Parcelable {
private String mIfaceName;
private ArrayList<LinkAddress> mLinkAddresses = new ArrayList<LinkAddress>();
private ArrayList<InetAddress> mDnses = new ArrayList<InetAddress>();
+ private boolean mUsePrivateDns;
+ private String mPrivateDnsServerName;
private String mDomains;
private ArrayList<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
private ProxyInfo mHttpProxy;
@@ -165,6 +167,8 @@ public final class LinkProperties implements Parcelable {
mIfaceName = source.getInterfaceName();
for (LinkAddress l : source.getLinkAddresses()) mLinkAddresses.add(l);
for (InetAddress i : source.getDnsServers()) mDnses.add(i);
+ mUsePrivateDns = source.mUsePrivateDns;
+ mPrivateDnsServerName = source.mPrivateDnsServerName;
mDomains = source.getDomains();
for (RouteInfo r : source.getRoutes()) mRoutes.add(r);
mHttpProxy = (source.getHttpProxy() == null) ?
@@ -391,6 +395,59 @@ public final class LinkProperties implements Parcelable {
}
/**
+ * Set whether private DNS is currently in use on this network.
+ *
+ * @param usePrivateDns The private DNS state.
+ * @hide
+ */
+ public void setUsePrivateDns(boolean usePrivateDns) {
+ mUsePrivateDns = usePrivateDns;
+ }
+
+ /**
+ * Returns whether private DNS is currently in use on this network. When
+ * private DNS is in use, applications must not send unencrypted DNS
+ * queries as doing so could reveal private user information. Furthermore,
+ * if private DNS is in use and {@link #getPrivateDnsServerName} is not
+ * {@code null}, DNS queries must be sent to the specified DNS server.
+ *
+ * @return {@code true} if private DNS is in use, {@code false} otherwise.
+ */
+ public boolean isPrivateDnsActive() {
+ return mUsePrivateDns;
+ }
+
+ /**
+ * Set the name of the private DNS server to which private DNS queries
+ * should be sent when in strict mode. This value should be {@code null}
+ * when private DNS is off or in opportunistic mode.
+ *
+ * @param privateDnsServerName The private DNS server name.
+ * @hide
+ */
+ public void setPrivateDnsServerName(@Nullable String privateDnsServerName) {
+ mPrivateDnsServerName = privateDnsServerName;
+ }
+
+ /**
+ * Returns the private DNS server name that is in use. If not {@code null},
+ * private DNS is in strict mode. In this mode, applications should ensure
+ * that all DNS queries are encrypted and sent to this hostname and that
+ * queries are only sent if the hostname's certificate is valid. If
+ * {@code null} and {@link #isPrivateDnsActive} is {@code true}, private
+ * DNS is in opportunistic mode, and applications should ensure that DNS
+ * queries are encrypted and sent to a DNS server returned by
+ * {@link #getDnsServers}. System DNS will handle each of these cases
+ * correctly, but applications implementing their own DNS lookups must make
+ * sure to follow these requirements.
+ *
+ * @return The private DNS server name.
+ */
+ public @Nullable String getPrivateDnsServerName() {
+ return mPrivateDnsServerName;
+ }
+
+ /**
* Sets the DNS domain search path used on this link.
*
* @param domains A {@link String} listing in priority order the comma separated
@@ -622,6 +679,8 @@ public final class LinkProperties implements Parcelable {
mIfaceName = null;
mLinkAddresses.clear();
mDnses.clear();
+ mUsePrivateDns = false;
+ mPrivateDnsServerName = null;
mDomains = null;
mRoutes.clear();
mHttpProxy = null;
@@ -649,6 +708,13 @@ public final class LinkProperties implements Parcelable {
for (InetAddress addr : mDnses) dns += addr.getHostAddress() + ",";
dns += "] ";
+ String usePrivateDns = "UsePrivateDns: " + mUsePrivateDns + " ";
+
+ String privateDnsServerName = "";
+ if (privateDnsServerName != null) {
+ privateDnsServerName = "PrivateDnsServerName: " + mPrivateDnsServerName + " ";
+ }
+
String domainName = "Domains: " + mDomains;
String mtu = " MTU: " + mMtu;
@@ -671,8 +737,9 @@ public final class LinkProperties implements Parcelable {
}
stacked += "] ";
}
- return "{" + ifaceName + linkAddresses + routes + dns + domainName + mtu
- + tcpBuffSizes + proxy + stacked + "}";
+ return "{" + ifaceName + linkAddresses + routes + dns + usePrivateDns
+ + privateDnsServerName + domainName + mtu + tcpBuffSizes + proxy
+ + stacked + "}";
}
/**
@@ -896,6 +963,20 @@ public final class LinkProperties implements Parcelable {
}
/**
+ * Compares this {@code LinkProperties} private DNS settings against the
+ * target.
+ *
+ * @param target LinkProperties to compare.
+ * @return {@code true} if both are identical, {@code false} otherwise.
+ * @hide
+ */
+ public boolean isIdenticalPrivateDns(LinkProperties target) {
+ return (isPrivateDnsActive() == target.isPrivateDnsActive()
+ && TextUtils.equals(getPrivateDnsServerName(),
+ target.getPrivateDnsServerName()));
+ }
+
+ /**
* Compares this {@code LinkProperties} Routes against the target
*
* @param target LinkProperties to compare.
@@ -989,14 +1070,15 @@ public final class LinkProperties implements Parcelable {
* stacked interfaces are not so much a property of the link as a
* description of connections between links.
*/
- return isIdenticalInterfaceName(target) &&
- isIdenticalAddresses(target) &&
- isIdenticalDnses(target) &&
- isIdenticalRoutes(target) &&
- isIdenticalHttpProxy(target) &&
- isIdenticalStackedLinks(target) &&
- isIdenticalMtu(target) &&
- isIdenticalTcpBufferSizes(target);
+ return isIdenticalInterfaceName(target)
+ && isIdenticalAddresses(target)
+ && isIdenticalDnses(target)
+ && isIdenticalPrivateDns(target)
+ && isIdenticalRoutes(target)
+ && isIdenticalHttpProxy(target)
+ && isIdenticalStackedLinks(target)
+ && isIdenticalMtu(target)
+ && isIdenticalTcpBufferSizes(target);
}
/**
@@ -1091,7 +1173,9 @@ public final class LinkProperties implements Parcelable {
+ ((null == mHttpProxy) ? 0 : mHttpProxy.hashCode())
+ mStackedLinks.hashCode() * 47)
+ mMtu * 51
- + ((null == mTcpBufferSizes) ? 0 : mTcpBufferSizes.hashCode());
+ + ((null == mTcpBufferSizes) ? 0 : mTcpBufferSizes.hashCode())
+ + (mUsePrivateDns ? 57 : 0)
+ + ((null == mPrivateDnsServerName) ? 0 : mPrivateDnsServerName.hashCode());
}
/**
@@ -1108,6 +1192,8 @@ public final class LinkProperties implements Parcelable {
for(InetAddress d : mDnses) {
dest.writeByteArray(d.getAddress());
}
+ dest.writeBoolean(mUsePrivateDns);
+ dest.writeString(mPrivateDnsServerName);
dest.writeString(mDomains);
dest.writeInt(mMtu);
dest.writeString(mTcpBufferSizes);
@@ -1148,6 +1234,8 @@ public final class LinkProperties implements Parcelable {
netProp.addDnsServer(InetAddress.getByAddress(in.createByteArray()));
} catch (UnknownHostException e) { }
}
+ netProp.setUsePrivateDns(in.readBoolean());
+ netProp.setPrivateDnsServerName(in.readString());
netProp.setDomains(in.readString());
netProp.setMtu(in.readInt());
netProp.setTcpBufferSizes(in.readString());
diff --git a/android/net/MacAddress.java b/android/net/MacAddress.java
index d6992aae..287bdc88 100644
--- a/android/net/MacAddress.java
+++ b/android/net/MacAddress.java
@@ -17,6 +17,7 @@
package android.net;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
@@ -60,7 +61,7 @@ public final class MacAddress implements Parcelable {
})
public @interface MacAddressType { }
- /** Indicates a MAC address of unknown type. */
+ /** @hide 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;
@@ -92,7 +93,7 @@ public final class MacAddress implements Parcelable {
*
* @return the int constant representing the MAC address type of this MacAddress.
*/
- public @MacAddressType int addressType() {
+ public @MacAddressType int getAddressType() {
if (equals(BROADCAST_ADDRESS)) {
return TYPE_BROADCAST;
}
@@ -120,12 +121,12 @@ public final class MacAddress implements Parcelable {
/**
* @return a byte array representation of this MacAddress.
*/
- public byte[] toByteArray() {
+ public @NonNull byte[] toByteArray() {
return byteAddrFromLongAddr(mAddr);
}
@Override
- public String toString() {
+ public @NonNull String toString() {
return stringAddrFromLongAddr(mAddr);
}
@@ -133,7 +134,7 @@ public final class MacAddress implements Parcelable {
* @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() {
+ public @NonNull String toOuiString() {
return String.format(
"%02x:%02x:%02x", (mAddr >> 40) & 0xff, (mAddr >> 32) & 0xff, (mAddr >> 24) & 0xff);
}
@@ -197,7 +198,7 @@ public final class MacAddress implements Parcelable {
if (!isMacAddress(addr)) {
return TYPE_UNKNOWN;
}
- return MacAddress.fromBytes(addr).addressType();
+ return MacAddress.fromBytes(addr).getAddressType();
}
/**
@@ -211,7 +212,7 @@ public final class MacAddress implements Parcelable {
*
* @hide
*/
- public static byte[] byteAddrFromStringAddr(String addr) {
+ public static @NonNull byte[] byteAddrFromStringAddr(String addr) {
Preconditions.checkNotNull(addr);
String[] parts = addr.split(":");
if (parts.length != ETHER_ADDR_LEN) {
@@ -239,7 +240,7 @@ public final class MacAddress implements Parcelable {
*
* @hide
*/
- public static String stringAddrFromByteAddr(byte[] addr) {
+ public static @NonNull String stringAddrFromByteAddr(byte[] addr) {
if (!isMacAddress(addr)) {
return null;
}
@@ -291,7 +292,7 @@ public final class MacAddress implements Parcelable {
// Internal conversion function equivalent to stringAddrFromByteAddr(byteAddrFromLongAddr(addr))
// that avoids the allocation of an intermediary byte[].
- private static String stringAddrFromLongAddr(long addr) {
+ private static @NonNull String stringAddrFromLongAddr(long addr) {
return String.format("%02x:%02x:%02x:%02x:%02x:%02x",
(addr >> 40) & 0xff,
(addr >> 32) & 0xff,
@@ -310,7 +311,7 @@ public final class MacAddress implements Parcelable {
* @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) {
+ public static @NonNull MacAddress fromString(@NonNull String addr) {
return new MacAddress(longAddrFromStringAddr(addr));
}
@@ -322,7 +323,7 @@ public final class MacAddress implements Parcelable {
* @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) {
+ public static @NonNull MacAddress fromBytes(@NonNull byte[] addr) {
return new MacAddress(longAddrFromByteAddr(addr));
}
@@ -336,7 +337,7 @@ public final class MacAddress implements Parcelable {
*
* @hide
*/
- public static MacAddress createRandomUnicastAddress() {
+ public static @NonNull MacAddress createRandomUnicastAddress() {
return createRandomUnicastAddress(BASE_GOOGLE_MAC, new Random());
}
@@ -352,7 +353,7 @@ public final class MacAddress implements Parcelable {
*
* @hide
*/
- public static MacAddress createRandomUnicastAddress(MacAddress base, Random r) {
+ public static @NonNull 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;
diff --git a/android/net/Network.java b/android/net/Network.java
index 903b602b..5df168d2 100644
--- a/android/net/Network.java
+++ b/android/net/Network.java
@@ -21,6 +21,7 @@ import android.os.Parcelable;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
+import android.util.proto.ProtoOutputStream;
import com.android.okhttp.internalandroidapi.Dns;
import com.android.okhttp.internalandroidapi.HttpURLConnectionFactory;
@@ -356,13 +357,13 @@ public class Network implements Parcelable {
// Multiple Provisioning Domains API recommendations, as made by the
// IETF mif working group.
//
- // The HANDLE_MAGIC value MUST be kept in sync with the corresponding
+ // The handleMagic value MUST be kept in sync with the corresponding
// value in the native/android/net.c NDK implementation.
if (netId == 0) {
return 0L; // make this zero condition obvious for debugging
}
- final long HANDLE_MAGIC = 0xfacade;
- return (((long) netId) << 32) | HANDLE_MAGIC;
+ final long handleMagic = 0xcafed00dL;
+ return (((long) netId) << 32) | handleMagic;
}
// implement the Parcelable interface
@@ -402,4 +403,11 @@ public class Network implements Parcelable {
public String toString() {
return Integer.toString(netId);
}
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(NetworkProto.NET_ID, netId);
+ proto.end(token);
+ }
}
diff --git a/android/net/NetworkAgent.java b/android/net/NetworkAgent.java
index 2dacf8f4..52a23548 100644
--- a/android/net/NetworkAgent.java
+++ b/android/net/NetworkAgent.java
@@ -17,6 +17,7 @@
package android.net;
import android.content.Context;
+import android.net.ConnectivityManager.PacketKeepalive;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
@@ -26,7 +27,6 @@ import android.util.Log;
import com.android.internal.util.AsyncChannel;
import com.android.internal.util.Protocol;
-import android.net.ConnectivityManager.PacketKeepalive;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
@@ -101,20 +101,6 @@ public abstract class NetworkAgent extends Handler {
public static final int EVENT_NETWORK_SCORE_CHANGED = BASE + 4;
/**
- * Sent by the NetworkAgent to ConnectivityService to add new UID ranges
- * to be forced into this Network. For VPNs only.
- * obj = UidRange[] to forward
- */
- public static final int EVENT_UID_RANGES_ADDED = BASE + 5;
-
- /**
- * Sent by the NetworkAgent to ConnectivityService to remove UID ranges
- * from being forced into this Network. For VPNs only.
- * obj = UidRange[] to stop forwarding
- */
- public static final int EVENT_UID_RANGES_REMOVED = BASE + 6;
-
- /**
* Sent by ConnectivityService to the NetworkAgent to inform the agent of the
* networks status - whether we could use the network or could not, due to
* either a bad network configuration (no internet link) or captive portal.
@@ -390,22 +376,6 @@ public abstract class NetworkAgent extends Handler {
}
/**
- * Called by the VPN code when it wants to add ranges of UIDs to be routed
- * through the VPN network.
- */
- public void addUidRanges(UidRange[] ranges) {
- queueOrSendMessage(EVENT_UID_RANGES_ADDED, ranges);
- }
-
- /**
- * Called by the VPN code when it wants to remove ranges of UIDs from being routed
- * through the VPN network.
- */
- public void removeUidRanges(UidRange[] ranges) {
- queueOrSendMessage(EVENT_UID_RANGES_REMOVED, ranges);
- }
-
- /**
* Called by the bearer to indicate this network was manually selected by the user.
* This should be called before the NetworkInfo is marked CONNECTED so that this
* Network can be given special treatment at that time. If {@code acceptUnvalidated} is
diff --git a/android/net/NetworkCapabilities.java b/android/net/NetworkCapabilities.java
index f468e5d2..8e05cfa9 100644
--- a/android/net/NetworkCapabilities.java
+++ b/android/net/NetworkCapabilities.java
@@ -20,6 +20,8 @@ import android.annotation.IntDef;
import android.net.ConnectivityManager.NetworkCallback;
import android.os.Parcel;
import android.os.Parcelable;
+import android.util.ArraySet;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.BitUtils;
@@ -28,6 +30,7 @@ import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Objects;
+import java.util.Set;
import java.util.StringJoiner;
/**
@@ -46,6 +49,7 @@ import java.util.StringJoiner;
*/
public final class NetworkCapabilities implements Parcelable {
private static final String TAG = "NetworkCapabilities";
+ private static final int INVALID_UID = -1;
/**
* @hide
@@ -63,6 +67,8 @@ public final class NetworkCapabilities implements Parcelable {
mLinkDownBandwidthKbps = nc.mLinkDownBandwidthKbps;
mNetworkSpecifier = nc.mNetworkSpecifier;
mSignalStrength = nc.mSignalStrength;
+ mUids = nc.mUids;
+ mEstablishingVpnAppUid = nc.mEstablishingVpnAppUid;
}
}
@@ -76,6 +82,8 @@ public final class NetworkCapabilities implements Parcelable {
mLinkUpBandwidthKbps = mLinkDownBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
mNetworkSpecifier = null;
mSignalStrength = SIGNAL_STRENGTH_UNSPECIFIED;
+ mUids = null;
+ mEstablishingVpnAppUid = INVALID_UID;
}
/**
@@ -107,6 +115,7 @@ public final class NetworkCapabilities implements Parcelable {
NET_CAPABILITY_CAPTIVE_PORTAL,
NET_CAPABILITY_NOT_ROAMING,
NET_CAPABILITY_FOREGROUND,
+ NET_CAPABILITY_NOT_CONGESTED,
})
public @interface NetCapability { }
@@ -234,8 +243,17 @@ public final class NetworkCapabilities implements Parcelable {
*/
public static final int NET_CAPABILITY_FOREGROUND = 19;
+ /**
+ * Indicates that this network is not congested.
+ * <p>
+ * When a network is congested, the device should defer network traffic that
+ * can be done at a later time without breaking developer contracts.
+ * @hide
+ */
+ public static final int NET_CAPABILITY_NOT_CONGESTED = 20;
+
private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS;
- private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_FOREGROUND;
+ private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_NOT_CONGESTED;
/**
* Network capabilities that are expected to be mutable, i.e., can change while a particular
@@ -248,7 +266,8 @@ public final class NetworkCapabilities implements Parcelable {
(1 << NET_CAPABILITY_VALIDATED) |
(1 << NET_CAPABILITY_CAPTIVE_PORTAL) |
(1 << NET_CAPABILITY_NOT_ROAMING) |
- (1 << NET_CAPABILITY_FOREGROUND);
+ (1 << NET_CAPABILITY_FOREGROUND) |
+ (1 << NET_CAPABILITY_NOT_CONGESTED);
/**
* Network capabilities that are not allowed in NetworkRequests. This exists because the
@@ -386,12 +405,9 @@ public final class NetworkCapabilities implements Parcelable {
* @hide
*/
public String describeFirstNonRequestableCapability() {
- if (hasCapability(NET_CAPABILITY_VALIDATED)) return "NET_CAPABILITY_VALIDATED";
- if (hasCapability(NET_CAPABILITY_CAPTIVE_PORTAL)) return "NET_CAPABILITY_CAPTIVE_PORTAL";
- if (hasCapability(NET_CAPABILITY_FOREGROUND)) return "NET_CAPABILITY_FOREGROUND";
- // This cannot happen unless the preceding checks are incomplete.
- if ((mNetworkCapabilities & NON_REQUESTABLE_CAPABILITIES) != 0) {
- return "unknown non-requestable capabilities " + Long.toHexString(mNetworkCapabilities);
+ final long nonRequestable = (mNetworkCapabilities & NON_REQUESTABLE_CAPABILITIES);
+ if (nonRequestable != 0) {
+ return capabilityNameOf(BitUtils.unpackBits(nonRequestable)[0]);
}
if (mLinkUpBandwidthKbps != 0 || mLinkDownBandwidthKbps != 0) return "link bandwidth";
if (hasSignalStrength()) return "signalStrength";
@@ -610,6 +626,29 @@ public final class NetworkCapabilities implements Parcelable {
}
/**
+ * UID of the app that manages this network, or INVALID_UID if none/unknown.
+ *
+ * This field keeps track of the UID of the app that created this network and is in charge
+ * of managing it. In the practice, it is used to store the UID of VPN apps so it is named
+ * accordingly, but it may be renamed if other mechanisms are offered for third party apps
+ * to create networks.
+ *
+ * Because this field is only used in the services side (and to avoid apps being able to
+ * set this to whatever they want), this field is not parcelled and will not be conserved
+ * across the IPC boundary.
+ * @hide
+ */
+ private int mEstablishingVpnAppUid = INVALID_UID;
+
+ /**
+ * Set the UID of the managing app.
+ * @hide
+ */
+ public void setEstablishingVpnAppUid(final int uid) {
+ mEstablishingVpnAppUid = uid;
+ }
+
+ /**
* Value indicating that link bandwidth is unspecified.
* @hide
*/
@@ -828,6 +867,174 @@ public final class NetworkCapabilities implements Parcelable {
}
/**
+ * List of UIDs this network applies to. No restriction if null.
+ * <p>
+ * This is typically (and at this time, only) used by VPN. This network is only available to
+ * the UIDs in this list, and it is their default network. Apps in this list that wish to
+ * bypass the VPN can do so iff the VPN app allows them to or if they are privileged. If this
+ * member is null, then the network is not restricted by app UID. If it's an empty list, then
+ * it means nobody can use it.
+ * As a special exception, the app managing this network (as identified by its UID stored in
+ * mEstablishingVpnAppUid) can always see this network. This is embodied by a special check in
+ * satisfiedByUids. That still does not mean the network necessarily <strong>applies</strong>
+ * to the app that manages it as determined by #appliesToUid.
+ * <p>
+ * Please note that in principle a single app can be associated with multiple UIDs because
+ * each app will have a different UID when it's run as a different (macro-)user. A single
+ * macro user can only have a single active VPN app at any given time however.
+ * <p>
+ * Also please be aware this class does not try to enforce any normalization on this. Callers
+ * can only alter the UIDs by setting them wholesale : this class does not provide any utility
+ * to add or remove individual UIDs or ranges. If callers have any normalization needs on
+ * their own (like requiring sortedness or no overlap) they need to enforce it
+ * themselves. Some of the internal methods also assume this is normalized as in no adjacent
+ * or overlapping ranges are present.
+ *
+ * @hide
+ */
+ private ArraySet<UidRange> mUids = null;
+
+ /**
+ * Convenience method to set the UIDs this network applies to to a single UID.
+ * @hide
+ */
+ public NetworkCapabilities setSingleUid(int uid) {
+ final ArraySet<UidRange> identity = new ArraySet<>(1);
+ identity.add(new UidRange(uid, uid));
+ setUids(identity);
+ return this;
+ }
+
+ /**
+ * Set the list of UIDs this network applies to.
+ * This makes a copy of the set so that callers can't modify it after the call.
+ * @hide
+ */
+ public NetworkCapabilities setUids(Set<UidRange> uids) {
+ if (null == uids) {
+ mUids = null;
+ } else {
+ mUids = new ArraySet<>(uids);
+ }
+ return this;
+ }
+
+ /**
+ * Get the list of UIDs this network applies to.
+ * This returns a copy of the set so that callers can't modify the original object.
+ * @hide
+ */
+ public Set<UidRange> getUids() {
+ return null == mUids ? null : new ArraySet<>(mUids);
+ }
+
+ /**
+ * Test whether this network applies to this UID.
+ * @hide
+ */
+ public boolean appliesToUid(int uid) {
+ if (null == mUids) return true;
+ for (UidRange range : mUids) {
+ if (range.contains(uid)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Tests if the set of UIDs that this network applies to is the same of the passed set of UIDs.
+ * <p>
+ * This test only checks whether equal range objects are in both sets. It will
+ * return false if the ranges are not exactly the same, even if the covered UIDs
+ * are for an equivalent result.
+ * <p>
+ * Note that this method is not very optimized, which is fine as long as it's not used very
+ * often.
+ * <p>
+ * nc is assumed nonnull.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public boolean equalsUids(NetworkCapabilities nc) {
+ Set<UidRange> comparedUids = nc.mUids;
+ if (null == comparedUids) return null == mUids;
+ if (null == mUids) return false;
+ // Make a copy so it can be mutated to check that all ranges in mUids
+ // also are in uids.
+ final Set<UidRange> uids = new ArraySet<>(mUids);
+ for (UidRange range : comparedUids) {
+ if (!uids.contains(range)) {
+ return false;
+ }
+ uids.remove(range);
+ }
+ return uids.isEmpty();
+ }
+
+ /**
+ * Test whether the passed NetworkCapabilities satisfies the UIDs this capabilities require.
+ *
+ * This method is called on the NetworkCapabilities embedded in a request with the
+ * capabilities of an available network. It checks whether all the UIDs from this listen
+ * (representing the UIDs that must have access to the network) are satisfied by the UIDs
+ * in the passed nc (representing the UIDs that this network is available to).
+ * <p>
+ * As a special exception, the UID that created the passed network (as represented by its
+ * mEstablishingVpnAppUid field) always satisfies a NetworkRequest requiring it (of LISTEN
+ * or REQUEST types alike), even if the network does not apply to it. That is so a VPN app
+ * can see its own network when it listens for it.
+ * <p>
+ * nc is assumed nonnull. Else, NPE.
+ * @see #appliesToUid
+ * @hide
+ */
+ public boolean satisfiedByUids(NetworkCapabilities nc) {
+ if (null == nc.mUids) return true; // The network satisfies everything.
+ if (null == mUids) return false; // Not everything allowed but requires everything
+ for (UidRange requiredRange : mUids) {
+ if (requiredRange.contains(nc.mEstablishingVpnAppUid)) return true;
+ if (!nc.appliesToUidRange(requiredRange)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Returns whether this network applies to the passed ranges.
+ * This assumes that to apply, the passed range has to be entirely contained
+ * within one of the ranges this network applies to. If the ranges are not normalized,
+ * this method may return false even though all required UIDs are covered because no
+ * single range contained them all.
+ * @hide
+ */
+ @VisibleForTesting
+ public boolean appliesToUidRange(UidRange requiredRange) {
+ if (null == mUids) return true;
+ for (UidRange uidRange : mUids) {
+ if (uidRange.containsRange(requiredRange)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Combine the UIDs this network currently applies to with the UIDs the passed
+ * NetworkCapabilities apply to.
+ * nc is assumed nonnull.
+ */
+ private void combineUids(NetworkCapabilities nc) {
+ if (null == nc.mUids || null == mUids) {
+ mUids = null;
+ return;
+ }
+ mUids.addAll(nc.mUids);
+ }
+
+ /**
* Combine a set of Capabilities to this one. Useful for coming up with the complete set
* @hide
*/
@@ -837,6 +1044,7 @@ public final class NetworkCapabilities implements Parcelable {
combineLinkBandwidths(nc);
combineSpecifiers(nc);
combineSignalStrength(nc);
+ combineUids(nc);
}
/**
@@ -849,12 +1057,13 @@ public final class NetworkCapabilities implements Parcelable {
* @hide
*/
private boolean satisfiedByNetworkCapabilities(NetworkCapabilities nc, boolean onlyImmutable) {
- return (nc != null &&
- satisfiedByNetCapabilities(nc, onlyImmutable) &&
- satisfiedByTransportTypes(nc) &&
- (onlyImmutable || satisfiedByLinkBandwidths(nc)) &&
- satisfiedBySpecifier(nc) &&
- (onlyImmutable || satisfiedBySignalStrength(nc)));
+ return (nc != null
+ && satisfiedByNetCapabilities(nc, onlyImmutable)
+ && satisfiedByTransportTypes(nc)
+ && (onlyImmutable || satisfiedByLinkBandwidths(nc))
+ && satisfiedBySpecifier(nc)
+ && (onlyImmutable || satisfiedBySignalStrength(nc))
+ && (onlyImmutable || satisfiedByUids(nc)));
}
/**
@@ -935,24 +1144,26 @@ public final class NetworkCapabilities implements Parcelable {
@Override
public boolean equals(Object obj) {
if (obj == null || (obj instanceof NetworkCapabilities == false)) return false;
- NetworkCapabilities that = (NetworkCapabilities)obj;
- return (equalsNetCapabilities(that) &&
- equalsTransportTypes(that) &&
- equalsLinkBandwidths(that) &&
- equalsSignalStrength(that) &&
- equalsSpecifier(that));
+ NetworkCapabilities that = (NetworkCapabilities) obj;
+ return (equalsNetCapabilities(that)
+ && equalsTransportTypes(that)
+ && equalsLinkBandwidths(that)
+ && equalsSignalStrength(that)
+ && equalsSpecifier(that)
+ && equalsUids(that));
}
@Override
public int hashCode() {
- return ((int)(mNetworkCapabilities & 0xFFFFFFFF) +
- ((int)(mNetworkCapabilities >> 32) * 3) +
- ((int)(mTransportTypes & 0xFFFFFFFF) * 5) +
- ((int)(mTransportTypes >> 32) * 7) +
- (mLinkUpBandwidthKbps * 11) +
- (mLinkDownBandwidthKbps * 13) +
- Objects.hashCode(mNetworkSpecifier) * 17 +
- (mSignalStrength * 19));
+ return ((int) (mNetworkCapabilities & 0xFFFFFFFF)
+ + ((int) (mNetworkCapabilities >> 32) * 3)
+ + ((int) (mTransportTypes & 0xFFFFFFFF) * 5)
+ + ((int) (mTransportTypes >> 32) * 7)
+ + (mLinkUpBandwidthKbps * 11)
+ + (mLinkDownBandwidthKbps * 13)
+ + Objects.hashCode(mNetworkSpecifier) * 17
+ + (mSignalStrength * 19)
+ + Objects.hashCode(mUids) * 23);
}
@Override
@@ -967,6 +1178,7 @@ public final class NetworkCapabilities implements Parcelable {
dest.writeInt(mLinkDownBandwidthKbps);
dest.writeParcelable((Parcelable) mNetworkSpecifier, flags);
dest.writeInt(mSignalStrength);
+ dest.writeArraySet(mUids);
}
public static final Creator<NetworkCapabilities> CREATOR =
@@ -981,6 +1193,8 @@ public final class NetworkCapabilities implements Parcelable {
netCap.mLinkDownBandwidthKbps = in.readInt();
netCap.mNetworkSpecifier = in.readParcelable(null);
netCap.mSignalStrength = in.readInt();
+ netCap.mUids = (ArraySet<UidRange>) in.readArraySet(
+ null /* ClassLoader, null for default */);
return netCap;
}
@Override
@@ -1013,7 +1227,37 @@ public final class NetworkCapabilities implements Parcelable {
String signalStrength = (hasSignalStrength() ? " SignalStrength: " + mSignalStrength : "");
- return "[" + transports + capabilities + upBand + dnBand + specifier + signalStrength + "]";
+ String uids = (null != mUids ? " Uids: <" + mUids + ">" : "");
+
+ String establishingAppUid = " EstablishingAppUid: " + mEstablishingVpnAppUid;
+
+ return "[" + transports + capabilities + upBand + dnBand + specifier + signalStrength
+ + uids + establishingAppUid + "]";
+ }
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ for (int transport : getTransportTypes()) {
+ proto.write(NetworkCapabilitiesProto.TRANSPORTS, transport);
+ }
+
+ for (int capability : getCapabilities()) {
+ proto.write(NetworkCapabilitiesProto.CAPABILITIES, capability);
+ }
+
+ proto.write(NetworkCapabilitiesProto.LINK_UP_BANDWIDTH_KBPS, mLinkUpBandwidthKbps);
+ proto.write(NetworkCapabilitiesProto.LINK_DOWN_BANDWIDTH_KBPS, mLinkDownBandwidthKbps);
+
+ if (mNetworkSpecifier != null) {
+ proto.write(NetworkCapabilitiesProto.NETWORK_SPECIFIER, mNetworkSpecifier.toString());
+ }
+
+ proto.write(NetworkCapabilitiesProto.CAN_REPORT_SIGNAL_STRENGTH, hasSignalStrength());
+ proto.write(NetworkCapabilitiesProto.SIGNAL_STRENGTH, mSignalStrength);
+
+ proto.end(token);
}
/**
@@ -1054,6 +1298,7 @@ public final class NetworkCapabilities implements Parcelable {
case NET_CAPABILITY_CAPTIVE_PORTAL: return "CAPTIVE_PORTAL";
case NET_CAPABILITY_NOT_ROAMING: return "NOT_ROAMING";
case NET_CAPABILITY_FOREGROUND: return "FOREGROUND";
+ case NET_CAPABILITY_NOT_CONGESTED: return "NOT_CONGESTED";
default: return Integer.toString(capability);
}
}
diff --git a/android/net/NetworkIdentity.java b/android/net/NetworkIdentity.java
index d3b35998..ce2de855 100644
--- a/android/net/NetworkIdentity.java
+++ b/android/net/NetworkIdentity.java
@@ -58,21 +58,24 @@ public class NetworkIdentity implements Comparable<NetworkIdentity> {
final String mNetworkId;
final boolean mRoaming;
final boolean mMetered;
+ final boolean mDefaultNetwork;
public NetworkIdentity(
int type, int subType, String subscriberId, String networkId, boolean roaming,
- boolean metered) {
+ boolean metered, boolean defaultNetwork) {
mType = type;
mSubType = COMBINE_SUBTYPE_ENABLED ? SUBTYPE_COMBINED : subType;
mSubscriberId = subscriberId;
mNetworkId = networkId;
mRoaming = roaming;
mMetered = metered;
+ mDefaultNetwork = defaultNetwork;
}
@Override
public int hashCode() {
- return Objects.hash(mType, mSubType, mSubscriberId, mNetworkId, mRoaming, mMetered);
+ return Objects.hash(mType, mSubType, mSubscriberId, mNetworkId, mRoaming, mMetered,
+ mDefaultNetwork);
}
@Override
@@ -82,7 +85,8 @@ public class NetworkIdentity implements Comparable<NetworkIdentity> {
return mType == ident.mType && mSubType == ident.mSubType && mRoaming == ident.mRoaming
&& Objects.equals(mSubscriberId, ident.mSubscriberId)
&& Objects.equals(mNetworkId, ident.mNetworkId)
- && mMetered == ident.mMetered;
+ && mMetered == ident.mMetered
+ && mDefaultNetwork == ident.mDefaultNetwork;
}
return false;
}
@@ -109,6 +113,7 @@ public class NetworkIdentity implements Comparable<NetworkIdentity> {
builder.append(", ROAMING");
}
builder.append(", metered=").append(mMetered);
+ builder.append(", defaultNetwork=").append(mDefaultNetwork);
return builder.append("}").toString();
}
@@ -125,6 +130,7 @@ public class NetworkIdentity implements Comparable<NetworkIdentity> {
proto.write(NetworkIdentityProto.NETWORK_ID, mNetworkId);
proto.write(NetworkIdentityProto.ROAMING, mRoaming);
proto.write(NetworkIdentityProto.METERED, mMetered);
+ proto.write(NetworkIdentityProto.DEFAULT_NETWORK, mDefaultNetwork);
proto.end(start);
}
@@ -153,6 +159,10 @@ public class NetworkIdentity implements Comparable<NetworkIdentity> {
return mMetered;
}
+ public boolean getDefaultNetwork() {
+ return mDefaultNetwork;
+ }
+
/**
* Scrub given IMSI on production builds.
*/
@@ -183,7 +193,8 @@ public class NetworkIdentity implements Comparable<NetworkIdentity> {
* Build a {@link NetworkIdentity} from the given {@link NetworkState},
* assuming that any mobile networks are using the current IMSI.
*/
- public static NetworkIdentity buildNetworkIdentity(Context context, NetworkState state) {
+ public static NetworkIdentity buildNetworkIdentity(Context context, NetworkState state,
+ boolean defaultNetwork) {
final int type = state.networkInfo.getType();
final int subType = state.networkInfo.getSubtype();
@@ -216,7 +227,8 @@ public class NetworkIdentity implements Comparable<NetworkIdentity> {
}
}
- return new NetworkIdentity(type, subType, subscriberId, networkId, roaming, metered);
+ return new NetworkIdentity(type, subType, subscriberId, networkId, roaming, metered,
+ defaultNetwork);
}
@Override
@@ -237,6 +249,9 @@ public class NetworkIdentity implements Comparable<NetworkIdentity> {
if (res == 0) {
res = Boolean.compare(mMetered, another.mMetered);
}
+ if (res == 0) {
+ res = Boolean.compare(mDefaultNetwork, another.mDefaultNetwork);
+ }
return res;
}
}
diff --git a/android/net/NetworkPolicyManager.java b/android/net/NetworkPolicyManager.java
index 81c49a33..2c5a021e 100644
--- a/android/net/NetworkPolicyManager.java
+++ b/android/net/NetworkPolicyManager.java
@@ -29,7 +29,6 @@ import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.os.RemoteException;
import android.os.UserHandle;
-import android.telephony.SubscriptionPlan;
import android.util.DebugUtils;
import android.util.Pair;
@@ -114,6 +113,9 @@ public class NetworkPolicyManager {
*/
public static final String EXTRA_NETWORK_TEMPLATE = "android.net.NETWORK_TEMPLATE";
+ public static final int OVERRIDE_UNMETERED = 1 << 0;
+ public static final int OVERRIDE_CONGESTED = 1 << 1;
+
private final Context mContext;
private INetworkPolicyManager mService;
@@ -329,7 +331,7 @@ public class NetworkPolicyManager {
* to access network when the device is idle or in battery saver mode. Otherwise, false.
*/
public static boolean isProcStateAllowedWhileIdleOrPowerSaveMode(int procState) {
- return procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+ return procState <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
}
/**
@@ -337,7 +339,7 @@ public class NetworkPolicyManager {
* to access network when the device is in data saver mode. Otherwise, false.
*/
public static boolean isProcStateAllowedWhileOnRestrictBackground(int procState) {
- return procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+ return procState <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
}
public static String resolveNetworkId(WifiConfiguration config) {
@@ -348,4 +350,13 @@ public class NetworkPolicyManager {
public static String resolveNetworkId(String ssid) {
return WifiInfo.removeDoubleQuotes(ssid);
}
+
+ /** {@hide} */
+ public static class Listener extends INetworkPolicyListener.Stub {
+ @Override public void onUidRulesChanged(int uid, int uidRules) { }
+ @Override public void onMeteredIfacesChanged(String[] meteredIfaces) { }
+ @Override public void onRestrictBackgroundChanged(boolean restrictBackground) { }
+ @Override public void onUidPoliciesChanged(int uid, int uidPolicies) { }
+ @Override public void onSubscriptionOverride(int subId, int overrideMask, int overrideValue) { }
+ }
}
diff --git a/android/net/NetworkRequest.java b/android/net/NetworkRequest.java
index 97ded2d7..a0724091 100644
--- a/android/net/NetworkRequest.java
+++ b/android/net/NetworkRequest.java
@@ -20,6 +20,7 @@ import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;
+import android.util.proto.ProtoOutputStream;
import java.util.Objects;
@@ -389,6 +390,35 @@ public class NetworkRequest implements Parcelable {
", " + networkCapabilities.toString() + " ]";
}
+ private int typeToProtoEnum(Type t) {
+ switch (t) {
+ case NONE:
+ return NetworkRequestProto.TYPE_NONE;
+ case LISTEN:
+ return NetworkRequestProto.TYPE_LISTEN;
+ case TRACK_DEFAULT:
+ return NetworkRequestProto.TYPE_TRACK_DEFAULT;
+ case REQUEST:
+ return NetworkRequestProto.TYPE_REQUEST;
+ case BACKGROUND_REQUEST:
+ return NetworkRequestProto.TYPE_BACKGROUND_REQUEST;
+ default:
+ return NetworkRequestProto.TYPE_UNKNOWN;
+ }
+ }
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(NetworkRequestProto.TYPE, typeToProtoEnum(type));
+ proto.write(NetworkRequestProto.REQUEST_ID, requestId);
+ proto.write(NetworkRequestProto.LEGACY_TYPE, legacyType);
+ networkCapabilities.writeToProto(proto, NetworkRequestProto.NETWORK_CAPABILITIES);
+
+ proto.end(token);
+ }
+
public boolean equals(Object obj) {
if (obj instanceof NetworkRequest == false) return false;
NetworkRequest that = (NetworkRequest)obj;
diff --git a/android/net/NetworkStats.java b/android/net/NetworkStats.java
index 171adc05..01b2b392 100644
--- a/android/net/NetworkStats.java
+++ b/android/net/NetworkStats.java
@@ -82,6 +82,13 @@ public class NetworkStats implements Parcelable {
/** {@link #roaming} value where roaming data is accounted. */
public static final int ROAMING_YES = 1;
+ /** {@link #onDefaultNetwork} value to account for all default network states. */
+ public static final int DEFAULT_NETWORK_ALL = -1;
+ /** {@link #onDefaultNetwork} value to account for usage while not the default network. */
+ public static final int DEFAULT_NETWORK_NO = 0;
+ /** {@link #onDefaultNetwork} value to account for usage while the default network. */
+ public static final int DEFAULT_NETWORK_YES = 1;
+
/** Denotes a request for stats at the interface level. */
public static final int STATS_PER_IFACE = 0;
/** Denotes a request for stats at the interface and UID level. */
@@ -102,6 +109,7 @@ public class NetworkStats implements Parcelable {
private int[] tag;
private int[] metered;
private int[] roaming;
+ private int[] defaultNetwork;
private long[] rxBytes;
private long[] rxPackets;
private long[] txBytes;
@@ -125,6 +133,12 @@ public class NetworkStats implements Parcelable {
* getSummary().
*/
public int roaming;
+ /**
+ * Note that this is only populated w/ the default value when read from /proc or written
+ * to disk. We merge in the correct value when reporting this value to clients of
+ * getSummary().
+ */
+ public int defaultNetwork;
public long rxBytes;
public long rxPackets;
public long txBytes;
@@ -142,18 +156,20 @@ public class NetworkStats implements Parcelable {
public Entry(String iface, int uid, int set, int tag, long rxBytes, long rxPackets,
long txBytes, long txPackets, long operations) {
- this(iface, uid, set, tag, METERED_NO, ROAMING_NO, rxBytes, rxPackets, txBytes,
- txPackets, operations);
+ this(iface, uid, set, tag, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO,
+ rxBytes, rxPackets, txBytes, txPackets, operations);
}
public Entry(String iface, int uid, int set, int tag, int metered, int roaming,
- long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) {
+ int defaultNetwork, long rxBytes, long rxPackets, long txBytes, long txPackets,
+ long operations) {
this.iface = iface;
this.uid = uid;
this.set = set;
this.tag = tag;
this.metered = metered;
this.roaming = roaming;
+ this.defaultNetwork = defaultNetwork;
this.rxBytes = rxBytes;
this.rxPackets = rxPackets;
this.txBytes = txBytes;
@@ -187,6 +203,7 @@ public class NetworkStats implements Parcelable {
builder.append(" tag=").append(tagToString(tag));
builder.append(" metered=").append(meteredToString(metered));
builder.append(" roaming=").append(roamingToString(roaming));
+ builder.append(" defaultNetwork=").append(defaultNetworkToString(defaultNetwork));
builder.append(" rxBytes=").append(rxBytes);
builder.append(" rxPackets=").append(rxPackets);
builder.append(" txBytes=").append(txBytes);
@@ -200,7 +217,8 @@ public class NetworkStats implements Parcelable {
if (o instanceof Entry) {
final Entry e = (Entry) o;
return uid == e.uid && set == e.set && tag == e.tag && metered == e.metered
- && roaming == e.roaming && rxBytes == e.rxBytes && rxPackets == e.rxPackets
+ && roaming == e.roaming && defaultNetwork == e.defaultNetwork
+ && rxBytes == e.rxBytes && rxPackets == e.rxPackets
&& txBytes == e.txBytes && txPackets == e.txPackets
&& operations == e.operations && iface.equals(e.iface);
}
@@ -209,7 +227,7 @@ public class NetworkStats implements Parcelable {
@Override
public int hashCode() {
- return Objects.hash(uid, set, tag, metered, roaming, iface);
+ return Objects.hash(uid, set, tag, metered, roaming, defaultNetwork, iface);
}
}
@@ -224,6 +242,7 @@ public class NetworkStats implements Parcelable {
this.tag = new int[initialSize];
this.metered = new int[initialSize];
this.roaming = new int[initialSize];
+ this.defaultNetwork = new int[initialSize];
this.rxBytes = new long[initialSize];
this.rxPackets = new long[initialSize];
this.txBytes = new long[initialSize];
@@ -238,6 +257,7 @@ public class NetworkStats implements Parcelable {
this.tag = EmptyArray.INT;
this.metered = EmptyArray.INT;
this.roaming = EmptyArray.INT;
+ this.defaultNetwork = EmptyArray.INT;
this.rxBytes = EmptyArray.LONG;
this.rxPackets = EmptyArray.LONG;
this.txBytes = EmptyArray.LONG;
@@ -256,6 +276,7 @@ public class NetworkStats implements Parcelable {
tag = parcel.createIntArray();
metered = parcel.createIntArray();
roaming = parcel.createIntArray();
+ defaultNetwork = parcel.createIntArray();
rxBytes = parcel.createLongArray();
rxPackets = parcel.createLongArray();
txBytes = parcel.createLongArray();
@@ -274,6 +295,7 @@ public class NetworkStats implements Parcelable {
dest.writeIntArray(tag);
dest.writeIntArray(metered);
dest.writeIntArray(roaming);
+ dest.writeIntArray(defaultNetwork);
dest.writeLongArray(rxBytes);
dest.writeLongArray(rxPackets);
dest.writeLongArray(txBytes);
@@ -308,10 +330,11 @@ public class NetworkStats implements Parcelable {
@VisibleForTesting
public NetworkStats addValues(String iface, int uid, int set, int tag, int metered, int roaming,
- long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) {
+ int defaultNetwork, long rxBytes, long rxPackets, long txBytes, long txPackets,
+ long operations) {
return addValues(new Entry(
- iface, uid, set, tag, metered, roaming, rxBytes, rxPackets, txBytes, txPackets,
- operations));
+ iface, uid, set, tag, metered, roaming, defaultNetwork, rxBytes, rxPackets,
+ txBytes, txPackets, operations));
}
/**
@@ -327,6 +350,7 @@ public class NetworkStats implements Parcelable {
tag = Arrays.copyOf(tag, newLength);
metered = Arrays.copyOf(metered, newLength);
roaming = Arrays.copyOf(roaming, newLength);
+ defaultNetwork = Arrays.copyOf(defaultNetwork, newLength);
rxBytes = Arrays.copyOf(rxBytes, newLength);
rxPackets = Arrays.copyOf(rxPackets, newLength);
txBytes = Arrays.copyOf(txBytes, newLength);
@@ -341,6 +365,7 @@ public class NetworkStats implements Parcelable {
tag[size] = entry.tag;
metered[size] = entry.metered;
roaming[size] = entry.roaming;
+ defaultNetwork[size] = entry.defaultNetwork;
rxBytes[size] = entry.rxBytes;
rxPackets[size] = entry.rxPackets;
txBytes[size] = entry.txBytes;
@@ -362,6 +387,7 @@ public class NetworkStats implements Parcelable {
entry.tag = tag[i];
entry.metered = metered[i];
entry.roaming = roaming[i];
+ entry.defaultNetwork = defaultNetwork[i];
entry.rxBytes = rxBytes[i];
entry.rxPackets = rxPackets[i];
entry.txBytes = txBytes[i];
@@ -416,7 +442,7 @@ public class NetworkStats implements Parcelable {
*/
public NetworkStats combineValues(Entry entry) {
final int i = findIndex(entry.iface, entry.uid, entry.set, entry.tag, entry.metered,
- entry.roaming);
+ entry.roaming, entry.defaultNetwork);
if (i == -1) {
// only create new entry when positive contribution
addValues(entry);
@@ -444,10 +470,12 @@ public class NetworkStats implements Parcelable {
/**
* Find first stats index that matches the requested parameters.
*/
- public int findIndex(String iface, int uid, int set, int tag, int metered, int roaming) {
+ public int findIndex(String iface, int uid, int set, int tag, int metered, int roaming,
+ int defaultNetwork) {
for (int i = 0; i < size; i++) {
if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i]
&& metered == this.metered[i] && roaming == this.roaming[i]
+ && defaultNetwork == this.defaultNetwork[i]
&& Objects.equals(iface, this.iface[i])) {
return i;
}
@@ -461,7 +489,7 @@ public class NetworkStats implements Parcelable {
*/
@VisibleForTesting
public int findIndexHinted(String iface, int uid, int set, int tag, int metered, int roaming,
- int hintIndex) {
+ int defaultNetwork, int hintIndex) {
for (int offset = 0; offset < size; offset++) {
final int halfOffset = offset / 2;
@@ -475,6 +503,7 @@ public class NetworkStats implements Parcelable {
if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i]
&& metered == this.metered[i] && roaming == this.roaming[i]
+ && defaultNetwork == this.defaultNetwork[i]
&& Objects.equals(iface, this.iface[i])) {
return i;
}
@@ -489,7 +518,8 @@ public class NetworkStats implements Parcelable {
*/
public void spliceOperationsFrom(NetworkStats stats) {
for (int i = 0; i < size; i++) {
- final int j = stats.findIndex(iface[i], uid[i], set[i], tag[i], metered[i], roaming[i]);
+ final int j = stats.findIndex(iface[i], uid[i], set[i], tag[i], metered[i], roaming[i],
+ defaultNetwork[i]);
if (j == -1) {
operations[i] = 0;
} else {
@@ -581,6 +611,7 @@ public class NetworkStats implements Parcelable {
entry.tag = TAG_NONE;
entry.metered = METERED_ALL;
entry.roaming = ROAMING_ALL;
+ entry.defaultNetwork = DEFAULT_NETWORK_ALL;
entry.rxBytes = 0;
entry.rxPackets = 0;
entry.txBytes = 0;
@@ -677,6 +708,7 @@ public class NetworkStats implements Parcelable {
entry.tag = left.tag[i];
entry.metered = left.metered[i];
entry.roaming = left.roaming[i];
+ entry.defaultNetwork = left.defaultNetwork[i];
entry.rxBytes = left.rxBytes[i];
entry.rxPackets = left.rxPackets[i];
entry.txBytes = left.txBytes[i];
@@ -685,7 +717,7 @@ public class NetworkStats implements Parcelable {
// find remote row that matches, and subtract
final int j = right.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag,
- entry.metered, entry.roaming, i);
+ entry.metered, entry.roaming, entry.defaultNetwork, i);
if (j != -1) {
// Found matching row, subtract remote value.
entry.rxBytes -= right.rxBytes[j];
@@ -725,6 +757,7 @@ public class NetworkStats implements Parcelable {
entry.tag = TAG_NONE;
entry.metered = METERED_ALL;
entry.roaming = ROAMING_ALL;
+ entry.defaultNetwork = DEFAULT_NETWORK_ALL;
entry.operations = 0L;
for (int i = 0; i < size; i++) {
@@ -755,6 +788,7 @@ public class NetworkStats implements Parcelable {
entry.tag = TAG_NONE;
entry.metered = METERED_ALL;
entry.roaming = ROAMING_ALL;
+ entry.defaultNetwork = DEFAULT_NETWORK_ALL;
for (int i = 0; i < size; i++) {
// skip specific tags, since already counted in TAG_NONE
@@ -802,6 +836,7 @@ public class NetworkStats implements Parcelable {
pw.print(" tag="); pw.print(tagToString(tag[i]));
pw.print(" metered="); pw.print(meteredToString(metered[i]));
pw.print(" roaming="); pw.print(roamingToString(roaming[i]));
+ pw.print(" defaultNetwork="); pw.print(defaultNetworkToString(defaultNetwork[i]));
pw.print(" rxBytes="); pw.print(rxBytes[i]);
pw.print(" rxPackets="); pw.print(rxPackets[i]);
pw.print(" txBytes="); pw.print(txBytes[i]);
@@ -900,6 +935,22 @@ public class NetworkStats implements Parcelable {
}
}
+ /**
+ * Return text description of {@link #defaultNetwork} value.
+ */
+ public static String defaultNetworkToString(int defaultNetwork) {
+ switch (defaultNetwork) {
+ case DEFAULT_NETWORK_ALL:
+ return "ALL";
+ case DEFAULT_NETWORK_NO:
+ return "NO";
+ case DEFAULT_NETWORK_YES:
+ return "YES";
+ default:
+ return "UNKNOWN";
+ }
+ }
+
@Override
public String toString() {
final CharArrayWriter writer = new CharArrayWriter();
@@ -1055,6 +1106,7 @@ public class NetworkStats implements Parcelable {
tmpEntry.set = set[i];
tmpEntry.metered = metered[i];
tmpEntry.roaming = roaming[i];
+ tmpEntry.defaultNetwork = defaultNetwork[i];
combineValues(tmpEntry);
if (tag[i] == TAG_NONE) {
moved.add(tmpEntry);
@@ -1075,6 +1127,7 @@ public class NetworkStats implements Parcelable {
moved.iface = underlyingIface;
moved.metered = METERED_ALL;
moved.roaming = ROAMING_ALL;
+ moved.defaultNetwork = DEFAULT_NETWORK_ALL;
combineValues(moved);
// Caveat: if the vpn software uses tag, the total tagged traffic may be greater than
@@ -1085,13 +1138,13 @@ public class NetworkStats implements Parcelable {
// roaming data after applying these adjustments, by checking the NetworkIdentity of the
// underlying iface.
int idxVpnBackground = findIndex(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE,
- METERED_NO, ROAMING_NO);
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
if (idxVpnBackground != -1) {
tunSubtract(idxVpnBackground, this, moved);
}
int idxVpnForeground = findIndex(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE,
- METERED_NO, ROAMING_NO);
+ METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
if (idxVpnForeground != -1) {
tunSubtract(idxVpnForeground, this, moved);
}
diff --git a/android/net/NetworkTemplate.java b/android/net/NetworkTemplate.java
index b307c5d6..8efd39a7 100644
--- a/android/net/NetworkTemplate.java
+++ b/android/net/NetworkTemplate.java
@@ -24,6 +24,15 @@ import static android.net.ConnectivityManager.TYPE_WIFI;
import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
import static android.net.ConnectivityManager.TYPE_WIMAX;
import static android.net.NetworkIdentity.COMBINE_SUBTYPE_ENABLED;
+import static android.net.NetworkStats.DEFAULT_NETWORK_ALL;
+import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_ALL;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkStats.ROAMING_ALL;
+import static android.net.NetworkStats.ROAMING_NO;
+import static android.net.NetworkStats.ROAMING_YES;
import static android.net.wifi.WifiInfo.removeDoubleQuotes;
import static android.telephony.TelephonyManager.NETWORK_CLASS_2_G;
import static android.telephony.TelephonyManager.NETWORK_CLASS_3_G;
@@ -191,16 +200,30 @@ public class NetworkTemplate implements Parcelable {
private final String mNetworkId;
+ // Matches for the NetworkStats constants METERED_*, ROAMING_* and DEFAULT_NETWORK_*.
+ private final int mMetered;
+ private final int mRoaming;
+ private final int mDefaultNetwork;
+
public NetworkTemplate(int matchRule, String subscriberId, String networkId) {
this(matchRule, subscriberId, new String[] { subscriberId }, networkId);
}
public NetworkTemplate(int matchRule, String subscriberId, String[] matchSubscriberIds,
String networkId) {
+ this(matchRule, subscriberId, matchSubscriberIds, networkId, METERED_ALL, ROAMING_ALL,
+ DEFAULT_NETWORK_ALL);
+ }
+
+ public NetworkTemplate(int matchRule, String subscriberId, String[] matchSubscriberIds,
+ String networkId, int metered, int roaming, int defaultNetwork) {
mMatchRule = matchRule;
mSubscriberId = subscriberId;
mMatchSubscriberIds = matchSubscriberIds;
mNetworkId = networkId;
+ mMetered = metered;
+ mRoaming = roaming;
+ mDefaultNetwork = defaultNetwork;
if (!isKnownMatchRule(matchRule)) {
Log.e(TAG, "Unknown network template rule " + matchRule
@@ -213,6 +236,9 @@ public class NetworkTemplate implements Parcelable {
mSubscriberId = in.readString();
mMatchSubscriberIds = in.createStringArray();
mNetworkId = in.readString();
+ mMetered = in.readInt();
+ mRoaming = in.readInt();
+ mDefaultNetwork = in.readInt();
}
@Override
@@ -221,6 +247,9 @@ public class NetworkTemplate implements Parcelable {
dest.writeString(mSubscriberId);
dest.writeStringArray(mMatchSubscriberIds);
dest.writeString(mNetworkId);
+ dest.writeInt(mMetered);
+ dest.writeInt(mRoaming);
+ dest.writeInt(mDefaultNetwork);
}
@Override
@@ -243,12 +272,23 @@ public class NetworkTemplate implements Parcelable {
if (mNetworkId != null) {
builder.append(", networkId=").append(mNetworkId);
}
+ if (mMetered != METERED_ALL) {
+ builder.append(", metered=").append(NetworkStats.meteredToString(mMetered));
+ }
+ if (mRoaming != ROAMING_ALL) {
+ builder.append(", roaming=").append(NetworkStats.roamingToString(mRoaming));
+ }
+ if (mDefaultNetwork != DEFAULT_NETWORK_ALL) {
+ builder.append(", defaultNetwork=").append(NetworkStats.defaultNetworkToString(
+ mDefaultNetwork));
+ }
return builder.toString();
}
@Override
public int hashCode() {
- return Objects.hash(mMatchRule, mSubscriberId, mNetworkId);
+ return Objects.hash(mMatchRule, mSubscriberId, mNetworkId, mMetered, mRoaming,
+ mDefaultNetwork);
}
@Override
@@ -257,7 +297,10 @@ public class NetworkTemplate implements Parcelable {
final NetworkTemplate other = (NetworkTemplate) obj;
return mMatchRule == other.mMatchRule
&& Objects.equals(mSubscriberId, other.mSubscriberId)
- && Objects.equals(mNetworkId, other.mNetworkId);
+ && Objects.equals(mNetworkId, other.mNetworkId)
+ && mMetered == other.mMetered
+ && mRoaming == other.mRoaming
+ && mDefaultNetwork == other.mDefaultNetwork;
}
return false;
}
@@ -300,6 +343,10 @@ public class NetworkTemplate implements Parcelable {
* Test if given {@link NetworkIdentity} matches this template.
*/
public boolean matches(NetworkIdentity ident) {
+ if (!matchesMetered(ident)) return false;
+ if (!matchesRoaming(ident)) return false;
+ if (!matchesDefaultNetwork(ident)) return false;
+
switch (mMatchRule) {
case MATCH_MOBILE_ALL:
return matchesMobile(ident);
@@ -326,6 +373,24 @@ public class NetworkTemplate implements Parcelable {
}
}
+ private boolean matchesMetered(NetworkIdentity ident) {
+ return (mMetered == METERED_ALL)
+ || (mMetered == METERED_YES && ident.mMetered)
+ || (mMetered == METERED_NO && !ident.mMetered);
+ }
+
+ private boolean matchesRoaming(NetworkIdentity ident) {
+ return (mRoaming == ROAMING_ALL)
+ || (mRoaming == ROAMING_YES && ident.mRoaming)
+ || (mRoaming == ROAMING_NO && !ident.mRoaming);
+ }
+
+ private boolean matchesDefaultNetwork(NetworkIdentity ident) {
+ return (mDefaultNetwork == DEFAULT_NETWORK_ALL)
+ || (mDefaultNetwork == DEFAULT_NETWORK_YES && ident.mDefaultNetwork)
+ || (mDefaultNetwork == DEFAULT_NETWORK_NO && !ident.mDefaultNetwork);
+ }
+
public boolean matchesSubscriberId(String subscriberId) {
return ArrayUtils.contains(mMatchSubscriberIds, subscriberId);
}
diff --git a/android/net/NetworkWatchlistManager.java b/android/net/NetworkWatchlistManager.java
index 42e43c8a..49047d3a 100644
--- a/android/net/NetworkWatchlistManager.java
+++ b/android/net/NetworkWatchlistManager.java
@@ -59,8 +59,8 @@ public class NetworkWatchlistManager {
/**
* Report network watchlist records if necessary.
*
- * Watchlist report process will run summarize records into a single report, then the
- * report will be processed by differential privacy framework and store it on disk.
+ * Watchlist report process will summarize records into a single report, then the
+ * report will be processed by differential privacy framework and stored on disk.
*
* @hide
*/
@@ -72,4 +72,30 @@ public class NetworkWatchlistManager {
e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Reload network watchlist.
+ *
+ * @hide
+ */
+ public void reloadWatchlist() {
+ try {
+ mNetworkWatchlistManager.reloadWatchlist();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to reload watchlist");
+ e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Get Network Watchlist config file hash.
+ */
+ public byte[] getWatchlistConfigHash() {
+ try {
+ return mNetworkWatchlistManager.getWatchlistConfigHash();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to get watchlist config hash");
+ throw e.rethrowFromSystemServer();
+ }
+ }
}
diff --git a/android/net/TrafficStats.java b/android/net/TrafficStats.java
index 196a3bc9..bda720bb 100644
--- a/android/net/TrafficStats.java
+++ b/android/net/TrafficStats.java
@@ -27,6 +27,7 @@ import android.content.Context;
import android.media.MediaPlayer;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.util.DataUnit;
import com.android.server.NetworkManagementSocketTagger;
@@ -56,15 +57,20 @@ public class TrafficStats {
*/
public final static int UNSUPPORTED = -1;
- /** @hide */
+ /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+ @Deprecated
public static final long KB_IN_BYTES = 1024;
- /** @hide */
+ /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+ @Deprecated
public static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
- /** @hide */
+ /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+ @Deprecated
public static final long GB_IN_BYTES = MB_IN_BYTES * 1024;
- /** @hide */
+ /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+ @Deprecated
public static final long TB_IN_BYTES = GB_IN_BYTES * 1024;
- /** @hide */
+ /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+ @Deprecated
public static final long PB_IN_BYTES = TB_IN_BYTES * 1024;
/**
diff --git a/android/net/apf/ApfFilter.java b/android/net/apf/ApfFilter.java
index 31a1abb3..7d9736ed 100644
--- a/android/net/apf/ApfFilter.java
+++ b/android/net/apf/ApfFilter.java
@@ -38,6 +38,7 @@ import android.net.metrics.ApfProgramEvent;
import android.net.metrics.ApfStats;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.RaEvent;
+import android.net.util.InterfaceParams;
import android.system.ErrnoException;
import android.system.Os;
import android.system.PacketSocketAddress;
@@ -56,7 +57,6 @@ import java.lang.Thread;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
-import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
@@ -247,7 +247,7 @@ public class ApfFilter {
private final ApfCapabilities mApfCapabilities;
private final IpClient.Callback mIpClientCallback;
- private final NetworkInterface mNetworkInterface;
+ private final InterfaceParams mInterfaceParams;
private final IpConnectivityLog mMetricsLog;
@VisibleForTesting
@@ -269,11 +269,11 @@ public class ApfFilter {
private int mIPv4PrefixLength;
@VisibleForTesting
- ApfFilter(ApfConfiguration config, NetworkInterface networkInterface,
+ ApfFilter(ApfConfiguration config, InterfaceParams ifParams,
IpClient.Callback ipClientCallback, IpConnectivityLog log) {
mApfCapabilities = config.apfCapabilities;
mIpClientCallback = ipClientCallback;
- mNetworkInterface = networkInterface;
+ mInterfaceParams = ifParams;
mMulticastFilter = config.multicastFilter;
mDrop802_3Frames = config.ieee802_3Filter;
@@ -287,7 +287,7 @@ public class ApfFilter {
}
private void log(String s) {
- Log.d(TAG, "(" + mNetworkInterface.getName() + "): " + s);
+ Log.d(TAG, "(" + mInterfaceParams.name + "): " + s);
}
@GuardedBy("this")
@@ -332,14 +332,14 @@ public class ApfFilter {
void maybeStartFilter() {
FileDescriptor socket;
try {
- mHardwareAddress = mNetworkInterface.getHardwareAddress();
+ mHardwareAddress = mInterfaceParams.macAddr.toByteArray();
synchronized(this) {
// Install basic filters
installNewProgramLocked();
}
socket = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IPV6);
- PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IPV6,
- mNetworkInterface.getIndex());
+ PacketSocketAddress addr = new PacketSocketAddress(
+ (short) ETH_P_IPV6, mInterfaceParams.index);
Os.bind(socket, addr);
NetworkUtils.attachRaFilter(socket, mApfCapabilities.apfPacketFormat);
} catch(SocketException|ErrnoException e) {
@@ -1168,10 +1168,10 @@ public class ApfFilter {
* filtering using APF programs.
*/
public static ApfFilter maybeCreate(ApfConfiguration config,
- NetworkInterface networkInterface, IpClient.Callback ipClientCallback) {
- if (config == null) return null;
+ InterfaceParams ifParams, IpClient.Callback ipClientCallback) {
+ if (config == null || ifParams == null) return null;
ApfCapabilities apfCapabilities = config.apfCapabilities;
- if (apfCapabilities == null || networkInterface == null) return null;
+ if (apfCapabilities == null) return null;
if (apfCapabilities.apfVersionSupported == 0) return null;
if (apfCapabilities.maximumApfProgramSize < 512) {
Log.e(TAG, "Unacceptably small APF limit: " + apfCapabilities.maximumApfProgramSize);
@@ -1186,7 +1186,7 @@ public class ApfFilter {
Log.e(TAG, "Unsupported APF version: " + apfCapabilities.apfVersionSupported);
return null;
}
- return new ApfFilter(config, networkInterface, ipClientCallback, new IpConnectivityLog());
+ return new ApfFilter(config, ifParams, ipClientCallback, new IpConnectivityLog());
}
public synchronized void shutdown() {
diff --git a/android/net/dhcp/DhcpClient.java b/android/net/dhcp/DhcpClient.java
index ed78175b..a956cefd 100644
--- a/android/net/dhcp/DhcpClient.java
+++ b/android/net/dhcp/DhcpClient.java
@@ -34,6 +34,7 @@ import android.net.TrafficStats;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.DhcpClientEvent;
import android.net.metrics.DhcpErrorEvent;
+import android.net.util.InterfaceParams;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -50,7 +51,6 @@ import java.io.FileDescriptor;
import java.io.IOException;
import java.lang.Thread;
import java.net.Inet4Address;
-import java.net.NetworkInterface;
import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.Arrays;
@@ -187,7 +187,8 @@ public class DhcpClient extends StateMachine {
private final String mIfaceName;
private boolean mRegisteredForPreDhcpNotification;
- private NetworkInterface mIface;
+ private InterfaceParams mIface;
+ // TODO: MacAddress-ify more of this class hierarchy.
private byte[] mHwAddr;
private PacketSocketAddress mInterfaceBroadcastAddr;
private int mTransactionId;
@@ -221,8 +222,9 @@ public class DhcpClient extends StateMachine {
return new WakeupMessage(mContext, getHandler(), cmdName, cmd);
}
+ // TODO: Take an InterfaceParams instance instead of an interface name String.
private DhcpClient(Context context, StateMachine controller, String iface) {
- super(TAG);
+ super(TAG, controller.getHandler());
mContext = context;
mController = controller;
@@ -262,23 +264,23 @@ public class DhcpClient extends StateMachine {
}
public static DhcpClient makeDhcpClient(
- Context context, StateMachine controller, String intf) {
- DhcpClient client = new DhcpClient(context, controller, intf);
+ Context context, StateMachine controller, InterfaceParams ifParams) {
+ DhcpClient client = new DhcpClient(context, controller, ifParams.name);
+ client.mIface = ifParams;
client.start();
return client;
}
private boolean initInterface() {
- try {
- mIface = NetworkInterface.getByName(mIfaceName);
- mHwAddr = mIface.getHardwareAddress();
- mInterfaceBroadcastAddr = new PacketSocketAddress(mIface.getIndex(),
- DhcpPacket.ETHER_BROADCAST);
- return true;
- } catch(SocketException | NullPointerException e) {
- Log.e(TAG, "Can't determine ifindex or MAC address for " + mIfaceName, e);
+ if (mIface == null) mIface = InterfaceParams.getByName(mIfaceName);
+ if (mIface == null) {
+ Log.e(TAG, "Can't determine InterfaceParams for " + mIfaceName);
return false;
}
+
+ mHwAddr = mIface.macAddr.toByteArray();
+ mInterfaceBroadcastAddr = new PacketSocketAddress(mIface.index, DhcpPacket.ETHER_BROADCAST);
+ return true;
}
private void startNewTransaction() {
@@ -293,7 +295,7 @@ public class DhcpClient extends StateMachine {
private boolean initPacketSocket() {
try {
mPacketSock = Os.socket(AF_PACKET, SOCK_RAW, ETH_P_IP);
- PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IP, mIface.getIndex());
+ PacketSocketAddress addr = new PacketSocketAddress((short) ETH_P_IP, mIface.index);
Os.bind(mPacketSock, addr);
NetworkUtils.attachDhcpFilter(mPacketSock);
} catch(SocketException|ErrnoException e) {
diff --git a/android/net/ip/ConnectivityPacketTracker.java b/android/net/ip/ConnectivityPacketTracker.java
index 6cf4fa9a..e6ddbbc9 100644
--- a/android/net/ip/ConnectivityPacketTracker.java
+++ b/android/net/ip/ConnectivityPacketTracker.java
@@ -21,6 +21,7 @@ import static android.system.OsConstants.*;
import android.net.NetworkUtils;
import android.net.util.PacketReader;
import android.net.util.ConnectivityPacketSummary;
+import android.net.util.InterfaceParams;
import android.os.Handler;
import android.system.ErrnoException;
import android.system.Os;
@@ -35,7 +36,6 @@ import libcore.util.HexEncoding;
import java.io.FileDescriptor;
import java.io.InterruptedIOException;
import java.io.IOException;
-import java.net.NetworkInterface;
import java.net.SocketException;
@@ -69,24 +69,12 @@ public class ConnectivityPacketTracker {
private boolean mRunning;
private String mDisplayName;
- public ConnectivityPacketTracker(Handler h, NetworkInterface netif, LocalLog log) {
- final String ifname;
- final int ifindex;
- final byte[] hwaddr;
- final int mtu;
-
- try {
- ifname = netif.getName();
- ifindex = netif.getIndex();
- hwaddr = netif.getHardwareAddress();
- mtu = netif.getMTU();
- } catch (NullPointerException|SocketException e) {
- throw new IllegalArgumentException("bad network interface", e);
- }
+ public ConnectivityPacketTracker(Handler h, InterfaceParams ifParams, LocalLog log) {
+ if (ifParams == null) throw new IllegalArgumentException("null InterfaceParams");
- mTag = TAG + "." + ifname;
+ mTag = TAG + "." + ifParams.name;
mLog = log;
- mPacketListener = new PacketListener(h, ifindex, hwaddr, mtu);
+ mPacketListener = new PacketListener(h, ifParams);
}
public void start(String displayName) {
@@ -102,13 +90,11 @@ public class ConnectivityPacketTracker {
}
private final class PacketListener extends PacketReader {
- private final int mIfIndex;
- private final byte mHwAddr[];
+ private final InterfaceParams mInterface;
- PacketListener(Handler h, int ifindex, byte[] hwaddr, int mtu) {
- super(h, mtu);
- mIfIndex = ifindex;
- mHwAddr = hwaddr;
+ PacketListener(Handler h, InterfaceParams ifParams) {
+ super(h, ifParams.defaultMtu);
+ mInterface = ifParams;
}
@Override
@@ -117,7 +103,7 @@ public class ConnectivityPacketTracker {
try {
s = Os.socket(AF_PACKET, SOCK_RAW, 0);
NetworkUtils.attachControlPacketFilter(s, ARPHRD_ETHER);
- Os.bind(s, new PacketSocketAddress((short) ETH_P_ALL, mIfIndex));
+ Os.bind(s, new PacketSocketAddress((short) ETH_P_ALL, mInterface.index));
} catch (ErrnoException | IOException e) {
logError("Failed to create packet tracking socket: ", e);
closeFd(s);
@@ -129,7 +115,7 @@ public class ConnectivityPacketTracker {
@Override
protected void handlePacket(byte[] recvbuf, int length) {
final String summary = ConnectivityPacketSummary.summarize(
- mHwAddr, recvbuf, length);
+ mInterface.macAddr, recvbuf, length);
if (summary == null) return;
if (DBG) Log.d(mTag, summary);
diff --git a/android/net/ip/IpClient.java b/android/net/ip/IpClient.java
index fdb366c5..d3a97b38 100644
--- a/android/net/ip/IpClient.java
+++ b/android/net/ip/IpClient.java
@@ -35,6 +35,7 @@ import android.net.apf.ApfFilter;
import android.net.dhcp.DhcpClient;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.IpManagerEvent;
+import android.net.util.InterfaceParams;
import android.net.util.MultinetworkPolicyTracker;
import android.net.util.NetdService;
import android.net.util.NetworkConstants;
@@ -63,7 +64,6 @@ import java.io.PrintWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
-import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Collection;
@@ -556,7 +556,7 @@ public class IpClient extends StateMachine {
private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
private final InterfaceController mInterfaceCtrl;
- private NetworkInterface mNetworkInterface;
+ private InterfaceParams mInterfaceParams;
/**
* Non-final member variables accessed only from within our StateMachine.
@@ -722,7 +722,12 @@ public class IpClient extends StateMachine {
return;
}
- getNetworkInterface();
+ mInterfaceParams = InterfaceParams.getByName(mInterfaceName);
+ if (mInterfaceParams == null) {
+ logError("Failed to find InterfaceParams for " + mInterfaceName);
+ // TODO: call doImmediateProvisioningFailure() with an error code
+ // indicating something like "interface not ready".
+ }
mCallback.setNeighborDiscoveryOffload(true);
sendMessage(CMD_START, new ProvisioningConfiguration(req));
@@ -858,7 +863,7 @@ public class IpClient extends StateMachine {
protected String getLogRecString(Message msg) {
final String logLine = String.format(
"%s/%d %d %d %s [%s]",
- mInterfaceName, mNetworkInterface == null ? -1 : mNetworkInterface.getIndex(),
+ mInterfaceName, (mInterfaceParams == null) ? -1 : mInterfaceParams.index,
msg.arg1, msg.arg2, Objects.toString(msg.obj), mMsgStateLogger);
final String richerLogLine = getWhatToString(msg.what) + " " + logLine;
@@ -889,15 +894,6 @@ public class IpClient extends StateMachine {
mLog.log(msg);
}
- private void getNetworkInterface() {
- try {
- mNetworkInterface = NetworkInterface.getByName(mInterfaceName);
- } catch (SocketException | NullPointerException e) {
- // TODO: throw new IllegalStateException.
- logError("Failed to get interface object: %s", e);
- }
- }
-
// This needs to be called with care to ensure that our LinkProperties
// are in sync with the actual LinkProperties of the interface. For example,
// we should only call this if we know for sure that there are no IP addresses
@@ -1218,7 +1214,7 @@ public class IpClient extends StateMachine {
}
} else {
// Start DHCPv4.
- mDhcpClient = DhcpClient.makeDhcpClient(mContext, IpClient.this, mInterfaceName);
+ mDhcpClient = DhcpClient.makeDhcpClient(mContext, IpClient.this, mInterfaceParams);
mDhcpClient.registerForPreDhcpNotification();
mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP);
}
@@ -1245,7 +1241,7 @@ public class IpClient extends StateMachine {
try {
mIpReachabilityMonitor = new IpReachabilityMonitor(
mContext,
- mInterfaceName,
+ mInterfaceParams,
getHandler(),
mLog,
new IpReachabilityMonitor.Callback() {
@@ -1447,7 +1443,7 @@ public class IpClient extends StateMachine {
mContext.getResources().getBoolean(R.bool.config_apfDrop802_3Frames);
apfConfig.ethTypeBlackList =
mContext.getResources().getIntArray(R.array.config_apfEthTypeBlackList);
- mApfFilter = ApfFilter.maybeCreate(apfConfig, mNetworkInterface, mCallback);
+ mApfFilter = ApfFilter.maybeCreate(apfConfig, mInterfaceParams, mCallback);
// TODO: investigate the effects of any multicast filtering racing/interfering with the
// rest of this IP configuration startup.
if (mApfFilter == null) {
@@ -1515,7 +1511,7 @@ public class IpClient extends StateMachine {
private ConnectivityPacketTracker createPacketTracker() {
try {
return new ConnectivityPacketTracker(
- getHandler(), mNetworkInterface, mConnectivityPacketLog);
+ getHandler(), mInterfaceParams, mConnectivityPacketLog);
} catch (IllegalArgumentException e) {
return null;
}
diff --git a/android/net/ip/IpNeighborMonitor.java b/android/net/ip/IpNeighborMonitor.java
index 68073347..fc07aa1e 100644
--- a/android/net/ip/IpNeighborMonitor.java
+++ b/android/net/ip/IpNeighborMonitor.java
@@ -16,7 +16,11 @@
package android.net.ip;
-import android.net.netlink.NetlinkConstants;
+import static android.net.netlink.NetlinkConstants.hexify;
+import static android.net.netlink.NetlinkConstants.RTM_DELNEIGH;
+import static android.net.netlink.NetlinkConstants.stringForNlMsgType;
+
+import android.net.MacAddress;
import android.net.netlink.NetlinkErrorMessage;
import android.net.netlink.NetlinkMessage;
import android.net.netlink.NetlinkSocket;
@@ -92,37 +96,35 @@ public class IpNeighborMonitor extends PacketReader {
final int ifindex;
final InetAddress ip;
final short nudState;
- final byte[] linkLayerAddr;
+ final MacAddress macAddr;
public NeighborEvent(long elapsedMs, short msgType, int ifindex, InetAddress ip,
- short nudState, byte[] linkLayerAddr) {
+ short nudState, MacAddress macAddr) {
this.elapsedMs = elapsedMs;
this.msgType = msgType;
this.ifindex = ifindex;
this.ip = ip;
this.nudState = nudState;
- this.linkLayerAddr = linkLayerAddr;
+ this.macAddr = macAddr;
}
boolean isConnected() {
- return (msgType != NetlinkConstants.RTM_DELNEIGH) &&
- StructNdMsg.isNudStateConnected(nudState);
+ return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateConnected(nudState);
}
boolean isValid() {
- return (msgType != NetlinkConstants.RTM_DELNEIGH) &&
- StructNdMsg.isNudStateValid(nudState);
+ return (msgType != RTM_DELNEIGH) && StructNdMsg.isNudStateValid(nudState);
}
@Override
public String toString() {
final StringJoiner j = new StringJoiner(",", "NeighborEvent{", "}");
return j.add("@" + elapsedMs)
- .add(NetlinkConstants.stringForNlMsgType(msgType))
+ .add(stringForNlMsgType(msgType))
.add("if=" + ifindex)
.add(ip.getHostAddress())
.add(StructNdMsg.stringForNudState(nudState))
- .add("[" + NetlinkConstants.hexify(linkLayerAddr) + "]")
+ .add("[" + macAddr + "]")
.toString();
}
}
@@ -183,7 +185,7 @@ public class IpNeighborMonitor extends PacketReader {
final NetlinkMessage nlMsg = NetlinkMessage.parse(byteBuffer);
if (nlMsg == null || nlMsg.getHeader() == null) {
byteBuffer.position(position);
- mLog.e("unparsable netlink msg: " + NetlinkConstants.hexify(byteBuffer));
+ mLog.e("unparsable netlink msg: " + hexify(byteBuffer));
break;
}
@@ -217,12 +219,13 @@ public class IpNeighborMonitor extends PacketReader {
final int ifindex = ndMsg.ndm_ifindex;
final InetAddress destination = neighMsg.getDestination();
final short nudState =
- (msgType == NetlinkConstants.RTM_DELNEIGH)
+ (msgType == RTM_DELNEIGH)
? StructNdMsg.NUD_NONE
: ndMsg.ndm_state;
final NeighborEvent event = new NeighborEvent(
- whenMs, msgType, ifindex, destination, nudState, neighMsg.getLinkLayerAddress());
+ whenMs, msgType, ifindex, destination, nudState,
+ getMacAddress(neighMsg.getLinkLayerAddress()));
if (VDBG) {
Log.d(TAG, neighMsg.toString());
@@ -233,4 +236,16 @@ public class IpNeighborMonitor extends PacketReader {
mConsumer.accept(event);
}
+
+ private static MacAddress getMacAddress(byte[] linkLayerAddress) {
+ if (linkLayerAddress != null) {
+ try {
+ return MacAddress.fromBytes(linkLayerAddress);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Failed to parse link-layer address: " + hexify(linkLayerAddress));
+ }
+ }
+
+ return null;
+ }
}
diff --git a/android/net/ip/IpReachabilityMonitor.java b/android/net/ip/IpReachabilityMonitor.java
index b31ffbba..7e02a288 100644
--- a/android/net/ip/IpReachabilityMonitor.java
+++ b/android/net/ip/IpReachabilityMonitor.java
@@ -26,6 +26,7 @@ import android.net.ip.IpNeighborMonitor.NeighborEvent;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.IpReachabilityEvent;
import android.net.netlink.StructNdMsg;
+import android.net.util.InterfaceParams;
import android.net.util.MultinetworkPolicyTracker;
import android.net.util.SharedLog;
import android.os.Handler;
@@ -46,9 +47,7 @@ import java.io.PrintWriter;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.InetSocketAddress;
-import java.net.NetworkInterface;
import java.net.SocketAddress;
-import java.net.SocketException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
@@ -168,8 +167,7 @@ public class IpReachabilityMonitor {
}
}
- private final String mInterfaceName;
- private final int mInterfaceIndex;
+ private final InterfaceParams mInterfaceParams;
private final IpNeighborMonitor mIpNeighborMonitor;
private final SharedLog mLog;
private final Callback mCallback;
@@ -182,30 +180,25 @@ public class IpReachabilityMonitor {
private volatile long mLastProbeTimeMs;
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, Handler h, SharedLog log, Callback callback,
+ Context context, InterfaceParams ifParams, Handler h, SharedLog log, Callback callback,
MultinetworkPolicyTracker tracker) {
- this(ifName, getInterfaceIndex(ifName), h, log, callback, tracker,
- Dependencies.makeDefault(context, ifName));
+ this(ifParams, h, log, callback, tracker, Dependencies.makeDefault(context, ifParams.name));
}
@VisibleForTesting
- IpReachabilityMonitor(String ifName, int ifIndex, Handler h, SharedLog log, Callback callback,
+ IpReachabilityMonitor(InterfaceParams ifParams, Handler h, SharedLog log, Callback callback,
MultinetworkPolicyTracker tracker, Dependencies dependencies) {
- mInterfaceName = ifName;
+ if (ifParams == null) throw new IllegalArgumentException("null InterfaceParams");
+
+ mInterfaceParams = ifParams;
mLog = log.forSubComponent(TAG);
mCallback = callback;
mMultinetworkPolicyTracker = tracker;
- mInterfaceIndex = ifIndex;
mDependencies = dependencies;
mIpNeighborMonitor = new IpNeighborMonitor(h, mLog,
(NeighborEvent event) -> {
- if (mInterfaceIndex != event.ifindex) return;
+ if (mInterfaceParams.index != event.ifindex) return;
if (!mNeighborWatchList.containsKey(event.ip)) return;
final NeighborEvent prev = mNeighborWatchList.put(event.ip, event);
@@ -241,7 +234,7 @@ public class IpReachabilityMonitor {
private String describeWatchList(String sep) {
final StringBuilder sb = new StringBuilder();
- sb.append("iface{" + mInterfaceName + "/" + mInterfaceIndex + "}," + sep);
+ sb.append("iface{" + mInterfaceParams + "}," + sep);
sb.append("ntable=[" + sep);
String delimiter = "";
for (Map.Entry<InetAddress, NeighborEvent> entry : mNeighborWatchList.entrySet()) {
@@ -262,10 +255,10 @@ public class IpReachabilityMonitor {
}
public void updateLinkProperties(LinkProperties lp) {
- if (!mInterfaceName.equals(lp.getInterfaceName())) {
+ if (!mInterfaceParams.name.equals(lp.getInterfaceName())) {
// TODO: figure out whether / how to cope with interface changes.
Log.wtf(TAG, "requested LinkProperties interface '" + lp.getInterfaceName() +
- "' does not match: " + mInterfaceName);
+ "' does not match: " + mInterfaceParams.name);
return;
}
@@ -353,10 +346,10 @@ public class IpReachabilityMonitor {
mDependencies.acquireWakeLock(getProbeWakeLockDuration());
}
- for (InetAddress target : ipProbeList) {
- final int rval = IpNeighborMonitor.startKernelNeighborProbe(mInterfaceIndex, target);
+ for (InetAddress ip : ipProbeList) {
+ final int rval = IpNeighborMonitor.startKernelNeighborProbe(mInterfaceParams.index, ip);
mLog.log(String.format("put neighbor %s into NUD_PROBE state (rval=%d)",
- target.getHostAddress(), rval));
+ ip.getHostAddress(), rval));
logEvent(IpReachabilityEvent.PROBE, rval);
}
mLastProbeTimeMs = SystemClock.elapsedRealtime();
@@ -378,22 +371,9 @@ public class IpReachabilityMonitor {
return (numUnicastProbes * retransTimeMs) + gracePeriodMs;
}
- private static int getInterfaceIndex(String ifname) {
- final NetworkInterface iface;
- try {
- iface = NetworkInterface.getByName(ifname);
- } catch (SocketException e) {
- throw new IllegalArgumentException("invalid interface '" + ifname + "': ", e);
- }
- if (iface == null) {
- throw new IllegalArgumentException("NetworkInterface was null for " + ifname);
- }
- return iface.getIndex();
- }
-
private void logEvent(int probeType, int errorCode) {
int eventType = probeType | (errorCode & 0xff);
- mMetricsLog.log(mInterfaceName, new IpReachabilityEvent(eventType));
+ mMetricsLog.log(mInterfaceParams.name, new IpReachabilityEvent(eventType));
}
private void logNudFailed(ProvisioningChange delta) {
@@ -401,6 +381,6 @@ public class IpReachabilityMonitor {
boolean isFromProbe = (duration < getProbeWakeLockDuration());
boolean isProvisioningLost = (delta == ProvisioningChange.LOST_PROVISIONING);
int eventType = IpReachabilityEvent.nudFailureEventType(isFromProbe, isProvisioningLost);
- mMetricsLog.log(mInterfaceName, new IpReachabilityEvent(eventType));
+ mMetricsLog.log(mInterfaceParams.name, new IpReachabilityEvent(eventType));
}
}
diff --git a/android/net/ip/RouterAdvertisementDaemon.java b/android/net/ip/RouterAdvertisementDaemon.java
index cb3123ce..49a1e79f 100644
--- a/android/net/ip/RouterAdvertisementDaemon.java
+++ b/android/net/ip/RouterAdvertisementDaemon.java
@@ -25,6 +25,7 @@ import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.NetworkUtils;
import android.net.TrafficStats;
+import android.net.util.InterfaceParams;
import android.system.ErrnoException;
import android.system.Os;
import android.system.StructGroupReq;
@@ -96,9 +97,7 @@ public class RouterAdvertisementDaemon {
(byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
};
- private final String mIfName;
- private final int mIfIndex;
- private final byte[] mHwAddr;
+ private final InterfaceParams mInterface;
private final InetSocketAddress mAllNodes;
// This lock is to protect the RA from being updated while being
@@ -223,11 +222,9 @@ public class RouterAdvertisementDaemon {
}
- public RouterAdvertisementDaemon(String ifname, int ifindex, byte[] hwaddr) {
- mIfName = ifname;
- mIfIndex = ifindex;
- mHwAddr = hwaddr;
- mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mIfIndex), 0);
+ public RouterAdvertisementDaemon(InterfaceParams ifParams) {
+ mInterface = ifParams;
+ mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0);
mDeprecatedInfoTracker = new DeprecatedInfoTracker();
}
@@ -279,7 +276,7 @@ public class RouterAdvertisementDaemon {
try {
putHeader(ra, mRaParams != null && mRaParams.hasDefaultRoute);
- putSlla(ra, mHwAddr);
+ putSlla(ra, mInterface.macAddr.toByteArray());
mRaLength = ra.position();
// https://tools.ietf.org/html/rfc5175#section-4 says:
@@ -579,9 +576,9 @@ public class RouterAdvertisementDaemon {
// Setting SNDTIMEO is purely for defensive purposes.
Os.setsockoptTimeval(
mSocket, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(SEND_TIMEOUT_MS));
- Os.setsockoptIfreq(mSocket, SOL_SOCKET, SO_BINDTODEVICE, mIfName);
+ Os.setsockoptIfreq(mSocket, SOL_SOCKET, SO_BINDTODEVICE, mInterface.name);
NetworkUtils.protectFromVpn(mSocket);
- NetworkUtils.setupRaSocket(mSocket, mIfIndex);
+ NetworkUtils.setupRaSocket(mSocket, mInterface.index);
} catch (ErrnoException | IOException e) {
Log.e(TAG, "Failed to create RA daemon socket: " + e);
return false;
@@ -614,7 +611,7 @@ public class RouterAdvertisementDaemon {
final InetAddress destip = dest.getAddress();
return (destip instanceof Inet6Address) &&
destip.isLinkLocalAddress() &&
- (((Inet6Address) destip).getScopeId() == mIfIndex);
+ (((Inet6Address) destip).getScopeId() == mInterface.index);
}
private void maybeSendRA(InetSocketAddress dest) {
diff --git a/android/net/metrics/WakeupStats.java b/android/net/metrics/WakeupStats.java
index 7277ba34..bb36536f 100644
--- a/android/net/metrics/WakeupStats.java
+++ b/android/net/metrics/WakeupStats.java
@@ -80,7 +80,7 @@ public class WakeupStats {
break;
}
- switch (ev.dstHwAddr.addressType()) {
+ switch (ev.dstHwAddr.getAddressType()) {
case MacAddress.TYPE_UNICAST:
l2UnicastCount++;
break;
diff --git a/android/net/util/ConnectivityPacketSummary.java b/android/net/util/ConnectivityPacketSummary.java
index dae93afb..4951400e 100644
--- a/android/net/util/ConnectivityPacketSummary.java
+++ b/android/net/util/ConnectivityPacketSummary.java
@@ -17,6 +17,7 @@
package android.net.util;
import android.net.dhcp.DhcpPacket;
+import android.net.MacAddress;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -45,21 +46,20 @@ public class ConnectivityPacketSummary {
private final ByteBuffer mPacket;
private final String mSummary;
- public static String summarize(byte[] hwaddr, byte[] buffer) {
+ public static String summarize(MacAddress hwaddr, byte[] buffer) {
return summarize(hwaddr, buffer, buffer.length);
}
// Methods called herein perform some but by no means all error checking.
// They may throw runtime exceptions on malformed packets.
- public static String summarize(byte[] hwaddr, byte[] buffer, int length) {
- if ((hwaddr == null) || (hwaddr.length != ETHER_ADDR_LEN)) return null;
- if (buffer == null) return null;
+ public static String summarize(MacAddress macAddr, byte[] buffer, int length) {
+ if ((macAddr == null) || (buffer == null)) return null;
length = Math.min(length, buffer.length);
- return (new ConnectivityPacketSummary(hwaddr, buffer, length)).toString();
+ return (new ConnectivityPacketSummary(macAddr, buffer, length)).toString();
}
- private ConnectivityPacketSummary(byte[] hwaddr, byte[] buffer, int length) {
- mHwAddr = hwaddr;
+ private ConnectivityPacketSummary(MacAddress macAddr, byte[] buffer, int length) {
+ mHwAddr = macAddr.toByteArray();
mBytes = buffer;
mLength = Math.min(length, mBytes.length);
mPacket = ByteBuffer.wrap(mBytes, 0, mLength);
diff --git a/android/net/util/InterfaceParams.java b/android/net/util/InterfaceParams.java
new file mode 100644
index 00000000..a4b2fbb6
--- /dev/null
+++ b/android/net/util/InterfaceParams.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.net.util;
+
+import static android.net.MacAddress.ALL_ZEROS_ADDRESS;
+import static android.net.util.NetworkConstants.ETHER_MTU;
+import static android.net.util.NetworkConstants.IPV6_MIN_MTU;
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import android.net.MacAddress;
+import android.text.TextUtils;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
+
+
+/**
+ * Encapsulate the interface parameters common to IpClient/IpServer components.
+ *
+ * Basically all java.net.NetworkInterface methods throw Exceptions. IpClient
+ * and IpServer (sub)components need most or all of this information at some
+ * point during their lifecycles, so pass only this simplified object around
+ * which can be created once when IpClient/IpServer are told to start.
+ *
+ * @hide
+ */
+public class InterfaceParams {
+ public final String name;
+ public final int index;
+ public final MacAddress macAddr;
+ public final int defaultMtu;
+
+ public static InterfaceParams getByName(String name) {
+ final NetworkInterface netif = getNetworkInterfaceByName(name);
+ if (netif == null) return null;
+
+ // Not all interfaces have MAC addresses, e.g. rmnet_data0.
+ final MacAddress macAddr = getMacAddress(netif);
+
+ try {
+ return new InterfaceParams(name, netif.getIndex(), macAddr, netif.getMTU());
+ } catch (IllegalArgumentException|SocketException e) {
+ return null;
+ }
+ }
+
+ public InterfaceParams(String name, int index, MacAddress macAddr) {
+ this(name, index, macAddr, ETHER_MTU);
+ }
+
+ public InterfaceParams(String name, int index, MacAddress macAddr, int defaultMtu) {
+ checkArgument((!TextUtils.isEmpty(name)), "impossible interface name");
+ checkArgument((index > 0), "invalid interface index");
+ this.name = name;
+ this.index = index;
+ this.macAddr = (macAddr != null) ? macAddr : ALL_ZEROS_ADDRESS;
+ this.defaultMtu = (defaultMtu > IPV6_MIN_MTU) ? defaultMtu : IPV6_MIN_MTU;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("%s/%d/%s/%d", name, index, macAddr, defaultMtu);
+ }
+
+ private static NetworkInterface getNetworkInterfaceByName(String name) {
+ try {
+ return NetworkInterface.getByName(name);
+ } catch (NullPointerException|SocketException e) {
+ return null;
+ }
+ }
+
+ private static MacAddress getMacAddress(NetworkInterface netif) {
+ try {
+ return MacAddress.fromBytes(netif.getHardwareAddress());
+ } catch (IllegalArgumentException|NullPointerException|SocketException e) {
+ return null;
+ }
+ }
+}
diff --git a/android/net/util/MultinetworkPolicyTracker.java b/android/net/util/MultinetworkPolicyTracker.java
index 424e40d2..30c5cd98 100644
--- a/android/net/util/MultinetworkPolicyTracker.java
+++ b/android/net/util/MultinetworkPolicyTracker.java
@@ -122,6 +122,7 @@ public class MultinetworkPolicyTracker {
return mAvoidBadWifi;
}
+ // TODO: move this to MultipathPolicyTracker.
public int getMeteredMultipathPreference() {
return mMeteredMultipathPreference;
}
diff --git a/android/net/util/NetworkConstants.java b/android/net/util/NetworkConstants.java
index 5a3a8be9..984c9f81 100644
--- a/android/net/util/NetworkConstants.java
+++ b/android/net/util/NetworkConstants.java
@@ -121,6 +121,14 @@ public final class NetworkConstants {
public static final int ICMP_ECHO_DATA_OFFSET = 8;
/**
+ * ICMPv4 constants.
+ *
+ * See also:
+ * - https://tools.ietf.org/html/rfc792
+ */
+ public static final int ICMPV4_ECHO_REQUEST_TYPE = 8;
+
+ /**
* ICMPv6 constants.
*
* See also:
@@ -139,6 +147,8 @@ public final class NetworkConstants {
public static final int ICMPV6_ND_OPTION_TLLA = 2;
public static final int ICMPV6_ND_OPTION_MTU = 5;
+ public static final int ICMPV6_ECHO_REQUEST_TYPE = 128;
+
/**
* UDP constants.
*
@@ -157,6 +167,14 @@ public final class NetworkConstants {
public static final int DHCP4_CLIENT_PORT = 68;
/**
+ * DNS constants.
+ *
+ * See also:
+ * - https://tools.ietf.org/html/rfc1035
+ */
+ public static final int DNS_SERVER_PORT = 53;
+
+ /**
* Utility functions.
*/
public static byte asByte(int i) { return (byte) i; }
diff --git a/android/net/wifi/ScanResult.java b/android/net/wifi/ScanResult.java
index b6ad9261..c46789ca 100644
--- a/android/net/wifi/ScanResult.java
+++ b/android/net/wifi/ScanResult.java
@@ -21,7 +21,9 @@ import android.os.Parcel;
import android.os.Parcelable;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
+import java.util.Objects;
/**
* Describes information about a detected access point. In addition
@@ -227,6 +229,50 @@ public class ScanResult implements Parcelable {
public long seen;
/**
+ * On devices with multiple hardware radio chains, this class provides metadata about
+ * each radio chain that was used to receive this scan result (probe response or beacon).
+ * {@hide}
+ */
+ public static class RadioChainInfo {
+ /** Vendor defined id for a radio chain. */
+ public int id;
+ /** Detected signal level in dBm (also known as the RSSI) on this radio chain. */
+ public int level;
+
+ @Override
+ public String toString() {
+ return "RadioChainInfo: id=" + id + ", level=" + level;
+ }
+
+ @Override
+ public boolean equals(Object otherObj) {
+ if (this == otherObj) {
+ return true;
+ }
+ if (!(otherObj instanceof RadioChainInfo)) {
+ return false;
+ }
+ RadioChainInfo other = (RadioChainInfo) otherObj;
+ return id == other.id && level == other.level;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, level);
+ }
+ };
+
+ /**
+ * Information about the list of the radio chains used to receive this scan result
+ * (probe response or beacon).
+ *
+ * For Example: On devices with 2 hardware radio chains, this list could hold 1 or 2
+ * entries based on whether this scan result was received using one or both the chains.
+ * {@hide}
+ */
+ public RadioChainInfo[] radioChainInfos;
+
+ /**
* @hide
* Update RSSI of the scan result
* @param previousRssi
@@ -248,18 +294,6 @@ public class ScanResult implements Parcelable {
}
/**
- * num IP configuration failures
- * @hide
- */
- public int numIpConfigFailures;
-
- /**
- * @hide
- * Last time we blacklisted the ScanResult
- */
- public long blackListTimestamp;
-
- /**
* Status indicating the scan result does not correspond to a user's saved configuration
* @hide
* @removed
@@ -268,12 +302,6 @@ public class ScanResult implements Parcelable {
public boolean untrusted;
/**
- * Number of time we connected to it
- * @hide
- */
- public int numConnection;
-
- /**
* Number of time autojoin used it
* @hide
*/
@@ -386,12 +414,6 @@ public class ScanResult implements Parcelable {
*/
public List<String> anqpLines;
- /**
- * @hide
- * storing the raw bytes of full result IEs
- **/
- public byte[] bytes;
-
/** information elements from beacon
* @hide
*/
@@ -481,6 +503,7 @@ public class ScanResult implements Parcelable {
this.isCarrierAp = false;
this.carrierApEapType = UNSPECIFIED;
this.carrierName = null;
+ this.radioChainInfos = null;
}
/** {@hide} */
@@ -502,6 +525,7 @@ public class ScanResult implements Parcelable {
this.isCarrierAp = false;
this.carrierApEapType = UNSPECIFIED;
this.carrierName = null;
+ this.radioChainInfos = null;
}
/** {@hide} */
@@ -530,6 +554,7 @@ public class ScanResult implements Parcelable {
this.isCarrierAp = false;
this.carrierApEapType = UNSPECIFIED;
this.carrierName = null;
+ this.radioChainInfos = null;
}
/** {@hide} */
@@ -563,15 +588,14 @@ public class ScanResult implements Parcelable {
distanceSdCm = source.distanceSdCm;
seen = source.seen;
untrusted = source.untrusted;
- numConnection = source.numConnection;
numUsage = source.numUsage;
- numIpConfigFailures = source.numIpConfigFailures;
venueName = source.venueName;
operatorFriendlyName = source.operatorFriendlyName;
flags = source.flags;
isCarrierAp = source.isCarrierAp;
carrierApEapType = source.carrierApEapType;
carrierName = source.carrierName;
+ radioChainInfos = source.radioChainInfos;
}
}
@@ -615,6 +639,7 @@ public class ScanResult implements Parcelable {
sb.append(", Carrier AP: ").append(isCarrierAp ? "yes" : "no");
sb.append(", Carrier AP EAP Type: ").append(carrierApEapType);
sb.append(", Carrier name: ").append(carrierName);
+ sb.append(", Radio Chain Infos: ").append(Arrays.toString(radioChainInfos));
return sb.toString();
}
@@ -646,9 +671,7 @@ public class ScanResult implements Parcelable {
dest.writeInt(centerFreq1);
dest.writeLong(seen);
dest.writeInt(untrusted ? 1 : 0);
- dest.writeInt(numConnection);
dest.writeInt(numUsage);
- dest.writeInt(numIpConfigFailures);
dest.writeString((venueName != null) ? venueName.toString() : "");
dest.writeString((operatorFriendlyName != null) ? operatorFriendlyName.toString() : "");
dest.writeLong(this.flags);
@@ -687,6 +710,16 @@ public class ScanResult implements Parcelable {
dest.writeInt(isCarrierAp ? 1 : 0);
dest.writeInt(carrierApEapType);
dest.writeString(carrierName);
+
+ if (radioChainInfos != null) {
+ dest.writeInt(radioChainInfos.length);
+ for (int i = 0; i < radioChainInfos.length; i++) {
+ dest.writeInt(radioChainInfos[i].id);
+ dest.writeInt(radioChainInfos[i].level);
+ }
+ } else {
+ dest.writeInt(0);
+ }
}
/** Implement the Parcelable interface {@hide} */
@@ -718,9 +751,7 @@ public class ScanResult implements Parcelable {
sr.seen = in.readLong();
sr.untrusted = in.readInt() != 0;
- sr.numConnection = in.readInt();
sr.numUsage = in.readInt();
- sr.numIpConfigFailures = in.readInt();
sr.venueName = in.readString();
sr.operatorFriendlyName = in.readString();
sr.flags = in.readLong();
@@ -759,6 +790,15 @@ public class ScanResult implements Parcelable {
sr.isCarrierAp = in.readInt() != 0;
sr.carrierApEapType = in.readInt();
sr.carrierName = in.readString();
+ n = in.readInt();
+ if (n != 0) {
+ sr.radioChainInfos = new RadioChainInfo[n];
+ for (int i = 0; i < n; i++) {
+ sr.radioChainInfos[i] = new RadioChainInfo();
+ sr.radioChainInfos[i].id = in.readInt();
+ sr.radioChainInfos[i].level = in.readInt();
+ }
+ }
return sr;
}
diff --git a/android/net/wifi/WifiActivityEnergyInfo.java b/android/net/wifi/WifiActivityEnergyInfo.java
index 29bf02ca..03c9fbee 100644
--- a/android/net/wifi/WifiActivityEnergyInfo.java
+++ b/android/net/wifi/WifiActivityEnergyInfo.java
@@ -56,6 +56,11 @@ public final class WifiActivityEnergyInfo implements Parcelable {
/**
* @hide
*/
+ public long mControllerScanTimeMs;
+
+ /**
+ * @hide
+ */
public long mControllerIdleTimeMs;
/**
@@ -69,13 +74,14 @@ public final class WifiActivityEnergyInfo implements Parcelable {
public static final int STACK_STATE_STATE_IDLE = 3;
public WifiActivityEnergyInfo(long timestamp, int stackState,
- long txTime, long[] txTimePerLevel, long rxTime, long idleTime,
- long energyUsed) {
+ long txTime, long[] txTimePerLevel, long rxTime, long scanTime,
+ long idleTime, long energyUsed) {
mTimestamp = timestamp;
mStackState = stackState;
mControllerTxTimeMs = txTime;
mControllerTxTimePerLevelMs = txTimePerLevel;
mControllerRxTimeMs = rxTime;
+ mControllerScanTimeMs = scanTime;
mControllerIdleTimeMs = idleTime;
mControllerEnergyUsed = energyUsed;
}
@@ -88,6 +94,7 @@ public final class WifiActivityEnergyInfo implements Parcelable {
+ " mControllerTxTimeMs=" + mControllerTxTimeMs
+ " mControllerTxTimePerLevelMs=" + Arrays.toString(mControllerTxTimePerLevelMs)
+ " mControllerRxTimeMs=" + mControllerRxTimeMs
+ + " mControllerScanTimeMs=" + mControllerScanTimeMs
+ " mControllerIdleTimeMs=" + mControllerIdleTimeMs
+ " mControllerEnergyUsed=" + mControllerEnergyUsed
+ " }";
@@ -101,10 +108,11 @@ public final class WifiActivityEnergyInfo implements Parcelable {
long txTime = in.readLong();
long[] txTimePerLevel = in.createLongArray();
long rxTime = in.readLong();
+ long scanTime = in.readLong();
long idleTime = in.readLong();
long energyUsed = in.readLong();
return new WifiActivityEnergyInfo(timestamp, stackState,
- txTime, txTimePerLevel, rxTime, idleTime, energyUsed);
+ txTime, txTimePerLevel, rxTime, scanTime, idleTime, energyUsed);
}
public WifiActivityEnergyInfo[] newArray(int size) {
return new WifiActivityEnergyInfo[size];
@@ -117,6 +125,7 @@ public final class WifiActivityEnergyInfo implements Parcelable {
out.writeLong(mControllerTxTimeMs);
out.writeLongArray(mControllerTxTimePerLevelMs);
out.writeLong(mControllerRxTimeMs);
+ out.writeLong(mControllerScanTimeMs);
out.writeLong(mControllerIdleTimeMs);
out.writeLong(mControllerEnergyUsed);
}
@@ -157,6 +166,13 @@ public final class WifiActivityEnergyInfo implements Parcelable {
}
/**
+ * @return scan time in ms
+ */
+ public long getControllerScanTimeMillis() {
+ return mControllerScanTimeMs;
+ }
+
+ /**
* @return idle time in ms
*/
public long getControllerIdleTimeMillis() {
@@ -183,6 +199,7 @@ public final class WifiActivityEnergyInfo implements Parcelable {
public boolean isValid() {
return ((mControllerTxTimeMs >=0) &&
(mControllerRxTimeMs >=0) &&
+ (mControllerScanTimeMs >=0) &&
(mControllerIdleTimeMs >=0));
}
-}
+} \ No newline at end of file
diff --git a/android/net/wifi/WifiConfiguration.java b/android/net/wifi/WifiConfiguration.java
index 6438631c..8d1a00b0 100644
--- a/android/net/wifi/WifiConfiguration.java
+++ b/android/net/wifi/WifiConfiguration.java
@@ -20,6 +20,7 @@ import android.annotation.SystemApi;
import android.content.pm.PackageManager;
import android.net.IpConfiguration;
import android.net.IpConfiguration.ProxySettings;
+import android.net.MacAddress;
import android.net.ProxyInfo;
import android.net.StaticIpConfiguration;
import android.net.Uri;
@@ -54,8 +55,10 @@ public class WifiConfiguration implements Parcelable {
/** {@hide} */
public static final String pskVarName = "psk";
/** {@hide} */
+ @Deprecated
public static final String[] wepKeyVarNames = { "wep_key0", "wep_key1", "wep_key2", "wep_key3" };
/** {@hide} */
+ @Deprecated
public static final String wepTxKeyIdxVarName = "wep_tx_keyidx";
/** {@hide} */
public static final String priorityVarName = "priority";
@@ -82,6 +85,9 @@ public class WifiConfiguration implements Parcelable {
/** WPA is not used; plaintext or static WEP could be used. */
public static final int NONE = 0;
/** WPA pre-shared key (requires {@code preSharedKey} to be specified). */
+ /** @deprecated Due to security and performance limitations, use of WPA-1 networks
+ * is discouraged. WPA-2 (RSN) should be used instead. */
+ @Deprecated
public static final int WPA_PSK = 1;
/** WPA using EAP authentication. Generally used with an external authentication server. */
public static final int WPA_EAP = 2;
@@ -115,8 +121,8 @@ public class WifiConfiguration implements Parcelable {
public static final String varName = "key_mgmt";
- public static final String[] strings = { "NONE", "WPA_PSK", "WPA_EAP", "IEEE8021X",
- "WPA2_PSK", "OSEN", "FT_PSK", "FT_EAP" };
+ public static final String[] strings = { "NONE", /* deprecated */ "WPA_PSK", "WPA_EAP",
+ "IEEE8021X", "WPA2_PSK", "OSEN", "FT_PSK", "FT_EAP" };
}
/**
@@ -125,7 +131,10 @@ public class WifiConfiguration implements Parcelable {
public static class Protocol {
private Protocol() { }
- /** WPA/IEEE 802.11i/D3.0 */
+ /** WPA/IEEE 802.11i/D3.0
+ * @deprecated Due to security and performance limitations, use of WPA-1 networks
+ * is discouraged. WPA-2 (RSN) should be used instead. */
+ @Deprecated
public static final int WPA = 0;
/** WPA2/IEEE 802.11i */
public static final int RSN = 1;
@@ -147,7 +156,10 @@ public class WifiConfiguration implements Parcelable {
/** Open System authentication (required for WPA/WPA2) */
public static final int OPEN = 0;
- /** Shared Key authentication (requires static WEP keys) */
+ /** Shared Key authentication (requires static WEP keys)
+ * @deprecated Due to security and performance limitations, use of WEP networks
+ * is discouraged. */
+ @Deprecated
public static final int SHARED = 1;
/** LEAP/Network EAP (only used with LEAP) */
public static final int LEAP = 2;
@@ -165,7 +177,10 @@ public class WifiConfiguration implements Parcelable {
/** Use only Group keys (deprecated) */
public static final int NONE = 0;
- /** Temporal Key Integrity Protocol [IEEE 802.11i/D7.0] */
+ /** Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]
+ * @deprecated Due to security and performance limitations, use of WPA-1 networks
+ * is discouraged. WPA-2 (RSN) should be used instead. */
+ @Deprecated
public static final int TKIP = 1;
/** AES in Counter mode with CBC-MAC [RFC 3610, IEEE 802.11i/D7.0] */
public static final int CCMP = 2;
@@ -187,9 +202,15 @@ public class WifiConfiguration implements Parcelable {
public static class GroupCipher {
private GroupCipher() { }
- /** WEP40 = WEP (Wired Equivalent Privacy) with 40-bit key (original 802.11) */
+ /** WEP40 = WEP (Wired Equivalent Privacy) with 40-bit key (original 802.11)
+ * @deprecated Due to security and performance limitations, use of WEP networks
+ * is discouraged. */
+ @Deprecated
public static final int WEP40 = 0;
- /** WEP104 = WEP (Wired Equivalent Privacy) with 104-bit key */
+ /** WEP104 = WEP (Wired Equivalent Privacy) with 104-bit key
+ * @deprecated Due to security and performance limitations, use of WEP networks
+ * is discouraged. */
+ @Deprecated
public static final int WEP104 = 1;
/** Temporal Key Integrity Protocol [IEEE 802.11i/D7.0] */
public static final int TKIP = 2;
@@ -203,7 +224,8 @@ public class WifiConfiguration implements Parcelable {
public static final String varName = "group";
public static final String[] strings =
- { "WEP40", "WEP104", "TKIP", "CCMP", "GTK_NOT_USED" };
+ { /* deprecated */ "WEP40", /* deprecated */ "WEP104",
+ "TKIP", "CCMP", "GTK_NOT_USED" };
}
/** Possible status of a network configuration. */
@@ -267,8 +289,15 @@ public class WifiConfiguration implements Parcelable {
public static final int AP_BAND_5GHZ = 1;
/**
+ * Device is allowed to choose the optimal band (2Ghz or 5Ghz) based on device capability,
+ * operating country code and current radio conditions.
+ * @hide
+ */
+ public static final int AP_BAND_ANY = -1;
+
+ /**
* The band which AP resides on
- * 0-2G 1-5G
+ * -1:Any 0:2G 1:5G
* By default, 2G is chosen
* @hide
*/
@@ -302,10 +331,16 @@ public class WifiConfiguration implements Parcelable {
* When the value of one of these keys is read, the actual key is
* not returned, just a "*" if the key has a value, or the null
* string otherwise.
+ * @deprecated Due to security and performance limitations, use of WEP networks
+ * is discouraged.
*/
+ @Deprecated
public String[] wepKeys;
- /** Default WEP key index, ranging from 0 to 3. */
+ /** Default WEP key index, ranging from 0 to 3.
+ * @deprecated Due to security and performance limitations, use of WEP networks
+ * is discouraged. */
+ @Deprecated
public int wepTxKeyIndex;
/**
@@ -845,6 +880,52 @@ public class WifiConfiguration implements Parcelable {
@SystemApi
public int numAssociation;
+ /**
+ * @hide
+ * Randomized MAC address to use with this particular network
+ */
+ private MacAddress mRandomizedMacAddress;
+
+ /**
+ * @hide
+ * Checks if the given MAC address can be used for Connected Mac Randomization
+ * by verifying that it is non-null, unicast, and locally assigned.
+ * @param mac MacAddress to check
+ * @return true if mac is good to use
+ */
+ private boolean isValidMacAddressForRandomization(MacAddress mac) {
+ return mac != null && !mac.isMulticastAddress() && mac.isLocallyAssigned();
+ }
+
+ /**
+ * @hide
+ * Returns Randomized MAC address to use with the network.
+ * If it is not set/valid, create a new randomized address.
+ */
+ public MacAddress getOrCreateRandomizedMacAddress() {
+ if (!isValidMacAddressForRandomization(mRandomizedMacAddress)) {
+ mRandomizedMacAddress = MacAddress.createRandomUnicastAddress();
+ }
+ return mRandomizedMacAddress;
+ }
+
+ /**
+ * @hide
+ * Returns MAC address set to be the local randomized MAC address.
+ * Does not guarantee that the returned address is valid for use.
+ */
+ public MacAddress getRandomizedMacAddress() {
+ return mRandomizedMacAddress;
+ }
+
+ /**
+ * @hide
+ * @param mac MacAddress to change into
+ */
+ public void setRandomizedMacAddress(MacAddress mac) {
+ mRandomizedMacAddress = mac;
+ }
+
/** @hide
* Boost given to RSSI on a home network for the purpose of calculating the score
* This adds stickiness to home networks, as defined by:
@@ -2117,6 +2198,7 @@ public class WifiConfiguration implements Parcelable {
updateTime = source.updateTime;
shared = source.shared;
recentFailure.setAssociationStatus(source.recentFailure.getAssociationStatus());
+ mRandomizedMacAddress = source.mRandomizedMacAddress;
}
}
@@ -2184,6 +2266,7 @@ public class WifiConfiguration implements Parcelable {
dest.writeInt(shared ? 1 : 0);
dest.writeString(mPasspointManagementObjectTree);
dest.writeInt(recentFailure.getAssociationStatus());
+ dest.writeParcelable(mRandomizedMacAddress, flags);
}
/** Implement the Parcelable interface {@hide} */
@@ -2252,6 +2335,7 @@ public class WifiConfiguration implements Parcelable {
config.shared = in.readInt() != 0;
config.mPasspointManagementObjectTree = in.readString();
config.recentFailure.setAssociationStatus(in.readInt());
+ config.mRandomizedMacAddress = in.readParcelable(null);
return config;
}
diff --git a/android/net/wifi/WifiConnectionStatistics.java b/android/net/wifi/WifiConnectionStatistics.java
deleted file mode 100644
index 1120c66e..00000000
--- a/android/net/wifi/WifiConnectionStatistics.java
+++ /dev/null
@@ -1,158 +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.annotation.SystemApi;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.text.TextUtils;
-
-import java.util.HashMap;
-
-/**
- * Wifi Connection Statistics: gather various stats regarding WiFi connections,
- * connection requests, auto-join
- * and WiFi usage.
- * @hide
- * @removed
- */
-@SystemApi
-public class WifiConnectionStatistics implements Parcelable {
- private static final String TAG = "WifiConnnectionStatistics";
-
- /**
- * history of past connection to untrusted SSID
- * Key = SSID
- * Value = num connection
- */
- public HashMap<String, WifiNetworkConnectionStatistics> untrustedNetworkHistory;
-
- // Number of time we polled the chip and were on 5GHz
- public int num5GhzConnected;
-
- // Number of time we polled the chip and were on 2.4GHz
- public int num24GhzConnected;
-
- // Number autojoin attempts
- public int numAutoJoinAttempt;
-
- // Number auto-roam attempts
- public int numAutoRoamAttempt;
-
- // Number wifimanager join attempts
- public int numWifiManagerJoinAttempt;
-
- public WifiConnectionStatistics() {
- untrustedNetworkHistory = new HashMap<String, WifiNetworkConnectionStatistics>();
- }
-
- public void incrementOrAddUntrusted(String SSID, int connection, int usage) {
- WifiNetworkConnectionStatistics stats;
- if (TextUtils.isEmpty(SSID))
- return;
- if (untrustedNetworkHistory.containsKey(SSID)) {
- stats = untrustedNetworkHistory.get(SSID);
- if (stats != null){
- stats.numConnection = connection + stats.numConnection;
- stats.numUsage = usage + stats.numUsage;
- }
- } else {
- stats = new WifiNetworkConnectionStatistics(connection, usage);
- }
- if (stats != null) {
- untrustedNetworkHistory.put(SSID, stats);
- }
- }
-
- @Override
- public String toString() {
- StringBuilder sbuf = new StringBuilder();
- sbuf.append("Connected on: 2.4Ghz=").append(num24GhzConnected);
- sbuf.append(" 5Ghz=").append(num5GhzConnected).append("\n");
- sbuf.append(" join=").append(numWifiManagerJoinAttempt);
- sbuf.append("\\").append(numAutoJoinAttempt).append("\n");
- sbuf.append(" roam=").append(numAutoRoamAttempt).append("\n");
-
- for (String Key : untrustedNetworkHistory.keySet()) {
- WifiNetworkConnectionStatistics stats = untrustedNetworkHistory.get(Key);
- if (stats != null) {
- sbuf.append(Key).append(" ").append(stats.toString()).append("\n");
- }
- }
- return sbuf.toString();
- }
-
- /** copy constructor*/
- public WifiConnectionStatistics(WifiConnectionStatistics source) {
- untrustedNetworkHistory = new HashMap<String, WifiNetworkConnectionStatistics>();
- if (source != null) {
- untrustedNetworkHistory.putAll(source.untrustedNetworkHistory);
- }
- }
-
- /** Implement the Parcelable interface */
- public int describeContents() {
- return 0;
- }
-
- /** Implement the Parcelable interface */
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(num24GhzConnected);
- dest.writeInt(num5GhzConnected);
- dest.writeInt(numAutoJoinAttempt);
- dest.writeInt(numAutoRoamAttempt);
- dest.writeInt(numWifiManagerJoinAttempt);
-
- dest.writeInt(untrustedNetworkHistory.size());
- for (String Key : untrustedNetworkHistory.keySet()) {
- WifiNetworkConnectionStatistics num = untrustedNetworkHistory.get(Key);
- dest.writeString(Key);
- dest.writeInt(num.numConnection);
- dest.writeInt(num.numUsage);
-
- }
- }
-
- /** Implement the Parcelable interface */
- public static final Creator<WifiConnectionStatistics> CREATOR =
- new Creator<WifiConnectionStatistics>() {
- public WifiConnectionStatistics createFromParcel(Parcel in) {
- WifiConnectionStatistics stats = new WifiConnectionStatistics();
- stats.num24GhzConnected = in.readInt();
- stats.num5GhzConnected = in.readInt();
- stats.numAutoJoinAttempt = in.readInt();
- stats.numAutoRoamAttempt = in.readInt();
- stats.numWifiManagerJoinAttempt = in.readInt();
- int n = in.readInt();
- while (n-- > 0) {
- String Key = in.readString();
- int numConnection = in.readInt();
- int numUsage = in.readInt();
- WifiNetworkConnectionStatistics st =
- new WifiNetworkConnectionStatistics(numConnection, numUsage);
- stats.untrustedNetworkHistory.put(Key, st);
- }
- return stats;
- }
-
- public WifiConnectionStatistics[] newArray(int size) {
- return new WifiConnectionStatistics[size];
- }
- };
-}
diff --git a/android/net/wifi/WifiManager.java b/android/net/wifi/WifiManager.java
index ea9be290..05dcb335 100644
--- a/android/net/wifi/WifiManager.java
+++ b/android/net/wifi/WifiManager.java
@@ -16,6 +16,7 @@
package android.net.wifi;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -55,6 +56,8 @@ import com.android.server.net.NetworkPinner;
import dalvik.system.CloseGuard;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.net.InetAddress;
import java.util.Collections;
@@ -432,6 +435,17 @@ public class WifiManager {
*/
public static final String EXTRA_WIFI_AP_MODE = "wifi_ap_mode";
+ /** @hide */
+ @IntDef(flag = false, prefix = { "WIFI_AP_STATE_" }, value = {
+ WIFI_AP_STATE_DISABLING,
+ WIFI_AP_STATE_DISABLED,
+ WIFI_AP_STATE_ENABLING,
+ WIFI_AP_STATE_ENABLED,
+ WIFI_AP_STATE_FAILED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface WifiApState {}
+
/**
* Wi-Fi AP is currently being disabled. The state will change to
* {@link #WIFI_AP_STATE_DISABLED} if it finishes successfully.
@@ -486,6 +500,14 @@ public class WifiManager {
@SystemApi
public static final int WIFI_AP_STATE_FAILED = 14;
+ /** @hide */
+ @IntDef(flag = false, prefix = { "SAP_START_FAILURE_" }, value = {
+ SAP_START_FAILURE_GENERAL,
+ SAP_START_FAILURE_NO_CHANNEL,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SapStartFailure {}
+
/**
* If WIFI AP start failed, this reason code means there is no legal channel exists on
* user selected band by regulatory
@@ -557,14 +579,9 @@ public class WifiManager {
public static final String EXTRA_SUPPLICANT_CONNECTED = "connected";
/**
* Broadcast intent action indicating that the state of Wi-Fi connectivity
- * has changed. One extra provides the new state
- * in the form of a {@link android.net.NetworkInfo} object. If the new
- * state is CONNECTED, additional extras may provide the BSSID and WifiInfo of
- * the access point.
- * as a {@code String}.
+ * has changed. An extra provides the new state
+ * in the form of a {@link android.net.NetworkInfo} object.
* @see #EXTRA_NETWORK_INFO
- * @see #EXTRA_BSSID
- * @see #EXTRA_WIFI_INFO
*/
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public static final String NETWORK_STATE_CHANGED_ACTION = "android.net.wifi.STATE_CHANGE";
@@ -576,17 +593,16 @@ public class WifiManager {
public static final String EXTRA_NETWORK_INFO = "networkInfo";
/**
* The lookup key for a String giving the BSSID of the access point to which
- * we are connected. Only present when the new state is CONNECTED.
- * Retrieve with
- * {@link android.content.Intent#getStringExtra(String)}.
+ * we are connected. No longer used.
*/
+ @Deprecated
public static final String EXTRA_BSSID = "bssid";
/**
* The lookup key for a {@link android.net.wifi.WifiInfo} object giving the
- * information about the access point to which we are connected. Only present
- * when the new state is CONNECTED. Retrieve with
- * {@link android.content.Intent#getParcelableExtra(String)}.
+ * information about the access point to which we are connected.
+ * No longer used.
*/
+ @Deprecated
public static final String EXTRA_WIFI_INFO = "wifiInfo";
/**
* Broadcast intent action indicating that the state of establishing a connection to
@@ -695,11 +711,11 @@ public class WifiManager {
* representing if the scan was successful or not.
* Scans may fail for multiple reasons, these may include:
* <ol>
- * <li>A non-privileged app requested too many scans in a certain period of time.
- * This may lead to additional scan request rejections via "scan throttling".
- * See
- * <a href="https://developer.android.com/preview/features/background-location-limits.html">
- * here</a> for details.
+ * <li>An app requested too many scans in a certain period of time.
+ * This may lead to additional scan request rejections via "scan throttling" for both
+ * foreground and background apps.
+ * Note: Apps holding android.Manifest.permission.NETWORK_SETTINGS permission are
+ * exempted from scan throttling.
* </li>
* <li>The device is idle and scanning is disabled.</li>
* <li>Wifi hardware reported a scan failure.</li>
@@ -1005,20 +1021,6 @@ public class WifiManager {
}
/**
- * @hide
- * @removed
- */
- @SystemApi
- @RequiresPermission(android.Manifest.permission.READ_WIFI_CREDENTIAL)
- public WifiConnectionStatistics getConnectionStatistics() {
- try {
- return mService.getConnectionStatistics();
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
- /**
* Returns a WifiConfiguration matching this ScanResult
*
* @param scanResult scanResult that represents the BSSID
@@ -1130,7 +1132,7 @@ public class WifiManager {
*/
private int addOrUpdateNetwork(WifiConfiguration config) {
try {
- return mService.addOrUpdateNetwork(config);
+ return mService.addOrUpdateNetwork(config, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1151,7 +1153,7 @@ public class WifiManager {
*/
public void addOrUpdatePasspointConfiguration(PasspointConfiguration config) {
try {
- if (!mService.addOrUpdatePasspointConfiguration(config)) {
+ if (!mService.addOrUpdatePasspointConfiguration(config, mContext.getOpPackageName())) {
throw new IllegalArgumentException();
}
} catch (RemoteException e) {
@@ -1168,7 +1170,7 @@ public class WifiManager {
*/
public void removePasspointConfiguration(String fqdn) {
try {
- if (!mService.removePasspointConfiguration(fqdn)) {
+ if (!mService.removePasspointConfiguration(fqdn, mContext.getOpPackageName())) {
throw new IllegalArgumentException();
}
} catch (RemoteException e) {
@@ -1254,7 +1256,7 @@ public class WifiManager {
*/
public boolean removeNetwork(int netId) {
try {
- return mService.removeNetwork(netId);
+ return mService.removeNetwork(netId, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1300,7 +1302,7 @@ public class WifiManager {
boolean success;
try {
- success = mService.enableNetwork(netId, attemptConnect);
+ success = mService.enableNetwork(netId, attemptConnect, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1326,7 +1328,7 @@ public class WifiManager {
*/
public boolean disableNetwork(int netId) {
try {
- return mService.disableNetwork(netId);
+ return mService.disableNetwork(netId, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -1339,7 +1341,7 @@ public class WifiManager {
*/
public boolean disconnect() {
try {
- mService.disconnect();
+ mService.disconnect(mContext.getOpPackageName());
return true;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -1354,7 +1356,7 @@ public class WifiManager {
*/
public boolean reconnect() {
try {
- mService.reconnect();
+ mService.reconnect(mContext.getOpPackageName());
return true;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -1369,7 +1371,7 @@ public class WifiManager {
*/
public boolean reassociate() {
try {
- mService.reassociate();
+ mService.reassociate(mContext.getOpPackageName());
return true;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -1596,7 +1598,10 @@ public class WifiManager {
* {@code ((WifiManager) getSystemService(WIFI_SERVICE)).getScanResults()}</li>
* </ol>
* @return {@code true} if the operation succeeded, i.e., the scan was initiated.
+ * @deprecated The ability for apps to trigger scan requests will be removed in a future
+ * release.
*/
+ @Deprecated
public boolean startScan() {
return startScan(null);
}
@@ -1615,58 +1620,12 @@ public class WifiManager {
}
/**
- * startLocationRestrictedScan()
- * Trigger a scan which will not make use of DFS channels and is thus not suitable for
- * establishing wifi connection.
- * @deprecated This API is nolonger supported.
- * Use {@link android.net.wifi.WifiScanner} API
- * @hide
- * @removed
- */
- @Deprecated
- @SystemApi
- @SuppressLint("Doclava125")
- public boolean startLocationRestrictedScan(WorkSource workSource) {
- return false;
- }
-
- /**
- * Check if the Batched Scan feature is supported.
- *
- * @return false if not supported.
- * @deprecated This API is nolonger supported.
- * Use {@link android.net.wifi.WifiScanner} API
- * @hide
- * @removed
- */
- @Deprecated
- @SystemApi
- @SuppressLint("Doclava125")
- public boolean isBatchedScanSupported() {
- return false;
- }
-
- /**
- * Retrieve the latest batched scan result. This should be called immediately after
- * {@link BATCHED_SCAN_RESULTS_AVAILABLE_ACTION} is received.
- * @deprecated This API is nolonger supported.
- * Use {@link android.net.wifi.WifiScanner} API
- * @hide
- * @removed
- */
- @Deprecated
- @SystemApi
- @SuppressLint("Doclava125")
- public List<BatchedScanResult> getBatchedScanResults() {
- return null;
- }
-
- /**
* Creates a configuration token describing the current network of MIME type
* application/vnd.wfa.wsc. Can be used to configure WiFi networks via NFC.
*
* @return hex-string encoded configuration token or null if there is no current network
* @hide
+ * @deprecated This API is deprecated
*/
public String getCurrentNetworkWpsNfcConfigurationToken() {
try {
@@ -1742,7 +1701,7 @@ public class WifiManager {
@Deprecated
public boolean saveConfiguration() {
try {
- return mService.saveConfiguration();
+ return mService.saveConfiguration(mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -2177,7 +2136,7 @@ public class WifiManager {
@RequiresPermission(android.Manifest.permission.CHANGE_WIFI_STATE)
public boolean setWifiApConfiguration(WifiConfiguration wifiConfig) {
try {
- mService.setWifiApConfiguration(wifiConfig);
+ mService.setWifiApConfiguration(wifiConfig, mContext.getOpPackageName());
return true;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
@@ -2252,20 +2211,34 @@ public class WifiManager {
/** @hide */
public static final int SAVE_NETWORK_SUCCEEDED = BASE + 9;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int START_WPS = BASE + 10;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int START_WPS_SUCCEEDED = BASE + 11;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int WPS_FAILED = BASE + 12;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int WPS_COMPLETED = BASE + 13;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int CANCEL_WPS = BASE + 14;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int CANCEL_WPS_FAILED = BASE + 15;
- /** @hide */
+ /** @hide
+ * @deprecated This is deprecated
+ */
public static final int CANCEL_WPS_SUCCEDED = BASE + 16;
/** @hide */
@@ -2305,15 +2278,25 @@ public class WifiManager {
public static final int BUSY = 2;
/* WPS specific errors */
- /** WPS overlap detected */
+ /** WPS overlap detected
+ * @deprecated This is deprecated
+ */
public static final int WPS_OVERLAP_ERROR = 3;
- /** WEP on WPS is prohibited */
+ /** WEP on WPS is prohibited
+ * @deprecated This is deprecated
+ */
public static final int WPS_WEP_PROHIBITED = 4;
- /** TKIP only prohibited */
+ /** TKIP only prohibited
+ * @deprecated This is deprecated
+ */
public static final int WPS_TKIP_ONLY_PROHIBITED = 5;
- /** Authentication failure on WPS */
+ /** Authentication failure on WPS
+ * @deprecated This is deprecated
+ */
public static final int WPS_AUTH_FAILURE = 6;
- /** WPS timed out */
+ /** WPS timed out
+ * @deprecated This is deprecated
+ */
public static final int WPS_TIMED_OUT = 7;
/**
@@ -2354,12 +2337,19 @@ public class WifiManager {
public void onFailure(int reason);
}
- /** Interface for callback invocation on a start WPS action */
+ /** Interface for callback invocation on a start WPS action
+ * @deprecated This is deprecated
+ */
public static abstract class WpsCallback {
- /** WPS start succeeded */
+
+ /** WPS start succeeded
+ * @deprecated This API is deprecated
+ */
public abstract void onStarted(String pin);
- /** WPS operation completed successfully */
+ /** WPS operation completed successfully
+ * @deprecated This API is deprecated
+ */
public abstract void onSucceeded();
/**
@@ -2368,6 +2358,7 @@ public class WifiManager {
* {@link #WPS_TKIP_ONLY_PROHIBITED}, {@link #WPS_OVERLAP_ERROR},
* {@link #WPS_WEP_PROHIBITED}, {@link #WPS_TIMED_OUT} or {@link #WPS_AUTH_FAILURE}
* and some generic errors.
+ * @deprecated This API is deprecated
*/
public abstract void onFailed(int reason);
}
@@ -2388,6 +2379,119 @@ public class WifiManager {
}
/**
+ * Base class for soft AP callback. Should be extended by applications and set when calling
+ * {@link WifiManager#registerSoftApCallback(SoftApCallback, Handler)}.
+ *
+ * @hide
+ */
+ public interface SoftApCallback {
+ /**
+ * Called when soft AP state changes.
+ *
+ * @param state new new AP state. One of {@link #WIFI_AP_STATE_DISABLED},
+ * {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED},
+ * {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
+ * @param failureReason reason when in failed state. One of
+ * {@link #SAP_START_FAILURE_GENERAL}, {@link #SAP_START_FAILURE_NO_CHANNEL}
+ */
+ public abstract void onStateChanged(@WifiApState int state,
+ @SapStartFailure int failureReason);
+
+ /**
+ * Called when number of connected clients to soft AP changes.
+ *
+ * @param numClients number of connected clients
+ */
+ public abstract void onNumClientsChanged(int numClients);
+ }
+
+ /**
+ * Callback proxy for SoftApCallback objects.
+ *
+ * @hide
+ */
+ private static class SoftApCallbackProxy extends ISoftApCallback.Stub {
+ private final Handler mHandler;
+ private final SoftApCallback mCallback;
+
+ SoftApCallbackProxy(Looper looper, SoftApCallback callback) {
+ mHandler = new Handler(looper);
+ mCallback = callback;
+ }
+
+ @Override
+ public void onStateChanged(int state, int failureReason) throws RemoteException {
+ Log.v(TAG, "SoftApCallbackProxy: onStateChanged: state=" + state + ", failureReason=" +
+ failureReason);
+ mHandler.post(() -> {
+ mCallback.onStateChanged(state, failureReason);
+ });
+ }
+
+ @Override
+ public void onNumClientsChanged(int numClients) throws RemoteException {
+ Log.v(TAG, "SoftApCallbackProxy: onNumClientsChanged: numClients=" + numClients);
+ mHandler.post(() -> {
+ mCallback.onNumClientsChanged(numClients);
+ });
+ }
+ }
+
+ /**
+ * Registers a callback for Soft AP. See {@link SoftApCallback}. Caller will receive the current
+ * soft AP state and number of connected devices immediately after a successful call to this API
+ * via callback. Note that receiving an immediate WIFI_AP_STATE_FAILED value for soft AP state
+ * indicates that the latest attempt to start soft AP has failed. Caller can unregister a
+ * previously registered callback using {@link unregisterSoftApCallback}
+ * <p>
+ * Applications should have the
+ * {@link android.Manifest.permission#NETWORK_SETTINGS NETWORK_SETTINGS} permission. Callers
+ * without the permission will trigger a {@link java.lang.SecurityException}.
+ * <p>
+ *
+ * @param callback Callback for soft AP events
+ * @param handler The Handler on whose thread to execute the callbacks of the {@code callback}
+ * object. If null, then the application's main thread will be used.
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+ public void registerSoftApCallback(@NonNull SoftApCallback callback,
+ @Nullable Handler handler) {
+ if (callback == null) throw new IllegalArgumentException("callback cannot be null");
+ Log.v(TAG, "registerSoftApCallback: callback=" + callback + ", handler=" + handler);
+
+ Looper looper = (handler == null) ? mContext.getMainLooper() : handler.getLooper();
+ Binder binder = new Binder();
+ try {
+ mService.registerSoftApCallback(binder, new SoftApCallbackProxy(looper, callback),
+ callback.hashCode());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Allow callers to unregister a previously registered callback. After calling this method,
+ * applications will no longer receive soft AP events.
+ *
+ * @param callback Callback to unregister for soft AP events
+ *
+ * @hide
+ */
+ @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+ public void unregisterSoftApCallback(@NonNull SoftApCallback callback) {
+ if (callback == null) throw new IllegalArgumentException("callback cannot be null");
+ Log.v(TAG, "unregisterSoftApCallback: callback=" + callback);
+
+ try {
+ mService.unregisterSoftApCallback(callback.hashCode());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* LocalOnlyHotspotReservation that contains the {@link WifiConfiguration} for the active
* LocalOnlyHotspot request.
* <p>
@@ -2948,7 +3052,7 @@ public class WifiManager {
public void disableEphemeralNetwork(String SSID) {
if (SSID == null) throw new IllegalArgumentException("SSID cannot be null");
try {
- mService.disableEphemeralNetwork(SSID);
+ mService.disableEphemeralNetwork(SSID, mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -2961,6 +3065,7 @@ public class WifiManager {
* @param listener for callbacks on success or failure. Can be null.
* @throws IllegalStateException if the WifiManager instance needs to be
* initialized again
+ * @deprecated This API is deprecated
*/
public void startWps(WpsInfo config, WpsCallback listener) {
if (config == null) throw new IllegalArgumentException("config cannot be null");
@@ -2973,6 +3078,7 @@ public class WifiManager {
* @param listener for callbacks on success or failure. Can be null.
* @throws IllegalStateException if the WifiManager instance needs to be
* initialized again
+ * @deprecated This API is deprecated
*/
public void cancelWps(WpsCallback listener) {
getChannel().sendMessage(CANCEL_WPS, 0, putListener(listener));
@@ -2987,7 +3093,7 @@ public class WifiManager {
*/
public Messenger getWifiServiceMessenger() {
try {
- return mService.getWifiServiceMessenger();
+ return mService.getWifiServiceMessenger(mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -3123,7 +3229,7 @@ public class WifiManager {
public void setWorkSource(WorkSource ws) {
synchronized (mBinder) {
- if (ws != null && ws.size() == 0) {
+ if (ws != null && ws.isEmpty()) {
ws = null;
}
boolean changed = true;
@@ -3135,7 +3241,7 @@ public class WifiManager {
changed = mWorkSource != null;
mWorkSource = new WorkSource(ws);
} else {
- changed = mWorkSource.diff(ws);
+ changed = !mWorkSource.equals(ws);
if (changed) {
mWorkSource.set(ws);
}
@@ -3487,33 +3593,13 @@ public class WifiManager {
}
/**
- * Deprecated
- * Does nothing
- * @hide
- * @deprecated
- */
- public void setAllowScansWithTraffic(int enabled) {
- return;
- }
-
- /**
- * Deprecated
- * returns value for 'disabled'
- * @hide
- * @deprecated
- */
- public int getAllowScansWithTraffic() {
- return 0;
- }
-
- /**
* Resets all wifi manager settings back to factory defaults.
*
* @hide
*/
public void factoryReset() {
try {
- mService.factoryReset();
+ mService.factoryReset(mContext.getOpPackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/android/net/wifi/aware/DiscoverySessionCallback.java b/android/net/wifi/aware/DiscoverySessionCallback.java
index aa2c268c..2052f155 100644
--- a/android/net/wifi/aware/DiscoverySessionCallback.java
+++ b/android/net/wifi/aware/DiscoverySessionCallback.java
@@ -131,7 +131,6 @@ public class DiscoverySessionCallback {
* {@link SubscribeConfig#SUBSCRIBE_TYPE_ACTIVE} discovery sessions this
* is the subscriber's match filter.
* @param distanceMm The measured distance to the Publisher in mm.
- * @hide
*/
public void onServiceDiscoveredWithinRange(PeerHandle peerHandle,
byte[] serviceSpecificInfo, List<byte[]> matchFilter, int distanceMm) {
diff --git a/android/net/wifi/aware/PublishConfig.java b/android/net/wifi/aware/PublishConfig.java
index 7a5049d7..7a0250bf 100644
--- a/android/net/wifi/aware/PublishConfig.java
+++ b/android/net/wifi/aware/PublishConfig.java
@@ -376,8 +376,6 @@ public final class PublishConfig implements Parcelable {
*
* @return The builder to facilitate chaining
* {@code builder.setXXX(..).setXXX(..)}.
- *
- * @hide
*/
public Builder setRangingEnabled(boolean enable) {
mEnableRanging = enable;
diff --git a/android/net/wifi/aware/SubscribeConfig.java b/android/net/wifi/aware/SubscribeConfig.java
index 91f8e520..2eab76a1 100644
--- a/android/net/wifi/aware/SubscribeConfig.java
+++ b/android/net/wifi/aware/SubscribeConfig.java
@@ -435,8 +435,6 @@ public final class SubscribeConfig implements Parcelable {
*
* @return The builder to facilitate chaining
* {@code builder.setXXX(..).setXXX(..)}.
- *
- * @hide
*/
public Builder setMinDistanceMm(int minDistanceMm) {
mMinDistanceMm = minDistanceMm;
@@ -466,8 +464,6 @@ public final class SubscribeConfig implements Parcelable {
*
* @return The builder to facilitate chaining
* {@code builder.setXXX(..).setXXX(..)}.
- *
- * @hide
*/
public Builder setMaxDistanceMm(int maxDistanceMm) {
mMaxDistanceMm = maxDistanceMm;
diff --git a/android/net/wifi/aware/WifiAwareManager.java b/android/net/wifi/aware/WifiAwareManager.java
index d57d1524..2f0c3163 100644
--- a/android/net/wifi/aware/WifiAwareManager.java
+++ b/android/net/wifi/aware/WifiAwareManager.java
@@ -269,6 +269,10 @@ public class WifiAwareManager {
+ identityChangedListener);
}
+ if (attachCallback == null) {
+ throw new IllegalArgumentException("Null callback provided");
+ }
+
synchronized (mLock) {
Looper looper = (handler == null) ? Looper.getMainLooper() : handler.getLooper();
@@ -300,6 +304,10 @@ public class WifiAwareManager {
DiscoverySessionCallback callback) {
if (VDBG) Log.v(TAG, "publish(): clientId=" + clientId + ", config=" + publishConfig);
+ if (callback == null) {
+ throw new IllegalArgumentException("Null callback provided");
+ }
+
try {
mService.publish(mContext.getOpPackageName(), clientId, publishConfig,
new WifiAwareDiscoverySessionCallbackProxy(this, looper, true, callback,
@@ -333,6 +341,10 @@ public class WifiAwareManager {
}
}
+ if (callback == null) {
+ throw new IllegalArgumentException("Null callback provided");
+ }
+
try {
mService.subscribe(mContext.getOpPackageName(), clientId, subscribeConfig,
new WifiAwareDiscoverySessionCallbackProxy(this, looper, false, callback,
diff --git a/android/net/wifi/rtt/LocationCivic.java b/android/net/wifi/rtt/LocationCivic.java
new file mode 100644
index 00000000..610edb63
--- /dev/null
+++ b/android/net/wifi/rtt/LocationCivic.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.rtt;
+
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Location Civic Report (LCR).
+ * <p>
+ * The information matches the IEEE 802.11-2016 LCR report.
+ * <p>
+ * Note: depending on the mechanism by which this information is returned (i.e. the API which
+ * returns an instance of this class) it is possibly Self Reported (by the peer). In such a case
+ * the information is NOT validated - use with caution. Consider validating it with other sources
+ * of information before using it.
+ */
+public final class LocationCivic implements Parcelable {
+ private final byte[] mData;
+
+ /**
+ * Parse the raw LCR information element (byte array) and extract the LocationCivic structure.
+ *
+ * Note: any parsing errors or invalid/unexpected errors will result in a null being returned.
+ *
+ * @hide
+ */
+ @Nullable
+ public static LocationCivic parseInformationElement(byte id, byte[] data) {
+ // TODO
+ return null;
+ }
+
+ /** @hide */
+ public LocationCivic(byte[] data) {
+ mData = data;
+ }
+
+ /**
+ * Return the Location Civic data reported by the peer.
+ *
+ * @return An arbitrary location information.
+ */
+ public byte[] getData() {
+ return mData;
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByteArray(mData);
+ }
+
+ public static final Parcelable.Creator<LocationCivic> CREATOR =
+ new Parcelable.Creator<LocationCivic>() {
+ @Override
+ public LocationCivic[] newArray(int size) {
+ return new LocationCivic[size];
+ }
+
+ @Override
+ public LocationCivic createFromParcel(Parcel in) {
+ byte[] data = in.createByteArray();
+
+ return new LocationCivic(data);
+ }
+ };
+
+ /** @hide */
+ @Override
+ public String toString() {
+ return new StringBuilder("LCR: data=").append(Arrays.toString(mData)).toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof LocationCivic)) {
+ return false;
+ }
+
+ LocationCivic lhs = (LocationCivic) o;
+
+ return Arrays.equals(mData, lhs.mData);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mData);
+ }
+}
diff --git a/android/net/wifi/rtt/LocationConfigurationInformation.java b/android/net/wifi/rtt/LocationConfigurationInformation.java
new file mode 100644
index 00000000..8aba56aa
--- /dev/null
+++ b/android/net/wifi/rtt/LocationConfigurationInformation.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.rtt;
+
+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;
+import java.util.Objects;
+
+/**
+ * The Device Location Configuration Information (LCI) specifies the location information of a peer
+ * device (e.g. an Access Point).
+ * <p>
+ * The information matches the IEEE 802.11-2016 LCI report (Location configuration information
+ * report).
+ * <p>
+ * Note: depending on the mechanism by which this information is returned (i.e. the API which
+ * returns an instance of this class) it is possibly Self Reported (by the peer). In such a case
+ * the information is NOT validated - use with caution. Consider validating it with other sources
+ * of information before using it.
+ */
+public final class LocationConfigurationInformation implements Parcelable {
+ /** @hide */
+ @IntDef({
+ ALTITUDE_UNKNOWN, ALTITUDE_IN_METERS, ALTITUDE_IN_FLOORS })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AltitudeTypes {
+ }
+
+ /**
+ * Define an Altitude Type returned by {@link #getAltitudeType()}. Indicates that the location
+ * does not specify an altitude or altitude uncertainty. The corresponding methods,
+ * {@link #getAltitude()} and {@link #getAltitudeUncertainty()} are not valid and will throw
+ * an exception.
+ */
+ public static final int ALTITUDE_UNKNOWN = 0;
+
+ /**
+ * Define an Altitude Type returned by {@link #getAltitudeType()}. Indicates that the location
+ * specifies the altitude and altitude uncertainty in meters. The corresponding methods,
+ * {@link #getAltitude()} and {@link #getAltitudeUncertainty()} return a valid value in meters.
+ */
+ public static final int ALTITUDE_IN_METERS = 1;
+
+ /**
+ * Define an Altitude Type returned by {@link #getAltitudeType()}. Indicates that the
+ * location specifies the altitude in floors, and does not specify an altitude uncertainty.
+ * The {@link #getAltitude()} method returns valid value in floors, and the
+ * {@link #getAltitudeUncertainty()} method is not valid and will throw an exception.
+ */
+ public static final int ALTITUDE_IN_FLOORS = 2;
+
+ private final double mLatitude;
+ private final double mLatitudeUncertainty;
+ private final double mLongitude;
+ private final double mLongitudeUncertainty;
+ private final int mAltitudeType;
+ private final double mAltitude;
+ private final double mAltitudeUncertainty;
+
+ /**
+ * Parse the raw LCI information element (byte array) and extract the
+ * LocationConfigurationInformation structure.
+ *
+ * Note: any parsing errors or invalid/unexpected errors will result in a null being returned.
+ *
+ * @hide
+ */
+ @Nullable
+ public static LocationConfigurationInformation parseInformationElement(byte id, byte[] data) {
+ // TODO
+ return null;
+ }
+
+ /** @hide */
+ public LocationConfigurationInformation(double latitude, double latitudeUncertainty,
+ double longitude, double longitudeUncertainty, @AltitudeTypes int altitudeType,
+ double altitude, double altitudeUncertainty) {
+ mLatitude = latitude;
+ mLatitudeUncertainty = latitudeUncertainty;
+ mLongitude = longitude;
+ mLongitudeUncertainty = longitudeUncertainty;
+ mAltitudeType = altitudeType;
+ mAltitude = altitude;
+ mAltitudeUncertainty = altitudeUncertainty;
+ }
+
+ /**
+ * Get latitude in degrees. Values are per WGS 84 reference system. Valid values are between
+ * -90 and 90.
+ *
+ * @return Latitude in degrees.
+ */
+ public double getLatitude() {
+ return mLatitude;
+ }
+
+ /**
+ * Get the uncertainty of the latitude {@link #getLatitude()} in degrees. A value of 0 indicates
+ * an unknown uncertainty.
+ *
+ * @return Uncertainty of the latitude in degrees.
+ */
+ public double getLatitudeUncertainty() {
+ return mLatitudeUncertainty;
+ }
+
+ /**
+ * Get longitude in degrees. Values are per WGS 84 reference system. Valid values are between
+ * -180 and 180.
+ *
+ * @return Longitude in degrees.
+ */
+ public double getLongitude() {
+ return mLongitude;
+ }
+
+ /**
+ * Get the uncertainty of the longitude {@link #getLongitude()} ()} in degrees. A value of 0
+ * indicates an unknown uncertainty.
+ *
+ * @return Uncertainty of the longitude in degrees.
+ */
+ public double getLongitudeUncertainty() {
+ return mLongitudeUncertainty;
+ }
+
+ /**
+ * Specifies the type of the altitude measurement returned by {@link #getAltitude()} and
+ * {@link #getAltitudeUncertainty()}. The possible values are:
+ * <li>{@link #ALTITUDE_UNKNOWN}: The altitude and altitude uncertainty are not provided.
+ * <li>{@link #ALTITUDE_IN_METERS}: The altitude and altitude uncertainty are provided in
+ * meters. Values are per WGS 84 reference system.
+ * <li>{@link #ALTITUDE_IN_FLOORS}: The altitude is provided in floors, the altitude uncertainty
+ * is not provided.
+ *
+ * @return The type of the altitude and altitude uncertainty.
+ */
+ public @AltitudeTypes int getAltitudeType() {
+ return mAltitudeType;
+ }
+
+ /**
+ * The altitude is interpreted according to the {@link #getAltitudeType()}. The possible values
+ * are:
+ * <li>{@link #ALTITUDE_UNKNOWN}: The altitude is not provided - this method will throw an
+ * exception.
+ * <li>{@link #ALTITUDE_IN_METERS}: The altitude is provided in meters. Values are per WGS 84
+ * reference system.
+ * <li>{@link #ALTITUDE_IN_FLOORS}: The altitude is provided in floors.
+ *
+ * @return Altitude value whose meaning is specified by {@link #getAltitudeType()}.
+ */
+ public double getAltitude() {
+ if (mAltitudeType == ALTITUDE_UNKNOWN) {
+ throw new IllegalStateException(
+ "getAltitude(): invoked on an invalid type: getAltitudeType()==UNKNOWN");
+ }
+ return mAltitude;
+ }
+
+ /**
+ * Only valid if the the {@link #getAltitudeType()} is equal to {@link #ALTITUDE_IN_METERS} -
+ * otherwise this method will throw an exception.
+ * <p>
+ * Get the uncertainty of the altitude {@link #getAltitude()} in meters. A value of 0
+ * indicates an unknown uncertainty.
+ *
+ * @return Uncertainty of the altitude in meters.
+ */
+ public double getAltitudeUncertainty() {
+ if (mAltitudeType != ALTITUDE_IN_METERS) {
+ throw new IllegalStateException(
+ "getAltitude(): invoked on an invalid type: getAltitudeType()!=IN_METERS");
+ }
+ return mAltitudeUncertainty;
+ }
+
+ /** @hide */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /** @hide */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeDouble(mLatitude);
+ dest.writeDouble(mLatitudeUncertainty);
+ dest.writeDouble(mLongitude);
+ dest.writeDouble(mLongitudeUncertainty);
+ dest.writeInt(mAltitudeType);
+ dest.writeDouble(mAltitude);
+ dest.writeDouble(mAltitudeUncertainty);
+ }
+
+ public static final Creator<LocationConfigurationInformation> CREATOR =
+ new Creator<LocationConfigurationInformation>() {
+ @Override
+ public LocationConfigurationInformation[] newArray(int size) {
+ return new LocationConfigurationInformation[size];
+ }
+
+ @Override
+ public LocationConfigurationInformation createFromParcel(Parcel in) {
+ double latitude = in.readDouble();
+ double latitudeUnc = in.readDouble();
+ double longitude = in.readDouble();
+ double longitudeUnc = in.readDouble();
+ int altitudeType = in.readInt();
+ double altitude = in.readDouble();
+ double altitudeUnc = in.readDouble();
+
+ return new LocationConfigurationInformation(latitude, latitudeUnc, longitude,
+ longitudeUnc, altitudeType, altitude, altitudeUnc);
+ }
+ };
+
+ /** @hide */
+ @Override
+ public String toString() {
+ return new StringBuilder("LCI: latitude=").append(mLatitude).append(
+ ", latitudeUncertainty=").append(mLatitudeUncertainty).append(
+ ", longitude=").append(mLongitude).append(", longitudeUncertainty=").append(
+ mLongitudeUncertainty).append(", altitudeType=").append(mAltitudeType).append(
+ ", altitude=").append(mAltitude).append(", altitudeUncertainty=").append(
+ mAltitudeUncertainty).toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof LocationConfigurationInformation)) {
+ return false;
+ }
+
+ LocationConfigurationInformation lhs = (LocationConfigurationInformation) o;
+
+ return mLatitude == lhs.mLatitude && mLatitudeUncertainty == lhs.mLatitudeUncertainty
+ && mLongitude == lhs.mLongitude
+ && mLongitudeUncertainty == lhs.mLongitudeUncertainty
+ && mAltitudeType == lhs.mAltitudeType && mAltitude == lhs.mAltitude
+ && mAltitudeUncertainty == lhs.mAltitudeUncertainty;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mLatitude, mLatitudeUncertainty, mLongitude, mLongitudeUncertainty,
+ mAltitudeType, mAltitude, mAltitudeUncertainty);
+ }
+}
diff --git a/android/net/wifi/rtt/RangingRequest.java b/android/net/wifi/rtt/RangingRequest.java
index b4e3097a..32f21b9c 100644
--- a/android/net/wifi/rtt/RangingRequest.java
+++ b/android/net/wifi/rtt/RangingRequest.java
@@ -17,6 +17,7 @@
package android.net.wifi.rtt;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.net.MacAddress;
import android.net.wifi.ScanResult;
import android.net.wifi.aware.AttachCallback;
@@ -41,8 +42,6 @@ import java.util.StringJoiner;
* The ranging request is a batch request - specifying a set of devices (specified using
* {@link RangingRequest.Builder#addAccessPoint(ScanResult)} and
* {@link RangingRequest.Builder#addAccessPoints(List)}).
- *
- * @hide RTT_API
*/
public final class RangingRequest implements Parcelable {
private static final int MAX_PEERS = 10;
@@ -198,7 +197,7 @@ public final class RangingRequest implements Parcelable {
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()}.
@@ -206,8 +205,9 @@ public final class RangingRequest implements Parcelable {
* @param responder Information on the RTT Responder.
* @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
*
- * @hide (SystemApi)
+ * @hide
*/
+ @SystemApi
public Builder addResponder(@NonNull ResponderConfig responder) {
if (responder == null) {
throw new IllegalArgumentException("Null Responder!");
diff --git a/android/net/wifi/rtt/RangingResult.java b/android/net/wifi/rtt/RangingResult.java
index a380fae7..201833bf 100644
--- a/android/net/wifi/rtt/RangingResult.java
+++ b/android/net/wifi/rtt/RangingResult.java
@@ -18,6 +18,7 @@ package android.net.wifi.rtt;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.net.MacAddress;
import android.net.wifi.aware.PeerHandle;
import android.os.Handler;
@@ -36,8 +37,6 @@ import java.util.Objects;
* <p>
* A ranging result is the distance measurement result for a single device specified in the
* {@link RangingRequest}.
- *
- * @hide RTT_API
*/
public final class RangingResult implements Parcelable {
private static final String TAG = "RangingResult";
@@ -66,29 +65,37 @@ public final class RangingResult implements Parcelable {
private final int mDistanceMm;
private final int mDistanceStdDevMm;
private final int mRssi;
+ private final LocationConfigurationInformation mLci;
+ private final LocationCivic mLcr;
private final long mTimestamp;
/** @hide */
public RangingResult(@RangeResultStatus int status, @NonNull MacAddress mac, int distanceMm,
- int distanceStdDevMm, int rssi, long timestamp) {
+ int distanceStdDevMm, int rssi, LocationConfigurationInformation lci, LocationCivic lcr,
+ long timestamp) {
mStatus = status;
mMac = mac;
mPeerHandle = null;
mDistanceMm = distanceMm;
mDistanceStdDevMm = distanceStdDevMm;
mRssi = rssi;
+ mLci = lci;
+ mLcr = lcr;
mTimestamp = timestamp;
}
/** @hide */
public RangingResult(@RangeResultStatus int status, PeerHandle peerHandle, int distanceMm,
- int distanceStdDevMm, int rssi, long timestamp) {
+ int distanceStdDevMm, int rssi, LocationConfigurationInformation lci, LocationCivic lcr,
+ long timestamp) {
mStatus = status;
mMac = null;
mPeerHandle = peerHandle;
mDistanceMm = distanceMm;
mDistanceStdDevMm = distanceStdDevMm;
mRssi = rssi;
+ mLci = lci;
+ mLcr = lcr;
mTimestamp = timestamp;
}
@@ -108,6 +115,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.
*/
+ @Nullable
public MacAddress getMacAddress() {
return mMac;
}
@@ -119,7 +127,7 @@ public final class RangingResult implements Parcelable {
* <p>
* Will return a {@code null} for results corresponding to requests issued using a MAC address.
*/
- public PeerHandle getPeerHandle() {
+ @Nullable public PeerHandle getPeerHandle() {
return mPeerHandle;
}
@@ -169,6 +177,38 @@ public final class RangingResult implements Parcelable {
}
/**
+ * @return The Location Configuration Information (LCI) as self-reported by the peer.
+ * <p>
+ * Note: the information is NOT validated - use with caution. Consider validating it with
+ * other sources of information before using it.
+ */
+ @Nullable
+ public LocationConfigurationInformation getReportedLocationConfigurationInformation() {
+ if (mStatus != STATUS_SUCCESS) {
+ throw new IllegalStateException(
+ "getReportedLocationConfigurationInformation(): invoked on an invalid result: "
+ + "getStatus()=" + mStatus);
+ }
+ return mLci;
+ }
+
+ /**
+ * @return The Location Civic report (LCR) as self-reported by the peer.
+ * <p>
+ * Note: the information is NOT validated - use with caution. Consider validating it with
+ * other sources of information before using it.
+ */
+ @Nullable
+ public LocationCivic getReportedLocationCivic() {
+ if (mStatus != STATUS_SUCCESS) {
+ throw new IllegalStateException(
+ "getReportedLocationCivic(): invoked on an invalid result: getStatus()="
+ + mStatus);
+ }
+ return mLcr;
+ }
+
+ /**
* @return The timestamp, in us since boot, at which the ranging operation was performed.
* <p>
* Only valid if {@link #getStatus()} returns {@link #STATUS_SUCCESS}, otherwise will throw an
@@ -182,13 +222,11 @@ public final class RangingResult implements Parcelable {
return mTimestamp;
}
- /** @hide */
@Override
public int describeContents() {
return 0;
}
- /** @hide */
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(mStatus);
@@ -207,10 +245,21 @@ public final class RangingResult implements Parcelable {
dest.writeInt(mDistanceMm);
dest.writeInt(mDistanceStdDevMm);
dest.writeInt(mRssi);
+ if (mLci == null) {
+ dest.writeBoolean(false);
+ } else {
+ dest.writeBoolean(true);
+ mLci.writeToParcel(dest, flags);
+ }
+ if (mLcr == null) {
+ dest.writeBoolean(false);
+ } else {
+ dest.writeBoolean(true);
+ mLcr.writeToParcel(dest, flags);
+ }
dest.writeLong(mTimestamp);
}
- /** @hide */
public static final Creator<RangingResult> CREATOR = new Creator<RangingResult>() {
@Override
public RangingResult[] newArray(int size) {
@@ -233,13 +282,23 @@ public final class RangingResult implements Parcelable {
int distanceMm = in.readInt();
int distanceStdDevMm = in.readInt();
int rssi = in.readInt();
+ boolean lciPresent = in.readBoolean();
+ LocationConfigurationInformation lci = null;
+ if (lciPresent) {
+ lci = LocationConfigurationInformation.CREATOR.createFromParcel(in);
+ }
+ boolean lcrPresent = in.readBoolean();
+ LocationCivic lcr = null;
+ if (lcrPresent) {
+ lcr = LocationCivic.CREATOR.createFromParcel(in);
+ }
long timestamp = in.readLong();
if (peerHandlePresent) {
return new RangingResult(status, peerHandle, distanceMm, distanceStdDevMm, rssi,
- timestamp);
+ lci, lcr, timestamp);
} else {
return new RangingResult(status, mac, distanceMm, distanceStdDevMm, rssi,
- timestamp);
+ lci, lcr, timestamp);
}
}
};
@@ -251,8 +310,8 @@ public final class RangingResult implements Parcelable {
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();
+ ", rssi=").append(mRssi).append(", lci=").append(mLci).append(", lcr=").append(
+ mLcr).append(", timestamp=").append(mTimestamp).append("]").toString();
}
@Override
@@ -270,12 +329,13 @@ public final class RangingResult implements Parcelable {
return mStatus == lhs.mStatus && Objects.equals(mMac, lhs.mMac) && Objects.equals(
mPeerHandle, lhs.mPeerHandle) && mDistanceMm == lhs.mDistanceMm
&& mDistanceStdDevMm == lhs.mDistanceStdDevMm && mRssi == lhs.mRssi
+ && Objects.equals(mLci, lhs.mLci) && Objects.equals(mLcr, lhs.mLcr)
&& mTimestamp == lhs.mTimestamp;
}
@Override
public int hashCode() {
return Objects.hash(mStatus, mMac, mPeerHandle, mDistanceMm, mDistanceStdDevMm, mRssi,
- mTimestamp);
+ mLci, mLcr, mTimestamp);
}
}
diff --git a/android/net/wifi/rtt/RangingResultCallback.java b/android/net/wifi/rtt/RangingResultCallback.java
index c8aea3c4..9639dc80 100644
--- a/android/net/wifi/rtt/RangingResultCallback.java
+++ b/android/net/wifi/rtt/RangingResultCallback.java
@@ -17,6 +17,7 @@
package android.net.wifi.rtt;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.os.Handler;
import java.lang.annotation.Retention;
@@ -31,8 +32,6 @@ import java.util.List;
* peers then the {@link #onRangingResults(List)} will be called with the set of results (@link
* {@link RangingResult}, each of which has its own success/failure code
* {@link RangingResult#getStatus()}.
- *
- * @hide RTT_API
*/
public abstract class RangingResultCallback {
/** @hide */
@@ -68,5 +67,5 @@ public abstract class RangingResultCallback {
*
* @param results List of range measurements, one per requested device.
*/
- public abstract void onRangingResults(List<RangingResult> results);
+ public abstract void onRangingResults(@NonNull List<RangingResult> results);
}
diff --git a/android/net/wifi/rtt/ResponderConfig.java b/android/net/wifi/rtt/ResponderConfig.java
index c3e10074..fb723c59 100644
--- a/android/net/wifi/rtt/ResponderConfig.java
+++ b/android/net/wifi/rtt/ResponderConfig.java
@@ -18,6 +18,7 @@ package android.net.wifi.rtt;
import android.annotation.IntDef;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import android.net.MacAddress;
import android.net.wifi.ScanResult;
import android.net.wifi.aware.PeerHandle;
@@ -35,8 +36,9 @@ import java.util.Objects;
* A Responder configuration may be constructed from a {@link ScanResult} or manually (with the
* data obtained out-of-band from a peer).
*
- * @hide (@SystemApi)
+ * @hide
*/
+@SystemApi
public final class ResponderConfig implements Parcelable {
private static final int AWARE_BAND_2_DISCOVERY_CHANNEL = 2437;
@@ -290,7 +292,7 @@ public final class ResponderConfig implements Parcelable {
MacAddress macAddress = MacAddress.fromString(scanResult.BSSID);
int responderType = RESPONDER_AP;
boolean supports80211mc = scanResult.is80211mcResponder();
- int channelWidth = translcateScanResultChannelWidth(scanResult.channelWidth);
+ int channelWidth = translateScanResultChannelWidth(scanResult.channelWidth);
int frequency = scanResult.frequency;
int centerFreq0 = scanResult.centerFreq0;
int centerFreq1 = scanResult.centerFreq1;
@@ -454,7 +456,7 @@ public final class ResponderConfig implements Parcelable {
}
/** @hide */
- static int translcateScanResultChannelWidth(int scanResultChannelWidth) {
+ static int translateScanResultChannelWidth(int scanResultChannelWidth) {
switch (scanResultChannelWidth) {
case ScanResult.CHANNEL_WIDTH_20MHZ:
return CHANNEL_WIDTH_20MHZ;
@@ -468,7 +470,7 @@ public final class ResponderConfig implements Parcelable {
return CHANNEL_WIDTH_80MHZ_PLUS_MHZ;
default:
throw new IllegalArgumentException(
- "translcateScanResultChannelWidth: bad " + scanResultChannelWidth);
+ "translateScanResultChannelWidth: bad " + scanResultChannelWidth);
}
}
}
diff --git a/android/net/wifi/rtt/WifiRttManager.java b/android/net/wifi/rtt/WifiRttManager.java
index b4c690f4..ec6c46ec 100644
--- a/android/net/wifi/rtt/WifiRttManager.java
+++ b/android/net/wifi/rtt/WifiRttManager.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package android.net.wifi.rtt;
import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
@@ -5,6 +21,7 @@ import static android.Manifest.permission.ACCESS_WIFI_STATE;
import static android.Manifest.permission.CHANGE_WIFI_STATE;
import static android.Manifest.permission.LOCATION_HARDWARE;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
@@ -38,8 +55,6 @@ import java.util.List;
* changes in RTT usability register for the {@link #ACTION_WIFI_RTT_STATE_CHANGED}
* broadcast. Note that this broadcast is not sticky - you should register for it and then
* check the above API to avoid a race condition.
- *
- * @hide RTT_API
*/
@SystemService(Context.WIFI_RTT_RANGING_SERVICE)
public class WifiRttManager {
@@ -71,6 +86,8 @@ public class WifiRttManager {
* Returns the current status of RTT API: whether or not RTT is available. To track
* changes in the state of RTT API register for the
* {@link #ACTION_WIFI_RTT_STATE_CHANGED} broadcast.
+ * <p>Note: availability of RTT does not mean that the app can use the API. The app's
+ * permissions and platform Location Mode are validated at run-time.
*
* @return A boolean indicating whether the app can use the RTT API at this time (true) or
* not (false).
@@ -95,8 +112,8 @@ public class WifiRttManager {
* will be used.
*/
@RequiresPermission(allOf = {ACCESS_COARSE_LOCATION, CHANGE_WIFI_STATE, ACCESS_WIFI_STATE})
- public void startRanging(RangingRequest request, RangingResultCallback callback,
- @Nullable Handler handler) {
+ public void startRanging(@NonNull RangingRequest request,
+ @NonNull RangingResultCallback callback, @Nullable Handler handler) {
startRanging(null, request, callback, handler);
}
@@ -112,17 +129,22 @@ public class WifiRttManager {
* callback} object. If a null is provided then the application's main thread
* will be used.
*
- * @hide (@SystemApi)
+ * @hide
*/
+ @SystemApi
@RequiresPermission(allOf = {LOCATION_HARDWARE, ACCESS_COARSE_LOCATION, CHANGE_WIFI_STATE,
ACCESS_WIFI_STATE})
- public void startRanging(@Nullable WorkSource workSource, RangingRequest request,
- RangingResultCallback callback, @Nullable Handler handler) {
+ public void startRanging(@Nullable WorkSource workSource, @NonNull RangingRequest request,
+ @NonNull RangingResultCallback callback, @Nullable Handler handler) {
if (VDBG) {
Log.v(TAG, "startRanging: workSource=" + workSource + ", request=" + request
+ ", callback=" + callback + ", handler=" + handler);
}
+ if (callback == null) {
+ throw new IllegalArgumentException("Null callback provided");
+ }
+
Looper looper = (handler == null) ? Looper.getMainLooper() : handler.getLooper();
Binder binder = new Binder();
try {
@@ -139,10 +161,11 @@ public class WifiRttManager {
*
* @param workSource The work-sources of the requesters.
*
- * @hide (@SystemApi)
+ * @hide
*/
+ @SystemApi
@RequiresPermission(allOf = {LOCATION_HARDWARE})
- public void cancelRanging(WorkSource workSource) {
+ public void cancelRanging(@Nullable WorkSource workSource) {
if (VDBG) {
Log.v(TAG, "cancelRanging: workSource=" + workSource);
}
diff --git a/android/os/BatteryStats.java b/android/os/BatteryStats.java
index 1e847c59..03a8dba5 100644
--- a/android/os/BatteryStats.java
+++ b/android/os/BatteryStats.java
@@ -35,6 +35,7 @@ import android.util.proto.ProtoOutputStream;
import android.view.Display;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.location.gnssmetrics.GnssMetrics;
import com.android.internal.os.BatterySipper;
import com.android.internal.os.BatteryStatsHelper;
@@ -180,6 +181,11 @@ public abstract class BatteryStats implements Parcelable {
public static final int FOREGROUND_SERVICE = 22;
/**
+ * A constant indicating an aggregate wifi multicast timer
+ */
+ public static final int WIFI_AGGREGATE_MULTICAST_ENABLED = 23;
+
+ /**
* Include all of the data in the stats, including previously saved data.
*/
public static final int STATS_SINCE_CHARGED = 0;
@@ -230,8 +236,11 @@ public abstract class BatteryStats implements Parcelable {
* New in version 29:
* - Process states re-ordered. TOP_SLEEPING now below BACKGROUND. HEAVY_WEIGHT introduced.
* - CPU times per UID process state
+ * New in version 30:
+ * - Uid.PROCESS_STATE_FOREGROUND_SERVICE only tracks
+ * ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE.
*/
- static final int CHECKIN_VERSION = 29;
+ static final int CHECKIN_VERSION = 30;
/**
* Old version, we hit 9 and ran out of room, need to remove.
@@ -327,6 +336,9 @@ public abstract class BatteryStats implements Parcelable {
private final StringBuilder mFormatBuilder = new StringBuilder(32);
private final Formatter mFormatter = new Formatter(mFormatBuilder);
+ private static final String CELLULAR_CONTROLLER_NAME = "Cellular";
+ private static final String WIFI_CONTROLLER_NAME = "WiFi";
+
/**
* Indicates times spent by the uid at each cpu frequency in all process states.
*
@@ -404,6 +416,13 @@ public abstract class BatteryStats implements Parcelable {
/**
* @return a non-null {@link LongCounter} representing time spent (milliseconds) in the
+ * scan state.
+ */
+ public abstract LongCounter getScanTimeCounter();
+
+
+ /**
+ * @return a non-null {@link LongCounter} representing time spent (milliseconds) in the
* receive state.
*/
public abstract LongCounter getRxTimeCounter();
@@ -520,8 +539,8 @@ public abstract class BatteryStats implements Parcelable {
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.
+ } else if (procState == ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+ // State when app has put itself in the foreground.
return Uid.PROCESS_STATE_FOREGROUND_SERVICE;
} else if (procState <= ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND) {
// Persistent and other foreground states go here.
@@ -675,6 +694,14 @@ public abstract class BatteryStats implements Parcelable {
public abstract long[] getCpuFreqTimes(int which);
public abstract long[] getScreenOffCpuFreqTimes(int which);
+ /**
+ * Returns cpu active time of an uid.
+ */
+ public abstract long getCpuActiveTime();
+ /**
+ * Returns cpu times of an uid on each cluster
+ */
+ public abstract long[] getCpuClusterTimes();
/**
* Returns cpu times of an uid at a particular process state.
@@ -689,17 +716,17 @@ public abstract class BatteryStats implements Parcelable {
// total time a uid has had any processes running at all.
/**
- * Time this uid has any processes in the top state (or above such as persistent).
+ * Time this uid has any processes in the top state.
*/
public static final int PROCESS_STATE_TOP = 0;
/**
- * Time this uid has any process with a started out bound foreground service, but
+ * Time this uid has any process with a started foreground service, but
* none in the "top" state.
*/
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 1;
/**
* Time this uid has any process in an active foreground state, but none in the
- * "top sleeping" or better state.
+ * "foreground service" or better state. Persistent and other foreground states go here.
*/
public static final int PROCESS_STATE_FOREGROUND = 2;
/**
@@ -1492,6 +1519,10 @@ public abstract class BatteryStats implements Parcelable {
public static final int STATE2_WIFI_SIGNAL_STRENGTH_SHIFT = 4;
public static final int STATE2_WIFI_SIGNAL_STRENGTH_MASK =
0x7 << STATE2_WIFI_SIGNAL_STRENGTH_SHIFT;
+ // Values for NUM_GPS_SIGNAL_QUALITY_LEVELS
+ public static final int STATE2_GPS_SIGNAL_QUALITY_SHIFT = 7;
+ public static final int STATE2_GPS_SIGNAL_QUALITY_MASK =
+ 0x1 << STATE2_GPS_SIGNAL_QUALITY_SHIFT;
public static final int STATE2_POWER_SAVE_FLAG = 1<<31;
public static final int STATE2_VIDEO_ON_FLAG = 1<<30;
@@ -2080,6 +2111,23 @@ public abstract class BatteryStats implements Parcelable {
*/
public abstract int getNumConnectivityChange(int which);
+
+ /**
+ * Returns the time in microseconds that the phone has been running with
+ * the given GPS signal quality level
+ *
+ * {@hide}
+ */
+ public abstract long getGpsSignalQualityTime(int strengthBin,
+ long elapsedRealtimeUs, int which);
+
+ /**
+ * Returns the GPS battery drain in mA-ms
+ *
+ * {@hide}
+ */
+ public abstract long getGpsBatteryDrainMaMs();
+
/**
* Returns the time in microseconds that the phone has been on while the device was
* running on battery.
@@ -2304,6 +2352,9 @@ public abstract class BatteryStats implements Parcelable {
WIFI_SUPPL_STATE_NAMES, WIFI_SUPPL_STATE_SHORT_NAMES),
new BitDescription(HistoryItem.STATE2_CAMERA_FLAG, "camera", "ca"),
new BitDescription(HistoryItem.STATE2_BLUETOOTH_SCAN_FLAG, "ble_scan", "bles"),
+ new BitDescription(HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK,
+ HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT, "gps_signal_quality", "Gss",
+ new String[] { "poor", "good"}, new String[] { "poor", "good"}),
};
public static final String[] HISTORY_EVENT_NAMES = new String[] {
@@ -2334,6 +2385,22 @@ public abstract class BatteryStats implements Parcelable {
};
/**
+ * Returns total time for WiFi Multicast Wakelock timer.
+ * Note that this may be different from the sum of per uid timer values.
+ *
+ * {@hide}
+ */
+ public abstract long getWifiMulticastWakelockTime(long elapsedRealtimeUs, int which);
+
+ /**
+ * Returns total time for WiFi Multicast Wakelock timer
+ * Note that this may be different from the sum of per uid timer values.
+ *
+ * {@hide}
+ */
+ public abstract int getWifiMulticastWakelockCount(int which);
+
+ /**
* Returns the time in microseconds that wifi has been on while the device was
* running on battery.
*
@@ -2342,6 +2409,14 @@ public abstract class BatteryStats implements Parcelable {
public abstract long getWifiOnTime(long elapsedRealtimeUs, int which);
/**
+ * Returns the time in microseconds that wifi has been active while the device was
+ * running on battery.
+ *
+ * {@hide}
+ */
+ public abstract long getWifiActiveTime(long elapsedRealtimeUs, int which);
+
+ /**
* Returns the time in microseconds that wifi has been on and the driver has
* been in the running state while the device was running on battery.
*
@@ -3288,6 +3363,20 @@ public abstract class BatteryStats implements Parcelable {
final long sleepTimeMs
= totalControllerActivityTimeMs - (idleTimeMs + rxTimeMs + totalTxTimeMs);
+ if (controllerName.equals(WIFI_CONTROLLER_NAME)) {
+ final long scanTimeMs = counter.getScanTimeCounter().getCountLocked(which);
+ sb.setLength(0);
+ sb.append(prefix);
+ sb.append(" ");
+ sb.append(controllerName);
+ sb.append(" Scan time: ");
+ formatTimeMs(sb, scanTimeMs);
+ sb.append("(");
+ sb.append(formatRatioLocked(scanTimeMs, totalControllerActivityTimeMs));
+ sb.append(")");
+ pw.println(sb.toString());
+ }
+
sb.setLength(0);
sb.append(prefix);
sb.append(" ");
@@ -3329,7 +3418,7 @@ public abstract class BatteryStats implements Parcelable {
String [] powerLevel;
switch(controllerName) {
- case "Cellular":
+ case CELLULAR_CONTROLLER_NAME:
powerLevel = new String[] {
" less than 0dBm: ",
" 0dBm to 8dBm: ",
@@ -3442,16 +3531,13 @@ public abstract class BatteryStats implements Parcelable {
screenDozeTime / 1000);
- // Calculate both wakelock and wifi multicast wakelock times across all uids.
+ // Calculate wakelock times across all uids.
long fullWakeLockTimeTotal = 0;
long partialWakeLockTimeTotal = 0;
- long multicastWakeLockTimeTotalMicros = 0;
- int multicastWakeLockCountTotal = 0;
for (int iu = 0; iu < NU; iu++) {
final Uid u = uidStats.valueAt(iu);
- // First calculating the wakelock stats
final ArrayMap<String, ? extends BatteryStats.Uid.Wakelock> wakelocks
= u.getWakelockStats();
for (int iw=wakelocks.size()-1; iw>=0; iw--) {
@@ -3469,13 +3555,6 @@ public abstract class BatteryStats implements Parcelable {
rawRealtime, which);
}
}
-
- // Now calculating the wifi multicast wakelock stats
- final Timer mcTimer = u.getMulticastWakelockStats();
- if (mcTimer != null) {
- multicastWakeLockTimeTotalMicros += mcTimer.getTotalTimeLocked(rawRealtime, which);
- multicastWakeLockCountTotal += mcTimer.getCountLocked(which);
- }
}
// Dump network stats
@@ -3592,6 +3671,9 @@ public abstract class BatteryStats implements Parcelable {
dumpLine(pw, 0 /* uid */, category, WIFI_SIGNAL_STRENGTH_COUNT_DATA, args);
// Dump Multicast total stats
+ final long multicastWakeLockTimeTotalMicros =
+ getWifiMulticastWakelockTime(rawRealtime, which);
+ final int multicastWakeLockCountTotal = getWifiMulticastWakelockCount(which);
dumpLine(pw, 0 /* uid */, category, WIFI_MULTICAST_TOTAL_DATA,
multicastWakeLockTimeTotalMicros / 1000,
multicastWakeLockCountTotal);
@@ -4456,18 +4538,15 @@ public abstract class BatteryStats implements Parcelable {
pw.print(" Connectivity changes: "); pw.println(connChanges);
}
- // Calculate both wakelock and wifi multicast wakelock times across all uids.
+ // Calculate wakelock times across all uids.
long fullWakeLockTimeTotalMicros = 0;
long partialWakeLockTimeTotalMicros = 0;
- long multicastWakeLockTimeTotalMicros = 0;
- int multicastWakeLockCountTotal = 0;
final ArrayList<TimerEntry> timers = new ArrayList<>();
for (int iu = 0; iu < NU; iu++) {
final Uid u = uidStats.valueAt(iu);
- // First calculate wakelock statistics
final ArrayMap<String, ? extends BatteryStats.Uid.Wakelock> wakelocks
= u.getWakelockStats();
for (int iw=wakelocks.size()-1; iw>=0; iw--) {
@@ -4495,13 +4574,6 @@ public abstract class BatteryStats implements Parcelable {
}
}
}
-
- // Next calculate wifi multicast wakelock statistics
- final Timer mcTimer = u.getMulticastWakelockStats();
- if (mcTimer != null) {
- multicastWakeLockTimeTotalMicros += mcTimer.getTotalTimeLocked(rawRealtime, which);
- multicastWakeLockCountTotal += mcTimer.getCountLocked(which);
- }
}
final long mobileRxTotalBytes = getNetworkActivityBytes(NETWORK_MOBILE_RX_DATA, which);
@@ -4531,6 +4603,9 @@ public abstract class BatteryStats implements Parcelable {
pw.println(sb.toString());
}
+ final long multicastWakeLockTimeTotalMicros =
+ getWifiMulticastWakelockTime(rawRealtime, which);
+ final int multicastWakeLockCountTotal = getWifiMulticastWakelockCount(which);
if (multicastWakeLockTimeTotalMicros != 0) {
sb.setLength(0);
sb.append(prefix);
@@ -4631,7 +4706,7 @@ public abstract class BatteryStats implements Parcelable {
if (!didOne) sb.append(" (no activity)");
pw.println(sb.toString());
- printControllerActivity(pw, sb, prefix, "Cellular",
+ printControllerActivity(pw, sb, prefix, CELLULAR_CONTROLLER_NAME,
getModemControllerActivity(), which);
pw.print(prefix);
@@ -4640,6 +4715,16 @@ public abstract class BatteryStats implements Parcelable {
sb.append(" Wifi Statistics:");
pw.println(sb.toString());
+ pw.print(prefix);
+ sb.setLength(0);
+ sb.append(prefix);
+ sb.append(" Wifi kernel active time: ");
+ final long wifiActiveTime = getWifiActiveTime(rawRealtime, which);
+ formatTimeMs(sb, wifiActiveTime / 1000);
+ sb.append("("); sb.append(formatRatioLocked(wifiActiveTime, whichBatteryRealtime));
+ sb.append(")");
+ pw.println(sb.toString());
+
pw.print(" Wifi data received: "); pw.println(formatBytesLocked(wifiRxTotalBytes));
pw.print(" Wifi data sent: "); pw.println(formatBytesLocked(wifiTxTotalBytes));
pw.print(" Wifi packets received: "); pw.println(wifiRxTotalPackets);
@@ -4717,7 +4802,45 @@ public abstract class BatteryStats implements Parcelable {
if (!didOne) sb.append(" (no activity)");
pw.println(sb.toString());
- printControllerActivity(pw, sb, prefix, "WiFi", getWifiControllerActivity(), which);
+ printControllerActivity(pw, sb, prefix, WIFI_CONTROLLER_NAME,
+ getWifiControllerActivity(), which);
+
+ pw.print(prefix);
+ sb.setLength(0);
+ sb.append(prefix);
+ sb.append(" GPS Statistics:");
+ pw.println(sb.toString());
+
+ sb.setLength(0);
+ sb.append(prefix);
+ sb.append(" GPS signal quality (Top 4 Average CN0):");
+ final String[] gpsSignalQualityDescription = new String[]{
+ "poor (less than 20 dBHz): ",
+ "good (greater than 20 dBHz): "};
+ final int numGpsSignalQualityBins = Math.min(GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS,
+ gpsSignalQualityDescription.length);
+ for (int i=0; i<numGpsSignalQualityBins; i++) {
+ final long time = getGpsSignalQualityTime(i, rawRealtime, which);
+ sb.append("\n ");
+ sb.append(prefix);
+ sb.append(" ");
+ sb.append(gpsSignalQualityDescription[i]);
+ formatTimeMs(sb, time/1000);
+ sb.append("(");
+ sb.append(formatRatioLocked(time, whichBatteryRealtime));
+ sb.append(") ");
+ }
+ pw.println(sb.toString());
+
+ final long gpsBatteryDrainMaMs = getGpsBatteryDrainMaMs();
+ if (gpsBatteryDrainMaMs > 0) {
+ pw.print(prefix);
+ sb.setLength(0);
+ sb.append(prefix);
+ sb.append(" Battery Drain (mAh): ");
+ sb.append(Double.toString(((double) gpsBatteryDrainMaMs)/(3600 * 1000)));
+ pw.println(sb.toString());
+ }
pw.print(prefix);
sb.setLength(0);
@@ -5158,8 +5281,8 @@ public abstract class BatteryStats implements Parcelable {
pw.println(sb.toString());
}
- printControllerActivityIfInteresting(pw, sb, prefix + " ", "Modem",
- u.getModemControllerActivity(), which);
+ printControllerActivityIfInteresting(pw, sb, prefix + " ",
+ CELLULAR_CONTROLLER_NAME, u.getModemControllerActivity(), which);
if (wifiRxBytes > 0 || wifiTxBytes > 0 || wifiRxPackets > 0 || wifiTxPackets > 0) {
pw.print(prefix); pw.print(" Wi-Fi network: ");
@@ -5213,7 +5336,7 @@ public abstract class BatteryStats implements Parcelable {
pw.println(sb.toString());
}
- printControllerActivityIfInteresting(pw, sb, prefix + " ", "WiFi",
+ printControllerActivityIfInteresting(pw, sb, prefix + " ", WIFI_CONTROLLER_NAME,
u.getWifiControllerActivity(), which);
if (btRxBytes > 0 || btTxBytes > 0) {
@@ -7051,6 +7174,28 @@ public abstract class BatteryStats implements Parcelable {
}
}
}
+
+ for (int procState = 0; procState < Uid.NUM_PROCESS_STATE; ++procState) {
+ final long[] timesMs = u.getCpuFreqTimes(which, procState);
+ if (timesMs != null && timesMs.length == cpuFreqs.length) {
+ long[] screenOffTimesMs = u.getScreenOffCpuFreqTimes(which, procState);
+ if (screenOffTimesMs == null) {
+ screenOffTimesMs = new long[timesMs.length];
+ }
+ final long procToken = proto.start(UidProto.Cpu.BY_PROCESS_STATE);
+ proto.write(UidProto.Cpu.ByProcessState.PROCESS_STATE, procState);
+ for (int ic = 0; ic < timesMs.length; ++ic) {
+ long cToken = proto.start(UidProto.Cpu.ByProcessState.BY_FREQUENCY);
+ proto.write(UidProto.Cpu.ByFrequency.FREQUENCY_INDEX, ic + 1);
+ proto.write(UidProto.Cpu.ByFrequency.TOTAL_DURATION_MS,
+ timesMs[ic]);
+ proto.write(UidProto.Cpu.ByFrequency.SCREEN_OFF_DURATION_MS,
+ screenOffTimesMs[ic]);
+ proto.end(cToken);
+ }
+ proto.end(procToken);
+ }
+ }
proto.end(cpuToken);
// Flashlight (FLASHLIGHT_DATA)
@@ -7535,22 +7680,9 @@ public abstract class BatteryStats implements Parcelable {
proto.end(mToken);
// Wifi multicast wakelock total stats (WIFI_MULTICAST_WAKELOCK_TOTAL_DATA)
- // Calculate multicast wakelock stats across all uids.
- long multicastWakeLockTimeTotalUs = 0;
- int multicastWakeLockCountTotal = 0;
-
- for (int iu = 0; iu < uidStats.size(); iu++) {
- final Uid u = uidStats.valueAt(iu);
-
- final Timer mcTimer = u.getMulticastWakelockStats();
-
- if (mcTimer != null) {
- multicastWakeLockTimeTotalUs +=
- mcTimer.getTotalTimeLocked(rawRealtimeUs, which);
- multicastWakeLockCountTotal += mcTimer.getCountLocked(which);
- }
- }
-
+ final long multicastWakeLockTimeTotalUs =
+ getWifiMulticastWakelockTime(rawRealtimeUs, which);
+ final int multicastWakeLockCountTotal = getWifiMulticastWakelockCount(which);
final long wmctToken = proto.start(SystemProto.WIFI_MULTICAST_WAKELOCK_TOTAL);
proto.write(SystemProto.WifiMulticastWakelockTotal.DURATION_MS,
multicastWakeLockTimeTotalUs / 1000);
diff --git a/android/os/Binder.java b/android/os/Binder.java
index 33470f36..eb264d6d 100644
--- a/android/os/Binder.java
+++ b/android/os/Binder.java
@@ -805,7 +805,7 @@ final class BinderProxy implements IBinder {
/**
* Return the total number of pairs in the map.
*/
- int size() {
+ private int size() {
int size = 0;
for (ArrayList<WeakReference<BinderProxy>> a : mMainIndexValues) {
if (a != null) {
@@ -816,6 +816,24 @@ final class BinderProxy implements IBinder {
}
/**
+ * Return the total number of pairs in the map containing values that have
+ * not been cleared. More expensive than the above size function.
+ */
+ private int unclearedSize() {
+ int size = 0;
+ for (ArrayList<WeakReference<BinderProxy>> a : mMainIndexValues) {
+ if (a != null) {
+ for (WeakReference<BinderProxy> ref : a) {
+ if (ref.get() != null) {
+ ++size;
+ }
+ }
+ }
+ }
+ return size;
+ }
+
+ /**
* Remove ith entry from the hash bucket indicated by hash.
*/
private void remove(int hash, int index) {
@@ -908,17 +926,31 @@ final class BinderProxy implements IBinder {
Log.v(Binder.TAG, "BinderProxy map growth! bucket size = " + size
+ " total = " + totalSize);
mWarnBucketSize += WARN_INCREMENT;
- if (Build.IS_DEBUGGABLE && totalSize > CRASH_AT_SIZE) {
- diagnosticCrash();
+ if (Build.IS_DEBUGGABLE && totalSize >= CRASH_AT_SIZE) {
+ // Use the number of uncleared entries to determine whether we should
+ // really report a histogram and crash. We don't want to fundamentally
+ // change behavior for a debuggable process, so we GC only if we are
+ // about to crash.
+ final int totalUnclearedSize = unclearedSize();
+ if (totalUnclearedSize >= CRASH_AT_SIZE) {
+ dumpProxyInterfaceCounts();
+ Runtime.getRuntime().gc();
+ throw new AssertionError("Binder ProxyMap has too many entries: "
+ + totalSize + " (total), " + totalUnclearedSize + " (uncleared), "
+ + unclearedSize() + " (uncleared after GC). BinderProxy leak?");
+ } else if (totalSize > 3 * totalUnclearedSize / 2) {
+ Log.v(Binder.TAG, "BinderProxy map has many cleared entries: "
+ + (totalSize - totalUnclearedSize) + " of " + totalSize
+ + " are cleared");
+ }
}
}
}
/**
- * Dump a histogram to the logcat, then throw an assertion error. Used to diagnose
- * abnormally large proxy maps.
+ * Dump a histogram to the logcat. Used to diagnose abnormally large proxy maps.
*/
- private void diagnosticCrash() {
+ private void dumpProxyInterfaceCounts() {
Map<String, Integer> counts = new HashMap<>();
for (ArrayList<WeakReference<BinderProxy>> a : mMainIndexValues) {
if (a != null) {
@@ -953,11 +985,6 @@ final class BinderProxy implements IBinder {
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/Bundle.java b/android/os/Bundle.java
index c58153aa..7ae5a673 100644
--- a/android/os/Bundle.java
+++ b/android/os/Bundle.java
@@ -21,6 +21,7 @@ import android.util.ArrayMap;
import android.util.Size;
import android.util.SizeF;
import android.util.SparseArray;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
@@ -1272,4 +1273,21 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable {
}
return mMap.toString();
}
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ if (mParcelledData != null) {
+ if (isEmptyParcel()) {
+ proto.write(BundleProto.PARCELLED_DATA_SIZE, 0);
+ } else {
+ proto.write(BundleProto.PARCELLED_DATA_SIZE, mParcelledData.dataSize());
+ }
+ } else {
+ proto.write(BundleProto.MAP_DATA, mMap.toString());
+ }
+
+ proto.end(token);
+ }
}
diff --git a/android/os/ConfigUpdate.java b/android/os/ConfigUpdate.java
index 94a44ec3..dda0ed8a 100644
--- a/android/os/ConfigUpdate.java
+++ b/android/os/ConfigUpdate.java
@@ -82,6 +82,14 @@ public final class ConfigUpdate {
public static final String ACTION_UPDATE_SMART_SELECTION
= "android.intent.action.UPDATE_SMART_SELECTION";
+ /**
+ * Update network watchlist config file.
+ * @hide
+ */
+ @SystemApi
+ public static final String ACTION_UPDATE_NETWORK_WATCHLIST
+ = "android.intent.action.UPDATE_NETWORK_WATCHLIST";
+
private ConfigUpdate() {
}
}
diff --git a/android/os/Debug.java b/android/os/Debug.java
index 848ab88d..33e8c3e4 100644
--- a/android/os/Debug.java
+++ b/android/os/Debug.java
@@ -2352,22 +2352,28 @@ public final class Debug
}
/**
- * Attach a library as a jvmti agent to the current runtime.
+ * Attach a library as a jvmti agent to the current runtime, with the given classloader
+ * determining the library search path.
+ * <p>
+ * Note: agents may only be attached to debuggable apps. Otherwise, this function will
+ * throw a SecurityException.
*
- * @param library library containing the agent
- * @param options options passed to the agent
+ * @param library the library containing the agent.
+ * @param options the options passed to the agent.
+ * @param classLoader the classloader determining the library search path.
*
- * @throws IOException If the agent could not be attached
+ * @throws IOException if the agent could not be attached.
+ * @throws SecurityException if the app is not debuggable.
*/
- public static void attachJvmtiAgent(@NonNull String library, @Nullable String options)
- throws IOException {
+ public static void attachJvmtiAgent(@NonNull String library, @Nullable String options,
+ @Nullable ClassLoader classLoader) throws IOException {
Preconditions.checkNotNull(library);
Preconditions.checkArgument(!library.contains("="));
if (options == null) {
- VMDebug.attachAgent(library);
+ VMDebug.attachAgent(library, classLoader);
} else {
- VMDebug.attachAgent(library + "=" + options);
+ VMDebug.attachAgent(library + "=" + options, classLoader);
}
}
}
diff --git a/android/os/Environment.java b/android/os/Environment.java
index b1794a6d..62731e84 100644
--- a/android/os/Environment.java
+++ b/android/os/Environment.java
@@ -292,6 +292,16 @@ public class Environment {
}
/** {@hide} */
+ public static File getDataVendorCeDirectory(int userId) {
+ return buildPath(getDataDirectory(), "vendor_ce", String.valueOf(userId));
+ }
+
+ /** {@hide} */
+ public static File getDataVendorDeDirectory(int userId) {
+ return buildPath(getDataDirectory(), "vendor_de", String.valueOf(userId));
+ }
+
+ /** {@hide} */
public static File getProfileSnapshotPath(String packageName, String codePath) {
return buildPath(buildPath(getDataDirectory(), "misc", "profiles", "ref", packageName,
"primary.prof.snapshot"));
diff --git a/android/os/Handler.java b/android/os/Handler.java
index 3ca1005b..fc88e900 100644
--- a/android/os/Handler.java
+++ b/android/os/Handler.java
@@ -202,7 +202,8 @@ public class Handler {
mLooper = Looper.myLooper();
if (mLooper == null) {
throw new RuntimeException(
- "Can't create handler inside thread that has not called Looper.prepare()");
+ "Can't create handler inside thread " + Thread.currentThread()
+ + " that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue;
mCallback = callback;
@@ -388,6 +389,8 @@ public class Handler {
* The runnable will be run on the thread to which this handler is attached.
*
* @param r The Runnable that will be executed.
+ * @param token An instance which can be used to cancel {@code r} via
+ * {@link #removeCallbacksAndMessages}.
* @param uptimeMillis The absolute time at which the callback should run,
* using the {@link android.os.SystemClock#uptimeMillis} time-base.
*
@@ -430,6 +433,32 @@ public class Handler {
}
/**
+ * Causes the Runnable r to be added to the message queue, to be run
+ * after the specified amount of time elapses.
+ * The runnable will be run on the thread to which this handler
+ * is attached.
+ * <b>The time-base is {@link android.os.SystemClock#uptimeMillis}.</b>
+ * Time spent in deep sleep will add an additional delay to execution.
+ *
+ * @param r The Runnable that will be executed.
+ * @param token An instance which can be used to cancel {@code r} via
+ * {@link #removeCallbacksAndMessages}.
+ * @param delayMillis The delay (in milliseconds) until the Runnable
+ * will be executed.
+ *
+ * @return Returns true if the Runnable was successfully placed in to the
+ * message queue. Returns false on failure, usually because the
+ * looper processing the message queue is exiting. Note that a
+ * result of true does not mean the Runnable will be processed --
+ * if the looper is quit before the delivery time of the message
+ * occurs then the message will be dropped.
+ */
+ public final boolean postDelayed(Runnable r, Object token, long delayMillis)
+ {
+ return sendMessageDelayed(getPostMessage(r, token), delayMillis);
+ }
+
+ /**
* Posts a message to an object that implements Runnable.
* Causes the Runnable r to executed on the next iteration through the
* message queue. The runnable will be run on the thread to which this
diff --git a/android/os/HidlSupport.java b/android/os/HidlSupport.java
index a080c8dc..335bf9d8 100644
--- a/android/os/HidlSupport.java
+++ b/android/os/HidlSupport.java
@@ -16,6 +16,8 @@
package android.os;
+import android.annotation.SystemApi;
+
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
@@ -25,6 +27,7 @@ import java.util.Objects;
import java.util.stream.IntStream;
/** @hide */
+@SystemApi
public class HidlSupport {
/**
* Similar to Objects.deepEquals, but also take care of lists.
@@ -36,7 +39,9 @@ public class HidlSupport {
* 2.3 Both are Lists, elements are checked recursively
* 2.4 (If both are collections other than lists or maps, throw an error)
* 2.5 lft.equals(rgt) returns true
+ * @hide
*/
+ @SystemApi
public static boolean deepEquals(Object lft, Object rgt) {
if (lft == rgt) {
return true;
@@ -86,8 +91,30 @@ public class HidlSupport {
}
/**
+ * Class which can be used to fetch an object out of a lambda. Fetching an object
+ * out of a local scope with HIDL is a common operation (although usually it can
+ * and should be avoided).
+ *
+ * @param <E> Inner object type.
+ * @hide
+ */
+ public static final class Mutable<E> {
+ public E value;
+
+ public Mutable() {
+ value = null;
+ }
+
+ public Mutable(E value) {
+ this.value = value;
+ }
+ }
+
+ /**
* Similar to Arrays.deepHashCode, but also take care of lists.
+ * @hide
*/
+ @SystemApi
public static int deepHashCode(Object o) {
if (o == null) {
return 0;
@@ -114,6 +141,7 @@ public class HidlSupport {
return o.hashCode();
}
+ /** @hide */
private static void throwErrorIfUnsupportedType(Object o) {
if (o instanceof Collection<?> && !(o instanceof List<?>)) {
throw new UnsupportedOperationException(
@@ -127,6 +155,7 @@ public class HidlSupport {
}
}
+ /** @hide */
private static int primitiveArrayHashCode(Object o) {
Class<?> elementType = o.getClass().getComponentType();
if (elementType == boolean.class) {
@@ -166,7 +195,9 @@ public class HidlSupport {
* - If both interfaces are stubs, asBinder() returns the object itself. By default,
* auto-generated IFoo.Stub does not override equals(), but an implementation can
* optionally override it, and {@code interfacesEqual} will use it here.
+ * @hide
*/
+ @SystemApi
public static boolean interfacesEqual(IHwInterface lft, Object rgt) {
if (lft == rgt) {
return true;
@@ -182,6 +213,10 @@ public class HidlSupport {
/**
* Return PID of process if sharable to clients.
+ * @hide
*/
public static native int getPidIfSharable();
+
+ /** @hide */
+ public HidlSupport() {}
}
diff --git a/android/os/HwBinder.java b/android/os/HwBinder.java
index 5e2a0815..ecac0029 100644
--- a/android/os/HwBinder.java
+++ b/android/os/HwBinder.java
@@ -16,16 +16,20 @@
package android.os;
+import android.annotation.SystemApi;
+
import libcore.util.NativeAllocationRegistry;
import java.util.NoSuchElementException;
/** @hide */
+@SystemApi
public abstract class HwBinder implements IHwBinder {
private static final String TAG = "HwBinder";
private static final NativeAllocationRegistry sNativeRegistry;
+ /** @hide */
public HwBinder() {
native_setup();
@@ -34,33 +38,55 @@ public abstract class HwBinder implements IHwBinder {
mNativeContext);
}
+ /** @hide */
@Override
public final native void transact(
int code, HwParcel request, HwParcel reply, int flags)
throws RemoteException;
+ /** @hide */
public abstract void onTransact(
int code, HwParcel request, HwParcel reply, int flags)
throws RemoteException;
+ /** @hide */
public native final void registerService(String serviceName)
throws RemoteException;
+ /** @hide */
public static final IHwBinder getService(
String iface,
String serviceName)
throws RemoteException, NoSuchElementException {
return getService(iface, serviceName, false /* retry */);
}
+ /** @hide */
public static native final IHwBinder getService(
String iface,
String serviceName,
boolean retry)
throws RemoteException, NoSuchElementException;
+ /**
+ * Configures how many threads the process-wide hwbinder threadpool
+ * has to process incoming requests.
+ *
+ * @hide
+ */
+ @SystemApi
public static native final void configureRpcThreadpool(
long maxThreads, boolean callerWillJoin);
+ /**
+ * Current thread will join hwbinder threadpool and process
+ * commands in the pool. Should be called after configuring
+ * a threadpool with callerWillJoin true and then registering
+ * the provided service if this thread doesn't need to do
+ * anything else.
+ *
+ * @hide
+ */
+ @SystemApi
public static native final void joinRpcThreadpool();
// Returns address of the "freeFunction".
@@ -83,6 +109,7 @@ public abstract class HwBinder implements IHwBinder {
/**
* Notifies listeners that a system property has changed
+ * @hide
*/
public static void reportSyspropChanged() {
native_report_sysprop_change();
diff --git a/android/os/HwBlob.java b/android/os/HwBlob.java
index 5e9b9ae3..405651e9 100644
--- a/android/os/HwBlob.java
+++ b/android/os/HwBlob.java
@@ -17,10 +17,17 @@
package android.os;
import android.annotation.NonNull;
+import android.annotation.SystemApi;
import libcore.util.NativeAllocationRegistry;
-/** @hide */
+/**
+ * Represents fixed sized allocation of marshalled data used. Helper methods
+ * allow for access to the unmarshalled data in a variety of ways.
+ *
+ * @hide
+ */
+@SystemApi
public class HwBlob {
private static final String TAG = "HwBlob";
@@ -34,48 +41,276 @@ public class HwBlob {
mNativeContext);
}
+ /**
+ * @param offset offset to unmarshall a boolean from
+ * @return the unmarshalled boolean value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final boolean getBool(long offset);
+ /**
+ * @param offset offset to unmarshall a byte from
+ * @return the unmarshalled byte value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final byte getInt8(long offset);
+ /**
+ * @param offset offset to unmarshall a short from
+ * @return the unmarshalled short value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final short getInt16(long offset);
+ /**
+ * @param offset offset to unmarshall an int from
+ * @return the unmarshalled int value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final int getInt32(long offset);
+ /**
+ * @param offset offset to unmarshall a long from
+ * @return the unmarshalled long value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final long getInt64(long offset);
+ /**
+ * @param offset offset to unmarshall a float from
+ * @return the unmarshalled float value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final float getFloat(long offset);
+ /**
+ * @param offset offset to unmarshall a double from
+ * @return the unmarshalled double value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final double getDouble(long offset);
+ /**
+ * @param offset offset to unmarshall a string from
+ * @return the unmarshalled string value
+ * @throws IndexOutOfBoundsException when offset is out of this HwBlob
+ */
public native final String getString(long offset);
/**
- The copyTo... methods copy the blob's data, starting from the given
- byte offset, into the array. A total of "size" _elements_ are copied.
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jboolean)] out of the blob.
*/
public native final void copyToBoolArray(long offset, boolean[] array, int size);
+ /**
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jbyte)] out of the blob.
+ */
public native final void copyToInt8Array(long offset, byte[] array, int size);
+ /**
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jshort)] out of the blob.
+ */
public native final void copyToInt16Array(long offset, short[] array, int size);
+ /**
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jint)] out of the blob.
+ */
public native final void copyToInt32Array(long offset, int[] array, int size);
+ /**
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jlong)] out of the blob.
+ */
public native final void copyToInt64Array(long offset, long[] array, int size);
+ /**
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jfloat)] out of the blob.
+ */
public native final void copyToFloatArray(long offset, float[] array, int size);
+ /**
+ * Copy the blobs data starting from the given byte offset into the range, copying
+ * a total of size elements.
+ *
+ * @param offset starting location in blob
+ * @param array destination array
+ * @param size total number of elements to copy
+ * @throws IllegalArgumentException array.length < size
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jdouble)] out of the blob.
+ */
public native final void copyToDoubleArray(long offset, double[] array, int size);
+ /**
+ * Writes a boolean value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jboolean)] is out of range
+ */
public native final void putBool(long offset, boolean x);
+ /**
+ * Writes a byte value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jbyte)] is out of range
+ */
public native final void putInt8(long offset, byte x);
+ /**
+ * Writes a short value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jshort)] is out of range
+ */
public native final void putInt16(long offset, short x);
+ /**
+ * Writes a int value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jint)] is out of range
+ */
public native final void putInt32(long offset, int x);
+ /**
+ * Writes a long value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jlong)] is out of range
+ */
public native final void putInt64(long offset, long x);
+ /**
+ * Writes a float value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jfloat)] is out of range
+ */
public native final void putFloat(long offset, float x);
+ /**
+ * Writes a double value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jdouble)] is out of range
+ */
public native final void putDouble(long offset, double x);
+ /**
+ * Writes a string value at an offset.
+ *
+ * @param offset location to write value
+ * @param x value to write
+ * @throws IndexOutOfBoundsException when [offset, offset + sizeof(jstring)] is out of range
+ */
public native final void putString(long offset, String x);
+ /**
+ * Put a boolean array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jboolean)] out of the blob.
+ */
public native final void putBoolArray(long offset, boolean[] x);
+ /**
+ * Put a byte array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jbyte)] out of the blob.
+ */
public native final void putInt8Array(long offset, byte[] x);
+ /**
+ * Put a short array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jshort)] out of the blob.
+ */
public native final void putInt16Array(long offset, short[] x);
+ /**
+ * Put a int array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jint)] out of the blob.
+ */
public native final void putInt32Array(long offset, int[] x);
+ /**
+ * Put a long array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jlong)] out of the blob.
+ */
public native final void putInt64Array(long offset, long[] x);
+ /**
+ * Put a float array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jfloat)] out of the blob.
+ */
public native final void putFloatArray(long offset, float[] x);
+ /**
+ * Put a double array contiguously at an offset in the blob.
+ *
+ * @param offset location to write values
+ * @param x array to write
+ * @throws IndexOutOfBoundsException [offset, offset + size * sizeof(jdouble)] out of the blob.
+ */
public native final void putDoubleArray(long offset, double[] x);
+ /**
+ * Write another HwBlob into this blob at the specified location.
+ *
+ * @param offset location to write value
+ * @param blob data to write
+ * @throws IndexOutOfBoundsException if [offset, offset + blob's size] outside of the range of
+ * this blob.
+ */
public native final void putBlob(long offset, HwBlob blob);
+ /**
+ * @return current handle of HwBlob for reference in a parcelled binder transaction
+ */
public native final long handle();
+ /**
+ * Convert a primitive to a wrapped array for boolean.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Boolean[] wrapArray(@NonNull boolean[] array) {
final int n = array.length;
Boolean[] wrappedArray = new Boolean[n];
@@ -85,6 +320,12 @@ public class HwBlob {
return wrappedArray;
}
+ /**
+ * Convert a primitive to a wrapped array for long.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Long[] wrapArray(@NonNull long[] array) {
final int n = array.length;
Long[] wrappedArray = new Long[n];
@@ -94,6 +335,12 @@ public class HwBlob {
return wrappedArray;
}
+ /**
+ * Convert a primitive to a wrapped array for byte.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Byte[] wrapArray(@NonNull byte[] array) {
final int n = array.length;
Byte[] wrappedArray = new Byte[n];
@@ -103,6 +350,12 @@ public class HwBlob {
return wrappedArray;
}
+ /**
+ * Convert a primitive to a wrapped array for short.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Short[] wrapArray(@NonNull short[] array) {
final int n = array.length;
Short[] wrappedArray = new Short[n];
@@ -112,6 +365,12 @@ public class HwBlob {
return wrappedArray;
}
+ /**
+ * Convert a primitive to a wrapped array for int.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Integer[] wrapArray(@NonNull int[] array) {
final int n = array.length;
Integer[] wrappedArray = new Integer[n];
@@ -121,6 +380,12 @@ public class HwBlob {
return wrappedArray;
}
+ /**
+ * Convert a primitive to a wrapped array for float.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Float[] wrapArray(@NonNull float[] array) {
final int n = array.length;
Float[] wrappedArray = new Float[n];
@@ -130,6 +395,12 @@ public class HwBlob {
return wrappedArray;
}
+ /**
+ * Convert a primitive to a wrapped array for double.
+ *
+ * @param array from array
+ * @return transformed array
+ */
public static Double[] wrapArray(@NonNull double[] array) {
final int n = array.length;
Double[] wrappedArray = new Double[n];
diff --git a/android/os/HwParcel.java b/android/os/HwParcel.java
index 4ba11447..0eb62c95 100644
--- a/android/os/HwParcel.java
+++ b/android/os/HwParcel.java
@@ -16,17 +16,32 @@
package android.os;
-import java.util.ArrayList;
-import java.util.Arrays;
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
import libcore.util.NativeAllocationRegistry;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+
/** @hide */
+@SystemApi
public class HwParcel {
private static final String TAG = "HwParcel";
+ @IntDef(prefix = { "STATUS_" }, value = {
+ STATUS_SUCCESS,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Status {}
+
+ /**
+ * Success return error for a transaction. Written to parcels
+ * using writeStatus.
+ */
public static final int STATUS_SUCCESS = 0;
- public static final int STATUS_ERROR = -1;
private static final NativeAllocationRegistry sNativeRegistry;
@@ -38,6 +53,9 @@ public class HwParcel {
mNativeContext);
}
+ /**
+ * Creates an initialized and empty parcel.
+ */
public HwParcel() {
native_setup(true /* allocate */);
@@ -46,25 +64,106 @@ public class HwParcel {
mNativeContext);
}
+ /**
+ * Writes an interface token into the parcel used to verify that
+ * a transaction has made it to the write type of interface.
+ *
+ * @param interfaceName fully qualified name of interface message
+ * is being sent to.
+ */
public native final void writeInterfaceToken(String interfaceName);
+ /**
+ * Writes a boolean value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeBool(boolean val);
+ /**
+ * Writes a byte value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeInt8(byte val);
+ /**
+ * Writes a short value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeInt16(short val);
+ /**
+ * Writes a int value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeInt32(int val);
+ /**
+ * Writes a long value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeInt64(long val);
+ /**
+ * Writes a float value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeFloat(float val);
+ /**
+ * Writes a double value to the end of the parcel.
+ * @param val to write
+ */
public native final void writeDouble(double val);
+ /**
+ * Writes a String value to the end of the parcel.
+ *
+ * Note, this will be converted to UTF-8 when it is written.
+ *
+ * @param val to write
+ */
public native final void writeString(String val);
+ /**
+ * Writes an array of boolean values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeBoolVector(boolean[] val);
+ /**
+ * Writes an array of byte values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeInt8Vector(byte[] val);
+ /**
+ * Writes an array of short values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeInt16Vector(short[] val);
+ /**
+ * Writes an array of int values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeInt32Vector(int[] val);
+ /**
+ * Writes an array of long values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeInt64Vector(long[] val);
+ /**
+ * Writes an array of float values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeFloatVector(float[] val);
+ /**
+ * Writes an array of double values to the end of the parcel.
+ * @param val to write
+ */
private native final void writeDoubleVector(double[] val);
+ /**
+ * Writes an array of String values to the end of the parcel.
+ *
+ * Note, these will be converted to UTF-8 as they are written.
+ *
+ * @param val to write
+ */
private native final void writeStringVector(String[] val);
+ /**
+ * Helper method to write a list of Booleans to val.
+ * @param val list to write
+ */
public final void writeBoolVector(ArrayList<Boolean> val) {
final int n = val.size();
boolean[] array = new boolean[n];
@@ -75,6 +174,10 @@ public class HwParcel {
writeBoolVector(array);
}
+ /**
+ * Helper method to write a list of Booleans to the end of the parcel.
+ * @param val list to write
+ */
public final void writeInt8Vector(ArrayList<Byte> val) {
final int n = val.size();
byte[] array = new byte[n];
@@ -85,6 +188,10 @@ public class HwParcel {
writeInt8Vector(array);
}
+ /**
+ * Helper method to write a list of Shorts to the end of the parcel.
+ * @param val list to write
+ */
public final void writeInt16Vector(ArrayList<Short> val) {
final int n = val.size();
short[] array = new short[n];
@@ -95,6 +202,10 @@ public class HwParcel {
writeInt16Vector(array);
}
+ /**
+ * Helper method to write a list of Integers to the end of the parcel.
+ * @param val list to write
+ */
public final void writeInt32Vector(ArrayList<Integer> val) {
final int n = val.size();
int[] array = new int[n];
@@ -105,6 +216,10 @@ public class HwParcel {
writeInt32Vector(array);
}
+ /**
+ * Helper method to write a list of Longs to the end of the parcel.
+ * @param val list to write
+ */
public final void writeInt64Vector(ArrayList<Long> val) {
final int n = val.size();
long[] array = new long[n];
@@ -115,6 +230,10 @@ public class HwParcel {
writeInt64Vector(array);
}
+ /**
+ * Helper method to write a list of Floats to the end of the parcel.
+ * @param val list to write
+ */
public final void writeFloatVector(ArrayList<Float> val) {
final int n = val.size();
float[] array = new float[n];
@@ -125,6 +244,10 @@ public class HwParcel {
writeFloatVector(array);
}
+ /**
+ * Helper method to write a list of Doubles to the end of the parcel.
+ * @param val list to write
+ */
public final void writeDoubleVector(ArrayList<Double> val) {
final int n = val.size();
double[] array = new double[n];
@@ -135,93 +258,272 @@ public class HwParcel {
writeDoubleVector(array);
}
+ /**
+ * Helper method to write a list of Strings to the end of the parcel.
+ * @param val list to write
+ */
public final void writeStringVector(ArrayList<String> val) {
writeStringVector(val.toArray(new String[val.size()]));
}
+ /**
+ * Write a hwbinder object to the end of the parcel.
+ * @param binder value to write
+ */
public native final void writeStrongBinder(IHwBinder binder);
+ /**
+ * Checks to make sure that the interface name matches the name written by the parcel
+ * sender by writeInterfaceToken
+ *
+ * @throws SecurityException interface doesn't match
+ */
public native final void enforceInterface(String interfaceName);
+
+ /**
+ * Reads a boolean value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final boolean readBool();
+ /**
+ * Reads a byte value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final byte readInt8();
+ /**
+ * Reads a short value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final short readInt16();
+ /**
+ * Reads a int value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final int readInt32();
+ /**
+ * Reads a long value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final long readInt64();
+ /**
+ * Reads a float value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final float readFloat();
+ /**
+ * Reads a double value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final double readDouble();
+ /**
+ * Reads a String value from the current location in the parcel.
+ * @return value parsed from the parcel
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final String readString();
+ /**
+ * Reads an array of boolean values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final boolean[] readBoolVectorAsArray();
+ /**
+ * Reads an array of byte values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final byte[] readInt8VectorAsArray();
+ /**
+ * Reads an array of short values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final short[] readInt16VectorAsArray();
+ /**
+ * Reads an array of int values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final int[] readInt32VectorAsArray();
+ /**
+ * Reads an array of long values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final long[] readInt64VectorAsArray();
+ /**
+ * Reads an array of float values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final float[] readFloatVectorAsArray();
+ /**
+ * Reads an array of double values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final double[] readDoubleVectorAsArray();
+ /**
+ * Reads an array of String values from the parcel.
+ * @return array of parsed values
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
private native final String[] readStringVectorAsArray();
+ /**
+ * Convenience method to read a Boolean vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Boolean> readBoolVector() {
Boolean[] array = HwBlob.wrapArray(readBoolVectorAsArray());
return new ArrayList<Boolean>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a Byte vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Byte> readInt8Vector() {
Byte[] array = HwBlob.wrapArray(readInt8VectorAsArray());
return new ArrayList<Byte>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a Short vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Short> readInt16Vector() {
Short[] array = HwBlob.wrapArray(readInt16VectorAsArray());
return new ArrayList<Short>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a Integer vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Integer> readInt32Vector() {
Integer[] array = HwBlob.wrapArray(readInt32VectorAsArray());
return new ArrayList<Integer>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a Long vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Long> readInt64Vector() {
Long[] array = HwBlob.wrapArray(readInt64VectorAsArray());
return new ArrayList<Long>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a Float vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Float> readFloatVector() {
Float[] array = HwBlob.wrapArray(readFloatVectorAsArray());
return new ArrayList<Float>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a Double vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<Double> readDoubleVector() {
Double[] array = HwBlob.wrapArray(readDoubleVectorAsArray());
return new ArrayList<Double>(Arrays.asList(array));
}
+ /**
+ * Convenience method to read a String vector as an ArrayList.
+ * @return array of parsed values.
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public final ArrayList<String> readStringVector() {
return new ArrayList<String>(Arrays.asList(readStringVectorAsArray()));
}
+ /**
+ * Reads a strong binder value from the parcel.
+ * @return binder object read from parcel or null if no binder can be read
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final IHwBinder readStrongBinder();
- // Handle is stored as part of the blob.
+ /**
+ * Read opaque segment of data as a blob.
+ * @return blob of size expectedSize
+ * @throws IllegalArgumentException if the parcel has no more data
+ */
public native final HwBlob readBuffer(long expectedSize);
+ /**
+ * Read a buffer written using scatter gather.
+ *
+ * @param expectedSize size that buffer should be
+ * @param parentHandle handle from which to read the embedded buffer
+ * @param offset offset into parent
+ * @param nullable whether or not to allow for a null return
+ * @return blob of data with size expectedSize
+ * @throws NoSuchElementException if an embedded buffer is not available to read
+ * @throws IllegalArgumentException if expectedSize < 0
+ * @throws NullPointerException if the transaction specified the blob to be null
+ * but nullable is false
+ */
public native final HwBlob readEmbeddedBuffer(
long expectedSize, long parentHandle, long offset,
boolean nullable);
+ /**
+ * Write a buffer into the transaction.
+ * @param blob blob to write into the parcel.
+ */
public native final void writeBuffer(HwBlob blob);
-
+ /**
+ * Write a status value into the blob.
+ * @param status value to write
+ */
public native final void writeStatus(int status);
+ /**
+ * @throws IllegalArgumentException if a success vaue cannot be read
+ * @throws RemoteException if success value indicates a transaction error
+ */
public native final void verifySuccess();
+ /**
+ * Should be called to reduce memory pressure when this object no longer needs
+ * to be written to.
+ */
public native final void releaseTemporaryStorage();
+ /**
+ * Should be called when object is no longer needed to reduce possible memory
+ * pressure if the Java GC does not get to this object in time.
+ */
public native final void release();
+ /**
+ * Sends the parcel to the specified destination.
+ */
public native final void send();
// Returns address of the "freeFunction".
diff --git a/android/os/IHwBinder.java b/android/os/IHwBinder.java
index 619f4dc6..ce9f6c16 100644
--- a/android/os/IHwBinder.java
+++ b/android/os/IHwBinder.java
@@ -16,26 +16,47 @@
package android.os;
+import android.annotation.SystemApi;
+
/** @hide */
+@SystemApi
public interface IHwBinder {
// These MUST match their corresponding libhwbinder/IBinder.h definition !!!
+ /** @hide */
public static final int FIRST_CALL_TRANSACTION = 1;
+ /** @hide */
public static final int FLAG_ONEWAY = 1;
+ /** @hide */
public void transact(
int code, HwParcel request, HwParcel reply, int flags)
throws RemoteException;
+ /** @hide */
public IHwInterface queryLocalInterface(String descriptor);
/**
* Interface for receiving a callback when the process hosting a service
* has gone away.
*/
+ @SystemApi
public interface DeathRecipient {
+ /**
+ * Callback for a registered process dying.
+ */
+ @SystemApi
public void serviceDied(long cookie);
}
+ /**
+ * Notifies the death recipient with the cookie when the process containing
+ * this binder dies.
+ */
+ @SystemApi
public boolean linkToDeath(DeathRecipient recipient, long cookie);
+ /**
+ * Unregisters the death recipient from this binder.
+ */
+ @SystemApi
public boolean unlinkToDeath(DeathRecipient recipient);
}
diff --git a/android/os/IHwInterface.java b/android/os/IHwInterface.java
index 7c5ac6f4..a2f59a9a 100644
--- a/android/os/IHwInterface.java
+++ b/android/os/IHwInterface.java
@@ -16,7 +16,13 @@
package android.os;
+import android.annotation.SystemApi;
/** @hide */
+@SystemApi
public interface IHwInterface {
+ /**
+ * Returns the binder object that corresponds to an interface.
+ */
+ @SystemApi
public IHwBinder asBinder();
}
diff --git a/android/os/PackageManagerPerfTest.java b/android/os/PackageManagerPerfTest.java
new file mode 100644
index 00000000..145fbcd2
--- /dev/null
+++ b/android/os/PackageManagerPerfTest.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR 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 static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class PackageManagerPerfTest {
+ private static final String PERMISSION_NAME_EXISTS =
+ "com.android.perftests.core.TestPermission";
+ private static final String PERMISSION_NAME_DOESNT_EXIST =
+ "com.android.perftests.core.TestBadPermission";
+ private static final ComponentName TEST_ACTIVITY =
+ new ComponentName("com.android.perftests.core", "android.perftests.utils.StubActivity");
+
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testCheckPermissionExists() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
+ final String packageName = TEST_ACTIVITY.getPackageName();
+
+ while (state.keepRunning()) {
+ int ret = pm.checkPermission(PERMISSION_NAME_EXISTS, packageName);
+ }
+ }
+
+ @Test
+ public void testCheckPermissionDoesntExist() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
+ final String packageName = TEST_ACTIVITY.getPackageName();
+
+ while (state.keepRunning()) {
+ int ret = pm.checkPermission(PERMISSION_NAME_DOESNT_EXIST, packageName);
+ }
+ }
+
+ @Test
+ public void testQueryIntentActivities() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
+ final Intent intent = new Intent("com.android.perftests.core.PERFTEST");
+
+ while (state.keepRunning()) {
+ pm.queryIntentActivities(intent, 0);
+ }
+ }
+
+ @Test
+ public void testGetPackageInfo() throws Exception {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
+ final String packageName = TEST_ACTIVITY.getPackageName();
+
+ while (state.keepRunning()) {
+ pm.getPackageInfo(packageName, 0);
+ }
+ }
+
+ @Test
+ public void testGetApplicationInfo() throws Exception {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
+ final String packageName = TEST_ACTIVITY.getPackageName();
+
+ while (state.keepRunning()) {
+ pm.getApplicationInfo(packageName, 0);
+ }
+ }
+
+ @Test
+ public void testGetActivityInfo() throws Exception {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final PackageManager pm = InstrumentationRegistry.getTargetContext().getPackageManager();
+
+ while (state.keepRunning()) {
+ pm.getActivityInfo(TEST_ACTIVITY, 0);
+ }
+ }
+}
diff --git a/android/os/PersistableBundle.java b/android/os/PersistableBundle.java
index 3ed5b174..40eceb8a 100644
--- a/android/os/PersistableBundle.java
+++ b/android/os/PersistableBundle.java
@@ -18,6 +18,7 @@ package android.os;
import android.annotation.Nullable;
import android.util.ArrayMap;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.util.XmlUtils;
@@ -321,4 +322,21 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa
}
return mMap.toString();
}
+
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ if (mParcelledData != null) {
+ if (isEmptyParcel()) {
+ proto.write(PersistableBundleProto.PARCELLED_DATA_SIZE, 0);
+ } else {
+ proto.write(PersistableBundleProto.PARCELLED_DATA_SIZE, mParcelledData.dataSize());
+ }
+ } else {
+ proto.write(PersistableBundleProto.MAP_DATA, mMap.toString());
+ }
+
+ proto.end(token);
+ }
}
diff --git a/android/os/PowerManager.java b/android/os/PowerManager.java
index cd6d41b3..3d17ffb7 100644
--- a/android/os/PowerManager.java
+++ b/android/os/PowerManager.java
@@ -23,6 +23,7 @@ import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.content.Context;
import android.util.Log;
+import android.util.proto.ProtoOutputStream;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -565,6 +566,42 @@ public final class PowerManager {
int OPTIONAL_SENSORS = 13;
}
+ /**
+ * Either the location providers shouldn't be affected by battery saver,
+ * or battery saver is off.
+ */
+ public static final int LOCATION_MODE_NO_CHANGE = 0;
+
+ /**
+ * In this mode, the GPS based location provider should be disabled when battery saver is on and
+ * the device is non-interactive.
+ */
+ public static final int LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF = 1;
+
+ /**
+ * All location providers should be disabled when battery saver is on and
+ * the device is non-interactive.
+ */
+ public static final int LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF = 2;
+
+ /**
+ * In this mode, all the location providers will be kept available, but location fixes
+ * should only be provided to foreground apps.
+ */
+ public static final int LOCATION_MODE_FOREGROUND_ONLY = 3;
+
+ /**
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"LOCATION_MODE_"}, value = {
+ LOCATION_MODE_NO_CHANGE,
+ LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF,
+ LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF,
+ LOCATION_MODE_FOREGROUND_ONLY,
+ })
+ public @interface LocationPowerSaveMode {}
+
final Context mContext;
final IPowerManager mService;
final Handler mHandler;
@@ -964,24 +1001,6 @@ public final class PowerManager {
return false;
}
- /**
- * Sets the brightness of the backlights (screen, keyboard, button).
- * <p>
- * Requires the {@link android.Manifest.permission#DEVICE_POWER} permission.
- * </p>
- *
- * @param brightness The brightness value from 0 to 255.
- *
- * @hide Requires signature permission.
- */
- public void setBacklightBrightness(int brightness) {
- try {
- mService.setTemporaryScreenBrightnessSettingOverride(brightness);
- } catch (RemoteException e) {
- throw e.rethrowFromSystemServer();
- }
- }
-
/**
* Returns true if the specified wake lock level is supported.
*
@@ -1143,6 +1162,24 @@ public final class PowerManager {
}
/**
+ * Returns how location features should behave when battery saver is on. When battery saver
+ * is off, this will always return {@link #LOCATION_MODE_NO_CHANGE}.
+ *
+ * <p>This API is normally only useful for components that provide location features.
+ *
+ * @see #isPowerSaveMode()
+ * @see #ACTION_POWER_SAVE_MODE_CHANGED
+ */
+ @LocationPowerSaveMode
+ public int getLocationPowerSaveMode() {
+ final PowerSaveState powerSaveState = getPowerSaveState(ServiceType.GPS);
+ if (!powerSaveState.globalBatterySaverEnabled) {
+ return LOCATION_MODE_NO_CHANGE;
+ }
+ return powerSaveState.gpsMode;
+ }
+
+ /**
* Returns true if the device is currently in idle mode. This happens when a device
* has been sitting unused and unmoving for a sufficiently long period of time, so that
* it decides to go into a lower power-use state. This may involve things like turning
@@ -1598,6 +1635,21 @@ public final class PowerManager {
}
}
+ /** @hide */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ synchronized (mToken) {
+ final long token = proto.start(fieldId);
+ proto.write(PowerManagerProto.WakeLockProto.HEX_STRING,
+ Integer.toHexString(System.identityHashCode(this)));
+ proto.write(PowerManagerProto.WakeLockProto.HELD, mHeld);
+ proto.write(PowerManagerProto.WakeLockProto.INTERNAL_COUNT, mInternalCount);
+ if (mWorkSource != null) {
+ mWorkSource.writeToProto(proto, PowerManagerProto.WakeLockProto.WORK_SOURCE);
+ }
+ proto.end(token);
+ }
+ }
+
/**
* Wraps a Runnable such that this method immediately acquires the wake lock and then
* once the Runnable is done the wake lock is released.
diff --git a/android/os/PowerManagerInternal.java b/android/os/PowerManagerInternal.java
index 3ef0961f..c7d89b0c 100644
--- a/android/os/PowerManagerInternal.java
+++ b/android/os/PowerManagerInternal.java
@@ -71,6 +71,24 @@ public abstract class PowerManagerInternal {
}
/**
+ * Converts platform constants to proto enums.
+ */
+ public static int wakefulnessToProtoEnum(int wakefulness) {
+ switch (wakefulness) {
+ case WAKEFULNESS_ASLEEP:
+ return PowerManagerInternalProto.WAKEFULNESS_ASLEEP;
+ case WAKEFULNESS_AWAKE:
+ return PowerManagerInternalProto.WAKEFULNESS_AWAKE;
+ case WAKEFULNESS_DREAMING:
+ return PowerManagerInternalProto.WAKEFULNESS_DREAMING;
+ case WAKEFULNESS_DOZING:
+ return PowerManagerInternalProto.WAKEFULNESS_DOZING;
+ default:
+ return wakefulness;
+ }
+ }
+
+ /**
* Returns true if the wakefulness state represents an interactive state
* as defined by {@link android.os.PowerManager#isInteractive}.
*/
diff --git a/android/os/Process.java b/android/os/Process.java
index 0874d93e..6833908b 100644
--- a/android/os/Process.java
+++ b/android/os/Process.java
@@ -143,7 +143,7 @@ public class Process {
* Defines the UID/GID for the WebView zygote process.
* @hide
*/
- public static final int WEBVIEW_ZYGOTE_UID = 1051;
+ public static final int WEBVIEW_ZYGOTE_UID = 1053;
/**
* Defines the UID used for resource tracking for OTA updates.
@@ -151,6 +151,12 @@ public class Process {
*/
public static final int OTA_UPDATE_UID = 1061;
+ /**
+ * Defines the UID used for incidentd.
+ * @hide
+ */
+ public static final int INCIDENTD_UID = 1067;
+
/** {@hide} */
public static final int NOBODY_UID = 9999;
@@ -269,6 +275,15 @@ public class Process {
public static final int THREAD_PRIORITY_URGENT_DISPLAY = -8;
/**
+ * Standard priority of video threads. Applications can not normally
+ * change to this priority.
+ * Use with {@link #setThreadPriority(int)} and
+ * {@link #setThreadPriority(int, int)}, <b>not</b> with the normal
+ * {@link java.lang.Thread} class.
+ */
+ public static final int THREAD_PRIORITY_VIDEO = -10;
+
+ /**
* Standard priority of audio threads. Applications can not normally
* change to this priority.
* Use with {@link #setThreadPriority(int)} and
@@ -559,6 +574,14 @@ public class Process {
}
/**
+ * Returns whether the given uid belongs to a system core component or not.
+ * @hide
+ */
+ public static boolean isCoreUid(int uid) {
+ return UserHandle.isCore(uid);
+ }
+
+ /**
* Returns whether the given uid belongs to an application.
* @param uid A kernel uid.
* @return Whether the uid corresponds to an application sandbox running in
diff --git a/android/os/PssPerfTest.java b/android/os/PssPerfTest.java
new file mode 100644
index 00000000..400115de
--- /dev/null
+++ b/android/os/PssPerfTest.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package android.os;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@LargeTest
+public class PssPerfTest {
+ @Rule
+ public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+ @Test
+ public void testPss() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ while (state.keepRunning()) {
+ Debug.getPss();
+ }
+ }
+}
diff --git a/android/os/RecoverySystem.java b/android/os/RecoverySystem.java
index 673a8ba6..3e8e8854 100644
--- a/android/os/RecoverySystem.java
+++ b/android/os/RecoverySystem.java
@@ -61,6 +61,7 @@ import java.util.HashSet;
import java.util.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipInputStream;
@@ -101,6 +102,9 @@ public class RecoverySystem {
private static final String ACTION_EUICC_FACTORY_RESET =
"com.android.internal.action.EUICC_FACTORY_RESET";
+ /** used in {@link #wipeEuiccData} as package name of callback intent */
+ private static final String PACKAGE_NAME_WIPING_EUICC_DATA_CALLBACK = "android";
+
/**
* The recovery image uses this file to identify the location (i.e. blocks)
* of an OTA package on the /data partition. The block map file is
@@ -751,7 +755,9 @@ public class RecoverySystem {
// Block until the ordered broadcast has completed.
condition.block();
- wipeEuiccData(context, wipeEuicc);
+ if (wipeEuicc) {
+ wipeEuiccData(context, PACKAGE_NAME_WIPING_EUICC_DATA_CALLBACK);
+ }
String shutdownArg = null;
if (shutdown) {
@@ -767,19 +773,27 @@ public class RecoverySystem {
bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
}
- private static void wipeEuiccData(Context context, final boolean isWipeEuicc) {
+ /**
+ * Returns whether wipe Euicc data successfully or not.
+ *
+ * @param packageName the package name of the caller app.
+ *
+ * @hide
+ */
+ public static boolean wipeEuiccData(Context context, final String packageName) {
ContentResolver cr = context.getContentResolver();
if (Settings.Global.getInt(cr, Settings.Global.EUICC_PROVISIONED, 0) == 0) {
// If the eUICC isn't provisioned, there's no reason to either wipe or retain profiles,
// as there's nothing to wipe nor retain.
Log.d(TAG, "Skipping eUICC wipe/retain as it is not provisioned");
- return;
+ return true;
}
EuiccManager euiccManager = (EuiccManager) context.getSystemService(
Context.EUICC_SERVICE);
if (euiccManager != null && euiccManager.isEnabled()) {
CountDownLatch euiccFactoryResetLatch = new CountDownLatch(1);
+ final AtomicBoolean wipingSucceeded = new AtomicBoolean(false);
BroadcastReceiver euiccWipeFinishReceiver = new BroadcastReceiver() {
@Override
@@ -788,19 +802,11 @@ public class RecoverySystem {
if (getResultCode() != EuiccManager.EMBEDDED_SUBSCRIPTION_RESULT_OK) {
int detailedCode = intent.getIntExtra(
EuiccManager.EXTRA_EMBEDDED_SUBSCRIPTION_DETAILED_CODE, 0);
- if (isWipeEuicc) {
- Log.e(TAG, "Error wiping euicc data, Detailed code = "
- + detailedCode);
- } else {
- Log.e(TAG, "Error retaining euicc data, Detailed code = "
- + detailedCode);
- }
+ Log.e(TAG, "Error wiping euicc data, Detailed code = "
+ + detailedCode);
} else {
- if (isWipeEuicc) {
- Log.d(TAG, "Successfully wiped euicc data.");
- } else {
- Log.d(TAG, "Successfully retained euicc data.");
- }
+ Log.d(TAG, "Successfully wiped euicc data.");
+ wipingSucceeded.set(true /* newValue */);
}
euiccFactoryResetLatch.countDown();
}
@@ -808,7 +814,7 @@ public class RecoverySystem {
};
Intent intent = new Intent(ACTION_EUICC_FACTORY_RESET);
- intent.setPackage("android");
+ intent.setPackage(packageName);
PendingIntent callbackIntent = PendingIntent.getBroadcastAsUser(
context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT, UserHandle.SYSTEM);
IntentFilter filterConsent = new IntentFilter();
@@ -818,11 +824,7 @@ public class RecoverySystem {
Handler euiccHandler = new Handler(euiccHandlerThread.getLooper());
context.getApplicationContext()
.registerReceiver(euiccWipeFinishReceiver, filterConsent, null, euiccHandler);
- if (isWipeEuicc) {
- euiccManager.eraseSubscriptions(callbackIntent);
- } else {
- euiccManager.retainSubscriptionsForFactoryReset(callbackIntent);
- }
+ euiccManager.eraseSubscriptions(callbackIntent);
try {
long waitingTimeMillis = Settings.Global.getLong(
context.getContentResolver(),
@@ -834,22 +836,19 @@ public class RecoverySystem {
waitingTimeMillis = MAX_EUICC_FACTORY_RESET_TIMEOUT_MILLIS;
}
if (!euiccFactoryResetLatch.await(waitingTimeMillis, TimeUnit.MILLISECONDS)) {
- if (isWipeEuicc) {
- Log.e(TAG, "Timeout wiping eUICC data.");
- } else {
- Log.e(TAG, "Timeout retaining eUICC data.");
- }
+ Log.e(TAG, "Timeout wiping eUICC data.");
+ return false;
}
- context.getApplicationContext().unregisterReceiver(euiccWipeFinishReceiver);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
- if (isWipeEuicc) {
- Log.e(TAG, "Wiping eUICC data interrupted", e);
- } else {
- Log.e(TAG, "Retaining eUICC data interrupted", e);
- }
+ Log.e(TAG, "Wiping eUICC data interrupted", e);
+ return false;
+ } finally {
+ context.getApplicationContext().unregisterReceiver(euiccWipeFinishReceiver);
}
+ return wipingSucceeded.get();
}
+ return false;
}
/** {@hide} */
diff --git a/android/os/StatsDimensionsValue.java b/android/os/StatsDimensionsValue.java
new file mode 100644
index 00000000..257cc525
--- /dev/null
+++ b/android/os/StatsDimensionsValue.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.os;
+
+import android.annotation.SystemApi;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Container for statsd dimension value information, corresponding to a
+ * stats_log.proto's DimensionValue.
+ *
+ * This consists of a field (an int representing a statsd atom field)
+ * and a value (which may be one of a number of types).
+ *
+ * <p>
+ * Only a single value is held, and it is necessarily one of the following types:
+ * {@link String}, int, long, boolean, float,
+ * or tuple (i.e. {@link List} of {@code StatsDimensionsValue}).
+ *
+ * The type of value held can be retrieved using {@link #getValueType()}, which returns one of the
+ * following ints, depending on the type of value:
+ * <ul>
+ * <li>{@link #STRING_VALUE_TYPE}</li>
+ * <li>{@link #INT_VALUE_TYPE}</li>
+ * <li>{@link #LONG_VALUE_TYPE}</li>
+ * <li>{@link #BOOLEAN_VALUE_TYPE}</li>
+ * <li>{@link #FLOAT_VALUE_TYPE}</li>
+ * <li>{@link #TUPLE_VALUE_TYPE}</li>
+ * </ul>
+ * Alternatively, this can be determined using {@link #isValueType(int)} with one of these constants
+ * as a parameter.
+ * The value itself can be retrieved using the correct get...Value() function for its type.
+ *
+ * <p>
+ * The field is always an int, and always exists; it can be obtained using {@link #getField()}.
+ *
+ *
+ * @hide
+ */
+@SystemApi
+public final class StatsDimensionsValue implements Parcelable {
+ private static final String TAG = "StatsDimensionsValue";
+
+ // Values of the value type correspond to stats_log.proto's DimensionValue fields.
+ // Keep constants in sync with services/include/android/os/StatsDimensionsValue.h.
+ /** Indicates that this holds a String. */
+ public static final int STRING_VALUE_TYPE = 2;
+ /** Indicates that this holds an int. */
+ public static final int INT_VALUE_TYPE = 3;
+ /** Indicates that this holds a long. */
+ public static final int LONG_VALUE_TYPE = 4;
+ /** Indicates that this holds a boolean. */
+ public static final int BOOLEAN_VALUE_TYPE = 5;
+ /** Indicates that this holds a float. */
+ public static final int FLOAT_VALUE_TYPE = 6;
+ /** Indicates that this holds a List of StatsDimensionsValues. */
+ public static final int TUPLE_VALUE_TYPE = 7;
+
+ /** Value of a stats_log.proto DimensionsValue.field. */
+ private final int mField;
+
+ /** Type of stats_log.proto DimensionsValue.value, according to the VALUE_TYPEs above. */
+ private final int mValueType;
+
+ /**
+ * Value of a stats_log.proto DimensionsValue.value.
+ * String, Integer, Long, Boolean, Float, or StatsDimensionsValue[].
+ */
+ private final Object mValue; // immutable or array of immutables
+
+ /**
+ * Creates a {@code StatsDimensionValue} from a parcel.
+ *
+ * @hide
+ */
+ public StatsDimensionsValue(Parcel in) {
+ mField = in.readInt();
+ mValueType = in.readInt();
+ mValue = readValueFromParcel(mValueType, in);
+ }
+
+ /**
+ * Return the field, i.e. the tag of a statsd atom.
+ *
+ * @return the field
+ */
+ public int getField() {
+ return mField;
+ }
+
+ /**
+ * Retrieve the String held, if any.
+ *
+ * @return the {@link String} held if {@link #getValueType()} == {@link #STRING_VALUE_TYPE},
+ * null otherwise
+ */
+ public String getStringValue() {
+ try {
+ if (mValueType == STRING_VALUE_TYPE) return (String) mValue;
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ }
+ return null;
+ }
+
+ /**
+ * Retrieve the int held, if any.
+ *
+ * @return the int held if {@link #getValueType()} == {@link #INT_VALUE_TYPE}, 0 otherwise
+ */
+ public int getIntValue() {
+ try {
+ if (mValueType == INT_VALUE_TYPE) return (Integer) mValue;
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ }
+ return 0;
+ }
+
+ /**
+ * Retrieve the long held, if any.
+ *
+ * @return the long held if {@link #getValueType()} == {@link #LONG_VALUE_TYPE}, 0 otherwise
+ */
+ public long getLongValue() {
+ try {
+ if (mValueType == LONG_VALUE_TYPE) return (Long) mValue;
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ }
+ return 0;
+ }
+
+ /**
+ * Retrieve the boolean held, if any.
+ *
+ * @return the boolean held if {@link #getValueType()} == {@link #BOOLEAN_VALUE_TYPE},
+ * false otherwise
+ */
+ public boolean getBooleanValue() {
+ try {
+ if (mValueType == BOOLEAN_VALUE_TYPE) return (Boolean) mValue;
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ }
+ return false;
+ }
+
+ /**
+ * Retrieve the float held, if any.
+ *
+ * @return the float held if {@link #getValueType()} == {@link #FLOAT_VALUE_TYPE}, 0 otherwise
+ */
+ public float getFloatValue() {
+ try {
+ if (mValueType == FLOAT_VALUE_TYPE) return (Float) mValue;
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ }
+ return 0;
+ }
+
+ /**
+ * Retrieve the tuple, in the form of a {@link List} of {@link StatsDimensionsValue}, held,
+ * if any.
+ *
+ * @return the {@link List} of {@link StatsDimensionsValue} held
+ * if {@link #getValueType()} == {@link #TUPLE_VALUE_TYPE},
+ * null otherwise
+ */
+ public List<StatsDimensionsValue> getTupleValueList() {
+ if (mValueType != TUPLE_VALUE_TYPE) {
+ return null;
+ }
+ try {
+ StatsDimensionsValue[] orig = (StatsDimensionsValue[]) mValue;
+ List<StatsDimensionsValue> copy = new ArrayList<>(orig.length);
+ // Shallow copy since StatsDimensionsValue is immutable anyway
+ for (int i = 0; i < orig.length; i++) {
+ copy.add(orig[i]);
+ }
+ return copy;
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ return null;
+ }
+ }
+
+ /**
+ * Returns the constant representing the type of value stored, namely one of
+ * <ul>
+ * <li>{@link #STRING_VALUE_TYPE}</li>
+ * <li>{@link #INT_VALUE_TYPE}</li>
+ * <li>{@link #LONG_VALUE_TYPE}</li>
+ * <li>{@link #BOOLEAN_VALUE_TYPE}</li>
+ * <li>{@link #FLOAT_VALUE_TYPE}</li>
+ * <li>{@link #TUPLE_VALUE_TYPE}</li>
+ * </ul>
+ *
+ * @return the constant representing the type of value stored
+ */
+ public int getValueType() {
+ return mValueType;
+ }
+
+ /**
+ * Returns whether the type of value stored is equal to the given type.
+ *
+ * @param valueType int representing the type of value stored, as used in {@link #getValueType}
+ * @return true if {@link #getValueType()} is equal to {@code valueType}.
+ */
+ public boolean isValueType(int valueType) {
+ return mValueType == valueType;
+ }
+
+ /**
+ * Returns a String representing the information in this StatsDimensionValue.
+ * No guarantees are made about the format of this String.
+ *
+ * @return String representation
+ *
+ * @hide
+ */
+ // Follows the format of statsd's dimension.h toString.
+ public String toString() {
+ try {
+ StringBuilder sb = new StringBuilder();
+ sb.append(mField);
+ sb.append(":");
+ if (mValueType == TUPLE_VALUE_TYPE) {
+ sb.append("{");
+ StatsDimensionsValue[] sbvs = (StatsDimensionsValue[]) mValue;
+ for (int i = 0; i < sbvs.length; i++) {
+ sb.append(sbvs[i].toString());
+ sb.append("|");
+ }
+ sb.append("}");
+ } else {
+ sb.append(mValue.toString());
+ }
+ return sb.toString();
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "Failed to successfully get value", e);
+ }
+ return "";
+ }
+
+ /**
+ * Parcelable Creator for StatsDimensionsValue.
+ */
+ public static final Parcelable.Creator<StatsDimensionsValue> CREATOR = new
+ Parcelable.Creator<StatsDimensionsValue>() {
+ public StatsDimensionsValue createFromParcel(Parcel in) {
+ return new StatsDimensionsValue(in);
+ }
+
+ public StatsDimensionsValue[] newArray(int size) {
+ return new StatsDimensionsValue[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mField);
+ out.writeInt(mValueType);
+ writeValueToParcel(mValueType, mValue, out, flags);
+ }
+
+ /** Writes mValue to a parcel. Returns true if succeeds. */
+ private static boolean writeValueToParcel(int valueType, Object value, Parcel out, int flags) {
+ try {
+ switch (valueType) {
+ case STRING_VALUE_TYPE:
+ out.writeString((String) value);
+ return true;
+ case INT_VALUE_TYPE:
+ out.writeInt((Integer) value);
+ return true;
+ case LONG_VALUE_TYPE:
+ out.writeLong((Long) value);
+ return true;
+ case BOOLEAN_VALUE_TYPE:
+ out.writeBoolean((Boolean) value);
+ return true;
+ case FLOAT_VALUE_TYPE:
+ out.writeFloat((Float) value);
+ return true;
+ case TUPLE_VALUE_TYPE: {
+ StatsDimensionsValue[] values = (StatsDimensionsValue[]) value;
+ out.writeInt(values.length);
+ for (int i = 0; i < values.length; i++) {
+ values[i].writeToParcel(out, flags);
+ }
+ return true;
+ }
+ default:
+ Slog.w(TAG, "readValue of an impossible type " + valueType);
+ return false;
+ }
+ } catch (ClassCastException e) {
+ Slog.w(TAG, "writeValue cast failed", e);
+ return false;
+ }
+ }
+
+ /** Reads mValue from a parcel. */
+ private static Object readValueFromParcel(int valueType, Parcel parcel) {
+ switch (valueType) {
+ case STRING_VALUE_TYPE:
+ return parcel.readString();
+ case INT_VALUE_TYPE:
+ return parcel.readInt();
+ case LONG_VALUE_TYPE:
+ return parcel.readLong();
+ case BOOLEAN_VALUE_TYPE:
+ return parcel.readBoolean();
+ case FLOAT_VALUE_TYPE:
+ return parcel.readFloat();
+ case TUPLE_VALUE_TYPE: {
+ final int sz = parcel.readInt();
+ StatsDimensionsValue[] values = new StatsDimensionsValue[sz];
+ for (int i = 0; i < sz; i++) {
+ values[i] = new StatsDimensionsValue(parcel);
+ }
+ return values;
+ }
+ default:
+ Slog.w(TAG, "readValue of an impossible type " + valueType);
+ return null;
+ }
+ }
+}
diff --git a/android/os/SystemProperties.java b/android/os/SystemProperties.java
index 4f6d322b..a9b86752 100644
--- a/android/os/SystemProperties.java
+++ b/android/os/SystemProperties.java
@@ -18,6 +18,7 @@ package android.os;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.SystemApi;
import android.util.Log;
import android.util.MutableInt;
@@ -33,6 +34,7 @@ import java.util.HashMap;
*
* {@hide}
*/
+@SystemApi
public class SystemProperties {
private static final String TAG = "SystemProperties";
private static final boolean TRACK_KEY_ACCESS = false;
@@ -40,9 +42,11 @@ public class SystemProperties {
/**
* Android O removed the property name length limit, but com.amazon.kindle 7.8.1.5
* uses reflection to read this whenever text is selected (http://b/36095274).
+ * @hide
*/
public static final int PROP_NAME_MAX = Integer.MAX_VALUE;
+ /** @hide */
public static final int PROP_VALUE_MAX = 91;
@GuardedBy("sChangeCallbacks")
@@ -86,8 +90,10 @@ public class SystemProperties {
*
* @param key the key to lookup
* @return an empty string if the {@code key} isn't found
+ * @hide
*/
@NonNull
+ @SystemApi
public static String get(@NonNull String key) {
if (TRACK_KEY_ACCESS) onKeyAccess(key);
return native_get(key);
@@ -100,8 +106,10 @@ public class SystemProperties {
* @param def the default value in case the property is not set or empty
* @return if the {@code key} isn't found, return {@code def} if it isn't null, or an empty
* string otherwise
+ * @hide
*/
@NonNull
+ @SystemApi
public static String get(@NonNull String key, @Nullable String def) {
if (TRACK_KEY_ACCESS) onKeyAccess(key);
return native_get(key, def);
@@ -114,7 +122,9 @@ public class SystemProperties {
* @param def a default value to return
* @return the key parsed as an integer, or def if the key isn't found or
* cannot be parsed
+ * @hide
*/
+ @SystemApi
public static int getInt(@NonNull String key, int def) {
if (TRACK_KEY_ACCESS) onKeyAccess(key);
return native_get_int(key, def);
@@ -127,7 +137,9 @@ public class SystemProperties {
* @param def a default value to return
* @return the key parsed as a long, or def if the key isn't found or
* cannot be parsed
+ * @hide
*/
+ @SystemApi
public static long getLong(@NonNull String key, long def) {
if (TRACK_KEY_ACCESS) onKeyAccess(key);
return native_get_long(key, def);
@@ -145,7 +157,9 @@ public class SystemProperties {
* @param def a default value to return
* @return the key parsed as a boolean, or def if the key isn't found or is
* not able to be parsed as a boolean.
+ * @hide
*/
+ @SystemApi
public static boolean getBoolean(@NonNull String key, boolean def) {
if (TRACK_KEY_ACCESS) onKeyAccess(key);
return native_get_boolean(key, def);
@@ -155,6 +169,7 @@ public class SystemProperties {
* Set the value for the given {@code key} to {@code val}.
*
* @throws IllegalArgumentException if the {@code val} exceeds 91 characters
+ * @hide
*/
public static void set(@NonNull String key, @Nullable String val) {
if (val != null && !val.startsWith("ro.") && val.length() > PROP_VALUE_MAX) {
@@ -170,6 +185,7 @@ public class SystemProperties {
*
* @param callback The {@link Runnable} that should be executed when a system property
* changes.
+ * @hide
*/
public static void addChangeCallback(@NonNull Runnable callback) {
synchronized (sChangeCallbacks) {
@@ -194,10 +210,14 @@ public class SystemProperties {
}
}
- /*
+ /**
* Notifies listeners that a system property has changed
+ * @hide
*/
public static void reportSyspropChanged() {
native_report_sysprop_change();
}
+
+ private SystemProperties() {
+ }
}
diff --git a/android/os/SystemUpdateManager.java b/android/os/SystemUpdateManager.java
new file mode 100644
index 00000000..ce3e2259
--- /dev/null
+++ b/android/os/SystemUpdateManager.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.os;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+
+/**
+ * Allows querying and posting system update information.
+ *
+ * {@hide}
+ */
+@SystemApi
+@SystemService(Context.SYSTEM_UPDATE_SERVICE)
+public class SystemUpdateManager {
+ private static final String TAG = "SystemUpdateManager";
+
+ /** The status key of the system update info, expecting an int value. */
+ @SystemApi
+ public static final String KEY_STATUS = "status";
+
+ /** The title of the current update, expecting a String value. */
+ @SystemApi
+ public static final String KEY_TITLE = "title";
+
+ /** Whether it is a security update, expecting a boolean value. */
+ @SystemApi
+ public static final String KEY_IS_SECURITY_UPDATE = "is_security_update";
+
+ /** The build fingerprint after installing the current update, expecting a String value. */
+ @SystemApi
+ public static final String KEY_TARGET_BUILD_FINGERPRINT = "target_build_fingerprint";
+
+ /** The security patch level after installing the current update, expecting a String value. */
+ @SystemApi
+ public static final String KEY_TARGET_SECURITY_PATCH_LEVEL = "target_security_patch_level";
+
+ /**
+ * The KEY_STATUS value that indicates there's no update status info available.
+ */
+ @SystemApi
+ public static final int STATUS_UNKNOWN = 0;
+
+ /**
+ * The KEY_STATUS value that indicates there's no pending update.
+ */
+ @SystemApi
+ public static final int STATUS_IDLE = 1;
+
+ /**
+ * The KEY_STATUS value that indicates an update is available for download, but pending user
+ * approval to start.
+ */
+ @SystemApi
+ public static final int STATUS_WAITING_DOWNLOAD = 2;
+
+ /**
+ * The KEY_STATUS value that indicates an update is in progress (i.e. downloading or installing
+ * has started).
+ */
+ @SystemApi
+ public static final int STATUS_IN_PROGRESS = 3;
+
+ /**
+ * The KEY_STATUS value that indicates an update is available for install.
+ */
+ @SystemApi
+ public static final int STATUS_WAITING_INSTALL = 4;
+
+ /**
+ * The KEY_STATUS value that indicates an update will be installed after a reboot. This applies
+ * to both of A/B and non-A/B OTAs.
+ */
+ @SystemApi
+ public static final int STATUS_WAITING_REBOOT = 5;
+
+ private final ISystemUpdateManager mService;
+
+ /** @hide */
+ public SystemUpdateManager(ISystemUpdateManager service) {
+ mService = checkNotNull(service, "missing ISystemUpdateManager");
+ }
+
+ /**
+ * Queries the current pending system update info.
+ *
+ * <p>Requires the {@link android.Manifest.permission#READ_SYSTEM_UPDATE_INFO} or
+ * {@link android.Manifest.permission#RECOVERY} permission.
+ *
+ * @return A {@code Bundle} that contains the pending system update information in key-value
+ * pairs.
+ *
+ * @throws SecurityException if the caller is not allowed to read the info.
+ */
+ @SystemApi
+ @RequiresPermission(anyOf = {
+ android.Manifest.permission.READ_SYSTEM_UPDATE_INFO,
+ android.Manifest.permission.RECOVERY,
+ })
+ public Bundle retrieveSystemUpdateInfo() {
+ try {
+ return mService.retrieveSystemUpdateInfo();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Allows a system updater to publish the pending update info.
+ *
+ * <p>The reported info will not persist across reboots. Because only the reporting updater
+ * understands the criteria to determine a successful/failed update.
+ *
+ * <p>Requires the {@link android.Manifest.permission#RECOVERY} permission.
+ *
+ * @param infoBundle The {@code PersistableBundle} that contains the system update information,
+ * such as the current update status. {@link #KEY_STATUS} is required in the bundle.
+ *
+ * @throws IllegalArgumentException if @link #KEY_STATUS} does not exist.
+ * @throws SecurityException if the caller is not allowed to update the info.
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.RECOVERY)
+ public void updateSystemUpdateInfo(PersistableBundle infoBundle) {
+ if (infoBundle == null || !infoBundle.containsKey(KEY_STATUS)) {
+ throw new IllegalArgumentException("Missing status in the bundle");
+ }
+ try {
+ mService.updateSystemUpdateInfo(infoBundle);
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/os/UserHandle.java b/android/os/UserHandle.java
index 6381b56a..5be72bc5 100644
--- a/android/os/UserHandle.java
+++ b/android/os/UserHandle.java
@@ -126,7 +126,10 @@ public final class UserHandle implements Parcelable {
return getAppId(uid1) == getAppId(uid2);
}
- /** @hide */
+ /**
+ * Whether a UID is an "isolated" UID.
+ * @hide
+ */
public static boolean isIsolated(int uid) {
if (uid > 0) {
final int appId = getAppId(uid);
@@ -136,7 +139,11 @@ public final class UserHandle implements Parcelable {
}
}
- /** @hide */
+ /**
+ * Whether a UID belongs to a regular app. *Note* "Not a regular app" does not mean
+ * "it's system", because of isolated UIDs. Use {@link #isCore} for that.
+ * @hide
+ */
public static boolean isApp(int uid) {
if (uid > 0) {
final int appId = getAppId(uid);
@@ -147,6 +154,19 @@ public final class UserHandle implements Parcelable {
}
/**
+ * Whether a UID belongs to a system core component or not.
+ * @hide
+ */
+ public static boolean isCore(int uid) {
+ if (uid > 0) {
+ final int appId = getAppId(uid);
+ return appId < Process.FIRST_APPLICATION_UID;
+ } else {
+ return false;
+ }
+ }
+
+ /**
* Returns the user for a given uid.
* @param uid A uid for an application running in a particular user.
* @return A {@link UserHandle} for that user.
diff --git a/android/os/UserManager.java b/android/os/UserManager.java
index dd9fd93e..13b5b5c9 100644
--- a/android/os/UserManager.java
+++ b/android/os/UserManager.java
@@ -209,6 +209,49 @@ public class UserManager {
public static final String DISALLOW_AIRPLANE_MODE = "no_airplane_mode";
/**
+ * Specifies if a user is disallowed from configuring brightness. When device owner sets it,
+ * it'll only be applied on the target(system) user.
+ *
+ * <p>The default value is <code>false</code>.
+ *
+ * <p>This user restriction has no effect on managed profiles.
+ * <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_BRIGHTNESS = "no_config_brightness";
+
+ /**
+ * Specifies if ambient display is disallowed for the user.
+ *
+ * <p>The default value is <code>false</code>.
+ *
+ * <p>This user restriction has no effect on managed profiles.
+ * <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_AMBIENT_DISPLAY = "no_ambient_display";
+
+ /**
+ * Specifies if a user is disallowed from changing screen off timeout.
+ *
+ * <p>The default value is <code>false</code>.
+ *
+ * <p>This user restriction has no effect on managed profiles.
+ * <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_SCREEN_TIMEOUT = "no_config_screen_timeout";
+
+ /**
* 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>.
@@ -748,6 +791,7 @@ public class UserManager {
* @see #getUserRestrictions()
* @hide
*/
+ @SystemApi
public static final String DISALLOW_RUN_IN_BACKGROUND = "no_run_in_background";
/**
@@ -877,6 +921,27 @@ public class UserManager {
public static final String DISALLOW_USER_SWITCH = "no_user_switch";
/**
+ * Specifies whether the user can share file / picture / data from the primary user into the
+ * managed profile, either by sending them from the primary side, or by picking up data within
+ * an app in the managed profile.
+ * <p>
+ * When a managed profile is created, the system allows the user to send data from the primary
+ * side to the profile by setting up certain default cross profile intent filters. If
+ * this is undesired, this restriction can be set to disallow it. Note that this restriction
+ * will not block any sharing allowed by explicit
+ * {@link DevicePolicyManager#addCrossProfileIntentFilter} calls by the profile owner.
+ * <p>
+ * This restriction is only meaningful when set by profile owner. When it is set by device
+ * owner, it does not have any effect.
+ * <p>
+ * The default value is <code>false</code>.
+ *
+ * @see DevicePolicyManager#addUserRestriction(ComponentName, String)
+ * @see DevicePolicyManager#clearUserRestriction(ComponentName, String)
+ * @see #getUserRestrictions()
+ */
+ public static final String DISALLOW_SHARE_INTO_MANAGED_PROFILE = "no_sharing_into_profile";
+ /**
* Application restriction key that is used to indicate the pending arrival
* of real restrictions for the app.
*
@@ -1392,6 +1457,34 @@ public class UserManager {
}
/**
+ * Return the time when the calling user started in elapsed milliseconds since boot,
+ * or 0 if not started.
+ *
+ * @hide
+ */
+ public long getUserStartRealtime() {
+ try {
+ return mService.getUserStartRealtime();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Return the time when the calling user was unlocked elapsed milliseconds since boot,
+ * or 0 if not unlocked.
+ *
+ * @hide
+ */
+ public long getUserUnlockRealtime() {
+ try {
+ return mService.getUserUnlockRealtime();
+ } catch (RemoteException re) {
+ throw re.rethrowFromSystemServer();
+ }
+ }
+
+ /**
* Returns the UserInfo object describing a specific user.
* Requires {@link android.Manifest.permission#MANAGE_USERS} permission.
* @param userHandle the user handle of the user whose information is being requested.
@@ -2166,6 +2259,12 @@ public class UserManager {
}
}
+ /** @removed */
+ @Deprecated
+ public boolean trySetQuietModeEnabled(boolean enableQuietMode, @NonNull UserHandle userHandle) {
+ return requestQuietModeEnabled(enableQuietMode, userHandle);
+ }
+
/**
* 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.
@@ -2191,21 +2290,22 @@ public class UserManager {
*
* @see #isQuietModeEnabled(UserHandle)
*/
- public boolean trySetQuietModeEnabled(boolean enableQuietMode, @NonNull UserHandle userHandle) {
- return trySetQuietModeEnabled(enableQuietMode, userHandle, null);
+ public boolean requestQuietModeEnabled(boolean enableQuietMode, @NonNull UserHandle userHandle) {
+ return requestQuietModeEnabled(enableQuietMode, userHandle, null);
}
/**
- * Similar to {@link #trySetQuietModeEnabled(boolean, UserHandle)}, except you can specify
- * a target to start when user is unlocked.
+ * Similar to {@link #requestQuietModeEnabled(boolean, UserHandle)}, except you can specify
+ * a target to start when user is unlocked. If {@code target} is specified, caller must have
+ * the {@link android.Manifest.permission#MANAGE_USERS} permission.
*
- * @see {@link #trySetQuietModeEnabled(boolean, UserHandle)}
+ * @see {@link #requestQuietModeEnabled(boolean, UserHandle)}
* @hide
*/
- public boolean trySetQuietModeEnabled(
+ public boolean requestQuietModeEnabled(
boolean enableQuietMode, @NonNull UserHandle userHandle, IntentSender target) {
try {
- return mService.trySetQuietModeEnabled(
+ return mService.requestQuietModeEnabled(
mContext.getPackageName(), enableQuietMode, userHandle.getIdentifier(), target);
} catch (RemoteException re) {
throw re.rethrowFromSystemServer();
diff --git a/android/os/VintfObject.java b/android/os/VintfObject.java
index 340f3fb8..12a495bf 100644
--- a/android/os/VintfObject.java
+++ b/android/os/VintfObject.java
@@ -76,8 +76,8 @@ public class VintfObject {
/**
* @return a list of VNDK snapshots supported by the framework, as
* specified in framework manifest. For example,
- * [("25.0.5", ["libjpeg.so", "libbase.so"]),
- * ("25.1.3", ["libjpeg.so", "libbase.so"])]
+ * [("27", ["libjpeg.so", "libbase.so"]),
+ * ("28", ["libjpeg.so", "libbase.so"])]
*/
public static native Map<String, String[]> getVndkSnapshots();
}
diff --git a/android/os/WorkSource.java b/android/os/WorkSource.java
index 401b4a36..d0c2870b 100644
--- a/android/os/WorkSource.java
+++ b/android/os/WorkSource.java
@@ -7,7 +7,6 @@ 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.
@@ -162,9 +161,21 @@ public class WorkSource implements Parcelable {
@Override
public boolean equals(Object o) {
- return o instanceof WorkSource
- && !diff((WorkSource) o)
- && Objects.equals(mChains, ((WorkSource) o).mChains);
+ if (o instanceof WorkSource) {
+ WorkSource other = (WorkSource) o;
+
+ if (diff(other)) {
+ return false;
+ }
+
+ if (mChains != null && !mChains.isEmpty()) {
+ return mChains.equals(other.mChains);
+ } else {
+ return other.mChains == null || other.mChains.isEmpty();
+ }
+ }
+
+ return false;
}
@Override
@@ -407,11 +418,11 @@ public class WorkSource implements Parcelable {
}
public boolean remove(WorkSource other) {
- if (mNum <= 0 || other.mNum <= 0) {
+ if (isEmpty() || other.isEmpty()) {
return false;
}
- boolean uidRemoved = false;
+ boolean uidRemoved;
if (mNames == null && other.mNames == null) {
uidRemoved = removeUids(other);
} else {
@@ -427,13 +438,8 @@ public class WorkSource implements Parcelable {
}
boolean chainRemoved = false;
- if (other.mChains != null) {
- if (mChains != null) {
- chainRemoved = mChains.removeAll(other.mChains);
- }
- } else if (mChains != null) {
- mChains.clear();
- chainRemoved = true;
+ if (other.mChains != null && mChains != null) {
+ chainRemoved = mChains.removeAll(other.mChains);
}
return uidRemoved || chainRemoved;
diff --git a/android/os/connectivity/GpsBatteryStats.java b/android/os/connectivity/GpsBatteryStats.java
new file mode 100644
index 00000000..f2ac5ef6
--- /dev/null
+++ b/android/os/connectivity/GpsBatteryStats.java
@@ -0,0 +1,108 @@
+/*
+ * 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.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.location.gnssmetrics.GnssMetrics;
+
+import java.util.Arrays;
+
+/**
+ * API for GPS power stats
+ *
+ * @hide
+ */
+public final class GpsBatteryStats implements Parcelable {
+
+ private long mLoggingDurationMs;
+ private long mEnergyConsumedMaMs;
+ private long[] mTimeInGpsSignalQualityLevel;
+
+ public static final Parcelable.Creator<GpsBatteryStats> CREATOR = new
+ Parcelable.Creator<GpsBatteryStats>() {
+ public GpsBatteryStats createFromParcel(Parcel in) {
+ return new GpsBatteryStats(in);
+ }
+
+ public GpsBatteryStats[] newArray(int size) {
+ return new GpsBatteryStats[size];
+ }
+ };
+
+ public GpsBatteryStats() {
+ initialize();
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeLong(mLoggingDurationMs);
+ out.writeLong(mEnergyConsumedMaMs);
+ out.writeLongArray(mTimeInGpsSignalQualityLevel);
+ }
+
+ public void readFromParcel(Parcel in) {
+ mLoggingDurationMs = in.readLong();
+ mEnergyConsumedMaMs = in.readLong();
+ in.readLongArray(mTimeInGpsSignalQualityLevel);
+ }
+
+ public long getLoggingDurationMs() {
+ return mLoggingDurationMs;
+ }
+
+ public long getEnergyConsumedMaMs() {
+ return mEnergyConsumedMaMs;
+ }
+
+ public long[] getTimeInGpsSignalQualityLevel() {
+ return mTimeInGpsSignalQualityLevel;
+ }
+
+ public void setLoggingDurationMs(long t) {
+ mLoggingDurationMs = t;
+ return;
+ }
+
+ public void setEnergyConsumedMaMs(long e) {
+ mEnergyConsumedMaMs = e;
+ return;
+ }
+
+ public void setTimeInGpsSignalQualityLevel(long[] t) {
+ mTimeInGpsSignalQualityLevel = Arrays.copyOfRange(t, 0,
+ Math.min(t.length, GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS));
+ return;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private GpsBatteryStats(Parcel in) {
+ initialize();
+ readFromParcel(in);
+ }
+
+ private void initialize() {
+ mLoggingDurationMs = 0;
+ mEnergyConsumedMaMs = 0;
+ mTimeInGpsSignalQualityLevel = new long[GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS];
+ return;
+ }
+} \ No newline at end of file
diff --git a/android/os/connectivity/WifiBatteryStats.java b/android/os/connectivity/WifiBatteryStats.java
new file mode 100644
index 00000000..e5341eee
--- /dev/null
+++ b/android/os/connectivity/WifiBatteryStats.java
@@ -0,0 +1,279 @@
+/*
+ * 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 java.util.Arrays;
+
+/**
+ * API for Wifi power stats
+ *
+ * @hide
+ */
+public final class WifiBatteryStats 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 mScanTimeMs;
+ private long mIdleTimeMs;
+ private long mRxTimeMs;
+ private long mTxTimeMs;
+ private long mEnergyConsumedMaMs;
+ private long mNumAppScanRequest;
+ private long[] mTimeInStateMs;
+ private long[] mTimeInSupplicantStateMs;
+ private long[] mTimeInRxSignalStrengthLevelMs;
+
+ public static final Parcelable.Creator<WifiBatteryStats> CREATOR = new
+ Parcelable.Creator<WifiBatteryStats>() {
+ public WifiBatteryStats createFromParcel(Parcel in) {
+ return new WifiBatteryStats(in);
+ }
+
+ public WifiBatteryStats[] newArray(int size) {
+ return new WifiBatteryStats[size];
+ }
+ };
+
+ public WifiBatteryStats() {
+ 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(mScanTimeMs);
+ out.writeLong(mIdleTimeMs);
+ out.writeLong(mRxTimeMs);
+ out.writeLong(mTxTimeMs);
+ out.writeLong(mEnergyConsumedMaMs);
+ out.writeLong(mNumAppScanRequest);
+ out.writeLongArray(mTimeInStateMs);
+ out.writeLongArray(mTimeInRxSignalStrengthLevelMs);
+ out.writeLongArray(mTimeInSupplicantStateMs);
+ }
+
+ 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();
+ mScanTimeMs = in.readLong();
+ mIdleTimeMs = in.readLong();
+ mRxTimeMs = in.readLong();
+ mTxTimeMs = in.readLong();
+ mEnergyConsumedMaMs = in.readLong();
+ mNumAppScanRequest = in.readLong();
+ in.readLongArray(mTimeInStateMs);
+ in.readLongArray(mTimeInRxSignalStrengthLevelMs);
+ in.readLongArray(mTimeInSupplicantStateMs);
+ }
+
+ 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 getScanTimeMs() {
+ return mScanTimeMs;
+ }
+
+ public long getIdleTimeMs() {
+ return mIdleTimeMs;
+ }
+
+ public long getRxTimeMs() {
+ return mRxTimeMs;
+ }
+
+ public long getTxTimeMs() {
+ return mTxTimeMs;
+ }
+
+ public long getEnergyConsumedMaMs() {
+ return mEnergyConsumedMaMs;
+ }
+
+ public long getNumAppScanRequest() {
+ return mNumAppScanRequest;
+ }
+
+ public long[] getTimeInStateMs() {
+ return mTimeInStateMs;
+ }
+
+ public long[] getTimeInRxSignalStrengthLevelMs() {
+ return mTimeInRxSignalStrengthLevelMs;
+ }
+
+ public long[] getTimeInSupplicantStateMs() {
+ return mTimeInSupplicantStateMs;
+ }
+
+ 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 setScanTimeMs(long t) {
+ mScanTimeMs = t;
+ return;
+ }
+
+ public void setIdleTimeMs(long t) {
+ mIdleTimeMs = t;
+ return;
+ }
+
+ public void setRxTimeMs(long t) {
+ mRxTimeMs = t;
+ return;
+ }
+
+ public void setTxTimeMs(long t) {
+ mTxTimeMs = t;
+ return;
+ }
+
+ public void setEnergyConsumedMaMs(long e) {
+ mEnergyConsumedMaMs = e;
+ return;
+ }
+
+ public void setNumAppScanRequest(long n) {
+ mNumAppScanRequest = n;
+ return;
+ }
+
+ public void setTimeInStateMs(long[] t) {
+ mTimeInStateMs = Arrays.copyOfRange(t, 0,
+ Math.min(t.length, BatteryStats.NUM_WIFI_STATES));
+ return;
+ }
+
+ public void setTimeInRxSignalStrengthLevelMs(long[] t) {
+ mTimeInRxSignalStrengthLevelMs = Arrays.copyOfRange(t, 0,
+ Math.min(t.length, BatteryStats.NUM_WIFI_SIGNAL_STRENGTH_BINS));
+ return;
+ }
+
+ public void setTimeInSupplicantStateMs(long[] t) {
+ mTimeInSupplicantStateMs = Arrays.copyOfRange(
+ t, 0, Math.min(t.length, BatteryStats.NUM_WIFI_SUPPL_STATES));
+ return;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ private WifiBatteryStats(Parcel in) {
+ initialize();
+ readFromParcel(in);
+ }
+
+ private void initialize() {
+ mLoggingDurationMs = 0;
+ mKernelActiveTimeMs = 0;
+ mNumPacketsTx = 0;
+ mNumBytesTx = 0;
+ mNumPacketsRx = 0;
+ mNumBytesRx = 0;
+ mSleepTimeMs = 0;
+ mScanTimeMs = 0;
+ mIdleTimeMs = 0;
+ mRxTimeMs = 0;
+ mTxTimeMs = 0;
+ mEnergyConsumedMaMs = 0;
+ mNumAppScanRequest = 0;
+ mTimeInStateMs = new long[BatteryStats.NUM_WIFI_STATES];
+ Arrays.fill(mTimeInStateMs, 0);
+ mTimeInRxSignalStrengthLevelMs = new long[BatteryStats.NUM_WIFI_SIGNAL_STRENGTH_BINS];
+ Arrays.fill(mTimeInRxSignalStrengthLevelMs, 0);
+ mTimeInSupplicantStateMs = new long[BatteryStats.NUM_WIFI_SUPPL_STATES];
+ Arrays.fill(mTimeInSupplicantStateMs, 0);
+ return;
+ }
+} \ No newline at end of file
diff --git a/android/os/storage/StorageManager.java b/android/os/storage/StorageManager.java
index 4c587a83..f4deeeda 100644
--- a/android/os/storage/StorageManager.java
+++ b/android/os/storage/StorageManager.java
@@ -16,9 +16,6 @@
package android.os.storage;
-import static android.net.TrafficStats.GB_IN_BYTES;
-import static android.net.TrafficStats.MB_IN_BYTES;
-
import android.annotation.BytesLong;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -59,6 +56,7 @@ import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import android.text.TextUtils;
+import android.util.DataUnit;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
@@ -116,6 +114,8 @@ public class StorageManager {
/** {@hide} */
public static final String PROP_HAS_ADOPTABLE = "vold.has_adoptable";
/** {@hide} */
+ public static final String PROP_HAS_RESERVED = "vold.has_reserved";
+ /** {@hide} */
public static final String PROP_FORCE_ADOPTABLE = "persist.fw.force_adoptable";
/** {@hide} */
public static final String PROP_EMULATE_FBE = "persist.sys.emulate_fbe";
@@ -123,8 +123,6 @@ public class StorageManager {
public static final String PROP_SDCARDFS = "persist.sys.sdcardfs";
/** {@hide} */
public static final String PROP_VIRTUAL_DISK = "persist.sys.virtual_disk";
- /** {@hide} */
- public static final String PROP_ADOPTABLE_FBE = "persist.sys.adoptable_fbe";
/** {@hide} */
public static final String UUID_PRIVATE_INTERNAL = null;
@@ -1199,12 +1197,12 @@ public class StorageManager {
}
private static final int DEFAULT_THRESHOLD_PERCENTAGE = 5;
- private static final long DEFAULT_THRESHOLD_MAX_BYTES = 500 * MB_IN_BYTES;
+ private static final long DEFAULT_THRESHOLD_MAX_BYTES = DataUnit.MEBIBYTES.toBytes(500);
private static final int DEFAULT_CACHE_PERCENTAGE = 10;
- private static final long DEFAULT_CACHE_MAX_BYTES = 5 * GB_IN_BYTES;
+ private static final long DEFAULT_CACHE_MAX_BYTES = DataUnit.GIBIBYTES.toBytes(5);
- private static final long DEFAULT_FULL_THRESHOLD_BYTES = MB_IN_BYTES;
+ private static final long DEFAULT_FULL_THRESHOLD_BYTES = DataUnit.MEBIBYTES.toBytes(1);
/**
* Return the number of available bytes until the given path is considered
@@ -1476,6 +1474,11 @@ public class StorageManager {
}
/** {@hide} */
+ public static boolean hasAdoptable() {
+ return SystemProperties.getBoolean(PROP_HAS_ADOPTABLE, false);
+ }
+
+ /** {@hide} */
public static File maybeTranslateEmulatedPathToInternal(File path) {
// Disabled now that FUSE has been replaced by sdcardfs
return path;
diff --git a/android/os/storage/StorageVolume.java b/android/os/storage/StorageVolume.java
index 070b8c1b..839a8bf4 100644
--- a/android/os/storage/StorageVolume.java
+++ b/android/os/storage/StorageVolume.java
@@ -394,4 +394,32 @@ public final class StorageVolume implements Parcelable {
parcel.writeString(mFsUuid);
parcel.writeString(mState);
}
+
+ /** {@hide} */
+ public static final class ScopedAccessProviderContract {
+
+ private ScopedAccessProviderContract() {
+ throw new UnsupportedOperationException("contains constants only");
+ }
+
+ public static final String AUTHORITY = "com.android.documentsui.scopedAccess";
+
+ public static final String TABLE_PACKAGES = "packages";
+ public static final String TABLE_PERMISSIONS = "permissions";
+
+ public static final String COL_PACKAGE = "package_name";
+ public static final String COL_VOLUME_UUID = "volume_uuid";
+ public static final String COL_DIRECTORY = "directory";
+ public static final String COL_GRANTED = "granted";
+
+ public static final String[] TABLE_PACKAGES_COLUMNS = new String[] { COL_PACKAGE };
+ public static final String[] TABLE_PERMISSIONS_COLUMNS =
+ new String[] { COL_PACKAGE, COL_VOLUME_UUID, COL_DIRECTORY, COL_GRANTED };
+
+ public static final int TABLE_PACKAGES_COL_PACKAGE = 0;
+ public static final int TABLE_PERMISSIONS_COL_PACKAGE = 0;
+ public static final int TABLE_PERMISSIONS_COL_VOLUME_UUID = 1;
+ public static final int TABLE_PERMISSIONS_COL_DIRECTORY = 2;
+ public static final int TABLE_PERMISSIONS_COL_GRANTED = 3;
+ }
}
diff --git a/android/perftests/utils/BenchmarkState.java b/android/perftests/utils/BenchmarkState.java
index bb9dc4ae..da17818b 100644
--- a/android/perftests/utils/BenchmarkState.java
+++ b/android/perftests/utils/BenchmarkState.java
@@ -25,7 +25,6 @@ import android.util.Log;
import java.io.File;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
@@ -78,10 +77,7 @@ public final class BenchmarkState {
// Statistics. These values will be filled when the benchmark has finished.
// The computation needs double precision, but long int is fine for final reporting.
- private long mMedian = 0;
- private double mMean = 0.0;
- private double mStandardDeviation = 0.0;
- private long mMin = 0;
+ private Stats mStats;
// Individual duration in nano seconds.
private ArrayList<Long> mResults = new ArrayList<>();
@@ -90,36 +86,6 @@ public final class BenchmarkState {
return TimeUnit.MILLISECONDS.toNanos(ms);
}
- /**
- * Calculates statistics.
- */
- private void calculateSatistics() {
- final int size = mResults.size();
- if (size <= 1) {
- throw new IllegalStateException("At least two results are necessary.");
- }
-
- Collections.sort(mResults);
- mMedian = size % 2 == 0 ? (mResults.get(size / 2) + mResults.get(size / 2 + 1)) / 2 :
- mResults.get(size / 2);
-
- mMin = mResults.get(0);
- for (int i = 0; i < size; ++i) {
- long result = mResults.get(i);
- mMean += result;
- if (result < mMin) {
- mMin = result;
- }
- }
- mMean /= (double) size;
-
- for (int i = 0; i < size; ++i) {
- final double tmp = mResults.get(i) - mMean;
- mStandardDeviation += tmp * tmp;
- }
- mStandardDeviation = Math.sqrt(mStandardDeviation / (double) (size - 1));
- }
-
// Stops the benchmark timer.
// This method can be called only when the timer is running.
public void pauseTiming() {
@@ -173,7 +139,7 @@ public final class BenchmarkState {
if (ENABLE_PROFILING) {
Debug.stopMethodTracing();
}
- calculateSatistics();
+ mStats = new Stats(mResults);
mState = FINISHED;
return false;
}
@@ -224,28 +190,28 @@ public final class BenchmarkState {
if (mState != FINISHED) {
throw new IllegalStateException("The benchmark hasn't finished");
}
- return (long) mMean;
+ return (long) mStats.getMean();
}
private long median() {
if (mState != FINISHED) {
throw new IllegalStateException("The benchmark hasn't finished");
}
- return mMedian;
+ return mStats.getMedian();
}
private long min() {
if (mState != FINISHED) {
throw new IllegalStateException("The benchmark hasn't finished");
}
- return mMin;
+ return mStats.getMin();
}
private long standardDeviation() {
if (mState != FINISHED) {
throw new IllegalStateException("The benchmark hasn't finished");
}
- return (long) mStandardDeviation;
+ return (long) mStats.getStandardDeviation();
}
private String summaryLine() {
diff --git a/android/perftests/utils/ManualBenchmarkState.java b/android/perftests/utils/ManualBenchmarkState.java
new file mode 100644
index 00000000..2c84db18
--- /dev/null
+++ b/android/perftests/utils/ManualBenchmarkState.java
@@ -0,0 +1,157 @@
+/*
+ * 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.perftests.utils;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.os.Bundle;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Provides a benchmark framework.
+ *
+ * This differs from BenchmarkState in that rather than the class measuring the the elapsed time,
+ * the test passes in the elapsed time.
+ *
+ * Example usage:
+ *
+ * public void sampleMethod() {
+ * ManualBenchmarkState state = new ManualBenchmarkState();
+ *
+ * int[] src = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
+ * long elapsedTime = 0;
+ * while (state.keepRunning(elapsedTime)) {
+ * long startTime = System.nanoTime();
+ * int[] dest = new int[src.length];
+ * System.arraycopy(src, 0, dest, 0, src.length);
+ * elapsedTime = System.nanoTime() - startTime;
+ * }
+ * System.out.println(state.summaryLine());
+ * }
+ *
+ * Or use the PerfManualStatusReporter TestRule.
+ *
+ * Make sure that the overhead of checking the clock does not noticeably affect the results.
+ */
+public final class ManualBenchmarkState {
+ private static final String TAG = ManualBenchmarkState.class.getSimpleName();
+
+ // TODO: Tune these values.
+ // warm-up for duration
+ private static final long WARMUP_DURATION_NS = TimeUnit.SECONDS.toNanos(5);
+ // minimum iterations to warm-up for
+ private static final int WARMUP_MIN_ITERATIONS = 8;
+
+ // target testing for duration
+ private static final long TARGET_TEST_DURATION_NS = TimeUnit.SECONDS.toNanos(16);
+ private static final int MAX_TEST_ITERATIONS = 1000000;
+ private static final int MIN_TEST_ITERATIONS = 10;
+
+ private static final int NOT_STARTED = 0; // The benchmark has not started yet.
+ private static final int WARMUP = 1; // The benchmark is warming up.
+ private static final int RUNNING = 2; // The benchmark is running.
+ private static final int FINISHED = 3; // The benchmark has stopped.
+
+ private int mState = NOT_STARTED; // Current benchmark state.
+
+ private long mWarmupStartTime = 0;
+ private int mWarmupIterations = 0;
+
+ private int mMaxIterations = 0;
+
+ // Individual duration in nano seconds.
+ private ArrayList<Long> mResults = new ArrayList<>();
+
+ // Statistics. These values will be filled when the benchmark has finished.
+ // The computation needs double precision, but long int is fine for final reporting.
+ private Stats mStats;
+
+ private void beginBenchmark(long warmupDuration, int iterations) {
+ mMaxIterations = (int) (TARGET_TEST_DURATION_NS / (warmupDuration / iterations));
+ mMaxIterations = Math.min(MAX_TEST_ITERATIONS,
+ Math.max(mMaxIterations, MIN_TEST_ITERATIONS));
+ mState = RUNNING;
+ }
+
+ /**
+ * Judges whether the benchmark needs more samples.
+ *
+ * For the usage, see class comment.
+ */
+ public boolean keepRunning(long duration) {
+ if (duration < 0) {
+ throw new RuntimeException("duration is negative: " + duration);
+ }
+ switch (mState) {
+ case NOT_STARTED:
+ mState = WARMUP;
+ mWarmupStartTime = System.nanoTime();
+ return true;
+ case WARMUP: {
+ final long timeSinceStartingWarmup = System.nanoTime() - mWarmupStartTime;
+ ++mWarmupIterations;
+ if (mWarmupIterations >= WARMUP_MIN_ITERATIONS
+ && timeSinceStartingWarmup >= WARMUP_DURATION_NS) {
+ beginBenchmark(timeSinceStartingWarmup, mWarmupIterations);
+ }
+ return true;
+ }
+ case RUNNING: {
+ mResults.add(duration);
+ final boolean keepRunning = mResults.size() < mMaxIterations;
+ if (!keepRunning) {
+ mStats = new Stats(mResults);
+ mState = FINISHED;
+ }
+ return keepRunning;
+ }
+ case FINISHED:
+ throw new IllegalStateException("The benchmark has finished.");
+ default:
+ throw new IllegalStateException("The benchmark is in an unknown state.");
+ }
+ }
+
+ private String summaryLine() {
+ final StringBuilder sb = new StringBuilder();
+ sb.append("Summary: ");
+ sb.append("median=").append(mStats.getMedian()).append("ns, ");
+ sb.append("mean=").append(mStats.getMean()).append("ns, ");
+ sb.append("min=").append(mStats.getMin()).append("ns, ");
+ sb.append("max=").append(mStats.getMax()).append("ns, ");
+ sb.append("sigma=").append(mStats.getStandardDeviation()).append(", ");
+ sb.append("iteration=").append(mResults.size()).append(", ");
+ sb.append("values=").append(mResults.toString());
+ return sb.toString();
+ }
+
+ public void sendFullStatusReport(Instrumentation instrumentation, String key) {
+ if (mState != FINISHED) {
+ throw new IllegalStateException("The benchmark hasn't finished");
+ }
+ Log.i(TAG, key + summaryLine());
+ final Bundle status = new Bundle();
+ status.putLong(key + "_median", mStats.getMedian());
+ status.putLong(key + "_mean", (long) mStats.getMean());
+ status.putLong(key + "_stddev", (long) mStats.getStandardDeviation());
+ instrumentation.sendStatus(Activity.RESULT_OK, status);
+ }
+}
+
diff --git a/android/perftests/utils/PerfManualStatusReporter.java b/android/perftests/utils/PerfManualStatusReporter.java
new file mode 100644
index 00000000..0de6f1d9
--- /dev/null
+++ b/android/perftests/utils/PerfManualStatusReporter.java
@@ -0,0 +1,73 @@
+/*
+ * 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.perftests.utils;
+
+import android.support.test.InstrumentationRegistry;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ * Use this rule to make sure we report the status after the test success.
+ *
+ * <code>
+ *
+ * @Rule public PerfManualStatusReporter mPerfStatusReporter = new PerfManualStatusReporter();
+ * @Test public void functionName() {
+ * ManualBenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ *
+ * int[] src = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
+ * long elapsedTime = 0;
+ * while (state.keepRunning(elapsedTime)) {
+ * long startTime = System.nanoTime();
+ * int[] dest = new int[src.length];
+ * System.arraycopy(src, 0, dest, 0, src.length);
+ * elapsedTime = System.nanoTime() - startTime;
+ * }
+ * }
+ * </code>
+ *
+ * When test succeeded, the status report will use the key as
+ * "functionName_*"
+ */
+
+public class PerfManualStatusReporter implements TestRule {
+ private final ManualBenchmarkState mState;
+
+ public PerfManualStatusReporter() {
+ mState = new ManualBenchmarkState();
+ }
+
+ public ManualBenchmarkState getBenchmarkState() {
+ return mState;
+ }
+
+ @Override
+ public Statement apply(Statement base, Description description) {
+ return new Statement() {
+ @Override
+ public void evaluate() throws Throwable {
+ base.evaluate();
+
+ mState.sendFullStatusReport(InstrumentationRegistry.getInstrumentation(),
+ description.getMethodName());
+ }
+ };
+ }
+}
+
diff --git a/android/perftests/utils/Stats.java b/android/perftests/utils/Stats.java
new file mode 100644
index 00000000..acc44a8f
--- /dev/null
+++ b/android/perftests/utils/Stats.java
@@ -0,0 +1,76 @@
+/*
+ * 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.perftests.utils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class Stats {
+ private long mMedian, mMin, mMax;
+ private double mMean, mStandardDeviation;
+
+ /* Calculate stats in constructor. */
+ public Stats(List<Long> values) {
+ // make a copy since we're modifying it
+ values = new ArrayList<>(values);
+ final int size = values.size();
+ if (size < 2) {
+ throw new IllegalArgumentException("At least two results are necessary.");
+ }
+
+ Collections.sort(values);
+
+ mMedian = size % 2 == 0 ? (values.get(size / 2) + values.get(size / 2 - 1)) / 2 :
+ values.get(size / 2);
+
+ mMin = values.get(0);
+ mMax = values.get(values.size() - 1);
+
+ for (int i = 0; i < size; ++i) {
+ long result = values.get(i);
+ mMean += result;
+ }
+ mMean /= (double) size;
+
+ for (int i = 0; i < size; ++i) {
+ final double tmp = values.get(i) - mMean;
+ mStandardDeviation += tmp * tmp;
+ }
+ mStandardDeviation = Math.sqrt(mStandardDeviation / (double) (size - 1));
+ }
+
+ public double getMean() {
+ return mMean;
+ }
+
+ public long getMedian() {
+ return mMedian;
+ }
+
+ public long getMax() {
+ return mMax;
+ }
+
+ public long getMin() {
+ return mMin;
+ }
+
+ public double getStandardDeviation() {
+ return mStandardDeviation;
+ }
+}
diff --git a/android/privacy/internal/rappor/RapporEncoder.java b/android/privacy/internal/rappor/RapporEncoder.java
index 2eca4c98..9ac2b3e1 100644
--- a/android/privacy/internal/rappor/RapporEncoder.java
+++ b/android/privacy/internal/rappor/RapporEncoder.java
@@ -33,7 +33,6 @@ import java.util.Random;
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,
@@ -66,8 +65,8 @@ public class RapporEncoder implements DifferentialPrivacyEncoder {
// Use SecureRandom as random generator.
random = sSecureRandom;
} else {
- // Hard-coded random generator, to have deterministic result.
- random = new Random(INSECURE_RANDOM_SEED);
+ // To have deterministic result by hard coding encoder id as seed.
+ random = new Random((long) config.mEncoderId.hashCode());
userSecret = INSECURE_SECRET;
}
mEncoder = new Encoder(random, null, null,
diff --git a/android/provider/AlarmClock.java b/android/provider/AlarmClock.java
index 21694575..7ad9e013 100644
--- a/android/provider/AlarmClock.java
+++ b/android/provider/AlarmClock.java
@@ -154,9 +154,12 @@ public final class AlarmClock {
public static final String ACTION_SET_TIMER = "android.intent.action.SET_TIMER";
/**
- * Activity Action: Dismiss timers.
+ * Activity Action: Dismiss a timer.
* <p>
- * Dismiss all currently expired timers. If there are no expired timers, then this is a no-op.
+ * The timer to dismiss should be specified using the Intent's data URI, which represents a
+ * deeplink to the timer.
+ * </p><p>
+ * If no data URI is provided, dismiss all expired timers.
* </p>
*/
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
diff --git a/android/provider/CallLog.java b/android/provider/CallLog.java
index 60df467b..c6c8d9d6 100644
--- a/android/provider/CallLog.java
+++ b/android/provider/CallLog.java
@@ -223,14 +223,13 @@ public class CallLog {
/** Call was WIFI call. */
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;
+ public static final int FEATURES_ASSISTED_DIALING_USED = 1 << 4;
+
+ /** Call was on RTT at some point */
+ public static final int FEATURES_RTT = 1 << 5;
/**
* The phone number as the user entered it.
diff --git a/android/provider/DocumentsContract.java b/android/provider/DocumentsContract.java
index 99fcdad4..e7fd59e4 100644
--- a/android/provider/DocumentsContract.java
+++ b/android/provider/DocumentsContract.java
@@ -16,7 +16,6 @@
package android.provider;
-import static android.net.TrafficStats.KB_IN_BYTES;
import static android.system.OsConstants.SEEK_SET;
import static com.android.internal.util.Preconditions.checkArgument;
@@ -51,6 +50,7 @@ import android.os.RemoteException;
import android.os.storage.StorageVolume;
import android.system.ErrnoException;
import android.system.Os;
+import android.util.DataUnit;
import android.util.Log;
import libcore.io.IoUtils;
@@ -173,7 +173,7 @@ public final class DocumentsContract {
/**
* Buffer is large enough to rewind past any EXIF headers.
*/
- private static final int THUMBNAIL_BUFFER_SIZE = (int) (128 * KB_IN_BYTES);
+ private static final int THUMBNAIL_BUFFER_SIZE = (int) DataUnit.KIBIBYTES.toBytes(128);
/** {@hide} */
public static final String EXTERNAL_STORAGE_PROVIDER_AUTHORITY =
diff --git a/android/provider/Settings.java b/android/provider/Settings.java
index 2f865141..1ea48618 100644
--- a/android/provider/Settings.java
+++ b/android/provider/Settings.java
@@ -16,6 +16,16 @@
package android.provider;
+import static android.provider.SettingsValidators.ANY_INTEGER_VALIDATOR;
+import static android.provider.SettingsValidators.ANY_STRING_VALIDATOR;
+import static android.provider.SettingsValidators.BOOLEAN_VALIDATOR;
+import static android.provider.SettingsValidators.COMPONENT_NAME_VALIDATOR;
+import static android.provider.SettingsValidators.LENIENT_IP_ADDRESS_VALIDATOR;
+import static android.provider.SettingsValidators.LOCALE_VALIDATOR;
+import static android.provider.SettingsValidators.NON_NEGATIVE_INTEGER_VALIDATOR;
+import static android.provider.SettingsValidators.PACKAGE_NAME_VALIDATOR;
+import static android.provider.SettingsValidators.URI_VALIDATOR;
+
import android.Manifest;
import android.annotation.IntDef;
import android.annotation.IntRange;
@@ -31,7 +41,6 @@ import android.app.ActivityThread;
import android.app.AppOpsManager;
import android.app.Application;
import android.app.NotificationChannel;
-import android.app.NotificationChannelGroup;
import android.app.NotificationManager;
import android.app.SearchManager;
import android.app.WallpaperManager;
@@ -65,6 +74,7 @@ import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ServiceManager;
import android.os.UserHandle;
+import android.provider.SettingsValidators.Validator;
import android.speech.tts.TextToSpeech;
import android.telephony.SubscriptionManager;
import android.text.TextUtils;
@@ -76,7 +86,6 @@ import android.util.MemoryIntArray;
import android.util.StatsLog;
import com.android.internal.annotations.GuardedBy;
-import com.android.internal.util.ArrayUtils;
import com.android.internal.widget.ILockSettings;
import java.io.IOException;
@@ -1334,18 +1343,6 @@ public final class Settings {
= "android.settings.CHANNEL_NOTIFICATION_SETTINGS";
/**
- * Activity Action: Show notification settings for a single {@link NotificationChannelGroup}.
- * <p>
- * Input: {@link #EXTRA_APP_PACKAGE}, the package containing the channel group to display.
- * Input: {@link #EXTRA_CHANNEL_GROUP_ID}, the id of the channel group to display.
- * <p>
- * Output: Nothing.
- */
- @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
- public static final String ACTION_CHANNEL_GROUP_NOTIFICATION_SETTINGS =
- "android.settings.CHANNEL_GROUP_NOTIFICATION_SETTINGS";
-
- /**
* Activity Extra: The package owner of the notification channel settings to display.
* <p>
* This must be passed as an extra field to the {@link #ACTION_CHANNEL_NOTIFICATION_SETTINGS}.
@@ -1361,15 +1358,6 @@ public final class Settings {
public static final String EXTRA_CHANNEL_ID = "android.provider.extra.CHANNEL_ID";
/**
- * Activity Extra: The {@link NotificationChannelGroup#getId()} of the notification channel
- * group settings to display.
- * <p>
- * This must be passed as an extra field to the
- * {@link #ACTION_CHANNEL_GROUP_NOTIFICATION_SETTINGS}.
- */
- public static final String EXTRA_CHANNEL_GROUP_ID = "android.provider.extra.CHANNEL_GROUP_ID";
-
- /**
* Activity Action: Show notification redaction settings.
*
* @hide
@@ -1491,6 +1479,21 @@ public final class Settings {
public static final String ACTION_REQUEST_SET_AUTOFILL_SERVICE =
"android.settings.REQUEST_SET_AUTOFILL_SERVICE";
+ /**
+ * Activity Action: Show screen for controlling which apps have access on volume directories.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ * <p>
+ * Applications typically use this action to ask the user to revert the "Do not ask again"
+ * status of directory access requests made by
+ * {@link android.os.storage.StorageVolume#createAccessIntent(String)}.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ public static final String ACTION_STORAGE_VOLUME_ACCESS_SETTINGS =
+ "android.settings.STORAGE_VOLUME_ACCESS_SETTINGS";
+
// End of Intent actions for Settings
/**
@@ -2119,11 +2122,6 @@ public final class Settings {
private static final float DEFAULT_FONT_SCALE = 1.0f;
- /** @hide */
- public static interface Validator {
- public boolean validate(String value);
- }
-
/**
* The content:// style URL for this table
*/
@@ -2228,41 +2226,6 @@ public final class Settings {
MOVED_TO_GLOBAL.add(Settings.Global.CERT_PIN_UPDATE_METADATA_URL);
}
- private static final Validator sBooleanValidator =
- new DiscreteValueValidator(new String[] {"0", "1"});
-
- private static final Validator sNonNegativeIntegerValidator = new Validator() {
- @Override
- public boolean validate(String value) {
- try {
- return Integer.parseInt(value) >= 0;
- } catch (NumberFormatException e) {
- return false;
- }
- }
- };
-
- private static final Validator sUriValidator = new Validator() {
- @Override
- public boolean validate(String value) {
- try {
- Uri.decode(value);
- return true;
- } catch (IllegalArgumentException e) {
- return false;
- }
- }
- };
-
- private static final Validator sLenientIpAddressValidator = new Validator() {
- private static final int MAX_IPV6_LENGTH = 45;
-
- @Override
- public boolean validate(String value) {
- return value.length() <= MAX_IPV6_LENGTH;
- }
- };
-
/** @hide */
public static void getMovedToGlobalSettings(Set<String> outKeySet) {
outKeySet.addAll(MOVED_TO_GLOBAL);
@@ -2730,64 +2693,35 @@ public final class Settings {
putIntForUser(cr, SHOW_GTALK_SERVICE_STATUS, flag ? 1 : 0, userHandle);
}
- private static final class DiscreteValueValidator implements Validator {
- private final String[] mValues;
-
- public DiscreteValueValidator(String[] values) {
- mValues = values;
- }
-
- @Override
- public boolean validate(String value) {
- return ArrayUtils.contains(mValues, value);
- }
- }
-
- private static final class InclusiveIntegerRangeValidator implements Validator {
- private final int mMin;
- private final int mMax;
-
- public InclusiveIntegerRangeValidator(int min, int max) {
- mMin = min;
- mMax = max;
- }
-
- @Override
- public boolean validate(String value) {
- try {
- final int intValue = Integer.parseInt(value);
- return intValue >= mMin && intValue <= mMax;
- } catch (NumberFormatException e) {
- return false;
- }
- }
- }
-
- private static final class InclusiveFloatRangeValidator implements Validator {
- private final float mMin;
- private final float mMax;
-
- public InclusiveFloatRangeValidator(float min, float max) {
- mMin = min;
- mMax = max;
- }
+ /**
+ * @deprecated Use {@link android.provider.Settings.Global#STAY_ON_WHILE_PLUGGED_IN} instead
+ */
+ @Deprecated
+ public static final String STAY_ON_WHILE_PLUGGED_IN = Global.STAY_ON_WHILE_PLUGGED_IN;
+ private static final Validator STAY_ON_WHILE_PLUGGED_IN_VALIDATOR = new Validator() {
@Override
public boolean validate(String value) {
try {
- final float floatValue = Float.parseFloat(value);
- return floatValue >= mMin && floatValue <= mMax;
+ int val = Integer.parseInt(value);
+ return (val == 0)
+ || (val == BatteryManager.BATTERY_PLUGGED_AC)
+ || (val == BatteryManager.BATTERY_PLUGGED_USB)
+ || (val == BatteryManager.BATTERY_PLUGGED_WIRELESS)
+ || (val == (BatteryManager.BATTERY_PLUGGED_AC
+ | BatteryManager.BATTERY_PLUGGED_USB))
+ || (val == (BatteryManager.BATTERY_PLUGGED_AC
+ | BatteryManager.BATTERY_PLUGGED_WIRELESS))
+ || (val == (BatteryManager.BATTERY_PLUGGED_USB
+ | BatteryManager.BATTERY_PLUGGED_WIRELESS))
+ || (val == (BatteryManager.BATTERY_PLUGGED_AC
+ | BatteryManager.BATTERY_PLUGGED_USB
+ | BatteryManager.BATTERY_PLUGGED_WIRELESS));
} catch (NumberFormatException e) {
return false;
}
}
- }
-
- /**
- * @deprecated Use {@link android.provider.Settings.Global#STAY_ON_WHILE_PLUGGED_IN} instead
- */
- @Deprecated
- public static final String STAY_ON_WHILE_PLUGGED_IN = Global.STAY_ON_WHILE_PLUGGED_IN;
+ };
/**
* What happens when the user presses the end call button if they're not
@@ -2802,7 +2736,7 @@ public final class Settings {
public static final String END_BUTTON_BEHAVIOR = "end_button_behavior";
private static final Validator END_BUTTON_BEHAVIOR_VALIDATOR =
- new InclusiveIntegerRangeValidator(0, 3);
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 3);
/**
* END_BUTTON_BEHAVIOR value for "go home".
@@ -2828,7 +2762,7 @@ public final class Settings {
*/
public static final String ADVANCED_SETTINGS = "advanced_settings";
- private static final Validator ADVANCED_SETTINGS_VALIDATOR = sBooleanValidator;
+ private static final Validator ADVANCED_SETTINGS_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* ADVANCED_SETTINGS default value.
@@ -2929,7 +2863,7 @@ public final class Settings {
@Deprecated
public static final String WIFI_USE_STATIC_IP = "wifi_use_static_ip";
- private static final Validator WIFI_USE_STATIC_IP_VALIDATOR = sBooleanValidator;
+ private static final Validator WIFI_USE_STATIC_IP_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* The static IP address.
@@ -2941,7 +2875,7 @@ public final class Settings {
@Deprecated
public static final String WIFI_STATIC_IP = "wifi_static_ip";
- private static final Validator WIFI_STATIC_IP_VALIDATOR = sLenientIpAddressValidator;
+ private static final Validator WIFI_STATIC_IP_VALIDATOR = LENIENT_IP_ADDRESS_VALIDATOR;
/**
* If using static IP, the gateway's IP address.
@@ -2953,7 +2887,7 @@ public final class Settings {
@Deprecated
public static final String WIFI_STATIC_GATEWAY = "wifi_static_gateway";
- private static final Validator WIFI_STATIC_GATEWAY_VALIDATOR = sLenientIpAddressValidator;
+ private static final Validator WIFI_STATIC_GATEWAY_VALIDATOR = LENIENT_IP_ADDRESS_VALIDATOR;
/**
* If using static IP, the net mask.
@@ -2965,7 +2899,7 @@ public final class Settings {
@Deprecated
public static final String WIFI_STATIC_NETMASK = "wifi_static_netmask";
- private static final Validator WIFI_STATIC_NETMASK_VALIDATOR = sLenientIpAddressValidator;
+ private static final Validator WIFI_STATIC_NETMASK_VALIDATOR = LENIENT_IP_ADDRESS_VALIDATOR;
/**
* If using static IP, the primary DNS's IP address.
@@ -2977,7 +2911,7 @@ public final class Settings {
@Deprecated
public static final String WIFI_STATIC_DNS1 = "wifi_static_dns1";
- private static final Validator WIFI_STATIC_DNS1_VALIDATOR = sLenientIpAddressValidator;
+ private static final Validator WIFI_STATIC_DNS1_VALIDATOR = LENIENT_IP_ADDRESS_VALIDATOR;
/**
* If using static IP, the secondary DNS's IP address.
@@ -2989,7 +2923,7 @@ public final class Settings {
@Deprecated
public static final String WIFI_STATIC_DNS2 = "wifi_static_dns2";
- private static final Validator WIFI_STATIC_DNS2_VALIDATOR = sLenientIpAddressValidator;
+ private static final Validator WIFI_STATIC_DNS2_VALIDATOR = LENIENT_IP_ADDRESS_VALIDATOR;
/**
* Determines whether remote devices may discover and/or connect to
@@ -3003,7 +2937,7 @@ public final class Settings {
"bluetooth_discoverability";
private static final Validator BLUETOOTH_DISCOVERABILITY_VALIDATOR =
- new InclusiveIntegerRangeValidator(0, 2);
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 2);
/**
* Bluetooth discoverability timeout. If this value is nonzero, then
@@ -3014,7 +2948,7 @@ public final class Settings {
"bluetooth_discoverability_timeout";
private static final Validator BLUETOOTH_DISCOVERABILITY_TIMEOUT_VALIDATOR =
- sNonNegativeIntegerValidator;
+ NON_NEGATIVE_INTEGER_VALIDATOR;
/**
* @deprecated Use {@link android.provider.Settings.Secure#LOCK_PATTERN_ENABLED}
@@ -3110,7 +3044,7 @@ public final class Settings {
@Deprecated
public static final String DIM_SCREEN = "dim_screen";
- private static final Validator DIM_SCREEN_VALIDATOR = sBooleanValidator;
+ private static final Validator DIM_SCREEN_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* The display color mode.
@@ -3130,7 +3064,8 @@ public final class Settings {
*/
public static final String SCREEN_OFF_TIMEOUT = "screen_off_timeout";
- private static final Validator SCREEN_OFF_TIMEOUT_VALIDATOR = sNonNegativeIntegerValidator;
+ private static final Validator SCREEN_OFF_TIMEOUT_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
/**
* The screen backlight brightness between 0 and 255.
@@ -3138,7 +3073,7 @@ public final class Settings {
public static final String SCREEN_BRIGHTNESS = "screen_brightness";
private static final Validator SCREEN_BRIGHTNESS_VALIDATOR =
- new InclusiveIntegerRangeValidator(0, 255);
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 255);
/**
* The screen backlight brightness between 0 and 255.
@@ -3147,14 +3082,14 @@ public final class Settings {
public static final String SCREEN_BRIGHTNESS_FOR_VR = "screen_brightness_for_vr";
private static final Validator SCREEN_BRIGHTNESS_FOR_VR_VALIDATOR =
- new InclusiveIntegerRangeValidator(0, 255);
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 255);
/**
* Control whether to enable automatic brightness mode.
*/
public static final String SCREEN_BRIGHTNESS_MODE = "screen_brightness_mode";
- private static final Validator SCREEN_BRIGHTNESS_MODE_VALIDATOR = sBooleanValidator;
+ private static final Validator SCREEN_BRIGHTNESS_MODE_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Adjustment to auto-brightness to make it generally more (>0.0 <1.0)
@@ -3164,7 +3099,7 @@ public final class Settings {
public static final String SCREEN_AUTO_BRIGHTNESS_ADJ = "screen_auto_brightness_adj";
private static final Validator SCREEN_AUTO_BRIGHTNESS_ADJ_VALIDATOR =
- new InclusiveFloatRangeValidator(-1, 1);
+ new SettingsValidators.InclusiveFloatRangeValidator(-1, 1);
/**
* SCREEN_BRIGHTNESS_MODE value for manual mode.
@@ -3203,7 +3138,7 @@ public final class Settings {
public static final String MODE_RINGER_STREAMS_AFFECTED = "mode_ringer_streams_affected";
private static final Validator MODE_RINGER_STREAMS_AFFECTED_VALIDATOR =
- sNonNegativeIntegerValidator;
+ NON_NEGATIVE_INTEGER_VALIDATOR;
/**
* Determines which streams are affected by mute. The
@@ -3213,7 +3148,7 @@ public final class Settings {
public static final String MUTE_STREAMS_AFFECTED = "mute_streams_affected";
private static final Validator MUTE_STREAMS_AFFECTED_VALIDATOR =
- sNonNegativeIntegerValidator;
+ NON_NEGATIVE_INTEGER_VALIDATOR;
/**
* Whether vibrate is on for different events. This is used internally,
@@ -3221,7 +3156,7 @@ public final class Settings {
*/
public static final String VIBRATE_ON = "vibrate_on";
- private static final Validator VIBRATE_ON_VALIDATOR = sBooleanValidator;
+ private static final Validator VIBRATE_ON_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* If 1, redirects the system vibrator to all currently attached input devices
@@ -3237,7 +3172,7 @@ public final class Settings {
*/
public static final String VIBRATE_INPUT_DEVICES = "vibrate_input_devices";
- private static final Validator VIBRATE_INPUT_DEVICES_VALIDATOR = sBooleanValidator;
+ private static final Validator VIBRATE_INPUT_DEVICES_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Ringer volume. This is used internally, changing this value will not
@@ -3316,7 +3251,7 @@ public final class Settings {
*/
public static final String MASTER_MONO = "master_mono";
- private static final Validator MASTER_MONO_VALIDATOR = sBooleanValidator;
+ private static final Validator MASTER_MONO_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether the notifications should use the ring volume (value of 1) or
@@ -3336,7 +3271,7 @@ public final class Settings {
public static final String NOTIFICATIONS_USE_RING_VOLUME =
"notifications_use_ring_volume";
- private static final Validator NOTIFICATIONS_USE_RING_VOLUME_VALIDATOR = sBooleanValidator;
+ private static final Validator NOTIFICATIONS_USE_RING_VOLUME_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether silent mode should allow vibration feedback. This is used
@@ -3352,7 +3287,7 @@ public final class Settings {
*/
public static final String VIBRATE_IN_SILENT = "vibrate_in_silent";
- private static final Validator VIBRATE_IN_SILENT_VALIDATOR = sBooleanValidator;
+ private static final Validator VIBRATE_IN_SILENT_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* The mapping of stream type (integer) to its setting.
@@ -3400,7 +3335,7 @@ public final class Settings {
*/
public static final String RINGTONE = "ringtone";
- private static final Validator RINGTONE_VALIDATOR = sUriValidator;
+ private static final Validator RINGTONE_VALIDATOR = URI_VALIDATOR;
/**
* A {@link Uri} that will point to the current default ringtone at any
@@ -3425,7 +3360,7 @@ public final class Settings {
*/
public static final String NOTIFICATION_SOUND = "notification_sound";
- private static final Validator NOTIFICATION_SOUND_VALIDATOR = sUriValidator;
+ private static final Validator NOTIFICATION_SOUND_VALIDATOR = URI_VALIDATOR;
/**
* A {@link Uri} that will point to the current default notification
@@ -3448,7 +3383,7 @@ public final class Settings {
*/
public static final String ALARM_ALERT = "alarm_alert";
- private static final Validator ALARM_ALERT_VALIDATOR = sUriValidator;
+ private static final Validator ALARM_ALERT_VALIDATOR = URI_VALIDATOR;
/**
* A {@link Uri} that will point to the current default alarm alert at
@@ -3470,31 +3405,21 @@ public final class Settings {
*/
public static final String MEDIA_BUTTON_RECEIVER = "media_button_receiver";
- private static final Validator MEDIA_BUTTON_RECEIVER_VALIDATOR = new Validator() {
- @Override
- public boolean validate(String value) {
- try {
- ComponentName.unflattenFromString(value);
- return true;
- } catch (NullPointerException e) {
- return false;
- }
- }
- };
+ private static final Validator MEDIA_BUTTON_RECEIVER_VALIDATOR = COMPONENT_NAME_VALIDATOR;
/**
* Setting to enable Auto Replace (AutoText) in text editors. 1 = On, 0 = Off
*/
public static final String TEXT_AUTO_REPLACE = "auto_replace";
- private static final Validator TEXT_AUTO_REPLACE_VALIDATOR = sBooleanValidator;
+ private static final Validator TEXT_AUTO_REPLACE_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Setting to enable Auto Caps in text editors. 1 = On, 0 = Off
*/
public static final String TEXT_AUTO_CAPS = "auto_caps";
- private static final Validator TEXT_AUTO_CAPS_VALIDATOR = sBooleanValidator;
+ private static final Validator TEXT_AUTO_CAPS_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Setting to enable Auto Punctuate in text editors. 1 = On, 0 = Off. This
@@ -3502,19 +3427,19 @@ public final class Settings {
*/
public static final String TEXT_AUTO_PUNCTUATE = "auto_punctuate";
- private static final Validator TEXT_AUTO_PUNCTUATE_VALIDATOR = sBooleanValidator;
+ private static final Validator TEXT_AUTO_PUNCTUATE_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Setting to showing password characters in text editors. 1 = On, 0 = Off
*/
public static final String TEXT_SHOW_PASSWORD = "show_password";
- private static final Validator TEXT_SHOW_PASSWORD_VALIDATOR = sBooleanValidator;
+ private static final Validator TEXT_SHOW_PASSWORD_VALIDATOR = BOOLEAN_VALIDATOR;
public static final String SHOW_GTALK_SERVICE_STATUS =
"SHOW_GTALK_SERVICE_STATUS";
- private static final Validator SHOW_GTALK_SERVICE_STATUS_VALIDATOR = sBooleanValidator;
+ private static final Validator SHOW_GTALK_SERVICE_STATUS_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Name of activity to use for wallpaper on the home screen.
@@ -3543,6 +3468,8 @@ public final class Settings {
@Deprecated
public static final String AUTO_TIME = Global.AUTO_TIME;
+ private static final Validator AUTO_TIME_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#AUTO_TIME_ZONE}
* instead
@@ -3550,6 +3477,8 @@ public final class Settings {
@Deprecated
public static final String AUTO_TIME_ZONE = Global.AUTO_TIME_ZONE;
+ private static final Validator AUTO_TIME_ZONE_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Display times as 12 or 24 hours
* 12
@@ -3559,7 +3488,7 @@ public final class Settings {
/** @hide */
public static final Validator TIME_12_24_VALIDATOR =
- new DiscreteValueValidator(new String[] {"12", "24", null});
+ new SettingsValidators.DiscreteValueValidator(new String[] {"12", "24", null});
/**
* Date format string
@@ -3592,7 +3521,7 @@ public final class Settings {
public static final String SETUP_WIZARD_HAS_RUN = "setup_wizard_has_run";
/** @hide */
- public static final Validator SETUP_WIZARD_HAS_RUN_VALIDATOR = sBooleanValidator;
+ public static final Validator SETUP_WIZARD_HAS_RUN_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Scaling factor for normal window animations. Setting to 0 will disable window
@@ -3631,7 +3560,7 @@ public final class Settings {
public static final String ACCELEROMETER_ROTATION = "accelerometer_rotation";
/** @hide */
- public static final Validator ACCELEROMETER_ROTATION_VALIDATOR = sBooleanValidator;
+ public static final Validator ACCELEROMETER_ROTATION_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Default screen rotation when no other policy applies.
@@ -3645,7 +3574,7 @@ public final class Settings {
/** @hide */
public static final Validator USER_ROTATION_VALIDATOR =
- new InclusiveIntegerRangeValidator(0, 3);
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 3);
/**
* Control whether the rotation lock toggle in the System UI should be hidden.
@@ -3663,7 +3592,7 @@ public final class Settings {
/** @hide */
public static final Validator HIDE_ROTATION_LOCK_TOGGLE_FOR_ACCESSIBILITY_VALIDATOR =
- sBooleanValidator;
+ BOOLEAN_VALIDATOR;
/**
* Whether the phone vibrates when it is ringing due to an incoming call. This will
@@ -3678,7 +3607,7 @@ public final class Settings {
public static final String VIBRATE_WHEN_RINGING = "vibrate_when_ringing";
/** @hide */
- public static final Validator VIBRATE_WHEN_RINGING_VALIDATOR = sBooleanValidator;
+ public static final Validator VIBRATE_WHEN_RINGING_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether the audible DTMF tones are played by the dialer when dialing. The value is
@@ -3687,7 +3616,7 @@ public final class Settings {
public static final String DTMF_TONE_WHEN_DIALING = "dtmf_tone";
/** @hide */
- public static final Validator DTMF_TONE_WHEN_DIALING_VALIDATOR = sBooleanValidator;
+ public static final Validator DTMF_TONE_WHEN_DIALING_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* CDMA only settings
@@ -3698,7 +3627,7 @@ public final class Settings {
public static final String DTMF_TONE_TYPE_WHEN_DIALING = "dtmf_tone_type";
/** @hide */
- public static final Validator DTMF_TONE_TYPE_WHEN_DIALING_VALIDATOR = sBooleanValidator;
+ public static final Validator DTMF_TONE_TYPE_WHEN_DIALING_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether the hearing aid is enabled. The value is
@@ -3708,7 +3637,7 @@ public final class Settings {
public static final String HEARING_AID = "hearing_aid";
/** @hide */
- public static final Validator HEARING_AID_VALIDATOR = sBooleanValidator;
+ public static final Validator HEARING_AID_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* CDMA only settings
@@ -3722,7 +3651,8 @@ public final class Settings {
public static final String TTY_MODE = "tty_mode";
/** @hide */
- public static final Validator TTY_MODE_VALIDATOR = new InclusiveIntegerRangeValidator(0, 3);
+ public static final Validator TTY_MODE_VALIDATOR =
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 3);
/**
* Whether the sounds effects (key clicks, lid open ...) are enabled. The value is
@@ -3731,7 +3661,7 @@ public final class Settings {
public static final String SOUND_EFFECTS_ENABLED = "sound_effects_enabled";
/** @hide */
- public static final Validator SOUND_EFFECTS_ENABLED_VALIDATOR = sBooleanValidator;
+ public static final Validator SOUND_EFFECTS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether the haptic feedback (long presses, ...) are enabled. The value is
@@ -3740,7 +3670,7 @@ public final class Settings {
public static final String HAPTIC_FEEDBACK_ENABLED = "haptic_feedback_enabled";
/** @hide */
- public static final Validator HAPTIC_FEEDBACK_ENABLED_VALIDATOR = sBooleanValidator;
+ public static final Validator HAPTIC_FEEDBACK_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* @deprecated Each application that shows web suggestions should have its own
@@ -3750,7 +3680,7 @@ public final class Settings {
public static final String SHOW_WEB_SUGGESTIONS = "show_web_suggestions";
/** @hide */
- public static final Validator SHOW_WEB_SUGGESTIONS_VALIDATOR = sBooleanValidator;
+ public static final Validator SHOW_WEB_SUGGESTIONS_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether the notification LED should repeatedly flash when a notification is
@@ -3760,7 +3690,7 @@ public final class Settings {
public static final String NOTIFICATION_LIGHT_PULSE = "notification_light_pulse";
/** @hide */
- public static final Validator NOTIFICATION_LIGHT_PULSE_VALIDATOR = sBooleanValidator;
+ public static final Validator NOTIFICATION_LIGHT_PULSE_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Show pointer location on screen?
@@ -3771,7 +3701,7 @@ public final class Settings {
public static final String POINTER_LOCATION = "pointer_location";
/** @hide */
- public static final Validator POINTER_LOCATION_VALIDATOR = sBooleanValidator;
+ public static final Validator POINTER_LOCATION_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Show touch positions on screen?
@@ -3782,7 +3712,7 @@ public final class Settings {
public static final String SHOW_TOUCHES = "show_touches";
/** @hide */
- public static final Validator SHOW_TOUCHES_VALIDATOR = sBooleanValidator;
+ public static final Validator SHOW_TOUCHES_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Log raw orientation data from
@@ -3796,7 +3726,7 @@ public final class Settings {
"window_orientation_listener_log";
/** @hide */
- public static final Validator WINDOW_ORIENTATION_LISTENER_LOG_VALIDATOR = sBooleanValidator;
+ public static final Validator WINDOW_ORIENTATION_LISTENER_LOG_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* @deprecated Use {@link android.provider.Settings.Global#POWER_SOUNDS_ENABLED}
@@ -3806,6 +3736,8 @@ public final class Settings {
@Deprecated
public static final String POWER_SOUNDS_ENABLED = Global.POWER_SOUNDS_ENABLED;
+ private static final Validator POWER_SOUNDS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#DOCK_SOUNDS_ENABLED}
* instead
@@ -3814,6 +3746,8 @@ public final class Settings {
@Deprecated
public static final String DOCK_SOUNDS_ENABLED = Global.DOCK_SOUNDS_ENABLED;
+ private static final Validator DOCK_SOUNDS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether to play sounds when the keyguard is shown and dismissed.
* @hide
@@ -3821,7 +3755,7 @@ public final class Settings {
public static final String LOCKSCREEN_SOUNDS_ENABLED = "lockscreen_sounds_enabled";
/** @hide */
- public static final Validator LOCKSCREEN_SOUNDS_ENABLED_VALIDATOR = sBooleanValidator;
+ public static final Validator LOCKSCREEN_SOUNDS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Whether the lockscreen should be completely disabled.
@@ -3830,7 +3764,7 @@ public final class Settings {
public static final String LOCKSCREEN_DISABLED = "lockscreen.disabled";
/** @hide */
- public static final Validator LOCKSCREEN_DISABLED_VALIDATOR = sBooleanValidator;
+ public static final Validator LOCKSCREEN_DISABLED_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* @deprecated Use {@link android.provider.Settings.Global#LOW_BATTERY_SOUND}
@@ -3897,7 +3831,7 @@ public final class Settings {
public static final String SIP_RECEIVE_CALLS = "sip_receive_calls";
/** @hide */
- public static final Validator SIP_RECEIVE_CALLS_VALIDATOR = sBooleanValidator;
+ public static final Validator SIP_RECEIVE_CALLS_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Call Preference String.
@@ -3908,8 +3842,9 @@ public final class Settings {
public static final String SIP_CALL_OPTIONS = "sip_call_options";
/** @hide */
- public static final Validator SIP_CALL_OPTIONS_VALIDATOR = new DiscreteValueValidator(
- new String[] {"SIP_ALWAYS", "SIP_ADDRESS_ONLY"});
+ public static final Validator SIP_CALL_OPTIONS_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(
+ new String[] {"SIP_ALWAYS", "SIP_ADDRESS_ONLY"});
/**
* One of the sip call options: Always use SIP with network access.
@@ -3918,7 +3853,7 @@ public final class Settings {
public static final String SIP_ALWAYS = "SIP_ALWAYS";
/** @hide */
- public static final Validator SIP_ALWAYS_VALIDATOR = sBooleanValidator;
+ public static final Validator SIP_ALWAYS_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* One of the sip call options: Only if destination is a SIP address.
@@ -3927,7 +3862,7 @@ public final class Settings {
public static final String SIP_ADDRESS_ONLY = "SIP_ADDRESS_ONLY";
/** @hide */
- public static final Validator SIP_ADDRESS_ONLY_VALIDATOR = sBooleanValidator;
+ public static final Validator SIP_ADDRESS_ONLY_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* @deprecated Use SIP_ALWAYS or SIP_ADDRESS_ONLY instead. Formerly used to indicate that
@@ -3940,7 +3875,7 @@ public final class Settings {
public static final String SIP_ASK_ME_EACH_TIME = "SIP_ASK_ME_EACH_TIME";
/** @hide */
- public static final Validator SIP_ASK_ME_EACH_TIME_VALIDATOR = sBooleanValidator;
+ public static final Validator SIP_ASK_ME_EACH_TIME_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* Pointer speed setting.
@@ -3954,7 +3889,7 @@ public final class Settings {
/** @hide */
public static final Validator POINTER_SPEED_VALIDATOR =
- new InclusiveFloatRangeValidator(-7, 7);
+ new SettingsValidators.InclusiveFloatRangeValidator(-7, 7);
/**
* Whether lock-to-app will be triggered by long-press on recents.
@@ -3963,7 +3898,7 @@ public final class Settings {
public static final String LOCK_TO_APP_ENABLED = "lock_to_app_enabled";
/** @hide */
- public static final Validator LOCK_TO_APP_ENABLED_VALIDATOR = sBooleanValidator;
+ public static final Validator LOCK_TO_APP_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* I am the lolrus.
@@ -3995,7 +3930,7 @@ public final class Settings {
public static final String SHOW_BATTERY_PERCENT = "status_bar_show_battery_percent";
/** @hide */
- private static final Validator SHOW_BATTERY_PERCENT_VALIDATOR = sBooleanValidator;
+ private static final Validator SHOW_BATTERY_PERCENT_VALIDATOR = BOOLEAN_VALIDATOR;
/**
* IMPORTANT: If you add a new public settings you also have to add it to
@@ -4067,6 +4002,9 @@ public final class Settings {
* Keys we no longer back up under the current schema, but want to continue to
* process when restoring historical backup datasets.
*
+ * All settings in {@link LEGACY_RESTORE_SETTINGS} array *must* have a non-null validator,
+ * otherwise they won't be restored.
+ *
* @hide
*/
public static final String[] LEGACY_RESTORE_SETTINGS = {
@@ -4175,11 +4113,15 @@ public final class Settings {
/**
* These are all public system settings
*
+ * All settings in {@link SETTINGS_TO_BACKUP} array *must* have a non-null validator,
+ * otherwise they won't be restored.
+ *
* @hide
*/
public static final Map<String, Validator> VALIDATORS = new ArrayMap<>();
static {
- VALIDATORS.put(END_BUTTON_BEHAVIOR,END_BUTTON_BEHAVIOR_VALIDATOR);
+ VALIDATORS.put(STAY_ON_WHILE_PLUGGED_IN, STAY_ON_WHILE_PLUGGED_IN_VALIDATOR);
+ VALIDATORS.put(END_BUTTON_BEHAVIOR, END_BUTTON_BEHAVIOR_VALIDATOR);
VALIDATORS.put(WIFI_USE_STATIC_IP, WIFI_USE_STATIC_IP_VALIDATOR);
VALIDATORS.put(BLUETOOTH_DISCOVERABILITY, BLUETOOTH_DISCOVERABILITY_VALIDATOR);
VALIDATORS.put(BLUETOOTH_DISCOVERABILITY_TIMEOUT,
@@ -4201,6 +4143,8 @@ public final class Settings {
VALIDATORS.put(TEXT_AUTO_CAPS, TEXT_AUTO_CAPS_VALIDATOR);
VALIDATORS.put(TEXT_AUTO_PUNCTUATE, TEXT_AUTO_PUNCTUATE_VALIDATOR);
VALIDATORS.put(TEXT_SHOW_PASSWORD, TEXT_SHOW_PASSWORD_VALIDATOR);
+ VALIDATORS.put(AUTO_TIME, AUTO_TIME_VALIDATOR);
+ VALIDATORS.put(AUTO_TIME_ZONE, AUTO_TIME_ZONE_VALIDATOR);
VALIDATORS.put(SHOW_GTALK_SERVICE_STATUS, SHOW_GTALK_SERVICE_STATUS_VALIDATOR);
VALIDATORS.put(WALLPAPER_ACTIVITY, WALLPAPER_ACTIVITY_VALIDATOR);
VALIDATORS.put(TIME_12_24, TIME_12_24_VALIDATOR);
@@ -4211,6 +4155,8 @@ public final class Settings {
VALIDATORS.put(DTMF_TONE_WHEN_DIALING, DTMF_TONE_WHEN_DIALING_VALIDATOR);
VALIDATORS.put(SOUND_EFFECTS_ENABLED, SOUND_EFFECTS_ENABLED_VALIDATOR);
VALIDATORS.put(HAPTIC_FEEDBACK_ENABLED, HAPTIC_FEEDBACK_ENABLED_VALIDATOR);
+ VALIDATORS.put(POWER_SOUNDS_ENABLED, POWER_SOUNDS_ENABLED_VALIDATOR);
+ VALIDATORS.put(DOCK_SOUNDS_ENABLED, DOCK_SOUNDS_ENABLED_VALIDATOR);
VALIDATORS.put(SHOW_WEB_SUGGESTIONS, SHOW_WEB_SUGGESTIONS_VALIDATOR);
VALIDATORS.put(WIFI_USE_STATIC_IP, WIFI_USE_STATIC_IP_VALIDATOR);
VALIDATORS.put(END_BUTTON_BEHAVIOR, END_BUTTON_BEHAVIOR_VALIDATOR);
@@ -4335,6 +4281,8 @@ public final class Settings {
@Deprecated
public static final String BLUETOOTH_ON = Global.BLUETOOTH_ON;
+ private static final Validator BLUETOOTH_ON_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#DATA_ROAMING} instead
*/
@@ -4412,6 +4360,8 @@ public final class Settings {
@Deprecated
public static final String USB_MASS_STORAGE_ENABLED = Global.USB_MASS_STORAGE_ENABLED;
+ private static final Validator USB_MASS_STORAGE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#USE_GOOGLE_MAIL} instead
*/
@@ -4441,6 +4391,9 @@ public final class Settings {
public static final String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON =
Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON;
+ private static final Validator WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use
* {@link android.provider.Settings.Global#WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY} instead
@@ -4449,6 +4402,9 @@ public final class Settings {
public static final String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY =
Global.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY;
+ private static final Validator WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#WIFI_NUM_OPEN_NETWORKS_KEPT}
* instead
@@ -4456,6 +4412,9 @@ public final class Settings {
@Deprecated
public static final String WIFI_NUM_OPEN_NETWORKS_KEPT = Global.WIFI_NUM_OPEN_NETWORKS_KEPT;
+ private static final Validator WIFI_NUM_OPEN_NETWORKS_KEPT_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#WIFI_ON} instead
*/
@@ -5218,6 +5177,8 @@ public final class Settings {
@Deprecated
public static final String BUGREPORT_IN_POWER_MENU = "bugreport_in_power_menu";
+ private static final Validator BUGREPORT_IN_POWER_MENU_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#ADB_ENABLED} instead
*/
@@ -5235,6 +5196,8 @@ public final class Settings {
@Deprecated
public static final String ALLOW_MOCK_LOCATION = "mock_location";
+ private static final Validator ALLOW_MOCK_LOCATION_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* On Android 8.0 (API level 26) and higher versions of the platform,
* a 64-bit number (expressed as a hexadecimal string), unique to
@@ -5280,6 +5243,8 @@ public final class Settings {
@Deprecated
public static final String BLUETOOTH_ON = Global.BLUETOOTH_ON;
+ private static final Validator BLUETOOTH_ON_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#DATA_ROAMING} instead
*/
@@ -5327,6 +5292,8 @@ public final class Settings {
@TestApi
public static final String AUTOFILL_SERVICE = "autofill_service";
+ private static final Validator AUTOFILL_SERVICE_VALIDATOR = COMPONENT_NAME_VALIDATOR;
+
/**
* Boolean indicating if Autofill supports field classification.
*
@@ -5413,9 +5380,38 @@ public final class Settings {
* List of input methods that are currently enabled. This is a string
* containing the IDs of all enabled input methods, each ID separated
* by ':'.
+ *
+ * Format like "ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0"
+ * where imeId is ComponentName and subtype is int32.
*/
public static final String ENABLED_INPUT_METHODS = "enabled_input_methods";
+ private static final Validator ENABLED_INPUT_METHODS_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ String[] inputMethods = value.split(":");
+ boolean valid = true;
+ for (String inputMethod : inputMethods) {
+ if (inputMethod.length() == 0) {
+ return false;
+ }
+ String[] subparts = inputMethod.split(";");
+ for (String subpart : subparts) {
+ // allow either a non negative integer or a ComponentName
+ valid |= (NON_NEGATIVE_INTEGER_VALIDATOR.validate(subpart)
+ || COMPONENT_NAME_VALIDATOR.validate(subpart));
+ }
+ if (!valid) {
+ return false;
+ }
+ }
+ return valid;
+ }
+ };
+
/**
* List of system input methods that are currently disabled. This is a string
* containing the IDs of all disabled input methods, each ID separated
@@ -5431,6 +5427,8 @@ public final class Settings {
*/
public static final String SHOW_IME_WITH_HARD_KEYBOARD = "show_ime_with_hard_keyboard";
+ private static final Validator SHOW_IME_WITH_HARD_KEYBOARD_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Host name and port for global http proxy. Uses ':' seperator for
* between host and port.
@@ -5503,37 +5501,54 @@ public final class Settings {
* Note: do not rely on this value being present in settings.db or on ContentObserver
* notifications for the corresponding Uri. Use {@link LocationManager#MODE_CHANGED_ACTION}
* to receive changes in this value.
+ *
+ * @deprecated To check location status, use {@link LocationManager#isLocationEnabled()}. To
+ * get the status of a location provider, use
+ * {@link LocationManager#isProviderEnabled(String)}.
*/
+ @Deprecated
public static final String LOCATION_MODE = "location_mode";
- /**
- * Stores the previous location mode when {@link #LOCATION_MODE} is set to
- * {@link #LOCATION_MODE_OFF}
- * @hide
- */
- public static final String LOCATION_PREVIOUS_MODE = "location_previous_mode";
/**
- * Sets all location providers to the previous states before location was turned off.
- * @hide
- */
- public static final int LOCATION_MODE_PREVIOUS = -1;
- /**
* Location access disabled.
+ *
+ * @deprecated To check location status, use {@link LocationManager#isLocationEnabled()}. To
+ * get the status of a location provider, use
+ * {@link LocationManager#isProviderEnabled(String)}.
*/
+ @Deprecated
public static final int LOCATION_MODE_OFF = 0;
+
/**
* Network Location Provider disabled, but GPS and other sensors enabled.
+ *
+ * @deprecated To check location status, use {@link LocationManager#isLocationEnabled()}. To
+ * get the status of a location provider, use
+ * {@link LocationManager#isProviderEnabled(String)}.
*/
+ @Deprecated
public static final int LOCATION_MODE_SENSORS_ONLY = 1;
+
/**
* Reduced power usage, such as limiting the number of GPS updates per hour. Requests
* with {@link android.location.Criteria#POWER_HIGH} may be downgraded to
* {@link android.location.Criteria#POWER_MEDIUM}.
+ *
+ * @deprecated To check location status, use {@link LocationManager#isLocationEnabled()}. To
+ * get the status of a location provider, use
+ * {@link LocationManager#isProviderEnabled(String)}.
*/
+ @Deprecated
public static final int LOCATION_MODE_BATTERY_SAVING = 2;
+
/**
* Best-effort location computation allowed.
+ *
+ * @deprecated To check location status, use {@link LocationManager#isLocationEnabled()}. To
+ * get the status of a location provider, use
+ * {@link LocationManager#isProviderEnabled(String)}.
*/
+ @Deprecated
public static final int LOCATION_MODE_HIGH_ACCURACY = 3;
/**
@@ -5707,6 +5722,8 @@ public final class Settings {
@Deprecated
public static final String USB_MASS_STORAGE_ENABLED = Global.USB_MASS_STORAGE_ENABLED;
+ private static final Validator USB_MASS_STORAGE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#USE_GOOGLE_MAIL} instead
*/
@@ -5718,6 +5735,8 @@ public final class Settings {
*/
public static final String ACCESSIBILITY_ENABLED = "accessibility_enabled";
+ private static final Validator ACCESSIBILITY_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Setting specifying if the accessibility shortcut is enabled.
* @hide
@@ -5725,6 +5744,8 @@ public final class Settings {
public static final String ACCESSIBILITY_SHORTCUT_ENABLED =
"accessibility_shortcut_enabled";
+ private static final Validator ACCESSIBILITY_SHORTCUT_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Setting specifying if the accessibility shortcut is enabled.
* @hide
@@ -5732,6 +5753,9 @@ public final class Settings {
public static final String ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN =
"accessibility_shortcut_on_lock_screen";
+ private static final Validator ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Setting specifying if the accessibility shortcut dialog has been shown to this user.
* @hide
@@ -5739,6 +5763,9 @@ public final class Settings {
public static final String ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN =
"accessibility_shortcut_dialog_shown";
+ private static final Validator ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Setting specifying the accessibility service to be toggled via the accessibility
* shortcut. Must be its flattened {@link ComponentName}.
@@ -5747,6 +5774,9 @@ public final class Settings {
public static final String ACCESSIBILITY_SHORTCUT_TARGET_SERVICE =
"accessibility_shortcut_target_service";
+ private static final Validator ACCESSIBILITY_SHORTCUT_TARGET_SERVICE_VALIDATOR =
+ COMPONENT_NAME_VALIDATOR;
+
/**
* Setting specifying the accessibility service or feature to be toggled via the
* accessibility button in the navigation bar. This is either a flattened
@@ -5757,17 +5787,32 @@ public final class Settings {
public static final String ACCESSIBILITY_BUTTON_TARGET_COMPONENT =
"accessibility_button_target_component";
+ private static final Validator ACCESSIBILITY_BUTTON_TARGET_COMPONENT_VALIDATOR =
+ new Validator() {
+ @Override
+ public boolean validate(String value) {
+ // technically either ComponentName or class name, but there's proper value
+ // validation at callsites, so allow any non-null string
+ return value != null;
+ }
+ };
+
/**
* If touch exploration is enabled.
*/
public static final String TOUCH_EXPLORATION_ENABLED = "touch_exploration_enabled";
+ private static final Validator TOUCH_EXPLORATION_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* List of the enabled accessibility providers.
*/
public static final String ENABLED_ACCESSIBILITY_SERVICES =
"enabled_accessibility_services";
+ private static final Validator ENABLED_ACCESSIBILITY_SERVICES_VALIDATOR =
+ new SettingsValidators.ComponentNameListValidator(":");
+
/**
* List of the accessibility services to which the user has granted
* permission to put the device into touch exploration mode.
@@ -5777,6 +5822,9 @@ public final class Settings {
public static final String TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES =
"touch_exploration_granted_accessibility_services";
+ private static final Validator TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES_VALIDATOR =
+ new SettingsValidators.ComponentNameListValidator(":");
+
/**
* Uri of the slice that's presented on the keyguard.
* Defaults to a slice with the date and next alarm.
@@ -5795,6 +5843,8 @@ public final class Settings {
@Deprecated
public static final String ACCESSIBILITY_SPEAK_PASSWORD = "speak_password";
+ private static final Validator ACCESSIBILITY_SPEAK_PASSWORD_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether to draw text with high contrast while in accessibility mode.
*
@@ -5803,6 +5853,9 @@ public final class Settings {
public static final String ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED =
"high_text_contrast_enabled";
+ private static final Validator ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Setting that specifies whether the display magnification is enabled via a system-wide
* triple tap gesture. Display magnifications allows the user to zoom in the display content
@@ -5815,6 +5868,9 @@ public final class Settings {
public static final String ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED =
"accessibility_display_magnification_enabled";
+ private static final Validator ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Setting that specifies whether the display magnification is enabled via a shortcut
* affordance within the system's navigation area. Display magnifications allows the user to
@@ -5826,6 +5882,9 @@ public final class Settings {
public static final String ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED =
"accessibility_display_magnification_navbar_enabled";
+ private static final Validator ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED_VALIDATOR
+ = BOOLEAN_VALIDATOR;
+
/**
* Setting that specifies what the display magnification scale is.
* Display magnifications allows the user to zoom in the display
@@ -5839,6 +5898,9 @@ public final class Settings {
public static final String ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE =
"accessibility_display_magnification_scale";
+ private static final Validator ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE_VALIDATOR =
+ new SettingsValidators.InclusiveFloatRangeValidator(1.0f, Float.MAX_VALUE);
+
/**
* Unused mangnification setting
*
@@ -5891,6 +5953,9 @@ public final class Settings {
public static final String ACCESSIBILITY_CAPTIONING_ENABLED =
"accessibility_captioning_enabled";
+ private static final Validator ACCESSIBILITY_CAPTIONING_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Setting that specifies the language for captions as a locale string,
* e.g. en_US.
@@ -5901,6 +5966,8 @@ public final class Settings {
public static final String ACCESSIBILITY_CAPTIONING_LOCALE =
"accessibility_captioning_locale";
+ private static final Validator ACCESSIBILITY_CAPTIONING_LOCALE_VALIDATOR = LOCALE_VALIDATOR;
+
/**
* Integer property that specifies the preset style for captions, one
* of:
@@ -5915,6 +5982,10 @@ public final class Settings {
public static final String ACCESSIBILITY_CAPTIONING_PRESET =
"accessibility_captioning_preset";
+ private static final Validator ACCESSIBILITY_CAPTIONING_PRESET_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[]{"-1", "0", "1", "2",
+ "3", "4"});
+
/**
* Integer property that specifes the background color for captions as a
* packed 32-bit color.
@@ -5925,6 +5996,9 @@ public final class Settings {
public static final String ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR =
"accessibility_captioning_background_color";
+ private static final Validator ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR_VALIDATOR =
+ ANY_INTEGER_VALIDATOR;
+
/**
* Integer property that specifes the foreground color for captions as a
* packed 32-bit color.
@@ -5935,6 +6009,9 @@ public final class Settings {
public static final String ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR =
"accessibility_captioning_foreground_color";
+ private static final Validator ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR_VALIDATOR =
+ ANY_INTEGER_VALIDATOR;
+
/**
* Integer property that specifes the edge type for captions, one of:
* <ul>
@@ -5949,6 +6026,9 @@ public final class Settings {
public static final String ACCESSIBILITY_CAPTIONING_EDGE_TYPE =
"accessibility_captioning_edge_type";
+ private static final Validator ACCESSIBILITY_CAPTIONING_EDGE_TYPE_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[]{"0", "1", "2"});
+
/**
* Integer property that specifes the edge color for captions as a
* packed 32-bit color.
@@ -5960,6 +6040,9 @@ public final class Settings {
public static final String ACCESSIBILITY_CAPTIONING_EDGE_COLOR =
"accessibility_captioning_edge_color";
+ private static final Validator ACCESSIBILITY_CAPTIONING_EDGE_COLOR_VALIDATOR =
+ ANY_INTEGER_VALIDATOR;
+
/**
* Integer property that specifes the window color for captions as a
* packed 32-bit color.
@@ -5970,6 +6053,9 @@ public final class Settings {
public static final String ACCESSIBILITY_CAPTIONING_WINDOW_COLOR =
"accessibility_captioning_window_color";
+ private static final Validator ACCESSIBILITY_CAPTIONING_WINDOW_COLOR_VALIDATOR =
+ ANY_INTEGER_VALIDATOR;
+
/**
* String property that specifies the typeface for captions, one of:
* <ul>
@@ -5985,6 +6071,10 @@ public final class Settings {
public static final String ACCESSIBILITY_CAPTIONING_TYPEFACE =
"accessibility_captioning_typeface";
+ private static final Validator ACCESSIBILITY_CAPTIONING_TYPEFACE_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[]{"DEFAULT",
+ "MONOSPACE", "SANS_SERIF", "SERIF"});
+
/**
* Floating point property that specifies font scaling for captions.
*
@@ -5993,12 +6083,18 @@ public final class Settings {
public static final String ACCESSIBILITY_CAPTIONING_FONT_SCALE =
"accessibility_captioning_font_scale";
+ private static final Validator ACCESSIBILITY_CAPTIONING_FONT_SCALE_VALIDATOR =
+ new SettingsValidators.InclusiveFloatRangeValidator(0.5f, 2.0f);
+
/**
* Setting that specifies whether display color inversion is enabled.
*/
public static final String ACCESSIBILITY_DISPLAY_INVERSION_ENABLED =
"accessibility_display_inversion_enabled";
+ private static final Validator ACCESSIBILITY_DISPLAY_INVERSION_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Setting that specifies whether display color space adjustment is
* enabled.
@@ -6008,15 +6104,24 @@ public final class Settings {
public static final String ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED =
"accessibility_display_daltonizer_enabled";
+ private static final Validator ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Integer property that specifies the type of color space adjustment to
- * perform. Valid values are defined in AccessibilityManager.
+ * perform. Valid values are defined in AccessibilityManager:
+ * - AccessibilityManager.DALTONIZER_DISABLED = -1
+ * - AccessibilityManager.DALTONIZER_SIMULATE_MONOCHROMACY = 0
+ * - AccessibilityManager.DALTONIZER_CORRECT_DEUTERANOMALY = 12
*
* @hide
*/
public static final String ACCESSIBILITY_DISPLAY_DALTONIZER =
"accessibility_display_daltonizer";
+ private static final Validator ACCESSIBILITY_DISPLAY_DALTONIZER_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[] {"-1", "0", "12"});
+
/**
* Setting that specifies whether automatic click when the mouse pointer stops moving is
* enabled.
@@ -6026,6 +6131,9 @@ public final class Settings {
public static final String ACCESSIBILITY_AUTOCLICK_ENABLED =
"accessibility_autoclick_enabled";
+ private static final Validator ACCESSIBILITY_AUTOCLICK_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Integer setting specifying amount of time in ms the mouse pointer has to stay still
* before performing click when {@link #ACCESSIBILITY_AUTOCLICK_ENABLED} is set.
@@ -6036,6 +6144,9 @@ public final class Settings {
public static final String ACCESSIBILITY_AUTOCLICK_DELAY =
"accessibility_autoclick_delay";
+ private static final Validator ACCESSIBILITY_AUTOCLICK_DELAY_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Whether or not larger size icons are used for the pointer of mouse/trackpad for
* accessibility.
@@ -6045,12 +6156,18 @@ public final class Settings {
public static final String ACCESSIBILITY_LARGE_POINTER_ICON =
"accessibility_large_pointer_icon";
+ private static final Validator ACCESSIBILITY_LARGE_POINTER_ICON_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* The timeout for considering a press to be a long press in milliseconds.
* @hide
*/
public static final String LONG_PRESS_TIMEOUT = "long_press_timeout";
+ private static final Validator LONG_PRESS_TIMEOUT_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* The duration in milliseconds between the first tap's up event and the second tap's
* down event for an interaction to be considered part of the same multi-press.
@@ -6104,16 +6221,22 @@ public final class Settings {
*/
public static final String TTS_DEFAULT_RATE = "tts_default_rate";
+ private static final Validator TTS_DEFAULT_RATE_VALIDATOR = NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Default text-to-speech engine pitch. 100 = 1x
*/
public static final String TTS_DEFAULT_PITCH = "tts_default_pitch";
+ private static final Validator TTS_DEFAULT_PITCH_VALIDATOR = NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Default text-to-speech engine.
*/
public static final String TTS_DEFAULT_SYNTH = "tts_default_synth";
+ private static final Validator TTS_DEFAULT_SYNTH_VALIDATOR = PACKAGE_NAME_VALIDATOR;
+
/**
* Default text-to-speech language.
*
@@ -6161,11 +6284,33 @@ public final class Settings {
*/
public static final String TTS_DEFAULT_LOCALE = "tts_default_locale";
+ private static final Validator TTS_DEFAULT_LOCALE_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ if (value == null || value.length() == 0) {
+ return false;
+ }
+ String[] ttsLocales = value.split(",");
+ boolean valid = true;
+ for (String ttsLocale : ttsLocales) {
+ String[] parts = ttsLocale.split(":");
+ valid |= ((parts.length == 2)
+ && (parts[0].length() > 0)
+ && ANY_STRING_VALIDATOR.validate(parts[0])
+ && LOCALE_VALIDATOR.validate(parts[1]));
+ }
+ return valid;
+ }
+ };
+
/**
* Space delimited list of plugin packages that are enabled.
*/
public static final String TTS_ENABLED_PLUGINS = "tts_enabled_plugins";
+ private static final Validator TTS_ENABLED_PLUGINS_VALIDATOR =
+ new SettingsValidators.PackageNameListValidator(" ");
+
/**
* @deprecated Use {@link android.provider.Settings.Global#WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON}
* instead.
@@ -6174,6 +6319,9 @@ public final class Settings {
public static final String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON =
Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON;
+ private static final Validator WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY}
* instead.
@@ -6182,6 +6330,9 @@ public final class Settings {
public static final String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY =
Global.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY;
+ private static final Validator WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#WIFI_NUM_OPEN_NETWORKS_KEPT}
* instead.
@@ -6190,6 +6341,9 @@ public final class Settings {
public static final String WIFI_NUM_OPEN_NETWORKS_KEPT =
Global.WIFI_NUM_OPEN_NETWORKS_KEPT;
+ private static final Validator WIFI_NUM_OPEN_NETWORKS_KEPT_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* @deprecated Use {@link android.provider.Settings.Global#WIFI_ON}
* instead.
@@ -6348,6 +6502,9 @@ public final class Settings {
public static final String PREFERRED_TTY_MODE =
"preferred_tty_mode";
+ private static final Validator PREFERRED_TTY_MODE_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[]{"0", "1", "2", "3"});
+
/**
* Whether the enhanced voice privacy mode is enabled.
* 0 = normal voice privacy
@@ -6356,6 +6513,8 @@ public final class Settings {
*/
public static final String ENHANCED_VOICE_PRIVACY_ENABLED = "enhanced_voice_privacy_enabled";
+ private static final Validator ENHANCED_VOICE_PRIVACY_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether the TTY mode mode is enabled.
* 0 = disabled
@@ -6364,6 +6523,8 @@ public final class Settings {
*/
public static final String TTY_MODE_ENABLED = "tty_mode_enabled";
+ private static final Validator TTY_MODE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Controls whether settings backup is enabled.
* Type: int ( 0 = disabled, 1 = enabled )
@@ -6534,24 +6695,32 @@ public final class Settings {
*/
public static final String MOUNT_PLAY_NOTIFICATION_SND = "mount_play_not_snd";
+ private static final Validator MOUNT_PLAY_NOTIFICATION_SND_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether or not UMS auto-starts on UMS host detection. (0 = false, 1 = true)
* @hide
*/
public static final String MOUNT_UMS_AUTOSTART = "mount_ums_autostart";
+ private static final Validator MOUNT_UMS_AUTOSTART_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether or not a notification is displayed on UMS host detection. (0 = false, 1 = true)
* @hide
*/
public static final String MOUNT_UMS_PROMPT = "mount_ums_prompt";
+ private static final Validator MOUNT_UMS_PROMPT_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether or not a notification is displayed while UMS is enabled. (0 = false, 1 = true)
* @hide
*/
public static final String MOUNT_UMS_NOTIFY_ENABLED = "mount_ums_notify_enabled";
+ private static final Validator MOUNT_UMS_NOTIFY_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* If nonzero, ANRs in invisible background processes bring up a dialog.
* Otherwise, the process will be silently killed.
@@ -6562,6 +6731,17 @@ public final class Settings {
public static final String ANR_SHOW_BACKGROUND = "anr_show_background";
/**
+ * If nonzero, crashes in foreground processes will bring up a dialog.
+ * Otherwise, the process will be silently killed.
+ * @hide
+ */
+ public static final String SHOW_FIRST_CRASH_DIALOG_DEV_OPTION =
+ "show_first_crash_dialog_dev_option";
+
+ private static final Validator SHOW_FIRST_CRASH_DIALOG_DEV_OPTION_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
+ /**
* The {@link ComponentName} string of the service to be used as the voice recognition
* service.
*
@@ -6586,6 +6766,8 @@ public final class Settings {
*/
public static final String SELECTED_SPELL_CHECKER = "selected_spell_checker";
+ private static final Validator SELECTED_SPELL_CHECKER_VALIDATOR = COMPONENT_NAME_VALIDATOR;
+
/**
* The {@link ComponentName} string of the selected subtype of the selected spell checker
* service which is one of the services managed by the text service manager.
@@ -6595,13 +6777,18 @@ public final class Settings {
public static final String SELECTED_SPELL_CHECKER_SUBTYPE =
"selected_spell_checker_subtype";
+ private static final Validator SELECTED_SPELL_CHECKER_SUBTYPE_VALIDATOR =
+ COMPONENT_NAME_VALIDATOR;
+
/**
- * The {@link ComponentName} string whether spell checker is enabled or not.
+ * Whether spell checker is enabled or not.
*
* @hide
*/
public static final String SPELL_CHECKER_ENABLED = "spell_checker_enabled";
+ private static final Validator SPELL_CHECKER_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* What happens when the user presses the Power button while in-call
* and the screen is on.<br/>
@@ -6613,6 +6800,9 @@ public final class Settings {
*/
public static final String INCALL_POWER_BUTTON_BEHAVIOR = "incall_power_button_behavior";
+ private static final Validator INCALL_POWER_BUTTON_BEHAVIOR_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[]{"1", "2"});
+
/**
* INCALL_POWER_BUTTON_BEHAVIOR value for "turn off screen".
* @hide
@@ -6668,12 +6858,16 @@ public final class Settings {
*/
public static final String WAKE_GESTURE_ENABLED = "wake_gesture_enabled";
+ private static final Validator WAKE_GESTURE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether the device should doze if configured.
* @hide
*/
public static final String DOZE_ENABLED = "doze_enabled";
+ private static final Validator DOZE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether doze should be always on.
* @hide
@@ -6686,6 +6880,8 @@ public final class Settings {
*/
public static final String DOZE_PULSE_ON_PICK_UP = "doze_pulse_on_pick_up";
+ private static final Validator DOZE_PULSE_ON_PICK_UP_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether the device should pulse on long press gesture.
* @hide
@@ -6698,6 +6894,8 @@ public final class Settings {
*/
public static final String DOZE_PULSE_ON_DOUBLE_TAP = "doze_pulse_on_double_tap";
+ private static final Validator DOZE_PULSE_ON_DOUBLE_TAP_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* The current night mode that has been selected by the user. Owned
* and controlled by UiModeManagerService. Constants are as per
@@ -6712,6 +6910,8 @@ public final class Settings {
*/
public static final String SCREENSAVER_ENABLED = "screensaver_enabled";
+ private static final Validator SCREENSAVER_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* The user's chosen screensaver components.
*
@@ -6721,6 +6921,9 @@ public final class Settings {
*/
public static final String SCREENSAVER_COMPONENTS = "screensaver_components";
+ private static final Validator SCREENSAVER_COMPONENTS_VALIDATOR =
+ new SettingsValidators.ComponentNameListValidator(",");
+
/**
* If screensavers are enabled, whether the screensaver should be automatically launched
* when the device is inserted into a (desk) dock.
@@ -6728,6 +6931,8 @@ public final class Settings {
*/
public static final String SCREENSAVER_ACTIVATE_ON_DOCK = "screensaver_activate_on_dock";
+ private static final Validator SCREENSAVER_ACTIVATE_ON_DOCK_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* If screensavers are enabled, whether the screensaver should be automatically launched
* when the screen times out when not on battery.
@@ -6735,6 +6940,8 @@ public final class Settings {
*/
public static final String SCREENSAVER_ACTIVATE_ON_SLEEP = "screensaver_activate_on_sleep";
+ private static final Validator SCREENSAVER_ACTIVATE_ON_SLEEP_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* If screensavers are enabled, the default screensaver component.
* @hide
@@ -6747,6 +6954,9 @@ public final class Settings {
*/
public static final String NFC_PAYMENT_DEFAULT_COMPONENT = "nfc_payment_default_component";
+ private static final Validator NFC_PAYMENT_DEFAULT_COMPONENT_VALIDATOR =
+ COMPONENT_NAME_VALIDATOR;
+
/**
* Whether NFC payment is handled by the foreground application or a default.
* @hide
@@ -6802,6 +7012,37 @@ public final class Settings {
public static final String ASSIST_DISCLOSURE_ENABLED = "assist_disclosure_enabled";
/**
+ * Control if rotation suggestions are sent to System UI when in rotation locked mode.
+ * Done to enable screen rotation while the the screen rotation is locked. Enabling will
+ * poll the accelerometer in rotation locked mode.
+ *
+ * If 0, then rotation suggestions are not sent to System UI. If 1, suggestions are sent.
+ *
+ * @hide
+ */
+
+ public static final String SHOW_ROTATION_SUGGESTIONS = "show_rotation_suggestions";
+
+ /**
+ * The disabled state of SHOW_ROTATION_SUGGESTIONS.
+ * @hide
+ */
+ public static final int SHOW_ROTATION_SUGGESTIONS_DISABLED = 0x0;
+
+ /**
+ * The enabled state of SHOW_ROTATION_SUGGESTIONS.
+ * @hide
+ */
+ public static final int SHOW_ROTATION_SUGGESTIONS_ENABLED = 0x1;
+
+ /**
+ * The default state of SHOW_ROTATION_SUGGESTIONS.
+ * @hide
+ */
+ public static final int SHOW_ROTATION_SUGGESTIONS_DEFAULT =
+ SHOW_ROTATION_SUGGESTIONS_ENABLED;
+
+ /**
* Read only list of the service components that the current user has explicitly allowed to
* see and assist with all of the user's notifications.
*
@@ -6813,6 +7054,9 @@ public final class Settings {
public static final String ENABLED_NOTIFICATION_ASSISTANT =
"enabled_notification_assistant";
+ private static final Validator ENABLED_NOTIFICATION_ASSISTANT_VALIDATOR =
+ new SettingsValidators.ComponentNameListValidator(":");
+
/**
* Read only list of the service components that the current user has explicitly allowed to
* see all of the user's notifications, separated by ':'.
@@ -6824,6 +7068,9 @@ public final class Settings {
@Deprecated
public static final String ENABLED_NOTIFICATION_LISTENERS = "enabled_notification_listeners";
+ private static final Validator ENABLED_NOTIFICATION_LISTENERS_VALIDATOR =
+ new SettingsValidators.ComponentNameListValidator(":");
+
/**
* Read only list of the packages that the current user has explicitly allowed to
* manage do not disturb, separated by ':'.
@@ -6836,6 +7083,9 @@ public final class Settings {
public static final String ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES =
"enabled_notification_policy_access_packages";
+ private static final Validator ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES_VALIDATOR =
+ new SettingsValidators.PackageNameListValidator(":");
+
/**
* Defines whether managed profile ringtones should be synced from it's parent profile
* <p>
@@ -6849,6 +7099,8 @@ public final class Settings {
@RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
public static final String SYNC_PARENT_SOUNDS = "sync_parent_sounds";
+ private static final Validator SYNC_PARENT_SOUNDS_VALIDATOR = BOOLEAN_VALIDATOR;
+
/** @hide */
public static final String IMMERSIVE_MODE_CONFIRMATIONS = "immersive_mode_confirmations";
@@ -6935,12 +7187,17 @@ public final class Settings {
*/
public static final String SLEEP_TIMEOUT = "sleep_timeout";
+ private static final Validator SLEEP_TIMEOUT_VALIDATOR =
+ new SettingsValidators.InclusiveIntegerRangeValidator(-1, Integer.MAX_VALUE);
+
/**
* Controls whether double tap to wake is enabled.
* @hide
*/
public static final String DOUBLE_TAP_TO_WAKE = "double_tap_to_wake";
+ private static final Validator DOUBLE_TAP_TO_WAKE_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* The current assistant component. It could be a voice interaction service,
* or an activity that handles ACTION_ASSIST, or empty which means using the default
@@ -6957,6 +7214,8 @@ public final class Settings {
*/
public static final String CAMERA_GESTURE_DISABLED = "camera_gesture_disabled";
+ private static final Validator CAMERA_GESTURE_DISABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether the camera launch gesture to double tap the power button when the screen is off
* should be disabled.
@@ -6966,6 +7225,9 @@ public final class Settings {
public static final String CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED =
"camera_double_tap_power_gesture_disabled";
+ private static final Validator CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Whether the camera double twist gesture to flip between front and back mode should be
* enabled.
@@ -6975,6 +7237,9 @@ public final class Settings {
public static final String CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED =
"camera_double_twist_to_flip_enabled";
+ private static final Validator CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Whether or not the smart camera lift trigger that launches the camera when the user moves
* the phone into a position for taking photos should be enabled.
@@ -6997,6 +7262,9 @@ public final class Settings {
*/
public static final String ASSIST_GESTURE_ENABLED = "assist_gesture_enabled";
+ private static final Validator ASSIST_GESTURE_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Sensitivity control for the assist gesture.
*
@@ -7004,6 +7272,9 @@ public final class Settings {
*/
public static final String ASSIST_GESTURE_SENSITIVITY = "assist_gesture_sensitivity";
+ private static final Validator ASSIST_GESTURE_SENSITIVITY_VALIDATOR =
+ new SettingsValidators.InclusiveFloatRangeValidator(0.0f, 1.0f);
+
/**
* Whether the assist gesture should silence alerts.
*
@@ -7012,6 +7283,9 @@ public final class Settings {
public static final String ASSIST_GESTURE_SILENCE_ALERTS_ENABLED =
"assist_gesture_silence_alerts_enabled";
+ private static final Validator ASSIST_GESTURE_SILENCE_ALERTS_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Whether the assist gesture should wake the phone.
*
@@ -7020,6 +7294,9 @@ public final class Settings {
public static final String ASSIST_GESTURE_WAKE_ENABLED =
"assist_gesture_wake_enabled";
+ private static final Validator ASSIST_GESTURE_WAKE_ENABLED_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* Whether Assist Gesture Deferred Setup has been completed
*
@@ -7027,6 +7304,8 @@ public final class Settings {
*/
public static final String ASSIST_GESTURE_SETUP_COMPLETE = "assist_gesture_setup_complete";
+ private static final Validator ASSIST_GESTURE_SETUP_COMPLETE_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Control whether Night display is currently activated.
* @hide
@@ -7039,6 +7318,8 @@ public final class Settings {
*/
public static final String NIGHT_DISPLAY_AUTO_MODE = "night_display_auto_mode";
+ private static final Validator NIGHT_DISPLAY_AUTO_MODE_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Control the color temperature of Night Display, represented in Kelvin.
* @hide
@@ -7046,6 +7327,9 @@ public final class Settings {
public static final String NIGHT_DISPLAY_COLOR_TEMPERATURE =
"night_display_color_temperature";
+ private static final Validator NIGHT_DISPLAY_COLOR_TEMPERATURE_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Custom time when Night display is scheduled to activate.
* Represented as milliseconds from midnight (e.g. 79200000 == 10pm).
@@ -7054,6 +7338,9 @@ public final class Settings {
public static final String NIGHT_DISPLAY_CUSTOM_START_TIME =
"night_display_custom_start_time";
+ private static final Validator NIGHT_DISPLAY_CUSTOM_START_TIME_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Custom time when Night display is scheduled to deactivate.
* Represented as milliseconds from midnight (e.g. 21600000 == 6am).
@@ -7061,6 +7348,9 @@ public final class Settings {
*/
public static final String NIGHT_DISPLAY_CUSTOM_END_TIME = "night_display_custom_end_time";
+ private static final Validator NIGHT_DISPLAY_CUSTOM_END_TIME_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* A String representing the LocalDateTime when Night display was last activated. Use to
* decide whether to apply the current activated state after a reboot or user change. In
@@ -7078,6 +7368,9 @@ public final class Settings {
*/
public static final String ENABLED_VR_LISTENERS = "enabled_vr_listeners";
+ private static final Validator ENABLED_VR_LISTENERS_VALIDATOR =
+ new SettingsValidators.ComponentNameListValidator(":");
+
/**
* Behavior of the display while in VR mode.
*
@@ -7087,6 +7380,9 @@ public final class Settings {
*/
public static final String VR_DISPLAY_MODE = "vr_display_mode";
+ private static final Validator VR_DISPLAY_MODE_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[]{"0", "1"});
+
/**
* Lower the display persistence while the system is in VR mode.
*
@@ -7143,6 +7439,9 @@ public final class Settings {
public static final String AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN =
"automatic_storage_manager_days_to_retain";
+ private static final Validator AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Default number of days of information for the automatic storage manager to retain.
*
@@ -7184,18 +7483,29 @@ public final class Settings {
public static final String SYSTEM_NAVIGATION_KEYS_ENABLED =
"system_navigation_keys_enabled";
+ private static final Validator SYSTEM_NAVIGATION_KEYS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Holds comma separated list of ordering of QS tiles.
* @hide
*/
public static final String QS_TILES = "sysui_qs_tiles";
- /**
- * Whether preloaded APKs have been installed for the user.
- * @hide
- */
- public static final String DEMO_USER_SETUP_COMPLETE
- = "demo_user_setup_complete";
+ private static final Validator QS_TILES_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ String[] tiles = value.split(",");
+ boolean valid = true;
+ for (String tile : tiles) {
+ // tile can be any non-empty string as specified by OEM
+ valid |= ((tile.length() > 0) && ANY_STRING_VALIDATOR.validate(tile));
+ }
+ return valid;
+ }
+ };
/**
* Specifies whether the web action API is enabled.
@@ -7232,18 +7542,38 @@ public final class Settings {
*/
public static final String NOTIFICATION_BADGING = "notification_badging";
+ private static final Validator NOTIFICATION_BADGING_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Comma separated list of QS tiles that have been auto-added already.
* @hide
*/
public static final String QS_AUTO_ADDED_TILES = "qs_auto_tiles";
+ private static final Validator QS_AUTO_ADDED_TILES_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ String[] tiles = value.split(",");
+ boolean valid = true;
+ for (String tile : tiles) {
+ // tile can be any non-empty string as specified by OEM
+ valid |= ((tile.length() > 0) && ANY_STRING_VALIDATOR.validate(tile));
+ }
+ return valid;
+ }
+ };
+
/**
* Whether the Lockdown button should be shown in the power menu.
* @hide
*/
public static final String LOCKDOWN_IN_POWER_MENU = "lockdown_in_power_menu";
+ private static final Validator LOCKDOWN_IN_POWER_MENU_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Backup manager behavioral parameters.
* This is encoded as a key=value list, separated by commas. Ex:
@@ -7272,6 +7602,13 @@ public final class Settings {
public static final String BACKUP_MANAGER_CONSTANTS = "backup_manager_constants";
/**
+ * Flag to set if the system should predictively attempt to re-enable Bluetooth while
+ * the user is driving.
+ * @hide
+ */
+ public static final String BLUETOOTH_ON_WHILE_DRIVING = "bluetooth_on_while_driving";
+
+ /**
* This are the settings to be backed up.
*
* NOTE: Settings are backed up and restored in the order they appear
@@ -7283,8 +7620,6 @@ public final class Settings {
public static final String[] SETTINGS_TO_BACKUP = {
BUGREPORT_IN_POWER_MENU, // moved to global
ALLOW_MOCK_LOCATION,
- PARENTAL_CONTROL_ENABLED,
- PARENTAL_CONTROL_REDIRECT_URL,
USB_MASS_STORAGE_ENABLED, // moved to global
ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
ACCESSIBILITY_DISPLAY_DALTONIZER,
@@ -7316,12 +7651,9 @@ public final class Settings {
ACCESSIBILITY_CAPTIONING_TYPEFACE,
ACCESSIBILITY_CAPTIONING_FONT_SCALE,
ACCESSIBILITY_CAPTIONING_WINDOW_COLOR,
- TTS_USE_DEFAULTS,
TTS_DEFAULT_RATE,
TTS_DEFAULT_PITCH,
TTS_DEFAULT_SYNTH,
- TTS_DEFAULT_LANG,
- TTS_DEFAULT_COUNTRY,
TTS_ENABLED_PLUGINS,
TTS_DEFAULT_LOCALE,
SHOW_IME_WITH_HARD_KEYBOARD,
@@ -7374,9 +7706,161 @@ public final class Settings {
SCREENSAVER_ACTIVATE_ON_DOCK,
SCREENSAVER_ACTIVATE_ON_SLEEP,
LOCKDOWN_IN_POWER_MENU,
+ SHOW_FIRST_CRASH_DIALOG_DEV_OPTION,
};
- /** @hide */
+ /**
+ * All settings in {@link SETTINGS_TO_BACKUP} array *must* have a non-null validator,
+ * otherwise they won't be restored.
+ *
+ * @hide
+ */
+ public static final Map<String, Validator> VALIDATORS = new ArrayMap<>();
+ static {
+ VALIDATORS.put(BUGREPORT_IN_POWER_MENU, BUGREPORT_IN_POWER_MENU_VALIDATOR);
+ VALIDATORS.put(ALLOW_MOCK_LOCATION, ALLOW_MOCK_LOCATION_VALIDATOR);
+ VALIDATORS.put(USB_MASS_STORAGE_ENABLED, USB_MASS_STORAGE_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_DISPLAY_INVERSION_ENABLED,
+ ACCESSIBILITY_DISPLAY_INVERSION_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_DISPLAY_DALTONIZER,
+ ACCESSIBILITY_DISPLAY_DALTONIZER_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED,
+ ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED,
+ ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED,
+ ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED_VALIDATOR);
+ VALIDATORS.put(AUTOFILL_SERVICE, AUTOFILL_SERVICE_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE,
+ ACCESSIBILITY_DISPLAY_MAGNIFICATION_SCALE_VALIDATOR);
+ VALIDATORS.put(ENABLED_ACCESSIBILITY_SERVICES,
+ ENABLED_ACCESSIBILITY_SERVICES_VALIDATOR);
+ VALIDATORS.put(ENABLED_VR_LISTENERS, ENABLED_VR_LISTENERS_VALIDATOR);
+ VALIDATORS.put(ENABLED_INPUT_METHODS, ENABLED_INPUT_METHODS_VALIDATOR);
+ VALIDATORS.put(TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES,
+ TOUCH_EXPLORATION_GRANTED_ACCESSIBILITY_SERVICES_VALIDATOR);
+ VALIDATORS.put(TOUCH_EXPLORATION_ENABLED, TOUCH_EXPLORATION_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_ENABLED, ACCESSIBILITY_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_SHORTCUT_TARGET_SERVICE,
+ ACCESSIBILITY_SHORTCUT_TARGET_SERVICE_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_BUTTON_TARGET_COMPONENT,
+ ACCESSIBILITY_BUTTON_TARGET_COMPONENT_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN,
+ ACCESSIBILITY_SHORTCUT_DIALOG_SHOWN_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_SHORTCUT_ENABLED,
+ ACCESSIBILITY_SHORTCUT_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN,
+ ACCESSIBILITY_SHORTCUT_ON_LOCK_SCREEN_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_SPEAK_PASSWORD, ACCESSIBILITY_SPEAK_PASSWORD_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED,
+ ACCESSIBILITY_HIGH_TEXT_CONTRAST_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_PRESET,
+ ACCESSIBILITY_CAPTIONING_PRESET_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_ENABLED,
+ ACCESSIBILITY_CAPTIONING_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_LOCALE,
+ ACCESSIBILITY_CAPTIONING_LOCALE_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR,
+ ACCESSIBILITY_CAPTIONING_BACKGROUND_COLOR_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR,
+ ACCESSIBILITY_CAPTIONING_FOREGROUND_COLOR_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_EDGE_TYPE,
+ ACCESSIBILITY_CAPTIONING_EDGE_TYPE_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_EDGE_COLOR,
+ ACCESSIBILITY_CAPTIONING_EDGE_COLOR_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_TYPEFACE,
+ ACCESSIBILITY_CAPTIONING_TYPEFACE_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_FONT_SCALE,
+ ACCESSIBILITY_CAPTIONING_FONT_SCALE_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_CAPTIONING_WINDOW_COLOR,
+ ACCESSIBILITY_CAPTIONING_WINDOW_COLOR_VALIDATOR);
+ VALIDATORS.put(TTS_DEFAULT_RATE, TTS_DEFAULT_RATE_VALIDATOR);
+ VALIDATORS.put(TTS_DEFAULT_PITCH, TTS_DEFAULT_PITCH_VALIDATOR);
+ VALIDATORS.put(TTS_DEFAULT_SYNTH, TTS_DEFAULT_SYNTH_VALIDATOR);
+ VALIDATORS.put(TTS_ENABLED_PLUGINS, TTS_ENABLED_PLUGINS_VALIDATOR);
+ VALIDATORS.put(TTS_DEFAULT_LOCALE, TTS_DEFAULT_LOCALE_VALIDATOR);
+ VALIDATORS.put(SHOW_IME_WITH_HARD_KEYBOARD, SHOW_IME_WITH_HARD_KEYBOARD_VALIDATOR);
+ VALIDATORS.put(WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON,
+ WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR);
+ VALIDATORS.put(WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY,
+ WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY_VALIDATOR);
+ VALIDATORS.put(WIFI_NUM_OPEN_NETWORKS_KEPT, WIFI_NUM_OPEN_NETWORKS_KEPT_VALIDATOR);
+ VALIDATORS.put(SELECTED_SPELL_CHECKER, SELECTED_SPELL_CHECKER_VALIDATOR);
+ VALIDATORS.put(SELECTED_SPELL_CHECKER_SUBTYPE,
+ SELECTED_SPELL_CHECKER_SUBTYPE_VALIDATOR);
+ VALIDATORS.put(SPELL_CHECKER_ENABLED, SPELL_CHECKER_ENABLED_VALIDATOR);
+ VALIDATORS.put(MOUNT_PLAY_NOTIFICATION_SND, MOUNT_PLAY_NOTIFICATION_SND_VALIDATOR);
+ VALIDATORS.put(MOUNT_UMS_AUTOSTART, MOUNT_UMS_AUTOSTART_VALIDATOR);
+ VALIDATORS.put(MOUNT_UMS_PROMPT, MOUNT_UMS_PROMPT_VALIDATOR);
+ VALIDATORS.put(MOUNT_UMS_NOTIFY_ENABLED, MOUNT_UMS_NOTIFY_ENABLED_VALIDATOR);
+ VALIDATORS.put(SLEEP_TIMEOUT, SLEEP_TIMEOUT_VALIDATOR);
+ VALIDATORS.put(DOUBLE_TAP_TO_WAKE, DOUBLE_TAP_TO_WAKE_VALIDATOR);
+ VALIDATORS.put(WAKE_GESTURE_ENABLED, WAKE_GESTURE_ENABLED_VALIDATOR);
+ VALIDATORS.put(LONG_PRESS_TIMEOUT, LONG_PRESS_TIMEOUT_VALIDATOR);
+ VALIDATORS.put(CAMERA_GESTURE_DISABLED, CAMERA_GESTURE_DISABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_AUTOCLICK_ENABLED,
+ ACCESSIBILITY_AUTOCLICK_ENABLED_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_AUTOCLICK_DELAY, ACCESSIBILITY_AUTOCLICK_DELAY_VALIDATOR);
+ VALIDATORS.put(ACCESSIBILITY_LARGE_POINTER_ICON,
+ ACCESSIBILITY_LARGE_POINTER_ICON_VALIDATOR);
+ VALIDATORS.put(PREFERRED_TTY_MODE, PREFERRED_TTY_MODE_VALIDATOR);
+ VALIDATORS.put(ENHANCED_VOICE_PRIVACY_ENABLED,
+ ENHANCED_VOICE_PRIVACY_ENABLED_VALIDATOR);
+ VALIDATORS.put(TTY_MODE_ENABLED, TTY_MODE_ENABLED_VALIDATOR);
+ VALIDATORS.put(INCALL_POWER_BUTTON_BEHAVIOR, INCALL_POWER_BUTTON_BEHAVIOR_VALIDATOR);
+ VALIDATORS.put(NIGHT_DISPLAY_CUSTOM_START_TIME,
+ NIGHT_DISPLAY_CUSTOM_START_TIME_VALIDATOR);
+ VALIDATORS.put(NIGHT_DISPLAY_CUSTOM_END_TIME, NIGHT_DISPLAY_CUSTOM_END_TIME_VALIDATOR);
+ VALIDATORS.put(NIGHT_DISPLAY_COLOR_TEMPERATURE,
+ NIGHT_DISPLAY_COLOR_TEMPERATURE_VALIDATOR);
+ VALIDATORS.put(NIGHT_DISPLAY_AUTO_MODE, NIGHT_DISPLAY_AUTO_MODE_VALIDATOR);
+ VALIDATORS.put(SYNC_PARENT_SOUNDS, SYNC_PARENT_SOUNDS_VALIDATOR);
+ VALIDATORS.put(CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED,
+ CAMERA_DOUBLE_TWIST_TO_FLIP_ENABLED_VALIDATOR);
+ VALIDATORS.put(CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED,
+ CAMERA_DOUBLE_TAP_POWER_GESTURE_DISABLED_VALIDATOR);
+ VALIDATORS.put(SYSTEM_NAVIGATION_KEYS_ENABLED,
+ SYSTEM_NAVIGATION_KEYS_ENABLED_VALIDATOR);
+ VALIDATORS.put(QS_TILES, QS_TILES_VALIDATOR);
+ VALIDATORS.put(DOZE_ENABLED, DOZE_ENABLED_VALIDATOR);
+ VALIDATORS.put(DOZE_PULSE_ON_PICK_UP, DOZE_PULSE_ON_PICK_UP_VALIDATOR);
+ VALIDATORS.put(DOZE_PULSE_ON_DOUBLE_TAP, DOZE_PULSE_ON_DOUBLE_TAP_VALIDATOR);
+ VALIDATORS.put(NFC_PAYMENT_DEFAULT_COMPONENT, NFC_PAYMENT_DEFAULT_COMPONENT_VALIDATOR);
+ VALIDATORS.put(AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN,
+ AUTOMATIC_STORAGE_MANAGER_DAYS_TO_RETAIN_VALIDATOR);
+ VALIDATORS.put(ASSIST_GESTURE_ENABLED, ASSIST_GESTURE_ENABLED_VALIDATOR);
+ VALIDATORS.put(ASSIST_GESTURE_SENSITIVITY, ASSIST_GESTURE_SENSITIVITY_VALIDATOR);
+ VALIDATORS.put(ASSIST_GESTURE_SETUP_COMPLETE, ASSIST_GESTURE_SETUP_COMPLETE_VALIDATOR);
+ VALIDATORS.put(ASSIST_GESTURE_SILENCE_ALERTS_ENABLED,
+ ASSIST_GESTURE_SILENCE_ALERTS_ENABLED_VALIDATOR);
+ VALIDATORS.put(ASSIST_GESTURE_WAKE_ENABLED, ASSIST_GESTURE_WAKE_ENABLED_VALIDATOR);
+ VALIDATORS.put(VR_DISPLAY_MODE, VR_DISPLAY_MODE_VALIDATOR);
+ VALIDATORS.put(NOTIFICATION_BADGING, NOTIFICATION_BADGING_VALIDATOR);
+ VALIDATORS.put(QS_AUTO_ADDED_TILES, QS_AUTO_ADDED_TILES_VALIDATOR);
+ VALIDATORS.put(SCREENSAVER_ENABLED, SCREENSAVER_ENABLED_VALIDATOR);
+ VALIDATORS.put(SCREENSAVER_COMPONENTS, SCREENSAVER_COMPONENTS_VALIDATOR);
+ VALIDATORS.put(SCREENSAVER_ACTIVATE_ON_DOCK, SCREENSAVER_ACTIVATE_ON_DOCK_VALIDATOR);
+ VALIDATORS.put(SCREENSAVER_ACTIVATE_ON_SLEEP, SCREENSAVER_ACTIVATE_ON_SLEEP_VALIDATOR);
+ VALIDATORS.put(LOCKDOWN_IN_POWER_MENU, LOCKDOWN_IN_POWER_MENU_VALIDATOR);
+ VALIDATORS.put(SHOW_FIRST_CRASH_DIALOG_DEV_OPTION,
+ SHOW_FIRST_CRASH_DIALOG_DEV_OPTION_VALIDATOR);
+ VALIDATORS.put(ENABLED_NOTIFICATION_LISTENERS,
+ ENABLED_NOTIFICATION_LISTENERS_VALIDATOR); //legacy restore setting
+ VALIDATORS.put(ENABLED_NOTIFICATION_ASSISTANT,
+ ENABLED_NOTIFICATION_ASSISTANT_VALIDATOR); //legacy restore setting
+ VALIDATORS.put(ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES,
+ ENABLED_NOTIFICATION_POLICY_ACCESS_PACKAGES_VALIDATOR); //legacy restore setting
+ }
+
+ /**
+ * Keys we no longer back up under the current schema, but want to continue to
+ * process when restoring historical backup datasets.
+ *
+ * All settings in {@link LEGACY_RESTORE_SETTINGS} array *must* have a non-null validator,
+ * otherwise they won't be restored.
+ *
+ * @hide
+ */
public static final String[] LEGACY_RESTORE_SETTINGS = {
ENABLED_NOTIFICATION_LISTENERS,
ENABLED_NOTIFICATION_ASSISTANT,
@@ -7398,7 +7882,6 @@ public final class Settings {
CLONE_TO_MANAGED_PROFILE.add(ENABLED_ACCESSIBILITY_SERVICES);
CLONE_TO_MANAGED_PROFILE.add(ENABLED_INPUT_METHODS);
CLONE_TO_MANAGED_PROFILE.add(LOCATION_MODE);
- CLONE_TO_MANAGED_PROFILE.add(LOCATION_PREVIOUS_MODE);
CLONE_TO_MANAGED_PROFILE.add(LOCATION_PROVIDERS_ALLOWED);
CLONE_TO_MANAGED_PROFILE.add(SELECTED_INPUT_METHOD_SUBTYPE);
}
@@ -7449,8 +7932,7 @@ public final class Settings {
* @param provider the location provider to query
* @return true if the provider is enabled
*
- * @deprecated use {@link #LOCATION_MODE} or
- * {@link LocationManager#isProviderEnabled(String)}
+ * @deprecated use {@link LocationManager#isProviderEnabled(String)}
*/
@Deprecated
public static final boolean isLocationProviderEnabled(ContentResolver cr, String provider) {
@@ -7463,12 +7945,13 @@ public final class Settings {
* @param provider the location provider to query
* @param userId the userId to query
* @return true if the provider is enabled
- * @deprecated use {@link #LOCATION_MODE} or
- * {@link LocationManager#isProviderEnabled(String)}
+ *
+ * @deprecated use {@link LocationManager#isProviderEnabled(String)}
* @hide
*/
@Deprecated
- public static final boolean isLocationProviderEnabledForUser(ContentResolver cr, String provider, int userId) {
+ public static final boolean isLocationProviderEnabledForUser(
+ ContentResolver cr, String provider, int userId) {
String allowedProviders = Settings.Secure.getStringForUser(cr,
LOCATION_PROVIDERS_ALLOWED, userId);
return TextUtils.delimitedStringContains(allowedProviders, ',', provider);
@@ -7479,7 +7962,8 @@ public final class Settings {
* @param cr the content resolver to use
* @param provider the location provider to enable or disable
* @param enabled true if the provider should be enabled
- * @deprecated use {@link #putInt(ContentResolver, String, int)} and {@link #LOCATION_MODE}
+ * @deprecated This API is deprecated. It requires WRITE_SECURE_SETTINGS permission to
+ * change location settings.
*/
@Deprecated
public static final void setLocationProviderEnabled(ContentResolver cr,
@@ -7495,8 +7979,8 @@ public final class Settings {
* @param enabled true if the provider should be enabled
* @param userId the userId for which to enable/disable providers
* @return true if the value was set, false on database errors
- * @deprecated use {@link #putIntForUser(ContentResolver, String, int, int)} and
- * {@link #LOCATION_MODE}
+ *
+ * @deprecated use {@link LocationManager#setProviderEnabledForUser(String, boolean, int)}
* @hide
*/
@Deprecated
@@ -7517,28 +8001,6 @@ public final class Settings {
}
/**
- * Saves the current location mode into {@link #LOCATION_PREVIOUS_MODE}.
- */
- private static final boolean saveLocationModeForUser(ContentResolver cr, int userId) {
- final int mode = getLocationModeForUser(cr, userId);
- return putIntForUser(cr, Settings.Secure.LOCATION_PREVIOUS_MODE, mode, userId);
- }
-
- /**
- * Restores the current location mode from {@link #LOCATION_PREVIOUS_MODE}.
- */
- private static final boolean restoreLocationModeForUser(ContentResolver cr, int userId) {
- int mode = getIntForUser(cr, Settings.Secure.LOCATION_PREVIOUS_MODE,
- LOCATION_MODE_HIGH_ACCURACY, userId);
- // Make sure that the previous mode is never "off". Otherwise the user won't be able to
- // turn on location any longer.
- if (mode == LOCATION_MODE_OFF) {
- mode = LOCATION_MODE_HIGH_ACCURACY;
- }
- return setLocationModeForUser(cr, mode, userId);
- }
-
- /**
* Thread-safe method for setting the location mode to one of
* {@link #LOCATION_MODE_HIGH_ACCURACY}, {@link #LOCATION_MODE_SENSORS_ONLY},
* {@link #LOCATION_MODE_BATTERY_SAVING}, or {@link #LOCATION_MODE_OFF}.
@@ -7551,18 +8013,20 @@ public final class Settings {
* @return true if the value was set, false on database errors
*
* @throws IllegalArgumentException if mode is not one of the supported values
+ *
+ * @deprecated To enable/disable location, use
+ * {@link LocationManager#setLocationEnabledForUser(boolean, int)}.
+ * To enable/disable a specific location provider, use
+ * {@link LocationManager#setProviderEnabledForUser(String, boolean, int)}.
*/
- private static final boolean setLocationModeForUser(ContentResolver cr, int mode,
- int userId) {
+ @Deprecated
+ private static boolean setLocationModeForUser(
+ ContentResolver cr, int mode, int userId) {
synchronized (mLocationSettingsLock) {
boolean gps = false;
boolean network = false;
switch (mode) {
- case LOCATION_MODE_PREVIOUS:
- // Retrieve the actual mode and set to that mode.
- return restoreLocationModeForUser(cr, userId);
case LOCATION_MODE_OFF:
- saveLocationModeForUser(cr, userId);
break;
case LOCATION_MODE_SENSORS_ONLY:
gps = true;
@@ -7577,15 +8041,7 @@ public final class Settings {
default:
throw new IllegalArgumentException("Invalid location mode: " + mode);
}
- // Note it's important that we set the NLP mode first. The Google implementation
- // of NLP clears its NLP consent setting any time it receives a
- // LocationManager.PROVIDERS_CHANGED_ACTION broadcast and NLP is disabled. Also,
- // it shows an NLP consent dialog any time it receives the broadcast, NLP is
- // enabled, and the NLP consent is not set. If 1) we were to enable GPS first,
- // 2) a setup wizard has its own NLP consent UI that sets the NLP consent setting,
- // and 3) the receiver happened to complete before we enabled NLP, then the Google
- // NLP would detect the attempt to enable NLP and show a redundant NLP consent
- // dialog. Then the people who wrote the setup wizard would be sad.
+
boolean nlpSuccess = Settings.Secure.setLocationProviderEnabledForUser(
cr, LocationManager.NETWORK_PROVIDER, network, userId);
boolean gpsSuccess = Settings.Secure.setLocationProviderEnabledForUser(
@@ -7772,12 +8228,16 @@ public final class Settings {
*/
public static final String AUTO_TIME = "auto_time";
+ private static final Validator AUTO_TIME_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Value to specify if the user prefers the time zone
* to be automatically fetched from the network (NITZ). 1=yes, 0=no
*/
public static final String AUTO_TIME_ZONE = "auto_time_zone";
+ private static final Validator AUTO_TIME_ZONE_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* URI for the car dock "in" event sound.
* @hide
@@ -7808,6 +8268,8 @@ public final class Settings {
*/
public static final String DOCK_SOUNDS_ENABLED = "dock_sounds_enabled";
+ private static final Validator DOCK_SOUNDS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether to play a sound for dock events, only when an accessibility service is on.
* @hide
@@ -7845,6 +8307,8 @@ public final class Settings {
*/
public static final String POWER_SOUNDS_ENABLED = "power_sounds_enabled";
+ private static final Validator POWER_SOUNDS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* URI for the "wireless charging started" sound.
* @hide
@@ -7858,6 +8322,8 @@ public final class Settings {
*/
public static final String CHARGING_SOUNDS_ENABLED = "charging_sounds_enabled";
+ private static final Validator CHARGING_SOUNDS_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether we keep the device on while the device is plugged in.
* Supported values are:
@@ -7871,6 +8337,30 @@ public final class Settings {
*/
public static final String STAY_ON_WHILE_PLUGGED_IN = "stay_on_while_plugged_in";
+ private static final Validator STAY_ON_WHILE_PLUGGED_IN_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ try {
+ int val = Integer.parseInt(value);
+ return (val == 0)
+ || (val == BatteryManager.BATTERY_PLUGGED_AC)
+ || (val == BatteryManager.BATTERY_PLUGGED_USB)
+ || (val == BatteryManager.BATTERY_PLUGGED_WIRELESS)
+ || (val == (BatteryManager.BATTERY_PLUGGED_AC
+ | BatteryManager.BATTERY_PLUGGED_USB))
+ || (val == (BatteryManager.BATTERY_PLUGGED_AC
+ | BatteryManager.BATTERY_PLUGGED_WIRELESS))
+ || (val == (BatteryManager.BATTERY_PLUGGED_USB
+ | BatteryManager.BATTERY_PLUGGED_WIRELESS))
+ || (val == (BatteryManager.BATTERY_PLUGGED_AC
+ | BatteryManager.BATTERY_PLUGGED_USB
+ | BatteryManager.BATTERY_PLUGGED_WIRELESS));
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ };
+
/**
* When the user has enable the option to have a "bug report" command
* in the power menu.
@@ -7878,6 +8368,8 @@ public final class Settings {
*/
public static final String BUGREPORT_IN_POWER_MENU = "bugreport_in_power_menu";
+ private static final Validator BUGREPORT_IN_POWER_MENU_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Whether ADB is enabled.
*/
@@ -7901,6 +8393,8 @@ public final class Settings {
*/
public static final String BLUETOOTH_ON = "bluetooth_on";
+ private static final Validator BLUETOOTH_ON_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* CDMA Cell Broadcast SMS
* 0 = CDMA Cell Broadcast SMS disabled
@@ -8519,6 +9013,8 @@ public final class Settings {
*/
public static final String USB_MASS_STORAGE_ENABLED = "usb_mass_storage_enabled";
+ private static final Validator USB_MASS_STORAGE_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* If this setting is set (to anything), then all references
* to Gmail on the device must change to Google Mail.
@@ -8655,6 +9151,25 @@ public final class Settings {
public static final String WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON =
"wifi_networks_available_notification_on";
+ private static final Validator WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
+ /**
+ * Whether to notify the user of carrier networks.
+ * <p>
+ * If not connected and the scan results have a carrier network, we will
+ * put this notification up. If we attempt to connect to a network or
+ * the carrier network(s) disappear, we remove the notification. When we
+ * show the notification, we will not show it again for
+ * {@link android.provider.Settings.Global#WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY} time.
+ * @hide
+ */
+ public static final String WIFI_CARRIER_NETWORKS_AVAILABLE_NOTIFICATION_ON =
+ "wifi_carrier_networks_available_notification_on";
+
+ private static final Validator WIFI_CARRIER_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR =
+ BOOLEAN_VALIDATOR;
+
/**
* {@hide}
*/
@@ -8668,6 +9183,9 @@ public final class Settings {
public static final String WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY =
"wifi_networks_available_repeat_delay";
+ private static final Validator WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* 802.11 country code in ISO 3166 format
* @hide
@@ -8697,6 +9215,9 @@ public final class Settings {
*/
public static final String WIFI_NUM_OPEN_NETWORKS_KEPT = "wifi_num_open_networks_kept";
+ private static final Validator WIFI_NUM_OPEN_NETWORKS_KEPT_VALIDATOR =
+ NON_NEGATIVE_INTEGER_VALIDATOR;
+
/**
* Whether the Wi-Fi should be on. Only the Wi-Fi service should touch this.
*/
@@ -8711,10 +9232,14 @@ public final class Settings {
/**
* Whether soft AP will shut down after a timeout period when no devices are connected.
+ *
+ * Type: int (0 for false, 1 for true)
* @hide
*/
public static final String SOFT_AP_TIMEOUT_ENABLED = "soft_ap_timeout_enabled";
+ private static final Validator SOFT_AP_TIMEOUT_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Value to specify if Wi-Fi Wakeup feature is enabled.
*
@@ -8724,6 +9249,8 @@ public final class Settings {
@SystemApi
public static final String WIFI_WAKEUP_ENABLED = "wifi_wakeup_enabled";
+ private static final Validator WIFI_WAKEUP_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* Value to specify if Wi-Fi Wakeup is available.
*
@@ -8770,6 +9297,9 @@ public final class Settings {
public static final String NETWORK_RECOMMENDATIONS_ENABLED =
"network_recommendations_enabled";
+ private static final Validator NETWORK_RECOMMENDATIONS_ENABLED_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[] {"-1", "0", "1"});
+
/**
* Which package name to use for network recommendations. If null, network recommendations
* will neither be requested nor accepted.
@@ -8790,8 +9320,16 @@ public final class Settings {
* Type: string package name or null if the feature is either not provided or disabled.
* @hide
*/
+ @TestApi
public static final String USE_OPEN_WIFI_PACKAGE = "use_open_wifi_package";
+ private static final Validator USE_OPEN_WIFI_PACKAGE_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ return (value == null) || PACKAGE_NAME_VALIDATOR.validate(value);
+ }
+ };
+
/**
* The number of milliseconds the {@link com.android.server.NetworkScoreService}
* will give a recommendation request to complete before returning a default response.
@@ -8813,13 +9351,52 @@ public final class Settings {
public static final String RECOMMENDED_NETWORK_EVALUATOR_CACHE_EXPIRY_MS =
"recommended_network_evaluator_cache_expiry_ms";
- /**
+ /**
* Settings to allow BLE scans to be enabled even when Bluetooth is turned off for
* connectivity.
* @hide
*/
- public static final String BLE_SCAN_ALWAYS_AVAILABLE =
- "ble_scan_always_enabled";
+ public static final String BLE_SCAN_ALWAYS_AVAILABLE = "ble_scan_always_enabled";
+
+ /**
+ * The length in milliseconds of a BLE scan window in a low-power scan mode.
+ * @hide
+ */
+ public static final String BLE_SCAN_LOW_POWER_WINDOW_MS = "ble_scan_low_power_window_ms";
+
+ /**
+ * The length in milliseconds of a BLE scan window in a balanced scan mode.
+ * @hide
+ */
+ public static final String BLE_SCAN_BALANCED_WINDOW_MS = "ble_scan_balanced_window_ms";
+
+ /**
+ * The length in milliseconds of a BLE scan window in a low-latency scan mode.
+ * @hide
+ */
+ public static final String BLE_SCAN_LOW_LATENCY_WINDOW_MS =
+ "ble_scan_low_latency_window_ms";
+
+ /**
+ * The length in milliseconds of a BLE scan interval in a low-power scan mode.
+ * @hide
+ */
+ public static final String BLE_SCAN_LOW_POWER_INTERVAL_MS =
+ "ble_scan_low_power_interval_ms";
+
+ /**
+ * The length in milliseconds of a BLE scan interval in a balanced scan mode.
+ * @hide
+ */
+ public static final String BLE_SCAN_BALANCED_INTERVAL_MS =
+ "ble_scan_balanced_interval_ms";
+
+ /**
+ * The length in milliseconds of a BLE scan interval in a low-latency scan mode.
+ * @hide
+ */
+ public static final String BLE_SCAN_LOW_LATENCY_INTERVAL_MS =
+ "ble_scan_low_latency_interval_ms";
/**
* Used to save the Wifi_ON state prior to tethering.
@@ -8871,6 +9448,9 @@ public final class Settings {
public static final String WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED =
"wifi_watchdog_poor_network_test_enabled";
+ private static final Validator WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED_VALIDATOR =
+ ANY_STRING_VALIDATOR;
+
/**
* Setting to turn on suspend optimizations at screen off on Wi-Fi. Enabled by default and
* needs to be set to 0 to disable it.
@@ -8887,6 +9467,14 @@ public final class Settings {
public static final String WIFI_VERBOSE_LOGGING_ENABLED =
"wifi_verbose_logging_enabled";
+ /**
+ * Setting to enable connected MAC randomization in Wi-Fi; disabled by default, and
+ * setting to 1 will enable it. In the future, additional values may be supported.
+ * @hide
+ */
+ public static final String WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED =
+ "wifi_connected_mac_randomization_enabled";
+
/**
* The maximum number of times we will retry a connection to an access
* point for which we have failed in acquiring an IP address from DHCP.
@@ -9406,11 +9994,16 @@ public final class Settings {
* @hide
*/
public static final String PRIVATE_DNS_MODE = "private_dns_mode";
+
+ private static final Validator PRIVATE_DNS_MODE_VALIDATOR = ANY_STRING_VALIDATOR;
+
/**
* @hide
*/
public static final String PRIVATE_DNS_SPECIFIER = "private_dns_specifier";
+ private static final Validator PRIVATE_DNS_SPECIFIER_VALIDATOR = ANY_STRING_VALIDATOR;
+
/** {@hide} */
public static final String
BLUETOOTH_HEADSET_PRIORITY_PREFIX = "bluetooth_headset_priority_";
@@ -9586,7 +10179,8 @@ public final class 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"
+ * "high_usage_app_count=3,reduced_battery_enabled=false,reduced_battery_percent=50,"
+ * "high_usage_battery_draining=25,high_usage_period_ms=3000"
*
* The following keys are supported:
*
@@ -9596,6 +10190,8 @@ public final class Settings {
* battery_saver_tip_enabled (boolean)
* high_usage_enabled (boolean)
* high_usage_app_count (int)
+ * high_usage_period_ms (long)
+ * high_usage_battery_draining (int)
* app_restriction_enabled (boolean)
* reduced_battery_enabled (boolean)
* reduced_battery_percent (int)
@@ -9626,6 +10222,25 @@ public final class Settings {
public static final String ALWAYS_ON_DISPLAY_CONSTANTS = "always_on_display_constants";
/**
+ * System VDSO global setting. This links to the "sys.vdso" system property.
+ * The following values are supported:
+ * false -> both 32 and 64 bit vdso disabled
+ * 32 -> 32 bit vdso enabled
+ * 64 -> 64 bit vdso enabled
+ * Any other value defaults to both 32 bit and 64 bit true.
+ * @hide
+ */
+ public static final String SYS_VDSO = "sys_vdso";
+
+ /**
+ * An integer to reduce the FPS by this factor. Only for experiments. Need to reboot the
+ * device for this setting to take full effect.
+ *
+ * @hide
+ */
+ public static final String FPS_DEVISOR = "fps_divisor";
+
+ /**
* App standby (app idle) specific settings.
* This is encoded as a key=value list, separated by commas. Ex:
* <p>
@@ -9781,6 +10396,24 @@ public final class Settings {
public static final String TEXT_CLASSIFIER_CONSTANTS = "text_classifier_constants";
/**
+ * BatteryStats specific settings.
+ * This is encoded as a key=value list, separated by commas. Ex: "foo=1,bar=true"
+ *
+ * The following keys are supported:
+ * <pre>
+ * track_cpu_times_by_proc_state (boolean)
+ * track_cpu_active_cluster_time (boolean)
+ * read_binary_cpu_time (boolean)
+ * </pre>
+ *
+ * <p>
+ * Type: string
+ * @hide
+ * see also com.android.internal.os.BatteryStatsImpl.Constants
+ */
+ public static final String BATTERY_STATS_CONSTANTS = "battery_stats_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)
@@ -9790,6 +10423,31 @@ public final class Settings {
public static final java.lang.String APP_STANDBY_ENABLED = "app_standby_enabled";
/**
+ * Feature flag to enable or disable the Forced App Standby feature.
+ * Type: int (0 for false, 1 for true)
+ * Default: 1
+ * @hide
+ */
+ public static final String FORCED_APP_STANDBY_ENABLED = "forced_app_standby_enabled";
+
+ /**
+ * Whether or not to enable Forced App Standby on small battery devices.
+ * Type: int (0 for false, 1 for true)
+ * Default: 0
+ * @hide
+ */
+ public static final String FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED
+ = "forced_app_standby_for_small_battery_enabled";
+
+ /**
+ * Whether or not Network Watchlist feature is enabled.
+ * Type: int (0 for false, 1 for true)
+ * Default: 0
+ * @hide
+ */
+ public static final String NETWORK_WATCHLIST_ENABLED = "network_watchlist_enabled";
+
+ /**
* Get the key that retrieves a bluetooth headset's priority.
* @hide
*/
@@ -9932,6 +10590,9 @@ public final class Settings {
*/
public static final String EMERGENCY_TONE = "emergency_tone";
+ private static final Validator EMERGENCY_TONE_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[] {"0", "1", "2"});
+
/**
* CDMA only settings
* Whether the auto retry is enabled. The value is
@@ -9940,6 +10601,8 @@ public final class Settings {
*/
public static final String CALL_AUTO_RETRY = "call_auto_retry";
+ private static final Validator CALL_AUTO_RETRY_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* A setting that can be read whether the emergency affordance is currently needed.
* The value is a boolean (1 or 0).
@@ -9999,6 +10662,7 @@ public final class Settings {
* If 1 low power mode is enabled.
* @hide
*/
+ @TestApi
public static final String LOW_POWER_MODE = "low_power";
/**
@@ -10008,6 +10672,9 @@ public final class Settings {
*/
public static final String LOW_POWER_MODE_TRIGGER_LEVEL = "low_power_trigger_level";
+ private static final Validator LOW_POWER_MODE_TRIGGER_LEVEL_VALIDATOR =
+ new SettingsValidators.InclusiveIntegerRangeValidator(0, 99);
+
/**
* If not 0, the activity manager will aggressively finish activities and
* processes as soon as they are no longer needed. If 0, the normal
@@ -10023,6 +10690,8 @@ public final class Settings {
*/
public static final String DOCK_AUDIO_MEDIA_ENABLED = "dock_audio_media_enabled";
+ private static final Validator DOCK_AUDIO_MEDIA_ENABLED_VALIDATOR = BOOLEAN_VALIDATOR;
+
/**
* The surround sound formats AC3, DTS or IEC61937 are
* available for use if they are detected.
@@ -10069,6 +10738,9 @@ public final class Settings {
*/
public static final String ENCODED_SURROUND_OUTPUT = "encoded_surround_output";
+ private static final Validator ENCODED_SURROUND_OUTPUT_VALIDATOR =
+ new SettingsValidators.DiscreteValueValidator(new String[] {"0", "1", "2"});
+
/**
* Persisted safe headphone volume management state by AudioService
* @hide
@@ -10564,10 +11236,20 @@ public final class Settings {
*
* @hide
*/
+ @TestApi
public static final String LOCATION_GLOBAL_KILL_SWITCH =
"location_global_kill_switch";
/**
+ * If set to 1, SettingsProvider's restoreAnyVersion="true" attribute will be ignored
+ * and restoring to lower version of platform API will be skipped.
+ *
+ * @hide
+ */
+ public static final String OVERRIDE_SETTINGS_PROVIDER_RESTORE_ANY_VERSION =
+ "override_settings_provider_restore_any_version";
+
+ /**
* Settings to backup. This is here so that it's in the same place as the settings
* keys and easy to update.
*
@@ -10595,6 +11277,7 @@ public final class Settings {
NETWORK_RECOMMENDATIONS_ENABLED,
WIFI_WAKEUP_ENABLED,
WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON,
+ WIFI_CARRIER_NETWORKS_AVAILABLE_NOTIFICATION_ON,
USE_OPEN_WIFI_PACKAGE,
WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED,
EMERGENCY_TONE,
@@ -10609,6 +11292,43 @@ public final class Settings {
};
/**
+ * All settings in {@link SETTINGS_TO_BACKUP} array *must* have a non-null validator,
+ * otherwise they won't be restored.
+ *
+ * @hide
+ */
+ public static final Map<String, Validator> VALIDATORS = new ArrayMap<>();
+ static {
+ VALIDATORS.put(BUGREPORT_IN_POWER_MENU, BUGREPORT_IN_POWER_MENU_VALIDATOR);
+ VALIDATORS.put(STAY_ON_WHILE_PLUGGED_IN, STAY_ON_WHILE_PLUGGED_IN_VALIDATOR);
+ VALIDATORS.put(AUTO_TIME, AUTO_TIME_VALIDATOR);
+ VALIDATORS.put(AUTO_TIME_ZONE, AUTO_TIME_ZONE_VALIDATOR);
+ VALIDATORS.put(POWER_SOUNDS_ENABLED, POWER_SOUNDS_ENABLED_VALIDATOR);
+ VALIDATORS.put(DOCK_SOUNDS_ENABLED, DOCK_SOUNDS_ENABLED_VALIDATOR);
+ VALIDATORS.put(CHARGING_SOUNDS_ENABLED, CHARGING_SOUNDS_ENABLED_VALIDATOR);
+ VALIDATORS.put(USB_MASS_STORAGE_ENABLED, USB_MASS_STORAGE_ENABLED_VALIDATOR);
+ VALIDATORS.put(NETWORK_RECOMMENDATIONS_ENABLED,
+ NETWORK_RECOMMENDATIONS_ENABLED_VALIDATOR);
+ VALIDATORS.put(WIFI_WAKEUP_ENABLED, WIFI_WAKEUP_ENABLED_VALIDATOR);
+ VALIDATORS.put(WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON,
+ WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR);
+ VALIDATORS.put(USE_OPEN_WIFI_PACKAGE, USE_OPEN_WIFI_PACKAGE_VALIDATOR);
+ VALIDATORS.put(WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED,
+ WIFI_WATCHDOG_POOR_NETWORK_TEST_ENABLED_VALIDATOR);
+ VALIDATORS.put(EMERGENCY_TONE, EMERGENCY_TONE_VALIDATOR);
+ VALIDATORS.put(CALL_AUTO_RETRY, CALL_AUTO_RETRY_VALIDATOR);
+ VALIDATORS.put(DOCK_AUDIO_MEDIA_ENABLED, DOCK_AUDIO_MEDIA_ENABLED_VALIDATOR);
+ VALIDATORS.put(ENCODED_SURROUND_OUTPUT, ENCODED_SURROUND_OUTPUT_VALIDATOR);
+ VALIDATORS.put(LOW_POWER_MODE_TRIGGER_LEVEL, LOW_POWER_MODE_TRIGGER_LEVEL_VALIDATOR);
+ VALIDATORS.put(BLUETOOTH_ON, BLUETOOTH_ON_VALIDATOR);
+ VALIDATORS.put(PRIVATE_DNS_MODE, PRIVATE_DNS_MODE_VALIDATOR);
+ VALIDATORS.put(PRIVATE_DNS_SPECIFIER, PRIVATE_DNS_SPECIFIER_VALIDATOR);
+ VALIDATORS.put(SOFT_AP_TIMEOUT_ENABLED, SOFT_AP_TIMEOUT_ENABLED_VALIDATOR);
+ VALIDATORS.put(WIFI_CARRIER_NETWORKS_AVAILABLE_NOTIFICATION_ON,
+ WIFI_CARRIER_NETWORKS_AVAILABLE_NOTIFICATION_ON_VALIDATOR);
+ }
+
+ /**
* Global settings that shouldn't be persisted.
*
* @hide
@@ -10617,7 +11337,15 @@ public final class Settings {
LOCATION_GLOBAL_KILL_SWITCH,
};
- /** @hide */
+ /**
+ * Keys we no longer back up under the current schema, but want to continue to
+ * process when restoring historical backup datasets.
+ *
+ * All settings in {@link LEGACY_RESTORE_SETTINGS} array *must* have a non-null validator,
+ * otherwise they won't be restored.
+ *
+ * @hide
+ */
public static final String[] LEGACY_RESTORE_SETTINGS = {
};
@@ -11220,6 +11948,42 @@ public final class Settings {
*/
public static final String ENABLE_GNSS_RAW_MEAS_FULL_TRACKING =
"enable_gnss_raw_meas_full_tracking";
+
+ /**
+ * Whether we've enabled zram on this device. Takes effect on
+ * reboot. The value "1" enables zram; "0" disables it, and
+ * everything else is unspecified.
+ * @hide
+ */
+ public static final String ZRAM_ENABLED =
+ "zram_enabled";
+
+ /**
+ * Whether smart replies in notifications are enabled.
+ * @hide
+ */
+ public static final String ENABLE_SMART_REPLIES_IN_NOTIFICATIONS =
+ "enable_smart_replies_in_notifications";
+
+ /**
+ * If nonzero, crashes in foreground processes will bring up a dialog.
+ * Otherwise, the process will be silently killed.
+ * @hide
+ */
+ public static final String SHOW_FIRST_CRASH_DIALOG = "show_first_crash_dialog";
+
+ /**
+ * If nonzero, crash dialogs will show an option to restart the app.
+ * @hide
+ */
+ public static final String SHOW_RESTART_IN_CRASH_DIALOG = "show_restart_in_crash_dialog";
+
+ /**
+ * If nonzero, crash dialogs will show an option to mute all future crash dialogs for
+ * this app.
+ * @hide
+ */
+ public static final String SHOW_MUTE_IN_CRASH_DIALOG = "show_mute_in_crash_dialog";
}
/**
diff --git a/android/provider/SettingsValidators.java b/android/provider/SettingsValidators.java
new file mode 100644
index 00000000..5885b6b5
--- /dev/null
+++ b/android/provider/SettingsValidators.java
@@ -0,0 +1,249 @@
+/*
+ * 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.provider;
+
+import android.content.ComponentName;
+import android.net.Uri;
+
+import com.android.internal.util.ArrayUtils;
+
+import java.util.Locale;
+
+/**
+ * This class provides both interface for validation and common validators
+ * used to ensure Settings have meaningful values.
+ *
+ * @hide
+ */
+public class SettingsValidators {
+
+ public static final Validator BOOLEAN_VALIDATOR =
+ new DiscreteValueValidator(new String[] {"0", "1"});
+
+ public static final Validator ANY_STRING_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ return true;
+ }
+ };
+
+ public static final Validator NON_NEGATIVE_INTEGER_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ try {
+ return Integer.parseInt(value) >= 0;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ };
+
+ public static final Validator ANY_INTEGER_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ try {
+ Integer.parseInt(value);
+ return true;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ };
+
+ public static final Validator URI_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ try {
+ Uri.decode(value);
+ return true;
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+ };
+
+ public static final Validator COMPONENT_NAME_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ return ComponentName.unflattenFromString(value) != null;
+ }
+ };
+
+ public static final Validator PACKAGE_NAME_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ return value != null && isStringPackageName(value);
+ }
+
+ private boolean isStringPackageName(String value) {
+ // The name may contain uppercase or lowercase letters ('A' through 'Z'), numbers,
+ // and underscores ('_'). However, individual package name parts may only
+ // start with letters.
+ // (https://developer.android.com/guide/topics/manifest/manifest-element.html#package)
+ if (value == null) {
+ return false;
+ }
+ String[] subparts = value.split("\\.");
+ boolean isValidPackageName = true;
+ for (String subpart : subparts) {
+ isValidPackageName &= isSubpartValidForPackageName(subpart);
+ if (!isValidPackageName) break;
+ }
+ return isValidPackageName;
+ }
+
+ private boolean isSubpartValidForPackageName(String subpart) {
+ if (subpart.length() == 0) return false;
+ boolean isValidSubpart = Character.isLetter(subpart.charAt(0));
+ for (int i = 1; i < subpart.length(); i++) {
+ isValidSubpart &= (Character.isLetterOrDigit(subpart.charAt(i))
+ || (subpart.charAt(i) == '_'));
+ if (!isValidSubpart) break;
+ }
+ return isValidSubpart;
+ }
+ };
+
+ public static final Validator LENIENT_IP_ADDRESS_VALIDATOR = new Validator() {
+ private static final int MAX_IPV6_LENGTH = 45;
+
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ return value.length() <= MAX_IPV6_LENGTH;
+ }
+ };
+
+ public static final Validator LOCALE_VALIDATOR = new Validator() {
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ Locale[] validLocales = Locale.getAvailableLocales();
+ for (Locale locale : validLocales) {
+ if (value.equals(locale.toString())) {
+ return true;
+ }
+ }
+ return false;
+ }
+ };
+
+ public interface Validator {
+ boolean validate(String value);
+ }
+
+ public static final class DiscreteValueValidator implements Validator {
+ private final String[] mValues;
+
+ public DiscreteValueValidator(String[] values) {
+ mValues = values;
+ }
+
+ @Override
+ public boolean validate(String value) {
+ return ArrayUtils.contains(mValues, value);
+ }
+ }
+
+ public static final class InclusiveIntegerRangeValidator implements Validator {
+ private final int mMin;
+ private final int mMax;
+
+ public InclusiveIntegerRangeValidator(int min, int max) {
+ mMin = min;
+ mMax = max;
+ }
+
+ @Override
+ public boolean validate(String value) {
+ try {
+ final int intValue = Integer.parseInt(value);
+ return intValue >= mMin && intValue <= mMax;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ }
+
+ public static final class InclusiveFloatRangeValidator implements Validator {
+ private final float mMin;
+ private final float mMax;
+
+ public InclusiveFloatRangeValidator(float min, float max) {
+ mMin = min;
+ mMax = max;
+ }
+
+ @Override
+ public boolean validate(String value) {
+ try {
+ final float floatValue = Float.parseFloat(value);
+ return floatValue >= mMin && floatValue <= mMax;
+ } catch (NumberFormatException e) {
+ return false;
+ }
+ }
+ }
+
+ public static final class ComponentNameListValidator implements Validator {
+ private final String mSeparator;
+
+ public ComponentNameListValidator(String separator) {
+ mSeparator = separator;
+ }
+
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ String[] elements = value.split(mSeparator);
+ for (String element : elements) {
+ if (!COMPONENT_NAME_VALIDATOR.validate(element)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ public static final class PackageNameListValidator implements Validator {
+ private final String mSeparator;
+
+ public PackageNameListValidator(String separator) {
+ mSeparator = separator;
+ }
+
+ @Override
+ public boolean validate(String value) {
+ if (value == null) {
+ return false;
+ }
+ String[] elements = value.split(mSeparator);
+ for (String element : elements) {
+ if (!PACKAGE_NAME_VALIDATOR.validate(element)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+}
diff --git a/android/provider/Telephony.java b/android/provider/Telephony.java
index 942ea009..8c457247 100644
--- a/android/provider/Telephony.java
+++ b/android/provider/Telephony.java
@@ -1102,6 +1102,16 @@ public final class Telephony {
"android.provider.Telephony.MMS_DOWNLOADED";
/**
+ * Broadcast Action: A debug code has been entered in the dialer. These "secret codes"
+ * are used to activate developer menus by dialing certain codes. And they are of the
+ * form {@code *#*#&lt;code&gt;#*#*}. The intent will have the data URI:
+ * {@code android_secret_code://&lt;code&gt;}.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String SECRET_CODE_ACTION =
+ "android.provider.Telephony.SECRET_CODE";
+
+ /**
* Broadcast action: When the default SMS package changes,
* the previous default SMS package and the new default SMS
* package are sent this broadcast to notify them of the change.
@@ -2564,6 +2574,35 @@ public final class Telephony {
public static final Uri CONTENT_URI = Uri.parse("content://telephony/carriers");
/**
+ * The {@code content://} style URL to be called from DevicePolicyManagerService,
+ * can manage DPC-owned APNs.
+ * @hide
+ */
+ public static final Uri DPC_URI = Uri.parse("content://telephony/carriers/dpc");
+
+ /**
+ * The {@code content://} style URL to be called from Telephony to query APNs.
+ * When DPC-owned APNs are enforced, only DPC-owned APNs are returned, otherwise only
+ * non-DPC-owned APNs are returned.
+ * @hide
+ */
+ public static final Uri FILTERED_URI = Uri.parse("content://telephony/carriers/filtered");
+
+ /**
+ * The {@code content://} style URL to be called from DevicePolicyManagerService
+ * or Telephony to manage whether DPC-owned APNs are enforced.
+ * @hide
+ */
+ public static final Uri ENFORCE_MANAGED_URI = Uri.parse(
+ "content://telephony/carriers/enforce_managed");
+
+ /**
+ * The column name for ENFORCE_MANAGED_URI, indicates whether DPC-owned APNs are enforced.
+ * @hide
+ */
+ public static final String ENFORCE_KEY = "enforced";
+
+ /**
* The default sort order for this table.
*/
public static final String DEFAULT_SORT_ORDER = "name ASC";
@@ -2693,6 +2732,7 @@ public final class Telephony {
* but is currently only used for LTE (14) and eHRPD (13).
* <P>Type: INTEGER</P>
*/
+ @Deprecated
public static final String BEARER = "bearer";
/**
@@ -2704,9 +2744,19 @@ public final class Telephony {
* <P>Type: INTEGER</P>
* @hide
*/
+ @Deprecated
public static final String BEARER_BITMASK = "bearer_bitmask";
/**
+ * Radio technology (network type) bitmask.
+ * To check what values can be contained, refer to
+ * {@link android.telephony.TelephonyManager}.
+ * Bitmask for a radio tech R is (1 << (R - 1))
+ * <P>Type: INTEGER</P>
+ */
+ public static final String NETWORK_TYPE_BITMASK = "network_type_bitmask";
+
+ /**
* MVNO type:
* {@code SPN (Service Provider Name), IMSI, GID (Group Identifier Level 1)}.
* <P>Type: TEXT</P>
diff --git a/android/provider/VoicemailContract.java b/android/provider/VoicemailContract.java
index 6a3c55ef..c568b6fb 100644
--- a/android/provider/VoicemailContract.java
+++ b/android/provider/VoicemailContract.java
@@ -106,9 +106,12 @@ public class VoicemailContract {
/**
* Broadcast intent to inform a new visual voicemail SMS has been received. This intent will
- * only be delivered to the telephony service. {@link #EXTRA_VOICEMAIL_SMS} will be included.
- */
- /** @hide */
+ * only be delivered to the telephony service.
+ *
+ * @see #EXTRA_VOICEMAIL_SMS
+ * @see #EXTRA_TARGET_PACKAGE
+ *
+ * @hide */
@SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
public static final String ACTION_VOICEMAIL_SMS_RECEIVED =
"com.android.internal.provider.action.VOICEMAIL_SMS_RECEIVED";
@@ -121,6 +124,19 @@ public class VoicemailContract {
public static final String EXTRA_VOICEMAIL_SMS = "android.provider.extra.VOICEMAIL_SMS";
/**
+ * Extra in {@link #ACTION_VOICEMAIL_SMS_RECEIVED} indicating the target package to bind {@link
+ * android.telephony.VisualVoicemailService}.
+ *
+ * <p>This extra should be set to android.telephony.VisualVoicemailSmsFilterSettings#packageName
+ * while performing filtering. Since the default dialer might change between the filter sending
+ * it and telephony binding to the service, this ensures the service will not receive SMS
+ * filtered by the previous app.
+ *
+ * @hide
+ */
+ public static final String EXTRA_TARGET_PACKAGE = "android.provider.extra.TARGET_PACAKGE";
+
+ /**
* Extra included in {@link Intent#ACTION_PROVIDER_CHANGED} broadcast intents to indicate if the
* receiving package made this change.
*/
@@ -172,6 +188,11 @@ public class VoicemailContract {
*/
public static final String DURATION = Calls.DURATION;
/**
+ * Whether or not the voicemail has been acknowledged (notification sent to the user).
+ * <P>Type: INTEGER (boolean)</P>
+ */
+ public static final String NEW = Calls.NEW;
+ /**
* Whether this item has been read or otherwise consumed by the user.
* <P>Type: INTEGER (boolean)</P>
*/
diff --git a/android/security/KeyStore.java b/android/security/KeyStore.java
index fabcdf00..e25386ba 100644
--- a/android/security/KeyStore.java
+++ b/android/security/KeyStore.java
@@ -424,15 +424,6 @@ public class KeyStore {
return getmtime(key, UID_SELF);
}
- public boolean duplicate(String srcKey, int srcUid, String destKey, int destUid) {
- try {
- return mBinder.duplicate(srcKey, srcUid, destKey, destUid) == NO_ERROR;
- } catch (RemoteException e) {
- Log.w(TAG, "Cannot connect to keystore", e);
- return false;
- }
- }
-
// TODO: remove this when it's removed from Settings
public boolean isHardwareBacked() {
return isHardwareBacked("RSA");
@@ -519,6 +510,19 @@ public class KeyStore {
return importKey(alias, args, format, keyData, UID_SELF, flags, outCharacteristics);
}
+ public int importWrappedKey(String wrappedKeyAlias, byte[] wrappedKey,
+ String wrappingKeyAlias,
+ byte[] maskingKey, KeymasterArguments args, long rootSid, long fingerprintSid, int uid,
+ KeyCharacteristics outCharacteristics) {
+ try {
+ return mBinder.importWrappedKey(wrappedKeyAlias, wrappedKey, wrappingKeyAlias,
+ maskingKey, args, rootSid, fingerprintSid, outCharacteristics);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Cannot connect to keystore", e);
+ return SYSTEM_ERROR;
+ }
+ }
+
public ExportResult exportKey(String alias, int format, KeymasterBlob clientId,
KeymasterBlob appId, int uid) {
try {
diff --git a/android/security/keymaster/KeymasterDefs.java b/android/security/keymaster/KeymasterDefs.java
index f409e5b7..34643703 100644
--- a/android/security/keymaster/KeymasterDefs.java
+++ b/android/security/keymaster/KeymasterDefs.java
@@ -73,6 +73,7 @@ public final class KeymasterDefs {
public static final int KM_TAG_USER_AUTH_TYPE = KM_ENUM | 504;
public static final int KM_TAG_AUTH_TIMEOUT = KM_UINT | 505;
public static final int KM_TAG_ALLOW_WHILE_ON_BODY = KM_BOOL | 506;
+ public static final int KM_TAG_TRUSTED_USER_PRESENCE_REQUIRED = KM_BOOL | 507;
public static final int KM_TAG_ALL_APPLICATIONS = KM_BOOL | 600;
public static final int KM_TAG_APPLICATION_ID = KM_BYTES | 601;
@@ -101,6 +102,7 @@ public final class KeymasterDefs {
public static final int KM_ALGORITHM_RSA = 1;
public static final int KM_ALGORITHM_EC = 3;
public static final int KM_ALGORITHM_AES = 32;
+ public static final int KM_ALGORITHM_3DES = 33;
public static final int KM_ALGORITHM_HMAC = 128;
// Block modes.
@@ -130,6 +132,7 @@ public final class KeymasterDefs {
public static final int KM_ORIGIN_GENERATED = 0;
public static final int KM_ORIGIN_IMPORTED = 2;
public static final int KM_ORIGIN_UNKNOWN = 3;
+ public static final int KM_ORIGIN_SECURELY_IMPORTED = 4;
// Key usability requirements.
public static final int KM_BLOB_STANDALONE = 0;
@@ -140,6 +143,7 @@ public final class KeymasterDefs {
public static final int KM_PURPOSE_DECRYPT = 1;
public static final int KM_PURPOSE_SIGN = 2;
public static final int KM_PURPOSE_VERIFY = 3;
+ public static final int KM_PURPOSE_WRAP = 5;
// Key formats.
public static final int KM_KEY_FORMAT_X509 = 0;
diff --git a/android/security/keystore/AndroidKeyStore3DESCipherSpi.java b/android/security/keystore/AndroidKeyStore3DESCipherSpi.java
new file mode 100644
index 00000000..01fd0624
--- /dev/null
+++ b/android/security/keystore/AndroidKeyStore3DESCipherSpi.java
@@ -0,0 +1,298 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+import android.security.keymaster.KeymasterArguments;
+import android.security.keymaster.KeymasterDefs;
+
+import java.security.AlgorithmParameters;
+import java.security.InvalidAlgorithmParameterException;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.ProviderException;
+import java.security.spec.AlgorithmParameterSpec;
+import java.security.spec.InvalidParameterSpecException;
+import java.util.Arrays;
+
+import javax.crypto.CipherSpi;
+import javax.crypto.spec.IvParameterSpec;
+
+/**
+ * Base class for Android Keystore 3DES {@link CipherSpi} implementations.
+ *
+ * @hide
+ */
+public class AndroidKeyStore3DESCipherSpi extends AndroidKeyStoreCipherSpiBase {
+
+ private static final int BLOCK_SIZE_BYTES = 8;
+
+ private final int mKeymasterBlockMode;
+ private final int mKeymasterPadding;
+ /** Whether this transformation requires an IV. */
+ private final boolean mIvRequired;
+
+ private byte[] mIv;
+
+ /** Whether the current {@code #mIv} has been used by the underlying crypto operation. */
+ private boolean mIvHasBeenUsed;
+
+ AndroidKeyStore3DESCipherSpi(
+ int keymasterBlockMode,
+ int keymasterPadding,
+ boolean ivRequired) {
+ mKeymasterBlockMode = keymasterBlockMode;
+ mKeymasterPadding = keymasterPadding;
+ mIvRequired = ivRequired;
+ }
+
+ abstract static class ECB extends AndroidKeyStore3DESCipherSpi {
+ protected ECB(int keymasterPadding) {
+ super(KeymasterDefs.KM_MODE_ECB, keymasterPadding, false);
+ }
+
+ public static class NoPadding extends ECB {
+ public NoPadding() {
+ super(KeymasterDefs.KM_PAD_NONE);
+ }
+ }
+
+ public static class PKCS7Padding extends ECB {
+ public PKCS7Padding() {
+ super(KeymasterDefs.KM_PAD_PKCS7);
+ }
+ }
+ }
+
+ abstract static class CBC extends AndroidKeyStore3DESCipherSpi {
+ protected CBC(int keymasterPadding) {
+ super(KeymasterDefs.KM_MODE_CBC, keymasterPadding, true);
+ }
+
+ public static class NoPadding extends CBC {
+ public NoPadding() {
+ super(KeymasterDefs.KM_PAD_NONE);
+ }
+ }
+
+ public static class PKCS7Padding extends CBC {
+ public PKCS7Padding() {
+ super(KeymasterDefs.KM_PAD_PKCS7);
+ }
+ }
+ }
+
+ @Override
+ protected void initKey(int i, Key key) throws InvalidKeyException {
+ if (!(key instanceof AndroidKeyStoreSecretKey)) {
+ throw new InvalidKeyException(
+ "Unsupported key: " + ((key != null) ? key.getClass().getName() : "null"));
+ }
+ if (!KeyProperties.KEY_ALGORITHM_3DES.equalsIgnoreCase(key.getAlgorithm())) {
+ throw new InvalidKeyException(
+ "Unsupported key algorithm: " + key.getAlgorithm() + ". Only " +
+ KeyProperties.KEY_ALGORITHM_3DES + " supported");
+ }
+ setKey((AndroidKeyStoreSecretKey) key);
+ }
+
+ @Override
+ protected int engineGetBlockSize() {
+ return BLOCK_SIZE_BYTES;
+ }
+
+ @Override
+ protected int engineGetOutputSize(int inputLen) {
+ return inputLen + 3 * BLOCK_SIZE_BYTES;
+ }
+
+ @Override
+ protected final byte[] engineGetIV() {
+ return ArrayUtils.cloneIfNotEmpty(mIv);
+ }
+
+ @Override
+ protected AlgorithmParameters engineGetParameters() {
+ if (!mIvRequired) {
+ return null;
+ }
+ if ((mIv != null) && (mIv.length > 0)) {
+ try {
+ AlgorithmParameters params = AlgorithmParameters.getInstance("DESede");
+ params.init(new IvParameterSpec(mIv));
+ return params;
+ } catch (NoSuchAlgorithmException e) {
+ throw new ProviderException(
+ "Failed to obtain 3DES AlgorithmParameters", e);
+ } catch (InvalidParameterSpecException e) {
+ throw new ProviderException(
+ "Failed to initialize 3DES AlgorithmParameters with an IV",
+ e);
+ }
+ }
+ return null;
+ }
+
+ @Override
+ protected void initAlgorithmSpecificParameters() throws InvalidKeyException {
+ if (!mIvRequired) {
+ return;
+ }
+
+ // IV is used
+ if (!isEncrypting()) {
+ throw new InvalidKeyException("IV required when decrypting"
+ + ". Use IvParameterSpec or AlgorithmParameters to provide it.");
+ }
+ }
+
+ @Override
+ protected void initAlgorithmSpecificParameters(AlgorithmParameterSpec params)
+ throws InvalidAlgorithmParameterException {
+ if (!mIvRequired) {
+ if (params != null) {
+ throw new InvalidAlgorithmParameterException("Unsupported parameters: " + params);
+ }
+ return;
+ }
+
+ // IV is used
+ if (params == null) {
+ if (!isEncrypting()) {
+ // IV must be provided by the caller
+ throw new InvalidAlgorithmParameterException(
+ "IvParameterSpec must be provided when decrypting");
+ }
+ return;
+ }
+ if (!(params instanceof IvParameterSpec)) {
+ throw new InvalidAlgorithmParameterException("Only IvParameterSpec supported");
+ }
+ mIv = ((IvParameterSpec) params).getIV();
+ if (mIv == null) {
+ throw new InvalidAlgorithmParameterException("Null IV in IvParameterSpec");
+ }
+ }
+
+ @Override
+ protected void initAlgorithmSpecificParameters(AlgorithmParameters params)
+ throws InvalidAlgorithmParameterException {
+ if (!mIvRequired) {
+ if (params != null) {
+ throw new InvalidAlgorithmParameterException("Unsupported parameters: " + params);
+ }
+ return;
+ }
+
+ // IV is used
+ if (params == null) {
+ if (!isEncrypting()) {
+ // IV must be provided by the caller
+ throw new InvalidAlgorithmParameterException("IV required when decrypting"
+ + ". Use IvParameterSpec or AlgorithmParameters to provide it.");
+ }
+ return;
+ }
+
+ if (!"DESede".equalsIgnoreCase(params.getAlgorithm())) {
+ throw new InvalidAlgorithmParameterException(
+ "Unsupported AlgorithmParameters algorithm: " + params.getAlgorithm()
+ + ". Supported: DESede");
+ }
+
+ IvParameterSpec ivSpec;
+ try {
+ ivSpec = params.getParameterSpec(IvParameterSpec.class);
+ } catch (InvalidParameterSpecException e) {
+ if (!isEncrypting()) {
+ // IV must be provided by the caller
+ throw new InvalidAlgorithmParameterException("IV required when decrypting"
+ + ", but not found in parameters: " + params, e);
+ }
+ mIv = null;
+ return;
+ }
+ mIv = ivSpec.getIV();
+ if (mIv == null) {
+ throw new InvalidAlgorithmParameterException("Null IV in AlgorithmParameters");
+ }
+ }
+
+ @Override
+ protected final int getAdditionalEntropyAmountForBegin() {
+ if ((mIvRequired) && (mIv == null) && (isEncrypting())) {
+ // IV will need to be generated
+ return BLOCK_SIZE_BYTES;
+ }
+
+ return 0;
+ }
+
+ @Override
+ protected int getAdditionalEntropyAmountForFinish() {
+ return 0;
+ }
+
+ @Override
+ protected void addAlgorithmSpecificParametersToBegin(KeymasterArguments keymasterArgs) {
+ if ((isEncrypting()) && (mIvRequired) && (mIvHasBeenUsed)) {
+ // IV is being reused for encryption: this violates security best practices.
+ throw new IllegalStateException(
+ "IV has already been used. Reusing IV in encryption mode violates security best"
+ + " practices.");
+ }
+
+ keymasterArgs.addEnum(KeymasterDefs.KM_TAG_ALGORITHM, KeymasterDefs.KM_ALGORITHM_3DES);
+ keymasterArgs.addEnum(KeymasterDefs.KM_TAG_BLOCK_MODE, mKeymasterBlockMode);
+ keymasterArgs.addEnum(KeymasterDefs.KM_TAG_PADDING, mKeymasterPadding);
+ if ((mIvRequired) && (mIv != null)) {
+ keymasterArgs.addBytes(KeymasterDefs.KM_TAG_NONCE, mIv);
+ }
+ }
+
+ @Override
+ protected void loadAlgorithmSpecificParametersFromBeginResult(
+ KeymasterArguments keymasterArgs) {
+ mIvHasBeenUsed = true;
+
+ // NOTE: Keymaster doesn't always return an IV, even if it's used.
+ byte[] returnedIv = keymasterArgs.getBytes(KeymasterDefs.KM_TAG_NONCE, null);
+ if ((returnedIv != null) && (returnedIv.length == 0)) {
+ returnedIv = null;
+ }
+
+ if (mIvRequired) {
+ if (mIv == null) {
+ mIv = returnedIv;
+ } else if ((returnedIv != null) && (!Arrays.equals(returnedIv, mIv))) {
+ throw new ProviderException("IV in use differs from provided IV");
+ }
+ } else {
+ if (returnedIv != null) {
+ throw new ProviderException(
+ "IV in use despite IV not being used by this transformation");
+ }
+ }
+ }
+
+ @Override
+ protected final void resetAll() {
+ mIv = null;
+ mIvHasBeenUsed = false;
+ super.resetAll();
+ }
+}
diff --git a/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java b/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java
index be390ffc..e4cf84af 100644
--- a/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java
+++ b/android/security/keystore/AndroidKeyStoreBCWorkaroundProvider.java
@@ -93,6 +93,16 @@ class AndroidKeyStoreBCWorkaroundProvider extends Provider {
putSymmetricCipherImpl("AES/CTR/NoPadding",
PACKAGE_NAME + ".AndroidKeyStoreUnauthenticatedAESCipherSpi$CTR$NoPadding");
+ putSymmetricCipherImpl("DESede/CBC/NoPadding",
+ PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$CBC$NoPadding");
+ putSymmetricCipherImpl("DESede/CBC/PKCS7Padding",
+ PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$CBC$PKCS7Padding");
+
+ putSymmetricCipherImpl("DESede/ECB/NoPadding",
+ PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$ECB$NoPadding");
+ putSymmetricCipherImpl("DESede/ECB/PKCS7Padding",
+ PACKAGE_NAME + ".AndroidKeyStore3DESCipherSpi$ECB$PKCS7Padding");
+
putSymmetricCipherImpl("AES/GCM/NoPadding",
PACKAGE_NAME + ".AndroidKeyStoreAuthenticatedAESCipherSpi$GCM$NoPadding");
diff --git a/android/security/keystore/AndroidKeyStoreCipherSpiBase.java b/android/security/keystore/AndroidKeyStoreCipherSpiBase.java
index fdebf379..5bcb34a6 100644
--- a/android/security/keystore/AndroidKeyStoreCipherSpiBase.java
+++ b/android/security/keystore/AndroidKeyStoreCipherSpiBase.java
@@ -307,7 +307,7 @@ abstract class AndroidKeyStoreCipherSpiBase extends CipherSpi implements KeyStor
*
* <p>This implementation returns {@code null}.
*
- * @returns stream or {@code null} if AAD is not supported by this cipher.
+ * @return stream or {@code null} if AAD is not supported by this cipher.
*/
@Nullable
protected KeyStoreCryptoOperationStreamer createAdditionalAuthenticationDataStreamer(
diff --git a/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java b/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java
index f1d1e166..379e1770 100644
--- a/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java
+++ b/android/security/keystore/AndroidKeyStoreKeyGeneratorSpi.java
@@ -60,6 +60,12 @@ public abstract class AndroidKeyStoreKeyGeneratorSpi extends KeyGeneratorSpi {
}
}
+ public static class DESede extends AndroidKeyStoreKeyGeneratorSpi {
+ public DESede() {
+ super(KeymasterDefs.KM_ALGORITHM_3DES, 168);
+ }
+ }
+
protected static abstract class HmacBase extends AndroidKeyStoreKeyGeneratorSpi {
protected HmacBase(int keymasterDigest) {
super(KeymasterDefs.KM_ALGORITHM_HMAC,
diff --git a/android/security/keystore/AndroidKeyStoreProvider.java b/android/security/keystore/AndroidKeyStoreProvider.java
index 55e6519d..10189263 100644
--- a/android/security/keystore/AndroidKeyStoreProvider.java
+++ b/android/security/keystore/AndroidKeyStoreProvider.java
@@ -80,6 +80,7 @@ public class AndroidKeyStoreProvider extends Provider {
// javax.crypto.KeyGenerator
put("KeyGenerator.AES", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$AES");
+ put("KeyGenerator.DESede", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$DESede");
put("KeyGenerator.HmacSHA1", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA1");
put("KeyGenerator.HmacSHA224", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA224");
put("KeyGenerator.HmacSHA256", PACKAGE_NAME + ".AndroidKeyStoreKeyGeneratorSpi$HmacSHA256");
@@ -88,6 +89,7 @@ public class AndroidKeyStoreProvider extends Provider {
// java.security.SecretKeyFactory
putSecretKeyFactoryImpl("AES");
+ putSecretKeyFactoryImpl("DESede");
putSecretKeyFactoryImpl("HmacSHA1");
putSecretKeyFactoryImpl("HmacSHA224");
putSecretKeyFactoryImpl("HmacSHA256");
@@ -348,7 +350,8 @@ public class AndroidKeyStoreProvider extends Provider {
}
if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_HMAC ||
- keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES) {
+ keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_AES ||
+ keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_3DES) {
return loadAndroidKeyStoreSecretKeyFromKeystore(userKeyAlias, uid,
keyCharacteristics);
} else if (keymasterAlgorithm == KeymasterDefs.KM_ALGORITHM_RSA ||
diff --git a/android/security/keystore/AndroidKeyStoreSpi.java b/android/security/keystore/AndroidKeyStoreSpi.java
index d73a9e29..440e0863 100644
--- a/android/security/keystore/AndroidKeyStoreSpi.java
+++ b/android/security/keystore/AndroidKeyStoreSpi.java
@@ -18,6 +18,7 @@ package android.security.keystore;
import libcore.util.EmptyArray;
import android.security.Credentials;
+import android.security.GateKeeper;
import android.security.KeyStore;
import android.security.KeyStoreParameter;
import android.security.keymaster.KeyCharacteristics;
@@ -25,6 +26,7 @@ import android.security.keymaster.KeymasterArguments;
import android.security.keymaster.KeymasterDefs;
import android.security.keystore.KeyProperties;
import android.security.keystore.KeyProtection;
+import android.security.keystore.WrappedKeyEntry;
import android.util.Log;
import java.io.ByteArrayInputStream;
@@ -744,6 +746,31 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi {
}
}
+ private void setWrappedKeyEntry(String alias, byte[] wrappedKeyBytes, String wrappingKeyAlias,
+ java.security.KeyStore.ProtectionParameter param) throws KeyStoreException {
+ if (param != null) {
+ throw new KeyStoreException("Protection parameters are specified inside wrapped keys");
+ }
+
+ byte[] maskingKey = new byte[32];
+ KeymasterArguments args = new KeymasterArguments(); // TODO: populate wrapping key args.
+
+ int errorCode = mKeyStore.importWrappedKey(
+ Credentials.USER_SECRET_KEY + alias,
+ wrappedKeyBytes,
+ Credentials.USER_PRIVATE_KEY + wrappingKeyAlias,
+ maskingKey,
+ args,
+ GateKeeper.getSecureUserId(),
+ 0, // FIXME fingerprint id?
+ mUid,
+ new KeyCharacteristics());
+ if (errorCode != KeyStore.NO_ERROR) {
+ throw new KeyStoreException("Failed to import wrapped key. Keystore error code: "
+ + errorCode);
+ }
+ }
+
@Override
public void engineSetKeyEntry(String alias, byte[] userKey, Certificate[] chain)
throws KeyStoreException {
@@ -974,6 +1001,9 @@ public class AndroidKeyStoreSpi extends KeyStoreSpi {
} else if (entry instanceof SecretKeyEntry) {
SecretKeyEntry secE = (SecretKeyEntry) entry;
setSecretKeyEntry(alias, secE.getSecretKey(), param);
+ } else if (entry instanceof WrappedKeyEntry) {
+ WrappedKeyEntry wke = (WrappedKeyEntry) entry;
+ setWrappedKeyEntry(alias, wke.getWrappedKeyBytes(), wke.getWrappingKeyAlias(), param);
} else {
throw new KeyStoreException(
"Entry must be a PrivateKeyEntry, SecretKeyEntry or TrustedCertificateEntry"
diff --git a/android/security/keystore/AttestationUtils.java b/android/security/keystore/AttestationUtils.java
index 0811100f..efee8b49 100644
--- a/android/security/keystore/AttestationUtils.java
+++ b/android/security/keystore/AttestationUtils.java
@@ -99,48 +99,35 @@ public abstract class AttestationUtils {
}
}
+ @NonNull private static KeymasterArguments prepareAttestationArgumentsForDeviceId(
+ Context context, @NonNull int[] idTypes, @NonNull byte[] attestationChallenge) throws
+ DeviceIdAttestationException {
+ // Verify that device ID attestation types are provided.
+ if (idTypes == null) {
+ throw new NullPointerException("Missing id types");
+ }
+
+ return prepareAttestationArguments(context, idTypes, attestationChallenge);
+ }
+
/**
- * 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.
- * If the device supports attestation in secure hardware, the chain will be rooted at a
- * trustworthy CA key. Otherwise, the chain will be rooted at an untrusted certificate. See
- * <a href="https://developer.android.com/training/articles/security-key-attestation.html">
- * Key Attestation</a> for the format of the certificate extension.
- * <p>
- * Attestation will only be successful when all of the following are true:
- * 1) The device has been set up to support device identifier attestation at the factory.
- * 2) The user has not permanently disabled device identifier attestation.
- * 3) You have permission to access the device identifiers you are requesting attestation for.
- * <p>
- * For privacy reasons, you cannot distinguish between (1) and (2). If attestation is
- * unsuccessful, the device may not support it in general or the user may have permanently
- * disabled it.
- *
- * @param context the context to use for retrieving device identifiers.
- * @param idTypes the types of device identifiers to attest.
- * @param attestationChallenge a blob to include in the certificate alongside the device
- * identifiers.
- *
- * @return a certificate chain containing the requested device identifiers in the first element
- *
- * @exception SecurityException if you are not permitted to obtain an attestation of the
- * device's identifiers.
- * @exception DeviceIdAttestationException if the attestation operation fails.
+ * Prepares Keymaster Arguments with attestation data.
+ * @hide should only be used by KeyChain.
*/
- @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
- @NonNull public static X509Certificate[] attestDeviceIds(Context context,
+ @NonNull public static KeymasterArguments prepareAttestationArguments(Context context,
@NonNull int[] idTypes, @NonNull byte[] attestationChallenge) throws
DeviceIdAttestationException {
// Check method arguments, retrieve requested device IDs and prepare attestation arguments.
- if (idTypes == null) {
- throw new NullPointerException("Missing id types");
- }
if (attestationChallenge == null) {
throw new NullPointerException("Missing attestation challenge");
}
final KeymasterArguments attestArgs = new KeymasterArguments();
attestArgs.addBytes(KeymasterDefs.KM_TAG_ATTESTATION_CHALLENGE, attestationChallenge);
+ // Return early if the caller did not request any device identifiers to be included in the
+ // attestation record.
+ if (idTypes == null) {
+ return attestArgs;
+ }
final Set<Integer> idTypesSet = new ArraySet<>(idTypes.length);
for (int idType : idTypes) {
idTypesSet.add(idType);
@@ -191,6 +178,44 @@ public abstract class AttestationUtils {
Build.MANUFACTURER.getBytes(StandardCharsets.UTF_8));
attestArgs.addBytes(KeymasterDefs.KM_TAG_ATTESTATION_ID_MODEL,
Build.MODEL.getBytes(StandardCharsets.UTF_8));
+ return attestArgs;
+ }
+
+ /**
+ * 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.
+ * If the device supports attestation in secure hardware, the chain will be rooted at a
+ * trustworthy CA key. Otherwise, the chain will be rooted at an untrusted certificate. See
+ * <a href="https://developer.android.com/training/articles/security-key-attestation.html">
+ * Key Attestation</a> for the format of the certificate extension.
+ * <p>
+ * Attestation will only be successful when all of the following are true:
+ * 1) The device has been set up to support device identifier attestation at the factory.
+ * 2) The user has not permanently disabled device identifier attestation.
+ * 3) You have permission to access the device identifiers you are requesting attestation for.
+ * <p>
+ * For privacy reasons, you cannot distinguish between (1) and (2). If attestation is
+ * unsuccessful, the device may not support it in general or the user may have permanently
+ * disabled it.
+ *
+ * @param context the context to use for retrieving device identifiers.
+ * @param idTypes the types of device identifiers to attest.
+ * @param attestationChallenge a blob to include in the certificate alongside the device
+ * identifiers.
+ *
+ * @return a certificate chain containing the requested device identifiers in the first element
+ *
+ * @exception SecurityException if you are not permitted to obtain an attestation of the
+ * device's identifiers.
+ * @exception DeviceIdAttestationException if the attestation operation fails.
+ */
+ @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ @NonNull public static X509Certificate[] attestDeviceIds(Context context,
+ @NonNull int[] idTypes, @NonNull byte[] attestationChallenge) throws
+ DeviceIdAttestationException {
+ final KeymasterArguments attestArgs = prepareAttestationArgumentsForDeviceId(
+ context, idTypes, attestationChallenge);
// Perform attestation.
final KeymasterCertificateChain outChain = new KeymasterCertificateChain();
diff --git a/android/security/keystore/BackwardsCompat.java b/android/security/keystore/BackwardsCompat.java
new file mode 100644
index 00000000..69558c4d
--- /dev/null
+++ b/android/security/keystore/BackwardsCompat.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Helpers for converting classes between old and new API, so we can preserve backwards
+ * compatibility while teamfooding. This will be removed soon.
+ *
+ * @hide
+ */
+class BackwardsCompat {
+
+
+ static KeychainProtectionParams toLegacyKeychainProtectionParams(
+ android.security.keystore.recovery.KeyChainProtectionParams keychainProtectionParams
+ ) {
+ return new KeychainProtectionParams.Builder()
+ .setUserSecretType(keychainProtectionParams.getUserSecretType())
+ .setSecret(keychainProtectionParams.getSecret())
+ .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat())
+ .setKeyDerivationParams(
+ toLegacyKeyDerivationParams(
+ keychainProtectionParams.getKeyDerivationParams()))
+ .build();
+ }
+
+ static KeyDerivationParams toLegacyKeyDerivationParams(
+ android.security.keystore.recovery.KeyDerivationParams keyDerivationParams
+ ) {
+ return new KeyDerivationParams(
+ keyDerivationParams.getAlgorithm(), keyDerivationParams.getSalt());
+ }
+
+ static WrappedApplicationKey toLegacyWrappedApplicationKey(
+ android.security.keystore.recovery.WrappedApplicationKey wrappedApplicationKey
+ ) {
+ return new WrappedApplicationKey.Builder()
+ .setAlias(wrappedApplicationKey.getAlias())
+ .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial())
+ .build();
+ }
+
+ static android.security.keystore.recovery.KeyDerivationParams fromLegacyKeyDerivationParams(
+ KeyDerivationParams keyDerivationParams
+ ) {
+ return new android.security.keystore.recovery.KeyDerivationParams(
+ keyDerivationParams.getAlgorithm(), keyDerivationParams.getSalt());
+ }
+
+ static android.security.keystore.recovery.WrappedApplicationKey fromLegacyWrappedApplicationKey(
+ WrappedApplicationKey wrappedApplicationKey
+ ) {
+ return new android.security.keystore.recovery.WrappedApplicationKey.Builder()
+ .setAlias(wrappedApplicationKey.getAlias())
+ .setEncryptedKeyMaterial(wrappedApplicationKey.getEncryptedKeyMaterial())
+ .build();
+ }
+
+ static List<android.security.keystore.recovery.WrappedApplicationKey>
+ fromLegacyWrappedApplicationKeys(List<WrappedApplicationKey> wrappedApplicationKeys
+ ) {
+ return map(wrappedApplicationKeys, BackwardsCompat::fromLegacyWrappedApplicationKey);
+ }
+
+ static List<android.security.keystore.recovery.KeyChainProtectionParams>
+ fromLegacyKeychainProtectionParams(
+ List<KeychainProtectionParams> keychainProtectionParams) {
+ return map(keychainProtectionParams, BackwardsCompat::fromLegacyKeychainProtectionParam);
+ }
+
+ static android.security.keystore.recovery.KeyChainProtectionParams
+ fromLegacyKeychainProtectionParam(KeychainProtectionParams keychainProtectionParams) {
+ return new android.security.keystore.recovery.KeyChainProtectionParams.Builder()
+ .setUserSecretType(keychainProtectionParams.getUserSecretType())
+ .setSecret(keychainProtectionParams.getSecret())
+ .setLockScreenUiFormat(keychainProtectionParams.getLockScreenUiFormat())
+ .setKeyDerivationParams(
+ fromLegacyKeyDerivationParams(
+ keychainProtectionParams.getKeyDerivationParams()))
+ .build();
+ }
+
+ static KeychainSnapshot toLegacyKeychainSnapshot(
+ android.security.keystore.recovery.KeyChainSnapshot keychainSnapshot
+ ) {
+ return new KeychainSnapshot.Builder()
+ .setCounterId(keychainSnapshot.getCounterId())
+ .setEncryptedRecoveryKeyBlob(keychainSnapshot.getEncryptedRecoveryKeyBlob())
+ .setTrustedHardwarePublicKey(keychainSnapshot.getTrustedHardwarePublicKey())
+ .setSnapshotVersion(keychainSnapshot.getSnapshotVersion())
+ .setMaxAttempts(keychainSnapshot.getMaxAttempts())
+ .setServerParams(keychainSnapshot.getServerParams())
+ .setKeychainProtectionParams(
+ map(keychainSnapshot.getKeyChainProtectionParams(),
+ BackwardsCompat::toLegacyKeychainProtectionParams))
+ .setWrappedApplicationKeys(
+ map(keychainSnapshot.getWrappedApplicationKeys(),
+ BackwardsCompat::toLegacyWrappedApplicationKey))
+ .build();
+ }
+
+ static <A, B> List<B> map(List<A> as, Function<A, B> f) {
+ ArrayList<B> bs = new ArrayList<>(as.size());
+ for (A a : as) {
+ bs.add(f.apply(a));
+ }
+ return bs;
+ }
+}
diff --git a/android/security/keystore/BadCertificateFormatException.java b/android/security/keystore/BadCertificateFormatException.java
new file mode 100644
index 00000000..ddc7bd23
--- /dev/null
+++ b/android/security/keystore/BadCertificateFormatException.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+/**
+ * Error thrown when the recovery agent supplies an invalid X509 certificate.
+ *
+ * @hide
+ */
+public class BadCertificateFormatException extends RecoveryControllerException {
+ public BadCertificateFormatException(String msg) {
+ super(msg);
+ }
+}
diff --git a/android/security/keystore/DecryptionFailedException.java b/android/security/keystore/DecryptionFailedException.java
new file mode 100644
index 00000000..945fcf6f
--- /dev/null
+++ b/android/security/keystore/DecryptionFailedException.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+/**
+ * Error thrown when decryption failed, due to an agent error. i.e., using the incorrect key,
+ * trying to decrypt garbage data, trying to decrypt data that has somehow been corrupted, etc.
+ *
+ * @hide
+ */
+public class DecryptionFailedException extends RecoveryControllerException {
+
+ public DecryptionFailedException(String msg) {
+ super(msg);
+ }
+}
diff --git a/android/security/keystore/InternalRecoveryServiceException.java b/android/security/keystore/InternalRecoveryServiceException.java
new file mode 100644
index 00000000..85829bed
--- /dev/null
+++ b/android/security/keystore/InternalRecoveryServiceException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+/**
+ * An error thrown when something went wrong internally in the recovery service.
+ *
+ * <p>This is an unexpected error, and indicates a problem with the service itself, rather than the
+ * caller having performed some kind of illegal action.
+ *
+ * @hide
+ */
+public class InternalRecoveryServiceException extends RecoveryControllerException {
+ public InternalRecoveryServiceException(String msg) {
+ super(msg);
+ }
+
+ public InternalRecoveryServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/android/security/recoverablekeystore/KeyDerivationParameters.java b/android/security/keystore/KeyDerivationParams.java
index 978e60ee..b19cee2d 100644
--- a/android/security/recoverablekeystore/KeyDerivationParameters.java
+++ b/android/security/keystore/KeyDerivationParams.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package android.security.recoverablekeystore;
+package android.security.keystore;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -28,21 +28,17 @@ import java.lang.annotation.RetentionPolicy;
/**
* Collection of parameters which define a key derivation function.
- * Supports
+ * Currently only supports salted SHA-256
*
- * <ul>
- * <li>SHA256
- * <li>Argon2id
- * </ul>
* @hide
*/
-public final class KeyDerivationParameters implements Parcelable {
+public final class KeyDerivationParams implements Parcelable {
private final int mAlgorithm;
private byte[] mSalt;
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({ALGORITHM_SHA256, ALGORITHM_ARGON2ID})
+ @IntDef(prefix = {"ALGORITHM_"}, value = {ALGORITHM_SHA256, ALGORITHM_ARGON2ID})
public @interface KeyDerivationAlgorithm {
}
@@ -53,6 +49,7 @@ public final class KeyDerivationParameters implements Parcelable {
/**
* Argon2ID
+ * @hide
*/
// TODO: add Argon2ID support.
public static final int ALGORITHM_ARGON2ID = 2;
@@ -60,11 +57,11 @@ public final class KeyDerivationParameters implements Parcelable {
/**
* 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);
+ public static KeyDerivationParams createSha256Params(@NonNull byte[] salt) {
+ return new KeyDerivationParams(ALGORITHM_SHA256, salt);
}
- private KeyDerivationParameters(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) {
+ KeyDerivationParams(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) {
mAlgorithm = algorithm;
mSalt = Preconditions.checkNotNull(salt);
}
@@ -83,24 +80,30 @@ public final class KeyDerivationParameters implements Parcelable {
return mSalt;
}
- public static final Parcelable.Creator<KeyDerivationParameters> CREATOR =
- new Parcelable.Creator<KeyDerivationParameters>() {
- public KeyDerivationParameters createFromParcel(Parcel in) {
- return new KeyDerivationParameters(in);
+ public static final Parcelable.Creator<KeyDerivationParams> CREATOR =
+ new Parcelable.Creator<KeyDerivationParams>() {
+ public KeyDerivationParams createFromParcel(Parcel in) {
+ return new KeyDerivationParams(in);
}
- public KeyDerivationParameters[] newArray(int length) {
- return new KeyDerivationParameters[length];
+ public KeyDerivationParams[] newArray(int length) {
+ return new KeyDerivationParams[length];
}
};
+ /**
+ * @hide
+ */
@Override
public void writeToParcel(Parcel out, int flags) {
out.writeInt(mAlgorithm);
out.writeByteArray(mSalt);
}
- protected KeyDerivationParameters(Parcel in) {
+ /**
+ * @hide
+ */
+ protected KeyDerivationParams(Parcel in) {
mAlgorithm = in.readInt();
mSalt = in.createByteArray();
}
diff --git a/android/security/keystore/KeyGenParameterSpec.java b/android/security/keystore/KeyGenParameterSpec.java
index 1238d877..1e2b873c 100644
--- a/android/security/keystore/KeyGenParameterSpec.java
+++ b/android/security/keystore/KeyGenParameterSpec.java
@@ -262,6 +262,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec {
private final boolean mUniqueIdIncluded;
private final boolean mUserAuthenticationValidWhileOnBody;
private final boolean mInvalidatedByBiometricEnrollment;
+ private final boolean mIsStrongBoxBacked;
/**
* @hide should be built with Builder
@@ -289,7 +290,8 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec {
byte[] attestationChallenge,
boolean uniqueIdIncluded,
boolean userAuthenticationValidWhileOnBody,
- boolean invalidatedByBiometricEnrollment) {
+ boolean invalidatedByBiometricEnrollment,
+ boolean isStrongBoxBacked) {
if (TextUtils.isEmpty(keyStoreAlias)) {
throw new IllegalArgumentException("keyStoreAlias must not be empty");
}
@@ -335,6 +337,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec {
mUniqueIdIncluded = uniqueIdIncluded;
mUserAuthenticationValidWhileOnBody = userAuthenticationValidWhileOnBody;
mInvalidatedByBiometricEnrollment = invalidatedByBiometricEnrollment;
+ mIsStrongBoxBacked = isStrongBoxBacked;
}
/**
@@ -625,6 +628,13 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec {
}
/**
+ * Returns {@code true} if the key is protected by a Strongbox security chip.
+ */
+ public boolean isStrongBoxBacked() {
+ return mIsStrongBoxBacked;
+ }
+
+ /**
* Builder of {@link KeyGenParameterSpec} instances.
*/
public final static class Builder {
@@ -652,6 +662,7 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec {
private boolean mUniqueIdIncluded = false;
private boolean mUserAuthenticationValidWhileOnBody;
private boolean mInvalidatedByBiometricEnrollment = true;
+ private boolean mIsStrongBoxBacked = false;
/**
* Creates a new instance of the {@code Builder}.
@@ -1177,6 +1188,15 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec {
}
/**
+ * Sets whether this key should be protected by a StrongBox security chip.
+ */
+ @NonNull
+ public Builder setIsStrongBoxBacked(boolean isStrongBoxBacked) {
+ mIsStrongBoxBacked = isStrongBoxBacked;
+ return this;
+ }
+
+ /**
* Builds an instance of {@code KeyGenParameterSpec}.
*/
@NonNull
@@ -1204,7 +1224,8 @@ public final class KeyGenParameterSpec implements AlgorithmParameterSpec {
mAttestationChallenge,
mUniqueIdIncluded,
mUserAuthenticationValidWhileOnBody,
- mInvalidatedByBiometricEnrollment);
+ mInvalidatedByBiometricEnrollment,
+ mIsStrongBoxBacked);
}
}
}
diff --git a/android/security/keystore/KeyProperties.java b/android/security/keystore/KeyProperties.java
index a250d1f0..f54b6dec 100644
--- a/android/security/keystore/KeyProperties.java
+++ b/android/security/keystore/KeyProperties.java
@@ -44,6 +44,7 @@ public abstract class KeyProperties {
PURPOSE_DECRYPT,
PURPOSE_SIGN,
PURPOSE_VERIFY,
+ PURPOSE_WRAP_KEY,
})
public @interface PurposeEnum {}
@@ -68,6 +69,11 @@ public abstract class KeyProperties {
public static final int PURPOSE_VERIFY = 1 << 3;
/**
+ * Purpose of key: wrapping and unwrapping wrapped keys for secure import.
+ */
+ public static final int PURPOSE_WRAP_KEY = 1 << 5;
+
+ /**
* @hide
*/
public static abstract class Purpose {
@@ -83,6 +89,8 @@ public abstract class KeyProperties {
return KeymasterDefs.KM_PURPOSE_SIGN;
case PURPOSE_VERIFY:
return KeymasterDefs.KM_PURPOSE_VERIFY;
+ case PURPOSE_WRAP_KEY:
+ return KeymasterDefs.KM_PURPOSE_WRAP;
default:
throw new IllegalArgumentException("Unknown purpose: " + purpose);
}
@@ -98,6 +106,8 @@ public abstract class KeyProperties {
return PURPOSE_SIGN;
case KeymasterDefs.KM_PURPOSE_VERIFY:
return PURPOSE_VERIFY;
+ case KeymasterDefs.KM_PURPOSE_WRAP:
+ return PURPOSE_WRAP_KEY;
default:
throw new IllegalArgumentException("Unknown purpose: " + purpose);
}
@@ -146,6 +156,15 @@ public abstract class KeyProperties {
/** Advanced Encryption Standard (AES) key. */
public static final String KEY_ALGORITHM_AES = "AES";
+ /**
+ * Triple Data Encryption Algorithm (3DES) key.
+ *
+ * @deprecated Included for interoperability with legacy systems. Prefer {@link
+ * KeyProperties#KEY_ALGORITHM_AES} for new development.
+ */
+ @Deprecated
+ public static final String KEY_ALGORITHM_3DES = "DESede";
+
/** Keyed-Hash Message Authentication Code (HMAC) key using SHA-1 as the hash. */
public static final String KEY_ALGORITHM_HMAC_SHA1 = "HmacSHA1";
@@ -196,6 +215,8 @@ public abstract class KeyProperties {
@NonNull @KeyAlgorithmEnum String algorithm) {
if (KEY_ALGORITHM_AES.equalsIgnoreCase(algorithm)) {
return KeymasterDefs.KM_ALGORITHM_AES;
+ } else if (KEY_ALGORITHM_3DES.equalsIgnoreCase(algorithm)) {
+ return KeymasterDefs.KM_ALGORITHM_3DES;
} else if (algorithm.toUpperCase(Locale.US).startsWith("HMAC")) {
return KeymasterDefs.KM_ALGORITHM_HMAC;
} else {
@@ -210,6 +231,8 @@ public abstract class KeyProperties {
switch (keymasterAlgorithm) {
case KeymasterDefs.KM_ALGORITHM_AES:
return KEY_ALGORITHM_AES;
+ case KeymasterDefs.KM_ALGORITHM_3DES:
+ return KEY_ALGORITHM_3DES;
case KeymasterDefs.KM_ALGORITHM_HMAC:
switch (keymasterDigest) {
case KeymasterDefs.KM_DIGEST_SHA1:
@@ -666,6 +689,10 @@ public abstract class KeyProperties {
*/
public static final int ORIGIN_UNKNOWN = 1 << 2;
+ /** Key was imported into the AndroidKeyStore in an encrypted wrapper */
+ public static final int ORIGIN_SECURELY_IMPORTED = 1 << 3;
+
+
/**
* @hide
*/
@@ -680,6 +707,8 @@ public abstract class KeyProperties {
return ORIGIN_IMPORTED;
case KeymasterDefs.KM_ORIGIN_UNKNOWN:
return ORIGIN_UNKNOWN;
+ case KeymasterDefs.KM_ORIGIN_SECURELY_IMPORTED:
+ return ORIGIN_SECURELY_IMPORTED;
default:
throw new IllegalArgumentException("Unknown origin: " + origin);
}
diff --git a/android/security/keystore/KeyProtection.java b/android/security/keystore/KeyProtection.java
index 2eb06631..dbacb9c5 100644
--- a/android/security/keystore/KeyProtection.java
+++ b/android/security/keystore/KeyProtection.java
@@ -488,9 +488,9 @@ public final class KeyProtection implements ProtectionParameter {
private int mUserAuthenticationValidityDurationSeconds = -1;
private boolean mUserAuthenticationValidWhileOnBody;
private boolean mInvalidatedByBiometricEnrollment = true;
-
private long mBoundToSecureUserId = GateKeeper.INVALID_SECURE_USER_ID;
private boolean mCriticalToDeviceEncryption = false;
+
/**
* Creates a new instance of the {@code Builder}.
*
diff --git a/android/security/keystore/KeychainProtectionParams.java b/android/security/keystore/KeychainProtectionParams.java
new file mode 100644
index 00000000..a940fdc7
--- /dev/null
+++ b/android/security/keystore/KeychainProtectionParams.java
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * A {@link KeychainSnapshot} is protected with a key derived from the user's lock screen. This
+ * class wraps all the data necessary to derive the same key on a recovering device:
+ *
+ * <ul>
+ * <li>UI parameters for the user's lock screen - so that if e.g., the user was using a pattern,
+ * the recovering device can display the pattern UI to the user when asking them to enter
+ * the lock screen from their previous device.
+ * <li>The algorithm used to derive a key from the user's lock screen, e.g. SHA-256 with a salt.
+ * </ul>
+ *
+ * <p>As such, this data is sent along with the {@link KeychainSnapshot} when syncing the current
+ * version of the keychain.
+ *
+ * <p>For now, the recoverable keychain only supports a single layer of protection, which is the
+ * user's lock screen. In the future, the keychain will support multiple layers of protection
+ * (e.g. an additional keychain password, along with the lock screen).
+ *
+ * @hide
+ */
+public final class KeychainProtectionParams implements Parcelable {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD})
+ public @interface UserSecretType {
+ }
+
+ /**
+ * Lockscreen secret is required to recover KeyStore.
+ */
+ public static final int TYPE_LOCKSCREEN = 100;
+
+ /**
+ * Custom passphrase, unrelated to lock screen, is required to recover KeyStore.
+ */
+ public static final int TYPE_CUSTOM_PASSWORD = 101;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({TYPE_PIN, TYPE_PASSWORD, TYPE_PATTERN})
+ public @interface LockScreenUiFormat {
+ }
+
+ /**
+ * Pin with digits only.
+ */
+ public static final int TYPE_PIN = 1;
+
+ /**
+ * Password. String with latin-1 characters only.
+ */
+ public static final int TYPE_PASSWORD = 2;
+
+ /**
+ * Pattern with 3 by 3 grid.
+ */
+ public static final int TYPE_PATTERN = 3;
+
+ @UserSecretType
+ private Integer mUserSecretType;
+
+ @LockScreenUiFormat
+ private Integer mLockScreenUiFormat;
+
+ /**
+ * Parameters of the key derivation function, including algorithm, difficulty, salt.
+ */
+ private KeyDerivationParams mKeyDerivationParams;
+ private byte[] mSecret; // Derived from user secret. The field must have limited visibility.
+
+ /**
+ * @param secret Constructor creates a reference to the secret. Caller must use
+ * @link {#clearSecret} to overwrite its value in memory.
+ * @hide
+ */
+ public KeychainProtectionParams(@UserSecretType int userSecretType,
+ @LockScreenUiFormat int lockScreenUiFormat,
+ @NonNull KeyDerivationParams keyDerivationParams,
+ @NonNull byte[] secret) {
+ mUserSecretType = userSecretType;
+ mLockScreenUiFormat = lockScreenUiFormat;
+ mKeyDerivationParams = Preconditions.checkNotNull(keyDerivationParams);
+ mSecret = Preconditions.checkNotNull(secret);
+ }
+
+ private KeychainProtectionParams() {
+
+ }
+
+ /**
+ * @see TYPE_LOCKSCREEN
+ * @see TYPE_CUSTOM_PASSWORD
+ */
+ public @UserSecretType int getUserSecretType() {
+ return mUserSecretType;
+ }
+
+ /**
+ * Specifies UX shown to user during recovery.
+ * Default value is {@code TYPE_LOCKSCREEN}
+ *
+ * @see TYPE_PIN
+ * @see TYPE_PASSWORD
+ * @see TYPE_PATTERN
+ */
+ public @LockScreenUiFormat int getLockScreenUiFormat() {
+ return mLockScreenUiFormat;
+ }
+
+ /**
+ * Specifies function used to derive symmetric key from user input
+ * Format is defined in separate util class.
+ */
+ public @NonNull KeyDerivationParams getKeyDerivationParams() {
+ return mKeyDerivationParams;
+ }
+
+ /**
+ * Secret derived from user input.
+ * Default value is empty array
+ *
+ * @return secret or empty array
+ */
+ public @NonNull byte[] getSecret() {
+ return mSecret;
+ }
+
+ /**
+ * Builder for creating {@link KeychainProtectionParams}.
+ */
+ public static class Builder {
+ private KeychainProtectionParams mInstance = new KeychainProtectionParams();
+
+ /**
+ * Sets user secret type.
+ *
+ * @see TYPE_LOCKSCREEN
+ * @see TYPE_CUSTOM_PASSWORD
+ * @param userSecretType The secret type
+ * @return This builder.
+ */
+ public Builder setUserSecretType(@UserSecretType int userSecretType) {
+ mInstance.mUserSecretType = userSecretType;
+ return this;
+ }
+
+ /**
+ * Sets UI format.
+ *
+ * @see TYPE_PIN
+ * @see TYPE_PASSWORD
+ * @see TYPE_PATTERN
+ * @param lockScreenUiFormat The UI format
+ * @return This builder.
+ */
+ public Builder setLockScreenUiFormat(@LockScreenUiFormat int lockScreenUiFormat) {
+ mInstance.mLockScreenUiFormat = lockScreenUiFormat;
+ return this;
+ }
+
+ /**
+ * Sets parameters of the key derivation function.
+ *
+ * @param keyDerivationParams Key derivation Params
+ * @return This builder.
+ */
+ public Builder setKeyDerivationParams(@NonNull KeyDerivationParams
+ keyDerivationParams) {
+ mInstance.mKeyDerivationParams = keyDerivationParams;
+ return this;
+ }
+
+ /**
+ * Secret derived from user input, or empty array.
+ *
+ * @param secret The secret.
+ * @return This builder.
+ */
+ public Builder setSecret(@NonNull byte[] secret) {
+ mInstance.mSecret = secret;
+ return this;
+ }
+
+
+ /**
+ * Creates a new {@link KeychainProtectionParams} instance.
+ * The instance will include default values, if {@link setSecret}
+ * or {@link setUserSecretType} were not called.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public KeychainProtectionParams build() {
+ if (mInstance.mUserSecretType == null) {
+ mInstance.mUserSecretType = TYPE_LOCKSCREEN;
+ }
+ Preconditions.checkNotNull(mInstance.mLockScreenUiFormat);
+ Preconditions.checkNotNull(mInstance.mKeyDerivationParams);
+ if (mInstance.mSecret == null) {
+ mInstance.mSecret = new byte[]{};
+ }
+ return mInstance;
+ }
+ }
+
+ /**
+ * Removes secret from memory than object is no longer used.
+ * Since finalizer call is not reliable, please use @link {#clearSecret} directly.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ clearSecret();
+ super.finalize();
+ }
+
+ /**
+ * Fills mSecret with zeroes.
+ */
+ public void clearSecret() {
+ Arrays.fill(mSecret, (byte) 0);
+ }
+
+ public static final Parcelable.Creator<KeychainProtectionParams> CREATOR =
+ new Parcelable.Creator<KeychainProtectionParams>() {
+ public KeychainProtectionParams createFromParcel(Parcel in) {
+ return new KeychainProtectionParams(in);
+ }
+
+ public KeychainProtectionParams[] newArray(int length) {
+ return new KeychainProtectionParams[length];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mUserSecretType);
+ out.writeInt(mLockScreenUiFormat);
+ out.writeTypedObject(mKeyDerivationParams, flags);
+ out.writeByteArray(mSecret);
+ }
+
+ /**
+ * @hide
+ */
+ protected KeychainProtectionParams(Parcel in) {
+ mUserSecretType = in.readInt();
+ mLockScreenUiFormat = in.readInt();
+ mKeyDerivationParams = in.readTypedObject(KeyDerivationParams.CREATOR);
+ mSecret = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/keystore/KeychainSnapshot.java b/android/security/keystore/KeychainSnapshot.java
new file mode 100644
index 00000000..23aec25e
--- /dev/null
+++ b/android/security/keystore/KeychainSnapshot.java
@@ -0,0 +1,290 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * A snapshot of a version of the keystore. Two events can trigger the generation of a new snapshot:
+ *
+ * <ul>
+ * <li>The user's lock screen changes. (A key derived from the user's lock screen is used to
+ * protected the keychain, which is why this forces a new snapshot.)
+ * <li>A key is added to or removed from the recoverable keychain.
+ * </ul>
+ *
+ * <p>The snapshot data is also encrypted with the remote trusted hardware's public key, so even
+ * the recovery agent itself should not be able to decipher the data. The recovery agent sends an
+ * instance of this to the remote trusted hardware whenever a new snapshot is generated. During a
+ * recovery flow, the recovery agent retrieves a snapshot from the remote trusted hardware. It then
+ * sends it to the framework, where it is decrypted using the user's lock screen from their previous
+ * device.
+ *
+ * @hide
+ */
+public final class KeychainSnapshot implements Parcelable {
+ private static final int DEFAULT_MAX_ATTEMPTS = 10;
+ private static final long DEFAULT_COUNTER_ID = 1L;
+
+ private int mSnapshotVersion;
+ private int mMaxAttempts = DEFAULT_MAX_ATTEMPTS;
+ private long mCounterId = DEFAULT_COUNTER_ID;
+ private byte[] mServerParams;
+ private byte[] mPublicKey;
+ private List<KeychainProtectionParams> mKeychainProtectionParams;
+ private List<WrappedApplicationKey> mEntryRecoveryData;
+ private byte[] mEncryptedRecoveryKeyBlob;
+
+ /**
+ * @hide
+ * Deprecated, consider using builder.
+ */
+ public KeychainSnapshot(
+ int snapshotVersion,
+ @NonNull List<KeychainProtectionParams> keychainProtectionParams,
+ @NonNull List<WrappedApplicationKey> wrappedApplicationKeys,
+ @NonNull byte[] encryptedRecoveryKeyBlob) {
+ mSnapshotVersion = snapshotVersion;
+ mKeychainProtectionParams =
+ Preconditions.checkCollectionElementsNotNull(keychainProtectionParams,
+ "keychainProtectionParams");
+ mEntryRecoveryData = Preconditions.checkCollectionElementsNotNull(wrappedApplicationKeys,
+ "wrappedApplicationKeys");
+ mEncryptedRecoveryKeyBlob = Preconditions.checkNotNull(encryptedRecoveryKeyBlob);
+ }
+
+ private KeychainSnapshot() {
+
+ }
+
+ /**
+ * Snapshot version for given account. It is incremented when user secret or list of application
+ * keys changes.
+ */
+ public int getSnapshotVersion() {
+ return mSnapshotVersion;
+ }
+
+ /**
+ * Number of user secret guesses allowed during Keychain recovery.
+ */
+ public int getMaxAttempts() {
+ return mMaxAttempts;
+ }
+
+ /**
+ * CounterId which is rotated together with user secret.
+ */
+ public long getCounterId() {
+ return mCounterId;
+ }
+
+ /**
+ * Server parameters.
+ */
+ public @NonNull byte[] getServerParams() {
+ return mServerParams;
+ }
+
+ /**
+ * Public key used to encrypt {@code encryptedRecoveryKeyBlob}.
+ *
+ * See implementation for binary key format
+ */
+ // TODO: document key format.
+ public @NonNull byte[] getTrustedHardwarePublicKey() {
+ return mPublicKey;
+ }
+
+ /**
+ * UI and key derivation parameters. Note that combination of secrets may be used.
+ */
+ public @NonNull List<KeychainProtectionParams> getKeychainProtectionParams() {
+ return mKeychainProtectionParams;
+ }
+
+ /**
+ * List of application keys, with key material encrypted by
+ * the recovery key ({@link #getEncryptedRecoveryKeyBlob}).
+ */
+ public @NonNull List<WrappedApplicationKey> getWrappedApplicationKeys() {
+ return mEntryRecoveryData;
+ }
+
+ /**
+ * Recovery key blob, encrypted by user secret and recovery service public key.
+ */
+ public @NonNull byte[] getEncryptedRecoveryKeyBlob() {
+ return mEncryptedRecoveryKeyBlob;
+ }
+
+ public static final Parcelable.Creator<KeychainSnapshot> CREATOR =
+ new Parcelable.Creator<KeychainSnapshot>() {
+ public KeychainSnapshot createFromParcel(Parcel in) {
+ return new KeychainSnapshot(in);
+ }
+
+ public KeychainSnapshot[] newArray(int length) {
+ return new KeychainSnapshot[length];
+ }
+ };
+
+ /**
+ * Builder for creating {@link KeychainSnapshot}.
+ *
+ * @hide
+ */
+ public static class Builder {
+ private KeychainSnapshot mInstance = new KeychainSnapshot();
+
+ /**
+ * Snapshot version for given account.
+ *
+ * @param snapshotVersion The snapshot version
+ * @return This builder.
+ */
+ public Builder setSnapshotVersion(int snapshotVersion) {
+ mInstance.mSnapshotVersion = snapshotVersion;
+ return this;
+ }
+
+ /**
+ * Sets the number of user secret guesses allowed during Keychain recovery.
+ *
+ * @param maxAttempts The maximum number of guesses.
+ * @return This builder.
+ */
+ public Builder setMaxAttempts(int maxAttempts) {
+ mInstance.mMaxAttempts = maxAttempts;
+ return this;
+ }
+
+ /**
+ * Sets counter id.
+ *
+ * @param counterId The counter id.
+ * @return This builder.
+ */
+ public Builder setCounterId(long counterId) {
+ mInstance.mCounterId = counterId;
+ return this;
+ }
+
+ /**
+ * Sets server parameters.
+ *
+ * @param serverParams The server parameters
+ * @return This builder.
+ */
+ public Builder setServerParams(byte[] serverParams) {
+ mInstance.mServerParams = serverParams;
+ return this;
+ }
+
+ /**
+ * Sets public key used to encrypt recovery blob.
+ *
+ * @param publicKey The public key
+ * @return This builder.
+ */
+ public Builder setTrustedHardwarePublicKey(byte[] publicKey) {
+ mInstance.mPublicKey = publicKey;
+ return this;
+ }
+
+ /**
+ * Sets UI and key derivation parameters
+ *
+ * @param recoveryMetadata The UI and key derivation parameters
+ * @return This builder.
+ */
+ public Builder setKeychainProtectionParams(
+ @NonNull List<KeychainProtectionParams> recoveryMetadata) {
+ mInstance.mKeychainProtectionParams = recoveryMetadata;
+ return this;
+ }
+
+ /**
+ * List of application keys.
+ *
+ * @param entryRecoveryData List of application keys
+ * @return This builder.
+ */
+ public Builder setWrappedApplicationKeys(List<WrappedApplicationKey> entryRecoveryData) {
+ mInstance.mEntryRecoveryData = entryRecoveryData;
+ return this;
+ }
+
+ /**
+ * Sets recovery key blob
+ *
+ * @param encryptedRecoveryKeyBlob The recovery key blob.
+ * @return This builder.
+ */
+ public Builder setEncryptedRecoveryKeyBlob(@NonNull byte[] encryptedRecoveryKeyBlob) {
+ mInstance.mEncryptedRecoveryKeyBlob = encryptedRecoveryKeyBlob;
+ return this;
+ }
+
+
+ /**
+ * Creates a new {@link KeychainSnapshot} instance.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public KeychainSnapshot build() {
+ Preconditions.checkCollectionElementsNotNull(mInstance.mKeychainProtectionParams,
+ "recoveryMetadata");
+ Preconditions.checkCollectionElementsNotNull(mInstance.mEntryRecoveryData,
+ "entryRecoveryData");
+ Preconditions.checkNotNull(mInstance.mEncryptedRecoveryKeyBlob);
+ Preconditions.checkNotNull(mInstance.mServerParams);
+ Preconditions.checkNotNull(mInstance.mPublicKey);
+ return mInstance;
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mSnapshotVersion);
+ out.writeTypedList(mKeychainProtectionParams);
+ out.writeByteArray(mEncryptedRecoveryKeyBlob);
+ out.writeTypedList(mEntryRecoveryData);
+ }
+
+ /**
+ * @hide
+ */
+ protected KeychainSnapshot(Parcel in) {
+ mSnapshotVersion = in.readInt();
+ mKeychainProtectionParams = in.createTypedArrayList(KeychainProtectionParams.CREATOR);
+ mEncryptedRecoveryKeyBlob = in.createByteArray();
+ mEntryRecoveryData = in.createTypedArrayList(WrappedApplicationKey.CREATOR);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/service/autofill/Scorer.java b/android/security/keystore/LockScreenRequiredException.java
index c4018558..b07fb9cd 100644
--- a/android/service/autofill/Scorer.java
+++ b/android/security/keystore/LockScreenRequiredException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -13,16 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package android.service.autofill;
+
+package android.security.keystore;
/**
- * Helper class used to calculate a score.
+ * Error thrown when trying to generate keys for a profile that has no lock screen set.
+ *
+ * <p>A lock screen must be set, as the lock screen is used to encrypt the snapshot.
*
- * <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.
+ * @hide
*/
-public interface Scorer {
-
+public class LockScreenRequiredException extends RecoveryControllerException {
+ public LockScreenRequiredException(String msg) {
+ super(msg);
+ }
}
diff --git a/android/security/keystore/RecoveryClaim.java b/android/security/keystore/RecoveryClaim.java
new file mode 100644
index 00000000..6f566af1
--- /dev/null
+++ b/android/security/keystore/RecoveryClaim.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+/**
+ * An attempt to recover a keychain protected by remote secure hardware.
+ *
+ * @hide
+ */
+public class RecoveryClaim {
+
+ private final RecoverySession mRecoverySession;
+ private final byte[] mClaimBytes;
+
+ RecoveryClaim(RecoverySession recoverySession, byte[] claimBytes) {
+ mRecoverySession = recoverySession;
+ mClaimBytes = claimBytes;
+ }
+
+ /**
+ * Returns the session associated with the recovery attempt. This is used to match the symmetric
+ * key, which remains internal to the framework, for decrypting the claim response.
+ *
+ * @return The session data.
+ */
+ public RecoverySession getRecoverySession() {
+ return mRecoverySession;
+ }
+
+ /**
+ * Returns the encrypted claim's bytes.
+ *
+ * <p>This should be sent by the recovery agent to the remote secure hardware, which will use
+ * it to decrypt the keychain, before sending it re-encrypted with the session's symmetric key
+ * to the device.
+ */
+ public byte[] getClaimBytes() {
+ return mClaimBytes;
+ }
+}
diff --git a/android/security/keystore/RecoveryController.java b/android/security/keystore/RecoveryController.java
new file mode 100644
index 00000000..8be6d526
--- /dev/null
+++ b/android/security/keystore/RecoveryController.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.widget.ILockSettings;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An assistant for generating {@link javax.crypto.SecretKey} instances that can be recovered by
+ * other Android devices belonging to the user. The exported keychain is protected by the user's
+ * lock screen.
+ *
+ * <p>The RecoveryController must be paired with a recovery agent. The recovery agent is responsible
+ * for transporting the keychain to remote trusted hardware. This hardware must prevent brute force
+ * attempts against the user's lock screen by limiting the number of allowed guesses (to, e.g., 10).
+ * After that number of incorrect guesses, the trusted hardware no longer allows access to the
+ * key chain.
+ *
+ * <p>For now only the recovery agent itself is able to create keys, so it is expected that the
+ * recovery agent is itself the system app.
+ *
+ * <p>A recovery agent requires the privileged permission
+ * {@code android.Manifest.permission#RECOVER_KEYSTORE}.
+ *
+ * @hide
+ */
+public class RecoveryController {
+ private static final String TAG = "RecoveryController";
+
+ /** Key has been successfully synced. */
+ public static final int RECOVERY_STATUS_SYNCED = 0;
+ /** Waiting for recovery agent to sync the key. */
+ public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1;
+ /** Recovery account is not available. */
+ public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2;
+ /** Key cannot be synced. */
+ public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3;
+
+ /**
+ * Failed because no snapshot is yet pending to be synced for the user.
+ *
+ * @hide
+ */
+ public static final int ERROR_NO_SNAPSHOT_PENDING = 21;
+
+ /**
+ * Failed due to an error internal to the recovery service. This is unexpected and indicates
+ * either a problem with the logic in the service, or a problem with a dependency of the
+ * service (such as AndroidKeyStore).
+ *
+ * @hide
+ */
+ public static final int ERROR_SERVICE_INTERNAL_ERROR = 22;
+
+ /**
+ * Failed because the user does not have a lock screen set.
+ *
+ * @hide
+ */
+ public static final int ERROR_INSECURE_USER = 23;
+
+ /**
+ * Error thrown when attempting to use a recovery session that has since been closed.
+ *
+ * @hide
+ */
+ public static final int ERROR_SESSION_EXPIRED = 24;
+
+ /**
+ * Failed because the provided certificate was not a valid X509 certificate.
+ *
+ * @hide
+ */
+ public static final int ERROR_BAD_CERTIFICATE_FORMAT = 25;
+
+ /**
+ * Error thrown if decryption failed. This might be because the tag is wrong, the key is wrong,
+ * the data has become corrupted, the data has been tampered with, etc.
+ *
+ * @hide
+ */
+ public static final int ERROR_DECRYPTION_FAILED = 26;
+
+
+ private final ILockSettings mBinder;
+
+ private RecoveryController(ILockSettings binder) {
+ mBinder = binder;
+ }
+
+ /**
+ * Gets a new instance of the class.
+ */
+ public static RecoveryController getInstance() {
+ ILockSettings lockSettings =
+ ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings"));
+ return new RecoveryController(lockSettings);
+ }
+
+ /**
+ * Initializes key recovery service for the calling application. RecoveryController
+ * randomly chooses one of the keys from the list and keeps it to use for future key export
+ * operations. Collection of all keys in the list must be signed by the provided {@code
+ * rootCertificateAlias}, which must also be present in the list of root certificates
+ * preinstalled on the device. The random selection allows RecoveryController to select
+ * which of a set of remote recovery service devices will be used.
+ *
+ * <p>In addition, RecoveryController enforces a delay of three months between
+ * consecutive initialization attempts, to limit the ability of an attacker to often switch
+ * remote recovery devices and significantly increase number of recovery attempts.
+ *
+ * @param rootCertificateAlias alias of a root certificate preinstalled on the device
+ * @param signedPublicKeyList binary blob a list of X509 certificates and signature
+ * @throws BadCertificateFormatException if the {@code signedPublicKeyList} is in a bad format.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void initRecoveryService(
+ @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList)
+ throws BadCertificateFormatException, InternalRecoveryServiceException {
+ try {
+ mBinder.initRecoveryService(rootCertificateAlias, signedPublicKeyList);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) {
+ throw new BadCertificateFormatException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(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.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public @NonNull KeychainSnapshot getRecoveryData(@NonNull byte[] account)
+ throws InternalRecoveryServiceException {
+ try {
+ return BackwardsCompat.toLegacyKeychainSnapshot(mBinder.getRecoveryData(account));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_NO_SNAPSHOT_PENDING) {
+ return null;
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link
+ * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at
+ * most one registered listener at any time.
+ *
+ * @param intent triggered when new snapshot is available. Unregisters listener if the value is
+ * {@code null}.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
+ throws InternalRecoveryServiceException {
+ try {
+ mBinder.setSnapshotCreatedPendingIntent(intent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a map from recovery agent accounts to corresponding KeyStore recovery snapshot
+ * version. Version zero is used, if no snapshots were created for the account.
+ *
+ * @return Map from recovery agent accounts to snapshot versions.
+ * @see KeychainSnapshot#getSnapshotVersion
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public @NonNull Map<byte[], Integer> getRecoverySnapshotVersions()
+ throws InternalRecoveryServiceException {
+ try {
+ // IPC doesn't support generic Maps.
+ @SuppressWarnings("unchecked")
+ Map<byte[], Integer> result =
+ (Map<byte[], Integer>) mBinder.getRecoverySnapshotVersions();
+ return result;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Server parameters used to generate new recovery key blobs. This value will be included in
+ * {@code KeychainSnapshot.getEncryptedRecoveryKeyBlob()}. The same value must be included
+ * in vaultParams {@link #startRecoverySession}
+ *
+ * @param serverParams included in recovery key blob.
+ * @see #getRecoveryData
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setServerParams(byte[] serverParams) throws InternalRecoveryServiceException {
+ try {
+ mBinder.setServerParams(serverParams);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Updates recovery status for given keys. It is used to notify keystore that key was
+ * successfully stored on the server or there were an error. Application can check this value
+ * using {@code getRecoveyStatus}.
+ *
+ * @param packageName Application whose recoverable keys' statuses are to be updated.
+ * @param aliases List of application-specific key aliases. If the array is empty, updates the
+ * status for all existing recoverable keys.
+ * @param status Status specific to recovery agent.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setRecoveryStatus(
+ @NonNull String packageName, @Nullable String[] aliases, int status)
+ throws NameNotFoundException, InternalRecoveryServiceException {
+ try {
+ mBinder.setRecoveryStatus(packageName, aliases, status);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a {@code Map} from Application's KeyStore key aliases to their recovery status.
+ * Negative status values are reserved for recovery agent specific codes. List of common codes:
+ *
+ * <ul>
+ * <li>{@link #RECOVERY_STATUS_SYNCED}
+ * <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS}
+ * <li>{@link #RECOVERY_STATUS_MISSING_ACCOUNT}
+ * <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE}
+ * </ul>
+ *
+ * @return {@code Map} from KeyStore alias to recovery status.
+ * @see #setRecoveryStatus
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public Map<String, Integer> getRecoveryStatus() throws InternalRecoveryServiceException {
+ try {
+ // IPC doesn't support generic Maps.
+ @SuppressWarnings("unchecked")
+ Map<String, Integer> result =
+ (Map<String, Integer>) mBinder.getRecoveryStatus(/*packageName=*/ null);
+ return result;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them
+ * is necessary to recover data.
+ *
+ * @param secretTypes {@link KeychainProtectionParams#TYPE_LOCKSCREEN} or {@link
+ * KeychainProtectionParams#TYPE_CUSTOM_PASSWORD}
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setRecoverySecretTypes(
+ @NonNull @KeychainProtectionParams.UserSecretType int[] secretTypes)
+ throws InternalRecoveryServiceException {
+ try {
+ mBinder.setRecoverySecretTypes(secretTypes);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is
+ * necessary to generate KeychainSnapshot.
+ *
+ * @return list of recovery secret types
+ * @see KeychainSnapshot
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public @NonNull @KeychainProtectionParams.UserSecretType int[] getRecoverySecretTypes()
+ throws InternalRecoveryServiceException {
+ try {
+ return mBinder.getRecoverySecretTypes();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a list of recovery secret types, necessary to create a pending recovery snapshot.
+ * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be
+ * called.
+ *
+ * @return list of recovery secret types
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @NonNull
+ public @KeychainProtectionParams.UserSecretType int[] getPendingRecoverySecretTypes()
+ throws InternalRecoveryServiceException {
+ try {
+ return mBinder.getPendingRecoverySecretTypes();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(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 verifierPublicKey Encoded {@code java.security.cert.X509Certificate} with Public key
+ * used to create the recovery blob on the source device.
+ * Keystore will verify the certificate using root of trust.
+ * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
+ * Used to limit number of guesses.
+ * @param vaultChallenge Data passed from server for this recovery session and used to prevent
+ * replay attacks
+ * @param secrets Secrets provided by user, the method only uses type and secret fields.
+ * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is
+ * encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric
+ * key and parameters necessary to identify the counter with the number of failed recovery
+ * attempts.
+ * @throws BadCertificateFormatException if the {@code verifierPublicKey} is in an incorrect
+ * format.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @NonNull public RecoveryClaim startRecoverySession(
+ @NonNull byte[] verifierPublicKey,
+ @NonNull byte[] vaultParams,
+ @NonNull byte[] vaultChallenge,
+ @NonNull List<KeychainProtectionParams> secrets)
+ throws BadCertificateFormatException, InternalRecoveryServiceException {
+ try {
+ RecoverySession recoverySession = RecoverySession.newInstance(this);
+ byte[] recoveryClaim =
+ mBinder.startRecoverySession(
+ recoverySession.getSessionId(),
+ verifierPublicKey,
+ vaultParams,
+ vaultChallenge,
+ BackwardsCompat.fromLegacyKeychainProtectionParams(secrets));
+ return new RecoveryClaim(recoverySession, recoveryClaim);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) {
+ throw new BadCertificateFormatException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Imports keys.
+ *
+ * @param session Related recovery session, as originally created by invoking
+ * {@link #startRecoverySession(byte[], byte[], byte[], List)}.
+ * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
+ * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
+ * and session. KeyStore only uses package names from the application info in {@link
+ * WrappedApplicationKey}. Caller is responsibility to perform certificates check.
+ * @return Map from alias to raw key material.
+ * @throws SessionExpiredException if {@code session} has since been closed.
+ * @throws DecryptionFailedException if unable to decrypt the snapshot.
+ * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
+ */
+ public Map<String, byte[]> recoverKeys(
+ @NonNull RecoverySession session,
+ @NonNull byte[] recoveryKeyBlob,
+ @NonNull List<WrappedApplicationKey> applicationKeys)
+ throws SessionExpiredException, DecryptionFailedException,
+ InternalRecoveryServiceException {
+ try {
+ return (Map<String, byte[]>) mBinder.recoverKeys(
+ session.getSessionId(),
+ recoveryKeyBlob,
+ BackwardsCompat.fromLegacyWrappedApplicationKeys(applicationKeys));
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_DECRYPTION_FAILED) {
+ throw new DecryptionFailedException(e.getMessage());
+ }
+ if (e.errorCode == ERROR_SESSION_EXPIRED) {
+ throw new SessionExpiredException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Deletes all data associated with {@code session}. Should not be invoked directly but via
+ * {@link RecoverySession#close()}.
+ *
+ * @hide
+ */
+ void closeSession(RecoverySession session) {
+ try {
+ mBinder.closeSession(session.getSessionId());
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Unexpected error trying to close session", e);
+ }
+ }
+
+ /**
+ * Generates a key called {@code alias} and loads it into the recoverable key store. Returns the
+ * raw material of the key.
+ *
+ * @param alias The key alias.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ * @throws LockScreenRequiredException if the user has not set a lock screen. This is required
+ * to generate recoverable keys, as the snapshots are encrypted using a key derived from the
+ * lock screen.
+ */
+ public byte[] generateAndStoreKey(@NonNull String alias)
+ throws InternalRecoveryServiceException, LockScreenRequiredException {
+ try {
+ return mBinder.generateAndStoreKey(alias);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_INSECURE_USER) {
+ throw new LockScreenRequiredException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Removes a key called {@code alias} from the recoverable key store.
+ *
+ * @param alias The key alias.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void removeKey(@NonNull String alias) throws InternalRecoveryServiceException {
+ try {
+ mBinder.removeKey(alias);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ private InternalRecoveryServiceException wrapUnexpectedServiceSpecificException(
+ ServiceSpecificException e) {
+ if (e.errorCode == ERROR_SERVICE_INTERNAL_ERROR) {
+ return new InternalRecoveryServiceException(e.getMessage());
+ }
+
+ // Should never happen. If it does, it's a bug, and we need to update how the method that
+ // called this throws its exceptions.
+ return new InternalRecoveryServiceException("Unexpected error code for method: "
+ + e.errorCode, e);
+ }
+}
diff --git a/android/security/keystore/RecoveryControllerException.java b/android/security/keystore/RecoveryControllerException.java
new file mode 100644
index 00000000..5b806b75
--- /dev/null
+++ b/android/security/keystore/RecoveryControllerException.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Base exception for errors thrown by {@link RecoveryController}.
+ *
+ * @hide
+ */
+public abstract class RecoveryControllerException extends GeneralSecurityException {
+ RecoveryControllerException() { }
+
+ RecoveryControllerException(String msg) {
+ super(msg);
+ }
+
+ public RecoveryControllerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/android/security/keystore/RecoverySession.java b/android/security/keystore/RecoverySession.java
new file mode 100644
index 00000000..ae8d91af
--- /dev/null
+++ b/android/security/keystore/RecoverySession.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+import java.security.SecureRandom;
+
+/**
+ * Session to recover a {@link KeychainSnapshot} from the remote trusted hardware, initiated by a
+ * recovery agent.
+ *
+ * @hide
+ */
+public class RecoverySession implements AutoCloseable {
+
+ private static final int SESSION_ID_LENGTH_BYTES = 16;
+
+ private final String mSessionId;
+ private final RecoveryController mRecoveryController;
+
+ private RecoverySession(RecoveryController recoveryController, String sessionId) {
+ mRecoveryController = recoveryController;
+ mSessionId = sessionId;
+ }
+
+ /**
+ * A new session, started by {@code recoveryManager}.
+ */
+ static RecoverySession newInstance(RecoveryController recoveryController) {
+ return new RecoverySession(recoveryController, newSessionId());
+ }
+
+ /**
+ * Returns a new random session ID.
+ */
+ private static String newSessionId() {
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES];
+ secureRandom.nextBytes(sessionId);
+ StringBuilder sb = new StringBuilder();
+ for (byte b : sessionId) {
+ sb.append(Byte.toHexString(b, /*upperCase=*/ false));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * An internal session ID, used by the framework to match recovery claims to snapshot responses.
+ */
+ String getSessionId() {
+ return mSessionId;
+ }
+
+ @Override
+ public void close() {
+ mRecoveryController.closeSession(this);
+ }
+}
diff --git a/android/os/Seccomp.java b/android/security/keystore/SessionExpiredException.java
index f14e93fe..f13e2060 100644
--- a/android/os/Seccomp.java
+++ b/android/security/keystore/SessionExpiredException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -14,11 +14,15 @@
* limitations under the License.
*/
-package android.os;
+package android.security.keystore;
/**
+ * Error thrown when attempting to use a {@link RecoverySession} that has since expired.
+ *
* @hide
*/
-public final class Seccomp {
- public static final native void setPolicy();
+public class SessionExpiredException extends RecoveryControllerException {
+ public SessionExpiredException(String msg) {
+ super(msg);
+ }
}
diff --git a/android/arch/lifecycle/LifecycleActivity.java b/android/security/keystore/StrongBoxUnavailableException.java
index 26bd5087..ad41a58e 100644
--- a/android/arch/lifecycle/LifecycleActivity.java
+++ b/android/security/keystore/StrongBoxUnavailableException.java
@@ -14,13 +14,15 @@
* limitations under the License.
*/
-package android.arch.lifecycle;
+package android.security.keystore;
-import android.support.v4.app.FragmentActivity;
+import java.security.ProviderException;
/**
- * @deprecated Use {@code android.support.v7.app.AppCompatActivity} instead of this class.
+ * Indicates that an operation could not be performed because the requested security hardware
+ * is not available.
*/
-@Deprecated
-public class LifecycleActivity extends FragmentActivity {
+public class StrongBoxUnavailableException extends ProviderException {
+
}
+
diff --git a/android/security/keystore/WrappedApplicationKey.java b/android/security/keystore/WrappedApplicationKey.java
new file mode 100644
index 00000000..522bb955
--- /dev/null
+++ b/android/security/keystore/WrappedApplicationKey.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * 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 WrappedApplicationKey implements Parcelable {
+ private String mAlias;
+ // The only supported format is AES-256 symmetric key.
+ private byte[] mEncryptedKeyMaterial;
+
+ /**
+ * Builder for creating {@link WrappedApplicationKey}.
+ */
+ public static class Builder {
+ private WrappedApplicationKey mInstance = new WrappedApplicationKey();
+
+ /**
+ * Sets Application-specific alias of the key.
+ *
+ * @param alias The alias.
+ * @return This builder.
+ */
+ public Builder setAlias(@NonNull String alias) {
+ mInstance.mAlias = alias;
+ return this;
+ }
+
+ /**
+ * Sets key material encrypted by recovery key.
+ *
+ * @param encryptedKeyMaterial The key material
+ * @return This builder
+ */
+
+ public Builder setEncryptedKeyMaterial(@NonNull byte[] encryptedKeyMaterial) {
+ mInstance.mEncryptedKeyMaterial = encryptedKeyMaterial;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link WrappedApplicationKey} instance.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public WrappedApplicationKey build() {
+ Preconditions.checkNotNull(mInstance.mAlias);
+ Preconditions.checkNotNull(mInstance.mEncryptedKeyMaterial);
+ return mInstance;
+ }
+ }
+
+ private WrappedApplicationKey() {
+
+ }
+
+ /**
+ * Deprecated - consider using Builder.
+ * @hide
+ */
+ public WrappedApplicationKey(@NonNull String alias, @NonNull byte[] encryptedKeyMaterial) {
+ mAlias = Preconditions.checkNotNull(alias);
+ mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial);
+ }
+
+ /**
+ * Application-specific alias of the key.
+ *
+ * @see java.security.KeyStore.aliases
+ */
+ public @NonNull String getAlias() {
+ return mAlias;
+ }
+
+ /** Key material encrypted by recovery key. */
+ public @NonNull byte[] getEncryptedKeyMaterial() {
+ return mEncryptedKeyMaterial;
+ }
+
+ public static final Parcelable.Creator<WrappedApplicationKey> CREATOR =
+ new Parcelable.Creator<WrappedApplicationKey>() {
+ public WrappedApplicationKey createFromParcel(Parcel in) {
+ return new WrappedApplicationKey(in);
+ }
+
+ public WrappedApplicationKey[] newArray(int length) {
+ return new WrappedApplicationKey[length];
+ }
+ };
+
+ /**
+ * @hide
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mAlias);
+ out.writeByteArray(mEncryptedKeyMaterial);
+ }
+
+ /**
+ * @hide
+ */
+ protected WrappedApplicationKey(Parcel in) {
+ mAlias = in.readString();
+ mEncryptedKeyMaterial = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/keystore/WrappedKeyEntry.java b/android/security/keystore/WrappedKeyEntry.java
new file mode 100644
index 00000000..a8f4afe7
--- /dev/null
+++ b/android/security/keystore/WrappedKeyEntry.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package android.security.keystore;
+
+import java.security.KeyStore.Entry;
+import java.security.spec.AlgorithmParameterSpec;
+
+/**
+ * An {@link Entry} that holds a wrapped key.
+ */
+public class WrappedKeyEntry implements Entry {
+
+ private final byte[] mWrappedKeyBytes;
+ private final String mWrappingKeyAlias;
+ private final String mTransformation;
+ private final AlgorithmParameterSpec mAlgorithmParameterSpec;
+
+ public WrappedKeyEntry(byte[] wrappedKeyBytes, String wrappingKeyAlias, String transformation,
+ AlgorithmParameterSpec algorithmParameterSpec) {
+ mWrappedKeyBytes = wrappedKeyBytes;
+ mWrappingKeyAlias = wrappingKeyAlias;
+ mTransformation = transformation;
+ mAlgorithmParameterSpec = algorithmParameterSpec;
+ }
+
+ public byte[] getWrappedKeyBytes() {
+ return mWrappedKeyBytes;
+ }
+
+ public String getWrappingKeyAlias() {
+ return mWrappingKeyAlias;
+ }
+
+ public String getTransformation() {
+ return mTransformation;
+ }
+
+ public AlgorithmParameterSpec getAlgorithmParameterSpec() {
+ return mAlgorithmParameterSpec;
+ }
+}
diff --git a/android/security/keystore/recovery/BadCertificateFormatException.java b/android/security/keystore/recovery/BadCertificateFormatException.java
new file mode 100644
index 00000000..e0781a52
--- /dev/null
+++ b/android/security/keystore/recovery/BadCertificateFormatException.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+/**
+ * Error thrown when the recovery agent supplies an invalid X509 certificate.
+ *
+ * @hide
+ * Deprecated
+ */
+public class BadCertificateFormatException extends RecoveryControllerException {
+ public BadCertificateFormatException(String msg) {
+ super(msg);
+ }
+}
diff --git a/android/security/keystore/recovery/DecryptionFailedException.java b/android/security/keystore/recovery/DecryptionFailedException.java
new file mode 100644
index 00000000..af00e053
--- /dev/null
+++ b/android/security/keystore/recovery/DecryptionFailedException.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+import android.annotation.SystemApi;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Error thrown when decryption failed, due to an agent error. i.e., using the incorrect key,
+ * trying to decrypt garbage data, trying to decrypt data that has somehow been corrupted, etc.
+ *
+ * @hide
+ */
+@SystemApi
+public class DecryptionFailedException extends GeneralSecurityException {
+ public DecryptionFailedException(String msg) {
+ super(msg);
+ }
+}
diff --git a/android/security/keystore/recovery/InternalRecoveryServiceException.java b/android/security/keystore/recovery/InternalRecoveryServiceException.java
new file mode 100644
index 00000000..218d26eb
--- /dev/null
+++ b/android/security/keystore/recovery/InternalRecoveryServiceException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+import android.annotation.SystemApi;
+
+import java.security.GeneralSecurityException;
+/**
+ * An error thrown when something went wrong internally in the recovery service.
+ *
+ * <p>This is an unexpected error, and indicates a problem with the service itself, rather than the
+ * caller having performed some kind of illegal action.
+ *
+ * @hide
+ */
+@SystemApi
+public class InternalRecoveryServiceException extends GeneralSecurityException {
+ public InternalRecoveryServiceException(String msg) {
+ super(msg);
+ }
+
+ public InternalRecoveryServiceException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/android/security/keystore/recovery/KeyChainProtectionParams.java b/android/security/keystore/recovery/KeyChainProtectionParams.java
new file mode 100644
index 00000000..a43952a8
--- /dev/null
+++ b/android/security/keystore/recovery/KeyChainProtectionParams.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.recovery;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+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;
+
+/**
+ * A {@link KeyChainSnapshot} is protected with a key derived from the user's lock screen. This
+ * class wraps all the data necessary to derive the same key on a recovering device:
+ *
+ * <ul>
+ * <li>UI parameters for the user's lock screen - so that if e.g., the user was using a pattern,
+ * the recovering device can display the pattern UI to the user when asking them to enter
+ * the lock screen from their previous device.
+ * <li>The algorithm used to derive a key from the user's lock screen, e.g. SHA-256 with a salt.
+ * </ul>
+ *
+ * <p>As such, this data is sent along with the {@link KeyChainSnapshot} when syncing the current
+ * version of the keychain.
+ *
+ * <p>For now, the recoverable keychain only supports a single layer of protection, which is the
+ * user's lock screen. In the future, the keychain will support multiple layers of protection
+ * (e.g. an additional keychain password, along with the lock screen).
+ *
+ * @hide
+ */
+@SystemApi
+public final class KeyChainProtectionParams implements Parcelable {
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"TYPE_"}, value = {TYPE_LOCKSCREEN, TYPE_CUSTOM_PASSWORD})
+ public @interface UserSecretType {
+ }
+
+ /**
+ * Lockscreen secret is required to recover KeyStore.
+ */
+ public static final int TYPE_LOCKSCREEN = 100;
+
+ /**
+ * Custom passphrase, unrelated to lock screen, is required to recover KeyStore.
+ */
+ public static final int TYPE_CUSTOM_PASSWORD = 101;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"UI_FORMAT_"}, value = {UI_FORMAT_PIN, UI_FORMAT_PASSWORD, UI_FORMAT_PATTERN})
+ public @interface LockScreenUiFormat {
+ }
+
+ /**
+ * Pin with digits only.
+ */
+ public static final int UI_FORMAT_PIN = 1;
+
+ /**
+ * Password. String with latin-1 characters only.
+ */
+ public static final int UI_FORMAT_PASSWORD = 2;
+
+ /**
+ * Pattern with 3 by 3 grid.
+ */
+ public static final int UI_FORMAT_PATTERN = 3;
+
+ @UserSecretType
+ private Integer mUserSecretType;
+
+ @LockScreenUiFormat
+ private Integer mLockScreenUiFormat;
+
+ /**
+ * Parameters of the key derivation function, including algorithm, difficulty, salt.
+ */
+ private KeyDerivationParams mKeyDerivationParams;
+ private byte[] mSecret; // Derived from user secret. The field must have limited visibility.
+
+ /**
+ * @param secret Constructor creates a reference to the secret. Caller must use
+ * @link {#clearSecret} to overwrite its value in memory.
+ * @hide
+ */
+ public KeyChainProtectionParams(@UserSecretType int userSecretType,
+ @LockScreenUiFormat int lockScreenUiFormat,
+ @NonNull KeyDerivationParams keyDerivationParams,
+ @NonNull byte[] secret) {
+ mUserSecretType = userSecretType;
+ mLockScreenUiFormat = lockScreenUiFormat;
+ mKeyDerivationParams = Preconditions.checkNotNull(keyDerivationParams);
+ mSecret = Preconditions.checkNotNull(secret);
+ }
+
+ private KeyChainProtectionParams() {
+
+ }
+
+ /**
+ * @see TYPE_LOCKSCREEN
+ * @see TYPE_CUSTOM_PASSWORD
+ */
+ public @UserSecretType int getUserSecretType() {
+ return mUserSecretType;
+ }
+
+ /**
+ * Specifies UX shown to user during recovery.
+ * Default value is {@code UI_FORMAT_LOCKSCREEN}
+ *
+ * @see UI_FORMAT_PIN
+ * @see UI_FORMAT_PASSWORD
+ * @see UI_FORMAT_PATTERN
+ */
+ public @LockScreenUiFormat int getLockScreenUiFormat() {
+ return mLockScreenUiFormat;
+ }
+
+ /**
+ * Specifies function used to derive symmetric key from user input
+ * Format is defined in separate util class.
+ */
+ public @NonNull KeyDerivationParams getKeyDerivationParams() {
+ return mKeyDerivationParams;
+ }
+
+ /**
+ * Secret derived from user input.
+ * Default value is empty array
+ *
+ * @return secret or empty array
+ */
+ public @NonNull byte[] getSecret() {
+ return mSecret;
+ }
+
+ /**
+ * Builder for creating {@link KeyChainProtectionParams}.
+ */
+ public static class Builder {
+ private KeyChainProtectionParams mInstance = new KeyChainProtectionParams();
+
+ /**
+ * Sets user secret type.
+ *
+ * @see TYPE_LOCKSCREEN
+ * @see TYPE_CUSTOM_PASSWORD
+ * @param userSecretType The secret type
+ * @return This builder.
+ */
+ public Builder setUserSecretType(@UserSecretType int userSecretType) {
+ mInstance.mUserSecretType = userSecretType;
+ return this;
+ }
+
+ /**
+ * Sets UI format.
+ *
+ * @see UI_FORMAT_PIN
+ * @see UI_FORMAT_PASSWORD
+ * @see UI_FORMAT_PATTERN
+ * @param lockScreenUiFormat The UI format
+ * @return This builder.
+ */
+ public Builder setLockScreenUiFormat(@LockScreenUiFormat int lockScreenUiFormat) {
+ mInstance.mLockScreenUiFormat = lockScreenUiFormat;
+ return this;
+ }
+
+ /**
+ * Sets parameters of the key derivation function.
+ *
+ * @param keyDerivationParams Key derivation Params
+ * @return This builder.
+ */
+ public Builder setKeyDerivationParams(@NonNull KeyDerivationParams
+ keyDerivationParams) {
+ mInstance.mKeyDerivationParams = keyDerivationParams;
+ return this;
+ }
+
+ /**
+ * Secret derived from user input, or empty array.
+ *
+ * @param secret The secret.
+ * @return This builder.
+ */
+ public Builder setSecret(@NonNull byte[] secret) {
+ mInstance.mSecret = secret;
+ return this;
+ }
+
+
+ /**
+ * Creates a new {@link KeyChainProtectionParams} instance.
+ * The instance will include default values, if {@link setSecret}
+ * or {@link setUserSecretType} were not called.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public KeyChainProtectionParams build() {
+ if (mInstance.mUserSecretType == null) {
+ mInstance.mUserSecretType = TYPE_LOCKSCREEN;
+ }
+ Preconditions.checkNotNull(mInstance.mLockScreenUiFormat);
+ Preconditions.checkNotNull(mInstance.mKeyDerivationParams);
+ if (mInstance.mSecret == null) {
+ mInstance.mSecret = new byte[]{};
+ }
+ return mInstance;
+ }
+ }
+
+ /**
+ * Removes secret from memory than object is no longer used.
+ * Since finalizer call is not reliable, please use @link {#clearSecret} directly.
+ */
+ @Override
+ protected void finalize() throws Throwable {
+ clearSecret();
+ super.finalize();
+ }
+
+ /**
+ * Fills mSecret with zeroes.
+ */
+ public void clearSecret() {
+ Arrays.fill(mSecret, (byte) 0);
+ }
+
+ public static final Parcelable.Creator<KeyChainProtectionParams> CREATOR =
+ new Parcelable.Creator<KeyChainProtectionParams>() {
+ public KeyChainProtectionParams createFromParcel(Parcel in) {
+ return new KeyChainProtectionParams(in);
+ }
+
+ public KeyChainProtectionParams[] newArray(int length) {
+ return new KeyChainProtectionParams[length];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mUserSecretType);
+ out.writeInt(mLockScreenUiFormat);
+ out.writeTypedObject(mKeyDerivationParams, flags);
+ out.writeByteArray(mSecret);
+ }
+
+ /**
+ * @hide
+ */
+ protected KeyChainProtectionParams(Parcel in) {
+ mUserSecretType = in.readInt();
+ mLockScreenUiFormat = in.readInt();
+ mKeyDerivationParams = in.readTypedObject(KeyDerivationParams.CREATOR);
+ mSecret = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/keystore/recovery/KeyChainSnapshot.java b/android/security/keystore/recovery/KeyChainSnapshot.java
new file mode 100644
index 00000000..df535ed9
--- /dev/null
+++ b/android/security/keystore/recovery/KeyChainSnapshot.java
@@ -0,0 +1,299 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.recovery;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.List;
+
+/**
+ * A snapshot of a version of the keystore. Two events can trigger the generation of a new snapshot:
+ *
+ * <ul>
+ * <li>The user's lock screen changes. (A key derived from the user's lock screen is used to
+ * protected the keychain, which is why this forces a new snapshot.)
+ * <li>A key is added to or removed from the recoverable keychain.
+ * </ul>
+ *
+ * <p>The snapshot data is also encrypted with the remote trusted hardware's public key, so even
+ * the recovery agent itself should not be able to decipher the data. The recovery agent sends an
+ * instance of this to the remote trusted hardware whenever a new snapshot is generated. During a
+ * recovery flow, the recovery agent retrieves a snapshot from the remote trusted hardware. It then
+ * sends it to the framework, where it is decrypted using the user's lock screen from their previous
+ * device.
+ *
+ * @hide
+ */
+@SystemApi
+public final class KeyChainSnapshot implements Parcelable {
+ private static final int DEFAULT_MAX_ATTEMPTS = 10;
+ private static final long DEFAULT_COUNTER_ID = 1L;
+
+ private int mSnapshotVersion;
+ private int mMaxAttempts = DEFAULT_MAX_ATTEMPTS;
+ private long mCounterId = DEFAULT_COUNTER_ID;
+ private byte[] mServerParams;
+ private byte[] mPublicKey;
+ private List<KeyChainProtectionParams> mKeyChainProtectionParams;
+ private List<WrappedApplicationKey> mEntryRecoveryData;
+ private byte[] mEncryptedRecoveryKeyBlob;
+
+ /**
+ * @hide
+ * Deprecated, consider using builder.
+ */
+ public KeyChainSnapshot(
+ int snapshotVersion,
+ @NonNull List<KeyChainProtectionParams> keyChainProtectionParams,
+ @NonNull List<WrappedApplicationKey> wrappedApplicationKeys,
+ @NonNull byte[] encryptedRecoveryKeyBlob) {
+ mSnapshotVersion = snapshotVersion;
+ mKeyChainProtectionParams =
+ Preconditions.checkCollectionElementsNotNull(keyChainProtectionParams,
+ "KeyChainProtectionParams");
+ mEntryRecoveryData = Preconditions.checkCollectionElementsNotNull(wrappedApplicationKeys,
+ "wrappedApplicationKeys");
+ mEncryptedRecoveryKeyBlob = Preconditions.checkNotNull(encryptedRecoveryKeyBlob);
+ }
+
+ private KeyChainSnapshot() {
+
+ }
+
+ /**
+ * Snapshot version for given account. It is incremented when user secret or list of application
+ * keys changes.
+ */
+ public int getSnapshotVersion() {
+ return mSnapshotVersion;
+ }
+
+ /**
+ * Number of user secret guesses allowed during Keychain recovery.
+ */
+ public int getMaxAttempts() {
+ return mMaxAttempts;
+ }
+
+ /**
+ * CounterId which is rotated together with user secret.
+ */
+ public long getCounterId() {
+ return mCounterId;
+ }
+
+ /**
+ * Server parameters.
+ */
+ public @NonNull byte[] getServerParams() {
+ return mServerParams;
+ }
+
+ /**
+ * Public key used to encrypt {@code encryptedRecoveryKeyBlob}.
+ *
+ * See implementation for binary key format
+ */
+ // TODO: document key format.
+ public @NonNull byte[] getTrustedHardwarePublicKey() {
+ return mPublicKey;
+ }
+
+ /**
+ * UI and key derivation parameters. Note that combination of secrets may be used.
+ */
+ public @NonNull List<KeyChainProtectionParams> getKeyChainProtectionParams() {
+ return mKeyChainProtectionParams;
+ }
+
+ /**
+ * List of application keys, with key material encrypted by
+ * the recovery key ({@link #getEncryptedRecoveryKeyBlob}).
+ */
+ public @NonNull List<WrappedApplicationKey> getWrappedApplicationKeys() {
+ return mEntryRecoveryData;
+ }
+
+ /**
+ * Recovery key blob, encrypted by user secret and recovery service public key.
+ */
+ public @NonNull byte[] getEncryptedRecoveryKeyBlob() {
+ return mEncryptedRecoveryKeyBlob;
+ }
+
+ public static final Creator<KeyChainSnapshot> CREATOR =
+ new Creator<KeyChainSnapshot>() {
+ public KeyChainSnapshot createFromParcel(Parcel in) {
+ return new KeyChainSnapshot(in);
+ }
+
+ public KeyChainSnapshot[] newArray(int length) {
+ return new KeyChainSnapshot[length];
+ }
+ };
+
+ /**
+ * Builder for creating {@link KeyChainSnapshot}.
+ * @hide
+ */
+ public static class Builder {
+ private KeyChainSnapshot mInstance = new KeyChainSnapshot();
+
+ /**
+ * Snapshot version for given account.
+ *
+ * @param snapshotVersion The snapshot version
+ * @return This builder.
+ */
+ public Builder setSnapshotVersion(int snapshotVersion) {
+ mInstance.mSnapshotVersion = snapshotVersion;
+ return this;
+ }
+
+ /**
+ * Sets the number of user secret guesses allowed during Keychain recovery.
+ *
+ * @param maxAttempts The maximum number of guesses.
+ * @return This builder.
+ */
+ public Builder setMaxAttempts(int maxAttempts) {
+ mInstance.mMaxAttempts = maxAttempts;
+ return this;
+ }
+
+ /**
+ * Sets counter id.
+ *
+ * @param counterId The counter id.
+ * @return This builder.
+ */
+ public Builder setCounterId(long counterId) {
+ mInstance.mCounterId = counterId;
+ return this;
+ }
+
+ /**
+ * Sets server parameters.
+ *
+ * @param serverParams The server parameters
+ * @return This builder.
+ */
+ public Builder setServerParams(byte[] serverParams) {
+ mInstance.mServerParams = serverParams;
+ return this;
+ }
+
+ /**
+ * Sets public key used to encrypt recovery blob.
+ *
+ * @param publicKey The public key
+ * @return This builder.
+ */
+ public Builder setTrustedHardwarePublicKey(byte[] publicKey) {
+ mInstance.mPublicKey = publicKey;
+ return this;
+ }
+
+ /**
+ * Sets UI and key derivation parameters
+ *
+ * @param recoveryMetadata The UI and key derivation parameters
+ * @return This builder.
+ */
+ public Builder setKeyChainProtectionParams(
+ @NonNull List<KeyChainProtectionParams> recoveryMetadata) {
+ mInstance.mKeyChainProtectionParams = recoveryMetadata;
+ return this;
+ }
+
+ /**
+ * List of application keys.
+ *
+ * @param entryRecoveryData List of application keys
+ * @return This builder.
+ */
+ public Builder setWrappedApplicationKeys(List<WrappedApplicationKey> entryRecoveryData) {
+ mInstance.mEntryRecoveryData = entryRecoveryData;
+ return this;
+ }
+
+ /**
+ * Sets recovery key blob
+ *
+ * @param encryptedRecoveryKeyBlob The recovery key blob.
+ * @return This builder.
+ */
+ public Builder setEncryptedRecoveryKeyBlob(@NonNull byte[] encryptedRecoveryKeyBlob) {
+ mInstance.mEncryptedRecoveryKeyBlob = encryptedRecoveryKeyBlob;
+ return this;
+ }
+
+
+ /**
+ * Creates a new {@link KeyChainSnapshot} instance.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public KeyChainSnapshot build() {
+ Preconditions.checkCollectionElementsNotNull(mInstance.mKeyChainProtectionParams,
+ "recoveryMetadata");
+ Preconditions.checkCollectionElementsNotNull(mInstance.mEntryRecoveryData,
+ "entryRecoveryData");
+ Preconditions.checkNotNull(mInstance.mEncryptedRecoveryKeyBlob);
+ Preconditions.checkNotNull(mInstance.mServerParams);
+ Preconditions.checkNotNull(mInstance.mPublicKey);
+ return mInstance;
+ }
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mSnapshotVersion);
+ out.writeTypedList(mKeyChainProtectionParams);
+ out.writeByteArray(mEncryptedRecoveryKeyBlob);
+ out.writeTypedList(mEntryRecoveryData);
+ out.writeInt(mMaxAttempts);
+ out.writeLong(mCounterId);
+ out.writeByteArray(mServerParams);
+ out.writeByteArray(mPublicKey);
+ }
+
+ /**
+ * @hide
+ */
+ protected KeyChainSnapshot(Parcel in) {
+ mSnapshotVersion = in.readInt();
+ mKeyChainProtectionParams = in.createTypedArrayList(KeyChainProtectionParams.CREATOR);
+ mEncryptedRecoveryKeyBlob = in.createByteArray();
+ mEntryRecoveryData = in.createTypedArrayList(WrappedApplicationKey.CREATOR);
+ mMaxAttempts = in.readInt();
+ mCounterId = in.readLong();
+ mServerParams = in.createByteArray();
+ mPublicKey = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/keystore/recovery/KeyDerivationParams.java b/android/security/keystore/recovery/KeyDerivationParams.java
new file mode 100644
index 00000000..fc909a0a
--- /dev/null
+++ b/android/security/keystore/recovery/KeyDerivationParams.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.security.keystore.recovery;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+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.
+ * Currently only supports salted SHA-256
+ *
+ * @hide
+ */
+@SystemApi
+public final class KeyDerivationParams implements Parcelable {
+ private final int mAlgorithm;
+ private byte[] mSalt;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = {"ALGORITHM_"}, value = {ALGORITHM_SHA256, ALGORITHM_ARGON2ID})
+ public @interface KeyDerivationAlgorithm {
+ }
+
+ /**
+ * Salted SHA256
+ */
+ public static final int ALGORITHM_SHA256 = 1;
+
+ /**
+ * Argon2ID
+ * @hide
+ */
+ // TODO: add Argon2ID support.
+ public static final int ALGORITHM_ARGON2ID = 2;
+
+ /**
+ * Creates instance of the class to to derive key using salted SHA256 hash.
+ */
+ public static KeyDerivationParams createSha256Params(@NonNull byte[] salt) {
+ return new KeyDerivationParams(ALGORITHM_SHA256, salt);
+ }
+
+ /**
+ * @hide
+ */
+ // TODO: Make private once legacy API is removed
+ public KeyDerivationParams(@KeyDerivationAlgorithm int algorithm, @NonNull byte[] salt) {
+ mAlgorithm = algorithm;
+ mSalt = Preconditions.checkNotNull(salt);
+ }
+
+ /**
+ * Gets algorithm.
+ */
+ public @KeyDerivationAlgorithm int getAlgorithm() {
+ return mAlgorithm;
+ }
+
+ /**
+ * Gets salt.
+ */
+ public @NonNull byte[] getSalt() {
+ return mSalt;
+ }
+
+ public static final Parcelable.Creator<KeyDerivationParams> CREATOR =
+ new Parcelable.Creator<KeyDerivationParams>() {
+ public KeyDerivationParams createFromParcel(Parcel in) {
+ return new KeyDerivationParams(in);
+ }
+
+ public KeyDerivationParams[] newArray(int length) {
+ return new KeyDerivationParams[length];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(mAlgorithm);
+ out.writeByteArray(mSalt);
+ }
+
+ /**
+ * @hide
+ */
+ protected KeyDerivationParams(Parcel in) {
+ mAlgorithm = in.readInt();
+ mSalt = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/keystore/recovery/LockScreenRequiredException.java b/android/security/keystore/recovery/LockScreenRequiredException.java
new file mode 100644
index 00000000..0062d290
--- /dev/null
+++ b/android/security/keystore/recovery/LockScreenRequiredException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+import android.annotation.SystemApi;
+
+import java.security.GeneralSecurityException;
+
+/**
+ * Error thrown when trying to generate keys for a profile that has no lock screen set.
+ *
+ * <p>A lock screen must be set, as the lock screen is used to encrypt the snapshot.
+ *
+ * @hide
+ */
+@SystemApi
+public class LockScreenRequiredException extends GeneralSecurityException {
+ public LockScreenRequiredException(String msg) {
+ super(msg);
+ }
+}
diff --git a/android/security/keystore/recovery/RecoveryClaim.java b/android/security/keystore/recovery/RecoveryClaim.java
new file mode 100644
index 00000000..45c6b4ff
--- /dev/null
+++ b/android/security/keystore/recovery/RecoveryClaim.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+/**
+ * An attempt to recover a keychain protected by remote secure hardware.
+ *
+ * @hide
+ * Deprecated
+ */
+public class RecoveryClaim {
+
+ private final RecoverySession mRecoverySession;
+ private final byte[] mClaimBytes;
+
+ RecoveryClaim(RecoverySession recoverySession, byte[] claimBytes) {
+ mRecoverySession = recoverySession;
+ mClaimBytes = claimBytes;
+ }
+
+ /**
+ * Returns the session associated with the recovery attempt. This is used to match the symmetric
+ * key, which remains internal to the framework, for decrypting the claim response.
+ *
+ * @return The session data.
+ */
+ public RecoverySession getRecoverySession() {
+ return mRecoverySession;
+ }
+
+ /**
+ * Returns the encrypted claim's bytes.
+ *
+ * <p>This should be sent by the recovery agent to the remote secure hardware, which will use
+ * it to decrypt the keychain, before sending it re-encrypted with the session's symmetric key
+ * to the device.
+ */
+ public byte[] getClaimBytes() {
+ return mClaimBytes;
+ }
+}
diff --git a/android/security/keystore/recovery/RecoveryController.java b/android/security/keystore/recovery/RecoveryController.java
new file mode 100644
index 00000000..71a36f19
--- /dev/null
+++ b/android/security/keystore/recovery/RecoveryController.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.security.keystore.recovery;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+
+import com.android.internal.widget.ILockSettings;
+
+import java.security.cert.CertificateException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An assistant for generating {@link javax.crypto.SecretKey} instances that can be recovered by
+ * other Android devices belonging to the user. The exported keychain is protected by the user's
+ * lock screen.
+ *
+ * <p>The RecoveryController must be paired with a recovery agent. The recovery agent is responsible
+ * for transporting the keychain to remote trusted hardware. This hardware must prevent brute force
+ * attempts against the user's lock screen by limiting the number of allowed guesses (to, e.g., 10).
+ * After that number of incorrect guesses, the trusted hardware no longer allows access to the
+ * key chain.
+ *
+ * <p>For now only the recovery agent itself is able to create keys, so it is expected that the
+ * recovery agent is itself the system app.
+ *
+ * <p>A recovery agent requires the privileged permission
+ * {@code android.Manifest.permission#RECOVER_KEYSTORE}.
+ *
+ * @hide
+ */
+@SystemApi
+public class RecoveryController {
+ private static final String TAG = "RecoveryController";
+
+ /** Key has been successfully synced. */
+ public static final int RECOVERY_STATUS_SYNCED = 0;
+ /** Waiting for recovery agent to sync the key. */
+ public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1;
+ /** Recovery account is not available. */
+ public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2;
+ /** Key cannot be synced. */
+ public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3;
+
+ /**
+ * Failed because no snapshot is yet pending to be synced for the user.
+ *
+ * @hide
+ */
+ public static final int ERROR_NO_SNAPSHOT_PENDING = 21;
+
+ /**
+ * Failed due to an error internal to the recovery service. This is unexpected and indicates
+ * either a problem with the logic in the service, or a problem with a dependency of the
+ * service (such as AndroidKeyStore).
+ *
+ * @hide
+ */
+ public static final int ERROR_SERVICE_INTERNAL_ERROR = 22;
+
+ /**
+ * Failed because the user does not have a lock screen set.
+ *
+ * @hide
+ */
+ public static final int ERROR_INSECURE_USER = 23;
+
+ /**
+ * Error thrown when attempting to use a recovery session that has since been closed.
+ *
+ * @hide
+ */
+ public static final int ERROR_SESSION_EXPIRED = 24;
+
+ /**
+ * Failed because the provided certificate was not a valid X509 certificate.
+ *
+ * @hide
+ */
+ public static final int ERROR_BAD_CERTIFICATE_FORMAT = 25;
+
+ /**
+ * Error thrown if decryption failed. This might be because the tag is wrong, the key is wrong,
+ * the data has become corrupted, the data has been tampered with, etc.
+ *
+ * @hide
+ */
+ public static final int ERROR_DECRYPTION_FAILED = 26;
+
+
+ private final ILockSettings mBinder;
+
+ private RecoveryController(ILockSettings binder) {
+ mBinder = binder;
+ }
+
+ /**
+ * Internal method used by {@code RecoverySession}.
+ *
+ * @hide
+ */
+ ILockSettings getBinder() {
+ return mBinder;
+ }
+
+ /**
+ * Gets a new instance of the class.
+ */
+ public static RecoveryController getInstance(Context context) {
+ ILockSettings lockSettings =
+ ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings"));
+ return new RecoveryController(lockSettings);
+ }
+
+ /**
+ * Initializes key recovery service for the calling application. RecoveryController
+ * randomly chooses one of the keys from the list and keeps it to use for future key export
+ * operations. Collection of all keys in the list must be signed by the provided {@code
+ * rootCertificateAlias}, which must also be present in the list of root certificates
+ * preinstalled on the device. The random selection allows RecoveryController to select
+ * which of a set of remote recovery service devices will be used.
+ *
+ * <p>In addition, RecoveryController enforces a delay of three months between
+ * consecutive initialization attempts, to limit the ability of an attacker to often switch
+ * remote recovery devices and significantly increase number of recovery attempts.
+ *
+ * @param rootCertificateAlias alias of a root certificate preinstalled on the device
+ * @param signedPublicKeyList binary blob a list of X509 certificates and signature
+ * @throws CertificateException if the {@code signedPublicKeyList} is in a bad format.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public void initRecoveryService(
+ @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList)
+ throws CertificateException, InternalRecoveryServiceException {
+ try {
+ mBinder.initRecoveryService(rootCertificateAlias, signedPublicKeyList);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) {
+ throw new CertificateException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns data necessary to store all recoverable keys. Key material is
+ * encrypted with user secret and recovery public key.
+ *
+ * @return Data necessary to recover keystore.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public @NonNull KeyChainSnapshot getRecoveryData()
+ throws InternalRecoveryServiceException {
+ try {
+ return mBinder.getRecoveryData(/*account=*/ new byte[]{});
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_NO_SNAPSHOT_PENDING) {
+ return null;
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link
+ * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at
+ * most one registered listener at any time.
+ *
+ * @param intent triggered when new snapshot is available. Unregisters listener if the value is
+ * {@code null}.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
+ throws InternalRecoveryServiceException {
+ try {
+ mBinder.setSnapshotCreatedPendingIntent(intent);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Server parameters used to generate new recovery key blobs. This value will be included in
+ * {@code KeyChainSnapshot.getEncryptedRecoveryKeyBlob()}. The same value must be included
+ * in vaultParams {@link #startRecoverySession}
+ *
+ * @param serverParams included in recovery key blob.
+ * @see #getRecoveryData
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public void setServerParams(byte[] serverParams) throws InternalRecoveryServiceException {
+ try {
+ mBinder.setServerParams(serverParams);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Gets aliases of recoverable keys for the application.
+ *
+ * @param packageName which recoverable keys' aliases will be returned.
+ *
+ * @return {@code List} of all aliases.
+ */
+ public List<String> getAliases(@Nullable String packageName)
+ throws InternalRecoveryServiceException {
+ try {
+ // TODO: update aidl
+ Map<String, Integer> allStatuses = mBinder.getRecoveryStatus(packageName);
+ return new ArrayList<>(allStatuses.keySet());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Updates recovery status for given key. 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 key's status are to be updated.
+ * @param alias Application-specific key alias.
+ * @param status Status specific to recovery agent.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public void setRecoveryStatus(
+ @NonNull String packageName, String alias, int status)
+ throws NameNotFoundException, InternalRecoveryServiceException {
+ try {
+ // TODO: update aidl
+ String[] aliases = alias == null ? null : new String[]{alias};
+ mBinder.setRecoveryStatus(packageName, aliases, status);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns recovery status for Application's KeyStore key.
+ * 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 key status is returned.
+ * @param alias Application-specific key alias.
+ * @return Recovery status.
+ * @see #setRecoveryStatus
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public int getRecoveryStatus(String packageName, String alias)
+ throws InternalRecoveryServiceException {
+ try {
+ // TODO: update aidl
+ Map<String, Integer> allStatuses = mBinder.getRecoveryStatus(packageName);
+ Integer status = allStatuses.get(alias);
+ if (status == null) {
+ return RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE;
+ } else {
+ return status;
+ }
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them
+ * is necessary to recover data.
+ *
+ * @param secretTypes {@link KeyChainProtectionParams#TYPE_LOCKSCREEN} or {@link
+ * KeyChainProtectionParams#TYPE_CUSTOM_PASSWORD}
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void setRecoverySecretTypes(
+ @NonNull @KeyChainProtectionParams.UserSecretType int[] secretTypes)
+ throws InternalRecoveryServiceException {
+ try {
+ mBinder.setRecoverySecretTypes(secretTypes);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is
+ * necessary to generate KeyChainSnapshot.
+ *
+ * @return list of recovery secret types
+ * @see KeyChainSnapshot
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public @NonNull @KeyChainProtectionParams.UserSecretType int[] getRecoverySecretTypes()
+ throws InternalRecoveryServiceException {
+ try {
+ return mBinder.getRecoverySecretTypes();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Returns a list of recovery secret types, necessary to create a pending recovery snapshot.
+ * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be
+ * called.
+ *
+ * @return list of recovery secret types
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @NonNull
+ public @KeyChainProtectionParams.UserSecretType int[] getPendingRecoverySecretTypes()
+ throws InternalRecoveryServiceException {
+ try {
+ return mBinder.getPendingRecoverySecretTypes();
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(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 KeyChainProtectionParams#clearSecret} to override the secret value in
+ * memory.
+ *
+ * @param recoverySecret user generated secret together with parameters necessary to regenerate
+ * it on a new device.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void recoverySecretAvailable(@NonNull KeyChainProtectionParams recoverySecret)
+ throws InternalRecoveryServiceException {
+ try {
+ mBinder.recoverySecretAvailable(recoverySecret);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Generates a AES256/GCM/NoPADDING key called {@code alias} and loads it into the recoverable
+ * key store. Returns the raw material of the key.
+ *
+ * @param alias The key alias.
+ * @param account The account associated with the key
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ * @throws LockScreenRequiredException if the user has not set a lock screen. This is required
+ * to generate recoverable keys, as the snapshots are encrypted using a key derived from the
+ * lock screen.
+ */
+ public byte[] generateAndStoreKey(@NonNull String alias, byte[] account)
+ throws InternalRecoveryServiceException, LockScreenRequiredException {
+ try {
+ // TODO: add account
+ return mBinder.generateAndStoreKey(alias);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == ERROR_INSECURE_USER) {
+ throw new LockScreenRequiredException(e.getMessage());
+ }
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Removes a key called {@code alias} from the recoverable key store.
+ *
+ * @param alias The key alias.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ public void removeKey(@NonNull String alias) throws InternalRecoveryServiceException {
+ try {
+ mBinder.removeKey(alias);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ throw wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ InternalRecoveryServiceException wrapUnexpectedServiceSpecificException(
+ ServiceSpecificException e) {
+ if (e.errorCode == ERROR_SERVICE_INTERNAL_ERROR) {
+ return new InternalRecoveryServiceException(e.getMessage());
+ }
+
+ // Should never happen. If it does, it's a bug, and we need to update how the method that
+ // called this throws its exceptions.
+ return new InternalRecoveryServiceException("Unexpected error code for method: "
+ + e.errorCode, e);
+ }
+}
diff --git a/android/bluetooth/client/map/BluetoothMasRequestUpdateInbox.java b/android/security/keystore/recovery/RecoveryControllerException.java
index aeec6321..2733acab 100644
--- a/android/bluetooth/client/map/BluetoothMasRequestUpdateInbox.java
+++ b/android/security/keystore/recovery/RecoveryControllerException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2014 The Android Open Source Project
+ * 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.
@@ -14,23 +14,24 @@
* limitations under the License.
*/
-package android.bluetooth.client.map;
+package android.security.keystore.recovery;
-import java.io.IOException;
+import java.security.GeneralSecurityException;
-import javax.obex.ClientSession;
-import javax.obex.HeaderSet;
-
-final class BluetoothMasRequestUpdateInbox extends BluetoothMasRequest {
-
- private static final String TYPE = "x-bt/MAP-messageUpdate";
+/**
+ * Base exception for errors thrown by {@link RecoveryController}.
+ *
+ * @hide
+ * Deprecated
+ */
+public abstract class RecoveryControllerException extends GeneralSecurityException {
+ RecoveryControllerException() { }
- public BluetoothMasRequestUpdateInbox() {
- mHeaderSet.setHeader(HeaderSet.TYPE, TYPE);
+ RecoveryControllerException(String msg) {
+ super(msg);
}
- @Override
- public void execute(ClientSession session) throws IOException {
- executePut(session, FILLER_BYTE);
+ public RecoveryControllerException(String message, Throwable cause) {
+ super(message, cause);
}
}
diff --git a/android/security/keystore/recovery/RecoverySession.java b/android/security/keystore/recovery/RecoverySession.java
new file mode 100644
index 00000000..4db5d6e0
--- /dev/null
+++ b/android/security/keystore/recovery/RecoverySession.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.security.keystore.recovery;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import java.security.SecureRandom;
+import java.security.cert.CertificateException;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Session to recover a {@link KeyChainSnapshot} from the remote trusted hardware, initiated by a
+ * recovery agent.
+ *
+ * @hide
+ */
+@SystemApi
+public class RecoverySession implements AutoCloseable {
+ private static final String TAG = "RecoverySession";
+
+ private static final int SESSION_ID_LENGTH_BYTES = 16;
+
+ private final String mSessionId;
+ private final RecoveryController mRecoveryController;
+
+ private RecoverySession(RecoveryController recoveryController, String sessionId) {
+ mRecoveryController = recoveryController;
+ mSessionId = sessionId;
+ }
+
+ /**
+ * A new session, started by {@code recoveryManager}.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ static RecoverySession newInstance(RecoveryController recoveryController) {
+ return new RecoverySession(recoveryController, newSessionId());
+ }
+
+ /**
+ * Returns a new random session ID.
+ */
+ private static String newSessionId() {
+ SecureRandom secureRandom = new SecureRandom();
+ byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES];
+ secureRandom.nextBytes(sessionId);
+ StringBuilder sb = new StringBuilder();
+ for (byte b : sessionId) {
+ sb.append(Byte.toHexString(b, /*upperCase=*/ false));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Starts a recovery session and returns a blob with proof of recovery secret possession.
+ * The method generates a symmetric key for a session, which trusted remote device can use to
+ * return recovery key.
+ *
+ * @param verifierPublicKey Encoded {@code java.security.cert.X509Certificate} with Public key
+ * used to create the recovery blob on the source device.
+ * Keystore will verify the certificate using root of trust.
+ * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
+ * Used to limit number of guesses.
+ * @param vaultChallenge Data passed from server for this recovery session and used to prevent
+ * replay attacks
+ * @param secrets Secrets provided by user, the method only uses type and secret fields.
+ * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is
+ * encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric
+ * key and parameters necessary to identify the counter with the number of failed recovery
+ * attempts.
+ * @throws CertificateException if the {@code verifierPublicKey} is in an incorrect
+ * format.
+ * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
+ * service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ @NonNull public byte[] start(
+ @NonNull byte[] verifierPublicKey,
+ @NonNull byte[] vaultParams,
+ @NonNull byte[] vaultChallenge,
+ @NonNull List<KeyChainProtectionParams> secrets)
+ throws CertificateException, InternalRecoveryServiceException {
+ try {
+ byte[] recoveryClaim =
+ mRecoveryController.getBinder().startRecoverySession(
+ mSessionId,
+ verifierPublicKey,
+ vaultParams,
+ vaultChallenge,
+ secrets);
+ return recoveryClaim;
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT) {
+ throw new CertificateException(e.getMessage());
+ }
+ throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * Imports keys.
+ *
+ * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
+ * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
+ * and session. KeyStore only uses package names from the application info in {@link
+ * WrappedApplicationKey}. Caller is responsibility to perform certificates check.
+ * @return Map from alias to raw key material.
+ * @throws SessionExpiredException if {@code session} has since been closed.
+ * @throws DecryptionFailedException if unable to decrypt the snapshot.
+ * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ public Map<String, byte[]> recoverKeys(
+ @NonNull byte[] recoveryKeyBlob,
+ @NonNull List<WrappedApplicationKey> applicationKeys)
+ throws SessionExpiredException, DecryptionFailedException,
+ InternalRecoveryServiceException {
+ try {
+ return (Map<String, byte[]>) mRecoveryController.getBinder().recoverKeys(
+ mSessionId, recoveryKeyBlob, applicationKeys);
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) {
+ throw new DecryptionFailedException(e.getMessage());
+ }
+ if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) {
+ throw new SessionExpiredException(e.getMessage());
+ }
+ throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
+ }
+ }
+
+ /**
+ * An internal session ID, used by the framework to match recovery claims to snapshot responses.
+ *
+ * @hide
+ */
+ String getSessionId() {
+ return mSessionId;
+ }
+
+ /**
+ * Deletes all data associated with {@code session}. Should not be invoked directly but via
+ * {@link RecoverySession#close()}.
+ */
+ @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
+ @Override
+ public void close() {
+ try {
+ mRecoveryController.getBinder().closeSession(mSessionId);
+ } catch (RemoteException | ServiceSpecificException e) {
+ Log.e(TAG, "Unexpected error trying to close session", e);
+ }
+ }
+}
diff --git a/android/support/design/widget/CircularBorderDrawableLollipop.java b/android/security/keystore/recovery/SessionExpiredException.java
index 80084048..8c18e419 100644
--- a/android/support/design/widget/CircularBorderDrawableLollipop.java
+++ b/android/security/keystore/recovery/SessionExpiredException.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 The Android Open Source Project
+ * 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.
@@ -14,21 +14,20 @@
* limitations under the License.
*/
-package android.support.design.widget;
+package android.security.keystore.recovery;
-import android.graphics.Outline;
-import android.support.annotation.RequiresApi;
+import android.annotation.SystemApi;
+
+import java.security.GeneralSecurityException;
/**
- * Lollipop version of {@link CircularBorderDrawable}.
+ * Error thrown when attempting to use a {@link RecoverySession} that has since expired.
+ *
+ * @hide
*/
-@RequiresApi(21)
-class CircularBorderDrawableLollipop extends CircularBorderDrawable {
-
- @Override
- public void getOutline(Outline outline) {
- copyBounds(mRect);
- outline.setOval(mRect);
+@SystemApi
+public class SessionExpiredException extends GeneralSecurityException {
+ public SessionExpiredException(String msg) {
+ super(msg);
}
-
}
diff --git a/android/security/keystore/recovery/WrappedApplicationKey.java b/android/security/keystore/recovery/WrappedApplicationKey.java
new file mode 100644
index 00000000..f360bbe9
--- /dev/null
+++ b/android/security/keystore/recovery/WrappedApplicationKey.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.recovery;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+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>Account Recovery Agent specific account associated with the key.
+ * <li>Encrypted key material.
+ * </ul>
+ *
+ * Note that Application info is not included. Recovery Agent can only make its own keys
+ * recoverable.
+ *
+ * @hide
+ */
+@SystemApi
+public final class WrappedApplicationKey implements Parcelable {
+ private String mAlias;
+ // The only supported format is AES-256 symmetric key.
+ private byte[] mEncryptedKeyMaterial;
+ private byte[] mAccount;
+
+ /**
+ * Builder for creating {@link WrappedApplicationKey}.
+ */
+ public static class Builder {
+ private WrappedApplicationKey mInstance = new WrappedApplicationKey();
+
+ /**
+ * Sets Application-specific alias of the key.
+ *
+ * @param alias The alias.
+ * @return This builder.
+ */
+ public Builder setAlias(@NonNull String alias) {
+ mInstance.mAlias = alias;
+ return this;
+ }
+
+ /**
+ * Sets Recovery agent specific account.
+ *
+ * @param account The account.
+ * @return This builder.
+ */
+ public Builder setAccount(@NonNull byte[] account) {
+ mInstance.mAccount = account;
+ return this;
+ }
+
+ /**
+ * Sets key material encrypted by recovery key.
+ *
+ * @param encryptedKeyMaterial The key material
+ * @return This builder
+ */
+
+ public Builder setEncryptedKeyMaterial(@NonNull byte[] encryptedKeyMaterial) {
+ mInstance.mEncryptedKeyMaterial = encryptedKeyMaterial;
+ return this;
+ }
+
+ /**
+ * Creates a new {@link WrappedApplicationKey} instance.
+ *
+ * @return new instance
+ * @throws NullPointerException if some required fields were not set.
+ */
+ @NonNull public WrappedApplicationKey build() {
+ Preconditions.checkNotNull(mInstance.mAlias);
+ Preconditions.checkNotNull(mInstance.mEncryptedKeyMaterial);
+ if (mInstance.mAccount == null) {
+ mInstance.mAccount = new byte[]{};
+ }
+ return mInstance;
+ }
+ }
+
+ private WrappedApplicationKey() {
+ }
+
+ /**
+ * Deprecated - consider using Builder.
+ * @hide
+ */
+ public WrappedApplicationKey(@NonNull String alias, @NonNull byte[] encryptedKeyMaterial) {
+ mAlias = Preconditions.checkNotNull(alias);
+ mEncryptedKeyMaterial = Preconditions.checkNotNull(encryptedKeyMaterial);
+ }
+
+ /**
+ * Application-specific alias of the key.
+ *
+ * @see java.security.KeyStore.aliases
+ */
+ public @NonNull String getAlias() {
+ return mAlias;
+ }
+
+ /** Key material encrypted by recovery key. */
+ public @NonNull byte[] getEncryptedKeyMaterial() {
+ return mEncryptedKeyMaterial;
+ }
+
+ /** Account, default value is empty array */
+ public @NonNull byte[] getAccount() {
+ if (mAccount == null) {
+ return new byte[]{};
+ }
+ return mAccount;
+ }
+
+ public static final Parcelable.Creator<WrappedApplicationKey> CREATOR =
+ new Parcelable.Creator<WrappedApplicationKey>() {
+ public WrappedApplicationKey createFromParcel(Parcel in) {
+ return new WrappedApplicationKey(in);
+ }
+
+ public WrappedApplicationKey[] newArray(int length) {
+ return new WrappedApplicationKey[length];
+ }
+ };
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mAlias);
+ out.writeByteArray(mEncryptedKeyMaterial);
+ out.writeByteArray(mAccount);
+ }
+
+ /**
+ * @hide
+ */
+ protected WrappedApplicationKey(Parcel in) {
+ mAlias = in.readString();
+ mEncryptedKeyMaterial = in.createByteArray();
+ mAccount = in.createByteArray();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+}
diff --git a/android/security/recoverablekeystore/KeyEntryRecoveryData.java b/android/security/recoverablekeystore/KeyEntryRecoveryData.java
deleted file mode 100644
index 80f5aa71..00000000
--- a/android/security/recoverablekeystore/KeyEntryRecoveryData.java
+++ /dev/null
@@ -1,90 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.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
deleted file mode 100644
index 087f7a25..00000000
--- a/android/security/recoverablekeystore/KeyStoreRecoveryData.java
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.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
deleted file mode 100644
index 43f9c805..00000000
--- a/android/security/recoverablekeystore/KeyStoreRecoveryMetadata.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.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
deleted file mode 100644
index 72a138a6..00000000
--- a/android/security/recoverablekeystore/RecoverableKeyStoreLoader.java
+++ /dev/null
@@ -1,467 +0,0 @@
-/*
- * Copyright (C) 2017 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package android.security.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/AutofillFieldClassificationService.java b/android/service/autofill/AutofillFieldClassificationService.java
new file mode 100644
index 00000000..1ef6100f
--- /dev/null
+++ b/android/service/autofill/AutofillFieldClassificationService.java
@@ -0,0 +1,235 @@
+/*
+ * 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.service.autofill;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.os.HandlerCaller;
+import com.android.internal.os.SomeArgs;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A service that calculates field classification scores.
+ *
+ * <p>A field classification score is a {@code float} representing how well an
+ * {@link AutofillValue} filled matches a expected value predicted by an autofill service
+ * &mdash;a full-match is {@code 1.0} (representing 100%), while a full mismatch is {@code 0.0}.
+ *
+ * <p>The exact score depends on the algorithm used to calculate it&mdash; the service must provide
+ * at least one default algorithm (which is used when the algorithm is not specified or is invalid),
+ * but it could provide more (in which case the algorithm name should be specifiied by the caller
+ * when calculating the scores).
+ *
+ * {@hide}
+ */
+@SystemApi
+public abstract class AutofillFieldClassificationService extends Service {
+
+ private static final String TAG = "AutofillFieldClassificationService";
+
+ private static final int MSG_GET_SCORES = 1;
+
+ /**
+ * The {@link Intent} action that must be declared as handled by a service
+ * in its manifest for the system to recognize it as a quota providing service.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.service.autofill.AutofillFieldClassificationService";
+
+ /**
+ * Manifest metadata key for the resource string containing the name of the default field
+ * classification algorithm.
+ */
+ public static final String SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM =
+ "android.autofill.field_classification.default_algorithm";
+ /**
+ * Manifest metadata key for the resource string array containing the names of all field
+ * classification algorithms provided by the service.
+ */
+ public static final String SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS =
+ "android.autofill.field_classification.available_algorithms";
+
+
+ /** {@hide} **/
+ public static final String EXTRA_SCORES = "scores";
+
+ private AutofillFieldClassificationServiceWrapper mWrapper;
+
+ private final HandlerCaller.Callback mHandlerCallback = (msg) -> {
+ final int action = msg.what;
+ final Bundle data = new Bundle();
+ final RemoteCallback callback;
+ switch (action) {
+ case MSG_GET_SCORES:
+ final SomeArgs args = (SomeArgs) msg.obj;
+ callback = (RemoteCallback) args.arg1;
+ final String algorithmName = (String) args.arg2;
+ final Bundle algorithmArgs = (Bundle) args.arg3;
+ @SuppressWarnings("unchecked")
+ final List<AutofillValue> actualValues = ((List<AutofillValue>) args.arg4);
+ @SuppressWarnings("unchecked")
+ final String[] userDataValues = (String[]) args.arg5;
+ final float[][] scores = onGetScores(algorithmName, algorithmArgs, actualValues,
+ Arrays.asList(userDataValues));
+ if (scores != null) {
+ data.putParcelable(EXTRA_SCORES, new Scores(scores));
+ }
+ break;
+ default:
+ Log.w(TAG, "Handling unknown message: " + action);
+ return;
+ }
+ callback.sendResult(data);
+ };
+
+ private final HandlerCaller mHandlerCaller = new HandlerCaller(null, Looper.getMainLooper(),
+ mHandlerCallback, true);
+
+ /** @hide */
+ public AutofillFieldClassificationService() {
+
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ mWrapper = new AutofillFieldClassificationServiceWrapper();
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return mWrapper;
+ }
+
+ /**
+ * Calculates field classification scores in a batch.
+ *
+ * <p>See {@link AutofillFieldClassificationService} for more info about field classification
+ * scores.
+ *
+ * @param algorithm name of the algorithm to be used to calculate the scores. If invalid, the
+ * default algorithm will be used instead.
+ * @param args optional arguments to be passed to the algorithm.
+ * @param actualValues values entered by the user.
+ * @param userDataValues values predicted from the user data.
+ * @return the calculated scores, with the first dimension representing actual values and the
+ * second dimension values from {@link UserData}.
+ *
+ * {@hide}
+ */
+ @Nullable
+ @SystemApi
+ public float[][] onGetScores(@Nullable String algorithm,
+ @Nullable Bundle args, @NonNull List<AutofillValue> actualValues,
+ @NonNull List<String> userDataValues) {
+ Log.e(TAG, "service implementation (" + getClass() + " does not implement onGetScore()");
+ return null;
+ }
+
+ private final class AutofillFieldClassificationServiceWrapper
+ extends IAutofillFieldClassificationService.Stub {
+ @Override
+ public void getScores(RemoteCallback callback, String algorithmName, Bundle algorithmArgs,
+ List<AutofillValue> actualValues, String[] userDataValues)
+ throws RemoteException {
+ // TODO(b/70939974): refactor to use PooledLambda
+ mHandlerCaller.obtainMessageOOOOO(MSG_GET_SCORES, callback, algorithmName,
+ algorithmArgs, actualValues, userDataValues).sendToTarget();
+ }
+ }
+
+ /**
+ * Helper class used to encapsulate a float[][] in a Parcelable.
+ *
+ * {@hide}
+ */
+ public static final class Scores implements Parcelable {
+ @NonNull
+ public final float[][] scores;
+
+ private Scores(Parcel parcel) {
+ final int size1 = parcel.readInt();
+ final int size2 = parcel.readInt();
+ scores = new float[size1][size2];
+ for (int i = 0; i < size1; i++) {
+ for (int j = 0; j < size2; j++) {
+ scores[i][j] = parcel.readFloat();
+ }
+ }
+ }
+
+ private Scores(@NonNull float[][] scores) {
+ this.scores = scores;
+ }
+
+ @Override
+ public String toString() {
+ final int size1 = scores.length;
+ final int size2 = size1 > 0 ? scores[0].length : 0;
+ final StringBuilder builder = new StringBuilder("Scores [")
+ .append(size1).append("x").append(size2).append("] ");
+ for (int i = 0; i < size1; i++) {
+ builder.append(i).append(": ").append(Arrays.toString(scores[i])).append(' ');
+ }
+ return builder.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ int size1 = scores.length;
+ int size2 = scores[0].length;
+ parcel.writeInt(size1);
+ parcel.writeInt(size2);
+ for (int i = 0; i < size1; i++) {
+ for (int j = 0; j < size2; j++) {
+ parcel.writeFloat(scores[i][j]);
+ }
+ }
+ }
+
+ public static final Creator<Scores> CREATOR = new Creator<Scores>() {
+ @Override
+ public Scores createFromParcel(Parcel parcel) {
+ return new Scores(parcel);
+ }
+
+ @Override
+ public Scores[] newArray(int size) {
+ return new Scores[size];
+ }
+ };
+ }
+}
diff --git a/android/service/autofill/CharSequenceTransformation.java b/android/service/autofill/CharSequenceTransformation.java
index 2413e97b..f52ac850 100644
--- a/android/service/autofill/CharSequenceTransformation.java
+++ b/android/service/autofill/CharSequenceTransformation.java
@@ -22,7 +22,6 @@ import android.annotation.NonNull;
import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
-import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import android.view.autofill.AutofillId;
@@ -31,6 +30,8 @@ import android.widget.TextView;
import com.android.internal.util.Preconditions;
+import java.util.LinkedHashMap;
+import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -62,7 +63,9 @@ import java.util.regex.Pattern;
public final class CharSequenceTransformation extends InternalTransformation implements
Transformation, Parcelable {
private static final String TAG = "CharSequenceTransformation";
- @NonNull private final ArrayMap<AutofillId, Pair<Pattern, String>> mFields;
+
+ // Must use LinkedHashMap to preserve insertion order.
+ @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields;
private CharSequenceTransformation(Builder builder) {
mFields = builder.mFields;
@@ -76,9 +79,9 @@ public final class CharSequenceTransformation extends InternalTransformation imp
final StringBuilder converted = new StringBuilder();
final int size = mFields.size();
if (sDebug) Log.d(TAG, size + " multiple fields on id " + childViewId);
- for (int i = 0; i < size; i++) {
- final AutofillId id = mFields.keyAt(i);
- final Pair<Pattern, String> field = mFields.valueAt(i);
+ for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
+ final AutofillId id = entry.getKey();
+ final Pair<Pattern, String> field = entry.getValue();
final String value = finder.findByAutofillId(id);
if (value == null) {
Log.w(TAG, "No value for id " + id);
@@ -107,8 +110,10 @@ public final class CharSequenceTransformation extends InternalTransformation imp
* Builder for {@link CharSequenceTransformation} objects.
*/
public static class Builder {
- @NonNull private final ArrayMap<AutofillId, Pair<Pattern, String>> mFields =
- new ArrayMap<>();
+
+ // Must use LinkedHashMap to preserve insertion order.
+ @NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields =
+ new LinkedHashMap<>();
private boolean mDestroyed;
/**
@@ -186,12 +191,15 @@ public final class CharSequenceTransformation extends InternalTransformation imp
final Pattern[] regexs = new Pattern[size];
final String[] substs = new String[size];
Pair<Pattern, String> pair;
- for (int i = 0; i < size; i++) {
- ids[i] = mFields.keyAt(i);
- pair = mFields.valueAt(i);
+ int i = 0;
+ for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
+ ids[i] = entry.getKey();
+ pair = entry.getValue();
regexs[i] = pair.first;
substs[i] = pair.second;
+ i++;
}
+
parcel.writeParcelableArray(ids, flags);
parcel.writeSerializable(regexs);
parcel.writeStringArray(substs);
diff --git a/android/service/autofill/FieldClassification.java b/android/service/autofill/FieldClassification.java
index 001b2917..cd1efd68 100644
--- a/android/service/autofill/FieldClassification.java
+++ b/android/service/autofill/FieldClassification.java
@@ -105,9 +105,6 @@ public final class FieldClassification {
/**
* 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 {
@@ -140,8 +137,9 @@ public final class FieldClassification {
* <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.
+ * <p>How the score is calculated depends on the
+ * {@link UserData.Builder#setFieldClassificationAlgorithm(String, android.os.Bundle)
+ * algorithm} used.
*/
public float getScore() {
return mScore;
diff --git a/android/service/autofill/InternalScorer.java b/android/service/autofill/InternalScorer.java
deleted file mode 100644
index 0da5afc2..00000000
--- a/android/service/autofill/InternalScorer.java
+++ /dev/null
@@ -1,40 +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.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/UserData.java b/android/service/autofill/UserData.java
index f0cc360f..90178480 100644
--- a/android/service/autofill/UserData.java
+++ b/android/service/autofill/UserData.java
@@ -25,10 +25,13 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityThread;
import android.content.ContentResolver;
+import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.Settings;
+import android.service.autofill.FieldClassification.Match;
import android.util.Log;
+import android.view.autofill.AutofillManager;
import android.view.autofill.Helper;
import com.android.internal.util.Preconditions;
@@ -49,21 +52,32 @@ public final class UserData implements Parcelable {
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 mAlgorithm;
+ private final Bundle mAlgorithmArgs;
private final String[] mRemoteIds;
private final String[] mValues;
private UserData(Builder builder) {
- mScorer = builder.mScorer;
+ mAlgorithm = builder.mAlgorithm;
+ mAlgorithmArgs = builder.mAlgorithmArgs;
mRemoteIds = new String[builder.mRemoteIds.size()];
builder.mRemoteIds.toArray(mRemoteIds);
mValues = new String[builder.mValues.size()];
builder.mValues.toArray(mValues);
}
+ /**
+ * Gets the name of the algorithm that is used to calculate
+ * {@link Match#getScore() match scores}.
+ */
+ @Nullable
+ public String getFieldClassificationAlgorithm() {
+ return mAlgorithm;
+ }
+
/** @hide */
- public InternalScorer getScorer() {
- return mScorer;
+ public Bundle getAlgorithmArgs() {
+ return mAlgorithmArgs;
}
/** @hide */
@@ -78,7 +92,9 @@ public final class UserData implements Parcelable {
/** @hide */
public void dump(String prefix, PrintWriter pw) {
- pw.print(prefix); pw.print("Scorer: "); pw.println(mScorer);
+ pw.print(prefix); pw.print("Algorithm: "); pw.print(mAlgorithm);
+ pw.print(" Args: "); pw.println(mAlgorithmArgs);
+
// 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++) {
@@ -105,9 +121,10 @@ public final class UserData implements Parcelable {
* 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 String mAlgorithm;
+ private Bundle mAlgorithmArgs;
private boolean mDestroyed;
/**
@@ -120,13 +137,9 @@ public final class UserData implements Parcelable {
* <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;
+ public Builder(@NonNull String remoteId, @NonNull String value) {
checkValidRemoteId(remoteId);
checkValidValue(value);
final int capacity = getMaxUserDataSize();
@@ -137,6 +150,28 @@ public final class UserData implements Parcelable {
}
/**
+ * Sets the algorithm used for <a href="#FieldClassification">field classification</a>.
+ *
+ * <p>The currently available algorithms can be retrieve through
+ * {@link AutofillManager#getAvailableFieldClassificationAlgorithms()}.
+ *
+ * <p>If not set, the
+ * {@link AutofillManager#getDefaultFieldClassificationAlgorithm() default algorithm} is
+ * used instead.
+ *
+ * @param name name of the algorithm or {@code null} to used default.
+ * @param args optional arguments to the algorithm.
+ *
+ * @return this builder
+ */
+ public Builder setFieldClassificationAlgorithm(@Nullable String name,
+ @Nullable Bundle args) {
+ mAlgorithm = name;
+ mAlgorithmArgs = args;
+ return this;
+ }
+
+ /**
* Adds a new value for user data.
*
* @param remoteId unique string used to identify the user data.
@@ -211,7 +246,7 @@ public final class UserData implements Parcelable {
public String toString() {
if (!sDebug) return super.toString();
- final StringBuilder builder = new StringBuilder("UserData: [scorer=").append(mScorer);
+ final StringBuilder builder = new StringBuilder("UserData: [algorithm=").append(mAlgorithm);
// Cannot disclose remote ids or values because they could contain PII
builder.append(", remoteIds=");
Helper.appendRedacted(builder, mRemoteIds);
@@ -231,9 +266,10 @@ public final class UserData implements Parcelable {
@Override
public void writeToParcel(Parcel parcel, int flags) {
- parcel.writeParcelable(mScorer, flags);
parcel.writeStringArray(mRemoteIds);
parcel.writeStringArray(mValues);
+ parcel.writeString(mAlgorithm);
+ parcel.writeBundle(mAlgorithmArgs);
}
public static final Parcelable.Creator<UserData> CREATOR =
@@ -243,10 +279,10 @@ public final class UserData implements Parcelable {
// 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]);
+ final Builder builder = new Builder(remoteIds[0], values[0])
+ .setFieldClassificationAlgorithm(parcel.readString(), parcel.readBundle());
for (int i = 1; i < remoteIds.length; i++) {
builder.add(remoteIds[i], values[i]);
}
diff --git a/android/service/carrier/CarrierIdentifier.java b/android/service/carrier/CarrierIdentifier.java
index b47e872d..09bba4b4 100644
--- a/android/service/carrier/CarrierIdentifier.java
+++ b/android/service/carrier/CarrierIdentifier.java
@@ -16,9 +16,14 @@
package android.service.carrier;
+import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.internal.telephony.uicc.IccUtils;
+
+import java.util.Objects;
+
/**
* Used to pass info to CarrierConfigService implementations so they can decide what values to
* return.
@@ -40,13 +45,13 @@ public class CarrierIdentifier implements Parcelable {
private String mMcc;
private String mMnc;
- private String mSpn;
- private String mImsi;
- private String mGid1;
- private String mGid2;
+ private @Nullable String mSpn;
+ private @Nullable String mImsi;
+ private @Nullable String mGid1;
+ private @Nullable String mGid2;
- public CarrierIdentifier(String mcc, String mnc, String spn, String imsi, String gid1,
- String gid2) {
+ public CarrierIdentifier(String mcc, String mnc, @Nullable String spn, @Nullable String imsi,
+ @Nullable String gid1, @Nullable String gid2) {
mMcc = mcc;
mMnc = mnc;
mSpn = spn;
@@ -55,6 +60,32 @@ public class CarrierIdentifier implements Parcelable {
mGid2 = gid2;
}
+ /**
+ * Creates a carrier identifier instance.
+ *
+ * @param mccMnc A 3-byte array as defined by 3GPP TS 24.008.
+ * @param gid1 The group identifier level 1.
+ * @param gid2 The group identifier level 2.
+ * @throws IllegalArgumentException If the length of {@code mccMnc} is not 3.
+ */
+ public CarrierIdentifier(byte[] mccMnc, @Nullable String gid1, @Nullable String gid2) {
+ if (mccMnc.length != 3) {
+ throw new IllegalArgumentException(
+ "MCC & MNC must be set by a 3-byte array: byte[" + mccMnc.length + "]");
+ }
+ String hex = IccUtils.bytesToHexString(mccMnc);
+ mMcc = new String(new char[] {hex.charAt(1), hex.charAt(0), hex.charAt(3)});
+ if (hex.charAt(2) == 'F') {
+ mMnc = new String(new char[] {hex.charAt(5), hex.charAt(4)});
+ } else {
+ mMnc = new String(new char[] {hex.charAt(5), hex.charAt(4), hex.charAt(2)});
+ }
+ mGid1 = gid1;
+ mGid2 = gid2;
+ mSpn = null;
+ mImsi = null;
+ }
+
/** @hide */
public CarrierIdentifier(Parcel parcel) {
readFromParcel(parcel);
@@ -71,26 +102,60 @@ public class CarrierIdentifier implements Parcelable {
}
/** Get the service provider name. */
+ @Nullable
public String getSpn() {
return mSpn;
}
/** Get the international mobile subscriber identity. */
+ @Nullable
public String getImsi() {
return mImsi;
}
/** Get the group identifier level 1. */
+ @Nullable
public String getGid1() {
return mGid1;
}
/** Get the group identifier level 2. */
+ @Nullable
public String getGid2() {
return mGid2;
}
@Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ CarrierIdentifier that = (CarrierIdentifier) obj;
+ return Objects.equals(mMcc, that.mMcc)
+ && Objects.equals(mMnc, that.mMnc)
+ && Objects.equals(mSpn, that.mSpn)
+ && Objects.equals(mImsi, that.mImsi)
+ && Objects.equals(mGid1, that.mGid1)
+ && Objects.equals(mGid2, that.mGid2);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + Objects.hashCode(mMcc);
+ result = 31 * result + Objects.hashCode(mMnc);
+ result = 31 * result + Objects.hashCode(mSpn);
+ result = 31 * result + Objects.hashCode(mImsi);
+ result = 31 * result + Objects.hashCode(mGid1);
+ result = 31 * result + Objects.hashCode(mGid2);
+ return result;
+ }
+
+ @Override
public int describeContents() {
return 0;
}
diff --git a/android/service/dreams/DreamService.java b/android/service/dreams/DreamService.java
index 2a245d04..99e2c620 100644
--- a/android/service/dreams/DreamService.java
+++ b/android/service/dreams/DreamService.java
@@ -17,6 +17,7 @@ package android.service.dreams;
import android.annotation.IdRes;
import android.annotation.LayoutRes;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.annotation.SdkConstant.SdkConstantType;
@@ -54,7 +55,6 @@ import com.android.internal.util.DumpUtils.Dump;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.util.List;
/**
* Extend this class to implement a custom dream (available to the user as a "Daydream").
@@ -458,8 +458,16 @@ public class DreamService extends Service implements Window.Callback {
* was processed in {@link #onCreate}.
*
* <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p>
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
*
+ * @param id the ID to search for
* @return The view if found or null otherwise.
+ * @see View#findViewById(int)
+ * @see DreamService#requireViewById(int)
*/
@Nullable
public <T extends View> T findViewById(@IdRes int id) {
@@ -467,6 +475,33 @@ public class DreamService extends Service implements Window.Callback {
}
/**
+ * Finds a view that was identified by the id attribute from the XML that was processed in
+ * {@link #onCreate}, or throws an IllegalArgumentException if the ID is invalid or there is no
+ * matching view in the hierarchy.
+ *
+ * <p>Note: Requires a window, do not call before {@link #onAttachedToWindow()}</p>
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#requireViewById(int)
+ * @see DreamService#findViewById(int)
+ */
+ @NonNull
+ public final <T extends View> T requireViewById(@IdRes int id) {
+ T view = findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException(
+ "ID does not reference a View inside this DreamService");
+ }
+ return view;
+ }
+
+ /**
* Marks this dream as interactive to receive input events.
*
* <p>Non-interactive dreams (default) will dismiss on the first input event.</p>
diff --git a/android/service/euicc/EuiccProfileInfo.java b/android/service/euicc/EuiccProfileInfo.java
index ba6c9a2d..8e752d1c 100644
--- a/android/service/euicc/EuiccProfileInfo.java
+++ b/android/service/euicc/EuiccProfileInfo.java
@@ -15,12 +15,19 @@
*/
package android.service.euicc;
+import android.annotation.IntDef;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
+import android.service.carrier.CarrierIdentifier;
import android.telephony.UiccAccessRule;
import android.text.TextUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
/**
* Information about an embedded profile (subscription) on an eUICC.
*
@@ -30,18 +37,90 @@ import android.text.TextUtils;
*/
public final class EuiccProfileInfo implements Parcelable {
+ /** Profile policy rules (bit mask) */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "POLICY_RULE_" }, value = {
+ POLICY_RULE_DO_NOT_DISABLE,
+ POLICY_RULE_DO_NOT_DELETE,
+ POLICY_RULE_DELETE_AFTER_DISABLING
+ })
+ public @interface PolicyRule {}
+ /** Once this profile is enabled, it cannot be disabled. */
+ public static final int POLICY_RULE_DO_NOT_DISABLE = 1;
+ /** This profile cannot be deleted. */
+ public static final int POLICY_RULE_DO_NOT_DELETE = 1 << 1;
+ /** This profile should be deleted after being disabled. */
+ public static final int POLICY_RULE_DELETE_AFTER_DISABLING = 1 << 2;
+
+ /** Class of the profile */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "PROFILE_CLASS_" }, value = {
+ PROFILE_CLASS_TESTING,
+ PROFILE_CLASS_PROVISIONING,
+ PROFILE_CLASS_OPERATIONAL,
+ PROFILE_CLASS_UNSET
+ })
+ public @interface ProfileClass {}
+ /** Testing profiles */
+ public static final int PROFILE_CLASS_TESTING = 0;
+ /** Provisioning profiles which are pre-loaded on eUICC */
+ public static final int PROFILE_CLASS_PROVISIONING = 1;
+ /** Operational profiles which can be pre-loaded or downloaded */
+ public static final int PROFILE_CLASS_OPERATIONAL = 2;
+ /**
+ * Profile class not set.
+ * @hide
+ */
+ public static final int PROFILE_CLASS_UNSET = -1;
+
+ /** State of the profile */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "PROFILE_STATE_" }, value = {
+ PROFILE_STATE_DISABLED,
+ PROFILE_STATE_ENABLED,
+ PROFILE_STATE_UNSET
+ })
+ public @interface ProfileState {}
+ /** Disabled profiles */
+ public static final int PROFILE_STATE_DISABLED = 0;
+ /** Enabled profile */
+ public static final int PROFILE_STATE_ENABLED = 1;
+ /**
+ * Profile state not set.
+ * @hide
+ */
+ public static final int PROFILE_STATE_UNSET = -1;
+
/** The iccid of the subscription. */
public final String iccid;
+ /** An optional nickname for the subscription. */
+ public final @Nullable String nickname;
+
+ /** The service provider name for the subscription. */
+ public final String serviceProviderName;
+
+ /** The profile name for the subscription. */
+ public final String profileName;
+
+ /** Profile class for the subscription. */
+ @ProfileClass public final int profileClass;
+
+ /** The profile state of the subscription. */
+ @ProfileState public final int state;
+
+ /** The operator Id of the subscription. */
+ public final CarrierIdentifier carrierIdentifier;
+
+ /** The policy rules of the subscription. */
+ @PolicyRule public final int policyRules;
+
/**
* Optional access rules defining which apps can manage this subscription. If unset, only the
* platform can manage it.
*/
public final @Nullable UiccAccessRule[] accessRules;
- /** An optional nickname for the subscription. */
- public final @Nullable String nickname;
-
public static final Creator<EuiccProfileInfo> CREATOR = new Creator<EuiccProfileInfo>() {
@Override
public EuiccProfileInfo createFromParcel(Parcel in) {
@@ -54,6 +133,12 @@ public final class EuiccProfileInfo implements Parcelable {
}
};
+ // TODO(b/70292228): Remove this method when LPA can be updated.
+ /**
+ * @hide
+ * @deprecated - Do not use.
+ */
+ @Deprecated
public EuiccProfileInfo(String iccid, @Nullable UiccAccessRule[] accessRules,
@Nullable String nickname) {
if (!TextUtils.isDigitsOnly(iccid)) {
@@ -62,23 +147,290 @@ public final class EuiccProfileInfo implements Parcelable {
this.iccid = iccid;
this.accessRules = accessRules;
this.nickname = nickname;
+
+ this.serviceProviderName = null;
+ this.profileName = null;
+ this.profileClass = PROFILE_CLASS_UNSET;
+ this.state = PROFILE_CLASS_UNSET;
+ this.carrierIdentifier = null;
+ this.policyRules = 0;
}
private EuiccProfileInfo(Parcel in) {
iccid = in.readString();
- accessRules = in.createTypedArray(UiccAccessRule.CREATOR);
nickname = in.readString();
+ serviceProviderName = in.readString();
+ profileName = in.readString();
+ profileClass = in.readInt();
+ state = in.readInt();
+ byte exist = in.readByte();
+ if (exist == (byte) 1) {
+ carrierIdentifier = CarrierIdentifier.CREATOR.createFromParcel(in);
+ } else {
+ carrierIdentifier = null;
+ }
+ policyRules = in.readInt();
+ accessRules = in.createTypedArray(UiccAccessRule.CREATOR);
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(iccid);
- dest.writeTypedArray(accessRules, flags);
dest.writeString(nickname);
+ dest.writeString(serviceProviderName);
+ dest.writeString(profileName);
+ dest.writeInt(profileClass);
+ dest.writeInt(state);
+ if (carrierIdentifier != null) {
+ dest.writeByte((byte) 1);
+ carrierIdentifier.writeToParcel(dest, flags);
+ } else {
+ dest.writeByte((byte) 0);
+ }
+ dest.writeInt(policyRules);
+ dest.writeTypedArray(accessRules, flags);
}
@Override
public int describeContents() {
return 0;
}
+
+ /** The builder to build a new {@link EuiccProfileInfo} instance. */
+ public static final class Builder {
+ public String iccid;
+ public UiccAccessRule[] accessRules;
+ public String nickname;
+ public String serviceProviderName;
+ public String profileName;
+ @ProfileClass public int profileClass;
+ @ProfileState public int state;
+ public CarrierIdentifier carrierIdentifier;
+ @PolicyRule public int policyRules;
+
+ public Builder() {}
+
+ public Builder(EuiccProfileInfo baseProfile) {
+ iccid = baseProfile.iccid;
+ nickname = baseProfile.nickname;
+ serviceProviderName = baseProfile.serviceProviderName;
+ profileName = baseProfile.profileName;
+ profileClass = baseProfile.profileClass;
+ state = baseProfile.state;
+ carrierIdentifier = baseProfile.carrierIdentifier;
+ policyRules = baseProfile.policyRules;
+ accessRules = baseProfile.accessRules;
+ }
+
+ /** Builds the profile instance. */
+ public EuiccProfileInfo build() {
+ if (iccid == null) {
+ throw new IllegalStateException("ICCID must be set for a profile.");
+ }
+ return new EuiccProfileInfo(
+ iccid,
+ nickname,
+ serviceProviderName,
+ profileName,
+ profileClass,
+ state,
+ carrierIdentifier,
+ policyRules,
+ accessRules);
+ }
+
+ /** Sets the iccId of the subscription. */
+ public Builder setIccid(String value) {
+ if (!TextUtils.isDigitsOnly(value)) {
+ throw new IllegalArgumentException("iccid contains invalid characters: " + value);
+ }
+ iccid = value;
+ return this;
+ }
+
+ /** Sets the nickname of the subscription. */
+ public Builder setNickname(String value) {
+ nickname = value;
+ return this;
+ }
+
+ /** Sets the service provider name of the subscription. */
+ public Builder setServiceProviderName(String value) {
+ serviceProviderName = value;
+ return this;
+ }
+
+ /** Sets the profile name of the subscription. */
+ public Builder setProfileName(String value) {
+ profileName = value;
+ return this;
+ }
+
+ /** Sets the profile class of the subscription. */
+ public Builder setProfileClass(@ProfileClass int value) {
+ profileClass = value;
+ return this;
+ }
+
+ /** Sets the state of the subscription. */
+ public Builder setState(@ProfileState int value) {
+ state = value;
+ return this;
+ }
+
+ /** Sets the carrier identifier of the subscription. */
+ public Builder setCarrierIdentifier(CarrierIdentifier value) {
+ carrierIdentifier = value;
+ return this;
+ }
+
+ /** Sets the policy rules of the subscription. */
+ public Builder setPolicyRules(@PolicyRule int value) {
+ policyRules = value;
+ return this;
+ }
+
+ /** Sets the access rules of the subscription. */
+ public Builder setUiccAccessRule(@Nullable UiccAccessRule[] value) {
+ accessRules = value;
+ return this;
+ }
+ }
+
+ private EuiccProfileInfo(
+ String iccid,
+ @Nullable String nickname,
+ String serviceProviderName,
+ String profileName,
+ @ProfileClass int profileClass,
+ @ProfileState int state,
+ CarrierIdentifier carrierIdentifier,
+ @PolicyRule int policyRules,
+ @Nullable UiccAccessRule[] accessRules) {
+ this.iccid = iccid;
+ this.nickname = nickname;
+ this.serviceProviderName = serviceProviderName;
+ this.profileName = profileName;
+ this.profileClass = profileClass;
+ this.state = state;
+ this.carrierIdentifier = carrierIdentifier;
+ this.policyRules = policyRules;
+ this.accessRules = accessRules;
+ }
+
+ /** Gets the ICCID string. */
+ public String getIccid() {
+ return iccid;
+ }
+
+ /** Gets the access rules. */
+ @Nullable
+ public UiccAccessRule[] getUiccAccessRules() {
+ return accessRules;
+ }
+
+ /** Gets the nickname. */
+ public String getNickname() {
+ return nickname;
+ }
+
+ /** Gets the service provider name. */
+ public String getServiceProviderName() {
+ return serviceProviderName;
+ }
+
+ /** Gets the profile name. */
+ public String getProfileName() {
+ return profileName;
+ }
+
+ /** Gets the profile class. */
+ @ProfileClass
+ public int getProfileClass() {
+ return profileClass;
+ }
+
+ /** Gets the state of the subscription. */
+ @ProfileState
+ public int getState() {
+ return state;
+ }
+
+ /** Gets the carrier identifier. */
+ public CarrierIdentifier getCarrierIdentifier() {
+ return carrierIdentifier;
+ }
+
+ /** Gets the policy rules. */
+ @PolicyRule
+ public int getPolicyRules() {
+ return policyRules;
+ }
+
+ /** Returns whether any policy rule exists. */
+ public boolean hasPolicyRules() {
+ return policyRules != 0;
+ }
+
+ /** Checks whether a certain policy rule exists. */
+ public boolean hasPolicyRule(@PolicyRule int policy) {
+ return (policyRules & policy) != 0;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ EuiccProfileInfo that = (EuiccProfileInfo) obj;
+ return Objects.equals(iccid, that.iccid)
+ && Objects.equals(nickname, that.nickname)
+ && Objects.equals(serviceProviderName, that.serviceProviderName)
+ && Objects.equals(profileName, that.profileName)
+ && profileClass == that.profileClass
+ && state == that.state
+ && Objects.equals(carrierIdentifier, that.carrierIdentifier)
+ && policyRules == that.policyRules
+ && Arrays.equals(accessRules, that.accessRules);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + Objects.hashCode(iccid);
+ result = 31 * result + Objects.hashCode(nickname);
+ result = 31 * result + Objects.hashCode(serviceProviderName);
+ result = 31 * result + Objects.hashCode(profileName);
+ result = 31 * result + profileClass;
+ result = 31 * result + state;
+ result = 31 * result + Objects.hashCode(carrierIdentifier);
+ result = 31 * result + policyRules;
+ result = 31 * result + Arrays.hashCode(accessRules);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "EuiccProfileInfo (nickname="
+ + nickname
+ + ", serviceProviderName="
+ + serviceProviderName
+ + ", profileName="
+ + profileName
+ + ", profileClass="
+ + profileClass
+ + ", state="
+ + state
+ + ", CarrierIdentifier="
+ + carrierIdentifier.toString()
+ + ", policyRules="
+ + policyRules
+ + ", accessRules="
+ + Arrays.toString(accessRules)
+ + ")";
+ }
}
diff --git a/android/service/euicc/EuiccService.java b/android/service/euicc/EuiccService.java
index fb530074..be858007 100644
--- a/android/service/euicc/EuiccService.java
+++ b/android/service/euicc/EuiccService.java
@@ -193,6 +193,18 @@ public abstract class EuiccService extends Service {
}
/**
+ * Callback class for {@link #onStartOtaIfNecessary(int, OtaStatusChangedCallback)}
+ *
+ * The status of OTA which can be {@code android.telephony.euicc.EuiccManager#EUICC_OTA_}
+ *
+ * @see IEuiccService#startOtaIfNecessary
+ */
+ public interface OtaStatusChangedCallback {
+ /** Called when OTA status is changed. */
+ void onOtaStatusChanged(int status);
+ }
+
+ /**
* Return the EID of the eUICC.
*
* @param slotId ID of the SIM slot being queried. This is currently not populated but is here
@@ -214,6 +226,16 @@ public abstract class EuiccService extends Service {
public abstract @OtaStatus int onGetOtaStatus(int slotId);
/**
+ * Perform OTA if current OS is not the latest one.
+ *
+ * @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.
+ * @param statusChangedCallback Function called when OTA status changed.
+ */
+ public abstract void onStartOtaIfNecessary(
+ int slotId, OtaStatusChangedCallback statusChangedCallback);
+
+ /**
* 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
@@ -396,6 +418,26 @@ public abstract class EuiccService extends Service {
}
@Override
+ public void startOtaIfNecessary(
+ int slotId, IOtaStatusChangedCallback statusChangedCallback) {
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ EuiccService.this.onStartOtaIfNecessary(slotId, new OtaStatusChangedCallback() {
+ @Override
+ public void onOtaStatusChanged(int status) {
+ try {
+ statusChangedCallback.onOtaStatusChanged(status);
+ } catch (RemoteException e) {
+ // Can't communicate with the phone process; ignore.
+ }
+ }
+ });
+ }
+ });
+ }
+
+ @Override
public void getOtaStatus(int slotId, IGetOtaStatusCallback callback) {
mExecutor.execute(new Runnable() {
@Override
diff --git a/android/service/notification/NotificationListenerService.java b/android/service/notification/NotificationListenerService.java
index 18d4a1e6..b7b2b2de 100644
--- a/android/service/notification/NotificationListenerService.java
+++ b/android/service/notification/NotificationListenerService.java
@@ -55,6 +55,7 @@ import android.util.Log;
import android.widget.RemoteViews;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.os.SomeArgs;
import java.lang.annotation.Retention;
@@ -890,6 +891,8 @@ public abstract class NotificationListenerService extends Service {
createLegacyIconExtras(notification);
// populate remote views for older clients.
maybePopulateRemoteViews(notification);
+ // populate people for older clients.
+ maybePopulatePeople(notification);
} catch (IllegalArgumentException e) {
if (corruptNotifications == null) {
corruptNotifications = new ArrayList<>(N);
@@ -1178,6 +1181,25 @@ public abstract class NotificationListenerService extends Service {
}
}
+ /**
+ * Populates remote views for pre-P targeting apps.
+ */
+ private void maybePopulatePeople(Notification notification) {
+ if (getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P) {
+ ArrayList<Notification.Person> people = notification.extras.getParcelableArrayList(
+ Notification.EXTRA_PEOPLE_LIST);
+ if (people != null && people.isEmpty()) {
+ int size = people.size();
+ String[] peopleArray = new String[size];
+ for (int i = 0; i < size; i++) {
+ Notification.Person person = people.get(i);
+ peopleArray[i] = person.resolveToLegacyUri();
+ }
+ notification.extras.putStringArray(Notification.EXTRA_PEOPLE, peopleArray);
+ }
+ }
+ }
+
/** @hide */
protected class NotificationListenerWrapper extends INotificationListener.Stub {
@Override
@@ -1522,7 +1544,11 @@ public abstract class NotificationListenerService extends Service {
return mShowBadge;
}
- private void populate(String key, int rank, boolean matchesInterruptionFilter,
+ /**
+ * @hide
+ */
+ @VisibleForTesting
+ public void populate(String key, int rank, boolean matchesInterruptionFilter,
int visibilityOverride, int suppressedVisualEffects, int importance,
CharSequence explanation, String overrideGroupKey,
NotificationChannel channel, ArrayList<String> overridePeople,
diff --git a/android/service/notification/NotifyingApp.java b/android/service/notification/NotifyingApp.java
new file mode 100644
index 00000000..38f18c6f
--- /dev/null
+++ b/android/service/notification/NotifyingApp.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.service.notification;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * @hide
+ */
+public final class NotifyingApp implements Parcelable, Comparable<NotifyingApp> {
+
+ private int mUid;
+ private String mPkg;
+ private long mLastNotified;
+
+ public NotifyingApp() {}
+
+ protected NotifyingApp(Parcel in) {
+ mUid = in.readInt();
+ mPkg = in.readString();
+ mLastNotified = in.readLong();
+ }
+
+ public int getUid() {
+ return mUid;
+ }
+
+ /**
+ * Sets the uid of the package that sent the notification. Returns self.
+ */
+ public NotifyingApp setUid(int mUid) {
+ this.mUid = mUid;
+ return this;
+ }
+
+ public String getPackage() {
+ return mPkg;
+ }
+
+ /**
+ * Sets the package that sent the notification. Returns self.
+ */
+ public NotifyingApp setPackage(@NonNull String mPkg) {
+ this.mPkg = mPkg;
+ return this;
+ }
+
+ public long getLastNotified() {
+ return mLastNotified;
+ }
+
+ /**
+ * Sets the time the notification was originally sent. Returns self.
+ */
+ public NotifyingApp setLastNotified(long mLastNotified) {
+ this.mLastNotified = mLastNotified;
+ return this;
+ }
+
+ public static final Creator<NotifyingApp> CREATOR = new Creator<NotifyingApp>() {
+ @Override
+ public NotifyingApp createFromParcel(Parcel in) {
+ return new NotifyingApp(in);
+ }
+
+ @Override
+ public NotifyingApp[] newArray(int size) {
+ return new NotifyingApp[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mUid);
+ dest.writeString(mPkg);
+ dest.writeLong(mLastNotified);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ NotifyingApp that = (NotifyingApp) o;
+ return getUid() == that.getUid()
+ && getLastNotified() == that.getLastNotified()
+ && Objects.equals(mPkg, that.mPkg);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getUid(), mPkg, getLastNotified());
+ }
+
+ /**
+ * Sorts notifying apps from newest last notified date to oldest.
+ */
+ @Override
+ public int compareTo(NotifyingApp o) {
+ if (getLastNotified() == o.getLastNotified()) {
+ if (getUid() == o.getUid()) {
+ return getPackage().compareTo(o.getPackage());
+ }
+ return Integer.compare(getUid(), o.getUid());
+ }
+
+ return -Long.compare(getLastNotified(), o.getLastNotified());
+ }
+
+ @Override
+ public String toString() {
+ return "NotifyingApp{"
+ + "mUid=" + mUid
+ + ", mPkg='" + mPkg + '\''
+ + ", mLastNotified=" + mLastNotified
+ + '}';
+ }
+}
diff --git a/android/service/trust/TrustAgentService.java b/android/service/trust/TrustAgentService.java
index 4bade9f9..40e84b96 100644
--- a/android/service/trust/TrustAgentService.java
+++ b/android/service/trust/TrustAgentService.java
@@ -18,6 +18,7 @@ package android.service.trust;
import android.Manifest;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.SdkConstant;
import android.annotation.SystemApi;
import android.app.Service;
@@ -37,6 +38,7 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.util.Log;
import android.util.Slog;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.List;
@@ -301,7 +303,7 @@ public class TrustAgentService extends Service {
public void onDeviceUnlockLockout(long timeoutMs) {
}
- /**
+ /**
* Called when an escrow token is added for user userId.
*
* @param token the added token
@@ -561,6 +563,31 @@ public class TrustAgentService extends Service {
}
}
+ /**
+ * Request showing a transient error message on the keyguard.
+ * The message will be visible on the lock screen or always on display if possible but can be
+ * overridden by other keyguard events of higher priority - eg. fingerprint auth error.
+ * Other trust agents may override your message if posted simultaneously.
+ *
+ * @param message Message to show.
+ */
+ public final void showKeyguardErrorMessage(@NonNull CharSequence message) {
+ if (message == null) {
+ throw new IllegalArgumentException("message cannot be null");
+ }
+ synchronized (mLock) {
+ if (mCallback == null) {
+ Slog.w(TAG, "Cannot show message because service is not connected to framework.");
+ throw new IllegalStateException("Trust agent is not connected");
+ }
+ try {
+ mCallback.showKeyguardErrorMessage(message);
+ } catch (RemoteException e) {
+ onError("calling showKeyguardErrorMessage");
+ }
+ }
+ }
+
@Override
public final IBinder onBind(Intent intent) {
if (DEBUG) Slog.v(TAG, "onBind() intent = " + intent);
diff --git a/android/service/wallpaper/WallpaperService.java b/android/service/wallpaper/WallpaperService.java
index 595bfb7a..8588df7f 100644
--- a/android/service/wallpaper/WallpaperService.java
+++ b/android/service/wallpaper/WallpaperService.java
@@ -563,9 +563,12 @@ public abstract class WallpaperService extends Service {
* Called when the device enters or exits ambient mode.
*
* @param inAmbientMode {@code true} if in ambient mode.
+ * @param animated {@code true} if you'll have te opportunity of animating your transition
+ * {@code false} when the screen will blank and the wallpaper should be
+ * set to ambient mode immediately.
* @hide
*/
- public void onAmbientModeChanged(boolean inAmbientMode) {
+ public void onAmbientModeChanged(boolean inAmbientMode, boolean animated) {
}
/**
@@ -1021,18 +1024,20 @@ public abstract class WallpaperService extends Service {
* Executes life cycle event and updates internal ambient mode state based on
* message sent from handler.
*
- * @param inAmbientMode True if in ambient mode.
+ * @param inAmbientMode {@code true} if in ambient mode.
+ * @param animated {@code true} if the transition will be animated.
* @hide
*/
@VisibleForTesting
- public void doAmbientModeChanged(boolean inAmbientMode) {
+ public void doAmbientModeChanged(boolean inAmbientMode, boolean animated) {
if (!mDestroyed) {
if (DEBUG) {
- Log.v(TAG, "onAmbientModeChanged(" + inAmbientMode + "): " + this);
+ Log.v(TAG, "onAmbientModeChanged(" + inAmbientMode + ", "
+ + animated + "): " + this);
}
mIsInAmbientMode = inAmbientMode;
if (mCreated) {
- onAmbientModeChanged(inAmbientMode);
+ onAmbientModeChanged(inAmbientMode, animated);
}
}
}
@@ -1278,8 +1283,10 @@ public abstract class WallpaperService extends Service {
}
@Override
- public void setInAmbientMode(boolean inAmbientDisplay) throws RemoteException {
- Message msg = mCaller.obtainMessageI(DO_IN_AMBIENT_MODE, inAmbientDisplay ? 1 : 0);
+ public void setInAmbientMode(boolean inAmbientDisplay, boolean animated)
+ throws RemoteException {
+ Message msg = mCaller.obtainMessageII(DO_IN_AMBIENT_MODE, inAmbientDisplay ? 1 : 0,
+ animated ? 1 : 0);
mCaller.sendMessage(msg);
}
@@ -1350,7 +1357,7 @@ public abstract class WallpaperService extends Service {
return;
}
case DO_IN_AMBIENT_MODE: {
- mEngine.doAmbientModeChanged(message.arg1 != 0);
+ mEngine.doAmbientModeChanged(message.arg1 != 0, message.arg2 != 0);
return;
}
case MSG_UPDATE_SURFACE:
diff --git a/android/support/LibraryVersions.java b/android/support/LibraryVersions.java
index 6d8d6bf5..813d9a85 100644
--- a/android/support/LibraryVersions.java
+++ b/android/support/LibraryVersions.java
@@ -26,19 +26,14 @@ public class LibraryVersions {
public static final Version SUPPORT_LIBRARY = new Version("28.0.0-SNAPSHOT");
/**
- * Version code for flatfoot 1.0 projects (room, lifecycles)
- */
- private static final Version FLATFOOT_1_0_BATCH = new Version("1.0.0");
-
- /**
* Version code for Room
*/
- public static final Version ROOM = FLATFOOT_1_0_BATCH;
+ public static final Version ROOM = new Version("1.1.0-alpha1");
/**
* Version code for Lifecycle extensions (ProcessLifecycleOwner, Fragment support)
*/
- public static final Version LIFECYCLES_EXT = new Version("1.1.0-SNAPSHOT");
+ public static final Version LIFECYCLES_EXT = new Version("1.1.0");
/**
* Version code for Lifecycle LiveData
@@ -53,9 +48,9 @@ public class LibraryVersions {
/**
* Version code for RecyclerView & Room paging
*/
- public static final Version PAGING = new Version("1.0.0-alpha4-1");
+ public static final Version PAGING = new Version("1.0.0-alpha5");
- private static final Version LIFECYCLES = new Version("1.0.3");
+ private static final Version LIFECYCLES = new Version("1.1.0");
/**
* Version code for Lifecycle libs that are required by the support library
@@ -70,15 +65,15 @@ public class LibraryVersions {
/**
* Version code for shared code of flatfoot
*/
- public static final Version ARCH_CORE = new Version("1.0.0");
+ public static final Version ARCH_CORE = new Version("1.1.0");
/**
* Version code for shared code of flatfoot runtime
*/
- public static final Version ARCH_RUNTIME = FLATFOOT_1_0_BATCH;
+ public static final Version ARCH_RUNTIME = ARCH_CORE;
/**
* Version code for shared testing code of flatfoot
*/
- public static final Version ARCH_CORE_TESTING = FLATFOOT_1_0_BATCH;
+ public static final Version ARCH_CORE_TESTING = ARCH_CORE;
}
diff --git a/android/support/Version.java b/android/support/Version.java
deleted file mode 100644
index 36c7728b..00000000
--- a/android/support/Version.java
+++ /dev/null
@@ -1,157 +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;
-
-import java.io.File;
-import java.util.regex.Matcher;
-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) {
- 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));
- mExtra = matcher.groupCount() == 4 ? matcher.group(4) : null;
- }
-
- @Override
- public int compareTo(Version version) {
- if (mMajor != version.mMajor) {
- return mMajor - version.mMajor;
- }
- if (mMinor != version.mMinor) {
- return mMinor - version.mMinor;
- }
- if (mPatch != version.mPatch) {
- return mPatch - version.mPatch;
- }
- if (mExtra == null) {
- if (version.mExtra == null) {
- return 0;
- }
- // not having any extra is always a later version
- return 1;
- } else {
- if (version.mExtra == null) {
- // not having any extra is always a later version
- return -1;
- }
- // gradle uses lexicographic ordering
- return mExtra.compareTo(version.mExtra);
- }
- }
-
- public boolean isPatch() {
- return mPatch != 0;
- }
-
- public boolean isSnapshot() {
- return "-SNAPSHOT".equals(mExtra);
- }
-
- public int getMajor() {
- return mMajor;
- }
-
- public int getMinor() {
- return mMinor;
- }
-
- public int getPatch() {
- return mPatch;
- }
-
- public String getExtra() {
- return mExtra;
- }
-
- @Override
- public String toString() {
- return mMajor + "." + mMinor + "." + mPatch + (mExtra != null ? mExtra : "");
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) return true;
- if (o == null || getClass() != o.getClass()) return false;
-
- Version version = (Version) o;
-
- if (mMajor != version.mMajor) return false;
- if (mMinor != version.mMinor) return false;
- if (mPatch != version.mPatch) return false;
- return mExtra != null ? mExtra.equals(version.mExtra) : version.mExtra == null;
- }
-
- @Override
- public int hashCode() {
- int result = mMajor;
- result = 31 * result + mMinor;
- result = 31 * result + mPatch;
- 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/VersionFileWriterTask.java b/android/support/VersionFileWriterTask.java
deleted file mode 100644
index aafa0236..00000000
--- a/android/support/VersionFileWriterTask.java
+++ /dev/null
@@ -1,109 +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;
-
-import com.android.build.gradle.LibraryExtension;
-
-import org.gradle.api.Action;
-import org.gradle.api.DefaultTask;
-import org.gradle.api.Project;
-import org.gradle.api.tasks.Input;
-import org.gradle.api.tasks.OutputFile;
-import org.gradle.api.tasks.TaskAction;
-
-import java.io.File;
-import java.io.IOException;
-import java.io.PrintWriter;
-
-/**
- * Task that allows to write a version to a given output file.
- */
-public class VersionFileWriterTask extends DefaultTask {
- public static final String RESOURCE_DIRECTORY = "generatedResources";
- public static final String VERSION_FILE_PATH =
- RESOURCE_DIRECTORY + "/META-INF/%s_%s.version";
-
- private String mVersion;
- private File mOutputFile;
-
- /**
- * Sets up Android Library project to have a task that generates a version file.
- *
- * @param project an Android Library project.
- */
- public static void setUpAndroidLibrary(Project project) {
- project.afterEvaluate(new Action<Project>() {
- @Override
- public void execute(Project project) {
- LibraryExtension library =
- project.getExtensions().findByType(LibraryExtension.class);
-
- String group = (String) project.getProperties().get("group");
- String artifactId = (String) project.getProperties().get("name");
- String version = (String) project.getProperties().get("version");
-
- // Add a java resource file to the library jar for version tracking purposes.
- File artifactName = new File(project.getBuildDir(),
- String.format(VersionFileWriterTask.VERSION_FILE_PATH,
- group, artifactId));
-
- VersionFileWriterTask writeVersionFile =
- project.getTasks().create("writeVersionFile", VersionFileWriterTask.class);
- writeVersionFile.setVersion(version);
- writeVersionFile.setOutputFile(artifactName);
-
- library.getLibraryVariants().all(
- libraryVariant -> libraryVariant.getProcessJavaResources().dependsOn(
- writeVersionFile));
-
- library.getSourceSets().getByName("main").getResources().srcDir(
- new File(project.getBuildDir(), VersionFileWriterTask.RESOURCE_DIRECTORY)
- );
- }
- });
- }
-
- @Input
- public String getVersion() {
- return mVersion;
- }
-
- public void setVersion(String version) {
- mVersion = version;
- }
-
- @OutputFile
- public File getOutputFile() {
- return mOutputFile;
- }
-
- public void setOutputFile(File outputFile) {
- mOutputFile = outputFile;
- }
-
- /**
- * The main method for actually writing out the file.
- *
- * @throws IOException
- */
- @TaskAction
- public void run() throws IOException {
- PrintWriter writer = new PrintWriter(mOutputFile);
- writer.println(mVersion);
- writer.close();
- }
-}
diff --git a/android/support/animation/AnimationHandler.java b/android/support/animation/AnimationHandler.java
index 6c39b23a..24bc43a2 100644
--- a/android/support/animation/AnimationHandler.java
+++ b/android/support/animation/AnimationHandler.java
@@ -35,8 +35,6 @@ import java.util.ArrayList;
* The handler uses the Choreographer by default for doing periodic callbacks. A custom
* AnimationFrameCallbackProvider can be set on the handler to provide timing pulse that
* may be independent of UI frame update. This could be useful in testing.
- *
- * @hide
*/
class AnimationHandler {
/**
@@ -57,7 +55,7 @@ class AnimationHandler {
* the new frame, so that they can update animation values as needed.
*/
class AnimationCallbackDispatcher {
- public void dispatchAnimationFrame() {
+ void dispatchAnimationFrame() {
mCurrentFrameTime = SystemClock.uptimeMillis();
AnimationHandler.this.doAnimationFrame(mCurrentFrameTime);
if (mAnimationCallbacks.size() > 0) {
@@ -72,7 +70,6 @@ class AnimationHandler {
/**
* Internal per-thread collections used to avoid set collisions as animations start and end
* while being processed.
- * @hide
*/
private final SimpleArrayMap<AnimationFrameCallback, Long> mDelayedCallbackStartTime =
new SimpleArrayMap<>();
@@ -249,7 +246,7 @@ class AnimationHandler {
* timing pulse without using Choreographer. That way we could use any arbitrary interval for
* our timing pulse in the tests.
*/
- public abstract static class AnimationFrameCallbackProvider {
+ abstract static class AnimationFrameCallbackProvider {
final AnimationCallbackDispatcher mDispatcher;
AnimationFrameCallbackProvider(AnimationCallbackDispatcher dispatcher) {
mDispatcher = dispatcher;
diff --git a/android/support/animation/DynamicAnimation.java b/android/support/animation/DynamicAnimation.java
index 8ea48b94..7cbd5bb2 100644
--- a/android/support/animation/DynamicAnimation.java
+++ b/android/support/animation/DynamicAnimation.java
@@ -18,6 +18,7 @@ package android.support.animation;
import android.os.Looper;
import android.support.annotation.FloatRange;
+import android.support.annotation.RestrictTo;
import android.support.v4.view.ViewCompat;
import android.util.AndroidRuntimeException;
import android.view.View;
@@ -631,6 +632,7 @@ public abstract class DynamicAnimation<T extends DynamicAnimation<T>>
*
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
public boolean doAnimationFrame(long frameTime) {
if (mLastFrameTime == 0) {
diff --git a/android/support/animation/SpringForce.java b/android/support/animation/SpringForce.java
index 5f95aa8c..dfb4c674 100644
--- a/android/support/animation/SpringForce.java
+++ b/android/support/animation/SpringForce.java
@@ -17,6 +17,7 @@
package android.support.animation;
import android.support.annotation.FloatRange;
+import android.support.annotation.RestrictTo;
/**
* Spring Force defines the characteristics of the spring being used in the animation.
@@ -210,6 +211,7 @@ public final class SpringForce implements Force {
/**
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
public float getAcceleration(float lastDisplacement, float lastVelocity) {
@@ -224,6 +226,7 @@ public final class SpringForce implements Force {
/**
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Override
public boolean isAtEquilibrium(float value, float velocity) {
if (Math.abs(velocity) < mVelocityThreshold
diff --git a/android/support/customtabs/CustomTabsClient.java b/android/support/customtabs/CustomTabsClient.java
index 2e955cbe..371b5a1f 100644
--- a/android/support/customtabs/CustomTabsClient.java
+++ b/android/support/customtabs/CustomTabsClient.java
@@ -45,7 +45,7 @@ public class CustomTabsClient {
private final ICustomTabsService mService;
private final ComponentName mServiceComponentName;
- /**@hide*/
+ /** @hide */
@RestrictTo(LIBRARY_GROUP)
CustomTabsClient(ICustomTabsService service, ComponentName componentName) {
mService = service;
diff --git a/android/support/design/internal/BaselineLayout.java b/android/support/design/internal/BaselineLayout.java
deleted file mode 100644
index 0bfdf249..00000000
--- a/android/support/design/internal/BaselineLayout.java
+++ /dev/null
@@ -1,116 +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.support.design.internal;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * A simple ViewGroup that aligns all the views inside on a baseline. Note: bottom padding for this
- * view will be measured starting from the baseline.
- *
- * @hide
- */
-public class BaselineLayout extends ViewGroup {
- private int mBaseline = -1;
-
- public BaselineLayout(Context context) {
- super(context, null, 0);
- }
-
- public BaselineLayout(Context context, AttributeSet attrs) {
- super(context, attrs, 0);
- }
-
- public BaselineLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final int count = getChildCount();
- int maxWidth = 0;
- int maxHeight = 0;
- int maxChildBaseline = -1;
- int maxChildDescent = -1;
- int childState = 0;
-
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() == GONE) {
- continue;
- }
-
- measureChild(child, widthMeasureSpec, heightMeasureSpec);
- final int baseline = child.getBaseline();
- if (baseline != -1) {
- maxChildBaseline = Math.max(maxChildBaseline, baseline);
- maxChildDescent = Math.max(maxChildDescent, child.getMeasuredHeight() - baseline);
- }
- maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
- maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
- childState = View.combineMeasuredStates(childState, child.getMeasuredState());
- }
- if (maxChildBaseline != -1) {
- maxChildDescent = Math.max(maxChildDescent, getPaddingBottom());
- maxHeight = Math.max(maxHeight, maxChildBaseline + maxChildDescent);
- mBaseline = maxChildBaseline;
- }
- maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
- maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
- setMeasuredDimension(
- View.resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
- View.resolveSizeAndState(maxHeight, heightMeasureSpec,
- childState << MEASURED_HEIGHT_STATE_SHIFT));
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- final int count = getChildCount();
- final int parentLeft = getPaddingLeft();
- final int parentRight = right - left - getPaddingRight();
- final int parentContentWidth = parentRight - parentLeft;
- final int parentTop = getPaddingTop();
-
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() == GONE) {
- continue;
- }
-
- final int width = child.getMeasuredWidth();
- final int height = child.getMeasuredHeight();
-
- final int childLeft = parentLeft + (parentContentWidth - width) / 2;
- final int childTop;
- if (mBaseline != -1 && child.getBaseline() != -1) {
- childTop = parentTop + mBaseline - child.getBaseline();
- } else {
- childTop = parentTop;
- }
-
- child.layout(childLeft, childTop, childLeft + width, childTop + height);
- }
- }
-
- @Override
- public int getBaseline() {
- return mBaseline;
- }
-}
diff --git a/android/support/design/internal/BottomNavigationItemView.java b/android/support/design/internal/BottomNavigationItemView.java
deleted file mode 100644
index fe5e636f..00000000
--- a/android/support/design/internal/BottomNavigationItemView.java
+++ /dev/null
@@ -1,258 +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.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v4.view.PointerIconCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.MenuView;
-import android.support.v7.widget.TooltipCompat;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.widget.FrameLayout;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class BottomNavigationItemView extends FrameLayout implements MenuView.ItemView {
- public static final int INVALID_ITEM_POSITION = -1;
-
- private static final int[] CHECKED_STATE_SET = { android.R.attr.state_checked };
-
- private final int mDefaultMargin;
- private final int mShiftAmount;
- private final float mScaleUpFactor;
- private final float mScaleDownFactor;
-
- private boolean mShiftingMode;
-
- private ImageView mIcon;
- private final TextView mSmallLabel;
- private final TextView mLargeLabel;
- private int mItemPosition = INVALID_ITEM_POSITION;
-
- private MenuItemImpl mItemData;
-
- private ColorStateList mIconTint;
-
- public BottomNavigationItemView(@NonNull Context context) {
- this(context, null);
- }
-
- public BottomNavigationItemView(@NonNull Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public BottomNavigationItemView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- final Resources res = getResources();
- int inactiveLabelSize =
- res.getDimensionPixelSize(R.dimen.design_bottom_navigation_text_size);
- int activeLabelSize = res.getDimensionPixelSize(
- R.dimen.design_bottom_navigation_active_text_size);
- mDefaultMargin = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_margin);
- mShiftAmount = inactiveLabelSize - activeLabelSize;
- mScaleUpFactor = 1f * activeLabelSize / inactiveLabelSize;
- mScaleDownFactor = 1f * inactiveLabelSize / activeLabelSize;
-
- LayoutInflater.from(context).inflate(R.layout.design_bottom_navigation_item, this, true);
- setBackgroundResource(R.drawable.design_bottom_navigation_item_background);
- mIcon = findViewById(R.id.icon);
- mSmallLabel = findViewById(R.id.smallLabel);
- mLargeLabel = findViewById(R.id.largeLabel);
- }
-
- @Override
- public void initialize(MenuItemImpl itemData, int menuType) {
- mItemData = itemData;
- setCheckable(itemData.isCheckable());
- setChecked(itemData.isChecked());
- setEnabled(itemData.isEnabled());
- setIcon(itemData.getIcon());
- setTitle(itemData.getTitle());
- setId(itemData.getItemId());
- setContentDescription(itemData.getContentDescription());
- TooltipCompat.setTooltipText(this, itemData.getTooltipText());
- }
-
- public void setItemPosition(int position) {
- mItemPosition = position;
- }
-
- public int getItemPosition() {
- return mItemPosition;
- }
-
- public void setShiftingMode(boolean enabled) {
- mShiftingMode = enabled;
- }
-
- @Override
- public MenuItemImpl getItemData() {
- return mItemData;
- }
-
- @Override
- public void setTitle(CharSequence title) {
- mSmallLabel.setText(title);
- mLargeLabel.setText(title);
- }
-
- @Override
- public void setCheckable(boolean checkable) {
- refreshDrawableState();
- }
-
- @Override
- public void setChecked(boolean checked) {
- mLargeLabel.setPivotX(mLargeLabel.getWidth() / 2);
- mLargeLabel.setPivotY(mLargeLabel.getBaseline());
- mSmallLabel.setPivotX(mSmallLabel.getWidth() / 2);
- mSmallLabel.setPivotY(mSmallLabel.getBaseline());
- if (mShiftingMode) {
- if (checked) {
- LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
- iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
- iconParams.topMargin = mDefaultMargin;
- mIcon.setLayoutParams(iconParams);
- mLargeLabel.setVisibility(VISIBLE);
- mLargeLabel.setScaleX(1f);
- mLargeLabel.setScaleY(1f);
- } else {
- LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
- iconParams.gravity = Gravity.CENTER;
- iconParams.topMargin = mDefaultMargin;
- mIcon.setLayoutParams(iconParams);
- mLargeLabel.setVisibility(INVISIBLE);
- mLargeLabel.setScaleX(0.5f);
- mLargeLabel.setScaleY(0.5f);
- }
- mSmallLabel.setVisibility(INVISIBLE);
- } else {
- if (checked) {
- LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
- iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
- iconParams.topMargin = mDefaultMargin + mShiftAmount;
- mIcon.setLayoutParams(iconParams);
- mLargeLabel.setVisibility(VISIBLE);
- mSmallLabel.setVisibility(INVISIBLE);
-
- mLargeLabel.setScaleX(1f);
- mLargeLabel.setScaleY(1f);
- mSmallLabel.setScaleX(mScaleUpFactor);
- mSmallLabel.setScaleY(mScaleUpFactor);
- } else {
- LayoutParams iconParams = (LayoutParams) mIcon.getLayoutParams();
- iconParams.gravity = Gravity.CENTER_HORIZONTAL | Gravity.TOP;
- iconParams.topMargin = mDefaultMargin;
- mIcon.setLayoutParams(iconParams);
- mLargeLabel.setVisibility(INVISIBLE);
- mSmallLabel.setVisibility(VISIBLE);
-
- mLargeLabel.setScaleX(mScaleDownFactor);
- mLargeLabel.setScaleY(mScaleDownFactor);
- mSmallLabel.setScaleX(1f);
- mSmallLabel.setScaleY(1f);
- }
- }
-
- refreshDrawableState();
- }
-
- @Override
- public void setEnabled(boolean enabled) {
- super.setEnabled(enabled);
- mSmallLabel.setEnabled(enabled);
- mLargeLabel.setEnabled(enabled);
- mIcon.setEnabled(enabled);
-
- if (enabled) {
- ViewCompat.setPointerIcon(this,
- PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND));
- } else {
- ViewCompat.setPointerIcon(this, null);
- }
-
- }
-
- @Override
- public int[] onCreateDrawableState(final int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
- if (mItemData != null && mItemData.isCheckable() && mItemData.isChecked()) {
- mergeDrawableStates(drawableState, CHECKED_STATE_SET);
- }
- return drawableState;
- }
-
- @Override
- public void setShortcut(boolean showShortcut, char shortcutKey) {
- }
-
- @Override
- public void setIcon(Drawable icon) {
- if (icon != null) {
- Drawable.ConstantState state = icon.getConstantState();
- icon = DrawableCompat.wrap(state == null ? icon : state.newDrawable()).mutate();
- DrawableCompat.setTintList(icon, mIconTint);
- }
- mIcon.setImageDrawable(icon);
- }
-
- @Override
- public boolean prefersCondensedTitle() {
- return false;
- }
-
- @Override
- public boolean showsIcon() {
- return true;
- }
-
- public void setIconTintList(ColorStateList tint) {
- mIconTint = tint;
- if (mItemData != null) {
- // Update the icon so that the tint takes effect
- setIcon(mItemData.getIcon());
- }
- }
-
- public void setTextColor(ColorStateList color) {
- mSmallLabel.setTextColor(color);
- mLargeLabel.setTextColor(color);
- }
-
- public void setItemBackground(int background) {
- Drawable backgroundDrawable = background == 0
- ? null : ContextCompat.getDrawable(getContext(), background);
- ViewCompat.setBackground(this, backgroundDrawable);
- }
-}
diff --git a/android/support/design/internal/BottomNavigationMenu.java b/android/support/design/internal/BottomNavigationMenu.java
deleted file mode 100644
index a86d2adf..00000000
--- a/android/support/design/internal/BottomNavigationMenu.java
+++ /dev/null
@@ -1,59 +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.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.support.annotation.RestrictTo;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.view.MenuItem;
-import android.view.SubMenu;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public final class BottomNavigationMenu extends MenuBuilder {
- public static final int MAX_ITEM_COUNT = 5;
-
- public BottomNavigationMenu(Context context) {
- super(context);
- }
-
- @Override
- public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) {
- throw new UnsupportedOperationException("BottomNavigationView does not support submenus");
- }
-
- @Override
- protected MenuItem addInternal(int group, int id, int categoryOrder, CharSequence title) {
- if (size() + 1 > MAX_ITEM_COUNT) {
- throw new IllegalArgumentException(
- "Maximum number of items supported by BottomNavigationView is " + MAX_ITEM_COUNT
- + ". Limit can be checked with BottomNavigationView#getMaxItemCount()");
- }
- stopDispatchingItemsChanged();
- final MenuItem item = super.addInternal(group, id, categoryOrder, title);
- if (item instanceof MenuItemImpl) {
- ((MenuItemImpl) item).setExclusiveCheckable(true);
- }
- startDispatchingItemsChanged();
- return item;
- }
-}
diff --git a/android/support/design/internal/BottomNavigationMenuView.java b/android/support/design/internal/BottomNavigationMenuView.java
deleted file mode 100644
index bf33454e..00000000
--- a/android/support/design/internal/BottomNavigationMenuView.java
+++ /dev/null
@@ -1,343 +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.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.transition.AutoTransition;
-import android.support.transition.TransitionManager;
-import android.support.transition.TransitionSet;
-import android.support.v4.util.Pools;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.animation.FastOutSlowInInterpolator;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.MenuView;
-import android.util.AttributeSet;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-
-/**
- * @hide For internal use only.
- */
-@RestrictTo(LIBRARY_GROUP)
-public class BottomNavigationMenuView extends ViewGroup implements MenuView {
- private static final long ACTIVE_ANIMATION_DURATION_MS = 115L;
-
- private final TransitionSet mSet;
- private final int mInactiveItemMaxWidth;
- private final int mInactiveItemMinWidth;
- private final int mActiveItemMaxWidth;
- private final int mItemHeight;
- private final OnClickListener mOnClickListener;
- private final Pools.Pool<BottomNavigationItemView> mItemPool = new Pools.SynchronizedPool<>(5);
-
- private boolean mShiftingMode = true;
-
- private BottomNavigationItemView[] mButtons;
- private int mSelectedItemId = 0;
- private int mSelectedItemPosition = 0;
- private ColorStateList mItemIconTint;
- private ColorStateList mItemTextColor;
- private int mItemBackgroundRes;
- private int[] mTempChildWidths;
-
- private BottomNavigationPresenter mPresenter;
- private MenuBuilder mMenu;
-
- public BottomNavigationMenuView(Context context) {
- this(context, null);
- }
-
- public BottomNavigationMenuView(Context context, AttributeSet attrs) {
- super(context, attrs);
- final Resources res = getResources();
- mInactiveItemMaxWidth = res.getDimensionPixelSize(
- R.dimen.design_bottom_navigation_item_max_width);
- mInactiveItemMinWidth = res.getDimensionPixelSize(
- R.dimen.design_bottom_navigation_item_min_width);
- mActiveItemMaxWidth = res.getDimensionPixelSize(
- R.dimen.design_bottom_navigation_active_item_max_width);
- mItemHeight = res.getDimensionPixelSize(R.dimen.design_bottom_navigation_height);
-
- mSet = new AutoTransition();
- mSet.setOrdering(TransitionSet.ORDERING_TOGETHER);
- mSet.setDuration(ACTIVE_ANIMATION_DURATION_MS);
- mSet.setInterpolator(new FastOutSlowInInterpolator());
- mSet.addTransition(new TextScale());
-
- mOnClickListener = new OnClickListener() {
- @Override
- public void onClick(View v) {
- final BottomNavigationItemView itemView = (BottomNavigationItemView) v;
- MenuItem item = itemView.getItemData();
- if (!mMenu.performItemAction(item, mPresenter, 0)) {
- item.setChecked(true);
- }
- }
- };
- mTempChildWidths = new int[BottomNavigationMenu.MAX_ITEM_COUNT];
- }
-
- @Override
- public void initialize(MenuBuilder menu) {
- mMenu = menu;
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final int width = MeasureSpec.getSize(widthMeasureSpec);
- final int count = getChildCount();
-
- final int heightSpec = MeasureSpec.makeMeasureSpec(mItemHeight, MeasureSpec.EXACTLY);
-
- if (mShiftingMode) {
- final int inactiveCount = count - 1;
- final int activeMaxAvailable = width - inactiveCount * mInactiveItemMinWidth;
- final int activeWidth = Math.min(activeMaxAvailable, mActiveItemMaxWidth);
- final int inactiveMaxAvailable = (width - activeWidth) / inactiveCount;
- final int inactiveWidth = Math.min(inactiveMaxAvailable, mInactiveItemMaxWidth);
- int extra = width - activeWidth - inactiveWidth * inactiveCount;
- for (int i = 0; i < count; i++) {
- mTempChildWidths[i] = (i == mSelectedItemPosition) ? activeWidth : inactiveWidth;
- if (extra > 0) {
- mTempChildWidths[i]++;
- extra--;
- }
- }
- } else {
- final int maxAvailable = width / (count == 0 ? 1 : count);
- final int childWidth = Math.min(maxAvailable, mActiveItemMaxWidth);
- int extra = width - childWidth * count;
- for (int i = 0; i < count; i++) {
- mTempChildWidths[i] = childWidth;
- if (extra > 0) {
- mTempChildWidths[i]++;
- extra--;
- }
- }
- }
-
- int totalWidth = 0;
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() == GONE) {
- continue;
- }
- child.measure(MeasureSpec.makeMeasureSpec(mTempChildWidths[i], MeasureSpec.EXACTLY),
- heightSpec);
- ViewGroup.LayoutParams params = child.getLayoutParams();
- params.width = child.getMeasuredWidth();
- totalWidth += child.getMeasuredWidth();
- }
- setMeasuredDimension(
- View.resolveSizeAndState(totalWidth,
- MeasureSpec.makeMeasureSpec(totalWidth, MeasureSpec.EXACTLY), 0),
- View.resolveSizeAndState(mItemHeight, heightSpec, 0));
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- final int count = getChildCount();
- final int width = right - left;
- final int height = bottom - top;
- int used = 0;
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- if (child.getVisibility() == GONE) {
- continue;
- }
- if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL) {
- child.layout(width - used - child.getMeasuredWidth(), 0, width - used, height);
- } else {
- child.layout(used, 0, child.getMeasuredWidth() + used, height);
- }
- used += child.getMeasuredWidth();
- }
- }
-
- @Override
- public int getWindowAnimations() {
- return 0;
- }
-
- /**
- * Sets the tint which is applied to the menu items' icons.
- *
- * @param tint the tint to apply
- */
- public void setIconTintList(ColorStateList tint) {
- mItemIconTint = tint;
- if (mButtons == null) return;
- for (BottomNavigationItemView item : mButtons) {
- item.setIconTintList(tint);
- }
- }
-
- /**
- * Returns the tint which is applied to menu items' icons.
- *
- * @return the ColorStateList that is used to tint menu items' icons
- */
- @Nullable
- public ColorStateList getIconTintList() {
- return mItemIconTint;
- }
-
- /**
- * Sets the text color to be used on menu items.
- *
- * @param color the ColorStateList used for menu items' text.
- */
- public void setItemTextColor(ColorStateList color) {
- mItemTextColor = color;
- if (mButtons == null) return;
- for (BottomNavigationItemView item : mButtons) {
- item.setTextColor(color);
- }
- }
-
- /**
- * Returns the text color used on menu items.
- *
- * @return the ColorStateList used for menu items' text
- */
- public ColorStateList getItemTextColor() {
- return mItemTextColor;
- }
-
- /**
- * Sets the resource ID to be used for item background.
- *
- * @param background the resource ID of the background
- */
- public void setItemBackgroundRes(int background) {
- mItemBackgroundRes = background;
- if (mButtons == null) return;
- for (BottomNavigationItemView item : mButtons) {
- item.setItemBackground(background);
- }
- }
-
- /**
- * Returns the resource ID for the background of the menu items.
- *
- * @return the resource ID for the background
- */
- public int getItemBackgroundRes() {
- return mItemBackgroundRes;
- }
-
- public void setPresenter(BottomNavigationPresenter presenter) {
- mPresenter = presenter;
- }
-
- public void buildMenuView() {
- removeAllViews();
- if (mButtons != null) {
- for (BottomNavigationItemView item : mButtons) {
- mItemPool.release(item);
- }
- }
- if (mMenu.size() == 0) {
- mSelectedItemId = 0;
- mSelectedItemPosition = 0;
- mButtons = null;
- return;
- }
- mButtons = new BottomNavigationItemView[mMenu.size()];
- mShiftingMode = mMenu.size() > 3;
- for (int i = 0; i < mMenu.size(); i++) {
- mPresenter.setUpdateSuspended(true);
- mMenu.getItem(i).setCheckable(true);
- mPresenter.setUpdateSuspended(false);
- BottomNavigationItemView child = getNewItem();
- mButtons[i] = child;
- child.setIconTintList(mItemIconTint);
- child.setTextColor(mItemTextColor);
- child.setItemBackground(mItemBackgroundRes);
- child.setShiftingMode(mShiftingMode);
- child.initialize((MenuItemImpl) mMenu.getItem(i), 0);
- child.setItemPosition(i);
- child.setOnClickListener(mOnClickListener);
- addView(child);
- }
- mSelectedItemPosition = Math.min(mMenu.size() - 1, mSelectedItemPosition);
- mMenu.getItem(mSelectedItemPosition).setChecked(true);
- }
-
- public void updateMenuView() {
- final int menuSize = mMenu.size();
- if (menuSize != mButtons.length) {
- // The size has changed. Rebuild menu view from scratch.
- buildMenuView();
- return;
- }
- int previousSelectedId = mSelectedItemId;
-
- for (int i = 0; i < menuSize; i++) {
- MenuItem item = mMenu.getItem(i);
- if (item.isChecked()) {
- mSelectedItemId = item.getItemId();
- mSelectedItemPosition = i;
- }
- }
- if (previousSelectedId != mSelectedItemId) {
- // Note: this has to be called before BottomNavigationItemView#initialize().
- TransitionManager.beginDelayedTransition(this, mSet);
- }
-
- for (int i = 0; i < menuSize; i++) {
- mPresenter.setUpdateSuspended(true);
- mButtons[i].initialize((MenuItemImpl) mMenu.getItem(i), 0);
- mPresenter.setUpdateSuspended(false);
- }
-
- }
-
- private BottomNavigationItemView getNewItem() {
- BottomNavigationItemView item = mItemPool.acquire();
- if (item == null) {
- item = new BottomNavigationItemView(getContext());
- }
- return item;
- }
-
- public int getSelectedItemId() {
- return mSelectedItemId;
- }
-
- void tryRestoreSelectedItemId(int itemId) {
- final int size = mMenu.size();
- for (int i = 0; i < size; i++) {
- MenuItem item = mMenu.getItem(i);
- if (itemId == item.getItemId()) {
- mSelectedItemId = itemId;
- mSelectedItemPosition = i;
- item.setChecked(true);
- break;
- }
- }
- }
-}
diff --git a/android/support/design/internal/BottomNavigationPresenter.java b/android/support/design/internal/BottomNavigationPresenter.java
deleted file mode 100644
index 1343a4bf..00000000
--- a/android/support/design/internal/BottomNavigationPresenter.java
+++ /dev/null
@@ -1,152 +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.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.MenuPresenter;
-import android.support.v7.view.menu.MenuView;
-import android.support.v7.view.menu.SubMenuBuilder;
-import android.view.ViewGroup;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class BottomNavigationPresenter implements MenuPresenter {
- private MenuBuilder mMenu;
- private BottomNavigationMenuView mMenuView;
- private boolean mUpdateSuspended = false;
- private int mId;
-
- public void setBottomNavigationMenuView(BottomNavigationMenuView menuView) {
- mMenuView = menuView;
- }
-
- @Override
- public void initForMenu(Context context, MenuBuilder menu) {
- mMenuView.initialize(mMenu);
- mMenu = menu;
- }
-
- @Override
- public MenuView getMenuView(ViewGroup root) {
- return mMenuView;
- }
-
- @Override
- public void updateMenuView(boolean cleared) {
- if (mUpdateSuspended) return;
- if (cleared) {
- mMenuView.buildMenuView();
- } else {
- mMenuView.updateMenuView();
- }
- }
-
- @Override
- public void setCallback(Callback cb) {}
-
- @Override
- public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
- return false;
- }
-
- @Override
- public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {}
-
- @Override
- public boolean flagActionItems() {
- return false;
- }
-
- @Override
- public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
- return false;
- }
-
- @Override
- public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
- return false;
- }
-
- public void setId(int id) {
- mId = id;
- }
-
- @Override
- public int getId() {
- return mId;
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- SavedState savedState = new SavedState();
- savedState.selectedItemId = mMenuView.getSelectedItemId();
- return savedState;
- }
-
- @Override
- public void onRestoreInstanceState(Parcelable state) {
- if (state instanceof SavedState) {
- mMenuView.tryRestoreSelectedItemId(((SavedState) state).selectedItemId);
- }
- }
-
- public void setUpdateSuspended(boolean updateSuspended) {
- mUpdateSuspended = updateSuspended;
- }
-
- static class SavedState implements Parcelable {
- int selectedItemId;
-
- SavedState() {}
-
- SavedState(Parcel in) {
- selectedItemId = in.readInt();
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel out, int flags) {
- out.writeInt(selectedItemId);
- }
-
- public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-}
diff --git a/android/support/design/internal/ForegroundLinearLayout.java b/android/support/design/internal/ForegroundLinearLayout.java
deleted file mode 100644
index 6d905038..00000000
--- a/android/support/design/internal/ForegroundLinearLayout.java
+++ /dev/null
@@ -1,240 +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 android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.NonNull;
-import android.support.annotation.RequiresApi;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.v7.widget.LinearLayoutCompat;
-import android.util.AttributeSet;
-import android.view.Gravity;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class ForegroundLinearLayout extends LinearLayoutCompat {
-
- private Drawable mForeground;
-
- private final Rect mSelfBounds = new Rect();
-
- private final Rect mOverlayBounds = new Rect();
-
- private int mForegroundGravity = Gravity.FILL;
-
- protected boolean mForegroundInPadding = true;
-
- boolean mForegroundBoundsChanged = false;
-
- public ForegroundLinearLayout(Context context) {
- this(context, null);
- }
-
- public ForegroundLinearLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public ForegroundLinearLayout(Context context, AttributeSet attrs, int defStyle) {
- super(context, attrs, defStyle);
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ForegroundLinearLayout,
- defStyle, 0);
-
- mForegroundGravity = a.getInt(
- R.styleable.ForegroundLinearLayout_android_foregroundGravity, mForegroundGravity);
-
- final Drawable d = a.getDrawable(R.styleable.ForegroundLinearLayout_android_foreground);
- if (d != null) {
- setForeground(d);
- }
-
- mForegroundInPadding = a.getBoolean(
- R.styleable.ForegroundLinearLayout_foregroundInsidePadding, true);
-
- a.recycle();
- }
-
- /**
- * Describes how the foreground is positioned.
- *
- * @return foreground gravity.
- * @see #setForegroundGravity(int)
- */
- @Override
- public int getForegroundGravity() {
- return mForegroundGravity;
- }
-
- /**
- * Describes how the foreground is positioned. Defaults to START and TOP.
- *
- * @param foregroundGravity See {@link android.view.Gravity}
- * @see #getForegroundGravity()
- */
- @Override
- public void setForegroundGravity(int foregroundGravity) {
- if (mForegroundGravity != foregroundGravity) {
- if ((foregroundGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
- foregroundGravity |= Gravity.START;
- }
-
- if ((foregroundGravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
- foregroundGravity |= Gravity.TOP;
- }
-
- mForegroundGravity = foregroundGravity;
-
- if (mForegroundGravity == Gravity.FILL && mForeground != null) {
- Rect padding = new Rect();
- mForeground.getPadding(padding);
- }
-
- requestLayout();
- }
- }
-
- @Override
- protected boolean verifyDrawable(Drawable who) {
- return super.verifyDrawable(who) || (who == mForeground);
- }
-
- @RequiresApi(11)
- @Override
- public void jumpDrawablesToCurrentState() {
- super.jumpDrawablesToCurrentState();
- if (mForeground != null) {
- mForeground.jumpToCurrentState();
- }
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
- if (mForeground != null && mForeground.isStateful()) {
- mForeground.setState(getDrawableState());
- }
- }
-
- /**
- * Supply a Drawable that is to be rendered on top of all of the child
- * views in the frame layout. Any padding in the Drawable will be taken
- * into account by ensuring that the children are inset to be placed
- * inside of the padding area.
- *
- * @param drawable The Drawable to be drawn on top of the children.
- */
- @Override
- public void setForeground(Drawable drawable) {
- if (mForeground != drawable) {
- if (mForeground != null) {
- mForeground.setCallback(null);
- unscheduleDrawable(mForeground);
- }
-
- mForeground = drawable;
-
- if (drawable != null) {
- setWillNotDraw(false);
- drawable.setCallback(this);
- if (drawable.isStateful()) {
- drawable.setState(getDrawableState());
- }
- if (mForegroundGravity == Gravity.FILL) {
- Rect padding = new Rect();
- drawable.getPadding(padding);
- }
- } else {
- setWillNotDraw(true);
- }
- requestLayout();
- invalidate();
- }
- }
-
- /**
- * Returns the drawable used as the foreground of this FrameLayout. The
- * foreground drawable, if non-null, is always drawn on top of the children.
- *
- * @return A Drawable or null if no foreground was set.
- */
- @Override
- public Drawable getForeground() {
- return mForeground;
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
- mForegroundBoundsChanged |= changed;
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- mForegroundBoundsChanged = true;
- }
-
- @Override
- public void draw(@NonNull Canvas canvas) {
- super.draw(canvas);
-
- if (mForeground != null) {
- final Drawable foreground = mForeground;
-
- if (mForegroundBoundsChanged) {
- mForegroundBoundsChanged = false;
- final Rect selfBounds = mSelfBounds;
- final Rect overlayBounds = mOverlayBounds;
-
- final int w = getRight() - getLeft();
- final int h = getBottom() - getTop();
-
- if (mForegroundInPadding) {
- selfBounds.set(0, 0, w, h);
- } else {
- selfBounds.set(getPaddingLeft(), getPaddingTop(),
- w - getPaddingRight(), h - getPaddingBottom());
- }
-
- Gravity.apply(mForegroundGravity, foreground.getIntrinsicWidth(),
- foreground.getIntrinsicHeight(), selfBounds, overlayBounds);
- foreground.setBounds(overlayBounds);
- }
-
- foreground.draw(canvas);
- }
- }
-
- @RequiresApi(21)
- @Override
- public void drawableHotspotChanged(float x, float y) {
- super.drawableHotspotChanged(x, y);
- if (mForeground != null) {
- mForeground.setHotspot(x, y);
- }
- }
-
-}
diff --git a/android/support/design/internal/NavigationMenu.java b/android/support/design/internal/NavigationMenu.java
deleted file mode 100644
index a0ec5e0d..00000000
--- a/android/support/design/internal/NavigationMenu.java
+++ /dev/null
@@ -1,49 +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 android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.support.annotation.RestrictTo;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.SubMenuBuilder;
-import android.view.SubMenu;
-
-/**
- * This is a {@link MenuBuilder} that returns an instance of {@link NavigationSubMenu} instead of
- * {@link SubMenuBuilder} when a sub menu is created.
- *
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class NavigationMenu extends MenuBuilder {
-
- public NavigationMenu(Context context) {
- super(context);
- }
-
- @Override
- public SubMenu addSubMenu(int group, int id, int categoryOrder, CharSequence title) {
- final MenuItemImpl item = (MenuItemImpl) addInternal(group, id, categoryOrder, title);
- final SubMenuBuilder subMenu = new NavigationSubMenu(getContext(), this, item);
- item.setSubMenu(subMenu);
- return subMenu;
- }
-
-}
diff --git a/android/support/design/internal/NavigationMenuItemView.java b/android/support/design/internal/NavigationMenuItemView.java
deleted file mode 100644
index eea9e90f..00000000
--- a/android/support/design/internal/NavigationMenuItemView.java
+++ /dev/null
@@ -1,272 +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 android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.StateListDrawable;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.v4.content.res.ResourcesCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v4.view.AccessibilityDelegateCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.accessibility.AccessibilityEventCompat;
-import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
-import android.support.v4.widget.TextViewCompat;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.MenuView;
-import android.support.v7.widget.TooltipCompat;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewStub;
-import android.widget.CheckedTextView;
-import android.widget.FrameLayout;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class NavigationMenuItemView extends ForegroundLinearLayout implements MenuView.ItemView {
-
- private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
-
- private final int mIconSize;
-
- private boolean mNeedsEmptyIcon;
-
- boolean mCheckable;
-
- private final CheckedTextView mTextView;
-
- private FrameLayout mActionArea;
-
- private MenuItemImpl mItemData;
-
- private ColorStateList mIconTintList;
-
- private boolean mHasIconTintList;
-
- private Drawable mEmptyDrawable;
-
- private final AccessibilityDelegateCompat mAccessibilityDelegate
- = new AccessibilityDelegateCompat() {
-
- @Override
- public void onInitializeAccessibilityNodeInfo(View host,
- AccessibilityNodeInfoCompat info) {
- super.onInitializeAccessibilityNodeInfo(host, info);
- info.setCheckable(mCheckable);
- }
-
- };
-
- public NavigationMenuItemView(Context context) {
- this(context, null);
- }
-
- public NavigationMenuItemView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public NavigationMenuItemView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- setOrientation(HORIZONTAL);
- LayoutInflater.from(context).inflate(R.layout.design_navigation_menu_item, this, true);
- mIconSize = context.getResources().getDimensionPixelSize(
- R.dimen.design_navigation_icon_size);
- mTextView = findViewById(R.id.design_menu_item_text);
- mTextView.setDuplicateParentStateEnabled(true);
- ViewCompat.setAccessibilityDelegate(mTextView, mAccessibilityDelegate);
- }
-
- @Override
- public void initialize(MenuItemImpl itemData, int menuType) {
- mItemData = itemData;
-
- setVisibility(itemData.isVisible() ? VISIBLE : GONE);
-
- if (getBackground() == null) {
- ViewCompat.setBackground(this, createDefaultBackground());
- }
-
- setCheckable(itemData.isCheckable());
- setChecked(itemData.isChecked());
- setEnabled(itemData.isEnabled());
- setTitle(itemData.getTitle());
- setIcon(itemData.getIcon());
- setActionView(itemData.getActionView());
- setContentDescription(itemData.getContentDescription());
- TooltipCompat.setTooltipText(this, itemData.getTooltipText());
- adjustAppearance();
- }
-
- private boolean shouldExpandActionArea() {
- return mItemData.getTitle() == null &&
- mItemData.getIcon() == null &&
- mItemData.getActionView() != null;
- }
-
- private void adjustAppearance() {
- if (shouldExpandActionArea()) {
- // Expand the actionView area
- mTextView.setVisibility(View.GONE);
- if (mActionArea != null) {
- LayoutParams params = (LayoutParams) mActionArea.getLayoutParams();
- params.width = LayoutParams.MATCH_PARENT;
- mActionArea.setLayoutParams(params);
- }
- } else {
- mTextView.setVisibility(View.VISIBLE);
- if (mActionArea != null) {
- LayoutParams params = (LayoutParams) mActionArea.getLayoutParams();
- params.width = LayoutParams.WRAP_CONTENT;
- mActionArea.setLayoutParams(params);
- }
- }
- }
-
- public void recycle() {
- if (mActionArea != null) {
- mActionArea.removeAllViews();
- }
- mTextView.setCompoundDrawables(null, null, null, null);
- }
-
- private void setActionView(View actionView) {
- if (actionView != null) {
- if (mActionArea == null) {
- mActionArea = (FrameLayout) ((ViewStub) findViewById(
- R.id.design_menu_item_action_area_stub)).inflate();
- }
- mActionArea.removeAllViews();
- mActionArea.addView(actionView);
- }
- }
-
- private StateListDrawable createDefaultBackground() {
- TypedValue value = new TypedValue();
- if (getContext().getTheme().resolveAttribute(
- android.support.v7.appcompat.R.attr.colorControlHighlight, value, true)) {
- StateListDrawable drawable = new StateListDrawable();
- drawable.addState(CHECKED_STATE_SET, new ColorDrawable(value.data));
- drawable.addState(EMPTY_STATE_SET, new ColorDrawable(Color.TRANSPARENT));
- return drawable;
- }
- return null;
- }
-
- @Override
- public MenuItemImpl getItemData() {
- return mItemData;
- }
-
- @Override
- public void setTitle(CharSequence title) {
- mTextView.setText(title);
- }
-
- @Override
- public void setCheckable(boolean checkable) {
- refreshDrawableState();
- if (mCheckable != checkable) {
- mCheckable = checkable;
- mAccessibilityDelegate.sendAccessibilityEvent(mTextView,
- AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
- }
- }
-
- @Override
- public void setChecked(boolean checked) {
- refreshDrawableState();
- mTextView.setChecked(checked);
- }
-
- @Override
- public void setShortcut(boolean showShortcut, char shortcutKey) {
- }
-
- @Override
- public void setIcon(Drawable icon) {
- if (icon != null) {
- if (mHasIconTintList) {
- Drawable.ConstantState state = icon.getConstantState();
- icon = DrawableCompat.wrap(state == null ? icon : state.newDrawable()).mutate();
- DrawableCompat.setTintList(icon, mIconTintList);
- }
- icon.setBounds(0, 0, mIconSize, mIconSize);
- } else if (mNeedsEmptyIcon) {
- if (mEmptyDrawable == null) {
- mEmptyDrawable = ResourcesCompat.getDrawable(getResources(),
- R.drawable.navigation_empty_icon, getContext().getTheme());
- if (mEmptyDrawable != null) {
- mEmptyDrawable.setBounds(0, 0, mIconSize, mIconSize);
- }
- }
- icon = mEmptyDrawable;
- }
- TextViewCompat.setCompoundDrawablesRelative(mTextView, icon, null, null, null);
- }
-
- @Override
- public boolean prefersCondensedTitle() {
- return false;
- }
-
- @Override
- public boolean showsIcon() {
- return true;
- }
-
- @Override
- protected int[] onCreateDrawableState(int extraSpace) {
- final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
- if (mItemData != null && mItemData.isCheckable() && mItemData.isChecked()) {
- mergeDrawableStates(drawableState, CHECKED_STATE_SET);
- }
- return drawableState;
- }
-
- void setIconTintList(ColorStateList tintList) {
- mIconTintList = tintList;
- mHasIconTintList = mIconTintList != null;
- if (mItemData != null) {
- // Update the icon so that the tint takes effect
- setIcon(mItemData.getIcon());
- }
- }
-
- public void setTextAppearance(int textAppearance) {
- TextViewCompat.setTextAppearance(mTextView, textAppearance);
- }
-
- public void setTextColor(ColorStateList colors) {
- mTextView.setTextColor(colors);
- }
-
- public void setNeedsEmptyIcon(boolean needsEmptyIcon) {
- mNeedsEmptyIcon = needsEmptyIcon;
- }
-
-}
diff --git a/android/support/design/internal/NavigationMenuPresenter.java b/android/support/design/internal/NavigationMenuPresenter.java
deleted file mode 100644
index 98ad4688..00000000
--- a/android/support/design/internal/NavigationMenuPresenter.java
+++ /dev/null
@@ -1,686 +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 android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcelable;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.StyleRes;
-import android.support.design.R;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.MenuPresenter;
-import android.support.v7.view.menu.MenuView;
-import android.support.v7.view.menu.SubMenuBuilder;
-import android.support.v7.widget.RecyclerView;
-import android.util.SparseArray;
-import android.view.LayoutInflater;
-import android.view.SubMenu;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class NavigationMenuPresenter implements MenuPresenter {
-
- private static final String STATE_HIERARCHY = "android:menu:list";
- private static final String STATE_ADAPTER = "android:menu:adapter";
- private static final String STATE_HEADER = "android:menu:header";
-
- private NavigationMenuView mMenuView;
- LinearLayout mHeaderLayout;
-
- private Callback mCallback;
- MenuBuilder mMenu;
- private int mId;
-
- NavigationMenuAdapter mAdapter;
- LayoutInflater mLayoutInflater;
-
- int mTextAppearance;
- boolean mTextAppearanceSet;
- ColorStateList mTextColor;
- ColorStateList mIconTintList;
- Drawable mItemBackground;
-
- /**
- * Padding to be inserted at the top of the list to avoid the first menu item
- * from being placed underneath the status bar.
- */
- private int mPaddingTopDefault;
-
- /**
- * Padding for separators between items
- */
- int mPaddingSeparator;
-
- @Override
- public void initForMenu(Context context, MenuBuilder menu) {
- mLayoutInflater = LayoutInflater.from(context);
- mMenu = menu;
- Resources res = context.getResources();
- mPaddingSeparator = res.getDimensionPixelOffset(
- R.dimen.design_navigation_separator_vertical_padding);
- }
-
- @Override
- public MenuView getMenuView(ViewGroup root) {
- if (mMenuView == null) {
- mMenuView = (NavigationMenuView) mLayoutInflater.inflate(
- R.layout.design_navigation_menu, root, false);
- if (mAdapter == null) {
- mAdapter = new NavigationMenuAdapter();
- }
- mHeaderLayout = (LinearLayout) mLayoutInflater
- .inflate(R.layout.design_navigation_item_header,
- mMenuView, false);
- mMenuView.setAdapter(mAdapter);
- }
- return mMenuView;
- }
-
- @Override
- public void updateMenuView(boolean cleared) {
- if (mAdapter != null) {
- mAdapter.update();
- }
- }
-
- @Override
- public void setCallback(Callback cb) {
- mCallback = cb;
- }
-
- @Override
- public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
- return false;
- }
-
- @Override
- public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
- if (mCallback != null) {
- mCallback.onCloseMenu(menu, allMenusAreClosing);
- }
- }
-
- @Override
- public boolean flagActionItems() {
- return false;
- }
-
- @Override
- public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
- return false;
- }
-
- @Override
- public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
- return false;
- }
-
- @Override
- public int getId() {
- return mId;
- }
-
- public void setId(int id) {
- mId = id;
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- if (Build.VERSION.SDK_INT >= 11) {
- // API 9-10 does not support ClassLoaderCreator, therefore things can crash if they're
- // loaded via different loaders. Rather than crash we just won't save state on those
- // platforms
- final Bundle state = new Bundle();
- if (mMenuView != null) {
- SparseArray<Parcelable> hierarchy = new SparseArray<>();
- mMenuView.saveHierarchyState(hierarchy);
- state.putSparseParcelableArray(STATE_HIERARCHY, hierarchy);
- }
- if (mAdapter != null) {
- state.putBundle(STATE_ADAPTER, mAdapter.createInstanceState());
- }
- if (mHeaderLayout != null) {
- SparseArray<Parcelable> header = new SparseArray<>();
- mHeaderLayout.saveHierarchyState(header);
- state.putSparseParcelableArray(STATE_HEADER, header);
- }
- return state;
- }
- return null;
- }
-
- @Override
- public void onRestoreInstanceState(final Parcelable parcelable) {
- if (parcelable instanceof Bundle) {
- Bundle state = (Bundle) parcelable;
- SparseArray<Parcelable> hierarchy = state.getSparseParcelableArray(STATE_HIERARCHY);
- if (hierarchy != null) {
- mMenuView.restoreHierarchyState(hierarchy);
- }
- Bundle adapterState = state.getBundle(STATE_ADAPTER);
- if (adapterState != null) {
- mAdapter.restoreInstanceState(adapterState);
- }
- SparseArray<Parcelable> header = state.getSparseParcelableArray(STATE_HEADER);
- if (header != null) {
- mHeaderLayout.restoreHierarchyState(header);
- }
- }
- }
-
- public void setCheckedItem(MenuItemImpl item) {
- mAdapter.setCheckedItem(item);
- }
-
- public View inflateHeaderView(@LayoutRes int res) {
- View view = mLayoutInflater.inflate(res, mHeaderLayout, false);
- addHeaderView(view);
- return view;
- }
-
- public void addHeaderView(@NonNull View view) {
- mHeaderLayout.addView(view);
- // The padding on top should be cleared.
- mMenuView.setPadding(0, 0, 0, mMenuView.getPaddingBottom());
- }
-
- public void removeHeaderView(@NonNull View view) {
- mHeaderLayout.removeView(view);
- if (mHeaderLayout.getChildCount() == 0) {
- mMenuView.setPadding(0, mPaddingTopDefault, 0, mMenuView.getPaddingBottom());
- }
- }
-
- public int getHeaderCount() {
- return mHeaderLayout.getChildCount();
- }
-
- public View getHeaderView(int index) {
- return mHeaderLayout.getChildAt(index);
- }
-
- @Nullable
- public ColorStateList getItemTintList() {
- return mIconTintList;
- }
-
- public void setItemIconTintList(@Nullable ColorStateList tint) {
- mIconTintList = tint;
- updateMenuView(false);
- }
-
- @Nullable
- public ColorStateList getItemTextColor() {
- return mTextColor;
- }
-
- public void setItemTextColor(@Nullable ColorStateList textColor) {
- mTextColor = textColor;
- updateMenuView(false);
- }
-
- public void setItemTextAppearance(@StyleRes int resId) {
- mTextAppearance = resId;
- mTextAppearanceSet = true;
- updateMenuView(false);
- }
-
- @Nullable
- public Drawable getItemBackground() {
- return mItemBackground;
- }
-
- public void setItemBackground(@Nullable Drawable itemBackground) {
- mItemBackground = itemBackground;
- updateMenuView(false);
- }
-
- public void setUpdateSuspended(boolean updateSuspended) {
- if (mAdapter != null) {
- mAdapter.setUpdateSuspended(updateSuspended);
- }
- }
-
- public void dispatchApplyWindowInsets(WindowInsetsCompat insets) {
- int top = insets.getSystemWindowInsetTop();
- if (mPaddingTopDefault != top) {
- mPaddingTopDefault = top;
- if (mHeaderLayout.getChildCount() == 0) {
- mMenuView.setPadding(0, mPaddingTopDefault, 0, mMenuView.getPaddingBottom());
- }
- }
- ViewCompat.dispatchApplyWindowInsets(mHeaderLayout, insets);
- }
-
- private abstract static class ViewHolder extends RecyclerView.ViewHolder {
-
- public ViewHolder(View itemView) {
- super(itemView);
- }
-
- }
-
- private static class NormalViewHolder extends ViewHolder {
-
- public NormalViewHolder(LayoutInflater inflater, ViewGroup parent,
- View.OnClickListener listener) {
- super(inflater.inflate(R.layout.design_navigation_item, parent, false));
- itemView.setOnClickListener(listener);
- }
-
- }
-
- private static class SubheaderViewHolder extends ViewHolder {
-
- public SubheaderViewHolder(LayoutInflater inflater, ViewGroup parent) {
- super(inflater.inflate(R.layout.design_navigation_item_subheader, parent, false));
- }
-
- }
-
- private static class SeparatorViewHolder extends ViewHolder {
-
- public SeparatorViewHolder(LayoutInflater inflater, ViewGroup parent) {
- super(inflater.inflate(R.layout.design_navigation_item_separator, parent, false));
- }
-
- }
-
- private static class HeaderViewHolder extends ViewHolder {
-
- public HeaderViewHolder(View itemView) {
- super(itemView);
- }
-
- }
-
- /**
- * Handles click events for the menu items. The items has to be {@link NavigationMenuItemView}.
- */
- final View.OnClickListener mOnClickListener = new View.OnClickListener() {
-
- @Override
- public void onClick(View v) {
- NavigationMenuItemView itemView = (NavigationMenuItemView) v;
- setUpdateSuspended(true);
- MenuItemImpl item = itemView.getItemData();
- boolean result = mMenu.performItemAction(item, NavigationMenuPresenter.this, 0);
- if (item != null && item.isCheckable() && result) {
- mAdapter.setCheckedItem(item);
- }
- setUpdateSuspended(false);
- updateMenuView(false);
- }
-
- };
-
- private class NavigationMenuAdapter extends RecyclerView.Adapter<ViewHolder> {
-
- private static final String STATE_CHECKED_ITEM = "android:menu:checked";
-
- private static final String STATE_ACTION_VIEWS = "android:menu:action_views";
- private static final int VIEW_TYPE_NORMAL = 0;
- private static final int VIEW_TYPE_SUBHEADER = 1;
- private static final int VIEW_TYPE_SEPARATOR = 2;
- private static final int VIEW_TYPE_HEADER = 3;
-
- private final ArrayList<NavigationMenuItem> mItems = new ArrayList<>();
- private MenuItemImpl mCheckedItem;
- private boolean mUpdateSuspended;
-
- NavigationMenuAdapter() {
- prepareMenuItems();
- }
-
- @Override
- public long getItemId(int position) {
- return position;
- }
-
- @Override
- public int getItemCount() {
- return mItems.size();
- }
-
- @Override
- public int getItemViewType(int position) {
- NavigationMenuItem item = mItems.get(position);
- if (item instanceof NavigationMenuSeparatorItem) {
- return VIEW_TYPE_SEPARATOR;
- } else if (item instanceof NavigationMenuHeaderItem) {
- return VIEW_TYPE_HEADER;
- } else if (item instanceof NavigationMenuTextItem) {
- NavigationMenuTextItem textItem = (NavigationMenuTextItem) item;
- if (textItem.getMenuItem().hasSubMenu()) {
- return VIEW_TYPE_SUBHEADER;
- } else {
- return VIEW_TYPE_NORMAL;
- }
- }
- throw new RuntimeException("Unknown item type.");
- }
-
- @Override
- public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
- switch (viewType) {
- case VIEW_TYPE_NORMAL:
- return new NormalViewHolder(mLayoutInflater, parent, mOnClickListener);
- case VIEW_TYPE_SUBHEADER:
- return new SubheaderViewHolder(mLayoutInflater, parent);
- case VIEW_TYPE_SEPARATOR:
- return new SeparatorViewHolder(mLayoutInflater, parent);
- case VIEW_TYPE_HEADER:
- return new HeaderViewHolder(mHeaderLayout);
- }
- return null;
- }
-
- @Override
- public void onBindViewHolder(ViewHolder holder, int position) {
- switch (getItemViewType(position)) {
- case VIEW_TYPE_NORMAL: {
- NavigationMenuItemView itemView = (NavigationMenuItemView) holder.itemView;
- itemView.setIconTintList(mIconTintList);
- if (mTextAppearanceSet) {
- itemView.setTextAppearance(mTextAppearance);
- }
- if (mTextColor != null) {
- itemView.setTextColor(mTextColor);
- }
- ViewCompat.setBackground(itemView, mItemBackground != null ?
- mItemBackground.getConstantState().newDrawable() : null);
- NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position);
- itemView.setNeedsEmptyIcon(item.needsEmptyIcon);
- itemView.initialize(item.getMenuItem(), 0);
- break;
- }
- case VIEW_TYPE_SUBHEADER: {
- TextView subHeader = (TextView) holder.itemView;
- NavigationMenuTextItem item = (NavigationMenuTextItem) mItems.get(position);
- subHeader.setText(item.getMenuItem().getTitle());
- break;
- }
- case VIEW_TYPE_SEPARATOR: {
- NavigationMenuSeparatorItem item =
- (NavigationMenuSeparatorItem) mItems.get(position);
- holder.itemView.setPadding(0, item.getPaddingTop(), 0,
- item.getPaddingBottom());
- break;
- }
- case VIEW_TYPE_HEADER: {
- break;
- }
- }
-
- }
-
- @Override
- public void onViewRecycled(ViewHolder holder) {
- if (holder instanceof NormalViewHolder) {
- ((NavigationMenuItemView) holder.itemView).recycle();
- }
- }
-
- public void update() {
- prepareMenuItems();
- notifyDataSetChanged();
- }
-
- /**
- * Flattens the visible menu items of {@link #mMenu} into {@link #mItems},
- * while inserting separators between items when necessary.
- */
- private void prepareMenuItems() {
- if (mUpdateSuspended) {
- return;
- }
- mUpdateSuspended = true;
- mItems.clear();
- mItems.add(new NavigationMenuHeaderItem());
-
- int currentGroupId = -1;
- int currentGroupStart = 0;
- boolean currentGroupHasIcon = false;
- for (int i = 0, totalSize = mMenu.getVisibleItems().size(); i < totalSize; i++) {
- MenuItemImpl item = mMenu.getVisibleItems().get(i);
- if (item.isChecked()) {
- setCheckedItem(item);
- }
- if (item.isCheckable()) {
- item.setExclusiveCheckable(false);
- }
- if (item.hasSubMenu()) {
- SubMenu subMenu = item.getSubMenu();
- if (subMenu.hasVisibleItems()) {
- if (i != 0) {
- mItems.add(new NavigationMenuSeparatorItem(mPaddingSeparator, 0));
- }
- mItems.add(new NavigationMenuTextItem(item));
- boolean subMenuHasIcon = false;
- int subMenuStart = mItems.size();
- for (int j = 0, size = subMenu.size(); j < size; j++) {
- MenuItemImpl subMenuItem = (MenuItemImpl) subMenu.getItem(j);
- if (subMenuItem.isVisible()) {
- if (!subMenuHasIcon && subMenuItem.getIcon() != null) {
- subMenuHasIcon = true;
- }
- if (subMenuItem.isCheckable()) {
- subMenuItem.setExclusiveCheckable(false);
- }
- if (item.isChecked()) {
- setCheckedItem(item);
- }
- mItems.add(new NavigationMenuTextItem(subMenuItem));
- }
- }
- if (subMenuHasIcon) {
- appendTransparentIconIfMissing(subMenuStart, mItems.size());
- }
- }
- } else {
- int groupId = item.getGroupId();
- if (groupId != currentGroupId) { // first item in group
- currentGroupStart = mItems.size();
- currentGroupHasIcon = item.getIcon() != null;
- if (i != 0) {
- currentGroupStart++;
- mItems.add(new NavigationMenuSeparatorItem(
- mPaddingSeparator, mPaddingSeparator));
- }
- } else if (!currentGroupHasIcon && item.getIcon() != null) {
- currentGroupHasIcon = true;
- appendTransparentIconIfMissing(currentGroupStart, mItems.size());
- }
- NavigationMenuTextItem textItem = new NavigationMenuTextItem(item);
- textItem.needsEmptyIcon = currentGroupHasIcon;
- mItems.add(textItem);
- currentGroupId = groupId;
- }
- }
- mUpdateSuspended = false;
- }
-
- private void appendTransparentIconIfMissing(int startIndex, int endIndex) {
- for (int i = startIndex; i < endIndex; i++) {
- NavigationMenuTextItem textItem = (NavigationMenuTextItem) mItems.get(i);
- textItem.needsEmptyIcon = true;
- }
- }
-
- public void setCheckedItem(MenuItemImpl checkedItem) {
- if (mCheckedItem == checkedItem || !checkedItem.isCheckable()) {
- return;
- }
- if (mCheckedItem != null) {
- mCheckedItem.setChecked(false);
- }
- mCheckedItem = checkedItem;
- checkedItem.setChecked(true);
- }
-
- public Bundle createInstanceState() {
- Bundle state = new Bundle();
- if (mCheckedItem != null) {
- state.putInt(STATE_CHECKED_ITEM, mCheckedItem.getItemId());
- }
- // Store the states of the action views.
- SparseArray<ParcelableSparseArray> actionViewStates = new SparseArray<>();
- for (int i = 0, size = mItems.size(); i < size; i++) {
- NavigationMenuItem navigationMenuItem = mItems.get(i);
- if (navigationMenuItem instanceof NavigationMenuTextItem) {
- MenuItemImpl item = ((NavigationMenuTextItem) navigationMenuItem).getMenuItem();
- View actionView = item != null ? item.getActionView() : null;
- if (actionView != null) {
- ParcelableSparseArray container = new ParcelableSparseArray();
- actionView.saveHierarchyState(container);
- actionViewStates.put(item.getItemId(), container);
- }
- }
- }
- state.putSparseParcelableArray(STATE_ACTION_VIEWS, actionViewStates);
- return state;
- }
-
- public void restoreInstanceState(Bundle state) {
- int checkedItem = state.getInt(STATE_CHECKED_ITEM, 0);
- if (checkedItem != 0) {
- mUpdateSuspended = true;
- for (int i = 0, size = mItems.size(); i < size; i++) {
- NavigationMenuItem item = mItems.get(i);
- if (item instanceof NavigationMenuTextItem) {
- MenuItemImpl menuItem = ((NavigationMenuTextItem) item).getMenuItem();
- if (menuItem != null && menuItem.getItemId() == checkedItem) {
- setCheckedItem(menuItem);
- break;
- }
- }
- }
- mUpdateSuspended = false;
- prepareMenuItems();
- }
- // Restore the states of the action views.
- SparseArray<ParcelableSparseArray> actionViewStates = state
- .getSparseParcelableArray(STATE_ACTION_VIEWS);
- if (actionViewStates != null) {
- for (int i = 0, size = mItems.size(); i < size; i++) {
- NavigationMenuItem navigationMenuItem = mItems.get(i);
- if (!(navigationMenuItem instanceof NavigationMenuTextItem)) {
- continue;
- }
- MenuItemImpl item = ((NavigationMenuTextItem) navigationMenuItem).getMenuItem();
- if (item == null) {
- continue;
- }
- View actionView = item.getActionView();
- if (actionView == null) {
- continue;
- }
- ParcelableSparseArray container = actionViewStates.get(item.getItemId());
- if (container == null) {
- continue;
- }
- actionView.restoreHierarchyState(container);
- }
- }
- }
-
- public void setUpdateSuspended(boolean updateSuspended) {
- mUpdateSuspended = updateSuspended;
- }
-
- }
-
- /**
- * Unified data model for all sorts of navigation menu items.
- */
- private interface NavigationMenuItem {
- }
-
- /**
- * Normal or subheader items.
- */
- private static class NavigationMenuTextItem implements NavigationMenuItem {
-
- private final MenuItemImpl mMenuItem;
-
- boolean needsEmptyIcon;
-
- NavigationMenuTextItem(MenuItemImpl item) {
- mMenuItem = item;
- }
-
- public MenuItemImpl getMenuItem() {
- return mMenuItem;
- }
-
- }
-
- /**
- * Separator items.
- */
- private static class NavigationMenuSeparatorItem implements NavigationMenuItem {
-
- private final int mPaddingTop;
-
- private final int mPaddingBottom;
-
- public NavigationMenuSeparatorItem(int paddingTop, int paddingBottom) {
- mPaddingTop = paddingTop;
- mPaddingBottom = paddingBottom;
- }
-
- public int getPaddingTop() {
- return mPaddingTop;
- }
-
- public int getPaddingBottom() {
- return mPaddingBottom;
- }
-
- }
-
- /**
- * Header (not subheader) items.
- */
- private static class NavigationMenuHeaderItem implements NavigationMenuItem {
- NavigationMenuHeaderItem() {
- }
- // The actual content is hold by NavigationMenuPresenter#mHeaderLayout.
- }
-
-}
diff --git a/android/support/design/internal/NavigationMenuView.java b/android/support/design/internal/NavigationMenuView.java
deleted file mode 100644
index 711f71ee..00000000
--- a/android/support/design/internal/NavigationMenuView.java
+++ /dev/null
@@ -1,58 +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 android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.support.annotation.RestrictTo;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuView;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.util.AttributeSet;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class NavigationMenuView extends RecyclerView implements MenuView {
-
- public NavigationMenuView(Context context) {
- this(context, null);
- }
-
- public NavigationMenuView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public NavigationMenuView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false));
- }
-
- @Override
- public void initialize(MenuBuilder menu) {
-
- }
-
- @Override
- public int getWindowAnimations() {
- return 0;
- }
-
-}
diff --git a/android/support/design/internal/NavigationSubMenu.java b/android/support/design/internal/NavigationSubMenu.java
deleted file mode 100644
index 1ff1e4f4..00000000
--- a/android/support/design/internal/NavigationSubMenu.java
+++ /dev/null
@@ -1,46 +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 android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.support.annotation.RestrictTo;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.view.menu.SubMenuBuilder;
-
-/**
- * This is a {@link SubMenuBuilder} that it notifies the parent {@link NavigationMenu} of its menu
- * updates.
- *
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class NavigationSubMenu extends SubMenuBuilder {
-
- public NavigationSubMenu(Context context, NavigationMenu menu, MenuItemImpl item) {
- super(context, menu, item);
- }
-
- @Override
- public void onItemsChanged(boolean structureChanged) {
- super.onItemsChanged(structureChanged);
- ((MenuBuilder) getParentMenu()).onItemsChanged(structureChanged);
- }
-
-}
diff --git a/android/support/design/internal/ParcelableSparseArray.java b/android/support/design/internal/ParcelableSparseArray.java
deleted file mode 100644
index b29000e6..00000000
--- a/android/support/design/internal/ParcelableSparseArray.java
+++ /dev/null
@@ -1,83 +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 android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.RestrictTo;
-import android.util.SparseArray;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class ParcelableSparseArray extends SparseArray<Parcelable> implements Parcelable {
-
- public ParcelableSparseArray() {
- super();
- }
-
- public ParcelableSparseArray(Parcel source, ClassLoader loader) {
- super();
- int size = source.readInt();
- int[] keys = new int[size];
- source.readIntArray(keys);
- Parcelable[] values = source.readParcelableArray(loader);
- for (int i = 0; i < size; ++i) {
- put(keys[i], values[i]);
- }
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
-
- @Override
- public void writeToParcel(Parcel parcel, int flags) {
- int size = size();
- int[] keys = new int[size];
- Parcelable[] values = new Parcelable[size];
- for (int i = 0; i < size; ++i) {
- keys[i] = keyAt(i);
- values[i] = valueAt(i);
- }
- parcel.writeInt(size);
- parcel.writeIntArray(keys);
- parcel.writeParcelableArray(values, flags);
- }
-
- public static final Creator<ParcelableSparseArray> CREATOR =
- new ClassLoaderCreator<ParcelableSparseArray>() {
- @Override
- public ParcelableSparseArray createFromParcel(Parcel source, ClassLoader loader) {
- return new ParcelableSparseArray(source, loader);
- }
-
- @Override
- public ParcelableSparseArray createFromParcel(Parcel source) {
- return new ParcelableSparseArray(source, null);
- }
-
- @Override
- public ParcelableSparseArray[] newArray(int size) {
- return new ParcelableSparseArray[size];
- }
- };
-}
diff --git a/android/support/design/internal/ScrimInsetsFrameLayout.java b/android/support/design/internal/ScrimInsetsFrameLayout.java
deleted file mode 100644
index 38f5b29b..00000000
--- a/android/support/design/internal/ScrimInsetsFrameLayout.java
+++ /dev/null
@@ -1,138 +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 android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.FrameLayout;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class ScrimInsetsFrameLayout extends FrameLayout {
-
- Drawable mInsetForeground;
-
- Rect mInsets;
-
- private Rect mTempRect = new Rect();
-
- public ScrimInsetsFrameLayout(Context context) {
- this(context, null);
- }
-
- public ScrimInsetsFrameLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public ScrimInsetsFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- final TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.ScrimInsetsFrameLayout, defStyleAttr,
- R.style.Widget_Design_ScrimInsetsFrameLayout);
- mInsetForeground = a.getDrawable(R.styleable.ScrimInsetsFrameLayout_insetForeground);
- a.recycle();
- setWillNotDraw(true); // No need to draw until the insets are adjusted
-
- ViewCompat.setOnApplyWindowInsetsListener(this,
- new android.support.v4.view.OnApplyWindowInsetsListener() {
- @Override
- public WindowInsetsCompat onApplyWindowInsets(View v,
- WindowInsetsCompat insets) {
- if (null == mInsets) {
- mInsets = new Rect();
- }
- mInsets.set(insets.getSystemWindowInsetLeft(),
- insets.getSystemWindowInsetTop(),
- insets.getSystemWindowInsetRight(),
- insets.getSystemWindowInsetBottom());
- onInsetsChanged(insets);
- setWillNotDraw(!insets.hasSystemWindowInsets() || mInsetForeground == null);
- ViewCompat.postInvalidateOnAnimation(ScrimInsetsFrameLayout.this);
- return insets.consumeSystemWindowInsets();
- }
- });
- }
-
- @Override
- public void draw(@NonNull Canvas canvas) {
- super.draw(canvas);
-
- int width = getWidth();
- int height = getHeight();
- if (mInsets != null && mInsetForeground != null) {
- int sc = canvas.save();
- canvas.translate(getScrollX(), getScrollY());
-
- // Top
- mTempRect.set(0, 0, width, mInsets.top);
- mInsetForeground.setBounds(mTempRect);
- mInsetForeground.draw(canvas);
-
- // Bottom
- mTempRect.set(0, height - mInsets.bottom, width, height);
- mInsetForeground.setBounds(mTempRect);
- mInsetForeground.draw(canvas);
-
- // Left
- mTempRect.set(0, mInsets.top, mInsets.left, height - mInsets.bottom);
- mInsetForeground.setBounds(mTempRect);
- mInsetForeground.draw(canvas);
-
- // Right
- mTempRect.set(width - mInsets.right, mInsets.top, width, height - mInsets.bottom);
- mInsetForeground.setBounds(mTempRect);
- mInsetForeground.draw(canvas);
-
- canvas.restoreToCount(sc);
- }
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- if (mInsetForeground != null) {
- mInsetForeground.setCallback(this);
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- if (mInsetForeground != null) {
- mInsetForeground.setCallback(null);
- }
- }
-
- protected void onInsetsChanged(WindowInsetsCompat insets) {
- }
-
-}
diff --git a/android/support/design/internal/SnackbarContentLayout.java b/android/support/design/internal/SnackbarContentLayout.java
deleted file mode 100644
index 2abf0127..00000000
--- a/android/support/design/internal/SnackbarContentLayout.java
+++ /dev/null
@@ -1,157 +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.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.design.widget.BaseTransientBottomBar;
-import android.support.v4.view.ViewCompat;
-import android.util.AttributeSet;
-import android.view.View;
-import android.widget.Button;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class SnackbarContentLayout extends LinearLayout implements
- BaseTransientBottomBar.ContentViewCallback {
- private TextView mMessageView;
- private Button mActionView;
-
- private int mMaxWidth;
- private int mMaxInlineActionWidth;
-
- public SnackbarContentLayout(Context context) {
- this(context, null);
- }
-
- public SnackbarContentLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout);
- mMaxWidth = a.getDimensionPixelSize(R.styleable.SnackbarLayout_android_maxWidth, -1);
- mMaxInlineActionWidth = a.getDimensionPixelSize(
- R.styleable.SnackbarLayout_maxActionInlineWidth, -1);
- a.recycle();
- }
-
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- mMessageView = findViewById(R.id.snackbar_text);
- mActionView = findViewById(R.id.snackbar_action);
- }
-
- public TextView getMessageView() {
- return mMessageView;
- }
-
- public Button getActionView() {
- return mActionView;
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- if (mMaxWidth > 0 && getMeasuredWidth() > mMaxWidth) {
- widthMeasureSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY);
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- final int multiLineVPadding = getResources().getDimensionPixelSize(
- R.dimen.design_snackbar_padding_vertical_2lines);
- final int singleLineVPadding = getResources().getDimensionPixelSize(
- R.dimen.design_snackbar_padding_vertical);
- final boolean isMultiLine = mMessageView.getLayout().getLineCount() > 1;
-
- boolean remeasure = false;
- if (isMultiLine && mMaxInlineActionWidth > 0
- && mActionView.getMeasuredWidth() > mMaxInlineActionWidth) {
- if (updateViewsWithinLayout(VERTICAL, multiLineVPadding,
- multiLineVPadding - singleLineVPadding)) {
- remeasure = true;
- }
- } else {
- final int messagePadding = isMultiLine ? multiLineVPadding : singleLineVPadding;
- if (updateViewsWithinLayout(HORIZONTAL, messagePadding, messagePadding)) {
- remeasure = true;
- }
- }
-
- if (remeasure) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
-
- private boolean updateViewsWithinLayout(final int orientation,
- final int messagePadTop, final int messagePadBottom) {
- boolean changed = false;
- if (orientation != getOrientation()) {
- setOrientation(orientation);
- changed = true;
- }
- if (mMessageView.getPaddingTop() != messagePadTop
- || mMessageView.getPaddingBottom() != messagePadBottom) {
- updateTopBottomPadding(mMessageView, messagePadTop, messagePadBottom);
- changed = true;
- }
- return changed;
- }
-
- private static void updateTopBottomPadding(View view, int topPadding, int bottomPadding) {
- if (ViewCompat.isPaddingRelative(view)) {
- ViewCompat.setPaddingRelative(view,
- ViewCompat.getPaddingStart(view), topPadding,
- ViewCompat.getPaddingEnd(view), bottomPadding);
- } else {
- view.setPadding(view.getPaddingLeft(), topPadding,
- view.getPaddingRight(), bottomPadding);
- }
- }
-
- @Override
- public void animateContentIn(int delay, int duration) {
- mMessageView.setAlpha(0f);
- mMessageView.animate().alpha(1f).setDuration(duration)
- .setStartDelay(delay).start();
-
- if (mActionView.getVisibility() == VISIBLE) {
- mActionView.setAlpha(0f);
- mActionView.animate().alpha(1f).setDuration(duration)
- .setStartDelay(delay).start();
- }
- }
-
- @Override
- public void animateContentOut(int delay, int duration) {
- mMessageView.setAlpha(1f);
- mMessageView.animate().alpha(0f).setDuration(duration)
- .setStartDelay(delay).start();
-
- if (mActionView.getVisibility() == VISIBLE) {
- mActionView.setAlpha(1f);
- mActionView.animate().alpha(0f).setDuration(duration)
- .setStartDelay(delay).start();
- }
- }
-}
diff --git a/android/support/design/internal/TextScale.java b/android/support/design/internal/TextScale.java
deleted file mode 100644
index 06c94729..00000000
--- a/android/support/design/internal/TextScale.java
+++ /dev/null
@@ -1,85 +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.support.design.internal;
-
-import android.animation.Animator;
-import android.animation.ValueAnimator;
-import android.support.annotation.RequiresApi;
-import android.support.annotation.RestrictTo;
-import android.support.transition.Transition;
-import android.support.transition.TransitionValues;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import java.util.Map;
-
-/**
- * @hide
- */
-@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
-@RequiresApi(14)
-public class TextScale extends Transition {
- private static final String PROPNAME_SCALE = "android:textscale:scale";
-
- @Override
- public void captureStartValues(TransitionValues transitionValues) {
- captureValues(transitionValues);
- }
-
- @Override
- public void captureEndValues(TransitionValues transitionValues) {
- captureValues(transitionValues);
- }
-
- private void captureValues(TransitionValues transitionValues) {
- if (transitionValues.view instanceof TextView) {
- TextView textview = (TextView) transitionValues.view;
- transitionValues.values.put(PROPNAME_SCALE, textview.getScaleX());
- }
- }
-
- @Override
- public Animator createAnimator(ViewGroup sceneRoot, TransitionValues startValues,
- TransitionValues endValues) {
- if (startValues == null || endValues == null || !(startValues.view instanceof TextView)
- || !(endValues.view instanceof TextView)) {
- return null;
- }
- final TextView view = (TextView) endValues.view;
- Map<String, Object> startVals = startValues.values;
- Map<String, Object> endVals = endValues.values;
- final float startSize = startVals.get(PROPNAME_SCALE) != null ? (float) startVals.get(
- PROPNAME_SCALE) : 1f;
- final float endSize = endVals.get(PROPNAME_SCALE) != null ? (float) endVals.get(
- PROPNAME_SCALE) : 1f;
- if (startSize == endSize) {
- return null;
- }
-
- ValueAnimator animator = ValueAnimator.ofFloat(startSize, endSize);
-
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator valueAnimator) {
- float animatedValue = (float) valueAnimator.getAnimatedValue();
- view.setScaleX(animatedValue);
- view.setScaleY(animatedValue);
- }
- });
- return animator;
- }
-}
diff --git a/android/support/design/internal/package-info.java b/android/support/design/internal/package-info.java
deleted file mode 100644
index 6b6f7bbd..00000000
--- a/android/support/design/internal/package-info.java
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-package android.support.design.internal;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.support.annotation.RestrictTo;
diff --git a/android/support/design/widget/AnimationUtils.java b/android/support/design/widget/AnimationUtils.java
deleted file mode 100644
index 3613afd8..00000000
--- a/android/support/design/widget/AnimationUtils.java
+++ /dev/null
@@ -1,45 +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 android.support.design.widget;
-
-import android.support.v4.view.animation.FastOutLinearInInterpolator;
-import android.support.v4.view.animation.FastOutSlowInInterpolator;
-import android.support.v4.view.animation.LinearOutSlowInInterpolator;
-import android.view.animation.DecelerateInterpolator;
-import android.view.animation.Interpolator;
-import android.view.animation.LinearInterpolator;
-
-class AnimationUtils {
-
- static final Interpolator LINEAR_INTERPOLATOR = new LinearInterpolator();
- static final Interpolator FAST_OUT_SLOW_IN_INTERPOLATOR = new FastOutSlowInInterpolator();
- static final Interpolator FAST_OUT_LINEAR_IN_INTERPOLATOR = new FastOutLinearInInterpolator();
- static final Interpolator LINEAR_OUT_SLOW_IN_INTERPOLATOR = new LinearOutSlowInInterpolator();
- static final Interpolator DECELERATE_INTERPOLATOR = new DecelerateInterpolator();
-
- /**
- * Linear interpolation between {@code startValue} and {@code endValue} by {@code fraction}.
- */
- static float lerp(float startValue, float endValue, float fraction) {
- return startValue + (fraction * (endValue - startValue));
- }
-
- static int lerp(int startValue, int endValue, float fraction) {
- return startValue + Math.round(fraction * (endValue - startValue));
- }
-
-}
diff --git a/android/support/design/widget/AppBarLayout.java b/android/support/design/widget/AppBarLayout.java
deleted file mode 100644
index 8304cd6a..00000000
--- a/android/support/design/widget/AppBarLayout.java
+++ /dev/null
@@ -1,1474 +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 android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.graphics.Rect;
-import android.os.Build;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RequiresApi;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
-import android.support.design.R;
-import android.support.v4.math.MathUtils;
-import android.support.v4.util.ObjectsCompat;
-import android.support.v4.view.AbsSavedState;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.animation.Interpolator;
-import android.widget.LinearLayout;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * AppBarLayout is a vertical {@link LinearLayout} which implements many of the features of
- * material designs app bar concept, namely scrolling gestures.
- * <p>
- * Children should provide their desired scrolling behavior through
- * {@link LayoutParams#setScrollFlags(int)} and the associated layout xml attribute:
- * {@code app:layout_scrollFlags}.
- *
- * <p>
- * This view depends heavily on being used as a direct child within a {@link CoordinatorLayout}.
- * If you use AppBarLayout within a different {@link ViewGroup}, most of it's functionality will
- * not work.
- * <p>
- * AppBarLayout also requires a separate scrolling sibling in order to know when to scroll.
- * The binding is done through the {@link ScrollingViewBehavior} behavior class, meaning that you
- * should set your scrolling view's behavior to be an instance of {@link ScrollingViewBehavior}.
- * A string resource containing the full class name is available.
- *
- * <pre>
- * &lt;android.support.design.widget.CoordinatorLayout
- * xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;
- * xmlns:app=&quot;http://schemas.android.com/apk/res-auto&quot;
- * android:layout_width=&quot;match_parent&quot;
- * android:layout_height=&quot;match_parent&quot;&gt;
- *
- * &lt;android.support.v4.widget.NestedScrollView
- * android:layout_width=&quot;match_parent&quot;
- * android:layout_height=&quot;match_parent&quot;
- * app:layout_behavior=&quot;@string/appbar_scrolling_view_behavior&quot;&gt;
- *
- * &lt;!-- Your scrolling content --&gt;
- *
- * &lt;/android.support.v4.widget.NestedScrollView&gt;
- *
- * &lt;android.support.design.widget.AppBarLayout
- * android:layout_height=&quot;wrap_content&quot;
- * android:layout_width=&quot;match_parent&quot;&gt;
- *
- * &lt;android.support.v7.widget.Toolbar
- * ...
- * app:layout_scrollFlags=&quot;scroll|enterAlways&quot;/&gt;
- *
- * &lt;android.support.design.widget.TabLayout
- * ...
- * app:layout_scrollFlags=&quot;scroll|enterAlways&quot;/&gt;
- *
- * &lt;/android.support.design.widget.AppBarLayout&gt;
- *
- * &lt;/android.support.design.widget.CoordinatorLayout&gt;
- * </pre>
- *
- * @see <a href="http://www.google.com/design/spec/layout/structure.html#structure-app-bar">
- * http://www.google.com/design/spec/layout/structure.html#structure-app-bar</a>
- */
-@CoordinatorLayout.DefaultBehavior(AppBarLayout.Behavior.class)
-public class AppBarLayout extends LinearLayout {
-
- static final int PENDING_ACTION_NONE = 0x0;
- static final int PENDING_ACTION_EXPANDED = 0x1;
- static final int PENDING_ACTION_COLLAPSED = 0x2;
- static final int PENDING_ACTION_ANIMATE_ENABLED = 0x4;
- static final int PENDING_ACTION_FORCE = 0x8;
-
- /**
- * Interface definition for a callback to be invoked when an {@link AppBarLayout}'s vertical
- * offset changes.
- */
- public interface OnOffsetChangedListener {
- /**
- * Called when the {@link AppBarLayout}'s layout offset has been changed. This allows
- * child views to implement custom behavior based on the offset (for instance pinning a
- * view at a certain y value).
- *
- * @param appBarLayout the {@link AppBarLayout} which offset has changed
- * @param verticalOffset the vertical offset for the parent {@link AppBarLayout}, in px
- */
- void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset);
- }
-
- private static final int INVALID_SCROLL_RANGE = -1;
-
- private int mTotalScrollRange = INVALID_SCROLL_RANGE;
- private int mDownPreScrollRange = INVALID_SCROLL_RANGE;
- private int mDownScrollRange = INVALID_SCROLL_RANGE;
-
- private boolean mHaveChildWithInterpolator;
-
- private int mPendingAction = PENDING_ACTION_NONE;
-
- private WindowInsetsCompat mLastInsets;
-
- private List<OnOffsetChangedListener> mListeners;
-
- private boolean mCollapsible;
- private boolean mCollapsed;
-
- private int[] mTmpStatesArray;
-
- public AppBarLayout(Context context) {
- this(context, null);
- }
-
- public AppBarLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- setOrientation(VERTICAL);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- if (Build.VERSION.SDK_INT >= 21) {
- // Use the bounds view outline provider so that we cast a shadow, even without a
- // background
- ViewUtilsLollipop.setBoundsViewOutlineProvider(this);
-
- // If we're running on API 21+, we should reset any state list animator from our
- // default style
- ViewUtilsLollipop.setStateListAnimatorFromAttrs(this, attrs, 0,
- R.style.Widget_Design_AppBarLayout);
- }
-
- final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.AppBarLayout,
- 0, R.style.Widget_Design_AppBarLayout);
- ViewCompat.setBackground(this, a.getDrawable(R.styleable.AppBarLayout_android_background));
- if (a.hasValue(R.styleable.AppBarLayout_expanded)) {
- setExpanded(a.getBoolean(R.styleable.AppBarLayout_expanded, false), false, false);
- }
- if (Build.VERSION.SDK_INT >= 21 && a.hasValue(R.styleable.AppBarLayout_elevation)) {
- ViewUtilsLollipop.setDefaultAppBarLayoutStateListAnimator(
- this, a.getDimensionPixelSize(R.styleable.AppBarLayout_elevation, 0));
- }
- if (Build.VERSION.SDK_INT >= 26) {
- // In O+, we have these values set in the style. Since there is no defStyleAttr for
- // AppBarLayout at the AppCompat level, check for these attributes here.
- if (a.hasValue(R.styleable.AppBarLayout_android_keyboardNavigationCluster)) {
- this.setKeyboardNavigationCluster(a.getBoolean(
- R.styleable.AppBarLayout_android_keyboardNavigationCluster, false));
- }
- if (a.hasValue(R.styleable.AppBarLayout_android_touchscreenBlocksFocus)) {
- this.setTouchscreenBlocksFocus(a.getBoolean(
- R.styleable.AppBarLayout_android_touchscreenBlocksFocus, false));
- }
- }
- a.recycle();
-
- ViewCompat.setOnApplyWindowInsetsListener(this,
- new android.support.v4.view.OnApplyWindowInsetsListener() {
- @Override
- public WindowInsetsCompat onApplyWindowInsets(View v,
- WindowInsetsCompat insets) {
- return onWindowInsetChanged(insets);
- }
- });
- }
-
- /**
- * Add a listener that will be called when the offset of this {@link AppBarLayout} changes.
- *
- * @param listener The listener that will be called when the offset changes.]
- *
- * @see #removeOnOffsetChangedListener(OnOffsetChangedListener)
- */
- public void addOnOffsetChangedListener(OnOffsetChangedListener listener) {
- if (mListeners == null) {
- mListeners = new ArrayList<>();
- }
- if (listener != null && !mListeners.contains(listener)) {
- mListeners.add(listener);
- }
- }
-
- /**
- * Remove the previously added {@link OnOffsetChangedListener}.
- *
- * @param listener the listener to remove.
- */
- public void removeOnOffsetChangedListener(OnOffsetChangedListener listener) {
- if (mListeners != null && listener != null) {
- mListeners.remove(listener);
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- invalidateScrollRanges();
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
- invalidateScrollRanges();
-
- mHaveChildWithInterpolator = false;
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- final LayoutParams childLp = (LayoutParams) child.getLayoutParams();
- final Interpolator interpolator = childLp.getScrollInterpolator();
-
- if (interpolator != null) {
- mHaveChildWithInterpolator = true;
- break;
- }
- }
-
- updateCollapsible();
- }
-
- private void updateCollapsible() {
- boolean haveCollapsibleChild = false;
- for (int i = 0, z = getChildCount(); i < z; i++) {
- if (((LayoutParams) getChildAt(i).getLayoutParams()).isCollapsible()) {
- haveCollapsibleChild = true;
- break;
- }
- }
- setCollapsibleState(haveCollapsibleChild);
- }
-
- private void invalidateScrollRanges() {
- // Invalidate the scroll ranges
- mTotalScrollRange = INVALID_SCROLL_RANGE;
- mDownPreScrollRange = INVALID_SCROLL_RANGE;
- mDownScrollRange = INVALID_SCROLL_RANGE;
- }
-
- @Override
- public void setOrientation(int orientation) {
- if (orientation != VERTICAL) {
- throw new IllegalArgumentException("AppBarLayout is always vertical and does"
- + " not support horizontal orientation");
- }
- super.setOrientation(orientation);
- }
-
- /**
- * Sets whether this {@link AppBarLayout} is expanded or not, animating if it has already
- * been laid out.
- *
- * <p>As with {@link AppBarLayout}'s scrolling, this method relies on this layout being a
- * direct child of a {@link CoordinatorLayout}.</p>
- *
- * @param expanded true if the layout should be fully expanded, false if it should
- * be fully collapsed
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_expanded
- */
- public void setExpanded(boolean expanded) {
- setExpanded(expanded, ViewCompat.isLaidOut(this));
- }
-
- /**
- * Sets whether this {@link AppBarLayout} is expanded or not.
- *
- * <p>As with {@link AppBarLayout}'s scrolling, this method relies on this layout being a
- * direct child of a {@link CoordinatorLayout}.</p>
- *
- * @param expanded true if the layout should be fully expanded, false if it should
- * be fully collapsed
- * @param animate Whether to animate to the new state
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_expanded
- */
- public void setExpanded(boolean expanded, boolean animate) {
- setExpanded(expanded, animate, true);
- }
-
- private void setExpanded(boolean expanded, boolean animate, boolean force) {
- mPendingAction = (expanded ? PENDING_ACTION_EXPANDED : PENDING_ACTION_COLLAPSED)
- | (animate ? PENDING_ACTION_ANIMATE_ENABLED : 0)
- | (force ? PENDING_ACTION_FORCE : 0);
- requestLayout();
- }
-
- @Override
- protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
- return p instanceof LayoutParams;
- }
-
- @Override
- protected LayoutParams generateDefaultLayoutParams() {
- return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
- }
-
- @Override
- public LayoutParams generateLayoutParams(AttributeSet attrs) {
- return new LayoutParams(getContext(), attrs);
- }
-
- @Override
- protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
- if (Build.VERSION.SDK_INT >= 19 && p instanceof LinearLayout.LayoutParams) {
- return new LayoutParams((LinearLayout.LayoutParams) p);
- } else if (p instanceof MarginLayoutParams) {
- return new LayoutParams((MarginLayoutParams) p);
- }
- return new LayoutParams(p);
- }
-
- boolean hasChildWithInterpolator() {
- return mHaveChildWithInterpolator;
- }
-
- /**
- * Returns the scroll range of all children.
- *
- * @return the scroll range in px
- */
- public final int getTotalScrollRange() {
- if (mTotalScrollRange != INVALID_SCROLL_RANGE) {
- return mTotalScrollRange;
- }
-
- int range = 0;
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final int childHeight = child.getMeasuredHeight();
- final int flags = lp.mScrollFlags;
-
- if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
- // We're set to scroll so add the child's height
- range += childHeight + lp.topMargin + lp.bottomMargin;
-
- if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
- // For a collapsing scroll, we to take the collapsed height into account.
- // We also break straight away since later views can't scroll beneath
- // us
- range -= ViewCompat.getMinimumHeight(child);
- break;
- }
- } else {
- // As soon as a view doesn't have the scroll flag, we end the range calculation.
- // This is because views below can not scroll under a fixed view.
- break;
- }
- }
- return mTotalScrollRange = Math.max(0, range - getTopInset());
- }
-
- boolean hasScrollableChildren() {
- return getTotalScrollRange() != 0;
- }
-
- /**
- * Return the scroll range when scrolling up from a nested pre-scroll.
- */
- int getUpNestedPreScrollRange() {
- return getTotalScrollRange();
- }
-
- /**
- * Return the scroll range when scrolling down from a nested pre-scroll.
- */
- int getDownNestedPreScrollRange() {
- if (mDownPreScrollRange != INVALID_SCROLL_RANGE) {
- // If we already have a valid value, return it
- return mDownPreScrollRange;
- }
-
- int range = 0;
- for (int i = getChildCount() - 1; i >= 0; i--) {
- final View child = getChildAt(i);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final int childHeight = child.getMeasuredHeight();
- final int flags = lp.mScrollFlags;
-
- if ((flags & LayoutParams.FLAG_QUICK_RETURN) == LayoutParams.FLAG_QUICK_RETURN) {
- // First take the margin into account
- range += lp.topMargin + lp.bottomMargin;
- // The view has the quick return flag combination...
- if ((flags & LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED) != 0) {
- // If they're set to enter collapsed, use the minimum height
- range += ViewCompat.getMinimumHeight(child);
- } else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
- // Only enter by the amount of the collapsed height
- range += childHeight - ViewCompat.getMinimumHeight(child);
- } else {
- // Else use the full height (minus the top inset)
- range += childHeight - getTopInset();
- }
- } else if (range > 0) {
- // If we've hit an non-quick return scrollable view, and we've already hit a
- // quick return view, return now
- break;
- }
- }
- return mDownPreScrollRange = Math.max(0, range);
- }
-
- /**
- * Return the scroll range when scrolling down from a nested scroll.
- */
- int getDownNestedScrollRange() {
- if (mDownScrollRange != INVALID_SCROLL_RANGE) {
- // If we already have a valid value, return it
- return mDownScrollRange;
- }
-
- int range = 0;
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- int childHeight = child.getMeasuredHeight();
- childHeight += lp.topMargin + lp.bottomMargin;
-
- final int flags = lp.mScrollFlags;
-
- if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
- // We're set to scroll so add the child's height
- range += childHeight;
-
- if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
- // For a collapsing exit scroll, we to take the collapsed height into account.
- // We also break the range straight away since later views can't scroll
- // beneath us
- range -= ViewCompat.getMinimumHeight(child) + getTopInset();
- break;
- }
- } else {
- // As soon as a view doesn't have the scroll flag, we end the range calculation.
- // This is because views below can not scroll under a fixed view.
- break;
- }
- }
- return mDownScrollRange = Math.max(0, range);
- }
-
- void dispatchOffsetUpdates(int offset) {
- // Iterate backwards through the list so that most recently added listeners
- // get the first chance to decide
- if (mListeners != null) {
- for (int i = 0, z = mListeners.size(); i < z; i++) {
- final OnOffsetChangedListener listener = mListeners.get(i);
- if (listener != null) {
- listener.onOffsetChanged(this, offset);
- }
- }
- }
- }
-
- final int getMinimumHeightForVisibleOverlappingContent() {
- final int topInset = getTopInset();
- final int minHeight = ViewCompat.getMinimumHeight(this);
- if (minHeight != 0) {
- // If this layout has a min height, use it (doubled)
- return (minHeight * 2) + topInset;
- }
-
- // Otherwise, we'll use twice the min height of our last child
- final int childCount = getChildCount();
- final int lastChildMinHeight = childCount >= 1
- ? ViewCompat.getMinimumHeight(getChildAt(childCount - 1)) : 0;
- if (lastChildMinHeight != 0) {
- return (lastChildMinHeight * 2) + topInset;
- }
-
- // If we reach here then we don't have a min height explicitly set. Instead we'll take a
- // guess at 1/3 of our height being visible
- return getHeight() / 3;
- }
-
- @Override
- protected int[] onCreateDrawableState(int extraSpace) {
- if (mTmpStatesArray == null) {
- // Note that we can't allocate this at the class level (in declaration) since
- // some paths in super View constructor are going to call this method before
- // that
- mTmpStatesArray = new int[2];
- }
- final int[] extraStates = mTmpStatesArray;
- final int[] states = super.onCreateDrawableState(extraSpace + extraStates.length);
-
- extraStates[0] = mCollapsible ? R.attr.state_collapsible : -R.attr.state_collapsible;
- extraStates[1] = mCollapsible && mCollapsed
- ? R.attr.state_collapsed : -R.attr.state_collapsed;
-
- return mergeDrawableStates(states, extraStates);
- }
-
- /**
- * Sets whether the AppBarLayout has collapsible children or not.
- *
- * @return true if the collapsible state changed
- */
- private boolean setCollapsibleState(boolean collapsible) {
- if (mCollapsible != collapsible) {
- mCollapsible = collapsible;
- refreshDrawableState();
- return true;
- }
- return false;
- }
-
- /**
- * Sets whether the AppBarLayout is in a collapsed state or not.
- *
- * @return true if the collapsed state changed
- */
- boolean setCollapsedState(boolean collapsed) {
- if (mCollapsed != collapsed) {
- mCollapsed = collapsed;
- refreshDrawableState();
- return true;
- }
- return false;
- }
-
- /**
- * @deprecated target elevation is now deprecated. AppBarLayout's elevation is now
- * controlled via a {@link android.animation.StateListAnimator}. If a target
- * elevation is set, either by this method or the {@code app:elevation} attribute,
- * a new state list animator is created which uses the given {@code elevation} value.
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_elevation
- */
- @Deprecated
- public void setTargetElevation(float elevation) {
- if (Build.VERSION.SDK_INT >= 21) {
- ViewUtilsLollipop.setDefaultAppBarLayoutStateListAnimator(this, elevation);
- }
- }
-
- /**
- * @deprecated target elevation is now deprecated. AppBarLayout's elevation is now
- * controlled via a {@link android.animation.StateListAnimator}. This method now
- * always returns 0.
- */
- @Deprecated
- public float getTargetElevation() {
- return 0;
- }
-
- int getPendingAction() {
- return mPendingAction;
- }
-
- void resetPendingAction() {
- mPendingAction = PENDING_ACTION_NONE;
- }
-
- @VisibleForTesting
- final int getTopInset() {
- return mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
- }
-
- WindowInsetsCompat onWindowInsetChanged(final WindowInsetsCompat insets) {
- WindowInsetsCompat newInsets = null;
-
- if (ViewCompat.getFitsSystemWindows(this)) {
- // If we're set to fit system windows, keep the insets
- newInsets = insets;
- }
-
- // If our insets have changed, keep them and invalidate the scroll ranges...
- if (!ObjectsCompat.equals(mLastInsets, newInsets)) {
- mLastInsets = newInsets;
- invalidateScrollRanges();
- }
-
- return insets;
- }
-
- public static class LayoutParams extends LinearLayout.LayoutParams {
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef(flag=true, value={
- SCROLL_FLAG_SCROLL,
- SCROLL_FLAG_EXIT_UNTIL_COLLAPSED,
- SCROLL_FLAG_ENTER_ALWAYS,
- SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED,
- SCROLL_FLAG_SNAP
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface ScrollFlags {}
-
- /**
- * The view will be scroll in direct relation to scroll events. This flag needs to be
- * set for any of the other flags to take effect. If any sibling views
- * before this one do not have this flag, then this value has no effect.
- */
- public static final int SCROLL_FLAG_SCROLL = 0x1;
-
- /**
- * When exiting (scrolling off screen) the view will be scrolled until it is
- * 'collapsed'. The collapsed height is defined by the view's minimum height.
- *
- * @see ViewCompat#getMinimumHeight(View)
- * @see View#setMinimumHeight(int)
- */
- public static final int SCROLL_FLAG_EXIT_UNTIL_COLLAPSED = 0x2;
-
- /**
- * When entering (scrolling on screen) the view will scroll on any downwards
- * scroll event, regardless of whether the scrolling view is also scrolling. This
- * is commonly referred to as the 'quick return' pattern.
- */
- public static final int SCROLL_FLAG_ENTER_ALWAYS = 0x4;
-
- /**
- * An additional flag for 'enterAlways' which modifies the returning view to
- * only initially scroll back to it's collapsed height. Once the scrolling view has
- * reached the end of it's scroll range, the remainder of this view will be scrolled
- * into view. The collapsed height is defined by the view's minimum height.
- *
- * @see ViewCompat#getMinimumHeight(View)
- * @see View#setMinimumHeight(int)
- */
- public static final int SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED = 0x8;
-
- /**
- * Upon a scroll ending, if the view is only partially visible then it will be snapped
- * and scrolled to it's closest edge. For example, if the view only has it's bottom 25%
- * displayed, it will be scrolled off screen completely. Conversely, if it's bottom 75%
- * is visible then it will be scrolled fully into view.
- */
- public static final int SCROLL_FLAG_SNAP = 0x10;
-
- /**
- * Internal flags which allows quick checking features
- */
- static final int FLAG_QUICK_RETURN = SCROLL_FLAG_SCROLL | SCROLL_FLAG_ENTER_ALWAYS;
- static final int FLAG_SNAP = SCROLL_FLAG_SCROLL | SCROLL_FLAG_SNAP;
- static final int COLLAPSIBLE_FLAGS = SCROLL_FLAG_EXIT_UNTIL_COLLAPSED
- | SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED;
-
- int mScrollFlags = SCROLL_FLAG_SCROLL;
- Interpolator mScrollInterpolator;
-
- public LayoutParams(Context c, AttributeSet attrs) {
- super(c, attrs);
- TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.AppBarLayout_Layout);
- mScrollFlags = a.getInt(R.styleable.AppBarLayout_Layout_layout_scrollFlags, 0);
- if (a.hasValue(R.styleable.AppBarLayout_Layout_layout_scrollInterpolator)) {
- int resId = a.getResourceId(
- R.styleable.AppBarLayout_Layout_layout_scrollInterpolator, 0);
- mScrollInterpolator = android.view.animation.AnimationUtils.loadInterpolator(
- c, resId);
- }
- a.recycle();
- }
-
- public LayoutParams(int width, int height) {
- super(width, height);
- }
-
- public LayoutParams(int width, int height, float weight) {
- super(width, height, weight);
- }
-
- public LayoutParams(ViewGroup.LayoutParams p) {
- super(p);
- }
-
- public LayoutParams(MarginLayoutParams source) {
- super(source);
- }
-
- @RequiresApi(19)
- public LayoutParams(LinearLayout.LayoutParams source) {
- // The copy constructor called here only exists on API 19+.
- super(source);
- }
-
- @RequiresApi(19)
- public LayoutParams(LayoutParams source) {
- // The copy constructor called here only exists on API 19+.
- super(source);
- mScrollFlags = source.mScrollFlags;
- mScrollInterpolator = source.mScrollInterpolator;
- }
-
- /**
- * Set the scrolling flags.
- *
- * @param flags bitwise int of {@link #SCROLL_FLAG_SCROLL},
- * {@link #SCROLL_FLAG_EXIT_UNTIL_COLLAPSED}, {@link #SCROLL_FLAG_ENTER_ALWAYS},
- * {@link #SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED} and {@link #SCROLL_FLAG_SNAP }.
- *
- * @see #getScrollFlags()
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollFlags
- */
- public void setScrollFlags(@ScrollFlags int flags) {
- mScrollFlags = flags;
- }
-
- /**
- * Returns the scrolling flags.
- *
- * @see #setScrollFlags(int)
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollFlags
- */
- @ScrollFlags
- public int getScrollFlags() {
- return mScrollFlags;
- }
-
- /**
- * Set the interpolator to when scrolling the view associated with this
- * {@link LayoutParams}.
- *
- * @param interpolator the interpolator to use, or null to use normal 1-to-1 scrolling.
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollInterpolator
- * @see #getScrollInterpolator()
- */
- public void setScrollInterpolator(Interpolator interpolator) {
- mScrollInterpolator = interpolator;
- }
-
- /**
- * Returns the {@link Interpolator} being used for scrolling the view associated with this
- * {@link LayoutParams}. Null indicates 'normal' 1-to-1 scrolling.
- *
- * @attr ref android.support.design.R.styleable#AppBarLayout_Layout_layout_scrollInterpolator
- * @see #setScrollInterpolator(Interpolator)
- */
- public Interpolator getScrollInterpolator() {
- return mScrollInterpolator;
- }
-
- /**
- * Returns true if the scroll flags are compatible for 'collapsing'
- */
- boolean isCollapsible() {
- return (mScrollFlags & SCROLL_FLAG_SCROLL) == SCROLL_FLAG_SCROLL
- && (mScrollFlags & COLLAPSIBLE_FLAGS) != 0;
- }
- }
-
- /**
- * The default {@link Behavior} for {@link AppBarLayout}. Implements the necessary nested
- * scroll handling with offsetting.
- */
- public static class Behavior extends HeaderBehavior<AppBarLayout> {
- private static final int MAX_OFFSET_ANIMATION_DURATION = 600; // ms
- private static final int INVALID_POSITION = -1;
-
- /**
- * Callback to allow control over any {@link AppBarLayout} dragging.
- */
- public static abstract class DragCallback {
- /**
- * Allows control over whether the given {@link AppBarLayout} can be dragged or not.
- *
- * <p>Dragging is defined as a direct touch on the AppBarLayout with movement. This
- * call does not affect any nested scrolling.</p>
- *
- * @return true if we are in a position to scroll the AppBarLayout via a drag, false
- * if not.
- */
- public abstract boolean canDrag(@NonNull AppBarLayout appBarLayout);
- }
-
- private int mOffsetDelta;
- private ValueAnimator mOffsetAnimator;
-
- private int mOffsetToChildIndexOnLayout = INVALID_POSITION;
- private boolean mOffsetToChildIndexOnLayoutIsMinHeight;
- private float mOffsetToChildIndexOnLayoutPerc;
-
- private WeakReference<View> mLastNestedScrollingChildRef;
- private DragCallback mOnDragCallback;
-
- public Behavior() {}
-
- public Behavior(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public boolean onStartNestedScroll(CoordinatorLayout parent, AppBarLayout child,
- View directTargetChild, View target, int nestedScrollAxes, int type) {
- // Return true if we're nested scrolling vertically, and we have scrollable children
- // and the scrolling view is big enough to scroll
- final boolean started = (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0
- && child.hasScrollableChildren()
- && parent.getHeight() - directTargetChild.getHeight() <= child.getHeight();
-
- if (started && mOffsetAnimator != null) {
- // Cancel any offset animation
- mOffsetAnimator.cancel();
- }
-
- // A new nested scroll has started so clear out the previous ref
- mLastNestedScrollingChildRef = null;
-
- return started;
- }
-
- @Override
- public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
- View target, int dx, int dy, int[] consumed, int type) {
- if (dy != 0) {
- int min, max;
- if (dy < 0) {
- // We're scrolling down
- min = -child.getTotalScrollRange();
- max = min + child.getDownNestedPreScrollRange();
- } else {
- // We're scrolling up
- min = -child.getUpNestedPreScrollRange();
- max = 0;
- }
- if (min != max) {
- consumed[1] = scroll(coordinatorLayout, child, dy, min, max);
- }
- }
- }
-
- @Override
- public void onNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout child,
- View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed,
- int type) {
- if (dyUnconsumed < 0) {
- // If the scrolling view is scrolling down but not consuming, it's probably be at
- // the top of it's content
- scroll(coordinatorLayout, child, dyUnconsumed,
- -child.getDownNestedScrollRange(), 0);
- }
- }
-
- @Override
- public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, AppBarLayout abl,
- View target, int type) {
- if (type == ViewCompat.TYPE_TOUCH) {
- // If we haven't been flung then let's see if the current view has been set to snap
- snapToChildIfNeeded(coordinatorLayout, abl);
- }
-
- // Keep a reference to the previous nested scrolling child
- mLastNestedScrollingChildRef = new WeakReference<>(target);
- }
-
- /**
- * Set a callback to control any {@link AppBarLayout} dragging.
- *
- * @param callback the callback to use, or {@code null} to use the default behavior.
- */
- public void setDragCallback(@Nullable DragCallback callback) {
- mOnDragCallback = callback;
- }
-
- private void animateOffsetTo(final CoordinatorLayout coordinatorLayout,
- final AppBarLayout child, final int offset, float velocity) {
- final int distance = Math.abs(getTopBottomOffsetForScrollingSibling() - offset);
-
- final int duration;
- velocity = Math.abs(velocity);
- if (velocity > 0) {
- duration = 3 * Math.round(1000 * (distance / velocity));
- } else {
- final float distanceRatio = (float) distance / child.getHeight();
- duration = (int) ((distanceRatio + 1) * 150);
- }
-
- animateOffsetWithDuration(coordinatorLayout, child, offset, duration);
- }
-
- private void animateOffsetWithDuration(final CoordinatorLayout coordinatorLayout,
- final AppBarLayout child, final int offset, final int duration) {
- final int currentOffset = getTopBottomOffsetForScrollingSibling();
- if (currentOffset == offset) {
- if (mOffsetAnimator != null && mOffsetAnimator.isRunning()) {
- mOffsetAnimator.cancel();
- }
- return;
- }
-
- if (mOffsetAnimator == null) {
- mOffsetAnimator = new ValueAnimator();
- mOffsetAnimator.setInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
- mOffsetAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animation) {
- setHeaderTopBottomOffset(coordinatorLayout, child,
- (int) animation.getAnimatedValue());
- }
- });
- } else {
- mOffsetAnimator.cancel();
- }
-
- mOffsetAnimator.setDuration(Math.min(duration, MAX_OFFSET_ANIMATION_DURATION));
- mOffsetAnimator.setIntValues(currentOffset, offset);
- mOffsetAnimator.start();
- }
-
- private int getChildIndexOnOffset(AppBarLayout abl, final int offset) {
- for (int i = 0, count = abl.getChildCount(); i < count; i++) {
- View child = abl.getChildAt(i);
- if (child.getTop() <= -offset && child.getBottom() >= -offset) {
- return i;
- }
- }
- return -1;
- }
-
- private void snapToChildIfNeeded(CoordinatorLayout coordinatorLayout, AppBarLayout abl) {
- final int offset = getTopBottomOffsetForScrollingSibling();
- final int offsetChildIndex = getChildIndexOnOffset(abl, offset);
- if (offsetChildIndex >= 0) {
- final View offsetChild = abl.getChildAt(offsetChildIndex);
- final LayoutParams lp = (LayoutParams) offsetChild.getLayoutParams();
- final int flags = lp.getScrollFlags();
-
- if ((flags & LayoutParams.FLAG_SNAP) == LayoutParams.FLAG_SNAP) {
- // We're set the snap, so animate the offset to the nearest edge
- int snapTop = -offsetChild.getTop();
- int snapBottom = -offsetChild.getBottom();
-
- if (offsetChildIndex == abl.getChildCount() - 1) {
- // If this is the last child, we need to take the top inset into account
- snapBottom += abl.getTopInset();
- }
-
- if (checkFlag(flags, LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED)) {
- // If the view is set only exit until it is collapsed, we'll abide by that
- snapBottom += ViewCompat.getMinimumHeight(offsetChild);
- } else if (checkFlag(flags, LayoutParams.FLAG_QUICK_RETURN
- | LayoutParams.SCROLL_FLAG_ENTER_ALWAYS)) {
- // If it's set to always enter collapsed, it actually has two states. We
- // select the state and then snap within the state
- final int seam = snapBottom + ViewCompat.getMinimumHeight(offsetChild);
- if (offset < seam) {
- snapTop = seam;
- } else {
- snapBottom = seam;
- }
- }
-
- final int newOffset = offset < (snapBottom + snapTop) / 2
- ? snapBottom
- : snapTop;
- animateOffsetTo(coordinatorLayout, abl,
- MathUtils.clamp(newOffset, -abl.getTotalScrollRange(), 0), 0);
- }
- }
- }
-
- private static boolean checkFlag(final int flags, final int check) {
- return (flags & check) == check;
- }
-
- @Override
- public boolean onMeasureChild(CoordinatorLayout parent, AppBarLayout child,
- int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
- int heightUsed) {
- final CoordinatorLayout.LayoutParams lp =
- (CoordinatorLayout.LayoutParams) child.getLayoutParams();
- if (lp.height == CoordinatorLayout.LayoutParams.WRAP_CONTENT) {
- // If the view is set to wrap on it's height, CoordinatorLayout by default will
- // cap the view at the CoL's height. Since the AppBarLayout can scroll, this isn't
- // what we actually want, so we measure it ourselves with an unspecified spec to
- // allow the child to be larger than it's parent
- parent.onMeasureChild(child, parentWidthMeasureSpec, widthUsed,
- MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), heightUsed);
- return true;
- }
-
- // Let the parent handle it as normal
- return super.onMeasureChild(parent, child, parentWidthMeasureSpec, widthUsed,
- parentHeightMeasureSpec, heightUsed);
- }
-
- @Override
- public boolean onLayoutChild(CoordinatorLayout parent, AppBarLayout abl,
- int layoutDirection) {
- boolean handled = super.onLayoutChild(parent, abl, layoutDirection);
-
- // The priority for for actions here is (first which is true wins):
- // 1. forced pending actions
- // 2. offsets for restorations
- // 3. non-forced pending actions
- final int pendingAction = abl.getPendingAction();
- if (mOffsetToChildIndexOnLayout >= 0 && (pendingAction & PENDING_ACTION_FORCE) == 0) {
- View child = abl.getChildAt(mOffsetToChildIndexOnLayout);
- int offset = -child.getBottom();
- if (mOffsetToChildIndexOnLayoutIsMinHeight) {
- offset += ViewCompat.getMinimumHeight(child) + abl.getTopInset();
- } else {
- offset += Math.round(child.getHeight() * mOffsetToChildIndexOnLayoutPerc);
- }
- setHeaderTopBottomOffset(parent, abl, offset);
- } else if (pendingAction != PENDING_ACTION_NONE) {
- final boolean animate = (pendingAction & PENDING_ACTION_ANIMATE_ENABLED) != 0;
- if ((pendingAction & PENDING_ACTION_COLLAPSED) != 0) {
- final int offset = -abl.getUpNestedPreScrollRange();
- if (animate) {
- animateOffsetTo(parent, abl, offset, 0);
- } else {
- setHeaderTopBottomOffset(parent, abl, offset);
- }
- } else if ((pendingAction & PENDING_ACTION_EXPANDED) != 0) {
- if (animate) {
- animateOffsetTo(parent, abl, 0, 0);
- } else {
- setHeaderTopBottomOffset(parent, abl, 0);
- }
- }
- }
-
- // Finally reset any pending states
- abl.resetPendingAction();
- mOffsetToChildIndexOnLayout = INVALID_POSITION;
-
- // We may have changed size, so let's constrain the top and bottom offset correctly,
- // just in case we're out of the bounds
- setTopAndBottomOffset(
- MathUtils.clamp(getTopAndBottomOffset(), -abl.getTotalScrollRange(), 0));
-
- // Update the AppBarLayout's drawable state for any elevation changes.
- // This is needed so that the elevation is set in the first layout, so that
- // we don't get a visual elevation jump pre-N (due to the draw dispatch skip)
- updateAppBarLayoutDrawableState(parent, abl, getTopAndBottomOffset(), 0, true);
-
- // Make sure we dispatch the offset update
- abl.dispatchOffsetUpdates(getTopAndBottomOffset());
-
- return handled;
- }
-
- @Override
- boolean canDragView(AppBarLayout view) {
- if (mOnDragCallback != null) {
- // If there is a drag callback set, it's in control
- return mOnDragCallback.canDrag(view);
- }
-
- // Else we'll use the default behaviour of seeing if it can scroll down
- if (mLastNestedScrollingChildRef != null) {
- // If we have a reference to a scrolling view, check it
- final View scrollingView = mLastNestedScrollingChildRef.get();
- return scrollingView != null && scrollingView.isShown()
- && !scrollingView.canScrollVertically(-1);
- } else {
- // Otherwise we assume that the scrolling view hasn't been scrolled and can drag.
- return true;
- }
- }
-
- @Override
- void onFlingFinished(CoordinatorLayout parent, AppBarLayout layout) {
- // At the end of a manual fling, check to see if we need to snap to the edge-child
- snapToChildIfNeeded(parent, layout);
- }
-
- @Override
- int getMaxDragOffset(AppBarLayout view) {
- return -view.getDownNestedScrollRange();
- }
-
- @Override
- int getScrollRangeForDragFling(AppBarLayout view) {
- return view.getTotalScrollRange();
- }
-
- @Override
- int setHeaderTopBottomOffset(CoordinatorLayout coordinatorLayout,
- AppBarLayout appBarLayout, int newOffset, int minOffset, int maxOffset) {
- final int curOffset = getTopBottomOffsetForScrollingSibling();
- int consumed = 0;
-
- if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
- // If we have some scrolling range, and we're currently within the min and max
- // offsets, calculate a new offset
- newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
- if (curOffset != newOffset) {
- final int interpolatedOffset = appBarLayout.hasChildWithInterpolator()
- ? interpolateOffset(appBarLayout, newOffset)
- : newOffset;
-
- final boolean offsetChanged = setTopAndBottomOffset(interpolatedOffset);
-
- // Update how much dy we have consumed
- consumed = curOffset - newOffset;
- // Update the stored sibling offset
- mOffsetDelta = newOffset - interpolatedOffset;
-
- if (!offsetChanged && appBarLayout.hasChildWithInterpolator()) {
- // If the offset hasn't changed and we're using an interpolated scroll
- // then we need to keep any dependent views updated. CoL will do this for
- // us when we move, but we need to do it manually when we don't (as an
- // interpolated scroll may finish early).
- coordinatorLayout.dispatchDependentViewsChanged(appBarLayout);
- }
-
- // Dispatch the updates to any listeners
- appBarLayout.dispatchOffsetUpdates(getTopAndBottomOffset());
-
- // Update the AppBarLayout's drawable state (for any elevation changes)
- updateAppBarLayoutDrawableState(coordinatorLayout, appBarLayout, newOffset,
- newOffset < curOffset ? -1 : 1, false);
- }
- } else {
- // Reset the offset delta
- mOffsetDelta = 0;
- }
-
- return consumed;
- }
-
- @VisibleForTesting
- boolean isOffsetAnimatorRunning() {
- return mOffsetAnimator != null && mOffsetAnimator.isRunning();
- }
-
- private int interpolateOffset(AppBarLayout layout, final int offset) {
- final int absOffset = Math.abs(offset);
-
- for (int i = 0, z = layout.getChildCount(); i < z; i++) {
- final View child = layout.getChildAt(i);
- final AppBarLayout.LayoutParams childLp = (LayoutParams) child.getLayoutParams();
- final Interpolator interpolator = childLp.getScrollInterpolator();
-
- if (absOffset >= child.getTop() && absOffset <= child.getBottom()) {
- if (interpolator != null) {
- int childScrollableHeight = 0;
- final int flags = childLp.getScrollFlags();
- if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
- // We're set to scroll so add the child's height plus margin
- childScrollableHeight += child.getHeight() + childLp.topMargin
- + childLp.bottomMargin;
-
- if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
- // For a collapsing scroll, we to take the collapsed height
- // into account.
- childScrollableHeight -= ViewCompat.getMinimumHeight(child);
- }
- }
-
- if (ViewCompat.getFitsSystemWindows(child)) {
- childScrollableHeight -= layout.getTopInset();
- }
-
- if (childScrollableHeight > 0) {
- final int offsetForView = absOffset - child.getTop();
- final int interpolatedDiff = Math.round(childScrollableHeight *
- interpolator.getInterpolation(
- offsetForView / (float) childScrollableHeight));
-
- return Integer.signum(offset) * (child.getTop() + interpolatedDiff);
- }
- }
-
- // If we get to here then the view on the offset isn't suitable for interpolated
- // scrolling. So break out of the loop
- break;
- }
- }
-
- return offset;
- }
-
- private void updateAppBarLayoutDrawableState(final CoordinatorLayout parent,
- final AppBarLayout layout, final int offset, final int direction,
- final boolean forceJump) {
- final View child = getAppBarChildOnOffset(layout, offset);
- if (child != null) {
- final AppBarLayout.LayoutParams childLp = (LayoutParams) child.getLayoutParams();
- final int flags = childLp.getScrollFlags();
- boolean collapsed = false;
-
- if ((flags & LayoutParams.SCROLL_FLAG_SCROLL) != 0) {
- final int minHeight = ViewCompat.getMinimumHeight(child);
-
- if (direction > 0 && (flags & (LayoutParams.SCROLL_FLAG_ENTER_ALWAYS
- | LayoutParams.SCROLL_FLAG_ENTER_ALWAYS_COLLAPSED)) != 0) {
- // We're set to enter always collapsed so we are only collapsed when
- // being scrolled down, and in a collapsed offset
- collapsed = -offset >= child.getBottom() - minHeight - layout.getTopInset();
- } else if ((flags & LayoutParams.SCROLL_FLAG_EXIT_UNTIL_COLLAPSED) != 0) {
- // We're set to exit until collapsed, so any offset which results in
- // the minimum height (or less) being shown is collapsed
- collapsed = -offset >= child.getBottom() - minHeight - layout.getTopInset();
- }
- }
-
- final boolean changed = layout.setCollapsedState(collapsed);
-
- if (Build.VERSION.SDK_INT >= 11 && (forceJump
- || (changed && shouldJumpElevationState(parent, layout)))) {
- // If the collapsed state changed, we may need to
- // jump to the current state if we have an overlapping view
- layout.jumpDrawablesToCurrentState();
- }
- }
- }
-
- private boolean shouldJumpElevationState(CoordinatorLayout parent, AppBarLayout layout) {
- // We should jump the elevated state if we have a dependent scrolling view which has
- // an overlapping top (i.e. overlaps us)
- final List<View> dependencies = parent.getDependents(layout);
- for (int i = 0, size = dependencies.size(); i < size; i++) {
- final View dependency = dependencies.get(i);
- final CoordinatorLayout.LayoutParams lp =
- (CoordinatorLayout.LayoutParams) dependency.getLayoutParams();
- final CoordinatorLayout.Behavior behavior = lp.getBehavior();
-
- if (behavior instanceof ScrollingViewBehavior) {
- return ((ScrollingViewBehavior) behavior).getOverlayTop() != 0;
- }
- }
- return false;
- }
-
- private static View getAppBarChildOnOffset(final AppBarLayout layout, final int offset) {
- final int absOffset = Math.abs(offset);
- for (int i = 0, z = layout.getChildCount(); i < z; i++) {
- final View child = layout.getChildAt(i);
- if (absOffset >= child.getTop() && absOffset <= child.getBottom()) {
- return child;
- }
- }
- return null;
- }
-
- @Override
- int getTopBottomOffsetForScrollingSibling() {
- return getTopAndBottomOffset() + mOffsetDelta;
- }
-
- @Override
- public Parcelable onSaveInstanceState(CoordinatorLayout parent, AppBarLayout abl) {
- final Parcelable superState = super.onSaveInstanceState(parent, abl);
- final int offset = getTopAndBottomOffset();
-
- // Try and find the first visible child...
- for (int i = 0, count = abl.getChildCount(); i < count; i++) {
- View child = abl.getChildAt(i);
- final int visBottom = child.getBottom() + offset;
-
- if (child.getTop() + offset <= 0 && visBottom >= 0) {
- final SavedState ss = new SavedState(superState);
- ss.firstVisibleChildIndex = i;
- ss.firstVisibleChildAtMinimumHeight =
- visBottom == (ViewCompat.getMinimumHeight(child) + abl.getTopInset());
- ss.firstVisibleChildPercentageShown = visBottom / (float) child.getHeight();
- return ss;
- }
- }
-
- // Else we'll just return the super state
- return superState;
- }
-
- @Override
- public void onRestoreInstanceState(CoordinatorLayout parent, AppBarLayout appBarLayout,
- Parcelable state) {
- if (state instanceof SavedState) {
- final SavedState ss = (SavedState) state;
- super.onRestoreInstanceState(parent, appBarLayout, ss.getSuperState());
- mOffsetToChildIndexOnLayout = ss.firstVisibleChildIndex;
- mOffsetToChildIndexOnLayoutPerc = ss.firstVisibleChildPercentageShown;
- mOffsetToChildIndexOnLayoutIsMinHeight = ss.firstVisibleChildAtMinimumHeight;
- } else {
- super.onRestoreInstanceState(parent, appBarLayout, state);
- mOffsetToChildIndexOnLayout = INVALID_POSITION;
- }
- }
-
- protected static class SavedState extends AbsSavedState {
- int firstVisibleChildIndex;
- float firstVisibleChildPercentageShown;
- boolean firstVisibleChildAtMinimumHeight;
-
- public SavedState(Parcel source, ClassLoader loader) {
- super(source, loader);
- firstVisibleChildIndex = source.readInt();
- firstVisibleChildPercentageShown = source.readFloat();
- firstVisibleChildAtMinimumHeight = source.readByte() != 0;
- }
-
- public SavedState(Parcelable superState) {
- super(superState);
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- super.writeToParcel(dest, flags);
- dest.writeInt(firstVisibleChildIndex);
- dest.writeFloat(firstVisibleChildPercentageShown);
- dest.writeByte((byte) (firstVisibleChildAtMinimumHeight ? 1 : 0));
- }
-
- public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel source, ClassLoader loader) {
- return new SavedState(source, loader);
- }
-
- @Override
- public SavedState createFromParcel(Parcel source) {
- return new SavedState(source, null);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
- }
-
- /**
- * Behavior which should be used by {@link View}s which can scroll vertically and support
- * nested scrolling to automatically scroll any {@link AppBarLayout} siblings.
- */
- public static class ScrollingViewBehavior extends HeaderScrollingViewBehavior {
-
- public ScrollingViewBehavior() {}
-
- public ScrollingViewBehavior(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- final TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.ScrollingViewBehavior_Layout);
- setOverlayTop(a.getDimensionPixelSize(
- R.styleable.ScrollingViewBehavior_Layout_behavior_overlapTop, 0));
- a.recycle();
- }
-
- @Override
- public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {
- // We depend on any AppBarLayouts
- return dependency instanceof AppBarLayout;
- }
-
- @Override
- public boolean onDependentViewChanged(CoordinatorLayout parent, View child,
- View dependency) {
- offsetChildAsNeeded(parent, child, dependency);
- return false;
- }
-
- @Override
- public boolean onRequestChildRectangleOnScreen(CoordinatorLayout parent, View child,
- Rect rectangle, boolean immediate) {
- final AppBarLayout header = findFirstDependency(parent.getDependencies(child));
- if (header != null) {
- // Offset the rect by the child's left/top
- rectangle.offset(child.getLeft(), child.getTop());
-
- final Rect parentRect = mTempRect1;
- parentRect.set(0, 0, parent.getWidth(), parent.getHeight());
-
- if (!parentRect.contains(rectangle)) {
- // If the rectangle can not be fully seen the visible bounds, collapse
- // the AppBarLayout
- header.setExpanded(false, !immediate);
- return true;
- }
- }
- return false;
- }
-
- private void offsetChildAsNeeded(CoordinatorLayout parent, View child, View dependency) {
- final CoordinatorLayout.Behavior behavior =
- ((CoordinatorLayout.LayoutParams) dependency.getLayoutParams()).getBehavior();
- if (behavior instanceof Behavior) {
- // Offset the child, pinning it to the bottom the header-dependency, maintaining
- // any vertical gap and overlap
- final Behavior ablBehavior = (Behavior) behavior;
- ViewCompat.offsetTopAndBottom(child, (dependency.getBottom() - child.getTop())
- + ablBehavior.mOffsetDelta
- + getVerticalLayoutGap()
- - getOverlapPixelsForOffset(dependency));
- }
- }
-
- @Override
- float getOverlapRatioForOffset(final View header) {
- if (header instanceof AppBarLayout) {
- final AppBarLayout abl = (AppBarLayout) header;
- final int totalScrollRange = abl.getTotalScrollRange();
- final int preScrollDown = abl.getDownNestedPreScrollRange();
- final int offset = getAppBarLayoutOffset(abl);
-
- if (preScrollDown != 0 && (totalScrollRange + offset) <= preScrollDown) {
- // If we're in a pre-scroll down. Don't use the offset at all.
- return 0;
- } else {
- final int availScrollRange = totalScrollRange - preScrollDown;
- if (availScrollRange != 0) {
- // Else we'll use a interpolated ratio of the overlap, depending on offset
- return 1f + (offset / (float) availScrollRange);
- }
- }
- }
- return 0f;
- }
-
- private static int getAppBarLayoutOffset(AppBarLayout abl) {
- final CoordinatorLayout.Behavior behavior =
- ((CoordinatorLayout.LayoutParams) abl.getLayoutParams()).getBehavior();
- if (behavior instanceof Behavior) {
- return ((Behavior) behavior).getTopBottomOffsetForScrollingSibling();
- }
- return 0;
- }
-
- @Override
- AppBarLayout findFirstDependency(List<View> views) {
- for (int i = 0, z = views.size(); i < z; i++) {
- View view = views.get(i);
- if (view instanceof AppBarLayout) {
- return (AppBarLayout) view;
- }
- }
- return null;
- }
-
- @Override
- int getScrollRange(View v) {
- if (v instanceof AppBarLayout) {
- return ((AppBarLayout) v).getTotalScrollRange();
- } else {
- return super.getScrollRange(v);
- }
- }
- }
-}
diff --git a/android/support/design/widget/BaseTransientBottomBar.java b/android/support/design/widget/BaseTransientBottomBar.java
deleted file mode 100644
index 18c9ef9d..00000000
--- a/android/support/design/widget/BaseTransientBottomBar.java
+++ /dev/null
@@ -1,753 +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 android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static android.support.design.widget.AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.os.Build;
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-import android.support.annotation.IntDef;
-import android.support.annotation.IntRange;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.design.R;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.view.accessibility.AccessibilityManager;
-import android.view.animation.Animation;
-import android.view.animation.AnimationUtils;
-import android.widget.FrameLayout;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.List;
-
-/**
- * Base class for lightweight transient bars that are displayed along the bottom edge of the
- * application window.
- *
- * @param <B> The transient bottom bar subclass.
- */
-public abstract class BaseTransientBottomBar<B extends BaseTransientBottomBar<B>> {
- /**
- * Base class for {@link BaseTransientBottomBar} callbacks.
- *
- * @param <B> The transient bottom bar subclass.
- * @see BaseTransientBottomBar#addCallback(BaseCallback)
- */
- public abstract static class BaseCallback<B> {
- /** Indicates that the Snackbar was dismissed via a swipe.*/
- public static final int DISMISS_EVENT_SWIPE = 0;
- /** Indicates that the Snackbar was dismissed via an action click.*/
- public static final int DISMISS_EVENT_ACTION = 1;
- /** Indicates that the Snackbar was dismissed via a timeout.*/
- public static final int DISMISS_EVENT_TIMEOUT = 2;
- /** Indicates that the Snackbar was dismissed via a call to {@link #dismiss()}.*/
- public static final int DISMISS_EVENT_MANUAL = 3;
- /** Indicates that the Snackbar was dismissed from a new Snackbar being shown.*/
- public static final int DISMISS_EVENT_CONSECUTIVE = 4;
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef({DISMISS_EVENT_SWIPE, DISMISS_EVENT_ACTION, DISMISS_EVENT_TIMEOUT,
- DISMISS_EVENT_MANUAL, DISMISS_EVENT_CONSECUTIVE})
- @Retention(RetentionPolicy.SOURCE)
- public @interface DismissEvent {}
-
- /**
- * Called when the given {@link BaseTransientBottomBar} has been dismissed, either
- * through a time-out, having been manually dismissed, or an action being clicked.
- *
- * @param transientBottomBar The transient bottom bar which has been dismissed.
- * @param event The event which caused the dismissal. One of either:
- * {@link #DISMISS_EVENT_SWIPE}, {@link #DISMISS_EVENT_ACTION},
- * {@link #DISMISS_EVENT_TIMEOUT}, {@link #DISMISS_EVENT_MANUAL} or
- * {@link #DISMISS_EVENT_CONSECUTIVE}.
- *
- * @see BaseTransientBottomBar#dismiss()
- */
- public void onDismissed(B transientBottomBar, @DismissEvent int event) {
- // empty
- }
-
- /**
- * Called when the given {@link BaseTransientBottomBar} is visible.
- *
- * @param transientBottomBar The transient bottom bar which is now visible.
- * @see BaseTransientBottomBar#show()
- */
- public void onShown(B transientBottomBar) {
- // empty
- }
- }
-
- /**
- * Interface that defines the behavior of the main content of a transient bottom bar.
- */
- public interface ContentViewCallback {
- /**
- * Animates the content of the transient bottom bar in.
- *
- * @param delay Animation delay.
- * @param duration Animation duration.
- */
- void animateContentIn(int delay, int duration);
-
- /**
- * Animates the content of the transient bottom bar out.
- *
- * @param delay Animation delay.
- * @param duration Animation duration.
- */
- void animateContentOut(int delay, int duration);
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef({LENGTH_INDEFINITE, LENGTH_SHORT, LENGTH_LONG})
- @IntRange(from = 1)
- @Retention(RetentionPolicy.SOURCE)
- public @interface Duration {}
-
- /**
- * Show the Snackbar indefinitely. This means that the Snackbar will be displayed from the time
- * that is {@link #show() shown} until either it is dismissed, or another Snackbar is shown.
- *
- * @see #setDuration
- */
- public static final int LENGTH_INDEFINITE = -2;
-
- /**
- * Show the Snackbar for a short period of time.
- *
- * @see #setDuration
- */
- public static final int LENGTH_SHORT = -1;
-
- /**
- * Show the Snackbar for a long period of time.
- *
- * @see #setDuration
- */
- public static final int LENGTH_LONG = 0;
-
- static final int ANIMATION_DURATION = 250;
- static final int ANIMATION_FADE_DURATION = 180;
-
- static final Handler sHandler;
- static final int MSG_SHOW = 0;
- static final int MSG_DISMISS = 1;
-
- // On JB/KK versions of the platform sometimes View.setTranslationY does not
- // result in layout / draw pass, and CoordinatorLayout relies on a draw pass to
- // happen to sync vertical positioning of all its child views
- private static final boolean USE_OFFSET_API = (Build.VERSION.SDK_INT >= 16)
- && (Build.VERSION.SDK_INT <= 19);
-
- static {
- sHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
- @Override
- public boolean handleMessage(Message message) {
- switch (message.what) {
- case MSG_SHOW:
- ((BaseTransientBottomBar) message.obj).showView();
- return true;
- case MSG_DISMISS:
- ((BaseTransientBottomBar) message.obj).hideView(message.arg1);
- return true;
- }
- return false;
- }
- });
- }
-
- private final ViewGroup mTargetParent;
- private final Context mContext;
- final SnackbarBaseLayout mView;
- private final ContentViewCallback mContentViewCallback;
- private int mDuration;
-
- private List<BaseCallback<B>> mCallbacks;
-
- private final AccessibilityManager mAccessibilityManager;
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- interface OnLayoutChangeListener {
- void onLayoutChange(View view, int left, int top, int right, int bottom);
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- interface OnAttachStateChangeListener {
- void onViewAttachedToWindow(View v);
- void onViewDetachedFromWindow(View v);
- }
-
- /**
- * Constructor for the transient bottom bar.
- *
- * @param parent The parent for this transient bottom bar.
- * @param content The content view for this transient bottom bar.
- * @param contentViewCallback The content view callback for this transient bottom bar.
- */
- protected BaseTransientBottomBar(@NonNull ViewGroup parent, @NonNull View content,
- @NonNull ContentViewCallback contentViewCallback) {
- if (parent == null) {
- throw new IllegalArgumentException("Transient bottom bar must have non-null parent");
- }
- if (content == null) {
- throw new IllegalArgumentException("Transient bottom bar must have non-null content");
- }
- if (contentViewCallback == null) {
- throw new IllegalArgumentException("Transient bottom bar must have non-null callback");
- }
-
- mTargetParent = parent;
- mContentViewCallback = contentViewCallback;
- mContext = parent.getContext();
-
- ThemeUtils.checkAppCompatTheme(mContext);
-
- LayoutInflater inflater = LayoutInflater.from(mContext);
- // Note that for backwards compatibility reasons we inflate a layout that is defined
- // in the extending Snackbar class. This is to prevent breakage of apps that have custom
- // coordinator layout behaviors that depend on that layout.
- mView = (SnackbarBaseLayout) inflater.inflate(
- R.layout.design_layout_snackbar, mTargetParent, false);
- mView.addView(content);
-
- ViewCompat.setAccessibilityLiveRegion(mView,
- ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
- ViewCompat.setImportantForAccessibility(mView,
- ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
-
- // Make sure that we fit system windows and have a listener to apply any insets
- ViewCompat.setFitsSystemWindows(mView, true);
- ViewCompat.setOnApplyWindowInsetsListener(mView,
- new android.support.v4.view.OnApplyWindowInsetsListener() {
- @Override
- public WindowInsetsCompat onApplyWindowInsets(View v,
- WindowInsetsCompat insets) {
- // Copy over the bottom inset as padding so that we're displayed
- // above the navigation bar
- v.setPadding(v.getPaddingLeft(), v.getPaddingTop(),
- v.getPaddingRight(), insets.getSystemWindowInsetBottom());
- return insets;
- }
- });
-
- mAccessibilityManager = (AccessibilityManager)
- mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
- }
-
- /**
- * Set how long to show the view for.
- *
- * @param duration either be one of the predefined lengths:
- * {@link #LENGTH_SHORT}, {@link #LENGTH_LONG}, or a custom duration
- * in milliseconds.
- */
- @NonNull
- public B setDuration(@Duration int duration) {
- mDuration = duration;
- return (B) this;
- }
-
- /**
- * Return the duration.
- *
- * @see #setDuration
- */
- @Duration
- public int getDuration() {
- return mDuration;
- }
-
- /**
- * Returns the {@link BaseTransientBottomBar}'s context.
- */
- @NonNull
- public Context getContext() {
- return mContext;
- }
-
- /**
- * Returns the {@link BaseTransientBottomBar}'s view.
- */
- @NonNull
- public View getView() {
- return mView;
- }
-
- /**
- * Show the {@link BaseTransientBottomBar}.
- */
- public void show() {
- SnackbarManager.getInstance().show(mDuration, mManagerCallback);
- }
-
- /**
- * Dismiss the {@link BaseTransientBottomBar}.
- */
- public void dismiss() {
- dispatchDismiss(BaseCallback.DISMISS_EVENT_MANUAL);
- }
-
- void dispatchDismiss(@BaseCallback.DismissEvent int event) {
- SnackbarManager.getInstance().dismiss(mManagerCallback, event);
- }
-
- /**
- * Adds the specified callback to the list of callbacks that will be notified of transient
- * bottom bar events.
- *
- * @param callback Callback to notify when transient bottom bar events occur.
- * @see #removeCallback(BaseCallback)
- */
- @NonNull
- public B addCallback(@NonNull BaseCallback<B> callback) {
- if (callback == null) {
- return (B) this;
- }
- if (mCallbacks == null) {
- mCallbacks = new ArrayList<BaseCallback<B>>();
- }
- mCallbacks.add(callback);
- return (B) this;
- }
-
- /**
- * Removes the specified callback from the list of callbacks that will be notified of transient
- * bottom bar events.
- *
- * @param callback Callback to remove from being notified of transient bottom bar events
- * @see #addCallback(BaseCallback)
- */
- @NonNull
- public B removeCallback(@NonNull BaseCallback<B> callback) {
- if (callback == null) {
- return (B) this;
- }
- if (mCallbacks == null) {
- // This can happen if this method is called before the first call to addCallback
- return (B) this;
- }
- mCallbacks.remove(callback);
- return (B) this;
- }
-
- /**
- * Return whether this {@link BaseTransientBottomBar} is currently being shown.
- */
- public boolean isShown() {
- return SnackbarManager.getInstance().isCurrent(mManagerCallback);
- }
-
- /**
- * Returns whether this {@link BaseTransientBottomBar} is currently being shown, or is queued
- * to be shown next.
- */
- public boolean isShownOrQueued() {
- return SnackbarManager.getInstance().isCurrentOrNext(mManagerCallback);
- }
-
- final SnackbarManager.Callback mManagerCallback = new SnackbarManager.Callback() {
- @Override
- public void show() {
- sHandler.sendMessage(sHandler.obtainMessage(MSG_SHOW, BaseTransientBottomBar.this));
- }
-
- @Override
- public void dismiss(int event) {
- sHandler.sendMessage(sHandler.obtainMessage(MSG_DISMISS, event, 0,
- BaseTransientBottomBar.this));
- }
- };
-
- final void showView() {
- if (mView.getParent() == null) {
- final ViewGroup.LayoutParams lp = mView.getLayoutParams();
-
- if (lp instanceof CoordinatorLayout.LayoutParams) {
- // If our LayoutParams are from a CoordinatorLayout, we'll setup our Behavior
- final CoordinatorLayout.LayoutParams clp = (CoordinatorLayout.LayoutParams) lp;
-
- final Behavior behavior = new Behavior();
- behavior.setStartAlphaSwipeDistance(0.1f);
- behavior.setEndAlphaSwipeDistance(0.6f);
- behavior.setSwipeDirection(SwipeDismissBehavior.SWIPE_DIRECTION_START_TO_END);
- behavior.setListener(new SwipeDismissBehavior.OnDismissListener() {
- @Override
- public void onDismiss(View view) {
- view.setVisibility(View.GONE);
- dispatchDismiss(BaseCallback.DISMISS_EVENT_SWIPE);
- }
-
- @Override
- public void onDragStateChanged(int state) {
- switch (state) {
- case SwipeDismissBehavior.STATE_DRAGGING:
- case SwipeDismissBehavior.STATE_SETTLING:
- // If the view is being dragged or settling, pause the timeout
- SnackbarManager.getInstance().pauseTimeout(mManagerCallback);
- break;
- case SwipeDismissBehavior.STATE_IDLE:
- // If the view has been released and is idle, restore the timeout
- SnackbarManager.getInstance()
- .restoreTimeoutIfPaused(mManagerCallback);
- break;
- }
- }
- });
- clp.setBehavior(behavior);
- // Also set the inset edge so that views can dodge the bar correctly
- clp.insetEdge = Gravity.BOTTOM;
- }
-
- mTargetParent.addView(mView);
- }
-
- mView.setOnAttachStateChangeListener(
- new BaseTransientBottomBar.OnAttachStateChangeListener() {
- @Override
- public void onViewAttachedToWindow(View v) {}
-
- @Override
- public void onViewDetachedFromWindow(View v) {
- if (isShownOrQueued()) {
- // If we haven't already been dismissed then this event is coming from a
- // non-user initiated action. Hence we need to make sure that we callback
- // and keep our state up to date. We need to post the call since
- // removeView() will call through to onDetachedFromWindow and thus overflow.
- sHandler.post(new Runnable() {
- @Override
- public void run() {
- onViewHidden(BaseCallback.DISMISS_EVENT_MANUAL);
- }
- });
- }
- }
- });
-
- if (ViewCompat.isLaidOut(mView)) {
- if (shouldAnimate()) {
- // If animations are enabled, animate it in
- animateViewIn();
- } else {
- // Else if anims are disabled just call back now
- onViewShown();
- }
- } else {
- // Otherwise, add one of our layout change listeners and show it in when laid out
- mView.setOnLayoutChangeListener(new BaseTransientBottomBar.OnLayoutChangeListener() {
- @Override
- public void onLayoutChange(View view, int left, int top, int right, int bottom) {
- mView.setOnLayoutChangeListener(null);
-
- if (shouldAnimate()) {
- // If animations are enabled, animate it in
- animateViewIn();
- } else {
- // Else if anims are disabled just call back now
- onViewShown();
- }
- }
- });
- }
- }
-
- void animateViewIn() {
- if (Build.VERSION.SDK_INT >= 12) {
- final int viewHeight = mView.getHeight();
- if (USE_OFFSET_API) {
- ViewCompat.offsetTopAndBottom(mView, viewHeight);
- } else {
- mView.setTranslationY(viewHeight);
- }
- final ValueAnimator animator = new ValueAnimator();
- animator.setIntValues(viewHeight, 0);
- animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
- animator.setDuration(ANIMATION_DURATION);
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- mContentViewCallback.animateContentIn(
- ANIMATION_DURATION - ANIMATION_FADE_DURATION,
- ANIMATION_FADE_DURATION);
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- onViewShown();
- }
- });
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- private int mPreviousAnimatedIntValue = viewHeight;
-
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- int currentAnimatedIntValue = (int) animator.getAnimatedValue();
- if (USE_OFFSET_API) {
- ViewCompat.offsetTopAndBottom(mView,
- currentAnimatedIntValue - mPreviousAnimatedIntValue);
- } else {
- mView.setTranslationY(currentAnimatedIntValue);
- }
- mPreviousAnimatedIntValue = currentAnimatedIntValue;
- }
- });
- animator.start();
- } else {
- final Animation anim = AnimationUtils.loadAnimation(mView.getContext(),
- R.anim.design_snackbar_in);
- anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
- anim.setDuration(ANIMATION_DURATION);
- anim.setAnimationListener(new Animation.AnimationListener() {
- @Override
- public void onAnimationEnd(Animation animation) {
- onViewShown();
- }
-
- @Override
- public void onAnimationStart(Animation animation) {}
-
- @Override
- public void onAnimationRepeat(Animation animation) {}
- });
- mView.startAnimation(anim);
- }
- }
-
- private void animateViewOut(final int event) {
- if (Build.VERSION.SDK_INT >= 12) {
- final ValueAnimator animator = new ValueAnimator();
- animator.setIntValues(0, mView.getHeight());
- animator.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
- animator.setDuration(ANIMATION_DURATION);
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- mContentViewCallback.animateContentOut(0, ANIMATION_FADE_DURATION);
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- onViewHidden(event);
- }
- });
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- private int mPreviousAnimatedIntValue = 0;
-
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- int currentAnimatedIntValue = (int) animator.getAnimatedValue();
- if (USE_OFFSET_API) {
- ViewCompat.offsetTopAndBottom(mView,
- currentAnimatedIntValue - mPreviousAnimatedIntValue);
- } else {
- mView.setTranslationY(currentAnimatedIntValue);
- }
- mPreviousAnimatedIntValue = currentAnimatedIntValue;
- }
- });
- animator.start();
- } else {
- final Animation anim = AnimationUtils.loadAnimation(mView.getContext(),
- R.anim.design_snackbar_out);
- anim.setInterpolator(FAST_OUT_SLOW_IN_INTERPOLATOR);
- anim.setDuration(ANIMATION_DURATION);
- anim.setAnimationListener(new Animation.AnimationListener() {
- @Override
- public void onAnimationEnd(Animation animation) {
- onViewHidden(event);
- }
-
- @Override
- public void onAnimationStart(Animation animation) {}
-
- @Override
- public void onAnimationRepeat(Animation animation) {}
- });
- mView.startAnimation(anim);
- }
- }
-
- final void hideView(@BaseCallback.DismissEvent final int event) {
- if (shouldAnimate() && mView.getVisibility() == View.VISIBLE) {
- animateViewOut(event);
- } else {
- // If anims are disabled or the view isn't visible, just call back now
- onViewHidden(event);
- }
- }
-
- void onViewShown() {
- SnackbarManager.getInstance().onShown(mManagerCallback);
- if (mCallbacks != null) {
- // Notify the callbacks. Do that from the end of the list so that if a callback
- // removes itself as the result of being called, it won't mess up with our iteration
- int callbackCount = mCallbacks.size();
- for (int i = callbackCount - 1; i >= 0; i--) {
- mCallbacks.get(i).onShown((B) this);
- }
- }
- }
-
- void onViewHidden(int event) {
- // First tell the SnackbarManager that it has been dismissed
- SnackbarManager.getInstance().onDismissed(mManagerCallback);
- if (mCallbacks != null) {
- // Notify the callbacks. Do that from the end of the list so that if a callback
- // removes itself as the result of being called, it won't mess up with our iteration
- int callbackCount = mCallbacks.size();
- for (int i = callbackCount - 1; i >= 0; i--) {
- mCallbacks.get(i).onDismissed((B) this, event);
- }
- }
- if (Build.VERSION.SDK_INT < 11) {
- // We need to hide the Snackbar on pre-v11 since it uses an old style Animation.
- // ViewGroup has special handling in removeView() when getAnimation() != null in
- // that it waits. This then means that the calculated insets are wrong and the
- // any dodging views do not return. We workaround it by setting the view to gone while
- // ViewGroup actually gets around to removing it.
- mView.setVisibility(View.GONE);
- }
- // Lastly, hide and remove the view from the parent (if attached)
- final ViewParent parent = mView.getParent();
- if (parent instanceof ViewGroup) {
- ((ViewGroup) parent).removeView(mView);
- }
- }
-
- /**
- * Returns true if we should animate the Snackbar view in/out.
- */
- boolean shouldAnimate() {
- return !mAccessibilityManager.isEnabled();
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- static class SnackbarBaseLayout extends FrameLayout {
- private BaseTransientBottomBar.OnLayoutChangeListener mOnLayoutChangeListener;
- private BaseTransientBottomBar.OnAttachStateChangeListener mOnAttachStateChangeListener;
-
- SnackbarBaseLayout(Context context) {
- this(context, null);
- }
-
- SnackbarBaseLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.SnackbarLayout);
- if (a.hasValue(R.styleable.SnackbarLayout_elevation)) {
- ViewCompat.setElevation(this, a.getDimensionPixelSize(
- R.styleable.SnackbarLayout_elevation, 0));
- }
- a.recycle();
-
- setClickable(true);
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
- if (mOnLayoutChangeListener != null) {
- mOnLayoutChangeListener.onLayoutChange(this, l, t, r, b);
- }
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- if (mOnAttachStateChangeListener != null) {
- mOnAttachStateChangeListener.onViewAttachedToWindow(this);
- }
-
- ViewCompat.requestApplyInsets(this);
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- if (mOnAttachStateChangeListener != null) {
- mOnAttachStateChangeListener.onViewDetachedFromWindow(this);
- }
- }
-
- void setOnLayoutChangeListener(
- BaseTransientBottomBar.OnLayoutChangeListener onLayoutChangeListener) {
- mOnLayoutChangeListener = onLayoutChangeListener;
- }
-
- void setOnAttachStateChangeListener(
- BaseTransientBottomBar.OnAttachStateChangeListener listener) {
- mOnAttachStateChangeListener = listener;
- }
- }
-
- final class Behavior extends SwipeDismissBehavior<SnackbarBaseLayout> {
- @Override
- public boolean canSwipeDismissView(View child) {
- return child instanceof SnackbarBaseLayout;
- }
-
- @Override
- public boolean onInterceptTouchEvent(CoordinatorLayout parent, SnackbarBaseLayout child,
- MotionEvent event) {
- switch (event.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- // We want to make sure that we disable any Snackbar timeouts if the user is
- // currently touching the Snackbar. We restore the timeout when complete
- if (parent.isPointInChildBounds(child, (int) event.getX(),
- (int) event.getY())) {
- SnackbarManager.getInstance().pauseTimeout(mManagerCallback);
- }
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- SnackbarManager.getInstance().restoreTimeoutIfPaused(mManagerCallback);
- break;
- }
- return super.onInterceptTouchEvent(parent, child, event);
- }
- }
-}
diff --git a/android/support/design/widget/BottomNavigationView.java b/android/support/design/widget/BottomNavigationView.java
deleted file mode 100644
index 61dba876..00000000
--- a/android/support/design/widget/BottomNavigationView.java
+++ /dev/null
@@ -1,477 +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.support.design.widget;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.IdRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.design.R;
-import android.support.design.internal.BottomNavigationMenu;
-import android.support.design.internal.BottomNavigationMenuView;
-import android.support.design.internal.BottomNavigationPresenter;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.view.AbsSavedState;
-import android.support.v4.view.ViewCompat;
-import android.support.v7.content.res.AppCompatResources;
-import android.support.v7.view.SupportMenuInflater;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.widget.TintTypedArray;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.FrameLayout;
-
-/**
- * <p>
- * Represents a standard bottom navigation bar for application. It is an implementation of
- * <a href="https://material.google.com/components/bottom-navigation.html">material design bottom
- * navigation</a>.
- * </p>
- *
- * <p>
- * Bottom navigation bars make it easy for users to explore and switch between top-level views in
- * a single tap. It should be used when application has three to five top-level destinations.
- * </p>
- *
- * <p>
- * The bar contents can be populated by specifying a menu resource file. Each menu item title, icon
- * and enabled state will be used for displaying bottom navigation bar items. Menu items can also be
- * used for programmatically selecting which destination is currently active. It can be done using
- * {@code MenuItem#setChecked(true)}
- * </p>
- *
- * <pre>
- * layout resource file:
- * &lt;android.support.design.widget.BottomNavigationView
- * xmlns:android="http://schemas.android.com/apk/res/android"
- * xmlns:app="http://schemas.android.com/apk/res-auto"
- * android:id="@+id/navigation"
- * android:layout_width="match_parent"
- * android:layout_height="56dp"
- * android:layout_gravity="start"
- * app:menu="@menu/my_navigation_items" /&gt;
- *
- * res/menu/my_navigation_items.xml:
- * &lt;menu xmlns:android="http://schemas.android.com/apk/res/android"&gt;
- * &lt;item android:id="@+id/action_search"
- * android:title="@string/menu_search"
- * android:icon="@drawable/ic_search" /&gt;
- * &lt;item android:id="@+id/action_settings"
- * android:title="@string/menu_settings"
- * android:icon="@drawable/ic_add" /&gt;
- * &lt;item android:id="@+id/action_navigation"
- * android:title="@string/menu_navigation"
- * android:icon="@drawable/ic_action_navigation_menu" /&gt;
- * &lt;/menu&gt;
- * </pre>
- */
-public class BottomNavigationView extends FrameLayout {
-
- private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
- private static final int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled};
-
- private static final int MENU_PRESENTER_ID = 1;
-
- private final MenuBuilder mMenu;
- private final BottomNavigationMenuView mMenuView;
- private final BottomNavigationPresenter mPresenter = new BottomNavigationPresenter();
- private MenuInflater mMenuInflater;
-
- private OnNavigationItemSelectedListener mSelectedListener;
- private OnNavigationItemReselectedListener mReselectedListener;
-
- public BottomNavigationView(Context context) {
- this(context, null);
- }
-
- public BottomNavigationView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public BottomNavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- // Create the menu
- mMenu = new BottomNavigationMenu(context);
-
- mMenuView = new BottomNavigationMenuView(context);
- FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
- params.gravity = Gravity.CENTER;
- mMenuView.setLayoutParams(params);
-
- mPresenter.setBottomNavigationMenuView(mMenuView);
- mPresenter.setId(MENU_PRESENTER_ID);
- mMenuView.setPresenter(mPresenter);
- mMenu.addMenuPresenter(mPresenter);
- mPresenter.initForMenu(getContext(), mMenu);
-
- // Custom attributes
- TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
- R.styleable.BottomNavigationView, defStyleAttr,
- R.style.Widget_Design_BottomNavigationView);
-
- if (a.hasValue(R.styleable.BottomNavigationView_itemIconTint)) {
- mMenuView.setIconTintList(
- a.getColorStateList(R.styleable.BottomNavigationView_itemIconTint));
- } else {
- mMenuView.setIconTintList(
- createDefaultColorStateList(android.R.attr.textColorSecondary));
- }
- if (a.hasValue(R.styleable.BottomNavigationView_itemTextColor)) {
- mMenuView.setItemTextColor(
- a.getColorStateList(R.styleable.BottomNavigationView_itemTextColor));
- } else {
- mMenuView.setItemTextColor(
- createDefaultColorStateList(android.R.attr.textColorSecondary));
- }
- if (a.hasValue(R.styleable.BottomNavigationView_elevation)) {
- ViewCompat.setElevation(this, a.getDimensionPixelSize(
- R.styleable.BottomNavigationView_elevation, 0));
- }
-
- int itemBackground = a.getResourceId(R.styleable.BottomNavigationView_itemBackground, 0);
- mMenuView.setItemBackgroundRes(itemBackground);
-
- if (a.hasValue(R.styleable.BottomNavigationView_menu)) {
- inflateMenu(a.getResourceId(R.styleable.BottomNavigationView_menu, 0));
- }
- a.recycle();
-
- addView(mMenuView, params);
- if (Build.VERSION.SDK_INT < 21) {
- addCompatibilityTopDivider(context);
- }
-
- mMenu.setCallback(new MenuBuilder.Callback() {
- @Override
- public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
- if (mReselectedListener != null && item.getItemId() == getSelectedItemId()) {
- mReselectedListener.onNavigationItemReselected(item);
- return true; // item is already selected
- }
- return mSelectedListener != null
- && !mSelectedListener.onNavigationItemSelected(item);
- }
-
- @Override
- public void onMenuModeChange(MenuBuilder menu) {}
- });
- }
-
- /**
- * Set a listener that will be notified when a bottom navigation item is selected. This listener
- * will also be notified when the currently selected item is reselected, unless an
- * {@link OnNavigationItemReselectedListener} has also been set.
- *
- * @param listener The listener to notify
- *
- * @see #setOnNavigationItemReselectedListener(OnNavigationItemReselectedListener)
- */
- public void setOnNavigationItemSelectedListener(
- @Nullable OnNavigationItemSelectedListener listener) {
- mSelectedListener = listener;
- }
-
- /**
- * Set a listener that will be notified when the currently selected bottom navigation item is
- * reselected. This does not require an {@link OnNavigationItemSelectedListener} to be set.
- *
- * @param listener The listener to notify
- *
- * @see #setOnNavigationItemSelectedListener(OnNavigationItemSelectedListener)
- */
- public void setOnNavigationItemReselectedListener(
- @Nullable OnNavigationItemReselectedListener listener) {
- mReselectedListener = listener;
- }
-
- /**
- * Returns the {@link Menu} instance associated with this bottom navigation bar.
- */
- @NonNull
- public Menu getMenu() {
- return mMenu;
- }
-
- /**
- * Inflate a menu resource into this navigation view.
- *
- * <p>Existing items in the menu will not be modified or removed.</p>
- *
- * @param resId ID of a menu resource to inflate
- */
- public void inflateMenu(int resId) {
- mPresenter.setUpdateSuspended(true);
- getMenuInflater().inflate(resId, mMenu);
- mPresenter.setUpdateSuspended(false);
- mPresenter.updateMenuView(true);
- }
-
- /**
- * @return The maximum number of items that can be shown in BottomNavigationView.
- */
- public int getMaxItemCount() {
- return BottomNavigationMenu.MAX_ITEM_COUNT;
- }
-
- /**
- * Returns the tint which is applied to our menu items' icons.
- *
- * @see #setItemIconTintList(ColorStateList)
- *
- * @attr ref R.styleable#BottomNavigationView_itemIconTint
- */
- @Nullable
- public ColorStateList getItemIconTintList() {
- return mMenuView.getIconTintList();
- }
-
- /**
- * Set the tint which is applied to our menu items' icons.
- *
- * @param tint the tint to apply.
- *
- * @attr ref R.styleable#BottomNavigationView_itemIconTint
- */
- public void setItemIconTintList(@Nullable ColorStateList tint) {
- mMenuView.setIconTintList(tint);
- }
-
- /**
- * Returns colors used for the different states (normal, selected, focused, etc.) of the menu
- * item text.
- *
- * @see #setItemTextColor(ColorStateList)
- *
- * @return the ColorStateList of colors used for the different states of the menu items text.
- *
- * @attr ref R.styleable#BottomNavigationView_itemTextColor
- */
- @Nullable
- public ColorStateList getItemTextColor() {
- return mMenuView.getItemTextColor();
- }
-
- /**
- * Set the colors to use for the different states (normal, selected, focused, etc.) of the menu
- * item text.
- *
- * @see #getItemTextColor()
- *
- * @attr ref R.styleable#BottomNavigationView_itemTextColor
- */
- public void setItemTextColor(@Nullable ColorStateList textColor) {
- mMenuView.setItemTextColor(textColor);
- }
-
- /**
- * Returns the background resource of the menu items.
- *
- * @see #setItemBackgroundResource(int)
- *
- * @attr ref R.styleable#BottomNavigationView_itemBackground
- */
- @DrawableRes
- public int getItemBackgroundResource() {
- return mMenuView.getItemBackgroundRes();
- }
-
- /**
- * Set the background of our menu items to the given resource.
- *
- * @param resId The identifier of the resource.
- *
- * @attr ref R.styleable#BottomNavigationView_itemBackground
- */
- public void setItemBackgroundResource(@DrawableRes int resId) {
- mMenuView.setItemBackgroundRes(resId);
- }
-
- /**
- * Returns the currently selected menu item ID, or zero if there is no menu.
- *
- * @see #setSelectedItemId(int)
- */
- @IdRes
- public int getSelectedItemId() {
- return mMenuView.getSelectedItemId();
- }
-
- /**
- * Set the selected menu item ID. This behaves the same as tapping on an item.
- *
- * @param itemId The menu item ID. If no item has this ID, the current selection is unchanged.
- *
- * @see #getSelectedItemId()
- */
- public void setSelectedItemId(@IdRes int itemId) {
- MenuItem item = mMenu.findItem(itemId);
- if (item != null) {
- if (!mMenu.performItemAction(item, mPresenter, 0)) {
- item.setChecked(true);
- }
- }
- }
-
- /**
- * Listener for handling selection events on bottom navigation items.
- */
- public interface OnNavigationItemSelectedListener {
-
- /**
- * Called when an item in the bottom navigation menu is selected.
- *
- * @param item The selected item
- *
- * @return true to display the item as the selected item and false if the item should not
- * be selected. Consider setting non-selectable items as disabled preemptively to
- * make them appear non-interactive.
- */
- boolean onNavigationItemSelected(@NonNull MenuItem item);
- }
-
- /**
- * Listener for handling reselection events on bottom navigation items.
- */
- public interface OnNavigationItemReselectedListener {
-
- /**
- * Called when the currently selected item in the bottom navigation menu is selected again.
- *
- * @param item The selected item
- */
- void onNavigationItemReselected(@NonNull MenuItem item);
- }
-
- private void addCompatibilityTopDivider(Context context) {
- View divider = new View(context);
- divider.setBackgroundColor(
- ContextCompat.getColor(context, R.color.design_bottom_navigation_shadow_color));
- FrameLayout.LayoutParams dividerParams = new FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- getResources().getDimensionPixelSize(
- R.dimen.design_bottom_navigation_shadow_height));
- divider.setLayoutParams(dividerParams);
- addView(divider);
- }
-
- private MenuInflater getMenuInflater() {
- if (mMenuInflater == null) {
- mMenuInflater = new SupportMenuInflater(getContext());
- }
- return mMenuInflater;
- }
-
- private ColorStateList createDefaultColorStateList(int baseColorThemeAttr) {
- final TypedValue value = new TypedValue();
- if (!getContext().getTheme().resolveAttribute(baseColorThemeAttr, value, true)) {
- return null;
- }
- ColorStateList baseColor = AppCompatResources.getColorStateList(
- getContext(), value.resourceId);
- if (!getContext().getTheme().resolveAttribute(
- android.support.v7.appcompat.R.attr.colorPrimary, value, true)) {
- return null;
- }
- int colorPrimary = value.data;
- int defaultColor = baseColor.getDefaultColor();
- return new ColorStateList(new int[][]{
- DISABLED_STATE_SET,
- CHECKED_STATE_SET,
- EMPTY_STATE_SET
- }, new int[]{
- baseColor.getColorForState(DISABLED_STATE_SET, defaultColor),
- colorPrimary,
- defaultColor
- });
- }
-
- @Override
- protected Parcelable onSaveInstanceState() {
- Parcelable superState = super.onSaveInstanceState();
- SavedState savedState = new SavedState(superState);
- savedState.menuPresenterState = new Bundle();
- mMenu.savePresenterStates(savedState.menuPresenterState);
- return savedState;
- }
-
- @Override
- protected void onRestoreInstanceState(Parcelable state) {
- if (!(state instanceof SavedState)) {
- super.onRestoreInstanceState(state);
- return;
- }
- SavedState savedState = (SavedState) state;
- super.onRestoreInstanceState(savedState.getSuperState());
- mMenu.restorePresenterStates(savedState.menuPresenterState);
- }
-
- static class SavedState extends AbsSavedState {
- Bundle menuPresenterState;
-
- public SavedState(Parcelable superState) {
- super(superState);
- }
-
- public SavedState(Parcel source, ClassLoader loader) {
- super(source, loader);
- readFromParcel(source, loader);
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel out, int flags) {
- super.writeToParcel(out, flags);
- out.writeBundle(menuPresenterState);
- }
-
- private void readFromParcel(Parcel in, ClassLoader loader) {
- menuPresenterState = in.readBundle(loader);
- }
-
- public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in, ClassLoader loader) {
- return new SavedState(in, loader);
- }
-
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in, null);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-}
diff --git a/android/support/design/widget/BottomSheetBehavior.java b/android/support/design/widget/BottomSheetBehavior.java
deleted file mode 100644
index 00ce8f90..00000000
--- a/android/support/design/widget/BottomSheetBehavior.java
+++ /dev/null
@@ -1,829 +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 android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
-import android.support.design.R;
-import android.support.v4.math.MathUtils;
-import android.support.v4.view.AbsSavedState;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.widget.ViewDragHelper;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.ref.WeakReference;
-
-
-/**
- * An interaction behavior plugin for a child view of {@link CoordinatorLayout} to make it work as
- * a bottom sheet.
- */
-public class BottomSheetBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
-
- /**
- * Callback for monitoring events about bottom sheets.
- */
- public abstract static class BottomSheetCallback {
-
- /**
- * Called when the bottom sheet changes its state.
- *
- * @param bottomSheet The bottom sheet view.
- * @param newState The new state. This will be one of {@link #STATE_DRAGGING},
- * {@link #STATE_SETTLING}, {@link #STATE_EXPANDED},
- * {@link #STATE_COLLAPSED}, or {@link #STATE_HIDDEN}.
- */
- public abstract void onStateChanged(@NonNull View bottomSheet, @State int newState);
-
- /**
- * Called when the bottom sheet is being dragged.
- *
- * @param bottomSheet The bottom sheet view.
- * @param slideOffset The new offset of this bottom sheet within [-1,1] range. Offset
- * increases as this bottom sheet is moving upward. From 0 to 1 the sheet
- * is between collapsed and expanded states and from -1 to 0 it is
- * between hidden and collapsed states.
- */
- public abstract void onSlide(@NonNull View bottomSheet, float slideOffset);
- }
-
- /**
- * The bottom sheet is dragging.
- */
- public static final int STATE_DRAGGING = 1;
-
- /**
- * The bottom sheet is settling.
- */
- public static final int STATE_SETTLING = 2;
-
- /**
- * The bottom sheet is expanded.
- */
- public static final int STATE_EXPANDED = 3;
-
- /**
- * The bottom sheet is collapsed.
- */
- public static final int STATE_COLLAPSED = 4;
-
- /**
- * The bottom sheet is hidden.
- */
- public static final int STATE_HIDDEN = 5;
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef({STATE_EXPANDED, STATE_COLLAPSED, STATE_DRAGGING, STATE_SETTLING, STATE_HIDDEN})
- @Retention(RetentionPolicy.SOURCE)
- public @interface State {}
-
- /**
- * Peek at the 16:9 ratio keyline of its parent.
- *
- * <p>This can be used as a parameter for {@link #setPeekHeight(int)}.
- * {@link #getPeekHeight()} will return this when the value is set.</p>
- */
- public static final int PEEK_HEIGHT_AUTO = -1;
-
- private static final float HIDE_THRESHOLD = 0.5f;
-
- private static final float HIDE_FRICTION = 0.1f;
-
- private float mMaximumVelocity;
-
- private int mPeekHeight;
-
- private boolean mPeekHeightAuto;
-
- private int mPeekHeightMin;
-
- int mMinOffset;
-
- int mMaxOffset;
-
- boolean mHideable;
-
- private boolean mSkipCollapsed;
-
- @State
- int mState = STATE_COLLAPSED;
-
- ViewDragHelper mViewDragHelper;
-
- private boolean mIgnoreEvents;
-
- private int mLastNestedScrollDy;
-
- private boolean mNestedScrolled;
-
- int mParentHeight;
-
- WeakReference<V> mViewRef;
-
- WeakReference<View> mNestedScrollingChildRef;
-
- private BottomSheetCallback mCallback;
-
- private VelocityTracker mVelocityTracker;
-
- int mActivePointerId;
-
- private int mInitialY;
-
- boolean mTouchingScrollingChild;
-
- /**
- * Default constructor for instantiating BottomSheetBehaviors.
- */
- public BottomSheetBehavior() {
- }
-
- /**
- * Default constructor for inflating BottomSheetBehaviors from layout.
- *
- * @param context The {@link Context}.
- * @param attrs The {@link AttributeSet}.
- */
- public BottomSheetBehavior(Context context, AttributeSet attrs) {
- super(context, attrs);
- TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.BottomSheetBehavior_Layout);
- TypedValue value = a.peekValue(R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight);
- if (value != null && value.data == PEEK_HEIGHT_AUTO) {
- setPeekHeight(value.data);
- } else {
- setPeekHeight(a.getDimensionPixelSize(
- R.styleable.BottomSheetBehavior_Layout_behavior_peekHeight, PEEK_HEIGHT_AUTO));
- }
- setHideable(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_hideable, false));
- setSkipCollapsed(a.getBoolean(R.styleable.BottomSheetBehavior_Layout_behavior_skipCollapsed,
- false));
- a.recycle();
- ViewConfiguration configuration = ViewConfiguration.get(context);
- mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
- }
-
- @Override
- public Parcelable onSaveInstanceState(CoordinatorLayout parent, V child) {
- return new SavedState(super.onSaveInstanceState(parent, child), mState);
- }
-
- @Override
- public void onRestoreInstanceState(CoordinatorLayout parent, V child, Parcelable state) {
- SavedState ss = (SavedState) state;
- super.onRestoreInstanceState(parent, child, ss.getSuperState());
- // Intermediate states are restored as collapsed state
- if (ss.state == STATE_DRAGGING || ss.state == STATE_SETTLING) {
- mState = STATE_COLLAPSED;
- } else {
- mState = ss.state;
- }
- }
-
- @Override
- public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
- if (ViewCompat.getFitsSystemWindows(parent) && !ViewCompat.getFitsSystemWindows(child)) {
- ViewCompat.setFitsSystemWindows(child, true);
- }
- int savedTop = child.getTop();
- // First let the parent lay it out
- parent.onLayoutChild(child, layoutDirection);
- // Offset the bottom sheet
- mParentHeight = parent.getHeight();
- int peekHeight;
- if (mPeekHeightAuto) {
- if (mPeekHeightMin == 0) {
- mPeekHeightMin = parent.getResources().getDimensionPixelSize(
- R.dimen.design_bottom_sheet_peek_height_min);
- }
- peekHeight = Math.max(mPeekHeightMin, mParentHeight - parent.getWidth() * 9 / 16);
- } else {
- peekHeight = mPeekHeight;
- }
- mMinOffset = Math.max(0, mParentHeight - child.getHeight());
- mMaxOffset = Math.max(mParentHeight - peekHeight, mMinOffset);
- if (mState == STATE_EXPANDED) {
- ViewCompat.offsetTopAndBottom(child, mMinOffset);
- } else if (mHideable && mState == STATE_HIDDEN) {
- ViewCompat.offsetTopAndBottom(child, mParentHeight);
- } else if (mState == STATE_COLLAPSED) {
- ViewCompat.offsetTopAndBottom(child, mMaxOffset);
- } else if (mState == STATE_DRAGGING || mState == STATE_SETTLING) {
- ViewCompat.offsetTopAndBottom(child, savedTop - child.getTop());
- }
- if (mViewDragHelper == null) {
- mViewDragHelper = ViewDragHelper.create(parent, mDragCallback);
- }
- mViewRef = new WeakReference<>(child);
- mNestedScrollingChildRef = new WeakReference<>(findScrollingChild(child));
- return true;
- }
-
- @Override
- public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
- if (!child.isShown()) {
- mIgnoreEvents = true;
- return false;
- }
- int action = event.getActionMasked();
- // Record the velocity
- if (action == MotionEvent.ACTION_DOWN) {
- reset();
- }
- if (mVelocityTracker == null) {
- mVelocityTracker = VelocityTracker.obtain();
- }
- mVelocityTracker.addMovement(event);
- switch (action) {
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- mTouchingScrollingChild = false;
- mActivePointerId = MotionEvent.INVALID_POINTER_ID;
- // Reset the ignore flag
- if (mIgnoreEvents) {
- mIgnoreEvents = false;
- return false;
- }
- break;
- case MotionEvent.ACTION_DOWN:
- int initialX = (int) event.getX();
- mInitialY = (int) event.getY();
- View scroll = mNestedScrollingChildRef != null
- ? mNestedScrollingChildRef.get() : null;
- if (scroll != null && parent.isPointInChildBounds(scroll, initialX, mInitialY)) {
- mActivePointerId = event.getPointerId(event.getActionIndex());
- mTouchingScrollingChild = true;
- }
- mIgnoreEvents = mActivePointerId == MotionEvent.INVALID_POINTER_ID &&
- !parent.isPointInChildBounds(child, initialX, mInitialY);
- break;
- }
- if (!mIgnoreEvents && mViewDragHelper.shouldInterceptTouchEvent(event)) {
- return true;
- }
- // We have to handle cases that the ViewDragHelper does not capture the bottom sheet because
- // it is not the top most view of its parent. This is not necessary when the touch event is
- // happening over the scrolling content as nested scrolling logic handles that case.
- View scroll = mNestedScrollingChildRef.get();
- return action == MotionEvent.ACTION_MOVE && scroll != null &&
- !mIgnoreEvents && mState != STATE_DRAGGING &&
- !parent.isPointInChildBounds(scroll, (int) event.getX(), (int) event.getY()) &&
- Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop();
- }
-
- @Override
- public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
- if (!child.isShown()) {
- return false;
- }
- int action = event.getActionMasked();
- if (mState == STATE_DRAGGING && action == MotionEvent.ACTION_DOWN) {
- return true;
- }
- if (mViewDragHelper != null) {
- mViewDragHelper.processTouchEvent(event);
- }
- // Record the velocity
- if (action == MotionEvent.ACTION_DOWN) {
- reset();
- }
- if (mVelocityTracker == null) {
- mVelocityTracker = VelocityTracker.obtain();
- }
- mVelocityTracker.addMovement(event);
- // The ViewDragHelper tries to capture only the top-most View. We have to explicitly tell it
- // to capture the bottom sheet in case it is not captured and the touch slop is passed.
- if (action == MotionEvent.ACTION_MOVE && !mIgnoreEvents) {
- if (Math.abs(mInitialY - event.getY()) > mViewDragHelper.getTouchSlop()) {
- mViewDragHelper.captureChildView(child, event.getPointerId(event.getActionIndex()));
- }
- }
- return !mIgnoreEvents;
- }
-
- @Override
- public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, V child,
- View directTargetChild, View target, int nestedScrollAxes) {
- mLastNestedScrollDy = 0;
- mNestedScrolled = false;
- return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL) != 0;
- }
-
- @Override
- public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target, int dx,
- int dy, int[] consumed) {
- View scrollingChild = mNestedScrollingChildRef.get();
- if (target != scrollingChild) {
- return;
- }
- int currentTop = child.getTop();
- int newTop = currentTop - dy;
- if (dy > 0) { // Upward
- if (newTop < mMinOffset) {
- consumed[1] = currentTop - mMinOffset;
- ViewCompat.offsetTopAndBottom(child, -consumed[1]);
- setStateInternal(STATE_EXPANDED);
- } else {
- consumed[1] = dy;
- ViewCompat.offsetTopAndBottom(child, -dy);
- setStateInternal(STATE_DRAGGING);
- }
- } else if (dy < 0) { // Downward
- if (!target.canScrollVertically(-1)) {
- if (newTop <= mMaxOffset || mHideable) {
- consumed[1] = dy;
- ViewCompat.offsetTopAndBottom(child, -dy);
- setStateInternal(STATE_DRAGGING);
- } else {
- consumed[1] = currentTop - mMaxOffset;
- ViewCompat.offsetTopAndBottom(child, -consumed[1]);
- setStateInternal(STATE_COLLAPSED);
- }
- }
- }
- dispatchOnSlide(child.getTop());
- mLastNestedScrollDy = dy;
- mNestedScrolled = true;
- }
-
- @Override
- public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, V child, View target) {
- if (child.getTop() == mMinOffset) {
- setStateInternal(STATE_EXPANDED);
- return;
- }
- if (mNestedScrollingChildRef == null || target != mNestedScrollingChildRef.get()
- || !mNestedScrolled) {
- return;
- }
- int top;
- int targetState;
- if (mLastNestedScrollDy > 0) {
- top = mMinOffset;
- targetState = STATE_EXPANDED;
- } else if (mHideable && shouldHide(child, getYVelocity())) {
- top = mParentHeight;
- targetState = STATE_HIDDEN;
- } else if (mLastNestedScrollDy == 0) {
- int currentTop = child.getTop();
- if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) {
- top = mMinOffset;
- targetState = STATE_EXPANDED;
- } else {
- top = mMaxOffset;
- targetState = STATE_COLLAPSED;
- }
- } else {
- top = mMaxOffset;
- targetState = STATE_COLLAPSED;
- }
- if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
- setStateInternal(STATE_SETTLING);
- ViewCompat.postOnAnimation(child, new SettleRunnable(child, targetState));
- } else {
- setStateInternal(targetState);
- }
- mNestedScrolled = false;
- }
-
- @Override
- public boolean onNestedPreFling(CoordinatorLayout coordinatorLayout, V child, View target,
- float velocityX, float velocityY) {
- return target == mNestedScrollingChildRef.get() &&
- (mState != STATE_EXPANDED ||
- super.onNestedPreFling(coordinatorLayout, child, target,
- velocityX, velocityY));
- }
-
- /**
- * Sets the height of the bottom sheet when it is collapsed.
- *
- * @param peekHeight The height of the collapsed bottom sheet in pixels, or
- * {@link #PEEK_HEIGHT_AUTO} to configure the sheet to peek automatically
- * at 16:9 ratio keyline.
- * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight
- */
- public final void setPeekHeight(int peekHeight) {
- boolean layout = false;
- if (peekHeight == PEEK_HEIGHT_AUTO) {
- if (!mPeekHeightAuto) {
- mPeekHeightAuto = true;
- layout = true;
- }
- } else if (mPeekHeightAuto || mPeekHeight != peekHeight) {
- mPeekHeightAuto = false;
- mPeekHeight = Math.max(0, peekHeight);
- mMaxOffset = mParentHeight - peekHeight;
- layout = true;
- }
- if (layout && mState == STATE_COLLAPSED && mViewRef != null) {
- V view = mViewRef.get();
- if (view != null) {
- view.requestLayout();
- }
- }
- }
-
- /**
- * Gets the height of the bottom sheet when it is collapsed.
- *
- * @return The height of the collapsed bottom sheet in pixels, or {@link #PEEK_HEIGHT_AUTO}
- * if the sheet is configured to peek automatically at 16:9 ratio keyline
- * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_peekHeight
- */
- public final int getPeekHeight() {
- return mPeekHeightAuto ? PEEK_HEIGHT_AUTO : mPeekHeight;
- }
-
- /**
- * Sets whether this bottom sheet can hide when it is swiped down.
- *
- * @param hideable {@code true} to make this bottom sheet hideable.
- * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_hideable
- */
- public void setHideable(boolean hideable) {
- mHideable = hideable;
- }
-
- /**
- * Gets whether this bottom sheet can hide when it is swiped down.
- *
- * @return {@code true} if this bottom sheet can hide.
- * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_hideable
- */
- public boolean isHideable() {
- return mHideable;
- }
-
- /**
- * Sets whether this bottom sheet should skip the collapsed state when it is being hidden
- * after it is expanded once. Setting this to true has no effect unless the sheet is hideable.
- *
- * @param skipCollapsed True if the bottom sheet should skip the collapsed state.
- * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed
- */
- public void setSkipCollapsed(boolean skipCollapsed) {
- mSkipCollapsed = skipCollapsed;
- }
-
- /**
- * Sets whether this bottom sheet should skip the collapsed state when it is being hidden
- * after it is expanded once.
- *
- * @return Whether the bottom sheet should skip the collapsed state.
- * @attr ref android.support.design.R.styleable#BottomSheetBehavior_Layout_behavior_skipCollapsed
- */
- public boolean getSkipCollapsed() {
- return mSkipCollapsed;
- }
-
- /**
- * Sets a callback to be notified of bottom sheet events.
- *
- * @param callback The callback to notify when bottom sheet events occur.
- */
- public void setBottomSheetCallback(BottomSheetCallback callback) {
- mCallback = callback;
- }
-
- /**
- * Sets the state of the bottom sheet. The bottom sheet will transition to that state with
- * animation.
- *
- * @param state One of {@link #STATE_COLLAPSED}, {@link #STATE_EXPANDED}, or
- * {@link #STATE_HIDDEN}.
- */
- public final void setState(final @State int state) {
- if (state == mState) {
- return;
- }
- if (mViewRef == null) {
- // The view is not laid out yet; modify mState and let onLayoutChild handle it later
- if (state == STATE_COLLAPSED || state == STATE_EXPANDED ||
- (mHideable && state == STATE_HIDDEN)) {
- mState = state;
- }
- return;
- }
- final V child = mViewRef.get();
- if (child == null) {
- return;
- }
- // Start the animation; wait until a pending layout if there is one.
- ViewParent parent = child.getParent();
- if (parent != null && parent.isLayoutRequested() && ViewCompat.isAttachedToWindow(child)) {
- child.post(new Runnable() {
- @Override
- public void run() {
- startSettlingAnimation(child, state);
- }
- });
- } else {
- startSettlingAnimation(child, state);
- }
- }
-
- /**
- * Gets the current state of the bottom sheet.
- *
- * @return One of {@link #STATE_EXPANDED}, {@link #STATE_COLLAPSED}, {@link #STATE_DRAGGING},
- * {@link #STATE_SETTLING}, and {@link #STATE_HIDDEN}.
- */
- @State
- public final int getState() {
- return mState;
- }
-
- void setStateInternal(@State int state) {
- if (mState == state) {
- return;
- }
- mState = state;
- View bottomSheet = mViewRef.get();
- if (bottomSheet != null && mCallback != null) {
- mCallback.onStateChanged(bottomSheet, state);
- }
- }
-
- private void reset() {
- mActivePointerId = ViewDragHelper.INVALID_POINTER;
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
- }
-
- boolean shouldHide(View child, float yvel) {
- if (mSkipCollapsed) {
- return true;
- }
- if (child.getTop() < mMaxOffset) {
- // It should not hide, but collapse.
- return false;
- }
- final float newTop = child.getTop() + yvel * HIDE_FRICTION;
- return Math.abs(newTop - mMaxOffset) / (float) mPeekHeight > HIDE_THRESHOLD;
- }
-
- @VisibleForTesting
- View findScrollingChild(View view) {
- if (ViewCompat.isNestedScrollingEnabled(view)) {
- return view;
- }
- if (view instanceof ViewGroup) {
- ViewGroup group = (ViewGroup) view;
- for (int i = 0, count = group.getChildCount(); i < count; i++) {
- View scrollingChild = findScrollingChild(group.getChildAt(i));
- if (scrollingChild != null) {
- return scrollingChild;
- }
- }
- }
- return null;
- }
-
- private float getYVelocity() {
- mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
- return mVelocityTracker.getYVelocity(mActivePointerId);
- }
-
- void startSettlingAnimation(View child, int state) {
- int top;
- if (state == STATE_COLLAPSED) {
- top = mMaxOffset;
- } else if (state == STATE_EXPANDED) {
- top = mMinOffset;
- } else if (mHideable && state == STATE_HIDDEN) {
- top = mParentHeight;
- } else {
- throw new IllegalArgumentException("Illegal state argument: " + state);
- }
- if (mViewDragHelper.smoothSlideViewTo(child, child.getLeft(), top)) {
- setStateInternal(STATE_SETTLING);
- ViewCompat.postOnAnimation(child, new SettleRunnable(child, state));
- } else {
- setStateInternal(state);
- }
- }
-
- private final ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
-
- @Override
- public boolean tryCaptureView(View child, int pointerId) {
- if (mState == STATE_DRAGGING) {
- return false;
- }
- if (mTouchingScrollingChild) {
- return false;
- }
- if (mState == STATE_EXPANDED && mActivePointerId == pointerId) {
- View scroll = mNestedScrollingChildRef.get();
- if (scroll != null && scroll.canScrollVertically(-1)) {
- // Let the content scroll up
- return false;
- }
- }
- return mViewRef != null && mViewRef.get() == child;
- }
-
- @Override
- public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
- dispatchOnSlide(top);
- }
-
- @Override
- public void onViewDragStateChanged(int state) {
- if (state == ViewDragHelper.STATE_DRAGGING) {
- setStateInternal(STATE_DRAGGING);
- }
- }
-
- @Override
- public void onViewReleased(View releasedChild, float xvel, float yvel) {
- int top;
- @State int targetState;
- if (yvel < 0) { // Moving up
- top = mMinOffset;
- targetState = STATE_EXPANDED;
- } else if (mHideable && shouldHide(releasedChild, yvel)) {
- top = mParentHeight;
- targetState = STATE_HIDDEN;
- } else if (yvel == 0.f) {
- int currentTop = releasedChild.getTop();
- if (Math.abs(currentTop - mMinOffset) < Math.abs(currentTop - mMaxOffset)) {
- top = mMinOffset;
- targetState = STATE_EXPANDED;
- } else {
- top = mMaxOffset;
- targetState = STATE_COLLAPSED;
- }
- } else {
- top = mMaxOffset;
- targetState = STATE_COLLAPSED;
- }
- if (mViewDragHelper.settleCapturedViewAt(releasedChild.getLeft(), top)) {
- setStateInternal(STATE_SETTLING);
- ViewCompat.postOnAnimation(releasedChild,
- new SettleRunnable(releasedChild, targetState));
- } else {
- setStateInternal(targetState);
- }
- }
-
- @Override
- public int clampViewPositionVertical(View child, int top, int dy) {
- return MathUtils.clamp(top, mMinOffset, mHideable ? mParentHeight : mMaxOffset);
- }
-
- @Override
- public int clampViewPositionHorizontal(View child, int left, int dx) {
- return child.getLeft();
- }
-
- @Override
- public int getViewVerticalDragRange(View child) {
- if (mHideable) {
- return mParentHeight - mMinOffset;
- } else {
- return mMaxOffset - mMinOffset;
- }
- }
- };
-
- void dispatchOnSlide(int top) {
- View bottomSheet = mViewRef.get();
- if (bottomSheet != null && mCallback != null) {
- if (top > mMaxOffset) {
- mCallback.onSlide(bottomSheet, (float) (mMaxOffset - top) /
- (mParentHeight - mMaxOffset));
- } else {
- mCallback.onSlide(bottomSheet,
- (float) (mMaxOffset - top) / ((mMaxOffset - mMinOffset)));
- }
- }
- }
-
- @VisibleForTesting
- int getPeekHeightMin() {
- return mPeekHeightMin;
- }
-
- private class SettleRunnable implements Runnable {
-
- private final View mView;
-
- @State
- private final int mTargetState;
-
- SettleRunnable(View view, @State int targetState) {
- mView = view;
- mTargetState = targetState;
- }
-
- @Override
- public void run() {
- if (mViewDragHelper != null && mViewDragHelper.continueSettling(true)) {
- ViewCompat.postOnAnimation(mView, this);
- } else {
- setStateInternal(mTargetState);
- }
- }
- }
-
- protected static class SavedState extends AbsSavedState {
- @State
- final int state;
-
- public SavedState(Parcel source) {
- this(source, null);
- }
-
- public SavedState(Parcel source, ClassLoader loader) {
- super(source, loader);
- //noinspection ResourceType
- state = source.readInt();
- }
-
- public SavedState(Parcelable superState, @State int state) {
- super(superState);
- this.state = state;
- }
-
- @Override
- public void writeToParcel(Parcel out, int flags) {
- super.writeToParcel(out, flags);
- out.writeInt(state);
- }
-
- public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in, ClassLoader loader) {
- return new SavedState(in, loader);
- }
-
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in, null);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-
- /**
- * A utility function to get the {@link BottomSheetBehavior} associated with the {@code view}.
- *
- * @param view The {@link View} with {@link BottomSheetBehavior}.
- * @return The {@link BottomSheetBehavior} associated with the {@code view}.
- */
- @SuppressWarnings("unchecked")
- public static <V extends View> BottomSheetBehavior<V> from(V view) {
- ViewGroup.LayoutParams params = view.getLayoutParams();
- if (!(params instanceof CoordinatorLayout.LayoutParams)) {
- throw new IllegalArgumentException("The view is not a child of CoordinatorLayout");
- }
- CoordinatorLayout.Behavior behavior = ((CoordinatorLayout.LayoutParams) params)
- .getBehavior();
- if (!(behavior instanceof BottomSheetBehavior)) {
- throw new IllegalArgumentException(
- "The view is not associated with BottomSheetBehavior");
- }
- return (BottomSheetBehavior<V>) behavior;
- }
-
-}
diff --git a/android/support/design/widget/BottomSheetDialog.java b/android/support/design/widget/BottomSheetDialog.java
deleted file mode 100644
index 19b5782d..00000000
--- a/android/support/design/widget/BottomSheetDialog.java
+++ /dev/null
@@ -1,230 +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 android.support.design.widget;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.os.Build;
-import android.os.Bundle;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.StyleRes;
-import android.support.design.R;
-import android.support.v4.view.AccessibilityDelegateCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
-import android.support.v7.app.AppCompatDialog;
-import android.util.TypedValue;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.Window;
-import android.view.WindowManager;
-import android.widget.FrameLayout;
-
-/**
- * Base class for {@link android.app.Dialog}s styled as a bottom sheet.
- */
-public class BottomSheetDialog extends AppCompatDialog {
-
- private BottomSheetBehavior<FrameLayout> mBehavior;
-
- boolean mCancelable = true;
- private boolean mCanceledOnTouchOutside = true;
- private boolean mCanceledOnTouchOutsideSet;
-
- public BottomSheetDialog(@NonNull Context context) {
- this(context, 0);
- }
-
- public BottomSheetDialog(@NonNull Context context, @StyleRes int theme) {
- super(context, getThemeResId(context, theme));
- // We hide the title bar for any style configuration. Otherwise, there will be a gap
- // above the bottom sheet when it is expanded.
- supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
- }
-
- protected BottomSheetDialog(@NonNull Context context, boolean cancelable,
- OnCancelListener cancelListener) {
- super(context, cancelable, cancelListener);
- supportRequestWindowFeature(Window.FEATURE_NO_TITLE);
- mCancelable = cancelable;
- }
-
- @Override
- public void setContentView(@LayoutRes int layoutResId) {
- super.setContentView(wrapInBottomSheet(layoutResId, null, null));
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- Window window = getWindow();
- if (window != null) {
- if (Build.VERSION.SDK_INT >= 21) {
- window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
- window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
- }
- window.setLayout(ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT);
- }
- }
-
- @Override
- public void setContentView(View view) {
- super.setContentView(wrapInBottomSheet(0, view, null));
- }
-
- @Override
- public void setContentView(View view, ViewGroup.LayoutParams params) {
- super.setContentView(wrapInBottomSheet(0, view, params));
- }
-
- @Override
- public void setCancelable(boolean cancelable) {
- super.setCancelable(cancelable);
- if (mCancelable != cancelable) {
- mCancelable = cancelable;
- if (mBehavior != null) {
- mBehavior.setHideable(cancelable);
- }
- }
- }
-
- @Override
- protected void onStart() {
- super.onStart();
- if (mBehavior != null) {
- mBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
- }
- }
-
- @Override
- public void setCanceledOnTouchOutside(boolean cancel) {
- super.setCanceledOnTouchOutside(cancel);
- if (cancel && !mCancelable) {
- mCancelable = true;
- }
- mCanceledOnTouchOutside = cancel;
- mCanceledOnTouchOutsideSet = true;
- }
-
- private View wrapInBottomSheet(int layoutResId, View view, ViewGroup.LayoutParams params) {
- final FrameLayout container = (FrameLayout) View.inflate(getContext(),
- R.layout.design_bottom_sheet_dialog, null);
- final CoordinatorLayout coordinator =
- (CoordinatorLayout) container.findViewById(R.id.coordinator);
- if (layoutResId != 0 && view == null) {
- view = getLayoutInflater().inflate(layoutResId, coordinator, false);
- }
- FrameLayout bottomSheet = (FrameLayout) coordinator.findViewById(R.id.design_bottom_sheet);
- mBehavior = BottomSheetBehavior.from(bottomSheet);
- mBehavior.setBottomSheetCallback(mBottomSheetCallback);
- mBehavior.setHideable(mCancelable);
- if (params == null) {
- bottomSheet.addView(view);
- } else {
- bottomSheet.addView(view, params);
- }
- // We treat the CoordinatorLayout as outside the dialog though it is technically inside
- coordinator.findViewById(R.id.touch_outside).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- if (mCancelable && isShowing() && shouldWindowCloseOnTouchOutside()) {
- cancel();
- }
- }
- });
- // Handle accessibility events
- ViewCompat.setAccessibilityDelegate(bottomSheet, new AccessibilityDelegateCompat() {
- @Override
- public void onInitializeAccessibilityNodeInfo(View host,
- AccessibilityNodeInfoCompat info) {
- super.onInitializeAccessibilityNodeInfo(host, info);
- if (mCancelable) {
- info.addAction(AccessibilityNodeInfoCompat.ACTION_DISMISS);
- info.setDismissable(true);
- } else {
- info.setDismissable(false);
- }
- }
-
- @Override
- public boolean performAccessibilityAction(View host, int action, Bundle args) {
- if (action == AccessibilityNodeInfoCompat.ACTION_DISMISS && mCancelable) {
- cancel();
- return true;
- }
- return super.performAccessibilityAction(host, action, args);
- }
- });
- bottomSheet.setOnTouchListener(new View.OnTouchListener() {
- @Override
- public boolean onTouch(View view, MotionEvent event) {
- // Consume the event and prevent it from falling through
- return true;
- }
- });
- return container;
- }
-
- boolean shouldWindowCloseOnTouchOutside() {
- if (!mCanceledOnTouchOutsideSet) {
- if (Build.VERSION.SDK_INT < 11) {
- mCanceledOnTouchOutside = true;
- } else {
- TypedArray a = getContext().obtainStyledAttributes(
- new int[]{android.R.attr.windowCloseOnTouchOutside});
- mCanceledOnTouchOutside = a.getBoolean(0, true);
- a.recycle();
- }
- mCanceledOnTouchOutsideSet = true;
- }
- return mCanceledOnTouchOutside;
- }
-
- private static int getThemeResId(Context context, int themeId) {
- if (themeId == 0) {
- // If the provided theme is 0, then retrieve the dialogTheme from our theme
- TypedValue outValue = new TypedValue();
- if (context.getTheme().resolveAttribute(
- R.attr.bottomSheetDialogTheme, outValue, true)) {
- themeId = outValue.resourceId;
- } else {
- // bottomSheetDialogTheme is not provided; we default to our light theme
- themeId = R.style.Theme_Design_Light_BottomSheetDialog;
- }
- }
- return themeId;
- }
-
- private BottomSheetBehavior.BottomSheetCallback mBottomSheetCallback
- = new BottomSheetBehavior.BottomSheetCallback() {
- @Override
- public void onStateChanged(@NonNull View bottomSheet,
- @BottomSheetBehavior.State int newState) {
- if (newState == BottomSheetBehavior.STATE_HIDDEN) {
- cancel();
- }
- }
-
- @Override
- public void onSlide(@NonNull View bottomSheet, float slideOffset) {
- }
- };
-
-}
diff --git a/android/support/design/widget/BottomSheetDialogFragment.java b/android/support/design/widget/BottomSheetDialogFragment.java
deleted file mode 100644
index 88429880..00000000
--- a/android/support/design/widget/BottomSheetDialogFragment.java
+++ /dev/null
@@ -1,35 +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 android.support.design.widget;
-
-import android.app.Dialog;
-import android.os.Bundle;
-import android.support.v4.app.DialogFragment;
-import android.support.v7.app.AppCompatDialogFragment;
-
-/**
- * Modal bottom sheet. This is a version of {@link DialogFragment} that shows a bottom sheet
- * using {@link BottomSheetDialog} instead of a floating dialog.
- */
-public class BottomSheetDialogFragment extends AppCompatDialogFragment {
-
- @Override
- public Dialog onCreateDialog(Bundle savedInstanceState) {
- return new BottomSheetDialog(getContext(), getTheme());
- }
-
-}
diff --git a/android/support/design/widget/CheckableImageButton.java b/android/support/design/widget/CheckableImageButton.java
deleted file mode 100644
index f2745817..00000000
--- a/android/support/design/widget/CheckableImageButton.java
+++ /dev/null
@@ -1,101 +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.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.support.annotation.RestrictTo;
-import android.support.v4.view.AccessibilityDelegateCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.accessibility.AccessibilityEventCompat;
-import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
-import android.support.v7.widget.AppCompatImageButton;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.accessibility.AccessibilityEvent;
-import android.widget.Checkable;
-
-/**
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class CheckableImageButton extends AppCompatImageButton implements Checkable {
-
- private static final int[] DRAWABLE_STATE_CHECKED = new int[]{android.R.attr.state_checked};
-
- private boolean mChecked;
-
- public CheckableImageButton(Context context) {
- this(context, null);
- }
-
- public CheckableImageButton(Context context, AttributeSet attrs) {
- this(context, attrs, android.support.v7.appcompat.R.attr.imageButtonStyle);
- }
-
- public CheckableImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- ViewCompat.setAccessibilityDelegate(this, new AccessibilityDelegateCompat() {
- @Override
- public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
- super.onInitializeAccessibilityEvent(host, event);
- event.setChecked(isChecked());
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(View host,
- AccessibilityNodeInfoCompat info) {
- super.onInitializeAccessibilityNodeInfo(host, info);
- info.setCheckable(true);
- info.setChecked(isChecked());
- }
- });
- }
-
- @Override
- public void setChecked(boolean checked) {
- if (mChecked != checked) {
- mChecked = checked;
- refreshDrawableState();
- sendAccessibilityEvent(
- AccessibilityEventCompat.TYPE_WINDOW_CONTENT_CHANGED);
- }
- }
-
- @Override
- public boolean isChecked() {
- return mChecked;
- }
-
- @Override
- public void toggle() {
- setChecked(!mChecked);
- }
-
- @Override
- public int[] onCreateDrawableState(int extraSpace) {
- if (mChecked) {
- return mergeDrawableStates(
- super.onCreateDrawableState(extraSpace + DRAWABLE_STATE_CHECKED.length),
- DRAWABLE_STATE_CHECKED);
- } else {
- return super.onCreateDrawableState(extraSpace);
- }
- }
-}
diff --git a/android/support/design/widget/CircularBorderDrawable.java b/android/support/design/widget/CircularBorderDrawable.java
deleted file mode 100644
index 617a5010..00000000
--- a/android/support/design/widget/CircularBorderDrawable.java
+++ /dev/null
@@ -1,211 +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 android.support.design.widget;
-
-import android.content.res.ColorStateList;
-import android.graphics.Canvas;
-import android.graphics.ColorFilter;
-import android.graphics.LinearGradient;
-import android.graphics.Paint;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Shader;
-import android.graphics.drawable.Drawable;
-import android.support.v4.graphics.ColorUtils;
-
-/**
- * A drawable which draws an oval 'border'.
- */
-class CircularBorderDrawable extends Drawable {
-
- /**
- * We actually draw the stroke wider than the border size given. This is to reduce any
- * potential transparent space caused by anti-aliasing and padding rounding.
- * This value defines the multiplier used to determine to draw stroke width.
- */
- private static final float DRAW_STROKE_WIDTH_MULTIPLE = 1.3333f;
-
- final Paint mPaint;
- final Rect mRect = new Rect();
- final RectF mRectF = new RectF();
-
- float mBorderWidth;
-
- private int mTopOuterStrokeColor;
- private int mTopInnerStrokeColor;
- private int mBottomOuterStrokeColor;
- private int mBottomInnerStrokeColor;
-
- private ColorStateList mBorderTint;
- private int mCurrentBorderTintColor;
-
- private boolean mInvalidateShader = true;
-
- private float mRotation;
-
- public CircularBorderDrawable() {
- mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
- mPaint.setStyle(Paint.Style.STROKE);
- }
-
- void setGradientColors(int topOuterStrokeColor, int topInnerStrokeColor,
- int bottomOuterStrokeColor, int bottomInnerStrokeColor) {
- mTopOuterStrokeColor = topOuterStrokeColor;
- mTopInnerStrokeColor = topInnerStrokeColor;
- mBottomOuterStrokeColor = bottomOuterStrokeColor;
- mBottomInnerStrokeColor = bottomInnerStrokeColor;
- }
-
- /**
- * Set the border width
- */
- void setBorderWidth(float width) {
- if (mBorderWidth != width) {
- mBorderWidth = width;
- mPaint.setStrokeWidth(width * DRAW_STROKE_WIDTH_MULTIPLE);
- mInvalidateShader = true;
- invalidateSelf();
- }
- }
-
- @Override
- public void draw(Canvas canvas) {
- if (mInvalidateShader) {
- mPaint.setShader(createGradientShader());
- mInvalidateShader = false;
- }
-
- final float halfBorderWidth = mPaint.getStrokeWidth() / 2f;
- final RectF rectF = mRectF;
-
- // We need to inset the oval bounds by half the border width. This is because stroke draws
- // the center of the border on the dimension. Whereas we want the stroke on the inside.
- copyBounds(mRect);
- rectF.set(mRect);
- rectF.left += halfBorderWidth;
- rectF.top += halfBorderWidth;
- rectF.right -= halfBorderWidth;
- rectF.bottom -= halfBorderWidth;
-
- canvas.save();
- canvas.rotate(mRotation, rectF.centerX(), rectF.centerY());
- // Draw the oval
- canvas.drawOval(rectF, mPaint);
- canvas.restore();
- }
-
- @Override
- public boolean getPadding(Rect padding) {
- final int borderWidth = Math.round(mBorderWidth);
- padding.set(borderWidth, borderWidth, borderWidth, borderWidth);
- return true;
- }
-
- @Override
- public void setAlpha(int alpha) {
- mPaint.setAlpha(alpha);
- invalidateSelf();
- }
-
- void setBorderTint(ColorStateList tint) {
- if (tint != null) {
- mCurrentBorderTintColor = tint.getColorForState(getState(), mCurrentBorderTintColor);
- }
- mBorderTint = tint;
- mInvalidateShader = true;
- invalidateSelf();
- }
-
- @Override
- public void setColorFilter(ColorFilter colorFilter) {
- mPaint.setColorFilter(colorFilter);
- invalidateSelf();
- }
-
- @Override
- public int getOpacity() {
- return mBorderWidth > 0 ? PixelFormat.TRANSLUCENT : PixelFormat.TRANSPARENT;
- }
-
- final void setRotation(float rotation) {
- if (rotation != mRotation) {
- mRotation = rotation;
- invalidateSelf();
- }
- }
-
- @Override
- protected void onBoundsChange(Rect bounds) {
- mInvalidateShader = true;
- }
-
- @Override
- public boolean isStateful() {
- return (mBorderTint != null && mBorderTint.isStateful()) || super.isStateful();
- }
-
- @Override
- protected boolean onStateChange(int[] state) {
- if (mBorderTint != null) {
- final int newColor = mBorderTint.getColorForState(state, mCurrentBorderTintColor);
- if (newColor != mCurrentBorderTintColor) {
- mInvalidateShader = true;
- mCurrentBorderTintColor = newColor;
- }
- }
- if (mInvalidateShader) {
- invalidateSelf();
- }
- return mInvalidateShader;
- }
-
- /**
- * Creates a vertical {@link LinearGradient}
- * @return
- */
- private Shader createGradientShader() {
- final Rect rect = mRect;
- copyBounds(rect);
-
- final float borderRatio = mBorderWidth / rect.height();
-
- final int[] colors = new int[6];
- colors[0] = ColorUtils.compositeColors(mTopOuterStrokeColor, mCurrentBorderTintColor);
- colors[1] = ColorUtils.compositeColors(mTopInnerStrokeColor, mCurrentBorderTintColor);
- colors[2] = ColorUtils.compositeColors(
- ColorUtils.setAlphaComponent(mTopInnerStrokeColor, 0), mCurrentBorderTintColor);
- colors[3] = ColorUtils.compositeColors(
- ColorUtils.setAlphaComponent(mBottomInnerStrokeColor, 0), mCurrentBorderTintColor);
- colors[4] = ColorUtils.compositeColors(mBottomInnerStrokeColor, mCurrentBorderTintColor);
- colors[5] = ColorUtils.compositeColors(mBottomOuterStrokeColor, mCurrentBorderTintColor);
-
- final float[] positions = new float[6];
- positions[0] = 0f;
- positions[1] = borderRatio;
- positions[2] = 0.5f;
- positions[3] = 0.5f;
- positions[4] = 1f - borderRatio;
- positions[5] = 1f;
-
- return new LinearGradient(
- 0, rect.top,
- 0, rect.bottom,
- colors, positions,
- Shader.TileMode.CLAMP);
- }
-}
diff --git a/android/support/design/widget/CollapsingTextHelper.java b/android/support/design/widget/CollapsingTextHelper.java
deleted file mode 100644
index a33cabc3..00000000
--- a/android/support/design/widget/CollapsingTextHelper.java
+++ /dev/null
@@ -1,723 +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 android.support.design.widget;
-
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Typeface;
-import android.os.Build;
-import android.support.annotation.ColorInt;
-import android.support.v4.math.MathUtils;
-import android.support.v4.text.TextDirectionHeuristicsCompat;
-import android.support.v4.view.GravityCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v7.widget.TintTypedArray;
-import android.text.TextPaint;
-import android.text.TextUtils;
-import android.view.Gravity;
-import android.view.View;
-import android.view.animation.Interpolator;
-
-final class CollapsingTextHelper {
-
- // Pre-JB-MR2 doesn't support HW accelerated canvas scaled text so we will workaround it
- // by using our own texture
- private static final boolean USE_SCALING_TEXTURE = Build.VERSION.SDK_INT < 18;
-
- private static final boolean DEBUG_DRAW = false;
- private static final Paint DEBUG_DRAW_PAINT;
- static {
- DEBUG_DRAW_PAINT = DEBUG_DRAW ? new Paint() : null;
- if (DEBUG_DRAW_PAINT != null) {
- DEBUG_DRAW_PAINT.setAntiAlias(true);
- DEBUG_DRAW_PAINT.setColor(Color.MAGENTA);
- }
- }
-
- private final View mView;
-
- private boolean mDrawTitle;
- private float mExpandedFraction;
-
- private final Rect mExpandedBounds;
- private final Rect mCollapsedBounds;
- private final RectF mCurrentBounds;
- private int mExpandedTextGravity = Gravity.CENTER_VERTICAL;
- private int mCollapsedTextGravity = Gravity.CENTER_VERTICAL;
- private float mExpandedTextSize = 15;
- private float mCollapsedTextSize = 15;
- private ColorStateList mExpandedTextColor;
- private ColorStateList mCollapsedTextColor;
-
- private float mExpandedDrawY;
- private float mCollapsedDrawY;
- private float mExpandedDrawX;
- private float mCollapsedDrawX;
- private float mCurrentDrawX;
- private float mCurrentDrawY;
- private Typeface mCollapsedTypeface;
- private Typeface mExpandedTypeface;
- private Typeface mCurrentTypeface;
-
- private CharSequence mText;
- private CharSequence mTextToDraw;
- private boolean mIsRtl;
-
- private boolean mUseTexture;
- private Bitmap mExpandedTitleTexture;
- private Paint mTexturePaint;
- private float mTextureAscent;
- private float mTextureDescent;
-
- private float mScale;
- private float mCurrentTextSize;
-
- private int[] mState;
-
- private boolean mBoundsChanged;
-
- private final TextPaint mTextPaint;
-
- private Interpolator mPositionInterpolator;
- private Interpolator mTextSizeInterpolator;
-
- private float mCollapsedShadowRadius, mCollapsedShadowDx, mCollapsedShadowDy;
- private int mCollapsedShadowColor;
-
- private float mExpandedShadowRadius, mExpandedShadowDx, mExpandedShadowDy;
- private int mExpandedShadowColor;
-
- public CollapsingTextHelper(View view) {
- mView = view;
-
- mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG | Paint.SUBPIXEL_TEXT_FLAG);
-
- mCollapsedBounds = new Rect();
- mExpandedBounds = new Rect();
- mCurrentBounds = new RectF();
- }
-
- void setTextSizeInterpolator(Interpolator interpolator) {
- mTextSizeInterpolator = interpolator;
- recalculate();
- }
-
- void setPositionInterpolator(Interpolator interpolator) {
- mPositionInterpolator = interpolator;
- recalculate();
- }
-
- void setExpandedTextSize(float textSize) {
- if (mExpandedTextSize != textSize) {
- mExpandedTextSize = textSize;
- recalculate();
- }
- }
-
- void setCollapsedTextSize(float textSize) {
- if (mCollapsedTextSize != textSize) {
- mCollapsedTextSize = textSize;
- recalculate();
- }
- }
-
- void setCollapsedTextColor(ColorStateList textColor) {
- if (mCollapsedTextColor != textColor) {
- mCollapsedTextColor = textColor;
- recalculate();
- }
- }
-
- void setExpandedTextColor(ColorStateList textColor) {
- if (mExpandedTextColor != textColor) {
- mExpandedTextColor = textColor;
- recalculate();
- }
- }
-
- void setExpandedBounds(int left, int top, int right, int bottom) {
- if (!rectEquals(mExpandedBounds, left, top, right, bottom)) {
- mExpandedBounds.set(left, top, right, bottom);
- mBoundsChanged = true;
- onBoundsChanged();
- }
- }
-
- void setCollapsedBounds(int left, int top, int right, int bottom) {
- if (!rectEquals(mCollapsedBounds, left, top, right, bottom)) {
- mCollapsedBounds.set(left, top, right, bottom);
- mBoundsChanged = true;
- onBoundsChanged();
- }
- }
-
- void onBoundsChanged() {
- mDrawTitle = mCollapsedBounds.width() > 0 && mCollapsedBounds.height() > 0
- && mExpandedBounds.width() > 0 && mExpandedBounds.height() > 0;
- }
-
- void setExpandedTextGravity(int gravity) {
- if (mExpandedTextGravity != gravity) {
- mExpandedTextGravity = gravity;
- recalculate();
- }
- }
-
- int getExpandedTextGravity() {
- return mExpandedTextGravity;
- }
-
- void setCollapsedTextGravity(int gravity) {
- if (mCollapsedTextGravity != gravity) {
- mCollapsedTextGravity = gravity;
- recalculate();
- }
- }
-
- int getCollapsedTextGravity() {
- return mCollapsedTextGravity;
- }
-
- void setCollapsedTextAppearance(int resId) {
- TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), resId,
- android.support.v7.appcompat.R.styleable.TextAppearance);
- if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor)) {
- mCollapsedTextColor = a.getColorStateList(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor);
- }
- if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize)) {
- mCollapsedTextSize = a.getDimensionPixelSize(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize,
- (int) mCollapsedTextSize);
- }
- mCollapsedShadowColor = a.getInt(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowColor, 0);
- mCollapsedShadowDx = a.getFloat(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDx, 0);
- mCollapsedShadowDy = a.getFloat(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDy, 0);
- mCollapsedShadowRadius = a.getFloat(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowRadius, 0);
- a.recycle();
-
- if (Build.VERSION.SDK_INT >= 16) {
- mCollapsedTypeface = readFontFamilyTypeface(resId);
- }
-
- recalculate();
- }
-
- void setExpandedTextAppearance(int resId) {
- TintTypedArray a = TintTypedArray.obtainStyledAttributes(mView.getContext(), resId,
- android.support.v7.appcompat.R.styleable.TextAppearance);
- if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor)) {
- mExpandedTextColor = a.getColorStateList(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor);
- }
- if (a.hasValue(android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize)) {
- mExpandedTextSize = a.getDimensionPixelSize(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize,
- (int) mExpandedTextSize);
- }
- mExpandedShadowColor = a.getInt(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowColor, 0);
- mExpandedShadowDx = a.getFloat(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDx, 0);
- mExpandedShadowDy = a.getFloat(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowDy, 0);
- mExpandedShadowRadius = a.getFloat(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_shadowRadius, 0);
- a.recycle();
-
- if (Build.VERSION.SDK_INT >= 16) {
- mExpandedTypeface = readFontFamilyTypeface(resId);
- }
-
- recalculate();
- }
-
- private Typeface readFontFamilyTypeface(int resId) {
- final TypedArray a = mView.getContext().obtainStyledAttributes(resId,
- new int[]{android.R.attr.fontFamily});
- try {
- final String family = a.getString(0);
- if (family != null) {
- return Typeface.create(family, Typeface.NORMAL);
- }
- } finally {
- a.recycle();
- }
- return null;
- }
-
- void setCollapsedTypeface(Typeface typeface) {
- if (areTypefacesDifferent(mCollapsedTypeface, typeface)) {
- mCollapsedTypeface = typeface;
- recalculate();
- }
- }
-
- void setExpandedTypeface(Typeface typeface) {
- if (areTypefacesDifferent(mExpandedTypeface, typeface)) {
- mExpandedTypeface = typeface;
- recalculate();
- }
- }
-
- void setTypefaces(Typeface typeface) {
- mCollapsedTypeface = mExpandedTypeface = typeface;
- recalculate();
- }
-
- Typeface getCollapsedTypeface() {
- return mCollapsedTypeface != null ? mCollapsedTypeface : Typeface.DEFAULT;
- }
-
- Typeface getExpandedTypeface() {
- return mExpandedTypeface != null ? mExpandedTypeface : Typeface.DEFAULT;
- }
-
- /**
- * Set the value indicating the current scroll value. This decides how much of the
- * background will be displayed, as well as the title metrics/positioning.
- *
- * A value of {@code 0.0} indicates that the layout is fully expanded.
- * A value of {@code 1.0} indicates that the layout is fully collapsed.
- */
- void setExpansionFraction(float fraction) {
- fraction = MathUtils.clamp(fraction, 0f, 1f);
-
- if (fraction != mExpandedFraction) {
- mExpandedFraction = fraction;
- calculateCurrentOffsets();
- }
- }
-
- final boolean setState(final int[] state) {
- mState = state;
-
- if (isStateful()) {
- recalculate();
- return true;
- }
-
- return false;
- }
-
- final boolean isStateful() {
- return (mCollapsedTextColor != null && mCollapsedTextColor.isStateful())
- || (mExpandedTextColor != null && mExpandedTextColor.isStateful());
- }
-
- float getExpansionFraction() {
- return mExpandedFraction;
- }
-
- float getCollapsedTextSize() {
- return mCollapsedTextSize;
- }
-
- float getExpandedTextSize() {
- return mExpandedTextSize;
- }
-
- private void calculateCurrentOffsets() {
- calculateOffsets(mExpandedFraction);
- }
-
- private void calculateOffsets(final float fraction) {
- interpolateBounds(fraction);
- mCurrentDrawX = lerp(mExpandedDrawX, mCollapsedDrawX, fraction,
- mPositionInterpolator);
- mCurrentDrawY = lerp(mExpandedDrawY, mCollapsedDrawY, fraction,
- mPositionInterpolator);
-
- setInterpolatedTextSize(lerp(mExpandedTextSize, mCollapsedTextSize,
- fraction, mTextSizeInterpolator));
-
- if (mCollapsedTextColor != mExpandedTextColor) {
- // If the collapsed and expanded text colors are different, blend them based on the
- // fraction
- mTextPaint.setColor(blendColors(
- getCurrentExpandedTextColor(), getCurrentCollapsedTextColor(), fraction));
- } else {
- mTextPaint.setColor(getCurrentCollapsedTextColor());
- }
-
- mTextPaint.setShadowLayer(
- lerp(mExpandedShadowRadius, mCollapsedShadowRadius, fraction, null),
- lerp(mExpandedShadowDx, mCollapsedShadowDx, fraction, null),
- lerp(mExpandedShadowDy, mCollapsedShadowDy, fraction, null),
- blendColors(mExpandedShadowColor, mCollapsedShadowColor, fraction));
-
- ViewCompat.postInvalidateOnAnimation(mView);
- }
-
- @ColorInt
- private int getCurrentExpandedTextColor() {
- if (mState != null) {
- return mExpandedTextColor.getColorForState(mState, 0);
- } else {
- return mExpandedTextColor.getDefaultColor();
- }
- }
-
- @ColorInt
- private int getCurrentCollapsedTextColor() {
- if (mState != null) {
- return mCollapsedTextColor.getColorForState(mState, 0);
- } else {
- return mCollapsedTextColor.getDefaultColor();
- }
- }
-
- private void calculateBaseOffsets() {
- final float currentTextSize = mCurrentTextSize;
-
- // We then calculate the collapsed text size, using the same logic
- calculateUsingTextSize(mCollapsedTextSize);
- float width = mTextToDraw != null ?
- mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0;
- final int collapsedAbsGravity = GravityCompat.getAbsoluteGravity(mCollapsedTextGravity,
- mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
- switch (collapsedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
- case Gravity.BOTTOM:
- mCollapsedDrawY = mCollapsedBounds.bottom;
- break;
- case Gravity.TOP:
- mCollapsedDrawY = mCollapsedBounds.top - mTextPaint.ascent();
- break;
- case Gravity.CENTER_VERTICAL:
- default:
- float textHeight = mTextPaint.descent() - mTextPaint.ascent();
- float textOffset = (textHeight / 2) - mTextPaint.descent();
- mCollapsedDrawY = mCollapsedBounds.centerY() + textOffset;
- break;
- }
- switch (collapsedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
- case Gravity.CENTER_HORIZONTAL:
- mCollapsedDrawX = mCollapsedBounds.centerX() - (width / 2);
- break;
- case Gravity.RIGHT:
- mCollapsedDrawX = mCollapsedBounds.right - width;
- break;
- case Gravity.LEFT:
- default:
- mCollapsedDrawX = mCollapsedBounds.left;
- break;
- }
-
- calculateUsingTextSize(mExpandedTextSize);
- width = mTextToDraw != null
- ? mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()) : 0;
- final int expandedAbsGravity = GravityCompat.getAbsoluteGravity(mExpandedTextGravity,
- mIsRtl ? ViewCompat.LAYOUT_DIRECTION_RTL : ViewCompat.LAYOUT_DIRECTION_LTR);
- switch (expandedAbsGravity & Gravity.VERTICAL_GRAVITY_MASK) {
- case Gravity.BOTTOM:
- mExpandedDrawY = mExpandedBounds.bottom;
- break;
- case Gravity.TOP:
- mExpandedDrawY = mExpandedBounds.top - mTextPaint.ascent();
- break;
- case Gravity.CENTER_VERTICAL:
- default:
- float textHeight = mTextPaint.descent() - mTextPaint.ascent();
- float textOffset = (textHeight / 2) - mTextPaint.descent();
- mExpandedDrawY = mExpandedBounds.centerY() + textOffset;
- break;
- }
- switch (expandedAbsGravity & GravityCompat.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
- case Gravity.CENTER_HORIZONTAL:
- mExpandedDrawX = mExpandedBounds.centerX() - (width / 2);
- break;
- case Gravity.RIGHT:
- mExpandedDrawX = mExpandedBounds.right - width;
- break;
- case Gravity.LEFT:
- default:
- mExpandedDrawX = mExpandedBounds.left;
- break;
- }
-
- // The bounds have changed so we need to clear the texture
- clearTexture();
- // Now reset the text size back to the original
- setInterpolatedTextSize(currentTextSize);
- }
-
- private void interpolateBounds(float fraction) {
- mCurrentBounds.left = lerp(mExpandedBounds.left, mCollapsedBounds.left,
- fraction, mPositionInterpolator);
- mCurrentBounds.top = lerp(mExpandedDrawY, mCollapsedDrawY,
- fraction, mPositionInterpolator);
- mCurrentBounds.right = lerp(mExpandedBounds.right, mCollapsedBounds.right,
- fraction, mPositionInterpolator);
- mCurrentBounds.bottom = lerp(mExpandedBounds.bottom, mCollapsedBounds.bottom,
- fraction, mPositionInterpolator);
- }
-
- public void draw(Canvas canvas) {
- final int saveCount = canvas.save();
-
- if (mTextToDraw != null && mDrawTitle) {
- float x = mCurrentDrawX;
- float y = mCurrentDrawY;
-
- final boolean drawTexture = mUseTexture && mExpandedTitleTexture != null;
-
- final float ascent;
- final float descent;
- if (drawTexture) {
- ascent = mTextureAscent * mScale;
- descent = mTextureDescent * mScale;
- } else {
- ascent = mTextPaint.ascent() * mScale;
- descent = mTextPaint.descent() * mScale;
- }
-
- if (DEBUG_DRAW) {
- // Just a debug tool, which drawn a magenta rect in the text bounds
- canvas.drawRect(mCurrentBounds.left, y + ascent, mCurrentBounds.right, y + descent,
- DEBUG_DRAW_PAINT);
- }
-
- if (drawTexture) {
- y += ascent;
- }
-
- if (mScale != 1f) {
- canvas.scale(mScale, mScale, x, y);
- }
-
- if (drawTexture) {
- // If we should use a texture, draw it instead of text
- canvas.drawBitmap(mExpandedTitleTexture, x, y, mTexturePaint);
- } else {
- canvas.drawText(mTextToDraw, 0, mTextToDraw.length(), x, y, mTextPaint);
- }
- }
-
- canvas.restoreToCount(saveCount);
- }
-
- private boolean calculateIsRtl(CharSequence text) {
- final boolean defaultIsRtl = ViewCompat.getLayoutDirection(mView)
- == ViewCompat.LAYOUT_DIRECTION_RTL;
- return (defaultIsRtl
- ? TextDirectionHeuristicsCompat.FIRSTSTRONG_RTL
- : TextDirectionHeuristicsCompat.FIRSTSTRONG_LTR).isRtl(text, 0, text.length());
- }
-
- private void setInterpolatedTextSize(float textSize) {
- calculateUsingTextSize(textSize);
-
- // Use our texture if the scale isn't 1.0
- mUseTexture = USE_SCALING_TEXTURE && mScale != 1f;
-
- if (mUseTexture) {
- // Make sure we have an expanded texture if needed
- ensureExpandedTexture();
- }
-
- ViewCompat.postInvalidateOnAnimation(mView);
- }
-
- private boolean areTypefacesDifferent(Typeface first, Typeface second) {
- return (first != null && !first.equals(second)) || (first == null && second != null);
- }
-
- private void calculateUsingTextSize(final float textSize) {
- if (mText == null) return;
-
- final float collapsedWidth = mCollapsedBounds.width();
- final float expandedWidth = mExpandedBounds.width();
-
- final float availableWidth;
- final float newTextSize;
- boolean updateDrawText = false;
-
- if (isClose(textSize, mCollapsedTextSize)) {
- newTextSize = mCollapsedTextSize;
- mScale = 1f;
- if (areTypefacesDifferent(mCurrentTypeface, mCollapsedTypeface)) {
- mCurrentTypeface = mCollapsedTypeface;
- updateDrawText = true;
- }
- availableWidth = collapsedWidth;
- } else {
- newTextSize = mExpandedTextSize;
- if (areTypefacesDifferent(mCurrentTypeface, mExpandedTypeface)) {
- mCurrentTypeface = mExpandedTypeface;
- updateDrawText = true;
- }
- if (isClose(textSize, mExpandedTextSize)) {
- // If we're close to the expanded text size, snap to it and use a scale of 1
- mScale = 1f;
- } else {
- // Else, we'll scale down from the expanded text size
- mScale = textSize / mExpandedTextSize;
- }
-
- final float textSizeRatio = mCollapsedTextSize / mExpandedTextSize;
- // This is the size of the expanded bounds when it is scaled to match the
- // collapsed text size
- final float scaledDownWidth = expandedWidth * textSizeRatio;
-
- if (scaledDownWidth > collapsedWidth) {
- // If the scaled down size is larger than the actual collapsed width, we need to
- // cap the available width so that when the expanded text scales down, it matches
- // the collapsed width
- availableWidth = Math.min(collapsedWidth / textSizeRatio, expandedWidth);
- } else {
- // Otherwise we'll just use the expanded width
- availableWidth = expandedWidth;
- }
- }
-
- if (availableWidth > 0) {
- updateDrawText = (mCurrentTextSize != newTextSize) || mBoundsChanged || updateDrawText;
- mCurrentTextSize = newTextSize;
- mBoundsChanged = false;
- }
-
- if (mTextToDraw == null || updateDrawText) {
- mTextPaint.setTextSize(mCurrentTextSize);
- mTextPaint.setTypeface(mCurrentTypeface);
- // Use linear text scaling if we're scaling the canvas
- mTextPaint.setLinearText(mScale != 1f);
-
- // If we don't currently have text to draw, or the text size has changed, ellipsize...
- final CharSequence title = TextUtils.ellipsize(mText, mTextPaint,
- availableWidth, TextUtils.TruncateAt.END);
- if (!TextUtils.equals(title, mTextToDraw)) {
- mTextToDraw = title;
- mIsRtl = calculateIsRtl(mTextToDraw);
- }
- }
- }
-
- private void ensureExpandedTexture() {
- if (mExpandedTitleTexture != null || mExpandedBounds.isEmpty()
- || TextUtils.isEmpty(mTextToDraw)) {
- return;
- }
-
- calculateOffsets(0f);
- mTextureAscent = mTextPaint.ascent();
- mTextureDescent = mTextPaint.descent();
-
- final int w = Math.round(mTextPaint.measureText(mTextToDraw, 0, mTextToDraw.length()));
- final int h = Math.round(mTextureDescent - mTextureAscent);
-
- if (w <= 0 || h <= 0) {
- return; // If the width or height are 0, return
- }
-
- mExpandedTitleTexture = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
-
- Canvas c = new Canvas(mExpandedTitleTexture);
- c.drawText(mTextToDraw, 0, mTextToDraw.length(), 0, h - mTextPaint.descent(), mTextPaint);
-
- if (mTexturePaint == null) {
- // Make sure we have a paint
- mTexturePaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
- }
- }
-
- public void recalculate() {
- if (mView.getHeight() > 0 && mView.getWidth() > 0) {
- // If we've already been laid out, calculate everything now otherwise we'll wait
- // until a layout
- calculateBaseOffsets();
- calculateCurrentOffsets();
- }
- }
-
- /**
- * Set the title to display
- *
- * @param text
- */
- void setText(CharSequence text) {
- if (text == null || !text.equals(mText)) {
- mText = text;
- mTextToDraw = null;
- clearTexture();
- recalculate();
- }
- }
-
- CharSequence getText() {
- return mText;
- }
-
- private void clearTexture() {
- if (mExpandedTitleTexture != null) {
- mExpandedTitleTexture.recycle();
- mExpandedTitleTexture = null;
- }
- }
-
- /**
- * Returns true if {@code value} is 'close' to it's closest decimal value. Close is currently
- * defined as it's difference being < 0.001.
- */
- private static boolean isClose(float value, float targetValue) {
- return Math.abs(value - targetValue) < 0.001f;
- }
-
- ColorStateList getExpandedTextColor() {
- return mExpandedTextColor;
- }
-
- ColorStateList getCollapsedTextColor() {
- return mCollapsedTextColor;
- }
-
- /**
- * Blend {@code color1} and {@code color2} using the given ratio.
- *
- * @param ratio of which to blend. 0.0 will return {@code color1}, 0.5 will give an even blend,
- * 1.0 will return {@code color2}.
- */
- private static int blendColors(int color1, int color2, float ratio) {
- final float inverseRatio = 1f - ratio;
- float a = (Color.alpha(color1) * inverseRatio) + (Color.alpha(color2) * ratio);
- float r = (Color.red(color1) * inverseRatio) + (Color.red(color2) * ratio);
- float g = (Color.green(color1) * inverseRatio) + (Color.green(color2) * ratio);
- float b = (Color.blue(color1) * inverseRatio) + (Color.blue(color2) * ratio);
- return Color.argb((int) a, (int) r, (int) g, (int) b);
- }
-
- private static float lerp(float startValue, float endValue, float fraction,
- Interpolator interpolator) {
- if (interpolator != null) {
- fraction = interpolator.getInterpolation(fraction);
- }
- return AnimationUtils.lerp(startValue, endValue, fraction);
- }
-
- private static boolean rectEquals(Rect r, int left, int top, int right, int bottom) {
- return !(r.left != left || r.top != top || r.right != right || r.bottom != bottom);
- }
-}
diff --git a/android/support/design/widget/CollapsingToolbarLayout.java b/android/support/design/widget/CollapsingToolbarLayout.java
deleted file mode 100644
index 8c9b7d49..00000000
--- a/android/support/design/widget/CollapsingToolbarLayout.java
+++ /dev/null
@@ -1,1308 +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 android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.TypedArray;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.Typeface;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.ColorInt;
-import android.support.annotation.DrawableRes;
-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.annotation.StyleRes;
-import android.support.design.R;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v4.math.MathUtils;
-import android.support.v4.util.ObjectsCompat;
-import android.support.v4.view.GravityCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.support.v4.widget.ViewGroupUtils;
-import android.support.v7.widget.Toolbar;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.widget.FrameLayout;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * CollapsingToolbarLayout is a wrapper for {@link Toolbar} which implements a collapsing app bar.
- * It is designed to be used as a direct child of a {@link AppBarLayout}.
- * CollapsingToolbarLayout contains the following features:
- *
- * <h4>Collapsing title</h4>
- * A title which is larger when the layout is fully visible but collapses and becomes smaller as
- * the layout is scrolled off screen. You can set the title to display via
- * {@link #setTitle(CharSequence)}. The title appearance can be tweaked via the
- * {@code collapsedTextAppearance} and {@code expandedTextAppearance} attributes.
- *
- * <h4>Content scrim</h4>
- * A full-bleed scrim which is show or hidden when the scroll position has hit a certain threshold.
- * You can change this via {@link #setContentScrim(Drawable)}.
- *
- * <h4>Status bar scrim</h4>
- * A scrim which is show or hidden behind the status bar when the scroll position has hit a certain
- * threshold. You can change this via {@link #setStatusBarScrim(Drawable)}. This only works
- * on {@link android.os.Build.VERSION_CODES#LOLLIPOP LOLLIPOP} devices when we set to fit system
- * windows.
- *
- * <h4>Parallax scrolling children</h4>
- * Child views can opt to be scrolled within this layout in a parallax fashion.
- * See {@link LayoutParams#COLLAPSE_MODE_PARALLAX} and
- * {@link LayoutParams#setParallaxMultiplier(float)}.
- *
- * <h4>Pinned position children</h4>
- * Child views can opt to be pinned in space globally. This is useful when implementing a
- * collapsing as it allows the {@link Toolbar} to be fixed in place even though this layout is
- * moving. See {@link LayoutParams#COLLAPSE_MODE_PIN}.
- *
- * <p><strong>Do not manually add views to the Toolbar at run time</strong>.
- * We will add a 'dummy view' to the Toolbar which allows us to work out the available space
- * for the title. This can interfere with any views which you add.</p>
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleTextAppearance
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleTextAppearance
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_contentScrim
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMargin
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_statusBarScrim
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_toolbarId
- */
-public class CollapsingToolbarLayout extends FrameLayout {
-
- private static final int DEFAULT_SCRIM_ANIMATION_DURATION = 600;
-
- private boolean mRefreshToolbar = true;
- private int mToolbarId;
- private Toolbar mToolbar;
- private View mToolbarDirectChild;
- private View mDummyView;
-
- private int mExpandedMarginStart;
- private int mExpandedMarginTop;
- private int mExpandedMarginEnd;
- private int mExpandedMarginBottom;
-
- private final Rect mTmpRect = new Rect();
- final CollapsingTextHelper mCollapsingTextHelper;
- private boolean mCollapsingTitleEnabled;
- private boolean mDrawCollapsingTitle;
-
- private Drawable mContentScrim;
- Drawable mStatusBarScrim;
- private int mScrimAlpha;
- private boolean mScrimsAreShown;
- private ValueAnimator mScrimAnimator;
- private long mScrimAnimationDuration;
- private int mScrimVisibleHeightTrigger = -1;
-
- private AppBarLayout.OnOffsetChangedListener mOnOffsetChangedListener;
-
- int mCurrentOffset;
-
- WindowInsetsCompat mLastInsets;
-
- public CollapsingToolbarLayout(Context context) {
- this(context, null);
- }
-
- public CollapsingToolbarLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public CollapsingToolbarLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- mCollapsingTextHelper = new CollapsingTextHelper(this);
- mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.DECELERATE_INTERPOLATOR);
-
- TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.CollapsingToolbarLayout, defStyleAttr,
- R.style.Widget_Design_CollapsingToolbar);
-
- mCollapsingTextHelper.setExpandedTextGravity(
- a.getInt(R.styleable.CollapsingToolbarLayout_expandedTitleGravity,
- GravityCompat.START | Gravity.BOTTOM));
- mCollapsingTextHelper.setCollapsedTextGravity(
- a.getInt(R.styleable.CollapsingToolbarLayout_collapsedTitleGravity,
- GravityCompat.START | Gravity.CENTER_VERTICAL));
-
- mExpandedMarginStart = mExpandedMarginTop = mExpandedMarginEnd = mExpandedMarginBottom =
- a.getDimensionPixelSize(R.styleable.CollapsingToolbarLayout_expandedTitleMargin, 0);
-
- if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart)) {
- mExpandedMarginStart = a.getDimensionPixelSize(
- R.styleable.CollapsingToolbarLayout_expandedTitleMarginStart, 0);
- }
- if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd)) {
- mExpandedMarginEnd = a.getDimensionPixelSize(
- R.styleable.CollapsingToolbarLayout_expandedTitleMarginEnd, 0);
- }
- if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginTop)) {
- mExpandedMarginTop = a.getDimensionPixelSize(
- R.styleable.CollapsingToolbarLayout_expandedTitleMarginTop, 0);
- }
- if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom)) {
- mExpandedMarginBottom = a.getDimensionPixelSize(
- R.styleable.CollapsingToolbarLayout_expandedTitleMarginBottom, 0);
- }
-
- mCollapsingTitleEnabled = a.getBoolean(
- R.styleable.CollapsingToolbarLayout_titleEnabled, true);
- setTitle(a.getText(R.styleable.CollapsingToolbarLayout_title));
-
- // First load the default text appearances
- mCollapsingTextHelper.setExpandedTextAppearance(
- R.style.TextAppearance_Design_CollapsingToolbar_Expanded);
- mCollapsingTextHelper.setCollapsedTextAppearance(
- android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Widget_ActionBar_Title);
-
- // Now overlay any custom text appearances
- if (a.hasValue(R.styleable.CollapsingToolbarLayout_expandedTitleTextAppearance)) {
- mCollapsingTextHelper.setExpandedTextAppearance(
- a.getResourceId(
- R.styleable.CollapsingToolbarLayout_expandedTitleTextAppearance, 0));
- }
- if (a.hasValue(R.styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance)) {
- mCollapsingTextHelper.setCollapsedTextAppearance(
- a.getResourceId(
- R.styleable.CollapsingToolbarLayout_collapsedTitleTextAppearance, 0));
- }
-
- mScrimVisibleHeightTrigger = a.getDimensionPixelSize(
- R.styleable.CollapsingToolbarLayout_scrimVisibleHeightTrigger, -1);
-
- mScrimAnimationDuration = a.getInt(
- R.styleable.CollapsingToolbarLayout_scrimAnimationDuration,
- DEFAULT_SCRIM_ANIMATION_DURATION);
-
- setContentScrim(a.getDrawable(R.styleable.CollapsingToolbarLayout_contentScrim));
- setStatusBarScrim(a.getDrawable(R.styleable.CollapsingToolbarLayout_statusBarScrim));
-
- mToolbarId = a.getResourceId(R.styleable.CollapsingToolbarLayout_toolbarId, -1);
-
- a.recycle();
-
- setWillNotDraw(false);
-
- ViewCompat.setOnApplyWindowInsetsListener(this,
- new android.support.v4.view.OnApplyWindowInsetsListener() {
- @Override
- public WindowInsetsCompat onApplyWindowInsets(View v,
- WindowInsetsCompat insets) {
- return onWindowInsetChanged(insets);
- }
- });
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- // Add an OnOffsetChangedListener if possible
- final ViewParent parent = getParent();
- if (parent instanceof AppBarLayout) {
- // Copy over from the ABL whether we should fit system windows
- ViewCompat.setFitsSystemWindows(this, ViewCompat.getFitsSystemWindows((View) parent));
-
- if (mOnOffsetChangedListener == null) {
- mOnOffsetChangedListener = new OffsetUpdateListener();
- }
- ((AppBarLayout) parent).addOnOffsetChangedListener(mOnOffsetChangedListener);
-
- // We're attached, so lets request an inset dispatch
- ViewCompat.requestApplyInsets(this);
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- // Remove our OnOffsetChangedListener if possible and it exists
- final ViewParent parent = getParent();
- if (mOnOffsetChangedListener != null && parent instanceof AppBarLayout) {
- ((AppBarLayout) parent).removeOnOffsetChangedListener(mOnOffsetChangedListener);
- }
-
- super.onDetachedFromWindow();
- }
-
- WindowInsetsCompat onWindowInsetChanged(final WindowInsetsCompat insets) {
- WindowInsetsCompat newInsets = null;
-
- if (ViewCompat.getFitsSystemWindows(this)) {
- // If we're set to fit system windows, keep the insets
- newInsets = insets;
- }
-
- // If our insets have changed, keep them and invalidate the scroll ranges...
- if (!ObjectsCompat.equals(mLastInsets, newInsets)) {
- mLastInsets = newInsets;
- requestLayout();
- }
-
- // Consume the insets. This is done so that child views with fitSystemWindows=true do not
- // get the default padding functionality from View
- return insets.consumeSystemWindowInsets();
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
-
- // If we don't have a toolbar, the scrim will be not be drawn in drawChild() below.
- // Instead, we draw it here, before our collapsing text.
- ensureToolbar();
- if (mToolbar == null && mContentScrim != null && mScrimAlpha > 0) {
- mContentScrim.mutate().setAlpha(mScrimAlpha);
- mContentScrim.draw(canvas);
- }
-
- // Let the collapsing text helper draw its text
- if (mCollapsingTitleEnabled && mDrawCollapsingTitle) {
- mCollapsingTextHelper.draw(canvas);
- }
-
- // Now draw the status bar scrim
- if (mStatusBarScrim != null && mScrimAlpha > 0) {
- final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
- if (topInset > 0) {
- mStatusBarScrim.setBounds(0, -mCurrentOffset, getWidth(),
- topInset - mCurrentOffset);
- mStatusBarScrim.mutate().setAlpha(mScrimAlpha);
- mStatusBarScrim.draw(canvas);
- }
- }
- }
-
- @Override
- protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
- // This is a little weird. Our scrim needs to be behind the Toolbar (if it is present),
- // but in front of any other children which are behind it. To do this we intercept the
- // drawChild() call, and draw our scrim just before the Toolbar is drawn
- boolean invalidated = false;
- if (mContentScrim != null && mScrimAlpha > 0 && isToolbarChild(child)) {
- mContentScrim.mutate().setAlpha(mScrimAlpha);
- mContentScrim.draw(canvas);
- invalidated = true;
- }
- return super.drawChild(canvas, child, drawingTime) || invalidated;
- }
-
- @Override
- protected void onSizeChanged(int w, int h, int oldw, int oldh) {
- super.onSizeChanged(w, h, oldw, oldh);
- if (mContentScrim != null) {
- mContentScrim.setBounds(0, 0, w, h);
- }
- }
-
- private void ensureToolbar() {
- if (!mRefreshToolbar) {
- return;
- }
-
- // First clear out the current Toolbar
- mToolbar = null;
- mToolbarDirectChild = null;
-
- if (mToolbarId != -1) {
- // If we have an ID set, try and find it and it's direct parent to us
- mToolbar = findViewById(mToolbarId);
- if (mToolbar != null) {
- mToolbarDirectChild = findDirectChild(mToolbar);
- }
- }
-
- if (mToolbar == null) {
- // If we don't have an ID, or couldn't find a Toolbar with the correct ID, try and find
- // one from our direct children
- Toolbar toolbar = null;
- for (int i = 0, count = getChildCount(); i < count; i++) {
- final View child = getChildAt(i);
- if (child instanceof Toolbar) {
- toolbar = (Toolbar) child;
- break;
- }
- }
- mToolbar = toolbar;
- }
-
- updateDummyView();
- mRefreshToolbar = false;
- }
-
- private boolean isToolbarChild(View child) {
- return (mToolbarDirectChild == null || mToolbarDirectChild == this)
- ? child == mToolbar
- : child == mToolbarDirectChild;
- }
-
- /**
- * Returns the direct child of this layout, which itself is the ancestor of the
- * given view.
- */
- private View findDirectChild(final View descendant) {
- View directChild = descendant;
- for (ViewParent p = descendant.getParent(); p != this && p != null; p = p.getParent()) {
- if (p instanceof View) {
- directChild = (View) p;
- }
- }
- return directChild;
- }
-
- private void updateDummyView() {
- if (!mCollapsingTitleEnabled && mDummyView != null) {
- // If we have a dummy view and we have our title disabled, remove it from its parent
- final ViewParent parent = mDummyView.getParent();
- if (parent instanceof ViewGroup) {
- ((ViewGroup) parent).removeView(mDummyView);
- }
- }
- if (mCollapsingTitleEnabled && mToolbar != null) {
- if (mDummyView == null) {
- mDummyView = new View(getContext());
- }
- if (mDummyView.getParent() == null) {
- mToolbar.addView(mDummyView, LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
- }
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- ensureToolbar();
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- final int mode = MeasureSpec.getMode(heightMeasureSpec);
- final int topInset = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
- if (mode == MeasureSpec.UNSPECIFIED && topInset > 0) {
- // If we have a top inset and we're set to wrap_content height we need to make sure
- // we add the top inset to our height, therefore we re-measure
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(
- getMeasuredHeight() + topInset, MeasureSpec.EXACTLY);
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
-
- if (mLastInsets != null) {
- // Shift down any views which are not set to fit system windows
- final int insetTop = mLastInsets.getSystemWindowInsetTop();
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- if (!ViewCompat.getFitsSystemWindows(child)) {
- if (child.getTop() < insetTop) {
- // If the child isn't set to fit system windows but is drawing within
- // the inset offset it down
- ViewCompat.offsetTopAndBottom(child, insetTop);
- }
- }
- }
- }
-
- // Update the collapsed bounds by getting it's transformed bounds
- if (mCollapsingTitleEnabled && mDummyView != null) {
- // We only draw the title if the dummy view is being displayed (Toolbar removes
- // views if there is no space)
- mDrawCollapsingTitle = ViewCompat.isAttachedToWindow(mDummyView)
- && mDummyView.getVisibility() == VISIBLE;
-
- if (mDrawCollapsingTitle) {
- final boolean isRtl = ViewCompat.getLayoutDirection(this)
- == ViewCompat.LAYOUT_DIRECTION_RTL;
-
- // Update the collapsed bounds
- final int maxOffset = getMaxOffsetForPinChild(
- mToolbarDirectChild != null ? mToolbarDirectChild : mToolbar);
- ViewGroupUtils.getDescendantRect(this, mDummyView, mTmpRect);
- mCollapsingTextHelper.setCollapsedBounds(
- mTmpRect.left + (isRtl
- ? mToolbar.getTitleMarginEnd()
- : mToolbar.getTitleMarginStart()),
- mTmpRect.top + maxOffset + mToolbar.getTitleMarginTop(),
- mTmpRect.right + (isRtl
- ? mToolbar.getTitleMarginStart()
- : mToolbar.getTitleMarginEnd()),
- mTmpRect.bottom + maxOffset - mToolbar.getTitleMarginBottom());
-
- // Update the expanded bounds
- mCollapsingTextHelper.setExpandedBounds(
- isRtl ? mExpandedMarginEnd : mExpandedMarginStart,
- mTmpRect.top + mExpandedMarginTop,
- right - left - (isRtl ? mExpandedMarginStart : mExpandedMarginEnd),
- bottom - top - mExpandedMarginBottom);
- // Now recalculate using the new bounds
- mCollapsingTextHelper.recalculate();
- }
- }
-
- // Update our child view offset helpers. This needs to be done after the title has been
- // setup, so that any Toolbars are in their original position
- for (int i = 0, z = getChildCount(); i < z; i++) {
- getViewOffsetHelper(getChildAt(i)).onViewLayout();
- }
-
- // Finally, set our minimum height to enable proper AppBarLayout collapsing
- if (mToolbar != null) {
- if (mCollapsingTitleEnabled && TextUtils.isEmpty(mCollapsingTextHelper.getText())) {
- // If we do not currently have a title, try and grab it from the Toolbar
- mCollapsingTextHelper.setText(mToolbar.getTitle());
- }
- if (mToolbarDirectChild == null || mToolbarDirectChild == this) {
- setMinimumHeight(getHeightWithMargins(mToolbar));
- } else {
- setMinimumHeight(getHeightWithMargins(mToolbarDirectChild));
- }
- }
-
- updateScrimVisibility();
- }
-
- private static int getHeightWithMargins(@NonNull final View view) {
- final ViewGroup.LayoutParams lp = view.getLayoutParams();
- if (lp instanceof MarginLayoutParams) {
- final MarginLayoutParams mlp = (MarginLayoutParams) lp;
- return view.getHeight() + mlp.topMargin + mlp.bottomMargin;
- }
- return view.getHeight();
- }
-
- static ViewOffsetHelper getViewOffsetHelper(View view) {
- ViewOffsetHelper offsetHelper = (ViewOffsetHelper) view.getTag(R.id.view_offset_helper);
- if (offsetHelper == null) {
- offsetHelper = new ViewOffsetHelper(view);
- view.setTag(R.id.view_offset_helper, offsetHelper);
- }
- return offsetHelper;
- }
-
- /**
- * Sets the title to be displayed by this view, if enabled.
- *
- * @see #setTitleEnabled(boolean)
- * @see #getTitle()
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_title
- */
- public void setTitle(@Nullable CharSequence title) {
- mCollapsingTextHelper.setText(title);
- }
-
- /**
- * Returns the title currently being displayed by this view. If the title is not enabled, then
- * this will return {@code null}.
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_title
- */
- @Nullable
- public CharSequence getTitle() {
- return mCollapsingTitleEnabled ? mCollapsingTextHelper.getText() : null;
- }
-
- /**
- * Sets whether this view should display its own title.
- *
- * <p>The title displayed by this view will shrink and grow based on the scroll offset.</p>
- *
- * @see #setTitle(CharSequence)
- * @see #isTitleEnabled()
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_titleEnabled
- */
- public void setTitleEnabled(boolean enabled) {
- if (enabled != mCollapsingTitleEnabled) {
- mCollapsingTitleEnabled = enabled;
- updateDummyView();
- requestLayout();
- }
- }
-
- /**
- * Returns whether this view is currently displaying its own title.
- *
- * @see #setTitleEnabled(boolean)
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_titleEnabled
- */
- public boolean isTitleEnabled() {
- return mCollapsingTitleEnabled;
- }
-
- /**
- * Set whether the content scrim and/or status bar scrim should be shown or not. Any change
- * in the vertical scroll may overwrite this value. Any visibility change will be animated if
- * this view has already been laid out.
- *
- * @param shown whether the scrims should be shown
- *
- * @see #getStatusBarScrim()
- * @see #getContentScrim()
- */
- public void setScrimsShown(boolean shown) {
- setScrimsShown(shown, ViewCompat.isLaidOut(this) && !isInEditMode());
- }
-
- /**
- * Set whether the content scrim and/or status bar scrim should be shown or not. Any change
- * in the vertical scroll may overwrite this value.
- *
- * @param shown whether the scrims should be shown
- * @param animate whether to animate the visibility change
- *
- * @see #getStatusBarScrim()
- * @see #getContentScrim()
- */
- public void setScrimsShown(boolean shown, boolean animate) {
- if (mScrimsAreShown != shown) {
- if (animate) {
- animateScrim(shown ? 0xFF : 0x0);
- } else {
- setScrimAlpha(shown ? 0xFF : 0x0);
- }
- mScrimsAreShown = shown;
- }
- }
-
- private void animateScrim(int targetAlpha) {
- ensureToolbar();
- if (mScrimAnimator == null) {
- mScrimAnimator = new ValueAnimator();
- mScrimAnimator.setDuration(mScrimAnimationDuration);
- mScrimAnimator.setInterpolator(
- targetAlpha > mScrimAlpha
- ? AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR
- : AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR);
- mScrimAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- setScrimAlpha((int) animator.getAnimatedValue());
- }
- });
- } else if (mScrimAnimator.isRunning()) {
- mScrimAnimator.cancel();
- }
-
- mScrimAnimator.setIntValues(mScrimAlpha, targetAlpha);
- mScrimAnimator.start();
- }
-
- void setScrimAlpha(int alpha) {
- if (alpha != mScrimAlpha) {
- final Drawable contentScrim = mContentScrim;
- if (contentScrim != null && mToolbar != null) {
- ViewCompat.postInvalidateOnAnimation(mToolbar);
- }
- mScrimAlpha = alpha;
- ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this);
- }
- }
-
- int getScrimAlpha() {
- return mScrimAlpha;
- }
-
- /**
- * Set the drawable to use for the content scrim from resources. Providing null will disable
- * the scrim functionality.
- *
- * @param drawable the drawable to display
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim
- * @see #getContentScrim()
- */
- public void setContentScrim(@Nullable Drawable drawable) {
- if (mContentScrim != drawable) {
- if (mContentScrim != null) {
- mContentScrim.setCallback(null);
- }
- mContentScrim = drawable != null ? drawable.mutate() : null;
- if (mContentScrim != null) {
- mContentScrim.setBounds(0, 0, getWidth(), getHeight());
- mContentScrim.setCallback(this);
- mContentScrim.setAlpha(mScrimAlpha);
- }
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- /**
- * Set the color to use for the content scrim.
- *
- * @param color the color to display
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim
- * @see #getContentScrim()
- */
- public void setContentScrimColor(@ColorInt int color) {
- setContentScrim(new ColorDrawable(color));
- }
-
- /**
- * Set the drawable to use for the content scrim from resources.
- *
- * @param resId drawable resource id
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim
- * @see #getContentScrim()
- */
- public void setContentScrimResource(@DrawableRes int resId) {
- setContentScrim(ContextCompat.getDrawable(getContext(), resId));
-
- }
-
- /**
- * Returns the drawable which is used for the foreground scrim.
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_contentScrim
- * @see #setContentScrim(Drawable)
- */
- @Nullable
- public Drawable getContentScrim() {
- return mContentScrim;
- }
-
- /**
- * Set the drawable to use for the status bar scrim from resources.
- * Providing null will disable the scrim functionality.
- *
- * <p>This scrim is only shown when we have been given a top system inset.</p>
- *
- * @param drawable the drawable to display
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim
- * @see #getStatusBarScrim()
- */
- public void setStatusBarScrim(@Nullable Drawable drawable) {
- if (mStatusBarScrim != drawable) {
- if (mStatusBarScrim != null) {
- mStatusBarScrim.setCallback(null);
- }
- mStatusBarScrim = drawable != null ? drawable.mutate() : null;
- if (mStatusBarScrim != null) {
- if (mStatusBarScrim.isStateful()) {
- mStatusBarScrim.setState(getDrawableState());
- }
- DrawableCompat.setLayoutDirection(mStatusBarScrim,
- ViewCompat.getLayoutDirection(this));
- mStatusBarScrim.setVisible(getVisibility() == VISIBLE, false);
- mStatusBarScrim.setCallback(this);
- mStatusBarScrim.setAlpha(mScrimAlpha);
- }
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
-
- final int[] state = getDrawableState();
- boolean changed = false;
-
- Drawable d = mStatusBarScrim;
- if (d != null && d.isStateful()) {
- changed |= d.setState(state);
- }
- d = mContentScrim;
- if (d != null && d.isStateful()) {
- changed |= d.setState(state);
- }
- if (mCollapsingTextHelper != null) {
- changed |= mCollapsingTextHelper.setState(state);
- }
-
- if (changed) {
- invalidate();
- }
- }
-
- @Override
- protected boolean verifyDrawable(Drawable who) {
- return super.verifyDrawable(who) || who == mContentScrim || who == mStatusBarScrim;
- }
-
- @Override
- public void setVisibility(int visibility) {
- super.setVisibility(visibility);
-
- final boolean visible = visibility == VISIBLE;
- if (mStatusBarScrim != null && mStatusBarScrim.isVisible() != visible) {
- mStatusBarScrim.setVisible(visible, false);
- }
- if (mContentScrim != null && mContentScrim.isVisible() != visible) {
- mContentScrim.setVisible(visible, false);
- }
- }
-
- /**
- * Set the color to use for the status bar scrim.
- *
- * <p>This scrim is only shown when we have been given a top system inset.</p>
- *
- * @param color the color to display
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim
- * @see #getStatusBarScrim()
- */
- public void setStatusBarScrimColor(@ColorInt int color) {
- setStatusBarScrim(new ColorDrawable(color));
- }
-
- /**
- * Set the drawable to use for the content scrim from resources.
- *
- * @param resId drawable resource id
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim
- * @see #getStatusBarScrim()
- */
- public void setStatusBarScrimResource(@DrawableRes int resId) {
- setStatusBarScrim(ContextCompat.getDrawable(getContext(), resId));
- }
-
- /**
- * Returns the drawable which is used for the status bar scrim.
- *
- * @attr ref R.styleable#CollapsingToolbarLayout_statusBarScrim
- * @see #setStatusBarScrim(Drawable)
- */
- @Nullable
- public Drawable getStatusBarScrim() {
- return mStatusBarScrim;
- }
-
- /**
- * Sets the text color and size for the collapsed title from the specified
- * TextAppearance resource.
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleTextAppearance
- */
- public void setCollapsedTitleTextAppearance(@StyleRes int resId) {
- mCollapsingTextHelper.setCollapsedTextAppearance(resId);
- }
-
- /**
- * Sets the text color of the collapsed title.
- *
- * @param color The new text color in ARGB format
- */
- public void setCollapsedTitleTextColor(@ColorInt int color) {
- setCollapsedTitleTextColor(ColorStateList.valueOf(color));
- }
-
- /**
- * Sets the text colors of the collapsed title.
- *
- * @param colors ColorStateList containing the new text colors
- */
- public void setCollapsedTitleTextColor(@NonNull ColorStateList colors) {
- mCollapsingTextHelper.setCollapsedTextColor(colors);
- }
-
- /**
- * Sets the horizontal alignment of the collapsed title and the vertical gravity that will
- * be used when there is extra space in the collapsed bounds beyond what is required for
- * the title itself.
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleGravity
- */
- public void setCollapsedTitleGravity(int gravity) {
- mCollapsingTextHelper.setCollapsedTextGravity(gravity);
- }
-
- /**
- * Returns the horizontal and vertical alignment for title when collapsed.
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_collapsedTitleGravity
- */
- public int getCollapsedTitleGravity() {
- return mCollapsingTextHelper.getCollapsedTextGravity();
- }
-
- /**
- * Sets the text color and size for the expanded title from the specified
- * TextAppearance resource.
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleTextAppearance
- */
- public void setExpandedTitleTextAppearance(@StyleRes int resId) {
- mCollapsingTextHelper.setExpandedTextAppearance(resId);
- }
-
- /**
- * Sets the text color of the expanded title.
- *
- * @param color The new text color in ARGB format
- */
- public void setExpandedTitleColor(@ColorInt int color) {
- setExpandedTitleTextColor(ColorStateList.valueOf(color));
- }
-
- /**
- * Sets the text colors of the expanded title.
- *
- * @param colors ColorStateList containing the new text colors
- */
- public void setExpandedTitleTextColor(@NonNull ColorStateList colors) {
- mCollapsingTextHelper.setExpandedTextColor(colors);
- }
-
- /**
- * Sets the horizontal alignment of the expanded title and the vertical gravity that will
- * be used when there is extra space in the expanded bounds beyond what is required for
- * the title itself.
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleGravity
- */
- public void setExpandedTitleGravity(int gravity) {
- mCollapsingTextHelper.setExpandedTextGravity(gravity);
- }
-
- /**
- * Returns the horizontal and vertical alignment for title when expanded.
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleGravity
- */
- public int getExpandedTitleGravity() {
- return mCollapsingTextHelper.getExpandedTextGravity();
- }
-
- /**
- * Set the typeface to use for the collapsed title.
- *
- * @param typeface typeface to use, or {@code null} to use the default.
- */
- public void setCollapsedTitleTypeface(@Nullable Typeface typeface) {
- mCollapsingTextHelper.setCollapsedTypeface(typeface);
- }
-
- /**
- * Returns the typeface used for the collapsed title.
- */
- @NonNull
- public Typeface getCollapsedTitleTypeface() {
- return mCollapsingTextHelper.getCollapsedTypeface();
- }
-
- /**
- * Set the typeface to use for the expanded title.
- *
- * @param typeface typeface to use, or {@code null} to use the default.
- */
- public void setExpandedTitleTypeface(@Nullable Typeface typeface) {
- mCollapsingTextHelper.setExpandedTypeface(typeface);
- }
-
- /**
- * Returns the typeface used for the expanded title.
- */
- @NonNull
- public Typeface getExpandedTitleTypeface() {
- return mCollapsingTextHelper.getExpandedTypeface();
- }
-
- /**
- * Sets the expanded title margins.
- *
- * @param start the starting title margin in pixels
- * @param top the top title margin in pixels
- * @param end the ending title margin in pixels
- * @param bottom the bottom title margin in pixels
- *
- * @see #getExpandedTitleMarginStart()
- * @see #getExpandedTitleMarginTop()
- * @see #getExpandedTitleMarginEnd()
- * @see #getExpandedTitleMarginBottom()
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMargin
- */
- public void setExpandedTitleMargin(int start, int top, int end, int bottom) {
- mExpandedMarginStart = start;
- mExpandedMarginTop = top;
- mExpandedMarginEnd = end;
- mExpandedMarginBottom = bottom;
- requestLayout();
- }
-
- /**
- * @return the starting expanded title margin in pixels
- *
- * @see #setExpandedTitleMarginStart(int)
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart
- */
- public int getExpandedTitleMarginStart() {
- return mExpandedMarginStart;
- }
-
- /**
- * Sets the starting expanded title margin in pixels.
- *
- * @param margin the starting title margin in pixels
- * @see #getExpandedTitleMarginStart()
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginStart
- */
- public void setExpandedTitleMarginStart(int margin) {
- mExpandedMarginStart = margin;
- requestLayout();
- }
-
- /**
- * @return the top expanded title margin in pixels
- * @see #setExpandedTitleMarginTop(int)
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginTop
- */
- public int getExpandedTitleMarginTop() {
- return mExpandedMarginTop;
- }
-
- /**
- * Sets the top expanded title margin in pixels.
- *
- * @param margin the top title margin in pixels
- * @see #getExpandedTitleMarginTop()
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginTop
- */
- public void setExpandedTitleMarginTop(int margin) {
- mExpandedMarginTop = margin;
- requestLayout();
- }
-
- /**
- * @return the ending expanded title margin in pixels
- * @see #setExpandedTitleMarginEnd(int)
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd
- */
- public int getExpandedTitleMarginEnd() {
- return mExpandedMarginEnd;
- }
-
- /**
- * Sets the ending expanded title margin in pixels.
- *
- * @param margin the ending title margin in pixels
- * @see #getExpandedTitleMarginEnd()
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginEnd
- */
- public void setExpandedTitleMarginEnd(int margin) {
- mExpandedMarginEnd = margin;
- requestLayout();
- }
-
- /**
- * @return the bottom expanded title margin in pixels
- * @see #setExpandedTitleMarginBottom(int)
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom
- */
- public int getExpandedTitleMarginBottom() {
- return mExpandedMarginBottom;
- }
-
- /**
- * Sets the bottom expanded title margin in pixels.
- *
- * @param margin the bottom title margin in pixels
- * @see #getExpandedTitleMarginBottom()
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_expandedTitleMarginBottom
- */
- public void setExpandedTitleMarginBottom(int margin) {
- mExpandedMarginBottom = margin;
- requestLayout();
- }
-
- /**
- * Set the amount of visible height in pixels used to define when to trigger a scrim
- * visibility change.
- *
- * <p>If the visible height of this view is less than the given value, the scrims will be
- * made visible, otherwise they are hidden.</p>
- *
- * @param height value in pixels used to define when to trigger a scrim visibility change
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_scrimVisibleHeightTrigger
- */
- public void setScrimVisibleHeightTrigger(@IntRange(from = 0) final int height) {
- if (mScrimVisibleHeightTrigger != height) {
- mScrimVisibleHeightTrigger = height;
- // Update the scrim visibility
- updateScrimVisibility();
- }
- }
-
- /**
- * Returns the amount of visible height in pixels used to define when to trigger a scrim
- * visibility change.
- *
- * @see #setScrimVisibleHeightTrigger(int)
- */
- public int getScrimVisibleHeightTrigger() {
- if (mScrimVisibleHeightTrigger >= 0) {
- // If we have one explicitly set, return it
- return mScrimVisibleHeightTrigger;
- }
-
- // Otherwise we'll use the default computed value
- final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
-
- final int minHeight = ViewCompat.getMinimumHeight(this);
- if (minHeight > 0) {
- // If we have a minHeight set, lets use 2 * minHeight (capped at our height)
- return Math.min((minHeight * 2) + insetTop, getHeight());
- }
-
- // If we reach here then we don't have a min height set. Instead we'll take a
- // guess at 1/3 of our height being visible
- return getHeight() / 3;
- }
-
- /**
- * Set the duration used for scrim visibility animations.
- *
- * @param duration the duration to use in milliseconds
- *
- * @attr ref android.support.design.R.styleable#CollapsingToolbarLayout_scrimAnimationDuration
- */
- public void setScrimAnimationDuration(@IntRange(from = 0) final long duration) {
- mScrimAnimationDuration = duration;
- }
-
- /**
- * Returns the duration in milliseconds used for scrim visibility animations.
- */
- public long getScrimAnimationDuration() {
- return mScrimAnimationDuration;
- }
-
- @Override
- protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
- return p instanceof LayoutParams;
- }
-
- @Override
- protected LayoutParams generateDefaultLayoutParams() {
- return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
- }
-
- @Override
- public FrameLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
- return new LayoutParams(getContext(), attrs);
- }
-
- @Override
- protected FrameLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
- return new LayoutParams(p);
- }
-
- public static class LayoutParams extends FrameLayout.LayoutParams {
-
- private static final float DEFAULT_PARALLAX_MULTIPLIER = 0.5f;
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef({
- COLLAPSE_MODE_OFF,
- COLLAPSE_MODE_PIN,
- COLLAPSE_MODE_PARALLAX
- })
- @Retention(RetentionPolicy.SOURCE)
- @interface CollapseMode {}
-
- /**
- * The view will act as normal with no collapsing behavior.
- */
- public static final int COLLAPSE_MODE_OFF = 0;
-
- /**
- * The view will pin in place until it reaches the bottom of the
- * {@link CollapsingToolbarLayout}.
- */
- public static final int COLLAPSE_MODE_PIN = 1;
-
- /**
- * The view will scroll in a parallax fashion. See {@link #setParallaxMultiplier(float)}
- * to change the multiplier used.
- */
- public static final int COLLAPSE_MODE_PARALLAX = 2;
-
- int mCollapseMode = COLLAPSE_MODE_OFF;
- float mParallaxMult = DEFAULT_PARALLAX_MULTIPLIER;
-
- public LayoutParams(Context c, AttributeSet attrs) {
- super(c, attrs);
-
- TypedArray a = c.obtainStyledAttributes(attrs,
- R.styleable.CollapsingToolbarLayout_Layout);
- mCollapseMode = a.getInt(
- R.styleable.CollapsingToolbarLayout_Layout_layout_collapseMode,
- COLLAPSE_MODE_OFF);
- setParallaxMultiplier(a.getFloat(
- R.styleable.CollapsingToolbarLayout_Layout_layout_collapseParallaxMultiplier,
- DEFAULT_PARALLAX_MULTIPLIER));
- a.recycle();
- }
-
- public LayoutParams(int width, int height) {
- super(width, height);
- }
-
- public LayoutParams(int width, int height, int gravity) {
- super(width, height, gravity);
- }
-
- public LayoutParams(ViewGroup.LayoutParams p) {
- super(p);
- }
-
- public LayoutParams(MarginLayoutParams source) {
- super(source);
- }
-
- @RequiresApi(19)
- public LayoutParams(FrameLayout.LayoutParams source) {
- // The copy constructor called here only exists on API 19+.
- super(source);
- }
-
- /**
- * Set the collapse mode.
- *
- * @param collapseMode one of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN}
- * or {@link #COLLAPSE_MODE_PARALLAX}.
- */
- public void setCollapseMode(@CollapseMode int collapseMode) {
- mCollapseMode = collapseMode;
- }
-
- /**
- * Returns the requested collapse mode.
- *
- * @return the current mode. One of {@link #COLLAPSE_MODE_OFF}, {@link #COLLAPSE_MODE_PIN}
- * or {@link #COLLAPSE_MODE_PARALLAX}.
- */
- @CollapseMode
- public int getCollapseMode() {
- return mCollapseMode;
- }
-
- /**
- * Set the parallax scroll multiplier used in conjunction with
- * {@link #COLLAPSE_MODE_PARALLAX}. A value of {@code 0.0} indicates no movement at all,
- * {@code 1.0f} indicates normal scroll movement.
- *
- * @param multiplier the multiplier.
- *
- * @see #getParallaxMultiplier()
- */
- public void setParallaxMultiplier(float multiplier) {
- mParallaxMult = multiplier;
- }
-
- /**
- * Returns the parallax scroll multiplier used in conjunction with
- * {@link #COLLAPSE_MODE_PARALLAX}.
- *
- * @see #setParallaxMultiplier(float)
- */
- public float getParallaxMultiplier() {
- return mParallaxMult;
- }
- }
-
- /**
- * Show or hide the scrims if needed
- */
- final void updateScrimVisibility() {
- if (mContentScrim != null || mStatusBarScrim != null) {
- setScrimsShown(getHeight() + mCurrentOffset < getScrimVisibleHeightTrigger());
- }
- }
-
- final int getMaxOffsetForPinChild(View child) {
- final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- return getHeight()
- - offsetHelper.getLayoutTop()
- - child.getHeight()
- - lp.bottomMargin;
- }
-
- private class OffsetUpdateListener implements AppBarLayout.OnOffsetChangedListener {
- OffsetUpdateListener() {
- }
-
- @Override
- public void onOffsetChanged(AppBarLayout layout, int verticalOffset) {
- mCurrentOffset = verticalOffset;
-
- final int insetTop = mLastInsets != null ? mLastInsets.getSystemWindowInsetTop() : 0;
-
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- final LayoutParams lp = (LayoutParams) child.getLayoutParams();
- final ViewOffsetHelper offsetHelper = getViewOffsetHelper(child);
-
- switch (lp.mCollapseMode) {
- case LayoutParams.COLLAPSE_MODE_PIN:
- offsetHelper.setTopAndBottomOffset(MathUtils.clamp(
- -verticalOffset, 0, getMaxOffsetForPinChild(child)));
- break;
- case LayoutParams.COLLAPSE_MODE_PARALLAX:
- offsetHelper.setTopAndBottomOffset(
- Math.round(-verticalOffset * lp.mParallaxMult));
- break;
- }
- }
-
- // Show or hide the scrims if needed
- updateScrimVisibility();
-
- if (mStatusBarScrim != null && insetTop > 0) {
- ViewCompat.postInvalidateOnAnimation(CollapsingToolbarLayout.this);
- }
-
- // Update the collapsing text's fraction
- final int expandRange = getHeight() - ViewCompat.getMinimumHeight(
- CollapsingToolbarLayout.this) - insetTop;
- mCollapsingTextHelper.setExpansionFraction(
- Math.abs(verticalOffset) / (float) expandRange);
- }
- }
-}
diff --git a/android/support/design/widget/CoordinatorLayout.java b/android/support/design/widget/CoordinatorLayout.java
index 03cce024..b7f47f40 100644
--- a/android/support/design/widget/CoordinatorLayout.java
+++ b/android/support/design/widget/CoordinatorLayout.java
@@ -366,7 +366,11 @@ public class CoordinatorLayout extends ViewGroup implements NestedScrollingParen
return insets;
}
- final WindowInsetsCompat getLastWindowInsets() {
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY_GROUP)
+ public final WindowInsetsCompat getLastWindowInsets() {
return mLastInsets;
}
diff --git a/android/support/design/widget/DrawableUtils.java b/android/support/design/widget/DrawableUtils.java
deleted file mode 100644
index df1c04b0..00000000
--- a/android/support/design/widget/DrawableUtils.java
+++ /dev/null
@@ -1,65 +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 android.support.design.widget;
-
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.DrawableContainer;
-import android.util.Log;
-
-import java.lang.reflect.Method;
-
-/**
- * Caution. Gross hacks ahead.
- */
-class DrawableUtils {
-
- private static final String LOG_TAG = "DrawableUtils";
-
- private static Method sSetConstantStateMethod;
- private static boolean sSetConstantStateMethodFetched;
-
- private DrawableUtils() {}
-
- static boolean setContainerConstantState(DrawableContainer drawable,
- Drawable.ConstantState constantState) {
- // We can use getDeclaredMethod() on v9+
- return setContainerConstantStateV9(drawable, constantState);
- }
-
- private static boolean setContainerConstantStateV9(DrawableContainer drawable,
- Drawable.ConstantState constantState) {
- if (!sSetConstantStateMethodFetched) {
- try {
- sSetConstantStateMethod = DrawableContainer.class.getDeclaredMethod(
- "setConstantState", DrawableContainer.DrawableContainerState.class);
- sSetConstantStateMethod.setAccessible(true);
- } catch (NoSuchMethodException e) {
- Log.e(LOG_TAG, "Could not fetch setConstantState(). Oh well.");
- }
- sSetConstantStateMethodFetched = true;
- }
- if (sSetConstantStateMethod != null) {
- try {
- sSetConstantStateMethod.invoke(drawable, constantState);
- return true;
- } catch (Exception e) {
- Log.e(LOG_TAG, "Could not invoke setConstantState(). Oh well.");
- }
- }
- return false;
- }
-}
diff --git a/android/support/design/widget/FloatingActionButton.java b/android/support/design/widget/FloatingActionButton.java
deleted file mode 100644
index f37b3798..00000000
--- a/android/support/design/widget/FloatingActionButton.java
+++ /dev/null
@@ -1,870 +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 android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.support.annotation.ColorInt;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
-import android.support.design.R;
-import android.support.design.widget.FloatingActionButtonImpl.InternalVisibilityChangedListener;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.widget.ViewGroupUtils;
-import android.support.v7.widget.AppCompatImageHelper;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.Gravity;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.List;
-
-/**
- * Floating action buttons are used for a special type of promoted action. They are distinguished
- * by a circled icon floating above the UI and have special motion behaviors related to morphing,
- * launching, and the transferring anchor point.
- *
- * <p>Floating action buttons come in two sizes: the default and the mini. The size can be
- * controlled with the {@code fabSize} attribute.</p>
- *
- * <p>As this class descends from {@link ImageView}, you can control the icon which is displayed
- * via {@link #setImageDrawable(Drawable)}.</p>
- *
- * <p>The background color of this view defaults to the your theme's {@code colorAccent}. If you
- * wish to change this at runtime then you can do so via
- * {@link #setBackgroundTintList(ColorStateList)}.</p>
- */
-@CoordinatorLayout.DefaultBehavior(FloatingActionButton.Behavior.class)
-public class FloatingActionButton extends VisibilityAwareImageButton {
-
- private static final String LOG_TAG = "FloatingActionButton";
-
- /**
- * Callback to be invoked when the visibility of a FloatingActionButton changes.
- */
- public abstract static class OnVisibilityChangedListener {
- /**
- * Called when a FloatingActionButton has been
- * {@link #show(OnVisibilityChangedListener) shown}.
- *
- * @param fab the FloatingActionButton that was shown.
- */
- public void onShown(FloatingActionButton fab) {}
-
- /**
- * Called when a FloatingActionButton has been
- * {@link #hide(OnVisibilityChangedListener) hidden}.
- *
- * @param fab the FloatingActionButton that was hidden.
- */
- public void onHidden(FloatingActionButton fab) {}
- }
-
- // These values must match those in the attrs declaration
-
- /**
- * The mini sized button. Will always been smaller than {@link #SIZE_NORMAL}.
- *
- * @see #setSize(int)
- */
- public static final int SIZE_MINI = 1;
-
- /**
- * The normal sized button. Will always been larger than {@link #SIZE_MINI}.
- *
- * @see #setSize(int)
- */
- public static final int SIZE_NORMAL = 0;
-
- /**
- * Size which will change based on the window size. For small sized windows
- * (largest screen dimension < 470dp) this will select a small sized button, and for
- * larger sized windows it will select a larger size.
- *
- * @see #setSize(int)
- */
- public static final int SIZE_AUTO = -1;
-
- /**
- * Indicates that FloatingActionButton should not have a custom size.
- */
- public static final int NO_CUSTOM_SIZE = 0;
-
- /**
- * The switch point for the largest screen edge where SIZE_AUTO switches from mini to normal.
- */
- private static final int AUTO_MINI_LARGEST_SCREEN_WIDTH = 470;
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({SIZE_MINI, SIZE_NORMAL, SIZE_AUTO})
- public @interface Size {}
-
- private ColorStateList mBackgroundTint;
- private PorterDuff.Mode mBackgroundTintMode;
-
- private int mBorderWidth;
- private int mRippleColor;
- private int mSize;
- private int mCustomSize;
- int mImagePadding;
- private int mMaxImageSize;
-
- boolean mCompatPadding;
- final Rect mShadowPadding = new Rect();
- private final Rect mTouchArea = new Rect();
-
- private AppCompatImageHelper mImageHelper;
-
- private FloatingActionButtonImpl mImpl;
-
- public FloatingActionButton(Context context) {
- this(context, null);
- }
-
- public FloatingActionButton(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public FloatingActionButton(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.FloatingActionButton, defStyleAttr,
- R.style.Widget_Design_FloatingActionButton);
- mBackgroundTint = a.getColorStateList(R.styleable.FloatingActionButton_backgroundTint);
- mBackgroundTintMode = ViewUtils.parseTintMode(a.getInt(
- R.styleable.FloatingActionButton_backgroundTintMode, -1), null);
- mRippleColor = a.getColor(R.styleable.FloatingActionButton_rippleColor, 0);
- mSize = a.getInt(R.styleable.FloatingActionButton_fabSize, SIZE_AUTO);
- mCustomSize = a.getDimensionPixelSize(R.styleable.FloatingActionButton_fabCustomSize,
- 0);
- mBorderWidth = a.getDimensionPixelSize(R.styleable.FloatingActionButton_borderWidth, 0);
- final float elevation = a.getDimension(R.styleable.FloatingActionButton_elevation, 0f);
- final float pressedTranslationZ = a.getDimension(
- R.styleable.FloatingActionButton_pressedTranslationZ, 0f);
- mCompatPadding = a.getBoolean(R.styleable.FloatingActionButton_useCompatPadding, false);
- a.recycle();
-
- mImageHelper = new AppCompatImageHelper(this);
- mImageHelper.loadFromAttributes(attrs, defStyleAttr);
-
- mMaxImageSize = (int) getResources().getDimension(R.dimen.design_fab_image_size);
-
- getImpl().setBackgroundDrawable(mBackgroundTint, mBackgroundTintMode,
- mRippleColor, mBorderWidth);
- getImpl().setElevation(elevation);
- getImpl().setPressedTranslationZ(pressedTranslationZ);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- final int preferredSize = getSizeDimension();
-
- mImagePadding = (preferredSize - mMaxImageSize) / 2;
- getImpl().updatePadding();
-
- final int w = resolveAdjustedSize(preferredSize, widthMeasureSpec);
- final int h = resolveAdjustedSize(preferredSize, heightMeasureSpec);
-
- // As we want to stay circular, we set both dimensions to be the
- // smallest resolved dimension
- final int d = Math.min(w, h);
-
- // We add the shadow's padding to the measured dimension
- setMeasuredDimension(
- d + mShadowPadding.left + mShadowPadding.right,
- d + mShadowPadding.top + mShadowPadding.bottom);
- }
-
- /**
- * Returns the ripple color for this button.
- *
- * @return the ARGB color used for the ripple
- * @see #setRippleColor(int)
- */
- @ColorInt
- public int getRippleColor() {
- return mRippleColor;
- }
-
- /**
- * Sets the ripple color for this button.
- *
- * <p>When running on devices with KitKat or below, we draw this color as a filled circle
- * rather than a ripple.</p>
- *
- * @param color ARGB color to use for the ripple
- * @attr ref android.support.design.R.styleable#FloatingActionButton_rippleColor
- * @see #getRippleColor()
- */
- public void setRippleColor(@ColorInt int color) {
- if (mRippleColor != color) {
- mRippleColor = color;
- getImpl().setRippleColor(color);
- }
- }
-
- /**
- * Returns the tint applied to the background drawable, if specified.
- *
- * @return the tint applied to the background drawable
- * @see #setBackgroundTintList(ColorStateList)
- */
- @Nullable
- @Override
- public ColorStateList getBackgroundTintList() {
- return mBackgroundTint;
- }
-
- /**
- * Applies a tint to the background drawable. Does not modify the current tint
- * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
- *
- * @param tint the tint to apply, may be {@code null} to clear tint
- */
- @Override
- public void setBackgroundTintList(@Nullable ColorStateList tint) {
- if (mBackgroundTint != tint) {
- mBackgroundTint = tint;
- getImpl().setBackgroundTintList(tint);
- }
- }
-
- /**
- * Returns the blending mode used to apply the tint to the background
- * drawable, if specified.
- *
- * @return the blending mode used to apply the tint to the background
- * drawable
- * @see #setBackgroundTintMode(PorterDuff.Mode)
- */
- @Nullable
- @Override
- public PorterDuff.Mode getBackgroundTintMode() {
- return mBackgroundTintMode;
- }
-
- /**
- * Specifies the blending mode used to apply the tint specified by
- * {@link #setBackgroundTintList(ColorStateList)}} to the background
- * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
- *
- * @param tintMode the blending mode used to apply the tint, may be
- * {@code null} to clear tint
- */
- @Override
- public void setBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
- if (mBackgroundTintMode != tintMode) {
- mBackgroundTintMode = tintMode;
- getImpl().setBackgroundTintMode(tintMode);
- }
- }
-
- @Override
- public void setBackgroundDrawable(Drawable background) {
- Log.i(LOG_TAG, "Setting a custom background is not supported.");
- }
-
- @Override
- public void setBackgroundResource(int resid) {
- Log.i(LOG_TAG, "Setting a custom background is not supported.");
- }
-
- @Override
- public void setBackgroundColor(int color) {
- Log.i(LOG_TAG, "Setting a custom background is not supported.");
- }
-
- @Override
- public void setImageResource(@DrawableRes int resId) {
- // Intercept this call and instead retrieve the Drawable via the image helper
- mImageHelper.setImageResource(resId);
- }
-
- /**
- * Shows the button.
- * <p>This method will animate the button show if the view has already been laid out.</p>
- */
- public void show() {
- show(null);
- }
-
- /**
- * Shows the button.
- * <p>This method will animate the button show if the view has already been laid out.</p>
- *
- * @param listener the listener to notify when this view is shown
- */
- public void show(@Nullable final OnVisibilityChangedListener listener) {
- show(listener, true);
- }
-
- void show(OnVisibilityChangedListener listener, boolean fromUser) {
- getImpl().show(wrapOnVisibilityChangedListener(listener), fromUser);
- }
-
- /**
- * Hides the button.
- * <p>This method will animate the button hide if the view has already been laid out.</p>
- */
- public void hide() {
- hide(null);
- }
-
- /**
- * Hides the button.
- * <p>This method will animate the button hide if the view has already been laid out.</p>
- *
- * @param listener the listener to notify when this view is hidden
- */
- public void hide(@Nullable OnVisibilityChangedListener listener) {
- hide(listener, true);
- }
-
- void hide(@Nullable OnVisibilityChangedListener listener, boolean fromUser) {
- getImpl().hide(wrapOnVisibilityChangedListener(listener), fromUser);
- }
-
- /**
- * Set whether FloatingActionButton should add inner padding on platforms Lollipop and after,
- * to ensure consistent dimensions on all platforms.
- *
- * @param useCompatPadding true if FloatingActionButton is adding inner padding on platforms
- * Lollipop and after, to ensure consistent dimensions on all platforms.
- *
- * @attr ref android.support.design.R.styleable#FloatingActionButton_useCompatPadding
- * @see #getUseCompatPadding()
- */
- public void setUseCompatPadding(boolean useCompatPadding) {
- if (mCompatPadding != useCompatPadding) {
- mCompatPadding = useCompatPadding;
- getImpl().onCompatShadowChanged();
- }
- }
-
- /**
- * Returns whether FloatingActionButton will add inner padding on platforms Lollipop and after.
- *
- * @return true if FloatingActionButton is adding inner padding on platforms Lollipop and after,
- * to ensure consistent dimensions on all platforms.
- *
- * @attr ref android.support.design.R.styleable#FloatingActionButton_useCompatPadding
- * @see #setUseCompatPadding(boolean)
- */
- public boolean getUseCompatPadding() {
- return mCompatPadding;
- }
-
- /**
- * Sets the size of the button.
- *
- * <p>The options relate to the options available on the material design specification.
- * {@link #SIZE_NORMAL} is larger than {@link #SIZE_MINI}. {@link #SIZE_AUTO} will choose
- * an appropriate size based on the screen size.</p>
- *
- * @param size one of {@link #SIZE_NORMAL}, {@link #SIZE_MINI} or {@link #SIZE_AUTO}
- *
- * @attr ref android.support.design.R.styleable#FloatingActionButton_fabSize
- */
- public void setSize(@Size int size) {
- if (size != mSize) {
- mSize = size;
- requestLayout();
- }
- }
-
- /**
- * Returns the chosen size for this button.
- *
- * @return one of {@link #SIZE_NORMAL}, {@link #SIZE_MINI} or {@link #SIZE_AUTO}
- * @see #setSize(int)
- */
- @Size
- public int getSize() {
- return mSize;
- }
-
- @Nullable
- private InternalVisibilityChangedListener wrapOnVisibilityChangedListener(
- @Nullable final OnVisibilityChangedListener listener) {
- if (listener == null) {
- return null;
- }
-
- return new InternalVisibilityChangedListener() {
- @Override
- public void onShown() {
- listener.onShown(FloatingActionButton.this);
- }
-
- @Override
- public void onHidden() {
- listener.onHidden(FloatingActionButton.this);
- }
- };
- }
-
- /**
- * Sets the size of the button to be a custom value in pixels. If set to
- * {@link #NO_CUSTOM_SIZE}, custom size will not be used and size will be calculated according
- * to {@link #setSize(int)} method.
- *
- * @param size preferred size in pixels, or zero
- *
- * @attr ref android.support.design.R.styleable#FloatingActionButton_fabCustomSize
- */
- public void setCustomSize(int size) {
- if (size < 0) {
- throw new IllegalArgumentException("Custom size should be non-negative.");
- }
- mCustomSize = size;
- }
-
- /**
- * Returns the custom size for this button.
- *
- * @return size in pixels, or {@link #NO_CUSTOM_SIZE}
- */
- public int getCustomSize() {
- return mCustomSize;
- }
-
- int getSizeDimension() {
- return getSizeDimension(mSize);
- }
-
- private int getSizeDimension(@Size final int size) {
- final Resources res = getResources();
- // If custom size is set, return it
- if (mCustomSize != NO_CUSTOM_SIZE) {
- return mCustomSize;
- }
- switch (size) {
- case SIZE_AUTO:
- // If we're set to auto, grab the size from resources and refresh
- final int width = res.getConfiguration().screenWidthDp;
- final int height = res.getConfiguration().screenHeightDp;
- return Math.max(width, height) < AUTO_MINI_LARGEST_SCREEN_WIDTH
- ? getSizeDimension(SIZE_MINI)
- : getSizeDimension(SIZE_NORMAL);
- case SIZE_MINI:
- return res.getDimensionPixelSize(R.dimen.design_fab_size_mini);
- case SIZE_NORMAL:
- default:
- return res.getDimensionPixelSize(R.dimen.design_fab_size_normal);
- }
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- getImpl().onAttachedToWindow();
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- getImpl().onDetachedFromWindow();
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
- getImpl().onDrawableStateChanged(getDrawableState());
- }
-
- @Override
- public void jumpDrawablesToCurrentState() {
- super.jumpDrawablesToCurrentState();
- getImpl().jumpDrawableToCurrentState();
- }
-
- /**
- * Return in {@code rect} the bounds of the actual floating action button content in view-local
- * coordinates. This is defined as anything within any visible shadow.
- *
- * @return true if this view actually has been laid out and has a content rect, else false.
- */
- public boolean getContentRect(@NonNull Rect rect) {
- if (ViewCompat.isLaidOut(this)) {
- rect.set(0, 0, getWidth(), getHeight());
- rect.left += mShadowPadding.left;
- rect.top += mShadowPadding.top;
- rect.right -= mShadowPadding.right;
- rect.bottom -= mShadowPadding.bottom;
- return true;
- } else {
- return false;
- }
- }
-
- /**
- * Returns the FloatingActionButton's background, minus any compatible shadow implementation.
- */
- @NonNull
- public Drawable getContentBackground() {
- return getImpl().getContentBackground();
- }
-
- private static int resolveAdjustedSize(int desiredSize, int measureSpec) {
- int result = desiredSize;
- int specMode = MeasureSpec.getMode(measureSpec);
- int specSize = MeasureSpec.getSize(measureSpec);
- switch (specMode) {
- case MeasureSpec.UNSPECIFIED:
- // Parent says we can be as big as we want. Just don't be larger
- // than max size imposed on ourselves.
- result = desiredSize;
- break;
- case MeasureSpec.AT_MOST:
- // Parent says we can be as big as we want, up to specSize.
- // Don't be larger than specSize, and don't be larger than
- // the max size imposed on ourselves.
- result = Math.min(desiredSize, specSize);
- break;
- case MeasureSpec.EXACTLY:
- // No choice. Do what we are told.
- result = specSize;
- break;
- }
- return result;
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- switch (ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- // Skipping the gesture if it doesn't start in in the FAB 'content' area
- if (getContentRect(mTouchArea)
- && !mTouchArea.contains((int) ev.getX(), (int) ev.getY())) {
- return false;
- }
- break;
- }
- return super.onTouchEvent(ev);
- }
-
- /**
- * Behavior designed for use with {@link FloatingActionButton} instances. Its main function
- * is to move {@link FloatingActionButton} views so that any displayed {@link Snackbar}s do
- * not cover them.
- */
- public static class Behavior extends CoordinatorLayout.Behavior<FloatingActionButton> {
- private static final boolean AUTO_HIDE_DEFAULT = true;
-
- private Rect mTmpRect;
- private OnVisibilityChangedListener mInternalAutoHideListener;
- private boolean mAutoHideEnabled;
-
- public Behavior() {
- super();
- mAutoHideEnabled = AUTO_HIDE_DEFAULT;
- }
-
- public Behavior(Context context, AttributeSet attrs) {
- super(context, attrs);
- TypedArray a = context.obtainStyledAttributes(attrs,
- R.styleable.FloatingActionButton_Behavior_Layout);
- mAutoHideEnabled = a.getBoolean(
- R.styleable.FloatingActionButton_Behavior_Layout_behavior_autoHide,
- AUTO_HIDE_DEFAULT);
- a.recycle();
- }
-
- /**
- * Sets whether the associated FloatingActionButton automatically hides when there is
- * not enough space to be displayed. This works with {@link AppBarLayout}
- * and {@link BottomSheetBehavior}.
- *
- * @attr ref android.support.design.R.styleable#FloatingActionButton_Behavior_Layout_behavior_autoHide
- * @param autoHide true to enable automatic hiding
- */
- public void setAutoHideEnabled(boolean autoHide) {
- mAutoHideEnabled = autoHide;
- }
-
- /**
- * Returns whether the associated FloatingActionButton automatically hides when there is
- * not enough space to be displayed.
- *
- * @attr ref android.support.design.R.styleable#FloatingActionButton_Behavior_Layout_behavior_autoHide
- * @return true if enabled
- */
- public boolean isAutoHideEnabled() {
- return mAutoHideEnabled;
- }
-
- @Override
- public void onAttachedToLayoutParams(@NonNull CoordinatorLayout.LayoutParams lp) {
- if (lp.dodgeInsetEdges == Gravity.NO_GRAVITY) {
- // If the developer hasn't set dodgeInsetEdges, lets set it to BOTTOM so that
- // we dodge any Snackbars
- lp.dodgeInsetEdges = Gravity.BOTTOM;
- }
- }
-
- @Override
- public boolean onDependentViewChanged(CoordinatorLayout parent, FloatingActionButton child,
- View dependency) {
- if (dependency instanceof AppBarLayout) {
- // If we're depending on an AppBarLayout we will show/hide it automatically
- // if the FAB is anchored to the AppBarLayout
- updateFabVisibilityForAppBarLayout(parent, (AppBarLayout) dependency, child);
- } else if (isBottomSheet(dependency)) {
- updateFabVisibilityForBottomSheet(dependency, child);
- }
- return false;
- }
-
- private static boolean isBottomSheet(@NonNull View view) {
- final ViewGroup.LayoutParams lp = view.getLayoutParams();
- if (lp instanceof CoordinatorLayout.LayoutParams) {
- return ((CoordinatorLayout.LayoutParams) lp)
- .getBehavior() instanceof BottomSheetBehavior;
- }
- return false;
- }
-
- @VisibleForTesting
- void setInternalAutoHideListener(OnVisibilityChangedListener listener) {
- mInternalAutoHideListener = listener;
- }
-
- private boolean shouldUpdateVisibility(View dependency, FloatingActionButton child) {
- final CoordinatorLayout.LayoutParams lp =
- (CoordinatorLayout.LayoutParams) child.getLayoutParams();
- if (!mAutoHideEnabled) {
- return false;
- }
-
- if (lp.getAnchorId() != dependency.getId()) {
- // The anchor ID doesn't match the dependency, so we won't automatically
- // show/hide the FAB
- return false;
- }
-
- //noinspection RedundantIfStatement
- if (child.getUserSetVisibility() != VISIBLE) {
- // The view isn't set to be visible so skip changing its visibility
- return false;
- }
-
- return true;
- }
-
- private boolean updateFabVisibilityForAppBarLayout(CoordinatorLayout parent,
- AppBarLayout appBarLayout, FloatingActionButton child) {
- if (!shouldUpdateVisibility(appBarLayout, child)) {
- return false;
- }
-
- if (mTmpRect == null) {
- mTmpRect = new Rect();
- }
-
- // First, let's get the visible rect of the dependency
- final Rect rect = mTmpRect;
- ViewGroupUtils.getDescendantRect(parent, appBarLayout, rect);
-
- if (rect.bottom <= appBarLayout.getMinimumHeightForVisibleOverlappingContent()) {
- // If the anchor's bottom is below the seam, we'll animate our FAB out
- child.hide(mInternalAutoHideListener, false);
- } else {
- // Else, we'll animate our FAB back in
- child.show(mInternalAutoHideListener, false);
- }
- return true;
- }
-
- private boolean updateFabVisibilityForBottomSheet(View bottomSheet,
- FloatingActionButton child) {
- if (!shouldUpdateVisibility(bottomSheet, child)) {
- return false;
- }
- CoordinatorLayout.LayoutParams lp =
- (CoordinatorLayout.LayoutParams) child.getLayoutParams();
- if (bottomSheet.getTop() < child.getHeight() / 2 + lp.topMargin) {
- child.hide(mInternalAutoHideListener, false);
- } else {
- child.show(mInternalAutoHideListener, false);
- }
- return true;
- }
-
- @Override
- public boolean onLayoutChild(CoordinatorLayout parent, FloatingActionButton child,
- int layoutDirection) {
- // First, let's make sure that the visibility of the FAB is consistent
- final List<View> dependencies = parent.getDependencies(child);
- for (int i = 0, count = dependencies.size(); i < count; i++) {
- final View dependency = dependencies.get(i);
- if (dependency instanceof AppBarLayout) {
- if (updateFabVisibilityForAppBarLayout(
- parent, (AppBarLayout) dependency, child)) {
- break;
- }
- } else if (isBottomSheet(dependency)) {
- if (updateFabVisibilityForBottomSheet(dependency, child)) {
- break;
- }
- }
- }
- // Now let the CoordinatorLayout lay out the FAB
- parent.onLayoutChild(child, layoutDirection);
- // Now offset it if needed
- offsetIfNeeded(parent, child);
- return true;
- }
-
- @Override
- public boolean getInsetDodgeRect(@NonNull CoordinatorLayout parent,
- @NonNull FloatingActionButton child, @NonNull Rect rect) {
- // Since we offset so that any internal shadow padding isn't shown, we need to make
- // sure that the shadow isn't used for any dodge inset calculations
- final Rect shadowPadding = child.mShadowPadding;
- rect.set(child.getLeft() + shadowPadding.left,
- child.getTop() + shadowPadding.top,
- child.getRight() - shadowPadding.right,
- child.getBottom() - shadowPadding.bottom);
- return true;
- }
-
- /**
- * Pre-Lollipop we use padding so that the shadow has enough space to be drawn. This method
- * offsets our layout position so that we're positioned correctly if we're on one of
- * our parent's edges.
- */
- private void offsetIfNeeded(CoordinatorLayout parent, FloatingActionButton fab) {
- final Rect padding = fab.mShadowPadding;
-
- if (padding != null && padding.centerX() > 0 && padding.centerY() > 0) {
- final CoordinatorLayout.LayoutParams lp =
- (CoordinatorLayout.LayoutParams) fab.getLayoutParams();
-
- int offsetTB = 0, offsetLR = 0;
-
- if (fab.getRight() >= parent.getWidth() - lp.rightMargin) {
- // If we're on the right edge, shift it the right
- offsetLR = padding.right;
- } else if (fab.getLeft() <= lp.leftMargin) {
- // If we're on the left edge, shift it the left
- offsetLR = -padding.left;
- }
- if (fab.getBottom() >= parent.getHeight() - lp.bottomMargin) {
- // If we're on the bottom edge, shift it down
- offsetTB = padding.bottom;
- } else if (fab.getTop() <= lp.topMargin) {
- // If we're on the top edge, shift it up
- offsetTB = -padding.top;
- }
-
- if (offsetTB != 0) {
- ViewCompat.offsetTopAndBottom(fab, offsetTB);
- }
- if (offsetLR != 0) {
- ViewCompat.offsetLeftAndRight(fab, offsetLR);
- }
- }
- }
- }
-
- /**
- * Returns the backward compatible elevation of the FloatingActionButton.
- *
- * @return the backward compatible elevation in pixels.
- * @attr ref android.support.design.R.styleable#FloatingActionButton_elevation
- * @see #setCompatElevation(float)
- */
- public float getCompatElevation() {
- return getImpl().getElevation();
- }
-
- /**
- * Updates the backward compatible elevation of the FloatingActionButton.
- *
- * @param elevation The backward compatible elevation in pixels.
- * @attr ref android.support.design.R.styleable#FloatingActionButton_elevation
- * @see #getCompatElevation()
- * @see #setUseCompatPadding(boolean)
- */
- public void setCompatElevation(float elevation) {
- getImpl().setElevation(elevation);
- }
-
- private FloatingActionButtonImpl getImpl() {
- if (mImpl == null) {
- mImpl = createImpl();
- }
- return mImpl;
- }
-
- private FloatingActionButtonImpl createImpl() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- return new FloatingActionButtonLollipop(this, new ShadowDelegateImpl());
- } else {
- return new FloatingActionButtonImpl(this, new ShadowDelegateImpl());
- }
- }
-
- private class ShadowDelegateImpl implements ShadowViewDelegate {
- ShadowDelegateImpl() {
- }
-
- @Override
- public float getRadius() {
- return getSizeDimension() / 2f;
- }
-
- @Override
- public void setShadowPadding(int left, int top, int right, int bottom) {
- mShadowPadding.set(left, top, right, bottom);
- setPadding(left + mImagePadding, top + mImagePadding,
- right + mImagePadding, bottom + mImagePadding);
- }
-
- @Override
- public void setBackgroundDrawable(Drawable background) {
- FloatingActionButton.super.setBackgroundDrawable(background);
- }
-
- @Override
- public boolean isCompatPaddingEnabled() {
- return mCompatPadding;
- }
- }
-}
diff --git a/android/support/design/widget/FloatingActionButtonImpl.java b/android/support/design/widget/FloatingActionButtonImpl.java
deleted file mode 100644
index 132cd81b..00000000
--- a/android/support/design/widget/FloatingActionButtonImpl.java
+++ /dev/null
@@ -1,531 +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 android.support.design.widget;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Color;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.LayerDrawable;
-import android.os.Build;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RequiresApi;
-import android.support.design.R;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v4.view.ViewCompat;
-import android.view.View;
-import android.view.ViewTreeObserver;
-import android.view.animation.Interpolator;
-
-@RequiresApi(14)
-class FloatingActionButtonImpl {
- static final Interpolator ANIM_INTERPOLATOR = AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR;
- static final long PRESSED_ANIM_DURATION = 100;
- static final long PRESSED_ANIM_DELAY = 100;
-
- static final int ANIM_STATE_NONE = 0;
- static final int ANIM_STATE_HIDING = 1;
- static final int ANIM_STATE_SHOWING = 2;
-
- int mAnimState = ANIM_STATE_NONE;
-
- private final StateListAnimator mStateListAnimator;
-
- ShadowDrawableWrapper mShadowDrawable;
-
- private float mRotation;
-
- Drawable mShapeDrawable;
- Drawable mRippleDrawable;
- CircularBorderDrawable mBorderDrawable;
- Drawable mContentBackground;
-
- float mElevation;
- float mPressedTranslationZ;
-
- interface InternalVisibilityChangedListener {
- void onShown();
- void onHidden();
- }
-
- static final int SHOW_HIDE_ANIM_DURATION = 200;
-
- static final int[] PRESSED_ENABLED_STATE_SET = {android.R.attr.state_pressed,
- android.R.attr.state_enabled};
- static final int[] FOCUSED_ENABLED_STATE_SET = {android.R.attr.state_focused,
- android.R.attr.state_enabled};
- static final int[] ENABLED_STATE_SET = {android.R.attr.state_enabled};
- static final int[] EMPTY_STATE_SET = new int[0];
-
- final VisibilityAwareImageButton mView;
- final ShadowViewDelegate mShadowViewDelegate;
-
- private final Rect mTmpRect = new Rect();
- private ViewTreeObserver.OnPreDrawListener mPreDrawListener;
-
- FloatingActionButtonImpl(VisibilityAwareImageButton view,
- ShadowViewDelegate shadowViewDelegate) {
- mView = view;
- mShadowViewDelegate = shadowViewDelegate;
-
- mStateListAnimator = new StateListAnimator();
-
- // Elevate with translationZ when pressed or focused
- mStateListAnimator.addState(PRESSED_ENABLED_STATE_SET,
- createAnimator(new ElevateToTranslationZAnimation()));
- mStateListAnimator.addState(FOCUSED_ENABLED_STATE_SET,
- createAnimator(new ElevateToTranslationZAnimation()));
- // Reset back to elevation by default
- mStateListAnimator.addState(ENABLED_STATE_SET,
- createAnimator(new ResetElevationAnimation()));
- // Set to 0 when disabled
- mStateListAnimator.addState(EMPTY_STATE_SET,
- createAnimator(new DisabledElevationAnimation()));
-
- mRotation = mView.getRotation();
- }
-
- void setBackgroundDrawable(ColorStateList backgroundTint,
- PorterDuff.Mode backgroundTintMode, int rippleColor, int borderWidth) {
- // Now we need to tint the original background with the tint, using
- // an InsetDrawable if we have a border width
- mShapeDrawable = DrawableCompat.wrap(createShapeDrawable());
- DrawableCompat.setTintList(mShapeDrawable, backgroundTint);
- if (backgroundTintMode != null) {
- DrawableCompat.setTintMode(mShapeDrawable, backgroundTintMode);
- }
-
- // Now we created a mask Drawable which will be used for touch feedback.
- GradientDrawable touchFeedbackShape = createShapeDrawable();
-
- // We'll now wrap that touch feedback mask drawable with a ColorStateList. We do not need
- // to inset for any border here as LayerDrawable will nest the padding for us
- mRippleDrawable = DrawableCompat.wrap(touchFeedbackShape);
- DrawableCompat.setTintList(mRippleDrawable, createColorStateList(rippleColor));
-
- final Drawable[] layers;
- if (borderWidth > 0) {
- mBorderDrawable = createBorderDrawable(borderWidth, backgroundTint);
- layers = new Drawable[] {mBorderDrawable, mShapeDrawable, mRippleDrawable};
- } else {
- mBorderDrawable = null;
- layers = new Drawable[] {mShapeDrawable, mRippleDrawable};
- }
-
- mContentBackground = new LayerDrawable(layers);
-
- mShadowDrawable = new ShadowDrawableWrapper(
- mView.getContext(),
- mContentBackground,
- mShadowViewDelegate.getRadius(),
- mElevation,
- mElevation + mPressedTranslationZ);
- mShadowDrawable.setAddPaddingForCorners(false);
- mShadowViewDelegate.setBackgroundDrawable(mShadowDrawable);
- }
-
- void setBackgroundTintList(ColorStateList tint) {
- if (mShapeDrawable != null) {
- DrawableCompat.setTintList(mShapeDrawable, tint);
- }
- if (mBorderDrawable != null) {
- mBorderDrawable.setBorderTint(tint);
- }
- }
-
- void setBackgroundTintMode(PorterDuff.Mode tintMode) {
- if (mShapeDrawable != null) {
- DrawableCompat.setTintMode(mShapeDrawable, tintMode);
- }
- }
-
-
- void setRippleColor(int rippleColor) {
- if (mRippleDrawable != null) {
- DrawableCompat.setTintList(mRippleDrawable, createColorStateList(rippleColor));
- }
- }
-
- final void setElevation(float elevation) {
- if (mElevation != elevation) {
- mElevation = elevation;
- onElevationsChanged(elevation, mPressedTranslationZ);
- }
- }
-
- float getElevation() {
- return mElevation;
- }
-
- final void setPressedTranslationZ(float translationZ) {
- if (mPressedTranslationZ != translationZ) {
- mPressedTranslationZ = translationZ;
- onElevationsChanged(mElevation, translationZ);
- }
- }
-
- void onElevationsChanged(float elevation, float pressedTranslationZ) {
- if (mShadowDrawable != null) {
- mShadowDrawable.setShadowSize(elevation, elevation + mPressedTranslationZ);
- updatePadding();
- }
- }
-
- void onDrawableStateChanged(int[] state) {
- mStateListAnimator.setState(state);
- }
-
- void jumpDrawableToCurrentState() {
- mStateListAnimator.jumpToCurrentState();
- }
-
- void hide(@Nullable final InternalVisibilityChangedListener listener, final boolean fromUser) {
- if (isOrWillBeHidden()) {
- // We either are or will soon be hidden, skip the call
- return;
- }
-
- mView.animate().cancel();
-
- if (shouldAnimateVisibilityChange()) {
- mAnimState = ANIM_STATE_HIDING;
-
- mView.animate()
- .scaleX(0f)
- .scaleY(0f)
- .alpha(0f)
- .setDuration(SHOW_HIDE_ANIM_DURATION)
- .setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR)
- .setListener(new AnimatorListenerAdapter() {
- private boolean mCancelled;
-
- @Override
- public void onAnimationStart(Animator animation) {
- mView.internalSetVisibility(View.VISIBLE, fromUser);
- mCancelled = false;
- }
-
- @Override
- public void onAnimationCancel(Animator animation) {
- mCancelled = true;
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- mAnimState = ANIM_STATE_NONE;
-
- if (!mCancelled) {
- mView.internalSetVisibility(fromUser ? View.GONE : View.INVISIBLE,
- fromUser);
- if (listener != null) {
- listener.onHidden();
- }
- }
- }
- });
- } else {
- // If the view isn't laid out, or we're in the editor, don't run the animation
- mView.internalSetVisibility(fromUser ? View.GONE : View.INVISIBLE, fromUser);
- if (listener != null) {
- listener.onHidden();
- }
- }
- }
-
- void show(@Nullable final InternalVisibilityChangedListener listener, final boolean fromUser) {
- if (isOrWillBeShown()) {
- // We either are or will soon be visible, skip the call
- return;
- }
-
- mView.animate().cancel();
-
- if (shouldAnimateVisibilityChange()) {
- mAnimState = ANIM_STATE_SHOWING;
-
- if (mView.getVisibility() != View.VISIBLE) {
- // If the view isn't visible currently, we'll animate it from a single pixel
- mView.setAlpha(0f);
- mView.setScaleY(0f);
- mView.setScaleX(0f);
- }
-
- mView.animate()
- .scaleX(1f)
- .scaleY(1f)
- .alpha(1f)
- .setDuration(SHOW_HIDE_ANIM_DURATION)
- .setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animation) {
- mView.internalSetVisibility(View.VISIBLE, fromUser);
- }
-
- @Override
- public void onAnimationEnd(Animator animation) {
- mAnimState = ANIM_STATE_NONE;
- if (listener != null) {
- listener.onShown();
- }
- }
- });
- } else {
- mView.internalSetVisibility(View.VISIBLE, fromUser);
- mView.setAlpha(1f);
- mView.setScaleY(1f);
- mView.setScaleX(1f);
- if (listener != null) {
- listener.onShown();
- }
- }
- }
-
- final Drawable getContentBackground() {
- return mContentBackground;
- }
-
- void onCompatShadowChanged() {
- // Ignore pre-v21
- }
-
- final void updatePadding() {
- Rect rect = mTmpRect;
- getPadding(rect);
- onPaddingUpdated(rect);
- mShadowViewDelegate.setShadowPadding(rect.left, rect.top, rect.right, rect.bottom);
- }
-
- void getPadding(Rect rect) {
- mShadowDrawable.getPadding(rect);
- }
-
- void onPaddingUpdated(Rect padding) {}
-
- void onAttachedToWindow() {
- if (requirePreDrawListener()) {
- ensurePreDrawListener();
- mView.getViewTreeObserver().addOnPreDrawListener(mPreDrawListener);
- }
- }
-
- void onDetachedFromWindow() {
- if (mPreDrawListener != null) {
- mView.getViewTreeObserver().removeOnPreDrawListener(mPreDrawListener);
- mPreDrawListener = null;
- }
- }
-
- boolean requirePreDrawListener() {
- return true;
- }
-
- CircularBorderDrawable createBorderDrawable(int borderWidth, ColorStateList backgroundTint) {
- final Context context = mView.getContext();
- CircularBorderDrawable borderDrawable = newCircularDrawable();
- borderDrawable.setGradientColors(
- ContextCompat.getColor(context, R.color.design_fab_stroke_top_outer_color),
- ContextCompat.getColor(context, R.color.design_fab_stroke_top_inner_color),
- ContextCompat.getColor(context, R.color.design_fab_stroke_end_inner_color),
- ContextCompat.getColor(context, R.color.design_fab_stroke_end_outer_color));
- borderDrawable.setBorderWidth(borderWidth);
- borderDrawable.setBorderTint(backgroundTint);
- return borderDrawable;
- }
-
- CircularBorderDrawable newCircularDrawable() {
- return new CircularBorderDrawable();
- }
-
- void onPreDraw() {
- final float rotation = mView.getRotation();
- if (mRotation != rotation) {
- mRotation = rotation;
- updateFromViewRotation();
- }
- }
-
- private void ensurePreDrawListener() {
- if (mPreDrawListener == null) {
- mPreDrawListener = new ViewTreeObserver.OnPreDrawListener() {
- @Override
- public boolean onPreDraw() {
- FloatingActionButtonImpl.this.onPreDraw();
- return true;
- }
- };
- }
- }
-
- GradientDrawable createShapeDrawable() {
- GradientDrawable d = newGradientDrawableForShape();
- d.setShape(GradientDrawable.OVAL);
- d.setColor(Color.WHITE);
- return d;
- }
-
- GradientDrawable newGradientDrawableForShape() {
- return new GradientDrawable();
- }
-
- boolean isOrWillBeShown() {
- if (mView.getVisibility() != View.VISIBLE) {
- // If we not currently visible, return true if we're animating to be shown
- return mAnimState == ANIM_STATE_SHOWING;
- } else {
- // Otherwise if we're visible, return true if we're not animating to be hidden
- return mAnimState != ANIM_STATE_HIDING;
- }
- }
-
- boolean isOrWillBeHidden() {
- if (mView.getVisibility() == View.VISIBLE) {
- // If we currently visible, return true if we're animating to be hidden
- return mAnimState == ANIM_STATE_HIDING;
- } else {
- // Otherwise if we're not visible, return true if we're not animating to be shown
- return mAnimState != ANIM_STATE_SHOWING;
- }
- }
-
- private ValueAnimator createAnimator(@NonNull ShadowAnimatorImpl impl) {
- final ValueAnimator animator = new ValueAnimator();
- animator.setInterpolator(ANIM_INTERPOLATOR);
- animator.setDuration(PRESSED_ANIM_DURATION);
- animator.addListener(impl);
- animator.addUpdateListener(impl);
- animator.setFloatValues(0, 1);
- return animator;
- }
-
- private abstract class ShadowAnimatorImpl extends AnimatorListenerAdapter
- implements ValueAnimator.AnimatorUpdateListener {
- private boolean mValidValues;
- private float mShadowSizeStart;
- private float mShadowSizeEnd;
-
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- if (!mValidValues) {
- mShadowSizeStart = mShadowDrawable.getShadowSize();
- mShadowSizeEnd = getTargetShadowSize();
- mValidValues = true;
- }
-
- mShadowDrawable.setShadowSize(mShadowSizeStart
- + ((mShadowSizeEnd - mShadowSizeStart) * animator.getAnimatedFraction()));
- }
-
- @Override
- public void onAnimationEnd(Animator animator) {
- mShadowDrawable.setShadowSize(mShadowSizeEnd);
- mValidValues = false;
- }
-
- /**
- * @return the shadow size we want to animate to.
- */
- protected abstract float getTargetShadowSize();
- }
-
- private class ResetElevationAnimation extends ShadowAnimatorImpl {
- ResetElevationAnimation() {
- }
-
- @Override
- protected float getTargetShadowSize() {
- return mElevation;
- }
- }
-
- private class ElevateToTranslationZAnimation extends ShadowAnimatorImpl {
- ElevateToTranslationZAnimation() {
- }
-
- @Override
- protected float getTargetShadowSize() {
- return mElevation + mPressedTranslationZ;
- }
- }
-
- private class DisabledElevationAnimation extends ShadowAnimatorImpl {
- DisabledElevationAnimation() {
- }
-
- @Override
- protected float getTargetShadowSize() {
- return 0f;
- }
- }
-
- private static ColorStateList createColorStateList(int selectedColor) {
- final int[][] states = new int[3][];
- final int[] colors = new int[3];
- int i = 0;
-
- states[i] = FOCUSED_ENABLED_STATE_SET;
- colors[i] = selectedColor;
- i++;
-
- states[i] = PRESSED_ENABLED_STATE_SET;
- colors[i] = selectedColor;
- i++;
-
- // Default enabled state
- states[i] = new int[0];
- colors[i] = Color.TRANSPARENT;
- i++;
-
- return new ColorStateList(states, colors);
- }
-
- private boolean shouldAnimateVisibilityChange() {
- return ViewCompat.isLaidOut(mView) && !mView.isInEditMode();
- }
-
- private void updateFromViewRotation() {
- if (Build.VERSION.SDK_INT == 19) {
- // KitKat seems to have an issue with views which are rotated with angles which are
- // not divisible by 90. Worked around by moving to software rendering in these cases.
- if ((mRotation % 90) != 0) {
- if (mView.getLayerType() != View.LAYER_TYPE_SOFTWARE) {
- mView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
- }
- } else {
- if (mView.getLayerType() != View.LAYER_TYPE_NONE) {
- mView.setLayerType(View.LAYER_TYPE_NONE, null);
- }
- }
- }
-
- // Offset any View rotation
- if (mShadowDrawable != null) {
- mShadowDrawable.setRotation(-mRotation);
- }
- if (mBorderDrawable != null) {
- mBorderDrawable.setRotation(-mRotation);
- }
- }
-}
diff --git a/android/support/design/widget/FloatingActionButtonLollipop.java b/android/support/design/widget/FloatingActionButtonLollipop.java
deleted file mode 100644
index 0df83da5..00000000
--- a/android/support/design/widget/FloatingActionButtonLollipop.java
+++ /dev/null
@@ -1,226 +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 android.support.design.widget;
-
-import android.animation.Animator;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.StateListAnimator;
-import android.content.res.ColorStateList;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.GradientDrawable;
-import android.graphics.drawable.InsetDrawable;
-import android.graphics.drawable.LayerDrawable;
-import android.graphics.drawable.RippleDrawable;
-import android.os.Build;
-import android.support.annotation.RequiresApi;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.view.View;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@RequiresApi(21)
-class FloatingActionButtonLollipop extends FloatingActionButtonImpl {
-
- private InsetDrawable mInsetDrawable;
-
- FloatingActionButtonLollipop(VisibilityAwareImageButton view,
- ShadowViewDelegate shadowViewDelegate) {
- super(view, shadowViewDelegate);
- }
-
- @Override
- void setBackgroundDrawable(ColorStateList backgroundTint,
- PorterDuff.Mode backgroundTintMode, int rippleColor, int borderWidth) {
- // Now we need to tint the shape background with the tint
- mShapeDrawable = DrawableCompat.wrap(createShapeDrawable());
- DrawableCompat.setTintList(mShapeDrawable, backgroundTint);
- if (backgroundTintMode != null) {
- DrawableCompat.setTintMode(mShapeDrawable, backgroundTintMode);
- }
-
- final Drawable rippleContent;
- if (borderWidth > 0) {
- mBorderDrawable = createBorderDrawable(borderWidth, backgroundTint);
- rippleContent = new LayerDrawable(new Drawable[]{mBorderDrawable, mShapeDrawable});
- } else {
- mBorderDrawable = null;
- rippleContent = mShapeDrawable;
- }
-
- mRippleDrawable = new RippleDrawable(ColorStateList.valueOf(rippleColor),
- rippleContent, null);
-
- mContentBackground = mRippleDrawable;
-
- mShadowViewDelegate.setBackgroundDrawable(mRippleDrawable);
- }
-
- @Override
- void setRippleColor(int rippleColor) {
- if (mRippleDrawable instanceof RippleDrawable) {
- ((RippleDrawable) mRippleDrawable).setColor(ColorStateList.valueOf(rippleColor));
- } else {
- super.setRippleColor(rippleColor);
- }
- }
-
- @Override
- void onElevationsChanged(final float elevation, final float pressedTranslationZ) {
- if (Build.VERSION.SDK_INT == 21) {
- // Animations produce NPE in version 21. Bluntly set the values instead (matching the
- // logic in the animations below).
- if (mView.isEnabled()) {
- mView.setElevation(elevation);
- if (mView.isFocused() || mView.isPressed()) {
- mView.setTranslationZ(pressedTranslationZ);
- } else {
- mView.setTranslationZ(0);
- }
- } else {
- mView.setElevation(0);
- mView.setTranslationZ(0);
- }
- } else {
- final StateListAnimator stateListAnimator = new StateListAnimator();
-
- // Animate elevation and translationZ to our values when pressed
- AnimatorSet set = new AnimatorSet();
- set.play(ObjectAnimator.ofFloat(mView, "elevation", elevation).setDuration(0))
- .with(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z, pressedTranslationZ)
- .setDuration(PRESSED_ANIM_DURATION));
- set.setInterpolator(ANIM_INTERPOLATOR);
- stateListAnimator.addState(PRESSED_ENABLED_STATE_SET, set);
-
- // Same deal for when we're focused
- set = new AnimatorSet();
- set.play(ObjectAnimator.ofFloat(mView, "elevation", elevation).setDuration(0))
- .with(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z, pressedTranslationZ)
- .setDuration(PRESSED_ANIM_DURATION));
- set.setInterpolator(ANIM_INTERPOLATOR);
- stateListAnimator.addState(FOCUSED_ENABLED_STATE_SET, set);
-
- // Animate translationZ to 0 if not pressed
- set = new AnimatorSet();
- List<Animator> animators = new ArrayList<>();
- animators.add(ObjectAnimator.ofFloat(mView, "elevation", elevation).setDuration(0));
- if (Build.VERSION.SDK_INT >= 22 && Build.VERSION.SDK_INT <= 24) {
- // This is a no-op animation which exists here only for introducing the duration
- // because setting the delay (on the next animation) via "setDelay" or "after"
- // can trigger a NPE between android versions 22 and 24 (due to a framework
- // bug). The issue has been fixed in version 25.
- animators.add(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z,
- mView.getTranslationZ()).setDuration(PRESSED_ANIM_DELAY));
- }
- animators.add(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z, 0f)
- .setDuration(PRESSED_ANIM_DURATION));
- set.playSequentially(animators.toArray(new ObjectAnimator[0]));
- set.setInterpolator(ANIM_INTERPOLATOR);
- stateListAnimator.addState(ENABLED_STATE_SET, set);
-
- // Animate everything to 0 when disabled
- set = new AnimatorSet();
- set.play(ObjectAnimator.ofFloat(mView, "elevation", 0f).setDuration(0))
- .with(ObjectAnimator.ofFloat(mView, View.TRANSLATION_Z, 0f).setDuration(0));
- set.setInterpolator(ANIM_INTERPOLATOR);
- stateListAnimator.addState(EMPTY_STATE_SET, set);
-
- mView.setStateListAnimator(stateListAnimator);
- }
-
- if (mShadowViewDelegate.isCompatPaddingEnabled()) {
- updatePadding();
- }
- }
-
- @Override
- public float getElevation() {
- return mView.getElevation();
- }
-
- @Override
- void onCompatShadowChanged() {
- updatePadding();
- }
-
- @Override
- void onPaddingUpdated(Rect padding) {
- if (mShadowViewDelegate.isCompatPaddingEnabled()) {
- mInsetDrawable = new InsetDrawable(mRippleDrawable,
- padding.left, padding.top, padding.right, padding.bottom);
- mShadowViewDelegate.setBackgroundDrawable(mInsetDrawable);
- } else {
- mShadowViewDelegate.setBackgroundDrawable(mRippleDrawable);
- }
- }
-
- @Override
- void onDrawableStateChanged(int[] state) {
- // no-op
- }
-
- @Override
- void jumpDrawableToCurrentState() {
- // no-op
- }
-
- @Override
- boolean requirePreDrawListener() {
- return false;
- }
-
- @Override
- CircularBorderDrawable newCircularDrawable() {
- return new CircularBorderDrawableLollipop();
- }
-
- @Override
- GradientDrawable newGradientDrawableForShape() {
- return new AlwaysStatefulGradientDrawable();
- }
-
- @Override
- void getPadding(Rect rect) {
- if (mShadowViewDelegate.isCompatPaddingEnabled()) {
- final float radius = mShadowViewDelegate.getRadius();
- final float maxShadowSize = getElevation() + mPressedTranslationZ;
- final int hPadding = (int) Math.ceil(
- ShadowDrawableWrapper.calculateHorizontalPadding(maxShadowSize, radius, false));
- final int vPadding = (int) Math.ceil(
- ShadowDrawableWrapper.calculateVerticalPadding(maxShadowSize, radius, false));
- rect.set(hPadding, vPadding, hPadding, vPadding);
- } else {
- rect.set(0, 0, 0, 0);
- }
- }
-
- /**
- * LayerDrawable on L+ caches its isStateful() state and doesn't refresh it,
- * meaning that if we apply a tint to one of its children, the parent doesn't become
- * stateful and the tint doesn't work for state changes. We workaround it by saying that we
- * are always stateful. If we don't have a stateful tint, the change is ignored anyway.
- */
- static class AlwaysStatefulGradientDrawable extends GradientDrawable {
- @Override
- public boolean isStateful() {
- return true;
- }
- }
-}
diff --git a/android/support/design/widget/HeaderBehavior.java b/android/support/design/widget/HeaderBehavior.java
deleted file mode 100644
index a5d0edf6..00000000
--- a/android/support/design/widget/HeaderBehavior.java
+++ /dev/null
@@ -1,307 +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 android.support.design.widget;
-
-import android.content.Context;
-import android.support.design.widget.CoordinatorLayout.Behavior;
-import android.support.v4.math.MathUtils;
-import android.support.v4.view.ViewCompat;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.VelocityTracker;
-import android.view.View;
-import android.view.ViewConfiguration;
-import android.widget.OverScroller;
-
-/**
- * The {@link Behavior} for a view that sits vertically above scrolling a view.
- * See {@link HeaderScrollingViewBehavior}.
- */
-abstract class HeaderBehavior<V extends View> extends ViewOffsetBehavior<V> {
-
- private static final int INVALID_POINTER = -1;
-
- private Runnable mFlingRunnable;
- OverScroller mScroller;
-
- private boolean mIsBeingDragged;
- private int mActivePointerId = INVALID_POINTER;
- private int mLastMotionY;
- private int mTouchSlop = -1;
- private VelocityTracker mVelocityTracker;
-
- public HeaderBehavior() {}
-
- public HeaderBehavior(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
- if (mTouchSlop < 0) {
- mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
- }
-
- final int action = ev.getAction();
-
- // Shortcut since we're being dragged
- if (action == MotionEvent.ACTION_MOVE && mIsBeingDragged) {
- return true;
- }
-
- switch (ev.getActionMasked()) {
- case MotionEvent.ACTION_DOWN: {
- mIsBeingDragged = false;
- final int x = (int) ev.getX();
- final int y = (int) ev.getY();
- if (canDragView(child) && parent.isPointInChildBounds(child, x, y)) {
- mLastMotionY = y;
- mActivePointerId = ev.getPointerId(0);
- ensureVelocityTracker();
- }
- break;
- }
-
- case MotionEvent.ACTION_MOVE: {
- final int activePointerId = mActivePointerId;
- if (activePointerId == INVALID_POINTER) {
- // If we don't have a valid id, the touch down wasn't on content.
- break;
- }
- final int pointerIndex = ev.findPointerIndex(activePointerId);
- if (pointerIndex == -1) {
- break;
- }
-
- final int y = (int) ev.getY(pointerIndex);
- final int yDiff = Math.abs(y - mLastMotionY);
- if (yDiff > mTouchSlop) {
- mIsBeingDragged = true;
- mLastMotionY = y;
- }
- break;
- }
-
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP: {
- mIsBeingDragged = false;
- mActivePointerId = INVALID_POINTER;
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
- break;
- }
- }
-
- if (mVelocityTracker != null) {
- mVelocityTracker.addMovement(ev);
- }
-
- return mIsBeingDragged;
- }
-
- @Override
- public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent ev) {
- if (mTouchSlop < 0) {
- mTouchSlop = ViewConfiguration.get(parent.getContext()).getScaledTouchSlop();
- }
-
- switch (ev.getActionMasked()) {
- case MotionEvent.ACTION_DOWN: {
- final int x = (int) ev.getX();
- final int y = (int) ev.getY();
-
- if (parent.isPointInChildBounds(child, x, y) && canDragView(child)) {
- mLastMotionY = y;
- mActivePointerId = ev.getPointerId(0);
- ensureVelocityTracker();
- } else {
- return false;
- }
- break;
- }
-
- case MotionEvent.ACTION_MOVE: {
- final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
- if (activePointerIndex == -1) {
- return false;
- }
-
- final int y = (int) ev.getY(activePointerIndex);
- int dy = mLastMotionY - y;
-
- if (!mIsBeingDragged && Math.abs(dy) > mTouchSlop) {
- mIsBeingDragged = true;
- if (dy > 0) {
- dy -= mTouchSlop;
- } else {
- dy += mTouchSlop;
- }
- }
-
- if (mIsBeingDragged) {
- mLastMotionY = y;
- // We're being dragged so scroll the ABL
- scroll(parent, child, dy, getMaxDragOffset(child), 0);
- }
- break;
- }
-
- case MotionEvent.ACTION_UP:
- if (mVelocityTracker != null) {
- mVelocityTracker.addMovement(ev);
- mVelocityTracker.computeCurrentVelocity(1000);
- float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
- fling(parent, child, -getScrollRangeForDragFling(child), 0, yvel);
- }
- // $FALLTHROUGH
- case MotionEvent.ACTION_CANCEL: {
- mIsBeingDragged = false;
- mActivePointerId = INVALID_POINTER;
- if (mVelocityTracker != null) {
- mVelocityTracker.recycle();
- mVelocityTracker = null;
- }
- break;
- }
- }
-
- if (mVelocityTracker != null) {
- mVelocityTracker.addMovement(ev);
- }
-
- return true;
- }
-
- int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset) {
- return setHeaderTopBottomOffset(parent, header, newOffset,
- Integer.MIN_VALUE, Integer.MAX_VALUE);
- }
-
- int setHeaderTopBottomOffset(CoordinatorLayout parent, V header, int newOffset,
- int minOffset, int maxOffset) {
- final int curOffset = getTopAndBottomOffset();
- int consumed = 0;
-
- if (minOffset != 0 && curOffset >= minOffset && curOffset <= maxOffset) {
- // If we have some scrolling range, and we're currently within the min and max
- // offsets, calculate a new offset
- newOffset = MathUtils.clamp(newOffset, minOffset, maxOffset);
-
- if (curOffset != newOffset) {
- setTopAndBottomOffset(newOffset);
- // Update how much dy we have consumed
- consumed = curOffset - newOffset;
- }
- }
-
- return consumed;
- }
-
- int getTopBottomOffsetForScrollingSibling() {
- return getTopAndBottomOffset();
- }
-
- final int scroll(CoordinatorLayout coordinatorLayout, V header,
- int dy, int minOffset, int maxOffset) {
- return setHeaderTopBottomOffset(coordinatorLayout, header,
- getTopBottomOffsetForScrollingSibling() - dy, minOffset, maxOffset);
- }
-
- final boolean fling(CoordinatorLayout coordinatorLayout, V layout, int minOffset,
- int maxOffset, float velocityY) {
- if (mFlingRunnable != null) {
- layout.removeCallbacks(mFlingRunnable);
- mFlingRunnable = null;
- }
-
- if (mScroller == null) {
- mScroller = new OverScroller(layout.getContext());
- }
-
- mScroller.fling(
- 0, getTopAndBottomOffset(), // curr
- 0, Math.round(velocityY), // velocity.
- 0, 0, // x
- minOffset, maxOffset); // y
-
- if (mScroller.computeScrollOffset()) {
- mFlingRunnable = new FlingRunnable(coordinatorLayout, layout);
- ViewCompat.postOnAnimation(layout, mFlingRunnable);
- return true;
- } else {
- onFlingFinished(coordinatorLayout, layout);
- return false;
- }
- }
-
- /**
- * Called when a fling has finished, or the fling was initiated but there wasn't enough
- * velocity to start it.
- */
- void onFlingFinished(CoordinatorLayout parent, V layout) {
- // no-op
- }
-
- /**
- * Return true if the view can be dragged.
- */
- boolean canDragView(V view) {
- return false;
- }
-
- /**
- * Returns the maximum px offset when {@code view} is being dragged.
- */
- int getMaxDragOffset(V view) {
- return -view.getHeight();
- }
-
- int getScrollRangeForDragFling(V view) {
- return view.getHeight();
- }
-
- private void ensureVelocityTracker() {
- if (mVelocityTracker == null) {
- mVelocityTracker = VelocityTracker.obtain();
- }
- }
-
- private class FlingRunnable implements Runnable {
- private final CoordinatorLayout mParent;
- private final V mLayout;
-
- FlingRunnable(CoordinatorLayout parent, V layout) {
- mParent = parent;
- mLayout = layout;
- }
-
- @Override
- public void run() {
- if (mLayout != null && mScroller != null) {
- if (mScroller.computeScrollOffset()) {
- setHeaderTopBottomOffset(mParent, mLayout, mScroller.getCurrY());
- // Post ourselves so that we run on the next animation
- ViewCompat.postOnAnimation(mLayout, this);
- } else {
- onFlingFinished(mParent, mLayout);
- }
- }
- }
- }
-}
diff --git a/android/support/design/widget/HeaderScrollingViewBehavior.java b/android/support/design/widget/HeaderScrollingViewBehavior.java
deleted file mode 100644
index 81ddde54..00000000
--- a/android/support/design/widget/HeaderScrollingViewBehavior.java
+++ /dev/null
@@ -1,182 +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 android.support.design.widget;
-
-import android.content.Context;
-import android.graphics.Rect;
-import android.support.design.widget.CoordinatorLayout.Behavior;
-import android.support.v4.math.MathUtils;
-import android.support.v4.view.GravityCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.util.AttributeSet;
-import android.view.Gravity;
-import android.view.View;
-import android.view.ViewGroup;
-
-import java.util.List;
-
-/**
- * The {@link Behavior} for a scrolling view that is positioned vertically below another view.
- * See {@link HeaderBehavior}.
- */
-abstract class HeaderScrollingViewBehavior extends ViewOffsetBehavior<View> {
-
- final Rect mTempRect1 = new Rect();
- final Rect mTempRect2 = new Rect();
-
- private int mVerticalLayoutGap = 0;
- private int mOverlayTop;
-
- public HeaderScrollingViewBehavior() {}
-
- public HeaderScrollingViewBehavior(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public boolean onMeasureChild(CoordinatorLayout parent, View child,
- int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec,
- int heightUsed) {
- final int childLpHeight = child.getLayoutParams().height;
- if (childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
- || childLpHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
- // If the menu's height is set to match_parent/wrap_content then measure it
- // with the maximum visible height
-
- final List<View> dependencies = parent.getDependencies(child);
- final View header = findFirstDependency(dependencies);
- if (header != null) {
- if (ViewCompat.getFitsSystemWindows(header)
- && !ViewCompat.getFitsSystemWindows(child)) {
- // If the header is fitting system windows then we need to also,
- // otherwise we'll get CoL's compatible measuring
- ViewCompat.setFitsSystemWindows(child, true);
-
- if (ViewCompat.getFitsSystemWindows(child)) {
- // If the set succeeded, trigger a new layout and return true
- child.requestLayout();
- return true;
- }
- }
-
- int availableHeight = View.MeasureSpec.getSize(parentHeightMeasureSpec);
- if (availableHeight == 0) {
- // If the measure spec doesn't specify a size, use the current height
- availableHeight = parent.getHeight();
- }
-
- final int height = availableHeight - header.getMeasuredHeight()
- + getScrollRange(header);
- final int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(height,
- childLpHeight == ViewGroup.LayoutParams.MATCH_PARENT
- ? View.MeasureSpec.EXACTLY
- : View.MeasureSpec.AT_MOST);
-
- // Now measure the scrolling view with the correct height
- parent.onMeasureChild(child, parentWidthMeasureSpec,
- widthUsed, heightMeasureSpec, heightUsed);
-
- return true;
- }
- }
- return false;
- }
-
- @Override
- protected void layoutChild(final CoordinatorLayout parent, final View child,
- final int layoutDirection) {
- final List<View> dependencies = parent.getDependencies(child);
- final View header = findFirstDependency(dependencies);
-
- if (header != null) {
- final CoordinatorLayout.LayoutParams lp =
- (CoordinatorLayout.LayoutParams) child.getLayoutParams();
- final Rect available = mTempRect1;
- available.set(parent.getPaddingLeft() + lp.leftMargin,
- header.getBottom() + lp.topMargin,
- parent.getWidth() - parent.getPaddingRight() - lp.rightMargin,
- parent.getHeight() + header.getBottom()
- - parent.getPaddingBottom() - lp.bottomMargin);
-
- final WindowInsetsCompat parentInsets = parent.getLastWindowInsets();
- if (parentInsets != null && ViewCompat.getFitsSystemWindows(parent)
- && !ViewCompat.getFitsSystemWindows(child)) {
- // If we're set to handle insets but this child isn't, then it has been measured as
- // if there are no insets. We need to lay it out to match horizontally.
- // Top and bottom and already handled in the logic above
- available.left += parentInsets.getSystemWindowInsetLeft();
- available.right -= parentInsets.getSystemWindowInsetRight();
- }
-
- final Rect out = mTempRect2;
- GravityCompat.apply(resolveGravity(lp.gravity), child.getMeasuredWidth(),
- child.getMeasuredHeight(), available, out, layoutDirection);
-
- final int overlap = getOverlapPixelsForOffset(header);
-
- child.layout(out.left, out.top - overlap, out.right, out.bottom - overlap);
- mVerticalLayoutGap = out.top - header.getBottom();
- } else {
- // If we don't have a dependency, let super handle it
- super.layoutChild(parent, child, layoutDirection);
- mVerticalLayoutGap = 0;
- }
- }
-
- float getOverlapRatioForOffset(final View header) {
- return 1f;
- }
-
- final int getOverlapPixelsForOffset(final View header) {
- return mOverlayTop == 0 ? 0 : MathUtils.clamp(
- (int) (getOverlapRatioForOffset(header) * mOverlayTop), 0, mOverlayTop);
- }
-
- private static int resolveGravity(int gravity) {
- return gravity == Gravity.NO_GRAVITY ? GravityCompat.START | Gravity.TOP : gravity;
- }
-
- abstract View findFirstDependency(List<View> views);
-
- int getScrollRange(View v) {
- return v.getMeasuredHeight();
- }
-
- /**
- * The gap between the top of the scrolling view and the bottom of the header layout in pixels.
- */
- final int getVerticalLayoutGap() {
- return mVerticalLayoutGap;
- }
-
- /**
- * Set the distance that this view should overlap any {@link AppBarLayout}.
- *
- * @param overlayTop the distance in px
- */
- public final void setOverlayTop(int overlayTop) {
- mOverlayTop = overlayTop;
- }
-
- /**
- * Returns the distance that this view should overlap any {@link AppBarLayout}.
- */
- public final int getOverlayTop() {
- return mOverlayTop;
- }
-} \ No newline at end of file
diff --git a/android/support/design/widget/NavigationView.java b/android/support/design/widget/NavigationView.java
deleted file mode 100644
index 8fc8c76b..00000000
--- a/android/support/design/widget/NavigationView.java
+++ /dev/null
@@ -1,494 +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 android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.IdRes;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.StyleRes;
-import android.support.design.R;
-import android.support.design.internal.NavigationMenu;
-import android.support.design.internal.NavigationMenuPresenter;
-import android.support.design.internal.ScrimInsetsFrameLayout;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.view.AbsSavedState;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.WindowInsetsCompat;
-import android.support.v7.content.res.AppCompatResources;
-import android.support.v7.view.SupportMenuInflater;
-import android.support.v7.view.menu.MenuBuilder;
-import android.support.v7.view.menu.MenuItemImpl;
-import android.support.v7.widget.TintTypedArray;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-
-/**
- * Represents a standard navigation menu for application. The menu contents can be populated
- * by a menu resource file.
- * <p>NavigationView is typically placed inside a {@link android.support.v4.widget.DrawerLayout}.
- * </p>
- * <pre>
- * &lt;android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
- * xmlns:app="http://schemas.android.com/apk/res-auto"
- * android:id="@+id/drawer_layout"
- * android:layout_width="match_parent"
- * android:layout_height="match_parent"
- * android:fitsSystemWindows="true"&gt;
- *
- * &lt;!-- Your contents --&gt;
- *
- * &lt;android.support.design.widget.NavigationView
- * android:id="@+id/navigation"
- * android:layout_width="wrap_content"
- * android:layout_height="match_parent"
- * android:layout_gravity="start"
- * app:menu="@menu/my_navigation_items" /&gt;
- * &lt;/android.support.v4.widget.DrawerLayout&gt;
- * </pre>
- */
-public class NavigationView extends ScrimInsetsFrameLayout {
-
- private static final int[] CHECKED_STATE_SET = {android.R.attr.state_checked};
- private static final int[] DISABLED_STATE_SET = {-android.R.attr.state_enabled};
-
- private static final int PRESENTER_NAVIGATION_VIEW_ID = 1;
-
- private final NavigationMenu mMenu;
- private final NavigationMenuPresenter mPresenter = new NavigationMenuPresenter();
-
- OnNavigationItemSelectedListener mListener;
- private int mMaxWidth;
-
- private MenuInflater mMenuInflater;
-
- public NavigationView(Context context) {
- this(context, null);
- }
-
- public NavigationView(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public NavigationView(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- // Create the menu
- mMenu = new NavigationMenu(context);
-
- // Custom attributes
- TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
- R.styleable.NavigationView, defStyleAttr,
- R.style.Widget_Design_NavigationView);
-
- ViewCompat.setBackground(
- this, a.getDrawable(R.styleable.NavigationView_android_background));
- if (a.hasValue(R.styleable.NavigationView_elevation)) {
- ViewCompat.setElevation(this, a.getDimensionPixelSize(
- R.styleable.NavigationView_elevation, 0));
- }
- ViewCompat.setFitsSystemWindows(this,
- a.getBoolean(R.styleable.NavigationView_android_fitsSystemWindows, false));
-
- mMaxWidth = a.getDimensionPixelSize(R.styleable.NavigationView_android_maxWidth, 0);
-
- final ColorStateList itemIconTint;
- if (a.hasValue(R.styleable.NavigationView_itemIconTint)) {
- itemIconTint = a.getColorStateList(R.styleable.NavigationView_itemIconTint);
- } else {
- itemIconTint = createDefaultColorStateList(android.R.attr.textColorSecondary);
- }
-
- boolean textAppearanceSet = false;
- int textAppearance = 0;
- if (a.hasValue(R.styleable.NavigationView_itemTextAppearance)) {
- textAppearance = a.getResourceId(R.styleable.NavigationView_itemTextAppearance, 0);
- textAppearanceSet = true;
- }
-
- ColorStateList itemTextColor = null;
- if (a.hasValue(R.styleable.NavigationView_itemTextColor)) {
- itemTextColor = a.getColorStateList(R.styleable.NavigationView_itemTextColor);
- }
-
- if (!textAppearanceSet && itemTextColor == null) {
- // If there isn't a text appearance set, we'll use a default text color
- itemTextColor = createDefaultColorStateList(android.R.attr.textColorPrimary);
- }
-
- final Drawable itemBackground = a.getDrawable(R.styleable.NavigationView_itemBackground);
-
- mMenu.setCallback(new MenuBuilder.Callback() {
- @Override
- public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
- return mListener != null && mListener.onNavigationItemSelected(item);
- }
-
- @Override
- public void onMenuModeChange(MenuBuilder menu) {}
- });
- mPresenter.setId(PRESENTER_NAVIGATION_VIEW_ID);
- mPresenter.initForMenu(context, mMenu);
- mPresenter.setItemIconTintList(itemIconTint);
- if (textAppearanceSet) {
- mPresenter.setItemTextAppearance(textAppearance);
- }
- mPresenter.setItemTextColor(itemTextColor);
- mPresenter.setItemBackground(itemBackground);
- mMenu.addMenuPresenter(mPresenter);
- addView((View) mPresenter.getMenuView(this));
-
- if (a.hasValue(R.styleable.NavigationView_menu)) {
- inflateMenu(a.getResourceId(R.styleable.NavigationView_menu, 0));
- }
-
- if (a.hasValue(R.styleable.NavigationView_headerLayout)) {
- inflateHeaderView(a.getResourceId(R.styleable.NavigationView_headerLayout, 0));
- }
-
- a.recycle();
- }
-
- @Override
- protected Parcelable onSaveInstanceState() {
- Parcelable superState = super.onSaveInstanceState();
- SavedState state = new SavedState(superState);
- state.menuState = new Bundle();
- mMenu.savePresenterStates(state.menuState);
- return state;
- }
-
- @Override
- protected void onRestoreInstanceState(Parcelable savedState) {
- if (!(savedState instanceof SavedState)) {
- super.onRestoreInstanceState(savedState);
- return;
- }
- SavedState state = (SavedState) savedState;
- super.onRestoreInstanceState(state.getSuperState());
- mMenu.restorePresenterStates(state.menuState);
- }
-
- /**
- * Set a listener that will be notified when a menu item is selected.
- *
- * @param listener The listener to notify
- */
- public void setNavigationItemSelectedListener(
- @Nullable OnNavigationItemSelectedListener listener) {
- mListener = listener;
- }
-
- @Override
- protected void onMeasure(int widthSpec, int heightSpec) {
- switch (MeasureSpec.getMode(widthSpec)) {
- case MeasureSpec.EXACTLY:
- // Nothing to do
- break;
- case MeasureSpec.AT_MOST:
- widthSpec = MeasureSpec.makeMeasureSpec(
- Math.min(MeasureSpec.getSize(widthSpec), mMaxWidth), MeasureSpec.EXACTLY);
- break;
- case MeasureSpec.UNSPECIFIED:
- widthSpec = MeasureSpec.makeMeasureSpec(mMaxWidth, MeasureSpec.EXACTLY);
- break;
- }
- // Let super sort out the height
- super.onMeasure(widthSpec, heightSpec);
- }
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- @Override
- protected void onInsetsChanged(WindowInsetsCompat insets) {
- mPresenter.dispatchApplyWindowInsets(insets);
- }
-
- /**
- * Inflate a menu resource into this navigation view.
- *
- * <p>Existing items in the menu will not be modified or removed.</p>
- *
- * @param resId ID of a menu resource to inflate
- */
- public void inflateMenu(int resId) {
- mPresenter.setUpdateSuspended(true);
- getMenuInflater().inflate(resId, mMenu);
- mPresenter.setUpdateSuspended(false);
- mPresenter.updateMenuView(false);
- }
-
- /**
- * Returns the {@link Menu} instance associated with this navigation view.
- */
- public Menu getMenu() {
- return mMenu;
- }
-
- /**
- * Inflates a View and add it as a header of the navigation menu.
- *
- * @param res The layout resource ID.
- * @return a newly inflated View.
- */
- public View inflateHeaderView(@LayoutRes int res) {
- return mPresenter.inflateHeaderView(res);
- }
-
- /**
- * Adds a View as a header of the navigation menu.
- *
- * @param view The view to be added as a header of the navigation menu.
- */
- public void addHeaderView(@NonNull View view) {
- mPresenter.addHeaderView(view);
- }
-
- /**
- * Removes a previously-added header view.
- *
- * @param view The view to remove
- */
- public void removeHeaderView(@NonNull View view) {
- mPresenter.removeHeaderView(view);
- }
-
- /**
- * Gets the number of headers in this NavigationView.
- *
- * @return A positive integer representing the number of headers.
- */
- public int getHeaderCount() {
- return mPresenter.getHeaderCount();
- }
-
- /**
- * Gets the header view at the specified position.
- *
- * @param index The position at which to get the view from.
- * @return The header view the specified position or null if the position does not exist in this
- * NavigationView.
- */
- public View getHeaderView(int index) {
- return mPresenter.getHeaderView(index);
- }
-
- /**
- * Returns the tint which is applied to our menu items' icons.
- *
- * @see #setItemIconTintList(ColorStateList)
- *
- * @attr ref R.styleable#NavigationView_itemIconTint
- */
- @Nullable
- public ColorStateList getItemIconTintList() {
- return mPresenter.getItemTintList();
- }
-
- /**
- * Set the tint which is applied to our menu items' icons.
- *
- * @param tint the tint to apply.
- *
- * @attr ref R.styleable#NavigationView_itemIconTint
- */
- public void setItemIconTintList(@Nullable ColorStateList tint) {
- mPresenter.setItemIconTintList(tint);
- }
-
- /**
- * Returns the tint which is applied to our menu items' icons.
- *
- * @see #setItemTextColor(ColorStateList)
- *
- * @attr ref R.styleable#NavigationView_itemTextColor
- */
- @Nullable
- public ColorStateList getItemTextColor() {
- return mPresenter.getItemTextColor();
- }
-
- /**
- * Set the text color to be used on our menu items.
- *
- * @see #getItemTextColor()
- *
- * @attr ref R.styleable#NavigationView_itemTextColor
- */
- public void setItemTextColor(@Nullable ColorStateList textColor) {
- mPresenter.setItemTextColor(textColor);
- }
-
- /**
- * Returns the background drawable for our menu items.
- *
- * @see #setItemBackgroundResource(int)
- *
- * @attr ref R.styleable#NavigationView_itemBackground
- */
- @Nullable
- public Drawable getItemBackground() {
- return mPresenter.getItemBackground();
- }
-
- /**
- * Set the background of our menu items to the given resource.
- *
- * @param resId The identifier of the resource.
- *
- * @attr ref R.styleable#NavigationView_itemBackground
- */
- public void setItemBackgroundResource(@DrawableRes int resId) {
- setItemBackground(ContextCompat.getDrawable(getContext(), resId));
- }
-
- /**
- * Set the background of our menu items to a given resource. The resource should refer to
- * a Drawable object or null to use the default background set on this navigation menu.
- *
- * @attr ref R.styleable#NavigationView_itemBackground
- */
- public void setItemBackground(@Nullable Drawable itemBackground) {
- mPresenter.setItemBackground(itemBackground);
- }
-
- /**
- * Sets the currently checked item in this navigation menu.
- *
- * @param id The item ID of the currently checked item.
- */
- public void setCheckedItem(@IdRes int id) {
- MenuItem item = mMenu.findItem(id);
- if (item != null) {
- mPresenter.setCheckedItem((MenuItemImpl) item);
- }
- }
-
- /**
- * Set the text appearance of the menu items to a given resource.
- *
- * @attr ref R.styleable#NavigationView_itemTextAppearance
- */
- public void setItemTextAppearance(@StyleRes int resId) {
- mPresenter.setItemTextAppearance(resId);
- }
-
- private MenuInflater getMenuInflater() {
- if (mMenuInflater == null) {
- mMenuInflater = new SupportMenuInflater(getContext());
- }
- return mMenuInflater;
- }
-
- private ColorStateList createDefaultColorStateList(int baseColorThemeAttr) {
- final TypedValue value = new TypedValue();
- if (!getContext().getTheme().resolveAttribute(baseColorThemeAttr, value, true)) {
- return null;
- }
- ColorStateList baseColor = AppCompatResources.getColorStateList(
- getContext(), value.resourceId);
- if (!getContext().getTheme().resolveAttribute(
- android.support.v7.appcompat.R.attr.colorPrimary, value, true)) {
- return null;
- }
- int colorPrimary = value.data;
- int defaultColor = baseColor.getDefaultColor();
- return new ColorStateList(new int[][]{
- DISABLED_STATE_SET,
- CHECKED_STATE_SET,
- EMPTY_STATE_SET
- }, new int[]{
- baseColor.getColorForState(DISABLED_STATE_SET, defaultColor),
- colorPrimary,
- defaultColor
- });
- }
-
- /**
- * Listener for handling events on navigation items.
- */
- public interface OnNavigationItemSelectedListener {
-
- /**
- * Called when an item in the navigation menu is selected.
- *
- * @param item The selected item
- *
- * @return true to display the item as the selected item
- */
- public boolean onNavigationItemSelected(@NonNull MenuItem item);
- }
-
- /**
- * User interface state that is stored by NavigationView for implementing
- * onSaveInstanceState().
- */
- public static class SavedState extends AbsSavedState {
- public Bundle menuState;
-
- public SavedState(Parcel in, ClassLoader loader) {
- super(in, loader);
- menuState = in.readBundle(loader);
- }
-
- public SavedState(Parcelable superState) {
- super(superState);
- }
-
- @Override
- public void writeToParcel(@NonNull Parcel dest, int flags) {
- super.writeToParcel(dest, flags);
- dest.writeBundle(menuState);
- }
-
- public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in, ClassLoader loader) {
- return new SavedState(in, loader);
- }
-
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in, null);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-
-}
diff --git a/android/support/design/widget/ShadowDrawableWrapper.java b/android/support/design/widget/ShadowDrawableWrapper.java
deleted file mode 100644
index dfb8e1d6..00000000
--- a/android/support/design/widget/ShadowDrawableWrapper.java
+++ /dev/null
@@ -1,365 +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.support.design.widget;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.LinearGradient;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.PixelFormat;
-import android.graphics.RadialGradient;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.graphics.Shader;
-import android.graphics.drawable.Drawable;
-import android.support.design.R;
-import android.support.v4.content.ContextCompat;
-import android.support.v7.graphics.drawable.DrawableWrapper;
-
-/**
- * A {@link android.graphics.drawable.Drawable} which wraps another drawable and
- * draws a shadow around it.
- */
-class ShadowDrawableWrapper extends DrawableWrapper {
- // used to calculate content padding
- static final double COS_45 = Math.cos(Math.toRadians(45));
-
- static final float SHADOW_MULTIPLIER = 1.5f;
-
- static final float SHADOW_TOP_SCALE = 0.25f;
- static final float SHADOW_HORIZ_SCALE = 0.5f;
- static final float SHADOW_BOTTOM_SCALE = 1f;
-
- final Paint mCornerShadowPaint;
- final Paint mEdgeShadowPaint;
-
- final RectF mContentBounds;
-
- float mCornerRadius;
-
- Path mCornerShadowPath;
-
- // updated value with inset
- float mMaxShadowSize;
- // actual value set by developer
- float mRawMaxShadowSize;
-
- // multiplied value to account for shadow offset
- float mShadowSize;
- // actual value set by developer
- float mRawShadowSize;
-
- private boolean mDirty = true;
-
- private final int mShadowStartColor;
- private final int mShadowMiddleColor;
- private final int mShadowEndColor;
-
- private boolean mAddPaddingForCorners = true;
-
- private float mRotation;
-
- /**
- * If shadow size is set to a value above max shadow, we print a warning
- */
- private boolean mPrintedShadowClipWarning = false;
-
- public ShadowDrawableWrapper(Context context, Drawable content, float radius,
- float shadowSize, float maxShadowSize) {
- super(content);
-
- mShadowStartColor = ContextCompat.getColor(context, R.color.design_fab_shadow_start_color);
- mShadowMiddleColor = ContextCompat.getColor(context, R.color.design_fab_shadow_mid_color);
- mShadowEndColor = ContextCompat.getColor(context, R.color.design_fab_shadow_end_color);
-
- mCornerShadowPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG);
- mCornerShadowPaint.setStyle(Paint.Style.FILL);
- mCornerRadius = Math.round(radius);
- mContentBounds = new RectF();
- mEdgeShadowPaint = new Paint(mCornerShadowPaint);
- mEdgeShadowPaint.setAntiAlias(false);
- setShadowSize(shadowSize, maxShadowSize);
- }
-
- /**
- * Casts the value to an even integer.
- */
- private static int toEven(float value) {
- int i = Math.round(value);
- return (i % 2 == 1) ? i - 1 : i;
- }
-
- public void setAddPaddingForCorners(boolean addPaddingForCorners) {
- mAddPaddingForCorners = addPaddingForCorners;
- invalidateSelf();
- }
-
- @Override
- public void setAlpha(int alpha) {
- super.setAlpha(alpha);
- mCornerShadowPaint.setAlpha(alpha);
- mEdgeShadowPaint.setAlpha(alpha);
- }
-
- @Override
- protected void onBoundsChange(Rect bounds) {
- mDirty = true;
- }
-
- void setShadowSize(float shadowSize, float maxShadowSize) {
- if (shadowSize < 0 || maxShadowSize < 0) {
- throw new IllegalArgumentException("invalid shadow size");
- }
- shadowSize = toEven(shadowSize);
- maxShadowSize = toEven(maxShadowSize);
- if (shadowSize > maxShadowSize) {
- shadowSize = maxShadowSize;
- if (!mPrintedShadowClipWarning) {
- mPrintedShadowClipWarning = true;
- }
- }
- if (mRawShadowSize == shadowSize && mRawMaxShadowSize == maxShadowSize) {
- return;
- }
- mRawShadowSize = shadowSize;
- mRawMaxShadowSize = maxShadowSize;
- mShadowSize = Math.round(shadowSize * SHADOW_MULTIPLIER);
- mMaxShadowSize = maxShadowSize;
- mDirty = true;
- invalidateSelf();
- }
-
- @Override
- public boolean getPadding(Rect padding) {
- int vOffset = (int) Math.ceil(calculateVerticalPadding(mRawMaxShadowSize, mCornerRadius,
- mAddPaddingForCorners));
- int hOffset = (int) Math.ceil(calculateHorizontalPadding(mRawMaxShadowSize, mCornerRadius,
- mAddPaddingForCorners));
- padding.set(hOffset, vOffset, hOffset, vOffset);
- return true;
- }
-
- public static float calculateVerticalPadding(float maxShadowSize, float cornerRadius,
- boolean addPaddingForCorners) {
- if (addPaddingForCorners) {
- return (float) (maxShadowSize * SHADOW_MULTIPLIER + (1 - COS_45) * cornerRadius);
- } else {
- return maxShadowSize * SHADOW_MULTIPLIER;
- }
- }
-
- public static float calculateHorizontalPadding(float maxShadowSize, float cornerRadius,
- boolean addPaddingForCorners) {
- if (addPaddingForCorners) {
- return (float) (maxShadowSize + (1 - COS_45) * cornerRadius);
- } else {
- return maxShadowSize;
- }
- }
-
- @Override
- public int getOpacity() {
- return PixelFormat.TRANSLUCENT;
- }
-
- public void setCornerRadius(float radius) {
- radius = Math.round(radius);
- if (mCornerRadius == radius) {
- return;
- }
- mCornerRadius = radius;
- mDirty = true;
- invalidateSelf();
- }
-
- @Override
- public void draw(Canvas canvas) {
- if (mDirty) {
- buildComponents(getBounds());
- mDirty = false;
- }
- drawShadow(canvas);
-
- super.draw(canvas);
- }
-
- final void setRotation(float rotation) {
- if (mRotation != rotation) {
- mRotation = rotation;
- invalidateSelf();
- }
- }
-
- private void drawShadow(Canvas canvas) {
- final int rotateSaved = canvas.save();
- canvas.rotate(mRotation, mContentBounds.centerX(), mContentBounds.centerY());
-
- final float edgeShadowTop = -mCornerRadius - mShadowSize;
- final float shadowOffset = mCornerRadius;
- final boolean drawHorizontalEdges = mContentBounds.width() - 2 * shadowOffset > 0;
- final boolean drawVerticalEdges = mContentBounds.height() - 2 * shadowOffset > 0;
-
- final float shadowOffsetTop = mRawShadowSize - (mRawShadowSize * SHADOW_TOP_SCALE);
- final float shadowOffsetHorizontal = mRawShadowSize - (mRawShadowSize * SHADOW_HORIZ_SCALE);
- final float shadowOffsetBottom = mRawShadowSize - (mRawShadowSize * SHADOW_BOTTOM_SCALE);
-
- final float shadowScaleHorizontal = shadowOffset / (shadowOffset + shadowOffsetHorizontal);
- final float shadowScaleTop = shadowOffset / (shadowOffset + shadowOffsetTop);
- final float shadowScaleBottom = shadowOffset / (shadowOffset + shadowOffsetBottom);
-
- // LT
- int saved = canvas.save();
- canvas.translate(mContentBounds.left + shadowOffset, mContentBounds.top + shadowOffset);
- canvas.scale(shadowScaleHorizontal, shadowScaleTop);
- canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
- if (drawHorizontalEdges) {
- // TE
- canvas.scale(1f / shadowScaleHorizontal, 1f);
- canvas.drawRect(0, edgeShadowTop,
- mContentBounds.width() - 2 * shadowOffset, -mCornerRadius,
- mEdgeShadowPaint);
- }
- canvas.restoreToCount(saved);
- // RB
- saved = canvas.save();
- canvas.translate(mContentBounds.right - shadowOffset, mContentBounds.bottom - shadowOffset);
- canvas.scale(shadowScaleHorizontal, shadowScaleBottom);
- canvas.rotate(180f);
- canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
- if (drawHorizontalEdges) {
- // BE
- canvas.scale(1f / shadowScaleHorizontal, 1f);
- canvas.drawRect(0, edgeShadowTop,
- mContentBounds.width() - 2 * shadowOffset, -mCornerRadius + mShadowSize,
- mEdgeShadowPaint);
- }
- canvas.restoreToCount(saved);
- // LB
- saved = canvas.save();
- canvas.translate(mContentBounds.left + shadowOffset, mContentBounds.bottom - shadowOffset);
- canvas.scale(shadowScaleHorizontal, shadowScaleBottom);
- canvas.rotate(270f);
- canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
- if (drawVerticalEdges) {
- // LE
- canvas.scale(1f / shadowScaleBottom, 1f);
- canvas.drawRect(0, edgeShadowTop,
- mContentBounds.height() - 2 * shadowOffset, -mCornerRadius, mEdgeShadowPaint);
- }
- canvas.restoreToCount(saved);
- // RT
- saved = canvas.save();
- canvas.translate(mContentBounds.right - shadowOffset, mContentBounds.top + shadowOffset);
- canvas.scale(shadowScaleHorizontal, shadowScaleTop);
- canvas.rotate(90f);
- canvas.drawPath(mCornerShadowPath, mCornerShadowPaint);
- if (drawVerticalEdges) {
- // RE
- canvas.scale(1f / shadowScaleTop, 1f);
- canvas.drawRect(0, edgeShadowTop,
- mContentBounds.height() - 2 * shadowOffset, -mCornerRadius, mEdgeShadowPaint);
- }
- canvas.restoreToCount(saved);
-
- canvas.restoreToCount(rotateSaved);
- }
-
- private void buildShadowCorners() {
- RectF innerBounds = new RectF(-mCornerRadius, -mCornerRadius, mCornerRadius, mCornerRadius);
- RectF outerBounds = new RectF(innerBounds);
- outerBounds.inset(-mShadowSize, -mShadowSize);
-
- if (mCornerShadowPath == null) {
- mCornerShadowPath = new Path();
- } else {
- mCornerShadowPath.reset();
- }
- mCornerShadowPath.setFillType(Path.FillType.EVEN_ODD);
- mCornerShadowPath.moveTo(-mCornerRadius, 0);
- mCornerShadowPath.rLineTo(-mShadowSize, 0);
- // outer arc
- mCornerShadowPath.arcTo(outerBounds, 180f, 90f, false);
- // inner arc
- mCornerShadowPath.arcTo(innerBounds, 270f, -90f, false);
- mCornerShadowPath.close();
-
- float shadowRadius = -outerBounds.top;
- if (shadowRadius > 0f) {
- float startRatio = mCornerRadius / shadowRadius;
- float midRatio = startRatio + ((1f - startRatio) / 2f);
- mCornerShadowPaint.setShader(new RadialGradient(0, 0, shadowRadius,
- new int[]{0, mShadowStartColor, mShadowMiddleColor, mShadowEndColor},
- new float[]{0f, startRatio, midRatio, 1f},
- Shader.TileMode.CLAMP));
- }
-
- // we offset the content shadowSize/2 pixels up to make it more realistic.
- // this is why edge shadow shader has some extra space
- // When drawing bottom edge shadow, we use that extra space.
- mEdgeShadowPaint.setShader(new LinearGradient(0, innerBounds.top, 0, outerBounds.top,
- new int[]{mShadowStartColor, mShadowMiddleColor, mShadowEndColor},
- new float[]{0f, .5f, 1f}, Shader.TileMode.CLAMP));
- mEdgeShadowPaint.setAntiAlias(false);
- }
-
- private void buildComponents(Rect bounds) {
- // Card is offset SHADOW_MULTIPLIER * maxShadowSize to account for the shadow shift.
- // We could have different top-bottom offsets to avoid extra gap above but in that case
- // center aligning Views inside the CardView would be problematic.
- final float verticalOffset = mRawMaxShadowSize * SHADOW_MULTIPLIER;
- mContentBounds.set(bounds.left + mRawMaxShadowSize, bounds.top + verticalOffset,
- bounds.right - mRawMaxShadowSize, bounds.bottom - verticalOffset);
-
- getWrappedDrawable().setBounds((int) mContentBounds.left, (int) mContentBounds.top,
- (int) mContentBounds.right, (int) mContentBounds.bottom);
-
- buildShadowCorners();
- }
-
- public float getCornerRadius() {
- return mCornerRadius;
- }
-
- public void setShadowSize(float size) {
- setShadowSize(size, mRawMaxShadowSize);
- }
-
- public void setMaxShadowSize(float size) {
- setShadowSize(mRawShadowSize, size);
- }
-
- public float getShadowSize() {
- return mRawShadowSize;
- }
-
- public float getMaxShadowSize() {
- return mRawMaxShadowSize;
- }
-
- public float getMinWidth() {
- final float content = 2 *
- Math.max(mRawMaxShadowSize, mCornerRadius + mRawMaxShadowSize / 2);
- return content + mRawMaxShadowSize * 2;
- }
-
- public float getMinHeight() {
- final float content = 2 * Math.max(mRawMaxShadowSize, mCornerRadius
- + mRawMaxShadowSize * SHADOW_MULTIPLIER / 2);
- return content + (mRawMaxShadowSize * SHADOW_MULTIPLIER) * 2;
- }
-}
diff --git a/android/support/design/widget/Snackbar.java b/android/support/design/widget/Snackbar.java
deleted file mode 100644
index bd5ffbab..00000000
--- a/android/support/design/widget/Snackbar.java
+++ /dev/null
@@ -1,353 +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 android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.support.annotation.ColorInt;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.StringRes;
-import android.support.design.R;
-import android.support.design.internal.SnackbarContentLayout;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.widget.FrameLayout;
-import android.widget.TextView;
-
-/**
- * Snackbars provide lightweight feedback about an operation. They show a brief message at the
- * bottom of the screen on mobile and lower left on larger devices. Snackbars appear above all other
- * elements on screen and only one can be displayed at a time.
- * <p>
- * They automatically disappear after a timeout or after user interaction elsewhere on the screen,
- * particularly after interactions that summon a new surface or activity. Snackbars can be swiped
- * off screen.
- * <p>
- * Snackbars can contain an action which is set via
- * {@link #setAction(CharSequence, android.view.View.OnClickListener)}.
- * <p>
- * To be notified when a snackbar has been shown or dismissed, you can provide a {@link Callback}
- * via {@link BaseTransientBottomBar#addCallback(BaseCallback)}.</p>
- */
-public final class Snackbar extends BaseTransientBottomBar<Snackbar> {
-
- /**
- * Show the Snackbar indefinitely. This means that the Snackbar will be displayed from the time
- * that is {@link #show() shown} until either it is dismissed, or another Snackbar is shown.
- *
- * @see #setDuration
- */
- public static final int LENGTH_INDEFINITE = BaseTransientBottomBar.LENGTH_INDEFINITE;
-
- /**
- * Show the Snackbar for a short period of time.
- *
- * @see #setDuration
- */
- public static final int LENGTH_SHORT = BaseTransientBottomBar.LENGTH_SHORT;
-
- /**
- * Show the Snackbar for a long period of time.
- *
- * @see #setDuration
- */
- public static final int LENGTH_LONG = BaseTransientBottomBar.LENGTH_LONG;
-
- /**
- * Callback class for {@link Snackbar} instances.
- *
- * Note: this class is here to provide backwards-compatible way for apps written before
- * the existence of the base {@link BaseTransientBottomBar} class.
- *
- * @see BaseTransientBottomBar#addCallback(BaseCallback)
- */
- public static class Callback extends BaseCallback<Snackbar> {
- /** Indicates that the Snackbar was dismissed via a swipe.*/
- public static final int DISMISS_EVENT_SWIPE = BaseCallback.DISMISS_EVENT_SWIPE;
- /** Indicates that the Snackbar was dismissed via an action click.*/
- public static final int DISMISS_EVENT_ACTION = BaseCallback.DISMISS_EVENT_ACTION;
- /** Indicates that the Snackbar was dismissed via a timeout.*/
- public static final int DISMISS_EVENT_TIMEOUT = BaseCallback.DISMISS_EVENT_TIMEOUT;
- /** Indicates that the Snackbar was dismissed via a call to {@link #dismiss()}.*/
- public static final int DISMISS_EVENT_MANUAL = BaseCallback.DISMISS_EVENT_MANUAL;
- /** Indicates that the Snackbar was dismissed from a new Snackbar being shown.*/
- public static final int DISMISS_EVENT_CONSECUTIVE = BaseCallback.DISMISS_EVENT_CONSECUTIVE;
-
- @Override
- public void onShown(Snackbar sb) {
- // Stub implementation to make API check happy.
- }
-
- @Override
- public void onDismissed(Snackbar transientBottomBar, @DismissEvent int event) {
- // Stub implementation to make API check happy.
- }
- }
-
- @Nullable private BaseCallback<Snackbar> mCallback;
-
- private Snackbar(ViewGroup parent, View content, ContentViewCallback contentViewCallback) {
- super(parent, content, contentViewCallback);
- }
-
- /**
- * Make a Snackbar to display a message
- *
- * <p>Snackbar will try and find a parent view to hold Snackbar's view from the value given
- * to {@code view}. Snackbar will walk up the view tree trying to find a suitable parent,
- * which is defined as a {@link CoordinatorLayout} or the window decor's content view,
- * whichever comes first.
- *
- * <p>Having a {@link CoordinatorLayout} in your view hierarchy allows Snackbar to enable
- * certain features, such as swipe-to-dismiss and automatically moving of widgets like
- * {@link FloatingActionButton}.
- *
- * @param view The view to find a parent from.
- * @param text The text to show. Can be formatted text.
- * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link
- * #LENGTH_LONG}
- */
- @NonNull
- public static Snackbar make(@NonNull View view, @NonNull CharSequence text,
- @Duration int duration) {
- final ViewGroup parent = findSuitableParent(view);
- if (parent == null) {
- throw new IllegalArgumentException("No suitable parent found from the given view. "
- + "Please provide a valid view.");
- }
-
- final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
- final SnackbarContentLayout content =
- (SnackbarContentLayout) inflater.inflate(
- R.layout.design_layout_snackbar_include, parent, false);
- final Snackbar snackbar = new Snackbar(parent, content, content);
- snackbar.setText(text);
- snackbar.setDuration(duration);
- return snackbar;
- }
-
- /**
- * Make a Snackbar to display a message.
- *
- * <p>Snackbar will try and find a parent view to hold Snackbar's view from the value given
- * to {@code view}. Snackbar will walk up the view tree trying to find a suitable parent,
- * which is defined as a {@link CoordinatorLayout} or the window decor's content view,
- * whichever comes first.
- *
- * <p>Having a {@link CoordinatorLayout} in your view hierarchy allows Snackbar to enable
- * certain features, such as swipe-to-dismiss and automatically moving of widgets like
- * {@link FloatingActionButton}.
- *
- * @param view The view to find a parent from.
- * @param resId The resource id of the string resource to use. Can be formatted text.
- * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or {@link
- * #LENGTH_LONG}
- */
- @NonNull
- public static Snackbar make(@NonNull View view, @StringRes int resId, @Duration int duration) {
- return make(view, view.getResources().getText(resId), duration);
- }
-
- private static ViewGroup findSuitableParent(View view) {
- ViewGroup fallback = null;
- do {
- if (view instanceof CoordinatorLayout) {
- // We've found a CoordinatorLayout, use it
- return (ViewGroup) view;
- } else if (view instanceof FrameLayout) {
- if (view.getId() == android.R.id.content) {
- // If we've hit the decor content view, then we didn't find a CoL in the
- // hierarchy, so use it.
- return (ViewGroup) view;
- } else {
- // It's not the content view but we'll use it as our fallback
- fallback = (ViewGroup) view;
- }
- }
-
- if (view != null) {
- // Else, we will loop and crawl up the view hierarchy and try to find a parent
- final ViewParent parent = view.getParent();
- view = parent instanceof View ? (View) parent : null;
- }
- } while (view != null);
-
- // If we reach here then we didn't find a CoL or a suitable content view so we'll fallback
- return fallback;
- }
-
- /**
- * Update the text in this {@link Snackbar}.
- *
- * @param message The new text for this {@link BaseTransientBottomBar}.
- */
- @NonNull
- public Snackbar setText(@NonNull CharSequence message) {
- final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
- final TextView tv = contentLayout.getMessageView();
- tv.setText(message);
- return this;
- }
-
- /**
- * Update the text in this {@link Snackbar}.
- *
- * @param resId The new text for this {@link BaseTransientBottomBar}.
- */
- @NonNull
- public Snackbar setText(@StringRes int resId) {
- return setText(getContext().getText(resId));
- }
-
- /**
- * Set the action to be displayed in this {@link BaseTransientBottomBar}.
- *
- * @param resId String resource to display for the action
- * @param listener callback to be invoked when the action is clicked
- */
- @NonNull
- public Snackbar setAction(@StringRes int resId, View.OnClickListener listener) {
- return setAction(getContext().getText(resId), listener);
- }
-
- /**
- * Set the action to be displayed in this {@link BaseTransientBottomBar}.
- *
- * @param text Text to display for the action
- * @param listener callback to be invoked when the action is clicked
- */
- @NonNull
- public Snackbar setAction(CharSequence text, final View.OnClickListener listener) {
- final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
- final TextView tv = contentLayout.getActionView();
-
- if (TextUtils.isEmpty(text) || listener == null) {
- tv.setVisibility(View.GONE);
- tv.setOnClickListener(null);
- } else {
- tv.setVisibility(View.VISIBLE);
- tv.setText(text);
- tv.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- listener.onClick(view);
- // Now dismiss the Snackbar
- dispatchDismiss(BaseCallback.DISMISS_EVENT_ACTION);
- }
- });
- }
- return this;
- }
-
- /**
- * Sets the text color of the action specified in
- * {@link #setAction(CharSequence, View.OnClickListener)}.
- */
- @NonNull
- public Snackbar setActionTextColor(ColorStateList colors) {
- final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
- final TextView tv = contentLayout.getActionView();
- tv.setTextColor(colors);
- return this;
- }
-
- /**
- * Sets the text color of the action specified in
- * {@link #setAction(CharSequence, View.OnClickListener)}.
- */
- @NonNull
- public Snackbar setActionTextColor(@ColorInt int color) {
- final SnackbarContentLayout contentLayout = (SnackbarContentLayout) mView.getChildAt(0);
- final TextView tv = contentLayout.getActionView();
- tv.setTextColor(color);
- return this;
- }
-
- /**
- * Set a callback to be called when this the visibility of this {@link Snackbar}
- * changes. Note that this method is deprecated
- * and you should use {@link #addCallback(BaseCallback)} to add a callback and
- * {@link #removeCallback(BaseCallback)} to remove a registered callback.
- *
- * @param callback Callback to notify when transient bottom bar events occur.
- * @deprecated Use {@link #addCallback(BaseCallback)}
- * @see Callback
- * @see #addCallback(BaseCallback)
- * @see #removeCallback(BaseCallback)
- */
- @Deprecated
- @NonNull
- public Snackbar setCallback(Callback callback) {
- // The logic in this method emulates what we had before support for multiple
- // registered callbacks.
- if (mCallback != null) {
- removeCallback(mCallback);
- }
- if (callback != null) {
- addCallback(callback);
- }
- // Update the deprecated field so that we can remove the passed callback the next
- // time we're called
- mCallback = callback;
- return this;
- }
-
- /**
- * @hide
- *
- * Note: this class is here to provide backwards-compatible way for apps written before
- * the existence of the base {@link BaseTransientBottomBar} class.
- */
- @RestrictTo(LIBRARY_GROUP)
- public static final class SnackbarLayout extends BaseTransientBottomBar.SnackbarBaseLayout {
- public SnackbarLayout(Context context) {
- super(context);
- }
-
- public SnackbarLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- // Work around our backwards-compatible refactoring of Snackbar and inner content
- // being inflated against snackbar's parent (instead of against the snackbar itself).
- // Every child that is width=MATCH_PARENT is remeasured again and given the full width
- // minus the paddings.
- int childCount = getChildCount();
- int availableWidth = getMeasuredWidth() - getPaddingLeft() - getPaddingRight();
- for (int i = 0; i < childCount; i++) {
- View child = getChildAt(i);
- if (child.getLayoutParams().width == ViewGroup.LayoutParams.MATCH_PARENT) {
- child.measure(MeasureSpec.makeMeasureSpec(availableWidth, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(),
- MeasureSpec.EXACTLY));
- }
- }
- }
- }
-}
-
diff --git a/android/support/design/widget/SnackbarManager.java b/android/support/design/widget/SnackbarManager.java
deleted file mode 100644
index 43892d36..00000000
--- a/android/support/design/widget/SnackbarManager.java
+++ /dev/null
@@ -1,243 +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 android.support.design.widget;
-
-import android.os.Handler;
-import android.os.Looper;
-import android.os.Message;
-
-import java.lang.ref.WeakReference;
-
-/**
- * Manages {@link Snackbar}s.
- */
-class SnackbarManager {
-
- static final int MSG_TIMEOUT = 0;
-
- private static final int SHORT_DURATION_MS = 1500;
- private static final int LONG_DURATION_MS = 2750;
-
- private static SnackbarManager sSnackbarManager;
-
- static SnackbarManager getInstance() {
- if (sSnackbarManager == null) {
- sSnackbarManager = new SnackbarManager();
- }
- return sSnackbarManager;
- }
-
- private final Object mLock;
- private final Handler mHandler;
-
- private SnackbarRecord mCurrentSnackbar;
- private SnackbarRecord mNextSnackbar;
-
- private SnackbarManager() {
- mLock = new Object();
- mHandler = new Handler(Looper.getMainLooper(), new Handler.Callback() {
- @Override
- public boolean handleMessage(Message message) {
- switch (message.what) {
- case MSG_TIMEOUT:
- handleTimeout((SnackbarRecord) message.obj);
- return true;
- }
- return false;
- }
- });
- }
-
- interface Callback {
- void show();
- void dismiss(int event);
- }
-
- public void show(int duration, Callback callback) {
- synchronized (mLock) {
- if (isCurrentSnackbarLocked(callback)) {
- // Means that the callback is already in the queue. We'll just update the duration
- mCurrentSnackbar.duration = duration;
-
- // If this is the Snackbar currently being shown, call re-schedule it's
- // timeout
- mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
- scheduleTimeoutLocked(mCurrentSnackbar);
- return;
- } else if (isNextSnackbarLocked(callback)) {
- // We'll just update the duration
- mNextSnackbar.duration = duration;
- } else {
- // Else, we need to create a new record and queue it
- mNextSnackbar = new SnackbarRecord(duration, callback);
- }
-
- if (mCurrentSnackbar != null && cancelSnackbarLocked(mCurrentSnackbar,
- Snackbar.Callback.DISMISS_EVENT_CONSECUTIVE)) {
- // If we currently have a Snackbar, try and cancel it and wait in line
- return;
- } else {
- // Clear out the current snackbar
- mCurrentSnackbar = null;
- // Otherwise, just show it now
- showNextSnackbarLocked();
- }
- }
- }
-
- public void dismiss(Callback callback, int event) {
- synchronized (mLock) {
- if (isCurrentSnackbarLocked(callback)) {
- cancelSnackbarLocked(mCurrentSnackbar, event);
- } else if (isNextSnackbarLocked(callback)) {
- cancelSnackbarLocked(mNextSnackbar, event);
- }
- }
- }
-
- /**
- * Should be called when a Snackbar is no longer displayed. This is after any exit
- * animation has finished.
- */
- public void onDismissed(Callback callback) {
- synchronized (mLock) {
- if (isCurrentSnackbarLocked(callback)) {
- // If the callback is from a Snackbar currently show, remove it and show a new one
- mCurrentSnackbar = null;
- if (mNextSnackbar != null) {
- showNextSnackbarLocked();
- }
- }
- }
- }
-
- /**
- * Should be called when a Snackbar is being shown. This is after any entrance animation has
- * finished.
- */
- public void onShown(Callback callback) {
- synchronized (mLock) {
- if (isCurrentSnackbarLocked(callback)) {
- scheduleTimeoutLocked(mCurrentSnackbar);
- }
- }
- }
-
- public void pauseTimeout(Callback callback) {
- synchronized (mLock) {
- if (isCurrentSnackbarLocked(callback) && !mCurrentSnackbar.paused) {
- mCurrentSnackbar.paused = true;
- mHandler.removeCallbacksAndMessages(mCurrentSnackbar);
- }
- }
- }
-
- public void restoreTimeoutIfPaused(Callback callback) {
- synchronized (mLock) {
- if (isCurrentSnackbarLocked(callback) && mCurrentSnackbar.paused) {
- mCurrentSnackbar.paused = false;
- scheduleTimeoutLocked(mCurrentSnackbar);
- }
- }
- }
-
- public boolean isCurrent(Callback callback) {
- synchronized (mLock) {
- return isCurrentSnackbarLocked(callback);
- }
- }
-
- public boolean isCurrentOrNext(Callback callback) {
- synchronized (mLock) {
- return isCurrentSnackbarLocked(callback) || isNextSnackbarLocked(callback);
- }
- }
-
- private static class SnackbarRecord {
- final WeakReference<Callback> callback;
- int duration;
- boolean paused;
-
- SnackbarRecord(int duration, Callback callback) {
- this.callback = new WeakReference<>(callback);
- this.duration = duration;
- }
-
- boolean isSnackbar(Callback callback) {
- return callback != null && this.callback.get() == callback;
- }
- }
-
- private void showNextSnackbarLocked() {
- if (mNextSnackbar != null) {
- mCurrentSnackbar = mNextSnackbar;
- mNextSnackbar = null;
-
- final Callback callback = mCurrentSnackbar.callback.get();
- if (callback != null) {
- callback.show();
- } else {
- // The callback doesn't exist any more, clear out the Snackbar
- mCurrentSnackbar = null;
- }
- }
- }
-
- private boolean cancelSnackbarLocked(SnackbarRecord record, int event) {
- final Callback callback = record.callback.get();
- if (callback != null) {
- // Make sure we remove any timeouts for the SnackbarRecord
- mHandler.removeCallbacksAndMessages(record);
- callback.dismiss(event);
- return true;
- }
- return false;
- }
-
- private boolean isCurrentSnackbarLocked(Callback callback) {
- return mCurrentSnackbar != null && mCurrentSnackbar.isSnackbar(callback);
- }
-
- private boolean isNextSnackbarLocked(Callback callback) {
- return mNextSnackbar != null && mNextSnackbar.isSnackbar(callback);
- }
-
- private void scheduleTimeoutLocked(SnackbarRecord r) {
- if (r.duration == Snackbar.LENGTH_INDEFINITE) {
- // If we're set to indefinite, we don't want to set a timeout
- return;
- }
-
- int durationMs = LONG_DURATION_MS;
- if (r.duration > 0) {
- durationMs = r.duration;
- } else if (r.duration == Snackbar.LENGTH_SHORT) {
- durationMs = SHORT_DURATION_MS;
- }
- mHandler.removeCallbacksAndMessages(r);
- mHandler.sendMessageDelayed(Message.obtain(mHandler, MSG_TIMEOUT, r), durationMs);
- }
-
- void handleTimeout(SnackbarRecord record) {
- synchronized (mLock) {
- if (mCurrentSnackbar == record || mNextSnackbar == record) {
- cancelSnackbarLocked(record, Snackbar.Callback.DISMISS_EVENT_TIMEOUT);
- }
- }
- }
-
-}
diff --git a/android/support/design/widget/StateListAnimator.java b/android/support/design/widget/StateListAnimator.java
deleted file mode 100644
index aef24be3..00000000
--- a/android/support/design/widget/StateListAnimator.java
+++ /dev/null
@@ -1,116 +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 android.support.design.widget;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.util.StateSet;
-
-import java.util.ArrayList;
-
-final class StateListAnimator {
-
- private final ArrayList<Tuple> mTuples = new ArrayList<>();
-
- private Tuple mLastMatch = null;
- ValueAnimator mRunningAnimator = null;
-
- private final ValueAnimator.AnimatorListener mAnimationListener =
- new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- if (mRunningAnimator == animator) {
- mRunningAnimator = null;
- }
- }
- };
-
- /**
- * Associates the given Animation with the provided drawable state specs so that it will be run
- * when the View's drawable state matches the specs.
- *
- * @param specs The drawable state specs to match against
- * @param animator The animator to run when the specs match
- */
- public void addState(int[] specs, ValueAnimator animator) {
- Tuple tuple = new Tuple(specs, animator);
- animator.addListener(mAnimationListener);
- mTuples.add(tuple);
- }
-
- /**
- * Called by View
- */
- void setState(int[] state) {
- Tuple match = null;
- final int count = mTuples.size();
- for (int i = 0; i < count; i++) {
- final Tuple tuple = mTuples.get(i);
- if (StateSet.stateSetMatches(tuple.mSpecs, state)) {
- match = tuple;
- break;
- }
- }
- if (match == mLastMatch) {
- return;
- }
- if (mLastMatch != null) {
- cancel();
- }
-
- mLastMatch = match;
-
- if (match != null) {
- start(match);
- }
- }
-
- private void start(Tuple match) {
- mRunningAnimator = match.mAnimator;
- mRunningAnimator.start();
- }
-
- private void cancel() {
- if (mRunningAnimator != null) {
- mRunningAnimator.cancel();
- mRunningAnimator = null;
- }
- }
-
- /**
- * If there is an animation running for a recent state change, ends it.
- *
- * <p>This causes the animation to assign the end value(s) to the View.</p>
- */
- public void jumpToCurrentState() {
- if (mRunningAnimator != null) {
- mRunningAnimator.end();
- mRunningAnimator = null;
- }
- }
-
- static class Tuple {
- final int[] mSpecs;
- final ValueAnimator mAnimator;
-
- Tuple(int[] specs, ValueAnimator animator) {
- mSpecs = specs;
- mAnimator = animator;
- }
- }
-} \ No newline at end of file
diff --git a/android/support/design/widget/SwipeDismissBehavior.java b/android/support/design/widget/SwipeDismissBehavior.java
deleted file mode 100644
index d8573340..00000000
--- a/android/support/design/widget/SwipeDismissBehavior.java
+++ /dev/null
@@ -1,411 +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 android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.support.annotation.IntDef;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.widget.ViewDragHelper;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-
-/**
- * An interaction behavior plugin for child views of {@link CoordinatorLayout} to provide support
- * for the 'swipe-to-dismiss' gesture.
- */
-public class SwipeDismissBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
-
- /**
- * A view is not currently being dragged or animating as a result of a fling/snap.
- */
- public static final int STATE_IDLE = ViewDragHelper.STATE_IDLE;
-
- /**
- * A view is currently being dragged. The position is currently changing as a result
- * of user input or simulated user input.
- */
- public static final int STATE_DRAGGING = ViewDragHelper.STATE_DRAGGING;
-
- /**
- * A view is currently settling into place as a result of a fling or
- * predefined non-interactive motion.
- */
- public static final int STATE_SETTLING = ViewDragHelper.STATE_SETTLING;
-
- /** @hide */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef({SWIPE_DIRECTION_START_TO_END, SWIPE_DIRECTION_END_TO_START, SWIPE_DIRECTION_ANY})
- @Retention(RetentionPolicy.SOURCE)
- private @interface SwipeDirection {}
-
- /**
- * Swipe direction that only allows swiping in the direction of start-to-end. That is
- * left-to-right in LTR, or right-to-left in RTL.
- */
- public static final int SWIPE_DIRECTION_START_TO_END = 0;
-
- /**
- * Swipe direction that only allows swiping in the direction of end-to-start. That is
- * right-to-left in LTR or left-to-right in RTL.
- */
- public static final int SWIPE_DIRECTION_END_TO_START = 1;
-
- /**
- * Swipe direction which allows swiping in either direction.
- */
- public static final int SWIPE_DIRECTION_ANY = 2;
-
- private static final float DEFAULT_DRAG_DISMISS_THRESHOLD = 0.5f;
- private static final float DEFAULT_ALPHA_START_DISTANCE = 0f;
- private static final float DEFAULT_ALPHA_END_DISTANCE = DEFAULT_DRAG_DISMISS_THRESHOLD;
-
- ViewDragHelper mViewDragHelper;
- OnDismissListener mListener;
- private boolean mInterceptingEvents;
-
- private float mSensitivity = 0f;
- private boolean mSensitivitySet;
-
- int mSwipeDirection = SWIPE_DIRECTION_ANY;
- float mDragDismissThreshold = DEFAULT_DRAG_DISMISS_THRESHOLD;
- float mAlphaStartSwipeDistance = DEFAULT_ALPHA_START_DISTANCE;
- float mAlphaEndSwipeDistance = DEFAULT_ALPHA_END_DISTANCE;
-
- /**
- * Callback interface used to notify the application that the view has been dismissed.
- */
- public interface OnDismissListener {
- /**
- * Called when {@code view} has been dismissed via swiping.
- */
- public void onDismiss(View view);
-
- /**
- * Called when the drag state has changed.
- *
- * @param state the new state. One of
- * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
- */
- public void onDragStateChanged(int state);
- }
-
- /**
- * Set the listener to be used when a dismiss event occurs.
- *
- * @param listener the listener to use.
- */
- public void setListener(OnDismissListener listener) {
- mListener = listener;
- }
-
- /**
- * Sets the swipe direction for this behavior.
- *
- * @param direction one of the {@link #SWIPE_DIRECTION_START_TO_END},
- * {@link #SWIPE_DIRECTION_END_TO_START} or {@link #SWIPE_DIRECTION_ANY}
- */
- public void setSwipeDirection(@SwipeDirection int direction) {
- mSwipeDirection = direction;
- }
-
- /**
- * Set the threshold for telling if a view has been dragged enough to be dismissed.
- *
- * @param distance a ratio of a view's width, values are clamped to 0 >= x <= 1f;
- */
- public void setDragDismissDistance(float distance) {
- mDragDismissThreshold = clamp(0f, distance, 1f);
- }
-
- /**
- * The minimum swipe distance before the view's alpha is modified.
- *
- * @param fraction the distance as a fraction of the view's width.
- */
- public void setStartAlphaSwipeDistance(float fraction) {
- mAlphaStartSwipeDistance = clamp(0f, fraction, 1f);
- }
-
- /**
- * The maximum swipe distance for the view's alpha is modified.
- *
- * @param fraction the distance as a fraction of the view's width.
- */
- public void setEndAlphaSwipeDistance(float fraction) {
- mAlphaEndSwipeDistance = clamp(0f, fraction, 1f);
- }
-
- /**
- * Set the sensitivity used for detecting the start of a swipe. This only takes effect if
- * no touch handling has occured yet.
- *
- * @param sensitivity Multiplier for how sensitive we should be about detecting
- * the start of a drag. Larger values are more sensitive. 1.0f is normal.
- */
- public void setSensitivity(float sensitivity) {
- mSensitivity = sensitivity;
- mSensitivitySet = true;
- }
-
- @Override
- public boolean onInterceptTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
- boolean dispatchEventToHelper = mInterceptingEvents;
-
- switch (event.getActionMasked()) {
- case MotionEvent.ACTION_DOWN:
- mInterceptingEvents = parent.isPointInChildBounds(child,
- (int) event.getX(), (int) event.getY());
- dispatchEventToHelper = mInterceptingEvents;
- break;
- case MotionEvent.ACTION_UP:
- case MotionEvent.ACTION_CANCEL:
- // Reset the ignore flag for next time
- mInterceptingEvents = false;
- break;
- }
-
- if (dispatchEventToHelper) {
- ensureViewDragHelper(parent);
- return mViewDragHelper.shouldInterceptTouchEvent(event);
- }
- return false;
- }
-
- @Override
- public boolean onTouchEvent(CoordinatorLayout parent, V child, MotionEvent event) {
- if (mViewDragHelper != null) {
- mViewDragHelper.processTouchEvent(event);
- return true;
- }
- return false;
- }
-
- /**
- * Called when the user's input indicates that they want to swipe the given view.
- *
- * @param view View the user is attempting to swipe
- * @return true if the view can be dismissed via swiping, false otherwise
- */
- public boolean canSwipeDismissView(@NonNull View view) {
- return true;
- }
-
- private final ViewDragHelper.Callback mDragCallback = new ViewDragHelper.Callback() {
- private static final int INVALID_POINTER_ID = -1;
-
- private int mOriginalCapturedViewLeft;
- private int mActivePointerId = INVALID_POINTER_ID;
-
- @Override
- public boolean tryCaptureView(View child, int pointerId) {
- // Only capture if we don't already have an active pointer id
- return mActivePointerId == INVALID_POINTER_ID && canSwipeDismissView(child);
- }
-
- @Override
- public void onViewCaptured(View capturedChild, int activePointerId) {
- mActivePointerId = activePointerId;
- mOriginalCapturedViewLeft = capturedChild.getLeft();
-
- // The view has been captured, and thus a drag is about to start so stop any parents
- // intercepting
- final ViewParent parent = capturedChild.getParent();
- if (parent != null) {
- parent.requestDisallowInterceptTouchEvent(true);
- }
- }
-
- @Override
- public void onViewDragStateChanged(int state) {
- if (mListener != null) {
- mListener.onDragStateChanged(state);
- }
- }
-
- @Override
- public void onViewReleased(View child, float xvel, float yvel) {
- // Reset the active pointer ID
- mActivePointerId = INVALID_POINTER_ID;
-
- final int childWidth = child.getWidth();
- int targetLeft;
- boolean dismiss = false;
-
- if (shouldDismiss(child, xvel)) {
- targetLeft = child.getLeft() < mOriginalCapturedViewLeft
- ? mOriginalCapturedViewLeft - childWidth
- : mOriginalCapturedViewLeft + childWidth;
- dismiss = true;
- } else {
- // Else, reset back to the original left
- targetLeft = mOriginalCapturedViewLeft;
- }
-
- if (mViewDragHelper.settleCapturedViewAt(targetLeft, child.getTop())) {
- ViewCompat.postOnAnimation(child, new SettleRunnable(child, dismiss));
- } else if (dismiss && mListener != null) {
- mListener.onDismiss(child);
- }
- }
-
- private boolean shouldDismiss(View child, float xvel) {
- if (xvel != 0f) {
- final boolean isRtl = ViewCompat.getLayoutDirection(child)
- == ViewCompat.LAYOUT_DIRECTION_RTL;
-
- if (mSwipeDirection == SWIPE_DIRECTION_ANY) {
- // We don't care about the direction so return true
- return true;
- } else if (mSwipeDirection == SWIPE_DIRECTION_START_TO_END) {
- // We only allow start-to-end swiping, so the fling needs to be in the
- // correct direction
- return isRtl ? xvel < 0f : xvel > 0f;
- } else if (mSwipeDirection == SWIPE_DIRECTION_END_TO_START) {
- // We only allow end-to-start swiping, so the fling needs to be in the
- // correct direction
- return isRtl ? xvel > 0f : xvel < 0f;
- }
- } else {
- final int distance = child.getLeft() - mOriginalCapturedViewLeft;
- final int thresholdDistance = Math.round(child.getWidth() * mDragDismissThreshold);
- return Math.abs(distance) >= thresholdDistance;
- }
-
- return false;
- }
-
- @Override
- public int getViewHorizontalDragRange(View child) {
- return child.getWidth();
- }
-
- @Override
- public int clampViewPositionHorizontal(View child, int left, int dx) {
- final boolean isRtl = ViewCompat.getLayoutDirection(child)
- == ViewCompat.LAYOUT_DIRECTION_RTL;
- int min, max;
-
- if (mSwipeDirection == SWIPE_DIRECTION_START_TO_END) {
- if (isRtl) {
- min = mOriginalCapturedViewLeft - child.getWidth();
- max = mOriginalCapturedViewLeft;
- } else {
- min = mOriginalCapturedViewLeft;
- max = mOriginalCapturedViewLeft + child.getWidth();
- }
- } else if (mSwipeDirection == SWIPE_DIRECTION_END_TO_START) {
- if (isRtl) {
- min = mOriginalCapturedViewLeft;
- max = mOriginalCapturedViewLeft + child.getWidth();
- } else {
- min = mOriginalCapturedViewLeft - child.getWidth();
- max = mOriginalCapturedViewLeft;
- }
- } else {
- min = mOriginalCapturedViewLeft - child.getWidth();
- max = mOriginalCapturedViewLeft + child.getWidth();
- }
-
- return clamp(min, left, max);
- }
-
- @Override
- public int clampViewPositionVertical(View child, int top, int dy) {
- return child.getTop();
- }
-
- @Override
- public void onViewPositionChanged(View child, int left, int top, int dx, int dy) {
- final float startAlphaDistance = mOriginalCapturedViewLeft
- + child.getWidth() * mAlphaStartSwipeDistance;
- final float endAlphaDistance = mOriginalCapturedViewLeft
- + child.getWidth() * mAlphaEndSwipeDistance;
-
- if (left <= startAlphaDistance) {
- child.setAlpha(1f);
- } else if (left >= endAlphaDistance) {
- child.setAlpha(0f);
- } else {
- // We're between the start and end distances
- final float distance = fraction(startAlphaDistance, endAlphaDistance, left);
- child.setAlpha(clamp(0f, 1f - distance, 1f));
- }
- }
- };
-
- private void ensureViewDragHelper(ViewGroup parent) {
- if (mViewDragHelper == null) {
- mViewDragHelper = mSensitivitySet
- ? ViewDragHelper.create(parent, mSensitivity, mDragCallback)
- : ViewDragHelper.create(parent, mDragCallback);
- }
- }
-
- private class SettleRunnable implements Runnable {
- private final View mView;
- private final boolean mDismiss;
-
- SettleRunnable(View view, boolean dismiss) {
- mView = view;
- mDismiss = dismiss;
- }
-
- @Override
- public void run() {
- if (mViewDragHelper != null && mViewDragHelper.continueSettling(true)) {
- ViewCompat.postOnAnimation(mView, this);
- } else {
- if (mDismiss && mListener != null) {
- mListener.onDismiss(mView);
- }
- }
- }
- }
-
- static float clamp(float min, float value, float max) {
- return Math.min(Math.max(min, value), max);
- }
-
- static int clamp(int min, int value, int max) {
- return Math.min(Math.max(min, value), max);
- }
-
- /**
- * Retrieve the current drag state of this behavior. This will return one of
- * {@link #STATE_IDLE}, {@link #STATE_DRAGGING} or {@link #STATE_SETTLING}.
- *
- * @return The current drag state
- */
- public int getDragState() {
- return mViewDragHelper != null ? mViewDragHelper.getViewDragState() : STATE_IDLE;
- }
-
- /**
- * The fraction that {@code value} is between {@code startValue} and {@code endValue}.
- */
- static float fraction(float startValue, float endValue, float value) {
- return (value - startValue) / (endValue - startValue);
- }
-} \ No newline at end of file
diff --git a/android/support/design/widget/TabItem.java b/android/support/design/widget/TabItem.java
deleted file mode 100644
index 09b01dbe..00000000
--- a/android/support/design/widget/TabItem.java
+++ /dev/null
@@ -1,57 +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.support.design.widget;
-
-import android.content.Context;
-import android.graphics.drawable.Drawable;
-import android.support.design.R;
-import android.support.v7.widget.TintTypedArray;
-import android.util.AttributeSet;
-import android.view.View;
-
-/**
- * TabItem is a special 'view' which allows you to declare tab items for a {@link TabLayout}
- * within a layout. This view is not actually added to TabLayout, it is just a dummy which allows
- * setting of a tab items's text, icon and custom layout. See TabLayout for more information on how
- * to use it.
- *
- * @attr ref android.support.design.R.styleable#TabItem_android_icon
- * @attr ref android.support.design.R.styleable#TabItem_android_text
- * @attr ref android.support.design.R.styleable#TabItem_android_layout
- *
- * @see TabLayout
- */
-public final class TabItem extends View {
- final CharSequence mText;
- final Drawable mIcon;
- final int mCustomLayout;
-
- public TabItem(Context context) {
- this(context, null);
- }
-
- public TabItem(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
- R.styleable.TabItem);
- mText = a.getText(R.styleable.TabItem_android_text);
- mIcon = a.getDrawable(R.styleable.TabItem_android_icon);
- mCustomLayout = a.getResourceId(R.styleable.TabItem_android_layout, 0);
- a.recycle();
- }
-} \ No newline at end of file
diff --git a/android/support/design/widget/TabLayout.java b/android/support/design/widget/TabLayout.java
deleted file mode 100644
index 9b81465a..00000000
--- a/android/support/design/widget/TabLayout.java
+++ /dev/null
@@ -1,2217 +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 android.support.design.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static android.support.v4.view.ViewPager.SCROLL_STATE_DRAGGING;
-import static android.support.v4.view.ViewPager.SCROLL_STATE_IDLE;
-import static android.support.v4.view.ViewPager.SCROLL_STATE_SETTLING;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.content.res.Resources;
-import android.content.res.TypedArray;
-import android.database.DataSetObserver;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.drawable.Drawable;
-import android.os.Build;
-import android.support.annotation.ColorInt;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.IntDef;
-import android.support.annotation.LayoutRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.StringRes;
-import android.support.design.R;
-import android.support.v4.util.Pools;
-import android.support.v4.view.GravityCompat;
-import android.support.v4.view.PagerAdapter;
-import android.support.v4.view.PointerIconCompat;
-import android.support.v4.view.ViewCompat;
-import android.support.v4.view.ViewPager;
-import android.support.v4.widget.TextViewCompat;
-import android.support.v7.app.ActionBar;
-import android.support.v7.content.res.AppCompatResources;
-import android.support.v7.widget.TooltipCompat;
-import android.text.Layout;
-import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.SoundEffectConstants;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewParent;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.accessibility.AccessibilityNodeInfo;
-import android.widget.HorizontalScrollView;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.ref.WeakReference;
-import java.util.ArrayList;
-import java.util.Iterator;
-
-/**
- * TabLayout provides a horizontal layout to display tabs.
- *
- * <p>Population of the tabs to display is
- * done through {@link Tab} instances. You create tabs via {@link #newTab()}. From there you can
- * change the tab's label or icon via {@link Tab#setText(int)} and {@link Tab#setIcon(int)}
- * respectively. To display the tab, you need to add it to the layout via one of the
- * {@link #addTab(Tab)} methods. For example:
- * <pre>
- * TabLayout tabLayout = ...;
- * tabLayout.addTab(tabLayout.newTab().setText("Tab 1"));
- * tabLayout.addTab(tabLayout.newTab().setText("Tab 2"));
- * tabLayout.addTab(tabLayout.newTab().setText("Tab 3"));
- * </pre>
- * You should set a listener via {@link #setOnTabSelectedListener(OnTabSelectedListener)} to be
- * notified when any tab's selection state has been changed.
- *
- * <p>You can also add items to TabLayout in your layout through the use of {@link TabItem}.
- * An example usage is like so:</p>
- *
- * <pre>
- * &lt;android.support.design.widget.TabLayout
- * android:layout_height=&quot;wrap_content&quot;
- * android:layout_width=&quot;match_parent&quot;&gt;
- *
- * &lt;android.support.design.widget.TabItem
- * android:text=&quot;@string/tab_text&quot;/&gt;
- *
- * &lt;android.support.design.widget.TabItem
- * android:icon=&quot;@drawable/ic_android&quot;/&gt;
- *
- * &lt;/android.support.design.widget.TabLayout&gt;
- * </pre>
- *
- * <h3>ViewPager integration</h3>
- * <p>
- * If you're using a {@link android.support.v4.view.ViewPager} together
- * with this layout, you can call {@link #setupWithViewPager(ViewPager)} to link the two together.
- * This layout will be automatically populated from the {@link PagerAdapter}'s page titles.</p>
- *
- * <p>
- * This view also supports being used as part of a ViewPager's decor, and can be added
- * directly to the ViewPager in a layout resource file like so:</p>
- *
- * <pre>
- * &lt;android.support.v4.view.ViewPager
- * android:layout_width=&quot;match_parent&quot;
- * android:layout_height=&quot;match_parent&quot;&gt;
- *
- * &lt;android.support.design.widget.TabLayout
- * android:layout_width=&quot;match_parent&quot;
- * android:layout_height=&quot;wrap_content&quot;
- * android:layout_gravity=&quot;top&quot; /&gt;
- *
- * &lt;/android.support.v4.view.ViewPager&gt;
- * </pre>
- *
- * @see <a href="http://www.google.com/design/spec/components/tabs.html">Tabs</a>
- *
- * @attr ref android.support.design.R.styleable#TabLayout_tabPadding
- * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingStart
- * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingTop
- * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingEnd
- * @attr ref android.support.design.R.styleable#TabLayout_tabPaddingBottom
- * @attr ref android.support.design.R.styleable#TabLayout_tabContentStart
- * @attr ref android.support.design.R.styleable#TabLayout_tabBackground
- * @attr ref android.support.design.R.styleable#TabLayout_tabMinWidth
- * @attr ref android.support.design.R.styleable#TabLayout_tabMaxWidth
- * @attr ref android.support.design.R.styleable#TabLayout_tabTextAppearance
- */
-@ViewPager.DecorView
-public class TabLayout extends HorizontalScrollView {
-
- private static final int DEFAULT_HEIGHT_WITH_TEXT_ICON = 72; // dps
- static final int DEFAULT_GAP_TEXT_ICON = 8; // dps
- private static final int INVALID_WIDTH = -1;
- private static final int DEFAULT_HEIGHT = 48; // dps
- private static final int TAB_MIN_WIDTH_MARGIN = 56; //dps
- static final int FIXED_WRAP_GUTTER_MIN = 16; //dps
- static final int MOTION_NON_ADJACENT_OFFSET = 24;
-
- private static final int ANIMATION_DURATION = 300;
-
- private static final Pools.Pool<Tab> sTabPool = new Pools.SynchronizedPool<>(16);
-
- /**
- * Scrollable tabs display a subset of tabs at any given moment, and can contain longer tab
- * labels and a larger number of tabs. They are best used for browsing contexts in touch
- * interfaces when users don’t need to directly compare the tab labels.
- *
- * @see #setTabMode(int)
- * @see #getTabMode()
- */
- public static final int MODE_SCROLLABLE = 0;
-
- /**
- * Fixed tabs display all tabs concurrently and are best used with content that benefits from
- * quick pivots between tabs. The maximum number of tabs is limited by the view’s width.
- * Fixed tabs have equal width, based on the widest tab label.
- *
- * @see #setTabMode(int)
- * @see #getTabMode()
- */
- public static final int MODE_FIXED = 1;
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef(value = {MODE_SCROLLABLE, MODE_FIXED})
- @Retention(RetentionPolicy.SOURCE)
- public @interface Mode {}
-
- /**
- * Gravity used to fill the {@link TabLayout} as much as possible. This option only takes effect
- * when used with {@link #MODE_FIXED}.
- *
- * @see #setTabGravity(int)
- * @see #getTabGravity()
- */
- public static final int GRAVITY_FILL = 0;
-
- /**
- * Gravity used to lay out the tabs in the center of the {@link TabLayout}.
- *
- * @see #setTabGravity(int)
- * @see #getTabGravity()
- */
- public static final int GRAVITY_CENTER = 1;
-
- /**
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- @IntDef(flag = true, value = {GRAVITY_FILL, GRAVITY_CENTER})
- @Retention(RetentionPolicy.SOURCE)
- public @interface TabGravity {}
-
- /**
- * Callback interface invoked when a tab's selection state changes.
- */
- public interface OnTabSelectedListener {
-
- /**
- * Called when a tab enters the selected state.
- *
- * @param tab The tab that was selected
- */
- public void onTabSelected(Tab tab);
-
- /**
- * Called when a tab exits the selected state.
- *
- * @param tab The tab that was unselected
- */
- public void onTabUnselected(Tab tab);
-
- /**
- * Called when a tab that is already selected is chosen again by the user. Some applications
- * may use this action to return to the top level of a category.
- *
- * @param tab The tab that was reselected.
- */
- public void onTabReselected(Tab tab);
- }
-
- private final ArrayList<Tab> mTabs = new ArrayList<>();
- private Tab mSelectedTab;
-
- private final SlidingTabStrip mTabStrip;
-
- int mTabPaddingStart;
- int mTabPaddingTop;
- int mTabPaddingEnd;
- int mTabPaddingBottom;
-
- int mTabTextAppearance;
- ColorStateList mTabTextColors;
- float mTabTextSize;
- float mTabTextMultiLineSize;
-
- final int mTabBackgroundResId;
-
- int mTabMaxWidth = Integer.MAX_VALUE;
- private final int mRequestedTabMinWidth;
- private final int mRequestedTabMaxWidth;
- private final int mScrollableTabMinWidth;
-
- private int mContentInsetStart;
-
- int mTabGravity;
- int mMode;
-
- private OnTabSelectedListener mSelectedListener;
- private final ArrayList<OnTabSelectedListener> mSelectedListeners = new ArrayList<>();
- private OnTabSelectedListener mCurrentVpSelectedListener;
-
- private ValueAnimator mScrollAnimator;
-
- ViewPager mViewPager;
- private PagerAdapter mPagerAdapter;
- private DataSetObserver mPagerAdapterObserver;
- private TabLayoutOnPageChangeListener mPageChangeListener;
- private AdapterChangeListener mAdapterChangeListener;
- private boolean mSetupViewPagerImplicitly;
-
- // Pool we use as a simple RecyclerBin
- private final Pools.Pool<TabView> mTabViewPool = new Pools.SimplePool<>(12);
-
- public TabLayout(Context context) {
- this(context, null);
- }
-
- public TabLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- // Disable the Scroll Bar
- setHorizontalScrollBarEnabled(false);
-
- // Add the TabStrip
- mTabStrip = new SlidingTabStrip(context);
- super.addView(mTabStrip, 0, new HorizontalScrollView.LayoutParams(
- LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT));
-
- TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TabLayout,
- defStyleAttr, R.style.Widget_Design_TabLayout);
-
- mTabStrip.setSelectedIndicatorHeight(
- a.getDimensionPixelSize(R.styleable.TabLayout_tabIndicatorHeight, 0));
- mTabStrip.setSelectedIndicatorColor(a.getColor(R.styleable.TabLayout_tabIndicatorColor, 0));
-
- mTabPaddingStart = mTabPaddingTop = mTabPaddingEnd = mTabPaddingBottom = a
- .getDimensionPixelSize(R.styleable.TabLayout_tabPadding, 0);
- mTabPaddingStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingStart,
- mTabPaddingStart);
- mTabPaddingTop = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingTop,
- mTabPaddingTop);
- mTabPaddingEnd = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingEnd,
- mTabPaddingEnd);
- mTabPaddingBottom = a.getDimensionPixelSize(R.styleable.TabLayout_tabPaddingBottom,
- mTabPaddingBottom);
-
- mTabTextAppearance = a.getResourceId(R.styleable.TabLayout_tabTextAppearance,
- R.style.TextAppearance_Design_Tab);
-
- // Text colors/sizes come from the text appearance first
- final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance,
- android.support.v7.appcompat.R.styleable.TextAppearance);
- try {
- mTabTextSize = ta.getDimensionPixelSize(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize, 0);
- mTabTextColors = ta.getColorStateList(
- android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor);
- } finally {
- ta.recycle();
- }
-
- if (a.hasValue(R.styleable.TabLayout_tabTextColor)) {
- // If we have an explicit text color set, use it instead
- mTabTextColors = a.getColorStateList(R.styleable.TabLayout_tabTextColor);
- }
-
- if (a.hasValue(R.styleable.TabLayout_tabSelectedTextColor)) {
- // We have an explicit selected text color set, so we need to make merge it with the
- // current colors. This is exposed so that developers can use theme attributes to set
- // this (theme attrs in ColorStateLists are Lollipop+)
- final int selected = a.getColor(R.styleable.TabLayout_tabSelectedTextColor, 0);
- mTabTextColors = createColorStateList(mTabTextColors.getDefaultColor(), selected);
- }
-
- mRequestedTabMinWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMinWidth,
- INVALID_WIDTH);
- mRequestedTabMaxWidth = a.getDimensionPixelSize(R.styleable.TabLayout_tabMaxWidth,
- INVALID_WIDTH);
- mTabBackgroundResId = a.getResourceId(R.styleable.TabLayout_tabBackground, 0);
- mContentInsetStart = a.getDimensionPixelSize(R.styleable.TabLayout_tabContentStart, 0);
- mMode = a.getInt(R.styleable.TabLayout_tabMode, MODE_FIXED);
- mTabGravity = a.getInt(R.styleable.TabLayout_tabGravity, GRAVITY_FILL);
- a.recycle();
-
- // TODO add attr for these
- final Resources res = getResources();
- mTabTextMultiLineSize = res.getDimensionPixelSize(R.dimen.design_tab_text_size_2line);
- mScrollableTabMinWidth = res.getDimensionPixelSize(R.dimen.design_tab_scrollable_min_width);
-
- // Now apply the tab mode and gravity
- applyModeAndGravity();
- }
-
- /**
- * Sets the tab indicator's color for the currently selected tab.
- *
- * @param color color to use for the indicator
- *
- * @attr ref android.support.design.R.styleable#TabLayout_tabIndicatorColor
- */
- public void setSelectedTabIndicatorColor(@ColorInt int color) {
- mTabStrip.setSelectedIndicatorColor(color);
- }
-
- /**
- * Sets the tab indicator's height for the currently selected tab.
- *
- * @param height height to use for the indicator in pixels
- *
- * @attr ref android.support.design.R.styleable#TabLayout_tabIndicatorHeight
- */
- public void setSelectedTabIndicatorHeight(int height) {
- mTabStrip.setSelectedIndicatorHeight(height);
- }
-
- /**
- * Set the scroll position of the tabs. This is useful for when the tabs are being displayed as
- * part of a scrolling container such as {@link android.support.v4.view.ViewPager}.
- * <p>
- * Calling this method does not update the selected tab, it is only used for drawing purposes.
- *
- * @param position current scroll position
- * @param positionOffset Value from [0, 1) indicating the offset from {@code position}.
- * @param updateSelectedText Whether to update the text's selected state.
- */
- public void setScrollPosition(int position, float positionOffset, boolean updateSelectedText) {
- setScrollPosition(position, positionOffset, updateSelectedText, true);
- }
-
- void setScrollPosition(int position, float positionOffset, boolean updateSelectedText,
- boolean updateIndicatorPosition) {
- final int roundedPosition = Math.round(position + positionOffset);
- if (roundedPosition < 0 || roundedPosition >= mTabStrip.getChildCount()) {
- return;
- }
-
- // Set the indicator position, if enabled
- if (updateIndicatorPosition) {
- mTabStrip.setIndicatorPositionFromTabPosition(position, positionOffset);
- }
-
- // Now update the scroll position, canceling any running animation
- if (mScrollAnimator != null && mScrollAnimator.isRunning()) {
- mScrollAnimator.cancel();
- }
- scrollTo(calculateScrollXForTab(position, positionOffset), 0);
-
- // Update the 'selected state' view as we scroll, if enabled
- if (updateSelectedText) {
- setSelectedTabView(roundedPosition);
- }
- }
-
- private float getScrollPosition() {
- return mTabStrip.getIndicatorPosition();
- }
-
- /**
- * Add a tab to this layout. The tab will be added at the end of the list.
- * If this is the first tab to be added it will become the selected tab.
- *
- * @param tab Tab to add
- */
- public void addTab(@NonNull Tab tab) {
- addTab(tab, mTabs.isEmpty());
- }
-
- /**
- * Add a tab to this layout. The tab will be inserted at <code>position</code>.
- * If this is the first tab to be added it will become the selected tab.
- *
- * @param tab The tab to add
- * @param position The new position of the tab
- */
- public void addTab(@NonNull Tab tab, int position) {
- addTab(tab, position, mTabs.isEmpty());
- }
-
- /**
- * Add a tab to this layout. The tab will be added at the end of the list.
- *
- * @param tab Tab to add
- * @param setSelected True if the added tab should become the selected tab.
- */
- public void addTab(@NonNull Tab tab, boolean setSelected) {
- addTab(tab, mTabs.size(), setSelected);
- }
-
- /**
- * Add a tab to this layout. The tab will be inserted at <code>position</code>.
- *
- * @param tab The tab to add
- * @param position The new position of the tab
- * @param setSelected True if the added tab should become the selected tab.
- */
- public void addTab(@NonNull Tab tab, int position, boolean setSelected) {
- if (tab.mParent != this) {
- throw new IllegalArgumentException("Tab belongs to a different TabLayout.");
- }
- configureTab(tab, position);
- addTabView(tab);
-
- if (setSelected) {
- tab.select();
- }
- }
-
- private void addTabFromItemView(@NonNull TabItem item) {
- final Tab tab = newTab();
- if (item.mText != null) {
- tab.setText(item.mText);
- }
- if (item.mIcon != null) {
- tab.setIcon(item.mIcon);
- }
- if (item.mCustomLayout != 0) {
- tab.setCustomView(item.mCustomLayout);
- }
- if (!TextUtils.isEmpty(item.getContentDescription())) {
- tab.setContentDescription(item.getContentDescription());
- }
- addTab(tab);
- }
-
- /**
- * @deprecated Use {@link #addOnTabSelectedListener(OnTabSelectedListener)} and
- * {@link #removeOnTabSelectedListener(OnTabSelectedListener)}.
- */
- @Deprecated
- public void setOnTabSelectedListener(@Nullable OnTabSelectedListener listener) {
- // The logic in this method emulates what we had before support for multiple
- // registered listeners.
- if (mSelectedListener != null) {
- removeOnTabSelectedListener(mSelectedListener);
- }
- // Update the deprecated field so that we can remove the passed listener the next
- // time we're called
- mSelectedListener = listener;
- if (listener != null) {
- addOnTabSelectedListener(listener);
- }
- }
-
- /**
- * Add a {@link TabLayout.OnTabSelectedListener} that will be invoked when tab selection
- * changes.
- *
- * <p>Components that add a listener should take care to remove it when finished via
- * {@link #removeOnTabSelectedListener(OnTabSelectedListener)}.</p>
- *
- * @param listener listener to add
- */
- public void addOnTabSelectedListener(@NonNull OnTabSelectedListener listener) {
- if (!mSelectedListeners.contains(listener)) {
- mSelectedListeners.add(listener);
- }
- }
-
- /**
- * Remove the given {@link TabLayout.OnTabSelectedListener} that was previously added via
- * {@link #addOnTabSelectedListener(OnTabSelectedListener)}.
- *
- * @param listener listener to remove
- */
- public void removeOnTabSelectedListener(@NonNull OnTabSelectedListener listener) {
- mSelectedListeners.remove(listener);
- }
-
- /**
- * Remove all previously added {@link TabLayout.OnTabSelectedListener}s.
- */
- public void clearOnTabSelectedListeners() {
- mSelectedListeners.clear();
- }
-
- /**
- * Create and return a new {@link Tab}. You need to manually add this using
- * {@link #addTab(Tab)} or a related method.
- *
- * @return A new Tab
- * @see #addTab(Tab)
- */
- @NonNull
- public Tab newTab() {
- Tab tab = sTabPool.acquire();
- if (tab == null) {
- tab = new Tab();
- }
- tab.mParent = this;
- tab.mView = createTabView(tab);
- return tab;
- }
-
- /**
- * Returns the number of tabs currently registered with the action bar.
- *
- * @return Tab count
- */
- public int getTabCount() {
- return mTabs.size();
- }
-
- /**
- * Returns the tab at the specified index.
- */
- @Nullable
- public Tab getTabAt(int index) {
- return (index < 0 || index >= getTabCount()) ? null : mTabs.get(index);
- }
-
- /**
- * Returns the position of the current selected tab.
- *
- * @return selected tab position, or {@code -1} if there isn't a selected tab.
- */
- public int getSelectedTabPosition() {
- return mSelectedTab != null ? mSelectedTab.getPosition() : -1;
- }
-
- /**
- * Remove a tab from the layout. If the removed tab was selected it will be deselected
- * and another tab will be selected if present.
- *
- * @param tab The tab to remove
- */
- public void removeTab(Tab tab) {
- if (tab.mParent != this) {
- throw new IllegalArgumentException("Tab does not belong to this TabLayout.");
- }
-
- removeTabAt(tab.getPosition());
- }
-
- /**
- * Remove a tab from the layout. If the removed tab was selected it will be deselected
- * and another tab will be selected if present.
- *
- * @param position Position of the tab to remove
- */
- public void removeTabAt(int position) {
- final int selectedTabPosition = mSelectedTab != null ? mSelectedTab.getPosition() : 0;
- removeTabViewAt(position);
-
- final Tab removedTab = mTabs.remove(position);
- if (removedTab != null) {
- removedTab.reset();
- sTabPool.release(removedTab);
- }
-
- final int newTabCount = mTabs.size();
- for (int i = position; i < newTabCount; i++) {
- mTabs.get(i).setPosition(i);
- }
-
- if (selectedTabPosition == position) {
- selectTab(mTabs.isEmpty() ? null : mTabs.get(Math.max(0, position - 1)));
- }
- }
-
- /**
- * Remove all tabs from the action bar and deselect the current tab.
- */
- public void removeAllTabs() {
- // Remove all the views
- for (int i = mTabStrip.getChildCount() - 1; i >= 0; i--) {
- removeTabViewAt(i);
- }
-
- for (final Iterator<Tab> i = mTabs.iterator(); i.hasNext();) {
- final Tab tab = i.next();
- i.remove();
- tab.reset();
- sTabPool.release(tab);
- }
-
- mSelectedTab = null;
- }
-
- /**
- * Set the behavior mode for the Tabs in this layout. The valid input options are:
- * <ul>
- * <li>{@link #MODE_FIXED}: Fixed tabs display all tabs concurrently and are best used
- * with content that benefits from quick pivots between tabs.</li>
- * <li>{@link #MODE_SCROLLABLE}: Scrollable tabs display a subset of tabs at any given moment,
- * and can contain longer tab labels and a larger number of tabs. They are best used for
- * browsing contexts in touch interfaces when users don’t need to directly compare the tab
- * labels. This mode is commonly used with a {@link android.support.v4.view.ViewPager}.</li>
- * </ul>
- *
- * @param mode one of {@link #MODE_FIXED} or {@link #MODE_SCROLLABLE}.
- *
- * @attr ref android.support.design.R.styleable#TabLayout_tabMode
- */
- public void setTabMode(@Mode int mode) {
- if (mode != mMode) {
- mMode = mode;
- applyModeAndGravity();
- }
- }
-
- /**
- * Returns the current mode used by this {@link TabLayout}.
- *
- * @see #setTabMode(int)
- */
- @Mode
- public int getTabMode() {
- return mMode;
- }
-
- /**
- * Set the gravity to use when laying out the tabs.
- *
- * @param gravity one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
- *
- * @attr ref android.support.design.R.styleable#TabLayout_tabGravity
- */
- public void setTabGravity(@TabGravity int gravity) {
- if (mTabGravity != gravity) {
- mTabGravity = gravity;
- applyModeAndGravity();
- }
- }
-
- /**
- * The current gravity used for laying out tabs.
- *
- * @return one of {@link #GRAVITY_CENTER} or {@link #GRAVITY_FILL}.
- */
- @TabGravity
- public int getTabGravity() {
- return mTabGravity;
- }
-
- /**
- * Sets the text colors for the different states (normal, selected) used for the tabs.
- *
- * @see #getTabTextColors()
- */
- public void setTabTextColors(@Nullable ColorStateList textColor) {
- if (mTabTextColors != textColor) {
- mTabTextColors = textColor;
- updateAllTabs();
- }
- }
-
- /**
- * Gets the text colors for the different states (normal, selected) used for the tabs.
- */
- @Nullable
- public ColorStateList getTabTextColors() {
- return mTabTextColors;
- }
-
- /**
- * Sets the text colors for the different states (normal, selected) used for the tabs.
- *
- * @attr ref android.support.design.R.styleable#TabLayout_tabTextColor
- * @attr ref android.support.design.R.styleable#TabLayout_tabSelectedTextColor
- */
- public void setTabTextColors(int normalColor, int selectedColor) {
- setTabTextColors(createColorStateList(normalColor, selectedColor));
- }
-
- /**
- * The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}.
- *
- * <p>This is the same as calling {@link #setupWithViewPager(ViewPager, boolean)} with
- * auto-refresh enabled.</p>
- *
- * @param viewPager the ViewPager to link to, or {@code null} to clear any previous link
- */
- public void setupWithViewPager(@Nullable ViewPager viewPager) {
- setupWithViewPager(viewPager, true);
- }
-
- /**
- * The one-stop shop for setting up this {@link TabLayout} with a {@link ViewPager}.
- *
- * <p>This method will link the given ViewPager and this TabLayout together so that
- * changes in one are automatically reflected in the other. This includes scroll state changes
- * and clicks. The tabs displayed in this layout will be populated
- * from the ViewPager adapter's page titles.</p>
- *
- * <p>If {@code autoRefresh} is {@code true}, any changes in the {@link PagerAdapter} will
- * trigger this layout to re-populate itself from the adapter's titles.</p>
- *
- * <p>If the given ViewPager is non-null, it needs to already have a
- * {@link PagerAdapter} set.</p>
- *
- * @param viewPager the ViewPager to link to, or {@code null} to clear any previous link
- * @param autoRefresh whether this layout should refresh its contents if the given ViewPager's
- * content changes
- */
- public void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh) {
- setupWithViewPager(viewPager, autoRefresh, false);
- }
-
- private void setupWithViewPager(@Nullable final ViewPager viewPager, boolean autoRefresh,
- boolean implicitSetup) {
- if (mViewPager != null) {
- // If we've already been setup with a ViewPager, remove us from it
- if (mPageChangeListener != null) {
- mViewPager.removeOnPageChangeListener(mPageChangeListener);
- }
- if (mAdapterChangeListener != null) {
- mViewPager.removeOnAdapterChangeListener(mAdapterChangeListener);
- }
- }
-
- if (mCurrentVpSelectedListener != null) {
- // If we already have a tab selected listener for the ViewPager, remove it
- removeOnTabSelectedListener(mCurrentVpSelectedListener);
- mCurrentVpSelectedListener = null;
- }
-
- if (viewPager != null) {
- mViewPager = viewPager;
-
- // Add our custom OnPageChangeListener to the ViewPager
- if (mPageChangeListener == null) {
- mPageChangeListener = new TabLayoutOnPageChangeListener(this);
- }
- mPageChangeListener.reset();
- viewPager.addOnPageChangeListener(mPageChangeListener);
-
- // Now we'll add a tab selected listener to set ViewPager's current item
- mCurrentVpSelectedListener = new ViewPagerOnTabSelectedListener(viewPager);
- addOnTabSelectedListener(mCurrentVpSelectedListener);
-
- final PagerAdapter adapter = viewPager.getAdapter();
- if (adapter != null) {
- // Now we'll populate ourselves from the pager adapter, adding an observer if
- // autoRefresh is enabled
- setPagerAdapter(adapter, autoRefresh);
- }
-
- // Add a listener so that we're notified of any adapter changes
- if (mAdapterChangeListener == null) {
- mAdapterChangeListener = new AdapterChangeListener();
- }
- mAdapterChangeListener.setAutoRefresh(autoRefresh);
- viewPager.addOnAdapterChangeListener(mAdapterChangeListener);
-
- // Now update the scroll position to match the ViewPager's current item
- setScrollPosition(viewPager.getCurrentItem(), 0f, true);
- } else {
- // We've been given a null ViewPager so we need to clear out the internal state,
- // listeners and observers
- mViewPager = null;
- setPagerAdapter(null, false);
- }
-
- mSetupViewPagerImplicitly = implicitSetup;
- }
-
- /**
- * @deprecated Use {@link #setupWithViewPager(ViewPager)} to link a TabLayout with a ViewPager
- * together. When that method is used, the TabLayout will be automatically updated
- * when the {@link PagerAdapter} is changed.
- */
- @Deprecated
- public void setTabsFromPagerAdapter(@Nullable final PagerAdapter adapter) {
- setPagerAdapter(adapter, false);
- }
-
- @Override
- public boolean shouldDelayChildPressedState() {
- // Only delay the pressed state if the tabs can scroll
- return getTabScrollRange() > 0;
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
-
- if (mViewPager == null) {
- // If we don't have a ViewPager already, check if our parent is a ViewPager to
- // setup with it automatically
- final ViewParent vp = getParent();
- if (vp instanceof ViewPager) {
- // If we have a ViewPager parent and we've been added as part of its decor, let's
- // assume that we should automatically setup to display any titles
- setupWithViewPager((ViewPager) vp, true, true);
- }
- }
- }
-
- @Override
- protected void onDetachedFromWindow() {
- super.onDetachedFromWindow();
-
- if (mSetupViewPagerImplicitly) {
- // If we've been setup with a ViewPager implicitly, let's clear out any listeners, etc
- setupWithViewPager(null);
- mSetupViewPagerImplicitly = false;
- }
- }
-
- private int getTabScrollRange() {
- return Math.max(0, mTabStrip.getWidth() - getWidth() - getPaddingLeft()
- - getPaddingRight());
- }
-
- void setPagerAdapter(@Nullable final PagerAdapter adapter, final boolean addObserver) {
- if (mPagerAdapter != null && mPagerAdapterObserver != null) {
- // If we already have a PagerAdapter, unregister our observer
- mPagerAdapter.unregisterDataSetObserver(mPagerAdapterObserver);
- }
-
- mPagerAdapter = adapter;
-
- if (addObserver && adapter != null) {
- // Register our observer on the new adapter
- if (mPagerAdapterObserver == null) {
- mPagerAdapterObserver = new PagerAdapterObserver();
- }
- adapter.registerDataSetObserver(mPagerAdapterObserver);
- }
-
- // Finally make sure we reflect the new adapter
- populateFromPagerAdapter();
- }
-
- void populateFromPagerAdapter() {
- removeAllTabs();
-
- if (mPagerAdapter != null) {
- final int adapterCount = mPagerAdapter.getCount();
- for (int i = 0; i < adapterCount; i++) {
- addTab(newTab().setText(mPagerAdapter.getPageTitle(i)), false);
- }
-
- // Make sure we reflect the currently set ViewPager item
- if (mViewPager != null && adapterCount > 0) {
- final int curItem = mViewPager.getCurrentItem();
- if (curItem != getSelectedTabPosition() && curItem < getTabCount()) {
- selectTab(getTabAt(curItem));
- }
- }
- }
- }
-
- private void updateAllTabs() {
- for (int i = 0, z = mTabs.size(); i < z; i++) {
- mTabs.get(i).updateView();
- }
- }
-
- private TabView createTabView(@NonNull final Tab tab) {
- TabView tabView = mTabViewPool != null ? mTabViewPool.acquire() : null;
- if (tabView == null) {
- tabView = new TabView(getContext());
- }
- tabView.setTab(tab);
- tabView.setFocusable(true);
- tabView.setMinimumWidth(getTabMinWidth());
- return tabView;
- }
-
- private void configureTab(Tab tab, int position) {
- tab.setPosition(position);
- mTabs.add(position, tab);
-
- final int count = mTabs.size();
- for (int i = position + 1; i < count; i++) {
- mTabs.get(i).setPosition(i);
- }
- }
-
- private void addTabView(Tab tab) {
- final TabView tabView = tab.mView;
- mTabStrip.addView(tabView, tab.getPosition(), createLayoutParamsForTabs());
- }
-
- @Override
- public void addView(View child) {
- addViewInternal(child);
- }
-
- @Override
- public void addView(View child, int index) {
- addViewInternal(child);
- }
-
- @Override
- public void addView(View child, ViewGroup.LayoutParams params) {
- addViewInternal(child);
- }
-
- @Override
- public void addView(View child, int index, ViewGroup.LayoutParams params) {
- addViewInternal(child);
- }
-
- private void addViewInternal(final View child) {
- if (child instanceof TabItem) {
- addTabFromItemView((TabItem) child);
- } else {
- throw new IllegalArgumentException("Only TabItem instances can be added to TabLayout");
- }
- }
-
- private LinearLayout.LayoutParams createLayoutParamsForTabs() {
- final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
- LayoutParams.WRAP_CONTENT, LayoutParams.MATCH_PARENT);
- updateTabViewLayoutParams(lp);
- return lp;
- }
-
- private void updateTabViewLayoutParams(LinearLayout.LayoutParams lp) {
- if (mMode == MODE_FIXED && mTabGravity == GRAVITY_FILL) {
- lp.width = 0;
- lp.weight = 1;
- } else {
- lp.width = LinearLayout.LayoutParams.WRAP_CONTENT;
- lp.weight = 0;
- }
- }
-
- int dpToPx(int dps) {
- return Math.round(getResources().getDisplayMetrics().density * dps);
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- // If we have a MeasureSpec which allows us to decide our height, try and use the default
- // height
- final int idealHeight = dpToPx(getDefaultHeight()) + getPaddingTop() + getPaddingBottom();
- switch (MeasureSpec.getMode(heightMeasureSpec)) {
- case MeasureSpec.AT_MOST:
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(
- Math.min(idealHeight, MeasureSpec.getSize(heightMeasureSpec)),
- MeasureSpec.EXACTLY);
- break;
- case MeasureSpec.UNSPECIFIED:
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(idealHeight, MeasureSpec.EXACTLY);
- break;
- }
-
- final int specWidth = MeasureSpec.getSize(widthMeasureSpec);
- if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED) {
- // If we don't have an unspecified width spec, use the given size to calculate
- // the max tab width
- mTabMaxWidth = mRequestedTabMaxWidth > 0
- ? mRequestedTabMaxWidth
- : specWidth - dpToPx(TAB_MIN_WIDTH_MARGIN);
- }
-
- // Now super measure itself using the (possibly) modified height spec
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- if (getChildCount() == 1) {
- // If we're in fixed mode then we need to make the tab strip is the same width as us
- // so we don't scroll
- final View child = getChildAt(0);
- boolean remeasure = false;
-
- switch (mMode) {
- case MODE_SCROLLABLE:
- // We only need to resize the child if it's smaller than us. This is similar
- // to fillViewport
- remeasure = child.getMeasuredWidth() < getMeasuredWidth();
- break;
- case MODE_FIXED:
- // Resize the child so that it doesn't scroll
- remeasure = child.getMeasuredWidth() != getMeasuredWidth();
- break;
- }
-
- if (remeasure) {
- // Re-measure the child with a widthSpec set to be exactly our measure width
- int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec, getPaddingTop()
- + getPaddingBottom(), child.getLayoutParams().height);
- int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
- getMeasuredWidth(), MeasureSpec.EXACTLY);
- child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
- }
- }
- }
-
- private void removeTabViewAt(int position) {
- final TabView view = (TabView) mTabStrip.getChildAt(position);
- mTabStrip.removeViewAt(position);
- if (view != null) {
- view.reset();
- mTabViewPool.release(view);
- }
- requestLayout();
- }
-
- private void animateToTab(int newPosition) {
- if (newPosition == Tab.INVALID_POSITION) {
- return;
- }
-
- if (getWindowToken() == null || !ViewCompat.isLaidOut(this)
- || mTabStrip.childrenNeedLayout()) {
- // If we don't have a window token, or we haven't been laid out yet just draw the new
- // position now
- setScrollPosition(newPosition, 0f, true);
- return;
- }
-
- final int startScrollX = getScrollX();
- final int targetScrollX = calculateScrollXForTab(newPosition, 0);
-
- if (startScrollX != targetScrollX) {
- ensureScrollAnimator();
-
- mScrollAnimator.setIntValues(startScrollX, targetScrollX);
- mScrollAnimator.start();
- }
-
- // Now animate the indicator
- mTabStrip.animateIndicatorToPosition(newPosition, ANIMATION_DURATION);
- }
-
- private void ensureScrollAnimator() {
- if (mScrollAnimator == null) {
- mScrollAnimator = new ValueAnimator();
- mScrollAnimator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
- mScrollAnimator.setDuration(ANIMATION_DURATION);
- mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- scrollTo((int) animator.getAnimatedValue(), 0);
- }
- });
- }
- }
-
- void setScrollAnimatorListener(Animator.AnimatorListener listener) {
- ensureScrollAnimator();
- mScrollAnimator.addListener(listener);
- }
-
- private void setSelectedTabView(int position) {
- final int tabCount = mTabStrip.getChildCount();
- if (position < tabCount) {
- for (int i = 0; i < tabCount; i++) {
- final View child = mTabStrip.getChildAt(i);
- child.setSelected(i == position);
- }
- }
- }
-
- void selectTab(Tab tab) {
- selectTab(tab, true);
- }
-
- void selectTab(final Tab tab, boolean updateIndicator) {
- final Tab currentTab = mSelectedTab;
-
- if (currentTab == tab) {
- if (currentTab != null) {
- dispatchTabReselected(tab);
- animateToTab(tab.getPosition());
- }
- } else {
- final int newPosition = tab != null ? tab.getPosition() : Tab.INVALID_POSITION;
- if (updateIndicator) {
- if ((currentTab == null || currentTab.getPosition() == Tab.INVALID_POSITION)
- && newPosition != Tab.INVALID_POSITION) {
- // If we don't currently have a tab, just draw the indicator
- setScrollPosition(newPosition, 0f, true);
- } else {
- animateToTab(newPosition);
- }
- if (newPosition != Tab.INVALID_POSITION) {
- setSelectedTabView(newPosition);
- }
- }
- if (currentTab != null) {
- dispatchTabUnselected(currentTab);
- }
- mSelectedTab = tab;
- if (tab != null) {
- dispatchTabSelected(tab);
- }
- }
- }
-
- private void dispatchTabSelected(@NonNull final Tab tab) {
- for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
- mSelectedListeners.get(i).onTabSelected(tab);
- }
- }
-
- private void dispatchTabUnselected(@NonNull final Tab tab) {
- for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
- mSelectedListeners.get(i).onTabUnselected(tab);
- }
- }
-
- private void dispatchTabReselected(@NonNull final Tab tab) {
- for (int i = mSelectedListeners.size() - 1; i >= 0; i--) {
- mSelectedListeners.get(i).onTabReselected(tab);
- }
- }
-
- private int calculateScrollXForTab(int position, float positionOffset) {
- if (mMode == MODE_SCROLLABLE) {
- final View selectedChild = mTabStrip.getChildAt(position);
- final View nextChild = position + 1 < mTabStrip.getChildCount()
- ? mTabStrip.getChildAt(position + 1)
- : null;
- final int selectedWidth = selectedChild != null ? selectedChild.getWidth() : 0;
- final int nextWidth = nextChild != null ? nextChild.getWidth() : 0;
-
- // base scroll amount: places center of tab in center of parent
- int scrollBase = selectedChild.getLeft() + (selectedWidth / 2) - (getWidth() / 2);
- // offset amount: fraction of the distance between centers of tabs
- int scrollOffset = (int) ((selectedWidth + nextWidth) * 0.5f * positionOffset);
-
- return (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR)
- ? scrollBase + scrollOffset
- : scrollBase - scrollOffset;
- }
- return 0;
- }
-
- private void applyModeAndGravity() {
- int paddingStart = 0;
- if (mMode == MODE_SCROLLABLE) {
- // If we're scrollable, or fixed at start, inset using padding
- paddingStart = Math.max(0, mContentInsetStart - mTabPaddingStart);
- }
- ViewCompat.setPaddingRelative(mTabStrip, paddingStart, 0, 0, 0);
-
- switch (mMode) {
- case MODE_FIXED:
- mTabStrip.setGravity(Gravity.CENTER_HORIZONTAL);
- break;
- case MODE_SCROLLABLE:
- mTabStrip.setGravity(GravityCompat.START);
- break;
- }
-
- updateTabViews(true);
- }
-
- void updateTabViews(final boolean requestLayout) {
- for (int i = 0; i < mTabStrip.getChildCount(); i++) {
- View child = mTabStrip.getChildAt(i);
- child.setMinimumWidth(getTabMinWidth());
- updateTabViewLayoutParams((LinearLayout.LayoutParams) child.getLayoutParams());
- if (requestLayout) {
- child.requestLayout();
- }
- }
- }
-
- /**
- * A tab in this layout. Instances can be created via {@link #newTab()}.
- */
- public static final class Tab {
-
- /**
- * An invalid position for a tab.
- *
- * @see #getPosition()
- */
- public static final int INVALID_POSITION = -1;
-
- private Object mTag;
- private Drawable mIcon;
- private CharSequence mText;
- private CharSequence mContentDesc;
- private int mPosition = INVALID_POSITION;
- private View mCustomView;
-
- TabLayout mParent;
- TabView mView;
-
- Tab() {
- // Private constructor
- }
-
- /**
- * @return This Tab's tag object.
- */
- @Nullable
- public Object getTag() {
- return mTag;
- }
-
- /**
- * Give this Tab an arbitrary object to hold for later use.
- *
- * @param tag Object to store
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setTag(@Nullable Object tag) {
- mTag = tag;
- return this;
- }
-
-
- /**
- * Returns the custom view used for this tab.
- *
- * @see #setCustomView(View)
- * @see #setCustomView(int)
- */
- @Nullable
- public View getCustomView() {
- return mCustomView;
- }
-
- /**
- * Set a custom view to be used for this tab.
- * <p>
- * If the provided view contains a {@link TextView} with an ID of
- * {@link android.R.id#text1} then that will be updated with the value given
- * to {@link #setText(CharSequence)}. Similarly, if this layout contains an
- * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
- * the value given to {@link #setIcon(Drawable)}.
- * </p>
- *
- * @param view Custom view to be used as a tab.
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setCustomView(@Nullable View view) {
- mCustomView = view;
- updateView();
- return this;
- }
-
- /**
- * Set a custom view to be used for this tab.
- * <p>
- * If the inflated layout contains a {@link TextView} with an ID of
- * {@link android.R.id#text1} then that will be updated with the value given
- * to {@link #setText(CharSequence)}. Similarly, if this layout contains an
- * {@link ImageView} with ID {@link android.R.id#icon} then it will be updated with
- * the value given to {@link #setIcon(Drawable)}.
- * </p>
- *
- * @param resId A layout resource to inflate and use as a custom tab view
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setCustomView(@LayoutRes int resId) {
- final LayoutInflater inflater = LayoutInflater.from(mView.getContext());
- return setCustomView(inflater.inflate(resId, mView, false));
- }
-
- /**
- * Return the icon associated with this tab.
- *
- * @return The tab's icon
- */
- @Nullable
- public Drawable getIcon() {
- return mIcon;
- }
-
- /**
- * Return the current position of this tab in the action bar.
- *
- * @return Current position, or {@link #INVALID_POSITION} if this tab is not currently in
- * the action bar.
- */
- public int getPosition() {
- return mPosition;
- }
-
- void setPosition(int position) {
- mPosition = position;
- }
-
- /**
- * Return the text of this tab.
- *
- * @return The tab's text
- */
- @Nullable
- public CharSequence getText() {
- return mText;
- }
-
- /**
- * Set the icon displayed on this tab.
- *
- * @param icon The drawable to use as an icon
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setIcon(@Nullable Drawable icon) {
- mIcon = icon;
- updateView();
- return this;
- }
-
- /**
- * Set the icon displayed on this tab.
- *
- * @param resId A resource ID referring to the icon that should be displayed
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setIcon(@DrawableRes int resId) {
- if (mParent == null) {
- throw new IllegalArgumentException("Tab not attached to a TabLayout");
- }
- return setIcon(AppCompatResources.getDrawable(mParent.getContext(), resId));
- }
-
- /**
- * Set the text displayed on this tab. Text may be truncated if there is not room to display
- * the entire string.
- *
- * @param text The text to display
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setText(@Nullable CharSequence text) {
- mText = text;
- updateView();
- return this;
- }
-
- /**
- * Set the text displayed on this tab. Text may be truncated if there is not room to display
- * the entire string.
- *
- * @param resId A resource ID referring to the text that should be displayed
- * @return The current instance for call chaining
- */
- @NonNull
- public Tab setText(@StringRes int resId) {
- if (mParent == null) {
- throw new IllegalArgumentException("Tab not attached to a TabLayout");
- }
- return setText(mParent.getResources().getText(resId));
- }
-
- /**
- * Select this tab. Only valid if the tab has been added to the action bar.
- */
- public void select() {
- if (mParent == null) {
- throw new IllegalArgumentException("Tab not attached to a TabLayout");
- }
- mParent.selectTab(this);
- }
-
- /**
- * Returns true if this tab is currently selected.
- */
- public boolean isSelected() {
- if (mParent == null) {
- throw new IllegalArgumentException("Tab not attached to a TabLayout");
- }
- return mParent.getSelectedTabPosition() == mPosition;
- }
-
- /**
- * Set a description of this tab's content for use in accessibility support. If no content
- * description is provided the title will be used.
- *
- * @param resId A resource ID referring to the description text
- * @return The current instance for call chaining
- * @see #setContentDescription(CharSequence)
- * @see #getContentDescription()
- */
- @NonNull
- public Tab setContentDescription(@StringRes int resId) {
- if (mParent == null) {
- throw new IllegalArgumentException("Tab not attached to a TabLayout");
- }
- return setContentDescription(mParent.getResources().getText(resId));
- }
-
- /**
- * Set a description of this tab's content for use in accessibility support. If no content
- * description is provided the title will be used.
- *
- * @param contentDesc Description of this tab's content
- * @return The current instance for call chaining
- * @see #setContentDescription(int)
- * @see #getContentDescription()
- */
- @NonNull
- public Tab setContentDescription(@Nullable CharSequence contentDesc) {
- mContentDesc = contentDesc;
- updateView();
- return this;
- }
-
- /**
- * Gets a brief description of this tab's content for use in accessibility support.
- *
- * @return Description of this tab's content
- * @see #setContentDescription(CharSequence)
- * @see #setContentDescription(int)
- */
- @Nullable
- public CharSequence getContentDescription() {
- return mContentDesc;
- }
-
- void updateView() {
- if (mView != null) {
- mView.update();
- }
- }
-
- void reset() {
- mParent = null;
- mView = null;
- mTag = null;
- mIcon = null;
- mText = null;
- mContentDesc = null;
- mPosition = INVALID_POSITION;
- mCustomView = null;
- }
- }
-
- class TabView extends LinearLayout {
- private Tab mTab;
- private TextView mTextView;
- private ImageView mIconView;
-
- private View mCustomView;
- private TextView mCustomTextView;
- private ImageView mCustomIconView;
-
- private int mDefaultMaxLines = 2;
-
- public TabView(Context context) {
- super(context);
- if (mTabBackgroundResId != 0) {
- ViewCompat.setBackground(
- this, AppCompatResources.getDrawable(context, mTabBackgroundResId));
- }
- ViewCompat.setPaddingRelative(this, mTabPaddingStart, mTabPaddingTop,
- mTabPaddingEnd, mTabPaddingBottom);
- setGravity(Gravity.CENTER);
- setOrientation(VERTICAL);
- setClickable(true);
- ViewCompat.setPointerIcon(this,
- PointerIconCompat.getSystemIcon(getContext(), PointerIconCompat.TYPE_HAND));
- }
-
- @Override
- public boolean performClick() {
- final boolean handled = super.performClick();
-
- if (mTab != null) {
- if (!handled) {
- playSoundEffect(SoundEffectConstants.CLICK);
- }
- mTab.select();
- return true;
- } else {
- return handled;
- }
- }
-
- @Override
- public void setSelected(final boolean selected) {
- final boolean changed = isSelected() != selected;
-
- super.setSelected(selected);
-
- if (changed && selected && Build.VERSION.SDK_INT < 16) {
- // Pre-JB we need to manually send the TYPE_VIEW_SELECTED event
- sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
- }
-
- // Always dispatch this to the child views, regardless of whether the value has
- // changed
- if (mTextView != null) {
- mTextView.setSelected(selected);
- }
- if (mIconView != null) {
- mIconView.setSelected(selected);
- }
- if (mCustomView != null) {
- mCustomView.setSelected(selected);
- }
- }
-
- @Override
- public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
- super.onInitializeAccessibilityEvent(event);
- // This view masquerades as an action bar tab.
- event.setClassName(ActionBar.Tab.class.getName());
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
- super.onInitializeAccessibilityNodeInfo(info);
- // This view masquerades as an action bar tab.
- info.setClassName(ActionBar.Tab.class.getName());
- }
-
- @Override
- public void onMeasure(final int origWidthMeasureSpec, final int origHeightMeasureSpec) {
- final int specWidthSize = MeasureSpec.getSize(origWidthMeasureSpec);
- final int specWidthMode = MeasureSpec.getMode(origWidthMeasureSpec);
- final int maxWidth = getTabMaxWidth();
-
- final int widthMeasureSpec;
- final int heightMeasureSpec = origHeightMeasureSpec;
-
- if (maxWidth > 0 && (specWidthMode == MeasureSpec.UNSPECIFIED
- || specWidthSize > maxWidth)) {
- // If we have a max width and a given spec which is either unspecified or
- // larger than the max width, update the width spec using the same mode
- widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTabMaxWidth, MeasureSpec.AT_MOST);
- } else {
- // Else, use the original width spec
- widthMeasureSpec = origWidthMeasureSpec;
- }
-
- // Now lets measure
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- // We need to switch the text size based on whether the text is spanning 2 lines or not
- if (mTextView != null) {
- final Resources res = getResources();
- float textSize = mTabTextSize;
- int maxLines = mDefaultMaxLines;
-
- if (mIconView != null && mIconView.getVisibility() == VISIBLE) {
- // If the icon view is being displayed, we limit the text to 1 line
- maxLines = 1;
- } else if (mTextView != null && mTextView.getLineCount() > 1) {
- // Otherwise when we have text which wraps we reduce the text size
- textSize = mTabTextMultiLineSize;
- }
-
- final float curTextSize = mTextView.getTextSize();
- final int curLineCount = mTextView.getLineCount();
- final int curMaxLines = TextViewCompat.getMaxLines(mTextView);
-
- if (textSize != curTextSize || (curMaxLines >= 0 && maxLines != curMaxLines)) {
- // We've got a new text size and/or max lines...
- boolean updateTextView = true;
-
- if (mMode == MODE_FIXED && textSize > curTextSize && curLineCount == 1) {
- // If we're in fixed mode, going up in text size and currently have 1 line
- // then it's very easy to get into an infinite recursion.
- // To combat that we check to see if the change in text size
- // will cause a line count change. If so, abort the size change and stick
- // to the smaller size.
- final Layout layout = mTextView.getLayout();
- if (layout == null || approximateLineWidth(layout, 0, textSize)
- > getMeasuredWidth() - getPaddingLeft() - getPaddingRight()) {
- updateTextView = false;
- }
- }
-
- if (updateTextView) {
- mTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
- mTextView.setMaxLines(maxLines);
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
- }
- }
-
- void setTab(@Nullable final Tab tab) {
- if (tab != mTab) {
- mTab = tab;
- update();
- }
- }
-
- void reset() {
- setTab(null);
- setSelected(false);
- }
-
- final void update() {
- final Tab tab = mTab;
- final View custom = tab != null ? tab.getCustomView() : null;
- if (custom != null) {
- final ViewParent customParent = custom.getParent();
- if (customParent != this) {
- if (customParent != null) {
- ((ViewGroup) customParent).removeView(custom);
- }
- addView(custom);
- }
- mCustomView = custom;
- if (mTextView != null) {
- mTextView.setVisibility(GONE);
- }
- if (mIconView != null) {
- mIconView.setVisibility(GONE);
- mIconView.setImageDrawable(null);
- }
-
- mCustomTextView = (TextView) custom.findViewById(android.R.id.text1);
- if (mCustomTextView != null) {
- mDefaultMaxLines = TextViewCompat.getMaxLines(mCustomTextView);
- }
- mCustomIconView = (ImageView) custom.findViewById(android.R.id.icon);
- } else {
- // We do not have a custom view. Remove one if it already exists
- if (mCustomView != null) {
- removeView(mCustomView);
- mCustomView = null;
- }
- mCustomTextView = null;
- mCustomIconView = null;
- }
-
- if (mCustomView == null) {
- // If there isn't a custom view, we'll us our own in-built layouts
- if (mIconView == null) {
- ImageView iconView = (ImageView) LayoutInflater.from(getContext())
- .inflate(R.layout.design_layout_tab_icon, this, false);
- addView(iconView, 0);
- mIconView = iconView;
- }
- if (mTextView == null) {
- TextView textView = (TextView) LayoutInflater.from(getContext())
- .inflate(R.layout.design_layout_tab_text, this, false);
- addView(textView);
- mTextView = textView;
- mDefaultMaxLines = TextViewCompat.getMaxLines(mTextView);
- }
- TextViewCompat.setTextAppearance(mTextView, mTabTextAppearance);
- if (mTabTextColors != null) {
- mTextView.setTextColor(mTabTextColors);
- }
- updateTextAndIcon(mTextView, mIconView);
- } else {
- // Else, we'll see if there is a TextView or ImageView present and update them
- if (mCustomTextView != null || mCustomIconView != null) {
- updateTextAndIcon(mCustomTextView, mCustomIconView);
- }
- }
-
- // Finally update our selected state
- setSelected(tab != null && tab.isSelected());
- }
-
- private void updateTextAndIcon(@Nullable final TextView textView,
- @Nullable final ImageView iconView) {
- final Drawable icon = mTab != null ? mTab.getIcon() : null;
- final CharSequence text = mTab != null ? mTab.getText() : null;
- final CharSequence contentDesc = mTab != null ? mTab.getContentDescription() : null;
-
- if (iconView != null) {
- if (icon != null) {
- iconView.setImageDrawable(icon);
- iconView.setVisibility(VISIBLE);
- setVisibility(VISIBLE);
- } else {
- iconView.setVisibility(GONE);
- iconView.setImageDrawable(null);
- }
- iconView.setContentDescription(contentDesc);
- }
-
- final boolean hasText = !TextUtils.isEmpty(text);
- if (textView != null) {
- if (hasText) {
- textView.setText(text);
- textView.setVisibility(VISIBLE);
- setVisibility(VISIBLE);
- } else {
- textView.setVisibility(GONE);
- textView.setText(null);
- }
- textView.setContentDescription(contentDesc);
- }
-
- if (iconView != null) {
- MarginLayoutParams lp = ((MarginLayoutParams) iconView.getLayoutParams());
- int bottomMargin = 0;
- if (hasText && iconView.getVisibility() == VISIBLE) {
- // If we're showing both text and icon, add some margin bottom to the icon
- bottomMargin = dpToPx(DEFAULT_GAP_TEXT_ICON);
- }
- if (bottomMargin != lp.bottomMargin) {
- lp.bottomMargin = bottomMargin;
- iconView.requestLayout();
- }
- }
- TooltipCompat.setTooltipText(this, hasText ? null : contentDesc);
- }
-
- public Tab getTab() {
- return mTab;
- }
-
- /**
- * Approximates a given lines width with the new provided text size.
- */
- private float approximateLineWidth(Layout layout, int line, float textSize) {
- return layout.getLineWidth(line) * (textSize / layout.getPaint().getTextSize());
- }
- }
-
- private class SlidingTabStrip extends LinearLayout {
- private int mSelectedIndicatorHeight;
- private final Paint mSelectedIndicatorPaint;
-
- int mSelectedPosition = -1;
- float mSelectionOffset;
-
- private int mLayoutDirection = -1;
-
- private int mIndicatorLeft = -1;
- private int mIndicatorRight = -1;
-
- private ValueAnimator mIndicatorAnimator;
-
- SlidingTabStrip(Context context) {
- super(context);
- setWillNotDraw(false);
- mSelectedIndicatorPaint = new Paint();
- }
-
- void setSelectedIndicatorColor(int color) {
- if (mSelectedIndicatorPaint.getColor() != color) {
- mSelectedIndicatorPaint.setColor(color);
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- void setSelectedIndicatorHeight(int height) {
- if (mSelectedIndicatorHeight != height) {
- mSelectedIndicatorHeight = height;
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- boolean childrenNeedLayout() {
- for (int i = 0, z = getChildCount(); i < z; i++) {
- final View child = getChildAt(i);
- if (child.getWidth() <= 0) {
- return true;
- }
- }
- return false;
- }
-
- void setIndicatorPositionFromTabPosition(int position, float positionOffset) {
- if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
- mIndicatorAnimator.cancel();
- }
-
- mSelectedPosition = position;
- mSelectionOffset = positionOffset;
- updateIndicatorPosition();
- }
-
- float getIndicatorPosition() {
- return mSelectedPosition + mSelectionOffset;
- }
-
- @Override
- public void onRtlPropertiesChanged(int layoutDirection) {
- super.onRtlPropertiesChanged(layoutDirection);
-
- // Workaround for a bug before Android M where LinearLayout did not relayout itself when
- // layout direction changed.
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
- //noinspection WrongConstant
- if (mLayoutDirection != layoutDirection) {
- requestLayout();
- mLayoutDirection = layoutDirection;
- }
- }
- }
-
- @Override
- protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
-
- if (MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY) {
- // HorizontalScrollView will first measure use with UNSPECIFIED, and then with
- // EXACTLY. Ignore the first call since anything we do will be overwritten anyway
- return;
- }
-
- if (mMode == MODE_FIXED && mTabGravity == GRAVITY_CENTER) {
- final int count = getChildCount();
-
- // First we'll find the widest tab
- int largestTabWidth = 0;
- for (int i = 0, z = count; i < z; i++) {
- View child = getChildAt(i);
- if (child.getVisibility() == VISIBLE) {
- largestTabWidth = Math.max(largestTabWidth, child.getMeasuredWidth());
- }
- }
-
- if (largestTabWidth <= 0) {
- // If we don't have a largest child yet, skip until the next measure pass
- return;
- }
-
- final int gutter = dpToPx(FIXED_WRAP_GUTTER_MIN);
- boolean remeasure = false;
-
- if (largestTabWidth * count <= getMeasuredWidth() - gutter * 2) {
- // If the tabs fit within our width minus gutters, we will set all tabs to have
- // the same width
- for (int i = 0; i < count; i++) {
- final LinearLayout.LayoutParams lp =
- (LayoutParams) getChildAt(i).getLayoutParams();
- if (lp.width != largestTabWidth || lp.weight != 0) {
- lp.width = largestTabWidth;
- lp.weight = 0;
- remeasure = true;
- }
- }
- } else {
- // If the tabs will wrap to be larger than the width minus gutters, we need
- // to switch to GRAVITY_FILL
- mTabGravity = GRAVITY_FILL;
- updateTabViews(false);
- remeasure = true;
- }
-
- if (remeasure) {
- // Now re-measure after our changes
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int l, int t, int r, int b) {
- super.onLayout(changed, l, t, r, b);
-
- if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
- // If we're currently running an animation, lets cancel it and start a
- // new animation with the remaining duration
- mIndicatorAnimator.cancel();
- final long duration = mIndicatorAnimator.getDuration();
- animateIndicatorToPosition(mSelectedPosition,
- Math.round((1f - mIndicatorAnimator.getAnimatedFraction()) * duration));
- } else {
- // If we've been layed out, update the indicator position
- updateIndicatorPosition();
- }
- }
-
- private void updateIndicatorPosition() {
- final View selectedTitle = getChildAt(mSelectedPosition);
- int left, right;
-
- if (selectedTitle != null && selectedTitle.getWidth() > 0) {
- left = selectedTitle.getLeft();
- right = selectedTitle.getRight();
-
- if (mSelectionOffset > 0f && mSelectedPosition < getChildCount() - 1) {
- // Draw the selection partway between the tabs
- View nextTitle = getChildAt(mSelectedPosition + 1);
- left = (int) (mSelectionOffset * nextTitle.getLeft() +
- (1.0f - mSelectionOffset) * left);
- right = (int) (mSelectionOffset * nextTitle.getRight() +
- (1.0f - mSelectionOffset) * right);
- }
- } else {
- left = right = -1;
- }
-
- setIndicatorPosition(left, right);
- }
-
- void setIndicatorPosition(int left, int right) {
- if (left != mIndicatorLeft || right != mIndicatorRight) {
- // If the indicator's left/right has changed, invalidate
- mIndicatorLeft = left;
- mIndicatorRight = right;
- ViewCompat.postInvalidateOnAnimation(this);
- }
- }
-
- void animateIndicatorToPosition(final int position, int duration) {
- if (mIndicatorAnimator != null && mIndicatorAnimator.isRunning()) {
- mIndicatorAnimator.cancel();
- }
-
- final boolean isRtl = ViewCompat.getLayoutDirection(this)
- == ViewCompat.LAYOUT_DIRECTION_RTL;
-
- final View targetView = getChildAt(position);
- if (targetView == null) {
- // If we don't have a view, just update the position now and return
- updateIndicatorPosition();
- return;
- }
-
- final int targetLeft = targetView.getLeft();
- final int targetRight = targetView.getRight();
- final int startLeft;
- final int startRight;
-
- if (Math.abs(position - mSelectedPosition) <= 1) {
- // If the views are adjacent, we'll animate from edge-to-edge
- startLeft = mIndicatorLeft;
- startRight = mIndicatorRight;
- } else {
- // Else, we'll just grow from the nearest edge
- final int offset = dpToPx(MOTION_NON_ADJACENT_OFFSET);
- if (position < mSelectedPosition) {
- // We're going end-to-start
- if (isRtl) {
- startLeft = startRight = targetLeft - offset;
- } else {
- startLeft = startRight = targetRight + offset;
- }
- } else {
- // We're going start-to-end
- if (isRtl) {
- startLeft = startRight = targetRight + offset;
- } else {
- startLeft = startRight = targetLeft - offset;
- }
- }
- }
-
- if (startLeft != targetLeft || startRight != targetRight) {
- ValueAnimator animator = mIndicatorAnimator = new ValueAnimator();
- animator.setInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
- animator.setDuration(duration);
- animator.setFloatValues(0, 1);
- animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- final float fraction = animator.getAnimatedFraction();
- setIndicatorPosition(
- AnimationUtils.lerp(startLeft, targetLeft, fraction),
- AnimationUtils.lerp(startRight, targetRight, fraction));
- }
- });
- animator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- mSelectedPosition = position;
- mSelectionOffset = 0f;
- }
- });
- animator.start();
- }
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
-
- // Thick colored underline below the current selection
- if (mIndicatorLeft >= 0 && mIndicatorRight > mIndicatorLeft) {
- canvas.drawRect(mIndicatorLeft, getHeight() - mSelectedIndicatorHeight,
- mIndicatorRight, getHeight(), mSelectedIndicatorPaint);
- }
- }
- }
-
- private static ColorStateList createColorStateList(int defaultColor, int selectedColor) {
- final int[][] states = new int[2][];
- final int[] colors = new int[2];
- int i = 0;
-
- states[i] = SELECTED_STATE_SET;
- colors[i] = selectedColor;
- i++;
-
- // Default enabled state
- states[i] = EMPTY_STATE_SET;
- colors[i] = defaultColor;
- i++;
-
- return new ColorStateList(states, colors);
- }
-
- private int getDefaultHeight() {
- boolean hasIconAndText = false;
- for (int i = 0, count = mTabs.size(); i < count; i++) {
- Tab tab = mTabs.get(i);
- if (tab != null && tab.getIcon() != null && !TextUtils.isEmpty(tab.getText())) {
- hasIconAndText = true;
- break;
- }
- }
- return hasIconAndText ? DEFAULT_HEIGHT_WITH_TEXT_ICON : DEFAULT_HEIGHT;
- }
-
- private int getTabMinWidth() {
- if (mRequestedTabMinWidth != INVALID_WIDTH) {
- // If we have been given a min width, use it
- return mRequestedTabMinWidth;
- }
- // Else, we'll use the default value
- return mMode == MODE_SCROLLABLE ? mScrollableTabMinWidth : 0;
- }
-
- @Override
- public LayoutParams generateLayoutParams(AttributeSet attrs) {
- // We don't care about the layout params of any views added to us, since we don't actually
- // add them. The only view we add is the SlidingTabStrip, which is done manually.
- // We return the default layout params so that we don't blow up if we're given a TabItem
- // without android:layout_* values.
- return generateDefaultLayoutParams();
- }
-
- int getTabMaxWidth() {
- return mTabMaxWidth;
- }
-
- /**
- * A {@link ViewPager.OnPageChangeListener} class which contains the
- * necessary calls back to the provided {@link TabLayout} so that the tab position is
- * kept in sync.
- *
- * <p>This class stores the provided TabLayout weakly, meaning that you can use
- * {@link ViewPager#addOnPageChangeListener(ViewPager.OnPageChangeListener)
- * addOnPageChangeListener(OnPageChangeListener)} without removing the listener and
- * not cause a leak.
- */
- public static class TabLayoutOnPageChangeListener implements ViewPager.OnPageChangeListener {
- private final WeakReference<TabLayout> mTabLayoutRef;
- private int mPreviousScrollState;
- private int mScrollState;
-
- public TabLayoutOnPageChangeListener(TabLayout tabLayout) {
- mTabLayoutRef = new WeakReference<>(tabLayout);
- }
-
- @Override
- public void onPageScrollStateChanged(final int state) {
- mPreviousScrollState = mScrollState;
- mScrollState = state;
- }
-
- @Override
- public void onPageScrolled(final int position, final float positionOffset,
- final int positionOffsetPixels) {
- final TabLayout tabLayout = mTabLayoutRef.get();
- if (tabLayout != null) {
- // Only update the text selection if we're not settling, or we are settling after
- // being dragged
- final boolean updateText = mScrollState != SCROLL_STATE_SETTLING ||
- mPreviousScrollState == SCROLL_STATE_DRAGGING;
- // Update the indicator if we're not settling after being idle. This is caused
- // from a setCurrentItem() call and will be handled by an animation from
- // onPageSelected() instead.
- final boolean updateIndicator = !(mScrollState == SCROLL_STATE_SETTLING
- && mPreviousScrollState == SCROLL_STATE_IDLE);
- tabLayout.setScrollPosition(position, positionOffset, updateText, updateIndicator);
- }
- }
-
- @Override
- public void onPageSelected(final int position) {
- final TabLayout tabLayout = mTabLayoutRef.get();
- if (tabLayout != null && tabLayout.getSelectedTabPosition() != position
- && position < tabLayout.getTabCount()) {
- // Select the tab, only updating the indicator if we're not being dragged/settled
- // (since onPageScrolled will handle that).
- final boolean updateIndicator = mScrollState == SCROLL_STATE_IDLE
- || (mScrollState == SCROLL_STATE_SETTLING
- && mPreviousScrollState == SCROLL_STATE_IDLE);
- tabLayout.selectTab(tabLayout.getTabAt(position), updateIndicator);
- }
- }
-
- void reset() {
- mPreviousScrollState = mScrollState = SCROLL_STATE_IDLE;
- }
- }
-
- /**
- * A {@link TabLayout.OnTabSelectedListener} class which contains the necessary calls back
- * to the provided {@link ViewPager} so that the tab position is kept in sync.
- */
- public static class ViewPagerOnTabSelectedListener implements TabLayout.OnTabSelectedListener {
- private final ViewPager mViewPager;
-
- public ViewPagerOnTabSelectedListener(ViewPager viewPager) {
- mViewPager = viewPager;
- }
-
- @Override
- public void onTabSelected(TabLayout.Tab tab) {
- mViewPager.setCurrentItem(tab.getPosition());
- }
-
- @Override
- public void onTabUnselected(TabLayout.Tab tab) {
- // No-op
- }
-
- @Override
- public void onTabReselected(TabLayout.Tab tab) {
- // No-op
- }
- }
-
- private class PagerAdapterObserver extends DataSetObserver {
- PagerAdapterObserver() {
- }
-
- @Override
- public void onChanged() {
- populateFromPagerAdapter();
- }
-
- @Override
- public void onInvalidated() {
- populateFromPagerAdapter();
- }
- }
-
- private class AdapterChangeListener implements ViewPager.OnAdapterChangeListener {
- private boolean mAutoRefresh;
-
- AdapterChangeListener() {
- }
-
- @Override
- public void onAdapterChanged(@NonNull ViewPager viewPager,
- @Nullable PagerAdapter oldAdapter, @Nullable PagerAdapter newAdapter) {
- if (mViewPager == viewPager) {
- setPagerAdapter(newAdapter, mAutoRefresh);
- }
- }
-
- void setAutoRefresh(boolean autoRefresh) {
- mAutoRefresh = autoRefresh;
- }
- }
-}
diff --git a/android/support/design/widget/TextInputEditText.java b/android/support/design/widget/TextInputEditText.java
deleted file mode 100644
index ee6c32cd..00000000
--- a/android/support/design/widget/TextInputEditText.java
+++ /dev/null
@@ -1,65 +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.support.design.widget;
-
-import android.content.Context;
-import android.support.v7.widget.AppCompatEditText;
-import android.support.v7.widget.WithHint;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewParent;
-import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputConnection;
-
-/**
- * A special sub-class of {@link android.widget.EditText} designed for use as a child of
- * {@link TextInputLayout}.
- *
- * <p>Using this class allows us to display a hint in the IME when in 'extract' mode.</p>
- */
-public class TextInputEditText extends AppCompatEditText {
-
- public TextInputEditText(Context context) {
- super(context);
- }
-
- public TextInputEditText(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- public TextInputEditText(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- }
-
- @Override
- public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
- final InputConnection ic = super.onCreateInputConnection(outAttrs);
- if (ic != null && outAttrs.hintText == null) {
- // If we don't have a hint and our parent implements WithHint, use its hint for the
- // EditorInfo. This allows us to display a hint in 'extract mode'.
- ViewParent parent = getParent();
- while (parent instanceof View) {
- if (parent instanceof WithHint) {
- outAttrs.hintText = ((WithHint) parent).getHint();
- break;
- }
- parent = parent.getParent();
- }
- }
- return ic;
- }
-}
diff --git a/android/support/design/widget/TextInputLayout.java b/android/support/design/widget/TextInputLayout.java
deleted file mode 100644
index 0540678e..00000000
--- a/android/support/design/widget/TextInputLayout.java
+++ /dev/null
@@ -1,1530 +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 android.support.design.widget;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.content.Context;
-import android.content.res.ColorStateList;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.Rect;
-import android.graphics.Typeface;
-import android.graphics.drawable.ColorDrawable;
-import android.graphics.drawable.Drawable;
-import android.graphics.drawable.DrawableContainer;
-import android.os.Build;
-import android.os.Parcel;
-import android.os.Parcelable;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import android.support.annotation.StringRes;
-import android.support.annotation.StyleRes;
-import android.support.annotation.VisibleForTesting;
-import android.support.design.R;
-import android.support.v4.content.ContextCompat;
-import android.support.v4.graphics.drawable.DrawableCompat;
-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.accessibility.AccessibilityNodeInfoCompat;
-import android.support.v4.widget.Space;
-import android.support.v4.widget.TextViewCompat;
-import android.support.v4.widget.ViewGroupUtils;
-import android.support.v7.content.res.AppCompatResources;
-import android.support.v7.widget.AppCompatDrawableManager;
-import android.support.v7.widget.AppCompatTextView;
-import android.support.v7.widget.TintTypedArray;
-import android.support.v7.widget.WithHint;
-import android.text.Editable;
-import android.text.TextUtils;
-import android.text.TextWatcher;
-import android.text.method.PasswordTransformationMethod;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.util.SparseArray;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewStructure;
-import android.view.accessibility.AccessibilityEvent;
-import android.view.animation.AccelerateInterpolator;
-import android.widget.EditText;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-/**
- * Layout which wraps an {@link android.widget.EditText} (or descendant) to show a floating label
- * when the hint is hidden due to the user inputting text.
- *
- * <p>Also supports showing an error via {@link #setErrorEnabled(boolean)} and
- * {@link #setError(CharSequence)}, and a character counter via
- * {@link #setCounterEnabled(boolean)}.</p>
- *
- * <p>Password visibility toggling is also supported via the
- * {@link #setPasswordVisibilityToggleEnabled(boolean)} API and related attribute.
- * If enabled, a button is displayed to toggle between the password being displayed as plain-text
- * or disguised, when your EditText is set to display a password.</p>
- *
- * <p><strong>Note:</strong> When using the password toggle functionality, the 'end' compound
- * drawable of the EditText will be overridden while the toggle is enabled. To ensure that any
- * existing drawables are restored correctly, you should set those compound drawables relatively
- * (start/end), opposed to absolutely (left/right).</p>
- *
- * The {@link TextInputEditText} class is provided to be used as a child of this layout. Using
- * TextInputEditText allows TextInputLayout greater control over the visual aspects of any
- * text input. An example usage is as so:
- *
- * <pre>
- * &lt;android.support.design.widget.TextInputLayout
- * android:layout_width=&quot;match_parent&quot;
- * android:layout_height=&quot;wrap_content&quot;&gt;
- *
- * &lt;android.support.design.widget.TextInputEditText
- * android:layout_width=&quot;match_parent&quot;
- * android:layout_height=&quot;wrap_content&quot;
- * android:hint=&quot;@string/form_username&quot;/&gt;
- *
- * &lt;/android.support.design.widget.TextInputLayout&gt;
- * </pre>
- *
- * <p><strong>Note:</strong> The actual view hierarchy present under TextInputLayout is
- * <strong>NOT</strong> guaranteed to match the view hierarchy as written in XML. As a result,
- * calls to getParent() on children of the TextInputLayout -- such as an TextInputEditText --
- * may not return the TextInputLayout itself, but rather an intermediate View. If you need
- * to access a View directly, set an {@code android:id} and use {@link View#findViewById(int)}.
- */
-public class TextInputLayout extends LinearLayout implements WithHint {
-
- private static final int ANIMATION_DURATION = 200;
- private static final int INVALID_MAX_LENGTH = -1;
-
- private static final String LOG_TAG = "TextInputLayout";
-
- private final FrameLayout mInputFrame;
- EditText mEditText;
- private CharSequence mOriginalHint;
-
- private boolean mHintEnabled;
- private CharSequence mHint;
-
- private Paint mTmpPaint;
- private final Rect mTmpRect = new Rect();
-
- private LinearLayout mIndicatorArea;
- private int mIndicatorsAdded;
-
- private Typeface mTypeface;
-
- private boolean mErrorEnabled;
- TextView mErrorView;
- private int mErrorTextAppearance;
- private boolean mErrorShown;
- private CharSequence mError;
-
- boolean mCounterEnabled;
- private TextView mCounterView;
- private int mCounterMaxLength;
- private int mCounterTextAppearance;
- private int mCounterOverflowTextAppearance;
- private boolean mCounterOverflowed;
-
- private boolean mPasswordToggleEnabled;
- private Drawable mPasswordToggleDrawable;
- private CharSequence mPasswordToggleContentDesc;
- private CheckableImageButton mPasswordToggleView;
- private boolean mPasswordToggledVisible;
- private Drawable mPasswordToggleDummyDrawable;
- private Drawable mOriginalEditTextEndDrawable;
-
- private ColorStateList mPasswordToggleTintList;
- private boolean mHasPasswordToggleTintList;
- private PorterDuff.Mode mPasswordToggleTintMode;
- private boolean mHasPasswordToggleTintMode;
-
- private ColorStateList mDefaultTextColor;
- private ColorStateList mFocusedTextColor;
-
- // Only used for testing
- private boolean mHintExpanded;
-
- final CollapsingTextHelper mCollapsingTextHelper = new CollapsingTextHelper(this);
-
- private boolean mHintAnimationEnabled;
- private ValueAnimator mAnimator;
-
- private boolean mHasReconstructedEditTextBackground;
- private boolean mInDrawableStateChanged;
-
- private boolean mRestoringSavedState;
-
- public TextInputLayout(Context context) {
- this(context, null);
- }
-
- public TextInputLayout(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public TextInputLayout(Context context, AttributeSet attrs, int defStyleAttr) {
- // Can't call through to super(Context, AttributeSet, int) since it doesn't exist on API 10
- super(context, attrs);
-
- ThemeUtils.checkAppCompatTheme(context);
-
- setOrientation(VERTICAL);
- setWillNotDraw(false);
- setAddStatesFromChildren(true);
-
- mInputFrame = new FrameLayout(context);
- mInputFrame.setAddStatesFromChildren(true);
- addView(mInputFrame);
-
- mCollapsingTextHelper.setTextSizeInterpolator(AnimationUtils.FAST_OUT_SLOW_IN_INTERPOLATOR);
- mCollapsingTextHelper.setPositionInterpolator(new AccelerateInterpolator());
- mCollapsingTextHelper.setCollapsedTextGravity(Gravity.TOP | GravityCompat.START);
-
- final TintTypedArray a = TintTypedArray.obtainStyledAttributes(context, attrs,
- R.styleable.TextInputLayout, defStyleAttr, R.style.Widget_Design_TextInputLayout);
- mHintEnabled = a.getBoolean(R.styleable.TextInputLayout_hintEnabled, true);
- setHint(a.getText(R.styleable.TextInputLayout_android_hint));
- mHintAnimationEnabled = a.getBoolean(
- R.styleable.TextInputLayout_hintAnimationEnabled, true);
-
- if (a.hasValue(R.styleable.TextInputLayout_android_textColorHint)) {
- mDefaultTextColor = mFocusedTextColor =
- a.getColorStateList(R.styleable.TextInputLayout_android_textColorHint);
- }
-
- final int hintAppearance = a.getResourceId(
- R.styleable.TextInputLayout_hintTextAppearance, -1);
- if (hintAppearance != -1) {
- setHintTextAppearance(
- a.getResourceId(R.styleable.TextInputLayout_hintTextAppearance, 0));
- }
-
- mErrorTextAppearance = a.getResourceId(R.styleable.TextInputLayout_errorTextAppearance, 0);
- final boolean errorEnabled = a.getBoolean(R.styleable.TextInputLayout_errorEnabled, false);
-
- final boolean counterEnabled = a.getBoolean(
- R.styleable.TextInputLayout_counterEnabled, false);
- setCounterMaxLength(
- a.getInt(R.styleable.TextInputLayout_counterMaxLength, INVALID_MAX_LENGTH));
- mCounterTextAppearance = a.getResourceId(
- R.styleable.TextInputLayout_counterTextAppearance, 0);
- mCounterOverflowTextAppearance = a.getResourceId(
- R.styleable.TextInputLayout_counterOverflowTextAppearance, 0);
-
- mPasswordToggleEnabled = a.getBoolean(
- R.styleable.TextInputLayout_passwordToggleEnabled, false);
- mPasswordToggleDrawable = a.getDrawable(R.styleable.TextInputLayout_passwordToggleDrawable);
- mPasswordToggleContentDesc = a.getText(
- R.styleable.TextInputLayout_passwordToggleContentDescription);
- if (a.hasValue(R.styleable.TextInputLayout_passwordToggleTint)) {
- mHasPasswordToggleTintList = true;
- mPasswordToggleTintList = a.getColorStateList(
- R.styleable.TextInputLayout_passwordToggleTint);
- }
- if (a.hasValue(R.styleable.TextInputLayout_passwordToggleTintMode)) {
- mHasPasswordToggleTintMode = true;
- mPasswordToggleTintMode = ViewUtils.parseTintMode(
- a.getInt(R.styleable.TextInputLayout_passwordToggleTintMode, -1), null);
- }
-
- a.recycle();
-
- setErrorEnabled(errorEnabled);
- setCounterEnabled(counterEnabled);
- applyPasswordToggleTint();
-
- if (ViewCompat.getImportantForAccessibility(this)
- == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
- // Make sure we're important for accessibility if we haven't been explicitly not
- ViewCompat.setImportantForAccessibility(this,
- ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_YES);
- }
-
- ViewCompat.setAccessibilityDelegate(this, new TextInputAccessibilityDelegate());
- }
-
- @Override
- public void addView(View child, int index, final ViewGroup.LayoutParams params) {
- if (child instanceof EditText) {
- // Make sure that the EditText is vertically at the bottom, so that it sits on the
- // EditText's underline
- FrameLayout.LayoutParams flp = new FrameLayout.LayoutParams(params);
- flp.gravity = Gravity.CENTER_VERTICAL | (flp.gravity & ~Gravity.VERTICAL_GRAVITY_MASK);
- mInputFrame.addView(child, flp);
-
- // Now use the EditText's LayoutParams as our own and update them to make enough space
- // for the label
- mInputFrame.setLayoutParams(params);
- updateInputLayoutMargins();
-
- setEditText((EditText) child);
- } else {
- // Carry on adding the View...
- super.addView(child, index, params);
- }
- }
-
- /**
- * Set the typeface to use for the hint and any label views (such as counter and error views).
- *
- * @param typeface typeface to use, or {@code null} to use the default.
- */
- public void setTypeface(@Nullable Typeface typeface) {
- if ((mTypeface != null && !mTypeface.equals(typeface))
- || (mTypeface == null && typeface != null)) {
- mTypeface = typeface;
-
- mCollapsingTextHelper.setTypefaces(typeface);
- if (mCounterView != null) {
- mCounterView.setTypeface(typeface);
- }
- if (mErrorView != null) {
- mErrorView.setTypeface(typeface);
- }
- }
- }
-
- /**
- * Returns the typeface used for the hint and any label views (such as counter and error views).
- */
- @NonNull
- public Typeface getTypeface() {
- return mTypeface;
- }
-
- @Override
- public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) {
- if (mOriginalHint == null || mEditText == null) {
- super.dispatchProvideAutofillStructure(structure, flags);
- return;
- }
-
- // Temporarily sets child's hint to its original value so it is properly set in the
- // child's ViewStructure.
- final CharSequence hint = mEditText.getHint();
- mEditText.setHint(mOriginalHint);
- try {
- super.dispatchProvideAutofillStructure(structure, flags);
- } finally {
- mEditText.setHint(hint);
- }
- }
-
- private void setEditText(EditText editText) {
- // If we already have an EditText, throw an exception
- if (mEditText != null) {
- throw new IllegalArgumentException("We already have an EditText, can only have one");
- }
-
- if (!(editText instanceof TextInputEditText)) {
- Log.i(LOG_TAG, "EditText added is not a TextInputEditText. Please switch to using that"
- + " class instead.");
- }
-
- mEditText = editText;
-
- final boolean hasPasswordTransformation = hasPasswordTransformation();
-
- // Use the EditText's typeface, and it's text size for our expanded text
- if (!hasPasswordTransformation) {
- // We don't want a monospace font just because we have a password field
- mCollapsingTextHelper.setTypefaces(mEditText.getTypeface());
- }
- mCollapsingTextHelper.setExpandedTextSize(mEditText.getTextSize());
-
- final int editTextGravity = mEditText.getGravity();
- mCollapsingTextHelper.setCollapsedTextGravity(
- Gravity.TOP | (editTextGravity & ~Gravity.VERTICAL_GRAVITY_MASK));
- mCollapsingTextHelper.setExpandedTextGravity(editTextGravity);
-
- // Add a TextWatcher so that we know when the text input has changed
- mEditText.addTextChangedListener(new TextWatcher() {
- @Override
- public void afterTextChanged(Editable s) {
- updateLabelState(!mRestoringSavedState);
- if (mCounterEnabled) {
- updateCounter(s.length());
- }
- }
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {}
- });
-
- // Use the EditText's hint colors if we don't have one set
- if (mDefaultTextColor == null) {
- mDefaultTextColor = mEditText.getHintTextColors();
- }
-
- // If we do not have a valid hint, try and retrieve it from the EditText, if enabled
- if (mHintEnabled && TextUtils.isEmpty(mHint)) {
- // Save the hint so it can be restored on dispatchProvideAutofillStructure();
- mOriginalHint = mEditText.getHint();
- setHint(mOriginalHint);
- // Clear the EditText's hint as we will display it ourselves
- mEditText.setHint(null);
- }
-
- if (mCounterView != null) {
- updateCounter(mEditText.getText().length());
- }
-
- if (mIndicatorArea != null) {
- adjustIndicatorPadding();
- }
-
- updatePasswordToggleView();
-
- // Update the label visibility with no animation, but force a state change
- updateLabelState(false, true);
- }
-
- private void updateInputLayoutMargins() {
- // Create/update the LayoutParams so that we can add enough top margin
- // to the EditText so make room for the label
- final LayoutParams lp = (LayoutParams) mInputFrame.getLayoutParams();
- final int newTopMargin;
-
- if (mHintEnabled) {
- if (mTmpPaint == null) {
- mTmpPaint = new Paint();
- }
- mTmpPaint.setTypeface(mCollapsingTextHelper.getCollapsedTypeface());
- mTmpPaint.setTextSize(mCollapsingTextHelper.getCollapsedTextSize());
- newTopMargin = (int) -mTmpPaint.ascent();
- } else {
- newTopMargin = 0;
- }
-
- if (newTopMargin != lp.topMargin) {
- lp.topMargin = newTopMargin;
- mInputFrame.requestLayout();
- }
- }
-
- void updateLabelState(boolean animate) {
- updateLabelState(animate, false);
- }
-
- void updateLabelState(final boolean animate, final boolean force) {
- final boolean isEnabled = isEnabled();
- final boolean hasText = mEditText != null && !TextUtils.isEmpty(mEditText.getText());
- final boolean isFocused = arrayContains(getDrawableState(), android.R.attr.state_focused);
- final boolean isErrorShowing = !TextUtils.isEmpty(getError());
-
- if (mDefaultTextColor != null) {
- mCollapsingTextHelper.setExpandedTextColor(mDefaultTextColor);
- }
-
- if (isEnabled && mCounterOverflowed && mCounterView != null) {
- mCollapsingTextHelper.setCollapsedTextColor(mCounterView.getTextColors());
- } else if (isEnabled && isFocused && mFocusedTextColor != null) {
- mCollapsingTextHelper.setCollapsedTextColor(mFocusedTextColor);
- } else if (mDefaultTextColor != null) {
- mCollapsingTextHelper.setCollapsedTextColor(mDefaultTextColor);
- }
-
- if (hasText || (isEnabled() && (isFocused || isErrorShowing))) {
- // We should be showing the label so do so if it isn't already
- if (force || mHintExpanded) {
- collapseHint(animate);
- }
- } else {
- // We should not be showing the label so hide it
- if (force || !mHintExpanded) {
- expandHint(animate);
- }
- }
- }
-
- /**
- * Returns the {@link android.widget.EditText} used for text input.
- */
- @Nullable
- public EditText getEditText() {
- return mEditText;
- }
-
- /**
- * Set the hint to be displayed in the floating label, if enabled.
- *
- * @see #setHintEnabled(boolean)
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_android_hint
- */
- public void setHint(@Nullable CharSequence hint) {
- if (mHintEnabled) {
- setHintInternal(hint);
- sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
- }
- }
-
- private void setHintInternal(CharSequence hint) {
- mHint = hint;
- mCollapsingTextHelper.setText(hint);
- }
-
- /**
- * Returns the hint which is displayed in the floating label, if enabled.
- *
- * @return the hint, or null if there isn't one set, or the hint is not enabled.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_android_hint
- */
- @Override
- @Nullable
- public CharSequence getHint() {
- return mHintEnabled ? mHint : null;
- }
-
- /**
- * Sets whether the floating label functionality is enabled or not in this layout.
- *
- * <p>If enabled, any non-empty hint in the child EditText will be moved into the floating
- * hint, and its existing hint will be cleared. If disabled, then any non-empty floating hint
- * in this layout will be moved into the EditText, and this layout's hint will be cleared.</p>
- *
- * @see #setHint(CharSequence)
- * @see #isHintEnabled()
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_hintEnabled
- */
- public void setHintEnabled(boolean enabled) {
- if (enabled != mHintEnabled) {
- mHintEnabled = enabled;
-
- final CharSequence editTextHint = mEditText.getHint();
- if (!mHintEnabled) {
- if (!TextUtils.isEmpty(mHint) && TextUtils.isEmpty(editTextHint)) {
- // If the hint is disabled, but we have a hint set, and the EditText doesn't,
- // pass it through...
- mEditText.setHint(mHint);
- }
- // Now clear out any set hint
- setHintInternal(null);
- } else {
- if (!TextUtils.isEmpty(editTextHint)) {
- // If the hint is now enabled and the EditText has one set, we'll use it if
- // we don't already have one, and clear the EditText's
- if (TextUtils.isEmpty(mHint)) {
- setHint(editTextHint);
- }
- mEditText.setHint(null);
- }
- }
-
- // Now update the EditText top margin
- if (mEditText != null) {
- updateInputLayoutMargins();
- }
- }
- }
-
- /**
- * Returns whether the floating label functionality is enabled or not in this layout.
- *
- * @see #setHintEnabled(boolean)
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_hintEnabled
- */
- public boolean isHintEnabled() {
- return mHintEnabled;
- }
-
- /**
- * Sets the hint text color, size, style from the specified TextAppearance resource.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_hintTextAppearance
- */
- public void setHintTextAppearance(@StyleRes int resId) {
- mCollapsingTextHelper.setCollapsedTextAppearance(resId);
- mFocusedTextColor = mCollapsingTextHelper.getCollapsedTextColor();
-
- if (mEditText != null) {
- updateLabelState(false);
- // Text size might have changed so update the top margin
- updateInputLayoutMargins();
- }
- }
-
- private void addIndicator(TextView indicator, int index) {
- if (mIndicatorArea == null) {
- mIndicatorArea = new LinearLayout(getContext());
- mIndicatorArea.setOrientation(LinearLayout.HORIZONTAL);
- addView(mIndicatorArea, LinearLayout.LayoutParams.MATCH_PARENT,
- LinearLayout.LayoutParams.WRAP_CONTENT);
-
- // Add a flexible spacer in the middle so that the left/right views stay pinned
- final Space spacer = new Space(getContext());
- final LinearLayout.LayoutParams spacerLp = new LinearLayout.LayoutParams(0, 0, 1f);
- mIndicatorArea.addView(spacer, spacerLp);
-
- if (mEditText != null) {
- adjustIndicatorPadding();
- }
- }
- mIndicatorArea.setVisibility(View.VISIBLE);
- mIndicatorArea.addView(indicator, index);
- mIndicatorsAdded++;
- }
-
- private void adjustIndicatorPadding() {
- // Add padding to the error and character counter so that they match the EditText
- ViewCompat.setPaddingRelative(mIndicatorArea, ViewCompat.getPaddingStart(mEditText),
- 0, ViewCompat.getPaddingEnd(mEditText), mEditText.getPaddingBottom());
- }
-
- private void removeIndicator(TextView indicator) {
- if (mIndicatorArea != null) {
- mIndicatorArea.removeView(indicator);
- if (--mIndicatorsAdded == 0) {
- mIndicatorArea.setVisibility(View.GONE);
- }
- }
- }
-
- /**
- * Whether the error functionality is enabled or not in this layout. Enabling this
- * functionality before setting an error message via {@link #setError(CharSequence)}, will mean
- * that this layout will not change size when an error is displayed.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_errorEnabled
- */
- public void setErrorEnabled(boolean enabled) {
- if (mErrorEnabled != enabled) {
- if (mErrorView != null) {
- mErrorView.animate().cancel();
- }
-
- if (enabled) {
- mErrorView = new AppCompatTextView(getContext());
- mErrorView.setId(R.id.textinput_error);
- if (mTypeface != null) {
- mErrorView.setTypeface(mTypeface);
- }
- boolean useDefaultColor = false;
- try {
- TextViewCompat.setTextAppearance(mErrorView, mErrorTextAppearance);
-
- if (Build.VERSION.SDK_INT >= 23
- && mErrorView.getTextColors().getDefaultColor() == Color.MAGENTA) {
- // Caused by our theme not extending from Theme.Design*. On API 23 and
- // above, unresolved theme attrs result in MAGENTA rather than an exception.
- // Flag so that we use a decent default
- useDefaultColor = true;
- }
- } catch (Exception e) {
- // Caused by our theme not extending from Theme.Design*. Flag so that we use
- // a decent default
- useDefaultColor = true;
- }
- if (useDefaultColor) {
- // Probably caused by our theme not extending from Theme.Design*. Instead
- // we manually set something appropriate
- TextViewCompat.setTextAppearance(mErrorView,
- android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption);
- mErrorView.setTextColor(ContextCompat.getColor(getContext(),
- android.support.v7.appcompat.R.color.error_color_material));
- }
- mErrorView.setVisibility(INVISIBLE);
- ViewCompat.setAccessibilityLiveRegion(mErrorView,
- ViewCompat.ACCESSIBILITY_LIVE_REGION_POLITE);
- addIndicator(mErrorView, 0);
- } else {
- mErrorShown = false;
- updateEditTextBackground();
- removeIndicator(mErrorView);
- mErrorView = null;
- }
- mErrorEnabled = enabled;
- }
- }
-
- /**
- * Sets the text color and size for the error message from the specified
- * TextAppearance resource.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_errorTextAppearance
- */
- public void setErrorTextAppearance(@StyleRes int resId) {
- mErrorTextAppearance = resId;
- if (mErrorView != null) {
- TextViewCompat.setTextAppearance(mErrorView, resId);
- }
- }
-
- /**
- * Returns whether the error functionality is enabled or not in this layout.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_errorEnabled
- *
- * @see #setErrorEnabled(boolean)
- */
- public boolean isErrorEnabled() {
- return mErrorEnabled;
- }
-
- /**
- * Sets an error message that will be displayed below our {@link EditText}. If the
- * {@code error} is {@code null}, the error message will be cleared.
- * <p>
- * If the error functionality has not been enabled via {@link #setErrorEnabled(boolean)}, then
- * it will be automatically enabled if {@code error} is not empty.
- *
- * @param error Error message to display, or null to clear
- *
- * @see #getError()
- */
- public void setError(@Nullable final CharSequence error) {
- // Only animate if we're enabled, laid out, and we have a different error message
- setError(error, ViewCompat.isLaidOut(this) && isEnabled()
- && (mErrorView == null || !TextUtils.equals(mErrorView.getText(), error)));
- }
-
- private void setError(@Nullable final CharSequence error, final boolean animate) {
- mError = error;
-
- if (!mErrorEnabled) {
- if (TextUtils.isEmpty(error)) {
- // If error isn't enabled, and the error is empty, just return
- return;
- }
- // Else, we'll assume that they want to enable the error functionality
- setErrorEnabled(true);
- }
-
- mErrorShown = !TextUtils.isEmpty(error);
-
- // Cancel any on-going animation
- mErrorView.animate().cancel();
-
- if (mErrorShown) {
- mErrorView.setText(error);
- mErrorView.setVisibility(VISIBLE);
-
- if (animate) {
- if (mErrorView.getAlpha() == 1f) {
- // If it's currently 100% show, we'll animate it from 0
- mErrorView.setAlpha(0f);
- }
- mErrorView.animate()
- .alpha(1f)
- .setDuration(ANIMATION_DURATION)
- .setInterpolator(AnimationUtils.LINEAR_OUT_SLOW_IN_INTERPOLATOR)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationStart(Animator animator) {
- mErrorView.setVisibility(VISIBLE);
- }
- }).start();
- } else {
- // Set alpha to 1f, just in case
- mErrorView.setAlpha(1f);
- }
- } else {
- if (mErrorView.getVisibility() == VISIBLE) {
- if (animate) {
- mErrorView.animate()
- .alpha(0f)
- .setDuration(ANIMATION_DURATION)
- .setInterpolator(AnimationUtils.FAST_OUT_LINEAR_IN_INTERPOLATOR)
- .setListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animator) {
- mErrorView.setText(error);
- mErrorView.setVisibility(INVISIBLE);
- }
- }).start();
- } else {
- mErrorView.setText(error);
- mErrorView.setVisibility(INVISIBLE);
- }
- }
- }
-
- updateEditTextBackground();
- updateLabelState(animate);
- }
-
- /**
- * Whether the character counter functionality is enabled or not in this layout.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_counterEnabled
- */
- public void setCounterEnabled(boolean enabled) {
- if (mCounterEnabled != enabled) {
- if (enabled) {
- mCounterView = new AppCompatTextView(getContext());
- mCounterView.setId(R.id.textinput_counter);
- if (mTypeface != null) {
- mCounterView.setTypeface(mTypeface);
- }
- mCounterView.setMaxLines(1);
- try {
- TextViewCompat.setTextAppearance(mCounterView, mCounterTextAppearance);
- } catch (Exception e) {
- // Probably caused by our theme not extending from Theme.Design*. Instead
- // we manually set something appropriate
- TextViewCompat.setTextAppearance(mCounterView,
- android.support.v7.appcompat.R.style.TextAppearance_AppCompat_Caption);
- mCounterView.setTextColor(ContextCompat.getColor(getContext(),
- android.support.v7.appcompat.R.color.error_color_material));
- }
- addIndicator(mCounterView, -1);
- if (mEditText == null) {
- updateCounter(0);
- } else {
- updateCounter(mEditText.getText().length());
- }
- } else {
- removeIndicator(mCounterView);
- mCounterView = null;
- }
- mCounterEnabled = enabled;
- }
- }
-
- /**
- * Returns whether the character counter functionality is enabled or not in this layout.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_counterEnabled
- *
- * @see #setCounterEnabled(boolean)
- */
- public boolean isCounterEnabled() {
- return mCounterEnabled;
- }
-
- /**
- * Sets the max length to display at the character counter.
- *
- * @param maxLength maxLength to display. Any value less than or equal to 0 will not be shown.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_counterMaxLength
- */
- public void setCounterMaxLength(int maxLength) {
- if (mCounterMaxLength != maxLength) {
- if (maxLength > 0) {
- mCounterMaxLength = maxLength;
- } else {
- mCounterMaxLength = INVALID_MAX_LENGTH;
- }
- if (mCounterEnabled) {
- updateCounter(mEditText == null ? 0 : mEditText.getText().length());
- }
- }
- }
-
- @Override
- public void setEnabled(boolean enabled) {
- // Since we're set to addStatesFromChildren, we need to make sure that we set all
- // children to enabled/disabled otherwise any enabled children will wipe out our disabled
- // drawable state
- recursiveSetEnabled(this, enabled);
- super.setEnabled(enabled);
- }
-
- private static void recursiveSetEnabled(final ViewGroup vg, final boolean enabled) {
- for (int i = 0, count = vg.getChildCount(); i < count; i++) {
- final View child = vg.getChildAt(i);
- child.setEnabled(enabled);
- if (child instanceof ViewGroup) {
- recursiveSetEnabled((ViewGroup) child, enabled);
- }
- }
- }
-
- /**
- * Returns the max length shown at the character counter.
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_counterMaxLength
- */
- public int getCounterMaxLength() {
- return mCounterMaxLength;
- }
-
- void updateCounter(int length) {
- boolean wasCounterOverflowed = mCounterOverflowed;
- if (mCounterMaxLength == INVALID_MAX_LENGTH) {
- mCounterView.setText(String.valueOf(length));
- mCounterOverflowed = false;
- } else {
- mCounterOverflowed = length > mCounterMaxLength;
- if (wasCounterOverflowed != mCounterOverflowed) {
- TextViewCompat.setTextAppearance(mCounterView, mCounterOverflowed
- ? mCounterOverflowTextAppearance : mCounterTextAppearance);
- }
- mCounterView.setText(getContext().getString(R.string.character_counter_pattern,
- length, mCounterMaxLength));
- }
- if (mEditText != null && wasCounterOverflowed != mCounterOverflowed) {
- updateLabelState(false);
- updateEditTextBackground();
- }
- }
-
- private void updateEditTextBackground() {
- if (mEditText == null) {
- return;
- }
-
- Drawable editTextBackground = mEditText.getBackground();
- if (editTextBackground == null) {
- return;
- }
-
- ensureBackgroundDrawableStateWorkaround();
-
- if (android.support.v7.widget.DrawableUtils.canSafelyMutateDrawable(editTextBackground)) {
- editTextBackground = editTextBackground.mutate();
- }
-
- if (mErrorShown && mErrorView != null) {
- // Set a color filter of the error color
- editTextBackground.setColorFilter(
- AppCompatDrawableManager.getPorterDuffColorFilter(
- mErrorView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN));
- } else if (mCounterOverflowed && mCounterView != null) {
- // Set a color filter of the counter color
- editTextBackground.setColorFilter(
- AppCompatDrawableManager.getPorterDuffColorFilter(
- mCounterView.getCurrentTextColor(), PorterDuff.Mode.SRC_IN));
- } else {
- // Else reset the color filter and refresh the drawable state so that the
- // normal tint is used
- DrawableCompat.clearColorFilter(editTextBackground);
- mEditText.refreshDrawableState();
- }
- }
-
- private void ensureBackgroundDrawableStateWorkaround() {
- final int sdk = Build.VERSION.SDK_INT;
- if (sdk != 21 && sdk != 22) {
- // The workaround is only required on API 21-22
- return;
- }
- final Drawable bg = mEditText.getBackground();
- if (bg == null) {
- return;
- }
-
- if (!mHasReconstructedEditTextBackground) {
- // This is gross. There is an issue in the platform which affects container Drawables
- // where the first drawable retrieved from resources will propagate any changes
- // (like color filter) to all instances from the cache. We'll try to workaround it...
-
- final Drawable newBg = bg.getConstantState().newDrawable();
-
- if (bg instanceof DrawableContainer) {
- // If we have a Drawable container, we can try and set it's constant state via
- // reflection from the new Drawable
- mHasReconstructedEditTextBackground =
- DrawableUtils.setContainerConstantState(
- (DrawableContainer) bg, newBg.getConstantState());
- }
-
- if (!mHasReconstructedEditTextBackground) {
- // If we reach here then we just need to set a brand new instance of the Drawable
- // as the background. This has the unfortunate side-effect of wiping out any
- // user set padding, but I'd hope that use of custom padding on an EditText
- // is limited.
- ViewCompat.setBackground(mEditText, newBg);
- mHasReconstructedEditTextBackground = true;
- }
- }
- }
-
- static class SavedState extends AbsSavedState {
- CharSequence error;
- boolean isPasswordToggledVisible;
-
- SavedState(Parcelable superState) {
- super(superState);
- }
-
- SavedState(Parcel source, ClassLoader loader) {
- super(source, loader);
- error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(source);
- isPasswordToggledVisible = (source.readInt() == 1);
-
- }
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- super.writeToParcel(dest, flags);
- TextUtils.writeToParcel(error, dest, flags);
- dest.writeInt(isPasswordToggledVisible ? 1 : 0);
- }
-
- @Override
- public String toString() {
- return "TextInputLayout.SavedState{"
- + Integer.toHexString(System.identityHashCode(this))
- + " error=" + error + "}";
- }
-
- public static final Creator<SavedState> CREATOR = new ClassLoaderCreator<SavedState>() {
- @Override
- public SavedState createFromParcel(Parcel in, ClassLoader loader) {
- return new SavedState(in, loader);
- }
-
- @Override
- public SavedState createFromParcel(Parcel in) {
- return new SavedState(in, null);
- }
-
- @Override
- public SavedState[] newArray(int size) {
- return new SavedState[size];
- }
- };
- }
-
- @Override
- public Parcelable onSaveInstanceState() {
- Parcelable superState = super.onSaveInstanceState();
- SavedState ss = new SavedState(superState);
- if (mErrorShown) {
- ss.error = getError();
- }
- ss.isPasswordToggledVisible = mPasswordToggledVisible;
- return ss;
- }
-
- @Override
- protected void onRestoreInstanceState(Parcelable state) {
- if (!(state instanceof SavedState)) {
- super.onRestoreInstanceState(state);
- return;
- }
- SavedState ss = (SavedState) state;
- super.onRestoreInstanceState(ss.getSuperState());
- setError(ss.error);
- if (ss.isPasswordToggledVisible) {
- passwordVisibilityToggleRequested(true);
- }
- requestLayout();
- }
-
- @Override
- protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
- mRestoringSavedState = true;
- super.dispatchRestoreInstanceState(container);
- mRestoringSavedState = false;
- }
-
- /**
- * Returns the error message that was set to be displayed with
- * {@link #setError(CharSequence)}, or <code>null</code> if no error was set
- * or if error displaying is not enabled.
- *
- * @see #setError(CharSequence)
- */
- @Nullable
- public CharSequence getError() {
- return mErrorEnabled ? mError : null;
- }
-
- /**
- * Returns whether any hint state changes, due to being focused or non-empty text, are
- * animated.
- *
- * @see #setHintAnimationEnabled(boolean)
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_hintAnimationEnabled
- */
- public boolean isHintAnimationEnabled() {
- return mHintAnimationEnabled;
- }
-
- /**
- * Set whether any hint state changes, due to being focused or non-empty text, are
- * animated.
- *
- * @see #isHintAnimationEnabled()
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_hintAnimationEnabled
- */
- public void setHintAnimationEnabled(boolean enabled) {
- mHintAnimationEnabled = enabled;
- }
-
- @Override
- public void draw(Canvas canvas) {
- super.draw(canvas);
-
- if (mHintEnabled) {
- mCollapsingTextHelper.draw(canvas);
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- updatePasswordToggleView();
- super.onMeasure(widthMeasureSpec, heightMeasureSpec);
- }
-
- private void updatePasswordToggleView() {
- if (mEditText == null) {
- // If there is no EditText, there is nothing to update
- return;
- }
-
- if (shouldShowPasswordIcon()) {
- if (mPasswordToggleView == null) {
- mPasswordToggleView = (CheckableImageButton) LayoutInflater.from(getContext())
- .inflate(R.layout.design_text_input_password_icon, mInputFrame, false);
- mPasswordToggleView.setImageDrawable(mPasswordToggleDrawable);
- mPasswordToggleView.setContentDescription(mPasswordToggleContentDesc);
- mInputFrame.addView(mPasswordToggleView);
-
- mPasswordToggleView.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View view) {
- passwordVisibilityToggleRequested(false);
- }
- });
- }
-
- if (mEditText != null && ViewCompat.getMinimumHeight(mEditText) <= 0) {
- // We should make sure that the EditText has the same min-height as the password
- // toggle view. This ensure focus works properly, and there is no visual jump
- // if the password toggle is enabled/disabled.
- mEditText.setMinimumHeight(ViewCompat.getMinimumHeight(mPasswordToggleView));
- }
-
- mPasswordToggleView.setVisibility(VISIBLE);
- mPasswordToggleView.setChecked(mPasswordToggledVisible);
-
- // We need to add a dummy drawable as the end compound drawable so that the text is
- // indented and doesn't display below the toggle view
- if (mPasswordToggleDummyDrawable == null) {
- mPasswordToggleDummyDrawable = new ColorDrawable();
- }
- mPasswordToggleDummyDrawable.setBounds(0, 0, mPasswordToggleView.getMeasuredWidth(), 1);
-
- final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText);
- // Store the user defined end compound drawable so that we can restore it later
- if (compounds[2] != mPasswordToggleDummyDrawable) {
- mOriginalEditTextEndDrawable = compounds[2];
- }
- TextViewCompat.setCompoundDrawablesRelative(mEditText, compounds[0], compounds[1],
- mPasswordToggleDummyDrawable, compounds[3]);
-
- // Copy over the EditText's padding so that we match
- mPasswordToggleView.setPadding(mEditText.getPaddingLeft(),
- mEditText.getPaddingTop(), mEditText.getPaddingRight(),
- mEditText.getPaddingBottom());
- } else {
- if (mPasswordToggleView != null && mPasswordToggleView.getVisibility() == VISIBLE) {
- mPasswordToggleView.setVisibility(View.GONE);
- }
-
- if (mPasswordToggleDummyDrawable != null) {
- // Make sure that we remove the dummy end compound drawable if it exists, and then
- // clear it
- final Drawable[] compounds = TextViewCompat.getCompoundDrawablesRelative(mEditText);
- if (compounds[2] == mPasswordToggleDummyDrawable) {
- TextViewCompat.setCompoundDrawablesRelative(mEditText, compounds[0],
- compounds[1], mOriginalEditTextEndDrawable, compounds[3]);
- mPasswordToggleDummyDrawable = null;
- }
- }
- }
- }
-
- /**
- * Set the icon to use for the password visibility toggle button.
- *
- * <p>If you use an icon you should also set a description for its action
- * using {@link #setPasswordVisibilityToggleContentDescription(CharSequence)}.
- * This is used for accessibility.</p>
- *
- * @param resId resource id of the drawable to set, or 0 to clear the icon
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleDrawable
- */
- public void setPasswordVisibilityToggleDrawable(@DrawableRes int resId) {
- setPasswordVisibilityToggleDrawable(resId != 0
- ? AppCompatResources.getDrawable(getContext(), resId)
- : null);
- }
-
- /**
- * Set the icon to use for the password visibility toggle button.
- *
- * <p>If you use an icon you should also set a description for its action
- * using {@link #setPasswordVisibilityToggleContentDescription(CharSequence)}.
- * This is used for accessibility.</p>
- *
- * @param icon Drawable to set, may be null to clear the icon
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleDrawable
- */
- public void setPasswordVisibilityToggleDrawable(@Nullable Drawable icon) {
- mPasswordToggleDrawable = icon;
- if (mPasswordToggleView != null) {
- mPasswordToggleView.setImageDrawable(icon);
- }
- }
-
- /**
- * Set a content description for the navigation button if one is present.
- *
- * <p>The content description will be read via screen readers or other accessibility
- * systems to explain the action of the password visibility toggle.</p>
- *
- * @param resId Resource ID of a content description string to set,
- * or 0 to clear the description
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleContentDescription
- */
- public void setPasswordVisibilityToggleContentDescription(@StringRes int resId) {
- setPasswordVisibilityToggleContentDescription(
- resId != 0 ? getResources().getText(resId) : null);
- }
-
- /**
- * Set a content description for the navigation button if one is present.
- *
- * <p>The content description will be read via screen readers or other accessibility
- * systems to explain the action of the password visibility toggle.</p>
- *
- * @param description Content description to set, or null to clear the content description
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleContentDescription
- */
- public void setPasswordVisibilityToggleContentDescription(@Nullable CharSequence description) {
- mPasswordToggleContentDesc = description;
- if (mPasswordToggleView != null) {
- mPasswordToggleView.setContentDescription(description);
- }
- }
-
- /**
- * Returns the icon currently used for the password visibility toggle button.
- *
- * @see #setPasswordVisibilityToggleDrawable(Drawable)
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleDrawable
- */
- @Nullable
- public Drawable getPasswordVisibilityToggleDrawable() {
- return mPasswordToggleDrawable;
- }
-
- /**
- * Returns the currently configured content description for the password visibility
- * toggle button.
- *
- * <p>This will be used to describe the navigation action to users through mechanisms
- * such as screen readers.</p>
- */
- @Nullable
- public CharSequence getPasswordVisibilityToggleContentDescription() {
- return mPasswordToggleContentDesc;
- }
-
- /**
- * Returns whether the password visibility toggle functionality is currently enabled.
- *
- * @see #setPasswordVisibilityToggleEnabled(boolean)
- */
- public boolean isPasswordVisibilityToggleEnabled() {
- return mPasswordToggleEnabled;
- }
-
- /**
- * Returns whether the password visibility toggle functionality is enabled or not.
- *
- * <p>When enabled, a button is placed at the end of the EditText which enables the user
- * to switch between the field's input being visibly disguised or not.</p>
- *
- * @param enabled true to enable the functionality
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleEnabled
- */
- public void setPasswordVisibilityToggleEnabled(final boolean enabled) {
- if (mPasswordToggleEnabled != enabled) {
- mPasswordToggleEnabled = enabled;
-
- if (!enabled && mPasswordToggledVisible && mEditText != null) {
- // If the toggle is no longer enabled, but we remove the PasswordTransformation
- // to make the password visible, add it back
- mEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
- }
-
- // Reset the visibility tracking flag
- mPasswordToggledVisible = false;
-
- updatePasswordToggleView();
- }
- }
-
- /**
- * Applies a tint to the the password visibility toggle drawable. Does not modify the current
- * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
- *
- * <p>Subsequent calls to {@link #setPasswordVisibilityToggleDrawable(Drawable)} will
- * automatically mutate the drawable and apply the specified tint and tint mode using
- * {@link DrawableCompat#setTintList(Drawable, ColorStateList)}.</p>
- *
- * @param tintList the tint to apply, may be null to clear tint
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleTint
- */
- public void setPasswordVisibilityToggleTintList(@Nullable ColorStateList tintList) {
- mPasswordToggleTintList = tintList;
- mHasPasswordToggleTintList = true;
- applyPasswordToggleTint();
- }
-
- /**
- * Specifies the blending mode used to apply the tint specified by
- * {@link #setPasswordVisibilityToggleTintList(ColorStateList)} to the password
- * visibility toggle drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.</p>
- *
- * @param mode the blending mode used to apply the tint, may be null to clear tint
- *
- * @attr ref android.support.design.R.styleable#TextInputLayout_passwordToggleTintMode
- */
- public void setPasswordVisibilityToggleTintMode(@Nullable PorterDuff.Mode mode) {
- mPasswordToggleTintMode = mode;
- mHasPasswordToggleTintMode = true;
- applyPasswordToggleTint();
- }
-
- private void passwordVisibilityToggleRequested(boolean shouldSkipAnimations) {
- if (mPasswordToggleEnabled) {
- // Store the current cursor position
- final int selection = mEditText.getSelectionEnd();
-
- if (hasPasswordTransformation()) {
- mEditText.setTransformationMethod(null);
- mPasswordToggledVisible = true;
- } else {
- mEditText.setTransformationMethod(PasswordTransformationMethod.getInstance());
- mPasswordToggledVisible = false;
- }
-
- mPasswordToggleView.setChecked(mPasswordToggledVisible);
- if (shouldSkipAnimations) {
- mPasswordToggleView.jumpDrawablesToCurrentState();
- }
-
- // And restore the cursor position
- mEditText.setSelection(selection);
- }
- }
-
- private boolean hasPasswordTransformation() {
- return mEditText != null
- && mEditText.getTransformationMethod() instanceof PasswordTransformationMethod;
- }
-
- private boolean shouldShowPasswordIcon() {
- return mPasswordToggleEnabled && (hasPasswordTransformation() || mPasswordToggledVisible);
- }
-
- private void applyPasswordToggleTint() {
- if (mPasswordToggleDrawable != null
- && (mHasPasswordToggleTintList || mHasPasswordToggleTintMode)) {
- mPasswordToggleDrawable = DrawableCompat.wrap(mPasswordToggleDrawable).mutate();
-
- if (mHasPasswordToggleTintList) {
- DrawableCompat.setTintList(mPasswordToggleDrawable, mPasswordToggleTintList);
- }
- if (mHasPasswordToggleTintMode) {
- DrawableCompat.setTintMode(mPasswordToggleDrawable, mPasswordToggleTintMode);
- }
-
- if (mPasswordToggleView != null
- && mPasswordToggleView.getDrawable() != mPasswordToggleDrawable) {
- mPasswordToggleView.setImageDrawable(mPasswordToggleDrawable);
- }
- }
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
-
- if (mHintEnabled && mEditText != null) {
- final Rect rect = mTmpRect;
- ViewGroupUtils.getDescendantRect(this, mEditText, rect);
-
- final int l = rect.left + mEditText.getCompoundPaddingLeft();
- final int r = rect.right - mEditText.getCompoundPaddingRight();
-
- mCollapsingTextHelper.setExpandedBounds(
- l, rect.top + mEditText.getCompoundPaddingTop(),
- r, rect.bottom - mEditText.getCompoundPaddingBottom());
-
- // Set the collapsed bounds to be the the full height (minus padding) to match the
- // EditText's editable area
- mCollapsingTextHelper.setCollapsedBounds(l, getPaddingTop(),
- r, bottom - top - getPaddingBottom());
-
- mCollapsingTextHelper.recalculate();
- }
- }
-
- private void collapseHint(boolean animate) {
- if (mAnimator != null && mAnimator.isRunning()) {
- mAnimator.cancel();
- }
- if (animate && mHintAnimationEnabled) {
- animateToExpansionFraction(1f);
- } else {
- mCollapsingTextHelper.setExpansionFraction(1f);
- }
- mHintExpanded = false;
- }
-
- @Override
- protected void drawableStateChanged() {
- if (mInDrawableStateChanged) {
- // Some of the calls below will update the drawable state of child views. Since we're
- // using addStatesFromChildren we can get into infinite recursion, hence we'll just
- // exit in this instance
- return;
- }
-
- mInDrawableStateChanged = true;
-
- super.drawableStateChanged();
-
- final int[] state = getDrawableState();
- boolean changed = false;
-
- // Drawable state has changed so see if we need to update the label
- updateLabelState(ViewCompat.isLaidOut(this) && isEnabled());
-
- updateEditTextBackground();
-
- if (mCollapsingTextHelper != null) {
- changed |= mCollapsingTextHelper.setState(state);
- }
-
- if (changed) {
- invalidate();
- }
-
- mInDrawableStateChanged = false;
- }
-
- private void expandHint(boolean animate) {
- if (mAnimator != null && mAnimator.isRunning()) {
- mAnimator.cancel();
- }
- if (animate && mHintAnimationEnabled) {
- animateToExpansionFraction(0f);
- } else {
- mCollapsingTextHelper.setExpansionFraction(0f);
- }
- mHintExpanded = true;
- }
-
- @VisibleForTesting
- void animateToExpansionFraction(final float target) {
- if (mCollapsingTextHelper.getExpansionFraction() == target) {
- return;
- }
- if (mAnimator == null) {
- mAnimator = new ValueAnimator();
- mAnimator.setInterpolator(AnimationUtils.LINEAR_INTERPOLATOR);
- mAnimator.setDuration(ANIMATION_DURATION);
- mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
- @Override
- public void onAnimationUpdate(ValueAnimator animator) {
- mCollapsingTextHelper.setExpansionFraction((float) animator.getAnimatedValue());
- }
- });
- }
- mAnimator.setFloatValues(mCollapsingTextHelper.getExpansionFraction(), target);
- mAnimator.start();
- }
-
- @VisibleForTesting
- final boolean isHintExpanded() {
- return mHintExpanded;
- }
-
- private class TextInputAccessibilityDelegate extends AccessibilityDelegateCompat {
- TextInputAccessibilityDelegate() {
- }
-
- @Override
- public void onInitializeAccessibilityEvent(View host, AccessibilityEvent event) {
- super.onInitializeAccessibilityEvent(host, event);
- event.setClassName(TextInputLayout.class.getSimpleName());
- }
-
- @Override
- public void onPopulateAccessibilityEvent(View host, AccessibilityEvent event) {
- super.onPopulateAccessibilityEvent(host, event);
-
- final CharSequence text = mCollapsingTextHelper.getText();
- if (!TextUtils.isEmpty(text)) {
- event.getText().add(text);
- }
- }
-
- @Override
- public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) {
- super.onInitializeAccessibilityNodeInfo(host, info);
- info.setClassName(TextInputLayout.class.getSimpleName());
-
- final CharSequence text = mCollapsingTextHelper.getText();
- if (!TextUtils.isEmpty(text)) {
- info.setText(text);
- }
- if (mEditText != null) {
- info.setLabelFor(mEditText);
- }
- final CharSequence error = mErrorView != null ? mErrorView.getText() : null;
- if (!TextUtils.isEmpty(error)) {
- info.setContentInvalid(true);
- info.setError(error);
- }
- }
- }
-
- private static boolean arrayContains(int[] array, int value) {
- for (int v : array) {
- if (v == value) {
- return true;
- }
- }
- return false;
- }
-}
diff --git a/android/support/design/widget/ThemeUtils.java b/android/support/design/widget/ThemeUtils.java
deleted file mode 100644
index 821dcb64..00000000
--- a/android/support/design/widget/ThemeUtils.java
+++ /dev/null
@@ -1,37 +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 android.support.design.widget;
-
-import android.content.Context;
-import android.content.res.TypedArray;
-
-class ThemeUtils {
-
- private static final int[] APPCOMPAT_CHECK_ATTRS = {
- android.support.v7.appcompat.R.attr.colorPrimary
- };
-
- static void checkAppCompatTheme(Context context) {
- TypedArray a = context.obtainStyledAttributes(APPCOMPAT_CHECK_ATTRS);
- final boolean failed = !a.hasValue(0);
- a.recycle();
- if (failed) {
- throw new IllegalArgumentException("You need to use a Theme.AppCompat theme "
- + "(or descendant) with the design library.");
- }
- }
-}
diff --git a/android/support/design/widget/ViewOffsetBehavior.java b/android/support/design/widget/ViewOffsetBehavior.java
deleted file mode 100644
index 541de696..00000000
--- a/android/support/design/widget/ViewOffsetBehavior.java
+++ /dev/null
@@ -1,91 +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 android.support.design.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.view.View;
-
-/**
- * Behavior will automatically sets up a {@link ViewOffsetHelper} on a {@link View}.
- */
-class ViewOffsetBehavior<V extends View> extends CoordinatorLayout.Behavior<V> {
-
- private ViewOffsetHelper mViewOffsetHelper;
-
- private int mTempTopBottomOffset = 0;
- private int mTempLeftRightOffset = 0;
-
- public ViewOffsetBehavior() {}
-
- public ViewOffsetBehavior(Context context, AttributeSet attrs) {
- super(context, attrs);
- }
-
- @Override
- public boolean onLayoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
- // First let lay the child out
- layoutChild(parent, child, layoutDirection);
-
- if (mViewOffsetHelper == null) {
- mViewOffsetHelper = new ViewOffsetHelper(child);
- }
- mViewOffsetHelper.onViewLayout();
-
- if (mTempTopBottomOffset != 0) {
- mViewOffsetHelper.setTopAndBottomOffset(mTempTopBottomOffset);
- mTempTopBottomOffset = 0;
- }
- if (mTempLeftRightOffset != 0) {
- mViewOffsetHelper.setLeftAndRightOffset(mTempLeftRightOffset);
- mTempLeftRightOffset = 0;
- }
-
- return true;
- }
-
- protected void layoutChild(CoordinatorLayout parent, V child, int layoutDirection) {
- // Let the parent lay it out by default
- parent.onLayoutChild(child, layoutDirection);
- }
-
- public boolean setTopAndBottomOffset(int offset) {
- if (mViewOffsetHelper != null) {
- return mViewOffsetHelper.setTopAndBottomOffset(offset);
- } else {
- mTempTopBottomOffset = offset;
- }
- return false;
- }
-
- public boolean setLeftAndRightOffset(int offset) {
- if (mViewOffsetHelper != null) {
- return mViewOffsetHelper.setLeftAndRightOffset(offset);
- } else {
- mTempLeftRightOffset = offset;
- }
- return false;
- }
-
- public int getTopAndBottomOffset() {
- return mViewOffsetHelper != null ? mViewOffsetHelper.getTopAndBottomOffset() : 0;
- }
-
- public int getLeftAndRightOffset() {
- return mViewOffsetHelper != null ? mViewOffsetHelper.getLeftAndRightOffset() : 0;
- }
-} \ No newline at end of file
diff --git a/android/support/design/widget/ViewOffsetHelper.java b/android/support/design/widget/ViewOffsetHelper.java
deleted file mode 100644
index 088430af..00000000
--- a/android/support/design/widget/ViewOffsetHelper.java
+++ /dev/null
@@ -1,102 +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 android.support.design.widget;
-
-import android.support.v4.view.ViewCompat;
-import android.view.View;
-
-/**
- * Utility helper for moving a {@link android.view.View} around using
- * {@link android.view.View#offsetLeftAndRight(int)} and
- * {@link android.view.View#offsetTopAndBottom(int)}.
- * <p>
- * Also the setting of absolute offsets (similar to translationX/Y), rather than additive
- * offsets.
- */
-class ViewOffsetHelper {
-
- private final View mView;
-
- private int mLayoutTop;
- private int mLayoutLeft;
- private int mOffsetTop;
- private int mOffsetLeft;
-
- public ViewOffsetHelper(View view) {
- mView = view;
- }
-
- public void onViewLayout() {
- // Now grab the intended top
- mLayoutTop = mView.getTop();
- mLayoutLeft = mView.getLeft();
-
- // And offset it as needed
- updateOffsets();
- }
-
- private void updateOffsets() {
- ViewCompat.offsetTopAndBottom(mView, mOffsetTop - (mView.getTop() - mLayoutTop));
- ViewCompat.offsetLeftAndRight(mView, mOffsetLeft - (mView.getLeft() - mLayoutLeft));
- }
-
- /**
- * Set the top and bottom offset for this {@link ViewOffsetHelper}'s view.
- *
- * @param offset the offset in px.
- * @return true if the offset has changed
- */
- public boolean setTopAndBottomOffset(int offset) {
- if (mOffsetTop != offset) {
- mOffsetTop = offset;
- updateOffsets();
- return true;
- }
- return false;
- }
-
- /**
- * Set the left and right offset for this {@link ViewOffsetHelper}'s view.
- *
- * @param offset the offset in px.
- * @return true if the offset has changed
- */
- public boolean setLeftAndRightOffset(int offset) {
- if (mOffsetLeft != offset) {
- mOffsetLeft = offset;
- updateOffsets();
- return true;
- }
- return false;
- }
-
- public int getTopAndBottomOffset() {
- return mOffsetTop;
- }
-
- public int getLeftAndRightOffset() {
- return mOffsetLeft;
- }
-
- public int getLayoutTop() {
- return mLayoutTop;
- }
-
- public int getLayoutLeft() {
- return mLayoutLeft;
- }
-} \ No newline at end of file
diff --git a/android/support/design/widget/ViewUtils.java b/android/support/design/widget/ViewUtils.java
deleted file mode 100644
index c09eac16..00000000
--- a/android/support/design/widget/ViewUtils.java
+++ /dev/null
@@ -1,39 +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 android.support.design.widget;
-
-import android.graphics.PorterDuff;
-
-class ViewUtils {
- static PorterDuff.Mode parseTintMode(int value, PorterDuff.Mode defaultMode) {
- switch (value) {
- case 3:
- return PorterDuff.Mode.SRC_OVER;
- case 5:
- return PorterDuff.Mode.SRC_IN;
- case 9:
- return PorterDuff.Mode.SRC_ATOP;
- case 14:
- return PorterDuff.Mode.MULTIPLY;
- case 15:
- return PorterDuff.Mode.SCREEN;
- default:
- return defaultMode;
- }
- }
-
-}
diff --git a/android/support/design/widget/ViewUtilsLollipop.java b/android/support/design/widget/ViewUtilsLollipop.java
deleted file mode 100644
index 5927e9b8..00000000
--- a/android/support/design/widget/ViewUtilsLollipop.java
+++ /dev/null
@@ -1,79 +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 android.support.design.widget;
-
-import android.animation.AnimatorInflater;
-import android.animation.ObjectAnimator;
-import android.animation.StateListAnimator;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.support.annotation.RequiresApi;
-import android.support.design.R;
-import android.util.AttributeSet;
-import android.view.View;
-import android.view.ViewOutlineProvider;
-
-@RequiresApi(21)
-class ViewUtilsLollipop {
-
- private static final int[] STATE_LIST_ANIM_ATTRS = new int[] {android.R.attr.stateListAnimator};
-
- static void setBoundsViewOutlineProvider(View view) {
- view.setOutlineProvider(ViewOutlineProvider.BOUNDS);
- }
-
- static void setStateListAnimatorFromAttrs(View view, AttributeSet attrs,
- int defStyleAttr, int defStyleRes) {
- final Context context = view.getContext();
- final TypedArray a = context.obtainStyledAttributes(attrs, STATE_LIST_ANIM_ATTRS,
- defStyleAttr, defStyleRes);
- try {
- if (a.hasValue(0)) {
- StateListAnimator sla = AnimatorInflater.loadStateListAnimator(context,
- a.getResourceId(0, 0));
- view.setStateListAnimator(sla);
- }
- } finally {
- a.recycle();
- }
- }
-
- /**
- * Creates and sets a {@link StateListAnimator} with a custom elevation value
- */
- static void setDefaultAppBarLayoutStateListAnimator(final View view, final float elevation) {
- final int dur = view.getResources().getInteger(R.integer.app_bar_elevation_anim_duration);
-
- final StateListAnimator sla = new StateListAnimator();
-
- // Enabled and collapsible, but not collapsed means not elevated
- sla.addState(new int[]{android.R.attr.enabled, R.attr.state_collapsible,
- -R.attr.state_collapsed},
- ObjectAnimator.ofFloat(view, "elevation", 0f).setDuration(dur));
-
- // Default enabled state
- sla.addState(new int[]{android.R.attr.enabled},
- ObjectAnimator.ofFloat(view, "elevation", elevation).setDuration(dur));
-
- // Disabled state
- sla.addState(new int[0],
- ObjectAnimator.ofFloat(view, "elevation", 0).setDuration(0));
-
- view.setStateListAnimator(sla);
- }
-
-}
diff --git a/android/support/design/widget/VisibilityAwareImageButton.java b/android/support/design/widget/VisibilityAwareImageButton.java
deleted file mode 100644
index d7a0b13f..00000000
--- a/android/support/design/widget/VisibilityAwareImageButton.java
+++ /dev/null
@@ -1,55 +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 android.support.design.widget;
-
-import android.content.Context;
-import android.util.AttributeSet;
-import android.widget.ImageButton;
-
-class VisibilityAwareImageButton extends ImageButton {
-
- private int mUserSetVisibility;
-
- public VisibilityAwareImageButton(Context context) {
- this(context, null);
- }
-
- public VisibilityAwareImageButton(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public VisibilityAwareImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
- mUserSetVisibility = getVisibility();
- }
-
- @Override
- public void setVisibility(int visibility) {
- internalSetVisibility(visibility, true);
- }
-
- final void internalSetVisibility(int visibility, boolean fromUser) {
- super.setVisibility(visibility);
- if (fromUser) {
- mUserSetVisibility = visibility;
- }
- }
-
- final int getUserSetVisibility() {
- return mUserSetVisibility;
- }
-}
diff --git a/android/support/graphics/drawable/AndroidResources.java b/android/support/graphics/drawable/AndroidResources.java
index 31370a2b..804c6239 100644
--- a/android/support/graphics/drawable/AndroidResources.java
+++ b/android/support/graphics/drawable/AndroidResources.java
@@ -89,8 +89,7 @@ class AndroidResources {
public static final int[] STYLEABLE_ANIMATOR = {
0x01010141, 0x01010198, 0x010101be, 0x010101bf,
- 0x010101c0, 0x010102de, 0x010102df, 0x010102e0,
- 0x0111009c
+ 0x010101c0, 0x010102de, 0x010102df, 0x010102e0
};
public static final int STYLEABLE_ANIMATOR_INTERPOLATOR = 0;
@@ -101,7 +100,6 @@ class AndroidResources {
public static final int STYLEABLE_ANIMATOR_VALUE_FROM = 5;
public static final int STYLEABLE_ANIMATOR_VALUE_TO = 6;
public static final int STYLEABLE_ANIMATOR_VALUE_TYPE = 7;
- public static final int STYLEABLE_ANIMATOR_REMOVE_BEFORE_M_RELEASE = 8;
public static final int[] STYLEABLE_ANIMATOR_SET = {
0x010102e2
};
diff --git a/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java b/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java
index cff61bcc..bc521cc7 100644
--- a/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java
+++ b/android/support/graphics/drawable/AnimatedVectorDrawableCompat.java
@@ -118,6 +118,9 @@ import java.util.List;
* <td>trimPathStart</td>
* </tr>
* <tr>
+ * <td>trimPathEnd</td>
+ * </tr>
+ * <tr>
* <td>trimPathOffset</td>
* </tr>
* </table>
diff --git a/android/support/graphics/drawable/AnimatorInflaterCompat.java b/android/support/graphics/drawable/AnimatorInflaterCompat.java
index cfededb4..da522f6e 100644
--- a/android/support/graphics/drawable/AnimatorInflaterCompat.java
+++ b/android/support/graphics/drawable/AnimatorInflaterCompat.java
@@ -463,7 +463,6 @@ public class AnimatorInflaterCompat {
// the previously sampled contours' total length.
for (int i = 0; i < numPoints; ++i) {
pathMeasure.getPosTan(currentDistance, position, null);
- pathMeasure.getPosTan(currentDistance, position, null);
mX[i] = position[0];
mY[i] = position[1];
diff --git a/android/support/media/ExifInterface.java b/android/support/media/ExifInterface.java
index eea69ab1..6b437a69 100644
--- a/android/support/media/ExifInterface.java
+++ b/android/support/media/ExifInterface.java
@@ -23,6 +23,7 @@ import android.location.Location;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
import android.util.Log;
import android.util.Pair;
@@ -3550,6 +3551,7 @@ public class ExifInterface {
// Indices of Exif Ifd tag groups
/** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Retention(RetentionPolicy.SOURCE)
@IntDef({IFD_TYPE_PRIMARY, IFD_TYPE_EXIF, IFD_TYPE_GPS, IFD_TYPE_INTEROPERABILITY,
IFD_TYPE_THUMBNAIL, IFD_TYPE_PREVIEW, IFD_TYPE_ORF_MAKER_NOTE,
@@ -4567,6 +4569,7 @@ public class ExifInterface {
* @param timeStamp number of milliseconds since Jan. 1, 1970, midnight local time.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void setDateTime(long timeStamp) {
long sub = timeStamp % 1000;
setAttribute(TAG_DATETIME, sFormatter.format(new Date(timeStamp)));
@@ -4578,6 +4581,7 @@ public class ExifInterface {
* Returns -1 if the date time information if not available.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public long getDateTime() {
String dateTimeString = getAttribute(TAG_DATETIME);
if (dateTimeString == null
@@ -4614,6 +4618,7 @@ public class ExifInterface {
* Returns -1 if the date time information if not available.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public long getGpsDateTime() {
String date = getAttribute(TAG_GPS_DATESTAMP);
String time = getAttribute(TAG_GPS_TIMESTAMP);
diff --git a/android/support/media/tv/BasePreviewProgram.java b/android/support/media/tv/BasePreviewProgram.java
index eeaa5ea1..816b1a13 100644
--- a/android/support/media/tv/BasePreviewProgram.java
+++ b/android/support/media/tv/BasePreviewProgram.java
@@ -15,6 +15,7 @@
*/
package android.support.media.tv;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.ContentValues;
@@ -39,6 +40,7 @@ import java.util.TimeZone;
*
* @hide
*/
+@RestrictTo(LIBRARY)
public abstract class BasePreviewProgram extends BaseProgram {
/**
* @hide
diff --git a/android/support/media/tv/BaseProgram.java b/android/support/media/tv/BaseProgram.java
index 23b5cf9c..4c7882dd 100644
--- a/android/support/media/tv/BaseProgram.java
+++ b/android/support/media/tv/BaseProgram.java
@@ -15,6 +15,7 @@
*/
package android.support.media.tv;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.ContentValues;
@@ -37,6 +38,7 @@ import java.lang.annotation.RetentionPolicy;
* {@link TvContractCompat}.
* @hide
*/
+@RestrictTo(LIBRARY)
public abstract class BaseProgram {
/**
* @hide
diff --git a/android/support/media/tv/TvContractCompat.java b/android/support/media/tv/TvContractCompat.java
index de4fd04f..bd03bf1b 100644
--- a/android/support/media/tv/TvContractCompat.java
+++ b/android/support/media/tv/TvContractCompat.java
@@ -2422,6 +2422,7 @@ public final class TvContractCompat {
/** Canonical genres for TV programs. */
public static final class Genres {
/** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@StringDef({
FAMILY_KIDS,
SPORTS,
diff --git a/android/support/multidex/MultiDex.java b/android/support/multidex/MultiDex.java
index ab7f668b..2b681db0 100644
--- a/android/support/multidex/MultiDex.java
+++ b/android/support/multidex/MultiDex.java
@@ -28,6 +28,7 @@ import dalvik.system.DexFile;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Array;
+import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@@ -114,7 +115,8 @@ public final class MultiDex {
new File(applicationInfo.sourceDir),
new File(applicationInfo.dataDir),
CODE_CACHE_SECONDARY_FOLDER_NAME,
- NO_KEY_PREFIX);
+ NO_KEY_PREFIX,
+ true);
} catch (Exception e) {
Log.e(TAG, "MultiDex installation failure", e);
@@ -171,13 +173,15 @@ public final class MultiDex {
new File(instrumentationInfo.sourceDir),
dataDir,
instrumentationPrefix + CODE_CACHE_SECONDARY_FOLDER_NAME,
- instrumentationPrefix);
+ instrumentationPrefix,
+ false);
doInstallation(targetContext,
new File(applicationInfo.sourceDir),
dataDir,
CODE_CACHE_SECONDARY_FOLDER_NAME,
- NO_KEY_PREFIX);
+ NO_KEY_PREFIX,
+ false);
} catch (Exception e) {
Log.e(TAG, "MultiDex installation failure", e);
throw new RuntimeException("MultiDex installation failed (" + e.getMessage() + ").");
@@ -192,11 +196,15 @@ public final class MultiDex {
* @param dataDir data directory to use for code cache simulation.
* @param secondaryFolderName name of the folder for storing extractions.
* @param prefsKeyPrefix prefix of all stored preference keys.
+ * @param reinstallOnPatchRecoverableException if set to true, will attempt a clean extraction
+ * if a possibly recoverable exception occurs during classloader patching.
*/
private static void doInstallation(Context mainContext, File sourceApk, File dataDir,
- String secondaryFolderName, String prefsKeyPrefix) throws IOException,
+ String secondaryFolderName, String prefsKeyPrefix,
+ boolean reinstallOnPatchRecoverableException) throws IOException,
IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
- InvocationTargetException, NoSuchMethodException {
+ InvocationTargetException, NoSuchMethodException, SecurityException,
+ ClassNotFoundException, InstantiationException {
synchronized (installedApk) {
if (installedApk.contains(sourceApk)) {
return;
@@ -245,9 +253,38 @@ public final class MultiDex {
}
File dexDir = getDexDir(mainContext, dataDir, secondaryFolderName);
- List<? extends File> files =
- MultiDexExtractor.load(mainContext, sourceApk, dexDir, prefsKeyPrefix, false);
- installSecondaryDexes(loader, dexDir, files);
+ // MultiDexExtractor is taking the file lock and keeping it until it is closed.
+ // Keep it open during installSecondaryDexes and through forced extraction to ensure no
+ // extraction or optimizing dexopt is running in parallel.
+ MultiDexExtractor extractor = new MultiDexExtractor(sourceApk, dexDir);
+ IOException closeException = null;
+ try {
+ List<? extends File> files =
+ extractor.load(mainContext, prefsKeyPrefix, false);
+ try {
+ installSecondaryDexes(loader, dexDir, files);
+ // Some IOException causes may be fixed by a clean extraction.
+ } catch (IOException e) {
+ if (!reinstallOnPatchRecoverableException) {
+ throw e;
+ }
+ Log.w(TAG, "Failed to install extracted secondary dex files, retrying with "
+ + "forced extraction", e);
+ files = extractor.load(mainContext, prefsKeyPrefix, true);
+ installSecondaryDexes(loader, dexDir, files);
+ }
+ } finally {
+ try {
+ extractor.close();
+ } catch (IOException e) {
+ // Delay throw of close exception to ensure we don't override some exception
+ // thrown during the try block.
+ closeException = e;
+ }
+ }
+ if (closeException != null) {
+ throw closeException;
+ }
}
}
@@ -305,12 +342,13 @@ public final class MultiDex {
private static void installSecondaryDexes(ClassLoader loader, File dexDir,
List<? extends File> files)
throws IllegalArgumentException, IllegalAccessException, NoSuchFieldException,
- InvocationTargetException, NoSuchMethodException, IOException {
+ InvocationTargetException, NoSuchMethodException, IOException, SecurityException,
+ ClassNotFoundException, InstantiationException {
if (!files.isEmpty()) {
if (Build.VERSION.SDK_INT >= 19) {
V19.install(loader, files, dexDir);
} else if (Build.VERSION.SDK_INT >= 14) {
- V14.install(loader, files, dexDir);
+ V14.install(loader, files);
} else {
V4.install(loader, files);
}
@@ -460,11 +498,12 @@ public final class MultiDex {
*/
private static final class V19 {
- private static void install(ClassLoader loader,
+ static void install(ClassLoader loader,
List<? extends File> additionalClassPathEntries,
File optimizedDirectory)
throws IllegalArgumentException, IllegalAccessException,
- NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
+ NoSuchFieldException, InvocationTargetException, NoSuchMethodException,
+ IOException {
/* The patched class loader is expected to be a descendant of
* dalvik.system.BaseDexClassLoader. We modify its
* dalvik.system.DexPathList pathList field to append additional DEX
@@ -500,6 +539,10 @@ public final class MultiDex {
}
suppressedExceptionsField.set(dexPathList, dexElementsSuppressedExceptions);
+
+ IOException exception = new IOException("I/O exception during makeDexElement");
+ exception.initCause(suppressedExceptions.get(0));
+ throw exception;
}
}
@@ -526,11 +569,16 @@ public final class MultiDex {
*/
private static final class V14 {
- private static void install(ClassLoader loader,
- List<? extends File> additionalClassPathEntries,
- File optimizedDirectory)
- throws IllegalArgumentException, IllegalAccessException,
- NoSuchFieldException, InvocationTargetException, NoSuchMethodException {
+ private static final int EXTRACTED_SUFFIX_LENGTH =
+ MultiDexExtractor.EXTRACTED_SUFFIX.length();
+
+ private final Constructor<?> elementConstructor;
+
+ static void install(ClassLoader loader,
+ List<? extends File> additionalClassPathEntries)
+ throws IOException, SecurityException, IllegalArgumentException,
+ ClassNotFoundException, NoSuchMethodException, InstantiationException,
+ IllegalAccessException, InvocationTargetException, NoSuchFieldException {
/* The patched class loader is expected to be a descendant of
* dalvik.system.BaseDexClassLoader. We modify its
* dalvik.system.DexPathList pathList field to append additional DEX
@@ -538,22 +586,52 @@ public final class MultiDex {
*/
Field pathListField = findField(loader, "pathList");
Object dexPathList = pathListField.get(loader);
- expandFieldArray(dexPathList, "dexElements", makeDexElements(dexPathList,
- new ArrayList<File>(additionalClassPathEntries), optimizedDirectory));
+ expandFieldArray(dexPathList, "dexElements",
+ new V14().makeDexElements(additionalClassPathEntries));
+ }
+
+ private V14() throws ClassNotFoundException, SecurityException, NoSuchMethodException {
+ Class<?> elementClass = Class.forName("dalvik.system.DexPathList$Element");
+ elementConstructor =
+ elementClass.getConstructor(File.class, ZipFile.class, DexFile.class);
+ elementConstructor.setAccessible(true);
}
/**
- * A wrapper around
- * {@code private static final dalvik.system.DexPathList#makeDexElements}.
+ * An emulation of {@code private static final dalvik.system.DexPathList#makeDexElements}
+ * accepting only extracted secondary dex files.
+ * OS version is catching IOException and just logging some of them, this version is letting
+ * them through.
*/
- private static Object[] makeDexElements(
- Object dexPathList, ArrayList<File> files, File optimizedDirectory)
- throws IllegalAccessException, InvocationTargetException,
- NoSuchMethodException {
- Method makeDexElements =
- findMethod(dexPathList, "makeDexElements", ArrayList.class, File.class);
+ private Object[] makeDexElements(List<? extends File> files)
+ throws IOException, SecurityException, IllegalArgumentException,
+ InstantiationException, IllegalAccessException, InvocationTargetException {
+ Object[] elements = new Object[files.size()];
+ for (int i = 0; i < elements.length; i++) {
+ File file = files.get(i);
+ elements[i] = elementConstructor.newInstance(
+ file,
+ new ZipFile(file),
+ DexFile.loadDex(file.getPath(), optimizedPathFor(file), 0));
+ }
+ return elements;
+ }
- return (Object[]) makeDexElements.invoke(dexPathList, files, optimizedDirectory);
+ /**
+ * Converts a zip file path of an extracted secondary dex to an output file path for an
+ * associated optimized dex file.
+ */
+ private static String optimizedPathFor(File path) {
+ // Any reproducible name ending with ".dex" should do but lets keep the same name
+ // as DexPathList.optimizedPathFor
+
+ File optimizedDirectory = path.getParentFile();
+ String fileName = path.getName();
+ String optimizedFileName =
+ fileName.substring(0, fileName.length() - EXTRACTED_SUFFIX_LENGTH)
+ + MultiDexExtractor.DEX_SUFFIX;
+ File result = new File(optimizedDirectory, optimizedFileName);
+ return result.getPath();
}
}
@@ -561,7 +639,7 @@ public final class MultiDex {
* Installer for platform versions 4 to 13.
*/
private static final class V4 {
- private static void install(ClassLoader loader,
+ static void install(ClassLoader loader,
List<? extends File> additionalClassPathEntries)
throws IllegalArgumentException, IllegalAccessException,
NoSuchFieldException, IOException {
diff --git a/android/support/multidex/MultiDexExtractor.java b/android/support/multidex/MultiDexExtractor.java
index 39b6bf78..f0fd6d42 100644
--- a/android/support/multidex/MultiDexExtractor.java
+++ b/android/support/multidex/MultiDexExtractor.java
@@ -40,8 +40,10 @@ import java.util.zip.ZipOutputStream;
/**
* Exposes application secondary dex files as files in the application data
* directory.
+ * {@link MultiDexExtractor} is taking the file lock in the dex dir on creation and release it
+ * during close.
*/
-final class MultiDexExtractor {
+final class MultiDexExtractor implements Closeable {
/**
* Zip file containing one secondary dex file.
@@ -61,10 +63,10 @@ final class MultiDexExtractor {
* {@code classes3.dex}, etc.
*/
private static final String DEX_PREFIX = "classes";
- private static final String DEX_SUFFIX = ".dex";
+ static final String DEX_SUFFIX = ".dex";
private static final String EXTRACTED_NAME_EXT = ".classes";
- private static final String EXTRACTED_SUFFIX = ".zip";
+ static final String EXTRACTED_SUFFIX = ".zip";
private static final int MAX_EXTRACT_ATTEMPTS = 3;
private static final String PREFS_FILE = "multidex.version";
@@ -82,6 +84,35 @@ final class MultiDexExtractor {
private static final long NO_VALUE = -1L;
private static final String LOCK_FILENAME = "MultiDex.lock";
+ private final File sourceApk;
+ private final long sourceCrc;
+ private final File dexDir;
+ private final RandomAccessFile lockRaf;
+ private final FileChannel lockChannel;
+ private final FileLock cacheLock;
+
+ MultiDexExtractor(File sourceApk, File dexDir) throws IOException {
+ Log.i(TAG, "MultiDexExtractor(" + sourceApk.getPath() + ", " + dexDir.getPath() + ")");
+ this.sourceApk = sourceApk;
+ this.dexDir = dexDir;
+ sourceCrc = getZipCrc(sourceApk);
+ File lockFile = new File(dexDir, LOCK_FILENAME);
+ lockRaf = new RandomAccessFile(lockFile, "rw");
+ try {
+ lockChannel = lockRaf.getChannel();
+ try {
+ Log.i(TAG, "Blocking on lock " + lockFile.getPath());
+ cacheLock = lockChannel.lock();
+ } catch (IOException | RuntimeException | Error e) {
+ closeQuietly(lockChannel);
+ throw e;
+ }
+ Log.i(TAG, lockFile.getPath() + " locked");
+ } catch (IOException | RuntimeException | Error e) {
+ closeQuietly(lockRaf);
+ throw e;
+ }
+ }
/**
* Extracts application secondary dexes into files in the application data
@@ -92,74 +123,54 @@ final class MultiDexExtractor {
* @throws IOException if encounters a problem while reading or writing
* secondary dex files
*/
- static List<? extends File> load(Context context, File sourceApk, File dexDir,
- String prefsKeyPrefix,
- boolean forceReload) throws IOException {
+ List<? extends File> load(Context context, String prefsKeyPrefix, boolean forceReload)
+ throws IOException {
Log.i(TAG, "MultiDexExtractor.load(" + sourceApk.getPath() + ", " + forceReload + ", " +
prefsKeyPrefix + ")");
- long currentCrc = getZipCrc(sourceApk);
+ if (!cacheLock.isValid()) {
+ throw new IllegalStateException("MultiDexExtractor was closed");
+ }
- // Validity check and extraction must be done only while the lock file has been taken.
- File lockFile = new File(dexDir, LOCK_FILENAME);
- RandomAccessFile lockRaf = new RandomAccessFile(lockFile, "rw");
- FileChannel lockChannel = null;
- FileLock cacheLock = null;
List<ExtractedDex> files;
- IOException releaseLockException = null;
- try {
- lockChannel = lockRaf.getChannel();
- Log.i(TAG, "Blocking on lock " + lockFile.getPath());
- cacheLock = lockChannel.lock();
- Log.i(TAG, lockFile.getPath() + " locked");
-
- if (!forceReload && !isModified(context, sourceApk, currentCrc, prefsKeyPrefix)) {
- try {
- files = loadExistingExtractions(context, sourceApk, dexDir, prefsKeyPrefix);
- } catch (IOException ioe) {
- Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
- + " falling back to fresh extraction", ioe);
- files = performExtractions(sourceApk, dexDir);
- putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc,
- files);
- }
- } else {
- Log.i(TAG, "Detected that extraction must be performed.");
- files = performExtractions(sourceApk, dexDir);
- putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), currentCrc,
+ if (!forceReload && !isModified(context, sourceApk, sourceCrc, prefsKeyPrefix)) {
+ try {
+ files = loadExistingExtractions(context, prefsKeyPrefix);
+ } catch (IOException ioe) {
+ Log.w(TAG, "Failed to reload existing extracted secondary dex files,"
+ + " falling back to fresh extraction", ioe);
+ files = performExtractions();
+ putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
files);
}
- } finally {
- if (cacheLock != null) {
- try {
- cacheLock.release();
- } catch (IOException e) {
- Log.e(TAG, "Failed to release lock on " + lockFile.getPath());
- // Exception while releasing the lock is bad, we want to report it, but not at
- // the price of overriding any already pending exception.
- releaseLockException = e;
- }
- }
- if (lockChannel != null) {
- closeQuietly(lockChannel);
+ } else {
+ if (forceReload) {
+ Log.i(TAG, "Forced extraction must be performed.");
+ } else {
+ Log.i(TAG, "Detected that extraction must be performed.");
}
- closeQuietly(lockRaf);
- }
-
- if (releaseLockException != null) {
- throw releaseLockException;
+ files = performExtractions();
+ putStoredApkInfo(context, prefsKeyPrefix, getTimeStamp(sourceApk), sourceCrc,
+ files);
}
Log.i(TAG, "load found " + files.size() + " secondary dex files");
return files;
}
+ @Override
+ public void close() throws IOException {
+ cacheLock.release();
+ lockChannel.close();
+ lockRaf.close();
+ }
+
/**
* Load previously extracted secondary dex files. Should be called only while owning the lock on
* {@link #LOCK_FILENAME}.
*/
- private static List<ExtractedDex> loadExistingExtractions(
- Context context, File sourceApk, File dexDir,
+ private List<ExtractedDex> loadExistingExtractions(
+ Context context,
String prefsKeyPrefix)
throws IOException {
Log.i(TAG, "loading existing secondary dex files");
@@ -228,16 +239,14 @@ final class MultiDexExtractor {
return computedValue;
}
- private static List<ExtractedDex> performExtractions(File sourceApk, File dexDir)
- throws IOException {
+ private List<ExtractedDex> performExtractions() throws IOException {
final String extractedFilePrefix = sourceApk.getName() + EXTRACTED_NAME_EXT;
- // Ensure that whatever deletions happen in prepareDexDir only happen if the zip that
- // contains a secondary dex file in there is not consistent with the latest apk. Otherwise,
- // multi-process race conditions can cause a crash loop where one process deletes the zip
- // while another had created it.
- prepareDexDir(dexDir, extractedFilePrefix);
+ // It is safe to fully clear the dex dir because we own the file lock so no other process is
+ // extracting or running optimizing dexopt. It may cause crash of already running
+ // applications if for whatever reason we end up extracting again over a valid extraction.
+ clearDexDir();
List<ExtractedDex> files = new ArrayList<ExtractedDex>();
@@ -272,9 +281,9 @@ final class MultiDexExtractor {
}
// Log size and crc of the extracted zip file
- Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed") +
- " - length " + extractedFile.getAbsolutePath() + ": " +
- extractedFile.length() + " - crc: " + extractedFile.crc);
+ Log.i(TAG, "Extraction " + (isExtractionSuccessful ? "succeeded" : "failed")
+ + " '" + extractedFile.getAbsolutePath() + "': length "
+ + extractedFile.length() + " - crc: " + extractedFile.crc);
if (!isExtractionSuccessful) {
// Delete the extracted file
extractedFile.delete();
@@ -339,19 +348,15 @@ final class MultiDexExtractor {
}
/**
- * This removes old files.
+ * Clear the dex dir from all files but the lock.
*/
- private static void prepareDexDir(File dexDir, final String extractedFilePrefix) {
- FileFilter filter = new FileFilter() {
-
+ private void clearDexDir() {
+ File[] files = dexDir.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
- String name = pathname.getName();
- return !(name.startsWith(extractedFilePrefix)
- || name.equals(LOCK_FILENAME));
+ return !pathname.getName().equals(LOCK_FILENAME);
}
- };
- File[] files = dexDir.listFiles(filter);
+ });
if (files == null) {
Log.w(TAG, "Failed to list secondary dex dir content (" + dexDir.getPath() + ").");
return;
diff --git a/android/support/transition/TransitionSet.java b/android/support/transition/TransitionSet.java
index 404245a1..24075bbf 100644
--- a/android/support/transition/TransitionSet.java
+++ b/android/support/transition/TransitionSet.java
@@ -51,7 +51,7 @@ import java.util.ArrayList;
* transition on the affected view targets:</p>
* <pre>
* &lt;transitionSet xmlns:android="http://schemas.android.com/apk/res/android"
- * android:ordering="sequential"&gt;
+ * android:transitionOrdering="sequential"&gt;
* &lt;fade/&gt;
* &lt;changeBounds/&gt;
* &lt;/transitionSet&gt;
@@ -561,6 +561,15 @@ public class TransitionSet extends Transition {
}
@Override
+ public void setPropagation(TransitionPropagation propagation) {
+ super.setPropagation(propagation);
+ int numTransitions = mTransitions.size();
+ for (int i = 0; i < numTransitions; ++i) {
+ mTransitions.get(i).setPropagation(propagation);
+ }
+ }
+
+ @Override
public void setEpicenterCallback(EpicenterCallback epicenterCallback) {
super.setEpicenterCallback(epicenterCallback);
int numTransitions = mTransitions.size();
diff --git a/android/support/transition/TransitionSetTest.java b/android/support/transition/TransitionSetTest.java
index aec9ecb2..d82cd498 100644
--- a/android/support/transition/TransitionSetTest.java
+++ b/android/support/transition/TransitionSetTest.java
@@ -120,4 +120,12 @@ public class TransitionSetTest extends BaseTest {
assertThat(mTransition.getTargetTypes(), hasSize(0));
}
+ @Test
+ public void testSetPropagation() {
+ final TransitionPropagation propagation = new SidePropagation();
+ mTransitionSet.setPropagation(propagation);
+ assertThat(mTransitionSet.getPropagation(), is(propagation));
+ assertThat(mTransition.getPropagation(), is(propagation));
+ }
+
}
diff --git a/android/support/v13/view/DragAndDropPermissionsCompat.java b/android/support/v13/view/DragAndDropPermissionsCompat.java
index 13ed2038..5fe61da9 100644
--- a/android/support/v13/view/DragAndDropPermissionsCompat.java
+++ b/android/support/v13/view/DragAndDropPermissionsCompat.java
@@ -20,57 +20,16 @@ import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.app.Activity;
import android.os.Build;
-import android.support.annotation.RequiresApi;
+import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.view.DragAndDropPermissions;
import android.view.DragEvent;
/**
- * Helper for accessing features in {@link android.view.DragAndDropPermissions}
- * introduced after API level 13 in a backwards compatible fashion.
+ * Helper for accessing features in {@link android.view.DragAndDropPermissions} a backwards
+ * compatible fashion.
*/
public final class DragAndDropPermissionsCompat {
-
- interface DragAndDropPermissionsCompatImpl {
- Object request(Activity activity, DragEvent dragEvent);
- void release(Object dragAndDropPermissions);
- }
-
- static class BaseDragAndDropPermissionsCompatImpl implements DragAndDropPermissionsCompatImpl {
- @Override
- public Object request(Activity activity, DragEvent dragEvent) {
- return null;
- }
-
- @Override
- public void release(Object dragAndDropPermissions) {
- // no-op
- }
- }
-
- @RequiresApi(24)
- static class Api24DragAndDropPermissionsCompatImpl
- extends BaseDragAndDropPermissionsCompatImpl {
- @Override
- public Object request(Activity activity, DragEvent dragEvent) {
- return activity.requestDragAndDropPermissions(dragEvent);
- }
-
- @Override
- public void release(Object dragAndDropPermissions) {
- ((DragAndDropPermissions) dragAndDropPermissions).release();
- }
- }
-
- private static DragAndDropPermissionsCompatImpl IMPL;
- static {
- if (Build.VERSION.SDK_INT >= 24) {
- IMPL = new Api24DragAndDropPermissionsCompatImpl();
- } else {
- IMPL = new BaseDragAndDropPermissionsCompatImpl();
- }
- }
-
private Object mDragAndDropPermissions;
private DragAndDropPermissionsCompat(Object dragAndDropPermissions) {
@@ -79,18 +38,24 @@ public final class DragAndDropPermissionsCompat {
/** @hide */
@RestrictTo(LIBRARY_GROUP)
+ @Nullable
public static DragAndDropPermissionsCompat request(Activity activity, DragEvent dragEvent) {
- Object dragAndDropPermissions = IMPL.request(activity, dragEvent);
- if (dragAndDropPermissions != null) {
- return new DragAndDropPermissionsCompat(dragAndDropPermissions);
+ if (Build.VERSION.SDK_INT >= 24) {
+ DragAndDropPermissions dragAndDropPermissions =
+ activity.requestDragAndDropPermissions(dragEvent);
+ if (dragAndDropPermissions != null) {
+ return new DragAndDropPermissionsCompat(dragAndDropPermissions);
+ }
}
return null;
}
- /*
+ /**
* Revoke the permission grant explicitly.
*/
public void release() {
- IMPL.release(mDragAndDropPermissions);
+ if (Build.VERSION.SDK_INT >= 24) {
+ ((DragAndDropPermissions) mDragAndDropPermissions).release();
+ }
}
}
diff --git a/android/support/v13/view/DragStartHelper.java b/android/support/v13/view/DragStartHelper.java
index 85bc2f36..f8aed922 100644
--- a/android/support/v13/view/DragStartHelper.java
+++ b/android/support/v13/view/DragStartHelper.java
@@ -69,8 +69,8 @@ import android.view.View;
* </pre>
*/
public class DragStartHelper {
- final private View mView;
- final private OnDragStartListener mListener;
+ private final View mView;
+ private final OnDragStartListener mListener;
private int mLastTouchX, mLastTouchY;
private boolean mDragging;
diff --git a/android/support/v13/view/inputmethod/EditorInfoCompat.java b/android/support/v13/view/inputmethod/EditorInfoCompat.java
index 92743c25..309877dc 100644
--- a/android/support/v13/view/inputmethod/EditorInfoCompat.java
+++ b/android/support/v13/view/inputmethod/EditorInfoCompat.java
@@ -16,7 +16,6 @@
package android.support.v13.view.inputmethod;
-import android.support.annotation.RequiresApi;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
@@ -24,8 +23,7 @@ import android.support.annotation.Nullable;
import android.view.inputmethod.EditorInfo;
/**
- * Helper for accessing features in {@link EditorInfo} introduced after API level 13 in a backwards
- * compatible fashion.
+ * Helper for accessing features in {@link EditorInfo} in a backwards compatible fashion.
*/
public final class EditorInfoCompat {
@@ -69,63 +67,10 @@ public final class EditorInfoCompat {
*/
public static final int IME_FLAG_FORCE_ASCII = 0x80000000;
- private interface EditorInfoCompatImpl {
- void setContentMimeTypes(@NonNull EditorInfo editorInfo,
- @Nullable String[] contentMimeTypes);
- @NonNull
- String[] getContentMimeTypes(@NonNull EditorInfo editorInfo);
- }
-
private static final String[] EMPTY_STRING_ARRAY = new String[0];
- private static final class EditorInfoCompatBaseImpl implements EditorInfoCompatImpl {
- private static String CONTENT_MIME_TYPES_KEY =
- "android.support.v13.view.inputmethod.EditorInfoCompat.CONTENT_MIME_TYPES";
-
- @Override
- public void setContentMimeTypes(@NonNull EditorInfo editorInfo,
- @Nullable String[] contentMimeTypes) {
- if (editorInfo.extras == null) {
- editorInfo.extras = new Bundle();
- }
- editorInfo.extras.putStringArray(CONTENT_MIME_TYPES_KEY, contentMimeTypes);
- }
-
- @NonNull
- @Override
- public String[] getContentMimeTypes(@NonNull EditorInfo editorInfo) {
- if (editorInfo.extras == null) {
- return EMPTY_STRING_ARRAY;
- }
- String[] result = editorInfo.extras.getStringArray(CONTENT_MIME_TYPES_KEY);
- return result != null ? result : EMPTY_STRING_ARRAY;
- }
- }
-
- @RequiresApi(25)
- private static final class EditorInfoCompatApi25Impl implements EditorInfoCompatImpl {
- @Override
- public void setContentMimeTypes(@NonNull EditorInfo editorInfo,
- @Nullable String[] contentMimeTypes) {
- editorInfo.contentMimeTypes = contentMimeTypes;
- }
-
- @NonNull
- @Override
- public String[] getContentMimeTypes(@NonNull EditorInfo editorInfo) {
- final String[] result = editorInfo.contentMimeTypes;
- return result != null ? result : EMPTY_STRING_ARRAY;
- }
- }
-
- private static final EditorInfoCompatImpl IMPL;
- static {
- if (Build.VERSION.SDK_INT >= 25) {
- IMPL = new EditorInfoCompatApi25Impl();
- } else {
- IMPL = new EditorInfoCompatBaseImpl();
- }
- }
+ private static final String CONTENT_MIME_TYPES_KEY =
+ "android.support.v13.view.inputmethod.EditorInfoCompat.CONTENT_MIME_TYPES";
/**
* Sets MIME types that can be accepted by the target editor if the IME calls
@@ -140,7 +85,14 @@ public final class EditorInfoCompat {
*/
public static void setContentMimeTypes(@NonNull EditorInfo editorInfo,
@Nullable String[] contentMimeTypes) {
- IMPL.setContentMimeTypes(editorInfo, contentMimeTypes);
+ if (Build.VERSION.SDK_INT >= 25) {
+ editorInfo.contentMimeTypes = contentMimeTypes;
+ } else {
+ if (editorInfo.extras == null) {
+ editorInfo.extras = new Bundle();
+ }
+ editorInfo.extras.putStringArray(CONTENT_MIME_TYPES_KEY, contentMimeTypes);
+ }
}
/**
@@ -155,7 +107,16 @@ public final class EditorInfoCompat {
*/
@NonNull
public static String[] getContentMimeTypes(EditorInfo editorInfo) {
- return IMPL.getContentMimeTypes(editorInfo);
+ if (Build.VERSION.SDK_INT >= 25) {
+ final String[] result = editorInfo.contentMimeTypes;
+ return result != null ? result : EMPTY_STRING_ARRAY;
+ } else {
+ if (editorInfo.extras == null) {
+ return EMPTY_STRING_ARRAY;
+ }
+ String[] result = editorInfo.extras.getStringArray(CONTENT_MIME_TYPES_KEY);
+ return result != null ? result : EMPTY_STRING_ARRAY;
+ }
}
}
diff --git a/android/support/v13/view/inputmethod/InputConnectionCompat.java b/android/support/v13/view/inputmethod/InputConnectionCompat.java
index 5999575b..d77389bf 100644
--- a/android/support/v13/view/inputmethod/InputConnectionCompat.java
+++ b/android/support/v13/view/inputmethod/InputConnectionCompat.java
@@ -16,7 +16,6 @@
package android.support.v13.view.inputmethod;
-import android.support.annotation.RequiresApi;
import android.content.ClipDescription;
import android.net.Uri;
import android.os.Build;
@@ -36,138 +35,50 @@ import android.view.inputmethod.InputContentInfo;
*/
public final class InputConnectionCompat {
- private interface InputConnectionCompatImpl {
- boolean commitContent(@NonNull InputConnection inputConnection,
- @NonNull InputContentInfoCompat inputContentInfo, int flags, @Nullable Bundle opts);
-
- @NonNull
- InputConnection createWrapper(@NonNull InputConnection ic,
- @NonNull EditorInfo editorInfo, @NonNull OnCommitContentListener callback);
- }
-
- static final class InputContentInfoCompatBaseImpl implements InputConnectionCompatImpl {
-
- private static String COMMIT_CONTENT_ACTION =
- "android.support.v13.view.inputmethod.InputConnectionCompat.COMMIT_CONTENT";
- private static String COMMIT_CONTENT_CONTENT_URI_KEY =
- "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_URI";
- private static String COMMIT_CONTENT_DESCRIPTION_KEY =
- "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_DESCRIPTION";
- private static String COMMIT_CONTENT_LINK_URI_KEY =
- "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_LINK_URI";
- private static String COMMIT_CONTENT_OPTS_KEY =
- "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_OPTS";
- private static String COMMIT_CONTENT_FLAGS_KEY =
- "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_FLAGS";
- private static String COMMIT_CONTENT_RESULT_RECEIVER =
- "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_RESULT_RECEIVER";
-
- @Override
- public boolean commitContent(@NonNull InputConnection inputConnection,
- @NonNull InputContentInfoCompat inputContentInfo, int flags,
- @Nullable Bundle opts) {
- final Bundle params = new Bundle();
- params.putParcelable(COMMIT_CONTENT_CONTENT_URI_KEY, inputContentInfo.getContentUri());
- params.putParcelable(COMMIT_CONTENT_DESCRIPTION_KEY, inputContentInfo.getDescription());
- params.putParcelable(COMMIT_CONTENT_LINK_URI_KEY, inputContentInfo.getLinkUri());
- params.putInt(COMMIT_CONTENT_FLAGS_KEY, flags);
- params.putParcelable(COMMIT_CONTENT_OPTS_KEY, opts);
- // TODO: Support COMMIT_CONTENT_RESULT_RECEIVER.
- return inputConnection.performPrivateCommand(COMMIT_CONTENT_ACTION, params);
+ private static final String COMMIT_CONTENT_ACTION =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.COMMIT_CONTENT";
+ private static final String COMMIT_CONTENT_CONTENT_URI_KEY =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_URI";
+ private static final String COMMIT_CONTENT_DESCRIPTION_KEY =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_DESCRIPTION";
+ private static final String COMMIT_CONTENT_LINK_URI_KEY =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_LINK_URI";
+ private static final String COMMIT_CONTENT_OPTS_KEY =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_OPTS";
+ private static final String COMMIT_CONTENT_FLAGS_KEY =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_FLAGS";
+ private static final String COMMIT_CONTENT_RESULT_RECEIVER =
+ "android.support.v13.view.inputmethod.InputConnectionCompat.CONTENT_RESULT_RECEIVER";
+
+ static boolean handlePerformPrivateCommand(
+ @Nullable String action,
+ @NonNull Bundle data,
+ @NonNull OnCommitContentListener onCommitContentListener) {
+ if (!TextUtils.equals(COMMIT_CONTENT_ACTION, action)) {
+ return false;
}
-
- @NonNull
- @Override
- public InputConnection createWrapper(@NonNull InputConnection ic,
- @NonNull EditorInfo editorInfo,
- @NonNull OnCommitContentListener onCommitContentListener) {
- String[] contentMimeTypes = EditorInfoCompat.getContentMimeTypes(editorInfo);
- if (contentMimeTypes.length == 0) {
- return ic;
- }
- final OnCommitContentListener listener = onCommitContentListener;
- return new InputConnectionWrapper(ic, false /* mutable */) {
- @Override
- public boolean performPrivateCommand(String action, Bundle data) {
- if (InputContentInfoCompatBaseImpl.handlePerformPrivateCommand(action, data,
- listener)) {
- return true;
- }
- return super.performPrivateCommand(action, data);
- }
- };
+ if (data == null) {
+ return false;
}
-
- static boolean handlePerformPrivateCommand(
- @Nullable String action,
- @NonNull Bundle data,
- @NonNull OnCommitContentListener onCommitContentListener) {
- if (!TextUtils.equals(COMMIT_CONTENT_ACTION, action)) {
- return false;
+ ResultReceiver resultReceiver = null;
+ boolean result = false;
+ try {
+ resultReceiver = data.getParcelable(COMMIT_CONTENT_RESULT_RECEIVER);
+ final Uri contentUri = data.getParcelable(COMMIT_CONTENT_CONTENT_URI_KEY);
+ final ClipDescription description = data.getParcelable(
+ COMMIT_CONTENT_DESCRIPTION_KEY);
+ final Uri linkUri = data.getParcelable(COMMIT_CONTENT_LINK_URI_KEY);
+ final int flags = data.getInt(COMMIT_CONTENT_FLAGS_KEY);
+ final Bundle opts = data.getParcelable(COMMIT_CONTENT_OPTS_KEY);
+ final InputContentInfoCompat inputContentInfo =
+ new InputContentInfoCompat(contentUri, description, linkUri);
+ result = onCommitContentListener.onCommitContent(inputContentInfo, flags, opts);
+ } finally {
+ if (resultReceiver != null) {
+ resultReceiver.send(result ? 1 : 0, null);
}
- if (data == null) {
- return false;
- }
- ResultReceiver resultReceiver = null;
- boolean result = false;
- try {
- resultReceiver = data.getParcelable(COMMIT_CONTENT_RESULT_RECEIVER);
- final Uri contentUri = data.getParcelable(COMMIT_CONTENT_CONTENT_URI_KEY);
- final ClipDescription description = data.getParcelable(
- COMMIT_CONTENT_DESCRIPTION_KEY);
- final Uri linkUri = data.getParcelable(COMMIT_CONTENT_LINK_URI_KEY);
- final int flags = data.getInt(COMMIT_CONTENT_FLAGS_KEY);
- final Bundle opts = data.getParcelable(COMMIT_CONTENT_OPTS_KEY);
- final InputContentInfoCompat inputContentInfo =
- new InputContentInfoCompat(contentUri, description, linkUri);
- result = onCommitContentListener.onCommitContent(inputContentInfo, flags, opts);
- } finally {
- if (resultReceiver != null) {
- resultReceiver.send(result ? 1 : 0, null);
- }
- }
- return result;
- }
- }
-
- @RequiresApi(25)
- private static final class InputContentInfoCompatApi25Impl
- implements InputConnectionCompatImpl {
- @Override
- public boolean commitContent(@NonNull InputConnection inputConnection,
- @NonNull InputContentInfoCompat inputContentInfo, int flags,
- @Nullable Bundle opts) {
- return inputConnection.commitContent((InputContentInfo) inputContentInfo.unwrap(),
- flags, opts);
- }
-
- @Nullable
- @Override
- public InputConnection createWrapper(
- @Nullable InputConnection inputConnection, @NonNull EditorInfo editorInfo,
- @Nullable OnCommitContentListener onCommitContentListener) {
- final OnCommitContentListener listener = onCommitContentListener;
- return new InputConnectionWrapper(inputConnection, false /* mutable */) {
- @Override
- public boolean commitContent(InputContentInfo inputContentInfo, int flags,
- Bundle opts) {
- if (listener.onCommitContent(InputContentInfoCompat.wrap(inputContentInfo),
- flags, opts)) {
- return true;
- }
- return super.commitContent(inputContentInfo, flags, opts);
- }
- };
- }
- }
-
- private static final InputConnectionCompatImpl IMPL;
- static {
- if (Build.VERSION.SDK_INT >= 25) {
- IMPL = new InputContentInfoCompatApi25Impl();
- } else {
- IMPL = new InputContentInfoCompatBaseImpl();
}
+ return result;
}
/**
@@ -196,7 +107,19 @@ public final class InputConnectionCompat {
return false;
}
- return IMPL.commitContent(inputConnection, inputContentInfo, flags, opts);
+ if (Build.VERSION.SDK_INT >= 25) {
+ return inputConnection.commitContent(
+ (InputContentInfo) inputContentInfo.unwrap(), flags, opts);
+ } else {
+ final Bundle params = new Bundle();
+ params.putParcelable(COMMIT_CONTENT_CONTENT_URI_KEY, inputContentInfo.getContentUri());
+ params.putParcelable(COMMIT_CONTENT_DESCRIPTION_KEY, inputContentInfo.getDescription());
+ params.putParcelable(COMMIT_CONTENT_LINK_URI_KEY, inputContentInfo.getLinkUri());
+ params.putInt(COMMIT_CONTENT_FLAGS_KEY, flags);
+ params.putParcelable(COMMIT_CONTENT_OPTS_KEY, opts);
+ // TODO: Support COMMIT_CONTENT_RESULT_RECEIVER.
+ return inputConnection.performPrivateCommand(COMMIT_CONTENT_ACTION, params);
+ }
}
/**
@@ -276,7 +199,35 @@ public final class InputConnectionCompat {
if (onCommitContentListener == null) {
throw new IllegalArgumentException("onCommitContentListener must be non-null");
}
- return IMPL.createWrapper(inputConnection, editorInfo, onCommitContentListener);
+ if (Build.VERSION.SDK_INT >= 25) {
+ final OnCommitContentListener listener = onCommitContentListener;
+ return new InputConnectionWrapper(inputConnection, false /* mutable */) {
+ @Override
+ public boolean commitContent(InputContentInfo inputContentInfo, int flags,
+ Bundle opts) {
+ if (listener.onCommitContent(InputContentInfoCompat.wrap(inputContentInfo),
+ flags, opts)) {
+ return true;
+ }
+ return super.commitContent(inputContentInfo, flags, opts);
+ }
+ };
+ } else {
+ String[] contentMimeTypes = EditorInfoCompat.getContentMimeTypes(editorInfo);
+ if (contentMimeTypes.length == 0) {
+ return inputConnection;
+ }
+ final OnCommitContentListener listener = onCommitContentListener;
+ return new InputConnectionWrapper(inputConnection, false /* mutable */) {
+ @Override
+ public boolean performPrivateCommand(String action, Bundle data) {
+ if (InputConnectionCompat.handlePerformPrivateCommand(action, data, listener)) {
+ return true;
+ }
+ return super.performPrivateCommand(action, data);
+ }
+ };
+ }
}
}
diff --git a/android/support/v14/preference/EditTextPreferenceDialogFragment.java b/android/support/v14/preference/EditTextPreferenceDialogFragment.java
index 3ee58725..23b88288 100644
--- a/android/support/v14/preference/EditTextPreferenceDialogFragment.java
+++ b/android/support/v14/preference/EditTextPreferenceDialogFragment.java
@@ -11,7 +11,7 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
package android.support.v14.preference;
@@ -65,8 +65,8 @@ public class EditTextPreferenceDialogFragment extends PreferenceDialogFragment {
mEditText = (EditText) view.findViewById(android.R.id.edit);
if (mEditText == null) {
- throw new IllegalStateException("Dialog view must contain an EditText with id" +
- " @android:id/edit");
+ throw new IllegalStateException("Dialog view must contain an EditText with id"
+ + " @android:id/edit");
}
mEditText.setText(mText);
diff --git a/android/support/v14/preference/ListPreferenceDialogFragment.java b/android/support/v14/preference/ListPreferenceDialogFragment.java
index 6119071b..5374cd53 100644
--- a/android/support/v14/preference/ListPreferenceDialogFragment.java
+++ b/android/support/v14/preference/ListPreferenceDialogFragment.java
@@ -11,7 +11,7 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
package android.support.v14.preference;
diff --git a/android/support/v14/preference/MultiSelectListPreference.java b/android/support/v14/preference/MultiSelectListPreference.java
index f34b7dcd..16351fec 100644
--- a/android/support/v14/preference/MultiSelectListPreference.java
+++ b/android/support/v14/preference/MultiSelectListPreference.java
@@ -11,7 +11,7 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
package android.support.v14.preference;
@@ -23,6 +23,7 @@ import android.os.Parcelable;
import android.support.annotation.ArrayRes;
import android.support.annotation.NonNull;
import android.support.v4.content.res.TypedArrayUtils;
+import android.support.v7.preference.R;
import android.support.v7.preference.internal.AbstractMultiSelectListPreference;
import android.util.AttributeSet;
@@ -35,7 +36,7 @@ import java.util.Set;
* a dialog.
* <p>
* This preference will store a set of strings into the SharedPreferences.
- * This set will contain one or more values from the
+ * This set will contain one or more mValues from the
* {@link #setEntryValues(CharSequence[])} array.
*
* @attr name android:entries
@@ -51,16 +52,16 @@ public class MultiSelectListPreference extends AbstractMultiSelectListPreference
super(context, attrs, defStyleAttr, defStyleRes);
final TypedArray a = context.obtainStyledAttributes(attrs,
- android.support.v7.preference.R.styleable.MultiSelectListPreference, defStyleAttr,
+ R.styleable.MultiSelectListPreference, defStyleAttr,
defStyleRes);
mEntries = TypedArrayUtils.getTextArray(a,
- android.support.v7.preference.R.styleable.MultiSelectListPreference_entries,
- android.support.v7.preference.R.styleable.MultiSelectListPreference_android_entries);
+ R.styleable.MultiSelectListPreference_entries,
+ R.styleable.MultiSelectListPreference_android_entries);
mEntryValues = TypedArrayUtils.getTextArray(a,
- android.support.v7.preference.R.styleable.MultiSelectListPreference_entryValues,
- android.support.v7.preference.R.styleable.MultiSelectListPreference_android_entryValues);
+ R.styleable.MultiSelectListPreference_entryValues,
+ R.styleable.MultiSelectListPreference_android_entryValues);
a.recycle();
}
@@ -71,7 +72,7 @@ public class MultiSelectListPreference extends AbstractMultiSelectListPreference
public MultiSelectListPreference(Context context, AttributeSet attrs) {
this(context, attrs, TypedArrayUtils.getAttr(context,
- android.support.v7.preference.R.attr.dialogPreferenceStyle,
+ R.attr.dialogPreferenceStyle,
android.R.attr.dialogPreferenceStyle));
}
@@ -116,7 +117,7 @@ public class MultiSelectListPreference extends AbstractMultiSelectListPreference
* entries is selected. If a user clicks on the second item in entries, the
* second item in this array will be saved to the preference.
*
- * @param entryValues The array to be used as values to save for the preference.
+ * @param entryValues The array to be used as mValues to save for the preference.
*/
public void setEntryValues(CharSequence[] entryValues) {
mEntryValues = entryValues;
@@ -124,16 +125,16 @@ public class MultiSelectListPreference extends AbstractMultiSelectListPreference
/**
* @see #setEntryValues(CharSequence[])
- * @param entryValuesResId The entry values array as a resource.
+ * @param entryValuesResId The entry mValues array as a resource.
*/
public void setEntryValues(@ArrayRes int entryValuesResId) {
setEntryValues(getContext().getResources().getTextArray(entryValuesResId));
}
/**
- * Returns the array of values to be saved for the preference.
+ * Returns the array of mValues to be saved for the preference.
*
- * @return The array of values.
+ * @return The array of mValues.
*/
@Override
public CharSequence[] getEntryValues() {
@@ -144,7 +145,7 @@ public class MultiSelectListPreference extends AbstractMultiSelectListPreference
* Sets the value of the key. This should contain entries in
* {@link #getEntryValues()}.
*
- * @param values The values to set for the key.
+ * @param values The mValues to set for the key.
*/
@Override
public void setValues(Set<String> values) {
@@ -163,7 +164,7 @@ public class MultiSelectListPreference extends AbstractMultiSelectListPreference
}
/**
- * Returns the index of the given value (in the entry values array).
+ * Returns the index of the given value (in the entry mValues array).
*
* @param value The value whose index should be returned.
* @return The index of the value, or -1 if not found.
@@ -219,7 +220,7 @@ public class MultiSelectListPreference extends AbstractMultiSelectListPreference
}
final SavedState myState = new SavedState(superState);
- myState.values = getValues();
+ myState.mValues = getValues();
return myState;
}
@@ -233,31 +234,31 @@ public class MultiSelectListPreference extends AbstractMultiSelectListPreference
SavedState myState = (SavedState) state;
super.onRestoreInstanceState(myState.getSuperState());
- setValues(myState.values);
+ setValues(myState.mValues);
}
private static class SavedState extends BaseSavedState {
- Set<String> values;
+ Set<String> mValues;
- public SavedState(Parcel source) {
+ SavedState(Parcel source) {
super(source);
final int size = source.readInt();
- values = new HashSet<>();
+ mValues = new HashSet<>();
String[] strings = new String[size];
source.readStringArray(strings);
- Collections.addAll(values, strings);
+ Collections.addAll(mValues, strings);
}
- public SavedState(Parcelable superState) {
+ SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
super.writeToParcel(dest, flags);
- dest.writeInt(values.size());
- dest.writeStringArray(values.toArray(new String[values.size()]));
+ dest.writeInt(mValues.size());
+ dest.writeStringArray(mValues.toArray(new String[mValues.size()]));
}
public static final Parcelable.Creator<SavedState> CREATOR =
diff --git a/android/support/v14/preference/MultiSelectListPreferenceDialogFragment.java b/android/support/v14/preference/MultiSelectListPreferenceDialogFragment.java
index 81925833..db81644a 100644
--- a/android/support/v14/preference/MultiSelectListPreferenceDialogFragment.java
+++ b/android/support/v14/preference/MultiSelectListPreferenceDialogFragment.java
@@ -11,7 +11,7 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
package android.support.v14.preference;
@@ -60,8 +60,8 @@ public class MultiSelectListPreferenceDialogFragment extends PreferenceDialogFra
if (preference.getEntries() == null || preference.getEntryValues() == null) {
throw new IllegalStateException(
- "MultiSelectListPreference requires an entries array and " +
- "an entryValues array.");
+ "MultiSelectListPreference requires an entries array and "
+ + "an entryValues array.");
}
mNewValues.clear();
diff --git a/android/support/v14/preference/PreferenceDialogFragment.java b/android/support/v14/preference/PreferenceDialogFragment.java
index e7b9f403..a4ae4a96 100644
--- a/android/support/v14/preference/PreferenceDialogFragment.java
+++ b/android/support/v14/preference/PreferenceDialogFragment.java
@@ -11,7 +11,7 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
package android.support.v14.preference;
@@ -78,8 +78,8 @@ public abstract class PreferenceDialogFragment extends DialogFragment implements
final Fragment rawFragment = getTargetFragment();
if (!(rawFragment instanceof DialogPreference.TargetFragment)) {
- throw new IllegalStateException("Target fragment must implement TargetFragment" +
- " interface");
+ throw new IllegalStateException("Target fragment must implement TargetFragment"
+ + " interface");
}
final DialogPreference.TargetFragment fragment =
@@ -132,9 +132,9 @@ public abstract class PreferenceDialogFragment extends DialogFragment implements
}
}
+ @NonNull
@Override
- public @NonNull
- Dialog onCreateDialog(Bundle savedInstanceState) {
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
final Context context = getActivity();
mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE;
diff --git a/android/support/v14/preference/PreferenceFragment.java b/android/support/v14/preference/PreferenceFragment.java
index 24210505..406465f1 100644
--- a/android/support/v14/preference/PreferenceFragment.java
+++ b/android/support/v14/preference/PreferenceFragment.java
@@ -11,7 +11,7 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License
+ * limitations under the License.
*/
package android.support.v14.preference;
@@ -44,6 +44,7 @@ import android.support.v7.preference.PreferenceManager;
import android.support.v7.preference.PreferenceRecyclerViewAccessibilityDelegate;
import android.support.v7.preference.PreferenceScreen;
import android.support.v7.preference.PreferenceViewHolder;
+import android.support.v7.preference.R;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.util.TypedValue;
@@ -145,7 +146,7 @@ public abstract class PreferenceFragment extends Fragment implements
private final DividerDecoration mDividerDecoration = new DividerDecoration();
private static final int MSG_BIND_PREFERENCES = 1;
- private Handler mHandler = new Handler() {
+ private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
@@ -157,7 +158,7 @@ public abstract class PreferenceFragment extends Fragment implements
}
};
- final private Runnable mRequestFocus = new Runnable() {
+ private final Runnable mRequestFocus = new Runnable() {
@Override
public void run() {
mList.focusableViewAvailable(mList);
@@ -484,7 +485,7 @@ public abstract class PreferenceFragment extends Fragment implements
handled = ((OnPreferenceStartFragmentCallback) getCallbackFragment())
.onPreferenceStartFragment(this, preference);
}
- if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback){
+ if (!handled && getActivity() instanceof OnPreferenceStartFragmentCallback) {
handled = ((OnPreferenceStartFragmentCallback) getActivity())
.onPreferenceStartFragment(this, preference);
}
@@ -656,8 +657,8 @@ public abstract class PreferenceFragment extends Fragment implements
} else if (preference instanceof MultiSelectListPreference) {
f = MultiSelectListPreferenceDialogFragment.newInstance(preference.getKey());
} else {
- throw new IllegalArgumentException("Tried to display dialog for unknown " +
- "preference type. Did you forget to override onDisplayPreferenceDialog()?");
+ throw new IllegalArgumentException("Tried to display dialog for unknown "
+ + "preference type. Did you forget to override onDisplayPreferenceDialog()?");
}
f.setTargetFragment(this, 0);
f.show(getFragmentManager(), DIALOG_FRAGMENT_TAG);
@@ -686,8 +687,7 @@ public abstract class PreferenceFragment extends Fragment implements
@Override
public void run() {
final RecyclerView.Adapter adapter = mList.getAdapter();
- if (!(adapter instanceof
- PreferenceGroup.PreferencePositionCallback)) {
+ if (!(adapter instanceof PreferenceGroup.PreferencePositionCallback)) {
if (adapter != null) {
throw new IllegalStateException("Adapter must implement "
+ "PreferencePositionCallback");
@@ -726,7 +726,7 @@ public abstract class PreferenceFragment extends Fragment implements
private final Preference mPreference;
private final String mKey;
- public ScrollToPreferenceObserver(RecyclerView.Adapter adapter, RecyclerView list,
+ ScrollToPreferenceObserver(RecyclerView.Adapter adapter, RecyclerView list,
Preference preference, String key) {
mAdapter = adapter;
mList = list;
diff --git a/android/support/v14/preference/SwitchPreference.java b/android/support/v14/preference/SwitchPreference.java
index eae20b8d..197de4e1 100644
--- a/android/support/v14/preference/SwitchPreference.java
+++ b/android/support/v14/preference/SwitchPreference.java
@@ -1,18 +1,18 @@
/*
-* 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
-*/
+ * 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.v14.preference;
@@ -24,6 +24,7 @@ import android.support.annotation.RestrictTo;
import android.support.v4.content.res.TypedArrayUtils;
import android.support.v7.preference.AndroidResources;
import android.support.v7.preference.PreferenceViewHolder;
+import android.support.v7.preference.R;
import android.support.v7.preference.TwoStatePreference;
import android.util.AttributeSet;
import android.view.View;
diff --git a/android/support/v17/leanback/app/PlaybackFragment.java b/android/support/v17/leanback/app/PlaybackFragment.java
index e2e6be48..dc59e0eb 100644
--- a/android/support/v17/leanback/app/PlaybackFragment.java
+++ b/android/support/v17/leanback/app/PlaybackFragment.java
@@ -29,6 +29,7 @@ import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.R;
import android.support.v17.leanback.animation.LogAccelerateInterpolator;
import android.support.v17.leanback.animation.LogDecelerateInterpolator;
@@ -106,6 +107,7 @@ public class PlaybackFragment extends Fragment {
* Resets the focus on the button in the middle of control row.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void resetFocus() {
ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView()
.findViewHolderForAdapterPosition(0);
@@ -185,6 +187,7 @@ public class PlaybackFragment extends Fragment {
* @hide
* @deprecated use {@link PlaybackSupportFragment}
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
@Deprecated
public static class OnFadeCompleteListener {
public void onFadeInComplete() {
@@ -366,6 +369,7 @@ public class PlaybackFragment extends Fragment {
* Sets the listener to be called when fade in or out has completed.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void setFadeCompleteListener(OnFadeCompleteListener listener) {
mFadeCompleteListener = listener;
}
@@ -374,6 +378,7 @@ public class PlaybackFragment extends Fragment {
* Returns the listener to be called when fade in or out has completed.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public OnFadeCompleteListener getFadeCompleteListener() {
return mFadeCompleteListener;
}
diff --git a/android/support/v17/leanback/app/PlaybackSupportFragment.java b/android/support/v17/leanback/app/PlaybackSupportFragment.java
index a8741aba..ee17e84d 100644
--- a/android/support/v17/leanback/app/PlaybackSupportFragment.java
+++ b/android/support/v17/leanback/app/PlaybackSupportFragment.java
@@ -26,6 +26,7 @@ import android.os.Handler;
import android.os.Message;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.R;
import android.support.v17.leanback.animation.LogAccelerateInterpolator;
import android.support.v17.leanback.animation.LogDecelerateInterpolator;
@@ -101,6 +102,7 @@ public class PlaybackSupportFragment extends Fragment {
* Resets the focus on the button in the middle of control row.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void resetFocus() {
ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) getVerticalGridView()
.findViewHolderForAdapterPosition(0);
@@ -179,6 +181,7 @@ public class PlaybackSupportFragment extends Fragment {
* completion events.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public static class OnFadeCompleteListener {
public void onFadeInComplete() {
}
@@ -359,6 +362,7 @@ public class PlaybackSupportFragment extends Fragment {
* Sets the listener to be called when fade in or out has completed.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public void setFadeCompleteListener(OnFadeCompleteListener listener) {
mFadeCompleteListener = listener;
}
@@ -367,6 +371,7 @@ public class PlaybackSupportFragment extends Fragment {
* Returns the listener to be called when fade in or out has completed.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public OnFadeCompleteListener getFadeCompleteListener() {
return mFadeCompleteListener;
}
diff --git a/android/support/v17/leanback/media/PlaybackControlGlue.java b/android/support/v17/leanback/media/PlaybackControlGlue.java
index 5bf6cc17..0a788f6e 100644
--- a/android/support/v17/leanback/media/PlaybackControlGlue.java
+++ b/android/support/v17/leanback/media/PlaybackControlGlue.java
@@ -20,6 +20,7 @@ import android.content.Context;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.Message;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.widget.AbstractDetailsDescriptionPresenter;
import android.support.v17.leanback.widget.Action;
import android.support.v17.leanback.widget.ArrayObjectAdapter;
@@ -356,6 +357,7 @@ public abstract class PlaybackControlGlue extends PlaybackGlue
/**
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
protected SparseArrayObjectAdapter createPrimaryActionsAdapter(
PresenterSelector presenterSelector) {
SparseArrayObjectAdapter adapter = new SparseArrayObjectAdapter(presenterSelector);
diff --git a/android/support/v17/leanback/media/PlaybackTransportControlGlue.java b/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
index 4aa9bf66..b81f979b 100644
--- a/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
+++ b/android/support/v17/leanback/media/PlaybackTransportControlGlue.java
@@ -365,7 +365,7 @@ public class PlaybackTransportControlGlue<T extends PlayerAdapter>
@Override
public void onSeekFinished(boolean cancelled) {
if (!cancelled) {
- if (mLastUserPosition > 0) {
+ if (mLastUserPosition >= 0) {
seekTo(mLastUserPosition);
}
} else {
diff --git a/android/support/v17/leanback/transition/TransitionEpicenterCallback.java b/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
index ec7f84cf..bb8e686a 100644
--- a/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
+++ b/android/support/v17/leanback/transition/TransitionEpicenterCallback.java
@@ -14,11 +14,13 @@
package android.support.v17.leanback.transition;
import android.graphics.Rect;
+import android.support.annotation.RestrictTo;
/**
* Class to get the epicenter of Transition.
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public abstract class TransitionEpicenterCallback {
/**
@@ -31,4 +33,3 @@ public abstract class TransitionEpicenterCallback {
*/
public abstract Rect onGetEpicenter(Object transition);
}
-
diff --git a/android/support/v17/leanback/util/MathUtil.java b/android/support/v17/leanback/util/MathUtil.java
index 487188de..bf74e405 100644
--- a/android/support/v17/leanback/util/MathUtil.java
+++ b/android/support/v17/leanback/util/MathUtil.java
@@ -13,10 +13,13 @@
*/
package android.support.v17.leanback.util;
+import android.support.annotation.RestrictTo;
+
/**
* Math Utilities for leanback library.
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public final class MathUtil {
private MathUtil() {
diff --git a/android/support/v17/leanback/widget/DetailsParallaxDrawable.java b/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
index 37e3480c..1eea7974 100644
--- a/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
+++ b/android/support/v17/leanback/widget/DetailsParallaxDrawable.java
@@ -22,6 +22,7 @@ import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.ColorInt;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.R;
import android.support.v17.leanback.graphics.CompositeDrawable;
import android.support.v17.leanback.graphics.FitWidthBitmapDrawable;
@@ -56,6 +57,7 @@ import android.util.TypedValue;
* </li>
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public class DetailsParallaxDrawable extends CompositeDrawable {
private Drawable mBottomDrawable;
diff --git a/android/support/v17/leanback/widget/GridLayoutManager.java b/android/support/v17/leanback/widget/GridLayoutManager.java
index 613198fd..810cb3bf 100644
--- a/android/support/v17/leanback/widget/GridLayoutManager.java
+++ b/android/support/v17/leanback/widget/GridLayoutManager.java
@@ -2645,8 +2645,9 @@ final class GridLayoutManager extends RecyclerView.LayoutManager {
mPrimaryScrollExtra = primaryScrollExtra;
View view = findViewByPosition(position);
// scrollToView() is based on Adapter position. Only call scrollToView() when item
- // is still valid.
- if (view != null && getAdapterPositionByView(view) == position) {
+ // is still valid and no layout is requested, otherwise defer to next layout pass.
+ if (!mBaseGridView.isLayoutRequested()
+ && view != null && getAdapterPositionByView(view) == position) {
mFlag |= PF_IN_SELECTION;
scrollToView(view, smooth);
mFlag &= ~PF_IN_SELECTION;
diff --git a/android/support/v17/leanback/widget/MediaRowFocusView.java b/android/support/v17/leanback/widget/MediaRowFocusView.java
index 1418a2a4..471f64e1 100644
--- a/android/support/v17/leanback/widget/MediaRowFocusView.java
+++ b/android/support/v17/leanback/widget/MediaRowFocusView.java
@@ -17,14 +17,16 @@ import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.RectF;
+import android.support.annotation.RestrictTo;
+import android.support.v17.leanback.R;
import android.util.AttributeSet;
import android.view.View;
-import android.support.v17.leanback.R;
/**
* Creates a view for a media item row in a playlist
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
class MediaRowFocusView extends View {
private final Paint mPaint;
diff --git a/android/support/v17/leanback/widget/ParallaxEffect.java b/android/support/v17/leanback/widget/ParallaxEffect.java
index 5c06e29b..e1af7626 100644
--- a/android/support/v17/leanback/widget/ParallaxEffect.java
+++ b/android/support/v17/leanback/widget/ParallaxEffect.java
@@ -17,6 +17,7 @@
package android.support.v17.leanback.widget;
import android.animation.PropertyValuesHolder;
+import android.support.annotation.RestrictTo;
import android.support.v17.leanback.widget.Parallax.FloatProperty;
import android.support.v17.leanback.widget.Parallax.FloatPropertyMarkerValue;
import android.support.v17.leanback.widget.Parallax.IntProperty;
@@ -70,6 +71,7 @@ public abstract class ParallaxEffect {
* @return A list of Float objects that represents weight associated with each variable range.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public final List<Float> getWeights() {
return mWeights;
}
@@ -96,6 +98,7 @@ public abstract class ParallaxEffect {
* range.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public final void setWeights(float... weights) {
for (float weight : weights) {
if (weight <= 0) {
@@ -121,6 +124,7 @@ public abstract class ParallaxEffect {
* @return This ParallaxEffect object, allowing calls to methods in this class to be chained.
* @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
public final ParallaxEffect weights(float... weights) {
setWeights(weights);
return this;
diff --git a/android/support/v17/leanback/widget/VideoSurfaceView.java b/android/support/v17/leanback/widget/VideoSurfaceView.java
index 29d778c0..d42a60d6 100644
--- a/android/support/v17/leanback/widget/VideoSurfaceView.java
+++ b/android/support/v17/leanback/widget/VideoSurfaceView.java
@@ -17,6 +17,7 @@
package android.support.v17.leanback.widget;
import android.content.Context;
+import android.support.annotation.RestrictTo;
import android.util.AttributeSet;
import android.view.SurfaceView;
@@ -26,6 +27,7 @@ import android.view.SurfaceView;
* This class disables setTransitionVisibility() to avoid the problem.
* @hide
*/
+@RestrictTo(RestrictTo.Scope.LIBRARY)
public class VideoSurfaceView extends SurfaceView {
public VideoSurfaceView(Context context) {
diff --git a/android/support/v4/app/ActivityCompat.java b/android/support/v4/app/ActivityCompat.java
index 9d15be1d..333871a0 100644
--- a/android/support/v4/app/ActivityCompat.java
+++ b/android/support/v4/app/ActivityCompat.java
@@ -29,6 +29,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.os.Parcelable;
+import android.support.annotation.IdRes;
import android.support.annotation.IntRange;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@@ -340,6 +341,31 @@ public class ActivityCompat extends ContextCompat {
}
/**
+ * Finds a view that was identified by the {@code android:id} XML attribute that was processed
+ * in {@link Activity#onCreate}, or throws an IllegalArgumentException if the ID is invalid, or
+ * there is no matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see Activity#findViewById(int)
+ * @see android.support.v4.view.ViewCompat#requireViewById(View, int)
+ */
+ @NonNull
+ public static <T extends View> T requireViewById(@NonNull Activity activity, @IdRes int id) {
+ // TODO: use and link to Activity#requireViewById() directly, once available
+ T view = activity.findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this Activity");
+ }
+ return view;
+ }
+
+ /**
* When {@link android.app.ActivityOptions#makeSceneTransitionAnimation(Activity,
* android.view.View, String)} was used to start an Activity, <var>callback</var>
* will be called to handle shared elements on the <i>launched</i> Activity. This requires
@@ -538,6 +564,7 @@ public class ActivityCompat extends ContextCompat {
* URIs. {@code null} if no content URIs are associated with the event or if permissions could
* not be granted.
*/
+ @Nullable
public static DragAndDropPermissionsCompat requestDragAndDropPermissions(Activity activity,
DragEvent dragEvent) {
return DragAndDropPermissionsCompat.request(activity, dragEvent);
diff --git a/android/support/v4/app/Fragment.java b/android/support/v4/app/Fragment.java
index 5b560cd2..f3c73ae5 100644
--- a/android/support/v4/app/Fragment.java
+++ b/android/support/v4/app/Fragment.java
@@ -575,7 +575,6 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener
/**
* Return the {@link Context} this fragment is currently associated with.
*/
- @Nullable
public Context getContext() {
return mHost == null ? null : mHost.getContext();
}
@@ -585,7 +584,6 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener
* May return {@code null} if the fragment is associated with a {@link Context}
* instead.
*/
- @Nullable
final public FragmentActivity getActivity() {
return mHost == null ? null : (FragmentActivity) mHost.getActivity();
}
@@ -594,7 +592,6 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener
* Return the host object of this fragment. May return {@code null} if the fragment
* isn't currently being hosted.
*/
- @Nullable
final public Object getHost() {
return mHost == null ? null : mHost.onGetHost();
}
@@ -655,7 +652,6 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener
* <p>If this Fragment is a child of another Fragment, the FragmentManager
* returned here will be the parent's {@link #getChildFragmentManager()}.
*/
- @Nullable
final public FragmentManager getFragmentManager() {
return mFragmentManager;
}
@@ -864,6 +860,12 @@ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener
}
mUserVisibleHint = isVisibleToUser;
mDeferStart = mState < STARTED && !isVisibleToUser;
+ if (mSavedFragmentState != null) {
+ // Ensure that if the user visible hint is set before the Fragment has
+ // restored its state that we don't lose the new value
+ mSavedFragmentState.putBoolean(FragmentManagerImpl.USER_VISIBLE_HINT_TAG,
+ mUserVisibleHint);
+ }
}
/**
diff --git a/android/support/v4/app/FragmentActivity.java b/android/support/v4/app/FragmentActivity.java
index 78161a87..e3f56849 100644
--- a/android/support/v4/app/FragmentActivity.java
+++ b/android/support/v4/app/FragmentActivity.java
@@ -273,6 +273,7 @@ public class FragmentActivity extends BaseFragmentActivityApi16 implements
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
+ mFragments.noteStateNotSaved();
mFragments.dispatchConfigurationChanged(newConfig);
}
diff --git a/android/support/v4/app/LoaderManager.java b/android/support/v4/app/LoaderManager.java
index 521b2184..32e211a5 100644
--- a/android/support/v4/app/LoaderManager.java
+++ b/android/support/v4/app/LoaderManager.java
@@ -16,8 +16,11 @@
package android.support.v4.app;
-import android.app.Activity;
import android.os.Bundle;
+import android.os.Looper;
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.v4.content.Loader;
import android.support.v4.util.DebugUtils;
import android.support.v4.util.SparseArrayCompat;
@@ -44,11 +47,15 @@ public abstract class LoaderManager {
/**
* Instantiate and return a new Loader for the given ID.
*
+ * <p>This will always be called from the process's main thread.
+ *
* @param id The ID whose loader is to be created.
* @param args Any arguments supplied by the caller.
* @return Return a new Loader instance that is ready to start loading.
*/
- public Loader<D> onCreateLoader(int id, Bundle args);
+ @MainThread
+ @NonNull
+ Loader<D> onCreateLoader(int id, @Nullable Bundle args);
/**
* Called when a previously created loader has finished its load. Note
@@ -86,19 +93,25 @@ public abstract class LoaderManager {
* method so that the old Cursor is not closed.
* </ul>
*
+ * <p>This will always be called from the process's main thread.
+ *
* @param loader The Loader that has finished.
* @param data The data generated by the Loader.
*/
- public void onLoadFinished(Loader<D> loader, D data);
+ @MainThread
+ void onLoadFinished(@NonNull Loader<D> loader, D data);
/**
* Called when a previously created loader is being reset, and thus
* making its data unavailable. The application should at this point
* remove any references it has to the Loader's data.
*
+ * <p>This will always be called from the process's main thread.
+ *
* @param loader The Loader that is being reset.
*/
- public void onLoaderReset(Loader<D> loader);
+ @MainThread
+ void onLoaderReset(@NonNull Loader<D> loader);
}
/**
@@ -115,6 +128,8 @@ public abstract class LoaderManager {
* be called immediately (inside of this function), so you must be prepared
* for this to happen.
*
+ * <p>Must be called from the process's main thread.
+ *
* @param id A unique identifier for this loader. Can be whatever you want.
* Identifiers are scoped to a particular LoaderManager instance.
* @param args Optional arguments to supply to the loader at construction.
@@ -123,8 +138,10 @@ public abstract class LoaderManager {
* @param callback Interface the LoaderManager will call to report about
* changes in the state of the loader. Required.
*/
- public abstract <D> Loader<D> initLoader(int id, Bundle args,
- LoaderManager.LoaderCallbacks<D> callback);
+ @MainThread
+ @NonNull
+ public abstract <D> Loader<D> initLoader(int id, @Nullable Bundle args,
+ @NonNull LoaderManager.LoaderCallbacks<D> callback);
/**
* Starts a new or restarts an existing {@link android.content.Loader} in
@@ -135,27 +152,35 @@ public abstract class LoaderManager {
* its work. The callback will be delivered before the old loader
* is destroyed.
*
+ * <p>Must be called from the process's main thread.
+ *
* @param id A unique identifier for this loader. Can be whatever you want.
* Identifiers are scoped to a particular LoaderManager instance.
* @param args Optional arguments to supply to the loader at construction.
* @param callback Interface the LoaderManager will call to report about
* changes in the state of the loader. Required.
*/
- public abstract <D> Loader<D> restartLoader(int id, Bundle args,
- LoaderManager.LoaderCallbacks<D> callback);
+ @MainThread
+ @NonNull
+ public abstract <D> Loader<D> restartLoader(int id, @Nullable Bundle args,
+ @NonNull LoaderManager.LoaderCallbacks<D> callback);
/**
* Stops and removes the loader with the given ID. If this loader
* had previously reported data to the client through
* {@link LoaderCallbacks#onLoadFinished(Loader, Object)}, a call
* will be made to {@link LoaderCallbacks#onLoaderReset(Loader)}.
+ *
+ * <p>Must be called from the process's main thread.
*/
+ @MainThread
public abstract void destroyLoader(int id);
/**
* Return the Loader with the given id or null if no matching Loader
* is found.
*/
+ @Nullable
public abstract <D> Loader<D> getLoader(int id);
/**
@@ -378,7 +403,7 @@ class LoaderManagerImpl extends LoaderManager {
}
@Override
- public void onLoadCanceled(Loader<Object> loader) {
+ public void onLoadCanceled(@NonNull Loader<Object> loader) {
if (DEBUG) Log.v(TAG, "onLoadCanceled: " + this);
if (mDestroyed) {
@@ -407,7 +432,7 @@ class LoaderManagerImpl extends LoaderManager {
}
@Override
- public void onLoadComplete(Loader<Object> loader, Object data) {
+ public void onLoadComplete(@NonNull Loader<Object> loader, Object data) {
if (DEBUG) Log.v(TAG, "onLoadComplete: " + this);
if (mDestroyed) {
@@ -563,36 +588,18 @@ class LoaderManagerImpl extends LoaderManager {
}
}
- /**
- * Call to initialize a particular ID with a Loader. If this ID already
- * has a Loader associated with it, it is left unchanged and any previous
- * callbacks replaced with the newly provided ones. If there is not currently
- * a Loader for the ID, a new one is created and started.
- *
- * <p>This function should generally be used when a component is initializing,
- * to ensure that a Loader it relies on is created. This allows it to re-use
- * an existing Loader's data if there already is one, so that for example
- * when an {@link Activity} is re-created after a configuration change it
- * does not need to re-create its loaders.
- *
- * <p>Note that in the case where an existing Loader is re-used, the
- * <var>args</var> given here <em>will be ignored</em> because you will
- * continue using the previous Loader.
- *
- * @param id A unique (to this LoaderManager instance) identifier under
- * which to manage the new Loader.
- * @param args Optional arguments that will be propagated to
- * {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onCreateLoader(int, Bundle) LoaderCallbacks.onCreateLoader()}.
- * @param callback Interface implementing management of this Loader. Required.
- * Its onCreateLoader() method will be called while inside of the function to
- * instantiate the Loader object.
- */
+ @MainThread
+ @NonNull
@Override
@SuppressWarnings("unchecked")
- public <D> Loader<D> initLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
+ public <D> Loader<D> initLoader(int id, @Nullable Bundle args,
+ @NonNull LoaderManager.LoaderCallbacks<D> callback) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ throw new IllegalStateException("initLoader must be called on the main thread");
+ }
LoaderInfo info = mLoaders.get(id);
@@ -615,35 +622,18 @@ class LoaderManagerImpl extends LoaderManager {
return (Loader<D>)info.mLoader;
}
- /**
- * Call to re-create the Loader associated with a particular ID. If there
- * is currently a Loader associated with this ID, it will be
- * canceled/stopped/destroyed as appropriate. A new Loader with the given
- * arguments will be created and its data delivered to you once available.
- *
- * <p>This function does some throttling of Loaders. If too many Loaders
- * have been created for the given ID but not yet generated their data,
- * new calls to this function will create and return a new Loader but not
- * actually start it until some previous loaders have completed.
- *
- * <p>After calling this function, any previous Loaders associated with
- * this ID will be considered invalid, and you will receive no further
- * data updates from them.
- *
- * @param id A unique (to this LoaderManager instance) identifier under
- * which to manage the new Loader.
- * @param args Optional arguments that will be propagated to
- * {@link android.support.v4.app.LoaderManager.LoaderCallbacks#onCreateLoader(int, Bundle) LoaderCallbacks.onCreateLoader()}.
- * @param callback Interface implementing management of this Loader. Required.
- * Its onCreateLoader() method will be called while inside of the function to
- * instantiate the Loader object.
- */
+ @MainThread
+ @NonNull
@Override
@SuppressWarnings("unchecked")
- public <D> Loader<D> restartLoader(int id, Bundle args, LoaderManager.LoaderCallbacks<D> callback) {
+ public <D> Loader<D> restartLoader(int id, @Nullable Bundle args,
+ @NonNull LoaderManager.LoaderCallbacks<D> callback) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ throw new IllegalStateException("restartLoader must be called on the main thread");
+ }
LoaderInfo info = mLoaders.get(id);
if (DEBUG) Log.v(TAG, "restartLoader in " + this + ": args=" + args);
@@ -701,18 +691,15 @@ class LoaderManagerImpl extends LoaderManager {
return (Loader<D>)info.mLoader;
}
- /**
- * Rip down, tear apart, shred to pieces a current Loader ID. After returning
- * from this function, any Loader objects associated with this ID are
- * destroyed. Any data associated with them is destroyed. You better not
- * be using it when you do this.
- * @param id Identifier of the Loader to be destroyed.
- */
+ @MainThread
@Override
public void destroyLoader(int id) {
if (mCreatingLoader) {
throw new IllegalStateException("Called while creating a loader");
}
+ if (Looper.getMainLooper() != Looper.myLooper()) {
+ throw new IllegalStateException("destroyLoader must be called on the main thread");
+ }
if (DEBUG) Log.v(TAG, "destroyLoader in " + this + " of " + id);
int idx = mLoaders.indexOfKey(id);
@@ -732,10 +719,7 @@ class LoaderManagerImpl extends LoaderManager {
}
}
- /**
- * Return the most recent Loader object associated with the
- * given ID.
- */
+ @Nullable
@Override
@SuppressWarnings("unchecked")
public <D> Loader<D> getLoader(int id) {
diff --git a/android/support/v4/app/NotificationCompat.java b/android/support/v4/app/NotificationCompat.java
index 6f74e18c..9d71ad1f 100644
--- a/android/support/v4/app/NotificationCompat.java
+++ b/android/support/v4/app/NotificationCompat.java
@@ -453,6 +453,7 @@ public class NotificationCompat {
public @interface StreamType {}
/** @hide */
+ @RestrictTo(LIBRARY_GROUP)
@Retention(SOURCE)
@IntDef({VISIBILITY_PUBLIC, VISIBILITY_PRIVATE, VISIBILITY_SECRET})
public @interface NotificationVisibility {}
@@ -2114,8 +2115,16 @@ public class NotificationCompat {
/**
* 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.
+ *
+ * <p>This API's behavior was changed in SDK version {@link Build.VERSION_CODES#P}. If your
+ * application's target version is less than {@link Build.VERSION_CODES#P}, setting a
+ * conversation title to a non-null value will make {@link #isGroupConversation()} return
+ * {@code true} and passing {@code null} will make it return {@code false}. In
+ * {@link Build.VERSION_CODES#P} and beyond, use {@link #setGroupConversation(boolean)}
+ * to set group conversation status.
+ *
+ * @param conversationTitle Title displayed for this conversation
+ * @return this object for method chaining
*/
public MessagingStyle setConversationTitle(@Nullable CharSequence conversationTitle) {
mConversationTitle = conversationTitle;
@@ -2185,9 +2194,27 @@ public class NotificationCompat {
}
/**
- * Returns {@code true} if this notification represents a group conversation.
+ * Returns {@code true} if this notification represents a group conversation, otherwise
+ * {@code false}.
+ *
+ * <p> If the application that generated this {@link MessagingStyle} targets an SDK version
+ * less than {@link Build.VERSION_CODES#P}, this method becomes dependent on whether or
+ * not the conversation title is set; returning {@code true} if the conversation title is
+ * a non-null value, or {@code false} otherwise. From {@link Build.VERSION_CODES#P} forward,
+ * this method returns what's set by {@link #setGroupConversation(boolean)} allowing for
+ * named, non-group conversations.
+ *
+ * @see #setConversationTitle(CharSequence)
*/
public boolean isGroupConversation() {
+ // When target SDK version is < P, a non-null conversation title dictates if this is
+ // as group conversation.
+ if (mBuilder != null
+ && mBuilder.mContext.getApplicationInfo().targetSdkVersion
+ < Build.VERSION_CODES.P) {
+ return mConversationTitle != null;
+ }
+
return mIsGroupConversation;
}
@@ -2769,6 +2796,66 @@ public class NotificationCompat {
* to attach actions.
*/
public static class Action {
+ /**
+ * {@link SemanticAction}: No semantic action defined.
+ */
+ public static final int SEMANTIC_ACTION_NONE = 0;
+
+ /**
+ * {@link SemanticAction}: Reply to a conversation, chat, group, or wherever replies
+ * may be appropriate.
+ */
+ public static final int SEMANTIC_ACTION_REPLY = 1;
+
+ /**
+ * {@link SemanticAction}: Mark content as read.
+ */
+ public static final int SEMANTIC_ACTION_MARK_AS_READ = 2;
+
+ /**
+ * {@link SemanticAction}: Mark content as unread.
+ */
+ public static final int SEMANTIC_ACTION_MARK_AS_UNREAD = 3;
+
+ /**
+ * {@link SemanticAction}: Delete the content associated with the notification. This
+ * could mean deleting an email, message, etc.
+ */
+ public static final int SEMANTIC_ACTION_DELETE = 4;
+
+ /**
+ * {@link SemanticAction}: Archive the content associated with the notification. This
+ * could mean archiving an email, message, etc.
+ */
+ public static final int SEMANTIC_ACTION_ARCHIVE = 5;
+
+ /**
+ * {@link SemanticAction}: Mute the content associated with the notification. This could
+ * mean silencing a conversation or currently playing media.
+ */
+ public static final int SEMANTIC_ACTION_MUTE = 6;
+
+ /**
+ * {@link SemanticAction}: Unmute the content associated with the notification. This could
+ * mean un-silencing a conversation or currently playing media.
+ */
+ public static final int SEMANTIC_ACTION_UNMUTE = 7;
+
+ /**
+ * {@link SemanticAction}: Mark content with a thumbs up.
+ */
+ public static final int SEMANTIC_ACTION_THUMBS_UP = 8;
+
+ /**
+ * {@link SemanticAction}: Mark content with a thumbs down.
+ */
+ public static final int SEMANTIC_ACTION_THUMBS_DOWN = 9;
+
+ static final String EXTRA_SHOWS_USER_INTERFACE =
+ "android.support.action.showsUserInterface";
+
+ static final String EXTRA_SEMANTIC_ACTION = "android.support.action.semanticAction";
+
final Bundle mExtras;
private final RemoteInput[] mRemoteInputs;
@@ -2785,6 +2872,9 @@ public class NotificationCompat {
private final RemoteInput[] mDataOnlyRemoteInputs;
private boolean mAllowGeneratedReplies;
+ private boolean mShowsUserInterface = true;
+
+ private final @SemanticAction int mSemanticAction;
/**
* Small icon representing the action.
@@ -2801,12 +2891,13 @@ public class NotificationCompat {
public PendingIntent actionIntent;
public Action(int icon, CharSequence title, PendingIntent intent) {
- this(icon, title, intent, new Bundle(), null, null, true);
+ this(icon, title, intent, new Bundle(), null, null, true, SEMANTIC_ACTION_NONE, true);
}
Action(int icon, CharSequence title, PendingIntent intent, Bundle extras,
RemoteInput[] remoteInputs, RemoteInput[] dataOnlyRemoteInputs,
- boolean allowGeneratedReplies) {
+ boolean allowGeneratedReplies, @SemanticAction int semanticAction,
+ boolean showsUserInterface) {
this.icon = icon;
this.title = NotificationCompat.Builder.limitCharSequenceLength(title);
this.actionIntent = intent;
@@ -2814,6 +2905,8 @@ public class NotificationCompat {
this.mRemoteInputs = remoteInputs;
this.mDataOnlyRemoteInputs = dataOnlyRemoteInputs;
this.mAllowGeneratedReplies = allowGeneratedReplies;
+ this.mSemanticAction = semanticAction;
+ this.mShowsUserInterface = showsUserInterface;
}
public int getIcon() {
@@ -2853,6 +2946,17 @@ public class NotificationCompat {
}
/**
+ * Returns the {@link SemanticAction} associated with this {@link Action}. A
+ * {@link SemanticAction} denotes what an {@link Action}'s {@link PendingIntent} will do
+ * (eg. reply, mark as read, delete, etc).
+ *
+ * @see SemanticAction
+ */
+ public @SemanticAction int getSemanticAction() {
+ return mSemanticAction;
+ }
+
+ /**
* Get the list of inputs to be collected from the user that ONLY accept data when this
* action is sent. These remote inputs are guaranteed to return true on a call to
* {@link RemoteInput#isDataOnly}.
@@ -2867,6 +2971,14 @@ public class NotificationCompat {
}
/**
+ * Return whether or not triggering this {@link Action}'s {@link PendingIntent} will open a
+ * user interface.
+ */
+ public boolean getShowsUserInterface() {
+ return mShowsUserInterface;
+ }
+
+ /**
* Builder class for {@link Action} objects.
*/
public static final class Builder {
@@ -2876,6 +2988,8 @@ public class NotificationCompat {
private boolean mAllowGeneratedReplies = true;
private final Bundle mExtras;
private ArrayList<RemoteInput> mRemoteInputs;
+ private @SemanticAction int mSemanticAction;
+ private boolean mShowsUserInterface = true;
/**
* Construct a new builder for {@link Action} object.
@@ -2884,7 +2998,7 @@ public class NotificationCompat {
* @param intent the {@link PendingIntent} to fire when users trigger this action
*/
public Builder(int icon, CharSequence title, PendingIntent intent) {
- this(icon, title, intent, new Bundle(), null, true);
+ this(icon, title, intent, new Bundle(), null, true, SEMANTIC_ACTION_NONE, true);
}
/**
@@ -2894,11 +3008,13 @@ public class NotificationCompat {
*/
public Builder(Action action) {
this(action.icon, action.title, action.actionIntent, new Bundle(action.mExtras),
- action.getRemoteInputs(), action.getAllowGeneratedReplies());
+ action.getRemoteInputs(), action.getAllowGeneratedReplies(),
+ action.getSemanticAction(), action.mShowsUserInterface);
}
private Builder(int icon, CharSequence title, PendingIntent intent, Bundle extras,
- RemoteInput[] remoteInputs, boolean allowGeneratedReplies) {
+ RemoteInput[] remoteInputs, boolean allowGeneratedReplies,
+ @SemanticAction int semanticAction, boolean showsUserInterface) {
mIcon = icon;
mTitle = NotificationCompat.Builder.limitCharSequenceLength(title);
mIntent = intent;
@@ -2906,6 +3022,8 @@ public class NotificationCompat {
mRemoteInputs = remoteInputs == null ? null : new ArrayList<>(
Arrays.asList(remoteInputs));
mAllowGeneratedReplies = allowGeneratedReplies;
+ mSemanticAction = semanticAction;
+ mShowsUserInterface = showsUserInterface;
}
/**
@@ -2961,6 +3079,32 @@ public class NotificationCompat {
}
/**
+ * Sets the {@link SemanticAction} for this {@link Action}. A {@link SemanticAction}
+ * denotes what an {@link Action}'s {@link PendingIntent} will do (eg. reply, mark
+ * as read, delete, etc).
+ * @param semanticAction a {@link SemanticAction} defined within {@link Action} with
+ * {@code SEMANTIC_ACTION_} prefixes
+ * @return this object for method chaining
+ */
+ public Builder setSemanticAction(@SemanticAction int semanticAction) {
+ mSemanticAction = semanticAction;
+ return this;
+ }
+
+ /**
+ * Set whether or not this {@link Action}'s {@link PendingIntent} will open a user
+ * interface.
+ * @param showsUserInterface {@code true} if this {@link Action}'s {@link PendingIntent}
+ * will open a user interface, otherwise {@code false}
+ * @return this object for method chaining
+ * The default value is {@code true}
+ */
+ public Builder setShowsUserInterface(boolean showsUserInterface) {
+ mShowsUserInterface = showsUserInterface;
+ return this;
+ }
+
+ /**
* Apply an extender to this action builder. Extenders may be used to add
* metadata or change options on this builder.
*/
@@ -2991,7 +3135,8 @@ public class NotificationCompat {
RemoteInput[] textInputsArr = textInputs.isEmpty()
? null : textInputs.toArray(new RemoteInput[textInputs.size()]);
return new Action(mIcon, mTitle, mIntent, mExtras, textInputsArr,
- dataOnlyInputsArr, mAllowGeneratedReplies);
+ dataOnlyInputsArr, mAllowGeneratedReplies, mSemanticAction,
+ mShowsUserInterface);
}
}
@@ -3251,6 +3396,27 @@ public class NotificationCompat {
return (mFlags & FLAG_HINT_DISPLAY_INLINE) != 0;
}
}
+
+ /**
+ * Provides meaning to an {@link Action} that hints at what the associated
+ * {@link PendingIntent} will do. For example, an {@link Action} with a
+ * {@link PendingIntent} that replies to a text message notification may have the
+ * {@link #SEMANTIC_ACTION_REPLY} {@link SemanticAction} set within it.
+ */
+ @IntDef({
+ SEMANTIC_ACTION_NONE,
+ SEMANTIC_ACTION_REPLY,
+ SEMANTIC_ACTION_MARK_AS_READ,
+ SEMANTIC_ACTION_MARK_AS_UNREAD,
+ SEMANTIC_ACTION_DELETE,
+ SEMANTIC_ACTION_ARCHIVE,
+ SEMANTIC_ACTION_MUTE,
+ SEMANTIC_ACTION_UNMUTE,
+ SEMANTIC_ACTION_THUMBS_UP,
+ SEMANTIC_ACTION_THUMBS_DOWN
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SemanticAction {}
}
@@ -4651,8 +4817,21 @@ public class NotificationCompat {
allowGeneratedReplies = action.getExtras().getBoolean(
NotificationCompatJellybean.EXTRA_ALLOW_GENERATED_REPLIES);
}
+
+ final boolean showsUserInterface =
+ action.getExtras().getBoolean(Action.EXTRA_SHOWS_USER_INTERFACE, true);
+
+ final @Action.SemanticAction int semanticAction;
+ if (Build.VERSION.SDK_INT >= 28) {
+ semanticAction = action.getSemanticAction();
+ } else {
+ semanticAction = action.getExtras().getInt(
+ Action.EXTRA_SEMANTIC_ACTION, Action.SEMANTIC_ACTION_NONE);
+ }
+
return new Action(action.icon, action.title, action.actionIntent,
- action.getExtras(), remoteInputs, null, allowGeneratedReplies);
+ action.getExtras(), remoteInputs, null, allowGeneratedReplies,
+ semanticAction, showsUserInterface);
}
/**
diff --git a/android/support/v4/app/NotificationCompatBuilder.java b/android/support/v4/app/NotificationCompatBuilder.java
index db775a55..e5fb4f95 100644
--- a/android/support/v4/app/NotificationCompatBuilder.java
+++ b/android/support/v4/app/NotificationCompatBuilder.java
@@ -248,6 +248,15 @@ class NotificationCompatBuilder implements NotificationBuilderWithBuilderAccesso
if (Build.VERSION.SDK_INT >= 24) {
actionBuilder.setAllowGeneratedReplies(action.getAllowGeneratedReplies());
}
+
+ actionExtras.putInt(NotificationCompat.Action.EXTRA_SEMANTIC_ACTION,
+ action.getSemanticAction());
+ if (Build.VERSION.SDK_INT >= 28) {
+ actionBuilder.setSemanticAction(action.getSemanticAction());
+ }
+
+ actionExtras.putBoolean(NotificationCompat.Action.EXTRA_SHOWS_USER_INTERFACE,
+ action.getShowsUserInterface());
actionBuilder.addExtras(actionExtras);
mBuilder.addAction(actionBuilder.build());
} else if (Build.VERSION.SDK_INT >= 16) {
diff --git a/android/support/v4/app/NotificationCompatJellybean.java b/android/support/v4/app/NotificationCompatJellybean.java
index 9cdd2e95..82f89412 100644
--- a/android/support/v4/app/NotificationCompatJellybean.java
+++ b/android/support/v4/app/NotificationCompatJellybean.java
@@ -129,7 +129,8 @@ class NotificationCompatJellybean {
allowGeneratedReplies = extras.getBoolean(EXTRA_ALLOW_GENERATED_REPLIES);
}
return new NotificationCompat.Action(icon, title, actionIntent, extras, remoteInputs,
- dataOnlyRemoteInputs, allowGeneratedReplies);
+ dataOnlyRemoteInputs, allowGeneratedReplies,
+ NotificationCompat.Action.SEMANTIC_ACTION_NONE, true);
}
public static Bundle writeActionAndGetExtras(
@@ -236,7 +237,9 @@ class NotificationCompatJellybean {
bundle.getBundle(KEY_EXTRAS),
fromBundleArray(getBundleArrayFromBundle(bundle, KEY_REMOTE_INPUTS)),
fromBundleArray(getBundleArrayFromBundle(bundle, KEY_DATA_ONLY_REMOTE_INPUTS)),
- allowGeneratedReplies);
+ allowGeneratedReplies,
+ NotificationCompat.Action.SEMANTIC_ACTION_NONE,
+ true);
}
static Bundle getBundleForAction(NotificationCompat.Action action) {
diff --git a/android/support/v4/app/NotificationManagerCompat.java b/android/support/v4/app/NotificationManagerCompat.java
index 07fcb6c7..7099cb9b 100644
--- a/android/support/v4/app/NotificationManagerCompat.java
+++ b/android/support/v4/app/NotificationManagerCompat.java
@@ -190,7 +190,7 @@ public final class NotificationManagerCompat {
* @param id the ID of the notification
* @param notification the notification to post to the system
*/
- public void notify(int id, Notification notification) {
+ public void notify(int id, @NonNull Notification notification) {
notify(null, id, notification);
}
diff --git a/android/support/v4/content/FileProvider.java b/android/support/v4/content/FileProvider.java
index 8599911a..16164be9 100644
--- a/android/support/v4/content/FileProvider.java
+++ b/android/support/v4/content/FileProvider.java
@@ -29,6 +29,7 @@ import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
+import android.os.Build;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
@@ -177,6 +178,17 @@ import java.util.Map;
* subdirectory is the same as the value returned by
* {@link Context#getExternalCacheDir() Context.getExternalCacheDir()}.
* </dd>
+ * <dt>
+ * <pre class="prettyprint">
+ *&lt;external-media-path name="<i>name</i>" path="<i>path</i>" /&gt;
+ *</pre>
+ * </dt>
+ * <dd>
+ * Represents files in the root of your app's external media area. The root path of this
+ * subdirectory is the same as the value returned by the first result of
+ * {@link Context#getExternalMediaDirs() Context.getExternalMediaDirs()}.
+ * <p><strong>Note:</strong> this directory is only available on API 21+ devices.</p>
+ * </dd>
* </dl>
* <p>
* These child elements all use the same attributes:
@@ -336,6 +348,7 @@ public class FileProvider extends ContentProvider {
private static final String TAG_EXTERNAL = "external-path";
private static final String TAG_EXTERNAL_FILES = "external-files-path";
private static final String TAG_EXTERNAL_CACHE = "external-cache-path";
+ private static final String TAG_EXTERNAL_MEDIA = "external-media-path";
private static final String ATTR_NAME = "name";
private static final String ATTR_PATH = "path";
@@ -622,6 +635,12 @@ public class FileProvider extends ContentProvider {
if (externalCacheDirs.length > 0) {
target = externalCacheDirs[0];
}
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP
+ && TAG_EXTERNAL_MEDIA.equals(tag)) {
+ File[] externalMediaDirs = context.getExternalMediaDirs();
+ if (externalMediaDirs.length > 0) {
+ target = externalMediaDirs[0];
+ }
}
if (target != null) {
diff --git a/android/support/v4/content/Loader.java b/android/support/v4/content/Loader.java
index 2ac10d73..431964d2 100644
--- a/android/support/v4/content/Loader.java
+++ b/android/support/v4/content/Loader.java
@@ -19,6 +19,7 @@ package android.support.v4.content;
import android.content.Context;
import android.database.ContentObserver;
import android.os.Handler;
+import android.support.annotation.MainThread;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.DebugUtils;
@@ -123,6 +124,7 @@ public class Loader<D> {
*
* @param data the result of the load
*/
+ @MainThread
public void deliverResult(@Nullable D data) {
if (mListener != null) {
mListener.onLoadComplete(this, data);
@@ -135,6 +137,7 @@ public class Loader<D> {
*
* Must be called from the process's main thread.
*/
+ @MainThread
public void deliverCancellation() {
if (mOnLoadCanceledListener != null) {
mOnLoadCanceledListener.onLoadCanceled(this);
@@ -163,6 +166,7 @@ public class Loader<D> {
*
* <p>Must be called from the process's main thread.
*/
+ @MainThread
public void registerListener(int id, @NonNull OnLoadCompleteListener<D> listener) {
if (mListener != null) {
throw new IllegalStateException("There is already a listener registered");
@@ -176,6 +180,7 @@ public class Loader<D> {
*
* Must be called from the process's main thread.
*/
+ @MainThread
public void unregisterListener(@NonNull OnLoadCompleteListener<D> listener) {
if (mListener == null) {
throw new IllegalStateException("No listener register");
@@ -195,6 +200,7 @@ public class Loader<D> {
*
* @param listener The listener to register.
*/
+ @MainThread
public void registerOnLoadCanceledListener(@NonNull OnLoadCanceledListener<D> listener) {
if (mOnLoadCanceledListener != null) {
throw new IllegalStateException("There is already a listener registered");
@@ -210,6 +216,7 @@ public class Loader<D> {
*
* @param listener The listener to unregister.
*/
+ @MainThread
public void unregisterOnLoadCanceledListener(@NonNull OnLoadCanceledListener<D> listener) {
if (mOnLoadCanceledListener == null) {
throw new IllegalStateException("No listener register");
@@ -268,6 +275,7 @@ public class Loader<D> {
*
* <p>Must be called from the process's main thread.
*/
+ @MainThread
public final void startLoading() {
mStarted = true;
mReset = false;
@@ -279,7 +287,9 @@ public class Loader<D> {
* Subclasses must implement this to take care of loading their data,
* as per {@link #startLoading()}. This is not called by clients directly,
* but as a result of a call to {@link #startLoading()}.
+ * This will always be called from the process's main thread.
*/
+ @MainThread
protected void onStartLoading() {
}
@@ -301,6 +311,7 @@ public class Loader<D> {
* is still running and the {@link OnLoadCanceledListener} will be called
* when the task completes.
*/
+ @MainThread
public boolean cancelLoad() {
return onCancelLoad();
}
@@ -316,6 +327,7 @@ public class Loader<D> {
* is still running and the {@link OnLoadCanceledListener} will be called
* when the task completes.
*/
+ @MainThread
protected boolean onCancelLoad() {
return false;
}
@@ -328,6 +340,7 @@ public class Loader<D> {
*
* <p>Must be called from the process's main thread.
*/
+ @MainThread
public void forceLoad() {
onForceLoad();
}
@@ -336,6 +349,7 @@ public class Loader<D> {
* Subclasses must implement this to take care of requests to {@link #forceLoad()}.
* This will always be called from the process's main thread.
*/
+ @MainThread
protected void onForceLoad() {
}
@@ -359,6 +373,7 @@ public class Loader<D> {
*
* <p>Must be called from the process's main thread.
*/
+ @MainThread
public void stopLoading() {
mStarted = false;
onStopLoading();
@@ -370,6 +385,7 @@ public class Loader<D> {
* but as a result of a call to {@link #stopLoading()}.
* This will always be called from the process's main thread.
*/
+ @MainThread
protected void onStopLoading() {
}
@@ -383,12 +399,15 @@ public class Loader<D> {
* Tell the Loader that it is being abandoned. This is called prior
* to {@link #reset} to have it retain its current data but not report
* any new data.
+ *
+ * <p>Must be called from the process's main thread.
*/
+ @MainThread
public void abandon() {
mAbandoned = true;
onAbandon();
}
-
+
/**
* Subclasses implement this to take care of being abandoned. This is
* an optional intermediate state prior to {@link #onReset()} -- it means that
@@ -397,10 +416,12 @@ public class Loader<D> {
* loader <em>must</em> keep its last reported data valid until the final
* {@link #onReset()} happens. You can retrieve the current abandoned
* state with {@link #isAbandoned}.
+ * This will always be called from the process's main thread.
*/
+ @MainThread
protected void onAbandon() {
}
-
+
/**
* This function will normally be called for you automatically by
* {@link android.support.v4.app.LoaderManager} when destroying a Loader. When using
@@ -419,6 +440,7 @@ public class Loader<D> {
*
* <p>Must be called from the process's main thread.
*/
+ @MainThread
public void reset() {
onReset();
mReset = true;
@@ -434,6 +456,7 @@ public class Loader<D> {
* but as a result of a call to {@link #reset()}.
* This will always be called from the process's main thread.
*/
+ @MainThread
protected void onReset() {
}
@@ -481,6 +504,7 @@ public class Loader<D> {
*
* <p>Must be called from the process's main thread.
*/
+ @MainThread
public void onContentChanged() {
if (mStarted) {
forceLoad();
diff --git a/android/support/v4/content/WakefulBroadcastReceiver.java b/android/support/v4/content/WakefulBroadcastReceiver.java
index 8ec3eee6..78555aa4 100644
--- a/android/support/v4/content/WakefulBroadcastReceiver.java
+++ b/android/support/v4/content/WakefulBroadcastReceiver.java
@@ -34,6 +34,9 @@ import android.util.SparseArray;
* for you; you must request the {@link android.Manifest.permission#WAKE_LOCK}
* permission to use it.</p>
*
+ * <p>Wakelocks held by this class are reported to tools as
+ * {@code "androidx.core:wake:<component-name>"}.</p>
+ *
* <h3>Example</h3>
*
* <p>A {@link WakefulBroadcastReceiver} uses the method
@@ -103,7 +106,7 @@ public abstract class WakefulBroadcastReceiver extends BroadcastReceiver {
PowerManager pm = (PowerManager)context.getSystemService(Context.POWER_SERVICE);
PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
- "wake:" + comp.flattenToShortString());
+ "androidx.core:wake:" + comp.flattenToShortString());
wl.setReferenceCounted(false);
wl.acquire(60 * 1000);
sActiveWakeLocks.put(id, wl);
diff --git a/android/support/v4/graphics/TypefaceCompatUtil.java b/android/support/v4/graphics/TypefaceCompatUtil.java
index b5d206c8..c524f820 100644
--- a/android/support/v4/graphics/TypefaceCompatUtil.java
+++ b/android/support/v4/graphics/TypefaceCompatUtil.java
@@ -94,11 +94,15 @@ public class TypefaceCompatUtil {
@RequiresApi(19)
public static ByteBuffer mmap(Context context, CancellationSignal cancellationSignal, Uri uri) {
final ContentResolver resolver = context.getContentResolver();
- try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r", cancellationSignal);
- FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) {
- FileChannel channel = fis.getChannel();
- final long size = channel.size();
- return channel.map(FileChannel.MapMode.READ_ONLY, 0, size);
+ try (ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r", cancellationSignal)) {
+ if (pfd == null) {
+ return null;
+ }
+ try (FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) {
+ FileChannel channel = fis.getChannel();
+ final long size = channel.size();
+ return channel.map(FileChannel.MapMode.READ_ONLY, 0, size);
+ }
} catch (IOException e) {
return null;
}
diff --git a/android/support/v4/graphics/drawable/DrawableCompat.java b/android/support/v4/graphics/drawable/DrawableCompat.java
index 4e988eaf..f15354ef 100644
--- a/android/support/v4/graphics/drawable/DrawableCompat.java
+++ b/android/support/v4/graphics/drawable/DrawableCompat.java
@@ -229,8 +229,8 @@ public final class DrawableCompat {
// children manually
if (drawable instanceof InsetDrawable) {
clearColorFilter(((InsetDrawable) drawable).getDrawable());
- } else if (drawable instanceof DrawableWrapper) {
- clearColorFilter(((DrawableWrapper) drawable).getWrappedDrawable());
+ } else if (drawable instanceof WrappedDrawable) {
+ clearColorFilter(((WrappedDrawable) drawable).getWrappedDrawable());
} else if (drawable instanceof DrawableContainer) {
final DrawableContainer container = (DrawableContainer) drawable;
final DrawableContainer.DrawableContainerState state =
@@ -307,17 +307,17 @@ public final class DrawableCompat {
return drawable;
} else if (Build.VERSION.SDK_INT >= 21) {
if (!(drawable instanceof TintAwareDrawable)) {
- return new DrawableWrapperApi21(drawable);
+ return new WrappedDrawableApi21(drawable);
}
return drawable;
} else if (Build.VERSION.SDK_INT >= 19) {
if (!(drawable instanceof TintAwareDrawable)) {
- return new DrawableWrapperApi19(drawable);
+ return new WrappedDrawableApi19(drawable);
}
return drawable;
} else {
if (!(drawable instanceof TintAwareDrawable)) {
- return new DrawableWrapperApi14(drawable);
+ return new WrappedDrawableApi14(drawable);
}
return drawable;
}
@@ -335,8 +335,8 @@ public final class DrawableCompat {
*/
@SuppressWarnings("TypeParameterUnusedInFormals")
public static <T extends Drawable> T unwrap(@NonNull Drawable drawable) {
- if (drawable instanceof DrawableWrapper) {
- return (T) ((DrawableWrapper) drawable).getWrappedDrawable();
+ if (drawable instanceof WrappedDrawable) {
+ return (T) ((WrappedDrawable) drawable).getWrappedDrawable();
}
return (T) drawable;
}
diff --git a/android/support/v4/graphics/drawable/DrawableWrapper.java b/android/support/v4/graphics/drawable/WrappedDrawable.java
index 1574b363..3bd1d683 100644
--- a/android/support/v4/graphics/drawable/DrawableWrapper.java
+++ b/android/support/v4/graphics/drawable/WrappedDrawable.java
@@ -28,7 +28,7 @@ import android.support.annotation.RestrictTo;
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
-public interface DrawableWrapper {
+public interface WrappedDrawable {
Drawable getWrappedDrawable();
void setWrappedDrawable(Drawable drawable);
}
diff --git a/android/support/v4/graphics/drawable/DrawableWrapperApi14.java b/android/support/v4/graphics/drawable/WrappedDrawableApi14.java
index 5b1bbc70..d1218bc7 100644
--- a/android/support/v4/graphics/drawable/DrawableWrapperApi14.java
+++ b/android/support/v4/graphics/drawable/WrappedDrawableApi14.java
@@ -26,7 +26,6 @@ import android.graphics.Region;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.annotation.RequiresApi;
/**
* Drawable which delegates all calls to its wrapped {@link Drawable}.
@@ -34,10 +33,8 @@ import android.support.annotation.RequiresApi;
* Also allows backward compatible tinting via a color or {@link ColorStateList}.
* This functionality is accessed via static methods in {@code DrawableCompat}.
*/
-
-@RequiresApi(14)
-class DrawableWrapperApi14 extends Drawable
- implements Drawable.Callback, DrawableWrapper, TintAwareDrawable {
+class WrappedDrawableApi14 extends Drawable
+ implements Drawable.Callback, WrappedDrawable, TintAwareDrawable {
static final PorterDuff.Mode DEFAULT_TINT_MODE = PorterDuff.Mode.SRC_IN;
@@ -50,7 +47,7 @@ class DrawableWrapperApi14 extends Drawable
Drawable mDrawable;
- DrawableWrapperApi14(@NonNull DrawableWrapperState state, @Nullable Resources res) {
+ WrappedDrawableApi14(@NonNull DrawableWrapperState state, @Nullable Resources res) {
mState = state;
updateLocalState(res);
}
@@ -60,7 +57,7 @@ class DrawableWrapperApi14 extends Drawable
*
* @param dr the drawable to wrap
*/
- DrawableWrapperApi14(@Nullable Drawable dr) {
+ WrappedDrawableApi14(@Nullable Drawable dr) {
mState = mutateConstantState();
// Now set the drawable...
setWrappedDrawable(dr);
@@ -73,26 +70,17 @@ class DrawableWrapperApi14 extends Drawable
*/
private void updateLocalState(@Nullable Resources res) {
if (mState != null && mState.mDrawableState != null) {
- final Drawable dr = newDrawableFromState(mState.mDrawableState, res);
- setWrappedDrawable(dr);
+ setWrappedDrawable(mState.mDrawableState.newDrawable(res));
}
}
- /**
- * Allows us to call ConstantState.newDrawable(*) is a API safe way
- */
- protected Drawable newDrawableFromState(@NonNull Drawable.ConstantState state,
- @Nullable Resources res) {
- return state.newDrawable(res);
- }
-
@Override
public void jumpToCurrentState() {
mDrawable.jumpToCurrentState();
}
@Override
- public void draw(Canvas canvas) {
+ public void draw(@NonNull Canvas canvas) {
mDrawable.draw(canvas);
}
@@ -144,17 +132,19 @@ class DrawableWrapperApi14 extends Drawable
}
@Override
- public boolean setState(final int[] stateSet) {
+ public boolean setState(@NonNull int[] stateSet) {
boolean handled = mDrawable.setState(stateSet);
handled = updateTint(stateSet) || handled;
return handled;
}
+ @NonNull
@Override
public int[] getState() {
return mDrawable.getState();
}
+ @NonNull
@Override
public Drawable getCurrent() {
return mDrawable.getCurrent();
@@ -196,7 +186,7 @@ class DrawableWrapperApi14 extends Drawable
}
@Override
- public boolean getPadding(Rect padding) {
+ public boolean getPadding(@NonNull Rect padding) {
return mDrawable.getPadding(padding);
}
@@ -210,6 +200,7 @@ class DrawableWrapperApi14 extends Drawable
return null;
}
+ @NonNull
@Override
public Drawable mutate() {
if (!mMutated && super.mutate() == this) {
@@ -242,7 +233,7 @@ class DrawableWrapperApi14 extends Drawable
* {@inheritDoc}
*/
@Override
- public void invalidateDrawable(Drawable who) {
+ public void invalidateDrawable(@NonNull Drawable who) {
invalidateSelf();
}
@@ -250,7 +241,7 @@ class DrawableWrapperApi14 extends Drawable
* {@inheritDoc}
*/
@Override
- public void scheduleDrawable(Drawable who, Runnable what, long when) {
+ public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
scheduleSelf(what, when);
}
@@ -258,7 +249,7 @@ class DrawableWrapperApi14 extends Drawable
* {@inheritDoc}
*/
@Override
- public void unscheduleDrawable(Drawable who, Runnable what) {
+ public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
unscheduleSelf(what);
}
@@ -279,7 +270,7 @@ class DrawableWrapperApi14 extends Drawable
}
@Override
- public void setTintMode(PorterDuff.Mode tintMode) {
+ public void setTintMode(@NonNull PorterDuff.Mode tintMode) {
mState.mTintMode = tintMode;
updateTint(getState());
}
@@ -364,11 +355,13 @@ class DrawableWrapperApi14 extends Drawable
}
}
+ @NonNull
@Override
public Drawable newDrawable() {
return newDrawable(null);
}
+ @NonNull
@Override
public abstract Drawable newDrawable(@Nullable Resources res);
@@ -389,9 +382,10 @@ class DrawableWrapperApi14 extends Drawable
super(orig, res);
}
+ @NonNull
@Override
public Drawable newDrawable(@Nullable Resources res) {
- return new DrawableWrapperApi14(this, res);
+ return new WrappedDrawableApi14(this, res);
}
}
}
diff --git a/android/support/v4/graphics/drawable/DrawableWrapperApi19.java b/android/support/v4/graphics/drawable/WrappedDrawableApi19.java
index 7707591d..7d6b8d87 100644
--- a/android/support/v4/graphics/drawable/DrawableWrapperApi19.java
+++ b/android/support/v4/graphics/drawable/WrappedDrawableApi19.java
@@ -23,13 +23,13 @@ import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
@RequiresApi(19)
-class DrawableWrapperApi19 extends DrawableWrapperApi14 {
+class WrappedDrawableApi19 extends WrappedDrawableApi14 {
- DrawableWrapperApi19(Drawable drawable) {
+ WrappedDrawableApi19(Drawable drawable) {
super(drawable);
}
- DrawableWrapperApi19(DrawableWrapperState state, Resources resources) {
+ WrappedDrawableApi19(DrawableWrapperState state, Resources resources) {
super(state, resources);
}
@@ -55,9 +55,10 @@ class DrawableWrapperApi19 extends DrawableWrapperApi14 {
super(orig, res);
}
+ @NonNull
@Override
public Drawable newDrawable(@Nullable Resources res) {
- return new DrawableWrapperApi19(this, res);
+ return new WrappedDrawableApi19(this, res);
}
}
}
diff --git a/android/support/v4/graphics/drawable/DrawableWrapperApi21.java b/android/support/v4/graphics/drawable/WrappedDrawableApi21.java
index 5195cc93..b5507428 100644
--- a/android/support/v4/graphics/drawable/DrawableWrapperApi21.java
+++ b/android/support/v4/graphics/drawable/WrappedDrawableApi21.java
@@ -16,8 +16,6 @@
package android.support.v4.graphics.drawable;
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
import android.content.res.ColorStateList;
import android.content.res.Resources;
import android.graphics.Outline;
@@ -32,22 +30,21 @@ 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.util.Log;
import java.lang.reflect.Method;
@RequiresApi(21)
-class DrawableWrapperApi21 extends DrawableWrapperApi19 {
- private static final String TAG = "DrawableWrapperApi21";
+class WrappedDrawableApi21 extends WrappedDrawableApi19 {
+ private static final String TAG = "WrappedDrawableApi21";
private static Method sIsProjectedDrawableMethod;
- DrawableWrapperApi21(Drawable drawable) {
+ WrappedDrawableApi21(Drawable drawable) {
super(drawable);
findAndCacheIsProjectedDrawableMethod();
}
- DrawableWrapperApi21(DrawableWrapperState state, Resources resources) {
+ WrappedDrawableApi21(DrawableWrapperState state, Resources resources) {
super(state, resources);
findAndCacheIsProjectedDrawableMethod();
}
@@ -63,10 +60,11 @@ class DrawableWrapperApi21 extends DrawableWrapperApi19 {
}
@Override
- public void getOutline(Outline outline) {
+ public void getOutline(@NonNull Outline outline) {
mDrawable.getOutline(outline);
}
+ @NonNull
@Override
public Rect getDirtyBounds() {
return mDrawable.getDirtyBounds();
@@ -100,7 +98,7 @@ class DrawableWrapperApi21 extends DrawableWrapperApi19 {
}
@Override
- public boolean setState(int[] stateSet) {
+ public boolean setState(@NonNull int[] stateSet) {
if (super.setState(stateSet)) {
// Manually invalidate because the framework doesn't currently force an invalidation
// on a state change
@@ -123,9 +121,9 @@ class DrawableWrapperApi21 extends DrawableWrapperApi19 {
}
/**
- * @hide
+ * This method is overriding hidden framework method in {@link Drawable}. It is used by the
+ * system and thus it should not be removed.
*/
- @RestrictTo(LIBRARY_GROUP)
public boolean isProjected() {
if (mDrawable != null && sIsProjectedDrawableMethod != null) {
try {
@@ -150,9 +148,10 @@ class DrawableWrapperApi21 extends DrawableWrapperApi19 {
super(orig, res);
}
+ @NonNull
@Override
public Drawable newDrawable(@Nullable Resources res) {
- return new DrawableWrapperApi21(this, res);
+ return new WrappedDrawableApi21(this, res);
}
}
diff --git a/android/support/v4/util/ArraySet.java b/android/support/v4/util/ArraySet.java
index ab080fa8..8444d2c0 100644
--- a/android/support/v4/util/ArraySet.java
+++ b/android/support/v4/util/ArraySet.java
@@ -18,6 +18,8 @@ package android.support.v4.util;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.util.Log;
@@ -69,16 +71,15 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
* The first entry in the array is a pointer to the next array in the
* list; the second entry is a pointer to the int[] hash code array for it.
*/
- static Object[] sBaseCache;
- static int sBaseCacheSize;
- static Object[] sTwiceBaseCache;
- static int sTwiceBaseCacheSize;
+ private static Object[] sBaseCache;
+ private static int sBaseCacheSize;
+ private static Object[] sTwiceBaseCache;
+ private static int sTwiceBaseCacheSize;
- final boolean mIdentityHashCode;
- int[] mHashes;
- Object[] mArray;
- int mSize;
- MapCollections<E, E> mCollections;
+ private int[] mHashes;
+ private Object[] mArray;
+ private int mSize;
+ private MapCollections<E, E> mCollections;
private int indexOf(Object key, int hash) {
final int N = mSize;
@@ -238,19 +239,13 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
* will grow once items are added to it.
*/
public ArraySet() {
- this(0, false);
+ this(0);
}
/**
* Create a new ArraySet with a given initial capacity.
*/
public ArraySet(int capacity) {
- this(capacity, false);
- }
-
- /** {@hide} */
- public ArraySet(int capacity, boolean identityHashCode) {
- mIdentityHashCode = identityHashCode;
if (capacity == 0) {
mHashes = INT;
mArray = OBJECT;
@@ -263,15 +258,17 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
/**
* Create a new ArraySet with the mappings from the given ArraySet.
*/
- public ArraySet(ArraySet<E> set) {
+ public ArraySet(@Nullable ArraySet<E> set) {
this();
if (set != null) {
addAll(set);
}
}
- /** {@hide} */
- public ArraySet(Collection<E> set) {
+ /**
+ * Create a new ArraySet with the mappings from the given {@link Collection}.
+ */
+ public ArraySet(@Nullable Collection<E> set) {
this();
if (set != null) {
addAll(set);
@@ -326,8 +323,7 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
* @return Returns the index of the value if it exists, else a negative integer.
*/
public int indexOf(Object key) {
- return key == null ? indexOfNull()
- : indexOf(key, mIdentityHashCode ? System.identityHashCode(key) : key.hashCode());
+ return key == null ? indexOfNull() : indexOf(key, key.hashCode());
}
/**
@@ -335,6 +331,7 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
* @param index The desired index, must be between 0 and {@link #size()}-1.
* @return Returns the value stored at the given index.
*/
+ @Nullable
public E valueAt(int index) {
return (E) mArray[index];
}
@@ -357,14 +354,14 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
* when the class of the object is inappropriate for this set.
*/
@Override
- public boolean add(E value) {
+ public boolean add(@Nullable E value) {
final int hash;
int index;
if (value == null) {
hash = 0;
index = indexOfNull();
} else {
- hash = mIdentityHashCode ? System.identityHashCode(value) : value.hashCode();
+ hash = value.hashCode();
index = indexOf(value, hash);
}
if (index >= 0) {
@@ -413,8 +410,7 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
@RestrictTo(LIBRARY_GROUP)
public void append(E value) {
final int index = mSize;
- final int hash = value == null ? 0
- : (mIdentityHashCode ? System.identityHashCode(value) : value.hashCode());
+ final int hash = value == null ? 0 : value.hashCode();
if (index >= mHashes.length) {
throw new IllegalStateException("Array is full");
}
@@ -439,7 +435,7 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
* Perform a {@link #add(Object)} of all values in <var>array</var>
* @param array The array whose contents are to be retrieved.
*/
- public void addAll(ArraySet<? extends E> array) {
+ public void addAll(@NonNull ArraySet<? extends E> array) {
final int N = array.mSize;
ensureCapacity(mSize + N);
if (mSize == 0) {
@@ -555,6 +551,7 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
return mSize;
}
+ @NonNull
@Override
public Object[] toArray() {
Object[] result = new Object[mSize];
@@ -562,8 +559,9 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
return result;
}
+ @NonNull
@Override
- public <T> T[] toArray(T[] array) {
+ public <T> T[] toArray(@NonNull T[] array) {
if (array.length < mSize) {
@SuppressWarnings("unchecked") T[] newArray =
(T[]) Array.newInstance(array.getClass().getComponentType(), mSize);
@@ -732,7 +730,7 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
* in <var>collection</var>, else returns false.
*/
@Override
- public boolean containsAll(Collection<?> collection) {
+ public boolean containsAll(@NonNull Collection<?> collection) {
Iterator<?> it = collection.iterator();
while (it.hasNext()) {
if (!contains(it.next())) {
@@ -747,7 +745,7 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
* @param collection The collection whose contents are to be retrieved.
*/
@Override
- public boolean addAll(Collection<? extends E> collection) {
+ public boolean addAll(@NonNull Collection<? extends E> collection) {
ensureCapacity(mSize + collection.size());
boolean added = false;
for (E value : collection) {
@@ -762,7 +760,7 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
* @return Returns true if any values were removed from the array set, else false.
*/
@Override
- public boolean removeAll(Collection<?> collection) {
+ public boolean removeAll(@NonNull Collection<?> collection) {
boolean removed = false;
for (Object value : collection) {
removed |= remove(value);
@@ -777,7 +775,7 @@ public final class ArraySet<E> implements Collection<E>, Set<E> {
* @return Returns true if any values were removed from the array set, else false.
*/
@Override
- public boolean retainAll(Collection<?> collection) {
+ public boolean retainAll(@NonNull Collection<?> collection) {
boolean removed = false;
for (int i = mSize - 1; i >= 0; i--) {
if (!collection.contains(mArray[i])) {
diff --git a/android/support/v4/util/LongSparseArray.java b/android/support/v4/util/LongSparseArray.java
index 25b6bb97..febb5d52 100644
--- a/android/support/v4/util/LongSparseArray.java
+++ b/android/support/v4/util/LongSparseArray.java
@@ -235,6 +235,14 @@ public class LongSparseArray<E> implements Cloneable {
}
/**
+ * Return true if size() is 0.
+ * @return true if size() is 0.
+ */
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+
+ /**
* Given an index in the range <code>0...size()-1</code>, returns
* the key from the <code>index</code>th key-value mapping that this
* LongSparseArray stores.
diff --git a/android/support/v4/util/ObjectsCompat.java b/android/support/v4/util/ObjectsCompat.java
index b6c740e5..747cfb45 100644
--- a/android/support/v4/util/ObjectsCompat.java
+++ b/android/support/v4/util/ObjectsCompat.java
@@ -18,6 +18,7 @@ package android.support.v4.util;
import android.os.Build;
import android.support.annotation.Nullable;
+import java.util.Arrays;
import java.util.Objects;
/**
@@ -51,4 +52,46 @@ public class ObjectsCompat {
return (a == b) || (a != null && a.equals(b));
}
}
+
+ /**
+ * Returns the hash code of a non-{@code null} argument and 0 for a {@code null} argument.
+ *
+ * @param o an object
+ * @return the hash code of a non-{@code null} argument and 0 for a {@code null} argument
+ * @see Object#hashCode
+ */
+ public static int hashCode(@Nullable Object o) {
+ return o != null ? o.hashCode() : 0;
+ }
+
+ /**
+ * Generates a hash code for a sequence of input values. The hash code is generated as if all
+ * the input values were placed into an array, and that array were hashed by calling
+ * {@link Arrays#hashCode(Object[])}.
+ *
+ * <p>This method is useful for implementing {@link Object#hashCode()} on objects containing
+ * multiple fields. For example, if an object that has three fields, {@code x}, {@code y}, and
+ * {@code z}, one could write:
+ *
+ * <blockquote><pre>
+ * &#064;Override public int hashCode() {
+ * return ObjectsCompat.hash(x, y, z);
+ * }
+ * </pre></blockquote>
+ *
+ * <b>Warning: When a single object reference is supplied, the returned value does not equal the
+ * hash code of that object reference.</b> This value can be computed by calling
+ * {@link #hashCode(Object)}.
+ *
+ * @param values the values to be hashed
+ * @return a hash value of the sequence of input values
+ * @see Arrays#hashCode(Object[])
+ */
+ public static int hash(@Nullable Object... values) {
+ if (Build.VERSION.SDK_INT >= 19) {
+ return Objects.hash(values);
+ } else {
+ return Arrays.hashCode(values);
+ }
+ }
}
diff --git a/android/support/v4/util/SparseArrayCompat.java b/android/support/v4/util/SparseArrayCompat.java
index aedc4ad0..5238cf06 100644
--- a/android/support/v4/util/SparseArrayCompat.java
+++ b/android/support/v4/util/SparseArrayCompat.java
@@ -228,6 +228,14 @@ public class SparseArrayCompat<E> implements Cloneable {
}
/**
+ * Return true if size() is 0.
+ * @return true if size() is 0.
+ */
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+
+ /**
* Given an index in the range <code>0...size()-1</code>, returns
* the key from the <code>index</code>th key-value mapping that this
* SparseArray stores.
diff --git a/android/support/v4/view/ViewCompat.java b/android/support/v4/view/ViewCompat.java
index 204a1218..abdaa1ad 100644
--- a/android/support/v4/view/ViewCompat.java
+++ b/android/support/v4/view/ViewCompat.java
@@ -60,6 +60,7 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.WeakHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
/**
* Helper for accessing features in {@link View}.
@@ -443,6 +444,7 @@ public class ViewCompat {
private static Field sMinHeightField;
private static boolean sMinHeightFieldFetched;
private static WeakHashMap<View, String> sTransitionNameMap;
+ private static final AtomicInteger sNextGeneratedId = new AtomicInteger(1);
private Method mDispatchStartTemporaryDetach;
private Method mDispatchFinishTemporaryDetach;
private boolean mTempDetachBound;
@@ -1020,6 +1022,21 @@ public class ViewCompat {
public boolean isImportantForAutofill(@NonNull View v) {
return true;
}
+
+ /**
+ * {@link ViewCompat#generateViewId()}
+ */
+ public int generateViewId() {
+ for (;;) {
+ final int result = sNextGeneratedId.get();
+ // aapt-generated IDs have the high byte nonzero; clamp to the range under that.
+ int newValue = result + 1;
+ if (newValue > 0x00FFFFFF) newValue = 1; // Roll over to 1, not 0.
+ if (sNextGeneratedId.compareAndSet(result, newValue)) {
+ return result;
+ }
+ }
+ }
}
@RequiresApi(15)
@@ -1178,6 +1195,11 @@ public class ViewCompat {
public Display getDisplay(View view) {
return view.getDisplay();
}
+
+ @Override
+ public int generateViewId() {
+ return View.generateViewId();
+ }
}
@RequiresApi(18)
@@ -2413,6 +2435,30 @@ public class ViewCompat {
}
/**
+ * Finds the first descendant view with the given ID, the view itself if the ID matches
+ * {@link View#getId()}, or throws an IllegalArgumentException if the ID is invalid or there
+ * is no matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#findViewById(int)
+ */
+ @NonNull
+ public static <T extends View> T requireViewById(@NonNull View view, @IdRes int id) {
+ // TODO: use and link to View#requireViewById() directly, once available
+ T targetView = view.findViewById(id);
+ if (targetView == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this View");
+ }
+ return targetView;
+ }
+
+ /**
* Indicates whether this View is opaque. An opaque View guarantees that it will
* draw all the pixels overlapping its bounds using a fully opaque color.
*
@@ -3931,5 +3977,15 @@ public class ViewCompat {
return IMPL.hasExplicitFocusable(view);
}
+ /**
+ * Generate a value suitable for use in {@link View#setId(int)}.
+ * This value will not collide with ID values generated at build time by aapt for R.id.
+ *
+ * @return a generated ID value
+ */
+ public static int generateViewId() {
+ return IMPL.generateViewId();
+ }
+
protected ViewCompat() {}
}
diff --git a/android/support/v4/view/ViewConfigurationCompat.java b/android/support/v4/view/ViewConfigurationCompat.java
index 60d37a9f..a12387b1 100644
--- a/android/support/v4/view/ViewConfigurationCompat.java
+++ b/android/support/v4/view/ViewConfigurationCompat.java
@@ -117,5 +117,17 @@ public final class ViewConfigurationCompat {
return 0;
}
+ /**
+ * @param config Used to get the hover slop directly from the {@link ViewConfiguration}.
+ *
+ * @return The hover slop value.
+ */
+ public static int getScaledHoverSlop(ViewConfiguration config) {
+ if (android.os.Build.VERSION.SDK_INT >= 28) {
+ return config.getScaledHoverSlop();
+ }
+ return config.getScaledTouchSlop() / 2;
+ }
+
private ViewConfigurationCompat() {}
}
diff --git a/android/support/v4/view/WindowCompat.java b/android/support/v4/view/WindowCompat.java
index cdf4789c..dd0a736c 100644
--- a/android/support/v4/view/WindowCompat.java
+++ b/android/support/v4/view/WindowCompat.java
@@ -16,6 +16,8 @@
package android.support.v4.view;
+import android.support.annotation.IdRes;
+import android.support.annotation.NonNull;
import android.view.View;
import android.view.Window;
@@ -59,4 +61,29 @@ public final class WindowCompat {
public static final int FEATURE_ACTION_MODE_OVERLAY = 10;
private WindowCompat() {}
+
+ /**
+ * Finds a view that was identified by the {@code android:id} XML attribute
+ * that was processed in {@link android.app.Activity#onCreate}, or throws an
+ * IllegalArgumentException if the ID is invalid, or there is no matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see ViewCompat#requireViewById(View, int)
+ * @see Window#findViewById(int)
+ */
+ @NonNull
+ public static <T extends View> T requireViewById(@NonNull Window window, @IdRes int id) {
+ // TODO: use and link to Window#requireViewById() directly, once available
+ T view = window.findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this Window");
+ }
+ return view;
+ }
}
diff --git a/android/support/v4/widget/ContentLoadingProgressBar.java b/android/support/v4/widget/ContentLoadingProgressBar.java
index 356c7b9e..631bec5b 100644
--- a/android/support/v4/widget/ContentLoadingProgressBar.java
+++ b/android/support/v4/widget/ContentLoadingProgressBar.java
@@ -93,9 +93,10 @@ public class ContentLoadingProgressBar extends ProgressBar {
* hidden until it has been shown for at least a minimum show time. If the
* progress view was not yet visible, cancels showing the progress view.
*/
- public void hide() {
+ public synchronized void hide() {
mDismissed = true;
removeCallbacks(mDelayedShow);
+ mPostedShow = false;
long diff = System.currentTimeMillis() - mStartTime;
if (diff >= MIN_SHOW_TIME || mStartTime == -1) {
// The progress spinner has been shown long enough
@@ -117,11 +118,12 @@ public class ContentLoadingProgressBar extends ProgressBar {
* Show the progress view after waiting for a minimum delay. If
* during that time, hide() is called, the view is never made visible.
*/
- public void show() {
+ public synchronized void show() {
// Reset the start time.
mStartTime = -1;
mDismissed = false;
removeCallbacks(mDelayedHide);
+ mPostedHide = false;
if (!mPostedShow) {
postDelayed(mDelayedShow, MIN_DELAY);
mPostedShow = true;
diff --git a/android/support/v4/widget/CursorAdapter.java b/android/support/v4/widget/CursorAdapter.java
index e68229e0..3ea6fc8a 100644
--- a/android/support/v4/widget/CursorAdapter.java
+++ b/android/support/v4/widget/CursorAdapter.java
@@ -43,55 +43,55 @@ public abstract class CursorAdapter extends BaseAdapter implements Filterable,
CursorFilter.CursorFilterClient {
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected boolean mDataValid;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected boolean mAutoRequery;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected Cursor mCursor;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected Context mContext;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected int mRowIDColumn;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected ChangeObserver mChangeObserver;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected DataSetObserver mDataSetObserver;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected CursorFilter mCursorFilter;
/**
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected FilterQueryProvider mFilterQueryProvider;
diff --git a/android/support/v4/widget/SimpleCursorAdapter.java b/android/support/v4/widget/SimpleCursorAdapter.java
index 291f9e15..ba3ee506 100644
--- a/android/support/v4/widget/SimpleCursorAdapter.java
+++ b/android/support/v4/widget/SimpleCursorAdapter.java
@@ -37,14 +37,14 @@ public class SimpleCursorAdapter extends ResourceCursorAdapter {
/**
* A list of columns containing the data to bind to the UI.
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected int[] mFrom;
/**
* A list of View ids representing the views to which the data must be bound.
* This field should be made private, so it is hidden from the SDK.
- * {@hide}
+ * @hide
*/
@RestrictTo(LIBRARY_GROUP)
protected int[] mTo;
diff --git a/android/support/v4/widget/TextViewCompat.java b/android/support/v4/widget/TextViewCompat.java
index dc87a38b..8789815f 100644
--- a/android/support/v4/widget/TextViewCompat.java
+++ b/android/support/v4/widget/TextViewCompat.java
@@ -18,6 +18,11 @@ package android.support.v4.widget;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.DrawableRes;
@@ -28,14 +33,22 @@ import android.support.annotation.RequiresApi;
import android.support.annotation.RestrictTo;
import android.support.annotation.StyleRes;
import android.support.v4.os.BuildCompat;
+import android.text.Editable;
import android.util.Log;
import android.util.TypedValue;
+import android.view.ActionMode;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.View;
import android.widget.TextView;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
/**
* Helper for accessing features in {@link TextView}.
@@ -219,6 +232,11 @@ public final class TextViewCompat {
}
return new int[0];
}
+
+ public void setCustomSelectionActionModeCallback(TextView textView,
+ ActionMode.Callback callback) {
+ textView.setCustomSelectionActionModeCallback(callback);
+ }
}
@RequiresApi(16)
@@ -314,8 +332,160 @@ public final class TextViewCompat {
}
}
+ @RequiresApi(26)
+ static class TextViewCompatApi26Impl extends TextViewCompatApi23Impl {
+ @Override
+ public void setCustomSelectionActionModeCallback(final TextView textView,
+ final ActionMode.Callback callback) {
+ if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O
+ && Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1) {
+ super.setCustomSelectionActionModeCallback(textView, callback);
+ return;
+ }
+
+
+ // A bug in O and O_MR1 causes a number of options for handling the ACTION_PROCESS_TEXT
+ // intent after selection to not be displayed in the menu, although they should be.
+ // Here we fix this, by removing the menu items created by the framework code, and
+ // adding them (and the missing ones) back correctly.
+ textView.setCustomSelectionActionModeCallback(new ActionMode.Callback() {
+ // This constant should be correlated with its definition in the
+ // android.widget.Editor class.
+ private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
+
+ // References to the MenuBuilder class and its removeItemAt(int) method.
+ // Since in most cases the menu instance processed by this callback is going
+ // to be a MenuBuilder, we keep these references to avoid querying for them
+ // frequently by reflection in recomputeProcessTextMenuItems.
+ private Class mMenuBuilderClass;
+ private Method mMenuBuilderRemoveItemAtMethod;
+ private boolean mCanUseMenuBuilderReferences;
+ private boolean mInitializedMenuBuilderReferences = false;
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ return callback.onCreateActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ recomputeProcessTextMenuItems(menu);
+ return callback.onPrepareActionMode(mode, menu);
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ return callback.onActionItemClicked(mode, item);
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ callback.onDestroyActionMode(mode);
+ }
+
+ private void recomputeProcessTextMenuItems(final Menu menu) {
+ final Context context = textView.getContext();
+ final PackageManager packageManager = context.getPackageManager();
+
+ if (!mInitializedMenuBuilderReferences) {
+ mInitializedMenuBuilderReferences = true;
+ try {
+ mMenuBuilderClass =
+ Class.forName("com.android.internal.view.menu.MenuBuilder");
+ mMenuBuilderRemoveItemAtMethod = mMenuBuilderClass
+ .getDeclaredMethod("removeItemAt", Integer.TYPE);
+ mCanUseMenuBuilderReferences = true;
+ } catch (ClassNotFoundException | NoSuchMethodException e) {
+ mMenuBuilderClass = null;
+ mMenuBuilderRemoveItemAtMethod = null;
+ mCanUseMenuBuilderReferences = false;
+ }
+ }
+ // Remove the menu items created for ACTION_PROCESS_TEXT handlers.
+ try {
+ final Method removeItemAtMethod =
+ (mCanUseMenuBuilderReferences && mMenuBuilderClass.isInstance(menu))
+ ? mMenuBuilderRemoveItemAtMethod
+ : menu.getClass()
+ .getDeclaredMethod("removeItemAt", Integer.TYPE);
+ for (int i = menu.size() - 1; i >= 0; --i) {
+ final MenuItem item = menu.getItem(i);
+ if (item.getIntent() != null && Intent.ACTION_PROCESS_TEXT
+ .equals(item.getIntent().getAction())) {
+ removeItemAtMethod.invoke(menu, i);
+ }
+ }
+ } catch (NoSuchMethodException | IllegalAccessException
+ | InvocationTargetException e) {
+ // There is a menu custom implementation used which is not providing
+ // a removeItemAt(int) menu. There is nothing we can do in this case.
+ return;
+ }
+
+ // Populate the menu again with the ACTION_PROCESS_TEXT handlers.
+ final List<ResolveInfo> supportedActivities =
+ getSupportedActivities(context, packageManager);
+ for (int i = 0; i < supportedActivities.size(); ++i) {
+ final ResolveInfo info = supportedActivities.get(i);
+ menu.add(Menu.NONE, Menu.NONE,
+ MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
+ info.loadLabel(packageManager))
+ .setIntent(createProcessTextIntentForResolveInfo(info, textView))
+ .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+ }
+ }
+
+ private List<ResolveInfo> getSupportedActivities(final Context context,
+ final PackageManager packageManager) {
+ final List<ResolveInfo> supportedActivities = new ArrayList<>();
+ boolean canStartActivityForResult = context instanceof Activity;
+ if (!canStartActivityForResult) {
+ return supportedActivities;
+ }
+ final List<ResolveInfo> unfiltered =
+ packageManager.queryIntentActivities(createProcessTextIntent(), 0);
+ for (ResolveInfo info : unfiltered) {
+ if (isSupportedActivity(info, context)) {
+ supportedActivities.add(info);
+ }
+ }
+ return supportedActivities;
+ }
+
+ private boolean isSupportedActivity(final ResolveInfo info, final Context context) {
+ if (context.getPackageName().equals(info.activityInfo.packageName)) {
+ return true;
+ }
+ if (!info.activityInfo.exported) {
+ return false;
+ }
+ return info.activityInfo.permission == null
+ || context.checkSelfPermission(info.activityInfo.permission)
+ == PackageManager.PERMISSION_GRANTED;
+ }
+
+ private Intent createProcessTextIntentForResolveInfo(final ResolveInfo info,
+ final TextView textView) {
+ return createProcessTextIntent()
+ .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !isEditable(textView))
+ .setClassName(info.activityInfo.packageName, info.activityInfo.name);
+ }
+
+ private boolean isEditable(final TextView textView) {
+ return textView instanceof Editable
+ && textView.onCheckIsTextEditor()
+ && textView.isEnabled();
+ }
+
+ private Intent createProcessTextIntent() {
+ return new Intent().setAction(Intent.ACTION_PROCESS_TEXT).setType("text/plain");
+ }
+ });
+ }
+ }
+
@RequiresApi(27)
- static class TextViewCompatApi27Impl extends TextViewCompatApi23Impl {
+ static class TextViewCompatApi27Impl extends TextViewCompatApi26Impl {
@Override
public void setAutoSizeTextTypeWithDefaults(TextView textView, int autoSizeTextType) {
textView.setAutoSizeTextTypeWithDefaults(autoSizeTextType);
@@ -369,6 +539,8 @@ public final class TextViewCompat {
static {
if (BuildCompat.isAtLeastOMR1()) {
IMPL = new TextViewCompatApi27Impl();
+ } else if (Build.VERSION.SDK_INT >= 26) {
+ IMPL = new TextViewCompatApi26Impl();
} else if (Build.VERSION.SDK_INT >= 23) {
IMPL = new TextViewCompatApi23Impl();
} else if (Build.VERSION.SDK_INT >= 18) {
@@ -600,4 +772,31 @@ public final class TextViewCompat {
public static int[] getAutoSizeTextAvailableSizes(@NonNull TextView textView) {
return IMPL.getAutoSizeTextAvailableSizes(textView);
}
+
+ /**
+ * Sets a selection action mode callback on a TextView.
+ *
+ * Also this method can be used to fix a bug in framework SDK 26. On these affected devices,
+ * the bug causes the menu containing the options for handling ACTION_PROCESS_TEXT after text
+ * selection to miss a number of items. This method can be used to fix this wrong behaviour for
+ * a text view, by passing any custom callback implementation. If no custom callback is desired,
+ * a no-op implementation should be provided.
+ *
+ * Note that, by default, the bug will only be fixed when the default floating toolbar menu
+ * implementation is used. If a custom implementation of {@link Menu} is provided, this should
+ * provide the method Menu#removeItemAt(int) which removes a menu item by its position,
+ * as given by Menu#getItem(int). Also, the following post condition should hold: a call
+ * to removeItemAt(i), should not modify the results of getItem(j) for any j < i. Intuitively,
+ * removing an element from the menu should behave as removing an element from a list.
+ * Note that this method does not exist in the {@link Menu} interface. However, it is required,
+ * and going to be called by reflection, in order to display the correct process text items in
+ * the menu.
+ *
+ * @param textView The TextView to set the action selection mode callback on.
+ * @param callback The action selection mode callback to set on textView.
+ */
+ public static void setCustomSelectionActionModeCallback(@NonNull TextView textView,
+ @NonNull ActionMode.Callback callback) {
+ IMPL.setCustomSelectionActionModeCallback(textView, callback);
+ }
}
diff --git a/android/support/v7/preference/Preference.java b/android/support/v7/preference/Preference.java
index fa8461db..88262cd9 100644
--- a/android/support/v7/preference/Preference.java
+++ b/android/support/v7/preference/Preference.java
@@ -16,6 +16,7 @@
package android.support.v7.preference;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.Context;
@@ -1297,6 +1298,7 @@ public class Preference implements Comparable<Preference> {
* preference was removed, modified, and re-added to a {@link PreferenceGroup}
* @hide
*/
+ @RestrictTo(LIBRARY)
public final boolean wasDetached() {
return mWasDetached;
}
@@ -1305,6 +1307,7 @@ public class Preference implements Comparable<Preference> {
* Clears the {@link #wasDetached()} status
* @hide
*/
+ @RestrictTo(LIBRARY)
public final void clearWasDetached() {
mWasDetached = false;
}
diff --git a/android/support/v7/recyclerview/extensions/ListAdapter.java b/android/support/v7/recyclerview/extensions/ListAdapter.java
index 8b28072e..721e0da4 100644
--- a/android/support/v7/recyclerview/extensions/ListAdapter.java
+++ b/android/support/v7/recyclerview/extensions/ListAdapter.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.
@@ -17,6 +17,8 @@
package android.support.v7.recyclerview.extensions;
import android.support.annotation.NonNull;
+import android.support.v7.util.AdapterListUpdateCallback;
+import android.support.v7.util.DiffUtil;
import android.support.v7.widget.RecyclerView;
import java.util.List;
@@ -66,7 +68,8 @@ import java.util.List;
* public void onBindViewHolder(UserViewHolder holder, int position) {
* holder.bindTo(getItem(position));
* }
- * public static final DiffCallback&lt;User> DIFF_CALLBACK = new DiffCallback&lt;User>() {
+ * public static final DiffUtil.ItemCallback&lt;User> DIFF_CALLBACK =
+ * new DiffUtil.ItemCallback&lt;User>() {
* {@literal @}Override
* public boolean areItemsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
@@ -95,14 +98,14 @@ public abstract class ListAdapter<T, VH extends RecyclerView.ViewHolder>
private final ListAdapterHelper<T> mHelper;
@SuppressWarnings("unused")
- protected ListAdapter(@NonNull DiffCallback<T> diffCallback) {
- mHelper = new ListAdapterHelper<>(new ListAdapterHelper.AdapterCallback(this),
- new ListAdapterConfig.Builder<T>().setDiffCallback(diffCallback).build());
+ protected ListAdapter(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
+ mHelper = new ListAdapterHelper<>(new AdapterListUpdateCallback(this),
+ new ListAdapterConfig.Builder<>(diffCallback).build());
}
@SuppressWarnings("unused")
protected ListAdapter(@NonNull ListAdapterConfig<T> config) {
- mHelper = new ListAdapterHelper<>(new ListAdapterHelper.AdapterCallback(this), config);
+ mHelper = new ListAdapterHelper<>(new AdapterListUpdateCallback(this), config);
}
/**
diff --git a/android/support/v7/recyclerview/extensions/ListAdapterConfig.java b/android/support/v7/recyclerview/extensions/ListAdapterConfig.java
index 25697a11..53fe4bbf 100644
--- a/android/support/v7/recyclerview/extensions/ListAdapterConfig.java
+++ b/android/support/v7/recyclerview/extensions/ListAdapterConfig.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,79 +16,91 @@
package android.support.v7.recyclerview.extensions;
-import android.arch.core.executor.ArchTaskExecutor;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+import android.support.v7.util.DiffUtil;
import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
/**
* Configuration object for {@link ListAdapter}, {@link ListAdapterHelper}, and similar
* background-thread list diffing adapter logic.
* <p>
- * At minimum, defines item diffing behavior with a {@link DiffCallback}, used to compute item
- * differences to pass to a RecyclerView adapter.
+ * At minimum, defines item diffing behavior with a {@link DiffUtil.ItemCallback}, used to compute
+ * item differences to pass to a RecyclerView adapter.
*
* @param <T> Type of items in the lists, and being compared.
*/
public final class ListAdapterConfig<T> {
+ @NonNull
private final Executor mMainThreadExecutor;
+ @NonNull
private final Executor mBackgroundThreadExecutor;
- private final DiffCallback<T> mDiffCallback;
+ @NonNull
+ private final DiffUtil.ItemCallback<T> mDiffCallback;
- private ListAdapterConfig(Executor mainThreadExecutor, Executor backgroundThreadExecutor,
- DiffCallback<T> diffCallback) {
+ private ListAdapterConfig(
+ @NonNull Executor mainThreadExecutor,
+ @NonNull Executor backgroundThreadExecutor,
+ @NonNull DiffUtil.ItemCallback<T> diffCallback) {
mMainThreadExecutor = mainThreadExecutor;
mBackgroundThreadExecutor = backgroundThreadExecutor;
mDiffCallback = diffCallback;
}
+ /** @hide */
+ @SuppressWarnings("WeakerAccess")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
public Executor getMainThreadExecutor() {
return mMainThreadExecutor;
}
+ /** @hide */
+ @SuppressWarnings("WeakerAccess")
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
public Executor getBackgroundThreadExecutor() {
return mBackgroundThreadExecutor;
}
- public DiffCallback<T> getDiffCallback() {
+ @SuppressWarnings("WeakerAccess")
+ @NonNull
+ public DiffUtil.ItemCallback<T> getDiffCallback() {
return mDiffCallback;
}
/**
* Builder class for {@link ListAdapterConfig}.
- * <p>
- * You must at minimum specify a DiffCallback with {@link #setDiffCallback(DiffCallback)}
*
* @param <T>
*/
public static class Builder<T> {
private Executor mMainThreadExecutor;
private Executor mBackgroundThreadExecutor;
- private DiffCallback<T> mDiffCallback;
+ private final DiffUtil.ItemCallback<T> mDiffCallback;
- /**
- * The {@link DiffCallback} to be used while diffing an old list with the updated one.
- * Must be provided.
- *
- * @param diffCallback The {@link DiffCallback} instance to compare items in the list.
- * @return this
- */
- @SuppressWarnings("WeakerAccess")
- public ListAdapterConfig.Builder<T> setDiffCallback(DiffCallback<T> diffCallback) {
+ public Builder(@NonNull DiffUtil.ItemCallback<T> diffCallback) {
mDiffCallback = diffCallback;
- return this;
}
/**
* If provided, defines the main thread executor used to dispatch adapter update
* notifications on the main thread.
* <p>
- * If not provided, it will default to the UI thread.
+ * If not provided, it will default to the main thread.
*
* @param executor The executor which can run tasks in the UI thread.
* @return this
+ *
+ * @hide
*/
- @SuppressWarnings("unused")
- public ListAdapterConfig.Builder<T> setMainThreadExecutor(Executor executor) {
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @NonNull
+ public Builder<T> setMainThreadExecutor(Executor executor) {
mMainThreadExecutor = executor;
return this;
}
@@ -97,36 +109,55 @@ public final class ListAdapterConfig<T> {
* If provided, defines the background executor used to calculate the diff between an old
* and a new list.
* <p>
- * If not provided, defaults to the IO thread pool from Architecture Components.
+ * If not provided, defaults to two thread pool executor, shared by all ListAdapterConfigs.
*
* @param executor The background executor to run list diffing.
* @return this
*/
- @SuppressWarnings("unused")
- public ListAdapterConfig.Builder<T> setBackgroundThreadExecutor(Executor executor) {
+ @SuppressWarnings({"unused", "WeakerAccess"})
+ @NonNull
+ public Builder<T> setBackgroundThreadExecutor(Executor executor) {
mBackgroundThreadExecutor = executor;
return this;
}
+ private static class MainThreadExecutor implements Executor {
+ final Handler mHandler = new Handler(Looper.getMainLooper());
+ @Override
+ public void execute(@NonNull Runnable command) {
+ mHandler.post(command);
+ }
+ }
+
/**
* Creates a {@link ListAdapterHelper} with the given parameters.
*
* @return A new ListAdapterConfig.
*/
+ @NonNull
public ListAdapterConfig<T> build() {
- if (mDiffCallback == null) {
- throw new IllegalArgumentException("Must provide a diffCallback");
+ if (mMainThreadExecutor == null) {
+ mMainThreadExecutor = sMainThreadExecutor;
}
if (mBackgroundThreadExecutor == null) {
- mBackgroundThreadExecutor = ArchTaskExecutor.getIOThreadExecutor();
- }
- if (mMainThreadExecutor == null) {
- mMainThreadExecutor = ArchTaskExecutor.getMainThreadExecutor();
+ synchronized (sExecutorLock) {
+ if (sDiffExecutor == null) {
+ sDiffExecutor = Executors.newFixedThreadPool(2);
+ }
+ }
+ mBackgroundThreadExecutor = sDiffExecutor;
}
return new ListAdapterConfig<>(
mMainThreadExecutor,
mBackgroundThreadExecutor,
mDiffCallback);
}
+
+ // TODO: remove the below once supportlib has its own appropriate executors
+ private static final Object sExecutorLock = new Object();
+ private static Executor sDiffExecutor = null;
+
+ // TODO: use MainThreadExecutor from supportlib once one exists
+ private static final Executor sMainThreadExecutor = new MainThreadExecutor();
}
}
diff --git a/android/support/v7/recyclerview/extensions/ListAdapterHelper.java b/android/support/v7/recyclerview/extensions/ListAdapterHelper.java
index d0c7bb3e..bb231b17 100644
--- a/android/support/v7/recyclerview/extensions/ListAdapterHelper.java
+++ b/android/support/v7/recyclerview/extensions/ListAdapterHelper.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,8 +16,8 @@
package android.support.v7.recyclerview.extensions;
-import android.arch.lifecycle.LiveData;
-import android.support.annotation.RestrictTo;
+import android.support.annotation.NonNull;
+import android.support.v7.util.AdapterListUpdateCallback;
import android.support.v7.util.DiffUtil;
import android.support.v7.util.ListUpdateCallback;
import android.support.v7.widget.RecyclerView;
@@ -25,17 +25,18 @@ import android.support.v7.widget.RecyclerView;
import java.util.List;
/**
- * Helper object for displaying a List in {@link RecyclerView.Adapter RecyclerView.Adapter}, which
- * signals the adapter of changes when the List is changed by computing changes with DiffUtil in the
+ * Helper object for displaying a List in
+ * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, which signals the
+ * adapter of changes when the List is changed by computing changes with DiffUtil in the
* background.
* <p>
* For simplicity, the {@link ListAdapter} wrapper class can often be used instead of the
* helper directly. This helper class is exposed for complex cases, and where overriding an adapter
* base class to support List diffing isn't convenient.
* <p>
- * The ListAdapterHelper can take a {@link LiveData} of List and present the data simply for an
- * adapter. It computes differences in List contents via DiffUtil on a background thread as new
- * Lists are received.
+ * The ListAdapterHelper can consume the values from a LiveData of <code>List</code> and present the
+ * data simply for an adapter. It computes differences in List contents via {@link DiffUtil} on a
+ * background thread as new <code>List</code>s are received.
* <p>
* It provides a simple list-like API with {@link #getItem(int)} and {@link #getItemCount()} for an
* adapter to acquire and present data objects.
@@ -68,10 +69,8 @@ import java.util.List;
* }
*
* class UserAdapter extends RecyclerView.Adapter&lt;UserViewHolder> {
- * private final ListAdapterHelper&lt;User> mHelper;
- * public UserAdapter(ListAdapterHelper.Builder&lt;User> builder) {
- * mHelper = new ListAdapterHelper(this, User.DIFF_CALLBACK);
- * }
+ * private final ListAdapterHelper&lt;User> mHelper =
+ * new ListAdapterHelper(this, DIFF_CALLBACK);
* {@literal @}Override
* public int getItemCount() {
* return mHelper.getItemCount();
@@ -84,7 +83,8 @@ import java.util.List;
* User user = mHelper.getItem(position);
* holder.bindTo(user);
* }
- * public static final DiffCallback&lt;User> DIFF_CALLBACK = new DiffCallback&lt;User>() {
+ * public static final DiffUtil.ItemCallback&lt;User> DIFF_CALLBACK
+ * = new DiffUtil.ItemCallback&lt;User>() {
* {@literal @}Override
* public boolean areItemsTheSame(
* {@literal @}NonNull User oldUser, {@literal @}NonNull User newUser) {
@@ -107,47 +107,37 @@ public class ListAdapterHelper<T> {
private final ListUpdateCallback mUpdateCallback;
private final ListAdapterConfig<T> mConfig;
- @SuppressWarnings("WeakerAccess")
- public ListAdapterHelper(ListUpdateCallback listUpdateCallback,
- ListAdapterConfig<T> config) {
- mUpdateCallback = listUpdateCallback;
- mConfig = config;
+ /**
+ * Convenience for
+ * {@code PagedListAdapterHelper(new AdapterListUpdateCallback(adapter),
+ * new ListAdapterConfig.Builder().setDiffCallback(diffCallback).build());}
+ *
+ * @param adapter Adapter to dispatch position updates to.
+ * @param diffCallback ItemCallback that compares items to dispatch appropriate animations when
+ *
+ * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter)
+ */
+ public ListAdapterHelper(@NonNull RecyclerView.Adapter adapter,
+ @NonNull DiffUtil.ItemCallback<T> diffCallback) {
+ mUpdateCallback = new AdapterListUpdateCallback(adapter);
+ mConfig = new ListAdapterConfig.Builder<>(diffCallback).build();
}
/**
- * Default ListUpdateCallback that dispatches directly to an adapter. Can be replaced by a
- * custom ListUpdateCallback if e.g. your adapter has a header in it, and so has an offset
- * between list positions and adapter positions.
+ * Create a ListAdapterHelper with the provided config, and ListUpdateCallback to dispatch
+ * updates to.
+ *
+ * @param listUpdateCallback Callback to dispatch updates to.
+ * @param config Config to define background work Executor, and DiffUtil.ItemCallback for
+ * computing List diffs.
*
- * @hide
+ * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter)
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- public static class AdapterCallback implements ListUpdateCallback {
- private final RecyclerView.Adapter mAdapter;
-
- public AdapterCallback(RecyclerView.Adapter adapter) {
- mAdapter = adapter;
- }
-
- @Override
- public void onInserted(int position, int count) {
- mAdapter.notifyItemRangeInserted(position, count);
- }
-
- @Override
- public void onRemoved(int position, int count) {
- mAdapter.notifyItemRangeRemoved(position, count);
- }
-
- @Override
- public void onMoved(int fromPosition, int toPosition) {
- mAdapter.notifyItemMoved(fromPosition, toPosition);
- }
-
- @Override
- public void onChanged(int position, int count, Object payload) {
- mAdapter.notifyItemRangeChanged(position, count, payload);
- }
+ @SuppressWarnings("WeakerAccess")
+ public ListAdapterHelper(@NonNull ListUpdateCallback listUpdateCallback,
+ @NonNull ListAdapterConfig<T> config) {
+ mUpdateCallback = listUpdateCallback;
+ mConfig = config;
}
private List<T> mList;
@@ -173,7 +163,8 @@ public class ListAdapterHelper<T> {
/**
* Get the number of items currently presented by this AdapterHelper. This value can be directly
- * returned to {@link RecyclerView.Adapter#getItemCount()}.
+ * returned to {@link android.support.v7.widget.RecyclerView.Adapter#getItemCount()
+ * RecyclerView.Adapter.getItemCount()}.
*
* @return Number of items being presented.
*/
diff --git a/android/support/v7/util/AdapterListUpdateCallback.java b/android/support/v7/util/AdapterListUpdateCallback.java
new file mode 100644
index 00000000..f86ba7d0
--- /dev/null
+++ b/android/support/v7/util/AdapterListUpdateCallback.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 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.support.v7.util;
+
+import android.support.annotation.NonNull;
+import android.support.v7.widget.RecyclerView;
+
+/**
+ * ListUpdateCallback that dispatches update events to the given adapter.
+ *
+ * @see DiffUtil.DiffResult#dispatchUpdatesTo(RecyclerView.Adapter)
+ */
+public final class AdapterListUpdateCallback implements ListUpdateCallback {
+ @NonNull
+ private final RecyclerView.Adapter mAdapter;
+
+ /**
+ * Creates an AdapterListUpdateCallback that will dispatch update events to the given adapter.
+ *
+ * @param adapter The Adapter to send updates to.
+ */
+ public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
+ mAdapter = adapter;
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onInserted(int position, int count) {
+ mAdapter.notifyItemRangeInserted(position, count);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onRemoved(int position, int count) {
+ mAdapter.notifyItemRangeRemoved(position, count);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onMoved(int fromPosition, int toPosition) {
+ mAdapter.notifyItemMoved(fromPosition, toPosition);
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onChanged(int position, int count, Object payload) {
+ mAdapter.notifyItemRangeChanged(position, count, payload);
+ }
+}
diff --git a/android/support/v7/util/DiffUtil.java b/android/support/v7/util/DiffUtil.java
index ebc33f31..a55a21d5 100644
--- a/android/support/v7/util/DiffUtil.java
+++ b/android/support/v7/util/DiffUtil.java
@@ -16,7 +16,6 @@
package android.support.v7.util;
-import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
@@ -369,7 +368,7 @@ public class DiffUtil {
*
* @see Callback#areItemsTheSame(int, int)
*/
- public abstract boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem);
+ public abstract boolean areItemsTheSame(T oldItem, T newItem);
/**
* Called to check whether two items have the same data.
@@ -392,7 +391,7 @@ public class DiffUtil {
*
* @see Callback#areContentsTheSame(int, int)
*/
- public abstract boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem);
+ public abstract boolean areContentsTheSame(T oldItem, T newItem);
/**
* When {@link #areItemsTheSame(T, T)} returns {@code true} for two items and
@@ -409,7 +408,7 @@ public class DiffUtil {
* @see Callback#getChangePayload(int, int)
*/
@SuppressWarnings({"WeakerAccess", "unused"})
- public Object getChangePayload(@NonNull T oldItem, @NonNull T newItem) {
+ public Object getChangePayload(T oldItem, T newItem) {
return null;
}
}
@@ -721,35 +720,16 @@ public class DiffUtil {
*
* @param adapter A RecyclerView adapter which was displaying the old list and will start
* displaying the new list.
+ * @see AdapterListUpdateCallback
*/
public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
- dispatchUpdatesTo(new ListUpdateCallback() {
- @Override
- public void onInserted(int position, int count) {
- adapter.notifyItemRangeInserted(position, count);
- }
-
- @Override
- public void onRemoved(int position, int count) {
- adapter.notifyItemRangeRemoved(position, count);
- }
-
- @Override
- public void onMoved(int fromPosition, int toPosition) {
- adapter.notifyItemMoved(fromPosition, toPosition);
- }
-
- @Override
- public void onChanged(int position, int count, Object payload) {
- adapter.notifyItemRangeChanged(position, count, payload);
- }
- });
+ dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
}
/**
* Dispatches update operations to the given Callback.
* <p>
- * These updates are atomic such that the first update call effects every update call that
+ * These updates are atomic such that the first update call affects every update call that
* comes after it (the same as RecyclerView).
*
* @param updateCallback The callback to receive the update operations.
diff --git a/android/support/v7/widget/AppCompatEditText.java b/android/support/v7/widget/AppCompatEditText.java
index 6831fcbf..fdda68eb 100644
--- a/android/support/v7/widget/AppCompatEditText.java
+++ b/android/support/v7/widget/AppCompatEditText.java
@@ -25,8 +25,10 @@ import android.graphics.drawable.Drawable;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
+import android.support.v4.os.BuildCompat;
import android.support.v4.view.TintableBackgroundView;
import android.support.v7.appcompat.R;
+import android.text.Editable;
import android.util.AttributeSet;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
@@ -71,6 +73,20 @@ public class AppCompatEditText extends EditText implements TintableBackgroundVie
mTextHelper.applyCompoundDrawablesTints();
}
+ /**
+ * Return the text that the view is displaying. If an editable text has not been set yet, this
+ * will return null.
+ */
+ @Override
+ @Nullable public Editable getText() {
+ if (BuildCompat.isAtLeastP()) {
+ return super.getText();
+ }
+ // A bug pre-P makes getText() crash if called before the first setText due to a cast, so
+ // retrieve the editable text.
+ return super.getEditableText();
+ }
+
@Override
public void setBackgroundResource(@DrawableRes int resId) {
super.setBackgroundResource(resId);
diff --git a/android/support/v7/widget/AppCompatProgressBarHelper.java b/android/support/v7/widget/AppCompatProgressBarHelper.java
index 443281e2..a95873cb 100644
--- a/android/support/v7/widget/AppCompatProgressBarHelper.java
+++ b/android/support/v7/widget/AppCompatProgressBarHelper.java
@@ -27,7 +27,7 @@ import android.graphics.drawable.LayerDrawable;
import android.graphics.drawable.ShapeDrawable;
import android.graphics.drawable.shapes.RoundRectShape;
import android.graphics.drawable.shapes.Shape;
-import android.support.v4.graphics.drawable.DrawableWrapper;
+import android.support.v4.graphics.drawable.WrappedDrawable;
import android.util.AttributeSet;
import android.view.Gravity;
import android.widget.ProgressBar;
@@ -69,11 +69,11 @@ class AppCompatProgressBarHelper {
* traverse layer and state list drawables.
*/
private Drawable tileify(Drawable drawable, boolean clip) {
- if (drawable instanceof DrawableWrapper) {
- Drawable inner = ((DrawableWrapper) drawable).getWrappedDrawable();
+ if (drawable instanceof WrappedDrawable) {
+ Drawable inner = ((WrappedDrawable) drawable).getWrappedDrawable();
if (inner != null) {
inner = tileify(inner, clip);
- ((DrawableWrapper) drawable).setWrappedDrawable(inner);
+ ((WrappedDrawable) drawable).setWrappedDrawable(inner);
}
} else if (drawable instanceof LayerDrawable) {
LayerDrawable background = (LayerDrawable) drawable;
diff --git a/android/support/v7/widget/ContentFrameLayout.java b/android/support/v7/widget/ContentFrameLayout.java
index 11002805..f777901c 100644
--- a/android/support/v7/widget/ContentFrameLayout.java
+++ b/android/support/v7/widget/ContentFrameLayout.java
@@ -16,6 +16,7 @@
package android.support.v7.widget;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static android.view.View.MeasureSpec.AT_MOST;
import static android.view.View.MeasureSpec.EXACTLY;
@@ -33,6 +34,7 @@ import android.widget.FrameLayout;
/**
* @hide
*/
+@RestrictTo(LIBRARY)
public class ContentFrameLayout extends FrameLayout {
public interface OnAttachListener {
diff --git a/android/support/v7/widget/DrawableUtils.java b/android/support/v7/widget/DrawableUtils.java
index c7820b6b..9216726e 100644
--- a/android/support/v7/widget/DrawableUtils.java
+++ b/android/support/v7/widget/DrawableUtils.java
@@ -30,6 +30,7 @@ import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.RestrictTo;
import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v4.graphics.drawable.WrappedDrawable;
import android.util.Log;
import java.lang.reflect.Field;
@@ -146,9 +147,9 @@ public class DrawableUtils {
}
}
}
- } else if (drawable instanceof android.support.v4.graphics.drawable.DrawableWrapper) {
+ } else if (drawable instanceof WrappedDrawable) {
return canSafelyMutateDrawable(
- ((android.support.v4.graphics.drawable.DrawableWrapper) drawable)
+ ((WrappedDrawable) drawable)
.getWrappedDrawable());
} else if (drawable instanceof android.support.v7.graphics.drawable.DrawableWrapper) {
return canSafelyMutateDrawable(
diff --git a/android/support/v7/widget/DropDownListView.java b/android/support/v7/widget/DropDownListView.java
index 5cad340e..cccb82be 100644
--- a/android/support/v7/widget/DropDownListView.java
+++ b/android/support/v7/widget/DropDownListView.java
@@ -17,12 +17,23 @@
package android.support.v7.widget;
import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.os.Build;
+import android.support.v4.graphics.drawable.DrawableCompat;
import android.support.v4.view.ViewPropertyAnimatorCompat;
import android.support.v4.widget.ListViewAutoScrollHelper;
import android.support.v7.appcompat.R;
+import android.support.v7.graphics.drawable.DrawableWrapper;
import android.view.MotionEvent;
import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+
+import java.lang.reflect.Field;
/**
* <p>Wrapper class for a ListView. This wrapper can hijack the focus to
@@ -30,7 +41,21 @@ import android.view.View;
* displayed on screen within a drop down. The focus is never actually
* passed to the drop down in this mode; the list only looks focused.</p>
*/
-class DropDownListView extends ListViewCompat {
+class DropDownListView extends ListView {
+ public static final int INVALID_POSITION = -1;
+ public static final int NO_POSITION = -1;
+
+ private final Rect mSelectorRect = new Rect();
+ private int mSelectionLeftPadding = 0;
+ private int mSelectionTopPadding = 0;
+ private int mSelectionRightPadding = 0;
+ private int mSelectionBottomPadding = 0;
+
+ private int mMotionPosition;
+
+ private Field mIsChildViewEnabled;
+
+ private GateKeeperDrawable mSelector;
/*
* WARNING: This is a workaround for a touch mode issue.
@@ -81,10 +106,306 @@ class DropDownListView extends ListViewCompat {
*
* @param context this view's context
*/
- public DropDownListView(Context context, boolean hijackFocus) {
+ DropDownListView(Context context, boolean hijackFocus) {
super(context, null, R.attr.dropDownListViewStyle);
mHijackFocus = hijackFocus;
setCacheColorHint(0); // Transparent, since the background drawable could be anything.
+
+ try {
+ mIsChildViewEnabled = AbsListView.class.getDeclaredField("mIsChildViewEnabled");
+ mIsChildViewEnabled.setAccessible(true);
+ } catch (NoSuchFieldException e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ @Override
+ public boolean isInTouchMode() {
+ // WARNING: Please read the comment where mListSelectionHidden is declared
+ return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
+ }
+
+ /**
+ * <p>Returns the focus state in the drop down.</p>
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean hasWindowFocus() {
+ return mHijackFocus || super.hasWindowFocus();
+ }
+
+ /**
+ * <p>Returns the focus state in the drop down.</p>
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean isFocused() {
+ return mHijackFocus || super.isFocused();
+ }
+
+ /**
+ * <p>Returns the focus state in the drop down.</p>
+ *
+ * @return true always if hijacking focus
+ */
+ @Override
+ public boolean hasFocus() {
+ return mHijackFocus || super.hasFocus();
+ }
+
+ @Override
+ public void setSelector(Drawable sel) {
+ mSelector = sel != null ? new GateKeeperDrawable(sel) : null;
+ super.setSelector(mSelector);
+
+ final Rect padding = new Rect();
+ if (sel != null) {
+ sel.getPadding(padding);
+ }
+
+ mSelectionLeftPadding = padding.left;
+ mSelectionTopPadding = padding.top;
+ mSelectionRightPadding = padding.right;
+ mSelectionBottomPadding = padding.bottom;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ setSelectorEnabled(true);
+ updateSelectorStateCompat();
+ }
+
+ @Override
+ protected void dispatchDraw(Canvas canvas) {
+ final boolean drawSelectorOnTop = false;
+ if (!drawSelectorOnTop) {
+ drawSelectorCompat(canvas);
+ }
+
+ super.dispatchDraw(canvas);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ switch (ev.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ mMotionPosition = pointToPosition((int) ev.getX(), (int) ev.getY());
+ break;
+ }
+ return super.onTouchEvent(ev);
+ }
+
+ /**
+ * Find a position that can be selected (i.e., is not a separator).
+ *
+ * @param position The starting position to look at.
+ * @param lookDown Whether to look down for other positions.
+ * @return The next selectable position starting at position and then searching either up or
+ * down. Returns {@link #INVALID_POSITION} if nothing can be found.
+ */
+ public int lookForSelectablePosition(int position, boolean lookDown) {
+ final ListAdapter adapter = getAdapter();
+ if (adapter == null || isInTouchMode()) {
+ return INVALID_POSITION;
+ }
+
+ final int count = adapter.getCount();
+ if (!getAdapter().areAllItemsEnabled()) {
+ if (lookDown) {
+ position = Math.max(0, position);
+ while (position < count && !adapter.isEnabled(position)) {
+ position++;
+ }
+ } else {
+ position = Math.min(position, count - 1);
+ while (position >= 0 && !adapter.isEnabled(position)) {
+ position--;
+ }
+ }
+
+ if (position < 0 || position >= count) {
+ return INVALID_POSITION;
+ }
+ return position;
+ } else {
+ if (position < 0 || position >= count) {
+ return INVALID_POSITION;
+ }
+ return position;
+ }
+ }
+
+ /**
+ * Measures the height of the given range of children (inclusive) and returns the height
+ * with this ListView's padding and divider heights included. If maxHeight is provided, the
+ * measuring will stop when the current height reaches maxHeight.
+ *
+ * @param widthMeasureSpec The width measure spec to be given to a child's
+ * {@link View#measure(int, int)}.
+ * @param startPosition The position of the first child to be shown.
+ * @param endPosition The (inclusive) position of the last child to be
+ * shown. Specify {@link #NO_POSITION} if the last child
+ * should be the last available child from the adapter.
+ * @param maxHeight The maximum height that will be returned (if all the
+ * children don't fit in this value, this value will be
+ * returned).
+ * @param disallowPartialChildPosition In general, whether the returned height should only
+ * contain entire children. This is more powerful--it is
+ * the first inclusive position at which partial
+ * children will not be allowed. Example: it looks nice
+ * to have at least 3 completely visible children, and
+ * in portrait this will most likely fit; but in
+ * landscape there could be times when even 2 children
+ * can not be completely shown, so a value of 2
+ * (remember, inclusive) would be good (assuming
+ * startPosition is 0).
+ * @return The height of this ListView with the given children.
+ */
+ public int measureHeightOfChildrenCompat(int widthMeasureSpec, int startPosition,
+ int endPosition, final int maxHeight,
+ int disallowPartialChildPosition) {
+
+ final int paddingTop = getListPaddingTop();
+ final int paddingBottom = getListPaddingBottom();
+ final int paddingLeft = getListPaddingLeft();
+ final int paddingRight = getListPaddingRight();
+ final int reportedDividerHeight = getDividerHeight();
+ final Drawable divider = getDivider();
+
+ final ListAdapter adapter = getAdapter();
+
+ if (adapter == null) {
+ return paddingTop + paddingBottom;
+ }
+
+ // Include the padding of the list
+ int returnedHeight = paddingTop + paddingBottom;
+ final int dividerHeight = ((reportedDividerHeight > 0) && divider != null)
+ ? reportedDividerHeight : 0;
+
+ // The previous height value that was less than maxHeight and contained
+ // no partial children
+ int prevHeightWithoutPartialChild = 0;
+
+ View child = null;
+ int viewType = 0;
+ int count = adapter.getCount();
+ for (int i = 0; i < count; i++) {
+ int newType = adapter.getItemViewType(i);
+ if (newType != viewType) {
+ child = null;
+ viewType = newType;
+ }
+ child = adapter.getView(i, child, this);
+
+ // Compute child height spec
+ int heightMeasureSpec;
+ ViewGroup.LayoutParams childLp = child.getLayoutParams();
+
+ if (childLp == null) {
+ childLp = generateDefaultLayoutParams();
+ child.setLayoutParams(childLp);
+ }
+
+ if (childLp.height > 0) {
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(childLp.height,
+ MeasureSpec.EXACTLY);
+ } else {
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ }
+ child.measure(widthMeasureSpec, heightMeasureSpec);
+
+ // Since this view was measured directly against the parent measure
+ // spec, we must measure it again before reuse.
+ child.forceLayout();
+
+ if (i > 0) {
+ // Count the divider for all but one child
+ returnedHeight += dividerHeight;
+ }
+
+ returnedHeight += child.getMeasuredHeight();
+
+ if (returnedHeight >= maxHeight) {
+ // We went over, figure out which height to return. If returnedHeight >
+ // maxHeight, then the i'th position did not fit completely.
+ return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
+ && (i > disallowPartialChildPosition) // We've past the min pos
+ && (prevHeightWithoutPartialChild > 0) // We have a prev height
+ && (returnedHeight != maxHeight) // i'th child did not fit completely
+ ? prevHeightWithoutPartialChild
+ : maxHeight;
+ }
+
+ if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
+ prevHeightWithoutPartialChild = returnedHeight;
+ }
+ }
+
+ // At this point, we went through the range of children, and they each
+ // completely fit, so return the returnedHeight
+ return returnedHeight;
+ }
+
+ private void setSelectorEnabled(boolean enabled) {
+ if (mSelector != null) {
+ mSelector.setEnabled(enabled);
+ }
+ }
+
+ private static class GateKeeperDrawable extends DrawableWrapper {
+ private boolean mEnabled;
+
+ GateKeeperDrawable(Drawable drawable) {
+ super(drawable);
+ mEnabled = true;
+ }
+
+ void setEnabled(boolean enabled) {
+ mEnabled = enabled;
+ }
+
+ @Override
+ public boolean setState(int[] stateSet) {
+ if (mEnabled) {
+ return super.setState(stateSet);
+ }
+ return false;
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ if (mEnabled) {
+ super.draw(canvas);
+ }
+ }
+
+ @Override
+ public void setHotspot(float x, float y) {
+ if (mEnabled) {
+ super.setHotspot(x, y);
+ }
+ }
+
+ @Override
+ public void setHotspotBounds(int left, int top, int right, int bottom) {
+ if (mEnabled) {
+ super.setHotspotBounds(left, top, right, bottom);
+ }
+ }
+
+ @Override
+ public boolean setVisible(boolean visible, boolean restart) {
+ if (mEnabled) {
+ return super.setVisible(visible, restart);
+ }
+ return false;
+ }
}
/**
@@ -169,6 +490,77 @@ class DropDownListView extends ListViewCompat {
mListSelectionHidden = hideListSelection;
}
+ private void updateSelectorStateCompat() {
+ Drawable selector = getSelector();
+ if (selector != null && touchModeDrawsInPressedStateCompat() && isPressed()) {
+ selector.setState(getDrawableState());
+ }
+ }
+
+ private void drawSelectorCompat(Canvas canvas) {
+ if (!mSelectorRect.isEmpty()) {
+ final Drawable selector = getSelector();
+ if (selector != null) {
+ selector.setBounds(mSelectorRect);
+ selector.draw(canvas);
+ }
+ }
+ }
+
+ private void positionSelectorLikeTouchCompat(int position, View sel, float x, float y) {
+ positionSelectorLikeFocusCompat(position, sel);
+
+ Drawable selector = getSelector();
+ if (selector != null && position != INVALID_POSITION) {
+ DrawableCompat.setHotspot(selector, x, y);
+ }
+ }
+
+ private void positionSelectorLikeFocusCompat(int position, View sel) {
+ // If we're changing position, update the visibility since the selector
+ // is technically being detached from the previous selection.
+ final Drawable selector = getSelector();
+ final boolean manageState = selector != null && position != INVALID_POSITION;
+ if (manageState) {
+ selector.setVisible(false, false);
+ }
+
+ positionSelectorCompat(position, sel);
+
+ if (manageState) {
+ final Rect bounds = mSelectorRect;
+ final float x = bounds.exactCenterX();
+ final float y = bounds.exactCenterY();
+ selector.setVisible(getVisibility() == VISIBLE, false);
+ DrawableCompat.setHotspot(selector, x, y);
+ }
+ }
+
+ private void positionSelectorCompat(int position, View sel) {
+ final Rect selectorRect = mSelectorRect;
+ selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom());
+
+ // Adjust for selection padding.
+ selectorRect.left -= mSelectionLeftPadding;
+ selectorRect.top -= mSelectionTopPadding;
+ selectorRect.right += mSelectionRightPadding;
+ selectorRect.bottom += mSelectionBottomPadding;
+
+ try {
+ // AbsListView.mIsChildViewEnabled controls the selector's state so we need to
+ // modify its value
+ final boolean isChildViewEnabled = mIsChildViewEnabled.getBoolean(this);
+ if (sel.isEnabled() != isChildViewEnabled) {
+ mIsChildViewEnabled.set(this, !isChildViewEnabled);
+ if (position != INVALID_POSITION) {
+ refreshDrawableState();
+ }
+ }
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ }
+ }
+
private void clearPressedItem() {
mDrawsInPressedState = false;
setPressed(false);
@@ -233,44 +625,7 @@ class DropDownListView extends ListViewCompat {
refreshDrawableState();
}
- @Override
- protected boolean touchModeDrawsInPressedStateCompat() {
- return mDrawsInPressedState || super.touchModeDrawsInPressedStateCompat();
- }
-
- @Override
- public boolean isInTouchMode() {
- // WARNING: Please read the comment where mListSelectionHidden is declared
- return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
- }
-
- /**
- * <p>Returns the focus state in the drop down.</p>
- *
- * @return true always if hijacking focus
- */
- @Override
- public boolean hasWindowFocus() {
- return mHijackFocus || super.hasWindowFocus();
- }
-
- /**
- * <p>Returns the focus state in the drop down.</p>
- *
- * @return true always if hijacking focus
- */
- @Override
- public boolean isFocused() {
- return mHijackFocus || super.isFocused();
- }
-
- /**
- * <p>Returns the focus state in the drop down.</p>
- *
- * @return true always if hijacking focus
- */
- @Override
- public boolean hasFocus() {
- return mHijackFocus || super.hasFocus();
+ private boolean touchModeDrawsInPressedStateCompat() {
+ return mDrawsInPressedState;
}
}
diff --git a/android/support/v7/widget/LinearLayoutCompat.java b/android/support/v7/widget/LinearLayoutCompat.java
index f071ae4f..ef68896f 100644
--- a/android/support/v7/widget/LinearLayoutCompat.java
+++ b/android/support/v7/widget/LinearLayoutCompat.java
@@ -16,6 +16,7 @@
package android.support.v7.widget;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.content.Context;
@@ -559,6 +560,7 @@ public class LinearLayoutCompat extends ViewGroup {
* @return true if there should be a divider before the child at childIndex
* @hide Pending API consideration. Currently only used internally by the system.
*/
+ @RestrictTo(LIBRARY)
protected boolean hasDividerBeforeChildAt(int childIndex) {
if (childIndex == 0) {
return (mShowDividers & SHOW_DIVIDER_BEGINNING) != 0;
diff --git a/android/support/v7/widget/ListViewCompat.java b/android/support/v7/widget/ListViewCompat.java
deleted file mode 100644
index 3a2fba3b..00000000
--- a/android/support/v7/widget/ListViewCompat.java
+++ /dev/null
@@ -1,413 +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.support.v7.widget;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.content.Context;
-import android.graphics.Canvas;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
-import android.support.annotation.RestrictTo;
-import android.support.v4.graphics.drawable.DrawableCompat;
-import android.support.v7.graphics.drawable.DrawableWrapper;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AbsListView;
-import android.widget.ListAdapter;
-import android.widget.ListView;
-
-import java.lang.reflect.Field;
-
-/**
- * This class contains a number of useful things for ListView. Mainly used by
- * {@link android.support.v7.widget.ListPopupWindow}.
- *
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public class ListViewCompat extends ListView {
-
- public static final int INVALID_POSITION = -1;
- public static final int NO_POSITION = -1;
-
- private static final int[] STATE_SET_NOTHING = new int[] { 0 };
-
- final Rect mSelectorRect = new Rect();
- int mSelectionLeftPadding = 0;
- int mSelectionTopPadding = 0;
- int mSelectionRightPadding = 0;
- int mSelectionBottomPadding = 0;
-
- protected int mMotionPosition;
-
- private Field mIsChildViewEnabled;
-
- private GateKeeperDrawable mSelector;
-
- public ListViewCompat(Context context) {
- this(context, null);
- }
-
- public ListViewCompat(Context context, AttributeSet attrs) {
- this(context, attrs, 0);
- }
-
- public ListViewCompat(Context context, AttributeSet attrs, int defStyleAttr) {
- super(context, attrs, defStyleAttr);
-
- try {
- mIsChildViewEnabled = AbsListView.class.getDeclaredField("mIsChildViewEnabled");
- mIsChildViewEnabled.setAccessible(true);
- } catch (NoSuchFieldException e) {
- e.printStackTrace();
- }
- }
-
- @Override
- public void setSelector(Drawable sel) {
- mSelector = sel != null ? new GateKeeperDrawable(sel) : null;
- super.setSelector(mSelector);
-
- final Rect padding = new Rect();
- if (sel != null) {
- sel.getPadding(padding);
- }
-
- mSelectionLeftPadding = padding.left;
- mSelectionTopPadding = padding.top;
- mSelectionRightPadding = padding.right;
- mSelectionBottomPadding = padding.bottom;
- }
-
- @Override
- protected void drawableStateChanged() {
- super.drawableStateChanged();
-
- setSelectorEnabled(true);
- updateSelectorStateCompat();
- }
-
- @Override
- protected void dispatchDraw(Canvas canvas) {
- final boolean drawSelectorOnTop = false;
- if (!drawSelectorOnTop) {
- drawSelectorCompat(canvas);
- }
-
- super.dispatchDraw(canvas);
- }
-
- @Override
- public boolean onTouchEvent(MotionEvent ev) {
- switch (ev.getAction()) {
- case MotionEvent.ACTION_DOWN:
- mMotionPosition = pointToPosition((int) ev.getX(), (int) ev.getY());
- break;
- }
- return super.onTouchEvent(ev);
- }
-
- protected void updateSelectorStateCompat() {
- Drawable selector = getSelector();
- if (selector != null && shouldShowSelectorCompat()) {
- selector.setState(getDrawableState());
- }
- }
-
- protected boolean shouldShowSelectorCompat() {
- return touchModeDrawsInPressedStateCompat() && isPressed();
- }
-
- protected boolean touchModeDrawsInPressedStateCompat() {
- return false;
- }
-
- protected void drawSelectorCompat(Canvas canvas) {
- if (!mSelectorRect.isEmpty()) {
- final Drawable selector = getSelector();
- if (selector != null) {
- selector.setBounds(mSelectorRect);
- selector.draw(canvas);
- }
- }
- }
-
- /**
- * Find a position that can be selected (i.e., is not a separator).
- *
- * @param position The starting position to look at.
- * @param lookDown Whether to look down for other positions.
- * @return The next selectable position starting at position and then searching either up or
- * down. Returns {@link #INVALID_POSITION} if nothing can be found.
- */
- public int lookForSelectablePosition(int position, boolean lookDown) {
- final ListAdapter adapter = getAdapter();
- if (adapter == null || isInTouchMode()) {
- return INVALID_POSITION;
- }
-
- final int count = adapter.getCount();
- if (!getAdapter().areAllItemsEnabled()) {
- if (lookDown) {
- position = Math.max(0, position);
- while (position < count && !adapter.isEnabled(position)) {
- position++;
- }
- } else {
- position = Math.min(position, count - 1);
- while (position >= 0 && !adapter.isEnabled(position)) {
- position--;
- }
- }
-
- if (position < 0 || position >= count) {
- return INVALID_POSITION;
- }
- return position;
- } else {
- if (position < 0 || position >= count) {
- return INVALID_POSITION;
- }
- return position;
- }
- }
-
- protected void positionSelectorLikeTouchCompat(int position, View sel, float x, float y) {
- positionSelectorLikeFocusCompat(position, sel);
-
- Drawable selector = getSelector();
- if (selector != null && position != INVALID_POSITION) {
- DrawableCompat.setHotspot(selector, x, y);
- }
- }
-
- protected void positionSelectorLikeFocusCompat(int position, View sel) {
- // If we're changing position, update the visibility since the selector
- // is technically being detached from the previous selection.
- final Drawable selector = getSelector();
- final boolean manageState = selector != null && position != INVALID_POSITION;
- if (manageState) {
- selector.setVisible(false, false);
- }
-
- positionSelectorCompat(position, sel);
-
- if (manageState) {
- final Rect bounds = mSelectorRect;
- final float x = bounds.exactCenterX();
- final float y = bounds.exactCenterY();
- selector.setVisible(getVisibility() == VISIBLE, false);
- DrawableCompat.setHotspot(selector, x, y);
- }
- }
-
- protected void positionSelectorCompat(int position, View sel) {
- final Rect selectorRect = mSelectorRect;
- selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom());
-
- // Adjust for selection padding.
- selectorRect.left -= mSelectionLeftPadding;
- selectorRect.top -= mSelectionTopPadding;
- selectorRect.right += mSelectionRightPadding;
- selectorRect.bottom += mSelectionBottomPadding;
-
- try {
- // AbsListView.mIsChildViewEnabled controls the selector's state so we need to
- // modify its value
- final boolean isChildViewEnabled = mIsChildViewEnabled.getBoolean(this);
- if (sel.isEnabled() != isChildViewEnabled) {
- mIsChildViewEnabled.set(this, !isChildViewEnabled);
- if (position != INVALID_POSITION) {
- refreshDrawableState();
- }
- }
- } catch (IllegalAccessException e) {
- e.printStackTrace();
- }
- }
-
- /**
- * Measures the height of the given range of children (inclusive) and returns the height
- * with this ListView's padding and divider heights included. If maxHeight is provided, the
- * measuring will stop when the current height reaches maxHeight.
- *
- * @param widthMeasureSpec The width measure spec to be given to a child's
- * {@link View#measure(int, int)}.
- * @param startPosition The position of the first child to be shown.
- * @param endPosition The (inclusive) position of the last child to be
- * shown. Specify {@link #NO_POSITION} if the last child
- * should be the last available child from the adapter.
- * @param maxHeight The maximum height that will be returned (if all the
- * children don't fit in this value, this value will be
- * returned).
- * @param disallowPartialChildPosition In general, whether the returned height should only
- * contain entire children. This is more powerful--it is
- * the first inclusive position at which partial
- * children will not be allowed. Example: it looks nice
- * to have at least 3 completely visible children, and
- * in portrait this will most likely fit; but in
- * landscape there could be times when even 2 children
- * can not be completely shown, so a value of 2
- * (remember, inclusive) would be good (assuming
- * startPosition is 0).
- * @return The height of this ListView with the given children.
- */
- public int measureHeightOfChildrenCompat(int widthMeasureSpec, int startPosition,
- int endPosition, final int maxHeight,
- int disallowPartialChildPosition) {
-
- final int paddingTop = getListPaddingTop();
- final int paddingBottom = getListPaddingBottom();
- final int paddingLeft = getListPaddingLeft();
- final int paddingRight = getListPaddingRight();
- final int reportedDividerHeight = getDividerHeight();
- final Drawable divider = getDivider();
-
- final ListAdapter adapter = getAdapter();
-
- if (adapter == null) {
- return paddingTop + paddingBottom;
- }
-
- // Include the padding of the list
- int returnedHeight = paddingTop + paddingBottom;
- final int dividerHeight = ((reportedDividerHeight > 0) && divider != null)
- ? reportedDividerHeight : 0;
-
- // The previous height value that was less than maxHeight and contained
- // no partial children
- int prevHeightWithoutPartialChild = 0;
-
- View child = null;
- int viewType = 0;
- int count = adapter.getCount();
- for (int i = 0; i < count; i++) {
- int newType = adapter.getItemViewType(i);
- if (newType != viewType) {
- child = null;
- viewType = newType;
- }
- child = adapter.getView(i, child, this);
-
- // Compute child height spec
- int heightMeasureSpec;
- ViewGroup.LayoutParams childLp = child.getLayoutParams();
-
- if (childLp == null) {
- childLp = generateDefaultLayoutParams();
- child.setLayoutParams(childLp);
- }
-
- if (childLp.height > 0) {
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(childLp.height,
- MeasureSpec.EXACTLY);
- } else {
- heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
- }
- child.measure(widthMeasureSpec, heightMeasureSpec);
-
- // Since this view was measured directly against the parent measure
- // spec, we must measure it again before reuse.
- child.forceLayout();
-
- if (i > 0) {
- // Count the divider for all but one child
- returnedHeight += dividerHeight;
- }
-
- returnedHeight += child.getMeasuredHeight();
-
- if (returnedHeight >= maxHeight) {
- // We went over, figure out which height to return. If returnedHeight >
- // maxHeight, then the i'th position did not fit completely.
- return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
- && (i > disallowPartialChildPosition) // We've past the min pos
- && (prevHeightWithoutPartialChild > 0) // We have a prev height
- && (returnedHeight != maxHeight) // i'th child did not fit completely
- ? prevHeightWithoutPartialChild
- : maxHeight;
- }
-
- if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
- prevHeightWithoutPartialChild = returnedHeight;
- }
- }
-
- // At this point, we went through the range of children, and they each
- // completely fit, so return the returnedHeight
- return returnedHeight;
- }
-
- protected void setSelectorEnabled(boolean enabled) {
- if (mSelector != null) {
- mSelector.setEnabled(enabled);
- }
- }
-
- private static class GateKeeperDrawable extends DrawableWrapper {
- private boolean mEnabled;
-
- public GateKeeperDrawable(Drawable drawable) {
- super(drawable);
- mEnabled = true;
- }
-
- void setEnabled(boolean enabled) {
- mEnabled = enabled;
- }
-
- @Override
- public boolean setState(int[] stateSet) {
- if (mEnabled) {
- return super.setState(stateSet);
- }
- return false;
- }
-
- @Override
- public void draw(Canvas canvas) {
- if (mEnabled) {
- super.draw(canvas);
- }
- }
-
- @Override
- public void setHotspot(float x, float y) {
- if (mEnabled) {
- super.setHotspot(x, y);
- }
- }
-
- @Override
- public void setHotspotBounds(int left, int top, int right, int bottom) {
- if (mEnabled) {
- super.setHotspotBounds(left, top, right, bottom);
- }
- }
-
- @Override
- public boolean setVisible(boolean visible, boolean restart) {
- if (mEnabled) {
- return super.setVisible(visible, restart);
- }
- return false;
- }
- }
-}
diff --git a/android/support/v7/widget/RecyclerView.java b/android/support/v7/widget/RecyclerView.java
index a2879796..b195d3c0 100644
--- a/android/support/v7/widget/RecyclerView.java
+++ b/android/support/v7/widget/RecyclerView.java
@@ -2722,7 +2722,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
removeCallbacks(mItemAnimatorRunner);
mViewInfoStore.onDetach();
- if (ALLOW_THREAD_GAP_WORK) {
+ if (ALLOW_THREAD_GAP_WORK && mGapWorker != null) {
// Unregister with gap worker
mGapWorker.remove(this);
mGapWorker = null;
@@ -6643,11 +6643,19 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
* @see #onCreateViewHolder(ViewGroup, int)
*/
public final VH createViewHolder(@NonNull ViewGroup parent, int viewType) {
- TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
- final VH holder = onCreateViewHolder(parent, viewType);
- holder.mItemViewType = viewType;
- TraceCompat.endSection();
- return holder;
+ try {
+ TraceCompat.beginSection(TRACE_CREATE_VIEW_TAG);
+ final VH holder = onCreateViewHolder(parent, viewType);
+ if (holder.itemView.getParent() != null) {
+ throw new IllegalStateException("ViewHolder views must not be attached when"
+ + " created. Ensure that you are not passing 'true' to the attachToRoot"
+ + " parameter of LayoutInflater.inflate(..., boolean attachToRoot)");
+ }
+ holder.mItemViewType = viewType;
+ return holder;
+ } finally {
+ TraceCompat.endSection();
+ }
}
/**
@@ -10108,7 +10116,7 @@ public class RecyclerView extends ViewGroup implements ScrollingView, NestedScro
if (vScroll == 0 && hScroll == 0) {
return false;
}
- mRecyclerView.scrollBy(hScroll, vScroll);
+ mRecyclerView.smoothScrollBy(hScroll, vScroll);
return true;
}
diff --git a/android/support/v7/widget/StaggeredGridLayoutManager.java b/android/support/v7/widget/StaggeredGridLayoutManager.java
index 55fb14e8..4e560b45 100644
--- a/android/support/v7/widget/StaggeredGridLayoutManager.java
+++ b/android/support/v7/widget/StaggeredGridLayoutManager.java
@@ -16,6 +16,7 @@
package android.support.v7.widget;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_HEAD;
import static android.support.v7.widget.LayoutState.ITEM_DIRECTION_TAIL;
@@ -2069,6 +2070,7 @@ public class StaggeredGridLayoutManager extends RecyclerView.LayoutManager imple
/** @hide */
@Override
+ @RestrictTo(LIBRARY)
public void collectAdjacentPrefetchPositions(int dx, int dy, RecyclerView.State state,
LayoutPrefetchRegistry layoutPrefetchRegistry) {
/* This method uses the simplifying assumption that the next N items (where N = span count)
diff --git a/android/support/v7/widget/TooltipCompatHandler.java b/android/support/v7/widget/TooltipCompatHandler.java
index 63a61982..8de44e6f 100644
--- a/android/support/v7/widget/TooltipCompatHandler.java
+++ b/android/support/v7/widget/TooltipCompatHandler.java
@@ -22,6 +22,7 @@ import static android.view.View.SYSTEM_UI_FLAG_LOW_PROFILE;
import android.content.Context;
import android.support.annotation.RestrictTo;
import android.support.v4.view.ViewCompat;
+import android.support.v4.view.ViewConfigurationCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.MotionEvent;
@@ -46,6 +47,7 @@ class TooltipCompatHandler implements View.OnLongClickListener, View.OnHoverList
private final View mAnchor;
private final CharSequence mTooltipText;
+ private final int mHoverSlop;
private final Runnable mShowRunnable = new Runnable() {
@Override
@@ -104,6 +106,9 @@ class TooltipCompatHandler implements View.OnLongClickListener, View.OnHoverList
private TooltipCompatHandler(View anchor, CharSequence tooltipText) {
mAnchor = anchor;
mTooltipText = tooltipText;
+ mHoverSlop = ViewConfigurationCompat.getScaledHoverSlop(
+ ViewConfiguration.get(mAnchor.getContext()));
+ clearAnchorPos();
mAnchor.setOnLongClickListener(this);
mAnchor.setOnHoverListener(this);
@@ -129,13 +134,12 @@ class TooltipCompatHandler implements View.OnLongClickListener, View.OnHoverList
}
switch (event.getAction()) {
case MotionEvent.ACTION_HOVER_MOVE:
- if (mAnchor.isEnabled() && mPopup == null) {
- mAnchorX = (int) event.getX();
- mAnchorY = (int) event.getY();
+ if (mAnchor.isEnabled() && mPopup == null && updateAnchorPos(event)) {
setPendingHandler(this);
}
break;
case MotionEvent.ACTION_HOVER_EXIT:
+ clearAnchorPos();
hide();
break;
}
@@ -188,6 +192,7 @@ class TooltipCompatHandler implements View.OnLongClickListener, View.OnHoverList
if (mPopup != null) {
mPopup.hide();
mPopup = null;
+ clearAnchorPos();
mAnchor.removeOnAttachStateChangeListener(this);
} else {
Log.e(TAG, "sActiveHandler.mPopup == null");
@@ -216,4 +221,31 @@ class TooltipCompatHandler implements View.OnLongClickListener, View.OnHoverList
private void cancelPendingShow() {
mAnchor.removeCallbacks(mShowRunnable);
}
+
+ /**
+ * Update the anchor position if it significantly (that is by at least mHoverSlope)
+ * 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;
+ }
}
diff --git a/android/support/wear/widget/BoxInsetLayout.java b/android/support/wear/widget/BoxInsetLayout.java
index a8b13814..383bcb7d 100644
--- a/android/support/wear/widget/BoxInsetLayout.java
+++ b/android/support/wear/widget/BoxInsetLayout.java
@@ -46,7 +46,7 @@ import java.lang.annotation.RetentionPolicy;
@UiThread
public class BoxInsetLayout extends ViewGroup {
- private static final float FACTOR = 0.146467f; //(1 - sqrt(2)/2)/2
+ private static final float FACTOR = 0.146447f; //(1 - sqrt(2)/2)/2
private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START;
private final int mScreenHeight;
diff --git a/android/system/Os.java b/android/system/Os.java
index cc24cc5e..a4b90e33 100644
--- a/android/system/Os.java
+++ b/android/system/Os.java
@@ -280,12 +280,7 @@ public final class Os {
/** @hide */ public static int ioctlInt(FileDescriptor fd, int cmd, Int32Ref arg) throws ErrnoException {
- libcore.util.MutableInt internalArg = new libcore.util.MutableInt(arg.value);
- try {
- return Libcore.os.ioctlInt(fd, cmd, internalArg);
- } finally {
- arg.value = internalArg.value;
- }
+ return Libcore.os.ioctlInt(fd, cmd, arg);
}
/**
@@ -472,18 +467,8 @@ public final class Os {
/**
* See <a href="http://man7.org/linux/man-pages/man2/sendfile.2.html">sendfile(2)</a>.
*/
- public static long sendfile(FileDescriptor outFd, FileDescriptor inFd, Int64Ref inOffset, long byteCount) throws ErrnoException {
- if (inOffset == null) {
- return Libcore.os.sendfile(outFd, inFd, null, byteCount);
- } else {
- libcore.util.MutableLong internalInOffset = new libcore.util.MutableLong(
- inOffset.value);
- try {
- return Libcore.os.sendfile(outFd, inFd, internalInOffset, byteCount);
- } finally {
- inOffset.value = internalInOffset.value;
- }
- }
+ public static long sendfile(FileDescriptor outFd, FileDescriptor inFd, Int64Ref offset, long byteCount) throws ErrnoException {
+ return Libcore.os.sendfile(outFd, inFd, offset, byteCount);
}
/**
@@ -587,6 +572,12 @@ public final class Os {
public static void socketpair(int domain, int type, int protocol, FileDescriptor fd1, FileDescriptor fd2) throws ErrnoException { Libcore.os.socketpair(domain, type, protocol, fd1, fd2); }
/**
+ * See <a href="http://man7.org/linux/man-pages/man2/splice.2.html">splice(2)</a>.
+ * @hide
+ */
+ public static long splice(FileDescriptor fdIn, Int64Ref offIn, FileDescriptor fdOut, Int64Ref offOut, long len, int flags) throws ErrnoException { return Libcore.os.splice(fdIn, offIn, fdOut, offOut, len, flags); }
+
+ /**
* See <a href="http://man7.org/linux/man-pages/man2/stat.2.html">stat(2)</a>.
*/
public static StructStat stat(String path) throws ErrnoException { return Libcore.os.stat(path); }
@@ -652,16 +643,7 @@ public final class Os {
* @throws IllegalArgumentException if {@code status != null && status.length != 1}
*/
public static int waitpid(int pid, Int32Ref status, int options) throws ErrnoException {
- if (status == null) {
- return Libcore.os.waitpid(pid, null, options);
- } else {
- libcore.util.MutableInt internalStatus = new libcore.util.MutableInt(status.value);
- try {
- return Libcore.os.waitpid(pid, internalStatus, options);
- } finally {
- status.value = internalStatus.value;
- }
- }
+ return Libcore.os.waitpid(pid, status, options);
}
/**
diff --git a/android/system/OsConstants.java b/android/system/OsConstants.java
index 83a1b41a..1b8c2fff 100644
--- a/android/system/OsConstants.java
+++ b/android/system/OsConstants.java
@@ -486,6 +486,9 @@ public final class OsConstants {
public static final int SO_SNDLOWAT = placeholder();
public static final int SO_SNDTIMEO = placeholder();
public static final int SO_TYPE = placeholder();
+ /** @hide */ public static final int SPLICE_F_MOVE = placeholder();
+ /** @hide */ public static final int SPLICE_F_NONBLOCK = placeholder();
+ /** @hide */ public static final int SPLICE_F_MORE = placeholder();
public static final int STDERR_FILENO = placeholder();
public static final int STDIN_FILENO = placeholder();
public static final int STDOUT_FILENO = placeholder();
diff --git a/android/telecom/Call.java b/android/telecom/Call.java
index 20911012..67994179 100644
--- a/android/telecom/Call.java
+++ b/android/telecom/Call.java
@@ -419,7 +419,6 @@ public final class Call {
/**
* Indicates the call used Assisted Dialing.
* See also {@link Connection#PROPERTY_ASSISTED_DIALING_USED}
- * @hide
*/
public static final int PROPERTY_ASSISTED_DIALING_USED = 0x00000200;
@@ -1408,7 +1407,7 @@ public final class Call {
* @param extras Bundle containing extra information associated with the event.
*/
public void sendCallEvent(String event, Bundle extras) {
- mInCallAdapter.sendCallEvent(mTelecomCallId, event, extras);
+ mInCallAdapter.sendCallEvent(mTelecomCallId, event, mTargetSdkVersion, extras);
}
/**
@@ -1961,6 +1960,15 @@ public final class Call {
}
}
+ /** {@hide} */
+ final void internalOnHandoverComplete() {
+ for (CallbackRecord<Callback> record : mCallbackRecords) {
+ final Call call = this;
+ final Callback callback = record.getCallback();
+ record.getHandler().post(() -> callback.onHandoverComplete(call));
+ }
+ }
+
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 aaef8d3d..63f970a4 100644
--- a/android/telecom/Connection.java
+++ b/android/telecom/Connection.java
@@ -35,6 +35,7 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.ArraySet;
@@ -401,7 +402,6 @@ public abstract class Connection extends Conferenceable {
/**
* Set by the framework to indicate that a connection is using assisted dialing.
- * @hide
*/
public static final int PROPERTY_ASSISTED_DIALING_USED = 1 << 9;
@@ -2538,6 +2538,19 @@ public abstract class Connection extends Conferenceable {
}
/**
+ * Adds a parcelable extra to this {@code Connection}.
+ *
+ * @param key The extra key.
+ * @param value The value.
+ * @hide
+ */
+ public final void putExtra(@NonNull String key, @NonNull Parcelable value) {
+ Bundle newExtras = new Bundle();
+ newExtras.putParcelable(key, value);
+ putExtras(newExtras);
+ }
+
+ /**
* Removes extras from this {@code Connection}.
*
* @param keys The keys of the extras to remove.
@@ -2788,6 +2801,15 @@ public abstract class Connection extends Conferenceable {
public void onCallEvent(String event, Bundle extras) {}
/**
+ * Notifies this {@link Connection} that a handover has completed.
+ * <p>
+ * A handover is initiated with {@link android.telecom.Call#handoverTo(PhoneAccountHandle, int,
+ * Bundle)} on the initiating side of the handover, and
+ * {@link TelecomManager#acceptHandover(Uri, int, PhoneAccountHandle)}.
+ */
+ public void onHandoverComplete() {}
+
+ /**
* Notifies this {@link Connection} of a change to the extras made outside the
* {@link ConnectionService}.
* <p>
diff --git a/android/telecom/ConnectionService.java b/android/telecom/ConnectionService.java
index 6af01aee..c1040adc 100644
--- a/android/telecom/ConnectionService.java
+++ b/android/telecom/ConnectionService.java
@@ -140,6 +140,7 @@ public abstract class ConnectionService extends Service {
private static final String SESSION_POST_DIAL_CONT = "CS.oPDC";
private static final String SESSION_PULL_EXTERNAL_CALL = "CS.pEC";
private static final String SESSION_SEND_CALL_EVENT = "CS.sCE";
+ private static final String SESSION_HANDOVER_COMPLETE = "CS.hC";
private static final String SESSION_EXTRAS_CHANGED = "CS.oEC";
private static final String SESSION_START_RTT = "CS.+RTT";
private static final String SESSION_STOP_RTT = "CS.-RTT";
@@ -179,6 +180,7 @@ public abstract class ConnectionService extends Service {
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 final int MSG_HANDOVER_COMPLETE = 33;
private static Connection sNullConnection;
@@ -298,6 +300,19 @@ public abstract class ConnectionService extends Service {
}
@Override
+ public void handoverComplete(String callId, Session.Info sessionInfo) {
+ Log.startSession(sessionInfo, SESSION_HANDOVER_COMPLETE);
+ try {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = callId;
+ args.arg2 = Log.createSubsession();
+ mHandler.obtainMessage(MSG_HANDOVER_COMPLETE, args).sendToTarget();
+ } finally {
+ Log.endSession();
+ }
+ }
+
+ @Override
public void abort(String callId, Session.Info sessionInfo) {
Log.startSession(sessionInfo, SESSION_ABORT);
try {
@@ -1028,6 +1043,19 @@ public abstract class ConnectionService extends Service {
}
break;
}
+ case MSG_HANDOVER_COMPLETE: {
+ SomeArgs args = (SomeArgs) msg.obj;
+ try {
+ Log.continueSession((Session) args.arg2,
+ SESSION_HANDLER + SESSION_HANDOVER_COMPLETE);
+ String callId = (String) args.arg1;
+ notifyHandoverComplete(callId);
+ } finally {
+ args.recycle();
+ Log.endSession();
+ }
+ break;
+ }
case MSG_ON_EXTRAS_CHANGED: {
SomeArgs args = (SomeArgs) msg.obj;
try {
@@ -1445,19 +1473,24 @@ public abstract class ConnectionService extends Service {
final ConnectionRequest request,
boolean isIncoming,
boolean isUnknown) {
+ boolean isLegacyHandover = request.getExtras() != null &&
+ request.getExtras().getBoolean(TelecomManager.EXTRA_IS_HANDOVER, false);
+ boolean isHandover = request.getExtras() != null && request.getExtras().getBoolean(
+ TelecomManager.EXTRA_IS_HANDOVER_CONNECTION, false);
Log.d(this, "createConnection, callManagerAccount: %s, callId: %s, request: %s, " +
- "isIncoming: %b, isUnknown: %b", callManagerAccount, callId, request,
- isIncoming,
- isUnknown);
+ "isIncoming: %b, isUnknown: %b, isLegacyHandover: %b, isHandover: %b",
+ callManagerAccount, callId, request, isIncoming, isUnknown, isLegacyHandover,
+ isHandover);
Connection connection = null;
- if (getApplicationContext().getApplicationInfo().targetSdkVersion >
- Build.VERSION_CODES.O_MR1 && request.getExtras() != null &&
- request.getExtras().getBoolean(TelecomManager.EXTRA_IS_HANDOVER,false)) {
+ if (isHandover) {
+ PhoneAccountHandle fromPhoneAccountHandle = request.getExtras() != null
+ ? (PhoneAccountHandle) request.getExtras().getParcelable(
+ TelecomManager.EXTRA_HANDOVER_FROM_PHONE_ACCOUNT) : null;
if (!isIncoming) {
- connection = onCreateOutgoingHandoverConnection(callManagerAccount, request);
+ connection = onCreateOutgoingHandoverConnection(fromPhoneAccountHandle, request);
} else {
- connection = onCreateIncomingHandoverConnection(callManagerAccount, request);
+ connection = onCreateIncomingHandoverConnection(fromPhoneAccountHandle, request);
}
} else {
connection = isUnknown ? onCreateUnknownConnection(callManagerAccount, request)
@@ -1754,6 +1787,19 @@ public abstract class ConnectionService extends Service {
}
/**
+ * Notifies a {@link Connection} that a handover has completed.
+ *
+ * @param callId The ID of the call which completed handover.
+ */
+ private void notifyHandoverComplete(String callId) {
+ Log.d(this, "notifyHandoverComplete(%s)", callId);
+ Connection connection = findConnectionForAction(callId, "notifyHandoverComplete");
+ if (connection != null) {
+ connection.onHandoverComplete();
+ }
+ }
+
+ /**
* Notifies a {@link Connection} or {@link Conference} of a change to the extras from Telecom.
* <p>
* These extra changes can originate from Telecom itself, or from an {@link InCallService} via
diff --git a/android/telecom/InCallAdapter.java b/android/telecom/InCallAdapter.java
index 4bc2a9b1..658685fe 100644
--- a/android/telecom/InCallAdapter.java
+++ b/android/telecom/InCallAdapter.java
@@ -286,11 +286,12 @@ public final class InCallAdapter {
*
* @param callId The callId to send the event for.
* @param event The event.
+ * @param targetSdkVer Target sdk version of the app calling this api
* @param extras Extras associated with the event.
*/
- public void sendCallEvent(String callId, String event, Bundle extras) {
+ public void sendCallEvent(String callId, String event, int targetSdkVer, Bundle extras) {
try {
- mAdapter.sendCallEvent(callId, event, extras);
+ mAdapter.sendCallEvent(callId, event, targetSdkVer, extras);
} catch (RemoteException ignored) {
}
}
diff --git a/android/telecom/InCallService.java b/android/telecom/InCallService.java
index 74fa62d6..fcf04c9a 100644
--- a/android/telecom/InCallService.java
+++ b/android/telecom/InCallService.java
@@ -81,6 +81,7 @@ public abstract class InCallService extends Service {
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;
+ private static final int MSG_ON_HANDOVER_COMPLETE = 13;
/** Default Handler used to consolidate binder method calls onto a single thread. */
private final Handler mHandler = new Handler(Looper.getMainLooper()) {
@@ -157,6 +158,11 @@ public abstract class InCallService extends Service {
mPhone.internalOnHandoverFailed(callId, error);
break;
}
+ case MSG_ON_HANDOVER_COMPLETE: {
+ String callId = (String) msg.obj;
+ mPhone.internalOnHandoverComplete(callId);
+ break;
+ }
default:
break;
}
@@ -237,6 +243,11 @@ public abstract class InCallService extends Service {
public void onHandoverFailed(String callId, int error) {
mHandler.obtainMessage(MSG_ON_HANDOVER_FAILED, error, 0, callId).sendToTarget();
}
+
+ @Override
+ public void onHandoverComplete(String callId) {
+ mHandler.obtainMessage(MSG_ON_HANDOVER_COMPLETE, callId).sendToTarget();
+ }
}
private Phone.Listener mPhoneListener = new Phone.Listener() {
diff --git a/android/telecom/Log.java b/android/telecom/Log.java
index 3361b5b6..83ca4702 100644
--- a/android/telecom/Log.java
+++ b/android/telecom/Log.java
@@ -340,24 +340,6 @@ public class Log {
return sSessionManager;
}
- private static MessageDigest sMessageDigest;
-
- public static void initMd5Sum() {
- new AsyncTask<Void, Void, Void>() {
- @Override
- public Void doInBackground(Void... args) {
- MessageDigest md;
- try {
- md = MessageDigest.getInstance("SHA-1");
- } catch (NoSuchAlgorithmException e) {
- md = null;
- }
- sMessageDigest = md;
- return null;
- }
- }.execute();
- }
-
public static void setTag(String tag) {
TAG = tag;
DEBUG = isLoggable(android.util.Log.DEBUG);
@@ -425,44 +407,13 @@ public class Log {
/**
* Redact personally identifiable information for production users.
* If we are running in verbose mode, return the original string,
- * and return "****" if we are running on the user build, otherwise
- * return a SHA-1 hash of the input string.
+ * and return "***" otherwise.
*/
public static String pii(Object pii) {
if (pii == null || VERBOSE) {
return String.valueOf(pii);
}
- return "[" + secureHash(String.valueOf(pii).getBytes()) + "]";
- }
-
- private static String secureHash(byte[] input) {
- // Refrain from logging user personal information in user build.
- if (USER_BUILD) {
- return "****";
- }
-
- if (sMessageDigest != null) {
- sMessageDigest.reset();
- sMessageDigest.update(input);
- byte[] result = sMessageDigest.digest();
- return encodeHex(result);
- } else {
- return "Uninitialized SHA1";
- }
- }
-
- private static String encodeHex(byte[] bytes) {
- StringBuffer hex = new StringBuffer(bytes.length * 2);
-
- for (int i = 0; i < bytes.length; i++) {
- int byteIntValue = bytes[i] & 0xff;
- if (byteIntValue < 0x10) {
- hex.append("0");
- }
- hex.append(Integer.toString(byteIntValue, 16));
- }
-
- return hex.toString();
+ return "***";
}
private static String getPrefixFromObject(Object obj) {
diff --git a/android/telecom/Phone.java b/android/telecom/Phone.java
index b5394b9b..99f94f28 100644
--- a/android/telecom/Phone.java
+++ b/android/telecom/Phone.java
@@ -230,6 +230,13 @@ public final class Phone {
}
}
+ final void internalOnHandoverComplete(String callId) {
+ Call call = mCallByTelecomCallId.get(callId);
+ if (call != null) {
+ call.internalOnHandoverComplete();
+ }
+ }
+
/**
* Called to destroy the phone and cleanup any lingering calls.
*/
diff --git a/android/telecom/TelecomManager.java b/android/telecom/TelecomManager.java
index 15355ac7..1fe5db5d 100644
--- a/android/telecom/TelecomManager.java
+++ b/android/telecom/TelecomManager.java
@@ -24,6 +24,7 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
+import android.os.Build;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -110,6 +111,12 @@ public class TelecomManager {
"android.telecom.action.SHOW_RESPOND_VIA_SMS_SETTINGS";
/**
+ * The {@link android.content.Intent} action used to show the assisted dialing settings.
+ */
+ public static final String ACTION_SHOW_ASSISTED_DIALING_SETTINGS =
+ "android.telecom.action.SHOW_ASSISTED_DIALING_SETTINGS";
+
+ /**
* The {@link android.content.Intent} action used to show the settings page used to configure
* {@link PhoneAccount} preferences.
*/
@@ -236,6 +243,15 @@ public class TelecomManager {
"android.telecom.extra.INCOMING_CALL_EXTRAS";
/**
+ * Optional extra for {@link #ACTION_INCOMING_CALL} containing a boolean to indicate that the
+ * call has an externally generated ringer. Used by the HfpClientConnectionService when In Band
+ * Ringtone is enabled to prevent two ringers from being generated.
+ * @hide
+ */
+ public static final String EXTRA_CALL_EXTERNAL_RINGER =
+ "android.telecom.extra.CALL_EXTERNAL_RINGER";
+
+ /**
* Optional extra for {@link android.content.Intent#ACTION_CALL} and
* {@link android.content.Intent#ACTION_DIAL} {@code Intent} containing a {@link Bundle}
* which contains metadata about the call. This {@link Bundle} will be saved into
@@ -369,6 +385,17 @@ public class TelecomManager {
public static final String EXTRA_IS_HANDOVER = "android.telecom.extra.IS_HANDOVER";
/**
+ * When {@code true} indicates that a request to create a new connection is for the purpose of
+ * a handover. Note: This is used with the
+ * {@link android.telecom.Call#handoverTo(PhoneAccountHandle, int, Bundle)} API as part of the
+ * internal communication mechanism with the {@link android.telecom.ConnectionService}. It is
+ * not the same as the legacy {@link #EXTRA_IS_HANDOVER} extra.
+ * @hide
+ */
+ public static final String EXTRA_IS_HANDOVER_CONNECTION =
+ "android.telecom.extra.IS_HANDOVER_CONNECTION";
+
+ /**
* Parcelable extra used with {@link #EXTRA_IS_HANDOVER} to indicate the source
* {@link PhoneAccountHandle} when initiating a handover which {@link ConnectionService}
* the handover is from.
@@ -592,12 +619,17 @@ public class TelecomManager {
/**
* 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 bundle indicated by this extra store information related to the assisted dialing action.
+ */
+ public static final String EXTRA_ASSISTED_DIALING_TRANSFORMATION_INFO =
+ "android.telecom.extra.ASSISTED_DIALING_TRANSFORMATION_INFO";
+
+ /**
* The following 4 constants define how properties such as phone numbers and names are
* displayed to the user.
*/
@@ -653,7 +685,6 @@ public class TelecomManager {
mContext = context;
}
mTelecomServiceOverride = telecomServiceImpl;
- android.telecom.Log.initMd5Sum();
}
/**
@@ -1432,6 +1463,13 @@ public class TelecomManager {
public void addNewIncomingCall(PhoneAccountHandle phoneAccount, Bundle extras) {
try {
if (isServiceConnected()) {
+ if (extras != null && extras.getBoolean(EXTRA_IS_HANDOVER) &&
+ mContext.getApplicationContext().getApplicationInfo().targetSdkVersion >
+ Build.VERSION_CODES.O_MR1) {
+ Log.e("TAG", "addNewIncomingCall failed. Use public api " +
+ "acceptHandover for API > O-MR1");
+ // TODO add "return" after DUO team adds support for new handover API
+ }
getTelecomService().addNewIncomingCall(
phoneAccount, extras == null ? new Bundle() : extras);
}
diff --git a/android/telecom/TransformationInfo.java b/android/telecom/TransformationInfo.java
new file mode 100644
index 00000000..3e848c6f
--- /dev/null
+++ b/android/telecom/TransformationInfo.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.telecom;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+/**
+ * A container class to hold information related to the Assisted Dialing operation. All member
+ * variables must be set when constructing a new instance of this class.
+ */
+public final class TransformationInfo implements Parcelable {
+ private String mOriginalNumber;
+ private String mTransformedNumber;
+ private String mUserHomeCountryCode;
+ private String mUserRoamingCountryCode;
+ private int mTransformedNumberCountryCallingCode;
+
+ public TransformationInfo(String originalNumber,
+ String transformedNumber,
+ String userHomeCountryCode,
+ String userRoamingCountryCode,
+ int transformedNumberCountryCallingCode) {
+ String missing = "";
+ if (originalNumber == null) {
+ missing += " mOriginalNumber";
+ }
+ if (transformedNumber == null) {
+ missing += " mTransformedNumber";
+ }
+ if (userHomeCountryCode == null) {
+ missing += " mUserHomeCountryCode";
+ }
+ if (userRoamingCountryCode == null) {
+ missing += " mUserRoamingCountryCode";
+ }
+
+ if (!missing.isEmpty()) {
+ throw new IllegalStateException("Missing required properties:" + missing);
+ }
+ this.mOriginalNumber = originalNumber;
+ this.mTransformedNumber = transformedNumber;
+ this.mUserHomeCountryCode = userHomeCountryCode;
+ this.mUserRoamingCountryCode = userRoamingCountryCode;
+ this.mTransformedNumberCountryCallingCode = transformedNumberCountryCallingCode;
+ }
+
+ public int describeContents() {
+ return 0;
+ }
+
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(mOriginalNumber);
+ out.writeString(mTransformedNumber);
+ out.writeString(mUserHomeCountryCode);
+ out.writeString(mUserRoamingCountryCode);
+ out.writeInt(mTransformedNumberCountryCallingCode);
+ }
+
+ public static final Parcelable.Creator<TransformationInfo> CREATOR
+ = new Parcelable.Creator<TransformationInfo>() {
+ public TransformationInfo createFromParcel(Parcel in) {
+ return new TransformationInfo(in);
+ }
+
+ public TransformationInfo[] newArray(int size) {
+ return new TransformationInfo[size];
+ }
+ };
+
+ private TransformationInfo(Parcel in) {
+ mOriginalNumber = in.readString();
+ mTransformedNumber = in.readString();
+ mUserHomeCountryCode = in.readString();
+ mUserRoamingCountryCode = in.readString();
+ mTransformedNumberCountryCallingCode = in.readInt();
+ }
+
+ /**
+ * The original number that underwent Assisted Dialing.
+ */
+ public String getOriginalNumber() {
+ return mOriginalNumber;
+ }
+
+ /**
+ * The number after it underwent Assisted Dialing.
+ */
+ public String getTransformedNumber() {
+ return mTransformedNumber;
+ }
+
+ /**
+ * The user's home country code that was used when attempting to transform the number.
+ */
+ public String getUserHomeCountryCode() {
+ return mUserHomeCountryCode;
+ }
+
+ /**
+ * The users's roaming country code that was used when attempting to transform the number.
+ */
+ public String getUserRoamingCountryCode() {
+ return mUserRoamingCountryCode;
+ }
+
+ /**
+ * The country calling code that was used in the transformation.
+ */
+ public int getTransformedNumberCountryCallingCode() {
+ return mTransformedNumberCountryCallingCode;
+ }
+}
diff --git a/android/telephony/RadioNetworkConstants.java b/android/telephony/AccessNetworkConstants.java
index 5f5dd82e..7cd16128 100644
--- a/android/telephony/RadioNetworkConstants.java
+++ b/android/telephony/AccessNetworkConstants.java
@@ -16,24 +16,39 @@
package android.telephony;
+import android.annotation.SystemApi;
+
/**
- * Contains radio access network related constants.
+ * Contains access network related constants.
*/
-public final class RadioNetworkConstants {
+public final class AccessNetworkConstants {
- public static final class RadioAccessNetworks {
+ public static final class AccessNetworkType {
+ public static final int UNKNOWN = 0;
public static final int GERAN = 1;
public static final int UTRAN = 2;
public static final int EUTRAN = 3;
- /** @hide */
public static final int CDMA2000 = 4;
+ public static final int IWLAN = 5;
+ }
+
+ /**
+ * Wireless transportation type
+ * @hide
+ */
+ @SystemApi
+ public static final class TransportType {
+ /** Wireless Wide Area Networks (i.e. Cellular) */
+ public static final int WWAN = 1;
+ /** Wireless Local Area Networks (i.e. Wifi) */
+ public static final int WLAN = 2;
}
/**
* Frenquency bands for GERAN.
* http://www.etsi.org/deliver/etsi_ts/145000_145099/145005/14.00.00_60/ts_145005v140000p.pdf
*/
- public static final class GeranBands {
+ public static final class GeranBand {
public static final int BAND_T380 = 1;
public static final int BAND_T410 = 2;
public static final int BAND_450 = 3;
@@ -54,7 +69,7 @@ public final class RadioNetworkConstants {
* Frenquency bands for UTRAN.
* http://www.etsi.org/deliver/etsi_ts/125100_125199/125104/13.03.00_60/ts_125104v130p.pdf
*/
- public static final class UtranBands {
+ public static final class UtranBand {
public static final int BAND_1 = 1;
public static final int BAND_2 = 2;
public static final int BAND_3 = 3;
@@ -83,7 +98,7 @@ public final class RadioNetworkConstants {
* Frenquency bands for EUTRAN.
* http://www.etsi.org/deliver/etsi_ts/136100_136199/136101/14.03.00_60/ts_136101v140p.pdf
*/
- public static final class EutranBands {
+ public static final class EutranBand {
public static final int BAND_1 = 1;
public static final int BAND_2 = 2;
public static final int BAND_3 = 3;
diff --git a/android/telephony/CarrierConfigManager.java b/android/telephony/CarrierConfigManager.java
index 6a47d050..91d86c69 100644
--- a/android/telephony/CarrierConfigManager.java
+++ b/android/telephony/CarrierConfigManager.java
@@ -39,13 +39,29 @@ public class CarrierConfigManager {
private final static String TAG = "CarrierConfigManager";
/**
+ * Extra included in {@link #ACTION_CARRIER_CONFIG_CHANGED} to indicate the slot index that the
+ * broadcast is for.
+ */
+ public static final String EXTRA_SLOT_INDEX = "android.telephony.extra.SLOT_INDEX";
+
+ /**
+ * Optional extra included in {@link #ACTION_CARRIER_CONFIG_CHANGED} to indicate the
+ * subscription index that the broadcast is for, if a valid one is available.
+ */
+ public static final String EXTRA_SUBSCRIPTION_INDEX =
+ SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX;
+
+ /**
* @hide
*/
public CarrierConfigManager() {
}
/**
- * This intent is broadcast by the system when carrier config changes.
+ * This intent is broadcast by the system when carrier config changes. An int is specified in
+ * {@link #EXTRA_SLOT_INDEX} to indicate the slot index that this is for. An optional int extra
+ * {@link #EXTRA_SUBSCRIPTION_INDEX} is included to indicate the subscription index if a valid
+ * one is available for the slot index.
*/
public static final String
ACTION_CARRIER_CONFIG_CHANGED = "android.telephony.action.CARRIER_CONFIG_CHANGED";
@@ -275,7 +291,6 @@ public class CarrierConfigManager {
*
* @see SubscriptionManager#getSubscriptionPlans(int)
* @see SubscriptionManager#setSubscriptionPlans(int, java.util.List)
- * @hide
*/
@SystemApi
public static final String KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING =
@@ -337,6 +352,19 @@ public class CarrierConfigManager {
"notify_handover_video_from_wifi_to_lte_bool";
/**
+ * Flag specifying whether the carrier wants to notify the user when a VT call has been handed
+ * over from LTE to WIFI.
+ * <p>
+ * The handover notification is sent as a
+ * {@link TelephonyManager#EVENT_HANDOVER_VIDEO_FROM_LTE_TO_WIFI}
+ * {@link android.telecom.Connection} event, which an {@link android.telecom.InCallService}
+ * should use to trigger the display of a user-facing message.
+ * @hide
+ */
+ public static final String KEY_NOTIFY_HANDOVER_VIDEO_FROM_LTE_TO_WIFI_BOOL =
+ "notify_handover_video_from_lte_to_wifi_bool";
+
+ /**
* Flag specifying whether the carrier supports downgrading a video call (tx, rx or tx/rx)
* directly to an audio call.
* @hide
@@ -947,8 +975,9 @@ public class CarrierConfigManager {
public static final String KEY_CARRIER_NAME_OVERRIDE_BOOL = "carrier_name_override_bool";
/**
- * String to identify carrier name in CarrierConfig app. This string is used only if
- * #KEY_CARRIER_NAME_OVERRIDE_BOOL is true
+ * String to identify carrier name in CarrierConfig app. This string overrides SPN if
+ * #KEY_CARRIER_NAME_OVERRIDE_BOOL is true; otherwise, it will be used if its value is provided
+ * and SPN is unavailable
* @hide
*/
public static final String KEY_CARRIER_NAME_STRING = "carrier_name_string";
@@ -1003,6 +1032,13 @@ public class CarrierConfigManager {
public static final String KEY_ALWAYS_SHOW_DATA_RAT_ICON_BOOL =
"always_show_data_rat_icon_bool";
+ /**
+ * Boolean to decide whether to show precise call failed cause to user
+ * @hide
+ */
+ public static final String KEY_SHOW_PRECISE_FAILED_CAUSE_BOOL =
+ "show_precise_failed_cause_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.
public static final String KEY_MMS_ALIAS_ENABLED_BOOL = "aliasEnabled";
@@ -1623,6 +1659,13 @@ public class CarrierConfigManager {
"roaming_operator_string_array";
/**
+ * Controls whether Assisted Dialing is enabled and the preference is shown. This feature
+ * transforms numbers when the user is roaming.
+ */
+ public static final String KEY_ASSISTED_DIALING_ENABLED_BOOL =
+ "assisted_dialing_enabled_bool";
+
+ /**
* URL from which the proto containing the public key of the Carrier used for
* IMSI encryption will be downloaded.
* @hide
@@ -1724,6 +1767,22 @@ public class CarrierConfigManager {
*/
public static final String KEY_CARRIER_CONFIG_APPLIED_BOOL = "carrier_config_applied_bool";
+ /**
+ * Determines whether we should show a warning asking the user to check with their carrier
+ * on pricing when the user enabled data roaming.
+ * default to false.
+ * @hide
+ */
+ public static final String KEY_CHECK_PRICING_WITH_CARRIER_FOR_DATA_ROAMING_BOOL =
+ "check_pricing_with_carrier_data_roaming_bool";
+
+ /**
+ * List of thresholds of RSRP for determining the display level of LTE signal bar.
+ * @hide
+ */
+ public static final String KEY_LTE_RSRP_THRESHOLDS_INT_ARRAY =
+ "lte_rsrp_thresholds_int_array";
+
/** The default value for every variable. */
private final static PersistableBundle sDefaults;
@@ -1740,6 +1799,7 @@ public class CarrierConfigManager {
sDefaults.putBoolean(KEY_CARRIER_VOLTE_AVAILABLE_BOOL, false);
sDefaults.putBoolean(KEY_CARRIER_VT_AVAILABLE_BOOL, false);
sDefaults.putBoolean(KEY_NOTIFY_HANDOVER_VIDEO_FROM_WIFI_TO_LTE_BOOL, false);
+ sDefaults.putBoolean(KEY_NOTIFY_HANDOVER_VIDEO_FROM_LTE_TO_WIFI_BOOL, false);
sDefaults.putBoolean(KEY_SUPPORT_DOWNGRADE_VT_TO_AUDIO_BOOL, true);
sDefaults.putString(KEY_DEFAULT_VM_NUMBER_STRING, "");
sDefaults.putBoolean(KEY_CONFIG_TELEPHONY_USE_OWN_NUMBER_FOR_VOICEMAIL_BOOL, false);
@@ -2002,14 +2062,26 @@ public class CarrierConfigManager {
false);
sDefaults.putStringArray(KEY_NON_ROAMING_OPERATOR_STRING_ARRAY, null);
sDefaults.putStringArray(KEY_ROAMING_OPERATOR_STRING_ARRAY, null);
+ sDefaults.putBoolean(KEY_ASSISTED_DIALING_ENABLED_BOOL, true);
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_SHOW_PRECISE_FAILED_CAUSE_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);
+ sDefaults.putBoolean(KEY_CHECK_PRICING_WITH_CARRIER_FOR_DATA_ROAMING_BOOL, false);
+ sDefaults.putIntArray(KEY_LTE_RSRP_THRESHOLDS_INT_ARRAY,
+ new int[] {
+ -140, /* SIGNAL_STRENGTH_NONE_OR_UNKNOWN */
+ -128, /* SIGNAL_STRENGTH_POOR */
+ -118, /* SIGNAL_STRENGTH_MODERATE */
+ -108, /* SIGNAL_STRENGTH_GOOD */
+ -98, /* SIGNAL_STRENGTH_GREAT */
+ -44
+ });
}
/**
diff --git a/android/telephony/CellIdentity.java b/android/telephony/CellIdentity.java
new file mode 100644
index 00000000..e092d52d
--- /dev/null
+++ b/android/telephony/CellIdentity.java
@@ -0,0 +1,173 @@
+/*
+ * 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;
+
+import android.annotation.CallSuper;
+import android.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * CellIdentity represents the identity of a unique cell. This is the base class for
+ * CellIdentityXxx which represents cell identity for specific network access technology.
+ */
+public abstract class CellIdentity implements Parcelable {
+ /**
+ * Cell identity type
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "TYPE_", value = {TYPE_GSM, TYPE_CDMA, TYPE_LTE, TYPE_WCDMA, TYPE_TDSCDMA})
+ public @interface Type {}
+
+ /**
+ * Unknown cell identity type
+ * @hide
+ */
+ public static final int TYPE_UNKNOWN = 0;
+ /**
+ * GSM cell identity type
+ * @hide
+ */
+ public static final int TYPE_GSM = 1;
+ /**
+ * CDMA cell identity type
+ * @hide
+ */
+ public static final int TYPE_CDMA = 2;
+ /**
+ * LTE cell identity type
+ * @hide
+ */
+ public static final int TYPE_LTE = 3;
+ /**
+ * WCDMA cell identity type
+ * @hide
+ */
+ public static final int TYPE_WCDMA = 4;
+ /**
+ * TDS-CDMA cell identity type
+ * @hide
+ */
+ public static final int TYPE_TDSCDMA = 5;
+
+ // Log tag
+ /** @hide */
+ protected final String mTag;
+ // Cell identity type
+ /** @hide */
+ protected final int mType;
+ // 3-digit Mobile Country Code in string format. Null for CDMA cell identity.
+ /** @hide */
+ protected final String mMccStr;
+ // 2 or 3-digit Mobile Network Code in string format. Null for CDMA cell identity.
+ /** @hide */
+ protected final String mMncStr;
+
+ /** @hide */
+ protected CellIdentity(String tag, int type, String mcc, String mnc) {
+ mTag = tag;
+ mType = type;
+
+ // Only allow INT_MAX if unknown string mcc/mnc
+ if (mcc == null || mcc.matches("^[0-9]{3}$")) {
+ mMccStr = mcc;
+ } else if (mcc.isEmpty() || mcc.equals(String.valueOf(Integer.MAX_VALUE))) {
+ // If the mccStr is empty or unknown, set it as null.
+ mMccStr = null;
+ } else {
+ // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MCC format
+ // after the bug got fixed.
+ mMccStr = null;
+ log("invalid MCC format: " + mcc);
+ }
+
+ if (mnc == null || mnc.matches("^[0-9]{2,3}$")) {
+ mMncStr = mnc;
+ } else if (mnc.isEmpty() || mnc.equals(String.valueOf(Integer.MAX_VALUE))) {
+ // If the mncStr is empty or unknown, set it as null.
+ mMncStr = null;
+ } else {
+ // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MNC format
+ // after the bug got fixed.
+ mMncStr = null;
+ log("invalid MNC format: " + mnc);
+ }
+ }
+
+ /** Implement the Parcelable interface */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * @hide
+ * @return The type of the cell identity
+ */
+ public @Type int getType() { return mType; }
+
+ /**
+ * Used by child classes for parceling.
+ *
+ * @hide
+ */
+ @CallSuper
+ public void writeToParcel(Parcel dest, int type) {
+ dest.writeInt(type);
+ dest.writeString(mMccStr);
+ dest.writeString(mMncStr);
+ }
+
+ /**
+ * Construct from Parcel
+ * @hide
+ */
+ protected CellIdentity(String tag, int type, Parcel source) {
+ this(tag, type, source.readString(), source.readString());
+ }
+
+ /** Implement the Parcelable interface */
+ public static final Creator<CellIdentity> CREATOR =
+ new Creator<CellIdentity>() {
+ @Override
+ public CellIdentity createFromParcel(Parcel in) {
+ int type = in.readInt();
+ switch (type) {
+ case TYPE_GSM: return CellIdentityGsm.createFromParcelBody(in);
+ case TYPE_WCDMA: return CellIdentityWcdma.createFromParcelBody(in);
+ case TYPE_CDMA: return CellIdentityCdma.createFromParcelBody(in);
+ case TYPE_LTE: return CellIdentityLte.createFromParcelBody(in);
+ case TYPE_TDSCDMA: return CellIdentityTdscdma.createFromParcelBody(in);
+ default: throw new IllegalArgumentException("Bad Cell identity Parcel");
+ }
+ }
+
+ @Override
+ public CellIdentity[] newArray(int size) {
+ return new CellIdentity[size];
+ }
+ };
+
+ /** @hide */
+ protected void log(String s) {
+ Rlog.w(mTag, s);
+ }
+} \ No newline at end of file
diff --git a/android/telephony/CellIdentityCdma.java b/android/telephony/CellIdentityCdma.java
index ddc938e6..2e1d1dc3 100644
--- a/android/telephony/CellIdentityCdma.java
+++ b/android/telephony/CellIdentityCdma.java
@@ -17,8 +17,6 @@
package android.telephony;
import android.os.Parcel;
-import android.os.Parcelable;
-import android.telephony.Rlog;
import android.text.TextUtils;
import java.util.Objects;
@@ -26,9 +24,8 @@ import java.util.Objects;
/**
* CellIdentity is to represent a unique CDMA cell
*/
-public final class CellIdentityCdma implements Parcelable {
-
- private static final String LOG_TAG = "CellSignalStrengthCdma";
+public final class CellIdentityCdma extends CellIdentity {
+ private static final String TAG = CellIdentityCdma.class.getSimpleName();
private static final boolean DBG = false;
// Network Id 0..65535
@@ -60,6 +57,7 @@ public final class CellIdentityCdma implements Parcelable {
* @hide
*/
public CellIdentityCdma() {
+ super(TAG, TYPE_CDMA, null, null);
mNetworkId = Integer.MAX_VALUE;
mSystemId = Integer.MAX_VALUE;
mBasestationId = Integer.MAX_VALUE;
@@ -81,7 +79,7 @@ public final class CellIdentityCdma implements Parcelable {
*
* @hide
*/
- public CellIdentityCdma (int nid, int sid, int bid, int lon, int lat) {
+ public CellIdentityCdma(int nid, int sid, int bid, int lon, int lat) {
this(nid, sid, bid, lon, lat, null, null);
}
@@ -99,8 +97,9 @@ public final class CellIdentityCdma implements Parcelable {
*
* @hide
*/
- public CellIdentityCdma (int nid, int sid, int bid, int lon, int lat, String alphal,
+ public CellIdentityCdma(int nid, int sid, int bid, int lon, int lat, String alphal,
String alphas) {
+ super(TAG, TYPE_CDMA, null, null);
mNetworkId = nid;
mSystemId = sid;
mBasestationId = bid;
@@ -196,40 +195,33 @@ public final class CellIdentityCdma implements Parcelable {
CellIdentityCdma o = (CellIdentityCdma) other;
- return mNetworkId == o.mNetworkId &&
- mSystemId == o.mSystemId &&
- mBasestationId == o.mBasestationId &&
- mLatitude == o.mLatitude &&
- mLongitude == o.mLongitude &&
- TextUtils.equals(mAlphaLong, o.mAlphaLong) &&
- TextUtils.equals(mAlphaShort, o.mAlphaShort);
+ return mNetworkId == o.mNetworkId
+ && mSystemId == o.mSystemId
+ && mBasestationId == o.mBasestationId
+ && mLatitude == o.mLatitude
+ && mLongitude == o.mLongitude
+ && TextUtils.equals(mAlphaLong, o.mAlphaLong)
+ && TextUtils.equals(mAlphaShort, o.mAlphaShort);
}
@Override
public String toString() {
- StringBuilder sb = new StringBuilder("CellIdentityCdma:{");
- sb.append(" mNetworkId="); sb.append(mNetworkId);
- sb.append(" mSystemId="); sb.append(mSystemId);
- sb.append(" mBasestationId="); sb.append(mBasestationId);
- sb.append(" mLongitude="); sb.append(mLongitude);
- sb.append(" mLatitude="); sb.append(mLatitude);
- sb.append(" mAlphaLong="); sb.append(mAlphaLong);
- sb.append(" mAlphaShort="); sb.append(mAlphaShort);
- sb.append("}");
-
- return sb.toString();
- }
-
- /** Implement the Parcelable interface */
- @Override
- public int describeContents() {
- return 0;
+ return new StringBuilder(TAG)
+ .append(":{ mNetworkId=").append(mNetworkId)
+ .append(" mSystemId=").append(mSystemId)
+ .append(" mBasestationId=").append(mBasestationId)
+ .append(" mLongitude=").append(mLongitude)
+ .append(" mLatitude=").append(mLatitude)
+ .append(" mAlphaLong=").append(mAlphaLong)
+ .append(" mAlphaShort=").append(mAlphaShort)
+ .append("}").toString();
}
/** Implement the Parcelable interface */
@Override
public void writeToParcel(Parcel dest, int flags) {
if (DBG) log("writeToParcel(Parcel, int): " + toString());
+ super.writeToParcel(dest, TYPE_CDMA);
dest.writeInt(mNetworkId);
dest.writeInt(mSystemId);
dest.writeInt(mBasestationId);
@@ -241,10 +233,16 @@ public final class CellIdentityCdma implements Parcelable {
/** Construct from Parcel, type has already been processed */
private CellIdentityCdma(Parcel in) {
- this(in.readInt(), in.readInt(), in.readInt(), in.readInt(), in.readInt(),
- in.readString(), in.readString());
-
- if (DBG) log("CellIdentityCdma(Parcel): " + toString());
+ super(TAG, TYPE_CDMA, in);
+ mNetworkId = in.readInt();
+ mSystemId = in.readInt();
+ mBasestationId = in.readInt();
+ mLongitude = in.readInt();
+ mLatitude = in.readInt();
+ mAlphaLong = in.readString();
+ mAlphaShort = in.readString();
+
+ if (DBG) log(toString());
}
/** Implement the Parcelable interface */
@@ -253,7 +251,8 @@ public final class CellIdentityCdma implements Parcelable {
new Creator<CellIdentityCdma>() {
@Override
public CellIdentityCdma createFromParcel(Parcel in) {
- return new CellIdentityCdma(in);
+ in.readInt(); // skip
+ return createFromParcelBody(in);
}
@Override
@@ -262,10 +261,8 @@ public final class CellIdentityCdma implements Parcelable {
}
};
- /**
- * log
- */
- private static void log(String s) {
- Rlog.w(LOG_TAG, s);
+ /** @hide */
+ protected static CellIdentityCdma createFromParcelBody(Parcel in) {
+ return new CellIdentityCdma(in);
}
}
diff --git a/android/telephony/CellIdentityGsm.java b/android/telephony/CellIdentityGsm.java
index 376e6aa7..f948f812 100644
--- a/android/telephony/CellIdentityGsm.java
+++ b/android/telephony/CellIdentityGsm.java
@@ -17,8 +17,6 @@
package android.telephony;
import android.os.Parcel;
-import android.os.Parcelable;
-import android.telephony.Rlog;
import android.text.TextUtils;
import java.util.Objects;
@@ -26,9 +24,8 @@ import java.util.Objects;
/**
* CellIdentity to represent a unique GSM cell
*/
-public final class CellIdentityGsm implements Parcelable {
-
- private static final String LOG_TAG = "CellIdentityGsm";
+public final class CellIdentityGsm extends CellIdentity {
+ private static final String TAG = CellIdentityGsm.class.getSimpleName();
private static final boolean DBG = false;
// 16-bit Location Area Code, 0..65535
@@ -39,10 +36,6 @@ public final class CellIdentityGsm implements Parcelable {
private final int mArfcn;
// 6-bit Base Station Identity Code
private final int mBsic;
- // 3-digit Mobile Country Code in string format
- private final String mMccStr;
- // 2 or 3-digit Mobile Network Code in string format
- private final String mMncStr;
// long alpha Operator Name String or Enhanced Operator Name String
private final String mAlphaLong;
// short alpha Operator Name String or Enhanced Operator Name String
@@ -52,12 +45,11 @@ public final class CellIdentityGsm implements Parcelable {
* @hide
*/
public CellIdentityGsm() {
+ super(TAG, TYPE_GSM, null, null);
mLac = Integer.MAX_VALUE;
mCid = Integer.MAX_VALUE;
mArfcn = Integer.MAX_VALUE;
mBsic = Integer.MAX_VALUE;
- mMccStr = null;
- mMncStr = null;
mAlphaLong = null;
mAlphaShort = null;
}
@@ -70,7 +62,7 @@ public final class CellIdentityGsm implements Parcelable {
*
* @hide
*/
- public CellIdentityGsm (int mcc, int mnc, int lac, int cid) {
+ public CellIdentityGsm(int mcc, int mnc, int lac, int cid) {
this(lac, cid, Integer.MAX_VALUE, Integer.MAX_VALUE,
String.valueOf(mcc), String.valueOf(mnc), null, null);
}
@@ -86,7 +78,7 @@ public final class CellIdentityGsm implements Parcelable {
*
* @hide
*/
- public CellIdentityGsm (int mcc, int mnc, int lac, int cid, int arfcn, int bsic) {
+ public CellIdentityGsm(int mcc, int mnc, int lac, int cid, int arfcn, int bsic) {
this(lac, cid, arfcn, bsic, String.valueOf(mcc), String.valueOf(mnc), null, null);
}
@@ -103,8 +95,9 @@ public final class CellIdentityGsm implements Parcelable {
*
* @hide
*/
- public CellIdentityGsm (int lac, int cid, int arfcn, int bsic, String mccStr,
+ public CellIdentityGsm(int lac, int cid, int arfcn, int bsic, String mccStr,
String mncStr, String alphal, String alphas) {
+ super(TAG, TYPE_GSM, mccStr, mncStr);
mLac = lac;
mCid = cid;
mArfcn = arfcn;
@@ -112,31 +105,6 @@ public final class CellIdentityGsm implements Parcelable {
// for inbound parcels
mBsic = (bsic == 0xFF) ? Integer.MAX_VALUE : bsic;
- // Only allow INT_MAX if unknown string mcc/mnc
- if (mccStr == null || mccStr.matches("^[0-9]{3}$")) {
- mMccStr = mccStr;
- } else if (mccStr.isEmpty() || mccStr.equals(String.valueOf(Integer.MAX_VALUE))) {
- // If the mccStr is empty or unknown, set it as null.
- mMccStr = null;
- } else {
- // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MCC format
- // after the bug got fixed.
- mMccStr = null;
- log("invalid MCC format: " + mccStr);
- }
-
- if (mncStr == null || mncStr.matches("^[0-9]{2,3}$")) {
- mMncStr = mncStr;
- } else if (mncStr.isEmpty() || mncStr.equals(String.valueOf(Integer.MAX_VALUE))) {
- // If the mncStr is empty or unknown, set it as null.
- mMncStr = null;
- } else {
- // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MNC format
- // after the bug got fixed.
- mMncStr = null;
- log("invalid MNC format: " + mncStr);
- }
-
mAlphaLong = alphal;
mAlphaShort = alphas;
}
@@ -237,6 +205,7 @@ public final class CellIdentityGsm implements Parcelable {
/**
+ * @deprecated Primary Scrambling Code is not applicable to GSM.
* @return Integer.MAX_VALUE, undefined for GSM
*/
@Deprecated
@@ -260,58 +229,54 @@ public final class CellIdentityGsm implements Parcelable {
}
CellIdentityGsm o = (CellIdentityGsm) other;
- return mLac == o.mLac &&
- mCid == o.mCid &&
- mArfcn == o.mArfcn &&
- mBsic == o.mBsic &&
- TextUtils.equals(mMccStr, o.mMccStr) &&
- TextUtils.equals(mMncStr, o.mMncStr) &&
- TextUtils.equals(mAlphaLong, o.mAlphaLong) &&
- TextUtils.equals(mAlphaShort, o.mAlphaShort);
+ return mLac == o.mLac
+ && mCid == o.mCid
+ && mArfcn == o.mArfcn
+ && mBsic == o.mBsic
+ && TextUtils.equals(mMccStr, o.mMccStr)
+ && TextUtils.equals(mMncStr, o.mMncStr)
+ && TextUtils.equals(mAlphaLong, o.mAlphaLong)
+ && TextUtils.equals(mAlphaShort, o.mAlphaShort);
}
@Override
public String toString() {
- StringBuilder sb = new StringBuilder("CellIdentityGsm:{");
- sb.append(" mLac=").append(mLac);
- sb.append(" mCid=").append(mCid);
- sb.append(" mArfcn=").append(mArfcn);
- sb.append(" mBsic=").append("0x").append(Integer.toHexString(mBsic));
- sb.append(" mMcc=").append(mMccStr);
- sb.append(" mMnc=").append(mMncStr);
- sb.append(" mAlphaLong=").append(mAlphaLong);
- sb.append(" mAlphaShort=").append(mAlphaShort);
- sb.append("}");
-
- return sb.toString();
- }
-
- /** Implement the Parcelable interface */
- @Override
- public int describeContents() {
- return 0;
+ return new StringBuilder(TAG)
+ .append(":{ mLac=").append(mLac)
+ .append(" mCid=").append(mCid)
+ .append(" mArfcn=").append(mArfcn)
+ .append(" mBsic=").append("0x").append(Integer.toHexString(mBsic))
+ .append(" mMcc=").append(mMccStr)
+ .append(" mMnc=").append(mMncStr)
+ .append(" mAlphaLong=").append(mAlphaLong)
+ .append(" mAlphaShort=").append(mAlphaShort)
+ .append("}").toString();
}
/** Implement the Parcelable interface */
@Override
public void writeToParcel(Parcel dest, int flags) {
if (DBG) log("writeToParcel(Parcel, int): " + toString());
+ super.writeToParcel(dest, TYPE_GSM);
dest.writeInt(mLac);
dest.writeInt(mCid);
dest.writeInt(mArfcn);
dest.writeInt(mBsic);
- dest.writeString(mMccStr);
- dest.writeString(mMncStr);
dest.writeString(mAlphaLong);
dest.writeString(mAlphaShort);
}
/** Construct from Parcel, type has already been processed */
private CellIdentityGsm(Parcel in) {
- this(in.readInt(), in.readInt(), in.readInt(), in.readInt(), in.readString(),
- in.readString(), in.readString(), in.readString());
-
- if (DBG) log("CellIdentityGsm(Parcel): " + toString());
+ super(TAG, TYPE_GSM, in);
+ mLac = in.readInt();
+ mCid = in.readInt();
+ mArfcn = in.readInt();
+ mBsic = in.readInt();
+ mAlphaLong = in.readString();
+ mAlphaShort = in.readString();
+
+ if (DBG) log(toString());
}
/** Implement the Parcelable interface */
@@ -320,7 +285,8 @@ public final class CellIdentityGsm implements Parcelable {
new Creator<CellIdentityGsm>() {
@Override
public CellIdentityGsm createFromParcel(Parcel in) {
- return new CellIdentityGsm(in);
+ in.readInt(); // skip
+ return createFromParcelBody(in);
}
@Override
@@ -329,10 +295,8 @@ public final class CellIdentityGsm implements Parcelable {
}
};
- /**
- * log
- */
- private static void log(String s) {
- Rlog.w(LOG_TAG, s);
+ /** @hide */
+ protected static CellIdentityGsm createFromParcelBody(Parcel in) {
+ return new CellIdentityGsm(in);
}
-} \ No newline at end of file
+}
diff --git a/android/telephony/CellIdentityLte.java b/android/telephony/CellIdentityLte.java
index 6ca5daf6..7f20c8ae 100644
--- a/android/telephony/CellIdentityLte.java
+++ b/android/telephony/CellIdentityLte.java
@@ -17,8 +17,6 @@
package android.telephony;
import android.os.Parcel;
-import android.os.Parcelable;
-import android.telephony.Rlog;
import android.text.TextUtils;
import java.util.Objects;
@@ -26,9 +24,8 @@ import java.util.Objects;
/**
* CellIdentity is to represent a unique LTE cell
*/
-public final class CellIdentityLte implements Parcelable {
-
- private static final String LOG_TAG = "CellIdentityLte";
+public final class CellIdentityLte extends CellIdentity {
+ private static final String TAG = CellIdentityLte.class.getSimpleName();
private static final boolean DBG = false;
// 28-bit cell identity
@@ -39,10 +36,6 @@ public final class CellIdentityLte implements Parcelable {
private final int mTac;
// 18-bit Absolute RF Channel Number
private final int mEarfcn;
- // 3-digit Mobile Country Code in string format
- private final String mMccStr;
- // 2 or 3-digit Mobile Network Code in string format
- private final String mMncStr;
// long alpha Operator Name String or Enhanced Operator Name String
private final String mAlphaLong;
// short alpha Operator Name String or Enhanced Operator Name String
@@ -52,12 +45,11 @@ public final class CellIdentityLte implements Parcelable {
* @hide
*/
public CellIdentityLte() {
+ super(TAG, TYPE_LTE, null, null);
mCi = Integer.MAX_VALUE;
mPci = Integer.MAX_VALUE;
mTac = Integer.MAX_VALUE;
mEarfcn = Integer.MAX_VALUE;
- mMccStr = null;
- mMncStr = null;
mAlphaLong = null;
mAlphaShort = null;
}
@@ -72,7 +64,7 @@ public final class CellIdentityLte implements Parcelable {
*
* @hide
*/
- public CellIdentityLte (int mcc, int mnc, int ci, int pci, int tac) {
+ public CellIdentityLte(int mcc, int mnc, int ci, int pci, int tac) {
this(ci, pci, tac, Integer.MAX_VALUE, String.valueOf(mcc), String.valueOf(mnc), null, null);
}
@@ -87,7 +79,7 @@ public final class CellIdentityLte implements Parcelable {
*
* @hide
*/
- public CellIdentityLte (int mcc, int mnc, int ci, int pci, int tac, int earfcn) {
+ public CellIdentityLte(int mcc, int mnc, int ci, int pci, int tac, int earfcn) {
this(ci, pci, tac, earfcn, String.valueOf(mcc), String.valueOf(mnc), null, null);
}
@@ -104,38 +96,13 @@ public final class CellIdentityLte implements Parcelable {
*
* @hide
*/
- public CellIdentityLte (int ci, int pci, int tac, int earfcn, String mccStr,
+ public CellIdentityLte(int ci, int pci, int tac, int earfcn, String mccStr,
String mncStr, String alphal, String alphas) {
+ super(TAG, TYPE_LTE, mccStr, mncStr);
mCi = ci;
mPci = pci;
mTac = tac;
mEarfcn = earfcn;
-
- // Only allow INT_MAX if unknown string mcc/mnc
- if (mccStr == null || mccStr.matches("^[0-9]{3}$")) {
- mMccStr = mccStr;
- } else if (mccStr.isEmpty() || mccStr.equals(String.valueOf(Integer.MAX_VALUE))) {
- // If the mccStr is empty or unknown, set it as null.
- mMccStr = null;
- } else {
- // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MCC format
- // after the bug got fixed.
- mMccStr = null;
- log("invalid MCC format: " + mccStr);
- }
-
- if (mncStr == null || mncStr.matches("^[0-9]{2,3}$")) {
- mMncStr = mncStr;
- } else if (mncStr.isEmpty() || mncStr.equals(String.valueOf(Integer.MAX_VALUE))) {
- // If the mncStr is empty or unknown, set it as null.
- mMncStr = null;
- } else {
- // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MNC format
- // after the bug got fixed.
- mMncStr = null;
- log("invalid MNC format: " + mncStr);
- }
-
mAlphaLong = alphal;
mAlphaShort = alphas;
}
@@ -248,58 +215,54 @@ public final class CellIdentityLte implements Parcelable {
}
CellIdentityLte o = (CellIdentityLte) other;
- return mCi == o.mCi &&
- mPci == o.mPci &&
- mTac == o.mTac &&
- mEarfcn == o.mEarfcn &&
- TextUtils.equals(mMccStr, o.mMccStr) &&
- TextUtils.equals(mMncStr, o.mMncStr) &&
- TextUtils.equals(mAlphaLong, o.mAlphaLong) &&
- TextUtils.equals(mAlphaShort, o.mAlphaShort);
+ return mCi == o.mCi
+ && mPci == o.mPci
+ && mTac == o.mTac
+ && mEarfcn == o.mEarfcn
+ && TextUtils.equals(mMccStr, o.mMccStr)
+ && TextUtils.equals(mMncStr, o.mMncStr)
+ && TextUtils.equals(mAlphaLong, o.mAlphaLong)
+ && TextUtils.equals(mAlphaShort, o.mAlphaShort);
}
@Override
public String toString() {
- StringBuilder sb = new StringBuilder("CellIdentityLte:{");
- sb.append(" mCi="); sb.append(mCi);
- sb.append(" mPci="); sb.append(mPci);
- sb.append(" mTac="); sb.append(mTac);
- sb.append(" mEarfcn="); sb.append(mEarfcn);
- sb.append(" mMcc="); sb.append(mMccStr);
- sb.append(" mMnc="); sb.append(mMncStr);
- sb.append(" mAlphaLong="); sb.append(mAlphaLong);
- sb.append(" mAlphaShort="); sb.append(mAlphaShort);
- sb.append("}");
-
- return sb.toString();
- }
-
- /** Implement the Parcelable interface */
- @Override
- public int describeContents() {
- return 0;
+ return new StringBuilder(TAG)
+ .append(":{ mCi=").append(mCi)
+ .append(" mPci=").append(mPci)
+ .append(" mTac=").append(mTac)
+ .append(" mEarfcn=").append(mEarfcn)
+ .append(" mMcc=").append(mMccStr)
+ .append(" mMnc=").append(mMncStr)
+ .append(" mAlphaLong=").append(mAlphaLong)
+ .append(" mAlphaShort=").append(mAlphaShort)
+ .append("}").toString();
}
/** Implement the Parcelable interface */
@Override
public void writeToParcel(Parcel dest, int flags) {
if (DBG) log("writeToParcel(Parcel, int): " + toString());
+ super.writeToParcel(dest, TYPE_LTE);
dest.writeInt(mCi);
dest.writeInt(mPci);
dest.writeInt(mTac);
dest.writeInt(mEarfcn);
- dest.writeString(mMccStr);
- dest.writeString(mMncStr);
dest.writeString(mAlphaLong);
dest.writeString(mAlphaShort);
}
/** Construct from Parcel, type has already been processed */
private CellIdentityLte(Parcel in) {
- this(in.readInt(), in.readInt(), in.readInt(), in.readInt(), in.readString(),
- in.readString(), in.readString(), in.readString());
-
- if (DBG) log("CellIdentityLte(Parcel): " + toString());
+ super(TAG, TYPE_LTE, in);
+ mCi = in.readInt();
+ mPci = in.readInt();
+ mTac = in.readInt();
+ mEarfcn = in.readInt();
+ mAlphaLong = in.readString();
+ mAlphaShort = in.readString();
+
+ if (DBG) log(toString());
}
/** Implement the Parcelable interface */
@@ -308,7 +271,8 @@ public final class CellIdentityLte implements Parcelable {
new Creator<CellIdentityLte>() {
@Override
public CellIdentityLte createFromParcel(Parcel in) {
- return new CellIdentityLte(in);
+ in.readInt(); // skip;
+ return createFromParcelBody(in);
}
@Override
@@ -317,10 +281,8 @@ public final class CellIdentityLte implements Parcelable {
}
};
- /**
- * log
- */
- private static void log(String s) {
- Rlog.w(LOG_TAG, s);
+ /** @hide */
+ protected static CellIdentityLte createFromParcelBody(Parcel in) {
+ return new CellIdentityLte(in);
}
-} \ No newline at end of file
+}
diff --git a/android/telephony/CellIdentityTdscdma.java b/android/telephony/CellIdentityTdscdma.java
new file mode 100644
index 00000000..001d19f7
--- /dev/null
+++ b/android/telephony/CellIdentityTdscdma.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 android.telephony;
+
+import android.os.Parcel;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * CellIdentity is to represent a unique TD-SCDMA cell
+ */
+public final class CellIdentityTdscdma extends CellIdentity {
+ private static final String TAG = CellIdentityTdscdma.class.getSimpleName();
+ private static final boolean DBG = false;
+
+ // 16-bit Location Area Code, 0..65535, INT_MAX if unknown.
+ private final int mLac;
+ // 28-bit UMTS Cell Identity described in TS 25.331, 0..268435455, INT_MAX if unknown.
+ private final int mCid;
+ // 8-bit Cell Parameters ID described in TS 25.331, 0..127, INT_MAX if unknown.
+ private final int mCpid;
+
+ /**
+ * @hide
+ */
+ public CellIdentityTdscdma() {
+ super(TAG, TYPE_TDSCDMA, null, null);
+ mLac = Integer.MAX_VALUE;
+ mCid = Integer.MAX_VALUE;
+ mCpid = Integer.MAX_VALUE;
+ }
+
+ /**
+ * @param mcc 3-digit Mobile Country Code, 0..999
+ * @param mnc 2 or 3-digit Mobile Network Code, 0..999
+ * @param lac 16-bit Location Area Code, 0..65535, INT_MAX if unknown
+ * @param cid 28-bit UMTS Cell Identity described in TS 25.331, 0..268435455, INT_MAX if unknown
+ * @param cpid 8-bit Cell Parameters ID described in TS 25.331, 0..127, INT_MAX if unknown
+ *
+ * @hide
+ */
+ public CellIdentityTdscdma(int mcc, int mnc, int lac, int cid, int cpid) {
+ this(String.valueOf(mcc), String.valueOf(mnc), lac, cid, cpid);
+ }
+
+ /**
+ * @param mcc 3-digit Mobile Country Code in string format
+ * @param mnc 2 or 3-digit Mobile Network Code in string format
+ * @param lac 16-bit Location Area Code, 0..65535, INT_MAX if unknown
+ * @param cid 28-bit UMTS Cell Identity described in TS 25.331, 0..268435455, INT_MAX if unknown
+ * @param cpid 8-bit Cell Parameters ID described in TS 25.331, 0..127, INT_MAX if unknown
+ *
+ * @hide
+ */
+ public CellIdentityTdscdma(String mcc, String mnc, int lac, int cid, int cpid) {
+ super(TAG, TYPE_TDSCDMA, mcc, mnc);
+ mLac = lac;
+ mCid = cid;
+ mCpid = cpid;
+ }
+
+ private CellIdentityTdscdma(CellIdentityTdscdma cid) {
+ this(cid.mMccStr, cid.mMncStr, cid.mLac, cid.mCid, cid.mCpid);
+ }
+
+ CellIdentityTdscdma copy() {
+ return new CellIdentityTdscdma(this);
+ }
+
+ /**
+ * Get Mobile Country Code in string format
+ * @return Mobile Country Code in string format, null if unknown
+ */
+ public String getMccStr() {
+ return mMccStr;
+ }
+
+ /**
+ * Get Mobile Network Code in string format
+ * @return Mobile Network Code in string format, null if unknown
+ */
+ public String getMncStr() {
+ return mMncStr;
+ }
+
+ /**
+ * @return 16-bit Location Area Code, 0..65535, INT_MAX if unknown
+ */
+ public int getLac() {
+ return mLac;
+ }
+
+ /**
+ * @return 28-bit UMTS Cell Identity described in TS 25.331, 0..268435455, INT_MAX if unknown
+ */
+ public int getCid() {
+ return mCid;
+ }
+
+ /**
+ * @return 8-bit Cell Parameters ID described in TS 25.331, 0..127, INT_MAX if unknown
+ */
+ public int getCpid() {
+ return mCpid;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mMccStr, mMncStr, mLac, mCid, mCpid);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) {
+ return true;
+ }
+
+ if (!(other instanceof CellIdentityTdscdma)) {
+ return false;
+ }
+
+ CellIdentityTdscdma o = (CellIdentityTdscdma) other;
+ return TextUtils.equals(mMccStr, o.mMccStr)
+ && TextUtils.equals(mMncStr, o.mMncStr)
+ && mLac == o.mLac
+ && mCid == o.mCid
+ && mCpid == o.mCpid;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder(TAG)
+ .append(":{ mMcc=").append(mMccStr)
+ .append(" mMnc=").append(mMncStr)
+ .append(" mLac=").append(mLac)
+ .append(" mCid=").append(mCid)
+ .append(" mCpid=").append(mCpid)
+ .append("}").toString();
+ }
+
+ /** Implement the Parcelable interface */
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ if (DBG) log("writeToParcel(Parcel, int): " + toString());
+ super.writeToParcel(dest, TYPE_TDSCDMA);
+ dest.writeInt(mLac);
+ dest.writeInt(mCid);
+ dest.writeInt(mCpid);
+ }
+
+ /** Construct from Parcel, type has already been processed */
+ private CellIdentityTdscdma(Parcel in) {
+ super(TAG, TYPE_TDSCDMA, in);
+ mLac = in.readInt();
+ mCid = in.readInt();
+ mCpid = in.readInt();
+
+ if (DBG) log(toString());
+ }
+
+ /** Implement the Parcelable interface */
+ @SuppressWarnings("hiding")
+ public static final Creator<CellIdentityTdscdma> CREATOR =
+ new Creator<CellIdentityTdscdma>() {
+ @Override
+ public CellIdentityTdscdma createFromParcel(Parcel in) {
+ in.readInt(); // skip
+ return createFromParcelBody(in);
+ }
+
+ @Override
+ public CellIdentityTdscdma[] newArray(int size) {
+ return new CellIdentityTdscdma[size];
+ }
+ };
+
+ /** @hide */
+ protected static CellIdentityTdscdma createFromParcelBody(Parcel in) {
+ return new CellIdentityTdscdma(in);
+ }
+}
diff --git a/android/telephony/CellIdentityWcdma.java b/android/telephony/CellIdentityWcdma.java
index e4bb4f29..1aa1715e 100644
--- a/android/telephony/CellIdentityWcdma.java
+++ b/android/telephony/CellIdentityWcdma.java
@@ -17,8 +17,6 @@
package android.telephony;
import android.os.Parcel;
-import android.os.Parcelable;
-import android.telephony.Rlog;
import android.text.TextUtils;
import java.util.Objects;
@@ -26,9 +24,8 @@ import java.util.Objects;
/**
* CellIdentity to represent a unique UMTS cell
*/
-public final class CellIdentityWcdma implements Parcelable {
-
- private static final String LOG_TAG = "CellIdentityWcdma";
+public final class CellIdentityWcdma extends CellIdentity {
+ private static final String TAG = CellIdentityWcdma.class.getSimpleName();
private static final boolean DBG = false;
// 16-bit Location Area Code, 0..65535
@@ -39,10 +36,6 @@ public final class CellIdentityWcdma implements Parcelable {
private final int mPsc;
// 16-bit UMTS Absolute RF Channel Number
private final int mUarfcn;
- // 3-digit Mobile Country Code in string format
- private final String mMccStr;
- // 2 or 3-digit Mobile Network Code in string format
- private final String mMncStr;
// long alpha Operator Name String or Enhanced Operator Name String
private final String mAlphaLong;
// short alpha Operator Name String or Enhanced Operator Name String
@@ -52,12 +45,11 @@ public final class CellIdentityWcdma implements Parcelable {
* @hide
*/
public CellIdentityWcdma() {
+ super(TAG, TYPE_TDSCDMA, null, null);
mLac = Integer.MAX_VALUE;
mCid = Integer.MAX_VALUE;
mPsc = Integer.MAX_VALUE;
mUarfcn = Integer.MAX_VALUE;
- mMccStr = null;
- mMncStr = null;
mAlphaLong = null;
mAlphaShort = null;
}
@@ -106,36 +98,11 @@ public final class CellIdentityWcdma implements Parcelable {
*/
public CellIdentityWcdma (int lac, int cid, int psc, int uarfcn,
String mccStr, String mncStr, String alphal, String alphas) {
+ super(TAG, TYPE_WCDMA, mccStr, mncStr);
mLac = lac;
mCid = cid;
mPsc = psc;
mUarfcn = uarfcn;
-
- // Only allow INT_MAX if unknown string mcc/mnc
- if (mccStr == null || mccStr.matches("^[0-9]{3}$")) {
- mMccStr = mccStr;
- } else if (mccStr.isEmpty() || mccStr.equals(String.valueOf(Integer.MAX_VALUE))) {
- // If the mccStr is empty or unknown, set it as null.
- mMccStr = null;
- } else {
- // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MCC format
- // after the bug got fixed.
- mMccStr = null;
- log("invalid MCC format: " + mccStr);
- }
-
- if (mncStr == null || mncStr.matches("^[0-9]{2,3}$")) {
- mMncStr = mncStr;
- } else if (mncStr.isEmpty() || mncStr.equals(String.valueOf(Integer.MAX_VALUE))) {
- // If the mncStr is empty or unknown, set it as null.
- mMncStr = null;
- } else {
- // TODO: b/69384059 Should throw IllegalArgumentException for the invalid MNC format
- // after the bug got fixed.
- mMncStr = null;
- log("invalid MNC format: " + mncStr);
- }
-
mAlphaLong = alphal;
mAlphaShort = alphas;
}
@@ -250,58 +217,53 @@ public final class CellIdentityWcdma implements Parcelable {
}
CellIdentityWcdma o = (CellIdentityWcdma) other;
- return mLac == o.mLac &&
- mCid == o.mCid &&
- mPsc == o.mPsc &&
- mUarfcn == o.mUarfcn &&
- TextUtils.equals(mMccStr, o.mMccStr) &&
- TextUtils.equals(mMncStr, o.mMncStr) &&
- TextUtils.equals(mAlphaLong, o.mAlphaLong) &&
- TextUtils.equals(mAlphaShort, o.mAlphaShort);
+ return mLac == o.mLac
+ && mCid == o.mCid
+ && mPsc == o.mPsc
+ && mUarfcn == o.mUarfcn
+ && TextUtils.equals(mMccStr, o.mMccStr)
+ && TextUtils.equals(mMncStr, o.mMncStr)
+ && TextUtils.equals(mAlphaLong, o.mAlphaLong)
+ && TextUtils.equals(mAlphaShort, o.mAlphaShort);
}
@Override
public String toString() {
- StringBuilder sb = new StringBuilder("CellIdentityWcdma:{");
- sb.append(" mLac=").append(mLac);
- sb.append(" mCid=").append(mCid);
- sb.append(" mPsc=").append(mPsc);
- sb.append(" mUarfcn=").append(mUarfcn);
- sb.append(" mMcc=").append(mMccStr);
- sb.append(" mMnc=").append(mMncStr);
- sb.append(" mAlphaLong=").append(mAlphaLong);
- sb.append(" mAlphaShort=").append(mAlphaShort);
- sb.append("}");
-
- return sb.toString();
- }
-
- /** Implement the Parcelable interface */
- @Override
- public int describeContents() {
- return 0;
+ return new StringBuilder(TAG)
+ .append(":{ mLac=").append(mLac)
+ .append(" mCid=").append(mCid)
+ .append(" mPsc=").append(mPsc)
+ .append(" mUarfcn=").append(mUarfcn)
+ .append(" mMcc=").append(mMccStr)
+ .append(" mMnc=").append(mMncStr)
+ .append(" mAlphaLong=").append(mAlphaLong)
+ .append(" mAlphaShort=").append(mAlphaShort)
+ .append("}").toString();
}
/** Implement the Parcelable interface */
@Override
public void writeToParcel(Parcel dest, int flags) {
if (DBG) log("writeToParcel(Parcel, int): " + toString());
+ super.writeToParcel(dest, TYPE_WCDMA);
dest.writeInt(mLac);
dest.writeInt(mCid);
dest.writeInt(mPsc);
dest.writeInt(mUarfcn);
- dest.writeString(mMccStr);
- dest.writeString(mMncStr);
dest.writeString(mAlphaLong);
dest.writeString(mAlphaShort);
}
/** Construct from Parcel, type has already been processed */
private CellIdentityWcdma(Parcel in) {
- this(in.readInt(), in.readInt(), in.readInt(), in.readInt(), in.readString(),
- in.readString(), in.readString(), in.readString());
-
- if (DBG) log("CellIdentityWcdma(Parcel): " + toString());
+ super(TAG, TYPE_WCDMA, in);
+ mLac = in.readInt();
+ mCid = in.readInt();
+ mPsc = in.readInt();
+ mUarfcn = in.readInt();
+ mAlphaLong = in.readString();
+ mAlphaShort = in.readString();
+ if (DBG) log(toString());
}
/** Implement the Parcelable interface */
@@ -310,7 +272,8 @@ public final class CellIdentityWcdma implements Parcelable {
new Creator<CellIdentityWcdma>() {
@Override
public CellIdentityWcdma createFromParcel(Parcel in) {
- return new CellIdentityWcdma(in);
+ in.readInt(); // skip
+ return createFromParcelBody(in);
}
@Override
@@ -319,10 +282,8 @@ public final class CellIdentityWcdma implements Parcelable {
}
};
- /**
- * log
- */
- private static void log(String s) {
- Rlog.w(LOG_TAG, s);
+ /** @hide */
+ protected static CellIdentityWcdma createFromParcelBody(Parcel in) {
+ return new CellIdentityWcdma(in);
}
} \ No newline at end of file
diff --git a/android/telephony/DisconnectCause.java b/android/telephony/DisconnectCause.java
index 56e1e640..4fa304ae 100644
--- a/android/telephony/DisconnectCause.java
+++ b/android/telephony/DisconnectCause.java
@@ -310,6 +310,13 @@ public class DisconnectCause {
* {@hide}
*/
public static final int DIAL_VIDEO_MODIFIED_TO_DIAL_VIDEO = 70;
+
+ /**
+ * The network has reported that an alternative emergency number has been dialed, but the user
+ * must exit airplane mode to place the call.
+ */
+ public static final int IMS_SIP_ALTERNATE_EMERGENCY_CALL = 71;
+
//*********************************************************************************************
// When adding a disconnect type:
// 1) Update toString() with the newly added disconnect type.
@@ -462,6 +469,8 @@ public class DisconnectCause {
return "EMERGENCY_PERM_FAILURE";
case NORMAL_UNSPECIFIED:
return "NORMAL_UNSPECIFIED";
+ case IMS_SIP_ALTERNATE_EMERGENCY_CALL:
+ return "IMS_SIP_ALTERNATE_EMERGENCY_CALL";
default:
return "INVALID: " + cause;
}
diff --git a/android/telephony/NetworkRegistrationState.java b/android/telephony/NetworkRegistrationState.java
new file mode 100644
index 00000000..e0510694
--- /dev/null
+++ b/android/telephony/NetworkRegistrationState.java
@@ -0,0 +1,258 @@
+/*
+ * 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;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Description of a mobile network registration state
+ * @hide
+ */
+@SystemApi
+public class NetworkRegistrationState implements Parcelable {
+ /**
+ * Network domain
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "DOMAIN_", value = {DOMAIN_CS, DOMAIN_PS})
+ public @interface Domain {}
+
+ /** Circuit switching domain */
+ public static final int DOMAIN_CS = 1;
+ /** Packet switching domain */
+ public static final int DOMAIN_PS = 2;
+
+ /**
+ * Registration state
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "REG_STATE_",
+ value = {REG_STATE_NOT_REG_NOT_SEARCHING, REG_STATE_HOME, REG_STATE_NOT_REG_SEARCHING,
+ REG_STATE_DENIED, REG_STATE_UNKNOWN, REG_STATE_ROAMING})
+ public @interface RegState {}
+
+ /** Not registered. The device is not currently searching a new operator to register */
+ public static final int REG_STATE_NOT_REG_NOT_SEARCHING = 0;
+ /** Registered on home network */
+ public static final int REG_STATE_HOME = 1;
+ /** Not registered. The device is currently searching a new operator to register */
+ public static final int REG_STATE_NOT_REG_SEARCHING = 2;
+ /** Registration denied */
+ public static final int REG_STATE_DENIED = 3;
+ /** Registration state is unknown */
+ public static final int REG_STATE_UNKNOWN = 4;
+ /** Registered on roaming network */
+ public static final int REG_STATE_ROAMING = 5;
+
+ /**
+ * Supported service type
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "SERVICE_TYPE_",
+ value = {SERVICE_TYPE_VOICE, SERVICE_TYPE_DATA, SERVICE_TYPE_SMS, SERVICE_TYPE_VIDEO,
+ SERVICE_TYPE_EMERGENCY})
+ public @interface ServiceType {}
+
+ public static final int SERVICE_TYPE_VOICE = 1;
+ public static final int SERVICE_TYPE_DATA = 2;
+ public static final int SERVICE_TYPE_SMS = 3;
+ public static final int SERVICE_TYPE_VIDEO = 4;
+ public static final int SERVICE_TYPE_EMERGENCY = 5;
+
+ /** {@link AccessNetworkConstants.TransportType}*/
+ private final int mTransportType;
+
+ @Domain
+ private final int mDomain;
+
+ @RegState
+ private final int mRegState;
+
+ private final int mAccessNetworkTechnology;
+
+ private final int mReasonForDenial;
+
+ private final boolean mEmergencyOnly;
+
+ private final int[] mAvailableServices;
+
+ @Nullable
+ private final CellIdentity mCellIdentity;
+
+
+ /**
+ * @param transportType Transport type. Must be {@link AccessNetworkConstants.TransportType}
+ * @param domain Network domain. Must be DOMAIN_CS or DOMAIN_PS.
+ * @param regState Network registration state.
+ * @param accessNetworkTechnology See TelephonyManager NETWORK_TYPE_XXXX.
+ * @param reasonForDenial Reason for denial if the registration state is DENIED.
+ * @param availableServices The supported service.
+ * @param cellIdentity The identity representing a unique cell
+ */
+ public NetworkRegistrationState(int transportType, int domain, int regState,
+ int accessNetworkTechnology, int reasonForDenial, boolean emergencyOnly,
+ int[] availableServices, @Nullable CellIdentity cellIdentity) {
+ mTransportType = transportType;
+ mDomain = domain;
+ mRegState = regState;
+ mAccessNetworkTechnology = accessNetworkTechnology;
+ mReasonForDenial = reasonForDenial;
+ mAvailableServices = availableServices;
+ mCellIdentity = cellIdentity;
+ mEmergencyOnly = emergencyOnly;
+ }
+
+ protected NetworkRegistrationState(Parcel source) {
+ mTransportType = source.readInt();
+ mDomain = source.readInt();
+ mRegState = source.readInt();
+ mAccessNetworkTechnology = source.readInt();
+ mReasonForDenial = source.readInt();
+ mEmergencyOnly = source.readBoolean();
+ mAvailableServices = source.createIntArray();
+ mCellIdentity = source.readParcelable(CellIdentity.class.getClassLoader());
+ }
+
+ /**
+ * @return The transport type.
+ */
+ public int getTransportType() { return mTransportType; }
+
+ /**
+ * @return The network domain.
+ */
+ public @Domain int getDomain() { return mDomain; }
+
+ /**
+ * @return The registration state.
+ */
+ public @RegState int getRegState() {
+ return mRegState;
+ }
+
+ /**
+ * @return Whether emergency is enabled.
+ */
+ public boolean isEmergencyEnabled() { return mEmergencyOnly; }
+
+ /**
+ * @return List of available service types.
+ */
+ public int[] getAvailableServices() { return mAvailableServices; }
+
+ /**
+ * @return The access network technology. Must be one of TelephonyManager.NETWORK_TYPE_XXXX.
+ */
+ public int getAccessNetworkTechnology() {
+ return mAccessNetworkTechnology;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ private static String regStateToString(int regState) {
+ switch (regState) {
+ case REG_STATE_NOT_REG_NOT_SEARCHING: return "NOT_REG_NOT_SEARCHING";
+ case REG_STATE_HOME: return "HOME";
+ case REG_STATE_NOT_REG_SEARCHING: return "NOT_REG_SEARCHING";
+ case REG_STATE_DENIED: return "DENIED";
+ case REG_STATE_UNKNOWN: return "UNKNOWN";
+ case REG_STATE_ROAMING: return "ROAMING";
+ }
+ return "Unknown reg state " + regState;
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder("NetworkRegistrationState{")
+ .append("transportType=").append(mTransportType)
+ .append(" domain=").append((mDomain == DOMAIN_CS) ? "CS" : "PS")
+ .append(" regState=").append(regStateToString(mRegState))
+ .append(" accessNetworkTechnology=")
+ .append(TelephonyManager.getNetworkTypeName(mAccessNetworkTechnology))
+ .append(" reasonForDenial=").append(mReasonForDenial)
+ .append(" emergencyEnabled=").append(mEmergencyOnly)
+ .append(" supportedServices=").append(mAvailableServices)
+ .append(" cellIdentity=").append(mCellIdentity)
+ .append("}").toString();
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mTransportType, mDomain, mRegState, mAccessNetworkTechnology,
+ mReasonForDenial, mEmergencyOnly, mAvailableServices, mCellIdentity);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+
+ if (o == null || !(o instanceof NetworkRegistrationState)) {
+ return false;
+ }
+
+ NetworkRegistrationState other = (NetworkRegistrationState) o;
+ return mTransportType == other.mTransportType
+ && mDomain == other.mDomain
+ && mRegState == other.mRegState
+ && mAccessNetworkTechnology == other.mAccessNetworkTechnology
+ && mReasonForDenial == other.mReasonForDenial
+ && mEmergencyOnly == other.mEmergencyOnly
+ && (mAvailableServices == other.mAvailableServices
+ || Arrays.equals(mAvailableServices, other.mAvailableServices))
+ && mCellIdentity == other.mCellIdentity;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mTransportType);
+ dest.writeInt(mDomain);
+ dest.writeInt(mRegState);
+ dest.writeInt(mAccessNetworkTechnology);
+ dest.writeInt(mReasonForDenial);
+ dest.writeBoolean(mEmergencyOnly);
+ dest.writeIntArray(mAvailableServices);
+ dest.writeParcelable(mCellIdentity, 0);
+ }
+
+ public static final Parcelable.Creator<NetworkRegistrationState> CREATOR =
+ new Parcelable.Creator<NetworkRegistrationState>() {
+ @Override
+ public NetworkRegistrationState createFromParcel(Parcel source) {
+ return new NetworkRegistrationState(source);
+ }
+
+ @Override
+ public NetworkRegistrationState[] newArray(int size) {
+ return new NetworkRegistrationState[size];
+ }
+ };
+}
diff --git a/android/telephony/NetworkScanRequest.java b/android/telephony/NetworkScanRequest.java
index ea503c3e..9726569a 100644
--- a/android/telephony/NetworkScanRequest.java
+++ b/android/telephony/NetworkScanRequest.java
@@ -143,7 +143,11 @@ public final class NetworkScanRequest implements Parcelable {
int incrementalResultsPeriodicity,
ArrayList<String> mccMncs) {
this.mScanType = scanType;
- this.mSpecifiers = specifiers.clone();
+ if (specifiers != null) {
+ this.mSpecifiers = specifiers.clone();
+ } else {
+ this.mSpecifiers = null;
+ }
this.mSearchPeriodicity = searchPeriodicity;
this.mMaxSearchTime = maxSearchTime;
this.mIncrementalResults = incrementalResults;
@@ -187,7 +191,7 @@ public final class NetworkScanRequest implements Parcelable {
/** Returns the radio access technologies with bands or channels that need to be scanned. */
public RadioAccessSpecifier[] getSpecifiers() {
- return mSpecifiers.clone();
+ return mSpecifiers == null ? null : mSpecifiers.clone();
}
/**
diff --git a/android/telephony/NetworkService.java b/android/telephony/NetworkService.java
new file mode 100644
index 00000000..6b3584c1
--- /dev/null
+++ b/android/telephony/NetworkService.java
@@ -0,0 +1,314 @@
+/*
+ * 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;
+
+import android.annotation.CallSuper;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.SparseArray;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base class of network service. Services that extend NetworkService must register the service in
+ * their AndroidManifest to be detected by the framework. They must be protected by the permission
+ * "android.permission.BIND_NETWORK_SERVICE". The network service definition in the manifest must
+ * follow the following format:
+ * ...
+ * <service android:name=".xxxNetworkService"
+ * android:permission="android.permission.BIND_NETWORK_SERVICE" >
+ * <intent-filter>
+ * <action android:name="android.telephony.NetworkService" />
+ * </intent-filter>
+ * </service>
+ * @hide
+ */
+@SystemApi
+public abstract class NetworkService extends Service {
+
+ private final String TAG = NetworkService.class.getSimpleName();
+
+ public static final String NETWORK_SERVICE_INTERFACE = "android.telephony.NetworkService";
+ public static final String NETWORK_SERVICE_EXTRA_SLOT_ID = "android.telephony.extra.SLOT_ID";
+
+ private static final int NETWORK_SERVICE_INTERNAL_REQUEST_INITIALIZE_SERVICE = 1;
+ private static final int NETWORK_SERVICE_GET_REGISTRATION_STATE = 2;
+ private static final int NETWORK_SERVICE_REGISTER_FOR_STATE_CHANGE = 3;
+ private static final int NETWORK_SERVICE_UNREGISTER_FOR_STATE_CHANGE = 4;
+ private static final int NETWORK_SERVICE_INDICATION_NETWORK_STATE_CHANGED = 5;
+
+
+ private final HandlerThread mHandlerThread;
+
+ private final NetworkServiceHandler mHandler;
+
+ private final SparseArray<NetworkServiceProvider> mServiceMap = new SparseArray<>();
+
+ private final SparseArray<INetworkServiceWrapper> mBinderMap = new SparseArray<>();
+
+ /**
+ * The abstract class of the actual network service implementation. The network service provider
+ * must extend this class to support network connection. Note that each instance of network
+ * service is associated with one physical SIM slot.
+ */
+ public class NetworkServiceProvider {
+ private final int mSlotId;
+
+ private final List<INetworkServiceCallback>
+ mNetworkRegistrationStateChangedCallbacks = new ArrayList<>();
+
+ public NetworkServiceProvider(int slotId) {
+ mSlotId = slotId;
+ }
+
+ /**
+ * @return SIM slot id the network service associated with.
+ */
+ public final int getSlotId() {
+ return mSlotId;
+ }
+
+ /**
+ * API to get network registration state. The result will be passed to the callback.
+ * @param domain
+ * @param callback
+ * @return SIM slot id the network service associated with.
+ */
+ public void getNetworkRegistrationState(int domain, NetworkServiceCallback callback) {
+ callback.onGetNetworkRegistrationStateComplete(
+ NetworkServiceCallback.RESULT_ERROR_UNSUPPORTED, null);
+ }
+
+ public final void notifyNetworkRegistrationStateChanged() {
+ mHandler.obtainMessage(NETWORK_SERVICE_INDICATION_NETWORK_STATE_CHANGED,
+ mSlotId, 0, null).sendToTarget();
+ }
+
+ private void registerForStateChanged(INetworkServiceCallback callback) {
+ synchronized (mNetworkRegistrationStateChangedCallbacks) {
+ mNetworkRegistrationStateChangedCallbacks.add(callback);
+ }
+ }
+
+ private void unregisterForStateChanged(INetworkServiceCallback callback) {
+ synchronized (mNetworkRegistrationStateChangedCallbacks) {
+ mNetworkRegistrationStateChangedCallbacks.remove(callback);
+ }
+ }
+
+ private void notifyStateChangedToCallbacks() {
+ for (INetworkServiceCallback callback : mNetworkRegistrationStateChangedCallbacks) {
+ try {
+ callback.onNetworkStateChanged();
+ } catch (RemoteException exception) {
+ // Doing nothing.
+ }
+ }
+ }
+
+ /**
+ * Called when the instance of network service is destroyed (e.g. got unbind or binder died).
+ */
+ @CallSuper
+ protected void onDestroy() {
+ mNetworkRegistrationStateChangedCallbacks.clear();
+ }
+ }
+
+ private class NetworkServiceHandler extends Handler {
+
+ NetworkServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ final int slotId = message.arg1;
+ final INetworkServiceCallback callback = (INetworkServiceCallback) message.obj;
+ NetworkServiceProvider service;
+
+ synchronized (mServiceMap) {
+ service = mServiceMap.get(slotId);
+ }
+
+ switch (message.what) {
+ case NETWORK_SERVICE_INTERNAL_REQUEST_INITIALIZE_SERVICE:
+ service = createNetworkServiceProvider(message.arg1);
+ if (service != null) {
+ mServiceMap.put(slotId, service);
+ }
+ break;
+ case NETWORK_SERVICE_GET_REGISTRATION_STATE:
+ if (service == null) break;
+ int domainId = message.arg2;
+ service.getNetworkRegistrationState(domainId,
+ new NetworkServiceCallback(callback));
+
+ break;
+ case NETWORK_SERVICE_REGISTER_FOR_STATE_CHANGE:
+ if (service == null) break;
+ service.registerForStateChanged(callback);
+ break;
+ case NETWORK_SERVICE_UNREGISTER_FOR_STATE_CHANGE:
+ if (service == null) break;
+ service.unregisterForStateChanged(callback);
+ break;
+ case NETWORK_SERVICE_INDICATION_NETWORK_STATE_CHANGED:
+ if (service == null) break;
+ service.notifyStateChangedToCallbacks();
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ /** @hide */
+ protected NetworkService() {
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+
+ mHandler = new NetworkServiceHandler(mHandlerThread.getLooper());
+ log("network service created");
+ }
+
+ /**
+ * Create the instance of {@link NetworkServiceProvider}. Network service provider must override
+ * this method to facilitate the creation of {@link NetworkServiceProvider} instances. The system
+ * will call this method after binding the network service for each active SIM slot id.
+ *
+ * @param slotId SIM slot id the network service associated with.
+ * @return Network service object
+ */
+ protected abstract NetworkServiceProvider createNetworkServiceProvider(int slotId);
+
+ /** @hide */
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (intent == null || !NETWORK_SERVICE_INTERFACE.equals(intent.getAction())) {
+ loge("Unexpected intent " + intent);
+ return null;
+ }
+
+ int slotId = intent.getIntExtra(
+ NETWORK_SERVICE_EXTRA_SLOT_ID, SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+
+ if (!SubscriptionManager.isValidSlotIndex(slotId)) {
+ loge("Invalid slot id " + slotId);
+ return null;
+ }
+
+ log("onBind: slot id=" + slotId);
+
+ INetworkServiceWrapper binder = mBinderMap.get(slotId);
+ if (binder == null) {
+ Message msg = mHandler.obtainMessage(
+ NETWORK_SERVICE_INTERNAL_REQUEST_INITIALIZE_SERVICE);
+ msg.arg1 = slotId;
+ msg.sendToTarget();
+
+ binder = new INetworkServiceWrapper(slotId);
+ mBinderMap.put(slotId, binder);
+ }
+
+ return binder;
+ }
+
+ /** @hide */
+ @Override
+ public boolean onUnbind(Intent intent) {
+ int slotId = intent.getIntExtra(NETWORK_SERVICE_EXTRA_SLOT_ID,
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+ if (mBinderMap.get(slotId) != null) {
+ NetworkServiceProvider serviceImpl;
+ synchronized (mServiceMap) {
+ serviceImpl = mServiceMap.get(slotId);
+ }
+ // We assume only one component might bind to the service. So if onUnbind is ever
+ // called, we destroy the serviceImpl.
+ if (serviceImpl != null) {
+ serviceImpl.onDestroy();
+ }
+ mBinderMap.remove(slotId);
+ }
+
+ return false;
+ }
+
+ /** @hide */
+ @Override
+ public void onDestroy() {
+ synchronized (mServiceMap) {
+ for (int i = 0; i < mServiceMap.size(); i++) {
+ NetworkServiceProvider serviceImpl = mServiceMap.get(i);
+ if (serviceImpl != null) {
+ serviceImpl.onDestroy();
+ }
+ }
+ mServiceMap.clear();
+ }
+
+ mHandlerThread.quit();
+ }
+
+ /**
+ * A wrapper around INetworkService that forwards calls to implementations of
+ * {@link NetworkService}.
+ */
+ private class INetworkServiceWrapper extends INetworkService.Stub {
+
+ private final int mSlotId;
+
+ INetworkServiceWrapper(int slotId) {
+ mSlotId = slotId;
+ }
+
+ @Override
+ public void getNetworkRegistrationState(int domain, INetworkServiceCallback callback) {
+ mHandler.obtainMessage(NETWORK_SERVICE_GET_REGISTRATION_STATE, mSlotId,
+ domain, callback).sendToTarget();
+ }
+
+ @Override
+ public void registerForNetworkRegistrationStateChanged(INetworkServiceCallback callback) {
+ mHandler.obtainMessage(NETWORK_SERVICE_REGISTER_FOR_STATE_CHANGE, mSlotId,
+ 0, callback).sendToTarget();
+ }
+
+ @Override
+ public void unregisterForNetworkRegistrationStateChanged(INetworkServiceCallback callback) {
+ mHandler.obtainMessage(NETWORK_SERVICE_UNREGISTER_FOR_STATE_CHANGE, mSlotId,
+ 0, callback).sendToTarget();
+ }
+ }
+
+ private final void log(String s) {
+ Rlog.d(TAG, s);
+ }
+
+ private final void loge(String s) {
+ Rlog.e(TAG, s);
+ }
+} \ No newline at end of file
diff --git a/android/telephony/NetworkServiceCallback.java b/android/telephony/NetworkServiceCallback.java
new file mode 100644
index 00000000..92ebf367
--- /dev/null
+++ b/android/telephony/NetworkServiceCallback.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 android.telephony;
+
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.os.RemoteException;
+import android.telephony.NetworkService.NetworkServiceProvider;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+
+/**
+ * Network service callback. Object of this class is passed to NetworkServiceProvider upon
+ * calling getNetworkRegistrationState, to receive asynchronous feedback from NetworkServiceProvider
+ * upon onGetNetworkRegistrationStateComplete. It's like a wrapper of INetworkServiceCallback
+ * because INetworkServiceCallback can't be a parameter type in public APIs.
+ *
+ * @hide
+ */
+@SystemApi
+public class NetworkServiceCallback {
+
+ private static final String mTag = NetworkServiceCallback.class.getSimpleName();
+
+ /**
+ * Result of network requests
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({RESULT_SUCCESS, RESULT_ERROR_UNSUPPORTED, RESULT_ERROR_INVALID_ARG, RESULT_ERROR_BUSY,
+ RESULT_ERROR_ILLEGAL_STATE, RESULT_ERROR_FAILED})
+ public @interface Result {}
+
+ /** Request is completed successfully */
+ public static final int RESULT_SUCCESS = 0;
+ /** Request is not support */
+ public static final int RESULT_ERROR_UNSUPPORTED = 1;
+ /** Request contains invalid arguments */
+ public static final int RESULT_ERROR_INVALID_ARG = 2;
+ /** Service is busy */
+ public static final int RESULT_ERROR_BUSY = 3;
+ /** Request sent in illegal state */
+ public static final int RESULT_ERROR_ILLEGAL_STATE = 4;
+ /** Request failed */
+ public static final int RESULT_ERROR_FAILED = 5;
+
+ private final WeakReference<INetworkServiceCallback> mCallback;
+
+ /** @hide */
+ public NetworkServiceCallback(INetworkServiceCallback callback) {
+ mCallback = new WeakReference<>(callback);
+ }
+
+ /**
+ * Called to indicate result of
+ * {@link NetworkServiceProvider#getNetworkRegistrationState(int, NetworkServiceCallback)}
+ *
+ * @param result Result status like {@link NetworkServiceCallback#RESULT_SUCCESS} or
+ * {@link NetworkServiceCallback#RESULT_ERROR_UNSUPPORTED}
+ * @param state The state information to be returned to callback.
+ */
+ public void onGetNetworkRegistrationStateComplete(int result, NetworkRegistrationState state) {
+ INetworkServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onGetNetworkRegistrationStateComplete(result, state);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onGetNetworkRegistrationStateComplete on the remote");
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/android/telephony/PhoneStateListener.java b/android/telephony/PhoneStateListener.java
index c7e51310..0ee870aa 100644
--- a/android/telephony/PhoneStateListener.java
+++ b/android/telephony/PhoneStateListener.java
@@ -244,7 +244,22 @@ public class PhoneStateListener {
*/
public static final int LISTEN_DATA_ACTIVATION_STATE = 0x00040000;
- /*
+ /**
+ * Listen for changes to the user mobile data state
+ *
+ * @see #onUserMobileDataStateChanged
+ */
+ public static final int LISTEN_USER_MOBILE_DATA_STATE = 0x00080000;
+
+ /**
+ * Listen for changes to the physical channel configuration.
+ *
+ * @see #onPhysicalChannelConfigurationChanged
+ * @hide
+ */
+ public static final int LISTEN_PHYSICAL_CHANNEL_CONFIGURATION = 0x00100000;
+
+ /*
* Subscription used to listen to the phone state changes
* @hide
*/
@@ -349,10 +364,16 @@ public class PhoneStateListener {
case LISTEN_DATA_ACTIVATION_STATE:
PhoneStateListener.this.onDataActivationStateChanged((int)msg.obj);
break;
+ case LISTEN_USER_MOBILE_DATA_STATE:
+ PhoneStateListener.this.onUserMobileDataStateChanged((boolean)msg.obj);
+ break;
case LISTEN_CARRIER_NETWORK_CHANGE:
PhoneStateListener.this.onCarrierNetworkChange((boolean)msg.obj);
break;
-
+ case LISTEN_PHYSICAL_CHANNEL_CONFIGURATION:
+ PhoneStateListener.this.onPhysicalChannelConfigurationChanged(
+ (List<PhysicalChannelConfig>)msg.obj);
+ break;
}
}
};
@@ -543,6 +564,24 @@ public class PhoneStateListener {
}
/**
+ * Callback invoked when the user mobile data state has changed
+ * @param enabled indicates whether the current user mobile data state is enabled or disabled.
+ */
+ public void onUserMobileDataStateChanged(boolean enabled) {
+ // default implementation empty
+ }
+
+ /**
+ * Callback invoked when the current physical channel configuration has changed
+ *
+ * @param configs List of the current {@link PhysicalChannelConfig}s
+ * @hide
+ */
+ public void onPhysicalChannelConfigurationChanged(List<PhysicalChannelConfig> configs) {
+ // default implementation empty
+ }
+
+ /**
* Callback invoked when telephony has received notice from a carrier
* app that a network action that could result in connectivity loss
* has been requested by an app using
@@ -654,6 +693,10 @@ public class PhoneStateListener {
send(LISTEN_DATA_ACTIVATION_STATE, 0, 0, activationState);
}
+ public void onUserMobileDataStateChanged(boolean enabled) {
+ send(LISTEN_USER_MOBILE_DATA_STATE, 0, 0, enabled);
+ }
+
public void onCarrierNetworkChange(boolean active) {
send(LISTEN_CARRIER_NETWORK_CHANGE, 0, 0, active);
}
diff --git a/android/telephony/PhysicalChannelConfig.java b/android/telephony/PhysicalChannelConfig.java
new file mode 100644
index 00000000..651d68d8
--- /dev/null
+++ b/android/telephony/PhysicalChannelConfig.java
@@ -0,0 +1,127 @@
+/*
+ * 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.telephony;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.annotation.IntDef;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * @hide
+ */
+public final class PhysicalChannelConfig implements Parcelable {
+
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({CONNECTION_PRIMARY_SERVING, CONNECTION_SECONDARY_SERVING})
+ public @interface ConnectionStatus {}
+
+ /**
+ * UE has connection to cell for signalling and possibly data (3GPP 36.331, 25.331).
+ */
+ public static final int CONNECTION_PRIMARY_SERVING = 1;
+
+ /**
+ * UE has connection to cell for data (3GPP 36.331, 25.331).
+ */
+ public static final int CONNECTION_SECONDARY_SERVING = 2;
+
+ /**
+ * Connection status of the cell.
+ *
+ * <p>One of {@link #CONNECTION_PRIMARY_SERVING}, {@link #CONNECTION_SECONDARY_SERVING}.
+ */
+ private int mCellConnectionStatus;
+
+ /**
+ * Cell bandwidth, in kHz.
+ */
+ private int mCellBandwidthDownlinkKhz;
+
+ public PhysicalChannelConfig(int status, int bandwidth) {
+ mCellConnectionStatus = status;
+ mCellBandwidthDownlinkKhz = bandwidth;
+ }
+
+ public PhysicalChannelConfig(Parcel in) {
+ mCellConnectionStatus = in.readInt();
+ mCellBandwidthDownlinkKhz = in.readInt();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mCellConnectionStatus);
+ dest.writeInt(mCellBandwidthDownlinkKhz);
+ }
+
+ /**
+ * @return Cell bandwidth, in kHz
+ */
+ public int getCellBandwidthDownlink() {
+ return mCellBandwidthDownlinkKhz;
+ }
+
+ /**
+ * Gets the connection status of the cell.
+ *
+ * @see #CONNECTION_PRIMARY_SERVING
+ * @see #CONNECTION_SECONDARY_SERVING
+ *
+ * @return Connection status of the cell
+ */
+ @ConnectionStatus
+ public int getConnectionStatus() {
+ return mCellConnectionStatus;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+
+ if (!(o instanceof PhysicalChannelConfig)) {
+ return false;
+ }
+
+ PhysicalChannelConfig config = (PhysicalChannelConfig) o;
+ return mCellConnectionStatus == config.mCellConnectionStatus
+ && mCellBandwidthDownlinkKhz == config.mCellBandwidthDownlinkKhz;
+ }
+
+ @Override
+ public int hashCode() {
+ return (mCellBandwidthDownlinkKhz * 29) + (mCellConnectionStatus * 31);
+ }
+
+ public static final Parcelable.Creator<PhysicalChannelConfig> CREATOR =
+ new Parcelable.Creator<PhysicalChannelConfig>() {
+ public PhysicalChannelConfig createFromParcel(Parcel in) {
+ return new PhysicalChannelConfig(in);
+ }
+
+ public PhysicalChannelConfig[] newArray(int size) {
+ return new PhysicalChannelConfig[size];
+ }
+ };
+}
diff --git a/android/telephony/RadioAccessSpecifier.java b/android/telephony/RadioAccessSpecifier.java
index 5412c617..81e7ed01 100644
--- a/android/telephony/RadioAccessSpecifier.java
+++ b/android/telephony/RadioAccessSpecifier.java
@@ -33,7 +33,7 @@ public final class RadioAccessSpecifier implements Parcelable {
*
* This parameter must be provided or else the scan will be rejected.
*
- * See {@link RadioNetworkConstants.RadioAccessNetworks} for details.
+ * See {@link AccessNetworkConstants.AccessNetworkType} for details.
*/
private int mRadioAccessNetwork;
@@ -43,7 +43,7 @@ public final class RadioAccessSpecifier implements Parcelable {
* 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.
+ * See {@link AccessNetworkConstants} for details.
*/
private int[] mBands;
@@ -56,7 +56,7 @@ public final class RadioAccessSpecifier implements Parcelable {
* 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.
+ * See {@link AccessNetworkConstants} for details.
*/
private int[] mChannels;
@@ -72,14 +72,22 @@ public final class RadioAccessSpecifier implements Parcelable {
*/
public RadioAccessSpecifier(int ran, int[] bands, int[] channels) {
this.mRadioAccessNetwork = ran;
- this.mBands = bands.clone();
- this.mChannels = channels.clone();
+ if (bands != null) {
+ this.mBands = bands.clone();
+ } else {
+ this.mBands = null;
+ }
+ if (channels != null) {
+ this.mChannels = channels.clone();
+ } else {
+ this.mChannels = null;
+ }
}
/**
* Returns the radio access network that needs to be scanned.
*
- * The returned value is define in {@link RadioNetworkConstants.RadioAccessNetworks};
+ * The returned value is define in {@link AccessNetworkConstants.AccessNetworkType};
*/
public int getRadioAccessNetwork() {
return mRadioAccessNetwork;
@@ -88,17 +96,17 @@ public final class RadioAccessSpecifier implements Parcelable {
/**
* 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
+ * The returned value is defined in either of {@link AccessNetworkConstants.GeranBand},
+ * {@link AccessNetworkConstants.UtranBand} and {@link AccessNetworkConstants.EutranBand}, and
* it depends on the returned value of {@link #getRadioAccessNetwork()}.
*/
public int[] getBands() {
- return mBands.clone();
+ return mBands == null ? null : mBands.clone();
}
/** Returns the frequency channels that need to be scanned. */
public int[] getChannels() {
- return mChannels.clone();
+ return mChannels == null ? null : mChannels.clone();
}
public static final Parcelable.Creator<RadioAccessSpecifier> CREATOR =
diff --git a/android/telephony/ServiceState.java b/android/telephony/ServiceState.java
index 116e711e..77706e8f 100644
--- a/android/telephony/ServiceState.java
+++ b/android/telephony/ServiceState.java
@@ -16,12 +16,19 @@
package android.telephony;
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
-import android.telephony.Rlog;
import android.text.TextUtils;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import java.util.ArrayList;
+import java.util.List;
+
/**
* Contains phone state and service related information.
*
@@ -105,6 +112,31 @@ public class ServiceState implements Parcelable {
/** @hide */
public static final int RIL_REG_STATE_UNKNOWN_EMERGENCY_CALL_ENABLED = 14;
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "RIL_RADIO_TECHNOLOGY_" },
+ value = {
+ RIL_RADIO_TECHNOLOGY_UNKNOWN,
+ RIL_RADIO_TECHNOLOGY_GPRS,
+ RIL_RADIO_TECHNOLOGY_EDGE,
+ RIL_RADIO_TECHNOLOGY_UMTS,
+ RIL_RADIO_TECHNOLOGY_IS95A,
+ RIL_RADIO_TECHNOLOGY_IS95B,
+ RIL_RADIO_TECHNOLOGY_1xRTT,
+ RIL_RADIO_TECHNOLOGY_EVDO_0,
+ RIL_RADIO_TECHNOLOGY_EVDO_A,
+ RIL_RADIO_TECHNOLOGY_HSDPA,
+ RIL_RADIO_TECHNOLOGY_HSUPA,
+ RIL_RADIO_TECHNOLOGY_HSPA,
+ RIL_RADIO_TECHNOLOGY_EVDO_B,
+ RIL_RADIO_TECHNOLOGY_EHRPD,
+ RIL_RADIO_TECHNOLOGY_LTE,
+ RIL_RADIO_TECHNOLOGY_HSPAP,
+ RIL_RADIO_TECHNOLOGY_GSM,
+ RIL_RADIO_TECHNOLOGY_TD_SCDMA,
+ RIL_RADIO_TECHNOLOGY_IWLAN,
+ RIL_RADIO_TECHNOLOGY_LTE_CA})
+ public @interface RilRadioTechnology {}
/**
* Available radio technologies for GSM, UMTS and CDMA.
* Duplicates the constants from hardware/radio/include/ril.h
@@ -162,6 +194,12 @@ public class ServiceState implements Parcelable {
*/
public static final int RIL_RADIO_TECHNOLOGY_LTE_CA = 19;
+ /**
+ * Number of radio technologies for GSM, UMTS and CDMA.
+ * @hide
+ */
+ private static final int NEXT_RIL_RADIO_TECHNOLOGY = 20;
+
/** @hide */
public static final int RIL_RADIO_CDMA_TECHNOLOGY_BITMASK =
(1 << (RIL_RADIO_TECHNOLOGY_IS95A - 1))
@@ -216,6 +254,11 @@ public class ServiceState implements Parcelable {
*/
public static final int ROAMING_TYPE_INTERNATIONAL = 3;
+ /**
+ * Unknown ID. Could be returned by {@link #getNetworkId()} or {@link #getSystemId()}
+ */
+ public static final int UNKNOWN_ID = -1;
+
private int mVoiceRoamingType;
private int mDataRoamingType;
private String mVoiceOperatorAlphaLong;
@@ -247,6 +290,8 @@ public class ServiceState implements Parcelable {
* Reference: 3GPP TS 36.104 5.4.3 */
private int mLteEarfcnRsrpBoost = 0;
+ private List<NetworkRegistrationState> mNetworkRegistrationStates = new ArrayList<>();
+
/**
* get String description of roaming type
* @hide
@@ -327,6 +372,7 @@ public class ServiceState implements Parcelable {
mIsDataRoamingFromRegistration = s.mIsDataRoamingFromRegistration;
mIsUsingCarrierAggregation = s.mIsUsingCarrierAggregation;
mLteEarfcnRsrpBoost = s.mLteEarfcnRsrpBoost;
+ mNetworkRegistrationStates = new ArrayList<>(s.mNetworkRegistrationStates);
}
/**
@@ -357,6 +403,8 @@ public class ServiceState implements Parcelable {
mIsDataRoamingFromRegistration = in.readInt() != 0;
mIsUsingCarrierAggregation = in.readInt() != 0;
mLteEarfcnRsrpBoost = in.readInt();
+ mNetworkRegistrationStates = new ArrayList<>();
+ in.readList(mNetworkRegistrationStates, NetworkRegistrationState.class.getClassLoader());
}
public void writeToParcel(Parcel out, int flags) {
@@ -384,6 +432,7 @@ public class ServiceState implements Parcelable {
out.writeInt(mIsDataRoamingFromRegistration ? 1 : 0);
out.writeInt(mIsUsingCarrierAggregation ? 1 : 0);
out.writeInt(mLteEarfcnRsrpBoost);
+ out.writeList(mNetworkRegistrationStates);
}
public int describeContents() {
@@ -712,13 +761,14 @@ public class ServiceState implements Parcelable {
s.mCdmaDefaultRoamingIndicator)
&& mIsEmergencyOnly == s.mIsEmergencyOnly
&& mIsDataRoamingFromRegistration == s.mIsDataRoamingFromRegistration
- && mIsUsingCarrierAggregation == s.mIsUsingCarrierAggregation);
+ && mIsUsingCarrierAggregation == s.mIsUsingCarrierAggregation)
+ && mNetworkRegistrationStates.containsAll(s.mNetworkRegistrationStates);
}
/**
* Convert radio technology to String
*
- * @param radioTechnology
+ * @param rt radioTechnology
* @return String representation of the RAT
*
* @hide
@@ -845,6 +895,7 @@ public class ServiceState implements Parcelable {
.append(", mIsDataRoamingFromRegistration=").append(mIsDataRoamingFromRegistration)
.append(", mIsUsingCarrierAggregation=").append(mIsUsingCarrierAggregation)
.append(", mLteEarfcnRsrpBoost=").append(mLteEarfcnRsrpBoost)
+ .append(", mNetworkRegistrationStates=").append(mNetworkRegistrationStates)
.append("}").toString();
}
@@ -874,6 +925,7 @@ public class ServiceState implements Parcelable {
mIsDataRoamingFromRegistration = false;
mIsUsingCarrierAggregation = false;
mLteEarfcnRsrpBoost = 0;
+ mNetworkRegistrationStates = new ArrayList<>();
}
public void setStateOutOfService() {
@@ -1153,7 +1205,8 @@ public class ServiceState implements Parcelable {
return getRilDataRadioTechnology();
}
- private int rilRadioTechnologyToNetworkType(int rt) {
+ /** @hide */
+ public static int rilRadioTechnologyToNetworkType(@RilRadioTechnology int rt) {
switch(rt) {
case ServiceState.RIL_RADIO_TECHNOLOGY_GPRS:
return TelephonyManager.NETWORK_TYPE_GPRS;
@@ -1212,12 +1265,20 @@ public class ServiceState implements Parcelable {
return this.mCssIndicator ? 1 : 0;
}
- /** @hide */
+ /**
+ * Get the CDMA NID (Network Identification Number), a number uniquely identifying a network
+ * within a wireless system. (Defined in 3GPP2 C.S0023 3.4.8)
+ * @return The CDMA NID or {@link #UNKNOWN_ID} if not available.
+ */
public int getNetworkId() {
return this.mNetworkId;
}
- /** @hide */
+ /**
+ * Get the CDMA SID (System Identification Number), a number uniquely identifying a wireless
+ * system. (Defined in 3GPP2 C.S0023 3.4.8)
+ * @return The CDMA SID or {@link #UNKNOWN_ID} if not available.
+ */
public int getSystemId() {
return this.mSystemId;
}
@@ -1300,6 +1361,34 @@ public class ServiceState implements Parcelable {
return bearerBitmask;
}
+ /** @hide */
+ public static int convertNetworkTypeBitmaskToBearerBitmask(int networkTypeBitmask) {
+ if (networkTypeBitmask == 0) {
+ return 0;
+ }
+ int bearerBitmask = 0;
+ for (int bearerInt = 0; bearerInt < NEXT_RIL_RADIO_TECHNOLOGY; bearerInt++) {
+ if (bitmaskHasTech(networkTypeBitmask, rilRadioTechnologyToNetworkType(bearerInt))) {
+ bearerBitmask |= getBitmaskForTech(bearerInt);
+ }
+ }
+ return bearerBitmask;
+ }
+
+ /** @hide */
+ public static int convertBearerBitmaskToNetworkTypeBitmask(int bearerBitmask) {
+ if (bearerBitmask == 0) {
+ return 0;
+ }
+ int networkTypeBitmask = 0;
+ for (int bearerInt = 0; bearerInt < NEXT_RIL_RADIO_TECHNOLOGY; bearerInt++) {
+ if (bitmaskHasTech(bearerBitmask, bearerInt)) {
+ networkTypeBitmask |= getBitmaskForTech(rilRadioTechnologyToNetworkType(bearerInt));
+ }
+ }
+ return networkTypeBitmask;
+ }
+
/**
* Returns a merged ServiceState consisting of the base SS with voice settings from the
* voice SS. The voice SS is only used if it is IN_SERVICE (otherwise the base SS is returned).
@@ -1318,4 +1407,52 @@ public class ServiceState implements Parcelable {
return newSs;
}
+
+ /**
+ * Get all of the available network registration states.
+ *
+ * @return List of registration states
+ * @hide
+ */
+ @SystemApi
+ public List<NetworkRegistrationState> getNetworkRegistrationStates() {
+ return mNetworkRegistrationStates;
+ }
+
+ /**
+ * Get the network registration states with given transport type.
+ *
+ * @param transportType The transport type. See {@link AccessNetworkConstants.TransportType}
+ * @return List of registration states.
+ * @hide
+ */
+ @SystemApi
+ public List<NetworkRegistrationState> getNetworkRegistrationStates(int transportType) {
+ List<NetworkRegistrationState> list = new ArrayList<>();
+ for (NetworkRegistrationState networkRegistrationState : mNetworkRegistrationStates) {
+ if (networkRegistrationState.getTransportType() == transportType) {
+ list.add(networkRegistrationState);
+ }
+ }
+ return list;
+ }
+
+ /**
+ * Get the network registration states with given transport type and domain.
+ *
+ * @param transportType The transport type. See {@link AccessNetworkConstants.TransportType}
+ * @param domain The network domain. Must be DOMAIN_CS or DOMAIN_PS.
+ * @return The matching NetworkRegistrationState.
+ * @hide
+ */
+ @SystemApi
+ public NetworkRegistrationState getNetworkRegistrationStates(int transportType, int domain) {
+ for (NetworkRegistrationState networkRegistrationState : mNetworkRegistrationStates) {
+ if (networkRegistrationState.getTransportType() == transportType
+ && networkRegistrationState.getDomain() == domain) {
+ return networkRegistrationState;
+ }
+ }
+ return null;
+ }
}
diff --git a/android/telephony/SignalStrength.java b/android/telephony/SignalStrength.java
index de02de7b..fc2ef278 100644
--- a/android/telephony/SignalStrength.java
+++ b/android/telephony/SignalStrength.java
@@ -19,9 +19,13 @@ package android.telephony;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
+import android.telephony.CarrierConfigManager;
import android.util.Log;
import android.content.res.Resources;
+import java.util.ArrayList;
+import java.util.Arrays;
+
/**
* Contains phone signal strength related information.
*/
@@ -47,10 +51,15 @@ public class SignalStrength implements Parcelable {
"none", "poor", "moderate", "good", "great"
};
- /** @hide */
- //Use int max, as -1 is a valid value in signal strength
- public static final int INVALID = 0x7FFFFFFF;
+ /**
+ * Use Integer.MAX_VALUE because -1 is a valid value in signal strength.
+ * @hide
+ */
+ public static final int INVALID = Integer.MAX_VALUE;
+ private static final int LTE_RSRP_THRESHOLDS_NUM = 6;
+
+ /** Parameters reported by the Radio */
private int mGsmSignalStrength; // Valid values are (0-31, 99) as defined in TS 27.007 8.5
private int mGsmBitErrorRate; // bit error rate (0-7, 99) as defined in TS 27.007 8.5
private int mCdmaDbm; // This value is the RSSI value
@@ -63,13 +72,18 @@ public class SignalStrength implements Parcelable {
private int mLteRsrq;
private int mLteRssnr;
private int mLteCqi;
- private int mLteRsrpBoost; // offset to be reduced from the rsrp threshold while calculating
- // signal strength level
private int mTdScdmaRscp;
- private boolean isGsm; // This value is set by the ServiceStateTracker onSignalStrengthResult
+ /** Parameters from the framework */
+ private int mLteRsrpBoost; // offset to be reduced from the rsrp threshold while calculating
+ // signal strength level
+ private boolean mIsGsm; // This value is set by the ServiceStateTracker
+ // onSignalStrengthResult.
private boolean mUseOnlyRsrpForLteLevel; // Use only RSRP for the number of LTE signal bar.
+ // The threshold of LTE RSRP for determining the display level of LTE signal bar.
+ private int mLteRsrpThresholds[] = new int[LTE_RSRP_THRESHOLDS_NUM];
+
/**
* Create a new SignalStrength from a intent notifier Bundle
*
@@ -94,27 +108,12 @@ public class SignalStrength implements Parcelable {
* @hide
*/
public SignalStrength() {
- mGsmSignalStrength = 99;
- mGsmBitErrorRate = -1;
- mCdmaDbm = -1;
- mCdmaEcio = -1;
- mEvdoDbm = -1;
- mEvdoEcio = -1;
- mEvdoSnr = -1;
- mLteSignalStrength = 99;
- mLteRsrp = INVALID;
- mLteRsrq = INVALID;
- mLteRssnr = INVALID;
- mLteCqi = INVALID;
- mLteRsrpBoost = 0;
- mTdScdmaRscp = INVALID;
- isGsm = true;
- mUseOnlyRsrpForLteLevel = false;
+ this(true);
}
/**
* This constructor is used to create SignalStrength with default
- * values and set the isGsmFlag with the value passed in the input
+ * values and set the gsmFlag with the value passed in the input
*
* @param gsmFlag true if Gsm Phone,false if Cdma phone
* @return newly created SignalStrength
@@ -133,46 +132,48 @@ public class SignalStrength implements Parcelable {
mLteRsrq = INVALID;
mLteRssnr = INVALID;
mLteCqi = INVALID;
- mLteRsrpBoost = 0;
mTdScdmaRscp = INVALID;
- isGsm = gsmFlag;
+ mLteRsrpBoost = 0;
+ mIsGsm = gsmFlag;
mUseOnlyRsrpForLteLevel = false;
+ setLteRsrpThresholds(getDefaultLteRsrpThresholds());
}
/**
- * Constructor
- *
- * @hide
- */
- public SignalStrength(int gsmSignalStrength, int gsmBitErrorRate,
- int cdmaDbm, int cdmaEcio,
- int evdoDbm, int evdoEcio, int evdoSnr,
- int lteSignalStrength, int lteRsrp, int lteRsrq, int lteRssnr, int lteCqi,
- int lteRsrpBoost, int tdScdmaRscp, boolean gsmFlag, boolean lteLevelBaseOnRsrp) {
- initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio,
- evdoDbm, evdoEcio, evdoSnr, lteSignalStrength, lteRsrp,
- lteRsrq, lteRssnr, lteCqi, lteRsrpBoost, gsmFlag, lteLevelBaseOnRsrp);
- mTdScdmaRscp = tdScdmaRscp;
- }
-
- /**
- * Constructor
+ * Constructor with all fields present
*
* @hide
*/
- public SignalStrength(int gsmSignalStrength, int gsmBitErrorRate,
+ public SignalStrength(
+ int gsmSignalStrength, int gsmBitErrorRate,
int cdmaDbm, int cdmaEcio,
int evdoDbm, int evdoEcio, int evdoSnr,
int lteSignalStrength, int lteRsrp, int lteRsrq, int lteRssnr, int lteCqi,
- int tdScdmaRscp, boolean gsmFlag) {
- initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio,
- evdoDbm, evdoEcio, evdoSnr, lteSignalStrength, lteRsrp,
- lteRsrq, lteRssnr, lteCqi, 0, gsmFlag, false);
- mTdScdmaRscp = tdScdmaRscp;
+ int tdScdmaRscp,
+ // values Added by config
+ int lteRsrpBoost, boolean gsmFlag, boolean lteLevelBaseOnRsrp) {
+ mGsmSignalStrength = gsmSignalStrength;
+ mGsmBitErrorRate = gsmBitErrorRate;
+ mCdmaDbm = cdmaDbm;
+ mCdmaEcio = cdmaEcio;
+ mEvdoDbm = evdoDbm;
+ mEvdoEcio = evdoEcio;
+ mEvdoSnr = evdoSnr;
+ mLteSignalStrength = lteSignalStrength;
+ mLteRsrp = lteRsrp;
+ mLteRsrq = lteRsrq;
+ mLteRssnr = lteRssnr;
+ mLteCqi = lteCqi;
+ mTdScdmaRscp = INVALID;
+ mLteRsrpBoost = lteRsrpBoost;
+ mIsGsm = gsmFlag;
+ mUseOnlyRsrpForLteLevel = lteLevelBaseOnRsrp;
+ setLteRsrpThresholds(getDefaultLteRsrpThresholds());
+ if (DBG) log("initialize: " + toString());
}
/**
- * Constructor
+ * Constructor for only values provided by Radio HAL
*
* @hide
*/
@@ -180,24 +181,10 @@ public class SignalStrength implements Parcelable {
int cdmaDbm, int cdmaEcio,
int evdoDbm, int evdoEcio, int evdoSnr,
int lteSignalStrength, int lteRsrp, int lteRsrq, int lteRssnr, int lteCqi,
- boolean gsmFlag) {
- initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio,
+ int tdScdmaRscp) {
+ this(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio,
evdoDbm, evdoEcio, evdoSnr, lteSignalStrength, lteRsrp,
- lteRsrq, lteRssnr, lteCqi, 0, gsmFlag, false);
- }
-
- /**
- * Constructor
- *
- * @hide
- */
- public SignalStrength(int gsmSignalStrength, int gsmBitErrorRate,
- int cdmaDbm, int cdmaEcio,
- int evdoDbm, int evdoEcio, int evdoSnr,
- boolean gsmFlag) {
- initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio,
- evdoDbm, evdoEcio, evdoSnr, 99, INVALID,
- INVALID, INVALID, INVALID, 0, gsmFlag, false);
+ lteRsrq, lteRssnr, lteCqi, tdScdmaRscp, 0, true, false);
}
/**
@@ -212,74 +199,6 @@ public class SignalStrength implements Parcelable {
}
/**
- * Initialize gsm/cdma values, sets lte values to defaults.
- *
- * @param gsmSignalStrength
- * @param gsmBitErrorRate
- * @param cdmaDbm
- * @param cdmaEcio
- * @param evdoDbm
- * @param evdoEcio
- * @param evdoSnr
- * @param gsm
- *
- * @hide
- */
- public void initialize(int gsmSignalStrength, int gsmBitErrorRate,
- int cdmaDbm, int cdmaEcio,
- int evdoDbm, int evdoEcio, int evdoSnr,
- boolean gsm) {
- initialize(gsmSignalStrength, gsmBitErrorRate, cdmaDbm, cdmaEcio,
- evdoDbm, evdoEcio, evdoSnr, 99, INVALID,
- INVALID, INVALID, INVALID, 0, gsm, false);
- }
-
- /**
- * Initialize all the values
- *
- * @param gsmSignalStrength
- * @param gsmBitErrorRate
- * @param cdmaDbm
- * @param cdmaEcio
- * @param evdoDbm
- * @param evdoEcio
- * @param evdoSnr
- * @param lteSignalStrength
- * @param lteRsrp
- * @param lteRsrq
- * @param lteRssnr
- * @param lteCqi
- * @param lteRsrpBoost
- * @param gsm
- * @param useOnlyRsrpForLteLevel
- *
- * @hide
- */
- public void initialize(int gsmSignalStrength, int gsmBitErrorRate,
- int cdmaDbm, int cdmaEcio,
- int evdoDbm, int evdoEcio, int evdoSnr,
- int lteSignalStrength, int lteRsrp, int lteRsrq, int lteRssnr, int lteCqi,
- int lteRsrpBoost, boolean gsm, boolean useOnlyRsrpForLteLevel) {
- mGsmSignalStrength = gsmSignalStrength;
- mGsmBitErrorRate = gsmBitErrorRate;
- mCdmaDbm = cdmaDbm;
- mCdmaEcio = cdmaEcio;
- mEvdoDbm = evdoDbm;
- mEvdoEcio = evdoEcio;
- mEvdoSnr = evdoSnr;
- mLteSignalStrength = lteSignalStrength;
- mLteRsrp = lteRsrp;
- mLteRsrq = lteRsrq;
- mLteRssnr = lteRssnr;
- mLteCqi = lteCqi;
- mLteRsrpBoost = lteRsrpBoost;
- mTdScdmaRscp = INVALID;
- isGsm = gsm;
- mUseOnlyRsrpForLteLevel = useOnlyRsrpForLteLevel;
- if (DBG) log("initialize: " + toString());
- }
-
- /**
* @hide
*/
protected void copyFrom(SignalStrength s) {
@@ -295,10 +214,11 @@ public class SignalStrength implements Parcelable {
mLteRsrq = s.mLteRsrq;
mLteRssnr = s.mLteRssnr;
mLteCqi = s.mLteCqi;
- mLteRsrpBoost = s.mLteRsrpBoost;
mTdScdmaRscp = s.mTdScdmaRscp;
- isGsm = s.isGsm;
+ mLteRsrpBoost = s.mLteRsrpBoost;
+ mIsGsm = s.mIsGsm;
mUseOnlyRsrpForLteLevel = s.mUseOnlyRsrpForLteLevel;
+ setLteRsrpThresholds(s.mLteRsrpThresholds);
}
/**
@@ -321,37 +241,11 @@ public class SignalStrength implements Parcelable {
mLteRsrq = in.readInt();
mLteRssnr = in.readInt();
mLteCqi = in.readInt();
- mLteRsrpBoost = in.readInt();
mTdScdmaRscp = in.readInt();
- isGsm = (in.readInt() != 0);
- mUseOnlyRsrpForLteLevel = (in.readInt() != 0);
- }
-
- /**
- * Make a SignalStrength object from the given parcel as passed up by
- * the ril which does not have isGsm. isGsm will be changed by ServiceStateTracker
- * so the default is a don't care.
- *
- * @hide
- */
- public static SignalStrength makeSignalStrengthFromRilParcel(Parcel in) {
- if (DBG) log("Size of signalstrength parcel:" + in.dataSize());
-
- SignalStrength ss = new SignalStrength();
- ss.mGsmSignalStrength = in.readInt();
- ss.mGsmBitErrorRate = in.readInt();
- ss.mCdmaDbm = in.readInt();
- ss.mCdmaEcio = in.readInt();
- ss.mEvdoDbm = in.readInt();
- ss.mEvdoEcio = in.readInt();
- ss.mEvdoSnr = in.readInt();
- ss.mLteSignalStrength = in.readInt();
- ss.mLteRsrp = in.readInt();
- ss.mLteRsrq = in.readInt();
- ss.mLteRssnr = in.readInt();
- ss.mLteCqi = in.readInt();
- ss.mTdScdmaRscp = in.readInt();
- return ss;
+ mLteRsrpBoost = in.readInt();
+ mIsGsm = in.readBoolean();
+ mUseOnlyRsrpForLteLevel = in.readBoolean();
+ in.readIntArray(mLteRsrpThresholds);
}
/**
@@ -370,10 +264,11 @@ public class SignalStrength implements Parcelable {
out.writeInt(mLteRsrq);
out.writeInt(mLteRssnr);
out.writeInt(mLteCqi);
- out.writeInt(mLteRsrpBoost);
out.writeInt(mTdScdmaRscp);
- out.writeInt(isGsm ? 1 : 0);
- out.writeInt(mUseOnlyRsrpForLteLevel ? 1 : 0);
+ out.writeInt(mLteRsrpBoost);
+ out.writeBoolean(mIsGsm);
+ out.writeBoolean(mUseOnlyRsrpForLteLevel);
+ out.writeIntArray(mLteRsrpThresholds);
}
/**
@@ -436,24 +331,24 @@ public class SignalStrength implements Parcelable {
}
/**
- * Fix {@link #isGsm} based on the signal strength data.
+ * Fix {@link #mIsGsm} based on the signal strength data.
*
* @hide
*/
public void fixType() {
- isGsm = getCdmaRelatedSignalStrength() == SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
+ mIsGsm = getCdmaRelatedSignalStrength() == SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
}
/**
* @param true - Gsm, Lte phones
* false - Cdma phones
*
- * Used by voice phone to set the isGsm
+ * Used by voice phone to set the mIsGsm
* flag
* @hide
*/
public void setGsm(boolean gsmFlag) {
- isGsm = gsmFlag;
+ mIsGsm = gsmFlag;
}
/**
@@ -480,6 +375,22 @@ public class SignalStrength implements Parcelable {
}
/**
+ * Sets the threshold array for determining the display level of LTE signal bar.
+ *
+ * @param lteRsrpThresholds int array for determining the display level.
+ *
+ * @hide
+ */
+ public void setLteRsrpThresholds(int[] lteRsrpThresholds) {
+ if ((lteRsrpThresholds == null)
+ || (lteRsrpThresholds.length != LTE_RSRP_THRESHOLDS_NUM)) {
+ Log.wtf(LOG_TAG, "setLteRsrpThresholds - lteRsrpThresholds is invalid.");
+ return;
+ }
+ System.arraycopy(lteRsrpThresholds, 0, mLteRsrpThresholds, 0, LTE_RSRP_THRESHOLDS_NUM);
+ }
+
+ /**
* Get the GSM Signal Strength, valid values are (0-31, 99) as defined in TS
* 27.007 8.5
*/
@@ -568,7 +479,7 @@ public class SignalStrength implements Parcelable {
* while 4 represents a very strong signal strength.
*/
public int getLevel() {
- int level = isGsm ? getGsmRelatedSignalStrength() : getCdmaRelatedSignalStrength();
+ int level = mIsGsm ? getGsmRelatedSignalStrength() : getCdmaRelatedSignalStrength();
if (DBG) log("getLevel=" + level);
return level;
}
@@ -580,15 +491,13 @@ public class SignalStrength implements Parcelable {
*/
public int getAsuLevel() {
int asuLevel = 0;
- if (isGsm) {
- if (getLteLevel() == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) {
- if (getTdScdmaLevel() == SIGNAL_STRENGTH_NONE_OR_UNKNOWN) {
- asuLevel = getGsmAsuLevel();
- } else {
- asuLevel = getTdScdmaAsuLevel();
- }
- } else {
+ if (mIsGsm) {
+ if (mLteRsrp != SignalStrength.INVALID) {
asuLevel = getLteAsuLevel();
+ } else if (mTdScdmaRscp != SignalStrength.INVALID) {
+ asuLevel = getTdScdmaAsuLevel();
+ } else {
+ asuLevel = getGsmAsuLevel();
}
} else {
int cdmaAsuLevel = getCdmaAsuLevel();
@@ -833,25 +742,18 @@ public class SignalStrength implements Parcelable {
*/
int rssiIconLevel = SIGNAL_STRENGTH_NONE_OR_UNKNOWN, rsrpIconLevel = -1, snrIconLevel = -1;
- int[] threshRsrp = Resources.getSystem().getIntArray(
- com.android.internal.R.array.config_lteDbmThresholds);
- if (threshRsrp.length != 6) {
- Log.wtf(LOG_TAG, "getLteLevel - config_lteDbmThresholds has invalid num of elements."
- + " Cannot evaluate RSRP signal.");
- } else {
- if (mLteRsrp > threshRsrp[5]) {
- rsrpIconLevel = -1;
- } else if (mLteRsrp >= (threshRsrp[4] - mLteRsrpBoost)) {
- rsrpIconLevel = SIGNAL_STRENGTH_GREAT;
- } else if (mLteRsrp >= (threshRsrp[3] - mLteRsrpBoost)) {
- rsrpIconLevel = SIGNAL_STRENGTH_GOOD;
- } else if (mLteRsrp >= (threshRsrp[2] - mLteRsrpBoost)) {
- rsrpIconLevel = SIGNAL_STRENGTH_MODERATE;
- } else if (mLteRsrp >= (threshRsrp[1] - mLteRsrpBoost)) {
- rsrpIconLevel = SIGNAL_STRENGTH_POOR;
- } else if (mLteRsrp >= threshRsrp[0]) {
- rsrpIconLevel = SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
- }
+ if (mLteRsrp > mLteRsrpThresholds[5]) {
+ rsrpIconLevel = -1;
+ } else if (mLteRsrp >= (mLteRsrpThresholds[4] - mLteRsrpBoost)) {
+ rsrpIconLevel = SIGNAL_STRENGTH_GREAT;
+ } else if (mLteRsrp >= (mLteRsrpThresholds[3] - mLteRsrpBoost)) {
+ rsrpIconLevel = SIGNAL_STRENGTH_GOOD;
+ } else if (mLteRsrp >= (mLteRsrpThresholds[2] - mLteRsrpBoost)) {
+ rsrpIconLevel = SIGNAL_STRENGTH_MODERATE;
+ } else if (mLteRsrp >= (mLteRsrpThresholds[1] - mLteRsrpBoost)) {
+ rsrpIconLevel = SIGNAL_STRENGTH_POOR;
+ } else if (mLteRsrp >= mLteRsrpThresholds[0]) {
+ rsrpIconLevel = SIGNAL_STRENGTH_NONE_OR_UNKNOWN;
}
if (useOnlyRsrpForLteLevel()) {
@@ -937,7 +839,7 @@ public class SignalStrength implements Parcelable {
* @return true if this is for GSM
*/
public boolean isGsm() {
- return this.isGsm;
+ return this.mIsGsm;
}
/**
@@ -1009,8 +911,8 @@ public class SignalStrength implements Parcelable {
+ (mEvdoDbm * primeNum) + (mEvdoEcio * primeNum) + (mEvdoSnr * primeNum)
+ (mLteSignalStrength * primeNum) + (mLteRsrp * primeNum)
+ (mLteRsrq * primeNum) + (mLteRssnr * primeNum) + (mLteCqi * primeNum)
- + (mLteRsrpBoost * primeNum) + (mTdScdmaRscp * primeNum) + (isGsm ? 1 : 0)
- + (mUseOnlyRsrpForLteLevel ? 1 : 0));
+ + (mLteRsrpBoost * primeNum) + (mTdScdmaRscp * primeNum) + (mIsGsm ? 1 : 0)
+ + (mUseOnlyRsrpForLteLevel ? 1 : 0) + (Arrays.hashCode(mLteRsrpThresholds)));
}
/**
@@ -1044,8 +946,9 @@ public class SignalStrength implements Parcelable {
&& mLteCqi == s.mLteCqi
&& mLteRsrpBoost == s.mLteRsrpBoost
&& mTdScdmaRscp == s.mTdScdmaRscp
- && isGsm == s.isGsm
- && mUseOnlyRsrpForLteLevel == s.mUseOnlyRsrpForLteLevel);
+ && mIsGsm == s.mIsGsm
+ && mUseOnlyRsrpForLteLevel == s.mUseOnlyRsrpForLteLevel
+ && Arrays.equals(mLteRsrpThresholds, s.mLteRsrpThresholds));
}
/**
@@ -1068,9 +971,10 @@ public class SignalStrength implements Parcelable {
+ " " + mLteCqi
+ " " + mLteRsrpBoost
+ " " + mTdScdmaRscp
- + " " + (isGsm ? "gsm|lte" : "cdma")
+ + " " + (mIsGsm ? "gsm|lte" : "cdma")
+ " " + (mUseOnlyRsrpForLteLevel ? "use_only_rsrp_for_lte_level" :
- "use_rsrp_and_rssnr_for_lte_level"));
+ "use_rsrp_and_rssnr_for_lte_level")
+ + " " + (Arrays.toString(mLteRsrpThresholds)));
}
/** Returns the signal strength related to GSM. */
@@ -1122,10 +1026,14 @@ public class SignalStrength implements Parcelable {
mLteRsrq = m.getInt("LteRsrq");
mLteRssnr = m.getInt("LteRssnr");
mLteCqi = m.getInt("LteCqi");
- mLteRsrpBoost = m.getInt("lteRsrpBoost");
+ mLteRsrpBoost = m.getInt("LteRsrpBoost");
mTdScdmaRscp = m.getInt("TdScdma");
- isGsm = m.getBoolean("isGsm");
- mUseOnlyRsrpForLteLevel = m.getBoolean("useOnlyRsrpForLteLevel");
+ mIsGsm = m.getBoolean("IsGsm");
+ mUseOnlyRsrpForLteLevel = m.getBoolean("UseOnlyRsrpForLteLevel");
+ ArrayList<Integer> lteRsrpThresholds = m.getIntegerArrayList("lteRsrpThresholds");
+ for (int i = 0; i < lteRsrpThresholds.size(); i++) {
+ mLteRsrpThresholds[i] = lteRsrpThresholds.get(i);
+ }
}
/**
@@ -1147,10 +1055,25 @@ public class SignalStrength implements Parcelable {
m.putInt("LteRsrq", mLteRsrq);
m.putInt("LteRssnr", mLteRssnr);
m.putInt("LteCqi", mLteCqi);
- m.putInt("lteRsrpBoost", mLteRsrpBoost);
+ m.putInt("LteRsrpBoost", mLteRsrpBoost);
m.putInt("TdScdma", mTdScdmaRscp);
- m.putBoolean("isGsm", isGsm);
- m.putBoolean("useOnlyRsrpForLteLevel", mUseOnlyRsrpForLteLevel);
+ m.putBoolean("IsGsm", mIsGsm);
+ m.putBoolean("UseOnlyRsrpForLteLevel", mUseOnlyRsrpForLteLevel);
+ ArrayList<Integer> lteRsrpThresholds = new ArrayList<Integer>();
+ for (int value : mLteRsrpThresholds) {
+ lteRsrpThresholds.add(value);
+ }
+ m.putIntegerArrayList("lteRsrpThresholds", lteRsrpThresholds);
+ }
+
+ /**
+ * Gets the default threshold array for determining the display level of LTE signal bar.
+ *
+ * @return int array for determining the display level.
+ */
+ private int[] getDefaultLteRsrpThresholds() {
+ return CarrierConfigManager.getDefaultConfig().getIntArray(
+ CarrierConfigManager.KEY_LTE_RSRP_THRESHOLDS_INT_ARRAY);
}
/**
diff --git a/android/telephony/SubscriptionInfo.java b/android/telephony/SubscriptionInfo.java
index 4e1c15fa..38408fe3 100644
--- a/android/telephony/SubscriptionInfo.java
+++ b/android/telephony/SubscriptionInfo.java
@@ -126,14 +126,31 @@ public class SubscriptionInfo implements Parcelable {
private UiccAccessRule[] mAccessRules;
/**
+ * The ID of the SIM card. It is the ICCID of the active profile for a UICC card and the EID
+ * for an eUICC card.
+ */
+ private String mCardId;
+
+ /**
+ * @hide
+ */
+ public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName,
+ CharSequence carrierName, int nameSource, int iconTint, String number, int roaming,
+ Bitmap icon, int mcc, int mnc, String countryIso) {
+ this(id, iccId, simSlotIndex, displayName, carrierName, nameSource, iconTint, number,
+ roaming, icon, mcc, mnc, countryIso, false /* isEmbedded */,
+ null /* accessRules */, null /* accessRules */);
+ }
+
+ /**
* @hide
*/
public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName,
CharSequence carrierName, int nameSource, int iconTint, String number, int roaming,
- Bitmap icon, int mcc, int mnc, String countryIso) {
+ Bitmap icon, int mcc, int mnc, String countryIso, boolean isEmbedded,
+ @Nullable UiccAccessRule[] accessRules) {
this(id, iccId, simSlotIndex, displayName, carrierName, nameSource, iconTint, number,
- roaming, icon, mcc, mnc, countryIso, false /* isEmbedded */,
- null /* accessRules */);
+ roaming, icon, mcc, mnc, countryIso, isEmbedded, accessRules, null /* cardId */);
}
/**
@@ -142,7 +159,7 @@ public class SubscriptionInfo implements Parcelable {
public SubscriptionInfo(int id, String iccId, int simSlotIndex, CharSequence displayName,
CharSequence carrierName, int nameSource, int iconTint, String number, int roaming,
Bitmap icon, int mcc, int mnc, String countryIso, boolean isEmbedded,
- @Nullable UiccAccessRule[] accessRules) {
+ @Nullable UiccAccessRule[] accessRules, String cardId) {
this.mId = id;
this.mIccId = iccId;
this.mSimSlotIndex = simSlotIndex;
@@ -158,6 +175,7 @@ public class SubscriptionInfo implements Parcelable {
this.mCountryIso = countryIso;
this.mIsEmbedded = isEmbedded;
this.mAccessRules = accessRules;
+ this.mCardId = cardId;
}
/**
@@ -387,6 +405,14 @@ public class SubscriptionInfo implements Parcelable {
return mAccessRules;
}
+ /**
+ * @return the ID of the SIM card which contains the subscription.
+ * @hide
+ */
+ public String getCardId() {
+ return this.mCardId;
+ }
+
public static final Parcelable.Creator<SubscriptionInfo> CREATOR = new Parcelable.Creator<SubscriptionInfo>() {
@Override
public SubscriptionInfo createFromParcel(Parcel source) {
@@ -405,10 +431,11 @@ public class SubscriptionInfo implements Parcelable {
Bitmap iconBitmap = Bitmap.CREATOR.createFromParcel(source);
boolean isEmbedded = source.readBoolean();
UiccAccessRule[] accessRules = source.createTypedArray(UiccAccessRule.CREATOR);
+ String cardId = source.readString();
return new SubscriptionInfo(id, iccId, simSlotIndex, displayName, carrierName,
nameSource, iconTint, number, dataRoaming, iconBitmap, mcc, mnc, countryIso,
- isEmbedded, accessRules);
+ isEmbedded, accessRules, cardId);
}
@Override
@@ -434,6 +461,7 @@ public class SubscriptionInfo implements Parcelable {
mIconBitmap.writeToParcel(dest, flags);
dest.writeBoolean(mIsEmbedded);
dest.writeTypedArray(mAccessRules, flags);
+ dest.writeString(mCardId);
}
@Override
@@ -459,11 +487,13 @@ public class SubscriptionInfo implements Parcelable {
@Override
public String toString() {
String iccIdToPrint = givePrintableIccid(mIccId);
+ String cardIdToPrint = givePrintableIccid(mCardId);
return "{id=" + mId + ", iccId=" + iccIdToPrint + " simSlotIndex=" + mSimSlotIndex
+ " displayName=" + mDisplayName + " carrierName=" + mCarrierName
+ " nameSource=" + mNameSource + " iconTint=" + mIconTint
+ " dataRoaming=" + mDataRoaming + " iconBitmap=" + mIconBitmap + " mcc " + mMcc
+ " mnc " + mMnc + " isEmbedded " + mIsEmbedded
- + " accessRules " + Arrays.toString(mAccessRules) + "}";
+ + " accessRules " + Arrays.toString(mAccessRules)
+ + " cardId=" + cardIdToPrint + "}";
}
}
diff --git a/android/telephony/SubscriptionManager.java b/android/telephony/SubscriptionManager.java
index 1e6abf22..debf43da 100644
--- a/android/telephony/SubscriptionManager.java
+++ b/android/telephony/SubscriptionManager.java
@@ -16,31 +16,44 @@
package android.telephony;
+import static android.net.NetworkPolicyManager.OVERRIDE_CONGESTED;
+import static android.net.NetworkPolicyManager.OVERRIDE_UNMETERED;
+
+import android.annotation.DurationMillisLong;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
-import android.annotation.SystemApi;
import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
import android.annotation.SystemService;
+import android.app.BroadcastOptions;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.net.INetworkPolicyManager;
+import android.net.NetworkCapabilities;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
import android.util.DisplayMetrics;
+
import com.android.internal.telephony.IOnSubscriptionsChangedListener;
import com.android.internal.telephony.ISub;
import com.android.internal.telephony.ITelephonyRegistry;
import com.android.internal.telephony.PhoneConstants;
+
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.concurrent.TimeUnit;
/**
* SubscriptionManager is the application interface to SubscriptionController
@@ -271,6 +284,14 @@ public class SubscriptionManager {
public static final String IS_EMBEDDED = "is_embedded";
/**
+ * TelephonyProvider column name for SIM card identifier. For UICC card it is the ICCID of the
+ * current enabled profile on the card, while for eUICC card it is the EID of the card.
+ * <P>Type: TEXT (String)</P>
+ * @hide
+ */
+ public static final String CARD_ID = "card_id";
+
+ /**
* TelephonyProvider column name for the encoded {@link UiccAccessRule}s from
* {@link UiccAccessRule#encodeRules}. Only present if {@link #IS_EMBEDDED} is 1.
* <p>TYPE: BLOB
@@ -430,6 +451,55 @@ public class SubscriptionManager {
= "android.telephony.action.DEFAULT_SMS_SUBSCRIPTION_CHANGED";
/**
+ * Activity Action: Display UI for managing the billing relationship plans
+ * between a carrier and a specific subscriber.
+ * <p>
+ * Carrier apps are encouraged to implement this activity, and the OS will
+ * provide an affordance to quickly enter this activity, typically via
+ * Settings. This affordance will only be shown when the carrier app is
+ * actively providing subscription plan information via
+ * {@link #setSubscriptionPlans(int, List)}.
+ * <p>
+ * Contains {@link #EXTRA_SUBSCRIPTION_INDEX} to indicate which subscription
+ * the user is interested in.
+ */
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ @SystemApi
+ public static final String ACTION_MANAGE_SUBSCRIPTION_PLANS
+ = "android.telephony.action.MANAGE_SUBSCRIPTION_PLANS";
+
+ /**
+ * Broadcast Action: Request a refresh of the billing relationship plans
+ * between a carrier and a specific subscriber.
+ * <p>
+ * Carrier apps are encouraged to implement this receiver, and the OS will
+ * provide an affordance to request a refresh. This affordance will only be
+ * shown when the carrier app is actively providing subscription plan
+ * information via {@link #setSubscriptionPlans(int, List)}.
+ * <p>
+ * Contains {@link #EXTRA_SUBSCRIPTION_INDEX} to indicate which subscription
+ * the user is interested in.
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @SystemApi
+ public static final String ACTION_REFRESH_SUBSCRIPTION_PLANS
+ = "android.telephony.action.REFRESH_SUBSCRIPTION_PLANS";
+
+ /**
+ * Broadcast Action: The billing relationship plans between a carrier and a
+ * specific subscriber has changed.
+ * <p>
+ * Contains {@link #EXTRA_SUBSCRIPTION_INDEX} to indicate which subscription
+ * changed.
+ *
+ * @hide
+ */
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ @RequiresPermission(android.Manifest.permission.MANAGE_SUBSCRIPTION_PLANS)
+ public static final String ACTION_SUBSCRIPTION_PLANS_CHANGED
+ = "android.telephony.action.SUBSCRIPTION_PLANS_CHANGED";
+
+ /**
* Integer extra used with {@link #ACTION_DEFAULT_SUBSCRIPTION_CHANGED} and
* {@link #ACTION_DEFAULT_SMS_SUBSCRIPTION_CHANGED} to indicate the subscription
* which has changed.
@@ -437,6 +507,7 @@ public class SubscriptionManager {
public static final String EXTRA_SUBSCRIPTION_INDEX = "android.telephony.extra.SUBSCRIPTION_INDEX";
private final Context mContext;
+ private INetworkPolicyManager mNetworkPolicy;
/**
* A listener class for monitoring changes to {@link SubscriptionInfo} records.
@@ -515,16 +586,21 @@ public class SubscriptionManager {
}
/**
- * Get an instance of the SubscriptionManager from the Context.
- * This invokes {@link android.content.Context#getSystemService
- * Context.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE)}.
- *
- * @param context to use.
- * @return SubscriptionManager instance
+ * @deprecated developers should always obtain references directly from
+ * {@link Context#getSystemService(Class)}.
*/
+ @Deprecated
public static SubscriptionManager from(Context context) {
- return (SubscriptionManager) context.getSystemService(
- Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+ return (SubscriptionManager) context
+ .getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
+ }
+
+ private final INetworkPolicyManager getNetworkPolicy() {
+ if (mNetworkPolicy == null) {
+ mNetworkPolicy = INetworkPolicyManager.Stub
+ .asInterface(ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
+ }
+ return mNetworkPolicy;
}
/**
@@ -1612,21 +1688,18 @@ public class SubscriptionManager {
* This method is only accessible to the following narrow set of apps:
* <ul>
* <li>The carrier app for this subscriberId, as determined by
- * {@link TelephonyManager#hasCarrierPrivileges(int)}.
+ * {@link TelephonyManager#hasCarrierPrivileges()}.
* <li>The carrier app explicitly delegated access through
* {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
* </ul>
*
* @param subId the subscriber this relationship applies to
- * @hide
*/
@SystemApi
public @NonNull List<SubscriptionPlan> getSubscriptionPlans(int subId) {
- final INetworkPolicyManager npm = INetworkPolicyManager.Stub
- .asInterface(ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
try {
SubscriptionPlan[] subscriptionPlans =
- npm.getSubscriptionPlans(subId, mContext.getOpPackageName());
+ getNetworkPolicy().getSubscriptionPlans(subId, mContext.getOpPackageName());
return subscriptionPlans == null
? Collections.emptyList() : Arrays.asList(subscriptionPlans);
} catch (RemoteException e) {
@@ -1641,7 +1714,7 @@ public class SubscriptionManager {
* This method is only accessible to the following narrow set of apps:
* <ul>
* <li>The carrier app for this subscriberId, as determined by
- * {@link TelephonyManager#hasCarrierPrivileges(int)}.
+ * {@link TelephonyManager#hasCarrierPrivileges()}.
* <li>The carrier app explicitly delegated access through
* {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
* </ul>
@@ -1650,17 +1723,173 @@ public class SubscriptionManager {
* @param plans the list of plans. The first plan is always the primary and
* most important plan. Any additional plans are secondary and
* may not be displayed or used by decision making logic.
- * @hide
*/
@SystemApi
public void setSubscriptionPlans(int subId, @NonNull List<SubscriptionPlan> plans) {
- final INetworkPolicyManager npm = INetworkPolicyManager.Stub
- .asInterface(ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
try {
- npm.setSubscriptionPlans(subId, plans.toArray(new SubscriptionPlan[plans.size()]),
- mContext.getOpPackageName());
+ getNetworkPolicy().setSubscriptionPlans(subId,
+ plans.toArray(new SubscriptionPlan[plans.size()]), mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /** @hide */
+ private String getSubscriptionPlansOwner(int subId) {
+ try {
+ return getNetworkPolicy().getSubscriptionPlansOwner(subId);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
+
+ /**
+ * Temporarily override the billing relationship plan between a carrier and
+ * a specific subscriber to be considered unmetered. This will be reflected
+ * to apps via {@link NetworkCapabilities#NET_CAPABILITY_NOT_METERED}.
+ * <p>
+ * This method is only accessible to the following narrow set of apps:
+ * <ul>
+ * <li>The carrier app for this subscriberId, as determined by
+ * {@link TelephonyManager#hasCarrierPrivileges()}.
+ * <li>The carrier app explicitly delegated access through
+ * {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
+ * </ul>
+ *
+ * @param subId the subscriber this override applies to.
+ * @param overrideUnmetered set if the billing relationship should be
+ * considered unmetered.
+ * @param timeoutMillis the timeout after which the requested override will
+ * be automatically cleared, or {@code 0} to leave in the
+ * requested state until explicitly cleared, or the next reboot,
+ * whichever happens first.
+ */
+ @SystemApi
+ public void setSubscriptionOverrideUnmetered(int subId, boolean overrideUnmetered,
+ @DurationMillisLong long timeoutMillis) {
+ try {
+ final int overrideValue = overrideUnmetered ? OVERRIDE_UNMETERED : 0;
+ mNetworkPolicy.setSubscriptionOverride(subId, OVERRIDE_UNMETERED, overrideValue,
+ timeoutMillis, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Temporarily override the billing relationship plan between a carrier and
+ * a specific subscriber to be considered congested. This will cause the
+ * device to delay certain network requests when possible, such as developer
+ * jobs that are willing to run in a flexible time window.
+ * <p>
+ * This method is only accessible to the following narrow set of apps:
+ * <ul>
+ * <li>The carrier app for this subscriberId, as determined by
+ * {@link TelephonyManager#hasCarrierPrivileges()}.
+ * <li>The carrier app explicitly delegated access through
+ * {@link CarrierConfigManager#KEY_CONFIG_PLANS_PACKAGE_OVERRIDE_STRING}.
+ * </ul>
+ *
+ * @param subId the subscriber this override applies to.
+ * @param overrideCongested set if the subscription should be considered
+ * congested.
+ * @param timeoutMillis the timeout after which the requested override will
+ * be automatically cleared, or {@code 0} to leave in the
+ * requested state until explicitly cleared, or the next reboot,
+ * whichever happens first.
+ */
+ @SystemApi
+ public void setSubscriptionOverrideCongested(int subId, boolean overrideCongested,
+ @DurationMillisLong long timeoutMillis) {
+ try {
+ final int overrideValue = overrideCongested ? OVERRIDE_CONGESTED : 0;
+ mNetworkPolicy.setSubscriptionOverride(subId, OVERRIDE_CONGESTED, overrideValue,
+ timeoutMillis, mContext.getOpPackageName());
+ } catch (RemoteException e) {
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Create an {@link Intent} that can be launched towards the carrier app
+ * that is currently defining the billing relationship plan through
+ * {@link #setSubscriptionPlans(int, List)}.
+ *
+ * @return ready to launch Intent targeted towards the carrier app, or
+ * {@code null} if no carrier app is defined, or if the defined
+ * carrier app provides no management activity.
+ * @hide
+ */
+ public @Nullable Intent createManageSubscriptionIntent(int subId) {
+ // Bail if no owner
+ final String owner = getSubscriptionPlansOwner(subId);
+ if (owner == null) return null;
+
+ // Bail if no plans
+ final List<SubscriptionPlan> plans = getSubscriptionPlans(subId);
+ if (plans.isEmpty()) return null;
+
+ final Intent intent = new Intent(ACTION_MANAGE_SUBSCRIPTION_PLANS);
+ intent.setPackage(owner);
+ intent.putExtra(EXTRA_SUBSCRIPTION_INDEX, subId);
+
+ // Bail if not implemented
+ if (mContext.getPackageManager().queryIntentActivities(intent,
+ PackageManager.MATCH_DEFAULT_ONLY).isEmpty()) {
+ return null;
+ }
+
+ return intent;
+ }
+
+ /** @hide */
+ private @Nullable Intent createRefreshSubscriptionIntent(int subId) {
+ // Bail if no owner
+ final String owner = getSubscriptionPlansOwner(subId);
+ if (owner == null) return null;
+
+ // Bail if no plans
+ final List<SubscriptionPlan> plans = getSubscriptionPlans(subId);
+ if (plans.isEmpty()) return null;
+
+ final Intent intent = new Intent(ACTION_REFRESH_SUBSCRIPTION_PLANS);
+ intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ intent.setPackage(owner);
+ intent.putExtra(EXTRA_SUBSCRIPTION_INDEX, subId);
+
+ // Bail if not implemented
+ if (mContext.getPackageManager().queryBroadcastReceivers(intent, 0).isEmpty()) {
+ return null;
+ }
+
+ return intent;
+ }
+
+ /**
+ * Check if there is a carrier app that is currently defining the billing
+ * relationship plan through {@link #setSubscriptionPlans(int, List)} that
+ * supports refreshing of subscription plans.
+ *
+ * @hide
+ */
+ public boolean isSubscriptionPlansRefreshSupported(int subId) {
+ return createRefreshSubscriptionIntent(subId) != null;
+ }
+
+ /**
+ * Request that the carrier app that is currently defining the billing
+ * relationship plan through {@link #setSubscriptionPlans(int, List)}
+ * refresh its subscription plans.
+ * <p>
+ * If the app is able to successfully update the plans, you'll expect to
+ * receive the {@link #ACTION_SUBSCRIPTION_PLANS_CHANGED} broadcast.
+ *
+ * @hide
+ */
+ public void requestSubscriptionPlansRefresh(int subId) {
+ final Intent intent = createRefreshSubscriptionIntent(subId);
+ final BroadcastOptions options = BroadcastOptions.makeBasic();
+ options.setTemporaryAppWhitelistDuration(TimeUnit.MINUTES.toMillis(1));
+ mContext.sendBroadcast(intent, null, options.toBundle());
+ }
}
diff --git a/android/telephony/SubscriptionPlan.java b/android/telephony/SubscriptionPlan.java
index 265e3e7c..94116521 100644
--- a/android/telephony/SubscriptionPlan.java
+++ b/android/telephony/SubscriptionPlan.java
@@ -43,7 +43,6 @@ import java.util.Iterator;
*
* @see SubscriptionManager#setSubscriptionPlans(int, java.util.List)
* @see SubscriptionManager#getSubscriptionPlans(int)
- * @hide
*/
@SystemApi
public final class SubscriptionPlan implements Parcelable {
diff --git a/android/telephony/TelephonyManager.java b/android/telephony/TelephonyManager.java
index af5b1908..0a6d9604 100644
--- a/android/telephony/TelephonyManager.java
+++ b/android/telephony/TelephonyManager.java
@@ -22,8 +22,8 @@ import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
import android.annotation.SdkConstant;
-import android.annotation.SuppressLint;
import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.SystemService;
import android.annotation.WorkerThread;
@@ -53,6 +53,7 @@ import android.util.Log;
import com.android.ims.internal.IImsMMTelFeature;
import com.android.ims.internal.IImsRcsFeature;
+import com.android.ims.internal.IImsRegistration;
import com.android.ims.internal.IImsServiceFeatureCallback;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telecom.ITelecomService;
@@ -60,7 +61,6 @@ import com.android.internal.telephony.CellNetworkScanResult;
import com.android.internal.telephony.IPhoneSubInfo;
import com.android.internal.telephony.ITelephony;
import com.android.internal.telephony.ITelephonyRegistry;
-import com.android.internal.telephony.OperatorInfo;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.RILConstants;
import com.android.internal.telephony.TelephonyProperties;
@@ -831,6 +831,17 @@ public class TelephonyManager {
"android.telephony.event.EVENT_HANDOVER_VIDEO_FROM_WIFI_TO_LTE";
/**
+ * {@link android.telecom.Connection} event used to indicate that an IMS call has be
+ * successfully handed over from LTE to WIFI.
+ * <p>
+ * Sent via {@link android.telecom.Connection#sendConnectionEvent(String, Bundle)}.
+ * The {@link Bundle} parameter is expected to be null when this connection event is used.
+ * @hide
+ */
+ public static final String EVENT_HANDOVER_VIDEO_FROM_LTE_TO_WIFI =
+ "android.telephony.event.EVENT_HANDOVER_VIDEO_FROM_LTE_TO_WIFI";
+
+ /**
* {@link android.telecom.Connection} event used to indicate that an IMS call failed to be
* handed over from LTE to WIFI.
* <p>
@@ -1012,8 +1023,8 @@ public class TelephonyManager {
/**
* An int extra used with {@link #ACTION_SUBSCRIPTION_CARRIER_IDENTITY_CHANGED} which indicates
- * the updated carrier id {@link TelephonyManager#getSubscriptionCarrierId()} of the current
- * subscription.
+ * the updated carrier id {@link TelephonyManager#getAndroidCarrierIdForSubscription()} of
+ * the current subscription.
* <p>Will be {@link TelephonyManager#UNKNOWN_CARRIER_ID} if the subscription is unavailable or
* the carrier cannot be identified.
*/
@@ -2110,6 +2121,110 @@ public class TelephonyManager {
* carrier restrictions.
*/
public static final int SIM_STATE_CARD_RESTRICTED = 9;
+ /**
+ * SIM card state: Loaded: SIM card applications have been loaded
+ * @hide
+ */
+ @SystemApi
+ public static final int SIM_STATE_LOADED = 10;
+ /**
+ * SIM card state: SIM Card is present
+ * @hide
+ */
+ @SystemApi
+ public static final int SIM_STATE_PRESENT = 11;
+
+ /**
+ * Extra included in {@link #ACTION_SIM_CARD_STATE_CHANGED} and
+ * {@link #ACTION_SIM_APPLICATION_STATE_CHANGED} to indicate the card/application state.
+ *
+ * @hide
+ */
+ @SystemApi
+ public static final String EXTRA_SIM_STATE = "android.telephony.extra.SIM_STATE";
+
+ /**
+ * Broadcast Action: The sim card state has changed.
+ * The intent will have the following extra values:</p>
+ * <dl>
+ * <dt>{@link #EXTRA_SIM_STATE}</dt>
+ * <dd>The sim card state. One of:
+ * <dl>
+ * <dt>{@link #SIM_STATE_ABSENT}</dt>
+ * <dd>SIM card not found</dd>
+ * <dt>{@link #SIM_STATE_CARD_IO_ERROR}</dt>
+ * <dd>SIM card IO error</dd>
+ * <dt>{@link #SIM_STATE_CARD_RESTRICTED}</dt>
+ * <dd>SIM card is restricted</dd>
+ * <dt>{@link #SIM_STATE_PRESENT}</dt>
+ * <dd>SIM card is present</dd>
+ * </dl>
+ * </dd>
+ * </dl>
+ *
+ * <p class="note">Requires the READ_PRIVILEGED_PHONE_STATE permission.
+ *
+ * <p class="note">The current state can also be queried using {@link #getSimCardState()}.
+ *
+ * <p class="note">This is a protected intent that can only be sent by the system.
+ * @hide
+ */
+ @SystemApi
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_SIM_CARD_STATE_CHANGED =
+ "android.telephony.action.SIM_CARD_STATE_CHANGED";
+
+ /**
+ * Broadcast Action: The sim application state has changed.
+ * The intent will have the following extra values:</p>
+ * <dl>
+ * <dt>{@link #EXTRA_SIM_STATE}</dt>
+ * <dd>The sim application state. One of:
+ * <dl>
+ * <dt>{@link #SIM_STATE_NOT_READY}</dt>
+ * <dd>SIM card applications not ready</dd>
+ * <dt>{@link #SIM_STATE_PIN_REQUIRED}</dt>
+ * <dd>SIM card PIN locked</dd>
+ * <dt>{@link #SIM_STATE_PUK_REQUIRED}</dt>
+ * <dd>SIM card PUK locked</dd>
+ * <dt>{@link #SIM_STATE_NETWORK_LOCKED}</dt>
+ * <dd>SIM card network locked</dd>
+ * <dt>{@link #SIM_STATE_PERM_DISABLED}</dt>
+ * <dd>SIM card permanently disabled due to PUK failures</dd>
+ * <dt>{@link #SIM_STATE_LOADED}</dt>
+ * <dd>SIM card data loaded</dd>
+ * </dl>
+ * </dd>
+ * </dl>
+ *
+ * <p class="note">Requires the READ_PRIVILEGED_PHONE_STATE permission.
+ *
+ * <p class="note">The current state can also be queried using
+ * {@link #getSimApplicationState()}.
+ *
+ * <p class="note">This is a protected intent that can only be sent by the system.
+ * @hide
+ */
+ @SystemApi
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_SIM_APPLICATION_STATE_CHANGED =
+ "android.telephony.action.SIM_APPLICATION_STATE_CHANGED";
+
+ /**
+ * Broadcast Action: Status of the SIM slots on the device has changed.
+ *
+ * <p class="note">Requires the READ_PRIVILEGED_PHONE_STATE permission.
+ *
+ * <p class="note">The status can be queried using
+ * {@link #getUiccSlotsInfo()}
+ *
+ * <p class="note">This is a protected intent that can only be sent by the system.
+ * @hide
+ */
+ @SystemApi
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_SIM_SLOT_STATUS_CHANGED =
+ "android.telephony.action.SIM_SLOT_STATUS_CHANGED";
/**
* @return true if a ICC card is present
@@ -2156,6 +2271,14 @@ public class TelephonyManager {
* @see #SIM_STATE_CARD_RESTRICTED
*/
public int getSimState() {
+ int simState = getSimStateIncludingLoaded();
+ if (simState == SIM_STATE_LOADED) {
+ simState = SIM_STATE_READY;
+ }
+ return simState;
+ }
+
+ private int getSimStateIncludingLoaded() {
int slotIndex = getSlotIndex();
// slotIndex may be invalid due to sim being absent. In that case query all slots to get
// sim state
@@ -2174,7 +2297,63 @@ public class TelephonyManager {
"state as absent");
return SIM_STATE_ABSENT;
}
- return getSimState(slotIndex);
+ return SubscriptionManager.getSimStateForSlotIndex(slotIndex);
+ }
+
+ /**
+ * Returns a constant indicating the state of the default SIM card.
+ *
+ * @see #SIM_STATE_UNKNOWN
+ * @see #SIM_STATE_ABSENT
+ * @see #SIM_STATE_CARD_IO_ERROR
+ * @see #SIM_STATE_CARD_RESTRICTED
+ * @see #SIM_STATE_PRESENT
+ *
+ * @hide
+ */
+ @SystemApi
+ public int getSimCardState() {
+ int simCardState = getSimState();
+ switch (simCardState) {
+ case SIM_STATE_UNKNOWN:
+ case SIM_STATE_ABSENT:
+ case SIM_STATE_CARD_IO_ERROR:
+ case SIM_STATE_CARD_RESTRICTED:
+ return simCardState;
+ default:
+ return SIM_STATE_PRESENT;
+ }
+ }
+
+ /**
+ * Returns a constant indicating the state of the card applications on the default SIM card.
+ *
+ * @see #SIM_STATE_UNKNOWN
+ * @see #SIM_STATE_PIN_REQUIRED
+ * @see #SIM_STATE_PUK_REQUIRED
+ * @see #SIM_STATE_NETWORK_LOCKED
+ * @see #SIM_STATE_NOT_READY
+ * @see #SIM_STATE_PERM_DISABLED
+ * @see #SIM_STATE_LOADED
+ *
+ * @hide
+ */
+ @SystemApi
+ public int getSimApplicationState() {
+ int simApplicationState = getSimStateIncludingLoaded();
+ switch (simApplicationState) {
+ case SIM_STATE_UNKNOWN:
+ case SIM_STATE_ABSENT:
+ case SIM_STATE_CARD_IO_ERROR:
+ case SIM_STATE_CARD_RESTRICTED:
+ return SIM_STATE_UNKNOWN;
+ case SIM_STATE_READY:
+ // Ready is not a valid state anymore. The state that is broadcast goes from
+ // NOT_READY to either LOCKED or LOADED.
+ return SIM_STATE_NOT_READY;
+ default:
+ return simApplicationState;
+ }
}
/**
@@ -2195,6 +2374,9 @@ public class TelephonyManager {
*/
public int getSimState(int slotIndex) {
int simState = SubscriptionManager.getSimStateForSlotIndex(slotIndex);
+ if (simState == SIM_STATE_LOADED) {
+ simState = SIM_STATE_READY;
+ }
return simState;
}
@@ -2416,6 +2598,53 @@ public class TelephonyManager {
}
}
+ /**
+ * Gets all the UICC slots.
+ *
+ * @return UiccSlotInfo array.
+ *
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)
+ public UiccSlotInfo[] getUiccSlotsInfo() {
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony == null) {
+ return null;
+ }
+ return telephony.getUiccSlotsInfo();
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ /**
+ * Map logicalSlot to physicalSlot, and activate the physicalSlot if it is inactive. For
+ * example, passing the physicalSlots array [1, 0] means mapping the first item 1, which is
+ * physical slot index 1, to the logical slot 0; and mapping the second item 0, which is
+ * physical slot index 0, to the logical slot 1. The index of the array means the index of the
+ * logical slots.
+ *
+ * @param physicalSlots Index i in the array representing physical slot for phone i. The array
+ * size should be same as {@link #getPhoneCount()}.
+ * @return boolean Return true if the switch succeeds, false if the switch fails.
+ * @hide
+ */
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
+ public boolean switchSlots(int[] physicalSlots) {
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony == null) {
+ return false;
+ }
+ return telephony.switchSlots(physicalSlots);
+ } catch (RemoteException e) {
+ return false;
+ }
+ }
+
//
//
// Subscriber Info
@@ -2503,6 +2732,33 @@ public class TelephonyManager {
}
}
+ /**
+ * Resets the Carrier Keys in the database. This involves 2 steps:
+ * 1. Delete the keys from the database.
+ * 2. Send an intent to download new Certificates.
+ * <p>
+ * Requires Permission:
+ * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
+ * @hide
+ */
+ public void resetCarrierKeysForImsiEncryption() {
+ try {
+ IPhoneSubInfo info = getSubscriberInfo();
+ if (info == null) {
+ throw new RuntimeException("IMSI error: Subscriber Info is null");
+ }
+ int subId = getSubId(SubscriptionManager.getDefaultDataSubscriptionId());
+ info.resetCarrierKeysForImsiEncryption(subId, mContext.getOpPackageName());
+ } catch (RemoteException ex) {
+ Rlog.e(TAG, "getCarrierInfoForImsiEncryption RemoteException" + ex);
+ throw new RuntimeException("IMSI error: Remote Exception");
+ } catch (NullPointerException ex) {
+ // This could happen before phone restarts due to crashing
+ Rlog.e(TAG, "getCarrierInfoForImsiEncryption NullPointerException" + ex);
+ throw new RuntimeException("IMSI error: Null Pointer exception");
+ }
+ }
+
/**
* @param keyAvailability bitmask that defines the availabilty of keys for a type.
* @param keyType the key type which is being checked. (WLAN, EPDG)
@@ -2538,7 +2794,7 @@ public class TelephonyManager {
* device keystore.
* <p>
* Requires Permission:
- * {@link android.Manifest.permission#READ_PHONE_STATE READ_PHONE_STATE}
+ * {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
* @param imsiEncryptionInfo which includes the Key Type, the Public Key
* (java.security.PublicKey) and the Key Identifier.and the Key Identifier.
* The keyIdentifier Attribute value pair that helps a server locate
@@ -4921,6 +5177,25 @@ public class TelephonyManager {
}
/**
+ * @return the {@IImsRegistration} interface that corresponds with the slot index and feature.
+ * @param slotIndex The SIM slot corresponding to the ImsService ImsRegistration is active for.
+ * @param feature An integer indicating the feature that we wish to get the ImsRegistration for.
+ * Corresponds to features defined in ImsFeature.
+ * @hide
+ */
+ public @Nullable IImsRegistration getImsRegistration(int slotIndex, int feature) {
+ try {
+ ITelephony telephony = getITelephony();
+ if (telephony != null) {
+ return telephony.getImsRegistration(slotIndex, feature);
+ }
+ } catch (RemoteException e) {
+ Rlog.e(TAG, "getImsRegistration, RemoteException: " + e.getMessage());
+ }
+ return null;
+ }
+
+ /**
* Set IMS registration state
*
* @param Registration state
@@ -6253,8 +6528,10 @@ public class TelephonyManager {
* <p>Requires Permission:
* {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
*
- * @hide
+ * {@hide}
**/
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
public void setSimPowerState(int state) {
setSimPowerStateForSlot(getSlotIndex(), state);
}
@@ -6273,8 +6550,10 @@ public class TelephonyManager {
* <p>Requires Permission:
* {@link android.Manifest.permission#MODIFY_PHONE_STATE MODIFY_PHONE_STATE}
*
- * @hide
+ * {@hide}
**/
+ @SystemApi
+ @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE)
public void setSimPowerStateForSlot(int slotIndex, int state) {
try {
ITelephony telephony = getITelephony();
@@ -6750,14 +7029,19 @@ 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.
+ * <p>To recognize a carrier (including MVNO) as a first-class identity, Android assigns each
+ * carrier with a canonical integer a.k.a. android carrier id. The Android carrier ID is an
+ * Android platform-wide identifier for a carrier. AOSP maintains carrier ID assignments in
+ * <a href="https://android.googlesource.com/platform/packages/providers/TelephonyProvider/+/master/assets/carrier_list.textpb">here</a>
+ *
+ * <p>Apps which have carrier-specific configurations or business logic can use the carrier id
+ * as an Android platform-wide identifier for carriers.
*
* @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() {
+ public int getAndroidCarrierIdForSubscription() {
try {
ITelephony service = getITelephony();
return service.getSubscriptionCarrierId(getSubId());
@@ -6773,17 +7057,18 @@ public class TelephonyManager {
/**
* 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>Carrier name is a user-facing name of carrier id
+ * {@link #getAndroidCarrierIdForSubscription()}, 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 #getAndroidCarrierIdForSubscription()} 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() {
+ public CharSequence getAndroidCarrierNameForSubscription() {
try {
ITelephony service = getITelephony();
return service.getSubscriptionCarrierName(getSubId());
diff --git a/android/telephony/UiccAccessRule.java b/android/telephony/UiccAccessRule.java
index e42a7583..39372019 100644
--- a/android/telephony/UiccAccessRule.java
+++ b/android/telephony/UiccAccessRule.java
@@ -32,6 +32,7 @@ import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
+import java.util.Objects;
/**
* Describes a single UICC access rule according to the GlobalPlatform Secure Element Access Control
@@ -205,6 +206,21 @@ public final class UiccAccessRule implements Parcelable {
}
@Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ UiccAccessRule that = (UiccAccessRule) obj;
+ return Arrays.equals(mCertificateHash, that.mCertificateHash)
+ && Objects.equals(mPackageName, that.mPackageName)
+ && mAccessType == that.mAccessType;
+ }
+
+ @Override
public String toString() {
return "cert: " + IccUtils.bytesToHexString(mCertificateHash) + " pkg: " +
mPackageName + " access: " + mAccessType;
diff --git a/android/telephony/UiccSlotInfo.java b/android/telephony/UiccSlotInfo.java
new file mode 100644
index 00000000..0b3cbad0
--- /dev/null
+++ b/android/telephony/UiccSlotInfo.java
@@ -0,0 +1,158 @@
+/*
+ * 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.telephony;
+
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+import android.annotation.IntDef;
+
+/**
+ * Class for the information of a UICC slot.
+ * @hide
+ */
+@SystemApi
+public class UiccSlotInfo implements Parcelable {
+ /**
+ * Card state.
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "CARD_STATE_INFO_" }, value = {
+ CARD_STATE_INFO_ABSENT,
+ CARD_STATE_INFO_PRESENT,
+ CARD_STATE_INFO_ERROR,
+ CARD_STATE_INFO_RESTRICTED
+ })
+ public @interface CardStateInfo {}
+
+ /** Card state absent. */
+ public static final int CARD_STATE_INFO_ABSENT = 1;
+
+ /** Card state present. */
+ public static final int CARD_STATE_INFO_PRESENT = 2;
+
+ /** Card state error. */
+ public static final int CARD_STATE_INFO_ERROR = 3;
+
+ /** Card state restricted. */
+ public static final int CARD_STATE_INFO_RESTRICTED = 4;
+
+ public final boolean isActive;
+ public final boolean isEuicc;
+ public final String cardId;
+ public final @CardStateInfo int cardStateInfo;
+
+ public static final Creator<UiccSlotInfo> CREATOR = new Creator<UiccSlotInfo>() {
+ @Override
+ public UiccSlotInfo createFromParcel(Parcel in) {
+ return new UiccSlotInfo(in);
+ }
+
+ @Override
+ public UiccSlotInfo[] newArray(int size) {
+ return new UiccSlotInfo[size];
+ }
+ };
+
+ private UiccSlotInfo(Parcel in) {
+ isActive = in.readByte() != 0;
+ isEuicc = in.readByte() != 0;
+ cardId = in.readString();
+ cardStateInfo = in.readInt();
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeByte((byte) (isActive ? 1 : 0));
+ dest.writeByte((byte) (isEuicc ? 1 : 0));
+ dest.writeString(cardId);
+ dest.writeInt(cardStateInfo);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public UiccSlotInfo(boolean isActive, boolean isEuicc, String cardId,
+ @CardStateInfo int cardStateInfo) {
+ this.isActive = isActive;
+ this.isEuicc = isEuicc;
+ this.cardId = cardId;
+ this.cardStateInfo = cardStateInfo;
+ }
+
+ public boolean getIsActive() {
+ return isActive;
+ }
+
+ public boolean getIsEuicc() {
+ return isEuicc;
+ }
+
+ public String getCardId() {
+ return cardId;
+ }
+
+ @CardStateInfo
+ public int getCardStateInfo() {
+ return cardStateInfo;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ UiccSlotInfo that = (UiccSlotInfo) obj;
+ return (isActive == that.isActive)
+ && (isEuicc == that.isEuicc)
+ && (cardId == that.cardId)
+ && (cardStateInfo == that.cardStateInfo);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + (isActive ? 1 : 0);
+ result = 31 * result + (isEuicc ? 1 : 0);
+ result = 31 * result + Objects.hashCode(cardId);
+ result = 31 * result + cardStateInfo;
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "UiccSlotInfo (isActive="
+ + isActive
+ + ", isEuicc="
+ + isEuicc
+ + ", cardId="
+ + cardId
+ + ", cardState="
+ + cardStateInfo
+ + ")";
+ }
+}
diff --git a/android/telephony/VisualVoicemailSmsFilterSettings.java b/android/telephony/VisualVoicemailSmsFilterSettings.java
index 8ed96a3a..7eeb1ce7 100644
--- a/android/telephony/VisualVoicemailSmsFilterSettings.java
+++ b/android/telephony/VisualVoicemailSmsFilterSettings.java
@@ -15,12 +15,10 @@
*/
package android.telephony;
-import android.content.Context;
import android.os.Parcel;
import android.os.Parcelable;
-
-import android.telecom.PhoneAccountHandle;
import android.telephony.VisualVoicemailService.VisualVoicemailTask;
+
import java.util.Collections;
import java.util.List;
@@ -75,6 +73,7 @@ public final class VisualVoicemailSmsFilterSettings implements Parcelable {
private String mClientPrefix = DEFAULT_CLIENT_PREFIX;
private List<String> mOriginatingNumbers = DEFAULT_ORIGINATING_NUMBERS;
private int mDestinationPort = DEFAULT_DESTINATION_PORT;
+ private String mPackageName;
public VisualVoicemailSmsFilterSettings build() {
return new VisualVoicemailSmsFilterSettings(this);
@@ -116,6 +115,15 @@ public final class VisualVoicemailSmsFilterSettings implements Parcelable {
return this;
}
+ /**
+ * The package that registered this filter.
+ *
+ * @hide
+ */
+ public Builder setPackageName(String packageName) {
+ mPackageName = packageName;
+ return this;
+ }
}
/**
@@ -138,12 +146,20 @@ public final class VisualVoicemailSmsFilterSettings implements Parcelable {
public final int destinationPort;
/**
+ * The package that registered this filter.
+ *
+ * @hide
+ */
+ public final String packageName;
+
+ /**
* Use {@link Builder} to construct
*/
private VisualVoicemailSmsFilterSettings(Builder builder) {
clientPrefix = builder.mClientPrefix;
originatingNumbers = builder.mOriginatingNumbers;
destinationPort = builder.mDestinationPort;
+ packageName = builder.mPackageName;
}
public static final Creator<VisualVoicemailSmsFilterSettings> CREATOR =
@@ -154,7 +170,7 @@ public final class VisualVoicemailSmsFilterSettings implements Parcelable {
builder.setClientPrefix(in.readString());
builder.setOriginatingNumbers(in.createStringArrayList());
builder.setDestinationPort(in.readInt());
-
+ builder.setPackageName(in.readString());
return builder.build();
}
@@ -174,10 +190,11 @@ public final class VisualVoicemailSmsFilterSettings implements Parcelable {
dest.writeString(clientPrefix);
dest.writeStringList(originatingNumbers);
dest.writeInt(destinationPort);
+ dest.writeString(packageName);
}
@Override
- public String toString(){
+ public String toString() {
return "[VisualVoicemailSmsFilterSettings "
+ "clientPrefix=" + clientPrefix
+ ", originatingNumbers=" + originatingNumbers
diff --git a/android/telephony/data/ApnSetting.java b/android/telephony/data/ApnSetting.java
new file mode 100644
index 00000000..73a05af8
--- /dev/null
+++ b/android/telephony/data/ApnSetting.java
@@ -0,0 +1,1351 @@
+/*
+ * 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.telephony.data;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.StringDef;
+import android.content.ContentValues;
+import android.database.Cursor;
+import android.hardware.radio.V1_0.ApnTypes;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.Telephony;
+import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.TelephonyManager;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A class representing an APN configuration.
+ */
+public class ApnSetting implements Parcelable {
+
+ static final String LOG_TAG = "ApnSetting";
+ private static final boolean VDBG = false;
+
+ private final String mEntryName;
+ private final String mApnName;
+ private final InetAddress mProxy;
+ private final int mPort;
+ private final URL mMmsc;
+ private final InetAddress mMmsProxy;
+ private final int mMmsPort;
+ private final String mUser;
+ private final String mPassword;
+ private final int mAuthType;
+ private final List<String> mTypes;
+ private final int mTypesBitmap;
+ private final int mId;
+ private final String mOperatorNumeric;
+ private final String mProtocol;
+ private final String mRoamingProtocol;
+ private final int mMtu;
+
+ private final boolean mCarrierEnabled;
+
+ private final int mNetworkTypeBitmask;
+
+ private final int mProfileId;
+
+ private final boolean mModemCognitive;
+ private final int mMaxConns;
+ private final int mWaitTime;
+ private final int mMaxConnsTime;
+
+ private final String mMvnoType;
+ private final String mMvnoMatchData;
+
+ private boolean mPermanentFailed = false;
+
+ /**
+ * Returns the types bitmap of the APN.
+ *
+ * @return types bitmap of the APN
+ * @hide
+ */
+ public int getTypesBitmap() {
+ return mTypesBitmap;
+ }
+
+ /**
+ * Returns the MTU size of the mobile interface to which the APN connected.
+ *
+ * @return the MTU size of the APN
+ * @hide
+ */
+ public int getMtu() {
+ return mMtu;
+ }
+
+ /**
+ * Returns the profile id to which the APN saved in modem.
+ *
+ * @return the profile id of the APN
+ * @hide
+ */
+ public int getProfileId() {
+ return mProfileId;
+ }
+
+ /**
+ * Returns if the APN setting is to be set in modem.
+ *
+ * @return is the APN setting to be set in modem
+ * @hide
+ */
+ public boolean getModemCognitive() {
+ return mModemCognitive;
+ }
+
+ /**
+ * Returns the max connections of this APN.
+ *
+ * @return the max connections of this APN
+ * @hide
+ */
+ public int getMaxConns() {
+ return mMaxConns;
+ }
+
+ /**
+ * Returns the wait time for retry of the APN.
+ *
+ * @return the wait time for retry of the APN
+ * @hide
+ */
+ public int getWaitTime() {
+ return mWaitTime;
+ }
+
+ /**
+ * Returns the time to limit max connection for the APN.
+ *
+ * @return the time to limit max connection for the APN
+ * @hide
+ */
+ public int getMaxConnsTime() {
+ return mMaxConnsTime;
+ }
+
+ /**
+ * Returns the MVNO data. Examples:
+ * "spn": A MOBILE, BEN NL
+ * "imsi": 302720x94, 2060188
+ * "gid": 4E, 33
+ * "iccid": 898603 etc..
+ *
+ * @return the mvno match data
+ * @hide
+ */
+ public String getMvnoMatchData() {
+ return mMvnoMatchData;
+ }
+
+ /**
+ * Indicates this APN setting is permanently failed and cannot be
+ * retried by the retry manager anymore.
+ *
+ * @return if this APN setting is permanently failed
+ * @hide
+ */
+ public boolean getPermanentFailed() {
+ return mPermanentFailed;
+ }
+
+ /**
+ * Sets if this APN setting is permanently failed.
+ *
+ * @param permanentFailed if this APN setting is permanently failed
+ * @hide
+ */
+ public void setPermanentFailed(boolean permanentFailed) {
+ mPermanentFailed = permanentFailed;
+ }
+
+ /**
+ * Returns the entry name of the APN.
+ *
+ * @return the entry name for the APN
+ */
+ public String getEntryName() {
+ return mEntryName;
+ }
+
+ /**
+ * Returns the name of the APN.
+ *
+ * @return APN name
+ */
+ public String getApnName() {
+ return mApnName;
+ }
+
+ /**
+ * Returns the proxy address of the APN.
+ *
+ * @return proxy address.
+ */
+ public InetAddress getProxy() {
+ return mProxy;
+ }
+
+ /**
+ * Returns the proxy port of the APN.
+ *
+ * @return proxy port
+ */
+ public int getPort() {
+ return mPort;
+ }
+ /**
+ * Returns the MMSC URL of the APN.
+ *
+ * @return MMSC URL.
+ */
+ public URL getMmsc() {
+ return mMmsc;
+ }
+
+ /**
+ * Returns the MMS proxy address of the APN.
+ *
+ * @return MMS proxy address.
+ */
+ public InetAddress getMmsProxy() {
+ return mMmsProxy;
+ }
+
+ /**
+ * Returns the MMS proxy port of the APN.
+ *
+ * @return MMS proxy port
+ */
+ public int getMmsPort() {
+ return mMmsPort;
+ }
+
+ /**
+ * Returns the APN username of the APN.
+ *
+ * @return APN username
+ */
+ public String getUser() {
+ return mUser;
+ }
+
+ /**
+ * Returns the APN password of the APN.
+ *
+ * @return APN password
+ */
+ public String getPassword() {
+ return mPassword;
+ }
+
+ /** @hide */
+ @IntDef({
+ AUTH_TYPE_NONE,
+ AUTH_TYPE_PAP,
+ AUTH_TYPE_CHAP,
+ AUTH_TYPE_PAP_OR_CHAP,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface AuthType {}
+
+ /**
+ * Returns the authentication type of the APN.
+ *
+ * Example of possible values: {@link #AUTH_TYPE_NONE}, {@link #AUTH_TYPE_PAP}.
+ *
+ * @return authentication type
+ */
+ @AuthType
+ public int getAuthType() {
+ return mAuthType;
+ }
+
+ /** @hide */
+ @StringDef({
+ TYPE_DEFAULT,
+ TYPE_MMS,
+ TYPE_SUPL,
+ TYPE_DUN,
+ TYPE_HIPRI,
+ TYPE_FOTA,
+ TYPE_IMS,
+ TYPE_CBS,
+ TYPE_IA,
+ TYPE_EMERGENCY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ApnType {}
+
+ /**
+ * Returns the list of APN types of the APN.
+ *
+ * Example of possible values: {@link #TYPE_DEFAULT}, {@link #TYPE_MMS}.
+ *
+ * @return the list of APN types
+ */
+ @ApnType
+ public List<String> getTypes() {
+ return mTypes;
+ }
+
+ /**
+ * Returns the unique database id for this entry.
+ *
+ * @return the unique database id
+ */
+ public int getId() {
+ return mId;
+ }
+
+ /**
+ * Returns the numeric operator ID for the APN. Usually
+ * {@link android.provider.Telephony.Carriers#MCC} +
+ * {@link android.provider.Telephony.Carriers#MNC}.
+ *
+ * @return the numeric operator ID
+ */
+ public String getOperatorNumeric() {
+ return mOperatorNumeric;
+ }
+
+ /** @hide */
+ @StringDef({
+ PROTOCOL_IP,
+ PROTOCOL_IPV6,
+ PROTOCOL_IPV4V6,
+ PROTOCOL_PPP,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ProtocolType {}
+
+ /**
+ * Returns the protocol to use to connect to this APN.
+ *
+ * One of the {@code PDP_type} values in TS 27.007 section 10.1.1.
+ * Example of possible values: {@link #PROTOCOL_IP}, {@link #PROTOCOL_IPV6}.
+ *
+ * @return the protocol
+ */
+ @ProtocolType
+ public String getProtocol() {
+ return mProtocol;
+ }
+
+ /**
+ * Returns the protocol to use to connect to this APN when roaming.
+ *
+ * The syntax is the same as {@link android.provider.Telephony.Carriers#PROTOCOL}.
+ *
+ * @return the roaming protocol
+ */
+ public String getRoamingProtocol() {
+ return mRoamingProtocol;
+ }
+
+ /**
+ * Returns the current status of APN.
+ *
+ * {@code true} : enabled APN.
+ * {@code false} : disabled APN.
+ *
+ * @return the current status
+ */
+ public boolean isEnabled() {
+ return mCarrierEnabled;
+ }
+
+ /**
+ * Returns a bitmask describing the Radio Technologies(Network Types) which this APN may use.
+ *
+ * NetworkType bitmask is calculated from NETWORK_TYPE defined in {@link TelephonyManager}.
+ *
+ * Examples of Network Types include {@link TelephonyManager#NETWORK_TYPE_UNKNOWN},
+ * {@link TelephonyManager#NETWORK_TYPE_GPRS}, {@link TelephonyManager#NETWORK_TYPE_EDGE}.
+ *
+ * @return a bitmask describing the Radio Technologies(Network Types)
+ */
+ public int getNetworkTypeBitmask() {
+ return mNetworkTypeBitmask;
+ }
+
+ /** @hide */
+ @StringDef({
+ MVNO_TYPE_SPN,
+ MVNO_TYPE_IMSI,
+ MVNO_TYPE_GID,
+ MVNO_TYPE_ICCID,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface MvnoType {}
+
+ /**
+ * Returns the MVNO match type for this APN.
+ *
+ * Example of possible values: {@link #MVNO_TYPE_SPN}, {@link #MVNO_TYPE_IMSI}.
+ *
+ * @return the MVNO match type
+ */
+ @MvnoType
+ public String getMvnoType() {
+ return mMvnoType;
+ }
+
+ private ApnSetting(Builder builder) {
+ this.mEntryName = builder.mEntryName;
+ this.mApnName = builder.mApnName;
+ this.mProxy = builder.mProxy;
+ this.mPort = builder.mPort;
+ this.mMmsc = builder.mMmsc;
+ this.mMmsProxy = builder.mMmsProxy;
+ this.mMmsPort = builder.mMmsPort;
+ this.mUser = builder.mUser;
+ this.mPassword = builder.mPassword;
+ this.mAuthType = builder.mAuthType;
+ this.mTypes = (builder.mTypes == null ? new ArrayList<String>() : builder.mTypes);
+ this.mTypesBitmap = builder.mTypesBitmap;
+ this.mId = builder.mId;
+ this.mOperatorNumeric = builder.mOperatorNumeric;
+ this.mProtocol = builder.mProtocol;
+ this.mRoamingProtocol = builder.mRoamingProtocol;
+ this.mMtu = builder.mMtu;
+ this.mCarrierEnabled = builder.mCarrierEnabled;
+ this.mNetworkTypeBitmask = builder.mNetworkTypeBitmask;
+ this.mProfileId = builder.mProfileId;
+ this.mModemCognitive = builder.mModemCognitive;
+ this.mMaxConns = builder.mMaxConns;
+ this.mWaitTime = builder.mWaitTime;
+ this.mMaxConnsTime = builder.mMaxConnsTime;
+ this.mMvnoType = builder.mMvnoType;
+ this.mMvnoMatchData = builder.mMvnoMatchData;
+ }
+
+ /** @hide */
+ public static ApnSetting makeApnSetting(int id, String operatorNumeric, String entryName,
+ String apnName, InetAddress proxy, int port, URL mmsc, InetAddress mmsProxy,
+ int mmsPort, String user, String password, int authType, List<String> types,
+ String protocol, String roamingProtocol, boolean carrierEnabled,
+ int networkTypeBitmask, int profileId, boolean modemCognitive, int maxConns,
+ int waitTime, int maxConnsTime, int mtu, String mvnoType, String mvnoMatchData) {
+ return new Builder()
+ .setId(id)
+ .setOperatorNumeric(operatorNumeric)
+ .setEntryName(entryName)
+ .setApnName(apnName)
+ .setProxy(proxy)
+ .setPort(port)
+ .setMmsc(mmsc)
+ .setMmsProxy(mmsProxy)
+ .setMmsPort(mmsPort)
+ .setUser(user)
+ .setPassword(password)
+ .setAuthType(authType)
+ .setTypes(types)
+ .setProtocol(protocol)
+ .setRoamingProtocol(roamingProtocol)
+ .setCarrierEnabled(carrierEnabled)
+ .setNetworkTypeBitmask(networkTypeBitmask)
+ .setProfileId(profileId)
+ .setModemCognitive(modemCognitive)
+ .setMaxConns(maxConns)
+ .setWaitTime(waitTime)
+ .setMaxConnsTime(maxConnsTime)
+ .setMtu(mtu)
+ .setMvnoType(mvnoType)
+ .setMvnoMatchData(mvnoMatchData)
+ .build();
+ }
+
+ /** @hide */
+ public static ApnSetting makeApnSetting(Cursor cursor) {
+ String[] types = parseTypes(
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.TYPE)));
+ int networkTypeBitmask = cursor.getInt(
+ cursor.getColumnIndexOrThrow(Telephony.Carriers.NETWORK_TYPE_BITMASK));
+ if (networkTypeBitmask == 0) {
+ final int bearerBitmask = cursor.getInt(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.BEARER_BITMASK));
+ networkTypeBitmask =
+ ServiceState.convertBearerBitmaskToNetworkTypeBitmask(bearerBitmask);
+ }
+
+ return makeApnSetting(
+ cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers._ID)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.NUMERIC)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.NAME)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.APN)),
+ inetAddressFromString(cursor.getString(
+ cursor.getColumnIndexOrThrow(Telephony.Carriers.PROXY))),
+ portFromString(cursor.getString(
+ cursor.getColumnIndexOrThrow(Telephony.Carriers.PORT))),
+ URLFromString(cursor.getString(
+ cursor.getColumnIndexOrThrow(Telephony.Carriers.MMSC))),
+ inetAddressFromString(cursor.getString(
+ cursor.getColumnIndexOrThrow(Telephony.Carriers.MMSPROXY))),
+ portFromString(cursor.getString(
+ cursor.getColumnIndexOrThrow(Telephony.Carriers.MMSPORT))),
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.USER)),
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.PASSWORD)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.AUTH_TYPE)),
+ Arrays.asList(types),
+ cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.PROTOCOL)),
+ cursor.getString(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.ROAMING_PROTOCOL)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.CARRIER_ENABLED)) == 1,
+ networkTypeBitmask,
+ cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.PROFILE_ID)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.MODEM_COGNITIVE)) == 1,
+ cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.MAX_CONNS)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.WAIT_TIME)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.MAX_CONNS_TIME)),
+ cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.MTU)),
+ cursor.getString(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.MVNO_TYPE)),
+ cursor.getString(cursor.getColumnIndexOrThrow(
+ Telephony.Carriers.MVNO_MATCH_DATA)));
+ }
+
+ /** @hide */
+ public static ApnSetting makeApnSetting(ApnSetting apn) {
+ return makeApnSetting(apn.mId, apn.mOperatorNumeric, apn.mEntryName, apn.mApnName,
+ apn.mProxy, apn.mPort, apn.mMmsc, apn.mMmsProxy, apn.mMmsPort, apn.mUser,
+ apn.mPassword, apn.mAuthType, apn.mTypes, apn.mProtocol, apn.mRoamingProtocol,
+ apn.mCarrierEnabled, apn.mNetworkTypeBitmask, apn.mProfileId,
+ apn.mModemCognitive, apn.mMaxConns, apn.mWaitTime, apn.mMaxConnsTime, apn.mMtu,
+ apn.mMvnoType, apn.mMvnoMatchData);
+ }
+
+ /** @hide */
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("[ApnSettingV4] ")
+ .append(mEntryName)
+ .append(", ").append(mId)
+ .append(", ").append(mOperatorNumeric)
+ .append(", ").append(mApnName)
+ .append(", ").append(inetAddressToString(mProxy))
+ .append(", ").append(URLToString(mMmsc))
+ .append(", ").append(inetAddressToString(mMmsProxy))
+ .append(", ").append(portToString(mMmsPort))
+ .append(", ").append(portToString(mPort))
+ .append(", ").append(mAuthType).append(", ");
+ for (int i = 0; i < mTypes.size(); i++) {
+ sb.append(mTypes.get(i));
+ if (i < mTypes.size() - 1) {
+ sb.append(" | ");
+ }
+ }
+ sb.append(", ").append(mProtocol);
+ sb.append(", ").append(mRoamingProtocol);
+ sb.append(", ").append(mCarrierEnabled);
+ sb.append(", ").append(mProfileId);
+ sb.append(", ").append(mModemCognitive);
+ sb.append(", ").append(mMaxConns);
+ sb.append(", ").append(mWaitTime);
+ sb.append(", ").append(mMaxConnsTime);
+ sb.append(", ").append(mMtu);
+ sb.append(", ").append(mMvnoType);
+ sb.append(", ").append(mMvnoMatchData);
+ sb.append(", ").append(mPermanentFailed);
+ sb.append(", ").append(mNetworkTypeBitmask);
+ return sb.toString();
+ }
+
+ /**
+ * Returns true if there are MVNO params specified.
+ * @hide
+ */
+ public boolean hasMvnoParams() {
+ return !TextUtils.isEmpty(mMvnoType) && !TextUtils.isEmpty(mMvnoMatchData);
+ }
+
+ /** @hide */
+ public boolean canHandleType(String type) {
+ if (!mCarrierEnabled) return false;
+ boolean wildcardable = true;
+ if (TYPE_IA.equalsIgnoreCase(type)) wildcardable = false;
+ for (String t : mTypes) {
+ // DEFAULT handles all, and HIPRI is handled by DEFAULT
+ if (t.equalsIgnoreCase(type)
+ || (wildcardable && t.equalsIgnoreCase(TYPE_ALL))
+ || (t.equalsIgnoreCase(TYPE_DEFAULT)
+ && type.equalsIgnoreCase(TYPE_HIPRI))) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // check whether the types of two APN same (even only one type of each APN is same)
+ private boolean typeSameAny(ApnSetting first, ApnSetting second) {
+ if (VDBG) {
+ StringBuilder apnType1 = new StringBuilder(first.mApnName + ": ");
+ for (int index1 = 0; index1 < first.mTypes.size(); index1++) {
+ apnType1.append(first.mTypes.get(index1));
+ apnType1.append(",");
+ }
+
+ StringBuilder apnType2 = new StringBuilder(second.mApnName + ": ");
+ for (int index1 = 0; index1 < second.mTypes.size(); index1++) {
+ apnType2.append(second.mTypes.get(index1));
+ apnType2.append(",");
+ }
+ Rlog.d(LOG_TAG, "APN1: is " + apnType1);
+ Rlog.d(LOG_TAG, "APN2: is " + apnType2);
+ }
+
+ for (int index1 = 0; index1 < first.mTypes.size(); index1++) {
+ for (int index2 = 0; index2 < second.mTypes.size(); index2++) {
+ if (first.mTypes.get(index1).equals(ApnSetting.TYPE_ALL)
+ || second.mTypes.get(index2).equals(ApnSetting.TYPE_ALL)
+ || first.mTypes.get(index1).equals(second.mTypes.get(index2))) {
+ if (VDBG) Rlog.d(LOG_TAG, "typeSameAny: return true");
+ return true;
+ }
+ }
+ }
+
+ if (VDBG) Rlog.d(LOG_TAG, "typeSameAny: return false");
+ return false;
+ }
+
+ // TODO - if we have this function we should also have hashCode.
+ // Also should handle changes in type order and perhaps case-insensitivity
+ /** @hide */
+ public boolean equals(Object o) {
+ if (o instanceof ApnSetting == false) {
+ return false;
+ }
+
+ ApnSetting other = (ApnSetting) o;
+
+ return mEntryName.equals(other.mEntryName)
+ && Objects.equals(mId, other.mId)
+ && Objects.equals(mOperatorNumeric, other.mOperatorNumeric)
+ && Objects.equals(mApnName, other.mApnName)
+ && Objects.equals(mProxy, other.mProxy)
+ && Objects.equals(mMmsc, other.mMmsc)
+ && Objects.equals(mMmsProxy, other.mMmsProxy)
+ && Objects.equals(mMmsPort, other.mMmsPort)
+ && Objects.equals(mPort,other.mPort)
+ && Objects.equals(mUser, other.mUser)
+ && Objects.equals(mPassword, other.mPassword)
+ && Objects.equals(mAuthType, other.mAuthType)
+ && Objects.equals(mTypes, other.mTypes)
+ && Objects.equals(mTypesBitmap, other.mTypesBitmap)
+ && Objects.equals(mProtocol, other.mProtocol)
+ && Objects.equals(mRoamingProtocol, other.mRoamingProtocol)
+ && Objects.equals(mCarrierEnabled, other.mCarrierEnabled)
+ && Objects.equals(mProfileId, other.mProfileId)
+ && Objects.equals(mModemCognitive, other.mModemCognitive)
+ && Objects.equals(mMaxConns, other.mMaxConns)
+ && Objects.equals(mWaitTime, other.mWaitTime)
+ && Objects.equals(mMaxConnsTime, other.mMaxConnsTime)
+ && Objects.equals(mMtu, other.mMtu)
+ && Objects.equals(mMvnoType, other.mMvnoType)
+ && Objects.equals(mMvnoMatchData, other.mMvnoMatchData)
+ && Objects.equals(mNetworkTypeBitmask, other.mNetworkTypeBitmask);
+ }
+
+ /**
+ * Compare two APN settings
+ *
+ * Note: This method does not compare 'mId', 'mNetworkTypeBitmask'. We only use this for
+ * determining if tearing a data call is needed when conditions change. See
+ * cleanUpConnectionsOnUpdatedApns in DcTracker.
+ *
+ * @param o the other object to compare
+ * @param isDataRoaming True if the device is on data roaming
+ * @return True if the two APN settings are same
+ * @hide
+ */
+ public boolean equals(Object o, boolean isDataRoaming) {
+ if (!(o instanceof ApnSetting)) {
+ return false;
+ }
+
+ ApnSetting other = (ApnSetting) o;
+
+ return mEntryName.equals(other.mEntryName)
+ && Objects.equals(mOperatorNumeric, other.mOperatorNumeric)
+ && Objects.equals(mApnName, other.mApnName)
+ && Objects.equals(mProxy, other.mProxy)
+ && Objects.equals(mMmsc, other.mMmsc)
+ && Objects.equals(mMmsProxy, other.mMmsProxy)
+ && Objects.equals(mMmsPort, other.mMmsPort)
+ && Objects.equals(mPort, other.mPort)
+ && Objects.equals(mUser, other.mUser)
+ && Objects.equals(mPassword, other.mPassword)
+ && Objects.equals(mAuthType, other.mAuthType)
+ && Objects.equals(mTypes, other.mTypes)
+ && Objects.equals(mTypesBitmap, other.mTypesBitmap)
+ && (isDataRoaming || Objects.equals(mProtocol,other.mProtocol))
+ && (!isDataRoaming || Objects.equals(mRoamingProtocol, other.mRoamingProtocol))
+ && Objects.equals(mCarrierEnabled, other.mCarrierEnabled)
+ && Objects.equals(mProfileId, other.mProfileId)
+ && Objects.equals(mModemCognitive, other.mModemCognitive)
+ && Objects.equals(mMaxConns, other.mMaxConns)
+ && Objects.equals(mWaitTime, other.mWaitTime)
+ && Objects.equals(mMaxConnsTime, other.mMaxConnsTime)
+ && Objects.equals(mMtu, other.mMtu)
+ && Objects.equals(mMvnoType, other.mMvnoType)
+ && Objects.equals(mMvnoMatchData, other.mMvnoMatchData);
+ }
+
+ /**
+ * Check if neither mention DUN and are substantially similar
+ *
+ * @param other The other APN settings to compare
+ * @return True if two APN settings are similar
+ * @hide
+ */
+ public boolean similar(ApnSetting other) {
+ return (!this.canHandleType(TYPE_DUN)
+ && !other.canHandleType(TYPE_DUN)
+ && Objects.equals(this.mApnName, other.mApnName)
+ && !typeSameAny(this, other)
+ && xorEqualsInetAddress(this.mProxy, other.mProxy)
+ && xorEqualsPort(this.mPort, other.mPort)
+ && xorEquals(this.mProtocol, other.mProtocol)
+ && xorEquals(this.mRoamingProtocol, other.mRoamingProtocol)
+ && Objects.equals(this.mCarrierEnabled, other.mCarrierEnabled)
+ && Objects.equals(this.mProfileId, other.mProfileId)
+ && Objects.equals(this.mMvnoType, other.mMvnoType)
+ && Objects.equals(this.mMvnoMatchData, other.mMvnoMatchData)
+ && xorEqualsURL(this.mMmsc, other.mMmsc)
+ && xorEqualsInetAddress(this.mMmsProxy, other.mMmsProxy)
+ && xorEqualsPort(this.mMmsPort, other.mMmsPort))
+ && Objects.equals(this.mNetworkTypeBitmask, other.mNetworkTypeBitmask);
+ }
+
+ // Equal or one is not specified.
+ private boolean xorEquals(String first, String second) {
+ return (Objects.equals(first, second)
+ || TextUtils.isEmpty(first)
+ || TextUtils.isEmpty(second));
+ }
+
+ // Equal or one is not specified.
+ private boolean xorEqualsInetAddress(InetAddress first, InetAddress second) {
+ return first == null || second == null || first.equals(second);
+ }
+
+ // Equal or one is not specified.
+ private boolean xorEqualsURL(URL first, URL second) {
+ return first == null || second == null || first.equals(second);
+ }
+
+ // Equal or one is not specified.
+ private boolean xorEqualsPort(int first, int second) {
+ return first == -1 || second == -1 || Objects.equals(first, second);
+ }
+
+ // Helper function to convert APN string into a 32-bit bitmask.
+ private static int getApnBitmask(String apn) {
+ switch (apn) {
+ case TYPE_DEFAULT: return ApnTypes.DEFAULT;
+ case TYPE_MMS: return ApnTypes.MMS;
+ case TYPE_SUPL: return ApnTypes.SUPL;
+ case TYPE_DUN: return ApnTypes.DUN;
+ case TYPE_HIPRI: return ApnTypes.HIPRI;
+ case TYPE_FOTA: return ApnTypes.FOTA;
+ case TYPE_IMS: return ApnTypes.IMS;
+ case TYPE_CBS: return ApnTypes.CBS;
+ case TYPE_IA: return ApnTypes.IA;
+ case TYPE_EMERGENCY: return ApnTypes.EMERGENCY;
+ case TYPE_ALL: return ApnTypes.ALL;
+ default: return ApnTypes.NONE;
+ }
+ }
+
+ private String deParseTypes(List<String> types) {
+ if (types == null) {
+ return null;
+ }
+ return TextUtils.join(",", types);
+ }
+
+ private String nullToEmpty(String stringValue) {
+ return stringValue == null ? "" : stringValue;
+ }
+
+ /** @hide */
+ // Called by DPM.
+ public ContentValues toContentValues() {
+ ContentValues apnValue = new ContentValues();
+ apnValue.put(Telephony.Carriers.NUMERIC, nullToEmpty(mOperatorNumeric));
+ apnValue.put(Telephony.Carriers.NAME, nullToEmpty(mEntryName));
+ apnValue.put(Telephony.Carriers.APN, nullToEmpty(mApnName));
+ apnValue.put(Telephony.Carriers.PROXY, mProxy == null ? "" : inetAddressToString(mProxy));
+ apnValue.put(Telephony.Carriers.PORT, portToString(mPort));
+ apnValue.put(Telephony.Carriers.MMSC, mMmsc == null ? "" : URLToString(mMmsc));
+ apnValue.put(Telephony.Carriers.MMSPORT, portToString(mMmsPort));
+ apnValue.put(Telephony.Carriers.MMSPROXY, mMmsProxy == null
+ ? "" : inetAddressToString(mMmsProxy));
+ apnValue.put(Telephony.Carriers.USER, nullToEmpty(mUser));
+ apnValue.put(Telephony.Carriers.PASSWORD, nullToEmpty(mPassword));
+ apnValue.put(Telephony.Carriers.AUTH_TYPE, mAuthType);
+ String apnType = deParseTypes(mTypes);
+ apnValue.put(Telephony.Carriers.TYPE, nullToEmpty(apnType));
+ apnValue.put(Telephony.Carriers.PROTOCOL, nullToEmpty(mProtocol));
+ apnValue.put(Telephony.Carriers.ROAMING_PROTOCOL, nullToEmpty(mRoamingProtocol));
+ apnValue.put(Telephony.Carriers.CARRIER_ENABLED, mCarrierEnabled);
+ apnValue.put(Telephony.Carriers.MVNO_TYPE, nullToEmpty(mMvnoType));
+ apnValue.put(Telephony.Carriers.NETWORK_TYPE_BITMASK, mNetworkTypeBitmask);
+
+ return apnValue;
+ }
+
+ /**
+ * @param types comma delimited list of APN types
+ * @return array of APN types
+ * @hide
+ */
+ public static String[] parseTypes(String types) {
+ String[] result;
+ // If unset, set to DEFAULT.
+ if (TextUtils.isEmpty(types)) {
+ result = new String[1];
+ result[0] = TYPE_ALL;
+ } else {
+ result = types.split(",");
+ }
+ return result;
+ }
+
+ private static URL URLFromString(String url) {
+ try {
+ return TextUtils.isEmpty(url) ? null : new URL(url);
+ } catch (MalformedURLException e) {
+ Log.e(LOG_TAG, "Can't parse URL from string.");
+ return null;
+ }
+ }
+
+ private static String URLToString(URL url) {
+ return url == null ? "" : url.toString();
+ }
+
+ private static InetAddress inetAddressFromString(String inetAddress) {
+ if (TextUtils.isEmpty(inetAddress)) {
+ return null;
+ }
+ try {
+ return InetAddress.getByName(inetAddress);
+ } catch (UnknownHostException e) {
+ Log.e(LOG_TAG, "Can't parse InetAddress from string: unknown host.");
+ return null;
+ }
+ }
+
+ private static String inetAddressToString(InetAddress inetAddress) {
+ if (inetAddress == null) {
+ return null;
+ }
+ final String inetAddressString = inetAddress.toString();
+ if (TextUtils.isEmpty(inetAddressString)) {
+ return null;
+ }
+ final String hostName = inetAddressString.substring(0, inetAddressString.indexOf("/"));
+ final String address = inetAddressString.substring(inetAddressString.indexOf("/") + 1);
+ if (TextUtils.isEmpty(hostName) && TextUtils.isEmpty(address)) {
+ return null;
+ }
+ return TextUtils.isEmpty(hostName) ? address : hostName;
+ }
+
+ private static int portFromString(String strPort) {
+ int port = -1;
+ if (!TextUtils.isEmpty(strPort)) {
+ try {
+ port = Integer.parseInt(strPort);
+ } catch (NumberFormatException e) {
+ Log.e(LOG_TAG, "Can't parse port from String");
+ }
+ }
+ return port;
+ }
+
+ private static String portToString(int port) {
+ return port == -1 ? "" : Integer.toString(port);
+ }
+
+ // Implement Parcelable.
+ @Override
+ /** @hide */
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ /** @hide */
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mId);
+ dest.writeString(mOperatorNumeric);
+ dest.writeString(mEntryName);
+ dest.writeString(mApnName);
+ dest.writeValue(mProxy);
+ dest.writeInt(mPort);
+ dest.writeValue(mMmsc);
+ dest.writeValue(mMmsProxy);
+ dest.writeInt(mMmsPort);
+ dest.writeString(mUser);
+ dest.writeString(mPassword);
+ dest.writeInt(mAuthType);
+ dest.writeStringArray(mTypes.toArray(new String[0]));
+ dest.writeString(mProtocol);
+ dest.writeString(mRoamingProtocol);
+ dest.writeInt(mCarrierEnabled ? 1: 0);
+ dest.writeString(mMvnoType);
+ dest.writeInt(mNetworkTypeBitmask);
+ }
+
+ private static ApnSetting readFromParcel(Parcel in) {
+ final int id = in.readInt();
+ final String operatorNumeric = in.readString();
+ final String entryName = in.readString();
+ final String apnName = in.readString();
+ final InetAddress proxy = (InetAddress)in.readValue(InetAddress.class.getClassLoader());
+ final int port = in.readInt();
+ final URL mmsc = (URL)in.readValue(URL.class.getClassLoader());
+ final InetAddress mmsProxy = (InetAddress)in.readValue(InetAddress.class.getClassLoader());
+ final int mmsPort = in.readInt();
+ final String user = in.readString();
+ final String password = in.readString();
+ final int authType = in.readInt();
+ final List<String> types = Arrays.asList(in.readStringArray());
+ final String protocol = in.readString();
+ final String roamingProtocol = in.readString();
+ final boolean carrierEnabled = in.readInt() > 0;
+ final String mvnoType = in.readString();
+ final int networkTypeBitmask = in.readInt();
+
+ return makeApnSetting(id, operatorNumeric, entryName, apnName,
+ proxy, port, mmsc, mmsProxy, mmsPort, user, password, authType, types, protocol,
+ roamingProtocol, carrierEnabled, networkTypeBitmask, 0, false,
+ 0, 0, 0, 0, mvnoType, null);
+ }
+
+ public static final Parcelable.Creator<ApnSetting> CREATOR =
+ new Parcelable.Creator<ApnSetting>() {
+ @Override
+ public ApnSetting createFromParcel(Parcel in) {
+ return readFromParcel(in);
+ }
+
+ @Override
+ public ApnSetting[] newArray(int size) {
+ return new ApnSetting[size];
+ }
+ };
+
+ /**
+ * APN types for data connections. These are usage categories for an APN
+ * entry. One APN entry may support multiple APN types, eg, a single APN
+ * may service regular internet traffic ("default") as well as MMS-specific
+ * connections.<br/>
+ * ALL is a special type to indicate that this APN entry can
+ * service all data connections.
+ */
+ public static final String TYPE_ALL = "*";
+ /** APN type for default data traffic */
+ public static final String TYPE_DEFAULT = "default";
+ /** APN type for MMS traffic */
+ public static final String TYPE_MMS = "mms";
+ /** APN type for SUPL assisted GPS */
+ public static final String TYPE_SUPL = "supl";
+ /** APN type for DUN traffic */
+ public static final String TYPE_DUN = "dun";
+ /** APN type for HiPri traffic */
+ public static final String TYPE_HIPRI = "hipri";
+ /** APN type for FOTA */
+ public static final String TYPE_FOTA = "fota";
+ /** APN type for IMS */
+ public static final String TYPE_IMS = "ims";
+ /** APN type for CBS */
+ public static final String TYPE_CBS = "cbs";
+ /** APN type for IA Initial Attach APN */
+ public static final String TYPE_IA = "ia";
+ /** APN type for Emergency PDN. This is not an IA apn, but is used
+ * for access to carrier services in an emergency call situation. */
+ public static final String TYPE_EMERGENCY = "emergency";
+ /**
+ * Array of all APN types
+ *
+ * @hide
+ */
+ public static final String[] ALL_TYPES = {
+ TYPE_DEFAULT,
+ TYPE_MMS,
+ TYPE_SUPL,
+ TYPE_DUN,
+ TYPE_HIPRI,
+ TYPE_FOTA,
+ TYPE_IMS,
+ TYPE_CBS,
+ TYPE_IA,
+ TYPE_EMERGENCY
+ };
+
+ // Possible values for authentication types.
+ public static final int AUTH_TYPE_NONE = 0;
+ public static final int AUTH_TYPE_PAP = 1;
+ public static final int AUTH_TYPE_CHAP = 2;
+ public static final int AUTH_TYPE_PAP_OR_CHAP = 3;
+
+ // Possible values for protocol.
+ public static final String PROTOCOL_IP = "IP";
+ public static final String PROTOCOL_IPV6 = "IPV6";
+ public static final String PROTOCOL_IPV4V6 = "IPV4V6";
+ public static final String PROTOCOL_PPP = "PPP";
+
+ // Possible values for MVNO type.
+ public static final String MVNO_TYPE_SPN = "spn";
+ public static final String MVNO_TYPE_IMSI = "imsi";
+ public static final String MVNO_TYPE_GID = "gid";
+ public static final String MVNO_TYPE_ICCID = "iccid";
+
+ public static class Builder{
+ private String mEntryName;
+ private String mApnName;
+ private InetAddress mProxy;
+ private int mPort = -1;
+ private URL mMmsc;
+ private InetAddress mMmsProxy;
+ private int mMmsPort = -1;
+ private String mUser;
+ private String mPassword;
+ private int mAuthType;
+ private List<String> mTypes;
+ private int mTypesBitmap;
+ private int mId;
+ private String mOperatorNumeric;
+ private String mProtocol;
+ private String mRoamingProtocol;
+ private int mMtu;
+ private int mNetworkTypeBitmask;
+ private boolean mCarrierEnabled;
+ private int mProfileId;
+ private boolean mModemCognitive;
+ private int mMaxConns;
+ private int mWaitTime;
+ private int mMaxConnsTime;
+ private String mMvnoType;
+ private String mMvnoMatchData;
+
+ /**
+ * Default constructor for Builder.
+ */
+ public Builder() {}
+
+ /**
+ * Sets the unique database id for this entry.
+ *
+ * @param id the unique database id to set for this entry
+ */
+ private Builder setId(int id) {
+ this.mId = id;
+ return this;
+ }
+
+ /**
+ * Set the MTU size of the mobile interface to which the APN connected.
+ *
+ * @param mtu the MTU size to set for the APN
+ * @hide
+ */
+ public Builder setMtu(int mtu) {
+ this.mMtu = mtu;
+ return this;
+ }
+
+ /**
+ * Sets the profile id to which the APN saved in modem.
+ *
+ * @param profileId the profile id to set for the APN
+ * @hide
+ */
+ public Builder setProfileId(int profileId) {
+ this.mProfileId = profileId;
+ return this;
+ }
+
+ /**
+ * Sets if the APN setting is to be set in modem.
+ *
+ * @param modemCognitive if the APN setting is to be set in modem
+ * @hide
+ */
+ public Builder setModemCognitive(boolean modemCognitive) {
+ this.mModemCognitive = modemCognitive;
+ return this;
+ }
+
+ /**
+ * Sets the max connections of this APN.
+ *
+ * @param maxConns the max connections of this APN
+ * @hide
+ */
+ public Builder setMaxConns(int maxConns) {
+ this.mMaxConns = maxConns;
+ return this;
+ }
+
+ /**
+ * Sets the wait time for retry of the APN.
+ *
+ * @param waitTime the wait time for retry of the APN
+ * @hide
+ */
+ public Builder setWaitTime(int waitTime) {
+ this.mWaitTime = waitTime;
+ return this;
+ }
+
+ /**
+ * Sets the time to limit max connection for the APN.
+ *
+ * @param maxConnsTime the time to limit max connection for the APN
+ * @hide
+ */
+ public Builder setMaxConnsTime(int maxConnsTime) {
+ this.mMaxConnsTime = maxConnsTime;
+ return this;
+ }
+
+ /**
+ * Sets the MVNO match data for the APN.
+ *
+ * @param mvnoMatchData the MVNO match data for the APN
+ * @hide
+ */
+ public Builder setMvnoMatchData(String mvnoMatchData) {
+ this.mMvnoMatchData = mvnoMatchData;
+ return this;
+ }
+
+ /**
+ * Sets the entry name of the APN.
+ *
+ * @param entryName the entry name to set for the APN
+ */
+ public Builder setEntryName(String entryName) {
+ this.mEntryName = entryName;
+ return this;
+ }
+
+ /**
+ * Sets the name of the APN.
+ *
+ * @param apnName the name to set for the APN
+ */
+ public Builder setApnName(String apnName) {
+ this.mApnName = apnName;
+ return this;
+ }
+
+ /**
+ * Sets the proxy address of the APN.
+ *
+ * @param proxy the proxy address to set for the APN
+ */
+ public Builder setProxy(InetAddress proxy) {
+ this.mProxy = proxy;
+ return this;
+ }
+
+ /**
+ * Sets the proxy port of the APN.
+ *
+ * @param port the proxy port to set for the APN
+ */
+ public Builder setPort(int port) {
+ this.mPort = port;
+ return this;
+ }
+
+ /**
+ * Sets the MMSC URL of the APN.
+ *
+ * @param mmsc the MMSC URL to set for the APN
+ */
+ public Builder setMmsc(URL mmsc) {
+ this.mMmsc = mmsc;
+ return this;
+ }
+
+ /**
+ * Sets the MMS proxy address of the APN.
+ *
+ * @param mmsProxy the MMS proxy address to set for the APN
+ */
+ public Builder setMmsProxy(InetAddress mmsProxy) {
+ this.mMmsProxy = mmsProxy;
+ return this;
+ }
+
+ /**
+ * Sets the MMS proxy port of the APN.
+ *
+ * @param mmsPort the MMS proxy port to set for the APN
+ */
+ public Builder setMmsPort(int mmsPort) {
+ this.mMmsPort = mmsPort;
+ return this;
+ }
+
+ /**
+ * Sets the APN username of the APN.
+ *
+ * @param user the APN username to set for the APN
+ */
+ public Builder setUser(String user) {
+ this.mUser = user;
+ return this;
+ }
+
+ /**
+ * Sets the APN password of the APN.
+ *
+ * @see android.provider.Telephony.Carriers#PASSWORD
+ * @param password the APN password to set for the APN
+ */
+ public Builder setPassword(String password) {
+ this.mPassword = password;
+ return this;
+ }
+
+ /**
+ * Sets the authentication type of the APN.
+ *
+ * Example of possible values: {@link #AUTH_TYPE_NONE}, {@link #AUTH_TYPE_PAP}.
+ *
+ * @param authType the authentication type to set for the APN
+ */
+ public Builder setAuthType(@AuthType int authType) {
+ this.mAuthType = authType;
+ return this;
+ }
+
+ /**
+ * Sets the list of APN types of the APN.
+ *
+ * Example of possible values: {@link #TYPE_DEFAULT}, {@link #TYPE_MMS}.
+ *
+ * @param types the list of APN types to set for the APN
+ */
+ public Builder setTypes(@ApnType List<String> types) {
+ this.mTypes = types;
+ int apnBitmap = 0;
+ for (int i = 0; i < mTypes.size(); i++) {
+ mTypes.set(i, mTypes.get(i).toLowerCase());
+ apnBitmap |= getApnBitmask(mTypes.get(i));
+ }
+ this.mTypesBitmap = apnBitmap;
+ return this;
+ }
+
+ /**
+ * Set the numeric operator ID for the APN.
+ *
+ * @param operatorNumeric the numeric operator ID to set for this entry
+ */
+ public Builder setOperatorNumeric(String operatorNumeric) {
+ this.mOperatorNumeric = operatorNumeric;
+ return this;
+ }
+
+ /**
+ * Sets the protocol to use to connect to this APN.
+ *
+ * One of the {@code PDP_type} values in TS 27.007 section 10.1.1.
+ * Example of possible values: {@link #PROTOCOL_IP}, {@link #PROTOCOL_IPV6}.
+ *
+ * @param protocol the protocol to set to use to connect to this APN
+ */
+ public Builder setProtocol(@ProtocolType String protocol) {
+ this.mProtocol = protocol;
+ return this;
+ }
+
+ /**
+ * Sets the protocol to use to connect to this APN when roaming.
+ *
+ * @param roamingProtocol the protocol to set to use to connect to this APN when roaming
+ */
+ public Builder setRoamingProtocol(String roamingProtocol) {
+ this.mRoamingProtocol = roamingProtocol;
+ return this;
+ }
+
+ /**
+ * Sets the current status for this APN.
+ *
+ * @param carrierEnabled the current status to set for this APN
+ */
+ public Builder setCarrierEnabled(boolean carrierEnabled) {
+ this.mCarrierEnabled = carrierEnabled;
+ return this;
+ }
+
+ /**
+ * Sets Radio Technology (Network Type) info for this APN.
+ *
+ * @param networkTypeBitmask the Radio Technology (Network Type) info
+ */
+ public Builder setNetworkTypeBitmask(int networkTypeBitmask) {
+ this.mNetworkTypeBitmask = networkTypeBitmask;
+ return this;
+ }
+
+ /**
+ * Sets the MVNO match type for this APN.
+ *
+ * Example of possible values: {@link #MVNO_TYPE_SPN}, {@link #MVNO_TYPE_IMSI}.
+ *
+ * @param mvnoType the MVNO match type to set for this APN
+ */
+ public Builder setMvnoType(@MvnoType String mvnoType) {
+ this.mMvnoType = mvnoType;
+ return this;
+ }
+
+ public ApnSetting build() {
+ return new ApnSetting(this);
+ }
+ }
+}
+
diff --git a/android/telephony/data/DataCallResponse.java b/android/telephony/data/DataCallResponse.java
index da51c861..ef3a183f 100644
--- a/android/telephony/data/DataCallResponse.java
+++ b/android/telephony/data/DataCallResponse.java
@@ -20,6 +20,7 @@ package android.telephony.data;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
+import android.net.LinkAddress;
import android.os.Parcel;
import android.os.Parcelable;
@@ -40,7 +41,7 @@ public final class DataCallResponse implements Parcelable {
private final int mActive;
private final String mType;
private final String mIfname;
- private final List<InterfaceAddress> mAddresses;
+ private final List<LinkAddress> mAddresses;
private final List<InetAddress> mDnses;
private final List<InetAddress> mGateways;
private final List<String> mPcscfs;
@@ -71,7 +72,7 @@ public final class DataCallResponse implements Parcelable {
*/
public DataCallResponse(int status, int suggestedRetryTime, int cid, int active,
@Nullable String type, @Nullable String ifname,
- @Nullable List<InterfaceAddress> addresses,
+ @Nullable List<LinkAddress> addresses,
@Nullable List<InetAddress> dnses,
@Nullable List<InetAddress> gateways,
@Nullable List<String> pcscfs, int mtu) {
@@ -96,7 +97,7 @@ public final class DataCallResponse implements Parcelable {
mType = source.readString();
mIfname = source.readString();
mAddresses = new ArrayList<>();
- source.readList(mAddresses, InterfaceAddress.class.getClassLoader());
+ source.readList(mAddresses, LinkAddress.class.getClassLoader());
mDnses = new ArrayList<>();
source.readList(mDnses, InetAddress.class.getClassLoader());
mGateways = new ArrayList<>();
@@ -140,10 +141,10 @@ public final class DataCallResponse implements Parcelable {
public String getIfname() { return mIfname; }
/**
- * @return A list of {@link InterfaceAddress}
+ * @return A list of {@link LinkAddress}
*/
@NonNull
- public List<InterfaceAddress> getAddresses() { return mAddresses; }
+ public List<LinkAddress> getAddresses() { return mAddresses; }
/**
* @return A list of DNS server addresses, e.g., "192.0.1.3" or
diff --git a/android/telephony/data/DataService.java b/android/telephony/data/DataService.java
new file mode 100644
index 00000000..fa19ea06
--- /dev/null
+++ b/android/telephony/data/DataService.java
@@ -0,0 +1,567 @@
+/*
+ * 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.CallSuper;
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.Intent;
+import android.net.LinkProperties;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.telephony.AccessNetworkConstants;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionManager;
+import android.util.SparseArray;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base class of data service. Services that extend DataService must register the service in
+ * their AndroidManifest to be detected by the framework. They must be protected by the permission
+ * "android.permission.BIND_DATA_SERVICE". The data service definition in the manifest must follow
+ * the following format:
+ * ...
+ * <service android:name=".xxxDataService"
+ * android:permission="android.permission.BIND_DATA_SERVICE" >
+ * <intent-filter>
+ * <action android:name="android.telephony.data.DataService" />
+ * </intent-filter>
+ * </service>
+ * @hide
+ */
+@SystemApi
+public abstract class DataService extends Service {
+ private static final String TAG = DataService.class.getSimpleName();
+
+ public static final String DATA_SERVICE_INTERFACE = "android.telephony.data.DataService";
+ public static final String DATA_SERVICE_EXTRA_SLOT_ID = "android.telephony.data.extra.SLOT_ID";
+
+ /** {@hide} */
+ @IntDef(prefix = "REQUEST_REASON_", value = {
+ REQUEST_REASON_NORMAL,
+ REQUEST_REASON_HANDOVER,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface SetupDataReason {}
+
+ /** {@hide} */
+ @IntDef(prefix = "REQUEST_REASON_", value = {
+ REQUEST_REASON_NORMAL,
+ REQUEST_REASON_SHUTDOWN,
+ REQUEST_REASON_HANDOVER,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface DeactivateDataReason {}
+
+
+ /** The reason of the data request is normal */
+ public static final int REQUEST_REASON_NORMAL = 1;
+
+ /** The reason of the data request is device shutdown */
+ public static final int REQUEST_REASON_SHUTDOWN = 2;
+
+ /** The reason of the data request is IWLAN handover */
+ public static final int REQUEST_REASON_HANDOVER = 3;
+
+ private static final int DATA_SERVICE_INTERNAL_REQUEST_INITIALIZE_SERVICE = 1;
+ private static final int DATA_SERVICE_REQUEST_SETUP_DATA_CALL = 2;
+ private static final int DATA_SERVICE_REQUEST_DEACTIVATE_DATA_CALL = 3;
+ private static final int DATA_SERVICE_REQUEST_SET_INITIAL_ATTACH_APN = 4;
+ private static final int DATA_SERVICE_REQUEST_SET_DATA_PROFILE = 5;
+ private static final int DATA_SERVICE_REQUEST_GET_DATA_CALL_LIST = 6;
+ private static final int DATA_SERVICE_REQUEST_REGISTER_DATA_CALL_LIST_CHANGED = 7;
+ private static final int DATA_SERVICE_REQUEST_UNREGISTER_DATA_CALL_LIST_CHANGED = 8;
+ private static final int DATA_SERVICE_INDICATION_DATA_CALL_LIST_CHANGED = 9;
+
+ private final HandlerThread mHandlerThread;
+
+ private final DataServiceHandler mHandler;
+
+ private final SparseArray<DataServiceProvider> mServiceMap = new SparseArray<>();
+
+ private final SparseArray<IDataServiceWrapper> mBinderMap = new SparseArray<>();
+
+ /**
+ * The abstract class of the actual data service implementation. The data service provider
+ * must extend this class to support data connection. Note that each instance of data service
+ * provider is associated with one physical SIM slot.
+ */
+ public class DataServiceProvider {
+
+ private final int mSlotId;
+
+ private final List<IDataServiceCallback> mDataCallListChangedCallbacks = new ArrayList<>();
+
+ /**
+ * Constructor
+ * @param slotId SIM slot id the data service provider associated with.
+ */
+ public DataServiceProvider(int slotId) {
+ mSlotId = slotId;
+ }
+
+ /**
+ * @return SIM slot id the data service provider associated with.
+ */
+ public final int getSlotId() {
+ return mSlotId;
+ }
+
+ /**
+ * Setup a data connection. The data service provider must implement this method to support
+ * establishing a packet data connection. When completed or error, the service must invoke
+ * the provided callback to notify the platform.
+ *
+ * @param accessNetworkType Access network type that the data call will be established on.
+ * Must be one of {@link AccessNetworkConstants.AccessNetworkType}.
+ * @param dataProfile Data profile used for data call setup. See {@link DataProfile}
+ * @param isRoaming True if the device is data roaming.
+ * @param allowRoaming True if data roaming is allowed by the user.
+ * @param reason The reason for data setup. Must be {@link #REQUEST_REASON_NORMAL} or
+ * {@link #REQUEST_REASON_HANDOVER}.
+ * @param linkProperties If {@code reason} is {@link #REQUEST_REASON_HANDOVER}, this is the
+ * link properties of the existing data connection, otherwise null.
+ * @param callback The result callback for this request.
+ */
+ public void setupDataCall(int accessNetworkType, DataProfile dataProfile, boolean isRoaming,
+ boolean allowRoaming, @SetupDataReason int reason,
+ LinkProperties linkProperties, DataServiceCallback callback) {
+ // The default implementation is to return unsupported.
+ callback.onSetupDataCallComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED, null);
+ }
+
+ /**
+ * Deactivate a data connection. The data service provider must implement this method to
+ * support data connection tear down. When completed or error, the service must invoke the
+ * provided callback to notify the platform.
+ *
+ * @param cid Call id returned in the callback of {@link DataServiceProvider#setupDataCall(
+ * int, DataProfile, boolean, boolean, int, LinkProperties, DataServiceCallback)}.
+ * @param reason The reason for data deactivation. Must be {@link #REQUEST_REASON_NORMAL},
+ * {@link #REQUEST_REASON_SHUTDOWN} or {@link #REQUEST_REASON_HANDOVER}.
+ * @param callback The result callback for this request.
+ */
+ public void deactivateDataCall(int cid, @DeactivateDataReason int reason,
+ DataServiceCallback callback) {
+ // The default implementation is to return unsupported.
+ callback.onDeactivateDataCallComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Set an APN to initial attach network.
+ *
+ * @param dataProfile Data profile used for data call setup. See {@link DataProfile}.
+ * @param isRoaming True if the device is data roaming.
+ * @param callback The result callback for this request.
+ */
+ public void setInitialAttachApn(DataProfile dataProfile, boolean isRoaming,
+ DataServiceCallback callback) {
+ // The default implementation is to return unsupported.
+ callback.onSetInitialAttachApnComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Send current carrier's data profiles to the data service for data call setup. This is
+ * only for CDMA carrier that can change the profile through OTA. The data service should
+ * always uses the latest data profile sent by the framework.
+ *
+ * @param dps A list of data profiles.
+ * @param isRoaming True if the device is data roaming.
+ * @param callback The result callback for this request.
+ */
+ public void setDataProfile(List<DataProfile> dps, boolean isRoaming,
+ DataServiceCallback callback) {
+ // The default implementation is to return unsupported.
+ callback.onSetDataProfileComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED);
+ }
+
+ /**
+ * Get the active data call list.
+ *
+ * @param callback The result callback for this request.
+ */
+ public void getDataCallList(DataServiceCallback callback) {
+ // The default implementation is to return unsupported.
+ callback.onGetDataCallListComplete(DataServiceCallback.RESULT_ERROR_UNSUPPORTED, null);
+ }
+
+ private void registerForDataCallListChanged(IDataServiceCallback callback) {
+ synchronized (mDataCallListChangedCallbacks) {
+ mDataCallListChangedCallbacks.add(callback);
+ }
+ }
+
+ private void unregisterForDataCallListChanged(IDataServiceCallback callback) {
+ synchronized (mDataCallListChangedCallbacks) {
+ mDataCallListChangedCallbacks.remove(callback);
+ }
+ }
+
+ /**
+ * Notify the system that current data call list changed. Data service must invoke this
+ * method whenever there is any data call status changed.
+ *
+ * @param dataCallList List of the current active data call.
+ */
+ public final void notifyDataCallListChanged(List<DataCallResponse> dataCallList) {
+ synchronized (mDataCallListChangedCallbacks) {
+ for (IDataServiceCallback callback : mDataCallListChangedCallbacks) {
+ mHandler.obtainMessage(DATA_SERVICE_INDICATION_DATA_CALL_LIST_CHANGED, mSlotId,
+ 0, new DataCallListChangedIndication(dataCallList, callback))
+ .sendToTarget();
+ }
+ }
+ }
+
+ /**
+ * Called when the instance of data service is destroyed (e.g. got unbind or binder died).
+ */
+ @CallSuper
+ protected void onDestroy() {
+ mDataCallListChangedCallbacks.clear();
+ }
+ }
+
+ private static final class SetupDataCallRequest {
+ public final int accessNetworkType;
+ public final DataProfile dataProfile;
+ public final boolean isRoaming;
+ public final boolean allowRoaming;
+ public final int reason;
+ public final LinkProperties linkProperties;
+ public final IDataServiceCallback callback;
+ SetupDataCallRequest(int accessNetworkType, DataProfile dataProfile, boolean isRoaming,
+ boolean allowRoaming, int reason, LinkProperties linkProperties,
+ IDataServiceCallback callback) {
+ this.accessNetworkType = accessNetworkType;
+ this.dataProfile = dataProfile;
+ this.isRoaming = isRoaming;
+ this.allowRoaming = allowRoaming;
+ this.linkProperties = linkProperties;
+ this.reason = reason;
+ this.callback = callback;
+ }
+ }
+
+ private static final class DeactivateDataCallRequest {
+ public final int cid;
+ public final int reason;
+ public final IDataServiceCallback callback;
+ DeactivateDataCallRequest(int cid, int reason, IDataServiceCallback callback) {
+ this.cid = cid;
+ this.reason = reason;
+ this.callback = callback;
+ }
+ }
+
+ private static final class SetInitialAttachApnRequest {
+ public final DataProfile dataProfile;
+ public final boolean isRoaming;
+ public final IDataServiceCallback callback;
+ SetInitialAttachApnRequest(DataProfile dataProfile, boolean isRoaming,
+ IDataServiceCallback callback) {
+ this.dataProfile = dataProfile;
+ this.isRoaming = isRoaming;
+ this.callback = callback;
+ }
+ }
+
+ private static final class SetDataProfileRequest {
+ public final List<DataProfile> dps;
+ public final boolean isRoaming;
+ public final IDataServiceCallback callback;
+ SetDataProfileRequest(List<DataProfile> dps, boolean isRoaming,
+ IDataServiceCallback callback) {
+ this.dps = dps;
+ this.isRoaming = isRoaming;
+ this.callback = callback;
+ }
+ }
+
+ private static final class DataCallListChangedIndication {
+ public final List<DataCallResponse> dataCallList;
+ public final IDataServiceCallback callback;
+ DataCallListChangedIndication(List<DataCallResponse> dataCallList,
+ IDataServiceCallback callback) {
+ this.dataCallList = dataCallList;
+ this.callback = callback;
+ }
+ }
+
+ private class DataServiceHandler extends Handler {
+
+ DataServiceHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ IDataServiceCallback callback;
+ final int slotId = message.arg1;
+ DataServiceProvider service;
+
+ synchronized (mServiceMap) {
+ service = mServiceMap.get(slotId);
+ }
+
+ switch (message.what) {
+ case DATA_SERVICE_INTERNAL_REQUEST_INITIALIZE_SERVICE:
+ service = createDataServiceProvider(message.arg1);
+ if (service != null) {
+ mServiceMap.put(slotId, service);
+ }
+ break;
+ case DATA_SERVICE_REQUEST_SETUP_DATA_CALL:
+ if (service == null) break;
+ SetupDataCallRequest setupDataCallRequest = (SetupDataCallRequest) message.obj;
+ service.setupDataCall(setupDataCallRequest.accessNetworkType,
+ setupDataCallRequest.dataProfile, setupDataCallRequest.isRoaming,
+ setupDataCallRequest.allowRoaming, setupDataCallRequest.reason,
+ setupDataCallRequest.linkProperties,
+ new DataServiceCallback(setupDataCallRequest.callback));
+
+ break;
+ case DATA_SERVICE_REQUEST_DEACTIVATE_DATA_CALL:
+ if (service == null) break;
+ DeactivateDataCallRequest deactivateDataCallRequest =
+ (DeactivateDataCallRequest) message.obj;
+ service.deactivateDataCall(deactivateDataCallRequest.cid,
+ deactivateDataCallRequest.reason,
+ new DataServiceCallback(deactivateDataCallRequest.callback));
+ break;
+ case DATA_SERVICE_REQUEST_SET_INITIAL_ATTACH_APN:
+ if (service == null) break;
+ SetInitialAttachApnRequest setInitialAttachApnRequest =
+ (SetInitialAttachApnRequest) message.obj;
+ service.setInitialAttachApn(setInitialAttachApnRequest.dataProfile,
+ setInitialAttachApnRequest.isRoaming,
+ new DataServiceCallback(setInitialAttachApnRequest.callback));
+ break;
+ case DATA_SERVICE_REQUEST_SET_DATA_PROFILE:
+ if (service == null) break;
+ SetDataProfileRequest setDataProfileRequest =
+ (SetDataProfileRequest) message.obj;
+ service.setDataProfile(setDataProfileRequest.dps,
+ setDataProfileRequest.isRoaming,
+ new DataServiceCallback(setDataProfileRequest.callback));
+ break;
+ case DATA_SERVICE_REQUEST_GET_DATA_CALL_LIST:
+ if (service == null) break;
+
+ service.getDataCallList(new DataServiceCallback(
+ (IDataServiceCallback) message.obj));
+ break;
+ case DATA_SERVICE_REQUEST_REGISTER_DATA_CALL_LIST_CHANGED:
+ if (service == null) break;
+ service.registerForDataCallListChanged((IDataServiceCallback) message.obj);
+ break;
+ case DATA_SERVICE_REQUEST_UNREGISTER_DATA_CALL_LIST_CHANGED:
+ if (service == null) break;
+ callback = (IDataServiceCallback) message.obj;
+ service.unregisterForDataCallListChanged(callback);
+ break;
+ case DATA_SERVICE_INDICATION_DATA_CALL_LIST_CHANGED:
+ if (service == null) break;
+ DataCallListChangedIndication indication =
+ (DataCallListChangedIndication) message.obj;
+ try {
+ indication.callback.onDataCallListChanged(indication.dataCallList);
+ } catch (RemoteException e) {
+ loge("Failed to call onDataCallListChanged. " + e);
+ }
+ break;
+ }
+ }
+ }
+
+ /** @hide */
+ protected DataService() {
+ mHandlerThread = new HandlerThread(TAG);
+ mHandlerThread.start();
+
+ mHandler = new DataServiceHandler(mHandlerThread.getLooper());
+ log("Data service created");
+ }
+
+ /**
+ * Create the instance of {@link DataServiceProvider}. Data service provider must override
+ * this method to facilitate the creation of {@link DataServiceProvider} instances. The system
+ * will call this method after binding the data service for each active SIM slot id.
+ *
+ * @param slotId SIM slot id the data service associated with.
+ * @return Data service object
+ */
+ public abstract DataServiceProvider createDataServiceProvider(int slotId);
+
+ /** @hide */
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (intent == null || !DATA_SERVICE_INTERFACE.equals(intent.getAction())) {
+ loge("Unexpected intent " + intent);
+ return null;
+ }
+
+ int slotId = intent.getIntExtra(
+ DATA_SERVICE_EXTRA_SLOT_ID, SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+
+ if (!SubscriptionManager.isValidSlotIndex(slotId)) {
+ loge("Invalid slot id " + slotId);
+ return null;
+ }
+
+ log("onBind: slot id=" + slotId);
+
+ IDataServiceWrapper binder = mBinderMap.get(slotId);
+ if (binder == null) {
+ Message msg = mHandler.obtainMessage(DATA_SERVICE_INTERNAL_REQUEST_INITIALIZE_SERVICE);
+ msg.arg1 = slotId;
+ msg.sendToTarget();
+
+ binder = new IDataServiceWrapper(slotId);
+ mBinderMap.put(slotId, binder);
+ }
+
+ return binder;
+ }
+
+ /** @hide */
+ @Override
+ public boolean onUnbind(Intent intent) {
+ int slotId = intent.getIntExtra(DATA_SERVICE_EXTRA_SLOT_ID,
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX);
+ if (mBinderMap.get(slotId) != null) {
+ DataServiceProvider serviceImpl;
+ synchronized (mServiceMap) {
+ serviceImpl = mServiceMap.get(slotId);
+ }
+ if (serviceImpl != null) {
+ serviceImpl.onDestroy();
+ }
+ mBinderMap.remove(slotId);
+ }
+
+ // If all clients unbinds, quit the handler thread
+ if (mBinderMap.size() == 0) {
+ mHandlerThread.quit();
+ }
+
+ return false;
+ }
+
+ /** @hide */
+ @Override
+ public void onDestroy() {
+ synchronized (mServiceMap) {
+ for (int i = 0; i < mServiceMap.size(); i++) {
+ DataServiceProvider serviceImpl = mServiceMap.get(i);
+ if (serviceImpl != null) {
+ serviceImpl.onDestroy();
+ }
+ }
+ mServiceMap.clear();
+ }
+
+ mHandlerThread.quit();
+ }
+
+ /**
+ * A wrapper around IDataService that forwards calls to implementations of {@link DataService}.
+ */
+ private class IDataServiceWrapper extends IDataService.Stub {
+
+ private final int mSlotId;
+
+ IDataServiceWrapper(int slotId) {
+ mSlotId = slotId;
+ }
+
+ @Override
+ public void setupDataCall(int accessNetworkType, DataProfile dataProfile,
+ boolean isRoaming, boolean allowRoaming, int reason,
+ LinkProperties linkProperties, IDataServiceCallback callback) {
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_SETUP_DATA_CALL, mSlotId, 0,
+ new SetupDataCallRequest(accessNetworkType, dataProfile, isRoaming,
+ allowRoaming, reason, linkProperties, callback))
+ .sendToTarget();
+ }
+
+ @Override
+ public void deactivateDataCall(int cid, int reason, IDataServiceCallback callback) {
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_DEACTIVATE_DATA_CALL, mSlotId, 0,
+ new DeactivateDataCallRequest(cid, reason, callback))
+ .sendToTarget();
+ }
+
+ @Override
+ public void setInitialAttachApn(DataProfile dataProfile, boolean isRoaming,
+ IDataServiceCallback callback) {
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_SET_INITIAL_ATTACH_APN, mSlotId, 0,
+ new SetInitialAttachApnRequest(dataProfile, isRoaming, callback))
+ .sendToTarget();
+ }
+
+ @Override
+ public void setDataProfile(List<DataProfile> dps, boolean isRoaming,
+ IDataServiceCallback callback) {
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_SET_DATA_PROFILE, mSlotId, 0,
+ new SetDataProfileRequest(dps, isRoaming, callback)).sendToTarget();
+ }
+
+ @Override
+ public void getDataCallList(IDataServiceCallback callback) {
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_GET_DATA_CALL_LIST, mSlotId, 0,
+ callback).sendToTarget();
+ }
+
+ @Override
+ public void registerForDataCallListChanged(IDataServiceCallback callback) {
+ if (callback == null) {
+ loge("Callback is null");
+ return;
+ }
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_REGISTER_DATA_CALL_LIST_CHANGED, mSlotId,
+ 0, callback).sendToTarget();
+ }
+
+ @Override
+ public void unregisterForDataCallListChanged(IDataServiceCallback callback) {
+ if (callback == null) {
+ loge("Callback is null");
+ return;
+ }
+ mHandler.obtainMessage(DATA_SERVICE_REQUEST_UNREGISTER_DATA_CALL_LIST_CHANGED, mSlotId,
+ 0, callback).sendToTarget();
+ }
+ }
+
+ private void log(String s) {
+ Rlog.d(TAG, s);
+ }
+
+ private void loge(String s) {
+ Rlog.e(TAG, s);
+ }
+}
diff --git a/android/telephony/data/DataServiceCallback.java b/android/telephony/data/DataServiceCallback.java
new file mode 100644
index 00000000..b6a81f94
--- /dev/null
+++ b/android/telephony/data/DataServiceCallback.java
@@ -0,0 +1,172 @@
+/*
+ * 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.IntDef;
+import android.annotation.SystemApi;
+import android.os.RemoteException;
+import android.telephony.Rlog;
+import android.telephony.data.DataService.DataServiceProvider;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+/**
+ * Data service callback, which is for bound data service to invoke for solicited and unsolicited
+ * response. The caller is responsible to create a callback object for each single asynchronous
+ * request.
+ *
+ * @hide
+ */
+@SystemApi
+public class DataServiceCallback {
+
+ private static final String mTag = DataServiceCallback.class.getSimpleName();
+
+ /**
+ * Result of data requests
+ * @hide
+ */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({RESULT_SUCCESS, RESULT_ERROR_UNSUPPORTED, RESULT_ERROR_INVALID_ARG, RESULT_ERROR_BUSY,
+ RESULT_ERROR_ILLEGAL_STATE})
+ public @interface Result {}
+
+ /** Request is completed successfully */
+ public static final int RESULT_SUCCESS = 0;
+ /** Request is not support */
+ public static final int RESULT_ERROR_UNSUPPORTED = 1;
+ /** Request contains invalid arguments */
+ public static final int RESULT_ERROR_INVALID_ARG = 2;
+ /** Service is busy */
+ public static final int RESULT_ERROR_BUSY = 3;
+ /** Request sent in illegal state */
+ public static final int RESULT_ERROR_ILLEGAL_STATE = 4;
+
+ private final WeakReference<IDataServiceCallback> mCallback;
+
+ /** @hide */
+ public DataServiceCallback(IDataServiceCallback callback) {
+ mCallback = new WeakReference<>(callback);
+ }
+
+ /**
+ * Called to indicate result for the request {@link DataServiceProvider#setupDataCall(int,
+ * DataProfile, boolean, boolean, boolean, DataServiceCallback)}.
+ *
+ * @param result The result code. Must be one of the {@link Result}.
+ * @param response Setup data call response.
+ */
+ public void onSetupDataCallComplete(@Result int result, DataCallResponse response) {
+ IDataServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onSetupDataCallComplete(result, response);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onSetupDataCallComplete on the remote");
+ }
+ }
+ }
+
+ /**
+ * Called to indicate result for the request {@link DataServiceProvider#deactivateDataCall(int,
+ * boolean, boolean, DataServiceCallback)}.
+ *
+ * @param result The result code. Must be one of the {@link Result}.
+ */
+ public void onDeactivateDataCallComplete(@Result int result) {
+ IDataServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onDeactivateDataCallComplete(result);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onDeactivateDataCallComplete on the remote");
+ }
+ }
+ }
+
+ /**
+ * Called to indicate result for the request {@link DataServiceProvider#setInitialAttachApn(
+ * DataProfile, boolean, DataServiceCallback)}.
+ *
+ * @param result The result code. Must be one of the {@link Result}.
+ */
+ public void onSetInitialAttachApnComplete(@Result int result) {
+ IDataServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onSetInitialAttachApnComplete(result);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onSetInitialAttachApnComplete on the remote");
+ }
+ }
+ }
+
+ /**
+ * Called to indicate result for the request {@link DataServiceProvider#setDataProfile(List,
+ * boolean, DataServiceCallback)}.
+ *
+ * @param result The result code. Must be one of the {@link Result}.
+ */
+ @SystemApi
+ public void onSetDataProfileComplete(@Result int result) {
+ IDataServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onSetDataProfileComplete(result);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onSetDataProfileComplete on the remote");
+ }
+ }
+ }
+
+ /**
+ * Called to indicate result for the request {@link DataServiceProvider#getDataCallList(
+ * DataServiceCallback)}.
+ *
+ * @param result The result code. Must be one of the {@link Result}.
+ * @param dataCallList List of the current active data connection.
+ */
+ public void onGetDataCallListComplete(@Result int result, List<DataCallResponse> dataCallList) {
+ IDataServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onGetDataCallListComplete(result, dataCallList);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onGetDataCallListComplete on the remote");
+ }
+ }
+ }
+
+ /**
+ * Called to indicate that data connection list changed.
+ *
+ * @param dataCallList List of the current active data connection.
+ */
+ public void onDataCallListChanged(List<DataCallResponse> dataCallList) {
+ IDataServiceCallback callback = mCallback.get();
+ if (callback != null) {
+ try {
+ callback.onDataCallListChanged(dataCallList);
+ } catch (RemoteException e) {
+ Rlog.e(mTag, "Failed to onDataCallListChanged on the remote");
+ }
+ }
+ }
+}
diff --git a/android/telephony/data/InterfaceAddress.java b/android/telephony/data/InterfaceAddress.java
deleted file mode 100644
index 00d212a5..00000000
--- a/android/telephony/data/InterfaceAddress.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.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/EuiccCardManager.java b/android/telephony/euicc/EuiccCardManager.java
new file mode 100644
index 00000000..88bae336
--- /dev/null
+++ b/android/telephony/euicc/EuiccCardManager.java
@@ -0,0 +1,680 @@
+/*
+ * 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.telephony.euicc;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.service.euicc.EuiccProfileInfo;
+import android.util.Log;
+
+import com.android.internal.telephony.euicc.IAuthenticateServerCallback;
+import com.android.internal.telephony.euicc.ICancelSessionCallback;
+import com.android.internal.telephony.euicc.IDeleteProfileCallback;
+import com.android.internal.telephony.euicc.IDisableProfileCallback;
+import com.android.internal.telephony.euicc.IEuiccCardController;
+import com.android.internal.telephony.euicc.IGetAllProfilesCallback;
+import com.android.internal.telephony.euicc.IGetDefaultSmdpAddressCallback;
+import com.android.internal.telephony.euicc.IGetEuiccChallengeCallback;
+import com.android.internal.telephony.euicc.IGetEuiccInfo1Callback;
+import com.android.internal.telephony.euicc.IGetEuiccInfo2Callback;
+import com.android.internal.telephony.euicc.IGetProfileCallback;
+import com.android.internal.telephony.euicc.IGetRulesAuthTableCallback;
+import com.android.internal.telephony.euicc.IGetSmdsAddressCallback;
+import com.android.internal.telephony.euicc.IListNotificationsCallback;
+import com.android.internal.telephony.euicc.ILoadBoundProfilePackageCallback;
+import com.android.internal.telephony.euicc.IPrepareDownloadCallback;
+import com.android.internal.telephony.euicc.IRemoveNotificationFromListCallback;
+import com.android.internal.telephony.euicc.IResetMemoryCallback;
+import com.android.internal.telephony.euicc.IRetrieveNotificationCallback;
+import com.android.internal.telephony.euicc.IRetrieveNotificationListCallback;
+import com.android.internal.telephony.euicc.ISetDefaultSmdpAddressCallback;
+import com.android.internal.telephony.euicc.ISetNicknameCallback;
+import com.android.internal.telephony.euicc.ISwitchToProfileCallback;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * EuiccCardManager is the application interface to an eSIM card.
+ *
+ * @hide
+ *
+ * TODO(b/35851809): Make this a SystemApi.
+ */
+public class EuiccCardManager {
+ private static final String TAG = "EuiccCardManager";
+
+ /** Reason for canceling a profile download session */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = { "CANCEL_REASON_" }, value = {
+ CANCEL_REASON_END_USER_REJECTED,
+ CANCEL_REASON_POSTPONED,
+ CANCEL_REASON_TIMEOUT,
+ CANCEL_REASON_PPR_NOT_ALLOWED
+ })
+ public @interface CancelReason {}
+
+ /**
+ * The end user has rejected the download. The profile will be put into the error state and
+ * cannot be downloaded again without the operator's change.
+ */
+ public static final int CANCEL_REASON_END_USER_REJECTED = 0;
+
+ /** The download has been postponed and can be restarted later. */
+ public static final int CANCEL_REASON_POSTPONED = 1;
+
+ /** The download has been timed out and can be restarted later. */
+ public static final int CANCEL_REASON_TIMEOUT = 2;
+
+ /**
+ * The profile to be downloaded cannot be installed due to its policy rule is not allowed by
+ * the RAT (Rules Authorisation Table) on the eUICC or by other installed profiles. The
+ * download can be restarted later.
+ */
+ public static final int CANCEL_REASON_PPR_NOT_ALLOWED = 3;
+
+ /** Options for resetting eUICC memory */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "RESET_OPTION_" }, value = {
+ RESET_OPTION_DELETE_OPERATIONAL_PROFILES,
+ RESET_OPTION_DELETE_FIELD_LOADED_TEST_PROFILES,
+ RESET_OPTION_RESET_DEFAULT_SMDP_ADDRESS
+ })
+ public @interface ResetOption {}
+
+ /** Deletes all operational profiles. */
+ public static final int RESET_OPTION_DELETE_OPERATIONAL_PROFILES = 1;
+
+ /** Deletes all field-loaded testing profiles. */
+ public static final int RESET_OPTION_DELETE_FIELD_LOADED_TEST_PROFILES = 1 << 1;
+
+ /** Resets the default SM-DP+ address. */
+ public static final int RESET_OPTION_RESET_DEFAULT_SMDP_ADDRESS = 1 << 2;
+
+ /** Result code of execution with no error. */
+ public static final int RESULT_OK = 0;
+
+ /**
+ * Callback to receive the result of an eUICC card API.
+ *
+ * @param <T> Type of the result.
+ */
+ public interface ResultCallback<T> {
+ /**
+ * This method will be called when an eUICC card API call is completed.
+ *
+ * @param resultCode This can be {@link #RESULT_OK} or other positive values returned by the
+ * eUICC.
+ * @param result The result object. It can be null if the {@code resultCode} is not
+ * {@link #RESULT_OK}.
+ */
+ void onComplete(int resultCode, T result);
+ }
+
+ private final Context mContext;
+
+ /** @hide */
+ public EuiccCardManager(Context context) {
+ mContext = context;
+ }
+
+ private IEuiccCardController getIEuiccCardController() {
+ return IEuiccCardController.Stub.asInterface(
+ ServiceManager.getService("euicc_card_controller"));
+ }
+
+ /**
+ * Gets all the profiles on eUicc.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback The callback to get the result code and all the profiles.
+ */
+ public void getAllProfiles(String cardId, ResultCallback<EuiccProfileInfo[]> callback) {
+ try {
+ getIEuiccCardController().getAllProfiles(mContext.getOpPackageName(), cardId,
+ new IGetAllProfilesCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccProfileInfo[] profiles) {
+ callback.onComplete(resultCode, profiles);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getAllProfiles", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the profile of the given iccid.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param iccid The iccid of the profile.
+ * @param callback The callback to get the result code and profile.
+ */
+ public void getProfile(String cardId, String iccid, ResultCallback<EuiccProfileInfo> callback) {
+ try {
+ getIEuiccCardController().getProfile(mContext.getOpPackageName(), cardId, iccid,
+ new IGetProfileCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccProfileInfo profile) {
+ callback.onComplete(resultCode, profile);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getProfile", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Disables the profile of the given iccid.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param iccid The iccid of the profile.
+ * @param refresh Whether sending the REFRESH command to modem.
+ * @param callback The callback to get the result code.
+ */
+ public void disableProfile(String cardId, String iccid, boolean refresh,
+ ResultCallback<Void> callback) {
+ try {
+ getIEuiccCardController().disableProfile(mContext.getOpPackageName(), cardId, iccid,
+ refresh, new IDisableProfileCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode) {
+ callback.onComplete(resultCode, null);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling disableProfile", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Switches from the current profile to another profile. The current profile will be disabled
+ * and the specified profile will be enabled.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param iccid The iccid of the profile to switch to.
+ * @param refresh Whether sending the REFRESH command to modem.
+ * @param callback The callback to get the result code and the EuiccProfileInfo enabled.
+ */
+ public void switchToProfile(String cardId, String iccid, boolean refresh,
+ ResultCallback<EuiccProfileInfo> callback) {
+ try {
+ getIEuiccCardController().switchToProfile(mContext.getOpPackageName(), cardId, iccid,
+ refresh, new ISwitchToProfileCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccProfileInfo profile) {
+ callback.onComplete(resultCode, profile);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling switchToProfile", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets the nickname of the profile of the given iccid.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param iccid The iccid of the profile.
+ * @param nickname The nickname of the profile.
+ * @param callback The callback to get the result code.
+ */
+ public void setNickname(String cardId, String iccid, String nickname,
+ ResultCallback<Void> callback) {
+ try {
+ getIEuiccCardController().setNickname(mContext.getOpPackageName(), cardId, iccid,
+ nickname, new ISetNicknameCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode) {
+ callback.onComplete(resultCode, null);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling setNickname", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Deletes the profile of the given iccid from eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param iccid The iccid of the profile.
+ * @param callback The callback to get the result code.
+ */
+ public void deleteProfile(String cardId, String iccid, ResultCallback<Void> callback) {
+ try {
+ getIEuiccCardController().deleteProfile(mContext.getOpPackageName(), cardId, iccid,
+ new IDeleteProfileCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode) {
+ callback.onComplete(resultCode, null);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling deleteProfile", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Resets the eUICC memory.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param options Bits of the options of resetting which parts of the eUICC memory. See
+ * EuiccCard for details.
+ * @param callback The callback to get the result code.
+ */
+ public void resetMemory(String cardId, @ResetOption int options, ResultCallback<Void> callback) {
+ try {
+ getIEuiccCardController().resetMemory(mContext.getOpPackageName(), cardId, options,
+ new IResetMemoryCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode) {
+ callback.onComplete(resultCode, null);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling resetMemory", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the default SM-DP+ address from eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback The callback to get the result code and the default SM-DP+ address.
+ */
+ public void getDefaultSmdpAddress(String cardId, ResultCallback<String> callback) {
+ try {
+ getIEuiccCardController().getDefaultSmdpAddress(mContext.getOpPackageName(), cardId,
+ new IGetDefaultSmdpAddressCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, String address) {
+ callback.onComplete(resultCode, address);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getDefaultSmdpAddress", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the SM-DS address from eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback The callback to get the result code and the SM-DS address.
+ */
+ public void getSmdsAddress(String cardId, ResultCallback<String> callback) {
+ try {
+ getIEuiccCardController().getSmdsAddress(mContext.getOpPackageName(), cardId,
+ new IGetSmdsAddressCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, String address) {
+ callback.onComplete(resultCode, address);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getSmdsAddress", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Sets the default SM-DP+ address of eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param defaultSmdpAddress The default SM-DP+ address to set.
+ * @param callback The callback to get the result code.
+ */
+ public void setDefaultSmdpAddress(String cardId, String defaultSmdpAddress, ResultCallback<Void> callback) {
+ try {
+ getIEuiccCardController().setDefaultSmdpAddress(mContext.getOpPackageName(), cardId,
+ defaultSmdpAddress,
+ new ISetDefaultSmdpAddressCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode) {
+ callback.onComplete(resultCode, null);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling setDefaultSmdpAddress", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets Rules Authorisation Table.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback the callback to get the result code and the rule authorisation table.
+ */
+ public void getRulesAuthTable(String cardId, ResultCallback<EuiccRulesAuthTable> callback) {
+ try {
+ getIEuiccCardController().getRulesAuthTable(mContext.getOpPackageName(), cardId,
+ new IGetRulesAuthTableCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccRulesAuthTable rat) {
+ callback.onComplete(resultCode, rat);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getRulesAuthTable", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the eUICC challenge for new profile downloading.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback the callback to get the result code and the challenge.
+ */
+ public void getEuiccChallenge(String cardId, ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().getEuiccChallenge(mContext.getOpPackageName(), cardId,
+ new IGetEuiccChallengeCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] challenge) {
+ callback.onComplete(resultCode, challenge);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getEuiccChallenge", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the eUICC info1 defined in GSMA RSP v2.0+ for new profile downloading.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback the callback to get the result code and the info1.
+ */
+ public void getEuiccInfo1(String cardId, ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().getEuiccInfo1(mContext.getOpPackageName(), cardId,
+ new IGetEuiccInfo1Callback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] info) {
+ callback.onComplete(resultCode, info);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getEuiccInfo1", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Gets the eUICC info2 defined in GSMA RSP v2.0+ for new profile downloading.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param callback the callback to get the result code and the info2.
+ */
+ public void getEuiccInfo2(String cardId, ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().getEuiccInfo2(mContext.getOpPackageName(), cardId,
+ new IGetEuiccInfo2Callback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] info) {
+ callback.onComplete(resultCode, info);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling getEuiccInfo2", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Authenticates the SM-DP+ server by the eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param matchingId the activation code token defined in GSMA RSP v2.0+ or empty when it is not
+ * required.
+ * @param serverSigned1 ASN.1 data in byte array signed and returned by the SM-DP+ server.
+ * @param serverSignature1 ASN.1 data in byte array indicating a SM-DP+ signature which is
+ * returned by SM-DP+ server.
+ * @param euiccCiPkIdToBeUsed ASN.1 data in byte array indicating CI Public Key Identifier to be
+ * used by the eUICC for signature which is returned by SM-DP+ server. This is defined in
+ * GSMA RSP v2.0+.
+ * @param serverCertificate ASN.1 data in byte array indicating SM-DP+ Certificate returned by
+ * SM-DP+ server.
+ * @param callback the callback to get the result code and a byte array which represents a
+ * {@code AuthenticateServerResponse} defined in GSMA RSP v2.0+.
+ */
+ public void authenticateServer(String cardId, String matchingId, byte[] serverSigned1,
+ byte[] serverSignature1, byte[] euiccCiPkIdToBeUsed, byte[] serverCertificate,
+ ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().authenticateServer(
+ mContext.getOpPackageName(),
+ cardId,
+ matchingId,
+ serverSigned1,
+ serverSignature1,
+ euiccCiPkIdToBeUsed,
+ serverCertificate,
+ new IAuthenticateServerCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] response) {
+ callback.onComplete(resultCode, response);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling authenticateServer", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Prepares the profile download request sent to SM-DP+.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param hashCc the hash of confirmation code. It can be null if there is no confirmation code
+ * required.
+ * @param smdpSigned2 ASN.1 data in byte array indicating the data to be signed by the SM-DP+
+ * returned by SM-DP+ server.
+ * @param smdpSignature2 ASN.1 data in byte array indicating the SM-DP+ signature returned by
+ * SM-DP+ server.
+ * @param smdpCertificate ASN.1 data in byte array indicating the SM-DP+ Certificate returned
+ * by SM-DP+ server.
+ * @param callback the callback to get the result code and a byte array which represents a
+ * {@code PrepareDownloadResponse} defined in GSMA RSP v2.0+
+ */
+ public void prepareDownload(String cardId, @Nullable byte[] hashCc, byte[] smdpSigned2,
+ byte[] smdpSignature2, byte[] smdpCertificate, ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().prepareDownload(
+ mContext.getOpPackageName(),
+ cardId,
+ hashCc,
+ smdpSigned2,
+ smdpSignature2,
+ smdpCertificate,
+ new IPrepareDownloadCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] response) {
+ callback.onComplete(resultCode, response);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling prepareDownload", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Loads a downloaded bound profile package onto the eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param boundProfilePackage the Bound Profile Package data returned by SM-DP+ server.
+ * @param callback the callback to get the result code and a byte array which represents a
+ * {@code LoadBoundProfilePackageResponse} defined in GSMA RSP v2.0+.
+ */
+ public void loadBoundProfilePackage(String cardId, byte[] boundProfilePackage,
+ ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().loadBoundProfilePackage(
+ mContext.getOpPackageName(),
+ cardId,
+ boundProfilePackage,
+ new ILoadBoundProfilePackageCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] response) {
+ callback.onComplete(resultCode, response);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling loadBoundProfilePackage", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Cancels the current profile download session.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param transactionId the transaction ID returned by SM-DP+ server.
+ * @param reason the cancel reason.
+ * @param callback the callback to get the result code and an byte[] which represents a
+ * {@code CancelSessionResponse} defined in GSMA RSP v2.0+.
+ */
+ public void cancelSession(String cardId, byte[] transactionId, @CancelReason int reason,
+ ResultCallback<byte[]> callback) {
+ try {
+ getIEuiccCardController().cancelSession(
+ mContext.getOpPackageName(),
+ cardId,
+ transactionId,
+ reason,
+ new ICancelSessionCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, byte[] response) {
+ callback.onComplete(resultCode, response);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling cancelSession", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Lists all notifications of the given {@code notificationEvents}.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param events bits of the event types ({@link EuiccNotification.Event}) to list.
+ * @param callback the callback to get the result code and the list of notifications.
+ */
+ public void listNotifications(String cardId, @EuiccNotification.Event int events,
+ ResultCallback<EuiccNotification[]> callback) {
+ try {
+ getIEuiccCardController().listNotifications(mContext.getOpPackageName(), cardId, events,
+ new IListNotificationsCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccNotification[] notifications) {
+ callback.onComplete(resultCode, notifications);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling listNotifications", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieves contents of all notification of the given {@code events}.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param events bits of the event types ({@link EuiccNotification.Event}) to list.
+ * @param callback the callback to get the result code and the list of notifications.
+ */
+ public void retrieveNotificationList(String cardId, @EuiccNotification.Event int events,
+ ResultCallback<EuiccNotification[]> callback) {
+ try {
+ getIEuiccCardController().retrieveNotificationList(mContext.getOpPackageName(), cardId,
+ events, new IRetrieveNotificationListCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccNotification[] notifications) {
+ callback.onComplete(resultCode, notifications);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling retrieveNotificationList", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Retrieves the content of a notification of the given {@code seqNumber}.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param seqNumber the sequence number of the notification.
+ * @param callback the callback to get the result code and the notification.
+ */
+ public void retrieveNotification(String cardId, int seqNumber,
+ ResultCallback<EuiccNotification> callback) {
+ try {
+ getIEuiccCardController().retrieveNotification(mContext.getOpPackageName(), cardId,
+ seqNumber, new IRetrieveNotificationCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode, EuiccNotification notification) {
+ callback.onComplete(resultCode, notification);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling retrieveNotification", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+
+ /**
+ * Removes a notification from eUICC.
+ *
+ * @param cardId The Id of the eUICC.
+ * @param seqNumber the sequence number of the notification.
+ * @param callback the callback to get the result code.
+ */
+ public void removeNotificationFromList(String cardId, int seqNumber,
+ ResultCallback<Void> callback) {
+ try {
+ getIEuiccCardController().removeNotificationFromList(
+ mContext.getOpPackageName(),
+ cardId,
+ seqNumber,
+ new IRemoveNotificationFromListCallback.Stub() {
+ @Override
+ public void onComplete(int resultCode) {
+ callback.onComplete(resultCode, null);
+ }
+ });
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error calling removeNotificationFromList", e);
+ throw e.rethrowFromSystemServer();
+ }
+ }
+}
diff --git a/android/telephony/euicc/EuiccManager.java b/android/telephony/euicc/EuiccManager.java
index 176057dd..7f913ceb 100644
--- a/android/telephony/euicc/EuiccManager.java
+++ b/android/telephony/euicc/EuiccManager.java
@@ -19,6 +19,7 @@ import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.SdkConstant;
import android.annotation.SystemApi;
+import android.annotation.TestApi;
import android.app.Activity;
import android.app.PendingIntent;
import android.content.Context;
@@ -60,6 +61,30 @@ public class EuiccManager {
public static final String ACTION_MANAGE_EMBEDDED_SUBSCRIPTIONS =
"android.telephony.euicc.action.MANAGE_EMBEDDED_SUBSCRIPTIONS";
+
+ /**
+ * Broadcast Action: The eUICC OTA status is changed.
+ * <p class="note">
+ * Requires the {@link android.Manifest.permission#WRITE_EMBEDDED_SUBSCRIPTIONS} permission.
+ *
+ * <p class="note">This is a protected intent that can only be sent
+ * by the system.
+ * TODO(b/35851809): Make this a SystemApi.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_OTA_STATUS_CHANGED =
+ "android.telephony.euicc.action.OTA_STATUS_CHANGED";
+
+ /**
+ * Broadcast Action: The action sent to carrier app so it knows the carrier setup is not
+ * completed.
+ *
+ * TODO(b/35851809): Make this a public API.
+ */
+ @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_NOTIFY_CARRIER_SETUP =
+ "android.telephony.euicc.action.NOTIFY_CARRIER_SETUP";
+
/**
* Intent action to provision an embedded subscription.
*
@@ -251,8 +276,8 @@ public class EuiccManager {
*
* @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.
+ * TODO(b/35851809): Make this a SystemApi.
*/
- @SystemApi
public int getOtaStatus() {
if (!isEnabled()) {
return EUICC_OTA_STATUS_UNAVAILABLE;
@@ -574,7 +599,11 @@ public class EuiccManager {
}
}
- private static IEuiccController getIEuiccController() {
+ /**
+ * @hide
+ */
+ @TestApi
+ protected IEuiccController getIEuiccController() {
return IEuiccController.Stub.asInterface(ServiceManager.getService("econtroller"));
}
}
diff --git a/android/telephony/euicc/EuiccNotification.java b/android/telephony/euicc/EuiccNotification.java
new file mode 100644
index 00000000..ef3c1ce8
--- /dev/null
+++ b/android/telephony/euicc/EuiccNotification.java
@@ -0,0 +1,179 @@
+/*
+ * 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.telephony.euicc;
+
+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;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * This represents a signed notification which is defined in SGP.22. It can be either a profile
+ * installation result or a notification generated for profile operations (e.g., enabling,
+ * disabling, or deleting).
+ *
+ * @hide
+ *
+ * TODO(b/35851809): Make this a @SystemApi.
+ */
+public class EuiccNotification implements Parcelable {
+ /** Event */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "EVENT_" }, value = {
+ EVENT_INSTALL,
+ EVENT_ENABLE,
+ EVENT_DISABLE,
+ EVENT_DELETE
+ })
+ public @interface Event {}
+
+ /** A profile is downloaded and installed. */
+ public static final int EVENT_INSTALL = 1;
+
+ /** A profile is enabled. */
+ public static final int EVENT_ENABLE = 1 << 1;
+
+ /** A profile is disabled. */
+ public static final int EVENT_DISABLE = 1 << 2;
+
+ /** A profile is deleted. */
+ public static final int EVENT_DELETE = 1 << 3;
+
+ /** Value of the bits of all above events */
+ @Event
+ public static final int ALL_EVENTS =
+ EVENT_INSTALL | EVENT_ENABLE | EVENT_DISABLE | EVENT_DELETE;
+
+ private final int mSeq;
+ private final String mTargetAddr;
+ @Event private final int mEvent;
+ @Nullable private final byte[] mData;
+
+ /**
+ * Creates an instance.
+ *
+ * @param seq The sequence number of this notification.
+ * @param targetAddr The target server where to send this notification.
+ * @param event The event which causes this notification.
+ * @param data The data which needs to be sent to the target server. This can be null for
+ * building a list of notification metadata without data.
+ */
+ public EuiccNotification(int seq, String targetAddr, @Event int event, @Nullable byte[] data) {
+ mSeq = seq;
+ mTargetAddr = targetAddr;
+ mEvent = event;
+ mData = data;
+ }
+
+ /** @return The sequence number of this notification. */
+ public int getSeq() {
+ return mSeq;
+ }
+
+ /** @return The target server address where this notification should be sent to. */
+ public String getTargetAddr() {
+ return mTargetAddr;
+ }
+
+ /** @return The event of this notification. */
+ @Event
+ public int getEvent() {
+ return mEvent;
+ }
+
+ /** @return The notification data which needs to be sent to the target server. */
+ @Nullable
+ public byte[] getData() {
+ return mData;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ EuiccNotification that = (EuiccNotification) obj;
+ return mSeq == that.mSeq
+ && Objects.equals(mTargetAddr, that.mTargetAddr)
+ && mEvent == that.mEvent
+ && Arrays.equals(mData, that.mData);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + mSeq;
+ result = 31 * result + Objects.hashCode(mTargetAddr);
+ result = 31 * result + mEvent;
+ result = 31 * result + Arrays.hashCode(mData);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "EuiccNotification (seq="
+ + mSeq
+ + ", targetAddr="
+ + mTargetAddr
+ + ", event="
+ + mEvent
+ + ", data="
+ + (mData == null ? "null" : "byte[" + mData.length + "]")
+ + ")";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mSeq);
+ dest.writeString(mTargetAddr);
+ dest.writeInt(mEvent);
+ dest.writeByteArray(mData);
+ }
+
+ private EuiccNotification(Parcel source) {
+ mSeq = source.readInt();
+ mTargetAddr = source.readString();
+ mEvent = source.readInt();
+ mData = source.createByteArray();
+ }
+
+ public static final Creator<EuiccNotification> CREATOR =
+ new Creator<EuiccNotification>() {
+ @Override
+ public EuiccNotification createFromParcel(Parcel source) {
+ return new EuiccNotification(source);
+ }
+
+ @Override
+ public EuiccNotification[] newArray(int size) {
+ return new EuiccNotification[size];
+ }
+ };
+}
diff --git a/android/telephony/euicc/EuiccRulesAuthTable.java b/android/telephony/euicc/EuiccRulesAuthTable.java
new file mode 100644
index 00000000..7efe0436
--- /dev/null
+++ b/android/telephony/euicc/EuiccRulesAuthTable.java
@@ -0,0 +1,260 @@
+/*
+ * 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.telephony.euicc;
+
+import android.annotation.IntDef;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.carrier.CarrierIdentifier;
+import android.service.euicc.EuiccProfileInfo;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * This represents the RAT (Rules Authorisation Table) stored on eUICC.
+ *
+ * @hide
+ *
+ * TODO(b/35851809): Make this a @SystemApi.
+ */
+public final class EuiccRulesAuthTable implements Parcelable {
+ /** Profile policy rule flags */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "POLICY_RULE_FLAG_" }, value = {
+ POLICY_RULE_FLAG_CONSENT_REQUIRED
+ })
+ public @interface PolicyRuleFlag {}
+
+ /** User consent is required to install the profile. */
+ public static final int POLICY_RULE_FLAG_CONSENT_REQUIRED = 1;
+
+ private final int[] mPolicyRules;
+ private final CarrierIdentifier[][] mCarrierIds;
+ private final int[] mPolicyRuleFlags;
+
+ /** This is used to build new {@link EuiccRulesAuthTable} instance. */
+ public static final class Builder {
+ private int[] mPolicyRules;
+ private CarrierIdentifier[][] mCarrierIds;
+ private int[] mPolicyRuleFlags;
+ private int mPosition;
+
+ /**
+ * Creates a new builder.
+ *
+ * @param ruleNum The number of authorisation rules in the table.
+ */
+ public Builder(int ruleNum) {
+ mPolicyRules = new int[ruleNum];
+ mCarrierIds = new CarrierIdentifier[ruleNum][];
+ mPolicyRuleFlags = new int[ruleNum];
+ }
+
+ /**
+ * Builds the RAT instance. This builder should not be used anymore after this method is
+ * called, otherwise {@link NullPointerException} will be thrown.
+ */
+ public EuiccRulesAuthTable build() {
+ if (mPosition != mPolicyRules.length) {
+ throw new IllegalStateException(
+ "Not enough rules are added, expected: "
+ + mPolicyRules.length
+ + ", added: "
+ + mPosition);
+ }
+ return new EuiccRulesAuthTable(mPolicyRules, mCarrierIds, mPolicyRuleFlags);
+ }
+
+ /**
+ * Adds an authorisation rule.
+ *
+ * @throws ArrayIndexOutOfBoundsException If the {@code mPosition} is larger than the size
+ * this table.
+ */
+ public Builder add(int policyRules, CarrierIdentifier[] carrierId, int policyRuleFlags) {
+ if (mPosition >= mPolicyRules.length) {
+ throw new ArrayIndexOutOfBoundsException(mPosition);
+ }
+ mPolicyRules[mPosition] = policyRules;
+ mCarrierIds[mPosition] = carrierId;
+ mPolicyRuleFlags[mPosition] = policyRuleFlags;
+ mPosition++;
+ return this;
+ }
+ }
+
+ /**
+ * @param mccRule A 2-character or 3-character string which can be either MCC or MNC. The
+ * character 'E' is used as a wild char to match any digit.
+ * @param mcc A 2-character or 3-character string which can be either MCC or MNC.
+ * @return Whether the {@code mccRule} matches {@code mcc}.
+ *
+ * @hide
+ */
+ @VisibleForTesting
+ public static boolean match(String mccRule, String mcc) {
+ if (mccRule.length() < mcc.length()) {
+ return false;
+ }
+ for (int i = 0; i < mccRule.length(); i++) {
+ // 'E' is the wild char to match any digit.
+ if (mccRule.charAt(i) == 'E'
+ || (i < mcc.length() && mccRule.charAt(i) == mcc.charAt(i))) {
+ continue;
+ }
+ return false;
+ }
+ return true;
+ }
+
+ private EuiccRulesAuthTable(int[] policyRules, CarrierIdentifier[][] carrierIds,
+ int[] policyRuleFlags) {
+ mPolicyRules = policyRules;
+ mCarrierIds = carrierIds;
+ mPolicyRuleFlags = policyRuleFlags;
+ }
+
+ /**
+ * Finds the index of the first authorisation rule matching the given policy and carrier id. If
+ * the returned index is not negative, the carrier is allowed to apply this policy to its
+ * profile.
+ *
+ * @param policy The policy rule.
+ * @param carrierId The carrier id.
+ * @return The index of authorization rule. If no rule is found, -1 will be returned.
+ */
+ public int findIndex(@EuiccProfileInfo.PolicyRule int policy, CarrierIdentifier carrierId) {
+ for (int i = 0; i < mPolicyRules.length; i++) {
+ if ((mPolicyRules[i] & policy) == 0) {
+ continue;
+ }
+ CarrierIdentifier[] carrierIds = mCarrierIds[i];
+ if (carrierIds == null || carrierIds.length == 0) {
+ continue;
+ }
+ for (int j = 0; j < carrierIds.length; j++) {
+ CarrierIdentifier ruleCarrierId = carrierIds[j];
+ if (!match(ruleCarrierId.getMcc(), carrierId.getMcc())
+ || !match(ruleCarrierId.getMnc(), carrierId.getMnc())) {
+ continue;
+ }
+ String gid = ruleCarrierId.getGid1();
+ if (!TextUtils.isEmpty(gid) && !gid.equals(carrierId.getGid1())) {
+ continue;
+ }
+ gid = ruleCarrierId.getGid2();
+ if (!TextUtils.isEmpty(gid) && !gid.equals(carrierId.getGid2())) {
+ continue;
+ }
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Tests if the entry in the table has the given policy rule flag.
+ *
+ * @param index The index of the entry.
+ * @param flag The policy rule flag to be tested.
+ * @throws ArrayIndexOutOfBoundsException If the {@code index} is negative or larger than the
+ * size of this table.
+ */
+ public boolean hasPolicyRuleFlag(int index, @PolicyRuleFlag int flag) {
+ if (index < 0 || index >= mPolicyRules.length) {
+ throw new ArrayIndexOutOfBoundsException(index);
+ }
+ return (mPolicyRuleFlags[index] & flag) != 0;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeIntArray(mPolicyRules);
+ for (CarrierIdentifier[] ids : mCarrierIds) {
+ dest.writeTypedArray(ids, flags);
+ }
+ dest.writeIntArray(mPolicyRuleFlags);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ EuiccRulesAuthTable that = (EuiccRulesAuthTable) obj;
+ if (mCarrierIds.length != that.mCarrierIds.length) {
+ return false;
+ }
+ for (int i = 0; i < mCarrierIds.length; i++) {
+ CarrierIdentifier[] carrierIds = mCarrierIds[i];
+ CarrierIdentifier[] thatCarrierIds = that.mCarrierIds[i];
+ if (carrierIds != null && thatCarrierIds != null) {
+ if (carrierIds.length != thatCarrierIds.length) {
+ return false;
+ }
+ for (int j = 0; j < carrierIds.length; j++) {
+ if (!carrierIds[j].equals(thatCarrierIds[j])) {
+ return false;
+ }
+ }
+ continue;
+ } else if (carrierIds == null && thatCarrierIds == null) {
+ continue;
+ }
+ return false;
+ }
+
+ return Arrays.equals(mPolicyRules, that.mPolicyRules)
+ && Arrays.equals(mPolicyRuleFlags, that.mPolicyRuleFlags);
+ }
+
+ private EuiccRulesAuthTable(Parcel source) {
+ mPolicyRules = source.createIntArray();
+ int len = mPolicyRules.length;
+ mCarrierIds = new CarrierIdentifier[len][];
+ for (int i = 0; i < len; i++) {
+ mCarrierIds[i] = source.createTypedArray(CarrierIdentifier.CREATOR);
+ }
+ mPolicyRuleFlags = source.createIntArray();
+ }
+
+ public static final Creator<EuiccRulesAuthTable> CREATOR =
+ new Creator<EuiccRulesAuthTable>() {
+ @Override
+ public EuiccRulesAuthTable createFromParcel(Parcel source) {
+ return new EuiccRulesAuthTable(source);
+ }
+
+ @Override
+ public EuiccRulesAuthTable[] newArray(int size) {
+ return new EuiccRulesAuthTable[size];
+ }
+ };
+}
diff --git a/android/telephony/ims/ImsService.java b/android/telephony/ims/ImsService.java
index 8230eafc..aaa0f085 100644
--- a/android/telephony/ims/ImsService.java
+++ b/android/telephony/ims/ImsService.java
@@ -26,12 +26,14 @@ import android.telephony.CarrierConfigManager;
import android.telephony.ims.feature.ImsFeature;
import android.telephony.ims.feature.MMTelFeature;
import android.telephony.ims.feature.RcsFeature;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.util.Log;
import android.util.SparseArray;
import com.android.ims.internal.IImsFeatureStatusCallback;
import com.android.ims.internal.IImsMMTelFeature;
import com.android.ims.internal.IImsRcsFeature;
+import com.android.ims.internal.IImsRegistration;
import com.android.ims.internal.IImsServiceController;
import com.android.internal.annotations.VisibleForTesting;
@@ -113,6 +115,12 @@ public class ImsService extends Service {
throws RemoteException {
ImsService.this.removeImsFeature(slotId, featureType, c);
}
+
+ @Override
+ public IImsRegistration getRegistration(int slotId) throws RemoteException {
+ ImsRegistrationImplBase r = ImsService.this.getRegistration(slotId);
+ return r != null ? r.getBinder() : null;
+ }
};
/**
@@ -174,6 +182,8 @@ public class ImsService extends Service {
f.setSlotId(slotId);
f.addImsFeatureStatusCallback(c);
addImsFeature(slotId, featureType, f);
+ // TODO: Remove once new onFeatureReady AIDL is merged in.
+ f.onFeatureReady();
}
private void addImsFeature(int slotId, int featureType, ImsFeature f) {
@@ -236,4 +246,13 @@ public class ImsService extends Service {
public @Nullable RcsFeature onCreateRcsFeature(int slotId) {
return null;
}
+
+ /**
+ * @param slotId The slot that is associated with the IMS Registration.
+ * @return the ImsRegistration implementation associated with the slot.
+ * @hide
+ */
+ public ImsRegistrationImplBase getRegistration(int slotId) {
+ return new ImsRegistrationImplBase();
+ }
}
diff --git a/android/telephony/ims/feature/ImsFeature.java b/android/telephony/ims/feature/ImsFeature.java
index ca4a210e..d47cea30 100644
--- a/android/telephony/ims/feature/ImsFeature.java
+++ b/android/telephony/ims/feature/ImsFeature.java
@@ -96,7 +96,7 @@ public abstract class ImsFeature {
new WeakHashMap<IImsFeatureStatusCallback, Boolean>());
private @ImsState int mState = STATE_NOT_AVAILABLE;
private int mSlotId = SubscriptionManager.INVALID_SIM_SLOT_INDEX;
- private Context mContext;
+ protected Context mContext;
public void setContext(Context context) {
mContext = context;
diff --git a/android/telephony/ims/feature/MMTelFeature.java b/android/telephony/ims/feature/MMTelFeature.java
index 4e095e3a..51971072 100644
--- a/android/telephony/ims/feature/MMTelFeature.java
+++ b/android/telephony/ims/feature/MMTelFeature.java
@@ -19,6 +19,7 @@ package android.telephony.ims.feature;
import android.app.PendingIntent;
import android.os.Message;
import android.os.RemoteException;
+import android.telephony.ims.internal.stub.SmsImplBase;
import com.android.ims.ImsCallProfile;
import com.android.ims.internal.IImsCallSession;
@@ -28,6 +29,7 @@ import com.android.ims.internal.IImsEcbm;
import com.android.ims.internal.IImsMMTelFeature;
import com.android.ims.internal.IImsMultiEndpoint;
import com.android.ims.internal.IImsRegistrationListener;
+import com.android.ims.internal.IImsSmsListener;
import com.android.ims.internal.IImsUt;
import com.android.ims.internal.ImsCallSession;
@@ -171,6 +173,42 @@ public class MMTelFeature extends ImsFeature {
return MMTelFeature.this.getMultiEndpointInterface();
}
}
+
+ @Override
+ public void setSmsListener(IImsSmsListener l) throws RemoteException {
+ synchronized (mLock) {
+ MMTelFeature.this.setSmsListener(l);
+ }
+ }
+
+ @Override
+ public void sendSms(int token, int messageRef, String format, String smsc, boolean retry,
+ byte[] pdu) {
+ synchronized (mLock) {
+ MMTelFeature.this.sendSms(token, messageRef, format, smsc, retry, pdu);
+ }
+ }
+
+ @Override
+ public void acknowledgeSms(int token, int messageRef, int result) {
+ synchronized (mLock) {
+ MMTelFeature.this.acknowledgeSms(token, messageRef, result);
+ }
+ }
+
+ @Override
+ public void acknowledgeSmsReport(int token, int messageRef, int result) {
+ synchronized (mLock) {
+ MMTelFeature.this.acknowledgeSmsReport(token, messageRef, result);
+ }
+ }
+
+ @Override
+ public String getSmsFormat() {
+ synchronized (mLock) {
+ return MMTelFeature.this.getSmsFormat();
+ }
+ }
};
/**
@@ -346,6 +384,39 @@ public class MMTelFeature extends ImsFeature {
return null;
}
+ public void setSmsListener(IImsSmsListener listener) {
+ getSmsImplementation().registerSmsListener(listener);
+ }
+
+ public void sendSms(int token, int messageRef, String format, String smsc, boolean isRetry,
+ byte[] pdu) {
+ getSmsImplementation().sendSms(token, messageRef, format, smsc, isRetry, pdu);
+ }
+
+ public void acknowledgeSms(int token, int messageRef,
+ @SmsImplBase.DeliverStatusResult int result) {
+ getSmsImplementation().acknowledgeSms(token, messageRef, result);
+ }
+
+ public void acknowledgeSmsReport(int token, int messageRef,
+ @SmsImplBase.StatusReportResult int result) {
+ getSmsImplementation().acknowledgeSmsReport(token, messageRef, result);
+ }
+
+ /**
+ * 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();
+ }
+
+ public String getSmsFormat() {
+ return getSmsImplementation().getSmsFormat();
+ }
+
@Override
public void onFeatureReady() {
diff --git a/android/telephony/ims/internal/ImsService.java b/android/telephony/ims/internal/ImsService.java
index b7c8ca0f..afaf3329 100644
--- a/android/telephony/ims/internal/ImsService.java
+++ b/android/telephony/ims/internal/ImsService.java
@@ -24,7 +24,6 @@ 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;
@@ -32,11 +31,12 @@ 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.telephony.ims.stub.ImsRegistrationImplBase;
import android.util.Log;
import android.util.SparseArray;
import com.android.ims.internal.IImsFeatureStatusCallback;
+import com.android.ims.internal.IImsRegistration;
import com.android.internal.annotations.VisibleForTesting;
/**
diff --git a/android/telephony/ims/internal/SmsImplBase.java b/android/telephony/ims/internal/SmsImplBase.java
deleted file mode 100644
index 47414cf7..00000000
--- a/android/telephony/ims/internal/SmsImplBase.java
+++ /dev/null
@@ -1,260 +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.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
index 4d188734..5dbf077e 100644
--- a/android/telephony/ims/internal/feature/CapabilityChangeRequest.java
+++ b/android/telephony/ims/internal/feature/CapabilityChangeRequest.java
@@ -18,7 +18,7 @@ package android.telephony.ims.internal.feature;
import android.os.Parcel;
import android.os.Parcelable;
-import android.telephony.ims.internal.stub.ImsRegistrationImplBase;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.util.ArraySet;
import java.util.ArrayList;
diff --git a/android/telephony/ims/internal/feature/MmTelFeature.java b/android/telephony/ims/internal/feature/MmTelFeature.java
index 2f350c86..9b576c72 100644
--- a/android/telephony/ims/internal/feature/MmTelFeature.java
+++ b/android/telephony/ims/internal/feature/MmTelFeature.java
@@ -21,15 +21,11 @@ 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.ImsRegistrationImplBase;
import android.telephony.ims.stub.ImsEcbmImplBase;
import android.telephony.ims.stub.ImsMultiEndpointImplBase;
import android.telephony.ims.stub.ImsUtImplBase;
@@ -68,11 +64,6 @@ public class MmTelFeature extends ImsFeature {
}
@Override
- public void setSmsListener(IImsSmsListener l) throws RemoteException {
- MmTelFeature.this.setSmsListener(l);
- }
-
- @Override
public int getFeatureState() throws RemoteException {
synchronized (mLock) {
return MmTelFeature.this.getFeatureState();
@@ -152,34 +143,6 @@ public class MmTelFeature extends ImsFeature {
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();
- }
- }
};
/**
@@ -261,6 +224,15 @@ public class MmTelFeature extends ImsFeature {
}
/**
+ * Updates the Listener when the voice message count for IMS has changed.
+ * @param count an integer representing the new message count.
+ */
+ @Override
+ public void onVoiceMessageCountUpdate(int count) {
+
+ }
+
+ /**
* Called when the IMS provider receives an incoming call.
* @param c The {@link ImsCallSession} associated with the new call.
*/
@@ -282,10 +254,6 @@ public class MmTelFeature extends ImsFeature {
}
}
- private void setSmsListener(IImsSmsListener listener) {
- getSmsImplementation().registerSmsListener(listener);
- }
-
private void queryCapabilityConfigurationInternal(int capability, int radioTech,
IImsCapabilityCallback c) {
boolean enabled = queryCapabilityConfiguration(capability, radioTech);
@@ -447,32 +415,6 @@ public class MmTelFeature extends ImsFeature {
// 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() {
diff --git a/android/telephony/ims/internal/stub/SmsImplBase.java b/android/telephony/ims/internal/stub/SmsImplBase.java
new file mode 100644
index 00000000..113dad46
--- /dev/null
+++ b/android/telephony/ims/internal/stub/SmsImplBase.java
@@ -0,0 +1,271 @@
+/*
+ * 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.telephony.ims.internal.stub;
+
+import android.annotation.IntDef;
+import android.os.RemoteException;
+import android.telephony.SmsManager;
+import android.telephony.SmsMessage;
+import android.telephony.ims.internal.feature.MmTelFeature;
+import android.util.Log;
+
+import com.android.ims.internal.IImsSmsListener;
+
+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 token unique token generated by the platform that should be used when triggering
+ * callbacks for this specific 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 smsc the Short Message Service Center address.
+ * @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 token, int messageRef, String format, String smsc, boolean isRetry,
+ byte[] pdu) {
+ // Base implementation returns error. Should be overridden.
+ try {
+ onSendSmsResult(token, 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(int, String, byte[])}
+ * has been called to deliver the result to the IMS provider.
+ *
+ * @param token token provided in {@link #onSmsReceived(int, String, byte[])}
+ * @param result result of delivering the message. Valid values are defined in
+ * {@link DeliverStatusResult}
+ * @param messageRef the message reference
+ */
+ public void acknowledgeSms(int token, int messageRef, @DeliverStatusResult int result) {
+ Log.e(LOG_TAG, "acknowledgeSms() not implemented.");
+ }
+
+ /**
+ * This method will be triggered by the platform after
+ * {@link #onSmsStatusReportReceived(int, int, String, byte[])} has been called to provide the
+ * result to the IMS provider.
+ *
+ * @param token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])}
+ * @param result result of delivering the message. Valid values are defined in
+ * {@link StatusReportResult}
+ * @param messageRef the message reference
+ */
+ public void acknowledgeSmsReport(int token, int messageRef, @StatusReportResult int result) {
+ Log.e(LOG_TAG, "acknowledgeSmsReport() not implemented.");
+ }
+
+ /**
+ * 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, int)}.
+ *
+ * This method must not be called before {@link MmTelFeature#onFeatureReady()} is called.
+ *
+ * @param token unique token generated by IMS providers that the platform will use to trigger
+ * callbacks for this message.
+ * @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(int token, String format, byte[] pdu)
+ throws IllegalStateException {
+ synchronized (mLock) {
+ if (mListener == null) {
+ throw new IllegalStateException("Feature not ready.");
+ }
+ try {
+ mListener.onSmsReceived(token, format, pdu);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Can not deliver sms: " + e.getMessage());
+ acknowledgeSms(token, 0, 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 token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])}
+ * @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 token, int messageRef, @SendStatusResult int status,
+ int reason) throws IllegalStateException, RemoteException {
+ synchronized (mLock) {
+ if (mListener == null) {
+ throw new IllegalStateException("Feature not ready.");
+ }
+ mListener.onSendSmsResult(token, messageRef, status, reason);
+ }
+ }
+
+ /**
+ * Sets the status report of the sent message.
+ *
+ * @param token token provided in {@link #sendSms(int, int, String, String, boolean, byte[])}
+ * @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 token, int messageRef, String format,
+ byte[] pdu) {
+ synchronized (mLock) {
+ if (mListener == null) {
+ throw new IllegalStateException("Feature not ready.");
+ }
+ try {
+ mListener.onSmsStatusReportReceived(token, messageRef, format, pdu);
+ } catch (RemoteException e) {
+ Log.e(LOG_TAG, "Can not process sms status report: " + e.getMessage());
+ acknowledgeSmsReport(token, 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/stub/ImsRegistrationImplBase.java b/android/telephony/ims/stub/ImsRegistrationImplBase.java
index 558b009a..42af0836 100644
--- a/android/telephony/ims/internal/stub/ImsRegistrationImplBase.java
+++ b/android/telephony/ims/stub/ImsRegistrationImplBase.java
@@ -14,16 +14,19 @@
* limitations under the License
*/
-package android.telephony.ims.internal.stub;
+package android.telephony.ims.stub;
import android.annotation.IntDef;
+import android.net.Uri;
+import android.os.IBinder;
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 com.android.ims.internal.IImsRegistration;
+import com.android.ims.internal.IImsRegistrationCallback;
+import com.android.internal.annotations.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -62,23 +65,25 @@ public class ImsRegistrationImplBase {
// Registration states, used to notify new ImsRegistrationImplBase#Callbacks of the current
// state.
+ // The unknown state is set as the initialization state. This is so that we do not call back
+ // with NOT_REGISTERED in the case where the ImsService has not updated the registration state
+ // yet.
+ private static final int REGISTRATION_STATE_UNKNOWN = -1;
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.
+ * @hide
*/
- public static class Callback extends IImsRegistrationCallback.Stub {
-
+ public static class Callback {
/**
* 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) {
}
@@ -88,7 +93,6 @@ public class ImsRegistrationImplBase {
* @param imsRadioTech the radio access technology. Valid values are defined in
* {@link ImsRegistrationTech}.
*/
- @Override
public void onRegistering(@ImsRegistrationTech int imsRadioTech) {
}
@@ -97,7 +101,6 @@ public class ImsRegistrationImplBase {
*
* @param info the {@link ImsReasonInfo} associated with why registration was disconnected.
*/
- @Override
public void onDeregistered(ImsReasonInfo info) {
}
@@ -108,10 +111,19 @@ public class ImsRegistrationImplBase {
* @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) {
}
+
+ /**
+ * Returns a list of subscriber {@link Uri}s associated with this IMS subscription when
+ * it changes.
+ * @param uris new array of subscriber {@link Uri}s that are associated with this IMS
+ * subscription.
+ */
+ public void onSubscriberAssociatedUriChanged(Uri[] uris) {
+
+ }
}
private final IImsRegistration mBinder = new IImsRegistration.Stub() {
@@ -139,9 +151,9 @@ public class ImsRegistrationImplBase {
private @ImsRegistrationTech
int mConnectionType = REGISTRATION_TECH_NONE;
// Locked on mLock
- private int mRegistrationState = REGISTRATION_STATE_NOT_REGISTERED;
- // Locked on mLock
- private ImsReasonInfo mLastDisconnectCause;
+ private int mRegistrationState = REGISTRATION_STATE_UNKNOWN;
+ // Locked on mLock, create unspecified disconnect cause.
+ private ImsReasonInfo mLastDisconnectCause = new ImsReasonInfo();
public final IImsRegistration getBinder() {
return mBinder;
@@ -221,6 +233,17 @@ public class ImsRegistrationImplBase {
});
}
+ public final void onSubscriberAssociatedUriChanged(Uri[] uris) {
+ mCallbacks.broadcast((c) -> {
+ try {
+ c.onSubscriberAssociatedUriChanged(uris);
+ } catch (RemoteException e) {
+ Log.w(LOG_TAG, e + " " + "onSubscriberAssociatedUriChanged() - Skipping " +
+ "callback.");
+ }
+ });
+ }
+
private void updateToState(@ImsRegistrationTech int connType, int newState) {
synchronized (mLock) {
mConnectionType = connType;
@@ -241,7 +264,8 @@ public class ImsRegistrationImplBase {
}
}
- private @ImsRegistrationTech int getConnectionType() {
+ @VisibleForTesting
+ public final @ImsRegistrationTech int getConnectionType() {
synchronized (mLock) {
return mConnectionType;
}
@@ -271,6 +295,10 @@ public class ImsRegistrationImplBase {
c.onRegistered(getConnectionType());
break;
}
+ case REGISTRATION_STATE_UNKNOWN: {
+ // Do not callback if the state has not been updated yet by the ImsService.
+ break;
+ }
}
}
}
diff --git a/android/test/IsolatedContext.java b/android/test/IsolatedContext.java
index 0b77c006..6e4c41ee 100644
--- a/android/test/IsolatedContext.java
+++ b/android/test/IsolatedContext.java
@@ -17,12 +17,6 @@
package android.test;
import android.accounts.AccountManager;
-import android.accounts.AccountManagerCallback;
-import android.accounts.AccountManagerFuture;
-import android.accounts.AuthenticatorException;
-import android.accounts.OnAccountsUpdateListener;
-import android.accounts.OperationCanceledException;
-import android.accounts.Account;
import android.content.ContextWrapper;
import android.content.ContentResolver;
import android.content.Intent;
@@ -32,12 +26,10 @@ import android.content.BroadcastReceiver;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri;
-import android.os.Handler;
+import android.test.mock.MockAccountManager;
import java.io.File;
-import java.io.IOException;
import java.util.ArrayList;
-import java.util.concurrent.TimeUnit;
import java.util.List;
@@ -52,7 +44,7 @@ import java.util.List;
public class IsolatedContext extends ContextWrapper {
private ContentResolver mResolver;
- private final MockAccountManager mMockAccountManager;
+ private final AccountManager mMockAccountManager;
private List<Intent> mBroadcastIntents = new ArrayList<>();
@@ -60,7 +52,7 @@ public class IsolatedContext extends ContextWrapper {
ContentResolver resolver, Context targetContext) {
super(targetContext);
mResolver = resolver;
- mMockAccountManager = new MockAccountManager();
+ mMockAccountManager = MockAccountManager.newMockAccountManager(IsolatedContext.this);
}
/** Returns the list of intents that were broadcast since the last call to this method. */
@@ -123,71 +115,6 @@ public class IsolatedContext extends ContextWrapper {
return null;
}
- private class MockAccountManager extends AccountManager {
- public MockAccountManager() {
- super(IsolatedContext.this, null /* IAccountManager */, null /* handler */);
- }
-
- public void addOnAccountsUpdatedListener(OnAccountsUpdateListener listener,
- Handler handler, boolean updateImmediately) {
- // do nothing
- }
-
- public Account[] getAccounts() {
- return new Account[]{};
- }
-
- public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures(
- final String type, final String[] features,
- AccountManagerCallback<Account[]> callback, Handler handler) {
- return new MockAccountManagerFuture<Account[]>(new Account[0]);
- }
-
- public String blockingGetAuthToken(Account account, String authTokenType,
- boolean notifyAuthFailure)
- throws OperationCanceledException, IOException, AuthenticatorException {
- return null;
- }
-
-
- /**
- * A very simple AccountManagerFuture class
- * that returns what ever was passed in
- */
- private class MockAccountManagerFuture<T>
- implements AccountManagerFuture<T> {
-
- T mResult;
-
- public MockAccountManagerFuture(T result) {
- mResult = result;
- }
-
- public boolean cancel(boolean mayInterruptIfRunning) {
- return false;
- }
-
- public boolean isCancelled() {
- return false;
- }
-
- public boolean isDone() {
- return true;
- }
-
- public T getResult()
- throws OperationCanceledException, IOException, AuthenticatorException {
- return mResult;
- }
-
- public T getResult(long timeout, TimeUnit unit)
- throws OperationCanceledException, IOException, AuthenticatorException {
- return getResult();
- }
- }
-
- }
-
@Override
public File getFilesDir() {
return new File("/dev/null");
diff --git a/android/test/ProviderTestCase2.java b/android/test/ProviderTestCase2.java
index 1fa633e2..be18b530 100644
--- a/android/test/ProviderTestCase2.java
+++ b/android/test/ProviderTestCase2.java
@@ -21,6 +21,7 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.content.res.Resources;
+import android.test.mock.MockContentProvider;
import android.test.mock.MockContext;
import android.test.mock.MockContentResolver;
import android.database.DatabaseUtils;
@@ -152,7 +153,7 @@ public abstract class ProviderTestCase2<T extends ContentProvider> extends Andro
T instance = providerClass.newInstance();
ProviderInfo providerInfo = new ProviderInfo();
providerInfo.authority = authority;
- instance.attachInfoForTesting(context, providerInfo);
+ MockContentProvider.attachInfoForTesting(instance, context, providerInfo);
return instance;
}
diff --git a/android/test/RenamingDelegatingContext.java b/android/test/RenamingDelegatingContext.java
index fd333215..10ccebc4 100644
--- a/android/test/RenamingDelegatingContext.java
+++ b/android/test/RenamingDelegatingContext.java
@@ -21,6 +21,7 @@ import android.content.ContextWrapper;
import android.content.ContentProvider;
import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
+import android.test.mock.MockContentProvider;
import android.util.Log;
import java.io.File;
@@ -71,7 +72,7 @@ public class RenamingDelegatingContext extends ContextWrapper {
if (allowAccessToExistingFilesAndDbs) {
mContext.makeExistingFilesAndDbsAccessible();
}
- mProvider.attachInfoForTesting(mContext, null);
+ MockContentProvider.attachInfoForTesting(mProvider, mContext, null);
return mProvider;
}
diff --git a/android/test/ServiceTestCase.java b/android/test/ServiceTestCase.java
index c8ff0f90..cd54955f 100644
--- a/android/test/ServiceTestCase.java
+++ b/android/test/ServiceTestCase.java
@@ -23,6 +23,7 @@ import android.content.Intent;
import android.os.IBinder;
import android.test.mock.MockApplication;
+import android.test.mock.MockService;
import java.util.Random;
/**
@@ -163,14 +164,8 @@ public abstract class ServiceTestCase<T extends Service> extends AndroidTestCase
if (getApplication() == null) {
setApplication(new MockApplication());
}
- mService.attach(
- getContext(),
- null, // ActivityThread not actually used in Service
- mServiceClass.getName(),
- null, // token not needed when not talking with the activity manager
- getApplication(),
- null // mocked services don't talk with the activity manager
- );
+ MockService.attachForTesting(
+ mService, getContext(), mServiceClass.getName(), getApplication());
assertNotNull(mService);
diff --git a/android/test/mock/MockAccountManager.java b/android/test/mock/MockAccountManager.java
new file mode 100644
index 00000000..c9b4c7ba
--- /dev/null
+++ b/android/test/mock/MockAccountManager.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.test.mock;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.accounts.AccountManagerCallback;
+import android.accounts.AccountManagerFuture;
+import android.accounts.AuthenticatorException;
+import android.accounts.OnAccountsUpdateListener;
+import android.accounts.OperationCanceledException;
+import android.content.Context;
+import android.os.Handler;
+import java.io.IOException;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A mock {@link android.accounts.AccountManager} class.
+ *
+ * <p>Provided for use by {@code android.test.IsolatedContext}.
+ *
+ * @deprecated Use a mocking framework like <a href="https://github.com/mockito/mockito">Mockito</a>.
+ * New tests should be written using the
+ * <a href="{@docRoot}
+ * tools/testing-support-library/index.html">Android Testing Support Library</a>.
+ */
+@Deprecated
+public class MockAccountManager {
+
+ /**
+ * Create a new mock {@link AccountManager} instance.
+ *
+ * @param context the {@link Context} to which the returned object belongs.
+ * @return the new instance.
+ */
+ public static AccountManager newMockAccountManager(Context context) {
+ return new MockAccountManagerImpl(context);
+ }
+
+ private MockAccountManager() {
+ }
+
+ private static class MockAccountManagerImpl extends AccountManager {
+
+ MockAccountManagerImpl(Context context) {
+ super(context, null /* IAccountManager */, null /* handler */);
+ }
+
+ public void addOnAccountsUpdatedListener(OnAccountsUpdateListener listener,
+ Handler handler, boolean updateImmediately) {
+ // do nothing
+ }
+
+ public Account[] getAccounts() {
+ return new Account[] {};
+ }
+
+ public AccountManagerFuture<Account[]> getAccountsByTypeAndFeatures(
+ final String type, final String[] features,
+ AccountManagerCallback<Account[]> callback, Handler handler) {
+ return new MockAccountManagerFuture<Account[]>(new Account[0]);
+ }
+
+ public String blockingGetAuthToken(Account account, String authTokenType,
+ boolean notifyAuthFailure)
+ throws OperationCanceledException, IOException, AuthenticatorException {
+ return null;
+ }
+ }
+
+ /**
+ * A very simple AccountManagerFuture class
+ * that returns what ever was passed in
+ */
+ private static class MockAccountManagerFuture<T>
+ implements AccountManagerFuture<T> {
+
+ T mResult;
+
+ MockAccountManagerFuture(T result) {
+ mResult = result;
+ }
+
+ public boolean cancel(boolean mayInterruptIfRunning) {
+ return false;
+ }
+
+ public boolean isCancelled() {
+ return false;
+ }
+
+ public boolean isDone() {
+ return true;
+ }
+
+ public T getResult()
+ throws OperationCanceledException, IOException, AuthenticatorException {
+ return mResult;
+ }
+
+ public T getResult(long timeout, TimeUnit unit)
+ throws OperationCanceledException, IOException, AuthenticatorException {
+ return getResult();
+ }
+ }
+}
diff --git a/android/test/mock/MockContentProvider.java b/android/test/mock/MockContentProvider.java
index d5f3ce88..b917fbd8 100644
--- a/android/test/mock/MockContentProvider.java
+++ b/android/test/mock/MockContentProvider.java
@@ -277,4 +277,21 @@ public class MockContentProvider extends ContentProvider {
public final IContentProvider getIContentProvider() {
return mIContentProvider;
}
+
+ /**
+ * Like {@link #attachInfo(Context, android.content.pm.ProviderInfo)}, but for use
+ * when directly instantiating the provider for testing.
+ *
+ * <p>Provided for use by {@code android.test.ProviderTestCase2} and
+ * {@code android.test.RenamingDelegatingContext}.
+ *
+ * @deprecated Use a mocking framework like <a href="https://github.com/mockito/mockito">Mockito</a>.
+ * New tests should be written using the
+ * <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
+ */
+ @Deprecated
+ public static void attachInfoForTesting(
+ ContentProvider provider, Context context, ProviderInfo providerInfo) {
+ provider.attachInfoForTesting(context, providerInfo);
+ }
}
diff --git a/android/test/mock/MockPackageManager.java b/android/test/mock/MockPackageManager.java
index ce8019f8..1ddc52c9 100644
--- a/android/test/mock/MockPackageManager.java
+++ b/android/test/mock/MockPackageManager.java
@@ -108,6 +108,12 @@ public class MockPackageManager extends PackageManager {
throw new UnsupportedOperationException();
}
+ /** @hide */
+ @Override
+ public Intent getCarLaunchIntentForPackage(String packageName) {
+ throw new UnsupportedOperationException();
+ }
+
@Override
public int[] getPackageGids(String packageName) throws NameNotFoundException {
throw new UnsupportedOperationException();
@@ -1090,15 +1096,6 @@ public class MockPackageManager extends PackageManager {
* @hide
*/
@Override
- public void installPackage(Uri packageURI, PackageInstallObserver observer,
- int flags, String installerPackageName) {
- throw new UnsupportedOperationException();
- }
-
- /**
- * @hide
- */
- @Override
public void addCrossProfileIntentFilter(IntentFilter filter, int sourceUserId, int targetUserId,
int flags) {
throw new UnsupportedOperationException();
@@ -1183,4 +1180,33 @@ public class MockPackageManager extends PackageManager {
public ArtManager getArtManager() {
throw new UnsupportedOperationException();
}
+
+ /**
+ * @hide
+ */
+ @Override
+ public void setHarmfulAppWarning(String packageName, CharSequence warning) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * @hide
+ */
+ @Override
+ public CharSequence getHarmfulAppWarning(String packageName) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasSigningCertificate(
+ String packageName, byte[] certificate, @PackageManager.CertificateInputType int type) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean hasSigningCertificate(
+ int uid, byte[] certificate, @PackageManager.CertificateInputType int type) {
+ throw new UnsupportedOperationException();
+ }
+
}
diff --git a/android/test/mock/MockService.java b/android/test/mock/MockService.java
new file mode 100644
index 00000000..dbba4f32
--- /dev/null
+++ b/android/test/mock/MockService.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 android.test.mock;
+
+import android.app.Application;
+import android.app.Service;
+import android.content.Context;
+
+/**
+ * A mock {@link android.app.Service} class.
+ *
+ * <p>Provided for use by {@code android.test.ServiceTestCase}.
+ *
+ * @deprecated Use a mocking framework like <a href="https://github.com/mockito/mockito">Mockito</a>.
+ * New tests should be written using the
+ * <a href="{@docRoot}tools/testing-support-library/index.html">Android Testing Support Library</a>.
+ */
+@Deprecated
+public class MockService {
+
+ public static <T extends Service> void attachForTesting(Service service, Context context,
+ String serviceClassName,
+ Application application) {
+ service.attach(
+ context,
+ null, // ActivityThread not actually used in Service
+ serviceClassName,
+ null, // token not needed when not talking with the activity manager
+ application,
+ null // mocked services don't talk with the activity manager
+ );
+ }
+
+ private MockService() {
+ }
+}
diff --git a/android/text/BoringLayout.java b/android/text/BoringLayout.java
index ce38ebb9..6fa5312b 100644
--- a/android/text/BoringLayout.java
+++ b/android/text/BoringLayout.java
@@ -347,7 +347,14 @@ public class BoringLayout extends Layout implements TextUtils.EllipsizeCallback
TextLine line = TextLine.obtain();
line.set(paint, text, 0, textLength, Layout.DIR_LEFT_TO_RIGHT,
Layout.DIRS_ALL_LEFT_TO_RIGHT, false, null);
- fm.width = (int) Math.ceil(line.metrics(fm));
+ if (text instanceof MeasuredText) {
+ MeasuredText mt = (MeasuredText) text;
+ // Reaching here means there is only one paragraph.
+ MeasuredParagraph mp = mt.getMeasuredParagraph(0);
+ fm.width = (int) Math.ceil(mp.getWidth(0, mp.getTextLength()));
+ } else {
+ fm.width = (int) Math.ceil(line.metrics(fm));
+ }
TextLine.recycle(line);
return fm;
diff --git a/android/text/DynamicLayout.java b/android/text/DynamicLayout.java
index 6bca37af..18431cac 100644
--- a/android/text/DynamicLayout.java
+++ b/android/text/DynamicLayout.java
@@ -1096,6 +1096,11 @@ public class DynamicLayout extends Layout {
public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) {
if (o instanceof UpdateLayout) {
+ if (start > end) {
+ // Bug: 67926915 start cannot be determined, fallback to reflow from start
+ // instead of causing an exception
+ start = 0;
+ }
reflow(s, start, end - start, end - start);
reflow(s, nstart, nend - nstart, nend - nstart);
}
diff --git a/android/text/Layout.java b/android/text/Layout.java
index bf4b6ac5..aa97b2ab 100644
--- a/android/text/Layout.java
+++ b/android/text/Layout.java
@@ -1917,10 +1917,10 @@ public abstract class Layout {
private static float measurePara(TextPaint paint, CharSequence text, int start, int end,
TextDirectionHeuristic textDir) {
- MeasuredText mt = null;
+ MeasuredParagraph mt = null;
TextLine tl = TextLine.obtain();
try {
- mt = MeasuredText.buildForBidi(text, start, end, textDir, mt);
+ mt = MeasuredParagraph.buildForBidi(text, start, end, textDir, mt);
final char[] chars = mt.getChars();
final int len = chars.length;
final Directions directions = mt.getDirections(0, len);
diff --git a/android/text/MeasuredParagraph.java b/android/text/MeasuredParagraph.java
new file mode 100644
index 00000000..45fbf6f5
--- /dev/null
+++ b/android/text/MeasuredParagraph.java
@@ -0,0 +1,721 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.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.Pools.SynchronizedPool;
+
+import dalvik.annotation.optimization.CriticalNative;
+
+import libcore.util.NativeAllocationRegistry;
+
+import java.util.Arrays;
+
+/**
+ * MeasuredParagraph 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.
+ *
+ * MeasuredParagraph is NOT a thread safe object.
+ * @hide
+ */
+public class MeasuredParagraph {
+ private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
+
+ private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
+ MeasuredParagraph.class.getClassLoader(), nGetReleaseFunc(), 1024);
+
+ private MeasuredParagraph() {} // Use build static functions instead.
+
+ private static final SynchronizedPool<MeasuredParagraph> sPool = new SynchronizedPool<>(1);
+
+ private static @NonNull MeasuredParagraph obtain() { // Use build static functions instead.
+ final MeasuredParagraph mt = sPool.acquire();
+ return mt != null ? mt : new MeasuredParagraph();
+ }
+
+ /**
+ * Recycle the MeasuredParagraph.
+ *
+ * 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 MeasuredParagraph.
+ // 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;
+ }
+ }
+
+ // 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 length of the paragraph.
+ *
+ * This is always available.
+ */
+ public int getTextLength() {
+ return mTextLength;
+ }
+
+ /**
+ * 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 MeasuredParagraph is computed with buildForMeasurement.
+ * 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 MeasuredParagraph is computed with buildForMeasurement.
+ * 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 MeasuredParagraph is computed with buildForStaticLayout.
+ * 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 MeasuredParagraph is computed with buildForStaticLayout.
+ * Returns empty array in other cases.
+ */
+ public @NonNull IntArray getFontMetrics() {
+ return mFontMetrics;
+ }
+
+ /**
+ * Returns the native ptr of the MeasuredParagraph.
+ *
+ * This is available only if the MeasuredParagraph is computed with buildForStaticLayout.
+ * Returns 0 in other cases.
+ */
+ public /* Maybe Zero */ long getNativePtr() {
+ return mNativePtr;
+ }
+
+ /**
+ * Returns the width of the given range.
+ *
+ * This is not available if the MeasuredParagraph is computed with buildForBidi.
+ * Returns 0 if the MeasuredParagraph is computed with buildForBidi.
+ *
+ * @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
+ */
+ public float getWidth(int start, int end) {
+ if (mNativePtr == 0) {
+ // We have result in Java.
+ final float[] widths = mWidths.getRawArray();
+ float r = 0.0f;
+ for (int i = start; i < end; ++i) {
+ r += widths[i];
+ }
+ return r;
+ } else {
+ // We have result in native.
+ return nGetWidth(mNativePtr, start, end);
+ }
+ }
+
+ /**
+ * Generates new MeasuredParagraph 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 MeasuredParagraph if you want to recycle it.
+ *
+ * @return measured text
+ */
+ public static @NonNull MeasuredParagraph buildForBidi(@NonNull CharSequence text,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @NonNull TextDirectionHeuristic textDir,
+ @Nullable MeasuredParagraph recycle) {
+ final MeasuredParagraph mt = recycle == null ? obtain() : recycle;
+ mt.resetAndAnalyzeBidi(text, start, end, textDir);
+ return mt;
+ }
+
+ /**
+ * Generates new MeasuredParagraph 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 MeasuredParagraph if you want to recycle it.
+ *
+ * @return measured text
+ */
+ public static @NonNull MeasuredParagraph buildForMeasurement(@NonNull TextPaint paint,
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @NonNull TextDirectionHeuristic textDir,
+ @Nullable MeasuredParagraph recycle) {
+ final MeasuredParagraph 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 mt;
+ }
+
+ /**
+ * Generates new MeasuredParagraph 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 MeasuredParagraph if you want to recycle it.
+ *
+ * @return measured text
+ */
+ public static @NonNull MeasuredParagraph buildForStaticLayout(
+ @NonNull TextPaint paint,
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @NonNull TextDirectionHeuristic textDir,
+ boolean computeHyphenation,
+ boolean computeLayout,
+ @Nullable MeasuredParagraph recycle) {
+ final MeasuredParagraph 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(
+ nBuildNativeMeasuredParagraph(nativeBuilderPtr, mt.mCopiedBuffer,
+ computeHyphenation, computeLayout));
+ } 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(nBuildNativeMeasuredParagraph(nativeBuilderPtr, mt.mCopiedBuffer,
+ computeHyphenation, computeLayout));
+ } finally {
+ nFreeBuilder(nativeBuilderPtr);
+ }
+
+ return mt;
+ }
+
+ /**
+ * 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
+ */
+ 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;
+
+ if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
+ mCopiedBuffer = new char[mTextLength];
+ }
+ TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
+
+ // 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 = 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 > 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(mCopiedBuffer, 0, mTextLength)) {
+ mLevels.clear();
+ mParaDir = Layout.DIR_LEFT_TO_RIGHT;
+ mLtrWithoutBidi = true;
+ } else {
+ final int bidiRequest;
+ if (textDir == TextDirectionHeuristics.LTR) {
+ bidiRequest = Layout.DIR_REQUEST_LTR;
+ } else if (textDir == TextDirectionHeuristics.RTL) {
+ bidiRequest = Layout.DIR_REQUEST_RTL;
+ } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
+ bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR;
+ } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
+ bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
+ } else {
+ final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
+ bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
+ }
+ mLevels.resize(mTextLength);
+ mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
+ mLtrWithoutBidi = false;
+ }
+ }
+
+ 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);
+ }
+ }
+
+ 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 (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 {
+ nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
+ false /* isRtl */);
+ }
+ } 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);
+ }
+ }
+ }
+ }
+
+ 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;
+
+ final boolean needFontMetrics = nativeBuilderPtr != 0;
+
+ if (needFontMetrics && mCachedFm == null) {
+ mCachedFm = new Paint.FontMetricsInt();
+ }
+
+ ReplacementSpan replacement = null;
+ 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);
+ }
+ }
+ }
+
+ final int startInCopiedBuffer = start - mTextStart;
+ final int endInCopiedBuffer = end - mTextStart;
+
+ if (replacement != null) {
+ applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer,
+ nativeBuilderPtr);
+ } else {
+ applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeBuilderPtr);
+ }
+
+ if (needFontMetrics) {
+ if (mCachedPaint.baselineShift < 0) {
+ mCachedFm.ascent += mCachedPaint.baselineShift;
+ mCachedFm.top += mCachedPaint.baselineShift;
+ } else {
+ mCachedFm.descent += mCachedPaint.baselineShift;
+ mCachedFm.bottom += mCachedPaint.baselineShift;
+ }
+
+ mFontMetrics.append(mCachedFm.top);
+ mFontMetrics.append(mCachedFm.bottom);
+ mFontMetrics.append(mCachedFm.ascent);
+ mFontMetrics.append(mCachedFm.descent);
+ }
+ }
+
+ /**
+ * 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 MeasuredParagraph is computed with buildForMeasurement.
+ * 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) {
+ width -= w[i];
+ if (width < 0.0f) break;
+ i++;
+ }
+ while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
+ return i;
+ } else {
+ int i = limit - 1;
+ while (i >= 0) {
+ width -= w[i];
+ if (width < 0.0f) break;
+ i--;
+ }
+ while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
+ i++;
+ }
+ return limit - i - 1;
+ }
+ }
+
+ /**
+ * Returns the length of the substring.
+ *
+ * This only works if the MeasuredParagraph is computed with buildForMeasurement.
+ * Undefined behavior in other case.
+ */
+ @FloatRange(from = 0.0f) float measure(int start, int limit) {
+ float width = 0;
+ 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 MeasuredParagraph 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 MeasuredParagraph 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 nBuildNativeMeasuredParagraph(/* Non Zero */ long nativeBuilderPtr,
+ @NonNull char[] text,
+ boolean computeHyphenation,
+ boolean computeLayout);
+
+ private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
+
+ @CriticalNative
+ private static native float nGetWidth(/* Non Zero */ long nativePtr,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end);
+
+ @CriticalNative
+ private static native /* Non Zero */ long nGetReleaseFunc();
+}
diff --git a/android/text/MeasuredText_Delegate.java b/android/text/MeasuredParagraph_Delegate.java
index adcc774c..e4dbee0b 100644
--- a/android/text/MeasuredText_Delegate.java
+++ b/android/text/MeasuredParagraph_Delegate.java
@@ -33,32 +33,32 @@ import java.util.Arrays;
import libcore.util.NativeAllocationRegistry_Delegate;
/**
- * Delegate that provides implementation for native methods in {@link android.text.MeasuredText}
+ * Delegate that provides implementation for native methods in {@link android.text.MeasuredParagraph}
* <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 {
+public class MeasuredParagraph_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 final DelegateManager<MeasuredParagraphBuilder> sBuilderManager =
+ new DelegateManager<>(MeasuredParagraphBuilder.class);
+ private static final DelegateManager<MeasuredParagraph_Delegate> sManager =
+ new DelegateManager<>(MeasuredParagraph_Delegate.class);
private static long sFinalizer = -1;
private long mNativeBuilderPtr;
@LayoutlibDelegate
/*package*/ static long nInitBuilder() {
- return sBuilderManager.addNewDelegate(new MeasuredTextBuilder());
+ return sBuilderManager.addNewDelegate(new MeasuredParagraphBuilder());
}
/**
* Apply style to make native measured text.
*
- * @param nativeBuilderPtr The native MeasuredText builder pointer.
+ * @param nativeBuilderPtr The native MeasuredParagraph 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.
@@ -67,7 +67,7 @@ public class MeasuredText_Delegate {
@LayoutlibDelegate
/*package*/ static void nAddStyleRun(long nativeBuilderPtr, long paintPtr, int start,
int end, boolean isRtl) {
- MeasuredTextBuilder builder = sBuilderManager.getDelegate(nativeBuilderPtr);
+ MeasuredParagraphBuilder builder = sBuilderManager.getDelegate(nativeBuilderPtr);
if (builder == null) {
return;
}
@@ -77,7 +77,7 @@ public class MeasuredText_Delegate {
/**
* Apply ReplacementRun to make native measured text.
*
- * @param nativeBuilderPtr The native MeasuredText builder pointer.
+ * @param nativeBuilderPtr The native MeasuredParagraph 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.
@@ -86,7 +86,7 @@ public class MeasuredText_Delegate {
@LayoutlibDelegate
/*package*/ static void nAddReplacementRun(long nativeBuilderPtr, long paintPtr, int start,
int end, float width) {
- MeasuredTextBuilder builder = sBuilderManager.getDelegate(nativeBuilderPtr);
+ MeasuredParagraphBuilder builder = sBuilderManager.getDelegate(nativeBuilderPtr);
if (builder == null) {
return;
}
@@ -94,8 +94,9 @@ public class MeasuredText_Delegate {
}
@LayoutlibDelegate
- /*package*/ static long nBuildNativeMeasuredText(long nativeBuilderPtr, @NonNull char[] text) {
- MeasuredText_Delegate delegate = new MeasuredText_Delegate();
+ /*package*/ static long nBuildNativeMeasuredParagraph(long nativeBuilderPtr,
+ @NonNull char[] text, boolean computeHyphenation) {
+ MeasuredParagraph_Delegate delegate = new MeasuredParagraph_Delegate();
delegate.mNativeBuilderPtr = nativeBuilderPtr;
return sManager.addNewDelegate(delegate);
}
@@ -107,7 +108,7 @@ public class MeasuredText_Delegate {
@LayoutlibDelegate
/*package*/ static long nGetReleaseFunc() {
- synchronized (MeasuredText_Delegate.class) {
+ synchronized (MeasuredParagraph_Delegate.class) {
if (sFinalizer == -1) {
sFinalizer = NativeAllocationRegistry_Delegate.createFinalizer(
sManager::removeJavaReferenceFor);
@@ -126,11 +127,11 @@ public class MeasuredText_Delegate {
}
public static void computeRuns(long measuredTextPtr, Builder staticLayoutBuilder) {
- MeasuredText_Delegate delegate = sManager.getDelegate(measuredTextPtr);
+ MeasuredParagraph_Delegate delegate = sManager.getDelegate(measuredTextPtr);
if (delegate == null) {
return;
}
- MeasuredTextBuilder builder = sBuilderManager.getDelegate(delegate.mNativeBuilderPtr);
+ MeasuredParagraphBuilder builder = sBuilderManager.getDelegate(delegate.mNativeBuilderPtr);
if (builder == null) {
return;
}
@@ -172,7 +173,7 @@ public class MeasuredText_Delegate {
}
}
- private static class MeasuredTextBuilder {
+ private static class MeasuredParagraphBuilder {
private final ArrayList<Run> mRuns = new ArrayList<>();
}
}
diff --git a/android/text/MeasuredText.java b/android/text/MeasuredText.java
index 14d6f9e8..ff23395d 100644
--- a/android/text/MeasuredText.java
+++ b/android/text/MeasuredText.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2010 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.
@@ -16,661 +16,398 @@
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.Pools.SynchronizedPool;
+import android.util.IntArray;
-import dalvik.annotation.optimization.CriticalNative;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.Preconditions;
-import libcore.util.NativeAllocationRegistry;
-
-import java.util.Arrays;
+import java.util.ArrayList;
/**
- * 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
+ * A text which has already been measured.
*/
-public class MeasuredText {
- private static final char OBJECT_REPLACEMENT_CHARACTER = '\uFFFC';
+public class MeasuredText implements Spanned {
+ private static final char LINE_FEED = '\n';
- private static final NativeAllocationRegistry sRegistry = new NativeAllocationRegistry(
- MeasuredText.class.getClassLoader(), nGetReleaseFunc(), 1024);
+ // The original text.
+ private final @NonNull CharSequence mText;
- private MeasuredText() {} // Use build static functions instead.
+ // The inclusive start offset of the measuring target.
+ private final @IntRange(from = 0) int mStart;
- private static final SynchronizedPool<MeasuredText> sPool = new SynchronizedPool<>(1);
+ // The exclusive end offset of the measuring target.
+ private final @IntRange(from = 0) int mEnd;
- private static @NonNull MeasuredText obtain() { // Use build static functions instead.
- final MeasuredText mt = sPool.acquire();
- return mt != null ? mt : new MeasuredText();
- }
+ // 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 MeasuredParagraph[] mMeasuredParagraphs;
+
+ // The sorted paragraph end offsets.
+ private final @NonNull int[] mParagraphBreakPoints;
+
+ // The break strategy for this measured text.
+ private final @Layout.BreakStrategy int mBreakStrategy;
+
+ // The hyphenation frequency for this measured text.
+ private final @Layout.HyphenationFrequency int mHyphenationFrequency;
/**
- * Recycle the MeasuredText.
- *
- * Do not call any methods after you call this method.
+ * A Builder for MeasuredText
*/
- public void recycle() {
- release();
- sPool.release(this);
- }
+ public static final class Builder {
+ // Mandatory parameters.
+ private final @NonNull CharSequence mText;
+ private final @NonNull TextPaint mPaint;
+
+ // Members to be updated by setters.
+ private @IntRange(from = 0) int mStart;
+ private @IntRange(from = 0) int mEnd;
+ private TextDirectionHeuristic mTextDir = TextDirectionHeuristics.FIRSTSTRONG_LTR;
+ private @Layout.BreakStrategy int mBreakStrategy = Layout.BREAK_STRATEGY_HIGH_QUALITY;
+ private @Layout.HyphenationFrequency int mHyphenationFrequency =
+ Layout.HYPHENATION_FREQUENCY_NORMAL;
+
+
+ /**
+ * Builder constructor
+ *
+ * @param text The text to be measured.
+ * @param paint The paint to be used for drawing.
+ */
+ public Builder(@NonNull CharSequence text, @NonNull TextPaint paint) {
+ Preconditions.checkNotNull(text);
+ Preconditions.checkNotNull(paint);
+
+ mText = text;
+ mPaint = paint;
+ mStart = 0;
+ mEnd = text.length();
+ }
- // The casted original text.
- //
- // This may be null if the passed text is not a Spanned.
- private @Nullable Spanned mSpanned;
+ /**
+ * Set the range of measuring target.
+ *
+ * @param start The measuring target start offset in the text.
+ * @param end The measuring target end offset in the text.
+ */
+ public @NonNull Builder setRange(@IntRange(from = 0) int start,
+ @IntRange(from = 0) int end) {
+ Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
+ Preconditions.checkArgumentInRange(end, 0, mText.length(), "end");
+ Preconditions.checkArgument(start <= end, "The range is reversed.");
+
+ mStart = start;
+ mEnd = end;
+ return this;
+ }
- // The start offset of the target range in the original text (mSpanned);
- private @IntRange(from = 0) int mTextStart;
+ /**
+ * Set the text direction heuristic
+ *
+ * The default value is {@link TextDirectionHeuristics#FIRSTSTRONG_LTR}.
+ *
+ * @param textDir The text direction heuristic for resolving bidi behavior.
+ * @return this builder, useful for chaining.
+ */
+ public @NonNull Builder setTextDirection(@NonNull TextDirectionHeuristic textDir) {
+ Preconditions.checkNotNull(textDir);
+ mTextDir = textDir;
+ return this;
+ }
- // The length of the target range in the original text.
- private @IntRange(from = 0) int mTextLength;
+ /**
+ * Set the break strategy
+ *
+ * The default value is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}.
+ *
+ * @param breakStrategy The break strategy.
+ * @return this builder, useful for chaining.
+ */
+ public @NonNull Builder setBreakStrategy(@Layout.BreakStrategy int breakStrategy) {
+ mBreakStrategy = breakStrategy;
+ return this;
+ }
- // The copied character buffer for measuring text.
- //
- // The length of this array is mTextLength.
- private @Nullable char[] mCopiedBuffer;
+ /**
+ * Set the hyphenation frequency
+ *
+ * The default value is {@link Layout#HYPHENATION_FREQUENCY_NORMAL}.
+ *
+ * @param hyphenationFrequency The hyphenation frequency.
+ * @return this builder, useful for chaining.
+ */
+ public @NonNull Builder setHyphenationFrequency(
+ @Layout.HyphenationFrequency int hyphenationFrequency) {
+ mHyphenationFrequency = hyphenationFrequency;
+ return this;
+ }
- // The whole paragraph direction.
- private @Layout.Direction int mParaDir;
+ /**
+ * Build the measured text
+ *
+ * @return the measured text.
+ */
+ public @NonNull MeasuredText build() {
+ return build(true /* build full layout result */);
+ }
- // True if the text is LTR direction and doesn't contain any bidi characters.
- private boolean mLtrWithoutBidi;
+ /** @hide */
+ public @NonNull MeasuredText build(boolean computeLayout) {
+ final boolean needHyphenation = mBreakStrategy != Layout.BREAK_STRATEGY_SIMPLE
+ && mHyphenationFrequency != Layout.HYPHENATION_FREQUENCY_NONE;
+
+ final IntArray paragraphEnds = new IntArray();
+ final ArrayList<MeasuredParagraph> measuredTexts = new ArrayList<>();
+
+ int paraEnd = 0;
+ for (int paraStart = mStart; paraStart < mEnd; paraStart = paraEnd) {
+ paraEnd = TextUtils.indexOf(mText, LINE_FEED, paraStart, mEnd);
+ if (paraEnd < 0) {
+ // No LINE_FEED(U+000A) character found. Use end of the text as the paragraph
+ // end.
+ paraEnd = mEnd;
+ } else {
+ paraEnd++; // Includes LINE_FEED(U+000A) to the prev paragraph.
+ }
- // 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);
- }
+ paragraphEnds.add(paraEnd);
+ measuredTexts.add(MeasuredParagraph.buildForStaticLayout(
+ mPaint, mText, paraStart, paraEnd, mTextDir, needHyphenation,
+ computeLayout, null /* no recycle */));
+ }
- // Decouple the native object from this Java object and release the native object.
- private void unbindNativeObject() {
- if (mNativePtr != 0) {
- mNativeObjectCleaner.run();
- mNativePtr = 0;
+ return new MeasuredText(mText, mStart, mEnd, mPaint, mTextDir, mBreakStrategy,
+ mHyphenationFrequency, measuredTexts.toArray(
+ new MeasuredParagraph[measuredTexts.size()]),
+ paragraphEnds.toArray());
}
+ };
+
+ // Use MeasuredText.Builder instead.
+ private MeasuredText(@NonNull CharSequence text,
+ @IntRange(from = 0) int start,
+ @IntRange(from = 0) int end,
+ @NonNull TextPaint paint,
+ @NonNull TextDirectionHeuristic textDir,
+ @Layout.BreakStrategy int breakStrategy,
+ @Layout.HyphenationFrequency int frequency,
+ @NonNull MeasuredParagraph[] measuredTexts,
+ @NonNull int[] paragraphBreakPoints) {
+ mText = text;
+ mStart = start;
+ mEnd = end;
+ // Copy the paint so that we can keep the reference of typeface in native layout result.
+ mPaint = new TextPaint(paint);
+ mMeasuredParagraphs = measuredTexts;
+ mParagraphBreakPoints = paragraphBreakPoints;
+ mTextDir = textDir;
+ mBreakStrategy = breakStrategy;
+ mHyphenationFrequency = frequency;
}
- // Following two objects are for avoiding object allocation.
- private @NonNull TextPaint mCachedPaint = new TextPaint();
- private @Nullable Paint.FontMetricsInt mCachedFm;
-
/**
- * Releases internal buffers.
+ * Return the underlying text.
*/
- public void release() {
- reset();
- mLevels.clearWithReleasingLargeArray();
- mWidths.clearWithReleasingLargeArray();
- mFontMetrics.clearWithReleasingLargeArray();
- mSpanEndCache.clearWithReleasingLargeArray();
+ public @NonNull CharSequence getText() {
+ return mText;
}
/**
- * Resets the internal state for starting new text.
+ * Returns the inclusive start offset of measured region.
*/
- private void reset() {
- mSpanned = null;
- mCopiedBuffer = null;
- mWholeWidth = 0;
- mLevels.clear();
- mWidths.clear();
- mFontMetrics.clear();
- mSpanEndCache.clear();
- unbindNativeObject();
+ public @IntRange(from = 0) int getStart() {
+ return mStart;
}
/**
- * Returns the characters to be measured.
- *
- * This is always available.
+ * Returns the exclusive end offset of measured region.
*/
- public @NonNull char[] getChars() {
- return mCopiedBuffer;
+ public @IntRange(from = 0) int getEnd() {
+ return mEnd;
}
/**
- * Returns the paragraph direction.
- *
- * This is always available.
+ * Returns the text direction associated with char sequence.
*/
- public @Layout.Direction int getParagraphDir() {
- return mParaDir;
+ public @NonNull TextDirectionHeuristic getTextDir() {
+ return mTextDir;
}
/**
- * Returns the directions.
- *
- * This is always available.
+ * Returns the paint used to measure this text.
*/
- 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);
+ public @NonNull TextPaint getPaint() {
+ return mPaint;
}
/**
- * Returns the whole text width.
- *
- * This is available only if the MeasureText is computed with computeForMeasurement.
- * Returns 0 in other cases.
+ * Returns the length of the paragraph of this text.
*/
- public @FloatRange(from = 0.0f) float getWholeWidth() {
- return mWholeWidth;
+ public @IntRange(from = 0) int getParagraphCount() {
+ return mParagraphBreakPoints.length;
}
/**
- * Returns the individual character's width.
- *
- * This is available only if the MeasureText is computed with computeForMeasurement.
- * Returns empty array in other cases.
+ * Returns the paragraph start offset of the text.
*/
- public @NonNull FloatArray getWidths() {
- return mWidths;
+ 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 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.
+ * Returns the paragraph end offset of the text.
*/
- public @NonNull IntArray getSpanEndCache() {
- return mSpanEndCache;
+ public @IntRange(from = 0) int getParagraphEnd(@IntRange(from = 0) int paraIndex) {
+ Preconditions.checkArgumentInRange(paraIndex, 0, getParagraphCount(), "paraIndex");
+ return mParagraphBreakPoints[paraIndex];
}
- /**
- * 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;
+ /** @hide */
+ public @NonNull MeasuredParagraph getMeasuredParagraph(@IntRange(from = 0) int paraIndex) {
+ return mMeasuredParagraphs[paraIndex];
}
/**
- * Returns the native ptr of the MeasuredText.
- *
- * This is available only if the MeasureText is computed with computeForStaticLayout.
- * Returns 0 in other cases.
+ * Returns the break strategy for this text.
*/
- public /* Maybe Zero */ long getNativePtr() {
- return mNativePtr;
+ public @Layout.BreakStrategy int getBreakStrategy() {
+ return mBreakStrategy;
}
/**
- * 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
+ * Returns the hyphenation frequency for this 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;
+ public @Layout.HyphenationFrequency int getHyphenationFrequency() {
+ return mHyphenationFrequency;
}
/**
- * 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
+ * Returns true if the given TextPaint gives the same result of text layout for this text.
+ * @hide
*/
- 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 mt;
+ public boolean canUseMeasuredResult(@NonNull TextPaint paint) {
+ return mPaint.getTextSize() == paint.getTextSize()
+ && mPaint.getTextSkewX() == paint.getTextSkewX()
+ && mPaint.getTextScaleX() == paint.getTextScaleX()
+ && mPaint.getLetterSpacing() == paint.getLetterSpacing()
+ && mPaint.getWordSpacing() == paint.getWordSpacing()
+ && mPaint.getFlags() == paint.getFlags() // Maybe not all flag affects text layout.
+ && mPaint.getTextLocales() == paint.getTextLocales() // need to be equals?
+ && mPaint.getFontVariationSettings() == paint.getFontVariationSettings()
+ && mPaint.getTypeface() == paint.getTypeface()
+ && TextUtils.equals(mPaint.getFontFeatureSettings(), paint.getFontFeatureSettings());
}
- /**
- * 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);
+ /** @hide */
+ public int findParaIndex(@IntRange(from = 0) int pos) {
+ // TODO: Maybe good to remove paragraph concept from MeasuredText and add substring layout
+ // support to StaticLayout.
+ for (int i = 0; i < mParagraphBreakPoints.length; ++i) {
+ if (pos < mParagraphBreakPoints[i]) {
+ return i;
}
- return mt;
}
+ throw new IndexOutOfBoundsException(
+ "pos must be less than " + mParagraphBreakPoints[mParagraphBreakPoints.length - 1]
+ + ", gave " + pos);
+ }
- 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);
+ /** @hide */
+ public float getWidth(@IntRange(from = 0) int start, @IntRange(from = 0) int end) {
+ final int paraIndex = findParaIndex(start);
+ final int paraStart = getParagraphStart(paraIndex);
+ final int paraEnd = getParagraphEnd(paraIndex);
+ if (start < paraStart || paraEnd < end) {
+ throw new RuntimeException("Cannot measured across the paragraph:"
+ + "para: (" + paraStart + ", " + paraEnd + "), "
+ + "request: (" + start + ", " + end + ")");
}
-
- return mt;
+ return getMeasuredParagraph(paraIndex).getWidth(start - paraStart, end - paraStart);
}
- /**
- * 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
- */
- 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;
-
- if (mCopiedBuffer == null || mCopiedBuffer.length != mTextLength) {
- mCopiedBuffer = new char[mTextLength];
- }
- TextUtils.getChars(text, start, end, mCopiedBuffer, 0);
-
- // 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 = 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 > mTextLength) endInPara = mTextLength;
- Arrays.fill(mCopiedBuffer, startInPara, endInPara, OBJECT_REPLACEMENT_CHARACTER);
- }
- }
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // Spanned overrides
+ //
+ // Just proxy for underlying mText if appropriate.
- if ((textDir == TextDirectionHeuristics.LTR ||
- textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR ||
- textDir == TextDirectionHeuristics.ANYRTL_LTR) &&
- TextUtils.doesNotNeedBidi(mCopiedBuffer, 0, mTextLength)) {
- mLevels.clear();
- mParaDir = Layout.DIR_LEFT_TO_RIGHT;
- mLtrWithoutBidi = true;
+ @Override
+ public <T> T[] getSpans(int start, int end, Class<T> type) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpans(start, end, type);
} else {
- final int bidiRequest;
- if (textDir == TextDirectionHeuristics.LTR) {
- bidiRequest = Layout.DIR_REQUEST_LTR;
- } else if (textDir == TextDirectionHeuristics.RTL) {
- bidiRequest = Layout.DIR_REQUEST_RTL;
- } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_LTR) {
- bidiRequest = Layout.DIR_REQUEST_DEFAULT_LTR;
- } else if (textDir == TextDirectionHeuristics.FIRSTSTRONG_RTL) {
- bidiRequest = Layout.DIR_REQUEST_DEFAULT_RTL;
- } else {
- final boolean isRtl = textDir.isRtl(mCopiedBuffer, 0, mTextLength);
- bidiRequest = isRtl ? Layout.DIR_REQUEST_RTL : Layout.DIR_REQUEST_LTR;
- }
- mLevels.resize(mTextLength);
- mParaDir = AndroidBidi.bidi(bidiRequest, mCopiedBuffer, mLevels.getRawArray());
- mLtrWithoutBidi = false;
+ return ArrayUtils.emptyArray(type);
}
}
- 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;
+ @Override
+ public int getSpanStart(Object tag) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpanStart(tag);
} else {
- nAddReplacementRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
- width);
+ return -1;
}
}
- 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 (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 {
- nAddStyleRun(nativeBuilderPtr, mCachedPaint.getNativeInstance(), start, end,
- false /* isRtl */);
- }
+ @Override
+ public int getSpanEnd(Object tag) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpanEnd(tag);
} 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);
- }
- }
+ return -1;
}
}
- 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;
-
- final boolean needFontMetrics = nativeBuilderPtr != 0;
-
- if (needFontMetrics && mCachedFm == null) {
- mCachedFm = new Paint.FontMetricsInt();
- }
-
- ReplacementSpan replacement = null;
- 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);
- }
- }
- }
-
- final int startInCopiedBuffer = start - mTextStart;
- final int endInCopiedBuffer = end - mTextStart;
-
- if (replacement != null) {
- applyReplacementRun(replacement, startInCopiedBuffer, endInCopiedBuffer,
- nativeBuilderPtr);
+ @Override
+ public int getSpanFlags(Object tag) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).getSpanFlags(tag);
} else {
- applyStyleRun(startInCopiedBuffer, endInCopiedBuffer, nativeBuilderPtr);
- }
-
- if (needFontMetrics) {
- if (mCachedPaint.baselineShift < 0) {
- mCachedFm.ascent += mCachedPaint.baselineShift;
- mCachedFm.top += mCachedPaint.baselineShift;
- } else {
- mCachedFm.descent += mCachedPaint.baselineShift;
- mCachedFm.bottom += mCachedPaint.baselineShift;
- }
-
- mFontMetrics.append(mCachedFm.top);
- mFontMetrics.append(mCachedFm.bottom);
- mFontMetrics.append(mCachedFm.ascent);
- mFontMetrics.append(mCachedFm.descent);
+ return 0;
}
}
- /**
- * 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) {
- width -= w[i];
- if (width < 0.0f) break;
- i++;
- }
- while (i > 0 && mCopiedBuffer[i - 1] == ' ') i--;
- return i;
+ @Override
+ public int nextSpanTransition(int start, int limit, Class type) {
+ if (mText instanceof Spanned) {
+ return ((Spanned) mText).nextSpanTransition(start, limit, type);
} else {
- int i = limit - 1;
- while (i >= 0) {
- width -= w[i];
- if (width < 0.0f) break;
- i--;
- }
- while (i < limit - 1 && (mCopiedBuffer[i + 1] == ' ' || w[i + 1] == 0.0f)) {
- i++;
- }
- return limit - i - 1;
- }
- }
-
- /**
- * 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.getRawArray();
- for (int i = start; i < limit; ++i) {
- width += w[i];
+ return mText.length();
}
- 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);
+ ///////////////////////////////////////////////////////////////////////////////////////////////
+ // CharSequence overrides.
+ //
+ // Just proxy for underlying mText.
- /**
- * 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);
+ @Override
+ public int length() {
+ return mText.length();
+ }
- private static native long nBuildNativeMeasuredText(/* Non Zero */ long nativeBuilderPtr,
- @NonNull char[] text);
+ @Override
+ public char charAt(int index) {
+ // TODO: Should this be index + mStart ?
+ return mText.charAt(index);
+ }
- private static native void nFreeBuilder(/* Non Zero */ long nativeBuilderPtr);
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ // TODO: return MeasuredText.
+ // TODO: Should this be index + mStart, end + mStart ?
+ return mText.subSequence(start, end);
+ }
- @CriticalNative
- private static native /* Non Zero */ long nGetReleaseFunc();
+ @Override
+ public String toString() {
+ return mText.toString();
+ }
}
diff --git a/android/text/PremeasuredText.java b/android/text/PremeasuredText.java
deleted file mode 100644
index 465314dd..00000000
--- a/android/text/PremeasuredText.java
+++ /dev/null
@@ -1,272 +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.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 d69b1190..e62f4216 100644
--- a/android/text/StaticLayout.java
+++ b/android/text/StaticLayout.java
@@ -55,7 +55,8 @@ public class StaticLayout extends Layout {
* 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.
+ * - Create MeasuredParagraph by MeasuredParagraph.buildForStaticLayout which measures in
+ * native.
* - Run nComputeLineBreaks() to obtain line breaks for the paragraph.
*
* After all paragraphs, call finish() to release expensive buffers.
@@ -650,34 +651,48 @@ public class StaticLayout extends Layout {
b.mJustificationMode != Layout.JUSTIFICATION_MODE_NONE,
indents, mLeftPaddings, mRightPaddings);
- PremeasuredText premeasured = null;
+ MeasuredText measured = null;
final Spanned spanned;
- if (source instanceof PremeasuredText) {
- premeasured = (PremeasuredText) source;
+ final boolean canUseMeasuredText;
+ if (source instanceof MeasuredText) {
+ measured = (MeasuredText) source;
- final CharSequence original = premeasured.getText();
- spanned = (original instanceof Spanned) ? (Spanned) original : null;
-
- if (bufStart != premeasured.getStart() || bufEnd != premeasured.getEnd()) {
+ if (bufStart != measured.getStart() || bufEnd != measured.getEnd()) {
// The buffer position has changed. Re-measure here.
- premeasured = PremeasuredText.build(original, paint, textDir, bufStart, bufEnd);
+ canUseMeasuredText = false;
+ } else if (b.mBreakStrategy != measured.getBreakStrategy()
+ || b.mHyphenationFrequency != measured.getHyphenationFrequency()) {
+ // The computed hyphenation pieces may not be able to used. Re-measure it.
+ canUseMeasuredText = false;
} 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();
+ // We can use measured information.
+ canUseMeasuredText = true;
}
} else {
- premeasured = PremeasuredText.build(source, paint, textDir, bufStart, bufEnd);
+ canUseMeasuredText = false;
+ }
+
+ if (!canUseMeasuredText) {
+ measured = new MeasuredText.Builder(source, paint)
+ .setRange(bufStart, bufEnd)
+ .setTextDirection(textDir)
+ .setBreakStrategy(b.mBreakStrategy)
+ .setHyphenationFrequency(b.mHyphenationFrequency)
+ .build(false /* full layout is not necessary for line breaking */);
spanned = (source instanceof Spanned) ? (Spanned) source : null;
+ } else {
+ final CharSequence original = measured.getText();
+ spanned = (original instanceof Spanned) ? (Spanned) original : null;
+ // Overwrite with the one when measured.
+ // TODO: Give an option for developer not to overwrite and measure again here?
+ textDir = measured.getTextDir();
+ paint = measured.getPaint();
}
try {
- for (int paraIndex = 0; paraIndex < premeasured.getParagraphCount(); paraIndex++) {
- final int paraStart = premeasured.getParagraphStart(paraIndex);
- final int paraEnd = premeasured.getParagraphEnd(paraIndex);
+ for (int paraIndex = 0; paraIndex < measured.getParagraphCount(); paraIndex++) {
+ final int paraStart = measured.getParagraphStart(paraIndex);
+ final int paraEnd = measured.getParagraphEnd(paraIndex);
int firstWidthLineCount = 1;
int firstWidth = outerWidth;
@@ -743,10 +758,10 @@ 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();
+ final MeasuredParagraph measuredPara = measured.getMeasuredParagraph(paraIndex);
+ final char[] chs = measuredPara.getChars();
+ final int[] spanEndCache = measuredPara.getSpanEndCache().getRawArray();
+ final int[] fmCache = measuredPara.getFontMetrics().getRawArray();
// TODO: Stop keeping duplicated width copy in native and Java.
widths.resize(chs.length);
@@ -759,7 +774,7 @@ public class StaticLayout extends Layout {
// Inputs
chs,
- measured.getNativePtr(),
+ measuredPara.getNativePtr(),
paraEnd - paraStart,
firstWidth,
firstWidthLineCount,
@@ -863,7 +878,7 @@ public class StaticLayout extends Layout {
v = out(source, here, endPos,
ascent, descent, fmTop, fmBottom,
v, spacingmult, spacingadd, chooseHt, chooseHtv, fm,
- flags[breakIndex], needMultiply, measured, bufEnd,
+ flags[breakIndex], needMultiply, measuredPara, bufEnd,
includepad, trackpad, addLastLineSpacing, chs, widths.getRawArray(),
paraStart, ellipsize, ellipsizedWidth, lineWidths[breakIndex],
paint, moreChars);
@@ -894,8 +909,8 @@ public class StaticLayout extends Layout {
if ((bufEnd == bufStart || source.charAt(bufEnd - 1) == CHAR_NEW_LINE)
&& mLineCount < mMaximumVisibleLineCount) {
- final MeasuredText measured =
- MeasuredText.buildForBidi(source, bufEnd, bufEnd, textDir, null);
+ final MeasuredParagraph measuredPara =
+ MeasuredParagraph.buildForBidi(source, bufEnd, bufEnd, textDir, null);
paint.getFontMetricsInt(fm);
v = out(source,
bufEnd, bufEnd, fm.ascent, fm.descent,
@@ -903,7 +918,7 @@ public class StaticLayout extends Layout {
v,
spacingmult, spacingadd, null,
null, fm, 0,
- needMultiply, measured, bufEnd,
+ needMultiply, measuredPara, bufEnd,
includepad, trackpad, addLastLineSpacing, null,
null, bufStart, ellipsize,
ellipsizedWidth, 0, paint, false);
@@ -913,12 +928,10 @@ public class StaticLayout extends Layout {
}
}
- // The parameters that are not changed in the method are marked as final to make the code
- // easier to understand.
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, @NonNull final MeasuredText measured,
+ final int flags, final boolean needMultiply, @NonNull final MeasuredParagraph 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,
@@ -943,21 +956,29 @@ public class StaticLayout extends Layout {
mLineDirections = grow;
}
- lines[off + START] = start;
- lines[off + TOP] = v;
+ if (chooseHt != null) {
+ fm.ascent = above;
+ fm.descent = below;
+ fm.top = top;
+ fm.bottom = bottom;
- // Information about hyphenation, tabs, and directions are needed for determining
- // ellipsization, so the values should be assigned before ellipsization.
+ for (int i = 0; i < chooseHt.length; i++) {
+ if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
+ ((LineHeightSpan.WithDensity) chooseHt[i])
+ .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
+ } else {
+ chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
+ }
+ }
- // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining
- // one bit for start field
- lines[off + TAB] |= flags & TAB_MASK;
- lines[off + HYPHEN] = flags;
- lines[off + DIR] |= dir << DIR_SHIFT;
- mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
+ above = fm.ascent;
+ below = fm.descent;
+ top = fm.top;
+ bottom = fm.bottom;
+ }
- final boolean firstLine = (j == 0);
- final boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
+ boolean firstLine = (j == 0);
+ boolean currentLineIsTheLastVisibleOne = (j + 1 == mMaximumVisibleLineCount);
if (ellipsize != null) {
// If there is only one line, then do any type of ellipsis except when it is MARQUEE
@@ -970,9 +991,9 @@ public class StaticLayout extends Layout {
(!firstLine && (currentLineIsTheLastVisibleOne || !moreChars) &&
ellipsize == TextUtils.TruncateAt.END);
if (doEllipsis) {
- calculateEllipsis(text, start, end, widths, widthStart,
- ellipsisWidth - getTotalInsets(j), ellipsize, j,
- textWidth, paint, forceEllipsis, dir);
+ calculateEllipsis(start, end, widths, widthStart,
+ ellipsisWidth, ellipsize, j,
+ textWidth, paint, forceEllipsis);
}
}
@@ -991,28 +1012,6 @@ public class StaticLayout extends Layout {
}
}
- if (chooseHt != null) {
- fm.ascent = above;
- fm.descent = below;
- fm.top = top;
- fm.bottom = bottom;
-
- for (int i = 0; i < chooseHt.length; i++) {
- if (chooseHt[i] instanceof LineHeightSpan.WithDensity) {
- ((LineHeightSpan.WithDensity) chooseHt[i])
- .chooseHeight(text, start, end, chooseHtv[i], v, fm, paint);
-
- } else {
- chooseHt[i].chooseHeight(text, start, end, chooseHtv[i], v, fm);
- }
- }
-
- above = fm.ascent;
- below = fm.descent;
- top = fm.top;
- bottom = fm.bottom;
- }
-
if (firstLine) {
if (trackPad) {
mTopPadding = top - above;
@@ -1023,6 +1022,8 @@ public class StaticLayout extends Layout {
}
}
+ int extra;
+
if (lastLine) {
if (trackPad) {
mBottomPadding = bottom - below;
@@ -1033,9 +1034,8 @@ public class StaticLayout extends Layout {
}
}
- final int extra;
if (needMultiply && (addLastLineLineSpacing || !lastLine)) {
- final double ex = (below - above) * (spacingmult - 1) + spacingadd;
+ double ex = (below - above) * (spacingmult - 1) + spacingadd;
if (ex >= 0) {
extra = (int)(ex + EXTRA_ROUNDING);
} else {
@@ -1045,6 +1045,8 @@ public class StaticLayout extends Layout {
extra = 0;
}
+ lines[off + START] = start;
+ lines[off + TOP] = v;
lines[off + DESCENT] = below + extra;
lines[off + EXTRA] = extra;
@@ -1052,7 +1054,7 @@ public class StaticLayout extends Layout {
// store the height as if it was ellipsized
if (!mEllipsized && currentLineIsTheLastVisibleOne) {
// below calculation as if it was the last line
- final int maxLineBelow = includePad ? bottom : below;
+ int maxLineBelow = includePad ? bottom : below;
// similar to the calculation of v below, without the extra.
mMaxLineHeight = v + (maxLineBelow - above);
}
@@ -1061,13 +1063,23 @@ public class StaticLayout extends Layout {
lines[off + mColumns + START] = end;
lines[off + mColumns + TOP] = v;
+ // TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining
+ // one bit for start field
+ lines[off + TAB] |= flags & TAB_MASK;
+ lines[off + HYPHEN] = flags;
+ lines[off + DIR] |= dir << DIR_SHIFT;
+ mLineDirections[j] = measured.getDirections(start - widthStart, end - widthStart);
+
mLineCount++;
return v;
}
- private void calculateEllipsis(CharSequence text, int lineStart, int lineEnd, float[] widths,
- int widthStart, float avail, TextUtils.TruncateAt where, int line, float textWidth,
- TextPaint paint, boolean forceEllipsis, int dir) {
+ private void calculateEllipsis(int lineStart, int lineEnd,
+ float[] widths, int widthStart,
+ float avail, TextUtils.TruncateAt where,
+ int line, float textWidth, TextPaint paint,
+ boolean forceEllipsis) {
+ avail -= getTotalInsets(line);
if (textWidth <= avail && !forceEllipsis) {
// Everything fits!
mLines[mColumns * line + ELLIPSIS_START] = 0;
@@ -1075,53 +1087,11 @@ public class StaticLayout extends Layout {
return;
}
- float tempAvail = avail;
- int numberOfTries = 0;
- boolean lineFits = false;
- mWorkPaint.set(paint);
- do {
- final float ellipsizedWidth = guessEllipsis(text, lineStart, lineEnd, widths,
- widthStart, tempAvail, where, line, mWorkPaint, forceEllipsis, dir);
- if (ellipsizedWidth <= avail) {
- lineFits = true;
- } else {
- numberOfTries++;
- if (numberOfTries > 10) {
- // If the text still doesn't fit after ten tries, assume it will never fit and
- // ellipsize it all.
- mLines[mColumns * line + ELLIPSIS_START] = 0;
- mLines[mColumns * line + ELLIPSIS_COUNT] = lineEnd - lineStart;
- lineFits = true;
- } else {
- // Some side effect of ellipsization has caused the text to go over the
- // available width. Let's make the available width shorter by exactly that
- // amount and retry.
- tempAvail -= ellipsizedWidth - avail;
- }
- }
- } while (!lineFits);
- mEllipsized = true;
- }
+ float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where));
+ int ellipsisStart = 0;
+ int ellipsisCount = 0;
+ int len = lineEnd - lineStart;
- // Returns the width of the ellipsized line which in some rare cases can actually be larger
- // than 'avail' (due to kerning or other context-based effect of replacement of text by
- // ellipsis). If all the line needs to ellipsized away, or it's an invalud hyphenation mode,
- // returns 0 so the caller can stop iterating.
- //
- // This method temporarily modifies the TextPaint passed to it, so the TextPaint passed to it
- // should not be accessed while the method is running.
- private float guessEllipsis(CharSequence text, int lineStart, int lineEnd, float[] widths,
- int widthStart, float avail, TextUtils.TruncateAt where, int line,
- TextPaint paint, boolean forceEllipsis, int dir) {
- final int savedHyphenEdit = paint.getHyphenEdit();
- paint.setHyphenEdit(0);
- final float ellipsisWidth = paint.measureText(TextUtils.getEllipsisString(where));
- final int ellipsisStart;
- final int ellipsisCount;
- final int len = lineEnd - lineStart;
- final int offset = lineStart - widthStart;
-
- int hyphen = getHyphen(line);
// We only support start ellipsis on a single line
if (where == TextUtils.TruncateAt.START) {
if (mMaximumVisibleLineCount == 1) {
@@ -1129,9 +1099,9 @@ public class StaticLayout extends Layout {
int i;
for (i = len; i > 0; i--) {
- final float w = widths[i - 1 + offset];
+ float w = widths[i - 1 + lineStart - widthStart];
if (w + sum + ellipsisWidth > avail) {
- while (i < len && widths[i + offset] == 0.0f) {
+ while (i < len && widths[i + lineStart - widthStart] == 0.0f) {
i++;
}
break;
@@ -1142,13 +1112,9 @@ public class StaticLayout extends Layout {
ellipsisStart = 0;
ellipsisCount = i;
- // Strip the potential hyphenation at beginning of line.
- hyphen &= ~Paint.HYPHENEDIT_MASK_START_OF_LINE;
} else {
- ellipsisStart = 0;
- ellipsisCount = 0;
if (Log.isLoggable(TAG, Log.WARN)) {
- Log.w(TAG, "Start ellipsis only supported with one line");
+ Log.w(TAG, "Start Ellipsis only supported with one line");
}
}
} else if (where == TextUtils.TruncateAt.END || where == TextUtils.TruncateAt.MARQUEE ||
@@ -1157,7 +1123,7 @@ public class StaticLayout extends Layout {
int i;
for (i = 0; i < len; i++) {
- final float w = widths[i + offset];
+ float w = widths[i + lineStart - widthStart];
if (w + sum + ellipsisWidth > avail) {
break;
@@ -1166,27 +1132,24 @@ public class StaticLayout extends Layout {
sum += w;
}
- if (forceEllipsis && i == len && len > 0) {
+ ellipsisStart = i;
+ ellipsisCount = len - i;
+ if (forceEllipsis && ellipsisCount == 0 && len > 0) {
ellipsisStart = len - 1;
ellipsisCount = 1;
- } else {
- ellipsisStart = i;
- ellipsisCount = len - i;
}
- // Strip the potential hyphenation at end of line.
- hyphen &= ~Paint.HYPHENEDIT_MASK_END_OF_LINE;
- } else { // where = TextUtils.TruncateAt.MIDDLE
- // We only support middle ellipsis on a single line.
+ } else {
+ // where = TextUtils.TruncateAt.MIDDLE We only support middle ellipsis on a single line
if (mMaximumVisibleLineCount == 1) {
float lsum = 0, rsum = 0;
int left = 0, right = len;
- final float ravail = (avail - ellipsisWidth) / 2;
+ float ravail = (avail - ellipsisWidth) / 2;
for (right = len; right > 0; right--) {
- final float w = widths[right - 1 + offset];
+ float w = widths[right - 1 + lineStart - widthStart];
if (w + rsum > ravail) {
- while (right < len && widths[right + offset] == 0.0f) {
+ while (right < len && widths[right + lineStart - widthStart] == 0.0f) {
right++;
}
break;
@@ -1194,9 +1157,9 @@ public class StaticLayout extends Layout {
rsum += w;
}
- final float lavail = avail - ellipsisWidth - rsum;
+ float lavail = avail - ellipsisWidth - rsum;
for (left = 0; left < right; left++) {
- final float w = widths[left + offset];
+ float w = widths[left + lineStart - widthStart];
if (w + lsum > lavail) {
break;
@@ -1208,53 +1171,14 @@ public class StaticLayout extends Layout {
ellipsisStart = left;
ellipsisCount = right - left;
} else {
- ellipsisStart = 0;
- ellipsisCount = 0;
if (Log.isLoggable(TAG, Log.WARN)) {
- Log.w(TAG, "Middle ellipsis only supported with one line");
+ Log.w(TAG, "Middle Ellipsis only supported with one line");
}
}
}
+ mEllipsized = true;
mLines[mColumns * line + ELLIPSIS_START] = ellipsisStart;
mLines[mColumns * line + ELLIPSIS_COUNT] = ellipsisCount;
-
- if (ellipsisStart == 0 && (ellipsisCount == 0 || ellipsisCount == len)) {
- // Unsupported ellipsization mode or all text is ellipsized away. Return 0.
- return 0.0f;
- }
-
- final boolean isSpanned = text instanceof Spanned;
- final Ellipsizer ellipsizedText = isSpanned
- ? new SpannedEllipsizer(text)
- : new Ellipsizer(text);
- ellipsizedText.mLayout = this;
- ellipsizedText.mMethod = where;
-
- final boolean hasTabs = getLineContainsTab(line);
- final TabStops tabStops;
- if (hasTabs && isSpanned) {
- final TabStopSpan[] tabs = getParagraphSpans((Spanned) ellipsizedText, lineStart,
- lineEnd, TabStopSpan.class);
- if (tabs.length == 0) {
- tabStops = null;
- } else {
- tabStops = new TabStops(TAB_INCREMENT, tabs);
- }
- } else {
- tabStops = null;
- }
- paint.setHyphenEdit(hyphen);
- final TextLine textline = TextLine.obtain();
- textline.set(paint, ellipsizedText, lineStart, lineEnd, dir, getLineDirections(line),
- hasTabs, tabStops);
- // Since TextLine.metric() returns negative values for RTL text, multiplication by dir
- // converts it to an actual width. Note that we don't want to use the absolute value,
- // since we may actually have glyphs with negative advances, which by definition always
- // fit.
- final float ellipsizedWidth = textline.metrics(null) * dir;
- TextLine.recycle(textline);
- paint.setHyphenEdit(savedHyphenEdit);
- return ellipsizedWidth;
}
private float getTotalInsets(int line) {
@@ -1494,8 +1418,6 @@ public class StaticLayout extends Layout {
*/
private int mMaxLineHeight = DEFAULT_MAX_LINE_HEIGHT;
- private TextPaint mWorkPaint = new TextPaint();
-
private static final int COLUMNS_NORMAL = 5;
private static final int COLUMNS_ELLIPSIZE = 7;
private static final int START = 0;
diff --git a/android/text/StaticLayoutPerfTest.java b/android/text/StaticLayoutPerfTest.java
index 5653a039..682885b3 100644
--- a/android/text/StaticLayoutPerfTest.java
+++ b/android/text/StaticLayoutPerfTest.java
@@ -25,10 +25,14 @@ import android.support.test.filters.LargeTest;
import android.support.test.runner.AndroidJUnit4;
import android.content.res.ColorStateList;
+import android.graphics.Canvas;
import android.graphics.Typeface;
import android.text.Layout;
import android.text.style.TextAppearanceSpan;
+import android.view.DisplayListCanvas;
+import android.view.RenderNode;
+import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -52,7 +56,7 @@ public class StaticLayoutPerfTest {
private static final boolean NO_STYLE_TEXT = false;
private static final boolean STYLE_TEXT = true;
- private final Random mRandom = new Random(31415926535L);
+ private Random mRandom;
private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static final int ALPHABET_LENGTH = ALPHABET.length();
@@ -98,6 +102,11 @@ public class StaticLayoutPerfTest {
return ssb;
}
+ @Before
+ public void setUp() {
+ mRandom = new Random(0);
+ }
+
@Test
public void testCreate_FixedText_NoStyle_Greedy_NoHyphenation() {
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
@@ -190,8 +199,11 @@ public class StaticLayoutPerfTest {
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
state.pauseTiming();
- final PremeasuredText text = PremeasuredText.build(
- generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .build();
state.resumeTiming();
StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
@@ -206,8 +218,11 @@ public class StaticLayoutPerfTest {
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
state.pauseTiming();
- final PremeasuredText text = PremeasuredText.build(
- generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .build();
state.resumeTiming();
StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
@@ -222,8 +237,11 @@ public class StaticLayoutPerfTest {
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
state.pauseTiming();
- final PremeasuredText text = PremeasuredText.build(
- generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .build();
state.resumeTiming();
StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
@@ -238,8 +256,11 @@ public class StaticLayoutPerfTest {
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
state.pauseTiming();
- final PremeasuredText text = PremeasuredText.build(
- generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT, LTR);
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NORMAL)
+ .build();
state.resumeTiming();
StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
@@ -254,8 +275,11 @@ public class StaticLayoutPerfTest {
final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
while (state.keepRunning()) {
state.pauseTiming();
- final PremeasuredText text = PremeasuredText.build(
- generateRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT, LTR);
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT)
+ .setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE)
+ .setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE)
+ .build();
state.resumeTiming();
StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH)
@@ -264,4 +288,157 @@ public class StaticLayoutPerfTest {
.build();
}
}
+
+ @Test
+ public void testDraw_FixedText_NoStyled() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT);
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_RandomText_Styled() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, STYLE_TEXT);
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_RandomText_NoStyled() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT);
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_RandomText_Styled_WithoutCache() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, STYLE_TEXT);
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ Canvas.freeTextLayoutCaches();
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_RandomText_NoStyled_WithoutCache() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final CharSequence text = generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT);
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ Canvas.freeTextLayoutCaches();
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_MeasuredText_Styled() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT).build();
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_MeasuredText_NoStyled() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT).build();
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_MeasuredText_Styled_WithoutCache() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, STYLE_TEXT), PAINT).build();
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ Canvas.freeTextLayoutCaches();
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
+ @Test
+ public void testDraw_MeasuredText_NoStyled_WithoutCache() {
+ final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+ final RenderNode node = RenderNode.create("benchmark", null);
+ while (state.keepRunning()) {
+ state.pauseTiming();
+ final MeasuredText text = new MeasuredText.Builder(
+ generateRandomParagraph(WORD_LENGTH, NO_STYLE_TEXT), PAINT).build();
+ final StaticLayout layout =
+ StaticLayout.Builder.obtain(text, 0, text.length(), PAINT, TEXT_WIDTH).build();
+ final DisplayListCanvas c = node.start(1200, 200);
+ Canvas.freeTextLayoutCaches();
+ state.resumeTiming();
+
+ layout.draw(c);
+ }
+ }
+
}
diff --git a/android/text/StaticLayout_Delegate.java b/android/text/StaticLayout_Delegate.java
index d524954e..d7cb596e 100644
--- a/android/text/StaticLayout_Delegate.java
+++ b/android/text/StaticLayout_Delegate.java
@@ -87,7 +87,7 @@ public class StaticLayout_Delegate {
builder.mLineWidth = new LineWidth(firstWidth, firstWidthLineCount, restWidth);
builder.mTabStopCalculator = new TabStops(variableTabStops, defaultTabStop);
- MeasuredText_Delegate.computeRuns(measuredTextPtr, builder);
+ MeasuredParagraph_Delegate.computeRuns(measuredTextPtr, builder);
// compute all possible breakpoints.
BreakIterator it = BreakIterator.getLineInstance();
diff --git a/android/text/TextLine.java b/android/text/TextLine.java
index 86cc0141..55367dcc 100644
--- a/android/text/TextLine.java
+++ b/android/text/TextLine.java
@@ -60,6 +60,7 @@ public class TextLine {
private char[] mChars;
private boolean mCharsValid;
private Spanned mSpanned;
+ private MeasuredText mMeasured;
// Additional width of whitespace for justification. This value is per whitespace, thus
// the line width will increase by mAddedWidth x (number of stretchable whitespaces).
@@ -118,6 +119,7 @@ public class TextLine {
tl.mSpanned = null;
tl.mTabs = null;
tl.mChars = null;
+ tl.mMeasured = null;
tl.mMetricAffectingSpanSpanSet.recycle();
tl.mCharacterStyleSpanSet.recycle();
@@ -168,6 +170,14 @@ public class TextLine {
hasReplacement = mReplacementSpanSpanSet.numberOfSpans > 0;
}
+ mMeasured = null;
+ if (text instanceof MeasuredText) {
+ MeasuredText mt = (MeasuredText) text;
+ if (mt.canUseMeasuredResult(paint)) {
+ mMeasured = mt;
+ }
+ }
+
mCharsValid = hasReplacement || hasTabs || directions != Layout.DIRS_ALL_LEFT_TO_RIGHT;
if (mCharsValid) {
@@ -736,8 +746,13 @@ public class TextLine {
return wp.getRunAdvance(mChars, start, end, contextStart, contextEnd, runIsRtl, offset);
} else {
final int delta = mStart;
- return wp.getRunAdvance(mText, delta + start, delta + end,
- delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
+ if (mMeasured == null) {
+ // TODO: Enable measured getRunAdvance for ReplacementSpan and RTL text.
+ return wp.getRunAdvance(mText, delta + start, delta + end,
+ delta + contextStart, delta + contextEnd, runIsRtl, delta + offset);
+ } else {
+ return mMeasured.getWidth(start + delta, end + delta);
+ }
}
}
diff --git a/android/text/TextUtils.java b/android/text/TextUtils.java
index 9c9fbf23..af0eebfb 100644
--- a/android/text/TextUtils.java
+++ b/android/text/TextUtils.java
@@ -88,8 +88,8 @@ public class TextUtils {
/** {@hide} */
@NonNull
- public static String getEllipsisString(@NonNull TruncateAt method) {
- return (method == TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL;
+ public static String getEllipsisString(@NonNull TextUtils.TruncateAt method) {
+ return (method == TextUtils.TruncateAt.END_SMALL) ? ELLIPSIS_TWO_DOTS : ELLIPSIS_NORMAL;
}
@@ -1194,11 +1194,9 @@ public class TextUtils {
* or, if it does not fit, a truncated
* copy with ellipsis character added at the specified edge or center.
*/
- @NonNull
- public static CharSequence ellipsize(@NonNull CharSequence text,
- @NonNull TextPaint p,
- @FloatRange(from = 0.0) float avail,
- @NonNull TruncateAt where) {
+ public static CharSequence ellipsize(CharSequence text,
+ TextPaint p,
+ float avail, TruncateAt where) {
return ellipsize(text, p, avail, where, false, null);
}
@@ -1214,11 +1212,9 @@ public class TextUtils {
* report the start and end of the ellipsized range. TextDirection
* is determined by the first strong directional character.
*/
- @NonNull
- public static CharSequence ellipsize(@NonNull CharSequence text,
- @NonNull TextPaint paint,
- @FloatRange(from = 0.0) float avail,
- @NonNull TruncateAt where,
+ public static CharSequence ellipsize(CharSequence text,
+ TextPaint paint,
+ float avail, TruncateAt where,
boolean preserveLength,
@Nullable EllipsizeCallback callback) {
return ellipsize(text, paint, avail, where, preserveLength, callback,
@@ -1239,131 +1235,94 @@ public class TextUtils {
*
* @hide
*/
- @NonNull
- public static CharSequence ellipsize(@NonNull CharSequence text,
- @NonNull TextPaint paint,
- @FloatRange(from = 0.0) float avail,
- @NonNull TruncateAt where,
+ public static CharSequence ellipsize(CharSequence text,
+ TextPaint paint,
+ float avail, TruncateAt where,
boolean preserveLength,
@Nullable EllipsizeCallback callback,
- @NonNull TextDirectionHeuristic textDir,
- @NonNull String ellipsis) {
+ TextDirectionHeuristic textDir, String ellipsis) {
+
+ int len = text.length();
- final int len = text.length();
- MeasuredText mt = null;
- MeasuredText resultMt = null;
+ MeasuredParagraph mt = null;
try {
- mt = MeasuredText.buildForMeasurement(paint, text, 0, text.length(), textDir, mt);
+ mt = MeasuredParagraph.buildForMeasurement(paint, text, 0, text.length(), textDir, mt);
float width = mt.getWholeWidth();
if (width <= avail) {
if (callback != null) {
callback.ellipsized(0, 0);
}
+
return text;
}
- // First estimate of effective width of ellipsis.
- float ellipsisWidth = paint.measureText(ellipsis);
- int numberOfTries = 0;
- boolean textFits = false;
- int start, end;
- CharSequence result;
- do {
- if (avail < ellipsisWidth) {
- // Even the ellipsis can't fit. So it all goes.
- start = 0;
- end = len;
- } else {
- final float remainingWidth = avail - ellipsisWidth;
- if (where == TruncateAt.START) {
- start = 0;
- end = len - mt.breakText(len, false /* backwards */, remainingWidth);
- } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
- start = mt.breakText(len, true /* forwards */, remainingWidth);
- end = len;
- } else {
- end = len - mt.breakText(len, false /* backwards */, remainingWidth / 2);
- start = mt.breakText(end, true /* forwards */,
- remainingWidth - mt.measure(end, len));
- }
- }
+ // XXX assumes ellipsis string does not require shaping and
+ // is unaffected by style
+ float ellipsiswid = paint.measureText(ellipsis);
+ avail -= ellipsiswid;
+
+ int left = 0;
+ int right = len;
+ if (avail < 0) {
+ // it all goes
+ } else if (where == TruncateAt.START) {
+ right = len - mt.breakText(len, false, avail);
+ } else if (where == TruncateAt.END || where == TruncateAt.END_SMALL) {
+ left = mt.breakText(len, true, avail);
+ } else {
+ right = len - mt.breakText(len, false, avail / 2);
+ avail -= mt.measure(right, len);
+ left = mt.breakText(right, true, avail);
+ }
- final char[] buf = mt.getChars();
- final Spanned sp = text instanceof Spanned ? (Spanned) text : null;
-
- final int removed = end - start;
- final int remaining = len - removed;
- if (preserveLength) {
- int pos = start;
- if (remaining > 0 && removed >= ellipsis.length()) {
- ellipsis.getChars(0, ellipsis.length(), buf, start);
- pos += ellipsis.length();
- } // else eliminate the ellipsis
- while (pos < end) {
- buf[pos++] = ELLIPSIS_FILLER;
- }
- final String s = new String(buf, 0, len);
- if (sp == null) {
- result = s;
- } else {
- final SpannableString ss = new SpannableString(s);
- copySpansFrom(sp, 0, len, Object.class, ss, 0);
- result = ss;
- }
- } else {
- if (remaining == 0) {
- result = "";
- } else if (sp == null) {
- final StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
- sb.append(buf, 0, start);
- sb.append(ellipsis);
- sb.append(buf, end, len - end);
- result = sb.toString();
- } else {
- final SpannableStringBuilder ssb = new SpannableStringBuilder();
- ssb.append(text, 0, start);
- ssb.append(ellipsis);
- ssb.append(text, end, len);
- result = ssb;
- }
- }
+ if (callback != null) {
+ callback.ellipsized(left, right);
+ }
- if (remaining == 0) { // All text is gone.
- textFits = true;
- } else {
- resultMt = MeasuredText.buildForMeasurement(
- paint, result, 0, result.length(), textDir, resultMt);
- width = resultMt.getWholeWidth();
- if (width <= avail) {
- textFits = true;
- } else {
- numberOfTries++;
- if (numberOfTries > 10) {
- // If the text still doesn't fit after ten tries, assume it will never
- // fit and ellipsize it all. We do this by setting the width of the
- // ellipsis to be positive infinity, so we get to empty text in the next
- // round.
- ellipsisWidth = Float.POSITIVE_INFINITY;
- } else {
- // Adjust the width of the ellipsis by adding the amount 'width' is
- // still over.
- ellipsisWidth += width - avail;
- }
- }
+ final char[] buf = mt.getChars();
+ Spanned sp = text instanceof Spanned ? (Spanned) text : null;
+
+ final int removed = right - left;
+ final int remaining = len - removed;
+ if (preserveLength) {
+ if (remaining > 0 && removed >= ellipsis.length()) {
+ ellipsis.getChars(0, ellipsis.length(), buf, left);
+ left += ellipsis.length();
+ } // else skip the ellipsis
+ for (int i = left; i < right; i++) {
+ buf[i] = ELLIPSIS_FILLER;
}
- } while (!textFits);
- if (callback != null) {
- callback.ellipsized(start, end);
+ String s = new String(buf, 0, len);
+ if (sp == null) {
+ return s;
+ }
+ SpannableString ss = new SpannableString(s);
+ copySpansFrom(sp, 0, len, Object.class, ss, 0);
+ return ss;
}
- return result;
+
+ if (remaining == 0) {
+ return "";
+ }
+
+ if (sp == null) {
+ StringBuilder sb = new StringBuilder(remaining + ellipsis.length());
+ sb.append(buf, 0, left);
+ sb.append(ellipsis);
+ sb.append(buf, right, len - right);
+ return sb.toString();
+ }
+
+ SpannableStringBuilder ssb = new SpannableStringBuilder();
+ ssb.append(text, 0, left);
+ ssb.append(ellipsis);
+ ssb.append(text, right, len);
+ return ssb;
} finally {
if (mt != null) {
mt.recycle();
}
- if (resultMt != null) {
- resultMt.recycle();
- }
}
}
@@ -1394,6 +1353,7 @@ public class TextUtils {
* @return the formatted CharSequence. If even the shortest sequence (e.g. {@code "A, 11 more"})
* doesn't fit, it will return an empty string.
*/
+
public static CharSequence listEllipsize(@Nullable Context context,
@Nullable List<CharSequence> elements, @NonNull String separator,
@NonNull TextPaint paint, @FloatRange(from=0.0,fromInclusive=false) float avail,
@@ -1479,11 +1439,11 @@ public class TextUtils {
public static CharSequence commaEllipsize(CharSequence text, TextPaint p,
float avail, String oneMore, String more, TextDirectionHeuristic textDir) {
- MeasuredText mt = null;
- MeasuredText tempMt = null;
+ MeasuredParagraph mt = null;
+ MeasuredParagraph tempMt = null;
try {
int len = text.length();
- mt = MeasuredText.buildForMeasurement(p, text, 0, len, textDir, mt);
+ mt = MeasuredParagraph.buildForMeasurement(p, text, 0, len, textDir, mt);
final float width = mt.getWholeWidth();
if (width <= avail) {
return text;
@@ -1523,7 +1483,7 @@ public class TextUtils {
}
// XXX this is probably ok, but need to look at it more
- tempMt = MeasuredText.buildForMeasurement(
+ tempMt = MeasuredParagraph.buildForMeasurement(
p, format, 0, format.length(), textDir, tempMt);
float moreWid = tempMt.getWholeWidth();
diff --git a/android/text/format/Formatter.java b/android/text/format/Formatter.java
index 8c90156d..ad3b4b6d 100644
--- a/android/text/format/Formatter.java
+++ b/android/text/format/Formatter.java
@@ -20,11 +20,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.res.Resources;
-import android.icu.text.DecimalFormat;
import android.icu.text.MeasureFormat;
-import android.icu.text.NumberFormat;
-import android.icu.text.UnicodeSet;
-import android.icu.text.UnicodeSetSpanner;
import android.icu.util.Measure;
import android.icu.util.MeasureUnit;
import android.net.NetworkUtils;
@@ -32,8 +28,6 @@ import android.text.BidiFormatter;
import android.text.TextUtils;
import android.view.View;
-import java.lang.reflect.Constructor;
-import java.math.BigDecimal;
import java.util.Locale;
/**
@@ -43,8 +37,6 @@ import java.util.Locale;
public final class Formatter {
/** {@hide} */
- public static final int FLAG_DEFAULT = 0;
- /** {@hide} */
public static final int FLAG_SHORTER = 1 << 0;
/** {@hide} */
public static final int FLAG_CALCULATE_ROUNDED = 1 << 1;
@@ -66,9 +58,7 @@ public final class Formatter {
return context.getResources().getConfiguration().getLocales().get(0);
}
- /**
- * Wraps the source string in bidi formatting characters in RTL locales.
- */
+ /* Wraps the source string in bidi formatting characters in RTL locales */
private static String bidiWrap(@NonNull Context context, String source) {
final Locale locale = localeFromContext(context);
if (TextUtils.getLayoutDirectionFromLocale(locale) == View.LAYOUT_DIRECTION_RTL) {
@@ -97,7 +87,12 @@ public final class Formatter {
* @return formatted string with the number
*/
public static String formatFileSize(@Nullable Context context, long sizeBytes) {
- return formatFileSize(context, sizeBytes, FLAG_DEFAULT);
+ if (context == null) {
+ return "";
+ }
+ final BytesResult res = formatBytes(context.getResources(), sizeBytes, 0);
+ return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
+ res.value, res.units));
}
/**
@@ -105,207 +100,88 @@ public final class Formatter {
* (showing fewer digits of precision).
*/
public static String formatShortFileSize(@Nullable Context context, long sizeBytes) {
- return formatFileSize(context, sizeBytes, FLAG_SHORTER);
- }
-
- private static String formatFileSize(@Nullable Context context, long sizeBytes, int flags) {
if (context == null) {
return "";
}
- final RoundedBytesResult res = RoundedBytesResult.roundBytes(sizeBytes, flags);
- return bidiWrap(context, formatRoundedBytesResult(context, res));
- }
-
- private static String getSuffixOverride(@NonNull Resources res, MeasureUnit unit) {
- if (unit == MeasureUnit.BYTE) {
- return res.getString(com.android.internal.R.string.byteShort);
- } else { // unit == PETABYTE
- return res.getString(com.android.internal.R.string.petabyteShort);
- }
- }
-
- private static NumberFormat getNumberFormatter(Locale locale, int fractionDigits) {
- final NumberFormat numberFormatter = NumberFormat.getInstance(locale);
- numberFormatter.setMinimumFractionDigits(fractionDigits);
- numberFormatter.setMaximumFractionDigits(fractionDigits);
- numberFormatter.setGroupingUsed(false);
- if (numberFormatter instanceof DecimalFormat) {
- // We do this only for DecimalFormat, since in the general NumberFormat case, calling
- // setRoundingMode may throw an exception.
- numberFormatter.setRoundingMode(BigDecimal.ROUND_HALF_UP);
- }
- return numberFormatter;
- }
-
- private static String deleteFirstFromString(String source, String toDelete) {
- final int location = source.indexOf(toDelete);
- if (location == -1) {
- return source;
- } else {
- return source.substring(0, location)
- + source.substring(location + toDelete.length(), source.length());
- }
- }
-
- private static String formatMeasureShort(Locale locale, NumberFormat numberFormatter,
- float value, MeasureUnit units) {
- final MeasureFormat measureFormatter = MeasureFormat.getInstance(
- locale, MeasureFormat.FormatWidth.SHORT, numberFormatter);
- return measureFormatter.format(new Measure(value, units));
- }
-
- private static final UnicodeSetSpanner SPACES_AND_CONTROLS =
- new UnicodeSetSpanner(new UnicodeSet("[[:Zs:][:Cf:]]").freeze());
-
- private static String formatRoundedBytesResult(
- @NonNull Context context, @NonNull RoundedBytesResult input) {
- final Locale locale = localeFromContext(context);
- final NumberFormat numberFormatter = getNumberFormatter(locale, input.fractionDigits);
- if (input.units == MeasureUnit.BYTE || input.units == PETABYTE) {
- // ICU spells out "byte" instead of "B", and can't format petabytes yet.
- final String formattedNumber = numberFormatter.format(input.value);
- return context.getString(com.android.internal.R.string.fileSizeSuffix,
- formattedNumber, getSuffixOverride(context.getResources(), input.units));
- } else {
- return formatMeasureShort(locale, numberFormatter, input.value, input.units);
- }
+ final BytesResult res = formatBytes(context.getResources(), sizeBytes, FLAG_SHORTER);
+ return bidiWrap(context, context.getString(com.android.internal.R.string.fileSizeSuffix,
+ res.value, res.units));
}
/** {@hide} */
public static BytesResult formatBytes(Resources res, long sizeBytes, int flags) {
- final RoundedBytesResult rounded = RoundedBytesResult.roundBytes(sizeBytes, flags);
- final Locale locale = res.getConfiguration().getLocales().get(0);
- final NumberFormat numberFormatter = getNumberFormatter(locale, rounded.fractionDigits);
- final String formattedNumber = numberFormatter.format(rounded.value);
- final String units;
- if (rounded.units == MeasureUnit.BYTE || rounded.units == PETABYTE) {
- // ICU spells out "byte" instead of "B", and can't format petabytes yet.
- units = getSuffixOverride(res, rounded.units);
- } else {
- // Since ICU does not give us access to the pattern, we need to extract the unit string
- // from ICU, which we do by taking out the formatted number out of the formatted string
- // and trimming the result of spaces and controls.
- final String formattedMeasure = formatMeasureShort(
- locale, numberFormatter, rounded.value, rounded.units);
- final String numberRemoved = deleteFirstFromString(formattedMeasure, formattedNumber);
- units = SPACES_AND_CONTROLS.trim(numberRemoved).toString();
+ final boolean isNegative = (sizeBytes < 0);
+ float result = isNegative ? -sizeBytes : sizeBytes;
+ int suffix = com.android.internal.R.string.byteShort;
+ long mult = 1;
+ if (result > 900) {
+ suffix = com.android.internal.R.string.kilobyteShort;
+ mult = 1000;
+ result = result / 1000;
}
- return new BytesResult(formattedNumber, units, rounded.roundedBytes);
- }
-
- /**
- * ICU doesn't support PETABYTE yet. Fake it so that we can treat all units the same way.
- */
- private static final MeasureUnit PETABYTE = createPetaByte();
-
- /**
- * Create a petabyte MeasureUnit without registering it with ICU.
- * ICU doesn't support user-create MeasureUnit and the only public (but hidden) method to do so
- * is {@link MeasureUnit#internalGetInstance(String, String)} which also registers the unit as
- * an available type and thus leaks it to code that doesn't expect or support it.
- * <p>This method uses reflection to create an instance of MeasureUnit to avoid leaking it. This
- * instance is <b>only</b> to be used in this class.
- */
- private static MeasureUnit createPetaByte() {
- try {
- Constructor<MeasureUnit> constructor = MeasureUnit.class
- .getDeclaredConstructor(String.class, String.class);
- constructor.setAccessible(true);
- return constructor.newInstance("digital", "petabyte");
- } catch (ReflectiveOperationException e) {
- throw new RuntimeException("Failed to create petabyte MeasureUnit", e);
+ if (result > 900) {
+ suffix = com.android.internal.R.string.megabyteShort;
+ mult *= 1000;
+ result = result / 1000;
}
- }
-
- private static class RoundedBytesResult {
- public final float value;
- public final MeasureUnit units;
- public final int fractionDigits;
- public final long roundedBytes;
-
- private RoundedBytesResult(
- float value, MeasureUnit units, int fractionDigits, long roundedBytes) {
- this.value = value;
- this.units = units;
- this.fractionDigits = fractionDigits;
- this.roundedBytes = roundedBytes;
+ if (result > 900) {
+ suffix = com.android.internal.R.string.gigabyteShort;
+ mult *= 1000;
+ result = result / 1000;
}
-
- /**
- * Returns a RoundedBytesResult object based on the input size in bytes and the rounding
- * flags. The result can be used for formatting.
- */
- static RoundedBytesResult roundBytes(long sizeBytes, int flags) {
- final boolean isNegative = (sizeBytes < 0);
- float result = isNegative ? -sizeBytes : sizeBytes;
- MeasureUnit units = MeasureUnit.BYTE;
- long mult = 1;
- if (result > 900) {
- units = MeasureUnit.KILOBYTE;
- mult = 1000;
- result = result / 1000;
- }
- if (result > 900) {
- units = MeasureUnit.MEGABYTE;
- mult *= 1000;
- result = result / 1000;
- }
- if (result > 900) {
- units = MeasureUnit.GIGABYTE;
- mult *= 1000;
- result = result / 1000;
- }
- if (result > 900) {
- units = MeasureUnit.TERABYTE;
- mult *= 1000;
- result = result / 1000;
- }
- if (result > 900) {
- units = PETABYTE;
- mult *= 1000;
- result = result / 1000;
+ if (result > 900) {
+ suffix = com.android.internal.R.string.terabyteShort;
+ mult *= 1000;
+ result = result / 1000;
+ }
+ if (result > 900) {
+ suffix = com.android.internal.R.string.petabyteShort;
+ mult *= 1000;
+ result = result / 1000;
+ }
+ // Note we calculate the rounded long by ourselves, but still let String.format()
+ // compute the rounded value. String.format("%f", 0.1) might not return "0.1" due to
+ // floating point errors.
+ final int roundFactor;
+ final String roundFormat;
+ if (mult == 1 || result >= 100) {
+ roundFactor = 1;
+ roundFormat = "%.0f";
+ } else if (result < 1) {
+ roundFactor = 100;
+ roundFormat = "%.2f";
+ } else if (result < 10) {
+ if ((flags & FLAG_SHORTER) != 0) {
+ roundFactor = 10;
+ roundFormat = "%.1f";
+ } else {
+ roundFactor = 100;
+ roundFormat = "%.2f";
}
- // Note we calculate the rounded long by ourselves, but still let NumberFormat compute
- // the rounded value. NumberFormat.format(0.1) might not return "0.1" due to floating
- // point errors.
- final int roundFactor;
- final int roundDigits;
- if (mult == 1 || result >= 100) {
+ } else { // 10 <= result < 100
+ if ((flags & FLAG_SHORTER) != 0) {
roundFactor = 1;
- roundDigits = 0;
- } else if (result < 1) {
+ roundFormat = "%.0f";
+ } else {
roundFactor = 100;
- roundDigits = 2;
- } else if (result < 10) {
- if ((flags & FLAG_SHORTER) != 0) {
- roundFactor = 10;
- roundDigits = 1;
- } else {
- roundFactor = 100;
- roundDigits = 2;
- }
- } else { // 10 <= result < 100
- if ((flags & FLAG_SHORTER) != 0) {
- roundFactor = 1;
- roundDigits = 0;
- } else {
- roundFactor = 100;
- roundDigits = 2;
- }
+ roundFormat = "%.2f";
}
+ }
- if (isNegative) {
- result = -result;
- }
+ if (isNegative) {
+ result = -result;
+ }
+ final String roundedString = String.format(roundFormat, result);
- // Note this might overflow if abs(result) >= Long.MAX_VALUE / 100, but that's like
- // 80PB so it's okay (for now)...
- final long roundedBytes =
- (flags & FLAG_CALCULATE_ROUNDED) == 0 ? 0
- : (((long) Math.round(result * roundFactor)) * mult / roundFactor);
+ // Note this might overflow if abs(result) >= Long.MAX_VALUE / 100, but that's like 80PB so
+ // it's okay (for now)...
+ final long roundedBytes =
+ (flags & FLAG_CALCULATE_ROUNDED) == 0 ? 0
+ : (((long) Math.round(result * roundFactor)) * mult / roundFactor);
- return new RoundedBytesResult(result, units, roundDigits, roundedBytes);
- }
+ final String units = res.getString(suffix);
+
+ return new BytesResult(roundedString, units, roundedBytes);
}
/**
diff --git a/android/text/format/Time.java b/android/text/format/Time.java
index bbd9c9c0..562ae7ad 100644
--- a/android/text/format/Time.java
+++ b/android/text/format/Time.java
@@ -358,7 +358,7 @@ public class Time {
}
/**
- * Return the current time in YYYYMMDDTHHMMSS<tz> format
+ * Return the current time in YYYYMMDDTHHMMSS&lt;tz&gt; format
*/
@Override
public String toString() {
@@ -738,6 +738,7 @@ public class Time {
* <p>
* You should also use <tt>toMillis(false)</tt> if you want
* to read back the same milliseconds that you set with {@link #set(long)}
+ * or {@link #set(Time)} or after parsing a date string.
*
* <p>
* This method can return {@code -1} when the date / time fields have been
@@ -745,8 +746,6 @@ public class Time {
* For example, when daylight savings transitions cause an hour to be
* skipped: times within that hour will return {@code -1} if isDst =
* {@code -1}.
- *
- * or {@link #set(Time)} or after parsing a date string.
*/
public long toMillis(boolean ignoreDst) {
calculator.copyFieldsFromTime(this);
diff --git a/android/text/style/AbsoluteSizeSpan.java b/android/text/style/AbsoluteSizeSpan.java
index 908ef55a..3b4eea76 100644
--- a/android/text/style/AbsoluteSizeSpan.java
+++ b/android/text/style/AbsoluteSizeSpan.java
@@ -16,71 +16,105 @@
package android.text.style;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * A span that changes the size of the text it's attached to.
+ * <p>
+ * For example, the size of the text can be changed to 55dp like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with absolute size span");
+ *string.setSpan(new AbsoluteSizeSpan(55, true), 10, 23, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/absolutesizespan.png" />
+ * <figcaption>Text with text size updated.</figcaption>
+ */
public class AbsoluteSizeSpan extends MetricAffectingSpan implements ParcelableSpan {
private final int mSize;
- private boolean mDip;
+ private final boolean mDip;
/**
* Set the text size to <code>size</code> physical pixels.
*/
public AbsoluteSizeSpan(int size) {
- mSize = size;
+ this(size, false);
}
/**
- * Set the text size to <code>size</code> physical pixels,
- * or to <code>size</code> device-independent pixels if
- * <code>dip</code> is true.
+ * Set the text size to <code>size</code> physical pixels, or to <code>size</code>
+ * device-independent pixels if <code>dip</code> is true.
*/
public AbsoluteSizeSpan(int size, boolean dip) {
mSize = size;
mDip = dip;
}
- public AbsoluteSizeSpan(Parcel src) {
+ /**
+ * Creates an {@link AbsoluteSizeSpan} from a parcel.
+ */
+ public AbsoluteSizeSpan(@NonNull Parcel src) {
mSize = src.readInt();
mDip = src.readInt() != 0;
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.ABSOLUTE_SIZE_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
dest.writeInt(mSize);
dest.writeInt(mDip ? 1 : 0);
}
+ /**
+ * Get the text size. This is in physical pixels if {@link #getDip()} returns false or in
+ * device-independent pixels if {@link #getDip()} returns true.
+ *
+ * @return the text size, either in physical pixels or device-independent pixels.
+ * @see AbsoluteSizeSpan#AbsoluteSizeSpan(int, boolean)
+ */
public int getSize() {
return mSize;
}
+ /**
+ * Returns whether the size is in device-independent pixels or not, depending on the
+ * <code>dip</code> flag passed in {@link #AbsoluteSizeSpan(int, boolean)}
+ *
+ * @return <code>true</code> if the size is in device-independent pixels, <code>false</code>
+ * otherwise
+ *
+ * @see #AbsoluteSizeSpan(int, boolean)
+ */
public boolean getDip() {
return mDip;
}
@Override
- public void updateDrawState(TextPaint ds) {
+ public void updateDrawState(@NonNull TextPaint ds) {
if (mDip) {
ds.setTextSize(mSize * ds.density);
} else {
@@ -89,7 +123,7 @@ public class AbsoluteSizeSpan extends MetricAffectingSpan implements ParcelableS
}
@Override
- public void updateMeasureState(TextPaint ds) {
+ public void updateMeasureState(@NonNull TextPaint ds) {
if (mDip) {
ds.setTextSize(mSize * ds.density);
} else {
diff --git a/android/text/style/BackgroundColorSpan.java b/android/text/style/BackgroundColorSpan.java
index de05f50c..44e35615 100644
--- a/android/text/style/BackgroundColorSpan.java
+++ b/android/text/style/BackgroundColorSpan.java
@@ -16,52 +16,88 @@
package android.text.style;
+import android.annotation.ColorInt;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * Changes the background color of the text to which the span is attached.
+ * <p>
+ * For example, to set a green background color for a text you would create a {@link
+ * android.text.SpannableString} based on the text and set the span.
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with a background color span");
+ *string.setSpan(new BackgroundColorSpan(color), 12, 28, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/backgroundcolorspan.png" />
+ * <figcaption>Set a background color for the text.</figcaption>
+ */
public class BackgroundColorSpan extends CharacterStyle
implements UpdateAppearance, ParcelableSpan {
private final int mColor;
- public BackgroundColorSpan(int color) {
+ /**
+ * Creates a {@link BackgroundColorSpan} from a color integer.
+ * <p>
+ *
+ * @param color color integer that defines the background color
+ * @see android.content.res.Resources#getColor(int, Resources.Theme)
+ */
+ public BackgroundColorSpan(@ColorInt int color) {
mColor = color;
}
- public BackgroundColorSpan(Parcel src) {
+ /**
+ * Creates a {@link BackgroundColorSpan} from a parcel.
+ */
+ public BackgroundColorSpan(@NonNull Parcel src) {
mColor = src.readInt();
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.BACKGROUND_COLOR_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
dest.writeInt(mColor);
}
+ /**
+ * @return the background color of this span.
+ * @see BackgroundColorSpan#BackgroundColorSpan(int)
+ */
+ @ColorInt
public int getBackgroundColor() {
return mColor;
}
+ /**
+ * Updates the background color of the TextPaint.
+ */
@Override
- public void updateDrawState(TextPaint ds) {
- ds.bgColor = mColor;
+ public void updateDrawState(@NonNull TextPaint textPaint) {
+ textPaint.bgColor = mColor;
}
}
diff --git a/android/text/style/BulletSpan.java b/android/text/style/BulletSpan.java
index 43dd0ff5..70175c86 100644
--- a/android/text/style/BulletSpan.java
+++ b/android/text/style/BulletSpan.java
@@ -16,6 +16,11 @@
package android.text.style;
+import android.annotation.ColorInt;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Px;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
@@ -26,38 +31,108 @@ import android.text.ParcelableSpan;
import android.text.Spanned;
import android.text.TextUtils;
+/**
+ * A span which styles paragraphs as bullet points (respecting layout direction).
+ * <p>
+ * BulletSpans must be attached from the first character to the last character of a single
+ * paragraph, otherwise the bullet point will not be displayed but the first paragraph encountered
+ * will have a leading margin.
+ * <p>
+ * BulletSpans allow configuring the following elements:
+ * <ul>
+ * <li><b>gap width</b> - the distance, in pixels, between the bullet point and the paragraph.
+ * Default value is 2px.</li>
+ * <li><b>color</b> - the bullet point color. By default, the bullet point color is 0 - no color,
+ * so it uses the TextView's text color.</li>
+ * <li><b>bullet radius</b> - the radius, in pixels, of the bullet point. Default value is
+ * 4px.</li>
+ * </ul>
+ * For example, a BulletSpan using the default values can be constructed like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with\nBullet point");
+ *string.setSpan(new BulletSpan(), 10, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/defaultbulletspan.png" />
+ * <figcaption>BulletSpan constructed with default values.</figcaption>
+ * <p>
+ * <p>
+ * To construct a BulletSpan with a gap width of 40px, green bullet point and bullet radius of
+ * 20px:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with\nBullet point");
+ *string.setSpan(new BulletSpan(40, color, 20), 10, 22, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/custombulletspan.png" />
+ * <figcaption>Customized BulletSpan.</figcaption>
+ */
public class BulletSpan implements LeadingMarginSpan, ParcelableSpan {
- private final int mGapWidth;
- private final boolean mWantColor;
- private final int mColor;
-
// Bullet is slightly bigger to avoid aliasing artifacts on mdpi devices.
- private static final float BULLET_RADIUS = 3 * 1.2f;
- private static Path sBulletPath = null;
+ private static final int STANDARD_BULLET_RADIUS = 4;
public static final int STANDARD_GAP_WIDTH = 2;
+ private static final int STANDARD_COLOR = 0;
+ @Px
+ private final int mGapWidth;
+ @Px
+ private final int mBulletRadius;
+ private Path mBulletPath = null;
+ @ColorInt
+ private final int mColor;
+ private final boolean mWantColor;
+
+ /**
+ * Creates a {@link BulletSpan} with the default values.
+ */
public BulletSpan() {
- mGapWidth = STANDARD_GAP_WIDTH;
- mWantColor = false;
- mColor = 0;
+ this(STANDARD_GAP_WIDTH, STANDARD_COLOR, false, STANDARD_BULLET_RADIUS);
}
+ /**
+ * Creates a {@link BulletSpan} based on a gap width
+ *
+ * @param gapWidth the distance, in pixels, between the bullet point and the paragraph.
+ */
public BulletSpan(int gapWidth) {
- mGapWidth = gapWidth;
- mWantColor = false;
- mColor = 0;
+ this(gapWidth, STANDARD_COLOR, false, STANDARD_BULLET_RADIUS);
}
- public BulletSpan(int gapWidth, int color) {
+ /**
+ * Creates a {@link BulletSpan} based on a gap width and a color integer.
+ *
+ * @param gapWidth the distance, in pixels, between the bullet point and the paragraph.
+ * @param color the bullet point color, as a color integer
+ * @see android.content.res.Resources#getColor(int, Resources.Theme)
+ */
+ public BulletSpan(int gapWidth, @ColorInt int color) {
+ this(gapWidth, color, true, STANDARD_BULLET_RADIUS);
+ }
+
+ /**
+ * Creates a {@link BulletSpan} based on a gap width and a color integer.
+ *
+ * @param gapWidth the distance, in pixels, between the bullet point and the paragraph.
+ * @param color the bullet point color, as a color integer.
+ * @param bulletRadius the radius of the bullet point, in pixels.
+ * @see android.content.res.Resources#getColor(int, Resources.Theme)
+ */
+ public BulletSpan(int gapWidth, @ColorInt int color, @IntRange(from = 0) int bulletRadius) {
+ this(gapWidth, color, true, bulletRadius);
+ }
+
+ private BulletSpan(int gapWidth, @ColorInt int color, boolean wantColor,
+ @IntRange(from = 0) int bulletRadius) {
mGapWidth = gapWidth;
- mWantColor = true;
+ mBulletRadius = bulletRadius;
mColor = color;
+ mWantColor = wantColor;
}
- public BulletSpan(Parcel src) {
+ /**
+ * Creates a {@link BulletSpan} from a parcel.
+ */
+ public BulletSpan(@NonNull Parcel src) {
mGapWidth = src.readInt();
mWantColor = src.readInt() != 0;
mColor = src.readInt();
+ mBulletRadius = src.readInt();
}
@Override
@@ -77,68 +152,97 @@ public class BulletSpan implements LeadingMarginSpan, ParcelableSpan {
}
@Override
- public void writeToParcel(Parcel dest, int flags) {
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
@Override
- public void writeToParcelInternal(Parcel dest, int flags) {
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
dest.writeInt(mGapWidth);
dest.writeInt(mWantColor ? 1 : 0);
dest.writeInt(mColor);
+ dest.writeInt(mBulletRadius);
}
@Override
public int getLeadingMargin(boolean first) {
- return (int) (2 * BULLET_RADIUS + mGapWidth);
+ return 2 * mBulletRadius + mGapWidth;
+ }
+
+ /**
+ * Get the distance, in pixels, between the bullet point and the paragraph.
+ *
+ * @return the distance, in pixels, between the bullet point and the paragraph.
+ */
+ public int getGapWidth() {
+ return mGapWidth;
+ }
+
+ /**
+ * Get the radius, in pixels, of the bullet point.
+ *
+ * @return the radius, in pixels, of the bullet point.
+ */
+ public int getBulletRadius() {
+ return mBulletRadius;
+ }
+
+ /**
+ * Get the bullet point color.
+ *
+ * @return the bullet point color
+ */
+ public int getColor() {
+ return mColor;
}
@Override
- public void drawLeadingMargin(Canvas c, Paint p, int x, int dir,
- int top, int baseline, int bottom,
- CharSequence text, int start, int end,
- boolean first, Layout l) {
+ public void drawLeadingMargin(@NonNull Canvas canvas, @NonNull Paint paint, int x, int dir,
+ int top, int baseline, int bottom,
+ @NonNull CharSequence text, int start, int end,
+ boolean first, @Nullable Layout layout) {
if (((Spanned) text).getSpanStart(this) == start) {
- Paint.Style style = p.getStyle();
+ Paint.Style style = paint.getStyle();
int oldcolor = 0;
if (mWantColor) {
- oldcolor = p.getColor();
- p.setColor(mColor);
+ oldcolor = paint.getColor();
+ paint.setColor(mColor);
}
- p.setStyle(Paint.Style.FILL);
+ paint.setStyle(Paint.Style.FILL);
- if (l != null) {
+ if (layout != null) {
// "bottom" position might include extra space as a result of line spacing
// configuration. Subtract extra space in order to show bullet in the vertical
// center of characters.
- final int line = l.getLineForOffset(start);
- bottom = bottom - l.getLineExtra(line);
+ final int line = layout.getLineForOffset(start);
+ bottom = bottom - layout.getLineExtra(line);
}
- final float y = (top + bottom) / 2f;
+ final float yPosition = (top + bottom) / 2f;
+ final float xPosition = x + dir * mBulletRadius;
- if (c.isHardwareAccelerated()) {
- if (sBulletPath == null) {
- sBulletPath = new Path();
- sBulletPath.addCircle(0.0f, 0.0f, BULLET_RADIUS, Direction.CW);
+ if (canvas.isHardwareAccelerated()) {
+ if (mBulletPath == null) {
+ mBulletPath = new Path();
+ mBulletPath.addCircle(0.0f, 0.0f, mBulletRadius, Direction.CW);
}
- c.save();
- c.translate(x + dir * BULLET_RADIUS, y);
- c.drawPath(sBulletPath, p);
- c.restore();
+ canvas.save();
+ canvas.translate(xPosition, yPosition);
+ canvas.drawPath(mBulletPath, paint);
+ canvas.restore();
} else {
- c.drawCircle(x + dir * BULLET_RADIUS, y, BULLET_RADIUS, p);
+ canvas.drawCircle(xPosition, yPosition, mBulletRadius, paint);
}
if (mWantColor) {
- p.setColor(oldcolor);
+ paint.setColor(oldcolor);
}
- p.setStyle(style);
+ paint.setStyle(style);
}
}
}
diff --git a/android/text/style/ForegroundColorSpan.java b/android/text/style/ForegroundColorSpan.java
index 2bc6d540..f7706745 100644
--- a/android/text/style/ForegroundColorSpan.java
+++ b/android/text/style/ForegroundColorSpan.java
@@ -17,53 +17,88 @@
package android.text.style;
import android.annotation.ColorInt;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * Changes the color of the text to which the span is attached.
+ * <p>
+ * For example, to set a green text color you would create a {@link
+ * android.text.SpannableString} based on the text and set the span.
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with a foreground color span");
+ *string.setSpan(new ForegroundColorSpan(color), 12, 28, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/foregroundcolorspan.png" />
+ * <figcaption>Set a text color.</figcaption>
+ */
public class ForegroundColorSpan extends CharacterStyle
implements UpdateAppearance, ParcelableSpan {
private final int mColor;
+ /**
+ * Creates a {@link ForegroundColorSpan} from a color integer.
+ * <p>
+ * To get the color integer associated with a particular color resource ID, use
+ * {@link android.content.res.Resources#getColor(int, Resources.Theme)}
+ *
+ * @param color color integer that defines the text color
+ */
public ForegroundColorSpan(@ColorInt int color) {
mColor = color;
}
- public ForegroundColorSpan(Parcel src) {
+ /**
+ * Creates a {@link ForegroundColorSpan} from a parcel.
+ */
+ public ForegroundColorSpan(@NonNull Parcel src) {
mColor = src.readInt();
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.FOREGROUND_COLOR_SPAN;
}
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
dest.writeInt(mColor);
}
+ /**
+ * @return the foreground color of this span.
+ * @see ForegroundColorSpan#ForegroundColorSpan(int)
+ */
@ColorInt
public int getForegroundColor() {
return mColor;
}
+ /**
+ * Updates the color of the TextPaint to the foreground color.
+ */
@Override
- public void updateDrawState(TextPaint ds) {
- ds.setColor(mColor);
+ public void updateDrawState(@NonNull TextPaint textPaint) {
+ textPaint.setColor(mColor);
}
}
diff --git a/android/text/style/RelativeSizeSpan.java b/android/text/style/RelativeSizeSpan.java
index 95f048a2..3094f27a 100644
--- a/android/text/style/RelativeSizeSpan.java
+++ b/android/text/style/RelativeSizeSpan.java
@@ -16,56 +16,85 @@
package android.text.style;
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * Uniformly scales the size of the text to which it's attached by a certain proportion.
+ * <p>
+ * For example, a <code>RelativeSizeSpan</code> that increases the text size by 50% can be
+ * constructed like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with relative size span");
+ *string.setSpan(new RelativeSizeSpan(1.5f), 10, 24, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/relativesizespan.png" />
+ * <figcaption>Text increased by 50% with <code>RelativeSizeSpan</code>.</figcaption>
+ */
public class RelativeSizeSpan extends MetricAffectingSpan implements ParcelableSpan {
private final float mProportion;
- public RelativeSizeSpan(float proportion) {
+ /**
+ * Creates a {@link RelativeSizeSpan} based on a proportion.
+ *
+ * @param proportion the proportion with which the text is scaled.
+ */
+ public RelativeSizeSpan(@FloatRange(from = 0) float proportion) {
mProportion = proportion;
}
- public RelativeSizeSpan(Parcel src) {
+ /**
+ * Creates a {@link RelativeSizeSpan} from a parcel.
+ */
+ public RelativeSizeSpan(@NonNull Parcel src) {
mProportion = src.readFloat();
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.RELATIVE_SIZE_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
dest.writeFloat(mProportion);
}
+ /**
+ * @return the proportion with which the text size is changed.
+ */
public float getSizeChange() {
return mProportion;
}
@Override
- public void updateDrawState(TextPaint ds) {
+ public void updateDrawState(@NonNull TextPaint ds) {
ds.setTextSize(ds.getTextSize() * mProportion);
}
@Override
- public void updateMeasureState(TextPaint ds) {
+ public void updateMeasureState(@NonNull TextPaint ds) {
ds.setTextSize(ds.getTextSize() * mProportion);
}
}
diff --git a/android/text/style/ScaleXSpan.java b/android/text/style/ScaleXSpan.java
index d0850185..6ef4cecc 100644
--- a/android/text/style/ScaleXSpan.java
+++ b/android/text/style/ScaleXSpan.java
@@ -16,45 +16,79 @@
package android.text.style;
+import android.annotation.FloatRange;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * Scales horizontally the size of the text to which it's attached by a certain factor.
+ * <p>
+ * Values > 1.0 will stretch the text wider. Values < 1.0 will stretch the text narrower.
+ * <p>
+ * For example, a <code>ScaleXSpan</code> that stretches the text size by 100% can be
+ * constructed like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with ScaleX span");
+ *string.setSpan(new ScaleXSpan(2f), 10, 16, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/scalexspan.png" />
+ * <figcaption>Text scaled by 100% with <code>ScaleXSpan</code>.</figcaption>
+ */
public class ScaleXSpan extends MetricAffectingSpan implements ParcelableSpan {
private final float mProportion;
- public ScaleXSpan(float proportion) {
+ /**
+ * Creates a {@link ScaleXSpan} based on a proportion. Values > 1.0 will stretch the text wider.
+ * Values < 1.0 will stretch the text narrower.
+ *
+ * @param proportion the horizontal scale factor.
+ */
+ public ScaleXSpan(@FloatRange(from = 0) float proportion) {
mProportion = proportion;
}
- public ScaleXSpan(Parcel src) {
+ /**
+ * Creates a {@link ScaleXSpan} from a parcel.
+ */
+ public ScaleXSpan(@NonNull Parcel src) {
mProportion = src.readFloat();
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.SCALE_X_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
dest.writeFloat(mProportion);
}
+ /**
+ * Get the horizontal scale factor for the text.
+ *
+ * @return the horizontal scale factor.
+ */
public float getScaleX() {
return mProportion;
}
diff --git a/android/text/style/StrikethroughSpan.java b/android/text/style/StrikethroughSpan.java
index 1389704f..a6305050 100644
--- a/android/text/style/StrikethroughSpan.java
+++ b/android/text/style/StrikethroughSpan.java
@@ -16,42 +16,65 @@
package android.text.style;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * A span that strikes through the text it's attached to.
+ * <p>
+ * The span can be used like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with strikethrough span");
+ *string.setSpan(new StrikethroughSpan(), 10, 23, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/strikethroughspan.png" />
+ * <figcaption>Strikethrough text.</figcaption>
+ */
public class StrikethroughSpan extends CharacterStyle
implements UpdateAppearance, ParcelableSpan {
+
+ /**
+ * Creates a {@link StrikethroughSpan}.
+ */
public StrikethroughSpan() {
}
-
- public StrikethroughSpan(Parcel src) {
+
+ /**
+ * Creates a {@link StrikethroughSpan} from a parcel.
+ */
+ public StrikethroughSpan(@NonNull Parcel src) {
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.STRIKETHROUGH_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
}
@Override
- public void updateDrawState(TextPaint ds) {
+ public void updateDrawState(@NonNull TextPaint ds) {
ds.setStrikeThruText(true);
}
}
diff --git a/android/text/style/SubscriptSpan.java b/android/text/style/SubscriptSpan.java
index f1b0d38c..3d15aad6 100644
--- a/android/text/style/SubscriptSpan.java
+++ b/android/text/style/SubscriptSpan.java
@@ -16,46 +16,74 @@
package android.text.style;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * The span that moves the position of the text baseline lower.
+ * <p>
+ * The span can be used like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("☕- C8H10N4O2\n");
+ *string.setSpan(new SubscriptSpan(), 4, 5, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ *string.setSpan(new SubscriptSpan(), 6, 8, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ *string.setSpan(new SubscriptSpan(), 9, 10, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+ *string.setSpan(new SubscriptSpan(), 11, 12, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/subscriptspan.png" />
+ * <figcaption>Text with <code>SubscriptSpan</code>.</figcaption>
+ * Note: Since the span affects the position of the text, if the text is on the last line of a
+ * TextView, it may appear cut.
+ */
public class SubscriptSpan extends MetricAffectingSpan implements ParcelableSpan {
+
+ /**
+ * Creates a {@link SubscriptSpan}.
+ */
public SubscriptSpan() {
}
-
- public SubscriptSpan(Parcel src) {
+
+ /**
+ * Creates a {@link SubscriptSpan} from a parcel.
+ */
+ public SubscriptSpan(@NonNull Parcel src) {
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.SUBSCRIPT_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
+ @Override
public void writeToParcel(Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
+ @Override
public void writeToParcelInternal(Parcel dest, int flags) {
}
@Override
- public void updateDrawState(TextPaint tp) {
- tp.baselineShift -= (int) (tp.ascent() / 2);
+ public void updateDrawState(@NonNull TextPaint textPaint) {
+ textPaint.baselineShift -= (int) (textPaint.ascent() / 2);
}
@Override
- public void updateMeasureState(TextPaint tp) {
- tp.baselineShift -= (int) (tp.ascent() / 2);
+ public void updateMeasureState(@NonNull TextPaint textPaint) {
+ textPaint.baselineShift -= (int) (textPaint.ascent() / 2);
}
}
diff --git a/android/text/style/SuperscriptSpan.java b/android/text/style/SuperscriptSpan.java
index abcf688f..3dc9d3fd 100644
--- a/android/text/style/SuperscriptSpan.java
+++ b/android/text/style/SuperscriptSpan.java
@@ -16,46 +16,71 @@
package android.text.style;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * The span that moves the position of the text baseline higher.
+ * <p>
+ * The span can be used like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("1st example");
+ *string.setSpan(new SuperscriptSpan(), 1, 3, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/superscriptspan.png" />
+ * <figcaption>Text with <code>SuperscriptSpan</code>.</figcaption>
+ * Note: Since the span affects the position of the text, if the text is on the first line of a
+ * TextView, it may appear cut. This can be avoided by decreasing the text size with an {@link
+ * AbsoluteSizeSpan}
+ */
public class SuperscriptSpan extends MetricAffectingSpan implements ParcelableSpan {
+ /**
+ * Creates a {@link SuperscriptSpan}.
+ */
public SuperscriptSpan() {
}
-
- public SuperscriptSpan(Parcel src) {
+
+ /**
+ * Creates a {@link SuperscriptSpan} from a parcel.
+ */
+ public SuperscriptSpan(@NonNull Parcel src) {
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.SUPERSCRIPT_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
}
@Override
- public void updateDrawState(TextPaint tp) {
- tp.baselineShift += (int) (tp.ascent() / 2);
+ public void updateDrawState(@NonNull TextPaint textPaint) {
+ textPaint.baselineShift += (int) (textPaint.ascent() / 2);
}
@Override
- public void updateMeasureState(TextPaint tp) {
- tp.baselineShift += (int) (tp.ascent() / 2);
+ public void updateMeasureState(@NonNull TextPaint textPaint) {
+ textPaint.baselineShift += (int) (textPaint.ascent() / 2);
}
}
diff --git a/android/text/style/UnderlineSpan.java b/android/text/style/UnderlineSpan.java
index 9024dcd3..800838ef 100644
--- a/android/text/style/UnderlineSpan.java
+++ b/android/text/style/UnderlineSpan.java
@@ -16,42 +16,65 @@
package android.text.style;
+import android.annotation.NonNull;
import android.os.Parcel;
import android.text.ParcelableSpan;
import android.text.TextPaint;
import android.text.TextUtils;
+/**
+ * A span that underlines the text it's attached to.
+ * <p>
+ * The span can be used like this:
+ * <pre>{@code
+ * SpannableString string = new SpannableString("Text with underline span");
+ *string.setSpan(new UnderlineSpan(), 10, 19, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);}</pre>
+ * <img src="{@docRoot}reference/android/images/text/style/underlinespan.png" />
+ * <figcaption>Underlined text.</figcaption>
+ */
public class UnderlineSpan extends CharacterStyle
implements UpdateAppearance, ParcelableSpan {
+
+ /**
+ * Creates an {@link UnderlineSpan}.
+ */
public UnderlineSpan() {
}
-
- public UnderlineSpan(Parcel src) {
+
+ /**
+ * Creates an {@link UnderlineSpan} from a parcel.
+ */
+ public UnderlineSpan(@NonNull Parcel src) {
}
-
+
+ @Override
public int getSpanTypeId() {
return getSpanTypeIdInternal();
}
/** @hide */
+ @Override
public int getSpanTypeIdInternal() {
return TextUtils.UNDERLINE_SPAN;
}
-
+
+ @Override
public int describeContents() {
return 0;
}
- public void writeToParcel(Parcel dest, int flags) {
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
writeToParcelInternal(dest, flags);
}
/** @hide */
- public void writeToParcelInternal(Parcel dest, int flags) {
+ @Override
+ public void writeToParcelInternal(@NonNull Parcel dest, int flags) {
}
@Override
- public void updateDrawState(TextPaint ds) {
+ public void updateDrawState(@NonNull TextPaint ds) {
ds.setUnderlineText(true);
}
}
diff --git a/android/util/DataUnit.java b/android/util/DataUnit.java
new file mode 100644
index 00000000..ea4266ec
--- /dev/null
+++ b/android/util/DataUnit.java
@@ -0,0 +1,44 @@
+/*
+ * 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 android.util;
+
+import java.time.temporal.ChronoUnit;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A {@code DataUnit} represents data sizes at a given unit of granularity and
+ * provides utility methods to convert across units.
+ * <p>
+ * Note that both SI units (powers of 10) and IEC units (powers of 2) are
+ * supported, and you'll need to pick the correct one for your use-case. For
+ * example, Wikipedia defines a "kilobyte" as an SI unit of 1000 bytes, and a
+ * "kibibyte" as an IEC unit of 1024 bytes.
+ * <p>
+ * This design is mirrored after {@link TimeUnit} and {@link ChronoUnit}.
+ */
+public enum DataUnit {
+ KILOBYTES { @Override public long toBytes(long v) { return v * 1_000; } },
+ MEGABYTES { @Override public long toBytes(long v) { return v * 1_000_000; } },
+ GIGABYTES { @Override public long toBytes(long v) { return v * 1_000_000_000; } },
+ KIBIBYTES { @Override public long toBytes(long v) { return v * 1_024; } },
+ MEBIBYTES { @Override public long toBytes(long v) { return v * 1_048_576; } },
+ GIBIBYTES { @Override public long toBytes(long v) { return v * 1_073_741_824; } };
+
+ public long toBytes(long v) {
+ throw new AbstractMethodError();
+ }
+}
diff --git a/android/util/FeatureFlagUtils.java b/android/util/FeatureFlagUtils.java
index bfb51309..25a177ed 100644
--- a/android/util/FeatureFlagUtils.java
+++ b/android/util/FeatureFlagUtils.java
@@ -38,13 +38,15 @@ public class FeatureFlagUtils {
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_app_info_v2", "true");
DEFAULT_FLAGS.put("settings_connected_device_v2", "true");
- DEFAULT_FLAGS.put("settings_battery_v2", "false");
+ DEFAULT_FLAGS.put("settings_battery_v2", "true");
DEFAULT_FLAGS.put("settings_battery_display_app_list", "false");
- DEFAULT_FLAGS.put("settings_security_settings_v2", "false");
+ DEFAULT_FLAGS.put("settings_security_settings_v2", "true");
+ DEFAULT_FLAGS.put("settings_zone_picker_v2", "true");
+ DEFAULT_FLAGS.put("settings_suggestion_ui_v2", "false");
+ DEFAULT_FLAGS.put("settings_about_phone_v2", "false");
+ DEFAULT_FLAGS.put("settings_bluetooth_while_driving", "false");
}
/**
diff --git a/android/util/KeyValueListParser.java b/android/util/KeyValueListParser.java
index 0a00794a..7eef63ef 100644
--- a/android/util/KeyValueListParser.java
+++ b/android/util/KeyValueListParser.java
@@ -17,6 +17,9 @@ package android.util;
import android.text.TextUtils;
+import java.time.Duration;
+import java.time.format.DateTimeParseException;
+
/**
* Parses a list of key=value pairs, separated by some delimiter, and puts the results in
* an internal Map. Values can be then queried by key, or if not found, a default value
@@ -189,4 +192,24 @@ public class KeyValueListParser {
public String keyAt(int index) {
return mValues.keyAt(index);
}
+
+ /**
+ * {@hide}
+ * Parse a duration in millis based on java.time.Duration or just a number (millis)
+ */
+ public long getDurationMillis(String key, long def) {
+ String value = mValues.get(key);
+ if (value != null) {
+ try {
+ if (value.startsWith("P") || value.startsWith("p")) {
+ return Duration.parse(value).toMillis();
+ } else {
+ return Long.parseLong(value);
+ }
+ } catch (NumberFormatException | DateTimeParseException e) {
+ // fallthrough
+ }
+ }
+ return def;
+ }
}
diff --git a/android/util/MutableBoolean.java b/android/util/MutableBoolean.java
index ed837ab6..44e73cc3 100644
--- a/android/util/MutableBoolean.java
+++ b/android/util/MutableBoolean.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableBoolean {
public boolean value;
diff --git a/android/util/MutableByte.java b/android/util/MutableByte.java
index cc6b00a8..b9ec25da 100644
--- a/android/util/MutableByte.java
+++ b/android/util/MutableByte.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableByte {
public byte value;
diff --git a/android/util/MutableChar.java b/android/util/MutableChar.java
index 9a2e2bce..9f7a9ae8 100644
--- a/android/util/MutableChar.java
+++ b/android/util/MutableChar.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableChar {
public char value;
diff --git a/android/util/MutableDouble.java b/android/util/MutableDouble.java
index bd7329a3..56e539bc 100644
--- a/android/util/MutableDouble.java
+++ b/android/util/MutableDouble.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableDouble {
public double value;
diff --git a/android/util/MutableFloat.java b/android/util/MutableFloat.java
index e6f2d7dc..6d7ad59d 100644
--- a/android/util/MutableFloat.java
+++ b/android/util/MutableFloat.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableFloat {
public float value;
diff --git a/android/util/MutableInt.java b/android/util/MutableInt.java
index a3d8606d..bb245660 100644
--- a/android/util/MutableInt.java
+++ b/android/util/MutableInt.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableInt {
public int value;
diff --git a/android/util/MutableLong.java b/android/util/MutableLong.java
index 575068ea..86e70e1b 100644
--- a/android/util/MutableLong.java
+++ b/android/util/MutableLong.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableLong {
public long value;
diff --git a/android/util/MutableShort.java b/android/util/MutableShort.java
index 48fb232b..b94ab073 100644
--- a/android/util/MutableShort.java
+++ b/android/util/MutableShort.java
@@ -17,7 +17,9 @@
package android.util;
/**
+ * @deprecated This class will be removed from a future version of the Android API.
*/
+@Deprecated
public final class MutableShort {
public short value;
diff --git a/android/util/PackageUtils.java b/android/util/PackageUtils.java
index e2e9d53e..a5e38189 100644
--- a/android/util/PackageUtils.java
+++ b/android/util/PackageUtils.java
@@ -105,7 +105,7 @@ public final class PackageUtils {
* @param data The data.
* @return The digest or null if an error occurs.
*/
- public static @Nullable String computeSha256Digest(@NonNull byte[] data) {
+ public static @Nullable byte[] computeSha256DigestBytes(@NonNull byte[] data) {
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance("SHA256");
@@ -116,6 +116,15 @@ public final class PackageUtils {
messageDigest.update(data);
- return ByteStringUtils.toHexString(messageDigest.digest());
+ return messageDigest.digest();
+ }
+
+ /**
+ * Computes the SHA256 digest of some data.
+ * @param data The data.
+ * @return The digest or null if an error occurs.
+ */
+ public static @Nullable String computeSha256Digest(@NonNull byte[] data) {
+ return ByteStringUtils.toHexString(computeSha256DigestBytes(data));
}
}
diff --git a/android/util/StatsManager.java b/android/util/StatsManager.java
index 26a3c361..687aa837 100644
--- a/android/util/StatsManager.java
+++ b/android/util/StatsManager.java
@@ -17,19 +17,33 @@ package android.util;
import android.Manifest;
import android.annotation.RequiresPermission;
-import android.annotation.SystemApi;
import android.os.IBinder;
import android.os.IStatsManager;
import android.os.RemoteException;
import android.os.ServiceManager;
+
+/*
+ *
+ *
+ *
+ *
+ * THIS ENTIRE FILE IS ONLY TEMPORARY TO PREVENT BREAKAGES OF DEPENDENCIES ON OLD APIS.
+ * The new StatsManager is to be found in android.app.StatsManager.
+ * TODO: Delete this file!
+ *
+ *
+ *
+ *
+ */
+
+
/**
* API for StatsD clients to send configurations and retrieve data.
*
* @hide
*/
-@SystemApi
-public final class StatsManager {
+public class StatsManager {
IStatsManager mService;
private static final String TAG = "StatsManager";
@@ -42,10 +56,20 @@ public final class StatsManager {
}
/**
+ * Temporary to prevent build failures. Will be deleted.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean addConfiguration(String configKey, byte[] config, String pkg, String cls) {
+ // To prevent breakages of dependencies on old API.
+
+ return false;
+ }
+
+ /**
* Clients can send a configuration and simultaneously registers the name of a broadcast
* receiver that listens for when it should request data.
*
- * @param configKey An arbitrary string that allows clients to track the configuration.
+ * @param configKey An arbitrary integer that allows clients to track the configuration.
* @param config Wire-encoded StatsDConfig proto that specifies metrics (and all
* dependencies eg, conditions and matchers).
* @param pkg The package name to receive the broadcast.
@@ -53,7 +77,7 @@ public final class StatsManager {
* @return true if successful
*/
@RequiresPermission(Manifest.permission.DUMP)
- public boolean addConfiguration(String configKey, byte[] config, String pkg, String cls) {
+ public boolean addConfiguration(long configKey, byte[] config, String pkg, String cls) {
synchronized (this) {
try {
IStatsManager service = getIStatsManagerLocked();
@@ -70,13 +94,22 @@ public final class StatsManager {
}
/**
+ * Temporary to prevent build failures. Will be deleted.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public boolean removeConfiguration(String configKey) {
+ // To prevent breakages of old dependencies.
+ return false;
+ }
+
+ /**
* Remove a configuration from logging.
*
* @param configKey Configuration key to remove.
* @return true if successful
*/
@RequiresPermission(Manifest.permission.DUMP)
- public boolean removeConfiguration(String configKey) {
+ public boolean removeConfiguration(long configKey) {
synchronized (this) {
try {
IStatsManager service = getIStatsManagerLocked();
@@ -93,6 +126,16 @@ public final class StatsManager {
}
/**
+ * Temporary to prevent build failures. Will be deleted.
+ */
+ @RequiresPermission(Manifest.permission.DUMP)
+ public byte[] getData(String configKey) {
+ // TODO: remove this and all other methods with String-based config keys.
+ // To prevent build breakages of dependencies.
+ return null;
+ }
+
+ /**
* Clients can request data with a binder call. This getter is destructive and also clears
* the retrieved metrics from statsd memory.
*
@@ -100,7 +143,7 @@ public final class StatsManager {
* @return Serialized ConfigMetricsReportList proto. Returns null on failure.
*/
@RequiresPermission(Manifest.permission.DUMP)
- public byte[] getData(String configKey) {
+ public byte[] getData(long configKey) {
synchronized (this) {
try {
IStatsManager service = getIStatsManagerLocked();
diff --git a/android/util/TimeUtils.java b/android/util/TimeUtils.java
index cc4a0b60..84ae20b9 100644
--- a/android/util/TimeUtils.java
+++ b/android/util/TimeUtils.java
@@ -18,30 +18,18 @@ package android.util;
import android.os.SystemClock;
+import libcore.util.TimeZoneFinder;
+import libcore.util.ZoneInfoDB;
+
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
-import java.util.ArrayList;
import java.util.Calendar;
-import java.util.Collection;
-import java.util.Collections;
import java.util.Date;
-import java.util.List;
-import libcore.util.TimeZoneFinder;
-import libcore.util.ZoneInfoDB;
-
/**
* A class containing utility methods related to time zones.
*/
public class TimeUtils {
/** @hide */ public TimeUtils() {}
- private static final boolean DBG = false;
- private static final String TAG = "TimeUtils";
-
- /** Cached results of getTimeZonesWithUniqueOffsets */
- private static final Object sLastUniqueLockObj = new Object();
- private static List<String> sLastUniqueZoneOffsets = null;
- private static String sLastUniqueCountry = null;
-
/** {@hide} */
private static SimpleDateFormat sLoggingFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@@ -76,86 +64,6 @@ public class TimeUtils {
}
/**
- * Returns an immutable list of unique time zone IDs for the country.
- *
- * @param country to find
- * @return unmodifiable list of unique time zones, maybe empty but never null.
- * @hide
- */
- public static List<String> getTimeZoneIdsWithUniqueOffsets(String country) {
- synchronized(sLastUniqueLockObj) {
- if ((country != null) && country.equals(sLastUniqueCountry)) {
- if (DBG) {
- Log.d(TAG, "getTimeZonesWithUniqueOffsets(" +
- country + "): return cached version");
- }
- return sLastUniqueZoneOffsets;
- }
- }
-
- Collection<android.icu.util.TimeZone> zones = getIcuTimeZones(country);
- ArrayList<android.icu.util.TimeZone> uniqueTimeZones = new ArrayList<>();
- for (android.icu.util.TimeZone zone : zones) {
- // See if we already have this offset,
- // Using slow but space efficient and these are small.
- boolean found = false;
- for (int i = 0; i < uniqueTimeZones.size(); i++) {
- if (uniqueTimeZones.get(i).getRawOffset() == zone.getRawOffset()) {
- found = true;
- break;
- }
- }
- if (!found) {
- if (DBG) {
- Log.d(TAG, "getTimeZonesWithUniqueOffsets: add unique offset=" +
- zone.getRawOffset() + " zone.getID=" + zone.getID());
- }
- uniqueTimeZones.add(zone);
- }
- }
-
- synchronized(sLastUniqueLockObj) {
- // Cache the last result
- sLastUniqueZoneOffsets = extractZoneIds(uniqueTimeZones);
- sLastUniqueCountry = country;
-
- return sLastUniqueZoneOffsets;
- }
- }
-
- private static List<String> extractZoneIds(List<android.icu.util.TimeZone> timeZones) {
- List<String> ids = new ArrayList<>(timeZones.size());
- for (android.icu.util.TimeZone timeZone : timeZones) {
- ids.add(timeZone.getID());
- }
- return Collections.unmodifiableList(ids);
- }
-
- /**
- * Returns an immutable list of frozen ICU time zones for the country.
- *
- * @param countryIso is a two character country code.
- * @return TimeZone list, maybe empty but never null.
- * @hide
- */
- private static List<android.icu.util.TimeZone> getIcuTimeZones(String countryIso) {
- if (countryIso == null) {
- if (DBG) Log.d(TAG, "getIcuTimeZones(null): return empty list");
- return Collections.emptyList();
- }
- List<android.icu.util.TimeZone> timeZones =
- TimeZoneFinder.getInstance().lookupTimeZonesByCountry(countryIso);
- if (timeZones == null) {
- if (DBG) {
- Log.d(TAG, "getIcuTimeZones(" + countryIso
- + "): returned null, converting to empty list");
- }
- return Collections.emptyList();
- }
- return timeZones;
- }
-
- /**
* Returns a String indicating the version of the time zone database currently
* in use. The format of the string is dependent on the underlying time zone
* database implementation, but will typically contain the year in which the database
diff --git a/android/util/apk/ApkSignatureSchemeV2Verifier.java b/android/util/apk/ApkSignatureSchemeV2Verifier.java
index 530937e7..5a09dab5 100644
--- a/android/util/apk/ApkSignatureSchemeV2Verifier.java
+++ b/android/util/apk/ApkSignatureSchemeV2Verifier.java
@@ -16,6 +16,7 @@
package android.util.apk;
+import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
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;
@@ -23,6 +24,9 @@ import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PKCS1_V1_5_WIT
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.SIGNATURE_VERITY_DSA_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_VERITY_ECDSA_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256;
import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getContentDigestAlgorithmJcaDigestAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
@@ -35,11 +39,11 @@ 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.DigestException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
@@ -103,7 +107,8 @@ public class ApkSignatureSchemeV2Verifier {
*/
public static X509Certificate[][] verify(String apkFile)
throws SignatureNotFoundException, SecurityException, IOException {
- return verify(apkFile, true);
+ VerifiedSigner vSigner = verify(apkFile, true);
+ return vSigner.certs;
}
/**
@@ -117,10 +122,11 @@ public class ApkSignatureSchemeV2Verifier {
*/
public static X509Certificate[][] plsCertsNoVerifyOnlyCerts(String apkFile)
throws SignatureNotFoundException, SecurityException, IOException {
- return verify(apkFile, false);
+ VerifiedSigner vSigner = verify(apkFile, false);
+ return vSigner.certs;
}
- private static X509Certificate[][] verify(String apkFile, boolean verifyIntegrity)
+ private static VerifiedSigner verify(String apkFile, boolean verifyIntegrity)
throws SignatureNotFoundException, SecurityException, IOException {
try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) {
return verify(apk, verifyIntegrity);
@@ -136,10 +142,10 @@ public class ApkSignatureSchemeV2Verifier {
* verify.
* @throws IOException if an I/O error occurs while reading the APK file.
*/
- private static X509Certificate[][] verify(RandomAccessFile apk, boolean verifyIntegrity)
+ private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity)
throws SignatureNotFoundException, SecurityException, IOException {
SignatureInfo signatureInfo = findSignature(apk);
- return verify(apk.getFD(), signatureInfo, verifyIntegrity);
+ return verify(apk, signatureInfo, verifyIntegrity);
}
/**
@@ -161,10 +167,10 @@ public class ApkSignatureSchemeV2Verifier {
* @param signatureInfo APK Signature Scheme v2 Block and information relevant for verifying it
* against the APK file.
*/
- private static X509Certificate[][] verify(
- FileDescriptor apkFileDescriptor,
+ private static VerifiedSigner verify(
+ RandomAccessFile apk,
SignatureInfo signatureInfo,
- boolean doVerifyIntegrity) throws SecurityException {
+ boolean doVerifyIntegrity) throws SecurityException, IOException {
int signerCount = 0;
Map<Integer, byte[]> contentDigests = new ArrayMap<>();
List<X509Certificate[]> signerCerts = new ArrayList<>();
@@ -202,16 +208,17 @@ public class ApkSignatureSchemeV2Verifier {
}
if (doVerifyIntegrity) {
- ApkSigningBlockUtils.verifyIntegrity(
- contentDigests,
- apkFileDescriptor,
- signatureInfo.apkSigningBlockOffset,
- signatureInfo.centralDirOffset,
- signatureInfo.eocdOffset,
- signatureInfo.eocd);
+ ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo);
}
- return signerCerts.toArray(new X509Certificate[signerCerts.size()][]);
+ byte[] verityRootHash = null;
+ if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
+ verityRootHash = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256);
+ }
+
+ return new VerifiedSigner(
+ signerCerts.toArray(new X509Certificate[signerCerts.size()][]),
+ verityRootHash);
}
private static X509Certificate[] verifySigner(
@@ -386,6 +393,25 @@ public class ApkSignatureSchemeV2Verifier {
}
return;
}
+
+ static byte[] getVerityRootHash(String apkPath)
+ throws IOException, SignatureNotFoundException, SecurityException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ VerifiedSigner vSigner = verify(apk, false);
+ return vSigner.verityRootHash;
+ }
+ }
+
+ static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)
+ throws IOException, SignatureNotFoundException, SecurityException, DigestException,
+ NoSuchAlgorithmException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ return ApkSigningBlockUtils.generateApkVerity(apkPath, bufferFactory, signatureInfo);
+ }
+ }
+
private static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) {
switch (sigAlgorithm) {
case SIGNATURE_RSA_PSS_WITH_SHA256:
@@ -395,9 +421,28 @@ public class ApkSignatureSchemeV2Verifier {
case SIGNATURE_ECDSA_WITH_SHA256:
case SIGNATURE_ECDSA_WITH_SHA512:
case SIGNATURE_DSA_WITH_SHA256:
+ case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
return true;
default:
return false;
}
}
+
+ /**
+ * Verified APK Signature Scheme v2 signer.
+ *
+ * @hide for internal use only.
+ */
+ public static class VerifiedSigner {
+ public final X509Certificate[][] certs;
+ public final byte[] verityRootHash;
+
+ public VerifiedSigner(X509Certificate[][] certs, byte[] verityRootHash) {
+ this.certs = certs;
+ this.verityRootHash = verityRootHash;
+ }
+
+ }
}
diff --git a/android/util/apk/ApkSignatureSchemeV3Verifier.java b/android/util/apk/ApkSignatureSchemeV3Verifier.java
index e43dee35..1b04eb2f 100644
--- a/android/util/apk/ApkSignatureSchemeV3Verifier.java
+++ b/android/util/apk/ApkSignatureSchemeV3Verifier.java
@@ -16,6 +16,7 @@
package android.util.apk;
+import static android.util.apk.ApkSigningBlockUtils.CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
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;
@@ -23,6 +24,9 @@ import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_RSA_PKCS1_V1_5_WIT
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.SIGNATURE_VERITY_DSA_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_VERITY_ECDSA_WITH_SHA256;
+import static android.util.apk.ApkSigningBlockUtils.SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256;
import static android.util.apk.ApkSigningBlockUtils.compareSignatureAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getContentDigestAlgorithmJcaDigestAlgorithm;
import static android.util.apk.ApkSigningBlockUtils.getLengthPrefixedSlice;
@@ -36,11 +40,11 @@ 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.DigestException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
@@ -136,7 +140,7 @@ public class ApkSignatureSchemeV3Verifier {
private static VerifiedSigner verify(RandomAccessFile apk, boolean verifyIntegrity)
throws SignatureNotFoundException, SecurityException, IOException {
SignatureInfo signatureInfo = findSignature(apk);
- return verify(apk.getFD(), signatureInfo, verifyIntegrity);
+ return verify(apk, signatureInfo, verifyIntegrity);
}
/**
@@ -159,7 +163,7 @@ public class ApkSignatureSchemeV3Verifier {
* against the APK file.
*/
private static VerifiedSigner verify(
- FileDescriptor apkFileDescriptor,
+ RandomAccessFile apk,
SignatureInfo signatureInfo,
boolean doVerifyIntegrity) throws SecurityException {
int signerCount = 0;
@@ -206,13 +210,11 @@ public class ApkSignatureSchemeV3Verifier {
}
if (doVerifyIntegrity) {
- ApkSigningBlockUtils.verifyIntegrity(
- contentDigests,
- apkFileDescriptor,
- signatureInfo.apkSigningBlockOffset,
- signatureInfo.centralDirOffset,
- signatureInfo.eocdOffset,
- signatureInfo.eocd);
+ ApkSigningBlockUtils.verifyIntegrity(contentDigests, apk, signatureInfo);
+ }
+
+ if (contentDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
+ result.verityRootHash = contentDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256);
}
return result;
@@ -503,6 +505,24 @@ public class ApkSignatureSchemeV3Verifier {
return new VerifiedProofOfRotation(certs, flagsList);
}
+ static byte[] getVerityRootHash(String apkPath)
+ throws IOException, SignatureNotFoundException, SecurityException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ VerifiedSigner vSigner = verify(apk, false);
+ return vSigner.verityRootHash;
+ }
+ }
+
+ static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)
+ throws IOException, SignatureNotFoundException, SecurityException, DigestException,
+ NoSuchAlgorithmException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ SignatureInfo signatureInfo = findSignature(apk);
+ return ApkSigningBlockUtils.generateApkVerity(apkPath, bufferFactory, signatureInfo);
+ }
+ }
+
private static boolean isSupportedSignatureAlgorithm(int sigAlgorithm) {
switch (sigAlgorithm) {
case SIGNATURE_RSA_PSS_WITH_SHA256:
@@ -512,6 +532,9 @@ public class ApkSignatureSchemeV3Verifier {
case SIGNATURE_ECDSA_WITH_SHA256:
case SIGNATURE_ECDSA_WITH_SHA512:
case SIGNATURE_DSA_WITH_SHA256:
+ case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
return true;
default:
return false;
@@ -542,6 +565,8 @@ public class ApkSignatureSchemeV3Verifier {
public final X509Certificate[] certs;
public final VerifiedProofOfRotation por;
+ public byte[] verityRootHash;
+
public VerifiedSigner(X509Certificate[] certs, VerifiedProofOfRotation por) {
this.certs = certs;
this.por = por;
diff --git a/android/util/apk/ApkSignatureVerifier.java b/android/util/apk/ApkSignatureVerifier.java
index 81467292..87943725 100644
--- a/android/util/apk/ApkSignatureVerifier.java
+++ b/android/util/apk/ApkSignatureVerifier.java
@@ -25,6 +25,7 @@ import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
import android.content.pm.PackageParser;
import android.content.pm.PackageParser.PackageParserException;
+import android.content.pm.PackageParser.SigningDetails.SignatureSchemeVersion;
import android.content.pm.Signature;
import android.os.Trace;
import android.util.jar.StrictJarFile;
@@ -35,7 +36,9 @@ import libcore.io.IoUtils;
import java.io.IOException;
import java.io.InputStream;
+import java.security.DigestException;
import java.security.GeneralSecurityException;
+import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.ArrayList;
@@ -52,10 +55,6 @@ import java.util.zip.ZipEntry;
*/
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<>();
/**
@@ -63,10 +62,11 @@ public class ApkSignatureVerifier {
*
* @throws PackageParserException if the APK's signature failed to verify.
*/
- public static Result verify(String apkPath, int minSignatureSchemeVersion)
+ public static PackageParser.SigningDetails verify(String apkPath,
+ @SignatureSchemeVersion int minSignatureSchemeVersion)
throws PackageParserException {
- if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V3) {
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_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
@@ -80,10 +80,23 @@ public class ApkSignatureVerifier {
ApkSignatureSchemeV3Verifier.verify(apkPath);
Certificate[][] signerCerts = new Certificate[][] { vSigner.certs };
Signature[] signerSigs = convertToSignatures(signerCerts);
- return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V3);
+ Signature[] pastSignerSigs = null;
+ int[] pastSignerSigsFlags = null;
+ if (vSigner.por != null) {
+ // populate proof-of-rotation information
+ pastSignerSigs = new Signature[vSigner.por.certs.size()];
+ pastSignerSigsFlags = new int[vSigner.por.flagsList.size()];
+ for (int i = 0; i < pastSignerSigs.length; i++) {
+ pastSignerSigs[i] = new Signature(vSigner.por.certs.get(i).getEncoded());
+ pastSignerSigsFlags[i] = vSigner.por.flagsList.get(i);
+ }
+ }
+ return new PackageParser.SigningDetails(
+ signerSigs, SignatureSchemeVersion.SIGNING_BLOCK_V3,
+ pastSignerSigs, pastSignerSigsFlags);
} catch (SignatureNotFoundException e) {
- // not signed with v2, try older if allowed
- if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V3) {
+ // not signed with v3, try older if allowed
+ if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V3) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No APK Signature Scheme v3 signature in package " + apkPath, e);
}
@@ -91,13 +104,13 @@ public class ApkSignatureVerifier {
// 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);
+ + " using APK Signature Scheme v3", e);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
// redundant, protective version check
- if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V2) {
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_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
@@ -110,10 +123,11 @@ public class ApkSignatureVerifier {
Certificate[][] signerCerts = ApkSignatureSchemeV2Verifier.verify(apkPath);
Signature[] signerSigs = convertToSignatures(signerCerts);
- return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V2);
+ return new PackageParser.SigningDetails(
+ signerSigs, SignatureSchemeVersion.SIGNING_BLOCK_V2);
} catch (SignatureNotFoundException e) {
// not signed with v2, try older if allowed
- if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V2) {
+ if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V2) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No APK Signature Scheme v2 signature in package " + apkPath, e);
}
@@ -127,7 +141,7 @@ public class ApkSignatureVerifier {
}
// redundant, protective version check
- if (minSignatureSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME) {
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.JAR) {
// 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
@@ -145,7 +159,8 @@ public class ApkSignatureVerifier {
*
* @throws PackageParserException if there was a problem collecting certificates
*/
- private static Result verifyV1Signature(String apkPath, boolean verifyFull)
+ private static PackageParser.SigningDetails verifyV1Signature(
+ String apkPath, boolean verifyFull)
throws PackageParserException {
StrictJarFile jarFile = null;
@@ -211,7 +226,7 @@ public class ApkSignatureVerifier {
}
}
}
- return new Result(lastCerts, lastSigs, VERSION_JAR_SIGNATURE_SCHEME);
+ return new PackageParser.SigningDetails(lastSigs, SignatureSchemeVersion.JAR);
} catch (GeneralSecurityException e) {
throw new PackageParserException(INSTALL_PARSE_FAILED_CERTIFICATE_ENCODING,
"Failed to collect certificates from " + apkPath, e);
@@ -289,10 +304,11 @@ public class ApkSignatureVerifier {
* @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)
+ public static PackageParser.SigningDetails plsCertsNoVerifyOnlyCerts(
+ String apkPath, int minSignatureSchemeVersion)
throws PackageParserException {
- if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V3) {
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_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
@@ -300,30 +316,43 @@ public class ApkSignatureVerifier {
}
// first try v3
- Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "verifyV3");
+ Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "certsOnlyV3");
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);
+ Signature[] pastSignerSigs = null;
+ int[] pastSignerSigsFlags = null;
+ if (vSigner.por != null) {
+ // populate proof-of-rotation information
+ pastSignerSigs = new Signature[vSigner.por.certs.size()];
+ pastSignerSigsFlags = new int[vSigner.por.flagsList.size()];
+ for (int i = 0; i < pastSignerSigs.length; i++) {
+ pastSignerSigs[i] = new Signature(vSigner.por.certs.get(i).getEncoded());
+ pastSignerSigsFlags[i] = vSigner.por.flagsList.get(i);
+ }
+ }
+ return new PackageParser.SigningDetails(
+ signerSigs, SignatureSchemeVersion.SIGNING_BLOCK_V3,
+ pastSignerSigs, pastSignerSigsFlags);
} catch (SignatureNotFoundException e) {
- // not signed with v2, try older if allowed
- if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V3) {
+ // not signed with v3, try older if allowed
+ if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_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
+ // APK Signature Scheme v3 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);
+ + " using APK Signature Scheme v3", e);
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
// redundant, protective version check
- if (minSignatureSchemeVersion > VERSION_APK_SIGNATURE_SCHEME_V2) {
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.SIGNING_BLOCK_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
@@ -336,10 +365,11 @@ public class ApkSignatureVerifier {
Certificate[][] signerCerts =
ApkSignatureSchemeV2Verifier.plsCertsNoVerifyOnlyCerts(apkPath);
Signature[] signerSigs = convertToSignatures(signerCerts);
- return new Result(signerCerts, signerSigs, VERSION_APK_SIGNATURE_SCHEME_V2);
+ return new PackageParser.SigningDetails(signerSigs,
+ SignatureSchemeVersion.SIGNING_BLOCK_V2);
} catch (SignatureNotFoundException e) {
// not signed with v2, try older if allowed
- if (minSignatureSchemeVersion >= VERSION_APK_SIGNATURE_SCHEME_V2) {
+ if (minSignatureSchemeVersion >= SignatureSchemeVersion.SIGNING_BLOCK_V2) {
throw new PackageParserException(INSTALL_PARSE_FAILED_NO_CERTIFICATES,
"No APK Signature Scheme v2 signature in package " + apkPath, e);
}
@@ -353,7 +383,7 @@ public class ApkSignatureVerifier {
}
// redundant, protective version check
- if (minSignatureSchemeVersion > VERSION_JAR_SIGNATURE_SCHEME) {
+ if (minSignatureSchemeVersion > SignatureSchemeVersion.JAR) {
// 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
@@ -365,6 +395,38 @@ public class ApkSignatureVerifier {
}
/**
+ * @return the verity root hash in the Signing Block.
+ */
+ public static byte[] getVerityRootHash(String apkPath)
+ throws IOException, SignatureNotFoundException, SecurityException {
+ // first try v3
+ try {
+ return ApkSignatureSchemeV3Verifier.getVerityRootHash(apkPath);
+ } catch (SignatureNotFoundException e) {
+ // try older version
+ }
+ return ApkSignatureSchemeV2Verifier.getVerityRootHash(apkPath);
+ }
+
+ /**
+ * Generates the Merkle tree and verity metadata to the buffer allocated by the {@code
+ * ByteBufferFactory}.
+ *
+ * @return the verity root hash of the generated Merkle tree.
+ */
+ public static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory)
+ throws IOException, SignatureNotFoundException, SecurityException, DigestException,
+ NoSuchAlgorithmException {
+ // first try v3
+ try {
+ return ApkSignatureSchemeV3Verifier.generateApkVerity(apkPath, bufferFactory);
+ } catch (SignatureNotFoundException e) {
+ // try older version
+ }
+ return ApkSignatureSchemeV2Verifier.generateApkVerity(apkPath, bufferFactory);
+ }
+
+ /**
* Result of a successful APK verification operation.
*/
public static class Result {
diff --git a/android/util/apk/ApkSigningBlockUtils.java b/android/util/apk/ApkSigningBlockUtils.java
index 9279510a..4146f6fa 100644
--- a/android/util/apk/ApkSigningBlockUtils.java
+++ b/android/util/apk/ApkSigningBlockUtils.java
@@ -16,6 +16,7 @@
package android.util.apk;
+import android.util.ArrayMap;
import android.util.Pair;
import java.io.FileDescriptor;
@@ -30,6 +31,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
+import java.util.Arrays;
import java.util.Map;
/**
@@ -84,16 +86,41 @@ final class ApkSigningBlockUtils {
static void verifyIntegrity(
Map<Integer, byte[]> expectedDigests,
- FileDescriptor apkFileDescriptor,
- long apkSigningBlockOffset,
- long centralDirOffset,
- long eocdOffset,
- ByteBuffer eocdBuf) throws SecurityException {
-
+ RandomAccessFile apk,
+ SignatureInfo signatureInfo) throws SecurityException {
if (expectedDigests.isEmpty()) {
throw new SecurityException("No digests provided");
}
+ Map<Integer, byte[]> expected1MbChunkDigests = new ArrayMap<>();
+ if (expectedDigests.containsKey(CONTENT_DIGEST_CHUNKED_SHA256)) {
+ expected1MbChunkDigests.put(CONTENT_DIGEST_CHUNKED_SHA256,
+ expectedDigests.get(CONTENT_DIGEST_CHUNKED_SHA256));
+ }
+ if (expectedDigests.containsKey(CONTENT_DIGEST_CHUNKED_SHA512)) {
+ expected1MbChunkDigests.put(CONTENT_DIGEST_CHUNKED_SHA512,
+ expectedDigests.get(CONTENT_DIGEST_CHUNKED_SHA512));
+ }
+
+ if (expectedDigests.containsKey(CONTENT_DIGEST_VERITY_CHUNKED_SHA256)) {
+ verifyIntegrityForVerityBasedAlgorithm(
+ expectedDigests.get(CONTENT_DIGEST_VERITY_CHUNKED_SHA256), apk, signatureInfo);
+ } else if (!expected1MbChunkDigests.isEmpty()) {
+ try {
+ verifyIntegrityFor1MbChunkBasedAlgorithm(expected1MbChunkDigests, apk.getFD(),
+ signatureInfo);
+ } catch (IOException e) {
+ throw new SecurityException("Cannot get FD", e);
+ }
+ } else {
+ throw new SecurityException("No known digest exists for integrity check");
+ }
+ }
+
+ private static void verifyIntegrityFor1MbChunkBasedAlgorithm(
+ Map<Integer, byte[]> expectedDigests,
+ FileDescriptor apkFileDescriptor,
+ SignatureInfo signatureInfo) throws SecurityException {
// 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.
@@ -105,16 +132,18 @@ final class ApkSigningBlockUtils {
// 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);
+ new MemoryMappedFileDataSource(apkFileDescriptor, 0,
+ signatureInfo.apkSigningBlockOffset);
DataSource centralDir =
new MemoryMappedFileDataSource(
- apkFileDescriptor, centralDirOffset, eocdOffset - centralDirOffset);
+ apkFileDescriptor, signatureInfo.centralDirOffset,
+ signatureInfo.eocdOffset - signatureInfo.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();
+ ByteBuffer eocdBuf = signatureInfo.eocd.duplicate();
eocdBuf.order(ByteOrder.LITTLE_ENDIAN);
- ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, apkSigningBlockOffset);
+ ZipUtils.setZipEocdCentralDirectoryOffset(eocdBuf, signatureInfo.apkSigningBlockOffset);
DataSource eocd = new ByteBufferDataSource(eocdBuf);
int[] digestAlgorithms = new int[expectedDigests.size()];
@@ -126,7 +155,7 @@ final class ApkSigningBlockUtils {
byte[][] actualDigests;
try {
actualDigests =
- computeContentDigests(
+ computeContentDigestsPer1MbChunk(
digestAlgorithms,
new DataSource[] {beforeApkSigningBlock, centralDir, eocd});
} catch (DigestException e) {
@@ -144,7 +173,7 @@ final class ApkSigningBlockUtils {
}
}
- private static byte[][] computeContentDigests(
+ private static byte[][] computeContentDigestsPer1MbChunk(
int[] digestAlgorithms,
DataSource[] contents) throws DigestException {
// For each digest algorithm the result is computed as follows:
@@ -256,6 +285,46 @@ final class ApkSigningBlockUtils {
return result;
}
+ private static void verifyIntegrityForVerityBasedAlgorithm(
+ byte[] expectedRootHash,
+ RandomAccessFile apk,
+ SignatureInfo signatureInfo) throws SecurityException {
+ try {
+ ApkVerityBuilder.ApkVerityResult verity = ApkVerityBuilder.generateApkVerity(apk,
+ signatureInfo, new ByteBufferFactory() {
+ @Override
+ public ByteBuffer create(int capacity) {
+ return ByteBuffer.allocate(capacity);
+ }
+ });
+ if (!Arrays.equals(expectedRootHash, verity.rootHash)) {
+ throw new SecurityException("APK verity digest of contents did not verify");
+ }
+ } catch (DigestException | IOException | NoSuchAlgorithmException e) {
+ throw new SecurityException("Error during verification", e);
+ }
+ }
+
+ /**
+ * Generates the fsverity header and hash tree to be used by kernel for the given apk. This
+ * method does not check whether the root hash exists in the Signing Block or not.
+ *
+ * <p>The output is stored in the {@link ByteBuffer} created by the given {@link
+ * ByteBufferFactory}.
+ *
+ * @return the root hash of the generated hash tree.
+ */
+ public static byte[] generateApkVerity(String apkPath, ByteBufferFactory bufferFactory,
+ SignatureInfo signatureInfo)
+ throws IOException, SignatureNotFoundException, SecurityException, DigestException,
+ NoSuchAlgorithmException {
+ try (RandomAccessFile apk = new RandomAccessFile(apkPath, "r")) {
+ ApkVerityBuilder.ApkVerityResult result = ApkVerityBuilder.generateApkVerity(apk,
+ signatureInfo, bufferFactory);
+ return result.rootHash;
+ }
+ }
+
/**
* Returns the ZIP End of Central Directory (EoCD) and its offset in the file.
*
@@ -304,9 +373,13 @@ final class ApkSigningBlockUtils {
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 SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256 = 0x0401;
+ static final int SIGNATURE_VERITY_ECDSA_WITH_SHA256 = 0x0403;
+ static final int SIGNATURE_VERITY_DSA_WITH_SHA256 = 0x0405;
static final int CONTENT_DIGEST_CHUNKED_SHA256 = 1;
static final int CONTENT_DIGEST_CHUNKED_SHA512 = 2;
+ static final int CONTENT_DIGEST_VERITY_CHUNKED_SHA256 = 3;
static int compareSignatureAlgorithm(int sigAlgorithm1, int sigAlgorithm2) {
int digestAlgorithm1 = getSignatureAlgorithmContentDigestAlgorithm(sigAlgorithm1);
@@ -321,6 +394,7 @@ final class ApkSigningBlockUtils {
case CONTENT_DIGEST_CHUNKED_SHA256:
return 0;
case CONTENT_DIGEST_CHUNKED_SHA512:
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
return -1;
default:
throw new IllegalArgumentException(
@@ -329,6 +403,7 @@ final class ApkSigningBlockUtils {
case CONTENT_DIGEST_CHUNKED_SHA512:
switch (digestAlgorithm2) {
case CONTENT_DIGEST_CHUNKED_SHA256:
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
return 1;
case CONTENT_DIGEST_CHUNKED_SHA512:
return 0;
@@ -336,6 +411,18 @@ final class ApkSigningBlockUtils {
throw new IllegalArgumentException(
"Unknown digestAlgorithm2: " + digestAlgorithm2);
}
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
+ switch (digestAlgorithm2) {
+ case CONTENT_DIGEST_CHUNKED_SHA512:
+ return -1;
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
+ return 0;
+ case CONTENT_DIGEST_CHUNKED_SHA256:
+ return 1;
+ default:
+ throw new IllegalArgumentException(
+ "Unknown digestAlgorithm2: " + digestAlgorithm2);
+ }
default:
throw new IllegalArgumentException("Unknown digestAlgorithm1: " + digestAlgorithm1);
}
@@ -352,6 +439,10 @@ final class ApkSigningBlockUtils {
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
case SIGNATURE_ECDSA_WITH_SHA512:
return CONTENT_DIGEST_CHUNKED_SHA512;
+ case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
+ return CONTENT_DIGEST_VERITY_CHUNKED_SHA256;
default:
throw new IllegalArgumentException(
"Unknown signature algorithm: 0x"
@@ -362,6 +453,7 @@ final class ApkSigningBlockUtils {
static String getContentDigestAlgorithmJcaDigestAlgorithm(int digestAlgorithm) {
switch (digestAlgorithm) {
case CONTENT_DIGEST_CHUNKED_SHA256:
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
return "SHA-256";
case CONTENT_DIGEST_CHUNKED_SHA512:
return "SHA-512";
@@ -374,6 +466,7 @@ final class ApkSigningBlockUtils {
private static int getContentDigestAlgorithmOutputSizeBytes(int digestAlgorithm) {
switch (digestAlgorithm) {
case CONTENT_DIGEST_CHUNKED_SHA256:
+ case CONTENT_DIGEST_VERITY_CHUNKED_SHA256:
return 256 / 8;
case CONTENT_DIGEST_CHUNKED_SHA512:
return 512 / 8;
@@ -389,11 +482,14 @@ final class ApkSigningBlockUtils {
case SIGNATURE_RSA_PSS_WITH_SHA512:
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA512:
+ case SIGNATURE_VERITY_RSA_PKCS1_V1_5_WITH_SHA256:
return "RSA";
case SIGNATURE_ECDSA_WITH_SHA256:
case SIGNATURE_ECDSA_WITH_SHA512:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
return "EC";
case SIGNATURE_DSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
return "DSA";
default:
throw new IllegalArgumentException(
@@ -416,14 +512,17 @@ final class ApkSigningBlockUtils {
new PSSParameterSpec(
"SHA-512", "MGF1", MGF1ParameterSpec.SHA512, 512 / 8, 1));
case SIGNATURE_RSA_PKCS1_V1_5_WITH_SHA256:
+ case SIGNATURE_VERITY_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:
+ case SIGNATURE_VERITY_ECDSA_WITH_SHA256:
return Pair.create("SHA256withECDSA", null);
case SIGNATURE_ECDSA_WITH_SHA512:
return Pair.create("SHA512withECDSA", null);
case SIGNATURE_DSA_WITH_SHA256:
+ case SIGNATURE_VERITY_DSA_WITH_SHA256:
return Pair.create("SHA256withDSA", null);
default:
throw new IllegalArgumentException(
diff --git a/android/util/apk/ApkVerityBuilder.java b/android/util/apk/ApkVerityBuilder.java
index 7412ef41..ba21ccb8 100644
--- a/android/util/apk/ApkVerityBuilder.java
+++ b/android/util/apk/ApkVerityBuilder.java
@@ -68,31 +68,80 @@ abstract class ApkVerityBuilder {
static ApkVerityResult generateApkVerity(RandomAccessFile apk,
SignatureInfo signatureInfo, ByteBufferFactory bufferFactory)
throws IOException, SecurityException, NoSuchAlgorithmException, DigestException {
- assertSigningBlockAlignedAndHasFullPages(signatureInfo);
-
long signingBlockSize =
signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset;
- long dataSize = apk.length() - signingBlockSize - ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE;
+ long dataSize = apk.length() - signingBlockSize;
int[] levelOffset = calculateVerityLevelOffset(dataSize);
+
ByteBuffer output = bufferFactory.create(
CHUNK_SIZE_BYTES + // fsverity header + extensions + padding
- levelOffset[levelOffset.length - 1] + // Merkle tree size
- FSVERITY_HEADER_SIZE_BYTES); // second fsverity header (verbatim copy)
+ levelOffset[levelOffset.length - 1]); // Merkle tree size
+ output.order(ByteOrder.LITTLE_ENDIAN);
- // Start generating the tree from the block boundary as the kernel will expect.
- ByteBuffer treeOutput = slice(output, CHUNK_SIZE_BYTES,
- output.limit() - FSVERITY_HEADER_SIZE_BYTES);
- byte[] rootHash = generateApkVerityTree(apk, signatureInfo, DEFAULT_SALT, levelOffset,
- treeOutput);
+ ByteBuffer header = slice(output, 0, FSVERITY_HEADER_SIZE_BYTES);
+ ByteBuffer extensions = slice(output, FSVERITY_HEADER_SIZE_BYTES, CHUNK_SIZE_BYTES);
+ ByteBuffer tree = slice(output, CHUNK_SIZE_BYTES, output.limit());
+ byte[] apkDigestBytes = new byte[DIGEST_SIZE_BYTES];
+ ByteBuffer apkDigest = ByteBuffer.wrap(apkDigestBytes);
+ apkDigest.order(ByteOrder.LITTLE_ENDIAN);
- ByteBuffer integrityHeader = generateFsverityHeader(apk.length(), DEFAULT_SALT);
- output.put(integrityHeader);
- output.put(generateFsverityExtensions());
+ calculateFsveritySignatureInternal(apk, signatureInfo, tree, apkDigest, header, extensions);
- integrityHeader.rewind();
- output.put(integrityHeader);
output.rewind();
- return new ApkVerityResult(output, rootHash);
+ return new ApkVerityResult(output, apkDigestBytes);
+ }
+
+ /**
+ * Calculates the fsverity root hash for integrity measurement. This needs to be consistent to
+ * what kernel returns.
+ */
+ static byte[] generateFsverityRootHash(RandomAccessFile apk, ByteBuffer apkDigest,
+ SignatureInfo signatureInfo)
+ throws NoSuchAlgorithmException, DigestException, IOException {
+ ByteBuffer verityBlock = ByteBuffer.allocate(CHUNK_SIZE_BYTES)
+ .order(ByteOrder.LITTLE_ENDIAN);
+ ByteBuffer header = slice(verityBlock, 0, FSVERITY_HEADER_SIZE_BYTES);
+ ByteBuffer extensions = slice(verityBlock, FSVERITY_HEADER_SIZE_BYTES, CHUNK_SIZE_BYTES);
+
+ calculateFsveritySignatureInternal(apk, signatureInfo, null, null, header, extensions);
+
+ MessageDigest md = MessageDigest.getInstance(JCA_DIGEST_ALGORITHM);
+ md.update(DEFAULT_SALT);
+ md.update(verityBlock);
+ md.update(apkDigest);
+ return md.digest();
+ }
+
+ private static void calculateFsveritySignatureInternal(
+ RandomAccessFile apk, SignatureInfo signatureInfo, ByteBuffer treeOutput,
+ ByteBuffer rootHashOutput, ByteBuffer headerOutput, ByteBuffer extensionsOutput)
+ throws IOException, NoSuchAlgorithmException, DigestException {
+ assertSigningBlockAlignedAndHasFullPages(signatureInfo);
+
+ long signingBlockSize =
+ signatureInfo.centralDirOffset - signatureInfo.apkSigningBlockOffset;
+ long dataSize = apk.length() - signingBlockSize - ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE;
+ int[] levelOffset = calculateVerityLevelOffset(dataSize);
+
+ if (treeOutput != null) {
+ byte[] apkRootHash = generateApkVerityTree(apk, signatureInfo, DEFAULT_SALT,
+ levelOffset, treeOutput);
+ if (rootHashOutput != null) {
+ rootHashOutput.put(apkRootHash);
+ }
+ }
+
+ if (headerOutput != null) {
+ headerOutput.order(ByteOrder.LITTLE_ENDIAN);
+ generateFsverityHeader(headerOutput, apk.length(), levelOffset.length - 1,
+ DEFAULT_SALT);
+ }
+
+ if (extensionsOutput != null) {
+ extensionsOutput.order(ByteOrder.LITTLE_ENDIAN);
+ generateFsverityExtensions(extensionsOutput, signatureInfo.apkSigningBlockOffset,
+ signingBlockSize, signatureInfo.eocdOffset);
+ }
}
/**
@@ -164,11 +213,11 @@ abstract class ApkVerityBuilder {
}
private void fillUpLastOutputChunk() {
- int extra = (int) (BUFFER_SIZE - mOutput.position() % BUFFER_SIZE);
- if (extra == 0) {
+ int lastBlockSize = (int) (mOutput.position() % BUFFER_SIZE);
+ if (lastBlockSize == 0) {
return;
}
- mOutput.put(ByteBuffer.allocate(extra));
+ mOutput.put(ByteBuffer.allocate(BUFFER_SIZE - lastBlockSize));
}
}
@@ -211,7 +260,7 @@ abstract class ApkVerityBuilder {
eocdCdOffsetFieldPosition - signatureInfo.centralDirOffset),
MMAP_REGION_SIZE_BYTES);
- // 3. Fill up the rest of buffer with 0s.
+ // 3. Consume offset of Signing Block as an alternative EoCD.
ByteBuffer alternativeCentralDirOffset = ByteBuffer.allocate(
ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE).order(ByteOrder.LITTLE_ENDIAN);
alternativeCentralDirOffset.putInt(Math.toIntExact(signatureInfo.apkSigningBlockOffset));
@@ -259,36 +308,109 @@ abstract class ApkVerityBuilder {
return rootHash;
}
- private static ByteBuffer generateFsverityHeader(long fileSize, byte[] salt) {
+ private static void bufferPut(ByteBuffer buffer, byte value) {
+ // FIXME(b/72459251): buffer.put(value) does NOT work surprisingly. The position() after put
+ // does NOT even change. This hack workaround the problem, but the root cause remains
+ // unkonwn yet. This seems only happen when it goes through the apk install flow on my
+ // setup.
+ buffer.put(new byte[] { value });
+ }
+
+ private static ByteBuffer generateFsverityHeader(ByteBuffer buffer, long fileSize, int depth,
+ byte[] salt) {
if (salt.length != 8) {
throw new IllegalArgumentException("salt is not 8 bytes long");
}
- ByteBuffer buffer = ByteBuffer.allocate(FSVERITY_HEADER_SIZE_BYTES);
- buffer.order(ByteOrder.LITTLE_ENDIAN);
-
- // TODO(b/30972906): insert a reference when there is a public one.
+ // TODO(b/30972906): update the reference when there is a better one in public.
buffer.put("TrueBrew".getBytes()); // magic
- buffer.put((byte) 1); // major version
- buffer.put((byte) 0); // minor version
- buffer.put((byte) 12); // log2(block-size) == log2(4096)
- buffer.put((byte) 7); // log2(leaves-per-node) == log2(block-size / digest-size)
- // == log2(4096 / 32)
- buffer.putShort((short) 1); // meta algorithm, 1: SHA-256 FIXME finalize constant
- buffer.putShort((short) 1); // data algorithm, 1: SHA-256 FIXME finalize constant
- buffer.putInt(0x1); // flags, 0x1: has extension, FIXME also hide it
- buffer.putInt(0); // reserved
- buffer.putLong(fileSize); // original i_size
- buffer.put(salt); // salt (8 bytes)
-
- // TODO(b/30972906): Add extension.
+
+ bufferPut(buffer, (byte) 1); // major version
+ bufferPut(buffer, (byte) 0); // minor version
+ bufferPut(buffer, (byte) 12); // log2(block-size): log2(4096)
+ bufferPut(buffer, (byte) 7); // log2(leaves-per-node): log2(4096 / 32)
+
+ buffer.putShort((short) 1); // meta algorithm, SHA256_MODE == 1
+ buffer.putShort((short) 1); // data algorithm, SHA256_MODE == 1
+
+ buffer.putInt(0x1); // flags, 0x1: has extension
+ buffer.putInt(0); // reserved
+
+ buffer.putLong(fileSize); // original file size
+
+ bufferPut(buffer, (byte) 0); // auth block offset, disabled here
+ bufferPut(buffer, (byte) 2); // extension count
+ buffer.put(salt); // salt (8 bytes)
+ // skip(buffer, 22); // reserved
buffer.rewind();
return buffer;
}
- private static ByteBuffer generateFsverityExtensions() {
- return ByteBuffer.allocate(64); // TODO(b/30972906): implement this.
+ private static ByteBuffer generateFsverityExtensions(ByteBuffer buffer, long signingBlockOffset,
+ long signingBlockSize, long eocdOffset) {
+ // Snapshot of the FSVerity structs (subject to change once upstreamed).
+ //
+ // struct fsverity_extension {
+ // __le16 length;
+ // u8 type;
+ // u8 reserved[5];
+ // };
+ //
+ // struct fsverity_extension_elide {
+ // __le64 offset;
+ // __le64 length;
+ // }
+ //
+ // struct fsverity_extension_patch {
+ // __le64 offset;
+ // u8 length;
+ // u8 reserved[7];
+ // u8 databytes[];
+ // };
+
+ final int kSizeOfFsverityExtensionHeader = 8;
+
+ {
+ // struct fsverity_extension #1
+ final int kSizeOfFsverityElidedExtension = 16;
+
+ buffer.putShort((short) // total size of extension, padded to 64-bit alignment
+ (kSizeOfFsverityExtensionHeader + kSizeOfFsverityElidedExtension));
+ buffer.put((byte) 0); // ID of elide extension
+ skip(buffer, 5); // reserved
+
+ // struct fsverity_extension_elide
+ buffer.putLong(signingBlockOffset);
+ buffer.putLong(signingBlockSize);
+ }
+
+ {
+ // struct fsverity_extension #2
+ final int kSizeOfFsverityPatchExtension =
+ 8 + // offset size
+ 1 + // size of length from offset (up to 255)
+ 7 + // reserved
+ ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE;
+ final int kPadding = (int) divideRoundup(kSizeOfFsverityPatchExtension % 8, 8);
+
+ buffer.putShort((short) // total size of extension, padded to 64-bit alignment
+ (kSizeOfFsverityExtensionHeader + kSizeOfFsverityPatchExtension + kPadding));
+ buffer.put((byte) 1); // ID of patch extension
+ skip(buffer, 5); // reserved
+
+ // struct fsverity_extension_patch
+ buffer.putLong(eocdOffset); // offset
+ buffer.put((byte) ZIP_EOCD_CENTRAL_DIR_OFFSET_FIELD_SIZE); // length
+ skip(buffer, 7); // reserved
+ buffer.putInt(Math.toIntExact(signingBlockOffset)); // databytes
+
+ // There are extra kPadding bytes of 0s here, included in the total size field of the
+ // extension header. The output ByteBuffer is assumed to be initialized to 0.
+ }
+
+ buffer.rewind();
+ return buffer;
}
/**
@@ -344,6 +466,11 @@ abstract class ApkVerityBuilder {
return b.slice();
}
+ /** Skip the {@code ByteBuffer} position by {@code bytes}. */
+ private static void skip(ByteBuffer buffer, int bytes) {
+ buffer.position(buffer.position() + bytes);
+ }
+
/** Divides a number and round up to the closest integer. */
private static long divideRoundup(long dividend, long divisor) {
return (dividend + divisor - 1) / divisor;
diff --git a/android/util/proto/ProtoUtils.java b/android/util/proto/ProtoUtils.java
index 85b7ec82..c7bbb9f2 100644
--- a/android/util/proto/ProtoUtils.java
+++ b/android/util/proto/ProtoUtils.java
@@ -48,4 +48,26 @@ public class ProtoUtils {
proto.write(Duration.END_MS, endMs);
proto.end(token);
}
+
+ /**
+ * Helper function to write bit-wise flags to proto as repeated enums
+ * @hide
+ */
+ public static void writeBitWiseFlagsToProtoEnum(ProtoOutputStream proto, long fieldId,
+ int flags, int[] origEnums, int[] protoEnums) {
+ if (protoEnums.length != origEnums.length) {
+ throw new IllegalArgumentException("The length of origEnums must match protoEnums");
+ }
+ int len = origEnums.length;
+ for (int i = 0; i < len; i++) {
+ // handle zero flag case.
+ if (origEnums[i] == 0 && flags == 0) {
+ proto.write(fieldId, protoEnums[i]);
+ return;
+ }
+ if ((flags & origEnums[i]) != 0) {
+ proto.write(fieldId, protoEnums[i]);
+ }
+ }
+ }
}
diff --git a/android/view/Choreographer.java b/android/view/Choreographer.java
index ba6b6cf6..1caea577 100644
--- a/android/view/Choreographer.java
+++ b/android/view/Choreographer.java
@@ -235,6 +235,8 @@ public final class Choreographer {
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
+ // b/68769804: For low FPS experiments.
+ setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}
private static float getRefreshRate() {
@@ -371,6 +373,7 @@ public final class Choreographer {
* @see #removeCallbacks
* @hide
*/
+ @TestApi
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
@@ -389,6 +392,7 @@ public final class Choreographer {
* @see #removeCallback
* @hide
*/
+ @TestApi
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
if (action == null) {
@@ -438,6 +442,7 @@ public final class Choreographer {
* @see #postCallbackDelayed
* @hide
*/
+ @TestApi
public void removeCallbacks(int callbackType, Runnable action, Object token) {
if (callbackType < 0 || callbackType > CALLBACK_LAST) {
throw new IllegalArgumentException("callbackType is invalid");
@@ -605,6 +610,7 @@ public final class Choreographer {
void setFPSDivisor(int divisor) {
if (divisor <= 0) divisor = 1;
mFPSDivisor = divisor;
+ ThreadedRenderer.setFPSDivisor(divisor);
}
void doFrame(long frameTimeNanos, int frame) {
diff --git a/android/view/DisplayCutout.java b/android/view/DisplayCutout.java
index e448f14c..a61c8c1d 100644
--- a/android/view/DisplayCutout.java
+++ b/android/view/DisplayCutout.java
@@ -16,11 +16,15 @@
package android.view;
+import static android.view.DisplayCutoutProto.BOUNDS;
+import static android.view.DisplayCutoutProto.INSETS;
import static android.view.Surface.ROTATION_0;
import static android.view.Surface.ROTATION_180;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
+import android.content.res.Resources;
+import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.Rect;
@@ -28,7 +32,12 @@ import android.graphics.RectF;
import android.graphics.Region;
import android.os.Parcel;
import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.PathParser;
+import android.util.proto.ProtoOutputStream;
+import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import java.util.List;
@@ -40,6 +49,9 @@ import java.util.List;
*/
public final class DisplayCutout {
+ private static final String TAG = "DisplayCutout";
+ private static final String DP_MARKER = "@dp";
+
private static final Rect ZERO_RECT = new Rect();
private static final Region EMPTY_REGION = new Region();
@@ -150,11 +162,21 @@ public final class DisplayCutout {
@Override
public String toString() {
return "DisplayCutout{insets=" + mSafeInsets
- + " bounds=" + mBounds
+ + " boundingRect=" + getBoundingRect()
+ "}";
}
/**
+ * @hide
+ */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ mSafeInsets.writeToProto(proto, INSETS);
+ mBounds.getBounds().writeToProto(proto, BOUNDS);
+ proto.end(token);
+ }
+
+ /**
* Insets the reference frame of the cutout in the given directions.
*
* @return a copy of this instance which has been inset
@@ -266,9 +288,7 @@ public final class DisplayCutout {
* @hide
*/
public static DisplayCutout fromBoundingPolygon(List<Point> points) {
- Region bounds = Region.obtain();
Path path = new Path();
-
path.reset();
for (int i = 0; i < points.size(); i++) {
Point point = points.get(i);
@@ -279,18 +299,62 @@ public final class DisplayCutout {
}
}
path.close();
+ return fromBounds(path);
+ }
+ /**
+ * Creates an instance from a bounding {@link Path}.
+ *
+ * @hide
+ */
+ public static DisplayCutout fromBounds(Path path) {
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);
+ Region bounds = new Region();
bounds.setPath(path, clipRegion);
+ clipRegion.recycle();
return new DisplayCutout(ZERO_RECT, bounds);
}
/**
+ * Creates an instance according to @android:string/config_mainBuiltInDisplayCutout.
+ *
+ * @hide
+ */
+ public static DisplayCutout fromResources(Resources res, int displayWidth) {
+ String spec = res.getString(R.string.config_mainBuiltInDisplayCutout);
+ if (TextUtils.isEmpty(spec)) {
+ return null;
+ }
+ spec = spec.trim();
+ final boolean inDp = spec.endsWith(DP_MARKER);
+ if (inDp) {
+ spec = spec.substring(0, spec.length() - DP_MARKER.length());
+ }
+
+ Path p;
+ try {
+ p = PathParser.createPathFromPathData(spec);
+ } catch (Throwable e) {
+ Log.wtf(TAG, "Could not inflate cutout: ", e);
+ return null;
+ }
+
+ final Matrix m = new Matrix();
+ if (inDp) {
+ final float dpToPx = res.getDisplayMetrics().density;
+ m.postScale(dpToPx, dpToPx);
+ }
+ m.postTranslate(displayWidth / 2f, 0);
+ p.transform(m);
+ return fromBounds(p);
+ }
+
+ /**
* Helper class for passing {@link DisplayCutout} through binder.
*
* Needed, because {@code readFromParcel} cannot be used with immutable classes.
@@ -316,12 +380,23 @@ public final class DisplayCutout {
@Override
public void writeToParcel(Parcel out, int flags) {
- if (mInner == NO_CUTOUT) {
+ writeCutoutToParcel(mInner, out, flags);
+ }
+
+ /**
+ * Writes a DisplayCutout to a {@link Parcel}.
+ *
+ * @see #readCutoutFromParcel(Parcel)
+ */
+ public static void writeCutoutToParcel(DisplayCutout cutout, Parcel out, int flags) {
+ if (cutout == null) {
+ out.writeInt(-1);
+ } else if (cutout == NO_CUTOUT) {
out.writeInt(0);
} else {
out.writeInt(1);
- out.writeTypedObject(mInner.mSafeInsets, flags);
- out.writeTypedObject(mInner.mBounds, flags);
+ out.writeTypedObject(cutout.mSafeInsets, flags);
+ out.writeTypedObject(cutout.mBounds, flags);
}
}
@@ -332,13 +407,13 @@ public final class DisplayCutout {
* Needed for AIDL out parameters.
*/
public void readFromParcel(Parcel in) {
- mInner = readCutout(in);
+ mInner = readCutoutFromParcel(in);
}
public static final Creator<ParcelableWrapper> CREATOR = new Creator<ParcelableWrapper>() {
@Override
public ParcelableWrapper createFromParcel(Parcel in) {
- return new ParcelableWrapper(readCutout(in));
+ return new ParcelableWrapper(readCutoutFromParcel(in));
}
@Override
@@ -347,8 +422,17 @@ public final class DisplayCutout {
}
};
- private static DisplayCutout readCutout(Parcel in) {
- if (in.readInt() == 0) {
+ /**
+ * Reads a DisplayCutout from a {@link Parcel}.
+ *
+ * @see #writeCutoutToParcel(DisplayCutout, Parcel, int)
+ */
+ public static DisplayCutout readCutoutFromParcel(Parcel in) {
+ int variant = in.readInt();
+ if (variant == -1) {
+ return null;
+ }
+ if (variant == 0) {
return NO_CUTOUT;
}
diff --git a/android/view/DisplayInfo.java b/android/view/DisplayInfo.java
index b813ddb6..37e9815c 100644
--- a/android/view/DisplayInfo.java
+++ b/android/view/DisplayInfo.java
@@ -149,6 +149,13 @@ public final class DisplayInfo implements Parcelable {
public int overscanBottom;
/**
+ * The {@link DisplayCutout} if present, otherwise {@code null}.
+ *
+ * @hide
+ */
+ public DisplayCutout displayCutout;
+
+ /**
* The rotation of the display relative to its natural orientation.
* May be one of {@link android.view.Surface#ROTATION_0},
* {@link android.view.Surface#ROTATION_90}, {@link android.view.Surface#ROTATION_180},
@@ -301,6 +308,7 @@ public final class DisplayInfo implements Parcelable {
&& overscanTop == other.overscanTop
&& overscanRight == other.overscanRight
&& overscanBottom == other.overscanBottom
+ && Objects.equal(displayCutout, other.displayCutout)
&& rotation == other.rotation
&& modeId == other.modeId
&& defaultModeId == other.defaultModeId
@@ -342,6 +350,7 @@ public final class DisplayInfo implements Parcelable {
overscanTop = other.overscanTop;
overscanRight = other.overscanRight;
overscanBottom = other.overscanBottom;
+ displayCutout = other.displayCutout;
rotation = other.rotation;
modeId = other.modeId;
defaultModeId = other.defaultModeId;
@@ -379,6 +388,7 @@ public final class DisplayInfo implements Parcelable {
overscanTop = source.readInt();
overscanRight = source.readInt();
overscanBottom = source.readInt();
+ displayCutout = DisplayCutout.ParcelableWrapper.readCutoutFromParcel(source);
rotation = source.readInt();
modeId = source.readInt();
defaultModeId = source.readInt();
@@ -425,6 +435,7 @@ public final class DisplayInfo implements Parcelable {
dest.writeInt(overscanTop);
dest.writeInt(overscanRight);
dest.writeInt(overscanBottom);
+ DisplayCutout.ParcelableWrapper.writeCutoutToParcel(displayCutout, dest, flags);
dest.writeInt(rotation);
dest.writeInt(modeId);
dest.writeInt(defaultModeId);
diff --git a/android/view/IWindowManagerImpl.java b/android/view/IWindowManagerImpl.java
index 4d804c55..93e6c0b9 100644
--- a/android/view/IWindowManagerImpl.java
+++ b/android/view/IWindowManagerImpl.java
@@ -29,6 +29,7 @@ import android.os.IRemoteCallback;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.DisplayMetrics;
+import android.view.RemoteAnimationAdapter;
import com.android.internal.os.IResultReceiver;
import com.android.internal.policy.IKeyguardDismissCallback;
@@ -76,6 +77,11 @@ public class IWindowManagerImpl implements IWindowManager {
// ---- unused implementation of IWindowManager ----
@Override
+ public int getNavBarPosition() throws RemoteException {
+ return 0;
+ }
+
+ @Override
public void addWindowToken(IBinder arg0, int arg1, int arg2) throws RemoteException {
// TODO Auto-generated method stub
@@ -237,6 +243,10 @@ public class IWindowManagerImpl implements IWindowManager {
}
@Override
+ public void overridePendingAppTransitionRemote(RemoteAnimationAdapter adapter) {
+ }
+
+ @Override
public void prepareAppTransition(int arg0, boolean arg1) throws RemoteException {
// TODO Auto-generated method stub
@@ -416,7 +426,8 @@ public class IWindowManagerImpl implements IWindowManager {
}
@Override
- public void dismissKeyguard(IKeyguardDismissCallback callback) throws RemoteException {
+ public void dismissKeyguard(IKeyguardDismissCallback callback, CharSequence message)
+ throws RemoteException {
}
@Override
@@ -537,4 +548,17 @@ public class IWindowManagerImpl implements IWindowManager {
public void unregisterWallpaperVisibilityListener(IWallpaperVisibilityListener listener,
int displayId) throws RemoteException {
}
+
+ @Override
+ public void startWindowTrace() throws RemoteException {
+ }
+
+ @Override
+ public void stopWindowTrace() throws RemoteException {
+ }
+
+ @Override
+ public boolean isWindowTraceEnabled() throws RemoteException {
+ return false;
+ }
}
diff --git a/android/view/KeyEvent.java b/android/view/KeyEvent.java
index a2147b71..a5974056 100644
--- a/android/view/KeyEvent.java
+++ b/android/view/KeyEvent.java
@@ -804,11 +804,12 @@ public class KeyEvent extends InputEvent implements Parcelable {
public static final int KEYCODE_SYSTEM_NAVIGATION_LEFT = 282;
/** Key code constant: Consumed by the system for navigation right */
public static final int KEYCODE_SYSTEM_NAVIGATION_RIGHT = 283;
- /** Key code constant: Show all apps
- * @hide */
+ /** Key code constant: Show all apps */
public static final int KEYCODE_ALL_APPS = 284;
+ /** Key code constant: Refresh key. */
+ public static final int KEYCODE_REFRESH = 285;
- private static final int LAST_KEYCODE = KEYCODE_ALL_APPS;
+ private static final int LAST_KEYCODE = KEYCODE_REFRESH;
// NOTE: If you add a new keycode here you must also add it to:
// isSystem()
diff --git a/android/view/MotionEvent.java b/android/view/MotionEvent.java
index 04fa637b..1d7c1ded 100644
--- a/android/view/MotionEvent.java
+++ b/android/view/MotionEvent.java
@@ -26,6 +26,8 @@ import android.util.SparseArray;
import dalvik.annotation.optimization.CriticalNative;
import dalvik.annotation.optimization.FastNative;
+import java.util.Objects;
+
/**
* Object used to report movement (mouse, pen, finger, trackball) events.
* Motion events may hold either absolute or relative movements and other data,
@@ -173,6 +175,8 @@ public final class MotionEvent extends InputEvent implements Parcelable {
private static final long NS_PER_MS = 1000000;
private static final String LABEL_PREFIX = "AXIS_";
+ private static final boolean DEBUG_CONCISE_TOSTRING = false;
+
/**
* An invalid pointer id.
*
@@ -3236,31 +3240,42 @@ public final class MotionEvent extends InputEvent implements Parcelable {
public String toString() {
StringBuilder msg = new StringBuilder();
msg.append("MotionEvent { action=").append(actionToString(getAction()));
- msg.append(", actionButton=").append(buttonStateToString(getActionButton()));
+ appendUnless("0", msg, ", actionButton=", buttonStateToString(getActionButton()));
final int pointerCount = getPointerCount();
for (int i = 0; i < pointerCount; i++) {
- msg.append(", id[").append(i).append("]=").append(getPointerId(i));
- msg.append(", x[").append(i).append("]=").append(getX(i));
- msg.append(", y[").append(i).append("]=").append(getY(i));
- msg.append(", toolType[").append(i).append("]=").append(
- toolTypeToString(getToolType(i)));
+ appendUnless(i, msg, ", id[" + i + "]=", getPointerId(i));
+ float x = getX(i);
+ float y = getY(i);
+ if (!DEBUG_CONCISE_TOSTRING || x != 0f || y != 0f) {
+ msg.append(", x[").append(i).append("]=").append(x);
+ msg.append(", y[").append(i).append("]=").append(y);
+ }
+ appendUnless(TOOL_TYPE_SYMBOLIC_NAMES.get(TOOL_TYPE_FINGER),
+ msg, ", toolType[" + i + "]=", toolTypeToString(getToolType(i)));
}
- msg.append(", buttonState=").append(MotionEvent.buttonStateToString(getButtonState()));
- msg.append(", metaState=").append(KeyEvent.metaStateToString(getMetaState()));
- msg.append(", flags=0x").append(Integer.toHexString(getFlags()));
- msg.append(", edgeFlags=0x").append(Integer.toHexString(getEdgeFlags()));
- msg.append(", pointerCount=").append(pointerCount);
- msg.append(", historySize=").append(getHistorySize());
+ appendUnless("0", msg, ", buttonState=", MotionEvent.buttonStateToString(getButtonState()));
+ appendUnless("0", msg, ", metaState=", KeyEvent.metaStateToString(getMetaState()));
+ appendUnless("0", msg, ", flags=0x", Integer.toHexString(getFlags()));
+ appendUnless("0", msg, ", edgeFlags=0x", Integer.toHexString(getEdgeFlags()));
+ appendUnless(1, msg, ", pointerCount=", pointerCount);
+ appendUnless(0, msg, ", historySize=", getHistorySize());
msg.append(", eventTime=").append(getEventTime());
- msg.append(", downTime=").append(getDownTime());
- msg.append(", deviceId=").append(getDeviceId());
- msg.append(", source=0x").append(Integer.toHexString(getSource()));
+ if (!DEBUG_CONCISE_TOSTRING) {
+ msg.append(", downTime=").append(getDownTime());
+ msg.append(", deviceId=").append(getDeviceId());
+ msg.append(", source=0x").append(Integer.toHexString(getSource()));
+ }
msg.append(" }");
return msg.toString();
}
+ private static <T> void appendUnless(T defValue, StringBuilder sb, String key, T value) {
+ if (DEBUG_CONCISE_TOSTRING && Objects.equals(defValue, value)) return;
+ sb.append(key).append(value);
+ }
+
/**
* Returns a string that represents the symbolic name of the specified unmasked action
* such as "ACTION_DOWN", "ACTION_POINTER_DOWN(3)" or an equivalent numeric constant
diff --git a/android/view/NotificationHeaderView.java b/android/view/NotificationHeaderView.java
index ab0b3eec..fbba8abf 100644
--- a/android/view/NotificationHeaderView.java
+++ b/android/view/NotificationHeaderView.java
@@ -47,6 +47,7 @@ public class NotificationHeaderView extends ViewGroup {
private final int mGravity;
private View mAppName;
private View mHeaderText;
+ private View mSecondaryHeaderText;
private OnClickListener mExpandClickListener;
private HeaderTouchListener mTouchListener = new HeaderTouchListener();
private ImageView mExpandButton;
@@ -58,7 +59,6 @@ public class NotificationHeaderView extends ViewGroup {
private boolean mShowExpandButtonAtEnd;
private boolean mShowWorkBadgeAtEnd;
private Drawable mBackground;
- private int mHeaderBackgroundHeight;
private boolean mEntireHeaderClickable;
private boolean mExpandOnlyOnButton;
private boolean mAcceptAllTouches;
@@ -68,7 +68,7 @@ public class NotificationHeaderView extends ViewGroup {
@Override
public void getOutline(View view, Outline outline) {
if (mBackground != null) {
- outline.setRect(0, 0, getWidth(), mHeaderBackgroundHeight);
+ outline.setRect(0, 0, getWidth(), getHeight());
outline.setAlpha(1f);
}
}
@@ -91,8 +91,6 @@ public class NotificationHeaderView extends ViewGroup {
Resources res = getResources();
mChildMinWidth = res.getDimensionPixelSize(R.dimen.notification_header_shrink_min_width);
mContentEndMargin = res.getDimensionPixelSize(R.dimen.notification_content_margin_end);
- mHeaderBackgroundHeight = res.getDimensionPixelSize(
- R.dimen.notification_header_background_height);
mEntireHeaderClickable = res.getBoolean(R.bool.config_notificationHeaderClickableForExpand);
int[] attrIds = { android.R.attr.gravity };
@@ -106,6 +104,7 @@ public class NotificationHeaderView extends ViewGroup {
super.onFinishInflate();
mAppName = findViewById(com.android.internal.R.id.app_name_text);
mHeaderText = findViewById(com.android.internal.R.id.header_text);
+ mSecondaryHeaderText = findViewById(com.android.internal.R.id.header_text_secondary);
mExpandButton = findViewById(com.android.internal.R.id.expand_button);
mIcon = findViewById(com.android.internal.R.id.icon);
mProfileBadge = findViewById(com.android.internal.R.id.profile_badge);
@@ -137,26 +136,33 @@ public class NotificationHeaderView extends ViewGroup {
if (totalWidth > givenWidth) {
int overFlow = totalWidth - givenWidth;
// We are overflowing, lets shrink the app name first
- final int appWidth = mAppName.getMeasuredWidth();
- if (overFlow > 0 && mAppName.getVisibility() != GONE && appWidth > mChildMinWidth) {
- int newSize = appWidth - Math.min(appWidth - mChildMinWidth, overFlow);
- int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
- mAppName.measure(childWidthSpec, wrapContentHeightSpec);
- overFlow -= appWidth - newSize;
- }
- // still overflowing, finaly we shrink the header text
- if (overFlow > 0 && mHeaderText.getVisibility() != GONE) {
- // we're still too big
- final int textWidth = mHeaderText.getMeasuredWidth();
- int newSize = Math.max(0, textWidth - overFlow);
- int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
- mHeaderText.measure(childWidthSpec, wrapContentHeightSpec);
- }
+ overFlow = shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mAppName,
+ mChildMinWidth);
+
+ // still overflowing, we shrink the header text
+ overFlow = shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mHeaderText, 0);
+
+ // still overflowing, finally we shrink the secondary header text
+ shrinkViewForOverflow(wrapContentHeightSpec, overFlow, mSecondaryHeaderText,
+ 0);
}
mTotalWidth = Math.min(totalWidth, givenWidth);
setMeasuredDimension(givenWidth, givenHeight);
}
+ private int shrinkViewForOverflow(int heightSpec, int overFlow, View targetView,
+ int minimumWidth) {
+ final int oldWidth = targetView.getMeasuredWidth();
+ if (overFlow > 0 && targetView.getVisibility() != GONE && oldWidth > minimumWidth) {
+ // we're still too big
+ int newSize = Math.max(minimumWidth, oldWidth - overFlow);
+ int childWidthSpec = MeasureSpec.makeMeasureSpec(newSize, MeasureSpec.AT_MOST);
+ targetView.measure(childWidthSpec, heightSpec);
+ overFlow -= oldWidth - newSize;
+ }
+ return overFlow;
+ }
+
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = getPaddingStart();
@@ -228,7 +234,7 @@ public class NotificationHeaderView extends ViewGroup {
@Override
protected void onDraw(Canvas canvas) {
if (mBackground != null) {
- mBackground.setBounds(0, 0, getWidth(), mHeaderBackgroundHeight);
+ mBackground.setBounds(0, 0, getWidth(), getHeight());
mBackground.draw(canvas);
}
}
diff --git a/android/view/PointerIcon.java b/android/view/PointerIcon.java
index 998fd019..3fd46963 100644
--- a/android/view/PointerIcon.java
+++ b/android/view/PointerIcon.java
@@ -461,8 +461,10 @@ public final class PointerIcon implements Parcelable {
+ "refer to a bitmap drawable.");
}
+ final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
+ validateHotSpot(bitmap, hotSpotX, hotSpotY);
// Set the properties now that we have successfully loaded the icon.
- mBitmap = ((BitmapDrawable)drawable).getBitmap();
+ mBitmap = bitmap;
mHotSpotX = hotSpotX;
mHotSpotY = hotSpotY;
}
diff --git a/android/view/RecordingCanvas.java b/android/view/RecordingCanvas.java
index 5088cdc9..fbb862be 100644
--- a/android/view/RecordingCanvas.java
+++ b/android/view/RecordingCanvas.java
@@ -34,6 +34,7 @@ import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.TemporaryBuffer;
import android.text.GraphicsOperations;
+import android.text.MeasuredText;
import android.text.SpannableString;
import android.text.SpannedString;
import android.text.TextUtils;
@@ -473,7 +474,8 @@ public class RecordingCanvas extends Canvas {
}
nDrawTextRun(mNativeCanvasWrapper, text, index, count, contextIndex, contextCount,
- x, y, isRtl, paint.getNativeInstance());
+ x, y, isRtl, paint.getNativeInstance(), 0 /* measured text */,
+ 0 /* measured text offset */);
}
@Override
@@ -503,8 +505,20 @@ public class RecordingCanvas extends Canvas {
int len = end - start;
char[] buf = TemporaryBuffer.obtain(contextLen);
TextUtils.getChars(text, contextStart, contextEnd, buf, 0);
+ long measuredTextPtr = 0;
+ int measuredTextOffset = 0;
+ if (text instanceof MeasuredText) {
+ MeasuredText mt = (MeasuredText) text;
+ int paraIndex = mt.findParaIndex(start);
+ if (end <= mt.getParagraphEnd(paraIndex)) {
+ // Only support if the target is in the same paragraph.
+ measuredTextPtr = mt.getMeasuredParagraph(paraIndex).getNativePtr();
+ measuredTextOffset = start - mt.getParagraphStart(paraIndex);
+ }
+ }
nDrawTextRun(mNativeCanvasWrapper, buf, start - contextStart, len,
- 0, contextLen, x, y, isRtl, paint.getNativeInstance());
+ 0, contextLen, x, y, isRtl, paint.getNativeInstance(),
+ measuredTextPtr, measuredTextOffset);
TemporaryBuffer.recycle(buf);
}
}
@@ -626,7 +640,8 @@ public class RecordingCanvas extends Canvas {
@FastNative
private static native void nDrawTextRun(long nativeCanvas, char[] text, int start, int count,
- int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint);
+ int contextStart, int contextCount, float x, float y, boolean isRtl, long nativePaint,
+ long nativeMeasuredText, int measuredTextOffset);
@FastNative
private static native void nDrawTextOnPath(long nativeCanvas, char[] text, int index, int count,
diff --git a/android/view/RemoteAnimationAdapter.java b/android/view/RemoteAnimationAdapter.java
new file mode 100644
index 00000000..d597e597
--- /dev/null
+++ b/android/view/RemoteAnimationAdapter.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.view;
+
+import android.app.ActivityOptions;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Object that describes how to run a remote animation.
+ * <p>
+ * A remote animation lets another app control the entire app transition. It does so by
+ * <ul>
+ * <li>using {@link ActivityOptions#makeRemoteAnimation}</li>
+ * <li>using {@link IWindowManager#overridePendingAppTransitionRemote}</li>
+ * </ul>
+ * to register a {@link RemoteAnimationAdapter} that describes how the animation should be run:
+ * Along some meta-data, this object contains a callback that gets invoked from window manager when
+ * the transition is ready to be started.
+ * <p>
+ * Window manager supplies a list of {@link RemoteAnimationTarget}s into the callback. Each target
+ * contains information about the activity that is animating as well as
+ * {@link RemoteAnimationTarget#leash}. The controlling app can modify the leash like any other
+ * {@link SurfaceControl}, including the possibility to synchronize updating the leash's surface
+ * properties with a frame to be drawn using
+ * {@link SurfaceControl.Transaction#deferTransactionUntil}.
+ * <p>
+ * When the animation is done, the controlling app can invoke
+ * {@link IRemoteAnimationFinishedCallback} that gets supplied into
+ * {@link IRemoteAnimationRunner#onStartAnimation}
+ *
+ * @hide
+ */
+public class RemoteAnimationAdapter implements Parcelable {
+
+ private final IRemoteAnimationRunner mRunner;
+ private final long mDuration;
+ private final long mStatusBarTransitionDelay;
+
+ /**
+ * @param runner The interface that gets notified when we actually need to start the animation.
+ * @param duration The duration of the animation.
+ * @param statusBarTransitionDelay The desired delay for all visual animations in the
+ * status bar caused by this app animation in millis.
+ */
+ public RemoteAnimationAdapter(IRemoteAnimationRunner runner, long duration,
+ long statusBarTransitionDelay) {
+ mRunner = runner;
+ mDuration = duration;
+ mStatusBarTransitionDelay = statusBarTransitionDelay;
+ }
+
+ public RemoteAnimationAdapter(Parcel in) {
+ mRunner = IRemoteAnimationRunner.Stub.asInterface(in.readStrongBinder());
+ mDuration = in.readLong();
+ mStatusBarTransitionDelay = in.readLong();
+ }
+
+ public IRemoteAnimationRunner getRunner() {
+ return mRunner;
+ }
+
+ public long getDuration() {
+ return mDuration;
+ }
+
+ public long getStatusBarTransitionDelay() {
+ return mStatusBarTransitionDelay;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeStrongInterface(mRunner);
+ dest.writeLong(mDuration);
+ dest.writeLong(mStatusBarTransitionDelay);
+ }
+
+ public static final Creator<RemoteAnimationAdapter> CREATOR
+ = new Creator<RemoteAnimationAdapter>() {
+ public RemoteAnimationAdapter createFromParcel(Parcel in) {
+ return new RemoteAnimationAdapter(in);
+ }
+
+ public RemoteAnimationAdapter[] newArray(int size) {
+ return new RemoteAnimationAdapter[size];
+ }
+ };
+}
diff --git a/android/view/RemoteAnimationDefinition.java b/android/view/RemoteAnimationDefinition.java
new file mode 100644
index 00000000..381f6926
--- /dev/null
+++ b/android/view/RemoteAnimationDefinition.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.view;
+
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.SparseArray;
+import android.view.WindowManager.TransitionType;
+
+/**
+ * Defines which animation types should be overridden by which remote animation.
+ *
+ * @hide
+ */
+public class RemoteAnimationDefinition implements Parcelable {
+
+ private final SparseArray<RemoteAnimationAdapter> mTransitionAnimationMap;
+
+ public RemoteAnimationDefinition() {
+ mTransitionAnimationMap = new SparseArray<>();
+ }
+
+ /**
+ * Registers a remote animation for a specific transition.
+ *
+ * @param transition The transition type. Must be one of WindowManager.TRANSIT_* values.
+ * @param adapter The adapter that described how to run the remote animation.
+ */
+ public void addRemoteAnimation(@TransitionType int transition, RemoteAnimationAdapter adapter) {
+ mTransitionAnimationMap.put(transition, adapter);
+ }
+
+ /**
+ * Checks whether a remote animation for specific transition is defined.
+ *
+ * @param transition The transition type. Must be one of WindowManager.TRANSIT_* values.
+ * @return Whether this definition has defined a remote animation for the specified transition.
+ */
+ public boolean hasTransition(@TransitionType int transition) {
+ return mTransitionAnimationMap.get(transition) != null;
+ }
+
+ /**
+ * Retrieves the remote animation for a specific transition.
+ *
+ * @param transition The transition type. Must be one of WindowManager.TRANSIT_* values.
+ * @return The remote animation adapter for the specified transition.
+ */
+ public @Nullable RemoteAnimationAdapter getAdapter(@TransitionType int transition) {
+ return mTransitionAnimationMap.get(transition);
+ }
+
+ public RemoteAnimationDefinition(Parcel in) {
+ mTransitionAnimationMap = in.readSparseArray(null /* loader */);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeSparseArray((SparseArray) mTransitionAnimationMap);
+ }
+
+ public static final Creator<RemoteAnimationDefinition> CREATOR =
+ new Creator<RemoteAnimationDefinition>() {
+ public RemoteAnimationDefinition createFromParcel(Parcel in) {
+ return new RemoteAnimationDefinition(in);
+ }
+
+ public RemoteAnimationDefinition[] newArray(int size) {
+ return new RemoteAnimationDefinition[size];
+ }
+ };
+}
diff --git a/android/view/RemoteAnimationTarget.java b/android/view/RemoteAnimationTarget.java
new file mode 100644
index 00000000..c28c3894
--- /dev/null
+++ b/android/view/RemoteAnimationTarget.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.view;
+
+import android.annotation.IntDef;
+import android.app.WindowConfiguration;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Describes an activity to be animated as part of a remote animation.
+ *
+ * @hide
+ */
+public class RemoteAnimationTarget implements Parcelable {
+
+ /**
+ * The app is in the set of opening apps of this transition.
+ */
+ public static final int MODE_OPENING = 0;
+
+ /**
+ * The app is in the set of closing apps of this transition.
+ */
+ public static final int MODE_CLOSING = 1;
+
+ @IntDef(prefix = { "MODE_" }, value = {
+ MODE_OPENING,
+ MODE_CLOSING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Mode {}
+
+ /**
+ * The {@link Mode} to describe whether this app is opening or closing.
+ */
+ public final @Mode int mode;
+
+ /**
+ * The id of the task this app belongs to.
+ */
+ public final int taskId;
+
+ /**
+ * The {@link SurfaceControl} object to actually control the transform of the app.
+ */
+ public final SurfaceControl leash;
+
+ /**
+ * Whether the app is translucent and may reveal apps behind.
+ */
+ public final boolean isTranslucent;
+
+ /**
+ * The clip rect window manager applies when clipping the app's main surface in screen space
+ * coordinates. This is just a hint to the animation runner: If running a clip-rect animation,
+ * anything that extends beyond these bounds will not have any effect. This implies that any
+ * clip-rect animation should likely stop at these bounds.
+ */
+ public final Rect clipRect;
+
+ /**
+ * The index of the element in the tree in prefix order. This should be used for z-layering
+ * to preserve original z-layer order in the hierarchy tree assuming no "boosting" needs to
+ * happen.
+ */
+ public final int prefixOrderIndex;
+
+ /**
+ * The source position of the app, in screen spaces coordinates. If the position of the leash
+ * is modified from the controlling app, any animation transform needs to be offset by this
+ * amount.
+ */
+ public final Point position;
+
+ /**
+ * The bounds of the source container the app lives in, in screen space coordinates. If the crop
+ * of the leash is modified from the controlling app, it needs to take the source container
+ * bounds into account when calculating the crop.
+ */
+ public final Rect sourceContainerBounds;
+
+ /**
+ * The window configuration for the target.
+ */
+ public final WindowConfiguration windowConfiguration;
+
+ public RemoteAnimationTarget(int taskId, int mode, SurfaceControl leash, boolean isTranslucent,
+ Rect clipRect, int prefixOrderIndex, Point position, Rect sourceContainerBounds,
+ WindowConfiguration windowConfig) {
+ this.mode = mode;
+ this.taskId = taskId;
+ this.leash = leash;
+ this.isTranslucent = isTranslucent;
+ this.clipRect = new Rect(clipRect);
+ this.prefixOrderIndex = prefixOrderIndex;
+ this.position = new Point(position);
+ this.sourceContainerBounds = new Rect(sourceContainerBounds);
+ this.windowConfiguration = windowConfig;
+ }
+
+ public RemoteAnimationTarget(Parcel in) {
+ taskId = in.readInt();
+ mode = in.readInt();
+ leash = in.readParcelable(null);
+ isTranslucent = in.readBoolean();
+ clipRect = in.readParcelable(null);
+ prefixOrderIndex = in.readInt();
+ position = in.readParcelable(null);
+ sourceContainerBounds = in.readParcelable(null);
+ windowConfiguration = in.readParcelable(null);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(taskId);
+ dest.writeInt(mode);
+ dest.writeParcelable(leash, 0 /* flags */);
+ dest.writeBoolean(isTranslucent);
+ dest.writeParcelable(clipRect, 0 /* flags */);
+ dest.writeInt(prefixOrderIndex);
+ dest.writeParcelable(position, 0 /* flags */);
+ dest.writeParcelable(sourceContainerBounds, 0 /* flags */);
+ dest.writeParcelable(windowConfiguration, 0 /* flags */);
+ }
+
+ public static final Creator<RemoteAnimationTarget> CREATOR
+ = new Creator<RemoteAnimationTarget>() {
+ public RemoteAnimationTarget createFromParcel(Parcel in) {
+ return new RemoteAnimationTarget(in);
+ }
+
+ public RemoteAnimationTarget[] newArray(int size) {
+ return new RemoteAnimationTarget[size];
+ }
+ };
+}
diff --git a/android/view/Surface.java b/android/view/Surface.java
index a417a4a0..8830c90a 100644
--- a/android/view/Surface.java
+++ b/android/view/Surface.java
@@ -182,6 +182,11 @@ public class Surface implements Parcelable {
* SurfaceTexture}, which can attach them to an OpenGL ES texture via {@link
* SurfaceTexture#updateTexImage}.
*
+ * Please note that holding onto the Surface created here is not enough to
+ * keep the provided SurfaceTexture from being reclaimed. In that sense,
+ * the Surface will act like a
+ * {@link java.lang.ref.WeakReference weak reference} to the SurfaceTexture.
+ *
* @param surfaceTexture The {@link SurfaceTexture} that is updated by this
* Surface.
* @throws OutOfResourcesException if the surface could not be created.
@@ -278,6 +283,7 @@ public class Surface implements Parcelable {
*/
public long getNextFrameNumber() {
synchronized (mLock) {
+ checkNotReleasedLocked();
return nativeGetNextFrameNumber(mNativeObject);
}
}
diff --git a/android/view/SurfaceControl.java b/android/view/SurfaceControl.java
index 268e460d..bd7f8e54 100644
--- a/android/view/SurfaceControl.java
+++ b/android/view/SurfaceControl.java
@@ -16,20 +16,22 @@
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 static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+import static android.view.SurfaceControlProto.HASH_CODE;
+import static android.view.SurfaceControlProto.NAME;
import android.annotation.Size;
import android.graphics.Bitmap;
import android.graphics.GraphicBuffer;
-import android.graphics.PixelFormat;
import android.graphics.Matrix;
+import android.graphics.PixelFormat;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
@@ -40,11 +42,13 @@ import android.os.Process;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Log;
+import android.util.proto.ProtoOutputStream;
import android.view.Surface.OutOfResourcesException;
import com.android.internal.annotations.GuardedBy;
import dalvik.system.CloseGuard;
+
import libcore.util.NativeAllocationRegistry;
import java.io.Closeable;
@@ -628,6 +632,21 @@ public class SurfaceControl implements Parcelable {
nativeWriteToParcel(mNativeObject, dest);
}
+ /**
+ * Write to a protocol buffer output stream. Protocol buffer message definition is at {@link
+ * android.view.SurfaceControlProto}.
+ *
+ * @param proto Stream to write the SurfaceControl object to.
+ * @param fieldId Field Id of the SurfaceControl as defined in the parent message.
+ * @hide
+ */
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(HASH_CODE, System.identityHashCode(this));
+ proto.write(NAME, mName);
+ proto.end(token);
+ }
+
public static final Creator<SurfaceControl> CREATOR
= new Creator<SurfaceControl>() {
public SurfaceControl createFromParcel(Parcel in) {
diff --git a/android/view/ThreadedRenderer.java b/android/view/ThreadedRenderer.java
index 6a8f8b12..8b730f28 100644
--- a/android/view/ThreadedRenderer.java
+++ b/android/view/ThreadedRenderer.java
@@ -969,8 +969,6 @@ public final class ThreadedRenderer {
mInitialized = true;
mAppContext = context.getApplicationContext();
- // b/68769804: For low FPS experiments.
- setFPSDivisor(SystemProperties.getInt(DEBUG_FPS_DIVISOR, 1));
initSched(renderProxy);
initGraphicsStats();
}
@@ -1025,9 +1023,7 @@ public final class ThreadedRenderer {
/** b/68769804: For low FPS experiments. */
public static void setFPSDivisor(int divisor) {
- if (divisor <= 0) divisor = 1;
- Choreographer.getInstance().setFPSDivisor(divisor);
- nHackySetRTAnimationsEnabled(divisor == 1);
+ nHackySetRTAnimationsEnabled(divisor <= 1);
}
/** Not actually public - internal use only. This doc to make lint happy */
diff --git a/android/view/View.java b/android/view/View.java
index cc63a62c..3d6a6fee 100644
--- a/android/view/View.java
+++ b/android/view/View.java
@@ -16,6 +16,8 @@
package android.view;
+import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED;
+
import static java.lang.Math.max;
import android.animation.AnimatorInflater;
@@ -3226,6 +3228,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
private static final int PFLAG3_SCREEN_READER_FOCUSABLE = 0x10000000;
+ /**
+ * The last aggregated visibility. Used to detect when it truly changes.
+ */
+ private static final int PFLAG3_AGGREGATED_VISIBLE = 0x20000000;
+
/* End of masks for mPrivateFlags3 */
/**
@@ -3387,6 +3394,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* decorations when they are shown. You can perform layout of your inner
* UI elements to account for non-fullscreen system UI through the
* {@link #fitSystemWindows(Rect)} method.
+ *
+ * <p>Note: on displays that have a {@link DisplayCutout}, the window may still be placed
+ * differently than if {@link #SYSTEM_UI_FLAG_FULLSCREEN} was set, if the
+ * window's {@link WindowManager.LayoutParams#layoutInDisplayCutoutMode
+ * layoutInDisplayCutoutMode} is
+ * {@link WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ * LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT}. To avoid this, use either of the other modes.
+ *
+ * @see WindowManager.LayoutParams#layoutInDisplayCutoutMode
+ * @see WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ * @see WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+ * @see WindowManager.LayoutParams#LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
*/
public static final int SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN = 0x00000400;
@@ -4045,6 +4064,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
private CharSequence mContentDescription;
/**
+ * If this view represents a distinct part of the window, it can have a title that labels the
+ * area.
+ */
+ private CharSequence mAccessibilityPaneTitle;
+
+ /**
* Specifies the id of a view for which this view serves as a label for
* accessibility purposes.
*/
@@ -4182,6 +4207,16 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
private static boolean sUseDefaultFocusHighlight;
+ /**
+ * True if zero-sized views can be focused.
+ */
+ private static boolean sCanFocusZeroSized;
+
+ /**
+ * Always assign focus if a focusable View is available.
+ */
+ private static boolean sAlwaysAssignFocus;
+
private String mTransitionName;
static class TintInfo {
@@ -4407,7 +4442,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
private CheckForLongPress mPendingCheckForLongPress;
private CheckForTap mPendingCheckForTap = null;
private PerformClick mPerformClick;
- private SendViewScrolledAccessibilityEvent mSendViewScrolledAccessibilityEvent;
private UnsetPressedState mUnsetPressedState;
@@ -4798,6 +4832,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
sThrowOnInvalidFloatProperties = targetSdkVersion >= Build.VERSION_CODES.P;
+ sCanFocusZeroSized = targetSdkVersion < Build.VERSION_CODES.P;
+
+ sAlwaysAssignFocus = targetSdkVersion < Build.VERSION_CODES.P;
+
sCompatibilityDone = true;
}
}
@@ -5402,6 +5440,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
setScreenReaderFocusable(a.getBoolean(attr, false));
}
break;
+ case R.styleable.View_accessibilityPaneTitle:
+ if (a.peekValue(attr) != null) {
+ setAccessibilityPaneTitle(a.getString(attr));
+ }
+ break;
}
}
@@ -6983,8 +7026,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* Called when this view wants to give up focus. If focus is cleared
* {@link #onFocusChanged(boolean, int, android.graphics.Rect)} is called.
* <p>
- * <strong>Note:</strong> When a View clears focus the framework is trying
- * to give focus to the first focusable View from the top. Hence, if this
+ * <strong>Note:</strong> When not in touch-mode, the framework will try to give focus
+ * to the first focusable View from the top after focus is cleared. Hence, if this
* View is the first from the top that can take focus, then all callbacks
* related to clearing focus will be invoked after which the framework will
* give focus to this view.
@@ -6995,7 +7038,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
System.out.println(this + " clearFocus()");
}
- clearFocusInternal(null, true, true);
+ final boolean refocus = sAlwaysAssignFocus || !isInTouchMode();
+ clearFocusInternal(null, true, refocus);
}
/**
@@ -7010,6 +7054,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
void clearFocusInternal(View focused, boolean propagate, boolean refocus) {
if ((mPrivateFlags & PFLAG_FOCUSED) != 0) {
mPrivateFlags &= ~PFLAG_FOCUSED;
+ clearParentsWantFocus();
if (propagate && mParent != null) {
mParent.clearChildFocus(this);
@@ -7156,7 +7201,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
if (gainFocus) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
} else {
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
}
@@ -7189,20 +7234,24 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
notifyEnterOrExitForAutoFillIfNeeded(gainFocus);
}
- private void notifyEnterOrExitForAutoFillIfNeeded(boolean enter) {
- if (isAutofillable() && isAttachedToWindow()) {
+ /** @hide */
+ public void notifyEnterOrExitForAutoFillIfNeeded(boolean enter) {
+ if (canNotifyAutofillEnterExitEvent()) {
AutofillManager afm = getAutofillManager();
if (afm != null) {
- if (enter && hasWindowFocus() && isFocused()) {
+ if (enter && isFocused()) {
// We have not been laid out yet, hence cannot evaluate
// whether this view is visible to the user, we will do
// the evaluation once layout is complete.
if (!isLaidOut()) {
mPrivateFlags3 |= PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
} else if (isVisibleToUser()) {
+ // TODO This is a potential problem that View gets focus before it's visible
+ // to User. Ideally View should handle the event when isVisibleToUser()
+ // becomes true where it should issue notifyViewEntered().
afm.notifyViewEntered(this);
}
- } else if (!hasWindowFocus() || !isFocused()) {
+ } else if (!isFocused()) {
afm.notifyViewExited(this);
}
}
@@ -7210,6 +7259,34 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
/**
+ * If this view is a visually distinct portion of a window, for example the content view of
+ * a fragment that is replaced, it is considered a pane for accessibility purposes. In order
+ * for accessibility services to understand the views role, and to announce its title as
+ * appropriate, such views should have pane titles.
+ *
+ * @param accessibilityPaneTitle The pane's title.
+ *
+ * {@see AccessibilityNodeInfo#setPaneTitle(CharSequence)}
+ */
+ public void setAccessibilityPaneTitle(CharSequence accessibilityPaneTitle) {
+ if (!TextUtils.equals(accessibilityPaneTitle, mAccessibilityPaneTitle)) {
+ mAccessibilityPaneTitle = accessibilityPaneTitle;
+ notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_TITLE);
+ }
+ }
+
+ /**
+ * Get the title of the pane for purposes of accessibility.
+ *
+ * @return The current pane title.
+ *
+ * {@see #setAccessibilityPaneTitle}.
+ */
+ public CharSequence getAccessibilityPaneTitle() {
+ return mAccessibilityPaneTitle;
+ }
+
+ /**
* Sends an accessibility event of the given type. If accessibility is
* not enabled this method has no effect. The default implementation calls
* {@link #onInitializeAccessibilityEvent(AccessibilityEvent)} first
@@ -7308,7 +7385,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* @hide
*/
public void sendAccessibilityEventUncheckedInternal(AccessibilityEvent event) {
- if (!isShown()) {
+ // Panes disappearing are relevant even if though the view is no longer visible.
+ boolean isWindowStateChanged =
+ (event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ boolean isWindowDisappearedEvent = isWindowStateChanged && ((event.getContentChangeTypes()
+ & AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED) != 0);
+ if (!isShown() && !isWindowDisappearedEvent) {
return;
}
onInitializeAccessibilityEvent(event);
@@ -7416,6 +7498,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* @hide
*/
public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+ if ((event.getEventType() == AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED)
+ && !TextUtils.isEmpty(getAccessibilityPaneTitle())) {
+ event.getText().add(getAccessibilityPaneTitle());
+ }
}
/**
@@ -8237,6 +8323,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
&& getAutofillViewId() > LAST_APP_AUTOFILL_ID;
}
+ /** @hide */
+ public boolean canNotifyAutofillEnterExitEvent() {
+ return isAutofillable() && isAttachedToWindow();
+ }
+
private void populateVirtualStructure(ViewStructure structure,
AccessibilityNodeProvider provider, AccessibilityNodeInfo info) {
structure.setId(AccessibilityNodeInfo.getVirtualDescendantId(info.getSourceNodeId()),
@@ -8459,6 +8550,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
info.setLongClickable(isLongClickable());
info.setContextClickable(isContextClickable());
info.setLiveRegion(getAccessibilityLiveRegion());
+ if ((mTooltipInfo != null) && (mTooltipInfo.mTooltipText != null)) {
+ info.setTooltipText(mTooltipInfo.mTooltipText);
+ info.addAction((mTooltipInfo.mTooltipPopup == null)
+ ? AccessibilityNodeInfo.AccessibilityAction.ACTION_SHOW_TOOLTIP
+ : AccessibilityNodeInfo.AccessibilityAction.ACTION_HIDE_TOOLTIP);
+ }
// TODO: These make sense only if we are in an AdapterView but all
// views can be selected. Maybe from accessibility perspective
@@ -8506,6 +8603,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
info.addAction(AccessibilityAction.ACTION_SHOW_ON_SCREEN);
populateAccessibilityNodeInfoDrawingOrderInParent(info);
+ info.setPaneTitle(mAccessibilityPaneTitle);
}
/**
@@ -8826,9 +8924,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
final boolean nonEmptyDesc = contentDescription != null && contentDescription.length() > 0;
if (nonEmptyDesc && getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
} else {
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION);
}
}
@@ -8861,8 +8959,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
return;
}
mAccessibilityTraversalBeforeId = beforeId;
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
/**
@@ -8905,8 +9002,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
return;
}
mAccessibilityTraversalAfterId = afterId;
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
/**
@@ -8948,8 +9044,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
&& mID == View.NO_ID) {
mID = generateViewId();
}
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
/**
@@ -10046,6 +10141,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
/**
+ * @return {@code true} if laid-out and not about to do another layout.
+ */
+ boolean isLayoutValid() {
+ return isLaidOut() && ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == 0);
+ }
+
+ /**
* If this view doesn't do any drawing on its own, set this flag to
* allow further optimizations. By default, this flag is not set on
* View, but could be set on some View subclasses such as ViewGroup.
@@ -10442,8 +10544,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
if (pflags3 != mPrivateFlags3) {
mPrivateFlags3 = pflags3;
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
}
@@ -10817,7 +10918,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
if (views == null) {
return;
}
- if (!isFocusable() || !isEnabled()) {
+ if (!canTakeFocus()) {
return;
}
if ((focusableMode & FOCUSABLES_TOUCH_MODE) == FOCUSABLES_TOUCH_MODE
@@ -11031,8 +11132,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* descendants.
*
* A view will not actually take focus if it is not focusable ({@link #isFocusable} returns
- * false), or if it is focusable and it is not focusable in touch mode
- * ({@link #isFocusableInTouchMode}) while the device is in touch mode.
+ * false), or if it can't be focused due to other conditions (not focusable in touch mode
+ * ({@link #isFocusableInTouchMode}) while the device is in touch mode, not visible, not
+ * enabled, or has no size).
*
* See also {@link #focusSearch(int)}, which is what you call to say that you
* have focus, and you want your parent to look for the next one.
@@ -11139,9 +11241,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
private boolean requestFocusNoSearch(int direction, Rect previouslyFocusedRect) {
// need to be focusable
- if ((mViewFlags & FOCUSABLE) != FOCUSABLE
- || (mViewFlags & VISIBILITY_MASK) != VISIBLE
- || (mViewFlags & ENABLED_MASK) != ENABLED) {
+ if (!canTakeFocus()) {
return false;
}
@@ -11156,10 +11256,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
return false;
}
+ if (!isLayoutValid()) {
+ mPrivateFlags |= PFLAG_WANTS_FOCUS;
+ } else {
+ clearParentsWantFocus();
+ }
+
handleFocusGainInternal(direction, previouslyFocusedRect);
return true;
}
+ void clearParentsWantFocus() {
+ if (mParent instanceof View) {
+ ((View) mParent).mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
+ ((View) mParent).clearParentsWantFocus();
+ }
+ }
+
/**
* Call this to try to give focus to a specific view or to one of its descendants. This is a
* special variant of {@link #requestFocus() } that will allow views that are not focusable in
@@ -11261,8 +11374,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
mPrivateFlags2 &= ~PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK;
mPrivateFlags2 |= (mode << PFLAG2_ACCESSIBILITY_LIVE_REGION_SHIFT)
& PFLAG2_ACCESSIBILITY_LIVE_REGION_MASK;
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
}
@@ -11319,10 +11431,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
mPrivateFlags2 |= (mode << PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_SHIFT)
& PFLAG2_IMPORTANT_FOR_ACCESSIBILITY_MASK;
if (!maySkipNotify || oldIncludeForAccessibility != includeForAccessibility()) {
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
} else {
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
}
}
@@ -11380,6 +11491,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* {@link #getAccessibilityLiveRegion()} is not
* {@link #ACCESSIBILITY_LIVE_REGION_NONE}.
* </ul>
+ * <li>Has an accessibility pane title, see {@link #setAccessibilityPaneTitle}</li>
* </ol>
*
* @return Whether the view is exposed for accessibility.
@@ -11406,7 +11518,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
return mode == IMPORTANT_FOR_ACCESSIBILITY_YES || isActionableForAccessibility()
|| hasListenersForAccessibility() || getAccessibilityNodeProvider() != null
- || getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE;
+ || getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE
+ || (mAccessibilityPaneTitle != null);
}
/**
@@ -11496,25 +11609,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*
* @hide
*/
- public void notifyViewAccessibilityStateChangedIfNeeded(int changeType) {
- if (!AccessibilityManager.getInstance(mContext).isEnabled() || mAttachInfo == null) {
- return;
- }
- // If this is a live region, we should send a subtree change event
- // from this view immediately. Otherwise, we can let it propagate up.
- if (getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE) {
- final AccessibilityEvent event = AccessibilityEvent.obtain();
- event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
- event.setContentChangeTypes(changeType);
- sendAccessibilityEventUnchecked(event);
- } else if (mParent != null) {
- try {
- mParent.notifySubtreeAccessibilityStateChanged(this, this, changeType);
- } catch (AbstractMethodError e) {
- Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
- " does not fully implement ViewParent", e);
- }
- }
+ public void notifyAccessibilityStateChanged(int changeType) {
+ notifyAccessibilityStateChanged(this, changeType);
}
/**
@@ -11528,22 +11624,42 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*
* @hide
*/
- public void notifySubtreeAccessibilityStateChangedIfNeeded() {
+ public void notifyAccessibilitySubtreeChanged() {
+ if ((mPrivateFlags2 & PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED) == 0) {
+ mPrivateFlags2 |= PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED;
+ notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+ }
+ }
+
+ void notifyAccessibilityStateChanged(View source, int changeType) {
if (!AccessibilityManager.getInstance(mContext).isEnabled() || mAttachInfo == null) {
return;
}
- if ((mPrivateFlags2 & PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED) == 0) {
- mPrivateFlags2 |= PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED;
+ // Changes to views with a pane title count as window state changes, as the pane title
+ // marks them as significant parts of the UI.
+ if (!TextUtils.isEmpty(getAccessibilityPaneTitle())) {
+ final AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+ event.setContentChangeTypes(changeType);
+ onPopulateAccessibilityEvent(event);
if (mParent != null) {
try {
- mParent.notifySubtreeAccessibilityStateChanged(
- this, this, AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+ mParent.requestSendAccessibilityEvent(this, event);
} catch (AbstractMethodError e) {
- Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
- " does not fully implement ViewParent", e);
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName()
+ + " does not fully implement ViewParent", e);
}
}
}
+
+ if (mParent != null) {
+ try {
+ mParent.notifySubtreeAccessibilityStateChanged(this, source, changeType);
+ } catch (AbstractMethodError e) {
+ Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName()
+ + " does not fully implement ViewParent", e);
+ }
+ }
}
/**
@@ -11563,8 +11679,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
/**
* Reset the flag indicating the accessibility state of the subtree rooted
* at this view changed.
+ *
+ * @hide
*/
- void resetSubtreeAccessibilityStateChanged() {
+ public void resetSubtreeAccessibilityStateChanged() {
mPrivateFlags2 &= ~PFLAG2_SUBTREE_ACCESSIBILITY_STATE_CHANGED;
}
@@ -11725,8 +11843,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|| getAccessibilitySelectionEnd() != end)
&& (start == end)) {
setAccessibilitySelection(start, end);
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
return true;
}
} break;
@@ -11743,6 +11860,21 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
return true;
}
} break;
+ case R.id.accessibilityActionShowTooltip: {
+ if ((mTooltipInfo != null) && (mTooltipInfo.mTooltipPopup != null)) {
+ // Tooltip already showing
+ return false;
+ }
+ return showLongClickTooltip(0, 0);
+ }
+ case R.id.accessibilityActionHideTooltip: {
+ if ((mTooltipInfo == null) || (mTooltipInfo.mTooltipPopup == null)) {
+ // No tooltip showing
+ return false;
+ }
+ hideTooltip();
+ return true;
+ }
}
return false;
}
@@ -12347,8 +12479,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
imm.focusIn(this);
}
- notifyEnterOrExitForAutoFillIfNeeded(hasWindowFocus);
-
refreshDrawableState();
}
@@ -12467,6 +12597,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
@CallSuper
public void onVisibilityAggregated(boolean isVisible) {
+ // Update our internal visibility tracking so we can detect changes
+ boolean oldVisible = (mPrivateFlags3 & PFLAG3_AGGREGATED_VISIBLE) != 0;
+ mPrivateFlags3 = isVisible ? (mPrivateFlags3 | PFLAG3_AGGREGATED_VISIBLE)
+ : (mPrivateFlags3 & ~PFLAG3_AGGREGATED_VISIBLE);
if (isVisible && mAttachInfo != null) {
initialAwakenScrollBars();
}
@@ -12507,6 +12641,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
}
}
+ if (!TextUtils.isEmpty(getAccessibilityPaneTitle())) {
+ if (isVisible != oldVisible) {
+ notifyAccessibilityStateChanged(isVisible
+ ? AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_APPEARED
+ : AccessibilityEvent.CONTENT_CHANGE_TYPE_PANE_DISAPPEARED);
+ }
+ }
}
/**
@@ -13531,6 +13672,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
mAttachInfo.mUnbufferedDispatchRequested = true;
}
+ private boolean canTakeFocus() {
+ return ((mViewFlags & VISIBILITY_MASK) == VISIBLE)
+ && ((mViewFlags & FOCUSABLE) == FOCUSABLE)
+ && ((mViewFlags & ENABLED_MASK) == ENABLED)
+ && (sCanFocusZeroSized || !isLayoutValid() || (mBottom > mTop) && (mRight > mLeft));
+ }
+
/**
* Set flags controlling behavior of this view.
*
@@ -13550,6 +13698,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
return;
}
int privateFlags = mPrivateFlags;
+ boolean shouldNotifyFocusableAvailable = false;
// If focusable is auto, update the FOCUSABLE bit.
int focusableChangedByAuto = 0;
@@ -13588,7 +13737,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|| focusableChangedByAuto == 0
|| viewRootImpl == null
|| viewRootImpl.mThread == Thread.currentThread()) {
- mParent.focusableViewAvailable(this);
+ shouldNotifyFocusableAvailable = true;
}
}
}
@@ -13611,10 +13760,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
// about in case nothing has focus. even if this specific view
// isn't focusable, it may contain something that is, so let
// the root view try to give this focus if nothing else does.
- if ((mParent != null) && ((mViewFlags & ENABLED_MASK) == ENABLED)
- && (mBottom > mTop) && (mRight > mLeft)) {
- mParent.focusableViewAvailable(this);
- }
+ shouldNotifyFocusableAvailable = true;
}
}
@@ -13623,17 +13769,18 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
// a view becoming enabled should notify the parent as long as the view is also
// visible and the parent wasn't already notified by becoming visible during this
// setFlags invocation.
- if ((mViewFlags & VISIBILITY_MASK) == VISIBLE
- && ((changed & VISIBILITY_MASK) == 0)) {
- if ((mParent != null) && (mViewFlags & ENABLED_MASK) == ENABLED) {
- mParent.focusableViewAvailable(this);
- }
- }
+ shouldNotifyFocusableAvailable = true;
} else {
if (hasFocus()) clearFocus();
}
}
+ if (shouldNotifyFocusableAvailable) {
+ if (mParent != null && canTakeFocus()) {
+ mParent.focusableViewAvailable(this);
+ }
+ }
+
/* Check if the GONE bit has changed */
if ((changed & GONE) != 0) {
needGlobalAttributesUpdate(false);
@@ -13713,7 +13860,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
((!(mParent instanceof ViewGroup)) || ((ViewGroup) mParent).isShown())) {
dispatchVisibilityAggregated(newVisibility == VISIBLE);
}
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -13759,14 +13906,12 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
|| (changed & CLICKABLE) != 0 || (changed & LONG_CLICKABLE) != 0
|| (changed & CONTEXT_CLICKABLE) != 0) {
if (oldIncludeForAccessibility != includeForAccessibility()) {
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
} else {
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
} else if ((changed & ENABLED_MASK) != 0) {
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
}
}
@@ -13800,10 +13945,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
* @param oldt Previous vertical scroll origin.
*/
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
- if (AccessibilityManager.getInstance(mContext).isEnabled()) {
- postSendViewScrolledAccessibilityEventCallback(l - oldl, t - oldt);
+ ViewRootImpl root = getViewRootImpl();
+ if (root != null) {
+ root.getAccessibilityState()
+ .getSendViewScrolledAccessibilityEvent()
+ .post(this, /* dx */ l - oldl, /* dy */ t - oldt);
}
mBackgroundSizeChanged = true;
@@ -14199,7 +14347,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -14243,7 +14391,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -14287,7 +14435,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -14324,7 +14472,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -14361,7 +14509,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -14564,7 +14712,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
if (mTransformationInfo.mAlpha != alpha) {
// Report visibility changes, which can affect children, to accessibility
if ((alpha == 0) ^ (mTransformationInfo.mAlpha == 0)) {
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
mTransformationInfo.mAlpha = alpha;
if (onSetAlpha((int) (alpha * 255))) {
@@ -15066,7 +15214,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -15100,7 +15248,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
invalidateViewProperty(false, true);
invalidateParentIfNeededAndWasQuickRejected();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -15270,7 +15418,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
public void invalidateOutline() {
rebuildOutline();
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
invalidateViewProperty(false, false);
}
@@ -15465,7 +15613,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
invalidateParentIfNeeded();
}
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -15513,7 +15661,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
invalidateParentIfNeeded();
}
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
}
@@ -16391,18 +16539,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
/**
- * Post a callback to send a {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} event.
- * This event is sent at most once every
- * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}.
- */
- private void postSendViewScrolledAccessibilityEventCallback(int dx, int dy) {
- if (mSendViewScrolledAccessibilityEvent == null) {
- mSendViewScrolledAccessibilityEvent = new SendViewScrolledAccessibilityEvent();
- }
- mSendViewScrolledAccessibilityEvent.post(dx, dy);
- }
-
- /**
* Called by a parent to request that a child update its values for mScrollX
* and mScrollY if necessary. This will typically be done if the child is
* animating a scroll using a {@link android.widget.Scroller Scroller}
@@ -17657,7 +17793,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
removeUnsetPressCallback();
removeLongPressCallback();
removePerformClickCallback();
- cancel(mSendViewScrolledAccessibilityEvent);
+ if (mAttachInfo != null
+ && mAttachInfo.mViewRootImpl.mAccessibilityState != null
+ && mAttachInfo.mViewRootImpl.mAccessibilityState.isScrollEventSenderInitialized()) {
+ mAttachInfo.mViewRootImpl.mAccessibilityState
+ .getSendViewScrolledAccessibilityEvent()
+ .cancelIfPendingFor(this);
+ }
stopNestedScroll();
// Anything that started animating right before detach should already
@@ -17761,6 +17903,15 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
/**
+ * Return the window this view is currently attached to. Used in
+ * {@link android.app.ActivityView} to communicate with WM.
+ * @hide
+ */
+ protected IWindow getWindow() {
+ return mAttachInfo != null ? mAttachInfo.mWindow : null;
+ }
+
+ /**
* Return the visibility value of the least visible component passed.
*/
int combineVisibility(int vis1, int vis2) {
@@ -18936,7 +19087,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*
* @hide
*/
- public Bitmap createSnapshot(Bitmap.Config quality, int backgroundColor, boolean skipChildren) {
+ public Bitmap createSnapshot(ViewDebug.CanvasProvider canvasProvider, boolean skipChildren) {
int width = mRight - mLeft;
int height = mBottom - mTop;
@@ -18945,71 +19096,48 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
width = (int) ((width * scale) + 0.5f);
height = (int) ((height * scale) + 0.5f);
- Bitmap bitmap = Bitmap.createBitmap(mResources.getDisplayMetrics(),
- width > 0 ? width : 1, height > 0 ? height : 1, quality);
- if (bitmap == null) {
- throw new OutOfMemoryError();
- }
-
- Resources resources = getResources();
- if (resources != null) {
- bitmap.setDensity(resources.getDisplayMetrics().densityDpi);
- }
+ Canvas oldCanvas = null;
+ try {
+ Canvas canvas = canvasProvider.getCanvas(this,
+ width > 0 ? width : 1, height > 0 ? height : 1);
- Canvas canvas;
- if (attachInfo != null) {
- canvas = attachInfo.mCanvas;
- if (canvas == null) {
- canvas = new Canvas();
+ if (attachInfo != null) {
+ oldCanvas = attachInfo.mCanvas;
+ // Temporarily clobber the cached Canvas in case one of our children
+ // is also using a drawing cache. Without this, the children would
+ // steal the canvas by attaching their own bitmap to it and bad, bad
+ // things would happen (invisible views, corrupted drawings, etc.)
+ attachInfo.mCanvas = null;
}
- canvas.setBitmap(bitmap);
- // Temporarily clobber the cached Canvas in case one of our children
- // is also using a drawing cache. Without this, the children would
- // steal the canvas by attaching their own bitmap to it and bad, bad
- // things would happen (invisible views, corrupted drawings, etc.)
- attachInfo.mCanvas = null;
- } else {
- // This case should hopefully never or seldom happen
- canvas = new Canvas(bitmap);
- }
- boolean enabledHwBitmapsInSwMode = canvas.isHwBitmapsInSwModeEnabled();
- canvas.setHwBitmapsInSwModeEnabled(true);
- if ((backgroundColor & 0xff000000) != 0) {
- bitmap.eraseColor(backgroundColor);
- }
- computeScroll();
- final int restoreCount = canvas.save();
- canvas.scale(scale, scale);
- canvas.translate(-mScrollX, -mScrollY);
+ computeScroll();
+ final int restoreCount = canvas.save();
+ canvas.scale(scale, scale);
+ canvas.translate(-mScrollX, -mScrollY);
- // Temporarily remove the dirty mask
- int flags = mPrivateFlags;
- mPrivateFlags &= ~PFLAG_DIRTY_MASK;
+ // Temporarily remove the dirty mask
+ int flags = mPrivateFlags;
+ mPrivateFlags &= ~PFLAG_DIRTY_MASK;
- // Fast path for layouts with no backgrounds
- if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
- dispatchDraw(canvas);
- drawAutofilledHighlight(canvas);
- if (mOverlay != null && !mOverlay.isEmpty()) {
- mOverlay.getOverlayView().draw(canvas);
+ // Fast path for layouts with no backgrounds
+ if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
+ dispatchDraw(canvas);
+ drawAutofilledHighlight(canvas);
+ if (mOverlay != null && !mOverlay.isEmpty()) {
+ mOverlay.getOverlayView().draw(canvas);
+ }
+ } else {
+ draw(canvas);
}
- } else {
- draw(canvas);
- }
-
- mPrivateFlags = flags;
- canvas.restoreToCount(restoreCount);
- canvas.setBitmap(null);
- canvas.setHwBitmapsInSwModeEnabled(enabledHwBitmapsInSwMode);
-
- if (attachInfo != null) {
- // Restore the cached Canvas for our siblings
- attachInfo.mCanvas = canvas;
+ mPrivateFlags = flags;
+ canvas.restoreToCount(restoreCount);
+ return canvasProvider.createBitmap();
+ } finally {
+ if (oldCanvas != null) {
+ attachInfo.mCanvas = oldCanvas;
+ }
}
-
- return bitmap;
}
/**
@@ -20160,15 +20288,58 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
}
+ final boolean wasLayoutValid = isLayoutValid();
+
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
+ if (!wasLayoutValid && isFocused()) {
+ mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
+ if (canTakeFocus()) {
+ // We have a robust focus, so parents should no longer be wanting focus.
+ clearParentsWantFocus();
+ } else if (!getViewRootImpl().isInLayout()) {
+ // This is a weird case. Most-likely the user, rather than ViewRootImpl, called
+ // layout. In this case, there's no guarantee that parent layouts will be evaluated
+ // and thus the safest action is to clear focus here.
+ clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
+ clearParentsWantFocus();
+ } else if (!hasParentWantsFocus()) {
+ // original requestFocus was likely on this view directly, so just clear focus
+ clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
+ }
+ // otherwise, we let parents handle re-assigning focus during their layout passes.
+ } else if ((mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
+ mPrivateFlags &= ~PFLAG_WANTS_FOCUS;
+ View focused = findFocus();
+ if (focused != null) {
+ // Try to restore focus as close as possible to our starting focus.
+ if (!restoreDefaultFocus() && !hasParentWantsFocus()) {
+ // Give up and clear focus once we've reached the top-most parent which wants
+ // focus.
+ focused.clearFocusInternal(null, /* propagate */ true, /* refocus */ false);
+ }
+ }
+ }
+
if ((mPrivateFlags3 & PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT) != 0) {
mPrivateFlags3 &= ~PFLAG3_NOTIFY_AUTOFILL_ENTER_ON_LAYOUT;
notifyEnterOrExitForAutoFillIfNeeded(true);
}
}
+ private boolean hasParentWantsFocus() {
+ ViewParent parent = mParent;
+ while (parent instanceof ViewGroup) {
+ ViewGroup pv = (ViewGroup) parent;
+ if ((pv.mPrivateFlags & PFLAG_WANTS_FOCUS) != 0) {
+ return true;
+ }
+ parent = pv.mParent;
+ }
+ return false;
+ }
+
/**
* Called from layout when this view should
* assign a size and position to each of its children.
@@ -20256,7 +20427,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
mForegroundInfo.mBoundsChanged = true;
}
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
return changed;
}
@@ -20275,6 +20446,23 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
mOverlay.getOverlayView().setRight(newWidth);
mOverlay.getOverlayView().setBottom(newHeight);
}
+ // If this isn't laid out yet, focus assignment will be handled during the "deferment/
+ // backtracking" of requestFocus during layout, so don't touch focus here.
+ if (!sCanFocusZeroSized && isLayoutValid()) {
+ if (newWidth <= 0 || newHeight <= 0) {
+ if (hasFocus()) {
+ clearFocus();
+ if (mParent instanceof ViewGroup) {
+ ((ViewGroup) mParent).clearFocusedInCluster();
+ }
+ }
+ clearAccessibilityFocus();
+ } else if (oldWidth <= 0 || oldHeight <= 0) {
+ if (mParent != null && canTakeFocus()) {
+ mParent.focusableViewAvailable(this);
+ }
+ }
+ }
rebuildOutline();
}
@@ -21683,8 +21871,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
if (selected) {
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
} else {
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
}
}
@@ -22038,7 +22225,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*
* @param id the ID to search for
* @return a view with given ID if found, or {@code null} otherwise
- * @see View#findViewById(int)
+ * @see View#requireViewById(int)
*/
@Nullable
public final <T extends View> T findViewById(@IdRes int id) {
@@ -22049,6 +22236,29 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
/**
+ * Finds the first descendant view with the given ID, the view itself if the ID matches
+ * {@link #getId()}, or throws an IllegalArgumentException if the ID is invalid or there is no
+ * matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#findViewById(int)
+ */
+ @NonNull
+ public final <T extends View> T requireViewById(@IdRes int id) {
+ T view = findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this View");
+ }
+ return view;
+ }
+
+ /**
* Finds a view by its unuque and stable accessibility id.
*
* @param accessibilityId The searched accessibility id.
@@ -23391,15 +23601,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
data.prepareToLeaveProcess((flags & View.DRAG_FLAG_GLOBAL) != 0);
}
- boolean okay = false;
-
Point shadowSize = new Point();
Point shadowTouchPoint = new Point();
shadowBuilder.onProvideShadowMetrics(shadowSize, shadowTouchPoint);
- if ((shadowSize.x < 0) || (shadowSize.y < 0) ||
- (shadowTouchPoint.x < 0) || (shadowTouchPoint.y < 0)) {
- throw new IllegalStateException("Drag shadow dimensions must not be negative");
+ if ((shadowSize.x <= 0) || (shadowSize.y <= 0)
+ || (shadowTouchPoint.x < 0) || (shadowTouchPoint.y < 0)) {
+ throw new IllegalStateException("Drag shadow dimensions must be positive");
}
if (ViewDebug.DEBUG_DRAG) {
@@ -23410,40 +23618,50 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
mAttachInfo.mDragSurface.release();
}
mAttachInfo.mDragSurface = new Surface();
+ mAttachInfo.mDragToken = null;
+
+ final ViewRootImpl root = mAttachInfo.mViewRootImpl;
+ final SurfaceSession session = new SurfaceSession(root.mSurface);
+ final SurfaceControl surface = new SurfaceControl.Builder(session)
+ .setName("drag surface")
+ .setSize(shadowSize.x, shadowSize.y)
+ .setFormat(PixelFormat.TRANSLUCENT)
+ .build();
try {
- mAttachInfo.mDragToken = mAttachInfo.mSession.prepareDrag(mAttachInfo.mWindow,
- flags, shadowSize.x, shadowSize.y, mAttachInfo.mDragSurface);
- if (ViewDebug.DEBUG_DRAG) Log.d(VIEW_LOG_TAG, "prepareDrag returned token="
- + mAttachInfo.mDragToken + " surface=" + mAttachInfo.mDragSurface);
- if (mAttachInfo.mDragToken != null) {
- Canvas canvas = mAttachInfo.mDragSurface.lockCanvas(null);
- try {
- canvas.drawColor(0, PorterDuff.Mode.CLEAR);
- shadowBuilder.onDrawShadow(canvas);
- } finally {
- mAttachInfo.mDragSurface.unlockCanvasAndPost(canvas);
- }
-
- final ViewRootImpl root = getViewRootImpl();
+ mAttachInfo.mDragSurface.copyFrom(surface);
+ final Canvas canvas = mAttachInfo.mDragSurface.lockCanvas(null);
+ try {
+ canvas.drawColor(0, PorterDuff.Mode.CLEAR);
+ shadowBuilder.onDrawShadow(canvas);
+ } finally {
+ mAttachInfo.mDragSurface.unlockCanvasAndPost(canvas);
+ }
- // Cache the local state object for delivery with DragEvents
- root.setLocalDragState(myLocalState);
+ // Cache the local state object for delivery with DragEvents
+ root.setLocalDragState(myLocalState);
- // repurpose 'shadowSize' for the last touch point
- root.getLastTouchPoint(shadowSize);
+ // repurpose 'shadowSize' for the last touch point
+ root.getLastTouchPoint(shadowSize);
- okay = mAttachInfo.mSession.performDrag(mAttachInfo.mWindow, mAttachInfo.mDragToken,
- root.getLastTouchSource(), shadowSize.x, shadowSize.y,
- shadowTouchPoint.x, shadowTouchPoint.y, data);
- if (ViewDebug.DEBUG_DRAG) Log.d(VIEW_LOG_TAG, "performDrag returned " + okay);
+ mAttachInfo.mDragToken = mAttachInfo.mSession.performDrag(
+ mAttachInfo.mWindow, flags, surface, root.getLastTouchSource(),
+ shadowSize.x, shadowSize.y, shadowTouchPoint.x, shadowTouchPoint.y, data);
+ if (ViewDebug.DEBUG_DRAG) {
+ Log.d(VIEW_LOG_TAG, "performDrag returned " + mAttachInfo.mDragToken);
}
+
+ return mAttachInfo.mDragToken != null;
} catch (Exception e) {
Log.e(VIEW_LOG_TAG, "Unable to initiate drag", e);
- mAttachInfo.mDragSurface.destroy();
- mAttachInfo.mDragSurface = null;
+ return false;
+ } finally {
+ if (mAttachInfo.mDragToken == null) {
+ mAttachInfo.mDragSurface.destroy();
+ mAttachInfo.mDragSurface = null;
+ root.setLocalDragState(null);
+ }
+ session.kill();
}
-
- return okay;
}
/**
@@ -26240,53 +26458,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
/**
- * Resuable callback for sending
- * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} accessibility event.
- */
- private class SendViewScrolledAccessibilityEvent implements Runnable {
- public volatile boolean mIsPending;
- public int mDeltaX;
- public int mDeltaY;
-
- public void post(int dx, int dy) {
- mDeltaX += dx;
- mDeltaY += dy;
- if (!mIsPending) {
- mIsPending = true;
- postDelayed(this, ViewConfiguration.getSendRecurringAccessibilityEventsInterval());
- }
- }
-
- @Override
- public void run() {
- if (AccessibilityManager.getInstance(mContext).isEnabled()) {
- AccessibilityEvent event = AccessibilityEvent.obtain(
- AccessibilityEvent.TYPE_VIEW_SCROLLED);
- event.setScrollDeltaX(mDeltaX);
- event.setScrollDeltaY(mDeltaY);
- sendAccessibilityEventUnchecked(event);
- }
- reset();
- }
-
- private void reset() {
- mIsPending = false;
- mDeltaX = 0;
- mDeltaY = 0;
- }
- }
-
- /**
- * Remove the pending callback for sending a
- * {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} accessibility event.
- */
- private void cancel(@Nullable SendViewScrolledAccessibilityEvent callback) {
- if (callback == null || !callback.mIsPending) return;
- removeCallbacks(callback);
- callback.reset();
- }
-
- /**
* <p>
* This class represents a delegate that can be registered in a {@link View}
* to enhance accessibility support via composition rather via inheritance.
@@ -26902,6 +27073,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
final boolean fromTouch = (mPrivateFlags3 & PFLAG3_FINGER_DOWN) == PFLAG3_FINGER_DOWN;
mTooltipInfo.mTooltipPopup.show(this, x, y, fromTouch, mTooltipInfo.mTooltipText);
mAttachInfo.mTooltipHost = this;
+ // The available accessibility actions have changed
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
return true;
}
@@ -26920,6 +27093,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
if (mAttachInfo != null) {
mAttachInfo.mTooltipHost = null;
}
+ // The available accessibility actions have changed
+ notifyAccessibilityStateChanged(CONTENT_CHANGE_TYPE_UNDEFINED);
}
private boolean showLongClickTooltip(int x, int y) {
@@ -26928,8 +27103,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
return showTooltip(x, y, true);
}
- private void showHoverTooltip() {
- showTooltip(mTooltipInfo.mAnchorX, mTooltipInfo.mAnchorY, false);
+ private boolean showHoverTooltip() {
+ return showTooltip(mTooltipInfo.mAnchorX, mTooltipInfo.mAnchorY, false);
}
boolean dispatchTooltipHoverEvent(MotionEvent event) {
diff --git a/android/view/ViewDebug.java b/android/view/ViewDebug.java
index afa94131..b09934e3 100644
--- a/android/view/ViewDebug.java
+++ b/android/view/ViewDebug.java
@@ -21,6 +21,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
+import android.graphics.Point;
import android.graphics.Rect;
import android.os.Debug;
import android.os.Handler;
@@ -773,16 +774,15 @@ public class ViewDebug {
final CountDownLatch latch = new CountDownLatch(1);
final Bitmap[] cache = new Bitmap[1];
- captureView.post(new Runnable() {
- public void run() {
- try {
- cache[0] = captureView.createSnapshot(
- Bitmap.Config.ARGB_8888, 0, skipChildren);
- } catch (OutOfMemoryError e) {
- Log.w("View", "Out of memory for bitmap");
- } finally {
- latch.countDown();
- }
+ captureView.post(() -> {
+ try {
+ CanvasProvider provider = captureView.isHardwareAccelerated()
+ ? new HardwareCanvasProvider() : new SoftwareCanvasProvider();
+ cache[0] = captureView.createSnapshot(provider, skipChildren);
+ } catch (OutOfMemoryError e) {
+ Log.w("View", "Out of memory for bitmap");
+ } finally {
+ latch.countDown();
}
});
@@ -1740,4 +1740,86 @@ public class ViewDebug {
}
});
}
+
+ /**
+ * @hide
+ */
+ public static class SoftwareCanvasProvider implements CanvasProvider {
+
+ private Canvas mCanvas;
+ private Bitmap mBitmap;
+ private boolean mEnabledHwBitmapsInSwMode;
+
+ @Override
+ public Canvas getCanvas(View view, int width, int height) {
+ mBitmap = Bitmap.createBitmap(view.getResources().getDisplayMetrics(),
+ width, height, Bitmap.Config.ARGB_8888);
+ if (mBitmap == null) {
+ throw new OutOfMemoryError();
+ }
+ mBitmap.setDensity(view.getResources().getDisplayMetrics().densityDpi);
+
+ if (view.mAttachInfo != null) {
+ mCanvas = view.mAttachInfo.mCanvas;
+ }
+ if (mCanvas == null) {
+ mCanvas = new Canvas();
+ }
+ mEnabledHwBitmapsInSwMode = mCanvas.isHwBitmapsInSwModeEnabled();
+ mCanvas.setBitmap(mBitmap);
+ return mCanvas;
+ }
+
+ @Override
+ public Bitmap createBitmap() {
+ mCanvas.setBitmap(null);
+ mCanvas.setHwBitmapsInSwModeEnabled(mEnabledHwBitmapsInSwMode);
+ return mBitmap;
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public static class HardwareCanvasProvider implements CanvasProvider {
+
+ private View mView;
+ private Point mSize;
+ private RenderNode mNode;
+ private DisplayListCanvas mCanvas;
+
+ @Override
+ public Canvas getCanvas(View view, int width, int height) {
+ mView = view;
+ mSize = new Point(width, height);
+ mNode = RenderNode.create("ViewDebug", mView);
+ mNode.setLeftTopRightBottom(0, 0, width, height);
+ mNode.setClipToBounds(false);
+ mCanvas = mNode.start(width, height);
+ return mCanvas;
+ }
+
+ @Override
+ public Bitmap createBitmap() {
+ mNode.end(mCanvas);
+ return ThreadedRenderer.createHardwareBitmap(mNode, mSize.x, mSize.y);
+ }
+ }
+
+ /**
+ * @hide
+ */
+ public interface CanvasProvider {
+
+ /**
+ * Returns a canvas which can be used to draw {@param view}
+ */
+ Canvas getCanvas(View view, int width, int height);
+
+ /**
+ * Creates a bitmap from previously returned canvas
+ * @return
+ */
+ Bitmap createBitmap();
+ }
}
diff --git a/android/view/ViewGroup.java b/android/view/ViewGroup.java
index 122df934..4631261e 100644
--- a/android/view/ViewGroup.java
+++ b/android/view/ViewGroup.java
@@ -57,6 +57,7 @@ import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.LayoutAnimationController;
import android.view.animation.Transformation;
+import android.view.autofill.Helper;
import com.android.internal.R;
@@ -3215,22 +3216,31 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
int descendantFocusability = getDescendantFocusability();
+ boolean result;
switch (descendantFocusability) {
case FOCUS_BLOCK_DESCENDANTS:
- return super.requestFocus(direction, previouslyFocusedRect);
+ result = super.requestFocus(direction, previouslyFocusedRect);
+ break;
case FOCUS_BEFORE_DESCENDANTS: {
final boolean took = super.requestFocus(direction, previouslyFocusedRect);
- return took ? took : onRequestFocusInDescendants(direction, previouslyFocusedRect);
+ result = took ? took : onRequestFocusInDescendants(direction,
+ previouslyFocusedRect);
+ break;
}
case FOCUS_AFTER_DESCENDANTS: {
final boolean took = onRequestFocusInDescendants(direction, previouslyFocusedRect);
- return took ? took : super.requestFocus(direction, previouslyFocusedRect);
+ result = took ? took : super.requestFocus(direction, previouslyFocusedRect);
+ break;
}
default:
throw new IllegalStateException("descendant focusability must be "
+ "one of FOCUS_BEFORE_DESCENDANTS, FOCUS_AFTER_DESCENDANTS, FOCUS_BLOCK_DESCENDANTS "
+ "but is " + descendantFocusability);
}
+ if (result && !isLayoutValid() && ((mPrivateFlags & PFLAG_WANTS_FOCUS) == 0)) {
+ mPrivateFlags |= PFLAG_WANTS_FOCUS;
+ }
+ return result;
}
/**
@@ -3465,8 +3475,10 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
if (!isLaidOut()) {
- Log.v(VIEW_LOG_TAG, "dispatchProvideStructure(): not laid out, ignoring "
- + childrenCount + " children of " + getAccessibilityViewId());
+ if (Helper.sVerbose) {
+ Log.v(VIEW_LOG_TAG, "dispatchProvideStructure(): not laid out, ignoring "
+ + childrenCount + " children of " + getAccessibilityViewId());
+ }
return;
}
@@ -3637,44 +3649,34 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
return ViewGroup.class.getName();
}
- @Override
- public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) {
- // If this is a live region, we should send a subtree change event
- // from this view. Otherwise, we can let it propagate up.
- if (getAccessibilityLiveRegion() != ACCESSIBILITY_LIVE_REGION_NONE) {
- notifyViewAccessibilityStateChangedIfNeeded(
- AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
- } else if (mParent != null) {
- try {
- mParent.notifySubtreeAccessibilityStateChanged(this, source, changeType);
- } catch (AbstractMethodError e) {
- Log.e(VIEW_LOG_TAG, mParent.getClass().getSimpleName() +
- " does not fully implement ViewParent", e);
- }
- }
- }
-
/** @hide */
@Override
- public void notifySubtreeAccessibilityStateChangedIfNeeded() {
+ public void notifyAccessibilitySubtreeChanged() {
if (!AccessibilityManager.getInstance(mContext).isEnabled() || mAttachInfo == null) {
return;
}
// If something important for a11y is happening in this subtree, make sure it's dispatched
// from a view that is important for a11y so it doesn't get lost.
- if ((getImportantForAccessibility() != IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS)
- && !isImportantForAccessibility() && (getChildCount() > 0)) {
+ if (getImportantForAccessibility() != IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
+ && !isImportantForAccessibility()
+ && getChildCount() > 0) {
ViewParent a11yParent = getParentForAccessibility();
if (a11yParent instanceof View) {
- ((View) a11yParent).notifySubtreeAccessibilityStateChangedIfNeeded();
+ ((View) a11yParent).notifyAccessibilitySubtreeChanged();
return;
}
}
- super.notifySubtreeAccessibilityStateChangedIfNeeded();
+ super.notifyAccessibilitySubtreeChanged();
+ }
+
+ @Override
+ public void notifySubtreeAccessibilityStateChanged(View child, View source, int changeType) {
+ notifyAccessibilityStateChanged(source, changeType);
}
+ /** @hide */
@Override
- void resetSubtreeAccessibilityStateChanged() {
+ public void resetSubtreeAccessibilityStateChanged() {
super.resetSubtreeAccessibilityStateChanged();
View[] children = mChildren;
final int childCount = mChildrenCount;
@@ -3854,7 +3856,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
* @hide
*/
@Override
- public Bitmap createSnapshot(Bitmap.Config quality, int backgroundColor, boolean skipChildren) {
+ public Bitmap createSnapshot(ViewDebug.CanvasProvider canvasProvider, boolean skipChildren) {
int count = mChildrenCount;
int[] visibilities = null;
@@ -3870,17 +3872,17 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
}
- Bitmap b = super.createSnapshot(quality, backgroundColor, skipChildren);
-
- if (skipChildren) {
- for (int i = 0; i < count; i++) {
- View child = getChildAt(i);
- child.mViewFlags = (child.mViewFlags & ~View.VISIBILITY_MASK)
- | (visibilities[i] & View.VISIBILITY_MASK);
+ try {
+ return super.createSnapshot(canvasProvider, skipChildren);
+ } finally {
+ if (skipChildren) {
+ for (int i = 0; i < count; i++) {
+ View child = getChildAt(i);
+ child.mViewFlags = (child.mViewFlags & ~View.VISIBILITY_MASK)
+ | (visibilities[i] & View.VISIBILITY_MASK);
+ }
}
}
-
- return b;
}
/** Return true if this ViewGroup is laying out using optical bounds. */
@@ -5086,7 +5088,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
}
if (child.getVisibility() != View.GONE) {
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
if (mTransientIndices != null) {
@@ -5356,7 +5358,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
dispatchViewRemoved(view);
if (view.getVisibility() != View.GONE) {
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
int transientCount = mTransientIndices == null ? 0 : mTransientIndices.size();
@@ -6075,7 +6077,7 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager
if (invalidate) {
invalidateViewProperty(false, false);
}
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
@Override
diff --git a/android/view/ViewRootImpl.java b/android/view/ViewRootImpl.java
index 6c5091c2..30f584c5 100644
--- a/android/view/ViewRootImpl.java
+++ b/android/view/ViewRootImpl.java
@@ -20,7 +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.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
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;
@@ -89,9 +89,11 @@ import android.view.accessibility.AccessibilityManager.HighTextContrastChangeLis
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.accessibility.AccessibilityViewHierarchyState;
import android.view.accessibility.AccessibilityWindowInfo;
import android.view.accessibility.IAccessibilityInteractionConnection;
import android.view.accessibility.IAccessibilityInteractionConnectionCallback;
+import android.view.accessibility.ThrottlingAccessibilityEventSender;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.inputmethod.InputMethodManager;
@@ -113,7 +115,6 @@ import java.io.OutputStream;
import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
-import java.util.HashSet;
import java.util.concurrent.CountDownLatch;
/**
@@ -460,10 +461,6 @@ public final class ViewRootImpl implements ViewParent,
new AccessibilityInteractionConnectionManager();
final HighContrastTextManager mHighContrastTextManager;
- SendWindowContentChangedAccessibilityEvent mSendWindowContentChangedAccessibilityEvent;
-
- HashSet<View> mTempHashSet;
-
private final int mDensity;
private final int mNoncompatDensity;
@@ -478,6 +475,8 @@ public final class ViewRootImpl implements ViewParent,
private boolean mNeedsRendererSetup;
+ protected AccessibilityViewHierarchyState mAccessibilityState;
+
/**
* Consistency verifier for debugging purposes.
*/
@@ -531,7 +530,7 @@ public final class ViewRootImpl implements ViewParent,
mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
if (!sCompatibilityDone) {
- sAlwaysAssignFocus = true;
+ sAlwaysAssignFocus = mTargetSdkVersion < Build.VERSION_CODES.P;
sCompatibilityDone = true;
}
@@ -1597,9 +1596,9 @@ public final class ViewRootImpl implements ViewParent,
void dispatchApplyInsets(View host) {
WindowInsets insets = getWindowInsets(true /* forceConstruct */);
- final boolean layoutInCutout =
- (mWindowAttributes.flags2 & FLAG2_LAYOUT_IN_DISPLAY_CUTOUT_AREA) != 0;
- if (!layoutInCutout) {
+ final boolean dispatchCutout = (mWindowAttributes.layoutInDisplayCutoutMode
+ == LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS);
+ if (!dispatchCutout) {
// 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();
@@ -2338,7 +2337,7 @@ public final class ViewRootImpl implements ViewParent,
}
if (mFirst) {
- if (sAlwaysAssignFocus) {
+ if (sAlwaysAssignFocus || !isInTouchMode()) {
// handle first focus request
if (DEBUG_INPUT_RESIZE) {
Log.v(mTag, "First: mView.hasFocus()=" + mView.hasFocus());
@@ -3609,7 +3608,7 @@ public final class ViewRootImpl implements ViewParent,
checkThread();
if (mView != null) {
if (!mView.hasFocus()) {
- if (sAlwaysAssignFocus) {
+ if (sAlwaysAssignFocus || !isInTouchMode()) {
v.requestFocus();
}
} else {
@@ -4212,10 +4211,7 @@ public final class ViewRootImpl implements ViewParent,
// find the best view to give focus to in this brave new non-touch-mode
// world
- final View focused = focusSearch(null, View.FOCUS_DOWN);
- if (focused != null) {
- return focused.requestFocus(View.FOCUS_DOWN);
- }
+ return mView.restoreDefaultFocus();
}
return false;
}
@@ -7262,11 +7258,9 @@ public final class ViewRootImpl implements ViewParent,
* {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval()}.
*/
private void postSendWindowContentChangedCallback(View source, int changeType) {
- if (mSendWindowContentChangedAccessibilityEvent == null) {
- mSendWindowContentChangedAccessibilityEvent =
- new SendWindowContentChangedAccessibilityEvent();
- }
- mSendWindowContentChangedAccessibilityEvent.runOrPost(source, changeType);
+ getAccessibilityState()
+ .getSendWindowContentChangedAccessibilityEvent()
+ .runOrPost(source, changeType);
}
/**
@@ -7274,11 +7268,20 @@ public final class ViewRootImpl implements ViewParent,
* {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} event.
*/
private void removeSendWindowContentChangedCallback() {
- if (mSendWindowContentChangedAccessibilityEvent != null) {
- mHandler.removeCallbacks(mSendWindowContentChangedAccessibilityEvent);
+ if (mAccessibilityState != null
+ && mAccessibilityState.isWindowContentChangedEventSenderInitialized()) {
+ ThrottlingAccessibilityEventSender.cancelIfPending(
+ mAccessibilityState.getSendWindowContentChangedAccessibilityEvent());
}
}
+ AccessibilityViewHierarchyState getAccessibilityState() {
+ if (mAccessibilityState == null) {
+ mAccessibilityState = new AccessibilityViewHierarchyState();
+ }
+ return mAccessibilityState;
+ }
+
@Override
public boolean showContextMenuForChild(View originalView) {
return false;
@@ -7314,12 +7317,8 @@ public final class ViewRootImpl implements ViewParent,
return false;
}
- // Immediately flush pending content changed event (if any) to preserve event order
- if (event.getEventType() != AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
- && mSendWindowContentChangedAccessibilityEvent != null
- && mSendWindowContentChangedAccessibilityEvent.mSource != null) {
- mSendWindowContentChangedAccessibilityEvent.removeCallbacksAndRun();
- }
+ // Send any pending event to prevent reordering
+ flushPendingAccessibilityEvents();
// Intercept accessibility focus events fired by virtual nodes to keep
// track of accessibility focus position in such nodes.
@@ -7363,6 +7362,19 @@ public final class ViewRootImpl implements ViewParent,
return true;
}
+ /** @hide */
+ public void flushPendingAccessibilityEvents() {
+ if (mAccessibilityState != null) {
+ if (mAccessibilityState.isScrollEventSenderInitialized()) {
+ mAccessibilityState.getSendViewScrolledAccessibilityEvent().sendNowIfPending();
+ }
+ if (mAccessibilityState.isWindowContentChangedEventSenderInitialized()) {
+ mAccessibilityState.getSendWindowContentChangedAccessibilityEvent()
+ .sendNowIfPending();
+ }
+ }
+ }
+
/**
* Updates the focused virtual view, when necessary, in response to a
* content changed event.
@@ -7497,39 +7509,6 @@ public final class ViewRootImpl implements ViewParent,
return View.TEXT_ALIGNMENT_RESOLVED_DEFAULT;
}
- private View getCommonPredecessor(View first, View second) {
- if (mTempHashSet == null) {
- mTempHashSet = new HashSet<View>();
- }
- HashSet<View> seen = mTempHashSet;
- seen.clear();
- View firstCurrent = first;
- while (firstCurrent != null) {
- seen.add(firstCurrent);
- ViewParent firstCurrentParent = firstCurrent.mParent;
- if (firstCurrentParent instanceof View) {
- firstCurrent = (View) firstCurrentParent;
- } else {
- firstCurrent = null;
- }
- }
- View secondCurrent = second;
- while (secondCurrent != null) {
- if (seen.contains(secondCurrent)) {
- seen.clear();
- return secondCurrent;
- }
- ViewParent secondCurrentParent = secondCurrent.mParent;
- if (secondCurrentParent instanceof View) {
- secondCurrent = (View) secondCurrentParent;
- } else {
- secondCurrent = null;
- }
- }
- seen.clear();
- return null;
- }
-
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
@@ -8140,80 +8119,6 @@ public final class ViewRootImpl implements ViewParent,
}
}
- private class SendWindowContentChangedAccessibilityEvent implements Runnable {
- private int mChangeTypes = 0;
-
- public View mSource;
- public long mLastEventTimeMillis;
-
- @Override
- public void run() {
- // Protect against re-entrant code and attempt to do the right thing in the case that
- // we're multithreaded.
- View source = mSource;
- mSource = null;
- if (source == null) {
- Log.e(TAG, "Accessibility content change has no source");
- return;
- }
- // The accessibility may be turned off while we were waiting so check again.
- if (AccessibilityManager.getInstance(mContext).isEnabled()) {
- mLastEventTimeMillis = SystemClock.uptimeMillis();
- AccessibilityEvent event = AccessibilityEvent.obtain();
- event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
- event.setContentChangeTypes(mChangeTypes);
- source.sendAccessibilityEventUnchecked(event);
- } else {
- mLastEventTimeMillis = 0;
- }
- // In any case reset to initial state.
- source.resetSubtreeAccessibilityStateChanged();
- mChangeTypes = 0;
- }
-
- public void runOrPost(View source, int changeType) {
- if (mHandler.getLooper() != Looper.myLooper()) {
- CalledFromWrongThreadException e = new CalledFromWrongThreadException("Only the "
- + "original thread that created a view hierarchy can touch its views.");
- // TODO: Throw the exception
- Log.e(TAG, "Accessibility content change on non-UI thread. Future Android "
- + "versions will throw an exception.", e);
- // Attempt to recover. This code does not eliminate the thread safety issue, but
- // it should force any issues to happen near the above log.
- mHandler.removeCallbacks(this);
- if (mSource != null) {
- // Dispatch whatever was pending. It's still possible that the runnable started
- // just before we removed the callbacks, and bad things will happen, but at
- // least they should happen very close to the logged error.
- run();
- }
- }
- if (mSource != null) {
- // If there is no common predecessor, then mSource points to
- // a removed view, hence in this case always prefer the source.
- View predecessor = getCommonPredecessor(mSource, source);
- mSource = (predecessor != null) ? predecessor : source;
- mChangeTypes |= changeType;
- return;
- }
- mSource = source;
- mChangeTypes = changeType;
- final long timeSinceLastMillis = SystemClock.uptimeMillis() - mLastEventTimeMillis;
- final long minEventIntevalMillis =
- ViewConfiguration.getSendRecurringAccessibilityEventsInterval();
- if (timeSinceLastMillis >= minEventIntevalMillis) {
- removeCallbacksAndRun();
- } else {
- mHandler.postDelayed(this, minEventIntevalMillis - timeSinceLastMillis);
- }
- }
-
- public void removeCallbacksAndRun() {
- mHandler.removeCallbacks(this);
- run();
- }
- }
-
private static class KeyFallbackManager {
// This is used to ensure that key-fallback events are only dispatched once. We attempt
diff --git a/android/view/ViewStructure.java b/android/view/ViewStructure.java
index d665dde3..1d94abeb 100644
--- a/android/view/ViewStructure.java
+++ b/android/view/ViewStructure.java
@@ -26,6 +26,8 @@ import android.util.Pair;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillValue;
+import com.android.internal.util.Preconditions;
+
import java.util.List;
/**
@@ -204,6 +206,16 @@ public abstract class ViewStructure {
public abstract void setTextLines(int[] charOffsets, int[] baselines);
/**
+ * Sets the identifier used to set the text associated with this view.
+ *
+ * <p>Should only be set when the node is used for autofill purposes - it will be ignored
+ * when used for Assist.
+ */
+ public void setTextIdEntry(@NonNull String entryName) {
+ Preconditions.checkNotNull(entryName);
+ }
+
+ /**
* Set optional hint text associated with this view; this is for example the text that is
* shown by an EditText when it is empty to indicate to the user the kind of text to input.
*/
diff --git a/android/view/Window.java b/android/view/Window.java
index 176927fe..5bd0782d 100644
--- a/android/view/Window.java
+++ b/android/view/Window.java
@@ -1339,9 +1339,9 @@ public abstract class Window {
/**
* Finds a view that was identified by the {@code android:id} XML attribute
- * that was processed in {@link android.app.Activity#onCreate}. This will
- * implicitly call {@link #getDecorView} with all of the associated
- * side-effects.
+ * that was processed in {@link android.app.Activity#onCreate}.
+ * <p>
+ * This will implicitly call {@link #getDecorView} with all of the associated side-effects.
* <p>
* <strong>Note:</strong> In most cases -- depending on compiler support --
* the resulting view is automatically cast to the target class type. If
@@ -1351,11 +1351,35 @@ public abstract class Window {
* @param id the ID to search for
* @return a view with given ID if found, or {@code null} otherwise
* @see View#findViewById(int)
+ * @see Window#requireViewById(int)
*/
@Nullable
public <T extends View> T findViewById(@IdRes int id) {
return getDecorView().findViewById(id);
}
+ /**
+ * Finds a view that was identified by the {@code android:id} XML attribute
+ * that was processed in {@link android.app.Activity#onCreate}, or throws an
+ * IllegalArgumentException if the ID is invalid, or there is no matching view in the hierarchy.
+ * <p>
+ * <strong>Note:</strong> In most cases -- depending on compiler support --
+ * the resulting view is automatically cast to the target class type. If
+ * the target class type is unconstrained, an explicit cast may be
+ * necessary.
+ *
+ * @param id the ID to search for
+ * @return a view with given ID
+ * @see View#requireViewById(int)
+ * @see Window#findViewById(int)
+ */
+ @NonNull
+ public final <T extends View> T requireViewById(@IdRes int id) {
+ T view = findViewById(id);
+ if (view == null) {
+ throw new IllegalArgumentException("ID does not reference a View inside this Window");
+ }
+ return view;
+ }
/**
* Convenience for
@@ -2244,9 +2268,36 @@ public abstract class Window {
* <p>
* The transitionName for the view background will be "android:navigation:background".
* </p>
+ * @attr ref android.R.styleable#Window_navigationBarColor
*/
public abstract void setNavigationBarColor(@ColorInt int color);
+ /**
+ * Shows a thin line of the specified color between the navigation bar and the app
+ * content.
+ * <p>
+ * For this to take effect,
+ * the window must be drawing the system bar backgrounds with
+ * {@link android.view.WindowManager.LayoutParams#FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS} and
+ * {@link android.view.WindowManager.LayoutParams#FLAG_TRANSLUCENT_NAVIGATION} must not be set.
+ *
+ * @param dividerColor The color of the thin line.
+ * @attr ref android.R.styleable#Window_navigationBarDividerColor
+ */
+ public void setNavigationBarDividerColor(@ColorInt int dividerColor) {
+ }
+
+ /**
+ * Retrieves the color of the navigation bar divider.
+ *
+ * @return The color of the navigation bar divider color.
+ * @see #setNavigationBarColor(int)
+ * @attr ref android.R.styleable#Window_navigationBarDividerColor
+ */
+ public @ColorInt int getNavigationBarDividerColor() {
+ return 0;
+ }
+
/** @hide */
public void setTheme(int resId) {
}
diff --git a/android/view/WindowManager.java b/android/view/WindowManager.java
index cbe012af..1c5e8719 100644
--- a/android/view/WindowManager.java
+++ b/android/view/WindowManager.java
@@ -17,10 +17,34 @@
package android.view;
import static android.content.pm.ActivityInfo.COLOR_MODE_DEFAULT;
+import static android.view.WindowLayoutParamsProto.ALPHA;
+import static android.view.WindowLayoutParamsProto.BUTTON_BRIGHTNESS;
+import static android.view.WindowLayoutParamsProto.COLOR_MODE;
+import static android.view.WindowLayoutParamsProto.FLAGS;
+import static android.view.WindowLayoutParamsProto.FORMAT;
+import static android.view.WindowLayoutParamsProto.GRAVITY;
+import static android.view.WindowLayoutParamsProto.HAS_SYSTEM_UI_LISTENERS;
+import static android.view.WindowLayoutParamsProto.HEIGHT;
+import static android.view.WindowLayoutParamsProto.HORIZONTAL_MARGIN;
+import static android.view.WindowLayoutParamsProto.INPUT_FEATURE_FLAGS;
+import static android.view.WindowLayoutParamsProto.NEEDS_MENU_KEY;
+import static android.view.WindowLayoutParamsProto.PREFERRED_REFRESH_RATE;
+import static android.view.WindowLayoutParamsProto.PRIVATE_FLAGS;
+import static android.view.WindowLayoutParamsProto.ROTATION_ANIMATION;
+import static android.view.WindowLayoutParamsProto.SCREEN_BRIGHTNESS;
+import static android.view.WindowLayoutParamsProto.SOFT_INPUT_MODE;
+import static android.view.WindowLayoutParamsProto.SUBTREE_SYSTEM_UI_VISIBILITY_FLAGS;
+import static android.view.WindowLayoutParamsProto.SYSTEM_UI_VISIBILITY_FLAGS;
+import static android.view.WindowLayoutParamsProto.TYPE;
+import static android.view.WindowLayoutParamsProto.USER_ACTIVITY_TIMEOUT;
+import static android.view.WindowLayoutParamsProto.VERTICAL_MARGIN;
+import static android.view.WindowLayoutParamsProto.WIDTH;
+import static android.view.WindowLayoutParamsProto.WINDOW_ANIMATIONS;
+import static android.view.WindowLayoutParamsProto.X;
+import static android.view.WindowLayoutParamsProto.Y;
import android.Manifest.permission;
import android.annotation.IntDef;
-import android.annotation.LongDef;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
@@ -74,11 +98,198 @@ public interface WindowManager extends ViewManager {
int DOCKED_BOTTOM = 4;
/** @hide */
- final static String INPUT_CONSUMER_PIP = "pip_input_consumer";
+ String INPUT_CONSUMER_PIP = "pip_input_consumer";
/** @hide */
- final static String INPUT_CONSUMER_NAVIGATION = "nav_input_consumer";
+ String INPUT_CONSUMER_NAVIGATION = "nav_input_consumer";
/** @hide */
- final static String INPUT_CONSUMER_WALLPAPER = "wallpaper_input_consumer";
+ String INPUT_CONSUMER_WALLPAPER = "wallpaper_input_consumer";
+ /** @hide */
+ String INPUT_CONSUMER_RECENTS_ANIMATION = "recents_animation_input_consumer";
+
+ /**
+ * Not set up for a transition.
+ * @hide
+ */
+ int TRANSIT_UNSET = -1;
+
+ /**
+ * No animation for transition.
+ * @hide
+ */
+ int TRANSIT_NONE = 0;
+
+ /**
+ * A window in a new activity is being opened on top of an existing one in the same task.
+ * @hide
+ */
+ int TRANSIT_ACTIVITY_OPEN = 6;
+
+ /**
+ * The window in the top-most activity is being closed to reveal the previous activity in the
+ * same task.
+ * @hide
+ */
+ int TRANSIT_ACTIVITY_CLOSE = 7;
+
+ /**
+ * A window in a new task is being opened on top of an existing one in another activity's task.
+ * @hide
+ */
+ int TRANSIT_TASK_OPEN = 8;
+
+ /**
+ * A window in the top-most activity is being closed to reveal the previous activity in a
+ * different task.
+ * @hide
+ */
+ int TRANSIT_TASK_CLOSE = 9;
+
+ /**
+ * A window in an existing task is being displayed on top of an existing one in another
+ * activity's task.
+ * @hide
+ */
+ int TRANSIT_TASK_TO_FRONT = 10;
+
+ /**
+ * A window in an existing task is being put below all other tasks.
+ * @hide
+ */
+ int TRANSIT_TASK_TO_BACK = 11;
+
+ /**
+ * A window in a new activity that doesn't have a wallpaper is being opened on top of one that
+ * does, effectively closing the wallpaper.
+ * @hide
+ */
+ int TRANSIT_WALLPAPER_CLOSE = 12;
+
+ /**
+ * A window in a new activity that does have a wallpaper is being opened on one that didn't,
+ * effectively opening the wallpaper.
+ * @hide
+ */
+ int TRANSIT_WALLPAPER_OPEN = 13;
+
+ /**
+ * A window in a new activity is being opened on top of an existing one, and both are on top
+ * of the wallpaper.
+ * @hide
+ */
+ int TRANSIT_WALLPAPER_INTRA_OPEN = 14;
+
+ /**
+ * The window in the top-most activity is being closed to reveal the previous activity, and
+ * both are on top of the wallpaper.
+ * @hide
+ */
+ int TRANSIT_WALLPAPER_INTRA_CLOSE = 15;
+
+ /**
+ * A window in a new task is being opened behind an existing one in another activity's task.
+ * The new window will show briefly and then be gone.
+ * @hide
+ */
+ int TRANSIT_TASK_OPEN_BEHIND = 16;
+
+ /**
+ * A window in a task is being animated in-place.
+ * @hide
+ */
+ int TRANSIT_TASK_IN_PLACE = 17;
+
+ /**
+ * An activity is being relaunched (e.g. due to configuration change).
+ * @hide
+ */
+ int TRANSIT_ACTIVITY_RELAUNCH = 18;
+
+ /**
+ * A task is being docked from recents.
+ * @hide
+ */
+ int TRANSIT_DOCK_TASK_FROM_RECENTS = 19;
+
+ /**
+ * Keyguard is going away.
+ * @hide
+ */
+ int TRANSIT_KEYGUARD_GOING_AWAY = 20;
+
+ /**
+ * Keyguard is going away with showing an activity behind that requests wallpaper.
+ * @hide
+ */
+ int TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER = 21;
+
+ /**
+ * Keyguard is being occluded.
+ * @hide
+ */
+ int TRANSIT_KEYGUARD_OCCLUDE = 22;
+
+ /**
+ * Keyguard is being unoccluded.
+ * @hide
+ */
+ int TRANSIT_KEYGUARD_UNOCCLUDE = 23;
+
+ /**
+ * @hide
+ */
+ @IntDef(prefix = { "TRANSIT_" }, value = {
+ TRANSIT_UNSET,
+ TRANSIT_NONE,
+ TRANSIT_ACTIVITY_OPEN,
+ TRANSIT_ACTIVITY_CLOSE,
+ TRANSIT_TASK_OPEN,
+ TRANSIT_TASK_CLOSE,
+ TRANSIT_TASK_TO_FRONT,
+ TRANSIT_TASK_TO_BACK,
+ TRANSIT_WALLPAPER_CLOSE,
+ TRANSIT_WALLPAPER_OPEN,
+ TRANSIT_WALLPAPER_INTRA_OPEN,
+ TRANSIT_WALLPAPER_INTRA_CLOSE,
+ TRANSIT_TASK_OPEN_BEHIND,
+ TRANSIT_TASK_IN_PLACE,
+ TRANSIT_ACTIVITY_RELAUNCH,
+ TRANSIT_DOCK_TASK_FROM_RECENTS,
+ TRANSIT_KEYGUARD_GOING_AWAY,
+ TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER,
+ TRANSIT_KEYGUARD_OCCLUDE,
+ TRANSIT_KEYGUARD_UNOCCLUDE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface TransitionType {}
+
+ /**
+ * Transition flag: Keyguard is going away, but keeping the notification shade open
+ * @hide
+ */
+ int TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE = 0x1;
+
+ /**
+ * Transition flag: Keyguard is going away, but doesn't want an animation for it
+ * @hide
+ */
+ int TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION = 0x2;
+
+ /**
+ * Transition flag: Keyguard is going away while it was showing the system wallpaper.
+ * @hide
+ */
+ int TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER = 0x4;
+
+ /**
+ * @hide
+ */
+ @IntDef(flag = true, prefix = { "TRANSIT_FLAG_" }, value = {
+ TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE,
+ TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION,
+ TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface TransitionFlags {}
/**
* Exception that is thrown when trying to add view whose
@@ -863,7 +1074,12 @@ public interface WindowManager extends ViewManager {
* decorations around the border (such as the status bar). The
* window must correctly position its contents to take the screen
* decoration into account. This flag is normally set for you
- * by Window as described in {@link Window#setFlags}. */
+ * by Window as described in {@link Window#setFlags}.
+ *
+ * <p>Note: on displays that have a {@link DisplayCutout}, the window may be placed
+ * such that it avoids the {@link DisplayCutout} area if necessary according to the
+ * {@link #layoutInDisplayCutoutMode}.
+ */
public static final int FLAG_LAYOUT_IN_SCREEN = 0x00000100;
/** Window flag: allow window to extend outside of the screen. */
@@ -1269,33 +1485,6 @@ 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
@@ -2024,6 +2213,77 @@ public interface WindowManager extends ViewManager {
*/
public boolean hasSystemUiListeners;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(
+ flag = true,
+ value = {LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT,
+ LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS,
+ LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER})
+ @interface LayoutInDisplayCutoutMode {}
+
+ /**
+ * Controls how the window is laid out if there is a {@link DisplayCutout}.
+ *
+ * <p>
+ * Defaults to {@link #LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT}.
+ *
+ * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT
+ * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
+ * @see #LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
+ * @see DisplayCutout
+ */
+ @LayoutInDisplayCutoutMode
+ public int layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
+
+ /**
+ * The window is allowed to extend into the {@link DisplayCutout} area, only if the
+ * {@link DisplayCutout} is fully contained within the status bar. Otherwise, the window is
+ * laid out such that it does not overlap with the {@link DisplayCutout} area.
+ *
+ * <p>
+ * In practice, this means that if the window did not set FLAG_FULLSCREEN or
+ * SYSTEM_UI_FLAG_FULLSCREEN, it can extend into the cutout area in portrait.
+ * Otherwise (i.e. fullscreen or landscape) it is laid out such that it does overlap the
+ * cutout area.
+ *
+ * <p>
+ * The usual precautions for not overlapping with the status bar are sufficient for ensuring
+ * that no important content overlaps with the DisplayCutout.
+ *
+ * @see DisplayCutout
+ * @see WindowInsets
+ */
+ public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT = 0;
+
+ /**
+ * The window is always allowed to extend into the {@link DisplayCutout} area,
+ * even if fullscreen or in landscape.
+ *
+ * <p>
+ * The window must make sure that no important content overlaps with the
+ * {@link DisplayCutout}.
+ *
+ * @see DisplayCutout
+ * @see WindowInsets#getDisplayCutout()
+ */
+ public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS = 1;
+
+ /**
+ * The window is never allowed to overlap with the DisplayCutout area.
+ *
+ * <p>
+ * This should be used with windows that transiently set SYSTEM_UI_FLAG_FULLSCREEN to
+ * avoid a relayout of the window when the flag is set or cleared.
+ *
+ * @see DisplayCutout
+ * @see View#SYSTEM_UI_FLAG_FULLSCREEN SYSTEM_UI_FLAG_FULLSCREEN
+ * @see View#SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ */
+ public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER = 2;
+
+
/**
* When this window has focus, disable touch pad pointer gesture processing.
* The window will receive raw position updates from the touch pad instead
@@ -2247,9 +2507,9 @@ 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(layoutInDisplayCutoutMode);
out.writeInt(gravity);
out.writeFloat(horizontalMargin);
out.writeFloat(verticalMargin);
@@ -2303,9 +2563,9 @@ public interface WindowManager extends ViewManager {
y = in.readInt();
type = in.readInt();
flags = in.readInt();
- flags2 = in.readLong();
privateFlags = in.readInt();
softInputMode = in.readInt();
+ layoutInDisplayCutoutMode = in.readInt();
gravity = in.readInt();
horizontalMargin = in.readFloat();
verticalMargin = in.readFloat();
@@ -2436,10 +2696,6 @@ 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;
@@ -2448,6 +2704,10 @@ public interface WindowManager extends ViewManager {
softInputMode = o.softInputMode;
changes |= SOFT_INPUT_MODE_CHANGED;
}
+ if (layoutInDisplayCutoutMode != o.layoutInDisplayCutoutMode) {
+ layoutInDisplayCutoutMode = o.layoutInDisplayCutoutMode;
+ changes |= LAYOUT_CHANGED;
+ }
if (gravity != o.gravity) {
gravity = o.gravity;
changes |= LAYOUT_CHANGED;
@@ -2625,6 +2885,10 @@ public interface WindowManager extends ViewManager {
sb.append(softInputModeToString(softInputMode));
sb.append('}');
}
+ if (layoutInDisplayCutoutMode != 0) {
+ sb.append(" layoutInDisplayCutoutMode=");
+ sb.append(layoutInDisplayCutoutModeToString(layoutInDisplayCutoutMode));
+ }
sb.append(" ty=");
sb.append(ViewDebug.intToString(LayoutParams.class, "type", type));
if (format != PixelFormat.OPAQUE) {
@@ -2693,11 +2957,6 @@ 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(
@@ -2722,7 +2981,32 @@ public interface WindowManager extends ViewManager {
*/
public void writeToProto(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
- proto.write(WindowLayoutParamsProto.TYPE, type);
+ proto.write(TYPE, type);
+ proto.write(X, x);
+ proto.write(Y, y);
+ proto.write(WIDTH, width);
+ proto.write(HEIGHT, height);
+ proto.write(HORIZONTAL_MARGIN, horizontalMargin);
+ proto.write(VERTICAL_MARGIN, verticalMargin);
+ proto.write(GRAVITY, gravity);
+ proto.write(SOFT_INPUT_MODE, softInputMode);
+ proto.write(FORMAT, format);
+ proto.write(WINDOW_ANIMATIONS, windowAnimations);
+ proto.write(ALPHA, alpha);
+ proto.write(SCREEN_BRIGHTNESS, screenBrightness);
+ proto.write(BUTTON_BRIGHTNESS, buttonBrightness);
+ proto.write(ROTATION_ANIMATION, rotationAnimation);
+ proto.write(PREFERRED_REFRESH_RATE, preferredRefreshRate);
+ proto.write(WindowLayoutParamsProto.PREFERRED_DISPLAY_MODE_ID, preferredDisplayModeId);
+ proto.write(HAS_SYSTEM_UI_LISTENERS, hasSystemUiListeners);
+ proto.write(INPUT_FEATURE_FLAGS, inputFeatures);
+ proto.write(USER_ACTIVITY_TIMEOUT, userActivityTimeout);
+ proto.write(NEEDS_MENU_KEY, needsMenuKey);
+ proto.write(COLOR_MODE, mColorMode);
+ proto.write(FLAGS, flags);
+ proto.write(PRIVATE_FLAGS, privateFlags);
+ proto.write(SYSTEM_UI_VISIBILITY_FLAGS, systemUiVisibility);
+ proto.write(SUBTREE_SYSTEM_UI_VISIBILITY_FLAGS, subtreeSystemUiVisibility);
proto.end(token);
}
@@ -2797,6 +3081,20 @@ public interface WindowManager extends ViewManager {
&& height == WindowManager.LayoutParams.MATCH_PARENT;
}
+ private static String layoutInDisplayCutoutModeToString(
+ @LayoutInDisplayCutoutMode int mode) {
+ switch (mode) {
+ case LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT:
+ return "default";
+ case LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS:
+ return "always";
+ case LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER:
+ return "never";
+ default:
+ return "unknown(" + mode + ")";
+ }
+ }
+
private static String softInputModeToString(@SoftInputModeFlags int softInputMode) {
final StringBuilder result = new StringBuilder();
final int state = softInputMode & SOFT_INPUT_MASK_STATE;
diff --git a/android/view/WindowManagerPolicyConstants.java b/android/view/WindowManagerPolicyConstants.java
index 21943bd6..a6f36bbf 100644
--- a/android/view/WindowManagerPolicyConstants.java
+++ b/android/view/WindowManagerPolicyConstants.java
@@ -18,8 +18,6 @@ package android.view;
import static android.view.Display.DEFAULT_DISPLAY;
-import android.annotation.SystemApi;
-
/**
* Constants for interfacing with WindowManagerService and WindowManagerPolicyInternal.
* @hide
@@ -47,6 +45,11 @@ public interface WindowManagerPolicyConstants {
int PRESENCE_INTERNAL = 1 << 0;
int PRESENCE_EXTERNAL = 1 << 1;
+ // Navigation bar position values
+ int NAV_BAR_LEFT = 1 << 0;
+ int NAV_BAR_RIGHT = 1 << 1;
+ int NAV_BAR_BOTTOM = 1 << 2;
+
/**
* Sticky broadcast of the current HDMI plugged state.
*/
@@ -62,7 +65,6 @@ public interface WindowManagerPolicyConstants {
* Set to {@code true} when intent was invoked from pressing the home key.
* @hide
*/
- @SystemApi
String EXTRA_FROM_HOME_KEY = "android.intent.extra.FROM_HOME_KEY";
// TODO: move this to a more appropriate place.
diff --git a/android/view/accessibility/AccessibilityEvent.java b/android/view/accessibility/AccessibilityEvent.java
index 1d19a9f5..e0f74a7d 100644
--- a/android/view/accessibility/AccessibilityEvent.java
+++ b/android/view/accessibility/AccessibilityEvent.java
@@ -38,19 +38,14 @@ import java.util.List;
* <p>
* An accessibility event is fired by an individual view which populates the event with
* data for its state and requests from its parent to send the event to interested
- * parties. The parent can optionally add an {@link AccessibilityRecord} for itself before
- * dispatching a similar request to its parent. A parent can also choose not to respect the
- * request for sending an event. The accessibility event is sent by the topmost view in the
- * view tree. Therefore, an {@link android.accessibilityservice.AccessibilityService} can
- * explore all records in an accessibility event to obtain more information about the
- * context in which the event was fired.
+ * parties. The parent can optionally modify or even block the event based on its broader
+ * understanding of the user interface's context.
* </p>
* <p>
- * The main purpose of an accessibility event is to expose enough information for an
- * {@link android.accessibilityservice.AccessibilityService} to provide meaningful feedback
- * to the user. Sometimes however, an accessibility service may need more contextual
- * information then the one in the event pay-load. In such cases the service can obtain
- * the event source which is an {@link AccessibilityNodeInfo} (snapshot of a View state)
+ * The main purpose of an accessibility event is to communicate changes in the UI to an
+ * {@link android.accessibilityservice.AccessibilityService}. The service may then inspect,
+ * if needed the user interface by examining the View hierarchy, as represented by a tree of
+ * {@link AccessibilityNodeInfo}s (snapshot of a View state)
* which can be used for exploring the window content. Note that the privilege for accessing
* an event's source, thus the window content, has to be explicitly requested. For more
* details refer to {@link android.accessibilityservice.AccessibilityService}. If an
@@ -85,21 +80,6 @@ import java.util.List;
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #isChecked()} - Whether the source is checked.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
* </ul>
* </p>
* <p>
@@ -113,21 +93,6 @@ import java.util.List;
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #isChecked()} - Whether the source is checked.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
* </ul>
* </p>
* <p>
@@ -141,23 +106,6 @@ import java.util.List;
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #isChecked()} - Whether the source is checked.</li>
- * <li>{@link #getItemCount()} - The number of selectable items of the source.</li>
- * <li>{@link #getCurrentItemIndex()} - The currently selected item index.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
* </ul>
* </p>
* <p>
@@ -171,23 +119,6 @@ import java.util.List;
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #isChecked()} - Whether the source is checked.</li>
- * <li>{@link #getItemCount()} - The number of focusable items on the screen.</li>
- * <li>{@link #getCurrentItemIndex()} - The currently focused item index.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
* </ul>
* </p>
* <p>
@@ -201,15 +132,11 @@ import java.util.List;
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #isChecked()} - Whether the source is checked.</li>
+ * <li>{@link #getText()} - The new text of the source.</li>
+ * <li>{@link #getBeforeText()} - The text of the source before the change.</li>
* <li>{@link #getFromIndex()} - The text change start index.</li>
* <li>{@link #getAddedCount()} - The number of added characters.</li>
* <li>{@link #getRemovedCount()} - The number of removed characters.</li>
- * <li>{@link #getBeforeText()} - The text of the source before the change.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
* </ul>
* </p>
* <p>
@@ -223,13 +150,6 @@ import java.util.List;
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #getFromIndex()} - The selection start index.</li>
- * <li>{@link #getToIndex()} - The selection end index.</li>
- * <li>{@link #getItemCount()} - The length of the source text.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
* </ul>
* </p>
* <b>View text traversed at movement granularity</b> - represents the event of traversing the
@@ -251,23 +171,11 @@ import java.util.List;
* <li>{@link #getToIndex()} - The end of the text that was skipped over in this movement.
* This is the ending point when moving forward through the text, but not when moving
* back.</li>
- * <li>{@link #isPassword()} - Whether the source is password.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getMovementGranularity()} - Sets the granularity at which a view's text
- * was traversed.</li>
* <li>{@link #getAction()} - Gets traversal action which specifies the direction.</li>
* </ul>
* </p>
* <p>
- * <b>View scrolled</b> - represents the event of scrolling a view. If
- * the source is a descendant of {@link android.widget.AdapterView} the
- * scroll is reported in terms of visible items - the first visible item,
- * the last visible item, and the total items - because the the source
- * is unaware of its pixel size since its adapter is responsible for
- * creating views. In all other cases the scroll is reported as the current
- * scroll on the X and Y axis respectively plus the height of the source in
- * pixels.</br>
+ * <b>View scrolled</b> - represents the event of scrolling a view. </br>
* <em>Type:</em> {@link #TYPE_VIEW_SCROLLED}</br>
* <em>Properties:</em></br>
* <ul>
@@ -276,37 +184,19 @@ import java.util.List;
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
+ * <li>{@link #getScrollDeltaX()} - The difference in the horizontal position.</li>
+ * <li>{@link #getScrollDeltaY()} - The difference in the vertical position.</li>
* </ul>
- * <em>Note:</em> This event type is not dispatched to descendants though
- * {@link android.view.View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)
- * View.dispatchPopulateAccessibilityEvent(AccessibilityEvent)}, hence the event
- * source {@link android.view.View} and the sub-tree rooted at it will not receive
- * calls to {@link android.view.View#onPopulateAccessibilityEvent(AccessibilityEvent)
- * View.onPopulateAccessibilityEvent(AccessibilityEvent)}. The preferred way to add
- * text content to such events is by setting the
- * {@link android.R.styleable#View_contentDescription contentDescription} of the source
- * view.</br>
* </p>
* <p>
* <b>TRANSITION TYPES</b></br>
* </p>
* <p>
- * <b>Window state changed</b> - represents the event of opening a
- * {@link android.widget.PopupWindow}, {@link android.view.Menu},
- * {@link android.app.Dialog}, etc.</br>
+ * <b>Window state changed</b> - represents the event of a change to a section of
+ * the user interface that is visually distinct. Should be sent from either the
+ * root view of a window or from a view that is marked as a pane
+ * {@link android.view.View#setAccessibilityPaneTitle(CharSequence)}. Not that changes
+ * to true windows are represented by {@link #TYPE_WINDOWS_CHANGED}.</br>
* <em>Type:</em> {@link #TYPE_WINDOW_STATE_CHANGED}</br>
* <em>Properties:</em></br>
* <ul>
@@ -315,8 +205,7 @@ import java.util.List;
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
+ * <li>{@link #getText()} - The text of the source's sub-tree, including the pane titles.</li>
* </ul>
* </p>
* <p>
@@ -325,10 +214,6 @@ import java.util.List;
* a view size, etc.</br>
* </p>
* <p>
- * <strong>Note:</strong> This event is fired only for the window source of the
- * last accessibility event different from {@link #TYPE_NOTIFICATION_STATE_CHANGED}
- * and its purpose is to notify clients that the content of the user interaction
- * window has changed.</br>
* <em>Type:</em> {@link #TYPE_WINDOW_CONTENT_CHANGED}</br>
* <em>Properties:</em></br>
* <ul>
@@ -339,32 +224,26 @@ import java.util.List;
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
* </ul>
- * <em>Note:</em> This event type is not dispatched to descendants though
- * {@link android.view.View#dispatchPopulateAccessibilityEvent(AccessibilityEvent)
- * View.dispatchPopulateAccessibilityEvent(AccessibilityEvent)}, hence the event
- * source {@link android.view.View} and the sub-tree rooted at it will not receive
- * calls to {@link android.view.View#onPopulateAccessibilityEvent(AccessibilityEvent)
- * View.onPopulateAccessibilityEvent(AccessibilityEvent)}. The preferred way to add
- * text content to such events is by setting the
- * {@link android.R.styleable#View_contentDescription contentDescription} of the source
- * view.</br>
* </p>
* <p>
- * <b>Windows changed</b> - represents the event of changes in the windows shown on
+ * <b>Windows changed</b> - represents a change in the windows shown on
* the screen such as a window appeared, a window disappeared, a window size changed,
- * a window layer changed, etc.</br>
+ * a window layer changed, etc. These events should only come from the system, which is responsible
+ * for managing windows. The list of windows is available from
+ * {@link android.accessibilityservice.AccessibilityService#getWindows()}.
+ * For regions of the user interface that are presented as windows but are
+ * controlled by an app's process, use {@link #TYPE_WINDOW_STATE_CHANGED}.</br>
* <em>Type:</em> {@link #TYPE_WINDOWS_CHANGED}</br>
* <em>Properties:</em></br>
* <ul>
* <li>{@link #getEventType()} - The type of the event.</li>
* <li>{@link #getEventTime()} - The event time.</li>
+ * <li>{@link #getWindowChanges()}</li> - The specific change to the source window
* </ul>
* <em>Note:</em> You can retrieve the {@link AccessibilityWindowInfo} for the window
- * source of the event via {@link AccessibilityEvent#getSource()} to get the source
- * node on which then call {@link AccessibilityNodeInfo#getWindow()
- * AccessibilityNodeInfo.getWindow()} to get the window. Also all windows on the screen can
- * be retrieved by a call to {@link android.accessibilityservice.AccessibilityService#getWindows()
- * android.accessibilityservice.AccessibilityService.getWindows()}.
+ * source of the event by looking through the list returned by
+ * {@link android.accessibilityservice.AccessibilityService#getWindows()} for the window whose ID
+ * matches {@link #getWindowId()}.
* </p>
* <p>
* <b>NOTIFICATION TYPES</b></br>
@@ -402,19 +281,6 @@ import java.util.List;
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
* </ul>
* </p>
* <b>View hover exit</b> - represents the event of stopping to hover
@@ -428,19 +294,6 @@ import java.util.List;
* <li>{@link #getClassName()} - The class name of the source.</li>
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
- * <li>{@link #getText()} - The text of the source's sub-tree.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
- * <li>{@link #getContentDescription()} - The content description of the source.</li>
- * <li>{@link #getScrollX()} - The offset of the source left edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getScrollY()} - The offset of the source top edge in pixels
- * (without descendants of AdapterView).</li>
- * <li>{@link #getFromIndex()} - The zero based index of the first visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getToIndex()} - The zero based index of the last visible item of the source,
- * inclusive (for descendants of AdapterView).</li>
- * <li>{@link #getItemCount()} - The total items of the source
- * (for descendants of AdapterView).</li>
* </ul>
* </p>
* <p>
@@ -513,10 +366,10 @@ import java.util.List;
* <b>MISCELLANEOUS TYPES</b></br>
* </p>
* <p>
- * <b>Announcement</b> - represents the event of an application making an
- * announcement. Usually this announcement is related to some sort of a context
- * change for which none of the events representing UI transitions is a good fit.
- * For example, announcing a new page in a book.</br>
+ * <b>Announcement</b> - represents the event of an application requesting a screen reader to make
+ * an announcement. Because the event carries no semantic meaning, this event is appropriate only
+ * in exceptional situations where additional screen reader output is needed but other types of
+ * accessibility services do not need to be aware of the change.</br>
* <em>Type:</em> {@link #TYPE_ANNOUNCEMENT}</br>
* <em>Properties:</em></br>
* <ul>
@@ -526,7 +379,6 @@ import java.util.List;
* <li>{@link #getPackageName()} - The package name of the source.</li>
* <li>{@link #getEventTime()} - The event time.</li>
* <li>{@link #getText()} - The text of the announcement.</li>
- * <li>{@link #isEnabled()} - Whether the source is enabled.</li>
* </ul>
* </p>
*
@@ -586,8 +438,10 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
public static final int TYPE_VIEW_TEXT_CHANGED = 0x00000010;
/**
- * Represents the event of opening a {@link android.widget.PopupWindow},
- * {@link android.view.Menu}, {@link android.app.Dialog}, etc.
+ * Represents the event of a change to a visually distinct section of the user interface.
+ * These events should only be dispatched from {@link android.view.View}s that have
+ * accessibility pane titles, and replaces {@link #TYPE_WINDOW_CONTENT_CHANGED} for those
+ * sources. Details about the change are available from {@link #getContentChangeTypes()}.
*/
public static final int TYPE_WINDOW_STATE_CHANGED = 0x00000020;
@@ -674,7 +528,8 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
public static final int TYPE_TOUCH_INTERACTION_END = 0x00200000;
/**
- * Represents the event change in the windows shown on the screen.
+ * Represents the event change in the system windows shown on the screen. This event type should
+ * only be dispatched by the system.
*/
public static final int TYPE_WINDOWS_CHANGED = 0x00400000;
@@ -696,7 +551,8 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
/**
* Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event:
- * A node in the subtree rooted at the source node was added or removed.
+ * One or more content changes occurred in the the subtree rooted at the source node,
+ * or the subtree's structure changed when a node was added or removed.
*/
public static final int CONTENT_CHANGE_TYPE_SUBTREE = 0x00000001;
@@ -712,6 +568,124 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
*/
public static final int CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION = 0x00000004;
+ /**
+ * Change type for {@link #TYPE_WINDOW_STATE_CHANGED} event:
+ * The node's pane title changed.
+ */
+ public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 0x00000008;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_STATE_CHANGED} event:
+ * The node has a pane title, and either just appeared or just was assigned a title when it
+ * had none before.
+ */
+ public static final int CONTENT_CHANGE_TYPE_PANE_APPEARED = 0x00000010;
+
+ /**
+ * Change type for {@link #TYPE_WINDOW_STATE_CHANGED} event:
+ * Can mean one of two slightly different things. The primary meaning is that the node has
+ * a pane title, and was removed from the node hierarchy. It will also be sent if the pane
+ * title is set to {@code null} after it contained a title.
+ * No source will be returned if the node is no longer on the screen. To make the change more
+ * clear for the user, the first entry in {@link #getText()} will return the value that would
+ * have been returned by {@code getSource().getPaneTitle()}.
+ */
+ public static final int CONTENT_CHANGE_TYPE_PANE_DISAPPEARED = 0x00000020;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window was added.
+ */
+ public static final int WINDOWS_CHANGE_ADDED = 0x00000001;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * A window was removed.
+ */
+ public static final int WINDOWS_CHANGE_REMOVED = 0x00000002;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's title changed.
+ */
+ public static final int WINDOWS_CHANGE_TITLE = 0x00000004;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's bounds changed.
+ */
+ public static final int WINDOWS_CHANGE_BOUNDS = 0x00000008;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's layer changed.
+ */
+ public static final int WINDOWS_CHANGE_LAYER = 0x00000010;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's {@link AccessibilityWindowInfo#isActive()} changed.
+ */
+ public static final int WINDOWS_CHANGE_ACTIVE = 0x00000020;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's {@link AccessibilityWindowInfo#isFocused()} changed.
+ */
+ public static final int WINDOWS_CHANGE_FOCUSED = 0x00000040;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's {@link AccessibilityWindowInfo#isAccessibilityFocused()} changed.
+ */
+ public static final int WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED = 0x00000080;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's parent changed.
+ */
+ public static final int WINDOWS_CHANGE_PARENT = 0x00000100;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window's children changed.
+ */
+ public static final int WINDOWS_CHANGE_CHILDREN = 0x00000200;
+
+ /**
+ * Change type for {@link #TYPE_WINDOWS_CHANGED} event:
+ * The window either entered or exited picture-in-picture mode.
+ */
+ public static final int WINDOWS_CHANGE_PIP = 0x00000400;
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "WINDOWS_CHANGE_" }, value = {
+ WINDOWS_CHANGE_ADDED,
+ WINDOWS_CHANGE_REMOVED,
+ WINDOWS_CHANGE_TITLE,
+ WINDOWS_CHANGE_BOUNDS,
+ WINDOWS_CHANGE_LAYER,
+ WINDOWS_CHANGE_ACTIVE,
+ WINDOWS_CHANGE_FOCUSED,
+ WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED,
+ WINDOWS_CHANGE_PARENT,
+ WINDOWS_CHANGE_CHILDREN,
+ WINDOWS_CHANGE_PIP
+ })
+ public @interface WindowsChangeTypes {}
+
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(flag = true, prefix = { "CONTENT_CHANGE_TYPE_" },
+ value = {
+ CONTENT_CHANGE_TYPE_UNDEFINED,
+ CONTENT_CHANGE_TYPE_SUBTREE,
+ CONTENT_CHANGE_TYPE_TEXT,
+ CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION,
+ CONTENT_CHANGE_TYPE_PANE_TITLE
+ })
+ public @interface ContentChangeTypes {}
/** @hide */
@IntDef(flag = true, prefix = { "TYPE_" }, value = {
@@ -782,6 +756,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
int mMovementGranularity;
int mAction;
int mContentChangeTypes;
+ int mWindowChangeTypes;
private ArrayList<AccessibilityRecord> mRecords;
@@ -802,6 +777,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
mMovementGranularity = event.mMovementGranularity;
mAction = event.mAction;
mContentChangeTypes = event.mContentChangeTypes;
+ mWindowChangeTypes = event.mWindowChangeTypes;
mEventTime = event.mEventTime;
mPackageName = event.mPackageName;
}
@@ -885,6 +861,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
* <li>{@link AccessibilityEvent#CONTENT_CHANGE_TYPE_UNDEFINED}
* </ul>
*/
+ @ContentChangeTypes
public int getContentChangeTypes() {
return mContentChangeTypes;
}
@@ -913,12 +890,49 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
* @throws IllegalStateException If called from an AccessibilityService.
* @see #getContentChangeTypes()
*/
- public void setContentChangeTypes(int changeTypes) {
+ public void setContentChangeTypes(@ContentChangeTypes int changeTypes) {
enforceNotSealed();
mContentChangeTypes = changeTypes;
}
/**
+ * Get the bit mask of change types signaled by a {@link #TYPE_WINDOWS_CHANGED} event. A
+ * single event may represent multiple change types.
+ *
+ * @return The bit mask of change types.
+ */
+ @WindowsChangeTypes
+ public int getWindowChanges() {
+ return mWindowChangeTypes;
+ }
+
+ /** @hide */
+ public void setWindowChanges(@WindowsChangeTypes int changes) {
+ mWindowChangeTypes = changes;
+ }
+
+ private static String windowChangeTypesToString(@WindowsChangeTypes int types) {
+ return BitUtils.flagsToString(types, AccessibilityEvent::singleWindowChangeTypeToString);
+ }
+
+ private static String singleWindowChangeTypeToString(int type) {
+ switch (type) {
+ case WINDOWS_CHANGE_ADDED: return "WINDOWS_CHANGE_ADDED";
+ case WINDOWS_CHANGE_REMOVED: return "WINDOWS_CHANGE_REMOVED";
+ case WINDOWS_CHANGE_TITLE: return "WINDOWS_CHANGE_TITLE";
+ case WINDOWS_CHANGE_BOUNDS: return "WINDOWS_CHANGE_BOUNDS";
+ case WINDOWS_CHANGE_LAYER: return "WINDOWS_CHANGE_LAYER";
+ case WINDOWS_CHANGE_ACTIVE: return "WINDOWS_CHANGE_ACTIVE";
+ case WINDOWS_CHANGE_FOCUSED: return "WINDOWS_CHANGE_FOCUSED";
+ case WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED:
+ return "WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED";
+ case WINDOWS_CHANGE_PARENT: return "WINDOWS_CHANGE_PARENT";
+ case WINDOWS_CHANGE_CHILDREN: return "WINDOWS_CHANGE_CHILDREN";
+ default: return Integer.toHexString(type);
+ }
+ }
+
+ /**
* Sets the event type.
*
* @param eventType The event type.
@@ -1025,6 +1039,26 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
}
/**
+ * Convenience method to obtain a {@link #TYPE_WINDOWS_CHANGED} event for a specific window and
+ * change set.
+ *
+ * @param windowId The ID of the window that changed
+ * @param windowChangeTypes The changes to populate
+ * @return An instance of a TYPE_WINDOWS_CHANGED, populated with the requested fields and with
+ * importantForAccessibility set to {@code true}.
+ *
+ * @hide
+ */
+ public static AccessibilityEvent obtainWindowsChangedEvent(
+ int windowId, int windowChangeTypes) {
+ final AccessibilityEvent event = AccessibilityEvent.obtain(TYPE_WINDOWS_CHANGED);
+ event.setWindowId(windowId);
+ event.setWindowChanges(windowChangeTypes);
+ event.setImportantForAccessibility(true);
+ return event;
+ }
+
+ /**
* Returns a cached instance if such is available or a new one is
* instantiated with its type property set.
*
@@ -1099,6 +1133,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
mMovementGranularity = 0;
mAction = 0;
mContentChangeTypes = 0;
+ mWindowChangeTypes = 0;
mPackageName = null;
mEventTime = 0;
if (mRecords != null) {
@@ -1120,6 +1155,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
mMovementGranularity = parcel.readInt();
mAction = parcel.readInt();
mContentChangeTypes = parcel.readInt();
+ mWindowChangeTypes = parcel.readInt();
mPackageName = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
mEventTime = parcel.readLong();
mConnectionId = parcel.readInt();
@@ -1178,6 +1214,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
parcel.writeInt(mMovementGranularity);
parcel.writeInt(mAction);
parcel.writeInt(mContentChangeTypes);
+ parcel.writeInt(mWindowChangeTypes);
TextUtils.writeToParcel(mPackageName, parcel, 0);
parcel.writeLong(mEventTime);
parcel.writeInt(mConnectionId);
@@ -1236,41 +1273,33 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par
builder.append("EventType: ").append(eventTypeToString(mEventType));
builder.append("; EventTime: ").append(mEventTime);
builder.append("; PackageName: ").append(mPackageName);
- builder.append("; MovementGranularity: ").append(mMovementGranularity);
- builder.append("; Action: ").append(mAction);
- builder.append(super.toString());
- if (DEBUG) {
- builder.append("\n");
+ if (!DEBUG_CONCISE_TOSTRING || mMovementGranularity != 0) {
+ builder.append("; MovementGranularity: ").append(mMovementGranularity);
+ }
+ if (!DEBUG_CONCISE_TOSTRING || mAction != 0) {
+ builder.append("; Action: ").append(mAction);
+ }
+ if (!DEBUG_CONCISE_TOSTRING || mContentChangeTypes != 0) {
builder.append("; ContentChangeTypes: ").append(
contentChangeTypesToString(mContentChangeTypes));
- builder.append("; sourceWindowId: ").append(mSourceWindowId);
- builder.append("; mSourceNodeId: ").append(mSourceNodeId);
- for (int i = 0; i < getRecordCount(); i++) {
- final AccessibilityRecord record = getRecord(i);
- builder.append(" Record ");
- builder.append(i);
- builder.append(":");
- builder.append(" [ ClassName: " + record.mClassName);
- builder.append("; Text: " + record.mText);
- builder.append("; ContentDescription: " + record.mContentDescription);
- builder.append("; ItemCount: " + record.mItemCount);
- builder.append("; CurrentItemIndex: " + record.mCurrentItemIndex);
- builder.append("; IsEnabled: " + record.isEnabled());
- builder.append("; IsPassword: " + record.isPassword());
- builder.append("; IsChecked: " + record.isChecked());
- builder.append("; IsFullScreen: " + record.isFullScreen());
- builder.append("; Scrollable: " + record.isScrollable());
- builder.append("; BeforeText: " + record.mBeforeText);
- builder.append("; FromIndex: " + record.mFromIndex);
- builder.append("; ToIndex: " + record.mToIndex);
- builder.append("; ScrollX: " + record.mScrollX);
- builder.append("; ScrollY: " + record.mScrollY);
- builder.append("; AddedCount: " + record.mAddedCount);
- builder.append("; RemovedCount: " + record.mRemovedCount);
- builder.append("; ParcelableData: " + record.mParcelableData);
- builder.append(" ]");
+ }
+ if (!DEBUG_CONCISE_TOSTRING || mWindowChangeTypes != 0) {
+ builder.append("; WindowChangeTypes: ").append(
+ contentChangeTypesToString(mWindowChangeTypes));
+ }
+ super.appendTo(builder);
+ if (DEBUG || DEBUG_CONCISE_TOSTRING) {
+ if (!DEBUG_CONCISE_TOSTRING) {
builder.append("\n");
}
+ if (DEBUG) {
+ builder.append("; SourceWindowId: ").append(mSourceWindowId);
+ builder.append("; SourceNodeId: ").append(mSourceNodeId);
+ }
+ for (int i = 0; i < getRecordCount(); i++) {
+ builder.append(" Record ").append(i).append(":");
+ getRecord(i).appendTo(builder).append("\n");
+ }
} else {
builder.append("; recordCount: ").append(getRecordCount());
}
diff --git a/android/view/accessibility/AccessibilityNodeInfo.java b/android/view/accessibility/AccessibilityNodeInfo.java
index 28ef6978..23e7d619 100644
--- a/android/view/accessibility/AccessibilityNodeInfo.java
+++ b/android/view/accessibility/AccessibilityNodeInfo.java
@@ -639,6 +639,8 @@ public class AccessibilityNodeInfo implements Parcelable {
private static final int BOOLEAN_PROPERTY_IS_SHOWING_HINT = 0x0100000;
+ private static final int BOOLEAN_PROPERTY_IS_HEADING = 0x0200000;
+
/**
* Bits that provide the id of a virtual descendant of a view.
*/
@@ -723,7 +725,9 @@ public class AccessibilityNodeInfo implements Parcelable {
private CharSequence mText;
private CharSequence mHintText;
private CharSequence mError;
+ private CharSequence mPaneTitle;
private CharSequence mContentDescription;
+ private CharSequence mTooltipText;
private String mViewIdResourceName;
private ArrayList<String> mExtraDataKeys;
@@ -2033,6 +2037,33 @@ public class AccessibilityNodeInfo implements Parcelable {
}
/**
+ * If this node represents a visually distinct region of the screen that may update separately
+ * from the rest of the window, it is considered a pane. Set the pane title to indicate that
+ * the node is a pane, and to provide a title for it.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ * @param paneTitle The title of the pane represented by this node.
+ */
+ public void setPaneTitle(@Nullable CharSequence paneTitle) {
+ enforceNotSealed();
+ mPaneTitle = (paneTitle == null)
+ ? null : paneTitle.subSequence(0, paneTitle.length());
+ }
+
+ /**
+ * Get the title of the pane represented by this node.
+ *
+ * @return The title of the pane represented by this node, or {@code null} if this node does
+ * not represent a pane.
+ */
+ public @Nullable CharSequence getPaneTitle() {
+ return mPaneTitle;
+ }
+
+ /**
* Get the drawing order of the view corresponding it this node.
* <p>
* Drawing order is determined only within the node's parent, so this index is only relative
@@ -2381,6 +2412,30 @@ public class AccessibilityNodeInfo implements Parcelable {
}
/**
+ * Returns whether node represents a heading.
+ *
+ * @return {@code true} if the node is a heading, {@code false} otherwise.
+ */
+ public boolean isHeading() {
+ return getBooleanProperty(BOOLEAN_PROPERTY_IS_HEADING);
+ }
+
+ /**
+ * Sets whether the node represents a heading.
+ *
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param isHeading {@code true} if the node is a heading, {@code false} otherwise.
+ */
+ public void setHeading(boolean isHeading) {
+ setBooleanProperty(BOOLEAN_PROPERTY_IS_HEADING, isHeading);
+ }
+
+ /**
* Gets the package this node comes from.
*
* @return The package name.
@@ -2601,6 +2656,34 @@ public class AccessibilityNodeInfo implements Parcelable {
}
/**
+ * Gets the tooltip text of this node.
+ *
+ * @return The tooltip text.
+ */
+ @Nullable
+ public CharSequence getTooltipText() {
+ return mTooltipText;
+ }
+
+ /**
+ * Sets the tooltip text of this node.
+ * <p>
+ * <strong>Note:</strong> Cannot be called from an
+ * {@link android.accessibilityservice.AccessibilityService}.
+ * This class is made immutable before being delivered to an AccessibilityService.
+ * </p>
+ *
+ * @param tooltipText The tooltip text.
+ *
+ * @throws IllegalStateException If called from an AccessibilityService.
+ */
+ public void setTooltipText(@Nullable CharSequence tooltipText) {
+ enforceNotSealed();
+ mTooltipText = (tooltipText == null) ? null
+ : tooltipText.subSequence(0, tooltipText.length());
+ }
+
+ /**
* Sets the view for which the view represented by this info serves as a
* label for accessibility purposes.
*
@@ -3151,6 +3234,14 @@ public class AccessibilityNodeInfo implements Parcelable {
nonDefaultFields |= bitAt(fieldIndex);
}
fieldIndex++;
+ if (!Objects.equals(mPaneTitle, DEFAULT.mPaneTitle)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
+ if (!Objects.equals(mTooltipText, DEFAULT.mTooltipText)) {
+ nonDefaultFields |= bitAt(fieldIndex);
+ }
+ fieldIndex++;
if (!Objects.equals(mViewIdResourceName, DEFAULT.mViewIdResourceName)) {
nonDefaultFields |= bitAt(fieldIndex);
}
@@ -3270,6 +3361,9 @@ public class AccessibilityNodeInfo implements Parcelable {
if (isBitSet(nonDefaultFields, fieldIndex++)) {
parcel.writeCharSequence(mContentDescription);
}
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mPaneTitle);
+ if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mTooltipText);
+
if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeString(mViewIdResourceName);
if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeInt(mTextSelectionStart);
@@ -3341,6 +3435,8 @@ public class AccessibilityNodeInfo implements Parcelable {
mHintText = other.mHintText;
mError = other.mError;
mContentDescription = other.mContentDescription;
+ mPaneTitle = other.mPaneTitle;
+ mTooltipText = other.mTooltipText;
mViewIdResourceName = other.mViewIdResourceName;
if (mActions != null) mActions.clear();
@@ -3461,6 +3557,8 @@ public class AccessibilityNodeInfo implements Parcelable {
if (isBitSet(nonDefaultFields, fieldIndex++)) {
mContentDescription = parcel.readCharSequence();
}
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mPaneTitle = parcel.readString();
+ if (isBitSet(nonDefaultFields, fieldIndex++)) mTooltipText = parcel.readCharSequence();
if (isBitSet(nonDefaultFields, fieldIndex++)) mViewIdResourceName = parcel.readString();
if (isBitSet(nonDefaultFields, fieldIndex++)) mTextSelectionStart = parcel.readInt();
@@ -3623,6 +3721,10 @@ public class AccessibilityNodeInfo implements Parcelable {
return "ACTION_SET_PROGRESS";
case R.id.accessibilityActionContextClick:
return "ACTION_CONTEXT_CLICK";
+ case R.id.accessibilityActionShowTooltip:
+ return "ACTION_SHOW_TOOLTIP";
+ case R.id.accessibilityActionHideTooltip:
+ return "ACTION_HIDE_TOOLTIP";
default:
return "ACTION_UNKNOWN";
}
@@ -3736,6 +3838,7 @@ public class AccessibilityNodeInfo implements Parcelable {
builder.append("; error: ").append(mError);
builder.append("; maxTextLength: ").append(mMaxTextLength);
builder.append("; contentDescription: ").append(mContentDescription);
+ builder.append("; tooltipText: ").append(mTooltipText);
builder.append("; viewIdResName: ").append(mViewIdResourceName);
builder.append("; checkable: ").append(isCheckable());
@@ -4150,6 +4253,20 @@ public class AccessibilityNodeInfo implements Parcelable {
public static final AccessibilityAction ACTION_MOVE_WINDOW =
new AccessibilityAction(R.id.accessibilityActionMoveWindow);
+ /**
+ * Action to show a tooltip. A node should expose this action only for views with tooltip
+ * text that but are not currently showing a tooltip.
+ */
+ public static final AccessibilityAction ACTION_SHOW_TOOLTIP =
+ new AccessibilityAction(R.id.accessibilityActionShowTooltip);
+
+ /**
+ * Action to hide a tooltip. A node should expose this action only for views that are
+ * currently showing a tooltip.
+ */
+ public static final AccessibilityAction ACTION_HIDE_TOOLTIP =
+ new AccessibilityAction(R.id.accessibilityActionHideTooltip);
+
private final int mActionId;
private final CharSequence mLabel;
@@ -4562,7 +4679,8 @@ public class AccessibilityNodeInfo implements Parcelable {
* @param rowSpan The number of rows the item spans.
* @param columnIndex The column index at which the item is located.
* @param columnSpan The number of columns the item spans.
- * @param heading Whether the item is a heading.
+ * @param heading Whether the item is a heading. (Prefer
+ * {@link AccessibilityNodeInfo#setHeading(boolean)}).
*/
public static CollectionItemInfo obtain(int rowIndex, int rowSpan,
int columnIndex, int columnSpan, boolean heading) {
@@ -4576,7 +4694,8 @@ public class AccessibilityNodeInfo implements Parcelable {
* @param rowSpan The number of rows the item spans.
* @param columnIndex The column index at which the item is located.
* @param columnSpan The number of columns the item spans.
- * @param heading Whether the item is a heading.
+ * @param heading Whether the item is a heading. (Prefer
+ * {@link AccessibilityNodeInfo#setHeading(boolean)})
* @param selected Whether the item is selected.
*/
public static CollectionItemInfo obtain(int rowIndex, int rowSpan,
@@ -4663,6 +4782,7 @@ public class AccessibilityNodeInfo implements Parcelable {
* heading, table header, etc.
*
* @return If the item is a heading.
+ * @deprecated Use {@link AccessibilityNodeInfo#isHeading()}
*/
public boolean isHeading() {
return mHeading;
diff --git a/android/view/accessibility/AccessibilityRecord.java b/android/view/accessibility/AccessibilityRecord.java
index fa505c97..0a709f8f 100644
--- a/android/view/accessibility/AccessibilityRecord.java
+++ b/android/view/accessibility/AccessibilityRecord.java
@@ -16,6 +16,8 @@
package android.view.accessibility;
+import static com.android.internal.util.CollectionUtils.isEmpty;
+
import android.annotation.Nullable;
import android.os.Parcelable;
import android.view.View;
@@ -55,6 +57,8 @@ import java.util.List;
* @see AccessibilityNodeInfo
*/
public class AccessibilityRecord {
+ /** @hide */
+ protected static final boolean DEBUG_CONCISE_TOSTRING = false;
private static final int UNDEFINED = -1;
@@ -888,28 +892,69 @@ public class AccessibilityRecord {
@Override
public String toString() {
- StringBuilder builder = new StringBuilder();
- builder.append(" [ ClassName: " + mClassName);
- builder.append("; Text: " + mText);
- builder.append("; ContentDescription: " + mContentDescription);
- builder.append("; ItemCount: " + mItemCount);
- builder.append("; CurrentItemIndex: " + mCurrentItemIndex);
- builder.append("; IsEnabled: " + getBooleanProperty(PROPERTY_ENABLED));
- builder.append("; IsPassword: " + getBooleanProperty(PROPERTY_PASSWORD));
- builder.append("; IsChecked: " + getBooleanProperty(PROPERTY_CHECKED));
- builder.append("; IsFullScreen: " + getBooleanProperty(PROPERTY_FULL_SCREEN));
- builder.append("; Scrollable: " + getBooleanProperty(PROPERTY_SCROLLABLE));
- builder.append("; BeforeText: " + mBeforeText);
- builder.append("; FromIndex: " + mFromIndex);
- builder.append("; ToIndex: " + mToIndex);
- builder.append("; ScrollX: " + mScrollX);
- builder.append("; ScrollY: " + mScrollY);
- builder.append("; MaxScrollX: " + mMaxScrollX);
- builder.append("; MaxScrollY: " + mMaxScrollY);
- builder.append("; AddedCount: " + mAddedCount);
- builder.append("; RemovedCount: " + mRemovedCount);
- builder.append("; ParcelableData: " + mParcelableData);
+ return appendTo(new StringBuilder()).toString();
+ }
+
+ StringBuilder appendTo(StringBuilder builder) {
+ builder.append(" [ ClassName: ").append(mClassName);
+ if (!DEBUG_CONCISE_TOSTRING || !isEmpty(mText)) {
+ appendPropName(builder, "Text").append(mText);
+ }
+ append(builder, "ContentDescription", mContentDescription);
+ append(builder, "ItemCount", mItemCount);
+ append(builder, "CurrentItemIndex", mCurrentItemIndex);
+
+ appendUnless(true, PROPERTY_ENABLED, builder);
+ appendUnless(false, PROPERTY_PASSWORD, builder);
+ appendUnless(false, PROPERTY_CHECKED, builder);
+ appendUnless(false, PROPERTY_FULL_SCREEN, builder);
+ appendUnless(false, PROPERTY_SCROLLABLE, builder);
+
+ append(builder, "BeforeText", mBeforeText);
+ append(builder, "FromIndex", mFromIndex);
+ append(builder, "ToIndex", mToIndex);
+ append(builder, "ScrollX", mScrollX);
+ append(builder, "ScrollY", mScrollY);
+ append(builder, "MaxScrollX", mMaxScrollX);
+ append(builder, "MaxScrollY", mMaxScrollY);
+ append(builder, "AddedCount", mAddedCount);
+ append(builder, "RemovedCount", mRemovedCount);
+ append(builder, "ParcelableData", mParcelableData);
builder.append(" ]");
- return builder.toString();
+ return builder;
+ }
+
+ private void appendUnless(boolean defValue, int prop, StringBuilder builder) {
+ boolean value = getBooleanProperty(prop);
+ if (DEBUG_CONCISE_TOSTRING && value == defValue) return;
+ appendPropName(builder, singleBooleanPropertyToString(prop))
+ .append(value);
+ }
+
+ private static String singleBooleanPropertyToString(int prop) {
+ switch (prop) {
+ case PROPERTY_CHECKED: return "Checked";
+ case PROPERTY_ENABLED: return "Enabled";
+ case PROPERTY_PASSWORD: return "Password";
+ case PROPERTY_FULL_SCREEN: return "FullScreen";
+ case PROPERTY_SCROLLABLE: return "Scrollable";
+ case PROPERTY_IMPORTANT_FOR_ACCESSIBILITY:
+ return "ImportantForAccessibility";
+ default: return Integer.toHexString(prop);
+ }
+ }
+
+ private void append(StringBuilder builder, String propName, int propValue) {
+ if (DEBUG_CONCISE_TOSTRING && propValue == UNDEFINED) return;
+ appendPropName(builder, propName).append(propValue);
+ }
+
+ private void append(StringBuilder builder, String propName, Object propValue) {
+ if (DEBUG_CONCISE_TOSTRING && propValue == null) return;
+ appendPropName(builder, propName).append(propValue);
+ }
+
+ private StringBuilder appendPropName(StringBuilder builder, String propName) {
+ return builder.append("; ").append(propName).append(": ");
}
}
diff --git a/android/view/accessibility/AccessibilityViewHierarchyState.java b/android/view/accessibility/AccessibilityViewHierarchyState.java
new file mode 100644
index 00000000..447fafaa
--- /dev/null
+++ b/android/view/accessibility/AccessibilityViewHierarchyState.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.accessibility;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * Accessibility-related state of a {@link android.view.ViewRootImpl}
+ *
+ * @hide
+ */
+public class AccessibilityViewHierarchyState {
+ private @Nullable SendViewScrolledAccessibilityEvent mSendViewScrolledAccessibilityEvent;
+ private @Nullable SendWindowContentChangedAccessibilityEvent
+ mSendWindowContentChangedAccessibilityEvent;
+
+ /**
+ * @return a {@link SendViewScrolledAccessibilityEvent}, creating one if needed
+ */
+ public @NonNull SendViewScrolledAccessibilityEvent getSendViewScrolledAccessibilityEvent() {
+ if (mSendViewScrolledAccessibilityEvent == null) {
+ mSendViewScrolledAccessibilityEvent = new SendViewScrolledAccessibilityEvent();
+ }
+ return mSendViewScrolledAccessibilityEvent;
+ }
+
+ public boolean isScrollEventSenderInitialized() {
+ return mSendViewScrolledAccessibilityEvent != null;
+ }
+
+ /**
+ * @return a {@link SendWindowContentChangedAccessibilityEvent}, creating one if needed
+ */
+ public @NonNull SendWindowContentChangedAccessibilityEvent
+ getSendWindowContentChangedAccessibilityEvent() {
+ if (mSendWindowContentChangedAccessibilityEvent == null) {
+ mSendWindowContentChangedAccessibilityEvent =
+ new SendWindowContentChangedAccessibilityEvent();
+ }
+ return mSendWindowContentChangedAccessibilityEvent;
+ }
+
+ public boolean isWindowContentChangedEventSenderInitialized() {
+ return mSendWindowContentChangedAccessibilityEvent != null;
+ }
+}
diff --git a/android/view/accessibility/AccessibilityWindowInfo.java b/android/view/accessibility/AccessibilityWindowInfo.java
index ef1a3f3b..c1c9174c 100644
--- a/android/view/accessibility/AccessibilityWindowInfo.java
+++ b/android/view/accessibility/AccessibilityWindowInfo.java
@@ -21,9 +21,12 @@ import android.annotation.TestApi;
import android.graphics.Rect;
import android.os.Parcel;
import android.os.Parcelable;
+import android.text.TextUtils;
import android.util.LongArray;
import android.util.Pools.SynchronizedPool;
+import android.view.accessibility.AccessibilityEvent.WindowsChangeTypes;
+import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -575,7 +578,7 @@ public final class AccessibilityWindowInfo implements Parcelable {
StringBuilder builder = new StringBuilder();
builder.append("AccessibilityWindowInfo[");
builder.append("title=").append(mTitle);
- builder.append("id=").append(mId);
+ builder.append(", id=").append(mId);
builder.append(", type=").append(typeToString(mType));
builder.append(", layer=").append(mLayer);
builder.append(", bounds=").append(mBoundsInScreen);
@@ -713,6 +716,60 @@ public final class AccessibilityWindowInfo implements Parcelable {
return false;
}
+ /**
+ * Reports how this window differs from a possibly different state of the same window. The
+ * argument must have the same id and type as neither of those properties may change.
+ *
+ * @param other The new state.
+ * @return A set of flags showing how the window has changes, or 0 if the two states are the
+ * same.
+ *
+ * @hide
+ */
+ @WindowsChangeTypes
+ public int differenceFrom(AccessibilityWindowInfo other) {
+ if (other.mId != mId) {
+ throw new IllegalArgumentException("Not same window.");
+ }
+ if (other.mType != mType) {
+ throw new IllegalArgumentException("Not same type.");
+ }
+ int changes = 0;
+ if (!TextUtils.equals(mTitle, other.mTitle)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_TITLE;
+ }
+
+ if (!mBoundsInScreen.equals(other.mBoundsInScreen)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_BOUNDS;
+ }
+ if (mLayer != other.mLayer) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_LAYER;
+ }
+ if (getBooleanProperty(BOOLEAN_PROPERTY_ACTIVE)
+ != other.getBooleanProperty(BOOLEAN_PROPERTY_ACTIVE)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_ACTIVE;
+ }
+ if (getBooleanProperty(BOOLEAN_PROPERTY_FOCUSED)
+ != other.getBooleanProperty(BOOLEAN_PROPERTY_FOCUSED)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_FOCUSED;
+ }
+ if (getBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED)
+ != other.getBooleanProperty(BOOLEAN_PROPERTY_ACCESSIBILITY_FOCUSED)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED;
+ }
+ if (getBooleanProperty(BOOLEAN_PROPERTY_PICTURE_IN_PICTURE)
+ != other.getBooleanProperty(BOOLEAN_PROPERTY_PICTURE_IN_PICTURE)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_PIP;
+ }
+ if (mParentId != other.mParentId) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_PARENT;
+ }
+ if (!Objects.equals(mChildIds, other.mChildIds)) {
+ changes |= AccessibilityEvent.WINDOWS_CHANGE_CHILDREN;
+ }
+ return changes;
+ }
+
public static final Parcelable.Creator<AccessibilityWindowInfo> CREATOR =
new Creator<AccessibilityWindowInfo>() {
@Override
diff --git a/android/view/accessibility/SendViewScrolledAccessibilityEvent.java b/android/view/accessibility/SendViewScrolledAccessibilityEvent.java
new file mode 100644
index 00000000..40a1b6a2
--- /dev/null
+++ b/android/view/accessibility/SendViewScrolledAccessibilityEvent.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 android.view.accessibility;
+
+
+import android.annotation.NonNull;
+import android.view.View;
+
+/**
+ * Sender for {@link AccessibilityEvent#TYPE_VIEW_SCROLLED} accessibility event.
+ *
+ * @hide
+ */
+public class SendViewScrolledAccessibilityEvent extends ThrottlingAccessibilityEventSender {
+
+ public int mDeltaX;
+ public int mDeltaY;
+
+ /**
+ * Post a scroll event to be sent for the given view
+ */
+ public void post(View source, int dx, int dy) {
+ if (!isPendingFor(source)) sendNowIfPending();
+
+ mDeltaX += dx;
+ mDeltaY += dy;
+
+ if (!isPendingFor(source)) scheduleFor(source);
+ }
+
+ @Override
+ protected void performSendEvent(@NonNull View source) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_SCROLLED);
+ event.setScrollDeltaX(mDeltaX);
+ event.setScrollDeltaY(mDeltaY);
+ source.sendAccessibilityEventUnchecked(event);
+ }
+
+ @Override
+ protected void resetState(@NonNull View source) {
+ mDeltaX = 0;
+ mDeltaY = 0;
+ }
+}
diff --git a/android/view/accessibility/SendWindowContentChangedAccessibilityEvent.java b/android/view/accessibility/SendWindowContentChangedAccessibilityEvent.java
new file mode 100644
index 00000000..df38fba5
--- /dev/null
+++ b/android/view/accessibility/SendWindowContentChangedAccessibilityEvent.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 android.view.accessibility;
+
+
+import static com.android.internal.util.ObjectUtils.firstNotNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.view.View;
+import android.view.ViewParent;
+
+import java.util.HashSet;
+
+/**
+ * @hide
+ */
+public class SendWindowContentChangedAccessibilityEvent
+ extends ThrottlingAccessibilityEventSender {
+
+ private int mChangeTypes = 0;
+
+ private HashSet<View> mTempHashSet;
+
+ @Override
+ protected void performSendEvent(@NonNull View source) {
+ AccessibilityEvent event = AccessibilityEvent.obtain();
+ event.setEventType(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED);
+ event.setContentChangeTypes(mChangeTypes);
+ source.sendAccessibilityEventUnchecked(event);
+ }
+
+ @Override
+ protected void resetState(@Nullable View source) {
+ if (source != null) {
+ source.resetSubtreeAccessibilityStateChanged();
+ }
+ mChangeTypes = 0;
+ }
+
+ /**
+ * Post the {@link AccessibilityEvent#TYPE_WINDOW_CONTENT_CHANGED} event with the given
+ * {@link AccessibilityEvent#getContentChangeTypes change type} for the given view
+ */
+ public void runOrPost(View source, int changeType) {
+ if (source.getAccessibilityLiveRegion() != View.ACCESSIBILITY_LIVE_REGION_NONE) {
+ sendNowIfPending();
+ mChangeTypes = changeType;
+ sendNow(source);
+ } else {
+ mChangeTypes |= changeType;
+ scheduleFor(source);
+ }
+ }
+
+ @Override
+ protected @Nullable View tryMerge(@NonNull View oldSource, @NonNull View newSource) {
+ // If there is no common predecessor, then oldSource points to
+ // a removed view, hence in this case always prefer the newSource.
+ return firstNotNull(
+ getCommonPredecessor(oldSource, newSource),
+ newSource);
+ }
+
+ private View getCommonPredecessor(View first, View second) {
+ if (mTempHashSet == null) {
+ mTempHashSet = new HashSet<>();
+ }
+ HashSet<View> seen = mTempHashSet;
+ seen.clear();
+ View firstCurrent = first;
+ while (firstCurrent != null) {
+ seen.add(firstCurrent);
+ ViewParent firstCurrentParent = firstCurrent.getParent();
+ if (firstCurrentParent instanceof View) {
+ firstCurrent = (View) firstCurrentParent;
+ } else {
+ firstCurrent = null;
+ }
+ }
+ View secondCurrent = second;
+ while (secondCurrent != null) {
+ if (seen.contains(secondCurrent)) {
+ seen.clear();
+ return secondCurrent;
+ }
+ ViewParent secondCurrentParent = secondCurrent.getParent();
+ if (secondCurrentParent instanceof View) {
+ secondCurrent = (View) secondCurrentParent;
+ } else {
+ secondCurrent = null;
+ }
+ }
+ seen.clear();
+ return null;
+ }
+}
diff --git a/android/view/accessibility/ThrottlingAccessibilityEventSender.java b/android/view/accessibility/ThrottlingAccessibilityEventSender.java
new file mode 100644
index 00000000..66fa3010
--- /dev/null
+++ b/android/view/accessibility/ThrottlingAccessibilityEventSender.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.accessibility;
+
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewRootImpl;
+import android.view.ViewRootImpl.CalledFromWrongThreadException;
+
+/**
+ * A throttling {@link AccessibilityEvent} sender that relies on its currently associated
+ * 'source' view's {@link View#postDelayed delayed execution} to delay and possibly
+ * {@link #tryMerge merge} together any events that come in less than
+ * {@link ViewConfiguration#getSendRecurringAccessibilityEventsInterval
+ * the configured amount of milliseconds} apart.
+ *
+ * The suggested usage is to create a singleton extending this class, holding any state specific to
+ * the particular event type that the subclass represents, and have an 'entrypoint' method that
+ * delegates to {@link #scheduleFor(View)}.
+ * For example:
+ *
+ * {@code
+ * public void post(View view, String text, int resId) {
+ * mText = text;
+ * mId = resId;
+ * scheduleFor(view);
+ * }
+ * }
+ *
+ * @see #scheduleFor(View)
+ * @see #tryMerge(View, View)
+ * @see #performSendEvent(View)
+ * @hide
+ */
+public abstract class ThrottlingAccessibilityEventSender {
+
+ private static final boolean DEBUG = false;
+ private static final String LOG_TAG = "ThrottlingA11ySender";
+
+ View mSource;
+ private long mLastSendTimeMillis = Long.MIN_VALUE;
+ private boolean mIsPending = false;
+
+ private final Runnable mWorker = () -> {
+ View source = mSource;
+ if (DEBUG) Log.d(LOG_TAG, thisClass() + ".run(mSource = " + source + ")");
+
+ if (!checkAndResetIsPending() || source == null) {
+ resetStateInternal();
+ return;
+ }
+
+ // Accessibility may be turned off while we were waiting
+ if (isAccessibilityEnabled(source)) {
+ mLastSendTimeMillis = SystemClock.uptimeMillis();
+ performSendEvent(source);
+ }
+ resetStateInternal();
+ };
+
+ /**
+ * Populate and send an {@link AccessibilityEvent} using the given {@code source} view, as well
+ * as any extra data from this instance's state.
+ *
+ * Send the event via {@link View#sendAccessibilityEventUnchecked(AccessibilityEvent)} or
+ * {@link View#sendAccessibilityEvent(int)} on the provided {@code source} view to allow for
+ * overrides of those methods on {@link View} subclasses to take effect, and/or make sure that
+ * an {@link View#getAccessibilityDelegate() accessibility delegate} is not ignored if any.
+ */
+ protected abstract void performSendEvent(@NonNull View source);
+
+ /**
+ * Perform optional cleanup after {@link #performSendEvent}
+ *
+ * @param source the view this event was associated with
+ */
+ protected abstract void resetState(@Nullable View source);
+
+ /**
+ * Attempt to merge the pending events for source views {@code oldSource} and {@code newSource}
+ * into one, with source set to the resulting {@link View}
+ *
+ * A result of {@code null} means merger is not possible, resulting in the currently pending
+ * event being flushed before proceeding.
+ */
+ protected @Nullable View tryMerge(@NonNull View oldSource, @NonNull View newSource) {
+ return null;
+ }
+
+ /**
+ * Schedules a {@link #performSendEvent} with the source {@link View} set to given
+ * {@code source}
+ *
+ * If an event is already scheduled a {@link #tryMerge merge} will be attempted.
+ * If merging is not possible (as indicated by the null result from {@link #tryMerge}),
+ * the currently scheduled event will be {@link #sendNow sent immediately} and the new one
+ * will be scheduled afterwards.
+ */
+ protected final void scheduleFor(@NonNull View source) {
+ if (DEBUG) Log.d(LOG_TAG, thisClass() + ".scheduleFor(source = " + source + ")");
+
+ Handler uiHandler = source.getHandler();
+ if (uiHandler == null || uiHandler.getLooper() != Looper.myLooper()) {
+ CalledFromWrongThreadException e = new CalledFromWrongThreadException(
+ "Expected to be called from main thread but was called from "
+ + Thread.currentThread());
+ // TODO: Throw the exception
+ Log.e(LOG_TAG, "Accessibility content change on non-UI thread. Future Android "
+ + "versions will throw an exception.", e);
+ }
+
+ if (!isAccessibilityEnabled(source)) return;
+
+ if (mIsPending) {
+ View merged = tryMerge(mSource, source);
+ if (merged != null) {
+ setSource(merged);
+ return;
+ } else {
+ sendNow();
+ }
+ }
+
+ setSource(source);
+
+ final long timeSinceLastMillis = SystemClock.uptimeMillis() - mLastSendTimeMillis;
+ final long minEventIntervalMillis =
+ ViewConfiguration.getSendRecurringAccessibilityEventsInterval();
+ if (timeSinceLastMillis >= minEventIntervalMillis) {
+ sendNow();
+ } else {
+ mSource.postDelayed(mWorker, minEventIntervalMillis - timeSinceLastMillis);
+ }
+ }
+
+ static boolean isAccessibilityEnabled(@NonNull View contextProvider) {
+ return AccessibilityManager.getInstance(contextProvider.getContext()).isEnabled();
+ }
+
+ protected final void sendNow(View source) {
+ setSource(source);
+ sendNow();
+ }
+
+ private void sendNow() {
+ mSource.removeCallbacks(mWorker);
+ mWorker.run();
+ }
+
+ /**
+ * Flush the event if one is pending
+ */
+ public void sendNowIfPending() {
+ if (mIsPending) sendNow();
+ }
+
+ /**
+ * Cancel the event if one is pending and is for the given view
+ */
+ public final void cancelIfPendingFor(@NonNull View source) {
+ if (isPendingFor(source)) cancelIfPending(this);
+ }
+
+ /**
+ * @return whether an event is currently pending for the given source view
+ */
+ protected final boolean isPendingFor(@Nullable View source) {
+ return mIsPending && mSource == source;
+ }
+
+ /**
+ * Cancel the event if one is not null and pending
+ */
+ public static void cancelIfPending(@Nullable ThrottlingAccessibilityEventSender sender) {
+ if (sender == null || !sender.checkAndResetIsPending()) return;
+ sender.mSource.removeCallbacks(sender.mWorker);
+ sender.resetStateInternal();
+ }
+
+ void resetStateInternal() {
+ if (DEBUG) Log.d(LOG_TAG, thisClass() + ".resetStateInternal()");
+
+ resetState(mSource);
+ setSource(null);
+ }
+
+ boolean checkAndResetIsPending() {
+ if (mIsPending) {
+ mIsPending = false;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ private void setSource(@Nullable View source) {
+ if (DEBUG) Log.d(LOG_TAG, thisClass() + ".setSource(" + source + ")");
+
+ if (source == null && mIsPending) {
+ Log.e(LOG_TAG, "mSource nullified while callback still pending: " + this);
+ return;
+ }
+
+ if (source != null && !mIsPending) {
+ // At most one can be pending at any given time
+ View oldSource = mSource;
+ if (oldSource != null) {
+ ViewRootImpl viewRootImpl = oldSource.getViewRootImpl();
+ if (viewRootImpl != null) {
+ viewRootImpl.flushPendingAccessibilityEvents();
+ }
+ }
+ mIsPending = true;
+ }
+ mSource = source;
+ }
+
+ String thisClass() {
+ return getClass().getSimpleName();
+ }
+
+ @Override
+ public String toString() {
+ return thisClass() + "(" + mSource + ")";
+ }
+
+}
diff --git a/android/view/animation/AnimationUtils.java b/android/view/animation/AnimationUtils.java
index f5c36139..990fbdb0 100644
--- a/android/view/animation/AnimationUtils.java
+++ b/android/view/animation/AnimationUtils.java
@@ -156,6 +156,8 @@ public class AnimationUtils {
anim = new RotateAnimation(c, attrs);
} else if (name.equals("translate")) {
anim = new TranslateAnimation(c, attrs);
+ } else if (name.equals("cliprect")) {
+ anim = new ClipRectAnimation(c, attrs);
} else {
throw new RuntimeException("Unknown animation name: " + parser.getName());
}
diff --git a/android/view/animation/ClipRectAnimation.java b/android/view/animation/ClipRectAnimation.java
index e194927e..21509d3a 100644
--- a/android/view/animation/ClipRectAnimation.java
+++ b/android/view/animation/ClipRectAnimation.java
@@ -16,7 +16,11 @@
package android.view.animation;
+import android.content.Context;
+import android.content.res.TypedArray;
import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
/**
* An animation that controls the clip of an object. See the
@@ -26,8 +30,84 @@ import android.graphics.Rect;
* @hide
*/
public class ClipRectAnimation extends Animation {
- protected Rect mFromRect = new Rect();
- protected Rect mToRect = new Rect();
+ protected final Rect mFromRect = new Rect();
+ protected final Rect mToRect = new Rect();
+
+ private int mFromLeftType = ABSOLUTE;
+ private int mFromTopType = ABSOLUTE;
+ private int mFromRightType = ABSOLUTE;
+ private int mFromBottomType = ABSOLUTE;
+
+ private int mToLeftType = ABSOLUTE;
+ private int mToTopType = ABSOLUTE;
+ private int mToRightType = ABSOLUTE;
+ private int mToBottomType = ABSOLUTE;
+
+ private float mFromLeftValue;
+ private float mFromTopValue;
+ private float mFromRightValue;
+ private float mFromBottomValue;
+
+ private float mToLeftValue;
+ private float mToTopValue;
+ private float mToRightValue;
+ private float mToBottomValue;
+
+ /**
+ * Constructor used when a ClipRectAnimation is loaded from a resource.
+ *
+ * @param context Application context to use
+ * @param attrs Attribute set from which to read values
+ */
+ public ClipRectAnimation(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ TypedArray a = context.obtainStyledAttributes(attrs,
+ com.android.internal.R.styleable.ClipRectAnimation);
+
+ Description d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_fromLeft));
+ mFromLeftType = d.type;
+ mFromLeftValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_fromTop));
+ mFromTopType = d.type;
+ mFromTopValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_fromRight));
+ mFromRightType = d.type;
+ mFromRightValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_fromBottom));
+ mFromBottomType = d.type;
+ mFromBottomValue = d.value;
+
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_toLeft));
+ mToLeftType = d.type;
+ mToLeftValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_toTop));
+ mToTopType = d.type;
+ mToTopValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_toRight));
+ mToRightType = d.type;
+ mToRightValue = d.value;
+
+ d = Description.parseValue(a.peekValue(
+ com.android.internal.R.styleable.ClipRectAnimation_toBottom));
+ mToBottomType = d.type;
+ mToBottomValue = d.value;
+
+ a.recycle();
+ }
/**
* Constructor to use when building a ClipRectAnimation from code
@@ -39,8 +119,15 @@ public class ClipRectAnimation extends Animation {
if (fromClip == null || toClip == null) {
throw new RuntimeException("Expected non-null animation clip rects");
}
- mFromRect.set(fromClip);
- mToRect.set(toClip);
+ mFromLeftValue = fromClip.left;
+ mFromTopValue = fromClip.top;
+ mFromRightValue= fromClip.right;
+ mFromBottomValue = fromClip.bottom;
+
+ mToLeftValue = toClip.left;
+ mToTopValue = toClip.top;
+ mToRightValue= toClip.right;
+ mToBottomValue = toClip.bottom;
}
/**
@@ -48,8 +135,7 @@ public class ClipRectAnimation extends Animation {
*/
public ClipRectAnimation(int fromL, int fromT, int fromR, int fromB,
int toL, int toT, int toR, int toB) {
- mFromRect.set(fromL, fromT, fromR, fromB);
- mToRect.set(toL, toT, toR, toB);
+ this(new Rect(fromL, fromT, fromR, fromB), new Rect(toL, toT, toR, toB));
}
@Override
@@ -65,4 +151,17 @@ public class ClipRectAnimation extends Animation {
public boolean willChangeTransformationMatrix() {
return false;
}
+
+ @Override
+ public void initialize(int width, int height, int parentWidth, int parentHeight) {
+ super.initialize(width, height, parentWidth, parentHeight);
+ mFromRect.set((int) resolveSize(mFromLeftType, mFromLeftValue, width, parentWidth),
+ (int) resolveSize(mFromTopType, mFromTopValue, height, parentHeight),
+ (int) resolveSize(mFromRightType, mFromRightValue, width, parentWidth),
+ (int) resolveSize(mFromBottomType, mFromBottomValue, height, parentHeight));
+ mToRect.set((int) resolveSize(mToLeftType, mToLeftValue, width, parentWidth),
+ (int) resolveSize(mToTopType, mToTopValue, height, parentHeight),
+ (int) resolveSize(mToRightType, mToRightValue, width, parentWidth),
+ (int) resolveSize(mToBottomType, mToBottomValue, height, parentHeight));
+ }
}
diff --git a/android/view/autofill/AutofillManager.java b/android/view/autofill/AutofillManager.java
index 26974545..4b24a71c 100644
--- a/android/view/autofill/AutofillManager.java
+++ b/android/view/autofill/AutofillManager.java
@@ -53,6 +53,8 @@ import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -168,7 +170,6 @@ public final class AutofillManager {
public static final String EXTRA_CLIENT_STATE =
"android.view.autofill.extra.CLIENT_STATE";
-
/** @hide */
public static final String EXTRA_RESTORE_SESSION_TOKEN =
"android.view.autofill.extra.RESTORE_SESSION_TOKEN";
@@ -258,6 +259,12 @@ public final class AutofillManager {
public static final int STATE_DISABLED_BY_SERVICE = 4;
/**
+ * Timeout in ms for calls to the field classification service.
+ * @hide
+ */
+ public static final int FC_SERVICE_TIMEOUT = 5000;
+
+ /**
* Makes an authentication id from a request id and a dataset id.
*
* @param requestId The request id.
@@ -340,6 +347,10 @@ public final class AutofillManager {
@GuardedBy("mLock")
@Nullable private AutofillId mSaveTriggerId;
+ /** set to true when onInvisibleForAutofill is called, used by onAuthenticationResult */
+ @GuardedBy("mLock")
+ private boolean mOnInvisibleCalled;
+
/** If set, session is commited when the activity is finished; otherwise session is canceled. */
@GuardedBy("mLock")
private boolean mSaveOnFinish;
@@ -396,6 +407,11 @@ public final class AutofillManager {
boolean isVisibleForAutofill();
/**
+ * Client might disable enter/exit event e.g. when activity is paused.
+ */
+ boolean isDisablingEnterExitEventForAutofill();
+
+ /**
* Finds views by traversing the hierarchies of the client.
*
* @param viewIds The autofill ids of the views to find
@@ -498,6 +514,19 @@ public final class AutofillManager {
}
/**
+ * Called once the client becomes invisible.
+ *
+ * @see AutofillClient#isVisibleForAutofill()
+ *
+ * {@hide}
+ */
+ public void onInvisibleForAutofill() {
+ synchronized (mLock) {
+ mOnInvisibleCalled = true;
+ }
+ }
+
+ /**
* Save state before activity lifecycle
*
* @param outState Place to store the state
@@ -622,21 +651,45 @@ public final class AutofillManager {
return false;
}
+ private boolean isClientVisibleForAutofillLocked() {
+ final AutofillClient client = getClient();
+ return client != null && client.isVisibleForAutofill();
+ }
+
+ private boolean isClientDisablingEnterExitEvent() {
+ final AutofillClient client = getClient();
+ return client != null && client.isDisablingEnterExitEventForAutofill();
+ }
+
private void notifyViewEntered(@NonNull View view, int flags) {
if (!hasAutofillFeature()) {
return;
}
- AutofillCallback callback = null;
+ AutofillCallback callback;
synchronized (mLock) {
- if (shouldIgnoreViewEnteredLocked(view, flags)) return;
+ callback = notifyViewEnteredLocked(view, flags);
+ }
- ensureServiceClientAddedIfNeededLocked();
+ if (callback != null) {
+ mCallback.onAutofillEvent(view, AutofillCallback.EVENT_INPUT_UNAVAILABLE);
+ }
+ }
- if (!mEnabled) {
- if (mCallback != null) {
- callback = mCallback;
- }
- } else {
+ /** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */
+ private AutofillCallback notifyViewEnteredLocked(@NonNull View view, int flags) {
+ if (shouldIgnoreViewEnteredLocked(view, flags)) return null;
+
+ AutofillCallback callback = null;
+
+ ensureServiceClientAddedIfNeededLocked();
+
+ if (!mEnabled) {
+ if (mCallback != null) {
+ callback = mCallback;
+ }
+ } else {
+ // don't notify entered when Activity is already in background
+ if (!isClientDisablingEnterExitEvent()) {
final AutofillId id = getAutofillId(view);
final AutofillValue value = view.getAutofillValue();
@@ -649,10 +702,7 @@ public final class AutofillManager {
}
}
}
-
- if (callback != null) {
- mCallback.onAutofillEvent(view, AutofillCallback.EVENT_INPUT_UNAVAILABLE);
- }
+ return callback;
}
/**
@@ -665,9 +715,16 @@ public final class AutofillManager {
return;
}
synchronized (mLock) {
- ensureServiceClientAddedIfNeededLocked();
+ notifyViewExitedLocked(view);
+ }
+ }
- if (mEnabled && isActiveLocked()) {
+ void notifyViewExitedLocked(@NonNull View view) {
+ ensureServiceClientAddedIfNeededLocked();
+
+ if (mEnabled && isActiveLocked()) {
+ // dont notify exited when Activity is already in background
+ if (!isClientDisablingEnterExitEvent()) {
final AutofillId id = getAutofillId(view);
// Update focus on existing session.
@@ -718,7 +775,7 @@ public final class AutofillManager {
}
}
if (mTrackedViews != null) {
- mTrackedViews.notifyViewVisibilityChanged(id, isVisible);
+ mTrackedViews.notifyViewVisibilityChangedLocked(id, isVisible);
}
}
}
@@ -751,17 +808,32 @@ public final class AutofillManager {
if (!hasAutofillFeature()) {
return;
}
- AutofillCallback callback = null;
+ AutofillCallback callback;
synchronized (mLock) {
- if (shouldIgnoreViewEnteredLocked(view, flags)) return;
+ callback = notifyViewEnteredLocked(view, virtualId, bounds, flags);
+ }
- ensureServiceClientAddedIfNeededLocked();
+ if (callback != null) {
+ callback.onAutofillEvent(view, virtualId,
+ AutofillCallback.EVENT_INPUT_UNAVAILABLE);
+ }
+ }
- if (!mEnabled) {
- if (mCallback != null) {
- callback = mCallback;
- }
- } else {
+ /** Returns AutofillCallback if need fire EVENT_INPUT_UNAVAILABLE */
+ private AutofillCallback notifyViewEnteredLocked(View view, int virtualId, Rect bounds,
+ int flags) {
+ AutofillCallback callback = null;
+ if (shouldIgnoreViewEnteredLocked(view, flags)) return callback;
+
+ ensureServiceClientAddedIfNeededLocked();
+
+ if (!mEnabled) {
+ if (mCallback != null) {
+ callback = mCallback;
+ }
+ } else {
+ // don't notify entered when Activity is already in background
+ if (!isClientDisablingEnterExitEvent()) {
final AutofillId id = getAutofillId(view, virtualId);
if (!isActiveLocked()) {
@@ -773,11 +845,7 @@ public final class AutofillManager {
}
}
}
-
- if (callback != null) {
- callback.onAutofillEvent(view, virtualId,
- AutofillCallback.EVENT_INPUT_UNAVAILABLE);
- }
+ return callback;
}
/**
@@ -791,9 +859,16 @@ public final class AutofillManager {
return;
}
synchronized (mLock) {
- ensureServiceClientAddedIfNeededLocked();
+ notifyViewExitedLocked(view, virtualId);
+ }
+ }
- if (mEnabled && isActiveLocked()) {
+ private void notifyViewExitedLocked(@NonNull View view, int virtualId) {
+ ensureServiceClientAddedIfNeededLocked();
+
+ if (mEnabled && isActiveLocked()) {
+ // don't notify exited when Activity is already in background
+ if (!isClientDisablingEnterExitEvent()) {
final AutofillId id = getAutofillId(view, virtualId);
// Update focus on existing session.
@@ -1027,7 +1102,9 @@ public final class AutofillManager {
* 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.
+ * <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.
*
* @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.
@@ -1079,6 +1156,47 @@ public final class AutofillManager {
}
/**
+ * Gets the name of the default algorithm used for
+ * <a href="AutofillService.html#FieldClassification">field classification</a>.
+ *
+ * <p>The default algorithm is used when the algorithm on {@link UserData} is invalid or not
+ * set.
+ *
+ * <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.
+ */
+ @Nullable
+ public String getDefaultFieldClassificationAlgorithm() {
+ try {
+ return mService.getDefaultFieldClassificationAlgorithm();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return null;
+ }
+ }
+
+ /**
+ * Gets the name of all algorithms currently available 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 returns an empty list if the caller currently doesn't have an enabled autofill service
+ * for the user.
+ */
+ @NonNull
+ public List<String> getAvailableFieldClassificationAlgorithms() {
+ final String[] algorithms;
+ try {
+ algorithms = mService.getAvailableFieldClassificationAlgorithms();
+ return algorithms != null ? Arrays.asList(algorithms) : Collections.emptyList();
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ return null;
+ }
+ }
+
+ /**
* Returns {@code true} if autofill is supported by the current device and
* is supported for this user.
*
@@ -1109,7 +1227,7 @@ public final class AutofillManager {
}
/** @hide */
- public void onAuthenticationResult(int authenticationId, Intent data) {
+ public void onAuthenticationResult(int authenticationId, Intent data, View focusView) {
if (!hasAutofillFeature()) {
return;
}
@@ -1121,9 +1239,24 @@ public final class AutofillManager {
if (sDebug) Log.d(TAG, "onAuthenticationResult(): d=" + data);
synchronized (mLock) {
- if (!isActiveLocked() || data == null) {
+ if (!isActiveLocked()) {
+ return;
+ }
+ // If authenticate activity closes itself during onCreate(), there is no onStop/onStart
+ // of app activity. We enforce enter event to re-show fill ui in such case.
+ // CTS example:
+ // LoginActivityTest#testDatasetAuthTwoFieldsUserCancelsFirstAttempt
+ // LoginActivityTest#testFillResponseAuthBothFieldsUserCancelsFirstAttempt
+ if (!mOnInvisibleCalled && focusView != null
+ && focusView.canNotifyAutofillEnterExitEvent()) {
+ notifyViewExitedLocked(focusView);
+ notifyViewEnteredLocked(focusView, 0);
+ }
+ if (data == null) {
+ // data is set to null when result is not RESULT_OK
return;
}
+
final Parcelable result = data.getParcelableExtra(EXTRA_AUTHENTICATION_RESULT);
final Bundle responseData = new Bundle();
responseData.putParcelable(EXTRA_AUTHENTICATION_RESULT, result);
@@ -1356,6 +1489,9 @@ public final class AutofillManager {
if (sessionId == mSessionId) {
final AutofillClient client = getClient();
if (client != null) {
+ // clear mOnInvisibleCalled and we will see if receive onInvisibleForAutofill()
+ // before onAuthenticationResult()
+ mOnInvisibleCalled = false;
client.autofillCallbackAuthenticate(authenticationId, intent, fillInIntent);
}
}
@@ -1721,6 +1857,7 @@ public final class AutofillManager {
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);
+ pw.print(pfx); pw.print("onInvisibleCalled "); pw.println(mOnInvisibleCalled);
pw.print(pfx); pw.print("last autofilled data: "); pw.println(mLastAutofilledData);
pw.print(pfx); pw.print("tracked views: ");
if (mTrackedViews == null) {
@@ -1891,15 +2028,13 @@ public final class AutofillManager {
* @param id the id of the view/virtual view whose visibility changed.
* @param isVisible visible if the view is visible in the view hierarchy.
*/
- void notifyViewVisibilityChanged(@NonNull AutofillId id, boolean isVisible) {
- AutofillClient client = getClient();
-
+ void notifyViewVisibilityChangedLocked(@NonNull AutofillId id, boolean isVisible) {
if (sDebug) {
Log.d(TAG, "notifyViewVisibilityChanged(): id=" + id + " isVisible="
+ isVisible);
}
- if (client != null && client.isVisibleForAutofill()) {
+ if (isClientVisibleForAutofillLocked()) {
if (isVisible) {
if (isInSet(mInvisibleTrackedIds, id)) {
mInvisibleTrackedIds = removeFromSet(mInvisibleTrackedIds, id);
diff --git a/android/view/autofill/AutofillPopupWindow.java b/android/view/autofill/AutofillPopupWindow.java
index 5cba21e3..e80fdd93 100644
--- a/android/view/autofill/AutofillPopupWindow.java
+++ b/android/view/autofill/AutofillPopupWindow.java
@@ -78,8 +78,10 @@ public class AutofillPopupWindow extends PopupWindow {
public AutofillPopupWindow(@NonNull IAutofillWindowPresenter presenter) {
mWindowPresenter = new WindowPresenter(presenter);
+ setTouchModal(false);
setOutsideTouchable(true);
- setInputMethodMode(INPUT_METHOD_NEEDED);
+ setInputMethodMode(INPUT_METHOD_NOT_NEEDED);
+ setFocusable(true);
}
@Override
diff --git a/android/view/inputmethod/ExtractedText.java b/android/view/inputmethod/ExtractedText.java
index 003f221d..1eb300ea 100644
--- a/android/view/inputmethod/ExtractedText.java
+++ b/android/view/inputmethod/ExtractedText.java
@@ -29,6 +29,8 @@ import android.text.TextUtils;
public class ExtractedText implements Parcelable {
/**
* The text that has been extracted.
+ *
+ * @see android.widget.TextView#getText()
*/
public CharSequence text;
@@ -88,6 +90,8 @@ public class ExtractedText implements Parcelable {
/**
* The hint that has been extracted.
+ *
+ * @see android.widget.TextView#getHint()
*/
public CharSequence hint;
diff --git a/android/view/inputmethod/InputConnection.java b/android/view/inputmethod/InputConnection.java
index 57f9895f..eba91763 100644
--- a/android/view/inputmethod/InputConnection.java
+++ b/android/view/inputmethod/InputConnection.java
@@ -1,17 +1,17 @@
/*
- * Copyright (C) 2007-2008 The Android Open Source Project
+ * Copyright (C) 2007 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. 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.inputmethod;
@@ -21,6 +21,7 @@ import android.annotation.Nullable;
import android.inputmethodservice.InputMethodService;
import android.os.Bundle;
import android.os.Handler;
+import android.os.LocaleList;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
@@ -131,13 +132,13 @@ public interface InputConnection {
* spans. <strong>Editor authors</strong>: you should strive to
* send text with styles if possible, but it is not required.
*/
- static final int GET_TEXT_WITH_STYLES = 0x0001;
+ int GET_TEXT_WITH_STYLES = 0x0001;
/**
* Flag for use with {@link #getExtractedText} to indicate you
* would like to receive updates when the extracted text changes.
*/
- public static final int GET_EXTRACTED_TEXT_MONITOR = 0x0001;
+ int GET_EXTRACTED_TEXT_MONITOR = 0x0001;
/**
* Get <var>n</var> characters of text before the current cursor
@@ -176,7 +177,7 @@ public interface InputConnection {
* @return the text before the cursor position; the length of the
* returned text might be less than <var>n</var>.
*/
- public CharSequence getTextBeforeCursor(int n, int flags);
+ CharSequence getTextBeforeCursor(int n, int flags);
/**
* Get <var>n</var> characters of text after the current cursor
@@ -215,7 +216,7 @@ public interface InputConnection {
* @return the text after the cursor position; the length of the
* returned text might be less than <var>n</var>.
*/
- public CharSequence getTextAfterCursor(int n, int flags);
+ CharSequence getTextAfterCursor(int n, int flags);
/**
* Gets the selected text, if any.
@@ -249,7 +250,7 @@ public interface InputConnection {
* later, returns false when the target application does not implement
* this method.
*/
- public CharSequence getSelectedText(int flags);
+ CharSequence getSelectedText(int flags);
/**
* Retrieve the current capitalization mode in effect at the
@@ -279,7 +280,7 @@ public interface InputConnection {
* @return the caps mode flags that are in effect at the current
* cursor position. See TYPE_TEXT_FLAG_CAPS_* in {@link android.text.InputType}.
*/
- public int getCursorCapsMode(int reqModes);
+ int getCursorCapsMode(int reqModes);
/**
* Retrieve the current text in the input connection's editor, and
@@ -314,8 +315,7 @@ public interface InputConnection {
* longer valid of the editor can't comply with the request for
* some reason.
*/
- public ExtractedText getExtractedText(ExtractedTextRequest request,
- int flags);
+ ExtractedText getExtractedText(ExtractedTextRequest request, int flags);
/**
* Delete <var>beforeLength</var> characters of text before the
@@ -342,8 +342,8 @@ public interface InputConnection {
* delete more characters than are in the editor, as that may have
* ill effects on the application. Calling this method will cause
* the editor to call
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * on your service after the batch input is over.</p>
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on your service after the batch input is over.</p>
*
* <p><strong>Editor authors:</strong> please be careful of race
* conditions in implementing this call. An IME can make a change
@@ -369,7 +369,7 @@ public interface InputConnection {
* that range.
* @return true on success, false if the input connection is no longer valid.
*/
- public boolean deleteSurroundingText(int beforeLength, int afterLength);
+ boolean deleteSurroundingText(int beforeLength, int afterLength);
/**
* A variant of {@link #deleteSurroundingText(int, int)}. Major differences are:
@@ -397,7 +397,7 @@ public interface InputConnection {
* @return true on success, false if the input connection is no longer valid. Returns
* {@code false} when the target application does not implement this method.
*/
- public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength);
+ boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength);
/**
* Replace the currently composing text with the given text, and
@@ -416,8 +416,8 @@ public interface InputConnection {
* <p>This is usually called by IMEs to add or remove or change
* characters in the composing span. Calling this method will
* cause the editor to call
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * on the current IME after the batch input is over.</p>
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.</p>
*
* <p><strong>Editor authors:</strong> please keep in mind the
* text may be very similar or completely different than what was
@@ -455,7 +455,7 @@ public interface InputConnection {
* @return true on success, false if the input connection is no longer
* valid.
*/
- public boolean setComposingText(CharSequence text, int newCursorPosition);
+ boolean setComposingText(CharSequence text, int newCursorPosition);
/**
* Mark a certain region of text as composing text. If there was a
@@ -474,8 +474,8 @@ public interface InputConnection {
* <p>Since this does not change the contents of the text, editors should not call
* {@link InputMethodManager#updateSelection(View, int, int, int, int)} and
* IMEs should not receive
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}.
- * </p>
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)}.</p>
*
* <p>This has no impact on the cursor/selection position. It may
* result in the cursor being anywhere inside or outside the
@@ -488,7 +488,7 @@ public interface InputConnection {
* valid. In {@link android.os.Build.VERSION_CODES#N} and later, false is returned when the
* target application does not implement this method.
*/
- public boolean setComposingRegion(int start, int end);
+ boolean setComposingRegion(int start, int end);
/**
* Have the text editor finish whatever composing text is
@@ -507,7 +507,7 @@ public interface InputConnection {
* @return true on success, false if the input connection
* is no longer valid.
*/
- public boolean finishComposingText();
+ boolean finishComposingText();
/**
* Commit text to the text box and set the new cursor position.
@@ -522,8 +522,8 @@ public interface InputConnection {
* then {@link #finishComposingText()}.</p>
*
* <p>Calling this method will cause the editor to call
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * on the current IME after the batch input is over.
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.
* <strong>Editor authors</strong>, for this to happen you need to
* make the changes known to the input method by calling
* {@link InputMethodManager#updateSelection(View, int, int, int, int)},
@@ -543,7 +543,7 @@ public interface InputConnection {
* @return true on success, false if the input connection is no longer
* valid.
*/
- public boolean commitText(CharSequence text, int newCursorPosition);
+ boolean commitText(CharSequence text, int newCursorPosition);
/**
* Commit a completion the user has selected from the possible ones
@@ -569,8 +569,8 @@ public interface InputConnection {
*
* <p>Calling this method (with a valid {@link CompletionInfo} object)
* will cause the editor to call
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * on the current IME after the batch input is over.
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.
* <strong>Editor authors</strong>, for this to happen you need to
* make the changes known to the input method by calling
* {@link InputMethodManager#updateSelection(View, int, int, int, int)},
@@ -581,15 +581,15 @@ public interface InputConnection {
* @return true on success, false if the input connection is no longer
* valid.
*/
- public boolean commitCompletion(CompletionInfo text);
+ boolean commitCompletion(CompletionInfo text);
/**
* Commit a correction automatically performed on the raw user's input. A
* typical example would be to correct typos using a dictionary.
*
* <p>Calling this method will cause the editor to call
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * on the current IME after the batch input is over.
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.
* <strong>Editor authors</strong>, for this to happen you need to
* make the changes known to the input method by calling
* {@link InputMethodManager#updateSelection(View, int, int, int, int)},
@@ -601,7 +601,7 @@ public interface InputConnection {
* In {@link android.os.Build.VERSION_CODES#N} and later, returns false
* when the target application does not implement this method.
*/
- public boolean commitCorrection(CorrectionInfo correctionInfo);
+ boolean commitCorrection(CorrectionInfo correctionInfo);
/**
* Set the selection of the text editor. To set the cursor
@@ -609,8 +609,8 @@ public interface InputConnection {
*
* <p>Since this moves the cursor, calling this method will cause
* the editor to call
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * on the current IME after the batch input is over.
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} on the current IME after the batch input is over.
* <strong>Editor authors</strong>, for this to happen you need to
* make the changes known to the input method by calling
* {@link InputMethodManager#updateSelection(View, int, int, int, int)},
@@ -628,7 +628,7 @@ public interface InputConnection {
* @return true on success, false if the input connection is no longer
* valid.
*/
- public boolean setSelection(int start, int end);
+ boolean setSelection(int start, int end);
/**
* Have the editor perform an action it has said it can do.
@@ -642,7 +642,7 @@ public interface InputConnection {
* @return true on success, false if the input connection is no longer
* valid.
*/
- public boolean performEditorAction(int editorAction);
+ boolean performEditorAction(int editorAction);
/**
* Perform a context menu action on the field. The given id may be one of:
@@ -652,7 +652,7 @@ public interface InputConnection {
* {@link android.R.id#paste}, {@link android.R.id#copyUrl},
* or {@link android.R.id#switchInputMethod}
*/
- public boolean performContextMenuAction(int id);
+ boolean performContextMenuAction(int id);
/**
* Tell the editor that you are starting a batch of editor
@@ -662,8 +662,8 @@ public interface InputConnection {
*
* <p><strong>IME authors:</strong> use this to avoid getting
* calls to
- * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int, int, int)}
- * corresponding to intermediate state. Also, use this to avoid
+ * {@link android.inputmethodservice.InputMethodService#onUpdateSelection(int, int, int, int,
+ * int, int)} corresponding to intermediate state. Also, use this to avoid
* flickers that may arise from displaying intermediate state. Be
* sure to call {@link #endBatchEdit} for each call to this, or
* you may block updates in the editor.</p>
@@ -678,7 +678,7 @@ public interface InputConnection {
* this method starts a batch edit, that means it will always return true
* unless the input connection is no longer valid.
*/
- public boolean beginBatchEdit();
+ boolean beginBatchEdit();
/**
* Tell the editor that you are done with a batch edit previously
@@ -696,7 +696,7 @@ public interface InputConnection {
* the latest one (in other words, if the nesting count is > 0), false
* otherwise or if the input connection is no longer valid.
*/
- public boolean endBatchEdit();
+ boolean endBatchEdit();
/**
* Send a key event to the process that is currently attached
@@ -734,7 +734,7 @@ public interface InputConnection {
* @see KeyCharacterMap#PREDICTIVE
* @see KeyCharacterMap#ALPHA
*/
- public boolean sendKeyEvent(KeyEvent event);
+ boolean sendKeyEvent(KeyEvent event);
/**
* Clear the given meta key pressed states in the given input
@@ -749,7 +749,7 @@ public interface InputConnection {
* @return true on success, false if the input connection is no longer
* valid.
*/
- public boolean clearMetaKeyStates(int states);
+ boolean clearMetaKeyStates(int states);
/**
* Called back when the connected IME switches between fullscreen and normal modes.
@@ -766,7 +766,7 @@ public interface InputConnection {
* devices.
* @see InputMethodManager#isFullscreenMode()
*/
- public boolean reportFullscreenMode(boolean enabled);
+ boolean reportFullscreenMode(boolean enabled);
/**
* API to send private commands from an input method to its
@@ -786,7 +786,7 @@ public interface InputConnection {
* associated editor understood it), false if the input connection is no longer
* valid.
*/
- public boolean performPrivateCommand(String action, Bundle data);
+ boolean performPrivateCommand(String action, Bundle data);
/**
* The editor is requested to call
@@ -794,7 +794,7 @@ public interface InputConnection {
* once, as soon as possible, regardless of cursor/anchor position changes. This flag can be
* used together with {@link #CURSOR_UPDATE_MONITOR}.
*/
- public static final int CURSOR_UPDATE_IMMEDIATE = 1 << 0;
+ int CURSOR_UPDATE_IMMEDIATE = 1 << 0;
/**
* The editor is requested to call
@@ -805,7 +805,7 @@ public interface InputConnection {
* This flag can be used together with {@link #CURSOR_UPDATE_IMMEDIATE}.
* </p>
*/
- public static final int CURSOR_UPDATE_MONITOR = 1 << 1;
+ int CURSOR_UPDATE_MONITOR = 1 << 1;
/**
* Called by the input method to ask the editor for calling back
@@ -821,7 +821,7 @@ public interface InputConnection {
* In {@link android.os.Build.VERSION_CODES#N} and later, returns {@code false} also when the
* target application does not implement this method.
*/
- public boolean requestCursorUpdates(int cursorUpdateMode);
+ boolean requestCursorUpdates(int cursorUpdateMode);
/**
* Called by the {@link InputMethodManager} to enable application developers to specify a
@@ -832,7 +832,7 @@ public interface InputConnection {
*
* @return {@code null} to use the default {@link Handler}.
*/
- public Handler getHandler();
+ Handler getHandler();
/**
* Called by the system up to only once to notify that the system is about to invalidate
@@ -846,7 +846,7 @@ public interface InputConnection {
*
* <p>Note: This does nothing when called from input methods.</p>
*/
- public void closeConnection();
+ void closeConnection();
/**
* When this flag is used, the editor will be able to request read access to the content URI
@@ -863,7 +863,7 @@ public interface InputConnection {
* client is able to request a temporary read-only access even after the current IME is switched
* to any other IME as long as the client keeps {@link InputContentInfo} object.</p>
**/
- public static int INPUT_CONTENT_GRANT_READ_URI_PERMISSION =
+ int INPUT_CONTENT_GRANT_READ_URI_PERMISSION =
android.content.Intent.FLAG_GRANT_READ_URI_PERMISSION; // 0x00000001
/**
@@ -897,6 +897,39 @@ public interface InputConnection {
* @return {@code true} if this request is accepted by the application, whether the request
* is already handled or still being handled in background, {@code false} otherwise.
*/
- public boolean commitContent(@NonNull InputContentInfo inputContentInfo, int flags,
+ boolean commitContent(@NonNull InputContentInfo inputContentInfo, int flags,
@Nullable Bundle opts);
+
+ /**
+ * Called by the input method to tell a hint about the locales of text to be committed.
+ *
+ * <p>This is just a hint for editor authors (and the system) to choose better options when
+ * they have to disambiguate languages, like editor authors can do for input methods with
+ * {@link EditorInfo#hintLocales}.</p>
+ *
+ * <p>The language hint provided by this callback should have higher priority than
+ * {@link InputMethodSubtype#getLanguageTag()}, which cannot be updated dynamically.</p>
+ *
+ * <p>Note that in general it is discouraged for input method to specify
+ * {@link android.text.style.LocaleSpan} when inputting text, mainly because of application
+ * compatibility concerns.</p>
+ * <ul>
+ * <li>When an existing text that already has {@link android.text.style.LocaleSpan} is being
+ * modified by both the input method and application, there is no reliable and easy way to
+ * keep track of who modified {@link android.text.style.LocaleSpan}. For instance, if the
+ * text was updated by JavaScript, it it highly likely that span information is completely
+ * removed, while some input method attempts to preserve spans if possible.</li>
+ * <li>There is no clear semantics regarding whether {@link android.text.style.LocaleSpan}
+ * means a weak (ignorable) hint or a strong hint. This becomes more problematic when
+ * multiple {@link android.text.style.LocaleSpan} instances are specified to the same
+ * text region, especially when those spans are conflicting.</li>
+ * </ul>
+ * @param languageHint list of languages sorted by the priority and/or probability
+ */
+ default void reportLanguageHint(@NonNull LocaleList languageHint) {
+ // Intentionally empty.
+ //
+ // We need to have *some* default implementation for the source compatibility.
+ // See Bug 72127682 for details.
+ }
}
diff --git a/android/view/inputmethod/InputConnectionWrapper.java b/android/view/inputmethod/InputConnectionWrapper.java
index 317730ca..cbe6856b 100644
--- a/android/view/inputmethod/InputConnectionWrapper.java
+++ b/android/view/inputmethod/InputConnectionWrapper.java
@@ -1,23 +1,25 @@
/*
- * Copyright (C) 2007-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. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.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.
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR 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.inputmethod;
+import android.annotation.NonNull;
import android.os.Bundle;
import android.os.Handler;
+import android.os.LocaleList;
import android.view.KeyEvent;
/**
@@ -74,6 +76,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public CharSequence getTextBeforeCursor(int n, int flags) {
return mTarget.getTextBeforeCursor(n, flags);
}
@@ -82,6 +85,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public CharSequence getTextAfterCursor(int n, int flags) {
return mTarget.getTextAfterCursor(n, flags);
}
@@ -90,6 +94,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public CharSequence getSelectedText(int flags) {
return mTarget.getSelectedText(flags);
}
@@ -98,6 +103,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public int getCursorCapsMode(int reqModes) {
return mTarget.getCursorCapsMode(reqModes);
}
@@ -106,6 +112,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
return mTarget.getExtractedText(request, flags);
}
@@ -114,6 +121,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
return mTarget.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
}
@@ -122,6 +130,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
return mTarget.deleteSurroundingText(beforeLength, afterLength);
}
@@ -130,6 +139,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
return mTarget.setComposingText(text, newCursorPosition);
}
@@ -138,6 +148,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean setComposingRegion(int start, int end) {
return mTarget.setComposingRegion(start, end);
}
@@ -146,6 +157,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean finishComposingText() {
return mTarget.finishComposingText();
}
@@ -154,6 +166,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean commitText(CharSequence text, int newCursorPosition) {
return mTarget.commitText(text, newCursorPosition);
}
@@ -162,6 +175,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean commitCompletion(CompletionInfo text) {
return mTarget.commitCompletion(text);
}
@@ -170,6 +184,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean commitCorrection(CorrectionInfo correctionInfo) {
return mTarget.commitCorrection(correctionInfo);
}
@@ -178,6 +193,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean setSelection(int start, int end) {
return mTarget.setSelection(start, end);
}
@@ -186,6 +202,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean performEditorAction(int editorAction) {
return mTarget.performEditorAction(editorAction);
}
@@ -194,6 +211,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean performContextMenuAction(int id) {
return mTarget.performContextMenuAction(id);
}
@@ -202,6 +220,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean beginBatchEdit() {
return mTarget.beginBatchEdit();
}
@@ -210,6 +229,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean endBatchEdit() {
return mTarget.endBatchEdit();
}
@@ -218,6 +238,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean sendKeyEvent(KeyEvent event) {
return mTarget.sendKeyEvent(event);
}
@@ -226,6 +247,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean clearMetaKeyStates(int states) {
return mTarget.clearMetaKeyStates(states);
}
@@ -234,6 +256,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean reportFullscreenMode(boolean enabled) {
return mTarget.reportFullscreenMode(enabled);
}
@@ -242,6 +265,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean performPrivateCommand(String action, Bundle data) {
return mTarget.performPrivateCommand(action, data);
}
@@ -250,6 +274,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean requestCursorUpdates(int cursorUpdateMode) {
return mTarget.requestCursorUpdates(cursorUpdateMode);
}
@@ -258,6 +283,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public Handler getHandler() {
return mTarget.getHandler();
}
@@ -266,6 +292,7 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public void closeConnection() {
mTarget.closeConnection();
}
@@ -274,7 +301,17 @@ public class InputConnectionWrapper implements InputConnection {
* {@inheritDoc}
* @throws NullPointerException if the target is {@code null}.
*/
+ @Override
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
return mTarget.commitContent(inputContentInfo, flags, opts);
}
+
+ /**
+ * {@inheritDoc}
+ * @throws NullPointerException if the target is {@code null}.
+ */
+ @Override
+ public void reportLanguageHint(@NonNull LocaleList languageHint) {
+ mTarget.reportLanguageHint(languageHint);
+ }
}
diff --git a/android/view/inputmethod/InputMethodManager.java b/android/view/inputmethod/InputMethodManager.java
index 80d7b6b7..7db5c320 100644
--- a/android/view/inputmethod/InputMethodManager.java
+++ b/android/view/inputmethod/InputMethodManager.java
@@ -337,20 +337,23 @@ public final class InputMethodManager {
int mCursorCandEnd;
/**
- * Represents an invalid action notification sequence number. {@link InputMethodManagerService}
- * always issues a positive integer for action notification sequence numbers. Thus -1 is
- * guaranteed to be different from any valid sequence number.
+ * Represents an invalid action notification sequence number.
+ * {@link com.android.server.InputMethodManagerService} always issues a positive integer for
+ * action notification sequence numbers. Thus {@code -1} is guaranteed to be different from any
+ * valid sequence number.
*/
private static final int NOT_AN_ACTION_NOTIFICATION_SEQUENCE_NUMBER = -1;
/**
- * The next sequence number that is to be sent to {@link InputMethodManagerService} via
+ * The next sequence number that is to be sent to
+ * {@link com.android.server.InputMethodManagerService} via
* {@link IInputMethodManager#notifyUserAction(int)} at once when a user action is observed.
*/
private int mNextUserActionNotificationSequenceNumber =
NOT_AN_ACTION_NOTIFICATION_SEQUENCE_NUMBER;
/**
- * The last sequence number that is already sent to {@link InputMethodManagerService}.
+ * The last sequence number that is already sent to
+ * {@link com.android.server.InputMethodManagerService}.
*/
private int mLastSentUserActionNotificationSequenceNumber =
NOT_AN_ACTION_NOTIFICATION_SEQUENCE_NUMBER;
@@ -1079,15 +1082,15 @@ public final class InputMethodManager {
}
/**
- * Flag for {@link #hideSoftInputFromWindow} to indicate that the soft
- * input window should only be hidden if it was not explicitly shown
+ * Flag for {@link #hideSoftInputFromWindow} and {@link InputMethodService#requestHideSelf(int)}
+ * to indicate that the soft input window should only be hidden if it was not explicitly shown
* by the user.
*/
public static final int HIDE_IMPLICIT_ONLY = 0x0001;
/**
- * Flag for {@link #hideSoftInputFromWindow} to indicate that the soft
- * input window should normally be hidden, unless it was originally
+ * Flag for {@link #hideSoftInputFromWindow} and {@link InputMethodService#requestShowSelf(int)}
+ * to indicate that the soft input window should normally be hidden, unless it was originally
* shown with {@link #SHOW_FORCED}.
*/
public static final int HIDE_NOT_ALWAYS = 0x0002;
@@ -1255,12 +1258,7 @@ public final class InputMethodManager {
// The view is running on a different thread than our own, so
// we need to reschedule our work for over there.
if (DEBUG) Log.v(TAG, "Starting input: reschedule to view thread");
- vh.post(new Runnable() {
- @Override
- public void run() {
- startInputInner(startInputReason, null, 0, 0, 0);
- }
- });
+ vh.post(() -> startInputInner(startInputReason, null, 0, 0, 0));
return false;
}
@@ -1871,9 +1869,9 @@ 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 Use {@link InputMethodService#requestHideSelf(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) {
@@ -1903,9 +1901,9 @@ 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 Use {@link InputMethodService#requestShowSelf(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) {
@@ -2429,8 +2427,8 @@ public final class InputMethodManager {
* Allow the receiver of {@link InputContentInfo} to obtain a temporary read-only access
* permission to the content.
*
- * <p>See {@link android.inputmethodservice.InputMethodService#exposeContent(InputContentInfo, EditorInfo)}
- * for details.</p>
+ * <p>See {@link android.inputmethodservice.InputMethodService#exposeContent(InputContentInfo,
+ * InputConnection)} for details.</p>
*
* @param token Supplies the identifying token given to an input method when it was started,
* which allows it to perform this operation on itself.
diff --git a/android/view/textclassifier/EntityConfidence.java b/android/view/textclassifier/EntityConfidence.java
index 19660d95..69a59a5b 100644
--- a/android/view/textclassifier/EntityConfidence.java
+++ b/android/view/textclassifier/EntityConfidence.java
@@ -18,6 +18,8 @@ package android.view.textclassifier;
import android.annotation.FloatRange;
import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.ArrayMap;
import com.android.internal.util.Preconditions;
@@ -30,17 +32,16 @@ import java.util.Map;
/**
* Helper object for setting and getting entity scores for classified text.
*
- * @param <T> the entity type.
* @hide
*/
-final class EntityConfidence<T> {
+final class EntityConfidence implements Parcelable {
- private final ArrayMap<T, Float> mEntityConfidence = new ArrayMap<>();
- private final ArrayList<T> mSortedEntities = new ArrayList<>();
+ private final ArrayMap<String, Float> mEntityConfidence = new ArrayMap<>();
+ private final ArrayList<String> mSortedEntities = new ArrayList<>();
EntityConfidence() {}
- EntityConfidence(@NonNull EntityConfidence<T> source) {
+ EntityConfidence(@NonNull EntityConfidence source) {
Preconditions.checkNotNull(source);
mEntityConfidence.putAll(source.mEntityConfidence);
mSortedEntities.addAll(source.mSortedEntities);
@@ -54,24 +55,16 @@ final class EntityConfidence<T> {
* @param source a map from entity to a confidence value in the range 0 (low confidence) to
* 1 (high confidence).
*/
- EntityConfidence(@NonNull Map<T, Float> source) {
+ EntityConfidence(@NonNull Map<String, 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()) {
+ for (Map.Entry<String, 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);
- });
+ resetSortedEntitiesFromMap();
}
/**
@@ -79,7 +72,7 @@ final class EntityConfidence<T> {
* high confidence to low confidence.
*/
@NonNull
- public List<T> getEntities() {
+ public List<String> getEntities() {
return Collections.unmodifiableList(mSortedEntities);
}
@@ -89,7 +82,7 @@ final class EntityConfidence<T> {
* classified text.
*/
@FloatRange(from = 0.0, to = 1.0)
- public float getConfidenceScore(T entity) {
+ public float getConfidenceScore(String entity) {
if (mEntityConfidence.containsKey(entity)) {
return mEntityConfidence.get(entity);
}
@@ -100,4 +93,51 @@ final class EntityConfidence<T> {
public String toString() {
return mEntityConfidence.toString();
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mEntityConfidence.size());
+ for (Map.Entry<String, Float> entry : mEntityConfidence.entrySet()) {
+ dest.writeString(entry.getKey());
+ dest.writeFloat(entry.getValue());
+ }
+ }
+
+ public static final Parcelable.Creator<EntityConfidence> CREATOR =
+ new Parcelable.Creator<EntityConfidence>() {
+ @Override
+ public EntityConfidence createFromParcel(Parcel in) {
+ return new EntityConfidence(in);
+ }
+
+ @Override
+ public EntityConfidence[] newArray(int size) {
+ return new EntityConfidence[size];
+ }
+ };
+
+ private EntityConfidence(Parcel in) {
+ final int numEntities = in.readInt();
+ mEntityConfidence.ensureCapacity(numEntities);
+ for (int i = 0; i < numEntities; ++i) {
+ mEntityConfidence.put(in.readString(), in.readFloat());
+ }
+ resetSortedEntitiesFromMap();
+ }
+
+ private void resetSortedEntitiesFromMap() {
+ mSortedEntities.clear();
+ 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);
+ });
+ }
}
diff --git a/android/view/textclassifier/TextClassification.java b/android/view/textclassifier/TextClassification.java
index 7ffbf635..7089677d 100644
--- a/android/view/textclassifier/TextClassification.java
+++ b/android/view/textclassifier/TextClassification.java
@@ -22,8 +22,13 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.ArrayMap;
import android.view.View.OnClickListener;
import android.view.textclassifier.TextClassifier.EntityType;
@@ -52,7 +57,7 @@ import java.util.Map;
* Button button = new Button(context);
* button.setCompoundDrawablesWithIntrinsicBounds(classification.getIcon(), null, null, null);
* button.setText(classification.getLabel());
- * button.setOnClickListener(classification.getOnClickListener());
+ * button.setOnClickListener(v -> context.startActivity(classification.getIntent()));
* }</pre>
*
* <p>e.g. starting an action mode with menu items that can handle the classified text:
@@ -90,7 +95,6 @@ import java.util.Map;
* ...
* });
* }</pre>
- *
*/
public final class TextClassification {
@@ -99,6 +103,10 @@ public final class TextClassification {
*/
static final TextClassification EMPTY = new TextClassification.Builder().build();
+ // TODO(toki): investigate a way to derive this based on device properties.
+ private static final int MAX_PRIMARY_ICON_SIZE = 192;
+ private static final int MAX_SECONDARY_ICON_SIZE = 144;
+
@NonNull private final String mText;
@Nullable private final Drawable mPrimaryIcon;
@Nullable private final String mPrimaryLabel;
@@ -107,8 +115,7 @@ public final class TextClassification {
@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 EntityConfidence mEntityConfidence;
@NonNull private final String mSignature;
private TextClassification(
@@ -120,12 +127,10 @@ public final class TextClassification {
@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;
mPrimaryIcon = primaryIcon;
mPrimaryLabel = primaryLabel;
@@ -134,8 +139,7 @@ public final class TextClassification {
mSecondaryIcons = secondaryIcons;
mSecondaryLabels = secondaryLabels;
mSecondaryIntents = secondaryIntents;
- mSecondaryOnClickListeners = secondaryOnClickListeners;
- mEntityConfidence = new EntityConfidence<>(entityConfidence);
+ mEntityConfidence = new EntityConfidence(entityConfidence);
mSignature = signature;
}
@@ -186,7 +190,6 @@ public final class TextClassification {
* @see #getSecondaryIntent(int)
* @see #getSecondaryLabel(int)
* @see #getSecondaryIcon(int)
- * @see #getSecondaryOnClickListener(int)
*/
@IntRange(from = 0)
public int getSecondaryActionsCount() {
@@ -198,13 +201,10 @@ public final class TextClassification {
* classified text.
*
* @param index Index of the action to get the icon for.
- *
* @throws IndexOutOfBoundsException if the specified index is out of range.
- *
* @see #getSecondaryActionsCount() for the number of actions available.
* @see #getSecondaryIntent(int)
* @see #getSecondaryLabel(int)
- * @see #getSecondaryOnClickListener(int)
* @see #getIcon()
*/
@Nullable
@@ -228,13 +228,10 @@ public final class TextClassification {
* 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 #getSecondaryActionsCount()
* @see #getSecondaryIntent(int)
* @see #getSecondaryIcon(int)
- * @see #getSecondaryOnClickListener(int)
* @see #getLabel()
*/
@Nullable
@@ -257,13 +254,10 @@ public final class TextClassification {
* 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 #getSecondaryActionsCount()
* @see #getSecondaryLabel(int)
* @see #getSecondaryIcon(int)
- * @see #getSecondaryOnClickListener(int)
* @see #getIntent()
*/
@Nullable
@@ -282,29 +276,10 @@ public final class TextClassification {
}
/**
- * 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 #getSecondaryActionsCount()
- * @see #getSecondaryIntent(int)
- * @see #getSecondaryLabel(int)
- * @see #getSecondaryIcon(int)
- * @see #getOnClickListener()
- */
- @Nullable
- public OnClickListener getSecondaryOnClickListener(int index) {
- return mSecondaryOnClickListeners.get(index);
- }
-
- /**
* Returns the <i>primary</i> OnClickListener that may be triggered to act on the classified
- * text.
- *
- * @see #getSecondaryOnClickListener(int)
+ * text. This field is not parcelable and will be null for all objects read from a parcel.
+ * Instead, call Context#startActivity(Intent) with the result of #getSecondaryIntent(int).
+ * Note that this may fail if the activity doesn't have permission to send the intent.
*/
@Nullable
public OnClickListener getOnClickListener() {
@@ -334,6 +309,42 @@ public final class TextClassification {
mSignature);
}
+ /** Helper for parceling via #ParcelableWrapper. */
+ private void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mText);
+ final Bitmap primaryIconBitmap = drawableToBitmap(mPrimaryIcon, MAX_PRIMARY_ICON_SIZE);
+ dest.writeInt(primaryIconBitmap != null ? 1 : 0);
+ if (primaryIconBitmap != null) {
+ primaryIconBitmap.writeToParcel(dest, flags);
+ }
+ dest.writeString(mPrimaryLabel);
+ dest.writeInt(mPrimaryIntent != null ? 1 : 0);
+ if (mPrimaryIntent != null) {
+ mPrimaryIntent.writeToParcel(dest, flags);
+ }
+ // mPrimaryOnClickListener is not parcelable.
+ dest.writeTypedList(drawablesToBitmaps(mSecondaryIcons, MAX_SECONDARY_ICON_SIZE));
+ dest.writeStringList(mSecondaryLabels);
+ dest.writeTypedList(mSecondaryIntents);
+ mEntityConfidence.writeToParcel(dest, flags);
+ dest.writeString(mSignature);
+ }
+
+ /** Helper for unparceling via #ParcelableWrapper. */
+ private TextClassification(Parcel in) {
+ mText = in.readString();
+ mPrimaryIcon = in.readInt() == 0
+ ? null : new BitmapDrawable(null, Bitmap.CREATOR.createFromParcel(in));
+ mPrimaryLabel = in.readString();
+ mPrimaryIntent = in.readInt() == 0 ? null : Intent.CREATOR.createFromParcel(in);
+ mPrimaryOnClickListener = null; // not parcelable
+ mSecondaryIcons = bitmapsToDrawables(in.createTypedArrayList(Bitmap.CREATOR));
+ mSecondaryLabels = in.createStringArrayList();
+ mSecondaryIntents = in.createTypedArrayList(Intent.CREATOR);
+ mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+ mSignature = in.readString();
+ }
+
/**
* Creates an OnClickListener that starts an activity with the specified intent.
*
@@ -349,6 +360,68 @@ public final class TextClassification {
}
/**
+ * Returns a Bitmap representation of the Drawable
+ *
+ * @param drawable The drawable to convert.
+ * @param maxDims The maximum edge length of the resulting bitmap (in pixels).
+ */
+ @Nullable
+ private static Bitmap drawableToBitmap(@Nullable Drawable drawable, int maxDims) {
+ if (drawable == null) {
+ return null;
+ }
+ final int actualWidth = Math.max(1, drawable.getIntrinsicWidth());
+ final int actualHeight = Math.max(1, drawable.getIntrinsicHeight());
+ final double scaleWidth = ((double) maxDims) / actualWidth;
+ final double scaleHeight = ((double) maxDims) / actualHeight;
+ final double scale = Math.min(1.0, Math.min(scaleWidth, scaleHeight));
+ final int width = (int) (actualWidth * scale);
+ final int height = (int) (actualHeight * scale);
+ if (drawable instanceof BitmapDrawable) {
+ final BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+ if (actualWidth != width || actualHeight != height) {
+ return Bitmap.createScaledBitmap(
+ bitmapDrawable.getBitmap(), width, height, /*filter=*/false);
+ } else {
+ return bitmapDrawable.getBitmap();
+ }
+ } else {
+ final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ return bitmap;
+ }
+ }
+
+ /**
+ * Returns a list of drawables converted to Bitmaps
+ *
+ * @param drawables The drawables to convert.
+ * @param maxDims The maximum edge length of the resulting bitmaps (in pixels).
+ */
+ private static List<Bitmap> drawablesToBitmaps(List<Drawable> drawables, int maxDims) {
+ final List<Bitmap> bitmaps = new ArrayList<>(drawables.size());
+ for (Drawable drawable : drawables) {
+ bitmaps.add(drawableToBitmap(drawable, maxDims));
+ }
+ return bitmaps;
+ }
+
+ /** Returns a list of drawable wrappers for a list of bitmaps. */
+ private static List<Drawable> bitmapsToDrawables(List<Bitmap> bitmaps) {
+ final List<Drawable> drawables = new ArrayList<>(bitmaps.size());
+ for (Bitmap bitmap : bitmaps) {
+ if (bitmap != null) {
+ drawables.add(new BitmapDrawable(null, bitmap));
+ } else {
+ drawables.add(null);
+ }
+ }
+ return drawables;
+ }
+
+ /**
* Builder for building {@link TextClassification} objects.
*
* <p>e.g.
@@ -358,9 +431,9 @@ public final class TextClassification {
* .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)
+ * .setPrimaryAction(intent, label, icon)
+ * .addSecondaryAction(intent1, label1, icon1)
+ * .addSecondaryAction(intent2, label2, icon2)
* .build();
* }</pre>
*/
@@ -370,7 +443,6 @@ public final class TextClassification {
@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;
@@ -413,16 +485,14 @@ public final class TextClassification {
* <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)
+ * @see #setPrimaryAction(Intent, String, Drawable)
*/
public Builder addSecondaryAction(
- @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon,
- @Nullable OnClickListener onClickListener) {
- if (intent != null || label != null || icon != null || onClickListener != null) {
+ @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon) {
+ if (intent != null || label != null || icon != null) {
mSecondaryIntents.add(intent);
mSecondaryLabels.add(label);
mSecondaryIcons.add(icon);
- mSecondaryOnClickListeners.add(onClickListener);
}
return this;
}
@@ -432,7 +502,6 @@ public final class TextClassification {
*/
public Builder clearSecondaryActions() {
mSecondaryIntents.clear();
- mSecondaryOnClickListeners.clear();
mSecondaryLabels.clear();
mSecondaryIcons.clear();
return this;
@@ -440,26 +509,23 @@ public final class TextClassification {
/**
* 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)}.
+ * equivalent to calling {@code setIntent(intent).setLabel(label).setIcon(icon)}.
*
* <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)
+ * @see #addSecondaryAction(Intent, String, Drawable)
*/
public Builder setPrimaryAction(
- @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon,
- @Nullable OnClickListener onClickListener) {
- return setIntent(intent).setLabel(label).setIcon(icon)
- .setOnClickListener(onClickListener);
+ @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon) {
+ return setIntent(intent).setLabel(label).setIcon(icon);
}
/**
* 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)
+ * @see #setPrimaryAction(Intent, String, Drawable)
*/
public Builder setIcon(@Nullable Drawable icon) {
mPrimaryIcon = icon;
@@ -470,7 +536,7 @@ public final class TextClassification {
* 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)
+ * @see #setPrimaryAction(Intent, String, Drawable)
*/
public Builder setLabel(@Nullable String label) {
mPrimaryLabel = label;
@@ -481,7 +547,7 @@ public final class TextClassification {
* 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)
+ * @see #setPrimaryAction(Intent, String, Drawable)
*/
public Builder setIntent(@Nullable Intent intent) {
mPrimaryIntent = intent;
@@ -490,9 +556,8 @@ public final class TextClassification {
/**
* 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)
+ * the classified text. This field is not parcelable and will always be null when the
+ * object is read from a parcel.
*/
public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
mPrimaryOnClickListener = onClickListener;
@@ -515,10 +580,8 @@ public final class TextClassification {
public TextClassification build() {
return new TextClassification(
mText,
- mPrimaryIcon, mPrimaryLabel,
- mPrimaryIntent, mPrimaryOnClickListener,
- mSecondaryIcons, mSecondaryLabels,
- mSecondaryIntents, mSecondaryOnClickListeners,
+ mPrimaryIcon, mPrimaryLabel, mPrimaryIntent, mPrimaryOnClickListener,
+ mSecondaryIcons, mSecondaryLabels, mSecondaryIntents,
mEntityConfidence, mSignature);
}
}
@@ -526,9 +589,11 @@ public final class TextClassification {
/**
* Optional input parameters for generating TextClassification.
*/
- public static final class Options {
+ public static final class Options implements Parcelable {
+
+ private @Nullable LocaleList mDefaultLocales;
- private LocaleList mDefaultLocales;
+ public Options() {}
/**
* @param defaultLocales ordered list of locale preferences that may be used to disambiguate
@@ -548,5 +613,80 @@ public final class TextClassification {
public LocaleList getDefaultLocales() {
return mDefaultLocales;
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mDefaultLocales != null ? 1 : 0);
+ if (mDefaultLocales != null) {
+ mDefaultLocales.writeToParcel(dest, flags);
+ }
+ }
+
+ public static final Parcelable.Creator<Options> CREATOR =
+ new Parcelable.Creator<Options>() {
+ @Override
+ public Options createFromParcel(Parcel in) {
+ return new Options(in);
+ }
+
+ @Override
+ public Options[] newArray(int size) {
+ return new Options[size];
+ }
+ };
+
+ private Options(Parcel in) {
+ if (in.readInt() > 0) {
+ mDefaultLocales = LocaleList.CREATOR.createFromParcel(in);
+ }
+ }
+ }
+
+ /**
+ * Parcelable wrapper for TextClassification objects.
+ * @hide
+ */
+ public static final class ParcelableWrapper implements Parcelable {
+
+ @NonNull private TextClassification mTextClassification;
+
+ public ParcelableWrapper(@NonNull TextClassification textClassification) {
+ Preconditions.checkNotNull(textClassification);
+ mTextClassification = textClassification;
+ }
+
+ @NonNull
+ public TextClassification getTextClassification() {
+ return mTextClassification;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ mTextClassification.writeToParcel(dest, flags);
+ }
+
+ public static final Parcelable.Creator<ParcelableWrapper> CREATOR =
+ new Parcelable.Creator<ParcelableWrapper>() {
+ @Override
+ public ParcelableWrapper createFromParcel(Parcel in) {
+ return new ParcelableWrapper(new TextClassification(in));
+ }
+
+ @Override
+ public ParcelableWrapper[] newArray(int size) {
+ return new ParcelableWrapper[size];
+ }
+ };
+
}
}
diff --git a/android/view/textclassifier/TextClassifier.java b/android/view/textclassifier/TextClassifier.java
index ed604303..e9715c51 100644
--- a/android/view/textclassifier/TextClassifier.java
+++ b/android/view/textclassifier/TextClassifier.java
@@ -23,6 +23,8 @@ import android.annotation.Nullable;
import android.annotation.StringDef;
import android.annotation.WorkerThread;
import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.ArraySet;
import com.android.internal.util.Preconditions;
@@ -275,8 +277,8 @@ public interface TextClassifier {
/**
* Returns a {@link Collection} of the entity types in the specified preset.
*
- * @see #ENTITIES_ALL
- * @see #ENTITIES_NONE
+ * @see #ENTITY_PRESET_ALL
+ * @see #ENTITY_PRESET_NONE
*/
default Collection<String> getEntitiesForPreset(@EntityPreset int entityPreset) {
return Collections.EMPTY_LIST;
@@ -305,7 +307,7 @@ public interface TextClassifier {
*
* Configs are initially based on a predefined preset, and can be modified from there.
*/
- final class EntityConfig {
+ final class EntityConfig implements Parcelable {
private final @TextClassifier.EntityPreset int mEntityPreset;
private final Collection<String> mExcludedEntityTypes;
private final Collection<String> mIncludedEntityTypes;
@@ -355,6 +357,37 @@ public interface TextClassifier {
}
return Collections.unmodifiableList(entities);
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mEntityPreset);
+ dest.writeStringList(new ArrayList<>(mExcludedEntityTypes));
+ dest.writeStringList(new ArrayList<>(mIncludedEntityTypes));
+ }
+
+ public static final Parcelable.Creator<EntityConfig> CREATOR =
+ new Parcelable.Creator<EntityConfig>() {
+ @Override
+ public EntityConfig createFromParcel(Parcel in) {
+ return new EntityConfig(in);
+ }
+
+ @Override
+ public EntityConfig[] newArray(int size) {
+ return new EntityConfig[size];
+ }
+ };
+
+ private EntityConfig(Parcel in) {
+ mEntityPreset = in.readInt();
+ mExcludedEntityTypes = new ArraySet<>(in.createStringArrayList());
+ mIncludedEntityTypes = new ArraySet<>(in.createStringArrayList());
+ }
}
/**
diff --git a/android/view/textclassifier/TextClassifierConstants.java b/android/view/textclassifier/TextClassifierConstants.java
index 51e6168e..00695b79 100644
--- a/android/view/textclassifier/TextClassifierConstants.java
+++ b/android/view/textclassifier/TextClassifierConstants.java
@@ -45,19 +45,24 @@ public final class TextClassifierConstants {
"smart_selection_dark_launch";
private static final String SMART_SELECTION_ENABLED_FOR_EDIT_TEXT =
"smart_selection_enabled_for_edit_text";
+ private static final String SMART_LINKIFY_ENABLED =
+ "smart_linkify_enabled";
private static final boolean SMART_SELECTION_DARK_LAUNCH_DEFAULT = false;
private static final boolean SMART_SELECTION_ENABLED_FOR_EDIT_TEXT_DEFAULT = true;
+ private static final boolean SMART_LINKIFY_ENABLED_DEFAULT = true;
/** Default settings. */
static final TextClassifierConstants DEFAULT = new TextClassifierConstants();
private final boolean mDarkLaunch;
private final boolean mSuggestSelectionEnabledForEditableText;
+ private final boolean mSmartLinkifyEnabled;
private TextClassifierConstants() {
mDarkLaunch = SMART_SELECTION_DARK_LAUNCH_DEFAULT;
mSuggestSelectionEnabledForEditableText = SMART_SELECTION_ENABLED_FOR_EDIT_TEXT_DEFAULT;
+ mSmartLinkifyEnabled = SMART_LINKIFY_ENABLED_DEFAULT;
}
private TextClassifierConstants(@Nullable String settings) {
@@ -74,6 +79,9 @@ public final class TextClassifierConstants {
mSuggestSelectionEnabledForEditableText = parser.getBoolean(
SMART_SELECTION_ENABLED_FOR_EDIT_TEXT,
SMART_SELECTION_ENABLED_FOR_EDIT_TEXT_DEFAULT);
+ mSmartLinkifyEnabled = parser.getBoolean(
+ SMART_LINKIFY_ENABLED,
+ SMART_LINKIFY_ENABLED_DEFAULT);
}
static TextClassifierConstants loadFromString(String settings) {
@@ -87,4 +95,8 @@ public final class TextClassifierConstants {
public boolean isSuggestSelectionEnabledForEditableText() {
return mSuggestSelectionEnabledForEditableText;
}
+
+ public boolean isSmartLinkifyEnabled() {
+ return mSmartLinkifyEnabled;
+ }
}
diff --git a/android/view/textclassifier/TextClassifierImpl.java b/android/view/textclassifier/TextClassifierImpl.java
index aea3cb06..7db0e76d 100644
--- a/android/view/textclassifier/TextClassifierImpl.java
+++ b/android/view/textclassifier/TextClassifierImpl.java
@@ -32,7 +32,6 @@ import android.provider.ContactsContract;
import android.provider.Settings;
import android.text.util.Linkify;
import android.util.Patterns;
-import android.view.View.OnClickListener;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.MetricsLogger;
@@ -187,6 +186,11 @@ final class TextClassifierImpl implements TextClassifier {
Utils.validateInput(text);
final String textString = text.toString();
final TextLinks.Builder builder = new TextLinks.Builder(textString);
+
+ if (!getSettings().isSmartLinkifyEnabled()) {
+ return builder.build();
+ }
+
try {
final LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null;
final Collection<String> entitiesToIdentify =
@@ -457,12 +461,10 @@ final class TextClassifierImpl implements TextClassifier {
}
}
final String labelString = (label != null) ? label.toString() : null;
- final OnClickListener onClickListener =
- TextClassification.createStartActivityOnClickListener(mContext, intent);
if (i == 0) {
- builder.setPrimaryAction(intent, labelString, icon, onClickListener);
+ builder.setPrimaryAction(intent, labelString, icon);
} else {
- builder.addSecondaryAction(intent, labelString, icon, onClickListener);
+ builder.addSecondaryAction(intent, labelString, icon);
}
}
}
diff --git a/android/view/textclassifier/TextLinks.java b/android/view/textclassifier/TextLinks.java
index 6c587cf9..ba854e04 100644
--- a/android/view/textclassifier/TextLinks.java
+++ b/android/view/textclassifier/TextLinks.java
@@ -20,6 +20,8 @@ import android.annotation.FloatRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.text.SpannableString;
import android.text.style.ClickableSpan;
import android.view.View;
@@ -38,7 +40,7 @@ import java.util.function.Function;
* A collection of links, representing subsequences of text and the entity types (phone number,
* address, url, etc) they may be.
*/
-public final class TextLinks {
+public final class TextLinks implements Parcelable {
private final String mFullText;
private final List<TextLink> mLinks;
@@ -83,11 +85,40 @@ public final class TextLinks {
return true;
}
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mFullText);
+ dest.writeTypedList(mLinks);
+ }
+
+ public static final Parcelable.Creator<TextLinks> CREATOR =
+ new Parcelable.Creator<TextLinks>() {
+ @Override
+ public TextLinks createFromParcel(Parcel in) {
+ return new TextLinks(in);
+ }
+
+ @Override
+ public TextLinks[] newArray(int size) {
+ return new TextLinks[size];
+ }
+ };
+
+ private TextLinks(Parcel in) {
+ mFullText = in.readString();
+ mLinks = in.createTypedArrayList(TextLink.CREATOR);
+ }
+
/**
* A link, identifying a substring of text and possible entity types for it.
*/
- public static final class TextLink {
- private final EntityConfidence<String> mEntityScores;
+ public static final class TextLink implements Parcelable {
+ private final EntityConfidence mEntityScores;
private final String mOriginalText;
private final int mStart;
private final int mEnd;
@@ -105,7 +136,7 @@ public final class TextLinks {
mOriginalText = originalText;
mStart = start;
mEnd = end;
- mEntityScores = new EntityConfidence<>(entityScores);
+ mEntityScores = new EntityConfidence(entityScores);
}
/**
@@ -153,16 +184,51 @@ public final class TextLinks {
@TextClassifier.EntityType String entityType) {
return mEntityScores.getConfidenceScore(entityType);
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ mEntityScores.writeToParcel(dest, flags);
+ dest.writeString(mOriginalText);
+ dest.writeInt(mStart);
+ dest.writeInt(mEnd);
+ }
+
+ public static final Parcelable.Creator<TextLink> CREATOR =
+ new Parcelable.Creator<TextLink>() {
+ @Override
+ public TextLink createFromParcel(Parcel in) {
+ return new TextLink(in);
+ }
+
+ @Override
+ public TextLink[] newArray(int size) {
+ return new TextLink[size];
+ }
+ };
+
+ private TextLink(Parcel in) {
+ mEntityScores = EntityConfidence.CREATOR.createFromParcel(in);
+ mOriginalText = in.readString();
+ mStart = in.readInt();
+ mEnd = in.readInt();
+ }
}
/**
* Optional input parameters for generating TextLinks.
*/
- public static final class Options {
+ public static final class Options implements Parcelable {
private LocaleList mDefaultLocales;
private TextClassifier.EntityConfig mEntityConfig;
+ public Options() {}
+
/**
* @param defaultLocales ordered list of locale preferences that may be used to
* disambiguate the provided text. If no locale preferences exist,
@@ -201,6 +267,45 @@ public final class TextLinks {
public TextClassifier.EntityConfig getEntityConfig() {
return mEntityConfig;
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mDefaultLocales != null ? 1 : 0);
+ if (mDefaultLocales != null) {
+ mDefaultLocales.writeToParcel(dest, flags);
+ }
+ dest.writeInt(mEntityConfig != null ? 1 : 0);
+ if (mEntityConfig != null) {
+ mEntityConfig.writeToParcel(dest, flags);
+ }
+ }
+
+ public static final Parcelable.Creator<Options> CREATOR =
+ new Parcelable.Creator<Options>() {
+ @Override
+ public Options createFromParcel(Parcel in) {
+ return new Options(in);
+ }
+
+ @Override
+ public Options[] newArray(int size) {
+ return new Options[size];
+ }
+ };
+
+ private Options(Parcel in) {
+ if (in.readInt() > 0) {
+ mDefaultLocales = LocaleList.CREATOR.createFromParcel(in);
+ }
+ if (in.readInt() > 0) {
+ mEntityConfig = TextClassifier.EntityConfig.CREATOR.createFromParcel(in);
+ }
+ }
}
/**
diff --git a/android/view/textclassifier/TextSelection.java b/android/view/textclassifier/TextSelection.java
index 25e9e7ec..774d42db 100644
--- a/android/view/textclassifier/TextSelection.java
+++ b/android/view/textclassifier/TextSelection.java
@@ -21,6 +21,8 @@ import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.util.ArrayMap;
import android.view.textclassifier.TextClassifier.EntityType;
@@ -36,7 +38,7 @@ public final class TextSelection {
private final int mStartIndex;
private final int mEndIndex;
- @NonNull private final EntityConfidence<String> mEntityConfidence;
+ @NonNull private final EntityConfidence mEntityConfidence;
@NonNull private final String mSignature;
private TextSelection(
@@ -44,7 +46,7 @@ public final class TextSelection {
@NonNull String signature) {
mStartIndex = startIndex;
mEndIndex = endIndex;
- mEntityConfidence = new EntityConfidence<>(entityConfidence);
+ mEntityConfidence = new EntityConfidence(entityConfidence);
mSignature = signature;
}
@@ -110,6 +112,22 @@ public final class TextSelection {
mStartIndex, mEndIndex, mEntityConfidence, mSignature);
}
+ /** Helper for parceling via #ParcelableWrapper. */
+ private void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mStartIndex);
+ dest.writeInt(mEndIndex);
+ mEntityConfidence.writeToParcel(dest, flags);
+ dest.writeString(mSignature);
+ }
+
+ /** Helper for unparceling via #ParcelableWrapper. */
+ private TextSelection(Parcel in) {
+ mStartIndex = in.readInt();
+ mEndIndex = in.readInt();
+ mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+ mSignature = in.readString();
+ }
+
/**
* Builder used to build {@link TextSelection} objects.
*/
@@ -170,11 +188,13 @@ public final class TextSelection {
/**
* Optional input parameters for generating TextSelection.
*/
- public static final class Options {
+ public static final class Options implements Parcelable {
- private LocaleList mDefaultLocales;
+ private @Nullable LocaleList mDefaultLocales;
private boolean mDarkLaunchAllowed;
+ public Options() {}
+
/**
* @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
@@ -216,5 +236,82 @@ public final class TextSelection {
public boolean isDarkLaunchAllowed() {
return mDarkLaunchAllowed;
}
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mDefaultLocales != null ? 1 : 0);
+ if (mDefaultLocales != null) {
+ mDefaultLocales.writeToParcel(dest, flags);
+ }
+ dest.writeInt(mDarkLaunchAllowed ? 1 : 0);
+ }
+
+ public static final Parcelable.Creator<Options> CREATOR =
+ new Parcelable.Creator<Options>() {
+ @Override
+ public Options createFromParcel(Parcel in) {
+ return new Options(in);
+ }
+
+ @Override
+ public Options[] newArray(int size) {
+ return new Options[size];
+ }
+ };
+
+ private Options(Parcel in) {
+ if (in.readInt() > 0) {
+ mDefaultLocales = LocaleList.CREATOR.createFromParcel(in);
+ }
+ mDarkLaunchAllowed = in.readInt() != 0;
+ }
+ }
+
+ /**
+ * Parcelable wrapper for TextSelection objects.
+ * @hide
+ */
+ public static final class ParcelableWrapper implements Parcelable {
+
+ @NonNull private TextSelection mTextSelection;
+
+ public ParcelableWrapper(@NonNull TextSelection textSelection) {
+ Preconditions.checkNotNull(textSelection);
+ mTextSelection = textSelection;
+ }
+
+ @NonNull
+ public TextSelection getTextSelection() {
+ return mTextSelection;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ mTextSelection.writeToParcel(dest, flags);
+ }
+
+ public static final Parcelable.Creator<ParcelableWrapper> CREATOR =
+ new Parcelable.Creator<ParcelableWrapper>() {
+ @Override
+ public ParcelableWrapper createFromParcel(Parcel in) {
+ return new ParcelableWrapper(new TextSelection(in));
+ }
+
+ @Override
+ public ParcelableWrapper[] newArray(int size) {
+ return new ParcelableWrapper[size];
+ }
+ };
+
}
}
diff --git a/android/webkit/FindAddress.java b/android/webkit/FindAddress.java
new file mode 100644
index 00000000..31b24273
--- /dev/null
+++ b/android/webkit/FindAddress.java
@@ -0,0 +1,478 @@
+/*
+ * 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.webkit;
+
+import java.util.Locale;
+import java.util.regex.MatchResult;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Java implementation of legacy WebView.findAddress algorithm.
+ *
+ * @hide
+ */
+class FindAddress {
+ static class ZipRange {
+ int mLow;
+ int mHigh;
+ int mException1;
+ int mException2;
+ ZipRange(int low, int high, int exception1, int exception2) {
+ mLow = low;
+ mHigh = high;
+ mException1 = exception1;
+ mException2 = exception1;
+ }
+ boolean matches(String zipCode) {
+ int prefix = Integer.parseInt(zipCode.substring(0, 2));
+ return (mLow <= prefix && prefix <= mHigh) || prefix == mException1
+ || prefix == mException2;
+ }
+ }
+
+ // Addresses consist of at least this many words, not including state and zip code.
+ private static final int MIN_ADDRESS_WORDS = 4;
+
+ // Adddresses consist of at most this many words, not including state and zip code.
+ private static final int MAX_ADDRESS_WORDS = 14;
+
+ // Addresses consist of at most this many lines.
+ private static final int MAX_ADDRESS_LINES = 5;
+
+ // No words in an address are longer than this many characters.
+ private static final int kMaxAddressNameWordLength = 25;
+
+ // Location name should be in the first MAX_LOCATION_NAME_DISTANCE words
+ private static final int MAX_LOCATION_NAME_DISTANCE = 5;
+
+ private static final ZipRange[] sStateZipCodeRanges = {
+ new ZipRange(99, 99, -1, -1), // AK Alaska.
+ new ZipRange(35, 36, -1, -1), // AL Alabama.
+ new ZipRange(71, 72, -1, -1), // AR Arkansas.
+ new ZipRange(96, 96, -1, -1), // AS American Samoa.
+ new ZipRange(85, 86, -1, -1), // AZ Arizona.
+ new ZipRange(90, 96, -1, -1), // CA California.
+ new ZipRange(80, 81, -1, -1), // CO Colorado.
+ new ZipRange(6, 6, -1, -1), // CT Connecticut.
+ new ZipRange(20, 20, -1, -1), // DC District of Columbia.
+ new ZipRange(19, 19, -1, -1), // DE Delaware.
+ new ZipRange(32, 34, -1, -1), // FL Florida.
+ new ZipRange(96, 96, -1, -1), // FM Federated States of Micronesia.
+ new ZipRange(30, 31, -1, -1), // GA Georgia.
+ new ZipRange(96, 96, -1, -1), // GU Guam.
+ new ZipRange(96, 96, -1, -1), // HI Hawaii.
+ new ZipRange(50, 52, -1, -1), // IA Iowa.
+ new ZipRange(83, 83, -1, -1), // ID Idaho.
+ new ZipRange(60, 62, -1, -1), // IL Illinois.
+ new ZipRange(46, 47, -1, -1), // IN Indiana.
+ new ZipRange(66, 67, 73, -1), // KS Kansas.
+ new ZipRange(40, 42, -1, -1), // KY Kentucky.
+ new ZipRange(70, 71, -1, -1), // LA Louisiana.
+ new ZipRange(1, 2, -1, -1), // MA Massachusetts.
+ new ZipRange(20, 21, -1, -1), // MD Maryland.
+ new ZipRange(3, 4, -1, -1), // ME Maine.
+ new ZipRange(96, 96, -1, -1), // MH Marshall Islands.
+ new ZipRange(48, 49, -1, -1), // MI Michigan.
+ new ZipRange(55, 56, -1, -1), // MN Minnesota.
+ new ZipRange(63, 65, -1, -1), // MO Missouri.
+ new ZipRange(96, 96, -1, -1), // MP Northern Mariana Islands.
+ new ZipRange(38, 39, -1, -1), // MS Mississippi.
+ new ZipRange(55, 56, -1, -1), // MT Montana.
+ new ZipRange(27, 28, -1, -1), // NC North Carolina.
+ new ZipRange(58, 58, -1, -1), // ND North Dakota.
+ new ZipRange(68, 69, -1, -1), // NE Nebraska.
+ new ZipRange(3, 4, -1, -1), // NH New Hampshire.
+ new ZipRange(7, 8, -1, -1), // NJ New Jersey.
+ new ZipRange(87, 88, 86, -1), // NM New Mexico.
+ new ZipRange(88, 89, 96, -1), // NV Nevada.
+ new ZipRange(10, 14, 0, 6), // NY New York.
+ new ZipRange(43, 45, -1, -1), // OH Ohio.
+ new ZipRange(73, 74, -1, -1), // OK Oklahoma.
+ new ZipRange(97, 97, -1, -1), // OR Oregon.
+ new ZipRange(15, 19, -1, -1), // PA Pennsylvania.
+ new ZipRange(6, 6, 0, 9), // PR Puerto Rico.
+ new ZipRange(96, 96, -1, -1), // PW Palau.
+ new ZipRange(2, 2, -1, -1), // RI Rhode Island.
+ new ZipRange(29, 29, -1, -1), // SC South Carolina.
+ new ZipRange(57, 57, -1, -1), // SD South Dakota.
+ new ZipRange(37, 38, -1, -1), // TN Tennessee.
+ new ZipRange(75, 79, 87, 88), // TX Texas.
+ new ZipRange(84, 84, -1, -1), // UT Utah.
+ new ZipRange(22, 24, 20, -1), // VA Virginia.
+ new ZipRange(6, 9, -1, -1), // VI Virgin Islands.
+ new ZipRange(5, 5, -1, -1), // VT Vermont.
+ new ZipRange(98, 99, -1, -1), // WA Washington.
+ new ZipRange(53, 54, -1, -1), // WI Wisconsin.
+ new ZipRange(24, 26, -1, -1), // WV West Virginia.
+ new ZipRange(82, 83, -1, -1) // WY Wyoming.
+ };
+
+ // Newlines
+ private static final String NL = "\n\u000B\u000C\r\u0085\u2028\u2029";
+
+ // Space characters
+ private static final String SP = "\u0009\u0020\u00A0\u1680\u2000\u2001"
+ + "\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200A\u202F"
+ + "\u205F\u3000";
+
+ // Whitespace
+ private static final String WS = SP + NL;
+
+ // Characters that are considered word delimiters.
+ private static final String WORD_DELIM = ",*\u2022" + WS;
+
+ // Lookahead for word end.
+ private static final String WORD_END = "(?=[" + WORD_DELIM + "]|$)";
+
+ // Address words are a sequence of non-delimiter characters.
+ private static final Pattern sWordRe =
+ Pattern.compile("[^" + WORD_DELIM + "]+" + WORD_END, Pattern.CASE_INSENSITIVE);
+
+ // Characters that are considered suffix delimiters for house numbers.
+ private static final String HOUSE_POST_DELIM = ",\"'" + WS;
+
+ // Lookahead for house end.
+ private static final String HOUSE_END = "(?=[" + HOUSE_POST_DELIM + "]|$)";
+
+ // Characters that are considered prefix delimiters for house numbers.
+ private static final String HOUSE_PRE_DELIM = ":" + HOUSE_POST_DELIM;
+
+ // A house number component is "one" or a number, optionally
+ // followed by a single alphabetic character, or
+ private static final String HOUSE_COMPONENT = "(?:one|\\d+([a-z](?=[^a-z]|$)|st|nd|rd|th)?)";
+
+ // House numbers are a repetition of |HOUSE_COMPONENT|, separated by -, and followed by
+ // a delimiter character.
+ private static final Pattern sHouseNumberRe =
+ Pattern.compile(HOUSE_COMPONENT + "(?:-" + HOUSE_COMPONENT + ")*" + HOUSE_END,
+ Pattern.CASE_INSENSITIVE);
+
+ // XXX: do we want to accept whitespace other than 0x20 in state names?
+ private static final Pattern sStateRe = Pattern.compile("(?:"
+ + "(ak|alaska)|"
+ + "(al|alabama)|"
+ + "(ar|arkansas)|"
+ + "(as|american[" + SP + "]+samoa)|"
+ + "(az|arizona)|"
+ + "(ca|california)|"
+ + "(co|colorado)|"
+ + "(ct|connecticut)|"
+ + "(dc|district[" + SP + "]+of[" + SP + "]+columbia)|"
+ + "(de|delaware)|"
+ + "(fl|florida)|"
+ + "(fm|federated[" + SP + "]+states[" + SP + "]+of[" + SP + "]+micronesia)|"
+ + "(ga|georgia)|"
+ + "(gu|guam)|"
+ + "(hi|hawaii)|"
+ + "(ia|iowa)|"
+ + "(id|idaho)|"
+ + "(il|illinois)|"
+ + "(in|indiana)|"
+ + "(ks|kansas)|"
+ + "(ky|kentucky)|"
+ + "(la|louisiana)|"
+ + "(ma|massachusetts)|"
+ + "(md|maryland)|"
+ + "(me|maine)|"
+ + "(mh|marshall[" + SP + "]+islands)|"
+ + "(mi|michigan)|"
+ + "(mn|minnesota)|"
+ + "(mo|missouri)|"
+ + "(mp|northern[" + SP + "]+mariana[" + SP + "]+islands)|"
+ + "(ms|mississippi)|"
+ + "(mt|montana)|"
+ + "(nc|north[" + SP + "]+carolina)|"
+ + "(nd|north[" + SP + "]+dakota)|"
+ + "(ne|nebraska)|"
+ + "(nh|new[" + SP + "]+hampshire)|"
+ + "(nj|new[" + SP + "]+jersey)|"
+ + "(nm|new[" + SP + "]+mexico)|"
+ + "(nv|nevada)|"
+ + "(ny|new[" + SP + "]+york)|"
+ + "(oh|ohio)|"
+ + "(ok|oklahoma)|"
+ + "(or|oregon)|"
+ + "(pa|pennsylvania)|"
+ + "(pr|puerto[" + SP + "]+rico)|"
+ + "(pw|palau)|"
+ + "(ri|rhode[" + SP + "]+island)|"
+ + "(sc|south[" + SP + "]+carolina)|"
+ + "(sd|south[" + SP + "]+dakota)|"
+ + "(tn|tennessee)|"
+ + "(tx|texas)|"
+ + "(ut|utah)|"
+ + "(va|virginia)|"
+ + "(vi|virgin[" + SP + "]+islands)|"
+ + "(vt|vermont)|"
+ + "(wa|washington)|"
+ + "(wi|wisconsin)|"
+ + "(wv|west[" + SP + "]+virginia)|"
+ + "(wy|wyoming)"
+ + ")" + WORD_END,
+ Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern sLocationNameRe = Pattern.compile("(?:"
+ + "alley|annex|arcade|ave[.]?|avenue|alameda|bayou|"
+ + "beach|bend|bluffs?|bottom|boulevard|branch|bridge|"
+ + "brooks?|burgs?|bypass|broadway|camino|camp|canyon|"
+ + "cape|causeway|centers?|circles?|cliffs?|club|common|"
+ + "corners?|course|courts?|coves?|creek|crescent|crest|"
+ + "crossing|crossroad|curve|circulo|dale|dam|divide|"
+ + "drives?|estates?|expressway|extensions?|falls?|ferry|"
+ + "fields?|flats?|fords?|forest|forges?|forks?|fort|"
+ + "freeway|gardens?|gateway|glens?|greens?|groves?|"
+ + "harbors?|haven|heights|highway|hills?|hollow|inlet|"
+ + "islands?|isle|junctions?|keys?|knolls?|lakes?|land|"
+ + "landing|lane|lights?|loaf|locks?|lodge|loop|mall|"
+ + "manors?|meadows?|mews|mills?|mission|motorway|mount|"
+ + "mountains?|neck|orchard|oval|overpass|parks?|"
+ + "parkways?|pass|passage|path|pike|pines?|plains?|"
+ + "plaza|points?|ports?|prairie|privada|radial|ramp|"
+ + "ranch|rapids?|rd[.]?|rest|ridges?|river|roads?|route|"
+ + "row|rue|run|shoals?|shores?|skyway|springs?|spurs?|"
+ + "squares?|station|stravenue|stream|st[.]?|streets?|"
+ + "summit|speedway|terrace|throughway|trace|track|"
+ + "trafficway|trail|tunnel|turnpike|underpass|unions?|"
+ + "valleys?|viaduct|views?|villages?|ville|vista|walks?|"
+ + "wall|ways?|wells?|xing|xrd)" + WORD_END,
+ Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern sSuffixedNumberRe =
+ Pattern.compile("(\\d+)(st|nd|rd|th)", Pattern.CASE_INSENSITIVE);
+
+ private static final Pattern sZipCodeRe =
+ Pattern.compile("(?:\\d{5}(?:-\\d{4})?)" + WORD_END, Pattern.CASE_INSENSITIVE);
+
+ private static boolean checkHouseNumber(String houseNumber) {
+ // Make sure that there are at most 5 digits.
+ int digitCount = 0;
+ for (int i = 0; i < houseNumber.length(); ++i) {
+ if (Character.isDigit(houseNumber.charAt(i))) ++digitCount;
+ }
+ if (digitCount > 5) return false;
+
+ // Make sure that any ordinals are valid.
+ Matcher suffixMatcher = sSuffixedNumberRe.matcher(houseNumber);
+ while (suffixMatcher.find()) {
+ int num = Integer.parseInt(suffixMatcher.group(1));
+ if (num == 0) {
+ return false; // 0th is invalid.
+ }
+ String suffix = suffixMatcher.group(2).toLowerCase(Locale.getDefault());
+ switch (num % 10) {
+ case 1:
+ return suffix.equals(num % 100 == 11 ? "th" : "st");
+ case 2:
+ return suffix.equals(num % 100 == 12 ? "th" : "nd");
+ case 3:
+ return suffix.equals(num % 100 == 13 ? "th" : "rd");
+ default:
+ return suffix.equals("th");
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Attempt to match a house number beginnning at position offset
+ * in content. The house number must be followed by a word
+ * delimiter or the end of the string, and if offset is non-zero,
+ * then it must also be preceded by a word delimiter.
+ *
+ * @return a MatchResult if a valid house number was found.
+ */
+ private static MatchResult matchHouseNumber(String content, int offset) {
+ if (offset > 0 && HOUSE_PRE_DELIM.indexOf(content.charAt(offset - 1)) == -1) return null;
+ Matcher matcher = sHouseNumberRe.matcher(content).region(offset, content.length());
+ if (matcher.lookingAt()) {
+ MatchResult matchResult = matcher.toMatchResult();
+ if (checkHouseNumber(matchResult.group(0))) return matchResult;
+ }
+ return null;
+ }
+
+ /**
+ * Attempt to match a US state beginnning at position offset in
+ * content. The matching state must be followed by a word
+ * delimiter or the end of the string, and if offset is non-zero,
+ * then it must also be preceded by a word delimiter.
+ *
+ * @return a MatchResult if a valid US state (or two letter code)
+ * was found.
+ */
+ private static MatchResult matchState(String content, int offset) {
+ if (offset > 0 && WORD_DELIM.indexOf(content.charAt(offset - 1)) == -1) return null;
+ Matcher stateMatcher = sStateRe.matcher(content).region(offset, content.length());
+ return stateMatcher.lookingAt() ? stateMatcher.toMatchResult() : null;
+ }
+
+ /**
+ * Test whether zipCode matches the U.S. zip code format (ddddd or
+ * ddddd-dddd) and is within the expected range, given that
+ * stateMatch is a match of sStateRe.
+ *
+ * @return true if zipCode is a valid zip code, is legal for the
+ * matched state, and is followed by a word delimiter or the end
+ * of the string.
+ */
+ private static boolean isValidZipCode(String zipCode, MatchResult stateMatch) {
+ if (stateMatch == null) return false;
+ // Work out the index of the state, based on which group matched.
+ int stateIndex = stateMatch.groupCount();
+ while (stateIndex > 0) {
+ if (stateMatch.group(stateIndex--) != null) break;
+ }
+ return sZipCodeRe.matcher(zipCode).matches()
+ && sStateZipCodeRanges[stateIndex].matches(zipCode);
+ }
+
+ /**
+ * Test whether location is one of the valid locations.
+ *
+ * @return true if location starts with a valid location name
+ * followed by a word delimiter or the end of the string.
+ */
+ private static boolean isValidLocationName(String location) {
+ return sLocationNameRe.matcher(location).matches();
+ }
+
+ /**
+ * Attempt to match a complete address in content, starting with
+ * houseNumberMatch.
+ *
+ * @param content The string to search.
+ * @param houseNumberMatch A matching house number to start extending.
+ * @return +ve: the end of the match
+ * +ve: the position to restart searching for house numbers, negated.
+ */
+ private static int attemptMatch(String content, MatchResult houseNumberMatch) {
+ int restartPos = -1;
+ int nonZipMatch = -1;
+ int it = houseNumberMatch.end();
+ int numLines = 1;
+ boolean consecutiveHouseNumbers = true;
+ boolean foundLocationName = false;
+ int wordCount = 1;
+ String lastWord = "";
+
+ Matcher matcher = sWordRe.matcher(content);
+
+ for (; it < content.length(); lastWord = matcher.group(0), it = matcher.end()) {
+ if (!matcher.find(it)) {
+ // No more words in the input sequence.
+ return -content.length();
+ }
+ if (matcher.end() - matcher.start() > kMaxAddressNameWordLength) {
+ // Word is too long to be part of an address. Fail.
+ return -matcher.end();
+ }
+
+ // Count the number of newlines we just consumed.
+ while (it < matcher.start()) {
+ if (NL.indexOf(content.charAt(it++)) != -1) ++numLines;
+ }
+
+ // Consumed too many lines. Fail.
+ if (numLines > MAX_ADDRESS_LINES) break;
+
+ // Consumed too many words. Fail.
+ if (++wordCount > MAX_ADDRESS_WORDS) break;
+
+ if (matchHouseNumber(content, it) != null) {
+ if (consecutiveHouseNumbers && numLines > 1) {
+ // Last line ended with a number, and this this line starts with one.
+ // Restart at this number.
+ return -it;
+ }
+ // Remember the position of this match as the restart position.
+ if (restartPos == -1) restartPos = it;
+ continue;
+ }
+
+ consecutiveHouseNumbers = false;
+
+ if (isValidLocationName(matcher.group(0))) {
+ foundLocationName = true;
+ continue;
+ }
+
+ if (wordCount == MAX_LOCATION_NAME_DISTANCE && !foundLocationName) {
+ // Didn't find a location name in time. Fail.
+ it = matcher.end();
+ break;
+ }
+
+ if (foundLocationName && wordCount > MIN_ADDRESS_WORDS) {
+ // We can now attempt to match a state.
+ MatchResult stateMatch = matchState(content, it);
+ if (stateMatch != null) {
+ if (lastWord.equals("et") && stateMatch.group(0).equals("al")) {
+ // Reject "et al" as a false postitive.
+ it = stateMatch.end();
+ break;
+ }
+
+ // At this point we've matched a state; try to match a zip code after it.
+ Matcher zipMatcher = sWordRe.matcher(content);
+ if (zipMatcher.find(stateMatch.end())
+ && isValidZipCode(zipMatcher.group(0), stateMatch)) {
+ return zipMatcher.end();
+ }
+ // The content ends with a state but no zip
+ // code. This is a legal match according to the
+ // documentation. N.B. This differs from the
+ // original c++ implementation, which only allowed
+ // the zip code to be optional at the end of the
+ // string, which presumably is a bug. Now we
+ // prefer to find a match with a zip code, but
+ // remember non-zip matches and return them if
+ // necessary.
+ nonZipMatch = stateMatch.end();
+ }
+ }
+ }
+
+ if (nonZipMatch > 0) return nonZipMatch;
+
+ return -(restartPos > 0 ? restartPos : it);
+ }
+
+ /**
+ * Return the first matching address in content.
+ *
+ * @param content The string to search.
+ * @return The first valid address, or null if no address was matched.
+ */
+ static String findAddress(String content) {
+ Matcher houseNumberMatcher = sHouseNumberRe.matcher(content);
+ int start = 0;
+ while (houseNumberMatcher.find(start)) {
+ if (checkHouseNumber(houseNumberMatcher.group(0))) {
+ start = houseNumberMatcher.start();
+ int end = attemptMatch(content, houseNumberMatcher);
+ if (end > 0) {
+ return content.substring(start, end);
+ }
+ start = -end;
+ } else {
+ start = houseNumberMatcher.end();
+ }
+ }
+ return null;
+ }
+}
diff --git a/android/webkit/SafeBrowsingResponse.java b/android/webkit/SafeBrowsingResponse.java
index 960b56bd..1d3a617a 100644
--- a/android/webkit/SafeBrowsingResponse.java
+++ b/android/webkit/SafeBrowsingResponse.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 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.
@@ -16,5 +16,36 @@
package android.webkit;
-public class SafeBrowsingResponse {
+/**
+ * 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);
}
diff --git a/android/webkit/WebViewClient.java b/android/webkit/WebViewClient.java
index f5d220c0..d0f9eee3 100644
--- a/android/webkit/WebViewClient.java
+++ b/android/webkit/WebViewClient.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017 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.
@@ -13,7 +13,545 @@
* 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(prefix = { "SAFE_BROWSING_THREAT_" }, value = {
+ 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/WebViewFactory.java b/android/webkit/WebViewFactory.java
index b3522ec9..e9fe4811 100644
--- a/android/webkit/WebViewFactory.java
+++ b/android/webkit/WebViewFactory.java
@@ -27,7 +27,6 @@ import android.content.pm.PackageManager;
import android.content.pm.Signature;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.os.StrictMode;
import android.os.Trace;
import android.util.AndroidRuntimeException;
import android.util.ArraySet;
@@ -251,7 +250,6 @@ public final class WebViewFactory {
"WebView.disableWebView() was called: WebView is disabled");
}
- StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
Trace.traceBegin(Trace.TRACE_TAG_WEBVIEW, "WebViewFactory.getProvider()");
try {
Class<WebViewFactoryProvider> providerClass = getProviderClass();
@@ -279,7 +277,6 @@ public final class WebViewFactory {
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW);
- StrictMode.setThreadPolicy(oldPolicy);
}
}
}
diff --git a/android/webkit/WebViewFactoryProvider.java b/android/webkit/WebViewFactoryProvider.java
index 3ced6a5f..4f7cdabd 100644
--- a/android/webkit/WebViewFactoryProvider.java
+++ b/android/webkit/WebViewFactoryProvider.java
@@ -172,4 +172,10 @@ public interface WebViewFactoryProvider {
* @return the singleton WebViewDatabase instance
*/
WebViewDatabase getWebViewDatabase(Context context);
+
+ /**
+ * Gets the classloader used to load internal WebView implementation classes. This interface
+ * should only be used by the WebView Support Library.
+ */
+ ClassLoader getWebViewClassLoader();
}
diff --git a/android/widget/AbsListView.java b/android/widget/AbsListView.java
index e0c897d3..594d2400 100644
--- a/android/widget/AbsListView.java
+++ b/android/widget/AbsListView.java
@@ -19,6 +19,7 @@ package android.widget;
import android.annotation.ColorInt;
import android.annotation.DrawableRes;
import android.annotation.NonNull;
+import android.annotation.TestApi;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
@@ -30,6 +31,7 @@ import android.graphics.drawable.TransitionDrawable;
import android.os.Bundle;
import android.os.Debug;
import android.os.Handler;
+import android.os.LocaleList;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.StrictMode;
@@ -2744,7 +2746,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
}
private void drawSelector(Canvas canvas) {
- if (!mSelectorRect.isEmpty()) {
+ if (shouldDrawSelector()) {
final Drawable selector = mSelector;
selector.setBounds(mSelectorRect);
selector.draw(canvas);
@@ -2752,6 +2754,14 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
}
/**
+ * @hide
+ */
+ @TestApi
+ public final boolean shouldDrawSelector() {
+ return !mSelectorRect.isEmpty();
+ }
+
+ /**
* Controls whether the selection highlight drawable should be drawn on top of the item or
* behind it.
*
@@ -6026,6 +6036,11 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
return getTarget().commitContent(inputContentInfo, flags, opts);
}
+
+ @Override
+ public void reportLanguageHint(@NonNull LocaleList languageHint) {
+ getTarget().reportLanguageHint(languageHint);
+ }
}
/**
@@ -6849,7 +6864,7 @@ public abstract class AbsListView extends AdapterView<ListAdapter> implements Te
// detached and we do not allow detached views to fire accessibility
// events. So we are announcing that the subtree changed giving a chance
// to clients holding on to a view in this subtree to refresh it.
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
// Don't scrap views that have transient state.
diff --git a/android/widget/AdapterView.java b/android/widget/AdapterView.java
index 6c192563..08374cb1 100644
--- a/android/widget/AdapterView.java
+++ b/android/widget/AdapterView.java
@@ -1093,7 +1093,7 @@ public abstract class AdapterView<T extends Adapter> extends ViewGroup {
checkSelectionChanged();
}
- notifySubtreeAccessibilityStateChangedIfNeeded();
+ notifyAccessibilitySubtreeChanged();
}
/**
diff --git a/android/widget/CheckedTextView.java b/android/widget/CheckedTextView.java
index 92bfd56d..af01a3eb 100644
--- a/android/widget/CheckedTextView.java
+++ b/android/widget/CheckedTextView.java
@@ -132,7 +132,7 @@ public class CheckedTextView extends TextView implements Checkable {
if (mChecked != checked) {
mChecked = checked;
refreshDrawableState();
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
}
}
diff --git a/android/widget/CompoundButton.java b/android/widget/CompoundButton.java
index 0762b156..e57f1536 100644
--- a/android/widget/CompoundButton.java
+++ b/android/widget/CompoundButton.java
@@ -158,7 +158,7 @@ public abstract class CompoundButton extends Button implements Checkable {
mCheckedFromResource = false;
mChecked = checked;
refreshDrawableState();
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
// Avoid infinite recursions if setChecked() is called from a listener
diff --git a/android/widget/EditText.java b/android/widget/EditText.java
index 336c20cd..728824c2 100644
--- a/android/widget/EditText.java
+++ b/android/widget/EditText.java
@@ -106,6 +106,10 @@ public class EditText extends TextView {
@Override
public Editable getText() {
CharSequence text = super.getText();
+ // This can only happen during construction.
+ if (text == null) {
+ return null;
+ }
if (text instanceof Editable) {
return (Editable) super.getText();
}
diff --git a/android/widget/Editor.java b/android/widget/Editor.java
index 05d18d18..7bb0db1c 100644
--- a/android/widget/Editor.java
+++ b/android/widget/Editor.java
@@ -2095,14 +2095,7 @@ public class Editor {
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);
}
@@ -2179,7 +2172,8 @@ public class Editor {
return false;
}
- if (!checkField() || !mTextView.hasSelection()) {
+ if (actionMode != TextActionMode.TEXT_LINK
+ && (!checkField() || !mTextView.hasSelection())) {
return false;
}
@@ -3679,6 +3673,8 @@ public class Editor {
mIsShowingUp = true;
super.show();
}
+
+ mSuggestionListView.setVisibility(mNumberOfSuggestions == 0 ? View.GONE : View.VISIBLE);
}
@Override
@@ -4012,7 +4008,6 @@ public class Editor {
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,
@@ -4020,14 +4015,15 @@ public class Editor {
.setIcon(textClassification.getIcon())
.setIntent(textClassification.getIntent());
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
- mAssistClickHandlers.put(item, textClassification.getOnClickListener());
+ mAssistClickHandlers.put(
+ item, TextClassification.createStartActivityOnClickListener(
+ mTextView.getContext(), textClassification.getIntent()));
}
final int count = textClassification.getSecondaryActionsCount();
for (int i = 0; i < count; i++) {
if (!isValidAssistMenuItem(
textClassification.getSecondaryIcon(i),
textClassification.getSecondaryLabel(i),
- textClassification.getSecondaryOnClickListener(i),
textClassification.getSecondaryIntent(i))) {
continue;
}
@@ -4038,7 +4034,9 @@ public class Editor {
.setIcon(textClassification.getSecondaryIcon(i))
.setIntent(textClassification.getSecondaryIntent(i));
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
- mAssistClickHandlers.put(item, textClassification.getSecondaryOnClickListener(i));
+ mAssistClickHandlers.put(item,
+ TextClassification.createStartActivityOnClickListener(
+ mTextView.getContext(), textClassification.getSecondaryIntent(i)));
}
}
@@ -4054,10 +4052,9 @@ public class Editor {
}
}
- private boolean isValidAssistMenuItem(
- Drawable icon, CharSequence label, OnClickListener onClick, Intent intent) {
+ private boolean isValidAssistMenuItem(Drawable icon, CharSequence label, Intent intent) {
final boolean hasUi = icon != null || !TextUtils.isEmpty(label);
- final boolean hasAction = onClick != null || isSupportedIntent(intent);
+ final boolean hasAction = isSupportedIntent(intent);
return hasUi && hasAction;
}
@@ -4632,7 +4629,7 @@ public class Editor {
return 0;
}
- protected final void showMagnifier() {
+ protected final void showMagnifier(@NonNull final MotionEvent event) {
if (mMagnifier == null) {
return;
}
@@ -4658,9 +4655,10 @@ public class Editor {
final Layout layout = mTextView.getLayout();
final int lineNumber = layout.getLineForOffset(offset);
- // Horizontally snap to character offset.
- final float xPosInView = getHorizontal(mTextView.getLayout(), offset)
- + mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
+ // Horizontally move the magnifier smoothly.
+ final int[] textViewLocationOnScreen = new int[2];
+ mTextView.getLocationOnScreen(textViewLocationOnScreen);
+ final float xPosInView = event.getRawX() - textViewLocationOnScreen[0];
// Vertically snap to middle of current line.
final float yPosInView = (mTextView.getLayout().getLineTop(lineNumber)
+ mTextView.getLayout().getLineBottom(lineNumber)) / 2.0f
@@ -4855,11 +4853,11 @@ public class Editor {
case MotionEvent.ACTION_DOWN:
mDownPositionX = ev.getRawX();
mDownPositionY = ev.getRawY();
- showMagnifier();
+ showMagnifier(ev);
break;
case MotionEvent.ACTION_MOVE:
- showMagnifier();
+ showMagnifier(ev);
break;
case MotionEvent.ACTION_UP:
@@ -5213,11 +5211,11 @@ public class Editor {
// re-engages the handle.
mTouchWordDelta = 0.0f;
mPrevX = UNSET_X_VALUE;
- showMagnifier();
+ showMagnifier(event);
break;
case MotionEvent.ACTION_MOVE:
- showMagnifier();
+ showMagnifier(event);
break;
case MotionEvent.ACTION_UP:
diff --git a/android/widget/Magnifier.java b/android/widget/Magnifier.java
index 26dfcc2d..310b1708 100644
--- a/android/widget/Magnifier.java
+++ b/android/widget/Magnifier.java
@@ -32,6 +32,7 @@ import android.view.PixelCopy;
import android.view.Surface;
import android.view.SurfaceView;
import android.view.View;
+import android.view.ViewParent;
import com.android.internal.util.Preconditions;
@@ -44,6 +45,8 @@ public final class Magnifier {
private static final int NONEXISTENT_PREVIOUS_CONFIG_VALUE = -1;
// The view to which this magnifier is attached.
private final View mView;
+ // The coordinates of the view in the surface.
+ private final int[] mViewCoordinatesInSurface;
// The window containing the magnifier.
private final PopupWindow mWindow;
// The center coordinates of the window containing the magnifier.
@@ -87,6 +90,8 @@ public final class Magnifier {
com.android.internal.R.dimen.magnifier_height);
mZoomScale = context.getResources().getFloat(
com.android.internal.R.dimen.magnifier_zoom_scale);
+ // The view's surface coordinates will not be updated until the magnifier is first shown.
+ mViewCoordinatesInSurface = new int[2];
mWindow = new PopupWindow(context);
mWindow.setContentView(content);
@@ -120,9 +125,34 @@ public final class Magnifier {
configureCoordinates(xPosInView, yPosInView);
// Clamp startX value to avoid distorting the rendering of the magnifier content.
- final int startX = Math.max(0, Math.min(
+ // For this, we compute:
+ // - zeroScrollXInSurface: this is the start x of mView, where this is not masked by a
+ // potential scrolling container. For example, if mView is a
+ // TextView contained in a HorizontalScrollView,
+ // mViewCoordinatesInSurface will reflect the surface position of
+ // the first text character, rather than the position of the first
+ // visible one. Therefore, we need to add back the amount of
+ // scrolling from the parent containers.
+ // - actualWidth: similarly, the width of a View will be larger than its actually visible
+ // width when it is contained in a scrolling container. We need to use
+ // the minimum width of a scrolling container which contains this view.
+ int zeroScrollXInSurface = mViewCoordinatesInSurface[0];
+ int actualWidth = mView.getWidth();
+ ViewParent viewParent = mView.getParent();
+ while (viewParent instanceof View) {
+ final View container = (View) viewParent;
+ if (container.canScrollHorizontally(-1 /* left scroll */)
+ || container.canScrollHorizontally(1 /* right scroll */)) {
+ zeroScrollXInSurface += container.getScrollX();
+ actualWidth = Math.min(actualWidth, container.getWidth()
+ - container.getPaddingLeft() - container.getPaddingRight());
+ }
+ viewParent = viewParent.getParent();
+ }
+
+ final int startX = Math.max(zeroScrollXInSurface, Math.min(
mCenterZoomCoords.x - mBitmap.getWidth() / 2,
- mView.getWidth() - mBitmap.getWidth()));
+ zeroScrollXInSurface + actualWidth - mBitmap.getWidth()));
final int startY = mCenterZoomCoords.y - mBitmap.getHeight() / 2;
if (xPosInView != mPrevPosInView.x || yPosInView != mPrevPosInView.y) {
@@ -169,10 +199,9 @@ public final class Magnifier {
posX = xPosInView;
posY = yPosInView;
} else {
- final int[] coordinatesInSurface = new int[2];
- mView.getLocationInSurface(coordinatesInSurface);
- posX = xPosInView + coordinatesInSurface[0];
- posY = yPosInView + coordinatesInSurface[1];
+ mView.getLocationInSurface(mViewCoordinatesInSurface);
+ posX = xPosInView + mViewCoordinatesInSurface[0];
+ posY = yPosInView + mViewCoordinatesInSurface[1];
}
mCenterZoomCoords.x = Math.round(posX);
diff --git a/android/widget/MediaControlView2.java b/android/widget/MediaControlView2.java
new file mode 100644
index 00000000..f1d633a2
--- /dev/null
+++ b/android/widget/MediaControlView2.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.widget;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.session.MediaController;
+import android.media.update.ApiLoader;
+import android.media.update.MediaControlView2Provider;
+import android.media.update.ViewProvider;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+/**
+ * A View that contains the controls for MediaPlayer2.
+ * It provides a wide range of UI including buttons such as "Play/Pause", "Rewind", "Fast Forward",
+ * "Subtitle", "Full Screen", and it is also possible to add multiple custom buttons.
+ *
+ * <p>
+ * <em> MediaControlView2 can be initialized in two different ways: </em>
+ * 1) When VideoView2 is initialized, it automatically initializes a MediaControlView2 instance and
+ * adds it to the view.
+ * 2) Initialize MediaControlView2 programmatically and add it to a ViewGroup instance.
+ *
+ * In the first option, VideoView2 automatically connects MediaControlView2 to MediaController2,
+ * which is necessary to communicate with MediaSession2. In the second option, however, the
+ * developer needs to manually retrieve a MediaController2 instance and set it to MediaControlView2
+ * by calling setController(MediaController2 controller).
+ *
+ * TODO PUBLIC API
+ * @hide
+ */
+public class MediaControlView2 extends FrameLayout {
+ /** @hide */
+ @IntDef({
+ BUTTON_PLAY_PAUSE,
+ BUTTON_FFWD,
+ BUTTON_REW,
+ BUTTON_NEXT,
+ BUTTON_PREV,
+ BUTTON_SUBTITLE,
+ BUTTON_FULL_SCREEN,
+ BUTTON_OVERFLOW,
+ BUTTON_MUTE,
+ BUTTON_ASPECT_RATIO,
+ BUTTON_SETTINGS
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Button {}
+
+ public static final int BUTTON_PLAY_PAUSE = 1;
+ public static final int BUTTON_FFWD = 2;
+ public static final int BUTTON_REW = 3;
+ public static final int BUTTON_NEXT = 4;
+ public static final int BUTTON_PREV = 5;
+ public static final int BUTTON_SUBTITLE = 6;
+ public static final int BUTTON_FULL_SCREEN = 7;
+ public static final int BUTTON_OVERFLOW = 8;
+ public static final int BUTTON_MUTE = 9;
+ public static final int BUTTON_ASPECT_RATIO = 10;
+ public static final int BUTTON_SETTINGS = 11;
+
+ private final MediaControlView2Provider mProvider;
+
+ public MediaControlView2(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public MediaControlView2(@NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mProvider = ApiLoader.getProvider(context)
+ .createMediaControlView2(this, new SuperProvider());
+ }
+
+ /**
+ * @hide
+ */
+ public MediaControlView2Provider getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * Sets MediaController2 instance to control corresponding MediaSession2.
+ */
+ public void setController(MediaController controller) {
+ mProvider.setController_impl(controller);
+ }
+
+ /**
+ * Shows the control view on screen. It will disappear automatically after 3 seconds of
+ * inactivity.
+ */
+ public void show() {
+ mProvider.show_impl();
+ }
+
+ /**
+ * Shows the control view on screen. It will disappear automatically after {@code timeout}
+ * milliseconds of inactivity.
+ */
+ public void show(int timeout) {
+ mProvider.show_impl(timeout);
+ }
+
+ /**
+ * Returns whether the control view is currently shown or hidden.
+ */
+ public boolean isShowing() {
+ return mProvider.isShowing_impl();
+ }
+
+ /**
+ * Hide the control view from the screen.
+ */
+ public void hide() {
+ mProvider.hide_impl();
+ }
+
+ /**
+ * If the media selected has a subtitle track, calling this method will display the subtitle at
+ * the bottom of the view. If a media has multiple subtitle tracks, this method will select the
+ * first one of them.
+ */
+ public void showSubtitle() {
+ mProvider.showSubtitle_impl();
+ }
+
+ /**
+ * Hides the currently displayed subtitle.
+ */
+ public void hideSubtitle() {
+ mProvider.hideSubtitle_impl();
+ }
+
+ /**
+ * Set listeners for previous and next buttons to customize the behavior of clicking them.
+ * The UI for these buttons are provided as default and will be automatically displayed when
+ * this method is called.
+ *
+ * @param next Listener for clicking next button
+ * @param prev Listener for clicking previous button
+ */
+ public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) {
+ mProvider.setPrevNextListeners_impl(next, prev);
+ }
+
+ /**
+ * Hides the specified button from view.
+ *
+ * @param button the constant integer assigned to individual buttons
+ * @param visible whether the button should be visible or not
+ */
+ public void setButtonVisibility(int button, boolean visible) {
+ mProvider.setButtonVisibility_impl(button, visible);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ mProvider.onAttachedToWindow_impl();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ mProvider.onDetachedFromWindow_impl();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return mProvider.getAccessibilityClassName_impl();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ return mProvider.onTouchEvent_impl(ev);
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ return mProvider.onTrackballEvent_impl(ev);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return mProvider.onKeyDown_impl(keyCode, event);
+ }
+
+ @Override
+ public void onFinishInflate() {
+ mProvider.onFinishInflate_impl();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return mProvider.dispatchKeyEvent_impl(event);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mProvider.setEnabled_impl(enabled);
+ }
+
+ private class SuperProvider implements ViewProvider {
+ @Override
+ public void onAttachedToWindow_impl() {
+ MediaControlView2.super.onAttachedToWindow();
+ }
+
+ @Override
+ public void onDetachedFromWindow_impl() {
+ MediaControlView2.super.onDetachedFromWindow();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName_impl() {
+ return MediaControlView2.super.getAccessibilityClassName();
+ }
+
+ @Override
+ public boolean onTouchEvent_impl(MotionEvent ev) {
+ return MediaControlView2.super.onTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTrackballEvent_impl(MotionEvent ev) {
+ return MediaControlView2.super.onTrackballEvent(ev);
+ }
+
+ @Override
+ public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
+ return MediaControlView2.super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public void onFinishInflate_impl() {
+ MediaControlView2.super.onFinishInflate();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent_impl(KeyEvent event) {
+ return MediaControlView2.super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public void setEnabled_impl(boolean enabled) {
+ MediaControlView2.super.setEnabled(enabled);
+ }
+ }
+}
diff --git a/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java
index 2c6466cd..3bfa520c 100644
--- a/android/widget/SelectionActionModeHelper.java
+++ b/android/widget/SelectionActionModeHelper.java
@@ -235,10 +235,13 @@ public final class SelectionActionModeHelper {
@Editor.TextActionMode int actionMode, @Nullable SelectionResult result) {
final CharSequence text = getText(mTextView);
if (result != null && text instanceof Spannable
- && (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
+ && (mTextView.isTextSelectable()
+ || mTextView.isTextEditable()
+ || actionMode == Editor.TextActionMode.TEXT_LINK)) {
// Do not change the selection if TextClassifier should be dark launched.
if (!mTextView.getTextClassifier().getSettings().isDarkLaunch()) {
Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
+ mTextView.invalidate();
}
mTextClassification = result.mClassification;
} else {
@@ -250,8 +253,17 @@ public final class SelectionActionModeHelper {
&& (mTextView.isTextSelectable() || mTextView.isTextEditable())) {
controller.show();
}
- if (result != null && actionMode == Editor.TextActionMode.SELECTION) {
- mSelectionTracker.onSmartSelection(result);
+ if (result != null) {
+ switch (actionMode) {
+ case Editor.TextActionMode.SELECTION:
+ mSelectionTracker.onSmartSelection(result);
+ break;
+ case Editor.TextActionMode.TEXT_LINK:
+ mSelectionTracker.onLinkSelected(result);
+ break;
+ default:
+ break;
+ }
}
}
mEditor.setRestartActionModeOnNextRefresh(false);
@@ -486,12 +498,24 @@ public final class SelectionActionModeHelper {
* Called when selection action mode is started and the results come from a classifier.
*/
public void onSmartSelection(SelectionResult result) {
+ onClassifiedSelection(result);
+ mLogger.logSelectionModified(
+ result.mStart, result.mEnd, result.mClassification, result.mSelection);
+ }
+
+ /**
+ * Called when link action mode is started and the classification comes from a classifier.
+ */
+ public void onLinkSelected(SelectionResult result) {
+ onClassifiedSelection(result);
+ // TODO: log (b/70246800)
+ }
+
+ private void onClassifiedSelection(SelectionResult result) {
if (isSelectionStarted()) {
mSelectionStart = result.mStart;
mSelectionEnd = result.mEnd;
mAllowReset = mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd;
- mLogger.logSelectionModified(
- result.mStart, result.mEnd, result.mClassification, result.mSelection);
}
}
diff --git a/android/widget/TextView.java b/android/widget/TextView.java
index 1e17f34a..7d3fcf46 100644
--- a/android/widget/TextView.java
+++ b/android/widget/TextView.java
@@ -27,8 +27,10 @@ import android.annotation.ColorInt;
import android.annotation.DrawableRes;
import android.annotation.FloatRange;
import android.annotation.IntDef;
+import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.annotation.Px;
import android.annotation.Size;
import android.annotation.StringRes;
import android.annotation.StyleRes;
@@ -44,6 +46,7 @@ import android.content.UndoManager;
import android.content.res.ColorStateList;
import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
+import android.content.res.ResourceId;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.content.res.XmlResourceParser;
@@ -51,6 +54,7 @@ import android.graphics.BaseCanvas;
import android.graphics.Canvas;
import android.graphics.Insets;
import android.graphics.Paint;
+import android.graphics.Paint.FontMetricsInt;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.Rect;
@@ -76,8 +80,8 @@ import android.text.GraphicsOperations;
import android.text.InputFilter;
import android.text.InputType;
import android.text.Layout;
+import android.text.MeasuredText;
import android.text.ParcelableSpan;
-import android.text.PremeasuredText;
import android.text.Selection;
import android.text.SpanWatcher;
import android.text.Spannable;
@@ -295,6 +299,7 @@ import java.util.Locale;
* @attr ref android.R.styleable#TextView_imeActionId
* @attr ref android.R.styleable#TextView_editorExtras
* @attr ref android.R.styleable#TextView_elegantTextHeight
+ * @attr ref android.R.styleable#TextView_fallbackLineSpacing
* @attr ref android.R.styleable#TextView_letterSpacing
* @attr ref android.R.styleable#TextView_fontFeatureSettings
* @attr ref android.R.styleable#TextView_breakStrategy
@@ -304,12 +309,12 @@ import java.util.Locale;
* @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
* @attr ref android.R.styleable#TextView_autoSizeStepGranularity
* @attr ref android.R.styleable#TextView_autoSizePresetSizes
+ * @attr ref android.R.styleable#TextView_accessibilityHeading
*/
@RemoteView
public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
static final String LOG_TAG = "TextView";
static final boolean DEBUG_EXTRACT = false;
- static final boolean DEBUG_AUTOFILL = false;
private static final float[] TEMP_POSITION = new float[2];
// Enum for the "typeface" XML parameter.
@@ -399,6 +404,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private int mCurTextColor;
private int mCurHintTextColor;
private boolean mFreezesText;
+ private boolean mIsAccessibilityHeading;
private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
@@ -654,7 +660,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// True if internationalized input should be used for numbers and date and time.
private final boolean mUseInternationalizedInput;
// True if fallback fonts that end up getting used should be allowed to affect line spacing.
- /* package */ final boolean mUseFallbackLineSpacing;
+ /* package */ boolean mUseFallbackLineSpacing;
@ViewDebug.ExportedProperty(category = "text")
private int mGravity = Gravity.TOP | Gravity.START;
@@ -785,9 +791,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// mAutoSizeStepGranularityInPx.
private boolean mHasPresetAutoSizeValues = false;
- // Indicates whether the text was set from resources or dynamically, so it can be used to
+ // Indicates whether the text was set statically or dynamically, so it can be used to
// sanitize autofill requests.
- private boolean mTextFromResource = false;
+ private boolean mTextSetFromXmlOrResourceId = false;
+ // Resource id used to set the text - used for autofill purposes.
+ private @StringRes int mTextId = ResourceId.ID_NULL;
/**
* Kick-start the font cache for the zygote process (to pay the cost of
@@ -921,12 +929,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
int inputType = EditorInfo.TYPE_NULL;
a = theme.obtainStyledAttributes(
attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
+ int firstBaselineToTopHeight = -1;
+ int lastBaselineToBottomHeight = -1;
+ int lineHeight = -1;
readTextAppearance(context, a, attributes, true /* styleArray */);
int n = a.getIndexCount();
- boolean fromResourceId = false;
+ // Must set id in a temporary variable because it will be reset by setText()
+ boolean textIsSetFromXml = false;
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
@@ -1068,7 +1080,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
break;
case com.android.internal.R.styleable.TextView_text:
- fromResourceId = true;
+ textIsSetFromXml = true;
+ mTextId = a.getResourceId(attr, ResourceId.ID_NULL);
text = a.getText(attr);
break;
@@ -1244,6 +1257,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
case com.android.internal.R.styleable.TextView_justificationMode:
mJustificationMode = a.getInt(attr, Layout.JUSTIFICATION_MODE_NONE);
break;
+
+ case com.android.internal.R.styleable.TextView_firstBaselineToTopHeight:
+ firstBaselineToTopHeight = a.getDimensionPixelSize(attr, -1);
+ break;
+
+ case com.android.internal.R.styleable.TextView_lastBaselineToBottomHeight:
+ lastBaselineToBottomHeight = a.getDimensionPixelSize(attr, -1);
+ break;
+
+ case com.android.internal.R.styleable.TextView_lineHeight:
+ lineHeight = a.getDimensionPixelSize(attr, -1);
+ break;
+ case com.android.internal.R.styleable.TextView_accessibilityHeading:
+ mIsAccessibilityHeading = a.getBoolean(attr, false);
}
}
@@ -1460,8 +1487,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
setText(text, bufferType);
- if (fromResourceId) {
- mTextFromResource = true;
+ if (textIsSetFromXml) {
+ mTextSetFromXmlOrResourceId = true;
}
if (hint != null) setHint(hint);
@@ -1558,6 +1585,16 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
} else {
mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_NONE;
}
+
+ if (firstBaselineToTopHeight >= 0) {
+ setFirstBaselineToTopHeight(firstBaselineToTopHeight);
+ }
+ if (lastBaselineToBottomHeight >= 0) {
+ setLastBaselineToBottomHeight(lastBaselineToBottomHeight);
+ }
+ if (lineHeight >= 0) {
+ setLineHeight(lineHeight);
+ }
}
/**
@@ -2360,7 +2397,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
setText(mText);
if (hasPasswordTransformationMethod()) {
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
}
@@ -3160,6 +3197,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
+ /**
+ * @inheritDoc
+ *
+ * @see #setFirstBaselineToTopHeight(int)
+ * @see #setLastBaselineToBottomHeight(int)
+ */
@Override
public void setPadding(int left, int top, int right, int bottom) {
if (left != mPaddingLeft
@@ -3174,6 +3217,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
invalidate();
}
+ /**
+ * @inheritDoc
+ *
+ * @see #setFirstBaselineToTopHeight(int)
+ * @see #setLastBaselineToBottomHeight(int)
+ */
@Override
public void setPaddingRelative(int start, int top, int end, int bottom) {
if (start != getPaddingStart()
@@ -3189,6 +3238,97 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
/**
+ * Updates the top padding of the TextView so that {@code firstBaselineToTopHeight} is
+ * equal to the distance between the firt text baseline and the top of this TextView.
+ * <strong>Note</strong> that if {@code FontMetrics.top} or {@code FontMetrics.ascent} was
+ * already greater than {@code firstBaselineToTopHeight}, the top padding is not updated.
+ *
+ * @param firstBaselineToTopHeight distance between first baseline to top of the container
+ * in pixels
+ *
+ * @see #getFirstBaselineToTopHeight()
+ * @see #setPadding(int, int, int, int)
+ * @see #setPaddingRelative(int, int, int, int)
+ *
+ * @attr ref android.R.styleable#TextView_firstBaselineToTopHeight
+ */
+ public void setFirstBaselineToTopHeight(@Px @IntRange(from = 0) int firstBaselineToTopHeight) {
+ Preconditions.checkArgumentNonnegative(firstBaselineToTopHeight);
+
+ final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt();
+ final int fontMetricsTop;
+ if (getIncludeFontPadding()) {
+ fontMetricsTop = fontMetrics.top;
+ } else {
+ fontMetricsTop = fontMetrics.ascent;
+ }
+
+ // TODO: Decide if we want to ignore density ratio (i.e. when the user changes font size
+ // in settings). At the moment, we don't.
+
+ if (firstBaselineToTopHeight > Math.abs(fontMetricsTop)) {
+ final int paddingTop = firstBaselineToTopHeight - (-fontMetricsTop);
+ setPadding(getPaddingLeft(), paddingTop, getPaddingRight(), getPaddingBottom());
+ }
+ }
+
+ /**
+ * Updates the bottom padding of the TextView so that {@code lastBaselineToBottomHeight} is
+ * equal to the distance between the last text baseline and the bottom of this TextView.
+ * <strong>Note</strong> that if {@code FontMetrics.bottom} or {@code FontMetrics.descent} was
+ * already greater than {@code lastBaselineToBottomHeight}, the bottom padding is not updated.
+ *
+ * @param lastBaselineToBottomHeight distance between last baseline to bottom of the container
+ * in pixels
+ *
+ * @see #getLastBaselineToBottomHeight()
+ * @see #setPadding(int, int, int, int)
+ * @see #setPaddingRelative(int, int, int, int)
+ *
+ * @attr ref android.R.styleable#TextView_lastBaselineToBottomHeight
+ */
+ public void setLastBaselineToBottomHeight(
+ @Px @IntRange(from = 0) int lastBaselineToBottomHeight) {
+ Preconditions.checkArgumentNonnegative(lastBaselineToBottomHeight);
+
+ final FontMetricsInt fontMetrics = getPaint().getFontMetricsInt();
+ final int fontMetricsBottom;
+ if (getIncludeFontPadding()) {
+ fontMetricsBottom = fontMetrics.bottom;
+ } else {
+ fontMetricsBottom = fontMetrics.descent;
+ }
+
+ // TODO: Decide if we want to ignore density ratio (i.e. when the user changes font size
+ // in settings). At the moment, we don't.
+
+ if (lastBaselineToBottomHeight > Math.abs(fontMetricsBottom)) {
+ final int paddingBottom = lastBaselineToBottomHeight - fontMetricsBottom;
+ setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), paddingBottom);
+ }
+ }
+
+ /**
+ * Returns the distance between the first text baseline and the top of this TextView.
+ *
+ * @see #setFirstBaselineToTopHeight(int)
+ * @attr ref android.R.styleable#TextView_firstBaselineToTopHeight
+ */
+ public int getFirstBaselineToTopHeight() {
+ return getPaddingTop() - getPaint().getFontMetricsInt().top;
+ }
+
+ /**
+ * Returns the distance between the last text baseline and the bottom of this TextView.
+ *
+ * @see #setLastBaselineToBottomHeight(int)
+ * @attr ref android.R.styleable#TextView_lastBaselineToBottomHeight
+ */
+ public int getLastBaselineToBottomHeight() {
+ return getPaddingBottom() + getPaint().getFontMetricsInt().bottom;
+ }
+
+ /**
* Gets the autolink mask of the text. See {@link
* android.text.util.Linkify#ALL Linkify.ALL} and peers for
* possible values.
@@ -3250,6 +3390,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
float mShadowDx = 0, mShadowDy = 0, mShadowRadius = 0;
boolean mHasElegant = false;
boolean mElegant = false;
+ boolean mHasFallbackLineSpacing = false;
+ boolean mFallbackLineSpacing = false;
boolean mHasLetterSpacing = false;
float mLetterSpacing = 0;
String mFontFeatureSettings = null;
@@ -3274,6 +3416,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
+ " mShadowRadius:" + mShadowRadius + "\n"
+ " mHasElegant:" + mHasElegant + "\n"
+ " mElegant:" + mElegant + "\n"
+ + " mHasFallbackLineSpacing:" + mHasFallbackLineSpacing + "\n"
+ + " mFallbackLineSpacing:" + mFallbackLineSpacing + "\n"
+ " mHasLetterSpacing:" + mHasLetterSpacing + "\n"
+ " mLetterSpacing:" + mLetterSpacing + "\n"
+ " mFontFeatureSettings:" + mFontFeatureSettings + "\n"
@@ -3312,6 +3456,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
com.android.internal.R.styleable.TextAppearance_shadowRadius);
sAppearanceValues.put(com.android.internal.R.styleable.TextView_elegantTextHeight,
com.android.internal.R.styleable.TextAppearance_elegantTextHeight);
+ sAppearanceValues.put(com.android.internal.R.styleable.TextView_fallbackLineSpacing,
+ com.android.internal.R.styleable.TextAppearance_fallbackLineSpacing);
sAppearanceValues.put(com.android.internal.R.styleable.TextView_letterSpacing,
com.android.internal.R.styleable.TextAppearance_letterSpacing);
sAppearanceValues.put(com.android.internal.R.styleable.TextView_fontFeatureSettings,
@@ -3402,6 +3548,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
attributes.mHasElegant = true;
attributes.mElegant = appearance.getBoolean(attr, attributes.mElegant);
break;
+ case com.android.internal.R.styleable.TextAppearance_fallbackLineSpacing:
+ attributes.mHasFallbackLineSpacing = true;
+ attributes.mFallbackLineSpacing = appearance.getBoolean(attr,
+ attributes.mFallbackLineSpacing);
+ break;
case com.android.internal.R.styleable.TextAppearance_letterSpacing:
attributes.mHasLetterSpacing = true;
attributes.mLetterSpacing =
@@ -3455,6 +3606,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
setElegantTextHeight(attributes.mElegant);
}
+ if (attributes.mHasFallbackLineSpacing) {
+ setFallbackLineSpacing(attributes.mFallbackLineSpacing);
+ }
+
if (attributes.mHasLetterSpacing) {
setLetterSpacing(attributes.mLetterSpacing);
}
@@ -3736,7 +3891,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
*
* @param elegant set the paint's elegant metrics flag.
*
- * @see Paint#isElegantTextHeight(boolean)
+ * @see #isElegantTextHeight()
+ * @see Paint#isElegantTextHeight()
*
* @attr ref android.R.styleable#TextView_elegantTextHeight
*/
@@ -3752,6 +3908,43 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
/**
+ * Set whether to respect the ascent and descent of the fallback fonts that are used in
+ * displaying the text (which is needed to avoid text from consecutive lines running into
+ * each other). If set, fallback fonts that end up getting used can increase the ascent
+ * and descent of the lines that they are used on.
+ * <p/>
+ * It is required to be true if text could be in languages like Burmese or Tibetan where text
+ * is typically much taller or deeper than Latin text.
+ *
+ * @param enabled whether to expand linespacing based on fallback fonts, {@code true} by default
+ *
+ * @see StaticLayout.Builder#setUseLineSpacingFromFallbacks(boolean)
+ *
+ * @attr ref android.R.styleable#TextView_fallbackLineSpacing
+ */
+ public void setFallbackLineSpacing(boolean enabled) {
+ if (mUseFallbackLineSpacing != enabled) {
+ mUseFallbackLineSpacing = enabled;
+ if (mLayout != null) {
+ nullLayouts();
+ requestLayout();
+ invalidate();
+ }
+ }
+ }
+
+ /**
+ * @return whether fallback line spacing is enabled, {@code true} by default
+ *
+ * @see #setFallbackLineSpacing(boolean)
+ *
+ * @attr ref android.R.styleable#TextView_fallbackLineSpacing
+ */
+ public boolean isFallbackLineSpacing() {
+ return mUseFallbackLineSpacing;
+ }
+
+ /**
* Get the value of the TextView's elegant height metrics flag. This setting selects font
* variants that have not been compacted to fit Latin-based vertical
* metrics, and also increases top and bottom bounds to provide more space.
@@ -4917,6 +5110,53 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
/**
+ * Sets an explicit line height for this TextView. This is equivalent to the vertical distance
+ * between subsequent baselines in the TextView.
+ *
+ * @param lineHeight the line height in pixels
+ *
+ * @see #setLineSpacing(float, float)
+ * @see #getLineSpacing()
+ *
+ * @attr ref android.R.styleable#TextView_lineHeight
+ */
+ public void setLineHeight(@Px @IntRange(from = 0) int lineHeight) {
+ Preconditions.checkArgumentNonnegative(lineHeight);
+
+ final int fontHeight = getPaint().getFontMetricsInt(null);
+ // Make sure we don't setLineSpacing if it's not needed to avoid unnecessary redraw.
+ if (lineHeight != fontHeight) {
+ // Set lineSpacingExtra by the difference of lineSpacing with lineHeight
+ setLineSpacing(lineHeight - fontHeight, 1f);
+ }
+ }
+
+ /**
+ * Gets whether this view is a heading for accessibility purposes.
+ *
+ * @return {@code true} if the view is a heading, {@code false} otherwise.
+ *
+ * @attr ref android.R.styleable#TextView_accessibilityHeading
+ */
+ public boolean isAccessibilityHeading() {
+ return mIsAccessibilityHeading;
+ }
+
+ /**
+ * Set if view is a heading for a section of content for accessibility purposes.
+ *
+ * @param isHeading {@code true} if the view is a heading, {@code false} otherwise.
+ *
+ * @attr ref android.R.styleable#TextView_accessibilityHeading
+ */
+ public void setAccessibilityHeading(boolean isHeading) {
+ if (isHeading != mIsAccessibilityHeading) {
+ mIsAccessibilityHeading = isHeading;
+ notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+ }
+ }
+
+ /**
* Convenience method to append the specified text to the TextView's
* display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE}
* if it was not already editable.
@@ -5278,7 +5518,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
- mTextFromResource = false;
+ mTextSetFromXmlOrResourceId = false;
if (text == null) {
text = "";
}
@@ -5336,7 +5576,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 PremeasuredText || text instanceof CharWrapper)) {
+ } else if (!(text instanceof MeasuredText || text instanceof CharWrapper)) {
text = TextUtils.stringOrSpannedString(text);
}
@@ -5419,7 +5659,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
- notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
+ notifyAccessibilityStateChanged(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
if (needEditableForNotification) {
sendAfterTextChanged((Editable) text);
@@ -5516,7 +5756,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
@android.view.RemotableViewMethod
public final void setText(@StringRes int resid) {
setText(getContext().getResources().getText(resid));
- mTextFromResource = true;
+ mTextSetFromXmlOrResourceId = true;
+ mTextId = resid;
}
/**
@@ -5543,7 +5784,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
*/
public final void setText(@StringRes int resid, BufferType type) {
setText(getContext().getResources().getText(resid), type);
- mTextFromResource = true;
+ mTextSetFromXmlOrResourceId = true;
+ mTextId = resid;
}
/**
@@ -6151,7 +6393,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
public void setError(CharSequence error, Drawable icon) {
createEditorIfNeeded();
mEditor.setError(error, icon);
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
}
@@ -9066,8 +9308,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
/**
*
- * Checks whether the transformation method applied to this TextView is set to ALL CAPS. This
- * settings is internally ignored if this field is editable or selectable.
+ * Checks whether the transformation method applied to this TextView is set to ALL CAPS.
* @return Whether the current transformation method is for ALL CAPS.
*
* @see #setAllCaps(boolean)
@@ -9456,7 +9697,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
if (afm != null) {
- if (DEBUG_AUTOFILL) {
+ if (android.view.autofill.Helper.sVerbose) {
Log.v(LOG_TAG, "sendAfterTextChanged(): notify AFM for text=" + mText);
}
afm.notifyValueChanged(TextView.this);
@@ -10234,7 +10475,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
final boolean isPassword = hasPasswordTransformationMethod()
|| isPasswordInputType(getInputType());
if (forAutofill) {
- structure.setDataIsSensitive(!mTextFromResource);
+ structure.setDataIsSensitive(!mTextSetFromXmlOrResourceId);
+ if (mTextId != ResourceId.ID_NULL) {
+ try {
+ structure.setTextIdEntry(getResources().getResourceEntryName(mTextId));
+ } catch (Resources.NotFoundException e) {
+ if (android.view.autofill.Helper.sVerbose) {
+ Log.v(LOG_TAG, "onProvideAutofillStructure(): cannot set name for text id "
+ + mTextId + ": " + e.getMessage());
+ }
+ }
+ }
}
if (!isPassword || forAutofill) {
@@ -10455,6 +10706,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
info.setText(getTextForAccessibility());
info.setHintText(mHint);
info.setShowingHintText(isShowingHint());
+ info.setHeading(mIsAccessibilityHeading);
if (mBufferType == BufferType.EDITABLE) {
info.setEditable(true);
@@ -10942,6 +11194,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return true;
case ID_COPY:
+ // For link action mode in a non-selectable/non-focusable TextView,
+ // make sure that we set the appropriate min/max.
+ final int selStart = getSelectionStart();
+ final int selEnd = getSelectionEnd();
+ min = Math.max(0, Math.min(selStart, selEnd));
+ max = Math.max(0, Math.max(selStart, selEnd));
final ClipData copyData = ClipData.newPlainText(null, getTransformedText(min, max));
if (setPrimaryClip(copyData)) {
stopTextActionMode();
@@ -11164,11 +11422,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
*/
public boolean requestActionMode(@NonNull TextLinks.TextLink link) {
Preconditions.checkNotNull(link);
- if (mEditor != null) {
- mEditor.startLinkActionModeAsync(link);
- return true;
- }
- return false;
+ createEditorIfNeeded();
+ mEditor.startLinkActionModeAsync(link);
+ return true;
}
/**
* @hide
@@ -11883,7 +12139,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private final Choreographer mChoreographer;
private byte mStatus = MARQUEE_STOPPED;
- private final float mPixelsPerSecond;
+ private final float mPixelsPerMs;
private float mMaxScroll;
private float mMaxFadeScroll;
private float mGhostStart;
@@ -11896,7 +12152,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
Marquee(TextView v) {
final float density = v.getContext().getResources().getDisplayMetrics().density;
- mPixelsPerSecond = MARQUEE_DP_PER_SECOND * density;
+ mPixelsPerMs = MARQUEE_DP_PER_SECOND * density / 1000f;
mView = new WeakReference<TextView>(v);
mChoreographer = Choreographer.getInstance();
}
@@ -11941,7 +12197,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
long currentMs = mChoreographer.getFrameTime();
long deltaMs = currentMs - mLastAnimationMs;
mLastAnimationMs = currentMs;
- float deltaPx = deltaMs / 1000f * mPixelsPerSecond;
+ float deltaPx = deltaMs * mPixelsPerMs;
mScroll += deltaPx;
if (mScroll > mMaxScroll) {
mScroll = mMaxScroll;
diff --git a/android/widget/VideoView2.java b/android/widget/VideoView2.java
new file mode 100644
index 00000000..8650c0a0
--- /dev/null
+++ b/android/widget/VideoView2.java
@@ -0,0 +1,602 @@
+/*
+ * Copyright 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.widget;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.MediaPlayerBase;
+import android.media.update.ApiLoader;
+import android.media.update.VideoView2Provider;
+import android.media.update.ViewProvider;
+import android.net.Uri;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Map;
+
+// TODO: Use @link tag to refer MediaPlayer2 in docs once MediaPlayer2.java is submitted. Same to
+// MediaSession2.
+// TODO: change the reference from MediaPlayer to MediaPlayer2.
+/**
+ * Displays a video file. VideoView2 class is a View class which is wrapping MediaPlayer2 so that
+ * developers can easily implement a video rendering application.
+ *
+ * <p>
+ * <em> Data sources that VideoView2 supports : </em>
+ * VideoView2 can play video files and audio-only fiels as
+ * well. It can load from various sources such as resources or content providers. The supported
+ * media file formats are the same as MediaPlayer2.
+ *
+ * <p>
+ * <em> View type can be selected : </em>
+ * VideoView2 can render videos on top of TextureView as well as
+ * SurfaceView selectively. The default is SurfaceView and it can be changed using
+ * {@link #setViewType(int)} method. Using SurfaceView is recommended in most cases for saving
+ * battery. TextureView might be preferred for supporting various UIs such as animation and
+ * translucency.
+ *
+ * <p>
+ * <em> Differences between {@link VideoView} class : </em>
+ * VideoView2 covers and inherits the most of
+ * VideoView's functionalities. The main differences are
+ * <ul>
+ * <li> VideoView2 inherits FrameLayout and renders videos using SurfaceView and TextureView
+ * selectively while VideoView inherits SurfaceView class.
+ * <li> VideoView2 is integrated with MediaControlView2 and a default MediaControlView2 instance is
+ * attached to VideoView2 by default. If a developer does not want to use the default
+ * MediaControlView2, needs to set enableControlView attribute to false. For instance,
+ * <pre>
+ * &lt;VideoView2
+ * android:id="@+id/video_view"
+ * xmlns:widget="http://schemas.android.com/apk/com.android.media.update"
+ * widget:enableControlView="false" /&gt;
+ * </pre>
+ * If a developer wants to attach a customed MediaControlView2, then set enableControlView attribute
+ * to false and assign the customed media control widget using {@link #setMediaControlView2}.
+ * <li> VideoView2 is integrated with MediaPlayer2 while VideoView is integrated with MediaPlayer.
+ * <li> VideoView2 is integrated with MediaSession2 and so it responses with media key events.
+ * A VideoView2 keeps a MediaSession2 instance internally and connects it to a corresponding
+ * MediaControlView2 instance.
+ * </p>
+ * </ul>
+ *
+ * <p>
+ * <em> Audio focus and audio attributes : </em>
+ * By default, VideoView2 requests audio focus with
+ * {@link AudioManager#AUDIOFOCUS_GAIN}. Use {@link #setAudioFocusRequest(int)} to change this
+ * behavior. The default {@link AudioAttributes} used during playback have a usage of
+ * {@link AudioAttributes#USAGE_MEDIA} and a content type of
+ * {@link AudioAttributes#CONTENT_TYPE_MOVIE}, use {@link #setAudioAttributes(AudioAttributes)} to
+ * modify them.
+ *
+ * <p>
+ * Note: VideoView2 does not retain its full state when going into the background. In particular, it
+ * does not restore the current play state, play position, selected tracks. Applications should save
+ * and restore these on their own in {@link android.app.Activity#onSaveInstanceState} and
+ * {@link android.app.Activity#onRestoreInstanceState}.
+ *
+ * @hide
+ */
+public class VideoView2 extends FrameLayout {
+ /** @hide */
+ @IntDef({
+ VIEW_TYPE_TEXTUREVIEW,
+ VIEW_TYPE_SURFACEVIEW
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ViewType {}
+
+ public static final int VIEW_TYPE_SURFACEVIEW = 1;
+ public static final int VIEW_TYPE_TEXTUREVIEW = 2;
+
+ private final VideoView2Provider mProvider;
+
+ public VideoView2(@NonNull Context context) {
+ this(context, null);
+ }
+
+ public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public VideoView2(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public VideoView2(
+ @NonNull Context context, @Nullable AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+
+ mProvider = ApiLoader.getProvider(context).createVideoView2(this, new SuperProvider(),
+ attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * @hide
+ */
+ public VideoView2Provider getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * Sets MediaControlView2 instance. It will replace the previously assigned MediaControlView2
+ * instance if any.
+ *
+ * @param mediaControlView a media control view2 instance.
+ */
+ public void setMediaControlView2(MediaControlView2 mediaControlView) {
+ mProvider.setMediaControlView2_impl(mediaControlView);
+ }
+
+ /**
+ * Returns MediaControlView2 instance which is currently attached to VideoView2 by default or by
+ * {@link #setMediaControlView2} method.
+ */
+ public MediaControlView2 getMediaControlView2() {
+ return mProvider.getMediaControlView2_impl();
+ }
+
+ /**
+ * Starts playback with the media contents specified by {@link #setVideoURI} and
+ * {@link #setVideoPath}.
+ * If it has been paused, this method will resume playback from the current position.
+ */
+ public void start() {
+ mProvider.start_impl();
+ }
+
+ /**
+ * Pauses playback.
+ */
+ public void pause() {
+ mProvider.pause_impl();
+ }
+
+ /**
+ * Gets the duration of the media content specified by #setVideoURI and #setVideoPath
+ * in milliseconds.
+ */
+ public int getDuration() {
+ return mProvider.getDuration_impl();
+ }
+
+ /**
+ * Gets current playback position in milliseconds.
+ */
+ public int getCurrentPosition() {
+ return mProvider.getCurrentPosition_impl();
+ }
+
+ // TODO: mention about key-frame related behavior.
+ /**
+ * Moves the media by specified time position.
+ * @param msec the offset in milliseconds from the start to seek to.
+ */
+ public void seekTo(int msec) {
+ mProvider.seekTo_impl(msec);
+ }
+
+ /**
+ * Says if the media is currently playing.
+ * @return true if the media is playing, false if it is not (eg. paused or stopped).
+ */
+ public boolean isPlaying() {
+ return mProvider.isPlaying_impl();
+ }
+
+ // TODO: check what will return if it is a local media.
+ /**
+ * Gets the percentage (0-100) of the content that has been buffered or played so far.
+ */
+ public int getBufferPercentage() {
+ return mProvider.getBufferPercentage_impl();
+ }
+
+ /**
+ * Returns the audio session ID.
+ */
+ public int getAudioSessionId() {
+ return mProvider.getAudioSessionId_impl();
+ }
+
+ /**
+ * Starts rendering closed caption or subtitles if there is any. The first subtitle track will
+ * be chosen by default if there multiple subtitle tracks exist.
+ */
+ public void showSubtitle() {
+ mProvider.showSubtitle_impl();
+ }
+
+ /**
+ * Stops showing closed captions or subtitles.
+ */
+ public void hideSubtitle() {
+ mProvider.hideSubtitle_impl();
+ }
+
+ /**
+ * Sets full screen mode.
+ */
+ public void setFullScreen(boolean fullScreen) {
+ mProvider.setFullScreen_impl(fullScreen);
+ }
+
+ // TODO: This should be revised after integration with MediaPlayer2.
+ /**
+ * Sets playback speed.
+ *
+ * It is expressed as a multiplicative factor, where normal speed is 1.0f. If it is less than
+ * or equal to zero, it will be just ignored and nothing will be changed. If it exceeds the
+ * maximum speed that internal engine supports, system will determine best handling or it will
+ * be reset to the normal speed 1.0f.
+ * @param speed the playback speed. It should be positive.
+ */
+ public void setSpeed(float speed) {
+ mProvider.setSpeed_impl(speed);
+ }
+
+ /**
+ * Returns current speed setting.
+ *
+ * If setSpeed() has never been called, returns the default value 1.0f.
+ * @return current speed setting
+ */
+ public float getSpeed() {
+ return mProvider.getSpeed_impl();
+ }
+
+ /**
+ * Sets which type of audio focus will be requested during the playback, or configures playback
+ * to not request audio focus. Valid values for focus requests are
+ * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+ * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+ * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+ * requested when playback starts. You can for instance use this when playing a silent animation
+ * through this class, and you don't want to affect other audio applications playing in the
+ * background.
+ *
+ * @param focusGain the type of audio focus gain that will be requested, or
+ * {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during
+ * playback.
+ */
+ public void setAudioFocusRequest(int focusGain) {
+ mProvider.setAudioFocusRequest_impl(focusGain);
+ }
+
+ /**
+ * Sets the {@link AudioAttributes} to be used during the playback of the video.
+ *
+ * @param attributes non-null <code>AudioAttributes</code>.
+ */
+ public void setAudioAttributes(@NonNull AudioAttributes attributes) {
+ mProvider.setAudioAttributes_impl(attributes);
+ }
+
+ /**
+ * Sets a remote player for handling playback of the selected route from MediaControlView2.
+ * If this is not called, MediaCotrolView2 will not show the route button.
+ *
+ * @param routeCategories the list of media control categories in
+ * {@link android.support.v7.media.MediaControlIntent}
+ * @param player the player to handle the selected route. If null, a default
+ * route player will be used.
+ * @throws IllegalStateException if MediaControlView2 is not set.
+ */
+ public void setRouteAttributes(@NonNull List<String> routeCategories,
+ @Nullable MediaPlayerBase player) {
+ mProvider.setRouteAttributes_impl(routeCategories, player);
+ }
+
+ /**
+ * Sets video path.
+ *
+ * @param path the path of the video.
+ */
+ public void setVideoPath(String path) {
+ mProvider.setVideoPath_impl(path);
+ }
+
+ /**
+ * Sets video URI.
+ *
+ * @param uri the URI of the video.
+ */
+ public void setVideoURI(Uri uri) {
+ mProvider.setVideoURI_impl(uri);
+ }
+
+ /**
+ * Sets video URI using specific headers.
+ *
+ * @param uri the URI of the video.
+ * @param headers the headers for the URI request.
+ * Note that the cross domain redirection is allowed by default, but that can be
+ * changed with key/value pairs through the headers parameter with
+ * "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+ * to disallow or allow cross domain redirection.
+ */
+ public void setVideoURI(Uri uri, Map<String, String> headers) {
+ mProvider.setVideoURI_impl(uri, headers);
+ }
+
+ /**
+ * Selects which view will be used to render video between SurfacView and TextureView.
+ *
+ * @param viewType the view type to render video
+ * <ul>
+ * <li>{@link #VIEW_TYPE_SURFACEVIEW}
+ * <li>{@link #VIEW_TYPE_TEXTUREVIEW}
+ * </ul>
+ */
+ public void setViewType(@ViewType int viewType) {
+ mProvider.setViewType_impl(viewType);
+ }
+
+ /**
+ * Returns view type.
+ *
+ * @return view type. See {@see setViewType}.
+ */
+ @ViewType
+ public int getViewType() {
+ return mProvider.getViewType_impl();
+ }
+
+ /**
+ * Stops playback and release all the resources. This should be called whenever a VideoView2
+ * instance is no longer to be used.
+ */
+ public void stopPlayback() {
+ mProvider.stopPlayback_impl();
+ }
+
+ /**
+ * Registers a callback to be invoked when the media file is loaded and ready to go.
+ *
+ * @param l the callback that will be run.
+ */
+ public void setOnPreparedListener(OnPreparedListener l) {
+ mProvider.setOnPreparedListener_impl(l);
+ }
+
+ /**
+ * Registers a callback to be invoked when the end of a media file has been reached during
+ * playback.
+ *
+ * @param l the callback that will be run.
+ */
+ public void setOnCompletionListener(OnCompletionListener l) {
+ mProvider.setOnCompletionListener_impl(l);
+ }
+
+ /**
+ * Registers a callback to be invoked when an error occurs during playback or setup. If no
+ * listener is specified, or if the listener returned false, VideoView2 will inform the user of
+ * any errors.
+ *
+ * @param l The callback that will be run
+ */
+ public void setOnErrorListener(OnErrorListener l) {
+ mProvider.setOnErrorListener_impl(l);
+ }
+
+ /**
+ * Registers a callback to be invoked when an informational event occurs during playback or
+ * setup.
+ *
+ * @param l The callback that will be run
+ */
+ public void setOnInfoListener(OnInfoListener l) {
+ mProvider.setOnInfoListener_impl(l);
+ }
+
+ /**
+ * Registers a callback to be invoked when a view type change is done.
+ * {@see #setViewType(int)}
+ * @param l The callback that will be run
+ */
+ public void setOnViewTypeChangedListener(OnViewTypeChangedListener l) {
+ mProvider.setOnViewTypeChangedListener_impl(l);
+ }
+
+ /**
+ * Registers a callback to be invoked when the fullscreen mode should be changed.
+ */
+ public void setFullScreenChangedListener(OnFullScreenChangedListener l) {
+ mProvider.setFullScreenChangedListener_impl(l);
+ }
+
+ /**
+ * Interface definition of a callback to be invoked when the viw type has been changed.
+ */
+ public interface OnViewTypeChangedListener {
+ /**
+ * Called when the view type has been changed.
+ * @see #setViewType(int)
+ * @param viewType
+ * <ul>
+ * <li>{@link #VIEW_TYPE_SURFACEVIEW}
+ * <li>{@link #VIEW_TYPE_TEXTUREVIEW}
+ * </ul>
+ */
+ void onViewTypeChanged(@ViewType int viewType);
+ }
+
+ /**
+ * Interface definition of a callback to be invoked when the media source is ready for playback.
+ */
+ public interface OnPreparedListener {
+ /**
+ * Called when the media file is ready for playback.
+ */
+ void onPrepared();
+ }
+
+ /**
+ * Interface definition for a callback to be invoked when playback of a media source has
+ * completed.
+ */
+ public interface OnCompletionListener {
+ /**
+ * Called when the end of a media source is reached during playback.
+ */
+ void onCompletion();
+ }
+
+ /**
+ * Interface definition of a callback to be invoked when there has been an error during an
+ * asynchronous operation.
+ */
+ public interface OnErrorListener {
+ // TODO: Redefine error codes.
+ /**
+ * Called to indicate an error.
+ * @param what the type of error that has occurred
+ * @param extra an extra code, specific to the error.
+ * @return true if the method handled the error, false if it didn't.
+ * @see MediaPlayer#OnErrorListener
+ */
+ boolean onError(int what, int extra);
+ }
+
+ /**
+ * Interface definition of a callback to be invoked to communicate some info and/or warning
+ * about the media or its playback.
+ */
+ public interface OnInfoListener {
+ /**
+ * Called to indicate an info or a warning.
+ * @param what the type of info or warning.
+ * @param extra an extra code, specific to the info.
+ *
+ * @see MediaPlayer#OnInfoListener
+ */
+ void onInfo(int what, int extra);
+ }
+
+ /**
+ * Interface definition of a callback to be invoked to inform the fullscreen mode is changed.
+ */
+ public interface OnFullScreenChangedListener {
+ /**
+ * Called to indicate a fullscreen mode change.
+ */
+ void onFullScreenChanged(boolean fullScreen);
+ }
+
+ @Override
+ protected void onAttachedToWindow() {
+ mProvider.onAttachedToWindow_impl();
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ mProvider.onDetachedFromWindow_impl();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return mProvider.getAccessibilityClassName_impl();
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ return mProvider.onTouchEvent_impl(ev);
+ }
+
+ @Override
+ public boolean onTrackballEvent(MotionEvent ev) {
+ return mProvider.onTrackballEvent_impl(ev);
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ return mProvider.onKeyDown_impl(keyCode, event);
+ }
+
+ @Override
+ public void onFinishInflate() {
+ mProvider.onFinishInflate_impl();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent(KeyEvent event) {
+ return mProvider.dispatchKeyEvent_impl(event);
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ mProvider.setEnabled_impl(enabled);
+ }
+
+ private class SuperProvider implements ViewProvider {
+ @Override
+ public void onAttachedToWindow_impl() {
+ VideoView2.super.onAttachedToWindow();
+ }
+
+ @Override
+ public void onDetachedFromWindow_impl() {
+ VideoView2.super.onDetachedFromWindow();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName_impl() {
+ return VideoView2.super.getAccessibilityClassName();
+ }
+
+ @Override
+ public boolean onTouchEvent_impl(MotionEvent ev) {
+ return VideoView2.super.onTouchEvent(ev);
+ }
+
+ @Override
+ public boolean onTrackballEvent_impl(MotionEvent ev) {
+ return VideoView2.super.onTrackballEvent(ev);
+ }
+
+ @Override
+ public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
+ return VideoView2.super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public void onFinishInflate_impl() {
+ VideoView2.super.onFinishInflate();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent_impl(KeyEvent event) {
+ return VideoView2.super.dispatchKeyEvent(event);
+ }
+
+ @Override
+ public void setEnabled_impl(boolean enabled) {
+ VideoView2.super.setEnabled(enabled);
+ }
+ }
+}
diff --git a/androidx/app/slice/Slice.java b/androidx/app/slice/Slice.java
index 3c5f1bf6..d0137b1d 100644
--- a/androidx/app/slice/Slice.java
+++ b/androidx/app/slice/Slice.java
@@ -54,7 +54,6 @@ 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;
@@ -308,7 +307,9 @@ public final class Slice {
* Add remote input to the slice being constructed
* @param subType Optional template-specific type information
* @see {@link SliceItem#getSubType()}
+ * @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public Slice.Builder addRemoteInput(RemoteInput remoteInput, @Nullable String subType,
@SliceHint List<String> hints) {
return addRemoteInput(remoteInput, subType, hints.toArray(new String[hints.size()]));
@@ -318,7 +319,9 @@ public final class Slice {
* Add remote input to the slice being constructed
* @param subType Optional template-specific type information
* @see {@link SliceItem#getSubType()}
+ * @hide
*/
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public Slice.Builder addRemoteInput(RemoteInput remoteInput, @Nullable String subType,
@SliceHint String... hints) {
mItems.add(new SliceItem(remoteInput, FORMAT_REMOTE_INPUT, subType, hints));
@@ -395,7 +398,11 @@ public final class Slice {
return toString("");
}
- private String toString(String indent) {
+ /**
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY)
+ public String toString(String indent) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < mItems.length; i++) {
sb.append(indent);
@@ -418,20 +425,34 @@ public final class Slice {
}
/**
+ */
+ public static @Nullable Slice bindSlice(Context context, @NonNull Uri uri) {
+ throw new RuntimeException("Stub, to be removed");
+ }
+
+ /**
+ */
+ public static @Nullable Slice bindSlice(Context context, @NonNull Intent intent) {
+ throw new RuntimeException("Stub, to be removed");
+ }
+
+ /**
* Turns a slice Uri into slice content.
*
+ * @hide
* @param context Context to be used.
* @param uri The URI to a slice provider
* @return The Slice provided by the app or null if none is given.
* @see Slice
*/
+ @RestrictTo(Scope.LIBRARY_GROUP)
@SuppressWarnings("NewApi")
- public static @Nullable Slice bindSlice(Context context, @NonNull Uri uri) {
- // TODO: Hide this and only allow binding through SliceView.
+ public static @Nullable Slice bindSlice(Context context, @NonNull Uri uri,
+ List<SliceSpec> supportedSpecs) {
if (BuildCompat.isAtLeastP()) {
- return callBindSlice(context, uri, Collections.<SliceSpec>emptyList());
+ return callBindSlice(context, uri, supportedSpecs);
} else {
- return SliceProviderCompat.bindSlice(context, uri, Collections.<SliceSpec>emptyList());
+ return SliceProviderCompat.bindSlice(context, uri, supportedSpecs);
}
}
@@ -448,6 +469,7 @@ public final class Slice {
* {@link ContentProvider} associated with the given intent this will throw
* {@link IllegalArgumentException}.
*
+ * @hide
* @param context The context to use.
* @param intent The intent associated with a slice.
* @return The Slice provided by the app or null if none is given.
@@ -455,14 +477,14 @@ public final class Slice {
* @see SliceProvider#onMapIntentToUri(Intent)
* @see Intent
*/
+ @RestrictTo(Scope.LIBRARY_GROUP)
@SuppressWarnings("NewApi")
- public static @Nullable Slice bindSlice(Context context, @NonNull Intent intent) {
- // TODO: Hide this and only allow binding through SliceView.
+ public static @Nullable Slice bindSlice(Context context, @NonNull Intent intent,
+ List<SliceSpec> supportedSpecs) {
if (BuildCompat.isAtLeastP()) {
- return callBindSlice(context, intent, Collections.<SliceSpec>emptyList());
+ return callBindSlice(context, intent, supportedSpecs);
} else {
- return SliceProviderCompat.bindSlice(context, intent,
- Collections.<SliceSpec>emptyList());
+ return SliceProviderCompat.bindSlice(context, intent, supportedSpecs);
}
}
diff --git a/androidx/app/slice/SliceItem.java b/androidx/app/slice/SliceItem.java
index 3d58f3b5..e7d27294 100644
--- a/androidx/app/slice/SliceItem.java
+++ b/androidx/app/slice/SliceItem.java
@@ -52,7 +52,6 @@ import java.util.List;
* <li>{@link android.app.slice.SliceItem#FORMAT_ACTION}</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>
* 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
@@ -100,6 +99,15 @@ public class SliceItem {
* @hide
*/
@RestrictTo(Scope.LIBRARY)
+ public SliceItem(Object obj, @SliceType String format, String subType,
+ @Slice.SliceHint List<String> hints) {
+ this (obj, format, subType, hints.toArray(new String[hints.size()]));
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(Scope.LIBRARY)
public SliceItem(PendingIntent intent, Slice slice, String format, String subType,
@Slice.SliceHint String[] hints) {
this(new Pair<>(intent, slice), format, subType, hints);
@@ -186,8 +194,10 @@ public class SliceItem {
/**
* @return The remote input held by this {@link android.app.slice.SliceItem#FORMAT_REMOTE_INPUT}
* SliceItem
+ * @hide
*/
@RequiresApi(20)
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public RemoteInput getRemoteInput() {
return (RemoteInput) mObj;
}
@@ -348,4 +358,34 @@ public class SliceItem {
}
return "Unrecognized format: " + format;
}
+
+ /**
+ * @hide
+ * @return A string representation of this slice item.
+ */
+ @RestrictTo(Scope.LIBRARY)
+ @Override
+ public String toString() {
+ return toString("");
+ }
+
+ private String toString(String indent) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(indent);
+ if (FORMAT_SLICE.equals(mFormat)) {
+ sb.append("slice:\n");
+ sb.append(getSlice().toString(indent + " "));
+ } else if (FORMAT_ACTION.equals(mFormat)) {
+ sb.append("action:\n");
+ sb.append(getSlice().toString(indent + " "));
+ } else if (FORMAT_TEXT.equals(mFormat)) {
+ sb.append("text: ");
+ sb.append(getText());
+ sb.append("\n");
+ } else {
+ sb.append(SliceItem.typeToString(getFormat()));
+ sb.append("\n");
+ }
+ return sb.toString();
+ }
}
diff --git a/androidx/app/slice/SliceManager.java b/androidx/app/slice/SliceManager.java
new file mode 100644
index 00000000..deea908e
--- /dev/null
+++ b/androidx/app/slice/SliceManager.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright 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 androidx.app.slice;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v4.os.BuildCompat;
+
+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.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public abstract class SliceManager {
+
+ /**
+ * Get a {@link SliceManager}.
+ */
+ @SuppressWarnings("NewApi")
+ public static @NonNull SliceManager get(@NonNull Context context) {
+ if (BuildCompat.isAtLeastP()) {
+ return new SliceManagerWrapper(context);
+ } else {
+ return new SliceManagerCompat(context);
+ }
+ }
+
+ /**
+ * 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.
+ * @see SliceProvider#onSlicePinned(Uri)
+ */
+ public abstract void registerSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback);
+
+ /**
+ * 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.
+ * @see SliceProvider#onSlicePinned(Uri)
+ */
+ public abstract void registerSliceCallback(@NonNull Uri uri, @NonNull Executor executor,
+ @NonNull SliceCallback callback);
+
+ /**
+ * 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 abstract void unregisterSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback);
+
+ /**
+ * 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.
+ * @see SliceProvider#onSlicePinned(Uri)
+ */
+ public abstract void pinSlice(@NonNull Uri uri);
+
+ /**
+ * 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 abstract void unpinSlice(@NonNull Uri uri);
+
+ /**
+ * 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.
+ * @hide
+ * @see SliceSpec
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public abstract @NonNull List<SliceSpec> getPinnedSpecs(@NonNull Uri uri);
+
+ /**
+ * Turns a slice Uri into slice content.
+ *
+ * @param uri The URI to a slice provider
+ * @return The Slice provided by the app or null if none is given.
+ * @see Slice
+ */
+ public abstract @Nullable Slice bindSlice(@NonNull Uri uri);
+
+ /**
+ * Turns a slice intent into slice content. Expects an explicit intent. If there is no
+ * {@link android.content.ContentProvider} associated with the given intent this will throw
+ * {@link IllegalArgumentException}.
+ *
+ * @param intent The intent associated with a slice.
+ * @return The Slice provided by the app or null if none is given.
+ * @see Slice
+ * @see androidx.app.slice.SliceProvider#onMapIntentToUri(Intent)
+ * @see Intent
+ */
+ public abstract @Nullable Slice bindSlice(@NonNull Intent intent);
+
+ /**
+ * 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(@NonNull Slice s);
+ }
+}
diff --git a/androidx/app/slice/SliceManagerCompat.java b/androidx/app/slice/SliceManagerCompat.java
new file mode 100644
index 00000000..67613a58
--- /dev/null
+++ b/androidx/app/slice/SliceManagerCompat.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 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 androidx.app.slice;
+
+import static androidx.app.slice.widget.SliceLiveData.SUPPORTED_SPECS;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.Looper;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.util.ArrayMap;
+import android.util.Pair;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import androidx.app.slice.compat.SliceProviderCompat;
+import androidx.app.slice.widget.SliceLiveData;
+
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+class SliceManagerCompat extends SliceManager {
+ private final ArrayMap<Pair<Uri, SliceCallback>, SliceListenerImpl> mListenerLookup =
+ new ArrayMap<>();
+ private final Context mContext;
+
+ SliceManagerCompat(Context context) {
+ mContext = context;
+ }
+
+ @Override
+ public void registerSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback) {
+ final Handler h = new Handler(Looper.getMainLooper());
+ registerSliceCallback(uri, new Executor() {
+ @Override
+ public void execute(@NonNull Runnable command) {
+ h.post(command);
+ }
+ }, callback);
+ }
+
+ @Override
+ public void registerSliceCallback(@NonNull Uri uri, @NonNull Executor executor,
+ @NonNull SliceCallback callback) {
+ pinSlice(uri);
+ getListener(uri, callback, new SliceListenerImpl(uri, executor, callback)).startListening();
+ }
+
+ @Override
+ public void unregisterSliceCallback(@NonNull Uri uri, @NonNull SliceCallback callback) {
+ unpinSlice(uri);
+ SliceListenerImpl impl = mListenerLookup.remove(new Pair<>(uri, callback));
+ if (impl != null) impl.stopListening();
+ }
+
+ @Override
+ public void pinSlice(@NonNull Uri uri) {
+ SliceProviderCompat.pinSlice(mContext, uri, SliceLiveData.SUPPORTED_SPECS);
+ }
+
+ @Override
+ public void unpinSlice(@NonNull Uri uri) {
+ SliceProviderCompat.unpinSlice(mContext, uri, SliceLiveData.SUPPORTED_SPECS);
+ }
+
+ @Override
+ public @NonNull List<SliceSpec> getPinnedSpecs(@NonNull Uri uri) {
+ return SliceProviderCompat.getPinnedSpecs(mContext, uri);
+ }
+
+ @Nullable
+ @Override
+ public Slice bindSlice(@NonNull Uri uri) {
+ return SliceProviderCompat.bindSlice(mContext, uri, SUPPORTED_SPECS);
+ }
+
+ @Nullable
+ @Override
+ public Slice bindSlice(@NonNull Intent intent) {
+ return SliceProviderCompat.bindSlice(mContext, intent, SUPPORTED_SPECS);
+ }
+
+ private SliceListenerImpl getListener(Uri uri, SliceCallback callback,
+ SliceListenerImpl listener) {
+ Pair<Uri, SliceCallback> key = new Pair<>(uri, callback);
+ if (mListenerLookup.containsKey(key)) {
+ mListenerLookup.get(key).stopListening();
+ }
+ mListenerLookup.put(key, listener);
+ return listener;
+ }
+
+ private class SliceListenerImpl {
+
+ private Uri mUri;
+ private final Executor mExecutor;
+ private final SliceCallback mCallback;
+
+ SliceListenerImpl(Uri uri, Executor executor, SliceCallback callback) {
+ mUri = uri;
+ mExecutor = executor;
+ mCallback = callback;
+ }
+
+ void startListening() {
+ mContext.getContentResolver().registerContentObserver(mUri, true, mObserver);
+ }
+
+ void stopListening() {
+ mContext.getContentResolver().unregisterContentObserver(mObserver);
+ }
+
+ private final Runnable mUpdateSlice = new Runnable() {
+ @Override
+ public void run() {
+ final Slice s = Slice.bindSlice(mContext, mUri, SUPPORTED_SPECS);
+ mExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ mCallback.onSliceUpdated(s);
+ }
+ });
+ }
+ };
+
+ private final ContentObserver mObserver = new ContentObserver(
+ new Handler(Looper.getMainLooper())) {
+ @Override
+ public void onChange(boolean selfChange) {
+ AsyncTask.execute(mUpdateSlice);
+ }
+ };
+ }
+}
diff --git a/androidx/app/slice/SliceManagerTest.java b/androidx/app/slice/SliceManagerTest.java
new file mode 100644
index 00000000..53f092d2
--- /dev/null
+++ b/androidx/app/slice/SliceManagerTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright 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 androidx.app.slice;
+
+import static junit.framework.Assert.assertEquals;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.v4.os.BuildCompat;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+import androidx.app.slice.widget.SliceLiveData;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class SliceManagerTest {
+
+ private final Context mContext = InstrumentationRegistry.getContext();
+ private SliceProvider mSliceProvider;
+ private SliceManager mManager;
+
+ @Before
+ public void setup() {
+ TestSliceProvider.sSliceProviderReceiver = mSliceProvider = mock(SliceProvider.class);
+ mManager = createSliceManager(mContext);
+ }
+
+ private SliceManager createSliceManager(Context context) {
+ if (BuildCompat.isAtLeastP()) {
+ android.app.slice.SliceManager manager = mock(android.app.slice.SliceManager.class);
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ TestSliceProvider.sSliceProviderReceiver.onSlicePinned(
+ (Uri) invocation.getArguments()[0]);
+ return null;
+ }
+ }).when(manager).pinSlice(any(Uri.class), any(List.class));
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) throws Throwable {
+ TestSliceProvider.sSliceProviderReceiver.onSliceUnpinned(
+ (Uri) invocation.getArguments()[0]);
+ return null;
+ }
+ }).when(manager).unpinSlice(any(Uri.class));
+ return new SliceManagerWrapper(context, manager);
+ } else {
+ return SliceManager.get(context);
+ }
+ }
+
+ @Test
+ public void testPin() {
+ Uri uri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(mContext.getPackageName())
+ .build();
+ mManager.pinSlice(uri);
+ verify(mSliceProvider).onSlicePinned(eq(uri));
+ }
+
+ @Test
+ public void testUnpin() {
+ Uri uri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(mContext.getPackageName())
+ .build();
+ mManager.pinSlice(uri);
+ clearInvocations(mSliceProvider);
+ mManager.unpinSlice(uri);
+ verify(mSliceProvider).onSliceUnpinned(eq(uri));
+ }
+
+ @Test
+ public void testCallback() {
+ if (BuildCompat.isAtLeastP()) {
+ return;
+ }
+ Uri uri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(mContext.getPackageName())
+ .build();
+ Slice s = new Slice.Builder(uri).build();
+ SliceManager.SliceCallback callback = mock(SliceManager.SliceCallback.class);
+ when(mSliceProvider.onBindSlice(eq(uri))).thenReturn(s);
+ mManager.registerSliceCallback(uri, new Executor() {
+ @Override
+ public void execute(@NonNull Runnable command) {
+ command.run();
+ }
+ }, callback);
+
+ mContext.getContentResolver().notifyChange(uri, null);
+
+ verify(callback, timeout(2000)).onSliceUpdated(any(Slice.class));
+ }
+
+ @Test
+ public void testPinnedSpecs() {
+ if (BuildCompat.isAtLeastP()) {
+ return;
+ }
+ Uri uri = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(mContext.getPackageName())
+ .build();
+ mManager.pinSlice(uri);
+ verify(mSliceProvider).onSlicePinned(eq(uri));
+
+ assertEquals(SliceLiveData.SUPPORTED_SPECS, mManager.getPinnedSpecs(uri));
+ }
+
+ public static class TestSliceProvider extends SliceProvider {
+
+ public static SliceProvider sSliceProviderReceiver;
+
+ @Override
+ public boolean onCreateSliceProvider() {
+ if (sSliceProviderReceiver != null) {
+ sSliceProviderReceiver.onCreateSliceProvider();
+ }
+ return true;
+ }
+
+ @Override
+ public Slice onBindSlice(Uri sliceUri) {
+ if (sSliceProviderReceiver != null) {
+ return sSliceProviderReceiver.onBindSlice(sliceUri);
+ }
+ return null;
+ }
+
+ @NonNull
+ @Override
+ public Uri onMapIntentToUri(Intent intent) {
+ if (sSliceProviderReceiver != null) {
+ return sSliceProviderReceiver.onMapIntentToUri(intent);
+ }
+ return null;
+ }
+
+ @Override
+ public void onSlicePinned(Uri sliceUri) {
+ if (sSliceProviderReceiver != null) {
+ sSliceProviderReceiver.onSlicePinned(sliceUri);
+ }
+ }
+
+ @Override
+ public void onSliceUnpinned(Uri sliceUri) {
+ if (sSliceProviderReceiver != null) {
+ sSliceProviderReceiver.onSliceUnpinned(sliceUri);
+ }
+ }
+ }
+}
diff --git a/androidx/app/slice/SliceManagerWrapper.java b/androidx/app/slice/SliceManagerWrapper.java
new file mode 100644
index 00000000..8b2265cd
--- /dev/null
+++ b/androidx/app/slice/SliceManagerWrapper.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 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 androidx.app.slice;
+
+import static androidx.app.slice.SliceConvert.unwrap;
+import static androidx.app.slice.widget.SliceLiveData.SUPPORTED_SPECS;
+
+import android.app.slice.Slice;
+import android.app.slice.SliceSpec;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RequiresApi;
+import android.support.annotation.RestrictTo;
+
+import java.util.List;
+import java.util.WeakHashMap;
+import java.util.concurrent.Executor;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@RequiresApi(api = 28)
+class SliceManagerWrapper extends SliceManager {
+ private final android.app.slice.SliceManager mManager;
+ private final WeakHashMap<SliceCallback, android.app.slice.SliceManager.SliceCallback>
+ mCallbacks = new WeakHashMap<>();
+ private final List<SliceSpec> mSpecs;
+ private final Context mContext;
+
+ SliceManagerWrapper(Context context) {
+ this(context, context.getSystemService(android.app.slice.SliceManager.class));
+ }
+
+ SliceManagerWrapper(Context context, android.app.slice.SliceManager manager) {
+ mContext = context;
+ mManager = manager;
+ mSpecs = unwrap(SUPPORTED_SPECS);
+ }
+
+ @Override
+ public void registerSliceCallback(@NonNull Uri uri,
+ @NonNull SliceCallback callback) {
+ mManager.registerSliceCallback(uri, addCallback(callback), mSpecs);
+ }
+
+ @Override
+ public void registerSliceCallback(@NonNull Uri uri, @NonNull Executor executor,
+ @NonNull SliceCallback callback) {
+ mManager.registerSliceCallback(uri, addCallback(callback), mSpecs, executor);
+ }
+
+ @Override
+ public void unregisterSliceCallback(@NonNull Uri uri,
+ @NonNull SliceCallback callback) {
+ mManager.unregisterSliceCallback(uri, mCallbacks.get(callback));
+ }
+
+ @Override
+ public void pinSlice(@NonNull Uri uri) {
+ mManager.pinSlice(uri, mSpecs);
+ }
+
+ @Override
+ public void unpinSlice(@NonNull Uri uri) {
+ mManager.unpinSlice(uri);
+ }
+
+ @Override
+ public @NonNull List<androidx.app.slice.SliceSpec> getPinnedSpecs(@NonNull Uri uri) {
+ return SliceConvert.wrap(mManager.getPinnedSpecs(uri));
+ }
+
+ @Nullable
+ @Override
+ public androidx.app.slice.Slice bindSlice(@NonNull Uri uri) {
+ return SliceConvert.wrap(android.app.slice.Slice.bindSlice(
+ mContext.getContentResolver(), uri, unwrap(SUPPORTED_SPECS)));
+ }
+
+ @Nullable
+ @Override
+ public androidx.app.slice.Slice bindSlice(@NonNull Intent intent) {
+ return SliceConvert.wrap(android.app.slice.Slice.bindSlice(
+ mContext, intent, unwrap(SUPPORTED_SPECS)));
+ }
+
+ private android.app.slice.SliceManager.SliceCallback addCallback(final SliceCallback callback) {
+ android.app.slice.SliceManager.SliceCallback ret = mCallbacks.get(callback);
+ if (ret == null) {
+ ret = new android.app.slice.SliceManager.SliceCallback() {
+ @Override
+ public void onSliceUpdated(Slice s) {
+ callback.onSliceUpdated(SliceConvert.wrap(s));
+ }
+ };
+ mCallbacks.put(callback, ret);
+ }
+ return ret;
+ }
+}
diff --git a/androidx/app/slice/SliceProvider.java b/androidx/app/slice/SliceProvider.java
index 8ec2dbef..ef2af9d1 100644
--- a/androidx/app/slice/SliceProvider.java
+++ b/androidx/app/slice/SliceProvider.java
@@ -31,7 +31,7 @@ import java.util.List;
import androidx.app.slice.compat.ContentProviderWrapper;
import androidx.app.slice.compat.SliceProviderCompat;
-import androidx.app.slice.compat.SliceProviderWrapper;
+import androidx.app.slice.compat.SliceProviderWrapperContainer;
/**
* A SliceProvider allows an app to provide content to be displayed in system spaces. This content
@@ -80,7 +80,7 @@ public abstract class SliceProvider extends ContentProviderWrapper {
public void attachInfo(Context context, ProviderInfo info) {
ContentProvider impl;
if (BuildCompat.isAtLeastP()) {
- impl = new SliceProviderWrapper(this);
+ impl = new SliceProviderWrapperContainer.SliceProviderWrapper(this);
} else {
impl = new SliceProviderCompat(this);
}
@@ -121,6 +121,42 @@ public abstract class SliceProvider extends ContentProviderWrapper {
public abstract Slice onBindSlice(Uri sliceUri);
/**
+ * 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)
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ 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)
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ 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}.
diff --git a/androidx/app/slice/SliceSpecs.java b/androidx/app/slice/SliceSpecs.java
new file mode 100644
index 00000000..ed4658d2
--- /dev/null
+++ b/androidx/app/slice/SliceSpecs.java
@@ -0,0 +1,51 @@
+/*
+ * 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.support.annotation.RestrictTo;
+
+/**
+ * Constants for each of the slice specs
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class SliceSpecs {
+
+ /**
+ * Most basic slice, only has icon, title, and summary.
+ */
+ public static final SliceSpec BASIC = new SliceSpec("androidx.app.slice.BASIC", 1);
+
+ /**
+ * List of rows, each row has start/end items, title, summary.
+ * Also supports grid rows.
+ */
+ public static final SliceSpec LIST = new SliceSpec("androidx.app.slice.LIST", 1);
+
+ /**
+ * Messaging template. Each message contains a timestamp and a message, it optionally contains
+ * a source of where the message came from.
+ */
+ public static final SliceSpec MESSAGING = new SliceSpec("androidx.app.slice.MESSAGING", 1);
+
+ /**
+ * Grid template.
+ * Lists can contain grids, so use the same spec for both. Grid needs a spec to use because
+ * it can be a top level builder.
+ */
+ public static final SliceSpec GRID = LIST;
+}
diff --git a/androidx/app/slice/SliceTest.java b/androidx/app/slice/SliceTest.java
index 350c1770..0ede29dc 100644
--- a/androidx/app/slice/SliceTest.java
+++ b/androidx/app/slice/SliceTest.java
@@ -48,6 +48,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Arrays;
+import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -66,7 +67,8 @@ public class SliceTest {
public void testProcess() {
sFlag = false;
Slice.bindSlice(mContext,
- BASE_URI.buildUpon().appendPath("set_flag").build());
+ BASE_URI.buildUpon().appendPath("set_flag").build(),
+ Collections.<SliceSpec>emptyList());
assertFalse(sFlag);
}
@@ -77,14 +79,14 @@ public class SliceTest {
@Test
public void testSliceUri() {
- Slice s = Slice.bindSlice(mContext, BASE_URI);
+ Slice s = Slice.bindSlice(mContext, BASE_URI, Collections.<SliceSpec>emptyList());
assertEquals(BASE_URI, s.getUri());
}
@Test
public void testSubSlice() {
Uri uri = BASE_URI.buildUpon().appendPath("subslice").build();
- Slice s = Slice.bindSlice(mContext, uri);
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
assertEquals(uri, s.getUri());
assertEquals(1, s.getItems().size());
@@ -99,7 +101,7 @@ public class SliceTest {
@Test
public void testText() {
Uri uri = BASE_URI.buildUpon().appendPath("text").build();
- Slice s = Slice.bindSlice(mContext, uri);
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
assertEquals(uri, s.getUri());
assertEquals(1, s.getItems().size());
@@ -112,7 +114,7 @@ public class SliceTest {
@Test
public void testIcon() {
Uri uri = BASE_URI.buildUpon().appendPath("icon").build();
- Slice s = Slice.bindSlice(mContext, uri);
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
assertEquals(uri, s.getUri());
assertEquals(1, s.getItems().size());
@@ -136,7 +138,7 @@ public class SliceTest {
mContext.registerReceiver(receiver,
new IntentFilter(mContext.getPackageName() + ".action"));
Uri uri = BASE_URI.buildUpon().appendPath("action").build();
- Slice s = Slice.bindSlice(mContext, uri);
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
assertEquals(uri, s.getUri());
assertEquals(1, s.getItems().size());
@@ -159,7 +161,7 @@ public class SliceTest {
@Test
public void testInt() {
Uri uri = BASE_URI.buildUpon().appendPath("int").build();
- Slice s = Slice.bindSlice(mContext, uri);
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
assertEquals(uri, s.getUri());
assertEquals(1, s.getItems().size());
@@ -171,7 +173,7 @@ public class SliceTest {
@Test
public void testTimestamp() {
Uri uri = BASE_URI.buildUpon().appendPath("timestamp").build();
- Slice s = Slice.bindSlice(mContext, uri);
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
assertEquals(uri, s.getUri());
assertEquals(1, s.getItems().size());
@@ -185,7 +187,7 @@ public class SliceTest {
// 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);
+ Slice s = Slice.bindSlice(mContext, uri, Collections.<SliceSpec>emptyList());
assertEquals(uri, s.getUri());
assertEquals(Arrays.asList(HINT_LIST), s.getHints());
diff --git a/androidx/app/slice/builders/GridBuilder.java b/androidx/app/slice/builders/GridBuilder.java
index f51b0263..32f8a692 100644
--- a/androidx/app/slice/builders/GridBuilder.java
+++ b/androidx/app/slice/builders/GridBuilder.java
@@ -16,13 +16,11 @@
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.content.Context;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
@@ -33,6 +31,10 @@ import android.support.annotation.RestrictTo;
import java.util.function.Consumer;
import androidx.app.slice.Slice;
+import androidx.app.slice.SliceSpecs;
+import androidx.app.slice.builders.impl.GridBuilderBasicImpl;
+import androidx.app.slice.builders.impl.GridBuilderListV1Impl;
+import androidx.app.slice.builders.impl.TemplateBuilderImpl;
/**
* Builder to construct a row of slice content in a grid format.
@@ -43,36 +45,65 @@ import androidx.app.slice.Slice;
*/
public class GridBuilder extends TemplateSliceBuilder {
+ private androidx.app.slice.builders.impl.GridBuilder mImpl;
+
/**
* Create a builder which will construct a slice displayed in a grid format.
* @param uri Uri to tag for this slice.
+ * @hide
*/
- public GridBuilder(@NonNull Uri uri) {
- super(new Slice.Builder(uri));
+ @RestrictTo(LIBRARY_GROUP)
+ public GridBuilder(@NonNull Context context, @NonNull Uri uri) {
+ super(new Slice.Builder(uri), context);
}
/**
* Create a builder which will construct a slice displayed in a grid format.
* @param parent The builder constructing the parent slice.
+ * @hide
*/
- public GridBuilder(@NonNull TemplateSliceBuilder parent) {
- super(new Slice.Builder(parent.getBuilder()));
+ @RestrictTo(LIBRARY_GROUP)
+ public GridBuilder(@NonNull ListBuilder parent) {
+ super(parent.getImpl().createGridBuilder());
}
/**
- * @hide
*/
- @RestrictTo(LIBRARY_GROUP)
- @Override
- public void apply(Slice.Builder builder) {
+ public GridBuilder(@NonNull Uri uri) {
+ super(uri);
+ throw new RuntimeException("Stub, to be removed");
+ }
+
+ /**
+ */
+ public GridBuilder(@NonNull TemplateSliceBuilder z) {
+ super((Uri) null);
+ throw new RuntimeException("Stub, to be removed");
}
@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();
+ return mImpl.buildIndividual();
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ @Override
+ protected TemplateBuilderImpl selectImpl() {
+ if (checkCompatible(SliceSpecs.GRID)) {
+ return new GridBuilderListV1Impl(getBuilder(), SliceSpecs.GRID);
+ } else if (checkCompatible(SliceSpecs.BASIC)) {
+ return new GridBuilderBasicImpl(getBuilder(), SliceSpecs.GRID);
+ }
+ return null;
+ }
+
+ @Override
+ void setImpl(TemplateBuilderImpl impl) {
+ mImpl = (androidx.app.slice.builders.impl.GridBuilder) impl;
}
/**
@@ -80,7 +111,7 @@ public class GridBuilder extends TemplateSliceBuilder {
*/
@NonNull
public GridBuilder addCell(@NonNull CellBuilder builder) {
- getBuilder().addSubSlice(builder.build());
+ mImpl.addCell((TemplateBuilderImpl) builder.mImpl);
return this;
}
@@ -96,6 +127,14 @@ public class GridBuilder extends TemplateSliceBuilder {
}
/**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public androidx.app.slice.builders.impl.GridBuilder getImpl() {
+ return mImpl;
+ }
+
+ /**
* 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
@@ -103,7 +142,7 @@ public class GridBuilder extends TemplateSliceBuilder {
* the image.
*
* <pre class="prettyprint">
- * CellBuilder cb = new CellBuilder(sliceUri);
+ * CellBuilder cb = new CellBuilder(parent, sliceUri);
* cb.addText("First text")
* .addImage(middleIcon)
* .addText("Second text");
@@ -113,23 +152,36 @@ public class GridBuilder extends TemplateSliceBuilder {
* </p>
*/
public static final class CellBuilder extends TemplateSliceBuilder {
-
- private PendingIntent mContentIntent;
+ private androidx.app.slice.builders.impl.GridBuilder.CellBuilder mImpl;
/**
* 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());
+ super(parent.mImpl.createGridBuilder());
+ }
+
+ /**
+ */
+ public CellBuilder(@NonNull Uri uri) {
+ super(uri);
+ throw new RuntimeException("Stub, to be removed");
}
/**
* Create a builder which will construct a slice displayed as a cell in a grid.
* @param uri Uri to tag for this slice.
+ * @hide
*/
- public CellBuilder(@NonNull Uri uri) {
- super(new Slice.Builder(uri));
+ @RestrictTo(LIBRARY_GROUP)
+ public CellBuilder(@NonNull GridBuilder parent, @NonNull Uri uri) {
+ super(parent.mImpl.createGridBuilder(uri));
+ }
+
+ @Override
+ void setImpl(TemplateBuilderImpl impl) {
+ mImpl = (androidx.app.slice.builders.impl.GridBuilder.CellBuilder) impl;
}
/**
@@ -138,7 +190,7 @@ public class GridBuilder extends TemplateSliceBuilder {
*/
@NonNull
public CellBuilder addText(@NonNull CharSequence text) {
- getBuilder().addText(text, null);
+ mImpl.addText(text);
return this;
}
@@ -149,7 +201,7 @@ public class GridBuilder extends TemplateSliceBuilder {
*/
@NonNull
public CellBuilder addTitleText(@NonNull CharSequence text) {
- getBuilder().addText(text, null, HINT_LARGE);
+ mImpl.addTitleText(text);
return this;
}
@@ -161,7 +213,7 @@ public class GridBuilder extends TemplateSliceBuilder {
*/
@NonNull
public CellBuilder addLargeImage(@NonNull Icon image) {
- getBuilder().addIcon(image, null, HINT_LARGE);
+ mImpl.addLargeImage(image);
return this;
}
@@ -173,7 +225,7 @@ public class GridBuilder extends TemplateSliceBuilder {
*/
@NonNull
public CellBuilder addImage(@NonNull Icon image) {
- getBuilder().addIcon(image, null);
+ mImpl.addImage(image);
return this;
}
@@ -182,28 +234,8 @@ public class GridBuilder extends TemplateSliceBuilder {
*/
@NonNull
public CellBuilder setContentIntent(@NonNull PendingIntent intent) {
- mContentIntent = intent;
+ mImpl.setContentIntent(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
index 2cb75d72..6c312854 100644
--- a/androidx/app/slice/builders/ListBuilder.java
+++ b/androidx/app/slice/builders/ListBuilder.java
@@ -16,35 +16,26 @@
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;
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.content.Context;
import android.graphics.drawable.Icon;
import android.net.Uri;
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 java.util.ArrayList;
import java.util.function.Consumer;
import androidx.app.slice.Slice;
-import androidx.app.slice.SliceItem;
-import androidx.app.slice.core.SliceHints;
+import androidx.app.slice.SliceSpecs;
+import androidx.app.slice.builders.impl.ListBuilderBasicImpl;
+import androidx.app.slice.builders.impl.ListBuilderV1Impl;
+import androidx.app.slice.builders.impl.TemplateBuilderImpl;
/**
* Builder to construct slice content in a list format.
@@ -68,22 +59,28 @@ import androidx.app.slice.core.SliceHints;
public class ListBuilder extends TemplateSliceBuilder {
private boolean mHasSummary;
+ private androidx.app.slice.builders.impl.ListBuilder mImpl;
/**
- * 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);
+ throw new RuntimeException("Stub, to be removed");
}
/**
+ * Create a builder which will construct a slice that will display rows of content.
+ * @param uri Uri to tag for this slice.
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
- @Override
- public void apply(androidx.app.slice.Slice.Builder builder) {
+ public ListBuilder(@NonNull Context context, @NonNull Uri uri) {
+ super(context, uri);
+ }
+ @Override
+ void setImpl(TemplateBuilderImpl impl) {
+ mImpl = (androidx.app.slice.builders.impl.ListBuilder) impl;
}
/**
@@ -91,7 +88,7 @@ public class ListBuilder extends TemplateSliceBuilder {
*/
@NonNull
public ListBuilder addRow(@NonNull RowBuilder builder) {
- getBuilder().addSubSlice(builder.build());
+ mImpl.addRow((TemplateBuilderImpl) builder.mImpl);
return this;
}
@@ -112,7 +109,7 @@ public class ListBuilder extends TemplateSliceBuilder {
*/
@NonNull
public ListBuilder addGrid(@NonNull GridBuilder builder) {
- getBuilder().addSubSlice(builder.build());
+ mImpl.addGrid((TemplateBuilderImpl) builder.getImpl());
return this;
}
@@ -140,8 +137,7 @@ public class ListBuilder extends TemplateSliceBuilder {
throw new IllegalArgumentException("Trying to add summary row when one has "
+ "already been added");
}
- builder.getBuilder().addHints(HINT_SUMMARY);
- getBuilder().addSubSlice(builder.build(), null);
+ mImpl.addSummaryRow((TemplateBuilderImpl) builder.mImpl);
mHasSummary = true;
return this;
}
@@ -162,8 +158,7 @@ public class ListBuilder extends TemplateSliceBuilder {
}
RowBuilder b = new RowBuilder(this);
c.accept(b);
- b.getBuilder().addHints(HINT_SUMMARY);
- getBuilder().addSubSlice(b.build(), null);
+ mImpl.addSummaryRow((TemplateBuilderImpl) b.mImpl);
mHasSummary = true;
return this;
}
@@ -175,11 +170,33 @@ public class ListBuilder extends TemplateSliceBuilder {
@RestrictTo(LIBRARY_GROUP)
@NonNull
public ListBuilder setColor(int color) {
- getBuilder().addInt(color, SUBTYPE_COLOR);
+ mImpl.setColor(color);
return this;
}
/**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ @Override
+ protected TemplateBuilderImpl selectImpl() {
+ if (checkCompatible(SliceSpecs.LIST)) {
+ return new ListBuilderV1Impl(getBuilder(), SliceSpecs.LIST);
+ } else if (checkCompatible(SliceSpecs.BASIC)) {
+ return new ListBuilderBasicImpl(getBuilder(), SliceSpecs.BASIC);
+ }
+ return null;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public androidx.app.slice.builders.impl.ListBuilder getImpl() {
+ return mImpl;
+ }
+
+ /**
* Sub-builder to construct a row of slice content.
* <p>
* Row content can have:
@@ -201,15 +218,11 @@ public class ListBuilder extends TemplateSliceBuilder {
*/
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 androidx.app.slice.builders.impl.ListBuilder.RowBuilder mImpl;
+
+ private boolean mHasEndActionOrToggle;
private boolean mHasEndImage;
+ private boolean mHasDefaultToggle;
private boolean mHasTimestamp;
/**
@@ -217,15 +230,34 @@ public class ListBuilder extends TemplateSliceBuilder {
* @param parent The builder constructing the parent slice.
*/
public RowBuilder(@NonNull ListBuilder parent) {
- super(parent.createChildBuilder());
+ super(parent.mImpl.createRowBuilder());
+ }
+
+ /**
+ */
+ public RowBuilder(@NonNull Uri uri) {
+ super(uri);
+ throw new RuntimeException("Stub, to be removed");
}
/**
* Create a builder which will construct a slice displayed in a row format.
* @param uri Uri to tag for this slice.
+ * @hide
*/
- public RowBuilder(@NonNull Uri uri) {
- super(new Slice.Builder(uri));
+ @RestrictTo(LIBRARY_GROUP)
+ public RowBuilder(@NonNull ListBuilder parent, @NonNull Uri uri) {
+ super(parent.mImpl.createRowBuilder(uri));
+ }
+
+ /**
+ * Create a builder which will construct a slice displayed in a row format.
+ * @param uri Uri to tag for this slice.
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public RowBuilder(@NonNull Context context, @NonNull Uri uri) {
+ super(new ListBuilder(context, uri).mImpl.createRowBuilder(uri));
}
/**
@@ -234,7 +266,7 @@ public class ListBuilder extends TemplateSliceBuilder {
*/
@NonNull
public RowBuilder setIsHeader(boolean isHeader) {
- mIsHeader = isHeader;
+ mImpl.setIsHeader(isHeader);
return this;
}
@@ -251,7 +283,7 @@ public class ListBuilder extends TemplateSliceBuilder {
throw new IllegalArgumentException("Trying to add a timestamp when one has "
+ "already been added");
}
- mStartItem = new SliceItem(timeStamp, FORMAT_TIMESTAMP, null, new String[0]);
+ mImpl.setTitleItem(timeStamp);
mHasTimestamp = true;
return this;
}
@@ -264,7 +296,7 @@ public class ListBuilder extends TemplateSliceBuilder {
*/
@NonNull
public RowBuilder setTitleItem(@NonNull Icon icon) {
- mStartItem = new SliceItem(icon, FORMAT_IMAGE, null, new String[0]);
+ mImpl.setTitleItem(icon);
return this;
}
@@ -276,8 +308,7 @@ public class ListBuilder extends TemplateSliceBuilder {
*/
@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]);
+ mImpl.setTitleItem(icon, action);
return this;
}
@@ -286,7 +317,7 @@ public class ListBuilder extends TemplateSliceBuilder {
*/
@NonNull
public RowBuilder setContentIntent(@NonNull PendingIntent action) {
- mContentIntent = action;
+ mImpl.setContentIntent(action);
return this;
}
@@ -295,7 +326,7 @@ public class ListBuilder extends TemplateSliceBuilder {
*/
@NonNull
public RowBuilder setTitle(CharSequence title) {
- mTitleItem = new SliceItem(title, FORMAT_TEXT, null, new String[] {HINT_TITLE});
+ mImpl.setTitle(title);
return this;
}
@@ -304,7 +335,7 @@ public class ListBuilder extends TemplateSliceBuilder {
*/
@NonNull
public RowBuilder setSubtitle(CharSequence subtitle) {
- mSubtitleItem = new SliceItem(subtitle, FORMAT_TEXT, null, new String[0]);
+ mImpl.setSubtitle(subtitle);
return this;
}
@@ -318,32 +349,31 @@ public class ListBuilder extends TemplateSliceBuilder {
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]));
+ mImpl.addEndItem(timeStamp);
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
+ * 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) {
+ if (mHasEndActionOrToggle) {
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}));
+ mImpl.addEndItem(icon);
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
+ * tappable icons is not permitted. If an icon has already been added, this will throw
* {@link IllegalArgumentException}.
*/
@NonNull
@@ -353,79 +383,60 @@ public class ListBuilder extends TemplateSliceBuilder {
+ "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;
+ mImpl.addEndItem(icon, action);
+ mHasEndActionOrToggle = 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.
+ * Adds a toggle action 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 an
+ * {@link IllegalArgumentException}.
*/
@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;
+ return addToggleInternal(action, isChecked, null);
}
/**
- * 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.
+ * Adds a toggle action to be displayed with custom icons to represent checked and
+ * unchecked state 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 an
+ * {@link IllegalArgumentException}.
*/
@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.");
+ return addToggleInternal(action, isChecked, icon);
+ }
+
+ private RowBuilder addToggleInternal(@NonNull PendingIntent action, boolean isChecked,
+ @Nullable Icon icon) {
+ if (mHasEndImage) {
+ throw new IllegalStateException("Trying to add a toggle to end items when an "
+ + "icon has already been added. End items cannot have a mixture of "
+ + "tappable icons and icons.");
}
- @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;
+ if (mHasDefaultToggle) {
+ throw new IllegalStateException("Only one non-custom toggle can be added "
+ + "in a single row. If you would like to include multiple toggles "
+ + "in a row, set a custom icon for each toggle.");
+ }
+ mImpl.addToggle(action, isChecked, icon);
+ mHasDefaultToggle = icon == null;
+ mHasEndActionOrToggle = 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);
+ void setImpl(TemplateBuilderImpl impl) {
+ mImpl = (androidx.app.slice.builders.impl.ListBuilder.RowBuilder) impl;
+ }
+
+ /**
+ */
+ public void apply(Slice.Builder builder) {
+ throw new RuntimeException("Stub, to be removed");
}
}
}
diff --git a/androidx/app/slice/builders/MessagingSliceBuilder.java b/androidx/app/slice/builders/MessagingSliceBuilder.java
index 66bb3457..434ab750 100644
--- a/androidx/app/slice/builders/MessagingSliceBuilder.java
+++ b/androidx/app/slice/builders/MessagingSliceBuilder.java
@@ -16,8 +16,10 @@
package androidx.app.slice.builders;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import android.content.Context;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Build;
@@ -28,10 +30,18 @@ import android.support.annotation.RestrictTo;
import java.util.function.Consumer;
import androidx.app.slice.Slice;
+import androidx.app.slice.SliceSpecs;
+import androidx.app.slice.builders.impl.MessagingBasicImpl;
+import androidx.app.slice.builders.impl.MessagingBuilder;
+import androidx.app.slice.builders.impl.MessagingListV1Impl;
+import androidx.app.slice.builders.impl.MessagingV1Impl;
+import androidx.app.slice.builders.impl.TemplateBuilderImpl;
/**
* Builder to construct slice content in a messaging format.
+ * @hide
*/
+@RestrictTo(LIBRARY_GROUP)
public class MessagingSliceBuilder extends TemplateSliceBuilder {
/**
@@ -40,24 +50,28 @@ public class MessagingSliceBuilder extends TemplateSliceBuilder {
*/
public static final int MAXIMUM_RETAINED_MESSAGES = 50;
+ private MessagingBuilder mBuilder;
+
+ /**
+ */
public MessagingSliceBuilder(@NonNull Uri uri) {
super(uri);
+ throw new RuntimeException("Stub, to be removed");
}
/**
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
- @Override
- public void apply(androidx.app.slice.Slice.Builder builder) {
-
+ public MessagingSliceBuilder(@NonNull Context context, @NonNull Uri uri) {
+ super(context, uri);
}
/**
* Add a subslice to this builder.
*/
public MessagingSliceBuilder add(MessageBuilder builder) {
- getBuilder().addSubSlice(builder.build());
+ mBuilder.add((TemplateBuilderImpl) builder.mImpl);
return this;
}
@@ -71,23 +85,48 @@ public class MessagingSliceBuilder extends TemplateSliceBuilder {
return add(b);
}
+ @Override
+ void setImpl(TemplateBuilderImpl impl) {
+ mBuilder = (MessagingBuilder) impl;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ @Override
+ protected TemplateBuilderImpl selectImpl() {
+ if (checkCompatible(SliceSpecs.MESSAGING)) {
+ return new MessagingV1Impl(getBuilder(), SliceSpecs.MESSAGING);
+ } else if (checkCompatible(SliceSpecs.LIST)) {
+ return new MessagingListV1Impl(getBuilder(), SliceSpecs.LIST);
+ } else if (checkCompatible(SliceSpecs.BASIC)) {
+ return new MessagingBasicImpl(getBuilder(), SliceSpecs.BASIC);
+ }
+ return null;
+ }
+
/**
* Builder for adding a message to {@link MessagingSliceBuilder}.
*/
public static final class MessageBuilder extends TemplateSliceBuilder {
+
+ private MessagingBuilder.MessageBuilder mImpl;
+
/**
+ * Creates a MessageBuilder with the specified parent.
* @hide
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @RestrictTo(LIBRARY_GROUP)
public MessageBuilder(MessagingSliceBuilder parent) {
- super(parent.createChildBuilder());
+ super(parent.mBuilder.createMessageBuilder());
}
/**
* Add the icon used to display contact in the messaging experience
*/
public MessageBuilder addSource(Icon source) {
- getBuilder().addIcon(source, android.app.slice.Slice.SUBTYPE_SOURCE);
+ mImpl.addSource(source);
return this;
}
@@ -95,7 +134,7 @@ public class MessagingSliceBuilder extends TemplateSliceBuilder {
* Add the text to be used for this message.
*/
public MessageBuilder addText(CharSequence text) {
- getBuilder().addText(text, null);
+ mImpl.addText(text);
return this;
}
@@ -103,12 +142,19 @@ public class MessagingSliceBuilder extends TemplateSliceBuilder {
* Add the time at which this message arrived in ms since Unix epoch
*/
public MessageBuilder addTimestamp(long timestamp) {
- getBuilder().addTimestamp(timestamp, null);
+ mImpl.addTimestamp(timestamp);
return this;
}
@Override
+ void setImpl(TemplateBuilderImpl impl) {
+ mImpl = (MessagingBuilder.MessageBuilder) impl;
+ }
+
+ /**
+ */
public void apply(Slice.Builder builder) {
+ throw new RuntimeException("Stub, to be removed");
}
}
}
diff --git a/androidx/app/slice/builders/TemplateSliceBuilder.java b/androidx/app/slice/builders/TemplateSliceBuilder.java
index 464f940a..dfbd0559 100644
--- a/androidx/app/slice/builders/TemplateSliceBuilder.java
+++ b/androidx/app/slice/builders/TemplateSliceBuilder.java
@@ -18,57 +18,140 @@ package androidx.app.slice.builders;
import static android.support.annotation.RestrictTo.Scope.LIBRARY;
+import android.content.Context;
import android.net.Uri;
import android.support.annotation.RestrictTo;
+import android.util.Log;
+import android.util.Pair;
+
+import java.util.Arrays;
+import java.util.List;
import androidx.app.slice.Slice;
+import androidx.app.slice.SliceProvider;
+import androidx.app.slice.SliceSpec;
+import androidx.app.slice.SliceSpecs;
+import androidx.app.slice.builders.impl.TemplateBuilderImpl;
/**
* Base class of builders of various template types.
*/
public abstract class TemplateSliceBuilder {
- private final Slice.Builder mSliceBuilder;
+ private static final String TAG = "TemplateSliceBuilder";
+
+ private final Slice.Builder mBuilder;
+ private final Context mContext;
+ private final TemplateBuilderImpl mImpl;
+ private List<SliceSpec> mSpecs;
+ /**
+ */
public TemplateSliceBuilder(Uri uri) {
- mSliceBuilder = new Slice.Builder(uri);
+ this(null, uri);
+ throw new RuntimeException("Stub, to be removed");
}
/**
* @hide
*/
@RestrictTo(LIBRARY)
- protected TemplateSliceBuilder(Slice.Builder b) {
- mSliceBuilder = b;
+ protected TemplateSliceBuilder(TemplateBuilderImpl impl) {
+ mContext = null;
+ mBuilder = null;
+ mImpl = impl;
+ setImpl(impl);
}
/**
* @hide
*/
@RestrictTo(LIBRARY)
- public Slice.Builder getBuilder() {
- return mSliceBuilder;
+ protected TemplateSliceBuilder(Slice.Builder b, Context context) {
+ mBuilder = b;
+ mContext = context;
+ mSpecs = getSpecs();
+ mImpl = selectImpl();
+ if (mImpl == null) {
+ throw new IllegalArgumentException("No valid specs found");
+ }
+ setImpl(mImpl);
}
/**
* @hide
*/
@RestrictTo(LIBRARY)
- public Slice.Builder createChildBuilder() {
- return new Slice.Builder(mSliceBuilder);
+ public TemplateSliceBuilder(Context context, Uri uri) {
+ mBuilder = new Slice.Builder(uri);
+ mContext = context;
+ mSpecs = getSpecs();
+ mImpl = selectImpl();
+ if (mImpl == null) {
+ throw new IllegalArgumentException("No valid specs found");
+ }
+ setImpl(mImpl);
}
/**
* Construct the slice.
*/
public Slice build() {
- apply(mSliceBuilder);
- return mSliceBuilder.build();
+ return mImpl.build();
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ protected Slice.Builder getBuilder() {
+ return mBuilder;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ abstract void setImpl(TemplateBuilderImpl impl);
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ protected TemplateBuilderImpl selectImpl() {
+ return null;
}
/**
* @hide
*/
@RestrictTo(LIBRARY)
- public abstract void apply(Slice.Builder builder);
+ protected boolean checkCompatible(SliceSpec candidate) {
+ final int size = mSpecs.size();
+ for (int i = 0; i < size; i++) {
+ if (mSpecs.get(i).canRender(candidate)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private List<SliceSpec> getSpecs() {
+ if (SliceProvider.getCurrentSpecs() != null) {
+ return SliceProvider.getCurrentSpecs();
+ }
+ // TODO: Support getting specs from pinned info.
+ Log.w(TAG, "Not currently bunding a slice");
+ return Arrays.asList(SliceSpecs.BASIC);
+ }
+
+ /**
+ * This is for typing, to clean up the code.
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ static <T> Pair<SliceSpec, Class<? extends TemplateBuilderImpl>> pair(SliceSpec spec,
+ Class<T> cls) {
+ return new Pair(spec, cls);
+ }
}
diff --git a/androidx/app/slice/builders/impl/GridBuilder.java b/androidx/app/slice/builders/impl/GridBuilder.java
new file mode 100644
index 00000000..28134be3
--- /dev/null
+++ b/androidx/app/slice/builders/impl/GridBuilder.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright 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 androidx.app.slice.builders.impl;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+
+import androidx.app.slice.Slice;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public interface GridBuilder {
+ /**
+ * Create an TemplateBuilderImpl that implements {@link CellBuilder}.
+ */
+ TemplateBuilderImpl createGridBuilder();
+
+ /**
+ * Create an TemplateBuilderImpl that implements {@link CellBuilder} with the specified Uri.
+ */
+ TemplateBuilderImpl createGridBuilder(Uri uri);
+
+ /**
+ * Add a cell to this builder. Expected to be a builder from {@link #createGridBuilder};
+ */
+ void addCell(TemplateBuilderImpl impl);
+
+ /**
+ * Builds a standalone slice of this grid builder (i.e. not contained within a List).
+ */
+ Slice buildIndividual();
+
+ /**
+ */
+ interface CellBuilder {
+ /**
+ * 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
+ void addText(@NonNull CharSequence text);
+
+ /**
+ * 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
+ void addTitleText(@NonNull CharSequence text);
+
+ /**
+ * 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
+ void addLargeImage(@NonNull Icon image);
+
+ /**
+ * 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
+ void addImage(@NonNull Icon image);
+
+ /**
+ * Sets the action to be invoked if the user taps on this cell in the row.
+ */
+ @NonNull
+ void setContentIntent(@NonNull PendingIntent intent);
+ }
+}
diff --git a/androidx/app/slice/builders/impl/GridBuilderBasicImpl.java b/androidx/app/slice/builders/impl/GridBuilderBasicImpl.java
new file mode 100644
index 00000000..8205013f
--- /dev/null
+++ b/androidx/app/slice/builders/impl/GridBuilderBasicImpl.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 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 androidx.app.slice.builders.impl;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceSpec;
+
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class GridBuilderBasicImpl extends TemplateBuilderImpl implements GridBuilder {
+
+ /**
+ */
+ public GridBuilderBasicImpl(Slice.Builder b, SliceSpec spec) {
+ super(b, spec);
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createGridBuilder() {
+ return new CellBuilder(this);
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createGridBuilder(Uri uri) {
+ return new CellBuilder(uri);
+ }
+
+ /**
+ */
+ @Override
+ public void addCell(TemplateBuilderImpl impl) {
+ // TODO: Consider extracting some grid content for the basic version.
+ }
+
+ /**
+ */
+ @Override
+ public Slice buildIndividual() {
+ // Empty slice, nothing useful from a grid to basic.
+ return getBuilder().build();
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+
+ }
+
+ /**
+ */
+ public static final class CellBuilder extends TemplateBuilderImpl implements
+ GridBuilder.CellBuilder {
+
+ /**
+ */
+ public CellBuilder(@NonNull GridBuilderBasicImpl parent) {
+ super(parent.createChildBuilder(), null);
+ }
+
+ /**
+ */
+ public CellBuilder(@NonNull Uri uri) {
+ super(new Slice.Builder(uri), null);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addText(@NonNull CharSequence text) {
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addTitleText(@NonNull CharSequence text) {
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addLargeImage(@NonNull Icon image) {
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addImage(@NonNull Icon image) {
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void setContentIntent(@NonNull PendingIntent intent) {
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+
+ }
+ }
+}
diff --git a/androidx/app/slice/builders/impl/GridBuilderListV1Impl.java b/androidx/app/slice/builders/impl/GridBuilderListV1Impl.java
new file mode 100644
index 00000000..d1a9e12f
--- /dev/null
+++ b/androidx/app/slice/builders/impl/GridBuilderListV1Impl.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.builders.impl;
+
+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 android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceSpec;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class GridBuilderListV1Impl extends TemplateBuilderImpl implements GridBuilder {
+
+ /**
+ */
+ public GridBuilderListV1Impl(@NonNull Slice.Builder builder, SliceSpec spec) {
+ super(builder, spec);
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+ builder.addHints(HINT_HORIZONTAL, HINT_LIST_ITEM);
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createGridBuilder() {
+ return new CellBuilder(this);
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createGridBuilder(Uri uri) {
+ return new CellBuilder(uri);
+ }
+
+ /**
+ */
+ @Override
+ public void addCell(TemplateBuilderImpl builder) {
+ getBuilder().addSubSlice(builder.build());
+ }
+
+ /**
+ */
+ @Override
+ public Slice buildIndividual() {
+ return new Slice.Builder(getBuilder()).addHints(HINT_HORIZONTAL, HINT_LIST_ITEM)
+ .addSubSlice(getBuilder()
+ .addHints(HINT_HORIZONTAL, HINT_LIST_ITEM).build()).build();
+ }
+
+ /**
+ */
+ public static final class CellBuilder extends TemplateBuilderImpl implements
+ GridBuilder.CellBuilder {
+
+ private PendingIntent mContentIntent;
+
+ /**
+ */
+ public CellBuilder(@NonNull GridBuilderListV1Impl parent) {
+ super(parent.createChildBuilder(), null);
+ }
+
+ /**
+ */
+ public CellBuilder(@NonNull Uri uri) {
+ super(new Slice.Builder(uri), null);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addText(@NonNull CharSequence text) {
+ getBuilder().addText(text, null);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addTitleText(@NonNull CharSequence text) {
+ getBuilder().addText(text, null, HINT_LARGE);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addLargeImage(@NonNull Icon image) {
+ getBuilder().addIcon(image, null, HINT_LARGE);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addImage(@NonNull Icon image) {
+ getBuilder().addIcon(image, null);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void setContentIntent(@NonNull PendingIntent intent) {
+ mContentIntent = intent;
+ }
+
+ /**
+ * @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/impl/ListBuilder.java b/androidx/app/slice/builders/impl/ListBuilder.java
new file mode 100644
index 00000000..d9605b44
--- /dev/null
+++ b/androidx/app/slice/builders/impl/ListBuilder.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 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 androidx.app.slice.builders.impl;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.support.annotation.RestrictTo;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public interface ListBuilder {
+
+ /**
+ * Add a row to list builder.
+ */
+ void addRow(TemplateBuilderImpl impl);
+ /**
+ * Add a grid row to the list builder.
+ */
+ void addGrid(TemplateBuilderImpl impl);
+ /**
+ * Add a summary row for this template. The summary content is displayed
+ * when the slice is displayed in small format.
+ */
+ void addSummaryRow(TemplateBuilderImpl builder);
+
+ /**
+ * Sets the color to tint items displayed by this template (e.g. icons).
+ */
+ void setColor(int color);
+
+ /**
+ * Create a builder that implements {@link RowBuilder}.
+ */
+ TemplateBuilderImpl createRowBuilder();
+ /**
+ * Create a builder that implements {@link RowBuilder}.
+ */
+ TemplateBuilderImpl createRowBuilder(Uri uri);
+
+ /**
+ * Create a builder that implements {@link GridBuilder}.
+ */
+ TemplateBuilderImpl createGridBuilder();
+
+ /**
+ */
+ public interface RowBuilder {
+
+ /**
+ * 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.
+ */
+ void setIsHeader(boolean isHeader);
+
+ /**
+ * 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.
+ */
+ void setTitleItem(long timeStamp);
+
+ /**
+ * 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.
+ */
+ void setTitleItem(Icon icon);
+
+ /**
+ * 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.
+ */
+ void setTitleItem(Icon icon, PendingIntent action);
+
+ /**
+ * Sets the action to be invoked if the user taps on the main content of the template.
+ */
+ void setContentIntent(PendingIntent action);
+
+ /**
+ * Sets the title text.
+ */
+ void setTitle(CharSequence title);
+
+ /**
+ * Sets the subtitle text.
+ */
+ void setSubtitle(CharSequence subtitle);
+
+ /**
+ * Adds a timestamp to be displayed at the end of the row.
+ */
+ void addEndItem(long timeStamp);
+
+ /**
+ * Adds an icon to be displayed at the end of the row.
+ */
+ void addEndItem(Icon icon);
+
+ /**
+ * Adds a tappable icon to be displayed at the end of the row.
+ */
+ void addEndItem(Icon icon, PendingIntent action);
+
+ /**
+ * Adds a toggle action to the template with custom icons to represent checked and unchecked
+ * state.
+ */
+ void addToggle(PendingIntent action, boolean isChecked, Icon icon);
+ }
+}
diff --git a/androidx/app/slice/builders/impl/ListBuilderBasicImpl.java b/androidx/app/slice/builders/impl/ListBuilderBasicImpl.java
new file mode 100644
index 00000000..f9e4ac63
--- /dev/null
+++ b/androidx/app/slice/builders/impl/ListBuilderBasicImpl.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright 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 androidx.app.slice.builders.impl;
+
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.app.PendingIntent;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceSpec;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class ListBuilderBasicImpl extends TemplateBuilderImpl implements ListBuilder {
+
+ /**
+ */
+ public ListBuilderBasicImpl(Slice.Builder b, SliceSpec spec) {
+ super(b, spec);
+ }
+
+ /**
+ */
+ @Override
+ public void addRow(TemplateBuilderImpl impl) {
+ // Do nothing.
+ }
+
+ /**
+ */
+ @Override
+ public void addGrid(TemplateBuilderImpl impl) {
+ // Do nothing.
+ }
+
+ /**
+ */
+ @Override
+ public void addSummaryRow(TemplateBuilderImpl builder) {
+ RowBuilderImpl row = (RowBuilderImpl) builder;
+ if (row.mIcon != null) {
+ getBuilder().addIcon(row.mIcon, null);
+ }
+ if (row.mTitle != null) {
+ getBuilder().addText(row.mTitle, null, android.app.slice.Slice.HINT_TITLE);
+ }
+ if (row.mSubtitle != null) {
+ getBuilder().addText(row.mSubtitle, null);
+ }
+ }
+
+ /**
+ */
+ @Override
+ public void setColor(int color) {
+
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createRowBuilder() {
+ return new RowBuilderImpl(this);
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createRowBuilder(Uri uri) {
+ return new RowBuilderImpl(uri);
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createGridBuilder() {
+ return new GridBuilderBasicImpl(createChildBuilder(), null);
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+
+ }
+
+ /**
+ */
+ public static class RowBuilderImpl extends TemplateBuilderImpl
+ implements ListBuilder.RowBuilder {
+ private Icon mIcon;
+ private CharSequence mTitle;
+ private CharSequence mSubtitle;
+
+ /**
+ */
+ public RowBuilderImpl(@NonNull ListBuilderBasicImpl parent) {
+ super(parent.createChildBuilder(), null);
+ }
+
+ /**
+ */
+ public RowBuilderImpl(@NonNull Uri uri) {
+ super(new Slice.Builder(uri), null);
+ }
+
+ /**
+ */
+ @Override
+ public void addEndItem(Icon icon) {
+
+ }
+
+ /**
+ */
+ @Override
+ public void addEndItem(Icon icon, PendingIntent action) {
+
+ }
+
+ /**
+ */
+ @Override
+ public void addToggle(PendingIntent action, boolean isChecked, Icon icon) {
+
+ }
+
+ /**
+ */
+ @Override
+ public void setIsHeader(boolean isHeader) {
+
+ }
+
+ /**
+ */
+ @Override
+ public void setTitleItem(long timeStamp) {
+
+ }
+
+ /**
+ */
+ @Override
+ public void setTitleItem(Icon icon) {
+ mIcon = icon;
+ }
+
+ /**
+ */
+ @Override
+ public void setTitleItem(Icon icon, PendingIntent action) {
+ mIcon = icon;
+ }
+
+ /**
+ */
+ @Override
+ public void setContentIntent(PendingIntent action) {
+
+ }
+
+ /**
+ */
+ @Override
+ public void setTitle(CharSequence title) {
+ mTitle = title;
+ }
+
+ /**
+ */
+ @Override
+ public void setSubtitle(CharSequence subtitle) {
+ mSubtitle = subtitle;
+ }
+
+ /**
+ */
+ @Override
+ public void addEndItem(long timeStamp) {
+
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+
+ }
+ }
+}
diff --git a/androidx/app/slice/builders/impl/ListBuilderV1Impl.java b/androidx/app/slice/builders/impl/ListBuilderV1Impl.java
new file mode 100644
index 00000000..15a17c62
--- /dev/null
+++ b/androidx/app/slice/builders/impl/ListBuilderV1Impl.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 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 androidx.app.slice.builders.impl;
+
+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;
+
+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.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+
+import java.util.ArrayList;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceItem;
+import androidx.app.slice.SliceSpec;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class ListBuilderV1Impl extends TemplateBuilderImpl implements ListBuilder {
+
+ /**
+ */
+ public ListBuilderV1Impl(Slice.Builder b, SliceSpec spec) {
+ super(b, spec);
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+
+ }
+
+ /**
+ * Add a row to list builder.
+ */
+ @NonNull
+ @Override
+ public void addRow(@NonNull TemplateBuilderImpl builder) {
+ getBuilder().addSubSlice(builder.build());
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addGrid(@NonNull TemplateBuilderImpl builder) {
+ getBuilder().addSubSlice(builder.build());
+ }
+
+ /**
+ */
+ @Override
+ public void addSummaryRow(TemplateBuilderImpl builder) {
+ builder.getBuilder().addHints(HINT_SUMMARY);
+ getBuilder().addSubSlice(builder.build(), null);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void setColor(int color) {
+ getBuilder().addInt(color, SUBTYPE_COLOR);
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createRowBuilder() {
+ return new RowBuilderImpl(this);
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createRowBuilder(Uri uri) {
+ return new RowBuilderImpl(uri);
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createGridBuilder() {
+ return new GridBuilderListV1Impl(createChildBuilder(), null);
+ }
+
+ /**
+ */
+ public static class RowBuilderImpl extends TemplateBuilderImpl
+ implements ListBuilder.RowBuilder {
+
+ private boolean mIsHeader;
+ private PendingIntent mContentIntent;
+ private SliceItem mTitleItem;
+ private SliceItem mSubtitleItem;
+ private SliceItem mStartItem;
+ private ArrayList<SliceItem> mEndItems = new ArrayList<>();
+
+ /**
+ */
+ public RowBuilderImpl(@NonNull ListBuilderV1Impl parent) {
+ super(parent.createChildBuilder(), null);
+ }
+
+ /**
+ */
+ public RowBuilderImpl(@NonNull Uri uri) {
+ super(new Slice.Builder(uri), null);
+ }
+
+ /**
+ */
+ public RowBuilderImpl(Slice.Builder builder) {
+ super(builder, null);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void setIsHeader(boolean isHeader) {
+ mIsHeader = isHeader;
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void setTitleItem(long timeStamp) {
+ mStartItem = new SliceItem(timeStamp, FORMAT_TIMESTAMP, null, new String[0]);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void setTitleItem(@NonNull Icon icon) {
+ mStartItem = new SliceItem(icon, FORMAT_IMAGE, null, new String[0]);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void 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]);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void setContentIntent(@NonNull PendingIntent action) {
+ mContentIntent = action;
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void setTitle(CharSequence title) {
+ mTitleItem = new SliceItem(title, FORMAT_TEXT, null, new String[]{HINT_TITLE});
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void setSubtitle(CharSequence subtitle) {
+ mSubtitleItem = new SliceItem(subtitle, FORMAT_TEXT, null, new String[0]);
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addEndItem(long timeStamp) {
+ mEndItems.add(new SliceItem(timeStamp, FORMAT_TIMESTAMP, null, new String[0]));
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addEndItem(@NonNull Icon icon) {
+ mEndItems.add(new SliceItem(icon, FORMAT_IMAGE, null,
+ new String[]{HINT_NO_TINT, HINT_LARGE}));
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addEndItem(@NonNull Icon icon, @NonNull PendingIntent action) {
+ Slice actionSlice = new Slice.Builder(getBuilder()).addIcon(icon, null).build();
+ mEndItems.add(new SliceItem(action, actionSlice, FORMAT_ACTION, null, new String[0]));
+ }
+
+ /**
+ */
+ @NonNull
+ @Override
+ public void addToggle(@NonNull PendingIntent action, boolean isChecked,
+ @NonNull Icon icon) {
+ @Slice.SliceHint String[] hints = isChecked
+ ? new String[] {SUBTYPE_TOGGLE, HINT_SELECTED}
+ : new String[] {SUBTYPE_TOGGLE};
+ Slice.Builder actionSliceBuilder = new Slice.Builder(getBuilder()).addHints(hints);
+ if (icon != null) {
+ actionSliceBuilder.addIcon(icon, null);
+ }
+ Slice actionSlice = actionSliceBuilder.build();
+ mEndItems.add(new SliceItem(action, actionSlice, FORMAT_ACTION, null, hints));
+ }
+
+ /**
+ */
+ @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/impl/MessagingBasicImpl.java b/androidx/app/slice/builders/impl/MessagingBasicImpl.java
new file mode 100644
index 00000000..843302c8
--- /dev/null
+++ b/androidx/app/slice/builders/impl/MessagingBasicImpl.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 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 androidx.app.slice.builders.impl;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.graphics.drawable.Icon;
+import android.support.annotation.RestrictTo;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceSpec;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class MessagingBasicImpl extends TemplateBuilderImpl implements
+ MessagingBuilder {
+ private MessageBuilder mLastMessage;
+
+ /**
+ */
+ public MessagingBasicImpl(Slice.Builder builder, SliceSpec spec) {
+ super(builder, spec);
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+ if (mLastMessage != null) {
+ if (mLastMessage.mIcon != null) {
+ builder.addIcon(mLastMessage.mIcon, null);
+ }
+ if (mLastMessage.mText != null) {
+ builder.addText(mLastMessage.mText, null);
+ }
+ }
+ }
+
+ /**
+ */
+ @Override
+ public void add(TemplateBuilderImpl builder) {
+ MessageBuilder b = (MessageBuilder) builder;
+ if (mLastMessage == null || mLastMessage.mTimestamp < b.mTimestamp) {
+ mLastMessage = b;
+ }
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createMessageBuilder() {
+ return new MessageBuilder(this);
+ }
+
+ /**
+ */
+ public static final class MessageBuilder extends TemplateBuilderImpl
+ implements MessagingBuilder.MessageBuilder {
+
+ private Icon mIcon;
+ private CharSequence mText;
+ private long mTimestamp;
+
+ /**
+ */
+ public MessageBuilder(MessagingBasicImpl parent) {
+ this(parent.createChildBuilder());
+ }
+
+ /**
+ */
+ private MessageBuilder(Slice.Builder builder) {
+ super(builder, null);
+ }
+
+ /**
+ */
+ @Override
+ public void addSource(Icon source) {
+ mIcon = source;
+ }
+
+ /**
+ */
+ @Override
+ public void addText(CharSequence text) {
+ mText = text;
+ }
+
+ /**
+ */
+ @Override
+ public void addTimestamp(long timestamp) {
+ mTimestamp = timestamp;
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+ }
+ }
+}
diff --git a/androidx/app/slice/builders/impl/MessagingBuilder.java b/androidx/app/slice/builders/impl/MessagingBuilder.java
new file mode 100644
index 00000000..635f160d
--- /dev/null
+++ b/androidx/app/slice/builders/impl/MessagingBuilder.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 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 androidx.app.slice.builders.impl;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.graphics.drawable.Icon;
+import android.support.annotation.RestrictTo;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public interface MessagingBuilder {
+ /**
+ * Add a subslice to this builder.
+ */
+ void add(TemplateBuilderImpl builder);
+
+ /**
+ * Create a builder that implements {@link MessageBuilder}
+ */
+ TemplateBuilderImpl createMessageBuilder();
+
+ /**
+ */
+ public interface MessageBuilder {
+
+ /**
+ * Add the icon used to display contact in the messaging experience
+ */
+ void addSource(Icon source);
+
+ /**
+ * Add the text to be used for this message.
+ */
+ void addText(CharSequence text);
+
+ /**
+ * Add the time at which this message arrived in ms since Unix epoch
+ */
+ void addTimestamp(long timestamp);
+ }
+}
diff --git a/androidx/app/slice/builders/impl/MessagingListV1Impl.java b/androidx/app/slice/builders/impl/MessagingListV1Impl.java
new file mode 100644
index 00000000..408ad0b4
--- /dev/null
+++ b/androidx/app/slice/builders/impl/MessagingListV1Impl.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 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 androidx.app.slice.builders.impl;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.graphics.drawable.Icon;
+import android.support.annotation.RestrictTo;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceSpec;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public class MessagingListV1Impl extends TemplateBuilderImpl implements MessagingBuilder{
+
+ private final ListBuilderV1Impl mListBuilder;
+
+ /**
+ */
+ public MessagingListV1Impl(Slice.Builder b, SliceSpec spec) {
+ super(b, spec);
+ mListBuilder = new ListBuilderV1Impl(b, spec);
+ }
+
+ /**
+ */
+ @Override
+ public void add(TemplateBuilderImpl builder) {
+ MessageBuilder b = (MessageBuilder) builder;
+ mListBuilder.addRow(b.mListBuilder);
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createMessageBuilder() {
+ return new MessageBuilder(this);
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+ mListBuilder.apply(builder);
+ }
+
+ /**
+ */
+ public static final class MessageBuilder extends TemplateBuilderImpl
+ implements MessagingBuilder.MessageBuilder {
+ private final ListBuilderV1Impl.RowBuilderImpl mListBuilder;
+
+ /**
+ */
+ public MessageBuilder(MessagingListV1Impl parent) {
+ this(parent.createChildBuilder());
+ }
+
+ private MessageBuilder(Slice.Builder builder) {
+ super(builder, null);
+ mListBuilder = new ListBuilderV1Impl.RowBuilderImpl(builder);
+ }
+
+ /**
+ */
+ @Override
+ public void addSource(Icon source) {
+ mListBuilder.setTitleItem(source);
+ }
+
+ /**
+ */
+ @Override
+ public void addText(CharSequence text) {
+ mListBuilder.setSubtitle(text);
+ }
+
+ /**
+ */
+ @Override
+ public void addTimestamp(long timestamp) {
+ mListBuilder.addEndItem(timestamp);
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+ mListBuilder.apply(builder);
+ }
+ }
+}
diff --git a/androidx/app/slice/builders/impl/MessagingV1Impl.java b/androidx/app/slice/builders/impl/MessagingV1Impl.java
new file mode 100644
index 00000000..4e07139e
--- /dev/null
+++ b/androidx/app/slice/builders/impl/MessagingV1Impl.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 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 androidx.app.slice.builders.impl;
+
+import static android.app.slice.Slice.SUBTYPE_MESSAGE;
+
+import android.graphics.drawable.Icon;
+import android.support.annotation.RestrictTo;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceSpec;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class MessagingV1Impl extends TemplateBuilderImpl implements MessagingBuilder {
+
+ /**
+ */
+ public MessagingV1Impl(Slice.Builder b, SliceSpec spec) {
+ super(b, spec);
+ }
+
+ /**
+ */
+ @Override
+ public void add(TemplateBuilderImpl builder) {
+ getBuilder().addSubSlice(builder.build(), SUBTYPE_MESSAGE);
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+
+ }
+
+ /**
+ */
+ @Override
+ public TemplateBuilderImpl createMessageBuilder() {
+ return new MessageBuilder(this);
+ }
+
+ /**
+ */
+ public static final class MessageBuilder extends TemplateBuilderImpl
+ implements MessagingBuilder.MessageBuilder {
+ /**
+ */
+ public MessageBuilder(MessagingV1Impl parent) {
+ super(parent.createChildBuilder(), null);
+ }
+
+ /**
+ */
+ @Override
+ public void addSource(Icon source) {
+ getBuilder().addIcon(source, android.app.slice.Slice.SUBTYPE_SOURCE);
+ }
+
+ /**
+ */
+ @Override
+ public void addText(CharSequence text) {
+ getBuilder().addText(text, null);
+ }
+
+ /**
+ */
+ @Override
+ public void addTimestamp(long timestamp) {
+ getBuilder().addTimestamp(timestamp, null);
+ }
+
+ /**
+ */
+ @Override
+ public void apply(Slice.Builder builder) {
+ }
+ }
+}
diff --git a/androidx/app/slice/builders/impl/TemplateBuilderImpl.java b/androidx/app/slice/builders/impl/TemplateBuilderImpl.java
new file mode 100644
index 00000000..294677e5
--- /dev/null
+++ b/androidx/app/slice/builders/impl/TemplateBuilderImpl.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.app.slice.builders.impl;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
+
+import android.support.annotation.RestrictTo;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceSpec;
+
+/**
+ * @hide
+ */
+@RestrictTo(LIBRARY)
+public abstract class TemplateBuilderImpl {
+
+ private final Slice.Builder mSliceBuilder;
+ private final SliceSpec mSpec;
+
+ protected TemplateBuilderImpl(Slice.Builder b, SliceSpec spec) {
+ mSliceBuilder = b;
+ mSpec = spec;
+ }
+
+ /**
+ * Construct the slice.
+ */
+ public Slice build() {
+ mSliceBuilder.setSpec(mSpec);
+ apply(mSliceBuilder);
+ return mSliceBuilder.build();
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public Slice.Builder getBuilder() {
+ return mSliceBuilder;
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public Slice.Builder createChildBuilder() {
+ return new Slice.Builder(mSliceBuilder);
+ }
+
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public abstract void apply(Slice.Builder builder);
+}
diff --git a/androidx/app/slice/compat/CompatPinnedList.java b/androidx/app/slice/compat/CompatPinnedList.java
new file mode 100644
index 00000000..7a3b900a
--- /dev/null
+++ b/androidx/app/slice/compat/CompatPinnedList.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright 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 androidx.app.slice.compat;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.util.ArraySet;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+import androidx.app.slice.SliceSpec;
+
+/**
+ * Tracks the current packages requesting pinning of any given slice. It will clear the
+ * list after a reboot since the packages are no longer requesting pinning.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class CompatPinnedList {
+
+ private static final String LAST_BOOT = "last_boot";
+ private static final String PIN_PREFIX = "pinned_";
+ private static final String SPEC_NAME_PREFIX = "spec_names_";
+ private static final String SPEC_REV_PREFIX = "spec_revs_";
+
+ // Max skew between bootup times that we think its probably rebooted.
+ // There could be some difference in our calculated boot up time if the thread
+ // sleeps between currentTimeMillis and elapsedRealtime.
+ // Its probably safe to assume the device can't boot twice within 2 secs.
+ private static final long BOOT_THRESHOLD = 2000;
+
+ private final Context mContext;
+ private final String mPrefsName;
+
+ public CompatPinnedList(Context context, String prefsName) {
+ mContext = context;
+ mPrefsName = prefsName;
+ }
+
+ private SharedPreferences getPrefs() {
+ SharedPreferences prefs = mContext.getSharedPreferences(mPrefsName, Context.MODE_PRIVATE);
+ long lastBootTime = prefs.getLong(LAST_BOOT, 0);
+ long currentBootTime = getBootTime();
+ if (Math.abs(lastBootTime - currentBootTime) > BOOT_THRESHOLD) {
+ prefs.edit()
+ .clear()
+ .putLong(LAST_BOOT, currentBootTime)
+ .commit();
+ }
+ return prefs;
+ }
+
+ private Set<String> getPins(Uri uri) {
+ return getPrefs().getStringSet(PIN_PREFIX + uri.toString(), new ArraySet<String>());
+ }
+
+ /**
+ * Get the list of specs for a pinned Uri.
+ */
+ public synchronized List<SliceSpec> getSpecs(Uri uri) {
+ List<SliceSpec> specs = new ArrayList<>();
+ SharedPreferences prefs = getPrefs();
+ String specNamesStr = prefs.getString(SPEC_NAME_PREFIX + uri.toString(), null);
+ String specRevsStr = prefs.getString(SPEC_REV_PREFIX + uri.toString(), null);
+ if (TextUtils.isEmpty(specNamesStr) || TextUtils.isEmpty(specRevsStr)) {
+ return Collections.emptyList();
+ }
+ String[] specNames = specNamesStr.split(",");
+ String[] specRevs = specRevsStr.split(",");
+ if (specNames.length != specRevs.length) {
+ return Collections.emptyList();
+ }
+ for (int i = 0; i < specNames.length; i++) {
+ specs.add(new SliceSpec(specNames[i], Integer.parseInt(specRevs[i])));
+ }
+ return specs;
+ }
+
+ private void setPins(Uri uri, Set<String> pins) {
+ getPrefs().edit()
+ .putStringSet(PIN_PREFIX + uri.toString(), pins)
+ .commit();
+ }
+
+ private void setSpecs(Uri uri, List<SliceSpec> specs) {
+ String[] specNames = new String[specs.size()];
+ String[] specRevs = new String[specs.size()];
+ for (int i = 0; i < specs.size(); i++) {
+ specNames[i] = specs.get(i).getType();
+ specRevs[i] = String.valueOf(specs.get(i).getRevision());
+ }
+ getPrefs().edit()
+ .putString(SPEC_NAME_PREFIX + uri.toString(), TextUtils.join(",", specNames))
+ .putString(SPEC_REV_PREFIX + uri.toString(), TextUtils.join(",", specRevs))
+ .commit();
+ }
+
+ @VisibleForTesting
+ protected long getBootTime() {
+ return System.currentTimeMillis() - SystemClock.elapsedRealtime();
+ }
+
+ /**
+ * Adds a pin for a specific uri/pkg pair and returns true if the
+ * uri was not previously pinned.
+ */
+ public synchronized boolean addPin(Uri uri, String pkg, List<SliceSpec> specs) {
+ Set<String> pins = getPins(uri);
+ boolean wasNotPinned = pins.isEmpty();
+ pins.add(pkg);
+ setPins(uri, pins);
+ if (wasNotPinned) {
+ setSpecs(uri, specs);
+ } else {
+ setSpecs(uri, mergeSpecs(getSpecs(uri), specs));
+ }
+ return wasNotPinned;
+ }
+
+ /**
+ * Removes a pin for a specific uri/pkg pair and returns true if the
+ * uri is no longer pinned (but was).
+ */
+ public synchronized boolean removePin(Uri uri, String pkg) {
+ Set<String> pins = getPins(uri);
+ if (pins.isEmpty() || !pins.contains(pkg)) {
+ return false;
+ }
+ pins.remove(pkg);
+ setPins(uri, pins);
+ return pins.size() == 0;
+ }
+
+ private static List<SliceSpec> mergeSpecs(List<SliceSpec> specs,
+ List<SliceSpec> supportedSpecs) {
+ for (int i = 0; i < specs.size(); i++) {
+ SliceSpec s = specs.get(i);
+ SliceSpec other = findSpec(supportedSpecs, s.getType());
+ if (other == null) {
+ specs.remove(i--);
+ } else if (other.getRevision() < s.getRevision()) {
+ specs.set(i, other);
+ }
+ }
+ return specs;
+ }
+
+ private static SliceSpec findSpec(List<SliceSpec> specs, String type) {
+ for (SliceSpec spec : specs) {
+ if (Objects.equals(spec.getType(), type)) {
+ return spec;
+ }
+ }
+ return null;
+ }
+}
diff --git a/androidx/app/slice/compat/CompatPinnedListTest.java b/androidx/app/slice/compat/CompatPinnedListTest.java
new file mode 100644
index 00000000..18bb6065
--- /dev/null
+++ b/androidx/app/slice/compat/CompatPinnedListTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright 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 androidx.app.slice.compat;
+
+import static android.content.Context.MODE_PRIVATE;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertTrue;
+
+import static org.junit.Assert.assertArrayEquals;
+
+import android.content.Context;
+import android.net.Uri;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import androidx.app.slice.SliceSpec;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class CompatPinnedListTest {
+
+ private final Context mContext = InstrumentationRegistry.getContext();
+ private CompatPinnedList mCompatPinnedList;
+ private List<SliceSpec> mSpecs;
+
+ private static final SliceSpec[] FIRST_SPECS = new SliceSpec[]{
+ new SliceSpec("spec1", 3),
+ new SliceSpec("spec2", 3),
+ new SliceSpec("spec3", 2),
+ new SliceSpec("spec4", 1),
+ };
+
+ private static final SliceSpec[] SECOND_SPECS = new SliceSpec[]{
+ new SliceSpec("spec2", 1),
+ new SliceSpec("spec3", 2),
+ new SliceSpec("spec4", 3),
+ new SliceSpec("spec5", 4),
+ };
+
+ @Before
+ public void setup() {
+ mCompatPinnedList = new CompatPinnedList(mContext, "test_file");
+ mSpecs = Collections.emptyList();
+ }
+
+ @After
+ public void tearDown() {
+ mContext.getSharedPreferences("test_file", MODE_PRIVATE).edit().clear().commit();
+ }
+
+ @Test
+ public void testAddFirstPin() {
+ assertTrue(mCompatPinnedList.addPin(Uri.parse("content://something/something"), "my_pkg",
+ mSpecs));
+ }
+
+ @Test
+ public void testAddSecondPin() {
+ assertTrue(mCompatPinnedList.addPin(Uri.parse("content://something/something"), "my_pkg",
+ mSpecs));
+ assertFalse(mCompatPinnedList.addPin(Uri.parse("content://something/something"), "my_pkg2",
+ mSpecs));
+ }
+
+ @Test
+ public void testAddMultipleUris() {
+ assertTrue(mCompatPinnedList.addPin(Uri.parse("content://something/something"), "my_pkg",
+ mSpecs));
+ assertTrue(mCompatPinnedList.addPin(Uri.parse("content://something/something2"), "my_pkg",
+ mSpecs));
+ }
+
+ @Test
+ public void testRemovePin() {
+ mCompatPinnedList.addPin(Uri.parse("content://something/something"), "my_pkg", mSpecs);
+ mCompatPinnedList.addPin(Uri.parse("content://something/something"), "my_pkg2", mSpecs);
+ assertFalse(mCompatPinnedList.removePin(Uri.parse("content://something/something"),
+ "my_pkg"));
+ assertTrue(mCompatPinnedList.removePin(Uri.parse("content://something/something"),
+ "my_pkg2"));
+ }
+
+ @Test
+ public void testMergeSpecs() {
+ Uri uri = Uri.parse("content://something/something");
+
+ assertEquals(Collections.emptyList(), mCompatPinnedList.getSpecs(uri));
+
+ mCompatPinnedList.addPin(uri, "my_pkg", Arrays.asList(FIRST_SPECS));
+ assertArrayEquals(FIRST_SPECS, mCompatPinnedList.getSpecs(uri).toArray(new SliceSpec[0]));
+
+ mCompatPinnedList.addPin(uri, "my_pkg2", Arrays.asList(SECOND_SPECS));
+ assertArrayEquals(new SliceSpec[]{
+ // spec1 is gone because it's not in the second set.
+ new SliceSpec("spec2", 1), // spec2 is 1 because it's smaller in the second set.
+ new SliceSpec("spec3", 2), // spec3 is the same in both sets
+ new SliceSpec("spec4", 1), // spec4 is 1 because it's smaller in the first set.
+ // spec5 is gone because it's not in the first set.
+ }, mCompatPinnedList.getSpecs(uri).toArray(new SliceSpec[0]));
+
+ }
+}
diff --git a/androidx/app/slice/compat/SlicePermissionActivity.java b/androidx/app/slice/compat/SlicePermissionActivity.java
new file mode 100644
index 00000000..78170aba
--- /dev/null
+++ b/androidx/app/slice/compat/SlicePermissionActivity.java
@@ -0,0 +1,94 @@
+/*
+ * Copyright 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 androidx.app.slice.compat;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.RestrictTo;
+import android.util.Log;
+import android.widget.TextView;
+
+import androidx.app.slice.core.R;
+
+/**
+ * Dialog that grants slice permissions for an app.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class SlicePermissionActivity extends Activity implements OnClickListener,
+ OnDismissListener {
+
+ private static final String TAG = "SlicePermissionActivity";
+
+ private Uri mUri;
+ private String mCallingPkg;
+ private String mProviderPkg;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mUri = getIntent().getParcelableExtra(SliceProviderCompat.EXTRA_BIND_URI);
+ mCallingPkg = getIntent().getStringExtra(SliceProviderCompat.EXTRA_PKG);
+ mProviderPkg = getIntent().getStringExtra(SliceProviderCompat.EXTRA_PROVIDER_PKG);
+
+ try {
+ PackageManager pm = getPackageManager();
+ CharSequence app1 = pm.getApplicationInfo(mCallingPkg, 0).loadLabel(pm);
+ CharSequence app2 = pm.getApplicationInfo(mProviderPkg, 0).loadLabel(pm);
+ AlertDialog dialog = new AlertDialog.Builder(this)
+ .setTitle(getString(R.string.abc_slice_permission_title, app1, app2))
+ .setView(R.layout.abc_slice_permission_request)
+ .setNegativeButton(R.string.abc_slice_permission_deny, this)
+ .setPositiveButton(R.string.abc_slice_permission_allow, this)
+ .setOnDismissListener(this)
+ .show();
+ TextView t1 = dialog.getWindow().getDecorView().findViewById(R.id.text1);
+ t1.setText(getString(R.string.abc_slice_permission_text_1, app2));
+ TextView t2 = dialog.getWindow().getDecorView().findViewById(R.id.text2);
+ t2.setText(getString(R.string.abc_slice_permission_text_2, app2));
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Couldn't find package", e);
+ finish();
+ }
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ grantUriPermission(mCallingPkg, mUri.buildUpon().path("").build(),
+ Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION);
+ getContentResolver().notifyChange(mUri, null);
+ }
+ finish();
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ finish();
+ }
+}
diff --git a/androidx/app/slice/compat/SliceProviderCompat.java b/androidx/app/slice/compat/SliceProviderCompat.java
index 503ba0a5..d1a8e65a 100644
--- a/androidx/app/slice/compat/SliceProviderCompat.java
+++ b/androidx/app/slice/compat/SliceProviderCompat.java
@@ -15,15 +15,19 @@
*/
package androidx.app.slice.compat;
+import static android.app.slice.Slice.HINT_LIST_ITEM;
import static android.app.slice.SliceProvider.SLICE_TYPE;
import android.Manifest.permission;
+import android.app.PendingIntent;
+import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.net.Uri;
@@ -37,6 +41,7 @@ import android.os.Process;
import android.os.RemoteException;
import android.os.StrictMode;
import android.os.StrictMode.ThreadPolicy;
+import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.annotation.RestrictTo.Scope;
import android.util.Log;
@@ -48,6 +53,7 @@ import java.util.concurrent.CountDownLatch;
import androidx.app.slice.Slice;
import androidx.app.slice.SliceProvider;
import androidx.app.slice.SliceSpec;
+import androidx.app.slice.core.R;
/**
* @hide
@@ -60,21 +66,42 @@ public class SliceProviderCompat extends ContentProvider {
public static final String EXTRA_BIND_URI = "slice_uri";
public static final String METHOD_SLICE = "bind_slice";
public static final String METHOD_MAP_INTENT = "map_slice";
+ public static final String METHOD_PIN = "pin_slice";
+ public static final String METHOD_UNPIN = "unpin_slice";
+ public static final String METHOD_GET_PINNED_SPECS = "get_specs";
+
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";
+ public static final String EXTRA_PKG = "pkg";
+ public static final String EXTRA_PROVIDER_PKG = "provider_pkg";
+ private static final String DATA_PREFIX = "slice_data_";
private static final boolean DEBUG = false;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private SliceProvider mSliceProvider;
+ private CompatPinnedList mPinnedList;
+ private String mBindingPkg;
public SliceProviderCompat(SliceProvider provider) {
mSliceProvider = provider;
}
+ /**
+ * Return the package name of the caller that initiated the binding request
+ * currently happening. The returned package will have been
+ * verified to belong to the calling UID. Returns {@code null} if not
+ * currently performing an {@link SliceProvider#onBindSlice(Uri)}.
+ */
+ public final @Nullable String getBindingPackage() {
+ return mBindingPkg;
+ }
+
@Override
public boolean onCreate() {
+ mPinnedList = new CompatPinnedList(getContext(),
+ DATA_PREFIX + mSliceProvider.getClass().getName());
return mSliceProvider.onCreateSliceProvider();
}
@@ -136,7 +163,7 @@ public class SliceProviderCompat extends ContentProvider {
}
List<SliceSpec> specs = getSpecs(extras);
- Slice s = handleBindSlice(uri, specs);
+ Slice s = handleBindSlice(uri, specs, getCallingPackage());
Bundle b = new Bundle();
b.putParcelable(EXTRA_SLICE, s.toBundle());
return b;
@@ -150,26 +177,101 @@ public class SliceProviderCompat extends ContentProvider {
Bundle b = new Bundle();
if (uri != null) {
List<SliceSpec> specs = getSpecs(extras);
- Slice s = handleBindSlice(uri, specs);
+ Slice s = handleBindSlice(uri, specs, getCallingPackage());
b.putParcelable(EXTRA_SLICE, s.toBundle());
} else {
b.putParcelable(EXTRA_SLICE, null);
}
return b;
+ } else if (method.equals(METHOD_PIN)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ List<SliceSpec> specs = getSpecs(extras);
+ String pkg = extras.getString(EXTRA_PKG);
+ if (mPinnedList.addPin(uri, pkg, specs)) {
+ handleSlicePinned(uri);
+ }
+ return null;
+ } else if (method.equals(METHOD_UNPIN)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ String pkg = extras.getString(EXTRA_PKG);
+ if (mPinnedList.removePin(uri, pkg)) {
+ handleSliceUnpinned(uri);
+ }
+ return null;
+ } else if (method.equals(METHOD_GET_PINNED_SPECS)) {
+ Uri uri = extras.getParcelable(EXTRA_BIND_URI);
+ Bundle b = new Bundle();
+ addSpecs(b, mPinnedList.getSpecs(uri));
+ return b;
}
return super.call(method, arg, extras);
}
- private Slice handleBindSlice(final Uri sliceUri, final List<SliceSpec> specs) {
+ private void handleSlicePinned(final Uri sliceUri) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ mSliceProvider.onSlicePinned(sliceUri);
+ } else {
+ final CountDownLatch latch = new CountDownLatch(1);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSliceProvider.onSlicePinned(sliceUri);
+ latch.countDown();
+ }
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private void handleSliceUnpinned(final Uri sliceUri) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ mSliceProvider.onSliceUnpinned(sliceUri);
+ } else {
+ final CountDownLatch latch = new CountDownLatch(1);
+ mHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ mSliceProvider.onSliceUnpinned(sliceUri);
+ latch.countDown();
+ }
+ });
+ try {
+ latch.await();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ }
+
+ private Slice handleBindSlice(final Uri sliceUri, final List<SliceSpec> specs,
+ final String callingPkg) {
+ // This can be removed once Slice#bindSlice is removed and everyone is using
+ // SliceManager#bindSlice.
+ String pkg = callingPkg != null ? callingPkg
+ : getContext().getPackageManager().getNameForUid(Binder.getCallingUid());
+ if (Binder.getCallingUid() != Process.myUid()) {
+ try {
+ getContext().enforceUriPermission(sliceUri,
+ Binder.getCallingPid(), Binder.getCallingUid(),
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+ "Slice binding requires write access to Uri");
+ } catch (SecurityException e) {
+ return createPermissionSlice(getContext(), sliceUri, pkg);
+ }
+ }
if (Looper.myLooper() == Looper.getMainLooper()) {
- return onBindSliceStrict(sliceUri, specs);
+ return onBindSliceStrict(sliceUri, specs, callingPkg);
} 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, specs);
+ output[0] = onBindSliceStrict(sliceUri, specs, callingPkg);
latch.countDown();
}
});
@@ -182,7 +284,54 @@ public class SliceProviderCompat extends ContentProvider {
}
}
- private Slice onBindSliceStrict(Uri sliceUri, List<SliceSpec> specs) {
+ /**
+ * Generate a slice that contains a permission request.
+ */
+ public static Slice createPermissionSlice(Context context, Uri sliceUri,
+ String callingPackage) {
+ return new Slice.Builder(sliceUri)
+ .addAction(createPermissionIntent(context, sliceUri, callingPackage),
+ new Slice.Builder(sliceUri.buildUpon().appendPath("permission").build())
+ .addText(getPermissionString(context, callingPackage), null)
+ .build(), null)
+ .addHints(HINT_LIST_ITEM)
+ .build();
+ }
+
+ /**
+ * Create a PendingIntent pointing at the permission dialog.
+ */
+ public static PendingIntent createPermissionIntent(Context context, Uri sliceUri,
+ String callingPackage) {
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName(context.getPackageName(),
+ "androidx.app.slice.compat.SlicePermissionActivity"));
+ intent.putExtra(EXTRA_BIND_URI, sliceUri);
+ intent.putExtra(EXTRA_PKG, callingPackage);
+ intent.putExtra(EXTRA_PROVIDER_PKG, context.getPackageName());
+ // Unique pending intent.
+ intent.setData(sliceUri.buildUpon().appendQueryParameter("package", callingPackage)
+ .build());
+
+ return PendingIntent.getActivity(context, 0, intent, 0);
+ }
+
+ /**
+ * Get string describing permission request.
+ */
+ public static CharSequence getPermissionString(Context context, String callingPackage) {
+ PackageManager pm = context.getPackageManager();
+ try {
+ return context.getString(R.string.abc_slices_permission_request,
+ pm.getApplicationInfo(callingPackage, 0).loadLabel(pm),
+ context.getApplicationInfo().loadLabel(pm));
+ } catch (PackageManager.NameNotFoundException e) {
+ // This shouldn't be possible since the caller is verified.
+ throw new RuntimeException("Unknown calling app", e);
+ }
+ }
+
+ private Slice onBindSliceStrict(Uri sliceUri, List<SliceSpec> specs, String callingPackage) {
ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
try {
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
@@ -190,7 +339,13 @@ public class SliceProviderCompat extends ContentProvider {
.penaltyDeath()
.build());
SliceProvider.setSpecs(specs);
- return mSliceProvider.onBindSlice(sliceUri);
+ try {
+ mBindingPkg = callingPackage;
+ return mSliceProvider.onBindSlice(sliceUri);
+ } finally {
+ mBindingPkg = null;
+ SliceProvider.setSpecs(null);
+ }
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
@@ -295,4 +450,78 @@ public class SliceProviderCompat extends ContentProvider {
provider.close();
}
}
+
+ /**
+ * Compat version of {@link android.app.slice.SliceManager#pinSlice}.
+ */
+ public static void pinSlice(Context context, Uri uri,
+ List<SliceSpec> supportedSpecs) {
+ ContentProviderClient provider = context.getContentResolver()
+ .acquireContentProviderClient(uri);
+ if (provider == null) {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ try {
+ Bundle extras = new Bundle();
+ extras.putParcelable(EXTRA_BIND_URI, uri);
+ extras.putString(EXTRA_PKG, context.getPackageName());
+ addSpecs(extras, supportedSpecs);
+ provider.call(METHOD_PIN, null, extras);
+ } catch (RemoteException e) {
+ // Arbitrary and not worth documenting, as Activity
+ // Manager will kill this process shortly anyway.
+ } finally {
+ provider.close();
+ }
+ }
+
+ /**
+ * Compat version of {@link android.app.slice.SliceManager#unpinSlice}.
+ */
+ public static void unpinSlice(Context context, Uri uri,
+ List<SliceSpec> supportedSpecs) {
+ ContentProviderClient provider = context.getContentResolver()
+ .acquireContentProviderClient(uri);
+ if (provider == null) {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ try {
+ Bundle extras = new Bundle();
+ extras.putParcelable(EXTRA_BIND_URI, uri);
+ extras.putString(EXTRA_PKG, context.getPackageName());
+ addSpecs(extras, supportedSpecs);
+ provider.call(METHOD_UNPIN, null, extras);
+ } catch (RemoteException e) {
+ // Arbitrary and not worth documenting, as Activity
+ // Manager will kill this process shortly anyway.
+ } finally {
+ provider.close();
+ }
+ }
+
+ /**
+ * Compat version of {@link android.app.slice.SliceManager#getPinnedSpecs(Uri)}.
+ */
+ public static List<SliceSpec> getPinnedSpecs(Context context, Uri uri) {
+ ContentProviderClient provider = context.getContentResolver()
+ .acquireContentProviderClient(uri);
+ if (provider == null) {
+ throw new IllegalArgumentException("Unknown URI " + uri);
+ }
+ try {
+ Bundle extras = new Bundle();
+ extras.putParcelable(EXTRA_BIND_URI, uri);
+ final Bundle res = provider.call(METHOD_GET_PINNED_SPECS, null, extras);
+ if (res == null) {
+ return null;
+ }
+ return getSpecs(res);
+ } catch (RemoteException e) {
+ // Arbitrary and not worth documenting, as Activity
+ // Manager will kill this process shortly anyway.
+ return null;
+ } finally {
+ provider.close();
+ }
+ }
}
diff --git a/androidx/app/slice/compat/SliceProviderWrapper.java b/androidx/app/slice/compat/SliceProviderWrapper.java
deleted file mode 100644
index 438b9641..00000000
--- a/androidx/app/slice/compat/SliceProviderWrapper.java
+++ /dev/null
@@ -1,65 +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 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;
-import android.app.slice.SliceSpec;
-import android.content.Intent;
-import android.net.Uri;
-import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.RestrictTo.Scope;
-
-import java.util.List;
-
-import androidx.app.slice.SliceConvert;
-
-/**
- * @hide
- */
-@RestrictTo(Scope.LIBRARY)
-@TargetApi(28)
-public class SliceProviderWrapper extends SliceProvider {
-
- private androidx.app.slice.SliceProvider mSliceProvider;
-
- public SliceProviderWrapper(androidx.app.slice.SliceProvider provider) {
- mSliceProvider = provider;
- }
-
- @Override
- public boolean onCreate() {
- return mSliceProvider.onCreateSliceProvider();
- }
-
- @Override
- public Slice onBindSlice(Uri sliceUri, List<SliceSpec> supportedVersions) {
- androidx.app.slice.SliceProvider.setSpecs(wrap(supportedVersions));
- return SliceConvert.unwrap(mSliceProvider.onBindSlice(sliceUri));
- }
-
- /**
- * Maps intents to uris.
- */
- @Override
- public @NonNull Uri onMapIntentToUri(Intent intent) {
- return mSliceProvider.onMapIntentToUri(intent);
- }
-}
diff --git a/androidx/app/slice/compat/SliceProviderWrapperContainer.java b/androidx/app/slice/compat/SliceProviderWrapperContainer.java
new file mode 100644
index 00000000..ebc2ad14
--- /dev/null
+++ b/androidx/app/slice/compat/SliceProviderWrapperContainer.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 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 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;
+import android.app.slice.SliceSpec;
+import android.content.Intent;
+import android.net.Uri;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+
+import java.util.List;
+
+import androidx.app.slice.SliceConvert;
+
+/**
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+@TargetApi(28)
+public class SliceProviderWrapperContainer {
+
+ /**
+ */
+ public static class SliceProviderWrapper extends SliceProvider {
+
+ private androidx.app.slice.SliceProvider mSliceProvider;
+
+ public SliceProviderWrapper(androidx.app.slice.SliceProvider provider) {
+ mSliceProvider = provider;
+ }
+
+ @Override
+ public boolean onCreate() {
+ return mSliceProvider.onCreateSliceProvider();
+ }
+
+ @Override
+ public Slice onBindSlice(Uri sliceUri, List<SliceSpec> supportedVersions) {
+ androidx.app.slice.SliceProvider.setSpecs(wrap(supportedVersions));
+ try {
+ return SliceConvert.unwrap(mSliceProvider.onBindSlice(sliceUri));
+ } finally {
+ androidx.app.slice.SliceProvider.setSpecs(null);
+ }
+ }
+
+ @Override
+ public void onSlicePinned(Uri sliceUri) {
+ mSliceProvider.onSlicePinned(sliceUri);
+ }
+
+ @Override
+ public void onSliceUnpinned(Uri sliceUri) {
+ mSliceProvider.onSliceUnpinned(sliceUri);
+ }
+
+ /**
+ * Maps intents to uris.
+ */
+ @Override
+ public @NonNull Uri onMapIntentToUri(Intent intent) {
+ return mSliceProvider.onMapIntentToUri(intent);
+ }
+ }
+}
diff --git a/androidx/app/slice/core/SliceHints.java b/androidx/app/slice/core/SliceHints.java
index 34acf934..c98f1d20 100644
--- a/androidx/app/slice/core/SliceHints.java
+++ b/androidx/app/slice/core/SliceHints.java
@@ -36,13 +36,29 @@ public class SliceHints {
public static final String SUBTYPE_TOGGLE = "toggle";
/**
+ * Subtype indicating that this content is the maximum value for a slider or progress.
+ */
+ public static final String SUBTYPE_MAX = "max";
+
+ /**
+ * Subtype indicating that this content is the current value for a slider or progress.
+ */
+ public static final String SUBTYPE_PROGRESS = "progress";
+
+ /**
* 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";
/**
+ * Key to retrieve an extra added to an intent when the value of a slider has changed.
+ */
+ public static final String EXTRA_SLIDER_VALUE = "android.app.slice.extra.SLIDER_VALUE";
+
+ /**
* Hint indicating this content should be shown instead of the normal content when the slice
* is in small format
*/
public static final String HINT_SUMMARY = "summary";
+
}
diff --git a/androidx/app/slice/core/SliceQuery.java b/androidx/app/slice/core/SliceQuery.java
index 8ab6c9a6..f0f23718 100644
--- a/androidx/app/slice/core/SliceQuery.java
+++ b/androidx/app/slice/core/SliceQuery.java
@@ -17,10 +17,7 @@
package androidx.app.slice.core;
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_TIMESTAMP;
import android.annotation.TargetApi;
import android.support.annotation.RestrictTo;
@@ -49,61 +46,6 @@ import androidx.app.slice.SliceItem;
public class SliceQuery {
/**
- * @return Whether this item is appropriate to be considered a "start" item, i.e. go in the
- * front slot of a row.
- */
- public static boolean isStartType(SliceItem item) {
- final String type = item.getFormat();
- return (!item.hasHint(SliceHints.SUBTYPE_TOGGLE)
- && (FORMAT_ACTION.equals(type) && (find(item, FORMAT_IMAGE) != null)))
- || FORMAT_IMAGE.equals(type)
- || FORMAT_TIMESTAMP.equals(type);
- }
-
- /**
- * @return Finds the first slice that has non-slice children.
- */
- public static SliceItem findFirstSlice(SliceItem slice) {
- if (!FORMAT_SLICE.equals(slice.getFormat())) {
- return slice;
- }
- List<SliceItem> items = slice.getSlice().getItems();
- for (int i = 0; i < items.size(); i++) {
- if (FORMAT_SLICE.equals(items.get(i).getFormat())) {
- SliceItem childSlice = items.get(i);
- return findFirstSlice(childSlice);
- } else {
- // Doesn't have slice children so return it
- return slice;
- }
- }
- // Slices all the way down, just return it
- return slice;
- }
-
- /**
- * @return Whether this item is a simple action, i.e. an action that only has an icon.
- */
- public static boolean isSimpleAction(SliceItem item) {
- if (FORMAT_ACTION.equals(item.getFormat())) {
- List<SliceItem> items = item.getSlice().getItems();
- boolean hasImage = false;
- for (int i = 0; i < items.size(); i++) {
- SliceItem child = items.get(i);
- if (FORMAT_IMAGE.equals(child.getFormat()) && !hasImage) {
- hasImage = true;
- } else if (FORMAT_INT.equals(child.getFormat())) {
- continue;
- } else {
- return false;
- }
- }
- return hasImage;
- }
- return false;
- }
-
- /**
*/
public static boolean hasAnyHints(SliceItem item, String... hints) {
if (hints == null) return false;
@@ -142,7 +84,6 @@ public class SliceQuery {
return true;
}
-
/**
*/
public static SliceItem findNotContaining(SliceItem container, List<SliceItem> list) {
diff --git a/androidx/app/slice/widget/ActionRow.java b/androidx/app/slice/widget/ActionRow.java
index 59dd3c77..2eaa0598 100644
--- a/androidx/app/slice/widget/ActionRow.java
+++ b/androidx/app/slice/widget/ActionRow.java
@@ -17,10 +17,8 @@
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_IMAGE;
-import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_REMOTE_INPUT;
import android.annotation.TargetApi;
@@ -105,17 +103,12 @@ public class ActionRow extends FrameLayout {
/**
* Set the actions and color for this action row.
*/
- public void setActions(SliceItem actionRow, SliceItem defColor) {
+ public void setActions(SliceItem actionRow, int color) {
removeAllViews();
mActionsGroup.removeAllViews();
addView(mActionsGroup);
-
- SliceItem color = SliceQuery.findSubtype(actionRow, FORMAT_INT, SUBTYPE_COLOR);
- if (color == null) {
- color = defColor;
- }
- if (color != null) {
- setColor(color.getInt());
+ if (color != -1) {
+ setColor(color);
}
SliceQuery.findAll(actionRow, FORMAT_ACTION).forEach(new Consumer<SliceItem>() {
@Override
diff --git a/androidx/app/slice/widget/EventInfo.java b/androidx/app/slice/widget/EventInfo.java
new file mode 100644
index 00000000..d45a34df
--- /dev/null
+++ b/androidx/app/slice/widget/EventInfo.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright 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 androidx.app.slice.widget;
+
+import android.support.annotation.IntDef;
+import android.support.annotation.RestrictTo;
+
+/**
+ * Represents information associated with a logged event on {@link SliceView}.
+ */
+public class EventInfo {
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef({
+ ROW_TYPE_SHORTCUT, ROW_TYPE_LIST, ROW_TYPE_GRID, ROW_TYPE_MESSAGING
+ })
+ public @interface SliceRowType {}
+
+ /**
+ * Indicates the slice is represented as a shortcut.
+ */
+ public static final int ROW_TYPE_SHORTCUT = -1;
+ /**
+ * Indicates the row is represented in a list template.
+ */
+ public static final int ROW_TYPE_LIST = 0;
+ /**
+ * Indicates the row is represented in a grid template.
+ */
+ public static final int ROW_TYPE_GRID = 1;
+ /**
+ * Indicates the row is represented as a messaging template.
+ */
+ public static final int ROW_TYPE_MESSAGING = 2;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef({
+ ACTION_TYPE_TOGGLE, ACTION_TYPE_BUTTON, ACTION_TYPE_SLIDER, ACTION_TYPE_CONTENT
+ })
+ public @interface SliceActionType{}
+
+ /**
+ * Indicates the event was an interaction with a toggle. Check {@link EventInfo#state} to
+ * see the new state of the toggle.
+ */
+ public static final int ACTION_TYPE_TOGGLE = 0;
+ /**
+ * Indicates the event was an interaction with a button. Check {@link EventInfo#actionPosition}
+ * to see where on the card the button is placed.
+ */
+ public static final int ACTION_TYPE_BUTTON = 1;
+ /**
+ * Indicates the event was an interaction with a slider. Check {@link EventInfo#state} to
+ * see the new state of the slider.
+ */
+ public static final int ACTION_TYPE_SLIDER = 2;
+ /**
+ * Indicates the event was a tap on the entire row.
+ */
+ public static final int ACTION_TYPE_CONTENT = 3;
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ @IntDef({
+ POSITION_START, POSITION_END, POSITION_CELL
+ })
+ public @interface SliceButtonPosition{}
+
+ /**
+ * Indicates the event was an interaction with a button positioned at the start of the row.
+ */
+ public static final int POSITION_START = 0;
+ /**
+ * Indicates the event was an interaction with a button positioned at the end of the row,
+ * potentially grouped with other buttons.
+ */
+ public static final int POSITION_END = 1;
+ /**
+ * Indicates the event was an interaction with a button positioned in a grid cell.
+ */
+ public static final int POSITION_CELL = 2;
+
+ /**
+ * Indicates the state of a toggle is off.
+ */
+ public static final int STATE_OFF = 0;
+ /**
+ * Indicates the state of a toggle is on.
+ */
+ public static final int STATE_ON = 1;
+
+ /**
+ * The display mode of the slice being interacted with.
+ */
+ public @SliceView.SliceMode int sliceMode;
+ /**
+ * The type of action that occurred.
+ */
+ public @SliceActionType int actionType;
+ /**
+ * The template type of the row that was interacted with in the slice.
+ */
+ public @SliceRowType int rowTemplateType;
+ /**
+ * Index of the row that was interacted with in the slice.
+ */
+ public int rowIndex;
+ /**
+ * If multiple buttons are presented in this {@link #actionPosition} on the row, then this is
+ * the index of that button that was interacted with. For total number of actions
+ * see {@link #actionCount}.
+ *
+ * <p>If the {@link #actionPosition} is {@link #POSITION_CELL} the button is a cell within
+ * a grid, and this index would represent the cell position.</p>
+ * <p>If the {@link #actionPosition} is {@link #POSITION_END} there might be other buttons
+ * in the end position, and this index would represent the position.</p>
+ */
+ public int actionIndex;
+ /**
+ * Total number of actions available in this row of the slice.
+ *
+ * <p>If the {@link #actionPosition} is {@link #POSITION_CELL} the button is a cell within
+ * a grid row, and this is the number of cells in the row.</p>
+ * <p>If the {@link #actionPosition} is {@link #POSITION_END} this is the number of buttons
+ * in the end position of this row.</p>
+ */
+ public int actionCount;
+ /**
+ * Position of the button on the template.
+ *
+ * {@link #POSITION_START}
+ * {@link #POSITION_END}
+ * {@link #POSITION_CELL}
+ */
+ public @SliceButtonPosition int actionPosition;
+ /**
+ * Represents the state after the event or -1 if not applicable for the event type.
+ *
+ * <p>For {@link #ACTION_TYPE_TOGGLE} events, the state will be either {@link #STATE_OFF}
+ * or {@link #STATE_ON}.</p>
+ * <p>For {@link #ACTION_TYPE_SLIDER} events, the state will be a number representing
+ * the new position of the slider.</p>
+ */
+ public int state;
+
+ /**
+ * Constructs an event info object with the required information for an event.
+ *
+ * @param sliceMode The display mode of the slice interacted with.
+ * @param actionType The type of action this event represents.
+ * @param rowTemplateType The template type of the row interacted with.
+ * @param rowIndex The index of the row that was interacted with in the slice.
+ */
+ public EventInfo(@SliceView.SliceMode int sliceMode, @SliceActionType int actionType,
+ @SliceRowType int rowTemplateType, int rowIndex) {
+ this.sliceMode = sliceMode;
+ this.actionType = actionType;
+ this.rowTemplateType = rowTemplateType;
+ this.rowIndex = rowIndex;
+
+ this.actionPosition = -1;
+ this.actionIndex = -1;
+ this.actionCount = -1;
+ this.state = -1;
+ }
+
+ /**
+ * Sets positional information for the event.
+ *
+ * @param actionPosition The position of the button on the template.
+ * @param actionIndex The index of that button that was interacted with.
+ * @param actionCount The number of actions available in this group of buttons on the slice.
+ */
+ public void setPosition(@SliceButtonPosition int actionPosition, int actionIndex,
+ int actionCount) {
+ this.actionPosition = actionPosition;
+ this.actionIndex = actionIndex;
+ this.actionCount = actionCount;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("mode=").append(SliceView.modeToString(sliceMode));
+ sb.append(", actionType=").append(actionToString(actionType));
+ sb.append(", rowTemplateType=").append(rowTypeToString(rowTemplateType));
+ sb.append(", rowIndex=").append(rowIndex);
+ sb.append(", actionPosition=").append(positionToString(actionPosition));
+ sb.append(", actionIndex=").append(actionIndex);
+ sb.append(", actionCount=").append(actionCount);
+ sb.append(", state=").append(state);
+ return sb.toString();
+ }
+
+ /**
+ * @return String representation of the provided position.
+ */
+ private static String positionToString(@SliceButtonPosition int position) {
+ switch (position) {
+ case POSITION_START:
+ return "START";
+ case POSITION_END:
+ return "END";
+ case POSITION_CELL:
+ return "CELL";
+ default:
+ return "unknown position: " + position;
+ }
+ }
+
+ /**
+ * @return String representation of the provided action.
+ */
+ private static String actionToString(@SliceActionType int action) {
+ switch (action) {
+ case ACTION_TYPE_TOGGLE:
+ return "TOGGLE";
+ case ACTION_TYPE_BUTTON:
+ return "BUTTON";
+ case ACTION_TYPE_SLIDER:
+ return "SLIDER";
+ case ACTION_TYPE_CONTENT:
+ return "CONTENT";
+ default:
+ return "unknown action: " + action;
+ }
+ }
+
+ /**
+ * @return String representation of the provided row template type.
+ */
+ private static String rowTypeToString(@SliceRowType int type) {
+ switch (type) {
+ case ROW_TYPE_LIST:
+ return "LIST";
+ case ROW_TYPE_GRID:
+ return "GRID";
+ case ROW_TYPE_MESSAGING:
+ return "MESSAGING";
+ case ROW_TYPE_SHORTCUT:
+ return "SHORTCUT";
+ default:
+ return "unknown row type: " + type;
+ }
+ }
+}
diff --git a/androidx/app/slice/widget/GridContent.java b/androidx/app/slice/widget/GridContent.java
new file mode 100644
index 00000000..9569dc08
--- /dev/null
+++ b/androidx/app/slice/widget/GridContent.java
@@ -0,0 +1,202 @@
+/*
+ * 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.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_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 android.support.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.app.slice.SliceItem;
+import androidx.app.slice.core.SliceQuery;
+
+/**
+ * Extracts information required to present content in a grid format from a slice.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class GridContent {
+
+ private boolean mAllImages;
+ public SliceItem mColorItem;
+ public ArrayList<CellContent> mGridContent = new ArrayList<>();
+
+ public GridContent(SliceItem gridItem) {
+ populate(gridItem);
+ }
+
+ private void reset() {
+ mColorItem = null;
+ mGridContent.clear();
+ }
+
+ /**
+ * @return whether this grid has content that is valid to display.
+ */
+ public boolean populate(SliceItem gridItem) {
+ reset();
+ mColorItem = SliceQuery.findSubtype(gridItem, FORMAT_INT, SUBTYPE_COLOR);
+ mAllImages = true;
+ if (FORMAT_SLICE.equals(gridItem.getFormat())) {
+ List<SliceItem> items = gridItem.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();
+ }
+ for (int i = 0; i < items.size(); i++) {
+ SliceItem item = items.get(i);
+ CellContent cc = new CellContent(item);
+ if (cc.isValid()) {
+ mGridContent.add(cc);
+ if (!cc.isImageOnly()) {
+ mAllImages = false;
+ }
+ }
+ }
+ } else {
+ CellContent cc = new CellContent(gridItem);
+ if (cc.isValid()) {
+ mGridContent.add(cc);
+ }
+ }
+ return isValid();
+ }
+
+ /**
+ * @return the list of cell content for this grid.
+ */
+ public ArrayList<CellContent> getGridContent() {
+ return mGridContent;
+ }
+
+ /**
+ * @return the color to tint content in this grid.
+ */
+ public SliceItem getColorItem() {
+ return mColorItem;
+ }
+
+ /**
+ * @return whether this grid has content that is valid to display.
+ */
+ public boolean isValid() {
+ return mGridContent.size() > 0;
+ }
+
+ /**
+ * @return whether the contents of this grid is just images.
+ */
+ public boolean isAllImages() {
+ return mAllImages;
+ }
+
+ /**
+ * Extracts information required to present content in a cell.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ public static class CellContent {
+ private SliceItem mContentIntent;
+ private ArrayList<SliceItem> mCellItems = new ArrayList<>();
+
+ public CellContent(SliceItem cellItem) {
+ populate(cellItem);
+ }
+
+ /**
+ * @return whether this row has content that is valid to display.
+ */
+ public boolean populate(SliceItem cellItem) {
+ final String format = cellItem.getFormat();
+ if (FORMAT_SLICE.equals(format) || FORMAT_ACTION.equals(format)) {
+ List<SliceItem> items = cellItem.getSlice().getItems();
+ // If we've only got one item that's a slice / action use those items instead
+ if (items.size() == 1 && (FORMAT_ACTION.equals(items.get(0).getFormat())
+ || FORMAT_SLICE.equals(items.get(0).getFormat()))) {
+ mContentIntent = items.get(0);
+ items = items.get(0).getSlice().getItems();
+ }
+ if (FORMAT_ACTION.equals(format)) {
+ mContentIntent = cellItem;
+ }
+ int textCount = 0;
+ int imageCount = 0;
+ for (int i = 0; i < items.size(); i++) {
+ final SliceItem item = items.get(i);
+ final String itemFormat = item.getFormat();
+ if (textCount < 2 && (FORMAT_TEXT.equals(itemFormat)
+ || FORMAT_TIMESTAMP.equals(itemFormat))) {
+ textCount++;
+ mCellItems.add(item);
+ } else if (imageCount < 1 && FORMAT_IMAGE.equals(item.getFormat())) {
+ imageCount++;
+ mCellItems.add(item);
+ }
+ }
+ } else if (isValidCellContent(cellItem)) {
+ mCellItems.add(cellItem);
+ }
+ return isValid();
+ }
+
+ /**
+ * @return the action to activate when this cell is tapped.
+ */
+ public SliceItem getContentIntent() {
+ return mContentIntent;
+ }
+
+ /**
+ * @return the slice items to display in this cell.
+ */
+ public ArrayList<SliceItem> getCellItems() {
+ return mCellItems;
+ }
+
+ /**
+ * @return whether this is content that is valid to show in a grid cell.
+ */
+ private boolean isValidCellContent(SliceItem cellItem) {
+ final String format = cellItem.getFormat();
+ return FORMAT_TEXT.equals(format)
+ || FORMAT_TIMESTAMP.equals(format)
+ || FORMAT_IMAGE.equals(format);
+ }
+
+ /**
+ * @return whether this grid has content that is valid to display.
+ */
+ public boolean isValid() {
+ return mCellItems.size() > 0 && mCellItems.size() <= 3;
+ }
+
+ /**
+ * @return whether this cell contains just an image.
+ */
+ public boolean isImageOnly() {
+ return mCellItems.size() == 1 && FORMAT_IMAGE.equals(mCellItems.get(0).getFormat());
+ }
+ }
+}
diff --git a/androidx/app/slice/widget/GridRowView.java b/androidx/app/slice/widget/GridRowView.java
index 6a5f44b3..8d01c64a 100644
--- a/androidx/app/slice/widget/GridRowView.java
+++ b/androidx/app/slice/widget/GridRowView.java
@@ -21,7 +21,6 @@ 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;
@@ -32,10 +31,11 @@ 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.ColorInt;
import android.support.annotation.RestrictTo;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.Pair;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.LayoutInflater;
@@ -47,6 +47,7 @@ import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout;
import android.widget.TextView;
+import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Predicate;
@@ -62,17 +63,15 @@ import androidx.app.slice.view.R;
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@TargetApi(24)
-public class GridRowView extends LinearLayout implements LargeSliceAdapter.SliceListView,
- View.OnClickListener, SliceView.SliceModeView {
+public class GridRowView extends SliceChildView implements View.OnClickListener {
private static final String TAG = "GridView";
- // TODO -- Should addRow notion to the builder so that apps could define the "see more" intent
+ // TODO -- Should add 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
@@ -85,7 +84,7 @@ public class GridRowView extends LinearLayout implements LargeSliceAdapter.Slice
// Max number of images that can show in a cell
private static final int MAX_CELL_IMAGES = 1;
- private SliceItem mColorItem;
+ private int mRowIndex;
private boolean mIsAllImages;
private @SliceView.SliceMode int mSliceMode = 0;
@@ -93,6 +92,8 @@ public class GridRowView extends LinearLayout implements LargeSliceAdapter.Slice
private int mLargeIconSize;
private int mBigPictureHeight;
private int mAllImagesHeight;
+ private GridContent mGridContent;
+ private LinearLayout mViewContainer;
public GridRowView(Context context) {
this(context, null);
@@ -105,6 +106,9 @@ public class GridRowView extends LinearLayout implements LargeSliceAdapter.Slice
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);
+ mViewContainer = new LinearLayout(getContext());
+ mViewContainer.setOrientation(LinearLayout.HORIZONTAL);
+ addView(mViewContainer, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
@@ -121,22 +125,19 @@ public class GridRowView extends LinearLayout implements LargeSliceAdapter.Slice
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;
+ public int getMode() {
+ return mSliceMode;
}
@Override
- public int getMode() {
- return mSliceMode;
+ public void setTint(@ColorInt int tintColor) {
+ super.setTint(tintColor);
+ if (mGridContent != null) {
+ // TODO -- could be smarter about this
+ resetView();
+ populateViews(mGridContent);
+ }
}
/**
@@ -144,49 +145,42 @@ public class GridRowView extends LinearLayout implements LargeSliceAdapter.Slice
*/
@Override
public void setSlice(Slice slice) {
+ resetView();
+ mRowIndex = 0;
mSliceMode = SliceView.MODE_SMALL;
Slice.Builder sb = new Slice.Builder(slice.getUri());
sb.addSubSlice(slice);
Slice parentSlice = sb.build();
- populateViews(parentSlice.getItems().get(0));
+ mGridContent = new GridContent(parentSlice.getItems().get(0));
+ populateViews(mGridContent);
}
/**
* This is called when GridView is being used as a component in a large template.
*/
@Override
- public void setSliceItem(SliceItem slice, boolean isHeader) {
+ public void setSliceItem(SliceItem slice, boolean isHeader, int index,
+ SliceView.OnSliceActionListener observer) {
+ resetView();
+ setSliceActionListener(observer);
+ mRowIndex = index;
mSliceMode = SliceView.MODE_LARGE;
- populateViews(slice);
+ mGridContent = new GridContent(slice);
+ populateViews(mGridContent);
}
- 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;
+ private void populateViews(GridContent gc) {
+ mIsAllImages = gc.isAllImages();
+ ArrayList<GridContent.CellContent> cells = gc.getGridContent();
+ final int max = mIsAllImages ? MAX_IMAGES : MAX_ALL;
+ for (int i = 0; i < cells.size(); i++) {
+ if (isFull()) {
+ break;
}
+ addCell(cells.get(i), i, Math.min(cells.size(), max));
}
- if (ALLOW_SEE_MORE && mIsAllImages && total > getChildCount()) {
- addSeeMoreCount(total - getChildCount());
+ if (ALLOW_SEE_MORE && mIsAllImages && cells.size() > getChildCount()) {
+ addSeeMoreCount(cells.size() - getChildCount());
}
}
@@ -206,7 +200,7 @@ public class GridRowView extends LinearLayout implements LargeSliceAdapter.Slice
v.setGravity(Gravity.CENTER);
frame.addView(v, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
- addView(frame);
+ mViewContainer.addView(frame);
}
private boolean isFull() {
@@ -215,84 +209,72 @@ public class GridRowView extends LinearLayout implements LargeSliceAdapter.Slice
/**
* 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) {
+ private void addCell(GridContent.CellContent cell, int index, int total) {
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();
- }
+
+ ArrayList<SliceItem> cellItems = cell.getCellItems();
+ SliceItem contentIntentItem = cell.getContentIntent();
+
+ int textCount = 0;
+ int imageCount = 0;
+ boolean added = false;
+ boolean singleItem = cellItems.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 = cellItems.stream().filter(new Predicate<SliceItem>() {
+ @Override
+ public boolean test(SliceItem s) {
+ return FORMAT_TEXT.equals(s.getFormat());
}
- }
- 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;
- }
+ }).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();
}
}
- if (added) {
- addView(cellContainer, new LayoutParams(0, WRAP_CONTENT, 1));
- if (actionItem != null) {
- cellContainer.setTag(actionItem);
- makeClickable(cellContainer);
+ }
+ for (int i = 0; i < cellItems.size(); i++) {
+ SliceItem item = cellItems.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, mTintColor, cellContainer, singleItem)) {
+ textCount++;
+ added = true;
+ }
+ } else if (imageCount < MAX_CELL_IMAGES && FORMAT_IMAGE.equals(item.getFormat())) {
+ if (addItem(item, mTintColor, cellContainer, singleItem)) {
+ imageCount++;
+ added = true;
}
}
- return imagesOnly;
- } else if (addItem(sliceItem, color, this, true)) {
- return FORMAT_IMAGE.equals(sliceItem.getFormat());
}
- return false;
+ if (added) {
+ mViewContainer.addView(cellContainer,
+ new LinearLayout.LayoutParams(0, WRAP_CONTENT, 1));
+ if (contentIntentItem != null) {
+ EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_BUTTON,
+ EventInfo.ROW_TYPE_GRID, mRowIndex);
+ info.setPosition(EventInfo.POSITION_CELL, index, total);
+ Pair<SliceItem, EventInfo> tagItem = new Pair(contentIntentItem, info);
+ cellContainer.setTag(tagItem);
+ makeClickable(cellContainer);
+ }
+ }
}
/**
@@ -305,7 +287,7 @@ public class GridRowView extends LinearLayout implements LargeSliceAdapter.Slice
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);
+ ? TITLE_TEXT_LAYOUT : TEXT_LAYOUT, null);
CharSequence text = FORMAT_TIMESTAMP.equals(format)
? SliceViewUtil.getRelativeTimeString(item.getTimestamp())
: item.getText();
@@ -315,7 +297,7 @@ public class GridRowView extends LinearLayout implements LargeSliceAdapter.Slice
} else if (FORMAT_IMAGE.equals(format)) {
ImageView iv = new ImageView(getContext());
iv.setImageIcon(item.getIcon());
- if (color != -1 && !item.hasHint(HINT_NO_TINT)) {
+ if (color != -1 && !item.hasHint(HINT_NO_TINT) && !item.hasHint(HINT_LARGE)) {
iv.setColorFilter(color);
}
int size = mIconSize;
@@ -337,18 +319,24 @@ public class GridRowView extends LinearLayout implements LargeSliceAdapter.Slice
@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);
- }
+ Pair<SliceItem, EventInfo> tagItem = (Pair<SliceItem, EventInfo>) view.getTag();
+ final SliceItem actionItem = tagItem.first;
+ final EventInfo info = tagItem.second;
+ if (actionItem != null && FORMAT_ACTION.equals(actionItem.getFormat())) {
+ try {
+ actionItem.getAction().send();
+ if (mObserver != null) {
+ mObserver.onSliceAction(info, actionItem);
}
- });
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG, "PendingIntent for slice cannot be sent", e);
+ }
}
}
+
+ @Override
+ public void resetView() {
+ mIsAllImages = true;
+ mViewContainer.removeAllViews();
+ }
}
diff --git a/androidx/app/slice/widget/LargeSliceAdapter.java b/androidx/app/slice/widget/LargeSliceAdapter.java
index b8f4bcf5..45f659dd 100644
--- a/androidx/app/slice/widget/LargeSliceAdapter.java
+++ b/androidx/app/slice/widget/LargeSliceAdapter.java
@@ -29,6 +29,7 @@ import android.content.Context;
import android.support.annotation.RestrictTo;
import android.support.v7.widget.RecyclerView;
import android.util.ArrayMap;
+import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -51,34 +52,53 @@ import androidx.app.slice.view.R;
@TargetApi(24)
public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.SliceViewHolder> {
- public static final int TYPE_DEFAULT = 1;
- 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;
+ static final int TYPE_DEFAULT = 1;
+ static final int TYPE_HEADER = 2; // TODO: headers shouldn't scroll off
+ static final int TYPE_GRID = 3;
+ static final int TYPE_MESSAGE = 4;
+ static final int TYPE_MESSAGE_LOCAL = 5;
private final IdGenerator mIdGen = new IdGenerator();
private final Context mContext;
private List<SliceWrapper> mSlices = new ArrayList<>();
- private SliceItem mColor;
+
+ private SliceView.OnSliceActionListener mSliceObserver;
+ private int mColor;
+ private AttributeSet mAttrs;
public LargeSliceAdapter(Context context) {
mContext = context;
setHasStableIds(true);
}
+ public void setSliceObserver(SliceView.OnSliceActionListener observer) {
+ mSliceObserver = observer;
+ }
+
/**
* Set the {@link SliceItem}'s to be displayed in the adapter and the accent color.
*/
- public void setSliceItems(List<SliceItem> slices, SliceItem color) {
+ public void setSliceItems(List<SliceItem> slices, int color) {
+ if (slices == null) {
+ mSlices.clear();
+ } else {
+ mIdGen.resetUsage();
+ mSlices = slices.stream().map(new Function<SliceItem, SliceWrapper>() {
+ @Override
+ public SliceWrapper apply(SliceItem s) {
+ return new SliceWrapper(s, mIdGen);
+ }
+ }).collect(Collectors.<SliceWrapper>toList());
+ }
mColor = color;
- mIdGen.resetUsage();
- mSlices = slices.stream().map(new Function<SliceItem, SliceWrapper>() {
- @Override
- public SliceWrapper apply(SliceItem s) {
- return new SliceWrapper(s, mIdGen);
- }
- }).collect(Collectors.<SliceWrapper>toList());
+ notifyDataSetChanged();
+ }
+
+ /**
+ * Sets the attribute set to use for views in the list.
+ */
+ public void setStyle(AttributeSet attrs) {
+ mAttrs = attrs;
notifyDataSetChanged();
}
@@ -108,8 +128,10 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.Sl
public void onBindViewHolder(SliceViewHolder holder, int position) {
SliceWrapper slice = mSlices.get(position);
if (holder.mSliceView != null) {
- holder.mSliceView.setColor(mColor);
- holder.mSliceView.setSliceItem(slice.mItem, position == 0 /* isHeader */);
+ holder.mSliceView.setTint(mColor);
+ holder.mSliceView.setStyle(mAttrs);
+ holder.mSliceView.setSliceItem(slice.mItem, position == 0 /* isHeader */,
+ position, mSliceObserver);
}
}
@@ -160,29 +182,14 @@ public class LargeSliceAdapter extends RecyclerView.Adapter<LargeSliceAdapter.Sl
* A {@link RecyclerView.ViewHolder} for presenting slices in {@link LargeSliceAdapter}.
*/
public static class SliceViewHolder extends RecyclerView.ViewHolder {
- public final SliceListView mSliceView;
+ public final SliceChildView mSliceView;
public SliceViewHolder(View itemView) {
super(itemView);
- mSliceView = itemView instanceof SliceListView ? (SliceListView) itemView : null;
+ mSliceView = itemView instanceof SliceChildView ? (SliceChildView) itemView : null;
}
}
- /**
- * View slices being displayed in {@link LargeSliceAdapter}.
- */
- public interface SliceListView {
- /**
- * Set the slice item for this view.
- */
- void setSliceItem(SliceItem slice, boolean isHeader);
-
- /**
- * Set the color for the items in this view.
- */
- void setColor(SliceItem color);
- }
-
private static class IdGenerator {
private long mNextLong = 0;
private final ArrayMap<String, Long> mCurrentIds = new ArrayMap<>();
diff --git a/androidx/app/slice/widget/LargeTemplateView.java b/androidx/app/slice/widget/LargeTemplateView.java
index d0e1364d..fa8397ee 100644
--- a/androidx/app/slice/widget/LargeTemplateView.java
+++ b/androidx/app/slice/widget/LargeTemplateView.java
@@ -16,31 +16,17 @@
package androidx.app.slice.widget;
-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.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.view.View;
-import android.widget.FrameLayout;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.Consumer;
+import android.util.AttributeSet;
import androidx.app.slice.Slice;
-import androidx.app.slice.SliceItem;
import androidx.app.slice.core.SliceQuery;
import androidx.app.slice.view.R;
@@ -49,7 +35,7 @@ import androidx.app.slice.view.R;
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@TargetApi(24)
-public class LargeTemplateView extends FrameLayout implements SliceView.SliceModeView {
+public class LargeTemplateView extends SliceChildView {
private final LargeSliceAdapter mAdapter;
private final RecyclerView mRecyclerView;
@@ -59,7 +45,6 @@ public class LargeTemplateView extends FrameLayout implements SliceView.SliceMod
public LargeTemplateView(Context context) {
super(context);
-
mRecyclerView = new RecyclerView(getContext());
mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
mAdapter = new LargeSliceAdapter(context);
@@ -69,13 +54,16 @@ public class LargeTemplateView extends FrameLayout implements SliceView.SliceMod
}
@Override
- public View getView() {
- return this;
+ public @SliceView.SliceMode int getMode() {
+ return SliceView.MODE_LARGE;
}
@Override
- public @SliceView.SliceMode int getMode() {
- return SliceView.MODE_LARGE;
+ public void setSliceActionListener(SliceView.OnSliceActionListener observer) {
+ mObserver = observer;
+ if (mAdapter != null) {
+ mAdapter.setSliceObserver(mObserver);
+ }
}
@Override
@@ -94,40 +82,22 @@ public class LargeTemplateView extends FrameLayout implements SliceView.SliceMod
@Override
public void setSlice(Slice slice) {
- SliceItem color = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR);
mSlice = slice;
- final List<SliceItem> items = new ArrayList<>();
- final boolean[] hasHeader = new boolean[1];
- if (SliceQuery.hasHints(slice, HINT_LIST)) {
- addList(slice, items);
- } else {
- slice.getItems().forEach(new Consumer<SliceItem>() {
- @Override
- public void accept(SliceItem item) {
- if (item.hasAnyHints(HINT_ACTIONS, HINT_SUMMARY)) {
- return;
- } else if (FORMAT_INT.equals(item.getFormat())) {
- return;
- } else if (FORMAT_SLICE.equals(item.getFormat())
- && item.hasHint(HINT_LIST)) {
- addList(item.getSlice(), items);
- } else if (item.hasHint(HINT_LIST_ITEM)) {
- items.add(item);
- } else if (!hasHeader[0]) {
- hasHeader[0] = true;
- items.add(0, item);
- } else {
- items.add(item);
- }
- }
- });
- }
- mAdapter.setSliceItems(items, color);
+ populate();
+ }
+
+ @Override
+ public void setStyle(AttributeSet attrs) {
+ super.setStyle(attrs);
+ mAdapter.setStyle(attrs);
}
- private void addList(Slice slice, List<SliceItem> items) {
- List<SliceItem> sliceItems = slice.getItems();
- items.addAll(sliceItems);
+ private void populate() {
+ if (mSlice == null) {
+ return;
+ }
+ ListContent lc = new ListContent(mSlice);
+ mAdapter.setSliceItems(lc.getRowItems(), mTintColor);
}
/**
@@ -137,4 +107,10 @@ public class LargeTemplateView extends FrameLayout implements SliceView.SliceMod
// TODO -- restrict / enable how much this view can show
mIsScrollable = isScrollable;
}
+
+ @Override
+ public void resetView() {
+ mSlice = null;
+ mAdapter.setSliceItems(null, -1);
+ }
}
diff --git a/androidx/app/slice/widget/ListContent.java b/androidx/app/slice/widget/ListContent.java
new file mode 100644
index 00000000..86e9409b
--- /dev/null
+++ b/androidx/app/slice/widget/ListContent.java
@@ -0,0 +1,187 @@
+/*
+ * 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_ACTIONS;
+import static android.app.slice.Slice.HINT_LIST;
+import static android.app.slice.Slice.HINT_LIST_ITEM;
+import static android.app.slice.Slice.SUBTYPE_COLOR;
+import static android.app.slice.SliceItem.FORMAT_ACTION;
+import static android.app.slice.SliceItem.FORMAT_INT;
+import static android.app.slice.SliceItem.FORMAT_SLICE;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceItem;
+import androidx.app.slice.core.SliceHints;
+import androidx.app.slice.core.SliceQuery;
+
+/**
+ * Extracts information required to present content in a list format from a slice.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class ListContent {
+
+ private SliceItem mColorItem;
+ private SliceItem mSummaryItem;
+ private ArrayList<SliceItem> mRowItems = new ArrayList<>();
+ private boolean mHasHeader;
+
+ public ListContent(Slice slice) {
+ populate(slice);
+ }
+
+ /**
+ * Resets the content.
+ */
+ public void reset() {
+ mColorItem = null;
+ mSummaryItem = null;
+ mRowItems.clear();
+ mHasHeader = false;
+ }
+
+ /**
+ * @return whether this row has content that is valid to display.
+ */
+ public boolean populate(Slice slice) {
+ reset();
+ mColorItem = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR);
+ // Find summary
+ SliceItem summaryItem = getSummaryItem(slice);
+ mSummaryItem = summaryItem;
+ // Filter + create row items
+ List<SliceItem> children = slice.getItems();
+ for (int i = 0; i < children.size(); i++) {
+ final SliceItem child = children.get(i);
+ final String format = child.getFormat();
+ if (!child.hasAnyHints(SliceHints.HINT_SUMMARY, HINT_ACTIONS)
+ && (FORMAT_ACTION.equals(format) || FORMAT_SLICE.equals(format))) {
+ if (!mHasHeader && !child.hasHint(HINT_LIST_ITEM)) {
+ mHasHeader = true;
+ mRowItems.add(0, child);
+ } else {
+ mRowItems.add(child);
+ }
+ }
+ }
+ return isValid();
+ }
+
+ /**
+ * @return whether this list has content that is valid to display.
+ */
+ public boolean isValid() {
+ return mSummaryItem != null
+ || mRowItems.size() > 0;
+ }
+
+ @Nullable
+ public SliceItem getColorItem() {
+ return mColorItem;
+ }
+
+ @Nullable
+ public SliceItem getSummaryItem() {
+ return mSummaryItem;
+ }
+
+ public ArrayList<SliceItem> getRowItems() {
+ return mRowItems;
+ }
+
+ /**
+ * @return whether this list has a header or not.
+ */
+ public boolean hasHeader() {
+ return mHasHeader;
+ }
+
+ /**
+ * @return A slice item of format slice that is hinted to be shown when the slice is in small
+ * format, or is the best option if nothing is appropriately hinted.
+ */
+ private static SliceItem getSummaryItem(@NonNull Slice slice) {
+ List<SliceItem> items = slice.getItems();
+ // See if a summary is specified
+ SliceItem summary = SliceQuery.find(slice, FORMAT_SLICE, SliceHints.HINT_SUMMARY, null);
+ if (summary != null) {
+ return summary;
+ }
+ // 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
+ if (firstSlice.hasHint(HINT_LIST)) {
+ // Check for header, use that if it exists
+ SliceItem listHeader = SliceQuery.find(firstSlice, FORMAT_SLICE,
+ null,
+ new String[] {
+ HINT_LIST_ITEM, HINT_LIST
+ });
+ if (listHeader != null) {
+ return findFirstSlice(listHeader);
+ } else {
+ // Otherwise use the first list item
+ SliceItem newFirst = firstSlice.getSlice().getItems().get(0);
+ return findFirstSlice(newFirst);
+ }
+ } else {
+ // Not a list, find first slice with non-slice children
+ return findFirstSlice(firstSlice);
+ }
+ }
+ // 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);
+ }
+
+ /**
+ * @return Finds the first slice that has non-slice children.
+ */
+ private static SliceItem findFirstSlice(SliceItem slice) {
+ if (!FORMAT_SLICE.equals(slice.getFormat())) {
+ return slice;
+ }
+ List<SliceItem> items = slice.getSlice().getItems();
+ for (int i = 0; i < items.size(); i++) {
+ if (FORMAT_SLICE.equals(items.get(i).getFormat())) {
+ SliceItem childSlice = items.get(i);
+ return findFirstSlice(childSlice);
+ } else {
+ // Doesn't have slice children so return it
+ return slice;
+ }
+ }
+ // Slices all the way down, just return it
+ return slice;
+ }
+}
diff --git a/androidx/app/slice/widget/MessageView.java b/androidx/app/slice/widget/MessageView.java
index e2678e16..9c4a7053 100644
--- a/androidx/app/slice/widget/MessageView.java
+++ b/androidx/app/slice/widget/MessageView.java
@@ -27,14 +27,13 @@ import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.support.annotation.RestrictTo;
import android.text.SpannableStringBuilder;
-import android.util.AttributeSet;
import android.util.TypedValue;
import android.widget.ImageView;
-import android.widget.LinearLayout;
import android.widget.TextView;
import java.util.function.Consumer;
+import androidx.app.slice.Slice;
import androidx.app.slice.SliceItem;
import androidx.app.slice.core.SliceQuery;
@@ -43,13 +42,30 @@ import androidx.app.slice.core.SliceQuery;
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@TargetApi(24)
-public class MessageView extends LinearLayout implements LargeSliceAdapter.SliceListView {
+public class MessageView extends SliceChildView {
private TextView mDetails;
private ImageView mIcon;
- public MessageView(Context context, AttributeSet attrs) {
- super(context, attrs);
+ private int mRowIndex;
+
+ public MessageView(Context context) {
+ super(context);
+ }
+
+ @Override
+ public int getMode() {
+ return SliceView.MODE_LARGE;
+ }
+
+ @Override
+ public void setSlice(Slice slice) {
+ // Do nothing it's always a list item
+ }
+
+ @Override
+ public void resetView() {
+ // TODO
}
@Override
@@ -60,12 +76,15 @@ public class MessageView extends LinearLayout implements LargeSliceAdapter.Slice
}
@Override
- public void setSliceItem(SliceItem slice, boolean isHeader) {
+ public void setSliceItem(SliceItem slice, boolean isHeader, int index,
+ SliceView.OnSliceActionListener observer) {
+ setSliceActionListener(observer);
+ mRowIndex = index;
SliceItem source = SliceQuery.findSubtype(slice, FORMAT_IMAGE, SUBTYPE_SOURCE);
if (source != null) {
final int iconSize = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
24, getContext().getResources().getDisplayMetrics());
- // TODO try and turn this into a drawable
+ // TODO: try and turn this into a drawable
Bitmap iconBm = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888);
Canvas iconCanvas = new Canvas(iconBm);
Drawable d = source.getIcon().loadDrawable(getContext());
@@ -85,10 +104,4 @@ public class MessageView extends LinearLayout implements LargeSliceAdapter.Slice
});
mDetails.setText(builder.toString());
}
-
- @Override
- public void setColor(SliceItem color) {
-
- }
-
}
diff --git a/androidx/app/slice/widget/RowContent.java b/androidx/app/slice/widget/RowContent.java
new file mode 100644
index 00000000..0e730a5e
--- /dev/null
+++ b/androidx/app/slice/widget/RowContent.java
@@ -0,0 +1,246 @@
+/*
+ * 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_TITLE;
+import static android.app.slice.Slice.SUBTYPE_SLIDER;
+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_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.Nullable;
+import android.support.annotation.RestrictTo;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.app.slice.SliceItem;
+import androidx.app.slice.core.SliceHints;
+import androidx.app.slice.core.SliceQuery;
+
+/**
+ * Extracts information required to present content in a row format from a slice.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+public class RowContent {
+ private static final String TAG = "RowContent";
+
+ private SliceItem mContentIntent;
+ private SliceItem mStartItem;
+ private SliceItem mTitleItem;
+ private SliceItem mSubtitleItem;
+ private ArrayList<SliceItem> mEndItems = new ArrayList<>();
+ private boolean mEndItemsContainAction;
+ private SliceItem mSlider;
+
+ public RowContent(SliceItem rowSlice, boolean showStartItem) {
+ populate(rowSlice, showStartItem);
+ }
+
+ /**
+ * Resets the content.
+ */
+ public void reset() {
+ mContentIntent = null;
+ mStartItem = null;
+ mTitleItem = null;
+ mSubtitleItem = null;
+ mEndItems.clear();
+ }
+
+ /**
+ * @return whether this row has content that is valid to display.
+ */
+ public boolean populate(SliceItem rowSlice, boolean showStartItem) {
+ reset();
+ if (!isValidRow(rowSlice)) {
+ Log.w(TAG, "Provided SliceItem is invalid for RowContent");
+ return false;
+ }
+ // Filter anything not viable for displaying in a row
+ ArrayList<SliceItem> rowItems = filterInvalidItems(rowSlice);
+ // If we've only got one item that's a slice / action use those items instead
+ if (rowItems.size() == 1 && (FORMAT_ACTION.equals(rowItems.get(0).getFormat())
+ || FORMAT_SLICE.equals(rowItems.get(0).getFormat()))) {
+ if (isValidRow(rowItems.get(0))) {
+ rowSlice = rowItems.get(0);
+ rowItems = filterInvalidItems(rowSlice);
+ }
+ }
+ // Content intent
+ if (FORMAT_ACTION.equals(rowSlice.getFormat())) {
+ mContentIntent = rowSlice;
+ }
+ if (SUBTYPE_SLIDER.equals(rowSlice.getSubType())) {
+ mSlider = rowSlice;
+ }
+ if (rowItems.size() > 0) {
+ // Start item
+ if (isStartType(rowItems.get(0))) {
+ if (showStartItem) {
+ mStartItem = rowItems.get(0);
+ }
+ rowItems.remove(0);
+ }
+ // Text + end items
+ ArrayList<SliceItem> endItems = new ArrayList<>();
+ for (int i = 0; i < rowItems.size(); i++) {
+ final SliceItem item = rowItems.get(i);
+ if (FORMAT_TEXT.equals(item.getFormat())) {
+ if ((mTitleItem == null || !mTitleItem.hasHint(HINT_TITLE))
+ && item.hasHint(HINT_TITLE)) {
+ mTitleItem = item;
+ } else if (mSubtitleItem == null) {
+ mSubtitleItem = item;
+ }
+ } else {
+ endItems.add(item);
+ }
+ }
+ // Special rules for end items: only one timestamp, can't be mixture of icons / actions
+ boolean hasTimestamp = mStartItem != null
+ && FORMAT_TIMESTAMP.equals(mStartItem.getFormat());
+ String desiredFormat = null;
+ for (int i = 0; i < endItems.size(); i++) {
+ final SliceItem item = endItems.get(i);
+ if (FORMAT_TIMESTAMP.equals(item.getFormat())) {
+ if (!hasTimestamp) {
+ hasTimestamp = true;
+ mEndItems.add(item);
+ }
+ } else if (desiredFormat == null) {
+ desiredFormat = item.getFormat();
+ mEndItems.add(item);
+ } else if (desiredFormat.equals(item.getFormat())) {
+ mEndItems.add(item);
+ mEndItemsContainAction |= FORMAT_ACTION.equals(item.getFormat());
+ }
+ }
+ }
+ return isValid();
+ }
+
+ /**
+ * @return the {@link SliceItem} representing the slider in this row; can be null
+ */
+ @Nullable
+ public SliceItem getSlider() {
+ return mSlider;
+ }
+
+ /**
+ * @return whether this row has content that is valid to display.
+ */
+ public boolean isValid() {
+ return mStartItem != null
+ || mTitleItem != null
+ || mSubtitleItem != null
+ || mEndItems.size() > 0;
+ }
+
+ @Nullable
+ public SliceItem getContentIntent() {
+ return mContentIntent;
+ }
+
+ @Nullable
+ public SliceItem getStartItem() {
+ return mStartItem;
+ }
+
+ @Nullable
+ public SliceItem getTitleItem() {
+ return mTitleItem;
+ }
+
+ @Nullable
+ public SliceItem getSubtitleItem() {
+ return mSubtitleItem;
+ }
+
+ public ArrayList<SliceItem> getEndItems() {
+ return mEndItems;
+ }
+
+ /**
+ * @return whether {@link #getEndItems()} contains a SliceItem with FORMAT_ACTION
+ */
+ public boolean endItemsContainAction() {
+ return mEndItemsContainAction;
+ }
+
+ /**
+ * @return whether this is a valid item to use to populate a row of content.
+ */
+ private static boolean isValidRow(SliceItem rowSlice) {
+ // Must be slice or action
+ if (FORMAT_SLICE.equals(rowSlice.getFormat())
+ || FORMAT_ACTION.equals(rowSlice.getFormat())) {
+ // Must have at least one legitimate child
+ List<SliceItem> rowItems = rowSlice.getSlice().getItems();
+ for (int i = 0; i < rowItems.size(); i++) {
+ if (isValidRowContent(rowSlice, rowItems.get(i))) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private static ArrayList<SliceItem> filterInvalidItems(SliceItem rowSlice) {
+ ArrayList<SliceItem> filteredList = new ArrayList<>();
+ for (SliceItem i : rowSlice.getSlice().getItems()) {
+ if (isValidRowContent(rowSlice, i)) {
+ filteredList.add(i);
+ }
+ }
+ return filteredList;
+ }
+
+ /**
+ * @return whether this item has valid content to display in a row.
+ */
+ private static boolean isValidRowContent(SliceItem slice, SliceItem item) {
+ // TODO -- filter for shortcut once that's in
+ final String itemFormat = item.getFormat();
+ // Must be a format that is presentable
+ return FORMAT_TEXT.equals(itemFormat)
+ || FORMAT_IMAGE.equals(itemFormat)
+ || FORMAT_TIMESTAMP.equals(itemFormat)
+ || FORMAT_REMOTE_INPUT.equals(itemFormat)
+ || FORMAT_ACTION.equals(itemFormat)
+ || (FORMAT_INT.equals(itemFormat) && SUBTYPE_SLIDER.equals(slice.getSubType()));
+ }
+
+ /**
+ * @return Whether this item is appropriate to be considered a "start" item, i.e. go in the
+ * front slot of a row.
+ */
+ private static boolean isStartType(SliceItem item) {
+ final String type = item.getFormat();
+ return (!item.hasHint(SliceHints.SUBTYPE_TOGGLE)
+ && (FORMAT_ACTION.equals(type) && (SliceQuery.find(item, FORMAT_IMAGE) != null)))
+ || FORMAT_IMAGE.equals(type)
+ || FORMAT_TIMESTAMP.equals(type);
+ }
+}
diff --git a/androidx/app/slice/widget/RowView.java b/androidx/app/slice/widget/RowView.java
index e7cd535f..47a8045e 100644
--- a/androidx/app/slice/widget/RowView.java
+++ b/androidx/app/slice/widget/RowView.java
@@ -16,21 +16,20 @@
package androidx.app.slice.widget;
-import static android.app.slice.Slice.HINT_LIST;
-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_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_SLIDER_VALUE;
import static androidx.app.slice.core.SliceHints.EXTRA_TOGGLE_STATE;
-import static androidx.app.slice.core.SliceHints.HINT_SUMMARY;
+import static androidx.app.slice.core.SliceHints.SUBTYPE_MAX;
+import static androidx.app.slice.core.SliceHints.SUBTYPE_PROGRESS;
+import static androidx.app.slice.core.SliceHints.SUBTYPE_TOGGLE;
+import static androidx.app.slice.widget.SliceView.MODE_LARGE;
+import static androidx.app.slice.widget.SliceView.MODE_SMALL;
import android.annotation.TargetApi;
import android.app.PendingIntent;
@@ -38,25 +37,25 @@ 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.ColorInt;
import android.support.annotation.RestrictTo;
import android.util.Log;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.CompoundButton;
-import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
+import android.widget.ProgressBar;
+import android.widget.SeekBar;
import android.widget.Switch;
import android.widget.TextView;
import android.widget.ToggleButton;
import java.util.ArrayList;
import java.util.List;
-import java.util.function.Predicate;
import androidx.app.slice.Slice;
import androidx.app.slice.SliceItem;
-import androidx.app.slice.core.SliceHints;
import androidx.app.slice.core.SliceQuery;
import androidx.app.slice.view.R;
@@ -68,29 +67,31 @@ import androidx.app.slice.view.R;
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@TargetApi(23)
-public class RowView extends FrameLayout implements SliceView.SliceModeView,
- LargeSliceAdapter.SliceListView, View.OnClickListener {
+public class RowView extends SliceChildView implements View.OnClickListener {
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 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 ArrayList<CompoundButton> mToggles = new ArrayList<>();
private LinearLayout mEndContainer;
+ private SeekBar mSeekBar;
+ private ProgressBar mProgressBar;
- private SliceItem mColorItem;
+ private boolean mInSmallMode;
+ private int mRowIndex;
+ private RowContent mRowContent;
private SliceItem mRowAction;
+ private boolean mIsHeader;
+
+ private int mIconSize;
+ private int mPadding;
public RowView(Context context) {
super(context);
@@ -104,31 +105,37 @@ public class RowView extends FrameLayout implements SliceView.SliceModeView,
mSecondaryText = (TextView) findViewById(android.R.id.summary);
mDivider = findViewById(R.id.divider);
mEndContainer = (LinearLayout) findViewById(android.R.id.widget_frame);
- }
-
- @Override
- public View getView() {
- return this;
+ mSeekBar = (SeekBar) findViewById(R.id.seek_bar);
+ mProgressBar = (ProgressBar) findViewById(R.id.progress_bar);
}
@Override
public @SliceView.SliceMode int getMode() {
- return SliceView.MODE_SMALL;
+ return mInSmallMode ? MODE_SMALL : MODE_LARGE;
}
@Override
- public void setColor(SliceItem color) {
- mColorItem = color;
+ public void setTint(@ColorInt int tintColor) {
+ super.setTint(tintColor);
+ if (mRowContent != null) {
+ // TODO -- can be smarter about this
+ resetView();
+ populateViews();
+ }
}
/**
* This is called when RowView is being used as a component in a large template.
*/
@Override
- public void setSliceItem(SliceItem slice, boolean isHeader) {
- mIsHeader = isHeader;
+ public void setSliceItem(SliceItem slice, boolean isHeader, int index,
+ SliceView.OnSliceActionListener observer) {
+ setSliceActionListener(observer);
mInSmallMode = false;
- populateViews(slice, slice);
+ mRowIndex = index;
+ mIsHeader = isHeader;
+ mRowContent = new RowContent(slice, !mIsHeader /* showStartItem */);
+ populateViews();
}
/**
@@ -137,208 +144,144 @@ public class RowView extends FrameLayout implements SliceView.SliceModeView,
@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), getSummaryItem(slice));
+ mRowIndex = 0;
+ mIsHeader = true;
+ ListContent lc = new ListContent(slice);
+ mRowContent = new RowContent(lc.getSummaryItem(), false /* showStartItem */);
+ populateViews();
}
- private SliceItem getSummaryItem(Slice slice) {
- List<SliceItem> items = slice.getItems();
- // 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
- if (firstSlice.hasHint(HINT_LIST)) {
- // Check for header, use that if it exists
- SliceItem listHeader = SliceQuery.find(firstSlice, FORMAT_SLICE,
- null,
- new String[] {
- HINT_LIST_ITEM, HINT_LIST
- });
- if (listHeader != null) {
- return SliceQuery.findFirstSlice(listHeader);
- } else {
- // Otherwise use the first list item
- SliceItem newFirst = firstSlice.getSlice().getItems().get(0);
- return SliceQuery.findFirstSlice(newFirst);
- }
- } else {
- // Not a list, find first slice with non-slice children
- return SliceQuery.findFirstSlice(firstSlice);
- }
- }
- // 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);
- }
-
- @TargetApi(24)
- private void populateViews(SliceItem fullSlice, SliceItem sliceItem) {
- resetViews();
- ArrayList<SliceItem> items = new ArrayList<>();
- if (FORMAT_SLICE.equals(sliceItem.getFormat())) {
- items = new ArrayList<>(sliceItem.getSlice().getItems());
- } else {
- items.add(sliceItem);
- }
-
- // These are the things that can go in our small template
- SliceItem startItem = null;
- SliceItem titleItem = null;
- SliceItem subTitle = null;
- ArrayList<SliceItem> endItems = new ArrayList<>();
-
- // 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())) {
- if (!SliceQuery.isSimpleAction(firstSlice)) {
- mRowAction = firstSlice;
- items.remove(0);
- // Populating with first action, bias to use slice associated with this action
- items.addAll(0, mRowAction.getSlice().getItems());
- }
- }
-
- // Look through our items and try to figure out main content
- for (int i = 0; i < items.size(); i++) {
- SliceItem item = items.get(i);
- List<String> hints = item.getHints();
- String itemType = item.getFormat();
- 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)) {
- startItem = item;
- } else if ((titleItem == null || !titleItem.hasHint(HINT_TITLE))
- && FORMAT_TEXT.equals(itemType)) {
- titleItem = item;
- } else {
- endItems.add(item);
- }
- } else if (FORMAT_TEXT.equals(item.getFormat())) {
- if (titleItem == null) {
- titleItem = item;
- } else if (subTitle == null) {
- subTitle = item;
- } else {
- endItems.add(item);
- }
- } else if (FORMAT_SLICE.equals(item.getFormat())) {
- List<SliceItem> subItems = item.getSlice().getItems();
- for (int j = 0; j < subItems.size(); j++) {
- endItems.add(subItems.get(j));
- }
- } else {
- endItems.add(item);
- }
+ private void populateViews() {
+ resetView();
+ boolean showStart = false;
+ final SliceItem startItem = mRowContent.getStartItem();
+ if (startItem != null) {
+ final EventInfo info = new EventInfo(getMode(),
+ EventInfo.ACTION_TYPE_BUTTON,
+ EventInfo.ROW_TYPE_LIST, mRowIndex);
+ info.setPosition(EventInfo.POSITION_START, 0, 1);
+ showStart = addItem(startItem, mTintColor, true /* isStart */, 0 /* padding */, info);
}
+ mStartContainer.setVisibility(showStart ? View.VISIBLE : View.GONE);
- 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 (!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);
+ final SliceItem titleItem = mRowContent.getTitleItem();
if (titleItem != null) {
mPrimaryText.setText(titleItem.getText());
}
mPrimaryText.setVisibility(titleItem != null ? View.VISIBLE : View.GONE);
+
+ final SliceItem subTitle = mRowContent.getSubtitleItem();
if (subTitle != null) {
mSecondaryText.setText(subTitle.getText());
}
mSecondaryText.setVisibility(subTitle != null ? View.VISIBLE : View.GONE);
- // 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.SUBTYPE_TOGGLE) && addToggle(mRowAction, color)) {
- // Can't show more end actions if we have a toggle so we're done
- makeClickable(this);
+ final SliceItem slider = mRowContent.getSlider();
+ if (slider != null) {
+ addSlider(slider);
return;
}
- // Check if we have a toggle somewhere in our end items
- SliceItem toggleItem = endItems.stream()
- .filter(new Predicate<SliceItem>() {
- @Override
- public boolean test(SliceItem item) {
- return FORMAT_ACTION.equals(item.getFormat())
- && SliceQuery.hasHints(item.getSlice(), SliceHints.SUBTYPE_TOGGLE);
- }
- }).findFirst().orElse(null);
- if (toggleItem != null) {
- 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;
- }
+
+ mRowAction = mRowContent.getContentIntent();
+ ArrayList<SliceItem> endItems = mRowContent.getEndItems();
+ boolean hasRowAction = mRowAction != null;
+ if (endItems.isEmpty()) {
+ if (hasRowAction) setViewClickable(this, true);
+ return;
}
- boolean clickableEndItem = false;
+
+ // If we're here we might be able to show end items
int itemCount = 0;
+ // Prefer to show actions as end items if possible; fall back to the first format type.
+ String desiredFormat = mRowContent.endItemsContainAction()
+ ? FORMAT_ACTION : endItems.get(0).getFormat();
+ boolean firstItemIsADefaultToggle = false;
for (int i = 0; i < endItems.size(); i++) {
- SliceItem item = endItems.get(i);
+ final SliceItem endItem = endItems.get(i);
+ final String endFormat = endItem.getFormat();
// 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)) {
+ && (desiredFormat.equals(endFormat)
+ || FORMAT_TIMESTAMP.equals(endFormat))) {
+ final EventInfo info = new EventInfo(getMode(),
+ EventInfo.ACTION_TYPE_BUTTON,
+ EventInfo.ROW_TYPE_LIST, mRowIndex);
+ info.setPosition(EventInfo.POSITION_END, i,
+ Math.min(endItems.size(), MAX_END_ITEMS));
+ if (addItem(endItem, mTintColor, false /* isStart */, mPadding, info)) {
itemCount++;
+ if (itemCount == 1) {
+ firstItemIsADefaultToggle = !mToggles.isEmpty()
+ && SliceQuery.find(endItem.getSlice(), FORMAT_IMAGE) == null;
+ }
}
}
}
- if (mRowAction != null) {
- makeClickable(clickableEndItem ? mContent : this);
+
+ boolean hasEndItemAction = FORMAT_ACTION.contentEquals(desiredFormat);
+ // If there is a row action and the first end item is a default toggle, show the divider.
+ mDivider.setVisibility(hasRowAction && firstItemIsADefaultToggle
+ ? View.VISIBLE : View.GONE);
+ if (hasRowAction) {
+ if (itemCount > 0 && hasEndItemAction) {
+ setViewClickable(mContent, true);
+ } else {
+ setViewClickable(this, true);
+ }
+ } else {
+ // If the only end item is an action, make the whole row clickable.
+ if (mRowContent.endItemsContainAction() && itemCount == 1) {
+ setViewClickable(this, true);
+ }
}
}
- /**
- * @return Whether a toggle was added.
- */
- private boolean addToggle(final SliceItem toggleItem, int color) {
- if (!FORMAT_ACTION.equals(toggleItem.getFormat())
- || !SliceQuery.hasHints(toggleItem.getSlice(), SliceHints.SUBTYPE_TOGGLE)) {
- return false;
+ private void addSlider(final SliceItem slider) {
+ final ProgressBar progressBar;
+ if (FORMAT_ACTION.equals(slider.getFormat())) {
+ // Seek bar
+ progressBar = mSeekBar;
+ mSeekBar.setVisibility(View.VISIBLE);
+ SliceItem thumb = SliceQuery.find(slider, FORMAT_IMAGE);
+ if (thumb != null) {
+ mSeekBar.setThumb(thumb.getIcon().loadDrawable(getContext()));
+ }
+ mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ try {
+ PendingIntent pi = slider.getAction();
+ Intent i = new Intent().putExtra(EXTRA_SLIDER_VALUE, progress);
+ // TODO: sending this PendingIntent should be rate limited.
+ pi.send(getContext(), 0, i, null, null);
+ } catch (CanceledException e) { }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) { }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) { }
+ });
+ } else {
+ // Progress bar
+ progressBar = mProgressBar;
+ mProgressBar.setVisibility(View.VISIBLE);
+ }
+ SliceItem max = SliceQuery.findSubtype(slider, FORMAT_INT, SUBTYPE_MAX);
+ if (max != null) {
+ progressBar.setMax(max.getInt());
+ }
+ SliceItem progress = SliceQuery.findSubtype(slider, FORMAT_INT, SUBTYPE_PROGRESS);
+ if (progress != null) {
+ progressBar.setProgress(progress.getInt());
}
+ }
+ /**
+ * Add a toggle view to container.
+ */
+ private void addToggle(final SliceItem toggleItem, int color, ViewGroup container) {
// Check if this is a custom toggle
Icon checkedIcon = null;
List<SliceItem> sliceItems = toggleItem.getSlice().getItems();
@@ -347,51 +290,63 @@ public class RowView extends FrameLayout implements SliceView.SliceModeView,
? sliceItems.get(0).getIcon()
: null;
}
+ final CompoundButton toggle;
if (checkedIcon != null) {
if (color != -1) {
- // TODO - Should these be tinted? What if the app wants diff colors per state?
+ // TODO - Should custom toggle buttons 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();
+ toggle = new ToggleButton(getContext());
+ ((ToggleButton) toggle).setTextOff("");
+ ((ToggleButton) toggle).setTextOn("");
+ toggle.setBackground(checkedIcon.loadDrawable(getContext()));
+ container.addView(toggle);
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) toggle.getLayoutParams();
lp.width = mIconSize;
lp.height = mIconSize;
} else {
- mToggle = new Switch(getContext());
- mEndContainer.addView(mToggle);
+ toggle = new Switch(getContext());
+ container.addView(toggle);
}
- mToggle.setChecked(SliceQuery.hasHints(toggleItem.getSlice(), HINT_SELECTED));
- mToggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
+ toggle.setChecked(SliceQuery.hasHints(toggleItem.getSlice(), HINT_SELECTED));
+ toggle.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
try {
PendingIntent pi = toggleItem.getAction();
Intent i = new Intent().putExtra(EXTRA_TOGGLE_STATE, isChecked);
pi.send(getContext(), 0, i, null, null);
+ if (mObserver != null) {
+ final EventInfo info = new EventInfo(getMode(),
+ EventInfo.ACTION_TYPE_TOGGLE,
+ EventInfo.ROW_TYPE_LIST, mRowIndex);
+ info.state = isChecked ? EventInfo.STATE_ON : EventInfo.STATE_OFF;
+ mObserver.onSliceAction(info, toggleItem);
+ }
} catch (CanceledException e) {
- mToggle.setSelected(!isChecked);
+ toggle.setSelected(!isChecked);
}
}
});
- return true;
+ mToggles.add(toggle);
}
/**
* 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 addItem(SliceItem sliceItem, int color, LinearLayout container, int padding) {
+ private boolean addItem(SliceItem sliceItem, int color, boolean isStart, int padding,
+ final EventInfo info) {
SliceItem image = null;
SliceItem action = null;
SliceItem timeStamp = null;
- if (FORMAT_ACTION.equals(sliceItem.getFormat())
- && !sliceItem.hasHint(SliceHints.SUBTYPE_TOGGLE)) {
+ ViewGroup container = isStart ? mStartContainer : mEndContainer;
+ if (FORMAT_ACTION.equals(sliceItem.getFormat())) {
+ if (SliceQuery.hasHints(sliceItem.getSlice(), SUBTYPE_TOGGLE)) {
+ addToggle(sliceItem, color, container);
+ return true;
+ }
image = SliceQuery.find(sliceItem.getSlice(), FORMAT_IMAGE);
timeStamp = SliceQuery.find(sliceItem.getSlice(), FORMAT_TIMESTAMP);
action = sliceItem;
@@ -424,16 +379,14 @@ public class RowView extends FrameLayout implements SliceView.SliceModeView,
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();
- }
+ try {
+ sliceAction.getAction().send();
+ if (mObserver != null) {
+ mObserver.onSliceAction(info, sliceAction);
}
- });
+ } catch (CanceledException e) {
+ e.printStackTrace();
+ }
}
});
addedView.setBackground(SliceViewUtil.getDrawable(getContext(),
@@ -446,33 +399,41 @@ public class RowView extends FrameLayout implements SliceView.SliceModeView,
public void onClick(View view) {
if (mRowAction != null && FORMAT_ACTION.equals(mRowAction.getFormat())) {
// Check for a row action
- AsyncTask.execute(new Runnable() {
- @Override
- public void run() {
- try {
- mRowAction.getAction().send();
- } catch (CanceledException e) {
- Log.w(TAG, "PendingIntent for slice cannot be sent", e);
- }
+ try {
+ mRowAction.getAction().send();
+ if (mObserver != null) {
+ EventInfo info = new EventInfo(getMode(), EventInfo.ACTION_TYPE_CONTENT,
+ EventInfo.ROW_TYPE_LIST, mRowIndex);
+ mObserver.onSliceAction(info, mRowAction);
}
- });
- } else if (mToggle != null) {
- // Or no row action so let's just toggle if we've got one
- mToggle.toggle();
+ } catch (CanceledException e) {
+ Log.w(TAG, "PendingIntent for slice cannot be sent", e);
+ }
+ } else if (mToggles.size() == 1) {
+ // If there is only one toggle and no row action, just toggle it.
+ mToggles.get(0).toggle();
}
}
- private void makeClickable(View layout) {
- layout.setOnClickListener(this);
- layout.setBackground(SliceViewUtil.getDrawable(getContext(),
- android.R.attr.selectableItemBackground));
+ private void setViewClickable(View layout, boolean isClickable) {
+ layout.setOnClickListener(isClickable ? this : null);
+ layout.setBackground(isClickable ? SliceViewUtil.getDrawable(getContext(),
+ android.R.attr.selectableItemBackground) : null);
+ layout.setClickable(isClickable);
}
- private void resetViews() {
+ @Override
+ public void resetView() {
+ setViewClickable(this, false);
+ setViewClickable(mContent, false);
mStartContainer.removeAllViews();
mEndContainer.removeAllViews();
mPrimaryText.setText(null);
mSecondaryText.setText(null);
+ mToggles.clear();
+ mRowAction = null;
mDivider.setVisibility(View.GONE);
+ mSeekBar.setVisibility(View.GONE);
+ mProgressBar.setVisibility(View.GONE);
}
}
diff --git a/androidx/app/slice/widget/ShortcutView.java b/androidx/app/slice/widget/ShortcutView.java
index 1b88d545..75e97b10 100644
--- a/androidx/app/slice/widget/ShortcutView.java
+++ b/androidx/app/slice/widget/ShortcutView.java
@@ -23,6 +23,7 @@ import static android.app.slice.Slice.SUBTYPE_SOURCE;
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 android.annotation.TargetApi;
@@ -34,14 +35,12 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ProviderInfo;
import android.content.res.Resources;
-import android.graphics.Color;
import android.graphics.drawable.Drawable;
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 android.widget.ImageView;
import androidx.app.slice.Slice;
import androidx.app.slice.SliceItem;
@@ -53,12 +52,13 @@ import androidx.app.slice.view.R;
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@TargetApi(23)
-public class ShortcutView extends FrameLayout implements SliceView.SliceModeView {
+public class ShortcutView extends SliceChildView {
private static final String TAG = "ShortcutView";
+ private Slice mSlice;
private Uri mUri;
- private PendingIntent mAction;
+ private SliceItem mActionItem;
private SliceItem mLabel;
private SliceItem mIcon;
@@ -73,31 +73,27 @@ public class ShortcutView extends FrameLayout implements SliceView.SliceModeView
}
@Override
- public View getView() {
- return this;
- }
-
- @Override
public void setSlice(Slice slice) {
- mLabel = null;
- mIcon = null;
- mAction = null;
- removeAllViews();
+ resetView();
+ mSlice = slice;
determineShortcutItems(getContext(), slice);
SliceItem colorItem = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR);
if (colorItem == null) {
colorItem = SliceQuery.findSubtype(slice, FORMAT_INT, SUBTYPE_COLOR);
}
- // TODO: pick better default colour
- final int color = colorItem != null ? colorItem.getInt() : Color.GRAY;
+ final int color = colorItem != null
+ ? colorItem.getInt()
+ : SliceViewUtil.getColorAccent(getContext());
ShapeDrawable circle = new ShapeDrawable(new OvalShape());
circle.setTint(color);
- setBackground(circle);
+ ImageView iv = new ImageView(getContext());
+ iv.setBackground(circle);
+ addView(iv);
if (mIcon != null) {
final boolean isLarge = mIcon.hasHint(HINT_LARGE)
|| SUBTYPE_SOURCE.equals(mIcon.getSubType());
final int iconSize = isLarge ? mLargeIconSize : mSmallIconSize;
- SliceViewUtil.createCircledIcon(getContext(), color, iconSize, mIcon.getIcon(),
+ SliceViewUtil.createCircledIcon(getContext(), iconSize, mIcon.getIcon(),
isLarge, this /* parent */);
mUri = slice.getUri();
setClickable(true);
@@ -115,13 +111,23 @@ public class ShortcutView extends FrameLayout implements SliceView.SliceModeView
public boolean performClick() {
if (!callOnClick()) {
try {
- if (mAction != null) {
- mAction.send();
+ if (mActionItem != null) {
+ mActionItem.getAction().send();
} else {
Intent intent = new Intent(Intent.ACTION_VIEW).setData(mUri);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
getContext().startActivity(intent);
}
+ if (mObserver != null) {
+ EventInfo ei = new EventInfo(SliceView.MODE_SHORTCUT,
+ EventInfo.ACTION_TYPE_BUTTON,
+ EventInfo.ROW_TYPE_SHORTCUT, 0 /* rowIndex */);
+ SliceItem interactedItem = mActionItem != null
+ ? mActionItem
+ : new SliceItem(mSlice, FORMAT_SLICE, null /* subtype */,
+ mSlice.getHints());
+ mObserver.onSliceAction(ei, interactedItem);
+ }
} catch (CanceledException e) {
e.printStackTrace();
}
@@ -138,16 +144,14 @@ public class ShortcutView extends FrameLayout implements SliceView.SliceModeView
if (titleItem != null) {
// Preferred case: hinted action containing hinted image and text
- mAction = titleItem.getAction();
+ mActionItem = titleItem;
mIcon = SliceQuery.find(titleItem.getSlice(), FORMAT_IMAGE, HINT_TITLE,
null);
mLabel = SliceQuery.find(titleItem.getSlice(), FORMAT_TEXT, HINT_TITLE,
null);
} else {
// No hinted action; just use the first one
- SliceItem actionItem = SliceQuery.find(slice, FORMAT_ACTION, (String) null,
- null);
- mAction = (actionItem != null) ? actionItem.getAction() : null;
+ mActionItem = SliceQuery.find(slice, FORMAT_ACTION, (String) null, null);
}
// First fallback: any hinted image and text
if (mIcon == null) {
@@ -168,7 +172,7 @@ public class ShortcutView extends FrameLayout implements SliceView.SliceModeView
null);
}
// Final fallback: use app info
- if (mIcon == null || mLabel == null || mAction == null) {
+ if (mIcon == null || mLabel == null || mActionItem == null) {
PackageManager pm = context.getPackageManager();
ProviderInfo providerInfo = pm.resolveContentProvider(
slice.getUri().getAuthority(), 0);
@@ -185,11 +189,24 @@ public class ShortcutView extends FrameLayout implements SliceView.SliceModeView
sb.addText(pm.getApplicationLabel(appInfo), null);
mLabel = sb.build().getItems().get(0);
}
- if (mAction == null) {
- mAction = PendingIntent.getActivity(context, 0,
- pm.getLaunchIntentForPackage(appInfo.packageName), 0);
+ if (mActionItem == null) {
+ mActionItem = new SliceItem(PendingIntent.getActivity(context, 0,
+ pm.getLaunchIntentForPackage(appInfo.packageName), 0),
+ new Slice.Builder(slice.getUri()).build(), FORMAT_SLICE,
+ null /* subtype */, null);
}
}
}
}
+
+ @Override
+ public void resetView() {
+ mSlice = null;
+ mUri = null;
+ mActionItem = null;
+ mLabel = null;
+ mIcon = null;
+ setBackground(null);
+ removeAllViews();
+ }
}
diff --git a/androidx/app/slice/widget/SliceChildView.java b/androidx/app/slice/widget/SliceChildView.java
new file mode 100644
index 00000000..f731c968
--- /dev/null
+++ b/androidx/app/slice/widget/SliceChildView.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 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 androidx.app.slice.widget;
+
+import android.content.Context;
+import android.support.annotation.ColorInt;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import androidx.app.slice.Slice;
+import androidx.app.slice.SliceItem;
+
+/**
+ * Base class for children views of {@link SliceView}.
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public abstract class SliceChildView extends FrameLayout {
+
+ protected SliceView.OnSliceActionListener mObserver;
+ protected int mTintColor;
+
+ public SliceChildView(@NonNull Context context) {
+ super(context);
+ }
+
+ public SliceChildView(Context context, AttributeSet attributeSet) {
+ this(context);
+ }
+
+ /**
+ * @return the mode of the slice being presented.
+ */
+ public abstract int getMode();
+
+ /**
+ * @param slice the slice to show in this view.
+ */
+ public abstract void setSlice(Slice slice);
+
+ /**
+ * Called when the view should be reset.
+ */
+ public abstract void resetView();
+
+ /**
+ * @return the view.
+ */
+ public View getView() {
+ return this;
+ }
+
+ /**
+ * Sets a custom color to use for tinting elements like icons for this view.
+ */
+ public void setTint(@ColorInt int tintColor) {
+ mTintColor = tintColor;
+ }
+
+ /**
+ * Sets the observer to notify when an interaction events occur on the view.
+ */
+ public void setSliceActionListener(SliceView.OnSliceActionListener observer) {
+ mObserver = observer;
+ }
+
+ /**
+ * Populates style information for this view.
+ */
+ public void setStyle(AttributeSet attrs) {
+ // TODO
+ }
+
+ /**
+ * Called when the slice being displayed in this view is an element of a larger list.
+ */
+ public void setSliceItem(SliceItem slice, boolean isHeader, int rowIndex,
+ SliceView.OnSliceActionListener observer) {
+ // Do nothing
+ }
+}
diff --git a/androidx/app/slice/widget/SliceLiveData.java b/androidx/app/slice/widget/SliceLiveData.java
index 8ef221a5..7aee0414 100644
--- a/androidx/app/slice/widget/SliceLiveData.java
+++ b/androidx/app/slice/widget/SliceLiveData.java
@@ -15,20 +15,23 @@
*/
package androidx.app.slice.widget;
+import static android.support.annotation.RestrictTo.Scope.LIBRARY;
+
import android.arch.lifecycle.LiveData;
import android.content.Context;
import android.content.Intent;
-import android.database.ContentObserver;
import android.net.Uri;
import android.os.AsyncTask;
-import android.os.Handler;
import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
import java.util.Arrays;
import java.util.List;
import androidx.app.slice.Slice;
+import androidx.app.slice.SliceManager;
import androidx.app.slice.SliceSpec;
+import androidx.app.slice.SliceSpecs;
/**
* Class with factory methods for creating LiveData that observes slices.
@@ -38,7 +41,12 @@ import androidx.app.slice.SliceSpec;
*/
public final class SliceLiveData {
- private static final List<SliceSpec> SUPPORTED_SPECS = Arrays.asList();
+ /**
+ * @hide
+ */
+ @RestrictTo(LIBRARY)
+ public static final List<SliceSpec> SUPPORTED_SPECS = Arrays.asList(SliceSpecs.BASIC,
+ SliceSpecs.LIST);
/**
* Produces an {@link LiveData} that tracks a Slice for a given Uri. To use
@@ -59,13 +67,13 @@ public final class SliceLiveData {
}
private static class SliceLiveDataImpl extends LiveData<Slice> {
- private final Context mContext;
private final Intent mIntent;
+ private final SliceManager mSliceManager;
private Uri mUri;
private SliceLiveDataImpl(Context context, Uri uri) {
super();
- mContext = context;
+ mSliceManager = SliceManager.get(context);
mUri = uri;
mIntent = null;
// TODO: Check if uri points at a Slice?
@@ -73,7 +81,7 @@ public final class SliceLiveData {
private SliceLiveDataImpl(Context context, Intent intent) {
super();
- mContext = context;
+ mSliceManager = SliceManager.get(context);
mUri = null;
mIntent = intent;
}
@@ -82,35 +90,34 @@ public final class SliceLiveData {
protected void onActive() {
AsyncTask.execute(mUpdateSlice);
if (mUri != null) {
- mContext.getContentResolver().registerContentObserver(mUri, false, mObserver);
+ mSliceManager.registerSliceCallback(mUri, mSliceCallback);
}
}
@Override
protected void onInactive() {
if (mUri != null) {
- mContext.getContentResolver().unregisterContentObserver(mObserver);
+ mSliceManager.unregisterSliceCallback(mUri, mSliceCallback);
}
}
private final Runnable mUpdateSlice = new Runnable() {
@Override
public void run() {
- Slice s = mUri != null ? Slice.bindSlice(mContext, mUri)
- : Slice.bindSlice(mContext, mIntent);
+ Slice s = mUri != null ? mSliceManager.bindSlice(mUri)
+ : mSliceManager.bindSlice(mIntent);
if (mUri == null && s != null) {
- mContext.getContentResolver().registerContentObserver(s.getUri(),
- false, mObserver);
mUri = s.getUri();
+ mSliceManager.registerSliceCallback(mUri, mSliceCallback);
}
postValue(s);
}
};
- private final ContentObserver mObserver = new ContentObserver(new Handler()) {
+ private final SliceManager.SliceCallback mSliceCallback = new SliceManager.SliceCallback() {
@Override
- public void onChange(boolean selfChange) {
- AsyncTask.execute(mUpdateSlice);
+ public void onSliceUpdated(@NonNull Slice s) {
+ postValue(s);
}
};
}
diff --git a/androidx/app/slice/widget/SliceView.java b/androidx/app/slice/widget/SliceView.java
index 951bdd5d..dbf1d671 100644
--- a/androidx/app/slice/widget/SliceView.java
+++ b/androidx/app/slice/widget/SliceView.java
@@ -23,11 +23,10 @@ import static android.app.slice.SliceItem.FORMAT_INT;
import static android.app.slice.SliceItem.FORMAT_SLICE;
import android.arch.lifecycle.Observer;
-import android.content.ContentResolver;
import android.content.Context;
import android.graphics.drawable.ColorDrawable;
-import android.net.Uri;
import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.util.AttributeSet;
@@ -78,25 +77,17 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
private static final String TAG = "SliceView";
/**
- * @hide
+ * Implement this interface to be notified of interactions with the slice displayed
+ * in this view.
+ * @see EventInfo
*/
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- public interface SliceModeView {
-
- /**
- * @return the mode of the slice being presented.
- */
- int getMode();
-
+ public interface OnSliceActionListener {
/**
- * @param slice the slice to show in this view.
+ * Called when an interaction has occurred with an element in this view.
+ * @param info the type of event that occurred.
+ * @param item the specific item within the {@link Slice} that was interacted with.
*/
- void setSlice(Slice slice);
-
- /**
- * @return the view.
- */
- View getView();
+ void onSliceAction(@NonNull EventInfo info, @NonNull SliceItem item);
}
/**
@@ -128,14 +119,18 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
* that selection.
*/
private static final int MODE_AUTO = 0;
-
private int mMode = MODE_AUTO;
- private SliceModeView mCurrentView;
- private final ActionRow mActions;
+ private SliceChildView mCurrentView;
private Slice mCurrentSlice;
- private boolean mShowActions = true;
- private boolean mIsScrollable;
+ private final ActionRow mActions;
private final int mShortcutSize;
+ private OnSliceActionListener mSliceObserver;
+
+ private boolean mShowActions = true;
+ private boolean mIsScrollable = true;
+
+ private int mThemeTintColor = -1;
+ private AttributeSet mAttrs;
public SliceView(Context context) {
this(context, null);
@@ -151,6 +146,7 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
public SliceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
+ mAttrs = attrs;
mActions = new ActionRow(getContext(), true);
mActions.setBackground(new ColorDrawable(0xffeeeeee));
mCurrentView = new LargeTemplateView(getContext());
@@ -162,31 +158,51 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int mode = MeasureSpec.getMode(widthMeasureSpec);
int width = MeasureSpec.getSize(widthMeasureSpec);
+ int childWidth = MeasureSpec.getSize(widthMeasureSpec);
+ int childHeight = MeasureSpec.getSize(heightMeasureSpec);
if (MODE_SHORTCUT == mMode) {
+ // TODO: consider scaling the shortcut to fit
+ childWidth = mShortcutSize;
width = mShortcutSize;
}
- if (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.UNSPECIFIED) {
- widthMeasureSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY);
- }
- measureChildren(widthMeasureSpec, heightMeasureSpec);
+ final int left = getPaddingLeft();
+ final int top = getPaddingTop();
+ final int right = getPaddingRight();
+ final int bot = getPaddingBottom();
+
+ // Measure the children without the padding
+ childWidth -= left + right;
+ childHeight -= top + bot;
+ int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY);
+ int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY);
+ measureChildren(childWidthMeasureSpec, childHeightMeasureSpec);
+
+ // Figure out parent height
int actionHeight = mActions.getVisibility() != View.GONE
? mActions.getMeasuredHeight()
: 0;
- int newHeightSpec = MeasureSpec.makeMeasureSpec(
- mCurrentView.getView().getMeasuredHeight() + actionHeight, MeasureSpec.EXACTLY);
+ int currViewHeight = mCurrentView.getView().getMeasuredHeight() + top + bot;
+ int newHeightSpec = MeasureSpec.makeMeasureSpec(currViewHeight + actionHeight,
+ MeasureSpec.EXACTLY);
+ // Figure out parent width
+ width += left + right;
setMeasuredDimension(width, newHeightSpec);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
View v = mCurrentView.getView();
- v.layout(0, 0, v.getMeasuredWidth(),
- v.getMeasuredHeight());
+ final int left = getPaddingLeft();
+ final int top = getPaddingTop();
+ final int right = getPaddingRight();
+ final int bottom = getPaddingBottom();
+ v.layout(left, top, left + v.getMeasuredWidth(), top + v.getMeasuredHeight());
if (mActions.getVisibility() != View.GONE) {
- mActions.layout(0, v.getMeasuredHeight(), mActions.getMeasuredWidth(),
- v.getMeasuredHeight() + mActions.getMeasuredHeight());
+ mActions.layout(left,
+ top + v.getMeasuredHeight() + bottom,
+ left + mActions.getMeasuredWidth() + right,
+ top + v.getMeasuredHeight() + bottom + mActions.getMeasuredHeight());
}
}
@@ -222,6 +238,15 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
}
/**
+ * Sets the listener to notify when an interaction events occur on the view.
+ * @see EventInfo
+ */
+ public void setOnSliceActionListener(@Nullable OnSliceActionListener observer) {
+ mSliceObserver = observer;
+ mCurrentView.setSliceActionListener(mSliceObserver);
+ }
+
+ /**
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY)
@@ -254,12 +279,12 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
reinflate();
}
- private SliceModeView createView(int mode) {
+ private SliceChildView createView(int mode) {
switch (mode) {
case MODE_SHORTCUT:
return new ShortcutView(getContext());
case MODE_SMALL:
- // Check if it's horizontal
+ // Check if it's horizontal and use a grid instead
if (SliceQuery.hasHints(mCurrentSlice, HINT_HORIZONTAL)) {
return new GridRowView(getContext());
} else {
@@ -271,43 +296,63 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
private void reinflate() {
if (mCurrentSlice == null) {
+ mCurrentView.resetView();
return;
}
// TODO: Smarter mapping here from one state to the next.
- SliceItem color = SliceQuery.findSubtype(mCurrentSlice, FORMAT_INT, SUBTYPE_COLOR);
- List<SliceItem> items = mCurrentSlice.getItems();
- SliceItem actionRow = SliceQuery.find(mCurrentSlice, FORMAT_SLICE,
- HINT_ACTIONS,
- null);
int mode = getMode();
if (mMode == mCurrentView.getMode()) {
mCurrentView.setSlice(mCurrentSlice);
} else {
removeAllViews();
mCurrentView = createView(mode);
+ if (mSliceObserver != null) {
+ mCurrentView.setSliceActionListener(mSliceObserver);
+ }
addView(mCurrentView.getView(), getChildLp(mCurrentView.getView()));
addView(mActions, getChildLp(mActions));
}
+ // Scrolling
if (mode == MODE_LARGE) {
((LargeTemplateView) mCurrentView).setScrollable(mIsScrollable);
}
+ // Styles
+ mCurrentView.setStyle(mAttrs);
+ // Set the slice
+ SliceItem actionRow = SliceQuery.find(mCurrentSlice, FORMAT_SLICE,
+ HINT_ACTIONS,
+ null);
+ List<SliceItem> items = mCurrentSlice.getItems();
if (items.size() > 1 || (items.size() != 0 && items.get(0) != actionRow)) {
mCurrentView.getView().setVisibility(View.VISIBLE);
mCurrentView.setSlice(mCurrentSlice);
} else {
mCurrentView.getView().setVisibility(View.GONE);
}
-
+ // Deal with actions
boolean showActions = mShowActions && actionRow != null
&& mode != MODE_SHORTCUT;
if (showActions) {
- mActions.setActions(actionRow, color);
+ mActions.setActions(actionRow, getTintColor());
mActions.setVisibility(View.VISIBLE);
} else {
mActions.setVisibility(View.GONE);
}
}
+ private int getTintColor() {
+ if (mThemeTintColor != -1) {
+ // Theme has specified a color, use that
+ return mThemeTintColor;
+ } else {
+ final SliceItem colorItem = SliceQuery.findSubtype(
+ mCurrentSlice, FORMAT_INT, SUBTYPE_COLOR);
+ return colorItem != null
+ ? colorItem.getInt()
+ : SliceViewUtil.getColorAccent(getContext());
+ }
+ }
+
private LayoutParams getChildLp(View child) {
if (child instanceof ShortcutView) {
return new LayoutParams(mShortcutSize, mShortcutSize);
@@ -316,12 +361,23 @@ public class SliceView extends ViewGroup implements Observer<Slice> {
}
}
- private static void validate(Uri sliceUri) {
- if (!ContentResolver.SCHEME_CONTENT.equals(sliceUri.getScheme())) {
- throw new RuntimeException("Invalid uri " + sliceUri);
- }
- if (sliceUri.getPathSegments().size() == 0) {
- throw new RuntimeException("Invalid uri " + sliceUri);
+ /**
+ * @return String representation of the provided mode.
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public static String modeToString(@SliceMode int mode) {
+ switch(mode) {
+ case MODE_AUTO:
+ return "MODE AUTO";
+ case MODE_SHORTCUT:
+ return "MODE SHORTCUT";
+ case MODE_SMALL:
+ return "MODE SMALL";
+ case MODE_LARGE:
+ return "MODE LARGE";
+ default:
+ return "unknown mode: " + mode;
}
}
}
diff --git a/androidx/app/slice/widget/SliceViewUtil.java b/androidx/app/slice/widget/SliceViewUtil.java
index c98215f3..12fe7c42 100644
--- a/androidx/app/slice/widget/SliceViewUtil.java
+++ b/androidx/app/slice/widget/SliceViewUtil.java
@@ -155,7 +155,7 @@ public class SliceViewUtil {
/**
*/
@TargetApi(28)
- public static void createCircledIcon(@NonNull Context context, int color, int iconSizePx,
+ public static void createCircledIcon(@NonNull Context context, int iconSizePx,
Icon icon, boolean isLarge, ViewGroup parent) {
ImageView v = new ImageView(context);
v.setImageIcon(icon);
diff --git a/androidx/browser/browseractions/BrowserActionItem.java b/androidx/browser/browseractions/BrowserActionItem.java
new file mode 100644
index 00000000..4bcb83e4
--- /dev/null
+++ b/androidx/browser/browseractions/BrowserActionItem.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.browser.browseractions;
+
+import android.app.PendingIntent;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+
+/**
+ * A wrapper class holding custom item of Browser Actions menu.
+ * The Bitmap is optional for a BrowserActionItem.
+ */
+public class BrowserActionItem {
+ private final String mTitle;
+ private final PendingIntent mAction;
+ @DrawableRes
+ private final int mIconId;
+
+ /**
+ * Constructor for BrowserActionItem with icon, string and action provided.
+ * @param title The string shown for a custom item.
+ * @param action The PendingIntent executed when a custom item is selected
+ * @param iconId The resource id of the icon shown for a custom item.
+ */
+ public BrowserActionItem(
+ @NonNull String title, @NonNull PendingIntent action, @DrawableRes int iconId) {
+ mTitle = title;
+ mAction = action;
+ mIconId = iconId;
+ }
+
+ /**
+ * Constructor for BrowserActionItem with only string and action provided.
+ * @param title The icon shown for a custom item.
+ * @param action The string shown for a custom item.
+ */
+ public BrowserActionItem(@NonNull String title, @NonNull PendingIntent action) {
+ this(title, action, 0);
+ }
+
+ /**
+ * @return The resource id of the icon.
+ */
+ public int getIconId() {
+ return mIconId;
+ }
+
+ /**
+ * @return The title of a custom item.
+ */
+ public String getTitle() {
+ return mTitle;
+ }
+
+ /**
+ * @return The action of a custom item.
+ */
+ public PendingIntent getAction() {
+ return mAction;
+ }
+}
diff --git a/androidx/browser/browseractions/BrowserActionsIntent.java b/androidx/browser/browseractions/BrowserActionsIntent.java
new file mode 100644
index 00000000..beb3d6ca
--- /dev/null
+++ b/androidx/browser/browseractions/BrowserActionsIntent.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 androidx.browser.browseractions;
+
+import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+import android.support.v4.content.ContextCompat;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Class holding the {@link Intent} and start bundle for a Browser Actions Activity.
+ *
+ * <p>
+ * <strong>Note:</strong> The constants below are public for the browser implementation's benefit.
+ * You are strongly encouraged to use {@link BrowserActionsIntent.Builder}.</p>
+ */
+public class BrowserActionsIntent {
+ private static final String TAG = "BrowserActions";
+ // Used to verify that an URL intent handler exists.
+ private static final String TEST_URL = "https://www.example.com";
+
+ /**
+ * Extra that specifies {@link PendingIntent} indicating which Application sends the {@link
+ * BrowserActionsIntent}.
+ */
+ public static final String EXTRA_APP_ID = "androidx.browser.browseractions.APP_ID";
+
+ /**
+ * Indicates that the user explicitly opted out of Browser Actions in the calling application.
+ */
+ public static final String ACTION_BROWSER_ACTIONS_OPEN =
+ "androidx.browser.browseractions.browser_action_open";
+
+ /**
+ * Extra resource id that specifies the icon of a custom item shown in the Browser Actions menu.
+ */
+ public static final String KEY_ICON_ID = "androidx.browser.browseractions.ICON_ID";
+
+ /**
+ * Extra string that specifies the title of a custom item shown in the Browser Actions menu.
+ */
+ public static final String KEY_TITLE = "androidx.browser.browseractions.TITLE";
+
+ /**
+ * Extra PendingIntent to be launched when a custom item is selected in the Browser Actions
+ * menu.
+ */
+ public static final String KEY_ACTION = "androidx.browser.browseractions.ACTION";
+
+ /**
+ * Extra that specifies the type of url for the Browser Actions menu.
+ */
+ public static final String EXTRA_TYPE = "androidx.browser.browseractions.extra.TYPE";
+
+ /**
+ * Extra that specifies List<Bundle> used for adding custom items to the Browser Actions menu.
+ */
+ public static final String EXTRA_MENU_ITEMS =
+ "androidx.browser.browseractions.extra.MENU_ITEMS";
+
+ /**
+ * Extra that specifies the PendingIntent to be launched when a browser specified menu item is
+ * selected. The id of the chosen item will be notified through the data of its Intent.
+ */
+ public static final String EXTRA_SELECTED_ACTION_PENDING_INTENT =
+ "androidx.browser.browseractions.extra.SELECTED_ACTION_PENDING_INTENT";
+
+ /**
+ * The maximum allowed number of custom items.
+ */
+ public static final int MAX_CUSTOM_ITEMS = 5;
+
+ /**
+ * Defines the types of url for Browser Actions menu.
+ */
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({URL_TYPE_NONE, URL_TYPE_IMAGE, URL_TYPE_VIDEO, URL_TYPE_AUDIO, URL_TYPE_FILE,
+ URL_TYPE_PLUGIN})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BrowserActionsUrlType {}
+ public static final int URL_TYPE_NONE = 0;
+ public static final int URL_TYPE_IMAGE = 1;
+ public static final int URL_TYPE_VIDEO = 2;
+ public static final int URL_TYPE_AUDIO = 3;
+ public static final int URL_TYPE_FILE = 4;
+ public static final int URL_TYPE_PLUGIN = 5;
+
+ /**
+ * Defines the the ids of the browser specified menu items in Browser Actions.
+ * TODO(ltian): A long term solution need, since other providers might have customized menus.
+ */
+ /** @hide */
+ @RestrictTo(LIBRARY_GROUP)
+ @IntDef({ITEM_INVALID_ITEM, ITEM_OPEN_IN_NEW_TAB, ITEM_OPEN_IN_INCOGNITO, ITEM_DOWNLOAD,
+ ITEM_COPY, ITEM_SHARE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface BrowserActionsItemId {}
+ public static final int ITEM_INVALID_ITEM = -1;
+ public static final int ITEM_OPEN_IN_NEW_TAB = 0;
+ public static final int ITEM_OPEN_IN_INCOGNITO = 1;
+ public static final int ITEM_DOWNLOAD = 2;
+ public static final int ITEM_COPY = 3;
+ public static final int ITEM_SHARE = 4;
+
+ /**
+ * An {@link Intent} used to start the Browser Actions Activity.
+ */
+ @NonNull private final Intent mIntent;
+
+ /**
+ * Gets the Intent of {@link BrowserActionsIntent}.
+ * @return the Intent of {@link BrowserActionsIntent}.
+ */
+ @NonNull public Intent getIntent() {
+ return mIntent;
+ }
+
+ private BrowserActionsIntent(@NonNull Intent intent) {
+ this.mIntent = intent;
+ }
+
+ /**
+ * Builder class for opening a Browser Actions context menu.
+ */
+ public static final class Builder {
+ private final Intent mIntent = new Intent(BrowserActionsIntent.ACTION_BROWSER_ACTIONS_OPEN);
+ private Context mContext;
+ private Uri mUri;
+ @BrowserActionsUrlType
+ private int mType;
+ private ArrayList<Bundle> mMenuItems = null;
+ private PendingIntent mOnItemSelectedPendingIntent = null;
+
+ /**
+ * Constructs a {@link BrowserActionsIntent.Builder} object associated with default setting
+ * for a selected url.
+ * @param context The context requesting the Browser Actions context menu.
+ * @param uri The selected url for Browser Actions menu.
+ */
+ public Builder(Context context, Uri uri) {
+ mContext = context;
+ mUri = uri;
+ mType = URL_TYPE_NONE;
+ mMenuItems = new ArrayList<>();
+ }
+
+ /**
+ * Sets the type of Browser Actions context menu.
+ * @param type The type of url.
+ */
+ public Builder setUrlType(@BrowserActionsUrlType int type) {
+ mType = type;
+ return this;
+ }
+
+ /**
+ * Sets the custom items list.
+ * Only maximum MAX_CUSTOM_ITEMS custom items are allowed,
+ * otherwise throws an {@link IllegalStateException}.
+ * @param items The list of {@link BrowserActionItem} for custom items.
+ */
+ public Builder setCustomItems(ArrayList<BrowserActionItem> items) {
+ if (items.size() > MAX_CUSTOM_ITEMS) {
+ throw new IllegalStateException(
+ "Exceeded maximum toolbar item count of " + MAX_CUSTOM_ITEMS);
+ }
+ for (int i = 0; i < items.size(); i++) {
+ if (TextUtils.isEmpty(items.get(i).getTitle())
+ || items.get(i).getAction() == null) {
+ throw new IllegalArgumentException(
+ "Custom item should contain a non-empty title and non-null intent.");
+ } else {
+ mMenuItems.add(getBundleFromItem(items.get(i)));
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets the custom items list.
+ * Only maximum MAX_CUSTOM_ITEMS custom items are allowed,
+ * otherwise throws an {@link IllegalStateException}.
+ * @param items The varargs of {@link BrowserActionItem} for custom items.
+ */
+ public Builder setCustomItems(BrowserActionItem... items) {
+ return setCustomItems(new ArrayList<BrowserActionItem>(Arrays.asList(items)));
+ }
+
+ /**
+ * Set the PendingIntent to be launched when a a browser specified menu item is selected.
+ * @param onItemSelectedPendingIntent The PendingIntent to be launched.
+ */
+ public Builder setOnItemSelectedAction(PendingIntent onItemSelectedPendingIntent) {
+ mOnItemSelectedPendingIntent = onItemSelectedPendingIntent;
+ return this;
+ }
+
+ /**
+ * Populates a {@link Bundle} to hold a custom item for Browser Actions menu.
+ * @param item A custom item for Browser Actions menu.
+ * @return The Bundle of custom item.
+ */
+ private Bundle getBundleFromItem(BrowserActionItem item) {
+ Bundle bundle = new Bundle();
+ bundle.putString(KEY_TITLE, item.getTitle());
+ bundle.putParcelable(KEY_ACTION, item.getAction());
+ if (item.getIconId() != 0) bundle.putInt(KEY_ICON_ID, item.getIconId());
+ return bundle;
+ }
+
+ /**
+ * Combines all the options that have been set and returns a new {@link
+ * BrowserActionsIntent} object.
+ */
+ public BrowserActionsIntent build() {
+ mIntent.setData(mUri);
+ mIntent.putExtra(EXTRA_TYPE, mType);
+ mIntent.putParcelableArrayListExtra(EXTRA_MENU_ITEMS, mMenuItems);
+ PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, new Intent(), 0);
+ mIntent.putExtra(EXTRA_APP_ID, pendingIntent);
+ if (mOnItemSelectedPendingIntent != null) {
+ mIntent.putExtra(
+ EXTRA_SELECTED_ACTION_PENDING_INTENT, mOnItemSelectedPendingIntent);
+ }
+ return new BrowserActionsIntent(mIntent);
+ }
+ }
+
+ /**
+ * Construct a BrowserActionsIntent with default settings and launch it to open a Browser
+ * Actions menu.
+ * @param context The context requesting for a Browser Actions menu.
+ * @param uri The url for Browser Actions menu.
+ */
+ public static void openBrowserAction(Context context, Uri uri) {
+ BrowserActionsIntent intent = new BrowserActionsIntent.Builder(context, uri).build();
+ launchIntent(context, intent.getIntent());
+ }
+
+ /**
+ * Construct a BrowserActionsIntent with custom settings and launch it to open a Browser Actions
+ * menu.
+ * @param context The context requesting for a Browser Actions menu.
+ * @param uri The url for Browser Actions menu.
+ * @param type The type of the url for context menu to be opened.
+ * @param items List of custom items to be added to Browser Actions menu.
+ * @param pendingIntent The PendingIntent to be launched when a browser specified menu item is
+ * selected.
+ */
+ public static void openBrowserAction(Context context, Uri uri, int type,
+ ArrayList<BrowserActionItem> items, PendingIntent pendingIntent) {
+ BrowserActionsIntent intent = new BrowserActionsIntent.Builder(context, uri)
+ .setUrlType(type)
+ .setCustomItems(items)
+ .setOnItemSelectedAction(pendingIntent)
+ .build();
+ launchIntent(context, intent.getIntent());
+ }
+
+ /**
+ * Launch an Intent to open a Browser Actions menu.
+ * It first checks if any Browser Actions provider is available to create the menu.
+ * If the default Browser supports Browser Actions, menu will be opened by the default Browser,
+ * otherwise show a intent picker.
+ * If not provider, a Browser Actions menu is opened locally from support library.
+ * @param context The context requesting for a Browser Actions menu.
+ * @param intent The {@link Intent} holds the setting for Browser Actions menu.
+ */
+ public static void launchIntent(Context context, Intent intent) {
+ List<ResolveInfo> handlers = getBrowserActionsIntentHandlers(context);
+ if (handlers == null || handlers.size() == 0) {
+ openFallbackBrowserActionsMenu(context, intent);
+ return;
+ } else if (handlers.size() == 1) {
+ intent.setPackage(handlers.get(0).activityInfo.packageName);
+ } else {
+ Intent viewIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(TEST_URL));
+ PackageManager pm = context.getPackageManager();
+ ResolveInfo defaultHandler =
+ pm.resolveActivity(viewIntent, PackageManager.MATCH_DEFAULT_ONLY);
+ if (defaultHandler != null) {
+ String defaultPackageName = defaultHandler.activityInfo.packageName;
+ for (int i = 0; i < handlers.size(); i++) {
+ if (defaultPackageName.equals(handlers.get(i).activityInfo.packageName)) {
+ intent.setPackage(defaultPackageName);
+ break;
+ }
+ }
+ }
+ }
+ ContextCompat.startActivity(context, intent, null);
+ }
+
+ /**
+ * Returns a list of Browser Actions providers available to handle the {@link
+ * BrowserActionsIntent}.
+ * @param context The context requesting for a Browser Actions menu.
+ * @return List of Browser Actions providers available to handle the intent.
+ */
+ private static List<ResolveInfo> getBrowserActionsIntentHandlers(Context context) {
+ Intent intent =
+ new Intent(BrowserActionsIntent.ACTION_BROWSER_ACTIONS_OPEN, Uri.parse(TEST_URL));
+ PackageManager pm = context.getPackageManager();
+ return pm.queryIntentActivities(intent, PackageManager.MATCH_ALL);
+ }
+
+ private static void openFallbackBrowserActionsMenu(Context context, Intent intent) {
+ Uri uri = intent.getData();
+ int type = intent.getIntExtra(EXTRA_TYPE, URL_TYPE_NONE);
+ ArrayList<Bundle> bundles = intent.getParcelableArrayListExtra(EXTRA_MENU_ITEMS);
+ List<BrowserActionItem> items = bundles != null ? parseBrowserActionItems(bundles) : null;
+ // TODO(ltian): display a fallback dialog showing all custom items from support library.
+ // http://crbug.com/789806.
+ return;
+ }
+
+ /**
+ * Gets custom item list for browser action menu.
+ * @param bundles Data for custom items from {@link BrowserActionsIntent}.
+ * @return List of {@link BrowserActionItem}
+ */
+ public static List<BrowserActionItem> parseBrowserActionItems(ArrayList<Bundle> bundles) {
+ List<BrowserActionItem> mActions = new ArrayList<>();
+ for (int i = 0; i < bundles.size(); i++) {
+ Bundle bundle = bundles.get(i);
+ String title = bundle.getString(BrowserActionsIntent.KEY_TITLE);
+ PendingIntent action = bundle.getParcelable(BrowserActionsIntent.KEY_ACTION);
+ @DrawableRes
+ int iconId = bundle.getInt(BrowserActionsIntent.KEY_ICON_ID);
+ if (TextUtils.isEmpty(title) || action == null) {
+ throw new IllegalArgumentException(
+ "Custom item should contain a non-empty title and non-null intent.");
+ } else {
+ BrowserActionItem item = new BrowserActionItem(title, action, iconId);
+ mActions.add(item);
+ }
+ }
+ return mActions;
+ }
+
+ /**
+ * Get the package name of the creator application.
+ * @param intent The {@link BrowserActionsIntent}.
+ * @return The creator package name.
+ */
+ @SuppressWarnings("deprecation")
+ public static String getCreatorPackageName(Intent intent) {
+ PendingIntent pendingIntent = intent.getParcelableExtra(BrowserActionsIntent.EXTRA_APP_ID);
+ if (pendingIntent != null) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
+ return pendingIntent.getCreatorPackage();
+ } else {
+ return pendingIntent.getTargetPackage();
+ }
+ }
+ return null;
+ }
+}
diff --git a/androidx/car/drawer/CarDrawerActivity.java b/androidx/car/drawer/CarDrawerActivity.java
index 9769142a..3929cca6 100644
--- a/androidx/car/drawer/CarDrawerActivity.java
+++ b/androidx/car/drawer/CarDrawerActivity.java
@@ -16,20 +16,24 @@
package androidx.car.drawer;
+import android.animation.ValueAnimator;
import android.content.res.Configuration;
+import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.LayoutRes;
import android.support.annotation.Nullable;
+import android.support.design.widget.AppBarLayout;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
-import android.support.v7.widget.Toolbar;
+import android.util.TypedValue;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.car.R;
+import androidx.car.widget.ClickThroughToolbar;
/**
* Common base Activity for car apps that need to present a Drawer.
@@ -46,6 +50,12 @@ import androidx.car.R;
*
* <p>This class will take care of drawer toggling and display.
*
+ * <p>This Activity also exposes the ability to have its toolbar optionally hide if any content
+ * in its main view is scrolled. Be default, this ability is turned off. Call
+ * {@link #setToolbarCollapsible()} to enable this behavior. Additionally, a user can set elevation
+ * on this toolbar by calling the appropriate {@link #setToolbarElevation(float)} method. There is
+ * elevation on the toolbar by default.
+ *
* <p>The rootAdapter can implement nested-navigation, in its click-handling, by passing the
* CarDrawerAdapter for the next level to
* {@link CarDrawerController#pushAdapter(CarDrawerAdapter)}.
@@ -54,7 +64,21 @@ import androidx.car.R;
* derivative.
*/
public class CarDrawerActivity extends AppCompatActivity {
+ private static final int ANIMATION_DURATION_MS = 100;
+ private static final float DRAWER_OPEN_OFFSET = 0.25f;
+
private CarDrawerController mDrawerController;
+ private AppBarLayout mAppBarLayout;
+
+ /**
+ * Whether or not the drawer is considered opened. This value is only set and unset when the
+ * drawer has passed the {@link #DRAWER_OPEN_OFFSET}.
+ */
+ private boolean mDrawerOpen;
+ private float mAppBarElevation;
+
+ private ClickThroughToolbar mToolbar;
+ private boolean mToolbarCollapsible;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -62,6 +86,10 @@ public class CarDrawerActivity extends AppCompatActivity {
setContentView(R.layout.car_drawer_activity);
+ mAppBarLayout = findViewById(R.id.appbar);
+ mAppBarLayout.setBackgroundColor(getThemeColorPrimary());
+ setToolbarElevation(getResources().getDimension(R.dimen.car_app_bar_default_elevation));
+
DrawerLayout drawerLayout = findViewById(R.id.drawer_layout);
ActionBarDrawerToggle drawerToggle = new ActionBarDrawerToggle(
this /* activity */,
@@ -69,10 +97,10 @@ public class CarDrawerActivity extends AppCompatActivity {
R.string.car_drawer_open,
R.string.car_drawer_close);
- Toolbar toolbar = findViewById(R.id.car_toolbar);
- setSupportActionBar(toolbar);
+ mToolbar = findViewById(R.id.car_toolbar);
+ setSupportActionBar(mToolbar);
- mDrawerController = new CarDrawerController(toolbar, drawerLayout, drawerToggle);
+ mDrawerController = new CarDrawerController(mToolbar, drawerLayout, drawerToggle);
CarDrawerAdapter rootAdapter = getRootAdapter();
if (rootAdapter != null) {
mDrawerController.setRootAdapter(rootAdapter);
@@ -80,6 +108,44 @@ public class CarDrawerActivity extends AppCompatActivity {
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeButtonEnabled(true);
+
+ mDrawerController.addDrawerListener(new DrawerLayout.DrawerListener() {
+ private final int mClosedDrawerToolbarColor = getThemeColorPrimary();
+
+ @Override
+ public void onDrawerSlide(View view, float slideOffset) {
+ if (slideOffset >= DRAWER_OPEN_OFFSET) {
+ // If this is the first time the drawer is considered open, then save the
+ // value of the elevation to restore later.
+ if (!mDrawerOpen) {
+ mDrawerOpen = true;
+ mAppBarElevation = mAppBarLayout.getElevation();
+ }
+
+ mAppBarLayout.setBackgroundColor(Color.TRANSPARENT);
+ setToolbarElevation(0);
+ setToolbarAlwaysShowInternal();
+ } else if (mDrawerOpen) {
+ // Only reset the state of the AppBar if the drawer has reached the open state.
+ mDrawerOpen = false;
+ mAppBarLayout.setBackgroundColor(mClosedDrawerToolbarColor);
+ setToolbarElevation(mAppBarElevation);
+
+ if (mToolbarCollapsible) {
+ setToolbarCollapsibleInternal();
+ }
+ }
+ }
+
+ @Override
+ public void onDrawerOpened(View view) {}
+
+ @Override
+ public void onDrawerClosed(View view) {}
+
+ @Override
+ public void onDrawerStateChanged(int i) {}
+ });
}
/**
@@ -135,6 +201,85 @@ public class CarDrawerActivity extends AppCompatActivity {
}
/**
+ * Sets whether clicks on the toolbar of this view will pass through to any views underneath it.
+ * By default, the toolbar will not allow clicks to pass through. This is the equivalent of
+ * passing {@code false} to this method.
+ *
+ * @param clickThrough {@code true} if clicks will pass through; {@code false} for the toolbar
+ * to eat all clicks.
+ */
+ public void setToolbarClickThrough(boolean clickThrough) {
+ mToolbar.setClickPassThrough(clickThrough);
+ }
+
+ /**
+ * Sets the elevation on the toolbar of this Activity.
+ *
+ * @param elevation The elevation to set.
+ */
+ public void setToolbarElevation(float elevation) {
+ // The AppBar's default animator needs to be set to null to manually change the elevation.
+ mAppBarLayout.setStateListAnimator(null);
+ mAppBarLayout.setElevation(elevation);
+ }
+
+ /**
+ * Sets the elevation of the toolbar and animate it from the current elevation value.
+ *
+ * @param elevation The elevation to set.
+ */
+ public void setToolbarElevationWithAnimation(float elevation) {
+ ValueAnimator elevationAnimator =
+ ValueAnimator.ofFloat(mAppBarLayout.getElevation(), elevation);
+ elevationAnimator
+ .setDuration(ANIMATION_DURATION_MS)
+ .addUpdateListener(animation -> setToolbarElevation(
+ (float) animation.getAnimatedValue()));
+ elevationAnimator.start();
+ }
+
+ /**
+ * Sets the toolbar of this Activity as collapsible. When any content in the main view of the
+ * Activity is scrolled, the toolbar will collapse and show itself accordingly.
+ */
+ public void setToolbarCollapsible() {
+ mToolbarCollapsible = true;
+ setToolbarCollapsibleInternal();
+ }
+
+ /**
+ * An internal-use method that sets the toolbar as collapsible. This version of the method
+ * does not override {@link #mToolbarCollapsible}, and thus retains whatever value a user
+ * of this Activity has set.
+ */
+ private void setToolbarCollapsibleInternal() {
+ AppBarLayout.LayoutParams params =
+ (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
+ params.setScrollFlags(AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL
+ | AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS);
+ }
+
+ /**
+ * Sets the toolbar to always show even if content in the main view of the Activity has been
+ * scrolled. This is the default behavior.
+ */
+ public void setToolbarAlwaysShow() {
+ mToolbarCollapsible = false;
+ setToolbarAlwaysShowInternal();
+ }
+
+ /**
+ * An internal-use method that sets the toolbar to always show. This version of the method does
+ * not override {@link #mToolbarCollapsible}, and thus retains whatever value a user of this
+ * Activity has set.
+ */
+ private void setToolbarAlwaysShowInternal() {
+ AppBarLayout.LayoutParams params =
+ (AppBarLayout.LayoutParams) mToolbar.getLayoutParams();
+ params.setScrollFlags(0);
+ }
+
+ /**
* Get the id of the main content Container which is a FrameLayout. Subclasses can add their own
* content/fragments inside here.
*
@@ -160,4 +305,14 @@ public class CarDrawerActivity extends AppCompatActivity {
public boolean onOptionsItemSelected(MenuItem item) {
return mDrawerController.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
}
+
+ /**
+ * Returns the color that has been set as {@code colorPrimary} on the current Theme of this
+ * Activity.
+ */
+ private int getThemeColorPrimary() {
+ TypedValue value = new TypedValue();
+ getTheme().resolveAttribute(android.R.attr.colorPrimary, value, true);
+ return value.data;
+ }
}
diff --git a/androidx/car/moderator/ContentRateLimiter.java b/androidx/car/moderator/ContentRateLimiter.java
new file mode 100644
index 00000000..3d282144
--- /dev/null
+++ b/androidx/car/moderator/ContentRateLimiter.java
@@ -0,0 +1,240 @@
+/*
+ * 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 androidx.car.moderator;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+
+import android.support.annotation.MainThread;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.util.Preconditions;
+import android.util.Log;
+
+/**
+ * A class that keeps track of a general number of permitted actions that happen over time and
+ * determines if a subsequent interaction is allowed. The type of interaction is arbitrary and not
+ * transparent to this class. Instead, it will refer to these actions as "permits," short for
+ * "permitted action." It is up to a user of this class to determine the unit of permits.
+ *
+ * <p>This class allows for two quick acquires in succession to only consume one permit. This is
+ * intended behavior to account for the fact that the user can be using many taps to scroll
+ * quickly. This can fit within the window for which a user does not necessary have their eyes
+ * off the road for a long period of time, and thus should not be penalized.
+ *
+ * <p>This class allows for the maximum number of permits that can be stored,the amount of permits
+ * that are filled each second, as well as the delay before re-fill to be configured.
+ */
+public class ContentRateLimiter {
+ private static final String TAG = "ContentRateLimiter";
+
+ /** The maximum number of stored permits. */
+ private final float mMaxStoredPermits;
+
+ /**
+ * The interval between two unit requests at our stable rate. For example, a stable rate of
+ * 5 permits per second has a stable interval of 200ms.
+ */
+ private final long mStableIntervalMs;
+
+ /**
+ * The amount of time to wait between when a permit is acquired and when the model starts
+ * refilling.
+ */
+ private final long mFillDelayMs;
+
+ /** Unlimited mode. Once enabled, any number of permits can be acquired and consumed. */
+ private boolean mUnlimitedModeEnabled;
+
+ /**
+ * Used to do incremental calculations by {@link #getLastCalculatedPermitCount()}, cannot be
+ * used directly.
+ */
+ private float mLastCalculatedPermitCount;
+
+ /** Time in milliseconds when permits can resume incrementing. */
+ private long mResumeIncrementingMs;
+
+ /** Tracks if the model will allow a second permit to be requested in the fill delay. */
+ private boolean mSecondaryFillDelayPermitAvailable = true;
+
+ private final ElapsedTimeProvider mElapsedTimeProvider;
+
+ /**
+ * An interface for a provider of the current time that has passed since boot. This interface
+ * is meant to abstract the {@link android.os.SystemClock} so that it can be mocked during
+ * testing.
+ */
+ interface ElapsedTimeProvider {
+ /** Returns milliseconds since boot, including time spent in sleep. */
+ long getElapsedRealtime();
+ }
+
+ /**
+ * Creates a {@code ContentRateLimiter} with the given parameters.
+ *
+ * @param acquiredPermitsPerSecond The amount of permits that are acquired each second.
+ * @param maxStoredPermits The maximum number of permits that can be stored.
+ * @param fillDelayMs The amount of time to wait between when a permit is acquired and when
+ * the number of available permits start refilling.
+ */
+ public ContentRateLimiter(float acquiredPermitsPerSecond, float maxStoredPermits,
+ long fillDelayMs) {
+ this(acquiredPermitsPerSecond, maxStoredPermits, fillDelayMs,
+ new SystemClockTimeProvider());
+ }
+
+ // A constructor that allows for the SystemClockTimeProvider to be provided. This is needed for
+ // testing so that the unit test does not rely on the actual SystemClock.
+ @VisibleForTesting
+ ContentRateLimiter(float acquiredPermitsPerSecond, float maxStoredPermits,
+ long fillDelayMs, ElapsedTimeProvider elapsedTimeProvider) {
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, String.format("permitsPerSecond: %f maxStoredPermits: %f, fillDelayMs %d",
+ acquiredPermitsPerSecond, maxStoredPermits, fillDelayMs));
+ }
+
+ Preconditions.checkArgument(acquiredPermitsPerSecond > 0);
+ Preconditions.checkArgument(maxStoredPermits >= 0);
+ Preconditions.checkArgument(fillDelayMs >= 0);
+
+ mStableIntervalMs = (long) (SECONDS.toMillis(1L) / acquiredPermitsPerSecond);
+ mMaxStoredPermits = maxStoredPermits;
+ mLastCalculatedPermitCount = maxStoredPermits;
+ mFillDelayMs = fillDelayMs;
+
+ mElapsedTimeProvider = elapsedTimeProvider;
+ mResumeIncrementingMs = mElapsedTimeProvider.getElapsedRealtime();
+ }
+
+ /** Gets the current number of stored permits ready to be used. */
+ @MainThread
+ public float getAvailablePermits() {
+ return getLastCalculatedPermitCount();
+ }
+
+ /**
+ * Sets the current number of stored permits that are ready to be used. If this value exceeds
+ * the maximum number of stored permits that is passed to the constructor, then the max value
+ * is used instead.
+ */
+ @MainThread
+ public void setAvailablePermits(float availablePermits) {
+ setLastCalculatedPermitCount(availablePermits, mElapsedTimeProvider.getElapsedRealtime());
+ }
+
+ /** Gets the max number of permits allowed to be stored for future usage. */
+ public float getMaxStoredPermits() {
+ return mMaxStoredPermits;
+ }
+
+ /**
+ * Checks if there are enough available permits for a single permit to be acquired.
+ *
+ * @return {@code true} if unlimited mode is enabled or enough permits are acquirable at the
+ * time of this call; {@code false} if there isn't the number of permits requested available
+ * currently.
+ */
+ @MainThread
+ public boolean tryAcquire() {
+ return tryAcquire(1);
+ }
+
+ /**
+ * Checks whether there are enough available permits to acquire.
+ *
+ * @return {@code true} if unlimited mode is enabled or enough permits are acquirable at the
+ * time of this call; {@code false} if there isn't the number of permits requested available
+ * currently.
+ */
+ @MainThread
+ public boolean tryAcquire(int permits) {
+ // Once unlimited mode is enabled, we can acquire any number of permits we want and don't
+ // consume the stored permits.
+ if (mUnlimitedModeEnabled) {
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Unlimited mode is enabled.");
+ }
+ return true;
+ }
+ float availablePermits = getLastCalculatedPermitCount();
+ long nowMs = mElapsedTimeProvider.getElapsedRealtime();
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, String.format("Requesting: %d, Stored: %f/%f", permits,
+ mLastCalculatedPermitCount, mMaxStoredPermits));
+ }
+ if (availablePermits <= permits) {
+ // Once locked out, the user is prevented from acquiring any more permits until they
+ // have waited long enough for a permit to refill. If the user attempts to acquire a
+ // permit during this time, the countdown timer until a permit is refilled is reset.
+ setLastCalculatedPermitCount(0, nowMs + mFillDelayMs);
+ return false;
+ } else if (nowMs < mResumeIncrementingMs && mSecondaryFillDelayPermitAvailable) {
+ // If a second permit is requested between the time a first permit was requested and
+ // the fill delay, allow the second permit to be acquired without decrementing the model
+ // and set the point where permits can resume incrementing {@link #mFillDelayMs} in the
+ // future.
+ setLastCalculatedPermitCount(availablePermits, nowMs + mFillDelayMs);
+ // Don't allow a third "free" permit to be acquired in the fill delay fringe.
+ mSecondaryFillDelayPermitAvailable = false;
+
+ if (Log.isLoggable(TAG, Log.DEBUG)) {
+ Log.d(TAG, "Used up free secondary permit");
+ }
+ return true;
+ } else {
+ // Decrement the available permits, and set the point where permits can resume
+ // incrementing {@link #mFillDelayMs} in the future.
+ setLastCalculatedPermitCount(availablePermits - permits, nowMs + mFillDelayMs);
+
+ if (Log.isLoggable(TAG, Log.VERBOSE)) {
+ Log.v(TAG, String.format("permits remaining %s, secondary permit available %s",
+ mLastCalculatedPermitCount,
+ mSecondaryFillDelayPermitAvailable));
+ }
+
+ mSecondaryFillDelayPermitAvailable = true;
+ return true;
+ }
+ }
+
+ /**
+ * Sets unlimited mode. If enabled, there is no restriction on the number of permits that
+ * can be acquired and any interaction does not consume stored permits.
+ */
+ public void setUnlimitedMode(boolean enabled) {
+ mUnlimitedModeEnabled = enabled;
+ }
+
+ /**
+ * Updates {@link #mLastCalculatedPermitCount} and {@link #mResumeIncrementingMs} based on the
+ * current time.
+ */
+ private float getLastCalculatedPermitCount() {
+ long nowMs = mElapsedTimeProvider.getElapsedRealtime();
+ if (nowMs > mResumeIncrementingMs) {
+ long deltaMs = nowMs - mResumeIncrementingMs;
+ float newPermits = deltaMs / (float) mStableIntervalMs;
+ setLastCalculatedPermitCount(mLastCalculatedPermitCount + newPermits, nowMs);
+ }
+ return mLastCalculatedPermitCount;
+ }
+
+ private void setLastCalculatedPermitCount(float newCount, long nextMs) {
+ mLastCalculatedPermitCount = Math.min(mMaxStoredPermits, newCount);
+ mResumeIncrementingMs = nextMs;
+ }
+}
diff --git a/androidx/car/moderator/SpeedBumpView.java b/androidx/car/moderator/SpeedBumpView.java
new file mode 100644
index 00000000..81a97ee9
--- /dev/null
+++ b/androidx/car/moderator/SpeedBumpView.java
@@ -0,0 +1,200 @@
+/*
+ * 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 androidx.car.moderator;
+
+import android.content.Context;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+
+import androidx.car.R;
+
+/**
+ * A wrapping view that will monitor all touch events on its children views and prevent the user
+ * from interacting if they have performed a preset number of interactions within a preset amount
+ * of time.
+ *
+ * <p>When the user has performed the maximum number of interactions per the set unit of time, a
+ * message explaining that they are no longer able to interact with the view is also displayed.
+ */
+public class SpeedBumpView extends FrameLayout {
+ /**
+ * The number of permitted actions that are acquired per second that the user has not
+ * interacted with the {@code SpeedBumpView}.
+ */
+ private static final float ACQUIRED_PERMITS_PER_SECOND = 0.5f;
+
+ /** The maximum number of permits that can be acquired when the user is idling. */
+ private static final float MAX_PERMIT_POOL = 5f;
+
+ /** The delay between when the permit pool has been depleted and when it begins to refill. */
+ private static final long PERMIT_FILL_DELAY_MS = 600L;
+
+ private int mLockOutMessageDurationMs;
+
+ private final ContentRateLimiter mContentRateLimiter = new ContentRateLimiter(
+ ACQUIRED_PERMITS_PER_SECOND,
+ MAX_PERMIT_POOL,
+ PERMIT_FILL_DELAY_MS);
+
+ /**
+ * Whether or not the user is currently allowed to interact with any child views of
+ * {@code SpeedBumpView}.
+ */
+ private boolean mInteractionPermitted = true;
+
+ private final Handler mHandler = new Handler();
+
+ private View mLockoutMessageView;
+ private ImageView mLockoutImageView;
+
+ public SpeedBumpView(Context context) {
+ super(context);
+ init();
+ }
+
+ public SpeedBumpView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ public SpeedBumpView(Context context, AttributeSet attrs, int defStyleAttrs) {
+ super(context, attrs, defStyleAttrs);
+ init();
+ }
+
+ public SpeedBumpView(Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
+ super(context, attrs, defStyleAttrs, defStyleRes);
+ init();
+ }
+
+ private void init() {
+ mLockOutMessageDurationMs =
+ getResources().getInteger(R.integer.speed_bump_lock_out_duration_ms);
+
+ LayoutInflater layoutInflater = LayoutInflater.from(getContext());
+ mLockoutMessageView = layoutInflater.inflate(R.layout.lock_out_message, this, false);
+ mLockoutImageView = mLockoutMessageView.findViewById(R.id.lock_out_drawable);
+
+ addView(mLockoutMessageView);
+ mLockoutMessageView.bringToFront();
+ }
+
+ @Override
+ public void addView(View child, int index, ViewGroup.LayoutParams params) {
+ super.addView(child, index, params);
+
+ // Always ensure that the lock out view has the highest Z-index so that it will show
+ // above all other views.
+ mLockoutMessageView.bringToFront();
+ }
+
+
+ // Overriding dispatchTouchEvent to intercept all touch events on child views.
+ @Override
+ public boolean dispatchTouchEvent(MotionEvent ev) {
+ int action = ev.getActionMasked();
+
+ // Check if the user has just finished an MotionEvent and count that as an action. Check
+ // the ContentRateLimiter to see if interaction is currently permitted.
+ if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
+ boolean nextActionPermitted = mContentRateLimiter.tryAcquire();
+
+ // Indicates that this is the first action that is not permitted. In this case, the
+ // child view should at least handle the ACTION_CANCEL or ACTION_UP, so call
+ // super.dispatchTouchEvent(), but lock out further interaction.
+ if (mInteractionPermitted && !nextActionPermitted) {
+ mInteractionPermitted = false;
+ showLockOutMessage();
+ return super.dispatchTouchEvent(ev);
+ }
+ }
+
+ // Otherwise, if interaction permitted, allow child views to handle touch events.
+ return mInteractionPermitted && super.dispatchTouchEvent(ev);
+ }
+
+ /**
+ * Displays a message that informs the user that they are not permitted to interact any further
+ * with the current view.
+ */
+ private void showLockOutMessage() {
+ // If the message is visible, then it's already showing or animating in. So, do nothing.
+ if (mLockoutMessageView.getVisibility() == VISIBLE) {
+ return;
+ }
+
+ Animation lockOutMessageIn =
+ AnimationUtils.loadAnimation(getContext(), R.anim.lock_out_message_in);
+ lockOutMessageIn.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ mLockoutMessageView.setVisibility(VISIBLE);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ // When the lock-out message is completely shown, let it display for
+ // mLockOutMessageDurationMs milliseconds before hiding it.
+ mHandler.postDelayed(SpeedBumpView.this::hideLockOutMessage,
+ mLockOutMessageDurationMs);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+
+ mLockoutMessageView.clearAnimation();
+ mLockoutMessageView.startAnimation(lockOutMessageIn);
+ ((AnimatedVectorDrawable) mLockoutImageView.getDrawable()).start();
+ }
+
+ /**
+ * Hides any lock-out messages. Once the message is hidden, interaction with the view is
+ * permitted.
+ */
+ private void hideLockOutMessage() {
+ if (mLockoutMessageView.getVisibility() != VISIBLE) {
+ return;
+ }
+
+ Animation lockOutMessageOut =
+ AnimationUtils.loadAnimation(getContext(), R.anim.lock_out_message_out);
+ lockOutMessageOut.setAnimationListener(new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {}
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ mLockoutMessageView.setVisibility(GONE);
+ mInteractionPermitted = true;
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {}
+ });
+ mLockoutMessageView.startAnimation(lockOutMessageOut);
+ }
+}
diff --git a/androidx/car/moderator/SystemClockTimeProvider.java b/androidx/car/moderator/SystemClockTimeProvider.java
new file mode 100644
index 00000000..11fc4852
--- /dev/null
+++ b/androidx/car/moderator/SystemClockTimeProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 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 androidx.car.moderator;
+
+import android.os.SystemClock;
+
+/**
+ * A class that wraps the {@link android.os.SystemClock} to allow for the clock provider to be
+ * swapped out for easier testing.
+ */
+class SystemClockTimeProvider implements ContentRateLimiter.ElapsedTimeProvider {
+ /** Returns milliseconds since boot, including time spent in sleep. */
+ @Override
+ public long getElapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+}
diff --git a/androidx/car/widget/ClickThroughToolbar.java b/androidx/car/widget/ClickThroughToolbar.java
new file mode 100644
index 00000000..ddfce770
--- /dev/null
+++ b/androidx/car/widget/ClickThroughToolbar.java
@@ -0,0 +1,78 @@
+/*
+ * 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 androidx.car.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.support.v7.widget.Toolbar;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import androidx.car.R;
+
+/**
+ * A toolbar that optionally supports allowing clicks on it to pass through to any underlying views.
+ *
+ * <p>By default, the {@link Toolbar} eats all touches on it. This view will override
+ * {@link #onTouchEvent(MotionEvent)} and return {@code false} if configured to allow pass through.
+ */
+public class ClickThroughToolbar extends Toolbar {
+ private boolean mAllowClickPassThrough;
+
+ public ClickThroughToolbar(Context context) {
+ super(context);
+ }
+
+ public ClickThroughToolbar(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ initAttributes(context, attrs, 0 /* defStyleAttrs */);
+ }
+
+ public ClickThroughToolbar(Context context, AttributeSet attrs, int defStyleAttrs) {
+ super(context, attrs, defStyleAttrs);
+ initAttributes(context, attrs, defStyleAttrs);
+ }
+
+ private void initAttributes(Context context, AttributeSet attrs, int defStyleAttrs) {
+ TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.ClickThroughToolbar, defStyleAttrs, 0 /* defStyleRes */);
+
+ mAllowClickPassThrough = a.getBoolean(R.styleable.ClickThroughToolbar_clickThrough, false);
+
+ a.recycle();
+ }
+
+ /**
+ * Whether or not clicks on this toolbar will pass through to any views that are underneath
+ * it. By default, this value is {@code false}.
+ *
+ * @param allowPassThrough {@code true} if clicks will pass through to an underlying view;
+ * {@code false} otherwise.
+ */
+ public void setClickPassThrough(boolean allowPassThrough) {
+ mAllowClickPassThrough = allowPassThrough;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ if (mAllowClickPassThrough) {
+ return false;
+ }
+
+ return super.onTouchEvent(ev);
+ }
+}
diff --git a/androidx/car/widget/ListItem.java b/androidx/car/widget/ListItem.java
index d292d6b2..74ff2dd5 100644
--- a/androidx/car/widget/ListItem.java
+++ b/androidx/car/widget/ListItem.java
@@ -1,701 +1,90 @@
-/*
- * 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;
+import java.util.function.Function;
/**
- * Class to build a list item.
- *
- * <p>An item supports primary action and supplemental action(s).
+ * Definition of items that can be inserted into {@link ListItemAdapter}.
*
- * <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.
+ * @param <VH> ViewHolder.
*/
-public class ListItem {
+public abstract class ListItem<VH extends RecyclerView.ViewHolder> {
- private Builder mBuilder;
+ // Whether the item should calculate view layout params. This usually happens when the item is
+ // updated after bind() is called. Calling bind() resets to false.
+ private boolean mDirty;
- private ListItem(Builder builder) {
- mBuilder = builder;
- }
+ // Tag for indicating whether to hide the divider.
+ private boolean mHideDivider;
+
+ /**
+ * Classes that extends {@code ListItem} should register its view type in
+ * {@link ListItemAdapter#registerListItemViewType(int, int, Function)}.
+ *
+ * @return type of this ListItem.
+ */
+ abstract int getViewType();
+
+ /**
+ * Called when ListItem is bound to its ViewHolder.
+ */
+ public abstract void bind(VH viewHolder);
/**
- * Applies all {@link ViewBinder} to {@code viewHolder}.
+ * Marks this item so that sub-views in ViewHolder will need layout params re-calculated
+ * in next bind().
+ *
+ * This method should be called in each setter.
*/
- void bind(ListItemAdapter.ViewHolder viewHolder) {
- setAllSubViewsGone(viewHolder);
- for (ViewBinder binder : mBuilder.mBinders) {
- binder.bind(viewHolder);
- }
+ protected void markDirty() {
+ mDirty = true;
}
- 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);
- }
+ /**
+ * Marks this item as not dirty - no need to calculate sub-view layout params in bind().
+ */
+ protected void markClean() {
+ mDirty = false;
}
/**
- * 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}.
+ * @return {@code true} if this item needs to calculate sub-view layout params.
*/
- public interface ViewBinder {
- /**
- * Provides a way to interact with views in view holder.
- */
- void bind(ListItemAdapter.ViewHolder viewHolder);
+ protected boolean isDirty() {
+ return mDirty;
}
/**
- * Builds a {@link ListItem}.
+ * Whether hide the item divider coming after this {@code ListItem}.
*
- * <p>With conflicting methods are called, e.g. setting primary action to both primary icon and
- * no icon, the last called method wins.
+ * <p>Note: For this to work, one must invoke
+ * {@code PagedListView.setDividerVisibilityManager(adapter} for {@link ListItemAdapter} and
+ * have dividers enabled on {@link PagedListView}.
*/
- 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;
+ public void setHideDivider(boolean hideDivider) {
+ mHideDivider = hideDivider;
+ markDirty();
+ }
- mAction1Text = action1Text;
- mAction1OnClickListener = action1OnClickListener;
- mShowAction1Divider = showAction1Divider;
- mAction2Text = action2Text;
- mAction2OnClickListener = action2OnClickListener;
- mShowAction2Divider = showAction2Divider;
- return this;
- }
+ /**
+ * @return {@code true} if the divider that comes after this ListItem should be hidden.
+ * Defaults to false.
+ */
+ public boolean shouldHideDivider() {
+ return mHideDivider;
+ };
+ /**
+ * Functional interface to provide a way to interact with views in {@code ViewHolder}.
+ * {@code ListItem} calls all added ViewBinders when it {@code bind}s to {@code ViewHolder}.
+ *
+ * @param <VH> extends {@link RecyclerView.ViewHolder}.
+ */
+ public interface ViewBinder<VH extends RecyclerView.ViewHolder> {
/**
- * 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>
+ * Provides a way to interact with views in view holder.
*/
- public Builder withViewBinder(ViewBinder binder) {
- mCustomBinders.add(binder);
- return this;
- }
+ void bind(VH viewHolder);
}
}
diff --git a/androidx/car/widget/ListItemAdapter.java b/androidx/car/widget/ListItemAdapter.java
index c9b61773..8dedbf5b 100644
--- a/androidx/car/widget/ListItemAdapter.java
+++ b/androidx/car/widget/ListItemAdapter.java
@@ -17,16 +17,17 @@
package androidx.car.widget;
import android.content.Context;
+import android.support.annotation.LayoutRes;
import android.support.v7.widget.CardView;
import android.support.v7.widget.RecyclerView;
+import android.util.SparseArray;
+import android.util.SparseIntArray;
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 java.util.function.Function;
import androidx.car.R;
import androidx.car.utils.ListItemBackgroundResolver;
@@ -34,10 +35,16 @@ import androidx.car.utils.ListItemBackgroundResolver;
/**
* Adapter for {@link PagedListView} to display {@link ListItem}.
*
- * Implements {@link PagedListView.ItemCap} - defaults to unlimited item count.
+ * <ul>
+ * <li> Implements {@link PagedListView.ItemCap} - defaults to unlimited item count.
+ * <li> Implements {@link PagedListView.DividerVisibilityManager} - to control dividers after
+ * individual {@link ListItem}.
+ * </ul>
+ *
*/
public class ListItemAdapter extends
- RecyclerView.Adapter<ListItemAdapter.ViewHolder> implements PagedListView.ItemCap {
+ RecyclerView.Adapter<RecyclerView.ViewHolder> implements PagedListView.ItemCap,
+ PagedListView.DividerVisibilityManager {
/**
* Constant class for background style of items.
@@ -63,6 +70,35 @@ public class ListItemAdapter extends
private int mBackgroundStyle;
+ static final int LIST_ITEM_TYPE_TEXT = 1;
+ static final int LIST_ITEM_TYPE_SEEKBAR = 2;
+
+ private final SparseIntArray mViewHolderLayoutResIds = new SparseIntArray();
+ private final SparseArray<Function<View, RecyclerView.ViewHolder>> mViewHolderCreator =
+ new SparseArray<>();
+
+ /**
+ * Registers a function that returns {@link android.support.v7.widget.RecyclerView.ViewHolder}
+ * for its matching view type returned by {@link ListItem#getViewType()}.
+ *
+ * <p>The function will receive a view as {@link RecyclerView.ViewHolder#itemView}. This view
+ * uses background defined by {@link BackgroundStyle}.
+ *
+ * <p>Subclasses of {@link ListItem} in package androidx.car.widget are already registered.
+ *
+ * @param viewType use negative value for custom view type.
+ * @param function function to create ViewHolder for {@code viewType}.
+ */
+ public void registerListItemViewType(int viewType, @LayoutRes int layoutResId,
+ Function<View, RecyclerView.ViewHolder> function) {
+ if (mViewHolderLayoutResIds.get(viewType) != 0
+ || mViewHolderCreator.get(viewType) != null) {
+ throw new IllegalArgumentException("View type is already registered.");
+ }
+ mViewHolderCreator.put(viewType, function);
+ mViewHolderLayoutResIds.put(viewType, layoutResId);
+ }
+
private final Context mContext;
private final ListItemProvider mItemProvider;
@@ -77,18 +113,31 @@ public class ListItemAdapter extends
mContext = context;
mItemProvider = itemProvider;
mBackgroundStyle = backgroundStyle;
+
+ registerListItemViewType(LIST_ITEM_TYPE_TEXT,
+ R.layout.car_list_item_text_content, TextListItem::createViewHolder);
+ registerListItemViewType(LIST_ITEM_TYPE_SEEKBAR,
+ R.layout.car_list_item_seekbar_content, SeekbarListItem::createViewHolder);
}
@Override
- public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ if (mViewHolderLayoutResIds.get(viewType) == 0
+ || mViewHolderCreator.get(viewType) == null) {
+ throw new IllegalArgumentException("Unregistered view type.");
+ }
+
LayoutInflater inflater = LayoutInflater.from(mContext);
- View itemView = inflater.inflate(R.layout.car_paged_list_item_content, parent, false);
+ View itemView = inflater.inflate(mViewHolderLayoutResIds.get(viewType), parent, false);
ViewGroup container = createListItemContainer();
container.addView(itemView);
- return new ViewHolder(container);
+ return mViewHolderCreator.get(viewType).apply(container);
}
+ /**
+ * Creates a view with background set by {@link BackgroundStyle}.
+ */
private ViewGroup createListItemContainer() {
ViewGroup container;
switch (mBackgroundStyle) {
@@ -121,7 +170,12 @@ public class ListItemAdapter extends
}
@Override
- public void onBindViewHolder(ViewHolder holder, int position) {
+ public int getItemViewType(int position) {
+ return mItemProvider.get(position).getViewType();
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
ListItem item = mItemProvider.get(position);
item.bind(holder);
@@ -143,86 +197,13 @@ public class ListItemAdapter extends
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;
- }
+ @Override
+ public boolean shouldHideDivider(int position) {
+ // By default we should show the divider i.e. return false.
- public View getAction2Divider() {
- return mAction2Divider;
- }
+ // Check if position is within range, and then check the item flag.
+ return position >= 0 && position < getItemCount()
+ && mItemProvider.get(position).shouldHideDivider();
}
+
}
diff --git a/androidx/car/widget/PagedListView.java b/androidx/car/widget/PagedListView.java
index d90d670b..654cba99 100644
--- a/androidx/car/widget/PagedListView.java
+++ b/androidx/car/widget/PagedListView.java
@@ -40,6 +40,7 @@ import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
+import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.car.R;
@@ -123,6 +124,10 @@ public class PagedListView extends FrameLayout {
private boolean mNeedsFocus;
+ @Gutter
+ private int mGutter;
+ private int mGutterSize;
+
/**
* Interface for a {@link android.support.v7.widget.RecyclerView.Adapter} to cap the number of
* items.
@@ -153,6 +158,23 @@ public class PagedListView extends FrameLayout {
}
/**
+ * Interface for controlling visibility of item dividers for individual items based on the
+ * item's position.
+ *
+ * <p> NOTE: interface takes effect only when dividers are enabled.
+ */
+ public interface DividerVisibilityManager {
+ /**
+ * Given an item position, returns whether the divider coming after that item should be
+ * hidden.
+ *
+ * @param position item position inside the adapter.
+ * @return true if divider is to be hidden, false if divider should be shown.
+ */
+ boolean shouldHideDivider(int position);
+ }
+
+ /**
* The possible values for @{link #setGutter}. The default value is actually
* {@link Gutter#BOTH}.
*/
@@ -228,12 +250,16 @@ public class PagedListView extends FrameLayout {
new LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false);
mRecyclerView.setLayoutManager(layoutManager);
- mSnapHelper = new PagedSnapHelper();
+ mSnapHelper = new PagedSnapHelper(context);
mSnapHelper.attachToRecyclerView(mRecyclerView);
mRecyclerView.setOnScrollListener(mRecyclerViewOnScrollListener);
mRecyclerView.getRecycledViewPool().setMaxRecycledViews(0, 12);
+ int defaultGutterSize = getResources().getDimensionPixelSize(R.dimen.car_margin);
+ mGutterSize = a.getDimensionPixelSize(R.styleable.PagedListView_gutterSize,
+ defaultGutterSize);
+
if (a.hasValue(R.styleable.PagedListView_gutter)) {
int gutter = a.getInt(R.styleable.PagedListView_gutter, Gutter.BOTH);
setGutter(gutter);
@@ -264,6 +290,12 @@ public class PagedListView extends FrameLayout {
mRecyclerView.addItemDecoration(new ItemSpacingDecoration(itemSpacing));
}
+ int listContentTopMargin =
+ a.getDimensionPixelSize(R.styleable.PagedListView_listContentTopOffset, 0);
+ if (listContentTopMargin > 0) {
+ mRecyclerView.addItemDecoration(new TopOffsetDecoration(listContentTopMargin));
+ }
+
// Set this to true so that this view consumes clicks events and views underneath
// don't receive this click event. Without this it's possible to click places in the
// view that don't capture the event, and as a result, elements visually hidden consume
@@ -307,11 +339,20 @@ public class PagedListView extends FrameLayout {
mScrollBarView.setVisibility(mScrollBarEnabled ? VISIBLE : GONE);
- // Modify the layout the Scroll Bar is not visible.
- if (!mScrollBarEnabled) {
+ if (mScrollBarEnabled) {
+ int topMargin =
+ a.getDimensionPixelSize(R.styleable.PagedListView_scrollBarTopMargin, 0);
+ setScrollBarTopMargin(topMargin);
+ } else {
MarginLayoutParams params = (MarginLayoutParams) mRecyclerView.getLayoutParams();
params.setMarginStart(0);
- mRecyclerView.setLayoutParams(params);
+ }
+
+ if (a.hasValue(R.styleable.PagedListView_scrollBarContainerWidth)) {
+ int carMargin = getResources().getDimensionPixelSize(R.dimen.car_margin);
+ int scrollBarContainerWidth = a.getDimensionPixelSize(
+ R.styleable.PagedListView_scrollBarContainerWidth, carMargin);
+ setScrollBarContainerWidth(scrollBarContainerWidth);
}
setDayNightStyle(DayNightStyle.AUTO);
@@ -341,20 +382,22 @@ public class PagedListView extends FrameLayout {
/**
* 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
+ * <p>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) {
+ mGutter = gutter;
+
int startPadding = 0;
int endPadding = 0;
- if ((gutter & Gutter.START) != 0) {
- startPadding = getResources().getDimensionPixelSize(R.dimen.car_margin);
+ if ((mGutter & Gutter.START) != 0) {
+ startPadding = mGutterSize;
}
- if ((gutter & Gutter.END) != 0) {
- endPadding = getResources().getDimensionPixelSize(R.dimen.car_margin);
+ if ((mGutter & Gutter.END) != 0) {
+ endPadding = mGutterSize;
}
mRecyclerView.setPaddingRelative(startPadding, 0, endPadding, 0);
@@ -363,6 +406,69 @@ public class PagedListView extends FrameLayout {
mRecyclerView.setClipToPadding(startPadding == 0 && endPadding == 0);
}
+ /**
+ * Sets the size of the gutter that appears at the start, end or both sizes of the items in
+ * the {@code PagedListView}.
+ *
+ * @param gutterSize The size of the gutter in pixels.
+ * @see #setGutter(int)
+ */
+ public void setGutterSize(int gutterSize) {
+ mGutterSize = gutterSize;
+
+ // Call setGutter to reset the gutter.
+ setGutter(mGutter);
+ }
+
+ /**
+ * Sets the width of the container that holds the scrollbar. The scrollbar will be centered
+ * within this width.
+ *
+ * @param width The width of the scrollbar container.
+ */
+ public void setScrollBarContainerWidth(int width) {
+ ViewGroup.LayoutParams layoutParams = mScrollBarView.getLayoutParams();
+ layoutParams.width = width;
+ mScrollBarView.requestLayout();
+ }
+
+ /**
+ * Sets the top margin above the scroll bar. By default, this margin is 0.
+ *
+ * @param topMargin The top margin.
+ */
+ public void setScrollBarTopMargin(int topMargin) {
+ MarginLayoutParams params = (MarginLayoutParams) mScrollBarView.getLayoutParams();
+ params.topMargin = topMargin;
+ mScrollBarView.requestLayout();
+ }
+
+ /**
+ * Sets an offset above the first item in the {@code PagedListView}. This offset is scrollable
+ * with the contents of the list.
+ *
+ * @param offset The top offset to add.
+ */
+ public void setListContentTopOffset(int offset) {
+ TopOffsetDecoration existing = null;
+ for (int i = 0, count = mRecyclerView.getItemDecorationCount(); i < count; i++) {
+ RecyclerView.ItemDecoration itemDecoration = mRecyclerView.getItemDecorationAt(i);
+ if (itemDecoration instanceof TopOffsetDecoration) {
+ existing = (TopOffsetDecoration) itemDecoration;
+ break;
+ }
+ }
+
+ if (offset == 0 && existing != null) {
+ mRecyclerView.removeItemDecoration(existing);
+ } else if (existing == null) {
+ mRecyclerView.addItemDecoration(new TopOffsetDecoration(offset));
+ } else {
+ existing.setTopOffset(offset);
+ }
+ mRecyclerView.invalidateItemDecorations();
+ }
+
@NonNull
public RecyclerView getRecyclerView() {
return mRecyclerView;
@@ -374,14 +480,15 @@ public class PagedListView extends FrameLayout {
* @param position The position in the list to scroll to.
*/
public void scrollToPosition(int position) {
- if (mRecyclerView.getLayoutManager() == null) {
+ RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
+ if (layoutManager == null) {
return;
}
- PagedSmoothScroller smoothScroller = new PagedSmoothScroller(getContext());
+ RecyclerView.SmoothScroller smoothScroller = mSnapHelper.createScroller(layoutManager);
smoothScroller.setTargetPosition(position);
- mRecyclerView.getLayoutManager().startSmoothScroll(smoothScroller);
+ layoutManager.startSmoothScroll(smoothScroller);
// Sometimes #scrollToPosition doesn't change the scroll state so we need to make sure
// the pagination arrows actually get updated. See b/15801119
@@ -409,9 +516,26 @@ public class PagedListView extends FrameLayout {
@NonNull RecyclerView.Adapter<? extends RecyclerView.ViewHolder> adapter) {
mAdapter = adapter;
mRecyclerView.setAdapter(adapter);
+
updateMaxItems();
}
+ /**
+ * Sets {@link DividerVisibilityManager} on all {@code DividerDecoration} item decorations.
+ *
+ * @param dvm {@code DividerVisibilityManager} to be set.
+ */
+ public void setDividerVisibilityManager(DividerVisibilityManager dvm) {
+ int decorCount = mRecyclerView.getItemDecorationCount();
+ for (int i = 0; i < decorCount; i++) {
+ RecyclerView.ItemDecoration decor = mRecyclerView.getItemDecorationAt(i);
+ if (decor instanceof DividerDecoration) {
+ ((DividerDecoration) decor).setVisibilityManager(dvm);
+ }
+ }
+ mRecyclerView.invalidateItemDecorations();
+ }
+
@Nullable
@SuppressWarnings("unchecked")
public RecyclerView.Adapter<? extends RecyclerView.ViewHolder> getAdapter() {
@@ -960,6 +1084,7 @@ public class PagedListView extends FrameLayout {
private final int mDividerStartMargin;
@IdRes private final int mDividerStartId;
@IdRes private final int mDividerEndId;
+ private DividerVisibilityManager mVisibilityManager;
/**
* @param dividerStartMargin The start offset of the dividing line. This offset will be
@@ -989,11 +1114,23 @@ public class PagedListView extends FrameLayout {
mPaint.setColor(mContext.getResources().getColor(R.color.car_list_divider));
}
+ /** Sets {@link DividerVisibilityManager} on the DividerDecoration.*/
+ public void setVisibilityManager(DividerVisibilityManager dvm) {
+ mVisibilityManager = dvm;
+ }
+
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
// Draw a divider line between each item. No need to draw the line for the last item.
for (int i = 0, childCount = parent.getChildCount(); i < childCount - 1; i++) {
View container = parent.getChildAt(i);
+
+ // if divider should be hidden for this item, proceeds without drawing it
+ int itemPosition = parent.getChildAdapterPosition(container);
+ if (hideDividerForAdapterPosition(itemPosition)) {
+ continue;
+ }
+
View nextContainer = parent.getChildAt(i + 1);
int spacing = nextContainer.getTop() - container.getBottom();
@@ -1034,14 +1171,52 @@ public class PagedListView extends FrameLayout {
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) {
+ int pos = parent.getChildAdapterPosition(view);
+
+ // Skip top offset when there is no divider above.
+ if (pos > 0 && !hideDividerForAdapterPosition(pos - 1)) {
outRect.top = mDividerHeight / 2;
}
- if (position < state.getItemCount() - 1) {
+
+ // Skip bottom offset when there is no divider below.
+ if (pos < state.getItemCount() - 1 && !hideDividerForAdapterPosition(pos)) {
outRect.bottom = mDividerHeight / 2;
}
}
+
+ private boolean hideDividerForAdapterPosition(int position) {
+ return mVisibilityManager != null && mVisibilityManager.shouldHideDivider(position);
+ }
+ }
+
+ /**
+ * A {@link android.support.v7.widget.RecyclerView.ItemDecoration} that will add a top offset
+ * to the first item in the RecyclerView it is added to.
+ */
+ private static class TopOffsetDecoration extends RecyclerView.ItemDecoration {
+ private int mTopOffset;
+
+ private TopOffsetDecoration(int topOffset) {
+ mTopOffset = topOffset;
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+ RecyclerView.State state) {
+ super.getItemOffsets(outRect, view, parent, state);
+ int position = parent.getChildAdapterPosition(view);
+
+ // Only set the offset for the first item.
+ if (position == 0) {
+ outRect.top = mTopOffset;
+ }
+ }
+
+ /**
+ * @param topOffset sets spacing between each item.
+ */
+ public void setTopOffset(int topOffset) {
+ mTopOffset = topOffset;
+ }
}
}
diff --git a/androidx/car/widget/PagedSnapHelper.java b/androidx/car/widget/PagedSnapHelper.java
index ad1c7104..9bf1fb6d 100644
--- a/androidx/car/widget/PagedSnapHelper.java
+++ b/androidx/car/widget/PagedSnapHelper.java
@@ -16,6 +16,7 @@
package androidx.car.widget;
+import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearSnapHelper;
@@ -36,6 +37,7 @@ public class PagedSnapHelper extends LinearSnapHelper {
*/
private static final float VIEW_VISIBLE_THRESHOLD = 0.5f;
+ private final PagedSmoothScroller mSmoothScroller;
private RecyclerView mRecyclerView;
// Orientation helpers are lazily created per LayoutManager.
@@ -45,6 +47,10 @@ public class PagedSnapHelper extends LinearSnapHelper {
@Nullable
private OrientationHelper mHorizontalHelper;
+ public PagedSnapHelper(Context context) {
+ mSmoothScroller = new PagedSmoothScroller(context);
+ }
+
@Override
public int[] calculateDistanceToFinalSnap(@NonNull RecyclerView.LayoutManager layoutManager,
@NonNull View targetView) {
@@ -167,6 +173,20 @@ public class PagedSnapHelper extends LinearSnapHelper {
}
/**
+ * Returns a scroller specific to this {@code PagedSnapHelper}. This scroller is used for all
+ * smooth scrolling operations, including flings.
+ *
+ * @param layoutManager The {@link RecyclerView.LayoutManager} associated with the attached
+ * {@link RecyclerView}.
+ *
+ * @return a {@link RecyclerView.SmoothScroller} which will handle the scrolling.
+ */
+ @Override
+ protected RecyclerView.SmoothScroller createScroller(RecyclerView.LayoutManager layoutManager) {
+ return mSmoothScroller;
+ }
+
+ /**
* 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.
diff --git a/androidx/car/widget/SeekbarListItem.java b/androidx/car/widget/SeekbarListItem.java
new file mode 100644
index 00000000..54c37ef9
--- /dev/null
+++ b/androidx/car/widget/SeekbarListItem.java
@@ -0,0 +1,582 @@
+/*
+ * Copyright 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 androidx.car.widget;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.IdRes;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.text.TextUtils;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.car.R;
+
+/**
+ * Class to build a list item with {@link SeekBar}.
+ *
+ * <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 - no icon is shown.
+ * <li>Empty Icon - {@code Seekbar} offsets start space as if there was an icon.
+ * </ul>
+ * <li>{@code Seekbar}: with optional {@code Text}.
+ * <li>{@code Supplemental Action}: presented by an icon of following types; aligned to
+ * the end of item.
+ * <ul>
+ * <li>Supplemental Icon.
+ * <li>Supplemental Empty Icon - {@code Seekbar} offsets end space as if there was an icon.
+ * </ul>
+ * </ul>
+ *
+ * {@code SeekbarListItem} binds data to {@link ViewHolder} based on components selected.
+ *
+ * <p>When conflicting methods are called (e.g. setting primary action to both primary icon and
+ * no icon), the last called method wins.
+ */
+public class SeekbarListItem extends ListItem<SeekbarListItem.ViewHolder> {
+
+ @Retention(SOURCE)
+ @IntDef({
+ PRIMARY_ACTION_TYPE_NO_ICON, PRIMARY_ACTION_TYPE_EMPTY_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_SMALL_ICON = 2;
+
+ @Retention(SOURCE)
+ @IntDef({SUPPLEMENTAL_ACTION_NO_ACTION, SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON,
+ SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON,
+ SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON_WITH_DIVIDER})
+ 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_SUPPLEMENTAL_EMPTY_ICON = 2;
+ private static final int SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON_WITH_DIVIDER = 3;
+
+ private final Context mContext;
+
+ private final List<ViewBinder<ViewHolder>> mBinders = new ArrayList<>();
+ // Store custom binders separately so they will bind after binders created by setters.
+ private final List<ViewBinder<ViewHolder>> mCustomBinders = new ArrayList<>();
+
+ @PrimaryActionType private int mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
+ private int mPrimaryActionIconResId;
+ private Drawable mPrimaryActionIconDrawable;
+
+ private String mText;
+
+ private int mProgress;
+ private int mMax;
+ private SeekBar.OnSeekBarChangeListener mOnSeekBarChangeListener;
+
+ @SupplementalActionType private int mSupplementalActionType = SUPPLEMENTAL_ACTION_NO_ACTION;
+ private int mSupplementalIconResId;
+ private View.OnClickListener mSupplementalIconOnClickListener;
+ private boolean mShowSupplementalIconDivider;
+
+ /**
+ * Creates a {@link SeekbarListItem.ViewHolder}.
+ */
+ public static ViewHolder createViewHolder(View itemView) {
+ return new ViewHolder(itemView);
+ }
+
+ /**
+ * Creates a SeekbarListItem.
+ *
+ * @param context context
+ * @param max the upper range of the SeekBar.
+ * @param progress the current progress of the specified value.
+ * @param listener listener to receive notification of changes to progress level.
+ * @param text displays a text on top of the SeekBar. Text beyond length required by
+ * regulation will be truncated. null value hides the text field.
+ */
+ public SeekbarListItem(Context context, int max, int progress,
+ SeekBar.OnSeekBarChangeListener listener, String text) {
+ mContext = context;
+
+ mMax = max;
+ mProgress = progress;
+ mOnSeekBarChangeListener = listener;
+
+ int limit = mContext.getResources().getInteger(
+ R.integer.car_list_item_text_length_limit);
+ if (TextUtils.isEmpty(text) || text.length() < limit) {
+ mText = text;
+ } else {
+ mText = text.substring(0, limit) + mContext.getString(R.string.ellipsis);
+ }
+
+ markDirty();
+ }
+
+ /**
+ * Used by {@link ListItemAdapter} to choose layout to inflate for view holder.
+ */
+ @Override
+ int getViewType() {
+ return ListItemAdapter.LIST_ITEM_TYPE_SEEKBAR;
+ }
+
+ /**
+ * Applies all {@link ViewBinder} to {@code ViewHolder}.
+ */
+ @Override
+ public void bind(ViewHolder viewHolder) {
+ if (isDirty()) {
+ mBinders.clear();
+
+ // Create binders that adjust layout params of each view.
+ setItemLayoutHeight();
+ setPrimaryAction();
+ setSeekBarAndText();
+ setSupplementalAction();
+
+ // Custom view binders are always applied after the one created by this class.
+ mBinders.addAll(mCustomBinders);
+
+ markClean();
+ }
+
+ // Hide all subviews then apply view binders to adjust subviews.
+ setSubViewsGone(viewHolder);
+ for (ViewBinder binder : mBinders) {
+ binder.bind(viewHolder);
+ }
+ }
+
+ private void setSubViewsGone(ViewHolder vh) {
+ View[] subviews = new View[] {
+ vh.getPrimaryIcon(),
+ // SeekBar is always visible.
+ vh.getText(),
+ vh.getSupplementalIcon(), vh.getSupplementalIconDivider(),
+ };
+ for (View v : subviews) {
+ v.setVisibility(View.GONE);
+ }
+ }
+
+ private void setItemLayoutHeight() {
+ int minHeight = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_single_line_list_item_height);
+ mBinders.add(vh -> {
+ vh.itemView.setMinimumHeight(minHeight);
+ vh.getContainerLayout().setMinimumHeight(minHeight);
+
+ ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams();
+ layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
+ vh.itemView.requestLayout();
+ });
+ }
+
+ private void setPrimaryAction() {
+ setPrimaryActionLayout();
+ setPrimaryActionContent();
+ }
+
+ private void setSeekBarAndText() {
+ setSeekBarAndTextContent();
+ setSeekBarAndTextLayout();
+ }
+
+ private void setSupplementalAction() {
+ setSupplementalActionLayout();
+ setSupplementalActionContent();
+ }
+
+ private void setPrimaryActionLayout() {
+ switch (mPrimaryActionType) {
+ case PRIMARY_ACTION_TYPE_NO_ICON:
+ case PRIMARY_ACTION_TYPE_EMPTY_ICON:
+ // Do nothing.
+ break;
+ case PRIMARY_ACTION_TYPE_SMALL_ICON:
+ int startMargin = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_keyline_1);
+ int iconSize = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_primary_icon_size);
+ mBinders.add(vh -> {
+ RelativeLayout.LayoutParams layoutParams =
+ (RelativeLayout.LayoutParams) vh.getPrimaryIcon().getLayoutParams();
+ // Icon size.
+ layoutParams.height = layoutParams.width = iconSize;
+ // Start margin.
+ layoutParams.addRule(RelativeLayout.ALIGN_PARENT_START);
+ layoutParams.setMarginStart(startMargin);
+ layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
+
+ vh.getPrimaryIcon().requestLayout();
+ });
+ break;
+ default:
+ throw new IllegalStateException("Unknown primary action type.");
+ }
+ }
+
+ private void setPrimaryActionContent() {
+ switch (mPrimaryActionType) {
+ case PRIMARY_ACTION_TYPE_NO_ICON:
+ case PRIMARY_ACTION_TYPE_EMPTY_ICON:
+ // Do nothing.
+ break;
+ case PRIMARY_ACTION_TYPE_SMALL_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;
+ default:
+ throw new IllegalStateException("Unknown primary action type.");
+ }
+ }
+
+ private void setSeekBarAndTextContent() {
+ mBinders.add(vh -> {
+ vh.getSeekBar().setMax(mMax);
+ vh.getSeekBar().setProgress(mProgress);
+ vh.getSeekBar().setOnSeekBarChangeListener(mOnSeekBarChangeListener);
+
+ if (!TextUtils.isEmpty(mText)) {
+ vh.getText().setVisibility(View.VISIBLE);
+ vh.getText().setText(mText);
+ vh.getText().setTextAppearance(R.style.CarBody1);
+ }
+ });
+ }
+
+ private void setSeekBarAndTextLayout() {
+ mBinders.add(vh -> {
+ // SeekBar is below text with a gap.
+ ViewGroup.MarginLayoutParams seekBarLayoutParams =
+ (ViewGroup.MarginLayoutParams) vh.getSeekBar().getLayoutParams();
+ seekBarLayoutParams.topMargin = TextUtils.isEmpty(mText)
+ ? 0
+ : mContext.getResources().getDimensionPixelSize(R.dimen.car_padding_1);
+ vh.getSeekBar().requestLayout();
+
+ // Set start and end margin of text and seek bar.
+ setViewStartMargin(vh.getSeekBarContainer());
+ setViewEndMargin(vh.getSeekBarContainer());
+
+ RelativeLayout.LayoutParams containerLayoutParams =
+ (RelativeLayout.LayoutParams) vh.getSeekBarContainer().getLayoutParams();
+ containerLayoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
+ });
+ }
+
+ // Helper method to set start margin of seekbar/text.
+ private void setViewStartMargin(View v) {
+ int startMarginResId;
+ switch (mPrimaryActionType) {
+ case PRIMARY_ACTION_TYPE_NO_ICON:
+ startMarginResId = R.dimen.car_keyline_1;
+ break;
+ case PRIMARY_ACTION_TYPE_EMPTY_ICON:
+ case PRIMARY_ACTION_TYPE_SMALL_ICON:
+ startMarginResId = R.dimen.car_keyline_3;
+ break;
+ default:
+ throw new IllegalStateException("Unknown primary action type.");
+ }
+ ViewGroup.MarginLayoutParams layoutParams =
+ (ViewGroup.MarginLayoutParams) v.getLayoutParams();
+ layoutParams.setMarginStart(
+ mContext.getResources().getDimensionPixelSize(startMarginResId));
+ v.requestLayout();
+ }
+
+ // Helper method to set end margin of seekbar/text.
+ private void setViewEndMargin(View v) {
+ RelativeLayout.LayoutParams layoutParams =
+ (RelativeLayout.LayoutParams) v.getLayoutParams();
+ int endMargin = 0;
+ switch (mSupplementalActionType) {
+ case SUPPLEMENTAL_ACTION_NO_ACTION:
+ // Aligned to parent end with margin.
+ layoutParams.addRule(RelativeLayout.ALIGN_PARENT_END);
+ layoutParams.removeRule(RelativeLayout.START_OF);
+ layoutParams.setMarginEnd(mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_keyline_1));
+ break;
+ case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON:
+ // Align to start of divider with padding.
+ layoutParams.addRule(RelativeLayout.START_OF, R.id.supplemental_icon_divider);
+ layoutParams.removeRule(RelativeLayout.ALIGN_PARENT_END);
+ layoutParams.setMarginEnd(mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_padding_4));
+ break;
+ case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON_WITH_DIVIDER:
+ // Align to parent end with a margin as if the icon and an optional divider were
+ // present. We do this by setting
+
+ // Add divider padding to icon, and width of divider.
+ endMargin += mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_padding_4);
+ endMargin += mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_vertical_line_divider_width);
+ // Fall through.
+ case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON:
+ // Add view padding, width of icon, and icon end margin.
+ endMargin += mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_padding_4);
+ endMargin += mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_primary_icon_size);
+ endMargin += mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_keyline_1);
+
+ layoutParams.addRule(RelativeLayout.ALIGN_PARENT_END);
+ layoutParams.removeRule(RelativeLayout.START_OF);
+ layoutParams.setMarginEnd(endMargin);
+ break;
+ default:
+ throw new IllegalStateException("Unknown supplemental action type.");
+ }
+ v.requestLayout();
+ }
+
+ private void setSupplementalActionLayout() {
+ int keyline1 = mContext.getResources().getDimensionPixelSize(R.dimen.car_keyline_1);
+ int padding4 = mContext.getResources().getDimensionPixelSize(R.dimen.car_padding_4);
+ mBinders.add(vh -> {
+ RelativeLayout.LayoutParams iconLayoutParams =
+ (RelativeLayout.LayoutParams) vh.getSupplementalIcon().getLayoutParams();
+ // Align to parent end with margin.
+ iconLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_END);
+ iconLayoutParams.setMarginEnd(keyline1);
+ iconLayoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
+
+ vh.getSupplementalIcon().requestLayout();
+
+ // Divider aligns to the start of supplemental icon with margin.
+ RelativeLayout.LayoutParams dividerLayoutParams =
+ (RelativeLayout.LayoutParams) vh.getSupplementalIconDivider()
+ .getLayoutParams();
+ dividerLayoutParams.addRule(RelativeLayout.START_OF, R.id.supplemental_icon);
+ dividerLayoutParams.setMarginEnd(padding4);
+ dividerLayoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
+
+ vh.getSupplementalIconDivider().requestLayout();
+ });
+ }
+
+ private void setSupplementalActionContent() {
+ switch (mSupplementalActionType) {
+ case SUPPLEMENTAL_ACTION_NO_ACTION:
+ case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON_WITH_DIVIDER:
+ case SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON:
+ break;
+ 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;
+ default:
+ throw new IllegalStateException("Unknown supplemental action type.");
+ }
+ }
+
+ /**
+ * Sets {@code Primary Action} to be represented by an icon.
+ *
+ * @param iconResId the resource identifier of the drawable.
+ */
+ public void setPrimaryActionIcon(@DrawableRes int iconResId) {
+ setPrimaryActionIcon(null, iconResId);
+ }
+
+ /**
+ * Sets {@code Primary Action} to be represented by an icon.
+ *
+ * @param drawable the Drawable to set, or null to clear the content.
+ */
+ public void setPrimaryActionIcon(Drawable drawable) {
+ setPrimaryActionIcon(drawable, 0);
+ }
+
+ private void setPrimaryActionIcon(Drawable drawable, @DrawableRes int iconResId) {
+ mPrimaryActionType = PRIMARY_ACTION_TYPE_SMALL_ICON;
+
+ mPrimaryActionIconDrawable = drawable;
+ mPrimaryActionIconResId = iconResId;
+
+ markDirty();
+ }
+
+ /**
+ * Sets {@code Primary Action} to be empty icon.
+ *
+ * {@code Seekbar} would have a start margin as if {@code Primary Action} were set as icon.
+ */
+ public void setPrimaryActionEmptyIcon() {
+ mPrimaryActionType = PRIMARY_ACTION_TYPE_EMPTY_ICON;
+
+ markDirty();
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+ */
+ public void setSupplementalIcon(int iconResId, boolean showSupplementalIconDivider) {
+ setSupplementalIcon(iconResId, showSupplementalIconDivider, null);
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+ */
+ public void setSupplementalIcon(@IdRes int iconResId,
+ boolean showSupplementalIconDivider, @Nullable View.OnClickListener listener) {
+ mSupplementalActionType = SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON;
+
+ mSupplementalIconResId = iconResId;
+ mShowSupplementalIconDivider = showSupplementalIconDivider;
+ mSupplementalIconOnClickListener = listener;
+
+ markDirty();
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be empty icon.
+ *
+ * {@code Seekbar} would have an end margin as if {@code Supplemental Action} were set.
+ */
+ public void setSupplementalEmptyIcon(boolean seekbarOffsetDividerWidth) {
+ mSupplementalActionType = seekbarOffsetDividerWidth
+ ? SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON_WITH_DIVIDER
+ : SUPPLEMENTAL_ACTION_SUPPLEMENTAL_EMPTY_ICON;
+ markDirty();
+ }
+
+ /**
+ * Adds {@code ViewBinder} to interact with sub-views in {@link ViewHolder}. These ViewBinders
+ * will always be applied after other {@code setFoobar} methods have bound.
+ *
+ * <p>Make sure to call setFoobar() method on the intended sub-view first.
+ *
+ * <p>Example:
+ * <pre>
+ * {@code
+ * SeekbarListItem item = new SeebarListItem(context);
+ * item.setPrimaryActionIcon(R.drawable.icon);
+ * item.addViewBinder((viewHolder) -> {
+ * viewHolder.getPrimaryIcon().doMoreStuff();
+ * });
+ * }
+ * </pre>
+ */
+ public void addViewBinder(ViewBinder<ViewHolder> viewBinder) {
+ mCustomBinders.add(viewBinder);
+ markDirty();
+ }
+
+ /**
+ * Holds views of SeekbarListItem.
+ */
+ public static class ViewHolder extends RecyclerView.ViewHolder {
+
+ private RelativeLayout mContainerLayout;
+
+ private ImageView mPrimaryIcon;
+
+ private LinearLayout mSeekBarContainer;
+ private TextView mText;
+ private SeekBar mSeekBar;
+
+ private View mSupplementalIconDivider;
+ private ImageView mSupplementalIcon;
+
+ public ViewHolder(View itemView) {
+ super(itemView);
+
+ mContainerLayout = itemView.findViewById(R.id.container);
+
+ mPrimaryIcon = itemView.findViewById(R.id.primary_icon);
+
+ mSeekBarContainer = itemView.findViewById(R.id.seek_bar_container);
+ mText = itemView.findViewById(R.id.text);
+ mSeekBar = itemView.findViewById(R.id.seek_bar);
+
+ mSupplementalIcon = itemView.findViewById(R.id.supplemental_icon);
+ mSupplementalIconDivider = itemView.findViewById(R.id.supplemental_icon_divider);
+ }
+
+ public RelativeLayout getContainerLayout() {
+ return mContainerLayout;
+ }
+
+ public ImageView getPrimaryIcon() {
+ return mPrimaryIcon;
+ }
+
+ public LinearLayout getSeekBarContainer() {
+ return mSeekBarContainer;
+ }
+
+ public TextView getText() {
+ return mText;
+ }
+
+ public SeekBar getSeekBar() {
+ return mSeekBar;
+ }
+
+ public ImageView getSupplementalIcon() {
+ return mSupplementalIcon;
+ }
+
+ public View getSupplementalIconDivider() {
+ return mSupplementalIconDivider;
+ }
+ }
+}
diff --git a/androidx/car/widget/TextListItem.java b/androidx/car/widget/TextListItem.java
new file mode 100644
index 00000000..71b6c969
--- /dev/null
+++ b/androidx/car/widget/TextListItem.java
@@ -0,0 +1,827 @@
+/*
+ * Copyright 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 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.view.ViewGroup;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.ImageView;
+import android.widget.RelativeLayout;
+import android.widget.Switch;
+import android.widget.TextView;
+
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.List;
+
+import androidx.car.R;
+
+/**
+ * Class to build a list item of text.
+ *
+ * <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 - no icon is shown.
+ * <li>Empty Icon - {@code Text} offsets start space as if there was an icon.
+ * </ul>
+ * <li>{@code Text}: supports any combination of the following text views.
+ * <ul>
+ * <li>Title
+ * <li>Body
+ * </ul>
+ * <li>{@code Supplemental Action}: 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
+ * <li>Switch
+ * </ul>
+ * </ul>
+ *
+ * <p>{@code TextListItem} binds data to {@link ViewHolder} based on components selected.
+ *
+ * <p>When conflicting setter methods are called (e.g. setting primary action to both primary icon
+ * and no icon), the last called method wins.
+ */
+public class TextListItem extends ListItem<TextListItem.ViewHolder> {
+
+ @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,
+ SUPPLEMENTAL_ACTION_SWITCH})
+ 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 static final int SUPPLEMENTAL_ACTION_SWITCH = 4;
+
+ private final Context mContext;
+
+ private final List<ViewBinder<ViewHolder>> mBinders = new ArrayList<>();
+ // Store custom binders separately so they will bind after binders created by setters.
+ private final List<ViewBinder<ViewHolder>> 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 boolean mSwitchChecked;
+ private boolean mShowSwitchDivider;
+ private CompoundButton.OnCheckedChangeListener mSwitchOnCheckedChangeListener;
+
+ private String mAction1Text;
+ private View.OnClickListener mAction1OnClickListener;
+ private boolean mShowAction1Divider;
+ private String mAction2Text;
+ private View.OnClickListener mAction2OnClickListener;
+ private boolean mShowAction2Divider;
+
+ /**
+ * Creates a {@link TextListItem.ViewHolder}.
+ */
+ public static ViewHolder createViewHolder(View itemView) {
+ return new ViewHolder(itemView);
+ }
+
+ public TextListItem(Context context) {
+ mContext = context;
+ markDirty();
+ }
+
+ /**
+ * Used by {@link ListItemAdapter} to choose layout to inflate for view holder.
+ */
+ @Override
+ public int getViewType() {
+ return ListItemAdapter.LIST_ITEM_TYPE_TEXT;
+ }
+
+ /**
+ * Applies all {@link ViewBinder} to {@code ViewHolder}.
+ */
+ @Override
+ public void bind(ViewHolder viewHolder) {
+ if (isDirty()) {
+ mBinders.clear();
+
+ // Create binders that adjust layout params of each view.
+ setItemLayoutHeight();
+ setPrimaryAction();
+ setText();
+ setSupplementalActions();
+ setOnClickListener();
+
+ // Custom view binders are always applied after the one created by this class.
+ mBinders.addAll(mCustomBinders);
+
+ markClean();
+ }
+
+ // Hide all subviews then apply view binders to adjust subviews.
+ setAllSubViewsGone(viewHolder);
+ for (ViewBinder binder : mBinders) {
+ binder.bind(viewHolder);
+ }
+ }
+
+ void setAllSubViewsGone(ViewHolder vh) {
+ View[] subviews = new View[] {
+ vh.getPrimaryIcon(),
+ vh.getTitle(), vh.getBody(),
+ vh.getSupplementalIcon(), vh.getSupplementalIconDivider(),
+ vh.getSwitch(), vh.getSwitchDivider(),
+ vh.getAction1(), vh.getAction1Divider(), vh.getAction2(), vh.getAction2Divider()};
+ for (View v : subviews) {
+ v.setVisibility(View.GONE);
+ }
+ }
+
+ /**
+ * 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 = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_single_line_list_item_height);
+ mBinders.add(vh -> {
+ ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams();
+ layoutParams.height = height;
+ vh.itemView.requestLayout();
+ });
+ } else {
+ // If body is present, the item should be at least as tall as min height, and wraps
+ // content.
+ int minHeight = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_double_line_list_item_height);
+ mBinders.add(vh -> {
+ vh.itemView.setMinimumHeight(minHeight);
+ vh.getContainerLayout().setMinimumHeight(minHeight);
+
+ ViewGroup.LayoutParams layoutParams = vh.itemView.getLayoutParams();
+ layoutParams.height = RecyclerView.LayoutParams.WRAP_CONTENT;
+ vh.itemView.requestLayout();
+ });
+ }
+ }
+
+ private void setPrimaryAction() {
+ setPrimaryIconContent();
+ setPrimaryIconLayout();
+ }
+
+ private void setText() {
+ setTextContent();
+ setTextVerticalMargin();
+ // Only set 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 = layoutParams.width = iconSize;
+
+ // Start margin.
+ layoutParams.setMarginStart(mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_keyline_1));
+
+ if (!TextUtils.isEmpty(mBody)) {
+ // Set icon top margin so that the icon remains in the same position it
+ // would've been in for non-long-text item, namely so that the center
+ // line of icon matches that of line item.
+ layoutParams.removeRule(RelativeLayout.CENTER_VERTICAL);
+ int itemHeight = mContext.getResources().getDimensionPixelSize(
+ R.dimen.car_double_line_list_item_height);
+ layoutParams.topMargin = (itemHeight - iconSize) / 2;
+ } else {
+ // If the icon can be centered vertically, leave the work for framework.
+ layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
+ layoutParams.topMargin = 0;
+ }
+ vh.getPrimaryIcon().requestLayout();
+ });
+ 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 = layoutParams.width = iconSize;
+
+ // No start margin.
+ layoutParams.setMarginStart(0);
+
+ // Always centered vertically.
+ layoutParams.addRule(RelativeLayout.CENTER_VERTICAL);
+ layoutParams.topMargin = 0;
+
+ vh.getPrimaryIcon().requestLayout();
+ });
+ 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().requestLayout();
+
+ RelativeLayout.LayoutParams bodyLayoutParams =
+ (RelativeLayout.LayoutParams) vh.getBody().getLayoutParams();
+ bodyLayoutParams.setMarginStart(startMargin);
+ vh.getBody().requestLayout();
+ });
+ }
+
+ /**
+ * 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().requestLayout();
+ });
+ } 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().requestLayout();
+ });
+ } 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().requestLayout();
+ // 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().requestLayout();
+ });
+ }
+ }
+
+ /**
+ * 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;
+ case SUPPLEMENTAL_ACTION_SWITCH:
+ mBinders.add(vh -> {
+ vh.getSwitch().setVisibility(View.VISIBLE);
+ vh.getSwitch().setChecked(mSwitchChecked);
+ vh.getSwitch().setOnCheckedChangeListener(mSwitchOnCheckedChangeListener);
+ if (mShowSwitchDivider) {
+ vh.getSwitchDivider().setVisibility(View.VISIBLE);
+ }
+ });
+ break;
+ default:
+ throw new IllegalArgumentException("Unrecognized supplemental action type.");
+ }
+ }
+
+ /**
+ * Sets {@link View.OnClickListener} of {@code TextListItem}.
+ */
+ public void setOnClickListener(View.OnClickListener listener) {
+ mOnClickListener = listener;
+ markDirty();
+ }
+
+ /**
+ * 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.
+ */
+ public void setPrimaryActionIcon(@DrawableRes int iconResId, boolean useLargeIcon) {
+ setPrimaryActionIcon(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.
+ */
+ public void setPrimaryActionIcon(Drawable drawable, boolean useLargeIcon) {
+ setPrimaryActionIcon(drawable, 0, useLargeIcon);
+ }
+
+ private void setPrimaryActionIcon(Drawable drawable, @DrawableRes int iconResId,
+ boolean useLargeIcon) {
+ mPrimaryActionType = useLargeIcon
+ ? PRIMARY_ACTION_TYPE_LARGE_ICON
+ : PRIMARY_ACTION_TYPE_SMALL_ICON;
+ mPrimaryActionIconResId = iconResId;
+ mPrimaryActionIconDrawable = drawable;
+
+ markDirty();
+ }
+
+ /**
+ * Sets {@code Primary Action} to be empty icon.
+ *
+ * <p>{@code Text} would have a start margin as if {@code Primary Action} were set to primary
+ * icon.
+ */
+ public void setPrimaryActionEmptyIcon() {
+ mPrimaryActionType = PRIMARY_ACTION_TYPE_EMPTY_ICON;
+ markDirty();
+ }
+
+ /**
+ * Sets {@code Primary Action} to have no icon. Text would align to the start of item.
+ */
+ public void setPrimaryActionNoIcon() {
+ mPrimaryActionType = PRIMARY_ACTION_TYPE_NO_ICON;
+ markDirty();
+ }
+
+ /**
+ * Sets the title of item.
+ *
+ * <p>Primary text is {@code Title} by default. It can be set by
+ * {@link #setBody(String, boolean)}
+ *
+ * <p>{@code Title} text is limited to one line, and ellipses at the end.
+ *
+ * @param title text to display as title.
+ */
+ public void setTitle(String title) {
+ mTitle = title;
+ markDirty();
+ }
+
+ /**
+ * Sets the body text of item.
+ *
+ * <p>Text beyond length required by regulation will be truncated. Defaults {@code Title}
+ * text as the primary.
+ * @param body text to be displayed.
+ */
+ public void setBody(String body) {
+ setBody(body, false);
+ }
+
+ /**
+ * Sets the body text of item.
+ *
+ * <p>Text beyond length required by regulation will be truncated.
+ *
+ * @param body text to be displayed.
+ * @param asPrimary sets {@code Body Text} as primary text of item.
+ */
+ public void setBody(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;
+
+ markDirty();
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+ *
+ * @param iconResId drawable resource id.
+ * @param showDivider whether to display a vertical bar that separates {@code text} and
+ * {@code Supplemental Icon}.
+ */
+ public void setSupplementalIcon(int iconResId, boolean showDivider) {
+ setSupplementalIcon(iconResId, showDivider, null);
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by an {@code Supplemental Icon}.
+ *
+ * @param iconResId drawable resource id.
+ * @param showDivider whether to display a vertical bar that separates {@code text} and
+ * {@code Supplemental Icon}.
+ * @param listener the callback that will run when icon is clicked.
+ */
+ public void setSupplementalIcon(int iconResId, boolean showDivider,
+ View.OnClickListener listener) {
+ mSupplementalActionType = SUPPLEMENTAL_ACTION_SUPPLEMENTAL_ICON;
+
+ mSupplementalIconResId = iconResId;
+ mSupplementalIconOnClickListener = listener;
+ mShowSupplementalIconDivider = showDivider;
+ markDirty();
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by an {@code Action Button}.
+ *
+ * @param text button text to display.
+ * @param showDivider whether to display a vertical bar that separates {@code Text} and
+ * {@code Action Button}.
+ * @param listener the callback that will run when action button is clicked.
+ */
+ public void setAction(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;
+
+ markDirty();
+ }
+
+ /**
+ * 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 void setActions(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;
+
+ markDirty();
+ }
+
+ /**
+ * Sets {@code Supplemental Action} to be represented by a {@link android.widget.Switch}.
+ *
+ * @param checked initial value for switched.
+ * @param showDivider whether to display a vertical bar between switch and text.
+ * @param listener callback to be invoked when the checked state is markDirty.
+ */
+ public void setSwitch(boolean checked, boolean showDivider,
+ CompoundButton.OnCheckedChangeListener listener) {
+ mSupplementalActionType = SUPPLEMENTAL_ACTION_SWITCH;
+
+ mSwitchChecked = checked;
+ mShowSwitchDivider = showDivider;
+ mSwitchOnCheckedChangeListener = listener;
+
+ markDirty();
+ }
+
+ /**
+ * Adds {@link ViewBinder} to interact with sub-views in {@link ViewHolder}. These ViewBinders
+ * will always bind after other {@code setFoobar} methods have bound.
+ *
+ * <p>Make sure to call setFoobar() method on the intended sub-view first.
+ *
+ * <p>Example:
+ * <pre>
+ * {@code
+ * TextListItem item = new TextListItem(context);
+ * item.setTitle("title");
+ * item.addViewBinder((viewHolder) -> {
+ * viewHolder.getTitle().doMoreStuff();
+ * });
+ * }
+ * </pre>
+ */
+ public void addViewBinder(ViewBinder<ViewHolder> binder) {
+ mCustomBinders.add(binder);
+ markDirty();
+ }
+
+ /**
+ * Holds views of TextListItem.
+ */
+ 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;
+
+ private Switch mSwitch;
+ private View mSwitchDivider;
+
+ 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);
+
+ mSwitch = itemView.findViewById(R.id.switch_widget);
+ mSwitchDivider = itemView.findViewById(R.id.switch_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 View getSwitchDivider() {
+ return mSwitchDivider;
+ }
+
+ public Switch getSwitch() {
+ return mSwitch;
+ }
+
+ public Button getAction1() {
+ return mAction1;
+ }
+
+ public View getAction1Divider() {
+ return mAction1Divider;
+ }
+
+ public Button getAction2() {
+ return mAction2;
+ }
+
+ public View getAction2Divider() {
+ return mAction2Divider;
+ }
+ }
+}
diff --git a/androidx/recyclerview/selection/ActivationCallbacks.java b/androidx/recyclerview/selection/ActivationCallbacks.java
deleted file mode 100644
index 606f35a7..00000000
--- a/androidx/recyclerview/selection/ActivationCallbacks.java
+++ /dev/null
@@ -1,55 +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.recyclerview.selection;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.support.annotation.RestrictTo;
-import android.view.MotionEvent;
-
-import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
-
-/**
- * Override methods in this class to connect specialized behaviors of the selection
- * code to the application environment.
- *
- * @param <K> Selection key type. Usually String or Long.
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public abstract class ActivationCallbacks<K> {
-
- static <K> ActivationCallbacks<K> dummy() {
- return new ActivationCallbacks<K>() {
- @Override
- public boolean onItemActivated(ItemDetails item, MotionEvent e) {
- return false;
- }
- };
- }
-
- /**
- * Called when an item is activated. An item is activitated, for example, when
- * there is no active selection and the user double clicks an item with a
- * pointing device like a Mouse.
- *
- * @param item details of the item.
- * @param e the event associated with item.
- * @return true if the event was handled.
- */
- public abstract boolean onItemActivated(ItemDetails<K> item, MotionEvent e);
-}
diff --git a/androidx/recyclerview/selection/BandPredicate.java b/androidx/recyclerview/selection/BandPredicate.java
deleted file mode 100644
index 9a5ae477..00000000
--- a/androidx/recyclerview/selection/BandPredicate.java
+++ /dev/null
@@ -1,131 +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.recyclerview.selection;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static android.support.v4.util.Preconditions.checkArgument;
-
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.v7.widget.GridLayoutManager;
-import android.support.v7.widget.LinearLayoutManager;
-import android.support.v7.widget.RecyclerView;
-import android.view.MotionEvent;
-import android.view.View;
-
-/**
- * Provides a means of controlling when and where band selection can be initiated.
- *
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public abstract class BandPredicate {
-
- /** @return true if band selection can be initiated in response to the {@link MotionEvent}. */
- public abstract boolean canInitiate(MotionEvent e);
-
- private static boolean hasSupportedLayoutManager(RecyclerView recView) {
- RecyclerView.LayoutManager lm = recView.getLayoutManager();
- return lm instanceof GridLayoutManager
- || lm instanceof LinearLayoutManager;
- }
-
- /**
- * Creates a new band predicate that permits initiation of band on areas
- * of a RecyclerView that map to RecyclerView.NO_POSITION.
- *
- * @param recView
- * @return
- */
- @SuppressWarnings("unused")
- public static BandPredicate noPosition(RecyclerView recView) {
- return new NoPosition(recView);
- }
-
- /**
- * Creates a new band predicate that permits initiation of band
- * anywhere doesn't correspond to a draggable region of a item.
- *
- * @param detailsLookup
- * @return
- */
- public static BandPredicate notDraggable(
- RecyclerView recView, ItemDetailsLookup detailsLookup) {
- return new NotDraggable(recView, detailsLookup);
- }
-
- /**
- * A BandPredicate that allows initiation of band selection only in areas of RecyclerView
- * that have {@link RecyclerView#NO_POSITION}. In most cases, this will be the empty areas
- * between views.
- */
- private static final class NoPosition extends BandPredicate {
-
- private final RecyclerView mRecView;
-
- NoPosition(RecyclerView recView) {
- checkArgument(recView != null);
-
- mRecView = recView;
- }
-
- @Override
- public boolean canInitiate(MotionEvent e) {
- if (!hasSupportedLayoutManager(mRecView)
- || mRecView.hasPendingAdapterUpdates()) {
- return false;
- }
-
- View itemView = mRecView.findChildViewUnder(e.getX(), e.getY());
- int position = itemView != null
- ? mRecView.getChildAdapterPosition(itemView)
- : RecyclerView.NO_POSITION;
-
- return position == RecyclerView.NO_POSITION;
- }
- }
-
- /**
- * A BandPredicate that allows initiation of band selection in any area that is not
- * draggable as determined by consulting
- * {@link ItemDetailsLookup#inItemDragRegion(MotionEvent)}.
- */
- private static final class NotDraggable extends BandPredicate {
-
- private final RecyclerView mRecView;
- private final ItemDetailsLookup mDetailsLookup;
-
- NotDraggable(RecyclerView recView, ItemDetailsLookup detailsLookup) {
- checkArgument(recView != null);
- checkArgument(detailsLookup != null);
-
- mRecView = recView;
- mDetailsLookup = detailsLookup;
- }
-
- @Override
- public boolean canInitiate(MotionEvent e) {
- if (!hasSupportedLayoutManager(mRecView)
- || mRecView.hasPendingAdapterUpdates()) {
- return false;
- }
-
- @Nullable ItemDetailsLookup.ItemDetails details = mDetailsLookup.getItemDetails(e);
- return (details == null) || !details.inDragRegion(e);
- }
- }
-}
diff --git a/androidx/recyclerview/selection/ContentLock.java b/androidx/recyclerview/selection/ContentLock.java
deleted file mode 100644
index 6891eab6..00000000
--- a/androidx/recyclerview/selection/ContentLock.java
+++ /dev/null
@@ -1,98 +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.recyclerview.selection;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static android.support.v4.util.Preconditions.checkState;
-
-import static androidx.recyclerview.selection.Shared.DEBUG;
-
-import android.content.Loader;
-import android.support.annotation.MainThread;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.util.Log;
-
-/**
- * ContentLock provides a mechanism to block content from reloading while selection
- * activities like gesture and band selection are active. Clients using live data
- * (data loaded, for example by a {@link Loader}), should route calls to load
- * content through this lock using {@link ContentLock#runWhenUnlocked(Runnable)}.
- *
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public final class ContentLock {
-
- private static final String TAG = "ContentLock";
-
- private int mLocks = 0;
- private @Nullable Runnable mCallback;
-
- /**
- * Increment the block count by 1
- */
- @MainThread
- synchronized void block() {
- mLocks++;
- if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mLocks + ".");
- }
-
- /**
- * Decrement the block count by 1; If no other object is trying to block and there exists some
- * callback, that callback will be run
- */
- @MainThread
- synchronized void unblock() {
- checkState(mLocks > 0);
-
- mLocks--;
- if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mLocks + ".");
-
- if (mLocks == 0 && mCallback != null) {
- mCallback.run();
- mCallback = null;
- }
- }
-
- /**
- * Attempts to run the given Runnable if not-locked, or else the Runnable is set to be ran next
- * (replacing any previous set Runnables).
- */
- @SuppressWarnings("unused")
- public synchronized void runWhenUnlocked(Runnable runnable) {
- if (mLocks == 0) {
- runnable.run();
- } else {
- mCallback = runnable;
- }
- }
-
- /**
- * Allows other selection code to perform a precondition check asserting the state is locked.
- */
- void checkLocked() {
- checkState(mLocks > 0);
- }
-
- /**
- * Allows other selection code to perform a precondition check asserting the state is unlocked.
- */
- void checkUnlocked() {
- checkState(mLocks == 0);
- }
-}
diff --git a/androidx/recyclerview/selection/SelectionHelper.java b/androidx/recyclerview/selection/SelectionHelper.java
deleted file mode 100644
index 276f9034..00000000
--- a/androidx/recyclerview/selection/SelectionHelper.java
+++ /dev/null
@@ -1,251 +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.recyclerview.selection;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-
-import java.util.Set;
-
-/**
- * SelectionManager provides support for managing selection within a RecyclerView instance.
- *
- * @see DefaultSelectionHelper for details on instantiation.
- *
- * @param <K> Selection key type. Usually String or Long.
- *
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public abstract class SelectionHelper<K> {
-
- /**
- * This value is included in the payload when SelectionHelper implementations
- * notify RecyclerView of changes. Clients can look for this in
- * {@code onBindViewHolder} to know if the bind event is occurring in response
- * to a selection state change.
- */
- public static final String SELECTION_CHANGED_MARKER = "Selection-Changed";
-
- /**
- * Adds {@code observer} to be notified when changes to selection occur.
- * This method allows observers to closely track changes to selection
- * avoiding the need to poll selection at performance critical points.
- */
- public abstract void addObserver(SelectionObserver observer);
-
- /** @return true if has a selection */
- public abstract boolean hasSelection();
-
- /**
- * Returns a Selection object that provides a live view on the current selection.
- *
- * @return The current selection.
- * @see #copySelection(Selection) on how to get a snapshot
- * of the selection that will not reflect future changes
- * to selection.
- */
- public abstract Selection getSelection();
-
- /**
- * Updates {@code dest} to reflect the current selection.
- */
- public abstract void copySelection(Selection dest);
-
- /**
- * @return true if the item specified by its id is selected. Shorthand for
- * {@code getSelection().contains(K)}.
- */
- public abstract boolean isSelected(@Nullable K key);
-
- /**
- * Restores the selected state of specified items. Used in cases such as restore the selection
- * after rotation etc. Provisional selection, being provisional 'n all, isn't restored.
- *
- * <p>This affords clients the ability to restore selection from selection saved
- * in Activity state. See {@link android.app.Activity#onCreate(Bundle)}.
- *
- * @param savedSelection selection being restored.
- */
- public abstract void restoreSelection(Selection savedSelection);
-
- abstract void onDataSetChanged();
-
- /**
- * Clears both primary selection and provisional selection.
- *
- * @return true if anything changed.
- */
- public abstract boolean clear();
-
- /**
- * Clears the selection and notifies (if something changes).
- */
- public abstract void clearSelection();
-
- /**
- * Sets the selected state of the specified items. Note that the callback will NOT
- * be consulted to see if an item can be selected.
- */
- public abstract boolean setItemsSelected(Iterable<K> keys, boolean selected);
-
- /**
- * Attempts to select an item.
- *
- * @return true if the item was selected. False if the item was not selected, or was
- * was already selected prior to the method being called.
- */
- public abstract boolean select(K key);
-
- /**
- * Attempts to deselect an item.
- *
- * @return true if the item was deselected. False if the item was not deselected, or was
- * was already deselected prior to the method being called.
- */
- public abstract boolean deselect(K key);
-
- /**
- * Selects the item at position and establishes the "anchor" for a range selection,
- * replacing any existing range anchor.
- *
- * @param position The anchor position for the selection range.
- */
- public abstract void startRange(int position);
-
- /**
- * Sets the end point for the active range selection.
- *
- * <p>This function should only be called when a range selection is active
- * (see {@link #isRangeActive()}. Items in the range [anchor, end] will be
- * selected.
- *
- * @param position The new end position for the selection range.
- * @throws IllegalStateException if a range selection is not active. Range selection
- * must have been started by a call to {@link #startRange(int)}.
- */
- public abstract void extendRange(int position);
-
- /**
- * Stops an in-progress range selection. All selection done with
- * {@link #extendProvisionalRange(int)} will be lost if
- * {@link Selection#mergeProvisionalSelection()} is not called beforehand.
- */
- public abstract void endRange();
-
- /**
- * @return Whether or not there is a current range selection active.
- */
- public abstract boolean isRangeActive();
-
- /**
- * Establishes the "anchor" at which a selection range begins. This "anchor" is consulted
- * when determining how to extend, and modify selection ranges. Calling this when a
- * range selection is active will reset the range selection.
- *
- * @param position the anchor position. Must already be selected.
- */
- protected abstract void anchorRange(int position);
-
- /**
- * @param position
- */
- // TODO: This is smelly. Maybe this type of logic needs to move into range selection,
- // then selection manager can have a startProvisionalRange and startRange. Or
- // maybe ranges always start life as provisional.
- protected abstract void extendProvisionalRange(int position);
-
- /**
- * Sets the provisional selection, replacing any existing selection.
- * @param newSelection
- */
- public abstract void setProvisionalSelection(Set<K> newSelection);
-
- /** Clears any existing provisional selection */
- public abstract void clearProvisionalSelection();
-
- /**
- * Converts the provisional selection into primary selection, then clears
- * provisional selection.
- */
- public abstract void mergeProvisionalSelection();
-
- /**
- * Observer interface providing access to information about Selection state changes.
- *
- * @param <K> Selection key type. Usually String or Long.
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public abstract static class SelectionObserver<K> {
-
- /**
- * Called when state of an item has been changed.
- */
- public void onItemStateChanged(K key, boolean selected) {
- }
-
- /**
- * Called when the underlying data set has change. After this method is called
- * the selection manager will attempt traverse the existing selection,
- * calling {@link #onItemStateChanged(K, boolean)} for each selected item,
- * and deselecting any items that cannot be selected given the updated dataset.
- */
- public void onSelectionReset() {
- }
-
- /**
- * Called immediately after completion of any set of changes, excluding
- * those resulting in calls to {@link #onSelectionReset()} and
- * {@link #onSelectionRestored()}.
- */
- public void onSelectionChanged() {
- }
-
- /**
- * Called immediately after selection is restored.
- * {@link #onItemStateChanged(K, boolean)} will not be called
- * for individual items in the selection.
- */
- public void onSelectionRestored() {
- }
- }
-
- /**
- * Implement SelectionPredicate to control when items can be selected or unselected.
- *
- * @param <K> Selection key type. Usually String or Long.
- *
- * @hide
- */
- @RestrictTo(LIBRARY_GROUP)
- public abstract static class SelectionPredicate<K> {
-
- /** @return true if the item at {@code id} can be set to {@code nextState}. */
- public abstract boolean canSetStateForKey(K key, boolean nextState);
-
- /** @return true if the item at {@code id} can be set to {@code nextState}. */
- public abstract boolean canSetStateAtPosition(int position, boolean nextState);
-
- /** @return true if more than a single item can be selected. */
- public abstract boolean canSelectMultiple();
- }
-}
diff --git a/androidx/recyclerview/selection/SelectionHelperBuilder.java b/androidx/recyclerview/selection/SelectionHelperBuilder.java
deleted file mode 100644
index 127a5116..00000000
--- a/androidx/recyclerview/selection/SelectionHelperBuilder.java
+++ /dev/null
@@ -1,341 +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.recyclerview.selection;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static android.support.v4.util.Preconditions.checkArgument;
-
-import android.content.Context;
-import android.support.annotation.DrawableRes;
-import android.support.annotation.RestrictTo;
-import android.support.v7.widget.RecyclerView;
-import android.view.GestureDetector;
-import android.view.HapticFeedbackConstants;
-import android.view.MotionEvent;
-
-import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
-
-/**
- * Builder class for assembling selection support. Example usage:
- *
- * <p><pre>SelectionHelperBuilder selSupport = new SelectionHelperBuilder(
- mRecView, new DemoStableIdProvider(mAdapter), detailsLookup);
-
- // By default multi-select is supported.
- SelectionHelper selHelper = selSupport
- .build();
-
- // This configuration support single selection for any element.
- SelectionHelper selHelper = selSupport
- .withSelectionPredicate(SelectionHelper.SelectionPredicate.SINGLE_ANYTHING)
- .build();
-
- // Lazily bind SelectionHelper. Allows us to defer initialization of the
- // SelectionHelper dependency until after the adapter is created.
- mAdapter.bindSelectionHelper(selHelper);
-
- * </pre></p>
- *
- * @see SelectionStorage for important deatils on retaining selection across Activity
- * lifecycle events.
- *
- * @param <K> Selection key type. Usually String or Long.
- *
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public final class SelectionHelperBuilder<K> {
-
- private final RecyclerView mRecView;
- private final RecyclerView.Adapter<?> mAdapter;
- private final Context mContext;
-
- // Content lock provides a mechanism to block content reload while selection
- // activities are active. If using a loader to load content, route
- // the call through the content lock using ContentLock#runWhenUnlocked.
- // This is especially useful when listening on content change notification.
- private final ContentLock mLock = new ContentLock();
-
- private SelectionPredicate<K> mSelectionPredicate = SelectionPredicates.selectAnything();
- private ItemKeyProvider<K> mKeyProvider;
- private ItemDetailsLookup<K> mDetailsLookup;
-
- private ActivationCallbacks<K> mActivationCallbacks = ActivationCallbacks.dummy();
- private FocusCallbacks<K> mFocusCallbacks = FocusCallbacks.dummy();
- private TouchCallbacks mTouchCallbacks = TouchCallbacks.DUMMY;
- private MouseCallbacks mMouseCallbacks = MouseCallbacks.DUMMY;
-
- private BandPredicate mBandPredicate;
- private int mBandOverlayId = R.drawable.selection_band_overlay;
-
- private int[] mGestureToolTypes = new int[] {
- MotionEvent.TOOL_TYPE_FINGER,
- MotionEvent.TOOL_TYPE_UNKNOWN
- };
-
- private int[] mBandToolTypes = new int[] {
- MotionEvent.TOOL_TYPE_MOUSE
- };
-
- public SelectionHelperBuilder(
- RecyclerView recView,
- ItemKeyProvider<K> keyProvider,
- ItemDetailsLookup<K> detailsLookup) {
-
- checkArgument(recView != null);
-
- mRecView = recView;
- mContext = recView.getContext();
- mAdapter = recView.getAdapter();
-
- checkArgument(mAdapter != null);
- checkArgument(keyProvider != null);
- checkArgument(detailsLookup != null);
-
- mDetailsLookup = detailsLookup;
- mKeyProvider = keyProvider;
-
- mBandPredicate = BandPredicate.notDraggable(mRecView, detailsLookup);
- }
-
- /**
- * Install seleciton predicate.
- * @param predicate
- * @return
- */
- public SelectionHelperBuilder<K> withSelectionPredicate(SelectionPredicate<K> predicate) {
- checkArgument(predicate != null);
- mSelectionPredicate = predicate;
- return this;
- }
-
- /**
- * Add activation callbacks to respond to taps/enter/double-click on items.
- *
- * @param callbacks
- * @return
- */
- public SelectionHelperBuilder<K> withActivationCallbacks(ActivationCallbacks<K> callbacks) {
- checkArgument(callbacks != null);
- mActivationCallbacks = callbacks;
- return this;
- }
-
- /**
- * Add focus callbacks to interfact with selection related focus changes.
- * @param callbacks
- * @return
- */
- public SelectionHelperBuilder<K> withFocusCallbacks(FocusCallbacks<K> callbacks) {
- checkArgument(callbacks != null);
- mFocusCallbacks = callbacks;
- return this;
- }
-
- /**
- * Configures mouse callbacks, replacing defaults.
- *
- * @param callbacks
- * @return
- */
- public SelectionHelperBuilder<K> withMouseCallbacks(MouseCallbacks callbacks) {
- checkArgument(callbacks != null);
-
- mMouseCallbacks = callbacks;
- return this;
- }
-
- /**
- * Replaces default touch callbacks.
- *
- * @param callbacks
- * @return
- */
- public SelectionHelperBuilder<K> withTouchCallbacks(TouchCallbacks callbacks) {
- checkArgument(callbacks != null);
-
- mTouchCallbacks = callbacks;
- return this;
- }
-
- /**
- * Replaces default gesture tooltypes.
- * @param toolTypes
- * @return
- */
- public SelectionHelperBuilder<K> withTouchTooltypes(int... toolTypes) {
- mGestureToolTypes = toolTypes;
- return this;
- }
-
- /**
- * Replaces default band overlay.
- *
- * @param bandOverlayId
- * @return
- */
- public SelectionHelperBuilder<K> withBandOverlay(@DrawableRes int bandOverlayId) {
- mBandOverlayId = bandOverlayId;
- return this;
- }
-
- /**
- * Replaces default band predicate.
- * @param bandPredicate
- * @return
- */
- public SelectionHelperBuilder<K> withBandPredicate(BandPredicate bandPredicate) {
-
- checkArgument(bandPredicate != null);
-
- mBandPredicate = bandPredicate;
- return this;
- }
-
- /**
- * Replaces default band tools types.
- * @param toolTypes
- * @return
- */
- public SelectionHelperBuilder<K> withBandTooltypes(int... toolTypes) {
- mBandToolTypes = toolTypes;
- return this;
- }
-
- /**
- * Prepares selection support and returns the corresponding SelectionHelper.
- *
- * @return
- */
- public SelectionHelper<K> build() {
-
- SelectionHelper<K> selectionHelper =
- new DefaultSelectionHelper<>(mKeyProvider, mSelectionPredicate);
-
- // Event glue between RecyclerView and SelectionHelper keeps the classes separate
- // so that a SelectionHelper can be shared across RecyclerView instances that
- // represent the same data in different ways.
- EventBridge.install(mAdapter, selectionHelper, mKeyProvider);
-
- AutoScroller scroller = new ViewAutoScroller(ViewAutoScroller.createScrollHost(mRecView));
-
- // Setup basic input handling, with the touch handler as the default consumer
- // of events. If mouse handling is configured as well, the mouse input
- // related handlers will intercept mouse input events.
-
- // GestureRouter is responsible for routing GestureDetector events
- // to tool-type specific handlers.
- GestureRouter<MotionInputHandler> gestureRouter = new GestureRouter<>();
- GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter);
-
- // TouchEventRouter takes its name from RecyclerView#OnItemTouchListener.
- // Despite "Touch" being in the name, it receives events for all types of tools.
- // This class is responsible for routing events to tool-type specific handlers,
- // and if not handled by a handler, on to a GestureDetector for analysis.
- TouchEventRouter eventRouter = new TouchEventRouter(gestureDetector);
-
- // GestureSelectionHelper provides logic that interprets a combination
- // of motions and gestures in order to provide gesture driven selection support
- // when used in conjunction with RecyclerView.
- final GestureSelectionHelper gestureHelper =
- GestureSelectionHelper.create(selectionHelper, mRecView, scroller, mLock);
-
- // Finally hook the framework up to listening to recycle view events.
- mRecView.addOnItemTouchListener(eventRouter);
-
- // But before you move on, there's more work to do. Event plumbing has been
- // installed, but we haven't registered any of our helpers or callbacks.
- // Helpers contain predefined logic converting events into selection related events.
- // Callbacks provide authors the ability to reponspond to other types of
- // events (like "active" a tapped item). This is broken up into two main
- // suites, one for "touch" and one for "mouse", though both can and should (usually)
- // be configued to handle other types of input (to satisfy user expectation).);
-
- // Provides high level glue for binding touch events
- // and gestures to selection framework.
- TouchInputHandler<K> touchHandler = new TouchInputHandler<K>(
- selectionHelper,
- mKeyProvider,
- mDetailsLookup,
- mSelectionPredicate,
- new Runnable() {
- @Override
- public void run() {
- if (mSelectionPredicate.canSelectMultiple()) {
- gestureHelper.start();
- }
- }
- },
- mTouchCallbacks,
- mActivationCallbacks,
- mFocusCallbacks,
- new Runnable() {
- @Override
- public void run() {
- mRecView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
- }
- });
-
- for (int toolType : mGestureToolTypes) {
- gestureRouter.register(toolType, touchHandler);
- eventRouter.register(toolType, gestureHelper);
- }
-
- // Provides high level glue for binding mouse events and gestures
- // to selection framework.
- MouseInputHandler<K> mouseHandler = new MouseInputHandler<>(
- selectionHelper,
- mKeyProvider,
- mDetailsLookup,
- mMouseCallbacks,
- mActivationCallbacks,
- mFocusCallbacks);
-
- for (int toolType : mBandToolTypes) {
- gestureRouter.register(toolType, mouseHandler);
- }
-
- // Band selection not supported in single select mode, or when key access
- // is limited to anything less than the entire corpus.
- // TODO: Since we cach grid info from laid out items, we could cache key too.
- // Then we couldn't have to limit to CORPUS access.
- if (mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED)
- && mSelectionPredicate.canSelectMultiple()) {
- // BandSelectionHelper provides support for band selection on-top of a RecyclerView
- // instance. Given the recycling nature of RecyclerView BandSelectionController
- // necessarily models and caches list/grid information as the user's pointer
- // interacts with the item in the RecyclerView. Selectable items that intersect
- // with the band, both on and off screen, are selected.
- BandSelectionHelper bandHelper = BandSelectionHelper.create(
- mRecView,
- scroller,
- mBandOverlayId,
- mKeyProvider,
- selectionHelper,
- mSelectionPredicate,
- mBandPredicate,
- mFocusCallbacks,
- mLock);
-
- for (int toolType : mBandToolTypes) {
- eventRouter.register(toolType, bandHelper);
- }
- }
-
- return selectionHelper;
- }
-}
diff --git a/androidx/recyclerview/selection/SelectionStorage.java b/androidx/recyclerview/selection/SelectionStorage.java
deleted file mode 100644
index 454a76b0..00000000
--- a/androidx/recyclerview/selection/SelectionStorage.java
+++ /dev/null
@@ -1,181 +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.recyclerview.selection;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-import static android.support.v4.util.Preconditions.checkArgument;
-
-import android.os.Bundle;
-import android.support.annotation.IntDef;
-import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.Set;
-
-/**
- * Helper class binding SelectionHelper and Activity lifecycle events facilitating
- * persistence of selection across activity lifecycle events.
- *
- * <p>Usage:<br><pre>
- void onCreate() {
- mLifecycleHelper = new SelectionStorage<>(SelectionStorage.TYPE_STRING, mSelectionHelper);
- if (savedInstanceState != null) {
- mSelectionStorage.onRestoreInstanceState(savedInstanceState);
- }
- }
- protected void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- mSelectionStorage.onSaveInstanceState(outState);
- }
- </pre>
- * @param <K> Selection key type. Usually String or Long.
- *
- * @hide
- */
-@RestrictTo(LIBRARY_GROUP)
-public final class SelectionStorage<K> {
-
- @VisibleForTesting
- static final String EXTRA_SAVED_SELECTION_TYPE = "androidx.recyclerview.selection.type";
-
- @VisibleForTesting
- static final String EXTRA_SAVED_SELECTION_ENTRIES = "androidx.recyclerview.selection.entries";
-
- public static final int TYPE_STRING = 0;
- public static final int TYPE_LONG = 1;
- @IntDef({
- TYPE_STRING,
- TYPE_LONG
- })
- @Retention(RetentionPolicy.SOURCE)
- @interface KeyType {}
-
- private final @KeyType int mKeyType;
- private final SelectionHelper<K> mHelper;
-
- /**
- * Creates a new lifecycle helper. {@code keyType}.
- *
- * @param keyType
- * @param helper
- */
- public SelectionStorage(@KeyType int keyType, SelectionHelper<K> helper) {
- checkArgument(
- keyType == TYPE_STRING || keyType == TYPE_LONG,
- "Only String and Integer presistence are supported by default.");
- checkArgument(helper != null);
-
- mKeyType = keyType;
- mHelper = helper;
- }
-
- /**
- * Preserves selection, if any.
- *
- * @param state
- */
- @SuppressWarnings("unchecked")
- public void onSaveInstanceState(Bundle state) {
- MutableSelection<K> sel = new MutableSelection<>();
- mHelper.copySelection(sel);
-
- state.putInt(EXTRA_SAVED_SELECTION_TYPE, mKeyType);
- switch (mKeyType) {
- case TYPE_STRING:
- writeStringSelection(state, ((Selection<String>) sel).mSelection);
- break;
- case TYPE_LONG:
- writeLongSelection(state, ((Selection<Long>) sel).mSelection);
- break;
- default:
- throw new UnsupportedOperationException("Unsupported key type: " + mKeyType);
- }
- }
-
- /**
- * Restores selection from previously saved state.
- *
- * @param state
- */
- public void onRestoreInstanceState(@Nullable Bundle state) {
- if (state == null) {
- return;
- }
-
- int keyType = state.getInt(EXTRA_SAVED_SELECTION_TYPE, -1);
- switch(keyType) {
- case TYPE_STRING:
- Selection<String> stringSel = readStringSelection(state);
- if (stringSel != null && !stringSel.isEmpty()) {
- mHelper.restoreSelection(stringSel);
- }
- break;
- case TYPE_LONG:
- Selection<Long> longSel = readLongSelection(state);
- if (longSel != null && !longSel.isEmpty()) {
- mHelper.restoreSelection(longSel);
- }
- break;
- default:
- throw new UnsupportedOperationException("Unsupported selection key type.");
- }
- }
-
- private @Nullable Selection<String> readStringSelection(Bundle state) {
- @Nullable ArrayList<String> stored =
- state.getStringArrayList(EXTRA_SAVED_SELECTION_ENTRIES);
- if (stored == null) {
- return null;
- }
-
- Selection<String> selection = new Selection<>();
- selection.mSelection.addAll(stored);
- return selection;
- }
-
- private @Nullable Selection<Long> readLongSelection(Bundle state) {
- @Nullable long[] stored = state.getLongArray(EXTRA_SAVED_SELECTION_ENTRIES);
- if (stored == null) {
- return null;
- }
-
- Selection<Long> selection = new Selection<>();
- for (long key : stored) {
- selection.mSelection.add(key);
- }
- return selection;
- }
-
- private void writeStringSelection(Bundle state, Set<String> selected) {
- ArrayList<String> value = new ArrayList<>(selected.size());
- value.addAll(selected);
- state.putStringArrayList(EXTRA_SAVED_SELECTION_ENTRIES, value);
- }
-
- private void writeLongSelection(Bundle state, Set<Long> selected) {
- long[] value = new long[selected.size()];
- int i = 0;
- for (Long key : selected) {
- value[i++] = key;
- }
- state.putLongArray(EXTRA_SAVED_SELECTION_ENTRIES, value);
- }
-}
diff --git a/androidx/textclassifier/EntityConfidence.java b/androidx/textclassifier/EntityConfidence.java
new file mode 100644
index 00000000..08852f5b
--- /dev/null
+++ b/androidx/textclassifier/EntityConfidence.java
@@ -0,0 +1,152 @@
+/*
+ * 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 androidx.textclassifier;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.FloatRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.RestrictTo;
+import android.support.v4.util.ArrayMap;
+import android.support.v4.util.Preconditions;
+import android.support.v4.util.SimpleArrayMap;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper object for setting and getting entity scores for classified text.
+ *
+ * @hide
+ */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+final class EntityConfidence implements Parcelable {
+
+ private final ArrayMap<String, Float> mEntityConfidence = new ArrayMap<>();
+ private final ArrayList<String> mSortedEntities = new ArrayList<>();
+
+ EntityConfidence() {}
+
+ EntityConfidence(@NonNull EntityConfidence source) {
+ Preconditions.checkNotNull(source);
+ mEntityConfidence.putAll((SimpleArrayMap<String, Float>) source.mEntityConfidence);
+ mSortedEntities.addAll(source.mSortedEntities);
+ }
+
+ /**
+ * Constructs an EntityConfidence from a map of entity to confidence.
+ *
+ * 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).
+ */
+ EntityConfidence(@NonNull Map<String, Float> source) {
+ Preconditions.checkNotNull(source);
+
+ // Prune non-existent entities and clamp to 1.
+ mEntityConfidence.ensureCapacity(source.size());
+ for (Map.Entry<String, Float> it : source.entrySet()) {
+ if (it.getValue() <= 0) continue;
+ mEntityConfidence.put(it.getKey(), Math.min(1, it.getValue()));
+ }
+ resetSortedEntitiesFromMap();
+ }
+
+ /**
+ * Returns an immutable list of entities found in the classified text ordered from
+ * high confidence to low confidence.
+ */
+ @NonNull
+ public List<String> getEntities() {
+ return Collections.unmodifiableList(mSortedEntities);
+ }
+
+ /**
+ * Returns the confidence score for the specified entity. The value ranges from
+ * 0 (low confidence) to 1 (high confidence). 0 indicates that the entity was not found for the
+ * classified text.
+ */
+ @FloatRange(from = 0.0, to = 1.0)
+ public float getConfidenceScore(String entity) {
+ if (mEntityConfidence.containsKey(entity)) {
+ return mEntityConfidence.get(entity);
+ }
+ return 0;
+ }
+
+ @Override
+ public String toString() {
+ return mEntityConfidence.toString();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mEntityConfidence.size());
+ for (Map.Entry<String, Float> entry : mEntityConfidence.entrySet()) {
+ dest.writeString(entry.getKey());
+ dest.writeFloat(entry.getValue());
+ }
+ }
+
+ public static final Parcelable.Creator<EntityConfidence> CREATOR =
+ new Parcelable.Creator<EntityConfidence>() {
+ @Override
+ public EntityConfidence createFromParcel(Parcel in) {
+ return new EntityConfidence(in);
+ }
+
+ @Override
+ public EntityConfidence[] newArray(int size) {
+ return new EntityConfidence[size];
+ }
+ };
+
+ private EntityConfidence(Parcel in) {
+ final int numEntities = in.readInt();
+ mEntityConfidence.ensureCapacity(numEntities);
+ for (int i = 0; i < numEntities; ++i) {
+ mEntityConfidence.put(in.readString(), in.readFloat());
+ }
+ resetSortedEntitiesFromMap();
+ }
+
+ private void resetSortedEntitiesFromMap() {
+ mSortedEntities.clear();
+ mSortedEntities.ensureCapacity(mEntityConfidence.size());
+ mSortedEntities.addAll(mEntityConfidence.keySet());
+ Collections.sort(mSortedEntities, new EntityConfidenceComparator());
+ }
+
+ /** Helper to sort entities according to their confidence. */
+ private class EntityConfidenceComparator implements Comparator<String> {
+ @Override
+ public int compare(String e1, String e2) {
+ float score1 = mEntityConfidence.get(e1);
+ float score2 = mEntityConfidence.get(e2);
+ return Float.compare(score2, score1);
+ }
+ }
+}
diff --git a/androidx/textclassifier/TextClassification.java b/androidx/textclassifier/TextClassification.java
new file mode 100644
index 00000000..cc175e2a
--- /dev/null
+++ b/androidx/textclassifier/TextClassification.java
@@ -0,0 +1,598 @@
+/*
+ * 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 androidx.textclassifier;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.FloatRange;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v4.util.ArrayMap;
+import android.support.v4.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import androidx.textclassifier.TextClassifier.EntityType;
+
+/**
+ * Information for generating a widget to handle classified text.
+ *
+ * <p>A TextClassification object contains icons, labels, and intents that may 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:
+ *
+ * <pre>{@code
+ * // Called preferably outside the UiThread.
+ * TextClassification classification = textClassifier.classifyText(allText, 10, 25);
+ *
+ * // Called on the UiThread.
+ * Button button = new Button(context);
+ * button.setCompoundDrawablesWithIntrinsicBounds(classification.getIcon(), null, null, null);
+ * button.setText(classification.getLabel());
+ * button.setOnClickListener(v -> context.startActivity(classification.getIntent()));
+ * }</pre>
+ *
+ * TODO: describe how to start action mode for classified text.
+ */
+public final class TextClassification implements Parcelable {
+
+ /**
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ static final TextClassification EMPTY = new TextClassification.Builder().build();
+
+ // TODO: investigate a way to derive this based on device properties.
+ private static final int MAX_PRIMARY_ICON_SIZE = 192;
+ private static final int MAX_SECONDARY_ICON_SIZE = 144;
+
+ @Nullable private final String mText;
+ @Nullable private final Drawable mPrimaryIcon;
+ @Nullable private final String mPrimaryLabel;
+ @Nullable private final Intent mPrimaryIntent;
+ @NonNull private final List<Drawable> mSecondaryIcons;
+ @NonNull private final List<String> mSecondaryLabels;
+ @NonNull private final List<Intent> mSecondaryIntents;
+ @NonNull private final EntityConfidence mEntityConfidence;
+ @NonNull private final String mSignature;
+
+ private TextClassification(
+ @Nullable String text,
+ @Nullable Drawable primaryIcon,
+ @Nullable String primaryLabel,
+ @Nullable Intent primaryIntent,
+ @NonNull List<Drawable> secondaryIcons,
+ @NonNull List<String> secondaryLabels,
+ @NonNull List<Intent> secondaryIntents,
+ @NonNull Map<String, Float> entityConfidence,
+ @NonNull String signature) {
+ Preconditions.checkArgument(secondaryLabels.size() == secondaryIntents.size());
+ Preconditions.checkArgument(secondaryIcons.size() == secondaryIntents.size());
+ mText = text;
+ mPrimaryIcon = primaryIcon;
+ mPrimaryLabel = primaryLabel;
+ mPrimaryIntent = primaryIntent;
+ mSecondaryIcons = secondaryIcons;
+ mSecondaryLabels = secondaryLabels;
+ mSecondaryIntents = secondaryIntents;
+ mEntityConfidence = new EntityConfidence(entityConfidence);
+ mSignature = signature;
+ }
+
+ /**
+ * Gets the classified text.
+ */
+ @Nullable
+ public String getText() {
+ return mText;
+ }
+
+ /**
+ * Returns the number of entities found in the classified text.
+ */
+ @IntRange(from = 0)
+ public int getEntityCount() {
+ return mEntityConfidence.getEntities().size();
+ }
+
+ /**
+ * Returns the entity at the specified index. Entities are ordered from high confidence
+ * to low confidence.
+ *
+ * @throws IndexOutOfBoundsException if the specified index is out of range.
+ * @see #getEntityCount() for the number of entities available.
+ */
+ @NonNull
+ public @EntityType String getEntity(int index) {
+ return mEntityConfidence.getEntities().get(index);
+ }
+
+ /**
+ * Returns the confidence score for the specified entity. The value ranges from
+ * 0 (low confidence) to 1 (high confidence). 0 indicates that the entity was not found for the
+ * classified text.
+ */
+ @FloatRange(from = 0.0, to = 1.0)
+ public float getConfidenceScore(@EntityType String entity) {
+ return mEntityConfidence.getConfidenceScore(entity);
+ }
+
+ /**
+ * 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)
+ */
+ @IntRange(from = 0)
+ public int getSecondaryActionsCount() {
+ return mSecondaryIntents.size();
+ }
+
+ /**
+ * 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 #getSecondaryActionsCount() for the number of actions available.
+ * @see #getSecondaryIntent(int)
+ * @see #getSecondaryLabel(int)
+ * @see #getIcon()
+ */
+ @Nullable
+ public Drawable getSecondaryIcon(int index) {
+ return mSecondaryIcons.get(index);
+ }
+
+ /**
+ * 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 mPrimaryIcon;
+ }
+
+ /**
+ * 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 #getSecondaryActionsCount()
+ * @see #getSecondaryIntent(int)
+ * @see #getSecondaryIcon(int)
+ * @see #getLabel()
+ */
+ @Nullable
+ public CharSequence getSecondaryLabel(int index) {
+ return mSecondaryLabels.get(index);
+ }
+
+ /**
+ * 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 mPrimaryLabel;
+ }
+
+ /**
+ * 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 #getSecondaryActionsCount()
+ * @see #getSecondaryLabel(int)
+ * @see #getSecondaryIcon(int)
+ * @see #getIntent()
+ */
+ @Nullable
+ public Intent getSecondaryIntent(int index) {
+ return mSecondaryIntents.get(index);
+ }
+
+ /**
+ * Returns the <i>primary</i> intent that may be fired to act on the classified text.
+ *
+ * @see #getSecondaryIntent(int)
+ */
+ @Nullable
+ public Intent getIntent() {
+ return mPrimaryIntent;
+ }
+
+ /**
+ * 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 getSignature() {
+ return mSignature;
+ }
+
+ @Override
+ public String toString() {
+ 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);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mText);
+ final Bitmap primaryIconBitmap = drawableToBitmap(mPrimaryIcon, MAX_PRIMARY_ICON_SIZE);
+ dest.writeInt(primaryIconBitmap != null ? 1 : 0);
+ if (primaryIconBitmap != null) {
+ primaryIconBitmap.writeToParcel(dest, flags);
+ }
+ dest.writeString(mPrimaryLabel);
+ dest.writeInt(mPrimaryIntent != null ? 1 : 0);
+ if (mPrimaryIntent != null) {
+ mPrimaryIntent.writeToParcel(dest, flags);
+ }
+ dest.writeTypedList(drawablesToBitmaps(mSecondaryIcons, MAX_SECONDARY_ICON_SIZE));
+ dest.writeStringList(mSecondaryLabels);
+ dest.writeTypedList(mSecondaryIntents);
+ mEntityConfidence.writeToParcel(dest, flags);
+ dest.writeString(mSignature);
+ }
+
+ public static final Parcelable.Creator<TextClassification> CREATOR =
+ new Parcelable.Creator<TextClassification>() {
+ @Override
+ public TextClassification createFromParcel(Parcel in) {
+ return new TextClassification(in);
+ }
+
+ @Override
+ public TextClassification[] newArray(int size) {
+ return new TextClassification[size];
+ }
+ };
+
+ private TextClassification(Parcel in) {
+ mText = in.readString();
+ mPrimaryIcon = in.readInt() == 0
+ ? null : new BitmapDrawable(null, Bitmap.CREATOR.createFromParcel(in));
+ mPrimaryLabel = in.readString();
+ mPrimaryIntent = in.readInt() == 0 ? null : Intent.CREATOR.createFromParcel(in);
+ mSecondaryIcons = bitmapsToDrawables(in.createTypedArrayList(Bitmap.CREATOR));
+ mSecondaryLabels = in.createStringArrayList();
+ mSecondaryIntents = in.createTypedArrayList(Intent.CREATOR);
+ mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+ mSignature = in.readString();
+ }
+
+ /**
+ * Returns a Bitmap representation of the Drawable
+ *
+ * @param drawable The drawable to convert.
+ * @param maxDims The maximum edge length of the resulting bitmap (in pixels).
+ */
+ @Nullable
+ private static Bitmap drawableToBitmap(@Nullable Drawable drawable, int maxDims) {
+ if (drawable == null) {
+ return null;
+ }
+ final int actualWidth = Math.max(1, drawable.getIntrinsicWidth());
+ final int actualHeight = Math.max(1, drawable.getIntrinsicHeight());
+ final double scaleWidth = ((double) maxDims) / actualWidth;
+ final double scaleHeight = ((double) maxDims) / actualHeight;
+ final double scale = Math.min(1.0, Math.min(scaleWidth, scaleHeight));
+ final int width = (int) (actualWidth * scale);
+ final int height = (int) (actualHeight * scale);
+ if (drawable instanceof BitmapDrawable) {
+ final BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable;
+ if (actualWidth != width || actualHeight != height) {
+ return Bitmap.createScaledBitmap(
+ bitmapDrawable.getBitmap(), width, height, /*filter=*/false);
+ } else {
+ return bitmapDrawable.getBitmap();
+ }
+ } else {
+ final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(bitmap);
+ drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
+ drawable.draw(canvas);
+ return bitmap;
+ }
+ }
+
+ /**
+ * Returns a list of drawables converted to Bitmaps
+ *
+ * @param drawables The drawables to convert.
+ * @param maxDims The maximum edge length of the resulting bitmaps (in pixels).
+ */
+ private static List<Bitmap> drawablesToBitmaps(List<Drawable> drawables, int maxDims) {
+ final List<Bitmap> bitmaps = new ArrayList<>(drawables.size());
+ for (Drawable drawable : drawables) {
+ bitmaps.add(drawableToBitmap(drawable, maxDims));
+ }
+ return bitmaps;
+ }
+
+ /** Returns a list of drawable wrappers for a list of bitmaps. */
+ private static List<Drawable> bitmapsToDrawables(List<Bitmap> bitmaps) {
+ final List<Drawable> drawables = new ArrayList<>(bitmaps.size());
+ for (Bitmap bitmap : bitmaps) {
+ if (bitmap != null) {
+ drawables.add(new BitmapDrawable(null, bitmap));
+ } else {
+ drawables.add(null);
+ }
+ }
+ return drawables;
+ }
+
+ /**
+ * 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)
+ * .addSecondaryAction(intent1, label1, icon1)
+ * .addSecondaryAction(intent2, label2, icon2)
+ * .build();
+ * }</pre>
+ */
+ public static final class Builder {
+
+ @NonNull private String mText;
+ @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 Map<String, Float> mEntityConfidence = new ArrayMap<>();
+ @Nullable Drawable mPrimaryIcon;
+ @Nullable String mPrimaryLabel;
+ @Nullable Intent mPrimaryIntent;
+ @NonNull private String mSignature = "";
+
+ /**
+ * Sets the classified text.
+ */
+ public Builder setText(@Nullable String text) {
+ mText = text;
+ return this;
+ }
+
+ /**
+ * 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.
+ * Values greater than 1 are clamped to 1.
+ */
+ public Builder setEntityType(
+ @NonNull @EntityType String type,
+ @FloatRange(from = 0.0, to = 1.0) float confidenceScore) {
+ mEntityConfidence.put(type, confidenceScore);
+ return this;
+ }
+
+ /**
+ * 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)
+ */
+ public Builder addSecondaryAction(
+ @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon) {
+ if (intent != null || label != null || icon != null) {
+ mSecondaryIntents.add(intent);
+ mSecondaryLabels.add(label);
+ mSecondaryIcons.add(icon);
+ }
+ return this;
+ }
+
+ /**
+ * Removes all the <i>secondary</i> actions.
+ */
+ public Builder clearSecondaryActions() {
+ mSecondaryIntents.clear();
+ mSecondaryLabels.clear();
+ mSecondaryIcons.clear();
+ return this;
+ }
+
+ /**
+ * 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)}.
+ *
+ * <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)
+ */
+ public Builder setPrimaryAction(
+ @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon) {
+ return setIntent(intent).setLabel(label).setIcon(icon);
+ }
+
+ /**
+ * 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)
+ */
+ public Builder setIcon(@Nullable Drawable icon) {
+ mPrimaryIcon = icon;
+ return this;
+ }
+
+ /**
+ * 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)
+ */
+ public Builder setLabel(@Nullable String label) {
+ mPrimaryLabel = label;
+ return this;
+ }
+
+ /**
+ * Sets the intent for the <i>primary</i> action that may be fired to act on the classified
+ * text.
+ *
+ * @see #setPrimaryAction(Intent, String, Drawable)
+ */
+ public Builder setIntent(@Nullable Intent intent) {
+ mPrimaryIntent = intent;
+ return this;
+ }
+
+ /**
+ * 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.
+ */
+ public Builder setSignature(@NonNull String signature) {
+ mSignature = Preconditions.checkNotNull(signature);
+ return this;
+ }
+
+ /**
+ * Builds and returns a {@link TextClassification} object.
+ */
+ public TextClassification build() {
+ return new TextClassification(
+ mText,
+ mPrimaryIcon, mPrimaryLabel, mPrimaryIntent,
+ mSecondaryIcons, mSecondaryLabels, mSecondaryIntents,
+ mEntityConfidence, mSignature);
+ }
+ }
+
+ /**
+ * Optional input parameters for generating TextClassification.
+ */
+ public static final class Options implements Parcelable {
+
+ private @Nullable ArrayList<Locale> mDefaultLocales;
+
+ public Options() {}
+
+ /**
+ * @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 Collection<Locale> defaultLocales) {
+ mDefaultLocales = defaultLocales == null ? null : new ArrayList<>(defaultLocales);
+ return this;
+ }
+
+ /**
+ * @return ordered list of locale preferences that can be used to disambiguate
+ * the provided text.
+ */
+ @Nullable
+ public List<Locale> getDefaultLocales() {
+ return mDefaultLocales;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mDefaultLocales != null ? mDefaultLocales.size() : 0);
+ if (mDefaultLocales != null) {
+ for (Locale locale : mDefaultLocales) {
+ dest.writeSerializable(locale);
+ }
+ }
+ }
+
+ public static final Parcelable.Creator<Options> CREATOR =
+ new Parcelable.Creator<Options>() {
+ @Override
+ public Options createFromParcel(Parcel in) {
+ return new Options(in);
+ }
+
+ @Override
+ public Options[] newArray(int size) {
+ return new Options[size];
+ }
+ };
+
+ private Options(Parcel in) {
+ final int numLocales = in.readInt();
+ if (numLocales > 0) {
+ mDefaultLocales = new ArrayList<>();
+ mDefaultLocales.ensureCapacity(numLocales);
+ for (int i = 0; i < numLocales; ++i) {
+ mDefaultLocales.add((Locale) in.readSerializable());
+ }
+ }
+ }
+ }
+}
diff --git a/androidx/textclassifier/TextClassifier.java b/androidx/textclassifier/TextClassifier.java
new file mode 100644
index 00000000..c0fa8b9c
--- /dev/null
+++ b/androidx/textclassifier/TextClassifier.java
@@ -0,0 +1,184 @@
+/*
+ * 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 androidx.textclassifier;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.IntDef;
+import android.support.annotation.RestrictTo;
+import android.support.annotation.StringDef;
+import android.support.v4.util.ArraySet;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Interface for providing text classification related features.
+ *
+ * TextClassifier acts as a proxy to either the system provided TextClassifier, or an equivalent
+ * implementation provided by an app. Each instance of the class therefore represents one connection
+ * to the classifier implementation.
+ *
+ * <p>Unless otherwise stated, methods of this interface are blocking operations.
+ * Avoid calling them on the UI thread.
+ */
+public class TextClassifier {
+
+ // TODO: describe in the class documentation how a TC implementation in chosen/located.
+
+ /** Signifies that the TextClassifier did not identify an entity. */
+ public static final String TYPE_UNKNOWN = "";
+ /** Signifies that the classifier ran, but didn't recognize a know entity. */
+ public static final String TYPE_OTHER = "other";
+ /** Identifies an e-mail address. */
+ public static final String TYPE_EMAIL = "email";
+ /** Identifies a phone number. */
+ public static final String TYPE_PHONE = "phone";
+ /** Identifies a physical address. */
+ public static final String TYPE_ADDRESS = "address";
+ /** Identifies a URL. */
+ public static final String TYPE_URL = "url";
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef(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. **/
+ static final int ENTITY_PRESET_ALL = 0;
+ /** Designates that the TextClassifier should identify no entities. **/
+ static final int ENTITY_PRESET_NONE = 1;
+ /** Designates that the TextClassifier should identify a base set of entities determined by the
+ * TextClassifier. **/
+ static final int ENTITY_PRESET_BASE = 2;
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {ENTITY_PRESET_ALL, ENTITY_PRESET_NONE, ENTITY_PRESET_BASE})
+ @interface EntityPreset {}
+
+ // TODO: add constructor, suggestSelection, classifyText, generateLinks, logEvent
+
+ /**
+ * Returns a {@link Collection} of the entity types in the specified preset.
+ *
+ * @see #ENTITY_PRESET_ALL
+ * @see #ENTITY_PRESET_NONE
+ */
+ /* package */ Collection<String> getEntitiesForPreset(@EntityPreset int entityPreset) {
+ // TODO: forward call to the classifier implementation.
+ return Collections.EMPTY_LIST;
+ }
+
+ /**
+ * Configuration object for specifying what entities to identify.
+ *
+ * Configs are initially based on a predefined preset, and can be modified from there.
+ */
+ static final class EntityConfig implements Parcelable {
+ private final @EntityPreset int mEntityPreset;
+ private final Collection<String> mExcludedEntityTypes;
+ private final Collection<String> mIncludedEntityTypes;
+
+ EntityConfig(@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) {
+ mIncludedEntityTypes.addAll(Arrays.asList(entities));
+ return this;
+ }
+
+ /**
+ * Specifies an entity to be excluded.
+ */
+ public EntityConfig excludeEntities(String... entities) {
+ mExcludedEntityTypes.addAll(Arrays.asList(entities));
+ 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);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mEntityPreset);
+ dest.writeStringList(new ArrayList<>(mExcludedEntityTypes));
+ dest.writeStringList(new ArrayList<>(mIncludedEntityTypes));
+ }
+
+ public static final Parcelable.Creator<EntityConfig> CREATOR =
+ new Parcelable.Creator<EntityConfig>() {
+ @Override
+ public EntityConfig createFromParcel(Parcel in) {
+ return new EntityConfig(in);
+ }
+
+ @Override
+ public EntityConfig[] newArray(int size) {
+ return new EntityConfig[size];
+ }
+ };
+
+ private EntityConfig(Parcel in) {
+ mEntityPreset = in.readInt();
+ mExcludedEntityTypes = new ArraySet<>(in.createStringArrayList());
+ mIncludedEntityTypes = new ArraySet<>(in.createStringArrayList());
+ }
+ }
+}
diff --git a/androidx/textclassifier/TextLinks.java b/androidx/textclassifier/TextLinks.java
new file mode 100644
index 00000000..9afcfef8
--- /dev/null
+++ b/androidx/textclassifier/TextLinks.java
@@ -0,0 +1,527 @@
+/*
+ * Copyright 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 androidx.textclassifier;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.FloatRange;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.RestrictTo;
+import android.support.v4.util.Preconditions;
+import android.text.Spannable;
+import android.text.style.ClickableSpan;
+import android.view.View;
+
+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;
+import java.util.Locale;
+import java.util.Map;
+
+import androidx.textclassifier.TextClassifier.EntityType;
+
+/**
+ * A collection of links, representing subsequences of text and the entity types (phone number,
+ * address, url, etc) they may be.
+ */
+public final class TextLinks implements Parcelable {
+ private final String mFullText;
+ private final List<TextLink> mLinks;
+
+ /** Links were successfully applied to the text. */
+ public static final int STATUS_LINKS_APPLIED = 0;
+ /** No links exist to apply to text. Links count is zero. */
+ public static final int STATUS_NO_LINKS_FOUND = 1;
+ /** No links applied to text. The links were filtered out. */
+ public static final int STATUS_NO_LINKS_APPLIED = 2;
+ /** The specified text does not match the text used to generate the links. */
+ public static final int STATUS_DIFFERENT_TEXT = 3;
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(value = {
+ STATUS_LINKS_APPLIED,
+ STATUS_NO_LINKS_FOUND,
+ STATUS_NO_LINKS_APPLIED,
+ STATUS_DIFFERENT_TEXT
+ })
+ public @interface Status {}
+
+ /** Do not replace {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to
+ * be applied to. Do not apply the TextLinkSpan. **/
+ public static final int APPLY_STRATEGY_IGNORE = 0;
+ /** Replace any {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to be
+ * applied to. **/
+ public static final int APPLY_STRATEGY_REPLACE = 1;
+
+ /** @hide */
+ @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({APPLY_STRATEGY_IGNORE, APPLY_STRATEGY_REPLACE})
+ public @interface ApplyStrategy {}
+
+ private TextLinks(String fullText, ArrayList<TextLink> links) {
+ mFullText = fullText;
+ mLinks = Collections.unmodifiableList(links);
+ }
+
+ /**
+ * Returns an unmodifiable Collection of the links.
+ */
+ public Collection<TextLink> getLinks() {
+ return mLinks;
+ }
+
+ /**
+ * Annotates the given text with the generated links. It will fail if the provided text doesn't
+ * match the original text used to crete the TextLinks.
+ *
+ * @param text the text to apply the links to. Must match the original text.
+ * @param spanFactory a factory to generate spans from TextLinks. Will use a default if null.
+ *
+ * @return one of {@link #STATUS_LINKS_APPLIED}, {@link #STATUS_NO_LINKS_FOUND},
+ * {@link #STATUS_NO_LINKS_APPLIED}, {@link #STATUS_DIFFERENT_TEXT}
+ */
+ @Status
+ public int apply(
+ @NonNull Spannable text,
+ @ApplyStrategy int applyStrategy,
+ @Nullable SpanFactory spanFactory) {
+ Preconditions.checkNotNull(text);
+ checkValidApplyStrategy(applyStrategy);
+ if (!mFullText.equals(text.toString())) {
+ return STATUS_DIFFERENT_TEXT;
+ }
+ if (mLinks.isEmpty()) {
+ return STATUS_NO_LINKS_FOUND;
+ }
+
+ if (spanFactory == null) {
+ spanFactory = DEFAULT_SPAN_FACTORY;
+ }
+ int applyCount = 0;
+ for (TextLink link : mLinks) {
+ final TextLinkSpan span = spanFactory.createSpan(link);
+ if (span != null) {
+ final ClickableSpan[] existingSpans = text.getSpans(
+ link.getStart(), link.getEnd(), ClickableSpan.class);
+ if (existingSpans.length > 0) {
+ if (applyStrategy == APPLY_STRATEGY_REPLACE) {
+ for (ClickableSpan existingSpan : existingSpans) {
+ text.removeSpan(existingSpan);
+ }
+ text.setSpan(span, link.getStart(), link.getEnd(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ applyCount++;
+ }
+ } else {
+ text.setSpan(span, link.getStart(), link.getEnd(),
+ Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+ applyCount++;
+ }
+ }
+ }
+ if (applyCount == 0) {
+ return STATUS_NO_LINKS_APPLIED;
+ }
+ return STATUS_LINKS_APPLIED;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mFullText);
+ dest.writeTypedList(mLinks);
+ }
+
+ public static final Parcelable.Creator<TextLinks> CREATOR =
+ new Parcelable.Creator<TextLinks>() {
+ @Override
+ public TextLinks createFromParcel(Parcel in) {
+ return new TextLinks(in);
+ }
+
+ @Override
+ public TextLinks[] newArray(int size) {
+ return new TextLinks[size];
+ }
+ };
+
+ private TextLinks(Parcel in) {
+ mFullText = in.readString();
+ mLinks = in.createTypedArrayList(TextLink.CREATOR);
+ }
+
+ /**
+ * A link, identifying a substring of text and possible entity types for it.
+ */
+ public static final class TextLink implements Parcelable {
+ private final EntityConfidence mEntityScores;
+ private final int mStart;
+ private final int mEnd;
+
+ /**
+ * Create a new TextLink.
+ *
+ * @throws IllegalArgumentException if entityScores is null or empty.
+ */
+ TextLink(int start, int end, @NonNull Map<String, Float> entityScores) {
+ Preconditions.checkNotNull(entityScores);
+ Preconditions.checkArgument(!entityScores.isEmpty());
+ Preconditions.checkArgument(start <= end);
+ mStart = start;
+ mEnd = end;
+ mEntityScores = new EntityConfidence(entityScores);
+ }
+
+ /**
+ * Returns the start index of this link in the original text.
+ *
+ * @return the start index.
+ */
+ public int getStart() {
+ return mStart;
+ }
+
+ /**
+ * Returns the end index of this link in the original text.
+ *
+ * @return the end index.
+ */
+ public int getEnd() {
+ return mEnd;
+ }
+
+ /**
+ * Returns the number of entity types that have confidence scores.
+ *
+ * @return the entity count.
+ */
+ public int getEntityCount() {
+ return mEntityScores.getEntities().size();
+ }
+
+ /**
+ * Returns the entity type at a given index. Entity types are sorted by confidence.
+ *
+ * @return the entity type at the provided index.
+ */
+ @NonNull public @EntityType String getEntity(int index) {
+ return mEntityScores.getEntities().get(index);
+ }
+
+ /**
+ * Returns the confidence score for a particular entity type.
+ *
+ * @param entityType the entity type.
+ */
+ public @FloatRange(from = 0.0, to = 1.0) float getConfidenceScore(
+ @EntityType String entityType) {
+ return mEntityScores.getConfidenceScore(entityType);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ mEntityScores.writeToParcel(dest, flags);
+ dest.writeInt(mStart);
+ dest.writeInt(mEnd);
+ }
+
+ public static final Parcelable.Creator<TextLink> CREATOR =
+ new Parcelable.Creator<TextLink>() {
+ @Override
+ public TextLink createFromParcel(Parcel in) {
+ return new TextLink(in);
+ }
+
+ @Override
+ public TextLink[] newArray(int size) {
+ return new TextLink[size];
+ }
+ };
+
+ private TextLink(Parcel in) {
+ mEntityScores = EntityConfidence.CREATOR.createFromParcel(in);
+ mStart = in.readInt();
+ mEnd = in.readInt();
+ }
+ }
+
+ /**
+ * Optional input parameters for generating TextLinks.
+ */
+ public static final class Options implements Parcelable {
+
+ private @Nullable ArrayList<Locale> mDefaultLocales;
+ private TextClassifier.EntityConfig mEntityConfig;
+ private @ApplyStrategy int mApplyStrategy;
+ private @Nullable SpanFactory mSpanFactory;
+
+ public Options() {}
+
+ /**
+ * @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 Collection<Locale> defaultLocales) {
+ mDefaultLocales = defaultLocales == null ? null : new ArrayList<>(defaultLocales);
+ return this;
+ }
+
+ /**
+ * 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;
+ }
+
+ /**
+ * Sets a strategy for resolving conflicts when applying generated links to text that
+ * already have links.
+ *
+ * @throws IllegalArgumentException if applyStrategy is not valid.
+ * @see #APPLY_STRATEGY_IGNORE
+ * @see #APPLY_STRATEGY_REPLACE
+ */
+ public Options setApplyStrategy(@ApplyStrategy int applyStrategy) {
+ checkValidApplyStrategy(applyStrategy);
+ mApplyStrategy = applyStrategy;
+ return this;
+ }
+
+ /**
+ * Sets a factory for converting a TextLink to a TextLinkSpan.
+ *
+ * <p><strong>Note: </strong>This is not parceled over IPC.
+ */
+ public Options setSpanFactory(@Nullable SpanFactory spanFactory) {
+ mSpanFactory = spanFactory;
+ return this;
+ }
+
+ /**
+ * @return ordered list of locale preferences that can be used to disambiguate
+ * the provided text.
+ */
+ @Nullable
+ public List<Locale> 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;
+ }
+
+ /**
+ * Returns the strategy for resolving conflicts when applying generated links to text that
+ * already have links.
+ *
+ * @see APPLY_STRATEGY_IGNORE
+ * @see APPLY_STRATEGY_REPLACE
+ */
+ @ApplyStrategy
+ public int getApplyStrategy() {
+ return mApplyStrategy;
+ }
+
+ /**
+ * Returns a factory for converting a TextLink to a TextLinkSpan.
+ *
+ * <p><strong>Note: </strong>This is not parcelable and will always return null if read
+ * from a parcel
+ */
+ @Nullable
+ public SpanFactory getSpanFactory() {
+ return mSpanFactory;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mDefaultLocales != null ? mDefaultLocales.size() : 0);
+ if (mDefaultLocales != null) {
+ for (Locale locale : mDefaultLocales) {
+ dest.writeSerializable(locale);
+ }
+ }
+ dest.writeInt(mEntityConfig != null ? 1 : 0);
+ if (mEntityConfig != null) {
+ mEntityConfig.writeToParcel(dest, flags);
+ }
+ dest.writeInt(mApplyStrategy);
+ // mSpanFactory is not parcelable
+ }
+
+ public static final Parcelable.Creator<Options> CREATOR =
+ new Parcelable.Creator<Options>() {
+ @Override
+ public Options createFromParcel(Parcel in) {
+ return new Options(in);
+ }
+
+ @Override
+ public Options[] newArray(int size) {
+ return new Options[size];
+ }
+ };
+
+ private Options(Parcel in) {
+ final int numLocales = in.readInt();
+ if (numLocales > 0) {
+ mDefaultLocales = new ArrayList<>();
+ mDefaultLocales.ensureCapacity(numLocales);
+ for (int i = 0; i < numLocales; ++i) {
+ mDefaultLocales.add((Locale) in.readSerializable());
+ }
+ }
+ if (in.readInt() > 0) {
+ mEntityConfig = TextClassifier.EntityConfig.CREATOR.createFromParcel(in);
+ }
+ mApplyStrategy = in.readInt();
+ // mSpanFactory is not parcelable
+ }
+ }
+
+ /**
+ * A function to create spans from TextLinks.
+ *
+ * Hidden until we convinced we want it to be part of the public API.
+ *
+ * @hide
+ */
+ @RestrictTo(RestrictTo.Scope.LIBRARY)
+ public interface SpanFactory {
+
+ /** Creates a span from a text link. */
+ TextLinkSpan createSpan(TextLink textLink);
+ }
+
+ /**
+ * A ClickableSpan for a TextLink.
+ */
+ public static class TextLinkSpan extends ClickableSpan {
+
+ private final TextLink mTextLink;
+
+ public TextLinkSpan(@Nullable TextLink textLink) {
+ mTextLink = textLink;
+ }
+
+ @Override
+ public void onClick(View widget) {
+ // TODO(jalt): integrate with AppCompatTextView to show action mode.
+ }
+
+ public final TextLink getTextLink() {
+ return mTextLink;
+ }
+ }
+
+ /**
+ * A builder to construct a TextLinks instance.
+ */
+ public static final class Builder {
+ private final String mFullText;
+ private final ArrayList<TextLink> mLinks;
+
+ /**
+ * Create a new TextLinks.Builder.
+ *
+ * @param fullText The full text to annotate with links.
+ */
+ public Builder(@NonNull String fullText) {
+ mFullText = Preconditions.checkNotNull(fullText);
+ mLinks = new ArrayList<>();
+ }
+
+ /**
+ * Adds a TextLink.
+ *
+ * @return this instance.
+ *
+ * @throws IllegalArgumentException if entityScores is null or empty.
+ */
+ public Builder addLink(int start, int end, @NonNull Map<String, Float> entityScores) {
+ mLinks.add(new TextLink(start, end, Preconditions.checkNotNull(entityScores)));
+ return this;
+ }
+
+ /**
+ * Removes all {@link TextLink}s.
+ */
+ public Builder clearTextLinks() {
+ mLinks.clear();
+ return this;
+ }
+
+ /**
+ * Constructs a TextLinks instance.
+ *
+ * @return the constructed TextLinks.
+ */
+ public TextLinks build() {
+ return new TextLinks(mFullText, mLinks);
+ }
+ }
+
+ /** The default span factory for TextView and AppCompatTextView. */
+ private static final SpanFactory DEFAULT_SPAN_FACTORY = new SpanFactory() {
+ @Override
+ public TextLinkSpan createSpan(TextLink textLink) {
+ return new TextLinkSpan(textLink);
+ }
+ };
+
+ /**
+ * @throws IllegalArgumentException if the value is invalid
+ */
+ private static void checkValidApplyStrategy(int applyStrategy) {
+ if (applyStrategy != APPLY_STRATEGY_IGNORE && applyStrategy != APPLY_STRATEGY_REPLACE) {
+ throw new IllegalArgumentException(
+ "Invalid apply strategy. See TextLinks.ApplyStrategy for options.");
+ }
+ }
+}
diff --git a/androidx/textclassifier/TextSelection.java b/androidx/textclassifier/TextSelection.java
new file mode 100644
index 00000000..17fcaacf
--- /dev/null
+++ b/androidx/textclassifier/TextSelection.java
@@ -0,0 +1,274 @@
+/*
+ * 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 androidx.textclassifier;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.support.annotation.FloatRange;
+import android.support.annotation.IntRange;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.util.ArrayMap;
+import android.support.v4.util.Preconditions;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+import androidx.textclassifier.TextClassifier.EntityType;
+
+/**
+ * Information about where text selection should be.
+ */
+public final class TextSelection implements Parcelable {
+
+ private final int mStartIndex;
+ private final int mEndIndex;
+ @NonNull private final EntityConfidence mEntityConfidence;
+ @NonNull private final String mSignature;
+
+ private TextSelection(
+ int startIndex, int endIndex, @NonNull Map<String, Float> entityConfidence,
+ @NonNull String signature) {
+ mStartIndex = startIndex;
+ mEndIndex = endIndex;
+ mEntityConfidence = new EntityConfidence(entityConfidence);
+ mSignature = signature;
+ }
+
+ /**
+ * Returns the start index of the text selection.
+ */
+ public int getSelectionStartIndex() {
+ return mStartIndex;
+ }
+
+ /**
+ * Returns the end index of the text selection.
+ */
+ public int getSelectionEndIndex() {
+ return mEndIndex;
+ }
+
+ /**
+ * Returns the number of entities found in the classified text.
+ */
+ @IntRange(from = 0)
+ public int getEntityCount() {
+ return mEntityConfidence.getEntities().size();
+ }
+
+ /**
+ * Returns the entity at the specified index. Entities are ordered from high confidence
+ * to low confidence.
+ *
+ * @throws IndexOutOfBoundsException if the specified index is out of range.
+ * @see #getEntityCount() for the number of entities available.
+ */
+ @NonNull
+ public @EntityType String getEntity(int index) {
+ return mEntityConfidence.getEntities().get(index);
+ }
+
+ /**
+ * Returns the confidence score for the specified entity. The value ranges from
+ * 0 (low confidence) to 1 (high confidence). 0 indicates that the entity was not found for the
+ * classified text.
+ */
+ @FloatRange(from = 0.0, to = 1.0)
+ public float getConfidenceScore(@EntityType String entity) {
+ return mEntityConfidence.getConfidenceScore(entity);
+ }
+
+ /**
+ * 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 getSignature() {
+ return mSignature;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ Locale.US,
+ "TextSelection {startIndex=%d, endIndex=%d, entities=%s, signature=%s}",
+ mStartIndex, mEndIndex, mEntityConfidence, mSignature);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mStartIndex);
+ dest.writeInt(mEndIndex);
+ mEntityConfidence.writeToParcel(dest, flags);
+ dest.writeString(mSignature);
+ }
+
+ public static final Parcelable.Creator<TextSelection> CREATOR =
+ new Parcelable.Creator<TextSelection>() {
+ @Override
+ public TextSelection createFromParcel(Parcel in) {
+ return new TextSelection(in);
+ }
+
+ @Override
+ public TextSelection[] newArray(int size) {
+ return new TextSelection[size];
+ }
+ };
+
+ private TextSelection(Parcel in) {
+ mStartIndex = in.readInt();
+ mEndIndex = in.readInt();
+ mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+ mSignature = in.readString();
+ }
+
+ /**
+ * Builder used to build {@link TextSelection} objects.
+ */
+ public static final class Builder {
+
+ private final int mStartIndex;
+ private final int mEndIndex;
+ @NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>();
+ @NonNull private String mSignature = "";
+
+ /**
+ * Creates a builder used to build {@link TextSelection} objects.
+ *
+ * @param startIndex the start index of the text selection.
+ * @param endIndex the end index of the text selection. Must be greater than startIndex
+ */
+ public Builder(@IntRange(from = 0) int startIndex, @IntRange(from = 0) int endIndex) {
+ Preconditions.checkArgument(startIndex >= 0);
+ Preconditions.checkArgument(endIndex > startIndex);
+ mStartIndex = startIndex;
+ mEndIndex = endIndex;
+ }
+
+ /**
+ * Sets an entity type for the classified text and assigns a confidence score.
+ *
+ * @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.
+ */
+ public Builder setEntityType(
+ @NonNull @EntityType String type,
+ @FloatRange(from = 0.0, to = 1.0) float confidenceScore) {
+ mEntityConfidence.put(type, confidenceScore);
+ return this;
+ }
+
+ /**
+ * 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.
+ */
+ public Builder setSignature(@NonNull String signature) {
+ mSignature = Preconditions.checkNotNull(signature);
+ return this;
+ }
+
+ /**
+ * Builds and returns {@link TextSelection} object.
+ */
+ public TextSelection build() {
+ return new TextSelection(
+ mStartIndex, mEndIndex, mEntityConfidence, mSignature);
+ }
+ }
+
+ /**
+ * Optional input parameters for generating TextSelection.
+ */
+ public static final class Options implements Parcelable {
+
+ private @Nullable ArrayList<Locale> mDefaultLocales;
+
+ public Options() {}
+
+ /**
+ * @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 Collection<Locale> defaultLocales) {
+ mDefaultLocales = defaultLocales == null ? null : new ArrayList<>(defaultLocales);
+ return this;
+ }
+
+ /**
+ * @return ordered list of locale preferences that can be used to disambiguate
+ * the provided text.
+ */
+ @Nullable
+ public List<Locale> getDefaultLocales() {
+ return mDefaultLocales;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mDefaultLocales != null ? mDefaultLocales.size() : 0);
+ if (mDefaultLocales != null) {
+ for (Locale locale : mDefaultLocales) {
+ dest.writeSerializable(locale);
+ }
+ }
+ }
+
+ public static final Parcelable.Creator<Options> CREATOR =
+ new Parcelable.Creator<Options>() {
+ @Override
+ public Options createFromParcel(Parcel in) {
+ return new Options(in);
+ }
+
+ @Override
+ public Options[] newArray(int size) {
+ return new Options[size];
+ }
+ };
+
+ private Options(Parcel in) {
+ final int numLocales = in.readInt();
+ if (numLocales > 0) {
+ mDefaultLocales = new ArrayList<>();
+ mDefaultLocales.ensureCapacity(numLocales);
+ for (int i = 0; i < numLocales; ++i) {
+ mDefaultLocales.add((Locale) in.readSerializable());
+ }
+ }
+ }
+ }
+}
diff --git a/androidx/recyclerview/selection/AutoScroller.java b/androidx/widget/recyclerview/selection/AutoScroller.java
index 13e87bd0..27e9c0ac 100644
--- a/androidx/recyclerview/selection/AutoScroller.java
+++ b/androidx/widget/recyclerview/selection/AutoScroller.java
@@ -14,15 +14,17 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import android.graphics.Point;
+import android.support.annotation.NonNull;
import android.support.annotation.RestrictTo;
/**
* Provides support for auto-scrolling a view.
+ *
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
@@ -32,11 +34,11 @@ public abstract class AutoScroller {
* Resets state of the scroller. Call this when the user activity that is driving
* auto-scrolling is done.
*/
- protected abstract void reset();
+ public abstract void reset();
/**
* Processes a new input location.
* @param location
*/
- protected abstract void scroll(Point location);
+ public abstract void scroll(@NonNull Point location);
}
diff --git a/androidx/widget/recyclerview/selection/BandPredicate.java b/androidx/widget/recyclerview/selection/BandPredicate.java
new file mode 100644
index 00000000..05f4b2f8
--- /dev/null
+++ b/androidx/widget/recyclerview/selection/BandPredicate.java
@@ -0,0 +1,137 @@
+/*
+ * 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.widget.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.MotionEvent;
+import android.view.View;
+
+/**
+ * Provides a means of controlling when and where band selection can be initiated.
+ *
+ * <p>
+ * Two default implementations are provided: {@link EmptyArea}, and {@link NonDraggableArea}.
+ *
+ * @see SelectionTracker.Builder#withBandPredicate(BandPredicate)
+ */
+public abstract class BandPredicate {
+
+ /**
+ * @return true if band selection can be initiated in response to the {@link MotionEvent}.
+ */
+ public abstract boolean canInitiate(MotionEvent e);
+
+ private static boolean hasSupportedLayoutManager(@NonNull RecyclerView recyclerView) {
+ RecyclerView.LayoutManager lm = recyclerView.getLayoutManager();
+ return lm instanceof GridLayoutManager
+ || lm instanceof LinearLayoutManager;
+ }
+
+ /**
+ * A BandPredicate that allows initiation of band selection only in areas of RecyclerView
+ * that map to {@link RecyclerView#NO_POSITION}. In most cases, this will be the empty areas
+ * between views.
+ *
+ * <p>
+ * Use this implementation to permit band selection only in empty areas
+ * surrounding view items. But be advised that if there is no empy area around
+ * view items, band selection cannot be initiated.
+ */
+ public static final class EmptyArea extends BandPredicate {
+
+ private final RecyclerView mRecyclerView;
+
+ /**
+ * @param recyclerView the owner RecyclerView
+ */
+ public EmptyArea(@NonNull RecyclerView recyclerView) {
+ checkArgument(recyclerView != null);
+
+ mRecyclerView = recyclerView;
+ }
+
+ @Override
+ public boolean canInitiate(@NonNull MotionEvent e) {
+ if (!hasSupportedLayoutManager(mRecyclerView)
+ || mRecyclerView.hasPendingAdapterUpdates()) {
+ return false;
+ }
+
+ View itemView = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
+ int position = itemView != null
+ ? mRecyclerView.getChildAdapterPosition(itemView)
+ : RecyclerView.NO_POSITION;
+
+ return position == RecyclerView.NO_POSITION;
+ }
+ }
+
+ /**
+ * A BandPredicate that allows initiation of band selection in any area that is not
+ * draggable as determined by consulting
+ * {@link ItemDetailsLookup#inItemDragRegion(MotionEvent)}. By default empty
+ * areas (those with a position that maps to {@link RecyclerView#NO_POSITION}
+ * are considered non-draggable.
+ *
+ * <p>
+ * Use this implementation in order to permit band selection in
+ * otherwise empty areas of a View. This is useful especially in
+ * list layouts where there is no empty space surrounding the list items,
+ * and individual list items may contain extra white space (like
+ * in a list of varying length words).
+ *
+ * @see ItemDetailsLookup#inItemDragRegion(MotionEvent)
+ */
+ public static final class NonDraggableArea extends BandPredicate {
+
+ private final RecyclerView mRecyclerView;
+ private final ItemDetailsLookup mDetailsLookup;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param recyclerView the owner RecyclerView
+ * @param detailsLookup provides access to item details.
+ */
+ public NonDraggableArea(
+ @NonNull RecyclerView recyclerView, @NonNull ItemDetailsLookup detailsLookup) {
+
+ checkArgument(recyclerView != null);
+ checkArgument(detailsLookup != null);
+
+ mRecyclerView = recyclerView;
+ mDetailsLookup = detailsLookup;
+ }
+
+ @Override
+ public boolean canInitiate(@NonNull MotionEvent e) {
+ if (!hasSupportedLayoutManager(mRecyclerView)
+ || mRecyclerView.hasPendingAdapterUpdates()) {
+ return false;
+ }
+
+ @Nullable ItemDetailsLookup.ItemDetails details = mDetailsLookup.getItemDetails(e);
+ return (details == null) || !details.inDragRegion(e);
+ }
+ }
+}
diff --git a/androidx/recyclerview/selection/BandSelectionHelper.java b/androidx/widget/recyclerview/selection/BandSelectionHelper.java
index 5362e2b5..16cab293 100644
--- a/androidx/recyclerview/selection/BandSelectionHelper.java
+++ b/androidx/widget/recyclerview/selection/BandSelectionHelper.java
@@ -14,16 +14,17 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.v4.util.Preconditions.checkArgument;
import static android.support.v4.util.Preconditions.checkState;
-import static androidx.recyclerview.selection.Shared.VERBOSE;
+import static androidx.widget.recyclerview.selection.Shared.VERBOSE;
import android.graphics.Point;
import android.graphics.Rect;
import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
@@ -34,20 +35,24 @@ import android.view.MotionEvent;
import java.util.Set;
-import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+import androidx.widget.recyclerview.selection.SelectionTracker.SelectionPredicate;
/**
* Provides mouse driven band-selection support when used in conjunction with a {@link RecyclerView}
* instance. This class is responsible for rendering a band overlay and manipulating selection
* status of the items it intersects with.
*
- * <p> Given the recycling nature of RecyclerView items that have scrolled off-screen would not
+ * <p>
+ * Given the recycling nature of RecyclerView items that have scrolled off-screen would not
* be selectable with a band that itself was partially rendered off-screen. To address this,
* BandSelectionController builds a model of the list/grid information presented by RecyclerView as
* the user interacts with items using their pointer (and the band). Selectable items that intersect
* with the band, both on and off screen, are selected on pointer up.
*
- * @param <K> Selection key type. Usually String or Long.
+ * @see SelectionTracker.Builder#withBandTooltypes(int...) for details on the specific
+ * tooltypes routed to this helper.
+ *
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/
class BandSelectionHelper<K> implements OnItemTouchListener {
@@ -56,11 +61,10 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
private final BandHost mHost;
private final ItemKeyProvider<K> mKeyProvider;
- private final SelectionHelper<K> mSelectionHelper;
- private final SelectionPredicate<K> mSelectionPredicate;
+ private final SelectionTracker<K> mSelectionTracker;
private final BandPredicate mBandPredicate;
- private final FocusCallbacks<K> mFocusCallbacks;
- private final ContentLock mLock;
+ private final FocusDelegate<K> mFocusDelegate;
+ private final OperationMonitor mLock;
private final AutoScroller mScroller;
private final GridModel.SelectionObserver mGridObserver;
@@ -72,30 +76,27 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
* See {@link BandSelectionHelper#create}.
*/
BandSelectionHelper(
- BandHost host,
- AutoScroller scroller,
- ItemKeyProvider<K> keyProvider,
- SelectionHelper<K> selectionHelper,
- SelectionPredicate<K> selectionPredicate,
- BandPredicate bandPredicate,
- FocusCallbacks<K> focusCallbacks,
- ContentLock lock) {
+ @NonNull BandHost host,
+ @NonNull AutoScroller scroller,
+ @NonNull ItemKeyProvider<K> keyProvider,
+ @NonNull SelectionTracker<K> selectionTracker,
+ @NonNull BandPredicate bandPredicate,
+ @NonNull FocusDelegate<K> focusDelegate,
+ @NonNull OperationMonitor lock) {
checkArgument(host != null);
checkArgument(scroller != null);
checkArgument(keyProvider != null);
- checkArgument(selectionHelper != null);
- checkArgument(selectionPredicate != null);
+ checkArgument(selectionTracker != null);
checkArgument(bandPredicate != null);
- checkArgument(focusCallbacks != null);
+ checkArgument(focusDelegate != null);
checkArgument(lock != null);
mHost = host;
mKeyProvider = keyProvider;
- mSelectionHelper = selectionHelper;
- mSelectionPredicate = selectionPredicate;
+ mSelectionTracker = selectionTracker;
mBandPredicate = bandPredicate;
- mFocusCallbacks = focusCallbacks;
+ mFocusDelegate = focusDelegate;
mLock = lock;
mHost.addOnScrollListener(
@@ -111,7 +112,7 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
mGridObserver = new GridModel.SelectionObserver<K>() {
@Override
public void onSelectionChanged(Set<K> updatedSelection) {
- mSelectionHelper.setProvisionalSelection(updatedSelection);
+ mSelectionTracker.setProvisionalSelection(updatedSelection);
}
};
}
@@ -122,24 +123,23 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
* @return new BandSelectionHelper instance.
*/
static <K> BandSelectionHelper create(
- RecyclerView recView,
- AutoScroller scroller,
+ @NonNull RecyclerView recyclerView,
+ @NonNull AutoScroller scroller,
@DrawableRes int bandOverlayId,
- ItemKeyProvider<K> keyProvider,
- SelectionHelper<K> selectionHelper,
- SelectionPredicate<K> selectionPredicate,
- BandPredicate bandPredicate,
- FocusCallbacks<K> focusCallbacks,
- ContentLock lock) {
+ @NonNull ItemKeyProvider<K> keyProvider,
+ @NonNull SelectionTracker<K> selectionTracker,
+ @NonNull SelectionPredicate<K> selectionPredicate,
+ @NonNull BandPredicate bandPredicate,
+ @NonNull FocusDelegate<K> focusDelegate,
+ @NonNull OperationMonitor lock) {
return new BandSelectionHelper<>(
- new DefaultBandHost<>(recView, bandOverlayId, keyProvider, selectionPredicate),
+ new DefaultBandHost<>(recyclerView, bandOverlayId, keyProvider, selectionPredicate),
scroller,
keyProvider,
- selectionHelper,
- selectionPredicate,
+ selectionTracker,
bandPredicate,
- focusCallbacks,
+ focusDelegate,
lock);
}
@@ -147,7 +147,7 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
boolean isActive() {
boolean active = mModel != null;
if (DEBUG && active) {
- mLock.checkLocked();
+ mLock.checkStarted();
}
return active;
}
@@ -171,22 +171,22 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
mOrigin = null;
mScroller.reset();
- mLock.unblock();
+ mLock.stop();
}
@VisibleForTesting
- boolean shouldStart(MotionEvent e) {
+ boolean shouldStart(@NonNull MotionEvent e) {
// b/30146357 && b/23793622. onInterceptTouchEvent does not dispatch events to onTouchEvent
// unless the event is != ACTION_DOWN. Thus, we need to actually start band selection when
// mouse moves.
- return MotionEvents.isPrimaryButtonPressed(e)
+ return MotionEvents.isPrimaryMouseButtonPressed(e)
&& MotionEvents.isActionMove(e)
&& mBandPredicate.canInitiate(e)
&& !isActive();
}
@VisibleForTesting
- boolean shouldStop(MotionEvent e) {
+ boolean shouldStop(@NonNull MotionEvent e) {
return isActive()
&& (MotionEvents.isActionUp(e)
|| MotionEvents.isActionPointerUp(e)
@@ -194,7 +194,7 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
}
@Override
- public boolean onInterceptTouchEvent(RecyclerView unused, MotionEvent e) {
+ public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
if (shouldStart(e)) {
startBandSelect(e);
} else if (shouldStop(e)) {
@@ -208,7 +208,7 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
* Processes a MotionEvent by starting, ending, or resizing the band select overlay.
*/
@Override
- public void onTouchEvent(RecyclerView unused, MotionEvent e) {
+ public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
if (shouldStop(e)) {
endBandSelect();
return;
@@ -241,11 +241,11 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
/**
* Starts band select by adding the drawable to the RecyclerView's overlay.
*/
- private void startBandSelect(MotionEvent e) {
+ private void startBandSelect(@NonNull MotionEvent e) {
checkState(!isActive());
if (!MotionEvents.isCtrlKeyPressed(e)) {
- mSelectionHelper.clearSelection();
+ mSelectionTracker.clearSelection();
}
Point origin = MotionEvents.getOrigin(e);
@@ -254,8 +254,8 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
mModel = mHost.createGridModel();
mModel.addOnSelectionChangedListener(mGridObserver);
- mLock.block();
- mFocusCallbacks.clearFocus();
+ mLock.start();
+ mFocusDelegate.clearFocus();
mOrigin = origin;
// NOTE: Pay heed that resizeBand modifies the y coordinates
// in onScrolled. Not sure if model expects this. If not
@@ -295,21 +295,21 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
// item selected (and nearest to the cursor).
int firstSelected = mModel.getPositionNearestOrigin();
if (firstSelected != GridModel.NOT_SET
- && mSelectionHelper.isSelected(mKeyProvider.getKey(firstSelected))) {
+ && mSelectionTracker.isSelected(mKeyProvider.getKey(firstSelected))) {
// Establish the band selection point as range anchor. This
// allows touch and keyboard based selection activities
// to be based on the band selection anchor point.
- mSelectionHelper.anchorRange(firstSelected);
+ mSelectionTracker.anchorRange(firstSelected);
}
- mSelectionHelper.mergeProvisionalSelection();
+ mSelectionTracker.mergeProvisionalSelection();
reset();
}
/**
- * @see RecyclerView.OnScrollListener
+ * @see OnScrollListener
*/
- private void onScrolled(RecyclerView recyclerView, int dx, int dy) {
+ private void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (!isActive()) {
return;
}
@@ -324,7 +324,7 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
* Provides functionality for BandController. Exists primarily to tests that are
* fully isolated from RecyclerView.
*
- * @param <K> Selection key type. Usually String or Long.
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/
abstract static class BandHost<K> {
@@ -338,7 +338,7 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
*
* @param bounds The boundaries of the band to show.
*/
- abstract void showBand(Rect bounds);
+ abstract void showBand(@NonNull Rect bounds);
/**
* Hide the band.
@@ -350,6 +350,6 @@ class BandSelectionHelper<K> implements OnItemTouchListener {
*
* @param listener
*/
- abstract void addOnScrollListener(RecyclerView.OnScrollListener listener);
+ abstract void addOnScrollListener(@NonNull OnScrollListener listener);
}
}
diff --git a/androidx/recyclerview/selection/DefaultBandHost.java b/androidx/widget/recyclerview/selection/DefaultBandHost.java
index f0fd4fe6..2d0b41f0 100644
--- a/androidx/recyclerview/selection/DefaultBandHost.java
+++ b/androidx/widget/recyclerview/selection/DefaultBandHost.java
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.v4.util.Preconditions.checkArgument;
@@ -23,12 +23,14 @@ import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.ItemDecoration;
+import android.support.v7.widget.RecyclerView.OnScrollListener;
import android.view.View;
-import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+import androidx.widget.recyclerview.selection.SelectionTracker.SelectionPredicate;
/**
* RecyclerView backed {@link BandSelectionHelper.BandHost}.
@@ -37,21 +39,21 @@ final class DefaultBandHost<K> extends GridModel.GridHost<K> {
private static final Rect NILL_RECT = new Rect(0, 0, 0, 0);
- private final RecyclerView mRecView;
+ private final RecyclerView mRecyclerView;
private final Drawable mBand;
private final ItemKeyProvider<K> mKeyProvider;
private final SelectionPredicate<K> mSelectionPredicate;
DefaultBandHost(
- RecyclerView recView,
+ @NonNull RecyclerView recyclerView,
@DrawableRes int bandOverlayId,
- ItemKeyProvider<K> keyProvider,
- SelectionPredicate<K> selectionPredicate) {
+ @NonNull ItemKeyProvider<K> keyProvider,
+ @NonNull SelectionPredicate<K> selectionPredicate) {
- checkArgument(recView != null);
+ checkArgument(recyclerView != null);
- mRecView = recView;
- mBand = mRecView.getContext().getResources().getDrawable(bandOverlayId);
+ mRecyclerView = recyclerView;
+ mBand = mRecyclerView.getContext().getResources().getDrawable(bandOverlayId);
checkArgument(mBand != null);
checkArgument(keyProvider != null);
@@ -60,7 +62,7 @@ final class DefaultBandHost<K> extends GridModel.GridHost<K> {
mKeyProvider = keyProvider;
mSelectionPredicate = selectionPredicate;
- mRecView.addItemDecoration(
+ mRecyclerView.addItemDecoration(
new ItemDecoration() {
@Override
public void onDrawOver(
@@ -79,45 +81,45 @@ final class DefaultBandHost<K> extends GridModel.GridHost<K> {
@Override
int getAdapterPositionAt(int index) {
- return mRecView.getChildAdapterPosition(mRecView.getChildAt(index));
+ return mRecyclerView.getChildAdapterPosition(mRecyclerView.getChildAt(index));
}
@Override
- void addOnScrollListener(RecyclerView.OnScrollListener listener) {
- mRecView.addOnScrollListener(listener);
+ void addOnScrollListener(@NonNull OnScrollListener listener) {
+ mRecyclerView.addOnScrollListener(listener);
}
@Override
- void removeOnScrollListener(RecyclerView.OnScrollListener listener) {
- mRecView.removeOnScrollListener(listener);
+ void removeOnScrollListener(@NonNull OnScrollListener listener) {
+ mRecyclerView.removeOnScrollListener(listener);
}
@Override
- Point createAbsolutePoint(Point relativePoint) {
- return new Point(relativePoint.x + mRecView.computeHorizontalScrollOffset(),
- relativePoint.y + mRecView.computeVerticalScrollOffset());
+ Point createAbsolutePoint(@NonNull Point relativePoint) {
+ return new Point(relativePoint.x + mRecyclerView.computeHorizontalScrollOffset(),
+ relativePoint.y + mRecyclerView.computeVerticalScrollOffset());
}
@Override
Rect getAbsoluteRectForChildViewAt(int index) {
- final View child = mRecView.getChildAt(index);
+ final View child = mRecyclerView.getChildAt(index);
final Rect childRect = new Rect();
child.getHitRect(childRect);
- childRect.left += mRecView.computeHorizontalScrollOffset();
- childRect.right += mRecView.computeHorizontalScrollOffset();
- childRect.top += mRecView.computeVerticalScrollOffset();
- childRect.bottom += mRecView.computeVerticalScrollOffset();
+ childRect.left += mRecyclerView.computeHorizontalScrollOffset();
+ childRect.right += mRecyclerView.computeHorizontalScrollOffset();
+ childRect.top += mRecyclerView.computeVerticalScrollOffset();
+ childRect.bottom += mRecyclerView.computeVerticalScrollOffset();
return childRect;
}
@Override
int getVisibleChildCount() {
- return mRecView.getChildCount();
+ return mRecyclerView.getChildCount();
}
@Override
int getColumnCount() {
- RecyclerView.LayoutManager layoutManager = mRecView.getLayoutManager();
+ RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
if (layoutManager instanceof GridLayoutManager) {
return ((GridLayoutManager) layoutManager).getSpanCount();
}
@@ -127,27 +129,27 @@ final class DefaultBandHost<K> extends GridModel.GridHost<K> {
}
@Override
- void showBand(Rect rect) {
+ void showBand(@NonNull Rect rect) {
mBand.setBounds(rect);
- // TODO: mRecView.invalidateItemDecorations() should work, but it isn't currently.
+ // TODO: mRecyclerView.invalidateItemDecorations() should work, but it isn't currently.
// NOTE: That without invalidating rv, the band only gets updated
// when the pointer moves off a the item view into "NO_POSITION" territory.
- mRecView.invalidate();
+ mRecyclerView.invalidate();
}
@Override
void hideBand() {
mBand.setBounds(NILL_RECT);
- // TODO: mRecView.invalidateItemDecorations() should work, but it isn't currently.
- mRecView.invalidate();
+ // TODO: mRecyclerView.invalidateItemDecorations() should work, but it isn't currently.
+ mRecyclerView.invalidate();
}
- private void onDrawBand(Canvas c) {
+ private void onDrawBand(@NonNull Canvas c) {
mBand.draw(c);
}
@Override
boolean hasView(int pos) {
- return mRecView.findViewHolderForAdapterPosition(pos) != null;
+ return mRecyclerView.findViewHolderForAdapterPosition(pos) != null;
}
}
diff --git a/androidx/recyclerview/selection/DefaultSelectionHelper.java b/androidx/widget/recyclerview/selection/DefaultSelectionTracker.java
index 5625e3da..d58f1c8a 100644
--- a/androidx/recyclerview/selection/DefaultSelectionHelper.java
+++ b/androidx/widget/recyclerview/selection/DefaultSelectionTracker.java
@@ -14,16 +14,19 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static android.support.v4.util.Preconditions.checkArgument;
import static android.support.v4.util.Preconditions.checkState;
-import static androidx.recyclerview.selection.Shared.DEBUG;
+import static androidx.widget.recyclerview.selection.Shared.DEBUG;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
+import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
@@ -32,56 +35,70 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
-import androidx.recyclerview.selection.Range.RangeType;
+import androidx.widget.recyclerview.selection.Range.RangeType;
/**
- * {@link SelectionHelper} providing support for traditional multi-item selection on top
+ * {@link SelectionTracker} providing support for traditional multi-item selection on top
* of {@link RecyclerView}.
*
- * <p>The class supports running in a single-select mode, which can be enabled
- * by passing {@code #MODE_SINGLE} to the constructor.
+ * <p>
+ * The class supports running in a single-select mode, which can be enabled using
+ * {@link SelectionPredicate#canSelectMultiple()}.
*
- * @param <K> Selection key type. Usually String or Long.
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP)
-public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
+public class DefaultSelectionTracker<K> extends SelectionTracker<K> {
- private static final String TAG = "DefaultSelectionHelper";
+ private static final String TAG = "DefaultSelectionTracker";
+ private static final String EXTRA_SELECTION_PREFIX = "androidx.widget.recyclerview.selection";
private final Selection<K> mSelection = new Selection<>();
private final List<SelectionObserver> mObservers = new ArrayList<>(1);
private final ItemKeyProvider<K> mKeyProvider;
private final SelectionPredicate<K> mSelectionPredicate;
+ private final StorageStrategy<K> mStorage;
private final RangeCallbacks mRangeCallbacks;
private final boolean mSingleSelect;
+ private final String mSelectionId;
private @Nullable Range mRange;
/**
* Creates a new instance.
*
+ * @param selectionId A unique string identifying this selection in the context
+ * of the activity or fragment.
* @param keyProvider client supplied class providing access to stable ids.
* @param selectionPredicate A predicate allowing the client to disallow selection
- * of individual elements.
+ * @param storage Strategy for storing typed selection in bundle.
*/
- public DefaultSelectionHelper(
- ItemKeyProvider keyProvider,
- SelectionPredicate selectionPredicate) {
-
+ public DefaultSelectionTracker(
+ @NonNull String selectionId,
+ @NonNull ItemKeyProvider keyProvider,
+ @NonNull SelectionPredicate selectionPredicate,
+ @NonNull StorageStrategy<K> storage) {
+
+ checkArgument(selectionId != null);
+ checkArgument(!selectionId.trim().isEmpty());
checkArgument(keyProvider != null);
checkArgument(selectionPredicate != null);
+ checkArgument(storage != null);
+ mSelectionId = selectionId;
mKeyProvider = keyProvider;
mSelectionPredicate = selectionPredicate;
+ mStorage = storage;
+
mRangeCallbacks = new RangeCallbacks();
mSingleSelect = !selectionPredicate.canSelectMultiple();
}
@Override
- public void addObserver(SelectionObserver callback) {
+ public void addObserver(@NonNull SelectionObserver callback) {
checkArgument(callback != null);
mObservers.add(callback);
}
@@ -97,7 +114,7 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
}
@Override
- public void copySelection(Selection dest) {
+ public void copySelection(@NonNull Selection dest) {
dest.copyFrom(mSelection);
}
@@ -107,7 +124,7 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
}
@Override
- public void restoreSelection(Selection other) {
+ public void restoreSelection(@NonNull Selection other) {
checkArgument(other != null);
setItemsSelectedQuietly(other.mSelection, true);
// NOTE: We intentionally don't restore provisional selection. It's provisional.
@@ -115,13 +132,13 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
}
@Override
- public boolean setItemsSelected(Iterable<K> keys, boolean selected) {
+ public boolean setItemsSelected(@NonNull Iterable<K> keys, boolean selected) {
boolean changed = setItemsSelectedQuietly(keys, selected);
notifySelectionChanged();
return changed;
}
- private boolean setItemsSelectedQuietly(Iterable<K> keys, boolean selected) {
+ private boolean setItemsSelectedQuietly(@NonNull Iterable<K> keys, boolean selected) {
boolean changed = false;
for (K key: keys) {
boolean itemChanged = selected
@@ -136,7 +153,17 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
}
@Override
- public void clearSelection() {
+ public boolean clearSelection() {
+ if (!hasSelection()) {
+ return false;
+ }
+
+ clearProvisionalSelection();
+ clearPrimarySelection();
+ return true;
+ }
+
+ private void clearPrimarySelection() {
if (!hasSelection()) {
return;
}
@@ -146,14 +173,6 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
notifySelectionChanged();
}
- @Override
- public boolean clear() {
- boolean somethingChanged = hasSelection();
- clearProvisionalSelection();
- clearSelection();
- return somethingChanged;
- }
-
/**
* Clears the selection, without notifying selection listeners.
* Returns items in previous selection. Callers are responsible for notifying
@@ -172,33 +191,33 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
}
@Override
- public boolean select(K key) {
+ public boolean select(@NonNull K key) {
checkArgument(key != null);
- if (!mSelection.contains(key)) {
- if (!canSetState(key, true)) {
- if (DEBUG) Log.d(TAG, "Select cancelled by selection predicate test.");
- return false;
- }
-
- // Enforce single selection policy.
- if (mSingleSelect && hasSelection()) {
- Selection prev = clearSelectionQuietly();
- notifySelectionCleared(prev);
- }
+ if (mSelection.contains(key)) {
+ return false;
+ }
- mSelection.add(key);
- notifyItemStateChanged(key, true);
- notifySelectionChanged();
+ if (!canSetState(key, true)) {
+ if (DEBUG) Log.d(TAG, "Select cancelled by selection predicate test.");
+ return false;
+ }
- return true;
+ // Enforce single selection policy.
+ if (mSingleSelect && hasSelection()) {
+ Selection prev = clearSelectionQuietly();
+ notifySelectionCleared(prev);
}
- return false;
+ mSelection.add(key);
+ notifyItemStateChanged(key, true);
+ notifySelectionChanged();
+
+ return true;
}
@Override
- public boolean deselect(K key) {
+ public boolean deselect(@NonNull K key) {
checkArgument(key != null);
if (mSelection.contains(key)) {
@@ -223,8 +242,10 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
@Override
public void startRange(int position) {
- select(mKeyProvider.getKey(position));
- anchorRange(position);
+ if (mSelection.contains(mKeyProvider.getKey(position))
+ || select(mKeyProvider.getKey(position))) {
+ anchorRange(position);
+ }
}
@Override
@@ -281,7 +302,7 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
}
@Override
- public void setProvisionalSelection(Set<K> newSelection) {
+ public void setProvisionalSelection(@NonNull Set<K> newSelection) {
if (mSingleSelect) {
return;
}
@@ -319,7 +340,7 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
return mRange != null;
}
- private boolean canSetState(K key, boolean nextState) {
+ private boolean canSetState(@NonNull K key, boolean nextState) {
return mSelectionPredicate.canSetStateForKey(key, nextState);
}
@@ -327,7 +348,7 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
void onDataSetChanged() {
mSelection.clearProvisionalSelection();
- notifySelectionReset();
+ notifySelectionRefresh();
for (K key : mSelection) {
// If the underlying data set has changed, before restoring
@@ -351,7 +372,7 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
* Notifies registered listeners when the selection status of a single item
* (identified by {@code position}) changes.
*/
- private void notifyItemStateChanged(K key, boolean selected) {
+ private void notifyItemStateChanged(@NonNull K key, boolean selected) {
checkArgument(key != null);
int lastListenerIndex = mObservers.size() - 1;
@@ -360,7 +381,7 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
}
}
- private void notifySelectionCleared(Selection<K> selection) {
+ private void notifySelectionCleared(@NonNull Selection<K> selection) {
for (K key: selection.mSelection) {
notifyItemStateChanged(key, false);
}
@@ -389,10 +410,10 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
}
}
- private void notifySelectionReset() {
+ private void notifySelectionRefresh() {
int lastListenerIndex = mObservers.size() - 1;
for (int i = lastListenerIndex; i >= 0; i--) {
- mObservers.get(i).onSelectionReset();
+ mObservers.get(i).onSelectionRefresh();
}
}
@@ -457,6 +478,38 @@ public class DefaultSelectionHelper<K> extends SelectionHelper<K> {
notifySelectionChanged();
}
+ @VisibleForTesting
+ String getInstanceStateKey() {
+ return EXTRA_SELECTION_PREFIX + ":" + mSelectionId;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public final void onSaveInstanceState(@NonNull Bundle state) {
+ if (mSelection.isEmpty()) {
+ return;
+ }
+
+ state.putBundle(getInstanceStateKey(), mStorage.asBundle(mSelection));
+ }
+
+ @Override
+ public final void onRestoreInstanceState(@Nullable Bundle state) {
+ if (state == null) {
+ return;
+ }
+
+ @Nullable Bundle selectionState = state.getBundle(getInstanceStateKey());
+ if (selectionState == null) {
+ return;
+ }
+
+ Selection<K> selection = mStorage.asSelection(selectionState);
+ if (selection != null && !selection.isEmpty()) {
+ restoreSelection(selection);
+ }
+ }
+
private final class RangeCallbacks extends Range.Callbacks {
@Override
void updateForRange(int begin, int end, boolean selected, int type) {
diff --git a/androidx/recyclerview/selection/EventBridge.java b/androidx/widget/recyclerview/selection/EventBridge.java
index b418ad4f..9b57de0e 100644
--- a/androidx/recyclerview/selection/EventBridge.java
+++ b/androidx/widget/recyclerview/selection/EventBridge.java
@@ -14,13 +14,15 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static android.support.v4.util.Preconditions.checkArgument;
-import static androidx.recyclerview.selection.Shared.VERBOSE;
+import static androidx.widget.recyclerview.selection.Shared.VERBOSE;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
@@ -28,7 +30,11 @@ import android.util.Log;
/**
* Provides the necessary glue to notify RecyclerView when selection data changes,
- * and to notify SelectionHelper when the underlying RecyclerView.Adapter data changes.
+ * and to notify SelectionTracker when the underlying RecyclerView.Adapter data changes.
+ *
+ * This strict decoupling is necessary to permit a single SelectionTracker to work
+ * with multiple RecyclerView instances. This may be necessary when multiple
+ * different views of data are presented to the user.
*
* @hide
*/
@@ -42,44 +48,43 @@ public class EventBridge {
* Installs the event bridge for on the supplied adapter/helper.
*
* @param adapter
- * @param selectionHelper
+ * @param selectionTracker
* @param keyProvider
- * @param <K>
+ *
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/
- @VisibleForTesting
public static <K> void install(
- RecyclerView.Adapter<?> adapter,
- SelectionHelper<K> selectionHelper,
- ItemKeyProvider<K> keyProvider) {
- new AdapterToSelectionHelper(adapter, selectionHelper);
- new SelectionHelperToAdapter<>(selectionHelper, keyProvider, adapter);
+ @NonNull RecyclerView.Adapter<?> adapter,
+ @NonNull SelectionTracker<K> selectionTracker,
+ @NonNull ItemKeyProvider<K> keyProvider) {
+
+ // setup bridges to relay selection events.
+ new AdapterToTrackerBridge(adapter, selectionTracker);
+ new TrackerToAdapterBridge<>(selectionTracker, keyProvider, adapter);
}
- private static final class AdapterToSelectionHelper extends RecyclerView.AdapterDataObserver {
+ private static final class AdapterToTrackerBridge extends RecyclerView.AdapterDataObserver {
- private final SelectionHelper<?> mSelectionHelper;
+ private final SelectionTracker<?> mSelectionTracker;
- AdapterToSelectionHelper(
- RecyclerView.Adapter<?> adapter,
- SelectionHelper<?> selectionHelper) {
+ AdapterToTrackerBridge(
+ @NonNull RecyclerView.Adapter<?> adapter,
+ @NonNull SelectionTracker<?> selectionTracker) {
adapter.registerAdapterDataObserver(this);
- checkArgument(selectionHelper != null);
- mSelectionHelper = selectionHelper;
+ checkArgument(selectionTracker != null);
+ mSelectionTracker = selectionTracker;
}
@Override
public void onChanged() {
- mSelectionHelper.onDataSetChanged();
+ mSelectionTracker.onDataSetChanged();
}
@Override
- public void onItemRangeChanged(int startPosition, int itemCount, Object payload) {
- // No change in position. Ignore, since we assume
- // selection is a user driven activity. So changes
- // in properties of items shouldn't result in a
- // change of selection.
- // TODO: It is possible properties of items chould change to make them unselectable.
+ public void onItemRangeChanged(int startPosition, int itemCount, @Nullable Object payload) {
+ // No change in position. Ignore.
+ // TODO(b/72393576): Properties of items could change. Should reevaluate selected status
}
@Override
@@ -98,18 +103,18 @@ public class EventBridge {
}
}
- private static final class SelectionHelperToAdapter<K>
- extends SelectionHelper.SelectionObserver<K> {
+ private static final class TrackerToAdapterBridge<K>
+ extends SelectionTracker.SelectionObserver<K> {
private final ItemKeyProvider<K> mKeyProvider;
private final RecyclerView.Adapter<?> mAdapter;
- SelectionHelperToAdapter(
- SelectionHelper<K> selectionHelper,
- ItemKeyProvider<K> keyProvider,
- RecyclerView.Adapter<?> adapter) {
+ TrackerToAdapterBridge(
+ @NonNull SelectionTracker<K> selectionTracker,
+ @NonNull ItemKeyProvider<K> keyProvider,
+ @NonNull RecyclerView.Adapter<?> adapter) {
- selectionHelper.addObserver(this);
+ selectionTracker.addObserver(this);
checkArgument(keyProvider != null);
checkArgument(adapter != null);
@@ -122,7 +127,7 @@ public class EventBridge {
* Called when state of an item has been changed.
*/
@Override
- public void onItemStateChanged(K key, boolean selected) {
+ public void onItemStateChanged(@NonNull K key, boolean selected) {
int position = mKeyProvider.getPosition(key);
if (VERBOSE) Log.v(TAG, "ITEM " + key + " CHANGED at pos: " + position);
@@ -131,7 +136,7 @@ public class EventBridge {
return;
}
- mAdapter.notifyItemChanged(position, SelectionHelper.SELECTION_CHANGED_MARKER);
+ mAdapter.notifyItemChanged(position, SelectionTracker.SELECTION_CHANGED_MARKER);
}
}
}
diff --git a/androidx/recyclerview/selection/FocusCallbacks.java b/androidx/widget/recyclerview/selection/FocusDelegate.java
index 4c1c12ef..1ecfcc4d 100644
--- a/androidx/recyclerview/selection/FocusCallbacks.java
+++ b/androidx/widget/recyclerview/selection/FocusDelegate.java
@@ -14,30 +14,25 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.support.annotation.RestrictTo;
+import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
-import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.widget.recyclerview.selection.ItemDetailsLookup.ItemDetails;
/**
- * Override methods in this class to connect specialized behaviors of the selection
- * code to the application environment.
- *
- * @param <K> Selection key type. Usually String or Long.
+ * Override methods in this class to provide application specific behaviors
+ * related to focusing item.
*
- * @hide
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/
-@RestrictTo(LIBRARY_GROUP)
-public abstract class FocusCallbacks<K> {
+public abstract class FocusDelegate<K> {
- static final <K> FocusCallbacks<K> dummy() {
- return new FocusCallbacks<K>() {
+ static final <K> FocusDelegate<K> dummy() {
+ return new FocusDelegate<K>() {
@Override
- public void focusItem(ItemDetails<K> item) {
+ public void focusItem(@NonNull ItemDetails<K> item) {
}
@Override
@@ -59,7 +54,7 @@ public abstract class FocusCallbacks<K> {
/**
* If environment supports focus, focus {@code item}.
*/
- public abstract void focusItem(ItemDetails<K> item);
+ public abstract void focusItem(@NonNull ItemDetails<K> item);
/**
* @return true if there is a focused item.
diff --git a/androidx/recyclerview/selection/GestureRouter.java b/androidx/widget/recyclerview/selection/GestureRouter.java
index 82fab878..46f0725c 100644
--- a/androidx/recyclerview/selection/GestureRouter.java
+++ b/androidx/widget/recyclerview/selection/GestureRouter.java
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.v4.util.Preconditions.checkArgument;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.GestureDetector.OnDoubleTapListener;
import android.view.GestureDetector.OnGestureListener;
@@ -36,7 +37,7 @@ final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener>
private final ToolHandlerRegistry<T> mDelegates;
- GestureRouter(T defaultDelegate) {
+ GestureRouter(@NonNull T defaultDelegate) {
checkArgument(defaultDelegate != null);
mDelegates = new ToolHandlerRegistry<>(defaultDelegate);
}
@@ -54,47 +55,49 @@ final class GestureRouter<T extends OnGestureListener & OnDoubleTapListener>
}
@Override
- public boolean onSingleTapConfirmed(MotionEvent e) {
+ public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
return mDelegates.get(e).onSingleTapConfirmed(e);
}
@Override
- public boolean onDoubleTap(MotionEvent e) {
+ public boolean onDoubleTap(@NonNull MotionEvent e) {
return mDelegates.get(e).onDoubleTap(e);
}
@Override
- public boolean onDoubleTapEvent(MotionEvent e) {
+ public boolean onDoubleTapEvent(@NonNull MotionEvent e) {
return mDelegates.get(e).onDoubleTapEvent(e);
}
@Override
- public boolean onDown(MotionEvent e) {
+ public boolean onDown(@NonNull MotionEvent e) {
return mDelegates.get(e).onDown(e);
}
@Override
- public void onShowPress(MotionEvent e) {
+ public void onShowPress(@NonNull MotionEvent e) {
mDelegates.get(e).onShowPress(e);
}
@Override
- public boolean onSingleTapUp(MotionEvent e) {
+ public boolean onSingleTapUp(@NonNull MotionEvent e) {
return mDelegates.get(e).onSingleTapUp(e);
}
@Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2,
+ float distanceX, float distanceY) {
return mDelegates.get(e2).onScroll(e1, e2, distanceX, distanceY);
}
@Override
- public void onLongPress(MotionEvent e) {
+ public void onLongPress(@NonNull MotionEvent e) {
mDelegates.get(e).onLongPress(e);
}
@Override
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+ public boolean onFling(@NonNull MotionEvent e1, @NonNull MotionEvent e2,
+ float velocityX, float velocityY) {
return mDelegates.get(e2).onFling(e1, e2, velocityX, velocityY);
}
}
diff --git a/androidx/recyclerview/selection/GestureSelectionHelper.java b/androidx/widget/recyclerview/selection/GestureSelectionHelper.java
index 2a28fc5d..431cc67b 100644
--- a/androidx/recyclerview/selection/GestureSelectionHelper.java
+++ b/androidx/widget/recyclerview/selection/GestureSelectionHelper.java
@@ -14,12 +14,13 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.v4.util.Preconditions.checkArgument;
import static android.support.v4.util.Preconditions.checkState;
import android.graphics.Point;
+import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
@@ -38,31 +39,31 @@ final class GestureSelectionHelper implements OnItemTouchListener {
private static final String TAG = "GestureSelectionHelper";
- private final SelectionHelper<?> mSelectionMgr;
+ private final SelectionTracker<?> mSelectionMgr;
private final AutoScroller mScroller;
private final ViewDelegate mView;
- private final ContentLock mLock;
+ private final OperationMonitor mLock;
private int mLastStartedItemPos = -1;
private boolean mStarted = false;
private Point mLastInterceptedPoint;
/**
- * See {@link #create(SelectionHelper, RecyclerView, AutoScroller, ContentLock)} for convenience
+ * See {@link GestureSelectionHelper#create} for convenience
* method.
*/
GestureSelectionHelper(
- SelectionHelper<?> selectionHelper,
- ViewDelegate view,
- AutoScroller scroller,
- ContentLock lock) {
+ @NonNull SelectionTracker<?> selectionTracker,
+ @NonNull ViewDelegate view,
+ @NonNull AutoScroller scroller,
+ @NonNull OperationMonitor lock) {
- checkArgument(selectionHelper != null);
+ checkArgument(selectionTracker != null);
checkArgument(view != null);
checkArgument(scroller != null);
checkArgument(lock != null);
- mSelectionMgr = selectionHelper;
+ mSelectionMgr = selectionTracker;
mView = view;
mScroller = scroller;
mLock = lock;
@@ -82,15 +83,15 @@ final class GestureSelectionHelper implements OnItemTouchListener {
// to make the implicit coupling less of a time bomb.
checkState(mSelectionMgr.isRangeActive());
- mLock.checkUnlocked();
+ mLock.checkStopped();
mStarted = true;
- mLock.block();
+ mLock.start();
}
@Override
/** @hide */
- public boolean onInterceptTouchEvent(RecyclerView unused, MotionEvent e) {
+ public boolean onInterceptTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
if (MotionEvents.isMouseEvent(e)) {
if (Shared.DEBUG) Log.w(TAG, "Unexpected Mouse event. Check configuration.");
}
@@ -110,7 +111,7 @@ final class GestureSelectionHelper implements OnItemTouchListener {
@Override
/** @hide */
- public void onTouchEvent(RecyclerView unused, MotionEvent e) {
+ public void onTouchEvent(@NonNull RecyclerView unused, @NonNull MotionEvent e) {
checkState(mStarted);
switch (e.getActionMasked()) {
@@ -133,7 +134,7 @@ final class GestureSelectionHelper implements OnItemTouchListener {
// Called when an ACTION_DOWN event is intercepted.
// If down event happens on an item, we mark that item's position as last started.
- private boolean handleInterceptedDownEvent(MotionEvent e) {
+ private boolean handleInterceptedDownEvent(@NonNull MotionEvent e) {
mLastStartedItemPos = mView.getItemUnder(e);
return mLastStartedItemPos != RecyclerView.NO_POSITION;
}
@@ -141,7 +142,7 @@ final class GestureSelectionHelper implements OnItemTouchListener {
// Called when ACTION_UP event is to be handled.
// Essentially, since this means all gesture movement is over, reset everything and apply
// provisional selection.
- private void handleUpEvent(MotionEvent e) {
+ private void handleUpEvent(@NonNull MotionEvent e) {
mSelectionMgr.mergeProvisionalSelection();
endSelection();
if (mLastStartedItemPos > -1) {
@@ -152,7 +153,7 @@ final class GestureSelectionHelper implements OnItemTouchListener {
// Called when ACTION_CANCEL event is to be handled.
// This means this gesture selection is aborted, so reset everything and abandon provisional
// selection.
- private void handleCancelEvent(MotionEvent unused) {
+ private void handleCancelEvent(@NonNull MotionEvent unused) {
mSelectionMgr.clearProvisionalSelection();
endSelection();
}
@@ -163,12 +164,12 @@ final class GestureSelectionHelper implements OnItemTouchListener {
mLastStartedItemPos = -1;
mStarted = false;
mScroller.reset();
- mLock.unblock();
+ mLock.stop();
}
// Call when an intercepted ACTION_MOVE event is passed down.
// At this point, we are sure user wants to gesture multi-select.
- private void handleMoveEvent(MotionEvent e) {
+ private void handleMoveEvent(@NonNull MotionEvent e) {
mLastInterceptedPoint = MotionEvents.getOrigin(e);
int lastGlidedItemPos = mView.getLastGlidedItemPosition(e);
@@ -202,14 +203,14 @@ final class GestureSelectionHelper implements OnItemTouchListener {
* Returns a new instance of GestureSelectionHelper.
*/
static GestureSelectionHelper create(
- SelectionHelper selectionMgr,
- RecyclerView recView,
- AutoScroller scroller,
- ContentLock lock) {
+ @NonNull SelectionTracker selectionMgr,
+ @NonNull RecyclerView recyclerView,
+ @NonNull AutoScroller scroller,
+ @NonNull OperationMonitor lock) {
return new GestureSelectionHelper(
selectionMgr,
- new RecyclerViewDelegate(recView),
+ new RecyclerViewDelegate(recyclerView),
scroller,
lock);
}
@@ -218,41 +219,41 @@ final class GestureSelectionHelper implements OnItemTouchListener {
abstract static class ViewDelegate {
abstract int getHeight();
- abstract int getItemUnder(MotionEvent e);
+ abstract int getItemUnder(@NonNull MotionEvent e);
- abstract int getLastGlidedItemPosition(MotionEvent e);
+ abstract int getLastGlidedItemPosition(@NonNull MotionEvent e);
}
@VisibleForTesting
static final class RecyclerViewDelegate extends ViewDelegate {
- private final RecyclerView mRecView;
+ private final RecyclerView mRecyclerView;
- RecyclerViewDelegate(RecyclerView view) {
- checkArgument(view != null);
- mRecView = view;
+ RecyclerViewDelegate(@NonNull RecyclerView recyclerView) {
+ checkArgument(recyclerView != null);
+ mRecyclerView = recyclerView;
}
@Override
int getHeight() {
- return mRecView.getHeight();
+ return mRecyclerView.getHeight();
}
@Override
- int getItemUnder(MotionEvent e) {
- View child = mRecView.findChildViewUnder(e.getX(), e.getY());
+ int getItemUnder(@NonNull MotionEvent e) {
+ View child = mRecyclerView.findChildViewUnder(e.getX(), e.getY());
return child != null
- ? mRecView.getChildAdapterPosition(child)
+ ? mRecyclerView.getChildAdapterPosition(child)
: RecyclerView.NO_POSITION;
}
@Override
- int getLastGlidedItemPosition(MotionEvent e) {
+ int getLastGlidedItemPosition(@NonNull MotionEvent e) {
// If user has moved his pointer to the bottom-right empty pane (ie. to the right of the
// last item of the recycler view), we would want to set that as the currentItemPos
- View lastItem = mRecView.getLayoutManager()
- .getChildAt(mRecView.getLayoutManager().getChildCount() - 1);
- int direction = ViewCompat.getLayoutDirection(mRecView);
+ View lastItem = mRecyclerView.getLayoutManager()
+ .getChildAt(mRecyclerView.getLayoutManager().getChildCount() - 1);
+ int direction = ViewCompat.getLayoutDirection(mRecyclerView);
final boolean pastLastItem = isPastLastItem(lastItem.getTop(),
lastItem.getLeft(),
lastItem.getRight(),
@@ -264,10 +265,10 @@ final class GestureSelectionHelper implements OnItemTouchListener {
// number
// of items in the adapter. Using the adapter is the for sure way to get the actual last
// item position.
- final float inboundY = getInboundY(mRecView.getHeight(), e.getY());
- return (pastLastItem) ? mRecView.getAdapter().getItemCount() - 1
- : mRecView.getChildAdapterPosition(
- mRecView.findChildViewUnder(e.getX(), inboundY));
+ final float inboundY = getInboundY(mRecyclerView.getHeight(), e.getY());
+ return (pastLastItem) ? mRecyclerView.getAdapter().getItemCount() - 1
+ : mRecyclerView.getChildAdapterPosition(
+ mRecyclerView.findChildViewUnder(e.getX(), inboundY));
}
/*
@@ -276,7 +277,8 @@ final class GestureSelectionHelper implements OnItemTouchListener {
* For RTL, it would to be to the left or to the bottom of the item.
*/
@VisibleForTesting
- static boolean isPastLastItem(int top, int left, int right, MotionEvent e, int direction) {
+ static boolean isPastLastItem(
+ int top, int left, int right, @NonNull MotionEvent e, int direction) {
if (direction == View.LAYOUT_DIRECTION_LTR) {
return e.getX() > right && e.getY() > top;
} else {
diff --git a/androidx/recyclerview/selection/GridModel.java b/androidx/widget/recyclerview/selection/GridModel.java
index 43589581..df451aee 100644
--- a/androidx/recyclerview/selection/GridModel.java
+++ b/androidx/widget/recyclerview/selection/GridModel.java
@@ -14,12 +14,14 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.v4.util.Preconditions.checkArgument;
import android.graphics.Point;
import android.graphics.Rect;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.OnScrollListener;
@@ -34,14 +36,14 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
-import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+import androidx.widget.recyclerview.selection.SelectionTracker.SelectionPredicate;
/**
* Provides a band selection item model for views within a RecyclerView. This class queries the
* RecyclerView to determine where its items are placed; then, once band selection is underway,
* it alerts listeners of which items are covered by the selections.
*
- * @param <K> Selection key type. Usually String or Long.
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/
final class GridModel<K> {
@@ -601,12 +603,15 @@ final class GridModel<K> {
final RelativeCoordinate mX;
final RelativeCoordinate mY;
- RelativePoint(List<Limits> columnLimits, List<Limits> rowLimits, Point point) {
+ RelativePoint(
+ @NonNull List<Limits> columnLimits,
+ @NonNull List<Limits> rowLimits, Point point) {
+
this.mX = new RelativeCoordinate(columnLimits, point.x);
this.mY = new RelativeCoordinate(rowLimits, point.y);
}
- RelativePoint(RelativeCoordinate x, RelativeCoordinate y) {
+ RelativePoint(@NonNull RelativeCoordinate x, @NonNull RelativeCoordinate y) {
this.mX = x;
this.mY = y;
}
@@ -617,7 +622,7 @@ final class GridModel<K> {
}
@Override
- public boolean equals(Object other) {
+ public boolean equals(@Nullable Object other) {
if (!(other instanceof RelativePoint)) {
return false;
}
@@ -674,11 +679,13 @@ final class GridModel<K> {
return cornerValue;
}
- private RelativeCoordinate min(RelativeCoordinate first, RelativeCoordinate second) {
+ private RelativeCoordinate min(
+ @NonNull RelativeCoordinate first, @NonNull RelativeCoordinate second) {
return first.compareTo(second) < 0 ? first : second;
}
- private RelativeCoordinate max(RelativeCoordinate first, RelativeCoordinate second) {
+ private RelativeCoordinate max(
+ @NonNull RelativeCoordinate first, @NonNull RelativeCoordinate second) {
return first.compareTo(second) > 0 ? first : second;
}
@@ -687,7 +694,9 @@ final class GridModel<K> {
* coordinate.
*/
private int getCoordinateValue(
- RelativeCoordinate coordinate, List<Limits> limitsList, boolean isStartOfRange) {
+ @NonNull RelativeCoordinate coordinate,
+ @NonNull List<Limits> limitsList,
+ boolean isStartOfRange) {
switch (coordinate.type) {
case RelativeCoordinate.BEFORE_FIRST_ITEM:
@@ -708,14 +717,15 @@ final class GridModel<K> {
}
private boolean areItemsCoveredByBand(
- RelativePoint first, RelativePoint second) {
+ @NonNull RelativePoint first, @NonNull RelativePoint second) {
return doesCoordinateLocationCoverItems(first.mX, second.mX)
&& doesCoordinateLocationCoverItems(first.mY, second.mY);
}
private boolean doesCoordinateLocationCoverItems(
- RelativeCoordinate pointerCoordinate, RelativeCoordinate originCoordinate) {
+ @NonNull RelativeCoordinate pointerCoordinate,
+ @NonNull RelativeCoordinate originCoordinate) {
if (pointerCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM
&& originCoordinate.type == RelativeCoordinate.BEFORE_FIRST_ITEM) {
@@ -743,7 +753,7 @@ final class GridModel<K> {
* Provides functionality for BandController. Exists primarily to tests that are
* fully isolated from RecyclerView.
*
- * @param <K> Selection key type. Usually String or Long.
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/
abstract static class GridHost<K> extends BandSelectionHelper.BandHost<K> {
@@ -752,13 +762,13 @@ final class GridModel<K> {
*
* @param listener
*/
- abstract void removeOnScrollListener(RecyclerView.OnScrollListener listener);
+ abstract void removeOnScrollListener(@NonNull OnScrollListener listener);
/**
* @param relativePoint for which to create absolute point.
* @return absolute point.
*/
- abstract Point createAbsolutePoint(Point relativePoint);
+ abstract Point createAbsolutePoint(@NonNull Point relativePoint);
/**
* @param index index of child.
diff --git a/androidx/recyclerview/selection/ItemDetailsLookup.java b/androidx/widget/recyclerview/selection/ItemDetailsLookup.java
index da30c97a..fa20c250 100644
--- a/androidx/recyclerview/selection/ItemDetailsLookup.java
+++ b/androidx/widget/recyclerview/selection/ItemDetailsLookup.java
@@ -14,56 +14,59 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+package androidx.widget.recyclerview.selection;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
import android.support.v7.widget.RecyclerView;
import android.view.MotionEvent;
/**
- * Provides event handlers w/ access to details about documents details
- * view items Documents in the UI (RecyclerView).
- *
- * @param <K> Selection key type. Usually String or Long.
+ * Provides selection library and event handlers access to details about view items
+ * presented by a {@link RecyclerView} instance. Implementations of this class provide
+ * supplementary information about view holders used to make selection policy decisions.
*
- * @hide
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/
-@RestrictTo(LIBRARY_GROUP)
public abstract class ItemDetailsLookup<K> {
- /** @return true if there is an item under the finger/cursor. */
- public boolean overItem(MotionEvent e) {
+ /**
+ * @return true if there is an item at the event coordinates.
+ */
+ public boolean overItem(@NonNull MotionEvent e) {
return getItemPosition(e) != RecyclerView.NO_POSITION;
}
- /** @return true if there is an item w/ a stable ID under the finger/cursor. */
- public boolean overItemWithSelectionKey(MotionEvent e) {
+ /**
+ * @return true if there is an item w/ a stable ID at the event coordinates.
+ */
+ public boolean overItemWithSelectionKey(@NonNull MotionEvent e) {
return overItem(e) && hasSelectionKey(getItemDetails(e));
}
/**
- * @return true if the event is over an area that can be dragged via touch
- * or via mouse. List items have a white area that is not draggable.
+ * @return true if the event coordinates are in an area of the item
+ * that can result in dragging the item. List items frequently have a white
+ * area that is not draggable allowing band selection to be initiated
+ * in that area.
*/
- public boolean inItemDragRegion(MotionEvent e) {
+ public boolean inItemDragRegion(@NonNull MotionEvent e) {
return overItem(e) && getItemDetails(e).inDragRegion(e);
}
/**
- * @return true if the event is in the "selection hot spot" region.
- * The hot spot region instantly selects in touch mode, vs launches.
+ * @return true if the event coordinates are in a "selection hot spot"
+ * region of an item. Contact in these regions result in immediate
+ * selection, even when there is no existing selection.
*/
- public boolean inItemSelectRegion(MotionEvent e) {
+ public boolean inItemSelectRegion(@NonNull MotionEvent e) {
return overItem(e) && getItemDetails(e).inSelectionHotspot(e);
}
/**
- * @return the adapter position of the item under the finger/cursor.
+ * @return the adapter position of the item at the event coordinates.
*/
- public int getItemPosition(MotionEvent e) {
+ public int getItemPosition(@NonNull MotionEvent e) {
@Nullable ItemDetails<?> item = getItemDetails(e);
return item != null
? item.getPosition()
@@ -79,17 +82,17 @@ public abstract class ItemDetailsLookup<K> {
}
/**
- * @return the DocumentDetails for the item under the event, or null.
+ * @return the ItemDetails for the item under the event, or null.
*/
- public abstract @Nullable ItemDetails<K> getItemDetails(MotionEvent e);
+ public abstract @Nullable ItemDetails<K> getItemDetails(@NonNull MotionEvent e);
/**
- * Abstract class providing helper classes with access to information about
- * RecyclerView item associated with a MotionEvent.
+ * Class providing access to information about a RecyclerView item.
+ * Information provided by this class is used by the selection library to
+ * implement various aspects of selection policy.
*
- * @param <K> Selection key type. Usually String or Long.
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/
- // TODO: Can this be merged with ViewHolder?
public abstract static class ItemDetails<K> {
/** @return the position of an item. */
@@ -109,7 +112,7 @@ public abstract class ItemDetailsLookup<K> {
* is useful for checkboxes and other UI affordances focused on enabling
* selection.
*/
- public boolean inSelectionHotspot(MotionEvent e) {
+ public boolean inSelectionHotspot(@NonNull MotionEvent e) {
return false;
}
@@ -118,19 +121,17 @@ public abstract class ItemDetailsLookup<K> {
* of the drag region. This allows the client to implement custom handling
* for events related to drag and drop.
*/
- public boolean inDragRegion(MotionEvent e) {
+ public boolean inDragRegion(@NonNull MotionEvent e) {
return false;
}
@Override
- public boolean equals(Object obj) {
- if (obj instanceof ItemDetails) {
- return isEqualTo((ItemDetails) obj);
- }
- return false;
+ public boolean equals(@Nullable Object obj) {
+ return (obj instanceof ItemDetails)
+ && isEqualTo((ItemDetails) obj);
}
- private boolean isEqualTo(ItemDetails other) {
+ private boolean isEqualTo(@NonNull ItemDetails other) {
K key = getSelectionKey();
boolean sameKeys = false;
if (key == null) {
diff --git a/androidx/recyclerview/selection/ItemKeyProvider.java b/androidx/widget/recyclerview/selection/ItemKeyProvider.java
index 134c4420..4422ee71 100644
--- a/androidx/recyclerview/selection/ItemKeyProvider.java
+++ b/androidx/widget/recyclerview/selection/ItemKeyProvider.java
@@ -14,29 +14,23 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
import static android.support.v4.util.Preconditions.checkArgument;
import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
- * Provides support for sting based stable ids in the RecyclerView selection helper.
- * Client code can use this to look up stable ids when working with selection
- * in application code.
+ * Provides selection library access to stable selection keys identifying items
+ * presented by a {@link android.support.v7.widget.RecyclerView RecyclerView} instance.
*
- * @param <K> Selection key type. Usually String or Long.
- *
- * @hide
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/
-@RestrictTo(LIBRARY_GROUP)
public abstract class ItemKeyProvider<K> {
/**
@@ -44,16 +38,14 @@ public abstract class ItemKeyProvider<K> {
* Key providers with this access type enjoy support for enhanced features like:
* SHIFT+click range selection, and band selection.
*/
- @VisibleForTesting // otherwise protected would do nicely.
public static final int SCOPE_MAPPED = 0;
/**
- * Provides access cached data based on what was recently bound in the view.
+ * Provides access to cached data based for items that were recently bound in the view.
* Employing this provider will result in a reduced feature-set, as some
- * featuers like SHIFT+click range selection and band selection are dependent
+ * features like SHIFT+click range selection and band selection are dependent
* on mapped access.
*/
- @VisibleForTesting // otherwise protected would do nicely.
public static final int SCOPE_CACHED = 1;
@IntDef({
@@ -61,14 +53,14 @@ public abstract class ItemKeyProvider<K> {
SCOPE_CACHED
})
@Retention(RetentionPolicy.SOURCE)
- protected @interface Scope {}
+ public @interface Scope {}
private final @Scope int mScope;
/**
* Creates a new provider with the given scope.
- * @param scope Scope can't change at runtime (at least code won't adapt)
- * so it must be specified in the constructor.
+ *
+ * @param scope Scope can't be changed at runtime.
*/
protected ItemKeyProvider(@Scope int scope) {
checkArgument(scope == SCOPE_MAPPED || scope == SCOPE_CACHED);
@@ -81,12 +73,12 @@ public abstract class ItemKeyProvider<K> {
}
/**
- * @return The selection key of the item at the given adapter position.
+ * @return The selection key at the given adapter position, or null.
*/
public abstract @Nullable K getKey(int position);
/**
- * @return the position of a stable ID, or RecyclerView.NO_POSITION.
+ * @return the position corresponding to the selection key, or RecyclerView.NO_POSITION.
*/
- public abstract int getPosition(K key);
+ public abstract int getPosition(@NonNull K key);
}
diff --git a/androidx/recyclerview/selection/MotionEvents.java b/androidx/widget/recyclerview/selection/MotionEvents.java
index dd9e54f2..0e503416 100644
--- a/androidx/recyclerview/selection/MotionEvents.java
+++ b/androidx/widget/recyclerview/selection/MotionEvents.java
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import android.graphics.Point;
+import android.support.annotation.NonNull;
import android.view.KeyEvent;
import android.view.MotionEvent;
@@ -27,56 +28,56 @@ final class MotionEvents {
private MotionEvents() {}
- static boolean isMouseEvent(MotionEvent e) {
+ static boolean isMouseEvent(@NonNull MotionEvent e) {
return e.getToolType(0) == MotionEvent.TOOL_TYPE_MOUSE;
}
- static boolean isTouchEvent(MotionEvent e) {
+ static boolean isTouchEvent(@NonNull MotionEvent e) {
return e.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER;
}
- static boolean isActionMove(MotionEvent e) {
+ static boolean isActionMove(@NonNull MotionEvent e) {
return e.getActionMasked() == MotionEvent.ACTION_MOVE;
}
- static boolean isActionDown(MotionEvent e) {
+ static boolean isActionDown(@NonNull MotionEvent e) {
return e.getActionMasked() == MotionEvent.ACTION_DOWN;
}
- static boolean isActionUp(MotionEvent e) {
+ static boolean isActionUp(@NonNull MotionEvent e) {
return e.getActionMasked() == MotionEvent.ACTION_UP;
}
- static boolean isActionPointerUp(MotionEvent e) {
+ static boolean isActionPointerUp(@NonNull MotionEvent e) {
return e.getActionMasked() == MotionEvent.ACTION_POINTER_UP;
}
@SuppressWarnings("unused")
- static boolean isActionPointerDown(MotionEvent e) {
+ static boolean isActionPointerDown(@NonNull MotionEvent e) {
return e.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN;
}
- static boolean isActionCancel(MotionEvent e) {
+ static boolean isActionCancel(@NonNull MotionEvent e) {
return e.getActionMasked() == MotionEvent.ACTION_CANCEL;
}
- static Point getOrigin(MotionEvent e) {
+ static Point getOrigin(@NonNull MotionEvent e) {
return new Point((int) e.getX(), (int) e.getY());
}
- static boolean isPrimaryButtonPressed(MotionEvent e) {
+ static boolean isPrimaryMouseButtonPressed(@NonNull MotionEvent e) {
return isButtonPressed(e, MotionEvent.BUTTON_PRIMARY);
}
- static boolean isSecondaryButtonPressed(MotionEvent e) {
+ static boolean isSecondaryMouseButtonPressed(@NonNull MotionEvent e) {
return isButtonPressed(e, MotionEvent.BUTTON_SECONDARY);
}
- static boolean isTertiaryButtonPressed(MotionEvent e) {
+ static boolean isTertiaryMouseButtonPressed(@NonNull MotionEvent e) {
return isButtonPressed(e, MotionEvent.BUTTON_TERTIARY);
}
- // TODO: Replace with MotionEvent.isButtonPressed once targeting 21 or higher.
+ // NOTE: Can replace this with MotionEvent.isButtonPressed once targeting 21 or higher.
private static boolean isButtonPressed(MotionEvent e, int button) {
if (button == 0) {
return false;
@@ -84,19 +85,19 @@ final class MotionEvents {
return (e.getButtonState() & button) == button;
}
- static boolean isShiftKeyPressed(MotionEvent e) {
+ static boolean isShiftKeyPressed(@NonNull MotionEvent e) {
return hasBit(e.getMetaState(), KeyEvent.META_SHIFT_ON);
}
- static boolean isCtrlKeyPressed(MotionEvent e) {
+ static boolean isCtrlKeyPressed(@NonNull MotionEvent e) {
return hasBit(e.getMetaState(), KeyEvent.META_CTRL_ON);
}
- static boolean isAltKeyPressed(MotionEvent e) {
+ static boolean isAltKeyPressed(@NonNull MotionEvent e) {
return hasBit(e.getMetaState(), KeyEvent.META_ALT_ON);
}
- static boolean isTouchpadScroll(MotionEvent e) {
+ static boolean isTouchpadScroll(@NonNull MotionEvent e) {
// Touchpad inputs are treated as mouse inputs, and when scrolling, there are no buttons
// returned.
return isMouseEvent(e) && isActionMove(e) && e.getButtonState() == 0;
diff --git a/androidx/recyclerview/selection/MotionInputHandler.java b/androidx/widget/recyclerview/selection/MotionInputHandler.java
index 1c063028..1229c253 100644
--- a/androidx/recyclerview/selection/MotionInputHandler.java
+++ b/androidx/widget/recyclerview/selection/MotionInputHandler.java
@@ -14,91 +14,92 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.v4.util.Preconditions.checkArgument;
import static android.support.v4.util.Preconditions.checkState;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.GestureDetector.SimpleOnGestureListener;
import android.view.MotionEvent;
-import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.widget.recyclerview.selection.ItemDetailsLookup.ItemDetails;
/**
* Base class for handlers that can be registered w/ {@link GestureRouter}.
*/
abstract class MotionInputHandler<K> extends SimpleOnGestureListener {
- protected final SelectionHelper<K> mSelectionHelper;
+ protected final SelectionTracker<K> mSelectionTracker;
private final ItemKeyProvider<K> mKeyProvider;
- private final FocusCallbacks<K> mFocusCallbacks;
+ private final FocusDelegate<K> mFocusDelegate;
MotionInputHandler(
- SelectionHelper<K> selectionHelper,
- ItemKeyProvider<K> keyProvider,
- FocusCallbacks<K> focusCallbacks) {
+ @NonNull SelectionTracker<K> selectionTracker,
+ @NonNull ItemKeyProvider<K> keyProvider,
+ @NonNull FocusDelegate<K> focusDelegate) {
- checkArgument(selectionHelper != null);
+ checkArgument(selectionTracker != null);
checkArgument(keyProvider != null);
- checkArgument(focusCallbacks != null);
+ checkArgument(focusDelegate != null);
- mSelectionHelper = selectionHelper;
+ mSelectionTracker = selectionTracker;
mKeyProvider = keyProvider;
- mFocusCallbacks = focusCallbacks;
+ mFocusDelegate = focusDelegate;
}
- final boolean selectItem(ItemDetails<K> details) {
+ final boolean selectItem(@NonNull ItemDetails<K> details) {
checkArgument(details != null);
checkArgument(hasPosition(details));
checkArgument(hasSelectionKey(details));
- if (mSelectionHelper.select(details.getSelectionKey())) {
- mSelectionHelper.anchorRange(details.getPosition());
+ if (mSelectionTracker.select(details.getSelectionKey())) {
+ mSelectionTracker.anchorRange(details.getPosition());
}
// we set the focus on this doc so it will be the origin for keyboard events or shift+clicks
// if there is only a single item selected, otherwise clear focus
- if (mSelectionHelper.getSelection().size() == 1) {
- mFocusCallbacks.focusItem(details);
+ if (mSelectionTracker.getSelection().size() == 1) {
+ mFocusDelegate.focusItem(details);
} else {
- mFocusCallbacks.clearFocus();
+ mFocusDelegate.clearFocus();
}
return true;
}
- protected final boolean focusItem(ItemDetails<K> details) {
+ protected final boolean focusItem(@NonNull ItemDetails<K> details) {
checkArgument(details != null);
checkArgument(hasSelectionKey(details));
- mSelectionHelper.clearSelection();
- mFocusCallbacks.focusItem(details);
+ mSelectionTracker.clearSelection();
+ mFocusDelegate.focusItem(details);
return true;
}
- protected final void extendSelectionRange(ItemDetails<K> details) {
+ protected final void extendSelectionRange(@NonNull ItemDetails<K> details) {
checkState(mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED));
checkArgument(hasPosition(details));
checkArgument(hasSelectionKey(details));
- mSelectionHelper.extendRange(details.getPosition());
- mFocusCallbacks.focusItem(details);
+ mSelectionTracker.extendRange(details.getPosition());
+ mFocusDelegate.focusItem(details);
}
- final boolean isRangeExtension(MotionEvent e) {
+ final boolean isRangeExtension(@NonNull MotionEvent e) {
return MotionEvents.isShiftKeyPressed(e)
- && mSelectionHelper.isRangeActive()
+ && mSelectionTracker.isRangeActive()
// Without full corpus access we can't reliably implement range
// as a user can scroll *anywhere* then SHIFT+click.
&& mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED);
}
- boolean shouldClearSelection(MotionEvent e, ItemDetails<K> item) {
+ boolean shouldClearSelection(@NonNull MotionEvent e, @NonNull ItemDetails<K> item) {
return !MotionEvents.isCtrlKeyPressed(e)
&& !item.inSelectionHotspot(e)
- && !mSelectionHelper.isSelected(item.getSelectionKey());
+ && !mSelectionTracker.isSelected(item.getSelectionKey());
}
static boolean hasSelectionKey(@Nullable ItemDetails<?> item) {
diff --git a/androidx/recyclerview/selection/MouseInputHandler.java b/androidx/widget/recyclerview/selection/MouseInputHandler.java
index b6fe36b0..6acf04fe 100644
--- a/androidx/recyclerview/selection/MouseInputHandler.java
+++ b/androidx/widget/recyclerview/selection/MouseInputHandler.java
@@ -14,34 +14,35 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.v4.util.Preconditions.checkArgument;
import static android.support.v4.util.Preconditions.checkState;
-import static androidx.recyclerview.selection.Shared.DEBUG;
-import static androidx.recyclerview.selection.Shared.VERBOSE;
+import static androidx.widget.recyclerview.selection.Shared.DEBUG;
+import static androidx.widget.recyclerview.selection.Shared.VERBOSE;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.MotionEvent;
-import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.widget.recyclerview.selection.ItemDetailsLookup.ItemDetails;
/**
* 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 driven selection support.
+ * to implement the primary policies around mouse input.
*/
final class MouseInputHandler<K> extends MotionInputHandler<K> {
private static final String TAG = "MouseInputDelegate";
private final ItemDetailsLookup<K> mDetailsLookup;
- private final MouseCallbacks mMouseCallbacks;
- private final ActivationCallbacks<K> mActivationCallbacks;
- private final FocusCallbacks<K> mFocusCallbacks;
+ private final OnContextClickListener mOnContextClickListener;
+ private final OnItemActivatedListener<K> mOnItemActivatedListener;
+ private final FocusDelegate<K> mFocusDelegate;
// The event has been handled in onSingleTapUp
private boolean mHandledTapUp;
@@ -49,30 +50,30 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
private boolean mHandledOnDown;
MouseInputHandler(
- SelectionHelper<K> selectionHelper,
- ItemKeyProvider<K> keyProvider,
- ItemDetailsLookup<K> detailsLookup,
- MouseCallbacks mouseCallbacks,
- ActivationCallbacks<K> activationCallbacks,
- FocusCallbacks<K> focusCallbacks) {
+ @NonNull SelectionTracker<K> selectionTracker,
+ @NonNull ItemKeyProvider<K> keyProvider,
+ @NonNull ItemDetailsLookup<K> detailsLookup,
+ @NonNull OnContextClickListener onContextClickListener,
+ @NonNull OnItemActivatedListener<K> onItemActivatedListener,
+ @NonNull FocusDelegate<K> focusDelegate) {
- super(selectionHelper, keyProvider, focusCallbacks);
+ super(selectionTracker, keyProvider, focusDelegate);
checkArgument(detailsLookup != null);
- checkArgument(mouseCallbacks != null);
- checkArgument(activationCallbacks != null);
+ checkArgument(onContextClickListener != null);
+ checkArgument(onItemActivatedListener != null);
mDetailsLookup = detailsLookup;
- mMouseCallbacks = mouseCallbacks;
- mActivationCallbacks = activationCallbacks;
- mFocusCallbacks = focusCallbacks;
+ mOnContextClickListener = onContextClickListener;
+ mOnItemActivatedListener = onItemActivatedListener;
+ mFocusDelegate = focusDelegate;
}
@Override
- public boolean onDown(MotionEvent e) {
+ public boolean onDown(@NonNull MotionEvent e) {
if (VERBOSE) Log.v(TAG, "Delegated onDown event.");
- if ((MotionEvents.isAltKeyPressed(e) && MotionEvents.isPrimaryButtonPressed(e))
- || MotionEvents.isSecondaryButtonPressed(e)) {
+ if ((MotionEvents.isAltKeyPressed(e) && MotionEvents.isPrimaryMouseButtonPressed(e))
+ || MotionEvents.isSecondaryMouseButtonPressed(e)) {
mHandledOnDown = true;
return onRightClick(e);
}
@@ -81,14 +82,15 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
}
@Override
- public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+ public boolean onScroll(@NonNull MotionEvent e1, @NonNull MotionEvent e2,
+ float distanceX, float distanceY) {
// Don't scroll content window in response to mouse drag
// If it's two-finger trackpad scrolling, we want to scroll
return !MotionEvents.isTouchpadScroll(e2);
}
@Override
- public boolean onSingleTapUp(MotionEvent e) {
+ public boolean onSingleTapUp(@NonNull MotionEvent e) {
// See b/27377794. Since we don't get a button state back from UP events, we have to
// explicitly save this state to know whether something was previously handled by
// DOWN events or not.
@@ -100,17 +102,17 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
if (!mDetailsLookup.overItemWithSelectionKey(e)) {
if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
- mSelectionHelper.clearSelection();
- mFocusCallbacks.clearFocus();
+ mSelectionTracker.clearSelection();
+ mFocusDelegate.clearFocus();
return false;
}
- if (MotionEvents.isTertiaryButtonPressed(e)) {
+ if (MotionEvents.isTertiaryMouseButtonPressed(e)) {
if (DEBUG) Log.d(TAG, "Ignoring middle click");
return false;
}
- if (mSelectionHelper.hasSelection()) {
+ if (mSelectionTracker.hasSelection()) {
onItemClick(e, mDetailsLookup.getItemDetails(e));
mHandledTapUp = true;
return true;
@@ -121,19 +123,19 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
// tap on an item when there is an existing selection. We could extend
// a selection, we could clear selection (then launch)
- private void onItemClick(MotionEvent e, ItemDetails<K> item) {
- checkState(mSelectionHelper.hasSelection());
+ private void onItemClick(@NonNull MotionEvent e, @NonNull ItemDetails<K> item) {
+ checkState(mSelectionTracker.hasSelection());
checkArgument(item != null);
if (isRangeExtension(e)) {
extendSelectionRange(item);
} else {
if (shouldClearSelection(e, item)) {
- mSelectionHelper.clearSelection();
+ mSelectionTracker.clearSelection();
}
- if (mSelectionHelper.isSelected(item.getSelectionKey())) {
- if (mSelectionHelper.deselect(item.getSelectionKey())) {
- mFocusCallbacks.clearFocus();
+ if (mSelectionTracker.isSelected(item.getSelectionKey())) {
+ if (mSelectionTracker.deselect(item.getSelectionKey())) {
+ mFocusDelegate.clearFocus();
}
} else {
selectOrFocusItem(item, e);
@@ -142,7 +144,7 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
}
@Override
- public boolean onSingleTapConfirmed(MotionEvent e) {
+ public boolean onSingleTapConfirmed(@NonNull MotionEvent e) {
if (mHandledTapUp) {
if (VERBOSE) {
Log.v(TAG,
@@ -152,7 +154,7 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
return false;
}
- if (mSelectionHelper.hasSelection()) {
+ if (mSelectionTracker.hasSelection()) {
return false; // should have been handled by onSingleTapUp.
}
@@ -161,7 +163,7 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
return false;
}
- if (MotionEvents.isTertiaryButtonPressed(e)) {
+ if (MotionEvents.isTertiaryMouseButtonPressed(e)) {
if (DEBUG) Log.d(TAG, "Ignoring middle click");
return false;
}
@@ -171,9 +173,9 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
return false;
}
- if (mFocusCallbacks.hasFocusedItem() && MotionEvents.isShiftKeyPressed(e)) {
- mSelectionHelper.startRange(mFocusCallbacks.getFocusedPosition());
- mSelectionHelper.extendRange(item.getPosition());
+ if (mFocusDelegate.hasFocusedItem() && MotionEvents.isShiftKeyPressed(e)) {
+ mSelectionTracker.startRange(mFocusDelegate.getFocusedPosition());
+ mSelectionTracker.extendRange(item.getPosition());
} else {
selectOrFocusItem(item, e);
}
@@ -181,7 +183,7 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
}
@Override
- public boolean onDoubleTap(MotionEvent e) {
+ public boolean onDoubleTap(@NonNull MotionEvent e) {
mHandledTapUp = false;
if (!mDetailsLookup.overItemWithSelectionKey(e)) {
@@ -189,20 +191,20 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
return false;
}
- if (MotionEvents.isTertiaryButtonPressed(e)) {
+ if (MotionEvents.isTertiaryMouseButtonPressed(e)) {
if (DEBUG) Log.d(TAG, "Ignoring middle click");
return false;
}
ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
- return (item != null) && mActivationCallbacks.onItemActivated(item, e);
+ return (item != null) && mOnItemActivatedListener.onItemActivated(item, e);
}
- private boolean onRightClick(MotionEvent e) {
+ private boolean onRightClick(@NonNull MotionEvent e) {
if (mDetailsLookup.overItemWithSelectionKey(e)) {
@Nullable ItemDetails<K> item = mDetailsLookup.getItemDetails(e);
- if (item != null && !mSelectionHelper.isSelected(item.getSelectionKey())) {
- mSelectionHelper.clearSelection();
+ if (item != null && !mSelectionTracker.isSelected(item.getSelectionKey())) {
+ mSelectionTracker.clearSelection();
selectItem(item);
}
}
@@ -210,10 +212,10 @@ final class MouseInputHandler<K> extends MotionInputHandler<K> {
// We always delegate final handling of the event,
// since the handler might want to show a context menu
// in an empty area or some other weirdo view.
- return mMouseCallbacks.onContextClick(e);
+ return mOnContextClickListener.onContextClick(e);
}
- private void selectOrFocusItem(ItemDetails<K> item, MotionEvent e) {
+ private void selectOrFocusItem(@NonNull ItemDetails<K> item, @NonNull MotionEvent e) {
if (item.inSelectionHotspot(e) || MotionEvents.isCtrlKeyPressed(e)) {
selectItem(item);
} else {
diff --git a/androidx/recyclerview/selection/MutableSelection.java b/androidx/widget/recyclerview/selection/MutableSelection.java
index 6e116986..a7531b0d 100644
--- a/androidx/recyclerview/selection/MutableSelection.java
+++ b/androidx/widget/recyclerview/selection/MutableSelection.java
@@ -14,36 +14,32 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.support.annotation.RestrictTo;
+import android.support.annotation.NonNull;
/**
- * Subclass of Selection exposing public support for mutating the underlying selection data.
- * This is useful for clients of {@link SelectionHelper} that wish to manipulate
- * a copy of selection data obtained via {@link SelectionHelper#copySelection(Selection)}.
- *
- * @param <K> Selection key type. Usually String or Long.
+ * Subclass of {@link Selection} exposing public support for mutating the underlying
+ * selection data. This is useful for clients of {@link SelectionTracker} that wish to
+ * manipulate a copy of selection data obtained via
+ * {@link SelectionTracker#copySelection(Selection)}.
*
- * @hide
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/
-@RestrictTo(LIBRARY_GROUP)
public final class MutableSelection<K> extends Selection<K> {
@Override
- public boolean add(K key) {
+ public boolean add(@NonNull K key) {
return super.add(key);
}
@Override
- public boolean remove(K key) {
+ public boolean remove(@NonNull K key) {
return super.remove(key);
}
@Override
- public void copyFrom(Selection<K> source) {
+ public void copyFrom(@NonNull Selection<K> source) {
super.copyFrom(source);
}
diff --git a/androidx/recyclerview/selection/MouseCallbacks.java b/androidx/widget/recyclerview/selection/OnContextClickListener.java
index 05c47c1f..9a9720c1 100644
--- a/androidx/recyclerview/selection/MouseCallbacks.java
+++ b/androidx/widget/recyclerview/selection/OnContextClickListener.java
@@ -14,28 +14,20 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.support.annotation.RestrictTo;
+import android.support.annotation.NonNull;
import android.view.MotionEvent;
/**
- * Override methods in this class to connect specialized behaviors of the selection
- * code to the application environment.
- *
- * @hide
+ * Override methods in this class to provide application specific behaviors
+ * related to mouse input.
*/
-@RestrictTo(LIBRARY_GROUP)
-public abstract class MouseCallbacks {
-
- static final MouseCallbacks DUMMY = new MouseCallbacks() {
- @Override
- public boolean onContextClick(MotionEvent e) {
- return false;
- }
- };
+/**
+ * Register an OnContextClickListener to be notified when a context click
+ * occurs.
+ */
+public interface OnContextClickListener {
/**
* Called when user performs a context click, usually via mouse pointer
@@ -44,5 +36,5 @@ public abstract class MouseCallbacks {
* @param e the event associated with the click.
* @return true if the event was handled.
*/
- public abstract boolean onContextClick(MotionEvent e);
+ boolean onContextClick(@NonNull MotionEvent e);
}
diff --git a/androidx/recyclerview/selection/TouchCallbacks.java b/androidx/widget/recyclerview/selection/OnDragInitiatedListener.java
index 59053927..893832f2 100644
--- a/androidx/recyclerview/selection/TouchCallbacks.java
+++ b/androidx/widget/recyclerview/selection/OnDragInitiatedListener.java
@@ -14,42 +14,32 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
-
-import android.support.annotation.RestrictTo;
+import android.support.annotation.NonNull;
import android.view.MotionEvent;
/**
- * Override methods in this class to connect specialized behaviors of the selection
- * code to the application environment.
- *
- * @hide
+ * Register an OnDragInitiatedListener to be notified of potential drag operations,
+ * and to handle them.
*/
-@RestrictTo(LIBRARY_GROUP)
-public abstract class TouchCallbacks {
-
- static final TouchCallbacks DUMMY = new TouchCallbacks() {
- @Override
- public boolean onDragInitiated(MotionEvent e) {
- return false;
- }
- };
+public interface OnDragInitiatedListener {
/**
* Called when a drag is initiated. Touch input handler only considers
* a drag to be initiated on long press on an existing selection,
* as normal touch and drag events are strongly associated with scrolling of the view.
*
- * <p>Drag will only be initiated when the item under the event is already selected.
+ * <p>
+ * Drag will only be initiated when the item under the event is already selected.
*
- * <p>The RecyclerView item at the coordinates of the MotionEvent is not supplied as a parameter
+ * <p>
+ * The RecyclerView item at the coordinates of the MotionEvent is not supplied as a parameter
* to this method as there may be multiple items selected. Clients can obtain the current
- * list of selected items from {@link SelectionHelper#copySelection(Selection)}.
+ * list of selected items from {@link SelectionTracker#copySelection(Selection)}.
*
* @param e the event associated with the drag.
* @return true if the event was handled.
*/
- public abstract boolean onDragInitiated(MotionEvent e);
+ boolean onDragInitiated(@NonNull MotionEvent e);
}
diff --git a/androidx/widget/recyclerview/selection/OnItemActivatedListener.java b/androidx/widget/recyclerview/selection/OnItemActivatedListener.java
new file mode 100644
index 00000000..bf5807d8
--- /dev/null
+++ b/androidx/widget/recyclerview/selection/OnItemActivatedListener.java
@@ -0,0 +1,43 @@
+/*
+ * 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.widget.recyclerview.selection;
+
+import android.support.annotation.NonNull;
+import android.view.MotionEvent;
+
+import androidx.widget.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+
+/**
+ * Register an OnItemActivatedListener to be notified when an item is activated
+ * (tapped or double clicked).
+ *
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+public interface OnItemActivatedListener<K> {
+
+ /**
+ * Called when an item is "activated". An item is activated, for example, when no selection
+ * exists and the user taps an item with her finger, or double clicks an item with a
+ * pointing device like a Mouse.
+ *
+ * @param item details of the item.
+ * @param e the event associated with item.
+ *
+ * @return true if the event was handled.
+ */
+ boolean onItemActivated(@NonNull ItemDetails<K> item, @NonNull MotionEvent e);
+}
diff --git a/androidx/widget/recyclerview/selection/OperationMonitor.java b/androidx/widget/recyclerview/selection/OperationMonitor.java
new file mode 100644
index 00000000..46b97e34
--- /dev/null
+++ b/androidx/widget/recyclerview/selection/OperationMonitor.java
@@ -0,0 +1,130 @@
+/*
+ * 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.widget.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+import static android.support.v4.util.Preconditions.checkState;
+
+import static androidx.widget.recyclerview.selection.Shared.DEBUG;
+
+import android.support.annotation.MainThread;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * OperationMonitor provides a mechanism to coordinate application
+ * logic with active user operations relating to selection (like band selection,
+ * or gesture selection).
+ *
+ * <p>
+ * The host {@link android.app.Activity} or {@link android.app.Fragment} should avoid changing
+ * {@link android.support.v7.widget.RecyclerView.Adapter Adapter} data while there
+ * are active selection operations, as this can result in a poor user experience.
+ *
+ * <p>
+ * To know when an operation is active listen to changes using an {@link OnChangeListener}.
+ */
+public final class OperationMonitor {
+
+ private static final String TAG = "OperationMonitor";
+
+ private int mNumOps = 0;
+ private List<OnChangeListener> mListeners = new ArrayList<>();
+
+ @MainThread
+ synchronized void start() {
+ mNumOps++;
+
+ if (mNumOps == 1) {
+ for (OnChangeListener l : mListeners) {
+ l.onChanged();
+ }
+ }
+
+ if (DEBUG) Log.v(TAG, "Incremented content lock count to " + mNumOps + ".");
+ }
+
+ @MainThread
+ synchronized void stop() {
+ checkState(mNumOps > 0);
+
+ mNumOps--;
+ if (DEBUG) Log.v(TAG, "Decremented content lock count to " + mNumOps + ".");
+
+ if (mNumOps == 0) {
+ for (OnChangeListener l : mListeners) {
+ l.onChanged();
+ }
+ }
+ }
+
+ /**
+ * @return true if there are any running operations.
+ */
+ @SuppressWarnings("unused")
+ public synchronized boolean isStarted() {
+ return mNumOps > 0;
+ }
+
+ /**
+ * Registers supplied listener to be notified when operation status changes.
+ * @param listener
+ */
+ public void addListener(@NonNull OnChangeListener listener) {
+ checkArgument(listener != null);
+ mListeners.add(listener);
+ }
+
+ /**
+ * Unregisters listener for further notifications.
+ * @param listener
+ */
+ public void removeListener(@NonNull OnChangeListener listener) {
+ checkArgument(listener != null);
+ mListeners.remove(listener);
+ }
+
+ /**
+ * Allows other selection code to perform a precondition check asserting the state is locked.
+ */
+ void checkStarted() {
+ checkState(mNumOps > 0);
+ }
+
+ /**
+ * Allows other selection code to perform a precondition check asserting the state is unlocked.
+ */
+ void checkStopped() {
+ checkState(mNumOps == 0);
+ }
+
+ /**
+ * Listen to changes in operation status. Authors should avoid
+ * changing the Adapter model while there are active operations.
+ */
+ public interface OnChangeListener {
+
+ /**
+ * Called when operation status changes. Call {@link OperationMonitor#isStarted()}
+ * to determine the current status.
+ */
+ void onChanged();
+ }
+}
diff --git a/androidx/recyclerview/selection/Range.java b/androidx/widget/recyclerview/selection/Range.java
index 632e4363..3984b698 100644
--- a/androidx/recyclerview/selection/Range.java
+++ b/androidx/widget/recyclerview/selection/Range.java
@@ -14,14 +14,15 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.v4.util.Preconditions.checkArgument;
import static android.support.v7.widget.RecyclerView.NO_POSITION;
-import static androidx.recyclerview.selection.Shared.DEBUG;
+import static androidx.widget.recyclerview.selection.Shared.DEBUG;
import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
import android.util.Log;
import java.lang.annotation.Retention;
@@ -38,15 +39,18 @@ final class Range {
* "Provisional" selection represents a overlay on the primary selection. A provisional
* selection maybe be eventually added to the primary selection, or it may be abandoned.
*
- * <p>E.g. BandSelectionHelper creates a provisional selection while a user is actively
+ * <p>
+ * E.g. BandSelectionHelper creates a provisional selection while a user is actively
* selecting items with a band. GestureSelectionHelper creates a provisional selection
* while a user is active selecting via gesture.
*
- * <p>Provisionally selected items are considered to be selected in
+ * <p>
+ * Provisionally selected items are considered to be selected in
* {@link Selection#contains(String)} and related methods. A provisional may be abandoned or
* merged into the promary selection.
*
- * <p>A provisional selection may intersect with the primary selection, however clearing the
+ * <p>
+ * A provisional selection may intersect with the primary selection, however clearing the
* provisional selection will not affect the primary selection where the two may intersect.
*/
static final int TYPE_PROVISIONAL = 1;
@@ -69,7 +73,7 @@ final class Range {
* @param position
* @param callbacks
*/
- Range(int position, Callbacks callbacks) {
+ Range(int position, @NonNull Callbacks callbacks) {
mBegin = position;
mCallbacks = callbacks;
if (DEBUG) Log.d(TAG, "Creating new Range anchored @ " + position);
@@ -177,7 +181,7 @@ final class Range {
}
/*
- * @see {@link DefaultSelectionHelper#updateForRange(int, int , boolean, int)}.
+ * @see {@link DefaultSelectionTracker#updateForRange(int, int , boolean, int)}.
*/
abstract static class Callbacks {
abstract void updateForRange(
diff --git a/androidx/recyclerview/selection/Selection.java b/androidx/widget/recyclerview/selection/Selection.java
index a6225307..173756ca 100644
--- a/androidx/recyclerview/selection/Selection.java
+++ b/androidx/widget/recyclerview/selection/Selection.java
@@ -14,13 +14,10 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
-
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+package androidx.widget.recyclerview.selection;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
-import android.support.annotation.RestrictTo;
-import android.support.annotation.VisibleForTesting;
import java.util.HashMap;
import java.util.HashSet;
@@ -29,35 +26,37 @@ import java.util.Map;
import java.util.Set;
/**
- * Object representing the current selection and provisional selection. Provides read only public
- * access, and private write access.
+ * Object representing a "primary" selection and a "provisional" selection.
+ *
* <p>
* This class tracks selected items by managing two sets:
*
- * <li>primary selection
+ * <p>
+ * <b>Primary Selection</b>
*
- * Primary selection consists of items tapped by a user or by lassoed by band select operation.
+ * <p>
+ * Primary selection (or just selection) consists of items selected by a user or
+ * lassoed by a completed band select operation.
*
- * <li>provisional selection
+ * <p>
+ * <b>Provisional Selection</b>
*
+ * <p>
* Provisional selections are selections which have been temporarily created
- * by an in-progress band select or gesture selection. Once the user releases the mouse button
- * or lifts their finger the corresponding provisional selection should be converted into
- * primary selection.
- *
- * <p>The total selection is the combination of
- * both the core selection and the provisional selection. Tracking both separately is necessary to
- * ensure that items in the core selection are not "erased" from the core selection when they
- * are temporarily included in a secondary selection (like band selection).
+ * by an in-progress operation such as band select or gesture selection. Once completed
+ * such operations convert provisional selection into primary selection, or if the
+ * operation is canceled cleared. Provisional selection exists to permit such operational
+ * selections to intersect with the primary selection without subsequently erasing the
+ * selection if the provisional selection is revised to not intersect with the primary
+ * selection.
*
- * @param <K> Selection key type. Usually String or Long.
+ * @see MutableSelection
*
- * @hide
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/
-@RestrictTo(LIBRARY_GROUP)
public class Selection<K> implements Iterable<K> {
- // NOTE: Not currently private as DefaultSelectionHelper directly manipulates values.
+ // NOTE: Not currently private as DefaultSelectionTracker directly manipulates values.
final Set<K> mSelection;
final Set<K> mProvisionalSelection;
@@ -67,9 +66,9 @@ public class Selection<K> implements Iterable<K> {
}
/**
- * Used by {@link SelectionStorage} when restoring selection.
+ * Used by {@link StorageStrategy} when restoring selection.
*/
- Selection(Set<K> selection) {
+ Selection(@NonNull Set<K> selection) {
mSelection = selection;
mProvisionalSelection = new HashSet<>();
}
@@ -113,7 +112,7 @@ public class Selection<K> implements Iterable<K> {
* one (if it exists) is abandoned.
* @return Map of ids added or removed. Added ids have a value of true, removed are false.
*/
- Map<K, Boolean> setProvisionalSelection(Set<K> newSelection) {
+ Map<K, Boolean> setProvisionalSelection(@NonNull Set<K> newSelection) {
Map<K, Boolean> delta = new HashMap<>();
for (K key: mProvisionalSelection) {
@@ -160,8 +159,7 @@ public class Selection<K> implements Iterable<K> {
* subsequent provisional selections which are different from this existing one cannot
* cause items in this existing provisional selection to become deselected.
*/
- @VisibleForTesting
- protected void mergeProvisionalSelection() {
+ void mergeProvisionalSelection() {
mSelection.addAll(mProvisionalSelection);
mProvisionalSelection.clear();
}
@@ -170,7 +168,6 @@ public class Selection<K> implements Iterable<K> {
* Abandons the existing provisional selection so that all items provisionally selected are
* now deselected.
*/
- @VisibleForTesting
void clearProvisionalSelection() {
mProvisionalSelection.clear();
}
@@ -180,13 +177,8 @@ public class Selection<K> implements Iterable<K> {
*
* @return true if the operation resulted in a modification to the selection.
*/
- boolean add(K key) {
- if (mSelection.contains(key)) {
- return false;
- }
-
- mSelection.add(key);
- return true;
+ boolean add(@NonNull K key) {
+ return mSelection.add(key);
}
/**
@@ -194,13 +186,8 @@ public class Selection<K> implements Iterable<K> {
*
* @return true if the operation resulted in a modification to the selection.
*/
- boolean remove(K key) {
- if (!mSelection.contains(key)) {
- return false;
- }
-
- mSelection.remove(key);
- return true;
+ boolean remove(@NonNull K key) {
+ return mSelection.remove(key);
}
/**
@@ -214,7 +201,7 @@ public class Selection<K> implements Iterable<K> {
* Clones primary and provisional selection from supplied {@link Selection}.
* Does not copy active range data.
*/
- void copyFrom(Selection<K> source) {
+ void copyFrom(@NonNull Selection<K> source) {
mSelection.clear();
mSelection.addAll(source.mSelection);
@@ -245,11 +232,8 @@ public class Selection<K> implements Iterable<K> {
@Override
public boolean equals(Object other) {
- if (this == other) {
- return true;
- }
-
- return other instanceof Selection && isEqualTo((Selection) other);
+ return (this == other)
+ || (other instanceof Selection && isEqualTo((Selection) other));
}
private boolean isEqualTo(Selection other) {
diff --git a/androidx/recyclerview/selection/SelectionPredicates.java b/androidx/widget/recyclerview/selection/SelectionPredicates.java
index 26253d98..bab1efe0 100644
--- a/androidx/recyclerview/selection/SelectionPredicates.java
+++ b/androidx/widget/recyclerview/selection/SelectionPredicates.java
@@ -14,20 +14,16 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
-import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import android.support.annotation.NonNull;
-import android.support.annotation.RestrictTo;
-
-import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+import androidx.widget.recyclerview.selection.SelectionTracker.SelectionPredicate;
/**
- * Utility class for creating SelectionPredicate instances.
- *
- * @hide
+ * Utility class for creating SelectionPredicate instances. Provides default
+ * implementations for common cases like "single selection" and "select anything".
*/
-@RestrictTo(LIBRARY_GROUP)
public final class SelectionPredicates {
private SelectionPredicates() {}
@@ -35,13 +31,14 @@ public final class SelectionPredicates {
/**
* Returns a selection predicate that allows multiples items to be selected, without
* any restrictions on which items can be selected.
- * @param <K>
+ *
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
* @return
*/
- public static <K> SelectionPredicate<K> selectAnything() {
+ public static <K> SelectionPredicate<K> createSelectAnything() {
return new SelectionPredicate<K>() {
@Override
- public boolean canSetStateForKey(K key, boolean nextState) {
+ public boolean canSetStateForKey(@NonNull K key, boolean nextState) {
return true;
}
@@ -60,13 +57,14 @@ public final class SelectionPredicates {
/**
* Returns a selection predicate that allows a single item to be selected, without
* any restrictions on which item can be selected.
- * @param <K>
+ *
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
* @return
*/
- public static <K> SelectionPredicate<K> selectSingleAnything() {
+ public static <K> SelectionPredicate<K> createSelectSingleAnything() {
return new SelectionPredicate<K>() {
@Override
- public boolean canSetStateForKey(K key, boolean nextState) {
+ public boolean canSetStateForKey(@NonNull K key, boolean nextState) {
return true;
}
diff --git a/androidx/widget/recyclerview/selection/SelectionTracker.java b/androidx/widget/recyclerview/selection/SelectionTracker.java
new file mode 100644
index 00000000..54658408
--- /dev/null
+++ b/androidx/widget/recyclerview/selection/SelectionTracker.java
@@ -0,0 +1,814 @@
+/*
+ * 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.widget.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.DrawableRes;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v7.widget.RecyclerView;
+import android.view.GestureDetector;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+
+import java.util.Set;
+
+/**
+ * SelectionTracker provides support for managing a selection of items in a RecyclerView instance.
+ *
+ * <p>
+ * This class provides support for managing a "primary" set of selected items,
+ * in addition to a "provisional" set of selected items using traditional collection
+ * like methods as well as more "range" operations.
+ *
+ * <p>
+ * Create an instance of SelectionTracker using {@link Builder SelectionTracker.Builder}.
+ *
+ * <p>
+ * <b>Inspecting the current selection</b>
+ *
+ * <p>
+ * The underlying selection is described by the {@link Selection} class.
+ *
+ * <p>
+ * A live view of the current selection can be obtained using {@link #getSelection}. Changes made
+ * to the selection using SelectionTracker will be immediately reflected in this Selection.
+ *
+ * <p>
+ * To obtain a stable snapshot of the selection use {@link #copySelection(Selection)}. See
+ * also {@link MutableSelection}.
+ *
+ * <p>
+ * Selection state for an individual item can be obtained using {@link #isSelected(Object)}.
+ *
+ * <p>
+ * <b>Provisional Selection</b>
+ *
+ * <p>
+ * Provisional selection exists to address issues where a transitory selection (like
+ * the selection use for an active band selection) might momentarily intersect
+ * with a previously established selection resulting in a some or all of the
+ * established selection being erased. These situations arise, for example, when band
+ * selection is being performed in "additive" mode (e.g. SHIFT or CTRL is pressed on
+ * the keyboard prior to mouse down), or when there's an active gesture selection
+ * (which can be initiated by long pressing an item while there is an existing selection).
+ *
+ * <p>
+ * A provisional selection can be abandoned, or merged into the primary selection.
+ *
+ * <p>
+ * <b>Ranges</b>
+ *
+ * <p>
+ * Ranges provide a mechanism for defining selection blocks based on starting and
+ * ending item positions. Traditional uses for this type of functionality would
+ * be selecting items from an established "anchor" point to another item tapped
+ * or clicked by a user. Think SHIFT+CLICK on devices with an attached mouse. Support
+ * for common range selection interactions (including touch driven gesture selection)
+ * is included by default when using {@link Builder}.
+ *
+ * <p>
+ * <b>Enforcing selection policies</b>
+ *
+ * <p>
+ * Which items can be selected by the user is a matter of policy in an Application.
+ * Developers supply these policies by way of {@link SelectionPredicate}.
+ *
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+public abstract class SelectionTracker<K> {
+
+ /**
+ * This value is included in the payload when SelectionTracker notifies RecyclerView
+ * of changes to selection. Look for this value in the {@code payload}
+ * Object argument supplied to
+ * {@link android.support.v7.widget.RecyclerView.Adapter#onBindViewHolder
+ * Adapter#onBindViewHolder}.
+ * If present the call is occurring in response to a selection state change.
+ * This would be a good opportunity to animate changes between unselected and selected state.
+ * When state is being restored, this argument will not be present.
+ */
+ public static final String SELECTION_CHANGED_MARKER = "Selection-Changed";
+
+ /**
+ * Adds {@code observer} to be notified when changes to selection occur.
+ *
+ * <p>
+ * Use an observer to track attributes about the selection and
+ * update the UI to reflect the state of the selection. For example, an author
+ * may use an observer to control the enabled status of menu items,
+ * or to initiate {@link android.view.ActionMode}.
+ */
+ public abstract void addObserver(SelectionObserver observer);
+
+ /** @return true if has a selection */
+ public abstract boolean hasSelection();
+
+ /**
+ * Returns a Selection object that provides a live view on the current selection.
+ *
+ * @return The current selection.
+ * @see #copySelection(Selection) on how to get a snapshot
+ * of the selection that will not reflect future changes
+ * to selection.
+ */
+ public abstract Selection getSelection();
+
+ /**
+ * Updates {@code dest} to reflect the current selection.
+ */
+ public abstract void copySelection(@NonNull Selection<K> dest);
+
+ /**
+ * @return true if the item specified by its id is selected. Shorthand for
+ * {@code getSelection().contains(K)}.
+ */
+ public abstract boolean isSelected(@Nullable K key);
+
+ /**
+ * Restores the selected state of specified items. Used in cases such as restore the selection
+ * after rotation etc. Provisional selection is not restored.
+ *
+ * <p>
+ * This affords clients the ability to restore selection from selection saved
+ * in Activity state.
+ *
+ * @see StorageStrategy details on selection state support.
+ *
+ * @param selection selection being restored.
+ */
+ public abstract void restoreSelection(@NonNull Selection<K> selection);
+
+ /**
+ * Clears both primary and provisional selections.
+ *
+ * @return true if primary selection changed.
+ */
+ public abstract boolean clearSelection();
+
+ /**
+ * Sets the selected state of the specified items if permitted after consulting
+ * SelectionPredicate.
+ */
+ public abstract boolean setItemsSelected(@NonNull Iterable<K> keys, boolean selected);
+
+ /**
+ * Attempts to select an item.
+ *
+ * @return true if the item was selected. False if the item could not be selected, or was
+ * was already selected.
+ */
+ public abstract boolean select(@NonNull K key);
+
+ /**
+ * Attempts to deselect an item.
+ *
+ * @return true if the item was deselected. False if the item could not be deselected, or was
+ * was already un-selected.
+ */
+ public abstract boolean deselect(@NonNull K key);
+
+ abstract void onDataSetChanged();
+
+ /**
+ * Attempts to establish a range selection at {@code position}, selecting the item
+ * at {@code position} if needed.
+ *
+ * @param position The "anchor" position for the range. Subsequent range operations
+ * (primarily keyboard and mouse based operations like SHIFT + click)
+ * work with the established anchor point to define selection ranges.
+ */
+ abstract void startRange(int position);
+
+ /**
+ * Sets the end point for the active range selection.
+ *
+ * <p>
+ * This function should only be called when a range selection is active
+ * (see {@link #isRangeActive()}. Items in the range [anchor, end] will be
+ * selected after consulting SelectionPredicate.
+ *
+ * @param position The new end position for the selection range.
+ * @throws IllegalStateException if a range selection is not active. Range selection
+ * must have been started by a call to {@link #startRange(int)}.
+ */
+ abstract void extendRange(int position);
+
+ /**
+ * Clears an in-progress range selection. Provisional range selection established
+ * using {@link #extendProvisionalRange(int)} will be cleared (unless
+ * {@link #mergeProvisionalSelection()} is called first.)
+ */
+ abstract void endRange();
+
+ /**
+ * @return Whether or not there is a current range selection active.
+ */
+ abstract boolean isRangeActive();
+
+ /**
+ * Establishes the "anchor" at which a selection range begins. This "anchor" is consulted
+ * when determining how to extend, and modify selection ranges. Calling this when a
+ * range selection is active will reset the range selection.
+ *
+ * TODO: Reconcile this with startRange. Maybe just docs need to be updated.
+ *
+ * @param position the anchor position. Must already be selected.
+ */
+ abstract void anchorRange(int position);
+
+ /**
+ * Creates a provisional selection from anchor to {@code position}.
+ *
+ * @param position the end point.
+ */
+ abstract void extendProvisionalRange(int position);
+
+ /**
+ * Sets the provisional selection, replacing any existing selection.
+ * @param newSelection
+ */
+ abstract void setProvisionalSelection(@NonNull Set<K> newSelection);
+
+ /**
+ * Clears any existing provisional selection
+ */
+ abstract void clearProvisionalSelection();
+
+ /**
+ * Converts the provisional selection into primary selection, then clears
+ * provisional selection.
+ */
+ abstract void mergeProvisionalSelection();
+
+ /**
+ * Preserves selection, if any. Call this method from Activity#onSaveInstanceState
+ *
+ * @param state Bundle instance supplied to onSaveInstanceState.
+ */
+ public abstract void onSaveInstanceState(@NonNull Bundle state);
+
+ /**
+ * Restores selection from previously saved state. Call this method from
+ * Activity#onCreate.
+ *
+ * @param state Bundle instance supplied to onCreate.
+ */
+ public abstract void onRestoreInstanceState(@Nullable Bundle state);
+
+ /**
+ * Observer class providing access to information about Selection state changes.
+ *
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+ public abstract static class SelectionObserver<K> {
+
+ /**
+ * Called when the state of an item has been changed.
+ */
+ public void onItemStateChanged(@NonNull K key, boolean selected) {
+ }
+
+ /**
+ * Called when the underlying data set has changed. After this method is called
+ * SelectionTracker will traverse the existing selection,
+ * calling {@link #onItemStateChanged(K, boolean)} for each selected item,
+ * and deselecting any items that cannot be selected given the updated data-set
+ * (and after consulting SelectionPredicate).
+ */
+ public void onSelectionRefresh() {
+ }
+
+ /**
+ * Called immediately after completion of any set of changes, excluding
+ * those resulting in calls to {@link #onSelectionRefresh()} and
+ * {@link #onSelectionRestored()}.
+ */
+ public void onSelectionChanged() {
+ }
+
+ /**
+ * Called immediately after selection is restored.
+ * {@link #onItemStateChanged(K, boolean)} will *not* be called
+ * for individual items in the selection.
+ */
+ public void onSelectionRestored() {
+ }
+ }
+
+ /**
+ * Implement SelectionPredicate to control when items can be selected or unselected.
+ * See {@link Builder#withSelectionPredicate(SelectionPredicate)}.
+ *
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
+ */
+ public abstract static class SelectionPredicate<K> {
+
+ /**
+ * Validates a change to selection for a specific key.
+ *
+ * @param key the item key
+ * @param nextState the next potential selected/unselected state
+ * @return true if the item at {@code id} can be set to {@code nextState}.
+ */
+ public abstract boolean canSetStateForKey(@NonNull K key, boolean nextState);
+
+ /**
+ * Validates a change to selection for a specific position. If necessary
+ * use {@link ItemKeyProvider} to identy associated key.
+ *
+ * @param position the item position
+ * @param nextState the next potential selected/unselected state
+ * @return true if the item at {@code id} can be set to {@code nextState}.
+ */
+ public abstract boolean canSetStateAtPosition(int position, boolean nextState);
+
+ /**
+ * Permits restriction to single selection mode. Single selection mode has
+ * unique behaviors in that it'll deselect an item already selected
+ * in order to select the new item.
+ *
+ * <p>
+ * In order to limit the number of items that can be selected,
+ * use {@link #canSetStateForKey(Object, boolean)} and
+ * {@link #canSetStateAtPosition(int, boolean)}.
+ *
+ * @return true if more than a single item can be selected.
+ */
+ public abstract boolean canSelectMultiple();
+ }
+
+ /**
+ * Builder is the primary mechanism for create a {@link SelectionTracker} that
+ * can be used with your RecyclerView. Once installed, users will be able to create and
+ * manipulate selection using a variety of intuitive techniques like tap, gesture,
+ * and mouse lasso.
+ *
+ * <p>
+ * Example usage:
+ * <pre>SelectionTracker tracker = new SelectionTracker.Builder<>(
+ * "my-selection-id",
+ * mRecyclerView,
+ * new DemoStableIdProvider(mAdapter),
+ * detailsLookup,
+ * StorageStrategy.createStringStorage())
+ * .build();
+ *
+ * // By default multi-select is supported.
+ * // This configuration supports single selection for any single element.
+ * <pre>SelectionTracker tracker = new SelectionTracker.Builder<>(
+ * "my-selection-id",
+ * mRecyclerView,
+ * new DemoStableIdProvider(mAdapter),
+ * detailsLookup,
+ * StorageStrategy.createStringStorage())
+ * .withSelectionPredicate(SelectionPredicates.createSelectAnything())
+ * .build();
+ *</pre>
+ *
+ * <p>
+ * <b>Restricting which items can be selected and limiting selection size</b>
+ *
+ * <p>
+ * {@link SelectionPredicate} provides a mechanism to restrict which Items can be selected,
+ * to limit the number of items that can be selected, as well as allowing the selection
+ * code to be placed into "single select" mode, which as the name indicates, constrains
+ * the selection size to a single item.
+ *
+ * <p>
+ * <b>Retaining state across Android lifecycle events</b>
+ *
+ * <p>
+ * Support for storage/persistence of selection must be configured and invoked manually
+ * owing to its reliance on Activity lifecycle events.
+ * Failure to include support for selection storage will result in the active selection
+ * being lost when the Activity receives a configuration change (e.g. rotation)
+ * or when the application process is destroyed by the OS to reclaim resources.
+ *
+ * <p>
+ * <b>Key Type</b>
+ *
+ * <p>
+ * Developers must decide on the key type used to identify selected items. These are the
+ * values your application will use to identify selected items. Support
+ * is provided for three types: Parcelable, String, and Long.
+ *
+ * <p>
+ * {@link Parcelable}: Any Parcelable type can be used as the selection key. This is especially
+ * useful for use with {@link android.net.Uri} as the Android URI implementation is both
+ * parcelable and makes for a natural stable selection key. If items in your view are
+ * associated with stable {@code content://} uris, you should use Uri.
+ *
+ * <p>
+ * {@link String}: Use String when a string based stable identifier is available.
+ *
+ * <p>
+ * {@link Long}: Use Long when RecyclerView's long stable ids are
+ * already in use. It comes with some limitations, however, as access to stable ids
+ * at runtime is limited. Band selection support is not available when using the default
+ * long key storage implementation. See @link {@link ItemKeyProvider} for details.
+ *
+ * <p>
+ * Usage:
+ *
+ * <pre>
+ * private SelectionTracker<Uri> mTracker;
+ *
+ * public void onCreate(Bundle savedInstanceState) {
+ * // See above for details on constructing a SelectionTracker instance.
+ *
+ * if (savedInstanceState != null) {
+ * mTracker.onRestoreInstanceState(savedInstanceState);
+ * }
+ * }
+ *
+ * protected void onSaveInstanceState(Bundle outState) {
+ * super.onSaveInstanceState(outState);
+ * mTracker.onSaveInstanceState(outState);
+ * }
+ * </pre>
+ *
+ * @param <K> Selection key type. Built in support is provided for {@link String},
+ * {@link Long}, and {@link Parcelable}. {@link StorageStrategy}
+ * provides factory methods for each type:
+ * {@link StorageStrategy#createStringStorage()},
+ * {@link StorageStrategy#createParcelableStorage(Class)},
+ * {@link StorageStrategy#createLongStorage()}
+ */
+ public static final class Builder<K> {
+
+ private final RecyclerView mRecyclerView;
+ private final RecyclerView.Adapter<?> mAdapter;
+ private final Context mContext;
+ private final String mSelectionId;
+ private final StorageStrategy<K> mStorage;
+
+ private SelectionPredicate<K> mSelectionPredicate =
+ SelectionPredicates.createSelectAnything();
+ private OperationMonitor mMonitor = new OperationMonitor();
+ private ItemKeyProvider<K> mKeyProvider;
+ private ItemDetailsLookup<K> mDetailsLookup;
+
+ private FocusDelegate<K> mFocusDelegate = FocusDelegate.dummy();
+
+ private OnItemActivatedListener<K> mOnItemActivatedListener;
+ private OnDragInitiatedListener mOnDragInitiatedListener;
+ private OnContextClickListener mOnContextClickListener;
+
+ private BandPredicate mBandPredicate;
+ private int mBandOverlayId = R.drawable.selection_band_overlay;
+
+ private int[] mGestureToolTypes = new int[] {
+ MotionEvent.TOOL_TYPE_FINGER,
+ MotionEvent.TOOL_TYPE_UNKNOWN
+ };
+
+ private int[] mBandToolTypes = new int[] {
+ MotionEvent.TOOL_TYPE_MOUSE
+ };
+
+ /**
+ * Creates a new SelectionTracker.Builder useful for configuring and creating
+ * a new SelectionTracker for use with your {@link RecyclerView}.
+ *
+ * @param selectionId A unique string identifying this selection in the context
+ * of the activity or fragment.
+ * @param recyclerView the owning RecyclerView
+ * @param keyProvider the source of selection keys
+ * @param detailsLookup the source of information about RecyclerView items.
+ * @param storage Strategy for type-safe storage of selection state in
+ * {@link Bundle}.
+ */
+ public Builder(
+ @NonNull String selectionId,
+ @NonNull RecyclerView recyclerView,
+ @NonNull ItemKeyProvider<K> keyProvider,
+ @NonNull ItemDetailsLookup<K> detailsLookup,
+ @NonNull StorageStrategy<K> storage) {
+
+ checkArgument(selectionId != null);
+ checkArgument(!selectionId.trim().isEmpty());
+ checkArgument(recyclerView != null);
+
+ mSelectionId = selectionId;
+ mRecyclerView = recyclerView;
+ mContext = recyclerView.getContext();
+ mAdapter = recyclerView.getAdapter();
+
+ checkArgument(mAdapter != null);
+ checkArgument(keyProvider != null);
+ checkArgument(detailsLookup != null);
+ checkArgument(storage != null);
+
+ mDetailsLookup = detailsLookup;
+ mKeyProvider = keyProvider;
+ mStorage = storage;
+
+ mBandPredicate = new BandPredicate.NonDraggableArea(mRecyclerView, detailsLookup);
+ }
+
+ /**
+ * Install selection predicate.
+ *
+ * @param predicate the predicate to be used.
+ * @return this
+ */
+ public Builder<K> withSelectionPredicate(
+ @NonNull SelectionPredicate<K> predicate) {
+
+ checkArgument(predicate != null);
+ mSelectionPredicate = predicate;
+ return this;
+ }
+
+ /**
+ * Add operation monitor allowing access to information about active
+ * operations (like band selection and gesture selection).
+ *
+ * @param monitor the monitor to be used
+ * @return this
+ */
+ public Builder<K> withOperationMonitor(
+ @NonNull OperationMonitor monitor) {
+
+ checkArgument(monitor != null);
+ mMonitor = monitor;
+ return this;
+ }
+
+ /**
+ * Add focus delegate to interact with selection related focus changes.
+ *
+ * @param delegate the delegate to be used
+ * @return this
+ */
+ public Builder<K> withFocusDelegate(@NonNull FocusDelegate<K> delegate) {
+ checkArgument(delegate != null);
+ mFocusDelegate = delegate;
+ return this;
+ }
+
+ /**
+ * Adds an item activation listener. Respond to taps/enter/double-click on items.
+ *
+ * @param listener the listener to be used
+ * @return this
+ */
+ public Builder<K> withOnItemActivatedListener(
+ @NonNull OnItemActivatedListener<K> listener) {
+
+ checkArgument(listener != null);
+
+ mOnItemActivatedListener = listener;
+ return this;
+ }
+
+ /**
+ * Adds a context click listener. Respond to right-click.
+ *
+ * @param listener the listener to be used
+ * @return this
+ */
+ public Builder<K> withOnContextClickListener(
+ @NonNull OnContextClickListener listener) {
+
+ checkArgument(listener != null);
+
+ mOnContextClickListener = listener;
+ return this;
+ }
+
+ /**
+ * Adds a drag initiated listener. Add support for drag and drop.
+ *
+ * @param listener the listener to be used
+ * @return this
+ */
+ public Builder<K> withOnDragInitiatedListener(
+ @NonNull OnDragInitiatedListener listener) {
+
+ checkArgument(listener != null);
+
+ mOnDragInitiatedListener = listener;
+ return this;
+ }
+
+ /**
+ * Replaces default tap and gesture tool-types. Defaults are:
+ * {@link MotionEvent#TOOL_TYPE_FINGER} and {@link MotionEvent#TOOL_TYPE_UNKNOWN}.
+ *
+ * @param toolTypes the tool types to be used
+ * @return this
+ */
+ public Builder<K> withGestureTooltypes(int... toolTypes) {
+ mGestureToolTypes = toolTypes;
+ return this;
+ }
+
+ /**
+ * Replaces default band overlay.
+ *
+ * @param bandOverlayId
+ * @return this
+ */
+ public Builder<K> withBandOverlay(@DrawableRes int bandOverlayId) {
+ mBandOverlayId = bandOverlayId;
+ return this;
+ }
+
+ /**
+ * Replaces default band predicate.
+ * @param bandPredicate
+ * @return this
+ */
+ public Builder<K> withBandPredicate(@NonNull BandPredicate bandPredicate) {
+ checkArgument(bandPredicate != null);
+
+ mBandPredicate = bandPredicate;
+ return this;
+ }
+
+ /**
+ * Replaces default band selection tool-types. Defaults are:
+ * {@link MotionEvent#TOOL_TYPE_MOUSE}.
+ *
+ * @param toolTypes the tool types to be used
+ * @return this
+ */
+ public Builder<K> withBandTooltypes(int... toolTypes) {
+ mBandToolTypes = toolTypes;
+ return this;
+ }
+
+ /**
+ * Prepares and returns a SelectionTracker.
+ *
+ * @return this
+ */
+ public SelectionTracker<K> build() {
+
+ SelectionTracker<K> tracker = new DefaultSelectionTracker<>(
+ mSelectionId, mKeyProvider, mSelectionPredicate, mStorage);
+
+ // Event glue between RecyclerView and SelectionTracker keeps the classes separate
+ // so that a SelectionTracker can be shared across RecyclerView instances that
+ // represent the same data in different ways.
+ EventBridge.install(mAdapter, tracker, mKeyProvider);
+
+ AutoScroller scroller =
+ new ViewAutoScroller(ViewAutoScroller.createScrollHost(mRecyclerView));
+
+ // Setup basic input handling, with the touch handler as the default consumer
+ // of events. If mouse handling is configured as well, the mouse input
+ // related handlers will intercept mouse input events.
+
+ // GestureRouter is responsible for routing GestureDetector events
+ // to tool-type specific handlers.
+ GestureRouter<MotionInputHandler> gestureRouter = new GestureRouter<>();
+ GestureDetector gestureDetector = new GestureDetector(mContext, gestureRouter);
+
+ // TouchEventRouter takes its name from RecyclerView#OnItemTouchListener.
+ // Despite "Touch" being in the name, it receives events for all types of tools.
+ // This class is responsible for routing events to tool-type specific handlers,
+ // and if not handled by a handler, on to a GestureDetector for analysis.
+ TouchEventRouter eventRouter = new TouchEventRouter(gestureDetector);
+
+ // GestureSelectionHelper provides logic that interprets a combination
+ // of motions and gestures in order to provide gesture driven selection support
+ // when used in conjunction with RecyclerView.
+ final GestureSelectionHelper gestureHelper =
+ GestureSelectionHelper.create(tracker, mRecyclerView, scroller, mMonitor);
+
+ // Finally hook the framework up to listening to recycle view events.
+ mRecyclerView.addOnItemTouchListener(eventRouter);
+
+ // But before you move on, there's more work to do. Event plumbing has been
+ // installed, but we haven't registered any of our helpers or callbacks.
+ // Helpers contain predefined logic converting events into selection related events.
+ // Callbacks provide developers the ability to reponspond to other types of
+ // events (like "activate" a tapped item). This is broken up into two main
+ // suites, one for "touch" and one for "mouse", though both can and should (usually)
+ // be configured to handle other types of input (to satisfy user expectation).);
+
+ // Internally, the code doesn't permit nullable listeners, so we lazily
+ // initialize dummy instances if the developer didn't supply a real listener.
+ mOnDragInitiatedListener = (mOnDragInitiatedListener != null)
+ ? mOnDragInitiatedListener
+ : new OnDragInitiatedListener() {
+ @Override
+ public boolean onDragInitiated(@NonNull MotionEvent e) {
+ return false;
+ }
+ };
+
+ mOnItemActivatedListener = (mOnItemActivatedListener != null)
+ ? mOnItemActivatedListener
+ : new OnItemActivatedListener<K>() {
+ @Override
+ public boolean onItemActivated(
+ @NonNull ItemDetailsLookup.ItemDetails<K> item,
+ @NonNull MotionEvent e) {
+ return false;
+ }
+ };
+
+ mOnContextClickListener = (mOnContextClickListener != null)
+ ? mOnContextClickListener
+ : new OnContextClickListener() {
+ @Override
+ public boolean onContextClick(@NonNull MotionEvent e) {
+ return false;
+ }
+ };
+
+ // Provides high level glue for binding touch events
+ // and gestures to selection framework.
+ TouchInputHandler<K> touchHandler = new TouchInputHandler<K>(
+ tracker,
+ mKeyProvider,
+ mDetailsLookup,
+ mSelectionPredicate,
+ new Runnable() {
+ @Override
+ public void run() {
+ if (mSelectionPredicate.canSelectMultiple()) {
+ gestureHelper.start();
+ }
+ }
+ },
+ mOnDragInitiatedListener,
+ mOnItemActivatedListener,
+ mFocusDelegate,
+ new Runnable() {
+ @Override
+ public void run() {
+ mRecyclerView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+ }
+ });
+
+ for (int toolType : mGestureToolTypes) {
+ gestureRouter.register(toolType, touchHandler);
+ eventRouter.register(toolType, gestureHelper);
+ }
+
+ // Provides high level glue for binding mouse events and gestures
+ // to selection framework.
+ MouseInputHandler<K> mouseHandler = new MouseInputHandler<>(
+ tracker,
+ mKeyProvider,
+ mDetailsLookup,
+ mOnContextClickListener,
+ mOnItemActivatedListener,
+ mFocusDelegate);
+
+ for (int toolType : mBandToolTypes) {
+ gestureRouter.register(toolType, mouseHandler);
+ }
+
+ // Band selection not supported in single select mode, or when key access
+ // is limited to anything less than the entire corpus.
+ if (mKeyProvider.hasAccess(ItemKeyProvider.SCOPE_MAPPED)
+ && mSelectionPredicate.canSelectMultiple()) {
+ // BandSelectionHelper provides support for band selection on-top of a RecyclerView
+ // instance. Given the recycling nature of RecyclerView BandSelectionController
+ // necessarily models and caches list/grid information as the user's pointer
+ // interacts with the item in the RecyclerView. Selectable items that intersect
+ // with the band, both on and off screen, are selected.
+ BandSelectionHelper bandHelper = BandSelectionHelper.create(
+ mRecyclerView,
+ scroller,
+ mBandOverlayId,
+ mKeyProvider,
+ tracker,
+ mSelectionPredicate,
+ mBandPredicate,
+ mFocusDelegate,
+ mMonitor);
+
+ for (int toolType : mBandToolTypes) {
+ eventRouter.register(toolType, bandHelper);
+ }
+ }
+
+ return tracker;
+ }
+ }
+}
diff --git a/androidx/recyclerview/selection/Shared.java b/androidx/widget/recyclerview/selection/Shared.java
index 3b791205..48b30a21 100644
--- a/androidx/recyclerview/selection/Shared.java
+++ b/androidx/widget/recyclerview/selection/Shared.java
@@ -14,15 +14,15 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
/**
- * Shared constants used in this package.
+ * Shared constants used in this package. Disable DEBUG and VERBOSE prior to releases.
*/
final class Shared {
static final boolean DEBUG = false;
- static final boolean VERBOSE = true;
+ static final boolean VERBOSE = false;
private Shared() {}
}
diff --git a/androidx/recyclerview/selection/StableIdKeyProvider.java b/androidx/widget/recyclerview/selection/StableIdKeyProvider.java
index 3dc78ca3..ce244652 100644
--- a/androidx/recyclerview/selection/StableIdKeyProvider.java
+++ b/androidx/widget/recyclerview/selection/StableIdKeyProvider.java
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.RestrictTo;
import android.support.v7.widget.RecyclerView;
@@ -29,7 +30,14 @@ import java.util.HashMap;
import java.util.Map;
/**
- * ItemKeyProvider that provides stable ids by way of cached RecyclerView.Adapter stable ids.
+ * ItemKeyProvider that provides stable ids by way of cached {@link RecyclerView.Adapter}
+ * stable ids. Items enter the cache as they are laid out by RecyclerView, and are removed
+ * from the cache as they are recycled.
+ *
+ * <p>
+ * There are trade-offs with this implementation as it necessarily auto-boxes {@code long}
+ * stable id values into {@code Long} values for use as selection keys. The core Selection API
+ * uses a parameterized key type to permit other keys (such as Strings or URIs).
*
* @hide
*/
@@ -38,17 +46,23 @@ public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
private final SparseArray<Long> mPositionToKey = new SparseArray<>();
private final Map<Long, Integer> mKeyToPosition = new HashMap<Long, Integer>();
- private final RecyclerView mRecView;
+ private final RecyclerView mRecyclerView;
- public StableIdKeyProvider(RecyclerView recView) {
+ /**
+ * Creates a new key provider that uses cached {@code long} stable ids associated
+ * with the RecyclerView items.
+ *
+ * @param recyclerView the owner RecyclerView
+ */
+ public StableIdKeyProvider(@NonNull RecyclerView recyclerView) {
// Since this provide is based on stable ids based on whats laid out in the window
// we can only satisfy "window" scope key access.
super(SCOPE_CACHED);
- mRecView = recView;
+ mRecyclerView = recyclerView;
- mRecView.addOnChildAttachStateChangeListener(
+ mRecyclerView.addOnChildAttachStateChangeListener(
new OnChildAttachStateChangeListener() {
@Override
public void onChildViewAttachedToWindow(View view) {
@@ -64,8 +78,8 @@ public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
}
- private void onAttached(View view) {
- RecyclerView.ViewHolder holder = mRecView.findContainingViewHolder(view);
+ private void onAttached(@NonNull View view) {
+ RecyclerView.ViewHolder holder = mRecyclerView.findContainingViewHolder(view);
int position = holder.getAdapterPosition();
long id = holder.getItemId();
if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
@@ -74,8 +88,8 @@ public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
}
}
- private void onDetached(View view) {
- RecyclerView.ViewHolder holder = mRecView.findContainingViewHolder(view);
+ private void onDetached(@NonNull View view) {
+ RecyclerView.ViewHolder holder = mRecyclerView.findContainingViewHolder(view);
int position = holder.getAdapterPosition();
long id = holder.getItemId();
if (position != RecyclerView.NO_POSITION && id != RecyclerView.NO_ID) {
@@ -90,7 +104,7 @@ public final class StableIdKeyProvider extends ItemKeyProvider<Long> {
}
@Override
- public int getPosition(Long key) {
+ public int getPosition(@NonNull Long key) {
if (mKeyToPosition.containsKey(key)) {
return mKeyToPosition.get(key);
}
diff --git a/androidx/widget/recyclerview/selection/StorageStrategy.java b/androidx/widget/recyclerview/selection/StorageStrategy.java
new file mode 100644
index 00000000..3418d1f8
--- /dev/null
+++ b/androidx/widget/recyclerview/selection/StorageStrategy.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright 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 androidx.widget.recyclerview.selection;
+
+import static android.support.v4.util.Preconditions.checkArgument;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.annotation.VisibleForTesting;
+
+import java.util.ArrayList;
+
+/**
+ * Strategy for storing keys in saved state. Extend this class when using custom
+ * key types that aren't parcelable. Prefer use of builtin storage strategies:
+ * {@link #createStringStorage()}, {@link #createLongStorage()},
+ * {@link #createParcelableStorage(Class)}.
+ *
+ * @see androidx.widget.recyclerview.selection.SelectionTracker.Builder for more detailed
+ * advice on which key type to use for your selection keys.
+ *
+ * @param <K> Selection key type. Built in support is provided for String, Long, and Parcelable
+ * types. Use the respective factory method to create a StorageStrategy instance
+ * appropriate to the desired type.
+ * {@link #createStringStorage()},
+ * {@link #createParcelableStorage(Class)},
+ * {@link #createLongStorage()}
+ */
+public abstract class StorageStrategy<K> {
+
+ @VisibleForTesting
+ static final String SELECTION_ENTRIES = "androidx.widget.recyclerview.selection.entries";
+
+ @VisibleForTesting
+ static final String SELECTION_KEY_TYPE = "androidx.widget.recyclerview.selection.type";
+
+ private final Class<K> mType;
+
+ /**
+ * Creates a new instance.
+ *
+ * @param type the key type class that is being used.
+ */
+ public StorageStrategy(@NonNull Class<K> type) {
+ checkArgument(type != null);
+ mType = type;
+ }
+
+ /**
+ * Create a {@link Selection} from supplied {@link Bundle}.
+ *
+ * @param state Bundle instance that may contain parceled Selection instance.
+ * @return
+ */
+ public abstract @Nullable Selection<K> asSelection(@NonNull Bundle state);
+
+ /**
+ * Creates a {@link Bundle} from supplied {@link Selection}.
+ *
+ * @param selection The selection to asBundle.
+ * @return
+ */
+ public abstract @NonNull Bundle asBundle(@NonNull Selection<K> selection);
+
+ String getKeyTypeName() {
+ return mType.getCanonicalName();
+ }
+
+ /**
+ * @return StorageStrategy suitable for use with {@link Parcelable} keys
+ * (like {@link android.net.Uri}).
+ */
+ public static <K extends Parcelable> StorageStrategy<K> createParcelableStorage(Class<K> type) {
+ return new ParcelableStorageStrategy(type);
+ }
+
+ /**
+ * @return StorageStrategy suitable for use with {@link String} keys.
+ */
+ public static StorageStrategy<String> createStringStorage() {
+ return new StringStorageStrategy();
+ }
+
+ /**
+ * @return StorageStrategy suitable for use with {@link Long} keys.
+ */
+ public static StorageStrategy<Long> createLongStorage() {
+ return new LongStorageStrategy();
+ }
+
+ private static class StringStorageStrategy extends StorageStrategy<String> {
+
+ StringStorageStrategy() {
+ super(String.class);
+ }
+
+ @Override
+ public @Nullable Selection<String> asSelection(@NonNull Bundle state) {
+
+ String keyType = state.getString(SELECTION_KEY_TYPE, null);
+ if (keyType == null || !keyType.equals(getKeyTypeName())) {
+ return null;
+ }
+
+ @Nullable ArrayList<String> stored = state.getStringArrayList(SELECTION_ENTRIES);
+ if (stored == null) {
+ return null;
+ }
+
+ Selection<String> selection = new Selection<>();
+ selection.mSelection.addAll(stored);
+ return selection;
+ }
+
+ @Override
+ public @NonNull Bundle asBundle(@NonNull Selection<String> selection) {
+
+ Bundle bundle = new Bundle();
+
+ bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
+
+ ArrayList<String> value = new ArrayList<>(selection.size());
+ value.addAll(selection.mSelection);
+ bundle.putStringArrayList(SELECTION_ENTRIES, value);
+
+ return bundle;
+ }
+ }
+
+ private static class LongStorageStrategy extends StorageStrategy<Long> {
+
+ LongStorageStrategy() {
+ super(Long.class);
+ }
+
+ @Override
+ public @Nullable Selection<Long> asSelection(@NonNull Bundle state) {
+ String keyType = state.getString(SELECTION_KEY_TYPE, null);
+ if (keyType == null || !keyType.equals(getKeyTypeName())) {
+ return null;
+ }
+
+ @Nullable long[] stored = state.getLongArray(SELECTION_ENTRIES);
+ if (stored == null) {
+ return null;
+ }
+
+ Selection<Long> selection = new Selection<>();
+ for (long key : stored) {
+ selection.mSelection.add(key);
+ }
+ return selection;
+ }
+
+ @Override
+ public @NonNull Bundle asBundle(@NonNull Selection<Long> selection) {
+
+ Bundle bundle = new Bundle();
+ bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
+
+ long[] value = new long[selection.size()];
+ int i = 0;
+ for (Long key : selection) {
+ value[i++] = key;
+ }
+ bundle.putLongArray(SELECTION_ENTRIES, value);
+
+ return bundle;
+ }
+ }
+
+ private static class ParcelableStorageStrategy<K extends Parcelable>
+ extends StorageStrategy<K> {
+
+ ParcelableStorageStrategy(Class<K> type) {
+ super(type);
+ checkArgument(Parcelable.class.isAssignableFrom(type));
+ }
+
+ @Override
+ public @Nullable Selection<K> asSelection(@NonNull Bundle state) {
+
+ String keyType = state.getString(SELECTION_KEY_TYPE, null);
+ if (keyType == null || !keyType.equals(getKeyTypeName())) {
+ return null;
+ }
+
+ @Nullable ArrayList<K> stored = state.getParcelableArrayList(SELECTION_ENTRIES);
+ if (stored == null) {
+ return null;
+ }
+
+ Selection<K> selection = new Selection<>();
+ selection.mSelection.addAll(stored);
+ return selection;
+ }
+
+ @Override
+ public @NonNull Bundle asBundle(@NonNull Selection<K> selection) {
+
+ Bundle bundle = new Bundle();
+ bundle.putString(SELECTION_KEY_TYPE, getKeyTypeName());
+
+ ArrayList<K> value = new ArrayList<>(selection.size());
+ value.addAll(selection.mSelection);
+ bundle.putParcelableArrayList(SELECTION_ENTRIES, value);
+
+ return bundle;
+ }
+ }
+}
diff --git a/androidx/recyclerview/selection/ToolHandlerRegistry.java b/androidx/widget/recyclerview/selection/ToolHandlerRegistry.java
index c7355295..d474964a 100644
--- a/androidx/recyclerview/selection/ToolHandlerRegistry.java
+++ b/androidx/widget/recyclerview/selection/ToolHandlerRegistry.java
@@ -14,11 +14,12 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.v4.util.Preconditions.checkArgument;
import static android.support.v4.util.Preconditions.checkState;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.MotionEvent;
@@ -26,7 +27,11 @@ import java.util.Arrays;
import java.util.List;
/**
- * Registry for tool specific event handler.
+ * Registry for tool specific event handler. This provides map like functionality,
+ * along with fallback to a default handler, while avoiding auto-boxing of tool
+ * type values that would be necessitated where a Map used.
+ *
+ * @param <T> type of item being registered.
*/
final class ToolHandlerRegistry<T> {
@@ -39,7 +44,7 @@ final class ToolHandlerRegistry<T> {
private final List<T> mHandlers = Arrays.asList(null, null, null, null, null);
private final T mDefault;
- ToolHandlerRegistry(T defaultDelegate) {
+ ToolHandlerRegistry(@NonNull T defaultDelegate) {
checkArgument(defaultDelegate != null);
mDefault = defaultDelegate;
@@ -61,7 +66,7 @@ final class ToolHandlerRegistry<T> {
mHandlers.set(toolType, delegate);
}
- T get(MotionEvent e) {
+ T get(@NonNull MotionEvent e) {
T d = mHandlers.get(e.getToolType(0));
return d != null ? d : mDefault;
}
diff --git a/androidx/recyclerview/selection/TouchEventRouter.java b/androidx/widget/recyclerview/selection/TouchEventRouter.java
index fbbca238..bfb5ef22 100644
--- a/androidx/recyclerview/selection/TouchEventRouter.java
+++ b/androidx/widget/recyclerview/selection/TouchEventRouter.java
@@ -14,10 +14,11 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.v4.util.Preconditions.checkArgument;
+import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.RecyclerView.OnItemTouchListener;
import android.view.GestureDetector;
@@ -28,7 +29,8 @@ import android.view.MotionEvent;
* and if not handled by a handler, on to a {@link GestureDetector} for further
* processing.
*
- * <p>TouchEventRouter takes its name from
+ * <p>
+ * TouchEventRouter takes its name from
* {@link RecyclerView#addOnItemTouchListener(OnItemTouchListener)}. Despite "Touch"
* being in the name, it receives MotionEvents for all types of tools.
*/
@@ -39,7 +41,9 @@ final class TouchEventRouter implements OnItemTouchListener {
private final GestureDetector mDetector;
private final ToolHandlerRegistry<OnItemTouchListener> mDelegates;
- TouchEventRouter(GestureDetector detector, OnItemTouchListener defaultDelegate) {
+ TouchEventRouter(
+ @NonNull GestureDetector detector, @NonNull OnItemTouchListener defaultDelegate) {
+
checkArgument(detector != null);
checkArgument(defaultDelegate != null);
@@ -47,19 +51,22 @@ final class TouchEventRouter implements OnItemTouchListener {
mDelegates = new ToolHandlerRegistry<>(defaultDelegate);
}
- TouchEventRouter(GestureDetector detector) {
+ TouchEventRouter(@NonNull GestureDetector detector) {
this(
detector,
// Supply a fallback listener does nothing...because the caller
// didn't supply a fallback.
new OnItemTouchListener() {
@Override
- public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ public boolean onInterceptTouchEvent(
+ @NonNull RecyclerView unused, @NonNull MotionEvent e) {
+
return false;
}
@Override
- public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ public void onTouchEvent(
+ @NonNull RecyclerView unused, @NonNull MotionEvent e) {
}
@Override
@@ -73,13 +80,13 @@ final class TouchEventRouter implements OnItemTouchListener {
* @param delegate An {@link OnItemTouchListener} to receive events
* of {@code toolType}.
*/
- void register(int toolType, OnItemTouchListener delegate) {
+ void register(int toolType, @NonNull OnItemTouchListener delegate) {
checkArgument(delegate != null);
mDelegates.set(toolType, delegate);
}
@Override
- public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent e) {
+ public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
boolean handled = mDelegates.get(e).onInterceptTouchEvent(rv, e);
// Forward all events to UserInputHandler.
@@ -91,7 +98,7 @@ final class TouchEventRouter implements OnItemTouchListener {
}
@Override
- public void onTouchEvent(RecyclerView rv, MotionEvent e) {
+ public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
mDelegates.get(e).onTouchEvent(rv, e);
// Note: even though this event is being handled as part of gestures such as drag and band,
diff --git a/androidx/recyclerview/selection/TouchInputHandler.java b/androidx/widget/recyclerview/selection/TouchInputHandler.java
index e07aeb19..20b75cb1 100644
--- a/androidx/recyclerview/selection/TouchInputHandler.java
+++ b/androidx/widget/recyclerview/selection/TouchInputHandler.java
@@ -14,21 +14,24 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.v4.util.Preconditions.checkArgument;
+import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.MotionEvent;
-import androidx.recyclerview.selection.ItemDetailsLookup.ItemDetails;
-import androidx.recyclerview.selection.SelectionHelper.SelectionPredicate;
+import androidx.widget.recyclerview.selection.ItemDetailsLookup.ItemDetails;
+import androidx.widget.recyclerview.selection.SelectionTracker.SelectionPredicate;
/**
* A MotionInputHandler that provides the high-level glue for touch driven selection. This class
* works with {@link RecyclerView}, {@link GestureRouter}, and {@link GestureSelectionHelper} to
- * provide robust user drive selection support.
+ * to implement the primary policies around touch input.
+ *
+ * @param <K> Selection key type. @see {@link StorageStrategy} for supported types.
*/
final class TouchInputHandler<K> extends MotionInputHandler<K> {
@@ -37,44 +40,44 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
private final ItemDetailsLookup<K> mDetailsLookup;
private final SelectionPredicate<K> mSelectionPredicate;
- private final ActivationCallbacks<K> mActivationCallbacks;
- private final TouchCallbacks mTouchCallbacks;
+ private final OnItemActivatedListener<K> mOnItemActivatedListener;
+ private final OnDragInitiatedListener mOnDragInitiatedListener;
private final Runnable mGestureStarter;
private final Runnable mHapticPerformer;
TouchInputHandler(
- SelectionHelper<K> selectionHelper,
- ItemKeyProvider<K> keyProvider,
- ItemDetailsLookup<K> detailsLookup,
- SelectionPredicate<K> selectionPredicate,
- Runnable gestureStarter,
- TouchCallbacks touchCallbacks,
- ActivationCallbacks<K> activationCallbacks,
- FocusCallbacks<K> focusCallbacks,
- Runnable hapticPerformer) {
-
- super(selectionHelper, keyProvider, focusCallbacks);
+ @NonNull SelectionTracker<K> selectionTracker,
+ @NonNull ItemKeyProvider<K> keyProvider,
+ @NonNull ItemDetailsLookup<K> detailsLookup,
+ @NonNull SelectionPredicate<K> selectionPredicate,
+ @NonNull Runnable gestureStarter,
+ @NonNull OnDragInitiatedListener onDragInitiatedListener,
+ @NonNull OnItemActivatedListener<K> onItemActivatedListener,
+ @NonNull FocusDelegate<K> focusDelegate,
+ @NonNull Runnable hapticPerformer) {
+
+ super(selectionTracker, keyProvider, focusDelegate);
checkArgument(detailsLookup != null);
checkArgument(selectionPredicate != null);
checkArgument(gestureStarter != null);
- checkArgument(activationCallbacks != null);
- checkArgument(touchCallbacks != null);
+ checkArgument(onItemActivatedListener != null);
+ checkArgument(onDragInitiatedListener != null);
checkArgument(hapticPerformer != null);
mDetailsLookup = detailsLookup;
mSelectionPredicate = selectionPredicate;
mGestureStarter = gestureStarter;
- mActivationCallbacks = activationCallbacks;
- mTouchCallbacks = touchCallbacks;
+ mOnItemActivatedListener = onItemActivatedListener;
+ mOnDragInitiatedListener = onDragInitiatedListener;
mHapticPerformer = hapticPerformer;
}
@Override
- public boolean onSingleTapUp(MotionEvent e) {
+ public boolean onSingleTapUp(@NonNull MotionEvent e) {
if (!mDetailsLookup.overItemWithSelectionKey(e)) {
if (DEBUG) Log.d(TAG, "Tap not associated w/ model item. Clearing selection.");
- mSelectionHelper.clearSelection();
+ mSelectionTracker.clearSelection();
return false;
}
@@ -84,11 +87,11 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
return false;
}
- if (mSelectionHelper.hasSelection()) {
+ if (mSelectionTracker.hasSelection()) {
if (isRangeExtension(e)) {
extendSelectionRange(item);
- } else if (mSelectionHelper.isSelected(item.getSelectionKey())) {
- mSelectionHelper.deselect(item.getSelectionKey());
+ } else if (mSelectionTracker.isSelected(item.getSelectionKey())) {
+ mSelectionTracker.deselect(item.getSelectionKey());
} else {
selectItem(item);
}
@@ -100,11 +103,11 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
// otherwise they activate.
return item.inSelectionHotspot(e)
? selectItem(item)
- : mActivationCallbacks.onItemActivated(item, e);
+ : mOnItemActivatedListener.onItemActivated(item, e);
}
@Override
- public void onLongPress(MotionEvent e) {
+ public void onLongPress(@NonNull MotionEvent e) {
if (!mDetailsLookup.overItemWithSelectionKey(e)) {
if (DEBUG) Log.d(TAG, "Ignoring LongPress on non-model-backed item.");
return;
@@ -122,7 +125,7 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
extendSelectionRange(item);
handled = true;
} else {
- if (!mSelectionHelper.isSelected(item.getSelectionKey())
+ if (!mSelectionTracker.isSelected(item.getSelectionKey())
&& mSelectionPredicate.canSetStateForKey(item.getSelectionKey(), true)) {
// If we cannot select it, we didn't apply anchoring - therefore should not
// start gesture selection
@@ -137,7 +140,7 @@ final class TouchInputHandler<K> extends MotionInputHandler<K> {
} else {
// We only initiate drag and drop on long press for touch to allow regular
// touch-based scrolling
- mTouchCallbacks.onDragInitiated(e);
+ mOnDragInitiatedListener.onDragInitiated(e);
handled = true;
}
}
diff --git a/androidx/recyclerview/selection/ViewAutoScroller.java b/androidx/widget/recyclerview/selection/ViewAutoScroller.java
index d13b0f24..39385082 100644
--- a/androidx/recyclerview/selection/ViewAutoScroller.java
+++ b/androidx/widget/recyclerview/selection/ViewAutoScroller.java
@@ -14,15 +14,16 @@
* limitations under the License.
*/
-package androidx.recyclerview.selection;
+package androidx.widget.recyclerview.selection;
import static android.support.v4.util.Preconditions.checkArgument;
import static android.support.v4.util.Preconditions.checkState;
-import static androidx.recyclerview.selection.Shared.DEBUG;
-import static androidx.recyclerview.selection.Shared.VERBOSE;
+import static androidx.widget.recyclerview.selection.Shared.DEBUG;
+import static androidx.widget.recyclerview.selection.Shared.VERBOSE;
import android.graphics.Point;
+import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.VisibleForTesting;
import android.support.v4.view.ViewCompat;
@@ -51,12 +52,12 @@ final class ViewAutoScroller extends AutoScroller {
private @Nullable Point mLastLocation;
private boolean mPassedInitialMotionThreshold;
- ViewAutoScroller(ScrollHost scrollHost) {
+ ViewAutoScroller(@NonNull ScrollHost scrollHost) {
this(scrollHost, DEFAULT_SCROLL_THRESHOLD_RATIO);
}
@VisibleForTesting
- ViewAutoScroller(ScrollHost scrollHost, float scrollThresholdRatio) {
+ ViewAutoScroller(@NonNull ScrollHost scrollHost, float scrollThresholdRatio) {
checkArgument(scrollHost != null);
@@ -72,7 +73,7 @@ final class ViewAutoScroller extends AutoScroller {
}
@Override
- protected void reset() {
+ public void reset() {
mHost.removeCallback(mRunner);
mOrigin = null;
mLastLocation = null;
@@ -80,7 +81,7 @@ final class ViewAutoScroller extends AutoScroller {
}
@Override
- protected void scroll(Point location) {
+ public void scroll(@NonNull Point location) {
mLastLocation = location;
// See #aboveMotionThreshold for details on how we track initial location.
@@ -152,7 +153,7 @@ final class ViewAutoScroller extends AutoScroller {
mHost.runAtNextFrame(mRunner);
}
- private boolean aboveMotionThreshold(Point location) {
+ private boolean aboveMotionThreshold(@NonNull Point location) {
// We reuse the scroll threshold to calculate a much smaller area
// in which we ignore motion initially.
int motionThreshold =
@@ -224,16 +225,16 @@ final class ViewAutoScroller extends AutoScroller {
/**
* @param r schedule runnable to be run at next convenient time.
*/
- abstract void runAtNextFrame(Runnable r);
+ abstract void runAtNextFrame(@NonNull Runnable r);
/**
* @param r remove runnable from being run.
*/
- abstract void removeCallback(Runnable r);
+ abstract void removeCallback(@NonNull Runnable r);
}
- public static ScrollHost createScrollHost(final RecyclerView view) {
- return new RuntimeHost(view);
+ static ScrollHost createScrollHost(final RecyclerView recyclerView) {
+ return new RuntimeHost(recyclerView);
}
/**
@@ -241,31 +242,31 @@ final class ViewAutoScroller extends AutoScroller {
*/
private static final class RuntimeHost extends ScrollHost {
- private final RecyclerView mRecView;
+ private final RecyclerView mRecyclerView;
- RuntimeHost(RecyclerView recView) {
- mRecView = recView;
+ RuntimeHost(@NonNull RecyclerView recyclerView) {
+ mRecyclerView = recyclerView;
}
@Override
- void runAtNextFrame(Runnable r) {
- ViewCompat.postOnAnimation(mRecView, r);
+ void runAtNextFrame(@NonNull Runnable r) {
+ ViewCompat.postOnAnimation(mRecyclerView, r);
}
@Override
- void removeCallback(Runnable r) {
- mRecView.removeCallbacks(r);
+ void removeCallback(@NonNull Runnable r) {
+ mRecyclerView.removeCallbacks(r);
}
@Override
void scrollBy(int dy) {
if (VERBOSE) Log.v(TAG, "Scrolling view by: " + dy);
- mRecView.scrollBy(0, dy);
+ mRecyclerView.scrollBy(0, dy);
}
@Override
int getViewHeight() {
- return mRecView.getHeight();
+ return mRecyclerView.getHeight();
}
}
}
diff --git a/benchmarks/regression/CharsetUtf8Benchmark.java b/benchmarks/regression/CharsetUtf8Benchmark.java
new file mode 100644
index 00000000..041e4355
--- /dev/null
+++ b/benchmarks/regression/CharsetUtf8Benchmark.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.regression;
+
+import android.icu.lang.UCharacter;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Decode the same size of ASCII, BMP, Supplementary character using fast-path UTF-8 decoder.
+ * The fast-path code is in {@link StringFactory#newStringFromBytes(byte[], int, int, Charset)}
+ */
+public class CharsetUtf8Benchmark {
+
+ private static final int NO_OF_BYTES = 0x400000; // 4MB
+ private static final byte[] ASCII = makeUnicodeRange(0, 0x7f, NO_OF_BYTES / 0x80);
+ private static final byte[] BMP2 = makeUnicodeRange(0x0080, 0x07ff, NO_OF_BYTES / 2 / 0x780);
+ private static final byte[] BMP3 = makeUnicodeRange(0x0800, 0xffff,
+ NO_OF_BYTES / 3 / 0xf000 /* 0x10000 - 0x0800 - no of surrogate code points */);
+ private static final byte[] SUPPLEMENTARY = makeUnicodeRange(0x10000, 0x10ffff,
+ NO_OF_BYTES / 4 / 0x100000);
+
+ private static byte[] makeUnicodeRange(int startingCodePoint, int endingCodePoint,
+ int repeated) {
+ StringBuilder builder = new StringBuilder();
+ for (int codePoint = startingCodePoint; codePoint <= endingCodePoint; codePoint++) {
+ if (codePoint < Character.MIN_SURROGATE || codePoint > Character.MAX_SURROGATE) {
+ builder.append(UCharacter.toString(codePoint));
+ }
+ }
+
+ String str = builder.toString();
+ builder = new StringBuilder();
+ for (int i = 0; i < repeated; i++) {
+ builder.append(str);
+ }
+ return builder.toString().getBytes();
+ }
+
+ public void time_ascii() {
+ new String(ASCII, StandardCharsets.UTF_8);
+ }
+
+ public void time_bmp2() {
+ new String(BMP2, StandardCharsets.UTF_8);
+ }
+
+ public void time_bmp3() {
+ new String(BMP3, StandardCharsets.UTF_8);
+ }
+
+ public void time_supplementary() {
+ new String(SUPPLEMENTARY, StandardCharsets.UTF_8);
+ }
+}
diff --git a/com/android/car/setupwizardlib/CarSetupWizardLayout.java b/com/android/car/setupwizardlib/CarSetupWizardLayout.java
index 9d754f6d..058e0230 100644
--- a/com/android/car/setupwizardlib/CarSetupWizardLayout.java
+++ b/com/android/car/setupwizardlib/CarSetupWizardLayout.java
@@ -31,15 +31,16 @@ import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
+import java.util.Locale;
+
/**
- * 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
+ * Custom layout for the Car Setup Wizard. Provides accessors for modifying elements such as buttons
+ * and progress bars. 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;
-
private TextView mToolbarTitle;
/* <p>The Primary Toolbar Button should always be used when there is only a single action that
@@ -50,14 +51,13 @@ public class CarSetupWizardLayout extends LinearLayout {
* while the Secondary is used for the negative action.</p>
*/
private Button mPrimaryToolbarButton;
+
/*
- * Flag to track the flat state.
+ * Flag to track the primary toolbar button flat state.
*/
private boolean mPrimaryToolbarButtonFlat;
private View.OnClickListener mPrimaryToolbarButtonOnClick;
private Button mSecondaryToolbarButton;
-
-
private ProgressBar mProgressBar;
public CarSetupWizardLayout(Context context) {
@@ -106,6 +106,7 @@ public class CarSetupWizardLayout extends LinearLayout {
boolean secondaryToolbarButtonEnabled;
boolean showProgressBar;
+ boolean indeterminateProgressBar;
try {
showBackButton = attrArray.getBoolean(
@@ -130,6 +131,8 @@ public class CarSetupWizardLayout extends LinearLayout {
R.styleable.CarSetupWizardLayout_secondaryToolbarButtonEnabled, true);
showProgressBar = attrArray.getBoolean(
R.styleable.CarSetupWizardLayout_showProgressBar, false);
+ indeterminateProgressBar = attrArray.getBoolean(
+ R.styleable.CarSetupWizardLayout_indeterminateProgressBar, true);
} finally {
attrArray.recycle();
}
@@ -178,6 +181,7 @@ public class CarSetupWizardLayout extends LinearLayout {
mProgressBar = findViewById(R.id.progress_bar);
setProgressBarVisible(showProgressBar);
+ setProgressBarIndeterminate(indeterminateProgressBar);
// Set orientation programmatically since the inflated layout uses <merge>
setOrientation(LinearLayout.VERTICAL);
@@ -203,36 +207,33 @@ public class CarSetupWizardLayout extends LinearLayout {
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);
- }
+ this.post(() -> {
+ 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 {
@@ -252,6 +253,13 @@ public class CarSetupWizardLayout extends LinearLayout {
}
/**
+ * Getter for the back button
+ */
+ public View getBackButton() {
+ return mBackButton;
+ }
+
+ /**
* Sets the header title visibility to given value.
*/
public void setToolbarTitleVisible(boolean visible) {
@@ -266,6 +274,13 @@ public class CarSetupWizardLayout extends LinearLayout {
}
/**
+ * Getter for the toolbar title
+ */
+ public TextView getToolbarTitle() {
+ return mToolbarTitle;
+ }
+
+ /**
* Set whether the primary continue button is enabled.
*/
public void setPrimaryToolbarButtonEnabled(boolean enabled) {
@@ -330,6 +345,14 @@ public class CarSetupWizardLayout extends LinearLayout {
}
/**
+ * Getter for the primary toolbar button
+ */
+ public Button getPrimaryToolbarButton() {
+ return mPrimaryToolbarButton;
+ }
+
+
+ /**
* Set whether the secondary continue button is enabled.
*/
public void setSecondaryToolbarButtonEnabled(boolean enabled) {
@@ -367,6 +390,13 @@ public class CarSetupWizardLayout extends LinearLayout {
}
/**
+ * Getter for the secondary toolbar button
+ */
+ public Button getSecondaryToolbarButton() {
+ return mSecondaryToolbarButton;
+ }
+
+ /**
* Set the progress bar visibility to the given visibility.
*/
public void setProgressBarVisible(boolean visible) {
@@ -374,17 +404,58 @@ public class CarSetupWizardLayout extends LinearLayout {
}
/**
+ * Set the progress bar indeterminate/determinate state.
+ */
+ public void setProgressBarIndeterminate(boolean indeterminate) {
+ mProgressBar.setIndeterminate(indeterminate);
+ }
+
+ /**
+ * Set the progress bar's progress.
+ */
+ public void setProgressBarProgress(int progress) {
+ setProgressBarIndeterminate(false);
+ mProgressBar.setProgress(progress);
+ }
+
+ /**
+ * Getter for the progress bar
+ */
+ public ProgressBar getProgressBar() {
+ return mProgressBar;
+ }
+
+ /**
+ * Sets the locale to be used for rendering.
+ */
+ public void applyLocale(Locale locale) {
+ if (locale == null) {
+ return;
+ }
+ int direction = TextUtils.getLayoutDirectionFromLocale(locale);
+ setLayoutDirection(direction);
+
+ mToolbarTitle.setTextLocale(locale);
+ mToolbarTitle.setLayoutDirection(direction);
+
+ mPrimaryToolbarButton.setTextLocale(locale);
+ mPrimaryToolbarButton.setLayoutDirection(direction);
+
+ mSecondaryToolbarButton.setTextLocale(locale);
+ mSecondaryToolbarButton.setLayoutDirection(direction);
+ }
+
+ /**
* 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);
+ ViewStub secondaryToolbarButtonStub = 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);
+ mSecondaryToolbarButton = 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
index 72d5a5ae..65ba6d48 100644
--- a/com/android/car/setupwizardlib/util/CarWizardManagerHelper.java
+++ b/com/android/car/setupwizardlib/util/CarWizardManagerHelper.java
@@ -20,12 +20,15 @@ import android.content.Context;
import android.content.Intent;
import android.provider.Settings;
+import java.util.Arrays;
+
/**
* <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";
+ static final String EXTRA_IS_DEALER = "dealer";
private static final String ACTION_NEXT = "com.android.wizard.NEXT";
private static final String EXTRA_RESULT_CODE = "com.android.setupwizard.ResultCode";
@@ -80,6 +83,8 @@ public final class CarWizardManagerHelper {
dstIntent.putExtra(EXTRA_WIZARD_BUNDLE, srcIntent.getBundleExtra(EXTRA_WIZARD_BUNDLE));
dstIntent.putExtra(EXTRA_IS_FIRST_RUN,
srcIntent.getBooleanExtra(EXTRA_IS_FIRST_RUN, false));
+ dstIntent.putExtra(EXTRA_IS_DEALER,
+ srcIntent.getBooleanExtra(EXTRA_IS_DEALER, false));
}
/**
@@ -94,6 +99,17 @@ public final class CarWizardManagerHelper {
}
/**
+ * Check whether an intent is intended for the dealer.
+ *
+ * @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 isDealerIntent(Intent intent) {
+ return intent.getBooleanExtra(EXTRA_IS_DEALER, 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.
diff --git a/com/android/commands/bmgr/Bmgr.java b/com/android/commands/bmgr/Bmgr.java
index b61cdecb..e87a78e2 100644
--- a/com/android/commands/bmgr/Bmgr.java
+++ b/com/android/commands/bmgr/Bmgr.java
@@ -23,8 +23,8 @@ import android.app.backup.IBackupManager;
import android.app.backup.IBackupObserver;
import android.app.backup.IRestoreObserver;
import android.app.backup.IRestoreSession;
-import android.app.backup.RestoreSet;
import android.app.backup.ISelectBackupTransportCallback;
+import android.app.backup.RestoreSet;
import android.content.ComponentName;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
@@ -37,6 +37,7 @@ import android.util.ArraySet;
import com.android.internal.annotations.GuardedBy;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.concurrent.CountDownLatch;
@@ -339,18 +340,16 @@ public final class Bmgr {
System.err.println(PM_NOT_RUNNING_ERR);
}
if (installedPackages != null) {
- List<String> packages = new ArrayList<>();
- for (PackageInfo pi : installedPackages) {
- try {
- if (mBmgr.isAppEligibleForBackup(pi.packageName)) {
- packages.add(pi.packageName);
- }
- } catch (RemoteException e) {
- System.err.println(e.toString());
- System.err.println(BMGR_NOT_RUNNING_ERR);
- }
+ String[] packages =
+ installedPackages.stream().map(p -> p.packageName).toArray(String[]::new);
+ String[] filteredPackages = {};
+ try {
+ filteredPackages = mBmgr.filterAppsEligibleForBackup(packages);
+ } catch (RemoteException e) {
+ System.err.println(e.toString());
+ System.err.println(BMGR_NOT_RUNNING_ERR);
}
- backupNowPackages(packages, nonIncrementalBackup);
+ backupNowPackages(Arrays.asList(filteredPackages), nonIncrementalBackup);
}
}
diff --git a/com/android/commands/sm/Sm.java b/com/android/commands/sm/Sm.java
index 77e8efaf..2bb7edcc 100644
--- a/com/android/commands/sm/Sm.java
+++ b/com/android/commands/sm/Sm.java
@@ -16,16 +16,10 @@
package com.android.commands.sm;
-import static android.os.storage.StorageManager.PROP_ADOPTABLE_FBE;
-import static android.os.storage.StorageManager.PROP_HAS_ADOPTABLE;
-import static android.os.storage.StorageManager.PROP_VIRTUAL_DISK;
-
-import android.os.IBinder;
import android.os.IVoldTaskListener;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.ServiceManager;
-import android.os.SystemProperties;
import android.os.storage.DiskInfo;
import android.os.storage.IStorageManager;
import android.os.storage.StorageManager;
@@ -145,15 +139,7 @@ public final class Sm {
}
public void runHasAdoptable() {
- final boolean hasHardware = SystemProperties.getBoolean(PROP_HAS_ADOPTABLE, false)
- || SystemProperties.getBoolean(PROP_VIRTUAL_DISK, false);
- final boolean hasSoftware;
- if (StorageManager.isFileEncryptedNativeOnly()) {
- hasSoftware = SystemProperties.getBoolean(PROP_ADOPTABLE_FBE, false);
- } else {
- hasSoftware = true;
- }
- System.out.println(hasHardware && hasSoftware);
+ System.out.println(StorageManager.hasAdoptable());
}
public void runGetPrimaryStorageUuid() throws RemoteException {
diff --git a/com/android/commands/svc/UsbCommand.java b/com/android/commands/svc/UsbCommand.java
index adbe9d01..34f6d7de 100644
--- a/com/android/commands/svc/UsbCommand.java
+++ b/com/android/commands/svc/UsbCommand.java
@@ -18,6 +18,7 @@ package com.android.commands.svc;
import android.content.Context;
import android.hardware.usb.IUsbManager;
+import android.hardware.usb.UsbManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
@@ -38,6 +39,9 @@ public class UsbCommand extends Svc.Command {
+ "\n"
+ "usage: svc usb setFunction [function] [usbDataUnlocked=false]\n"
+ " Set the current usb function and optionally the data lock state.\n\n"
+ + " svc usb setScreenUnlockedFunctions [function]\n"
+ + " Sets the functions which, if the device was charging,"
+ + " become current on screen unlock.\n"
+ " svc usb getFunction\n"
+ " Gets the list of currently enabled functions\n";
}
@@ -62,6 +66,16 @@ public class UsbCommand extends Svc.Command {
} else if ("getFunction".equals(args[1])) {
System.err.println(SystemProperties.get("sys.usb.config"));
return;
+ } else if ("setScreenUnlockedFunctions".equals(args[1])) {
+ IUsbManager usbMgr = IUsbManager.Stub.asInterface(ServiceManager.getService(
+ Context.USB_SERVICE));
+ try {
+ usbMgr.setScreenUnlockedFunctions((args.length >= 3 ? args[2] :
+ UsbManager.USB_FUNCTION_NONE));
+ } catch (RemoteException e) {
+ System.err.println("Error communicating with UsbManager: " + e);
+ }
+ return;
}
}
System.err.println(longHelp());
diff --git a/com/android/externalstorage/ExternalStorageProvider.java b/com/android/externalstorage/ExternalStorageProvider.java
index f844cc16..2a82fc9b 100644
--- a/com/android/externalstorage/ExternalStorageProvider.java
+++ b/com/android/externalstorage/ExternalStorageProvider.java
@@ -19,7 +19,6 @@ package com.android.externalstorage;
import android.annotation.Nullable;
import android.app.usage.StorageStatsManager;
import android.content.ContentResolver;
-import android.content.Context;
import android.content.UriPermission;
import android.database.Cursor;
import android.database.MatrixCursor;
@@ -28,7 +27,9 @@ import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
import android.os.Environment;
+import android.os.IBinder;
import android.os.UserHandle;
+import android.os.UserManager;
import android.os.storage.DiskInfo;
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
@@ -95,6 +96,7 @@ public class ExternalStorageProvider extends FileSystemProvider {
private static final String ROOT_ID_HOME = "home";
private StorageManager mStorageManager;
+ private UserManager mUserManager;
private final Object mRootsLock = new Object();
@@ -105,12 +107,35 @@ public class ExternalStorageProvider extends FileSystemProvider {
public boolean onCreate() {
super.onCreate(DEFAULT_DOCUMENT_PROJECTION);
- mStorageManager = (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
+ mStorageManager = getContext().getSystemService(StorageManager.class);
+ mUserManager = getContext().getSystemService(UserManager.class);
updateVolumes();
return true;
}
+ private void enforceShellRestrictions() {
+ if (UserHandle.getCallingAppId() == android.os.Process.SHELL_UID
+ && mUserManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER)) {
+ throw new SecurityException(
+ "Shell user cannot access files for user " + UserHandle.myUserId());
+ }
+ }
+
+ @Override
+ protected int enforceReadPermissionInner(Uri uri, String callingPkg, IBinder callerToken)
+ throws SecurityException {
+ enforceShellRestrictions();
+ return super.enforceReadPermissionInner(uri, callingPkg, callerToken);
+ }
+
+ @Override
+ protected int enforceWritePermissionInner(Uri uri, String callingPkg, IBinder callerToken)
+ throws SecurityException {
+ enforceShellRestrictions();
+ return super.enforceWritePermissionInner(uri, callingPkg, callerToken);
+ }
+
public void updateVolumes() {
synchronized (mRootsLock) {
updateVolumesLocked();
diff --git a/com/android/ims/ImsCallProfile.java b/com/android/ims/ImsCallProfile.java
index 489c208a..693aaff8 100644
--- a/com/android/ims/ImsCallProfile.java
+++ b/com/android/ims/ImsCallProfile.java
@@ -351,7 +351,7 @@ public class ImsCallProfile implements Parcelable {
mServiceType = in.readInt();
mCallType = in.readInt();
mCallExtras = in.readBundle();
- mMediaProfile = in.readParcelable(null);
+ mMediaProfile = in.readParcelable(ImsStreamMediaProfile.class.getClassLoader());
}
public static final Creator<ImsCallProfile> CREATOR = new Creator<ImsCallProfile>() {
diff --git a/com/android/ims/ImsConnectionStateListener.java b/com/android/ims/ImsConnectionStateListener.java
index 4425854b..216bc410 100644
--- a/com/android/ims/ImsConnectionStateListener.java
+++ b/com/android/ims/ImsConnectionStateListener.java
@@ -17,15 +17,42 @@
package com.android.ims;
import android.net.Uri;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
/**
* Listener for receiving notifications about changes to the IMS connection.
* It provides a state of IMS registration between UE and IMS network, the service
* availability of the local device during IMS registered.
- *
+ * @Deprecated Use {@link ImsRegistrationImplBase.Callback} instead.
* @hide
*/
-public class ImsConnectionStateListener {
+public class ImsConnectionStateListener extends ImsRegistrationImplBase.Callback {
+
+ @Override
+ public final void onRegistered(@ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech) {
+ onImsConnected(imsRadioTech);
+ }
+
+ @Override
+ public final void onRegistering(@ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech) {
+ onImsProgressing(imsRadioTech);
+ }
+
+ @Override
+ public final void onDeregistered(ImsReasonInfo info) {
+ onImsDisconnected(info);
+ }
+
+ @Override
+ public final void onTechnologyChangeFailed(
+ @ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech, ImsReasonInfo info) {
+ onRegistrationChangeFailed(imsRadioTech, info);
+ }
+
+ @Override
+ public void onSubscriberAssociatedUriChanged(Uri[] uris) {
+ registrationAssociatedUriChanged(uris);
+ }
/**
* Called when the device is connected to the IMS network with {@param imsRadioTech}.
*/
@@ -50,6 +77,7 @@ public class ImsConnectionStateListener {
/**
* Called when its suspended IMS connection is resumed, meaning the connection
* now allows throughput.
+ * @deprecated not used in newer IMS provider implementations.
*/
public void onImsResumed() {
// no-op
@@ -57,6 +85,7 @@ public class ImsConnectionStateListener {
/**
* Called when its current IMS connection is suspended, meaning there is no data throughput.
+ * @deprecated not used in newer IMS provider implementations.
*/
public void onImsSuspended() {
// no-op
@@ -64,6 +93,7 @@ public class ImsConnectionStateListener {
/**
* Called when its current IMS connection feature capability changes.
+ * @deprecated Not used in newer IMS provider implementations.
*/
public void onFeatureCapabilityChanged(int serviceClass,
int[] enabledFeatures, int[] disabledFeatures) {
@@ -72,6 +102,7 @@ public class ImsConnectionStateListener {
/**
* Called when waiting voice message count changes.
+ * @deprecated not used in newer IMS provider implementations.
*/
public void onVoiceMessageCountChanged(int count) {
// no-op
diff --git a/com/android/ims/ImsManager.java b/com/android/ims/ImsManager.java
index e991a5dd..d8ada6fe 100644
--- a/com/android/ims/ImsManager.java
+++ b/com/android/ims/ImsManager.java
@@ -25,25 +25,27 @@ import android.os.Message;
import android.os.Parcel;
import android.os.PersistableBundle;
import android.os.RemoteException;
-import android.os.ServiceManager;
import android.os.SystemProperties;
import android.provider.Settings;
import android.telecom.TelecomManager;
import android.telephony.CarrierConfigManager;
+import android.telephony.ims.internal.feature.ImsFeature;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
import android.telephony.Rlog;
import android.telephony.ServiceState;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
-import android.telephony.ims.feature.ImsFeature;
import android.util.Log;
import com.android.ims.internal.IImsCallSession;
import com.android.ims.internal.IImsConfig;
import com.android.ims.internal.IImsEcbm;
-import com.android.ims.internal.IImsMMTelFeature;
import com.android.ims.internal.IImsMultiEndpoint;
+import com.android.ims.internal.IImsRegistration;
+import com.android.ims.internal.IImsRegistrationCallback;
import com.android.ims.internal.IImsRegistrationListener;
import com.android.ims.internal.IImsServiceController;
+import com.android.ims.internal.IImsSmsListener;
import com.android.ims.internal.IImsUt;
import com.android.ims.internal.ImsCallSession;
import com.android.internal.annotations.VisibleForTesting;
@@ -55,6 +57,7 @@ import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedDeque;
+import java.util.concurrent.CopyOnWriteArraySet;
/**
* Provides APIs for IMS services, such as initiating IMS calls, and provides access to
@@ -79,13 +82,6 @@ public class ImsManager {
public static final int PROPERTY_DBG_ALLOW_IMS_OFF_OVERRIDE_DEFAULT = 0;
/**
- * For accessing the IMS related service.
- * Internal use only.
- * @hide
- */
- private static final String IMS_SERVICE = "ims";
-
- /**
* The result code to be sent back with the incoming call {@link PendingIntent}.
* @see #open(PendingIntent, ImsConnectionStateListener)
*/
@@ -198,14 +194,20 @@ public class ImsManager {
private ImsMultiEndpoint mMultiEndpoint = null;
- private Set<ImsServiceProxy.INotifyStatusChanged> mStatusCallbacks = new HashSet<>();
+ private Set<ImsServiceProxy.IFeatureUpdate> mStatusCallbacks = new CopyOnWriteArraySet<>();
// Keep track of the ImsRegistrationListenerProxys that have been created so that we can
// remove them from the ImsService.
private final Set<ImsConnectionStateListener> mRegistrationListeners = new HashSet<>();
- private final ImsRegistrationListenerProxy mRegistrationListenerProxy =
+
+ // Used for compatibility with the old Registration method
+ // TODO: Remove once the compat layer is in place
+ private final ImsRegistrationListenerProxy mImsRegistrationListenerProxy =
new ImsRegistrationListenerProxy();
+ // New API for registration to the ImsService.
+ private final ImsRegistrationCallback mRegistrationCallback = new ImsRegistrationCallback();
+
// When true, we have registered the mRegistrationListenerProxy with the ImsService. Don't do
// it again.
@@ -1314,7 +1316,7 @@ public class ImsManager {
* Adds a callback for status changed events if the binder is already available. If it is not,
* this method will throw an ImsException.
*/
- public void addNotifyStatusChangedCallbackIfAvailable(ImsServiceProxy.INotifyStatusChanged c)
+ public void addNotifyStatusChangedCallbackIfAvailable(ImsServiceProxy.IFeatureUpdate c)
throws ImsException {
if (!mImsServiceProxy.isBinderAlive()) {
throw new ImsException("Binder is not active!",
@@ -1325,6 +1327,14 @@ public class ImsManager {
}
}
+ public void removeNotifyStatusChangedCallback(ImsServiceProxy.IFeatureUpdate c) {
+ if (c != null) {
+ mStatusCallbacks.remove(c);
+ } else {
+ Log.w(TAG, "removeNotifyStatusChangedCallback: callback is null!");
+ }
+ }
+
/**
* Opens the IMS service for making calls and/or receiving generic IMS calls.
* The caller may make subsquent calls through {@link #makeCall}.
@@ -1409,9 +1419,7 @@ public class ImsManager {
* @throws NullPointerException if {@code listener} is null
* @throws ImsException if calling the IMS service results in an error
*/
- public void addRegistrationListener(ImsConnectionStateListener listener)
- throws ImsException {
-
+ public void addRegistrationListener(ImsConnectionStateListener listener) throws ImsException {
if (listener == null) {
throw new NullPointerException("listener can't be null");
}
@@ -1420,8 +1428,13 @@ public class ImsManager {
if (!mHasRegisteredForProxy) {
try {
checkAndThrowExceptionIfServiceUnavailable();
- mImsServiceProxy.addRegistrationListener(mRegistrationListenerProxy);
- log("RegistrationListenerProxy registered.");
+ // TODO: Remove once new MmTelFeature is merged in
+ mImsServiceProxy.addRegistrationListener(mImsRegistrationListenerProxy);
+ IImsRegistration regBinder = mImsServiceProxy.getRegistration();
+ if (regBinder != null) {
+ regBinder.addRegistrationCallback(mRegistrationCallback);
+ }
+ log("Registration Callback/Listener registered.");
// Only record if there isn't a RemoteException.
mHasRegisteredForProxy = true;
} catch (RemoteException e) {
@@ -1861,53 +1874,31 @@ public class ImsManager {
*/
private void createImsService() {
if (!mConfigDynamicBind) {
- // Old method of binding
+ // Deprecated method of binding
Rlog.i(TAG, "Creating ImsService using ServiceManager");
- mImsServiceProxy = getServiceProxyCompat();
+ mImsServiceProxy = ImsServiceProxyCompat.create(mContext, mPhoneId, mDeathRecipient);
} else {
Rlog.i(TAG, "Creating ImsService using ImsResolver");
- mImsServiceProxy = getServiceProxy();
+ mImsServiceProxy = ImsServiceProxy.create(mContext, mPhoneId);
}
+ // Forwarding interface to tell mStatusCallbacks that the Proxy is unavailable.
+ mImsServiceProxy.setStatusCallback(new ImsServiceProxy.IFeatureUpdate() {
+ @Override
+ public void notifyStateChanged() {
+ mStatusCallbacks.forEach(ImsServiceProxy.IFeatureUpdate::notifyStateChanged);
+ }
+
+ @Override
+ public void notifyUnavailable() {
+ mStatusCallbacks.forEach(ImsServiceProxy.IFeatureUpdate::notifyUnavailable);
+ }
+ });
// We have created a new ImsService connection, signal for re-registration
synchronized (mHasRegisteredLock) {
mHasRegisteredForProxy = false;
}
}
- // Deprecated method of binding with the ImsService defined in the ServiceManager.
- private ImsServiceProxyCompat getServiceProxyCompat() {
- IBinder binder = ServiceManager.checkService(IMS_SERVICE);
-
- if (binder != null) {
- try {
- binder.linkToDeath(mDeathRecipient, 0);
- } catch (RemoteException e) {
- }
- }
-
- return new ImsServiceProxyCompat(mPhoneId, binder);
- }
-
- // New method of binding with the ImsResolver
- private ImsServiceProxy getServiceProxy() {
- TelephonyManager tm = (TelephonyManager)
- mContext.getSystemService(Context.TELEPHONY_SERVICE);
- ImsServiceProxy serviceProxy = new ImsServiceProxy(mPhoneId, ImsFeature.MMTEL);
- serviceProxy.setStatusCallback(() -> mStatusCallbacks.forEach(
- ImsServiceProxy.INotifyStatusChanged::notifyStatusChanged));
- // Returns null if the service is not available.
- IImsMMTelFeature b = tm.getImsMMTelFeatureAndListen(mPhoneId,
- serviceProxy.getListener());
- if (b != null) {
- serviceProxy.setBinder(b.asBinder());
- // Trigger the cache to be updated for feature status.
- serviceProxy.getFeatureStatus();
- } else {
- Rlog.w(TAG, "getServiceProxy: b is null! Phone Id: " + mPhoneId);
- }
- return serviceProxy;
- }
-
/**
* Creates a {@link ImsCallSession} with the specified call profile.
* Use other methods, if applicable, instead of interacting with
@@ -2238,6 +2229,57 @@ public class ImsManager {
}
}
+ // New API for Registration, uses ImsConnectionStateListener for backwards compatibility with
+ // deprecated APIs.
+ private class ImsRegistrationCallback extends IImsRegistrationCallback.Stub {
+
+ @Override
+ public void onRegistered(int imsRadioTech) {
+ if (DBG) log("onRegistered ::");
+
+ synchronized (mRegistrationListeners) {
+ mRegistrationListeners.forEach(l -> l.onRegistered(imsRadioTech));
+ }
+ }
+
+ @Override
+ public void onRegistering(int imsRadioTech) {
+ if (DBG) log("onRegistering ::");
+
+ synchronized (mRegistrationListeners) {
+ mRegistrationListeners.forEach(l -> l.onRegistering(imsRadioTech));
+ }
+ }
+
+ @Override
+ public void onDeregistered(ImsReasonInfo imsReasonInfo) {
+ if (DBG) log("onDeregistered ::");
+
+ synchronized (mRegistrationListeners) {
+ mRegistrationListeners.forEach(l -> l.onDeregistered(imsReasonInfo));
+ }
+ }
+
+ @Override
+ public void onTechnologyChangeFailed(int targetRadioTech, ImsReasonInfo imsReasonInfo) {
+ if (DBG) log("onTechnologyChangeFailed :: targetAccessTech=" + targetRadioTech +
+ ", imsReasonInfo=" + imsReasonInfo);
+
+ synchronized (mRegistrationListeners) {
+ mRegistrationListeners.forEach(l -> l.onTechnologyChangeFailed(targetRadioTech,
+ imsReasonInfo));
+ }
+ }
+
+ @Override
+ public void onSubscriberAssociatedUriChanged(Uri[] uris) {
+ if (DBG) log("onSubscriberAssociatedUriChanged");
+ synchronized (mRegistrationListeners) {
+ mRegistrationListeners.forEach(l -> l.onSubscriberAssociatedUriChanged(uris));
+ }
+ }
+ }
+
/**
* Gets the ECBM interface to request ECBM exit.
*
@@ -2266,6 +2308,59 @@ public class ImsManager {
return mEcbm;
}
+ public void sendSms(int token, int messageRef, String format, String smsc, boolean isRetry,
+ byte[] pdu) throws ImsException {
+ try {
+ mImsServiceProxy.sendSms(token, messageRef, format, smsc, isRetry, pdu);
+ } catch (RemoteException e) {
+ throw new ImsException("sendSms()", e, ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
+ }
+ }
+
+ public void acknowledgeSms(int token, int messageRef, int result) throws ImsException {
+ try {
+ mImsServiceProxy.acknowledgeSms(token, messageRef, result);
+ } catch (RemoteException e) {
+ throw new ImsException("acknowledgeSms()", e,
+ ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
+ }
+ }
+
+ public void acknowledgeSmsReport(int token, int messageRef, int result) throws ImsException{
+ try {
+ mImsServiceProxy.acknowledgeSmsReport(token, messageRef, result);
+ } catch (RemoteException e) {
+ throw new ImsException("acknowledgeSmsReport()", e,
+ ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
+ }
+ }
+
+ public String getSmsFormat() throws ImsException{
+ try {
+ return mImsServiceProxy.getSmsFormat();
+ } catch (RemoteException e) {
+ throw new ImsException("getSmsFormat()", e,
+ ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
+ }
+ }
+
+ public void setSmsListener(IImsSmsListener listener) throws ImsException {
+ try {
+ mImsServiceProxy.setSmsListener(listener);
+ } catch (RemoteException e) {
+ throw new ImsException("setSmsListener()", e,
+ ImsReasonInfo.CODE_LOCAL_IMS_SERVICE_DOWN);
+ }
+ }
+
+ public void addRegistrationCallback(ImsRegistrationImplBase.Callback callback) {
+ // TODO: implement (coming in ag/3472519)
+ }
+
+ public void addCapabilitiesCallback(ImsFeature.CapabilityCallback callback) {
+ // TODO: implement (coming in ag/3472519)
+ }
+
/**
* Gets the Multi-Endpoint interface to subscribe to multi-enpoint notifications..
*
diff --git a/com/android/ims/ImsReasonInfo.java b/com/android/ims/ImsReasonInfo.java
index 4f6f68c3..83d9bd94 100644
--- a/com/android/ims/ImsReasonInfo.java
+++ b/com/android/ims/ImsReasonInfo.java
@@ -384,6 +384,13 @@ public class ImsReasonInfo implements Parcelable {
/** Call/IMS registration is failed/dropped because of a network detach */
public static final int CODE_NETWORK_DETACH = 1513;
+ /**
+ * Call failed due to SIP code 380 (Alternative Service response) while dialing an "undetected
+ * emergency number". This scenario is important in some regions where the carrier network will
+ * identify other non-emergency help numbers (e.g. mountain rescue) when attempting to dial.
+ */
+ public static final int CODE_SIP_ALTERNATE_EMERGENCY_CALL = 1514;
+
/* OEM specific error codes. To be used by OEMs when they don't want to
reveal error code which would be replaced by ERROR_UNSPECIFIED */
public static final int CODE_OEM_CAUSE_1 = 0xf001;
diff --git a/com/android/ims/ImsServiceProxy.java b/com/android/ims/ImsServiceProxy.java
index f3489194..7fcaac2b 100644
--- a/com/android/ims/ImsServiceProxy.java
+++ b/com/android/ims/ImsServiceProxy.java
@@ -16,11 +16,17 @@
package com.android.ims;
+import android.annotation.Nullable;
import android.app.PendingIntent;
+import android.content.Context;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
+import android.telephony.Rlog;
+import android.telephony.TelephonyManager;
import android.telephony.ims.feature.ImsFeature;
+import android.telephony.SmsMessage;
+import android.telephony.ims.internal.stub.SmsImplBase;
import android.util.Log;
import com.android.ims.internal.IImsCallSession;
@@ -29,8 +35,10 @@ import com.android.ims.internal.IImsConfig;
import com.android.ims.internal.IImsEcbm;
import com.android.ims.internal.IImsMMTelFeature;
import com.android.ims.internal.IImsMultiEndpoint;
+import com.android.ims.internal.IImsRegistration;
import com.android.ims.internal.IImsRegistrationListener;
import com.android.ims.internal.IImsServiceFeatureCallback;
+import com.android.ims.internal.IImsSmsListener;
import com.android.ims.internal.IImsUt;
/**
@@ -41,20 +49,58 @@ import com.android.ims.internal.IImsUt;
public class ImsServiceProxy {
- protected String LOG_TAG = "ImsServiceProxy";
+ protected static final String TAG = "ImsServiceProxy";
protected final int mSlotId;
protected IBinder mBinder;
private final int mSupportedFeature;
+ private Context mContext;
// Start by assuming the proxy is available for usage.
private boolean mIsAvailable = true;
// ImsFeature Status from the ImsService. Cached.
private Integer mFeatureStatusCached = null;
- private ImsServiceProxy.INotifyStatusChanged mStatusCallback;
+ private IFeatureUpdate mStatusCallback;
private final Object mLock = new Object();
- public interface INotifyStatusChanged {
- void notifyStatusChanged();
+ public static ImsServiceProxy create(Context context , int slotId) {
+ ImsServiceProxy serviceProxy = new ImsServiceProxy(context, slotId, ImsFeature.MMTEL);
+
+ TelephonyManager tm = getTelephonyManager(context);
+ if (tm == null) {
+ Rlog.w(TAG, "getServiceProxy: TelephonyManager is null!");
+ // Binder can be unset in this case because it will be torn down/recreated as part of
+ // a retry mechanism until the serviceProxy binder is set successfully.
+ return serviceProxy;
+ }
+
+ IImsMMTelFeature binder = tm.getImsMMTelFeatureAndListen(slotId,
+ serviceProxy.getListener());
+ if (binder != null) {
+ serviceProxy.setBinder(binder.asBinder());
+ // Trigger the cache to be updated for feature status.
+ serviceProxy.getFeatureStatus();
+ } else {
+ Rlog.w(TAG, "getServiceProxy: binder is null! Phone Id: " + slotId);
+ }
+ return serviceProxy;
+ }
+
+ public static TelephonyManager getTelephonyManager(Context context) {
+ return (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
+ }
+
+ public interface IFeatureUpdate {
+ /**
+ * Called when the ImsFeature has changed its state. Use
+ * {@link ImsFeature#getFeatureState()} to get the new state.
+ */
+ void notifyStateChanged();
+
+ /**
+ * Called when the ImsFeature has become unavailable due to the binder switching or app
+ * crashing. A new ImsServiceProxy should be requested for that feature.
+ */
+ void notifyUnavailable();
}
private final IImsServiceFeatureCallback mListenerBinder =
@@ -65,7 +111,7 @@ public class ImsServiceProxy {
// The feature has been re-enabled. This may happen when the service crashes.
synchronized (mLock) {
if (!mIsAvailable && mSlotId == slotId && feature == mSupportedFeature) {
- Log.i(LOG_TAG, "Feature enabled on slotId: " + slotId + " for feature: " +
+ Log.i(TAG, "Feature enabled on slotId: " + slotId + " for feature: " +
feature);
mIsAvailable = true;
}
@@ -76,9 +122,12 @@ public class ImsServiceProxy {
public void imsFeatureRemoved(int slotId, int feature) throws RemoteException {
synchronized (mLock) {
if (mIsAvailable && mSlotId == slotId && feature == mSupportedFeature) {
- Log.i(LOG_TAG, "Feature disabled on slotId: " + slotId + " for feature: " +
+ Log.i(TAG, "Feature disabled on slotId: " + slotId + " for feature: " +
feature);
mIsAvailable = false;
+ if (mStatusCallback != null) {
+ mStatusCallback.notifyUnavailable();
+ }
}
}
}
@@ -86,26 +135,32 @@ public class ImsServiceProxy {
@Override
public void imsStatusChanged(int slotId, int feature, int status) throws RemoteException {
synchronized (mLock) {
- Log.i(LOG_TAG, "imsStatusChanged: slot: " + slotId + " feature: " + feature +
+ Log.i(TAG, "imsStatusChanged: slot: " + slotId + " feature: " + feature +
" status: " + status);
if (mSlotId == slotId && feature == mSupportedFeature) {
mFeatureStatusCached = status;
if (mStatusCallback != null) {
- mStatusCallback.notifyStatusChanged();
+ mStatusCallback.notifyStateChanged();
}
}
}
}
};
- public ImsServiceProxy(int slotId, IBinder binder, int featureType) {
+ public ImsServiceProxy(Context context, int slotId, IBinder binder, int featureType) {
mSlotId = slotId;
mBinder = binder;
mSupportedFeature = featureType;
+ mContext = context;
}
- public ImsServiceProxy(int slotId, int featureType) {
- this(slotId, null, featureType);
+ public ImsServiceProxy(Context context, int slotId, int featureType) {
+ this(context, slotId, null, featureType);
+ }
+
+ public @Nullable IImsRegistration getRegistration() {
+ TelephonyManager tm = getTelephonyManager(mContext);
+ return tm != null ? tm.getImsRegistration(mSlotId, ImsFeature.MMTEL) : null;
}
public IImsServiceFeatureCallback getListener() {
@@ -246,7 +301,7 @@ public class ImsServiceProxy {
public int getFeatureStatus() {
synchronized (mLock) {
if (isBinderAlive() && mFeatureStatusCached != null) {
- Log.i(LOG_TAG, "getFeatureStatus - returning cached: " + mFeatureStatusCached);
+ Log.i(TAG, "getFeatureStatus - returning cached: " + mFeatureStatusCached);
return mFeatureStatusCached;
}
}
@@ -259,7 +314,7 @@ public class ImsServiceProxy {
// Cache only non-null value for feature status.
mFeatureStatusCached = status;
}
- Log.i(LOG_TAG, "getFeatureStatus - returning " + status);
+ Log.i(TAG, "getFeatureStatus - returning " + status);
return status;
}
@@ -280,10 +335,49 @@ public class ImsServiceProxy {
/**
* @param c Callback that will fire when the feature status has changed.
*/
- public void setStatusCallback(INotifyStatusChanged c) {
+ public void setStatusCallback(IFeatureUpdate c) {
mStatusCallback = c;
}
+ public void sendSms(int token, int messageRef, String format, String smsc, boolean isRetry,
+ byte[] pdu) throws RemoteException {
+ synchronized (mLock) {
+ checkServiceIsReady();
+ getServiceInterface(mBinder).sendSms(token, messageRef, format, smsc, isRetry,
+ pdu);
+ }
+ }
+
+ public void acknowledgeSms(int token, int messageRef,
+ @SmsImplBase.SendStatusResult int result) throws RemoteException {
+ synchronized (mLock) {
+ checkServiceIsReady();
+ getServiceInterface(mBinder).acknowledgeSms(token, messageRef, result);
+ }
+ }
+
+ public void acknowledgeSmsReport(int token, int messageRef,
+ @SmsImplBase.StatusReportResult int result) throws RemoteException {
+ synchronized (mLock) {
+ checkServiceIsReady();
+ getServiceInterface(mBinder).acknowledgeSmsReport(token, messageRef, result);
+ }
+ }
+
+ public String getSmsFormat() throws RemoteException {
+ synchronized (mLock) {
+ checkServiceIsReady();
+ return getServiceInterface(mBinder).getSmsFormat();
+ }
+ }
+
+ public void setSmsListener(IImsSmsListener listener) throws RemoteException {
+ synchronized (mLock) {
+ checkServiceIsReady();
+ getServiceInterface(mBinder).setSmsListener(listener);
+ }
+ }
+
/**
* @return Returns true if the ImsService is ready to take commands, false otherwise. If this
* method returns false, it doesn't mean that the Binder connection is not available (use
diff --git a/com/android/ims/ImsServiceProxyCompat.java b/com/android/ims/ImsServiceProxyCompat.java
index 5ba1f351..a6d1865e 100644
--- a/com/android/ims/ImsServiceProxyCompat.java
+++ b/com/android/ims/ImsServiceProxyCompat.java
@@ -17,15 +17,20 @@
package com.android.ims;
import android.app.PendingIntent;
+import android.content.Context;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.telephony.Rlog;
+import android.telephony.TelephonyManager;
import android.telephony.ims.feature.ImsFeature;
import com.android.ims.internal.IImsCallSession;
import com.android.ims.internal.IImsCallSessionListener;
import com.android.ims.internal.IImsConfig;
import com.android.ims.internal.IImsEcbm;
+import com.android.ims.internal.IImsMMTelFeature;
import com.android.ims.internal.IImsMultiEndpoint;
import com.android.ims.internal.IImsRegistrationListener;
import com.android.ims.internal.IImsService;
@@ -42,8 +47,31 @@ public class ImsServiceProxyCompat extends ImsServiceProxy {
private static final int SERVICE_ID = ImsFeature.MMTEL;
- public ImsServiceProxyCompat(int slotId, IBinder binder) {
- super(slotId, binder, SERVICE_ID);
+ /**
+ * For accessing the IMS related service.
+ * Internal use only.
+ * @hide
+ */
+ private static final String IMS_SERVICE = "ims";
+
+ public static ImsServiceProxyCompat create(Context context, int slotId,
+ IBinder.DeathRecipient recipient) {
+ IBinder binder = ServiceManager.checkService(IMS_SERVICE);
+
+ if (binder != null) {
+ try {
+ binder.linkToDeath(recipient, 0);
+ } catch (RemoteException e) {
+ }
+ }
+
+ // If the proxy is created with a null binder, subsequent calls that depend on a live
+ // binder will fail, causing this structure to be torn down and created again.
+ return new ImsServiceProxyCompat(context, slotId, binder);
+ }
+
+ public ImsServiceProxyCompat(Context context, int slotId, IBinder binder) {
+ super(context, slotId, binder, SERVICE_ID);
}
@Override
diff --git a/com/android/internal/app/ChooserActivity.java b/com/android/internal/app/ChooserActivity.java
index 6e0ba341..997d47fe 100644
--- a/com/android/internal/app/ChooserActivity.java
+++ b/com/android/internal/app/ChooserActivity.java
@@ -841,7 +841,7 @@ public class ChooserActivity extends ResolverActivity {
}
@Override
- public boolean startAsCaller(Activity activity, Bundle options, int userId) {
+ public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
final Intent intent = getBaseIntentToSend();
if (intent == null) {
return false;
@@ -860,8 +860,7 @@ public class ChooserActivity extends ResolverActivity {
final boolean ignoreTargetSecurity = mSourceInfo != null
&& mSourceInfo.getResolvedComponentName().getPackageName()
.equals(mChooserTarget.getComponentName().getPackageName());
- activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId);
- return true;
+ return activity.startAsCallerImpl(intent, options, ignoreTargetSecurity, userId);
}
@Override
diff --git a/com/android/internal/app/HarmfulAppWarningActivity.java b/com/android/internal/app/HarmfulAppWarningActivity.java
new file mode 100644
index 00000000..042da36c
--- /dev/null
+++ b/com/android/internal/app/HarmfulAppWarningActivity.java
@@ -0,0 +1,99 @@
+/*
+ * 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 com.android.internal.app;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.os.Bundle;
+import android.util.Log;
+import com.android.internal.R;
+
+/**
+ * This dialog is shown to the user before an activity in a harmful app is launched.
+ *
+ * See {@code PackageManager.setHarmfulAppInfo} for more info.
+ */
+public class HarmfulAppWarningActivity extends AlertActivity implements
+ DialogInterface.OnClickListener {
+ private static final String TAG = "HarmfulAppWarningActivity";
+
+ private static final String EXTRA_HARMFUL_APP_WARNING = "harmful_app_warning";
+
+ private String mPackageName;
+ private String mHarmfulAppWarning;
+ private IntentSender mTarget;
+
+ // [b/63909431] STOPSHIP replace placeholder UI with final Harmful App Warning UI
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ Intent intent = getIntent();
+ mPackageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME);
+ mTarget = intent.getParcelableExtra(Intent.EXTRA_INTENT);
+ mHarmfulAppWarning = intent.getStringExtra(EXTRA_HARMFUL_APP_WARNING);
+
+ if (mPackageName == null || mTarget == null || mHarmfulAppWarning == null) {
+ Log.wtf(TAG, "Invalid intent: " + intent.toString());
+ finish();
+ }
+
+ AlertController.AlertParams p = mAlertParams;
+ p.mTitle = getString(R.string.harmful_app_warning_title);
+ p.mMessage = mHarmfulAppWarning;
+ p.mPositiveButtonText = getString(R.string.harmful_app_warning_launch_anyway);
+ p.mPositiveButtonListener = this;
+ p.mNegativeButtonText = getString(R.string.harmful_app_warning_uninstall);
+ p.mNegativeButtonListener = this;
+
+ mAlert.installContent(mAlertParams);
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE:
+ getPackageManager().setHarmfulAppWarning(mPackageName, null);
+
+ IntentSender target = getIntent().getParcelableExtra(Intent.EXTRA_INTENT);
+ try {
+ startIntentSenderForResult(target, -1, null, 0, 0, 0);
+ } catch (IntentSender.SendIntentException e) {
+ // ignore..
+ }
+ finish();
+ break;
+ case DialogInterface.BUTTON_NEGATIVE:
+ getPackageManager().deletePackage(mPackageName, null, 0);
+ finish();
+ break;
+ }
+ }
+
+ public static Intent createHarmfulAppWarningIntent(Context context, String targetPackageName,
+ IntentSender target, CharSequence harmfulAppWarning) {
+ Intent intent = new Intent();
+ intent.setClass(context, HarmfulAppWarningActivity.class);
+ intent.putExtra(Intent.EXTRA_PACKAGE_NAME, targetPackageName);
+ intent.putExtra(Intent.EXTRA_INTENT, target);
+ intent.putExtra(EXTRA_HARMFUL_APP_WARNING, harmfulAppWarning);
+ return intent;
+ }
+}
diff --git a/com/android/internal/app/IntentForwarderActivity.java b/com/android/internal/app/IntentForwarderActivity.java
index 398d0879..86731bcb 100644
--- a/com/android/internal/app/IntentForwarderActivity.java
+++ b/com/android/internal/app/IntentForwarderActivity.java
@@ -107,7 +107,7 @@ public class IntentForwarderActivity extends Activity {
|| ChooserActivity.class.getName().equals(ri.activityInfo.name));
try {
- startActivityAsCaller(newIntent, null, false, targetUserId);
+ startActivityAsCaller(newIntent, null, null, false, targetUserId);
} catch (RuntimeException e) {
int launchedFromUid = -1;
String launchedFromPackage = "?";
diff --git a/com/android/internal/app/ResolverActivity.java b/com/android/internal/app/ResolverActivity.java
index ceb06f51..d6d44908 100644
--- a/com/android/internal/app/ResolverActivity.java
+++ b/com/android/internal/app/ResolverActivity.java
@@ -43,6 +43,7 @@ import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
+import android.os.IBinder;
import android.os.PatternMatcher;
import android.os.RemoteException;
import android.os.StrictMode;
@@ -857,6 +858,36 @@ public class ResolverActivity extends Activity {
}
}
+ public boolean startAsCallerImpl(Intent intent, Bundle options, boolean ignoreTargetSecurity,
+ int userId) {
+ // Pass intent to delegate chooser activity with permission token.
+ // TODO: This should move to a trampoline Activity in the system when the ChooserActivity
+ // moves into systemui
+ try {
+ // TODO: Once this is a small springboard activity, it can move off the UI process
+ // and we can move the request method to ActivityManagerInternal.
+ IBinder permissionToken = ActivityManager.getService()
+ .requestStartActivityPermissionToken(getActivityToken());
+ final Intent chooserIntent = new Intent();
+ final ComponentName delegateActivity = ComponentName.unflattenFromString(
+ Resources.getSystem().getString(R.string.config_chooserActivity));
+ chooserIntent.setClassName(delegateActivity.getPackageName(),
+ delegateActivity.getClassName());
+ chooserIntent.putExtra(ActivityManager.EXTRA_PERMISSION_TOKEN, permissionToken);
+
+ // TODO: These extras will change as chooser activity moves into systemui
+ chooserIntent.putExtra(Intent.EXTRA_INTENT, intent);
+ chooserIntent.putExtra(ActivityManager.EXTRA_OPTIONS, options);
+ chooserIntent.putExtra(ActivityManager.EXTRA_IGNORE_TARGET_SECURITY,
+ ignoreTargetSecurity);
+ chooserIntent.putExtra(Intent.EXTRA_USER_ID, userId);
+ startActivity(chooserIntent);
+ } catch (RemoteException e) {
+ Log.e(TAG, e.toString());
+ }
+ return true;
+ }
+
public void onActivityStarted(TargetInfo cti) {
// Do nothing
}
@@ -1181,9 +1212,8 @@ public class ResolverActivity extends Activity {
}
@Override
- public boolean startAsCaller(Activity activity, Bundle options, int userId) {
- activity.startActivityAsCaller(mResolvedIntent, options, false, userId);
- return true;
+ public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
+ return activity.startAsCallerImpl(mResolvedIntent, options, false, userId);
}
@Override
@@ -1242,7 +1272,7 @@ public class ResolverActivity extends Activity {
* @param userId userId to start as or {@link UserHandle#USER_NULL} for activity's caller
* @return true if the start completed successfully
*/
- boolean startAsCaller(Activity activity, Bundle options, int userId);
+ boolean startAsCaller(ResolverActivity activity, Bundle options, int userId);
/**
* Start the activity referenced by this target as a given user.
diff --git a/com/android/internal/app/ResolverComparator.java b/com/android/internal/app/ResolverComparator.java
index 77cfc2fc..96d3baf7 100644
--- a/com/android/internal/app/ResolverComparator.java
+++ b/com/android/internal/app/ResolverComparator.java
@@ -411,6 +411,9 @@ class ResolverComparator implements Comparator<ResolvedComponentInfo> {
mContext.unbindService(mConnection);
mConnection.destroy();
}
+ if (mAfterCompute != null) {
+ mAfterCompute.afterCompute();
+ }
if (DEBUG) {
Log.d(TAG, "Unbinded Resolver Ranker.");
}
@@ -573,7 +576,6 @@ class ResolverComparator implements Comparator<ResolvedComponentInfo> {
if (DEBUG) {
Log.d(TAG, "Has not found valid ResolverRankerService; Skip Prediction");
}
- return;
} else {
try {
mConnectSignal.await(CONNECTION_COST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
diff --git a/com/android/internal/app/SuggestedLocaleAdapter.java b/com/android/internal/app/SuggestedLocaleAdapter.java
index d1382415..46f47a31 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(Locale.getDefault());
+ int layoutDir = TextUtils.getLayoutDirectionFromLocale(item.getParent());
//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 2eadaf3a..902c8c16 100644
--- a/com/android/internal/app/UnlaunchableAppActivity.java
+++ b/com/android/internal/app/UnlaunchableAppActivity.java
@@ -111,7 +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) {
- UserManager.get(this).trySetQuietModeEnabled(false, UserHandle.of(mUserId), mTarget);
+ UserManager.get(this).requestQuietModeEnabled(false, UserHandle.of(mUserId), mTarget);
}
}
diff --git a/com/android/internal/app/procstats/ProcessState.java b/com/android/internal/app/procstats/ProcessState.java
index efc9c02f..a0be64b8 100644
--- a/com/android/internal/app/procstats/ProcessState.java
+++ b/com/android/internal/app/procstats/ProcessState.java
@@ -89,8 +89,8 @@ public final class ProcessState {
STATE_PERSISTENT, // ActivityManager.PROCESS_STATE_PERSISTENT
STATE_PERSISTENT, // ActivityManager.PROCESS_STATE_PERSISTENT_UI
STATE_TOP, // ActivityManager.PROCESS_STATE_TOP
- STATE_IMPORTANT_FOREGROUND, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
STATE_IMPORTANT_FOREGROUND, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
+ STATE_IMPORTANT_FOREGROUND, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
STATE_IMPORTANT_FOREGROUND, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
STATE_IMPORTANT_BACKGROUND, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
STATE_IMPORTANT_BACKGROUND, // ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND
@@ -153,7 +153,6 @@ public final class ProcessState {
private int mNumActiveServices;
private int mNumStartedServices;
- private int mNumExcessiveWake;
private int mNumExcessiveCpu;
private int mNumCachedKill;
@@ -470,9 +469,23 @@ public final class ProcessState {
}
}
- public void addPss(long pss, long uss, boolean always,
+ public void addPss(long pss, long uss, boolean always, int type, long duration,
ArrayMap<String, ProcessStateHolder> pkgList) {
ensureNotDead();
+ switch (type) {
+ case ProcessStats.ADD_PSS_INTERNAL:
+ mStats.mInternalPssCount++;
+ mStats.mInternalPssTime += duration;
+ break;
+ case ProcessStats.ADD_PSS_EXTERNAL:
+ mStats.mExternalPssCount++;
+ mStats.mExternalPssTime += duration;
+ break;
+ case ProcessStats.ADD_PSS_EXTERNAL_SLOW:
+ mStats.mExternalSlowPssCount++;
+ mStats.mExternalSlowPssTime += duration;
+ break;
+ }
if (!always) {
if (mLastPssState == mCurState && SystemClock.uptimeMillis()
< (mLastPssTime+(30*1000))) {
diff --git a/com/android/internal/app/procstats/ProcessStats.java b/com/android/internal/app/procstats/ProcessStats.java
index 96ba2b0c..35b2dd23 100644
--- a/com/android/internal/app/procstats/ProcessStats.java
+++ b/com/android/internal/app/procstats/ProcessStats.java
@@ -134,6 +134,10 @@ public final class ProcessStats implements Parcelable {
public static final int FLAG_SHUTDOWN = 1<<1;
public static final int FLAG_SYSPROPS = 1<<2;
+ public static final int ADD_PSS_INTERNAL = 0;
+ public static final int ADD_PSS_EXTERNAL = 1;
+ public static final int ADD_PSS_EXTERNAL_SLOW = 2;
+
public static final int[] ALL_MEM_ADJ = new int[] { ADJ_MEM_FACTOR_NORMAL,
ADJ_MEM_FACTOR_MODERATE, ADJ_MEM_FACTOR_LOW, ADJ_MEM_FACTOR_CRITICAL };
@@ -158,7 +162,7 @@ public final class ProcessStats implements Parcelable {
};
// Current version of the parcel format.
- private static final int PARCEL_VERSION = 23;
+ private static final int PARCEL_VERSION = 24;
// In-memory Parcel magic number, used to detect attempts to unmarshall bad data
private static final int MAGIC = 0x50535454;
@@ -183,6 +187,18 @@ public final class ProcessStats implements Parcelable {
boolean mHasSwappedOutPss;
+ // Count and total time expended doing "quick" pss computations for internal use.
+ public long mInternalPssCount;
+ public long mInternalPssTime;
+
+ // Count and total time expended doing "quick" pss computations due to external requests.
+ public long mExternalPssCount;
+ public long mExternalPssTime;
+
+ // Count and total time expended doing full/slow pss computations due to external requests.
+ public long mExternalSlowPssCount;
+ public long mExternalSlowPssTime;
+
public final SparseMappingTable mTableData = new SparseMappingTable();
public final long[] mSysMemUsageArgs = new long[SYS_MEM_USAGE_COUNT];
@@ -302,6 +318,13 @@ public final class ProcessStats implements Parcelable {
mTimePeriodEndRealtime += other.mTimePeriodEndRealtime - other.mTimePeriodStartRealtime;
mTimePeriodEndUptime += other.mTimePeriodEndUptime - other.mTimePeriodStartUptime;
+ mInternalPssCount += other.mInternalPssCount;
+ mInternalPssTime += other.mInternalPssTime;
+ mExternalPssCount += other.mExternalPssCount;
+ mExternalPssTime += other.mExternalPssTime;
+ mExternalSlowPssCount += other.mExternalSlowPssCount;
+ mExternalSlowPssTime += other.mExternalSlowPssTime;
+
mHasSwappedOutPss |= other.mHasSwappedOutPss;
}
@@ -500,6 +523,12 @@ public final class ProcessStats implements Parcelable {
buildTimePeriodStartClockStr();
mTimePeriodStartRealtime = mTimePeriodEndRealtime = SystemClock.elapsedRealtime();
mTimePeriodStartUptime = mTimePeriodEndUptime = SystemClock.uptimeMillis();
+ mInternalPssCount = 0;
+ mInternalPssTime = 0;
+ mExternalPssCount = 0;
+ mExternalPssTime = 0;
+ mExternalSlowPssCount = 0;
+ mExternalSlowPssTime = 0;
mTableData.reset();
Arrays.fill(mMemFactorDurations, 0);
mSysMemUsage.resetTable();
@@ -760,6 +789,12 @@ public final class ProcessStats implements Parcelable {
out.writeLong(mTimePeriodEndRealtime);
out.writeLong(mTimePeriodStartUptime);
out.writeLong(mTimePeriodEndUptime);
+ out.writeLong(mInternalPssCount);
+ out.writeLong(mInternalPssTime);
+ out.writeLong(mExternalPssCount);
+ out.writeLong(mExternalPssTime);
+ out.writeLong(mExternalSlowPssCount);
+ out.writeLong(mExternalSlowPssTime);
out.writeString(mRuntime);
out.writeInt(mHasSwappedOutPss ? 1 : 0);
out.writeInt(mFlags);
@@ -928,6 +963,12 @@ public final class ProcessStats implements Parcelable {
mTimePeriodEndRealtime = in.readLong();
mTimePeriodStartUptime = in.readLong();
mTimePeriodEndUptime = in.readLong();
+ mInternalPssCount = in.readLong();
+ mInternalPssTime = in.readLong();
+ mExternalPssCount = in.readLong();
+ mExternalPssTime = in.readLong();
+ mExternalSlowPssCount = in.readLong();
+ mExternalSlowPssTime = in.readLong();
mRuntime = in.readString();
mHasSwappedOutPss = in.readInt() != 0;
mFlags = in.readInt();
@@ -1484,9 +1525,31 @@ public final class ProcessStats implements Parcelable {
totalMem.processStateWeight[STATE_SERVICE_RESTARTING], totalMem.totalTime, totalPss,
totalMem.processStateSamples[STATE_SERVICE_RESTARTING]);
pw.println();
+ pw.println("PSS collection stats:");
+ pw.print(" Internal: ");
+ pw.print(mInternalPssCount);
+ pw.print("x over ");
+ TimeUtils.formatDuration(mInternalPssTime, pw);
+ pw.println();
+ pw.print(" External: ");
+ pw.print(mExternalPssCount);
+ pw.print("x over ");
+ TimeUtils.formatDuration(mExternalPssTime, pw);
+ pw.println();
+ pw.print(" External Slow: ");
+ pw.print(mExternalSlowPssCount);
+ pw.print("x over ");
+ TimeUtils.formatDuration(mExternalSlowPssTime, pw);
+ pw.println();
+ pw.println();
pw.print(" Start time: ");
pw.print(DateFormat.format("yyyy-MM-dd HH:mm:ss", mTimePeriodStartClock));
pw.println();
+ pw.print(" Total uptime: ");
+ TimeUtils.formatDuration(
+ (mRunning ? SystemClock.uptimeMillis() : mTimePeriodEndUptime)
+ - mTimePeriodStartUptime, pw);
+ pw.println();
pw.print(" Total elapsed time: ");
TimeUtils.formatDuration(
(mRunning ? SystemClock.elapsedRealtime() : mTimePeriodEndRealtime)
diff --git a/com/android/internal/colorextraction/types/Tonal.java b/com/android/internal/colorextraction/types/Tonal.java
index 71baaf17..9b7383fc 100644
--- a/com/android/internal/colorextraction/types/Tonal.java
+++ b/com/android/internal/colorextraction/types/Tonal.java
@@ -53,10 +53,8 @@ public class Tonal implements ExtractionType {
public static final int THRESHOLD_COLOR_LIGHT = 0xffe0e0e0;
public static final int MAIN_COLOR_LIGHT = 0xffe0e0e0;
- public static final int SECONDARY_COLOR_LIGHT = 0xff9e9e9e;
public static final int THRESHOLD_COLOR_DARK = 0xff212121;
public static final int MAIN_COLOR_DARK = 0xff000000;
- public static final int SECONDARY_COLOR_DARK = 0xff000000;
private final TonalPalette mGreyPalette;
private final ArrayList<TonalPalette> mTonalPalettes;
@@ -211,10 +209,8 @@ public class Tonal implements ExtractionType {
}
// Normal colors:
- // best fit + a 2 colors offset
outColorsNormal.setMainColor(mainColor);
- int secondaryIndex = primaryIndex + (primaryIndex >= 2 ? -2 : 2);
- outColorsNormal.setSecondaryColor(getColorInt(secondaryIndex, h, s, l));
+ outColorsNormal.setSecondaryColor(mainColor);
// Dark colors:
// Stops at 4th color, only lighter if dark text is supported
@@ -225,9 +221,9 @@ public class Tonal implements ExtractionType {
} else {
primaryIndex = Math.min(fitIndex, 3);
}
- secondaryIndex = primaryIndex + (primaryIndex >= 2 ? -2 : 2);
- outColorsDark.setMainColor(getColorInt(primaryIndex, h, s, l));
- outColorsDark.setSecondaryColor(getColorInt(secondaryIndex, h, s, l));
+ mainColor = getColorInt(primaryIndex, h, s, l);
+ outColorsDark.setMainColor(mainColor);
+ outColorsDark.setSecondaryColor(mainColor);
// Extra Dark:
// Stay close to dark colors until dark text is supported
@@ -238,9 +234,9 @@ public class Tonal implements ExtractionType {
} else {
primaryIndex = 2;
}
- secondaryIndex = primaryIndex + (primaryIndex >= 2 ? -2 : 2);
- outColorsExtraDark.setMainColor(getColorInt(primaryIndex, h, s, l));
- outColorsExtraDark.setSecondaryColor(getColorInt(secondaryIndex, h, s, l));
+ mainColor = getColorInt(primaryIndex, h, s, l);
+ outColorsExtraDark.setMainColor(mainColor);
+ outColorsExtraDark.setSecondaryColor(mainColor);
outColorsNormal.setSupportsDarkText(supportsDarkText);
outColorsDark.setSupportsDarkText(supportsDarkText);
@@ -273,11 +269,10 @@ public class Tonal implements ExtractionType {
boolean light = inWallpaperColors != null
&& (inWallpaperColors.getColorHints() & WallpaperColors.HINT_SUPPORTS_DARK_TEXT)
!= 0;
- int innerColor = light ? MAIN_COLOR_LIGHT : MAIN_COLOR_DARK;
- int outerColor = light ? SECONDARY_COLOR_LIGHT : SECONDARY_COLOR_DARK;
+ final int color = light ? MAIN_COLOR_LIGHT : MAIN_COLOR_DARK;
- outGradientColors.setMainColor(innerColor);
- outGradientColors.setSecondaryColor(outerColor);
+ outGradientColors.setMainColor(color);
+ outGradientColors.setSecondaryColor(color);
outGradientColors.setSupportsDarkText(light);
}
diff --git a/com/android/internal/content/PackageHelper.java b/com/android/internal/content/PackageHelper.java
index e765ab1e..8a456d1c 100644
--- a/com/android/internal/content/PackageHelper.java
+++ b/com/android/internal/content/PackageHelper.java
@@ -25,6 +25,7 @@ import android.content.pm.PackageInstaller.SessionParams;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PackageParser.PackageLite;
+import android.content.pm.dex.DexMetadataHelper;
import android.os.Environment;
import android.os.IBinder;
import android.os.RemoteException;
@@ -415,6 +416,9 @@ public class PackageHelper {
sizeBytes += codeFile.length();
}
+ // Include raw dex metadata files
+ sizeBytes += DexMetadataHelper.getPackageDexMetadataSize(pkg);
+
// Include all relevant native code
sizeBytes += NativeLibraryHelper.sumNativeBinariesWithOverride(handle, abiOverride);
diff --git a/com/android/internal/location/gnssmetrics/GnssMetrics.java b/com/android/internal/location/gnssmetrics/GnssMetrics.java
index 833376cf..603926f4 100644
--- a/com/android/internal/location/gnssmetrics/GnssMetrics.java
+++ b/com/android/internal/location/gnssmetrics/GnssMetrics.java
@@ -19,10 +19,12 @@ package com.android.internal.location.gnssmetrics;
import android.os.SystemClock;
import android.util.Base64;
+import android.util.Log;
import android.util.TimeUtils;
import java.util.Arrays;
+import com.android.internal.app.IBatteryStats;
import com.android.internal.location.nano.GnssLogsProto.GnssLog;
/**
@@ -31,14 +33,29 @@ import com.android.internal.location.nano.GnssLogsProto.GnssLog;
*/
public class GnssMetrics {
+ private static final String TAG = GnssMetrics.class.getSimpleName();
+
+ /* Constant which indicates GPS signal quality is poor */
+ public static final int GPS_SIGNAL_QUALITY_POOR = 0;
+
+ /* Constant which indicates GPS signal quality is good */
+ public static final int GPS_SIGNAL_QUALITY_GOOD = 1;
+
+ /* Number of GPS signal quality levels */
+ public static final int NUM_GPS_SIGNAL_QUALITY_LEVELS = GPS_SIGNAL_QUALITY_GOOD + 1;
+
/** Default time between location fixes (in millisecs) */
private static final int DEFAULT_TIME_BETWEEN_FIXES_MILLISECS = 1000;
/* The time since boot when logging started */
private String logStartInElapsedRealTime;
+ /* GNSS power metrics */
+ private GnssPowerMetrics mGnssPowerMetrics;
+
/** Constructor */
- public GnssMetrics() {
+ public GnssMetrics(IBatteryStats stats) {
+ mGnssPowerMetrics = new GnssPowerMetrics(stats);
locationFailureStatistics = new Statistics();
timeToFirstFixSecStatistics = new Statistics();
positionAccuracyMeterStatistics = new Statistics();
@@ -103,11 +120,18 @@ public class GnssMetrics {
*
*/
public void logCn0(float[] cn0s, int numSv) {
- if (numSv < 4) {
+ if (numSv == 0 || cn0s == null || cn0s.length == 0 || cn0s.length < numSv) {
+ if (numSv == 0) {
+ mGnssPowerMetrics.reportSignalQuality(null, 0);
+ }
return;
}
float[] cn0Array = Arrays.copyOf(cn0s, numSv);
Arrays.sort(cn0Array);
+ mGnssPowerMetrics.reportSignalQuality(cn0Array, numSv);
+ if (numSv < 4) {
+ return;
+ }
if (cn0Array[numSv - 4] > 0.0) {
double top4AvgCn0 = 0.0;
for (int i = numSv - 4; i < numSv; i++) {
@@ -265,4 +289,62 @@ public class GnssMetrics {
topFourAverageCn0Statistics.reset();
return;
}
+
+ /* Class for handling GNSS power related metrics */
+ private class GnssPowerMetrics {
+
+ /* Threshold for Top Four Average CN0 below which GNSS signal quality is declared poor */
+ private static final double POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ = 20.0;
+
+ /* Minimum change in Top Four Average CN0 needed to trigger a report */
+ private static final double REPORTING_THRESHOLD_DB_HZ = 1.0;
+
+ /* BatteryStats API */
+ private final IBatteryStats mBatteryStats;
+
+ /* Last reported Top Four Average CN0 */
+ private double mLastAverageCn0;
+
+ public GnssPowerMetrics(IBatteryStats stats) {
+ mBatteryStats = stats;
+ // Used to initialize the variable to a very small value (unachievable in practice) so that
+ // the first CNO report will trigger an update to BatteryStats
+ mLastAverageCn0 = -100.0;
+ }
+
+ /**
+ * Reports signal quality to BatteryStats. Signal quality is based on Top four average CN0. If
+ * the number of SVs seen is less than 4, then signal quality is the average CN0.
+ * Changes are reported only if the average CN0 changes by more than REPORTING_THRESHOLD_DB_HZ.
+ */
+ public void reportSignalQuality(float[] ascendingCN0Array, int numSv) {
+ double avgCn0 = 0.0;
+ if (numSv > 0) {
+ for (int i = Math.max(0, numSv - 4); i < numSv; i++) {
+ avgCn0 += (double) ascendingCN0Array[i];
+ }
+ avgCn0 /= Math.min(numSv, 4);
+ }
+ if (Math.abs(avgCn0 - mLastAverageCn0) < REPORTING_THRESHOLD_DB_HZ) {
+ return;
+ }
+ try {
+ mBatteryStats.noteGpsSignalQuality(getSignalLevel(avgCn0));
+ mLastAverageCn0 = avgCn0;
+ } catch (Exception e) {
+ Log.w(TAG, "Exception", e);
+ }
+ return;
+ }
+
+ /**
+ * Obtains signal level based on CN0
+ */
+ private int getSignalLevel(double cn0) {
+ if (cn0 > POOR_TOP_FOUR_AVG_CN0_THRESHOLD_DB_HZ) {
+ return GnssMetrics.GPS_SIGNAL_QUALITY_GOOD;
+ }
+ return GnssMetrics.GPS_SIGNAL_QUALITY_POOR;
+ }
+ }
} \ No newline at end of file
diff --git a/com/android/internal/net/NetworkStatsFactory.java b/com/android/internal/net/NetworkStatsFactory.java
index 5eda81ba..43abadec 100644
--- a/com/android/internal/net/NetworkStatsFactory.java
+++ b/com/android/internal/net/NetworkStatsFactory.java
@@ -31,13 +31,17 @@ import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.ProcFileReader;
+import com.google.android.collect.Lists;
import libcore.io.IoUtils;
+import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileReader;
import java.io.IOException;
import java.net.ProtocolException;
+import java.util.ArrayList;
import java.util.Objects;
/**
@@ -57,6 +61,8 @@ public class NetworkStatsFactory {
// Used for correct stats accounting on clatd interfaces.
private static final int IPV4V6_HEADER_DELTA = 20;
+ /** Path to {@code /proc/net/dev}. */
+ private final File mStatsIfaceDev;
/** Path to {@code /proc/net/xt_qtaguid/iface_stat_all}. */
private final File mStatsXtIfaceAll;
/** Path to {@code /proc/net/xt_qtaguid/iface_stat_fmt}. */
@@ -64,6 +70,8 @@ public class NetworkStatsFactory {
/** Path to {@code /proc/net/xt_qtaguid/stats}. */
private final File mStatsXtUid;
+ private boolean mUseBpfStats;
+
// TODO: to improve testability and avoid global state, do not use a static variable.
@GuardedBy("sStackedIfaces")
private static final ArrayMap<String, String> sStackedIfaces = new ArrayMap<>();
@@ -79,14 +87,54 @@ public class NetworkStatsFactory {
}
public NetworkStatsFactory() {
- this(new File("/proc/"));
+ this(new File("/proc/"), new File("/sys/fs/bpf/traffic_uid_stats_map").exists());
}
@VisibleForTesting
- public NetworkStatsFactory(File procRoot) {
+ public NetworkStatsFactory(File procRoot, boolean useBpfStats) {
+ mStatsIfaceDev = new File(procRoot, "net/dev");
mStatsXtIfaceAll = new File(procRoot, "net/xt_qtaguid/iface_stat_all");
mStatsXtIfaceFmt = new File(procRoot, "net/xt_qtaguid/iface_stat_fmt");
mStatsXtUid = new File(procRoot, "net/xt_qtaguid/stats");
+ mUseBpfStats = useBpfStats;
+ }
+
+ @VisibleForTesting
+ public NetworkStats readNetworkStatsIfaceDev() throws IOException {
+ final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
+
+ final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
+ final NetworkStats.Entry entry = new NetworkStats.Entry();
+
+ BufferedReader reader = null;
+ try {
+ reader = new BufferedReader(new FileReader(mStatsIfaceDev));
+
+ // skip first two header lines
+ reader.readLine();
+ reader.readLine();
+
+ // parse remaining lines
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String[] values = line.trim().split("\\:?\\s+");
+ entry.iface = values[0];
+ entry.uid = UID_ALL;
+ entry.set = SET_ALL;
+ entry.tag = TAG_NONE;
+ entry.rxBytes = Long.parseLong(values[1]);
+ entry.rxPackets = Long.parseLong(values[2]);
+ entry.txBytes = Long.parseLong(values[9]);
+ entry.txPackets = Long.parseLong(values[10]);
+ stats.addValues(entry);
+ }
+ } catch (NullPointerException|NumberFormatException e) {
+ throw new ProtocolException("problem parsing stats", e);
+ } finally {
+ IoUtils.closeQuietly(reader);
+ StrictMode.setThreadPolicy(savedPolicy);
+ }
+ return stats;
}
/**
@@ -98,6 +146,11 @@ public class NetworkStatsFactory {
* @throws IllegalStateException when problem parsing stats.
*/
public NetworkStats readNetworkStatsSummaryDev() throws IOException {
+
+ // Return the stats get from /proc/net/dev if switched to bpf module.
+ if (mUseBpfStats)
+ return readNetworkStatsIfaceDev();
+
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
final NetworkStats stats = new NetworkStats(SystemClock.elapsedRealtime(), 6);
@@ -149,6 +202,11 @@ public class NetworkStatsFactory {
* @throws IllegalStateException when problem parsing stats.
*/
public NetworkStats readNetworkStatsSummaryXt() throws IOException {
+
+ // Return the stats get from /proc/net/dev if qtaguid module is replaced.
+ if (mUseBpfStats)
+ return readNetworkStatsIfaceDev();
+
final StrictMode.ThreadPolicy savedPolicy = StrictMode.allowThreadDiskReads();
// return null when kernel doesn't support
@@ -219,7 +277,7 @@ public class NetworkStatsFactory {
}
NetworkStats.Entry adjust =
- new NetworkStats.Entry(baseIface, 0, 0, 0, 0L, 0L, 0L, 0L, 0L);
+ new NetworkStats.Entry(baseIface, 0, 0, 0, 0, 0, 0, 0L, 0L, 0L, 0L, 0L);
// Subtract any 464lat traffic seen for the root UID on the current base interface.
adjust.rxBytes -= (entry.rxBytes + entry.rxPackets * IPV4V6_HEADER_DELTA);
adjust.txBytes -= (entry.txBytes + entry.txPackets * IPV4V6_HEADER_DELTA);
@@ -254,7 +312,7 @@ public class NetworkStatsFactory {
stats = new NetworkStats(SystemClock.elapsedRealtime(), -1);
}
if (nativeReadNetworkStatsDetail(stats, mStatsXtUid.getAbsolutePath(), limitUid,
- limitIfaces, limitTag) != 0) {
+ limitIfaces, limitTag, mUseBpfStats) != 0) {
throw new IOException("Failed to parse network stats");
}
if (SANITY_CHECK_NATIVE) {
@@ -348,6 +406,6 @@ public class NetworkStatsFactory {
* are expected to monotonically increase since device boot.
*/
@VisibleForTesting
- public static native int nativeReadNetworkStatsDetail(
- NetworkStats stats, String path, int limitUid, String[] limitIfaces, int limitTag);
+ public static native int nativeReadNetworkStatsDetail(NetworkStats stats, String path,
+ int limitUid, String[] limitIfaces, int limitTag, boolean useBpfStats);
}
diff --git a/com/android/internal/os/BatteryStatsHelper.java b/com/android/internal/os/BatteryStatsHelper.java
index 15dc6f50..5a59e708 100644
--- a/com/android/internal/os/BatteryStatsHelper.java
+++ b/com/android/internal/os/BatteryStatsHelper.java
@@ -665,14 +665,14 @@ public class BatteryStatsHelper {
/**
* Calculate the baseline power usage for the device when it is in suspend and idle.
- * The device is drawing POWER_CPU_IDLE power at its lowest power state.
- * The device is drawing POWER_CPU_IDLE + POWER_CPU_AWAKE power when a wakelock is held.
+ * The device is drawing POWER_CPU_SUSPEND power at its lowest power state.
+ * The device is drawing POWER_CPU_SUSPEND + POWER_CPU_IDLE power when a wakelock is held.
*/
private void addIdleUsage() {
final double suspendPowerMaMs = (mTypeBatteryRealtimeUs / 1000) *
- mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_IDLE);
+ mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_SUSPEND);
final double idlePowerMaMs = (mTypeBatteryUptimeUs / 1000) *
- mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_AWAKE);
+ mPowerProfile.getAveragePower(PowerProfile.POWER_CPU_IDLE);
final double totalPowerMah = (suspendPowerMaMs + idlePowerMaMs) / (60 * 60 * 1000);
if (DEBUG && totalPowerMah != 0) {
Log.d(TAG, "Suspend: time=" + (mTypeBatteryRealtimeUs / 1000)
diff --git a/com/android/internal/os/BatteryStatsImpl.java b/com/android/internal/os/BatteryStatsImpl.java
index 72f07b7c..51f51c25 100644
--- a/com/android/internal/os/BatteryStatsImpl.java
+++ b/com/android/internal/os/BatteryStatsImpl.java
@@ -21,16 +21,21 @@ import android.annotation.Nullable;
import android.app.ActivityManager;
import android.bluetooth.BluetoothActivityEnergyInfo;
import android.bluetooth.UidTraffic;
+import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
+import android.database.ContentObserver;
import android.net.ConnectivityManager;
import android.net.NetworkStats;
+import android.net.Uri;
import android.net.wifi.WifiActivityEnergyInfo;
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.connectivity.WifiBatteryStats;
+import android.os.connectivity.GpsBatteryStats;
import android.os.FileUtils;
import android.os.Handler;
import android.os.IBatteryPropertiesRegistrar;
@@ -46,6 +51,7 @@ import android.os.SystemClock;
import android.os.UserHandle;
import android.os.WorkSource;
import android.os.WorkSource.WorkChain;
+import android.provider.Settings;
import android.telephony.DataConnectionRealTimeInfo;
import android.telephony.ModemActivityInfo;
import android.telephony.ServiceState;
@@ -54,6 +60,7 @@ import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.IntArray;
+import android.util.KeyValueListParser;
import android.util.Log;
import android.util.LogWriter;
import android.util.LongSparseArray;
@@ -73,6 +80,7 @@ import android.view.Display;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.location.gnssmetrics.GnssMetrics;
import com.android.internal.net.NetworkStatsFactory;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FastPrintWriter;
@@ -124,7 +132,7 @@ public class BatteryStatsImpl extends BatteryStats {
private static final int MAGIC = 0xBA757475; // 'BATSTATS'
// Current on-disk Parcel version
- private static final int VERSION = 172 + (USE_OLD_HISTORY ? 1000 : 0);
+ private static final int VERSION = 174 + (USE_OLD_HISTORY ? 1000 : 0);
// Maximum number of items we will record in the history.
private static final int MAX_HISTORY_ITEMS;
@@ -148,11 +156,11 @@ public class BatteryStatsImpl extends BatteryStats {
MAX_HISTORY_BUFFER = 96*1024; // 96KB
MAX_MAX_HISTORY_BUFFER = 128*1024; // 128KB
} else {
- MAX_HISTORY_ITEMS = 2000;
- MAX_MAX_HISTORY_ITEMS = 3000;
- MAX_WAKELOCKS_PER_UID = 100;
- MAX_HISTORY_BUFFER = 256*1024; // 256KB
- MAX_MAX_HISTORY_BUFFER = 320*1024; // 256KB
+ MAX_HISTORY_ITEMS = 4000;
+ MAX_MAX_HISTORY_ITEMS = 6000;
+ MAX_WAKELOCKS_PER_UID = 200;
+ MAX_HISTORY_BUFFER = 512*1024; // 512KB
+ MAX_MAX_HISTORY_BUFFER = 640*1024; // 640KB
}
}
@@ -193,6 +201,12 @@ public class BatteryStatsImpl extends BatteryStats {
protected KernelUidCpuFreqTimeReader mKernelUidCpuFreqTimeReader =
new KernelUidCpuFreqTimeReader();
@VisibleForTesting
+ protected KernelUidCpuActiveTimeReader mKernelUidCpuActiveTimeReader =
+ new KernelUidCpuActiveTimeReader();
+ @VisibleForTesting
+ protected KernelUidCpuClusterTimeReader mKernelUidCpuClusterTimeReader =
+ new KernelUidCpuClusterTimeReader();
+ @VisibleForTesting
protected KernelSingleUidTimeReader mKernelSingleUidTimeReader;
private final KernelMemoryBandwidthStats mKernelMemoryBandwidthStats
@@ -289,12 +303,21 @@ public class BatteryStatsImpl extends BatteryStats {
/**
* Update per-freq cpu times for all the uids in {@link #mPendingUids}.
*/
- public void updateProcStateCpuTimes() {
+ public void updateProcStateCpuTimes(boolean onBattery, boolean onBatteryScreenOff) {
final SparseIntArray uidStates;
synchronized (BatteryStatsImpl.this) {
+ if (!mConstants.TRACK_CPU_TIMES_BY_PROC_STATE) {
+ return;
+ }
if(!initKernelSingleUidTimeReaderLocked()) {
return;
}
+ // If the KernelSingleUidTimeReader has stale cpu times, then we shouldn't try to
+ // compute deltas since it might result in mis-attributing cpu times to wrong states.
+ if (mKernelSingleUidTimeReader.hasStaleData()) {
+ mPendingUids.clear();
+ return;
+ }
if (mPendingUids.size() == 0) {
return;
@@ -307,7 +330,6 @@ public class BatteryStatsImpl extends BatteryStats {
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
@@ -324,7 +346,6 @@ public class BatteryStatsImpl extends BatteryStats {
isolatedUids[j] = u.mChildUids.get(j);
}
}
- onBattery = mOnBatteryInternal;
}
long[] cpuTimesMs = mKernelSingleUidTimeReader.readDeltaMs(uid);
if (isolatedUids != null) {
@@ -335,27 +356,45 @@ public class BatteryStatsImpl extends BatteryStats {
}
if (onBattery && cpuTimesMs != null) {
synchronized (BatteryStatsImpl.this) {
- u.addProcStateTimesMs(procState, cpuTimesMs);
- u.addProcStateScreenOffTimesMs(procState, cpuTimesMs);
+ u.addProcStateTimesMs(procState, cpuTimesMs, onBattery);
+ u.addProcStateScreenOffTimesMs(procState, cpuTimesMs, onBatteryScreenOff);
}
}
}
}
+ public void copyFromAllUidsCpuTimes() {
+ synchronized (BatteryStatsImpl.this) {
+ copyFromAllUidsCpuTimes(
+ mOnBatteryTimeBase.isRunning(), mOnBatteryScreenOffTimeBase.isRunning());
+ }
+ }
+
/**
* 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() {
+ public void copyFromAllUidsCpuTimes(boolean onBattery, boolean onBatteryScreenOff) {
synchronized (BatteryStatsImpl.this) {
+ if (!mConstants.TRACK_CPU_TIMES_BY_PROC_STATE) {
+ return;
+ }
if(!initKernelSingleUidTimeReaderLocked()) {
return;
}
final SparseArray<long[]> allUidCpuFreqTimesMs =
mKernelUidCpuFreqTimeReader.getAllUidCpuFreqTimeMs();
+ // If the KernelSingleUidTimeReader has stale cpu times, then we shouldn't try to
+ // compute deltas since it might result in mis-attributing cpu times to wrong states.
+ if (mKernelSingleUidTimeReader.hasStaleData()) {
+ mKernelSingleUidTimeReader.setAllUidsCpuTimesMs(allUidCpuFreqTimesMs);
+ mKernelSingleUidTimeReader.markDataAsStale(false);
+ mPendingUids.clear();
+ return;
+ }
for (int i = allUidCpuFreqTimesMs.size() - 1; i >= 0; --i) {
final int uid = allUidCpuFreqTimesMs.keyAt(i);
final Uid u = getAvailableUidStatsLocked(mapUid(uid));
@@ -368,7 +407,7 @@ public class BatteryStatsImpl extends BatteryStats {
}
final long[] deltaTimesMs = mKernelSingleUidTimeReader.computeDelta(
uid, cpuTimesMs.clone());
- if (mOnBatteryInternal && deltaTimesMs != null) {
+ if (onBattery && deltaTimesMs != null) {
final int procState;
final int idx = mPendingUids.indexOfKey(uid);
if (idx >= 0) {
@@ -378,8 +417,8 @@ public class BatteryStatsImpl extends BatteryStats {
procState = u.mProcessState;
}
if (procState >= 0 && procState < Uid.NUM_PROCESS_STATE) {
- u.addProcStateTimesMs(procState, deltaTimesMs);
- u.addProcStateScreenOffTimesMs(procState, deltaTimesMs);
+ u.addProcStateTimesMs(procState, deltaTimesMs, onBattery);
+ u.addProcStateScreenOffTimesMs(procState, deltaTimesMs, onBatteryScreenOff);
}
}
}
@@ -443,8 +482,9 @@ public class BatteryStatsImpl extends BatteryStats {
Future<?> scheduleSync(String reason, int flags);
Future<?> scheduleCpuSyncDueToRemovedUid(int uid);
- Future<?> scheduleReadProcStateCpuTimes();
- Future<?> scheduleCopyFromAllUidsCpuTimes();
+ Future<?> scheduleReadProcStateCpuTimes(boolean onBattery, boolean onBatteryScreenOff);
+ Future<?> scheduleCopyFromAllUidsCpuTimes(boolean onBattery, boolean onBatteryScreenOff);
+ Future<?> scheduleCpuSyncDueToSettingChange();
}
public Handler mHandler;
@@ -506,8 +546,8 @@ public class BatteryStatsImpl extends BatteryStats {
final HistoryEventTracker mActiveEvents = new HistoryEventTracker();
long mHistoryBaseTime;
- boolean mHaveBatteryLevel = false;
- boolean mRecordingHistory = false;
+ protected boolean mHaveBatteryLevel = false;
+ protected boolean mRecordingHistory = false;
int mNumHistoryItems;
final Parcel mHistoryBuffer = Parcel.obtain();
@@ -635,6 +675,10 @@ public class BatteryStatsImpl extends BatteryStats {
int mCameraOnNesting;
StopwatchTimer mCameraOnTimer;
+ int mGpsSignalQualityBin = -1;
+ final StopwatchTimer[] mGpsSignalQualityTimer =
+ new StopwatchTimer[GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS];
+
int mPhoneSignalStrengthBin = -1;
int mPhoneSignalStrengthBinRaw = -1;
final StopwatchTimer[] mPhoneSignalStrengthsTimer =
@@ -652,6 +696,14 @@ public class BatteryStatsImpl extends BatteryStats {
new LongSamplingCounter[NUM_NETWORK_ACTIVITY_TYPES];
/**
+ * The WiFi Overall wakelock timer
+ * This timer tracks the actual aggregate time for which MC wakelocks are enabled
+ * since addition of per UID timers would not result in an accurate value due to overlapp of
+ * per uid wakelock timers
+ */
+ StopwatchTimer mWifiMulticastWakelockTimer;
+
+ /**
* The WiFi controller activity (time in tx, rx, idle, and power consumed) for the device.
*/
ControllerActivityCounterImpl mWifiActivity;
@@ -700,6 +752,8 @@ public class BatteryStatsImpl extends BatteryStats {
final StopwatchTimer[] mWifiSignalStrengthsTimer =
new StopwatchTimer[NUM_WIFI_SIGNAL_STRENGTH_BINS];
+ StopwatchTimer mWifiActiveTimer;
+
int mBluetoothScanNesting;
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
protected StopwatchTimer mBluetoothScanTimer;
@@ -799,6 +853,9 @@ public class BatteryStatsImpl extends BatteryStats {
@VisibleForTesting
protected PowerProfile mPowerProfile;
+ @GuardedBy("this")
+ private final Constants mConstants;
+
/*
* Holds a SamplingTimer associated with each Resource Power Manager state and voter,
* recording their times when on-battery (regardless of screen state).
@@ -887,6 +944,7 @@ public class BatteryStatsImpl extends BatteryStats {
mHandler = null;
mPlatformIdleStateCallback = null;
mUserInfoProvider = null;
+ mConstants = new Constants(mHandler);
clearHistoryLocked();
}
@@ -1223,12 +1281,10 @@ public class BatteryStatsImpl extends BatteryStats {
public long[] mCounts;
public long[] mLoadedCounts;
public long[] mUnpluggedCounts;
- public long[] mPluggedCounts;
private LongSamplingCounterArray(TimeBase timeBase, Parcel in) {
mTimeBase = timeBase;
- mPluggedCounts = in.createLongArray();
- mCounts = copyArray(mPluggedCounts, mCounts);
+ mCounts = in.createLongArray();
mLoadedCounts = in.createLongArray();
mUnpluggedCounts = in.createLongArray();
timeBase.add(this);
@@ -1247,17 +1303,16 @@ public class BatteryStatsImpl extends BatteryStats {
@Override
public void onTimeStarted(long elapsedRealTime, long baseUptime, long baseRealtime) {
- mUnpluggedCounts = copyArray(mPluggedCounts, mUnpluggedCounts);
+ mUnpluggedCounts = copyArray(mCounts, mUnpluggedCounts);
}
@Override
public void onTimeStopped(long elapsedRealtime, long baseUptime, long baseRealtime) {
- mPluggedCounts = copyArray(mCounts, mPluggedCounts);
}
@Override
public long[] getCountsLocked(int which) {
- long[] val = copyArray(mTimeBase.isRunning() ? mCounts : mPluggedCounts, null);
+ long[] val = copyArray(mCounts, null);
if (which == STATS_SINCE_UNPLUGGED) {
subtract(val, mUnpluggedCounts);
} else if (which != STATS_SINCE_CHARGED) {
@@ -1270,15 +1325,18 @@ public class BatteryStatsImpl extends BatteryStats {
public void logState(Printer pw, String prefix) {
pw.println(prefix + "mCounts=" + Arrays.toString(mCounts)
+ " mLoadedCounts=" + Arrays.toString(mLoadedCounts)
- + " mUnpluggedCounts=" + Arrays.toString(mUnpluggedCounts)
- + " mPluggedCounts=" + Arrays.toString(mPluggedCounts));
+ + " mUnpluggedCounts=" + Arrays.toString(mUnpluggedCounts));
}
public void addCountLocked(long[] counts) {
+ addCountLocked(counts, mTimeBase.isRunning());
+ }
+
+ public void addCountLocked(long[] counts, boolean isRunning) {
if (counts == null) {
return;
}
- if (mTimeBase.isRunning()) {
+ if (isRunning) {
if (mCounts == null) {
mCounts = new long[counts.length];
}
@@ -1298,7 +1356,6 @@ public class BatteryStatsImpl extends BatteryStats {
public void reset(boolean detachIfReset) {
fillArray(mCounts, 0);
fillArray(mLoadedCounts, 0);
- fillArray(mPluggedCounts, 0);
fillArray(mUnpluggedCounts, 0);
if (detachIfReset) {
detach();
@@ -1317,7 +1374,6 @@ public class BatteryStatsImpl extends BatteryStats {
mCounts = in.createLongArray();
mLoadedCounts = copyArray(mCounts, mLoadedCounts);
mUnpluggedCounts = copyArray(mCounts, mUnpluggedCounts);
- mPluggedCounts = copyArray(mCounts, mPluggedCounts);
}
public static void writeToParcel(Parcel out, LongSamplingCounterArray counterArray) {
@@ -2732,12 +2788,14 @@ public class BatteryStatsImpl extends BatteryStats {
public static class ControllerActivityCounterImpl extends ControllerActivityCounter
implements Parcelable {
private final LongSamplingCounter mIdleTimeMillis;
+ private final LongSamplingCounter mScanTimeMillis;
private final LongSamplingCounter mRxTimeMillis;
private final LongSamplingCounter[] mTxTimeMillis;
private final LongSamplingCounter mPowerDrainMaMs;
public ControllerActivityCounterImpl(TimeBase timeBase, int numTxStates) {
mIdleTimeMillis = new LongSamplingCounter(timeBase);
+ mScanTimeMillis = new LongSamplingCounter(timeBase);
mRxTimeMillis = new LongSamplingCounter(timeBase);
mTxTimeMillis = new LongSamplingCounter[numTxStates];
for (int i = 0; i < numTxStates; i++) {
@@ -2748,6 +2806,7 @@ public class BatteryStatsImpl extends BatteryStats {
public ControllerActivityCounterImpl(TimeBase timeBase, int numTxStates, Parcel in) {
mIdleTimeMillis = new LongSamplingCounter(timeBase, in);
+ mScanTimeMillis = new LongSamplingCounter(timeBase, in);
mRxTimeMillis = new LongSamplingCounter(timeBase, in);
final int recordedTxStates = in.readInt();
if (recordedTxStates != numTxStates) {
@@ -2763,6 +2822,7 @@ public class BatteryStatsImpl extends BatteryStats {
public void readSummaryFromParcel(Parcel in) {
mIdleTimeMillis.readSummaryFromParcelLocked(in);
+ mScanTimeMillis.readSummaryFromParcelLocked(in);
mRxTimeMillis.readSummaryFromParcelLocked(in);
final int recordedTxStates = in.readInt();
if (recordedTxStates != mTxTimeMillis.length) {
@@ -2781,6 +2841,7 @@ public class BatteryStatsImpl extends BatteryStats {
public void writeSummaryToParcel(Parcel dest) {
mIdleTimeMillis.writeSummaryFromParcelLocked(dest);
+ mScanTimeMillis.writeSummaryFromParcelLocked(dest);
mRxTimeMillis.writeSummaryFromParcelLocked(dest);
dest.writeInt(mTxTimeMillis.length);
for (LongSamplingCounter counter : mTxTimeMillis) {
@@ -2792,6 +2853,7 @@ public class BatteryStatsImpl extends BatteryStats {
@Override
public void writeToParcel(Parcel dest, int flags) {
mIdleTimeMillis.writeToParcel(dest);
+ mScanTimeMillis.writeToParcel(dest);
mRxTimeMillis.writeToParcel(dest);
dest.writeInt(mTxTimeMillis.length);
for (LongSamplingCounter counter : mTxTimeMillis) {
@@ -2802,6 +2864,7 @@ public class BatteryStatsImpl extends BatteryStats {
public void reset(boolean detachIfReset) {
mIdleTimeMillis.reset(detachIfReset);
+ mScanTimeMillis.reset(detachIfReset);
mRxTimeMillis.reset(detachIfReset);
for (LongSamplingCounter counter : mTxTimeMillis) {
counter.reset(detachIfReset);
@@ -2811,6 +2874,7 @@ public class BatteryStatsImpl extends BatteryStats {
public void detach() {
mIdleTimeMillis.detach();
+ mScanTimeMillis.detach();
mRxTimeMillis.detach();
for (LongSamplingCounter counter : mTxTimeMillis) {
counter.detach();
@@ -2828,6 +2892,15 @@ public class BatteryStatsImpl extends BatteryStats {
}
/**
+ * @return a LongSamplingCounter, measuring time spent in the scan state in
+ * milliseconds.
+ */
+ @Override
+ public LongSamplingCounter getScanTimeCounter() {
+ return mScanTimeMillis;
+ }
+
+ /**
* @return a LongSamplingCounter, measuring time spent in the receive state in
* milliseconds.
*/
@@ -3775,7 +3848,8 @@ public class BatteryStatsImpl extends BatteryStats {
+ " and battery is " + (unplugged ? "on" : "off"));
}
updateCpuTimeLocked();
- mExternalSync.scheduleCopyFromAllUidsCpuTimes();
+ mExternalSync.scheduleCopyFromAllUidsCpuTimes(mOnBatteryTimeBase.isRunning(),
+ mOnBatteryScreenOffTimeBase.isRunning());
mOnBatteryTimeBase.setRunning(unplugged, uptime, realtime);
if (updateOnBatteryTimeBase) {
@@ -3838,6 +3912,10 @@ public class BatteryStatsImpl extends BatteryStats {
}
mKernelUidCpuTimeReader.removeUid(isolatedUid);
mKernelUidCpuFreqTimeReader.removeUid(isolatedUid);
+ if (mConstants.TRACK_CPU_ACTIVE_CLUSTER_TIME) {
+ mKernelUidCpuActiveTimeReader.removeUid(isolatedUid);
+ mKernelUidCpuClusterTimeReader.removeUid(isolatedUid);
+ }
}
public int mapUid(int uid) {
@@ -3975,30 +4053,85 @@ public class BatteryStatsImpl extends BatteryStats {
addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_JOB_FINISH, name, uid);
}
- public void noteAlarmStartLocked(String name, int uid) {
+ public void noteAlarmStartLocked(String name, WorkSource workSource, int uid) {
+ noteAlarmStartOrFinishLocked(HistoryItem.EVENT_ALARM_START, name, workSource, uid);
+ }
+
+ public void noteAlarmFinishLocked(String name, WorkSource workSource, int uid) {
+ noteAlarmStartOrFinishLocked(HistoryItem.EVENT_ALARM_FINISH, name, workSource, uid);
+ }
+
+ private void noteAlarmStartOrFinishLocked(int historyItem, String name, WorkSource workSource,
+ int uid) {
if (!mRecordAllHistory) {
return;
}
- uid = mapUid(uid);
+
final long elapsedRealtime = mClocks.elapsedRealtime();
final long uptime = mClocks.uptimeMillis();
- if (!mActiveEvents.updateState(HistoryItem.EVENT_ALARM_START, name, uid, 0)) {
- return;
+
+ if (workSource != null) {
+ for (int i = 0; i < workSource.size(); ++i) {
+ uid = mapUid(workSource.get(i));
+ if (mActiveEvents.updateState(historyItem, name, uid, 0)) {
+ addHistoryEventLocked(elapsedRealtime, uptime, historyItem, name, uid);
+ }
+ }
+
+ List<WorkChain> workChains = workSource.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ uid = mapUid(workChains.get(i).getAttributionUid());
+ if (mActiveEvents.updateState(historyItem, name, uid, 0)) {
+ addHistoryEventLocked(elapsedRealtime, uptime, historyItem, name, uid);
+ }
+ }
+ }
+ } else {
+ uid = mapUid(uid);
+
+ if (mActiveEvents.updateState(historyItem, name, uid, 0)) {
+ addHistoryEventLocked(elapsedRealtime, uptime, historyItem, name, uid);
+ }
}
- addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_ALARM_START, name, uid);
}
- public void noteAlarmFinishLocked(String name, int uid) {
- if (!mRecordAllHistory) {
- return;
- }
- uid = mapUid(uid);
- final long elapsedRealtime = mClocks.elapsedRealtime();
- final long uptime = mClocks.uptimeMillis();
- if (!mActiveEvents.updateState(HistoryItem.EVENT_ALARM_FINISH, name, uid, 0)) {
- return;
+ public void noteWakupAlarmLocked(String packageName, int uid, WorkSource workSource,
+ String tag) {
+ if (workSource != null) {
+ for (int i = 0; i < workSource.size(); ++i) {
+ uid = workSource.get(i);
+ final String workSourceName = workSource.getName(i);
+
+ if (isOnBattery()) {
+ BatteryStatsImpl.Uid.Pkg pkg = getPackageStatsLocked(uid,
+ workSourceName != null ? workSourceName : packageName);
+ pkg.noteWakeupAlarmLocked(tag);
+ }
+ StatsLog.write_non_chained(StatsLog.WAKEUP_ALARM_OCCURRED, workSource.get(i),
+ workSource.getName(i), tag);
+ }
+
+ ArrayList<WorkChain> workChains = workSource.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ final WorkChain wc = workChains.get(i);
+ uid = wc.getAttributionUid();
+
+ if (isOnBattery()) {
+ BatteryStatsImpl.Uid.Pkg pkg = getPackageStatsLocked(uid, packageName);
+ pkg.noteWakeupAlarmLocked(tag);
+ }
+ StatsLog.write(StatsLog.WAKEUP_ALARM_OCCURRED, wc.getUids(), wc.getTags(), tag);
+ }
+ }
+ } else {
+ if (isOnBattery()) {
+ BatteryStatsImpl.Uid.Pkg pkg = getPackageStatsLocked(uid, packageName);
+ pkg.noteWakeupAlarmLocked(tag);
+ }
+ StatsLog.write_non_chained(StatsLog.WAKEUP_ALARM_OCCURRED, uid, null, tag);
}
- addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_ALARM_FINISH, name, uid);
}
private void requestWakelockCpuUpdate() {
@@ -4121,6 +4254,9 @@ public class BatteryStatsImpl extends BatteryStats {
if (wc != null) {
StatsLog.write(
StatsLog.WAKELOCK_STATE_CHANGED, wc.getUids(), wc.getTags(), type, name, 1);
+ } else {
+ StatsLog.write_non_chained(StatsLog.WAKELOCK_STATE_CHANGED, uid, null, type, name,
+ 1);
}
}
}
@@ -4161,6 +4297,9 @@ public class BatteryStatsImpl extends BatteryStats {
if (wc != null) {
StatsLog.write(
StatsLog.WAKELOCK_STATE_CHANGED, wc.getUids(), wc.getTags(), type, name, 0);
+ } else {
+ StatsLog.write_non_chained(StatsLog.WAKELOCK_STATE_CHANGED, uid, null, type, name,
+ 0);
}
}
}
@@ -4254,7 +4393,37 @@ public class BatteryStatsImpl extends BatteryStats {
}
public void noteLongPartialWakelockStart(String name, String historyName, int uid) {
+ StatsLog.write_non_chained(StatsLog.LONG_PARTIAL_WAKELOCK_STATE_CHANGED,
+ uid, null, name, historyName, 1);
+
uid = mapUid(uid);
+ noteLongPartialWakeLockStartInternal(name, historyName, uid);
+ }
+
+ public void noteLongPartialWakelockStartFromSource(String name, String historyName,
+ WorkSource workSource) {
+ final int N = workSource.size();
+ for (int i = 0; i < N; ++i) {
+ final int uid = mapUid(workSource.get(i));
+ noteLongPartialWakeLockStartInternal(name, historyName, uid);
+ StatsLog.write_non_chained(StatsLog.LONG_PARTIAL_WAKELOCK_STATE_CHANGED,
+ workSource.get(i), workSource.getName(i), name, historyName, 1);
+ }
+
+ final ArrayList<WorkChain> workChains = workSource.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ final WorkChain workChain = workChains.get(i);
+ final int uid = workChain.getAttributionUid();
+ noteLongPartialWakeLockStartInternal(name, historyName, uid);
+
+ StatsLog.write(StatsLog.LONG_PARTIAL_WAKELOCK_STATE_CHANGED,
+ workChain.getUids(), workChain.getTags(), name, historyName, 1);
+ }
+ }
+ }
+
+ private void noteLongPartialWakeLockStartInternal(String name, String historyName, int uid) {
final long elapsedRealtime = mClocks.elapsedRealtime();
final long uptime = mClocks.uptimeMillis();
if (historyName == null) {
@@ -4266,11 +4435,39 @@ public class BatteryStatsImpl extends BatteryStats {
}
addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_LONG_WAKE_LOCK_START,
historyName, uid);
- StatsLog.write(StatsLog.LONG_PARTIAL_WAKELOCK_STATE_CHANGED, uid, name, historyName, 1);
}
public void noteLongPartialWakelockFinish(String name, String historyName, int uid) {
+ StatsLog.write_non_chained(StatsLog.LONG_PARTIAL_WAKELOCK_STATE_CHANGED,
+ uid, null, name, historyName, 0);
+
uid = mapUid(uid);
+ noteLongPartialWakeLockFinishInternal(name, historyName, uid);
+ }
+
+ public void noteLongPartialWakelockFinishFromSource(String name, String historyName,
+ WorkSource workSource) {
+ final int N = workSource.size();
+ for (int i = 0; i < N; ++i) {
+ final int uid = mapUid(workSource.get(i));
+ noteLongPartialWakeLockFinishInternal(name, historyName, uid);
+ StatsLog.write_non_chained(StatsLog.LONG_PARTIAL_WAKELOCK_STATE_CHANGED,
+ workSource.get(i), workSource.getName(i), name, historyName, 0);
+ }
+
+ final ArrayList<WorkChain> workChains = workSource.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ final WorkChain workChain = workChains.get(i);
+ final int uid = workChain.getAttributionUid();
+ noteLongPartialWakeLockFinishInternal(name, historyName, uid);
+ StatsLog.write(StatsLog.LONG_PARTIAL_WAKELOCK_STATE_CHANGED,
+ workChain.getUids(), workChain.getTags(), name, historyName, 0);
+ }
+ }
+ }
+
+ private void noteLongPartialWakeLockFinishInternal(String name, String historyName, int uid) {
final long elapsedRealtime = mClocks.elapsedRealtime();
final long uptime = mClocks.uptimeMillis();
if (historyName == null) {
@@ -4282,7 +4479,6 @@ public class BatteryStatsImpl extends BatteryStats {
}
addHistoryEventLocked(elapsedRealtime, uptime, HistoryItem.EVENT_LONG_WAKE_LOCK_FINISH,
historyName, uid);
- StatsLog.write(StatsLog.LONG_PARTIAL_WAKELOCK_STATE_CHANGED, uid, name, historyName, 0);
}
void aggregateLastWakeupUptimeLocked(long uptimeMs) {
@@ -4415,10 +4611,37 @@ public class BatteryStatsImpl extends BatteryStats {
if (DEBUG_HISTORY) Slog.v(TAG, "Stop GPS to: "
+ Integer.toHexString(mHistoryCur.states));
addHistoryRecordLocked(elapsedRealtime, uptime);
+ stopAllGpsSignalQualityTimersLocked(-1);
+ mGpsSignalQualityBin = -1;
}
getUidStatsLocked(uid).noteStopGps(elapsedRealtime);
}
+ public void noteGpsSignalQualityLocked(int signalLevel) {
+ if (mGpsNesting == 0) {
+ return;
+ }
+ if (signalLevel < 0 || signalLevel >= GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS) {
+ stopAllGpsSignalQualityTimersLocked(-1);
+ return;
+ }
+ final long elapsedRealtime = mClocks.elapsedRealtime();
+ final long uptime = mClocks.uptimeMillis();
+ if (mGpsSignalQualityBin != signalLevel) {
+ if (mGpsSignalQualityBin >= 0) {
+ mGpsSignalQualityTimer[mGpsSignalQualityBin].stopRunningLocked(elapsedRealtime);
+ }
+ if(!mGpsSignalQualityTimer[signalLevel].isRunningLocked()) {
+ mGpsSignalQualityTimer[signalLevel].startRunningLocked(elapsedRealtime);
+ }
+ mHistoryCur.states2 = (mHistoryCur.states2&~HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK)
+ | (signalLevel << HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT);
+ addHistoryRecordLocked(elapsedRealtime, uptime);
+ mGpsSignalQualityBin = signalLevel;
+ }
+ return;
+ }
+
public void noteScreenStateLocked(int state) {
state = mPretendScreenOff ? Display.STATE_OFF : state;
@@ -4752,6 +4975,18 @@ public class BatteryStatsImpl extends BatteryStats {
mDailyPackageChanges.add(pc);
}
+ void stopAllGpsSignalQualityTimersLocked(int except) {
+ final long elapsedRealtime = mClocks.elapsedRealtime();
+ for (int i = 0; i < GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS; i++) {
+ if (i == except) {
+ continue;
+ }
+ while (mGpsSignalQualityTimer[i].isRunningLocked()) {
+ mGpsSignalQualityTimer[i].stopRunningLocked(elapsedRealtime);
+ }
+ }
+ }
+
public void notePhoneOnLocked() {
if (!mPhoneOn) {
final long elapsedRealtime = mClocks.elapsedRealtime();
@@ -5219,8 +5454,9 @@ public class BatteryStatsImpl extends BatteryStats {
}
}
- private void noteBluetoothScanStartedLocked(int uid, boolean isUnoptimized) {
- uid = mapUid(uid);
+ private void noteBluetoothScanStartedLocked(WorkChain workChain, int uid,
+ boolean isUnoptimized) {
+ uid = getAttributionUid(uid, workChain);
final long elapsedRealtime = mClocks.elapsedRealtime();
final long uptime = mClocks.uptimeMillis();
if (mBluetoothScanNesting == 0) {
@@ -5231,18 +5467,45 @@ public class BatteryStatsImpl extends BatteryStats {
mBluetoothScanTimer.startRunningLocked(elapsedRealtime);
}
mBluetoothScanNesting++;
+
+ if (workChain != null) {
+ StatsLog.write(StatsLog.BLE_SCAN_STATE_CHANGED,
+ workChain.getUids(), workChain.getTags(), 1);
+ if (isUnoptimized) {
+ StatsLog.write(StatsLog.BLE_UNOPTIMIZED_SCAN_STATE_CHANGED,
+ workChain.getUids(), workChain.getTags(), 1);
+ }
+ } else {
+ StatsLog.write_non_chained(StatsLog.BLE_SCAN_STATE_CHANGED, uid, null, 1);
+ if (isUnoptimized) {
+ StatsLog.write_non_chained(StatsLog.BLE_UNOPTIMIZED_SCAN_STATE_CHANGED, uid, null,
+ 1);
+ }
+ }
+
getUidStatsLocked(uid).noteBluetoothScanStartedLocked(elapsedRealtime, isUnoptimized);
+ if (workChain != null) {
+ getUidStatsLocked(uid).addBluetoothWorkChain(workChain, isUnoptimized);
+ }
}
public void noteBluetoothScanStartedFromSourceLocked(WorkSource ws, boolean isUnoptimized) {
final int N = ws.size();
for (int i = 0; i < N; i++) {
- noteBluetoothScanStartedLocked(ws.get(i), isUnoptimized);
+ noteBluetoothScanStartedLocked(null, ws.get(i), isUnoptimized);
+ }
+
+ final List<WorkChain> workChains = ws.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ noteBluetoothScanStartedLocked(workChains.get(i), -1, isUnoptimized);
+ }
}
}
- private void noteBluetoothScanStoppedLocked(int uid, boolean isUnoptimized) {
- uid = mapUid(uid);
+ private void noteBluetoothScanStoppedLocked(WorkChain workChain, int uid,
+ boolean isUnoptimized) {
+ uid = getAttributionUid(uid, workChain);
final long elapsedRealtime = mClocks.elapsedRealtime();
final long uptime = mClocks.uptimeMillis();
mBluetoothScanNesting--;
@@ -5253,13 +5516,47 @@ public class BatteryStatsImpl extends BatteryStats {
addHistoryRecordLocked(elapsedRealtime, uptime);
mBluetoothScanTimer.stopRunningLocked(elapsedRealtime);
}
+
+ if (workChain != null) {
+ StatsLog.write(
+ StatsLog.BLE_SCAN_STATE_CHANGED, workChain.getUids(), workChain.getTags(), 0);
+ if (isUnoptimized) {
+ StatsLog.write(StatsLog.BLE_UNOPTIMIZED_SCAN_STATE_CHANGED,
+ workChain.getUids(), workChain.getTags(), 0);
+ }
+ } else {
+ StatsLog.write_non_chained(StatsLog.BLE_SCAN_STATE_CHANGED, uid, null, 0);
+ if (isUnoptimized) {
+ StatsLog.write_non_chained(StatsLog.BLE_UNOPTIMIZED_SCAN_STATE_CHANGED, uid, null,
+ 0);
+ }
+ }
+
getUidStatsLocked(uid).noteBluetoothScanStoppedLocked(elapsedRealtime, isUnoptimized);
+ if (workChain != null) {
+ getUidStatsLocked(uid).removeBluetoothWorkChain(workChain, isUnoptimized);
+ }
+ }
+
+ private int getAttributionUid(int uid, WorkChain workChain) {
+ if (workChain != null) {
+ return mapUid(workChain.getAttributionUid());
+ }
+
+ return mapUid(uid);
}
public void noteBluetoothScanStoppedFromSourceLocked(WorkSource ws, boolean isUnoptimized) {
final int N = ws.size();
for (int i = 0; i < N; i++) {
- noteBluetoothScanStoppedLocked(ws.get(i), isUnoptimized);
+ noteBluetoothScanStoppedLocked(null, ws.get(i), isUnoptimized);
+ }
+
+ final List<WorkChain> workChains = ws.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ noteBluetoothScanStoppedLocked(workChains.get(i), -1, isUnoptimized);
+ }
}
}
@@ -5273,9 +5570,31 @@ public class BatteryStatsImpl extends BatteryStats {
+ Integer.toHexString(mHistoryCur.states2));
addHistoryRecordLocked(elapsedRealtime, uptime);
mBluetoothScanTimer.stopAllRunningLocked(elapsedRealtime);
+
+
for (int i=0; i<mUidStats.size(); i++) {
BatteryStatsImpl.Uid uid = mUidStats.valueAt(i);
uid.noteResetBluetoothScanLocked(elapsedRealtime);
+
+ List<WorkChain> allWorkChains = uid.getAllBluetoothWorkChains();
+ if (allWorkChains != null) {
+ for (int j = 0; j < allWorkChains.size(); ++j) {
+ StatsLog.write(StatsLog.BLE_SCAN_STATE_CHANGED,
+ allWorkChains.get(j).getUids(),
+ allWorkChains.get(j).getTags(), 0);
+ }
+ allWorkChains.clear();
+ }
+
+ List<WorkChain> unoptimizedWorkChains = uid.getUnoptimizedBluetoothWorkChains();
+ if (unoptimizedWorkChains != null) {
+ for (int j = 0; j < unoptimizedWorkChains.size(); ++j) {
+ StatsLog.write(StatsLog.BLE_UNOPTIMIZED_SCAN_STATE_CHANGED,
+ unoptimizedWorkChains.get(j).getUids(),
+ unoptimizedWorkChains.get(j).getTags(), 0);
+ }
+ unoptimizedWorkChains.clear();
+ }
}
}
}
@@ -5285,6 +5604,19 @@ public class BatteryStatsImpl extends BatteryStats {
for (int i = 0; i < N; i++) {
int uid = mapUid(ws.get(i));
getUidStatsLocked(uid).noteBluetoothScanResultsLocked(numNewResults);
+ StatsLog.write_non_chained(StatsLog.BLE_SCAN_RESULT_RECEIVED, ws.get(i), ws.getName(i),
+ numNewResults);
+ }
+
+ final List<WorkChain> workChains = ws.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ final WorkChain wc = workChains.get(i);
+ int uid = mapUid(wc.getAttributionUid());
+ getUidStatsLocked(uid).noteBluetoothScanResultsLocked(numNewResults);
+ StatsLog.write(StatsLog.BLE_SCAN_RESULT_RECEIVED,
+ wc.getUids(), wc.getTags(), numNewResults);
+ }
}
}
@@ -5308,8 +5640,11 @@ public class BatteryStatsImpl extends BatteryStats {
noteWifiRadioApWakeupLocked(elapsedRealtime, uptime, uid);
}
mHistoryCur.states |= HistoryItem.STATE_WIFI_RADIO_ACTIVE_FLAG;
+ mWifiActiveTimer.startRunningLocked(elapsedRealtime);
} else {
mHistoryCur.states &= ~HistoryItem.STATE_WIFI_RADIO_ACTIVE_FLAG;
+ mWifiActiveTimer.stopRunningLocked(
+ timestampNs / (1000 * 1000));
}
if (DEBUG_HISTORY) Slog.v(TAG, "Wifi network active " + active + " to: "
+ Integer.toHexString(mHistoryCur.states));
@@ -5334,6 +5669,15 @@ public class BatteryStatsImpl extends BatteryStats {
int uid = mapUid(ws.get(i));
getUidStatsLocked(uid).noteWifiRunningLocked(elapsedRealtime);
}
+
+ List<WorkChain> workChains = ws.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ int uid = mapUid(workChains.get(i).getAttributionUid());
+ getUidStatsLocked(uid).noteWifiRunningLocked(elapsedRealtime);
+ }
+ }
+
scheduleSyncExternalStatsLocked("wifi-running", ExternalStatsSync.UPDATE_WIFI);
} else {
Log.w(TAG, "noteWifiRunningLocked -- called while WIFI running");
@@ -5348,11 +5692,28 @@ public class BatteryStatsImpl extends BatteryStats {
int uid = mapUid(oldWs.get(i));
getUidStatsLocked(uid).noteWifiStoppedLocked(elapsedRealtime);
}
+
+ List<WorkChain> workChains = oldWs.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ int uid = mapUid(workChains.get(i).getAttributionUid());
+ getUidStatsLocked(uid).noteWifiStoppedLocked(elapsedRealtime);
+ }
+ }
+
N = newWs.size();
for (int i=0; i<N; i++) {
int uid = mapUid(newWs.get(i));
getUidStatsLocked(uid).noteWifiRunningLocked(elapsedRealtime);
}
+
+ workChains = newWs.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ int uid = mapUid(workChains.get(i).getAttributionUid());
+ getUidStatsLocked(uid).noteWifiRunningLocked(elapsedRealtime);
+ }
+ }
} else {
Log.w(TAG, "noteWifiRunningChangedLocked -- called while WIFI not running");
}
@@ -5373,6 +5734,15 @@ public class BatteryStatsImpl extends BatteryStats {
int uid = mapUid(ws.get(i));
getUidStatsLocked(uid).noteWifiStoppedLocked(elapsedRealtime);
}
+
+ List<WorkChain> workChains = ws.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ int uid = mapUid(workChains.get(i).getAttributionUid());
+ getUidStatsLocked(uid).noteWifiStoppedLocked(elapsedRealtime);
+ }
+ }
+
scheduleSyncExternalStatsLocked("wifi-stopped", ExternalStatsSync.UPDATE_WIFI);
} else {
Log.w(TAG, "noteWifiStoppedLocked -- called while WIFI not running");
@@ -5454,7 +5824,6 @@ public class BatteryStatsImpl extends BatteryStats {
int mWifiFullLockNesting = 0;
public void noteFullWifiLockAcquiredLocked(int uid) {
- uid = mapUid(uid);
final long elapsedRealtime = mClocks.elapsedRealtime();
final long uptime = mClocks.uptimeMillis();
if (mWifiFullLockNesting == 0) {
@@ -5468,7 +5837,6 @@ public class BatteryStatsImpl extends BatteryStats {
}
public void noteFullWifiLockReleasedLocked(int uid) {
- uid = mapUid(uid);
final long elapsedRealtime = mClocks.elapsedRealtime();
final long uptime = mClocks.uptimeMillis();
mWifiFullLockNesting--;
@@ -5484,7 +5852,6 @@ public class BatteryStatsImpl extends BatteryStats {
int mWifiScanNesting = 0;
public void noteWifiScanStartedLocked(int uid) {
- uid = mapUid(uid);
final long elapsedRealtime = mClocks.elapsedRealtime();
final long uptime = mClocks.uptimeMillis();
if (mWifiScanNesting == 0) {
@@ -5498,7 +5865,6 @@ public class BatteryStatsImpl extends BatteryStats {
}
public void noteWifiScanStoppedLocked(int uid) {
- uid = mapUid(uid);
final long elapsedRealtime = mClocks.elapsedRealtime();
final long uptime = mClocks.uptimeMillis();
mWifiScanNesting--;
@@ -5534,6 +5900,12 @@ public class BatteryStatsImpl extends BatteryStats {
if (DEBUG_HISTORY) Slog.v(TAG, "WIFI multicast on to: "
+ Integer.toHexString(mHistoryCur.states));
addHistoryRecordLocked(elapsedRealtime, uptime);
+
+ // Start Wifi Multicast overall timer
+ if (!mWifiMulticastWakelockTimer.isRunningLocked()) {
+ if (DEBUG_HISTORY) Slog.v(TAG, "WiFi Multicast Overall Timer Started");
+ mWifiMulticastWakelockTimer.startRunningLocked(elapsedRealtime);
+ }
}
mWifiMulticastNesting++;
getUidStatsLocked(uid).noteWifiMulticastEnabledLocked(elapsedRealtime);
@@ -5549,6 +5921,12 @@ public class BatteryStatsImpl extends BatteryStats {
if (DEBUG_HISTORY) Slog.v(TAG, "WIFI multicast off to: "
+ Integer.toHexString(mHistoryCur.states));
addHistoryRecordLocked(elapsedRealtime, uptime);
+
+ // Stop Wifi Multicast overall timer
+ if (mWifiMulticastWakelockTimer.isRunningLocked()) {
+ if (DEBUG_HISTORY) Slog.v(TAG, "Multicast Overall Timer Stopped");
+ mWifiMulticastWakelockTimer.stopRunningLocked(elapsedRealtime);
+ }
}
getUidStatsLocked(uid).noteWifiMulticastDisabledLocked(elapsedRealtime);
}
@@ -5556,28 +5934,82 @@ public class BatteryStatsImpl extends BatteryStats {
public void noteFullWifiLockAcquiredFromSourceLocked(WorkSource ws) {
int N = ws.size();
for (int i=0; i<N; i++) {
- noteFullWifiLockAcquiredLocked(ws.get(i));
+ final int uid = mapUid(ws.get(i));
+ noteFullWifiLockAcquiredLocked(uid);
+ StatsLog.write_non_chained(StatsLog.WIFI_LOCK_STATE_CHANGED, ws.get(i), ws.getName(i), 1);
+ }
+
+ final List<WorkChain> workChains = ws.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ final WorkChain workChain = workChains.get(i);
+ final int uid = mapUid(workChain.getAttributionUid());
+ noteFullWifiLockAcquiredLocked(uid);
+ StatsLog.write(StatsLog.WIFI_LOCK_STATE_CHANGED,
+ workChain.getUids(), workChain.getTags(), 1);
+ }
}
}
public void noteFullWifiLockReleasedFromSourceLocked(WorkSource ws) {
int N = ws.size();
for (int i=0; i<N; i++) {
- noteFullWifiLockReleasedLocked(ws.get(i));
+ final int uid = mapUid(ws.get(i));
+ noteFullWifiLockReleasedLocked(uid);
+ StatsLog.write_non_chained(StatsLog.WIFI_LOCK_STATE_CHANGED, ws.get(i), ws.getName(i), 0);
+ }
+
+ final List<WorkChain> workChains = ws.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ final WorkChain workChain = workChains.get(i);
+ final int uid = mapUid(workChain.getAttributionUid());
+ noteFullWifiLockReleasedLocked(uid);
+ StatsLog.write(StatsLog.WIFI_LOCK_STATE_CHANGED,
+ workChain.getUids(), workChain.getTags(), 0);
+ }
}
}
public void noteWifiScanStartedFromSourceLocked(WorkSource ws) {
int N = ws.size();
for (int i=0; i<N; i++) {
- noteWifiScanStartedLocked(ws.get(i));
+ final int uid = mapUid(ws.get(i));
+ noteWifiScanStartedLocked(uid);
+ StatsLog.write_non_chained(StatsLog.WIFI_SCAN_STATE_CHANGED, ws.get(i), ws.getName(i),
+ 1);
+ }
+
+ final List<WorkChain> workChains = ws.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ final WorkChain workChain = workChains.get(i);
+ final int uid = mapUid(workChain.getAttributionUid());
+ noteWifiScanStartedLocked(uid);
+ StatsLog.write(StatsLog.WIFI_SCAN_STATE_CHANGED, workChain.getUids(),
+ workChain.getTags(), 1);
+ }
}
}
public void noteWifiScanStoppedFromSourceLocked(WorkSource ws) {
int N = ws.size();
for (int i=0; i<N; i++) {
- noteWifiScanStoppedLocked(ws.get(i));
+ final int uid = mapUid(ws.get(i));
+ noteWifiScanStoppedLocked(uid);
+ StatsLog.write_non_chained(StatsLog.WIFI_SCAN_STATE_CHANGED, ws.get(i), ws.getName(i),
+ 0);
+ }
+
+ final List<WorkChain> workChains = ws.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ final WorkChain workChain = workChains.get(i);
+ final int uid = mapUid(workChain.getAttributionUid());
+ noteWifiScanStoppedLocked(uid);
+ StatsLog.write(StatsLog.WIFI_SCAN_STATE_CHANGED,
+ workChain.getUids(), workChain.getTags(), 0);
+ }
}
}
@@ -5586,26 +6018,26 @@ public class BatteryStatsImpl extends BatteryStats {
for (int i=0; i<N; i++) {
noteWifiBatchedScanStartedLocked(ws.get(i), csph);
}
- }
- public void noteWifiBatchedScanStoppedFromSourceLocked(WorkSource ws) {
- int N = ws.size();
- for (int i=0; i<N; i++) {
- noteWifiBatchedScanStoppedLocked(ws.get(i));
+ final List<WorkChain> workChains = ws.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ noteWifiBatchedScanStartedLocked(workChains.get(i).getAttributionUid(), csph);
+ }
}
}
- public void noteWifiMulticastEnabledFromSourceLocked(WorkSource ws) {
+ public void noteWifiBatchedScanStoppedFromSourceLocked(WorkSource ws) {
int N = ws.size();
for (int i=0; i<N; i++) {
- noteWifiMulticastEnabledLocked(ws.get(i));
+ noteWifiBatchedScanStoppedLocked(ws.get(i));
}
- }
- public void noteWifiMulticastDisabledFromSourceLocked(WorkSource ws) {
- int N = ws.size();
- for (int i=0; i<N; i++) {
- noteWifiMulticastDisabledLocked(ws.get(i));
+ final List<WorkChain> workChains = ws.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ noteWifiBatchedScanStoppedLocked(workChains.get(i).getAttributionUid());
+ }
}
}
@@ -5769,6 +6201,32 @@ public class BatteryStatsImpl extends BatteryStats {
return val;
}
+ @Override public long getGpsSignalQualityTime(int strengthBin,
+ long elapsedRealtimeUs, int which) {
+ if (strengthBin < 0 || strengthBin >= GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS) {
+ return 0;
+ }
+ return mGpsSignalQualityTimer[strengthBin].getTotalTimeLocked(
+ elapsedRealtimeUs, which);
+ }
+
+ @Override public long getGpsBatteryDrainMaMs() {
+ final double opVolt = mPowerProfile.getAveragePower(
+ PowerProfile.POWER_GPS_OPERATING_VOLTAGE) / 1000.0;
+ if (opVolt == 0) {
+ return 0;
+ }
+ double energyUsedMaMs = 0.0;
+ final int which = STATS_SINCE_CHARGED;
+ final long rawRealtime = SystemClock.elapsedRealtime() * 1000;
+ for(int i=0; i < GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS; i++) {
+ energyUsedMaMs
+ += mPowerProfile.getAveragePower(PowerProfile.POWER_GPS_SIGNAL_QUALITY_BASED, i)
+ * (getGpsSignalQualityTime(i, rawRealtime, which) / 1000);
+ }
+ return (long) energyUsedMaMs;
+ }
+
@Override public long getPhoneOnTime(long elapsedRealtimeUs, int which) {
return mPhoneOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
}
@@ -5835,10 +6293,24 @@ public class BatteryStatsImpl extends BatteryStats {
return (int)mMobileRadioActiveUnknownCount.getCountLocked(which);
}
+ @Override public long getWifiMulticastWakelockTime(
+ long elapsedRealtimeUs, int which) {
+ return mWifiMulticastWakelockTimer.getTotalTimeLocked(
+ elapsedRealtimeUs, which);
+ }
+
+ @Override public int getWifiMulticastWakelockCount(int which) {
+ return mWifiMulticastWakelockTimer.getCountLocked(which);
+ }
+
@Override public long getWifiOnTime(long elapsedRealtimeUs, int which) {
return mWifiOnTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
}
+ @Override public long getWifiActiveTime(long elapsedRealtimeUs, int which) {
+ return mWifiActiveTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
+ }
+
@Override public long getGlobalWifiRunningTime(long elapsedRealtimeUs, int which) {
return mGlobalWifiRunningTimer.getTotalTimeLocked(elapsedRealtimeUs, which);
}
@@ -6115,9 +6587,11 @@ public class BatteryStatsImpl extends BatteryStats {
LongSamplingCounter mUserCpuTime;
LongSamplingCounter mSystemCpuTime;
LongSamplingCounter[][] mCpuClusterSpeedTimesUs;
+ LongSamplingCounter mCpuActiveTimeMs;
LongSamplingCounterArray mCpuFreqTimeMs;
LongSamplingCounterArray mScreenOffCpuFreqTimeMs;
+ LongSamplingCounterArray mCpuClusterTimesMs;
LongSamplingCounterArray[] mProcStateTimeMs;
LongSamplingCounterArray[] mProcStateScreenOffTimeMs;
@@ -6164,6 +6638,15 @@ public class BatteryStatsImpl extends BatteryStats {
*/
final SparseArray<Pid> mPids = new SparseArray<>();
+ /**
+ * The list of WorkChains associated with active bluetooth scans.
+ *
+ * NOTE: This is a hack and it only needs to exist because there's a "reset" API that is
+ * supposed to stop and log all WorkChains that were currently active.
+ */
+ ArrayList<WorkChain> mAllBluetoothChains = null;
+ ArrayList<WorkChain> mUnoptimizedBluetoothChains = null;
+
public Uid(BatteryStatsImpl bsi, int uid) {
mBsi = bsi;
mUid = uid;
@@ -6178,6 +6661,8 @@ public class BatteryStatsImpl extends BatteryStats {
mUserCpuTime = new LongSamplingCounter(mBsi.mOnBatteryTimeBase);
mSystemCpuTime = new LongSamplingCounter(mBsi.mOnBatteryTimeBase);
+ mCpuActiveTimeMs = new LongSamplingCounter(mBsi.mOnBatteryTimeBase);
+ mCpuClusterTimesMs = new LongSamplingCounterArray(mBsi.mOnBatteryTimeBase);
mWakelockStats = mBsi.new OverflowArrayMap<Wakelock>(uid) {
@Override public Wakelock instantiateObject() {
@@ -6225,6 +6710,17 @@ public class BatteryStatsImpl extends BatteryStats {
}
@Override
+ public long getCpuActiveTime() {
+ return mCpuActiveTimeMs.getCountLocked(STATS_SINCE_CHARGED);
+ }
+
+ @Override
+ public long[] getCpuClusterTimes() {
+ return nullIfAllZeros(mCpuClusterTimesMs, STATS_SINCE_CHARGED);
+ }
+
+
+ @Override
public long[] getCpuFreqTimes(int which, int procState) {
if (which < 0 || which >= NUM_PROCESS_STATE) {
return null;
@@ -6288,7 +6784,7 @@ public class BatteryStatsImpl extends BatteryStats {
return null;
}
- private void addProcStateTimesMs(int procState, long[] cpuTimesMs) {
+ private void addProcStateTimesMs(int procState, long[] cpuTimesMs, boolean onBattery) {
if (mProcStateTimeMs == null) {
mProcStateTimeMs = new LongSamplingCounterArray[NUM_PROCESS_STATE];
}
@@ -6297,10 +6793,11 @@ public class BatteryStatsImpl extends BatteryStats {
mProcStateTimeMs[procState] = new LongSamplingCounterArray(
mBsi.mOnBatteryTimeBase);
}
- mProcStateTimeMs[procState].addCountLocked(cpuTimesMs);
+ mProcStateTimeMs[procState].addCountLocked(cpuTimesMs, onBattery);
}
- private void addProcStateScreenOffTimesMs(int procState, long[] cpuTimesMs) {
+ private void addProcStateScreenOffTimesMs(int procState, long[] cpuTimesMs,
+ boolean onBatteryScreenOff) {
if (mProcStateScreenOffTimeMs == null) {
mProcStateScreenOffTimeMs = new LongSamplingCounterArray[NUM_PROCESS_STATE];
}
@@ -6309,7 +6806,7 @@ public class BatteryStatsImpl extends BatteryStats {
mProcStateScreenOffTimeMs[procState] = new LongSamplingCounterArray(
mBsi.mOnBatteryScreenOffTimeBase);
}
- mProcStateScreenOffTimeMs[procState].addCountLocked(cpuTimesMs);
+ mProcStateScreenOffTimeMs[procState].addCountLocked(cpuTimesMs, onBatteryScreenOff);
}
@Override
@@ -6391,8 +6888,6 @@ public class BatteryStatsImpl extends BatteryStats {
mBsi.mFullWifiLockTimers, mBsi.mOnBatteryTimeBase);
}
mFullWifiLockTimer.startRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.WIFI_LOCK_STATE_CHANGED, getUid(), 1);
}
}
@@ -6401,10 +6896,6 @@ public class BatteryStatsImpl extends BatteryStats {
if (mFullWifiLockOut) {
mFullWifiLockOut = false;
mFullWifiLockTimer.stopRunningLocked(elapsedRealtimeMs);
- if (!mFullWifiLockTimer.isRunningLocked()) { // only tell statsd if truly stopped
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.WIFI_LOCK_STATE_CHANGED, getUid(), 0);
- }
}
}
@@ -6418,8 +6909,6 @@ public class BatteryStatsImpl extends BatteryStats {
mOnBatteryBackgroundTimeBase);
}
mWifiScanTimer.startRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.WIFI_SCAN_STATE_CHANGED, getUid(), 1);
}
}
@@ -6428,10 +6917,6 @@ public class BatteryStatsImpl extends BatteryStats {
if (mWifiScanStarted) {
mWifiScanStarted = false;
mWifiScanTimer.stopRunningLocked(elapsedRealtimeMs);
- if (!mWifiScanTimer.isRunningLocked()) { // only tell statsd if truly stopped
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.WIFI_SCAN_STATE_CHANGED, getUid(), 0);
- }
}
}
@@ -6474,6 +6959,8 @@ public class BatteryStatsImpl extends BatteryStats {
WIFI_MULTICAST_ENABLED, mBsi.mWifiMulticastTimers, mBsi.mOnBatteryTimeBase);
}
mWifiMulticastTimer.startRunningLocked(elapsedRealtimeMs);
+ StatsLog.write_non_chained(
+ StatsLog.WIFI_MULTICAST_LOCK_STATE_CHANGED, getUid(), null, 1);
}
}
@@ -6482,6 +6969,8 @@ public class BatteryStatsImpl extends BatteryStats {
if (mWifiMulticastEnabled) {
mWifiMulticastEnabled = false;
mWifiMulticastTimer.stopRunningLocked(elapsedRealtimeMs);
+ StatsLog.write_non_chained(
+ StatsLog.WIFI_MULTICAST_LOCK_STATE_CHANGED, getUid(), null, 0);
}
}
@@ -6534,16 +7023,14 @@ public class BatteryStatsImpl extends BatteryStats {
public void noteAudioTurnedOnLocked(long elapsedRealtimeMs) {
createAudioTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.AUDIO_STATE_CHANGED, getUid(), 1);
+ StatsLog.write_non_chained(StatsLog.AUDIO_STATE_CHANGED, getUid(), null, 1);
}
public void noteAudioTurnedOffLocked(long elapsedRealtimeMs) {
if (mAudioTurnedOnTimer != null) {
mAudioTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs);
if (!mAudioTurnedOnTimer.isRunningLocked()) { // only tell statsd if truly stopped
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.AUDIO_STATE_CHANGED, getUid(), 0);
+ StatsLog.write_non_chained(StatsLog.AUDIO_STATE_CHANGED, getUid(), null, 0);
}
}
}
@@ -6551,8 +7038,7 @@ public class BatteryStatsImpl extends BatteryStats {
public void noteResetAudioLocked(long elapsedRealtimeMs) {
if (mAudioTurnedOnTimer != null) {
mAudioTurnedOnTimer.stopAllRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.AUDIO_STATE_CHANGED, getUid(), 0);
+ StatsLog.write_non_chained(StatsLog.AUDIO_STATE_CHANGED, getUid(), null, 0);
}
}
@@ -6566,16 +7052,15 @@ public class BatteryStatsImpl extends BatteryStats {
public void noteVideoTurnedOnLocked(long elapsedRealtimeMs) {
createVideoTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.MEDIA_CODEC_ACTIVITY_CHANGED, getUid(), 1);
+ StatsLog.write_non_chained(StatsLog.MEDIA_CODEC_ACTIVITY_CHANGED, getUid(), null, 1);
}
public void noteVideoTurnedOffLocked(long elapsedRealtimeMs) {
if (mVideoTurnedOnTimer != null) {
mVideoTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs);
if (!mVideoTurnedOnTimer.isRunningLocked()) { // only tell statsd if truly stopped
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.MEDIA_CODEC_ACTIVITY_CHANGED, getUid(), 0);
+ StatsLog.write_non_chained(StatsLog.MEDIA_CODEC_ACTIVITY_CHANGED, getUid(),
+ null, 0);
}
}
}
@@ -6583,8 +7068,8 @@ public class BatteryStatsImpl extends BatteryStats {
public void noteResetVideoLocked(long elapsedRealtimeMs) {
if (mVideoTurnedOnTimer != null) {
mVideoTurnedOnTimer.stopAllRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.MEDIA_CODEC_ACTIVITY_CHANGED, getUid(), 0);
+ StatsLog.write_non_chained(StatsLog.MEDIA_CODEC_ACTIVITY_CHANGED, getUid(), null,
+ 0);
}
}
@@ -6598,16 +7083,15 @@ public class BatteryStatsImpl extends BatteryStats {
public void noteFlashlightTurnedOnLocked(long elapsedRealtimeMs) {
createFlashlightTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.FLASHLIGHT_STATE_CHANGED, getUid(), 1);
+ StatsLog.write_non_chained(StatsLog.FLASHLIGHT_STATE_CHANGED, getUid(), null,1);
}
public void noteFlashlightTurnedOffLocked(long elapsedRealtimeMs) {
if (mFlashlightTurnedOnTimer != null) {
mFlashlightTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs);
if (!mFlashlightTurnedOnTimer.isRunningLocked()) {
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.FLASHLIGHT_STATE_CHANGED, getUid(), 0);
+ StatsLog.write_non_chained(StatsLog.FLASHLIGHT_STATE_CHANGED, getUid(), null,
+ 0);
}
}
}
@@ -6615,8 +7099,7 @@ public class BatteryStatsImpl extends BatteryStats {
public void noteResetFlashlightLocked(long elapsedRealtimeMs) {
if (mFlashlightTurnedOnTimer != null) {
mFlashlightTurnedOnTimer.stopAllRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.FLASHLIGHT_STATE_CHANGED, getUid(), 0);
+ StatsLog.write_non_chained(StatsLog.FLASHLIGHT_STATE_CHANGED, getUid(), null, 0);
}
}
@@ -6630,16 +7113,14 @@ public class BatteryStatsImpl extends BatteryStats {
public void noteCameraTurnedOnLocked(long elapsedRealtimeMs) {
createCameraTurnedOnTimerLocked().startRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.CAMERA_STATE_CHANGED, getUid(), 1);
+ StatsLog.write_non_chained(StatsLog.CAMERA_STATE_CHANGED, getUid(), null, 1);
}
public void noteCameraTurnedOffLocked(long elapsedRealtimeMs) {
if (mCameraTurnedOnTimer != null) {
mCameraTurnedOnTimer.stopRunningLocked(elapsedRealtimeMs);
if (!mCameraTurnedOnTimer.isRunningLocked()) { // only tell statsd if truly stopped
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.CAMERA_STATE_CHANGED, getUid(), 0);
+ StatsLog.write_non_chained(StatsLog.CAMERA_STATE_CHANGED, getUid(), null, 0);
}
}
}
@@ -6647,8 +7128,7 @@ public class BatteryStatsImpl extends BatteryStats {
public void noteResetCameraLocked(long elapsedRealtimeMs) {
if (mCameraTurnedOnTimer != null) {
mCameraTurnedOnTimer.stopAllRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.CAMERA_STATE_CHANGED, getUid(), 0);
+ StatsLog.write_non_chained(StatsLog.CAMERA_STATE_CHANGED, getUid(), null, 0);
}
}
@@ -6695,44 +7175,63 @@ public class BatteryStatsImpl extends BatteryStats {
return mBluetoothUnoptimizedScanTimer;
}
- public void noteBluetoothScanStartedLocked(long elapsedRealtimeMs, boolean isUnoptimized) {
+ public void noteBluetoothScanStartedLocked(long elapsedRealtimeMs,
+ boolean isUnoptimized) {
createBluetoothScanTimerLocked().startRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.BLE_SCAN_STATE_CHANGED, getUid(), 1);
if (isUnoptimized) {
createBluetoothUnoptimizedScanTimerLocked().startRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.BLE_UNOPTIMIZED_SCAN_STATE_CHANGED, getUid(), 1);
}
}
public void noteBluetoothScanStoppedLocked(long elapsedRealtimeMs, boolean isUnoptimized) {
if (mBluetoothScanTimer != null) {
mBluetoothScanTimer.stopRunningLocked(elapsedRealtimeMs);
- if (!mBluetoothScanTimer.isRunningLocked()) { // only tell statsd if truly stopped
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.BLE_SCAN_STATE_CHANGED, getUid(), 0);
- }
}
if (isUnoptimized && mBluetoothUnoptimizedScanTimer != null) {
mBluetoothUnoptimizedScanTimer.stopRunningLocked(elapsedRealtimeMs);
- if (!mBluetoothUnoptimizedScanTimer.isRunningLocked()) {
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.BLE_UNOPTIMIZED_SCAN_STATE_CHANGED, getUid(), 0);
- }
}
}
+ public void addBluetoothWorkChain(WorkChain workChain, boolean isUnoptimized) {
+ if (mAllBluetoothChains == null) {
+ mAllBluetoothChains = new ArrayList<WorkChain>(4);
+ }
+
+ if (isUnoptimized && mUnoptimizedBluetoothChains == null) {
+ mUnoptimizedBluetoothChains = new ArrayList<WorkChain>(4);
+ }
+
+ mAllBluetoothChains.add(workChain);
+ if (isUnoptimized) {
+ mUnoptimizedBluetoothChains.add(workChain);
+ }
+ }
+
+ public void removeBluetoothWorkChain(WorkChain workChain, boolean isUnoptimized) {
+ if (mAllBluetoothChains != null) {
+ mAllBluetoothChains.remove(workChain);
+ }
+
+ if (isUnoptimized && mUnoptimizedBluetoothChains != null) {
+ mUnoptimizedBluetoothChains.remove(workChain);
+ }
+ }
+
+ public List<WorkChain> getAllBluetoothWorkChains() {
+ return mAllBluetoothChains;
+ }
+
+ public List<WorkChain> getUnoptimizedBluetoothWorkChains() {
+ return mUnoptimizedBluetoothChains;
+ }
+
+
public void noteResetBluetoothScanLocked(long elapsedRealtimeMs) {
if (mBluetoothScanTimer != null) {
mBluetoothScanTimer.stopAllRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.BLE_SCAN_STATE_CHANGED, getUid(), 0);
}
if (mBluetoothUnoptimizedScanTimer != null) {
mBluetoothUnoptimizedScanTimer.stopAllRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.BLE_UNOPTIMIZED_SCAN_STATE_CHANGED, getUid(), 0);
}
}
@@ -6754,9 +7253,6 @@ public class BatteryStatsImpl extends BatteryStats {
createBluetoothScanResultCounterLocked().addAtomic(numNewResults);
// Uses background timebase, so the count will only be incremented if uid in background.
createBluetoothScanResultBgCounterLocked().addAtomic(numNewResults);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- // TODO(statsd): This could be in AppScanStats instead, if desired.
- StatsLog.write(StatsLog.BLE_SCAN_RESULT_RECEIVED, getUid(), numNewResults);
}
@Override
@@ -7291,6 +7787,9 @@ public class BatteryStatsImpl extends BatteryStats {
mScreenOffCpuFreqTimeMs.reset(false);
}
+ mCpuActiveTimeMs.reset(false);
+ mCpuClusterTimesMs.reset(false);
+
if (mProcStateTimeMs != null) {
for (LongSamplingCounterArray counters : mProcStateTimeMs) {
if (counters != null) {
@@ -7495,6 +7994,8 @@ public class BatteryStatsImpl extends BatteryStats {
if (mScreenOffCpuFreqTimeMs != null) {
mScreenOffCpuFreqTimeMs.detach();
}
+ mCpuActiveTimeMs.detach();
+ mCpuClusterTimesMs.detach();
if (mProcStateTimeMs != null) {
for (LongSamplingCounterArray counters : mProcStateTimeMs) {
@@ -7770,6 +8271,10 @@ public class BatteryStatsImpl extends BatteryStats {
LongSamplingCounterArray.writeToParcel(out, mCpuFreqTimeMs);
LongSamplingCounterArray.writeToParcel(out, mScreenOffCpuFreqTimeMs);
+
+ mCpuActiveTimeMs.writeToParcel(out);
+ mCpuClusterTimesMs.writeToParcel(out);
+
if (mProcStateTimeMs != null) {
out.writeInt(mProcStateTimeMs.length);
for (LongSamplingCounterArray counters : mProcStateTimeMs) {
@@ -8087,6 +8592,9 @@ public class BatteryStatsImpl extends BatteryStats {
mScreenOffCpuFreqTimeMs = LongSamplingCounterArray.readFromParcel(
in, mBsi.mOnBatteryScreenOffTimeBase);
+ mCpuActiveTimeMs = new LongSamplingCounter(mBsi.mOnBatteryTimeBase, in);
+ mCpuClusterTimesMs = new LongSamplingCounterArray(mBsi.mOnBatteryTimeBase, in);
+
int length = in.readInt();
if (length == NUM_PROCESS_STATE) {
mProcStateTimeMs = new LongSamplingCounterArray[length];
@@ -9042,9 +9550,11 @@ public class BatteryStatsImpl extends BatteryStats {
if (mProcessState != ActivityManager.PROCESS_STATE_NONEXISTENT) {
mProcessStateTimer[mProcessState].stopRunningLocked(elapsedRealtimeMs);
- if (mBsi.mPerProcStateCpuTimesAvailable) {
+ if (mBsi.trackPerProcStateCpuTimes()) {
if (mBsi.mPendingUids.size() == 0) {
- mBsi.mExternalSync.scheduleReadProcStateCpuTimes();
+ mBsi.mExternalSync.scheduleReadProcStateCpuTimes(
+ mBsi.mOnBatteryTimeBase.isRunning(),
+ mBsi.mOnBatteryScreenOffTimeBase.isRunning());
}
if (mBsi.mPendingUids.indexOfKey(mUid) < 0
|| ArrayUtils.contains(CRITICAL_PROC_STATES, mProcessState)) {
@@ -9192,8 +9702,7 @@ public class BatteryStatsImpl extends BatteryStats {
DualTimer t = mSyncStats.startObject(name);
if (t != null) {
t.startRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.SYNC_STATE_CHANGED, getUid(), name, 1);
+ StatsLog.write_non_chained(StatsLog.SYNC_STATE_CHANGED, getUid(), null, name, 1);
}
}
@@ -9202,8 +9711,7 @@ public class BatteryStatsImpl extends BatteryStats {
if (t != null) {
t.stopRunningLocked(elapsedRealtimeMs);
if (!t.isRunningLocked()) { // only tell statsd if truly stopped
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.SYNC_STATE_CHANGED, getUid(), name, 0);
+ StatsLog.write_non_chained(StatsLog.SYNC_STATE_CHANGED, getUid(), null, name, 0);
}
}
}
@@ -9212,8 +9720,8 @@ public class BatteryStatsImpl extends BatteryStats {
DualTimer t = mJobStats.startObject(name);
if (t != null) {
t.startRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.SCHEDULED_JOB_STATE_CHANGED, getUid(), name, 1);
+ StatsLog.write_non_chained(StatsLog.SCHEDULED_JOB_STATE_CHANGED, getUid(), null,
+ name, 1);
}
}
@@ -9222,8 +9730,8 @@ public class BatteryStatsImpl extends BatteryStats {
if (t != null) {
t.stopRunningLocked(elapsedRealtimeMs);
if (!t.isRunningLocked()) { // only tell statsd if truly stopped
- // TODO(statsd): Possibly use a worksource instead of a uid.
- StatsLog.write(StatsLog.SCHEDULED_JOB_STATE_CHANGED, getUid(), name, 0);
+ StatsLog.write_non_chained(StatsLog.SCHEDULED_JOB_STATE_CHANGED, getUid(), null,
+ name, 0);
}
}
if (mBsi.mOnBatteryTimeBase.isRunning()) {
@@ -9334,11 +9842,11 @@ public class BatteryStatsImpl extends BatteryStats {
public void noteStartSensor(int sensor, long elapsedRealtimeMs) {
DualTimer t = getSensorTimerLocked(sensor, /* create= */ true);
t.startRunningLocked(elapsedRealtimeMs);
- // TODO(statsd): Possibly use a worksource instead of a uid.
if (sensor == Sensor.GPS) {
- StatsLog.write(StatsLog.GPS_SCAN_STATE_CHANGED, getUid(), 1);
+ StatsLog.write_non_chained(StatsLog.GPS_SCAN_STATE_CHANGED, getUid(), null, 1);
} else {
- StatsLog.write(StatsLog.SENSOR_STATE_CHANGED, getUid(), sensor, 1);
+ StatsLog.write_non_chained(StatsLog.SENSOR_STATE_CHANGED, getUid(), null, sensor,
+ 1);
}
}
@@ -9348,11 +9856,12 @@ public class BatteryStatsImpl extends BatteryStats {
if (t != null) {
t.stopRunningLocked(elapsedRealtimeMs);
if (!t.isRunningLocked()) { // only tell statsd if truly stopped
- // TODO(statsd): Possibly use a worksource instead of a uid.
if (sensor == Sensor.GPS) {
- StatsLog.write(StatsLog.GPS_SCAN_STATE_CHANGED, getUid(), 0);
+ StatsLog.write_non_chained(StatsLog.GPS_SCAN_STATE_CHANGED, getUid(), null,
+ 0);
} else {
- StatsLog.write(StatsLog.SENSOR_STATE_CHANGED, getUid(), sensor, 0);
+ StatsLog.write_non_chained(StatsLog.SENSOR_STATE_CHANGED, getUid(), null,
+ sensor, 0);
}
}
}
@@ -9394,6 +9903,7 @@ public class BatteryStatsImpl extends BatteryStats {
mCheckinFile = new AtomicFile(new File(systemDir, "batterystats-checkin.bin"));
mDailyFile = new AtomicFile(new File(systemDir, "batterystats-daily.xml"));
mHandler = new MyHandler(handler.getLooper());
+ mConstants = new Constants(mHandler);
mStartCount++;
mScreenOnTimer = new StopwatchTimer(mClocks, null, -1, null, mOnBatteryTimeBase);
mScreenDozeTimer = new StopwatchTimer(mClocks, null, -1, null, mOnBatteryTimeBase);
@@ -9435,6 +9945,8 @@ public class BatteryStatsImpl extends BatteryStats {
mMobileRadioActiveAdjustedTime = new LongSamplingCounter(mOnBatteryTimeBase);
mMobileRadioActiveUnknownTime = new LongSamplingCounter(mOnBatteryTimeBase);
mMobileRadioActiveUnknownCount = new LongSamplingCounter(mOnBatteryTimeBase);
+ mWifiMulticastWakelockTimer = new StopwatchTimer(mClocks, null,
+ WIFI_AGGREGATE_MULTICAST_ENABLED, null, mOnBatteryTimeBase);
mWifiOnTimer = new StopwatchTimer(mClocks, null, -4, null, mOnBatteryTimeBase);
mGlobalWifiRunningTimer = new StopwatchTimer(mClocks, null, -5, null, mOnBatteryTimeBase);
for (int i=0; i<NUM_WIFI_STATES; i++) {
@@ -9449,6 +9961,11 @@ public class BatteryStatsImpl extends BatteryStats {
mWifiSignalStrengthsTimer[i] = new StopwatchTimer(mClocks, null, -800-i, null,
mOnBatteryTimeBase);
}
+ mWifiActiveTimer = new StopwatchTimer(mClocks, null, -900, null, mOnBatteryTimeBase);
+ for (int i=0; i< GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS; i++) {
+ mGpsSignalQualityTimer[i] = new StopwatchTimer(mClocks, null, -1000-i, null,
+ mOnBatteryTimeBase);
+ }
mAudioOnTimer = new StopwatchTimer(mClocks, null, -7, null, mOnBatteryTimeBase);
mVideoOnTimer = new StopwatchTimer(mClocks, null, -8, null, mOnBatteryTimeBase);
mFlashlightOnTimer = new StopwatchTimer(mClocks, null, -9, null, mOnBatteryTimeBase);
@@ -9487,6 +10004,7 @@ public class BatteryStatsImpl extends BatteryStats {
mDailyFile = null;
mHandler = null;
mExternalSync = null;
+ mConstants = new Constants(mHandler);
clearHistoryLocked();
readFromParcel(p);
mPlatformIdleStateCallback = null;
@@ -10136,7 +10654,12 @@ public class BatteryStatsImpl extends BatteryStats {
for (int i=0; i<NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) {
mWifiSignalStrengthsTimer[i].reset(false);
}
+ mWifiMulticastWakelockTimer.reset(false);
+ mWifiActiveTimer.reset(false);
mWifiActivity.reset(false);
+ for (int i=0; i< GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS; i++) {
+ mGpsSignalQualityTimer[i].reset(false);
+ }
mBluetoothActivity.reset(false);
mModemActivity.reset(false);
mNumConnectivityChange = mLoadedNumConnectivityChange = mUnpluggedNumConnectivityChange = 0;
@@ -10399,6 +10922,7 @@ public class BatteryStatsImpl extends BatteryStats {
// Measured in mAms
final long txTimeMs = info.getControllerTxTimeMillis();
final long rxTimeMs = info.getControllerRxTimeMillis();
+ final long scanTimeMs = info.getControllerScanTimeMillis();
final long idleTimeMs = info.getControllerIdleTimeMillis();
final long totalTimeMs = txTimeMs + rxTimeMs + idleTimeMs;
@@ -10411,6 +10935,7 @@ public class BatteryStatsImpl extends BatteryStats {
Slog.d(TAG, " Rx Time: " + rxTimeMs + " ms");
Slog.d(TAG, " Idle Time: " + idleTimeMs + " ms");
Slog.d(TAG, " Total Time: " + totalTimeMs + " ms");
+ Slog.d(TAG, " Scan Time: " + scanTimeMs + " ms");
}
long totalWifiLockTimeMs = 0;
@@ -10544,6 +11069,8 @@ public class BatteryStatsImpl extends BatteryStats {
mWifiActivity.getRxTimeCounter().addCountLocked(info.getControllerRxTimeMillis());
mWifiActivity.getTxTimeCounters()[0].addCountLocked(
info.getControllerTxTimeMillis());
+ mWifiActivity.getScanTimeCounter().addCountLocked(
+ info.getControllerScanTimeMillis());
mWifiActivity.getIdleTimeCounter().addCountLocked(
info.getControllerIdleTimeMillis());
@@ -10587,6 +11114,39 @@ public class BatteryStatsImpl extends BatteryStats {
return;
}
+ if (activityInfo != null) {
+ mHasModemReporting = true;
+ mModemActivity.getIdleTimeCounter().addCountLocked(
+ activityInfo.getIdleTimeMillis());
+ mModemActivity.getRxTimeCounter().addCountLocked(activityInfo.getRxTimeMillis());
+ for (int lvl = 0; lvl < ModemActivityInfo.TX_POWER_LEVELS; lvl++) {
+ mModemActivity.getTxTimeCounters()[lvl]
+ .addCountLocked(activityInfo.getTxTimeMillis()[lvl]);
+ }
+
+ // POWER_MODEM_CONTROLLER_OPERATING_VOLTAGE is measured in mV, so convert to V.
+ final double opVolt = mPowerProfile.getAveragePower(
+ PowerProfile.POWER_MODEM_CONTROLLER_OPERATING_VOLTAGE) / 1000.0;
+ if (opVolt != 0) {
+ double energyUsed =
+ activityInfo.getSleepTimeMillis() *
+ mPowerProfile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_SLEEP)
+ + activityInfo.getIdleTimeMillis() *
+ mPowerProfile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_IDLE)
+ + activityInfo.getRxTimeMillis() *
+ mPowerProfile.getAveragePower(PowerProfile.POWER_MODEM_CONTROLLER_RX);
+ int[] txCurrentMa = activityInfo.getTxTimeMillis();
+ for (int i = 0; i < Math.min(txCurrentMa.length,
+ SignalStrength.NUM_SIGNAL_STRENGTH_BINS); i++) {
+ energyUsed += txCurrentMa[i] * mPowerProfile.getAveragePower(
+ PowerProfile.POWER_MODEM_CONTROLLER_TX, i);
+ }
+
+ // We store the power drain as mAms.
+ mModemActivity.getPowerCounter().addCountLocked((long) energyUsed);
+ }
+ }
+
final long elapsedRealtimeMs = mClocks.elapsedRealtime();
long radioTime = mMobileRadioActivePerAppTimer.getTimeSinceMarkLocked(
elapsedRealtimeMs * 1000);
@@ -10685,26 +11245,6 @@ public class BatteryStatsImpl extends BatteryStats {
mNetworkStatsPool.release(delta);
delta = null;
}
-
- if (activityInfo != null) {
- mHasModemReporting = true;
- mModemActivity.getIdleTimeCounter().addCountLocked(
- activityInfo.getIdleTimeMillis());
- mModemActivity.getRxTimeCounter().addCountLocked(activityInfo.getRxTimeMillis());
- for (int lvl = 0; lvl < ModemActivityInfo.TX_POWER_LEVELS; lvl++) {
- mModemActivity.getTxTimeCounters()[lvl]
- .addCountLocked(activityInfo.getTxTimeMillis()[lvl]);
- }
-
- // POWER_MODEM_CONTROLLER_OPERATING_VOLTAGE is measured in mV, so convert to V.
- final double opVolt = mPowerProfile.getAveragePower(
- PowerProfile.POWER_MODEM_CONTROLLER_OPERATING_VOLTAGE) / 1000.0;
- if (opVolt != 0) {
- // We store the power drain as mAms.
- mModemActivity.getPowerCounter().addCountLocked(
- (long) (activityInfo.getEnergyUsed() / opVolt));
- }
- }
}
}
@@ -11062,6 +11602,10 @@ public class BatteryStatsImpl extends BatteryStats {
if (!mOnBatteryInternal) {
mKernelUidCpuTimeReader.readDelta(null);
mKernelUidCpuFreqTimeReader.readDelta(null);
+ if (mConstants.TRACK_CPU_ACTIVE_CLUSTER_TIME) {
+ mKernelUidCpuActiveTimeReader.readDelta(null);
+ mKernelUidCpuClusterTimeReader.readDelta(null);
+ }
for (int cluster = mKernelCpuSpeedReaders.length - 1; cluster >= 0; --cluster) {
mKernelCpuSpeedReaders[cluster].readDelta();
}
@@ -11078,6 +11622,10 @@ public class BatteryStatsImpl extends BatteryStats {
updateClusterSpeedTimes(updatedUids);
}
readKernelUidCpuFreqTimesLocked(partialTimersToConsider);
+ if (mConstants.TRACK_CPU_ACTIVE_CLUSTER_TIME) {
+ readKernelUidCpuActiveTimesLocked();
+ readKernelUidCpuClusterTimesLocked();
+ }
}
/**
@@ -11389,6 +11937,64 @@ public class BatteryStatsImpl extends BatteryStats {
}
}
+ /**
+ * Take a snapshot of the cpu active times spent by each uid and update the corresponding
+ * counters.
+ */
+ @VisibleForTesting
+ public void readKernelUidCpuActiveTimesLocked() {
+ final long startTimeMs = mClocks.uptimeMillis();
+ mKernelUidCpuActiveTimeReader.readDelta((uid, cpuActiveTimesUs) -> {
+ uid = mapUid(uid);
+ if (Process.isIsolated(uid)) {
+ mKernelUidCpuActiveTimeReader.removeUid(uid);
+ Slog.w(TAG, "Got active times for an isolated uid with no mapping: " + uid);
+ return;
+ }
+ if (!mUserInfoProvider.exists(UserHandle.getUserId(uid))) {
+ Slog.w(TAG, "Got active times for an invalid user's uid " + uid);
+ mKernelUidCpuActiveTimeReader.removeUid(uid);
+ return;
+ }
+ final Uid u = getUidStatsLocked(uid);
+ u.mCpuActiveTimeMs.addCountLocked(cpuActiveTimesUs);
+ });
+
+ final long elapsedTimeMs = mClocks.uptimeMillis() - startTimeMs;
+ if (DEBUG_ENERGY_CPU || elapsedTimeMs >= 100) {
+ Slog.d(TAG, "Reading cpu active times took " + elapsedTimeMs + "ms");
+ }
+ }
+
+ /**
+ * Take a snapshot of the cpu cluster times spent by each uid and update the corresponding
+ * counters.
+ */
+ @VisibleForTesting
+ public void readKernelUidCpuClusterTimesLocked() {
+ final long startTimeMs = mClocks.uptimeMillis();
+ mKernelUidCpuClusterTimeReader.readDelta((uid, cpuClusterTimesUs) -> {
+ uid = mapUid(uid);
+ if (Process.isIsolated(uid)) {
+ mKernelUidCpuClusterTimeReader.removeUid(uid);
+ Slog.w(TAG, "Got cluster times for an isolated uid with no mapping: " + uid);
+ return;
+ }
+ if (!mUserInfoProvider.exists(UserHandle.getUserId(uid))) {
+ Slog.w(TAG, "Got cluster times for an invalid user's uid " + uid);
+ mKernelUidCpuClusterTimeReader.removeUid(uid);
+ return;
+ }
+ final Uid u = getUidStatsLocked(uid);
+ u.mCpuClusterTimesMs.addCountLocked(cpuClusterTimesUs);
+ });
+
+ final long elapsedTimeMs = mClocks.uptimeMillis() - startTimeMs;
+ if (DEBUG_ENERGY_CPU || elapsedTimeMs >= 100) {
+ Slog.d(TAG, "Reading cpu cluster times took " + elapsedTimeMs + "ms");
+ }
+ }
+
boolean setChargingLocked(boolean charging) {
if (mCharging != charging) {
mCharging = charging;
@@ -11992,6 +12598,71 @@ public class BatteryStatsImpl extends BatteryStats {
return s;
}
+ /*@hide */
+ public WifiBatteryStats getWifiBatteryStats() {
+ WifiBatteryStats s = new WifiBatteryStats();
+ final int which = STATS_SINCE_CHARGED;
+ final long rawRealTime = SystemClock.elapsedRealtime() * 1000;
+ final ControllerActivityCounter counter = getWifiControllerActivity();
+ final long idleTimeMs = counter.getIdleTimeCounter().getCountLocked(which);
+ final long scanTimeMs = counter.getScanTimeCounter().getCountLocked(which);
+ final long rxTimeMs = counter.getRxTimeCounter().getCountLocked(which);
+ final long txTimeMs = counter.getTxTimeCounters()[0].getCountLocked(which);
+ final long totalControllerActivityTimeMs
+ = computeBatteryRealtime(SystemClock.elapsedRealtime() * 1000, which) / 1000;
+ final long sleepTimeMs
+ = totalControllerActivityTimeMs - (idleTimeMs + rxTimeMs + txTimeMs);
+ final long energyConsumedMaMs = counter.getPowerCounter().getCountLocked(which);
+ long numAppScanRequest = 0;
+ for (int i = 0; i < mUidStats.size(); i++) {
+ numAppScanRequest += mUidStats.valueAt(i).mWifiScanTimer.getCountLocked(which);
+ }
+ long[] timeInStateMs = new long[NUM_WIFI_STATES];
+ for (int i=0; i<NUM_WIFI_STATES; i++) {
+ timeInStateMs[i] = getWifiStateTime(i, rawRealTime, which) / 1000;
+ }
+ long[] timeInSupplStateMs = new long[NUM_WIFI_SUPPL_STATES];
+ for (int i=0; i<NUM_WIFI_SUPPL_STATES; i++) {
+ timeInSupplStateMs[i] = getWifiSupplStateTime(i, rawRealTime, which) / 1000;
+ }
+ long[] timeSignalStrengthTimeMs = new long[NUM_WIFI_SIGNAL_STRENGTH_BINS];
+ for (int i=0; i<NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) {
+ timeSignalStrengthTimeMs[i] = getWifiSignalStrengthTime(i, rawRealTime, which) / 1000;
+ }
+ s.setLoggingDurationMs(computeBatteryRealtime(rawRealTime, which) / 1000);
+ s.setKernelActiveTimeMs(getWifiActiveTime(rawRealTime, which) / 1000);
+ s.setNumPacketsTx(getNetworkActivityPackets(NETWORK_WIFI_TX_DATA, which));
+ s.setNumBytesTx(getNetworkActivityBytes(NETWORK_WIFI_TX_DATA, which));
+ s.setNumPacketsRx(getNetworkActivityPackets(NETWORK_WIFI_RX_DATA, which));
+ s.setNumBytesRx(getNetworkActivityBytes(NETWORK_WIFI_RX_DATA, which));
+ s.setSleepTimeMs(sleepTimeMs);
+ s.setIdleTimeMs(idleTimeMs);
+ s.setRxTimeMs(rxTimeMs);
+ s.setTxTimeMs(txTimeMs);
+ s.setScanTimeMs(scanTimeMs);
+ s.setEnergyConsumedMaMs(energyConsumedMaMs);
+ s.setNumAppScanRequest(numAppScanRequest);
+ s.setTimeInStateMs(timeInStateMs);
+ s.setTimeInSupplicantStateMs(timeInSupplStateMs);
+ s.setTimeInRxSignalStrengthLevelMs(timeSignalStrengthTimeMs);
+ return s;
+ }
+
+ /*@hide */
+ public GpsBatteryStats getGpsBatteryStats() {
+ GpsBatteryStats s = new GpsBatteryStats();
+ final int which = STATS_SINCE_CHARGED;
+ final long rawRealTime = SystemClock.elapsedRealtime() * 1000;
+ s.setLoggingDurationMs(computeBatteryRealtime(rawRealTime, which) / 1000);
+ s.setEnergyConsumedMaMs(getGpsBatteryDrainMaMs());
+ long[] time = new long[GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS];
+ for (int i=0; i<time.length; i++) {
+ time[i] = getGpsSignalQualityTime(i, rawRealTime, which) / 1000;
+ }
+ s.setTimeInGpsSignalQualityLevel(time);
+ return s;
+ }
+
@Override
public LevelStepTracker getChargeLevelStepTracker() {
return mChargeStepTracker;
@@ -12239,6 +12910,96 @@ public class BatteryStatsImpl extends BatteryStats {
mShuttingDown = true;
}
+ public boolean trackPerProcStateCpuTimes() {
+ return mConstants.TRACK_CPU_TIMES_BY_PROC_STATE && mPerProcStateCpuTimesAvailable;
+ }
+
+ public void systemServicesReady(Context context) {
+ mConstants.startObserving(context.getContentResolver());
+ }
+
+ @VisibleForTesting
+ public final class Constants extends ContentObserver {
+ public static final String KEY_TRACK_CPU_TIMES_BY_PROC_STATE
+ = "track_cpu_times_by_proc_state";
+ public static final String KEY_TRACK_CPU_ACTIVE_CLUSTER_TIME
+ = "track_cpu_active_cluster_time";
+ public static final String KEY_READ_BINARY_CPU_TIME
+ = "read_binary_cpu_time";
+
+ private static final boolean DEFAULT_TRACK_CPU_TIMES_BY_PROC_STATE = true;
+ private static final boolean DEFAULT_TRACK_CPU_ACTIVE_CLUSTER_TIME = true;
+ private static final boolean DEFAULT_READ_BINARY_CPU_TIME = false;
+
+ public boolean TRACK_CPU_TIMES_BY_PROC_STATE = DEFAULT_TRACK_CPU_TIMES_BY_PROC_STATE;
+ public boolean TRACK_CPU_ACTIVE_CLUSTER_TIME = DEFAULT_TRACK_CPU_ACTIVE_CLUSTER_TIME;
+ // Not used right now.
+ public boolean READ_BINARY_CPU_TIME = DEFAULT_READ_BINARY_CPU_TIME;
+
+ private ContentResolver mResolver;
+ private final KeyValueListParser mParser = new KeyValueListParser(',');
+
+ public Constants(Handler handler) {
+ super(handler);
+ }
+
+ public void startObserving(ContentResolver resolver) {
+ mResolver = resolver;
+ mResolver.registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.BATTERY_STATS_CONSTANTS),
+ false /* notifyForDescendants */, this);
+ updateConstants();
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ updateConstants();
+ }
+
+ private void updateConstants() {
+ synchronized (BatteryStatsImpl.this) {
+ try {
+ mParser.setString(Settings.Global.getString(mResolver,
+ Settings.Global.BATTERY_STATS_CONSTANTS));
+ } catch (IllegalArgumentException e) {
+ // Failed to parse the settings string, log this and move on
+ // with defaults.
+ Slog.e(TAG, "Bad batterystats settings", e);
+ }
+
+ updateTrackCpuTimesByProcStateLocked(TRACK_CPU_TIMES_BY_PROC_STATE,
+ mParser.getBoolean(KEY_TRACK_CPU_TIMES_BY_PROC_STATE,
+ DEFAULT_TRACK_CPU_TIMES_BY_PROC_STATE));
+ TRACK_CPU_ACTIVE_CLUSTER_TIME = mParser.getBoolean(
+ KEY_TRACK_CPU_ACTIVE_CLUSTER_TIME, DEFAULT_TRACK_CPU_ACTIVE_CLUSTER_TIME);
+ READ_BINARY_CPU_TIME = mParser.getBoolean(
+ KEY_READ_BINARY_CPU_TIME, DEFAULT_READ_BINARY_CPU_TIME);
+
+ }
+ }
+
+ private void updateTrackCpuTimesByProcStateLocked(boolean wasEnabled, boolean isEnabled) {
+ TRACK_CPU_TIMES_BY_PROC_STATE = isEnabled;
+ if (isEnabled && !wasEnabled) {
+ mKernelSingleUidTimeReader.markDataAsStale(true);
+ mExternalSync.scheduleCpuSyncDueToSettingChange();
+ }
+ }
+
+ public void dumpLocked(PrintWriter pw) {
+ pw.print(KEY_TRACK_CPU_TIMES_BY_PROC_STATE); pw.print("=");
+ pw.println(TRACK_CPU_TIMES_BY_PROC_STATE);
+ pw.print(KEY_TRACK_CPU_ACTIVE_CLUSTER_TIME); pw.print("=");
+ pw.println(TRACK_CPU_ACTIVE_CLUSTER_TIME);
+ pw.print(KEY_READ_BINARY_CPU_TIME); pw.print("=");
+ pw.println(READ_BINARY_CPU_TIME);
+ }
+ }
+
+ public void dumpConstantsLocked(PrintWriter pw) {
+ mConstants.dumpLocked(pw);
+ }
+
Parcel mPendingWrite = null;
final ReentrantLock mWriteLock = new ReentrantLock();
@@ -12582,6 +13343,7 @@ public class BatteryStatsImpl extends BatteryStats {
mMobileRadioActiveAdjustedTime.readSummaryFromParcelLocked(in);
mMobileRadioActiveUnknownTime.readSummaryFromParcelLocked(in);
mMobileRadioActiveUnknownCount.readSummaryFromParcelLocked(in);
+ mWifiMulticastWakelockTimer.readSummaryFromParcelLocked(in);
mWifiRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
mWifiOn = false;
mWifiOnTimer.readSummaryFromParcelLocked(in);
@@ -12596,7 +13358,11 @@ public class BatteryStatsImpl extends BatteryStats {
for (int i=0; i<NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) {
mWifiSignalStrengthsTimer[i].readSummaryFromParcelLocked(in);
}
+ mWifiActiveTimer.readSummaryFromParcelLocked(in);
mWifiActivity.readSummaryFromParcel(in);
+ for (int i=0; i<GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS; i++) {
+ mGpsSignalQualityTimer[i].readSummaryFromParcelLocked(in);
+ }
mBluetoothActivity.readSummaryFromParcel(in);
mModemActivity.readSummaryFromParcel(in);
mHasWifiReporting = in.readInt() != 0;
@@ -12801,6 +13567,10 @@ public class BatteryStatsImpl extends BatteryStats {
in, mOnBatteryTimeBase);
u.mScreenOffCpuFreqTimeMs = LongSamplingCounterArray.readSummaryFromParcelLocked(
in, mOnBatteryScreenOffTimeBase);
+
+ u.mCpuActiveTimeMs.readSummaryFromParcelLocked(in);
+ u.mCpuClusterTimesMs.readSummaryFromParcelLocked(in);
+
int length = in.readInt();
if (length == Uid.NUM_PROCESS_STATE) {
u.mProcStateTimeMs = new LongSamplingCounterArray[length];
@@ -13022,6 +13792,7 @@ public class BatteryStatsImpl extends BatteryStats {
mMobileRadioActiveAdjustedTime.writeSummaryFromParcelLocked(out);
mMobileRadioActiveUnknownTime.writeSummaryFromParcelLocked(out);
mMobileRadioActiveUnknownCount.writeSummaryFromParcelLocked(out);
+ mWifiMulticastWakelockTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
mWifiOnTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
mGlobalWifiRunningTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
for (int i=0; i<NUM_WIFI_STATES; i++) {
@@ -13033,7 +13804,11 @@ public class BatteryStatsImpl extends BatteryStats {
for (int i=0; i<NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) {
mWifiSignalStrengthsTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS);
}
+ mWifiActiveTimer.writeSummaryFromParcelLocked(out, NOWREAL_SYS);
mWifiActivity.writeSummaryToParcel(out);
+ for (int i=0; i< GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS; i++) {
+ mGpsSignalQualityTimer[i].writeSummaryFromParcelLocked(out, NOWREAL_SYS);
+ }
mBluetoothActivity.writeSummaryToParcel(out);
mModemActivity.writeSummaryToParcel(out);
out.writeInt(mHasWifiReporting ? 1 : 0);
@@ -13276,6 +14051,9 @@ public class BatteryStatsImpl extends BatteryStats {
LongSamplingCounterArray.writeSummaryToParcelLocked(out, u.mCpuFreqTimeMs);
LongSamplingCounterArray.writeSummaryToParcelLocked(out, u.mScreenOffCpuFreqTimeMs);
+ u.mCpuActiveTimeMs.writeSummaryFromParcelLocked(out);
+ u.mCpuClusterTimesMs.writeSummaryToParcelLocked(out);
+
if (u.mProcStateTimeMs != null) {
out.writeInt(u.mProcStateTimeMs.length);
for (LongSamplingCounterArray counters : u.mProcStateTimeMs) {
@@ -13485,6 +14263,8 @@ public class BatteryStatsImpl extends BatteryStats {
mMobileRadioActiveAdjustedTime = new LongSamplingCounter(mOnBatteryTimeBase, in);
mMobileRadioActiveUnknownTime = new LongSamplingCounter(mOnBatteryTimeBase, in);
mMobileRadioActiveUnknownCount = new LongSamplingCounter(mOnBatteryTimeBase, in);
+ mWifiMulticastWakelockTimer = new StopwatchTimer(mClocks, null, -4, null,
+ mOnBatteryTimeBase, in);
mWifiRadioPowerState = DataConnectionRealTimeInfo.DC_POWER_STATE_LOW;
mWifiOn = false;
mWifiOnTimer = new StopwatchTimer(mClocks, null, -4, null, mOnBatteryTimeBase, in);
@@ -13503,9 +14283,14 @@ public class BatteryStatsImpl extends BatteryStats {
mWifiSignalStrengthsTimer[i] = new StopwatchTimer(mClocks, null, -800-i,
null, mOnBatteryTimeBase, in);
}
-
+ mWifiActiveTimer = new StopwatchTimer(mClocks, null, -900, null,
+ mOnBatteryTimeBase, in);
mWifiActivity = new ControllerActivityCounterImpl(mOnBatteryTimeBase,
NUM_WIFI_TX_LEVELS, in);
+ for (int i=0; i<GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS; i++) {
+ mGpsSignalQualityTimer[i] = new StopwatchTimer(mClocks, null, -1000-i,
+ null, mOnBatteryTimeBase, in);
+ }
mBluetoothActivity = new ControllerActivityCounterImpl(mOnBatteryTimeBase,
NUM_BT_TX_LEVELS, in);
mModemActivity = new ControllerActivityCounterImpl(mOnBatteryTimeBase,
@@ -13691,6 +14476,7 @@ public class BatteryStatsImpl extends BatteryStats {
mMobileRadioActiveAdjustedTime.writeToParcel(out);
mMobileRadioActiveUnknownTime.writeToParcel(out);
mMobileRadioActiveUnknownCount.writeToParcel(out);
+ mWifiMulticastWakelockTimer.writeToParcel(out, uSecRealtime);
mWifiOnTimer.writeToParcel(out, uSecRealtime);
mGlobalWifiRunningTimer.writeToParcel(out, uSecRealtime);
for (int i=0; i<NUM_WIFI_STATES; i++) {
@@ -13702,7 +14488,11 @@ public class BatteryStatsImpl extends BatteryStats {
for (int i=0; i<NUM_WIFI_SIGNAL_STRENGTH_BINS; i++) {
mWifiSignalStrengthsTimer[i].writeToParcel(out, uSecRealtime);
}
+ mWifiActiveTimer.writeToParcel(out, uSecRealtime);
mWifiActivity.writeToParcel(out, 0);
+ for (int i=0; i< GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS; i++) {
+ mGpsSignalQualityTimer[i].writeToParcel(out, uSecRealtime);
+ }
mBluetoothActivity.writeToParcel(out, 0);
mModemActivity.writeToParcel(out, 0);
out.writeInt(mHasWifiReporting ? 1 : 0);
@@ -13877,6 +14667,8 @@ public class BatteryStatsImpl extends BatteryStats {
mMobileRadioActiveTimer.logState(pr, " ");
pr.println("*** Mobile network active adjusted timer:");
mMobileRadioActiveAdjustedTime.logState(pr, " ");
+ pr.println("*** Wifi Multicast WakeLock Timer:");
+ mWifiMulticastWakelockTimer.logState(pr, " ");
pr.println("*** mWifiRadioPowerState=" + mWifiRadioPowerState);
pr.println("*** Wifi timer:");
mWifiOnTimer.logState(pr, " ");
@@ -13894,6 +14686,10 @@ public class BatteryStatsImpl extends BatteryStats {
pr.println("*** Wifi signal strength #" + i + ":");
mWifiSignalStrengthsTimer[i].logState(pr, " ");
}
+ for (int i=0; i<GnssMetrics.NUM_GPS_SIGNAL_QUALITY_LEVELS; i++) {
+ pr.println("*** GPS signal quality #" + i + ":");
+ mGpsSignalQualityTimer[i].logState(pr, " ");
+ }
pr.println("*** Flashlight timer:");
mFlashlightOnTimer.logState(pr, " ");
pr.println("*** Camera timer:");
diff --git a/com/android/internal/os/CpuPowerCalculator.java b/com/android/internal/os/CpuPowerCalculator.java
index bb743c15..a34e7f50 100644
--- a/com/android/internal/os/CpuPowerCalculator.java
+++ b/com/android/internal/os/CpuPowerCalculator.java
@@ -31,8 +31,7 @@ public class CpuPowerCalculator extends PowerCalculator {
@Override
public void calculateApp(BatterySipper app, BatteryStats.Uid u, long rawRealtimeUs,
- long rawUptimeUs, int statsType) {
-
+ long rawUptimeUs, int statsType) {
app.cpuTimeMs = (u.getUserCpuTimeUs(statsType) + u.getSystemCpuTimeUs(statsType)) / 1000;
final int numClusters = mProfile.getNumCpuClusters();
@@ -42,7 +41,7 @@ public class CpuPowerCalculator extends PowerCalculator {
for (int speed = 0; speed < speedsForCluster; speed++) {
final long timeUs = u.getTimeAtCpuSpeed(cluster, speed, statsType);
final double cpuSpeedStepPower = timeUs *
- mProfile.getAveragePowerForCpu(cluster, speed);
+ mProfile.getAveragePowerForCpuCore(cluster, speed);
if (DEBUG) {
Log.d(TAG, "UID " + u.getUid() + ": CPU cluster #" + cluster + " step #"
+ speed + " timeUs=" + timeUs + " power="
@@ -51,6 +50,25 @@ public class CpuPowerCalculator extends PowerCalculator {
cpuPowerMaUs += cpuSpeedStepPower;
}
}
+ cpuPowerMaUs += u.getCpuActiveTime() * mProfile.getAveragePower(
+ PowerProfile.POWER_CPU_ACTIVE);
+ long[] cpuClusterTimes = u.getCpuClusterTimes();
+ if (cpuClusterTimes != null) {
+ if (cpuClusterTimes.length == numClusters) {
+ for (int i = 0; i < numClusters; i++) {
+ double power = cpuClusterTimes[i] * mProfile.getAveragePowerForCpuCluster(i);
+ cpuPowerMaUs += power;
+ if (DEBUG) {
+ Log.d(TAG, "UID " + u.getUid() + ": CPU cluster #" + i + " clusterTimeUs="
+ + cpuClusterTimes[i] + " power="
+ + BatteryStatsHelper.makemAh(power / MICROSEC_IN_HR));
+ }
+ }
+ } else {
+ Log.w(TAG, "UID " + u.getUid() + " CPU cluster # mismatch: Power Profile # "
+ + numClusters + " actual # " + cpuClusterTimes.length);
+ }
+ }
app.cpuPowerMah = cpuPowerMaUs / MICROSEC_IN_HR;
if (DEBUG && (app.cpuTimeMs != 0 || app.cpuPowerMah != 0)) {
diff --git a/com/android/internal/os/KernelCpuSpeedReader.java b/com/android/internal/os/KernelCpuSpeedReader.java
index 4c0370c9..98fea010 100644
--- a/com/android/internal/os/KernelCpuSpeedReader.java
+++ b/com/android/internal/os/KernelCpuSpeedReader.java
@@ -38,6 +38,7 @@ public class KernelCpuSpeedReader {
private static final String TAG = "KernelCpuSpeedReader";
private final String mProcFile;
+ private final int mNumSpeedSteps;
private final long[] mLastSpeedTimesMs;
private final long[] mDeltaSpeedTimesMs;
@@ -50,6 +51,7 @@ public class KernelCpuSpeedReader {
public KernelCpuSpeedReader(int cpuNumber, int numSpeedSteps) {
mProcFile = String.format("/sys/devices/system/cpu/cpu%d/cpufreq/stats/time_in_state",
cpuNumber);
+ mNumSpeedSteps = numSpeedSteps;
mLastSpeedTimesMs = new long[numSpeedSteps];
mDeltaSpeedTimesMs = new long[numSpeedSteps];
long jiffyHz = Os.sysconf(OsConstants._SC_CLK_TCK);
@@ -90,4 +92,31 @@ public class KernelCpuSpeedReader {
}
return mDeltaSpeedTimesMs;
}
+
+ /**
+ * @return The time (in milliseconds) spent at different cpu speeds. The values should be
+ * monotonically increasing, unless the cpu was hotplugged.
+ */
+ public long[] readAbsolute() {
+ StrictMode.ThreadPolicy policy = StrictMode.allowThreadDiskReads();
+ long[] speedTimeMs = new long[mNumSpeedSteps];
+ try (BufferedReader reader = new BufferedReader(new FileReader(mProcFile))) {
+ TextUtils.SimpleStringSplitter splitter = new TextUtils.SimpleStringSplitter(' ');
+ String line;
+ int speedIndex = 0;
+ while (speedIndex < mNumSpeedSteps && (line = reader.readLine()) != null) {
+ splitter.setString(line);
+ splitter.next();
+ long time = Long.parseLong(splitter.next()) * mJiffyMillis;
+ speedTimeMs[speedIndex] = time;
+ speedIndex++;
+ }
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to read cpu-freq: " + e.getMessage());
+ Arrays.fill(speedTimeMs, 0);
+ } finally {
+ StrictMode.setThreadPolicy(policy);
+ }
+ return speedTimeMs;
+ }
}
diff --git a/com/android/internal/os/KernelSingleUidTimeReader.java b/com/android/internal/os/KernelSingleUidTimeReader.java
index ca635a40..ebeb24c4 100644
--- a/com/android/internal/os/KernelSingleUidTimeReader.java
+++ b/com/android/internal/os/KernelSingleUidTimeReader.java
@@ -46,12 +46,14 @@ public class KernelSingleUidTimeReader {
private final int mCpuFreqsCount;
@GuardedBy("this")
- private final SparseArray<long[]> mLastUidCpuTimeMs = new SparseArray<>();
+ private SparseArray<long[]> mLastUidCpuTimeMs = new SparseArray<>();
@GuardedBy("this")
private int mReadErrorCounter;
@GuardedBy("this")
private boolean mSingleUidCpuTimesAvailable = true;
+ @GuardedBy("this")
+ private boolean mHasStaleData;
private final Injector mInjector;
@@ -166,6 +168,30 @@ public class KernelSingleUidTimeReader {
return deltaTimesMs;
}
+ public void markDataAsStale(boolean hasStaleData) {
+ synchronized (this) {
+ mHasStaleData = hasStaleData;
+ }
+ }
+
+ public boolean hasStaleData() {
+ synchronized (this) {
+ return mHasStaleData;
+ }
+ }
+
+ public void setAllUidsCpuTimesMs(SparseArray<long[]> allUidsCpuTimesMs) {
+ synchronized (this) {
+ mLastUidCpuTimeMs.clear();
+ for (int i = allUidsCpuTimesMs.size() - 1; i >= 0; --i) {
+ final long[] cpuTimesMs = allUidsCpuTimesMs.valueAt(i);
+ if (cpuTimesMs != null) {
+ mLastUidCpuTimeMs.put(allUidsCpuTimesMs.keyAt(i), cpuTimesMs.clone());
+ }
+ }
+ }
+ }
+
public void removeUid(int uid) {
synchronized (this) {
mLastUidCpuTimeMs.delete(uid);
diff --git a/com/android/internal/os/KernelUidCpuActiveTimeReader.java b/com/android/internal/os/KernelUidCpuActiveTimeReader.java
new file mode 100644
index 00000000..cb96c5cd
--- /dev/null
+++ b/com/android/internal/os/KernelUidCpuActiveTimeReader.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 android.annotation.Nullable;
+import android.os.StrictMode;
+import android.os.SystemClock;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+
+/**
+ * Reads /proc/uid_concurrent_active_time which has the format:
+ * active: X (X is # cores)
+ * [uid0]: [time-0] [time-1] [time-2] ... (# entries = # cores)
+ * [uid1]: [time-0] [time-1] [time-2] ... ...
+ * ...
+ * Time-N means the CPU time a UID spent running concurrently with N other processes.
+ * The file contains a monotonically increasing count of time for a single boot. This class
+ * maintains the previous results of a call to {@link #readDelta} in order to provide a
+ * proper delta.
+ */
+public class KernelUidCpuActiveTimeReader {
+ private static final boolean DEBUG = false;
+ private static final String TAG = "KernelUidCpuActiveTimeReader";
+ private static final String UID_TIMES_PROC_FILE = "/proc/uid_concurrent_active_time";
+
+ private int mCoreCount;
+ private long mLastTimeReadMs;
+ private long mNowTimeMs;
+ private SparseArray<long[]> mLastUidCpuActiveTimeMs = new SparseArray<>();
+
+ public interface Callback {
+ void onUidCpuActiveTime(int uid, long cpuActiveTimeMs);
+ }
+
+ public void readDelta(@Nullable Callback cb) {
+ final int oldMask = StrictMode.allowThreadDiskReadsMask();
+ try (BufferedReader reader = new BufferedReader(new FileReader(UID_TIMES_PROC_FILE))) {
+ mNowTimeMs = SystemClock.elapsedRealtime();
+ readDeltaInternal(reader, cb);
+ mLastTimeReadMs = mNowTimeMs;
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to read " + UID_TIMES_PROC_FILE + ": " + e);
+ } finally {
+ StrictMode.setThreadPolicyMask(oldMask);
+ }
+ }
+
+ public void removeUid(int uid) {
+ mLastUidCpuActiveTimeMs.delete(uid);
+ }
+
+ public void removeUidsInRange(int startUid, int endUid) {
+ if (endUid < startUid) {
+ Slog.w(TAG, "End UID " + endUid + " is smaller than start UID " + startUid);
+ return;
+ }
+ mLastUidCpuActiveTimeMs.put(startUid, null);
+ mLastUidCpuActiveTimeMs.put(endUid, null);
+ final int firstIndex = mLastUidCpuActiveTimeMs.indexOfKey(startUid);
+ final int lastIndex = mLastUidCpuActiveTimeMs.indexOfKey(endUid);
+ mLastUidCpuActiveTimeMs.removeAtRange(firstIndex, lastIndex - firstIndex + 1);
+ }
+
+ @VisibleForTesting
+ public void readDeltaInternal(BufferedReader reader, @Nullable Callback cb) throws IOException {
+ String line = reader.readLine();
+ if (line == null || !line.startsWith("active:")) {
+ Slog.e(TAG, String.format("Malformed proc file: %s ", UID_TIMES_PROC_FILE));
+ return;
+ }
+ if (mCoreCount == 0) {
+ mCoreCount = Integer.parseInt(line.substring(line.indexOf(' ')+1));
+ }
+ while ((line = reader.readLine()) != null) {
+ final int index = line.indexOf(' ');
+ final int uid = Integer.parseInt(line.substring(0, index - 1), 10);
+ readTimesForUid(uid, line.substring(index + 1), cb);
+ }
+ }
+
+ private void readTimesForUid(int uid, String line, @Nullable Callback cb) {
+ long[] lastActiveTime = mLastUidCpuActiveTimeMs.get(uid);
+ if (lastActiveTime == null) {
+ lastActiveTime = new long[mCoreCount];
+ mLastUidCpuActiveTimeMs.put(uid, lastActiveTime);
+ }
+ final String[] timesStr = line.split(" ");
+ if (timesStr.length != mCoreCount) {
+ Slog.e(TAG, String.format("# readings don't match # cores, readings: %d, CPU cores: %d",
+ timesStr.length, mCoreCount));
+ return;
+ }
+ long sumDeltas = 0;
+ final long[] curActiveTime = new long[mCoreCount];
+ boolean notify = false;
+ for (int i = 0; i < mCoreCount; i++) {
+ // Times read will be in units of 10ms
+ curActiveTime[i] = Long.parseLong(timesStr[i], 10) * 10;
+ long delta = curActiveTime[i] - lastActiveTime[i];
+ if (delta < 0 || curActiveTime[i] < 0) {
+ if (DEBUG) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(String.format("Malformed cpu active time for UID=%d\n", uid));
+ sb.append(String.format("data=(%d,%d)\n", lastActiveTime[i], curActiveTime[i]));
+ sb.append("times=(");
+ TimeUtils.formatDuration(mLastTimeReadMs, sb);
+ sb.append(",");
+ TimeUtils.formatDuration(mNowTimeMs, sb);
+ sb.append(")");
+ Slog.e(TAG, sb.toString());
+ }
+ return;
+ }
+ notify |= delta > 0;
+ sumDeltas += delta / (i + 1);
+ }
+ if (notify) {
+ System.arraycopy(curActiveTime, 0, lastActiveTime, 0, mCoreCount);
+ if (cb != null) {
+ cb.onUidCpuActiveTime(uid, sumDeltas);
+ }
+ }
+ }
+}
diff --git a/com/android/internal/os/KernelUidCpuClusterTimeReader.java b/com/android/internal/os/KernelUidCpuClusterTimeReader.java
new file mode 100644
index 00000000..85153bc4
--- /dev/null
+++ b/com/android/internal/os/KernelUidCpuClusterTimeReader.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 com.android.internal.os;
+
+import android.annotation.Nullable;
+import android.os.StrictMode;
+import android.os.SystemClock;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.TimeUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Reads /proc/uid_concurrent_policy_time which has the format:
+ * policy0: X policy4: Y (there are X cores on policy0, Y cores on policy4)
+ * [uid0]: [time-0-0] [time-0-1] ... [time-1-0] [time-1-1] ...
+ * [uid1]: [time-0-0] [time-0-1] ... [time-1-0] [time-1-1] ...
+ * ...
+ * Time-X-Y means the time a UID spent on clusterX running concurrently with Y other processes.
+ * The file contains a monotonically increasing count of time for a single boot. This class
+ * maintains the previous results of a call to {@link #readDelta} in order to provide a proper
+ * delta.
+ */
+public class KernelUidCpuClusterTimeReader {
+
+ private static final boolean DEBUG = false;
+ private static final String TAG = "KernelUidCpuClusterTimeReader";
+ private static final String UID_TIMES_PROC_FILE = "/proc/uid_concurrent_policy_time";
+
+ // mCoreOnCluster[i] is the # of cores on cluster i
+ private int[] mCoreOnCluster;
+ private int mCores;
+ private long mLastTimeReadMs;
+ private long mNowTimeMs;
+ private SparseArray<long[]> mLastUidPolicyTimeMs = new SparseArray<>();
+
+ public interface Callback {
+ /**
+ * @param uid
+ * @param cpuActiveTimeMs the first dimension is cluster, the second dimension is the # of
+ * processes running concurrently with this uid.
+ */
+ void onUidCpuPolicyTime(int uid, long[] cpuActiveTimeMs);
+ }
+
+ public void readDelta(@Nullable Callback cb) {
+ final int oldMask = StrictMode.allowThreadDiskReadsMask();
+ try (BufferedReader reader = new BufferedReader(new FileReader(UID_TIMES_PROC_FILE))) {
+ mNowTimeMs = SystemClock.elapsedRealtime();
+ readDeltaInternal(reader, cb);
+ mLastTimeReadMs = mNowTimeMs;
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to read " + UID_TIMES_PROC_FILE + ": " + e);
+ } finally {
+ StrictMode.setThreadPolicyMask(oldMask);
+ }
+ }
+
+ public void removeUid(int uid) {
+ mLastUidPolicyTimeMs.delete(uid);
+ }
+
+ public void removeUidsInRange(int startUid, int endUid) {
+ if (endUid < startUid) {
+ Slog.w(TAG, "End UID " + endUid + " is smaller than start UID " + startUid);
+ return;
+ }
+ mLastUidPolicyTimeMs.put(startUid, null);
+ mLastUidPolicyTimeMs.put(endUid, null);
+ final int firstIndex = mLastUidPolicyTimeMs.indexOfKey(startUid);
+ final int lastIndex = mLastUidPolicyTimeMs.indexOfKey(endUid);
+ mLastUidPolicyTimeMs.removeAtRange(firstIndex, lastIndex - firstIndex + 1);
+ }
+
+ @VisibleForTesting
+ public void readDeltaInternal(BufferedReader reader, @Nullable Callback cb) throws IOException {
+ String line = reader.readLine();
+ if (line == null || !line.startsWith("policy")) {
+ Slog.e(TAG, String.format("Malformed proc file: %s ", UID_TIMES_PROC_FILE));
+ return;
+ }
+ if (mCoreOnCluster == null) {
+ List<Integer> list = new ArrayList<>();
+ String[] policies = line.split(" ");
+
+ if (policies.length == 0 || policies.length % 2 != 0) {
+ Slog.e(TAG, String.format("Malformed proc file: %s ", UID_TIMES_PROC_FILE));
+ return;
+ }
+
+ for (int i = 0; i < policies.length; i+=2) {
+ list.add(Integer.parseInt(policies[i+1]));
+ }
+
+ mCoreOnCluster = new int[list.size()];
+ for(int i=0;i<list.size();i++){
+ mCoreOnCluster[i] = list.get(i);
+ mCores += mCoreOnCluster[i];
+ }
+ }
+ while ((line = reader.readLine()) != null) {
+ final int index = line.indexOf(' ');
+ final int uid = Integer.parseInt(line.substring(0, index - 1), 10);
+ readTimesForUid(uid, line.substring(index + 1), cb);
+ }
+ }
+
+ private void readTimesForUid(int uid, String line, @Nullable Callback cb) {
+ long[] lastPolicyTime = mLastUidPolicyTimeMs.get(uid);
+ if (lastPolicyTime == null) {
+ lastPolicyTime = new long[mCores];
+ mLastUidPolicyTimeMs.put(uid, lastPolicyTime);
+ }
+ final String[] timeStr = line.split(" ");
+ if (timeStr.length != mCores) {
+ Slog.e(TAG, String.format("# readings don't match # cores, readings: %d, # CPU cores: %d",
+ timeStr.length, mCores));
+ return;
+ }
+ final long[] deltaPolicyTime = new long[mCores];
+ final long[] currPolicyTime = new long[mCores];
+ boolean notify = false;
+ for (int i = 0; i < mCores; i++) {
+ // Times read will be in units of 10ms
+ currPolicyTime[i] = Long.parseLong(timeStr[i], 10) * 10;
+ deltaPolicyTime[i] = currPolicyTime[i] - lastPolicyTime[i];
+ if (deltaPolicyTime[i] < 0 || currPolicyTime[i] < 0) {
+ if (DEBUG) {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(String.format("Malformed cpu policy time for UID=%d\n", uid));
+ sb.append(String.format("data=(%d,%d)\n", lastPolicyTime[i], currPolicyTime[i]));
+ sb.append("times=(");
+ TimeUtils.formatDuration(mLastTimeReadMs, sb);
+ sb.append(",");
+ TimeUtils.formatDuration(mNowTimeMs, sb);
+ sb.append(")");
+ Slog.e(TAG, sb.toString());
+ }
+ return;
+ }
+ notify |= deltaPolicyTime[i] > 0;
+ }
+ if (notify) {
+ System.arraycopy(currPolicyTime, 0, lastPolicyTime, 0, mCores);
+ if (cb != null) {
+ final long[] times = new long[mCoreOnCluster.length];
+ int core = 0;
+ for (int i = 0; i < mCoreOnCluster.length; i++) {
+ for (int j = 0; j < mCoreOnCluster[i]; j++) {
+ times[i] += deltaPolicyTime[core++] / (j+1);
+ }
+ }
+ cb.onUidCpuPolicyTime(uid, times);
+ }
+ }
+ }
+}
diff --git a/com/android/internal/os/PowerProfile.java b/com/android/internal/os/PowerProfile.java
index 872b465a..f4436d38 100644
--- a/com/android/internal/os/PowerProfile.java
+++ b/com/android/internal/os/PowerProfile.java
@@ -21,6 +21,7 @@ import android.content.Context;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.XmlUtils;
import org.xmlpull.v1.XmlPullParser;
@@ -43,23 +44,25 @@ public class PowerProfile {
public static final String POWER_NONE = "none";
/**
- * Power consumption when CPU is in power collapse mode.
+ * POWER_CPU_SUSPEND: Power consumption when CPU is in power collapse mode.
+ * POWER_CPU_IDLE: Power consumption when CPU is awake (when a wake lock is held). This should
+ * be zero on devices that can go into full CPU power collapse even when a wake
+ * lock is held. Otherwise, this is the power consumption in addition to
+ * POWER_CPU_SUSPEND due to a wake lock being held but with no CPU activity.
+ * POWER_CPU_ACTIVE: Power consumption when CPU is running, excluding power consumed by clusters
+ * and cores.
+ *
+ * CPU Power Equation (assume two clusters):
+ * Total power = POWER_CPU_SUSPEND (always added)
+ * + POWER_CPU_IDLE (skip this and below if in power collapse mode)
+ * + POWER_CPU_ACTIVE (skip this and below if CPU is not running, but a wakelock
+ * is held)
+ * + cluster_power.cluster0 + cluster_power.cluster1 (skip cluster not running)
+ * + core_power.cluster0 * num running cores in cluster 0
+ * + core_power.cluster1 * num running cores in cluster 1
*/
+ public static final String POWER_CPU_SUSPEND = "cpu.suspend";
public static final String POWER_CPU_IDLE = "cpu.idle";
-
- /**
- * Power consumption when CPU is awake (when a wake lock is held). This
- * should be 0 on devices that can go into full CPU power collapse even
- * when a wake lock is held. Otherwise, this is the power consumption in
- * addition to POWER_CPU_IDLE due to a wake lock being held but with no
- * CPU activity.
- */
- public static final String POWER_CPU_AWAKE = "cpu.awake";
-
- /**
- * Power consumption when CPU is in power collapse mode.
- */
- @Deprecated
public static final String POWER_CPU_ACTIVE = "cpu.active";
/**
@@ -94,18 +97,25 @@ public class PowerProfile {
public static final String POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE =
"bluetooth.controller.voltage";
+ public static final String POWER_MODEM_CONTROLLER_SLEEP = "modem.controller.sleep";
public static final String POWER_MODEM_CONTROLLER_IDLE = "modem.controller.idle";
public static final String POWER_MODEM_CONTROLLER_RX = "modem.controller.rx";
public static final String POWER_MODEM_CONTROLLER_TX = "modem.controller.tx";
public static final String POWER_MODEM_CONTROLLER_OPERATING_VOLTAGE =
"modem.controller.voltage";
- /**
+ /**
* Power consumption when GPS is on.
*/
public static final String POWER_GPS_ON = "gps.on";
/**
+ * GPS power parameters based on signal quality
+ */
+ public static final String POWER_GPS_SIGNAL_QUALITY_BASED = "gps.signalqualitybased";
+ public static final String POWER_GPS_OPERATING_VOLTAGE = "gps.voltage";
+
+ /**
* Power consumption when Bluetooth driver is on.
* @deprecated
*/
@@ -182,9 +192,6 @@ public class PowerProfile {
*/
public static final String POWER_CAMERA = "camera.avg";
- @Deprecated
- public static final String POWER_CPU_SPEEDS = "cpu.speeds";
-
/**
* Power consumed by wif batched scaning. Broken down into bins by
* Channels Scanned per Hour. May do 1-720 scans per hour of 1-100 channels
@@ -197,7 +204,15 @@ public class PowerProfile {
*/
public static final String POWER_BATTERY_CAPACITY = "battery.capacity";
- static final HashMap<String, Object> sPowerMap = new HashMap<>();
+ /**
+ * A map from Power Use Item to its power consumption.
+ */
+ static final HashMap<String, Double> sPowerItemMap = new HashMap<>();
+ /**
+ * A map from Power Use Item to an array of its power consumption
+ * (for items with variable power e.g. CPU).
+ */
+ static final HashMap<String, Double[]> sPowerArrayMap = new HashMap<>();
private static final String TAG_DEVICE = "device";
private static final String TAG_ITEM = "item";
@@ -207,23 +222,32 @@ public class PowerProfile {
private static final Object sLock = new Object();
+ @VisibleForTesting
public PowerProfile(Context context) {
- // Read the XML file for the given profile (normally only one per
- // device)
+ this(context, false);
+ }
+
+ /**
+ * For PowerProfileTest
+ */
+ @VisibleForTesting
+ public PowerProfile(Context context, boolean forTest) {
+ // Read the XML file for the given profile (normally only one per device)
synchronized (sLock) {
- if (sPowerMap.size() == 0) {
- readPowerValuesFromXml(context);
+ if (sPowerItemMap.size() == 0 && sPowerArrayMap.size() == 0) {
+ readPowerValuesFromXml(context, forTest);
}
initCpuClusters();
}
}
- private void readPowerValuesFromXml(Context context) {
- int id = com.android.internal.R.xml.power_profile;
+ private void readPowerValuesFromXml(Context context, boolean forTest) {
+ final int id = forTest ? com.android.internal.R.xml.power_profile_test :
+ com.android.internal.R.xml.power_profile;
final Resources resources = context.getResources();
XmlResourceParser parser = resources.getXml(id);
boolean parsingArray = false;
- ArrayList<Double> array = new ArrayList<Double>();
+ ArrayList<Double> array = new ArrayList<>();
String arrayName = null;
try {
@@ -237,7 +261,7 @@ public class PowerProfile {
if (parsingArray && !element.equals(TAG_ARRAYITEM)) {
// Finish array
- sPowerMap.put(arrayName, array.toArray(new Double[array.size()]));
+ sPowerArrayMap.put(arrayName, array.toArray(new Double[array.size()]));
parsingArray = false;
}
if (element.equals(TAG_ARRAY)) {
@@ -255,7 +279,7 @@ public class PowerProfile {
} catch (NumberFormatException nfe) {
}
if (element.equals(TAG_ITEM)) {
- sPowerMap.put(name, value);
+ sPowerItemMap.put(name, value);
} else if (parsingArray) {
array.add(value);
}
@@ -263,7 +287,7 @@ public class PowerProfile {
}
}
if (parsingArray) {
- sPowerMap.put(arrayName, array.toArray(new Double[array.size()]));
+ sPowerArrayMap.put(arrayName, array.toArray(new Double[array.size()]));
}
} catch (XmlPullParserException e) {
throw new RuntimeException(e);
@@ -279,10 +303,6 @@ public class PowerProfile {
com.android.internal.R.integer.config_bluetooth_rx_cur_ma,
com.android.internal.R.integer.config_bluetooth_tx_cur_ma,
com.android.internal.R.integer.config_bluetooth_operating_voltage_mv,
- com.android.internal.R.integer.config_wifi_idle_receive_cur_ma,
- com.android.internal.R.integer.config_wifi_active_rx_cur_ma,
- com.android.internal.R.integer.config_wifi_tx_cur_ma,
- com.android.internal.R.integer.config_wifi_operating_voltage_mv,
};
String[] configResIdKeys = new String[]{
@@ -290,62 +310,62 @@ public class PowerProfile {
POWER_BLUETOOTH_CONTROLLER_RX,
POWER_BLUETOOTH_CONTROLLER_TX,
POWER_BLUETOOTH_CONTROLLER_OPERATING_VOLTAGE,
- POWER_WIFI_CONTROLLER_IDLE,
- POWER_WIFI_CONTROLLER_RX,
- POWER_WIFI_CONTROLLER_TX,
- POWER_WIFI_CONTROLLER_OPERATING_VOLTAGE,
};
for (int i = 0; i < configResIds.length; i++) {
String key = configResIdKeys[i];
// if we already have some of these parameters in power_profile.xml, ignore the
// value in config.xml
- if ((sPowerMap.containsKey(key) && (Double) sPowerMap.get(key) > 0)) {
+ if ((sPowerItemMap.containsKey(key) && sPowerItemMap.get(key) > 0)) {
continue;
}
int value = resources.getInteger(configResIds[i]);
if (value > 0) {
- sPowerMap.put(key, (double) value);
+ sPowerItemMap.put(key, (double) value);
}
}
}
private CpuClusterKey[] mCpuClusters;
- private static final String POWER_CPU_CLUSTER_CORE_COUNT = "cpu.clusters.cores";
- private static final String POWER_CPU_CLUSTER_SPEED_PREFIX = "cpu.speeds.cluster";
- private static final String POWER_CPU_CLUSTER_ACTIVE_PREFIX = "cpu.active.cluster";
+ private static final String CPU_PER_CLUSTER_CORE_COUNT = "cpu.clusters.cores";
+ private static final String CPU_CLUSTER_POWER_COUNT = "cpu.cluster_power.cluster";
+ private static final String CPU_CORE_SPEED_PREFIX = "cpu.core_speeds.cluster";
+ private static final String CPU_CORE_POWER_PREFIX = "cpu.core_power.cluster";
- @SuppressWarnings("deprecation")
private void initCpuClusters() {
- // Figure out how many CPU clusters we're dealing with
- final Object obj = sPowerMap.get(POWER_CPU_CLUSTER_CORE_COUNT);
- if (obj == null || !(obj instanceof Double[])) {
+ if (sPowerArrayMap.containsKey(CPU_PER_CLUSTER_CORE_COUNT)) {
+ final Double[] data = sPowerArrayMap.get(CPU_PER_CLUSTER_CORE_COUNT);
+ mCpuClusters = new CpuClusterKey[data.length];
+ for (int cluster = 0; cluster < data.length; cluster++) {
+ int numCpusInCluster = (int) Math.round(data[cluster]);
+ mCpuClusters[cluster] = new CpuClusterKey(
+ CPU_CORE_SPEED_PREFIX + cluster, CPU_CLUSTER_POWER_COUNT + cluster,
+ CPU_CORE_POWER_PREFIX + cluster, numCpusInCluster);
+ }
+ } else {
// Default to single.
mCpuClusters = new CpuClusterKey[1];
- mCpuClusters[0] = new CpuClusterKey(POWER_CPU_SPEEDS, POWER_CPU_ACTIVE, 1);
-
- } else {
- final Double[] array = (Double[]) obj;
- mCpuClusters = new CpuClusterKey[array.length];
- for (int cluster = 0; cluster < array.length; cluster++) {
- int numCpusInCluster = (int) Math.round(array[cluster]);
- mCpuClusters[cluster] = new CpuClusterKey(
- POWER_CPU_CLUSTER_SPEED_PREFIX + cluster,
- POWER_CPU_CLUSTER_ACTIVE_PREFIX + cluster,
- numCpusInCluster);
+ int numCpus = 1;
+ if (sPowerItemMap.containsKey(CPU_PER_CLUSTER_CORE_COUNT)) {
+ numCpus = (int) Math.round(sPowerItemMap.get(CPU_PER_CLUSTER_CORE_COUNT));
}
+ mCpuClusters[0] = new CpuClusterKey(CPU_CORE_SPEED_PREFIX + 0,
+ CPU_CLUSTER_POWER_COUNT + 0, CPU_CORE_POWER_PREFIX + 0, numCpus);
}
}
public static class CpuClusterKey {
- private final String timeKey;
- private final String powerKey;
+ private final String freqKey;
+ private final String clusterPowerKey;
+ private final String corePowerKey;
private final int numCpus;
- private CpuClusterKey(String timeKey, String powerKey, int numCpus) {
- this.timeKey = timeKey;
- this.powerKey = powerKey;
+ private CpuClusterKey(String freqKey, String clusterPowerKey,
+ String corePowerKey, int numCpus) {
+ this.freqKey = freqKey;
+ this.clusterPowerKey = clusterPowerKey;
+ this.corePowerKey = corePowerKey;
this.numCpus = numCpus;
}
}
@@ -354,21 +374,30 @@ public class PowerProfile {
return mCpuClusters.length;
}
- public int getNumCoresInCpuCluster(int index) {
- return mCpuClusters[index].numCpus;
+ public int getNumCoresInCpuCluster(int cluster) {
+ return mCpuClusters[cluster].numCpus;
}
- public int getNumSpeedStepsInCpuCluster(int index) {
- Object value = sPowerMap.get(mCpuClusters[index].timeKey);
- if (value != null && value instanceof Double[]) {
- return ((Double[])value).length;
+ public int getNumSpeedStepsInCpuCluster(int cluster) {
+ if (cluster < 0 || cluster >= mCpuClusters.length) {
+ return 0; // index out of bound
+ }
+ if (sPowerArrayMap.containsKey(mCpuClusters[cluster].freqKey)) {
+ return sPowerArrayMap.get(mCpuClusters[cluster].freqKey).length;
}
return 1; // Only one speed
}
- public double getAveragePowerForCpu(int cluster, int step) {
+ public double getAveragePowerForCpuCluster(int cluster) {
if (cluster >= 0 && cluster < mCpuClusters.length) {
- return getAveragePower(mCpuClusters[cluster].powerKey, step);
+ return getAveragePower(mCpuClusters[cluster].clusterPowerKey);
+ }
+ return 0;
+ }
+
+ public double getAveragePowerForCpuCore(int cluster, int step) {
+ if (cluster >= 0 && cluster < mCpuClusters.length) {
+ return getAveragePower(mCpuClusters[cluster].corePowerKey, step);
}
return 0;
}
@@ -379,14 +408,10 @@ public class PowerProfile {
* @return the number of memory bandwidth buckets.
*/
public int getNumElements(String key) {
- if (sPowerMap.containsKey(key)) {
- Object data = sPowerMap.get(key);
- if (data instanceof Double[]) {
- final Double[] values = (Double[]) data;
- return values.length;
- } else {
- return 1;
- }
+ if (sPowerItemMap.containsKey(key)) {
+ return 1;
+ } else if (sPowerArrayMap.containsKey(key)) {
+ return sPowerArrayMap.get(key).length;
}
return 0;
}
@@ -399,13 +424,10 @@ public class PowerProfile {
* @return the average current in milliAmps.
*/
public double getAveragePowerOrDefault(String type, double defaultValue) {
- if (sPowerMap.containsKey(type)) {
- Object data = sPowerMap.get(type);
- if (data instanceof Double[]) {
- return ((Double[])data)[0];
- } else {
- return (Double) sPowerMap.get(type);
- }
+ if (sPowerItemMap.containsKey(type)) {
+ return sPowerItemMap.get(type);
+ } else if (sPowerArrayMap.containsKey(type)) {
+ return sPowerArrayMap.get(type)[0];
} else {
return defaultValue;
}
@@ -429,19 +451,16 @@ public class PowerProfile {
* @return the average current in milliAmps.
*/
public double getAveragePower(String type, int level) {
- if (sPowerMap.containsKey(type)) {
- Object data = sPowerMap.get(type);
- if (data instanceof Double[]) {
- final Double[] values = (Double[]) data;
- if (values.length > level && level >= 0) {
- return values[level];
- } else if (level < 0 || values.length == 0) {
- return 0;
- } else {
- return values[values.length - 1];
- }
+ if (sPowerItemMap.containsKey(type)) {
+ return sPowerItemMap.get(type);
+ } else if (sPowerArrayMap.containsKey(type)) {
+ final Double[] values = sPowerArrayMap.get(type);
+ if (values.length > level && level >= 0) {
+ return values[level];
+ } else if (level < 0 || values.length == 0) {
+ return 0;
} else {
- return (Double) data;
+ return values[values.length - 1];
}
} else {
return 0;
diff --git a/com/android/internal/os/WakelockPowerCalculator.java b/com/android/internal/os/WakelockPowerCalculator.java
index c7897b2b..486b5842 100644
--- a/com/android/internal/os/WakelockPowerCalculator.java
+++ b/com/android/internal/os/WakelockPowerCalculator.java
@@ -26,7 +26,7 @@ public class WakelockPowerCalculator extends PowerCalculator {
private long mTotalAppWakelockTimeMs = 0;
public WakelockPowerCalculator(PowerProfile profile) {
- mPowerWakelock = profile.getAveragePower(PowerProfile.POWER_CPU_AWAKE);
+ mPowerWakelock = profile.getAveragePower(PowerProfile.POWER_CPU_IDLE);
}
@Override
diff --git a/com/android/internal/os/Zygote.java b/com/android/internal/os/Zygote.java
index cbc63cf8..f9a2341f 100644
--- a/com/android/internal/os/Zygote.java
+++ b/com/android/internal/os/Zygote.java
@@ -53,6 +53,8 @@ public final class Zygote {
public static final int DISABLE_VERIFIER = 1 << 9;
/** Only use oat files located in /system. Otherwise use dex/jar/apk . */
public static final int ONLY_USE_SYSTEM_OAT_FILES = 1 << 10;
+ /** Do not enfore hidden API access restrictions. */
+ public static final int DISABLE_HIDDEN_API_CHECKS = 1 << 11;
/** No external storage should be mounted. */
public static final int MOUNT_EXTERNAL_NONE = IVold.REMOUNT_MODE_NONE;
@@ -67,6 +69,9 @@ public final class Zygote {
private Zygote() {}
+ /** Called for some security initialization before any fork. */
+ native static void nativeSecurityInit();
+
/**
* Forks a new VM instance. The current VM must have been started
* with the -Xzygote flag. <b>NOTE: new instance keeps all
@@ -153,6 +158,9 @@ public final class Zygote {
*/
public static int forkSystemServer(int uid, int gid, int[] gids, int runtimeFlags,
int[][] rlimits, long permittedCapabilities, long effectiveCapabilities) {
+ // SystemServer is always allowed to use hidden APIs.
+ runtimeFlags |= DISABLE_HIDDEN_API_CHECKS;
+
VM_HOOKS.preFork();
// Resets nice priority for zygote process.
resetNicePriority();
diff --git a/com/android/internal/os/ZygoteInit.java b/com/android/internal/os/ZygoteInit.java
index 2be6212b..56594705 100644
--- a/com/android/internal/os/ZygoteInit.java
+++ b/com/android/internal/os/ZygoteInit.java
@@ -30,7 +30,6 @@ import android.os.IInstalld;
import android.os.Environment;
import android.os.Process;
import android.os.RemoteException;
-import android.os.Seccomp;
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.os.SystemClock;
@@ -99,6 +98,10 @@ public class ZygoteInit {
private static final String SOCKET_NAME_ARG = "--socket-name=";
+ /* Dexopt flag to disable hidden API access checks when dexopting SystemServer.
+ * Must be kept in sync with com.android.server.pm.Installer. */
+ private static final int DEXOPT_DISABLE_HIDDEN_API_CHECKS = 1 << 10;
+
/**
* Used to pre-load resources.
*/
@@ -566,16 +569,21 @@ public class ZygoteInit {
if (dexoptNeeded != DexFile.NO_DEXOPT_NEEDED) {
final String packageName = "*";
final String outputPath = null;
- final int dexFlags = 0;
+ // Dexopt with a flag which lifts restrictions on hidden API usage.
+ // Offending methods would otherwise be re-verified at runtime and
+ // we want to avoid the performance overhead of that.
+ final int dexFlags = DEXOPT_DISABLE_HIDDEN_API_CHECKS;
final String compilerFilter = systemServerFilter;
final String uuid = StorageManager.UUID_PRIVATE_INTERNAL;
final String seInfo = null;
final String classLoaderContext =
getSystemServerClassLoaderContext(classPathForElement);
+ final int targetSdkVersion = 0; // SystemServer targets the system's SDK version
try {
installd.dexopt(classPathElement, Process.SYSTEM_UID, packageName,
instructionSet, dexoptNeeded, outputPath, dexFlags, compilerFilter,
- uuid, classLoaderContext, seInfo, false /* downgrade */);
+ uuid, classLoaderContext, seInfo, false /* downgrade */,
+ targetSdkVersion);
} catch (RemoteException | ServiceSpecificException e) {
// Ignore (but log), we need this on the classpath for fallback mode.
Log.w(TAG, "Failed compiling classpath element for system server: "
@@ -650,7 +658,7 @@ public class ZygoteInit {
String args[] = {
"--setuid=1000",
"--setgid=1000",
- "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1023,1032,3001,3002,3003,3006,3007,3009,3010",
+ "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1023,1032,1065,3001,3002,3003,3006,3007,3009,3010",
"--capabilities=" + capabilities + "," + capabilities,
"--nice-name=system_server",
"--runtime-args",
@@ -779,12 +787,11 @@ public class ZygoteInit {
// Zygote.
Trace.setTracingEnabled(false, 0);
+ Zygote.nativeSecurityInit();
+
// Zygote process unmounts root storage spaces.
Zygote.nativeUnmountStorageOnInit();
- // Set seccomp policy
- Seccomp.setPolicy();
-
ZygoteHooks.stopZygoteNoThreadCreation();
if (startSystemServer) {
diff --git a/com/android/internal/os/logging/MetricsLoggerWrapper.java b/com/android/internal/os/logging/MetricsLoggerWrapper.java
new file mode 100644
index 00000000..245a66e4
--- /dev/null
+++ b/com/android/internal/os/logging/MetricsLoggerWrapper.java
@@ -0,0 +1,99 @@
+/*
+ * 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 com.android.internal.os.logging;
+
+import android.content.Context;
+import android.util.StatsLog;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+/**
+ * Used to wrap different logging calls in one, so that client side code base is clean and more
+ * readable.
+ */
+public class MetricsLoggerWrapper {
+
+ private static final int METRIC_VALUE_DISMISSED_BY_TAP = 0;
+ private static final int METRIC_VALUE_DISMISSED_BY_DRAG = 1;
+
+ public static void logPictureInPictureDismissByTap(Context context) {
+ MetricsLogger.action(context, MetricsEvent.ACTION_PICTURE_IN_PICTURE_DISMISSED,
+ METRIC_VALUE_DISMISSED_BY_TAP);
+ StatsLog.write(StatsLog.PICTURE_IN_PICTURE_STATE_CHANGED,
+ context.getUserId(),
+ context.getApplicationInfo().packageName,
+ context.getApplicationInfo().className,
+ StatsLog.PICTURE_IN_PICTURE_STATE_CHANGED__STATE__DISMISSED);
+ }
+
+ public static void logPictureInPictureDismissByDrag(Context context) {
+ MetricsLogger.action(context,
+ MetricsEvent.ACTION_PICTURE_IN_PICTURE_DISMISSED,
+ METRIC_VALUE_DISMISSED_BY_DRAG);
+ StatsLog.write(StatsLog.PICTURE_IN_PICTURE_STATE_CHANGED,
+ context.getUserId(),
+ context.getApplicationInfo().packageName,
+ context.getApplicationInfo().className,
+ StatsLog.PICTURE_IN_PICTURE_STATE_CHANGED__STATE__DISMISSED);
+ }
+
+ public static void logPictureInPictureMinimize(Context context, boolean isMinimized) {
+ MetricsLogger.action(context, MetricsEvent.ACTION_PICTURE_IN_PICTURE_MINIMIZED,
+ isMinimized);
+ if (isMinimized) {
+ StatsLog.write(StatsLog.PICTURE_IN_PICTURE_STATE_CHANGED,
+ context.getUserId(),
+ context.getApplicationInfo().packageName,
+ context.getApplicationInfo().className,
+ StatsLog.PICTURE_IN_PICTURE_STATE_CHANGED__STATE__MINIMIZED);
+ } else {
+ StatsLog.write(StatsLog.PICTURE_IN_PICTURE_STATE_CHANGED,
+ context.getUserId(),
+ context.getApplicationInfo().packageName,
+ context.getApplicationInfo().className,
+ StatsLog.PICTURE_IN_PICTURE_STATE_CHANGED__STATE__EXPANDED_TO_FULL_SCREEN);
+ }
+ }
+
+ public static void logPictureInPictureMenuVisible(Context context, boolean menuStateFull) {
+ MetricsLogger.visibility(context, MetricsEvent.ACTION_PICTURE_IN_PICTURE_MENU,
+ menuStateFull);
+ }
+
+ public static void logPictureInPictureEnter(Context context,
+ boolean supportsEnterPipOnTaskSwitch) {
+ MetricsLogger.action(context, MetricsEvent.ACTION_PICTURE_IN_PICTURE_ENTERED,
+ supportsEnterPipOnTaskSwitch);
+ if (supportsEnterPipOnTaskSwitch) {
+ StatsLog.write(StatsLog.PICTURE_IN_PICTURE_STATE_CHANGED, context.getUserId(),
+ context.getApplicationInfo().packageName,
+ context.getApplicationInfo().className,
+ StatsLog.PICTURE_IN_PICTURE_STATE_CHANGED__STATE__ENTERED);
+ }
+ }
+
+ public static void logPictureInPictureFullScreen(Context context) {
+ MetricsLogger.action(context,
+ MetricsEvent.ACTION_PICTURE_IN_PICTURE_EXPANDED_TO_FULLSCREEN);
+ StatsLog.write(StatsLog.PICTURE_IN_PICTURE_STATE_CHANGED,
+ context.getUserId(),
+ context.getApplicationInfo().packageName,
+ context.getApplicationInfo().className,
+ StatsLog.PICTURE_IN_PICTURE_STATE_CHANGED__STATE__EXPANDED_TO_FULL_SCREEN);
+ }
+}
diff --git a/com/android/internal/policy/DecorView.java b/com/android/internal/policy/DecorView.java
index 5fddfba6..95bc3527 100644
--- a/com/android/internal/policy/DecorView.java
+++ b/com/android/internal/policy/DecorView.java
@@ -101,7 +101,6 @@ import static android.view.Window.DECOR_CAPTION_SHADE_LIGHT;
import static android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS;
import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
-import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_OVERSCAN;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
@@ -194,8 +193,6 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
// View added at runtime to draw under the status bar area
private View mStatusGuard;
- // View added at runtime to draw under the navigation bar area
- private View mNavigationGuard;
private final ColorViewState mStatusColorViewState =
new ColorViewState(STATUS_BAR_COLOR_VIEW_ATTRIBUTES);
@@ -1002,7 +999,6 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
mFrameOffsets.set(insets.getSystemWindowInsets());
insets = updateColorViews(insets, true /* animate */);
insets = updateStatusGuard(insets);
- insets = updateNavigationGuard(insets);
if (getForeground() != null) {
drawableChanged();
}
@@ -1062,7 +1058,10 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
WindowManager.LayoutParams attrs = mWindow.getAttributes();
int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();
- if (!mWindow.mIsFloating) {
+ // IME is an exceptional floating window that requires color view.
+ final boolean isImeWindow =
+ mWindow.getAttributes().type == WindowManager.LayoutParams.TYPE_INPUT_METHOD;
+ if (!mWindow.mIsFloating || isImeWindow) {
boolean disallowAnimate = !isLaidOut();
disallowAnimate |= ((mLastWindowFlags ^ attrs.flags)
& FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
@@ -1363,7 +1362,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
if (mStatusGuard == null) {
mStatusGuard = new View(mContext);
mStatusGuard.setBackgroundColor(mContext.getColor(
- R.color.input_method_navigation_guard));
+ R.color.decor_view_status_guard));
addView(mStatusGuard, indexOfChild(mStatusColorViewState.view),
new LayoutParams(LayoutParams.MATCH_PARENT,
mlp.topMargin, Gravity.START | Gravity.TOP));
@@ -1407,51 +1406,6 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
return insets;
}
- private WindowInsets updateNavigationGuard(WindowInsets insets) {
- // IME windows lay out below the nav bar, but the content view must not (for back compat)
- // Only make this adjustment if the window is not requesting layout in overscan
- if (mWindow.getAttributes().type == WindowManager.LayoutParams.TYPE_INPUT_METHOD
- && (mWindow.getAttributes().flags & FLAG_LAYOUT_IN_OVERSCAN) == 0) {
- // prevent the content view from including the nav bar height
- if (mWindow.mContentParent != null) {
- if (mWindow.mContentParent.getLayoutParams() instanceof MarginLayoutParams) {
- MarginLayoutParams mlp =
- (MarginLayoutParams) mWindow.mContentParent.getLayoutParams();
- mlp.bottomMargin = insets.getSystemWindowInsetBottom();
- mWindow.mContentParent.setLayoutParams(mlp);
- }
- }
- // position the navigation guard view, creating it if necessary
- if (mNavigationGuard == null) {
- mNavigationGuard = new View(mContext);
- mNavigationGuard.setBackgroundColor(mContext.getColor(
- R.color.input_method_navigation_guard));
- addView(mNavigationGuard, indexOfChild(mNavigationColorViewState.view),
- new LayoutParams(LayoutParams.MATCH_PARENT,
- insets.getSystemWindowInsetBottom(),
- Gravity.START | Gravity.BOTTOM));
- } else {
- LayoutParams lp = (LayoutParams) mNavigationGuard.getLayoutParams();
- lp.height = insets.getSystemWindowInsetBottom();
- mNavigationGuard.setLayoutParams(lp);
- }
- updateNavigationGuardColor();
- insets = insets.consumeSystemWindowInsets(
- false, false, false, true /* bottom */);
- }
- return insets;
- }
-
- void updateNavigationGuardColor() {
- if (mNavigationGuard != null) {
- // Make navigation bar guard invisible if the transparent color is specified.
- // Only TRANSPARENT is sufficient for hiding the navigation bar if the no software
- // keyboard is shown by IMS.
- mNavigationGuard.setVisibility(mWindow.getNavigationBarColor() == Color.TRANSPARENT ?
- View.INVISIBLE : View.VISIBLE);
- }
- }
-
/**
* Overrides the view outline when the activity enters picture-in-picture to ensure that it has
* an opaque shadow even if the window background is completely transparent. This only applies
@@ -2103,7 +2057,7 @@ public class DecorView extends FrameLayout implements RootViewSurfaceTaker, Wind
for (int i = getChildCount() - 1; i >= 0; i--) {
View v = getChildAt(i);
if (v != mStatusColorViewState.view && v != mNavigationColorViewState.view
- && v != mStatusGuard && v != mNavigationGuard) {
+ && v != mStatusGuard) {
removeViewAt(i);
}
}
diff --git a/com/android/internal/policy/KeyguardDismissCallback.java b/com/android/internal/policy/KeyguardDismissCallback.java
new file mode 100644
index 00000000..38337ec6
--- /dev/null
+++ b/com/android/internal/policy/KeyguardDismissCallback.java
@@ -0,0 +1,41 @@
+/*
+ * 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 com.android.internal.policy;
+
+import android.os.RemoteException;
+import com.android.internal.policy.IKeyguardDismissCallback;
+
+/**
+ * @hide
+ */
+public class KeyguardDismissCallback extends IKeyguardDismissCallback.Stub {
+
+ @Override
+ public void onDismissError() throws RemoteException {
+ // To be overidden
+ }
+
+ @Override
+ public void onDismissSucceeded() throws RemoteException {
+ // To be overidden
+ }
+
+ @Override
+ public void onDismissCancelled() throws RemoteException {
+ // To be overidden
+ }
+}
diff --git a/com/android/internal/policy/PhoneWindow.java b/com/android/internal/policy/PhoneWindow.java
index b13560c1..34b5ec81 100644
--- a/com/android/internal/policy/PhoneWindow.java
+++ b/com/android/internal/policy/PhoneWindow.java
@@ -3807,10 +3807,22 @@ public class PhoneWindow extends Window implements MenuBuilder.Callback {
mForcedNavigationBarColor = true;
if (mDecor != null) {
mDecor.updateColorViews(null, false /* animate */);
- mDecor.updateNavigationGuardColor();
}
}
+ @Override
+ public void setNavigationBarDividerColor(int navigationBarDividerColor) {
+ mNavigationBarDividerColor = navigationBarDividerColor;
+ if (mDecor != null) {
+ mDecor.updateColorViews(null, false /* animate */);
+ }
+ }
+
+ @Override
+ public int getNavigationBarDividerColor() {
+ return mNavigationBarDividerColor;
+ }
+
public void setIsStartingWindow(boolean isStartingWindow) {
mIsStartingWindow = isStartingWindow;
}
diff --git a/com/android/internal/print/DualDumpOutputStream.java b/com/android/internal/print/DualDumpOutputStream.java
new file mode 100644
index 00000000..4b10ef2f
--- /dev/null
+++ b/com/android/internal/print/DualDumpOutputStream.java
@@ -0,0 +1,276 @@
+/*
+ * 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 com.android.internal.print;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.LinkedList;
+
+/**
+ * Dump either to a proto or a print writer using the same interface.
+ *
+ * <p>This mirrors the interface of {@link ProtoOutputStream}.
+ */
+public class DualDumpOutputStream {
+ private static final String LOG_TAG = DualDumpOutputStream.class.getSimpleName();
+
+ // When writing to a proto, the proto
+ private final @Nullable ProtoOutputStream mProtoStream;
+
+ // When printing in clear text, the writer
+ private final @Nullable IndentingPrintWriter mIpw;
+ // Temporary storage of data when printing to mIpw
+ private final LinkedList<DumpObject> mDumpObjects = new LinkedList<>();
+
+ private static abstract class Dumpable {
+ final String name;
+
+ private Dumpable(String name) {
+ this.name = name;
+ }
+
+ abstract void print(IndentingPrintWriter ipw, boolean printName);
+ }
+
+ private static class DumpObject extends Dumpable {
+ private final LinkedHashMap<String, ArrayList<Dumpable>> mSubObjects = new LinkedHashMap<>();
+
+ private DumpObject(String name) {
+ super(name);
+ }
+
+ @Override
+ void print(IndentingPrintWriter ipw, boolean printName) {
+ if (printName) {
+ ipw.println(name + "={");
+ } else {
+ ipw.println("{");
+ }
+ ipw.increaseIndent();
+
+ for (ArrayList<Dumpable> subObject: mSubObjects.values()) {
+ int numDumpables = subObject.size();
+
+ if (numDumpables == 1) {
+ subObject.get(0).print(ipw, true);
+ } else {
+ ipw.println(subObject.get(0).name + "=[");
+ ipw.increaseIndent();
+
+ for (int i = 0; i < numDumpables; i++) {
+ subObject.get(i).print(ipw, false);
+ }
+
+ ipw.decreaseIndent();
+ ipw.println("]");
+ }
+ }
+
+ ipw.decreaseIndent();
+ ipw.println("}");
+ }
+
+ /**
+ * Add new field / subobject to this object.
+ *
+ * <p>If a name is added twice, they will be printed as a array
+ *
+ * @param fieldName name of the field added
+ * @param d The dumpable to add
+ */
+ public void add(String fieldName, Dumpable d) {
+ ArrayList<Dumpable> l = mSubObjects.get(fieldName);
+
+ if (l == null) {
+ l = new ArrayList<>(1);
+ mSubObjects.put(fieldName, l);
+ }
+
+ l.add(d);
+ }
+ }
+
+ private static class DumpField extends Dumpable {
+ private final String mValue;
+
+ private DumpField(String name, String value) {
+ super(name);
+ this.mValue = value;
+ }
+
+ @Override
+ void print(IndentingPrintWriter ipw, boolean printName) {
+ if (printName) {
+ ipw.println(name + "=" + mValue);
+ } else {
+ ipw.println(mValue);
+ }
+ }
+ }
+
+
+ /**
+ * Create a new DualDumpOutputStream. Only one output should be set.
+ *
+ * @param proto If dumping to proto the {@link ProtoOutputStream}
+ * @param ipw If dumping to a print writer, the {@link IndentingPrintWriter}
+ */
+ public DualDumpOutputStream(@Nullable ProtoOutputStream proto,
+ @Nullable IndentingPrintWriter ipw) {
+ if ((proto == null) == (ipw == null)) {
+ Log.e(LOG_TAG, "Cannot dump to clear text and proto at once. Ignoring proto");
+ proto = null;
+ }
+
+ mProtoStream = proto;
+ mIpw = ipw;
+
+ if (!isProto()) {
+ // Add root object
+ mDumpObjects.add(new DumpObject(null));
+ }
+ }
+
+ public void write(@NonNull String fieldName, long fieldId, double val) {
+ if (mProtoStream != null) {
+ mProtoStream.write(fieldId, val);
+ } else {
+ mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
+ }
+ }
+
+ public void write(@NonNull String fieldName, long fieldId, boolean val) {
+ if (mProtoStream != null) {
+ mProtoStream.write(fieldId, val);
+ } else {
+ mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
+ }
+ }
+
+ public void write(@NonNull String fieldName, long fieldId, int val) {
+ if (mProtoStream != null) {
+ mProtoStream.write(fieldId, val);
+ } else {
+ mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
+ }
+ }
+
+ public void write(@NonNull String fieldName, long fieldId, float val) {
+ if (mProtoStream != null) {
+ mProtoStream.write(fieldId, val);
+ } else {
+ mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
+ }
+ }
+
+ public void write(@NonNull String fieldName, long fieldId, byte[] val) {
+ if (mProtoStream != null) {
+ mProtoStream.write(fieldId, val);
+ } else {
+ mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, Arrays.toString(val)));
+ }
+ }
+
+ public void write(@NonNull String fieldName, long fieldId, long val) {
+ if (mProtoStream != null) {
+ mProtoStream.write(fieldId, val);
+ } else {
+ mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
+ }
+ }
+
+ public void write(@NonNull String fieldName, long fieldId, @Nullable String val) {
+ if (mProtoStream != null) {
+ mProtoStream.write(fieldId, val);
+ } else {
+ mDumpObjects.getLast().add(fieldName, new DumpField(fieldName, String.valueOf(val)));
+ }
+ }
+
+ public long start(@NonNull String fieldName, long fieldId) {
+ if (mProtoStream != null) {
+ return mProtoStream.start(fieldId);
+ } else {
+ DumpObject d = new DumpObject(fieldName);
+ mDumpObjects.getLast().add(fieldName, d);
+ mDumpObjects.addLast(d);
+ return System.identityHashCode(d);
+ }
+ }
+
+ public void end(long token) {
+ if (mProtoStream != null) {
+ mProtoStream.end(token);
+ } else {
+ if (System.identityHashCode(mDumpObjects.getLast()) != token) {
+ Log.w(LOG_TAG, "Unexpected token for ending " + mDumpObjects.getLast().name
+ + " at " + Arrays.toString(Thread.currentThread().getStackTrace()));
+ }
+ mDumpObjects.removeLast();
+ }
+ }
+
+ public void flush() {
+ if (mProtoStream != null) {
+ mProtoStream.flush();
+ } else {
+ if (mDumpObjects.size() == 1) {
+ mDumpObjects.getFirst().print(mIpw, false);
+
+ // Reset root object
+ mDumpObjects.clear();
+ mDumpObjects.add(new DumpObject(null));
+ }
+
+ mIpw.flush();
+ }
+ }
+
+ /**
+ * Add a dump from a different service into this dump.
+ *
+ * <p>Only for clear text dump. For proto dump use {@link #write(String, long, byte[])}.
+ *
+ * @param fieldName The name of the field
+ * @param nestedState The state of the dump
+ */
+ public void writeNested(@NonNull String fieldName, byte[] nestedState) {
+ if (mIpw == null) {
+ Log.w(LOG_TAG, "writeNested does not work for proto logging");
+ return;
+ }
+
+ mDumpObjects.getLast().add(fieldName,
+ new DumpField(fieldName, (new String(nestedState, StandardCharsets.UTF_8)).trim()));
+ }
+
+ /**
+ * @return {@code true} iff we are dumping to a proto
+ */
+ public boolean isProto() {
+ return mProtoStream != null;
+ }
+}
diff --git a/com/android/internal/print/DumpUtils.java b/com/android/internal/print/DumpUtils.java
index 28c7fc21..3192d5cb 100644
--- a/com/android/internal/print/DumpUtils.java
+++ b/com/android/internal/print/DumpUtils.java
@@ -39,7 +39,6 @@ import android.service.print.PrinterCapabilitiesProto;
import android.service.print.PrinterIdProto;
import android.service.print.PrinterInfoProto;
import android.service.print.ResolutionProto;
-import android.util.proto.ProtoOutputStream;
/**
* Utilities for dumping print related proto buffer
@@ -49,13 +48,14 @@ public class DumpUtils {
* Write a string to a proto if the string is not {@code null}.
*
* @param proto The proto to write to
+ * @param idName Clear text name of the proto-id
* @param id The proto-id of the string
* @param string The string to write
*/
- public static void writeStringIfNotNull(@NonNull ProtoOutputStream proto, long id,
- @Nullable String string) {
+ public static void writeStringIfNotNull(@NonNull DualDumpOutputStream proto, String idName,
+ long id, @Nullable String string) {
if (string != null) {
- proto.write(id, string);
+ proto.write(idName, id, string);
}
}
@@ -63,14 +63,15 @@ public class DumpUtils {
* Write a {@link ComponentName} to a proto.
*
* @param proto The proto to write to
+ * @param idName Clear text name of the proto-id
* @param id The proto-id of the component name
* @param component The component name to write
*/
- public static void writeComponentName(@NonNull ProtoOutputStream proto, long id,
- @NonNull ComponentName component) {
- long token = proto.start(id);
- proto.write(ComponentNameProto.PACKAGE_NAME, component.getPackageName());
- proto.write(ComponentNameProto.CLASS_NAME, component.getClassName());
+ public static void writeComponentName(@NonNull DualDumpOutputStream proto, String idName,
+ long id, @NonNull ComponentName component) {
+ long token = proto.start(idName, id);
+ proto.write("package_name", ComponentNameProto.PACKAGE_NAME, component.getPackageName());
+ proto.write("class_name", ComponentNameProto.CLASS_NAME, component.getClassName());
proto.end(token);
}
@@ -78,14 +79,16 @@ public class DumpUtils {
* Write a {@link PrinterId} to a proto.
*
* @param proto The proto to write to
+ * @param idName Clear text name of the proto-id
* @param id The proto-id of the component name
* @param printerId The printer id to write
*/
- public static void writePrinterId(@NonNull ProtoOutputStream proto, long id,
+ public static void writePrinterId(@NonNull DualDumpOutputStream proto, String idName, long id,
@NonNull PrinterId printerId) {
- long token = proto.start(id);
- writeComponentName(proto, PrinterIdProto.SERVICE_NAME, printerId.getServiceName());
- proto.write(PrinterIdProto.LOCAL_ID, printerId.getLocalId());
+ long token = proto.start(idName, id);
+ writeComponentName(proto, "service_name", PrinterIdProto.SERVICE_NAME,
+ printerId.getServiceName());
+ proto.write("local_id", PrinterIdProto.LOCAL_ID, printerId.getLocalId());
proto.end(token);
}
@@ -93,71 +96,76 @@ public class DumpUtils {
* Write a {@link PrinterCapabilitiesInfo} to a proto.
*
* @param proto The proto to write to
+ * @param idName Clear text name of the proto-id
* @param id The proto-id of the component name
* @param cap The capabilities to write
*/
public static void writePrinterCapabilities(@NonNull Context context,
- @NonNull ProtoOutputStream proto, long id, @NonNull PrinterCapabilitiesInfo cap) {
- long token = proto.start(id);
- writeMargins(proto, PrinterCapabilitiesProto.MIN_MARGINS, cap.getMinMargins());
+ @NonNull DualDumpOutputStream proto, String idName, long id,
+ @NonNull PrinterCapabilitiesInfo cap) {
+ long token = proto.start(idName, id);
+ writeMargins(proto, "min_margins", PrinterCapabilitiesProto.MIN_MARGINS,
+ cap.getMinMargins());
int numMediaSizes = cap.getMediaSizes().size();
for (int i = 0; i < numMediaSizes; i++) {
- writeMediaSize(context, proto, PrinterCapabilitiesProto.MEDIA_SIZES,
+ writeMediaSize(context, proto, "media_sizes", PrinterCapabilitiesProto.MEDIA_SIZES,
cap.getMediaSizes().get(i));
}
int numResolutions = cap.getResolutions().size();
for (int i = 0; i < numResolutions; i++) {
- writeResolution(proto, PrinterCapabilitiesProto.RESOLUTIONS,
+ writeResolution(proto, "resolutions", PrinterCapabilitiesProto.RESOLUTIONS,
cap.getResolutions().get(i));
}
if ((cap.getColorModes() & PrintAttributes.COLOR_MODE_MONOCHROME) != 0) {
- proto.write(PrinterCapabilitiesProto.COLOR_MODES,
+ proto.write("color_modes", PrinterCapabilitiesProto.COLOR_MODES,
PrintAttributesProto.COLOR_MODE_MONOCHROME);
}
if ((cap.getColorModes() & PrintAttributes.COLOR_MODE_COLOR) != 0) {
- proto.write(PrinterCapabilitiesProto.COLOR_MODES,
+ proto.write("color_modes", PrinterCapabilitiesProto.COLOR_MODES,
PrintAttributesProto.COLOR_MODE_COLOR);
}
if ((cap.getDuplexModes() & PrintAttributes.DUPLEX_MODE_NONE) != 0) {
- proto.write(PrinterCapabilitiesProto.DUPLEX_MODES,
+ proto.write("duplex_modes", PrinterCapabilitiesProto.DUPLEX_MODES,
PrintAttributesProto.DUPLEX_MODE_NONE);
}
if ((cap.getDuplexModes() & PrintAttributes.DUPLEX_MODE_LONG_EDGE) != 0) {
- proto.write(PrinterCapabilitiesProto.DUPLEX_MODES,
+ proto.write("duplex_modes", PrinterCapabilitiesProto.DUPLEX_MODES,
PrintAttributesProto.DUPLEX_MODE_LONG_EDGE);
}
if ((cap.getDuplexModes() & PrintAttributes.DUPLEX_MODE_SHORT_EDGE) != 0) {
- proto.write(PrinterCapabilitiesProto.DUPLEX_MODES,
+ proto.write("duplex_modes", PrinterCapabilitiesProto.DUPLEX_MODES,
PrintAttributesProto.DUPLEX_MODE_SHORT_EDGE);
}
proto.end(token);
}
-
/**
* Write a {@link PrinterInfo} to a proto.
*
* @param context The context used to resolve resources
* @param proto The proto to write to
+ * @param idName Clear text name of the proto-id
* @param id The proto-id of the component name
* @param info The printer info to write
*/
- public static void writePrinterInfo(@NonNull Context context, @NonNull ProtoOutputStream proto,
- long id, @NonNull PrinterInfo info) {
- long token = proto.start(id);
- writePrinterId(proto, PrinterInfoProto.ID, info.getId());
- proto.write(PrinterInfoProto.NAME, info.getName());
- proto.write(PrinterInfoProto.STATUS, info.getStatus());
- proto.write(PrinterInfoProto.DESCRIPTION, info.getDescription());
+ public static void writePrinterInfo(@NonNull Context context,
+ @NonNull DualDumpOutputStream proto, String idName, long id,
+ @NonNull PrinterInfo info) {
+ long token = proto.start(idName, id);
+ writePrinterId(proto, "id", PrinterInfoProto.ID, info.getId());
+ proto.write("name", PrinterInfoProto.NAME, info.getName());
+ proto.write("status", PrinterInfoProto.STATUS, info.getStatus());
+ proto.write("description", PrinterInfoProto.DESCRIPTION, info.getDescription());
PrinterCapabilitiesInfo cap = info.getCapabilities();
if (cap != null) {
- writePrinterCapabilities(context, proto, PrinterInfoProto.CAPABILITIES, cap);
+ writePrinterCapabilities(context, proto, "capabilities", PrinterInfoProto.CAPABILITIES,
+ cap);
}
proto.end(token);
@@ -168,16 +176,17 @@ public class DumpUtils {
*
* @param context The context used to resolve resources
* @param proto The proto to write to
+ * @param idName Clear text name of the proto-id
* @param id The proto-id of the component name
* @param mediaSize The media size to write
*/
- public static void writeMediaSize(@NonNull Context context, @NonNull ProtoOutputStream proto,
- long id, @NonNull PrintAttributes.MediaSize mediaSize) {
- long token = proto.start(id);
- proto.write(MediaSizeProto.ID, mediaSize.getId());
- proto.write(MediaSizeProto.LABEL, mediaSize.getLabel(context.getPackageManager()));
- proto.write(MediaSizeProto.HEIGHT_MILS, mediaSize.getHeightMils());
- proto.write(MediaSizeProto.WIDTH_MILS, mediaSize.getWidthMils());
+ public static void writeMediaSize(@NonNull Context context, @NonNull DualDumpOutputStream proto,
+ String idName, long id, @NonNull PrintAttributes.MediaSize mediaSize) {
+ long token = proto.start(idName, id);
+ proto.write("id", MediaSizeProto.ID, mediaSize.getId());
+ proto.write("label", MediaSizeProto.LABEL, mediaSize.getLabel(context.getPackageManager()));
+ proto.write("height_mils", MediaSizeProto.HEIGHT_MILS, mediaSize.getHeightMils());
+ proto.write("width_mils", MediaSizeProto.WIDTH_MILS, mediaSize.getWidthMils());
proto.end(token);
}
@@ -185,16 +194,17 @@ public class DumpUtils {
* Write a {@link PrintAttributes.Resolution} to a proto.
*
* @param proto The proto to write to
+ * @param idName Clear text name of the proto-id
* @param id The proto-id of the component name
* @param res The resolution to write
*/
- public static void writeResolution(@NonNull ProtoOutputStream proto, long id,
+ public static void writeResolution(@NonNull DualDumpOutputStream proto, String idName, long id,
@NonNull PrintAttributes.Resolution res) {
- long token = proto.start(id);
- proto.write(ResolutionProto.ID, res.getId());
- proto.write(ResolutionProto.LABEL, res.getLabel());
- proto.write(ResolutionProto.HORIZONTAL_DPI, res.getHorizontalDpi());
- proto.write(ResolutionProto.VERTICAL_DPI, res.getVerticalDpi());
+ long token = proto.start(idName, id);
+ proto.write("id", ResolutionProto.ID, res.getId());
+ proto.write("label", ResolutionProto.LABEL, res.getLabel());
+ proto.write("horizontal_DPI", ResolutionProto.HORIZONTAL_DPI, res.getHorizontalDpi());
+ proto.write("veritical_DPI", ResolutionProto.VERTICAL_DPI, res.getVerticalDpi());
proto.end(token);
}
@@ -202,16 +212,17 @@ public class DumpUtils {
* Write a {@link PrintAttributes.Margins} to a proto.
*
* @param proto The proto to write to
+ * @param idName Clear text name of the proto-id
* @param id The proto-id of the component name
* @param margins The margins to write
*/
- public static void writeMargins(@NonNull ProtoOutputStream proto, long id,
+ public static void writeMargins(@NonNull DualDumpOutputStream proto, String idName, long id,
@NonNull PrintAttributes.Margins margins) {
- long token = proto.start(id);
- proto.write(MarginsProto.TOP_MILS, margins.getTopMils());
- proto.write(MarginsProto.LEFT_MILS, margins.getLeftMils());
- proto.write(MarginsProto.RIGHT_MILS, margins.getRightMils());
- proto.write(MarginsProto.BOTTOM_MILS, margins.getBottomMils());
+ long token = proto.start(idName, id);
+ proto.write("top_mils", MarginsProto.TOP_MILS, margins.getTopMils());
+ proto.write("left_mils", MarginsProto.LEFT_MILS, margins.getLeftMils());
+ proto.write("right_mils", MarginsProto.RIGHT_MILS, margins.getRightMils());
+ proto.write("bottom_mils", MarginsProto.BOTTOM_MILS, margins.getBottomMils());
proto.end(token);
}
@@ -220,32 +231,34 @@ public class DumpUtils {
*
* @param context The context used to resolve resources
* @param proto The proto to write to
+ * @param idName Clear text name of the proto-id
* @param id The proto-id of the component name
* @param attributes The attributes to write
*/
public static void writePrintAttributes(@NonNull Context context,
- @NonNull ProtoOutputStream proto, long id, @NonNull PrintAttributes attributes) {
- long token = proto.start(id);
+ @NonNull DualDumpOutputStream proto, String idName, long id,
+ @NonNull PrintAttributes attributes) {
+ long token = proto.start(idName, id);
PrintAttributes.MediaSize mediaSize = attributes.getMediaSize();
if (mediaSize != null) {
- writeMediaSize(context, proto, PrintAttributesProto.MEDIA_SIZE, mediaSize);
+ writeMediaSize(context, proto, "media_size", PrintAttributesProto.MEDIA_SIZE, mediaSize);
}
- proto.write(PrintAttributesProto.IS_PORTRAIT, attributes.isPortrait());
+ proto.write("is_portrait", PrintAttributesProto.IS_PORTRAIT, attributes.isPortrait());
PrintAttributes.Resolution res = attributes.getResolution();
if (res != null) {
- writeResolution(proto, PrintAttributesProto.RESOLUTION, res);
+ writeResolution(proto, "resolution", PrintAttributesProto.RESOLUTION, res);
}
PrintAttributes.Margins minMargins = attributes.getMinMargins();
if (minMargins != null) {
- writeMargins(proto, PrintAttributesProto.MIN_MARGINS, minMargins);
+ writeMargins(proto, "min_margings", PrintAttributesProto.MIN_MARGINS, minMargins);
}
- proto.write(PrintAttributesProto.COLOR_MODE, attributes.getColorMode());
- proto.write(PrintAttributesProto.DUPLEX_MODE, attributes.getDuplexMode());
+ proto.write("color_mode", PrintAttributesProto.COLOR_MODE, attributes.getColorMode());
+ proto.write("duplex_mode", PrintAttributesProto.DUPLEX_MODE, attributes.getDuplexMode());
proto.end(token);
}
@@ -253,21 +266,22 @@ public class DumpUtils {
* Write a {@link PrintDocumentInfo} to a proto.
*
* @param proto The proto to write to
+ * @param idName Clear text name of the proto-id
* @param id The proto-id of the component name
* @param info The info to write
*/
- public static void writePrintDocumentInfo(@NonNull ProtoOutputStream proto, long id,
- @NonNull PrintDocumentInfo info) {
- long token = proto.start(id);
- proto.write(PrintDocumentInfoProto.NAME, info.getName());
+ public static void writePrintDocumentInfo(@NonNull DualDumpOutputStream proto, String idName,
+ long id, @NonNull PrintDocumentInfo info) {
+ long token = proto.start(idName, id);
+ proto.write("name", PrintDocumentInfoProto.NAME, info.getName());
int pageCount = info.getPageCount();
if (pageCount != PrintDocumentInfo.PAGE_COUNT_UNKNOWN) {
- proto.write(PrintDocumentInfoProto.PAGE_COUNT, pageCount);
+ proto.write("page_count", PrintDocumentInfoProto.PAGE_COUNT, pageCount);
}
- proto.write(PrintDocumentInfoProto.CONTENT_TYPE, info.getContentType());
- proto.write(PrintDocumentInfoProto.DATA_SIZE, info.getDataSize());
+ proto.write("content_type", PrintDocumentInfoProto.CONTENT_TYPE, info.getContentType());
+ proto.write("data_size", PrintDocumentInfoProto.DATA_SIZE, info.getDataSize());
proto.end(token);
}
@@ -275,14 +289,15 @@ public class DumpUtils {
* Write a {@link PageRange} to a proto.
*
* @param proto The proto to write to
+ * @param idName Clear text name of the proto-id
* @param id The proto-id of the component name
* @param range The range to write
*/
- public static void writePageRange(@NonNull ProtoOutputStream proto, long id,
+ public static void writePageRange(@NonNull DualDumpOutputStream proto, String idName, long id,
@NonNull PageRange range) {
- long token = proto.start(id);
- proto.write(PageRangeProto.START, range.getStart());
- proto.write(PageRangeProto.END, range.getEnd());
+ long token = proto.start(idName, id);
+ proto.write("start", PageRangeProto.START, range.getStart());
+ proto.write("end", PageRangeProto.END, range.getEnd());
proto.end(token);
}
@@ -291,64 +306,70 @@ public class DumpUtils {
*
* @param context The context used to resolve resources
* @param proto The proto to write to
+ * @param idName Clear text name of the proto-id
* @param id The proto-id of the component name
* @param printJobInfo The print job info to write
*/
- public static void writePrintJobInfo(@NonNull Context context, @NonNull ProtoOutputStream proto,
- long id, @NonNull PrintJobInfo printJobInfo) {
- long token = proto.start(id);
- proto.write(PrintJobInfoProto.LABEL, printJobInfo.getLabel());
+ public static void writePrintJobInfo(@NonNull Context context,
+ @NonNull DualDumpOutputStream proto, String idName, long id,
+ @NonNull PrintJobInfo printJobInfo) {
+ long token = proto.start(idName, id);
+ proto.write("label", PrintJobInfoProto.LABEL, printJobInfo.getLabel());
PrintJobId printJobId = printJobInfo.getId();
if (printJobId != null) {
- proto.write(PrintJobInfoProto.PRINT_JOB_ID, printJobId.flattenToString());
+ proto.write("print_job_id", PrintJobInfoProto.PRINT_JOB_ID,
+ printJobId.flattenToString());
}
int state = printJobInfo.getState();
if (state >= PrintJobInfoProto.STATE_CREATED && state <= PrintJobInfoProto.STATE_CANCELED) {
- proto.write(PrintJobInfoProto.STATE, state);
+ proto.write("state", PrintJobInfoProto.STATE, state);
} else {
- proto.write(PrintJobInfoProto.STATE, PrintJobInfoProto.STATE_UNKNOWN);
+ proto.write("state", PrintJobInfoProto.STATE, PrintJobInfoProto.STATE_UNKNOWN);
}
PrinterId printer = printJobInfo.getPrinterId();
if (printer != null) {
- writePrinterId(proto, PrintJobInfoProto.PRINTER, printer);
+ writePrinterId(proto, "printer", PrintJobInfoProto.PRINTER, printer);
}
String tag = printJobInfo.getTag();
if (tag != null) {
- proto.write(PrintJobInfoProto.TAG, tag);
+ proto.write("tag", PrintJobInfoProto.TAG, tag);
}
- proto.write(PrintJobInfoProto.CREATION_TIME, printJobInfo.getCreationTime());
+ proto.write("creation_time", PrintJobInfoProto.CREATION_TIME,
+ printJobInfo.getCreationTime());
PrintAttributes attributes = printJobInfo.getAttributes();
if (attributes != null) {
- writePrintAttributes(context, proto, PrintJobInfoProto.ATTRIBUTES, attributes);
+ writePrintAttributes(context, proto, "attributes", PrintJobInfoProto.ATTRIBUTES,
+ attributes);
}
PrintDocumentInfo docInfo = printJobInfo.getDocumentInfo();
if (docInfo != null) {
- writePrintDocumentInfo(proto, PrintJobInfoProto.DOCUMENT_INFO, docInfo);
+ writePrintDocumentInfo(proto, "document_info", PrintJobInfoProto.DOCUMENT_INFO,
+ docInfo);
}
- proto.write(PrintJobInfoProto.IS_CANCELING, printJobInfo.isCancelling());
+ proto.write("is_canceling", PrintJobInfoProto.IS_CANCELING, printJobInfo.isCancelling());
PageRange[] pages = printJobInfo.getPages();
if (pages != null) {
for (int i = 0; i < pages.length; i++) {
- writePageRange(proto, PrintJobInfoProto.PAGES, pages[i]);
+ writePageRange(proto, "pages", PrintJobInfoProto.PAGES, pages[i]);
}
}
- proto.write(PrintJobInfoProto.HAS_ADVANCED_OPTIONS,
+ proto.write("has_advanced_options", PrintJobInfoProto.HAS_ADVANCED_OPTIONS,
printJobInfo.getAdvancedOptions() != null);
- proto.write(PrintJobInfoProto.PROGRESS, printJobInfo.getProgress());
+ proto.write("progress", PrintJobInfoProto.PROGRESS, printJobInfo.getProgress());
CharSequence status = printJobInfo.getStatus(context.getPackageManager());
if (status != null) {
- proto.write(PrintJobInfoProto.STATUS, status.toString());
+ proto.write("status", PrintJobInfoProto.STATUS, status.toString());
}
proto.end(token);
diff --git a/com/android/internal/telephony/BaseCommands.java b/com/android/internal/telephony/BaseCommands.java
index b70e800c..8a0f9185 100644
--- a/com/android/internal/telephony/BaseCommands.java
+++ b/com/android/internal/telephony/BaseCommands.java
@@ -74,7 +74,7 @@ public abstract class BaseCommands implements CommandsInterface {
protected RegistrantList mCarrierInfoForImsiEncryptionRegistrants = new RegistrantList();
protected RegistrantList mRilNetworkScanResultRegistrants = new RegistrantList();
protected RegistrantList mModemResetRegistrants = new RegistrantList();
-
+ protected RegistrantList mNattKeepaliveStatusRegistrants = new RegistrantList();
protected Registrant mGsmSmsRegistrant;
protected Registrant mCdmaSmsRegistrant;
@@ -939,4 +939,20 @@ public abstract class BaseCommands implements CommandsInterface {
public void unregisterForCarrierInfoForImsiEncryption(Handler h) {
mCarrierInfoForImsiEncryptionRegistrants.remove(h);
}
+
+ @Override
+ public void registerForNattKeepaliveStatus(Handler h, int what, Object obj) {
+ Registrant r = new Registrant(h, what, obj);
+
+ synchronized (mStateMonitor) {
+ mNattKeepaliveStatusRegistrants.add(r);
+ }
+ }
+
+ @Override
+ public void unregisterForNattKeepaliveStatus(Handler h) {
+ synchronized (mStateMonitor) {
+ mNattKeepaliveStatusRegistrants.remove(h);
+ }
+ }
}
diff --git a/com/android/internal/telephony/CallFailCause.java b/com/android/internal/telephony/CallFailCause.java
index ed39b4dc..acc14327 100644
--- a/com/android/internal/telephony/CallFailCause.java
+++ b/com/android/internal/telephony/CallFailCause.java
@@ -24,33 +24,122 @@ package com.android.internal.telephony;
* CDMA call failure reasons are derived from the possible call failure scenarios described
* in "CDMA IS2000 - Release A (C.S0005-A v6.0)" standard.
*
+ * The detailed fail causes are defined in ITU Recommendation Q.850.
+ *
* {@hide}
*
*/
public interface CallFailCause {
+ // The disconnect cause is not valid (Not received a disconnect cause)
+ int NOT_VALID = -1;
+
// Unassigned/Unobtainable number
int UNOBTAINABLE_NUMBER = 1;
+ int NO_ROUTE_TO_DEST = 3;
+ int CHANNEL_UNACCEPTABLE = 6;
int OPERATOR_DETERMINED_BARRING = 8;
int NORMAL_CLEARING = 16;
- // Busy Tone
int USER_BUSY = 17;
+ int NO_USER_RESPONDING = 18;
+
+ /**
+ * This cause is used when the called party has been alerted but does not respond with a connect
+ * indication within a prescribed period of time. Note - This cause is not necessarily generated
+ * by Q.931 procedures but may be generated by internal network timers.
+ */
+ int USER_ALERTING_NO_ANSWER = 19;
+
+ /**
+ * The equipment sending this cause does not wish to accept this call, although it could have
+ * accepted the call because the equipment sending this cause is neither busy nor incompatible.
+ * The network may also generate this cause, indicating that the call was cleared due to a
+ * supplementary service constraint. The diagnostic field may contain additional information
+ * about the supplementary service and reason for rejection.
+ */
+ int CALL_REJECTED = 21;
- // No Tone
int NUMBER_CHANGED = 22;
+ int PRE_EMPTION = 25;
+
+ // The user has not been awarded the incoming call.
+ int NON_SELECTED_USER_CLEARING = 26;
+
+ int DESTINATION_OUT_OF_ORDER = 27;
+
+ // Incomplete number
+ int INVALID_NUMBER_FORMAT = 28;
+
+ // Supplementary service requested by the user cannot be provide by the network.
+ int FACILITY_REJECTED = 29;
+
int STATUS_ENQUIRY = 30;
int NORMAL_UNSPECIFIED = 31;
-
- // Congestion Tone
int NO_CIRCUIT_AVAIL = 34;
+
+ // Resource unavailable
+ int NETWORK_OUT_OF_ORDER = 38;
int TEMPORARY_FAILURE = 41;
int SWITCHING_CONGESTION = 42;
+ int ACCESS_INFORMATION_DISCARDED = 43;
int CHANNEL_NOT_AVAIL = 44;
+ int RESOURCES_UNAVAILABLE_UNSPECIFIED = 47;
int QOS_NOT_AVAIL = 49;
+
+ // Service or option unavailable
+ /**
+ * The user has requested a supplementary service, which is available, but the user is not
+ * authorized to use.
+ */
+ int REQUESTED_FACILITY_NOT_SUBSCRIBED = 50;
+ /**
+ * Although the called party is a member of the CUG (Closed User Group) for the incoming CUG
+ * call, incoming calls are not allowed to this member of the CUG.
+ */
+ int INCOMING_CALL_BARRED_WITHIN_CUG = 55;
+ int BEARER_CAPABILITY_NOT_AUTHORISED = 57;
int BEARER_NOT_AVAIL = 58;
+ /**
+ * This cause is used to report a service or option not available event only when no other cause
+ * between 49-62 (where a service or option is unavailable) applies.
+ */
+ int SERVICE_OR_OPTION_NOT_AVAILABLE = 63;
+ int BEARER_SERVICE_NOT_IMPLEMENTED = 65;
- // others
+ // Service or option not implemented
int ACM_LIMIT_EXCEEDED = 68;
+ int REQUESTED_FACILITY_NOT_IMPLEMENTED = 69;
+ /**
+ * The calling party has requested an unrestricted bearer service but that the equipment sending
+ * this cause only supports the restricted version of the requested bearer capability.
+ */
+ int ONLY_RESTRICTED_DIGITAL_INFO_BC_AVAILABLE = 70;
+ int SERVICE_OR_OPTION_NOT_IMPLEMENTED = 79;
+ int INVALID_TRANSACTION_ID_VALUE = 81;
+
+ // Invalid message
+ int USER_NOT_MEMBER_OF_CUG = 87;
+ int INCOMPATIBLE_DESTINATION = 88;
+ int INVALID_TRANSIT_NETWORK_SELECTION = 91;
+ int SEMANTICALLY_INCORRECT_MESSAGE = 95;
+ int INVALID_MANDATORY_INFORMATION = 96;
+
+ // Protocol error
+ int MESSAGE_TYPE_NON_EXISTENT = 97;
+ int MESSAGE_TYPE_NOT_COMPATIBLE_WITH_PROT_STATE = 98;
+ int IE_NON_EXISTENT_OR_NOT_IMPLEMENTED = 99;
+ /**
+ * The equipment sending this cause has received an information element which it has
+ * implemented; however, one or more fields in the information element are coded in such a way
+ * which has not been implemented by the equipment sending this cause.
+ */
+ int CONDITIONAL_IE_ERROR = 100;
+ int MESSAGE_NOT_COMPATIBLE_WITH_PROTOCOL_STATE = 101;
+ int RECOVERY_ON_TIMER_EXPIRY = 102;
+ int PROTOCOL_ERROR_UNSPECIFIED = 111;
+ int INTERWORKING_UNSPECIFIED = 127;
+
+ // Others
int CALL_BARRED = 240;
int FDN_BLOCKED = 241;
int IMEI_NOT_ACCEPTED = 243;
diff --git a/com/android/internal/telephony/CarrierActionAgent.java b/com/android/internal/telephony/CarrierActionAgent.java
index 41eebbfe..45824047 100644
--- a/com/android/internal/telephony/CarrierActionAgent.java
+++ b/com/android/internal/telephony/CarrierActionAgent.java
@@ -26,6 +26,7 @@ import android.os.Message;
import android.os.Registrant;
import android.os.RegistrantList;
import android.provider.Settings;
+import android.provider.Telephony;
import android.telephony.Rlog;
import android.telephony.TelephonyManager;
import android.util.LocalLog;
@@ -63,6 +64,7 @@ public class CarrierActionAgent extends Handler {
public static final int EVENT_MOBILE_DATA_SETTINGS_CHANGED = 5;
public static final int EVENT_DATA_ROAMING_OFF = 6;
public static final int EVENT_SIM_STATE_CHANGED = 7;
+ public static final int EVENT_APN_SETTINGS_CHANGED = 8;
/** Member variables */
private final Phone mPhone;
@@ -169,6 +171,8 @@ public class CarrierActionAgent extends Handler {
mSettingsObserver.observe(
Settings.Global.getUriFor(Settings.Global.AIRPLANE_MODE_ON),
EVENT_APM_SETTINGS_CHANGED);
+ mSettingsObserver.observe(
+ Telephony.Carriers.CONTENT_URI, EVENT_APN_SETTINGS_CHANGED);
if (mPhone.getServiceStateTracker() != null) {
mPhone.getServiceStateTracker().registerForDataRoamingOff(
this, EVENT_DATA_ROAMING_OFF, null, false);
@@ -182,6 +186,11 @@ public class CarrierActionAgent extends Handler {
}
}
break;
+ case EVENT_APN_SETTINGS_CHANGED:
+ log("EVENT_APN_SETTINGS_CHANGED");
+ // Reset carrier actions when APN change.
+ carrierActionReset();
+ break;
default:
loge("Unknown carrier action: " + msg.what);
}
diff --git a/com/android/internal/telephony/CarrierIdentifier.java b/com/android/internal/telephony/CarrierIdentifier.java
index 5a700aef..e207e5fd 100644
--- a/com/android/internal/telephony/CarrierIdentifier.java
+++ b/com/android/internal/telephony/CarrierIdentifier.java
@@ -34,6 +34,7 @@ import android.text.TextUtils;
import android.util.LocalLog;
import android.util.Log;
+import com.android.internal.telephony.metrics.TelephonyMetrics;
import com.android.internal.telephony.uicc.IccRecords;
import com.android.internal.telephony.uicc.UiccController;
import com.android.internal.util.IndentingPrintWriter;
@@ -542,6 +543,7 @@ public class CarrierIdentifier extends Handler {
maxRule = rule;
}
}
+
if (maxScore == CarrierMatchingRule.SCORE_INVALID) {
logd("[matchCarrier - no match] cid: " + TelephonyManager.UNKNOWN_CARRIER_ID
+ " name: " + null);
@@ -550,6 +552,29 @@ public class CarrierIdentifier extends Handler {
logd("[matchCarrier] cid: " + maxRule.mCid + " name: " + maxRule.mName);
updateCarrierIdAndName(maxRule.mCid, maxRule.mName);
}
+
+ /*
+ * Write Carrier Identification Matching event, logging with the
+ * carrierId, gid1 and carrier list version to differentiate below cases of metrics:
+ * 1) unknown mccmnc - the Carrier Id provider contains no rule that matches the
+ * read mccmnc.
+ * 2) the Carrier Id provider contains some rule(s) that match the read mccmnc,
+ * but the read gid1 is not matched within the highest-scored rule.
+ * 3) successfully found a matched carrier id in the provider.
+ * 4) use carrier list version to compare the unknown carrier ratio between each version.
+ */
+ String gid1ToLog = ((maxScore & CarrierMatchingRule.SCORE_GID1) == 0
+ && !TextUtils.isEmpty(subscriptionRule.mGid1)) ? subscriptionRule.mGid1 : null;
+ TelephonyMetrics.getInstance().writeCarrierIdMatchingEvent(
+ mPhone.getPhoneId(), getCarrierListVersion(), mCarrierId, gid1ToLog);
+ }
+
+ private int getCarrierListVersion() {
+ final Cursor cursor = mContext.getContentResolver().query(
+ Uri.withAppendedPath(Telephony.CarrierIdentification.CONTENT_URI,
+ "get_version"), null, null, null);
+ cursor.moveToFirst();
+ return cursor.getInt(0);
}
public int getCarrierId() {
@@ -583,6 +608,7 @@ public class CarrierIdentifier extends Handler {
ipw.println("mCarrierId: " + mCarrierId);
ipw.println("mCarrierName: " + mCarrierName);
+ ipw.println("version: " + getCarrierListVersion());
ipw.println("mCarrierMatchingRules on mccmnc: "
+ mTelephonyMgr.getSimOperatorNumericForPhone(mPhone.getPhoneId()));
diff --git a/com/android/internal/telephony/CarrierInfoManager.java b/com/android/internal/telephony/CarrierInfoManager.java
index d224c7df..f6457463 100644
--- a/com/android/internal/telephony/CarrierInfoManager.java
+++ b/com/android/internal/telephony/CarrierInfoManager.java
@@ -19,14 +19,18 @@ package com.android.internal.telephony;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
+import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteConstraintException;
+import android.os.UserHandle;
import android.provider.Telephony;
import android.telephony.ImsiEncryptionInfo;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
+import com.android.internal.telephony.metrics.TelephonyMetrics;
+
import java.util.Date;
/**
@@ -34,6 +38,16 @@ import java.util.Date;
*/
public class CarrierInfoManager {
private static final String LOG_TAG = "CarrierInfoManager";
+ private static final String KEY_TYPE = "KEY_TYPE";
+
+ /*
+ * Rate limit (in milliseconds) the number of times the Carrier keys can be reset.
+ * Do it at most once every 12 hours.
+ */
+ private static final int RESET_CARRIER_KEY_RATE_LIMIT = 12 * 60 * 60 * 1000;
+
+ // Last time the resetCarrierKeysForImsiEncryption API was called successfully.
+ private long mLastAccessResetCarrierKey = 0;
/**
* Returns Carrier specific information that will be used to encrypt the IMSI and IMPI.
@@ -98,9 +112,10 @@ public class CarrierInfoManager {
* @param context Context.
*/
public static void updateOrInsertCarrierKey(ImsiEncryptionInfo imsiEncryptionInfo,
- Context context) {
+ Context context, int phoneId) {
byte[] keyBytes = imsiEncryptionInfo.getPublicKey().getEncoded();
ContentResolver mContentResolver = context.getContentResolver();
+ TelephonyMetrics tm = TelephonyMetrics.getInstance();
// In the current design, MVNOs are not supported. If we decide to support them,
// we'll need to add to this CL.
ContentValues contentValues = new ContentValues();
@@ -113,6 +128,7 @@ public class CarrierInfoManager {
contentValues.put(Telephony.CarrierColumns.PUBLIC_KEY, keyBytes);
contentValues.put(Telephony.CarrierColumns.EXPIRATION_TIME,
imsiEncryptionInfo.getExpirationTime().getTime());
+ boolean downloadSuccessfull = true;
try {
Log.i(LOG_TAG, "Inserting imsiEncryptionInfo into db");
mContentResolver.insert(Telephony.CarrierColumns.CONTENT_URI, contentValues);
@@ -133,12 +149,17 @@ public class CarrierInfoManager {
String.valueOf(imsiEncryptionInfo.getKeyType())});
if (nRows == 0) {
Log.d(LOG_TAG, "Error updating values:" + imsiEncryptionInfo);
+ downloadSuccessfull = false;
}
} catch (Exception ex) {
Log.d(LOG_TAG, "Error updating values:" + imsiEncryptionInfo + ex);
+ downloadSuccessfull = false;
}
} catch (Exception e) {
Log.d(LOG_TAG, "Error inserting/updating values:" + imsiEncryptionInfo + e);
+ downloadSuccessfull = false;
+ } finally {
+ tm.writeCarrierKeyEvent(phoneId, imsiEncryptionInfo.getKeyType(), downloadSuccessfull);
}
}
@@ -153,13 +174,36 @@ public class CarrierInfoManager {
* @param context Context.
*/
public static void setCarrierInfoForImsiEncryption(ImsiEncryptionInfo imsiEncryptionInfo,
- Context context) {
+ Context context, int phoneId) {
Log.i(LOG_TAG, "inserting carrier key: " + imsiEncryptionInfo);
- updateOrInsertCarrierKey(imsiEncryptionInfo, context);
+ updateOrInsertCarrierKey(imsiEncryptionInfo, context, phoneId);
//todo send key to modem. Will be done in a subsequent CL.
}
/**
+ * Resets the Carrier Keys in the database. This involves 2 steps:
+ * 1. Delete the keys from the database.
+ * 2. Send an intent to download new Certificates.
+ * @param context Context
+ * @param mPhoneId phoneId
+ *
+ */
+ public void resetCarrierKeysForImsiEncryption(Context context, int mPhoneId) {
+ Log.i(LOG_TAG, "resetting carrier key");
+ // Check rate limit.
+ long now = System.currentTimeMillis();
+ if (now - mLastAccessResetCarrierKey < RESET_CARRIER_KEY_RATE_LIMIT) {
+ Log.i(LOG_TAG, "resetCarrierKeysForImsiEncryption: Access rate exceeded");
+ return;
+ }
+ mLastAccessResetCarrierKey = now;
+ deleteCarrierInfoForImsiEncryption(context);
+ Intent resetIntent = new Intent(TelephonyIntents.ACTION_CARRIER_CERTIFICATE_DOWNLOAD);
+ resetIntent.putExtra(PhoneConstants.PHONE_KEY, mPhoneId);
+ context.sendBroadcastAsUser(resetIntent, UserHandle.ALL);
+ }
+
+ /**
* Deletes all the keys for a given Carrier from the device keystore.
* @param context Context
*/
@@ -200,4 +244,4 @@ public class CarrierInfoManager {
Log.e(LOG_TAG, "Delete failed" + e);
}
}
-} \ No newline at end of file
+}
diff --git a/com/android/internal/telephony/CarrierKeyDownloadManager.java b/com/android/internal/telephony/CarrierKeyDownloadManager.java
index 66bc5291..5b53bcfd 100644
--- a/com/android/internal/telephony/CarrierKeyDownloadManager.java
+++ b/com/android/internal/telephony/CarrierKeyDownloadManager.java
@@ -57,6 +57,7 @@ import java.security.PublicKey;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Date;
+import java.util.Random;
import java.util.zip.GZIPInputStream;
/**
@@ -70,8 +71,15 @@ public class CarrierKeyDownloadManager {
private static final int DAY_IN_MILLIS = 24 * 3600 * 1000;
- // Start trying to renew the cert X days before it expires.
- private static final int DEFAULT_RENEWAL_WINDOW_DAYS = 7;
+ // Create a window prior to the key expiration, during which the cert will be
+ // downloaded. Defines the start date of that window. So if the key expires on
+ // Dec 21st, the start of the renewal window will be Dec 1st.
+ private static final int START_RENEWAL_WINDOW_DAYS = 21;
+
+ // This will define the end date of the window.
+ private static final int END_RENEWAL_WINDOW_DAYS = 7;
+
+
/* Intent for downloading the public key */
private static final String INTENT_KEY_RENEWAL_ALARM_PREFIX =
@@ -111,6 +119,7 @@ public class CarrierKeyDownloadManager {
filter.addAction(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED);
filter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
filter.addAction(INTENT_KEY_RENEWAL_ALARM_PREFIX + mPhone.getPhoneId());
+ filter.addAction(TelephonyIntents.ACTION_CARRIER_CERTIFICATE_DOWNLOAD);
mContext.registerReceiver(mBroadcastReceiver, filter, null, phone);
mDownloadManager = (DownloadManager) mContext.getSystemService(Context.DOWNLOAD_SERVICE);
}
@@ -123,6 +132,12 @@ public class CarrierKeyDownloadManager {
if (action.equals(INTENT_KEY_RENEWAL_ALARM_PREFIX + slotId)) {
Log.d(LOG_TAG, "Handling key renewal alarm: " + action);
handleAlarmOrConfigChange();
+ } else if (action.equals(TelephonyIntents.ACTION_CARRIER_CERTIFICATE_DOWNLOAD)) {
+ if (slotId == intent.getIntExtra(PhoneConstants.PHONE_KEY,
+ SubscriptionManager.INVALID_SIM_SLOT_INDEX)) {
+ Log.d(LOG_TAG, "Handling reset intent: " + action);
+ handleAlarmOrConfigChange();
+ }
} else if (action.equals(CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED)) {
if (slotId == intent.getIntExtra(PhoneConstants.PHONE_KEY,
SubscriptionManager.INVALID_SIM_SLOT_INDEX)) {
@@ -208,10 +223,16 @@ public class CarrierKeyDownloadManager {
// set the alarm to run in a day. Else, we'll set the alarm to run 7 days prior to
// expiration.
if (minExpirationDate == Long.MAX_VALUE || (minExpirationDate
- < System.currentTimeMillis() + DEFAULT_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS)) {
+ < System.currentTimeMillis() + END_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS)) {
minExpirationDate = System.currentTimeMillis() + DAY_IN_MILLIS;
} else {
- minExpirationDate = minExpirationDate - DEFAULT_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS;
+ // We don't want all the phones to download the certs simultaneously, so
+ // we pick a random time during the download window to avoid this situation.
+ Random random = new Random();
+ int max = START_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS;
+ int min = END_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS;
+ int randomTime = random.nextInt(max - min) + min;
+ minExpirationDate = minExpirationDate - randomTime;
}
return minExpirationDate;
}
@@ -478,7 +499,7 @@ public class CarrierKeyDownloadManager {
}
Date imsiDate = imsiEncryptionInfo.getExpirationTime();
long timeToExpire = imsiDate.getTime() - System.currentTimeMillis();
- return (timeToExpire < DEFAULT_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS) ? true : false;
+ return (timeToExpire < START_RENEWAL_WINDOW_DAYS * DAY_IN_MILLIS) ? true : false;
}
return false;
}
diff --git a/com/android/internal/telephony/CarrierServiceBindHelper.java b/com/android/internal/telephony/CarrierServiceBindHelper.java
index ab010cc3..d2f0a166 100644
--- a/com/android/internal/telephony/CarrierServiceBindHelper.java
+++ b/com/android/internal/telephony/CarrierServiceBindHelper.java
@@ -123,7 +123,7 @@ public class CarrierServiceBindHelper {
if (!SubscriptionManager.isValidPhoneId(phoneId)) {
return;
}
- if (TextUtils.isEmpty(simState)) return;
+ if (TextUtils.isEmpty(simState) || phoneId >= mLastSimState.length) return;
if (simState.equals(mLastSimState[phoneId])) {
// ignore consecutive duplicated events
return;
diff --git a/com/android/internal/telephony/CommandException.java b/com/android/internal/telephony/CommandException.java
index 1d2cc3a7..fe9fa72a 100644
--- a/com/android/internal/telephony/CommandException.java
+++ b/com/android/internal/telephony/CommandException.java
@@ -259,6 +259,8 @@ public class CommandException extends RuntimeException {
return new CommandException(Error.DEVICE_IN_USE);
case RILConstants.ABORTED:
return new CommandException(Error.ABORTED);
+ case RILConstants.INVALID_RESPONSE:
+ return new CommandException(Error.INVALID_RESPONSE);
case RILConstants.OEM_ERROR_1:
return new CommandException(Error.OEM_ERROR_1);
case RILConstants.OEM_ERROR_2:
diff --git a/com/android/internal/telephony/CommandsInterface.java b/com/android/internal/telephony/CommandsInterface.java
index 3731e02e..db44e8cd 100644
--- a/com/android/internal/telephony/CommandsInterface.java
+++ b/com/android/internal/telephony/CommandsInterface.java
@@ -16,6 +16,7 @@
package com.android.internal.telephony;
+import android.net.KeepalivePacketData;
import android.os.Handler;
import android.os.Message;
import android.os.WorkSource;
@@ -2073,7 +2074,7 @@ public interface CommandsInterface {
* Register for unsolicited PCO data. This information is carrier-specific,
* opaque binary blobs destined for carrier apps for interpretation.
*
- * @param h Handler for notificaiton message.
+ * @param h Handler for notification message.
* @param what User-defined message code.
* @param obj User object.
*/
@@ -2133,7 +2134,7 @@ public interface CommandsInterface {
/**
* Register for unsolicited Carrier Public Key.
*
- * @param h Handler for notificaiton message.
+ * @param h Handler for notification message.
* @param what User-defined message code.
* @param obj User object.
*/
@@ -2142,14 +2143,14 @@ public interface CommandsInterface {
/**
* DeRegister for unsolicited Carrier Public Key.
*
- * @param h Handler for notificaiton message.
+ * @param h Handler for notification message.
*/
void unregisterForCarrierInfoForImsiEncryption(Handler h);
/**
* Register for unsolicited Network Scan result.
*
- * @param h Handler for notificaiton message.
+ * @param h Handler for notification message.
* @param what User-defined message code.
* @param obj User object.
*/
@@ -2158,10 +2159,45 @@ public interface CommandsInterface {
/**
* DeRegister for unsolicited Network Scan result.
*
- * @param h Handler for notificaiton message.
+ * @param h Handler for notification message.
*/
void unregisterForNetworkScanResult(Handler h);
+ /**
+ * Register for unsolicited NATT Keepalive Status Indications
+ *
+ * @param h Handler for notification message.
+ * @param what User-defined message code.
+ * @param obj User object.
+ */
+ void registerForNattKeepaliveStatus(Handler h, int what, Object obj);
+
+ /**
+ * Deregister for unsolicited NATT Keepalive Status Indications.
+ *
+ * @param h Handler for notification message.
+ */
+ void unregisterForNattKeepaliveStatus(Handler h);
+
+ /**
+ * Start sending NATT Keepalive packets on a specified data connection
+ *
+ * @param contextId cid that identifies the data connection for this keepalive
+ * @param packetData the keepalive packet data description
+ * @param intervalMillis a time interval in ms between keepalive packet transmissions
+ * @param result a Message to return to the requester
+ */
+ void startNattKeepalive(
+ int contextId, KeepalivePacketData packetData, int intervalMillis, Message result);
+
+ /**
+ * Stop sending NATT Keepalive packets on a specified data connection
+ *
+ * @param sessionHandle the keepalive session handle (from the modem) to stop
+ * @param result a Message to return to the requester
+ */
+ void stopNattKeepalive(int sessionHandle, Message result);
+
default public List<ClientRequestStats> getClientRequestStats() {
return null;
}
diff --git a/com/android/internal/telephony/DefaultPhoneNotifier.java b/com/android/internal/telephony/DefaultPhoneNotifier.java
index 6a4dee7d..368c94df 100644
--- a/com/android/internal/telephony/DefaultPhoneNotifier.java
+++ b/com/android/internal/telephony/DefaultPhoneNotifier.java
@@ -309,6 +309,16 @@ public class DefaultPhoneNotifier implements PhoneNotifier {
}
}
+ @Override
+ public void notifyUserMobileDataStateChanged(Phone sender, boolean state) {
+ try {
+ mRegistry.notifyUserMobileDataStateChangedForPhoneId(
+ sender.getPhoneId(), sender.getSubId(), state);
+ } catch (RemoteException ex) {
+ // system process is dead
+ }
+ }
+
/**
* Convert the {@link Phone.DataActivityState} enum into the TelephonyManager.DATA_* constants
* for the public API.
diff --git a/com/android/internal/telephony/GsmCdmaConnection.java b/com/android/internal/telephony/GsmCdmaConnection.java
index afef78f4..0c8dea2b 100644
--- a/com/android/internal/telephony/GsmCdmaConnection.java
+++ b/com/android/internal/telephony/GsmCdmaConnection.java
@@ -504,6 +504,9 @@ public class GsmCdmaConnection extends Connection {
case CallFailCause.NORMAL_UNSPECIFIED:
return DisconnectCause.NORMAL_UNSPECIFIED;
+ case CallFailCause.USER_ALERTING_NO_ANSWER:
+ return DisconnectCause.TIMED_OUT;
+
case CallFailCause.ERROR_UNSPECIFIED:
case CallFailCause.NORMAL_CLEARING:
default:
diff --git a/com/android/internal/telephony/GsmCdmaPhone.java b/com/android/internal/telephony/GsmCdmaPhone.java
index 27364985..c9583d89 100644
--- a/com/android/internal/telephony/GsmCdmaPhone.java
+++ b/com/android/internal/telephony/GsmCdmaPhone.java
@@ -76,7 +76,7 @@ import com.android.internal.telephony.cdma.EriManager;
import com.android.internal.telephony.gsm.GsmMmiCode;
import com.android.internal.telephony.gsm.SuppServiceNotification;
import com.android.internal.telephony.test.SimulatedRadioControl;
-import com.android.internal.telephony.uicc.IccCardProxy;
+import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
import com.android.internal.telephony.uicc.IccException;
import com.android.internal.telephony.uicc.IccRecords;
import com.android.internal.telephony.uicc.IccVmNotSupportedException;
@@ -87,6 +87,7 @@ import com.android.internal.telephony.uicc.SIMRecords;
import com.android.internal.telephony.uicc.UiccCard;
import com.android.internal.telephony.uicc.UiccCardApplication;
import com.android.internal.telephony.uicc.UiccController;
+import com.android.internal.telephony.uicc.UiccProfile;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -183,13 +184,14 @@ public class GsmCdmaPhone extends Phone {
}
private IccSmsInterfaceManager mIccSmsInterfaceManager;
- private IccCardProxy mIccCardProxy;
private boolean mResetModemOnRadioTechnologyChange = false;
private int mRilVersion;
private boolean mBroadcastEmergencyCallStateChanges = false;
private CarrierKeyDownloadManager mCDM;
+ private CarrierInfoManager mCIM;
+
// Constructors
public GsmCdmaPhone(Context context, CommandsInterface ci, PhoneNotifier notifier, int phoneId,
@@ -242,7 +244,6 @@ public class GsmCdmaPhone extends Phone {
= (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, LOG_TAG);
mIccSmsInterfaceManager = mTelephonyComponentFactory.makeIccSmsInterfaceManager(this);
- mIccCardProxy = mTelephonyComponentFactory.makeIccCardProxy(mContext, mCi, mPhoneId);
mCi.registerForAvailable(this, EVENT_RADIO_AVAILABLE, null);
mCi.registerForOffOrNotAvailable(this, EVENT_RADIO_OFF_OR_NOT_AVAILABLE, null);
@@ -274,6 +275,7 @@ public class GsmCdmaPhone extends Phone {
mContext.registerReceiver(mBroadcastReceiver, new IntentFilter(
CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED));
mCDM = new CarrierKeyDownloadManager(this);
+ mCIM = new CarrierInfoManager();
}
private void initRatSpecific(int precisePhoneType) {
@@ -283,12 +285,16 @@ public class GsmCdmaPhone extends Phone {
mMeid = null;
mPrecisePhoneType = precisePhoneType;
+ logd("Precise phone type " + mPrecisePhoneType);
TelephonyManager tm = TelephonyManager.from(mContext);
+ UiccProfile uiccProfile = getUiccProfile();
if (isPhoneTypeGsm()) {
mCi.setPhoneType(PhoneConstants.PHONE_TYPE_GSM);
tm.setPhoneType(getPhoneId(), PhoneConstants.PHONE_TYPE_GSM);
- mIccCardProxy.setVoiceRadioTech(ServiceState.RIL_RADIO_TECHNOLOGY_UMTS);
+ if (uiccProfile != null) {
+ uiccProfile.setVoiceRadioTech(ServiceState.RIL_RADIO_TECHNOLOGY_UMTS);
+ }
} else {
mCdmaSubscriptionSource = mCdmaSSM.getCdmaSubscriptionSource();
// This is needed to handle phone process crashes
@@ -301,31 +307,30 @@ public class GsmCdmaPhone extends Phone {
mCi.setPhoneType(PhoneConstants.PHONE_TYPE_CDMA);
tm.setPhoneType(getPhoneId(), PhoneConstants.PHONE_TYPE_CDMA);
- mIccCardProxy.setVoiceRadioTech(ServiceState.RIL_RADIO_TECHNOLOGY_1xRTT);
+ if (uiccProfile != null) {
+ uiccProfile.setVoiceRadioTech(ServiceState.RIL_RADIO_TECHNOLOGY_1xRTT);
+ }
// Sets operator properties by retrieving from build-time system property
String operatorAlpha = SystemProperties.get("ro.cdma.home.operator.alpha");
String operatorNumeric = SystemProperties.get(PROPERTY_CDMA_HOME_OPERATOR_NUMERIC);
logd("init: operatorAlpha='" + operatorAlpha
+ "' operatorNumeric='" + operatorNumeric + "'");
- if (mUiccController.getUiccCardApplication(mPhoneId, UiccController.APP_FAM_3GPP) ==
- null || isPhoneTypeCdmaLte()) {
- if (!TextUtils.isEmpty(operatorAlpha)) {
- logd("init: set 'gsm.sim.operator.alpha' to operator='" + operatorAlpha + "'");
- tm.setSimOperatorNameForPhone(mPhoneId, operatorAlpha);
- }
- if (!TextUtils.isEmpty(operatorNumeric)) {
- logd("init: set 'gsm.sim.operator.numeric' to operator='" + operatorNumeric +
- "'");
- logd("update icc_operator_numeric=" + operatorNumeric);
- tm.setSimOperatorNumericForPhone(mPhoneId, operatorNumeric);
-
- SubscriptionController.getInstance().setMccMnc(operatorNumeric, getSubId());
- // Sets iso country property by retrieving from build-time system property
- setIsoCountryProperty(operatorNumeric);
- // Updates MCC MNC device configuration information
- logd("update mccmnc=" + operatorNumeric);
- MccTable.updateMccMncConfiguration(mContext, operatorNumeric, false);
- }
+ if (!TextUtils.isEmpty(operatorAlpha)) {
+ logd("init: set 'gsm.sim.operator.alpha' to operator='" + operatorAlpha + "'");
+ tm.setSimOperatorNameForPhone(mPhoneId, operatorAlpha);
+ }
+ if (!TextUtils.isEmpty(operatorNumeric)) {
+ logd("init: set 'gsm.sim.operator.numeric' to operator='" + operatorNumeric +
+ "'");
+ logd("update icc_operator_numeric=" + operatorNumeric);
+ tm.setSimOperatorNumericForPhone(mPhoneId, operatorNumeric);
+
+ SubscriptionController.getInstance().setMccMnc(operatorNumeric, getSubId());
+ // Sets iso country property by retrieving from build-time system property
+ setIsoCountryProperty(operatorNumeric);
+ // Updates MCC MNC device configuration information
+ logd("update mccmnc=" + operatorNumeric);
+ MccTable.updateMccMncConfiguration(mContext, operatorNumeric, false);
}
// Sets current entry in the telephony carrier table
@@ -678,11 +683,7 @@ public class GsmCdmaPhone extends Phone {
if (getUnitTestMode()) {
return;
}
- if (isPhoneTypeGsm() || isPhoneTypeCdmaLte()) {
- TelephonyManager.setTelephonyProperty(mPhoneId, property, value);
- } else {
- super.setSystemProperty(property, value);
- }
+ TelephonyManager.setTelephonyProperty(mPhoneId, property, value);
}
@Override
@@ -1537,7 +1538,7 @@ public class GsmCdmaPhone extends Phone {
@Override
public void setCarrierInfoForImsiEncryption(ImsiEncryptionInfo imsiEncryptionInfo) {
- CarrierInfoManager.setCarrierInfoForImsiEncryption(imsiEncryptionInfo, mContext);
+ CarrierInfoManager.setCarrierInfoForImsiEncryption(imsiEncryptionInfo, mContext, mPhoneId);
}
@Override
@@ -1551,6 +1552,11 @@ public class GsmCdmaPhone extends Phone {
}
@Override
+ public void resetCarrierKeysForImsiEncryption() {
+ mCIM.resetCarrierKeysForImsiEncryption(mContext, mPhoneId);
+ }
+
+ @Override
public String getGroupIdLevel1() {
if (isPhoneTypeGsm()) {
IccRecords r = mIccRecords.get();
@@ -1681,14 +1687,10 @@ public class GsmCdmaPhone extends Phone {
@Override
public String getSystemProperty(String property, String defValue) {
- if (isPhoneTypeGsm() || isPhoneTypeCdmaLte()) {
- if (getUnitTestMode()) {
- return null;
- }
- return TelephonyManager.getTelephonyProperty(mPhoneId, property, defValue);
- } else {
- return super.getSystemProperty(property, defValue);
+ if (getUnitTestMode()) {
+ return null;
}
+ return TelephonyManager.getTelephonyProperty(mPhoneId, property, defValue);
}
private boolean isValidCommandInterfaceCFAction (int commandInterfaceCFAction) {
@@ -2512,6 +2514,9 @@ public class GsmCdmaPhone extends Phone {
}
}
+ // todo: check if ICC availability needs to be handled here. mSimRecords should not be needed
+ // now because APIs can be called directly on UiccProfile, and that should handle the requests
+ // correctly based on supported apps, voice RAT, etc.
@Override
protected void onUpdateIccAvailability() {
if (mUiccController == null ) {
@@ -2537,7 +2542,7 @@ public class GsmCdmaPhone extends Phone {
if (mSimRecords != null) {
mSimRecords.unregisterForRecordsLoaded(this);
}
- if (isPhoneTypeCdmaLte()) {
+ if (isPhoneTypeCdmaLte() || isPhoneTypeCdma()) {
newUiccApplication = mUiccController.getUiccCardApplication(mPhoneId,
UiccController.APP_FAM_3GPP);
SIMRecords newSimRecords = null;
@@ -2599,28 +2604,24 @@ public class GsmCdmaPhone extends Phone {
*/
@Override
public boolean updateCurrentCarrierInProvider() {
- if (isPhoneTypeGsm() || isPhoneTypeCdmaLte()) {
- long currentDds = SubscriptionManager.getDefaultDataSubscriptionId();
- String operatorNumeric = getOperatorNumeric();
+ long currentDds = SubscriptionManager.getDefaultDataSubscriptionId();
+ String operatorNumeric = getOperatorNumeric();
- logd("updateCurrentCarrierInProvider: mSubId = " + getSubId()
- + " currentDds = " + currentDds + " operatorNumeric = " + operatorNumeric);
+ logd("updateCurrentCarrierInProvider: mSubId = " + getSubId()
+ + " currentDds = " + currentDds + " operatorNumeric = " + operatorNumeric);
- if (!TextUtils.isEmpty(operatorNumeric) && (getSubId() == currentDds)) {
- try {
- Uri uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "current");
- ContentValues map = new ContentValues();
- map.put(Telephony.Carriers.NUMERIC, operatorNumeric);
- mContext.getContentResolver().insert(uri, map);
- return true;
- } catch (SQLException e) {
- Rlog.e(LOG_TAG, "Can't store current operator", e);
- }
+ if (!TextUtils.isEmpty(operatorNumeric) && (getSubId() == currentDds)) {
+ try {
+ Uri uri = Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "current");
+ ContentValues map = new ContentValues();
+ map.put(Telephony.Carriers.NUMERIC, operatorNumeric);
+ mContext.getContentResolver().insert(uri, map);
+ return true;
+ } catch (SQLException e) {
+ Rlog.e(LOG_TAG, "Can't store current operator", e);
}
- return false;
- } else {
- return true;
}
+ return false;
}
//CDMA
@@ -3282,8 +3283,11 @@ public class GsmCdmaPhone extends Phone {
mCi.setRadioPower(oldPowerState, null);
}
- // update voice radio tech in icc card proxy
- mIccCardProxy.setVoiceRadioTech(newVoiceRadioTech);
+ // update voice radio tech in UiccProfile
+ UiccProfile uiccProfile = getUiccProfile();
+ if (uiccProfile != null) {
+ uiccProfile.setVoiceRadioTech(newVoiceRadioTech);
+ }
// Send an Intent to the PhoneApp that we had a radio technology change
Intent intent = new Intent(TelephonyIntents.ACTION_RADIO_TECHNOLOGY_CHANGED);
@@ -3300,7 +3304,13 @@ public class GsmCdmaPhone extends Phone {
+ (ServiceState.isGsm(newVoiceRadioTech) ? "GSM" : "CDMA"));
if (ServiceState.isCdma(newVoiceRadioTech)) {
- switchPhoneType(PhoneConstants.PHONE_TYPE_CDMA_LTE);
+ UiccCardApplication cdmaApplication =
+ mUiccController.getUiccCardApplication(mPhoneId, UiccController.APP_FAM_3GPP2);
+ if (cdmaApplication != null && cdmaApplication.getType() == AppType.APPTYPE_RUIM) {
+ switchPhoneType(PhoneConstants.PHONE_TYPE_CDMA);
+ } else {
+ switchPhoneType(PhoneConstants.PHONE_TYPE_CDMA_LTE);
+ }
} else if (ServiceState.isGsm(newVoiceRadioTech)) {
switchPhoneType(PhoneConstants.PHONE_TYPE_GSM);
} else {
@@ -3328,12 +3338,17 @@ public class GsmCdmaPhone extends Phone {
@Override
public boolean getIccRecordsLoaded() {
- return mIccCardProxy.getIccRecordsLoaded();
+ UiccProfile uiccProfile = getUiccProfile();
+ return uiccProfile != null && uiccProfile.getIccRecordsLoaded();
}
@Override
public IccCard getIccCard() {
- return mIccCardProxy;
+ return UiccController.getInstance().getUiccProfileForPhone(mPhoneId);
+ }
+
+ private UiccProfile getUiccProfile() {
+ return UiccController.getInstance().getUiccProfileForPhone(mPhoneId);
}
@Override
@@ -3365,14 +3380,6 @@ public class GsmCdmaPhone extends Phone {
pw.println(" isCspPlmnEnabled()=" + isCspPlmnEnabled());
pw.flush();
pw.println("++++++++++++++++++++++++++++++++");
-
- try {
- mIccCardProxy.dump(fd, pw, args);
- } catch (Exception e) {
- e.printStackTrace();
- }
- pw.flush();
- pw.println("++++++++++++++++++++++++++++++++");
pw.println("DeviceStateMonitor:");
mDeviceStateMonitor.dump(fd, pw, args);
pw.println("++++++++++++++++++++++++++++++++");
@@ -3420,8 +3427,16 @@ public class GsmCdmaPhone extends Phone {
if (mCdmaSubscriptionSource == CDMA_SUBSCRIPTION_NV) {
operatorNumeric = SystemProperties.get("ro.cdma.home.operator.numeric");
} else if (mCdmaSubscriptionSource == CDMA_SUBSCRIPTION_RUIM_SIM) {
- curIccRecords = mSimRecords;
- if (curIccRecords != null) {
+ UiccCardApplication uiccCardApplication = mUiccApplication.get();
+ if (uiccCardApplication != null
+ && uiccCardApplication.getType() == AppType.APPTYPE_RUIM) {
+ logd("Legacy RUIM app present");
+ curIccRecords = mIccRecords.get();
+ } else {
+ // Use sim-records for SimApp, USimApp, CSimApp and ISimApp.
+ curIccRecords = mSimRecords;
+ }
+ if (curIccRecords != null && curIccRecords == mSimRecords) {
operatorNumeric = curIccRecords.getOperatorNumeric();
} else {
curIccRecords = mIccRecords.get();
@@ -3534,4 +3549,22 @@ public class GsmCdmaPhone extends Phone {
return mWakeLock;
}
+ @Override
+ public int getLteOnCdmaMode() {
+ int currentConfig = super.getLteOnCdmaMode();
+ int lteOnCdmaModeDynamicValue = currentConfig;
+
+ UiccCardApplication cdmaApplication =
+ mUiccController.getUiccCardApplication(mPhoneId, UiccController.APP_FAM_3GPP2);
+ if (cdmaApplication != null && cdmaApplication.getType() == AppType.APPTYPE_RUIM) {
+ //Legacy RUIM cards don't support LTE.
+ lteOnCdmaModeDynamicValue = RILConstants.LTE_ON_CDMA_FALSE;
+
+ //Override only if static configuration is TRUE.
+ if (currentConfig == RILConstants.LTE_ON_CDMA_TRUE) {
+ return lteOnCdmaModeDynamicValue;
+ }
+ }
+ return currentConfig;
+ }
}
diff --git a/com/android/internal/telephony/IccCard.java b/com/android/internal/telephony/IccCard.java
index 272a1a61..7e98bc9e 100644
--- a/com/android/internal/telephony/IccCard.java
+++ b/com/android/internal/telephony/IccCard.java
@@ -21,7 +21,6 @@ import android.os.Message;
import com.android.internal.telephony.IccCardConstants.State;
import com.android.internal.telephony.uicc.IccCardApplicationStatus;
-import com.android.internal.telephony.uicc.IccFileHandler;
import com.android.internal.telephony.uicc.IccRecords;
/**
@@ -44,17 +43,13 @@ public interface IccCard {
*/
public State getState();
+ // todo: delete
/**
* @return IccRecords object belonging to current UiccCardApplication
*/
public IccRecords getIccRecords();
/**
- * @return IccFileHandler object belonging to current UiccCardApplication
- */
- public IccFileHandler getIccFileHandler();
-
- /**
* Notifies handler of any transition into IccCardConstants.State.NETWORK_LOCKED
*/
public void registerForNetworkLocked(Handler h, int what, Object obj);
@@ -96,13 +91,6 @@ public interface IccCard {
public void supplyPuk2 (String puk2, String newPin2, Message onComplete);
/**
- * Check whether fdn (fixed dialing number) service is available.
- * @return true if ICC fdn service available
- * false if ICC fdn service not available
- */
- public boolean getIccFdnAvailable();
-
- /**
* Supply Network depersonalization code to the RIL
*/
public void supplyNetworkDepersonalization (String pin, Message onComplete);
diff --git a/com/android/internal/telephony/IccCardConstants.java b/com/android/internal/telephony/IccCardConstants.java
index f3d9335f..d57f9afa 100644
--- a/com/android/internal/telephony/IccCardConstants.java
+++ b/com/android/internal/telephony/IccCardConstants.java
@@ -30,16 +30,14 @@ public class IccCardConstants {
public static final String INTENT_VALUE_ICC_NOT_READY = "NOT_READY";
/* ABSENT means ICC is missing */
public static final String INTENT_VALUE_ICC_ABSENT = "ABSENT";
+ /* PRESENT means ICC is present */
+ public static final String INTENT_VALUE_ICC_PRESENT = "PRESENT";
/* CARD_IO_ERROR means for three consecutive times there was SIM IO error */
static public final String INTENT_VALUE_ICC_CARD_IO_ERROR = "CARD_IO_ERROR";
/* CARD_RESTRICTED means card is present but not usable due to carrier restrictions */
static public final String INTENT_VALUE_ICC_CARD_RESTRICTED = "CARD_RESTRICTED";
/* LOCKED means ICC is locked by pin or by network */
public static final String INTENT_VALUE_ICC_LOCKED = "LOCKED";
- //TODO: we can remove this state in the future if Bug 18489776 analysis
- //#42's first race condition is resolved
- /* INTERNAL LOCKED means ICC is locked by pin or by network */
- public static final String INTENT_VALUE_ICC_INTERNAL_LOCKED = "INTERNAL_LOCKED";
/* READY means ICC is ready to access */
public static final String INTENT_VALUE_ICC_READY = "READY";
/* IMSI means ICC IMSI is ready in property */
@@ -77,7 +75,8 @@ public class IccCardConstants {
NOT_READY, /** ordinal(6) == {@See TelephonyManager#SIM_STATE_NOT_READY} */
PERM_DISABLED, /** ordinal(7) == {@See TelephonyManager#SIM_STATE_PERM_DISABLED} */
CARD_IO_ERROR, /** ordinal(8) == {@See TelephonyManager#SIM_STATE_CARD_IO_ERROR} */
- CARD_RESTRICTED;/** ordinal(9) == {@See TelephonyManager#SIM_STATE_CARD_RESTRICTED} */
+ CARD_RESTRICTED,/** ordinal(9) == {@See TelephonyManager#SIM_STATE_CARD_RESTRICTED} */
+ LOADED; /** ordinal(9) == {@See TelephonyManager#SIM_STATE_LOADED} */
public boolean isPinLocked() {
return ((this == PIN_REQUIRED) || (this == PUK_REQUIRED));
@@ -85,9 +84,9 @@ public class IccCardConstants {
public boolean iccCardExist() {
return ((this == PIN_REQUIRED) || (this == PUK_REQUIRED)
- || (this == NETWORK_LOCKED) || (this == READY)
+ || (this == NETWORK_LOCKED) || (this == READY) || (this == NOT_READY)
|| (this == PERM_DISABLED) || (this == CARD_IO_ERROR)
- || (this == CARD_RESTRICTED));
+ || (this == CARD_RESTRICTED) || (this == LOADED));
}
public static State intToState(int state) throws IllegalArgumentException {
@@ -102,6 +101,7 @@ public class IccCardConstants {
case 7: return PERM_DISABLED;
case 8: return CARD_IO_ERROR;
case 9: return CARD_RESTRICTED;
+ case 10: return LOADED;
default:
throw new IllegalArgumentException();
}
diff --git a/com/android/internal/telephony/IccSmsInterfaceManager.java b/com/android/internal/telephony/IccSmsInterfaceManager.java
index 0fc08c65..c3ae1b06 100644
--- a/com/android/internal/telephony/IccSmsInterfaceManager.java
+++ b/com/android/internal/telephony/IccSmsInterfaceManager.java
@@ -83,7 +83,7 @@ public class IccSmsInterfaceManager {
final protected Context mContext;
final protected AppOpsManager mAppOps;
final private UserManager mUserManager;
- protected SMSDispatcher mDispatcher;
+ protected SmsDispatchersController mDispatchersController;
protected Handler mHandler = new Handler() {
@Override
@@ -131,7 +131,7 @@ public class IccSmsInterfaceManager {
mContext = phone.getContext();
mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
- mDispatcher = new ImsSMSDispatcher(phone,
+ mDispatchersController = new SmsDispatchersController(phone,
phone.mSmsStorageMonitor, phone.mSmsUsageMonitor);
}
@@ -170,7 +170,7 @@ public class IccSmsInterfaceManager {
protected void updatePhoneObject(Phone phone) {
mPhone = phone;
- mDispatcher.updatePhoneObject(phone);
+ mDispatchersController.updatePhoneObject(phone);
}
protected void enforceReceiveAndSend(String message) {
@@ -379,7 +379,8 @@ public class IccSmsInterfaceManager {
return;
}
destAddr = filterDestAddress(destAddr);
- mDispatcher.sendData(destAddr, scAddr, destPort, data, sentIntent, deliveryIntent);
+ mDispatchersController.sendData(destAddr, scAddr, destPort, data, sentIntent,
+ deliveryIntent);
}
/**
@@ -451,7 +452,7 @@ public class IccSmsInterfaceManager {
enforcePrivilegedAppPermissions();
}
destAddr = filterDestAddress(destAddr);
- mDispatcher.sendText(destAddr, scAddr, text, sentIntent, deliveryIntent,
+ mDispatchersController.sendText(destAddr, scAddr, text, sentIntent, deliveryIntent,
null/*messageUri*/, callingPackage, persistMessageForNonDefaultSmsApp);
}
@@ -472,7 +473,17 @@ public class IccSmsInterfaceManager {
"\n format=" + format +
"\n receivedIntent=" + receivedIntent);
}
- mDispatcher.injectSmsPdu(pdu, format, receivedIntent);
+ mDispatchersController.injectSmsPdu(pdu, format,
+ result -> {
+ if (receivedIntent != null) {
+ try {
+ receivedIntent.send(result);
+ } catch (PendingIntent.CanceledException e) {
+ Rlog.d(LOG_TAG, "receivedIntent cancelled.");
+ }
+ }
+ }
+ );
}
/**
@@ -546,7 +557,7 @@ public class IccSmsInterfaceManager {
singleDeliveryIntent = deliveryIntents.get(i);
}
- mDispatcher.sendText(destAddr, scAddr, singlePart,
+ mDispatchersController.sendText(destAddr, scAddr, singlePart,
singleSentIntent, singleDeliveryIntent,
null/*messageUri*/, callingPackage,
persistMessageForNonDefaultSmsApp);
@@ -554,19 +565,19 @@ public class IccSmsInterfaceManager {
return;
}
- mDispatcher.sendMultipartText(destAddr, scAddr, (ArrayList<String>) parts,
+ mDispatchersController.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);
+ return mDispatchersController.getPremiumSmsPermission(packageName);
}
public void setPremiumSmsPermission(String packageName, int permission) {
- mDispatcher.setPremiumSmsPermission(packageName, permission);
+ mDispatchersController.setPremiumSmsPermission(packageName, permission);
}
/**
@@ -919,11 +930,11 @@ public class IccSmsInterfaceManager {
}
public boolean isImsSmsSupported() {
- return mDispatcher.isIms();
+ return mDispatchersController.isIms();
}
public String getImsSmsFormat() {
- return mDispatcher.getImsSmsFormat();
+ return mDispatchersController.getImsSmsFormat();
}
public void sendStoredText(String callingPkg, Uri messageUri, String scAddress,
@@ -951,7 +962,7 @@ public class IccSmsInterfaceManager {
return;
}
textAndAddress[1] = filterDestAddress(textAndAddress[1]);
- mDispatcher.sendText(textAndAddress[1], scAddress, textAndAddress[0],
+ mDispatchersController.sendText(textAndAddress[1], scAddress, textAndAddress[0],
sentIntent, deliveryIntent, messageUri, callingPkg,
true /* persistMessageForNonDefaultSmsApp */);
}
@@ -1007,14 +1018,14 @@ public class IccSmsInterfaceManager {
singleDeliveryIntent = deliveryIntents.get(i);
}
- mDispatcher.sendText(textAndAddress[1], scAddress, singlePart,
+ mDispatchersController.sendText(textAndAddress[1], scAddress, singlePart,
singleSentIntent, singleDeliveryIntent, messageUri, callingPkg,
true /* persistMessageForNonDefaultSmsApp */);
}
return;
}
- mDispatcher.sendMultipartText(
+ mDispatchersController.sendMultipartText(
textAndAddress[1], // destAddress
scAddress,
parts,
diff --git a/com/android/internal/telephony/ImsSMSDispatcher.java b/com/android/internal/telephony/ImsSMSDispatcher.java
deleted file mode 100644
index 4d8f62c9..00000000
--- a/com/android/internal/telephony/ImsSMSDispatcher.java
+++ /dev/null
@@ -1,408 +0,0 @@
-/*
- * Copyright (C) 2006 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES 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 static android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE;
-import android.app.PendingIntent;
-import android.app.PendingIntent.CanceledException;
-import android.net.Uri;
-import android.os.AsyncResult;
-import android.os.Message;
-import android.provider.Telephony.Sms.Intents;
-import android.telephony.Rlog;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.telephony.cdma.CdmaInboundSmsHandler;
-import com.android.internal.telephony.cdma.CdmaSMSDispatcher;
-import com.android.internal.telephony.gsm.GsmInboundSmsHandler;
-import com.android.internal.telephony.gsm.GsmSMSDispatcher;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-
-public class ImsSMSDispatcher extends SMSDispatcher {
- private static final String TAG = "RIL_ImsSms";
-
- private SMSDispatcher mCdmaDispatcher;
- private SMSDispatcher mGsmDispatcher;
-
- private GsmInboundSmsHandler mGsmInboundSmsHandler;
- private CdmaInboundSmsHandler mCdmaInboundSmsHandler;
-
-
- /** true if IMS is registered and sms is supported, false otherwise.*/
- private boolean mIms = false;
- private String mImsSmsFormat = SmsConstants.FORMAT_UNKNOWN;
-
- public ImsSMSDispatcher(Phone phone, SmsStorageMonitor storageMonitor,
- SmsUsageMonitor usageMonitor) {
- super(phone, usageMonitor, null);
- Rlog.d(TAG, "ImsSMSDispatcher created");
-
- // Create dispatchers, inbound SMS handlers and
- // broadcast undelivered messages in raw table.
- mCdmaDispatcher = new CdmaSMSDispatcher(phone, usageMonitor, this);
- mGsmInboundSmsHandler = GsmInboundSmsHandler.makeInboundSmsHandler(phone.getContext(),
- storageMonitor, phone);
- mCdmaInboundSmsHandler = CdmaInboundSmsHandler.makeInboundSmsHandler(phone.getContext(),
- storageMonitor, phone, (CdmaSMSDispatcher) mCdmaDispatcher);
- mGsmDispatcher = new GsmSMSDispatcher(phone, usageMonitor, this, mGsmInboundSmsHandler);
- SmsBroadcastUndelivered.initialize(phone.getContext(),
- mGsmInboundSmsHandler, mCdmaInboundSmsHandler);
- InboundSmsHandler.registerNewMessageNotificationActionHandler(phone.getContext());
-
- mCi.registerForOn(this, EVENT_RADIO_ON, null);
- mCi.registerForImsNetworkStateChanged(this, EVENT_IMS_STATE_CHANGED, null);
- }
-
- /* Updates the phone object when there is a change */
- @Override
- protected void updatePhoneObject(Phone phone) {
- Rlog.d(TAG, "In IMS updatePhoneObject ");
- super.updatePhoneObject(phone);
- mCdmaDispatcher.updatePhoneObject(phone);
- mGsmDispatcher.updatePhoneObject(phone);
- mGsmInboundSmsHandler.updatePhoneObject(phone);
- mCdmaInboundSmsHandler.updatePhoneObject(phone);
- }
-
- public void dispose() {
- mCi.unregisterForOn(this);
- mCi.unregisterForImsNetworkStateChanged(this);
- mGsmDispatcher.dispose();
- mCdmaDispatcher.dispose();
- mGsmInboundSmsHandler.dispose();
- mCdmaInboundSmsHandler.dispose();
- }
-
- /**
- * Handles events coming from the phone stack. Overridden from handler.
- *
- * @param msg the message to handle
- */
- @Override
- public void handleMessage(Message msg) {
- AsyncResult ar;
-
- switch (msg.what) {
- case EVENT_RADIO_ON:
- case EVENT_IMS_STATE_CHANGED: // received unsol
- mCi.getImsRegistrationState(this.obtainMessage(EVENT_IMS_STATE_DONE));
- break;
-
- case EVENT_IMS_STATE_DONE:
- ar = (AsyncResult) msg.obj;
-
- if (ar.exception == null) {
- updateImsInfo(ar);
- } else {
- Rlog.e(TAG, "IMS State query failed with exp "
- + ar.exception);
- }
- break;
-
- default:
- super.handleMessage(msg);
- }
- }
-
- private void setImsSmsFormat(int format) {
- // valid format?
- switch (format) {
- case PhoneConstants.PHONE_TYPE_GSM:
- mImsSmsFormat = "3gpp";
- break;
- case PhoneConstants.PHONE_TYPE_CDMA:
- mImsSmsFormat = "3gpp2";
- break;
- default:
- mImsSmsFormat = "unknown";
- break;
- }
- }
-
- private void updateImsInfo(AsyncResult ar) {
- int[] responseArray = (int[])ar.result;
-
- mIms = false;
- if (responseArray[0] == 1) { // IMS is registered
- Rlog.d(TAG, "IMS is registered!");
- mIms = true;
- } else {
- Rlog.d(TAG, "IMS is NOT registered!");
- }
-
- setImsSmsFormat(responseArray[1]);
-
- if (("unknown".equals(mImsSmsFormat))) {
- Rlog.e(TAG, "IMS format was unknown!");
- // failed to retrieve valid IMS SMS format info, set IMS to unregistered
- mIms = false;
- }
- }
-
- @Override
- public void sendData(String destAddr, String scAddr, int destPort,
- byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
- if (isCdmaMo()) {
- mCdmaDispatcher.sendData(destAddr, scAddr, destPort,
- data, sentIntent, deliveryIntent);
- } else {
- mGsmDispatcher.sendData(destAddr, scAddr, destPort,
- data, sentIntent, deliveryIntent);
- }
- }
-
- @Override
- public void sendMultipartText(String destAddr, String scAddr,
- ArrayList<String> parts, ArrayList<PendingIntent> sentIntents,
- ArrayList<PendingIntent> deliveryIntents, Uri messageUri, String callingPkg,
- boolean persistMessage) {
- if (isCdmaMo()) {
- mCdmaDispatcher.sendMultipartText(destAddr, scAddr,
- parts, sentIntents, deliveryIntents, messageUri, callingPkg, persistMessage);
- } else {
- mGsmDispatcher.sendMultipartText(destAddr, scAddr,
- parts, sentIntents, deliveryIntents, messageUri, callingPkg, persistMessage);
- }
- }
-
- @Override
- protected void sendSms(SmsTracker tracker) {
- // sendSms is a helper function to other send functions, sendText/Data...
- // it is not part of ISms.stub
- Rlog.e(TAG, "sendSms should never be called from here!");
- }
-
- @Override
- protected void sendSmsByPstn(SmsTracker tracker) {
- // This function should be defined in Gsm/CdmaDispatcher.
- Rlog.e(TAG, "sendSmsByPstn should never be called from here!");
- }
-
- @Override
- public void sendText(String destAddr, String scAddr, String text, PendingIntent sentIntent,
- PendingIntent deliveryIntent, Uri messageUri, String callingPkg,
- boolean persistMessage) {
- Rlog.d(TAG, "sendText");
- if (isCdmaMo()) {
- mCdmaDispatcher.sendText(destAddr, scAddr,
- text, sentIntent, deliveryIntent, messageUri, callingPkg, persistMessage);
- } else {
- mGsmDispatcher.sendText(destAddr, scAddr,
- text, sentIntent, deliveryIntent, messageUri, callingPkg, persistMessage);
- }
- }
-
- @VisibleForTesting
- @Override
- public void injectSmsPdu(byte[] pdu, String format, PendingIntent receivedIntent) {
- Rlog.d(TAG, "ImsSMSDispatcher:injectSmsPdu");
- try {
- // TODO We need to decide whether we should allow injecting GSM(3gpp)
- // SMS pdus when the phone is camping on CDMA(3gpp2) network and vice versa.
- android.telephony.SmsMessage msg =
- android.telephony.SmsMessage.createFromPdu(pdu, format);
-
- // Only class 1 SMS are allowed to be injected.
- if (msg == null ||
- msg.getMessageClass() != android.telephony.SmsMessage.MessageClass.CLASS_1) {
- if (msg == null) {
- Rlog.e(TAG, "injectSmsPdu: createFromPdu returned null");
- }
- if (receivedIntent != null) {
- receivedIntent.send(Intents.RESULT_SMS_GENERIC_ERROR);
- }
- return;
- }
-
- AsyncResult ar = new AsyncResult(receivedIntent, msg, null);
-
- if (format.equals(SmsConstants.FORMAT_3GPP)) {
- Rlog.i(TAG, "ImsSMSDispatcher:injectSmsText Sending msg=" + msg +
- ", format=" + format + "to mGsmInboundSmsHandler");
- mGsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_INJECT_SMS, ar);
- } else if (format.equals(SmsConstants.FORMAT_3GPP2)) {
- Rlog.i(TAG, "ImsSMSDispatcher:injectSmsText Sending msg=" + msg +
- ", format=" + format + "to mCdmaInboundSmsHandler");
- mCdmaInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_INJECT_SMS, ar);
- } else {
- // Invalid pdu format.
- Rlog.e(TAG, "Invalid pdu format: " + format);
- if (receivedIntent != null)
- receivedIntent.send(Intents.RESULT_SMS_GENERIC_ERROR);
- }
- } catch (Exception e) {
- Rlog.e(TAG, "injectSmsPdu failed: ", e);
- try {
- if (receivedIntent != null)
- receivedIntent.send(Intents.RESULT_SMS_GENERIC_ERROR);
- } catch (CanceledException ex) {}
- }
- }
-
- @Override
- public void sendRetrySms(SmsTracker tracker) {
- String oldFormat = tracker.mFormat;
-
- // newFormat will be based on voice technology
- String newFormat =
- (PhoneConstants.PHONE_TYPE_CDMA == mPhone.getPhoneType()) ?
- mCdmaDispatcher.getFormat() :
- mGsmDispatcher.getFormat();
-
- // was previously sent sms format match with voice tech?
- if (oldFormat.equals(newFormat)) {
- if (isCdmaFormat(newFormat)) {
- Rlog.d(TAG, "old format matched new format (cdma)");
- mCdmaDispatcher.sendSms(tracker);
- return;
- } else {
- Rlog.d(TAG, "old format matched new format (gsm)");
- mGsmDispatcher.sendSms(tracker);
- return;
- }
- }
-
- // format didn't match, need to re-encode.
- HashMap map = tracker.getData();
-
- // to re-encode, fields needed are: scAddr, destAddr, and
- // text if originally sent as sendText or
- // data and destPort if originally sent as sendData.
- if (!( map.containsKey("scAddr") && map.containsKey("destAddr") &&
- ( map.containsKey("text") ||
- (map.containsKey("data") && map.containsKey("destPort"))))) {
- // should never come here...
- Rlog.e(TAG, "sendRetrySms failed to re-encode per missing fields!");
- tracker.onFailed(mContext, RESULT_ERROR_GENERIC_FAILURE, 0/*errorCode*/);
- return;
- }
- String scAddr = (String)map.get("scAddr");
- String destAddr = (String)map.get("destAddr");
-
- SmsMessageBase.SubmitPduBase pdu = null;
- // figure out from tracker if this was sendText/Data
- if (map.containsKey("text")) {
- Rlog.d(TAG, "sms failed was text");
- String text = (String)map.get("text");
-
- if (isCdmaFormat(newFormat)) {
- Rlog.d(TAG, "old format (gsm) ==> new format (cdma)");
- pdu = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(
- scAddr, destAddr, text, (tracker.mDeliveryIntent != null), null);
- } else {
- Rlog.d(TAG, "old format (cdma) ==> new format (gsm)");
- pdu = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(
- scAddr, destAddr, text, (tracker.mDeliveryIntent != null), null);
- }
- } else if (map.containsKey("data")) {
- Rlog.d(TAG, "sms failed was data");
- byte[] data = (byte[])map.get("data");
- Integer destPort = (Integer)map.get("destPort");
-
- if (isCdmaFormat(newFormat)) {
- Rlog.d(TAG, "old format (gsm) ==> new format (cdma)");
- pdu = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(
- scAddr, destAddr, destPort.intValue(), data,
- (tracker.mDeliveryIntent != null));
- } else {
- Rlog.d(TAG, "old format (cdma) ==> new format (gsm)");
- pdu = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(
- scAddr, destAddr, destPort.intValue(), data,
- (tracker.mDeliveryIntent != null));
- }
- }
-
- // replace old smsc and pdu with newly encoded ones
- map.put("smsc", pdu.encodedScAddress);
- map.put("pdu", pdu.encodedMessage);
-
- SMSDispatcher dispatcher = (isCdmaFormat(newFormat)) ?
- mCdmaDispatcher : mGsmDispatcher;
-
- tracker.mFormat = dispatcher.getFormat();
- dispatcher.sendSms(tracker);
- }
-
- @Override
- protected void sendSubmitPdu(SmsTracker tracker) {
- sendRawPdu(tracker);
- }
-
- @Override
- protected String getFormat() {
- // this function should be defined in Gsm/CdmaDispatcher.
- Rlog.e(TAG, "getFormat should never be called from here!");
- return "unknown";
- }
-
- @Override
- protected GsmAlphabet.TextEncodingDetails calculateLength(
- CharSequence messageBody, boolean use7bitOnly) {
- Rlog.e(TAG, "Error! Not implemented for IMS.");
- return null;
- }
-
- @Override
- protected SmsTracker getNewSubmitPduTracker(String destinationAddress, String scAddress,
- String message, SmsHeader smsHeader, int format, PendingIntent sentIntent,
- PendingIntent deliveryIntent, boolean lastPart,
- AtomicInteger unsentPartCount, AtomicBoolean anyPartFailed, Uri messageUri,
- String fullMessageText) {
- Rlog.e(TAG, "Error! Not implemented for IMS.");
- return null;
- }
-
- @Override
- public boolean isIms() {
- return mIms;
- }
-
- @Override
- public String getImsSmsFormat() {
- return mImsSmsFormat;
- }
-
- /**
- * Determines whether or not to use CDMA format for MO SMS.
- * If SMS over IMS is supported, then format is based on IMS SMS format,
- * otherwise format is based on current phone type.
- *
- * @return true if Cdma format should be used for MO SMS, false otherwise.
- */
- private boolean isCdmaMo() {
- if (!isIms()) {
- // IMS is not registered, use Voice technology to determine SMS format.
- return (PhoneConstants.PHONE_TYPE_CDMA == mPhone.getPhoneType());
- }
- // IMS is registered with SMS support
- return isCdmaFormat(mImsSmsFormat);
- }
-
- /**
- * Determines whether or not format given is CDMA format.
- *
- * @param format
- * @return true if format given is CDMA format, false otherwise.
- */
- private boolean isCdmaFormat(String format) {
- return (mCdmaDispatcher.getFormat().equals(format));
- }
-}
diff --git a/com/android/internal/telephony/ImsSmsDispatcher.java b/com/android/internal/telephony/ImsSmsDispatcher.java
new file mode 100644
index 00000000..191acb88
--- /dev/null
+++ b/com/android/internal/telephony/ImsSmsDispatcher.java
@@ -0,0 +1,395 @@
+/*
+ * 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 com.android.internal.telephony;
+
+import android.app.Activity;
+import android.os.RemoteException;
+import android.os.Message;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.provider.Telephony.Sms;
+import android.content.Intent;
+import android.telephony.Rlog;
+
+import com.android.ims.ImsException;
+import com.android.ims.ImsManager;
+import com.android.ims.ImsServiceProxy;
+import com.android.ims.internal.IImsSmsListener;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
+import com.android.internal.telephony.util.SMSDispatcherUtil;
+import com.android.internal.telephony.gsm.SmsMessage;
+
+import android.telephony.ims.internal.feature.ImsFeature;
+import android.telephony.ims.internal.feature.MmTelFeature;
+import android.telephony.ims.internal.stub.SmsImplBase;
+import android.telephony.ims.internal.stub.SmsImplBase.SendStatusResult;
+import android.telephony.ims.internal.stub.SmsImplBase.StatusReportResult;
+import android.telephony.ims.stub.ImsRegistrationImplBase;
+import android.provider.Telephony.Sms.Intents;
+import android.util.Pair;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Responsible for communications with {@link com.android.ims.ImsManager} to send/receive messages
+ * over IMS.
+ */
+public class ImsSmsDispatcher extends SMSDispatcher {
+ // Initial condition for ims connection retry.
+ private static final int IMS_RETRY_STARTING_TIMEOUT_MS = 500; // ms
+ // Ceiling bitshift amount for service query timeout, calculated as:
+ // 2^mImsServiceRetryCount * IMS_RETRY_STARTING_TIMEOUT_MS, where
+ // mImsServiceRetryCount ∊ [0, CEILING_SERVICE_RETRY_COUNT].
+ private static final int CEILING_SERVICE_RETRY_COUNT = 6;
+
+ @VisibleForTesting
+ public Map<Integer, SmsTracker> mTrackers = new ConcurrentHashMap<>();
+ @VisibleForTesting
+ public AtomicInteger mNextToken = new AtomicInteger();
+ private final Object mLock = new Object();
+ private volatile boolean mIsSmsCapable;
+ private volatile boolean mIsImsServiceUp;
+ private volatile boolean mIsRegistered;
+ private volatile int mImsServiceRetryCount;
+
+ /**
+ * Default implementation of interface that calculates the ImsService retry timeout.
+ * Override-able for testing.
+ */
+ private IRetryTimeout mRetryTimeout = () -> {
+ int timeout = (1 << mImsServiceRetryCount) * IMS_RETRY_STARTING_TIMEOUT_MS;
+ if (mImsServiceRetryCount <= CEILING_SERVICE_RETRY_COUNT) {
+ mImsServiceRetryCount++;
+ }
+ return timeout;
+ };
+
+ /**
+ * Listen to the IMS service state change
+ *
+ */
+ private ImsRegistrationImplBase.Callback mRegistrationCallback =
+ new ImsRegistrationImplBase.Callback() {
+ @Override
+ public void onRegistered(
+ @ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech) {
+ Rlog.d(TAG, "onImsConnected imsRadioTech=" + imsRadioTech);
+ synchronized (mLock) {
+ mIsRegistered = true;
+ }
+ }
+
+ @Override
+ public void onRegistering(
+ @ImsRegistrationImplBase.ImsRegistrationTech int imsRadioTech) {
+ Rlog.d(TAG, "onImsProgressing imsRadioTech=" + imsRadioTech);
+ synchronized (mLock) {
+ mIsRegistered = false;
+ }
+ }
+
+ @Override
+ public void onDeregistered(com.android.ims.ImsReasonInfo info) {
+ Rlog.d(TAG, "onImsDisconnected imsReasonInfo=" + info);
+ synchronized (mLock) {
+ mIsRegistered = false;
+ }
+ }
+ };
+
+ private ImsFeature.CapabilityCallback mCapabilityCallback =
+ new ImsFeature.CapabilityCallback() {
+ @Override
+ public void onCapabilitiesStatusChanged(ImsFeature.Capabilities config) {
+ synchronized (mLock) {
+ mIsSmsCapable = config.isCapable(
+ MmTelFeature.MmTelCapabilities.CAPABILITY_TYPE_SMS);
+ }
+ }
+ };
+
+ // Callback fires when ImsManager MMTel Feature changes state
+ private ImsServiceProxy.IFeatureUpdate mNotifyStatusChangedCallback =
+ new ImsServiceProxy.IFeatureUpdate() {
+ @Override
+ public void notifyStateChanged() {
+ try {
+ int status = getImsManager().getImsServiceStatus();
+ Rlog.d(TAG, "Status Changed: " + status);
+ switch (status) {
+ case android.telephony.ims.feature.ImsFeature.STATE_READY: {
+ synchronized (mLock) {
+ setListeners();
+ mIsImsServiceUp = true;
+ }
+ break;
+ }
+ case android.telephony.ims.feature.ImsFeature.STATE_INITIALIZING:
+ // fall through
+ case android.telephony.ims.feature.ImsFeature.STATE_NOT_AVAILABLE:
+ synchronized (mLock) {
+ mIsImsServiceUp = false;
+ }
+ break;
+ default: {
+ Rlog.w(TAG, "Unexpected State!");
+ }
+ }
+ } catch (ImsException e) {
+ // Could not get the ImsService, retry!
+ retryGetImsService();
+ }
+ }
+
+ @Override
+ public void notifyUnavailable() {
+ retryGetImsService();
+ }
+ };
+
+ private final IImsSmsListener mImsSmsListener = new IImsSmsListener.Stub() {
+ @Override
+ public void onSendSmsResult(int token, int messageRef, @SendStatusResult int status,
+ int reason) throws RemoteException {
+ SmsTracker tracker = mTrackers.get(token);
+ if (tracker == null) {
+ throw new IllegalArgumentException("Invalid token.");
+ }
+ switch(reason) {
+ case SmsImplBase.SEND_STATUS_OK:
+ tracker.onSent(mContext);
+ break;
+ case SmsImplBase.SEND_STATUS_ERROR:
+ tracker.onFailed(mContext, reason, 0 /* errorCode */);
+ mTrackers.remove(token);
+ break;
+ case SmsImplBase.SEND_STATUS_ERROR_RETRY:
+ tracker.mRetryCount += 1;
+ sendSms(tracker);
+ break;
+ case SmsImplBase.SEND_STATUS_ERROR_FALLBACK:
+ fallbackToPstn(token, tracker);
+ break;
+ default:
+ }
+ }
+
+ @Override
+ public void onSmsStatusReportReceived(int token, int messageRef, String format, byte[] pdu)
+ throws RemoteException {
+ Rlog.d(TAG, "Status report received.");
+ SmsTracker tracker = mTrackers.get(token);
+ if (tracker == null) {
+ throw new RemoteException("Invalid token.");
+ }
+ Pair<Boolean, Boolean> result = mSmsDispatchersController.handleSmsStatusReport(
+ tracker, format, pdu);
+ Rlog.d(TAG, "Status report handle result, success: " + result.first +
+ "complete: " + result.second);
+ try {
+ getImsManager().acknowledgeSmsReport(
+ token,
+ messageRef,
+ result.first ? SmsImplBase.STATUS_REPORT_STATUS_OK
+ : SmsImplBase.STATUS_REPORT_STATUS_ERROR);
+ } catch (ImsException e) {
+ Rlog.e(TAG, "Failed to acknowledgeSmsReport(). Error: "
+ + e.getMessage());
+ }
+ if (result.second) {
+ mTrackers.remove(token);
+ }
+ }
+
+ @Override
+ public void onSmsReceived(int token, String format, byte[] pdu)
+ throws RemoteException {
+ Rlog.d(TAG, "SMS received.");
+ mSmsDispatchersController.injectSmsPdu(pdu, format, result -> {
+ Rlog.d(TAG, "SMS handled result: " + result);
+ try {
+ getImsManager().acknowledgeSms(token,
+ 0,
+ result == Intents.RESULT_SMS_HANDLED
+ ? SmsImplBase.STATUS_REPORT_STATUS_OK
+ : SmsImplBase.DELIVER_STATUS_ERROR);
+ } catch (ImsException e) {
+ Rlog.e(TAG, "Failed to acknowledgeSms(). Error: " + e.getMessage());
+ }
+ });
+ }
+ };
+
+ public ImsSmsDispatcher(Phone phone, SmsDispatchersController smsDispatchersController) {
+ super(phone, smsDispatchersController);
+
+ mImsServiceRetryCount = 0;
+ // Send a message to connect to the Ims Service and open a connection through
+ // getImsService().
+ sendEmptyMessage(EVENT_GET_IMS_SERVICE);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case EVENT_GET_IMS_SERVICE:
+ try {
+ getImsService();
+ } catch (ImsException e) {
+ Rlog.e(TAG, "setListeners: " + e);
+ retryGetImsService();
+ }
+ break;
+ default:
+ super.handleMessage(msg);
+ }
+ }
+
+ private void getImsService() throws ImsException {
+ Rlog.d(TAG, "getImsService");
+ // Adding to set, will be safe adding multiple times. If the ImsService is not active yet,
+ // this method will throw an ImsException.
+ getImsManager().addNotifyStatusChangedCallbackIfAvailable(mNotifyStatusChangedCallback);
+ // Wait for ImsService.STATE_READY to start listening for SMS.
+ // Call the callback right away for compatibility with older devices that do not use states.
+ mNotifyStatusChangedCallback.notifyStateChanged();
+ }
+
+ private void setListeners() throws ImsException {
+ getImsManager().addRegistrationCallback(mRegistrationCallback);
+ getImsManager().addCapabilitiesCallback(mCapabilityCallback);
+ getImsManager().setSmsListener(mImsSmsListener);
+ mImsServiceRetryCount = 0;
+ }
+
+ private void retryGetImsService() {
+ // The binder connection is already up. Do not try to get it again.
+ if (getImsManager().isServiceAvailable()) {
+ return;
+ }
+ // remove callback so we do not receive updates from old ImsServiceProxy when switching
+ // between ImsServices.
+ getImsManager().removeNotifyStatusChangedCallback(mNotifyStatusChangedCallback);
+ // Exponential backoff during retry, limited to 32 seconds.
+ Rlog.e(TAG, "getImsService: Retrying getting ImsService...");
+ removeMessages(EVENT_GET_IMS_SERVICE);
+ sendEmptyMessageDelayed(EVENT_GET_IMS_SERVICE, mRetryTimeout.get());
+ }
+
+ public boolean isAvailable() {
+ synchronized (mLock) {
+ return mIsImsServiceUp && mIsRegistered && mIsSmsCapable;
+ }
+ }
+
+ @Override
+ protected String getFormat() {
+ try {
+ return getImsManager().getSmsFormat();
+ } catch (ImsException e) {
+ Rlog.e(TAG, "Failed to get sms format. Error: " + e.getMessage());
+ return SmsConstants.FORMAT_UNKNOWN;
+ }
+ }
+
+ @Override
+ protected boolean shouldBlockSms() {
+ return SMSDispatcherUtil.shouldBlockSms(isCdmaMo(), mPhone);
+ }
+
+ @Override
+ protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
+ String message, boolean statusReportRequested, SmsHeader smsHeader) {
+ return SMSDispatcherUtil.getSubmitPdu(isCdmaMo(), scAddr, destAddr, message,
+ statusReportRequested, smsHeader);
+ }
+
+ @Override
+ protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
+ int destPort, byte[] message, boolean statusReportRequested) {
+ return SMSDispatcherUtil.getSubmitPdu(isCdmaMo(), scAddr, destAddr, destPort, message,
+ statusReportRequested);
+ }
+
+ @Override
+ protected TextEncodingDetails calculateLength(CharSequence messageBody, boolean use7bitOnly) {
+ return SMSDispatcherUtil.calculateLength(isCdmaMo(), messageBody, use7bitOnly);
+ }
+
+ @Override
+ public void sendSms(SmsTracker tracker) {
+ Rlog.d(TAG, "sendSms: "
+ + " mRetryCount=" + tracker.mRetryCount
+ + " mMessageRef=" + tracker.mMessageRef
+ + " SS=" + mPhone.getServiceState().getState());
+
+ HashMap<String, Object> map = tracker.getData();
+
+ byte[] pdu = (byte[]) map.get(MAP_KEY_PDU);
+ byte smsc[] = (byte[]) map.get(MAP_KEY_SMSC);
+ boolean isRetry = tracker.mRetryCount > 0;
+
+ if (SmsConstants.FORMAT_3GPP.equals(getFormat()) && tracker.mRetryCount > 0) {
+ // per TS 23.040 Section 9.2.3.6: If TP-MTI SMS-SUBMIT (0x01) type
+ // TP-RD (bit 2) is 1 for retry
+ // and TP-MR is set to previously failed sms TP-MR
+ if (((0x01 & pdu[0]) == 0x01)) {
+ pdu[0] |= 0x04; // TP-RD
+ pdu[1] = (byte) tracker.mMessageRef; // TP-MR
+ }
+ }
+
+ int token = mNextToken.incrementAndGet();
+ mTrackers.put(token, tracker);
+ try {
+ getImsManager().sendSms(
+ token,
+ tracker.mMessageRef,
+ getFormat(),
+ smsc != null ? new String(smsc) : null,
+ isRetry,
+ pdu);
+ } catch (ImsException e) {
+ Rlog.e(TAG, "sendSms failed. Falling back to PSTN. Error: " + e.getMessage());
+ fallbackToPstn(token, tracker);
+ }
+ }
+
+ private ImsManager getImsManager() {
+ return ImsManager.getInstance(mContext, mPhone.getPhoneId());
+ }
+
+ @VisibleForTesting
+ public void fallbackToPstn(int token, SmsTracker tracker) {
+ mSmsDispatchersController.sendRetrySms(tracker);
+ mTrackers.remove(token);
+ }
+
+ @Override
+ protected boolean isCdmaMo() {
+ return mSmsDispatchersController.isCdmaFormat(getFormat());
+ }
+
+ @VisibleForTesting
+ public interface IRetryTimeout {
+ int get();
+ }
+}
diff --git a/com/android/internal/telephony/InboundSmsHandler.java b/com/android/internal/telephony/InboundSmsHandler.java
index 2d663cd7..cb87032a 100644
--- a/com/android/internal/telephony/InboundSmsHandler.java
+++ b/com/android/internal/telephony/InboundSmsHandler.java
@@ -603,9 +603,9 @@ public abstract class InboundSmsHandler extends StateMachine {
*/
private void handleInjectSms(AsyncResult ar) {
int result;
- PendingIntent receivedIntent = null;
+ SmsDispatchersController.SmsInjectionCallback callback = null;
try {
- receivedIntent = (PendingIntent) ar.userObj;
+ callback = (SmsDispatchersController.SmsInjectionCallback) ar.userObj;
SmsMessage sms = (SmsMessage) ar.result;
if (sms == null) {
result = Intents.RESULT_SMS_GENERIC_ERROR;
@@ -617,10 +617,8 @@ public abstract class InboundSmsHandler extends StateMachine {
result = Intents.RESULT_SMS_GENERIC_ERROR;
}
- if (receivedIntent != null) {
- try {
- receivedIntent.send(result);
- } catch (CanceledException e) { }
+ if (callback != null) {
+ callback.onSmsInjectedResult(result);
}
}
diff --git a/com/android/internal/telephony/NetworkScanRequestTracker.java b/com/android/internal/telephony/NetworkScanRequestTracker.java
index c6198d1f..2f416ccf 100644
--- a/com/android/internal/telephony/NetworkScanRequestTracker.java
+++ b/com/android/internal/telephony/NetworkScanRequestTracker.java
@@ -16,9 +16,9 @@
package com.android.internal.telephony;
-import static android.telephony.RadioNetworkConstants.RadioAccessNetworks.EUTRAN;
-import static android.telephony.RadioNetworkConstants.RadioAccessNetworks.GERAN;
-import static android.telephony.RadioNetworkConstants.RadioAccessNetworks.UTRAN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.EUTRAN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.GERAN;
+import static android.telephony.AccessNetworkConstants.AccessNetworkType.UTRAN;
import android.hardware.radio.V1_0.RadioError;
import android.os.AsyncResult;
@@ -460,12 +460,12 @@ public final class NetworkScanRequestTracker {
// stopped, a new scan will automatically start with nsri.
// The new scan can interrupt the live scan only when all the below requirements are met:
// 1. There is 1 live scan and no other pending scan
- // 2. The new scan is requested by system process
- // 3. The live scan is not requested by system process
+ // 2. The new scan is requested by mobile network setting menu (owned by PHONE process)
+ // 3. The live scan is not requested by mobile network setting menu
private synchronized boolean interruptLiveScan(NetworkScanRequestInfo nsri) {
if (mLiveRequestInfo != null && mPendingRequestInfo == null
- && nsri.mUid == Process.SYSTEM_UID
- && mLiveRequestInfo.mUid != Process.SYSTEM_UID) {
+ && nsri.mUid == Process.PHONE_UID
+ && mLiveRequestInfo.mUid != Process.PHONE_UID) {
doInterruptScan(mLiveRequestInfo.mScanId);
mPendingRequestInfo = nsri;
notifyMessenger(mLiveRequestInfo, TelephonyScanManager.CALLBACK_SCAN_ERROR,
diff --git a/com/android/internal/telephony/NitzData.java b/com/android/internal/telephony/NitzData.java
index df3541b4..80f1c4ab 100644
--- a/com/android/internal/telephony/NitzData.java
+++ b/com/android/internal/telephony/NitzData.java
@@ -23,7 +23,6 @@ import android.telephony.Rlog;
import com.android.internal.annotations.VisibleForTesting;
import java.util.Calendar;
-import java.util.Date;
import java.util.TimeZone;
/**
@@ -35,7 +34,6 @@ import java.util.TimeZone;
@VisibleForTesting(visibility = PACKAGE)
public final class NitzData {
private static final String LOG_TAG = ServiceStateTracker.LOG_TAG;
- private static final int MS_PER_HOUR = 60 * 60 * 1000;
private static final int MS_PER_QUARTER_HOUR = 15 * 60 * 1000;
/* Time stamp after 19 January 2038 is not supported under 32 bit */
@@ -53,7 +51,7 @@ public final class NitzData {
private final TimeZone mEmulatorHostTimeZone;
private NitzData(String originalString, int zoneOffsetMillis, Integer dstOffsetMillis,
- long utcTimeMillis, TimeZone timeZone) {
+ long utcTimeMillis, TimeZone emulatorHostTimeZone) {
if (originalString == null) {
throw new NullPointerException("originalString==null");
}
@@ -61,7 +59,7 @@ public final class NitzData {
this.mZoneOffset = zoneOffsetMillis;
this.mDstOffset = dstOffsetMillis;
this.mCurrentTimeMillis = utcTimeMillis;
- this.mEmulatorHostTimeZone = timeZone;
+ this.mEmulatorHostTimeZone = emulatorHostTimeZone;
}
/**
@@ -139,9 +137,9 @@ 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) {
+ long utcTimeMillis, TimeZone emulatorHostTimeZone) {
return new NitzData("Test data", zoneOffsetMillis, dstOffsetMillis, utcTimeMillis,
- timeZone);
+ emulatorHostTimeZone);
}
/**
@@ -187,44 +185,6 @@ public final class NitzData {
return mEmulatorHostTimeZone;
}
- /**
- * Using information present in the supplied {@link NitzData} object, guess the time zone.
- * Because multiple time zones can have the same offset / DST state at a given time this process
- * is error prone; an arbitrary match is returned when there are multiple candidates. The
- * algorithm can also return a non-exact match by assuming that the DST information provided by
- * NITZ is incorrect. This method can return {@code null} if no time zones are found.
- */
- public static TimeZone guessTimeZone(NitzData nitzData) {
- int offset = nitzData.getLocalOffsetMillis();
- boolean dst = nitzData.isDst();
- long when = nitzData.getCurrentTimeInMillis();
- TimeZone guess = findTimeZone(offset, dst, when);
- if (guess == null) {
- // Couldn't find a proper timezone. Perhaps the DST data is wrong.
- guess = findTimeZone(offset, !dst, when);
- }
- return guess;
- }
-
- private static TimeZone findTimeZone(int offset, boolean dst, long when) {
- int rawOffset = offset;
- if (dst) {
- rawOffset -= MS_PER_HOUR;
- }
- String[] zones = TimeZone.getAvailableIDs(rawOffset);
- TimeZone guess = null;
- Date d = new Date(when);
- for (String zone : zones) {
- TimeZone tz = TimeZone.getTimeZone(zone);
- if (tz.getOffset(when) == offset && tz.inDaylightTime(d) == dst) {
- guess = tz;
- break;
- }
- }
-
- return guess;
- }
-
@Override
public boolean equals(Object o) {
if (this == o) {
diff --git a/com/android/internal/telephony/NitzStateMachine.java b/com/android/internal/telephony/NitzStateMachine.java
index b1aa1f0d..1a365eee 100644
--- a/com/android/internal/telephony/NitzStateMachine.java
+++ b/com/android/internal/telephony/NitzStateMachine.java
@@ -19,7 +19,6 @@ 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;
@@ -29,13 +28,14 @@ import android.util.LocalLog;
import android.util.TimeUtils;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.TimeZoneLookupHelper.CountryResult;
+import com.android.internal.telephony.TimeZoneLookupHelper.OffsetResult;
import com.android.internal.telephony.metrics.TelephonyMetrics;
+import com.android.internal.telephony.util.TimeStampedValue;
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;
/**
@@ -98,13 +98,6 @@ public class NitzStateMachine {
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());
}
@@ -113,71 +106,60 @@ public class NitzStateMachine {
private static final String LOG_TAG = ServiceStateTracker.LOG_TAG;
private static final boolean DBG = ServiceStateTracker.DBG;
+ // Time detection state.
+
/**
- * 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.
+ * The last NITZ-sourced time considered. If auto time detection was off at the time this may
+ * not have been used to set the device time, but it can be used if auto time detection is
+ * re-enabled.
*/
- 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 TimeStampedValue<Long> mSavedNitzTime;
- private final LocalLog mTimeLog = new LocalLog(15);
- private final LocalLog mTimeZoneLog = new LocalLog(15);
+ // Time Zone detection state.
/**
- * 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.
+ * Sometimes we get the NITZ time before we know what country we are in. We keep the time zone
+ * information from the NITZ string in mLatestNitzSignal so we can fix the time zone once we
+ * know the country.
*/
- private boolean mNeedFixZoneAfterNitz = false;
+ private boolean mNeedCountryCodeForNitz = false;
- private NitzData mNitzData;
+ private TimeStampedValue<NitzData> mLatestNitzSignal;
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;
+ /**
+ * Boolean is {@code true} if {@link #handleNitzReceived(TimeStampedValue)} has been called and
+ * was able to determine a time zone (which may not ultimately have been used due to user
+ * settings). Cleared by {@link #handleNetworkAvailable()} and
+ * {@link #handleNetworkUnavailable()}. The flag can be used when historic NITZ data may no
+ * longer be valid. {@code true} indicates it's not reasonable to try to set the time zone using
+ * less reliable algorithms than NITZ-based detection such as by just using network country
+ * code.
+ */
+ private boolean mNitzTimeZoneDetectionSuccessful = false;
+ // Miscellaneous dependencies and helpers not related to detection state.
+ private final LocalLog mTimeLog = new LocalLog(15);
+ private final LocalLog mTimeZoneLog = new LocalLog(15);
private final GsmCdmaPhone mPhone;
private final DeviceState mDeviceState;
private final TimeServiceHelper mTimeServiceHelper;
+ private final TimeZoneLookupHelper mTimeZoneLookupHelper;
+ /** Wake lock used while setting time of day. */
+ private final PowerManager.WakeLock mWakeLock;
+ private static final String WAKELOCK_TAG = "NitzStateMachine";
public NitzStateMachine(GsmCdmaPhone phone) {
this(phone,
- TelephonyComponentFactory.getInstance().makeTimeServiceHelper(phone.getContext()),
- new DeviceState(phone));
+ new TimeServiceHelper(phone.getContext()),
+ new DeviceState(phone),
+ new TimeZoneLookupHelper());
}
@VisibleForTesting
public NitzStateMachine(GsmCdmaPhone phone, TimeServiceHelper timeServiceHelper,
- DeviceState deviceState) {
+ DeviceState deviceState, TimeZoneLookupHelper timeZoneLookupHelper) {
mPhone = phone;
Context context = phone.getContext();
@@ -186,132 +168,206 @@ public class NitzStateMachine {
mWakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKELOCK_TAG);
mDeviceState = deviceState;
+ mTimeZoneLookupHelper = timeZoneLookupHelper;
mTimeServiceHelper = timeServiceHelper;
mTimeServiceHelper.setListener(new TimeServiceHelper.Listener() {
@Override
public void onTimeDetectionChange(boolean enabled) {
if (enabled) {
- revertToNitzTime();
+ handleAutoTimeEnabled();
}
}
@Override
public void onTimeZoneDetectionChange(boolean enabled) {
if (enabled) {
- revertToNitzTimeZone();
+ handleAutoTimeZoneEnabled();
}
}
});
}
/**
- * Called when the device's network country is known, allowing the time zone detection to be
- * substantially more precise.
+ * Called when the network country is set on the Phone. Although set, the network country code
+ * may be invalid.
+ *
+ * @param countryChanged true when the country code is known to have changed, false if it
+ * probably hasn't
*/
- 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));
+ public void handleNetworkCountryCodeSet(boolean countryChanged) {
+ mGotCountryCode = true;
+
+ String isoCountryCode = mDeviceState.getNetworkCountryIsoForPhone();
+ if (!TextUtils.isEmpty(isoCountryCode)
+ && !mNitzTimeZoneDetectionSuccessful
+ && mTimeServiceHelper.isTimeZoneDetectionEnabled()) {
+ updateTimeZoneByNetworkCountryCode(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 (countryChanged || mNeedCountryCodeForNitz) {
+ // TimeZone.getDefault() returns a default zone (GMT) even when time zone have never
+ // been set which makes it difficult to tell if it's what the user / time zone detection
+ // has chosen. isTimeZoneSettingInitialized() tells us whether the time zone of the
+ // device has ever been explicit set by the user or code.
+ final boolean isTimeZoneSettingInitialized =
+ mTimeServiceHelper.isTimeZoneSettingInitialized();
if (DBG) {
- Rlog.d(LOG_TAG, "fixTimeZone(): guessNitzTimeZone returned "
- + (zone == null ? zone : zone.getID()));
+ Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet:"
+ + " isTimeZoneSettingInitialized=" + isTimeZoneSettingInitialized
+ + " mLatestNitzSignal=" + mLatestNitzSignal
+ + " isoCountryCode=" + isoCountryCode);
}
- } 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);
+ String zoneId;
+ if (TextUtils.isEmpty(isoCountryCode) && mNeedCountryCodeForNitz) {
+ // Country code not found. This is likely a test network.
+ // Get a TimeZone based only on the NITZ parameters (best guess).
+
+ // mNeedCountryCodeForNitz is only set to true when mLatestNitzSignal is set so
+ // there's no need to check mLatestNitzSignal == null.
+ OffsetResult lookupResult =
+ mTimeZoneLookupHelper.lookupByNitz(mLatestNitzSignal.mValue);
if (DBG) {
- Rlog.d(LOG_TAG, "fixTimeZone: tzOffset=" + tzOffset
- + " ltod=" + TimeUtils.logTimeOfDay(ctm));
+ Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: guessZoneIdByNitz() returned"
+ + " lookupResult=" + lookupResult);
}
- if (mTimeServiceHelper.isTimeDetectionEnabled()) {
- long adj = ctm - tzOffset;
+ zoneId = lookupResult != null ? lookupResult.zoneId : null;
+ } else if (mLatestNitzSignal == null) {
+ zoneId = null;
+ if (DBG) {
+ Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: No cached NITZ data available,"
+ + " not setting zone");
+ }
+ } else { // mLatestNitzSignal != null
+ if (nitzOffsetMightBeBogus(mLatestNitzSignal.mValue)
+ && isTimeZoneSettingInitialized
+ && !countryUsesUtc(isoCountryCode, mLatestNitzSignal)) {
+
+ // This case means that (1) the device received an NITZ signal that could be
+ // bogus due to having a zero offset from UTC, (2) the device has had 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 system clock may be adjusted by taking the NITZ time and
+ // assuming the current zone setting is correct.
+
+ TimeZone zone = TimeZone.getDefault();
if (DBG) {
- Rlog.d(LOG_TAG, "fixTimeZone: adj ltod=" + TimeUtils.logTimeOfDay(adj));
+ Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: NITZ looks bogus, maybe using"
+ + " current default zone to adjust the system clock,"
+ + " mNeedCountryCodeForNitz=" + mNeedCountryCodeForNitz
+ + " mLatestNitzSignal=" + mLatestNitzSignal
+ + " zone=" + zone);
+ }
+ zoneId = zone.getID();
+
+ if (mNeedCountryCodeForNitz) {
+ NitzData nitzData = mLatestNitzSignal.mValue;
+ try {
+ // Acquire the wakelock as we're reading the elapsed realtime clock
+ // here.
+ mWakeLock.acquire();
+
+ // Use the time that came with the NITZ offset that we think is bogus:
+ // we just interpret it as local time.
+ long ctm = nitzData.getCurrentTimeInMillis();
+ long delayAdjustedCtm = ctm + (mTimeServiceHelper.elapsedRealtime()
+ - mLatestNitzSignal.mElapsedRealtime);
+ long tzOffset = zone.getOffset(delayAdjustedCtm);
+ if (DBG) {
+ Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet:"
+ + " tzOffset=" + tzOffset
+ + " delayAdjustedCtm="
+ + TimeUtils.logTimeOfDay(delayAdjustedCtm));
+ }
+ if (mTimeServiceHelper.isTimeDetectionEnabled()) {
+ long timeZoneAdjustedCtm = delayAdjustedCtm - tzOffset;
+ String msg = "handleNetworkCountryCodeSet: setting time"
+ + " timeZoneAdjustedCtm="
+ + TimeUtils.logTimeOfDay(timeZoneAdjustedCtm);
+ setAndBroadcastNetworkSetTime(msg, timeZoneAdjustedCtm);
+ } else {
+ // Adjust the saved NITZ time to account for tzOffset.
+ mSavedNitzTime = new TimeStampedValue<>(
+ mSavedNitzTime.mValue - tzOffset,
+ mSavedNitzTime.mElapsedRealtime);
+ if (DBG) {
+ Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet:"
+ + "adjusting time mSavedNitzTime=" + mSavedNitzTime);
+ }
+ }
+ } finally {
+ mWakeLock.release();
+ }
}
- setAndBroadcastNetworkSetTime(adj);
} else {
- // Adjust the saved NITZ time to account for tzOffset.
- mSavedTime = mSavedTime - tzOffset;
+ NitzData nitzData = mLatestNitzSignal.mValue;
+ OffsetResult lookupResult =
+ mTimeZoneLookupHelper.lookupByNitzCountry(nitzData, isoCountryCode);
if (DBG) {
- Rlog.d(LOG_TAG, "fixTimeZone: adj mSavedTime=" + mSavedTime);
+ Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: using"
+ + " guessZoneIdByNitzCountry(nitzData, isoCountryCode),"
+ + " nitzData=" + nitzData
+ + " isoCountryCode=" + isoCountryCode
+ + " lookupResult=" + lookupResult);
}
+ zoneId = lookupResult != null ? lookupResult.zoneId : null;
}
}
- 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);
+ final String tmpLog = "handleNetworkCountryCodeSet:"
+ + " isTimeZoneSettingInitialized=" + isTimeZoneSettingInitialized
+ + " mLatestNitzSignal=" + mLatestNitzSignal
+ + " isoCountryCode=" + isoCountryCode
+ + " mNeedCountryCodeForNitz=" + mNeedCountryCodeForNitz
+ + " zoneId=" + zoneId;
+ mTimeZoneLog.log(tmpLog);
- if (zone != null) {
- Rlog.d(LOG_TAG, "fixTimeZone: zone != null zone.getID=" + zone.getID());
- if (mTimeServiceHelper.isTimeZoneDetectionEnabled()) {
- setAndBroadcastNetworkSetTimeZone(zone.getID());
+ if (zoneId != null) {
+ Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: zoneId != null, zoneId=" + zoneId);
+ if (mTimeServiceHelper.isTimeZoneDetectionEnabled()) {
+ setAndBroadcastNetworkSetTimeZone(zoneId);
+ } else {
+ Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: skip changing zone as"
+ + " isTimeZoneDetectionEnabled() is false");
+ }
+ if (mNeedCountryCodeForNitz) {
+ mSavedTimeZoneId = zoneId;
+ }
} else {
- Rlog.d(LOG_TAG, "fixTimeZone: skip changing zone as getAutoTimeZone was false");
- }
- if (mNeedFixZoneAfterNitz) {
- saveNitzTimeZone(zone.getID());
+ Rlog.d(LOG_TAG, "handleNetworkCountryCodeSet: lookupResult == null, do nothing");
}
- } else {
- Rlog.d(LOG_TAG, "fixTimeZone: zone == null, do nothing for zone");
+ mNeedCountryCodeForNitz = false;
}
- mNeedFixZoneAfterNitz = false;
+ }
+
+ private boolean countryUsesUtc(
+ String isoCountryCode, TimeStampedValue<NitzData> nitzSignal) {
+ return mTimeZoneLookupHelper.countryUsesUtc(
+ isoCountryCode,
+ nitzSignal.mValue.getCurrentTimeInMillis());
+ }
+
+ /**
+ * Informs the {@link NitzStateMachine} that the network has become available.
+ */
+ public void handleNetworkAvailable() {
+ if (DBG) {
+ Rlog.d(LOG_TAG, "handleNetworkAvailable: mNitzTimeZoneDetectionSuccessful="
+ + mNitzTimeZoneDetectionSuccessful
+ + ", Setting mNitzTimeZoneDetectionSuccessful=false");
+ }
+ mNitzTimeZoneDetectionSuccessful = false;
+ }
+
+ /**
+ * Informs the {@link NitzStateMachine} that the network has become unavailable.
+ */
+ public void handleNetworkUnavailable() {
+ if (DBG) {
+ Rlog.d(LOG_TAG, "handleNetworkUnavailable");
+ }
+
+ mGotCountryCode = false;
+ mNitzTimeZoneDetectionSuccessful = false;
}
/**
@@ -325,65 +381,53 @@ public class NitzStateMachine {
/**
* Handle a new NITZ signal being received.
*/
- public void setTimeAndTimeZoneFromNitz(NitzData newNitzData, long nitzReceiveTime) {
- setTimeZoneFromNitz(newNitzData, nitzReceiveTime);
- setTimeFromNitz(newNitzData, nitzReceiveTime);
+ public void handleNitzReceived(TimeStampedValue<NitzData> nitzSignal) {
+ handleTimeZoneFromNitz(nitzSignal);
+ handleTimeFromNitz(nitzSignal);
}
- private void setTimeZoneFromNitz(NitzData newNitzData, long nitzReceiveTime) {
+ private void handleTimeZoneFromNitz(TimeStampedValue<NitzData> nitzSignal) {
try {
+ NitzData newNitzData = nitzSignal.mValue;
String iso = mDeviceState.getNetworkCountryIsoForPhone();
- TimeZone zone;
+ String zoneId;
if (newNitzData.getEmulatorHostTimeZone() != null) {
- zone = newNitzData.getEmulatorHostTimeZone();
+ zoneId = newNitzData.getEmulatorHostTimeZone().getID();
} else {
if (!mGotCountryCode) {
- zone = null;
- } else if (iso != null && iso.length() > 0) {
- zone = TimeUtils.getTimeZone(
- newNitzData.getLocalOffsetMillis(),
- newNitzData.isDst(),
- newNitzData.getCurrentTimeInMillis(),
- iso);
+ zoneId = null;
+ } else if (!TextUtils.isEmpty(iso)) {
+ OffsetResult lookupResult =
+ mTimeZoneLookupHelper.lookupByNitzCountry(newNitzData, iso);
+ zoneId = lookupResult != null ? lookupResult.zoneId : null;
} 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);
+ OffsetResult lookupResult = mTimeZoneLookupHelper.lookupByNitz(newNitzData);
if (DBG) {
- Rlog.d(LOG_TAG, "setTimeFromNITZ(): guessNitzTimeZone returned "
- + (zone == null ? zone : zone.getID()));
+ Rlog.d(LOG_TAG, "handleTimeZoneFromNitz: guessZoneIdByNitz returned"
+ + " lookupResult=" + lookupResult);
}
+ zoneId = lookupResult != null ? lookupResult.zoneId : null;
}
}
- 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
+ if ((zoneId == null)
+ || mLatestNitzSignal == null
+ || offsetInfoDiffers(newNitzData, mLatestNitzSignal.mValue)) {
+ // 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;
+ mNeedCountryCodeForNitz = true;
+ mLatestNitzSignal = nitzSignal;
}
- String tmpLog = "NITZ: newNitzData=" + newNitzData
- + " nitzReceiveTime=" + nitzReceiveTime
- + " zone=" + (zone != null ? zone.getID() : "NULL")
+ String tmpLog = "handleTimeZoneFromNitz: nitzSignal=" + nitzSignal
+ + " zoneId=" + zoneId
+ " iso=" + iso + " mGotCountryCode=" + mGotCountryCode
- + " mNeedFixZoneAfterNitz=" + mNeedFixZoneAfterNitz
+ + " mNeedCountryCodeForNitz=" + mNeedCountryCodeForNitz
+ " isTimeZoneDetectionEnabled()="
+ mTimeServiceHelper.isTimeZoneDetectionEnabled();
if (DBG) {
@@ -391,143 +435,160 @@ public class NitzStateMachine {
}
mTimeZoneLog.log(tmpLog);
- if (zone != null) {
+ if (zoneId != null) {
if (mTimeServiceHelper.isTimeZoneDetectionEnabled()) {
- setAndBroadcastNetworkSetTimeZone(zone.getID());
+ setAndBroadcastNetworkSetTimeZone(zoneId);
}
- saveNitzTimeZone(zone.getID());
+ mNitzTimeZoneDetectionSuccessful = true;
+ mSavedTimeZoneId = zoneId;
}
} catch (RuntimeException ex) {
- Rlog.e(LOG_TAG, "NITZ: Processing NITZ data " + newNitzData + " ex=" + ex);
+ Rlog.e(LOG_TAG, "handleTimeZoneFromNitz: Processing NITZ data"
+ + " nitzSignal=" + nitzSignal
+ + " ex=" + ex);
}
}
- private void setTimeFromNitz(NitzData newNitzData, long nitzReceiveTime) {
+ private static boolean offsetInfoDiffers(NitzData one, NitzData two) {
+ return one.getLocalOffsetMillis() != two.getLocalOffsetMillis()
+ || one.isDst() != two.isDst();
+ }
+
+ private void handleTimeFromNitz(TimeStampedValue<NitzData> nitzSignal) {
try {
boolean ignoreNitz = mDeviceState.getIgnoreNitz();
if (ignoreNitz) {
- Rlog.d(LOG_TAG, "NITZ: Not setting clock because gsm.ignore-nitz is set");
+ Rlog.d(LOG_TAG,
+ "handleTimeFromNitz: Not setting clock because gsm.ignore-nitz is set");
return;
}
try {
+ // Acquire the wake lock as we are reading the elapsed realtime clock and system
+ // clock.
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!
+ // Validate the nitzTimeSignal to reject obviously bogus elapsedRealtime values.
+ long elapsedRealtime = mTimeServiceHelper.elapsedRealtime();
+ long millisSinceNitzReceived = elapsedRealtime - nitzSignal.mElapsedRealtime;
+ if (millisSinceNitzReceived < 0 || millisSinceNitzReceived > Integer.MAX_VALUE) {
if (DBG) {
- Rlog.d(LOG_TAG, "NITZ: not setting time, processing has taken "
- + (millisSinceNitzReceived / (1000 * 60 * 60 * 24))
- + " days");
+ Rlog.d(LOG_TAG, "handleTimeFromNitz: not setting time, unexpected"
+ + " elapsedRealtime=" + elapsedRealtime
+ + " nitzSignal=" + nitzSignal);
}
return;
}
- // Adjust the NITZ time by the delay since it was received.
- long adjustedCurrentTimeMillis = newNitzData.getCurrentTimeInMillis();
- adjustedCurrentTimeMillis += millisSinceNitzReceived;
+ // Adjust the NITZ time by the delay since it was received to get the time now.
+ long adjustedCurrentTimeMillis =
+ nitzSignal.mValue.getCurrentTimeInMillis() + millisSinceNitzReceived;
+ long gained = adjustedCurrentTimeMillis - mTimeServiceHelper.currentTimeMillis();
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);
+ String logMsg = "handleTimeFromNitz:"
+ + " nitzSignal=" + nitzSignal
+ + " adjustedCurrentTimeMillis=" + adjustedCurrentTimeMillis
+ + " millisSinceNitzReceived= " + millisSinceNitzReceived
+ + " gained=" + gained;
+
+ if (mSavedNitzTime == null) {
+ logMsg += ": First update received.";
+ setAndBroadcastNetworkSetTime(logMsg, adjustedCurrentTimeMillis);
} else {
- if (DBG) {
- Rlog.d(LOG_TAG, "NITZ: ignore, a previous update was "
- + timeSinceLastUpdate + "ms ago and gained="
- + gained + "ms");
+ long elapsedRealtimeSinceLastSaved = mTimeServiceHelper.elapsedRealtime()
+ - mSavedNitzTime.mElapsedRealtime;
+ int nitzUpdateSpacing = mDeviceState.getNitzUpdateSpacingMillis();
+ int nitzUpdateDiff = mDeviceState.getNitzUpdateDiffMillis();
+ if (elapsedRealtimeSinceLastSaved > nitzUpdateSpacing
+ || Math.abs(gained) > nitzUpdateDiff) {
+ // Either it has been a while since we received an update, or the gain
+ // is sufficiently large that we want to act on it.
+ logMsg += ": New update received.";
+ setAndBroadcastNetworkSetTime(logMsg, adjustedCurrentTimeMillis);
+ } else {
+ if (DBG) {
+ Rlog.d(LOG_TAG, logMsg + ": Update throttled.");
+ }
+
+ // Return early. This means that we don't reset the
+ // mSavedNitzTime for next time and that we may act on more
+ // NITZ time signals overall but should end up with a system clock that
+ // tracks NITZ more closely than if we saved throttled values (which
+ // would reset mSavedNitzTime.elapsedRealtime used to calculate time
+ // since the last NITZ signal was received).
+ return;
}
- return;
}
}
- saveNitzTime(adjustedCurrentTimeMillis);
+
+ // Save the last NITZ time signal used so we can return to it later
+ // if auto-time detection is toggled.
+ mSavedNitzTime = new TimeStampedValue<>(
+ adjustedCurrentTimeMillis, nitzSignal.mElapsedRealtime);
} finally {
mWakeLock.release();
}
} catch (RuntimeException ex) {
- Rlog.e(LOG_TAG, "NITZ: Processing NITZ data " + newNitzData + " ex=" + ex);
+ Rlog.e(LOG_TAG, "handleTimeFromNitz: Processing NITZ data"
+ + " nitzSignal=" + nitzSignal
+ + " 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);
+ Rlog.d(LOG_TAG, "setAndBroadcastNetworkSetTimeZone: zoneId=" + zoneId);
}
mTimeServiceHelper.setDeviceTimeZone(zoneId);
if (DBG) {
Rlog.d(LOG_TAG,
- "setAndBroadcastNetworkSetTimeZone: call alarm.setTimeZone and broadcast"
+ "setAndBroadcastNetworkSetTimeZone: called setDeviceTimeZone()"
+ " zoneId=" + zoneId);
}
}
- private void setAndBroadcastNetworkSetTime(long time) {
+ private void setAndBroadcastNetworkSetTime(String msg, long time) {
+ if (!mWakeLock.isHeld()) {
+ Rlog.w(LOG_TAG, "setAndBroadcastNetworkSetTime: Wake lock not held while setting device"
+ + " time (msg=" + msg + ")");
+ }
+
+ msg = "setAndBroadcastNetworkSetTime: [Setting time to time=" + time + "]:" + msg;
if (DBG) {
- Rlog.d(LOG_TAG, "setAndBroadcastNetworkSetTime: time=" + time + "ms");
+ Rlog.d(LOG_TAG, msg);
}
+ mTimeLog.log(msg);
mTimeServiceHelper.setDeviceTime(time);
TelephonyMetrics.getInstance().writeNITZEvent(mPhone.getPhoneId(), time);
}
- private void revertToNitzTime() {
+ private void handleAutoTimeEnabled() {
if (DBG) {
- Rlog.d(LOG_TAG, "Reverting to NITZ Time: mSavedTime=" + mSavedTime
- + " mSavedAtTime=" + mSavedAtTime);
+ Rlog.d(LOG_TAG, "handleAutoTimeEnabled: Reverting to NITZ Time:"
+ + " mSavedNitzTime=" + mSavedNitzTime);
}
- 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));
+ if (mSavedNitzTime != null) {
+ try {
+ // Acquire the wakelock as we're reading the elapsed realtime clock here.
+ mWakeLock.acquire();
+
+ long elapsedRealtime = mTimeServiceHelper.elapsedRealtime();
+ String msg = "mSavedNitzTime: Reverting to NITZ time"
+ + " elapsedRealtime=" + elapsedRealtime
+ + " mSavedNitzTime=" + mSavedNitzTime;
+ long adjustedCurrentTimeMillis =
+ mSavedNitzTime.mValue + (elapsedRealtime - mSavedNitzTime.mElapsedRealtime);
+ setAndBroadcastNetworkSetTime(msg, adjustedCurrentTimeMillis);
+ } finally {
+ mWakeLock.release();
+ }
}
}
- private void revertToNitzTimeZone() {
- String tmpLog = "Reverting to NITZ TimeZone: tz=" + mSavedTimeZoneId;
+ private void handleAutoTimeZoneEnabled() {
+ String tmpLog = "handleAutoTimeZoneEnabled: Reverting to NITZ TimeZone:"
+ + " mSavedTimeZoneId=" + mSavedTimeZoneId;
if (DBG) {
Rlog.d(LOG_TAG, tmpLog);
}
@@ -546,14 +607,18 @@ public class NitzStateMachine {
* Dumps the current in-memory state to the supplied PrintWriter.
*/
public void dumpState(PrintWriter pw) {
- pw.println(" mNeedFixZoneAfterNitz=" + mNeedFixZoneAfterNitz);
- pw.println(" mNitzData=" + mNitzData);
+ // Time Detection State
+ pw.println(" mSavedTime=" + mSavedNitzTime);
+
+ // Time Zone Detection State
+ pw.println(" mNeedCountryCodeForNitz=" + mNeedCountryCodeForNitz);
+ pw.println(" mLatestNitzSignal=" + mLatestNitzSignal);
pw.println(" mGotCountryCode=" + mGotCountryCode);
- pw.println(" mSavedTimeZone=" + mSavedTimeZoneId);
- pw.println(" mSavedTime=" + mSavedTime);
- pw.println(" mSavedAtTime=" + mSavedAtTime);
+ pw.println(" mSavedTimeZoneId=" + mSavedTimeZoneId);
+ pw.println(" mNitzTimeZoneDetectionSuccessful=" + mNitzTimeZoneDetectionSuccessful);
+
+ // Miscellaneous
pw.println(" mWakeLock=" + mWakeLock);
- pw.println(" mNitzUpdatedTime=" + mNitzUpdatedTime);
pw.flush();
}
@@ -573,65 +638,44 @@ public class NitzStateMachine {
}
/**
- * Update time zone by network country code, works on countries which only have one time zone.
+ * Update time zone by network country code, works well on countries which only have one time
+ * zone or multiple zones with the same offset.
*
* @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);
+ private void updateTimeZoneByNetworkCountryCode(String iso) {
+ CountryResult lookupResult = mTimeZoneLookupHelper.lookupByCountry(
+ iso, mTimeServiceHelper.currentTimeMillis());
+ if (lookupResult != null && lookupResult.allZonesHaveSameOffset) {
+ String logMsg = "updateTimeZoneByNetworkCountryCode: set time"
+ + " lookupResult=" + lookupResult
+ + " iso=" + iso;
if (DBG) {
- Rlog.d(LOG_TAG, "updateTimeZoneByNetworkCountryCode: no nitz but one TZ for iso-cc="
- + iso
- + " with zone.getID=" + zoneId);
+ Rlog.d(LOG_TAG, logMsg);
}
- mTimeZoneLog.log("updateTimeZoneByNetworkCountryCode: set time zone=" + zoneId
- + " iso=" + iso);
- setAndBroadcastNetworkSetTimeZone(zoneId);
+ mTimeZoneLog.log(logMsg);
+ setAndBroadcastNetworkSetTimeZone(lookupResult.zoneId);
} else {
if (DBG) {
- Rlog.d(LOG_TAG,
- "updateTimeZoneByNetworkCountryCode: there are " + uniqueZoneIds.size()
- + " unique offsets for iso-cc='" + iso
- + "', do nothing");
+ Rlog.d(LOG_TAG, "updateTimeZoneByNetworkCountryCode: no good zone for"
+ + " iso=" + iso
+ + " lookupResult=" + lookupResult);
}
}
}
/**
- * Clear the mNitzUpdatedTime flag.
- */
- public void clearNitzUpdatedTime() {
- mNitzUpdatedTime = false;
- }
-
- /**
- * Get the mNitzUpdatedTime flag value.
+ * Get the mNitzTimeZoneDetectionSuccessful 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();
+ public boolean getNitzTimeZoneDetectionSuccessful() {
+ return mNitzTimeZoneDetectionSuccessful;
}
/**
* Returns the last NITZ data that was cached.
*/
public NitzData getCachedNitzData() {
- return mNitzData;
+ return mLatestNitzSignal != null ? mLatestNitzSignal.mValue : null;
}
/**
@@ -642,11 +686,4 @@ public class NitzStateMachine {
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 a2aeb441..54055464 100644
--- a/com/android/internal/telephony/Phone.java
+++ b/com/android/internal/telephony/Phone.java
@@ -2120,6 +2120,10 @@ public abstract class Phone extends Handler implements PhoneInternalInterface {
mNotifier.notifyDataActivationStateChanged(this, state);
}
+ public void notifyUserMobileDataStateChanged(boolean state) {
+ mNotifier.notifyUserMobileDataStateChanged(this, state);
+ }
+
public void notifySignalStrength() {
mNotifier.notifySignalStrength(this);
}
@@ -2997,6 +3001,15 @@ public abstract class Phone extends Handler implements PhoneInternalInterface {
}
/**
+ * Resets the Carrier Keys in the database. This involves 2 steps:
+ * 1. Delete the keys from the database.
+ * 2. Send an intent to download new Certificates.
+ */
+ public void resetCarrierKeysForImsiEncryption() {
+ return;
+ }
+
+ /**
* Return if UT capability of ImsPhone is enabled or not
*/
public boolean isUtEnabled() {
diff --git a/com/android/internal/telephony/PhoneFactory.java b/com/android/internal/telephony/PhoneFactory.java
index d2a65d9d..c857740e 100644
--- a/com/android/internal/telephony/PhoneFactory.java
+++ b/com/android/internal/telephony/PhoneFactory.java
@@ -35,14 +35,15 @@ import android.util.LocalLog;
import com.android.internal.os.BackgroundThread;
import com.android.internal.telephony.cdma.CdmaSubscriptionSourceManager;
import com.android.internal.telephony.dataconnection.TelephonyNetworkFactory;
+import com.android.internal.telephony.euicc.EuiccCardController;
import com.android.internal.telephony.euicc.EuiccController;
import com.android.internal.telephony.ims.ImsResolver;
import com.android.internal.telephony.imsphone.ImsPhone;
import com.android.internal.telephony.imsphone.ImsPhoneFactory;
import com.android.internal.telephony.sip.SipPhone;
import com.android.internal.telephony.sip.SipPhoneFactory;
-import com.android.internal.telephony.uicc.IccCardProxy;
import com.android.internal.telephony.uicc.UiccController;
+import com.android.internal.telephony.uicc.UiccProfile;
import com.android.internal.telephony.util.NotificationChannelController;
import com.android.internal.util.IndentingPrintWriter;
@@ -73,6 +74,7 @@ public class PhoneFactory {
static private UiccController sUiccController;
private static IntentBroadcaster sIntentBroadcaster;
private static @Nullable EuiccController sEuiccController;
+ private static @Nullable EuiccCardController sEuiccCardController;
static private CommandsInterface sCommandsInterface = null;
static private SubscriptionInfoUpdater sSubInfoRecordUpdater = null;
@@ -141,6 +143,7 @@ public class PhoneFactory {
if (context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_TELEPHONY_EUICC)) {
sEuiccController = EuiccController.init(context);
+ sEuiccCardController = EuiccCardController.init(context);
}
/* In case of multi SIM mode two instances of Phone, RIL are created,
@@ -432,7 +435,7 @@ public class PhoneFactory {
pw.println("++++++++++++++++++++++++++++++++");
try {
- ((IccCardProxy)phone.getIccCard()).dump(fd, pw, args);
+ ((UiccProfile) phone.getIccCard()).dump(fd, pw, args);
} catch (Exception e) {
e.printStackTrace();
}
@@ -467,6 +470,7 @@ public class PhoneFactory {
pw.increaseIndent();
try {
sEuiccController.dump(fd, pw, args);
+ sEuiccCardController.dump(fd, pw, args);
} catch (Exception e) {
e.printStackTrace();
}
diff --git a/com/android/internal/telephony/PhoneInternalInterface.java b/com/android/internal/telephony/PhoneInternalInterface.java
index feb3d68c..918a8b12 100644
--- a/com/android/internal/telephony/PhoneInternalInterface.java
+++ b/com/android/internal/telephony/PhoneInternalInterface.java
@@ -852,4 +852,9 @@ public interface PhoneInternalInterface {
* decrypt the permanent identity.
*/
public ImsiEncryptionInfo getCarrierInfoForImsiEncryption(int keyType);
+
+ /**
+ * Resets the Carrier Keys, by deleting them from the database and sending a download intent.
+ */
+ public void resetCarrierKeysForImsiEncryption();
}
diff --git a/com/android/internal/telephony/PhoneNotifier.java b/com/android/internal/telephony/PhoneNotifier.java
index 3b29a060..5b8048ff 100644
--- a/com/android/internal/telephony/PhoneNotifier.java
+++ b/com/android/internal/telephony/PhoneNotifier.java
@@ -62,4 +62,6 @@ public interface PhoneNotifier {
public void notifyVoiceActivationStateChanged(Phone sender, int activationState);
public void notifyDataActivationStateChanged(Phone sender, int activationState);
+
+ public void notifyUserMobileDataStateChanged(Phone sender, boolean state);
}
diff --git a/com/android/internal/telephony/PhoneSubInfoController.java b/com/android/internal/telephony/PhoneSubInfoController.java
index 9cd088ff..b0a418d1 100644
--- a/com/android/internal/telephony/PhoneSubInfoController.java
+++ b/com/android/internal/telephony/PhoneSubInfoController.java
@@ -26,14 +26,15 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.telephony.ImsiEncryptionInfo;
import android.telephony.PhoneNumberUtils;
-import android.telephony.SubscriptionManager;
import android.telephony.Rlog;
+import android.telephony.SubscriptionManager;
import com.android.internal.telephony.uicc.IsimRecords;
import com.android.internal.telephony.uicc.UiccCard;
import com.android.internal.telephony.uicc.UiccCardApplication;
import static android.Manifest.permission.CALL_PRIVILEGED;
+import static android.Manifest.permission.MODIFY_PHONE_STATE;
import static android.Manifest.permission.READ_PHONE_NUMBERS;
import static android.Manifest.permission.READ_PHONE_STATE;
import static android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE;
@@ -106,7 +107,7 @@ public class PhoneSubInfoController extends IPhoneSubInfo.Stub {
}
public ImsiEncryptionInfo getCarrierInfoForImsiEncryption(int subId, int keyType,
- String callingPackage) {
+ String callingPackage) {
Phone phone = getPhone(subId);
if (phone != null) {
if (!checkReadPhoneState(callingPackage, "getCarrierInfoForImsiEncryption")) {
@@ -123,9 +124,7 @@ public class PhoneSubInfoController extends IPhoneSubInfo.Stub {
ImsiEncryptionInfo imsiEncryptionInfo) {
Phone phone = getPhone(subId);
if (phone != null) {
- if (!checkReadPhoneState(callingPackage, "setCarrierInfoForImsiEncryption")) {
- return;
- }
+ enforceModifyPermission();
phone.setCarrierInfoForImsiEncryption(imsiEncryptionInfo);
} else {
loge("setCarrierInfoForImsiEncryption phone is null for Subscription:" + subId);
@@ -133,6 +132,25 @@ public class PhoneSubInfoController extends IPhoneSubInfo.Stub {
}
}
+ /**
+ * Resets the Carrier Keys in the database. This involves 2 steps:
+ * 1. Delete the keys from the database.
+ * 2. Send an intent to download new Certificates.
+ * @param subId
+ * @param callingPackage
+ */
+ public void resetCarrierKeysForImsiEncryption(int subId, String callingPackage) {
+ Phone phone = getPhone(subId);
+ if (phone != null) {
+ enforceModifyPermission();
+ phone.resetCarrierKeysForImsiEncryption();
+ return;
+ } else {
+ loge("resetCarrierKeysForImsiEncryption phone is null for Subscription:" + subId);
+ return;
+ }
+ }
+
public String getDeviceSvn(String callingPackage) {
return getDeviceSvnUsingSubId(getDefaultSubscription(), callingPackage);
@@ -327,6 +345,14 @@ public class PhoneSubInfoController extends IPhoneSubInfo.Stub {
}
}
+ /**
+ * Make sure caller has modify phone state permission.
+ */
+ private void enforceModifyPermission() {
+ mContext.enforceCallingOrSelfPermission(MODIFY_PHONE_STATE,
+ "Requires MODIFY_PHONE_STATE");
+ }
+
private int getDefaultSubscription() {
return PhoneFactory.getDefaultSubscription();
}
diff --git a/com/android/internal/telephony/RIL.java b/com/android/internal/telephony/RIL.java
index 57c42265..95c273c4 100644
--- a/com/android/internal/telephony/RIL.java
+++ b/com/android/internal/telephony/RIL.java
@@ -53,6 +53,8 @@ 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.KeepalivePacketData;
+import android.net.LinkAddress;
import android.net.NetworkUtils;
import android.os.AsyncResult;
import android.os.Build;
@@ -67,6 +69,8 @@ import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.WorkSource;
import android.service.carrier.CarrierIdentifier;
+import android.telephony.AccessNetworkConstants.AccessNetworkType;
+import android.telephony.CellIdentity;
import android.telephony.CellInfo;
import android.telephony.ClientRequestStats;
import android.telephony.ImsiEncryptionInfo;
@@ -76,7 +80,6 @@ import android.telephony.NetworkScanRequest;
import android.telephony.PhoneNumberUtils;
import android.telephony.RadioAccessFamily;
import android.telephony.RadioAccessSpecifier;
-import android.telephony.RadioNetworkConstants.RadioAccessNetworks;
import android.telephony.Rlog;
import android.telephony.SignalStrength;
import android.telephony.SmsManager;
@@ -84,7 +87,6 @@ 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;
@@ -97,7 +99,6 @@ import com.android.internal.telephony.cdma.CdmaSmsBroadcastConfigInfo;
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;
@@ -105,8 +106,9 @@ import java.io.DataInputStream;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.PrintWriter;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
import java.net.InetAddress;
-import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -506,64 +508,19 @@ 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);
- }
- }
+ if (result != null) {
+ AsyncResult.forMessage(result, null,
+ CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED));
+ result.sendToTarget();
}
}
@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);
- }
- }
+ if (result != null) {
+ AsyncResult.forMessage(result, null,
+ CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED));
+ result.sendToTarget();
}
}
@@ -1187,26 +1144,29 @@ public class RIL extends BaseCommands implements CommandsInterface {
// Process address
String[] addresses = null;
if (!TextUtils.isEmpty(dcResult.addresses)) {
- addresses = dcResult.addresses.split(" ");
+ addresses = dcResult.addresses.split("\\s+");
}
- List<InterfaceAddress> iaList = new ArrayList<>();
+ List<LinkAddress> laList = 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);
+ LinkAddress la;
+ // Check if the address contains prefix length. If yes, LinkAddress
+ // can parse that.
+ if (address.split("/").length == 2) {
+ la = new LinkAddress(address);
+ } else {
+ InetAddress ia = NetworkUtils.numericToInetAddress(address);
+ la = new LinkAddress(ia, (ia instanceof Inet4Address) ? 32 : 128);
+ }
+
+ laList.add(la);
+ } catch (IllegalArgumentException e) {
+ Rlog.e(RILJ_LOG_TAG, "Unknown address: " + address + ", " + e);
}
}
}
@@ -1214,7 +1174,7 @@ public class RIL extends BaseCommands implements CommandsInterface {
// Process dns
String[] dnses = null;
if (!TextUtils.isEmpty(dcResult.dnses)) {
- dnses = dcResult.dnses.split(" ");
+ dnses = dcResult.dnses.split("\\s+");
}
List<InetAddress> dnsList = new ArrayList<>();
@@ -1234,7 +1194,7 @@ public class RIL extends BaseCommands implements CommandsInterface {
// Process gateway
String[] gateways = null;
if (!TextUtils.isEmpty(dcResult.gateways)) {
- gateways = dcResult.gateways.split(" ");
+ gateways = dcResult.gateways.split("\\s+");
}
List<InetAddress> gatewayList = new ArrayList<>();
@@ -1257,10 +1217,10 @@ public class RIL extends BaseCommands implements CommandsInterface {
dcResult.active,
dcResult.type,
dcResult.ifname,
- iaList,
+ laList,
dnsList,
gatewayList,
- new ArrayList<>(Arrays.asList(dcResult.pcscf.trim().split("\\s*,\\s*"))),
+ new ArrayList<>(Arrays.asList(dcResult.pcscf.trim().split("\\s+"))),
dcResult.mtu
);
}
@@ -1751,13 +1711,13 @@ public class RIL extends BaseCommands implements CommandsInterface {
rasInHalFormat.radioAccessNetwork = ras.getRadioAccessNetwork();
List<Integer> bands = null;
switch (ras.getRadioAccessNetwork()) {
- case RadioAccessNetworks.GERAN:
+ case AccessNetworkType.GERAN:
bands = rasInHalFormat.geranBands;
break;
- case RadioAccessNetworks.UTRAN:
+ case AccessNetworkType.UTRAN:
bands = rasInHalFormat.utranBands;
break;
- case RadioAccessNetworks.EUTRAN:
+ case AccessNetworkType.EUTRAN:
bands = rasInHalFormat.eutranBands;
break;
default:
@@ -3077,7 +3037,7 @@ public class RIL extends BaseCommands implements CommandsInterface {
ImsSmsMessage msg = new ImsSmsMessage();
msg.tech = RILConstants.GSM_PHONE;
- msg.retry = (byte) retry == 1 ? true : false;
+ msg.retry = (byte) retry >= 1 ? true : false;
msg.messageRef = messageRef;
GsmSmsMessage gsmMsg = constructGsmSendSmsRilRequest(smscPdu, pdu);
@@ -3104,7 +3064,7 @@ public class RIL extends BaseCommands implements CommandsInterface {
ImsSmsMessage msg = new ImsSmsMessage();
msg.tech = RILConstants.CDMA_PHONE;
- msg.retry = (byte) retry == 1 ? true : false;
+ msg.retry = (byte) retry >= 1 ? true : false;
msg.messageRef = messageRef;
CdmaSmsMessage cdmaMsg = new CdmaSmsMessage();
@@ -3802,6 +3762,93 @@ public class RIL extends BaseCommands implements CommandsInterface {
}
@Override
+ public void startNattKeepalive(
+ int contextId, KeepalivePacketData packetData, int intervalMillis, Message result) {
+ checkNotNull(packetData, "KeepaliveRequest cannot be null.");
+ IRadio radioProxy = getRadioProxy(result);
+ if (radioProxy == null) {
+ riljLoge("Radio Proxy object is null!");
+ return;
+ }
+
+ android.hardware.radio.V1_1.IRadio radioProxy11 =
+ android.hardware.radio.V1_1.IRadio.castFrom(radioProxy);
+ if (radioProxy11 == null) {
+ if (result != null) {
+ AsyncResult.forMessage(result, null,
+ CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED));
+ result.sendToTarget();
+ }
+ return;
+ }
+
+ RILRequest rr = obtainRequest(
+ RIL_REQUEST_START_KEEPALIVE, result, mRILDefaultWorkSource);
+
+ if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+ try {
+ android.hardware.radio.V1_1.KeepaliveRequest req =
+ new android.hardware.radio.V1_1.KeepaliveRequest();
+
+ req.cid = contextId;
+
+ if (packetData.dstAddress instanceof Inet4Address) {
+ req.type = android.hardware.radio.V1_1.KeepaliveType.NATT_IPV4;
+ } else if (packetData.dstAddress instanceof Inet6Address) {
+ req.type = android.hardware.radio.V1_1.KeepaliveType.NATT_IPV6;
+ } else {
+ AsyncResult.forMessage(result, null,
+ CommandException.fromRilErrno(INVALID_ARGUMENTS));
+ result.sendToTarget();
+ return;
+ }
+
+ appendPrimitiveArrayToArrayList(
+ packetData.srcAddress.getAddress(), req.sourceAddress);
+ req.sourcePort = packetData.srcPort;
+ appendPrimitiveArrayToArrayList(
+ packetData.dstAddress.getAddress(), req.destinationAddress);
+ req.destinationPort = packetData.dstPort;
+
+ radioProxy11.startKeepalive(rr.mSerial, req);
+ } catch (RemoteException | RuntimeException e) {
+ handleRadioProxyExceptionForRR(rr, "startNattKeepalive", e);
+ }
+ }
+
+ @Override
+ public void stopNattKeepalive(int sessionHandle, Message result) {
+ IRadio radioProxy = getRadioProxy(result);
+ if (radioProxy == null) {
+ Rlog.e(RIL.RILJ_LOG_TAG, "Radio Proxy object is null!");
+ return;
+ }
+
+ android.hardware.radio.V1_1.IRadio radioProxy11 =
+ android.hardware.radio.V1_1.IRadio.castFrom(radioProxy);
+ if (radioProxy11 == null) {
+ if (result != null) {
+ AsyncResult.forMessage(result, null,
+ CommandException.fromRilErrno(REQUEST_NOT_SUPPORTED));
+ result.sendToTarget();
+ }
+ return;
+ }
+
+ RILRequest rr = obtainRequest(
+ RIL_REQUEST_STOP_KEEPALIVE, result, mRILDefaultWorkSource);
+
+ if (RILJ_LOGD) riljLog(rr.serialString() + "> " + requestToString(rr.mRequest));
+
+ try {
+ radioProxy11.stopKeepalive(rr.mSerial, sessionHandle);
+ } catch (RemoteException | RuntimeException e) {
+ handleRadioProxyExceptionForRR(rr, "stopNattKeepalive", e);
+ }
+ }
+
+ @Override
public void getIMEI(Message result) {
throw new RuntimeException("getIMEI not expected to be called");
}
@@ -4069,13 +4116,6 @@ public class RIL extends BaseCommands implements CommandsInterface {
return workSource;
}
- private String getWorkSourceClientId(WorkSource workSource) {
- if (workSource != null) {
- return String.valueOf(workSource.get(0)) + ":" + workSource.getName(0);
- }
-
- return null;
- }
/**
* Holds a PARTIAL_WAKE_LOCK whenever
@@ -4099,7 +4139,7 @@ public class RIL extends BaseCommands implements CommandsInterface {
mWakeLockCount++;
mWlSequenceNum++;
- String clientId = getWorkSourceClientId(rr.mWorkSource);
+ String clientId = rr.getWorkSourceClientId();
if (!mClientWakelockTracker.isClientActive(clientId)) {
if (mActiveWakelockWorkSource != null) {
mActiveWakelockWorkSource.add(rr.mWorkSource);
@@ -4161,7 +4201,7 @@ public class RIL extends BaseCommands implements CommandsInterface {
mClientWakelockTracker.stopTracking(rr.mClientId,
rr.mRequest, rr.mSerial,
(mWakeLockCount > 1) ? mWakeLockCount - 1 : 0);
- String clientId = getWorkSourceClientId(rr.mWorkSource);;
+ String clientId = rr.getWorkSourceClientId();
if (!mClientWakelockTracker.isClientActive(clientId)
&& (mActiveWakelockWorkSource != null)) {
mActiveWakelockWorkSource.remove(rr.mWorkSource);
@@ -4730,6 +4770,10 @@ public class RIL extends BaseCommands implements CommandsInterface {
return "RIL_REQUEST_GET_SLOT_STATUS";
case RIL_REQUEST_SET_LOGICAL_TO_PHYSICAL_SLOT_MAPPING:
return "RIL_REQUEST_SET_LOGICAL_TO_PHYSICAL_SLOT_MAPPING";
+ case RIL_REQUEST_START_KEEPALIVE:
+ return "RIL_REQUEST_START_KEEPALIVE";
+ case RIL_REQUEST_STOP_KEEPALIVE:
+ return "RIL_REQUEST_STOP_KEEPALIVE";
default: return "<unknown request>";
}
}
@@ -4834,6 +4878,8 @@ public class RIL extends BaseCommands implements CommandsInterface {
return "RIL_UNSOL_NETWORK_SCAN_RESULT";
case RIL_UNSOL_ICC_SLOT_STATUS:
return "RIL_UNSOL_ICC_SLOT_STATUS";
+ case RIL_UNSOL_KEEPALIVE_STATUS:
+ return "RIL_UNSOL_KEEPALIVE_STATUS";
default:
return "<unknown response>";
}
@@ -4914,6 +4960,13 @@ public class RIL extends BaseCommands implements CommandsInterface {
return mClientWakelockTracker.getClientRequestStats();
}
+ /** Append the data to the end of an ArrayList */
+ public static void appendPrimitiveArrayToArrayList(byte[] src, ArrayList<Byte> dst) {
+ for (byte b : src) {
+ dst.add(b);
+ }
+ }
+
public static ArrayList<Byte> primitiveArrayToArrayList(byte[] arr) {
ArrayList<Byte> arrayList = new ArrayList<>(arr.length);
for (byte b : arr) {
@@ -4922,6 +4975,7 @@ public class RIL extends BaseCommands implements CommandsInterface {
return arrayList;
}
+ /** Convert an ArrayList of Bytes to an exactly-sized primitive array */
public static byte[] arrayListToPrimitiveArray(ArrayList<Byte> bytes) {
byte[] ret = new byte[bytes.size()];
for (int i = 0; i < ret.length; i++) {
@@ -5008,12 +5062,13 @@ public class RIL extends BaseCommands implements CommandsInterface {
private static void writeToParcelForGsm(
Parcel p, int lac, int cid, int arfcn, int bsic, String mcc, String mnc,
String al, String as, int ss, int ber, int ta) {
+ p.writeInt(CellIdentity.TYPE_GSM);
+ p.writeString(mcc);
+ p.writeString(mnc);
p.writeInt(lac);
p.writeInt(cid);
p.writeInt(arfcn);
p.writeInt(bsic);
- p.writeString(mcc);
- p.writeString(mnc);
p.writeString(al);
p.writeString(as);
p.writeInt(ss);
@@ -5024,6 +5079,9 @@ public class RIL extends BaseCommands implements CommandsInterface {
private static void writeToParcelForCdma(
Parcel p, int ni, int si, int bsi, int lon, int lat, String al, String as,
int dbm, int ecio, int eDbm, int eEcio, int eSnr) {
+ p.writeInt(CellIdentity.TYPE_CDMA);
+ p.writeString(null);
+ p.writeString(null);
p.writeInt(ni);
p.writeInt(si);
p.writeInt(bsi);
@@ -5041,12 +5099,13 @@ public class RIL extends BaseCommands implements CommandsInterface {
private static void writeToParcelForLte(
Parcel p, int ci, int pci, int tac, int earfcn, String mcc, String mnc, String al,
String as, int ss, int rsrp, int rsrq, int rssnr, int cqi, int ta) {
+ p.writeInt(CellIdentity.TYPE_LTE);
+ p.writeString(mcc);
+ p.writeString(mnc);
p.writeInt(ci);
p.writeInt(pci);
p.writeInt(tac);
p.writeInt(earfcn);
- p.writeString(mcc);
- p.writeString(mnc);
p.writeString(al);
p.writeString(as);
p.writeInt(ss);
@@ -5060,12 +5119,13 @@ public class RIL extends BaseCommands implements CommandsInterface {
private static void writeToParcelForWcdma(
Parcel p, int lac, int cid, int psc, int uarfcn, String mcc, String mnc,
String al, String as, int ss, int ber) {
+ p.writeInt(CellIdentity.TYPE_WCDMA);
+ p.writeString(mcc);
+ p.writeString(mnc);
p.writeInt(lac);
p.writeInt(cid);
p.writeInt(psc);
p.writeInt(uarfcn);
- p.writeString(mcc);
- p.writeString(mnc);
p.writeString(al);
p.writeString(as);
p.writeInt(ss);
@@ -5073,28 +5133,6 @@ 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
@@ -5320,7 +5358,6 @@ public class RIL extends BaseCommands implements CommandsInterface {
signalStrength.lte.rsrq,
signalStrength.lte.rssnr,
signalStrength.lte.cqi,
- signalStrength.tdScdma.rscp,
- false /* gsmFlag - don't care; will be changed by SST */);
+ signalStrength.tdScdma.rscp);
}
}
diff --git a/com/android/internal/telephony/RILConstants.java b/com/android/internal/telephony/RILConstants.java
index f804cb06..cdee9e6f 100644
--- a/com/android/internal/telephony/RILConstants.java
+++ b/com/android/internal/telephony/RILConstants.java
@@ -105,6 +105,8 @@ public interface RILConstants {
int DEVICE_IN_USE = 64; /* Operation cannot be performed because the device
is currently in use */
int ABORTED = 65; /* Operation aborted */
+ int INVALID_RESPONSE = 66; /* Invalid response sent by vendor code */
+
// Below is list of OEM specific error codes which can by used by OEMs in case they don't want to
// reveal particular replacement for Generic failure
int OEM_ERROR_1 = 501;
@@ -419,6 +421,8 @@ cat include/telephony/ril.h | \
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_REQUEST_START_KEEPALIVE = 146;
+ int RIL_REQUEST_STOP_KEEPALIVE = 147;
int RIL_RESPONSE_ACKNOWLEDGEMENT = 800;
@@ -474,4 +478,5 @@ cat include/telephony/ril.h | \
int RIL_UNSOL_CARRIER_INFO_IMSI_ENCRYPTION = 1048;
int RIL_UNSOL_NETWORK_SCAN_RESULT = 1049;
int RIL_UNSOL_ICC_SLOT_STATUS = 1050;
+ int RIL_UNSOL_KEEPALIVE_STATUS = 1051;
}
diff --git a/com/android/internal/telephony/RILRequest.java b/com/android/internal/telephony/RILRequest.java
index c0a0b70d..ffe4a822 100644
--- a/com/android/internal/telephony/RILRequest.java
+++ b/com/android/internal/telephony/RILRequest.java
@@ -20,8 +20,10 @@ import android.os.AsyncResult;
import android.os.Message;
import android.os.SystemClock;
import android.os.WorkSource;
+import android.os.WorkSource.WorkChain;
import android.telephony.Rlog;
+import java.util.ArrayList;
import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;
@@ -110,13 +112,14 @@ public class RILRequest {
* @param workSource WorkSource to track the client
* @return a RILRequest instance from the pool.
*/
- static RILRequest obtain(int request, Message result, WorkSource workSource) {
+ // @VisibleForTesting
+ public static RILRequest obtain(int request, Message result, WorkSource workSource) {
RILRequest rr = null;
rr = obtain(request, result);
if (workSource != null) {
rr.mWorkSource = workSource;
- rr.mClientId = String.valueOf(workSource.get(0)) + ":" + workSource.getName(0);
+ rr.mClientId = rr.getWorkSourceClientId();
} else {
Rlog.e(LOG_TAG, "null workSource " + request);
}
@@ -125,6 +128,28 @@ public class RILRequest {
}
/**
+ * Generate a String client ID from the WorkSource.
+ */
+ // @VisibleForTesting
+ public String getWorkSourceClientId() {
+ if (mWorkSource == null || mWorkSource.isEmpty()) {
+ return null;
+ }
+
+ if (mWorkSource.size() > 0) {
+ return mWorkSource.get(0) + ":" + mWorkSource.getName(0);
+ }
+
+ final ArrayList<WorkChain> workChains = mWorkSource.getWorkChains();
+ if (workChains != null && !workChains.isEmpty()) {
+ final WorkChain workChain = workChains.get(0);
+ return workChain.getAttributionUid() + ":" + workChain.getTags()[0];
+ }
+
+ return null;
+ }
+
+ /**
* Returns a RILRequest instance to the pool.
*
* Note: This should only be called once per use.
diff --git a/com/android/internal/telephony/RadioIndication.java b/com/android/internal/telephony/RadioIndication.java
index 1e360c0f..8d77982e 100644
--- a/com/android/internal/telephony/RadioIndication.java
+++ b/com/android/internal/telephony/RadioIndication.java
@@ -28,7 +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_KEEPALIVE_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;
@@ -81,8 +81,8 @@ 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.KeepaliveStatus;
import android.hardware.radio.V1_2.IRadioIndication;
+import android.hardware.radio.V1_2.PhysicalChannelConfig;
import android.os.AsyncResult;
import android.os.SystemProperties;
import android.telephony.CellInfo;
@@ -94,11 +94,11 @@ 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.KeepaliveStatus;
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;
@@ -235,6 +235,22 @@ public class RadioIndication extends IRadioIndication.Stub {
}
}
+ /**
+ * Indicates current link capacity estimate.
+ */
+ public void currentLinkCapacityEstimate(int indicationType,
+ android.hardware.radio.V1_2.LinkCapacityEstimate lce) {
+ // TODO(b/70638175) Implement method.
+ }
+
+ /**
+ * Indicates current physical channel configuration.
+ */
+ public void currentPhysicalChannelConfigs(int indicationType,
+ ArrayList<PhysicalChannelConfig> configs) {
+ // TODO(b/70638175) Implement method.
+ }
+
public void dataCallListChanged(int indicationType, ArrayList<SetupDataCallResult> dcList) {
mRil.processIndication(indicationType);
@@ -625,6 +641,7 @@ public class RadioIndication extends IRadioIndication.Stub {
new AsyncResult (null, response, null));
}
+ /** Get unsolicited message for cellInfoList */
public void cellInfoList(int indicationType,
ArrayList<android.hardware.radio.V1_0.CellInfo> records) {
mRil.processIndication(indicationType);
@@ -636,21 +653,16 @@ public class RadioIndication extends IRadioIndication.Stub {
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) {
+ /** Get unsolicited message for cellInfoList using HAL V1_2 */
+ public void cellInfoList_1_2(int indicationType,
+ ArrayList<android.hardware.radio.V1_2.CellInfo> records) {
mRil.processIndication(indicationType);
- ArrayList<IccSlotStatus> iccSlotStatus = RIL.convertHalSlotsStatus(slotsStatus);
+ ArrayList<CellInfo> response = RIL.convertHalCellInfoList_1_2(records);
- if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_ICC_SLOT_STATUS, iccSlotStatus);
+ if (RIL.RILJ_LOGD) mRil.unsljLogRet(RIL_UNSOL_CELL_INFO_LIST, response);
- mRil.mIccStatusChangedRegistrants.notifyRegistrants(
- new AsyncResult(null, iccSlotStatus, null));
+ mRil.mRilCellInfoListRegistrants.notifyRegistrants(new AsyncResult(null, response, null));
}
/** Incremental network scan results */
@@ -832,10 +844,19 @@ public class RadioIndication extends IRadioIndication.Stub {
/**
* Indicates a change in the status of an ongoing Keepalive session
* @param indicationType RadioIndicationType
- * @param keepaliveStatus Status of the ongoing Keepalive session
+ * @param halStatus Status of the ongoing Keepalive session
*/
- public void keepaliveStatus(int indicationType, KeepaliveStatus keepaliveStatus) {
- throw new UnsupportedOperationException("keepaliveStatus Indications are not implemented");
+ public void keepaliveStatus(
+ int indicationType, android.hardware.radio.V1_1.KeepaliveStatus halStatus) {
+ mRil.processIndication(indicationType);
+
+ if (RIL.RILJ_LOGD) {
+ mRil.unsljLogRet(RIL_UNSOL_KEEPALIVE_STATUS,
+ "handle=" + halStatus.sessionHandle + " code=" + halStatus.code);
+ }
+
+ KeepaliveStatus ks = new KeepaliveStatus(halStatus.sessionHandle, halStatus.code);
+ mRil.mNattKeepaliveStatusRegistrants.notifyRegistrants(new AsyncResult(null, ks, null));
}
private CommandsInterface.RadioState getRadioStateFromInt(int stateInt) {
diff --git a/com/android/internal/telephony/RadioResponse.java b/com/android/internal/telephony/RadioResponse.java
index 53f4f91b..4cf99451 100644
--- a/com/android/internal/telephony/RadioResponse.java
+++ b/com/android/internal/telephony/RadioResponse.java
@@ -33,9 +33,7 @@ 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.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;
@@ -50,11 +48,11 @@ import android.telephony.TelephonyManager;
import android.telephony.data.DataCallResponse;
import android.text.TextUtils;
+import com.android.internal.telephony.dataconnection.KeepaliveStatus;
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;
@@ -116,22 +114,6 @@ public class RadioResponse extends IRadioResponse.Stub {
/**
* @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) {
@@ -1248,24 +1230,96 @@ public class RadioResponse extends IRadioResponse.Stub {
/**
* @param responseInfo Response info struct containing response type, serial no. and error
*/
+ public void setSignalStrengthReportingCriteriaResponse(RadioResponseInfo responseInfo) {
+ responseVoid(responseInfo);
+ }
+
+ /**
+ * @param responseInfo Response info struct containing response type, serial no. and error
+ */
+ public void setLinkCapacityReportingCriteriaResponse(RadioResponseInfo responseInfo) {
+ responseVoid(responseInfo);
+ }
+
+ /**
+ * @param responseInfo Response info struct containing response type, serial no. and error
+ */
public void setSimCardPowerResponse_1_1(RadioResponseInfo responseInfo) {
responseVoid(responseInfo);
}
+
/**
* @param responseInfo Response info struct containing response type, serial no. and error
* @param keepaliveStatus status of the keepalive with a handle for the session
*/
public void startKeepaliveResponse(RadioResponseInfo responseInfo,
- KeepaliveStatus keepaliveStatus) {
- throw new UnsupportedOperationException("startKeepaliveResponse not implemented");
+ android.hardware.radio.V1_1.KeepaliveStatus keepaliveStatus) {
+
+ RILRequest rr = mRil.processResponse(responseInfo);
+
+ if (rr == null) {
+ return;
+ }
+
+ KeepaliveStatus ret = null;
+
+ switch(responseInfo.error) {
+ case RadioError.NONE:
+ int convertedStatus = convertHalKeepaliveStatusCode(keepaliveStatus.code);
+ if (convertedStatus < 0) {
+ ret = new KeepaliveStatus(KeepaliveStatus.ERROR_UNSUPPORTED);
+ } else {
+ ret = new KeepaliveStatus(keepaliveStatus.sessionHandle, convertedStatus);
+ }
+ break;
+ case RadioError.REQUEST_NOT_SUPPORTED:
+ ret = new KeepaliveStatus(KeepaliveStatus.ERROR_UNSUPPORTED);
+ // The request is unsupported, which is ok. We'll report it to the higher
+ // layer and treat it as acceptable in the RIL.
+ responseInfo.error = RadioError.NONE;
+ break;
+ case RadioError.NO_RESOURCES:
+ ret = new KeepaliveStatus(KeepaliveStatus.ERROR_NO_RESOURCES);
+ break;
+ default:
+ ret = new KeepaliveStatus(KeepaliveStatus.ERROR_UNKNOWN);
+ break;
+ }
+ sendMessageResponse(rr.mResult, ret);
+ mRil.processResponseDone(rr, responseInfo, ret);
}
/**
* @param responseInfo Response info struct containing response type, serial no. and error
*/
public void stopKeepaliveResponse(RadioResponseInfo responseInfo) {
- throw new UnsupportedOperationException("stopKeepaliveResponse not implemented");
+ RILRequest rr = mRil.processResponse(responseInfo);
+
+ if (rr == null) {
+ return;
+ }
+
+ if (responseInfo.error == RadioError.NONE) {
+ sendMessageResponse(rr.mResult, null);
+ mRil.processResponseDone(rr, responseInfo, null);
+ } else {
+ //TODO: Error code translation
+ }
+ }
+
+ private int convertHalKeepaliveStatusCode(int halCode) {
+ switch (halCode) {
+ case android.hardware.radio.V1_1.KeepaliveStatusCode.ACTIVE:
+ return KeepaliveStatus.STATUS_ACTIVE;
+ case android.hardware.radio.V1_1.KeepaliveStatusCode.INACTIVE:
+ return KeepaliveStatus.STATUS_INACTIVE;
+ case android.hardware.radio.V1_1.KeepaliveStatusCode.PENDING:
+ return KeepaliveStatus.STATUS_PENDING;
+ default:
+ mRil.riljLog("Invalid Keepalive Status" + halCode);
+ return -1;
+ }
}
private IccCardStatus convertHalCardStatus(CardStatus cardStatus) {
@@ -1297,6 +1351,7 @@ public class RadioResponse extends IRadioResponse.Stub {
appStatus.pin1 = appStatus.PinStateFromRILInt(rilAppStatus.pin1);
appStatus.pin2 = appStatus.PinStateFromRILInt(rilAppStatus.pin2);
iccCardStatus.mApplications[i] = appStatus;
+ mRil.riljLog("IccCardApplicationStatus " + i + ":" + appStatus.toString());
}
return iccCardStatus;
}
@@ -1331,20 +1386,6 @@ 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 2ec51010..0c160e29 100644
--- a/com/android/internal/telephony/SMSDispatcher.java
+++ b/com/android/internal/telephony/SMSDispatcher.java
@@ -61,6 +61,7 @@ import android.telephony.CarrierMessagingServiceManager;
import android.telephony.PhoneNumberUtils;
import android.telephony.Rlog;
import android.telephony.ServiceState;
+import android.telephony.SmsManager;
import android.telephony.TelephonyManager;
import android.text.Html;
import android.text.Spanned;
@@ -77,6 +78,7 @@ import android.widget.TextView;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.cdma.sms.UserData;
import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
import com.android.internal.telephony.uicc.UiccCard;
import com.android.internal.telephony.uicc.UiccController;
@@ -92,6 +94,13 @@ public abstract class SMSDispatcher extends Handler {
static final String TAG = "SMSDispatcher"; // accessed from inner class
static final boolean DBG = false;
private static final String SEND_NEXT_MSG_EXTRA = "SendNextMsg";
+ protected static final String MAP_KEY_PDU = "pdu";
+ protected static final String MAP_KEY_SMSC = "smsc";
+ protected static final String MAP_KEY_DEST_ADDR = "destAddr";
+ protected static final String MAP_KEY_SC_ADDR = "scAddr";
+ protected static final String MAP_KEY_DEST_PORT = "destPort";
+ protected static final String MAP_KEY_DATA = "data";
+ protected static final String MAP_KEY_TEXT = "text";
private static final int PREMIUM_RULE_USE_SIM = 1;
private static final int PREMIUM_RULE_USE_NETWORK = 2;
@@ -123,18 +132,11 @@ public abstract class SMSDispatcher extends Handler {
/** Handle status report from {@code CdmaInboundSmsHandler}. */
protected static final int EVENT_HANDLE_STATUS_REPORT = 10;
- /** Radio is ON */
- protected static final int EVENT_RADIO_ON = 11;
-
- /** IMS registration/SMS format changed */
- protected static final int EVENT_IMS_STATE_CHANGED = 12;
-
- /** Callback from RIL_REQUEST_IMS_REGISTRATION_STATE */
- protected static final int EVENT_IMS_STATE_DONE = 13;
-
// other
protected static final int EVENT_NEW_ICC_SMS = 14;
protected static final int EVENT_ICC_CHANGED = 15;
+ protected static final int EVENT_GET_IMS_SERVICE = 16;
+
protected Phone mPhone;
protected final Context mContext;
@@ -159,10 +161,7 @@ public abstract class SMSDispatcher extends Handler {
*/
private static int sConcatenatedRef = new Random().nextInt(256);
- /** Outgoing message counter. Shared by all dispatchers. */
- private SmsUsageMonitor mUsageMonitor;
-
- private ImsSMSDispatcher mImsSMSDispatcher;
+ protected SmsDispatchersController mSmsDispatchersController;
/** Number of outgoing SmsTrackers waiting for user confirmation. */
private int mPendingTrackerCount;
@@ -179,16 +178,13 @@ public abstract class SMSDispatcher extends Handler {
/**
* Create a new SMS dispatcher.
* @param phone the Phone to use
- * @param usageMonitor the SmsUsageMonitor to use
*/
- protected SMSDispatcher(Phone phone, SmsUsageMonitor usageMonitor,
- ImsSMSDispatcher imsSMSDispatcher) {
+ protected SMSDispatcher(Phone phone, SmsDispatchersController smsDispatchersController) {
mPhone = phone;
- mImsSMSDispatcher = imsSMSDispatcher;
+ mSmsDispatchersController = smsDispatchersController;
mContext = phone.getContext();
mResolver = mContext.getContentResolver();
mCi = phone.mCi;
- mUsageMonitor = usageMonitor;
mTelephonyManager = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
mSettingsObserver = new SettingsObserver(this, mPremiumSmsRule, mContext);
mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
@@ -224,7 +220,6 @@ public abstract class SMSDispatcher extends Handler {
protected void updatePhoneObject(Phone phone) {
mPhone = phone;
- mUsageMonitor = phone.mSmsUsageMonitor;
Rlog.d(TAG, "Active phone changed to " + mPhone.getPhoneName() );
}
@@ -392,7 +387,7 @@ public abstract class SMSDispatcher extends Handler {
@Override
protected void onServiceReady(ICarrierMessagingService carrierMessagingService) {
HashMap<String, Object> map = mTracker.getData();
- String text = (String) map.get("text");
+ String text = (String) map.get(MAP_KEY_TEXT);
if (text != null) {
try {
@@ -424,8 +419,8 @@ public abstract class SMSDispatcher extends Handler {
@Override
protected void onServiceReady(ICarrierMessagingService carrierMessagingService) {
HashMap<String, Object> map = mTracker.getData();
- byte[] data = (byte[]) map.get("data");
- int destPort = (int) map.get("destPort");
+ byte[] data = (byte[]) map.get(MAP_KEY_DATA);
+ int destPort = (int) map.get(MAP_KEY_DEST_PORT);
if (data != null) {
try {
@@ -630,7 +625,19 @@ public abstract class SMSDispatcher extends Handler {
/**
* Send an SMS PDU. Usually just calls {@link sendRawPdu}.
*/
- protected abstract void sendSubmitPdu(SmsTracker tracker);
+ private void sendSubmitPdu(SmsTracker tracker) {
+ if (shouldBlockSms()) {
+ Rlog.d(TAG, "Block SMS in Emergency Callback mode");
+ tracker.onFailed(mContext, SmsManager.RESULT_ERROR_NO_SERVICE, 0/*errorCode*/);
+ } else {
+ sendRawPdu(tracker);
+ }
+ }
+
+ /**
+ * @return true if MO SMS should be blocked.
+ */
+ protected abstract boolean shouldBlockSms();
/**
* Called when SMS send completes. Broadcasts a sentIntent on success.
@@ -727,7 +734,9 @@ public abstract class SMSDispatcher extends Handler {
} else {
sentIntent.send(RESULT_ERROR_NO_SERVICE);
}
- } catch (CanceledException ex) {}
+ } catch (CanceledException ex) {
+ Rlog.e(TAG, "Failed to send result");
+ }
}
}
@@ -768,8 +777,25 @@ public abstract class SMSDispatcher extends Handler {
* broadcast when the message is delivered to the recipient. The
* raw pdu of the status report is in the extended data ("pdu").
*/
- protected abstract void sendData(String destAddr, String scAddr, int destPort,
- byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent);
+ protected void sendData(String destAddr, String scAddr, int destPort,
+ byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
+ SmsMessageBase.SubmitPduBase pdu = getSubmitPdu(
+ scAddr, destAddr, destPort, data, (deliveryIntent != null));
+ if (pdu != null) {
+ HashMap map = getSmsTrackerMap(destAddr, scAddr, destPort, data, pdu);
+ SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
+ null /*messageUri*/, false /*isExpectMore*/,
+ null /*fullMessageText*/, false /*isText*/,
+ true /*persistMessage*/);
+
+ if (!sendSmsByCarrierApp(true /* isDataSms */, tracker)) {
+ sendSubmitPdu(tracker);
+ }
+ } else {
+ Rlog.e(TAG, "SMSDispatcher.sendData(): getSubmitPdu() returned null");
+ triggerSentIntentForFailure(sentIntent);
+ }
+ }
/**
* Send a text based SMS.
@@ -798,21 +824,59 @@ public abstract class SMSDispatcher extends Handler {
* @param persistMessage whether to save the sent message into SMS DB for a
* non-default SMS app.
*/
- protected abstract void sendText(String destAddr, String scAddr, String text,
- PendingIntent sentIntent, PendingIntent deliveryIntent, Uri messageUri,
- String callingPkg, boolean persistMessage);
+ public void sendText(String destAddr, String scAddr, String text,
+ PendingIntent sentIntent, PendingIntent deliveryIntent, Uri messageUri,
+ String callingPkg, boolean persistMessage) {
+ Rlog.d(TAG, "sendText");
+ SmsMessageBase.SubmitPduBase pdu = getSubmitPdu(
+ scAddr, destAddr, text, (deliveryIntent != null), null);
+ if (pdu != null) {
+ HashMap map = getSmsTrackerMap(destAddr, scAddr, text, pdu);
+ SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
+ messageUri, false /*isExpectMore*/, text, true /*isText*/,
+ persistMessage);
+
+ if (!sendSmsByCarrierApp(false /* isDataSms */, tracker)) {
+ sendSubmitPdu(tracker);
+ }
+ } else {
+ Rlog.e(TAG, "SmsDispatcher.sendText(): getSubmitPdu() returned null");
+ triggerSentIntentForFailure(sentIntent);
+ }
+ }
- /**
- * Inject an SMS PDU into the android platform.
- *
- * @param pdu is the byte array of pdu to be injected into android telephony layer
- * @param format is the format of SMS pdu (3gpp or 3gpp2)
- * @param receivedIntent if not NULL this <code>PendingIntent</code> is
- * broadcast when the message is successfully received by the
- * android telephony layer. This intent is broadcasted at
- * the same time an SMS received from radio is responded back.
- */
- protected abstract void injectSmsPdu(byte[] pdu, String format, PendingIntent receivedIntent);
+ private void triggerSentIntentForFailure(PendingIntent sentIntent) {
+ if (sentIntent != null) {
+ try {
+ sentIntent.send(SmsManager.RESULT_ERROR_GENERIC_FAILURE);
+ } catch (CanceledException ex) {
+ Rlog.e(TAG, "Intent has been canceled!");
+ }
+ }
+ }
+
+ private boolean sendSmsByCarrierApp(boolean isDataSms, SmsTracker tracker ) {
+ String carrierPackage = getCarrierAppPackageName();
+ if (carrierPackage != null) {
+ Rlog.d(TAG, "Found carrier package.");
+ SmsSender smsSender;
+ if (isDataSms) {
+ smsSender = new DataSmsSender(tracker);
+ } else {
+ smsSender = new TextSmsSender(tracker);
+ }
+ smsSender.sendSmsByCarrierApp(carrierPackage, new SmsSenderCallback(smsSender));
+ return true;
+ }
+
+ return false;
+ }
+
+ protected abstract SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
+ String message, boolean statusReportRequested, SmsHeader smsHeader);
+
+ protected abstract SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
+ int destPort, byte[] message, boolean statusReportRequested);
/**
* Calculate the number of septets needed to encode the message. This function should only be
@@ -927,7 +991,8 @@ public abstract class SMSDispatcher extends Handler {
if (carrierPackage != null) {
Rlog.d(TAG, "Found carrier package.");
MultipartSmsSender smsSender = new MultipartSmsSender(parts, trackers);
- smsSender.sendSmsByCarrierApp(carrierPackage, new MultipartSmsSenderCallback(smsSender));
+ smsSender.sendSmsByCarrierApp(carrierPackage,
+ new MultipartSmsSenderCallback(smsSender));
} else {
Rlog.v(TAG, "No carrier package.");
for (SmsTracker tracker : trackers) {
@@ -943,11 +1008,57 @@ public abstract class SMSDispatcher extends Handler {
/**
* Create a new SubmitPdu and return the SMS tracker.
*/
- protected abstract SmsTracker getNewSubmitPduTracker(String destinationAddress, String scAddress,
+ private 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);
+ String fullMessageText) {
+ if (isCdmaMo()) {
+ UserData uData = new UserData();
+ uData.payloadStr = message;
+ uData.userDataHeader = smsHeader;
+ if (encoding == SmsConstants.ENCODING_7BIT) {
+ uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET;
+ } else { // assume UTF-16
+ uData.msgEncoding = UserData.ENCODING_UNICODE_16;
+ }
+ uData.msgEncodingSet = true;
+
+ /* By setting the statusReportRequested bit only for the
+ * last message fragment, this will result in only one
+ * callback to the sender when that last fragment delivery
+ * has been acknowledged. */
+ //TODO FIX
+ SmsMessageBase.SubmitPduBase submitPdu =
+ com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(destinationAddress,
+ uData, (deliveryIntent != null) && lastPart);
+
+ HashMap map = getSmsTrackerMap(destinationAddress, scAddress,
+ message, submitPdu);
+ return getSmsTracker(map, sentIntent, deliveryIntent,
+ getFormat(), unsentPartCount, anyPartFailed, messageUri, smsHeader,
+ false /*isExpectMore*/, fullMessageText, true /*isText*/,
+ true /*persistMessage*/);
+
+ } else {
+ SmsMessageBase.SubmitPduBase pdu =
+ com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(scAddress,
+ destinationAddress, message, deliveryIntent != null,
+ SmsHeader.toByteArray(smsHeader), 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, fullMessageText, true /*isText*/,
+ false /*persistMessage*/);
+ } else {
+ Rlog.e(TAG, "GsmSMSDispatcher.sendNewSubmitPdu(): getSubmitPdu() returned null");
+ return null;
+ }
+ }
+ }
/**
* Send an SMS
@@ -974,7 +1085,7 @@ public abstract class SMSDispatcher extends Handler {
@VisibleForTesting
public void sendRawPdu(SmsTracker tracker) {
HashMap map = tracker.getData();
- byte pdu[] = (byte[]) map.get("pdu");
+ byte pdu[] = (byte[]) map.get(MAP_KEY_PDU);
if (mSmsSendDisabled) {
Rlog.e(TAG, "Device does not support sending sms.");
@@ -1016,7 +1127,8 @@ public abstract class SMSDispatcher extends Handler {
// handler with the SmsTracker to request user confirmation before sending.
if (checkDestination(tracker)) {
// check for excessive outgoing SMS usage by this app
- if (!mUsageMonitor.check(appInfo.packageName, SINGLE_PART_SMS)) {
+ if (!mSmsDispatchersController.getUsageMonitor().check(
+ appInfo.packageName, SINGLE_PART_SMS)) {
sendMessage(obtainMessage(EVENT_SEND_LIMIT_REACHED_CONFIRMATION, tracker));
return;
}
@@ -1050,7 +1162,8 @@ public abstract class SMSDispatcher extends Handler {
simCountryIso = mTelephonyManager.getNetworkCountryIso();
}
- smsCategory = mUsageMonitor.checkDestination(tracker.mDestAddress, simCountryIso);
+ smsCategory = mSmsDispatchersController.getUsageMonitor().checkDestination(
+ tracker.mDestAddress, simCountryIso);
}
if (rule == PREMIUM_RULE_USE_NETWORK || rule == PREMIUM_RULE_USE_BOTH) {
String networkCountryIso = mTelephonyManager.getNetworkCountryIso();
@@ -1060,7 +1173,8 @@ public abstract class SMSDispatcher extends Handler {
}
smsCategory = SmsUsageMonitor.mergeShortCodeCategories(smsCategory,
- mUsageMonitor.checkDestination(tracker.mDestAddress, networkCountryIso));
+ mSmsDispatchersController.getUsageMonitor().checkDestination(
+ tracker.mDestAddress, networkCountryIso));
}
if (smsCategory == SmsUsageMonitor.CATEGORY_NOT_SHORT_CODE
@@ -1076,7 +1190,8 @@ public abstract class SMSDispatcher extends Handler {
}
// Wait for user confirmation unless the user has set permission to always allow/deny
- int premiumSmsPermission = mUsageMonitor.getPremiumSmsPermission(
+ int premiumSmsPermission =
+ mSmsDispatchersController.getUsageMonitor().getPremiumSmsPermission(
tracker.getAppPackageName());
if (premiumSmsPermission == SmsUsageMonitor.PREMIUM_SMS_PERMISSION_UNKNOWN) {
// First time trying to send to premium SMS.
@@ -1233,32 +1348,6 @@ public abstract class SMSDispatcher extends Handler {
}
/**
- * Returns the premium SMS permission for the specified package. If the package has never
- * been seen before, the default {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ASK_USER}
- * will be returned.
- * @param packageName the name of the package to query permission
- * @return one of {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_UNKNOWN},
- * {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ASK_USER},
- * {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_NEVER_ALLOW}, or
- * {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW}
- */
- public int getPremiumSmsPermission(String packageName) {
- return mUsageMonitor.getPremiumSmsPermission(packageName);
- }
-
- /**
- * Sets the premium SMS permission for the specified package and save the value asynchronously
- * to persistent storage.
- * @param packageName the name of the package to set permission
- * @param permission one of {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ASK_USER},
- * {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_NEVER_ALLOW}, or
- * {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW}
- */
- public void setPremiumSmsPermission(String packageName, int permission) {
- mUsageMonitor.setPremiumSmsPermission(packageName, permission);
- }
-
- /**
* Send the message along to the radio.
*
* @param tracker holds the SMS message to send
@@ -1266,23 +1355,16 @@ public abstract class SMSDispatcher extends Handler {
protected abstract void sendSms(SmsTracker tracker);
/**
- * Send the SMS via the PSTN network.
- *
- * @param tracker holds the Sms tracker ready to be sent
- */
- protected abstract void sendSmsByPstn(SmsTracker tracker);
-
- /**
* Retry the message along to the radio.
*
* @param tracker holds the SMS message to send
*/
public void sendRetrySms(SmsTracker tracker) {
- // re-routing to ImsSMSDispatcher
- if (mImsSMSDispatcher != null) {
- mImsSMSDispatcher.sendRetrySms(tracker);
+ // re-routing to SmsDispatchersController
+ if (mSmsDispatchersController != null) {
+ mSmsDispatchersController.sendRetrySms(tracker);
} else {
- Rlog.e(TAG, mImsSMSDispatcher + " is null. Retry failed");
+ Rlog.e(TAG, mSmsDispatchersController + " is null. Retry failed");
}
}
@@ -1320,7 +1402,8 @@ public abstract class SMSDispatcher extends Handler {
}
sendMultipartText(destinationAddress, scAddress, parts, sentIntents, deliveryIntents,
- null/*messageUri*/, null/*callingPkg*/, tracker.mPersistMessage);
+ null/*messageUri*/, null/*callingPkg*/,
+ tracker.mPersistMessage);
}
/**
@@ -1632,30 +1715,30 @@ public abstract class SMSDispatcher extends Handler {
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*/, isExpectMore,
- fullMessageText, isText, persistMessage);
+ null/*anyPartFailed*/, messageUri, null/*smsHeader*/,
+ isExpectMore, fullMessageText, isText, persistMessage);
}
protected HashMap<String, Object> getSmsTrackerMap(String destAddr, String scAddr,
String text, SmsMessageBase.SubmitPduBase pdu) {
HashMap<String, Object> map = new HashMap<String, Object>();
- map.put("destAddr", destAddr);
- map.put("scAddr", scAddr);
- map.put("text", text);
- map.put("smsc", pdu.encodedScAddress);
- map.put("pdu", pdu.encodedMessage);
+ map.put(MAP_KEY_DEST_ADDR, destAddr);
+ map.put(MAP_KEY_SC_ADDR, scAddr);
+ map.put(MAP_KEY_TEXT, text);
+ map.put(MAP_KEY_SMSC, pdu.encodedScAddress);
+ map.put(MAP_KEY_PDU, pdu.encodedMessage);
return map;
}
protected HashMap<String, Object> getSmsTrackerMap(String destAddr, String scAddr,
int destPort, byte[] data, SmsMessageBase.SubmitPduBase pdu) {
HashMap<String, Object> map = new HashMap<String, Object>();
- map.put("destAddr", destAddr);
- map.put("scAddr", scAddr);
- map.put("destPort", destPort);
- map.put("data", data);
- map.put("smsc", pdu.encodedScAddress);
- map.put("pdu", pdu.encodedMessage);
+ map.put(MAP_KEY_DEST_ADDR, destAddr);
+ map.put(MAP_KEY_SC_ADDR, scAddr);
+ map.put(MAP_KEY_DEST_PORT, destPort);
+ map.put(MAP_KEY_DATA, data);
+ map.put(MAP_KEY_SMSC, pdu.encodedScAddress);
+ map.put(MAP_KEY_PDU, pdu.encodedMessage);
return map;
}
@@ -1720,7 +1803,8 @@ public abstract class SMSDispatcher extends Handler {
}
sendMessage(msg);
}
- setPremiumSmsPermission(mTracker.getAppPackageName(), newSmsPermission);
+ mSmsDispatchersController.setPremiumSmsPermission(mTracker.getAppPackageName(),
+ newSmsPermission);
}
@Override
@@ -1755,23 +1839,14 @@ public abstract class SMSDispatcher extends Handler {
}
public boolean isIms() {
- if (mImsSMSDispatcher != null) {
- return mImsSMSDispatcher.isIms();
+ if (mSmsDispatchersController != null) {
+ return mSmsDispatchersController.isIms();
} else {
- Rlog.e(TAG, mImsSMSDispatcher + " is null");
+ Rlog.e(TAG, "mSmsDispatchersController is null");
return false;
}
}
- public String getImsSmsFormat() {
- if (mImsSMSDispatcher != null) {
- return mImsSMSDispatcher.getImsSmsFormat();
- } else {
- Rlog.e(TAG, mImsSMSDispatcher + " is null");
- return null;
- }
- }
-
private String getMultipartMessageText(ArrayList<String> parts) {
final StringBuilder sb = new StringBuilder();
for (String part : parts) {
@@ -1819,4 +1894,8 @@ public abstract class SMSDispatcher extends Handler {
throw new SecurityException("Caller is not phone or carrier app!");
}
}
+
+ protected boolean isCdmaMo() {
+ return mSmsDispatchersController.isCdmaMo();
+ }
}
diff --git a/com/android/internal/telephony/ServiceStateTracker.java b/com/android/internal/telephony/ServiceStateTracker.java
index 8d8f6dd2..7c5842f7 100644
--- a/com/android/internal/telephony/ServiceStateTracker.java
+++ b/com/android/internal/telephony/ServiceStateTracker.java
@@ -85,6 +85,7 @@ import com.android.internal.telephony.uicc.SIMRecords;
import com.android.internal.telephony.uicc.UiccCardApplication;
import com.android.internal.telephony.uicc.UiccController;
import com.android.internal.telephony.util.NotificationChannelController;
+import com.android.internal.telephony.util.TimeStampedValue;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.IndentingPrintWriter;
@@ -559,7 +560,7 @@ public class ServiceStateTracker extends Handler {
mMin = null;
mPrlVersion = null;
mIsMinInfoReady = false;
- mNitzState.clearNitzUpdatedTime();
+ mNitzState.handleNetworkUnavailable();
//cancel any pending pollstate request on voice tech switching
cancelPollState();
@@ -578,9 +579,7 @@ public class ServiceStateTracker extends Handler {
mCellLoc = new GsmCellLocation();
mNewCellLoc = new GsmCellLocation();
} else {
- if (mPhone.isPhoneTypeCdmaLte()) {
- mPhone.registerForSimRecordsLoaded(this, EVENT_SIM_RECORDS_LOADED, null);
- }
+ mPhone.registerForSimRecordsLoaded(this, EVENT_SIM_RECORDS_LOADED, null);
mCellLoc = new CdmaCellLocation();
mNewCellLoc = new CdmaCellLocation();
mCdmaSSM = CdmaSubscriptionSourceManager.getInstance(mPhone.getContext(), mCi, this,
@@ -2547,8 +2546,7 @@ public class ServiceStateTracker extends Handler {
mNewSS.setStateOutOfService();
mNewCellLoc.setStateInvalid();
setSignalStrengthDefaultValues();
- mNitzState.setNetworkCountryIsoAvailable(false);
- mNitzState.clearNitzUpdatedTime();
+ mNitzState.handleNetworkUnavailable();
pollStateDone();
break;
@@ -2556,8 +2554,7 @@ public class ServiceStateTracker extends Handler {
mNewSS.setStateOff();
mNewCellLoc.setStateInvalid();
setSignalStrengthDefaultValues();
- mNitzState.setNetworkCountryIsoAvailable(false);
- mNitzState.clearNitzUpdatedTime();
+ mNitzState.handleNetworkUnavailable();
// 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 ||
@@ -2788,17 +2785,12 @@ public class ServiceStateTracker extends Handler {
if (hasRegistered) {
mNetworkAttachedRegistrants.notifyRegistrants();
-
- if (DBG) {
- log("pollStateDone: hasRegistered, current mNitzState.getNitzUpdatedTime()="
- + mNitzState.getNitzUpdatedTime()
- + ". Calling mNitzState.clearNitzUpdatedTime()");
- }
- mNitzState.clearNitzUpdatedTime();
+ mNitzState.handleNetworkAvailable();
}
if (hasDeregistered) {
mNetworkDetachedRegistrants.notifyRegistrants();
+ mNitzState.handleNetworkUnavailable();
}
if (hasRejectCauseChanged) {
@@ -2811,6 +2803,7 @@ public class ServiceStateTracker extends Handler {
tm.setNetworkOperatorNameForPhone(mPhone.getPhoneId(), mSS.getOperatorAlpha());
String prevOperatorNumeric = tm.getNetworkOperatorForPhone(mPhone.getPhoneId());
+ String prevCountryIsoCode = tm.getNetworkCountryIso(mPhone.getPhoneId());
String operatorNumeric = mSS.getOperatorNumeric();
if (!mPhone.isPhoneTypeGsm()) {
@@ -2827,48 +2820,47 @@ public class ServiceStateTracker extends Handler {
if (isInvalidOperatorNumeric(operatorNumeric)) {
if (DBG) log("operatorNumeric " + operatorNumeric + " is invalid");
tm.setNetworkCountryIsoForPhone(mPhone.getPhoneId(), "");
- mNitzState.setNetworkCountryIsoAvailable(false);
- mNitzState.clearNitzUpdatedTime();
+ mNitzState.handleNetworkUnavailable();
} else if (mSS.getRilDataRadioTechnology() != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN) {
- // Update time zone, ISO, and IDD.
- //
// If the device is on IWLAN, modems manufacture a ServiceState with the MCC/MNC of
// the SIM as if we were talking to towers. Telephony code then uses that with
// mccTable to suggest a timezone. We shouldn't do that if the MCC/MNC is from IWLAN
- String iso = "";
- String mcc = "";
+ // Update IDD.
+ if (!mPhone.isPhoneTypeGsm()) {
+ setOperatorIdd(operatorNumeric);
+ }
+
+ // Update ISO.
+ String countryIsoCode = "";
try {
- mcc = operatorNumeric.substring(0, 3);
- iso = MccTable.countryCodeForMcc(Integer.parseInt(mcc));
+ String mcc = operatorNumeric.substring(0, 3);
+ countryIsoCode = MccTable.countryCodeForMcc(Integer.parseInt(mcc));
} catch (NumberFormatException | StringIndexOutOfBoundsException ex) {
loge("pollStateDone: countryCodeForMcc error: " + ex);
}
+ tm.setNetworkCountryIsoForPhone(mPhone.getPhoneId(), countryIsoCode);
- tm.setNetworkCountryIsoForPhone(mPhone.getPhoneId(), iso);
- mNitzState.setNetworkCountryIsoAvailable(true);
-
- if (!mcc.equals("000")
- && !TextUtils.isEmpty(iso)
- && mNitzState.shouldUpdateTimeZoneUsingCountryCode()) {
- mNitzState.updateTimeZoneByNetworkCountryCode(iso);
- }
-
- if (!mPhone.isPhoneTypeGsm()) {
- setOperatorIdd(operatorNumeric);
- }
+ // Update Time Zone.
+ boolean iccCardExists = iccCardExists();
+ boolean networkIsoChanged =
+ networkCountryIsoChanged(countryIsoCode, prevCountryIsoCode);
- 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);
+ // Determine countryChanged: networkIso is only reliable if there's an ICC card.
+ boolean countryChanged = iccCardExists && networkIsoChanged;
+ if (DBG) {
+ long ctm = System.currentTimeMillis();
+ log("Before handleNetworkCountryCodeKnown:"
+ + " countryChanged=" + countryChanged
+ + " iccCardExist=" + iccCardExists
+ + " countryIsoChanged=" + networkIsoChanged
+ + " operatorNumeric=" + operatorNumeric
+ + " prevOperatorNumeric=" + prevOperatorNumeric
+ + " countryIsoCode=" + countryIsoCode
+ + " prevCountryIsoCode=" + prevCountryIsoCode
+ + " ltod=" + TimeUtils.logTimeOfDay(ctm));
}
+ mNitzState.handleNetworkCountryCodeSet(countryChanged);
}
tm.setNetworkRoamingForPhone(mPhone.getPhoneId(),
@@ -3091,7 +3083,7 @@ public class ServiceStateTracker extends Handler {
if (lastNitzData == null) {
tzone = null;
} else {
- tzone = NitzData.guessTimeZone(lastNitzData);
+ tzone = TimeZoneLookupHelper.guessZoneByNitzStatic(lastNitzData);
if (ServiceStateTracker.DBG) {
log("fixUnknownMcc(): guessNitzTimeZone returned "
+ (tzone == null ? tzone : tzone.getID()));
@@ -3432,7 +3424,9 @@ public class ServiceStateTracker extends Handler {
NitzData newNitzData = NitzData.parse(nitzString);
if (newNitzData != null) {
try {
- mNitzState.setTimeAndTimeZoneFromNitz(newNitzData, nitzReceiveTime);
+ TimeStampedValue<NitzData> nitzSignal =
+ new TimeStampedValue<>(newNitzData, nitzReceiveTime);
+ mNitzState.handleNitzReceived(nitzSignal);
} finally {
if (DBG) {
long end = SystemClock.elapsedRealtime();
@@ -3772,86 +3766,44 @@ public class ServiceStateTracker extends Handler {
public void powerOffRadioSafely(DcTracker dcTracker) {
synchronized (this) {
if (!mPendingRadioPowerOffAfterDataOff) {
- if (mPhone.isPhoneTypeGsm() || mPhone.isPhoneTypeCdmaLte()) {
- int dds = SubscriptionManager.getDefaultDataSubscriptionId();
- // To minimize race conditions we call cleanUpAllConnections on
- // both if else paths instead of before this isDisconnected test.
- if (dcTracker.isDisconnected()
- && (dds == mPhone.getSubId()
- || (dds != mPhone.getSubId()
- && ProxyController.getInstance().isDataDisconnected(dds)))) {
- // To minimize race conditions we do this after isDisconnected
- dcTracker.cleanUpAllConnections(Phone.REASON_RADIO_TURNED_OFF);
- if (DBG) log("Data disconnected, turn off radio right away.");
- hangupAndPowerOff();
- } else {
- // hang up all active voice calls first
- if (mPhone.isPhoneTypeGsm() && mPhone.isInCall()) {
- mPhone.mCT.mRingingCall.hangupIfAlive();
- mPhone.mCT.mBackgroundCall.hangupIfAlive();
- mPhone.mCT.mForegroundCall.hangupIfAlive();
- }
- dcTracker.cleanUpAllConnections(Phone.REASON_RADIO_TURNED_OFF);
- if (dds != mPhone.getSubId()
- && !ProxyController.getInstance().isDataDisconnected(dds)) {
- if (DBG) log("Data is active on DDS. Wait for all data disconnect");
- // Data is not disconnected on DDS. Wait for the data disconnect complete
- // before sending the RADIO_POWER off.
- ProxyController.getInstance().registerForAllDataDisconnected(dds, this,
- EVENT_ALL_DATA_DISCONNECTED, null);
- mPendingRadioPowerOffAfterDataOff = true;
- }
- Message msg = Message.obtain(this);
- msg.what = EVENT_SET_RADIO_POWER_OFF;
- msg.arg1 = ++mPendingRadioPowerOffAfterDataOffTag;
- if (sendMessageDelayed(msg, 30000)) {
- if (DBG) log("Wait upto 30s for data to disconnect, then turn off radio.");
- mPendingRadioPowerOffAfterDataOff = true;
- } else {
- log("Cannot send delayed Msg, turn off radio right away.");
- hangupAndPowerOff();
- mPendingRadioPowerOffAfterDataOff = false;
- }
- }
+ int dds = SubscriptionManager.getDefaultDataSubscriptionId();
+ // To minimize race conditions we call cleanUpAllConnections on
+ // both if else paths instead of before this isDisconnected test.
+ if (dcTracker.isDisconnected()
+ && (dds == mPhone.getSubId()
+ || (dds != mPhone.getSubId()
+ && ProxyController.getInstance().isDataDisconnected(dds)))) {
+ // To minimize race conditions we do this after isDisconnected
+ dcTracker.cleanUpAllConnections(Phone.REASON_RADIO_TURNED_OFF);
+ if (DBG) log("Data disconnected, turn off radio right away.");
+ hangupAndPowerOff();
} else {
- // In some network, deactivate PDP connection cause releasing of RRC connection,
- // which MM/IMSI detaching request needs. Without this detaching, network can
- // not release the network resources previously attached.
- // So we are avoiding data detaching on these networks.
- String[] networkNotClearData = mPhone.getContext().getResources()
- .getStringArray(com.android.internal.R.array.networks_not_clear_data);
- String currentNetwork = mSS.getOperatorNumeric();
- if ((networkNotClearData != null) && (currentNetwork != null)) {
- for (int i = 0; i < networkNotClearData.length; i++) {
- if (currentNetwork.equals(networkNotClearData[i])) {
- // Don't clear data connection for this carrier
- if (DBG)
- log("Not disconnecting data for " + currentNetwork);
- hangupAndPowerOff();
- return;
- }
- }
+ // hang up all active voice calls first
+ if (mPhone.isPhoneTypeGsm() && mPhone.isInCall()) {
+ mPhone.mCT.mRingingCall.hangupIfAlive();
+ mPhone.mCT.mBackgroundCall.hangupIfAlive();
+ mPhone.mCT.mForegroundCall.hangupIfAlive();
}
- // To minimize race conditions we call cleanUpAllConnections on
- // both if else paths instead of before this isDisconnected test.
- if (dcTracker.isDisconnected()) {
- // To minimize race conditions we do this after isDisconnected
- dcTracker.cleanUpAllConnections(Phone.REASON_RADIO_TURNED_OFF);
- if (DBG) log("Data disconnected, turn off radio right away.");
- hangupAndPowerOff();
+ dcTracker.cleanUpAllConnections(Phone.REASON_RADIO_TURNED_OFF);
+ if (dds != mPhone.getSubId()
+ && !ProxyController.getInstance().isDataDisconnected(dds)) {
+ if (DBG) log("Data is active on DDS. Wait for all data disconnect");
+ // Data is not disconnected on DDS. Wait for the data disconnect complete
+ // before sending the RADIO_POWER off.
+ ProxyController.getInstance().registerForAllDataDisconnected(dds, this,
+ EVENT_ALL_DATA_DISCONNECTED, null);
+ mPendingRadioPowerOffAfterDataOff = true;
+ }
+ Message msg = Message.obtain(this);
+ msg.what = EVENT_SET_RADIO_POWER_OFF;
+ msg.arg1 = ++mPendingRadioPowerOffAfterDataOffTag;
+ if (sendMessageDelayed(msg, 30000)) {
+ if (DBG) log("Wait upto 30s for data to disconnect, then turn off radio.");
+ mPendingRadioPowerOffAfterDataOff = true;
} else {
- dcTracker.cleanUpAllConnections(Phone.REASON_RADIO_TURNED_OFF);
- Message msg = Message.obtain(this);
- msg.what = EVENT_SET_RADIO_POWER_OFF;
- msg.arg1 = ++mPendingRadioPowerOffAfterDataOffTag;
- if (sendMessageDelayed(msg, 30000)) {
- if (DBG)
- log("Wait upto 30s for data to disconnect, then turn off radio.");
- mPendingRadioPowerOffAfterDataOff = true;
- } else {
- log("Cannot send delayed Msg, turn off radio right away.");
- hangupAndPowerOff();
- }
+ log("Cannot send delayed Msg, turn off radio right away.");
+ hangupAndPowerOff();
+ mPendingRadioPowerOffAfterDataOff = false;
}
}
}
@@ -4001,6 +3953,7 @@ public class ServiceStateTracker extends Handler {
}
mSignalStrength.setLteRsrpBoost(mSS.getLteEarfcnRsrpBoost());
mSignalStrength.setUseOnlyRsrpForLteLevel(isUseOnlyRsrpForLteLevel());
+ mSignalStrength.setLteRsrpThresholds(getLteRsrpThresholds());
} else {
log("onSignalStrengthResult() Exception from RIL : " + ar.exception);
mSignalStrength = new SignalStrength(isGsm);
@@ -4033,49 +3986,36 @@ public class ServiceStateTracker extends Handler {
}
/**
- * Return true if the operator changed.
+ * Return true if the network operator's country code changed.
*/
- 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.
+ private boolean networkCountryIsoChanged(String newCountryIsoCode, String prevCountryIsoCode) {
+ // Return false if the new ISO code isn't valid as we don't know where we are.
+ // Return true if the previous ISO code wasn't valid, or if it was and the new one differs.
- // If mcc is invalid then we'll return false
- int mcc;
- try {
- mcc = Integer.parseInt(operatorNumeric.substring(0, 3));
- } catch (Exception e) {
+ // If newCountryIsoCode is invalid then we'll return false
+ if (TextUtils.isEmpty(newCountryIsoCode)) {
if (DBG) {
- log("mccChanged: no mcc, operatorNumeric=" + operatorNumeric + " retVal=false");
+ log("countryIsoChanged: no new country ISO code");
}
return false;
}
- // If prevMcc is invalid will make it different from mcc
- // so we'll return true if the card exists.
- int prevMcc;
- try {
- prevMcc = Integer.parseInt(prevOperatorNumeric.substring(0, 3));
- } catch (Exception e) {
- prevMcc = mcc + 1;
+ if (TextUtils.isEmpty(prevCountryIsoCode)) {
+ if (DBG) {
+ log("countryIsoChanged: no previous country ISO code");
+ }
+ return true;
}
+ return !newCountryIsoCode.equals(prevCountryIsoCode);
+ }
- // Determine if the Icc card exists
+ // Determine if the Icc card exists
+ private boolean iccCardExists() {
boolean iccCardExist = false;
if (mUiccApplcation != null) {
iccCardExist = mUiccApplcation.getState() != AppState.APPSTATE_UNKNOWN;
}
-
- // Determine retVal
- 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 +
- " ltod=" + TimeUtils.logTimeOfDay(ctm));
- }
- return retVal;
+ return iccCardExist;
}
public String getSystemProperty(String property, String defValue) {
@@ -4592,18 +4532,37 @@ public class ServiceStateTracker extends Handler {
* @return true if it should use only RSRP for the number of LTE signal bar.
*/
private boolean isUseOnlyRsrpForLteLevel() {
+ return getCarrierConfig().getBoolean(
+ CarrierConfigManager.KEY_USE_ONLY_RSRP_FOR_LTE_SIGNAL_BAR_BOOL);
+ }
+
+ /**
+ * Gets the threshold array for determining the display level of LTE signal bar.
+ *
+ * @return int array for determining the display level.
+ */
+ private int[] getLteRsrpThresholds() {
+ return getCarrierConfig().getIntArray(
+ CarrierConfigManager.KEY_LTE_RSRP_THRESHOLDS_INT_ARRAY);
+ }
+
+ /**
+ * Gets the carrier configuration values for a particular subscription.
+ *
+ * @return A {@link PersistableBundle} containing the config for the given subId,
+ * or default values for an invalid subId.
+ */
+ private PersistableBundle getCarrierConfig() {
CarrierConfigManager configManager = (CarrierConfigManager) mPhone.getContext()
.getSystemService(Context.CARRIER_CONFIG_SERVICE);
if (configManager != null) {
// If an invalid subId is used, this bundle will contain default values.
PersistableBundle config = configManager.getConfigForSubId(mPhone.getSubId());
if (config != null) {
- return config.getBoolean(
- CarrierConfigManager.KEY_USE_ONLY_RSRP_FOR_LTE_SIGNAL_BAR_BOOL);
+ return config;
}
}
// Return static default defined in CarrierConfigManager.
- return CarrierConfigManager.getDefaultConfig().getBoolean(
- CarrierConfigManager.KEY_USE_ONLY_RSRP_FOR_LTE_SIGNAL_BAR_BOOL);
+ return CarrierConfigManager.getDefaultConfig();
}
}
diff --git a/com/android/internal/telephony/SmsDispatchersController.java b/com/android/internal/telephony/SmsDispatchersController.java
new file mode 100644
index 00000000..f62a90ad
--- /dev/null
+++ b/com/android/internal/telephony/SmsDispatchersController.java
@@ -0,0 +1,562 @@
+/*
+ * Copyright (C) 2006 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.Activity;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+import android.provider.Telephony.Sms;
+import android.provider.Telephony.Sms.Intents;
+import android.telephony.Rlog;
+import android.telephony.SmsManager;
+import android.util.Pair;
+
+import com.android.ims.ImsManager;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.cdma.CdmaInboundSmsHandler;
+import com.android.internal.telephony.cdma.CdmaSMSDispatcher;
+import com.android.internal.telephony.gsm.GsmInboundSmsHandler;
+import com.android.internal.telephony.gsm.GsmSMSDispatcher;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ *
+ */
+public class SmsDispatchersController extends Handler {
+ private static final String TAG = "SmsDispatchersController";
+
+ /** Radio is ON */
+ private static final int EVENT_RADIO_ON = 11;
+
+ /** IMS registration/SMS format changed */
+ private static final int EVENT_IMS_STATE_CHANGED = 12;
+
+ /** Callback from RIL_REQUEST_IMS_REGISTRATION_STATE */
+ private static final int EVENT_IMS_STATE_DONE = 13;
+
+ private SMSDispatcher mCdmaDispatcher;
+ private SMSDispatcher mGsmDispatcher;
+ private ImsSmsDispatcher mImsSmsDispatcher;
+
+ private GsmInboundSmsHandler mGsmInboundSmsHandler;
+ private CdmaInboundSmsHandler mCdmaInboundSmsHandler;
+
+ private Phone mPhone;
+ /** Outgoing message counter. Shared by all dispatchers. */
+ private final SmsUsageMonitor mUsageMonitor;
+ private final CommandsInterface mCi;
+ private final Context mContext;
+
+ /** true if IMS is registered and sms is supported, false otherwise.*/
+ private boolean mIms = false;
+ private String mImsSmsFormat = SmsConstants.FORMAT_UNKNOWN;
+
+ public SmsDispatchersController(Phone phone, SmsStorageMonitor storageMonitor,
+ SmsUsageMonitor usageMonitor) {
+ Rlog.d(TAG, "SmsDispatchersController created");
+
+ mContext = phone.getContext();
+ mUsageMonitor = usageMonitor;
+ mCi = phone.mCi;
+ mPhone = phone;
+
+ // Create dispatchers, inbound SMS handlers and
+ // broadcast undelivered messages in raw table.
+ mImsSmsDispatcher = new ImsSmsDispatcher(phone, this);
+ mCdmaDispatcher = new CdmaSMSDispatcher(phone, this);
+ mGsmInboundSmsHandler = GsmInboundSmsHandler.makeInboundSmsHandler(phone.getContext(),
+ storageMonitor, phone);
+ mCdmaInboundSmsHandler = CdmaInboundSmsHandler.makeInboundSmsHandler(phone.getContext(),
+ storageMonitor, phone, (CdmaSMSDispatcher) mCdmaDispatcher);
+ mGsmDispatcher = new GsmSMSDispatcher(phone, this, mGsmInboundSmsHandler);
+ SmsBroadcastUndelivered.initialize(phone.getContext(),
+ mGsmInboundSmsHandler, mCdmaInboundSmsHandler);
+ InboundSmsHandler.registerNewMessageNotificationActionHandler(phone.getContext());
+
+ mCi.registerForOn(this, EVENT_RADIO_ON, null);
+ mCi.registerForImsNetworkStateChanged(this, EVENT_IMS_STATE_CHANGED, null);
+ }
+
+ /* Updates the phone object when there is a change */
+ protected void updatePhoneObject(Phone phone) {
+ Rlog.d(TAG, "In IMS updatePhoneObject ");
+ mCdmaDispatcher.updatePhoneObject(phone);
+ mGsmDispatcher.updatePhoneObject(phone);
+ mGsmInboundSmsHandler.updatePhoneObject(phone);
+ mCdmaInboundSmsHandler.updatePhoneObject(phone);
+ }
+
+ public void dispose() {
+ mCi.unregisterForOn(this);
+ mCi.unregisterForImsNetworkStateChanged(this);
+ mGsmDispatcher.dispose();
+ mCdmaDispatcher.dispose();
+ mGsmInboundSmsHandler.dispose();
+ mCdmaInboundSmsHandler.dispose();
+ }
+
+ /**
+ * Handles events coming from the phone stack. Overridden from handler.
+ *
+ * @param msg the message to handle
+ */
+ @Override
+ public void handleMessage(Message msg) {
+ AsyncResult ar;
+
+ switch (msg.what) {
+ case EVENT_RADIO_ON:
+ case EVENT_IMS_STATE_CHANGED: // received unsol
+ mCi.getImsRegistrationState(this.obtainMessage(EVENT_IMS_STATE_DONE));
+ break;
+
+ case EVENT_IMS_STATE_DONE:
+ ar = (AsyncResult) msg.obj;
+
+ if (ar.exception == null) {
+ updateImsInfo(ar);
+ } else {
+ Rlog.e(TAG, "IMS State query failed with exp "
+ + ar.exception);
+ }
+ break;
+
+ default:
+ if (isCdmaMo()) {
+ mCdmaDispatcher.handleMessage(msg);
+ } else {
+ mGsmDispatcher.handleMessage(msg);
+ }
+ }
+ }
+
+ private void setImsSmsFormat(int format) {
+ switch (format) {
+ case PhoneConstants.PHONE_TYPE_GSM:
+ mImsSmsFormat = SmsConstants.FORMAT_3GPP;
+ break;
+ case PhoneConstants.PHONE_TYPE_CDMA:
+ mImsSmsFormat = SmsConstants.FORMAT_3GPP2;
+ break;
+ default:
+ mImsSmsFormat = SmsConstants.FORMAT_UNKNOWN;
+ break;
+ }
+ }
+
+ private void updateImsInfo(AsyncResult ar) {
+ int[] responseArray = (int[]) ar.result;
+ setImsSmsFormat(responseArray[1]);
+ mIms = responseArray[0] == 1 && !SmsConstants.FORMAT_UNKNOWN.equals(mImsSmsFormat);
+ Rlog.d(TAG, "IMS registration state: " + mIms + " format: " + mImsSmsFormat);
+ }
+
+ /**
+ * Inject an SMS PDU into the android platform.
+ *
+ * @param pdu is the byte array of pdu to be injected into android telephony layer
+ * @param format is the format of SMS pdu (3gpp or 3gpp2)
+ * @param callback if not NULL this callback is triggered when the message is successfully
+ * received by the android telephony layer. This callback is triggered at
+ * the same time an SMS received from radio is responded back.
+ */
+ @VisibleForTesting
+ public void injectSmsPdu(byte[] pdu, String format, SmsInjectionCallback callback) {
+ Rlog.d(TAG, "SmsDispatchersController:injectSmsPdu");
+ try {
+ // TODO We need to decide whether we should allow injecting GSM(3gpp)
+ // SMS pdus when the phone is camping on CDMA(3gpp2) network and vice versa.
+ android.telephony.SmsMessage msg =
+ android.telephony.SmsMessage.createFromPdu(pdu, format);
+
+ // Only class 1 SMS are allowed to be injected.
+ if (msg == null
+ || msg.getMessageClass() != android.telephony.SmsMessage.MessageClass.CLASS_1) {
+ if (msg == null) {
+ Rlog.e(TAG, "injectSmsPdu: createFromPdu returned null");
+ }
+ callback.onSmsInjectedResult(Intents.RESULT_SMS_GENERIC_ERROR);
+ return;
+ }
+
+ AsyncResult ar = new AsyncResult(callback, msg, null);
+
+ if (format.equals(SmsConstants.FORMAT_3GPP)) {
+ Rlog.i(TAG, "SmsDispatchersController:injectSmsText Sending msg=" + msg
+ + ", format=" + format + "to mGsmInboundSmsHandler");
+ mGsmInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_INJECT_SMS, ar);
+ } else if (format.equals(SmsConstants.FORMAT_3GPP2)) {
+ Rlog.i(TAG, "SmsDispatchersController:injectSmsText Sending msg=" + msg
+ + ", format=" + format + "to mCdmaInboundSmsHandler");
+ mCdmaInboundSmsHandler.sendMessage(InboundSmsHandler.EVENT_INJECT_SMS, ar);
+ } else {
+ // Invalid pdu format.
+ Rlog.e(TAG, "Invalid pdu format: " + format);
+ callback.onSmsInjectedResult(Intents.RESULT_SMS_GENERIC_ERROR);
+ }
+ } catch (Exception e) {
+ Rlog.e(TAG, "injectSmsPdu failed: ", e);
+ callback.onSmsInjectedResult(Intents.RESULT_SMS_GENERIC_ERROR);
+ }
+ }
+
+ /**
+ * Retry the message along to the radio.
+ *
+ * @param tracker holds the SMS message to send
+ */
+ public void sendRetrySms(SMSDispatcher.SmsTracker tracker) {
+ String oldFormat = tracker.mFormat;
+
+ // newFormat will be based on voice technology
+ String newFormat =
+ (PhoneConstants.PHONE_TYPE_CDMA == mPhone.getPhoneType())
+ ? mCdmaDispatcher.getFormat() : mGsmDispatcher.getFormat();
+
+ // was previously sent sms format match with voice tech?
+ if (oldFormat.equals(newFormat)) {
+ if (isCdmaFormat(newFormat)) {
+ Rlog.d(TAG, "old format matched new format (cdma)");
+ mCdmaDispatcher.sendSms(tracker);
+ return;
+ } else {
+ Rlog.d(TAG, "old format matched new format (gsm)");
+ mGsmDispatcher.sendSms(tracker);
+ return;
+ }
+ }
+
+ // format didn't match, need to re-encode.
+ HashMap map = tracker.getData();
+
+ // to re-encode, fields needed are: scAddr, destAddr, and
+ // text if originally sent as sendText or
+ // data and destPort if originally sent as sendData.
+ if (!(map.containsKey("scAddr") && map.containsKey("destAddr")
+ && (map.containsKey("text")
+ || (map.containsKey("data") && map.containsKey("destPort"))))) {
+ // should never come here...
+ Rlog.e(TAG, "sendRetrySms failed to re-encode per missing fields!");
+ tracker.onFailed(mContext, SmsManager.RESULT_ERROR_GENERIC_FAILURE, 0/*errorCode*/);
+ return;
+ }
+ String scAddr = (String) map.get("scAddr");
+ String destAddr = (String) map.get("destAddr");
+
+ SmsMessageBase.SubmitPduBase pdu = null;
+ // figure out from tracker if this was sendText/Data
+ if (map.containsKey("text")) {
+ Rlog.d(TAG, "sms failed was text");
+ String text = (String) map.get("text");
+
+ if (isCdmaFormat(newFormat)) {
+ Rlog.d(TAG, "old format (gsm) ==> new format (cdma)");
+ pdu = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(
+ scAddr, destAddr, text, (tracker.mDeliveryIntent != null), null);
+ } else {
+ Rlog.d(TAG, "old format (cdma) ==> new format (gsm)");
+ pdu = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(
+ scAddr, destAddr, text, (tracker.mDeliveryIntent != null), null);
+ }
+ } else if (map.containsKey("data")) {
+ Rlog.d(TAG, "sms failed was data");
+ byte[] data = (byte[]) map.get("data");
+ Integer destPort = (Integer) map.get("destPort");
+
+ if (isCdmaFormat(newFormat)) {
+ Rlog.d(TAG, "old format (gsm) ==> new format (cdma)");
+ pdu = com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(
+ scAddr, destAddr, destPort.intValue(), data,
+ (tracker.mDeliveryIntent != null));
+ } else {
+ Rlog.d(TAG, "old format (cdma) ==> new format (gsm)");
+ pdu = com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(
+ scAddr, destAddr, destPort.intValue(), data,
+ (tracker.mDeliveryIntent != null));
+ }
+ }
+
+ // replace old smsc and pdu with newly encoded ones
+ map.put("smsc", pdu.encodedScAddress);
+ map.put("pdu", pdu.encodedMessage);
+
+ SMSDispatcher dispatcher = (isCdmaFormat(newFormat)) ? mCdmaDispatcher : mGsmDispatcher;
+
+ tracker.mFormat = dispatcher.getFormat();
+ dispatcher.sendSms(tracker);
+ }
+
+ public boolean isIms() {
+ return mIms;
+ }
+
+ public String getImsSmsFormat() {
+ return mImsSmsFormat;
+ }
+
+ /**
+ * Determines whether or not to use CDMA format for MO SMS.
+ * If SMS over IMS is supported, then format is based on IMS SMS format,
+ * otherwise format is based on current phone type.
+ *
+ * @return true if Cdma format should be used for MO SMS, false otherwise.
+ */
+ protected boolean isCdmaMo() {
+ if (!isIms()) {
+ // IMS is not registered, use Voice technology to determine SMS format.
+ return (PhoneConstants.PHONE_TYPE_CDMA == mPhone.getPhoneType());
+ }
+ // IMS is registered with SMS support
+ return isCdmaFormat(mImsSmsFormat);
+ }
+
+ /**
+ * Determines whether or not format given is CDMA format.
+ *
+ * @param format
+ * @return true if format given is CDMA format, false otherwise.
+ */
+ public boolean isCdmaFormat(String format) {
+ return (mCdmaDispatcher.getFormat().equals(format));
+ }
+
+ /**
+ * Send a data based SMS to a specific application port.
+ *
+ * @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 destPort the port to deliver the message to
+ * @param data 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>
+ * <code>RESULT_ERROR_NO_SERVICE</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").
+ */
+ protected void sendData(String destAddr, String scAddr, int destPort,
+ byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
+ if (mImsSmsDispatcher.isAvailable()) {
+ mImsSmsDispatcher.sendData(destAddr, scAddr, destPort, data, sentIntent,
+ deliveryIntent);
+ } else if (isCdmaMo()) {
+ mCdmaDispatcher.sendData(destAddr, scAddr, destPort, data, sentIntent, deliveryIntent);
+ } else {
+ mGsmDispatcher.sendData(destAddr, scAddr, destPort, data, sentIntent, deliveryIntent);
+ }
+ }
+
+ /**
+ * Send a text based SMS.
+ * @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>
+ * <code>RESULT_ERROR_NO_SERVICE</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
+ * @param messageUri optional URI of the message if it is already stored in the system
+ * @param callingPkg the calling package name
+ * @param persistMessage whether to save the sent message into SMS DB for a
+ * non-default SMS app.
+ */
+ public void sendText(String destAddr, String scAddr, String text,
+ PendingIntent sentIntent, PendingIntent deliveryIntent, Uri messageUri,
+ String callingPkg, boolean persistMessage) {
+ if (mImsSmsDispatcher.isAvailable()) {
+ mImsSmsDispatcher.sendText(destAddr, scAddr, text, sentIntent, deliveryIntent,
+ messageUri, callingPkg, persistMessage);
+ } else if (isCdmaMo()) {
+ mCdmaDispatcher.sendText(destAddr, scAddr, text, sentIntent, deliveryIntent, messageUri,
+ callingPkg, persistMessage);
+ } else {
+ mGsmDispatcher.sendText(destAddr, scAddr, text, sentIntent, deliveryIntent, messageUri,
+ callingPkg, persistMessage);
+ }
+ }
+
+ /**
+ * Send a multi-part text based SMS.
+ * @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>
+ * <code>RESULT_ERROR_NO_SERVICE</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
+ * @param messageUri optional URI of the message if it is already stored in the system
+ * @param callingPkg the calling package name
+ * @param persistMessage whether to save the sent message into SMS DB for a
+ * non-default SMS app.
+ */
+ protected void sendMultipartText(String destAddr, String scAddr,
+ ArrayList<String> parts, ArrayList<PendingIntent> sentIntents,
+ ArrayList<PendingIntent> deliveryIntents, Uri messageUri, String callingPkg,
+ boolean persistMessage) {
+ if (mImsSmsDispatcher.isAvailable()) {
+ mImsSmsDispatcher.sendMultipartText(destAddr, scAddr, parts, sentIntents,
+ deliveryIntents, messageUri, callingPkg, persistMessage);
+ } else if (isCdmaMo()) {
+ mCdmaDispatcher.sendMultipartText(destAddr, scAddr, parts, sentIntents, deliveryIntents,
+ messageUri, callingPkg, persistMessage);
+ } else {
+ mGsmDispatcher.sendMultipartText(destAddr, scAddr, parts, sentIntents, deliveryIntents,
+ messageUri, callingPkg, persistMessage);
+ }
+ }
+
+ /**
+ * Returns the premium SMS permission for the specified package. If the package has never
+ * been seen before, the default {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ASK_USER}
+ * will be returned.
+ * @param packageName the name of the package to query permission
+ * @return one of {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_UNKNOWN},
+ * {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ASK_USER},
+ * {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_NEVER_ALLOW}, or
+ * {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW}
+ */
+ public int getPremiumSmsPermission(String packageName) {
+ return mUsageMonitor.getPremiumSmsPermission(packageName);
+ }
+
+ /**
+ * Sets the premium SMS permission for the specified package and save the value asynchronously
+ * to persistent storage.
+ * @param packageName the name of the package to set permission
+ * @param permission one of {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ASK_USER},
+ * {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_NEVER_ALLOW}, or
+ * {@link SmsUsageMonitor#PREMIUM_SMS_PERMISSION_ALWAYS_ALLOW}
+ */
+ public void setPremiumSmsPermission(String packageName, int permission) {
+ mUsageMonitor.setPremiumSmsPermission(packageName, permission);
+ }
+
+ public SmsUsageMonitor getUsageMonitor() {
+ return mUsageMonitor;
+ }
+
+ /**
+ * Triggers the correct method for handling the sms status report based on the format.
+ *
+ * @param tracker the sms tracker.
+ * @param format the format.
+ * @param pdu the pdu of the report.
+ * @return a Pair in which the first boolean is whether the report was handled successfully
+ * or not and the second boolean is whether processing the sms is complete and the
+ * tracker no longer need to be kept track of, false if we should expect more callbacks
+ * and the tracker should be kept.
+ */
+ public Pair<Boolean, Boolean> handleSmsStatusReport(SMSDispatcher.SmsTracker tracker,
+ String format, byte[] pdu) {
+ if (isCdmaFormat(format)) {
+ return handleCdmaStatusReport(tracker, format, pdu);
+ } else {
+ return handleGsmStatusReport(tracker, format, pdu);
+ }
+ }
+
+ private Pair<Boolean, Boolean> handleCdmaStatusReport(SMSDispatcher.SmsTracker tracker,
+ String format, byte[] pdu) {
+ tracker.updateSentMessageStatus(mContext, Sms.STATUS_COMPLETE);
+ boolean success = triggerDeliveryIntent(tracker, format, pdu);
+ return new Pair(success, true /* complete */);
+ }
+
+ private Pair<Boolean, Boolean> handleGsmStatusReport(SMSDispatcher.SmsTracker tracker,
+ String format, byte[] pdu) {
+ com.android.internal.telephony.gsm.SmsMessage sms =
+ com.android.internal.telephony.gsm.SmsMessage.newFromCDS(pdu);
+ boolean complete = false;
+ boolean success = false;
+ if (sms != null) {
+ int tpStatus = sms.getStatus();
+ if(tpStatus >= Sms.STATUS_FAILED || tpStatus < Sms.STATUS_PENDING ) {
+ // Update the message status (COMPLETE or FAILED)
+ tracker.updateSentMessageStatus(mContext, tpStatus);
+ complete = true;
+ }
+ success = triggerDeliveryIntent(tracker, format, pdu);
+ }
+ return new Pair(success, complete);
+ }
+
+ private boolean triggerDeliveryIntent(SMSDispatcher.SmsTracker tracker, String format,
+ byte[] pdu) {
+ PendingIntent intent = tracker.mDeliveryIntent;
+ Intent fillIn = new Intent();
+ fillIn.putExtra("pdu", pdu);
+ fillIn.putExtra("format", format);
+ try {
+ intent.send(mContext, Activity.RESULT_OK, fillIn);
+ return true;
+ } catch (CanceledException ex) {
+ return false;
+ }
+ }
+
+
+ public interface SmsInjectionCallback {
+ void onSmsInjectedResult(int result);
+ }
+}
diff --git a/com/android/internal/telephony/SubscriptionController.java b/com/android/internal/telephony/SubscriptionController.java
index 2629b42d..74f7541f 100644
--- a/com/android/internal/telephony/SubscriptionController.java
+++ b/com/android/internal/telephony/SubscriptionController.java
@@ -46,6 +46,8 @@ import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.IccCardConstants.State;
import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.telephony.uicc.UiccCard;
+import com.android.internal.telephony.uicc.UiccController;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -317,6 +319,8 @@ public class SubscriptionController extends ISub.Stub {
SubscriptionManager.MCC));
int mnc = cursor.getInt(cursor.getColumnIndexOrThrow(
SubscriptionManager.MNC));
+ String cardId = cursor.getString(cursor.getColumnIndexOrThrow(
+ SubscriptionManager.CARD_ID));
// FIXME: consider stick this into database too
String countryIso = getSubscriptionCountryIso(id);
boolean isEmbedded = cursor.getInt(cursor.getColumnIndexOrThrow(
@@ -331,11 +335,13 @@ public class SubscriptionController extends ISub.Stub {
if (VDBG) {
String iccIdToPrint = SubscriptionInfo.givePrintableIccid(iccId);
+ String cardIdToPrint = SubscriptionInfo.givePrintableIccid(cardId);
logd("[getSubInfoRecord] id:" + id + " iccid:" + iccIdToPrint + " simSlotIndex:"
+ simSlotIndex + " displayName:" + displayName + " nameSource:" + nameSource
+ " iconTint:" + iconTint + " dataRoaming:" + dataRoaming
+ " mcc:" + mcc + " mnc:" + mnc + " countIso:" + countryIso + " isEmbedded:"
- + isEmbedded + " accessRules:" + Arrays.toString(accessRules));
+ + isEmbedded + " accessRules:" + Arrays.toString(accessRules)
+ + " cardId:" + cardIdToPrint);
}
// If line1number has been set to a different number, use it instead.
@@ -345,7 +351,7 @@ public class SubscriptionController extends ISub.Stub {
}
return new SubscriptionInfo(id, iccId, simSlotIndex, displayName, carrierName,
nameSource, iconTint, number, dataRoaming, iconBitmap, mcc, mnc, countryIso,
- isEmbedded, accessRules);
+ isEmbedded, accessRules, cardId);
}
/**
@@ -911,7 +917,7 @@ public class SubscriptionController extends ISub.Stub {
Cursor cursor = resolver.query(SubscriptionManager.CONTENT_URI,
new String[]{SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID,
SubscriptionManager.SIM_SLOT_INDEX, SubscriptionManager.NAME_SOURCE,
- SubscriptionManager.ICC_ID},
+ SubscriptionManager.ICC_ID, SubscriptionManager.CARD_ID},
SubscriptionManager.ICC_ID + "=?" + " OR " + SubscriptionManager.ICC_ID + "=?",
new String[]{iccId, IccUtils.getDecimalSubstring(iccId)}, null);
@@ -926,6 +932,7 @@ public class SubscriptionController extends ISub.Stub {
int oldSimInfoId = cursor.getInt(1);
int nameSource = cursor.getInt(2);
String oldIccId = cursor.getString(3);
+ String oldCardId = cursor.getString(4);
ContentValues value = new ContentValues();
if (slotIndex != oldSimInfoId) {
@@ -941,6 +948,14 @@ public class SubscriptionController extends ISub.Stub {
value.put(SubscriptionManager.ICC_ID, iccId);
}
+ UiccCard card = UiccController.getInstance().getUiccCardForPhone(slotIndex);
+ if (card != null) {
+ String cardId = card.getCardId();
+ if (cardId != null && cardId != oldCardId) {
+ value.put(SubscriptionManager.CARD_ID, cardId);
+ }
+ }
+
if (value.size() > 0) {
resolver.update(SubscriptionManager.CONTENT_URI, value,
SubscriptionManager.UNIQUE_KEY_SUBSCRIPTION_ID +
@@ -1076,6 +1091,17 @@ public class SubscriptionController extends ISub.Stub {
value.put(SubscriptionManager.COLOR, color);
value.put(SubscriptionManager.SIM_SLOT_INDEX, slotIndex);
value.put(SubscriptionManager.CARRIER_NAME, "");
+ UiccCard card = UiccController.getInstance().getUiccCardForPhone(slotIndex);
+ if (card != null) {
+ String cardId = card.getCardId();
+ if (cardId != null) {
+ value.put(SubscriptionManager.CARD_ID, cardId);
+ } else {
+ value.put(SubscriptionManager.CARD_ID, iccId);
+ }
+ } else {
+ value.put(SubscriptionManager.CARD_ID, iccId);
+ }
Uri uri = resolver.insert(SubscriptionManager.CONTENT_URI, value);
diff --git a/com/android/internal/telephony/SubscriptionInfoUpdater.java b/com/android/internal/telephony/SubscriptionInfoUpdater.java
index 57082d31..b93b683e 100644
--- a/com/android/internal/telephony/SubscriptionInfoUpdater.java
+++ b/com/android/internal/telephony/SubscriptionInfoUpdater.java
@@ -16,6 +16,7 @@
package com.android.internal.telephony;
+import android.Manifest;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.UserSwitchObserver;
@@ -50,9 +51,9 @@ 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.IccRecords;
import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.telephony.uicc.UiccProfile;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -73,7 +74,10 @@ public class SubscriptionInfoUpdater extends Handler {
private static final int EVENT_SIM_IO_ERROR = 6;
private static final int EVENT_SIM_UNKNOWN = 7;
private static final int EVENT_SIM_RESTRICTED = 8;
- private static final int EVENT_REFRESH_EMBEDDED_SUBSCRIPTIONS = 9;
+ private static final int EVENT_SIM_NOT_READY = 9;
+ private static final int EVENT_SIM_READY = 10;
+ private static final int EVENT_SIM_IMSI = 11;
+ private static final int EVENT_REFRESH_EMBEDDED_SUBSCRIPTIONS = 12;
private static final String ICCID_STRING_FOR_NO_SIM = "";
/**
@@ -106,6 +110,8 @@ public class SubscriptionInfoUpdater extends Handler {
private static Context mContext = null;
private static String mIccId[] = new String[PROJECT_SIM_NUM];
private static int[] mInsertSimState = new int[PROJECT_SIM_NUM];
+ private static int[] sSimCardState = new int[PROJECT_SIM_NUM];
+ private static int[] sSimApplicationState = new int[PROJECT_SIM_NUM];
private SubscriptionManager mSubscriptionManager = null;
private EuiccManager mEuiccManager;
private IPackageManager mPackageManager;
@@ -125,8 +131,7 @@ public class SubscriptionInfoUpdater extends Handler {
mEuiccManager = (EuiccManager) mContext.getSystemService(Context.EUICC_SERVICE);
mPackageManager = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
- IntentFilter intentFilter = new IntentFilter(TelephonyIntents.ACTION_SIM_STATE_CHANGED);
- intentFilter.addAction(IccCardProxy.ACTION_INTERNAL_SIM_STATE_CHANGED);
+ IntentFilter intentFilter = new IntentFilter(UiccProfile.ACTION_INTERNAL_SIM_STATE_CHANGED);
mContext.registerReceiver(sReceiver, intentFilter);
mCarrierServiceBindHelper = new CarrierServiceBindHelper(mContext);
@@ -173,8 +178,7 @@ public class SubscriptionInfoUpdater extends Handler {
String action = intent.getAction();
logd("Action: " + action);
- if (!action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED) &&
- !action.equals(IccCardProxy.ACTION_INTERNAL_SIM_STATE_CHANGED)) {
+ if (!action.equals(UiccProfile.ACTION_INTERNAL_SIM_STATE_CHANGED)) {
return;
}
@@ -182,16 +186,14 @@ public class SubscriptionInfoUpdater extends Handler {
SubscriptionManager.INVALID_SIM_SLOT_INDEX);
logd("slotIndex: " + slotIndex);
if (!SubscriptionManager.isValidSlotIndex(slotIndex)) {
- logd("ACTION_SIM_STATE_CHANGED contains invalid slotIndex: " + slotIndex);
+ logd("ACTION_INTERNAL_SIM_STATE_CHANGED contains invalid slotIndex: " + slotIndex);
return;
}
String simStatus = intent.getStringExtra(IccCardConstants.INTENT_KEY_ICC_STATE);
logd("simStatus: " + simStatus);
- // TODO: All of the below should be converted to ACTION_INTERNAL_SIM_STATE_CHANGED to
- // ensure that the SubscriptionInfo is updated before the broadcasts are sent out.
- if (action.equals(TelephonyIntents.ACTION_SIM_STATE_CHANGED)) {
+ if (action.equals(UiccProfile.ACTION_INTERNAL_SIM_STATE_CHANGED)) {
if (IccCardConstants.INTENT_VALUE_ICC_ABSENT.equals(simStatus)) {
sendMessage(obtainMessage(EVENT_SIM_ABSENT, slotIndex, -1));
} else if (IccCardConstants.INTENT_VALUE_ICC_UNKNOWN.equals(simStatus)) {
@@ -201,22 +203,17 @@ public class SubscriptionInfoUpdater extends Handler {
} else if (IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED.equals(simStatus)) {
sendMessage(obtainMessage(EVENT_SIM_RESTRICTED, slotIndex, -1));
} else if (IccCardConstants.INTENT_VALUE_ICC_NOT_READY.equals(simStatus)) {
- // ICC_NOT_READY is a terminal state for an eSIM on the boot profile. At this
- // phase, the subscription list is accessible.
- // TODO(b/64216093): Clean up this special case, likely by treating NOT_READY
- // as equivalent to ABSENT, once the rest of the system can handle it. Currently
- // this breaks SystemUI which shows a "No SIM" icon.
- sendEmptyMessage(EVENT_REFRESH_EMBEDDED_SUBSCRIPTIONS);
- } else {
- logd("Ignoring simStatus: " + simStatus);
- }
- } else if (action.equals(IccCardProxy.ACTION_INTERNAL_SIM_STATE_CHANGED)) {
- if (IccCardConstants.INTENT_VALUE_ICC_LOCKED.equals(simStatus)) {
+ sendEmptyMessage(EVENT_SIM_NOT_READY);
+ } else if (IccCardConstants.INTENT_VALUE_ICC_LOCKED.equals(simStatus)) {
String reason = intent.getStringExtra(
IccCardConstants.INTENT_KEY_LOCKED_REASON);
sendMessage(obtainMessage(EVENT_SIM_LOCKED, slotIndex, -1, reason));
} else if (IccCardConstants.INTENT_VALUE_ICC_LOADED.equals(simStatus)) {
sendMessage(obtainMessage(EVENT_SIM_LOADED, slotIndex, -1));
+ } else if (IccCardConstants.INTENT_VALUE_ICC_READY.equals(simStatus)) {
+ sendMessage(obtainMessage(EVENT_SIM_READY, slotIndex, -1));
+ } else if (IccCardConstants.INTENT_VALUE_ICC_IMSI.equals(simStatus)) {
+ sendMessage(obtainMessage(EVENT_SIM_IMSI, slotIndex, -1));
} else {
logd("Ignoring simStatus: " + simStatus);
}
@@ -254,7 +251,7 @@ public class SubscriptionInfoUpdater extends Handler {
break;
}
- case EVENT_SIM_LOADED:
+ case EVENT_SIM_LOADED:
handleSimLoaded(msg.arg1);
break;
@@ -268,6 +265,9 @@ public class SubscriptionInfoUpdater extends Handler {
case EVENT_SIM_UNKNOWN:
updateCarrierServices(msg.arg1, IccCardConstants.INTENT_VALUE_ICC_UNKNOWN);
+ broadcastSimStateChanged(msg.arg1, IccCardConstants.INTENT_VALUE_ICC_UNKNOWN, null);
+ broadcastSimCardStateChanged(msg.arg1, TelephonyManager.SIM_STATE_UNKNOWN);
+ broadcastSimApplicationStateChanged(msg.arg1, TelephonyManager.SIM_STATE_UNKNOWN);
break;
case EVENT_SIM_IO_ERROR:
@@ -276,8 +276,35 @@ public class SubscriptionInfoUpdater extends Handler {
case EVENT_SIM_RESTRICTED:
updateCarrierServices(msg.arg1, IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED);
+ broadcastSimStateChanged(msg.arg1,
+ IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED,
+ IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED);
+ broadcastSimCardStateChanged(msg.arg1, TelephonyManager.SIM_STATE_CARD_RESTRICTED);
+ broadcastSimApplicationStateChanged(msg.arg1, TelephonyManager.SIM_STATE_NOT_READY);
+ break;
+
+ case EVENT_SIM_READY:
+ broadcastSimStateChanged(msg.arg1, IccCardConstants.INTENT_VALUE_ICC_READY, null);
+ broadcastSimCardStateChanged(msg.arg1, TelephonyManager.SIM_STATE_PRESENT);
+ broadcastSimApplicationStateChanged(msg.arg1, TelephonyManager.SIM_STATE_NOT_READY);
break;
+ case EVENT_SIM_IMSI:
+ broadcastSimStateChanged(msg.arg1, IccCardConstants.INTENT_VALUE_ICC_IMSI, null);
+ break;
+
+ case EVENT_SIM_NOT_READY:
+ broadcastSimStateChanged(msg.arg1, IccCardConstants.INTENT_VALUE_ICC_NOT_READY,
+ null);
+ broadcastSimCardStateChanged(msg.arg1, TelephonyManager.SIM_STATE_PRESENT);
+ broadcastSimApplicationStateChanged(msg.arg1, TelephonyManager.SIM_STATE_NOT_READY);
+ // intentional fall through
+ // ICC_NOT_READY is a terminal state for an eSIM on the boot profile. At this
+ // phase, the subscription list is accessible.
+ // TODO(b/64216093): Clean up this special case, likely by treating NOT_READY
+ // as equivalent to ABSENT, once the rest of the system can handle it. Currently
+ // this breaks SystemUI which shows a "No SIM" icon.
+
case EVENT_REFRESH_EMBEDDED_SUBSCRIPTIONS:
if (updateEmbeddedSubscriptions()) {
SubscriptionController.getInstance().notifySubscriptionInfoChanged();
@@ -315,11 +342,11 @@ public class SubscriptionInfoUpdater extends Handler {
String iccId = mIccId[slotId];
if (iccId == null) {
IccRecords records = mPhone[slotId].getIccCard().getIccRecords();
- if (stripIccIdSuffix(records.getFullIccId()) == null) {
+ if (IccUtils.stripTrailingFs(records.getFullIccId()) == null) {
logd("handleSimLocked: IccID null");
return;
}
- mIccId[slotId] = stripIccIdSuffix(records.getFullIccId());
+ mIccId[slotId] = IccUtils.stripTrailingFs(records.getFullIccId());
} else {
logd("NOT Querying IccId its already set sIccid[" + slotId + "]=" + iccId);
}
@@ -330,6 +357,24 @@ public class SubscriptionInfoUpdater extends Handler {
updateCarrierServices(slotId, IccCardConstants.INTENT_VALUE_ICC_LOCKED);
broadcastSimStateChanged(slotId, IccCardConstants.INTENT_VALUE_ICC_LOCKED, reason);
+ broadcastSimCardStateChanged(slotId, TelephonyManager.SIM_STATE_PRESENT);
+ broadcastSimApplicationStateChanged(slotId, getSimStateFromLockedReason(reason));
+ }
+
+ private static int getSimStateFromLockedReason(String lockedReason) {
+ switch (lockedReason) {
+ case IccCardConstants.INTENT_VALUE_LOCKED_ON_PIN:
+ return TelephonyManager.SIM_STATE_PIN_REQUIRED;
+ case IccCardConstants.INTENT_VALUE_LOCKED_ON_PUK:
+ return TelephonyManager.SIM_STATE_PUK_REQUIRED;
+ case IccCardConstants.INTENT_VALUE_LOCKED_NETWORK:
+ return TelephonyManager.SIM_STATE_NETWORK_LOCKED;
+ case IccCardConstants.INTENT_VALUE_ABSENT_ON_PERM_DISABLED:
+ return TelephonyManager.SIM_STATE_PERM_DISABLED;
+ default:
+ Rlog.e(LOG_TAG, "Unexpected SIM locked reason " + lockedReason);
+ return TelephonyManager.SIM_STATE_UNKNOWN;
+ }
}
private void handleSimLoaded(int slotId) {
@@ -344,11 +389,11 @@ public class SubscriptionInfoUpdater extends Handler {
logd("handleSimLoaded: IccRecords null");
return;
}
- if (stripIccIdSuffix(records.getFullIccId()) == null) {
+ if (IccUtils.stripTrailingFs(records.getFullIccId()) == null) {
logd("handleSimLoaded: IccID null");
return;
}
- mIccId[slotId] = stripIccIdSuffix(records.getFullIccId());
+ mIccId[slotId] = IccUtils.stripTrailingFs(records.getFullIccId());
if (isAllIccIdQueryDone()) {
updateSubscriptionInfoByIccId();
@@ -424,6 +469,8 @@ public class SubscriptionInfoUpdater extends Handler {
mContext.getContentResolver(), mCurrentlyActiveUserId);
broadcastSimStateChanged(loadedSlotId, IccCardConstants.INTENT_VALUE_ICC_LOADED, null);
+ broadcastSimCardStateChanged(loadedSlotId, TelephonyManager.SIM_STATE_PRESENT);
+ broadcastSimApplicationStateChanged(loadedSlotId, TelephonyManager.SIM_STATE_LOADED);
updateCarrierServices(loadedSlotId, IccCardConstants.INTENT_VALUE_ICC_LOADED);
}
@@ -443,6 +490,9 @@ public class SubscriptionInfoUpdater extends Handler {
updateSubscriptionInfoByIccId();
}
updateCarrierServices(slotId, IccCardConstants.INTENT_VALUE_ICC_ABSENT);
+ broadcastSimStateChanged(slotId, IccCardConstants.INTENT_VALUE_ICC_ABSENT, null);
+ broadcastSimCardStateChanged(slotId, TelephonyManager.SIM_STATE_ABSENT);
+ broadcastSimApplicationStateChanged(slotId, TelephonyManager.SIM_STATE_NOT_READY);
}
private void handleSimError(int slotId) {
@@ -454,6 +504,10 @@ public class SubscriptionInfoUpdater extends Handler {
updateSubscriptionInfoByIccId();
}
updateCarrierServices(slotId, IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR);
+ broadcastSimStateChanged(slotId, IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR,
+ IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR);
+ broadcastSimCardStateChanged(slotId, TelephonyManager.SIM_STATE_CARD_IO_ERROR);
+ broadcastSimApplicationStateChanged(slotId, TelephonyManager.SIM_STATE_NOT_READY);
}
/**
@@ -765,12 +819,65 @@ public class SubscriptionInfoUpdater extends Handler {
IntentBroadcaster.getInstance().broadcastStickyIntent(i, slotId);
}
- // Remove trailing F's from full hexadecimal IccId, as they should be considered padding
- private String stripIccIdSuffix(String hexIccId) {
- if (hexIccId == null) {
- return null;
- } else {
- return hexIccId.replaceAll("(?i)f*$", "");
+ private void broadcastSimCardStateChanged(int phoneId, int state) {
+ if (state != sSimCardState[phoneId]) {
+ sSimCardState[phoneId] = state;
+ Intent i = new Intent(TelephonyManager.ACTION_SIM_CARD_STATE_CHANGED);
+ i.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ i.putExtra(TelephonyManager.EXTRA_SIM_STATE, state);
+ SubscriptionManager.putPhoneIdAndSubIdExtra(i, phoneId);
+ logd("Broadcasting intent ACTION_SIM_CARD_STATE_CHANGED " + simStateString(state)
+ + " for phone: " + phoneId);
+ mContext.sendBroadcast(i, Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
+ }
+ }
+
+ private void broadcastSimApplicationStateChanged(int phoneId, int state) {
+ // Broadcast if the state has changed, except if old state was UNKNOWN and new is NOT_READY,
+ // because that's the initial state and a broadcast should be sent only on a transition
+ // after SIM is PRESENT
+ if (!(state == sSimApplicationState[phoneId]
+ || (state == TelephonyManager.SIM_STATE_NOT_READY
+ && sSimApplicationState[phoneId] == TelephonyManager.SIM_STATE_UNKNOWN))) {
+ sSimApplicationState[phoneId] = state;
+ Intent i = new Intent(TelephonyManager.ACTION_SIM_APPLICATION_STATE_CHANGED);
+ i.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ i.putExtra(TelephonyManager.EXTRA_SIM_STATE, state);
+ SubscriptionManager.putPhoneIdAndSubIdExtra(i, phoneId);
+ logd("Broadcasting intent ACTION_SIM_APPLICATION_STATE_CHANGED " + simStateString(state)
+ + " for phone: " + phoneId);
+ mContext.sendBroadcast(i, Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
+ }
+ }
+
+ private static String simStateString(int state) {
+ switch (state) {
+ case TelephonyManager.SIM_STATE_UNKNOWN:
+ return "UNKNOWN";
+ case TelephonyManager.SIM_STATE_ABSENT:
+ return "ABSENT";
+ case TelephonyManager.SIM_STATE_PIN_REQUIRED:
+ return "PIN_REQUIRED";
+ case TelephonyManager.SIM_STATE_PUK_REQUIRED:
+ return "PUK_REQUIRED";
+ case TelephonyManager.SIM_STATE_NETWORK_LOCKED:
+ return "NETWORK_LOCKED";
+ case TelephonyManager.SIM_STATE_READY:
+ return "READY";
+ case TelephonyManager.SIM_STATE_NOT_READY:
+ return "NOT_READY";
+ case TelephonyManager.SIM_STATE_PERM_DISABLED:
+ return "PERM_DISABLED";
+ case TelephonyManager.SIM_STATE_CARD_IO_ERROR:
+ return "CARD_IO_ERROR";
+ case TelephonyManager.SIM_STATE_CARD_RESTRICTED:
+ return "CARD_RESTRICTED";
+ case TelephonyManager.SIM_STATE_LOADED:
+ return "LOADED";
+ case TelephonyManager.SIM_STATE_PRESENT:
+ return "PRESENT";
+ default:
+ return "INVALID";
}
}
diff --git a/com/android/internal/telephony/TelephonyComponentFactory.java b/com/android/internal/telephony/TelephonyComponentFactory.java
index aeed9dee..38073b84 100644
--- a/com/android/internal/telephony/TelephonyComponentFactory.java
+++ b/com/android/internal/telephony/TelephonyComponentFactory.java
@@ -29,6 +29,9 @@ import com.android.internal.telephony.imsphone.ImsExternalCallTracker;
import com.android.internal.telephony.imsphone.ImsPhone;
import com.android.internal.telephony.imsphone.ImsPhoneCallTracker;
import com.android.internal.telephony.uicc.IccCardProxy;
+import com.android.internal.telephony.uicc.IccCardStatus;
+import com.android.internal.telephony.uicc.UiccCard;
+import com.android.internal.telephony.uicc.UiccProfile;
/**
* This class has one-line methods to instantiate objects only. The purpose is to make code
@@ -68,13 +71,6 @@ public class TelephonyComponentFactory {
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);
}
@@ -107,6 +103,14 @@ public class TelephonyComponentFactory {
return new IccCardProxy(context, ci, phoneId);
}
+ /**
+ * Create a new UiccProfile object.
+ */
+ public UiccProfile makeUiccProfile(Context context, CommandsInterface ci, IccCardStatus ics,
+ int phoneId, UiccCard uiccCard) {
+ return new UiccProfile(context, ci, ics, phoneId, uiccCard);
+ }
+
public EriManager makeEriManager(Phone phone, Context context, int eriFileSource) {
return new EriManager(phone, context, eriFileSource);
}
diff --git a/com/android/internal/telephony/TelephonyIntents.java b/com/android/internal/telephony/TelephonyIntents.java
index f29d993c..51369d06 100644
--- a/com/android/internal/telephony/TelephonyIntents.java
+++ b/com/android/internal/telephony/TelephonyIntents.java
@@ -486,4 +486,10 @@ public class TelephonyIntents {
*/
public static final String ACTION_REQUEST_OMADM_CONFIGURATION_UPDATE =
"com.android.omadm.service.CONFIGURATION_UPDATE";
+
+ /**
+ * Broadcast action to trigger the Carrier Certificate download.
+ */
+ public static final String ACTION_CARRIER_CERTIFICATE_DOWNLOAD =
+ "com.android.internal.telephony.ACTION_CARRIER_CERTIFICATE_DOWNLOAD";
}
diff --git a/com/android/internal/telephony/TimeServiceHelper.java b/com/android/internal/telephony/TimeServiceHelper.java
index fb1efd05..94b094fc 100644
--- a/com/android/internal/telephony/TimeServiceHelper.java
+++ b/com/android/internal/telephony/TimeServiceHelper.java
@@ -91,6 +91,20 @@ public class TimeServiceHelper {
}
/**
+ * Returns the same value as {@link System#currentTimeMillis()}.
+ */
+ public long currentTimeMillis() {
+ return System.currentTimeMillis();
+ }
+
+ /**
+ * Returns the same value as {@link SystemClock#elapsedRealtime()}.
+ */
+ public long elapsedRealtime() {
+ return SystemClock.elapsedRealtime();
+ }
+
+ /**
* Returns true if the device has an explicit time zone set.
*/
public boolean isTimeZoneSettingInitialized() {
diff --git a/com/android/internal/telephony/TimeZoneLookupHelper.java b/com/android/internal/telephony/TimeZoneLookupHelper.java
new file mode 100644
index 00000000..101fddd7
--- /dev/null
+++ b/com/android/internal/telephony/TimeZoneLookupHelper.java
@@ -0,0 +1,298 @@
+/*
+ * 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.text.TextUtils;
+
+import libcore.util.CountryTimeZones;
+import libcore.util.TimeZoneFinder;
+
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * An interface to various time zone lookup behaviors.
+ */
+// Non-final to allow mocking.
+public class TimeZoneLookupHelper {
+
+ /**
+ * The result of looking up a time zone using offset information (and possibly more).
+ */
+ public static final class OffsetResult {
+
+ /** A zone that matches the supplied criteria. See also {@link #isOnlyMatch}. */
+ public final String zoneId;
+
+ /** True if there is only one matching time zone for the supplied criteria. */
+ public final boolean isOnlyMatch;
+
+ public OffsetResult(String zoneId, boolean isOnlyMatch) {
+ this.zoneId = zoneId;
+ this.isOnlyMatch = isOnlyMatch;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ OffsetResult result = (OffsetResult) o;
+
+ if (isOnlyMatch != result.isOnlyMatch) {
+ return false;
+ }
+ return zoneId.equals(result.zoneId);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = zoneId.hashCode();
+ result = 31 * result + (isOnlyMatch ? 1 : 0);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Result{"
+ + "zoneId='" + zoneId + '\''
+ + ", isOnlyMatch=" + isOnlyMatch
+ + '}';
+ }
+ }
+
+ /**
+ * The result of looking up a time zone using country information.
+ */
+ public static final class CountryResult {
+
+ /** A time zone for the country. */
+ public final String zoneId;
+
+ /**
+ * True if all the time zones in the country have the same offset at {@link #whenMillis}.
+ */
+ public final boolean allZonesHaveSameOffset;
+
+ /** The time associated with {@link #allZonesHaveSameOffset}. */
+ public final long whenMillis;
+
+ public CountryResult(String zoneId, boolean allZonesHaveSameOffset, long whenMillis) {
+ this.zoneId = zoneId;
+ this.allZonesHaveSameOffset = allZonesHaveSameOffset;
+ this.whenMillis = whenMillis;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ CountryResult that = (CountryResult) o;
+
+ if (allZonesHaveSameOffset != that.allZonesHaveSameOffset) {
+ return false;
+ }
+ if (whenMillis != that.whenMillis) {
+ return false;
+ }
+ return zoneId.equals(that.zoneId);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = zoneId.hashCode();
+ result = 31 * result + (allZonesHaveSameOffset ? 1 : 0);
+ result = 31 * result + (int) (whenMillis ^ (whenMillis >>> 32));
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "CountryResult{"
+ + "zoneId='" + zoneId + '\''
+ + ", allZonesHaveSameOffset=" + allZonesHaveSameOffset
+ + ", whenMillis=" + whenMillis
+ + '}';
+ }
+ }
+
+ private static final int MS_PER_HOUR = 60 * 60 * 1000;
+
+ /** The last CountryTimeZones object retrieved. */
+ private CountryTimeZones mLastCountryTimeZones;
+
+ public TimeZoneLookupHelper() {}
+
+ /**
+ * Looks for a time zone for the supplied NITZ and country information.
+ *
+ * <p><em>Note:</em> When there are multiple matching zones then one of the matching candidates
+ * will be returned in the result. If the current device default zone matches it will be
+ * returned in preference to other candidates. This method can return {@code null} if no
+ * matching time zones are found.
+ */
+ public OffsetResult lookupByNitzCountry(NitzData nitzData, String isoCountryCode) {
+ CountryTimeZones countryTimeZones = getCountryTimeZones(isoCountryCode);
+ if (countryTimeZones == null) {
+ return null;
+ }
+ android.icu.util.TimeZone bias = android.icu.util.TimeZone.getDefault();
+
+ CountryTimeZones.OffsetResult offsetResult = countryTimeZones.lookupByOffsetWithBias(
+ nitzData.getLocalOffsetMillis(), nitzData.isDst(),
+ nitzData.getCurrentTimeInMillis(), bias);
+
+ if (offsetResult == null) {
+ return null;
+ }
+ return new OffsetResult(offsetResult.mTimeZone.getID(), offsetResult.mOneMatch);
+ }
+
+ /**
+ * Looks for a time zone using only information present in the supplied {@link NitzData} object.
+ *
+ * <p><em>Note:</em> Because multiple time zones can have the same offset / DST state at a given
+ * time this process is error prone; an arbitrary match is returned when there are multiple
+ * candidates. The algorithm can also return a non-exact match by assuming that the DST
+ * information provided by NITZ is incorrect. This method can return {@code null} if no matching
+ * time zones are found.
+ */
+ public OffsetResult lookupByNitz(NitzData nitzData) {
+ return lookupByNitzStatic(nitzData);
+ }
+
+ /**
+ * Returns a time zone ID for the country if possible. For counties that use a single time zone
+ * this will provide a good choice. For countries with multiple time zones, a time zone is
+ * returned if all time zones used in the country currently have the same offset (currently ==
+ * according to the device's current system clock time). If this is not the case then
+ * {@code null} can be returned.
+ */
+ public CountryResult lookupByCountry(String isoCountryCode, long whenMillis) {
+ CountryTimeZones countryTimeZones = getCountryTimeZones(isoCountryCode);
+ if (countryTimeZones == null) {
+ // Unknown country code.
+ return null;
+ }
+ if (countryTimeZones.getDefaultTimeZoneId() == null) {
+ return null;
+ }
+
+ return new CountryResult(
+ countryTimeZones.getDefaultTimeZoneId(),
+ countryTimeZones.isDefaultOkForCountryTimeZoneDetection(whenMillis),
+ whenMillis);
+ }
+
+ /**
+ * Finds a time zone using only information present in the supplied {@link NitzData} object.
+ * This is a static method for use by {@link ServiceStateTracker}.
+ *
+ * <p><em>Note:</em> Because multiple time zones can have the same offset / DST state at a given
+ * time this process is error prone; an arbitrary match is returned when there are multiple
+ * candidates. The algorithm can also return a non-exact match by assuming that the DST
+ * information provided by NITZ is incorrect. This method can return {@code null} if no matching
+ * time zones are found.
+ */
+ static TimeZone guessZoneByNitzStatic(NitzData nitzData) {
+ OffsetResult result = lookupByNitzStatic(nitzData);
+ return result != null ? TimeZone.getTimeZone(result.zoneId) : null;
+ }
+
+ private static OffsetResult lookupByNitzStatic(NitzData nitzData) {
+ int utcOffsetMillis = nitzData.getLocalOffsetMillis();
+ boolean isDst = nitzData.isDst();
+ long timeMillis = nitzData.getCurrentTimeInMillis();
+
+ OffsetResult match = lookupByInstantOffsetDst(timeMillis, utcOffsetMillis, isDst);
+ if (match == null) {
+ // Couldn't find a proper timezone. Perhaps the DST data is wrong.
+ match = lookupByInstantOffsetDst(timeMillis, utcOffsetMillis, !isDst);
+ }
+ return match;
+ }
+
+ private static OffsetResult lookupByInstantOffsetDst(long timeMillis, int utcOffsetMillis,
+ boolean isDst) {
+ int rawOffset = utcOffsetMillis;
+ if (isDst) {
+ rawOffset -= MS_PER_HOUR;
+ }
+ String[] zones = TimeZone.getAvailableIDs(rawOffset);
+ TimeZone match = null;
+ Date d = new Date(timeMillis);
+ boolean isOnlyMatch = true;
+ for (String zone : zones) {
+ TimeZone tz = TimeZone.getTimeZone(zone);
+ if (tz.getOffset(timeMillis) == utcOffsetMillis && tz.inDaylightTime(d) == isDst) {
+ if (match == null) {
+ match = tz;
+ } else {
+ isOnlyMatch = false;
+ break;
+ }
+ }
+ }
+
+ if (match == null) {
+ return null;
+ }
+ return new OffsetResult(match.getID(), isOnlyMatch);
+ }
+
+ /**
+ * Returns {@code true} if the supplied (lower-case) ISO country code is for a country known to
+ * use a raw offset of zero from UTC at the time specified.
+ */
+ public boolean countryUsesUtc(String isoCountryCode, long whenMillis) {
+ if (TextUtils.isEmpty(isoCountryCode)) {
+ return false;
+ }
+
+ CountryTimeZones countryTimeZones = getCountryTimeZones(isoCountryCode);
+ return countryTimeZones != null && countryTimeZones.hasUtcZone(whenMillis);
+ }
+
+ private CountryTimeZones getCountryTimeZones(String isoCountryCode) {
+ // A single entry cache of the last CountryTimeZones object retrieved since there should
+ // be strong consistency across calls.
+ synchronized (this) {
+ if (mLastCountryTimeZones != null) {
+ if (mLastCountryTimeZones.isForCountryCode(isoCountryCode)) {
+ return mLastCountryTimeZones;
+ }
+ }
+
+ // Perform the lookup. It's very unlikely to return null, but we won't cache null.
+ CountryTimeZones countryTimeZones =
+ TimeZoneFinder.getInstance().lookupCountryTimeZones(isoCountryCode);
+ if (countryTimeZones != null) {
+ mLastCountryTimeZones = countryTimeZones;
+ }
+ return countryTimeZones;
+ }
+ }
+}
diff --git a/com/android/internal/telephony/VisualVoicemailSmsFilter.java b/com/android/internal/telephony/VisualVoicemailSmsFilter.java
index 1294762e..a87cbf1d 100644
--- a/com/android/internal/telephony/VisualVoicemailSmsFilter.java
+++ b/com/android/internal/telephony/VisualVoicemailSmsFilter.java
@@ -95,6 +95,7 @@ public class VisualVoicemailSmsFilter {
* Wrapper to combine multiple PDU into an SMS message
*/
private static class FullMessage {
+
public SmsMessage firstMessage;
public String fullMessageBody;
}
@@ -143,7 +144,7 @@ public class VisualVoicemailSmsFilter {
WrappedMessageData messageData = VisualVoicemailSmsParser
.parseAlternativeFormat(asciiMessage);
if (messageData != null) {
- sendVvmSmsBroadcast(context, phoneAccountHandle, messageData, null);
+ sendVvmSmsBroadcast(context, settings, phoneAccountHandle, messageData, null);
}
// Confidence for what the message actually is is low. Don't remove the message and let
// system decide. Usually because it is not parsable it will be dropped.
@@ -177,7 +178,7 @@ public class VisualVoicemailSmsFilter {
return false;
}
- sendVvmSmsBroadcast(context, phoneAccountHandle, messageData, null);
+ sendVvmSmsBroadcast(context, settings, phoneAccountHandle, messageData, null);
return true;
}
@@ -193,7 +194,7 @@ public class VisualVoicemailSmsFilter {
if (pattern.matcher(messageBody).matches()) {
Log.w(TAG, "Incoming SMS matches pattern " + pattern + " but has illegal format, "
+ "still dropping as VVM SMS");
- sendVvmSmsBroadcast(context, phoneAccountHandle, null, messageBody);
+ sendVvmSmsBroadcast(context, settings, phoneAccountHandle, null, messageBody);
return true;
}
}
@@ -233,7 +234,8 @@ public class VisualVoicemailSmsFilter {
}
}
- private static void sendVvmSmsBroadcast(Context context, PhoneAccountHandle phoneAccountHandle,
+ private static void sendVvmSmsBroadcast(Context context,
+ VisualVoicemailSmsFilterSettings filterSettings, PhoneAccountHandle phoneAccountHandle,
@Nullable WrappedMessageData messageData, @Nullable String messageBody) {
Log.i(TAG, "VVM SMS received");
Intent intent = new Intent(VoicemailContract.ACTION_VOICEMAIL_SMS_RECEIVED);
@@ -247,6 +249,7 @@ public class VisualVoicemailSmsFilter {
}
builder.setPhoneAccountHandle(phoneAccountHandle);
intent.putExtra(VoicemailContract.EXTRA_VOICEMAIL_SMS, builder.build());
+ intent.putExtra(VoicemailContract.EXTRA_TARGET_PACKAGE, filterSettings.packageName);
intent.setPackage(TELEPHONY_SERVICE_PACKAGE);
context.sendBroadcast(intent);
}
diff --git a/com/android/internal/telephony/cat/CatService.java b/com/android/internal/telephony/cat/CatService.java
index 0c79118c..91d01e6f 100644
--- a/com/android/internal/telephony/cat/CatService.java
+++ b/com/android/internal/telephony/cat/CatService.java
@@ -16,6 +16,11 @@
package com.android.internal.telephony.cat;
+import static com.android.internal.telephony.cat.CatCmdMessage.SetupEventListConstants
+ .IDLE_SCREEN_AVAILABLE_EVENT;
+import static com.android.internal.telephony.cat.CatCmdMessage.SetupEventListConstants
+ .LANGUAGE_SELECTION_EVENT;
+
import android.app.ActivityManagerNative;
import android.app.IActivityManager;
import android.app.backup.BackupManager;
@@ -31,31 +36,26 @@ import android.os.HandlerThread;
import android.os.LocaleList;
import android.os.Message;
import android.os.RemoteException;
-import android.os.SystemProperties;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import com.android.internal.telephony.CommandsInterface;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.SubscriptionController;
+import com.android.internal.telephony.uicc.IccCardStatus.CardState;
import com.android.internal.telephony.uicc.IccFileHandler;
import com.android.internal.telephony.uicc.IccRecords;
+import com.android.internal.telephony.uicc.IccRefreshResponse;
import com.android.internal.telephony.uicc.IccUtils;
import com.android.internal.telephony.uicc.UiccCard;
import com.android.internal.telephony.uicc.UiccCardApplication;
-import com.android.internal.telephony.uicc.IccCardStatus.CardState;
-import com.android.internal.telephony.uicc.IccRefreshResponse;
import com.android.internal.telephony.uicc.UiccController;
+import com.android.internal.telephony.uicc.UiccProfile;
import java.io.ByteArrayOutputStream;
import java.util.List;
import java.util.Locale;
-import static com.android.internal.telephony.cat.CatCmdMessage.
- SetupEventListConstants.IDLE_SCREEN_AVAILABLE_EVENT;
-import static com.android.internal.telephony.cat.CatCmdMessage.
- SetupEventListConstants.LANGUAGE_SELECTION_EVENT;
-
class RilMessage {
int mId;
Object mData;
@@ -134,9 +134,9 @@ public class CatService extends Handler implements AppInterface {
/* For multisim catservice should not be singleton */
private CatService(CommandsInterface ci, UiccCardApplication ca, IccRecords ir,
- Context context, IccFileHandler fh, UiccCard ic, int slotId) {
+ Context context, IccFileHandler fh, UiccProfile uiccProfile, int slotId) {
if (ci == null || ca == null || ir == null || context == null || fh == null
- || ic == null) {
+ || uiccProfile == null) {
throw new NullPointerException(
"Service: Input parameters must not be null");
}
@@ -192,15 +192,15 @@ public class CatService extends Handler implements AppInterface {
* @return The only Service object in the system
*/
public static CatService getInstance(CommandsInterface ci,
- Context context, UiccCard ic, int slotId) {
+ Context context, UiccProfile uiccProfile, int slotId) {
UiccCardApplication ca = null;
IccFileHandler fh = null;
IccRecords ir = null;
- if (ic != null) {
+ if (uiccProfile != null) {
/* Since Cat is not tied to any application, but rather is Uicc application
* in itself - just get first FileHandler and IccRecords object
*/
- ca = ic.getApplicationIndex(0);
+ ca = uiccProfile.getApplicationIndex(0);
if (ca != null) {
fh = ca.getIccFileHandler();
ir = ca.getIccRecords();
@@ -217,11 +217,11 @@ public class CatService extends Handler implements AppInterface {
}
if (sInstance[slotId] == null) {
if (ci == null || ca == null || ir == null || context == null || fh == null
- || ic == null) {
+ || uiccProfile == null) {
return null;
}
- sInstance[slotId] = new CatService(ci, ca, ir, context, fh, ic, slotId);
+ sInstance[slotId] = new CatService(ci, ca, ir, context, fh, uiccProfile, slotId);
} else if ((ir != null) && (mIccRecords != ir)) {
if (mIccRecords != null) {
mIccRecords.unregisterForRecordsLoaded(sInstance[slotId]);
@@ -1102,15 +1102,15 @@ public class CatService extends Handler implements AppInterface {
}
public void update(CommandsInterface ci,
- Context context, UiccCard ic) {
+ Context context, UiccProfile uiccProfile) {
UiccCardApplication ca = null;
IccRecords ir = null;
- if (ic != null) {
+ if (uiccProfile != null) {
/* Since Cat is not tied to any application, but rather is Uicc application
* in itself - just get first FileHandler and IccRecords object
*/
- ca = ic.getApplicationIndex(0);
+ ca = uiccProfile.getApplicationIndex(0);
if (ca != null) {
ir = ca.getIccRecords();
}
diff --git a/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java b/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java
index 1cfdc334..97f4c836 100644
--- a/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java
+++ b/com/android/internal/telephony/cdma/CdmaSMSDispatcher.java
@@ -20,38 +20,31 @@ import android.app.Activity;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Intent;
-import android.net.Uri;
import android.os.Message;
-import android.os.SystemProperties;
import android.provider.Telephony.Sms;
import android.telephony.Rlog;
import android.telephony.ServiceState;
-import android.telephony.SmsManager;
import android.telephony.TelephonyManager;
+import android.util.Pair;
-import com.android.internal.telephony.GsmAlphabet;
import com.android.internal.telephony.GsmCdmaPhone;
-import com.android.internal.telephony.ImsSMSDispatcher;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
-import com.android.internal.telephony.SMSDispatcher;
import com.android.internal.telephony.SmsConstants;
+import com.android.internal.telephony.SMSDispatcher;
+import com.android.internal.telephony.SmsDispatchersController;
import com.android.internal.telephony.SmsHeader;
-import com.android.internal.telephony.SmsUsageMonitor;
-import com.android.internal.telephony.TelephonyProperties;
-import com.android.internal.telephony.cdma.sms.UserData;
+import com.android.internal.telephony.util.SMSDispatcherUtil;
+import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
+import com.android.internal.telephony.SmsMessageBase;
-import java.util.HashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
public class CdmaSMSDispatcher extends SMSDispatcher {
private static final String TAG = "CdmaSMSDispatcher";
private static final boolean VDBG = false;
- public CdmaSMSDispatcher(Phone phone, SmsUsageMonitor usageMonitor,
- ImsSMSDispatcher imsSMSDispatcher) {
- super(phone, usageMonitor, imsSMSDispatcher);
+ public CdmaSMSDispatcher(Phone phone, SmsDispatchersController smsDispatchersController) {
+ super(phone, smsDispatchersController);
Rlog.d(TAG, "CdmaSMSDispatcher created");
}
@@ -79,152 +72,46 @@ public class CdmaSMSDispatcher extends SMSDispatcher {
}
}
- /**
- * Called from parent class to handle status report from {@code CdmaInboundSmsHandler}.
- * @param sms the CDMA SMS message to process
- */
- private void handleCdmaStatusReport(SmsMessage sms) {
- for (int i = 0, count = deliveryPendingList.size(); i < count; i++) {
- SmsTracker tracker = deliveryPendingList.get(i);
- if (tracker.mMessageRef == sms.mMessageRef) {
- // Found it. Remove from list and broadcast.
- deliveryPendingList.remove(i);
- // Update the message status (COMPLETE)
- tracker.updateSentMessageStatus(mContext, Sms.STATUS_COMPLETE);
-
- PendingIntent intent = tracker.mDeliveryIntent;
- Intent fillIn = new Intent();
- fillIn.putExtra("pdu", sms.getPdu());
- fillIn.putExtra("format", getFormat());
- try {
- intent.send(mContext, Activity.RESULT_OK, fillIn);
- } catch (CanceledException ex) {}
- break; // Only expect to see one tracker matching this message.
- }
- }
- }
-
- /** {@inheritDoc} */
- @Override
- public void sendData(String destAddr, String scAddr, int destPort,
- byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
- SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
- scAddr, destAddr, destPort, data, (deliveryIntent != null));
- if (pdu != null) {
- HashMap map = getSmsTrackerMap(destAddr, scAddr, destPort, data, pdu);
- SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
- null /*messageUri*/, false /*isExpectMore*/, null /*fullMessageText*/,
- false /*isText*/, true /*persistMessage*/);
-
- String carrierPackage = getCarrierAppPackageName();
- if (carrierPackage != null) {
- Rlog.d(TAG, "Found carrier package.");
- DataSmsSender smsSender = new DataSmsSender(tracker);
- smsSender.sendSmsByCarrierApp(carrierPackage, new SmsSenderCallback(smsSender));
- } else {
- Rlog.v(TAG, "No carrier package.");
- sendSubmitPdu(tracker);
- }
- } else {
- Rlog.e(TAG, "CdmaSMSDispatcher.sendData(): getSubmitPdu() returned null");
- if (sentIntent != null) {
- try {
- sentIntent.send(SmsManager.RESULT_ERROR_GENERIC_FAILURE);
- } catch (CanceledException ex) {
- Rlog.e(TAG, "Intent has been canceled!");
- }
- }
- }
- }
-
- /** {@inheritDoc} */
@Override
- public void sendText(String destAddr, String scAddr, String text, PendingIntent sentIntent,
- PendingIntent deliveryIntent, Uri messageUri, String callingPkg,
- boolean persistMessage) {
- SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
- scAddr, destAddr, text, (deliveryIntent != null), null);
- if (pdu != null) {
- HashMap map = getSmsTrackerMap(destAddr, scAddr, text, pdu);
- SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
- messageUri, false /*isExpectMore*/, text, true /*isText*/, persistMessage);
-
- String carrierPackage = getCarrierAppPackageName();
- if (carrierPackage != null) {
- Rlog.d(TAG, "Found carrier package.");
- TextSmsSender smsSender = new TextSmsSender(tracker);
- smsSender.sendSmsByCarrierApp(carrierPackage, new SmsSenderCallback(smsSender));
- } else {
- Rlog.v(TAG, "No carrier package.");
- sendSubmitPdu(tracker);
- }
- } else {
- Rlog.e(TAG, "CdmaSMSDispatcher.sendText(): getSubmitPdu() returned null");
- if (sentIntent != null) {
- try {
- sentIntent.send(SmsManager.RESULT_ERROR_GENERIC_FAILURE);
- } catch (CanceledException ex) {
- Rlog.e(TAG, "Intent has been canceled!");
- }
- }
- }
+ protected boolean shouldBlockSms() {
+ return SMSDispatcherUtil.shouldBlockSms(isCdmaMo(), mPhone);
}
- /** {@inheritDoc} */
@Override
- protected void injectSmsPdu(byte[] pdu, String format, PendingIntent receivedIntent) {
- throw new IllegalStateException("This method must be called only on ImsSMSDispatcher");
+ protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
+ String message, boolean statusReportRequested, SmsHeader smsHeader) {
+ return SMSDispatcherUtil.getSubmitPduCdma(scAddr, destAddr, message,
+ statusReportRequested, smsHeader);
}
- /** {@inheritDoc} */
@Override
- protected GsmAlphabet.TextEncodingDetails calculateLength(CharSequence messageBody,
- boolean use7bitOnly) {
- return SmsMessage.calculateLength(messageBody, use7bitOnly, false);
+ protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
+ int destPort, byte[] message, boolean statusReportRequested) {
+ return SMSDispatcherUtil.getSubmitPduCdma(scAddr, destAddr, destPort, message,
+ statusReportRequested);
}
- /** {@inheritDoc} */
@Override
- protected 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) {
- UserData uData = new UserData();
- uData.payloadStr = message;
- uData.userDataHeader = smsHeader;
- if (encoding == SmsConstants.ENCODING_7BIT) {
- uData.msgEncoding = UserData.ENCODING_GSM_7BIT_ALPHABET;
- } else { // assume UTF-16
- uData.msgEncoding = UserData.ENCODING_UNICODE_16;
- }
- uData.msgEncodingSet = true;
-
- /* By setting the statusReportRequested bit only for the
- * last message fragment, this will result in only one
- * callback to the sender when that last fragment delivery
- * has been acknowledged. */
- SmsMessage.SubmitPdu submitPdu = SmsMessage.getSubmitPdu(destinationAddress,
- uData, (deliveryIntent != null) && lastPart);
-
- HashMap map = getSmsTrackerMap(destinationAddress, scAddress,
- message, submitPdu);
- return getSmsTracker(map, sentIntent, deliveryIntent,
- getFormat(), unsentPartCount, anyPartFailed, messageUri, smsHeader,
- false /*isExpextMore*/, fullMessageText, true /*isText*/,
- true /*persistMessage*/);
+ protected TextEncodingDetails calculateLength(CharSequence messageBody, boolean use7bitOnly) {
+ return SMSDispatcherUtil.calculateLengthCdma(messageBody, use7bitOnly);
}
-
- @Override
- protected void sendSubmitPdu(SmsTracker tracker) {
- if (mPhone.isInEcm()) {
- if (VDBG) {
- Rlog.d(TAG, "Block SMS in Emergency Callback mode");
+ /**
+ * Called from parent class to handle status report from {@code CdmaInboundSmsHandler}.
+ * @param sms the CDMA SMS message to process
+ */
+ private void handleCdmaStatusReport(SmsMessage sms) {
+ for (int i = 0, count = deliveryPendingList.size(); i < count; i++) {
+ SmsTracker tracker = deliveryPendingList.get(i);
+ if (tracker.mMessageRef == sms.mMessageRef) {
+ Pair<Boolean, Boolean> result =
+ mSmsDispatchersController.handleSmsStatusReport(tracker, getFormat(),
+ sms.getPdu());
+ if (result.second) {
+ deliveryPendingList.remove(i);
+ }
+ break; // Only expect to see one tracker matching this message.
}
- tracker.onFailed(mContext, SmsManager.RESULT_ERROR_NO_SERVICE, 0/*errorCode*/);
- return;
}
- sendRawPdu(tracker);
}
/** {@inheritDoc} */
@@ -237,12 +124,6 @@ public class CdmaSMSDispatcher extends SMSDispatcher {
+ " mMessageRef=" + tracker.mMessageRef
+ " SS=" + mPhone.getServiceState().getState());
- sendSmsByPstn(tracker);
- }
-
- /** {@inheritDoc} */
- @Override
- protected void sendSmsByPstn(SmsTracker tracker) {
int ss = mPhone.getServiceState().getState();
// if sms over IMS is not supported on data and voice is not available...
if (!isIms() && ss != ServiceState.STATE_IN_SERVICE) {
diff --git a/com/android/internal/telephony/dataconnection/ApnSetting.java b/com/android/internal/telephony/dataconnection/ApnSetting.java
index 0eeed6a0..9394ecb8 100644
--- a/com/android/internal/telephony/dataconnection/ApnSetting.java
+++ b/com/android/internal/telephony/dataconnection/ApnSetting.java
@@ -49,6 +49,7 @@ public class ApnSetting {
static final String V2_FORMAT_REGEX = "^\\[ApnSettingV2\\]\\s*";
static final String V3_FORMAT_REGEX = "^\\[ApnSettingV3\\]\\s*";
+ static final String V4_FORMAT_REGEX = "^\\[ApnSettingV4\\]\\s*";
static final String TAG = "ApnSetting";
public final String carrier;
@@ -79,7 +80,10 @@ public class ApnSetting {
* To check what values can hold, refer to ServiceState.java.
* This should be spread to other technologies,
* but currently only used for LTE(14) and EHRPD(13).
+ *
+ * @deprecated use {@code networkTypeBitmask} instead
*/
+ @Deprecated
private final int bearer;
/**
* Radio Access Technology info
@@ -87,9 +91,19 @@ public class ApnSetting {
* technologies in ServiceState.
* This should be spread to other technologies,
* but currently only used for LTE(14) and EHRPD(13).
+ *
+ * @deprecated use {@code networkTypeBitmask} instead
*/
+ @Deprecated
public final int bearerBitmask;
+ /**
+ * Radio Technology (Network Type) info
+ * To check what values can hold, refer to TelephonyManager.java. This is a bitmask of radio
+ * technologies ({@code NETWORK_TYPE_} constants) in {@link TelephonyManager}.
+ */
+ public final int networkTypeBitmask;
+
/* ID of the profile in the modem */
public final int profileId;
public final boolean modemCognitive;
@@ -120,6 +134,11 @@ public class ApnSetting {
* */
public boolean permanentFailed = false;
+ /**
+ * @deprecated this constructor is no longer supported. Use the other constructor which takes
+ * a network type bitmask instead of the deprecated bearer bitmask and bearer field.
+ * */
+ @Deprecated
public ApnSetting(int id, String numeric, String carrier, String apn,
String proxy, String port,
String mmsc, String mmsProxy, String mmsPort,
@@ -160,14 +179,59 @@ public class ApnSetting {
this.mtu = mtu;
this.mvnoType = mvnoType;
this.mvnoMatchData = mvnoMatchData;
+ this.networkTypeBitmask = ServiceState.convertBearerBitmaskToNetworkTypeBitmask(
+ this.bearerBitmask);
+ }
+ public ApnSetting(int id, String numeric, String carrier, String apn,
+ String proxy, String port,
+ String mmsc, String mmsProxy, String mmsPort,
+ String user, String password, int authType, String[] types,
+ String protocol, String roamingProtocol, boolean carrierEnabled,
+ int networkTypeBitmask, int profileId, boolean modemCognitive, int maxConns,
+ int waitTime, int maxConnsTime, int mtu, String mvnoType,
+ String mvnoMatchData) {
+ this.id = id;
+ this.numeric = numeric;
+ this.carrier = carrier;
+ this.apn = apn;
+ this.proxy = proxy;
+ this.port = port;
+ this.mmsc = mmsc;
+ this.mmsProxy = mmsProxy;
+ this.mmsPort = mmsPort;
+ this.user = user;
+ this.password = password;
+ this.authType = authType;
+ this.types = new String[types.length];
+ int apnBitmap = 0;
+ for (int i = 0; i < types.length; i++) {
+ this.types[i] = types[i].toLowerCase();
+ apnBitmap |= getApnBitmask(this.types[i]);
+ }
+ this.typesBitmap = apnBitmap;
+ this.protocol = protocol;
+ this.roamingProtocol = roamingProtocol;
+ this.carrierEnabled = carrierEnabled;
+ this.bearer = 0;
+ this.bearerBitmask =
+ ServiceState.convertNetworkTypeBitmaskToBearerBitmask(networkTypeBitmask);
+ this.networkTypeBitmask = networkTypeBitmask;
+ this.profileId = profileId;
+ this.modemCognitive = modemCognitive;
+ this.maxConns = maxConns;
+ this.waitTime = waitTime;
+ this.maxConnsTime = maxConnsTime;
+ this.mtu = mtu;
+ this.mvnoType = mvnoType;
+ this.mvnoMatchData = mvnoMatchData;
}
public ApnSetting(ApnSetting apn) {
this(apn.id, apn.numeric, apn.carrier, apn.apn, apn.proxy, apn.port, apn.mmsc, apn.mmsProxy,
apn.mmsPort, apn.user, apn.password, apn.authType, apn.types, apn.protocol,
- apn.roamingProtocol, apn.carrierEnabled, apn.bearer, apn.bearerBitmask,
- apn.profileId, apn.modemCognitive, apn.maxConns, apn.waitTime, apn.maxConnsTime,
+ apn.roamingProtocol, apn.carrierEnabled, apn.networkTypeBitmask, apn.profileId,
+ apn.modemCognitive, apn.maxConns, apn.waitTime, apn.maxConnsTime,
apn.mtu, apn.mvnoType, apn.mvnoMatchData);
}
@@ -196,6 +260,13 @@ public class ApnSetting {
* <profileId>, <modemCognitive>, <maxConns>, <waitTime>, <maxConnsTime>, <mtu>,
* <mvnoType>, <mvnoMatchData>
*
+ * v4 format:
+ * [ApnSettingV4] <carrier>, <apn>, <proxy>, <port>, <user>, <password>, <server>,
+ * <mmsc>, <mmsproxy>, <mmsport>, <mcc>, <mnc>, <authtype>,
+ * <type>[| <type>...], <protocol>, <roaming_protocol>, <carrierEnabled>, <bearerBitmask>,
+ * <profileId>, <modemCognitive>, <maxConns>, <waitTime>, <maxConnsTime>, <mtu>,
+ * <mvnoType>, <mvnoMatchData>, <networkTypeBitmask>
+ *
* Note that the strings generated by toString() do not contain the username
* and password and thus cannot be read by this method.
*/
@@ -204,7 +275,10 @@ public class ApnSetting {
int version;
// matches() operates on the whole string, so append .* to the regex.
- if (data.matches(V3_FORMAT_REGEX + ".*")) {
+ if (data.matches(V4_FORMAT_REGEX + ".*")) {
+ version = 4;
+ data = data.replaceFirst(V4_FORMAT_REGEX, "");
+ } else if (data.matches(V3_FORMAT_REGEX + ".*")) {
version = 3;
data = data.replaceFirst(V3_FORMAT_REGEX, "");
} else if (data.matches(V2_FORMAT_REGEX + ".*")) {
@@ -230,6 +304,7 @@ public class ApnSetting {
String protocol, roamingProtocol;
boolean carrierEnabled;
int bearerBitmask = 0;
+ int networkTypeBitmask = 0;
int profileId = 0;
boolean modemCognitive = false;
int maxConns = 0;
@@ -275,12 +350,21 @@ public class ApnSetting {
mvnoType = a[24];
mvnoMatchData = a[25];
}
+ if (a.length > 26) {
+ networkTypeBitmask = ServiceState.getBitmaskFromString(a[26]);
+ }
}
- return new ApnSetting(-1,a[10]+a[11],a[0],a[1],a[2],a[3],a[7],a[8],
- a[9],a[4],a[5],authType,typeArray,protocol,roamingProtocol,carrierEnabled,0,
- bearerBitmask, profileId, modemCognitive, maxConns, waitTime, maxConnsTime, mtu,
- mvnoType, mvnoMatchData);
+ // If both bearerBitmask and networkTypeBitmask were specified, bearerBitmask would be
+ // ignored.
+ if (networkTypeBitmask == 0) {
+ networkTypeBitmask =
+ ServiceState.convertBearerBitmaskToNetworkTypeBitmask(bearerBitmask);
+ }
+ return new ApnSetting(-1, a[10] + a[11], a[0], a[1], a[2], a[3], a[7], a[8], a[9], a[4],
+ a[5], authType, typeArray, protocol, roamingProtocol, carrierEnabled,
+ networkTypeBitmask, profileId, modemCognitive, maxConns, waitTime, maxConnsTime,
+ mtu, mvnoType, mvnoMatchData);
}
/**
@@ -309,7 +393,7 @@ public class ApnSetting {
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
- sb.append("[ApnSettingV3] ")
+ sb.append("[ApnSettingV4] ")
.append(carrier)
.append(", ").append(id)
.append(", ").append(numeric)
@@ -340,6 +424,7 @@ public class ApnSetting {
sb.append(", ").append(mvnoType);
sb.append(", ").append(mvnoMatchData);
sb.append(", ").append(permanentFailed);
+ sb.append(", ").append(networkTypeBitmask);
return sb.toString();
}
@@ -565,14 +650,15 @@ public class ApnSetting {
&& maxConnsTime == other.maxConnsTime
&& mtu == other.mtu
&& mvnoType.equals(other.mvnoType)
- && mvnoMatchData.equals(other.mvnoMatchData);
+ && mvnoMatchData.equals(other.mvnoMatchData)
+ && networkTypeBitmask == other.networkTypeBitmask;
}
/**
* Compare two APN settings
*
- * Note: This method does not compare 'id', 'bearer', 'bearerBitmask'. We only use this for
- * determining if tearing a data call is needed when conditions change. See
+ * Note: This method does not compare 'id', 'bearer', 'bearerBitmask', 'networkTypeBitmask'.
+ * We only use this for determining if tearing a data call is needed when conditions change. See
* cleanUpConnectionsOnUpdatedApns in DcTracker.
*
* @param o the other object to compare
@@ -635,7 +721,8 @@ public class ApnSetting {
&& Objects.equals(this.mvnoMatchData, other.mvnoMatchData)
&& xorEquals(this.mmsc, other.mmsc)
&& xorEquals(this.mmsProxy, other.mmsProxy)
- && xorEquals(this.mmsPort, other.mmsPort));
+ && xorEquals(this.mmsPort, other.mmsPort))
+ && this.networkTypeBitmask == other.networkTypeBitmask;
}
// check whether the types of two APN same (even only one type of each APN is same)
diff --git a/com/android/internal/telephony/dataconnection/DataConnection.java b/com/android/internal/telephony/dataconnection/DataConnection.java
index d30301eb..0cc846fd 100644
--- a/com/android/internal/telephony/dataconnection/DataConnection.java
+++ b/com/android/internal/telephony/dataconnection/DataConnection.java
@@ -16,9 +16,14 @@
package com.android.internal.telephony.dataconnection;
+import static android.net.NetworkPolicyManager.OVERRIDE_CONGESTED;
+import static android.net.NetworkPolicyManager.OVERRIDE_UNMETERED;
+
import android.app.PendingIntent;
import android.content.Context;
import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.PacketKeepalive;
+import android.net.KeepalivePacketData;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.NetworkAgent;
@@ -39,10 +44,10 @@ 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;
+import android.util.SparseArray;
import android.util.TimeUtils;
import com.android.internal.annotations.VisibleForTesting;
@@ -65,7 +70,6 @@ import com.android.internal.util.StateMachine;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.io.StringWriter;
-import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
@@ -174,10 +178,11 @@ public class DataConnection extends StateMachine {
private DcFailCause mLastFailCause;
private static final String NULL_IP = "0.0.0.0";
private Object mUserData;
+ private int mSubscriptionOverride;
private int mRilRat = Integer.MAX_VALUE;
private int mDataRegState = Integer.MAX_VALUE;
private NetworkInfo mNetworkInfo;
- private NetworkAgent mNetworkAgent;
+ private DcNetworkAgent mNetworkAgent;
private LocalLog mNetCapsLocalLog = new LocalLog(50);
int mTag;
@@ -204,9 +209,15 @@ public class DataConnection extends StateMachine {
static final int EVENT_BW_REFRESH_RESPONSE = BASE + 14;
static final int EVENT_DATA_CONNECTION_VOICE_CALL_STARTED = BASE + 15;
static final int EVENT_DATA_CONNECTION_VOICE_CALL_ENDED = BASE + 16;
+ static final int EVENT_DATA_CONNECTION_OVERRIDE_CHANGED = BASE + 17;
+ static final int EVENT_KEEPALIVE_STATUS = BASE + 18;
+ static final int EVENT_KEEPALIVE_STARTED = BASE + 19;
+ static final int EVENT_KEEPALIVE_STOPPED = BASE + 20;
+ static final int EVENT_KEEPALIVE_START_REQUEST = BASE + 21;
+ static final int EVENT_KEEPALIVE_STOP_REQUEST = BASE + 22;
private static final int CMD_TO_STRING_COUNT =
- EVENT_DATA_CONNECTION_VOICE_CALL_ENDED - BASE + 1;
+ EVENT_KEEPALIVE_STOP_REQUEST - BASE + 1;
private static String[] sCmdToString = new String[CMD_TO_STRING_COUNT];
static {
@@ -230,6 +241,13 @@ public class DataConnection extends StateMachine {
"EVENT_DATA_CONNECTION_VOICE_CALL_STARTED";
sCmdToString[EVENT_DATA_CONNECTION_VOICE_CALL_ENDED - BASE] =
"EVENT_DATA_CONNECTION_VOICE_CALL_ENDED";
+ sCmdToString[EVENT_DATA_CONNECTION_OVERRIDE_CHANGED - BASE] =
+ "EVENT_DATA_CONNECTION_OVERRIDE_CHANGED";
+ sCmdToString[EVENT_KEEPALIVE_STATUS - BASE] = "EVENT_KEEPALIVE_STATUS";
+ sCmdToString[EVENT_KEEPALIVE_STARTED - BASE] = "EVENT_KEEPALIVE_STARTED";
+ sCmdToString[EVENT_KEEPALIVE_STOPPED - BASE] = "EVENT_KEEPALIVE_STOPPED";
+ sCmdToString[EVENT_KEEPALIVE_START_REQUEST - BASE] = "EVENT_KEEPALIVE_START_REQUEST";
+ sCmdToString[EVENT_KEEPALIVE_STOP_REQUEST - BASE] = "EVENT_KEEPALIVE_STOP_REQUEST";
}
// Convert cmd to string or null if unknown
static String cmdToString(int cmd) {
@@ -512,6 +530,12 @@ public class DataConnection extends StateMachine {
mPhone.mCi.setupDataCall(cp.mRilRat, dp, isModemRoaming, allowRoaming, msg);
}
+ public void onSubscriptionOverride(int overrideMask, int overrideValue) {
+ mSubscriptionOverride = (mSubscriptionOverride & ~overrideMask)
+ | (overrideValue & overrideMask);
+ sendMessage(obtainMessage(EVENT_DATA_CONNECTION_OVERRIDE_CHANGED));
+ }
+
/**
* TearDown the data connection when the deactivation is complete a Message with
* msg.what == EVENT_DEACTIVATE_DONE and msg.obj == AsyncResult with AsyncResult.obj
@@ -1007,10 +1031,17 @@ public class DataConnection extends StateMachine {
result.setNetworkSpecifier(new StringNetworkSpecifier(Integer.toString(mPhone.getSubId())));
- if (!mPhone.getServiceState().getDataRoaming()) {
- result.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
- } else {
- result.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
+ result.setCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING,
+ !mPhone.getServiceState().getDataRoaming());
+
+ result.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED);
+
+ // Override values set above when requested by policy
+ if ((mSubscriptionOverride & OVERRIDE_UNMETERED) != 0) {
+ result.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+ }
+ if ((mSubscriptionOverride & OVERRIDE_CONGESTED) != 0) {
+ result.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED);
}
return result;
@@ -1048,23 +1079,12 @@ public class DataConnection extends StateMachine {
// set link addresses
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.getAddress() instanceof Inet4Address) ? 32 : 128;
- }
- if (DBG) log("addr/pl=" + ia.getAddress() + "/" + addrPrefixLen);
- LinkAddress la;
- try {
- la = new LinkAddress(ia.getAddress(), addrPrefixLen);
- } catch (IllegalArgumentException e) {
- throw new UnknownHostException("Bad parameter for LinkAddress, ia="
- + ia.getAddress().getHostAddress() + "/" + addrPrefixLen);
+ for (LinkAddress la : response.getAddresses()) {
+ if (!la.getAddress().isAnyLocalAddress()) {
+ if (DBG) {
+ log("addr/pl=" + la.getAddress() + "/"
+ + la.getNetworkPrefixLength());
}
-
linkProperties.addLinkAddress(la);
}
}
@@ -1348,12 +1368,21 @@ public class DataConnection extends StateMachine {
break;
case EVENT_DATA_CONNECTION_ROAM_ON:
case EVENT_DATA_CONNECTION_ROAM_OFF:
+ case EVENT_DATA_CONNECTION_OVERRIDE_CHANGED:
updateNetworkInfo();
if (mNetworkAgent != null) {
mNetworkAgent.sendNetworkCapabilities(getNetworkCapabilities());
mNetworkAgent.sendNetworkInfo(mNetworkInfo);
}
break;
+ case EVENT_KEEPALIVE_START_REQUEST:
+ case EVENT_KEEPALIVE_STOP_REQUEST:
+ if (mNetworkAgent != null) {
+ mNetworkAgent.onPacketKeepaliveEvent(
+ msg.arg1,
+ ConnectivityManager.PacketKeepalive.ERROR_INVALID_NETWORK);
+ }
+ break;
default:
if (DBG) {
log("DcDefaultState: shouldn't happen but ignore msg.what="
@@ -1664,6 +1693,7 @@ public class DataConnection extends StateMachine {
* The state machine is connected, expecting an EVENT_DISCONNECT.
*/
private class DcActiveState extends State {
+
@Override public void enter() {
if (DBG) log("DcActiveState: enter dc=" + DataConnection.this);
@@ -1699,6 +1729,8 @@ public class DataConnection extends StateMachine {
mNetworkAgent = new DcNetworkAgent(getHandler().getLooper(), mPhone.getContext(),
"DcNetworkAgent", mNetworkInfo, getNetworkCapabilities(), mLinkProperties,
50, misc);
+ mPhone.mCi.registerForNattKeepaliveStatus(
+ getHandler(), DataConnection.EVENT_KEEPALIVE_STATUS, null);
}
@Override
@@ -1717,6 +1749,7 @@ public class DataConnection extends StateMachine {
mNetworkInfo.setDetailedState(NetworkInfo.DetailedState.DISCONNECTED,
reason, mNetworkInfo.getExtraInfo());
+ mPhone.mCi.unregisterForNattKeepaliveStatus(getHandler());
if (mNetworkAgent != null) {
mNetworkAgent.sendNetworkInfo(mNetworkInfo);
mNetworkAgent = null;
@@ -1796,7 +1829,8 @@ public class DataConnection extends StateMachine {
break;
}
case EVENT_DATA_CONNECTION_ROAM_ON:
- case EVENT_DATA_CONNECTION_ROAM_OFF: {
+ case EVENT_DATA_CONNECTION_ROAM_OFF:
+ case EVENT_DATA_CONNECTION_OVERRIDE_CHANGED: {
updateNetworkInfo();
if (mNetworkAgent != null) {
mNetworkAgent.sendNetworkCapabilities(getNetworkCapabilities());
@@ -1834,6 +1868,85 @@ public class DataConnection extends StateMachine {
retVal = HANDLED;
break;
}
+ case EVENT_KEEPALIVE_START_REQUEST: {
+ KeepalivePacketData pkt = (KeepalivePacketData) msg.obj;
+ int slotId = msg.arg1;
+ int intervalMillis = msg.arg2 * 1000;
+ mPhone.mCi.startNattKeepalive(
+ DataConnection.this.mCid, pkt, intervalMillis,
+ DataConnection.this.obtainMessage(
+ EVENT_KEEPALIVE_STARTED, slotId, 0, null));
+ retVal = HANDLED;
+ break;
+ }
+ case EVENT_KEEPALIVE_STOP_REQUEST: {
+ int slotId = msg.arg1;
+ int handle = mNetworkAgent.keepaliveTracker.getHandleForSlot(slotId);
+ if (handle < 0) {
+ loge("No slot found for stopPacketKeepalive! " + slotId);
+ retVal = HANDLED;
+ break;
+ } else {
+ logd("Stopping keepalive with handle: " + handle);
+ }
+
+ mPhone.mCi.stopNattKeepalive(
+ handle, DataConnection.this.obtainMessage(
+ EVENT_KEEPALIVE_STOPPED, handle, slotId, null));
+ retVal = HANDLED;
+ break;
+ }
+ case EVENT_KEEPALIVE_STARTED: {
+ AsyncResult ar = (AsyncResult) msg.obj;
+ final int slot = msg.arg1;
+ if (ar.exception != null || ar.result == null) {
+ loge("EVENT_KEEPALIVE_STARTED: error starting keepalive, e="
+ + ar.exception);
+ mNetworkAgent.onPacketKeepaliveEvent(
+ slot, ConnectivityManager.PacketKeepalive.ERROR_HARDWARE_ERROR);
+ } else {
+ KeepaliveStatus ks = (KeepaliveStatus) ar.result;
+ if (ks == null) {
+ loge("Null KeepaliveStatus received!");
+ } else {
+ mNetworkAgent.keepaliveTracker.handleKeepaliveStarted(slot, ks);
+ }
+ }
+ retVal = HANDLED;
+ break;
+ }
+ case EVENT_KEEPALIVE_STATUS: {
+ AsyncResult ar = (AsyncResult) msg.obj;
+ if (ar.exception != null) {
+ loge("EVENT_KEEPALIVE_STATUS: error in keepalive, e=" + ar.exception);
+ // We have no way to notify connectivity in this case.
+ }
+ if (ar.result != null) {
+ KeepaliveStatus ks = (KeepaliveStatus) ar.result;
+ mNetworkAgent.keepaliveTracker.handleKeepaliveStatus(ks);
+ }
+
+ retVal = HANDLED;
+ break;
+ }
+ case EVENT_KEEPALIVE_STOPPED: {
+ AsyncResult ar = (AsyncResult) msg.obj;
+ final int handle = msg.arg1;
+ final int slotId = msg.arg2;
+
+ if (ar.exception != null) {
+ loge("EVENT_KEEPALIVE_STOPPED: error stopping keepalive for handle="
+ + handle + " e=" + ar.exception);
+ mNetworkAgent.keepaliveTracker.handleKeepaliveStatus(
+ new KeepaliveStatus(KeepaliveStatus.ERROR_UNKNOWN));
+ } else {
+ log("Keepalive Stop Requested for handle=" + handle);
+ mNetworkAgent.keepaliveTracker.handleKeepaliveStatus(
+ new KeepaliveStatus(handle, KeepaliveStatus.STATUS_INACTIVE));
+ }
+ retVal = HANDLED;
+ break;
+ }
default:
if (VDBG) {
log("DcActiveState not handled msg.what=" + getWhatToString(msg.what));
@@ -1947,11 +2060,15 @@ public class DataConnection extends StateMachine {
private NetworkCapabilities mNetworkCapabilities;
+ public final DcKeepaliveTracker keepaliveTracker = new DcKeepaliveTracker();
+
public DcNetworkAgent(Looper l, Context c, String TAG, NetworkInfo ni,
NetworkCapabilities nc, LinkProperties lp, int score, NetworkMisc misc) {
super(l, c, TAG, ni, nc, lp, score, misc);
mNetCapsLocalLog.log("New network agent created. capabilities=" + nc);
mNetworkCapabilities = nc;
+ mPhone.mCi.registerForNattKeepaliveStatus(
+ getHandler(), EVENT_KEEPALIVE_STATUS, null);
}
@Override
@@ -2009,6 +2126,137 @@ public class DataConnection extends StateMachine {
}
super.sendNetworkCapabilities(networkCapabilities);
}
+
+ @Override
+ protected void startPacketKeepalive(Message msg) {
+ DataConnection.this.obtainMessage(EVENT_KEEPALIVE_START_REQUEST,
+ msg.arg1, msg.arg2, msg.obj).sendToTarget();
+ }
+
+ @Override
+ protected void stopPacketKeepalive(Message msg) {
+ DataConnection.this.obtainMessage(EVENT_KEEPALIVE_STOP_REQUEST,
+ msg.arg1, msg.arg2, msg.obj).sendToTarget();
+ }
+
+ private class DcKeepaliveTracker {
+ private class KeepaliveRecord {
+ public int slotId;
+ public int currentStatus;
+
+ KeepaliveRecord(int slotId, int status) {
+ this.slotId = slotId;
+ this.currentStatus = status;
+ }
+ };
+
+ private final SparseArray<KeepaliveRecord> mKeepalives = new SparseArray();
+
+ int getHandleForSlot(int slotId) {
+ for (int i = 0; i < mKeepalives.size(); i++) {
+ KeepaliveRecord kr = mKeepalives.valueAt(i);
+ if (kr.slotId == slotId) return mKeepalives.keyAt(i);
+ }
+ return -1;
+ }
+
+ int keepaliveStatusErrorToPacketKeepaliveError(int error) {
+ switch(error) {
+ case KeepaliveStatus.ERROR_NONE:
+ return PacketKeepalive.SUCCESS;
+ case KeepaliveStatus.ERROR_UNSUPPORTED:
+ return PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED;
+ case KeepaliveStatus.ERROR_NO_RESOURCES:
+ case KeepaliveStatus.ERROR_UNKNOWN:
+ default:
+ return PacketKeepalive.ERROR_HARDWARE_ERROR;
+ }
+ }
+
+ void handleKeepaliveStarted(final int slot, KeepaliveStatus ks) {
+ switch (ks.statusCode) {
+ case KeepaliveStatus.STATUS_INACTIVE:
+ DcNetworkAgent.this.onPacketKeepaliveEvent(slot,
+ keepaliveStatusErrorToPacketKeepaliveError(ks.errorCode));
+ break;
+ case KeepaliveStatus.STATUS_ACTIVE:
+ DcNetworkAgent.this.onPacketKeepaliveEvent(
+ slot, PacketKeepalive.SUCCESS);
+ // fall through to add record
+ case KeepaliveStatus.STATUS_PENDING:
+ log("Adding keepalive handle="
+ + ks.sessionHandle + " slot = " + slot);
+ mKeepalives.put(ks.sessionHandle,
+ new KeepaliveRecord(
+ slot, ks.statusCode));
+ break;
+ default:
+ loge("Invalid KeepaliveStatus Code: " + ks.statusCode);
+ break;
+ }
+ }
+
+ void handleKeepaliveStatus(KeepaliveStatus ks) {
+ final KeepaliveRecord kr;
+ kr = mKeepalives.get(ks.sessionHandle);
+
+ if (kr == null) {
+ // If there is no slot for the session handle, we received an event
+ // for a different data connection. This is not an error because the
+ // keepalive session events are broadcast to all listeners.
+ log("Discarding keepalive event for different data connection:" + ks);
+ return;
+ }
+ // Switch on the current state, to see what we do with the status update
+ switch (kr.currentStatus) {
+ case KeepaliveStatus.STATUS_INACTIVE:
+ loge("Inactive Keepalive received status!");
+ DcNetworkAgent.this.onPacketKeepaliveEvent(
+ kr.slotId, PacketKeepalive.ERROR_HARDWARE_ERROR);
+ break;
+ case KeepaliveStatus.STATUS_PENDING:
+ switch (ks.statusCode) {
+ case KeepaliveStatus.STATUS_INACTIVE:
+ DcNetworkAgent.this.onPacketKeepaliveEvent(kr.slotId,
+ keepaliveStatusErrorToPacketKeepaliveError(ks.errorCode));
+ kr.currentStatus = KeepaliveStatus.STATUS_INACTIVE;
+ mKeepalives.remove(ks.sessionHandle);
+ break;
+ case KeepaliveStatus.STATUS_ACTIVE:
+ log("Pending Keepalive received active status!");
+ kr.currentStatus = KeepaliveStatus.STATUS_ACTIVE;
+ DcNetworkAgent.this.onPacketKeepaliveEvent(
+ kr.slotId, PacketKeepalive.SUCCESS);
+ break;
+ case KeepaliveStatus.STATUS_PENDING:
+ loge("Invalid unsolicied Keepalive Pending Status!");
+ break;
+ default:
+ loge("Invalid Keepalive Status received, " + ks.statusCode);
+ }
+ break;
+ case KeepaliveStatus.STATUS_ACTIVE:
+ switch (ks.statusCode) {
+ case KeepaliveStatus.STATUS_INACTIVE:
+ loge("Keepalive received stopped status!");
+ DcNetworkAgent.this.onPacketKeepaliveEvent(
+ kr.slotId, PacketKeepalive.SUCCESS);
+ kr.currentStatus = KeepaliveStatus.STATUS_INACTIVE;
+ mKeepalives.remove(ks.sessionHandle);
+ break;
+ case KeepaliveStatus.STATUS_PENDING:
+ case KeepaliveStatus.STATUS_ACTIVE:
+ loge("Active Keepalive received invalid status!");
+ break;
+ default:
+ loge("Invalid Keepalive Status received, " + ks.statusCode);
+ }
+ break;
+ default:
+ loge("Invalid Keepalive Status received, " + kr.currentStatus);
+ }
+ }
+ };
}
// ******* "public" interface
@@ -2242,6 +2490,7 @@ public class DataConnection extends StateMachine {
pw.println("mLastFailTime=" + TimeUtils.logTimeOfDay(mLastFailTime));
pw.println("mLastFailCause=" + mLastFailCause);
pw.println("mUserData=" + mUserData);
+ pw.println("mSubscriptionOverride=" + Integer.toHexString(mSubscriptionOverride));
pw.println("mInstanceNumber=" + mInstanceNumber);
pw.println("mAc=" + mAc);
pw.println("Network capabilities changed history:");
diff --git a/com/android/internal/telephony/dataconnection/DcController.java b/com/android/internal/telephony/dataconnection/DcController.java
index c21713f1..b4ceff60 100644
--- a/com/android/internal/telephony/dataconnection/DcController.java
+++ b/com/android/internal/telephony/dataconnection/DcController.java
@@ -17,8 +17,10 @@
package com.android.internal.telephony.dataconnection;
import android.content.Context;
+import android.net.INetworkPolicyListener;
import android.net.LinkAddress;
import android.net.LinkProperties.CompareResult;
+import android.net.NetworkPolicyManager;
import android.net.NetworkUtils;
import android.os.AsyncResult;
import android.os.Build;
@@ -55,9 +57,10 @@ public class DcController extends StateMachine {
private DcTesterDeactivateAll mDcTesterDeactivateAll;
// package as its used by Testing code
- ArrayList<DataConnection> mDcListAll = new ArrayList<DataConnection>();
- private HashMap<Integer, DataConnection> mDcListActiveByCid =
- new HashMap<Integer, DataConnection>();
+ // @GuardedBy("mDcListAll")
+ final ArrayList<DataConnection> mDcListAll = new ArrayList<>();
+ // @GuardedBy("mDcListAll")
+ private final HashMap<Integer, DataConnection> mDcListActiveByCid = new HashMap<>();
/**
* Constants for the data connection activity:
@@ -72,7 +75,9 @@ public class DcController extends StateMachine {
private DccDefaultState mDccDefaultState = new DccDefaultState();
- TelephonyManager mTelephonyManager;
+ final TelephonyManager mTelephonyManager;
+ final NetworkPolicyManager mNetworkPolicyManager;
+
private PhoneStateListener mPhoneStateListener;
//mExecutingCarrierChange tracks whether the phone is currently executing
@@ -105,8 +110,12 @@ public class DcController extends StateMachine {
}
};
- mTelephonyManager = (TelephonyManager) phone.getContext().getSystemService(Context.TELEPHONY_SERVICE);
- if(mTelephonyManager != null) {
+ mTelephonyManager = (TelephonyManager) phone.getContext()
+ .getSystemService(Context.TELEPHONY_SERVICE);
+ mNetworkPolicyManager = (NetworkPolicyManager) phone.getContext()
+ .getSystemService(Context.NETWORK_POLICY_SERVICE);
+
+ if (mTelephonyManager != null) {
mTelephonyManager.listen(mPhoneStateListener,
PhoneStateListener.LISTEN_CARRIER_NETWORK_CHANGE);
}
@@ -125,29 +134,39 @@ public class DcController extends StateMachine {
}
void addDc(DataConnection dc) {
- mDcListAll.add(dc);
+ synchronized (mDcListAll) {
+ mDcListAll.add(dc);
+ }
}
void removeDc(DataConnection dc) {
- mDcListActiveByCid.remove(dc.mCid);
- mDcListAll.remove(dc);
+ synchronized (mDcListAll) {
+ mDcListActiveByCid.remove(dc.mCid);
+ mDcListAll.remove(dc);
+ }
}
public void addActiveDcByCid(DataConnection dc) {
if (DBG && dc.mCid < 0) {
log("addActiveDcByCid dc.mCid < 0 dc=" + dc);
}
- mDcListActiveByCid.put(dc.mCid, dc);
+ synchronized (mDcListAll) {
+ mDcListActiveByCid.put(dc.mCid, dc);
+ }
}
public DataConnection getActiveDcByCid(int cid) {
- return mDcListActiveByCid.get(cid);
+ synchronized (mDcListAll) {
+ return mDcListActiveByCid.get(cid);
+ }
}
void removeActiveDcByCid(DataConnection dc) {
- DataConnection removedDc = mDcListActiveByCid.remove(dc.mCid);
- if (DBG && removedDc == null) {
- log("removeActiveDcByCid removedDc=null dc=" + dc);
+ synchronized (mDcListAll) {
+ DataConnection removedDc = mDcListActiveByCid.remove(dc.mCid);
+ if (DBG && removedDc == null) {
+ log("removeActiveDcByCid removedDc=null dc=" + dc);
+ }
}
}
@@ -155,6 +174,21 @@ public class DcController extends StateMachine {
return mExecutingCarrierChange;
}
+ private final INetworkPolicyListener mListener = new NetworkPolicyManager.Listener() {
+ @Override
+ public void onSubscriptionOverride(int subId, int overrideMask, int overrideValue) {
+ if (mPhone == null || mPhone.getSubId() != subId) return;
+
+ final HashMap<Integer, DataConnection> dcListActiveByCid;
+ synchronized (mDcListAll) {
+ dcListActiveByCid = new HashMap<>(mDcListActiveByCid);
+ }
+ for (DataConnection dc : dcListActiveByCid.values()) {
+ dc.onSubscriptionOverride(overrideMask, overrideValue);
+ }
+ }
+ };
+
private class DccDefaultState extends State {
@Override
public void enter() {
@@ -166,6 +200,9 @@ public class DcController extends StateMachine {
mDcTesterDeactivateAll =
new DcTesterDeactivateAll(mPhone, DcController.this, getHandler());
}
+ if (mNetworkPolicyManager != null) {
+ mNetworkPolicyManager.registerListener(mListener);
+ }
}
@Override
@@ -177,6 +214,9 @@ public class DcController extends StateMachine {
if (mDcTesterDeactivateAll != null) {
mDcTesterDeactivateAll.dispose();
}
+ if (mNetworkPolicyManager != null) {
+ mNetworkPolicyManager.unregisterListener(mListener);
+ }
}
@Override
@@ -214,12 +254,19 @@ public class DcController extends StateMachine {
* @param dcsList as sent by RIL_UNSOL_DATA_CALL_LIST_CHANGED
*/
private void onDataStateChanged(ArrayList<DataCallResponse> dcsList) {
+ final ArrayList<DataConnection> dcListAll;
+ final HashMap<Integer, DataConnection> dcListActiveByCid;
+ synchronized (mDcListAll) {
+ dcListAll = new ArrayList<>(mDcListAll);
+ dcListActiveByCid = new HashMap<>(mDcListActiveByCid);
+ }
+
if (DBG) {
lr("onDataStateChanged: dcsList=" + dcsList
- + " mDcListActiveByCid=" + mDcListActiveByCid);
+ + " dcListActiveByCid=" + dcListActiveByCid);
}
if (VDBG) {
- log("onDataStateChanged: mDcListAll=" + mDcListAll);
+ log("onDataStateChanged: mDcListAll=" + dcListAll);
}
// Create hashmap of cid to DataCallResponse
@@ -232,7 +279,7 @@ public class DcController extends StateMachine {
// Add a DC that is active but not in the
// dcsList to the list of DC's to retry
ArrayList<DataConnection> dcsToRetry = new ArrayList<DataConnection>();
- for (DataConnection dc : mDcListActiveByCid.values()) {
+ for (DataConnection dc : dcListActiveByCid.values()) {
if (dataCallResponseListByCid.get(dc.mCid) == null) {
if (DBG) log("onDataStateChanged: add to retry dc=" + dc);
dcsToRetry.add(dc);
@@ -249,7 +296,7 @@ public class DcController extends StateMachine {
for (DataCallResponse newState : dcsList) {
- DataConnection dc = mDcListActiveByCid.get(newState.getCallId());
+ DataConnection dc = dcListActiveByCid.get(newState.getCallId());
if (dc == null) {
// UNSOL_DATA_CALL_LIST_CHANGED arrived before SETUP_DATA_CALL completed.
loge("onDataStateChanged: no associated DC yet, ignore");
@@ -437,14 +484,18 @@ public class DcController extends StateMachine {
@Override
public String toString() {
- return "mDcListAll=" + mDcListAll + " mDcListActiveByCid=" + mDcListActiveByCid;
+ synchronized (mDcListAll) {
+ return "mDcListAll=" + mDcListAll + " mDcListActiveByCid=" + mDcListActiveByCid;
+ }
}
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
super.dump(fd, pw, args);
pw.println(" mPhone=" + mPhone);
- pw.println(" mDcListAll=" + mDcListAll);
- pw.println(" mDcListActiveByCid=" + mDcListActiveByCid);
+ synchronized (mDcListAll) {
+ pw.println(" mDcListAll=" + mDcListAll);
+ pw.println(" mDcListActiveByCid=" + mDcListActiveByCid);
+ }
}
}
diff --git a/com/android/internal/telephony/dataconnection/DcTracker.java b/com/android/internal/telephony/dataconnection/DcTracker.java
index 86ee9a2d..c27d706a 100644
--- a/com/android/internal/telephony/dataconnection/DcTracker.java
+++ b/com/android/internal/telephony/dataconnection/DcTracker.java
@@ -812,6 +812,8 @@ public class DcTracker extends Handler {
}
}
+ mPhone.notifyUserMobileDataStateChanged(enabled);
+
// TODO: We should register for DataEnabledSetting's data enabled/disabled event and
// handle the rest from there.
if (enabled) {
@@ -1763,7 +1765,10 @@ public class DcTracker extends Handler {
}
for (ApnSetting dunSetting : dunCandidates) {
- if (!ServiceState.bitmaskHasTech(dunSetting.bearerBitmask, bearer)) continue;
+ if (!ServiceState.bitmaskHasTech(dunSetting.networkTypeBitmask,
+ ServiceState.rilRadioTechnologyToNetworkType(bearer))) {
+ continue;
+ }
if (dunSetting.numeric.equals(operator)) {
if (dunSetting.hasMvnoParams()) {
if (r != null && ApnSetting.mvnoMatches(r, dunSetting.mvnoType,
@@ -1841,6 +1846,9 @@ public class DcTracker extends Handler {
private ApnSetting makeApnSetting(Cursor cursor) {
String[] types = parseTypes(
cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.TYPE)));
+ int networkTypeBitmask = cursor.getInt(
+ cursor.getColumnIndexOrThrow(Telephony.Carriers.NETWORK_TYPE_BITMASK));
+
ApnSetting apn = new ApnSetting(
cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers._ID)),
cursor.getString(cursor.getColumnIndexOrThrow(Telephony.Carriers.NUMERIC)),
@@ -1866,8 +1874,7 @@ public class DcTracker extends Handler {
Telephony.Carriers.ROAMING_PROTOCOL)),
cursor.getInt(cursor.getColumnIndexOrThrow(
Telephony.Carriers.CARRIER_ENABLED)) == 1,
- cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.BEARER)),
- cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.BEARER_BITMASK)),
+ networkTypeBitmask,
cursor.getInt(cursor.getColumnIndexOrThrow(Telephony.Carriers.PROFILE_ID)),
cursor.getInt(cursor.getColumnIndexOrThrow(
Telephony.Carriers.MODEM_COGNITIVE)) == 1,
@@ -3319,7 +3326,8 @@ public class DcTracker extends Handler {
// ORDER BY Telephony.Carriers._ID ("_id")
Cursor cursor = mPhone.getContext().getContentResolver().query(
- Telephony.Carriers.CONTENT_URI, null, selection, null, Telephony.Carriers._ID);
+ Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "filtered"),
+ null, selection, null, Telephony.Carriers._ID);
if (cursor != null) {
if (cursor.getCount() > 0) {
@@ -3392,13 +3400,19 @@ public class DcTracker extends Handler {
String protocol = src.protocol.equals("IPV4V6") ? src.protocol : dest.protocol;
String roamingProtocol = src.roamingProtocol.equals("IPV4V6") ? src.roamingProtocol :
dest.roamingProtocol;
- int bearerBitmask = (dest.bearerBitmask == 0 || src.bearerBitmask == 0) ?
- 0 : (dest.bearerBitmask | src.bearerBitmask);
+ int networkTypeBitmask = (dest.networkTypeBitmask == 0 || src.networkTypeBitmask == 0)
+ ? 0 : (dest.networkTypeBitmask | src.networkTypeBitmask);
+ if (networkTypeBitmask == 0) {
+ int bearerBitmask = (dest.bearerBitmask == 0 || src.bearerBitmask == 0)
+ ? 0 : (dest.bearerBitmask | src.bearerBitmask);
+ networkTypeBitmask = ServiceState.convertBearerBitmaskToNetworkTypeBitmask(
+ bearerBitmask);
+ }
return new ApnSetting(id, dest.numeric, dest.carrier, dest.apn,
proxy, port, mmsc, mmsProxy, mmsPort, dest.user, dest.password,
dest.authType, resultTypes.toArray(new String[0]), protocol,
- roamingProtocol, dest.carrierEnabled, 0, bearerBitmask, dest.profileId,
+ roamingProtocol, dest.carrierEnabled, networkTypeBitmask, dest.profileId,
(dest.modemCognitive || src.modemCognitive), dest.maxConns, dest.waitTime,
dest.maxConnsTime, dest.mtu, dest.mvnoType, dest.mvnoMatchData);
}
@@ -3486,7 +3500,8 @@ public class DcTracker extends Handler {
+ mPreferredApn.numeric + ":" + mPreferredApn);
}
if (mPreferredApn.numeric.equals(operator)) {
- if (ServiceState.bitmaskHasTech(mPreferredApn.bearerBitmask, radioTech)) {
+ if (ServiceState.bitmaskHasTech(mPreferredApn.networkTypeBitmask,
+ ServiceState.rilRadioTechnologyToNetworkType(radioTech))) {
apnList.add(mPreferredApn);
if (DBG) log("buildWaitingApns: X added preferred apnList=" + apnList);
return apnList;
@@ -3505,13 +3520,15 @@ public class DcTracker extends Handler {
if (DBG) log("buildWaitingApns: mAllApnSettings=" + mAllApnSettings);
for (ApnSetting apn : mAllApnSettings) {
if (apn.canHandleType(requestedApnType)) {
- if (ServiceState.bitmaskHasTech(apn.bearerBitmask, radioTech)) {
+ if (ServiceState.bitmaskHasTech(apn.networkTypeBitmask,
+ ServiceState.rilRadioTechnologyToNetworkType(radioTech))) {
if (DBG) log("buildWaitingApns: adding apn=" + apn);
apnList.add(apn);
} else {
if (DBG) {
- log("buildWaitingApns: bearerBitmask:" + apn.bearerBitmask + " does " +
- "not include radioTech:" + radioTech);
+ log("buildWaitingApns: bearerBitmask:" + apn.bearerBitmask
+ + " or " + "networkTypeBitmask:" + apn.networkTypeBitmask
+ + "do not include radioTech:" + radioTech);
}
}
} else if (DBG) {
@@ -4282,7 +4299,8 @@ public class DcTracker extends Handler {
// DB will contain only one entry for Emergency APN
String selection = "type=\"emergency\"";
Cursor cursor = mPhone.getContext().getContentResolver().query(
- Telephony.Carriers.CONTENT_URI, null, selection, null, null);
+ Uri.withAppendedPath(Telephony.Carriers.CONTENT_URI, "filtered"),
+ null, selection, null, null);
if (cursor != null) {
if (cursor.getCount() > 0) {
@@ -4816,9 +4834,14 @@ public class DcTracker extends Handler {
@VisibleForTesting
public static DataProfile createDataProfile(ApnSetting apn, int profileId) {
int profileType;
- if (apn.bearerBitmask == 0) {
+
+ int bearerBitmap = 0;
+ bearerBitmap = ServiceState.convertNetworkTypeBitmaskToBearerBitmask(
+ apn.networkTypeBitmask);
+
+ if (bearerBitmap == 0) {
profileType = DataProfile.TYPE_COMMON;
- } else if (ServiceState.bearerBitmapHasCdma(apn.bearerBitmask)) {
+ } else if (ServiceState.bearerBitmapHasCdma(bearerBitmap)) {
profileType = DataProfile.TYPE_3GPP2;
} else {
profileType = DataProfile.TYPE_3GPP;
@@ -4827,7 +4850,7 @@ public class DcTracker extends Handler {
return new DataProfile(profileId, apn.apn, apn.protocol,
apn.authType, apn.user, apn.password, profileType,
apn.maxConnsTime, apn.maxConns, apn.waitTime, apn.carrierEnabled, apn.typesBitmap,
- apn.roamingProtocol, apn.bearerBitmask, apn.mtu, apn.mvnoType, apn.mvnoMatchData,
+ apn.roamingProtocol, bearerBitmap, apn.mtu, apn.mvnoType, apn.mvnoMatchData,
apn.modemCognitive);
}
}
diff --git a/com/android/internal/telephony/dataconnection/KeepaliveStatus.java b/com/android/internal/telephony/dataconnection/KeepaliveStatus.java
new file mode 100644
index 00000000..8b0ec4ff
--- /dev/null
+++ b/com/android/internal/telephony/dataconnection/KeepaliveStatus.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 com.android.internal.telephony.dataconnection;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class serves to pass around the parameters of Keepalive session
+ * status within the telephony framework.
+ *
+ * {@hide}
+ */
+public class KeepaliveStatus implements Parcelable {
+ private static final String LOG_TAG = "KeepaliveStatus";
+
+ /** This should match the HAL Radio::1_1::KeepaliveStatusCode */
+ public static final int STATUS_ACTIVE = 0;
+ public static final int STATUS_INACTIVE = 1;
+ public static final int STATUS_PENDING = 2;
+
+ public static final int ERROR_NONE = 0;
+ public static final int ERROR_UNSUPPORTED = 1;
+ public static final int ERROR_NO_RESOURCES = 2;
+ public static final int ERROR_UNKNOWN = 3;
+
+ public static final int INVALID_HANDLE = Integer.MAX_VALUE;
+
+ /** An opaque value that identifies this Keepalive status to the modem */
+ public final int sessionHandle;
+
+ /**
+ * A status code indicating whether this Keepalive session is
+ * active, inactive, or pending activation
+ */
+ public final int statusCode;
+
+ /** An error code indicating a lower layer failure, if any */
+ public final int errorCode;
+
+ public KeepaliveStatus(int error) {
+ sessionHandle = INVALID_HANDLE;
+ statusCode = STATUS_INACTIVE;
+ errorCode = error;
+ }
+
+ public KeepaliveStatus(int handle, int code) {
+ sessionHandle = handle;
+ statusCode = code;
+ errorCode = ERROR_NONE;
+ }
+
+
+ @Override
+ public String toString() {
+ return String.format("{errorCode=%d, sessionHandle=%d, statusCode=%d}",
+ errorCode, sessionHandle, statusCode);
+ }
+
+ // Parcelable Implementation
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(errorCode);
+ dest.writeInt(sessionHandle);
+ dest.writeInt(statusCode);
+ }
+
+ private KeepaliveStatus(Parcel p) {
+ errorCode = p.readInt();
+ sessionHandle = p.readInt();
+ statusCode = p.readInt();
+ }
+
+ public static final Parcelable.Creator<KeepaliveStatus> CREATOR =
+ new Parcelable.Creator<KeepaliveStatus>() {
+ @Override
+ public KeepaliveStatus createFromParcel(Parcel source) {
+ return new KeepaliveStatus(source);
+ }
+
+ @Override
+ public KeepaliveStatus[] newArray(int size) {
+ return new KeepaliveStatus[size];
+ }
+ };
+}
diff --git a/com/android/internal/telephony/euicc/EuiccCardController.java b/com/android/internal/telephony/euicc/EuiccCardController.java
new file mode 100644
index 00000000..442e956e
--- /dev/null
+++ b/com/android/internal/telephony/euicc/EuiccCardController.java
@@ -0,0 +1,275 @@
+/*
+ * 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 com.android.internal.telephony.euicc;
+
+import android.annotation.Nullable;
+import android.app.AppOpsManager;
+import android.content.Context;
+import android.content.pm.ComponentInfo;
+import android.os.Binder;
+import android.os.ServiceManager;
+import android.telephony.euicc.EuiccCardManager;
+import android.telephony.euicc.EuiccNotification;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/** Backing implementation of {@link EuiccCardManager}. */
+public class EuiccCardController extends IEuiccCardController.Stub {
+ private static final String TAG = "EuiccCardController";
+
+ private final Context mContext;
+ private AppOpsManager mAppOps;
+ private String mCallingPackage;
+ private ComponentInfo mBestComponent;
+ private static EuiccCardController sInstance;
+
+ /** Initialize the instance. Should only be called once. */
+ public static EuiccCardController init(Context context) {
+ synchronized (EuiccCardController.class) {
+ if (sInstance == null) {
+ sInstance = new EuiccCardController(context);
+ } else {
+ Log.wtf(TAG, "init() called multiple times! sInstance = " + sInstance);
+ }
+ }
+ return sInstance;
+ }
+
+ /** Get an instance. Assumes one has already been initialized with {@link #init}. */
+ public static EuiccCardController get() {
+ if (sInstance == null) {
+ synchronized (EuiccCardController.class) {
+ if (sInstance == null) {
+ throw new IllegalStateException("get() called before init()");
+ }
+ }
+ }
+ return sInstance;
+ }
+
+ private EuiccCardController(Context context) {
+ mContext = context;
+ mAppOps = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+
+ ServiceManager.addService("euicc_card_controller", this);
+ }
+
+ private void checkCallingPackage(String callingPackage) {
+ // Check the caller is LPA.
+ mAppOps.checkPackage(Binder.getCallingUid(), callingPackage);
+ mCallingPackage = callingPackage;
+ mBestComponent = EuiccConnector.findBestComponent(mContext.getPackageManager());
+ if (mBestComponent == null
+ || !TextUtils.equals(mCallingPackage, mBestComponent.packageName)) {
+ throw new SecurityException("The calling package can only be LPA.");
+ }
+ }
+
+ @Override
+ public void getAllProfiles(String callingPackage, String cardId,
+ IGetAllProfilesCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void getProfile(String callingPackage, String cardId, String iccid,
+ IGetProfileCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void disableProfile(String callingPackage, String cardId, String iccid, boolean refresh,
+ IDisableProfileCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void switchToProfile(String callingPackage, String cardId, String iccid, boolean refresh,
+ ISwitchToProfileCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void setNickname(String callingPackage, String cardId, String iccid, String nickname,
+ ISetNicknameCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void deleteProfile(String callingPackage, String cardId, String iccid,
+ IDeleteProfileCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void resetMemory(String callingPackage, String cardId,
+ @EuiccCardManager.ResetOption int options, IResetMemoryCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void getDefaultSmdpAddress(String callingPackage, String cardId,
+ IGetDefaultSmdpAddressCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void getSmdsAddress(String callingPackage, String cardId,
+ IGetSmdsAddressCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void setDefaultSmdpAddress(String callingPackage, String cardId, String address,
+ ISetDefaultSmdpAddressCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void getRulesAuthTable(String callingPackage, String cardId,
+ IGetRulesAuthTableCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void getEuiccChallenge(String callingPackage, String cardId,
+ IGetEuiccChallengeCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void getEuiccInfo1(String callingPackage, String cardId,
+ IGetEuiccInfo1Callback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void getEuiccInfo2(String callingPackage, String cardId,
+ IGetEuiccInfo2Callback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void authenticateServer(String callingPackage, String cardId, String matchingId,
+ byte[] serverSigned1, byte[] serverSignature1, byte[] euiccCiPkIdToBeUsed,
+ byte[] serverCertificate, IAuthenticateServerCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void prepareDownload(String callingPackage, String cardId, @Nullable byte[] hashCc,
+ byte[] smdpSigned2, byte[] smdpSignature2, byte[] smdpCertificate,
+ IPrepareDownloadCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void loadBoundProfilePackage(String callingPackage, String cardId,
+ byte[] boundProfilePackage, ILoadBoundProfilePackageCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void cancelSession(String callingPackage, String cardId, byte[] transactionId,
+ @EuiccCardManager.CancelReason int reason, ICancelSessionCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void listNotifications(String callingPackage, String cardId,
+ @EuiccNotification.Event int events, IListNotificationsCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void retrieveNotificationList(String callingPackage, String cardId,
+ @EuiccNotification.Event int events, IRetrieveNotificationListCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void retrieveNotification(String callingPackage, String cardId, int seqNumber,
+ IRetrieveNotificationCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void removeNotificationFromList(String callingPackage, String cardId, int seqNumber,
+ IRemoveNotificationFromListCallback callback) {
+ checkCallingPackage(callingPackage);
+
+ // TODO(b/38206971): Get EuiccCard instance from UiccController and call the API.
+ }
+
+ @Override
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.DUMP, "Requires DUMP");
+ final long token = Binder.clearCallingIdentity();
+
+ super.dump(fd, pw, args);
+ // TODO(b/38206971): dump more information.
+ pw.println("mCallingPackage=" + mCallingPackage);
+ pw.println("mBestComponent=" + mBestComponent);
+
+ Binder.restoreCallingIdentity(token);
+ }
+}
diff --git a/com/android/internal/telephony/euicc/EuiccConnector.java b/com/android/internal/telephony/euicc/EuiccConnector.java
index 3ee21641..54e56916 100644
--- a/com/android/internal/telephony/euicc/EuiccConnector.java
+++ b/com/android/internal/telephony/euicc/EuiccConnector.java
@@ -33,6 +33,7 @@ import android.content.pm.ServiceInfo;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
+import android.os.RemoteException;
import android.service.euicc.EuiccService;
import android.service.euicc.GetDefaultDownloadableSubscriptionListResult;
import android.service.euicc.GetDownloadableSubscriptionMetadataResult;
@@ -47,12 +48,14 @@ import android.service.euicc.IGetEidCallback;
import android.service.euicc.IGetEuiccInfoCallback;
import android.service.euicc.IGetEuiccProfileInfoListCallback;
import android.service.euicc.IGetOtaStatusCallback;
+import android.service.euicc.IOtaStatusChangedCallback;
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;
import android.telephony.euicc.EuiccManager.OtaStatus;
import android.text.TextUtils;
import android.util.ArraySet;
@@ -135,6 +138,7 @@ public class EuiccConnector extends StateMachine implements ServiceConnection {
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 final int CMD_START_OTA_IF_NECESSARY = 112;
private static boolean isEuiccCommand(int what) {
return what >= CMD_GET_EID;
@@ -195,6 +199,14 @@ public class EuiccConnector extends StateMachine implements ServiceConnection {
void onGetOtaStatusComplete(@OtaStatus int status);
}
+ /** Callback class for {@link #startOtaIfNecessary}. */
+ @VisibleForTesting(visibility = PACKAGE)
+ public interface OtaStatusChangedCallback extends BaseEuiccCommandCallback {
+ /**
+ * Called when OTA status is changed to {@link EuiccM}. */
+ void onOtaStatusChanged(int status);
+ }
+
static class GetMetadataRequest {
DownloadableSubscription mSubscription;
boolean mForceDeactivateSim;
@@ -389,6 +401,12 @@ public class EuiccConnector extends StateMachine implements ServiceConnection {
sendMessage(CMD_GET_OTA_STATUS, callback);
}
+ /** Asynchronously perform OTA update. */
+ @VisibleForTesting(visibility = PACKAGE)
+ public void startOtaIfNecessary(OtaStatusChangedCallback callback) {
+ sendMessage(CMD_START_OTA_IF_NECESSARY, callback);
+ }
+
/** Asynchronously fetch metadata for the given downloadable subscription. */
@VisibleForTesting(visibility = PACKAGE)
public void getDownloadableSubscriptionMetadata(DownloadableSubscription subscription,
@@ -839,6 +857,28 @@ public class EuiccConnector extends StateMachine implements ServiceConnection {
});
break;
}
+ case CMD_START_OTA_IF_NECESSARY: {
+ mEuiccService.startOtaIfNecessary(slotId,
+ new IOtaStatusChangedCallback.Stub() {
+ @Override
+ public void onOtaStatusChanged(int status)
+ throws RemoteException {
+ if (status == EuiccManager.EUICC_OTA_IN_PROGRESS) {
+ sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+ ((OtaStatusChangedCallback) callback)
+ .onOtaStatusChanged(status);
+ });
+ } else {
+ sendMessage(CMD_COMMAND_COMPLETE, (Runnable) () -> {
+ ((OtaStatusChangedCallback) callback)
+ .onOtaStatusChanged(status);
+ onCommandEnd(callback);
+ });
+ }
+ }
+ });
+ break;
+ }
default: {
Log.wtf(TAG, "Unimplemented eUICC command: " + message.what);
callback.onEuiccServiceUnavailable();
@@ -882,6 +922,7 @@ public class EuiccConnector extends StateMachine implements ServiceConnection {
case CMD_ERASE_SUBSCRIPTIONS:
case CMD_RETAIN_SUBSCRIPTIONS:
case CMD_GET_OTA_STATUS:
+ case CMD_START_OTA_IF_NECESSARY:
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 78ff3560..95ac6415 100644
--- a/com/android/internal/telephony/euicc/EuiccController.java
+++ b/com/android/internal/telephony/euicc/EuiccController.java
@@ -18,6 +18,7 @@ package com.android.internal.telephony.euicc;
import static android.telephony.euicc.EuiccManager.EUICC_OTA_STATUS_UNAVAILABLE;
import android.Manifest;
+import android.Manifest.permission;
import android.annotation.Nullable;
import android.app.AppOpsManager;
import android.app.PendingIntent;
@@ -46,6 +47,7 @@ import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.SubscriptionController;
+import com.android.internal.telephony.euicc.EuiccConnector.OtaStatusChangedCallback;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -191,6 +193,26 @@ public class EuiccController extends IEuiccController.Stub {
}
}
+
+ /**
+ * Start eUICC OTA update if current eUICC OS is not the latest one. When OTA is started or
+ * finished, the broadcast {@link EuiccManager#ACTION_OTA_STATUS_CHANGED} will be sent.
+ *
+ * This function will only be called from phone process and isn't exposed to the other apps.
+ */
+ public void startOtaUpdatingIfNecessary() {
+ mConnector.startOtaIfNecessary(
+ new OtaStatusChangedCallback() {
+ @Override
+ public void onOtaStatusChanged(int status) {
+ sendOtaStatusChangedBroadcast();
+ }
+
+ @Override
+ public void onEuiccServiceUnavailable() {}
+ });
+ }
+
@Override
public void getDownloadableSubscriptionMetadata(DownloadableSubscription subscription,
String callingPackage, PendingIntent callbackIntent) {
@@ -943,6 +965,16 @@ public class EuiccController extends IEuiccController.Stub {
}
}
+ /**
+ * Send broadcast {@link EuiccManager#ACTION_OTA_STATUS_CHANGED} for OTA status
+ * changed.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ public void sendOtaStatusChangedBroadcast() {
+ Intent intent = new Intent(EuiccManager.ACTION_OTA_STATUS_CHANGED);
+ mContext.sendBroadcast(intent, permission.WRITE_EMBEDDED_SUBSCRIPTIONS);
+ }
+
@Nullable
private SubscriptionInfo getSubscriptionForSubscriptionId(int subscriptionId) {
List<SubscriptionInfo> subs = mSubscriptionManager.getAvailableSubscriptionInfoList();
diff --git a/com/android/internal/telephony/gsm/GsmSMSDispatcher.java b/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
index 8f18c61d..6663a73e 100644
--- a/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
+++ b/com/android/internal/telephony/gsm/GsmSMSDispatcher.java
@@ -20,36 +20,33 @@ import android.app.Activity;
import android.app.PendingIntent;
import android.app.PendingIntent.CanceledException;
import android.content.Intent;
-import android.net.Uri;
import android.os.AsyncResult;
import android.os.Message;
import android.provider.Telephony.Sms;
import android.provider.Telephony.Sms.Intents;
import android.telephony.Rlog;
import android.telephony.ServiceState;
+import android.util.Pair;
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.telephony.GsmAlphabet;
-import com.android.internal.telephony.ImsSMSDispatcher;
+import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
import com.android.internal.telephony.InboundSmsHandler;
import com.android.internal.telephony.Phone;
-import com.android.internal.telephony.SMSDispatcher;
import com.android.internal.telephony.SmsConstants;
+import com.android.internal.telephony.SmsDispatchersController;
+import com.android.internal.telephony.SMSDispatcher;
import com.android.internal.telephony.SmsHeader;
-import com.android.internal.telephony.SmsUsageMonitor;
+import com.android.internal.telephony.SmsMessageBase;
import com.android.internal.telephony.uicc.IccRecords;
import com.android.internal.telephony.uicc.IccUtils;
import com.android.internal.telephony.uicc.UiccCardApplication;
import com.android.internal.telephony.uicc.UiccController;
+import com.android.internal.telephony.util.SMSDispatcherUtil;
import java.util.HashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
public final class GsmSMSDispatcher extends SMSDispatcher {
private static final String TAG = "GsmSMSDispatcher";
- private static final boolean VDBG = false;
protected UiccController mUiccController = null;
private AtomicReference<IccRecords> mIccRecords = new AtomicReference<IccRecords>();
private AtomicReference<UiccCardApplication> mUiccApplication =
@@ -59,10 +56,9 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
/** Status report received */
private static final int EVENT_NEW_SMS_STATUS_REPORT = 100;
- public GsmSMSDispatcher(Phone phone, SmsUsageMonitor usageMonitor,
- ImsSMSDispatcher imsSMSDispatcher,
+ public GsmSMSDispatcher(Phone phone, SmsDispatchersController smsDispatchersController,
GsmInboundSmsHandler gsmInboundSmsHandler) {
- super(phone, usageMonitor, imsSMSDispatcher);
+ super(phone, smsDispatchersController);
mCi.setOnSmsStatus(this, EVENT_NEW_SMS_STATUS_REPORT, null);
mGsmInboundSmsHandler = gsmInboundSmsHandler;
mUiccController = UiccController.getInstance();
@@ -109,6 +105,29 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
}
}
+ @Override
+ protected boolean shouldBlockSms() {
+ return SMSDispatcherUtil.shouldBlockSms(isCdmaMo(), mPhone);
+ }
+
+ @Override
+ protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
+ String message, boolean statusReportRequested, SmsHeader smsHeader) {
+ return SMSDispatcherUtil.getSubmitPduGsm(scAddr, destAddr, message, statusReportRequested);
+ }
+
+ @Override
+ protected SmsMessageBase.SubmitPduBase getSubmitPdu(String scAddr, String destAddr,
+ int destPort, byte[] message, boolean statusReportRequested) {
+ return SMSDispatcherUtil.getSubmitPduGsm(scAddr, destAddr, destPort, message,
+ statusReportRequested);
+ }
+
+ @Override
+ protected TextEncodingDetails calculateLength(CharSequence messageBody, boolean use7bitOnly) {
+ return SMSDispatcherUtil.calculateLengthGsm(messageBody, use7bitOnly);
+ }
+
/**
* Called when a status report is received. This should correspond to
* a previously successful SEND.
@@ -121,25 +140,17 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
SmsMessage sms = SmsMessage.newFromCDS(pdu);
if (sms != null) {
- int tpStatus = sms.getStatus();
int messageRef = sms.mMessageRef;
for (int i = 0, count = deliveryPendingList.size(); i < count; i++) {
SmsTracker tracker = deliveryPendingList.get(i);
if (tracker.mMessageRef == messageRef) {
- // Found it. Remove from list and broadcast.
- if(tpStatus >= Sms.STATUS_FAILED || tpStatus < Sms.STATUS_PENDING ) {
- deliveryPendingList.remove(i);
- // Update the message status (COMPLETE or FAILED)
- tracker.updateSentMessageStatus(mContext, tpStatus);
+ Pair<Boolean, Boolean> result = mSmsDispatchersController.handleSmsStatusReport(
+ tracker,
+ getFormat(),
+ pdu);
+ if (result.second) {
+ deliveryPendingList.remove(i);
}
- PendingIntent intent = tracker.mDeliveryIntent;
- Intent fillIn = new Intent();
- fillIn.putExtra("pdu", pdu);
- fillIn.putExtra("format", getFormat());
- try {
- intent.send(mContext, Activity.RESULT_OK, fillIn);
- } catch (CanceledException ex) {}
-
// Only expect to see one tracker matching this messageref
break;
}
@@ -150,101 +161,6 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
/** {@inheritDoc} */
@Override
- protected void sendData(String destAddr, String scAddr, int destPort,
- byte[] data, PendingIntent sentIntent, PendingIntent deliveryIntent) {
- SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
- scAddr, destAddr, destPort, data, (deliveryIntent != null));
- if (pdu != null) {
- HashMap map = getSmsTrackerMap(destAddr, scAddr, destPort, data, pdu);
- SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
- null /*messageUri*/, false /*isExpectMore*/, null /*fullMessageText*/,
- false /*isText*/, true /*persistMessage*/);
-
- String carrierPackage = getCarrierAppPackageName();
- if (carrierPackage != null) {
- Rlog.d(TAG, "Found carrier package.");
- DataSmsSender smsSender = new DataSmsSender(tracker);
- smsSender.sendSmsByCarrierApp(carrierPackage, new SmsSenderCallback(smsSender));
- } else {
- Rlog.v(TAG, "No carrier package.");
- sendRawPdu(tracker);
- }
- } else {
- Rlog.e(TAG, "GsmSMSDispatcher.sendData(): getSubmitPdu() returned null");
- }
- }
-
- /** {@inheritDoc} */
- @VisibleForTesting
- @Override
- public void sendText(String destAddr, String scAddr, String text, PendingIntent sentIntent,
- PendingIntent deliveryIntent, Uri messageUri, String callingPkg,
- boolean persistMessage) {
- SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(
- scAddr, destAddr, text, (deliveryIntent != null));
- if (pdu != null) {
- HashMap map = getSmsTrackerMap(destAddr, scAddr, text, pdu);
- SmsTracker tracker = getSmsTracker(map, sentIntent, deliveryIntent, getFormat(),
- messageUri, false /*isExpectMore*/, text /*fullMessageText*/, true /*isText*/,
- persistMessage);
-
- String carrierPackage = getCarrierAppPackageName();
- if (carrierPackage != null) {
- Rlog.d(TAG, "Found carrier package.");
- TextSmsSender smsSender = new TextSmsSender(tracker);
- smsSender.sendSmsByCarrierApp(carrierPackage, new SmsSenderCallback(smsSender));
- } else {
- Rlog.v(TAG, "No carrier package.");
- sendRawPdu(tracker);
- }
- } else {
- Rlog.e(TAG, "GsmSMSDispatcher.sendText(): getSubmitPdu() returned null");
- }
- }
-
- /** {@inheritDoc} */
- @Override
- protected void injectSmsPdu(byte[] pdu, String format, PendingIntent receivedIntent) {
- throw new IllegalStateException("This method must be called only on ImsSMSDispatcher");
- }
-
- /** {@inheritDoc} */
- @Override
- protected GsmAlphabet.TextEncodingDetails calculateLength(CharSequence messageBody,
- boolean use7bitOnly) {
- return SmsMessage.calculateLength(messageBody, use7bitOnly);
- }
-
- /** {@inheritDoc} */
- @Override
- protected 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) {
- SmsMessage.SubmitPdu pdu = SmsMessage.getSubmitPdu(scAddress, destinationAddress,
- message, deliveryIntent != null, SmsHeader.toByteArray(smsHeader),
- 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, fullMessageText, true /*isText*/,
- false /*persistMessage*/);
- } else {
- Rlog.e(TAG, "GsmSMSDispatcher.sendNewSubmitPdu(): getSubmitPdu() returned null");
- return null;
- }
- }
-
- @Override
- protected void sendSubmitPdu(SmsTracker tracker) {
- sendRawPdu(tracker);
- }
-
- /** {@inheritDoc} */
- @Override
protected void sendSms(SmsTracker tracker) {
HashMap<String, Object> map = tracker.getData();
@@ -271,12 +187,6 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
+ " mMessageRef=" + tracker.mMessageRef
+ " SS=" + mPhone.getServiceState().getState());
- sendSmsByPstn(tracker);
- }
-
- /** {@inheritDoc} */
- @Override
- protected void sendSmsByPstn(SmsTracker tracker) {
int ss = mPhone.getServiceState().getState();
// if sms over IMS is not supported on data and voice is not available...
if (!isIms() && ss != ServiceState.STATE_IN_SERVICE) {
@@ -284,10 +194,7 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
return;
}
- HashMap<String, Object> map = tracker.getData();
-
byte smsc[] = (byte[]) map.get("smsc");
- byte[] pdu = (byte[]) map.get("pdu");
Message reply = obtainMessage(EVENT_SEND_SMS_COMPLETE, tracker);
// sms over gsm is used:
@@ -295,15 +202,6 @@ public final class GsmSMSDispatcher extends SMSDispatcher {
// this is not a retry case after sms over IMS failed
// indicated by mImsRetry > 0
if (0 == tracker.mImsRetry && !isIms()) {
- if (tracker.mRetryCount > 0) {
- // per TS 23.040 Section 9.2.3.6: If TP-MTI SMS-SUBMIT (0x01) type
- // TP-RD (bit 2) is 1 for retry
- // and TP-MR is set to previously failed sms TP-MR
- if (((0x01 & pdu[0]) == 0x01)) {
- pdu[0] |= 0x04; // TP-RD
- pdu[1] = (byte) tracker.mMessageRef; // TP-MR
- }
- }
if (tracker.mRetryCount == 0 && tracker.mExpectMore) {
mCi.sendSMSExpectMore(IccUtils.bytesToHexString(smsc),
IccUtils.bytesToHexString(pdu), reply);
diff --git a/com/android/internal/telephony/ims/ImsResolver.java b/com/android/internal/telephony/ims/ImsResolver.java
index 4a649840..bb785e67 100644
--- a/com/android/internal/telephony/ims/ImsResolver.java
+++ b/com/android/internal/telephony/ims/ImsResolver.java
@@ -17,6 +17,7 @@
package com.android.internal.telephony.ims;
import android.Manifest;
+import android.annotation.Nullable;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -40,6 +41,7 @@ import android.util.SparseArray;
import com.android.ims.internal.IImsMMTelFeature;
import com.android.ims.internal.IImsRcsFeature;
+import com.android.ims.internal.IImsRegistration;
import com.android.ims.internal.IImsServiceFeatureCallback;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.PhoneConstants;
@@ -344,9 +346,20 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
return (controller != null) ? controller.getRcsFeature(slotId) : null;
}
+ /**
+ * Returns the ImsRegistration structure associated with the slotId and feature specified.
+ */
+ public @Nullable IImsRegistration getImsRegistration(int slotId, int feature)
+ throws RemoteException {
+ ImsServiceController controller = getImsServiceController(slotId, feature);
+ if (controller != null) {
+ return controller.getRegistration(slotId);
+ }
+ return null;
+ }
+
@VisibleForTesting
- public ImsServiceController getImsServiceControllerAndListen(int slotId, int feature,
- IImsServiceFeatureCallback callback) {
+ public ImsServiceController getImsServiceController(int slotId, int feature) {
if (slotId < 0 || slotId >= mNumSlots) {
return null;
}
@@ -358,6 +371,14 @@ public class ImsResolver implements ImsServiceController.ImsServiceControllerCal
}
controller = services.get(feature);
}
+ return controller;
+ }
+
+ @VisibleForTesting
+ public ImsServiceController getImsServiceControllerAndListen(int slotId, int feature,
+ IImsServiceFeatureCallback callback) {
+ ImsServiceController controller = getImsServiceController(slotId, feature);
+
if (controller != null) {
controller.addImsServiceFeatureListener(callback);
return controller;
diff --git a/com/android/internal/telephony/ims/ImsServiceController.java b/com/android/internal/telephony/ims/ImsServiceController.java
index 6f31b50a..284c9de3 100644
--- a/com/android/internal/telephony/ims/ImsServiceController.java
+++ b/com/android/internal/telephony/ims/ImsServiceController.java
@@ -34,6 +34,7 @@ import android.util.Pair;
import com.android.ims.internal.IImsFeatureStatusCallback;
import com.android.ims.internal.IImsMMTelFeature;
import com.android.ims.internal.IImsRcsFeature;
+import com.android.ims.internal.IImsRegistration;
import com.android.ims.internal.IImsServiceController;
import com.android.ims.internal.IImsServiceFeatureCallback;
import com.android.internal.annotations.VisibleForTesting;
@@ -366,10 +367,8 @@ public class ImsServiceController {
}
/**
- * Finds the difference between the set of features that the ImsService has active and the new
- * set defined in newImsFeatures. For every feature that is added,
- * {@link IImsServiceController#createImsFeature} is called on the service. For every ImsFeature
- * that is removed, {@link IImsServiceController#removeImsFeature} is called.
+ * For every feature that is added, the service calls the associated create. For every
+ * ImsFeature that is removed, {@link IImsServiceController#removeImsFeature} is called.
*/
public void changeImsServiceFeatures(HashSet<Pair<Integer, Integer>> newImsFeatures)
throws RemoteException {
@@ -466,6 +465,15 @@ public class ImsServiceController {
}
}
+ /**
+ * @return the IImsRegistration that corresponds to the slot id specified.
+ */
+ public IImsRegistration getRegistration(int slotId) throws RemoteException {
+ synchronized (mLock) {
+ return mIImsServiceController.getRegistration(slotId);
+ }
+ }
+
private void removeImsServiceFeatureListener() {
synchronized (mLock) {
mImsStatusCallbacks.clear();
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java b/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
index 05a33cc2..73959a3c 100644
--- a/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
+++ b/com/android/internal/telephony/imsphone/ImsPhoneCallTracker.java
@@ -24,7 +24,10 @@ import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
+import android.net.NetworkRequest;
import android.net.NetworkStats;
import android.net.Uri;
import android.os.AsyncResult;
@@ -84,6 +87,7 @@ import com.android.internal.telephony.CommandsInterface;
import com.android.internal.telephony.Connection;
import com.android.internal.telephony.Phone;
import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.SubscriptionController;
import com.android.internal.telephony.TelephonyProperties;
import com.android.internal.telephony.dataconnection.DataEnabledSettings;
import com.android.internal.telephony.gsm.SuppServiceNotification;
@@ -221,7 +225,6 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
log("onReceive : Updating mAllowEmergencyVideoCalls = " +
mAllowEmergencyVideoCalls);
}
- mCarrierConfigLoaded = true;
} else if (TelecomManager.ACTION_CHANGE_DEFAULT_DIALER.equals(intent.getAction())) {
mDefaultDialerUid.set(getPackageUid(context, intent.getStringExtra(
TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME)));
@@ -229,6 +232,24 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
}
};
+ /**
+ * Tracks whether we are currently monitoring network connectivity for the purpose of warning
+ * the user of an inability to handover from LTE to WIFI for video calls.
+ */
+ private boolean mIsMonitoringConnectivity = false;
+
+ /**
+ * Network callback used to schedule the handover check when a wireless network connects.
+ */
+ private ConnectivityManager.NetworkCallback mNetworkCallback =
+ new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onAvailable(Network network) {
+ Rlog.i(LOG_TAG, "Network available: " + network);
+ scheduleHandoverCheck();
+ }
+ };
+
//***** Constants
static final int MAX_CONNECTIONS = 7;
@@ -585,6 +606,22 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
private boolean mNotifyHandoverVideoFromWifiToLTE = false;
/**
+ * Carrier configuration option which determines whether the carrier wants to inform the user
+ * when a video call is handed over from LTE to WIFI.
+ * See {@link CarrierConfigManager#KEY_NOTIFY_HANDOVER_VIDEO_FROM_LTE_TO_WIFI_BOOL} for more
+ * information.
+ */
+ private boolean mNotifyHandoverVideoFromLTEToWifi = false;
+
+ /**
+ * When {@code} false, indicates that no handover from LTE to WIFI has occurred during the start
+ * of the call.
+ * When {@code true}, indicates that the start of call handover from LTE to WIFI has been
+ * attempted (it may have suceeded or failed).
+ */
+ private boolean mHasPerformedStartOfCallHandover = false;
+
+ /**
* Carrier configuration option which determines whether the carrier supports the
* {@link VideoProfile#STATE_PAUSED} signalling.
* See {@link CarrierConfigManager#KEY_SUPPORT_PAUSE_IMS_VIDEO_CALLS_BOOL} for more information.
@@ -628,30 +665,39 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
};
// Callback fires when ImsManager MMTel Feature changes state
- private ImsServiceProxy.INotifyStatusChanged mNotifyStatusChangedCallback = () -> {
- try {
- int status = mImsManager.getImsServiceStatus();
- log("Status Changed: " + status);
- switch(status) {
- case ImsFeature.STATE_READY: {
- startListeningForCalls();
- break;
- }
- case ImsFeature.STATE_INITIALIZING:
- // fall through
- case ImsFeature.STATE_NOT_AVAILABLE: {
- stopListeningForCalls();
- break;
+ private ImsServiceProxy.IFeatureUpdate mNotifyStatusChangedCallback =
+ new ImsServiceProxy.IFeatureUpdate() {
+ @Override
+ public void notifyStateChanged() {
+ try {
+ int status = mImsManager.getImsServiceStatus();
+ log("Status Changed: " + status);
+ switch (status) {
+ case ImsFeature.STATE_READY: {
+ startListeningForCalls();
+ break;
+ }
+ case ImsFeature.STATE_INITIALIZING:
+ // fall through
+ case ImsFeature.STATE_NOT_AVAILABLE: {
+ stopListeningForCalls();
+ break;
+ }
+ default: {
+ Log.w(LOG_TAG, "Unexpected State!");
+ }
+ }
+ } catch (ImsException e) {
+ // Could not get the ImsService, retry!
+ retryGetImsService();
+ }
}
- default: {
- Log.w(LOG_TAG, "Unexpected State!");
+
+ @Override
+ public void notifyUnavailable() {
+ retryGetImsService();
}
- }
- } catch (ImsException e) {
- // Could not get the ImsService, retry!
- retryGetImsService();
- }
- };
+ };
@VisibleForTesting
public interface IRetryTimeout {
@@ -757,7 +803,7 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
mImsManager.addNotifyStatusChangedCallbackIfAvailable(mNotifyStatusChangedCallback);
// Wait for ImsService.STATE_READY to start listening for calls.
// Call the callback right away for compatibility with older devices that do not use states.
- mNotifyStatusChangedCallback.notifyStatusChanged();
+ mNotifyStatusChangedCallback.notifyStateChanged();
}
private void startListeningForCalls() throws ImsException {
@@ -995,17 +1041,32 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
private void cacheCarrierConfiguration(int subId) {
CarrierConfigManager carrierConfigManager = (CarrierConfigManager)
mPhone.getContext().getSystemService(Context.CARRIER_CONFIG_SERVICE);
- if (carrierConfigManager == null) {
- loge("cacheCarrierConfiguration: No carrier config service found.");
+ if (carrierConfigManager == null
+ || !SubscriptionController.getInstance().isActiveSubId(subId)) {
+ loge("cacheCarrierConfiguration: No carrier config service found" + " "
+ + "or not active subId = " + subId);
+ mCarrierConfigLoaded = false;
return;
}
PersistableBundle carrierConfig = carrierConfigManager.getConfigForSubId(subId);
if (carrierConfig == null) {
loge("cacheCarrierConfiguration: Empty carrier config.");
+ mCarrierConfigLoaded = false;
return;
}
+ mCarrierConfigLoaded = true;
+ updateCarrierConfigCache(carrierConfig);
+ }
+
+ /**
+ * Updates the local carrier config cache from a bundle obtained from the carrier config
+ * manager. Also supports unit testing by injecting configuration at test time.
+ * @param carrierConfig The config bundle.
+ */
+ @VisibleForTesting
+ public void updateCarrierConfigCache(PersistableBundle carrierConfig) {
mAllowEmergencyVideoCalls =
carrierConfig.getBoolean(CarrierConfigManager.KEY_ALLOW_EMERGENCY_VIDEO_CALLS_BOOL);
mTreatDowngradedVideoCallsAsVideoCalls =
@@ -1023,6 +1084,8 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
CarrierConfigManager.KEY_SUPPORT_DOWNGRADE_VT_TO_AUDIO_BOOL);
mNotifyHandoverVideoFromWifiToLTE = carrierConfig.getBoolean(
CarrierConfigManager.KEY_NOTIFY_HANDOVER_VIDEO_FROM_WIFI_TO_LTE_BOOL);
+ mNotifyHandoverVideoFromLTEToWifi = carrierConfig.getBoolean(
+ CarrierConfigManager.KEY_NOTIFY_HANDOVER_VIDEO_FROM_LTE_TO_WIFI_BOOL);
mIgnoreDataEnabledChangedForVideoCalls = carrierConfig.getBoolean(
CarrierConfigManager.KEY_IGNORE_DATA_ENABLED_CHANGED_FOR_VIDEO_CALLS);
mIsViLteDataMetered = carrierConfig.getBoolean(
@@ -1883,6 +1946,8 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
int code = maybeRemapReasonCode(reasonInfo);
switch (code) {
+ case ImsReasonInfo.CODE_SIP_ALTERNATE_EMERGENCY_CALL:
+ return DisconnectCause.IMS_SIP_ALTERNATE_EMERGENCY_CALL;
case ImsReasonInfo.CODE_SIP_BAD_ADDRESS:
case ImsReasonInfo.CODE_SIP_NOT_REACHABLE:
return DisconnectCause.NUMBER_UNREACHABLE;
@@ -2072,13 +2137,18 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
processCallStateChange(imsCall, ImsPhoneCall.State.ACTIVE,
DisconnectCause.NOT_DISCONNECTED);
- if (mNotifyVtHandoverToWifiFail &&
- !imsCall.isWifiCall() && imsCall.isVideoCall() && isWifiConnected()) {
- // Schedule check to see if handover succeeded.
- sendMessageDelayed(obtainMessage(EVENT_CHECK_FOR_WIFI_HANDOVER, imsCall),
- HANDOVER_TO_WIFI_TIMEOUT_MS);
+ if (mNotifyVtHandoverToWifiFail && imsCall.isVideoCall() && !imsCall.isWifiCall()) {
+ if (isWifiConnected()) {
+ // Schedule check to see if handover succeeded.
+ sendMessageDelayed(obtainMessage(EVENT_CHECK_FOR_WIFI_HANDOVER, imsCall),
+ HANDOVER_TO_WIFI_TIMEOUT_MS);
+ } else {
+ // No wifi connectivity, so keep track of network availability for potential
+ // handover.
+ registerForConnectivityChanges();
+ }
}
-
+ mHasPerformedStartOfCallHandover = false;
mMetrics.writeOnImsCallStarted(mPhone.getPhoneId(), imsCall.getCallSession());
}
@@ -2565,18 +2635,35 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
boolean isHandoverToWifi = srcAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN
&& srcAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN
&& targetAccessTech == ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN;
- if (isHandoverToWifi) {
- // If we handed over to wifi successfully, don't check for failure in the future.
- removeMessages(EVENT_CHECK_FOR_WIFI_HANDOVER);
- }
+ // Only consider it a handover from WIFI if the source and target radio tech is known.
+ boolean isHandoverFromWifi =
+ srcAccessTech == ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN
+ && targetAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN
+ && targetAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN;
ImsPhoneConnection conn = findConnection(imsCall);
if (conn != null) {
- // Only consider it a handover from WIFI if the source and target radio tech is known.
- boolean isHandoverFromWifi =
- srcAccessTech == ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN
- && targetAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_UNKNOWN
- && targetAccessTech != ServiceState.RIL_RADIO_TECHNOLOGY_IWLAN;
+ if (conn.getDisconnectCause() == DisconnectCause.NOT_DISCONNECTED) {
+ if (isHandoverToWifi) {
+ removeMessages(EVENT_CHECK_FOR_WIFI_HANDOVER);
+
+ if (mNotifyHandoverVideoFromLTEToWifi && mHasPerformedStartOfCallHandover) {
+ // This is a handover which happened mid-call (ie not the start of call
+ // handover from LTE to WIFI), so we'll notify the InCall UI.
+ conn.onConnectionEvent(
+ TelephonyManager.EVENT_HANDOVER_VIDEO_FROM_LTE_TO_WIFI, null);
+ }
+
+ // We are on WIFI now so no need to get notified of network availability.
+ unregisterForConnectivityChanges();
+ } else if (isHandoverFromWifi && imsCall.isVideoCall()) {
+ // A video call just dropped from WIFI to LTE; we want to be informed if a
+ // new WIFI
+ // network comes into range.
+ registerForConnectivityChanges();
+ }
+ }
+
if (isHandoverFromWifi && imsCall.isVideoCall()) {
if (mNotifyHandoverVideoFromWifiToLTE && mIsDataEnabled) {
if (conn.getDisconnectCause() == DisconnectCause.NOT_DISCONNECTED) {
@@ -2602,6 +2689,9 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
loge("onCallHandover :: connection null.");
}
+ if (!mHasPerformedStartOfCallHandover) {
+ mHasPerformedStartOfCallHandover = true;
+ }
mMetrics.writeOnImsCallHandoverEvent(mPhone.getPhoneId(),
TelephonyCallSession.Event.Type.IMS_CALL_HANDOVER, imsCall.getCallSession(),
srcAccessTech, targetAccessTech, reasonInfo);
@@ -2627,11 +2717,20 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
// If we know we failed to handover, don't check for failure in the future.
removeMessages(EVENT_CHECK_FOR_WIFI_HANDOVER);
+ if (imsCall.isVideoCall()
+ && conn.getDisconnectCause() == DisconnectCause.NOT_DISCONNECTED) {
+ // Start listening for a WIFI network to come into range for potential handover.
+ registerForConnectivityChanges();
+ }
+
if (mNotifyVtHandoverToWifiFail) {
// Only notify others if carrier config indicates to do so.
conn.onHandoverToWifiFailed();
}
}
+ if (!mHasPerformedStartOfCallHandover) {
+ mHasPerformedStartOfCallHandover = true;
+ }
}
@Override
@@ -2709,6 +2808,8 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
public void onCallTerminated(ImsCall imsCall, ImsReasonInfo reasonInfo) {
if (DBG) log("mImsUssdListener onCallTerminated reasonCode=" + reasonInfo.getCode());
removeMessages(EVENT_CHECK_FOR_WIFI_HANDOVER);
+ mHasPerformedStartOfCallHandover = false;
+ unregisterForConnectivityChanges();
if (imsCall == mUssdSession) {
mUssdSession = null;
@@ -2977,12 +3078,24 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
case EVENT_CHECK_FOR_WIFI_HANDOVER:
if (msg.obj instanceof ImsCall) {
ImsCall imsCall = (ImsCall) msg.obj;
+ if (imsCall != mForegroundCall.getImsCall()) {
+ Rlog.i(LOG_TAG, "handoverCheck: no longer FG; check skipped.");
+ unregisterForConnectivityChanges();
+ // Handover check and its not the foreground call any more.
+ return;
+ }
if (!imsCall.isWifiCall()) {
// Call did not handover to wifi, notify of handover failure.
ImsPhoneConnection conn = findConnection(imsCall);
if (conn != null) {
+ Rlog.i(LOG_TAG, "handoverCheck: handover failed.");
conn.onHandoverToWifiFailed();
}
+
+ if (imsCall.isVideoCall()
+ && conn.getDisconnectCause() == DisconnectCause.NOT_DISCONNECTED) {
+ registerForConnectivityChanges();
+ }
}
}
break;
@@ -3040,7 +3153,8 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
// Uid -1 indicates this is for the overall device data usage.
vtDataUsageSnapshot.combineValues(new NetworkStats.Entry(
NetworkStatsService.VT_INTERFACE, -1, NetworkStats.SET_FOREGROUND,
- NetworkStats.TAG_NONE, 1, isRoaming, delta / 2, 0, delta / 2, 0, 0));
+ NetworkStats.TAG_NONE, NetworkStats.METERED_YES, isRoaming,
+ NetworkStats.DEFAULT_NETWORK_YES, delta / 2, 0, delta / 2, 0, 0));
mVtDataUsageSnapshot = vtDataUsageSnapshot;
// Create the snapshot of video call data usage per dialer. combineValues will create
@@ -3062,8 +3176,8 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
// the only thing we can do here is splitting the usage into half rx and half tx.
vtDataUsageUidSnapshot.combineValues(new NetworkStats.Entry(
NetworkStatsService.VT_INTERFACE, mDefaultDialerUid.get(),
- NetworkStats.SET_FOREGROUND, NetworkStats.TAG_NONE, 1, isRoaming, delta / 2,
- 0, delta / 2, 0, 0));
+ NetworkStats.SET_FOREGROUND, NetworkStats.TAG_NONE, NetworkStats.METERED_YES,
+ isRoaming, NetworkStats.DEFAULT_NETWORK_YES, delta / 2, 0, delta / 2, 0, 0));
mVtDataUsageUidSnapshot = vtDataUsageUidSnapshot;
}
@@ -3204,6 +3318,9 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
if (mImsManager.isServiceAvailable()) {
return;
}
+ // remove callback so we do not receive updates from old ImsServiceProxy when switching
+ // between ImsServices.
+ mImsManager.removeNotifyStatusChangedCallback(mNotifyStatusChangedCallback);
//Leave mImsManager as null, then CallStateException will be thrown when dialing
mImsManager = null;
// Exponential backoff during retry, limited to 32 seconds.
@@ -3475,7 +3592,7 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
// We do not want to update the ImsConfig for REASON_REGISTERED, since it can happen before
// the carrier config has loaded and will deregister IMS.
if (!mShouldUpdateImsConfigOnDisconnect
- && reason != DataEnabledSettings.REASON_REGISTERED) {
+ && reason != DataEnabledSettings.REASON_REGISTERED && mCarrierConfigLoaded) {
// This will call into updateVideoCallFeatureValue and eventually all clients will be
// asynchronously notified that the availability of VT over LTE has changed.
if (mImsManager != null) {
@@ -3606,12 +3723,75 @@ public class ImsPhoneCallTracker extends CallTracker implements ImsPullCall {
}
/**
+ * Registers for changes to network connectivity. Specifically requests the availability of new
+ * WIFI networks which an IMS video call could potentially hand over to.
+ */
+ private void registerForConnectivityChanges() {
+ if (mIsMonitoringConnectivity || !mNotifyVtHandoverToWifiFail) {
+ return;
+ }
+ ConnectivityManager cm = (ConnectivityManager) mPhone.getContext()
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (cm != null) {
+ Rlog.i(LOG_TAG, "registerForConnectivityChanges");
+ NetworkCapabilities capabilities = new NetworkCapabilities();
+ capabilities.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
+ NetworkRequest.Builder builder = new NetworkRequest.Builder();
+ builder.setCapabilities(capabilities);
+ cm.registerNetworkCallback(builder.build(), mNetworkCallback);
+ mIsMonitoringConnectivity = true;
+ }
+ }
+
+ /**
+ * Unregister for connectivity changes. Will be called when a call disconnects or if the call
+ * ends up handing over to WIFI.
+ */
+ private void unregisterForConnectivityChanges() {
+ if (!mIsMonitoringConnectivity || !mNotifyVtHandoverToWifiFail) {
+ return;
+ }
+ ConnectivityManager cm = (ConnectivityManager) mPhone.getContext()
+ .getSystemService(Context.CONNECTIVITY_SERVICE);
+ if (cm != null) {
+ Rlog.i(LOG_TAG, "unregisterForConnectivityChanges");
+ cm.unregisterNetworkCallback(mNetworkCallback);
+ mIsMonitoringConnectivity = false;
+ }
+ }
+
+ /**
+ * If the foreground call is a video call, schedule a handover check if one is not already
+ * scheduled. This method is intended ONLY for use when scheduling to watch for mid-call
+ * handovers.
+ */
+ private void scheduleHandoverCheck() {
+ ImsCall fgCall = mForegroundCall.getImsCall();
+ ImsPhoneConnection conn = mForegroundCall.getFirstConnection();
+ if (!mNotifyVtHandoverToWifiFail || fgCall == null || !fgCall.isVideoCall() || conn == null
+ || conn.getDisconnectCause() != DisconnectCause.NOT_DISCONNECTED) {
+ return;
+ }
+
+ if (!hasMessages(EVENT_CHECK_FOR_WIFI_HANDOVER)) {
+ Rlog.i(LOG_TAG, "scheduleHandoverCheck: schedule");
+ sendMessageDelayed(obtainMessage(EVENT_CHECK_FOR_WIFI_HANDOVER, fgCall),
+ HANDOVER_TO_WIFI_TIMEOUT_MS);
+ }
+ }
+
+ /**
* @return {@code true} if downgrading of a video call to audio is supported.
*/
public boolean isCarrierDowngradeOfVtCallSupported() {
return mSupportDowngradeVtToAudio;
}
+ @VisibleForTesting
+ public void setDataEnabled(boolean isDataEnabled) {
+ mIsDataEnabled = isDataEnabled;
+ }
+
private void handleFeatureCapabilityChanged(int serviceClass,
int[] enabledFeatures, int[] disabledFeatures) {
if (serviceClass == ImsServiceClass.MMTEL) {
diff --git a/com/android/internal/telephony/imsphone/ImsPhoneCommandInterface.java b/com/android/internal/telephony/imsphone/ImsPhoneCommandInterface.java
index 2683a802..41cffb68 100644
--- a/com/android/internal/telephony/imsphone/ImsPhoneCommandInterface.java
+++ b/com/android/internal/telephony/imsphone/ImsPhoneCommandInterface.java
@@ -17,6 +17,7 @@
package com.android.internal.telephony.imsphone;
import android.content.Context;
+import android.net.KeepalivePacketData;
import android.os.Handler;
import android.os.Message;
import android.service.carrier.CarrierIdentifier;
@@ -648,4 +649,13 @@ class ImsPhoneCommandInterface extends BaseCommands implements CommandsInterface
@Override
public void setSimCardPower(int state, Message result) {
}
+
+ @Override
+ public void startNattKeepalive(
+ int contextId, KeepalivePacketData packetData, int intervalMillis, Message result) {
+ }
+
+ @Override
+ public void stopNattKeepalive(int sessionHandle, Message result) {
+ }
}
diff --git a/com/android/internal/telephony/metrics/TelephonyEventBuilder.java b/com/android/internal/telephony/metrics/TelephonyEventBuilder.java
index 65308028..e06ead2d 100644
--- a/com/android/internal/telephony/metrics/TelephonyEventBuilder.java
+++ b/com/android/internal/telephony/metrics/TelephonyEventBuilder.java
@@ -20,6 +20,8 @@ import static com.android.internal.telephony.nano.TelephonyProto.ImsCapabilities
import static com.android.internal.telephony.nano.TelephonyProto.ImsConnectionState;
import static com.android.internal.telephony.nano.TelephonyProto.RilDataCall;
import static com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent;
+import static com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.CarrierIdMatching;
+import static com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.CarrierKeyChange;
import static com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.ModemRestart;
import static com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilDeactivateDataCall;
import static com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilSetupDataCall;
@@ -116,4 +118,19 @@ public class TelephonyEventBuilder {
mEvent.modemRestart = modemRestart;
return this;
}
+
+ /**
+ * Set and build Carrier Id Matching event
+ */
+ public TelephonyEventBuilder setCarrierIdMatching(CarrierIdMatching carrierIdMatching) {
+ mEvent.type = TelephonyEvent.Type.CARRIER_ID_MATCHING;
+ mEvent.carrierIdMatching = carrierIdMatching;
+ return this;
+ }
+
+ public TelephonyEventBuilder setCarrierKeyChange(CarrierKeyChange carrierKeyChange) {
+ mEvent.type = TelephonyEvent.Type.CARRIER_KEY_CHANGED;
+ mEvent.carrierKeyChange = carrierKeyChange;
+ return this;
+ }
}
diff --git a/com/android/internal/telephony/metrics/TelephonyMetrics.java b/com/android/internal/telephony/metrics/TelephonyMetrics.java
index ba41bea5..0c6a61d6 100644
--- a/com/android/internal/telephony/metrics/TelephonyMetrics.java
+++ b/com/android/internal/telephony/metrics/TelephonyMetrics.java
@@ -40,6 +40,7 @@ import android.os.SystemClock;
import android.telephony.Rlog;
import android.telephony.ServiceState;
import android.telephony.TelephonyHistogram;
+import android.telephony.TelephonyManager;
import android.telephony.data.DataCallResponse;
import android.text.TextUtils;
import android.util.Base64;
@@ -65,6 +66,9 @@ import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.E
import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.RilCall;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyCallSession.Event.RilCall.Type;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.CarrierIdMatching;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.CarrierIdMatchingResult;
+import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.CarrierKeyChange;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.ModemRestart;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilDeactivateDataCall;
import com.android.internal.telephony.nano.TelephonyProto.TelephonyEvent.RilSetupDataCall;
@@ -229,6 +233,8 @@ public class TelephonyMetrics {
return "DATA_STALL_ACTION";
case TelephonyEvent.Type.MODEM_RESTART:
return "MODEM_RESTART";
+ case TelephonyEvent.Type.CARRIER_ID_MATCHING:
+ return "CARRIER_ID_MATCHING";
default:
return Integer.toString(event);
}
@@ -507,7 +513,6 @@ public class TelephonyMetrics {
log.endTime = new TelephonyProto.Time();
log.endTime.systemTimestampMillis = System.currentTimeMillis();
log.endTime.elapsedTimestampMillis = SystemClock.elapsedRealtime();
-
return log;
}
@@ -523,6 +528,23 @@ public class TelephonyMetrics {
}
/**
+ * Write the Carrier Key change event
+ *
+ * @param phoneId Phone id
+ * @param keyType type of key
+ * @param isDownloadSuccessful true if the key was successfully downloaded
+ */
+ public void writeCarrierKeyEvent(int phoneId, int keyType, boolean isDownloadSuccessful) {
+ final CarrierKeyChange carrierKeyChange = new CarrierKeyChange();
+ carrierKeyChange.keyType = keyType;
+ carrierKeyChange.isDownloadSuccessful = isDownloadSuccessful;
+ TelephonyEvent event = new TelephonyEventBuilder(phoneId).setCarrierKeyChange(
+ carrierKeyChange).build();
+ addTelephonyEvent(event);
+ }
+
+
+ /**
* Get the time interval with reduced prevision
*
* @param previousTimestamp Previous timestamp in milliseconds
@@ -1750,6 +1772,33 @@ public class TelephonyMetrics {
addTelephonyEvent(event);
}
+ /**
+ * Write carrier identification matching event
+ *
+ * @param phoneId Phone id
+ * @param version Carrier table version
+ * @param cid Unique Carrier Id
+ * @param gid1 Group id level 1
+ */
+ public void writeCarrierIdMatchingEvent(int phoneId, int version, int cid, String gid1) {
+ final CarrierIdMatching carrierIdMatching = new CarrierIdMatching();
+ final CarrierIdMatchingResult carrierIdMatchingResult = new CarrierIdMatchingResult();
+
+ if (cid != TelephonyManager.UNKNOWN_CARRIER_ID) {
+ carrierIdMatchingResult.carrierId = cid;
+ if (gid1 != null) {
+ carrierIdMatchingResult.gid1 = gid1;
+ }
+ }
+
+ carrierIdMatching.cidTableVersion = version;
+ carrierIdMatching.result = carrierIdMatchingResult;
+
+ TelephonyEvent event = new TelephonyEventBuilder(phoneId).setCarrierIdMatching(
+ carrierIdMatching).build();
+ addTelephonyEvent(event);
+ }
+
//TODO: Expand the proto in the future
public void writeOnImsCallProgressing(int phoneId, ImsCallSession session) {}
public void writeOnImsCallStarted(int phoneId, ImsCallSession session) {}
diff --git a/com/android/internal/telephony/sip/SipCommandInterface.java b/com/android/internal/telephony/sip/SipCommandInterface.java
index 69832e88..ba9ae646 100644
--- a/com/android/internal/telephony/sip/SipCommandInterface.java
+++ b/com/android/internal/telephony/sip/SipCommandInterface.java
@@ -17,6 +17,7 @@
package com.android.internal.telephony.sip;
import android.content.Context;
+import android.net.KeepalivePacketData;
import android.os.Handler;
import android.os.Message;
import android.service.carrier.CarrierIdentifier;
@@ -650,4 +651,13 @@ class SipCommandInterface extends BaseCommands implements CommandsInterface {
@Override
public void setSimCardPower(int state, Message result) {
}
+
+ @Override
+ public void startNattKeepalive(
+ int contextId, KeepalivePacketData packetData, int intervalMillis, Message result) {
+ }
+
+ @Override
+ public void stopNattKeepalive(int sessionHandle, Message result) {
+ }
}
diff --git a/com/android/internal/telephony/test/SimulatedCommands.java b/com/android/internal/telephony/test/SimulatedCommands.java
index 62938220..b0a7763a 100644
--- a/com/android/internal/telephony/test/SimulatedCommands.java
+++ b/com/android/internal/telephony/test/SimulatedCommands.java
@@ -18,6 +18,8 @@ package com.android.internal.telephony.test;
import android.hardware.radio.V1_0.DataRegStateResult;
import android.hardware.radio.V1_0.VoiceRegStateResult;
+import android.net.KeepalivePacketData;
+import android.net.LinkAddress;
import android.net.NetworkUtils;
import android.os.AsyncResult;
import android.os.Handler;
@@ -38,7 +40,6 @@ 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;
@@ -172,6 +173,7 @@ public class SimulatedCommands extends BaseCommands
@Override
public void getIccCardStatus(Message result) {
+ SimulatedCommandsVerifier.getInstance().getIccCardStatus(result);
if (mIccCardStatus != null) {
resultSuccess(result, mIccCardStatus);
} else {
@@ -185,10 +187,12 @@ public class SimulatedCommands extends BaseCommands
@Override
public void getIccSlotsStatus(Message result) {
+ SimulatedCommandsVerifier.getInstance().getIccSlotsStatus(result);
if (mIccSlotStatus != null) {
resultSuccess(result, mIccSlotStatus);
} else {
- resultFail(result, null, new RuntimeException("IccSlotStatus not set"));
+ resultFail(result, null,
+ new CommandException(CommandException.Error.REQUEST_NOT_SUPPORTED));
}
}
@@ -860,8 +864,7 @@ public class SimulatedCommands extends BaseCommands
SignalStrength.INVALID, // lteRsrq
SignalStrength.INVALID, // lteRssnr
SignalStrength.INVALID, // lteCqi
- SignalStrength.INVALID, // tdScdmaRscp
- true // gsmFlag
+ SignalStrength.INVALID // tdScdmaRscp
);
}
@@ -1129,7 +1132,7 @@ public class SimulatedCommands extends BaseCommands
if (mDcResponse == null) {
try {
mDcResponse = new DataCallResponse(0, -1, 1, 2, "IP", "rmnet_data7",
- Arrays.asList(new InterfaceAddress("12.34.56.78", 0)),
+ Arrays.asList(new LinkAddress("12.34.56.78/32")),
Arrays.asList(NetworkUtils.numericToInetAddress("98.76.54.32")),
Arrays.asList(NetworkUtils.numericToInetAddress("11.22.33.44")),
null, 1440);
@@ -2104,8 +2107,7 @@ public class SimulatedCommands extends BaseCommands
SignalStrength.INVALID, // lteRsrq
SignalStrength.INVALID, // lteRssnr
SignalStrength.INVALID, // lteCqi
- SignalStrength.INVALID, // tdScdmaRscp
- true // gsmFlag
+ SignalStrength.INVALID // tdScdmaRscp
);
}
@@ -2192,4 +2194,25 @@ public class SimulatedCommands extends BaseCommands
public void setRadioPowerFailResponse(boolean fail) {
mIsRadioPowerFailResponse = fail;
}
+
+ @Override
+ public void registerForIccRefresh(Handler h, int what, Object obj) {
+ super.registerForIccRefresh(h, what, obj);
+ SimulatedCommandsVerifier.getInstance().registerForIccRefresh(h, what, obj);
+ }
+
+ @Override
+ public void unregisterForIccRefresh(Handler h) {
+ super.unregisterForIccRefresh(h);
+ SimulatedCommandsVerifier.getInstance().unregisterForIccRefresh(h);
+ }
+
+ @Override
+ public void startNattKeepalive(
+ int contextId, KeepalivePacketData packetData, int intervalMillis, Message result) {
+ }
+
+ @Override
+ public void stopNattKeepalive(int sessionHandle, Message result) {
+ }
}
diff --git a/com/android/internal/telephony/test/SimulatedCommandsVerifier.java b/com/android/internal/telephony/test/SimulatedCommandsVerifier.java
index 40b15a97..1ede3c62 100644
--- a/com/android/internal/telephony/test/SimulatedCommandsVerifier.java
+++ b/com/android/internal/telephony/test/SimulatedCommandsVerifier.java
@@ -16,6 +16,7 @@
package com.android.internal.telephony.test;
+import android.net.KeepalivePacketData;
import android.os.Handler;
import android.os.Message;
import android.service.carrier.CarrierIdentifier;
@@ -1406,4 +1407,21 @@ public class SimulatedCommandsVerifier implements CommandsInterface {
@Override
public void unregisterForCarrierInfoForImsiEncryption(Handler h) {
}
+
+ @Override
+ public void registerForNattKeepaliveStatus(Handler h, int what, Object obj) {
+ }
+
+ @Override
+ public void unregisterForNattKeepaliveStatus(Handler h) {
+ }
+
+ @Override
+ public void startNattKeepalive(
+ int contextId, KeepalivePacketData packetData, int intervalMillis, Message result) {
+ }
+
+ @Override
+ public void stopNattKeepalive(int sessionHandle, Message result) {
+ }
}
diff --git a/com/android/internal/telephony/uicc/IccCardProxy.java b/com/android/internal/telephony/uicc/IccCardProxy.java
index f3c5ca5f..7c18d35c 100644
--- a/com/android/internal/telephony/uicc/IccCardProxy.java
+++ b/com/android/internal/telephony/uicc/IccCardProxy.java
@@ -36,11 +36,9 @@ import com.android.internal.telephony.CommandsInterface.RadioState;
import com.android.internal.telephony.IccCard;
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.PhoneConstants;
import com.android.internal.telephony.RILConstants;
-import com.android.internal.telephony.TelephonyIntents;
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,8 +80,6 @@ public class IccCardProxy extends Handler implements IccCard {
private static final int EVENT_NETWORK_LOCKED = 9;
private static final int EVENT_ICC_RECORD_EVENTS = 500;
- private static final int EVENT_SUBSCRIPTION_ACTIVATED = 501;
- private static final int EVENT_SUBSCRIPTION_DEACTIVATED = 502;
private static final int EVENT_CARRIER_PRIVILEGES_LOADED = 503;
private Integer mPhoneId = null;
@@ -232,26 +228,17 @@ public class IccCardProxy extends Handler implements IccCard {
mUiccCard.registerForCarrierPrivilegeRulesLoaded(
this, EVENT_CARRIER_PRIVILEGES_LOADED, null);
} else {
- onRecordsLoaded();
+ setExternalState(State.LOADED);
}
break;
case EVENT_IMSI_READY:
- broadcastIccStateChangedIntent(IccCardConstants.INTENT_VALUE_ICC_IMSI, null);
+ broadcastInternalIccStateChangedIntent(IccCardConstants.INTENT_VALUE_ICC_IMSI,
+ null);
break;
case EVENT_NETWORK_LOCKED:
mNetworkLockedRegistrants.notifyRegistrants();
setExternalState(State.NETWORK_LOCKED);
break;
- case EVENT_SUBSCRIPTION_ACTIVATED:
- log("EVENT_SUBSCRIPTION_ACTIVATED");
- onSubscriptionActivated();
- break;
-
- case EVENT_SUBSCRIPTION_DEACTIVATED:
- log("EVENT_SUBSCRIPTION_DEACTIVATED");
- onSubscriptionDeactivated();
- break;
-
case EVENT_ICC_RECORD_EVENTS:
if ((mCurrentAppType == UiccController.APP_FAM_3GPP) && (mIccRecords != null)) {
AsyncResult ar = (AsyncResult)msg.obj;
@@ -268,7 +255,7 @@ public class IccCardProxy extends Handler implements IccCard {
if (mUiccCard != null) {
mUiccCard.unregisterForCarrierPrivilegeRulesLoaded(this);
}
- onRecordsLoaded();
+ setExternalState(State.LOADED);
break;
default:
@@ -277,21 +264,6 @@ public class IccCardProxy extends Handler implements IccCard {
}
}
- private void onSubscriptionActivated() {
- updateIccAvailability();
- updateStateProperty();
- }
-
- private void onSubscriptionDeactivated() {
- resetProperties();
- updateIccAvailability();
- updateStateProperty();
- }
-
- private void onRecordsLoaded() {
- broadcastInternalIccStateChangedIntent(IccCardConstants.INTENT_VALUE_ICC_LOADED, null);
- }
-
private void updateIccAvailability() {
synchronized (mLock) {
UiccSlot newSlot = mUiccController.getUiccSlotForPhone(mPhoneId);
@@ -416,12 +388,12 @@ public class IccCardProxy extends Handler implements IccCard {
}
if (mUiccApplication != null) {
mUiccApplication.registerForReady(this, EVENT_APP_READY, 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.registerForNetworkLockedRecordsLoaded(this, EVENT_NETWORK_LOCKED, null);
mIccRecords.registerForRecordsEvents(this, EVENT_ICC_RECORD_EVENTS, null);
}
}
@@ -429,39 +401,15 @@ public class IccCardProxy extends Handler implements IccCard {
private void unregisterUiccCardEvents() {
if (mUiccSlot != null) mUiccSlot.unregisterForAbsent(this);
if (mUiccCard != null) mUiccCard.unregisterForCarrierPrivilegeRulesLoaded(this);
- if (mUiccApplication != null) mUiccApplication.unregisterForReady(this);
- if (mUiccApplication != null) mUiccApplication.unregisterForLocked(this);
- if (mUiccApplication != null) mUiccApplication.unregisterForNetworkLocked(this);
- if (mIccRecords != null) mIccRecords.unregisterForImsiReady(this);
- if (mIccRecords != null) mIccRecords.unregisterForRecordsLoaded(this);
- if (mIccRecords != null) mIccRecords.unregisterForRecordsEvents(this);
- }
-
- private void updateStateProperty() {
- mTelephonyManager.setSimStateForPhone(mPhoneId, getState().toString());
- }
-
- private void broadcastIccStateChangedIntent(String value, String reason) {
- synchronized (mLock) {
- if (mPhoneId == null || !SubscriptionManager.isValidSlotIndex(mPhoneId)) {
- loge("broadcastIccStateChangedIntent: mPhoneId=" + mPhoneId
- + " is invalid; Return!!");
- 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
- // information
- // intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- intent.putExtra(PhoneConstants.PHONE_NAME_KEY, "Phone");
- intent.putExtra(IccCardConstants.INTENT_KEY_ICC_STATE, value);
- intent.putExtra(IccCardConstants.INTENT_KEY_LOCKED_REASON, reason);
- SubscriptionManager.putPhoneIdAndSubIdExtra(intent, mPhoneId);
- log("broadcastIccStateChangedIntent intent ACTION_SIM_STATE_CHANGED value=" + value
- + " reason=" + reason + " for mPhoneId=" + mPhoneId);
- IntentBroadcaster.getInstance().broadcastStickyIntent(intent, mPhoneId);
+ if (mUiccApplication != null) {
+ mUiccApplication.unregisterForReady(this);
+ }
+ if (mIccRecords != null) {
+ mIccRecords.unregisterForImsiReady(this);
+ mIccRecords.unregisterForRecordsLoaded(this);
+ mIccRecords.unregisterForLockedRecordsLoaded(this);
+ mIccRecords.unregisterForNetworkLockedRecordsLoaded(this);
+ mIccRecords.unregisterForRecordsEvents(this);
}
}
@@ -473,13 +421,14 @@ public class IccCardProxy extends Handler implements IccCard {
}
Intent intent = new Intent(ACTION_INTERNAL_SIM_STATE_CHANGED);
- intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
- | Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+ | Intent.FLAG_RECEIVER_FOREGROUND);
intent.putExtra(PhoneConstants.PHONE_NAME_KEY, "Phone");
intent.putExtra(IccCardConstants.INTENT_KEY_ICC_STATE, value);
intent.putExtra(IccCardConstants.INTENT_KEY_LOCKED_REASON, reason);
intent.putExtra(PhoneConstants.PHONE_KEY, mPhoneId); // SubId may not be valid.
- log("Sending intent ACTION_INTERNAL_SIM_STATE_CHANGED value=" + value
+ log("broadcastInternalIccStateChangedIntent: Sending intent "
+ + "ACTION_INTERNAL_SIM_STATE_CHANGED value = " + value
+ " for mPhoneId : " + mPhoneId);
ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_ALL);
}
@@ -500,15 +449,8 @@ public class IccCardProxy extends Handler implements IccCard {
log("setExternalState: set mPhoneId=" + mPhoneId + " mExternalState=" + mExternalState);
mTelephonyManager.setSimStateForPhone(mPhoneId, getState().toString());
- // For locked states, we should be sending internal broadcast.
- if (IccCardConstants.INTENT_VALUE_ICC_LOCKED.equals(
- getIccStateIntentString(mExternalState))) {
- broadcastInternalIccStateChangedIntent(getIccStateIntentString(mExternalState),
- getIccStateReason(mExternalState));
- } else {
- broadcastIccStateChangedIntent(getIccStateIntentString(mExternalState),
- getIccStateReason(mExternalState));
- }
+ broadcastInternalIccStateChangedIntent(getIccStateIntentString(mExternalState),
+ getIccStateReason(mExternalState));
}
}
@@ -566,6 +508,7 @@ public class IccCardProxy extends Handler implements IccCard {
case PERM_DISABLED: return IccCardConstants.INTENT_VALUE_ICC_LOCKED;
case CARD_IO_ERROR: return IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR;
case CARD_RESTRICTED: return IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED;
+ case LOADED: return IccCardConstants.INTENT_VALUE_ICC_LOADED;
default: return IccCardConstants.INTENT_VALUE_ICC_UNKNOWN;
}
}
@@ -601,16 +544,6 @@ public class IccCardProxy extends Handler implements IccCard {
}
}
- @Override
- public IccFileHandler getIccFileHandler() {
- synchronized (mLock) {
- if (mUiccApplication != null) {
- return mUiccApplication.getIccFileHandler();
- }
- return null;
- }
- }
-
/**
* Notifies handler of any transition into State.NETWORK_LOCKED
*/
@@ -723,11 +656,6 @@ public class IccCardProxy extends Handler implements IccCard {
}
}
- public boolean getIccFdnAvailable() {
- boolean retValue = mUiccApplication != null ? mUiccApplication.getIccFdnAvailable() : false;
- return retValue;
- }
-
public boolean getIccPin2Blocked() {
/* defaults to disabled */
Boolean retValue = mUiccApplication != null ? mUiccApplication.getIccPin2Blocked() : false;
diff --git a/com/android/internal/telephony/uicc/IccCardStatus.java b/com/android/internal/telephony/uicc/IccCardStatus.java
index 45e85c23..3998eb2b 100644
--- a/com/android/internal/telephony/uicc/IccCardStatus.java
+++ b/com/android/internal/telephony/uicc/IccCardStatus.java
@@ -64,7 +64,7 @@ public class IccCardStatus {
public int mGsmUmtsSubscriptionAppIndex;
public int mCdmaSubscriptionAppIndex;
public int mImsSubscriptionAppIndex;
- public int physicalSlotIndex;
+ public int physicalSlotIndex = UiccController.INVALID_SLOT_ID;
public String atr;
public String iccid;
diff --git a/com/android/internal/telephony/uicc/IccRecords.java b/com/android/internal/telephony/uicc/IccRecords.java
index b37467e3..a2985487 100644
--- a/com/android/internal/telephony/uicc/IccRecords.java
+++ b/com/android/internal/telephony/uicc/IccRecords.java
@@ -29,7 +29,6 @@ import android.telephony.TelephonyManager;
import android.text.TextUtils;
import com.android.internal.telephony.CommandsInterface;
-import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -48,6 +47,7 @@ public abstract class IccRecords extends Handler implements IccConstants {
// ***** Instance Variables
protected AtomicBoolean mDestroyed = new AtomicBoolean(false);
+ protected AtomicBoolean mLoaded = new AtomicBoolean(false);
protected Context mContext;
protected CommandsInterface mCi;
protected IccFileHandler mFh;
@@ -56,6 +56,7 @@ public abstract class IccRecords extends Handler implements IccConstants {
protected RegistrantList mRecordsLoadedRegistrants = new RegistrantList();
protected RegistrantList mLockedRecordsLoadedRegistrants = new RegistrantList();
+ protected RegistrantList mNetworkLockedRecordsLoadedRegistrants = new RegistrantList();
protected RegistrantList mImsiReadyRegistrants = new RegistrantList();
protected RegistrantList mRecordsEventsRegistrants = new RegistrantList();
protected RegistrantList mNewSmsRegistrants = new RegistrantList();
@@ -68,9 +69,15 @@ public abstract class IccRecords extends Handler implements IccConstants {
// ***** Cached SIM State; cleared on channel close
+ // SIM is not locked
+ protected static final int LOCKED_RECORDS_REQ_REASON_NONE = 0;
+ // Records requested for PIN or PUK locked SIM
+ protected static final int LOCKED_RECORDS_REQ_REASON_LOCKED = 1;
+ // Records requested for network locked SIM
+ protected static final int LOCKED_RECORDS_REQ_REASON_NETWORK_LOCKED = 2;
+
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 int mLockedRecordsReqReason = LOCKED_RECORDS_REQ_REASON_NONE;
protected String mIccId; // Includes only decimals (no hex)
protected String mFakeIccId;
@@ -139,6 +146,7 @@ public abstract class IccRecords extends Handler implements IccConstants {
public static final int EVENT_SPN = 2; // Service Provider Name
public static final int EVENT_GET_ICC_RECORD_DONE = 100;
+ public static final int EVENT_REFRESH = 31; // ICC refresh occurred
protected static final int EVENT_APP_READY = 1;
private static final int EVENT_AKA_AUTHENTICATE_DONE = 90;
@@ -160,7 +168,7 @@ public abstract class IccRecords extends Handler implements IccConstants {
+ " recordsToLoad=" + mRecordsToLoad
+ " adnCache=" + mAdnCache
+ " recordsRequested=" + mRecordsRequested
- + " lockedRecordsRequested=" + mLockedRecordsRequested
+ + " lockedRecordsReqReason=" + mLockedRecordsReqReason
+ " iccid=" + iccIdToPrint
+ (mCarrierTestOverride.isInTestMode() ? "mFakeIccid=" + mFakeIccId : "")
+ " msisdnTag=" + mMsisdnTag
@@ -224,6 +232,7 @@ public abstract class IccRecords extends Handler implements IccConstants {
mFakeIccId = mCarrierTestOverride.getFakeIccid();
log("load mFakeIccId: " + mFakeIccId);
}
+ mCi.registerForIccRefresh(this, EVENT_REFRESH, null);
}
/**
@@ -239,10 +248,15 @@ public abstract class IccRecords extends Handler implements IccConstants {
mLock.notifyAll();
}
+ mCi.unregisterForIccRefresh(this);
mParentApp = null;
mFh = null;
mCi = null;
mContext = null;
+ if (mAdnCache != null) {
+ mAdnCache.reset();
+ }
+ mLoaded.set(false);
}
public abstract void onReady();
@@ -313,7 +327,7 @@ public abstract class IccRecords extends Handler implements IccConstants {
}
/**
- * Register to be notified when records are loaded for a locked SIM
+ * Register to be notified when records are loaded for a PIN or PUK locked SIM
*/
public void registerForLockedRecordsLoaded(Handler h, int what, Object obj) {
if (mDestroyed.get()) {
@@ -335,6 +349,29 @@ public abstract class IccRecords extends Handler implements IccConstants {
mLockedRecordsLoadedRegistrants.remove(h);
}
+ /**
+ * Register to be notified when records are loaded for a network locked SIM
+ */
+ public void registerForNetworkLockedRecordsLoaded(Handler h, int what, Object obj) {
+ if (mDestroyed.get()) {
+ return;
+ }
+
+ Registrant r = new Registrant(h, what, obj);
+ mNetworkLockedRecordsLoadedRegistrants.add(r);
+
+ if (getNetworkLockedRecordsLoaded()) {
+ r.notifyRegistrant(new AsyncResult(null, null, null));
+ }
+ }
+
+ /**
+ * Unregister corresponding to registerForLockedRecordsLoaded()
+ */
+ public void unregisterForNetworkLockedRecordsLoaded(Handler h) {
+ mNetworkLockedRecordsLoadedRegistrants.remove(h);
+ }
+
public void registerForImsiReady(Handler h, int what, Object obj) {
if (mDestroyed.get()) {
return;
@@ -503,9 +540,9 @@ public abstract class IccRecords extends Handler implements IccConstants {
// which did occur after removing a SIM.
UiccCardApplication parentApp = mParentApp;
if (parentApp != null) {
- UiccCard card = parentApp.getUiccCard();
- if (card != null) {
- String brandOverride = card.getOperatorBrandOverride();
+ UiccProfile profile = parentApp.getUiccProfile();
+ if (profile != null) {
+ String brandOverride = profile.getOperatorBrandOverride();
if (brandOverride != null) {
log("getServiceProviderName: override, providerName=" + providerName);
providerName = brandOverride;
@@ -580,27 +617,18 @@ public abstract class IccRecords extends Handler implements IccConstants {
*/
public abstract void onRefresh(boolean fileChanged, int[] fileList);
- /**
- * Called by subclasses (SimRecords and RuimRecords) whenever
- * IccRefreshResponse.REFRESH_RESULT_INIT event received
- */
- protected void onIccRefreshInit() {
- mAdnCache.reset();
- mMncLength = UNINITIALIZED;
- UiccCardApplication parentApp = mParentApp;
- if ((parentApp != null) &&
- (parentApp.getState() == AppState.APPSTATE_READY)) {
- // This will cause files to be reread
- sendMessage(obtainMessage(EVENT_APP_READY));
- }
- }
-
public boolean getRecordsLoaded() {
return mRecordsToLoad == 0 && mRecordsRequested;
}
protected boolean getLockedRecordsLoaded() {
- return mRecordsToLoad == 0 && mLockedRecordsRequested;
+ return mRecordsToLoad == 0
+ && mLockedRecordsReqReason == LOCKED_RECORDS_REQ_REASON_LOCKED;
+ }
+
+ protected boolean getNetworkLockedRecordsLoaded() {
+ return mRecordsToLoad == 0
+ && mLockedRecordsReqReason == LOCKED_RECORDS_REQ_REASON_NETWORK_LOCKED;
}
//***** Overridden from Handler
@@ -629,6 +657,16 @@ public abstract class IccRecords extends Handler implements IccConstants {
}
break;
+ case EVENT_REFRESH:
+ ar = (AsyncResult)msg.obj;
+ if (DBG) log("Card REFRESH occurred: ");
+ if (ar.exception == null) {
+ handleRefresh((IccRefreshResponse)ar.result);
+ } else {
+ loge("Icc refresh Exception: " + ar.exception);
+ }
+ break;
+
case EVENT_AKA_AUTHENTICATE_DONE:
ar = (AsyncResult)msg.obj;
auth_rsp = null;
@@ -697,6 +735,32 @@ public abstract class IccRecords extends Handler implements IccConstants {
return null;
}
+ protected abstract void handleFileUpdate(int efid);
+
+ protected void handleRefresh(IccRefreshResponse refreshResponse){
+ if (refreshResponse == null) {
+ if (DBG) log("handleRefresh received without input");
+ return;
+ }
+
+ if (!TextUtils.isEmpty(refreshResponse.aid) &&
+ !refreshResponse.aid.equals(mParentApp.getAid())) {
+ // This is for different app. Ignore.
+ return;
+ }
+
+ switch (refreshResponse.refreshResult) {
+ case IccRefreshResponse.REFRESH_RESULT_FILE_UPDATE:
+ if (DBG) log("handleRefresh with SIM_FILE_UPDATED");
+ handleFileUpdate(refreshResponse.efId);
+ break;
+ default:
+ // unknown refresh operation
+ if (DBG) log("handleRefresh with unknown operation");
+ break;
+ }
+ }
+
protected abstract void onRecordLoaded();
protected abstract void onAllRecordsLoaded();
@@ -756,6 +820,15 @@ public abstract class IccRecords extends Handler implements IccConstants {
}
/**
+ * Indicates wether the ICC records have been loaded or not
+ *
+ * @return true if the records have been loaded, false otherwise.
+ */
+ public boolean isLoaded() {
+ return mLoaded.get();
+ }
+
+ /**
* Indicates wether SIM is in provisioned state or not.
* Overridden only if SIM can be dynamically provisioned via OTA.
*
@@ -864,6 +937,12 @@ public abstract class IccRecords extends Handler implements IccConstants {
pw.println(" mLockedRecordsLoadedRegistrants[" + i + "]="
+ ((Registrant) mLockedRecordsLoadedRegistrants.get(i)).getHandler());
}
+ pw.println(" mNetworkLockedRecordsLoadedRegistrants: size="
+ + mNetworkLockedRecordsLoadedRegistrants.size());
+ for (int i = 0; i < mNetworkLockedRecordsLoadedRegistrants.size(); i++) {
+ pw.println(" mLockedRecordsLoadedRegistrants[" + i + "]="
+ + ((Registrant) mNetworkLockedRecordsLoadedRegistrants.get(i)).getHandler());
+ }
pw.println(" mImsiReadyRegistrants: size=" + mImsiReadyRegistrants.size());
for (int i = 0; i < mImsiReadyRegistrants.size(); i++) {
pw.println(" mImsiReadyRegistrants[" + i + "]="
@@ -886,7 +965,7 @@ public abstract class IccRecords extends Handler implements IccConstants {
+ ((Registrant)mNetworkSelectionModeAutomaticRegistrants.get(i)).getHandler());
}
pw.println(" mRecordsRequested=" + mRecordsRequested);
- pw.println(" mLockedRecordsRequested=" + mLockedRecordsRequested);
+ pw.println(" mLockedRecordsReqReason=" + mLockedRecordsReqReason);
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
index f9e87c71..5fdd3225 100644
--- a/com/android/internal/telephony/uicc/IccSlotStatus.java
+++ b/com/android/internal/telephony/uicc/IccSlotStatus.java
@@ -17,6 +17,7 @@
package com.android.internal.telephony.uicc;
import android.telephony.SubscriptionInfo;
+import android.text.TextUtils;
/**
* This class represents the status of the physical UICC slots.
@@ -85,4 +86,21 @@ public class IccSlotStatus {
return sb.toString();
}
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+
+ IccSlotStatus that = (IccSlotStatus) obj;
+ return (cardState == that.cardState)
+ && (slotState == that.slotState)
+ && (logicalSlotIndex == that.logicalSlotIndex)
+ && (TextUtils.equals(atr, that.atr))
+ && (TextUtils.equals(iccid, that.iccid));
+ }
+
}
diff --git a/com/android/internal/telephony/uicc/IccUtils.java b/com/android/internal/telephony/uicc/IccUtils.java
index 9f8b3a82..c0954385 100644
--- a/com/android/internal/telephony/uicc/IccUtils.java
+++ b/com/android/internal/telephony/uicc/IccUtils.java
@@ -837,6 +837,13 @@ public class IccUtils {
}
/**
+ * Strip all the trailing 'F' characters of a string, e.g., an ICCID.
+ */
+ public static String stripTrailingFs(String s) {
+ return s == null ? null : s.replaceAll("(?i)f*$", "");
+ }
+
+ /**
* 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.
*/
diff --git a/com/android/internal/telephony/uicc/IsimUiccRecords.java b/com/android/internal/telephony/uicc/IsimUiccRecords.java
index cc9f3a32..93c49d5f 100644
--- a/com/android/internal/telephony/uicc/IsimUiccRecords.java
+++ b/com/android/internal/telephony/uicc/IsimUiccRecords.java
@@ -16,19 +16,12 @@
package com.android.internal.telephony.uicc;
-import static com.android.internal.telephony.uicc.IccConstants.EF_DOMAIN;
-import static com.android.internal.telephony.uicc.IccConstants.EF_IMPI;
-import static com.android.internal.telephony.uicc.IccConstants.EF_IMPU;
-import static com.android.internal.telephony.uicc.IccConstants.EF_IST;
-import static com.android.internal.telephony.uicc.IccConstants.EF_PCSCF;
-
import android.content.Context;
import android.content.Intent;
import android.os.AsyncResult;
import android.os.Message;
import android.telephony.Rlog;
import android.telephony.ServiceState;
-import android.text.TextUtils;
import com.android.internal.telephony.CommandsInterface;
import com.android.internal.telephony.gsm.SimTlv;
@@ -53,7 +46,6 @@ public class IsimUiccRecords extends IccRecords implements IsimRecords {
public static final String INTENT_ISIM_REFRESH = "com.android.intent.isim_refresh";
private static final int EVENT_APP_READY = 1;
- private static final int EVENT_ISIM_REFRESH = 31;
private static final int EVENT_ISIM_AUTHENTICATE_DONE = 91;
// ISIM EF records (see 3GPP TS 31.103)
@@ -84,13 +76,12 @@ public class IsimUiccRecords extends IccRecords implements IsimRecords {
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;
+ mLockedRecordsReqReason = LOCKED_RECORDS_REQ_REASON_NONE;
// recordsToLoad is set to 0 because no requests are made yet
mRecordsToLoad = 0;
// Start off by setting empty state
resetRecords();
- mCi.registerForIccRefresh(this, EVENT_ISIM_REFRESH, null);
mParentApp.registerForReady(this, EVENT_APP_READY, null);
if (DBG) log("IsimUiccRecords X ctor this=" + this);
@@ -123,15 +114,9 @@ public class IsimUiccRecords extends IccRecords implements IsimRecords {
onReady();
break;
- case EVENT_ISIM_REFRESH:
- ar = (AsyncResult)msg.obj;
- loge("ISim REFRESH(EVENT_ISIM_REFRESH) with exception: " + ar.exception);
- if (ar.exception == null) {
- Intent intent = new Intent(INTENT_ISIM_REFRESH);
- loge("send ISim REFRESH: " + INTENT_ISIM_REFRESH);
- mContext.sendBroadcast(intent);
- handleIsimRefresh((IccRefreshResponse)ar.result);
- }
+ case EVENT_REFRESH:
+ broadcastRefresh();
+ super.handleMessage(msg);
break;
case EVENT_ISIM_AUTHENTICATE_DONE:
@@ -199,7 +184,8 @@ public class IsimUiccRecords extends IccRecords implements IsimRecords {
auth_rsp = null;
mRecordsRequested = false;
- mLockedRecordsRequested = false;
+ mLockedRecordsReqReason = LOCKED_RECORDS_REQ_REASON_NONE;
+ mLoaded.set(false);
}
private class EfIsimImpiLoaded implements IccRecords.IccRecordLoaded {
@@ -297,7 +283,7 @@ public class IsimUiccRecords extends IccRecords implements IsimRecords {
if (getRecordsLoaded()) {
onAllRecordsLoaded();
- } else if (getLockedRecordsLoaded()) {
+ } else if (getLockedRecordsLoaded() || getNetworkLockedRecordsLoaded()) {
onLockedAllRecordsLoaded();
} else if (mRecordsToLoad < 0) {
loge("recordsToLoad <0, programmer error suspected");
@@ -307,16 +293,26 @@ public class IsimUiccRecords extends IccRecords implements IsimRecords {
private void onLockedAllRecordsLoaded() {
if (DBG) log("SIM locked; record load complete");
- mLockedRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
+ if (mLockedRecordsReqReason == LOCKED_RECORDS_REQ_REASON_LOCKED) {
+ mLockedRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
+ } else if (mLockedRecordsReqReason == LOCKED_RECORDS_REQ_REASON_NETWORK_LOCKED) {
+ mNetworkLockedRecordsLoadedRegistrants.notifyRegistrants(
+ new AsyncResult(null, null, null));
+ } else {
+ loge("onLockedAllRecordsLoaded: unexpected mLockedRecordsReqReason "
+ + mLockedRecordsReqReason);
+ }
}
@Override
protected void onAllRecordsLoaded() {
if (DBG) log("record load complete");
+ mLoaded.set(true);
mRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
}
- private void handleFileUpdate(int efid) {
+ @Override
+ protected void handleFileUpdate(int efid) {
switch (efid) {
case EF_IMPI:
mFh.loadEFTransparent(EF_IMPI, obtainMessage(
@@ -353,42 +349,10 @@ public class IsimUiccRecords extends IccRecords implements IsimRecords {
}
}
- private void handleIsimRefresh(IccRefreshResponse refreshResponse) {
- if (refreshResponse == null) {
- if (DBG) log("handleIsimRefresh received without input");
- return;
- }
-
- if (!TextUtils.isEmpty(refreshResponse.aid)
- && !refreshResponse.aid.equals(mParentApp.getAid())) {
- // This is for different app. Ignore.
- if (DBG) log("handleIsimRefresh received different app");
- return;
- }
-
- switch (refreshResponse.refreshResult) {
- case IccRefreshResponse.REFRESH_RESULT_FILE_UPDATE:
- if (DBG) log("handleIsimRefresh with REFRESH_RESULT_FILE_UPDATE");
- handleFileUpdate(refreshResponse.efId);
- break;
-
- case IccRefreshResponse.REFRESH_RESULT_INIT:
- if (DBG) log("handleIsimRefresh with REFRESH_RESULT_INIT");
- // need to reload all files (that we care about)
- // onIccRefreshInit();
- fetchIsimRecords();
- break;
-
- case IccRefreshResponse.REFRESH_RESULT_RESET:
- // Refresh reset is handled by the UiccCard object.
- if (DBG) log("handleIsimRefresh with REFRESH_RESULT_RESET");
- break;
-
- default:
- // unknown refresh operation
- if (DBG) log("handleIsimRefresh with unknown operation");
- break;
- }
+ private void broadcastRefresh() {
+ Intent intent = new Intent(INTENT_ISIM_REFRESH);
+ log("send ISim REFRESH: " + INTENT_ISIM_REFRESH);
+ mContext.sendBroadcast(intent);
}
/**
diff --git a/com/android/internal/telephony/uicc/RuimRecords.java b/com/android/internal/telephony/uicc/RuimRecords.java
index b57d8fcb..88ac071c 100644
--- a/com/android/internal/telephony/uicc/RuimRecords.java
+++ b/com/android/internal/telephony/uicc/RuimRecords.java
@@ -97,8 +97,8 @@ public class RuimRecords extends IccRecords {
private static final int EVENT_SMS_ON_RUIM = 21;
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;
+ private static final int EVENT_APP_NETWORK_LOCKED = 33;
public RuimRecords(UiccCardApplication app, Context c, CommandsInterface ci) {
super(app, c, ci);
@@ -106,19 +106,19 @@ public class RuimRecords extends IccRecords {
mAdnCache = new AdnRecordCache(mFh);
mRecordsRequested = false; // No load request is made till SIM ready
- mLockedRecordsRequested = false;
+ mLockedRecordsReqReason = LOCKED_RECORDS_REQ_REASON_NONE;
// recordsToLoad is set to 0 because no requests are made yet
mRecordsToLoad = 0;
// NOTE the EVENT_SMS_ON_RUIM is not registered
- mCi.registerForIccRefresh(this, EVENT_RUIM_REFRESH, null);
// Start off by setting empty state
resetRecords();
mParentApp.registerForReady(this, EVENT_APP_READY, null);
mParentApp.registerForLocked(this, EVENT_APP_LOCKED, null);
+ mParentApp.registerForNetworkLocked(this, EVENT_APP_NETWORK_LOCKED, null);
if (DBG) log("RuimRecords X ctor this=" + this);
}
@@ -126,8 +126,9 @@ public class RuimRecords extends IccRecords {
public void dispose() {
if (DBG) log("Disposing RuimRecords " + this);
//Unregister for all events
- mCi.unregisterForIccRefresh(this);
mParentApp.unregisterForReady(this);
+ mParentApp.unregisterForLocked(this);
+ mParentApp.unregisterForNetworkLocked(this);
resetRecords();
super.dispose();
}
@@ -155,7 +156,8 @@ 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;
+ mLockedRecordsReqReason = LOCKED_RECORDS_REQ_REASON_NONE;
+ mLoaded.set(false);
}
public String getMdnNumber() {
@@ -608,7 +610,8 @@ public class RuimRecords extends IccRecords {
break;
case EVENT_APP_LOCKED:
- onLocked();
+ case EVENT_APP_NETWORK_LOCKED:
+ onLocked(msg.what);
break;
case EVENT_GET_DEVICE_IDENTITY_DONE:
@@ -703,14 +706,6 @@ public class RuimRecords extends IccRecords {
log("Event EVENT_GET_SST_DONE Received");
break;
- case EVENT_RUIM_REFRESH:
- isRecordLoadResponse = false;
- ar = (AsyncResult)msg.obj;
- if (ar.exception == null) {
- handleRuimRefresh((IccRefreshResponse)ar.result);
- }
- break;
-
default:
super.handleMessage(msg); // IccRecords handles generic record load responses
@@ -756,7 +751,7 @@ public class RuimRecords extends IccRecords {
if (getRecordsLoaded()) {
onAllRecordsLoaded();
- } else if (getLockedRecordsLoaded()) {
+ } else if (getLockedRecordsLoaded() || getNetworkLockedRecordsLoaded()) {
onLockedAllRecordsLoaded();
} else if (mRecordsToLoad < 0) {
loge("recordsToLoad <0, programmer error suspected");
@@ -765,7 +760,15 @@ public class RuimRecords extends IccRecords {
}
private void onLockedAllRecordsLoaded() {
- mLockedRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
+ if (mLockedRecordsReqReason == LOCKED_RECORDS_REQ_REASON_LOCKED) {
+ mLockedRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
+ } else if (mLockedRecordsReqReason == LOCKED_RECORDS_REQ_REASON_NETWORK_LOCKED) {
+ mNetworkLockedRecordsLoadedRegistrants.notifyRegistrants(
+ new AsyncResult(null, null, null));
+ } else {
+ loge("onLockedAllRecordsLoaded: unexpected mLockedRecordsReqReason "
+ + mLockedRecordsReqReason);
+ }
}
@Override
@@ -805,11 +808,12 @@ public class RuimRecords extends IccRecords {
setSimLanguage(mEFli, mEFpl);
}
+ mLoaded.set(true);
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)) {
- int phoneId = mParentApp.getUiccCard().getPhoneId();
+ int phoneId = mParentApp.getUiccProfile().getPhoneId();
int subId = SubscriptionController.getInstance().getSubIdUsingPhoneId(phoneId);
if (SubscriptionManager.isValidSubscriptionId(subId)) {
SubscriptionManager.from(mContext).setDisplayNumber(mMdn, subId);
@@ -826,9 +830,10 @@ public class RuimRecords extends IccRecords {
mCi.getCDMASubscription(obtainMessage(EVENT_GET_CDMA_SUBSCRIPTION_DONE));
}
- private void onLocked() {
+ private void onLocked(int msg) {
if (DBG) log("only fetch EF_ICCID in locked state");
- mLockedRecordsRequested = true;
+ mLockedRecordsReqReason = msg == EVENT_APP_LOCKED ? LOCKED_RECORDS_REQ_REASON_LOCKED :
+ LOCKED_RECORDS_REQ_REASON_NETWORK_LOCKED;
mFh.loadEFTransparent(EF_ICCID, obtainMessage(EVENT_GET_ICCID_DONE));
mRecordsToLoad++;
@@ -933,38 +938,10 @@ public class RuimRecords extends IccRecords {
return 0;
}
- private void handleRuimRefresh(IccRefreshResponse refreshResponse) {
- if (refreshResponse == null) {
- if (DBG) log("handleRuimRefresh received without input");
- return;
- }
-
- if (!TextUtils.isEmpty(refreshResponse.aid)
- && !refreshResponse.aid.equals(mParentApp.getAid())) {
- // This is for different app. Ignore.
- return;
- }
-
- switch (refreshResponse.refreshResult) {
- case IccRefreshResponse.REFRESH_RESULT_FILE_UPDATE:
- if (DBG) log("handleRuimRefresh with SIM_REFRESH_FILE_UPDATED");
- mAdnCache.reset();
- fetchRuimRecords();
- break;
- case IccRefreshResponse.REFRESH_RESULT_INIT:
- if (DBG) log("handleRuimRefresh with SIM_REFRESH_INIT");
- // need to reload all files (that we care about)
- onIccRefreshInit();
- break;
- case IccRefreshResponse.REFRESH_RESULT_RESET:
- // Refresh reset is handled by the UiccCard object.
- if (DBG) log("handleRuimRefresh with SIM_REFRESH_RESET");
- break;
- default:
- // unknown refresh operation
- if (DBG) log("handleRuimRefresh with unknown operation");
- break;
- }
+ @Override
+ protected void handleFileUpdate(int efid) {
+ mAdnCache.reset();
+ fetchRuimRecords();
}
public String getMdn() {
diff --git a/com/android/internal/telephony/uicc/SIMRecords.java b/com/android/internal/telephony/uicc/SIMRecords.java
index 4c8f4eeb..67c09000 100644
--- a/com/android/internal/telephony/uicc/SIMRecords.java
+++ b/com/android/internal/telephony/uicc/SIMRecords.java
@@ -174,7 +174,8 @@ public class SIMRecords extends IccRecords {
private static final int SYSTEM_EVENT_BASE = 0x100;
private static final int EVENT_CARRIER_CONFIG_CHANGED = 1 + SYSTEM_EVENT_BASE;
private static final int EVENT_APP_LOCKED = 2 + SYSTEM_EVENT_BASE;
- private static final int EVENT_SIM_REFRESH = 3 + SYSTEM_EVENT_BASE;
+ private static final int EVENT_APP_NETWORK_LOCKED = 3 + SYSTEM_EVENT_BASE;
+
// Lookup table for carriers known to produce SIMs which incorrectly indicate MNC length.
@@ -211,18 +212,18 @@ public class SIMRecords extends IccRecords {
mVmConfig = new VoiceMailConstants();
mRecordsRequested = false; // No load request is made till SIM ready
- mLockedRecordsRequested = false;
+ mLockedRecordsReqReason = LOCKED_RECORDS_REQ_REASON_NONE;
// recordsToLoad is set to 0 because no requests are made yet
mRecordsToLoad = 0;
mCi.setOnSmsOnSim(this, EVENT_SMS_ON_SIM, null);
- mCi.registerForIccRefresh(this, EVENT_SIM_REFRESH, null);
// Start off by setting empty state
resetRecords();
mParentApp.registerForReady(this, EVENT_APP_READY, null);
mParentApp.registerForLocked(this, EVENT_APP_LOCKED, null);
+ mParentApp.registerForNetworkLocked(this, EVENT_APP_NETWORK_LOCKED, null);
if (DBG) log("SIMRecords X ctor this=" + this);
IntentFilter intentfilter = new IntentFilter();
@@ -243,10 +244,10 @@ public class SIMRecords extends IccRecords {
public void dispose() {
if (DBG) log("Disposing SIMRecords this=" + this);
//Unregister for all events
- mCi.unregisterForIccRefresh(this);
mCi.unSetOnSmsOnSim(this);
mParentApp.unregisterForReady(this);
mParentApp.unregisterForLocked(this);
+ mParentApp.unregisterForNetworkLocked(this);
mContext.unregisterReceiver(mReceiver);
resetRecords();
super.dispose();
@@ -291,7 +292,8 @@ 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;
+ mLockedRecordsReqReason = LOCKED_RECORDS_REQ_REASON_NONE;
+ mLoaded.set(false);
}
//***** Public Methods
@@ -666,7 +668,8 @@ public class SIMRecords extends IccRecords {
break;
case EVENT_APP_LOCKED:
- onLocked();
+ case EVENT_APP_NETWORK_LOCKED:
+ onLocked(msg.what);
break;
/* IO events */
@@ -1213,14 +1216,6 @@ public class SIMRecords extends IccRecords {
((Message) ar.userObj).sendToTarget();
}
break;
- case EVENT_SIM_REFRESH:
- isRecordLoadResponse = false;
- ar = (AsyncResult) msg.obj;
- if (DBG) log("Sim REFRESH with exception: " + ar.exception);
- if (ar.exception == null) {
- handleSimRefresh((IccRefreshResponse) ar.result);
- }
- break;
case EVENT_GET_CFIS_DONE:
isRecordLoadResponse = true;
@@ -1410,7 +1405,8 @@ public class SIMRecords extends IccRecords {
}
}
- private void handleFileUpdate(int efid) {
+ @Override
+ protected void handleFileUpdate(int efid) {
switch(efid) {
case EF_MBDN:
mRecordsToLoad++;
@@ -1454,39 +1450,6 @@ public class SIMRecords extends IccRecords {
}
}
- private void handleSimRefresh(IccRefreshResponse refreshResponse){
- if (refreshResponse == null) {
- if (DBG) log("handleSimRefresh received without input");
- return;
- }
-
- if (!TextUtils.isEmpty(refreshResponse.aid)
- && !refreshResponse.aid.equals(mParentApp.getAid())) {
- // This is for different app. Ignore.
- return;
- }
-
- switch (refreshResponse.refreshResult) {
- case IccRefreshResponse.REFRESH_RESULT_FILE_UPDATE:
- if (DBG) log("handleSimRefresh with SIM_FILE_UPDATED");
- handleFileUpdate(refreshResponse.efId);
- break;
- case IccRefreshResponse.REFRESH_RESULT_INIT:
- if (DBG) log("handleSimRefresh with SIM_REFRESH_INIT");
- // need to reload all files (that we care about)
- onIccRefreshInit();
- break;
- case IccRefreshResponse.REFRESH_RESULT_RESET:
- // Refresh reset is handled by the UiccCard object.
- if (DBG) log("handleSimRefresh with SIM_REFRESH_RESET");
- break;
- default:
- // unknown refresh operation
- if (DBG) log("handleSimRefresh with unknown operation");
- break;
- }
- }
-
/**
* Dispatch 3GPP format message to registrant ({@code GsmCdmaPhone}) to pass to the 3GPP SMS
* dispatcher for delivery.
@@ -1561,7 +1524,7 @@ public class SIMRecords extends IccRecords {
if (getRecordsLoaded()) {
onAllRecordsLoaded();
- } else if (getLockedRecordsLoaded()) {
+ } else if (getLockedRecordsLoaded() || getNetworkLockedRecordsLoaded()) {
onLockedAllRecordsLoaded();
} else if (mRecordsToLoad < 0) {
loge("recordsToLoad <0, programmer error suspected");
@@ -1596,7 +1559,15 @@ public class SIMRecords extends IccRecords {
private void onLockedAllRecordsLoaded() {
setSimLanguageFromEF();
- mLockedRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
+ if (mLockedRecordsReqReason == LOCKED_RECORDS_REQ_REASON_LOCKED) {
+ mLockedRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
+ } else if (mLockedRecordsReqReason == LOCKED_RECORDS_REQ_REASON_NETWORK_LOCKED) {
+ mNetworkLockedRecordsLoadedRegistrants.notifyRegistrants(
+ new AsyncResult(null, null, null));
+ } else {
+ loge("onLockedAllRecordsLoaded: unexpected mLockedRecordsReqReason "
+ + mLockedRecordsReqReason);
+ }
}
@Override
@@ -1630,7 +1601,7 @@ public class SIMRecords extends IccRecords {
}
setVoiceMailByCountry(operator);
-
+ mLoaded.set(true);
mRecordsLoadedRegistrants.notifyRegistrants(new AsyncResult(null, null, null));
}
@@ -1716,9 +1687,10 @@ public class SIMRecords extends IccRecords {
fetchSimRecords();
}
- private void onLocked() {
+ private void onLocked(int msg) {
if (DBG) log("only fetch EF_LI, EF_PL and EF_ICCID in locked state");
- mLockedRecordsRequested = true;
+ mLockedRecordsReqReason = msg == EVENT_APP_LOCKED ? LOCKED_RECORDS_REQ_REASON_LOCKED :
+ LOCKED_RECORDS_REQ_REASON_NETWORK_LOCKED;
loadEfLiAndEfPl();
@@ -1868,8 +1840,8 @@ public class SIMRecords extends IccRecords {
public int getDisplayRule(ServiceState serviceState) {
int rule;
- if (mParentApp != null && mParentApp.getUiccCard() != null &&
- mParentApp.getUiccCard().getOperatorBrandOverride() != null) {
+ if (mParentApp != null && mParentApp.getUiccProfile() != null
+ && mParentApp.getUiccProfile().getOperatorBrandOverride() != null) {
// If the operator has been overridden, treat it as the SPN file on the SIM did not exist.
rule = SPN_RULE_SHOW_PLMN;
} else if (TextUtils.isEmpty(getServiceProviderName()) || mSpnDisplayCondition == -1) {
diff --git a/com/android/internal/telephony/uicc/UiccCard.java b/com/android/internal/telephony/uicc/UiccCard.java
index 2d5e89ea..a664a082 100644
--- a/com/android/internal/telephony/uicc/UiccCard.java
+++ b/com/android/internal/telephony/uicc/UiccCard.java
@@ -16,45 +16,25 @@
package com.android.internal.telephony.uicc;
-import android.app.AlertDialog;
-import android.app.usage.UsageStatsManager;
import android.content.Context;
-import android.content.DialogInterface;
import android.content.Intent;
-import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
-import android.content.res.Resources;
-import android.net.Uri;
-import android.os.AsyncResult;
-import android.os.Binder;
import android.os.Handler;
import android.os.Message;
-import android.os.PersistableBundle;
-import android.os.Registrant;
-import android.os.RegistrantList;
-import android.preference.PreferenceManager;
-import android.provider.Settings;
-import android.telephony.CarrierConfigManager;
import android.telephony.Rlog;
import android.telephony.TelephonyManager;
-import android.text.TextUtils;
import android.util.LocalLog;
-import android.view.WindowManager;
-import com.android.internal.R;
import com.android.internal.telephony.CommandsInterface;
-import com.android.internal.telephony.SubscriptionController;
-import com.android.internal.telephony.cat.CatService;
+import com.android.internal.telephony.TelephonyComponentFactory;
import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
import com.android.internal.telephony.uicc.IccCardStatus.CardState;
import com.android.internal.telephony.uicc.IccCardStatus.PinState;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.util.Arrays;
-import java.util.HashSet;
import java.util.List;
/**
@@ -71,28 +51,12 @@ public class UiccCard {
private final Object mLock = new Object();
private CardState mCardState;
- private PinState mUniversalPinState;
- private int mGsmUmtsSubscriptionAppIndex;
- private int mCdmaSubscriptionAppIndex;
- private int mImsSubscriptionAppIndex;
- private UiccCardApplication[] mUiccApplications =
- new UiccCardApplication[IccCardStatus.CARD_MAX_APPS];
+ private String mIccid;
+ protected String mCardId;
+ private UiccProfile mUiccProfile;
private Context mContext;
private CommandsInterface mCi;
- private CatService mCatService;
- private UiccCarrierPrivilegeRules mCarrierPrivilegeRules;
-
- private RegistrantList mCarrierPrivilegeRegistrants = new RegistrantList();
-
- 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;
- private static final int EVENT_TRANSMIT_APDU_BASIC_CHANNEL_DONE = 18;
- private static final int EVENT_SIM_IO_DONE = 19;
- private static final int EVENT_CARRIER_PRIVILEGES_LOADED = 20;
-
private static final LocalLog mLocalLog = new LocalLog(100);
-
private final int mPhoneId;
public UiccCard(Context c, CommandsInterface ci, IccCardStatus ics, int phoneId) {
@@ -105,77 +69,31 @@ public class UiccCard {
public void dispose() {
synchronized (mLock) {
if (DBG) log("Disposing card");
- if (mCatService != null) mCatService.dispose();
- for (UiccCardApplication app : mUiccApplications) {
- if (app != null) {
- app.dispose();
- }
+ if (mUiccProfile != null) {
+ mUiccProfile.dispose();
}
- mCatService = null;
- mUiccApplications = null;
- mCarrierPrivilegeRules = null;
+ mUiccProfile = null;
}
}
public void update(Context c, CommandsInterface ci, IccCardStatus ics) {
synchronized (mLock) {
- CardState oldState = mCardState;
mCardState = ics.mCardState;
- mUniversalPinState = ics.mUniversalPinState;
- mGsmUmtsSubscriptionAppIndex = ics.mGsmUmtsSubscriptionAppIndex;
- mCdmaSubscriptionAppIndex = ics.mCdmaSubscriptionAppIndex;
- mImsSubscriptionAppIndex = ics.mImsSubscriptionAppIndex;
mContext = c;
mCi = ci;
+ mIccid = ics.iccid;
+ updateCardId();
- //update applications
- if (DBG) log(ics.mApplications.length + " applications");
- for ( int i = 0; i < mUiccApplications.length; i++) {
- if (mUiccApplications[i] == null) {
- //Create newly added Applications
- if (i < ics.mApplications.length) {
- mUiccApplications[i] = new UiccCardApplication(this,
- ics.mApplications[i], mContext, mCi);
- }
- } else if (i >= ics.mApplications.length) {
- //Delete removed applications
- mUiccApplications[i].dispose();
- mUiccApplications[i] = null;
+ if (mCardState != CardState.CARDSTATE_ABSENT) {
+ if (mUiccProfile == null) {
+ mUiccProfile = TelephonyComponentFactory.getInstance().makeUiccProfile(
+ mContext, mCi, ics, mPhoneId, this);
} else {
- //Update the rest
- mUiccApplications[i].update(ics.mApplications[i], mContext, mCi);
+ mUiccProfile.update(mContext, mCi, ics);
}
- }
-
- createAndUpdateCatServiceLocked();
-
- // Reload the carrier privilege rules if necessary.
- log("Before privilege rules: " + mCarrierPrivilegeRules + " : " + mCardState);
- if (mCarrierPrivilegeRules == null && mCardState == CardState.CARDSTATE_PRESENT) {
- mCarrierPrivilegeRules = new UiccCarrierPrivilegeRules(this,
- mHandler.obtainMessage(EVENT_CARRIER_PRIVILEGES_LOADED));
- } else if (mCarrierPrivilegeRules != null
- && mCardState != CardState.CARDSTATE_PRESENT) {
- mCarrierPrivilegeRules = null;
- }
-
- sanitizeApplicationIndexesLocked();
- }
- }
-
- private void createAndUpdateCatServiceLocked() {
- if (mUiccApplications.length > 0 && mUiccApplications[0] != null) {
- // Initialize or Reinitialize CatService
- if (mCatService == null) {
- mCatService = CatService.getInstance(mCi, mContext, this, mPhoneId);
} else {
- mCatService.update(mCi, mContext, this);
- }
- } else {
- if (mCatService != null) {
- mCatService.dispose();
+ throw new RuntimeException("Card state is absent when updating!");
}
- mCatService = null;
}
}
@@ -185,176 +103,59 @@ public class UiccCard {
}
/**
- * This function makes sure that application indexes are valid
- * and resets invalid indexes. (This should never happen, but in case
- * RIL misbehaves we need to manage situation gracefully)
+ * Updates the ID of the SIM card.
+ *
+ * <p>Whenever the {@link UiccCard#update(Context, CommandsInterface, IccCardStatus)} is called,
+ * this function needs to be called to update the card ID. Subclasses of {@link UiccCard}
+ * could override this function to set the {@link UiccCard#mCardId} to be something else instead
+ * of {@link UiccCard#mIccid}.</p>
*/
- private void sanitizeApplicationIndexesLocked() {
- mGsmUmtsSubscriptionAppIndex =
- checkIndexLocked(
- mGsmUmtsSubscriptionAppIndex, AppType.APPTYPE_SIM, AppType.APPTYPE_USIM);
- mCdmaSubscriptionAppIndex =
- checkIndexLocked(
- mCdmaSubscriptionAppIndex, AppType.APPTYPE_RUIM, AppType.APPTYPE_CSIM);
- mImsSubscriptionAppIndex =
- checkIndexLocked(mImsSubscriptionAppIndex, AppType.APPTYPE_ISIM, null);
- }
-
- private int checkIndexLocked(int index, AppType expectedAppType, AppType altExpectedAppType) {
- if (mUiccApplications == null || index >= mUiccApplications.length) {
- loge("App index " + index + " is invalid since there are no applications");
- return -1;
- }
-
- if (index < 0) {
- // This is normal. (i.e. no application of this type)
- return -1;
- }
-
- if (mUiccApplications[index].getType() != expectedAppType &&
- mUiccApplications[index].getType() != altExpectedAppType) {
- loge("App index " + index + " is invalid since it's not " +
- expectedAppType + " and not " + altExpectedAppType);
- return -1;
- }
-
- // Seems to be valid
- return index;
+ protected void updateCardId() {
+ mCardId = mIccid;
}
/**
* Notifies handler when carrier privilege rules are loaded.
+ * @deprecated Please use
+ * {@link UiccProfile#registerForCarrierPrivilegeRulesLoaded(Handler, int, Object)} instead.
*/
+ @Deprecated
public void registerForCarrierPrivilegeRulesLoaded(Handler h, int what, Object obj) {
synchronized (mLock) {
- Registrant r = new Registrant (h, what, obj);
-
- mCarrierPrivilegeRegistrants.add(r);
-
- if (areCarrierPriviligeRulesLoaded()) {
- r.notifyRegistrant();
+ if (mUiccProfile != null) {
+ mUiccProfile.registerForCarrierPrivilegeRulesLoaded(h, what, obj);
+ } else {
+ loge("registerForCarrierPrivilegeRulesLoaded Failed!");
}
}
}
+ /**
+ * @deprecated Please use
+ * {@link UiccProfile#unregisterForCarrierPrivilegeRulesLoaded(Handler)} instead.
+ */
+ @Deprecated
public void unregisterForCarrierPrivilegeRulesLoaded(Handler h) {
synchronized (mLock) {
- mCarrierPrivilegeRegistrants.remove(h);
- }
- }
-
- protected Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message msg){
- switch (msg.what) {
- case EVENT_OPEN_LOGICAL_CHANNEL_DONE:
- case EVENT_CLOSE_LOGICAL_CHANNEL_DONE:
- case EVENT_TRANSMIT_APDU_LOGICAL_CHANNEL_DONE:
- case EVENT_TRANSMIT_APDU_BASIC_CHANNEL_DONE:
- case EVENT_SIM_IO_DONE:
- AsyncResult ar = (AsyncResult)msg.obj;
- if (ar.exception != null) {
- loglocal("Exception: " + ar.exception);
- log("Error in SIM access with exception" + ar.exception);
- }
- AsyncResult.forMessage((Message)ar.userObj, ar.result, ar.exception);
- ((Message)ar.userObj).sendToTarget();
- break;
- case EVENT_CARRIER_PRIVILEGES_LOADED:
- onCarrierPriviligesLoadedMessage();
- break;
- default:
- loge("Unknown Event " + msg.what);
- }
- }
- };
-
- private boolean isPackageInstalled(String pkgName) {
- PackageManager pm = mContext.getPackageManager();
- try {
- pm.getPackageInfo(pkgName, PackageManager.GET_ACTIVITIES);
- if (DBG) log(pkgName + " is installed.");
- return true;
- } catch (PackageManager.NameNotFoundException e) {
- if (DBG) log(pkgName + " is not installed.");
- return false;
- }
- }
-
- private class ClickListener implements DialogInterface.OnClickListener {
- String pkgName;
- public ClickListener(String pkgName) {
- this.pkgName = pkgName;
- }
- @Override
- public void onClick(DialogInterface dialog, int which) {
- synchronized (mLock) {
- if (which == DialogInterface.BUTTON_POSITIVE) {
- Intent market = new Intent(Intent.ACTION_VIEW);
- market.setData(Uri.parse("market://details?id=" + pkgName));
- market.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- mContext.startActivity(market);
- } else if (which == DialogInterface.BUTTON_NEGATIVE) {
- if (DBG) log("Not now clicked for carrier app dialog.");
- }
- }
- }
- }
-
- private void promptInstallCarrierApp(String pkgName) {
- DialogInterface.OnClickListener listener = new ClickListener(pkgName);
-
- Resources r = Resources.getSystem();
- String message = r.getString(R.string.carrier_app_dialog_message);
- String buttonTxt = r.getString(R.string.carrier_app_dialog_button);
- String notNowTxt = r.getString(R.string.carrier_app_dialog_not_now);
-
- AlertDialog dialog = new AlertDialog.Builder(mContext)
- .setMessage(message)
- .setNegativeButton(notNowTxt, listener)
- .setPositiveButton(buttonTxt, listener)
- .create();
- dialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT);
- dialog.show();
- }
-
- private void onCarrierPriviligesLoadedMessage() {
- UsageStatsManager usm = (UsageStatsManager) mContext.getSystemService(
- Context.USAGE_STATS_SERVICE);
- if (usm != null) {
- usm.onCarrierPrivilegedAppsChanged();
- }
- synchronized (mLock) {
- mCarrierPrivilegeRegistrants.notifyRegistrants();
- String whitelistSetting = Settings.Global.getString(mContext.getContentResolver(),
- Settings.Global.CARRIER_APP_WHITELIST);
- if (TextUtils.isEmpty(whitelistSetting)) {
- return;
- }
- HashSet<String> carrierAppSet = new HashSet<String>(
- Arrays.asList(whitelistSetting.split("\\s*;\\s*")));
- if (carrierAppSet.isEmpty()) {
- return;
- }
-
- List<String> pkgNames = mCarrierPrivilegeRules.getPackageNames();
- for (String pkgName : pkgNames) {
- if (!TextUtils.isEmpty(pkgName) && carrierAppSet.contains(pkgName)
- && !isPackageInstalled(pkgName)) {
- promptInstallCarrierApp(pkgName);
- }
+ if (mUiccProfile != null) {
+ mUiccProfile.unregisterForCarrierPrivilegeRulesLoaded(h);
+ } else {
+ loge("unregisterForCarrierPrivilegeRulesLoaded Failed!");
}
}
}
+ /**
+ * @deprecated Please use {@link UiccProfile#isApplicationOnIcc(AppType)} instead.
+ */
+ @Deprecated
public boolean isApplicationOnIcc(IccCardApplicationStatus.AppType type) {
synchronized (mLock) {
- for (int i = 0 ; i < mUiccApplications.length; i++) {
- if (mUiccApplications[i] != null && mUiccApplications[i].getType() == type) {
- return true;
- }
+ if (mUiccProfile != null) {
+ return mUiccProfile.isApplicationOnIcc(type);
+ } else {
+ return false;
}
- return false;
}
}
@@ -364,39 +165,45 @@ public class UiccCard {
}
}
+ /**
+ * @deprecated Please use {@link UiccProfile#getUniversalPinState()} instead.
+ */
+ @Deprecated
public PinState getUniversalPinState() {
synchronized (mLock) {
- return mUniversalPinState;
+ if (mUiccProfile != null) {
+ return mUiccProfile.getUniversalPinState();
+ } else {
+ return PinState.PINSTATE_UNKNOWN;
+ }
}
}
+ /**
+ * @deprecated Please use {@link UiccProfile#getApplication(int)} instead.
+ */
+ @Deprecated
public UiccCardApplication getApplication(int family) {
synchronized (mLock) {
- int index = IccCardStatus.CARD_MAX_APPS;
- switch (family) {
- case UiccController.APP_FAM_3GPP:
- index = mGsmUmtsSubscriptionAppIndex;
- break;
- case UiccController.APP_FAM_3GPP2:
- index = mCdmaSubscriptionAppIndex;
- break;
- case UiccController.APP_FAM_IMS:
- index = mImsSubscriptionAppIndex;
- break;
- }
- if (index >= 0 && index < mUiccApplications.length) {
- return mUiccApplications[index];
+ if (mUiccProfile != null) {
+ return mUiccProfile.getApplication(family);
+ } else {
+ return null;
}
- return null;
}
}
+ /**
+ * @deprecated Please use {@link UiccProfile#getApplicationIndex(int)} instead.
+ */
+ @Deprecated
public UiccCardApplication getApplicationIndex(int index) {
synchronized (mLock) {
- if (index >= 0 && index < mUiccApplications.length) {
- return mUiccApplications[index];
+ if (mUiccProfile != null) {
+ return mUiccProfile.getApplicationIndex(index);
+ } else {
+ return null;
}
- return null;
}
}
@@ -405,16 +212,17 @@ public class UiccCard {
*
* @param type ICC application type (@see com.android.internal.telephony.PhoneConstants#APPTYPE_xxx)
* @return application corresponding to type or a null if no match found
+ *
+ * @deprecated Please use {@link UiccProfile#getApplicationByType(int)} instead.
*/
+ @Deprecated
public UiccCardApplication getApplicationByType(int type) {
synchronized (mLock) {
- for (int i = 0 ; i < mUiccApplications.length; i++) {
- if (mUiccApplications[i] != null &&
- mUiccApplications[i].getType().ordinal() == type) {
- return mUiccApplications[i];
- }
+ if (mUiccProfile != null) {
+ return mUiccProfile.getApplicationByType(type);
+ } else {
+ return null;
}
- return null;
}
}
@@ -422,232 +230,272 @@ public class UiccCard {
* Resets the application with the input AID. Returns true if any changes were made.
*
* A null aid implies a card level reset - all applications must be reset.
+ *
+ * @deprecated Please use {@link UiccProfile#resetAppWithAid(String)} instead.
*/
+ @Deprecated
public boolean resetAppWithAid(String aid) {
synchronized (mLock) {
- boolean changed = false;
- for (int i = 0; i < mUiccApplications.length; i++) {
- if (mUiccApplications[i] != null
- && (TextUtils.isEmpty(aid) || aid.equals(mUiccApplications[i].getAid()))) {
- // Delete removed applications
- mUiccApplications[i].dispose();
- mUiccApplications[i] = null;
- changed = true;
- }
- }
- if (TextUtils.isEmpty(aid)) {
- if (mCarrierPrivilegeRules != null) {
- mCarrierPrivilegeRules = null;
- changed = true;
- }
- if (mCatService != null) {
- mCatService.dispose();
- mCatService = null;
- changed = true;
- }
+ if (mUiccProfile != null) {
+ return mUiccProfile.resetAppWithAid(aid);
+ } else {
+ return false;
}
- return changed;
}
}
/**
* Exposes {@link CommandsInterface#iccOpenLogicalChannel}
+ * @deprecated Please use
+ * {@link UiccProfile#iccOpenLogicalChannel(String, int, Message)} instead.
*/
+ @Deprecated
public void iccOpenLogicalChannel(String AID, int p2, Message response) {
- loglocal("Open Logical Channel: " + AID + " , " + p2 + " by pid:" + Binder.getCallingPid()
- + " uid:" + Binder.getCallingUid());
- mCi.iccOpenLogicalChannel(AID, p2,
- mHandler.obtainMessage(EVENT_OPEN_LOGICAL_CHANNEL_DONE, response));
+ if (mUiccProfile != null) {
+ mUiccProfile.iccOpenLogicalChannel(AID, p2, response);
+ } else {
+ loge("iccOpenLogicalChannel Failed!");
+ }
}
/**
* Exposes {@link CommandsInterface#iccCloseLogicalChannel}
+ * @deprecated Please use
+ * {@link UiccProfile#iccCloseLogicalChannel(int, Message)} instead.
*/
+ @Deprecated
public void iccCloseLogicalChannel(int channel, Message response) {
- loglocal("Close Logical Channel: " + channel);
- mCi.iccCloseLogicalChannel(channel,
- mHandler.obtainMessage(EVENT_CLOSE_LOGICAL_CHANNEL_DONE, response));
+ if (mUiccProfile != null) {
+ mUiccProfile.iccCloseLogicalChannel(channel, response);
+ } else {
+ loge("iccCloseLogicalChannel Failed!");
+ }
}
/**
* Exposes {@link CommandsInterface#iccTransmitApduLogicalChannel}
+ * @deprecated Please use {@link
+ * UiccProfile#iccTransmitApduLogicalChannel(int, int, int, int, int, int, String, Message)}
+ * instead.
*/
+ @Deprecated
public void iccTransmitApduLogicalChannel(int channel, int cla, int command,
int p1, int p2, int p3, String data, Message response) {
- mCi.iccTransmitApduLogicalChannel(channel, cla, command, p1, p2, p3,
- data, mHandler.obtainMessage(EVENT_TRANSMIT_APDU_LOGICAL_CHANNEL_DONE, response));
+ if (mUiccProfile != null) {
+ mUiccProfile.iccTransmitApduLogicalChannel(channel, cla, command, p1, p2, p3,
+ data, response);
+ } else {
+ loge("iccTransmitApduLogicalChannel Failed!");
+ }
}
/**
* Exposes {@link CommandsInterface#iccTransmitApduBasicChannel}
+ * @deprecated Please use
+ * {@link UiccProfile#iccTransmitApduBasicChannel(int, int, int, int, int, String, Message)}
+ * instead.
*/
+ @Deprecated
public void iccTransmitApduBasicChannel(int cla, int command,
int p1, int p2, int p3, String data, Message response) {
- mCi.iccTransmitApduBasicChannel(cla, command, p1, p2, p3,
- data, mHandler.obtainMessage(EVENT_TRANSMIT_APDU_BASIC_CHANNEL_DONE, response));
+ if (mUiccProfile != null) {
+ mUiccProfile.iccTransmitApduBasicChannel(cla, command, p1, p2, p3, data, response);
+ } else {
+ loge("iccTransmitApduBasicChannel Failed!");
+ }
}
/**
* Exposes {@link CommandsInterface#iccIO}
+ * @deprecated Please use
+ * {@link UiccProfile#iccExchangeSimIO(int, int, int, int, int, String, Message)} instead.
*/
+ @Deprecated
public void iccExchangeSimIO(int fileID, int command, int p1, int p2, int p3,
String pathID, Message response) {
- mCi.iccIO(command, fileID, pathID, p1, p2, p3, null, null,
- mHandler.obtainMessage(EVENT_SIM_IO_DONE, response));
+ if (mUiccProfile != null) {
+ mUiccProfile.iccExchangeSimIO(fileID, command, p1, p2, p3, pathID, response);
+ } else {
+ loge("iccExchangeSimIO Failed!");
+ }
}
/**
* Exposes {@link CommandsInterface#sendEnvelopeWithStatus}
+ * @deprecated Please use {@link UiccProfile#sendEnvelopeWithStatus(String, Message)} instead.
*/
+ @Deprecated
public void sendEnvelopeWithStatus(String contents, Message response) {
- mCi.sendEnvelopeWithStatus(contents, response);
+ if (mUiccProfile != null) {
+ mUiccProfile.sendEnvelopeWithStatus(contents, response);
+ } else {
+ loge("sendEnvelopeWithStatus Failed!");
+ }
}
- /* Returns number of applications on this card */
+ /**
+ * Returns number of applications on this card
+ * @deprecated Please use {@link UiccProfile#getNumApplications()} instead.
+ */
+ @Deprecated
public int getNumApplications() {
- int count = 0;
- for (UiccCardApplication a : mUiccApplications) {
- if (a != null) {
- count++;
- }
+ if (mUiccProfile != null) {
+ return mUiccProfile.getNumApplications();
+ } else {
+ return 0;
}
- return count;
}
public int getPhoneId() {
return mPhoneId;
}
+ public UiccProfile getUiccProfile() {
+ return mUiccProfile;
+ }
+
/**
* Returns true iff carrier privileges rules are null (dont need to be loaded) or loaded.
+ * @deprecated Please use {@link UiccProfile#areCarrierPriviligeRulesLoaded()} instead.
*/
+ @Deprecated
public boolean areCarrierPriviligeRulesLoaded() {
- UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
- return carrierPrivilegeRules == null
- || carrierPrivilegeRules.areCarrierPriviligeRulesLoaded();
+ if (mUiccProfile != null) {
+ return mUiccProfile.areCarrierPriviligeRulesLoaded();
+ } else {
+ return false;
+ }
}
/**
* Returns true if there are some carrier privilege rules loaded and specified.
+ * @deprecated Please use {@link UiccProfile#hasCarrierPrivilegeRules()} instead.
*/
+ @Deprecated
public boolean hasCarrierPrivilegeRules() {
- UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
- return carrierPrivilegeRules != null && carrierPrivilegeRules.hasCarrierPrivilegeRules();
+ if (mUiccProfile != null) {
+ return mUiccProfile.hasCarrierPrivilegeRules();
+ } else {
+ return false;
+ }
}
/**
* Exposes {@link UiccCarrierPrivilegeRules#getCarrierPrivilegeStatus}.
+ * @deprecated Please use
+ * {@link UiccProfile#getCarrierPrivilegeStatus(Signature, String)} instead.
*/
+ @Deprecated
public int getCarrierPrivilegeStatus(Signature signature, String packageName) {
- UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
- return carrierPrivilegeRules == null
- ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED :
- carrierPrivilegeRules.getCarrierPrivilegeStatus(signature, packageName);
+ if (mUiccProfile != null) {
+ return mUiccProfile.getCarrierPrivilegeStatus(signature, packageName);
+ } else {
+ return TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED;
+ }
}
/**
* Exposes {@link UiccCarrierPrivilegeRules#getCarrierPrivilegeStatus}.
+ * @deprecated Please use
+ * {@link UiccProfile#getCarrierPrivilegeStatus(PackageManager, String)} instead.
*/
+ @Deprecated
public int getCarrierPrivilegeStatus(PackageManager packageManager, String packageName) {
- UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
- return carrierPrivilegeRules == null
- ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED :
- carrierPrivilegeRules.getCarrierPrivilegeStatus(packageManager, packageName);
+ if (mUiccProfile != null) {
+ return mUiccProfile.getCarrierPrivilegeStatus(packageManager, packageName);
+ } else {
+ return TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED;
+ }
}
/**
* Exposes {@link UiccCarrierPrivilegeRules#getCarrierPrivilegeStatus}.
+ * @deprecated Please use {@link UiccProfile#getCarrierPrivilegeStatus(PackageInfo)} instead.
*/
+ @Deprecated
public int getCarrierPrivilegeStatus(PackageInfo packageInfo) {
- UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
- return carrierPrivilegeRules == null
- ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED :
- carrierPrivilegeRules.getCarrierPrivilegeStatus(packageInfo);
+ if (mUiccProfile != null) {
+ return mUiccProfile.getCarrierPrivilegeStatus(packageInfo);
+ } else {
+ return TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED;
+ }
}
/**
* Exposes {@link UiccCarrierPrivilegeRules#getCarrierPrivilegeStatusForCurrentTransaction}.
+ * @deprecated Please use
+ * {@link UiccProfile#getCarrierPrivilegeStatusForCurrentTransaction(PackageManager)} instead.
*/
+ @Deprecated
public int getCarrierPrivilegeStatusForCurrentTransaction(PackageManager packageManager) {
- UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
- return carrierPrivilegeRules == null
- ? TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED :
- carrierPrivilegeRules.getCarrierPrivilegeStatusForCurrentTransaction(
- packageManager);
+ if (mUiccProfile != null) {
+ return mUiccProfile.getCarrierPrivilegeStatusForCurrentTransaction(packageManager);
+ } else {
+ return TelephonyManager.CARRIER_PRIVILEGE_STATUS_RULES_NOT_LOADED;
+ }
}
/**
* Exposes {@link UiccCarrierPrivilegeRules#getCarrierPackageNamesForIntent}.
+ * @deprecated Please use
+ * {@link UiccProfile#getCarrierPackageNamesForIntent(PackageManager, Intent)} instead.
*/
+ @Deprecated
public List<String> getCarrierPackageNamesForIntent(
PackageManager packageManager, Intent intent) {
- UiccCarrierPrivilegeRules carrierPrivilegeRules = getCarrierPrivilegeRules();
- return carrierPrivilegeRules == null ? null :
- carrierPrivilegeRules.getCarrierPackageNamesForIntent(
- packageManager, intent);
- }
-
- /** Returns a reference to the current {@link UiccCarrierPrivilegeRules}. */
- private UiccCarrierPrivilegeRules getCarrierPrivilegeRules() {
- synchronized (mLock) {
- return mCarrierPrivilegeRules;
+ if (mUiccProfile != null) {
+ return mUiccProfile.getCarrierPackageNamesForIntent(packageManager, intent);
+ } else {
+ return null;
}
}
+ /**
+ * @deprecated Please use {@link UiccProfile#setOperatorBrandOverride(String)} instead.
+ */
+ @Deprecated
public boolean setOperatorBrandOverride(String brand) {
- log("setOperatorBrandOverride: " + brand);
- log("current iccId: " + getIccId());
-
- String iccId = getIccId();
- if (TextUtils.isEmpty(iccId)) {
+ if (mUiccProfile != null) {
+ return mUiccProfile.setOperatorBrandOverride(brand);
+ } else {
return false;
}
+ }
- SharedPreferences.Editor spEditor =
- PreferenceManager.getDefaultSharedPreferences(mContext).edit();
- String key = OPERATOR_BRAND_OVERRIDE_PREFIX + iccId;
- if (brand == null) {
- spEditor.remove(key).commit();
+ /**
+ * @deprecated Please use {@link UiccProfile#getOperatorBrandOverride()} instead.
+ */
+ @Deprecated
+ public String getOperatorBrandOverride() {
+ if (mUiccProfile != null) {
+ return mUiccProfile.getOperatorBrandOverride();
} else {
- spEditor.putString(key, brand).commit();
+ return null;
}
- return true;
}
- public String getOperatorBrandOverride() {
- String iccId = getIccId();
- if (TextUtils.isEmpty(iccId)) {
+ public String getIccId() {
+ if (mIccid != null) {
+ return mIccid;
+ } else if (mUiccProfile != null) {
+ return mUiccProfile.getIccId();
+ } else {
return null;
}
- SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
- String brandName = sp.getString(OPERATOR_BRAND_OVERRIDE_PREFIX + iccId, null);
- if (brandName == null) {
- // Check if CarrierConfig sets carrier name
- CarrierConfigManager manager = (CarrierConfigManager)
- mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
- int subId = SubscriptionController.getInstance().getSubIdUsingPhoneId(mPhoneId);
- if (manager != null) {
- PersistableBundle bundle = manager.getConfigForSubId(subId);
- if (bundle != null && bundle.getBoolean(
- CarrierConfigManager.KEY_CARRIER_NAME_OVERRIDE_BOOL)) {
- brandName = bundle.getString(CarrierConfigManager.KEY_CARRIER_NAME_STRING);
- }
- }
- }
- return brandName;
}
- public String getIccId() {
- // ICCID should be same across all the apps.
- for (UiccCardApplication app : mUiccApplications) {
- if (app != null) {
- IccRecords ir = app.getIccRecords();
- if (ir != null && ir.getIccId() != null) {
- return ir.getIccId();
- }
- }
+ /**
+ * Returns the ID of this SIM card, it is the ICCID of the active profile on the card for a UICC
+ * card or the EID of the card for an eUICC card.
+ */
+ public String getCardId() {
+ if (mCardId != null) {
+ return mCardId;
+ } else if (mUiccProfile != null) {
+ return mUiccProfile.getIccId();
+ } else {
+ return null;
}
- return null;
}
private void log(String msg) {
@@ -665,59 +513,10 @@ public class UiccCard {
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("UiccCard:");
pw.println(" mCi=" + mCi);
- pw.println(" mCatService=" + mCatService);
- for (int i = 0; i < mCarrierPrivilegeRegistrants.size(); i++) {
- pw.println(" mCarrierPrivilegeRegistrants[" + i + "]="
- + ((Registrant)mCarrierPrivilegeRegistrants.get(i)).getHandler());
- }
pw.println(" mCardState=" + mCardState);
- pw.println(" mUniversalPinState=" + mUniversalPinState);
- pw.println(" mGsmUmtsSubscriptionAppIndex=" + mGsmUmtsSubscriptionAppIndex);
- pw.println(" mCdmaSubscriptionAppIndex=" + mCdmaSubscriptionAppIndex);
- pw.println(" mImsSubscriptionAppIndex=" + mImsSubscriptionAppIndex);
- pw.println(" mImsSubscriptionAppIndex=" + mImsSubscriptionAppIndex);
- pw.println(" mUiccApplications: length=" + mUiccApplications.length);
- for (int i = 0; i < mUiccApplications.length; i++) {
- if (mUiccApplications[i] == null) {
- pw.println(" mUiccApplications[" + i + "]=" + null);
- } else {
- pw.println(" mUiccApplications[" + i + "]="
- + mUiccApplications[i].getType() + " " + mUiccApplications[i]);
- }
- }
pw.println();
- // Print details of all applications
- for (UiccCardApplication app : mUiccApplications) {
- if (app != null) {
- app.dump(fd, pw, args);
- pw.println();
- }
- }
- // Print details of all IccRecords
- for (UiccCardApplication app : mUiccApplications) {
- if (app != null) {
- IccRecords ir = app.getIccRecords();
- if (ir != null) {
- ir.dump(fd, pw, args);
- pw.println();
- }
- }
- }
- // Print UiccCarrierPrivilegeRules and registrants.
- if (mCarrierPrivilegeRules == null) {
- pw.println(" mCarrierPrivilegeRules: null");
- } else {
- pw.println(" mCarrierPrivilegeRules: " + mCarrierPrivilegeRules);
- mCarrierPrivilegeRules.dump(fd, pw, args);
- }
- pw.println(" mCarrierPrivilegeRegistrants: size=" + mCarrierPrivilegeRegistrants.size());
- for (int i = 0; i < mCarrierPrivilegeRegistrants.size(); i++) {
- pw.println(" mCarrierPrivilegeRegistrants[" + i + "]="
- + ((Registrant)mCarrierPrivilegeRegistrants.get(i)).getHandler());
+ if (mUiccProfile != null) {
+ mUiccProfile.dump(fd, pw, args);
}
- pw.flush();
- pw.println("mLocalLog:");
- mLocalLog.dump(fd, pw, args);
- pw.flush();
}
}
diff --git a/com/android/internal/telephony/uicc/UiccCardApplication.java b/com/android/internal/telephony/uicc/UiccCardApplication.java
index fa6bc3a6..918e635d 100644
--- a/com/android/internal/telephony/uicc/UiccCardApplication.java
+++ b/com/android/internal/telephony/uicc/UiccCardApplication.java
@@ -30,7 +30,6 @@ import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppState;
import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
import com.android.internal.telephony.uicc.IccCardApplicationStatus.PersoSubState;
import com.android.internal.telephony.uicc.IccCardStatus.PinState;
-import com.android.internal.telephony.SubscriptionController;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -60,7 +59,7 @@ public class UiccCardApplication {
public static final int AUTH_CONTEXT_UNDEFINED = PhoneConstants.AUTH_CONTEXT_UNDEFINED;
private final Object mLock = new Object();
- private UiccCard mUiccCard; //parent
+ private UiccProfile mUiccProfile; //parent
private AppState mAppState;
private AppType mAppType;
private int mAuthContext;
@@ -87,12 +86,12 @@ public class UiccCardApplication {
private RegistrantList mPinLockedRegistrants = new RegistrantList();
private RegistrantList mNetworkLockedRegistrants = new RegistrantList();
- public UiccCardApplication(UiccCard uiccCard,
+ public UiccCardApplication(UiccProfile uiccProfile,
IccCardApplicationStatus as,
Context c,
CommandsInterface ci) {
if (DBG) log("Creating UiccApp: " + as);
- mUiccCard = uiccCard;
+ mUiccProfile = uiccProfile;
mAppState = as.app_state;
mAppType = as.app_type;
mAuthContext = getAuthContext(mAppType);
@@ -432,7 +431,7 @@ public class UiccCardApplication {
/**
* Notifies handler of any transition into State.isPinLocked()
*/
- public void registerForLocked(Handler h, int what, Object obj) {
+ protected void registerForLocked(Handler h, int what, Object obj) {
synchronized (mLock) {
Registrant r = new Registrant (h, what, obj);
mPinLockedRegistrants.add(r);
@@ -440,7 +439,7 @@ public class UiccCardApplication {
}
}
- public void unregisterForLocked(Handler h) {
+ protected void unregisterForLocked(Handler h) {
synchronized (mLock) {
mPinLockedRegistrants.remove(h);
}
@@ -449,7 +448,7 @@ public class UiccCardApplication {
/**
* Notifies handler of any transition into State.NETWORK_LOCKED
*/
- public void registerForNetworkLocked(Handler h, int what, Object obj) {
+ protected void registerForNetworkLocked(Handler h, int what, Object obj) {
synchronized (mLock) {
Registrant r = new Registrant (h, what, obj);
mNetworkLockedRegistrants.add(r);
@@ -457,7 +456,7 @@ public class UiccCardApplication {
}
}
- public void unregisterForNetworkLocked(Handler h) {
+ protected void unregisterForNetworkLocked(Handler h) {
synchronized (mLock) {
mNetworkLockedRegistrants.remove(h);
}
@@ -604,7 +603,7 @@ public class UiccCardApplication {
public PinState getPin1State() {
synchronized (mLock) {
if (mPin1Replaced) {
- return mUiccCard.getUniversalPinState();
+ return mUiccProfile.getUniversalPinState();
}
return mPin1State;
}
@@ -836,6 +835,24 @@ public class UiccCardApplication {
}
/**
+ * @return true if the UiccCardApplication is ready.
+ */
+ public boolean isReady() {
+ synchronized (mLock) {
+ if (mAppState != AppState.APPSTATE_READY) {
+ return false;
+ } else if (mPin1State == PinState.PINSTATE_ENABLED_NOT_VERIFIED
+ || mPin1State == PinState.PINSTATE_ENABLED_BLOCKED
+ || mPin1State == PinState.PINSTATE_ENABLED_PERM_BLOCKED) {
+ loge("Sanity check failed! APPSTATE is ready while PIN1 is not verified!!!");
+ return false;
+ } else {
+ return true;
+ }
+ }
+ }
+
+ /**
* @return true if ICC card is PIN2 blocked
*/
public boolean getIccPin2Blocked() {
@@ -854,11 +871,11 @@ public class UiccCardApplication {
}
public int getPhoneId() {
- return mUiccCard.getPhoneId();
+ return mUiccProfile.getPhoneId();
}
- protected UiccCard getUiccCard() {
- return mUiccCard;
+ protected UiccProfile getUiccProfile() {
+ return mUiccProfile;
}
private void log(String msg) {
@@ -871,7 +888,7 @@ public class UiccCardApplication {
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
pw.println("UiccCardApplication: " + this);
- pw.println(" mUiccCard=" + mUiccCard);
+ pw.println(" mUiccProfile=" + mUiccProfile);
pw.println(" mAppState=" + mAppState);
pw.println(" mAppType=" + mAppType);
pw.println(" mPersoSubState=" + mPersoSubState);
diff --git a/com/android/internal/telephony/uicc/UiccCarrierPrivilegeRules.java b/com/android/internal/telephony/uicc/UiccCarrierPrivilegeRules.java
index bfa458b8..56b299a3 100644
--- a/com/android/internal/telephony/uicc/UiccCarrierPrivilegeRules.java
+++ b/com/android/internal/telephony/uicc/UiccCarrierPrivilegeRules.java
@@ -178,7 +178,7 @@ public class UiccCarrierPrivilegeRules extends Handler {
}
}
- private UiccCard mUiccCard; // Parent
+ private UiccProfile mUiccProfile; // Parent
private UiccPkcs15 mUiccPkcs15; // ARF fallback
private AtomicInteger mState;
private List<UiccAccessRule> mAccessRules;
@@ -200,13 +200,13 @@ public class UiccCarrierPrivilegeRules extends Handler {
// Send open logical channel request.
String aid = (aidId == ARAD) ? ARAD_AID : ARAM_AID;
int p2 = 0x00;
- mUiccCard.iccOpenLogicalChannel(aid, p2, /* supported p2 value */
+ mUiccProfile.iccOpenLogicalChannel(aid, p2, /* supported p2 value */
obtainMessage(EVENT_OPEN_LOGICAL_CHANNEL_DONE, 0, aidId, null));
}
- public UiccCarrierPrivilegeRules(UiccCard uiccCard, Message loadedCallback) {
+ public UiccCarrierPrivilegeRules(UiccProfile uiccProfile, Message loadedCallback) {
log("Creating UiccCarrierPrivilegeRules");
- mUiccCard = uiccCard;
+ mUiccProfile = uiccProfile;
mState = new AtomicInteger(STATE_LOADING);
mStatusMessage = "Not loaded.";
mLoadedCallback = loadedCallback;
@@ -408,7 +408,7 @@ public class UiccCarrierPrivilegeRules extends Handler {
ar = (AsyncResult) msg.obj;
if (ar.exception == null && ar.result != null) {
mChannelId = ((int[]) ar.result)[0];
- mUiccCard.iccTransmitApduLogicalChannel(mChannelId, CLA, COMMAND, P1, P2, P3,
+ mUiccProfile.iccTransmitApduLogicalChannel(mChannelId, CLA, COMMAND, P1, P2, P3,
DATA, obtainMessage(EVENT_TRANSMIT_LOGICAL_CHANNEL_DONE, mChannelId,
mAIDInUse));
} else {
@@ -433,7 +433,7 @@ public class UiccCarrierPrivilegeRules extends Handler {
// if rules cannot be read from both ARA_D and ARA_M applet,
// fallback to PKCS15-based ARF.
log("No ARA, try ARF next.");
- mUiccPkcs15 = new UiccPkcs15(mUiccCard,
+ mUiccPkcs15 = new UiccPkcs15(mUiccProfile,
obtainMessage(EVENT_PKCS15_READ_DONE));
}
}
@@ -459,7 +459,7 @@ public class UiccCarrierPrivilegeRules extends Handler {
updateState(STATE_LOADED, "Success!");
}
} else {
- mUiccCard.iccTransmitApduLogicalChannel(mChannelId, CLA, COMMAND,
+ mUiccProfile.iccTransmitApduLogicalChannel(mChannelId, CLA, COMMAND,
P1, P2_EXTENDED_DATA, P3, DATA,
obtainMessage(EVENT_TRANSMIT_LOGICAL_CHANNEL_DONE,
mChannelId, mAIDInUse));
@@ -483,7 +483,7 @@ public class UiccCarrierPrivilegeRules extends Handler {
}
}
- mUiccCard.iccCloseLogicalChannel(mChannelId, obtainMessage(
+ mUiccProfile.iccCloseLogicalChannel(mChannelId, obtainMessage(
EVENT_CLOSE_LOGICAL_CHANNEL_DONE, 0, mAIDInUse));
mChannelId = -1;
break;
diff --git a/com/android/internal/telephony/uicc/UiccController.java b/com/android/internal/telephony/uicc/UiccController.java
index c7c802ce..05069da9 100644
--- a/com/android/internal/telephony/uicc/UiccController.java
+++ b/com/android/internal/telephony/uicc/UiccController.java
@@ -17,6 +17,7 @@
package com.android.internal.telephony.uicc;
import android.content.Context;
+import android.content.Intent;
import android.os.AsyncResult;
import android.os.Handler;
import android.os.Message;
@@ -27,12 +28,17 @@ import android.telephony.Rlog;
import android.telephony.TelephonyManager;
import android.text.format.Time;
+import com.android.internal.telephony.CommandException;
import com.android.internal.telephony.CommandsInterface;
import com.android.internal.telephony.PhoneConstants;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
import java.util.LinkedList;
+import java.util.Set;
/**
* This class is responsible for keeping all knowledge about
@@ -54,7 +60,13 @@ import java.util.LinkedList;
* UiccController
* #
* |
+ * UiccSlot[]
+ * #
+ * |
* UiccCard
+ * #
+ * |
+ * UiccProfile
* # #
* | ------------------
* UiccCardApplication CatService
@@ -72,35 +84,36 @@ import java.util.LinkedList;
* ^ stands for Generalization
*
* See also {@link com.android.internal.telephony.IccCard}
- * and {@link com.android.internal.telephony.uicc.IccCardProxy}
*/
public class UiccController extends Handler {
private static final boolean DBG = true;
+ private static final boolean VDBG = false; //STOPSHIP if true
private static final String LOG_TAG = "UiccController";
+ public static final int INVALID_SLOT_ID = -1;
+
public static final int APP_FAM_3GPP = 1;
public static final int APP_FAM_3GPP2 = 2;
public static final int APP_FAM_IMS = 3;
private static final int EVENT_ICC_STATUS_CHANGED = 1;
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 static final int EVENT_GET_ICC_STATUS_DONE = 3;
+ private static final int EVENT_GET_SLOT_STATUS_DONE = 4;
+ private static final int EVENT_RADIO_ON = 5;
+ private static final int EVENT_RADIO_AVAILABLE = 6;
+ private static final int EVENT_RADIO_UNAVAILABLE = 7;
+ private static final int EVENT_SIM_REFRESH = 8;
+
+ // this needs to be here, because on bootup we dont know which index maps to which UiccSlot
private CommandsInterface[] mCis;
- // 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 UiccSlot[] mUiccSlots;
+ private int[] mPhoneIdToSlotId;
+ private boolean mIsSlotStatusSupported = true;
private static final Object mLock = new Object();
private static UiccController mInstance;
+ private static ArrayList<IccSlotStatus> sLastSlotStatus;
private Context mContext;
@@ -118,7 +131,7 @@ public class UiccController extends Handler {
throw new RuntimeException("MSimUiccController.make() should only be called once");
}
mInstance = new UiccController(c, ci);
- return (UiccController)mInstance;
+ return mInstance;
}
}
@@ -126,37 +139,41 @@ public class UiccController extends Handler {
if (DBG) log("Creating UiccController");
mContext = c;
mCis = ci;
+ if (VDBG) {
+ log("config_num_physical_slots = " + c.getResources().getInteger(
+ com.android.internal.R.integer.config_num_physical_slots));
+ }
+ mUiccSlots = new UiccSlot[c.getResources().getInteger(
+ com.android.internal.R.integer.config_num_physical_slots)];
+ mPhoneIdToSlotId = new int[ci.length];
+ Arrays.fill(mPhoneIdToSlotId, INVALID_SLOT_ID);
+ if (VDBG) logPhoneIdToSlotIdMapping();
for (int i = 0; i < mCis.length; 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
+ mCis[i].registerForIccStatusChanged(this, EVENT_ICC_STATUS_CHANGED, i);
+ // slot status should be the same on all RILs; request it only for phoneId 0
+ if (i == 0) {
+ mCis[i].registerForIccSlotStatusChanged(this, EVENT_SLOT_STATUS_CHANGED, i);
+ }
// 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.
+ // i.e. not in CryptKeeper bounce, read SIM when radio state is available.
// 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_OR_SLOT_STATUS_CHANGED, index);
+ mCis[i].registerForAvailable(this, EVENT_RADIO_AVAILABLE, i);
} else {
- mCis[i].registerForOn(this, EVENT_ICC_OR_SLOT_STATUS_CHANGED, index);
+ mCis[i].registerForOn(this, EVENT_RADIO_ON, i);
}
- mCis[i].registerForNotAvailable(this, EVENT_RADIO_UNAVAILABLE, index);
- mCis[i].registerForIccRefresh(this, EVENT_SIM_REFRESH, index);
+ mCis[i].registerForNotAvailable(this, EVENT_RADIO_UNAVAILABLE, i);
+ mCis[i].registerForIccRefresh(this, EVENT_SIM_REFRESH, i);
}
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;
+ return mPhoneIdToSlotId[phoneId];
}
public static UiccController getInstance() {
@@ -198,23 +215,46 @@ public class UiccController extends Handler {
*/
public UiccCard getUiccCardForPhone(int phoneId) {
synchronized (mLock) {
- int slotId = getSlotIdFromPhoneId(phoneId);
- return getUiccCardForSlot(slotId);
+ if (isValidPhoneIndex(phoneId)) {
+ UiccSlot uiccSlot = getUiccSlotForPhone(phoneId);
+ if (uiccSlot != null) {
+ return uiccSlot.getUiccCard();
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * API to get UiccProfile corresponding to given phone id
+ * @return UiccProfile object corresponding to given phone id; null if there is no card/profile
+ * present for the phone id
+ */
+ public UiccProfile getUiccProfileForPhone(int phoneId) {
+ synchronized (mLock) {
+ if (isValidPhoneIndex(phoneId)) {
+ UiccCard uiccCard = getUiccCardForPhone(phoneId);
+ return uiccCard != null ? uiccCard.getUiccProfile() : null;
+ }
+ return null;
}
}
/**
- * API to get an array of all UiccSlots, which represents all physical slots on the device
- * @return array of all UiccSlots
+ * API to get all the UICC slots.
+ * @return UiccSlots array.
*/
public UiccSlot[] getUiccSlots() {
- // Return cloned array since we don't want to give out reference
- // to internal data structure.
synchronized (mLock) {
- return mUiccSlots.clone();
+ return mUiccSlots;
}
}
+ /** Map logicalSlot to physicalSlot, and activate the physicalSlot if it is inactive. */
+ public void switchSlots(int[] physicalSlots, Message response) {
+ // TODO(amitmahajan): Method implementation.
+ }
+
/**
* API to get UiccSlot object for a specific physical slot index on the device
* @return UiccSlot object for the given physical slot index
@@ -234,14 +274,47 @@ public class UiccController extends Handler {
*/
public UiccSlot getUiccSlotForPhone(int phoneId) {
synchronized (mLock) {
- int slotId = getSlotIdFromPhoneId(phoneId);
- if (isValidSlotIndex(slotId)) {
- return mUiccSlots[slotId];
+ if (isValidPhoneIndex(phoneId)) {
+ int slotId = getSlotIdFromPhoneId(phoneId);
+ if (isValidSlotIndex(slotId)) {
+ return mUiccSlots[slotId];
+ }
}
return null;
}
}
+ /**
+ * API to get UiccSlot object for a given cardId
+ * @param cardId Identifier for a SIM. This can be an ICCID, or an EID in case of an eSIM.
+ * @return int Index of UiccSlot for the given cardId if one is found, {@link #INVALID_SLOT_ID}
+ * otherwise
+ */
+ public int getUiccSlotForCardId(String cardId) {
+ synchronized (mLock) {
+ // first look up based on cardId
+ for (int idx = 0; idx < mUiccSlots.length; idx++) {
+ if (mUiccSlots[idx] != null) {
+ UiccCard uiccCard = mUiccSlots[idx].getUiccCard();
+ if (uiccCard != null) {
+ // todo: uncomment this once getCardId() is added
+ //if (cardId.equals(uiccCard.getCardId())) {
+ if (false) {
+ return idx;
+ }
+ }
+ }
+ }
+ // if a match is not found, do a lookup based on ICCID
+ for (int idx = 0; idx < mUiccSlots.length; idx++) {
+ if (mUiccSlots[idx] != null && cardId.equals(mUiccSlots[idx].getIccId())) {
+ return idx;
+ }
+ }
+ return INVALID_SLOT_ID;
+ }
+ }
+
// Easy to use API
public IccRecords getIccRecords(int phoneId, int family) {
synchronized (mLock) {
@@ -296,18 +369,45 @@ public class UiccController extends Handler {
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[phoneId].getIccCardStatus(obtainMessage(EVENT_GET_ICC_STATUS_DONE,
phoneId));
break;
+ case EVENT_RADIO_AVAILABLE:
+ case EVENT_RADIO_ON:
+ if (DBG) {
+ log("Received EVENT_RADIO_AVAILABLE/EVENT_RADIO_ON, calling "
+ + "getIccCardStatus");
+ }
+ mCis[phoneId].getIccCardStatus(obtainMessage(EVENT_GET_ICC_STATUS_DONE,
+ phoneId));
+ // slot status should be the same on all RILs; request it only for phoneId 0
+ if (phoneId == 0) {
+ if (DBG) {
+ log("Received EVENT_RADIO_AVAILABLE/EVENT_RADIO_ON for phoneId 0, "
+ + "calling getIccSlotsStatus");
+ }
+ mCis[phoneId].getIccSlotsStatus(obtainMessage(EVENT_GET_SLOT_STATUS_DONE,
+ phoneId));
+ }
+ break;
case EVENT_GET_ICC_STATUS_DONE:
if (DBG) log("Received EVENT_GET_ICC_STATUS_DONE");
onGetIccCardStatusDone(ar, phoneId);
break;
+ case EVENT_SLOT_STATUS_CHANGED:
+ case EVENT_GET_SLOT_STATUS_DONE:
+ if (DBG) {
+ log("Received EVENT_SLOT_STATUS_CHANGED or EVENT_GET_SLOT_STATUS_DONE");
+ }
+ onGetSlotStatusDone(ar);
+ break;
case EVENT_RADIO_UNAVAILABLE:
if (DBG) log("EVENT_RADIO_UNAVAILABLE, dispose card");
- mUiccSlots[getSlotIdFromPhoneId(phoneId)].onRadioStateUnavailable();
+ UiccSlot uiccSlot = getUiccSlotForPhone(phoneId);
+ if (uiccSlot != null) {
+ uiccSlot.onRadioStateUnavailable();
+ }
mIccChangedRegistrants.notifyRegistrants(new AsyncResult(null, phoneId, null));
break;
case EVENT_SIM_REFRESH:
@@ -316,6 +416,7 @@ public class UiccController extends Handler {
break;
default:
Rlog.e(LOG_TAG, " Unknown Event " + msg.what);
+ break;
}
}
}
@@ -360,18 +461,140 @@ public class UiccController extends Handler {
+ "never return an error", ar.exception);
return;
}
- if (!isValidCardIndex(index)) {
+ if (!isValidPhoneIndex(index)) {
Rlog.e(LOG_TAG,"onGetIccCardStatusDone: invalid index : " + index);
return;
}
IccCardStatus status = (IccCardStatus)ar.result;
- mUiccSlots[getSlotIdFromPhoneId(index)].update(mContext, mCis[index], status, index);
+ int slotId = status.physicalSlotIndex;
+ if (VDBG) log("onGetIccCardStatusDone: phoneId " + index + " physicalSlotIndex " + slotId);
+ if (slotId == INVALID_SLOT_ID) {
+ slotId = index;
+ mPhoneIdToSlotId[index] = slotId;
+ }
+
+ if (VDBG) logPhoneIdToSlotIdMapping();
+
+ if (mUiccSlots[slotId] == null) {
+ if (VDBG) {
+ log("Creating mUiccSlots[" + slotId + "]; mUiccSlots.length = "
+ + mUiccSlots.length);
+ }
+ mUiccSlots[slotId] = new UiccSlot(mContext, true);
+ }
+
+ mUiccSlots[slotId].update(mCis[index], status, index);
if (DBG) log("Notifying IccChangedRegistrants");
mIccChangedRegistrants.notifyRegistrants(new AsyncResult(null, index, null));
+ }
+
+ private synchronized void onGetSlotStatusDone(AsyncResult ar) {
+ if (!mIsSlotStatusSupported) {
+ if (VDBG) log("onGetSlotStatusDone: ignoring since mIsSlotStatusSupported is false");
+ return;
+ }
+ Throwable e = ar.exception;
+ if (e != null) {
+ if (!(e instanceof CommandException) || ((CommandException) e).getCommandError()
+ != CommandException.Error.REQUEST_NOT_SUPPORTED) {
+ // this is not expected; there should be no exception other than
+ // REQUEST_NOT_SUPPORTED
+ Rlog.e(LOG_TAG, "Unexpected error getting slot status.", ar.exception);
+ } else {
+ // REQUEST_NOT_SUPPORTED
+ log("onGetSlotStatusDone: request not supported; marking mIsSlotStatusSupported "
+ + "to false");
+ mIsSlotStatusSupported = false;
+ }
+ return;
+ }
+
+ ArrayList<IccSlotStatus> status = (ArrayList<IccSlotStatus>) ar.result;
+
+ if (!slotStatusChanged(status)) {
+ log("onGetSlotStatusDone: No change in slot status");
+ return;
+ }
+
+ int numActiveSlots = 0;
+ for (int i = 0; i < status.size(); i++) {
+ IccSlotStatus iss = status.get(i);
+ boolean isActive = (iss.slotState == IccSlotStatus.SlotState.SLOTSTATE_ACTIVE);
+ if (isActive) {
+ numActiveSlots++;
+
+ // sanity check: logicalSlotIndex should be valid for an active slot
+ if (!isValidPhoneIndex(iss.logicalSlotIndex)) {
+ throw new RuntimeException("Logical slot index " + iss.logicalSlotIndex
+ + " invalid for physical slot " + i);
+ }
+ mPhoneIdToSlotId[iss.logicalSlotIndex] = i;
+ }
+
+ if (mUiccSlots[i] == null) {
+ if (VDBG) {
+ log("Creating mUiccSlot[" + i + "]; mUiccSlots.length = " + mUiccSlots.length);
+ }
+ mUiccSlots[i] = new UiccSlot(mContext, isActive);
+ }
+
+ mUiccSlots[i].update(isActive ? mCis[iss.logicalSlotIndex] : null, iss);
+ }
+
+ if (VDBG) logPhoneIdToSlotIdMapping();
+
+ // sanity check: number of active slots should be valid
+ if (numActiveSlots != mPhoneIdToSlotId.length) {
+ throw new RuntimeException("Number of active slots " + numActiveSlots
+ + " does not match the expected value " + mPhoneIdToSlotId.length);
+ }
+ // sanity check: slotIds should be unique in mPhoneIdToSlotId
+ Set<Integer> slotIds = new HashSet<>();
+ for (int slotId : mPhoneIdToSlotId) {
+ if (slotIds.contains(slotId)) {
+ throw new RuntimeException("slotId " + slotId + " mapped to multiple phoneIds");
+ }
+ slotIds.add(slotId);
+ }
+
+ // broadcast slot status changed
+ Intent intent = new Intent(TelephonyManager.ACTION_SIM_SLOT_STATUS_CHANGED);
+ mContext.sendBroadcast(intent, android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE);
+ }
+
+ private boolean slotStatusChanged(ArrayList<IccSlotStatus> slotStatusList) {
+ if (sLastSlotStatus == null || sLastSlotStatus.size() != slotStatusList.size()) {
+ return true;
+ }
+ for (IccSlotStatus iccSlotStatus : slotStatusList) {
+ if (!sLastSlotStatus.contains(iccSlotStatus)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void logPhoneIdToSlotIdMapping() {
+ log("mPhoneIdToSlotId mapping:");
+ for (int i = 0; i < mPhoneIdToSlotId.length; i++) {
+ log(" phoneId " + i + " slotId " + mPhoneIdToSlotId[i]);
+ }
+ }
+ /**
+ * Slots are initialized when none of them are null.
+ * todo: is this even needed?
+ */
+ private synchronized boolean areSlotsInitialized() {
+ for (UiccSlot slot : mUiccSlots) {
+ if (slot == null) {
+ return false;
+ }
+ }
+ return true;
}
private void onSimRefresh(AsyncResult ar, Integer index) {
@@ -380,13 +603,18 @@ public class UiccController extends Handler {
return;
}
- if (!isValidCardIndex(index)) {
+ if (!isValidPhoneIndex(index)) {
Rlog.e(LOG_TAG,"onSimRefresh: invalid index : " + index);
return;
}
IccRefreshResponse resp = (IccRefreshResponse) ar.result;
- Rlog.d(LOG_TAG, "onSimRefresh: " + resp);
+ log("onSimRefresh: " + resp);
+
+ if (resp == null) {
+ Rlog.e(LOG_TAG, "onSimRefresh: received without input");
+ return;
+ }
UiccCard uiccCard = getUiccCardForPhone(index);
if (uiccCard == null) {
@@ -394,27 +622,32 @@ public class UiccController extends Handler {
return;
}
- if (resp.refreshResult != IccRefreshResponse.REFRESH_RESULT_RESET) {
- Rlog.d(LOG_TAG, "Ignoring non reset refresh: " + resp);
- return;
+ log("Handling refresh: " + resp);
+ boolean changed = false;
+ switch(resp.refreshResult) {
+ case IccRefreshResponse.REFRESH_RESULT_RESET:
+ case IccRefreshResponse.REFRESH_RESULT_INIT:
+ // Reset the required apps when we know about the refresh so that
+ // anyone interested does not get stale state.
+ changed = uiccCard.resetAppWithAid(resp.aid);
+ break;
+ default:
+ return;
}
- Rlog.d(LOG_TAG, "Handling refresh reset: " + resp);
-
- boolean changed = uiccCard.resetAppWithAid(resp.aid);
- if (changed) {
+ if (changed && resp.refreshResult == IccRefreshResponse.REFRESH_RESULT_RESET) {
boolean requirePowerOffOnSimRefreshReset = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_requireRadioPowerOffOnSimRefreshReset);
if (requirePowerOffOnSimRefreshReset) {
mCis[index].setRadioPower(false, null);
- } else {
- mCis[index].getIccCardStatus(obtainMessage(EVENT_GET_ICC_STATUS_DONE, index));
}
- mIccChangedRegistrants.notifyRegistrants(new AsyncResult(null, index, null));
}
+
+ // The card status could have changed. Get the latest state.
+ mCis[index].getIccCardStatus(obtainMessage(EVENT_GET_ICC_STATUS_DONE, index));
}
- private boolean isValidCardIndex(int index) {
+ private boolean isValidPhoneIndex(int index) {
return (index >= 0 && index < TelephonyManager.getDefault().getPhoneCount());
}
diff --git a/com/android/internal/telephony/uicc/UiccPkcs15.java b/com/android/internal/telephony/uicc/UiccPkcs15.java
index 80ebcbfb..b3a3482c 100644
--- a/com/android/internal/telephony/uicc/UiccPkcs15.java
+++ b/com/android/internal/telephony/uicc/UiccPkcs15.java
@@ -17,24 +17,15 @@
package com.android.internal.telephony.uicc;
import android.os.AsyncResult;
-import android.os.Binder;
import android.os.Handler;
import android.os.Message;
import android.telephony.Rlog;
-import android.telephony.TelephonyManager;
-import com.android.internal.telephony.CommandException;
-import com.android.internal.telephony.CommandsInterface;
-import com.android.internal.telephony.uicc.IccUtils;
import com.android.internal.telephony.uicc.UiccCarrierPrivilegeRules.TLV;
-import java.io.ByteArrayInputStream;
import java.io.FileDescriptor;
import java.io.PrintWriter;
-import java.lang.IllegalArgumentException;
-import java.lang.IndexOutOfBoundsException;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
import java.util.Locale;
@@ -89,7 +80,7 @@ public class UiccPkcs15 extends Handler {
private void selectFile() {
if (mChannelId >= 0) {
- mUiccCard.iccTransmitApduLogicalChannel(mChannelId, 0x00, 0xA4, 0x00, 0x04, 0x02,
+ mUiccProfile.iccTransmitApduLogicalChannel(mChannelId, 0x00, 0xA4, 0x00, 0x04, 0x02,
mFileId, obtainMessage(EVENT_SELECT_FILE_DONE));
} else {
log("EF based");
@@ -98,7 +89,7 @@ public class UiccPkcs15 extends Handler {
private void readBinary() {
if (mChannelId >=0 ) {
- mUiccCard.iccTransmitApduLogicalChannel(mChannelId, 0x00, 0xB0, 0x00, 0x00, 0x00,
+ mUiccProfile.iccTransmitApduLogicalChannel(mChannelId, 0x00, 0xB0, 0x00, 0x00, 0x00,
"", obtainMessage(EVENT_READ_BINARY_DONE));
} else {
log("EF based");
@@ -146,7 +137,7 @@ public class UiccPkcs15 extends Handler {
mCallback = callBack;
// Specified in ISO 7816-4 clause 7.1.1 0x04 means that FCP template is requested.
int p2 = 0x04;
- mUiccCard.iccOpenLogicalChannel(PKCS15_AID, p2, /* supported P2 value */
+ mUiccProfile.iccOpenLogicalChannel(PKCS15_AID, p2, /* supported P2 value */
obtainMessage(EVENT_OPEN_LOGICAL_CHANNEL_DONE));
}
@@ -176,7 +167,7 @@ public class UiccPkcs15 extends Handler {
}
}
- private UiccCard mUiccCard; // Parent
+ private UiccProfile mUiccProfile; // Parent
private Message mLoadedCallback;
private int mChannelId = -1; // Channel Id for communicating with UICC.
private List<String> mRules = new ArrayList<String>();
@@ -191,9 +182,9 @@ public class UiccPkcs15 extends Handler {
private static final int EVENT_LOAD_ACCF_DONE = 6;
private static final int EVENT_CLOSE_LOGICAL_CHANNEL_DONE = 7;
- public UiccPkcs15(UiccCard uiccCard, Message loadedCallback) {
+ public UiccPkcs15(UiccProfile uiccProfile, Message loadedCallback) {
log("Creating UiccPkcs15");
- mUiccCard = uiccCard;
+ mUiccProfile = uiccProfile;
mLoadedCallback = loadedCallback;
mPkcs15Selector = new Pkcs15Selector(obtainMessage(EVENT_SELECT_PKCS15_DONE));
}
@@ -249,7 +240,7 @@ public class UiccPkcs15 extends Handler {
private void cleanUp() {
log("cleanUp");
if (mChannelId >= 0) {
- mUiccCard.iccCloseLogicalChannel(mChannelId, obtainMessage(
+ mUiccProfile.iccCloseLogicalChannel(mChannelId, obtainMessage(
EVENT_CLOSE_LOGICAL_CHANNEL_DONE));
mChannelId = -1;
}
diff --git a/com/android/internal/telephony/uicc/UiccProfile.java b/com/android/internal/telephony/uicc/UiccProfile.java
index 3e7d2d7f..7e59e9ff 100644
--- a/com/android/internal/telephony/uicc/UiccProfile.java
+++ b/com/android/internal/telephony/uicc/UiccProfile.java
@@ -16,6 +16,7 @@
package com.android.internal.telephony.uicc;
+import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.usage.UsageStatsManager;
import android.content.Context;
@@ -31,11 +32,17 @@ import android.os.AsyncResult;
import android.os.Binder;
import android.os.Handler;
import android.os.Message;
+import android.os.PersistableBundle;
import android.os.Registrant;
import android.os.RegistrantList;
+import android.os.UserHandle;
import android.preference.PreferenceManager;
import android.provider.Settings;
+import android.telephony.CarrierConfigManager;
import android.telephony.Rlog;
+import android.telephony.ServiceState;
+import android.telephony.SubscriptionInfo;
+import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.LocalLog;
@@ -43,8 +50,16 @@ import android.view.WindowManager;
import com.android.internal.R;
import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.IccCard;
+import com.android.internal.telephony.IccCardConstants;
+import com.android.internal.telephony.MccTable;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.SubscriptionController;
import com.android.internal.telephony.cat.CatService;
import com.android.internal.telephony.uicc.IccCardApplicationStatus.AppType;
+import com.android.internal.telephony.uicc.IccCardStatus.CardState;
import com.android.internal.telephony.uicc.IccCardStatus.PinState;
import java.io.FileDescriptor;
@@ -65,9 +80,11 @@ import java.util.List;
*
* {@hide}
*/
-public class UiccProfile {
+public class UiccProfile extends Handler implements IccCard {
protected static final String LOG_TAG = "UiccProfile";
protected static final boolean DBG = true;
+ private static final boolean VDBG = false; //STOPSHIP if true
+ private static final boolean ICC_CARD_PROXY_REMOVED = true;
private static final String OPERATOR_BRAND_OVERRIDE_PREFIX = "operator_branding_";
@@ -83,6 +100,7 @@ public class UiccProfile {
private UiccCard mUiccCard; //parent
private CatService mCatService;
private UiccCarrierPrivilegeRules mCarrierPrivilegeRules;
+ private boolean mDisposed = false;
private RegistrantList mCarrierPrivilegeRegistrants = new RegistrantList();
@@ -97,12 +115,50 @@ public class UiccProfile {
private final int mPhoneId;
+ /*----------------------------------------------------*/
+ // logic moved over from IccCardProxy
+ private static final int EVENT_RADIO_OFF_OR_UNAVAILABLE = 1;
+ private static final int EVENT_ICC_LOCKED = 5;
+ private static final int EVENT_APP_READY = 6;
+ private static final int EVENT_RECORDS_LOADED = 7;
+ private static final int EVENT_NETWORK_LOCKED = 9;
+
+ private static final int EVENT_ICC_RECORD_EVENTS = 500;
+
+ private TelephonyManager mTelephonyManager;
+
+ private RegistrantList mNetworkLockedRegistrants = new RegistrantList();
+
+ private int mCurrentAppType = UiccController.APP_FAM_3GPP; //default to 3gpp?
+ private UiccCardApplication mUiccApplication = null;
+ private IccRecords mIccRecords = null;
+ private IccCardConstants.State mExternalState = IccCardConstants.State.UNKNOWN;
+
+ public static final String ACTION_INTERNAL_SIM_STATE_CHANGED =
+ "android.intent.action.internal_sim_state_changed";
+
+ /*----------------------------------------------------*/
+
public UiccProfile(Context c, CommandsInterface ci, IccCardStatus ics, int phoneId,
UiccCard uiccCard) {
if (DBG) log("Creating profile");
mUiccCard = uiccCard;
mPhoneId = phoneId;
+ if (ICC_CARD_PROXY_REMOVED) {
+ // set current app type based on phone type - do this before calling update() as that
+ // calls updateIccAvailability() which uses mCurrentAppType
+ Phone phone = PhoneFactory.getPhone(phoneId);
+ if (phone != null) {
+ setCurrentAppType(phone.getPhoneType() == PhoneConstants.PHONE_TYPE_GSM);
+ }
+ }
update(c, ci, ics);
+
+ if (ICC_CARD_PROXY_REMOVED) {
+ ci.registerForOffOrNotAvailable(this, EVENT_RADIO_OFF_OR_UNAVAILABLE, null);
+
+ resetProperties();
+ }
}
/**
@@ -111,6 +167,14 @@ public class UiccProfile {
public void dispose() {
synchronized (mLock) {
if (DBG) log("Disposing profile");
+
+ unregisterAllAppEvents();
+ unregisterCurrAppEvents();
+
+ if (ICC_CARD_PROXY_REMOVED) {
+ mCi.unregisterForOffOrNotAvailable(this);
+ }
+
if (mCatService != null) mCatService.dispose();
for (UiccCardApplication app : mUiccApplications) {
if (app != null) {
@@ -120,6 +184,591 @@ public class UiccProfile {
mCatService = null;
mUiccApplications = null;
mCarrierPrivilegeRules = null;
+ mDisposed = true;
+ }
+ }
+
+ /**
+ * The card application that the external world sees will be based on the
+ * voice radio technology only!
+ */
+ public void setVoiceRadioTech(int radioTech) {
+ synchronized (mLock) {
+ if (DBG) {
+ log("Setting radio tech " + ServiceState.rilRadioTechnologyToString(radioTech));
+ }
+ setCurrentAppType(ServiceState.isGsm(radioTech));
+ updateIccAvailability(false);
+ }
+ }
+
+ private void setCurrentAppType(boolean isGsm) {
+ if (VDBG) log("setCurrentAppType");
+ synchronized (mLock) {
+ boolean isLteOnCdmaMode = TelephonyManager.getLteOnCdmaModeStatic()
+ == PhoneConstants.LTE_ON_CDMA_TRUE;
+ if (isGsm || isLteOnCdmaMode) {
+ mCurrentAppType = UiccController.APP_FAM_3GPP;
+ } else {
+ mCurrentAppType = UiccController.APP_FAM_3GPP2;
+ }
+ }
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (mDisposed) {
+ loge("handleMessage: Received " + msg.what + " after dispose(); ignoring the message");
+ return;
+ }
+ switch (msg.what) {
+ case EVENT_RADIO_OFF_OR_UNAVAILABLE:
+ updateExternalState();
+ break;
+
+ case EVENT_ICC_LOCKED:
+ processLockedState();
+ break;
+
+ case EVENT_APP_READY:
+ if (VDBG) log("EVENT_APP_READY");
+ if (areAllApplicationsReady()) {
+ if (areAllRecordsLoaded() && areCarrierPriviligeRulesLoaded()) {
+ setExternalState(IccCardConstants.State.LOADED);
+ } else {
+ setExternalState(IccCardConstants.State.READY);
+ }
+ }
+ break;
+
+ case EVENT_RECORDS_LOADED:
+ if (VDBG) log("EVENT_RECORDS_LOADED");
+ if (!areAllRecordsLoaded()) {
+ break;
+ }
+ // Update the MCC/MNC.
+ if (mIccRecords != null) {
+ String operator = mIccRecords.getOperatorNumeric();
+ log("operator=" + operator + " mPhoneId=" + mPhoneId);
+
+ if (!TextUtils.isEmpty(operator)) {
+ mTelephonyManager.setSimOperatorNumericForPhone(mPhoneId, operator);
+ String countryCode = operator.substring(0, 3);
+ if (countryCode != null) {
+ mTelephonyManager.setSimCountryIsoForPhone(mPhoneId,
+ MccTable.countryCodeForMcc(Integer.parseInt(countryCode)));
+ } else {
+ loge("EVENT_RECORDS_LOADED Country code is null");
+ }
+ } else {
+ loge("EVENT_RECORDS_LOADED Operator name is null");
+ }
+ }
+ if (areCarrierPriviligeRulesLoaded()) {
+ setExternalState(IccCardConstants.State.LOADED);
+ }
+ break;
+
+ case EVENT_NETWORK_LOCKED:
+ mNetworkLockedRegistrants.notifyRegistrants();
+ setExternalState(IccCardConstants.State.NETWORK_LOCKED);
+ break;
+
+ case EVENT_ICC_RECORD_EVENTS:
+ if ((mCurrentAppType == UiccController.APP_FAM_3GPP) && (mIccRecords != null)) {
+ AsyncResult ar = (AsyncResult) msg.obj;
+ int eventCode = (Integer) ar.result;
+ if (eventCode == SIMRecords.EVENT_SPN) {
+ mTelephonyManager.setSimOperatorNameForPhone(
+ mPhoneId, mIccRecords.getServiceProviderName());
+ }
+ }
+ break;
+
+ case EVENT_CARRIER_PRIVILEGES_LOADED:
+ if (VDBG) log("EVENT_CARRIER_PRIVILEGES_LOADED");
+ onCarrierPriviligesLoadedMessage();
+ if (areAllRecordsLoaded()) {
+ setExternalState(IccCardConstants.State.LOADED);
+ }
+ break;
+
+ case EVENT_OPEN_LOGICAL_CHANNEL_DONE:
+ case EVENT_CLOSE_LOGICAL_CHANNEL_DONE:
+ case EVENT_TRANSMIT_APDU_LOGICAL_CHANNEL_DONE:
+ case EVENT_TRANSMIT_APDU_BASIC_CHANNEL_DONE:
+ case EVENT_SIM_IO_DONE:
+ AsyncResult ar = (AsyncResult) msg.obj;
+ if (ar.exception != null) {
+ loglocal("Exception: " + ar.exception);
+ log("Error in SIM access with exception" + ar.exception);
+ }
+ AsyncResult.forMessage((Message) ar.userObj, ar.result, ar.exception);
+ ((Message) ar.userObj).sendToTarget();
+ break;
+
+ default:
+ loge("Unhandled message with number: " + msg.what);
+ break;
+ }
+ }
+
+ private void updateIccAvailability(boolean allAppsChanged) {
+ synchronized (mLock) {
+ UiccCardApplication newApp;
+ IccRecords newRecords = null;
+ newApp = getApplication(mCurrentAppType);
+ if (newApp != null) {
+ newRecords = newApp.getIccRecords();
+ }
+
+ if (allAppsChanged) {
+ unregisterAllAppEvents();
+ registerAllAppEvents();
+ }
+
+ if (mIccRecords != newRecords || mUiccApplication != newApp) {
+ if (DBG) log("Icc changed. Reregistering.");
+ unregisterCurrAppEvents();
+ mUiccApplication = newApp;
+ mIccRecords = newRecords;
+ registerCurrAppEvents();
+ }
+ updateExternalState();
+ }
+ }
+
+ void resetProperties() {
+ if (mCurrentAppType == UiccController.APP_FAM_3GPP) {
+ log("update icc_operator_numeric=" + "");
+ mTelephonyManager.setSimOperatorNumericForPhone(mPhoneId, "");
+ mTelephonyManager.setSimCountryIsoForPhone(mPhoneId, "");
+ mTelephonyManager.setSimOperatorNameForPhone(mPhoneId, "");
+ }
+ }
+
+ private void updateExternalState() {
+
+ if (mUiccCard.getCardState() == IccCardStatus.CardState.CARDSTATE_ERROR) {
+ setExternalState(IccCardConstants.State.CARD_IO_ERROR);
+ return;
+ }
+
+ if (mUiccCard.getCardState() == IccCardStatus.CardState.CARDSTATE_RESTRICTED) {
+ setExternalState(IccCardConstants.State.CARD_RESTRICTED);
+ return;
+ }
+
+ if (mUiccApplication == null || !areAllApplicationsReady()) {
+ setExternalState(IccCardConstants.State.NOT_READY);
+ return;
+ }
+
+ // By process of elimination, the UICC Card State = PRESENT
+ switch (mUiccApplication.getState()) {
+ case APPSTATE_UNKNOWN:
+ /*
+ * APPSTATE_UNKNOWN is a catch-all state reported whenever the app
+ * is not explicitly in one of the other states. To differentiate the
+ * case where we know that there is a card present, but the APP is not
+ * ready, we choose NOT_READY here instead of unknown. This is possible
+ * in at least two cases:
+ * 1) A transient during the process of the SIM bringup
+ * 2) There is no valid App on the SIM to load, which can be the case with an
+ * eSIM/soft SIM.
+ */
+ setExternalState(IccCardConstants.State.NOT_READY);
+ break;
+ case APPSTATE_SUBSCRIPTION_PERSO:
+ if (mUiccApplication.getPersoSubState()
+ == IccCardApplicationStatus.PersoSubState.PERSOSUBSTATE_SIM_NETWORK) {
+ setExternalState(IccCardConstants.State.NETWORK_LOCKED);
+ }
+ // Otherwise don't change external SIM state.
+ break;
+ case APPSTATE_READY:
+ if (areAllApplicationsReady()) {
+ if (areAllRecordsLoaded() && areCarrierPriviligeRulesLoaded()) {
+ setExternalState(IccCardConstants.State.LOADED);
+ } else {
+ setExternalState(IccCardConstants.State.READY);
+ }
+ } else {
+ setExternalState(IccCardConstants.State.NOT_READY);
+ }
+ break;
+ }
+ }
+
+ private void registerAllAppEvents() {
+ // todo: all of these should be notified to UiccProfile directly without needing to register
+ for (UiccCardApplication app : mUiccApplications) {
+ if (app != null) {
+ if (VDBG) log("registerUiccCardEvents: registering for EVENT_APP_READY");
+ app.registerForReady(this, EVENT_APP_READY, null);
+ IccRecords ir = app.getIccRecords();
+ if (ir != null) {
+ if (VDBG) log("registerUiccCardEvents: registering for EVENT_RECORDS_LOADED");
+ ir.registerForRecordsLoaded(this, EVENT_RECORDS_LOADED, null);
+ ir.registerForRecordsEvents(this, EVENT_ICC_RECORD_EVENTS, null);
+ }
+ }
+ }
+ }
+
+ private void unregisterAllAppEvents() {
+ for (UiccCardApplication app : mUiccApplications) {
+ if (app != null) {
+ app.unregisterForReady(this);
+ IccRecords ir = app.getIccRecords();
+ if (ir != null) {
+ ir.unregisterForRecordsLoaded(this);
+ ir.unregisterForRecordsEvents(this);
+ }
+ }
+ }
+ }
+
+ private void registerCurrAppEvents() {
+ // In case of locked, only listen to the current application.
+ if (mIccRecords != null) {
+ mIccRecords.registerForLockedRecordsLoaded(this, EVENT_ICC_LOCKED, null);
+ mIccRecords.registerForNetworkLockedRecordsLoaded(this, EVENT_NETWORK_LOCKED, null);
+ }
+ }
+
+ private void unregisterCurrAppEvents() {
+ if (mIccRecords != null) {
+ mIccRecords.unregisterForLockedRecordsLoaded(this);
+ mIccRecords.unregisterForNetworkLockedRecordsLoaded(this);
+ }
+ }
+
+ private void broadcastInternalIccStateChangedIntent(String value, String reason) {
+ synchronized (mLock) {
+ Intent intent = new Intent(ACTION_INTERNAL_SIM_STATE_CHANGED);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
+ | Intent.FLAG_RECEIVER_FOREGROUND);
+ intent.putExtra(PhoneConstants.PHONE_NAME_KEY, "Phone");
+ intent.putExtra(IccCardConstants.INTENT_KEY_ICC_STATE, value);
+ intent.putExtra(IccCardConstants.INTENT_KEY_LOCKED_REASON, reason);
+ intent.putExtra(PhoneConstants.PHONE_KEY, mPhoneId); // SubId may not be valid.
+ log("Sending intent ACTION_INTERNAL_SIM_STATE_CHANGED value=" + value
+ + " for mPhoneId : " + mPhoneId);
+ ActivityManager.broadcastStickyIntent(intent, UserHandle.USER_ALL);
+ }
+ }
+
+ private void setExternalState(IccCardConstants.State newState, boolean override) {
+ synchronized (mLock) {
+ if (!SubscriptionManager.isValidSlotIndex(mPhoneId)) {
+ loge("setExternalState: mPhoneId=" + mPhoneId + " is invalid; Return!!");
+ return;
+ }
+
+ if (!override && newState == mExternalState) {
+ log("setExternalState: !override and newstate unchanged from " + newState);
+ return;
+ }
+ mExternalState = newState;
+ log("setExternalState: set mPhoneId=" + mPhoneId + " mExternalState=" + mExternalState);
+ mTelephonyManager.setSimStateForPhone(mPhoneId, getState().toString());
+
+ broadcastInternalIccStateChangedIntent(getIccStateIntentString(mExternalState),
+ getIccStateReason(mExternalState));
+ }
+ }
+
+ private void processLockedState() {
+ synchronized (mLock) {
+ if (mUiccApplication == null) {
+ //Don't need to do anything if non-existent application is locked
+ return;
+ }
+ PinState pin1State = mUiccApplication.getPin1State();
+ if (pin1State == PinState.PINSTATE_ENABLED_PERM_BLOCKED) {
+ setExternalState(IccCardConstants.State.PERM_DISABLED);
+ return;
+ }
+
+ IccCardApplicationStatus.AppState appState = mUiccApplication.getState();
+ switch (appState) {
+ case APPSTATE_PIN:
+ setExternalState(IccCardConstants.State.PIN_REQUIRED);
+ break;
+ case APPSTATE_PUK:
+ setExternalState(IccCardConstants.State.PUK_REQUIRED);
+ break;
+ case APPSTATE_DETECTED:
+ case APPSTATE_READY:
+ case APPSTATE_SUBSCRIPTION_PERSO:
+ case APPSTATE_UNKNOWN:
+ // Neither required
+ break;
+ }
+ }
+ }
+
+ private void setExternalState(IccCardConstants.State newState) {
+ setExternalState(newState, false);
+ }
+
+ /**
+ * Function to check if all ICC records have been loaded
+ * @return true if all ICC records have been loaded, false otherwise.
+ */
+ public boolean getIccRecordsLoaded() {
+ synchronized (mLock) {
+ if (mIccRecords != null) {
+ return mIccRecords.getRecordsLoaded();
+ }
+ return false;
+ }
+ }
+
+ private String getIccStateIntentString(IccCardConstants.State state) {
+ switch (state) {
+ case ABSENT: return IccCardConstants.INTENT_VALUE_ICC_ABSENT;
+ case PIN_REQUIRED: return IccCardConstants.INTENT_VALUE_ICC_LOCKED;
+ case PUK_REQUIRED: return IccCardConstants.INTENT_VALUE_ICC_LOCKED;
+ case NETWORK_LOCKED: return IccCardConstants.INTENT_VALUE_ICC_LOCKED;
+ case READY: return IccCardConstants.INTENT_VALUE_ICC_READY;
+ case NOT_READY: return IccCardConstants.INTENT_VALUE_ICC_NOT_READY;
+ case PERM_DISABLED: return IccCardConstants.INTENT_VALUE_ICC_LOCKED;
+ case CARD_IO_ERROR: return IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR;
+ case CARD_RESTRICTED: return IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED;
+ case LOADED: return IccCardConstants.INTENT_VALUE_ICC_LOADED;
+ default: return IccCardConstants.INTENT_VALUE_ICC_UNKNOWN;
+ }
+ }
+
+ /**
+ * Locked state have a reason (PIN, PUK, NETWORK, PERM_DISABLED, CARD_IO_ERROR)
+ * @return reason
+ */
+ private String getIccStateReason(IccCardConstants.State state) {
+ switch (state) {
+ case PIN_REQUIRED: return IccCardConstants.INTENT_VALUE_LOCKED_ON_PIN;
+ case PUK_REQUIRED: return IccCardConstants.INTENT_VALUE_LOCKED_ON_PUK;
+ case NETWORK_LOCKED: return IccCardConstants.INTENT_VALUE_LOCKED_NETWORK;
+ case PERM_DISABLED: return IccCardConstants.INTENT_VALUE_ABSENT_ON_PERM_DISABLED;
+ case CARD_IO_ERROR: return IccCardConstants.INTENT_VALUE_ICC_CARD_IO_ERROR;
+ case CARD_RESTRICTED: return IccCardConstants.INTENT_VALUE_ICC_CARD_RESTRICTED;
+ default: return null;
+ }
+ }
+
+ /* IccCard interface implementation */
+ @Override
+ public IccCardConstants.State getState() {
+ synchronized (mLock) {
+ return mExternalState;
+ }
+ }
+
+ @Override
+ public IccRecords getIccRecords() {
+ synchronized (mLock) {
+ return mIccRecords;
+ }
+ }
+
+ /**
+ * Notifies handler of any transition into State.NETWORK_LOCKED
+ */
+ @Override
+ public void registerForNetworkLocked(Handler h, int what, Object obj) {
+ synchronized (mLock) {
+ Registrant r = new Registrant(h, what, obj);
+
+ mNetworkLockedRegistrants.add(r);
+
+ if (getState() == IccCardConstants.State.NETWORK_LOCKED) {
+ r.notifyRegistrant();
+ }
+ }
+ }
+
+ @Override
+ public void unregisterForNetworkLocked(Handler h) {
+ synchronized (mLock) {
+ mNetworkLockedRegistrants.remove(h);
+ }
+ }
+
+ @Override
+ public void supplyPin(String pin, Message onComplete) {
+ synchronized (mLock) {
+ if (mUiccApplication != null) {
+ mUiccApplication.supplyPin(pin, onComplete);
+ } else if (onComplete != null) {
+ Exception e = new RuntimeException("ICC card is absent.");
+ AsyncResult.forMessage(onComplete).exception = e;
+ onComplete.sendToTarget();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void supplyPuk(String puk, String newPin, Message onComplete) {
+ synchronized (mLock) {
+ if (mUiccApplication != null) {
+ mUiccApplication.supplyPuk(puk, newPin, onComplete);
+ } else if (onComplete != null) {
+ Exception e = new RuntimeException("ICC card is absent.");
+ AsyncResult.forMessage(onComplete).exception = e;
+ onComplete.sendToTarget();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void supplyPin2(String pin2, Message onComplete) {
+ synchronized (mLock) {
+ if (mUiccApplication != null) {
+ mUiccApplication.supplyPin2(pin2, onComplete);
+ } else if (onComplete != null) {
+ Exception e = new RuntimeException("ICC card is absent.");
+ AsyncResult.forMessage(onComplete).exception = e;
+ onComplete.sendToTarget();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void supplyPuk2(String puk2, String newPin2, Message onComplete) {
+ synchronized (mLock) {
+ if (mUiccApplication != null) {
+ mUiccApplication.supplyPuk2(puk2, newPin2, onComplete);
+ } else if (onComplete != null) {
+ Exception e = new RuntimeException("ICC card is absent.");
+ AsyncResult.forMessage(onComplete).exception = e;
+ onComplete.sendToTarget();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void supplyNetworkDepersonalization(String pin, Message onComplete) {
+ synchronized (mLock) {
+ if (mUiccApplication != null) {
+ mUiccApplication.supplyNetworkDepersonalization(pin, onComplete);
+ } else if (onComplete != null) {
+ Exception e = new RuntimeException("CommandsInterface is not set.");
+ AsyncResult.forMessage(onComplete).exception = e;
+ onComplete.sendToTarget();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public boolean getIccLockEnabled() {
+ synchronized (mLock) {
+ /* defaults to false, if ICC is absent/deactivated */
+ return mUiccApplication != null && mUiccApplication.getIccLockEnabled();
+ }
+ }
+
+ @Override
+ public boolean getIccFdnEnabled() {
+ synchronized (mLock) {
+ return mUiccApplication != null && mUiccApplication.getIccFdnEnabled();
+ }
+ }
+
+ @Override
+ public boolean getIccPin2Blocked() {
+ /* defaults to disabled */
+ return mUiccApplication != null && mUiccApplication.getIccPin2Blocked();
+ }
+
+ @Override
+ public boolean getIccPuk2Blocked() {
+ /* defaults to disabled */
+ return mUiccApplication != null && mUiccApplication.getIccPuk2Blocked();
+ }
+
+ @Override
+ public void setIccLockEnabled(boolean enabled, String password, Message onComplete) {
+ synchronized (mLock) {
+ if (mUiccApplication != null) {
+ mUiccApplication.setIccLockEnabled(enabled, password, onComplete);
+ } else if (onComplete != null) {
+ Exception e = new RuntimeException("ICC card is absent.");
+ AsyncResult.forMessage(onComplete).exception = e;
+ onComplete.sendToTarget();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void setIccFdnEnabled(boolean enabled, String password, Message onComplete) {
+ synchronized (mLock) {
+ if (mUiccApplication != null) {
+ mUiccApplication.setIccFdnEnabled(enabled, password, onComplete);
+ } else if (onComplete != null) {
+ Exception e = new RuntimeException("ICC card is absent.");
+ AsyncResult.forMessage(onComplete).exception = e;
+ onComplete.sendToTarget();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void changeIccLockPassword(String oldPassword, String newPassword, Message onComplete) {
+ synchronized (mLock) {
+ if (mUiccApplication != null) {
+ mUiccApplication.changeIccLockPassword(oldPassword, newPassword, onComplete);
+ } else if (onComplete != null) {
+ Exception e = new RuntimeException("ICC card is absent.");
+ AsyncResult.forMessage(onComplete).exception = e;
+ onComplete.sendToTarget();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public void changeIccFdnPassword(String oldPassword, String newPassword, Message onComplete) {
+ synchronized (mLock) {
+ if (mUiccApplication != null) {
+ mUiccApplication.changeIccFdnPassword(oldPassword, newPassword, onComplete);
+ } else if (onComplete != null) {
+ Exception e = new RuntimeException("ICC card is absent.");
+ AsyncResult.forMessage(onComplete).exception = e;
+ onComplete.sendToTarget();
+ return;
+ }
+ }
+ }
+
+ @Override
+ public String getServiceProviderName() {
+ synchronized (mLock) {
+ if (mIccRecords != null) {
+ return mIccRecords.getServiceProviderName();
+ }
+ return null;
+ }
+ }
+
+ @Override
+ public boolean hasIccCard() {
+ synchronized (mLock) {
+ if (mUiccCard != null && mUiccCard.getCardState()
+ != IccCardStatus.CardState.CARDSTATE_ABSENT) {
+ return true;
+ }
+ loge("hasIccCard: UiccProfile is not null but UiccCard is null or card state is "
+ + "ABSENT");
+ return false;
}
}
@@ -134,6 +783,8 @@ public class UiccProfile {
mImsSubscriptionAppIndex = ics.mImsSubscriptionAppIndex;
mContext = c;
mCi = ci;
+ mTelephonyManager = (TelephonyManager) mContext.getSystemService(
+ Context.TELEPHONY_SERVICE);
//update applications
if (DBG) log(ics.mApplications.length + " applications");
@@ -141,7 +792,7 @@ public class UiccProfile {
if (mUiccApplications[i] == null) {
//Create newly added Applications
if (i < ics.mApplications.length) {
- mUiccApplications[i] = new UiccCardApplication(mUiccCard,
+ mUiccApplications[i] = new UiccCardApplication(this,
ics.mApplications[i], mContext, mCi);
}
} else if (i >= ics.mApplications.length) {
@@ -156,13 +807,18 @@ public class UiccProfile {
createAndUpdateCatServiceLocked();
- log("Before privilege rules: " + mCarrierPrivilegeRules);
- if (mCarrierPrivilegeRules == null) {
- mCarrierPrivilegeRules = new UiccCarrierPrivilegeRules(mUiccCard,
- mHandler.obtainMessage(EVENT_CARRIER_PRIVILEGES_LOADED));
+ // Reload the carrier privilege rules if necessary.
+ log("Before privilege rules: " + mCarrierPrivilegeRules + " : " + ics.mCardState);
+ if (mCarrierPrivilegeRules == null && ics.mCardState == CardState.CARDSTATE_PRESENT) {
+ mCarrierPrivilegeRules = new UiccCarrierPrivilegeRules(this,
+ obtainMessage(EVENT_CARRIER_PRIVILEGES_LOADED));
+ } else if (mCarrierPrivilegeRules != null
+ && ics.mCardState != CardState.CARDSTATE_PRESENT) {
+ mCarrierPrivilegeRules = null;
}
sanitizeApplicationIndexesLocked();
+ updateIccAvailability(true);
}
}
@@ -170,9 +826,9 @@ public class UiccProfile {
if (mUiccApplications.length > 0 && mUiccApplications[0] != null) {
// Initialize or Reinitialize CatService
if (mCatService == null) {
- mCatService = CatService.getInstance(mCi, mContext, mUiccCard, mPhoneId);
+ mCatService = CatService.getInstance(mCi, mContext, this, mPhoneId);
} else {
- mCatService.update(mCi, mContext, mUiccCard);
+ mCatService.update(mCi, mContext, this);
}
} else {
if (mCatService != null) {
@@ -203,6 +859,46 @@ public class UiccProfile {
checkIndexLocked(mImsSubscriptionAppIndex, AppType.APPTYPE_ISIM, null);
}
+ private boolean isSupportedApplication(UiccCardApplication app) {
+ if (app.getType() != AppType.APPTYPE_USIM && app.getType() != AppType.APPTYPE_CSIM
+ && app.getType() != AppType.APPTYPE_ISIM && app.getType() != AppType.APPTYPE_SIM
+ && app.getType() != AppType.APPTYPE_RUIM) {
+ return false;
+ }
+ return true;
+ }
+
+ private boolean areAllApplicationsReady() {
+ for (UiccCardApplication app : mUiccApplications) {
+ if (app != null && isSupportedApplication(app) && !app.isReady()) {
+ if (VDBG) log("areAllApplicationsReady: return false");
+ return false;
+ }
+ }
+ if (VDBG) {
+ log("areAllApplicationsReady: outside loop, return " + (mUiccApplications[0] != null));
+ }
+ // Returns false if there is no application in the UiccProfile.
+ return mUiccApplications[0] != null;
+ }
+
+ private boolean areAllRecordsLoaded() {
+ for (UiccCardApplication app : mUiccApplications) {
+ if (app != null && isSupportedApplication(app)) {
+ IccRecords ir = app.getIccRecords();
+ if (ir == null || !ir.isLoaded()) {
+ if (VDBG) log("areAllRecordsLoaded: return false");
+ return false;
+ }
+ }
+ }
+ if (VDBG) {
+ log("areAllRecordsLoaded: outside loop, return " + (mUiccApplications[0] != null));
+ }
+ // Returns false if there is no application in the UiccProfile.
+ return mUiccApplications[0] != null;
+ }
+
private int checkIndexLocked(int index, AppType expectedAppType, AppType altExpectedAppType) {
if (mUiccApplications == null || index >= mUiccApplications.length) {
loge("App index " + index + " is invalid since there are no applications");
@@ -255,32 +951,6 @@ public class UiccProfile {
}
}
- protected Handler mHandler = new Handler() {
- @Override
- public void handleMessage(Message msg) {
- switch (msg.what) {
- case EVENT_OPEN_LOGICAL_CHANNEL_DONE:
- case EVENT_CLOSE_LOGICAL_CHANNEL_DONE:
- case EVENT_TRANSMIT_APDU_LOGICAL_CHANNEL_DONE:
- case EVENT_TRANSMIT_APDU_BASIC_CHANNEL_DONE:
- case EVENT_SIM_IO_DONE:
- AsyncResult ar = (AsyncResult) msg.obj;
- if (ar.exception != null) {
- loglocal("Exception: " + ar.exception);
- log("Error in SIM access with exception" + ar.exception);
- }
- AsyncResult.forMessage((Message) ar.userObj, ar.result, ar.exception);
- ((Message) ar.userObj).sendToTarget();
- break;
- case EVENT_CARRIER_PRIVILEGES_LOADED:
- onCarrierPriviligesLoadedMessage();
- break;
- default:
- loge("Unknown Event " + msg.what);
- }
- }
- };
-
private boolean isPackageInstalled(String pkgName) {
PackageManager pm = mContext.getPackageManager();
try {
@@ -484,7 +1154,7 @@ public class UiccProfile {
loglocal("Open Logical Channel: " + aid + " , " + p2 + " by pid:" + Binder.getCallingPid()
+ " uid:" + Binder.getCallingUid());
mCi.iccOpenLogicalChannel(aid, p2,
- mHandler.obtainMessage(EVENT_OPEN_LOGICAL_CHANNEL_DONE, response));
+ obtainMessage(EVENT_OPEN_LOGICAL_CHANNEL_DONE, response));
}
/**
@@ -493,7 +1163,7 @@ public class UiccProfile {
public void iccCloseLogicalChannel(int channel, Message response) {
loglocal("Close Logical Channel: " + channel);
mCi.iccCloseLogicalChannel(channel,
- mHandler.obtainMessage(EVENT_CLOSE_LOGICAL_CHANNEL_DONE, response));
+ obtainMessage(EVENT_CLOSE_LOGICAL_CHANNEL_DONE, response));
}
/**
@@ -502,7 +1172,7 @@ public class UiccProfile {
public void iccTransmitApduLogicalChannel(int channel, int cla, int command,
int p1, int p2, int p3, String data, Message response) {
mCi.iccTransmitApduLogicalChannel(channel, cla, command, p1, p2, p3,
- data, mHandler.obtainMessage(EVENT_TRANSMIT_APDU_LOGICAL_CHANNEL_DONE, response));
+ data, obtainMessage(EVENT_TRANSMIT_APDU_LOGICAL_CHANNEL_DONE, response));
}
/**
@@ -511,7 +1181,7 @@ public class UiccProfile {
public void iccTransmitApduBasicChannel(int cla, int command,
int p1, int p2, int p3, String data, Message response) {
mCi.iccTransmitApduBasicChannel(cla, command, p1, p2, p3,
- data, mHandler.obtainMessage(EVENT_TRANSMIT_APDU_BASIC_CHANNEL_DONE, response));
+ data, obtainMessage(EVENT_TRANSMIT_APDU_BASIC_CHANNEL_DONE, response));
}
/**
@@ -520,7 +1190,7 @@ public class UiccProfile {
public void iccExchangeSimIO(int fileID, int command, int p1, int p2, int p3,
String pathID, Message response) {
mCi.iccIO(command, fileID, pathID, p1, p2, p3, null, null,
- mHandler.obtainMessage(EVENT_SIM_IO_DONE, response));
+ obtainMessage(EVENT_SIM_IO_DONE, response));
}
/**
@@ -631,7 +1301,7 @@ public class UiccProfile {
*/
public boolean setOperatorBrandOverride(String brand) {
log("setOperatorBrandOverride: " + brand);
- log("current iccId: " + getIccId());
+ log("current iccId: " + SubscriptionInfo.givePrintableIccid(getIccId()));
String iccId = getIccId();
if (TextUtils.isEmpty(iccId)) {
@@ -658,7 +1328,21 @@ public class UiccProfile {
return null;
}
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
- return sp.getString(OPERATOR_BRAND_OVERRIDE_PREFIX + iccId, null);
+ String brandName = sp.getString(OPERATOR_BRAND_OVERRIDE_PREFIX + iccId, null);
+ if (brandName == null) {
+ // Check if CarrierConfig sets carrier name
+ CarrierConfigManager manager = (CarrierConfigManager)
+ mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE);
+ int subId = SubscriptionController.getInstance().getSubIdUsingPhoneId(mPhoneId);
+ if (manager != null) {
+ PersistableBundle bundle = manager.getConfigForSubId(subId);
+ if (bundle != null && bundle.getBoolean(
+ CarrierConfigManager.KEY_CARRIER_NAME_OVERRIDE_BOOL)) {
+ brandName = bundle.getString(CarrierConfigManager.KEY_CARRIER_NAME_STRING);
+ }
+ }
+ }
+ return brandName;
}
/**
@@ -744,6 +1428,21 @@ public class UiccProfile {
+ ((Registrant) mCarrierPrivilegeRegistrants.get(i)).getHandler());
}
pw.flush();
+
+ if (ICC_CARD_PROXY_REMOVED) {
+ pw.println(" mNetworkLockedRegistrants: size=" + mNetworkLockedRegistrants.size());
+ for (int i = 0; i < mNetworkLockedRegistrants.size(); i++) {
+ pw.println(" mNetworkLockedRegistrants[" + i + "]="
+ + ((Registrant) mNetworkLockedRegistrants.get(i)).getHandler());
+ }
+ pw.println(" mCurrentAppType=" + mCurrentAppType);
+ pw.println(" mUiccCard=" + mUiccCard);
+ pw.println(" mUiccApplication=" + mUiccApplication);
+ pw.println(" mIccRecords=" + mIccRecords);
+ pw.println(" mExternalState=" + mExternalState);
+ pw.flush();
+ }
+
pw.println("sLocalLog:");
sLocalLog.dump(fd, pw, args);
pw.flush();
diff --git a/com/android/internal/telephony/uicc/UiccSlot.java b/com/android/internal/telephony/uicc/UiccSlot.java
index 8102b9b2..9319b4b1 100644
--- a/com/android/internal/telephony/uicc/UiccSlot.java
+++ b/com/android/internal/telephony/uicc/UiccSlot.java
@@ -57,6 +57,8 @@ public class UiccSlot extends Handler {
private CommandsInterface mCi;
private UiccCard mUiccCard;
private RadioState mLastRadioState = RadioState.RADIO_UNAVAILABLE;
+ private boolean mIsEuicc;
+ private String mIccId;
private RegistrantList mAbsentRegistrants = new RegistrantList();
@@ -75,12 +77,12 @@ public class UiccSlot extends Handler {
/**
* 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) {
+ public void update(CommandsInterface ci, IccCardStatus ics, int phoneId) {
synchronized (mLock) {
CardState oldState = mCardState;
mCardState = ics.mCardState;
+ parseAtr(ics.atr);
mCi = ci;
- mContext = c;
RadioState radioState = mCi.getRadioState();
if (DBG) {
@@ -96,6 +98,8 @@ public class UiccSlot extends Handler {
sendMessage(obtainMessage(EVENT_CARD_REMOVED, null));
}
+ // todo: broadcast sim state changed for absent/unknown when IccCardProxy is removed
+
// no card present in the slot now; dispose card and make mUiccCard null
mUiccCard.dispose();
mUiccCard = null;
@@ -113,7 +117,12 @@ public class UiccSlot extends Handler {
mUiccCard.dispose();
}
- mUiccCard = new UiccCard(mContext, mCi, ics, phoneId);
+ if (!mIsEuicc) {
+ mUiccCard = new UiccCard(mContext, mCi, ics, phoneId);
+ } else {
+ // todo: initialize new EuiccCard object here
+ //mUiccCard = new EuiccCard();
+ }
} else {
if (mUiccCard != null) {
mUiccCard.update(mContext, mCi, ics);
@@ -123,6 +132,57 @@ public class UiccSlot extends Handler {
}
}
+ /**
+ * Update slot based on IccSlotStatus.
+ */
+ public void update(CommandsInterface ci, IccSlotStatus iss) {
+ log("slotStatus update");
+ synchronized (mLock) {
+ mCi = ci;
+ if (iss.slotState == IccSlotStatus.SlotState.SLOTSTATE_INACTIVE) {
+ if (mActive) {
+ mActive = false;
+ // treat as radio state unavailable
+ onRadioStateUnavailable();
+ }
+ parseAtr(iss.atr);
+ mCardState = iss.cardState;
+ mIccId = iss.iccid;
+ } else if (!mActive && iss.slotState == IccSlotStatus.SlotState.SLOTSTATE_ACTIVE) {
+ mActive = true;
+ // todo - ignoring these fields for now; relying on sim state changed to update
+ // these
+ // iss.atr;
+ // iss.cardState;
+ // iss.iccid;
+ // iss.logicalSlotIndex;
+ }
+ }
+ }
+
+ private void parseAtr(String atr) {
+ // todo - parse atr and set mIsEuicc based on it
+ mIsEuicc = false;
+ }
+
+ public boolean isEuicc() {
+ return mIsEuicc;
+ }
+
+ public boolean isActive() {
+ return mActive;
+ }
+
+ public String getIccId() {
+ if (mIccId != null) {
+ return mIccId;
+ } else if (mUiccCard != null) {
+ return mUiccCard.getIccId();
+ } else {
+ return null;
+ }
+ }
+
@Override
protected void finalize() {
if (DBG) log("UiccSlot finalized");
@@ -243,6 +303,9 @@ public class UiccSlot extends Handler {
mUiccCard.dispose();
}
mUiccCard = null;
+
+ // todo: broadcast sim state changed for absent/unknown when IccCardProxy is removed
+
mCardState = CardState.CARDSTATE_ABSENT;
mLastRadioState = RadioState.RADIO_UNAVAILABLE;
}
diff --git a/com/android/internal/telephony/uicc/UiccStateChangedLauncher.java b/com/android/internal/telephony/uicc/UiccStateChangedLauncher.java
index a8186419..3ef421a8 100644
--- a/com/android/internal/telephony/uicc/UiccStateChangedLauncher.java
+++ b/com/android/internal/telephony/uicc/UiccStateChangedLauncher.java
@@ -65,10 +65,10 @@ public class UiccStateChangedLauncher extends Handler {
mIsRestricted = new boolean[TelephonyManager.getDefault().getPhoneCount()];
shouldNotify = true;
}
- UiccSlot[] uiccSlots = mUiccController.getUiccSlots();
- for (int i = 0; uiccSlots != null && i < uiccSlots.length; ++i) {
+ for (int i = 0; i < mIsRestricted.length; ++i) {
// Update only if restricted state changes.
- UiccCard uiccCard = uiccSlots[i].getUiccCard();
+
+ UiccCard uiccCard = mUiccController.getUiccCardForPhone(i);
if ((uiccCard == null
|| uiccCard.getCardState() != CardState.CARDSTATE_RESTRICTED)
!= mIsRestricted[i]) {
diff --git a/com/android/internal/telephony/uicc/euicc/EuiccCard.java b/com/android/internal/telephony/uicc/euicc/EuiccCard.java
new file mode 100644
index 00000000..7949e1ad
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/EuiccCard.java
@@ -0,0 +1,1101 @@
+/*
+ * 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 com.android.internal.telephony.uicc.euicc;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Handler;
+import android.service.carrier.CarrierIdentifier;
+import android.service.euicc.EuiccProfileInfo;
+import android.telephony.Rlog;
+import android.telephony.SubscriptionInfo;
+import android.telephony.UiccAccessRule;
+import android.telephony.euicc.EuiccCardManager;
+import android.telephony.euicc.EuiccNotification;
+import android.telephony.euicc.EuiccRulesAuthTable;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.PhoneFactory;
+import com.android.internal.telephony.uicc.IccCardStatus;
+import com.android.internal.telephony.uicc.IccUtils;
+import com.android.internal.telephony.uicc.UiccCard;
+import com.android.internal.telephony.uicc.asn1.Asn1Decoder;
+import com.android.internal.telephony.uicc.asn1.Asn1Node;
+import com.android.internal.telephony.uicc.asn1.InvalidAsn1DataException;
+import com.android.internal.telephony.uicc.asn1.TagNotFoundException;
+import com.android.internal.telephony.uicc.euicc.EuiccCardErrorException.OperationCode;
+import com.android.internal.telephony.uicc.euicc.apdu.ApduException;
+import com.android.internal.telephony.uicc.euicc.apdu.ApduSender;
+import com.android.internal.telephony.uicc.euicc.apdu.RequestBuilder;
+import com.android.internal.telephony.uicc.euicc.apdu.RequestProvider;
+import com.android.internal.telephony.uicc.euicc.async.AsyncResultCallback;
+import com.android.internal.telephony.uicc.euicc.async.AsyncResultHelper;
+
+import java.util.List;
+
+/**
+ * This represents an eUICC card to perform profile management operations asynchronously. This class
+ * includes methods defined by different versions of GSMA Spec (SGP.22).
+ */
+public class EuiccCard extends UiccCard {
+ private static final String LOG_TAG = "EuiccCard";
+ private static final boolean DBG = true;
+
+ private static final String ISD_R_AID = "A0000005591010FFFFFFFF8900000100";
+ private static final int ICCID_LENGTH = 20;
+
+ // APDU status for SIM refresh
+ private static final int APDU_ERROR_SIM_REFRESH = 0x6F00;
+
+ // These error codes are defined in GSMA SGP.22. 0 is the code for success.
+ private static final int CODE_OK = 0;
+
+ // Error code for profile not in expected state for the operation. This error includes the case
+ // that profile is not in disabled state when being enabled or deleted, and that profile is not
+ // in enabled state when being disabled.
+ private static final int CODE_PROFILE_NOT_IN_EXPECTED_STATE = 2;
+
+ // Error code for nothing to delete when resetting eUICC memory or removing notifications.
+ private static final int CODE_NOTHING_TO_DELETE = 1;
+
+ // Error code for no result available when retrieving notifications.
+ private static final int CODE_NO_RESULT_AVAILABLE = 1;
+
+ private static final EuiccSpecVersion SGP_2_0 = new EuiccSpecVersion(2, 0, 0);
+
+ // These interfaces are used for simplifying the code by leveraging lambdas.
+ private interface ApduRequestBuilder {
+ void build(RequestBuilder requestBuilder)
+ throws EuiccCardException, TagNotFoundException, InvalidAsn1DataException;
+ }
+
+ private interface ApduResponseHandler<T> {
+ T handleResult(byte[] response)
+ throws EuiccCardException, TagNotFoundException, InvalidAsn1DataException;
+ }
+
+ private interface ApduExceptionHandler {
+ void handleException(Throwable e);
+ }
+
+ private final ApduSender mApduSender;
+ private final Object mLock = new Object();
+ private EuiccSpecVersion mSpecVersion;
+ private String mEid;
+
+ public EuiccCard(Context c, CommandsInterface ci, IccCardStatus ics, int phoneId) {
+ super(c, ci, ics, phoneId);
+ // TODO: Set supportExtendedApdu based on ATR.
+ mApduSender = new ApduSender(ci, ISD_R_AID, false /* supportExtendedApdu */);
+ }
+
+ /**
+ * Gets the GSMA RSP specification version supported by this eUICC. This may return null if the
+ * version cannot be read.
+ */
+ public void getSpecVersion(AsyncResultCallback<EuiccSpecVersion> callback, Handler handler) {
+ if (mSpecVersion != null) {
+ AsyncResultHelper.returnResult(mSpecVersion, callback, handler);
+ return;
+ }
+
+ sendApdu(newRequestProvider((RequestBuilder requestBuilder) -> { /* Do nothing */ }),
+ (byte[] response) -> mSpecVersion, callback, handler);
+ }
+
+ /**
+ * Gets a list of user-visible profiles.
+ *
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 1.1.0 [GSMA SGP.22]
+ */
+ public void getAllProfiles(AsyncResultCallback<EuiccProfileInfo[]> callback, Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_PROFILES)
+ .addChildAsBytes(Tags.TAG_TAG_LIST, Tags.EUICC_PROFILE_TAGS)
+ .build().toHex())),
+ (byte[] response) -> {
+ List<Asn1Node> profileNodes = new Asn1Decoder(response).nextNode()
+ .getChild(Tags.TAG_CTX_COMP_0).getChildren(Tags.TAG_PROFILE_INFO);
+ int size = profileNodes.size();
+ EuiccProfileInfo[] profiles = new EuiccProfileInfo[size];
+ int profileCount = 0;
+ for (int i = 0; i < size; i++) {
+ Asn1Node profileNode = profileNodes.get(i);
+ if (!profileNode.hasChild(Tags.TAG_ICCID)) {
+ loge("Profile must have an ICCID.");
+ continue;
+ }
+ EuiccProfileInfo.Builder profileBuilder = new EuiccProfileInfo.Builder();
+ buildProfile(profileNode, profileBuilder);
+
+ EuiccProfileInfo profile = profileBuilder.build();
+ profiles[profileCount++] = profile;
+ }
+ return profiles;
+ },
+ callback, handler);
+ }
+
+ /**
+ * Gets a profile.
+ *
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 1.1.0 [GSMA SGP.22]
+ */
+ public final void getProfile(String iccid, AsyncResultCallback<EuiccProfileInfo> callback,
+ Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_PROFILES)
+ .addChildAsBytes(Tags.TAG_ICCID, IccUtils.bcdToBytes(iccid))
+ .addChildAsBytes(Tags.TAG_TAG_LIST, Tags.EUICC_PROFILE_TAGS)
+ .build().toHex())),
+ (byte[] response) -> {
+ List<Asn1Node> profileNodes = new Asn1Decoder(response).nextNode()
+ .getChild(Tags.TAG_CTX_COMP_0).getChildren(Tags.TAG_PROFILE_INFO);
+ if (profileNodes.isEmpty()) {
+ return null;
+ }
+ Asn1Node profileNode = profileNodes.get(0);
+ EuiccProfileInfo.Builder profileBuilder = new EuiccProfileInfo.Builder();
+ buildProfile(profileNode, profileBuilder);
+ return profileBuilder.build();
+ },
+ callback, handler);
+ }
+
+ /**
+ * Disables a profile of the given {@code iccid}.
+ *
+ * @param refresh Whether sending the REFRESH command to modem.
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 1.1.0 [GSMA SGP.22]
+ */
+ public void disableProfile(String iccid, boolean refresh, AsyncResultCallback<Void> callback,
+ Handler handler) {
+ sendApduWithSimResetErrorWorkaround(
+ newRequestProvider((RequestBuilder requestBuilder) -> {
+ byte[] iccidBytes = IccUtils.bcdToBytes(padTrailingFs(iccid));
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_DISABLE_PROFILE)
+ .addChild(Asn1Node.newBuilder(Tags.TAG_CTX_COMP_0)
+ .addChildAsBytes(Tags.TAG_ICCID, iccidBytes))
+ .addChildAsBoolean(Tags.TAG_CTX_1, refresh)
+ .build().toHex());
+ }),
+ (byte[] response) -> {
+ int result;
+ // SGP.22 v2.0 DisableProfileResponse
+ result = parseSimpleResult(response);
+ switch (result) {
+ case CODE_OK:
+ return null;
+ case CODE_PROFILE_NOT_IN_EXPECTED_STATE:
+ logd("Profile is already disabled, iccid: "
+ + SubscriptionInfo.givePrintableIccid(iccid));
+ return null;
+ default:
+ throw new EuiccCardErrorException(
+ EuiccCardErrorException.OPERATION_DISABLE_PROFILE, result);
+ }
+ },
+ callback, handler);
+ }
+
+ /**
+ * Switches from the current profile to another profile. The current profile will be disabled
+ * and the specified profile will be enabled.
+ *
+ * @param refresh Whether sending the REFRESH command to modem.
+ * @param callback The callback to get the EuiccProfile enabled.
+ * @param handler The handler to run the callback.
+ * @since 1.1.0 [GSMA SGP.22]
+ */
+ public void switchToProfile(String iccid, boolean refresh, AsyncResultCallback<Void> callback,
+ Handler handler) {
+ sendApduWithSimResetErrorWorkaround(
+ newRequestProvider((RequestBuilder requestBuilder) -> {
+ byte[] iccidBytes = IccUtils.bcdToBytes(padTrailingFs(iccid));
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_ENABLE_PROFILE)
+ .addChild(Asn1Node.newBuilder(Tags.TAG_CTX_COMP_0)
+ .addChildAsBytes(Tags.TAG_ICCID, iccidBytes))
+ .addChildAsBoolean(Tags.TAG_CTX_1, refresh)
+ .build().toHex());
+ }),
+ (byte[] response) -> {
+ int result;
+ // SGP.22 v2.0 EnableProfileResponse
+ result = parseSimpleResult(response);
+ switch (result) {
+ case CODE_OK:
+ return null;
+ case CODE_PROFILE_NOT_IN_EXPECTED_STATE:
+ logd("Profile is already enabled, iccid: "
+ + SubscriptionInfo.givePrintableIccid(iccid));
+ return null;
+ default:
+ throw new EuiccCardErrorException(
+ EuiccCardErrorException.OPERATION_SWITCH_TO_PROFILE, result);
+ }
+ },
+ callback, handler);
+ }
+
+ /**
+ * Gets the EID of the eUICC.
+ *
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 1.1.0 [GSMA SGP.22]
+ */
+ public void getEid(AsyncResultCallback<String> callback, Handler handler) {
+ if (mEid != null) {
+ AsyncResultHelper.returnResult(mEid, callback, handler);
+ return;
+ }
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_EID)
+ .addChildAsBytes(Tags.TAG_TAG_LIST, new byte[] {Tags.TAG_EID})
+ .build().toHex())),
+ (byte[] response) -> {
+ String eid = IccUtils.bytesToHexString(parseResponse(response)
+ .getChild(Tags.TAG_EID).asBytes());
+ synchronized (mLock) {
+ mEid = eid;
+ }
+ return eid;
+ },
+ callback, handler);
+ }
+
+ /**
+ * Sets the nickname of a profile.
+ *
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 1.1.0 [GSMA SGP.22]
+ */
+ public void setNickname(String iccid, String nickname, AsyncResultCallback<Void> callback,
+ Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_SET_NICKNAME)
+ .addChildAsBytes(Tags.TAG_ICCID,
+ IccUtils.bcdToBytes(padTrailingFs(iccid)))
+ .addChildAsString(Tags.TAG_NICKNAME, nickname)
+ .build().toHex())),
+ (byte[] response) -> {
+ // SGP.22 v2.0 SetNicknameResponse
+ int result = parseSimpleResult(response);
+ if (result != CODE_OK) {
+ throw new EuiccCardErrorException(
+ EuiccCardErrorException.OPERATION_SET_NICKNAME, result);
+ }
+ return null;
+ },
+ callback, handler);
+ }
+
+ /**
+ * Deletes a profile from eUICC.
+ *
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 1.1.0 [GSMA SGP.22]
+ */
+ public void deleteProfile(String iccid, AsyncResultCallback<Void> callback, Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) -> {
+ byte[] iccidBytes = IccUtils.bcdToBytes(padTrailingFs(iccid));
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_DELETE_PROFILE)
+ .addChildAsBytes(Tags.TAG_ICCID, iccidBytes)
+ .build().toHex());
+ }),
+ (byte[] response) -> {
+ // SGP.22 v2.0 DeleteProfileRequest
+ int result = parseSimpleResult(response);
+ if (result != CODE_OK) {
+ throw new EuiccCardErrorException(
+ EuiccCardErrorException.OPERATION_DELETE_PROFILE, result);
+ }
+ return null;
+ },
+ callback, handler);
+ }
+
+ /**
+ * Resets the eUICC memory (e.g., remove all profiles).
+ *
+ * @param options Bits of the options of resetting which parts of the eUICC memory.
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 1.1.0 [GSMA SGP.22]
+ */
+ public void resetMemory(@EuiccCardManager.ResetOption int options,
+ AsyncResultCallback<Void> callback, Handler handler) {
+ sendApduWithSimResetErrorWorkaround(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_EUICC_MEMORY_RESET)
+ .addChildAsBits(Tags.TAG_CTX_2, options)
+ .build().toHex())),
+ (byte[] response) -> {
+ int result = parseSimpleResult(response);
+ if (result != CODE_OK && result != CODE_NOTHING_TO_DELETE) {
+ throw new EuiccCardErrorException(
+ EuiccCardErrorException.OPERATION_RESET_MEMORY, result);
+ }
+ return null;
+ },
+ callback, handler);
+ }
+
+ /**
+ * Gets the default SM-DP+ address from eUICC.
+ *
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void getDefaultSmdpAddress(AsyncResultCallback<String> callback, Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(
+ Asn1Node.newBuilder(Tags.TAG_GET_CONFIGURED_ADDRESSES)
+ .build().toHex())),
+ (byte[] response) -> parseResponse(response).getChild(Tags.TAG_CTX_0).asString(),
+ callback, handler);
+ }
+
+ /**
+ * Gets the SM-DS address from eUICC.
+ *
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void getSmdsAddress(AsyncResultCallback<String> callback, Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(
+ Asn1Node.newBuilder(Tags.TAG_GET_CONFIGURED_ADDRESSES)
+ .build().toHex())),
+ (byte[] response) -> parseResponse(response).getChild(Tags.TAG_CTX_1).asString(),
+ callback, handler);
+ }
+
+ /**
+ * Sets the default SM-DP+ address of eUICC.
+ *
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void setDefaultSmdpAddress(String defaultSmdpAddress, AsyncResultCallback<Void> callback,
+ Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(
+ Asn1Node.newBuilder(Tags.TAG_SET_DEFAULT_SMDP_ADDRESS)
+ .addChildAsString(Tags.TAG_CTX_0, defaultSmdpAddress)
+ .build().toHex())),
+ (byte[] response) -> {
+ // SGP.22 v2.0 SetDefaultDpAddressResponse
+ int result = parseSimpleResult(response);
+ if (result != CODE_OK) {
+ throw new EuiccCardErrorException(
+ EuiccCardErrorException.OPERATION_SET_DEFAULT_SMDP_ADDRESS, result);
+ }
+ return null;
+ },
+ callback, handler);
+ }
+
+ /**
+ * Gets Rules Authorisation Table.
+ *
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void getRulesAuthTable(AsyncResultCallback<EuiccRulesAuthTable> callback,
+ Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_RAT)
+ .build().toHex())),
+ (byte[] response) -> {
+ Asn1Node root = parseResponse(response);
+ List<Asn1Node> nodes = root.getChildren(Tags.TAG_CTX_COMP_0);
+ EuiccRulesAuthTable.Builder builder =
+ new EuiccRulesAuthTable.Builder(nodes.size());
+ int size = nodes.size();
+ for (int i = 0; i < size; i++) {
+ Asn1Node node = nodes.get(i);
+ List<Asn1Node> opIdNodes = node.getChild(Tags.TAG_CTX_COMP_1).getChildren();
+ int opIdSize = opIdNodes.size();
+ CarrierIdentifier[] opIds = new CarrierIdentifier[opIdSize];
+ for (int j = 0; j < opIdSize; j++) {
+ opIds[j] = buildCarrierIdentifier(opIdNodes.get(j));
+ }
+ builder.add(node.getChild(Tags.TAG_CTX_0).asBits(), opIds,
+ node.getChild(Tags.TAG_CTX_2).asBits());
+ }
+ return builder.build();
+ },
+ callback, handler);
+ }
+
+ /**
+ * Gets the eUICC challenge for new profile downloading.
+ *
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void getEuiccChallenge(AsyncResultCallback<byte[]> callback, Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(
+ Asn1Node.newBuilder(Tags.TAG_GET_EUICC_CHALLENGE)
+ .build().toHex())),
+ (byte[] response) -> parseResponse(response).getChild(Tags.TAG_CTX_0).asBytes(),
+ callback, handler);
+ }
+
+ /**
+ * Gets the eUICC info1 for new profile downloading.
+ *
+ * @param callback The callback to get the result, which represents an {@code EUICCInfo1}
+ * defined in GSMA RSP v2.0+.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void getEuiccInfo1(AsyncResultCallback<byte[]> callback, Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_EUICC_INFO_1)
+ .build().toHex())),
+ (response) -> response,
+ callback, handler);
+ }
+
+ /**
+ * Gets the eUICC info2 for new profile downloading.
+ *
+ * @param callback The callback to get the result, which represents an {@code EUICCInfo2}
+ * defined in GSMA RSP v2.0+.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void getEuiccInfo2(AsyncResultCallback<byte[]> callback, Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_GET_EUICC_INFO_2)
+ .build().toHex())),
+ (response) -> response,
+ callback, handler);
+ }
+
+ /**
+ * Authenticates the SM-DP+ server by the eUICC. The parameters {@code serverSigned1}, {@code
+ * serverSignature1}, {@code euiccCiPkIdToBeUsed}, and {@code serverCertificate} are the ASN.1
+ * data returned by SM-DP+ server.
+ *
+ * @param matchingId The activation code or an empty string.
+ * @param callback The callback to get the result, which represents an {@code
+ * AuthenticateServerResponse} defined in GSMA RSP v2.0+.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void authenticateServer(String matchingId, byte[] serverSigned1, byte[] serverSignature1,
+ byte[] euiccCiPkIdToBeUsed, byte[] serverCertificate,
+ AsyncResultCallback<byte[]> callback, Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) -> {
+ byte[] imeiBytes = getDeviceId();
+ // TAC is the first 8 digits (4 bytes) of IMEI.
+ byte[] tacBytes = new byte[4];
+ System.arraycopy(imeiBytes, 0, tacBytes, 0, 4);
+
+ // TODO: Get device capabilities.
+ Asn1Node.Builder devCapsBuilder = Asn1Node.newBuilder(Tags.TAG_CTX_COMP_1);
+
+ Asn1Node.Builder ctxParams1Builder = Asn1Node.newBuilder(Tags.TAG_CTX_COMP_0)
+ .addChildAsString(Tags.TAG_CTX_0, matchingId)
+ .addChild(Asn1Node.newBuilder(Tags.TAG_CTX_COMP_1)
+ .addChildAsBytes(Tags.TAG_CTX_0, tacBytes)
+ .addChild(devCapsBuilder)
+ .addChildAsBytes(Tags.TAG_CTX_2, imeiBytes));
+
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_AUTHENTICATE_SERVER)
+ .addChild(new Asn1Decoder(serverSigned1).nextNode())
+ .addChild(new Asn1Decoder(serverSignature1).nextNode())
+ .addChild(new Asn1Decoder(euiccCiPkIdToBeUsed).nextNode())
+ .addChild(new Asn1Decoder(serverCertificate).nextNode())
+ .addChild(ctxParams1Builder)
+ .build().toHex());
+ }),
+ (byte[] response) ->
+ parseResponseAndCheckSimpleError(response,
+ EuiccCardErrorException.OPERATION_AUTHENTICATE_SERVER).toBytes(),
+ callback, handler);
+ }
+
+ /**
+ * Prepares the profile download request sent to SM-DP+. The parameters {@code smdpSigned2},
+ * {@code smdpSignature2}, and {@code smdpCertificate} are the ASN.1 data returned by SM-DP+
+ * server.
+ *
+ * @param hashCc The hash of confirmation code. It can be null if there is no confirmation code
+ * required.
+ * @param callback The callback to get the result, which represents an {@code
+ * PrepareDownloadResponse} defined in GSMA RSP v2.0+.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void prepareDownload(@Nullable byte[] hashCc, byte[] smdpSigned2, byte[] smdpSignature2,
+ byte[] smdpCertificate, AsyncResultCallback<byte[]> callback, Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) -> {
+ Asn1Node.Builder builder = Asn1Node.newBuilder(Tags.TAG_PREPARE_DOWNLOAD)
+ .addChild(new Asn1Decoder(smdpSigned2).nextNode())
+ .addChild(new Asn1Decoder(smdpSignature2).nextNode());
+ if (hashCc != null) {
+ builder.addChildAsBytes(Tags.TAG_UNI_4, hashCc);
+ }
+ requestBuilder.addStoreData(
+ builder.addChild(new Asn1Decoder(smdpCertificate).nextNode())
+ .build().toHex());
+ }),
+ (byte[] response) -> {
+ Asn1Node root = parseResponse(response);
+ if (root.hasChild(Tags.TAG_CTX_COMP_1, Tags.TAG_UNI_2)) {
+ throw new EuiccCardErrorException(
+ EuiccCardErrorException.OPERATION_PREPARE_DOWNLOAD,
+ root.getChild(Tags.TAG_CTX_COMP_1, Tags.TAG_UNI_2).asInteger());
+ }
+ return root.toBytes();
+ },
+ callback, handler);
+ }
+
+ /**
+ * Loads a downloaded bound profile package onto the eUICC.
+ *
+ * @param boundProfilePackage The Bound Profile Package data returned by SM-DP+ server.
+ * @param callback The callback to get the result, which represents an {@code
+ * LoadBoundProfilePackageResponse} defined in GSMA RSP v2.0+.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void loadBoundProfilePackage(byte[] boundProfilePackage,
+ AsyncResultCallback<byte[]> callback, Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) -> {
+ Asn1Node bppNode = new Asn1Decoder(boundProfilePackage).nextNode();
+ // initialiseSecureChannelRequest (ES8+.InitialiseSecureChannel)
+ Asn1Node initialiseSecureChannelRequest = bppNode.getChild(
+ Tags.TAG_INITIALISE_SECURE_CHANNEL);
+ // firstSequenceOf87 (ES8+.ConfigureISDP)
+ Asn1Node firstSequenceOf87 = bppNode.getChild(Tags.TAG_CTX_COMP_0);
+ // sequenceOf88 (ES8+.StoreMetadata)
+ Asn1Node sequenceOf88 = bppNode.getChild(Tags.TAG_CTX_COMP_1);
+ List<Asn1Node> metaDataSeqs = sequenceOf88.getChildren(Tags.TAG_CTX_8);
+ // sequenceOf86 (ES8+.LoadProfileElements #1)
+ Asn1Node sequenceOf86 = bppNode.getChild(Tags.TAG_CTX_COMP_3);
+ List<Asn1Node> elementSeqs = sequenceOf86.getChildren(Tags.TAG_CTX_6);
+
+ requestBuilder.addStoreData(bppNode.getHeadAsHex()
+ + initialiseSecureChannelRequest.toHex());
+
+ requestBuilder.addStoreData(firstSequenceOf87.toHex());
+
+ requestBuilder.addStoreData(sequenceOf88.getHeadAsHex());
+ int size = metaDataSeqs.size();
+ for (int i = 0; i < size; i++) {
+ requestBuilder.addStoreData(metaDataSeqs.get(i).toHex());
+ }
+
+ if (bppNode.hasChild(Tags.TAG_CTX_COMP_2)) {
+ requestBuilder.addStoreData(bppNode.getChild(Tags.TAG_CTX_COMP_2).toHex());
+ }
+
+ requestBuilder.addStoreData(sequenceOf86.getHeadAsHex());
+ size = elementSeqs.size();
+ for (int i = 0; i < size; i++) {
+ requestBuilder.addStoreData(elementSeqs.get(i).toHex());
+ }
+ }),
+ (byte[] response) -> {
+ // SGP.22 v2.0 ErrorResult
+ Asn1Node root = parseResponse(response);
+ if (root.hasChild(Tags.TAG_PROFILE_INSTALLATION_RESULT_DATA,
+ Tags.TAG_CTX_COMP_2, Tags.TAG_CTX_COMP_1, Tags.TAG_CTX_1)) {
+ Asn1Node errorNode = root.getChild(
+ Tags.TAG_PROFILE_INSTALLATION_RESULT_DATA, Tags.TAG_CTX_COMP_2,
+ Tags.TAG_CTX_COMP_1, Tags.TAG_CTX_1);
+ throw new EuiccCardErrorException(
+ EuiccCardErrorException.OPERATION_LOAD_BOUND_PROFILE_PACKAGE,
+ errorNode.asInteger(), errorNode);
+ }
+ return root.toBytes();
+ },
+ callback, handler);
+ }
+
+ /**
+ * Cancels the current profile download session.
+ *
+ * @param transactionId The transaction ID returned by SM-DP+ server.
+ * @param callback The callback to get the result, which represents an {@code
+ * CancelSessionResponse} defined in GSMA RSP v2.0+.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void cancelSession(byte[] transactionId, @EuiccCardManager.CancelReason int reason,
+ AsyncResultCallback<byte[]> callback, Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_CANCEL_SESSION)
+ .addChildAsBytes(Tags.TAG_CTX_0, transactionId)
+ .addChildAsInteger(Tags.TAG_CTX_1, reason)
+ .build().toHex())),
+ (byte[] response) ->
+ parseResponseAndCheckSimpleError(response,
+ EuiccCardErrorException.OPERATION_CANCEL_SESSION).toBytes(),
+ callback, handler);
+ }
+
+ /**
+ * Lists all notifications of the given {@code notificationEvents}.
+ *
+ * @param events Bits of the event types ({@link EuiccNotification.Event}) to list.
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void listNotifications(@EuiccNotification.Event int events,
+ AsyncResultCallback<EuiccNotification[]> callback, Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(Asn1Node.newBuilder(Tags.TAG_LIST_NOTIFICATION)
+ .addChildAsBits(Tags.TAG_CTX_1, events)
+ .build().toHex())),
+ (byte[] response) -> {
+ Asn1Node root = parseResponseAndCheckSimpleError(response,
+ EuiccCardErrorException.OPERATION_LIST_NOTIFICATIONS);
+ List<Asn1Node> nodes = root.getChild(Tags.TAG_CTX_COMP_0).getChildren();
+ EuiccNotification[] notifications = new EuiccNotification[nodes.size()];
+ for (int i = 0; i < notifications.length; ++i) {
+ notifications[i] = createNotification(nodes.get(i));
+ }
+ return notifications;
+ },
+ callback, handler);
+ }
+
+ /**
+ * Retrieves contents of all notification of the given {@code events}.
+ *
+ * @param events Bits of the event types ({@link EuiccNotification.Event}) to list.
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void retrieveNotificationList(@EuiccNotification.Event int events,
+ AsyncResultCallback<EuiccNotification[]> callback, Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(
+ Asn1Node.newBuilder(Tags.TAG_RETRIEVE_NOTIFICATIONS_LIST)
+ .addChild(Asn1Node.newBuilder(Tags.TAG_CTX_COMP_0)
+ .addChildAsBits(Tags.TAG_CTX_1, events))
+ .build().toHex())),
+ (byte[] response) -> {
+ Asn1Node root = parseResponse(response);
+ if (root.hasChild(Tags.TAG_CTX_1)) {
+ // SGP.22 v2.0 RetrieveNotificationsListResponse
+ int error = root.getChild(Tags.TAG_CTX_1).asInteger();
+ switch (error) {
+ case CODE_NO_RESULT_AVAILABLE:
+ return new EuiccNotification[0];
+ default:
+ throw new EuiccCardErrorException(
+ EuiccCardErrorException.OPERATION_RETRIEVE_NOTIFICATION,
+ error);
+ }
+ }
+ List<Asn1Node> nodes = root.getChild(Tags.TAG_CTX_COMP_0).getChildren();
+ EuiccNotification[] notifications = new EuiccNotification[nodes.size()];
+ for (int i = 0; i < notifications.length; ++i) {
+ notifications[i] = createNotification(nodes.get(i));
+ }
+ return notifications;
+ },
+ callback, handler);
+ }
+
+ /**
+ * Retrieves the content of a notification of the given {@code seqNumber}.
+ *
+ * @param seqNumber The sequence number of the notification.
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void retrieveNotification(int seqNumber, AsyncResultCallback<EuiccNotification> callback,
+ Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(
+ Asn1Node.newBuilder(Tags.TAG_RETRIEVE_NOTIFICATIONS_LIST)
+ .addChild(Asn1Node.newBuilder(Tags.TAG_CTX_COMP_0)
+ .addChildAsInteger(Tags.TAG_CTX_0, seqNumber))
+ .build().toHex())),
+ (byte[] response) -> {
+ Asn1Node root = parseResponseAndCheckSimpleError(response,
+ EuiccCardErrorException.OPERATION_RETRIEVE_NOTIFICATION);
+ List<Asn1Node> nodes = root.getChild(Tags.TAG_CTX_COMP_0).getChildren();
+ if (nodes.size() > 0) {
+ return createNotification(nodes.get(0));
+ }
+ return null;
+ },
+ callback, handler);
+ }
+
+ /**
+ * Removes a notification from eUICC.
+ *
+ * @param seqNumber The sequence number of the notification.
+ * @param callback The callback to get the result.
+ * @param handler The handler to run the callback.
+ * @since 2.0.0 [GSMA SGP.22]
+ */
+ public void removeNotificationFromList(int seqNumber, AsyncResultCallback<Void> callback,
+ Handler handler) {
+ sendApdu(
+ newRequestProvider((RequestBuilder requestBuilder) ->
+ requestBuilder.addStoreData(
+ Asn1Node.newBuilder(Tags.TAG_REMOVE_NOTIFICATION_FROM_LIST)
+ .addChildAsInteger(Tags.TAG_CTX_0, seqNumber)
+ .build().toHex())),
+ (byte[] response) -> {
+ // SGP.22 v2.0 NotificationSentResponse
+ int result = parseSimpleResult(response);
+ if (result != CODE_OK && result != CODE_NOTHING_TO_DELETE) {
+ throw new EuiccCardErrorException(
+ EuiccCardErrorException.OPERATION_REMOVE_NOTIFICATION_FROM_LIST,
+ result);
+ }
+ return null;
+ },
+ callback, handler);
+ }
+
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ protected byte[] getDeviceId() {
+ byte[] imeiBytes = new byte[8];
+ Phone phone = PhoneFactory.getPhone(getPhoneId());
+ if (phone != null) {
+ IccUtils.bcdToBytes(phone.getDeviceId(), imeiBytes);
+ }
+ return imeiBytes;
+ }
+
+ private RequestProvider newRequestProvider(ApduRequestBuilder builder) {
+ return (selectResponse, requestBuilder) -> {
+ EuiccSpecVersion ver = getOrExtractSpecVersion(selectResponse);
+ if (ver == null) {
+ throw new EuiccCardException("Cannot get eUICC spec version.");
+ }
+ try {
+ if (ver.compareTo(SGP_2_0) < 0) {
+ throw new EuiccCardException("eUICC spec version is unsupported: " + ver);
+ }
+ builder.build(requestBuilder);
+ } catch (InvalidAsn1DataException | TagNotFoundException e) {
+ throw new EuiccCardException("Cannot parse ASN1 to build request.", e);
+ }
+ };
+ }
+
+ private EuiccSpecVersion getOrExtractSpecVersion(byte[] selectResponse) {
+ // Uses the cached version.
+ if (mSpecVersion != null) {
+ return mSpecVersion;
+ }
+ // Parses and caches the version.
+ EuiccSpecVersion ver = EuiccSpecVersion.fromOpenChannelResponse(selectResponse);
+ if (ver != null) {
+ synchronized (mLock) {
+ if (mSpecVersion == null) {
+ mSpecVersion = ver;
+ }
+ }
+ }
+ return ver;
+ }
+
+ /**
+ * A wrapper on {@link ApduSender#send(RequestProvider, AsyncResultCallback, Handler)} to
+ * leverage lambda to simplify the sending APDU code.EuiccCardErrorException.
+ *
+ * @param requestBuilder Builds the request of APDU commands.
+ * @param responseHandler Converts the APDU response from bytes to expected result.
+ * @param <T> Type of the originally expected result.
+ */
+ private <T> void sendApdu(RequestProvider requestBuilder,
+ ApduResponseHandler<T> responseHandler, AsyncResultCallback<T> callback,
+ Handler handler) {
+ sendApdu(requestBuilder, responseHandler,
+ (e) -> callback.onException(new EuiccCardException("Cannot send APDU.", e)),
+ callback, handler);
+ }
+
+ /**
+ * This is a workaround solution to the bug that a SIM refresh may interrupt the modem to return
+ * the reset of responses of the original APDU command. This applies to disable profile, switch
+ * profile, and reset eUICC memory.
+ *
+ * <p>TODO: Use
+ * {@link #sendApdu(RequestProvider, ApduResponseHandler, AsyncResultCallback, Handler)} when
+ * this workaround is not needed.
+ */
+ private void sendApduWithSimResetErrorWorkaround(
+ RequestProvider requestBuilder, ApduResponseHandler<Void> responseHandler,
+ AsyncResultCallback<Void> callback, Handler handler) {
+ sendApdu(requestBuilder, responseHandler, (e) -> {
+ if (e instanceof ApduException
+ && ((ApduException) e).getApduStatus() == APDU_ERROR_SIM_REFRESH) {
+ logi("Sim is refreshed after disabling profile, no response got.");
+ callback.onResult(null);
+ } else {
+ callback.onException(new EuiccCardException("Cannot send APDU.", e));
+ }
+ }, callback, handler);
+ }
+
+ private <T> void sendApdu(RequestProvider requestBuilder,
+ ApduResponseHandler<T> responseHandler,
+ ApduExceptionHandler exceptionHandler, AsyncResultCallback<T> callback,
+ Handler handler) {
+ mApduSender.send(requestBuilder, new AsyncResultCallback<byte[]>() {
+ @Override
+ public void onResult(byte[] response) {
+ try {
+ callback.onResult(responseHandler.handleResult(response));
+ } catch (EuiccCardException e) {
+ callback.onException(e);
+ } catch (InvalidAsn1DataException | TagNotFoundException e) {
+ callback.onException(new EuiccCardException(
+ "Cannot parse response: " + IccUtils.bytesToHexString(response), e));
+ }
+ }
+
+ @Override
+ public void onException(Throwable e) {
+ exceptionHandler.handleException(e);
+ }
+ }, handler);
+ }
+
+ private static void buildProfile(Asn1Node profileNode, EuiccProfileInfo.Builder profileBuilder)
+ throws TagNotFoundException, InvalidAsn1DataException {
+ String strippedIccIdString =
+ stripTrailingFs(profileNode.getChild(Tags.TAG_ICCID).asBytes());
+ profileBuilder.setIccid(strippedIccIdString);
+
+ if (profileNode.hasChild(Tags.TAG_NICKNAME)) {
+ profileBuilder.setNickname(profileNode.getChild(Tags.TAG_NICKNAME).asString());
+ }
+
+ if (profileNode.hasChild(Tags.TAG_SERVICE_PROVIDER_NAME)) {
+ profileBuilder.setServiceProviderName(
+ profileNode.getChild(Tags.TAG_SERVICE_PROVIDER_NAME).asString());
+ }
+
+ if (profileNode.hasChild(Tags.TAG_PROFILE_NAME)) {
+ profileBuilder.setProfileName(
+ profileNode.getChild(Tags.TAG_PROFILE_NAME).asString());
+ }
+
+ if (profileNode.hasChild(Tags.TAG_OPERATOR_ID)) {
+ profileBuilder.setCarrierIdentifier(
+ buildCarrierIdentifier(profileNode.getChild(Tags.TAG_OPERATOR_ID)));
+ }
+
+ if (profileNode.hasChild(Tags.TAG_PROFILE_STATE)) {
+ // noinspection WrongConstant
+ profileBuilder.setState(profileNode.getChild(Tags.TAG_PROFILE_STATE).asInteger());
+ } else {
+ profileBuilder.setState(EuiccProfileInfo.PROFILE_STATE_DISABLED);
+ }
+
+ if (profileNode.hasChild(Tags.TAG_PROFILE_CLASS)) {
+ // noinspection WrongConstant
+ profileBuilder.setProfileClass(
+ profileNode.getChild(Tags.TAG_PROFILE_CLASS).asInteger());
+ } else {
+ profileBuilder.setProfileClass(EuiccProfileInfo.PROFILE_CLASS_OPERATIONAL);
+ }
+
+ if (profileNode.hasChild(Tags.TAG_PROFILE_POLICY_RULE)) {
+ // noinspection WrongConstant
+ profileBuilder.setPolicyRules(
+ profileNode.getChild(Tags.TAG_PROFILE_POLICY_RULE).asBits());
+ }
+
+ if (profileNode.hasChild(Tags.TAG_CARRIER_PRIVILEGE_RULES)) {
+ List<Asn1Node> refArDoNodes = profileNode.getChild(Tags.TAG_CARRIER_PRIVILEGE_RULES)
+ .getChildren(Tags.TAG_REF_AR_DO);
+ profileBuilder.setUiccAccessRule(buildUiccAccessRule(refArDoNodes));
+ }
+ }
+
+ private static CarrierIdentifier buildCarrierIdentifier(Asn1Node node)
+ throws InvalidAsn1DataException, TagNotFoundException {
+ String gid1 = null;
+ if (node.hasChild(Tags.TAG_CTX_1)) {
+ gid1 = IccUtils.bytesToHexString(node.getChild(Tags.TAG_CTX_1).asBytes());
+ }
+ String gid2 = null;
+ if (node.hasChild(Tags.TAG_CTX_2)) {
+ gid2 = IccUtils.bytesToHexString(node.getChild(Tags.TAG_CTX_2).asBytes());
+ }
+ return new CarrierIdentifier(node.getChild(Tags.TAG_CTX_0).asBytes(), gid1, gid2);
+ }
+
+ @Nullable
+ private static UiccAccessRule[] buildUiccAccessRule(List<Asn1Node> nodes)
+ throws InvalidAsn1DataException, TagNotFoundException {
+ if (nodes.isEmpty()) {
+ return null;
+ }
+ int count = nodes.size();
+ UiccAccessRule[] rules = new UiccAccessRule[count];
+ for (int i = 0; i < count; i++) {
+ Asn1Node node = nodes.get(i);
+ Asn1Node refDoNode = node.getChild(Tags.TAG_REF_DO);
+ byte[] signature = refDoNode.getChild(Tags.TAG_DEVICE_APP_ID_REF_DO).asBytes();
+
+ String packageName = null;
+ if (refDoNode.hasChild(Tags.TAG_PKG_REF_DO)) {
+ packageName = refDoNode.getChild(Tags.TAG_PKG_REF_DO).asString();
+ }
+ long accessType = 0;
+ if (node.hasChild(Tags.TAG_AR_DO, Tags.TAG_PERM_AR_DO)) {
+ Asn1Node permArDoNode = node.getChild(Tags.TAG_AR_DO, Tags.TAG_PERM_AR_DO);
+ accessType = permArDoNode.asRawLong();
+ }
+ rules[i] = new UiccAccessRule(signature, packageName, accessType);
+ }
+ return rules;
+ }
+
+ /**
+ * Creates an instance from the ASN.1 data.
+ *
+ * @param node This should be either {@code NotificationMetadata} or {@code PendingNotification}
+ * defined by SGP.22 v2.0.
+ * @throws TagNotFoundException If no notification tag is found in the bytes.
+ * @throws InvalidAsn1DataException If no valid data is found in the bytes.
+ */
+ private static EuiccNotification createNotification(Asn1Node node)
+ throws TagNotFoundException, InvalidAsn1DataException {
+ Asn1Node metadataNode;
+ if (node.getTag() == Tags.TAG_NOTIFICATION_METADATA) {
+ metadataNode = node;
+ } else if (node.getTag() == Tags.TAG_PROFILE_INSTALLATION_RESULT) {
+ metadataNode = node.getChild(Tags.TAG_PROFILE_INSTALLATION_RESULT_DATA,
+ Tags.TAG_NOTIFICATION_METADATA);
+ } else {
+ // Other signed notification
+ metadataNode = node.getChild(Tags.TAG_NOTIFICATION_METADATA);
+ }
+ // noinspection WrongConstant
+ return new EuiccNotification(metadataNode.getChild(Tags.TAG_SEQ).asInteger(),
+ metadataNode.getChild(Tags.TAG_TARGET_ADDR).asString(),
+ metadataNode.getChild(Tags.TAG_EVENT).asBits(),
+ node.getTag() == Tags.TAG_NOTIFICATION_METADATA ? null : node.toBytes());
+ }
+
+ /** Returns the first CONTEXT [0] as an integer. */
+ private static int parseSimpleResult(byte[] response)
+ throws EuiccCardException, TagNotFoundException, InvalidAsn1DataException {
+ return parseResponse(response).getChild(Tags.TAG_CTX_0).asInteger();
+ }
+
+ private static Asn1Node parseResponse(byte[] response)
+ throws EuiccCardException, InvalidAsn1DataException {
+ Asn1Decoder decoder = new Asn1Decoder(response);
+ if (!decoder.hasNextNode()) {
+ throw new EuiccCardException("Empty response", null);
+ }
+ return decoder.nextNode();
+ }
+
+ /**
+ * Parses the bytes into an ASN1 node and check if there is an error code represented at the
+ * context 1 tag. If there is an error code, an {@link EuiccCardErrorException} will be thrown
+ * with the given operation code.
+ */
+ private static Asn1Node parseResponseAndCheckSimpleError(byte[] response,
+ @OperationCode int opCode)
+ throws EuiccCardException, InvalidAsn1DataException, TagNotFoundException {
+ Asn1Node root = parseResponse(response);
+ if (root.hasChild(Tags.TAG_CTX_1)) {
+ throw new EuiccCardErrorException(opCode, root.getChild(Tags.TAG_CTX_1).asInteger());
+ }
+ return root;
+ }
+
+ /** Strip all the trailing 'F' characters of an iccId. */
+ private static String stripTrailingFs(byte[] iccId) {
+ return IccUtils.stripTrailingFs(IccUtils.bchToString(iccId, 0, iccId.length));
+ }
+
+ /** Pad an iccId with trailing 'F' characters until the length is 20. */
+ private static String padTrailingFs(String iccId) {
+ if (!TextUtils.isEmpty(iccId) && iccId.length() < ICCID_LENGTH) {
+ iccId += new String(new char[20 - iccId.length()]).replace('\0', 'F');
+ }
+ return iccId;
+ }
+
+ private static void loge(String message) {
+ Rlog.e(LOG_TAG, message);
+ }
+
+ private static void logi(String message) {
+ Rlog.i(LOG_TAG, message);
+ }
+
+ private static void logd(String message) {
+ if (DBG) {
+ Rlog.d(LOG_TAG, message);
+ }
+ }
+}
diff --git a/com/android/internal/telephony/uicc/euicc/EuiccCardErrorException.java b/com/android/internal/telephony/uicc/euicc/EuiccCardErrorException.java
new file mode 100644
index 00000000..4984485c
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/EuiccCardErrorException.java
@@ -0,0 +1,123 @@
+/*
+ * 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 com.android.internal.telephony.uicc.euicc;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+
+import com.android.internal.telephony.uicc.asn1.Asn1Node;
+import com.android.internal.telephony.uicc.euicc.apdu.ApduSender;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * The exception which is thrown when an error is returned in a successfully executed APDU command
+ * sent to eUICC. This exception means the response status is no-error
+ * ({@link ApduSender#STATUS_NO_ERROR}), but the action is failed due to eUICC specific logic.
+ */
+public class EuiccCardErrorException extends EuiccCardException {
+ /** Operations */
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef(prefix = "OPERATION_", value = {
+ OPERATION_UNKNOWN,
+ OPERATION_GET_PROFILE,
+ OPERATION_PREPARE_DOWNLOAD,
+ OPERATION_AUTHENTICATE_SERVER,
+ OPERATION_CANCEL_SESSION,
+ OPERATION_LOAD_BOUND_PROFILE_PACKAGE,
+ OPERATION_LIST_NOTIFICATIONS,
+ OPERATION_SET_NICKNAME,
+ OPERATION_RETRIEVE_NOTIFICATION,
+ OPERATION_REMOVE_NOTIFICATION_FROM_LIST,
+ OPERATION_SWITCH_TO_PROFILE,
+ OPERATION_DISABLE_PROFILE,
+ OPERATION_DELETE_PROFILE,
+ OPERATION_RESET_MEMORY,
+ OPERATION_SET_DEFAULT_SMDP_ADDRESS,
+ })
+ public @interface OperationCode {}
+
+ public static final int OPERATION_UNKNOWN = 0;
+ public static final int OPERATION_GET_PROFILE = 1;
+ public static final int OPERATION_PREPARE_DOWNLOAD = 2;
+ public static final int OPERATION_AUTHENTICATE_SERVER = 3;
+ public static final int OPERATION_CANCEL_SESSION = 4;
+ public static final int OPERATION_LOAD_BOUND_PROFILE_PACKAGE = 5;
+ public static final int OPERATION_LIST_NOTIFICATIONS = 6;
+ public static final int OPERATION_SET_NICKNAME = 7;
+ public static final int OPERATION_RETRIEVE_NOTIFICATION = 8;
+ public static final int OPERATION_REMOVE_NOTIFICATION_FROM_LIST = 9;
+ public static final int OPERATION_SWITCH_TO_PROFILE = 10;
+ public static final int OPERATION_DISABLE_PROFILE = 11;
+ public static final int OPERATION_DELETE_PROFILE = 12;
+ public static final int OPERATION_RESET_MEMORY = 13;
+ public static final int OPERATION_SET_DEFAULT_SMDP_ADDRESS = 14;
+
+ private final @OperationCode int mOperationCode;
+ private final int mErrorCode;
+ private final @Nullable Asn1Node mErrorDetails;
+
+ /**
+ * Creates an exception with an error code in the response of an APDU command.
+ *
+ * @param errorCode The meaning of the code depends on each APDU command. It should always be
+ * non-negative.
+ */
+ public EuiccCardErrorException(@OperationCode int operationCode, int errorCode) {
+ mOperationCode = operationCode;
+ mErrorCode = errorCode;
+ mErrorDetails = null;
+ }
+
+ /**
+ * Creates an exception with an error code and the error details in the response of an APDU
+ * command.
+ *
+ * @param errorCode The meaning of the code depends on each APDU command. It should always be
+ * non-negative.
+ * @param errorDetails The content of the details depends on each APDU command.
+ */
+ public EuiccCardErrorException(@OperationCode int operationCode, int errorCode,
+ @Nullable Asn1Node errorDetails) {
+ mOperationCode = operationCode;
+ mErrorCode = errorCode;
+ mErrorDetails = errorDetails;
+ }
+
+ /** @return The error code. The meaning of the code depends on each APDU command. */
+ public int getErrorCode() {
+ return mErrorCode;
+ }
+
+ /** @return The operation code. */
+ public int getOperationCode() {
+ return mOperationCode;
+ }
+
+ /** @return The error details. The meaning of the details depends on each APDU command. */
+ @Nullable
+ public Asn1Node getErrorDetails() {
+ return mErrorDetails;
+ }
+
+ @Override
+ public String getMessage() {
+ return "EuiccCardError: mOperatorCode=" + mOperationCode + ", mErrorCode=" + mErrorCode
+ + ", errorDetails=" + (mErrorDetails == null ? "null" : mErrorDetails.toHex());
+ }
+}
diff --git a/com/android/internal/telephony/uicc/euicc/EuiccCardException.java b/com/android/internal/telephony/uicc/euicc/EuiccCardException.java
new file mode 100644
index 00000000..5b989558
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/EuiccCardException.java
@@ -0,0 +1,33 @@
+/*
+ * 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 com.android.internal.telephony.uicc.euicc;
+
+/**
+ * The base exception class of all exceptions thrown by the methods of an {@link EuiccCard}
+ * instance.
+ */
+public class EuiccCardException extends Exception {
+ public EuiccCardException() {}
+
+ public EuiccCardException(String message) {
+ super(message);
+ }
+
+ public EuiccCardException(String message, Throwable throwable) {
+ super(message, throwable);
+ }
+}
diff --git a/com/android/internal/telephony/uicc/euicc/EuiccSpecVersion.java b/com/android/internal/telephony/uicc/euicc/EuiccSpecVersion.java
new file mode 100644
index 00000000..b0387165
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/EuiccSpecVersion.java
@@ -0,0 +1,144 @@
+/*
+ * 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 com.android.internal.telephony.uicc.euicc;
+
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.uicc.asn1.Asn1Decoder;
+import com.android.internal.telephony.uicc.asn1.Asn1Node;
+import com.android.internal.telephony.uicc.asn1.InvalidAsn1DataException;
+import com.android.internal.telephony.uicc.asn1.TagNotFoundException;
+
+import java.util.Arrays;
+
+/**
+ * This represents the version of GSMA SGP.22 spec in the form of 3 numbers: major, minor, and
+ * revision.
+ */
+public final class EuiccSpecVersion implements Comparable<EuiccSpecVersion> {
+ private static final String LOG_TAG = "EuiccSpecVer";
+
+ // ASN.1 Tags
+ private static final int TAG_ISD_R_APP_TEMPLATE = 0xE0;
+ private static final int TAG_VERSION = 0x82;
+
+ private final int[] mVersionValues = new int[3];
+
+ /**
+ * Parses the response of opening a logical channel to get spec version of the eUICC card.
+ *
+ * @return Parsed spec version. If any error is encountered, null will be returned.
+ */
+ public static EuiccSpecVersion fromOpenChannelResponse(byte[] response) {
+ Asn1Node node;
+ try {
+ Asn1Decoder decoder = new Asn1Decoder(response);
+ if (!decoder.hasNextNode()) {
+ return null;
+ }
+ node = decoder.nextNode();
+ } catch (InvalidAsn1DataException e) {
+ Rlog.e(LOG_TAG, "Cannot parse the select response of ISD-R.", e);
+ return null;
+ }
+ try {
+ byte[] versionType;
+ if (node.getTag() == TAG_ISD_R_APP_TEMPLATE) {
+ versionType = node.getChild(TAG_VERSION).asBytes();
+ } else {
+ versionType =
+ node.getChild(TAG_ISD_R_APP_TEMPLATE, TAG_VERSION).asBytes();
+ }
+ if (versionType.length == 3) {
+ return new EuiccSpecVersion(versionType);
+ } else {
+ Rlog.e(LOG_TAG, "Cannot parse select response of ISD-R: " + node.toHex());
+ }
+ } catch (InvalidAsn1DataException | TagNotFoundException e) {
+ Rlog.e(LOG_TAG, "Cannot parse select response of ISD-R: " + node.toHex());
+ }
+ return null;
+ }
+
+ public EuiccSpecVersion(int major, int minor, int revision) {
+ mVersionValues[0] = major;
+ mVersionValues[1] = minor;
+ mVersionValues[2] = revision;
+ }
+
+ /**
+ * @param version The version bytes from ASN1 data. The length must be 3.
+ */
+ public EuiccSpecVersion(byte[] version) {
+ mVersionValues[0] = version[0] & 0xFF;
+ mVersionValues[1] = version[1] & 0xFF;
+ mVersionValues[2] = version[2] & 0xFF;
+ }
+
+ public int getMajor() {
+ return mVersionValues[0];
+ }
+
+ public int getMinor() {
+ return mVersionValues[1];
+ }
+
+ public int getRevision() {
+ return mVersionValues[2];
+ }
+
+ @Override
+ public int compareTo(EuiccSpecVersion that) {
+ if (getMajor() > that.getMajor()) {
+ return 1;
+ } else if (getMajor() < that.getMajor()) {
+ return -1;
+ }
+ if (getMinor() > that.getMinor()) {
+ return 1;
+ } else if (getMinor() < that.getMinor()) {
+ return -1;
+ }
+ if (getRevision() > that.getRevision()) {
+ return 1;
+ } else if (getRevision() < that.getRevision()) {
+ return -1;
+ }
+ return 0;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null || getClass() != obj.getClass()) {
+ return false;
+ }
+ return Arrays.equals(mVersionValues, ((EuiccSpecVersion) obj).mVersionValues);
+ }
+
+ @Override
+ public int hashCode() {
+ return Arrays.hashCode(mVersionValues);
+ }
+
+ @Override
+ public String toString() {
+ return mVersionValues[0] + "." + mVersionValues[1] + "." + mVersionValues[2];
+ }
+}
diff --git a/com/android/internal/telephony/uicc/euicc/Tags.java b/com/android/internal/telephony/uicc/euicc/Tags.java
new file mode 100644
index 00000000..11c1cd22
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/Tags.java
@@ -0,0 +1,108 @@
+/*
+ * 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 com.android.internal.telephony.uicc.euicc;
+
+/**
+ * ASN1 tags used by {@link EuiccCard} implementation.
+ */
+class Tags {
+ // ASN.1 tags for commands
+ static final int TAG_GET_PROFILES = 0xBF2D;
+ static final int TAG_DISABLE_PROFILE = 0xBF32;
+ static final int TAG_ENABLE_PROFILE = 0xBF31;
+ static final int TAG_GET_EID = 0xBF3E;
+ static final int TAG_SET_NICKNAME = 0xBF29;
+ static final int TAG_DELETE_PROFILE = 0xBF33;
+ static final int TAG_GET_CONFIGURED_ADDRESSES = 0xBF3C;
+ static final int TAG_SET_DEFAULT_SMDP_ADDRESS = 0xBF3F;
+ static final int TAG_GET_RAT = 0xBF43;
+ static final int TAG_EUICC_MEMORY_RESET = 0xBF34;
+ static final int TAG_GET_EUICC_CHALLENGE = 0xBF2E;
+ static final int TAG_GET_EUICC_INFO_1 = 0xBF20;
+ static final int TAG_GET_EUICC_INFO_2 = 0xBF22;
+ static final int TAG_LIST_NOTIFICATION = 0xBF28;
+ static final int TAG_RETRIEVE_NOTIFICATIONS_LIST = 0xBF2B;
+ static final int TAG_REMOVE_NOTIFICATION_FROM_LIST = 0xBF30;
+ static final int TAG_AUTHENTICATE_SERVER = 0xBF38;
+ static final int TAG_PREPARE_DOWNLOAD = 0xBF21;
+ static final int TAG_INITIALISE_SECURE_CHANNEL = 0xBF23;
+
+ // Universal tags
+ static final int TAG_UNI_2 = 0x02;
+ static final int TAG_UNI_4 = 0x04;
+ // Context tags for primitive types
+ static final int TAG_CTX_0 = 0x80;
+ static final int TAG_CTX_1 = 0x81;
+ static final int TAG_CTX_2 = 0x82;
+ static final int TAG_CTX_3 = 0x83;
+ static final int TAG_CTX_4 = 0x84;
+ static final int TAG_CTX_5 = 0x85;
+ static final int TAG_CTX_6 = 0x86;
+ static final int TAG_CTX_7 = 0x87;
+ static final int TAG_CTX_8 = 0x88;
+ // Context tags for constructed (compound) types
+ static final int TAG_CTX_COMP_0 = 0xA0;
+ static final int TAG_CTX_COMP_1 = 0xA1;
+ static final int TAG_CTX_COMP_2 = 0xA2;
+ static final int TAG_CTX_COMP_3 = 0xA3;
+
+ // Command data tags
+ static final int TAG_PROFILE_INSTALLATION_RESULT = 0xBF37;
+ static final int TAG_PROFILE_INSTALLATION_RESULT_DATA = 0xBF27;
+ static final int TAG_NOTIFICATION_METADATA = 0xBF2F;
+ static final int TAG_SEQ = TAG_CTX_0;
+ static final int TAG_TARGET_ADDR = 0x0C;
+ static final int TAG_EVENT = TAG_CTX_1;
+ static final int TAG_CANCEL_SESSION = 0xBF41;
+ static final int TAG_PROFILE_INFO = 0xE3;
+ static final int TAG_TAG_LIST = 0x5C;
+ static final int TAG_EID = 0x5A;
+ static final int TAG_NICKNAME = 0x90;
+ static final int TAG_ICCID = 0x5A;
+ static final int TAG_PROFILE_STATE = 0x9F70;
+ static final int TAG_SERVICE_PROVIDER_NAME = 0x91;
+ static final int TAG_PROFILE_CLASS = 0x95;
+ static final int TAG_PROFILE_POLICY_RULE = 0x99;
+ static final int TAG_PROFILE_NAME = 0x92;
+ static final int TAG_OPERATOR_ID = 0xB7;
+ static final int TAG_CARRIER_PRIVILEGE_RULES = 0xBF76;
+
+ // Tags from the RefArDo data standard - https://source.android.com/devices/tech/config/uicc
+ static final int TAG_REF_AR_DO = 0xE2;
+ static final int TAG_REF_DO = 0xE1;
+ static final int TAG_DEVICE_APP_ID_REF_DO = 0xC1;
+ static final int TAG_PKG_REF_DO = 0xCA;
+ static final int TAG_AR_DO = 0xE3;
+ static final int TAG_PERM_AR_DO = 0xDB;
+
+ // TAG list for Euicc Profile
+ static final byte[] EUICC_PROFILE_TAGS = new byte[] {
+ TAG_ICCID,
+ (byte) TAG_NICKNAME,
+ (byte) TAG_SERVICE_PROVIDER_NAME,
+ (byte) TAG_PROFILE_NAME,
+ (byte) TAG_OPERATOR_ID,
+ (byte) (TAG_PROFILE_STATE / 256),
+ (byte) (TAG_PROFILE_STATE % 256),
+ (byte) TAG_PROFILE_CLASS,
+ (byte) TAG_PROFILE_POLICY_RULE,
+ (byte) (TAG_CARRIER_PRIVILEGE_RULES / 256),
+ (byte) (TAG_CARRIER_PRIVILEGE_RULES % 256),
+ };
+
+ private Tags() {}
+}
diff --git a/com/android/internal/telephony/uicc/euicc/apdu/ApduCommand.java b/com/android/internal/telephony/uicc/euicc/apdu/ApduCommand.java
new file mode 100644
index 00000000..7a8978f8
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/apdu/ApduCommand.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.telephony.uicc.euicc.apdu;
+
+/**
+ * Parts of an APDU command.
+ *
+ * @hide
+ */
+class ApduCommand {
+ /** Channel of an APDU as defined in GlobalPlatform Card Specification v.2.3. */
+ public final int channel;
+
+ /** Class of an APDU as defined in GlobalPlatform Card Specification v.2.3. */
+ public final int cla;
+
+ /** Instruction of an APDU as defined in GlobalPlatform Card Specification v.2.3. */
+ public final int ins;
+
+ /** Parameter 1 of an APDU as defined in GlobalPlatform Card Specification v.2.3. */
+ public final int p1;
+
+ /** Parameter 2 of an APDU as defined in GlobalPlatform Card Specification v.2.3. */
+ public final int p2;
+
+ /** Parameter 3 of an APDU as defined in GlobalPlatform Card Specification v.2.3. */
+ public final int p3;
+
+ /** Command data of an APDU as defined in GlobalPlatform Card Specification v.2.3. */
+ public final String cmdHex;
+
+ /** The parameters are defined as in GlobalPlatform Card Specification v.2.3. */
+ ApduCommand(int channel, int cla, int ins, int p1, int p2, int p3, String cmdHex) {
+ this.channel = channel;
+ this.cla = cla;
+ this.ins = ins;
+ this.p1 = p1;
+ this.p2 = p2;
+ this.p3 = p3;
+ this.cmdHex = cmdHex;
+ }
+
+ @Override
+ public String toString() {
+ return "ApduCommand(channel=" + channel + ", cla=" + cla + ", ins=" + ins + ", p1=" + p1
+ + ", p2=" + p2 + ", p3=" + p3 + ", cmd=" + cmdHex + ")";
+ }
+}
diff --git a/com/android/internal/telephony/uicc/euicc/apdu/ApduException.java b/com/android/internal/telephony/uicc/euicc/apdu/ApduException.java
new file mode 100644
index 00000000..9c68d446
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/apdu/ApduException.java
@@ -0,0 +1,63 @@
+/*
+ * 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 com.android.internal.telephony.uicc.euicc.apdu;
+
+import android.telephony.IccOpenLogicalChannelResponse;
+
+/**
+ * The exception of failing to execute an APDU command. It can be caused by an error happening on
+ * opening the basic or logical channel, or the response of the APDU command is not success
+ * ({@link ApduSender#STATUS_NO_ERROR}).
+ *
+ * @hide
+ */
+public class ApduException extends Exception {
+ private final int mApduStatus;
+
+ /** Creates an exception with the apduStatus code of the response of an APDU command. */
+ public ApduException(int apduStatus) {
+ super();
+ mApduStatus = apduStatus;
+ }
+
+ public ApduException(String message) {
+ super(message);
+ mApduStatus = 0;
+ }
+
+ /**
+ * @return The error status of the response of an APDU command. An error status can be any
+ * positive 16-bit integer (i.e., SW1 & SW2) other than
+ * {@link ApduSender#STATUS_NO_ERROR} which means no error. For an error encountered
+ * when opening a logical channel before the APDU command gets sent, this is not the
+ * status defined in {@link IccOpenLogicalChannelResponse}. In this caes, 0 will be
+ * returned and the message of this exception will have the detailed error information.
+ */
+ public int getApduStatus() {
+ return mApduStatus;
+ }
+
+ /** @return The hex string of the error status. */
+ public String getStatusHex() {
+ return Integer.toHexString(mApduStatus);
+ }
+
+ @Override
+ public String getMessage() {
+ return super.getMessage() + " (apduStatus=" + getStatusHex() + ")";
+ }
+}
diff --git a/com/android/internal/telephony/uicc/euicc/apdu/ApduSender.java b/com/android/internal/telephony/uicc/euicc/apdu/ApduSender.java
new file mode 100644
index 00000000..05cc78ca
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/apdu/ApduSender.java
@@ -0,0 +1,253 @@
+/*
+ * 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 com.android.internal.telephony.uicc.euicc.apdu;
+
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.telephony.IccOpenLogicalChannelResponse;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.uicc.IccIoResult;
+import com.android.internal.telephony.uicc.euicc.async.AsyncResultCallback;
+import com.android.internal.telephony.uicc.euicc.async.AsyncResultHelper;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * This class sends a list of APDU commands to an AID on a UICC. A logical channel will be opened
+ * before sending and closed after all APDU commands are sent. The complete response of the last
+ * APDU command will be returned. If any APDU command returns an error status (other than
+ * {@link #STATUS_NO_ERROR}) or causing an exception, an {@link ApduException} will be returned
+ * immediately without sending the rest of commands. This class is thread-safe.
+ *
+ * @hide
+ */
+public class ApduSender {
+ private static final String LOG_TAG = "ApduSender";
+
+ // Parameter and response used by the command to get extra responses of an APDU command.
+ private static final int INS_GET_MORE_RESPONSE = 0xC0;
+ private static final int SW1_MORE_RESPONSE = 0x61;
+
+ // Status code of APDU response
+ private static final int STATUS_NO_ERROR = 0x9000;
+
+ private static void logv(String msg) {
+ Rlog.v(LOG_TAG, msg);
+ }
+
+ private final String mAid;
+ private final boolean mSupportExtendedApdu;
+ private final OpenLogicalChannelInvocation mOpenChannel;
+ private final CloseLogicalChannelInvocation mCloseChannel;
+ private final TransmitApduLogicalChannelInvocation mTransmitApdu;
+
+ // Lock for accessing mChannelOpened. We only allow to open a single logical channel at any
+ // time for an AID.
+ private final Object mChannelLock = new Object();
+ private boolean mChannelOpened;
+
+ /**
+ * @param aid The AID that will be used to open a logical channel to.
+ */
+ public ApduSender(CommandsInterface ci, String aid, boolean supportExtendedApdu) {
+ mAid = aid;
+ mSupportExtendedApdu = supportExtendedApdu;
+ mOpenChannel = new OpenLogicalChannelInvocation(ci);
+ mCloseChannel = new CloseLogicalChannelInvocation(ci);
+ mTransmitApdu = new TransmitApduLogicalChannelInvocation(ci);
+ }
+
+ /**
+ * Sends APDU commands.
+ *
+ * @param requestProvider Will be called after a logical channel is opened successfully. This is
+ * in charge of building a request with all APDU commands to be sent. This won't be called
+ * if any error happens when opening a logical channel.
+ * @param resultCallback Will be called after an error or the last APDU command has been
+ * executed. The result will be the full response of the last APDU command. Error will be
+ * returned as an {@link ApduException} exception.
+ * @param handler The handler that {@code requestProvider} and {@code resultCallback} will be
+ * executed on.
+ */
+ public void send(
+ RequestProvider requestProvider,
+ AsyncResultCallback<byte[]> resultCallback,
+ Handler handler) {
+ synchronized (mChannelLock) {
+ if (mChannelOpened) {
+ AsyncResultHelper.throwException(
+ new ApduException("Logical channel has already been opened."),
+ resultCallback, handler);
+ return;
+ }
+ mChannelOpened = true;
+ }
+
+ mOpenChannel.invoke(mAid, new AsyncResultCallback<IccOpenLogicalChannelResponse>() {
+ @Override
+ public void onResult(IccOpenLogicalChannelResponse openChannelResponse) {
+ int channel = openChannelResponse.getChannel();
+ int status = openChannelResponse.getStatus();
+ if (channel == IccOpenLogicalChannelResponse.INVALID_CHANNEL
+ || status != IccOpenLogicalChannelResponse.STATUS_NO_ERROR) {
+ synchronized (mChannelLock) {
+ mChannelOpened = false;
+ }
+ resultCallback.onException(
+ new ApduException("Failed to open logical channel opened for AID: "
+ + mAid + ", with status: " + status));
+ return;
+ }
+
+ RequestBuilder builder = new RequestBuilder(channel, mSupportExtendedApdu);
+ Throwable requestException = null;
+ try {
+ requestProvider.buildRequest(openChannelResponse.getSelectResponse(), builder);
+ } catch (Throwable e) {
+ requestException = e;
+ }
+ if (builder.getCommands().isEmpty() || requestException != null) {
+ // Just close the channel if we don't have commands to send or an error
+ // was encountered.
+ closeAndReturn(channel, null /* response */, requestException, resultCallback,
+ handler);
+ return;
+ }
+ sendCommand(builder.getCommands(), 0 /* index */, resultCallback, handler);
+ }
+ }, handler);
+ }
+
+ /**
+ * Sends the current command and then continue to send the next one. If this is the last
+ * command or any error happens, {@code resultCallback} will be called.
+ *
+ * @param commands All commands to be sent.
+ * @param index The current command index.
+ */
+ private void sendCommand(
+ List<ApduCommand> commands,
+ int index,
+ AsyncResultCallback<byte[]> resultCallback,
+ Handler handler) {
+ ApduCommand command = commands.get(index);
+ mTransmitApdu.invoke(command, new AsyncResultCallback<IccIoResult>() {
+ @Override
+ public void onResult(IccIoResult response) {
+ // A long response may need to be fetched by multiple following-up APDU
+ // commands. Makes sure that we get the complete response.
+ getCompleteResponse(command.channel, response, null /* responseBuilder */,
+ new AsyncResultCallback<IccIoResult>() {
+ @Override
+ public void onResult(IccIoResult fullResponse) {
+ logv("Full APDU response: " + fullResponse);
+
+ int status = (fullResponse.sw1 << 8) | fullResponse.sw2;
+ if (status != STATUS_NO_ERROR) {
+ closeAndReturn(command.channel, null /* response */,
+ new ApduException(status), resultCallback, handler);
+ return;
+ }
+
+ // Last command
+ if (index == commands.size() - 1) {
+ closeAndReturn(command.channel, fullResponse.payload,
+ null /* exception */, resultCallback, handler);
+ return;
+ }
+
+ // Sends the next command
+ sendCommand(commands, index + 1, resultCallback, handler);
+ }
+ }, handler);
+ }
+ }, handler);
+ }
+
+ /**
+ * Gets the full response.
+ *
+ * @param lastResponse Will be checked to see if we need to fetch more.
+ * @param responseBuilder For continuously building the full response. It should not contain the
+ * last response. If it's null, a new builder will be created.
+ * @param resultCallback Error will be included in the result and no exception will be returned.
+ */
+ private void getCompleteResponse(
+ int channel,
+ IccIoResult lastResponse,
+ @Nullable ByteArrayOutputStream responseBuilder,
+ AsyncResultCallback<IccIoResult> resultCallback,
+ Handler handler) {
+ ByteArrayOutputStream resultBuilder =
+ responseBuilder == null ? new ByteArrayOutputStream() : responseBuilder;
+ try {
+ resultBuilder.write(lastResponse.payload);
+ } catch (IOException e) {
+ // Should never reach here.
+ }
+ if (lastResponse.sw1 != SW1_MORE_RESPONSE) {
+ lastResponse.payload = resultBuilder.toByteArray();
+ resultCallback.onResult(lastResponse);
+ return;
+ }
+
+ mTransmitApdu.invoke(
+ new ApduCommand(channel, 0 /* cls */, INS_GET_MORE_RESPONSE, 0 /* p1 */,
+ 0 /* p2 */, lastResponse.sw2, "" /* cmdHex */),
+ new AsyncResultCallback<IccIoResult>() {
+ @Override
+ public void onResult(IccIoResult response) {
+ getCompleteResponse(
+ channel, response, resultBuilder, resultCallback, handler);
+ }
+ }, handler);
+ }
+
+ /**
+ * Closes the opened logical channel.
+ *
+ * @param response If {@code exception} is null, this will be returned to {@code resultCallback}
+ * after the channel has been closed.
+ * @param exception If not null, this will be returned to {@code resultCallback} after the
+ * channel has been closed.
+ */
+ private void closeAndReturn(
+ int channel,
+ @Nullable byte[] response,
+ @Nullable Throwable exception,
+ AsyncResultCallback<byte[]> resultCallback,
+ Handler handler) {
+ mCloseChannel.invoke(channel, new AsyncResultCallback<Boolean>() {
+ @Override
+ public void onResult(Boolean aBoolean) {
+ synchronized (mChannelLock) {
+ mChannelOpened = false;
+ }
+
+ if (exception == null) {
+ resultCallback.onResult(response);
+ } else {
+ resultCallback.onException(exception);
+ }
+ }
+ }, handler);
+ }
+}
diff --git a/com/android/internal/telephony/uicc/euicc/apdu/CloseLogicalChannelInvocation.java b/com/android/internal/telephony/uicc/euicc/apdu/CloseLogicalChannelInvocation.java
new file mode 100644
index 00000000..edd89d6d
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/apdu/CloseLogicalChannelInvocation.java
@@ -0,0 +1,61 @@
+/*
+ * 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 com.android.internal.telephony.uicc.euicc.apdu;
+
+import android.os.AsyncResult;
+import android.os.Message;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.uicc.euicc.async.AsyncMessageInvocation;
+
+/**
+ * Invokes {@link CommandsInterface#iccCloseLogicalChannel(int, Message)}. This takes a channel id
+ * (Integer) as the input and return a boolean to indicate if closing the logical channel is
+ * succeeded. No exception will be returned to the result callback.
+ *
+ * @hide
+ */
+class CloseLogicalChannelInvocation extends AsyncMessageInvocation<Integer, Boolean> {
+ private static final String LOG_TAG = "CloseChan";
+
+ private final CommandsInterface mCi;
+
+ CloseLogicalChannelInvocation(CommandsInterface ci) {
+ mCi = ci;
+ }
+
+ @Override
+ protected void sendRequestMessage(Integer channel, Message msg) {
+ Rlog.v(LOG_TAG, "Channel: " + channel);
+ mCi.iccCloseLogicalChannel(channel, msg);
+ }
+
+ @Override
+ protected Boolean parseResult(AsyncResult ar) {
+ if (ar.exception == null) {
+ return true;
+ }
+ if (ar.exception instanceof CommandException) {
+ Rlog.e(LOG_TAG, "CommandException", ar.exception);
+ } else {
+ Rlog.e(LOG_TAG, "Unknown exception", ar.exception);
+ }
+ return false;
+ }
+}
diff --git a/com/android/internal/telephony/uicc/euicc/apdu/OpenLogicalChannelInvocation.java b/com/android/internal/telephony/uicc/euicc/apdu/OpenLogicalChannelInvocation.java
new file mode 100644
index 00000000..15b0c43a
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/apdu/OpenLogicalChannelInvocation.java
@@ -0,0 +1,93 @@
+/*
+ * 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 com.android.internal.telephony.uicc.euicc.apdu;
+
+import android.os.AsyncResult;
+import android.os.Message;
+import android.telephony.IccOpenLogicalChannelResponse;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.uicc.euicc.async.AsyncMessageInvocation;
+
+/**
+ * Invokes {@link CommandsInterface#iccOpenLogicalChannel(String, int, Message)}. This takes AID
+ * (String) as the input and return the response of opening a logical channel. Error will be
+ * included in the {@link IccOpenLogicalChannelResponse} result and no exception will be returned to
+ * the result callback.
+ *
+ * @hide
+ */
+class OpenLogicalChannelInvocation
+ extends AsyncMessageInvocation<String, IccOpenLogicalChannelResponse> {
+ private static final String LOG_TAG = "OpenChan";
+
+ private final CommandsInterface mCi;
+
+ OpenLogicalChannelInvocation(CommandsInterface ci) {
+ mCi = ci;
+ }
+
+ @Override
+ protected void sendRequestMessage(String aid, Message msg) {
+ mCi.iccOpenLogicalChannel(aid, 0, msg);
+ }
+
+ @Override
+ protected IccOpenLogicalChannelResponse parseResult(AsyncResult ar) {
+ IccOpenLogicalChannelResponse openChannelResp;
+ // The code below is copied from PhoneInterfaceManager.java.
+ // TODO: move this code into IccOpenLogicalChannelResponse so that it can be shared.
+ if (ar.exception == null && ar.result != null) {
+ int[] result = (int[]) ar.result;
+ int channel = result[0];
+ byte[] selectResponse = null;
+ if (result.length > 1) {
+ selectResponse = new byte[result.length - 1];
+ for (int i = 1; i < result.length; ++i) {
+ selectResponse[i - 1] = (byte) result[i];
+ }
+ }
+ openChannelResp = new IccOpenLogicalChannelResponse(
+ channel, IccOpenLogicalChannelResponse.STATUS_NO_ERROR, selectResponse);
+ } else {
+ if (ar.result == null) {
+ Rlog.e(LOG_TAG, "Empty response");
+ }
+ if (ar.exception != null) {
+ Rlog.e(LOG_TAG, "Exception", ar.exception);
+ }
+
+ int errorCode = IccOpenLogicalChannelResponse.STATUS_UNKNOWN_ERROR;
+ if (ar.exception instanceof CommandException) {
+ CommandException.Error error =
+ ((CommandException) (ar.exception)).getCommandError();
+ if (error == CommandException.Error.MISSING_RESOURCE) {
+ errorCode = IccOpenLogicalChannelResponse.STATUS_MISSING_RESOURCE;
+ } else if (error == CommandException.Error.NO_SUCH_ELEMENT) {
+ errorCode = IccOpenLogicalChannelResponse.STATUS_NO_SUCH_ELEMENT;
+ }
+ }
+ openChannelResp = new IccOpenLogicalChannelResponse(
+ IccOpenLogicalChannelResponse.INVALID_CHANNEL, errorCode, null);
+ }
+
+ Rlog.v(LOG_TAG, "Response: " + openChannelResp);
+ return openChannelResp;
+ }
+}
diff --git a/com/android/internal/telephony/uicc/euicc/apdu/RequestBuilder.java b/com/android/internal/telephony/uicc/euicc/apdu/RequestBuilder.java
new file mode 100644
index 00000000..c2a63eea
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/apdu/RequestBuilder.java
@@ -0,0 +1,101 @@
+/*
+ * 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 com.android.internal.telephony.uicc.euicc.apdu;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A helper class to build a request for sending APDU commands. It contains a few convenient methods
+ * to add different APDU commands to the request.
+ *
+ * @hide
+ */
+public class RequestBuilder {
+ // The maximum bytes of the data of an APDU command. If more bytes need to be sent than this
+ // limit, we need to separate them into multiple commands.
+ private static final int MAX_APDU_DATA_LEN = 0xFF;
+ // The maximum bytes of the data of an extended APDU command. If more bytes need to be sent than
+ // this limit, we need to separate them into multiple commands.
+ private static final int MAX_EXT_APDU_DATA_LEN = 0xFFFF;
+
+ // Parameters used by STORE DATA command sent to ISD-R, defined by SGP.22 v2.0.
+ private static final int CLA_STORE_DATA = 0x80;
+ private static final int INS_STORE_DATA = 0xE2;
+ private static final int P1_STORE_DATA_INTERM = 0x11;
+ private static final int P1_STORE_DATA_END = 0x91;
+
+ private final int mChannel;
+ private final int mMaxApduDataLen;
+ private final List<ApduCommand> mCommands = new ArrayList<>();
+
+ /**
+ * Adds an APDU command by specifying every parts. The parameters are defined as in
+ * GlobalPlatform Card Specification v.2.3.
+ */
+ public void addApdu(int cla, int ins, int p1, int p2, int p3, String cmdHex) {
+ mCommands.add(new ApduCommand(mChannel, cla, ins, p1, p2, p3, cmdHex));
+ }
+
+ /**
+ * Adds an APDU command with given command data. P3 will be the length of the command data
+ * bytes. The parameters are defined as in GlobalPlatform Card Specification v.2.3.
+ */
+ public void addApdu(int cla, int ins, int p1, int p2, String cmdHex) {
+ mCommands.add(new ApduCommand(mChannel, cla, ins, p1, p2, cmdHex.length() / 2, cmdHex));
+ }
+
+ /**
+ * Adds an APDU command with empty command data. The parameters are defined as in GlobalPlatform
+ * Card Specification v.2.3.
+ */
+ public void addApdu(int cla, int ins, int p1, int p2) {
+ mCommands.add(new ApduCommand(mChannel, cla, ins, p1, p2, 0, ""));
+ }
+
+ /**
+ * Adds a STORE DATA command. Long command length of which is larger than {@link
+ * #mMaxApduDataLen} will be automatically split into multiple ones.
+ *
+ * @param cmdHex The STORE DATA command in a hex string as defined in GlobalPlatform Card
+ * Specification v.2.3.
+ */
+ public void addStoreData(String cmdHex) {
+ final int cmdLen = mMaxApduDataLen * 2;
+ int startPos = 0;
+ int totalLen = cmdHex.length() / 2;
+ int totalSubCmds = totalLen == 0 ? 1 : (totalLen + mMaxApduDataLen - 1) / mMaxApduDataLen;
+ for (int i = 1; i < totalSubCmds; ++i) {
+ String data = cmdHex.substring(startPos, startPos + cmdLen);
+ addApdu(CLA_STORE_DATA, INS_STORE_DATA, P1_STORE_DATA_INTERM, i - 1, mMaxApduDataLen,
+ data);
+ startPos += cmdLen;
+ }
+ String data = cmdHex.substring(startPos);
+ addApdu(CLA_STORE_DATA, INS_STORE_DATA, P1_STORE_DATA_END, totalSubCmds - 1,
+ totalLen % mMaxApduDataLen, data);
+ }
+
+ List<ApduCommand> getCommands() {
+ return mCommands;
+ }
+
+ RequestBuilder(int channel, boolean supportExtendedApdu) {
+ mChannel = channel;
+ mMaxApduDataLen = supportExtendedApdu ? MAX_EXT_APDU_DATA_LEN : MAX_APDU_DATA_LEN;
+ }
+}
diff --git a/com/android/internal/telephony/uicc/euicc/apdu/RequestProvider.java b/com/android/internal/telephony/uicc/euicc/apdu/RequestProvider.java
new file mode 100644
index 00000000..e3f6bf7a
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/apdu/RequestProvider.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.android.internal.telephony.uicc.euicc.apdu;
+
+/**
+ * Request provider provides a request via the given request builder. This interface allows the
+ * caller of {@link ApduSender} to build different requests based on the response of opening a
+ * logical channel.
+ *
+ * @hide
+ */
+public interface RequestProvider {
+ /**
+ * Builds a request based on the response of opening the logical channel.
+ *
+ * @param selectResponse Response of selecting the channel.
+ * @throws Throwable If an exception is thrown, the result callback {@code onException} will
+ * be called to return the exception and no commands will be sent.
+ */
+ void buildRequest(byte[] selectResponse, RequestBuilder requestBuilder) throws Throwable;
+}
diff --git a/com/android/internal/telephony/uicc/euicc/apdu/TransmitApduLogicalChannelInvocation.java b/com/android/internal/telephony/uicc/euicc/apdu/TransmitApduLogicalChannelInvocation.java
new file mode 100644
index 00000000..296fe476
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/apdu/TransmitApduLogicalChannelInvocation.java
@@ -0,0 +1,73 @@
+/*
+ * 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 com.android.internal.telephony.uicc.euicc.apdu;
+
+import android.os.AsyncResult;
+import android.os.Message;
+import android.telephony.Rlog;
+
+import com.android.internal.telephony.CommandException;
+import com.android.internal.telephony.CommandsInterface;
+import com.android.internal.telephony.uicc.IccIoResult;
+import com.android.internal.telephony.uicc.euicc.async.AsyncMessageInvocation;
+
+/**
+ * Invokes {@link CommandsInterface#iccTransmitApduLogicalChannel(int, int, int, int, int, int,
+ * String, Message)}. This takes an APDU command as the input and return the response. The status of
+ * returned response will be 0x6F00 if any error happens. No exception will be returned to the
+ * result callback.
+ *
+ * @hide
+ */
+public class TransmitApduLogicalChannelInvocation
+ extends AsyncMessageInvocation<ApduCommand, IccIoResult> {
+ private static final String LOG_TAG = "TransApdu";
+ private static final int SW1_ERROR = 0x6F;
+
+ private final CommandsInterface mCi;
+
+ TransmitApduLogicalChannelInvocation(CommandsInterface ci) {
+ mCi = ci;
+ }
+
+ @Override
+ protected void sendRequestMessage(ApduCommand command, Message msg) {
+ Rlog.v(LOG_TAG, "Send: " + command);
+ mCi.iccTransmitApduLogicalChannel(command.channel, command.cla | command.channel,
+ command.ins, command.p1, command.p2, command.p3, command.cmdHex, msg);
+ }
+
+ @Override
+ protected IccIoResult parseResult(AsyncResult ar) {
+ IccIoResult response;
+ if (ar.exception == null && ar.result != null) {
+ response = (IccIoResult) ar.result;
+ } else {
+ if (ar.result == null) {
+ Rlog.e(LOG_TAG, "Empty response");
+ } else if (ar.exception instanceof CommandException) {
+ Rlog.e(LOG_TAG, "CommandException", ar.exception);
+ } else {
+ Rlog.e(LOG_TAG, "CommandException", ar.exception);
+ }
+ response = new IccIoResult(SW1_ERROR, 0 /* sw2 */, (byte[]) null /* payload */);
+ }
+
+ Rlog.v(LOG_TAG, "Response: " + response);
+ return response;
+ }
+}
diff --git a/com/android/internal/telephony/uicc/euicc/async/AsyncMessageInvocation.java b/com/android/internal/telephony/uicc/euicc/async/AsyncMessageInvocation.java
new file mode 100644
index 00000000..61761cc9
--- /dev/null
+++ b/com/android/internal/telephony/uicc/euicc/async/AsyncMessageInvocation.java
@@ -0,0 +1,72 @@
+/*
+ * 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 com.android.internal.telephony.uicc.euicc.async;
+
+import android.os.AsyncResult;
+import android.os.Handler;
+import android.os.Message;
+
+/**
+ * This class wraps an invocation to an asynchronous method using {@link Message} to be working with
+ * {@link AsyncResultCallback}. With this class, you can use callbacks instead of managing a state
+ * machine to complete a task relying on multiple asynchronous method calls.
+ *
+ * <p>Subclasses should override the abstract methods to invoke the actual asynchronous method and
+ * parse the returned result.
+ *
+ * @param <Request> Class of the request data.
+ * @param <Response> Class of the response data.
+ *
+ * @hide
+ */
+public abstract class AsyncMessageInvocation<Request, Response> implements Handler.Callback {
+ /**
+ * Executes an invocation.
+ *
+ * @param request The request to be sent with the invocation.
+ * @param resultCallback Will be called after result is returned.
+ * @param handler The handler that {@code resultCallback} will be executed on.
+ */
+ public final void invoke(
+ Request request, AsyncResultCallback<Response> resultCallback, Handler handler) {
+ Handler h = new Handler(handler.getLooper(), this);
+ sendRequestMessage(request, h.obtainMessage(0, resultCallback));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean handleMessage(Message msg) {
+ AsyncResult result = (AsyncResult) msg.obj;
+ AsyncResultCallback<Response> resultCallback =
+ (AsyncResultCallback<Response>) result.userObj;
+ try {
+ resultCallback.onResult(parseResult(result));
+ } catch (Throwable t) {
+ resultCallback.onException(t);
+ }
+ return true;
+ }
+
+ /**
+ * Calls the asynchronous method with the given {@code msg}. The implementation should convert
+ * the given {@code request} to the parameters of the asynchronous method.
+ */
+ protected abstract void sendRequestMessage(Request request, Message msg);
+
+ /** Parses the asynchronous result returned by the method to a {@link Response}. */
+ protected abstract Response parseResult(AsyncResult result) throws Throwable;
+}
diff --git a/com/android/internal/telephony/util/SMSDispatcherUtil.java b/com/android/internal/telephony/util/SMSDispatcherUtil.java
new file mode 100644
index 00000000..d2ee223c
--- /dev/null
+++ b/com/android/internal/telephony/util/SMSDispatcherUtil.java
@@ -0,0 +1,199 @@
+/*
+ * 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 com.android.internal.telephony.util;
+
+import com.android.internal.telephony.ImsSmsDispatcher;
+import com.android.internal.telephony.cdma.CdmaSMSDispatcher;
+import com.android.internal.telephony.GsmAlphabet.TextEncodingDetails;
+import com.android.internal.telephony.Phone;
+import com.android.internal.telephony.SmsMessageBase;
+import com.android.internal.telephony.SmsHeader;
+import com.android.internal.telephony.gsm.GsmSMSDispatcher;
+
+/**
+ * Utilities used by {@link com.android.internal.telephony.SMSDispatcher}'s subclasses.
+ *
+ * These methods can not be moved to {@link CdmaSMSDispatcher} and {@link GsmSMSDispatcher} because
+ * they also need to be called from {@link ImsSmsDispatcher} and the utilities will invoke the cdma
+ * or gsm version of the method based on the format.
+ */
+public final class SMSDispatcherUtil {
+ // Prevent instantiation.
+ private SMSDispatcherUtil() {}
+
+ /**
+ * Whether to block SMS or not.
+ *
+ * @param isCdma true if cdma format should be used.
+ * @param phone the Phone to use
+ * @return true to block sms; false otherwise.
+ */
+ public static boolean shouldBlockSms(boolean isCdma, Phone phone) {
+ return isCdma && phone.isInEcm();
+ }
+
+ /**
+ * Trigger the proper implementation for getting submit pdu for text sms based on format.
+ *
+ * @param isCdma true if cdma format should be used.
+ * @param scAddr is the service center address or null to use the current default SMSC
+ * @param destAddr the address to send the message to
+ * @param message the body of the message.
+ * @param statusReportRequested whether or not a status report is requested.
+ * @param smsHeader message header.
+ * @return the submit pdu.
+ */
+ public static SmsMessageBase.SubmitPduBase getSubmitPdu(boolean isCdma, String scAddr,
+ String destAddr, String message, boolean statusReportRequested, SmsHeader smsHeader) {
+ if (isCdma) {
+ return getSubmitPduCdma(scAddr, destAddr, message, statusReportRequested, smsHeader);
+ } else {
+ return getSubmitPduGsm(scAddr, destAddr, message, statusReportRequested);
+ }
+ }
+
+ /**
+ * Gsm implementation for
+ * {@link #getSubmitPdu(boolean, String, String, String, boolean, SmsHeader)}
+ *
+ * @param scAddr is the service center address or null to use the current default SMSC
+ * @param destAddr the address to send the message to
+ * @param message the body of the message.
+ * @param statusReportRequested whether or not a status report is requested.
+ * @return the submit pdu.
+ */
+ public static SmsMessageBase.SubmitPduBase getSubmitPduGsm(String scAddr, String destAddr,
+ String message, boolean statusReportRequested) {
+ return com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(scAddr, destAddr, message,
+ statusReportRequested);
+ }
+
+ /**
+ * Cdma implementation for
+ * {@link #getSubmitPdu(boolean, String, String, String, boolean, SmsHeader)}
+ *
+ * @param scAddr is the service center address or null to use the current default SMSC
+ * @param destAddr the address to send the message to
+ * @param message the body of the message.
+ * @param statusReportRequested whether or not a status report is requested.
+ * @param smsHeader message header.
+ * @return the submit pdu.
+ */
+ public static SmsMessageBase.SubmitPduBase getSubmitPduCdma(String scAddr, String destAddr,
+ String message, boolean statusReportRequested, SmsHeader smsHeader) {
+ return com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(scAddr, destAddr,
+ message, statusReportRequested, smsHeader);
+ }
+
+ /**
+ * Trigger the proper implementation for getting submit pdu for data sms based on format.
+ *
+ * @param isCdma true if cdma format should be used.
+ * @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 destPort the port to deliver the message to
+ * @param message the body of the message to send
+ * @param statusReportRequested whether or not a status report is requested.
+ * @return the submit pdu.
+ */
+ public static SmsMessageBase.SubmitPduBase getSubmitPdu(boolean isCdma, String scAddr,
+ String destAddr, int destPort, byte[] message, boolean statusReportRequested) {
+ if (isCdma) {
+ return getSubmitPduCdma(scAddr, destAddr, destPort, message, statusReportRequested);
+ } else {
+ return getSubmitPduGsm(scAddr, destAddr, destPort, message, statusReportRequested);
+ }
+ }
+
+ /**
+ * Cdma implementation of {@link #getSubmitPdu(boolean, String, String, int, byte[], boolean)}
+
+ * @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 destPort the port to deliver the message to
+ * @param message the body of the message to send
+ * @param statusReportRequested whether or not a status report is requested.
+ * @return the submit pdu.
+ */
+ public static SmsMessageBase.SubmitPduBase getSubmitPduCdma(String scAddr, String destAddr,
+ int destPort, byte[] message, boolean statusReportRequested) {
+ return com.android.internal.telephony.cdma.SmsMessage.getSubmitPdu(scAddr, destAddr,
+ destPort, message, statusReportRequested);
+ }
+
+ /**
+ * Gsm implementation of {@link #getSubmitPdu(boolean, String, String, int, byte[], boolean)}
+ *
+ * @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 destPort the port to deliver the message to
+ * @param message the body of the message to send
+ * @param statusReportRequested whether or not a status report is requested.
+ * @return the submit pdu.
+ */
+ public static SmsMessageBase.SubmitPduBase getSubmitPduGsm(String scAddr, String destAddr,
+ int destPort, byte[] message, boolean statusReportRequested) {
+ return com.android.internal.telephony.gsm.SmsMessage.getSubmitPdu(scAddr, destAddr,
+ destPort, message, statusReportRequested);
+
+ }
+
+ /**
+ * Calculate the number of septets needed to encode the message. This function should only be
+ * called for individual segments of multipart message.
+ *
+ * @param isCdma true if cdma format should be used.
+ * @param messageBody the message to encode
+ * @param use7bitOnly ignore (but still count) illegal characters if true
+ * @return TextEncodingDetails
+ */
+ public static TextEncodingDetails calculateLength(boolean isCdma, CharSequence messageBody,
+ boolean use7bitOnly) {
+ if (isCdma) {
+ return calculateLengthCdma(messageBody, use7bitOnly);
+ } else {
+ return calculateLengthGsm(messageBody, use7bitOnly);
+ }
+ }
+
+ /**
+ * Gsm implementation for {@link #calculateLength(boolean, CharSequence, boolean)}
+ *
+ * @param messageBody the message to encode
+ * @param use7bitOnly ignore (but still count) illegal characters if true
+ * @return TextEncodingDetails
+ */
+ public static TextEncodingDetails calculateLengthGsm(CharSequence messageBody,
+ boolean use7bitOnly) {
+ return com.android.internal.telephony.cdma.SmsMessage.calculateLength(messageBody,
+ use7bitOnly, false);
+
+ }
+
+ /**
+ * Cdma implementation for {@link #calculateLength(boolean, CharSequence, boolean)}
+ *
+ * @param messageBody the message to encode
+ * @param use7bitOnly ignore (but still count) illegal characters if true
+ * @return TextEncodingDetails
+ */
+ public static TextEncodingDetails calculateLengthCdma(CharSequence messageBody,
+ boolean use7bitOnly) {
+ return com.android.internal.telephony.gsm.SmsMessage.calculateLength(messageBody,
+ use7bitOnly);
+ }
+}
diff --git a/com/android/internal/telephony/util/TimeStampedValue.java b/com/android/internal/telephony/util/TimeStampedValue.java
new file mode 100644
index 00000000..e2628f6b
--- /dev/null
+++ b/com/android/internal/telephony/util/TimeStampedValue.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 com.android.internal.telephony.util;
+
+import android.os.SystemClock;
+
+/**
+ * A pair containing a value and an associated time stamp.
+ *
+ * @param <T> The type of the value.
+ */
+public final class TimeStampedValue<T> {
+
+ /** The value. */
+ public final T mValue;
+
+ /**
+ * The value of {@link SystemClock#elapsedRealtime} or equivalent when value was
+ * determined.
+ */
+ public final long mElapsedRealtime;
+
+ public TimeStampedValue(T value, long elapsedRealtime) {
+ this.mValue = value;
+ this.mElapsedRealtime = elapsedRealtime;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ TimeStampedValue<?> that = (TimeStampedValue<?>) o;
+
+ if (mElapsedRealtime != that.mElapsedRealtime) {
+ return false;
+ }
+ return mValue != null ? mValue.equals(that.mValue) : that.mValue == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mValue != null ? mValue.hashCode() : 0;
+ result = 31 * result + (int) (mElapsedRealtime ^ (mElapsedRealtime >>> 32));
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "TimeStampedValue{"
+ + "mValue=" + mValue
+ + ", elapsedRealtime=" + mElapsedRealtime
+ + '}';
+ }
+}
diff --git a/com/android/internal/util/ArrayUtils.java b/com/android/internal/util/ArrayUtils.java
index aa856688..621619c5 100644
--- a/com/android/internal/util/ArrayUtils.java
+++ b/com/android/internal/util/ArrayUtils.java
@@ -184,6 +184,13 @@ public class ArrayUtils {
}
/**
+ * Length of the given collection or 0 if it's null.
+ */
+ public static int size(@Nullable Collection<?> collection) {
+ return collection == null ? 0 : collection.size();
+ }
+
+ /**
* Checks that value is present as at least one of the elements of the array.
* @param array the array to check in
* @param value the value to check for
@@ -612,6 +619,10 @@ public class ArrayUtils {
return size - leftIdx;
}
+ public static @NonNull int[] defeatNullable(@Nullable int[] val) {
+ return (val != null) ? val : EmptyArray.INT;
+ }
+
public static @NonNull String[] defeatNullable(@Nullable String[] val) {
return (val != null) ? val : EmptyArray.STRING;
}
diff --git a/com/android/internal/util/CollectionUtils.java b/com/android/internal/util/CollectionUtils.java
index 7985e574..2f2c747d 100644
--- a/com/android/internal/util/CollectionUtils.java
+++ b/com/android/internal/util/CollectionUtils.java
@@ -16,8 +16,6 @@
package com.android.internal.util;
-import static com.android.internal.util.ArrayUtils.isEmpty;
-
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.util.ArraySet;
@@ -173,13 +171,20 @@ public class CollectionUtils {
}
/**
- * Returns the size of the given list, or 0 if the list is null
+ * Returns the size of the given collection, or 0 if null
*/
public static int size(@Nullable Collection<?> cur) {
return cur != null ? cur.size() : 0;
}
/**
+ * Returns whether the given collection {@link Collection#isEmpty is empty} or {@code null}
+ */
+ public static boolean isEmpty(@Nullable Collection<?> cur) {
+ return size(cur) == 0;
+ }
+
+ /**
* Returns the elements of the given list that are of type {@code c}
*/
public static @NonNull <T> List<T> filter(@Nullable List<?> list, Class<T> c) {
diff --git a/com/android/internal/util/ConcurrentUtils.java b/com/android/internal/util/ConcurrentUtils.java
index e35f9f45..e08eb587 100644
--- a/com/android/internal/util/ConcurrentUtils.java
+++ b/com/android/internal/util/ConcurrentUtils.java
@@ -18,11 +18,13 @@ package com.android.internal.util;
import android.os.Process;
+import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -86,4 +88,27 @@ public class ConcurrentUtils {
}
}
+ /**
+ * Waits for {@link CountDownLatch#countDown()} to be called on the {@param countDownLatch}.
+ * <p>If {@link CountDownLatch#countDown()} doesn't occur within {@param timeoutMs}, this
+ * method will throw {@code IllegalStateException}
+ * <p>If {@code InterruptedException} occurs, this method will interrupt the current thread
+ * and throw {@code IllegalStateException}
+ *
+ * @param countDownLatch the CountDownLatch which {@link CountDownLatch#countDown()} is
+ * being waited on.
+ * @param timeoutMs the maximum time waited for {@link CountDownLatch#countDown()}
+ * @param description a short description of the operation
+ */
+ public static void waitForCountDownNoInterrupt(CountDownLatch countDownLatch, long timeoutMs,
+ String description) {
+ try {
+ if (!countDownLatch.await(timeoutMs, TimeUnit.MILLISECONDS)) {
+ throw new IllegalStateException(description + " timed out.");
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new IllegalStateException(description + " interrupted.");
+ }
+ }
}
diff --git a/com/android/internal/util/DumpUtils.java b/com/android/internal/util/DumpUtils.java
index 66b777e8..2b510337 100644
--- a/com/android/internal/util/DumpUtils.java
+++ b/com/android/internal/util/DumpUtils.java
@@ -102,6 +102,7 @@ public final class DumpUtils {
case android.os.Process.ROOT_UID:
case android.os.Process.SYSTEM_UID:
case android.os.Process.SHELL_UID:
+ case android.os.Process.INCIDENTD_UID:
return true;
}
diff --git a/com/android/internal/util/ObjectUtils.java b/com/android/internal/util/ObjectUtils.java
index 59e5a640..379602ac 100644
--- a/com/android/internal/util/ObjectUtils.java
+++ b/com/android/internal/util/ObjectUtils.java
@@ -29,6 +29,9 @@ public class ObjectUtils {
return a != null ? a : Preconditions.checkNotNull(b);
}
+ /**
+ * Compares two {@link Nullable} objects with {@code null} values considered the smallest
+ */
public static <T extends Comparable> int compare(@Nullable T a, @Nullable T b) {
if (a != null) {
return (b != null) ? a.compareTo(b) : 1;
@@ -36,4 +39,13 @@ public class ObjectUtils {
return (b != null) ? -1 : 0;
}
}
+
+ /**
+ * @return {@code null} if the given instance is not of the given calss, or the given
+ * instance otherwise
+ */
+ @Nullable
+ public static <S, T extends S> T castOrNull(@Nullable S instance, @NonNull Class<T> c) {
+ return c.isInstance(instance) ? (T) instance : null;
+ }
}
diff --git a/com/android/internal/util/RingBuffer.java b/com/android/internal/util/RingBuffer.java
index 9a6e542c..8fc4c30e 100644
--- a/com/android/internal/util/RingBuffer.java
+++ b/com/android/internal/util/RingBuffer.java
@@ -67,16 +67,21 @@ public class RingBuffer<T> {
*/
public T getNextSlot() {
final int nextSlotIdx = indexOf(mCursor++);
- T item = mBuffer[nextSlotIdx];
- if (item == null) {
- try {
- item = (T) mBuffer.getClass().getComponentType().newInstance();
- } catch (IllegalAccessException | InstantiationException e) {
- return null;
- }
- mBuffer[nextSlotIdx] = item;
+ if (mBuffer[nextSlotIdx] == null) {
+ mBuffer[nextSlotIdx] = createNewItem();
+ }
+ return mBuffer[nextSlotIdx];
+ }
+
+ /**
+ * @return a new object of type <T> or null if a new object could not be created.
+ */
+ protected T createNewItem() {
+ try {
+ return (T) mBuffer.getClass().getComponentType().newInstance();
+ } catch (IllegalAccessException | InstantiationException e) {
+ return null;
}
- return item;
}
public T[] toArray() {
diff --git a/com/android/internal/util/ScreenshotHelper.java b/com/android/internal/util/ScreenshotHelper.java
new file mode 100644
index 00000000..7fd94c68
--- /dev/null
+++ b/com/android/internal/util/ScreenshotHelper.java
@@ -0,0 +1,139 @@
+package com.android.internal.util;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+
+public class ScreenshotHelper {
+ private static final String TAG = "ScreenshotHelper";
+
+ private static final String SYSUI_PACKAGE = "com.android.systemui";
+ private static final String SYSUI_SCREENSHOT_SERVICE =
+ "com.android.systemui.screenshot.TakeScreenshotService";
+ private static final String SYSUI_SCREENSHOT_ERROR_RECEIVER =
+ "com.android.systemui.screenshot.ScreenshotServiceErrorReceiver";
+
+ // Time until we give up on the screenshot & show an error instead.
+ private final int SCREENSHOT_TIMEOUT_MS = 10000;
+
+ private final Object mScreenshotLock = new Object();
+ private ServiceConnection mScreenshotConnection = null;
+ private final Context mContext;
+
+ public ScreenshotHelper(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Request a screenshot be taken.
+ *
+ * @param screenshotType The type of screenshot, for example either
+ * {@link android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN}
+ * or {@link android.view.WindowManager.TAKE_SCREENSHOT_SELECTED_REGION}
+ * @param hasStatus {@code true} if the status bar is currently showing. {@code false} if not.
+ * @param hasNav {@code true} if the navigation bar is currently showing. {@code false} if not.
+ * @param handler A handler used in case the screenshot times out
+ */
+ public void takeScreenshot(final int screenshotType, final boolean hasStatus,
+ final boolean hasNav, @NonNull Handler handler) {
+ synchronized (mScreenshotLock) {
+ if (mScreenshotConnection != null) {
+ return;
+ }
+ final ComponentName serviceComponent = new ComponentName(SYSUI_PACKAGE,
+ SYSUI_SCREENSHOT_SERVICE);
+ final Intent serviceIntent = new Intent();
+
+ final Runnable mScreenshotTimeout = new Runnable() {
+ @Override public void run() {
+ synchronized (mScreenshotLock) {
+ if (mScreenshotConnection != null) {
+ mContext.unbindService(mScreenshotConnection);
+ mScreenshotConnection = null;
+ notifyScreenshotError();
+ }
+ }
+ }
+ };
+
+ serviceIntent.setComponent(serviceComponent);
+ ServiceConnection conn = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ synchronized (mScreenshotLock) {
+ if (mScreenshotConnection != this) {
+ return;
+ }
+ Messenger messenger = new Messenger(service);
+ Message msg = Message.obtain(null, screenshotType);
+ final ServiceConnection myConn = this;
+ Handler h = new Handler(handler.getLooper()) {
+ @Override
+ public void handleMessage(Message msg) {
+ synchronized (mScreenshotLock) {
+ if (mScreenshotConnection == myConn) {
+ mContext.unbindService(mScreenshotConnection);
+ mScreenshotConnection = null;
+ handler.removeCallbacks(mScreenshotTimeout);
+ }
+ }
+ }
+ };
+ msg.replyTo = new Messenger(h);
+ msg.arg1 = hasStatus ? 1: 0;
+ msg.arg2 = hasNav ? 1: 0;
+ try {
+ messenger.send(msg);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Couldn't take screenshot: " + e);
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ synchronized (mScreenshotLock) {
+ if (mScreenshotConnection != null) {
+ mContext.unbindService(mScreenshotConnection);
+ mScreenshotConnection = null;
+ handler.removeCallbacks(mScreenshotTimeout);
+ notifyScreenshotError();
+ }
+ }
+ }
+ };
+ if (mContext.bindServiceAsUser(serviceIntent, conn,
+ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
+ UserHandle.CURRENT)) {
+ mScreenshotConnection = conn;
+ handler.postDelayed(mScreenshotTimeout, SCREENSHOT_TIMEOUT_MS);
+ }
+ }
+ }
+
+ /**
+ * Notifies the screenshot service to show an error.
+ */
+ private void notifyScreenshotError() {
+ // If the service process is killed, then ask it to clean up after itself
+ final ComponentName errorComponent = new ComponentName(SYSUI_PACKAGE,
+ SYSUI_SCREENSHOT_ERROR_RECEIVER);
+ // Broadcast needs to have a valid action. We'll just pick
+ // a generic one, since the receiver here doesn't care.
+ Intent errorIntent = new Intent(Intent.ACTION_USER_PRESENT);
+ errorIntent.setComponent(errorComponent);
+ errorIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT |
+ Intent.FLAG_RECEIVER_FOREGROUND);
+ mContext.sendBroadcastAsUser(errorIntent, UserHandle.CURRENT);
+ }
+
+}
diff --git a/com/android/internal/view/IInputConnectionWrapper.java b/com/android/internal/view/IInputConnectionWrapper.java
index 28291aef..e08caa8e 100644
--- a/com/android/internal/view/IInputConnectionWrapper.java
+++ b/com/android/internal/view/IInputConnectionWrapper.java
@@ -23,6 +23,7 @@ import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Bundle;
import android.os.Handler;
+import android.os.LocaleList;
import android.os.Looper;
import android.os.Message;
import android.os.RemoteException;
@@ -64,6 +65,7 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub {
private static final int DO_REQUEST_UPDATE_CURSOR_ANCHOR_INFO = 140;
private static final int DO_CLOSE_CONNECTION = 150;
private static final int DO_COMMIT_CONTENT = 160;
+ private static final int DO_REPORT_LANGUAGE_HINT = 170;
@GuardedBy("mLock")
@Nullable
@@ -217,6 +219,10 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub {
callback));
}
+ public void reportLanguageHint(@NonNull LocaleList languageHint) {
+ dispatchMessage(obtainMessageO(DO_REPORT_LANGUAGE_HINT, languageHint));
+ }
+
void dispatchMessage(Message msg) {
// If we are calling this from the main thread, then we can call
// right through. Otherwise, we need to send the message to the
@@ -577,6 +583,16 @@ public abstract class IInputConnectionWrapper extends IInputContext.Stub {
}
return;
}
+ case DO_REPORT_LANGUAGE_HINT: {
+ final LocaleList languageHint = (LocaleList) msg.obj;
+ final InputConnection ic = getInputConnection();
+ if (ic == null || !isActive()) {
+ Log.w(TAG, "reportLanguageHint on inactive InputConnection");
+ return;
+ }
+ ic.reportLanguageHint(languageHint);
+ return;
+ }
}
Log.w(TAG, "Unhandled message code: " + msg.what);
}
diff --git a/com/android/internal/view/InputConnectionWrapper.java b/com/android/internal/view/InputConnectionWrapper.java
index 5b65bbe1..34be598d 100644
--- a/com/android/internal/view/InputConnectionWrapper.java
+++ b/com/android/internal/view/InputConnectionWrapper.java
@@ -22,6 +22,7 @@ import android.annotation.NonNull;
import android.inputmethodservice.AbstractInputMethodService;
import android.os.Bundle;
import android.os.Handler;
+import android.os.LocaleList;
import android.os.RemoteException;
import android.os.SystemClock;
import android.util.Log;
@@ -620,6 +621,14 @@ public class InputConnectionWrapper implements InputConnection {
}
@AnyThread
+ public void reportLanguageHint(@NonNull LocaleList languageHint) {
+ try {
+ mIInputContext.reportLanguageHint(languageHint);
+ } catch (RemoteException e) {
+ }
+ }
+
+ @AnyThread
private boolean isMethodMissing(@MissingMethodFlags final int methodFlag) {
return (mMissingMethods & methodFlag) == methodFlag;
}
diff --git a/com/android/internal/widget/MessagingGroup.java b/com/android/internal/widget/MessagingGroup.java
index 792f9212..5577d6e8 100644
--- a/com/android/internal/widget/MessagingGroup.java
+++ b/com/android/internal/widget/MessagingGroup.java
@@ -20,17 +20,12 @@ import android.annotation.AttrRes;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.StyleRes;
+import android.app.Notification;
import android.content.Context;
-import android.graphics.Color;
-import android.graphics.ColorFilter;
-import android.graphics.PorterDuff;
-import android.graphics.PorterDuffColorFilter;
-import android.graphics.drawable.Drawable;
import android.graphics.drawable.Icon;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Pools;
-import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@@ -41,7 +36,6 @@ import android.widget.LinearLayout;
import android.widget.RemoteViews;
import com.android.internal.R;
-import com.android.internal.util.NotificationColorUtil;
import java.util.ArrayList;
import java.util.List;
@@ -60,12 +54,13 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou
private int mLayoutColor;
private CharSequence mAvatarName = "";
private Icon mAvatarIcon;
- private ColorFilter mMessageBackgroundFilter;
private int mTextColor;
private List<MessagingMessage> mMessages;
private ArrayList<MessagingMessage> mAddedMessages = new ArrayList<>();
private boolean mFirstLayout;
private boolean mIsHidingAnimated;
+ private boolean mNeedsGeneratedAvatar;
+ private Notification.Person mSender;
public MessagingGroup(@NonNull Context context) {
super(context);
@@ -94,27 +89,23 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou
mAvatarView = findViewById(R.id.message_icon);
}
- public void setSender(CharSequence sender) {
- if (sender == null) {
- mAvatarView.setVisibility(GONE);
- mSenderName.setVisibility(GONE);
- setGravity(Gravity.END);
- mMessageBackgroundFilter = new PorterDuffColorFilter(mLayoutColor,
- PorterDuff.Mode.SRC_ATOP);
- mTextColor = NotificationColorUtil.isColorLight(mLayoutColor) ? getNormalTextColor()
- : Color.WHITE;
- } else {
- mSenderName.setText(sender);
- mAvatarView.setVisibility(VISIBLE);
- mSenderName.setVisibility(VISIBLE);
- setGravity(Gravity.START);
- mMessageBackgroundFilter = null;
- mTextColor = getNormalTextColor();
+ public void setSender(Notification.Person sender, CharSequence nameOverride) {
+ mSender = sender;
+ if (nameOverride == null) {
+ nameOverride = sender.getName();
+ }
+ mSenderName.setText(nameOverride);
+ mNeedsGeneratedAvatar = sender.getIcon() == null;
+ if (!mNeedsGeneratedAvatar) {
+ setAvatar(sender.getIcon());
}
+ mAvatarView.setVisibility(VISIBLE);
+ mSenderName.setVisibility(VISIBLE);
+ mTextColor = getNormalTextColor();
}
private int getNormalTextColor() {
- return mContext.getColor(R.color.notification_primary_text_color_light);
+ return mContext.getColor(R.color.notification_secondary_text_color_light);
}
public void setAvatar(Icon icon) {
@@ -207,10 +198,6 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou
return mSenderName.getText();
}
- public void setSenderVisible(boolean visible) {
- mSenderName.setVisibility(visible ? VISIBLE : GONE);
- }
-
public static void dropCache() {
sInstancePool = new Pools.SynchronizedPool<>(10);
}
@@ -317,12 +304,6 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou
mMessageContainer.removeView(message);
mMessageContainer.addView(message, messageIndex);
}
- // Let's make sure the message color is correct
- Drawable targetDrawable = message.getBackground();
-
- if (targetDrawable != null) {
- targetDrawable.mutate().setColorFilter(mMessageBackgroundFilter);
- }
message.setTextColor(mTextColor);
}
mMessages = group;
@@ -379,7 +360,7 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou
return 0;
}
- public View getSender() {
+ public View getSenderView() {
return mSenderName;
}
@@ -390,4 +371,12 @@ public class MessagingGroup extends LinearLayout implements MessagingLinearLayou
public MessagingLinearLayout getMessageContainer() {
return mMessageContainer;
}
+
+ public boolean needsGeneratedAvatar() {
+ return mNeedsGeneratedAvatar;
+ }
+
+ public Notification.Person getSender() {
+ return mSender;
+ }
}
diff --git a/com/android/internal/widget/MessagingLayout.java b/com/android/internal/widget/MessagingLayout.java
index 2acdc015..d45c086e 100644
--- a/com/android/internal/widget/MessagingLayout.java
+++ b/com/android/internal/widget/MessagingLayout.java
@@ -35,7 +35,6 @@ import android.util.ArrayMap;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.RemotableViewMethod;
-import android.view.View;
import android.view.ViewTreeObserver;
import android.view.animation.Interpolator;
import android.view.animation.PathInterpolator;
@@ -69,7 +68,6 @@ public class MessagingLayout extends FrameLayout {
private List<MessagingMessage> mMessages = new ArrayList<>();
private List<MessagingMessage> mHistoricMessages = new ArrayList<>();
private MessagingLinearLayout mMessagingLinearLayout;
- private View mContractedMessage;
private boolean mShowHistoricMessages;
private ArrayList<MessagingGroup> mGroups = new ArrayList<>();
private TextView mTitleView;
@@ -81,6 +79,8 @@ public class MessagingLayout extends FrameLayout {
private Icon mLargeIcon;
private boolean mIsOneToOne;
private ArrayList<MessagingGroup> mAddedGroups = new ArrayList<>();
+ private Notification.Person mUser;
+ private CharSequence mNameReplacement;
public MessagingLayout(@NonNull Context context) {
super(context);
@@ -122,6 +122,11 @@ public class MessagingLayout extends FrameLayout {
}
@RemotableViewMethod
+ public void setNameReplacement(CharSequence nameReplacement) {
+ mNameReplacement = nameReplacement;
+ }
+
+ @RemotableViewMethod
public void setData(Bundle extras) {
Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
List<Notification.MessagingStyle.Message> newMessages
@@ -129,14 +134,30 @@ public class MessagingLayout extends FrameLayout {
Parcelable[] histMessages = extras.getParcelableArray(Notification.EXTRA_HISTORIC_MESSAGES);
List<Notification.MessagingStyle.Message> newHistoricMessages
= Notification.MessagingStyle.Message.getMessagesFromBundleArray(histMessages);
+ setUser(extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON));
mConversationTitle = null;
TextView headerText = findViewById(R.id.header_text);
if (headerText != null) {
mConversationTitle = headerText.getText();
}
+ addRemoteInputHistoryToMessages(newMessages,
+ extras.getCharSequenceArray(Notification.EXTRA_REMOTE_INPUT_HISTORY));
bind(newMessages, newHistoricMessages);
}
+ private void addRemoteInputHistoryToMessages(
+ List<Notification.MessagingStyle.Message> newMessages,
+ CharSequence[] remoteInputHistory) {
+ if (remoteInputHistory == null || remoteInputHistory.length == 0) {
+ return;
+ }
+ for (int i = remoteInputHistory.length - 1; i >= 0; i--) {
+ CharSequence message = remoteInputHistory[i];
+ newMessages.add(new Notification.MessagingStyle.Message(
+ message, 0, (Notification.Person) null));
+ }
+ }
+
private void bind(List<Notification.MessagingStyle.Message> newMessages,
List<Notification.MessagingStyle.Message> newHistoricMessages) {
@@ -152,7 +173,6 @@ public class MessagingLayout extends FrameLayout {
mMessages = messages;
mHistoricMessages = historicMessages;
- updateContractedMessage();
updateHistoricMessageVisibility();
updateTitleAndNamesDisplay();
}
@@ -163,12 +183,10 @@ public class MessagingLayout extends FrameLayout {
for (int i = 0; i < mGroups.size(); i++) {
MessagingGroup group = mGroups.get(i);
CharSequence senderName = group.getSenderName();
- if (TextUtils.isEmpty(senderName)) {
+ if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
continue;
}
- boolean visible = !mIsOneToOne;
- group.setSenderVisible(visible);
- if ((visible || mLargeIcon == null) && !uniqueNames.containsKey(senderName)) {
+ if (!uniqueNames.containsKey(senderName)) {
char c = senderName.charAt(0);
if (uniqueCharacters.containsKey(c)) {
// this character was already used, lets make it more unique. We first need to
@@ -191,8 +209,10 @@ public class MessagingLayout extends FrameLayout {
for (int i = 0; i < mGroups.size(); i++) {
// Let's now set the avatars
MessagingGroup group = mGroups.get(i);
+ boolean isOwnMessage = group.getSender() == mUser;
CharSequence senderName = group.getSenderName();
- if (TextUtils.isEmpty(senderName) || (mIsOneToOne && mLargeIcon != null)) {
+ if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)
+ || (mIsOneToOne && mLargeIcon != null && !isOwnMessage)) {
continue;
}
String symbol = uniqueNames.get(senderName);
@@ -207,10 +227,10 @@ public class MessagingLayout extends FrameLayout {
// Let's now set the avatars
MessagingGroup group = mGroups.get(i);
CharSequence senderName = group.getSenderName();
- if (TextUtils.isEmpty(senderName)) {
+ if (!group.needsGeneratedAvatar() || TextUtils.isEmpty(senderName)) {
continue;
}
- if (mIsOneToOne && mLargeIcon != null) {
+ if (mIsOneToOne && mLargeIcon != null && group.getSender() != mUser) {
group.setAvatar(mLargeIcon);
} else {
Icon cachedIcon = cachedAvatars.get(senderName);
@@ -234,7 +254,7 @@ public class MessagingLayout extends FrameLayout {
canvas.drawCircle(radius, radius, radius, mPaint);
boolean needDarkText = ColorUtils.calculateLuminance(color) > 0.5f;
mTextPaint.setColor(needDarkText ? Color.BLACK : Color.WHITE);
- mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.75f : mAvatarSize * 0.4f);
+ mTextPaint.setTextSize(symbol.length() == 1 ? mAvatarSize * 0.5f : mAvatarSize * 0.3f);
int yPos = (int) (radius - ((mTextPaint.descent() + mTextPaint.ascent()) / 2)) ;
canvas.drawText(symbol, radius, yPos, mTextPaint);
return Icon.createWithBitmap(bitmap);
@@ -270,11 +290,21 @@ public class MessagingLayout extends FrameLayout {
mIsOneToOne = oneToOne;
}
+ public void setUser(Notification.Person user) {
+ mUser = user;
+ if (mUser.getIcon() == null) {
+ Icon userIcon = Icon.createWithResource(getContext(),
+ com.android.internal.R.drawable.messaging_user);
+ userIcon.setTint(mLayoutColor);
+ mUser.setIcon(userIcon);
+ }
+ }
+
private void addMessagesToGroups(List<MessagingMessage> historicMessages,
List<MessagingMessage> messages) {
// Let's first find our groups!
List<List<MessagingMessage>> groups = new ArrayList<>();
- List<CharSequence> senders = new ArrayList<>();
+ List<Notification.Person> senders = new ArrayList<>();
// Lets first find the groups
findGroups(historicMessages, messages, groups, senders);
@@ -283,7 +313,8 @@ public class MessagingLayout extends FrameLayout {
createGroupViews(groups, senders);
}
- private void createGroupViews(List<List<MessagingMessage>> groups, List<CharSequence> senders) {
+ private void createGroupViews(List<List<MessagingMessage>> groups,
+ List<Notification.Person> senders) {
mGroups.clear();
for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) {
List<MessagingMessage> group = groups.get(groupIndex);
@@ -301,7 +332,12 @@ public class MessagingLayout extends FrameLayout {
mAddedGroups.add(newGroup);
}
newGroup.setLayoutColor(mLayoutColor);
- newGroup.setSender(senders.get(groupIndex));
+ Notification.Person sender = senders.get(groupIndex);
+ CharSequence nameOverride = null;
+ if (sender != mUser && mNameReplacement != null) {
+ nameOverride = mNameReplacement;
+ }
+ newGroup.setSender(sender, nameOverride);
mGroups.add(newGroup);
if (mMessagingLinearLayout.indexOfChild(newGroup) != groupIndex) {
@@ -314,8 +350,8 @@ public class MessagingLayout extends FrameLayout {
private void findGroups(List<MessagingMessage> historicMessages,
List<MessagingMessage> messages, List<List<MessagingMessage>> groups,
- List<CharSequence> senders) {
- CharSequence currentSender = null;
+ List<Notification.Person> senders) {
+ CharSequence currentSenderKey = null;
List<MessagingMessage> currentGroup = null;
int histSize = historicMessages.size();
for (int i = 0; i < histSize + messages.size(); i++) {
@@ -326,35 +362,23 @@ public class MessagingLayout extends FrameLayout {
message = messages.get(i - histSize);
}
boolean isNewGroup = currentGroup == null;
- CharSequence sender = message.getMessage().getSender();
- isNewGroup |= !TextUtils.equals(sender, currentSender);
+ Notification.Person sender = message.getMessage().getSenderPerson();
+ CharSequence key = sender == null ? null
+ : sender.getKey() == null ? sender.getName() : sender.getKey();
+ isNewGroup |= !TextUtils.equals(key, currentSenderKey);
if (isNewGroup) {
currentGroup = new ArrayList<>();
groups.add(currentGroup);
+ if (sender == null) {
+ sender = mUser;
+ }
senders.add(sender);
- currentSender = sender;
+ currentSenderKey = key;
}
currentGroup.add(message);
}
}
- private void updateContractedMessage() {
- for (int i = mMessages.size() - 1; i >= 0; i--) {
- MessagingMessage m = mMessages.get(i);
- // Incoming messages have a non-empty sender.
- if (!TextUtils.isEmpty(m.getMessage().getSender())) {
- mContractedMessage = m;
- return;
- }
- }
- if (!mMessages.isEmpty()) {
- // No incoming messages, fall back to outgoing message
- mContractedMessage = mMessages.get(mMessages.size() - 1);
- return;
- }
- mContractedMessage = null;
- }
-
/**
* Creates new messages, reusing existing ones if they are available.
*
@@ -418,7 +442,7 @@ public class MessagingLayout extends FrameLayout {
continue;
}
MessagingPropertyAnimator.fadeIn(group.getAvatar());
- MessagingPropertyAnimator.fadeIn(group.getSender());
+ MessagingPropertyAnimator.fadeIn(group.getSenderView());
MessagingPropertyAnimator.startLocalTranslationFrom(group,
group.getHeight(), LINEAR_OUT_SLOW_IN);
}
@@ -430,10 +454,6 @@ public class MessagingLayout extends FrameLayout {
}
}
- public View getContractedMessage() {
- return mContractedMessage;
- }
-
public MessagingLinearLayout getMessagingLinearLayout() {
return mMessagingLinearLayout;
}
diff --git a/com/android/internal/widget/RecyclerView.java b/com/android/internal/widget/RecyclerView.java
index 7abc76a8..408a4e9b 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.scrollBy(hScroll, vScroll);
+ mRecyclerView.smoothScrollBy(hScroll, vScroll);
return true;
}
diff --git a/com/android/internal/widget/ResolverDrawerLayout.java b/com/android/internal/widget/ResolverDrawerLayout.java
index 7635a727..6f2246aa 100644
--- a/com/android/internal/widget/ResolverDrawerLayout.java
+++ b/com/android/internal/widget/ResolverDrawerLayout.java
@@ -22,9 +22,7 @@ import com.android.internal.R;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
-import android.graphics.Color;
import android.graphics.Rect;
-import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcel;
@@ -504,7 +502,7 @@ public class ResolverDrawerLayout extends ViewGroup {
}
private void onCollapsedChanged(boolean isCollapsed) {
- notifyViewAccessibilityStateChangedIfNeeded(
+ notifyAccessibilityStateChanged(
AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
if (mScrollIndicatorDrawable != null) {
diff --git a/com/android/keyguard/CarrierText.java b/com/android/keyguard/CarrierText.java
index 13c48d0d..45d1aad8 100644
--- a/com/android/keyguard/CarrierText.java
+++ b/com/android/keyguard/CarrierText.java
@@ -268,6 +268,18 @@ public class CarrierText extends TextView {
}
}
+ @Override
+ protected void onVisibilityChanged(View changedView, int visibility) {
+ super.onVisibilityChanged(changedView, visibility);
+
+ // Only show marquee when visible
+ if (visibility == VISIBLE) {
+ setEllipsize(TextUtils.TruncateAt.MARQUEE);
+ } else {
+ setEllipsize(TextUtils.TruncateAt.END);
+ }
+ }
+
/**
* Top-level function for creating carrier text. Makes text based on simState, PLMN
* and SPN as well as device capabilities, such as being emergency call capable.
diff --git a/com/android/keyguard/KeyguardAbsKeyInputView.java b/com/android/keyguard/KeyguardAbsKeyInputView.java
index a9804139..d63ad084 100644
--- a/com/android/keyguard/KeyguardAbsKeyInputView.java
+++ b/com/android/keyguard/KeyguardAbsKeyInputView.java
@@ -280,7 +280,7 @@ public abstract class KeyguardAbsKeyInputView extends LinearLayout
@Override
public void showPromptReason(int reason) {
if (reason != PROMPT_REASON_NONE) {
- int promtReasonStringRes = getPromtReasonStringRes(reason);
+ int promtReasonStringRes = getPromptReasonStringRes(reason);
if (promtReasonStringRes != 0) {
mSecurityMessageDisplay.setMessage(promtReasonStringRes);
}
@@ -288,12 +288,12 @@ public abstract class KeyguardAbsKeyInputView extends LinearLayout
}
@Override
- public void showMessage(String message, int color) {
+ public void showMessage(CharSequence message, int color) {
mSecurityMessageDisplay.setNextMessageColor(color);
mSecurityMessageDisplay.setMessage(message);
}
- protected abstract int getPromtReasonStringRes(int reason);
+ protected abstract int getPromptReasonStringRes(int reason);
// Cause a VIRTUAL_KEY vibration
public void doHapticKeyClick() {
diff --git a/com/android/keyguard/KeyguardHostView.java b/com/android/keyguard/KeyguardHostView.java
index 27a3f7d4..f1a5ca9f 100644
--- a/com/android/keyguard/KeyguardHostView.java
+++ b/com/android/keyguard/KeyguardHostView.java
@@ -34,6 +34,7 @@ import android.widget.FrameLayout;
import com.android.internal.widget.LockPatternUtils;
import com.android.keyguard.KeyguardSecurityContainer.SecurityCallback;
import com.android.keyguard.KeyguardSecurityModel.SecurityMode;
+import com.android.settingslib.Utils;
import java.io.File;
@@ -171,10 +172,14 @@ public class KeyguardHostView extends FrameLayout implements SecurityCallback {
mSecurityContainer.showPromptReason(reason);
}
- public void showMessage(String message, int color) {
+ public void showMessage(CharSequence message, int color) {
mSecurityContainer.showMessage(message, color);
}
+ public void showErrorMessage(CharSequence message) {
+ showMessage(message, Utils.getColorError(mContext));
+ }
+
/**
* Dismisses the keyguard by going to the next screen or making it gone.
* @param targetUserId a user that needs to be the foreground user at the dismissal completion.
diff --git a/com/android/keyguard/KeyguardPasswordView.java b/com/android/keyguard/KeyguardPasswordView.java
index b6184a88..ff5f5e77 100644
--- a/com/android/keyguard/KeyguardPasswordView.java
+++ b/com/android/keyguard/KeyguardPasswordView.java
@@ -117,7 +117,7 @@ public class KeyguardPasswordView extends KeyguardAbsKeyInputView
}
@Override
- protected int getPromtReasonStringRes(int reason) {
+ protected int getPromptReasonStringRes(int reason) {
switch (reason) {
case PROMPT_REASON_RESTART:
return R.string.kg_prompt_reason_restart_password;
diff --git a/com/android/keyguard/KeyguardPatternView.java b/com/android/keyguard/KeyguardPatternView.java
index d636316d..cb066a10 100644
--- a/com/android/keyguard/KeyguardPatternView.java
+++ b/com/android/keyguard/KeyguardPatternView.java
@@ -398,7 +398,7 @@ public class KeyguardPatternView extends LinearLayout implements KeyguardSecurit
}
@Override
- public void showMessage(String message, int color) {
+ public void showMessage(CharSequence message, int color) {
mSecurityMessageDisplay.setNextMessageColor(color);
mSecurityMessageDisplay.setMessage(message);
}
diff --git a/com/android/keyguard/KeyguardPinBasedInputView.java b/com/android/keyguard/KeyguardPinBasedInputView.java
index c04ae68d..6539ccff 100644
--- a/com/android/keyguard/KeyguardPinBasedInputView.java
+++ b/com/android/keyguard/KeyguardPinBasedInputView.java
@@ -103,7 +103,7 @@ public abstract class KeyguardPinBasedInputView extends KeyguardAbsKeyInputView
}
@Override
- protected int getPromtReasonStringRes(int reason) {
+ protected int getPromptReasonStringRes(int reason) {
switch (reason) {
case PROMPT_REASON_RESTART:
return R.string.kg_prompt_reason_restart_pin;
diff --git a/com/android/keyguard/KeyguardSecurityContainer.java b/com/android/keyguard/KeyguardSecurityContainer.java
index 9f393215..8dc4609f 100644
--- a/com/android/keyguard/KeyguardSecurityContainer.java
+++ b/com/android/keyguard/KeyguardSecurityContainer.java
@@ -543,8 +543,7 @@ public class KeyguardSecurityContainer extends FrameLayout implements KeyguardSe
}
}
-
- public void showMessage(String message, int color) {
+ public void showMessage(CharSequence message, int color) {
if (mCurrentSecuritySelection != SecurityMode.None) {
getSecurityView(mCurrentSecuritySelection).showMessage(message, color);
}
diff --git a/com/android/keyguard/KeyguardSecurityView.java b/com/android/keyguard/KeyguardSecurityView.java
index 82908420..360dba3b 100644
--- a/com/android/keyguard/KeyguardSecurityView.java
+++ b/com/android/keyguard/KeyguardSecurityView.java
@@ -106,7 +106,7 @@ public interface KeyguardSecurityView {
* @param message the message to show
* @param color the color to use
*/
- void showMessage(String message, int color);
+ void showMessage(CharSequence message, int color);
/**
* Instruct the view to show usability hints, if any.
diff --git a/com/android/keyguard/KeyguardSecurityViewFlipper.java b/com/android/keyguard/KeyguardSecurityViewFlipper.java
index 6012c450..a2ff8f78 100644
--- a/com/android/keyguard/KeyguardSecurityViewFlipper.java
+++ b/com/android/keyguard/KeyguardSecurityViewFlipper.java
@@ -139,7 +139,7 @@ public class KeyguardSecurityViewFlipper extends ViewFlipper implements Keyguard
}
@Override
- public void showMessage(String message, int color) {
+ public void showMessage(CharSequence message, int color) {
KeyguardSecurityView ksv = getSecurityView();
if (ksv != null) {
ksv.showMessage(message, color);
diff --git a/com/android/keyguard/KeyguardSimPinView.java b/com/android/keyguard/KeyguardSimPinView.java
index 6e0b56e2..e7432ba5 100644
--- a/com/android/keyguard/KeyguardSimPinView.java
+++ b/com/android/keyguard/KeyguardSimPinView.java
@@ -168,7 +168,7 @@ public class KeyguardSimPinView extends KeyguardPinBasedInputView {
}
@Override
- protected int getPromtReasonStringRes(int reason) {
+ protected int getPromptReasonStringRes(int reason) {
// No message on SIM Pin
return 0;
}
diff --git a/com/android/keyguard/KeyguardSimPukView.java b/com/android/keyguard/KeyguardSimPukView.java
index 876d170e..afee8ece 100644
--- a/com/android/keyguard/KeyguardSimPukView.java
+++ b/com/android/keyguard/KeyguardSimPukView.java
@@ -211,7 +211,7 @@ public class KeyguardSimPukView extends KeyguardPinBasedInputView {
}
@Override
- protected int getPromtReasonStringRes(int reason) {
+ protected int getPromptReasonStringRes(int reason) {
// No message on SIM Puk
return 0;
}
diff --git a/com/android/keyguard/KeyguardSliceView.java b/com/android/keyguard/KeyguardSliceView.java
index b9bf80de..9ff68155 100644
--- a/com/android/keyguard/KeyguardSliceView.java
+++ b/com/android/keyguard/KeyguardSliceView.java
@@ -26,6 +26,9 @@ import android.graphics.Paint;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.provider.Settings;
+import android.text.Layout;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
@@ -40,6 +43,7 @@ import com.android.systemui.R;
import com.android.systemui.keyguard.KeyguardSliceProvider;
import com.android.systemui.tuner.TunerService;
+import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
@@ -98,9 +102,6 @@ public class KeyguardSliceView extends LinearLayout implements View.OnClickListe
protected void onAttachedToWindow() {
super.onAttachedToWindow();
- // Set initial content
- showSlice(Slice.bindSlice(getContext(), mKeyguardSliceUri));
-
// Make sure we always have the most current slice
mLiveData.observeForever(this);
}
@@ -132,11 +133,25 @@ public class KeyguardSliceView extends LinearLayout implements View.OnClickListe
android.app.slice.SliceItem.FORMAT_TEXT,
new String[]{android.app.slice.Slice.HINT_TITLE},
null /* nonHints */);
- mTitle.setText(mainTitle.getText());
+ CharSequence title = mainTitle.getText();
+ mTitle.setText(title);
+
+ // Check if we're already ellipsizing the text.
+ // We're going to figure out the best possible line break if not.
+ Layout layout = mTitle.getLayout();
+ if (layout != null){
+ final int lineCount = layout.getLineCount();
+ if (lineCount > 0) {
+ if (layout.getEllipsisCount(lineCount - 1) == 0) {
+ mTitle.setText(findBestLineBreak(title));
+ }
+ }
+ }
}
mClickActions.clear();
final int subItemsCount = subItems.size();
+ final int blendedColor = getTextColor();
for (int i = 0; i < subItemsCount; i++) {
SliceItem item = subItems.get(i);
@@ -145,7 +160,7 @@ public class KeyguardSliceView extends LinearLayout implements View.OnClickListe
KeyguardSliceButton button = mRow.findViewWithTag(itemTag);
if (button == null) {
button = new KeyguardSliceButton(mContext);
- button.setTextColor(mTextColor);
+ button.setTextColor(blendedColor);
button.setTag(itemTag);
} else {
mRow.removeView(button);
@@ -198,13 +213,53 @@ public class KeyguardSliceView extends LinearLayout implements View.OnClickListe
mListener.accept(mHasHeader);
}
+ /**
+ * Breaks a string in 2 lines where both have similar character count
+ * but first line is always longer.
+ *
+ * @param charSequence Original text.
+ * @return Optimal string.
+ */
+ private CharSequence findBestLineBreak(CharSequence charSequence) {
+ if (TextUtils.isEmpty(charSequence)) {
+ return charSequence;
+ }
+
+ String source = charSequence.toString();
+ // Ignore if there is only 1 word,
+ // or if line breaks were manually set.
+ if (source.contains("\n") || !source.contains(" ")) {
+ return source;
+ }
+
+ final String[] words = source.split(" ");
+ final StringBuilder optimalString = new StringBuilder(source.length());
+ int current = 0;
+ while (optimalString.length() < source.length() - optimalString.length()) {
+ optimalString.append(words[current]);
+ if (current < words.length - 1) {
+ optimalString.append(" ");
+ }
+ current++;
+ }
+ optimalString.append("\n");
+ for (int i = current; i < words.length; i++) {
+ optimalString.append(words[i]);
+ if (current < words.length - 1) {
+ optimalString.append(" ");
+ }
+ }
+
+ return optimalString.toString();
+ }
+
public void setDark(float darkAmount) {
mDarkAmount = darkAmount;
updateTextColors();
}
private void updateTextColors() {
- final int blendedColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount);
+ final int blendedColor = getTextColor();
mTitle.setTextColor(blendedColor);
int childCount = mRow.getChildCount();
for (int i = 0; i < childCount; i++) {
@@ -265,16 +320,20 @@ public class KeyguardSliceView extends LinearLayout implements View.OnClickListe
if (wasObserving) {
mLiveData.observeForever(this);
- showSlice(Slice.bindSlice(getContext(), mKeyguardSliceUri));
}
}
+ public int getTextColor() {
+ return ColorUtils.blendARGB(mTextColor, Color.WHITE, mDarkAmount);
+ }
+
/**
* Representation of an item that appears under the clock on main keyguard message.
* Shows optional separator.
*/
private class KeyguardSliceButton extends Button {
+ private static final float SEPARATOR_HEIGHT = 0.7f;
private final Paint mPaint;
private boolean mHasDivider;
@@ -291,6 +350,9 @@ public class KeyguardSliceView extends LinearLayout implements View.OnClickListe
setPadding(horizontalPadding, 0, horizontalPadding, 0);
setCompoundDrawablePadding((int) context.getResources()
.getDimension(R.dimen.widget_icon_padding));
+ setMaxWidth(KeyguardSliceView.this.getWidth() / 2);
+ setMaxLines(1);
+ setEllipsize(TruncateAt.END);
}
public void setHasDivider(boolean hasDivider) {
@@ -308,7 +370,9 @@ public class KeyguardSliceView extends LinearLayout implements View.OnClickListe
super.onDraw(canvas);
if (mHasDivider) {
final int lineX = getLayoutDirection() == LAYOUT_DIRECTION_RTL ? 0 : getWidth();
- canvas.drawLine(lineX, 0, lineX, getHeight(), mPaint);
+ final int height = (int) (getHeight() * SEPARATOR_HEIGHT);
+ final int startY = getHeight() / 2 - height / 2;
+ canvas.drawLine(lineX, startY, lineX, startY + height, mPaint);
}
}
}
diff --git a/com/android/keyguard/KeyguardStatusView.java b/com/android/keyguard/KeyguardStatusView.java
index 4b9a8744..2b656c22 100644
--- a/com/android/keyguard/KeyguardStatusView.java
+++ b/com/android/keyguard/KeyguardStatusView.java
@@ -16,7 +16,6 @@
package com.android.keyguard;
-import android.app.ActivityManager;
import android.app.AlarmManager;
import android.content.Context;
import android.content.res.Configuration;
@@ -40,7 +39,6 @@ import android.widget.TextClock;
import android.widget.TextView;
import com.android.internal.widget.LockPatternUtils;
-import com.android.systemui.ChargingView;
import com.google.android.collect.Sets;
@@ -60,7 +58,6 @@ public class KeyguardStatusView extends GridLayout {
private View mClockSeparator;
private TextView mOwnerInfo;
private ViewGroup mClockContainer;
- private ChargingView mBatteryDoze;
private KeyguardSliceView mKeyguardSlice;
private Runnable mPendingMarqueeStart;
private Handler mHandler;
@@ -155,11 +152,9 @@ public class KeyguardStatusView extends GridLayout {
mClockView.setAccessibilityDelegate(new KeyguardClockAccessibilityDelegate(mContext));
}
mOwnerInfo = findViewById(R.id.owner_info);
- mBatteryDoze = findViewById(R.id.battery_doze);
mKeyguardSlice = findViewById(R.id.keyguard_status_area);
mClockSeparator = findViewById(R.id.clock_separator);
- mVisibleInDoze = Sets.newArraySet(mBatteryDoze, mClockView, mKeyguardSlice,
- mClockSeparator);
+ mVisibleInDoze = Sets.newArraySet(mClockView, mKeyguardSlice, mClockSeparator);
mTextColor = mClockView.getCurrentTextColor();
mKeyguardSlice.setListener(this::onSliceContentChanged);
@@ -176,19 +171,16 @@ public class KeyguardStatusView extends GridLayout {
}
private void onSliceContentChanged(boolean hasHeader) {
- final float clockScale = hasHeader ? mSmallClockScale : 1;
+ final boolean smallClock = hasHeader || mPulsing;
+ final float clockScale = smallClock ? mSmallClockScale : 1;
float translation = (mClockView.getHeight() - (mClockView.getHeight() * clockScale)) / 2f;
- if (hasHeader) {
+ if (smallClock) {
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);
+ mClockSeparator.setVisibility(hasHeader && !mPulsing ? VISIBLE : GONE);
}
@Override
@@ -310,7 +302,7 @@ public class KeyguardStatusView extends GridLayout {
}
}
- public void setDark(float darkAmount) {
+ public void setDarkAmount(float darkAmount) {
if (mDarkAmount == darkAmount) {
return;
}
@@ -331,7 +323,6 @@ public class KeyguardStatusView extends GridLayout {
final int blendedTextColor = ColorUtils.blendARGB(mTextColor, Color.WHITE, darkAmount);
updateDozeVisibleViews();
- mBatteryDoze.setDark(dark);
mKeyguardSlice.setDark(darkAmount);
mClockView.setTextColor(blendedTextColor);
mClockSeparator.setBackgroundColor(blendedTextColor);
@@ -339,6 +330,8 @@ public class KeyguardStatusView extends GridLayout {
public void setPulsing(boolean pulsing) {
mPulsing = pulsing;
+ mKeyguardSlice.setVisibility(pulsing ? GONE : VISIBLE);
+ onSliceContentChanged(mKeyguardSlice.hasHeader());
updateDozeVisibleViews();
}
diff --git a/com/android/keyguard/KeyguardUpdateMonitor.java b/com/android/keyguard/KeyguardUpdateMonitor.java
index 2058f15b..9e4b4055 100644
--- a/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -372,6 +372,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
}
}
+ @Override
+ public void onTrustError(CharSequence message) {
+ dispatchErrorMessage(message);
+ }
+
protected void handleSimSubscriptionInfoChanged() {
if (DEBUG_SIM_STATES) {
Log.v(TAG, "onSubscriptionInfoChanged()");
@@ -706,6 +711,15 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
return mScreenOn;
}
+ private void dispatchErrorMessage(CharSequence message) {
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ KeyguardUpdateMonitorCallback cb = mCallbacks.get(i).get();
+ if (cb != null) {
+ cb.onTrustAgentErrorMessage(message);
+ }
+ }
+ }
+
static class DisplayClientState {
public int clientGeneration;
public boolean clearing;
@@ -977,6 +991,12 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
maxChargingWattage > fastThreshold ? CHARGING_FAST :
CHARGING_REGULAR;
}
+
+ @Override
+ public String toString() {
+ return "BatteryStatus{status=" + status + ",level=" + level + ",plugged=" + plugged
+ + ",health=" + health + ",maxChargingWattage=" + maxChargingWattage + "}";
+ }
}
public class StrongAuthTracker extends LockPatternUtils.StrongAuthTracker {
@@ -1237,7 +1257,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
mFingerprintCancelSignal.cancel();
}
mFingerprintCancelSignal = new CancellationSignal();
- mFpm.authenticate(null, mFingerprintCancelSignal, 0, mAuthenticationCallback, null, userId);
+ mFpm.authenticate(null, mFingerprintCancelSignal, 0, mAuthenticationCallback, null,
+ userId);
setFingerprintRunningState(FINGERPRINT_STATE_RUNNING);
}
}
@@ -1599,11 +1620,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
}
}
- private static boolean isBatteryUpdateInteresting(BatteryStatus old, BatteryStatus current) {
+ private boolean isBatteryUpdateInteresting(BatteryStatus old, BatteryStatus current) {
final boolean nowPluggedIn = current.isPluggedIn();
final boolean wasPluggedIn = old.isPluggedIn();
- final boolean stateChangedWhilePluggedIn =
- wasPluggedIn == true && nowPluggedIn == true
+ final boolean stateChangedWhilePluggedIn = wasPluggedIn && nowPluggedIn
&& (old.status != current.status);
// change in plug state is always interesting
@@ -1611,13 +1631,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener {
return true;
}
- // change in battery level while plugged in
- if (nowPluggedIn && old.level != current.level) {
- return true;
- }
-
- // change where battery needs charging
- if (!nowPluggedIn && current.isBatteryLow() && current.level != old.level) {
+ // change in battery level
+ if (old.level != current.level) {
return true;
}
diff --git a/com/android/keyguard/KeyguardUpdateMonitorCallback.java b/com/android/keyguard/KeyguardUpdateMonitorCallback.java
index 8d55eea4..1afcca64 100644
--- a/com/android/keyguard/KeyguardUpdateMonitorCallback.java
+++ b/com/android/keyguard/KeyguardUpdateMonitorCallback.java
@@ -17,13 +17,14 @@ package com.android.keyguard;
import android.app.admin.DevicePolicyManager;
import android.graphics.Bitmap;
+import android.hardware.fingerprint.FingerprintManager;
import android.media.AudioManager;
import android.os.SystemClock;
-import android.hardware.fingerprint.FingerprintManager;
import android.telephony.TelephonyManager;
import android.view.WindowManagerPolicyConstants;
import com.android.internal.telephony.IccCardConstants;
+import com.android.systemui.statusbar.KeyguardIndicationController;
/**
* Callback for general information relevant to lock screen.
@@ -271,4 +272,15 @@ public class KeyguardUpdateMonitorCallback {
* @param dreaming true if the dream's window has been created and is visible
*/
public void onDreamingStateChanged(boolean dreaming) { }
+
+ /**
+ * Called when an error message needs to be presented on the keyguard.
+ * Message will be visible briefly, and might be overridden by other keyguard events,
+ * like fingerprint authentication errors.
+ *
+ * @param message Message that indicates an error.
+ * @see KeyguardIndicationController.BaseKeyguardCallback#HIDE_DELAY_MS
+ * @see KeyguardIndicationController#showTransientIndication(CharSequence)
+ */
+ public void onTrustAgentErrorMessage(CharSequence message) { }
}
diff --git a/com/android/keyguard/ViewMediatorCallback.java b/com/android/keyguard/ViewMediatorCallback.java
index eff84c6a..5c681236 100644
--- a/com/android/keyguard/ViewMediatorCallback.java
+++ b/com/android/keyguard/ViewMediatorCallback.java
@@ -99,4 +99,10 @@ public interface ViewMediatorCallback {
* Invoked when the secondary display showing a keyguard window changes.
*/
void onSecondaryDisplayShowingChanged(int displayId);
+
+ /**
+ * Consumes a message that was enqueued to be displayed on the next time the bouncer shows up.
+ * @return Message that should be displayed above the challenge.
+ */
+ CharSequence consumeCustomMessage();
}
diff --git a/com/android/layoutlib/bridge/android/BridgePackageManager.java b/com/android/layoutlib/bridge/android/BridgePackageManager.java
index 37dc166f..eb836ca6 100644
--- a/com/android/layoutlib/bridge/android/BridgePackageManager.java
+++ b/com/android/layoutlib/bridge/android/BridgePackageManager.java
@@ -18,7 +18,6 @@ package com.android.layoutlib.bridge.android;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.PackageInstallObserver;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
@@ -51,7 +50,6 @@ import android.content.res.XmlResourceParser;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
-import android.net.Uri;
import android.os.Handler;
import android.os.UserHandle;
import android.os.storage.VolumeInfo;
@@ -112,6 +110,11 @@ public class BridgePackageManager extends PackageManager {
}
@Override
+ public Intent getCarLaunchIntentForPackage(String packageName) {
+ return null;
+ }
+
+ @Override
public int[] getPackageGids(String packageName) throws NameNotFoundException {
return new int[0];
}
@@ -611,11 +614,6 @@ public class BridgePackageManager extends PackageManager {
}
@Override
- public void installPackage(Uri packageURI, PackageInstallObserver observer, int flags,
- String installerPackageName) {
- }
-
- @Override
public int installExistingPackage(String packageName) throws NameNotFoundException {
return 0;
}
diff --git a/com/android/layoutlib/bridge/android/BridgePowerManager.java b/com/android/layoutlib/bridge/android/BridgePowerManager.java
index ed428ec9..3c1dc8d8 100644
--- a/com/android/layoutlib/bridge/android/BridgePowerManager.java
+++ b/com/android/layoutlib/bridge/android/BridgePowerManager.java
@@ -117,16 +117,6 @@ public class BridgePowerManager implements IPowerManager {
}
@Override
- public void setTemporaryScreenAutoBrightnessAdjustmentSettingOverride(float arg0) throws RemoteException {
- // pass for now.
- }
-
- @Override
- public void setTemporaryScreenBrightnessSettingOverride(int arg0) throws RemoteException {
- // pass for now.
- }
-
- @Override
public void setStayOnSetting(int arg0) throws RemoteException {
// pass for now.
}
diff --git a/com/android/layoutlib/bridge/android/BridgeWindowSession.java b/com/android/layoutlib/bridge/android/BridgeWindowSession.java
index bf37d637..336c78d8 100644
--- a/com/android/layoutlib/bridge/android/BridgeWindowSession.java
+++ b/com/android/layoutlib/bridge/android/BridgeWindowSession.java
@@ -29,6 +29,7 @@ import android.view.IWindowId;
import android.view.IWindowSession;
import android.view.InputChannel;
import android.view.Surface;
+import android.view.SurfaceControl;
import android.view.SurfaceView;
import android.view.WindowManager.LayoutParams;
@@ -129,20 +130,12 @@ public final class BridgeWindowSession implements IWindowSession {
}
@Override
- public IBinder prepareDrag(IWindow window, int flags,
- int thumbnailWidth, int thumbnailHeight, Surface outSurface)
- throws RemoteException {
- // pass for now
- return null;
- }
-
- @Override
- public boolean performDrag(IWindow window, IBinder dragToken,
+ public IBinder performDrag(IWindow window, int flags, SurfaceControl surface,
int touchSource, float touchX, float touchY, float thumbCenterX, float thumbCenterY,
ClipData data)
throws RemoteException {
// pass for now
- return false;
+ return null;
}
@Override
@@ -231,4 +224,10 @@ public final class BridgeWindowSession implements IWindowSession {
public void updatePointerIcon(IWindow window) {
// pass for now.
}
+
+ @Override
+ public void updateTapExcludeRegion(IWindow window, int regionId, int left, int top, int width,
+ int height) {
+ // pass for now.
+ }
}
diff --git a/com/android/media/MediaBrowser2Impl.java b/com/android/media/MediaBrowser2Impl.java
new file mode 100644
index 00000000..3abff130
--- /dev/null
+++ b/com/android/media/MediaBrowser2Impl.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 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 com.android.media;
+
+import android.content.Context;
+import android.media.IMediaSession2;
+import android.media.MediaBrowser2;
+import android.media.MediaBrowser2.BrowserCallback;
+import android.media.MediaSession2.CommandButton;
+import android.media.SessionToken2;
+import android.media.update.MediaBrowser2Provider;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+public class MediaBrowser2Impl extends MediaController2Impl implements MediaBrowser2Provider {
+ private final String TAG = "MediaBrowser2";
+ private final boolean DEBUG = true; // TODO(jaewan): change.
+
+ private final MediaBrowser2 mInstance;
+ private final MediaBrowser2.BrowserCallback mCallback;
+
+ public MediaBrowser2Impl(MediaBrowser2 instance, Context context, SessionToken2 token,
+ BrowserCallback callback, Executor executor) {
+ super(instance, context, token, callback, executor);
+ mInstance = instance;
+ mCallback = callback;
+ }
+
+ @Override
+ public void getBrowserRoot_impl(Bundle rootHints) {
+ final IMediaSession2 binder = getSessionBinder();
+ if (binder != null) {
+ try {
+ binder.getBrowserRoot(getControllerStub(), rootHints);
+ } catch (RemoteException e) {
+ // TODO(jaewan): Handle disconnect.
+ if (DEBUG) {
+ Log.w(TAG, "Cannot connect to the service or the session is gone", e);
+ }
+ }
+ } else {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ }
+ }
+
+ @Override
+ public void subscribe_impl(String parentId, Bundle options) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void unsubscribe_impl(String parentId, Bundle options) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void getItem_impl(String mediaId) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void getChildren_impl(String parentId, int page, int pageSize, Bundle options) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void search_impl(String query, int page, int pageSize, Bundle extras) {
+ // TODO(jaewan): Implement
+ }
+
+ public void onGetRootResult(
+ final Bundle rootHints, final String rootMediaId, final Bundle rootExtra) {
+ getCallbackExecutor().execute(() -> {
+ mCallback.onGetRootResult(rootHints, rootMediaId, rootExtra);
+ });
+ }
+
+ public void onCustomLayoutChanged(final List<CommandButton> layout) {
+ getCallbackExecutor().execute(() -> {
+ mCallback.onCustomLayoutChanged(layout);
+ });
+ }
+}
diff --git a/com/android/media/MediaController2Impl.java b/com/android/media/MediaController2Impl.java
new file mode 100644
index 00000000..35c5bef4
--- /dev/null
+++ b/com/android/media/MediaController2Impl.java
@@ -0,0 +1,601 @@
+/*
+ * Copyright 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 com.android.media;
+
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.media.IMediaSession2;
+import android.media.IMediaSession2Callback;
+import android.media.MediaController2.PlaybackInfo;
+import android.media.MediaItem2;
+import android.media.MediaSession2;
+import android.media.MediaSession2.Command;
+import android.media.MediaSession2.CommandButton;
+import android.media.MediaSession2.CommandGroup;
+import android.media.MediaController2;
+import android.media.MediaController2.ControllerCallback;
+import android.media.MediaSession2.PlaylistParam;
+import android.media.MediaSessionService2;
+import android.media.PlaybackState2;
+import android.media.Rating2;
+import android.media.SessionToken2;
+import android.media.session.PlaybackState;
+import android.media.update.MediaController2Provider;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.support.annotation.GuardedBy;
+import android.util.Log;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+public class MediaController2Impl implements MediaController2Provider {
+ private static final String TAG = "MediaController2";
+ private static final boolean DEBUG = true; // TODO(jaewan): Change
+
+ private final MediaController2 mInstance;
+
+ /**
+ * Flag used by MediaController2Record to filter playback callback.
+ */
+ static final int CALLBACK_FLAG_PLAYBACK = 0x1;
+
+ static final int REQUEST_CODE_ALL = 0;
+
+ private final Object mLock = new Object();
+
+ private final Context mContext;
+ private final MediaSession2CallbackStub mSessionCallbackStub;
+ private final SessionToken2 mToken;
+ private final ControllerCallback mCallback;
+ private final Executor mCallbackExecutor;
+ private final IBinder.DeathRecipient mDeathRecipient;
+
+ @GuardedBy("mLock")
+ private final List<PlaybackListenerHolder> mPlaybackListeners = new ArrayList<>();
+ @GuardedBy("mLock")
+ private SessionServiceConnection mServiceConnection;
+ @GuardedBy("mLock")
+ private boolean mIsReleased;
+
+ // Assignment should be used with the lock hold, but should be used without a lock to prevent
+ // potential deadlock.
+ // Postfix -Binder is added to explicitly show that it's potentially remote process call.
+ // Technically -Interface is more correct, but it may misread that it's interface (vs class)
+ // so let's keep this postfix until we find better postfix.
+ @GuardedBy("mLock")
+ private volatile IMediaSession2 mSessionBinder;
+
+ // TODO(jaewan): Require session activeness changed listener, because controller can be
+ // available when the session's player is null.
+ public MediaController2Impl(MediaController2 instance, Context context, SessionToken2 token,
+ ControllerCallback callback, Executor executor) {
+ mInstance = instance;
+
+ if (context == null) {
+ throw new IllegalArgumentException("context shouldn't be null");
+ }
+ if (token == null) {
+ throw new IllegalArgumentException("token shouldn't be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback shouldn't be null");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("executor shouldn't be null");
+ }
+ mContext = context;
+ mSessionCallbackStub = new MediaSession2CallbackStub(this);
+ mToken = token;
+ mCallback = callback;
+ mCallbackExecutor = executor;
+ mDeathRecipient = () -> {
+ mInstance.close();
+ };
+
+ mSessionBinder = null;
+
+ if (token.getSessionBinder() == null) {
+ mServiceConnection = new SessionServiceConnection();
+ connectToService();
+ } else {
+ mServiceConnection = null;
+ connectToSession(token.getSessionBinder());
+ }
+ }
+
+ // Should be only called by constructor.
+ private void connectToService() {
+ // Service. Needs to get fresh binder whenever connection is needed.
+ final Intent intent = new Intent(MediaSessionService2.SERVICE_INTERFACE);
+ intent.setClassName(mToken.getPackageName(), mToken.getServiceName());
+
+ // Use bindService() instead of startForegroundService() to start session service for three
+ // reasons.
+ // 1. Prevent session service owner's stopSelf() from destroying service.
+ // With the startForegroundService(), service's call of stopSelf() will trigger immediate
+ // onDestroy() calls on the main thread even when onConnect() is running in another
+ // thread.
+ // 2. Minimize APIs for developers to take care about.
+ // With bindService(), developers only need to take care about Service.onBind()
+ // but Service.onStartCommand() should be also taken care about with the
+ // startForegroundService().
+ // 3. Future support for UI-less playback
+ // If a service wants to keep running, it should be either foreground service or
+ // bounded service. But there had been request for the feature for system apps
+ // and using bindService() will be better fit with it.
+ // TODO(jaewan): Use bindServiceAsUser()??
+ boolean result = mContext.bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE);
+ if (!result) {
+ Log.w(TAG, "bind to " + mToken + " failed");
+ } else if (DEBUG) {
+ Log.d(TAG, "bind to " + mToken + " success");
+ }
+ }
+
+ private void connectToSession(IMediaSession2 sessionBinder) {
+ try {
+ sessionBinder.connect(mContext.getPackageName(), mSessionCallbackStub);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to call connection request. Framework will retry"
+ + " automatically");
+ }
+ }
+
+ @Override
+ public void close_impl() {
+ if (DEBUG) {
+ Log.d(TAG, "relese from " + mToken);
+ }
+ final IMediaSession2 binder;
+ synchronized (mLock) {
+ if (mIsReleased) {
+ // Prevent re-enterance from the ControllerCallback.onDisconnected()
+ return;
+ }
+ mIsReleased = true;
+ if (mServiceConnection != null) {
+ mContext.unbindService(mServiceConnection);
+ mServiceConnection = null;
+ }
+ mPlaybackListeners.clear();
+ binder = mSessionBinder;
+ mSessionBinder = null;
+ mSessionCallbackStub.destroy();
+ }
+ if (binder != null) {
+ try {
+ binder.asBinder().unlinkToDeath(mDeathRecipient, 0);
+ binder.release(mSessionCallbackStub);
+ } catch (RemoteException e) {
+ // No-op.
+ }
+ }
+ mCallbackExecutor.execute(() -> {
+ mCallback.onDisconnected();
+ });
+ }
+
+ IMediaSession2 getSessionBinder() {
+ return mSessionBinder;
+ }
+
+ MediaSession2CallbackStub getControllerStub() {
+ return mSessionCallbackStub;
+ }
+
+ Executor getCallbackExecutor() {
+ return mCallbackExecutor;
+ }
+
+ @Override
+ public SessionToken2 getSessionToken_impl() {
+ return mToken;
+ }
+
+ @Override
+ public boolean isConnected_impl() {
+ final IMediaSession2 binder = mSessionBinder;
+ return binder != null;
+ }
+
+ @Override
+ public void play_impl() {
+ sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_START);
+ }
+
+ @Override
+ public void pause_impl() {
+ sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE);
+ }
+
+ @Override
+ public void stop_impl() {
+ sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_STOP);
+ }
+
+ @Override
+ public void skipToPrevious_impl() {
+ sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM);
+ }
+
+ @Override
+ public void skipToNext_impl() {
+ sendCommand(MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM);
+ }
+
+ private void sendCommand(int code) {
+ // TODO(jaewan): optimization) Cache Command objects?
+ Command command = new Command(code);
+ // TODO(jaewan): Check if the command is in the allowed group.
+
+ final IMediaSession2 binder = mSessionBinder;
+ if (binder != null) {
+ try {
+ binder.sendCommand(mSessionCallbackStub, command.toBundle(), null);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Cannot connect to the service or the session is gone", e);
+ }
+ } else {
+ Log.w(TAG, "Session isn't active", new IllegalStateException());
+ }
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // TODO(jaewan): Implement follows
+ //////////////////////////////////////////////////////////////////////////////////////
+ @Override
+ public PendingIntent getSessionActivity_impl() {
+ // TODO(jaewan): Implement
+ return null;
+ }
+
+ @Override
+ public int getRatingType_impl() {
+ // TODO(jaewan): Implement
+ return 0;
+ }
+
+ @Override
+ public void setVolumeTo_impl(int value, int flags) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void adjustVolume_impl(int direction, int flags) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public PlaybackInfo getPlaybackInfo_impl() {
+ // TODO(jaewan): Implement
+ return null;
+ }
+
+ @Override
+ public void prepareFromUri_impl(Uri uri, Bundle extras) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void prepareFromSearch_impl(String query, Bundle extras) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void prepareMediaId_impl(String mediaId, Bundle extras) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void playFromSearch_impl(String query, Bundle extras) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void playFromUri_impl(String uri, Bundle extras) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void playFromMediaId_impl(String mediaId, Bundle extras) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void setRating_impl(Rating2 rating) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void sendCustomCommand_impl(Command command, Bundle args, ResultReceiver cb) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public List<MediaItem2> getPlaylist_impl() {
+ // TODO(jaewan): Implement
+ return null;
+ }
+
+ @Override
+ public void prepare_impl() {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void fastForward_impl() {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void rewind_impl() {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void seekTo_impl(long pos) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void setCurrentPlaylistItem_impl(int index) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public PlaybackState2 getPlaybackState_impl() {
+ // TODO(jaewan): Implement
+ return null;
+ }
+
+ @Override
+ public void removePlaylistItem_impl(MediaItem2 index) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void addPlaylistItem_impl(int index, MediaItem2 item) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public PlaylistParam getPlaylistParam_impl() {
+ // TODO(jaewan): Implement
+ return null;
+ }
+
+ ///////////////////////////////////////////////////
+ // Protected or private methods
+ ///////////////////////////////////////////////////
+ // Should be used without a lock to prevent potential deadlock.
+ private void registerCallbackForPlaybackNotLocked() {
+ final IMediaSession2 binder = mSessionBinder;
+ if (binder != null) {
+ try {
+ binder.registerCallback(mSessionCallbackStub,
+ CALLBACK_FLAG_PLAYBACK, REQUEST_CODE_ALL);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Cannot connect to the service or the session is gone", e);
+ }
+ }
+ }
+
+ private void pushPlaybackStateChanges(final PlaybackState2 state) {
+ synchronized (mLock) {
+ for (int i = 0; i < mPlaybackListeners.size(); i++) {
+ mPlaybackListeners.get(i).postPlaybackChange(state);
+ }
+ }
+ }
+
+ // Called when the result for connecting to the session was delivered.
+ // Should be used without a lock to prevent potential deadlock.
+ private void onConnectionChangedNotLocked(IMediaSession2 sessionBinder,
+ CommandGroup commandGroup) {
+ if (DEBUG) {
+ Log.d(TAG, "onConnectionChangedNotLocked sessionBinder=" + sessionBinder
+ + ", commands=" + commandGroup);
+ }
+ boolean release = false;
+ try {
+ if (sessionBinder == null || commandGroup == null) {
+ // Connection rejected.
+ release = true;
+ return;
+ }
+ boolean registerCallbackForPlaybackNeeded;
+ synchronized (mLock) {
+ if (mIsReleased) {
+ return;
+ }
+ if (mSessionBinder != null) {
+ Log.e(TAG, "Cannot be notified about the connection result many times."
+ + " Probably a bug or malicious app.");
+ release = true;
+ return;
+ }
+ mSessionBinder = sessionBinder;
+ try {
+ // Implementation for the local binder is no-op,
+ // so can be used without worrying about deadlock.
+ mSessionBinder.asBinder().linkToDeath(mDeathRecipient, 0);
+ } catch (RemoteException e) {
+ if (DEBUG) {
+ Log.d(TAG, "Session died too early.", e);
+ }
+ release = true;
+ return;
+ }
+ registerCallbackForPlaybackNeeded = !mPlaybackListeners.isEmpty();
+ }
+ // TODO(jaewan): Keep commands to prevents illegal API calls.
+ mCallbackExecutor.execute(() -> {
+ mCallback.onConnected(commandGroup);
+ });
+ if (registerCallbackForPlaybackNeeded) {
+ registerCallbackForPlaybackNotLocked();
+ }
+ } finally {
+ if (release) {
+ // Trick to call release() without holding the lock, to prevent potential deadlock
+ // with the developer's custom lock within the ControllerCallback.onDisconnected().
+ mInstance.close();
+ }
+ }
+ }
+
+ // TODO(jaewan): Pull out this from the controller2, and rename it to the MediaController2Stub
+ // or MediaBrowser2Stub.
+ static class MediaSession2CallbackStub extends IMediaSession2Callback.Stub {
+ private final WeakReference<MediaController2Impl> mController;
+
+ private MediaSession2CallbackStub(MediaController2Impl controller) {
+ mController = new WeakReference<>(controller);
+ }
+
+ private MediaController2Impl getController() throws IllegalStateException {
+ final MediaController2Impl controller = mController.get();
+ if (controller == null) {
+ throw new IllegalStateException("Controller is released");
+ }
+ return controller;
+ }
+
+ // TODO(jaewan): Refactor code to get rid of these pattern.
+ private MediaBrowser2Impl getBrowser() throws IllegalStateException {
+ final MediaController2Impl controller = getController();
+ if (controller instanceof MediaBrowser2Impl) {
+ return (MediaBrowser2Impl) controller;
+ }
+ return null;
+ }
+
+ public void destroy() {
+ mController.clear();
+ }
+
+ @Override
+ public void onPlaybackStateChanged(Bundle state) throws RuntimeException {
+ final MediaController2Impl controller = getController();
+ controller.pushPlaybackStateChanges(PlaybackState2.fromBundle(state));
+ }
+
+ @Override
+ public void onConnectionChanged(IMediaSession2 sessionBinder, Bundle commandGroup)
+ throws RuntimeException {
+ final MediaController2Impl controller;
+ try {
+ controller = getController();
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+ return;
+ }
+ controller.onConnectionChangedNotLocked(
+ sessionBinder, CommandGroup.fromBundle(commandGroup));
+ }
+
+ @Override
+ public void onGetRootResult(Bundle rootHints, String rootMediaId, Bundle rootExtra)
+ throws RuntimeException {
+ final MediaBrowser2Impl browser;
+ try {
+ browser = getBrowser();
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+ return;
+ }
+ if (browser == null) {
+ // TODO(jaewan): Revisit here. Could be a bug
+ return;
+ }
+ browser.onGetRootResult(rootHints, rootMediaId, rootExtra);
+ }
+
+ @Override
+ public void onCustomLayoutChanged(List<Bundle> commandButtonlist) {
+ if (commandButtonlist == null) {
+ // Illegal call. Ignore
+ return;
+ }
+ final MediaBrowser2Impl browser;
+ try {
+ browser = getBrowser();
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "Don't fail silently here. Highly likely a bug");
+ return;
+ }
+ if (browser == null) {
+ // TODO(jaewan): Revisit here. Could be a bug
+ return;
+ }
+ List<CommandButton> layout = new ArrayList<>();
+ for (int i = 0; i < commandButtonlist.size(); i++) {
+ CommandButton button = CommandButton.fromBundle(commandButtonlist.get(i));
+ if (button != null) {
+ layout.add(button);
+ }
+ }
+ browser.onCustomLayoutChanged(layout);
+ }
+ }
+
+ // This will be called on the main thread.
+ private class SessionServiceConnection implements ServiceConnection {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ // Note that it's always main-thread.
+ if (DEBUG) {
+ Log.d(TAG, "onServiceConnected " + name + " " + this);
+ }
+ // Sanity check
+ if (!mToken.getPackageName().equals(name.getPackageName())) {
+ Log.wtf(TAG, name + " was connected, but expected pkg="
+ + mToken.getPackageName() + " with id=" + mToken.getId());
+ return;
+ }
+ final IMediaSession2 sessionBinder = IMediaSession2.Stub.asInterface(service);
+ connectToSession(sessionBinder);
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ // Temporal lose of the binding because of the service crash. System will automatically
+ // rebind, so just no-op.
+ // TODO(jaewan): Really? Either disconnect cleanly or
+ if (DEBUG) {
+ Log.w(TAG, "Session service " + name + " is disconnected.");
+ }
+ }
+
+ @Override
+ public void onBindingDied(ComponentName name) {
+ // Permanent lose of the binding because of the service package update or removed.
+ // This SessionServiceRecord will be removed accordingly, but forget session binder here
+ // for sure.
+ mInstance.close();
+ }
+ }
+}
diff --git a/com/android/media/MediaLibraryService2Impl.java b/com/android/media/MediaLibraryService2Impl.java
new file mode 100644
index 00000000..ca32de3e
--- /dev/null
+++ b/com/android/media/MediaLibraryService2Impl.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright 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 com.android.media;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.media.MediaLibraryService2;
+import android.media.MediaLibraryService2.MediaLibrarySession;
+import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
+import android.media.MediaPlayerBase;
+import android.media.MediaSession2;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.MediaSessionService2;
+import android.media.VolumeProvider;
+import android.media.update.MediaLibraryService2Provider;
+import android.os.Bundle;
+
+import java.util.concurrent.Executor;
+
+public class MediaLibraryService2Impl extends MediaSessionService2Impl implements
+ MediaLibraryService2Provider {
+ private final MediaSessionService2 mInstance;
+ private MediaLibrarySession mLibrarySession;
+
+ public MediaLibraryService2Impl(MediaLibraryService2 instance) {
+ super(instance);
+ mInstance = instance;
+ }
+
+ @Override
+ public void onCreate_impl() {
+ super.onCreate_impl();
+
+ // Effectively final
+ MediaSession2 session = getSession();
+ if (!(session instanceof MediaLibrarySession)) {
+ throw new RuntimeException("Expected MediaLibrarySession, but returned MediaSession2");
+ }
+ mLibrarySession = (MediaLibrarySession) getSession();
+ }
+
+ @Override
+ Intent createServiceIntent() {
+ Intent serviceIntent = new Intent(mInstance, mInstance.getClass());
+ serviceIntent.setAction(MediaLibraryService2.SERVICE_INTERFACE);
+ return serviceIntent;
+ }
+
+ public static class MediaLibrarySessionImpl extends MediaSession2Impl
+ implements MediaLibrarySessionProvider {
+ private final MediaLibrarySession mInstance;
+ private final MediaLibrarySessionCallback mCallback;
+
+ public MediaLibrarySessionImpl(MediaLibrarySession instance, Context context,
+ MediaPlayerBase player, String id, Executor callbackExecutor,
+ MediaLibrarySessionCallback callback, VolumeProvider volumeProvider, int ratingType,
+ PendingIntent sessionActivity) {
+ super(instance, context, player, id, callbackExecutor, callback, volumeProvider,
+ ratingType, sessionActivity);
+ mInstance = instance;
+ mCallback = callback;
+ }
+
+ @Override
+ public void notifyChildrenChanged_impl(ControllerInfo controller, String parentId,
+ Bundle options) {
+ // TODO(jaewan): Implements
+ }
+
+ @Override
+ public void notifyChildrenChanged_impl(String parentId, Bundle options) {
+ // TODO(jaewan): Implements
+ }
+ }
+}
diff --git a/com/android/media/MediaSession2Impl.java b/com/android/media/MediaSession2Impl.java
new file mode 100644
index 00000000..fffb1e93
--- /dev/null
+++ b/com/android/media/MediaSession2Impl.java
@@ -0,0 +1,482 @@
+/*
+ * Copyright 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 com.android.media;
+
+import android.Manifest.permission;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.media.AudioAttributes;
+import android.media.IMediaSession2Callback;
+import android.media.MediaItem2;
+import android.media.MediaPlayerBase;
+import android.media.MediaSession2;
+import android.media.MediaSession2.Builder;
+import android.media.MediaSession2.Command;
+import android.media.MediaSession2.CommandButton;
+import android.media.MediaSession2.CommandGroup;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.PlaylistParam;
+import android.media.MediaSession2.SessionCallback;
+import android.media.PlaybackState2;
+import android.media.SessionToken2;
+import android.media.VolumeProvider;
+import android.media.session.MediaSessionManager;
+import android.media.session.PlaybackState;
+import android.media.update.MediaSession2Provider;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.ResultReceiver;
+import android.util.Log;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+public class MediaSession2Impl implements MediaSession2Provider {
+ private static final String TAG = "MediaSession2";
+ private static final boolean DEBUG = true;//Log.isLoggable(TAG, Log.DEBUG);
+
+ private final MediaSession2 mInstance;
+
+ private final Context mContext;
+ private final String mId;
+ private final Handler mHandler;
+ private final Executor mCallbackExecutor;
+ private final MediaSession2Stub mSessionStub;
+ private final SessionToken2 mSessionToken;
+
+ private MediaPlayerBase mPlayer;
+
+ private final List<PlaybackListenerHolder> mListeners = new ArrayList<>();
+ private MyPlaybackListener mListener;
+ private MediaSession2 instance;
+
+ /**
+ * Can be only called by the {@link Builder#build()}.
+ *
+ * @param instance
+ * @param context
+ * @param player
+ * @param id
+ * @param callback
+ * @param volumeProvider
+ * @param ratingType
+ * @param sessionActivity
+ */
+ public MediaSession2Impl(MediaSession2 instance, Context context, MediaPlayerBase player,
+ String id, Executor callbackExecutor, SessionCallback callback,
+ VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity) {
+ mInstance = instance;
+ // TODO(jaewan): Keep other params.
+
+ // Argument checks are done by builder already.
+ // Initialize finals first.
+ mContext = context;
+ mId = id;
+ mHandler = new Handler(Looper.myLooper());
+ mCallbackExecutor = callbackExecutor;
+ mSessionStub = new MediaSession2Stub(this, callback);
+ // Ask server to create session token for following reasons.
+ // 1. Make session ID unique per package.
+ // Server can only know if the package has another process and has another session
+ // with the same id. Let server check this.
+ // Note that 'ID is unique per package' is important for controller to distinguish
+ // a session in another package.
+ // 2. Easier to know the type of session.
+ // Session created here can be the session service token. In order distinguish,
+ // we need to iterate AndroidManifest.xml but it's already done by the server.
+ // Let server to create token with the type.
+ MediaSessionManager manager =
+ (MediaSessionManager) mContext.getSystemService(Context.MEDIA_SESSION_SERVICE);
+ mSessionToken = manager.createSessionToken(mContext.getPackageName(), mId, mSessionStub);
+ if (mSessionToken == null) {
+ throw new IllegalStateException("Session with the same id is already used by"
+ + " another process. Use MediaController2 instead.");
+ }
+
+ setPlayerInternal(player);
+ }
+
+ // TODO(jaewan): Add explicit release() and do not remove session object with the
+ // setPlayer(null). Token can be available when player is null, and
+ // controller can also attach to session.
+ @Override
+ public void setPlayer_impl(MediaPlayerBase player, VolumeProvider volumeProvider) throws IllegalArgumentException {
+ ensureCallingThread();
+ if (player == null) {
+ throw new IllegalArgumentException("player shouldn't be null");
+ }
+ setPlayerInternal(player);
+ }
+
+ private void setPlayerInternal(MediaPlayerBase player) {
+ if (mPlayer == player) {
+ // Player didn't changed. No-op.
+ return;
+ }
+ // TODO(jaewan): Find equivalent for the executor
+ //mHandler.removeCallbacksAndMessages(null);
+ if (mPlayer != null && mListener != null) {
+ // This might not work for a poorly implemented player.
+ mPlayer.removePlaybackListener(mListener);
+ }
+ mListener = new MyPlaybackListener(this, player);
+ player.addPlaybackListener(mCallbackExecutor, mListener);
+ notifyPlaybackStateChanged(player.getPlaybackState());
+ mPlayer = player;
+ }
+
+ @Override
+ public void close_impl() {
+ // Flush any pending messages.
+ mHandler.removeCallbacksAndMessages(null);
+ if (mSessionStub != null) {
+ if (DEBUG) {
+ Log.d(TAG, "session is now unavailable, id=" + mId);
+ }
+ // Invalidate previously published session stub.
+ mSessionStub.destroyNotLocked();
+ }
+ }
+
+ @Override
+ public MediaPlayerBase getPlayer_impl() {
+ return getPlayer();
+ }
+
+ // TODO(jaewan): Change this to @NonNull
+ @Override
+ public SessionToken2 getToken_impl() {
+ return mSessionToken;
+ }
+
+ @Override
+ public List<ControllerInfo> getConnectedControllers_impl() {
+ return mSessionStub.getControllers();
+ }
+
+ @Override
+ public void setAudioAttributes_impl(AudioAttributes attributes) {
+ // implement
+ }
+
+ @Override
+ public void setAudioFocusRequest_impl(int focusGain) {
+ // implement
+ }
+
+ @Override
+ public void play_impl() {
+ ensureCallingThread();
+ ensurePlayer();
+ mPlayer.play();
+ }
+
+ @Override
+ public void pause_impl() {
+ ensureCallingThread();
+ ensurePlayer();
+ mPlayer.pause();
+ }
+
+ @Override
+ public void stop_impl() {
+ ensureCallingThread();
+ ensurePlayer();
+ mPlayer.stop();
+ }
+
+ @Override
+ public void skipToPrevious_impl() {
+ ensureCallingThread();
+ ensurePlayer();
+ mPlayer.skipToPrevious();
+ }
+
+ @Override
+ public void skipToNext_impl() {
+ ensureCallingThread();
+ ensurePlayer();
+ mPlayer.skipToNext();
+ }
+
+ @Override
+ public void setCustomLayout_impl(ControllerInfo controller, List<CommandButton> layout) {
+ ensureCallingThread();
+ if (controller == null) {
+ throw new IllegalArgumentException("controller shouldn't be null");
+ }
+ if (layout == null) {
+ throw new IllegalArgumentException("layout shouldn't be null");
+ }
+ mSessionStub.notifyCustomLayoutNotLocked(controller, layout);
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////
+ // TODO(jaewan): Implement follows
+ //////////////////////////////////////////////////////////////////////////////////////
+ @Override
+ public void setPlayer_impl(MediaPlayerBase player) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void setAllowedCommands_impl(ControllerInfo controller, CommandGroup commands) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void notifyMetadataChanged_impl() {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void sendCustomCommand_impl(ControllerInfo controller, Command command, Bundle args,
+ ResultReceiver receiver) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void sendCustomCommand_impl(Command command, Bundle args) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void setPlaylist_impl(List<MediaItem2> playlist, PlaylistParam param) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void prepare_impl() {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void fastForward_impl() {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void rewind_impl() {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void seekTo_impl(long pos) {
+ // TODO(jaewan): Implement
+ }
+
+ @Override
+ public void setCurrentPlaylistItem_impl(int index) {
+ // TODO(jaewan): Implement
+ }
+
+ ///////////////////////////////////////////////////
+ // Protected or private methods
+ ///////////////////////////////////////////////////
+
+ // Enforces developers to call all the methods on the initially given thread
+ // because calls from the MediaController2 will be run on the thread.
+ // TODO(jaewan): Should we allow calls from the multiple thread?
+ // I prefer this way because allowing multiple thread may case tricky issue like
+ // b/63446360. If the {@link #setPlayer()} with {@code null} can be called from
+ // another thread, transport controls can be called after that.
+ // That's basically the developer's mistake, but they cannot understand what's
+ // happening behind until we tell them so.
+ // If enforcing callling thread doesn't look good, we can alternatively pick
+ // 1. Allow calls from random threads for all methods.
+ // 2. Allow calls from random threads for all methods, except for the
+ // {@link #setPlayer()}.
+ private void ensureCallingThread() {
+ // TODO(jaewan): Uncomment or remove
+ /*
+ if (mHandler.getLooper() != Looper.myLooper()) {
+ throw new IllegalStateException("Run this on the given thread");
+ }*/
+ }
+
+
+ private void ensurePlayer() {
+ // TODO(jaewan): Should we pend command instead? Follow the decision from MP2.
+ // Alternatively we can add a API like setAcceptsPendingCommands(boolean).
+ if (mPlayer == null) {
+ throw new IllegalStateException("Player isn't set");
+ }
+ }
+
+ Handler getHandler() {
+ return mHandler;
+ }
+
+ private void notifyPlaybackStateChanged(PlaybackState2 state) {
+ // Notify to listeners added directly to this session
+ for (int i = 0; i < mListeners.size(); i++) {
+ mListeners.get(i).postPlaybackChange(state);
+ }
+ // Notify to controllers as well.
+ mSessionStub.notifyPlaybackStateChangedNotLocked(state);
+ }
+
+ Context getContext() {
+ return mContext;
+ }
+
+ MediaSession2 getInstance() {
+ return mInstance;
+ }
+
+ MediaPlayerBase getPlayer() {
+ return mPlayer;
+ }
+
+ private static class MyPlaybackListener implements MediaPlayerBase.PlaybackListener {
+ private final WeakReference<MediaSession2Impl> mSession;
+ private final MediaPlayerBase mPlayer;
+
+ private MyPlaybackListener(MediaSession2Impl session, MediaPlayerBase player) {
+ mSession = new WeakReference<>(session);
+ mPlayer = player;
+ }
+
+ @Override
+ public void onPlaybackChanged(PlaybackState2 state) {
+ MediaSession2Impl session = mSession.get();
+ if (mPlayer != session.mInstance.getPlayer()) {
+ Log.w(TAG, "Unexpected playback state change notifications. Ignoring.",
+ new IllegalStateException());
+ return;
+ }
+ session.notifyPlaybackStateChanged(state);
+ }
+ }
+
+ public static class ControllerInfoImpl implements ControllerInfoProvider {
+ private final ControllerInfo mInstance;
+ private final int mUid;
+ private final String mPackageName;
+ private final boolean mIsTrusted;
+ private final IMediaSession2Callback mControllerBinder;
+
+ // Flag to indicate which callbacks should be returned for the controller binder.
+ // Either 0 or combination of {@link #CALLBACK_FLAG_PLAYBACK},
+ // {@link #CALLBACK_FLAG_SESSION_ACTIVENESS}
+ private int mFlag;
+
+ public ControllerInfoImpl(ControllerInfo instance, Context context, int uid,
+ int pid, String packageName, IMediaSession2Callback callback) {
+ mInstance = instance;
+ mUid = uid;
+ mPackageName = packageName;
+
+ // TODO(jaewan): Remove this workaround
+ if ("com.android.server.media".equals(packageName)) {
+ mIsTrusted = true;
+ } else if (context.checkPermission(permission.MEDIA_CONTENT_CONTROL, pid, uid) ==
+ PackageManager.PERMISSION_GRANTED) {
+ mIsTrusted = true;
+ } else {
+ // TODO(jaewan): Also consider enabled notification listener.
+ mIsTrusted = false;
+ // System apps may bind across the user so uid can be differ.
+ // Skip sanity check for the system app.
+ try {
+ int uidForPackage = context.getPackageManager().getPackageUid(packageName, 0);
+ if (uid != uidForPackage) {
+ throw new IllegalArgumentException("Illegal call from uid=" + uid +
+ ", pkg=" + packageName + ". Expected uid" + uidForPackage);
+ }
+ } catch (NameNotFoundException e) {
+ // Rethrow exception with different name because binder methods only accept
+ // RemoteException.
+ throw new IllegalArgumentException(e);
+ }
+ }
+ mControllerBinder = callback;
+ }
+
+ @Override
+ public String getPackageName_impl() {
+ return mPackageName;
+ }
+
+ @Override
+ public int getUid_impl() {
+ return mUid;
+ }
+
+ @Override
+ public boolean isTrusted_impl() {
+ return mIsTrusted;
+ }
+
+ @Override
+ public int hashCode_impl() {
+ return mControllerBinder.hashCode();
+ }
+
+ @Override
+ public boolean equals_impl(ControllerInfoProvider obj) {
+ return equals(obj);
+ }
+
+ @Override
+ public int hashCode() {
+ return mControllerBinder.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof ControllerInfoImpl)) {
+ return false;
+ }
+ ControllerInfoImpl other = (ControllerInfoImpl) obj;
+ return mControllerBinder.asBinder().equals(other.mControllerBinder.asBinder());
+ }
+
+ public ControllerInfo getInstance() {
+ return mInstance;
+ }
+
+ public IBinder getId() {
+ return mControllerBinder.asBinder();
+ }
+
+ public IMediaSession2Callback getControllerBinder() {
+ return mControllerBinder;
+ }
+
+ public boolean containsFlag(int flag) {
+ return (mFlag & flag) != 0;
+ }
+
+ public void addFlag(int flag) {
+ mFlag |= flag;
+ }
+
+ public void removeFlag(int flag) {
+ mFlag &= ~flag;
+ }
+
+ public static ControllerInfoImpl from(ControllerInfo controller) {
+ return (ControllerInfoImpl) controller.getProvider();
+ }
+ }
+}
diff --git a/com/android/media/MediaSession2Stub.java b/com/android/media/MediaSession2Stub.java
new file mode 100644
index 00000000..2f75dfaa
--- /dev/null
+++ b/com/android/media/MediaSession2Stub.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright 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 com.android.media;
+
+import static com.android.media.MediaController2Impl.CALLBACK_FLAG_PLAYBACK;
+
+import android.content.Context;
+import android.media.IMediaSession2;
+import android.media.IMediaSession2Callback;
+import android.media.MediaLibraryService2.BrowserRoot;
+import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
+import android.media.MediaSession2;
+import android.media.MediaSession2.Command;
+import android.media.MediaSession2.CommandButton;
+import android.media.MediaSession2.CommandGroup;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.PlaybackState2;
+import android.media.session.PlaybackState;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.support.annotation.GuardedBy;
+import android.util.ArrayMap;
+import android.util.Log;
+import com.android.media.MediaSession2Impl.ControllerInfoImpl;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MediaSession2Stub extends IMediaSession2.Stub {
+ private static final String TAG = "MediaSession2Stub";
+ private static final boolean DEBUG = true; // TODO(jaewan): Rename.
+
+ private final Object mLock = new Object();
+ private final CommandHandler mCommandHandler;
+ private final WeakReference<MediaSession2Impl> mSession;
+ private final Context mContext;
+ private final SessionCallback mSessionCallback;
+ private final MediaLibrarySessionCallback mLibraryCallback;
+
+ @GuardedBy("mLock")
+ private final ArrayMap<IBinder, ControllerInfo> mControllers = new ArrayMap<>();
+
+ public MediaSession2Stub(MediaSession2Impl session, SessionCallback callback) {
+ mSession = new WeakReference<>(session);
+ mContext = session.getContext();
+ // TODO(jaewan): Should be executor from the session builder
+ mCommandHandler = new CommandHandler(session.getHandler().getLooper());
+ mSessionCallback = callback;
+ mLibraryCallback = (callback instanceof MediaLibrarySessionCallback)
+ ? (MediaLibrarySessionCallback) callback : null;
+ }
+
+ public void destroyNotLocked() {
+ final List<ControllerInfo> list;
+ synchronized (mLock) {
+ mSession.clear();
+ mCommandHandler.removeCallbacksAndMessages(null);
+ list = getControllers();
+ mControllers.clear();
+ }
+ for (int i = 0; i < list.size(); i++) {
+ IMediaSession2Callback callbackBinder =
+ ((ControllerInfoImpl) list.get(i).getProvider()).getControllerBinder();
+ try {
+ // Should be used without a lock hold to prevent potential deadlock.
+ callbackBinder.onConnectionChanged(null, null);
+ } catch (RemoteException e) {
+ // Controller is gone. Should be fine because we're destroying.
+ }
+ }
+ }
+
+ private MediaSession2Impl getSession() throws IllegalStateException {
+ final MediaSession2Impl session = mSession.get();
+ if (session == null) {
+ throw new IllegalStateException("Session is died");
+ }
+ return session;
+ }
+
+ @Override
+ public void connect(String callingPackage, IMediaSession2Callback callback) {
+ if (callback == null) {
+ // Requesting connect without callback to receive result.
+ return;
+ }
+ ControllerInfo request = new ControllerInfo(mContext,
+ Binder.getCallingUid(), Binder.getCallingPid(), callingPackage, callback);
+ mCommandHandler.postConnect(request);
+ }
+
+ @Override
+ public void release(IMediaSession2Callback caller) throws RemoteException {
+ synchronized (mLock) {
+ ControllerInfo controllerInfo = mControllers.remove(caller.asBinder());
+ if (DEBUG) {
+ Log.d(TAG, "releasing " + controllerInfo);
+ }
+ }
+ }
+
+ @Override
+ public void sendCommand(IMediaSession2Callback caller, Bundle command, Bundle args)
+ throws RuntimeException {
+ ControllerInfo controller = getController(caller);
+ if (controller == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Command from a controller that hasn't connected. Ignore");
+ }
+ return;
+ }
+ mCommandHandler.postCommand(controller, Command.fromBundle(command), args);
+ }
+
+ @Override
+ public void getBrowserRoot(IMediaSession2Callback caller, Bundle rootHints)
+ throws RuntimeException {
+ if (mLibraryCallback == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Session cannot hand getBrowserRoot()");
+ }
+ return;
+ }
+ final ControllerInfo controller = getController(caller);
+ if (controller == null) {
+ if (DEBUG) {
+ Log.d(TAG, "getBrowerRoot from a controller that hasn't connected. Ignore");
+ }
+ return;
+ }
+ mCommandHandler.postOnGetRoot(controller, rootHints);
+ }
+
+ @Deprecated
+ @Override
+ public Bundle getPlaybackState() throws RemoteException {
+ MediaSession2Impl session = getSession();
+ // TODO(jaewan): Check if mPlayer.getPlaybackState() is safe here.
+ return session.getInstance().getPlayer().getPlaybackState().toBundle();
+ }
+
+ @Deprecated
+ @Override
+ public void registerCallback(final IMediaSession2Callback callbackBinder,
+ final int callbackFlag, final int requestCode) throws RemoteException {
+ // TODO(jaewan): Call onCommand() here. To do so, you should pend message.
+ synchronized (mLock) {
+ ControllerInfo controllerInfo = getController(callbackBinder);
+ if (controllerInfo == null) {
+ return;
+ }
+ ControllerInfoImpl.from(controllerInfo).addFlag(callbackFlag);
+ }
+ }
+
+ @Deprecated
+ @Override
+ public void unregisterCallback(IMediaSession2Callback callbackBinder, int callbackFlag)
+ throws RemoteException {
+ // TODO(jaewan): Call onCommand() here. To do so, you should pend message.
+ synchronized (mLock) {
+ ControllerInfo controllerInfo = getController(callbackBinder);
+ if (controllerInfo == null) {
+ return;
+ }
+ ControllerInfoImpl.from(controllerInfo).removeFlag(callbackFlag);
+ }
+ }
+
+ private ControllerInfo getController(IMediaSession2Callback caller) {
+ synchronized (mLock) {
+ return mControllers.get(caller.asBinder());
+ }
+ }
+
+ public List<ControllerInfo> getControllers() {
+ ArrayList<ControllerInfo> controllers = new ArrayList<>();
+ synchronized (mLock) {
+ for (int i = 0; i < mControllers.size(); i++) {
+ controllers.add(mControllers.valueAt(i));
+ }
+ }
+ return controllers;
+ }
+
+ public List<ControllerInfo> getControllersWithFlag(int flag) {
+ ArrayList<ControllerInfo> controllers = new ArrayList<>();
+ synchronized (mLock) {
+ for (int i = 0; i < mControllers.size(); i++) {
+ ControllerInfo controllerInfo = mControllers.valueAt(i);
+ if (ControllerInfoImpl.from(controllerInfo).containsFlag(flag)) {
+ controllers.add(controllerInfo);
+ }
+ }
+ }
+ return controllers;
+ }
+
+ // Should be used without a lock to prevent potential deadlock.
+ public void notifyPlaybackStateChangedNotLocked(PlaybackState2 state) {
+ final List<ControllerInfo> list = getControllersWithFlag(CALLBACK_FLAG_PLAYBACK);
+ for (int i = 0; i < list.size(); i++) {
+ IMediaSession2Callback callbackBinder =
+ ControllerInfoImpl.from(list.get(i)).getControllerBinder();
+ try {
+ callbackBinder.onPlaybackStateChanged(state.toBundle());
+ } catch (RemoteException e) {
+ Log.w(TAG, "Controller is gone", e);
+ // TODO(jaewan): What to do when the controller is gone?
+ }
+ }
+ }
+
+ public void notifyCustomLayoutNotLocked(ControllerInfo controller, List<CommandButton> layout) {
+ // TODO(jaewan): It's OK to be called while it's connecting, but not OK if the connection
+ // is rejected. Handle the case.
+ IMediaSession2Callback callbackBinder =
+ ControllerInfoImpl.from(controller).getControllerBinder();
+ try {
+ List<Bundle> layoutBundles = new ArrayList<>();
+ for (int i = 0; i < layout.size(); i++) {
+ Bundle bundle = layout.get(i).toBundle();
+ if (bundle != null) {
+ layoutBundles.add(bundle);
+ }
+ }
+ callbackBinder.onCustomLayoutChanged(layoutBundles);
+ } catch (RemoteException e) {
+ Log.w(TAG, "Controller is gone", e);
+ // TODO(jaewan): What to do when the controller is gone?
+ }
+ }
+
+ // TODO(jaewan): Remove this. We should use Executor given by the session builder.
+ private class CommandHandler extends Handler {
+ public static final int MSG_CONNECT = 1000;
+ public static final int MSG_COMMAND = 1001;
+ public static final int MSG_ON_GET_ROOT = 2000;
+
+ public CommandHandler(Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final MediaSession2Impl session = MediaSession2Stub.this.mSession.get();
+ if (session == null || session.getPlayer() == null) {
+ return;
+ }
+
+ switch (msg.what) {
+ case MSG_CONNECT: {
+ ControllerInfo request = (ControllerInfo) msg.obj;
+ CommandGroup allowedCommands = mSessionCallback.onConnect(request);
+ // Don't reject connection for the request from trusted app.
+ // Otherwise server will fail to retrieve session's information to dispatch
+ // media keys to.
+ boolean accept = allowedCommands != null || request.isTrusted();
+ ControllerInfoImpl impl = ControllerInfoImpl.from(request);
+ if (accept) {
+ synchronized (mLock) {
+ mControllers.put(impl.getId(), request);
+ }
+ if (allowedCommands == null) {
+ // For trusted apps, send non-null allowed commands to keep connection.
+ allowedCommands = new CommandGroup();
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "onConnectResult, request=" + request
+ + " accept=" + accept);
+ }
+ try {
+ impl.getControllerBinder().onConnectionChanged(
+ accept ? MediaSession2Stub.this : null,
+ allowedCommands == null ? null : allowedCommands.toBundle());
+ } catch (RemoteException e) {
+ // Controller may be died prematurely.
+ }
+ break;
+ }
+ case MSG_COMMAND: {
+ CommandParam param = (CommandParam) msg.obj;
+ Command command = param.command;
+ boolean accepted = mSessionCallback.onCommandRequest(
+ param.controller, command);
+ if (!accepted) {
+ // Don't run rejected command.
+ if (DEBUG) {
+ Log.d(TAG, "Command " + command + " from "
+ + param.controller + " was rejected by " + session);
+ }
+ return;
+ }
+
+ switch (param.command.getCommandCode()) {
+ case MediaSession2.COMMAND_CODE_PLAYBACK_START:
+ session.getInstance().play();
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_PAUSE:
+ session.getInstance().pause();
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_STOP:
+ session.getInstance().stop();
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_PREV_ITEM:
+ session.getInstance().skipToPrevious();
+ break;
+ case MediaSession2.COMMAND_CODE_PLAYBACK_SKIP_NEXT_ITEM:
+ session.getInstance().skipToNext();
+ break;
+ default:
+ // TODO(jaewan): Handle custom command.
+ }
+ break;
+ }
+ case MSG_ON_GET_ROOT: {
+ final CommandParam param = (CommandParam) msg.obj;
+ final ControllerInfoImpl controller = ControllerInfoImpl.from(param.controller);
+ BrowserRoot root = mLibraryCallback.onGetRoot(param.controller, param.args);
+ try {
+ controller.getControllerBinder().onGetRootResult(param.args,
+ root == null ? null : root.getRootId(),
+ root == null ? null : root.getExtras());
+ } catch (RemoteException e) {
+ // Controller may be died prematurely.
+ // TODO(jaewan): Handle this.
+ }
+ break;
+ }
+ }
+ }
+
+ public void postConnect(ControllerInfo request) {
+ obtainMessage(MSG_CONNECT, request).sendToTarget();
+ }
+
+ public void postCommand(ControllerInfo controller, Command command, Bundle args) {
+ CommandParam param = new CommandParam(controller, command, args);
+ obtainMessage(MSG_COMMAND, param).sendToTarget();
+ }
+
+ public void postOnGetRoot(ControllerInfo controller, Bundle rootHints) {
+ CommandParam param = new CommandParam(controller, null, rootHints);
+ obtainMessage(MSG_ON_GET_ROOT, param).sendToTarget();
+ }
+ }
+
+ private static class CommandParam {
+ public final ControllerInfo controller;
+ public final Command command;
+ public final Bundle args;
+
+ private CommandParam(ControllerInfo controller, Command command, Bundle args) {
+ this.controller = controller;
+ this.command = command;
+ this.args = args;
+ }
+ }
+}
diff --git a/com/android/media/MediaSessionService2Impl.java b/com/android/media/MediaSessionService2Impl.java
new file mode 100644
index 00000000..9d240822
--- /dev/null
+++ b/com/android/media/MediaSessionService2Impl.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright 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 com.android.media;
+
+import static android.content.Context.NOTIFICATION_SERVICE;
+
+import android.app.NotificationManager;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.MediaSession2;
+import android.media.MediaSessionService2;
+import android.media.MediaSessionService2.MediaNotification;
+import android.media.PlaybackState2;
+import android.media.session.PlaybackState;
+import android.media.update.MediaSessionService2Provider;
+import android.os.IBinder;
+import android.os.Looper;
+import android.support.annotation.GuardedBy;
+import android.util.Log;
+
+// TODO(jaewan): Need a test for session service itself.
+public class MediaSessionService2Impl implements MediaSessionService2Provider {
+
+ private static final String TAG = "MPSessionService"; // to meet 23 char limit in Log tag
+ private static final boolean DEBUG = true; // TODO(jaewan): Change this.
+
+ private final MediaSessionService2 mInstance;
+ private final PlaybackListener mListener = new SessionServicePlaybackListener();
+
+ private final Object mLock = new Object();
+ @GuardedBy("mLock")
+ private NotificationManager mNotificationManager;
+ @GuardedBy("mLock")
+ private Intent mStartSelfIntent;
+
+ private boolean mIsRunningForeground;
+ private MediaSession2 mSession;
+
+ public MediaSessionService2Impl(MediaSessionService2 instance) {
+ if (DEBUG) {
+ Log.d(TAG, "MediaSessionService2Impl(" + instance + ")");
+ }
+ mInstance = instance;
+ }
+
+ @Override
+ public MediaSession2 getSession_impl() {
+ return getSession();
+ }
+
+ MediaSession2 getSession() {
+ synchronized (mLock) {
+ return mSession;
+ }
+ }
+
+ @Override
+ public MediaNotification onUpdateNotification_impl(PlaybackState2 state) {
+ // Provide default notification UI later.
+ return null;
+ }
+
+ @Override
+ public void onCreate_impl() {
+ mNotificationManager = (NotificationManager) mInstance.getSystemService(
+ NOTIFICATION_SERVICE);
+ mStartSelfIntent = new Intent(mInstance, mInstance.getClass());
+
+ Intent serviceIntent = createServiceIntent();
+ ResolveInfo resolveInfo = mInstance.getPackageManager()
+ .resolveService(serviceIntent, PackageManager.GET_META_DATA);
+ String id;
+ if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+ throw new IllegalArgumentException("service " + mInstance + " doesn't implement"
+ + serviceIntent.getAction());
+ } else if (resolveInfo.serviceInfo.metaData == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Failed to resolve ID for " + mInstance + ". Using empty id");
+ }
+ id = "";
+ } else {
+ id = resolveInfo.serviceInfo.metaData.getString(
+ MediaSessionService2.SERVICE_META_DATA, "");
+ }
+ mSession = mInstance.onCreateSession(id);
+ if (mSession == null || !id.equals(mSession.getToken().getId())) {
+ throw new RuntimeException("Expected session with id " + id + ", but got " + mSession);
+ }
+ // TODO(jaewan): Uncomment here.
+ // mSession.addPlaybackListener(mListener, mSession.getExecutor());
+ }
+
+ Intent createServiceIntent() {
+ Intent serviceIntent = new Intent(mInstance, mInstance.getClass());
+ serviceIntent.setAction(MediaSessionService2.SERVICE_INTERFACE);
+ return serviceIntent;
+ }
+
+ public IBinder onBind_impl(Intent intent) {
+ if (MediaSessionService2.SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mSession.getToken().getSessionBinder().asBinder();
+ }
+ return null;
+ }
+
+ private void updateNotification(PlaybackState2 state) {
+ MediaNotification mediaNotification = mInstance.onUpdateNotification(state);
+ if (mediaNotification == null) {
+ return;
+ }
+ switch((int) state.getState()) {
+ case PlaybackState.STATE_PLAYING:
+ if (!mIsRunningForeground) {
+ mIsRunningForeground = true;
+ mInstance.startForegroundService(mStartSelfIntent);
+ mInstance.startForeground(mediaNotification.id, mediaNotification.notification);
+ return;
+ }
+ break;
+ case PlaybackState.STATE_STOPPED:
+ if (mIsRunningForeground) {
+ mIsRunningForeground = false;
+ mInstance.stopForeground(true);
+ return;
+ }
+ break;
+ }
+ mNotificationManager.notify(mediaNotification.id, mediaNotification.notification);
+ }
+
+ private class SessionServicePlaybackListener implements PlaybackListener {
+ @Override
+ public void onPlaybackChanged(PlaybackState2 state) {
+ if (state == null) {
+ Log.w(TAG, "Ignoring null playback state");
+ return;
+ }
+ MediaSession2Impl impl = (MediaSession2Impl) mSession.getProvider();
+ if (impl.getHandler().getLooper() != Looper.myLooper()) {
+ Log.w(TAG, "Ignoring " + state + ". Expected " + impl.getHandler().getLooper()
+ + " but " + Looper.myLooper());
+ return;
+ }
+ updateNotification(state);
+ }
+ }
+}
diff --git a/com/android/media/PlaybackListenerHolder.java b/com/android/media/PlaybackListenerHolder.java
new file mode 100644
index 00000000..7b336c4c
--- /dev/null
+++ b/com/android/media/PlaybackListenerHolder.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 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 com.android.media;
+
+import android.media.MediaPlayerBase.PlaybackListener;
+import android.media.PlaybackState2;
+import android.media.session.PlaybackState;
+import android.os.Handler;
+import android.os.Message;
+import android.support.annotation.NonNull;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Holds {@link PlaybackListener} with the {@link Handler}.
+ */
+public class PlaybackListenerHolder {
+ public final Executor executor;
+ public final PlaybackListener listener;
+
+ public PlaybackListenerHolder(Executor executor, @NonNull PlaybackListener listener) {
+ this.executor = executor;
+ this.listener = listener;
+ }
+
+ public void postPlaybackChange(final PlaybackState2 state) {
+ executor.execute(() -> listener.onPlaybackChanged(state));
+ }
+
+ /**
+ * Returns {@code true} if the given list contains a {@link PlaybackListenerHolder} that holds
+ * the given listener.
+ *
+ * @param list list to check
+ * @param listener listener to check
+ * @return {@code true} if the given list contains listener. {@code false} otherwise.
+ */
+ public static <Holder extends PlaybackListenerHolder> boolean contains(
+ @NonNull List<Holder> list, PlaybackListener listener) {
+ return indexOf(list, listener) >= 0;
+ }
+
+ /**
+ * Returns the index of the {@link PlaybackListenerHolder} that contains the given listener.
+ *
+ * @param list list to check
+ * @param listener listener to check
+ * @return {@code index} of item if the given list contains listener. {@code -1} otherwise.
+ */
+ public static <Holder extends PlaybackListenerHolder> int indexOf(
+ @NonNull List<Holder> list, PlaybackListener listener) {
+ for (int i = 0; i < list.size(); i++) {
+ if (list.get(i).listener == listener) {
+ return i;
+ }
+ }
+ return -1;
+ }
+}
diff --git a/com/android/media/update/ApiFactory.java b/com/android/media/update/ApiFactory.java
new file mode 100644
index 00000000..69f41303
--- /dev/null
+++ b/com/android/media/update/ApiFactory.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.media.update;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.media.MediaBrowser2;
+import android.media.MediaBrowser2.BrowserCallback;
+import android.media.MediaController2;
+import android.media.MediaLibraryService2;
+import android.media.MediaLibraryService2.MediaLibrarySession;
+import android.media.MediaLibraryService2.MediaLibrarySessionCallback;
+import android.media.MediaPlayerBase;
+import android.media.MediaSession2;
+import android.media.MediaSession2.ControllerInfo;
+import android.media.MediaSession2.SessionCallback;
+import android.media.MediaSessionService2;
+import android.media.IMediaSession2Callback;
+import android.media.SessionToken2;
+import android.media.VolumeProvider;
+import android.media.update.MediaBrowser2Provider;
+import android.media.update.MediaControlView2Provider;
+import android.media.update.MediaController2Provider;
+import android.media.update.MediaLibraryService2Provider.MediaLibrarySessionProvider;
+import android.media.update.MediaSession2Provider;
+import android.media.update.MediaSessionService2Provider;
+import android.media.update.VideoView2Provider;
+import android.media.update.StaticProvider;
+import android.media.update.ViewProvider;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.widget.MediaControlView2;
+import android.widget.VideoView2;
+
+import com.android.media.MediaBrowser2Impl;
+import com.android.media.MediaController2Impl;
+import com.android.media.MediaLibraryService2Impl;
+import com.android.media.MediaLibraryService2Impl.MediaLibrarySessionImpl;
+import com.android.media.MediaSession2Impl;
+import com.android.media.MediaSessionService2Impl;
+import com.android.widget.MediaControlView2Impl;
+import com.android.widget.VideoView2Impl;
+
+import java.util.concurrent.Executor;
+
+public class ApiFactory implements StaticProvider {
+ public static Object initialize(Resources libResources, Theme libTheme)
+ throws ReflectiveOperationException {
+ ApiHelper.initialize(libResources, libTheme);
+ return new ApiFactory();
+ }
+
+ @Override
+ public MediaController2Provider createMediaController2(
+ MediaController2 instance, Context context, SessionToken2 token,
+ MediaController2.ControllerCallback callback, Executor executor) {
+ return new MediaController2Impl(instance, context, token, callback, executor);
+ }
+
+ @Override
+ public MediaBrowser2Provider createMediaBrowser2(MediaBrowser2 instance, Context context,
+ SessionToken2 token, BrowserCallback callback, Executor executor) {
+ return new MediaBrowser2Impl(instance, context, token, callback, executor);
+ }
+
+ @Override
+ public MediaSession2Provider createMediaSession2(MediaSession2 instance, Context context,
+ MediaPlayerBase player, String id, Executor callbackExecutor, SessionCallback callback,
+ VolumeProvider volumeProvider, int ratingType,
+ PendingIntent sessionActivity) {
+ return new MediaSession2Impl(instance, context, player, id, callbackExecutor, callback,
+ volumeProvider, ratingType, sessionActivity);
+ }
+
+ @Override
+ public MediaSession2Provider.ControllerInfoProvider createMediaSession2ControllerInfoProvider(
+ ControllerInfo instance, Context context, int uid, int pid, String packageName,
+ IMediaSession2Callback callback) {
+ return new MediaSession2Impl.ControllerInfoImpl(
+ instance, context, uid, pid, packageName, callback);
+ }
+
+ @Override
+ public MediaSessionService2Provider createMediaSessionService2(
+ MediaSessionService2 instance) {
+ return new MediaSessionService2Impl(instance);
+ }
+
+ @Override
+ public MediaSessionService2Provider createMediaLibraryService2(
+ MediaLibraryService2 instance) {
+ return new MediaLibraryService2Impl(instance);
+ }
+
+ @Override
+ public MediaLibrarySessionProvider createMediaLibraryService2MediaLibrarySession(
+ MediaLibrarySession instance, Context context, MediaPlayerBase player, String id,
+ Executor callbackExecutor, MediaLibrarySessionCallback callback,
+ VolumeProvider volumeProvider, int ratingType, PendingIntent sessionActivity) {
+ return new MediaLibrarySessionImpl(instance, context, player, id, callbackExecutor,
+ callback, volumeProvider, ratingType, sessionActivity);
+ }
+
+ @Override
+ public MediaControlView2Provider createMediaControlView2(
+ MediaControlView2 instance, ViewProvider superProvider) {
+ return new MediaControlView2Impl(instance, superProvider);
+ }
+
+ @Override
+ public VideoView2Provider createVideoView2(
+ VideoView2 instance, ViewProvider superProvider,
+ @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ return new VideoView2Impl(instance, superProvider, attrs, defStyleAttr, defStyleRes);
+ }
+}
diff --git a/com/android/media/update/ApiHelper.java b/com/android/media/update/ApiHelper.java
new file mode 100644
index 00000000..b0ca1bdd
--- /dev/null
+++ b/com/android/media/update/ApiHelper.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright 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 com.android.media.update;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.XmlResourceParser;
+import android.util.AttributeSet;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.android.support.mediarouter.app.MediaRouteButton;
+
+public class ApiHelper {
+ private static ApiHelper sInstance;
+ private final Resources mLibResources;
+ private final Theme mLibTheme;
+
+ public static ApiHelper getInstance() {
+ return sInstance;
+ }
+
+ static void initialize(Resources libResources, Theme libTheme) {
+ if (sInstance == null) {
+ sInstance = new ApiHelper(libResources, libTheme);
+ }
+ }
+
+ private ApiHelper(Resources libResources, Theme libTheme) {
+ mLibResources = libResources;
+ mLibTheme = libTheme;
+ }
+
+ public static Resources getLibResources() {
+ return sInstance.mLibResources;
+ }
+
+ public static Resources.Theme getLibTheme() {
+ return sInstance.mLibTheme;
+ }
+
+ public static LayoutInflater getLayoutInflater(Context context) {
+ LayoutInflater layoutInflater = LayoutInflater.from(context).cloneInContext(
+ new ContextThemeWrapper(context, getLibTheme()));
+ layoutInflater.setFactory2(new LayoutInflater.Factory2() {
+ @Override
+ public View onCreateView(
+ View parent, String name, Context context, AttributeSet attrs) {
+ if (MediaRouteButton.class.getCanonicalName().equals(name)) {
+ return new MediaRouteButton(context, attrs);
+ }
+ return null;
+ }
+
+ @Override
+ public View onCreateView(String name, Context context, AttributeSet attrs) {
+ return onCreateView(null, name, context, attrs);
+ }
+ });
+ return layoutInflater;
+ }
+
+ public static View inflateLibLayout(Context context, int libResId) {
+ try (XmlResourceParser parser = getLibResources().getLayout(libResId)) {
+ return getLayoutInflater(context).inflate(parser, null);
+ }
+ }
+}
diff --git a/com/android/printspooler/model/PrintSpoolerService.java b/com/android/printspooler/model/PrintSpoolerService.java
index e0a3f6cb..6c744180 100644
--- a/com/android/printspooler/model/PrintSpoolerService.java
+++ b/com/android/printspooler/model/PrintSpoolerService.java
@@ -59,7 +59,9 @@ import android.util.proto.ProtoOutputStream;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.os.HandlerCaller;
+import com.android.internal.print.DualDumpOutputStream;
import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
import com.android.printspooler.R;
import com.android.printspooler.util.ApprovedPrintServices;
@@ -159,44 +161,11 @@ public final class PrintSpoolerService extends Service {
return new PrintSpooler();
}
- private void dumpLocked(PrintWriter pw, String[] args) {
- String prefix = (args.length > 0) ? args[0] : "";
- String tab = " ";
-
- pw.append(prefix).append("print jobs:").println();
- final int printJobCount = mPrintJobs.size();
- for (int i = 0; i < printJobCount; i++) {
- PrintJobInfo printJob = mPrintJobs.get(i);
- pw.append(prefix).append(tab).append(printJob.toString());
- pw.println();
- }
-
- pw.append(prefix).append("print job files:").println();
- File[] files = getFilesDir().listFiles();
- if (files != null) {
- final int fileCount = files.length;
- for (int i = 0; i < fileCount; i++) {
- File file = files[i];
- if (file.isFile() && file.getName().startsWith(PRINT_JOB_FILE_PREFIX)) {
- pw.append(prefix).append(tab).append(file.getName()).println();
- }
- }
- }
-
- pw.append(prefix).append("approved print services:").println();
- Set<String> approvedPrintServices = (new ApprovedPrintServices(this)).getApprovedServices();
- if (approvedPrintServices != null) {
- for (String approvedService : approvedPrintServices) {
- pw.append(prefix).append(tab).append(approvedService).println();
- }
- }
- }
-
- private void dumpLocked(@NonNull ProtoOutputStream proto) {
+ private void dumpLocked(@NonNull DualDumpOutputStream dumpStream) {
int numPrintJobs = mPrintJobs.size();
for (int i = 0; i < numPrintJobs; i++) {
- writePrintJobInfo(this, proto, PrintSpoolerInternalStateProto.PRINT_JOBS,
- mPrintJobs.get(i));
+ writePrintJobInfo(this, dumpStream, "print_jobs",
+ PrintSpoolerInternalStateProto.PRINT_JOBS, mPrintJobs.get(i));
}
File[] files = getFilesDir().listFiles();
@@ -204,7 +173,8 @@ public final class PrintSpoolerService extends Service {
for (int i = 0; i < files.length; i++) {
File file = files[i];
if (file.isFile() && file.getName().startsWith(PRINT_JOB_FILE_PREFIX)) {
- proto.write(PrintSpoolerInternalStateProto.PRINT_JOB_FILES, file.getName());
+ dumpStream.write("print_job_files",
+ PrintSpoolerInternalStateProto.PRINT_JOB_FILES, file.getName());
}
}
}
@@ -214,13 +184,13 @@ public final class PrintSpoolerService extends Service {
for (String approvedService : approvedPrintServices) {
ComponentName componentName = ComponentName.unflattenFromString(approvedService);
if (componentName != null) {
- writeComponentName(proto, PrintSpoolerInternalStateProto.APPROVED_SERVICES,
- componentName);
+ writeComponentName(dumpStream, "approved_services",
+ PrintSpoolerInternalStateProto.APPROVED_SERVICES, componentName);
}
}
}
- proto.flush();
+ dumpStream.flush();
}
@Override
@@ -244,9 +214,15 @@ public final class PrintSpoolerService extends Service {
try {
synchronized (mLock) {
if (dumpAsProto) {
- dumpLocked(new ProtoOutputStream(fd));
+ dumpLocked(new DualDumpOutputStream(new ProtoOutputStream(fd), null));
} else {
- dumpLocked(pw, args);
+ try (FileOutputStream out = new FileOutputStream(fd)) {
+ try (PrintWriter w = new PrintWriter(out)) {
+ dumpLocked(new DualDumpOutputStream(null, new IndentingPrintWriter(w,
+ " ")));
+ }
+ } catch (IOException ignored) {
+ }
}
}
} finally {
diff --git a/com/android/printspooler/widget/PrintContentView.java b/com/android/printspooler/widget/PrintContentView.java
index 8b00ed05..faa10cc6 100644
--- a/com/android/printspooler/widget/PrintContentView.java
+++ b/com/android/printspooler/widget/PrintContentView.java
@@ -23,6 +23,7 @@ import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
+
import com.android.printspooler.R;
/**
@@ -410,7 +411,7 @@ public final class PrintContentView extends ViewGroup implements View.OnClickLis
mPrintButton.offsetTopAndBottom(dy);
- mDraggableContent.notifySubtreeAccessibilityStateChangedIfNeeded();
+ mDraggableContent.notifyAccessibilitySubtreeChanged();
onDragProgress(progress);
}
diff --git a/com/android/printspooler/widget/PrintOptionsLayout.java b/com/android/printspooler/widget/PrintOptionsLayout.java
index 7a80a8bd..24cf218f 100644
--- a/com/android/printspooler/widget/PrintOptionsLayout.java
+++ b/com/android/printspooler/widget/PrintOptionsLayout.java
@@ -21,6 +21,7 @@ import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
+
import com.android.printspooler.R;
/**
@@ -126,6 +127,7 @@ public final class PrintOptionsLayout extends ViewGroup {
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int childCount = getChildCount();
final int rowCount = childCount / mColumnCount + childCount % mColumnCount;
+ final boolean isLayoutRtl = isLayoutRtl();
int cellStart = getPaddingStart();
int cellTop = getPaddingTop();
@@ -134,7 +136,13 @@ public final class PrintOptionsLayout extends ViewGroup {
int rowHeight = 0;
for (int col = 0; col < mColumnCount; col++) {
- final int childIndex = row * mColumnCount + col;
+ final int childIndex;
+ if (isLayoutRtl) {
+ // if RTL, layout the right most child first
+ childIndex = row * mColumnCount + (mColumnCount - col - 1);
+ } else {
+ childIndex = row * mColumnCount + col;
+ }
if (childIndex >= childCount) {
break;
@@ -148,14 +156,14 @@ public final class PrintOptionsLayout extends ViewGroup {
MarginLayoutParams childParams = (MarginLayoutParams) child.getLayoutParams();
- final int childLeft = cellStart + childParams.getMarginStart();
+ final int childStart = cellStart + childParams.getMarginStart();
final int childTop = cellTop + childParams.topMargin;
- final int childRight = childLeft + child.getMeasuredWidth();
+ final int childEnd = childStart + child.getMeasuredWidth();
final int childBottom = childTop + child.getMeasuredHeight();
- child.layout(childLeft, childTop, childRight, childBottom);
+ child.layout(childStart, childTop, childEnd, childBottom);
- cellStart = childRight + childParams.getMarginEnd();
+ cellStart = childEnd + childParams.getMarginEnd();
rowHeight = Math.max(rowHeight, child.getMeasuredHeight()
+ childParams.topMargin + childParams.bottomMargin);
diff --git a/com/android/providers/settings/SettingsBackupAgent.java b/com/android/providers/settings/SettingsBackupAgent.java
index f1fb208b..dd89df1b 100644
--- a/com/android/providers/settings/SettingsBackupAgent.java
+++ b/com/android/providers/settings/SettingsBackupAgent.java
@@ -31,10 +31,13 @@ import android.net.NetworkPolicyManager;
import android.net.Uri;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
+import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.os.UserHandle;
import android.provider.Settings;
+import android.provider.SettingsValidators.Validator;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.BackupUtils;
import android.util.Log;
@@ -50,6 +53,7 @@ import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -147,6 +151,16 @@ public class SettingsBackupAgent extends BackupAgentHelper {
// stored in the full-backup tarfile as well, so should not be changed.
private static final String STAGE_FILE = "flattened-data";
+ // List of keys that support restore to lower version of the SDK, introduced in Android P
+ private static final ArraySet<String> RESTORE_FROM_HIGHER_SDK_INT_SUPPORTED_KEYS =
+ new ArraySet<String>(Arrays.asList(new String[] {
+ KEY_NETWORK_POLICIES,
+ KEY_WIFI_NEW_CONFIG,
+ KEY_SYSTEM,
+ KEY_SECURE,
+ KEY_GLOBAL,
+ }));
+
private SettingsHelper mSettingsHelper;
private WifiManager mWifiManager;
@@ -209,6 +223,19 @@ public class SettingsBackupAgent extends BackupAgentHelper {
public void onRestore(BackupDataInput data, int appVersionCode,
ParcelFileDescriptor newState) throws IOException {
+ if (DEBUG) {
+ Log.d(TAG, "onRestore(): appVersionCode: " + appVersionCode
+ + "; Build.VERSION.SDK_INT: " + Build.VERSION.SDK_INT);
+ }
+
+ boolean overrideRestoreAnyVersion = Settings.Global.getInt(getContentResolver(),
+ Settings.Global.OVERRIDE_SETTINGS_PROVIDER_RESTORE_ANY_VERSION, 0) == 1;
+ if ((appVersionCode > Build.VERSION.SDK_INT) && overrideRestoreAnyVersion) {
+ Log.w(TAG, "Ignoring restore from API" + appVersionCode + " to API"
+ + Build.VERSION.SDK_INT + " due to settings flag override.");
+ return;
+ }
+
// versionCode of com.android.providers.settings corresponds to SDK_INT
mRestoredFromSdkInt = appVersionCode;
@@ -221,6 +248,15 @@ public class SettingsBackupAgent extends BackupAgentHelper {
while (data.readNextHeader()) {
final String key = data.getKey();
final int size = data.getDataSize();
+
+ // bail out of restoring from higher SDK_INT version for unsupported keys
+ if (appVersionCode > Build.VERSION.SDK_INT
+ && !RESTORE_FROM_HIGHER_SDK_INT_SUPPORTED_KEYS.contains(key)) {
+ Log.w(TAG, "Not restoring unrecognized key '"
+ + key + "' from future version " + appVersionCode);
+ continue;
+ }
+
switch (key) {
case KEY_SYSTEM :
restoreSettings(data, Settings.System.CONTENT_URI, movedToGlobal);
@@ -288,65 +324,9 @@ public class SettingsBackupAgent extends BackupAgentHelper {
@Override
public void onFullBackup(FullBackupDataOutput data) throws IOException {
- byte[] systemSettingsData = getSystemSettings();
- byte[] secureSettingsData = getSecureSettings();
- byte[] globalSettingsData = getGlobalSettings();
- byte[] lockSettingsData = getLockSettings(UserHandle.myUserId());
- byte[] locale = mSettingsHelper.getLocaleData();
- byte[] softApConfigData = getSoftAPConfiguration();
- byte[] netPoliciesData = getNetworkPolicies();
- byte[] wifiFullConfigData = getNewWifiConfigData();
-
- // Write the data to the staging file, then emit that as our tarfile
- // representation of the backed-up settings.
- String root = getFilesDir().getAbsolutePath();
- File stage = new File(root, STAGE_FILE);
- try {
- FileOutputStream filestream = new FileOutputStream(stage);
- BufferedOutputStream bufstream = new BufferedOutputStream(filestream);
- DataOutputStream out = new DataOutputStream(bufstream);
-
- if (DEBUG_BACKUP) Log.d(TAG, "Writing flattened data version " + FULL_BACKUP_VERSION);
- out.writeInt(FULL_BACKUP_VERSION);
-
- if (DEBUG_BACKUP) Log.d(TAG, systemSettingsData.length + " bytes of settings data");
- out.writeInt(systemSettingsData.length);
- out.write(systemSettingsData);
- if (DEBUG_BACKUP) {
- Log.d(TAG, secureSettingsData.length + " bytes of secure settings data");
- }
- out.writeInt(secureSettingsData.length);
- out.write(secureSettingsData);
- if (DEBUG_BACKUP) {
- Log.d(TAG, globalSettingsData.length + " bytes of global settings data");
- }
- out.writeInt(globalSettingsData.length);
- out.write(globalSettingsData);
- if (DEBUG_BACKUP) Log.d(TAG, locale.length + " bytes of locale data");
- out.writeInt(locale.length);
- out.write(locale);
- if (DEBUG_BACKUP) Log.d(TAG, lockSettingsData.length + " bytes of lock settings data");
- out.writeInt(lockSettingsData.length);
- out.write(lockSettingsData);
- if (DEBUG_BACKUP) Log.d(TAG, softApConfigData.length + " bytes of softap config data");
- out.writeInt(softApConfigData.length);
- out.write(softApConfigData);
- if (DEBUG_BACKUP) Log.d(TAG, netPoliciesData.length + " bytes of net policies data");
- out.writeInt(netPoliciesData.length);
- out.write(netPoliciesData);
- if (DEBUG_BACKUP) {
- Log.d(TAG, wifiFullConfigData.length + " bytes of wifi config data");
- }
- out.writeInt(wifiFullConfigData.length);
- out.write(wifiFullConfigData);
-
- out.flush(); // also flushes downstream
-
- // now we're set to emit the tar stream
- fullBackupFile(stage, data);
- } finally {
- stage.delete();
- }
+ // Full backup of SettingsBackupAgent support was removed in Android P. If you want to adb
+ // backup com.android.providers.settings package use \"-keyvalue\" flag.
+ // Full restore of SettingsBackupAgent is still available for backwards compatibility.
}
@Override
@@ -604,15 +584,19 @@ public class SettingsBackupAgent extends BackupAgentHelper {
// Figure out the white list and redirects to the global table. We restore anything
// in either the backup whitelist or the legacy-restore whitelist for this table.
final String[] whitelist;
+ Map<String, Validator> validators = null;
if (contentUri.equals(Settings.Secure.CONTENT_URI)) {
whitelist = concat(Settings.Secure.SETTINGS_TO_BACKUP,
Settings.Secure.LEGACY_RESTORE_SETTINGS);
+ validators = Settings.Secure.VALIDATORS;
} else if (contentUri.equals(Settings.System.CONTENT_URI)) {
whitelist = concat(Settings.System.SETTINGS_TO_BACKUP,
Settings.System.LEGACY_RESTORE_SETTINGS);
+ validators = Settings.System.VALIDATORS;
} else if (contentUri.equals(Settings.Global.CONTENT_URI)) {
whitelist = concat(Settings.Global.SETTINGS_TO_BACKUP,
Settings.Global.LEGACY_RESTORE_SETTINGS);
+ validators = Settings.Global.VALIDATORS;
} else {
throw new IllegalArgumentException("Unknown URI: " + contentUri);
}
@@ -660,6 +644,13 @@ public class SettingsBackupAgent extends BackupAgentHelper {
continue;
}
+ // only restore the settings that have valid values
+ if (!isValidSettingValue(key, value, validators)) {
+ Log.w(TAG, "Attempted restore of " + key + " setting, but its value didn't pass"
+ + " validation, value: " + value);
+ continue;
+ }
+
final Uri destination = (movedToGlobal != null && movedToGlobal.contains(key))
? Settings.Global.CONTENT_URI
: contentUri;
@@ -672,6 +663,15 @@ public class SettingsBackupAgent extends BackupAgentHelper {
}
}
+ private boolean isValidSettingValue(String key, String value,
+ Map<String, Validator> validators) {
+ if (key == null || validators == null) {
+ return false;
+ }
+ Validator validator = validators.get(key);
+ return (validator != null) && validator.validate(value);
+ }
+
private final String[] concat(String[] first, @Nullable String[] second) {
if (second == null || second.length == 0) {
return first;
diff --git a/com/android/providers/settings/SettingsHelper.java b/com/android/providers/settings/SettingsHelper.java
index fc765f4d..91957e1f 100644
--- a/com/android/providers/settings/SettingsHelper.java
+++ b/com/android/providers/settings/SettingsHelper.java
@@ -16,6 +16,7 @@
package com.android.providers.settings;
+import android.os.Process;
import com.android.internal.R;
import com.android.internal.app.LocalePicker;
import com.android.internal.annotations.VisibleForTesting;
@@ -29,6 +30,7 @@ import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
+import android.hardware.display.DisplayManager;
import android.icu.util.ULocale;
import android.location.LocationManager;
import android.media.AudioManager;
@@ -288,12 +290,12 @@ public class SettingsHelper {
}
final String GPS = LocationManager.GPS_PROVIDER;
boolean enabled =
- GPS.equals(value) ||
+ GPS.equals(value) ||
value.startsWith(GPS + ",") ||
value.endsWith("," + GPS) ||
value.contains("," + GPS + ",");
- Settings.Secure.setLocationProviderEnabled(
- mContext.getContentResolver(), GPS, enabled);
+ LocationManager lm = (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+ lm.setProviderEnabledForUser(GPS, enabled, Process.myUserHandle());
}
private void setSoundEffects(boolean enable) {
@@ -305,15 +307,7 @@ public class SettingsHelper {
}
private void setBrightness(int brightness) {
- try {
- IPowerManager power = IPowerManager.Stub.asInterface(
- ServiceManager.getService("power"));
- if (power != null) {
- power.setTemporaryScreenBrightnessSettingOverride(brightness);
- }
- } catch (RemoteException doe) {
-
- }
+ mContext.getSystemService(DisplayManager.class).setTemporaryBrightness(brightness);
}
/* package */ byte[] getLocaleData() {
diff --git a/com/android/providers/settings/SettingsProtoDumpUtil.java b/com/android/providers/settings/SettingsProtoDumpUtil.java
index 1d3f26ee..d556db47 100644
--- a/com/android/providers/settings/SettingsProtoDumpUtil.java
+++ b/com/android/providers/settings/SettingsProtoDumpUtil.java
@@ -1119,6 +1119,24 @@ class SettingsProtoDumpUtil {
dumpSetting(s, p,
Settings.Global.NOTIFICATION_SNOOZE_OPTIONS,
GlobalSettingsProto.NOTIFICATION_SNOOZE_OPTIONS);
+ dumpSetting(s, p,
+ Settings.Global.ZRAM_ENABLED,
+ GlobalSettingsProto.ZRAM_ENABLED);
+ dumpSetting(s, p,
+ Settings.Global.ENABLE_SMART_REPLIES_IN_NOTIFICATIONS,
+ GlobalSettingsProto.ENABLE_SMART_REPLIES_IN_NOTIFICATIONS);
+ dumpSetting(s, p,
+ Settings.Global.SHOW_FIRST_CRASH_DIALOG,
+ GlobalSettingsProto.SHOW_FIRST_CRASH_DIALOG);
+ dumpSetting(s, p,
+ Settings.Global.WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED,
+ GlobalSettingsProto.WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED);
+ dumpSetting(s, p,
+ Settings.Global.SHOW_RESTART_IN_CRASH_DIALOG,
+ GlobalSettingsProto.SHOW_RESTART_IN_CRASH_DIALOG);
+ dumpSetting(s, p,
+ Settings.Global.SHOW_MUTE_IN_CRASH_DIALOG,
+ GlobalSettingsProto.SHOW_MUTE_IN_CRASH_DIALOG);
}
/** Dump a single {@link SettingsState.Setting} to a proto buf */
@@ -1211,9 +1229,6 @@ class SettingsProtoDumpUtil {
dumpSetting(s, p,
Settings.Secure.LOCATION_MODE,
SecureSettingsProto.LOCATION_MODE);
- dumpSetting(s, p,
- Settings.Secure.LOCATION_PREVIOUS_MODE,
- SecureSettingsProto.LOCATION_PREVIOUS_MODE);
// Settings.Secure.LOCK_BIOMETRIC_WEAK_FLAGS intentionally excluded since it's deprecated.
dumpSetting(s, p,
Settings.Secure.LOCK_TO_APP_EXIT_LOCKED,
@@ -1510,6 +1525,9 @@ class SettingsProtoDumpUtil {
Settings.Secure.ANR_SHOW_BACKGROUND,
SecureSettingsProto.ANR_SHOW_BACKGROUND);
dumpSetting(s, p,
+ Settings.Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION,
+ SecureSettingsProto.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION);
+ dumpSetting(s, p,
Settings.Secure.VOICE_RECOGNITION_SERVICE,
SecureSettingsProto.VOICE_RECOGNITION_SERVICE);
dumpSetting(s, p,
@@ -1720,9 +1738,6 @@ class SettingsProtoDumpUtil {
Settings.Secure.QS_TILES,
SecureSettingsProto.QS_TILES);
dumpSetting(s, p,
- Settings.Secure.DEMO_USER_SETUP_COMPLETE,
- SecureSettingsProto.DEMO_USER_SETUP_COMPLETE);
- dumpSetting(s, p,
Settings.Secure.INSTANT_APPS_ENABLED,
SecureSettingsProto.INSTANT_APPS_ENABLED);
dumpSetting(s, p,
@@ -1746,6 +1761,9 @@ class SettingsProtoDumpUtil {
dumpSetting(s, p,
Settings.Secure.BACKUP_MANAGER_CONSTANTS,
SecureSettingsProto.BACKUP_MANAGER_CONSTANTS);
+ dumpSetting(s, p,
+ Settings.Secure.BLUETOOTH_ON_WHILE_DRIVING,
+ SecureSettingsProto.BLUETOOTH_ON_WHILE_DRIVING);
}
private static void dumpProtoSystemSettingsLocked(
diff --git a/com/android/providers/settings/SettingsProvider.java b/com/android/providers/settings/SettingsProvider.java
index bef2bcbd..b7d6da43 100644
--- a/com/android/providers/settings/SettingsProvider.java
+++ b/com/android/providers/settings/SettingsProvider.java
@@ -60,6 +60,7 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.os.UserManagerInternal;
import android.provider.Settings;
+import android.provider.SettingsValidators;
import android.provider.Settings.Global;
import android.provider.Settings.Secure;
import android.text.TextUtils;
@@ -297,6 +298,12 @@ public class SettingsProvider extends ContentProvider {
@Override
public boolean onCreate() {
Settings.setInSystemServer();
+
+ // fail to boot if there're any backed up settings that don't have a non-null validator
+ ensureAllBackedUpSystemSettingsHaveValidators();
+ ensureAllBackedUpGlobalSettingsHaveValidators();
+ ensureAllBackedUpSecureSettingsHaveValidators();
+
synchronized (mLock) {
mUserManager = UserManager.get(getContext());
mPackageManager = AppGlobals.getPackageManager();
@@ -314,6 +321,57 @@ public class SettingsProvider extends ContentProvider {
return true;
}
+ private void ensureAllBackedUpSystemSettingsHaveValidators() {
+ String offenders = getOffenders(concat(Settings.System.SETTINGS_TO_BACKUP,
+ Settings.System.LEGACY_RESTORE_SETTINGS), Settings.System.VALIDATORS);
+
+ failToBootIfOffendersPresent(offenders, "Settings.System");
+ }
+
+ private void ensureAllBackedUpGlobalSettingsHaveValidators() {
+ String offenders = getOffenders(concat(Settings.Global.SETTINGS_TO_BACKUP,
+ Settings.Global.LEGACY_RESTORE_SETTINGS), Settings.Global.VALIDATORS);
+
+ failToBootIfOffendersPresent(offenders, "Settings.Global");
+ }
+
+ private void ensureAllBackedUpSecureSettingsHaveValidators() {
+ String offenders = getOffenders(concat(Settings.Secure.SETTINGS_TO_BACKUP,
+ Settings.Secure.LEGACY_RESTORE_SETTINGS), Settings.Secure.VALIDATORS);
+
+ failToBootIfOffendersPresent(offenders, "Settings.Secure");
+ }
+
+ private void failToBootIfOffendersPresent(String offenders, String settingsType) {
+ if (offenders.length() > 0) {
+ throw new RuntimeException("All " + settingsType + " settings that are backed up"
+ + " have to have a non-null validator, but those don't: " + offenders);
+ }
+ }
+
+ private String getOffenders(String[] settingsToBackup, Map<String,
+ SettingsValidators.Validator> validators) {
+ StringBuilder offenders = new StringBuilder();
+ for (String setting : settingsToBackup) {
+ if (validators.get(setting) == null) {
+ offenders.append(setting).append(" ");
+ }
+ }
+ return offenders.toString();
+ }
+
+ private final String[] concat(String[] first, String[] second) {
+ if (second == null || second.length == 0) {
+ return first;
+ }
+ final int firstLen = first.length;
+ final int secondLen = second.length;
+ String[] both = new String[firstLen + secondLen];
+ System.arraycopy(first, 0, both, 0, firstLen);
+ System.arraycopy(second, 0, both, firstLen, secondLen);
+ return both;
+ }
+
@Override
public Bundle call(String method, String name, Bundle args) {
final int requestingUserId = getRequestingUserId(args);
@@ -1472,7 +1530,7 @@ public class SettingsProvider extends ContentProvider {
}
private void validateSystemSettingValue(String name, String value) {
- Settings.System.Validator validator = Settings.System.VALIDATORS.get(name);
+ SettingsValidators.Validator validator = Settings.System.VALIDATORS.get(name);
if (validator != null && !validator.validate(value)) {
throw new IllegalArgumentException("Invalid value: " + value
+ " for setting: " + name);
@@ -1584,6 +1642,20 @@ public class SettingsProvider extends ContentProvider {
restriction = UserManager.DISALLOW_SAFE_BOOT;
break;
+ case Settings.Global.AIRPLANE_MODE_ON:
+ if ("0".equals(value)) return false;
+ restriction = UserManager.DISALLOW_AIRPLANE_MODE;
+ break;
+
+ case Settings.Secure.DOZE_ENABLED:
+ case Settings.Secure.DOZE_ALWAYS_ON:
+ case Settings.Secure.DOZE_PULSE_ON_PICK_UP:
+ case Settings.Secure.DOZE_PULSE_ON_LONG_PRESS:
+ case Settings.Secure.DOZE_PULSE_ON_DOUBLE_TAP:
+ if ("0".equals(value)) return false;
+ restriction = UserManager.DISALLOW_AMBIENT_DISPLAY;
+ break;
+
default:
if (setting != null && setting.startsWith(Settings.Global.DATA_ROAMING)) {
if ("0".equals(value)) return false;
@@ -2592,7 +2664,9 @@ public class SettingsProvider extends ContentProvider {
public void onUidRemovedLocked(int uid) {
final SettingsState ssaidSettings = getSettingsLocked(SETTINGS_TYPE_SSAID,
UserHandle.getUserId(uid));
- ssaidSettings.deleteSettingLocked(Integer.toString(uid));
+ if (ssaidSettings != null) {
+ ssaidSettings.deleteSettingLocked(Integer.toString(uid));
+ }
}
@Nullable
@@ -2827,11 +2901,14 @@ public class SettingsProvider extends ContentProvider {
for (int i = 0; i < users.size(); i++) {
final int userId = users.get(i).id;
+ // Do we have to increment the generation for users that are not running?
+ // Yeah let's assume so...
+ final int key = makeKey(SETTINGS_TYPE_SECURE, userId);
+ mGenerationRegistry.incrementGeneration(key);
+
if (!mUserManager.isUserRunning(UserHandle.of(userId))) {
continue;
}
-
- final int key = makeKey(SETTINGS_TYPE_GLOBAL, userId);
final Uri uri = getNotificationUriFor(key, Secure.LOCATION_PROVIDERS_ALLOWED);
mHandler.obtainMessage(MyHandler.MSG_NOTIFY_URI_CHANGED,
@@ -2938,7 +3015,7 @@ public class SettingsProvider extends ContentProvider {
}
private final class UpgradeController {
- private static final int SETTINGS_VERSION = 150;
+ private static final int SETTINGS_VERSION = 151;
private final int mUserId;
@@ -3531,6 +3608,18 @@ public class SettingsProvider extends ContentProvider {
currentVersion = 150;
}
+ if (currentVersion == 150) {
+ // Version 151: Reset rotate locked setting for upgrading users
+ final SettingsState systemSettings = getSystemSettingsLocked(userId);
+ systemSettings.insertSettingLocked(
+ Settings.System.ACCELEROMETER_ROTATION,
+ getContext().getResources().getBoolean(
+ R.bool.def_accelerometer_rotation) ? "1" : "0",
+ null, true, SettingsState.SYSTEM_PACKAGE_NAME);
+
+ currentVersion = 151;
+ }
+
// vXXX: Add new settings above this point.
if (currentVersion != newVersion) {
diff --git a/com/android/server/AlarmManagerService.java b/com/android/server/AlarmManagerService.java
index 06cf9820..342b48ee 100644
--- a/com/android/server/AlarmManagerService.java
+++ b/com/android/server/AlarmManagerService.java
@@ -82,6 +82,7 @@ import java.util.TimeZone;
import java.util.TreeSet;
import java.util.function.Predicate;
+import static android.app.AlarmManager.FLAG_ALLOW_WHILE_IDLE;
import static android.app.AlarmManager.RTC_WAKEUP;
import static android.app.AlarmManager.RTC;
import static android.app.AlarmManager.ELAPSED_REALTIME_WAKEUP;
@@ -89,10 +90,10 @@ import static android.app.AlarmManager.ELAPSED_REALTIME;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
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.
@@ -149,6 +150,11 @@ class AlarmManagerService extends SystemService {
private long mNextNonWakeup;
private long mLastWakeupSet;
private long mLastWakeup;
+ private long mLastTickSet;
+ private long mLastTickIssued; // elapsed
+ private long mLastTickReceived;
+ private long mLastTickAdded;
+ private long mLastTickRemoved;
int mBroadcastRefCount = 0;
PowerManager.WakeLock mWakeLock;
boolean mLastWakeLockUnimportantForLogging;
@@ -170,7 +176,6 @@ class AlarmManagerService extends SystemService {
long mNextNonWakeupDeliveryTime;
long mLastTimeChangeClockTime;
long mLastTimeChangeRealtime;
- long mAllowWhileIdleMinTime;
int mNumTimeChanged;
// Bookkeeping about the identity of the "System UI" package, determined at runtime.
@@ -194,6 +199,12 @@ class AlarmManagerService extends SystemService {
*/
final SparseLongArray mLastAllowWhileIdleDispatch = new SparseLongArray();
+ /**
+ * For each uid, we store whether the last allow-while-idle alarm was dispatched while
+ * the uid was in foreground or not. We will use the allow_while_idle_short_time in such cases.
+ */
+ final SparseBooleanArray mUseAllowWhileIdleShortTime = new SparseBooleanArray();
+
final static class IdleDispatchEntry {
int uid;
String pkg;
@@ -237,7 +248,6 @@ class AlarmManagerService extends SystemService {
private static final String KEY_ALLOW_WHILE_IDLE_WHITELIST_DURATION
= "allow_while_idle_whitelist_duration";
private static final String KEY_LISTENER_TIMEOUT = "listener_timeout";
- private static final String KEY_BG_RESTRICTIONS_ENABLED = "limit_bg_alarms_enabled";
private static final long DEFAULT_MIN_FUTURITY = 5 * 1000;
private static final long DEFAULT_MIN_INTERVAL = 60 * 1000;
@@ -272,7 +282,6 @@ class AlarmManagerService extends SystemService {
public Constants(Handler handler) {
super(handler);
- updateAllowWhileIdleMinTimeLocked();
updateAllowWhileIdleWhitelistDurationLocked();
}
@@ -283,11 +292,6 @@ class AlarmManagerService extends SystemService {
updateConstants();
}
- public void updateAllowWhileIdleMinTimeLocked() {
- mAllowWhileIdleMinTime = mPendingIdleUntil != null
- ? ALLOW_WHILE_IDLE_LONG_TIME : ALLOW_WHILE_IDLE_SHORT_TIME;
- }
-
public void updateAllowWhileIdleWhitelistDurationLocked() {
if (mLastAllowWhileIdleWhitelistDuration != ALLOW_WHILE_IDLE_WHITELIST_DURATION) {
mLastAllowWhileIdleWhitelistDuration = ALLOW_WHILE_IDLE_WHITELIST_DURATION;
@@ -325,7 +329,6 @@ class AlarmManagerService extends SystemService {
LISTENER_TIMEOUT = mParser.getLong(KEY_LISTENER_TIMEOUT,
DEFAULT_LISTENER_TIMEOUT);
- updateAllowWhileIdleMinTimeLocked();
updateAllowWhileIdleWhitelistDurationLocked();
}
}
@@ -428,6 +431,9 @@ class AlarmManagerService extends SystemService {
end = seed.maxWhenElapsed;
flags = seed.flags;
alarms.add(seed);
+ if (seed.operation == mTimeTickSender) {
+ mLastTickAdded = System.currentTimeMillis();
+ }
}
int size() {
@@ -450,6 +456,9 @@ class AlarmManagerService extends SystemService {
index = 0 - index - 1;
}
alarms.add(index, alarm);
+ if (alarm.operation == mTimeTickSender) {
+ mLastTickAdded = System.currentTimeMillis();
+ }
if (DEBUG_BATCH) {
Slog.v(TAG, "Adding " + alarm + " to " + this);
}
@@ -481,6 +490,9 @@ class AlarmManagerService extends SystemService {
if (alarm.alarmClock != null) {
mNextAlarmClockMayChange = true;
}
+ if (alarm.operation == mTimeTickSender) {
+ mLastTickRemoved = System.currentTimeMillis();
+ }
} else {
if (alarm.whenElapsed > newStart) {
newStart = alarm.whenElapsed;
@@ -697,6 +709,39 @@ class AlarmManagerService extends SystemService {
}
return -1;
}
+ /** @return total count of the alarms in a set of alarm batches. */
+ static int getAlarmCount(ArrayList<Batch> batches) {
+ int ret = 0;
+
+ final int size = batches.size();
+ for (int i = 0; i < size; i++) {
+ ret += batches.get(i).size();
+ }
+ return ret;
+ }
+
+ boolean haveAlarmsTimeTickAlarm(ArrayList<Alarm> alarms) {
+ if (alarms.size() == 0) {
+ return false;
+ }
+ final int batchSize = alarms.size();
+ for (int j = 0; j < batchSize; j++) {
+ if (alarms.get(j).operation == mTimeTickSender) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean haveBatchesTimeTickAlarm(ArrayList<Batch> batches) {
+ final int numBatches = batches.size();
+ for (int i = 0; i < numBatches; i++) {
+ if (haveAlarmsTimeTickAlarm(batches.get(i).alarms)) {
+ return true;
+ }
+ }
+ return false;
+ }
// The RTC clock has moved arbitrarily, so we need to recalculate all the batching
void rebatchAllAlarms() {
@@ -706,6 +751,11 @@ class AlarmManagerService extends SystemService {
}
void rebatchAllAlarmsLocked(boolean doValidate) {
+ final int oldCount =
+ getAlarmCount(mAlarmBatches) + ArrayUtils.size(mPendingWhileIdleAlarms);
+ final boolean oldHasTick = haveBatchesTimeTickAlarm(mAlarmBatches)
+ || haveAlarmsTimeTickAlarm(mPendingWhileIdleAlarms);
+
ArrayList<Batch> oldSet = (ArrayList<Batch>) mAlarmBatches.clone();
mAlarmBatches.clear();
Alarm oldPendingIdleUntil = mPendingIdleUntil;
@@ -726,6 +776,18 @@ class AlarmManagerService extends SystemService {
restorePendingWhileIdleAlarmsLocked();
}
}
+ final int newCount =
+ getAlarmCount(mAlarmBatches) + ArrayUtils.size(mPendingWhileIdleAlarms);
+ final boolean newHasTick = haveBatchesTimeTickAlarm(mAlarmBatches)
+ || haveAlarmsTimeTickAlarm(mPendingWhileIdleAlarms);
+
+ if (oldCount != newCount) {
+ Slog.wtf(TAG, "Rebatching: total count changed from " + oldCount + " to " + newCount);
+ }
+ if (oldHasTick != newHasTick) {
+ Slog.wtf(TAG, "Rebatching: hasTick changed from " + oldHasTick + " to " + newHasTick);
+ }
+
rescheduleKernelAlarmsLocked();
updateNextAlarmClockLocked();
}
@@ -903,9 +965,6 @@ class AlarmManagerService extends SystemService {
}
}
- // Make sure we are using the correct ALLOW_WHILE_IDLE min time.
- mConstants.updateAllowWhileIdleMinTimeLocked();
-
// Reschedule everything.
rescheduleKernelAlarmsLocked();
updateNextAlarmClockLocked();
@@ -1159,12 +1218,12 @@ class AlarmManagerService extends SystemService {
// ignored; both services live in system_server
}
publishBinderService(Context.ALARM_SERVICE, mService);
- mForceAppStandbyTracker.start();
}
@Override
public void onBootPhase(int phase) {
if (phase == PHASE_SYSTEM_SERVICES_READY) {
+ mForceAppStandbyTracker.start();
mConstants.start(getContext().getContentResolver());
mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
mLocalDeviceIdleController
@@ -1357,7 +1416,6 @@ class AlarmManagerService extends SystemService {
return;
}
}
-
if (RECORD_DEVICE_IDLE_ALARMS) {
if ((a.flags & AlarmManager.FLAG_ALLOW_WHILE_IDLE) != 0) {
IdleDispatchEntry ent = new IdleDispatchEntry();
@@ -1408,7 +1466,6 @@ class AlarmManagerService extends SystemService {
}
mPendingIdleUntil = a;
- mConstants.updateAllowWhileIdleMinTimeLocked();
needRebatch = true;
} else if ((a.flags&AlarmManager.FLAG_WAKE_FROM_IDLE) != 0) {
if (mNextWakeFromIdle == null || mNextWakeFromIdle.whenElapsed > a.whenElapsed) {
@@ -1596,10 +1653,11 @@ class AlarmManagerService extends SystemService {
pw.println();
mForceAppStandbyTracker.dump(pw, " ");
+ pw.println();
final long nowRTC = System.currentTimeMillis();
final long nowELAPSED = SystemClock.elapsedRealtime();
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
pw.print(" nowRTC="); pw.print(nowRTC);
pw.print("="); pw.print(sdf.format(new Date(nowRTC)));
@@ -1607,8 +1665,13 @@ class AlarmManagerService extends SystemService {
pw.println();
pw.print(" mLastTimeChangeClockTime="); pw.print(mLastTimeChangeClockTime);
pw.print("="); pw.println(sdf.format(new Date(mLastTimeChangeClockTime)));
- pw.print(" mLastTimeChangeRealtime=");
- TimeUtils.formatDuration(mLastTimeChangeRealtime, pw);
+ pw.print(" mLastTimeChangeRealtime="); pw.println(mLastTimeChangeRealtime);
+ pw.print(" mLastTickIssued=");
+ pw.println(sdf.format(new Date(nowRTC - (nowELAPSED - mLastTickIssued))));
+ pw.print(" mLastTickReceived="); pw.println(sdf.format(new Date(mLastTickReceived)));
+ pw.print(" mLastTickSet="); pw.println(sdf.format(new Date(mLastTickSet)));
+ pw.print(" mLastTickAdded="); pw.println(sdf.format(new Date(mLastTickAdded)));
+ pw.print(" mLastTickRemoved="); pw.println(sdf.format(new Date(mLastTickRemoved)));
pw.println();
if (!mInteractive) {
pw.print(" Time since non-interactive: ");
@@ -1681,6 +1744,15 @@ class AlarmManagerService extends SystemService {
if (!blocked) {
pw.println(" none");
}
+ pw.print(" mUseAllowWhileIdleShortTime: [");
+ for (int i = 0; i < mUseAllowWhileIdleShortTime.size(); i++) {
+ if (mUseAllowWhileIdleShortTime.valueAt(i)) {
+ UserHandle.formatUid(pw, mUseAllowWhileIdleShortTime.keyAt(i));
+ pw.print(" ");
+ }
+ }
+ pw.println("]");
+
if (mPendingIdleUntil != null || mPendingWhileIdleAlarms.size() > 0) {
pw.println();
pw.println(" Idle mode state:");
@@ -1733,9 +1805,6 @@ class AlarmManagerService extends SystemService {
pw.println();
}
- pw.print(" mAllowWhileIdleMinTime=");
- TimeUtils.formatDuration(mAllowWhileIdleMinTime, pw);
- pw.println();
if (mLastAllowWhileIdleDispatch.size() > 0) {
pw.println(" Last allow while idle dispatch times:");
for (int i=0; i<mLastAllowWhileIdleDispatch.size(); i++) {
@@ -2002,8 +2071,6 @@ class AlarmManagerService extends SystemService {
f.writeToProto(proto, AlarmManagerServiceProto.OUTSTANDING_DELIVERIES);
}
- proto.write(AlarmManagerServiceProto.ALLOW_WHILE_IDLE_MIN_DURATION_MS,
- mAllowWhileIdleMinTime);
for (int i = 0; i < mLastAllowWhileIdleDispatch.size(); ++i) {
final long token = proto.start(
AlarmManagerServiceProto.LAST_ALLOW_WHILE_IDLE_DISPATCH_TIMES);
@@ -2392,6 +2459,10 @@ class AlarmManagerService extends SystemService {
}
void removeLocked(final int uid) {
+ if (uid == Process.SYSTEM_UID) {
+ Slog.wtf(TAG, "removeLocked: Shouldn't for UID=" + uid);
+ return;
+ }
boolean didRemove = false;
final Predicate<Alarm> whichAlarms = (Alarm a) -> a.uid == uid;
for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
@@ -2440,6 +2511,7 @@ class AlarmManagerService extends SystemService {
boolean didRemove = false;
final Predicate<Alarm> whichAlarms = (Alarm a) -> a.matches(packageName);
+ final boolean oldHasTick = haveBatchesTimeTickAlarm(mAlarmBatches);
for (int i = mAlarmBatches.size() - 1; i >= 0; i--) {
Batch b = mAlarmBatches.get(i);
didRemove |= b.remove(whichAlarms);
@@ -2447,6 +2519,11 @@ class AlarmManagerService extends SystemService {
mAlarmBatches.remove(i);
}
}
+ final boolean newHasTick = haveBatchesTimeTickAlarm(mAlarmBatches);
+ if (oldHasTick != newHasTick) {
+ Slog.wtf(TAG, "removeLocked: hasTick changed from " + oldHasTick + " to " + newHasTick);
+ }
+
for (int i = mPendingWhileIdleAlarms.size() - 1; i >= 0; i--) {
final Alarm a = mPendingWhileIdleAlarms.get(i);
if (a.matches(packageName)) {
@@ -2476,6 +2553,10 @@ class AlarmManagerService extends SystemService {
}
void removeForStoppedLocked(final int uid) {
+ if (uid == Process.SYSTEM_UID) {
+ Slog.wtf(TAG, "removeForStoppedLocked: Shouldn't for UID=" + uid);
+ return;
+ }
boolean didRemove = false;
final Predicate<Alarm> whichAlarms = (Alarm a) -> {
try {
@@ -2516,6 +2597,10 @@ class AlarmManagerService extends SystemService {
}
void removeUserLocked(int userHandle) {
+ if (userHandle == UserHandle.USER_SYSTEM) {
+ Slog.wtf(TAG, "removeForStoppedLocked: Shouldn't for user=" + userHandle);
+ return;
+ }
boolean didRemove = false;
final Predicate<Alarm> whichAlarms =
(Alarm a) -> UserHandle.getUserId(a.creatorUid) == userHandle;
@@ -2651,6 +2736,7 @@ class AlarmManagerService extends SystemService {
}
private boolean isBackgroundRestricted(Alarm alarm) {
+ final boolean allowWhileIdle = (alarm.flags & FLAG_ALLOW_WHILE_IDLE) != 0;
if (alarm.alarmClock != null) {
// Don't block alarm clocks
return false;
@@ -2663,7 +2749,8 @@ class AlarmManagerService extends SystemService {
final String sourcePackage =
(alarm.operation != null) ? alarm.operation.getCreatorPackage() : alarm.packageName;
final int sourceUid = alarm.creatorUid;
- return mForceAppStandbyTracker.areAlarmsRestricted(sourceUid, sourcePackage);
+ return mForceAppStandbyTracker.areAlarmsRestricted(sourceUid, sourcePackage,
+ allowWhileIdle);
}
private native long init();
@@ -2697,8 +2784,21 @@ class AlarmManagerService extends SystemService {
if ((alarm.flags&AlarmManager.FLAG_ALLOW_WHILE_IDLE) != 0) {
// If this is an ALLOW_WHILE_IDLE alarm, we constrain how frequently the app can
// schedule such alarms.
- long lastTime = mLastAllowWhileIdleDispatch.get(alarm.uid, 0);
- long minTime = lastTime + mAllowWhileIdleMinTime;
+ final long lastTime = mLastAllowWhileIdleDispatch.get(alarm.creatorUid, 0);
+ final boolean dozing = mPendingIdleUntil != null;
+ final boolean ebs = mForceAppStandbyTracker.isForceAllAppsStandbyEnabled();
+ final long minTime;
+ if (!dozing && !ebs) {
+ minTime = lastTime + mConstants.ALLOW_WHILE_IDLE_SHORT_TIME;
+ } else if (dozing) {
+ minTime = lastTime + mConstants.ALLOW_WHILE_IDLE_LONG_TIME;
+ } else if (mUseAllowWhileIdleShortTime.get(alarm.creatorUid)) {
+ // if the last allow-while-idle went off while uid was fg, or the uid
+ // recently came into fg, don't block the alarm for long.
+ minTime = lastTime + mConstants.ALLOW_WHILE_IDLE_SHORT_TIME;
+ } else {
+ minTime = lastTime + mConstants.ALLOW_WHILE_IDLE_LONG_TIME;
+ }
if (nowELAPSED < minTime) {
// Whoops, it hasn't been long enough since the last ALLOW_WHILE_IDLE
// alarm went off for this app. Reschedule the alarm to be in the
@@ -3035,15 +3135,8 @@ class AlarmManagerService extends SystemService {
Slog.v(TAG, "sending alarm " + alarm);
}
if (RECORD_ALARMS_IN_HISTORY) {
- if (alarm.workSource != null && alarm.workSource.size() > 0) {
- for (int wi=0; wi<alarm.workSource.size(); wi++) {
- ActivityManager.noteAlarmStart(
- alarm.operation, alarm.workSource.get(wi), alarm.statsTag);
- }
- } else {
- ActivityManager.noteAlarmStart(
- alarm.operation, alarm.uid, alarm.statsTag);
- }
+ ActivityManager.noteAlarmStart(alarm.operation, alarm.workSource, alarm.uid,
+ alarm.statsTag);
}
mDeliveryTracker.deliverLocked(alarm, nowELAPSED, allowWhileIdle);
} catch (RuntimeException e) {
@@ -3291,6 +3384,9 @@ class AlarmManagerService extends SystemService {
if (DEBUG_BATCH) {
Slog.v(TAG, "Received TIME_TICK alarm; rescheduling");
}
+ synchronized (mLock) {
+ mLastTickReceived = System.currentTimeMillis();
+ }
scheduleTimeTickEvent();
} else if (intent.getAction().equals(Intent.ACTION_DATE_CHANGED)) {
// Since the kernel does not keep track of DST, we need to
@@ -3316,6 +3412,11 @@ class AlarmManagerService extends SystemService {
setImpl(ELAPSED_REALTIME, SystemClock.elapsedRealtime() + tickEventDelay, 0,
0, mTimeTickSender, null, null, AlarmManager.FLAG_STANDALONE, workSource,
null, Process.myUid(), "android");
+
+ // Finally, remember when we set the tick alarm
+ synchronized (mLock) {
+ mLastTickSet = currentTime;
+ }
}
public void scheduleDateChangedEvent() {
@@ -3437,6 +3538,7 @@ class AlarmManagerService extends SystemService {
@Override public void onUidGone(int uid, boolean disabled) {
synchronized (mLock) {
+ mUseAllowWhileIdleShortTime.delete(uid);
if (disabled) {
removeForStoppedLocked(uid);
}
@@ -3444,6 +3546,9 @@ class AlarmManagerService extends SystemService {
}
@Override public void onUidActive(int uid) {
+ synchronized (mLock) {
+ mUseAllowWhileIdleShortTime.put(uid, true);
+ }
}
@Override public void onUidIdle(int uid, boolean disabled) {
@@ -3458,7 +3563,6 @@ class AlarmManagerService extends SystemService {
}
};
-
private final Listener mForceAppStandbyListener = new Listener() {
@Override
public void unblockAllUnrestrictedAlarms() {
@@ -3553,15 +3657,8 @@ class AlarmManagerService extends SystemService {
fs.aggregateTime += nowELAPSED - fs.startTime;
}
if (RECORD_ALARMS_IN_HISTORY) {
- if (inflight.mWorkSource != null && inflight.mWorkSource.size() > 0) {
- for (int wi=0; wi<inflight.mWorkSource.size(); wi++) {
- ActivityManager.noteAlarmFinish(
- inflight.mPendingIntent, inflight.mWorkSource.get(wi), inflight.mTag);
- }
- } else {
- ActivityManager.noteAlarmFinish(
- inflight.mPendingIntent, inflight.mUid, inflight.mTag);
- }
+ ActivityManager.noteAlarmFinish(inflight.mPendingIntent, inflight.mWorkSource,
+ inflight.mUid, inflight.mTag);
}
}
@@ -3674,6 +3771,11 @@ class AlarmManagerService extends SystemService {
if (alarm.operation != null) {
// PendingIntent alarm
mSendCount++;
+
+ if (alarm.priorityClass.priority == PRIO_TICK) {
+ mLastTickIssued = nowELAPSED;
+ }
+
try {
alarm.operation.send(getContext(), 0,
mBackgroundIntent.putExtra(
@@ -3681,6 +3783,9 @@ class AlarmManagerService extends SystemService {
mDeliveryTracker, mHandler, null,
allowWhileIdle ? mIdleOptions : null);
} catch (PendingIntent.CanceledException e) {
+ if (alarm.operation == mTimeTickSender) {
+ Slog.wtf(TAG, "mTimeTickSender canceled");
+ }
if (alarm.repeatInterval > 0) {
// This IntentSender is no longer valid, but this
// is a repeating alarm, so toss it
@@ -3739,7 +3844,12 @@ class AlarmManagerService extends SystemService {
if (allowWhileIdle) {
// Record the last time this uid handled an ALLOW_WHILE_IDLE alarm.
- mLastAllowWhileIdleDispatch.put(alarm.uid, nowELAPSED);
+ mLastAllowWhileIdleDispatch.put(alarm.creatorUid, nowELAPSED);
+ if (mForceAppStandbyTracker.isInForeground(alarm.creatorUid)) {
+ mUseAllowWhileIdleShortTime.put(alarm.creatorUid, true);
+ } else {
+ mUseAllowWhileIdleShortTime.put(alarm.creatorUid, false);
+ }
if (RECORD_DEVICE_IDLE_ALARMS) {
IdleDispatchEntry ent = new IdleDispatchEntry();
ent.uid = alarm.uid;
@@ -3771,18 +3881,9 @@ class AlarmManagerService extends SystemService {
|| alarm.type == RTC_WAKEUP) {
bs.numWakeup++;
fs.numWakeup++;
- if (alarm.workSource != null && alarm.workSource.size() > 0) {
- for (int wi=0; wi<alarm.workSource.size(); wi++) {
- final String wsName = alarm.workSource.getName(wi);
- ActivityManager.noteWakeupAlarm(
- alarm.operation, alarm.workSource.get(wi),
- (wsName != null) ? wsName : alarm.packageName,
- alarm.statsTag);
- }
- } else {
- ActivityManager.noteWakeupAlarm(
- alarm.operation, alarm.uid, alarm.packageName, alarm.statsTag);
- }
+ ActivityManager.noteWakeupAlarm(
+ alarm.operation, alarm.workSource, alarm.uid, alarm.packageName,
+ alarm.statsTag);
}
}
}
diff --git a/com/android/server/AppOpsService.java b/com/android/server/AppOpsService.java
index 4ffa5f1f..f4675fd3 100644
--- a/com/android/server/AppOpsService.java
+++ b/com/android/server/AppOpsService.java
@@ -1316,14 +1316,8 @@ public class AppOpsService extends IAppOpsService.Stub {
isPrivileged = (appInfo.privateFlags
& ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0;
} else {
- if ("media".equals(packageName)) {
- pkgUid = Process.MEDIA_UID;
- isPrivileged = false;
- } else if ("audioserver".equals(packageName)) {
- pkgUid = Process.AUDIOSERVER_UID;
- isPrivileged = false;
- } else if ("cameraserver".equals(packageName)) {
- pkgUid = Process.CAMERASERVER_UID;
+ pkgUid = resolveUid(packageName);
+ if (pkgUid >= 0) {
isPrivileged = false;
}
}
@@ -1957,9 +1951,8 @@ public class AppOpsService extends IAppOpsService.Stub {
if (nonpackageUid != -1) {
packageName = null;
} else {
- if ("root".equals(packageName)) {
- packageUid = 0;
- } else {
+ packageUid = resolveUid(packageName);
+ if (packageUid < 0) {
packageUid = AppGlobals.getPackageManager().getPackageUid(packageName,
PackageManager.MATCH_UNINSTALLED_PACKAGES, userId);
}
@@ -2052,6 +2045,10 @@ public class AppOpsService extends IAppOpsService.Stub {
}
if (ops == null || ops.size() <= 0) {
pw.println("No operations.");
+ if (shell.op > AppOpsManager.OP_NONE && shell.op < AppOpsManager._NUM_OP) {
+ pw.println("Default mode: " + AppOpsManager.modeToString(
+ AppOpsManager.opToDefaultMode(shell.op)));
+ }
return 0;
}
final long now = System.currentTimeMillis();
@@ -2061,24 +2058,7 @@ public class AppOpsService extends IAppOpsService.Stub {
AppOpsManager.OpEntry ent = entries.get(j);
pw.print(AppOpsManager.opToName(ent.getOp()));
pw.print(": ");
- switch (ent.getMode()) {
- case AppOpsManager.MODE_ALLOWED:
- pw.print("allow");
- break;
- case AppOpsManager.MODE_IGNORED:
- pw.print("ignore");
- break;
- case AppOpsManager.MODE_ERRORED:
- pw.print("deny");
- break;
- case AppOpsManager.MODE_DEFAULT:
- pw.print("default");
- break;
- default:
- pw.print("mode=");
- pw.print(ent.getMode());
- break;
- }
+ pw.print(AppOpsManager.modeToString(ent.getMode()));
if (ent.getTime() != 0) {
pw.print("; time=");
TimeUtils.formatDuration(now - ent.getTime(), pw);
@@ -2563,16 +2543,41 @@ public class AppOpsService extends IAppOpsService.Stub {
}
private static String resolvePackageName(int uid, String packageName) {
- if (uid == 0) {
+ if (uid == Process.ROOT_UID) {
return "root";
} else if (uid == Process.SHELL_UID) {
return "com.android.shell";
+ } else if (uid == Process.MEDIA_UID) {
+ return "media";
+ } else if (uid == Process.AUDIOSERVER_UID) {
+ return "audioserver";
+ } else if (uid == Process.CAMERASERVER_UID) {
+ return "cameraserver";
} else if (uid == Process.SYSTEM_UID && packageName == null) {
return "android";
}
return packageName;
}
+ private static int resolveUid(String packageName) {
+ if (packageName == null) {
+ return -1;
+ }
+ switch (packageName) {
+ case "root":
+ return Process.ROOT_UID;
+ case "shell":
+ return Process.SHELL_UID;
+ case "media":
+ return Process.MEDIA_UID;
+ case "audioserver":
+ return Process.AUDIOSERVER_UID;
+ case "cameraserver":
+ return Process.CAMERASERVER_UID;
+ }
+ return -1;
+ }
+
private static String[] getPackagesForUid(int uid) {
String[] packageNames = null;
try {
diff --git a/com/android/server/BatteryService.java b/com/android/server/BatteryService.java
index 04d292fa..dc5f5a27 100644
--- a/com/android/server/BatteryService.java
+++ b/com/android/server/BatteryService.java
@@ -383,16 +383,16 @@ public final class BatteryService extends SystemService {
}
}
- private void update(HealthInfo info) {
+ private void update(android.hardware.health.V2_0.HealthInfo info) {
traceBegin("HealthInfoUpdate");
synchronized (mLock) {
if (!mUpdatesStopped) {
- mHealthInfo = info;
+ mHealthInfo = info.legacy;
// Process the new values.
processValuesLocked(false);
mLock.notifyAll(); // for any waiters on new info
} else {
- copy(mLastHealthInfo, info);
+ copy(mLastHealthInfo, info.legacy);
}
}
traceEnd();
@@ -1010,7 +1010,7 @@ public final class BatteryService extends SystemService {
private final class HealthHalCallback extends IHealthInfoCallback.Stub
implements HealthServiceWrapper.Callback {
- @Override public void healthInfoChanged(HealthInfo props) {
+ @Override public void healthInfoChanged(android.hardware.health.V2_0.HealthInfo props) {
BatteryService.this.update(props);
}
// on new service registered
diff --git a/com/android/server/BluetoothManagerService.java b/com/android/server/BluetoothManagerService.java
index d9713a51..20777901 100644
--- a/com/android/server/BluetoothManagerService.java
+++ b/com/android/server/BluetoothManagerService.java
@@ -60,6 +60,7 @@ import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.util.Slog;
+import com.android.internal.R;
import com.android.internal.util.DumpUtils;
import com.android.server.pm.UserRestrictionsUtils;
@@ -415,9 +416,14 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
int systemUiUid = -1;
try {
- systemUiUid = mContext.getPackageManager()
- .getPackageUidAsUser("com.android.systemui", PackageManager.MATCH_SYSTEM_ONLY,
- UserHandle.USER_SYSTEM);
+ // Check if device is configured with no home screen, which implies no SystemUI.
+ boolean noHome = mContext.getResources().getBoolean(R.bool.config_noHomeScreen);
+ if (!noHome) {
+ systemUiUid = mContext.getPackageManager()
+ .getPackageUidAsUser("com.android.systemui", PackageManager.MATCH_SYSTEM_ONLY,
+ UserHandle.USER_SYSTEM);
+ }
+ Slog.d(TAG, "Detected SystemUiUid: " + Integer.toString(systemUiUid));
} 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);
@@ -433,10 +439,17 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
}
+ private boolean supportBluetoothPersistedState() {
+ return mContext.getResources().getBoolean(R.bool.config_supportBluetoothPersistedState);
+ }
+
/**
* Returns true if the Bluetooth saved state is "on"
*/
private boolean isBluetoothPersistedStateOn() {
+ if (!supportBluetoothPersistedState()) {
+ return false;
+ }
int state = Settings.Global.getInt(mContentResolver, Settings.Global.BLUETOOTH_ON, -1);
if (DBG) {
Slog.d(TAG, "Bluetooth persisted state: " + state);
@@ -448,6 +461,9 @@ class BluetoothManagerService extends IBluetoothManager.Stub {
* Returns true if the Bluetooth saved state is BLUETOOTH_ON_BLUETOOTH
*/
private boolean isBluetoothPersistedStateOnBluetooth() {
+ if (!supportBluetoothPersistedState()) {
+ return false;
+ }
return Settings.Global.getInt(mContentResolver, Settings.Global.BLUETOOTH_ON,
BLUETOOTH_ON_BLUETOOTH) == BLUETOOTH_ON_BLUETOOTH;
}
diff --git a/com/android/server/ConnectivityService.java b/com/android/server/ConnectivityService.java
index 86b01646..3bfa31a8 100644
--- a/com/android/server/ConnectivityService.java
+++ b/com/android/server/ConnectivityService.java
@@ -30,6 +30,7 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
import static android.net.NetworkCapabilities.TRANSPORT_VPN;
@@ -63,11 +64,13 @@ import android.net.NetworkConfig;
import android.net.NetworkInfo;
import android.net.NetworkInfo.DetailedState;
import android.net.NetworkMisc;
+import android.net.NetworkPolicyManager;
import android.net.NetworkQuotaInfo;
import android.net.NetworkRequest;
import android.net.NetworkSpecifier;
import android.net.NetworkState;
import android.net.NetworkUtils;
+import android.net.NetworkWatchlistManager;
import android.net.Proxy;
import android.net.ProxyInfo;
import android.net.RouteInfo;
@@ -104,6 +107,7 @@ import android.security.Credentials;
import android.security.KeyStore;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
+import android.util.ArraySet;
import android.util.LocalLog;
import android.util.LocalLog.ReadOnlyLocalLog;
import android.util.Log;
@@ -130,10 +134,13 @@ import com.android.internal.util.WakeupMessage;
import com.android.internal.util.XmlUtils;
import com.android.server.am.BatteryStatsService;
import com.android.server.connectivity.DataConnectionStats;
+import com.android.server.connectivity.DnsManager;
+import com.android.server.connectivity.DnsManager.PrivateDnsConfig;
import com.android.server.connectivity.IpConnectivityMetrics;
import com.android.server.connectivity.KeepaliveTracker;
import com.android.server.connectivity.LingerMonitor;
import com.android.server.connectivity.MockableSystemProperties;
+import com.android.server.connectivity.MultipathPolicyTracker;
import com.android.server.connectivity.NetworkAgentInfo;
import com.android.server.connectivity.NetworkDiagnostics;
import com.android.server.connectivity.NetworkMonitor;
@@ -172,6 +179,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
@@ -225,7 +233,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
@GuardedBy("mVpns")
private final SparseArray<Vpn> mVpns = new SparseArray<Vpn>();
+ // TODO: investigate if mLockdownEnabled can be removed and replaced everywhere by
+ // a direct call to LockdownVpnTracker.isEnabled().
+ @GuardedBy("mVpns")
private boolean mLockdownEnabled;
+ @GuardedBy("mVpns")
private LockdownVpnTracker mLockdownTracker;
final private Context mContext;
@@ -233,8 +245,6 @@ public class ConnectivityService extends IConnectivityManager.Stub
// 0 is full bad, 100 is full good
private int mDefaultInetConditionPublished = 0;
- private int mNumDnsEntries;
-
private boolean mTestMode;
private static ConnectivityService sServiceInstance;
@@ -397,6 +407,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
*/
private static final int EVENT_REVALIDATE_NETWORK = 36;
+ // Handle changes in Private DNS settings.
+ private static final int EVENT_PRIVATE_DNS_SETTINGS_CHANGED = 37;
+
private static String eventName(int what) {
return sMagicDecoderRing.get(what, Integer.toString(what));
}
@@ -408,6 +421,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
final private InternalHandler mHandler;
/** Handler used for incoming {@link NetworkStateTracker} events. */
final private NetworkStateTrackerHandler mTrackerHandler;
+ private final DnsManager mDnsManager;
private boolean mSystemReady;
private Intent mInitialBroadcast;
@@ -445,8 +459,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
private LingerMonitor mLingerMonitor;
// sequence number for Networks; keep in sync with system/netd/NetworkController.cpp
- private final static int MIN_NET_ID = 100; // some reserved marks
- private final static int MAX_NET_ID = 65535;
+ private static final int MIN_NET_ID = 100; // some reserved marks
+ private static final int MAX_NET_ID = 65535 - 0x0400; // Top 1024 bits reserved by IpSecService
private int mNextNetId = MIN_NET_ID;
// sequence number of NetworkRequests
@@ -498,6 +512,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
@VisibleForTesting
final MultinetworkPolicyTracker mMultinetworkPolicyTracker;
+ @VisibleForTesting
+ final MultipathPolicyTracker mMultipathPolicyTracker;
+
/**
* Implements support for the legacy "one network per network type" model.
*
@@ -723,12 +740,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
mSystemProperties = getSystemProperties();
mMetricsLog = logger;
- mDefaultRequest = createInternetRequestForTransport(-1, NetworkRequest.Type.REQUEST);
+ mDefaultRequest = createDefaultInternetRequestForTransport(-1, NetworkRequest.Type.REQUEST);
NetworkRequestInfo defaultNRI = new NetworkRequestInfo(null, mDefaultRequest, new Binder());
mNetworkRequests.put(mDefaultRequest, defaultNRI);
mNetworkRequestInfoLogs.log("REGISTER " + defaultNRI);
- mDefaultMobileDataRequest = createInternetRequestForTransport(
+ mDefaultMobileDataRequest = createDefaultInternetRequestForTransport(
NetworkCapabilities.TRANSPORT_CELLULAR, NetworkRequest.Type.BACKGROUND_REQUEST);
mHandlerThread = new HandlerThread("ConnectivityServiceThread");
@@ -880,6 +897,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
mMultinetworkPolicyTracker = createMultinetworkPolicyTracker(
mContext, mHandler, () -> rematchForAvoidBadWifiUpdate());
mMultinetworkPolicyTracker.start();
+
+ mMultipathPolicyTracker = new MultipathPolicyTracker(mContext, mHandler);
+
+ mDnsManager = new DnsManager(mContext, mNetd, mSystemProperties);
+ registerPrivateDnsSettingsCallbacks();
}
private Tethering makeTethering() {
@@ -890,7 +912,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
deps);
}
- private NetworkRequest createInternetRequestForTransport(
+ private NetworkRequest createDefaultInternetRequestForTransport(
int transportType, NetworkRequest.Type type) {
NetworkCapabilities netCap = new NetworkCapabilities();
netCap.addCapability(NET_CAPABILITY_INTERNET);
@@ -940,6 +962,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
EVENT_CONFIGURE_MOBILE_DATA_ALWAYS_ON);
}
+ private void registerPrivateDnsSettingsCallbacks() {
+ for (Uri u : DnsManager.getPrivateDnsSettingsUris()) {
+ mSettingsObserver.observe(u, EVENT_PRIVATE_DNS_SETTINGS_CHANGED);
+ }
+ }
+
private synchronized int nextNetworkRequestId() {
return mNextNetworkRequestId++;
}
@@ -995,9 +1023,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
private Network[] getVpnUnderlyingNetworks(int uid) {
- if (!mLockdownEnabled) {
- int user = UserHandle.getUserId(uid);
- synchronized (mVpns) {
+ synchronized (mVpns) {
+ if (!mLockdownEnabled) {
+ int user = UserHandle.getUserId(uid);
Vpn vpn = mVpns.get(user);
if (vpn != null && vpn.appliesToUid(uid)) {
return vpn.getUnderlyingNetworks();
@@ -1085,8 +1113,10 @@ public class ConnectivityService extends IConnectivityManager.Stub
if (isNetworkWithLinkPropertiesBlocked(state.linkProperties, uid, ignoreBlocked)) {
state.networkInfo.setDetailedState(DetailedState.BLOCKED, null, null);
}
- if (mLockdownTracker != null) {
- mLockdownTracker.augmentNetworkInfo(state.networkInfo);
+ synchronized (mVpns) {
+ if (mLockdownTracker != null) {
+ mLockdownTracker.augmentNetworkInfo(state.networkInfo);
+ }
}
}
@@ -1251,8 +1281,8 @@ public class ConnectivityService extends IConnectivityManager.Stub
result.put(nai.network, nc);
}
- if (!mLockdownEnabled) {
- synchronized (mVpns) {
+ synchronized (mVpns) {
+ if (!mLockdownEnabled) {
Vpn vpn = mVpns.get(userId);
if (vpn != null) {
Network[] networks = vpn.getUnderlyingNetworks();
@@ -1260,7 +1290,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
for (Network network : networks) {
nai = getNetworkAgentInfoForNetwork(network);
nc = getNetworkCapabilitiesInternal(nai);
+ // nc is a copy of the capabilities in nai, so it's fine to mutate it
+ // TODO : don't remove the UIDs when communicating with processes
+ // that have the NETWORK_SETTINGS permission.
if (nc != null) {
+ nc.setSingleUid(userId);
result.put(network, nc);
}
}
@@ -1482,15 +1516,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
return true;
}
- private final INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
+ private final INetworkPolicyListener mPolicyListener = new NetworkPolicyManager.Listener() {
@Override
public void onUidRulesChanged(int uid, int uidRules) {
// TODO: notify UID when it has requested targeted updates
}
@Override
- public void onMeteredIfacesChanged(String[] meteredIfaces) {
- }
- @Override
public void onRestrictBackgroundChanged(boolean restrictBackground) {
// TODO: relocate this specific callback in Tethering.
if (restrictBackground) {
@@ -1498,9 +1529,6 @@ public class ConnectivityService extends IConnectivityManager.Stub
mTethering.untetherAll();
}
}
- @Override
- public void onUidPoliciesChanged(int uid, int uidPolicies) {
- }
};
/**
@@ -1578,9 +1606,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
private Intent makeGeneralIntent(NetworkInfo info, String bcastType) {
- if (mLockdownTracker != null) {
- info = new NetworkInfo(info);
- mLockdownTracker.augmentNetworkInfo(info);
+ synchronized (mVpns) {
+ if (mLockdownTracker != null) {
+ info = new NetworkInfo(info);
+ mLockdownTracker.augmentNetworkInfo(info);
+ }
}
Intent intent = new Intent(bcastType);
@@ -1826,24 +1856,6 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
}
- private void flushVmDnsCache() {
- /*
- * Tell the VMs to toss their DNS caches
- */
- Intent intent = new Intent(Intent.ACTION_CLEAR_DNS_CACHE);
- intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
- /*
- * Connectivity events can happen before boot has completed ...
- */
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- final long ident = Binder.clearCallingIdentity();
- try {
- mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- }
-
@Override
public int getRestoreDefaultNetworkDelay(int networkType) {
String restoreDefaultNetworkDelayStr = mSystemProperties.get(
@@ -1968,6 +1980,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
pw.println();
dumpAvoidBadWifiSettings(pw);
+ pw.println();
+ mMultipathPolicyTracker.dump(pw);
+
if (argsContain(args, SHORT_ARG) == false) {
pw.println();
synchronized (mValidationLogs) {
@@ -2080,24 +2095,6 @@ public class ConnectivityService extends IConnectivityManager.Stub
if (score != null) updateNetworkScore(nai, score.intValue());
break;
}
- case NetworkAgent.EVENT_UID_RANGES_ADDED: {
- try {
- mNetd.addVpnUidRanges(nai.network.netId, (UidRange[])msg.obj);
- } catch (Exception e) {
- // Never crash!
- loge("Exception in addVpnUidRanges: " + e);
- }
- break;
- }
- case NetworkAgent.EVENT_UID_RANGES_REMOVED: {
- try {
- mNetd.removeVpnUidRanges(nai.network.netId, (UidRange[])msg.obj);
- } catch (Exception e) {
- // Never crash!
- loge("Exception in removeVpnUidRanges: " + e);
- }
- break;
- }
case NetworkAgent.EVENT_SET_EXPLICITLY_SELECTED: {
if (nai.everConnected && !nai.networkMisc.explicitlySelected) {
loge("ERROR: already-connected network explicitly selected.");
@@ -2122,36 +2119,59 @@ public class ConnectivityService extends IConnectivityManager.Stub
synchronized (mNetworkForNetId) {
nai = mNetworkForNetId.get(msg.arg2);
}
- if (nai != null) {
- final boolean valid =
- (msg.arg1 == NetworkMonitor.NETWORK_TEST_RESULT_VALID);
- final boolean wasValidated = nai.lastValidated;
- final boolean wasDefault = isDefaultNetwork(nai);
- if (DBG) log(nai.name() + " validation " + (valid ? "passed" : "failed") +
- (msg.obj == null ? "" : " with redirect to " + (String)msg.obj));
- if (valid != nai.lastValidated) {
- if (wasDefault) {
- metricsLogger().defaultNetworkMetrics().logDefaultNetworkValidity(
- SystemClock.elapsedRealtime(), valid);
- }
- final int oldScore = nai.getCurrentScore();
- nai.lastValidated = valid;
- nai.everValidated |= valid;
- updateCapabilities(oldScore, nai, nai.networkCapabilities);
- // If score has changed, rebroadcast to NetworkFactories. b/17726566
- if (oldScore != nai.getCurrentScore()) sendUpdatedScoreToFactories(nai);
+ if (nai == null) break;
+
+ final boolean valid = (msg.arg1 == NetworkMonitor.NETWORK_TEST_RESULT_VALID);
+ final boolean wasValidated = nai.lastValidated;
+ final boolean wasDefault = isDefaultNetwork(nai);
+
+ final PrivateDnsConfig privateDnsCfg = (msg.obj instanceof PrivateDnsConfig)
+ ? (PrivateDnsConfig) msg.obj : null;
+ final String redirectUrl = (msg.obj instanceof String) ? (String) msg.obj : "";
+
+ final boolean reevaluationRequired;
+ final String logMsg;
+ if (valid) {
+ reevaluationRequired = updatePrivateDns(nai, privateDnsCfg);
+ logMsg = (DBG && (privateDnsCfg != null))
+ ? " with " + privateDnsCfg.toString() : "";
+ } else {
+ reevaluationRequired = false;
+ logMsg = (DBG && !TextUtils.isEmpty(redirectUrl))
+ ? " with redirect to " + redirectUrl : "";
+ }
+ if (DBG) {
+ log(nai.name() + " validation " + (valid ? "passed" : "failed") + logMsg);
+ }
+ // If there is a change in Private DNS configuration,
+ // trigger reevaluation of the network to test it.
+ if (reevaluationRequired) {
+ nai.networkMonitor.sendMessage(
+ NetworkMonitor.CMD_FORCE_REEVALUATION, Process.SYSTEM_UID);
+ break;
+ }
+ if (valid != nai.lastValidated) {
+ if (wasDefault) {
+ metricsLogger().defaultNetworkMetrics().logDefaultNetworkValidity(
+ SystemClock.elapsedRealtime(), valid);
}
- updateInetCondition(nai);
- // Let the NetworkAgent know the state of its network
- Bundle redirectUrlBundle = new Bundle();
- redirectUrlBundle.putString(NetworkAgent.REDIRECT_URL_KEY, (String)msg.obj);
- nai.asyncChannel.sendMessage(
- NetworkAgent.CMD_REPORT_NETWORK_STATUS,
- (valid ? NetworkAgent.VALID_NETWORK : NetworkAgent.INVALID_NETWORK),
- 0, redirectUrlBundle);
- if (wasValidated && !nai.lastValidated) {
- handleNetworkUnvalidated(nai);
- }
+ final int oldScore = nai.getCurrentScore();
+ nai.lastValidated = valid;
+ nai.everValidated |= valid;
+ updateCapabilities(oldScore, nai, nai.networkCapabilities);
+ // If score has changed, rebroadcast to NetworkFactories. b/17726566
+ if (oldScore != nai.getCurrentScore()) sendUpdatedScoreToFactories(nai);
+ }
+ updateInetCondition(nai);
+ // Let the NetworkAgent know the state of its network
+ Bundle redirectUrlBundle = new Bundle();
+ redirectUrlBundle.putString(NetworkAgent.REDIRECT_URL_KEY, redirectUrl);
+ nai.asyncChannel.sendMessage(
+ NetworkAgent.CMD_REPORT_NETWORK_STATUS,
+ (valid ? NetworkAgent.VALID_NETWORK : NetworkAgent.INVALID_NETWORK),
+ 0, redirectUrlBundle);
+ if (wasValidated && !nai.lastValidated) {
+ handleNetworkUnvalidated(nai);
}
break;
}
@@ -2191,6 +2211,21 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
break;
}
+ case NetworkMonitor.EVENT_PRIVATE_DNS_CONFIG_RESOLVED: {
+ final NetworkAgentInfo nai;
+ synchronized (mNetworkForNetId) {
+ nai = mNetworkForNetId.get(msg.arg2);
+ }
+ if (nai == null) break;
+
+ final PrivateDnsConfig cfg = (PrivateDnsConfig) msg.obj;
+ final boolean reevaluationRequired = updatePrivateDns(nai, cfg);
+ if (nai.lastValidated && reevaluationRequired) {
+ nai.networkMonitor.sendMessage(
+ NetworkMonitor.CMD_FORCE_REEVALUATION, Process.SYSTEM_UID);
+ }
+ break;
+ }
}
return true;
}
@@ -2226,6 +2261,63 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
}
+ private void handlePrivateDnsSettingsChanged() {
+ final PrivateDnsConfig cfg = mDnsManager.getPrivateDnsConfig();
+
+ for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
+ // Private DNS only ever applies to networks that might provide
+ // Internet access and therefore also require validation.
+ if (!NetworkMonitor.isValidationRequired(
+ mDefaultRequest.networkCapabilities, nai.networkCapabilities)) {
+ continue;
+ }
+
+ // Notify the NetworkMonitor thread in case it needs to cancel or
+ // schedule DNS resolutions. If a DNS resolution is required the
+ // result will be sent back to us.
+ nai.networkMonitor.notifyPrivateDnsSettingsChanged(cfg);
+
+ if (!cfg.inStrictMode()) {
+ // No strict mode hostname DNS resolution needed, so just update
+ // DNS settings directly. In opportunistic and "off" modes this
+ // just reprograms netd with the network-supplied DNS servers
+ // (and of course the boolean of whether or not to attempt TLS).
+ //
+ // TODO: Consider code flow parity with strict mode, i.e. having
+ // NetworkMonitor relay the PrivateDnsConfig back to us and then
+ // performing this call at that time.
+ updatePrivateDns(nai, cfg);
+ }
+ }
+ }
+
+ private boolean updatePrivateDns(NetworkAgentInfo nai, PrivateDnsConfig newCfg) {
+ final boolean reevaluationRequired = true;
+ final boolean dontReevaluate = false;
+
+ final PrivateDnsConfig oldCfg = mDnsManager.updatePrivateDns(nai.network, newCfg);
+ updateDnses(nai.linkProperties, null, nai.network.netId);
+
+ if (newCfg == null) {
+ if (oldCfg == null) return dontReevaluate;
+ return oldCfg.useTls ? reevaluationRequired : dontReevaluate;
+ }
+
+ if (oldCfg == null) {
+ return newCfg.useTls ? reevaluationRequired : dontReevaluate;
+ }
+
+ if (oldCfg.useTls != newCfg.useTls) {
+ return reevaluationRequired;
+ }
+
+ if (newCfg.inStrictMode() && !Objects.equals(oldCfg.hostname, newCfg.hostname)) {
+ return reevaluationRequired;
+ }
+
+ return dontReevaluate;
+ }
+
private void updateLingerState(NetworkAgentInfo nai, long now) {
// 1. Update the linger timer. If it's changed, reschedule or cancel the alarm.
// 2. If the network was lingering and there are now requests, unlinger it.
@@ -2360,6 +2452,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
} catch (Exception e) {
loge("Exception removing network: " + e);
}
+ mDnsManager.removeNetwork(nai.network);
}
synchronized (mNetworkForNetId) {
mNetIdInUse.delete(nai.network.netId);
@@ -2516,6 +2609,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
private void handleRemoveNetworkRequest(final NetworkRequestInfo nri) {
nri.unlinkDeathRecipient();
mNetworkRequests.remove(nri.request);
+
synchronized (mUidToNetworkRequestCount) {
int requests = mUidToNetworkRequestCount.get(nri.mUid, 0);
if (requests < 1) {
@@ -2528,6 +2622,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
mUidToNetworkRequestCount.put(nri.mUid, requests - 1);
}
}
+
mNetworkRequestInfoLogs.log("RELEASE " + nri);
if (nri.request.isRequest()) {
boolean wasKept = false;
@@ -2805,6 +2900,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
return ConnectivityManager.MULTIPATH_PREFERENCE_UNMETERED;
}
+ Integer networkPreference = mMultipathPolicyTracker.getMultipathPreference(network);
+ if (networkPreference != null) {
+ return networkPreference;
+ }
+
return mMultinetworkPolicyTracker.getMeteredMultipathPreference();
}
@@ -2898,12 +2998,16 @@ public class ConnectivityService extends IConnectivityManager.Stub
for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
nai.networkMonitor.systemReady = true;
}
+ mMultipathPolicyTracker.start();
break;
}
case EVENT_REVALIDATE_NETWORK: {
handleReportNetworkConnectivity((Network) msg.obj, msg.arg1, toBool(msg.arg2));
break;
}
+ case EVENT_PRIVATE_DNS_SETTINGS_CHANGED:
+ handlePrivateDnsSettingsChanged();
+ break;
}
}
}
@@ -3450,9 +3554,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
public boolean prepareVpn(@Nullable String oldPackage, @Nullable String newPackage,
int userId) {
enforceCrossUserPermission(userId);
- throwIfLockdownEnabled();
synchronized (mVpns) {
+ throwIfLockdownEnabled();
Vpn vpn = mVpns.get(userId);
if (vpn != null) {
return vpn.prepare(oldPackage, newPackage);
@@ -3496,9 +3600,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
*/
@Override
public ParcelFileDescriptor establishVpn(VpnConfig config) {
- throwIfLockdownEnabled();
int user = UserHandle.getUserId(Binder.getCallingUid());
synchronized (mVpns) {
+ throwIfLockdownEnabled();
return mVpns.get(user).establish(config);
}
}
@@ -3509,13 +3613,13 @@ public class ConnectivityService extends IConnectivityManager.Stub
*/
@Override
public void startLegacyVpn(VpnProfile profile) {
- throwIfLockdownEnabled();
+ int user = UserHandle.getUserId(Binder.getCallingUid());
final LinkProperties egress = getActiveLinkProperties();
if (egress == null) {
throw new IllegalStateException("Missing active network connection");
}
- int user = UserHandle.getUserId(Binder.getCallingUid());
synchronized (mVpns) {
+ throwIfLockdownEnabled();
mVpns.get(user).startLegacyVpn(profile, mKeyStore, egress);
}
}
@@ -3541,11 +3645,11 @@ public class ConnectivityService extends IConnectivityManager.Stub
@Override
public VpnInfo[] getAllVpnInfo() {
enforceConnectivityInternalPermission();
- if (mLockdownEnabled) {
- return new VpnInfo[0];
- }
-
synchronized (mVpns) {
+ if (mLockdownEnabled) {
+ return new VpnInfo[0];
+ }
+
List<VpnInfo> infoList = new ArrayList<>();
for (int i = 0; i < mVpns.size(); i++) {
VpnInfo info = createVpnInfo(mVpns.valueAt(i));
@@ -3610,33 +3714,33 @@ public class ConnectivityService extends IConnectivityManager.Stub
return false;
}
- // Tear down existing lockdown if profile was removed
- mLockdownEnabled = LockdownVpnTracker.isEnabled();
- if (mLockdownEnabled) {
- byte[] profileTag = mKeyStore.get(Credentials.LOCKDOWN_VPN);
- if (profileTag == null) {
- Slog.e(TAG, "Lockdown VPN configured but cannot be read from keystore");
- return false;
- }
- String profileName = new String(profileTag);
- final VpnProfile profile = VpnProfile.decode(
- profileName, mKeyStore.get(Credentials.VPN + profileName));
- if (profile == null) {
- Slog.e(TAG, "Lockdown VPN configured invalid profile " + profileName);
- setLockdownTracker(null);
- return true;
- }
- int user = UserHandle.getUserId(Binder.getCallingUid());
- synchronized (mVpns) {
+ synchronized (mVpns) {
+ // Tear down existing lockdown if profile was removed
+ mLockdownEnabled = LockdownVpnTracker.isEnabled();
+ if (mLockdownEnabled) {
+ byte[] profileTag = mKeyStore.get(Credentials.LOCKDOWN_VPN);
+ if (profileTag == null) {
+ Slog.e(TAG, "Lockdown VPN configured but cannot be read from keystore");
+ return false;
+ }
+ String profileName = new String(profileTag);
+ final VpnProfile profile = VpnProfile.decode(
+ profileName, mKeyStore.get(Credentials.VPN + profileName));
+ if (profile == null) {
+ Slog.e(TAG, "Lockdown VPN configured invalid profile " + profileName);
+ setLockdownTracker(null);
+ return true;
+ }
+ int user = UserHandle.getUserId(Binder.getCallingUid());
Vpn vpn = mVpns.get(user);
if (vpn == null) {
Slog.w(TAG, "VPN for user " + user + " not ready yet. Skipping lockdown");
return false;
}
setLockdownTracker(new LockdownVpnTracker(mContext, mNetd, this, vpn, profile));
+ } else {
+ setLockdownTracker(null);
}
- } else {
- setLockdownTracker(null);
}
return true;
@@ -3646,6 +3750,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
* Internally set new {@link LockdownVpnTracker}, shutting down any existing
* {@link LockdownVpnTracker}. Can be {@code null} to disable lockdown.
*/
+ @GuardedBy("mVpns")
private void setLockdownTracker(LockdownVpnTracker tracker) {
// Shutdown any existing tracker
final LockdownVpnTracker existing = mLockdownTracker;
@@ -3660,6 +3765,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
}
+ @GuardedBy("mVpns")
private void throwIfLockdownEnabled() {
if (mLockdownEnabled) {
throw new IllegalStateException("Unavailable in lockdown mode");
@@ -3707,12 +3813,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
enforceConnectivityInternalPermission();
enforceCrossUserPermission(userId);
- // Can't set always-on VPN if legacy VPN is already in lockdown mode.
- if (LockdownVpnTracker.isEnabled()) {
- return false;
- }
-
synchronized (mVpns) {
+ // Can't set always-on VPN if legacy VPN is already in lockdown mode.
+ if (LockdownVpnTracker.isEnabled()) {
+ return false;
+ }
+
Vpn vpn = mVpns.get(userId);
if (vpn == null) {
Slog.w(TAG, "User " + userId + " has no Vpn configuration");
@@ -3888,9 +3994,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
userVpn = new Vpn(mHandler.getLooper(), mContext, mNetd, userId);
mVpns.put(userId, userVpn);
- }
- if (mUserManager.getUserInfo(userId).isPrimary() && LockdownVpnTracker.isEnabled()) {
- updateLockdownVpn();
+ if (mUserManager.getUserInfo(userId).isPrimary() && LockdownVpnTracker.isEnabled()) {
+ updateLockdownVpn();
+ }
}
}
@@ -3927,11 +4033,13 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
private void onUserUnlocked(int userId) {
- // User present may be sent because of an unlock, which might mean an unlocked keystore.
- if (mUserManager.getUserInfo(userId).isPrimary() && LockdownVpnTracker.isEnabled()) {
- updateLockdownVpn();
- } else {
- startAlwaysOnVpn(userId);
+ synchronized (mVpns) {
+ // User present may be sent because of an unlock, which might mean an unlocked keystore.
+ if (mUserManager.getUserInfo(userId).isPrimary() && LockdownVpnTracker.isEnabled()) {
+ updateLockdownVpn();
+ } else {
+ startAlwaysOnVpn(userId);
+ }
}
}
@@ -4131,6 +4239,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
// the system default network.
if (type == NetworkRequest.Type.TRACK_DEFAULT) {
networkCapabilities = new NetworkCapabilities(mDefaultRequest.networkCapabilities);
+ networkCapabilities.removeCapability(NET_CAPABILITY_NOT_VPN);
enforceAccessPermission();
} else {
networkCapabilities = new NetworkCapabilities(networkCapabilities);
@@ -4141,6 +4250,13 @@ public class ConnectivityService extends IConnectivityManager.Stub
enforceMeteredApnPolicy(networkCapabilities);
}
ensureRequestableCapabilities(networkCapabilities);
+ // Set the UID range for this request to the single UID of the requester.
+ // This will overwrite any allowed UIDs in the requested capabilities. Though there
+ // are no visible methods to set the UIDs, an app could use reflection to try and get
+ // networks for other apps so it's essential that the UIDs are overwritten.
+ // TODO : don't forcefully set the UID when communicating with processes
+ // that have the NETWORK_SETTINGS permission.
+ networkCapabilities.setSingleUid(Binder.getCallingUid());
if (timeoutMs < 0) {
throw new IllegalArgumentException("Bad timeout specified");
@@ -4214,6 +4330,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
enforceMeteredApnPolicy(networkCapabilities);
ensureRequestableCapabilities(networkCapabilities);
ensureValidNetworkSpecifier(networkCapabilities);
+ // TODO : don't forcefully set the UID when communicating with processes
+ // that have the NETWORK_SETTINGS permission.
+ networkCapabilities.setSingleUid(Binder.getCallingUid());
NetworkRequest networkRequest = new NetworkRequest(networkCapabilities, TYPE_NONE,
nextNetworkRequestId(), NetworkRequest.Type.REQUEST);
@@ -4267,6 +4386,9 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
+ // TODO : don't forcefully set the UIDs when communicating with processes
+ // that have the NETWORK_SETTINGS permission.
+ nc.setSingleUid(Binder.getCallingUid());
if (!ConnectivityManager.checkChangePermission(mContext)) {
// Apps without the CHANGE_NETWORK_STATE permission can't use background networks, so
// make all their listens include NET_CAPABILITY_FOREGROUND. That way, they will get
@@ -4295,8 +4417,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
ensureValidNetworkSpecifier(networkCapabilities);
- NetworkRequest networkRequest = new NetworkRequest(
- new NetworkCapabilities(networkCapabilities), TYPE_NONE, nextNetworkRequestId(),
+ final NetworkCapabilities nc = new NetworkCapabilities(networkCapabilities);
+ // TODO : don't forcefully set the UIDs when communicating with processes
+ // that have the NETWORK_SETTINGS permission.
+ nc.setSingleUid(Binder.getCallingUid());
+
+ NetworkRequest networkRequest = new NetworkRequest(nc, TYPE_NONE, nextNetworkRequestId(),
NetworkRequest.Type.LISTEN);
NetworkRequestInfo nri = new NetworkRequestInfo(networkRequest, operation);
if (VDBG) log("pendingListenForNetwork for " + nri);
@@ -4439,6 +4565,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
NetworkInfo networkInfo = na.networkInfo;
na.networkInfo = null;
updateNetworkInfo(na, networkInfo);
+ updateUids(na, null, na.networkCapabilities);
}
private void updateLinkProperties(NetworkAgentInfo networkAgent, LinkProperties oldLp) {
@@ -4586,40 +4713,17 @@ public class ConnectivityService extends IConnectivityManager.Stub
return; // no updating necessary
}
- Collection<InetAddress> dnses = newLp.getDnsServers();
- if (DBG) log("Setting DNS servers for network " + netId + " to " + dnses);
- try {
- mNetd.setDnsConfigurationForNetwork(
- netId, NetworkUtils.makeStrings(dnses), newLp.getDomains());
- } catch (Exception e) {
- loge("Exception in setDnsConfigurationForNetwork: " + e);
- }
final NetworkAgentInfo defaultNai = getDefaultNetwork();
- if (defaultNai != null && defaultNai.network.netId == netId) {
- setDefaultDnsSystemProperties(dnses);
- }
- flushVmDnsCache();
- }
+ final boolean isDefaultNetwork = (defaultNai != null && defaultNai.network.netId == netId);
- private void setDefaultDnsSystemProperties(Collection<InetAddress> dnses) {
- int last = 0;
- for (InetAddress dns : dnses) {
- ++last;
- setNetDnsProperty(last, dns.getHostAddress());
- }
- for (int i = last + 1; i <= mNumDnsEntries; ++i) {
- setNetDnsProperty(i, "");
+ if (DBG) {
+ final Collection<InetAddress> dnses = newLp.getDnsServers();
+ log("Setting DNS servers for network " + netId + " to " + dnses);
}
- mNumDnsEntries = last;
- }
-
- private void setNetDnsProperty(int which, String value) {
- final String key = "net.dns" + which;
- // Log and forget errors setting unsupported properties.
try {
- mSystemProperties.set(key, value);
+ mDnsManager.setDnsConfigurationForNetwork(netId, newLp, isDefaultNetwork);
} catch (Exception e) {
- Log.e(TAG, "Error setting unsupported net.dns property: ", e);
+ loge("Exception in setDnsConfigurationForNetwork: " + e);
}
}
@@ -4635,51 +4739,67 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
/**
- * Update the NetworkCapabilities for {@code networkAgent} to {@code networkCapabilities}
- * augmented with any stateful capabilities implied from {@code networkAgent}
- * (e.g., validated status and captive portal status).
- *
- * @param oldScore score of the network before any of the changes that prompted us
- * to call this function.
- * @param nai the network having its capabilities updated.
- * @param networkCapabilities the new network capabilities.
+ * Augments the NetworkCapabilities passed in by a NetworkAgent with capabilities that are
+ * maintained here that the NetworkAgent is not aware of (e.g., validated, captive portal,
+ * and foreground status).
*/
- private void updateCapabilities(
- int oldScore, NetworkAgentInfo nai, NetworkCapabilities networkCapabilities) {
+ private NetworkCapabilities mixInCapabilities(NetworkAgentInfo nai, NetworkCapabilities nc) {
// Once a NetworkAgent is connected, complain if some immutable capabilities are removed.
- if (nai.everConnected && !nai.networkCapabilities.satisfiedByImmutableNetworkCapabilities(
- networkCapabilities)) {
- // TODO: consider not complaining when a network agent degrade its capabilities if this
+ if (nai.everConnected &&
+ !nai.networkCapabilities.satisfiedByImmutableNetworkCapabilities(nc)) {
+ // TODO: consider not complaining when a network agent degrades its capabilities if this
// does not cause any request (that is not a listen) currently matching that agent to
// stop being matched by the updated agent.
- String diff = nai.networkCapabilities.describeImmutableDifferences(networkCapabilities);
+ String diff = nai.networkCapabilities.describeImmutableDifferences(nc);
if (!TextUtils.isEmpty(diff)) {
Slog.wtf(TAG, "BUG: " + nai + " lost immutable capabilities:" + diff);
}
}
// Don't modify caller's NetworkCapabilities.
- networkCapabilities = new NetworkCapabilities(networkCapabilities);
+ NetworkCapabilities newNc = new NetworkCapabilities(nc);
if (nai.lastValidated) {
- networkCapabilities.addCapability(NET_CAPABILITY_VALIDATED);
+ newNc.addCapability(NET_CAPABILITY_VALIDATED);
} else {
- networkCapabilities.removeCapability(NET_CAPABILITY_VALIDATED);
+ newNc.removeCapability(NET_CAPABILITY_VALIDATED);
}
if (nai.lastCaptivePortalDetected) {
- networkCapabilities.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
+ newNc.addCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
} else {
- networkCapabilities.removeCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
+ newNc.removeCapability(NET_CAPABILITY_CAPTIVE_PORTAL);
}
if (nai.isBackgroundNetwork()) {
- networkCapabilities.removeCapability(NET_CAPABILITY_FOREGROUND);
+ newNc.removeCapability(NET_CAPABILITY_FOREGROUND);
} else {
- networkCapabilities.addCapability(NET_CAPABILITY_FOREGROUND);
+ newNc.addCapability(NET_CAPABILITY_FOREGROUND);
}
- if (Objects.equals(nai.networkCapabilities, networkCapabilities)) return;
+ return newNc;
+ }
+
+ /**
+ * Update the NetworkCapabilities for {@code nai} to {@code nc}. Specifically:
+ *
+ * 1. Calls mixInCapabilities to merge the passed-in NetworkCapabilities {@code nc} with the
+ * capabilities we manage and store in {@code nai}, such as validated status and captive
+ * portal status)
+ * 2. Takes action on the result: changes network permissions, sends CAP_CHANGED callbacks, and
+ * potentially triggers rematches.
+ * 3. Directly informs other network stack components (NetworkStatsService, VPNs, etc. of the
+ * change.)
+ *
+ * @param oldScore score of the network before any of the changes that prompted us
+ * to call this function.
+ * @param nai the network having its capabilities updated.
+ * @param nc the new network capabilities.
+ */
+ private void updateCapabilities(int oldScore, NetworkAgentInfo nai, NetworkCapabilities nc) {
+ NetworkCapabilities newNc = mixInCapabilities(nai, nc);
+
+ if (Objects.equals(nai.networkCapabilities, newNc)) return;
final String oldPermission = getNetworkPermission(nai.networkCapabilities);
- final String newPermission = getNetworkPermission(networkCapabilities);
+ final String newPermission = getNetworkPermission(newNc);
if (!Objects.equals(oldPermission, newPermission) && nai.created && !nai.isVPN()) {
try {
mNetd.setNetworkPermission(nai.network.netId, newPermission);
@@ -4691,11 +4811,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
final NetworkCapabilities prevNc;
synchronized (nai) {
prevNc = nai.networkCapabilities;
- nai.networkCapabilities = networkCapabilities;
+ nai.networkCapabilities = newNc;
}
- if (nai.getCurrentScore() == oldScore &&
- networkCapabilities.equalRequestableCapabilities(prevNc)) {
+ updateUids(nai, prevNc, newNc);
+
+ if (nai.getCurrentScore() == oldScore && newNc.equalRequestableCapabilities(prevNc)) {
// If the requestable capabilities haven't changed, and the score hasn't changed, then
// the change we're processing can't affect any requests, it can only affect the listens
// on this network. We might have been called by rematchNetworkAndRequests when a
@@ -4711,15 +4832,15 @@ public class ConnectivityService extends IConnectivityManager.Stub
// Report changes that are interesting for network statistics tracking.
if (prevNc != null) {
final boolean meteredChanged = prevNc.hasCapability(NET_CAPABILITY_NOT_METERED) !=
- networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED);
+ newNc.hasCapability(NET_CAPABILITY_NOT_METERED);
final boolean roamingChanged = prevNc.hasCapability(NET_CAPABILITY_NOT_ROAMING) !=
- networkCapabilities.hasCapability(NET_CAPABILITY_NOT_ROAMING);
+ newNc.hasCapability(NET_CAPABILITY_NOT_ROAMING);
if (meteredChanged || roamingChanged) {
notifyIfacesChangedForNetworkStats();
}
}
- if (!networkCapabilities.hasTransport(TRANSPORT_VPN)) {
+ if (!newNc.hasTransport(TRANSPORT_VPN)) {
// Tell VPNs about updated capabilities, since they may need to
// bubble those changes through.
synchronized (mVpns) {
@@ -4731,6 +4852,34 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
}
+ private void updateUids(NetworkAgentInfo nai, NetworkCapabilities prevNc,
+ NetworkCapabilities newNc) {
+ Set<UidRange> prevRanges = null == prevNc ? null : prevNc.getUids();
+ Set<UidRange> newRanges = null == newNc ? null : newNc.getUids();
+ if (null == prevRanges) prevRanges = new ArraySet<>();
+ if (null == newRanges) newRanges = new ArraySet<>();
+ final Set<UidRange> prevRangesCopy = new ArraySet<>(prevRanges);
+
+ prevRanges.removeAll(newRanges);
+ newRanges.removeAll(prevRangesCopy);
+
+ try {
+ if (!newRanges.isEmpty()) {
+ final UidRange[] addedRangesArray = new UidRange[newRanges.size()];
+ newRanges.toArray(addedRangesArray);
+ mNetd.addVpnUidRanges(nai.network.netId, addedRangesArray);
+ }
+ if (!prevRanges.isEmpty()) {
+ final UidRange[] removedRangesArray = new UidRange[prevRanges.size()];
+ prevRanges.toArray(removedRangesArray);
+ mNetd.removeVpnUidRanges(nai.network.netId, removedRangesArray);
+ }
+ } catch (Exception e) {
+ // Never crash!
+ loge("Exception in updateUids: " + e);
+ }
+ }
+
public void handleUpdateLinkProperties(NetworkAgentInfo nai, LinkProperties newLp) {
if (mNetworkForNetId.get(nai.network.netId) != nai) {
// Ignore updates for disconnected networks
@@ -4822,7 +4971,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
break;
}
case ConnectivityManager.CALLBACK_CAP_CHANGED: {
- putParcelable(bundle, new NetworkCapabilities(networkAgent.networkCapabilities));
+ final NetworkCapabilities nc =
+ new NetworkCapabilities(networkAgent.networkCapabilities);
+ // TODO : don't remove the UIDs when communicating with processes
+ // that have the NETWORK_SETTINGS permission.
+ nc.setSingleUid(nri.mUid);
+ putParcelable(bundle, nc);
break;
}
case ConnectivityManager.CALLBACK_IP_CHANGED: {
@@ -4890,10 +5044,12 @@ public class ConnectivityService extends IConnectivityManager.Stub
} catch (Exception e) {
loge("Exception setting default network :" + e);
}
+
notifyLockdownVpn(newNetwork);
handleApplyDefaultProxy(newNetwork.linkProperties.getHttpProxy());
updateTcpBufferSizes(newNetwork);
- setDefaultDnsSystemProperties(newNetwork.linkProperties.getDnsServers());
+ mDnsManager.setDefaultDnsSystemProperties(newNetwork.linkProperties.getDnsServers());
+ notifyIfacesChangedForNetworkStats();
}
private void processListenRequests(NetworkAgentInfo nai, boolean capabilitiesChanged) {
@@ -5243,11 +5399,13 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
private void notifyLockdownVpn(NetworkAgentInfo nai) {
- if (mLockdownTracker != null) {
- if (nai != null && nai.isVPN()) {
- mLockdownTracker.onVpnStateChanged(nai.networkInfo);
- } else {
- mLockdownTracker.onNetworkInfoChanged();
+ synchronized (mVpns) {
+ if (mLockdownTracker != null) {
+ if (nai != null && nai.isVPN()) {
+ mLockdownTracker.onVpnStateChanged(nai.networkInfo);
+ } else {
+ mLockdownTracker.onNetworkInfoChanged();
+ }
}
}
}
@@ -5342,6 +5500,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
}
}
+ updateUids(networkAgent, networkAgent.networkCapabilities, null);
}
} else if ((oldInfo != null && oldInfo.getState() == NetworkInfo.State.SUSPENDED) ||
state == NetworkInfo.State.SUSPENDED) {
@@ -5465,44 +5624,62 @@ public class ConnectivityService extends IConnectivityManager.Stub
}
/**
+ * Returns the list of all interfaces that could be used by network traffic that does not
+ * explicitly specify a network. This includes the default network, but also all VPNs that are
+ * currently connected.
+ *
+ * Must be called on the handler thread.
+ */
+ private Network[] getDefaultNetworks() {
+ ArrayList<Network> defaultNetworks = new ArrayList<>();
+ NetworkAgentInfo defaultNetwork = getDefaultNetwork();
+ for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
+ if (nai.everConnected && (nai == defaultNetwork || nai.isVPN())) {
+ defaultNetworks.add(nai.network);
+ }
+ }
+ return defaultNetworks.toArray(new Network[0]);
+ }
+
+ /**
* Notify NetworkStatsService that the set of active ifaces has changed, or that one of the
* properties tracked by NetworkStatsService on an active iface has changed.
*/
private void notifyIfacesChangedForNetworkStats() {
try {
- mStatsService.forceUpdateIfaces();
+ mStatsService.forceUpdateIfaces(getDefaultNetworks());
} catch (Exception ignored) {
}
}
@Override
public boolean addVpnAddress(String address, int prefixLength) {
- throwIfLockdownEnabled();
int user = UserHandle.getUserId(Binder.getCallingUid());
synchronized (mVpns) {
+ throwIfLockdownEnabled();
return mVpns.get(user).addAddress(address, prefixLength);
}
}
@Override
public boolean removeVpnAddress(String address, int prefixLength) {
- throwIfLockdownEnabled();
int user = UserHandle.getUserId(Binder.getCallingUid());
synchronized (mVpns) {
+ throwIfLockdownEnabled();
return mVpns.get(user).removeAddress(address, prefixLength);
}
}
@Override
public boolean setUnderlyingNetworksForVpn(Network[] networks) {
- throwIfLockdownEnabled();
int user = UserHandle.getUserId(Binder.getCallingUid());
- boolean success;
+ final boolean success;
synchronized (mVpns) {
+ throwIfLockdownEnabled();
success = mVpns.get(user).setUnderlyingNetworks(networks);
}
if (success) {
- notifyIfacesChangedForNetworkStats();
+ mHandler.post(() -> notifyIfacesChangedForNetworkStats());
}
return success;
}
@@ -5558,31 +5735,31 @@ public class ConnectivityService extends IConnectivityManager.Stub
setAlwaysOnVpnPackage(userId, null, false);
setVpnPackageAuthorization(alwaysOnPackage, userId, false);
}
- }
- // Turn Always-on VPN off
- if (mLockdownEnabled && userId == UserHandle.USER_SYSTEM) {
- final long ident = Binder.clearCallingIdentity();
- try {
- mKeyStore.delete(Credentials.LOCKDOWN_VPN);
- mLockdownEnabled = false;
- setLockdownTracker(null);
- } finally {
- Binder.restoreCallingIdentity(ident);
+ // Turn Always-on VPN off
+ if (mLockdownEnabled && userId == UserHandle.USER_SYSTEM) {
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mKeyStore.delete(Credentials.LOCKDOWN_VPN);
+ mLockdownEnabled = false;
+ setLockdownTracker(null);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
}
- }
- // Turn VPN off
- VpnConfig vpnConfig = getVpnConfig(userId);
- if (vpnConfig != null) {
- if (vpnConfig.legacy) {
- prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN, userId);
- } else {
- // Prevent this app (packagename = vpnConfig.user) from initiating VPN connections
- // in the future without user intervention.
- setVpnPackageAuthorization(vpnConfig.user, userId, false);
+ // Turn VPN off
+ VpnConfig vpnConfig = getVpnConfig(userId);
+ if (vpnConfig != null) {
+ if (vpnConfig.legacy) {
+ prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN, userId);
+ } else {
+ // Prevent this app (packagename = vpnConfig.user) from initiating
+ // VPN connections in the future without user intervention.
+ setVpnPackageAuthorization(vpnConfig.user, userId, false);
- prepareVpn(null, VpnConfig.LEGACY_VPN, userId);
+ prepareVpn(null, VpnConfig.LEGACY_VPN, userId);
+ }
}
}
}
@@ -5591,6 +5768,17 @@ public class ConnectivityService extends IConnectivityManager.Stub
Settings.Global.NETWORK_AVOID_BAD_WIFI, null);
}
+ @Override
+ public byte[] getNetworkWatchlistConfigHash() {
+ NetworkWatchlistManager nwm = mContext.getSystemService(NetworkWatchlistManager.class);
+ if (nwm == null) {
+ loge("Unable to get NetworkWatchlistManager");
+ return null;
+ }
+ // Redirect it to network watchlist service to access watchlist file and calculate hash.
+ return nwm.getWatchlistConfigHash();
+ }
+
@VisibleForTesting
public NetworkMonitor createNetworkMonitor(Context context, Handler handler,
NetworkAgentInfo nai, NetworkRequest defaultRequest) {
diff --git a/com/android/server/DeviceIdleController.java b/com/android/server/DeviceIdleController.java
index 6f697c46..a12c85ae 100644
--- a/com/android/server/DeviceIdleController.java
+++ b/com/android/server/DeviceIdleController.java
@@ -795,64 +795,65 @@ public class DeviceIdleController extends SystemService
Slog.e(TAG, "Bad device idle settings", e);
}
- LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getLong(
+ LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getDurationMillis(
KEY_LIGHT_IDLE_AFTER_INACTIVE_TIMEOUT,
!COMPRESS_TIME ? 5 * 60 * 1000L : 15 * 1000L);
- LIGHT_PRE_IDLE_TIMEOUT = mParser.getLong(KEY_LIGHT_PRE_IDLE_TIMEOUT,
+ LIGHT_PRE_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_LIGHT_PRE_IDLE_TIMEOUT,
!COMPRESS_TIME ? 10 * 60 * 1000L : 30 * 1000L);
- LIGHT_IDLE_TIMEOUT = mParser.getLong(KEY_LIGHT_IDLE_TIMEOUT,
+ LIGHT_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_LIGHT_IDLE_TIMEOUT,
!COMPRESS_TIME ? 5 * 60 * 1000L : 15 * 1000L);
LIGHT_IDLE_FACTOR = mParser.getFloat(KEY_LIGHT_IDLE_FACTOR,
2f);
- LIGHT_MAX_IDLE_TIMEOUT = mParser.getLong(KEY_LIGHT_MAX_IDLE_TIMEOUT,
+ LIGHT_MAX_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_LIGHT_MAX_IDLE_TIMEOUT,
!COMPRESS_TIME ? 15 * 60 * 1000L : 60 * 1000L);
- LIGHT_IDLE_MAINTENANCE_MIN_BUDGET = mParser.getLong(
+ LIGHT_IDLE_MAINTENANCE_MIN_BUDGET = mParser.getDurationMillis(
KEY_LIGHT_IDLE_MAINTENANCE_MIN_BUDGET,
!COMPRESS_TIME ? 1 * 60 * 1000L : 15 * 1000L);
- LIGHT_IDLE_MAINTENANCE_MAX_BUDGET = mParser.getLong(
+ LIGHT_IDLE_MAINTENANCE_MAX_BUDGET = mParser.getDurationMillis(
KEY_LIGHT_IDLE_MAINTENANCE_MAX_BUDGET,
!COMPRESS_TIME ? 5 * 60 * 1000L : 30 * 1000L);
- MIN_LIGHT_MAINTENANCE_TIME = mParser.getLong(
+ MIN_LIGHT_MAINTENANCE_TIME = mParser.getDurationMillis(
KEY_MIN_LIGHT_MAINTENANCE_TIME,
!COMPRESS_TIME ? 5 * 1000L : 1 * 1000L);
- MIN_DEEP_MAINTENANCE_TIME = mParser.getLong(
+ MIN_DEEP_MAINTENANCE_TIME = mParser.getDurationMillis(
KEY_MIN_DEEP_MAINTENANCE_TIME,
!COMPRESS_TIME ? 30 * 1000L : 5 * 1000L);
long inactiveTimeoutDefault = (mSmallBatteryDevice ? 15 : 30) * 60 * 1000L;
- INACTIVE_TIMEOUT = mParser.getLong(KEY_INACTIVE_TIMEOUT,
+ INACTIVE_TIMEOUT = mParser.getDurationMillis(KEY_INACTIVE_TIMEOUT,
!COMPRESS_TIME ? inactiveTimeoutDefault : (inactiveTimeoutDefault / 10));
- SENSING_TIMEOUT = mParser.getLong(KEY_SENSING_TIMEOUT,
+ SENSING_TIMEOUT = mParser.getDurationMillis(KEY_SENSING_TIMEOUT,
!DEBUG ? 4 * 60 * 1000L : 60 * 1000L);
- LOCATING_TIMEOUT = mParser.getLong(KEY_LOCATING_TIMEOUT,
+ LOCATING_TIMEOUT = mParser.getDurationMillis(KEY_LOCATING_TIMEOUT,
!DEBUG ? 30 * 1000L : 15 * 1000L);
LOCATION_ACCURACY = mParser.getFloat(KEY_LOCATION_ACCURACY, 20);
- MOTION_INACTIVE_TIMEOUT = mParser.getLong(KEY_MOTION_INACTIVE_TIMEOUT,
+ MOTION_INACTIVE_TIMEOUT = mParser.getDurationMillis(KEY_MOTION_INACTIVE_TIMEOUT,
!COMPRESS_TIME ? 10 * 60 * 1000L : 60 * 1000L);
long idleAfterInactiveTimeout = (mSmallBatteryDevice ? 15 : 30) * 60 * 1000L;
- IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getLong(KEY_IDLE_AFTER_INACTIVE_TIMEOUT,
+ IDLE_AFTER_INACTIVE_TIMEOUT = mParser.getDurationMillis(
+ KEY_IDLE_AFTER_INACTIVE_TIMEOUT,
!COMPRESS_TIME ? idleAfterInactiveTimeout
: (idleAfterInactiveTimeout / 10));
- IDLE_PENDING_TIMEOUT = mParser.getLong(KEY_IDLE_PENDING_TIMEOUT,
+ IDLE_PENDING_TIMEOUT = mParser.getDurationMillis(KEY_IDLE_PENDING_TIMEOUT,
!COMPRESS_TIME ? 5 * 60 * 1000L : 30 * 1000L);
- MAX_IDLE_PENDING_TIMEOUT = mParser.getLong(KEY_MAX_IDLE_PENDING_TIMEOUT,
+ MAX_IDLE_PENDING_TIMEOUT = mParser.getDurationMillis(KEY_MAX_IDLE_PENDING_TIMEOUT,
!COMPRESS_TIME ? 10 * 60 * 1000L : 60 * 1000L);
IDLE_PENDING_FACTOR = mParser.getFloat(KEY_IDLE_PENDING_FACTOR,
2f);
- IDLE_TIMEOUT = mParser.getLong(KEY_IDLE_TIMEOUT,
+ IDLE_TIMEOUT = mParser.getDurationMillis(KEY_IDLE_TIMEOUT,
!COMPRESS_TIME ? 60 * 60 * 1000L : 6 * 60 * 1000L);
- MAX_IDLE_TIMEOUT = mParser.getLong(KEY_MAX_IDLE_TIMEOUT,
+ MAX_IDLE_TIMEOUT = mParser.getDurationMillis(KEY_MAX_IDLE_TIMEOUT,
!COMPRESS_TIME ? 6 * 60 * 60 * 1000L : 30 * 60 * 1000L);
IDLE_FACTOR = mParser.getFloat(KEY_IDLE_FACTOR,
2f);
- MIN_TIME_TO_ALARM = mParser.getLong(KEY_MIN_TIME_TO_ALARM,
+ MIN_TIME_TO_ALARM = mParser.getDurationMillis(KEY_MIN_TIME_TO_ALARM,
!COMPRESS_TIME ? 60 * 60 * 1000L : 6 * 60 * 1000L);
- MAX_TEMP_APP_WHITELIST_DURATION = mParser.getLong(
+ MAX_TEMP_APP_WHITELIST_DURATION = mParser.getDurationMillis(
KEY_MAX_TEMP_APP_WHITELIST_DURATION, 5 * 60 * 1000L);
- MMS_TEMP_APP_WHITELIST_DURATION = mParser.getLong(
+ MMS_TEMP_APP_WHITELIST_DURATION = mParser.getDurationMillis(
KEY_MMS_TEMP_APP_WHITELIST_DURATION, 60 * 1000L);
- SMS_TEMP_APP_WHITELIST_DURATION = mParser.getLong(
+ SMS_TEMP_APP_WHITELIST_DURATION = mParser.getDurationMillis(
KEY_SMS_TEMP_APP_WHITELIST_DURATION, 20 * 1000L);
- NOTIFICATION_WHITELIST_DURATION = mParser.getLong(
+ NOTIFICATION_WHITELIST_DURATION = mParser.getDurationMillis(
KEY_NOTIFICATION_WHITELIST_DURATION, 30 * 1000L);
}
}
@@ -1590,6 +1591,8 @@ public class DeviceIdleController extends SystemService
mPowerSaveWhitelistExceptIdleAppIdArray = buildAppIdArray(
mPowerSaveWhitelistAppsExceptIdle, mPowerSaveWhitelistUserApps,
mPowerSaveWhitelistExceptIdleAppIds);
+
+ passWhiteListToForceAppStandbyTrackerLocked();
}
return true;
} catch (PackageManager.NameNotFoundException e) {
@@ -1607,6 +1610,8 @@ public class DeviceIdleController extends SystemService
mPowerSaveWhitelistAppsExceptIdle, mPowerSaveWhitelistUserApps,
mPowerSaveWhitelistExceptIdleAppIds);
mPowerSaveWhitelistUserAppsExceptIdle.clear();
+
+ passWhiteListToForceAppStandbyTrackerLocked();
}
}
}
@@ -2571,7 +2576,7 @@ public class DeviceIdleController extends SystemService
private void passWhiteListToForceAppStandbyTrackerLocked() {
ForceAppStandbyTracker.getInstance(getContext()).setPowerSaveWhitelistAppIds(
- mPowerSaveWhitelistAllAppIdArray,
+ mPowerSaveWhitelistExceptIdleAppIdArray,
mTempWhitelistAppIdArray);
}
diff --git a/com/android/server/DiskStatsService.java b/com/android/server/DiskStatsService.java
index 2d2c6b0b..e884de00 100644
--- a/com/android/server/DiskStatsService.java
+++ b/com/android/server/DiskStatsService.java
@@ -19,6 +19,10 @@ package com.android.server;
import android.content.Context;
import android.os.Binder;
import android.os.Environment;
+import android.os.IBinder;
+import android.os.IStoraged;
+import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.StatFs;
import android.os.SystemClock;
import android.os.storage.StorageManager;
@@ -109,6 +113,12 @@ public class DiskStatsService extends Binder {
}
}
+ if (protoFormat) {
+ reportDiskWriteSpeedProto(proto);
+ } else {
+ reportDiskWriteSpeed(pw);
+ }
+
reportFreeSpace(Environment.getDataDirectory(), "Data", pw, proto,
DiskStatsFreeSpaceProto.FOLDER_DATA);
reportFreeSpace(Environment.getDownloadCacheDirectory(), "Cache", pw, proto,
@@ -285,4 +295,41 @@ public class DiskStatsService extends Binder {
Log.w(TAG, "exception reading diskstats cache file", e);
}
}
+
+ private int getRecentPerf() throws RemoteException, IllegalStateException {
+ IBinder binder = ServiceManager.getService("storaged");
+ if (binder == null) throw new IllegalStateException("storaged not found");
+ IStoraged storaged = IStoraged.Stub.asInterface(binder);
+ return storaged.getRecentPerf();
+ }
+
+ // Keep reportDiskWriteSpeed and reportDiskWriteSpeedProto in sync
+ private void reportDiskWriteSpeed(PrintWriter pw) {
+ try {
+ long perf = getRecentPerf();
+ if (perf != 0) {
+ pw.print("Recent Disk Write Speed (kB/s) = ");
+ pw.println(perf);
+ } else {
+ pw.println("Recent Disk Write Speed data unavailable");
+ Log.w(TAG, "Recent Disk Write Speed data unavailable!");
+ }
+ } catch (RemoteException | IllegalStateException e) {
+ pw.println(e.toString());
+ Log.e(TAG, e.toString());
+ }
+ }
+
+ private void reportDiskWriteSpeedProto(ProtoOutputStream proto) {
+ try {
+ long perf = getRecentPerf();
+ if (perf != 0) {
+ proto.write(DiskStatsServiceDumpProto.BENCHMARKED_WRITE_SPEED_KBPS, perf);
+ } else {
+ Log.w(TAG, "Recent Disk Write Speed data unavailable!");
+ }
+ } catch (RemoteException | IllegalStateException e) {
+ Log.e(TAG, e.toString());
+ }
+ }
}
diff --git a/com/android/server/EntropyMixer.java b/com/android/server/EntropyMixer.java
index 98777179..5e6e9d34 100644
--- a/com/android/server/EntropyMixer.java
+++ b/com/android/server/EntropyMixer.java
@@ -196,11 +196,14 @@ public class EntropyMixer extends Binder {
* Mixes in the output from HW RNG (if present) into the Linux RNG.
*/
private void addHwRandomEntropy() {
+ if (!new File(hwRandomDevice).exists()) {
+ // HW RNG not present/exposed -- ignore
+ return;
+ }
+
try {
RandomBlock.fromFile(hwRandomDevice).toFile(randomDevice, false);
Slog.i(TAG, "Added HW RNG output to entropy pool");
- } catch (FileNotFoundException ignored) {
- // HW RNG not present/exposed -- ignore
} catch (IOException e) {
Slog.w(TAG, "Failed to add HW RNG output to entropy pool", e);
}
diff --git a/com/android/server/ForceAppStandbyTracker.java b/com/android/server/ForceAppStandbyTracker.java
index 61d3833d..257845e3 100644
--- a/com/android/server/ForceAppStandbyTracker.java
+++ b/com/android/server/ForceAppStandbyTracker.java
@@ -17,6 +17,7 @@ package com.android.server;
import android.annotation.NonNull;
import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
import android.app.AppOpsManager.PackageOps;
import android.app.IActivityManager;
@@ -25,6 +26,9 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.BatteryManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -33,8 +37,10 @@ import android.os.PowerManagerInternal;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.UserHandle;
+import android.provider.Settings;
import android.util.ArraySet;
import android.util.Pair;
+import android.util.Slog;
import android.util.SparseBooleanArray;
import android.util.proto.ProtoOutputStream;
@@ -59,15 +65,16 @@ import java.util.List;
* - Global "force all apps standby" mode enforced by battery saver.
*
* TODO: In general, we can reduce the number of callbacks by checking all signals before sending
- * each callback. For example, even when an UID comes into the foreground, if it wasn't
- * originally restricted, then there's no need to send an event.
- * Doing this would be error-prone, so we punt it for now, but we should revisit it later.
+ * each callback. For example, even when an UID comes into the foreground, if it wasn't
+ * originally restricted, then there's no need to send an event.
+ * Doing this would be error-prone, so we punt it for now, but we should revisit it later.
*
* Test:
- atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
+ * atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/ForceAppStandbyTrackerTest.java
*/
public class ForceAppStandbyTracker {
private static final String TAG = "ForceAppStandbyTracker";
+ private static final boolean DEBUG = false;
@GuardedBy("ForceAppStandbyTracker.class")
private static ForceAppStandbyTracker sInstance;
@@ -85,6 +92,9 @@ public class ForceAppStandbyTracker {
private final MyHandler mHandler;
+ @VisibleForTesting
+ FeatureFlagsObserver mFlagsObserver;
+
/**
* Pair of (uid (not user-id), packageName) with OP_RUN_ANY_IN_BACKGROUND *not* allowed.
*/
@@ -94,6 +104,9 @@ public class ForceAppStandbyTracker {
@GuardedBy("mLock")
final SparseBooleanArray mForegroundUids = new SparseBooleanArray();
+ /**
+ * System except-idle + user whitelist in the device idle controller.
+ */
@GuardedBy("mLock")
private int[] mPowerWhitelistedAllAppIds = new int[0];
@@ -106,9 +119,93 @@ public class ForceAppStandbyTracker {
@GuardedBy("mLock")
boolean mStarted;
+ /**
+ * Only used for small battery use-case.
+ */
+ @GuardedBy("mLock")
+ boolean mIsPluggedIn;
+
+ @GuardedBy("mLock")
+ boolean mBatterySaverEnabled;
+
+ /**
+ * True if the forced app standby is currently enabled
+ */
@GuardedBy("mLock")
boolean mForceAllAppsStandby;
+ /**
+ * True if the forced app standby for small battery devices feature is enabled in settings
+ */
+ @GuardedBy("mLock")
+ boolean mForceAllAppStandbyForSmallBattery;
+
+ /**
+ * True if the forced app standby feature is enabled in settings
+ */
+ @GuardedBy("mLock")
+ boolean mForcedAppStandbyEnabled;
+
+ @VisibleForTesting
+ class FeatureFlagsObserver extends ContentObserver {
+ FeatureFlagsObserver() {
+ super(null);
+ }
+
+ void register() {
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.FORCED_APP_STANDBY_ENABLED),
+ false, this);
+
+ mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
+ Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED), false, this);
+ }
+
+ boolean isForcedAppStandbyEnabled() {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.FORCED_APP_STANDBY_ENABLED, 1) == 1;
+ }
+
+ boolean isForcedAppStandbyForSmallBatteryEnabled() {
+ return Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED, 0) == 1;
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ if (Settings.Global.getUriFor(Settings.Global.FORCED_APP_STANDBY_ENABLED).equals(uri)) {
+ final boolean enabled = isForcedAppStandbyEnabled();
+ synchronized (mLock) {
+ if (mForcedAppStandbyEnabled == enabled) {
+ return;
+ }
+ mForcedAppStandbyEnabled = enabled;
+ if (DEBUG) {
+ Slog.d(TAG,"Forced app standby feature flag changed: "
+ + mForcedAppStandbyEnabled);
+ }
+ }
+ mHandler.notifyForcedAppStandbyFeatureFlagChanged();
+ } else if (Settings.Global.getUriFor(
+ Settings.Global.FORCED_APP_STANDBY_FOR_SMALL_BATTERY_ENABLED).equals(uri)) {
+ final boolean enabled = isForcedAppStandbyForSmallBatteryEnabled();
+ synchronized (mLock) {
+ if (mForceAllAppStandbyForSmallBattery == enabled) {
+ return;
+ }
+ mForceAllAppStandbyForSmallBattery = enabled;
+ if (DEBUG) {
+ Slog.d(TAG, "Forced app standby for small battery feature flag changed: "
+ + mForceAllAppStandbyForSmallBattery);
+ }
+ updateForceAllAppStandbyState();
+ }
+ } else {
+ Slog.w(TAG, "Unexpected feature flag uri encountered: " + uri);
+ }
+ }
+ }
+
public static abstract class Listener {
/**
* This is called when the OP_RUN_ANY_IN_BACKGROUND appops changed for a package.
@@ -117,8 +214,11 @@ public class ForceAppStandbyTracker {
int uid, @NonNull String packageName) {
updateJobsForUidPackage(uid, packageName);
- if (!sender.areAlarmsRestricted(uid, packageName)) {
+ if (!sender.areAlarmsRestricted(uid, packageName, /*allowWhileIdle=*/ false)) {
unblockAlarmsForUidPackage(uid, packageName);
+ } else if (!sender.areAlarmsRestricted(uid, packageName, /*allowWhileIdle=*/ true)){
+ // we need to deliver the allow-while-idle alarms for this uid, package
+ unblockAllUnrestrictedAlarms();
}
}
@@ -246,6 +346,11 @@ public class ForceAppStandbyTracker {
mAppOpsManager = Preconditions.checkNotNull(injectAppOpsManager());
mAppOpsService = Preconditions.checkNotNull(injectIAppOpsService());
mPowerManagerInternal = Preconditions.checkNotNull(injectPowerManagerInternal());
+ mFlagsObserver = new FeatureFlagsObserver();
+ mFlagsObserver.register();
+ mForcedAppStandbyEnabled = mFlagsObserver.isForcedAppStandbyEnabled();
+ mForceAllAppStandbyForSmallBattery =
+ mFlagsObserver.isForcedAppStandbyForSmallBatteryEnabled();
try {
mIActivityManager.registerUidObserver(new UidObserver(),
@@ -260,16 +365,24 @@ public class ForceAppStandbyTracker {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_USER_REMOVED);
+ filter.addAction(Intent.ACTION_BATTERY_CHANGED);
mContext.registerReceiver(new MyReceiver(), filter);
refreshForcedAppStandbyUidPackagesLocked();
mPowerManagerInternal.registerLowPowerModeObserver(
ServiceType.FORCE_ALL_APPS_STANDBY,
- (state) -> updateForceAllAppsStandby(state.batterySaverEnabled));
+ (state) -> {
+ synchronized (mLock) {
+ mBatterySaverEnabled = state.batterySaverEnabled;
+ updateForceAllAppStandbyState();
+ }
+ });
+
+ mBatterySaverEnabled = mPowerManagerInternal.getLowPowerState(
+ ServiceType.FORCE_ALL_APPS_STANDBY).batterySaverEnabled;
- updateForceAllAppsStandby(mPowerManagerInternal.getLowPowerState(
- ServiceType.FORCE_ALL_APPS_STANDBY).batterySaverEnabled);
+ updateForceAllAppStandbyState();
}
}
@@ -294,6 +407,11 @@ public class ForceAppStandbyTracker {
return LocalServices.getService(PowerManagerInternal.class);
}
+ @VisibleForTesting
+ boolean isSmallBatteryDevice() {
+ return ActivityManager.isSmallBatteryDevice();
+ }
+
/**
* Update {@link #mRunAnyRestrictedPackages} with the current app ops state.
*/
@@ -323,18 +441,26 @@ public class ForceAppStandbyTracker {
}
}
- /**
- * Update {@link #mForceAllAppsStandby} and notifies the listeners.
- */
- void updateForceAllAppsStandby(boolean enable) {
+ private void updateForceAllAppStandbyState() {
synchronized (mLock) {
- if (enable == mForceAllAppsStandby) {
- return;
+ if (mForceAllAppStandbyForSmallBattery && isSmallBatteryDevice()) {
+ toggleForceAllAppsStandbyLocked(!mIsPluggedIn);
+ } else {
+ toggleForceAllAppsStandbyLocked(mBatterySaverEnabled);
}
- mForceAllAppsStandby = enable;
+ }
+ }
- mHandler.notifyForceAllAppsStandbyChanged();
+ /**
+ * Update {@link #mForceAllAppsStandby} and notifies the listeners.
+ */
+ private void toggleForceAllAppsStandbyLocked(boolean enable) {
+ if (enable == mForceAllAppsStandby) {
+ return;
}
+ mForceAllAppsStandby = enable;
+
+ mHandler.notifyForceAllAppsStandbyChanged();
}
private int findForcedAppStandbyUidPackageIndexLocked(int uid, @NonNull String packageName) {
@@ -364,7 +490,7 @@ public class ForceAppStandbyTracker {
*/
boolean updateForcedAppStandbyUidPackageLocked(int uid, @NonNull String packageName,
boolean restricted) {
- final int index = findForcedAppStandbyUidPackageIndexLocked(uid, packageName);
+ final int index = findForcedAppStandbyUidPackageIndexLocked(uid, packageName);
final boolean wasRestricted = index >= 0;
if (wasRestricted == restricted) {
return false;
@@ -382,7 +508,7 @@ public class ForceAppStandbyTracker {
*/
void uidToForeground(int uid) {
synchronized (mLock) {
- if (!UserHandle.isApp(uid)) {
+ if (UserHandle.isCore(uid)) {
return;
}
// TODO This can be optimized by calling indexOfKey and sharing the index for get and
@@ -400,7 +526,7 @@ public class ForceAppStandbyTracker {
*/
void uidToBackground(int uid, boolean remove) {
synchronized (mLock) {
- if (!UserHandle.isApp(uid)) {
+ if (UserHandle.isCore(uid)) {
return;
}
// TODO This can be optimized by calling indexOfKey and sharing the index for get and
@@ -418,25 +544,30 @@ public class ForceAppStandbyTracker {
}
private final class UidObserver extends IUidObserver.Stub {
- @Override public void onUidStateChanged(int uid, int procState, long procStateSeq) {
+ @Override
+ public void onUidStateChanged(int uid, int procState, long procStateSeq) {
}
- @Override public void onUidGone(int uid, boolean disabled) {
+ @Override
+ public void onUidGone(int uid, boolean disabled) {
uidToBackground(uid, /*remove=*/ true);
}
- @Override public void onUidActive(int uid) {
+ @Override
+ public void onUidActive(int uid) {
uidToForeground(uid);
}
- @Override public void onUidIdle(int uid, boolean disabled) {
+ @Override
+ public void onUidIdle(int uid, boolean disabled) {
// Just to avoid excessive memcpy, don't remove from the array in this case.
uidToBackground(uid, /*remove=*/ false);
}
- @Override public void onUidCachedChanged(int uid, boolean cached) {
+ @Override
+ public void onUidCachedChanged(int uid, boolean cached) {
}
- };
+ }
private final class AppOpsWatcher extends IAppOpsCallback.Stub {
@Override
@@ -464,6 +595,11 @@ public class ForceAppStandbyTracker {
if (userId > 0) {
mHandler.doUserRemoved(userId);
}
+ } else if (Intent.ACTION_BATTERY_CHANGED.equals(intent.getAction())) {
+ synchronized (mLock) {
+ mIsPluggedIn = (intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0);
+ }
+ updateForceAllAppStandbyState();
}
}
}
@@ -481,8 +617,8 @@ public class ForceAppStandbyTracker {
private static final int MSG_ALL_WHITELIST_CHANGED = 4;
private static final int MSG_TEMP_WHITELIST_CHANGED = 5;
private static final int MSG_FORCE_ALL_CHANGED = 6;
-
private static final int MSG_USER_REMOVED = 7;
+ private static final int MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED = 8;
public MyHandler(Looper looper) {
super(looper);
@@ -491,6 +627,7 @@ public class ForceAppStandbyTracker {
public void notifyUidForegroundStateChanged(int uid) {
obtainMessage(MSG_UID_STATE_CHANGED, uid, 0).sendToTarget();
}
+
public void notifyRunAnyAppOpsChanged(int uid, @NonNull String packageName) {
obtainMessage(MSG_RUN_ANY_CHANGED, uid, 0, packageName).sendToTarget();
}
@@ -511,12 +648,16 @@ public class ForceAppStandbyTracker {
obtainMessage(MSG_FORCE_ALL_CHANGED).sendToTarget();
}
+ public void notifyForcedAppStandbyFeatureFlagChanged() {
+ obtainMessage(MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED).sendToTarget();
+ }
+
public void doUserRemoved(int userId) {
obtainMessage(MSG_USER_REMOVED, userId, 0).sendToTarget();
}
@Override
- public void dispatchMessage(Message msg) {
+ public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_USER_REMOVED:
handleUserRemoved(msg.arg1);
@@ -562,6 +703,19 @@ public class ForceAppStandbyTracker {
l.onForceAllAppsStandbyChanged(sender);
}
return;
+ case MSG_FORCE_APP_STANDBY_FEATURE_FLAG_CHANGED:
+ // Feature flag for forced app standby changed.
+ final boolean unblockAlarms;
+ synchronized (mLock) {
+ unblockAlarms = !mForcedAppStandbyEnabled && !mForceAllAppsStandby;
+ }
+ for (Listener l : cloneListeners()) {
+ l.updateAllJobs();
+ if (unblockAlarms) {
+ l.unblockAllUnrestrictedAlarms();
+ }
+ }
+ return;
case MSG_USER_REMOVED:
handleUserRemoved(msg.arg1);
return;
@@ -667,22 +821,26 @@ public class ForceAppStandbyTracker {
/**
* @return whether alarms should be restricted for a UID package-name.
*/
- public boolean areAlarmsRestricted(int uid, @NonNull String packageName) {
- return isRestricted(uid, packageName, /*useTempWhitelistToo=*/ false);
+ public boolean areAlarmsRestricted(int uid, @NonNull String packageName,
+ boolean allowWhileIdle) {
+ return isRestricted(uid, packageName, /*useTempWhitelistToo=*/ false,
+ /* exemptOnBatterySaver =*/ allowWhileIdle);
}
/**
* @return whether jobs should be restricted for a UID package-name.
*/
- public boolean areJobsRestricted(int uid, @NonNull String packageName) {
- return isRestricted(uid, packageName, /*useTempWhitelistToo=*/ true);
+ public boolean areJobsRestricted(int uid, @NonNull String packageName,
+ boolean hasForegroundExemption) {
+ return isRestricted(uid, packageName, /*useTempWhitelistToo=*/ true,
+ hasForegroundExemption);
}
/**
* @return whether force-app-standby is effective for a UID package-name.
*/
private boolean isRestricted(int uid, @NonNull String packageName,
- boolean useTempWhitelistToo) {
+ boolean useTempWhitelistToo, boolean exemptOnBatterySaver) {
if (isInForeground(uid)) {
return false;
}
@@ -696,22 +854,25 @@ public class ForceAppStandbyTracker {
ArrayUtils.contains(mTempWhitelistedAppIds, appId)) {
return false;
}
-
- if (mForceAllAppsStandby) {
+ if (mForcedAppStandbyEnabled && isRunAnyRestrictedLocked(uid, packageName)) {
return true;
}
-
- return isRunAnyRestrictedLocked(uid, packageName);
+ if (exemptOnBatterySaver) {
+ return false;
+ }
+ return mForceAllAppsStandby;
}
}
/**
* @return whether a UID is in the foreground or not.
*
- * Note clients normally shouldn't need to access it. It's only for dumpsys.
+ * Note this information is based on the UID proc state callback, meaning it's updated
+ * asynchronously and may subtly be stale. If the fresh data is needed, use
+ * {@link ActivityManagerInternal#getUidProcessState} instead.
*/
public boolean isInForeground(int uid) {
- if (!UserHandle.isApp(uid)) {
+ if (UserHandle.isCore(uid)) {
return true;
}
synchronized (mLock) {
@@ -722,7 +883,6 @@ public class ForceAppStandbyTracker {
/**
* @return whether force all apps standby is enabled or not.
*
- * Note clients normally shouldn't need to access it.
*/
boolean isForceAllAppsStandbyEnabled() {
synchronized (mLock) {
@@ -766,10 +926,25 @@ public class ForceAppStandbyTracker {
public void dump(PrintWriter pw, String indent) {
synchronized (mLock) {
pw.print(indent);
+ pw.println("Forced App Standby Feature enabled: " + mForcedAppStandbyEnabled);
+
+ pw.print(indent);
pw.print("Force all apps standby: ");
pw.println(isForceAllAppsStandbyEnabled());
pw.print(indent);
+ pw.print("Small Battery Device: ");
+ pw.println(isSmallBatteryDevice());
+
+ pw.print(indent);
+ pw.print("Force all apps standby for small battery device: ");
+ pw.println(mForceAllAppStandbyForSmallBattery);
+
+ pw.print(indent);
+ pw.print("Plugged In: ");
+ pw.println(mIsPluggedIn);
+
+ pw.print(indent);
pw.print("Foreground uids: [");
String sep = "";
@@ -808,6 +983,11 @@ public class ForceAppStandbyTracker {
final long token = proto.start(fieldId);
proto.write(ForceAppStandbyTrackerProto.FORCE_ALL_APPS_STANDBY, mForceAllAppsStandby);
+ proto.write(ForceAppStandbyTrackerProto.IS_SMALL_BATTERY_DEVICE,
+ isSmallBatteryDevice());
+ proto.write(ForceAppStandbyTrackerProto.FORCE_ALL_APPS_STANDBY_FOR_SMALL_BATTERY,
+ mForceAllAppStandbyForSmallBattery);
+ proto.write(ForceAppStandbyTrackerProto.IS_CHARGING, mIsPluggedIn);
for (int i = 0; i < mForegroundUids.size(); i++) {
if (mForegroundUids.valueAt(i)) {
diff --git a/com/android/server/InputMethodManagerService.java b/com/android/server/InputMethodManagerService.java
index 55046959..21137adc 100644
--- a/com/android/server/InputMethodManagerService.java
+++ b/com/android/server/InputMethodManagerService.java
@@ -51,6 +51,7 @@ import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
import android.Manifest;
+import android.annotation.AnyThread;
import android.annotation.BinderThread;
import android.annotation.ColorInt;
import android.annotation.IntDef;
@@ -110,6 +111,7 @@ import android.os.ServiceManager;
import android.os.ShellCallback;
import android.os.ShellCommand;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.provider.Settings;
@@ -256,6 +258,44 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
private static final String ACTION_SHOW_INPUT_METHOD_PICKER =
"com.android.server.InputMethodManagerService.SHOW_INPUT_METHOD_PICKER";
+ /**
+ * Debug flag for overriding runtime {@link SystemProperties}.
+ */
+ @AnyThread
+ private static final class DebugFlag {
+ private static final Object LOCK = new Object();
+ private final String mKey;
+ @GuardedBy("LOCK")
+ private boolean mValue;
+
+ public DebugFlag(String key) {
+ mKey = key;
+ refresh();
+ }
+
+ void refresh() {
+ synchronized (LOCK) {
+ mValue = SystemProperties.getBoolean(mKey, true);
+ }
+ }
+
+ boolean value() {
+ synchronized (LOCK) {
+ return mValue;
+ }
+ }
+ }
+
+ /**
+ * Debug flags that can be overridden using "adb shell setprop <key>"
+ * Note: These flags are cached. To refresh, run "adb shell ime refresh_debug_properties".
+ */
+ private static final class DebugFlags {
+ static final DebugFlag FLAG_OPTIMIZE_START_INPUT =
+ new DebugFlag("debug.optimize_startinput");
+ }
+
+
final Context mContext;
final Resources mRes;
final Handler mHandler;
@@ -2930,8 +2970,12 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
if (!didStart && attribute != null) {
- res = startInputUncheckedLocked(cs, inputContext, missingMethods, attribute,
- controlFlags, startInputReason);
+ if (!DebugFlags.FLAG_OPTIMIZE_START_INPUT.value()
+ || (controlFlags
+ & InputMethodManager.CONTROL_WINDOW_IS_TEXT_EDITOR) != 0) {
+ res = startInputUncheckedLocked(cs, inputContext, missingMethods, attribute,
+ controlFlags, startInputReason);
+ }
}
}
} finally {
@@ -4703,6 +4747,8 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
return mService.handleShellCommandSetInputMethod(this);
case "reset":
return mService.handleShellCommandResetInputMethod(this);
+ case "refresh_debug_properties":
+ return refreshDebugProperties();
default:
getOutPrintWriter().println("Unknown command: " + imeCommand);
return ShellCommandResult.FAILURE;
@@ -4713,6 +4759,13 @@ public class InputMethodManagerService extends IInputMethodManager.Stub
}
@BinderThread
+ @ShellCommandResult
+ private int refreshDebugProperties() {
+ DebugFlags.FLAG_OPTIMIZE_START_INPUT.refresh();
+ return ShellCommandResult.SUCCESS;
+ }
+
+ @BinderThread
@Override
public void onHelp() {
try (PrintWriter pw = getOutPrintWriter()) {
diff --git a/com/android/server/IpSecService.java b/com/android/server/IpSecService.java
index d3ab1259..fe4ac6d7 100644
--- a/com/android/server/IpSecService.java
+++ b/com/android/server/IpSecService.java
@@ -19,11 +19,13 @@ package com.android.server;
import static android.Manifest.permission.DUMP;
import static android.net.IpSecManager.INVALID_RESOURCE_ID;
import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.EINVAL;
import static android.system.OsConstants.IPPROTO_UDP;
import static android.system.OsConstants.SOCK_DGRAM;
import static com.android.internal.util.Preconditions.checkNotNull;
import android.content.Context;
+import android.net.ConnectivityManager;
import android.net.IIpSecService;
import android.net.INetd;
import android.net.IpSecAlgorithm;
@@ -32,7 +34,9 @@ import android.net.IpSecManager;
import android.net.IpSecSpiResponse;
import android.net.IpSecTransform;
import android.net.IpSecTransformResponse;
+import android.net.IpSecTunnelInterfaceResponse;
import android.net.IpSecUdpEncapResponse;
+import android.net.Network;
import android.net.NetworkUtils;
import android.net.TrafficStats;
import android.net.util.NetdService;
@@ -48,9 +52,11 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
import java.io.FileDescriptor;
import java.io.IOException;
@@ -60,7 +66,6 @@ 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;
@@ -81,7 +86,7 @@ public class IpSecService extends IIpSecService.Stub {
private static final String NETD_SERVICE_NAME = "netd";
private static final int[] DIRECTIONS =
- new int[] {IpSecTransform.DIRECTION_OUT, IpSecTransform.DIRECTION_IN};
+ new int[] {IpSecManager.DIRECTION_OUT, IpSecManager.DIRECTION_IN};
private static final int NETD_FETCH_TIMEOUT_MS = 5000; // ms
private static final int MAX_PORT_BIND_ATTEMPTS = 10;
@@ -97,12 +102,19 @@ public class IpSecService extends IIpSecService.Stub {
static final int FREE_PORT_MIN = 1024; // ports 1-1023 are reserved
static final int PORT_MAX = 0xFFFF; // ports are an unsigned 16-bit integer
+ static final String TUNNEL_INTERFACE_PREFIX = "ipsec";
/* Binder context for this service */
private final Context mContext;
- /** Should be a never-repeating global ID for resources */
- private static AtomicInteger mNextResourceId = new AtomicInteger(0x00FADED0);
+ /**
+ * The next non-repeating global ID for tracking resources between users, this service, and
+ * kernel data structures. Accessing this variable is not thread safe, so it is only read or
+ * modified within blocks synchronized on IpSecService.this. We want to avoid -1
+ * (INVALID_RESOURCE_ID) and 0 (we probably forgot to initialize it).
+ */
+ @GuardedBy("IpSecService.this")
+ private int mNextResourceId = 1;
interface IpSecServiceConfiguration {
INetd getNetdInstance() throws RemoteException;
@@ -140,7 +152,7 @@ public class IpSecService extends IIpSecService.Stub {
* 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.
+ * and as such, the underlying resources and quota may not be cleaned up.
*/
void invalidate() throws RemoteException;
@@ -290,7 +302,12 @@ public class IpSecService extends IIpSecService.Stub {
}
}
- /* Very simple counting class that looks much like a counting semaphore */
+ /**
+ * Very simple counting class that looks much like a counting semaphore
+ *
+ * <p>This class is not thread-safe, and expects that that users of this class will ensure
+ * synchronization and thread safety by holding the IpSecService.this instance lock.
+ */
@VisibleForTesting
static class ResourceTracker {
private final int mMax;
@@ -333,27 +350,43 @@ public class IpSecService extends IIpSecService.Stub {
@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 each type of resource that a single UID may possess */
+ public static final int MAX_NUM_TUNNEL_INTERFACES = 2;
public static final int MAX_NUM_ENCAP_SOCKETS = 2;
public static final int MAX_NUM_TRANSFORMS = 4;
public static final int MAX_NUM_SPIS = 8;
+ /**
+ * Store each of the OwnedResource types in an (thinly wrapped) sparse array for indexing
+ * and explicit (user) reference management.
+ *
+ * <p>These are stored in separate arrays to improve debuggability and dump output clarity.
+ *
+ * <p>Resources are removed from this array when the user releases their explicit reference
+ * by calling one of the releaseResource() methods.
+ */
final RefcountedResourceArray<SpiRecord> mSpiRecords =
- new RefcountedResourceArray<>(TYPENAME_SPI);
- final ResourceTracker mSpiQuotaTracker = new ResourceTracker(MAX_NUM_SPIS);
-
+ new RefcountedResourceArray<>(SpiRecord.class.getSimpleName());
final RefcountedResourceArray<TransformRecord> mTransformRecords =
- new RefcountedResourceArray<>(TYPENAME_TRANSFORM);
- final ResourceTracker mTransformQuotaTracker = new ResourceTracker(MAX_NUM_TRANSFORMS);
-
+ new RefcountedResourceArray<>(TransformRecord.class.getSimpleName());
final RefcountedResourceArray<EncapSocketRecord> mEncapSocketRecords =
- new RefcountedResourceArray<>(TYPENAME_ENCAP_SOCKET);
+ new RefcountedResourceArray<>(EncapSocketRecord.class.getSimpleName());
+ final RefcountedResourceArray<TunnelInterfaceRecord> mTunnelInterfaceRecords =
+ new RefcountedResourceArray<>(TunnelInterfaceRecord.class.getSimpleName());
+
+ /**
+ * Trackers for quotas for each of the OwnedResource types.
+ *
+ * <p>These trackers are separate from the resource arrays, since they are incremented and
+ * decremented at different points in time. Specifically, quota is only returned upon final
+ * resource deallocation (after all explicit and implicit references are released). Note
+ * that it is possible that calls to releaseResource() will not return the used quota if
+ * there are other resources that depend on (are parents of) the resource being released.
+ */
+ final ResourceTracker mSpiQuotaTracker = new ResourceTracker(MAX_NUM_SPIS);
+ final ResourceTracker mTransformQuotaTracker = new ResourceTracker(MAX_NUM_TRANSFORMS);
final ResourceTracker mSocketQuotaTracker = new ResourceTracker(MAX_NUM_ENCAP_SOCKETS);
+ final ResourceTracker mTunnelQuotaTracker = new ResourceTracker(MAX_NUM_TUNNEL_INTERFACES);
void removeSpiRecord(int resourceId) {
mSpiRecords.remove(resourceId);
@@ -363,6 +396,10 @@ public class IpSecService extends IIpSecService.Stub {
mTransformRecords.remove(resourceId);
}
+ void removeTunnelInterfaceRecord(int resourceId) {
+ mTunnelInterfaceRecords.remove(resourceId);
+ }
+
void removeEncapSocketRecord(int resourceId) {
mEncapSocketRecords.remove(resourceId);
}
@@ -387,11 +424,15 @@ public class IpSecService extends IIpSecService.Stub {
}
}
+ /**
+ * This class is not thread-safe, and expects that that users of this class will ensure
+ * synchronization and thread safety by holding the IpSecService.this instance lock.
+ */
@VisibleForTesting
static final class UserResourceTracker {
private final SparseArray<UserRecord> mUserRecords = new SparseArray<>();
- /** Never-fail getter that populates the list of UIDs as-needed */
+ /** Lazy-initialization/getter that populates or retrieves the UserRecord as needed */
public UserRecord getUserRecord(int uid) {
checkCallerUid(uid);
@@ -420,18 +461,20 @@ public class IpSecService extends IIpSecService.Stub {
@VisibleForTesting final UserResourceTracker mUserResourceTracker = new UserResourceTracker();
/**
- * The KernelResourceRecord class provides a facility to cleanly and reliably track system
+ * The OwnedResourceRecord 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
+ * tracking arrays and kernel resources, respectively.
+ *
+ * <p>This class associates kernel resources with the UID that owns and controls them.
*/
- private abstract class KernelResourceRecord implements IResource {
+ private abstract class OwnedResourceRecord implements IResource {
final int pid;
final int uid;
protected final int mResourceId;
- KernelResourceRecord(int resourceId) {
+ OwnedResourceRecord(int resourceId) {
super();
if (resourceId == INVALID_RESOURCE_ID) {
throw new IllegalArgumentException("Resource ID must not be INVALID_RESOURCE_ID");
@@ -471,8 +514,6 @@ public class IpSecService extends IIpSecService.Stub {
}
};
- // TODO: Move this to right after RefcountedResource. With this here, Gerrit was showing many
- // more things as changed.
/**
* Thin wrapper over SparseArray to ensure resources exist, and simplify generic typing.
*
@@ -526,46 +567,56 @@ public class IpSecService extends IIpSecService.Stub {
}
}
- private final class TransformRecord extends KernelResourceRecord {
+ /**
+ * Tracks an SA in the kernel, and manages cleanup paths. Once a TransformRecord is
+ * created, the SpiRecord that originally tracked the SAs will reliquish the
+ * responsibility of freeing the underlying SA to this class via the mOwnedByTransform flag.
+ */
+ private final class TransformRecord extends OwnedResourceRecord {
private final IpSecConfig mConfig;
- private final SpiRecord[] mSpis;
+ private final SpiRecord mSpi;
private final EncapSocketRecord mSocket;
TransformRecord(
- int resourceId, IpSecConfig config, SpiRecord[] spis, EncapSocketRecord socket) {
+ int resourceId, IpSecConfig config, SpiRecord spi, EncapSocketRecord socket) {
super(resourceId);
mConfig = config;
- mSpis = spis;
+ mSpi = spi;
mSocket = socket;
+
+ spi.setOwnedByTransform();
}
public IpSecConfig getConfig() {
return mConfig;
}
- public SpiRecord getSpiRecord(int direction) {
- return mSpis[direction];
+ public SpiRecord getSpiRecord() {
+ return mSpi;
+ }
+
+ public EncapSocketRecord getSocketRecord() {
+ return mSocket;
}
/** always guarded by IpSecService#this */
@Override
public void freeUnderlyingResources() {
- for (int direction : DIRECTIONS) {
- int spi = mSpis[direction].getSpi();
- try {
- mSrvConfig
- .getNetdInstance()
- .ipSecDeleteSecurityAssociation(
- mResourceId,
- direction,
- mConfig.getLocalAddress(),
- mConfig.getRemoteAddress(),
- spi);
- } catch (ServiceSpecificException e) {
- // FIXME: get the error code and throw is at an IOException from Errno Exception
- } catch (RemoteException e) {
- Log.e(TAG, "Failed to delete SA with ID: " + mResourceId);
- }
+ int spi = mSpi.getSpi();
+ try {
+ mSrvConfig
+ .getNetdInstance()
+ .ipSecDeleteSecurityAssociation(
+ mResourceId,
+ mConfig.getSourceAddress(),
+ mConfig.getDestinationAddress(),
+ spi,
+ mConfig.getMarkValue(),
+ mConfig.getMarkMask());
+ } catch (ServiceSpecificException e) {
+ // FIXME: get the error code and throw is at an IOException from Errno Exception
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to delete SA with ID: " + mResourceId);
}
getResourceTracker().give();
@@ -589,10 +640,8 @@ public class IpSecService extends IIpSecService.Stub {
.append(super.toString())
.append(", mSocket=")
.append(mSocket)
- .append(", mSpis[OUT].mResourceId=")
- .append(mSpis[IpSecTransform.DIRECTION_OUT].mResourceId)
- .append(", mSpis[IN].mResourceId=")
- .append(mSpis[IpSecTransform.DIRECTION_IN].mResourceId)
+ .append(", mSpi.mResourceId=")
+ .append(mSpi.mResourceId)
.append(", mConfig=")
.append(mConfig)
.append("}");
@@ -600,45 +649,33 @@ public class IpSecService extends IIpSecService.Stub {
}
}
- private final class SpiRecord extends KernelResourceRecord {
- private final int mDirection;
- private final String mLocalAddress;
- private final String mRemoteAddress;
+ /**
+ * Tracks a single SA in the kernel, and manages cleanup paths. Once used in a Transform, the
+ * responsibility for cleaning up underlying resources will be passed to the TransformRecord
+ * object
+ */
+ private final class SpiRecord extends OwnedResourceRecord {
+ private final String mSourceAddress;
+ private final String mDestinationAddress;
private int mSpi;
private boolean mOwnedByTransform = false;
- SpiRecord(
- int resourceId,
- int direction,
- String localAddress,
- String remoteAddress,
- int spi) {
+ SpiRecord(int resourceId, String sourceAddress, String destinationAddress, int spi) {
super(resourceId);
- mDirection = direction;
- mLocalAddress = localAddress;
- mRemoteAddress = remoteAddress;
+ mSourceAddress = sourceAddress;
+ mDestinationAddress = destinationAddress;
mSpi = spi;
}
/** always guarded by IpSecService#this */
@Override
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
- // freed from the SpiRecord once used in a transform. (They refer to the same SA,
- // thus ownership and responsibility for freeing these resources passes to the
- // Transform object). Thus, we should let the user free them without penalty once
- // they are applied in a Transform object.
- return;
- }
-
try {
mSrvConfig
.getNetdInstance()
.ipSecDeleteSecurityAssociation(
- mResourceId, mDirection, mLocalAddress, mRemoteAddress, mSpi);
+ mResourceId, mSourceAddress, mDestinationAddress, mSpi, 0, 0);
} catch (ServiceSpecificException e) {
// FIXME: get the error code and throw is at an IOException from Errno Exception
} catch (RemoteException e) {
@@ -654,6 +691,10 @@ public class IpSecService extends IIpSecService.Stub {
return mSpi;
}
+ public String getDestinationAddress() {
+ return mDestinationAddress;
+ }
+
public void setOwnedByTransform() {
if (mOwnedByTransform) {
// Programming error
@@ -663,6 +704,10 @@ public class IpSecService extends IIpSecService.Stub {
mOwnedByTransform = true;
}
+ public boolean getOwnedByTransform() {
+ return mOwnedByTransform;
+ }
+
@Override
public void invalidate() throws RemoteException {
getUserRecord().removeSpiRecord(mResourceId);
@@ -681,12 +726,10 @@ public class IpSecService extends IIpSecService.Stub {
.append(super.toString())
.append(", mSpi=")
.append(mSpi)
- .append(", mDirection=")
- .append(mDirection)
- .append(", mLocalAddress=")
- .append(mLocalAddress)
- .append(", mRemoteAddress=")
- .append(mRemoteAddress)
+ .append(", mSourceAddress=")
+ .append(mSourceAddress)
+ .append(", mDestinationAddress=")
+ .append(mDestinationAddress)
.append(", mOwnedByTransform=")
.append(mOwnedByTransform)
.append("}");
@@ -694,7 +737,173 @@ public class IpSecService extends IIpSecService.Stub {
}
}
- private final class EncapSocketRecord extends KernelResourceRecord {
+ // These values have been reserved in ConnectivityService
+ @VisibleForTesting static final int TUN_INTF_NETID_START = 0xFC00;
+
+ @VisibleForTesting static final int TUN_INTF_NETID_RANGE = 0x0400;
+
+ private final SparseBooleanArray mTunnelNetIds = new SparseBooleanArray();
+ private int mNextTunnelNetIdIndex = 0;
+
+ /**
+ * Reserves a netId within the range of netIds allocated for IPsec tunnel interfaces
+ *
+ * <p>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.
+ *
+ * @return an integer key within the netId range, if successful
+ * @throws IllegalStateException if unsuccessful (all netId are currently reserved)
+ */
+ @VisibleForTesting
+ int reserveNetId() {
+ synchronized (mTunnelNetIds) {
+ for (int i = 0; i < TUN_INTF_NETID_RANGE; i++) {
+ int index = mNextTunnelNetIdIndex;
+ int netId = index + TUN_INTF_NETID_START;
+ if (++mNextTunnelNetIdIndex >= TUN_INTF_NETID_RANGE) mNextTunnelNetIdIndex = 0;
+ if (!mTunnelNetIds.get(netId)) {
+ mTunnelNetIds.put(netId, true);
+ return netId;
+ }
+ }
+ }
+ throw new IllegalStateException("No free netIds to allocate");
+ }
+
+ @VisibleForTesting
+ void releaseNetId(int netId) {
+ synchronized (mTunnelNetIds) {
+ mTunnelNetIds.delete(netId);
+ }
+ }
+
+ private final class TunnelInterfaceRecord extends OwnedResourceRecord {
+ private final String mInterfaceName;
+ private final Network mUnderlyingNetwork;
+
+ // outer addresses
+ private final String mLocalAddress;
+ private final String mRemoteAddress;
+
+ private final int mIkey;
+ private final int mOkey;
+
+ TunnelInterfaceRecord(
+ int resourceId,
+ String interfaceName,
+ Network underlyingNetwork,
+ String localAddr,
+ String remoteAddr,
+ int ikey,
+ int okey) {
+ super(resourceId);
+
+ mInterfaceName = interfaceName;
+ mUnderlyingNetwork = underlyingNetwork;
+ mLocalAddress = localAddr;
+ mRemoteAddress = remoteAddr;
+ mIkey = ikey;
+ mOkey = okey;
+ }
+
+ /** always guarded by IpSecService#this */
+ @Override
+ public void freeUnderlyingResources() {
+ // Calls to netd
+ // Teardown VTI
+ // Delete global policies
+ try {
+ mSrvConfig.getNetdInstance().removeVirtualTunnelInterface(mInterfaceName);
+
+ for (int direction : DIRECTIONS) {
+ int mark = (direction == IpSecManager.DIRECTION_IN) ? mIkey : mOkey;
+ mSrvConfig
+ .getNetdInstance()
+ .ipSecDeleteSecurityPolicy(
+ 0, direction, mLocalAddress, mRemoteAddress, mark, 0xffffffff);
+ }
+ } catch (ServiceSpecificException e) {
+ // FIXME: get the error code and throw is at an IOException from Errno Exception
+ } catch (RemoteException e) {
+ Log.e(
+ TAG,
+ "Failed to delete VTI with interface name: "
+ + mInterfaceName
+ + " and id: "
+ + mResourceId);
+ }
+
+ getResourceTracker().give();
+ releaseNetId(mIkey);
+ releaseNetId(mOkey);
+ }
+
+ public String getInterfaceName() {
+ return mInterfaceName;
+ }
+
+ public Network getUnderlyingNetwork() {
+ return mUnderlyingNetwork;
+ }
+
+ /** Returns the local, outer address for the tunnelInterface */
+ public String getLocalAddress() {
+ return mLocalAddress;
+ }
+
+ /** Returns the remote, outer address for the tunnelInterface */
+ public String getRemoteAddress() {
+ return mRemoteAddress;
+ }
+
+ public int getIkey() {
+ return mIkey;
+ }
+
+ public int getOkey() {
+ return mOkey;
+ }
+
+ @Override
+ protected ResourceTracker getResourceTracker() {
+ return getUserRecord().mTunnelQuotaTracker;
+ }
+
+ @Override
+ public void invalidate() {
+ getUserRecord().removeTunnelInterfaceRecord(mResourceId);
+ }
+
+ @Override
+ public String toString() {
+ return new StringBuilder()
+ .append("{super=")
+ .append(super.toString())
+ .append(", mInterfaceName=")
+ .append(mInterfaceName)
+ .append(", mUnderlyingNetwork=")
+ .append(mUnderlyingNetwork)
+ .append(", mLocalAddress=")
+ .append(mLocalAddress)
+ .append(", mRemoteAddress=")
+ .append(mRemoteAddress)
+ .append(", mIkey=")
+ .append(mIkey)
+ .append(", mOkey=")
+ .append(mOkey)
+ .append("}")
+ .toString();
+ }
+ }
+
+ /**
+ * Tracks a UDP encap socket, and manages cleanup paths
+ *
+ * <p>While this class does not manage non-kernel resources, race conditions around socket
+ * binding require that the service creates the encap socket, binds it and applies the socket
+ * policy before handing it to a user.
+ */
+ private final class EncapSocketRecord extends OwnedResourceRecord {
private FileDescriptor mSocket;
private final int mPort;
@@ -764,14 +973,17 @@ 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();
- }
- });
+ this(
+ context,
+ config,
+ (fd, uid) -> {
+ try {
+ TrafficStats.setThreadStatsUid(uid);
+ TrafficStats.tagFileDescriptor(fd);
+ } finally {
+ TrafficStats.clearThreadStatsUid();
+ }
+ });
}
/** @hide */
@@ -837,8 +1049,8 @@ public class IpSecService extends IIpSecService.Stub {
*/
private static void checkDirection(int direction) {
switch (direction) {
- case IpSecTransform.DIRECTION_OUT:
- case IpSecTransform.DIRECTION_IN:
+ case IpSecManager.DIRECTION_OUT:
+ case IpSecManager.DIRECTION_IN:
return;
}
throw new IllegalArgumentException("Invalid Direction: " + direction);
@@ -847,39 +1059,30 @@ public class IpSecService extends IIpSecService.Stub {
/** Get a new SPI and maintain the reservation in the system server */
@Override
public synchronized IpSecSpiResponse allocateSecurityParameterIndex(
- int direction, String remoteAddress, int requestedSpi, IBinder binder)
- throws RemoteException {
- checkDirection(direction);
- checkInetAddress(remoteAddress);
+ String destinationAddress, int requestedSpi, IBinder binder) throws RemoteException {
+ checkInetAddress(destinationAddress);
/* requestedSpi can be anything in the int range, so no check is needed. */
checkNotNull(binder, "Null Binder passed to allocateSecurityParameterIndex");
UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
- int resourceId = mNextResourceId.getAndIncrement();
+ final int resourceId = mNextResourceId++;
int spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
- String localAddress = "";
-
try {
if (!userRecord.mSpiQuotaTracker.isAvailable()) {
return new IpSecSpiResponse(
IpSecManager.Status.RESOURCE_UNAVAILABLE, INVALID_RESOURCE_ID, spi);
}
+
spi =
mSrvConfig
.getNetdInstance()
- .ipSecAllocateSpi(
- resourceId,
- direction,
- localAddress,
- remoteAddress,
- requestedSpi);
+ .ipSecAllocateSpi(resourceId, "", destinationAddress, requestedSpi);
Log.d(TAG, "Allocated SPI " + spi);
userRecord.mSpiRecords.put(
resourceId,
new RefcountedResource<SpiRecord>(
- new SpiRecord(resourceId, direction, localAddress, remoteAddress, spi),
- binder));
+ new SpiRecord(resourceId, "", destinationAddress, spi), binder));
} catch (ServiceSpecificException e) {
// TODO: Add appropriate checks when other ServiceSpecificException types are supported
return new IpSecSpiResponse(
@@ -978,7 +1181,7 @@ public class IpSecService extends IIpSecService.Stub {
int callingUid = Binder.getCallingUid();
UserRecord userRecord = mUserResourceTracker.getUserRecord(callingUid);
- int resourceId = mNextResourceId.getAndIncrement();
+ final int resourceId = mNextResourceId++;
FileDescriptor sockFd = null;
try {
if (!userRecord.mSocketQuotaTracker.isAvailable()) {
@@ -1024,35 +1227,160 @@ public class IpSecService extends IIpSecService.Stub {
}
/**
- * Checks an IpSecConfig parcel to ensure that the contents are sane and throws an
- * IllegalArgumentException if they are not.
+ * Create a tunnel interface for use in IPSec tunnel mode. The system server will cache the
+ * tunnel interface and a record of its owner so that it can and must be freed when no longer
+ * needed.
*/
- private void checkIpSecConfig(IpSecConfig config) {
- UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+ @Override
+ public synchronized IpSecTunnelInterfaceResponse createTunnelInterface(
+ String localAddr, String remoteAddr, Network underlyingNetwork, IBinder binder) {
+ checkNotNull(binder, "Null Binder passed to createTunnelInterface");
+ checkNotNull(underlyingNetwork, "No underlying network was specified");
+ checkInetAddress(localAddr);
+ checkInetAddress(remoteAddr);
- if (config.getLocalAddress() == null) {
- throw new IllegalArgumentException("Invalid null Local InetAddress");
- }
+ // TODO: Check that underlying network exists, and IP addresses not assigned to a different
+ // network (b/72316676).
- if (config.getRemoteAddress() == null) {
- throw new IllegalArgumentException("Invalid null Remote InetAddress");
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+ if (!userRecord.mTunnelQuotaTracker.isAvailable()) {
+ return new IpSecTunnelInterfaceResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
}
- switch (config.getMode()) {
- case IpSecTransform.MODE_TRANSPORT:
- if (!config.getLocalAddress().isEmpty()) {
- throw new IllegalArgumentException("Non-empty Local Address");
- }
- // Must be valid, and not a wildcard
- checkInetAddress(config.getRemoteAddress());
- break;
- case IpSecTransform.MODE_TUNNEL:
- break;
- default:
- throw new IllegalArgumentException(
- "Invalid IpSecTransform.mode: " + config.getMode());
+ final int resourceId = mNextResourceId++;
+ final int ikey = reserveNetId();
+ final int okey = reserveNetId();
+ String intfName = String.format("%s%d", TUNNEL_INTERFACE_PREFIX, resourceId);
+
+ try {
+ // Calls to netd:
+ // Create VTI
+ // Add inbound/outbound global policies
+ // (use reqid = 0)
+ mSrvConfig
+ .getNetdInstance()
+ .addVirtualTunnelInterface(intfName, localAddr, remoteAddr, ikey, okey);
+
+ for (int direction : DIRECTIONS) {
+ int mark = (direction == IpSecManager.DIRECTION_OUT) ? okey : ikey;
+
+ mSrvConfig
+ .getNetdInstance()
+ .ipSecAddSecurityPolicy(
+ 0, // Use 0 for reqId
+ direction,
+ "",
+ "",
+ 0,
+ mark,
+ 0xffffffff);
+ }
+
+ userRecord.mTunnelInterfaceRecords.put(
+ resourceId,
+ new RefcountedResource<TunnelInterfaceRecord>(
+ new TunnelInterfaceRecord(
+ resourceId,
+ intfName,
+ underlyingNetwork,
+ localAddr,
+ remoteAddr,
+ ikey,
+ okey),
+ binder));
+ return new IpSecTunnelInterfaceResponse(IpSecManager.Status.OK, resourceId, intfName);
+ } catch (RemoteException e) {
+ // Release keys if we got an error.
+ releaseNetId(ikey);
+ releaseNetId(okey);
+ throw e.rethrowFromSystemServer();
+ } catch (ServiceSpecificException e) {
+ // FIXME: get the error code and throw is at an IOException from Errno Exception
}
+ // If we make it to here, then something has gone wrong and we couldn't create a VTI.
+ // Release the keys that we reserved, and return an error status.
+ releaseNetId(ikey);
+ releaseNetId(okey);
+ return new IpSecTunnelInterfaceResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
+ }
+
+ /**
+ * Adds a new local address to the tunnel interface. This allows packets to be sent and received
+ * from multiple local IP addresses over the same tunnel.
+ */
+ @Override
+ public synchronized void addAddressToTunnelInterface(int tunnelResourceId, String localAddr) {
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+
+ // Get tunnelInterface record; if no such interface is found, will throw
+ // IllegalArgumentException
+ TunnelInterfaceRecord tunnelInterfaceInfo =
+ userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelResourceId);
+
+ // TODO: Add calls to netd:
+ // Add address to TunnelInterface
+ }
+
+ /**
+ * Remove a new local address from the tunnel interface. After removal, the address will no
+ * longer be available to send from, or receive on.
+ */
+ @Override
+ public synchronized void removeAddressFromTunnelInterface(
+ int tunnelResourceId, String localAddr) {
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+
+ // Get tunnelInterface record; if no such interface is found, will throw
+ // IllegalArgumentException
+ TunnelInterfaceRecord tunnelInterfaceInfo =
+ userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelResourceId);
+
+ // TODO: Add calls to netd:
+ // Remove address from TunnelInterface
+ }
+
+ /**
+ * Delete a TunnelInterface that has been been allocated by and registered with the system
+ * server
+ */
+ @Override
+ public synchronized void deleteTunnelInterface(int resourceId) throws RemoteException {
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+ releaseResource(userRecord.mTunnelInterfaceRecords, resourceId);
+ }
+
+ @VisibleForTesting
+ void validateAlgorithms(IpSecConfig config) throws IllegalArgumentException {
+ IpSecAlgorithm auth = config.getAuthentication();
+ IpSecAlgorithm crypt = config.getEncryption();
+ IpSecAlgorithm aead = config.getAuthenticatedEncryption();
+
+ // Validate the algorithm set
+ Preconditions.checkArgument(
+ aead != null || crypt != null || auth != null,
+ "No Encryption or Authentication algorithms specified");
+ Preconditions.checkArgument(
+ auth == null || auth.isAuthentication(),
+ "Unsupported algorithm for Authentication");
+ Preconditions.checkArgument(
+ crypt == null || crypt.isEncryption(), "Unsupported algorithm for Encryption");
+ Preconditions.checkArgument(
+ aead == null || aead.isAead(),
+ "Unsupported algorithm for Authenticated Encryption");
+ Preconditions.checkArgument(
+ aead == null || (auth == null && crypt == null),
+ "Authenticated Encryption is mutually exclusive with other Authentication "
+ + "or Encryption algorithms");
+ }
+
+ /**
+ * Checks an IpSecConfig parcel to ensure that the contents are sane and throws an
+ * IllegalArgumentException if they are not.
+ */
+ private void checkIpSecConfig(IpSecConfig config) {
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+
switch (config.getEncapType()) {
case IpSecTransform.ENCAP_NONE:
break;
@@ -1071,107 +1399,129 @@ public class IpSecService extends IIpSecService.Stub {
throw new IllegalArgumentException("Invalid Encap Type: " + config.getEncapType());
}
- for (int direction : DIRECTIONS) {
- IpSecAlgorithm crypt = config.getEncryption(direction);
- IpSecAlgorithm auth = config.getAuthentication(direction);
- IpSecAlgorithm authenticatedEncryption = config.getAuthenticatedEncryption(direction);
- if (authenticatedEncryption == null && crypt == null && auth == null) {
- throw new IllegalArgumentException(
- "No Encryption or Authentication algorithms specified");
- } else if (authenticatedEncryption != null && (auth != null || crypt != null)) {
+ validateAlgorithms(config);
+
+ // Retrieve SPI record; will throw IllegalArgumentException if not found
+ SpiRecord s = userRecord.mSpiRecords.getResourceOrThrow(config.getSpiResourceId());
+
+ // Check to ensure that SPI has not already been used.
+ if (s.getOwnedByTransform()) {
+ throw new IllegalStateException("SPI already in use; cannot be used in new Transforms");
+ }
+
+ // If no remote address is supplied, then use one from the SPI.
+ if (TextUtils.isEmpty(config.getDestinationAddress())) {
+ config.setDestinationAddress(s.getDestinationAddress());
+ }
+
+ // All remote addresses must match
+ if (!config.getDestinationAddress().equals(s.getDestinationAddress())) {
+ throw new IllegalArgumentException("Mismatched remote addresseses.");
+ }
+
+ // This check is technically redundant due to the chain of custody between the SPI and
+ // the IpSecConfig, but in the future if the dest is allowed to be set explicitly in
+ // the transform, this will prevent us from messing up.
+ checkInetAddress(config.getDestinationAddress());
+
+ // Require a valid source address for all transforms.
+ checkInetAddress(config.getSourceAddress());
+
+ switch (config.getMode()) {
+ case IpSecTransform.MODE_TRANSPORT:
+ case IpSecTransform.MODE_TUNNEL:
+ break;
+ default:
throw new IllegalArgumentException(
- "Authenticated Encryption is mutually"
- + " exclusive with other Authentication or Encryption algorithms");
- }
+ "Invalid IpSecTransform.mode: " + config.getMode());
+ }
+ }
+
+ private void createOrUpdateTransform(
+ IpSecConfig c, int resourceId, SpiRecord spiRecord, EncapSocketRecord socketRecord)
+ throws RemoteException {
- // Retrieve SPI record; will throw IllegalArgumentException if not found
- userRecord.mSpiRecords.getResourceOrThrow(config.getSpiResourceId(direction));
+ int encapType = c.getEncapType(), encapLocalPort = 0, encapRemotePort = 0;
+ if (encapType != IpSecTransform.ENCAP_NONE) {
+ encapLocalPort = socketRecord.getPort();
+ encapRemotePort = c.getEncapRemotePort();
}
+
+ IpSecAlgorithm auth = c.getAuthentication();
+ IpSecAlgorithm crypt = c.getEncryption();
+ IpSecAlgorithm authCrypt = c.getAuthenticatedEncryption();
+
+ mSrvConfig
+ .getNetdInstance()
+ .ipSecAddSecurityAssociation(
+ resourceId,
+ c.getMode(),
+ c.getSourceAddress(),
+ c.getDestinationAddress(),
+ (c.getNetwork() != null) ? c.getNetwork().netId : 0,
+ spiRecord.getSpi(),
+ c.getMarkValue(),
+ c.getMarkMask(),
+ (auth != null) ? auth.getName() : "",
+ (auth != null) ? auth.getKey() : new byte[] {},
+ (auth != null) ? auth.getTruncationLengthBits() : 0,
+ (crypt != null) ? crypt.getName() : "",
+ (crypt != null) ? crypt.getKey() : new byte[] {},
+ (crypt != null) ? crypt.getTruncationLengthBits() : 0,
+ (authCrypt != null) ? authCrypt.getName() : "",
+ (authCrypt != null) ? authCrypt.getKey() : new byte[] {},
+ (authCrypt != null) ? authCrypt.getTruncationLengthBits() : 0,
+ encapType,
+ encapLocalPort,
+ encapRemotePort);
}
/**
- * Create a transport mode transform, which represent two security associations (one in each
- * direction) in the kernel. The transform will be cached by the system server and must be freed
- * when no longer needed. It is possible to free one, deleting the SA from underneath sockets
- * that are using it, which will result in all of those sockets becoming unable to send or
- * receive data.
+ * Create a IPsec transform, which represents a single security association in the kernel. The
+ * transform will be cached by the system server and must be freed when no longer needed. It is
+ * possible to free one, deleting the SA from underneath sockets that are using it, which will
+ * result in all of those sockets becoming unable to send or receive data.
*/
@Override
- public synchronized IpSecTransformResponse createTransportModeTransform(
- IpSecConfig c, IBinder binder) throws RemoteException {
+ public synchronized IpSecTransformResponse createTransform(IpSecConfig c, IBinder binder)
+ throws RemoteException {
checkIpSecConfig(c);
- checkNotNull(binder, "Null Binder passed to createTransportModeTransform");
- int resourceId = mNextResourceId.getAndIncrement();
+ checkNotNull(binder, "Null Binder passed to createTransform");
+ final int resourceId = mNextResourceId++;
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);
+ List<RefcountedResource> dependencies = new ArrayList<>();
if (!userRecord.mTransformQuotaTracker.isAvailable()) {
return new IpSecTransformResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
}
- SpiRecord[] spis = new SpiRecord[DIRECTIONS.length];
- int encapType, encapLocalPort = 0, encapRemotePort = 0;
EncapSocketRecord socketRecord = null;
- encapType = c.getEncapType();
- if (encapType != IpSecTransform.ENCAP_NONE) {
+ if (c.getEncapType() != IpSecTransform.ENCAP_NONE) {
RefcountedResource<EncapSocketRecord> refcountedSocketRecord =
userRecord.mEncapSocketRecords.getRefcountedResourceOrThrow(
c.getEncapSocketResourceId());
dependencies.add(refcountedSocketRecord);
-
socketRecord = refcountedSocketRecord.getResource();
- encapLocalPort = socketRecord.getPort();
- encapRemotePort = c.getEncapRemotePort();
}
- for (int direction : DIRECTIONS) {
- IpSecAlgorithm auth = c.getAuthentication(direction);
- IpSecAlgorithm crypt = c.getEncryption(direction);
- IpSecAlgorithm authCrypt = c.getAuthenticatedEncryption(direction);
-
- RefcountedResource<SpiRecord> refcountedSpiRecord =
- userRecord.mSpiRecords.getRefcountedResourceOrThrow(
- c.getSpiResourceId(direction));
- dependencies.add(refcountedSpiRecord);
+ RefcountedResource<SpiRecord> refcountedSpiRecord =
+ userRecord.mSpiRecords.getRefcountedResourceOrThrow(c.getSpiResourceId());
+ dependencies.add(refcountedSpiRecord);
+ SpiRecord spiRecord = refcountedSpiRecord.getResource();
- spis[direction] = refcountedSpiRecord.getResource();
- int spi = spis[direction].getSpi();
- try {
- mSrvConfig
- .getNetdInstance()
- .ipSecAddSecurityAssociation(
- resourceId,
- c.getMode(),
- direction,
- c.getLocalAddress(),
- c.getRemoteAddress(),
- (c.getNetwork() != null) ? c.getNetwork().getNetworkHandle() : 0,
- spi,
- (auth != null) ? auth.getName() : "",
- (auth != null) ? auth.getKey() : new byte[] {},
- (auth != null) ? auth.getTruncationLengthBits() : 0,
- (crypt != null) ? crypt.getName() : "",
- (crypt != null) ? crypt.getKey() : new byte[] {},
- (crypt != null) ? crypt.getTruncationLengthBits() : 0,
- (authCrypt != null) ? authCrypt.getName() : "",
- (authCrypt != null) ? authCrypt.getKey() : new byte[] {},
- (authCrypt != null) ? authCrypt.getTruncationLengthBits() : 0,
- encapType,
- encapLocalPort,
- encapRemotePort);
- } catch (ServiceSpecificException e) {
- // FIXME: get the error code and throw is at an IOException from Errno Exception
- return new IpSecTransformResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
- }
+ try {
+ createOrUpdateTransform(c, resourceId, spiRecord, socketRecord);
+ } catch (ServiceSpecificException e) {
+ // FIXME: get the error code and throw is at an IOException from Errno Exception
+ return new IpSecTransformResponse(IpSecManager.Status.RESOURCE_UNAVAILABLE);
}
- // Both SAs were created successfully, time to construct a record and lock it away
+
+ // SA was created successfully, time to construct a record and lock it away
userRecord.mTransformRecords.put(
resourceId,
new RefcountedResource<TransformRecord>(
- new TransformRecord(resourceId, c, spis, socketRecord),
+ new TransformRecord(resourceId, c, spiRecord, socketRecord),
binder,
dependencies.toArray(new RefcountedResource[dependencies.size()])));
return new IpSecTransformResponse(IpSecManager.Status.OK, resourceId);
@@ -1184,7 +1534,7 @@ public class IpSecService extends IIpSecService.Stub {
* other reasons.
*/
@Override
- public synchronized void deleteTransportModeTransform(int resourceId) throws RemoteException {
+ public synchronized void deleteTransform(int resourceId) throws RemoteException {
UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
releaseResource(userRecord.mTransformRecords, resourceId);
}
@@ -1195,9 +1545,9 @@ public class IpSecService extends IIpSecService.Stub {
*/
@Override
public synchronized void applyTransportModeTransform(
- ParcelFileDescriptor socket, int resourceId) throws RemoteException {
+ ParcelFileDescriptor socket, int direction, int resourceId) throws RemoteException {
UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
-
+ checkDirection(direction);
// Get transform record; if no transform is found, will throw IllegalArgumentException
TransformRecord info = userRecord.mTransformRecords.getResourceOrThrow(resourceId);
@@ -1206,32 +1556,39 @@ public class IpSecService extends IIpSecService.Stub {
throw new SecurityException("Only the owner of an IpSec Transform may apply it!");
}
+ // Get config and check that to-be-applied transform has the correct mode
IpSecConfig c = info.getConfig();
+ Preconditions.checkArgument(
+ c.getMode() == IpSecTransform.MODE_TRANSPORT,
+ "Transform mode was not Transport mode; cannot be applied to a socket");
+
try {
- for (int direction : DIRECTIONS) {
- mSrvConfig
- .getNetdInstance()
- .ipSecApplyTransportModeTransform(
- socket.getFileDescriptor(),
- resourceId,
- direction,
- c.getLocalAddress(),
- c.getRemoteAddress(),
- info.getSpiRecord(direction).getSpi());
- }
+ mSrvConfig
+ .getNetdInstance()
+ .ipSecApplyTransportModeTransform(
+ socket.getFileDescriptor(),
+ resourceId,
+ direction,
+ c.getSourceAddress(),
+ c.getDestinationAddress(),
+ info.getSpiRecord().getSpi());
} catch (ServiceSpecificException e) {
- // FIXME: get the error code and throw is at an IOException from Errno Exception
+ if (e.errorCode == EINVAL) {
+ throw new IllegalArgumentException(e.toString());
+ } else {
+ throw e;
+ }
}
}
/**
- * Remove a transport mode transform from a socket, applying the default (empty) policy. This
- * will ensure that NO IPsec policy is applied to the socket (would be the equivalent of
- * applying a policy that performs no IPsec). Today the resourceId parameter is passed but not
- * used: reserved for future improved input validation.
+ * Remove transport mode transforms from a socket, applying the default (empty) policy. This
+ * ensures that NO IPsec policy is applied to the socket (would be the equivalent of applying a
+ * policy that performs no IPsec). Today the resourceId parameter is passed but not used:
+ * reserved for future improved input validation.
*/
@Override
- public synchronized void removeTransportModeTransform(ParcelFileDescriptor socket, int resourceId)
+ public synchronized void removeTransportModeTransforms(ParcelFileDescriptor socket)
throws RemoteException {
try {
mSrvConfig
@@ -1242,6 +1599,76 @@ public class IpSecService extends IIpSecService.Stub {
}
}
+ /**
+ * Apply an active tunnel mode transform to a TunnelInterface, which will apply the IPsec
+ * security association as a correspondent policy to the provided interface
+ */
+ @Override
+ public synchronized void applyTunnelModeTransform(
+ int tunnelResourceId, int direction, int transformResourceId) throws RemoteException {
+ checkDirection(direction);
+
+ UserRecord userRecord = mUserResourceTracker.getUserRecord(Binder.getCallingUid());
+
+ // Get transform record; if no transform is found, will throw IllegalArgumentException
+ TransformRecord transformInfo =
+ userRecord.mTransformRecords.getResourceOrThrow(transformResourceId);
+
+ // Get tunnelInterface record; if no such interface is found, will throw
+ // IllegalArgumentException
+ TunnelInterfaceRecord tunnelInterfaceInfo =
+ userRecord.mTunnelInterfaceRecords.getResourceOrThrow(tunnelResourceId);
+
+ // Get config and check that to-be-applied transform has the correct mode
+ IpSecConfig c = transformInfo.getConfig();
+ Preconditions.checkArgument(
+ c.getMode() == IpSecTransform.MODE_TUNNEL,
+ "Transform mode was not Tunnel mode; cannot be applied to a tunnel interface");
+
+ EncapSocketRecord socketRecord = null;
+ if (c.getEncapType() != IpSecTransform.ENCAP_NONE) {
+ socketRecord =
+ userRecord.mEncapSocketRecords.getResourceOrThrow(c.getEncapSocketResourceId());
+ }
+ SpiRecord spiRecord = userRecord.mSpiRecords.getResourceOrThrow(c.getSpiResourceId());
+
+ int mark =
+ (direction == IpSecManager.DIRECTION_IN)
+ ? tunnelInterfaceInfo.getIkey()
+ : tunnelInterfaceInfo.getOkey();
+
+ try {
+ c.setMarkValue(mark);
+ c.setMarkMask(0xffffffff);
+
+ if (direction == IpSecManager.DIRECTION_OUT) {
+ // Set output mark via underlying network (output only)
+ c.setNetwork(tunnelInterfaceInfo.getUnderlyingNetwork());
+
+ // If outbound, also add SPI to the policy.
+ mSrvConfig
+ .getNetdInstance()
+ .ipSecUpdateSecurityPolicy(
+ 0, // Use 0 for reqId
+ direction,
+ "",
+ "",
+ transformInfo.getSpiRecord().getSpi(),
+ mark,
+ 0xffffffff);
+ }
+
+ // Update SA with tunnel mark (ikey or okey based on direction)
+ createOrUpdateTransform(c, transformResourceId, spiRecord, socketRecord);
+ } catch (ServiceSpecificException e) {
+ if (e.errorCode == EINVAL) {
+ throw new IllegalArgumentException(e.toString());
+ } else {
+ throw e;
+ }
+ }
+ }
+
@Override
protected synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
mContext.enforceCallingOrSelfPermission(DUMP, TAG);
diff --git a/com/android/server/LocationManagerService.java b/com/android/server/LocationManagerService.java
index 57c992fd..9aa588fc 100644
--- a/com/android/server/LocationManagerService.java
+++ b/com/android/server/LocationManagerService.java
@@ -16,36 +16,10 @@
package com.android.server;
-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;
-import com.android.internal.os.BackgroundThread;
-import com.android.internal.util.ArrayUtils;
-import com.android.internal.util.DumpUtils;
-import com.android.server.location.ActivityRecognitionProxy;
-import com.android.server.location.FlpHardwareProvider;
-import com.android.server.location.FusedProxy;
-import com.android.server.location.GeocoderProxy;
-import com.android.server.location.GeofenceManager;
-import com.android.server.location.GeofenceProxy;
-import com.android.server.location.GnssLocationProvider;
-import com.android.server.location.GnssMeasurementsProvider;
-import com.android.server.location.GnssNavigationMessageProvider;
-import com.android.server.location.LocationBlacklist;
-import com.android.server.location.LocationFudger;
-import com.android.server.location.LocationProviderInterface;
-import com.android.server.location.LocationProviderProxy;
-import com.android.server.location.LocationRequestStatistics;
-import com.android.server.location.LocationRequestStatistics.PackageProviderKey;
-import com.android.server.location.LocationRequestStatistics.PackageStatistics;
-import com.android.server.location.MockProvider;
-import com.android.server.location.PassiveProvider;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import android.annotation.NonNull;
+import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
@@ -56,8 +30,8 @@ import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.content.res.Resources;
@@ -69,10 +43,10 @@ import android.location.GeocoderParams;
import android.location.Geofence;
import android.location.IBatchedLocationCallback;
import android.location.IGnssMeasurementsListener;
+import android.location.IGnssNavigationMessageListener;
import android.location.IGnssStatusListener;
import android.location.IGnssStatusProvider;
import android.location.IGpsGeofenceHardware;
-import android.location.IGnssNavigationMessageListener;
import android.location.ILocationListener;
import android.location.ILocationManager;
import android.location.INetInitiatedListener;
@@ -95,10 +69,35 @@ import android.os.UserManager;
import android.os.WorkSource;
import android.provider.Settings;
import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.EventLog;
import android.util.Log;
import android.util.Slog;
-
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.location.ProviderProperties;
+import com.android.internal.location.ProviderRequest;
+import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.DumpUtils;
+import com.android.server.location.ActivityRecognitionProxy;
+import com.android.server.location.FlpHardwareProvider;
+import com.android.server.location.FusedProxy;
+import com.android.server.location.GeocoderProxy;
+import com.android.server.location.GeofenceManager;
+import com.android.server.location.GeofenceProxy;
+import com.android.server.location.GnssLocationProvider;
+import com.android.server.location.GnssMeasurementsProvider;
+import com.android.server.location.GnssNavigationMessageProvider;
+import com.android.server.location.LocationBlacklist;
+import com.android.server.location.LocationFudger;
+import com.android.server.location.LocationProviderInterface;
+import com.android.server.location.LocationProviderProxy;
+import com.android.server.location.LocationRequestStatistics;
+import com.android.server.location.LocationRequestStatistics.PackageProviderKey;
+import com.android.server.location.LocationRequestStatistics.PackageStatistics;
+import com.android.server.location.MockProvider;
+import com.android.server.location.PassiveProvider;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -232,10 +231,9 @@ public class LocationManagerService extends ILocationManager.Stub {
private final ArraySet<String> mBackgroundThrottlePackageWhitelist = new ArraySet<>();
- private final ArrayMap<IGnssMeasurementsListener, Identity> mGnssMeasurementsListeners =
- new ArrayMap<>();
+ private final ArrayMap<IBinder, Identity> mGnssMeasurementsListeners = new ArrayMap<>();
- private final ArrayMap<IGnssNavigationMessageListener, Identity>
+ private final ArrayMap<IBinder, Identity>
mGnssNavigationMessageListeners = new ArrayMap<>();
// current active user on the device - other users are denied location data
@@ -438,23 +436,23 @@ public class LocationManagerService extends ILocationManager.Stub {
applyRequirementsLocked(provider);
}
- for (Entry<IGnssMeasurementsListener, Identity> entry
- : mGnssMeasurementsListeners.entrySet()) {
+ for (Entry<IBinder, Identity> entry : mGnssMeasurementsListeners.entrySet()) {
if (entry.getValue().mUid == uid) {
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());
+ mGnssMeasurementsProvider.addListener(
+ IGnssMeasurementsListener.Stub.asInterface(entry.getKey()));
} else {
- mGnssMeasurementsProvider.removeListener(entry.getKey());
+ mGnssMeasurementsProvider.removeListener(
+ IGnssMeasurementsListener.Stub.asInterface(entry.getKey()));
}
}
}
- for (Entry<IGnssNavigationMessageListener, Identity> entry
- : mGnssNavigationMessageListeners.entrySet()) {
+ for (Entry<IBinder, Identity> entry : mGnssNavigationMessageListeners.entrySet()) {
if (entry.getValue().mUid == uid) {
if (D) {
Log.d(TAG, "gnss navigation message listener from uid "
@@ -462,9 +460,11 @@ public class LocationManagerService extends ILocationManager.Stub {
+ (foreground ? "foreground" : "background)"));
}
if (foreground || isThrottlingExemptLocked(entry.getValue())) {
- mGnssNavigationMessageProvider.addListener(entry.getKey());
+ mGnssNavigationMessageProvider.addListener(
+ IGnssNavigationMessageListener.Stub.asInterface(entry.getKey()));
} else {
- mGnssNavigationMessageProvider.removeListener(entry.getKey());
+ mGnssNavigationMessageProvider.removeListener(
+ IGnssNavigationMessageListener.Stub.asInterface(entry.getKey()));
}
}
}
@@ -1377,10 +1377,7 @@ public class LocationManagerService extends ILocationManager.Stub {
if (mDisabledProviders.contains(provider)) {
return false;
}
- // Use system settings
- ContentResolver resolver = mContext.getContentResolver();
-
- return Settings.Secure.isLocationProviderEnabledForUser(resolver, provider, mCurrentUserId);
+ return isLocationProviderEnabledForUser(provider, mCurrentUserId);
}
/**
@@ -1399,6 +1396,23 @@ public class LocationManagerService extends ILocationManager.Stub {
}
/**
+ * Returns "true" if access to the specified location provider is allowed by the specified
+ * user's settings. Access to all location providers is forbidden to non-location-provider
+ * processes belonging to background users.
+ *
+ * @param provider the name of the location provider
+ * @param uid the requestor's UID
+ * @param userId the user id to query
+ */
+ private boolean isAllowedByUserSettingsLockedForUser(
+ String provider, int uid, int userId) {
+ if (!isCurrentProfile(UserHandle.getUserId(uid)) && !isUidALocationProvider(uid)) {
+ return false;
+ }
+ return isLocationProviderEnabledForUser(provider, userId);
+ }
+
+ /**
* Returns the permission string associated with the specified resolution level.
*
* @param resolutionLevel the resolution level
@@ -1424,10 +1438,10 @@ public class LocationManagerService extends ILocationManager.Stub {
*/
private int getAllowedResolutionLevel(int pid, int uid) {
if (mContext.checkPermission(android.Manifest.permission.ACCESS_FINE_LOCATION,
- pid, uid) == PackageManager.PERMISSION_GRANTED) {
+ pid, uid) == PERMISSION_GRANTED) {
return RESOLUTION_LEVEL_FINE;
} else if (mContext.checkPermission(android.Manifest.permission.ACCESS_COARSE_LOCATION,
- pid, uid) == PackageManager.PERMISSION_GRANTED) {
+ pid, uid) == PERMISSION_GRANTED) {
return RESOLUTION_LEVEL_COARSE;
} else {
return RESOLUTION_LEVEL_NONE;
@@ -2052,7 +2066,7 @@ public class LocationManagerService extends ILocationManager.Stub {
}
boolean callerHasLocationHardwarePermission =
mContext.checkCallingPermission(android.Manifest.permission.LOCATION_HARDWARE)
- == PackageManager.PERMISSION_GRANTED;
+ == PERMISSION_GRANTED;
LocationRequest sanitizedRequest = createSanitizedRequest(request, allowedResolutionLevel,
callerHasLocationHardwarePermission);
@@ -2254,6 +2268,64 @@ public class LocationManagerService extends ILocationManager.Stub {
}
}
+ /**
+ * Provides an interface to inject and set the last location if location is not available
+ * currently.
+ *
+ * This helps in cases where the product (Cars for example) has saved the last known location
+ * before powering off. This interface lets the client inject the saved location while the GPS
+ * chipset is getting its first fix, there by improving user experience.
+ *
+ * @param location - Location object to inject
+ * @return true if update was successful, false if not
+ */
+ @Override
+ public boolean injectLocation(Location location) {
+ mContext.enforceCallingPermission(android.Manifest.permission.LOCATION_HARDWARE,
+ "Location Hardware permission not granted to inject location");
+ mContext.enforceCallingPermission(android.Manifest.permission.ACCESS_FINE_LOCATION,
+ "Access Fine Location permission not granted to inject Location");
+
+ if (location == null) {
+ if (D) {
+ Log.d(TAG, "injectLocation(): called with null location");
+ }
+ return false;
+ }
+ LocationProviderInterface p = null;
+ String provider = location.getProvider();
+ if (provider != null) {
+ p = mProvidersByName.get(provider);
+ }
+ if (p == null) {
+ if (D) {
+ Log.d(TAG, "injectLocation(): unknown provider");
+ }
+ return false;
+ }
+ synchronized (mLock) {
+ if (!isAllowedByCurrentUserSettingsLocked(provider)) {
+ if (D) {
+ Log.d(TAG, "Location disabled in Settings for current user:" + mCurrentUserId);
+ }
+ return false;
+ } else {
+ // NOTE: If last location is already available, location is not injected. If
+ // provider's normal source (like a GPS chipset) have already provided an output,
+ // there is no need to inject this location.
+ if (mLastLocation.get(provider) == null) {
+ updateLastLocationLocked(location, provider);
+ } else {
+ if (D) {
+ Log.d(TAG, "injectLocation(): Location exists. Not updating");
+ }
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
@Override
public void requestGeofence(LocationRequest request, Geofence geofence, PendingIntent intent,
String packageName) {
@@ -2267,7 +2339,7 @@ public class LocationManagerService extends ILocationManager.Stub {
// Require that caller can manage given document
boolean callerHasLocationHardwarePermission =
mContext.checkCallingPermission(android.Manifest.permission.LOCATION_HARDWARE)
- == PackageManager.PERMISSION_GRANTED;
+ == PERMISSION_GRANTED;
LocationRequest sanitizedRequest = createSanitizedRequest(request, allowedResolutionLevel,
callerHasLocationHardwarePermission);
@@ -2343,7 +2415,7 @@ public class LocationManagerService extends ILocationManager.Stub {
synchronized (mLock) {
Identity callerIdentity
= new Identity(Binder.getCallingUid(), Binder.getCallingPid(), packageName);
- mGnssMeasurementsListeners.put(listener, callerIdentity);
+ mGnssMeasurementsListeners.put(listener.asBinder(), callerIdentity);
long identity = Binder.clearCallingIdentity();
try {
if (isThrottlingExemptLocked(callerIdentity)
@@ -2363,7 +2435,7 @@ public class LocationManagerService extends ILocationManager.Stub {
public void removeGnssMeasurementsListener(IGnssMeasurementsListener listener) {
if (mGnssMeasurementsProvider != null) {
synchronized (mLock) {
- mGnssMeasurementsListeners.remove(listener);
+ mGnssMeasurementsListeners.remove(listener.asBinder());
mGnssMeasurementsProvider.removeListener(listener);
}
}
@@ -2380,7 +2452,7 @@ public class LocationManagerService extends ILocationManager.Stub {
synchronized (mLock) {
Identity callerIdentity
= new Identity(Binder.getCallingUid(), Binder.getCallingPid(), packageName);
- mGnssNavigationMessageListeners.put(listener, callerIdentity);
+ mGnssNavigationMessageListeners.put(listener.asBinder(), callerIdentity);
long identity = Binder.clearCallingIdentity();
try {
if (isThrottlingExemptLocked(callerIdentity)
@@ -2400,7 +2472,7 @@ public class LocationManagerService extends ILocationManager.Stub {
public void removeGnssNavigationMessageListener(IGnssNavigationMessageListener listener) {
if (mGnssNavigationMessageProvider != null) {
synchronized (mLock) {
- mGnssNavigationMessageListeners.remove(listener);
+ mGnssNavigationMessageListeners.remove(listener.asBinder());
mGnssNavigationMessageProvider.removeListener(listener);
}
}
@@ -2417,7 +2489,7 @@ public class LocationManagerService extends ILocationManager.Stub {
// and check for ACCESS_LOCATION_EXTRA_COMMANDS
if ((mContext.checkCallingOrSelfPermission(ACCESS_LOCATION_EXTRA_COMMANDS)
- != PackageManager.PERMISSION_GRANTED)) {
+ != PERMISSION_GRANTED)) {
throw new SecurityException("Requires ACCESS_LOCATION_EXTRA_COMMANDS permission");
}
@@ -2487,8 +2559,64 @@ public class LocationManagerService extends ILocationManager.Stub {
return null;
}
+ /**
+ * Method for enabling or disabling location.
+ *
+ * @param enabled true to enable location. false to disable location
+ * @param userId the user id to set
+ */
+ @Override
+ public void setLocationEnabledForUser(boolean enabled, int userId) {
+ // Check INTERACT_ACROSS_USERS permission if userId is not current user id.
+ checkInteractAcrossUsersPermission(userId);
+
+ // Enable or disable all location providers
+ synchronized (mLock) {
+ for(String provider : getAllProviders()) {
+ setProviderEnabledForUser(provider, enabled, userId);
+ }
+ }
+ }
+
+ /**
+ * Returns the current enabled/disabled status of location
+ *
+ * @param userId the user id to query
+ * @return true if location is enabled. false if location is disabled.
+ */
+ @Override
+ public boolean isLocationEnabledForUser(int userId) {
+ // Check INTERACT_ACROSS_USERS permission if userId is not current user id.
+ checkInteractAcrossUsersPermission(userId);
+
+ // If at least one location provider is enabled, return true
+ synchronized (mLock) {
+ for (String provider : getAllProviders()) {
+ if (isProviderEnabledForUser(provider, userId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
@Override
public boolean isProviderEnabled(String provider) {
+ return isProviderEnabledForUser(provider, UserHandle.getCallingUserId());
+ }
+
+ /**
+ * Method for determining if a location provider is enabled.
+ *
+ * @param provider the location provider to query
+ * @param userId the user id to query
+ * @return true if the provider is enabled
+ */
+ @Override
+ public boolean isProviderEnabledForUser(String provider, int userId) {
+ // Check INTERACT_ACROSS_USERS permission if userId is not current user id.
+ checkInteractAcrossUsersPermission(userId);
+
// Fused provider is accessed indirectly via criteria rather than the provider-based APIs,
// so we discourage its use
if (LocationManager.FUSED_PROVIDER.equals(provider)) return false;
@@ -2498,7 +2626,48 @@ public class LocationManagerService extends ILocationManager.Stub {
try {
synchronized (mLock) {
LocationProviderInterface p = mProvidersByName.get(provider);
- return p != null && isAllowedByUserSettingsLocked(provider, uid);
+ return p != null
+ && isAllowedByUserSettingsLockedForUser(provider, uid, userId);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Method for enabling or disabling a single location provider.
+ *
+ * @param provider the name of the provider
+ * @param enabled true to enable the provider. false to disable the provider
+ * @param userId the user id to set
+ * @return true if the value was set successfully. false on failure.
+ */
+ @Override
+ public boolean setProviderEnabledForUser(
+ String provider, boolean enabled, int userId) {
+ mContext.enforceCallingPermission(
+ android.Manifest.permission.WRITE_SECURE_SETTINGS,
+ "Requires WRITE_SECURE_SETTINGS permission");
+
+ // Check INTERACT_ACROSS_USERS permission if userId is not current user id.
+ checkInteractAcrossUsersPermission(userId);
+
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ synchronized (mLock) {
+ // to ensure thread safety, we write the provider name with a '+' or '-'
+ // and let the SettingsProvider handle it rather than reading and modifying
+ // the list of enabled providers.
+ if (enabled) {
+ provider = "+" + provider;
+ } else {
+ provider = "-" + provider;
+ }
+ return Settings.Secure.putStringForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.LOCATION_PROVIDERS_ALLOWED,
+ provider,
+ userId);
}
} finally {
Binder.restoreCallingIdentity(identity);
@@ -2506,6 +2675,43 @@ public class LocationManagerService extends ILocationManager.Stub {
}
/**
+ * Read location provider status from Settings.Secure
+ *
+ * @param provider the location provider to query
+ * @param userId the user id to query
+ * @return true if the provider is enabled
+ */
+ private boolean isLocationProviderEnabledForUser(String provider, int userId) {
+ long identity = Binder.clearCallingIdentity();
+ try {
+ // Use system settings
+ ContentResolver cr = mContext.getContentResolver();
+ String allowedProviders = Settings.Secure.getStringForUser(
+ cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED, userId);
+ return TextUtils.delimitedStringContains(allowedProviders, ',', provider);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
+ /**
+ * Method for checking INTERACT_ACROSS_USERS permission if specified user id is not the same as
+ * current user id
+ *
+ * @param userId the user id to get or set value
+ */
+ private void checkInteractAcrossUsersPermission(int userId) {
+ int uid = Binder.getCallingUid();
+ if (UserHandle.getUserId(uid) != userId) {
+ if (ActivityManager.checkComponentPermission(
+ android.Manifest.permission.INTERACT_ACROSS_USERS, uid, -1, true)
+ != PERMISSION_GRANTED) {
+ throw new SecurityException("Requires INTERACT_ACROSS_USERS permission");
+ }
+ }
+ }
+
+ /**
* Returns "true" if the UID belongs to a bound location provider.
*
* @param uid the uid
@@ -2526,7 +2732,7 @@ public class LocationManagerService extends ILocationManager.Stub {
private void checkCallerIsProvider() {
if (mContext.checkCallingOrSelfPermission(INSTALL_LOCATION_PROVIDER)
- == PackageManager.PERMISSION_GRANTED) {
+ == PERMISSION_GRANTED) {
return;
}
@@ -2614,30 +2820,18 @@ public class LocationManagerService extends ILocationManager.Stub {
private void handleLocationChangedLocked(Location location, boolean passive) {
if (D) Log.d(TAG, "incoming location: " + location);
-
long now = SystemClock.elapsedRealtime();
String provider = (passive ? LocationManager.PASSIVE_PROVIDER : location.getProvider());
-
// Skip if the provider is unknown.
LocationProviderInterface p = mProvidersByName.get(provider);
if (p == null) return;
-
- // Update last known locations
- Location noGPSLocation = location.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION);
- Location lastNoGPSLocation;
+ updateLastLocationLocked(location, provider);
+ // mLastLocation should have been updated from the updateLastLocationLocked call above.
Location lastLocation = mLastLocation.get(provider);
if (lastLocation == null) {
- lastLocation = new Location(provider);
- mLastLocation.put(provider, lastLocation);
- } else {
- lastNoGPSLocation = lastLocation.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION);
- if (noGPSLocation == null && lastNoGPSLocation != null) {
- // New location has no no-GPS location: adopt last no-GPS location. This is set
- // directly into location because we do not want to notify COARSE clients.
- location.setExtraLocation(Location.EXTRA_NO_GPS_LOCATION, lastNoGPSLocation);
- }
+ Log.e(TAG, "handleLocationChangedLocked() updateLastLocation failed");
+ return;
}
- lastLocation.set(location);
// Update last known coarse interval location if enough time has passed.
Location lastLocationCoarseInterval = mLastLocationCoarseInterval.get(provider);
@@ -2653,7 +2847,7 @@ public class LocationManagerService extends ILocationManager.Stub {
// Don't ever return a coarse location that is more recent than the allowed update
// interval (i.e. don't allow an app to keep registering and unregistering for
// location updates to overcome the minimum interval).
- noGPSLocation =
+ Location noGPSLocation =
lastLocationCoarseInterval.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION);
// Skip if there are no UpdateRecords for this provider.
@@ -2778,6 +2972,30 @@ public class LocationManagerService extends ILocationManager.Stub {
}
}
+ /**
+ * Updates last location with the given location
+ *
+ * @param location new location to update
+ * @param provider Location provider to update for
+ */
+ private void updateLastLocationLocked(Location location, String provider) {
+ Location noGPSLocation = location.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION);
+ Location lastNoGPSLocation;
+ Location lastLocation = mLastLocation.get(provider);
+ if (lastLocation == null) {
+ lastLocation = new Location(provider);
+ mLastLocation.put(provider, lastLocation);
+ } else {
+ lastNoGPSLocation = lastLocation.getExtraLocation(Location.EXTRA_NO_GPS_LOCATION);
+ if (noGPSLocation == null && lastNoGPSLocation != null) {
+ // New location has no no-GPS location: adopt last no-GPS location. This is set
+ // directly into location because we do not want to notify COARSE clients.
+ location.setExtraLocation(Location.EXTRA_NO_GPS_LOCATION, lastNoGPSLocation);
+ }
+ }
+ lastLocation.set(location);
+ }
+
private class LocationWorkerHandler extends Handler {
public LocationWorkerHandler(Looper looper) {
super(looper, null, true);
@@ -3110,6 +3328,16 @@ public class LocationManagerService extends ILocationManager.Stub {
pw.println(" " + record);
}
}
+ pw.println(" Active GnssMeasurement Listeners:");
+ for (Identity identity : mGnssMeasurementsListeners.values()) {
+ pw.println(" " + identity.mPid + " " + identity.mUid + " "
+ + identity.mPackageName + ": " + isThrottlingExemptLocked(identity));
+ }
+ pw.println(" Active GnssNavigationMessage Listeners:");
+ for (Identity identity : mGnssNavigationMessageListeners.values()) {
+ pw.println(" " + identity.mPid + " " + identity.mUid + " "
+ + identity.mPackageName + ": " + isThrottlingExemptLocked(identity));
+ }
pw.println(" Overlay Provider Packages:");
for (LocationProviderInterface provider : mProviders) {
if (provider instanceof LocationProviderProxy) {
diff --git a/com/android/server/NetworkManagementService.java b/com/android/server/NetworkManagementService.java
index 8a15ded2..88ae2247 100644
--- a/com/android/server/NetworkManagementService.java
+++ b/com/android/server/NetworkManagementService.java
@@ -18,11 +18,9 @@ package com.android.server;
import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
import static android.Manifest.permission.DUMP;
+import static android.Manifest.permission.NETWORK_SETTINGS;
import static android.Manifest.permission.NETWORK_STACK;
import static android.Manifest.permission.SHUTDOWN;
-import static android.net.ConnectivityManager.PRIVATE_DNS_DEFAULT_MODE;
-import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
-import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_DOZABLE;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_DOZABLE;
import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_NONE;
@@ -209,12 +207,6 @@ public class NetworkManagementService extends INetworkManagementService.Stub
public static final int StrictCleartext = 617;
}
- /* Defaults for resolver parameters. */
- public static final int DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS = 1800;
- public static final int DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT = 25;
- public static final int DNS_RESOLVER_DEFAULT_MIN_SAMPLES = 8;
- public static final int DNS_RESOLVER_DEFAULT_MAX_SAMPLES = 64;
-
/**
* String indicating a softap command.
*/
@@ -1768,6 +1760,8 @@ public class NetworkManagementService extends INetworkManagementService.Stub
@Override
public boolean setDataSaverModeEnabled(boolean enable) {
+ mContext.enforceCallingOrSelfPermission(NETWORK_SETTINGS, TAG);
+
if (DBG) Log.d(TAG, "setDataSaverMode: " + enable);
synchronized (mQuotaLock) {
if (mDataSaverMode == enable) {
@@ -1947,80 +1941,19 @@ public class NetworkManagementService extends INetworkManagementService.Stub
}
@Override
- public void setDnsConfigurationForNetwork(int netId, String[] servers, String domains) {
+ public void setDnsConfigurationForNetwork(int netId, String[] servers, String[] domains,
+ int[] params, boolean useTls, String tlsHostname) {
mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
- final ContentResolver cr = mContext.getContentResolver();
-
- int sampleValidity = Settings.Global.getInt(cr,
- Settings.Global.DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS,
- DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS);
- if (sampleValidity < 0 || sampleValidity > 65535) {
- Slog.w(TAG, "Invalid sampleValidity=" + sampleValidity + ", using default=" +
- DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS);
- sampleValidity = DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS;
- }
-
- int successThreshold = Settings.Global.getInt(cr,
- Settings.Global.DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT,
- DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT);
- if (successThreshold < 0 || successThreshold > 100) {
- Slog.w(TAG, "Invalid successThreshold=" + successThreshold + ", using default=" +
- DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT);
- successThreshold = DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT;
- }
-
- int minSamples = Settings.Global.getInt(cr,
- Settings.Global.DNS_RESOLVER_MIN_SAMPLES, DNS_RESOLVER_DEFAULT_MIN_SAMPLES);
- int maxSamples = Settings.Global.getInt(cr,
- Settings.Global.DNS_RESOLVER_MAX_SAMPLES, DNS_RESOLVER_DEFAULT_MAX_SAMPLES);
- if (minSamples < 0 || minSamples > maxSamples || maxSamples > 64) {
- Slog.w(TAG, "Invalid sample count (min, max)=(" + minSamples + ", " + maxSamples +
- "), using default=(" + DNS_RESOLVER_DEFAULT_MIN_SAMPLES + ", " +
- DNS_RESOLVER_DEFAULT_MAX_SAMPLES + ")");
- minSamples = DNS_RESOLVER_DEFAULT_MIN_SAMPLES;
- maxSamples = DNS_RESOLVER_DEFAULT_MAX_SAMPLES;
- }
-
- final String[] domainStrs = domains == null ? new String[0] : domains.split(" ");
- final int[] params = { sampleValidity, successThreshold, minSamples, maxSamples };
- final boolean useTls = shouldUseTls(cr);
- // TODO: Populate tlsHostname once it's decided how the hostname's IP
- // addresses will be resolved:
- //
- // [1] network-provided DNS servers are included here with the
- // hostname and netd will use the network-provided servers to
- // resolve the hostname and fix up its internal structures, or
- //
- // [2] network-provided DNS servers are included here without the
- // hostname, the ConnectivityService layer resolves the given
- // hostname, and then reconfigures netd with this information.
- //
- // In practice, there will always be a need for ConnectivityService or
- // the captive portal app to use the network-provided services to make
- // some queries. This argues in favor of [1], in concert with another
- // mechanism, perhaps setting a high bit in the netid, to indicate
- // via existing DNS APIs which set of servers (network-provided or
- // non-network-provided private DNS) should be queried.
- final String tlsHostname = "";
final String[] tlsFingerprints = new String[0];
try {
- mNetdService.setResolverConfiguration(netId, servers, domainStrs, params,
- useTls, tlsHostname, tlsFingerprints);
+ mNetdService.setResolverConfiguration(
+ netId, servers, domains, params, useTls, tlsHostname, tlsFingerprints);
} catch (RemoteException e) {
throw new RuntimeException(e);
}
}
- private static boolean shouldUseTls(ContentResolver cr) {
- String privateDns = Settings.Global.getString(cr, Settings.Global.PRIVATE_DNS_MODE);
- if (TextUtils.isEmpty(privateDns)) {
- privateDns = PRIVATE_DNS_DEFAULT_MODE;
- }
- return privateDns.equals(PRIVATE_DNS_MODE_OPPORTUNISTIC) ||
- privateDns.startsWith(PRIVATE_DNS_MODE_PROVIDER_HOSTNAME);
- }
-
@Override
public void addVpnUidRanges(int netId, UidRange[] ranges) {
mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
@@ -2563,12 +2496,16 @@ public class NetworkManagementService extends INetworkManagementService.Stub
@Override
public void removeNetwork(int netId) {
- mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);
+ mContext.enforceCallingOrSelfPermission(NETWORK_STACK, TAG);
try {
- mConnector.execute("network", "destroy", netId);
- } catch (NativeDaemonConnectorException e) {
- throw e.rethrowAsParcelableException();
+ mNetdService.networkDestroy(netId);
+ } catch (ServiceSpecificException e) {
+ Log.w(TAG, "removeNetwork(" + netId + "): ", e);
+ throw e;
+ } catch (RemoteException e) {
+ Log.w(TAG, "removeNetwork(" + netId + "): ", e);
+ throw e.rethrowAsRuntimeException();
}
}
diff --git a/com/android/server/NetworkScoreService.java b/com/android/server/NetworkScoreService.java
index 44c02270..33f77697 100644
--- a/com/android/server/NetworkScoreService.java
+++ b/com/android/server/NetworkScoreService.java
@@ -26,6 +26,7 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
+import android.content.pm.PackageManagerInternal;
import android.database.ContentObserver;
import android.location.LocationManager;
import android.net.INetworkRecommendationProvider;
@@ -50,14 +51,17 @@ import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings.Global;
+import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.IntArray;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.os.TransferPipe;
+import com.android.internal.telephony.SmsApplication;
import com.android.internal.util.DumpUtils;
import java.io.FileDescriptor;
@@ -91,7 +95,8 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
private final Object mPackageMonitorLock = new Object();
private final Object mServiceConnectionLock = new Object();
private final Handler mHandler;
- private final DispatchingContentObserver mContentObserver;
+ private final DispatchingContentObserver mRecommendationSettingsObserver;
+ private final ContentObserver mUseOpenWifiPackageObserver;
private final Function<NetworkScorerAppData, ScoringServiceConnection> mServiceConnProducer;
@GuardedBy("mPackageMonitorLock")
@@ -255,8 +260,40 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
mContext.registerReceiverAsUser(
mLocationModeReceiver, UserHandle.SYSTEM, locationModeFilter,
null /* broadcastPermission*/, mHandler);
- mContentObserver = new DispatchingContentObserver(context, mHandler);
+ mRecommendationSettingsObserver = new DispatchingContentObserver(context, mHandler);
mServiceConnProducer = serviceConnProducer;
+ mUseOpenWifiPackageObserver = new ContentObserver(mHandler) {
+ @Override
+ public void onChange(boolean selfChange, Uri uri, int userId) {
+ Uri useOpenWifiPkgUri = Global.getUriFor(Global.USE_OPEN_WIFI_PACKAGE);
+ if (useOpenWifiPkgUri.equals(uri)) {
+ String useOpenWifiPackage = Global.getString(mContext.getContentResolver(),
+ Global.USE_OPEN_WIFI_PACKAGE);
+ if (!TextUtils.isEmpty(useOpenWifiPackage)) {
+ LocalServices.getService(PackageManagerInternal.class)
+ .grantDefaultPermissionsToDefaultUseOpenWifiApp(useOpenWifiPackage,
+ userId);
+ }
+ }
+ }
+ };
+ mContext.getContentResolver().registerContentObserver(
+ Global.getUriFor(Global.USE_OPEN_WIFI_PACKAGE),
+ false /*notifyForDescendants*/,
+ mUseOpenWifiPackageObserver);
+ // Set a callback for the package manager to query the use open wifi app.
+ LocalServices.getService(PackageManagerInternal.class).setUseOpenWifiAppPackagesProvider(
+ new PackageManagerInternal.PackagesProvider() {
+ @Override
+ public String[] getPackages(int userId) {
+ String useOpenWifiPackage = Global.getString(mContext.getContentResolver(),
+ Global.USE_OPEN_WIFI_PACKAGE);
+ if (!TextUtils.isEmpty(useOpenWifiPackage)) {
+ return new String[]{useOpenWifiPackage};
+ }
+ return null;
+ }
+ });
}
/** Called when the system is ready to run third-party code but before it actually does so. */
@@ -287,11 +324,11 @@ public class NetworkScoreService extends INetworkScoreService.Stub {
private void registerRecommendationSettingsObserver() {
final Uri packageNameUri = Global.getUriFor(Global.NETWORK_RECOMMENDATIONS_PACKAGE);
- mContentObserver.observe(packageNameUri,
+ mRecommendationSettingsObserver.observe(packageNameUri,
ServiceHandler.MSG_RECOMMENDATIONS_PACKAGE_CHANGED);
final Uri settingUri = Global.getUriFor(Global.NETWORK_RECOMMENDATIONS_ENABLED);
- mContentObserver.observe(settingUri,
+ mRecommendationSettingsObserver.observe(settingUri,
ServiceHandler.MSG_RECOMMENDATION_ENABLED_SETTING_CHANGED);
}
diff --git a/com/android/server/PersistentDataBlockManagerInternal.java b/com/android/server/PersistentDataBlockManagerInternal.java
index 80f8e519..1e9a0074 100644
--- a/com/android/server/PersistentDataBlockManagerInternal.java
+++ b/com/android/server/PersistentDataBlockManagerInternal.java
@@ -24,6 +24,13 @@ public interface PersistentDataBlockManagerInternal {
/** Stores the handle to a lockscreen credential to be used for Factory Reset Protection. */
void setFrpCredentialHandle(byte[] handle);
- /** Retrieves handle to a lockscreen credential to be used for Factory Reset Protection. */
+ /**
+ * Retrieves handle to a lockscreen credential to be used for Factory Reset Protection.
+ *
+ * @throws IllegalStateException if the underlying storage is corrupt or inaccessible.
+ */
byte[] getFrpCredentialHandle();
+
+ /** Update the OEM unlock enabled bit, bypassing user restriction checks. */
+ void forceOemUnlockEnabled(boolean enabled);
}
diff --git a/com/android/server/PersistentDataBlockService.java b/com/android/server/PersistentDataBlockService.java
index c32a2d10..21093b9f 100644
--- a/com/android/server/PersistentDataBlockService.java
+++ b/com/android/server/PersistentDataBlockService.java
@@ -28,6 +28,7 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.service.persistentdata.IPersistentDataBlockService;
import android.service.persistentdata.PersistentDataBlockManager;
+import android.util.Log;
import android.util.Slog;
import com.android.internal.R;
@@ -582,7 +583,12 @@ public class PersistentDataBlockService extends SystemService {
@Override
public boolean hasFrpCredentialHandle() {
enforcePersistentDataBlockAccess();
- return mInternalService.getFrpCredentialHandle() != null;
+ try {
+ return mInternalService.getFrpCredentialHandle() != null;
+ } catch (IllegalStateException e) {
+ Slog.e(TAG, "error reading frp handle", e);
+ throw new UnsupportedOperationException("cannot read frp credential");
+ }
}
};
@@ -638,7 +644,7 @@ public class PersistentDataBlockService extends SystemService {
@Override
public byte[] getFrpCredentialHandle() {
if (!enforceChecksumValidity()) {
- return null;
+ throw new IllegalStateException("invalid checksum");
}
DataInputStream inputStream;
@@ -646,8 +652,7 @@ public class PersistentDataBlockService extends SystemService {
inputStream = new DataInputStream(
new FileInputStream(new File(mDataBlockFile)));
} catch (FileNotFoundException e) {
- Slog.e(TAG, "partition not available");
- return null;
+ throw new IllegalStateException("frp partition not available");
}
try {
@@ -662,11 +667,18 @@ public class PersistentDataBlockService extends SystemService {
return bytes;
}
} catch (IOException e) {
- Slog.e(TAG, "unable to access persistent partition", e);
- return null;
+ throw new IllegalStateException("frp handle not readable", e);
} finally {
IoUtils.closeQuietly(inputStream);
}
}
+
+ @Override
+ public void forceOemUnlockEnabled(boolean enabled) {
+ synchronized (mLock) {
+ doSetOemUnlockEnabledLocked(enabled);
+ computeAndWriteDigestLocked();
+ }
+ }
};
}
diff --git a/com/android/server/StorageManagerService.java b/com/android/server/StorageManagerService.java
index 6a0d3ff9..7361e70a 100644
--- a/com/android/server/StorageManagerService.java
+++ b/com/android/server/StorageManagerService.java
@@ -49,6 +49,7 @@ import android.content.pm.ProviderInfo;
import android.content.pm.UserInfo;
import android.content.res.Configuration;
import android.content.res.ObbInfo;
+import android.database.ContentObserver;
import android.net.TrafficStats;
import android.net.Uri;
import android.os.Binder;
@@ -96,6 +97,7 @@ import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.AtomicFile;
+import android.util.DataUnit;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
@@ -169,6 +171,10 @@ class StorageManagerService extends IStorageManager.Stub
// Static direct instance pointer for the tightly-coupled idle service to use
static StorageManagerService sSelf = null;
+ /* Read during boot to decide whether to enable zram when available */
+ private static final String ZRAM_ENABLED_PROPERTY =
+ "persist.sys.zram_enabled";
+
public static class Lifecycle extends SystemService {
private StorageManagerService mStorageManagerService;
@@ -732,6 +738,41 @@ class StorageManagerService extends IStorageManager.Stub
// Start scheduling nominally-daily fstrim operations
MountServiceIdler.scheduleIdlePass(mContext);
+
+ // Toggle zram-enable system property in response to settings
+ mContext.getContentResolver().registerContentObserver(
+ Settings.Global.getUriFor(Settings.Global.ZRAM_ENABLED),
+ false /*notifyForDescendants*/,
+ new ContentObserver(null /* current thread */) {
+ @Override
+ public void onChange(boolean selfChange) {
+ refreshZramSettings();
+ }
+ });
+ refreshZramSettings();
+ }
+
+ /**
+ * Update the zram_enabled system property (which init reads to
+ * decide whether to enable zram) to reflect the zram_enabled
+ * preference (which we can change for experimentation purposes).
+ */
+ private void refreshZramSettings() {
+ String propertyValue = SystemProperties.get(ZRAM_ENABLED_PROPERTY);
+ if ("".equals(propertyValue)) {
+ return; // System doesn't have zram toggling support
+ }
+ String desiredPropertyValue =
+ Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.ZRAM_ENABLED,
+ 1) != 0
+ ? "1" : "0";
+ if (!desiredPropertyValue.equals(propertyValue)) {
+ // Avoid redundant disk writes by setting only if we're
+ // changing the property value. There's no race: we're the
+ // sole writer.
+ SystemProperties.set(ZRAM_ENABLED_PROPERTY, desiredPropertyValue);
+ }
}
/**
@@ -968,11 +1009,6 @@ class StorageManagerService extends IStorageManager.Stub
|| mForceAdoptable) {
flags |= DiskInfo.FLAG_ADOPTABLE;
}
- // Adoptable storage isn't currently supported on FBE devices
- if (StorageManager.isFileEncryptedNativeOnly()
- && !SystemProperties.getBoolean(StorageManager.PROP_ADOPTABLE_FBE, false)) {
- flags &= ~DiskInfo.FLAG_ADOPTABLE;
- }
mDisks.put(diskId, new DiskInfo(diskId, flags));
}
}
@@ -1910,12 +1946,6 @@ class StorageManagerService extends IStorageManager.Stub
}
if ((mask & StorageManager.DEBUG_FORCE_ADOPTABLE) != 0) {
- if (StorageManager.isFileEncryptedNativeOnly()
- && !SystemProperties.getBoolean(StorageManager.PROP_ADOPTABLE_FBE, false)) {
- throw new IllegalStateException(
- "Adoptable storage not available on device with native FBE");
- }
-
synchronized (mLock) {
mForceAdoptable = (flags & StorageManager.DEBUG_FORCE_ADOPTABLE) != 0;
@@ -2201,7 +2231,7 @@ class StorageManagerService extends IStorageManager.Stub
}
try {
- mVold.fdeEnable(type, password, IVold.ENCRYPTION_FLAG_IN_PLACE);
+ mVold.fdeEnable(type, password, 0);
} catch (Exception e) {
Slog.wtf(TAG, e);
return -1;
@@ -3519,8 +3549,8 @@ class StorageManagerService extends IStorageManager.Stub
pw.print(") total size: ");
pw.print(pair.second);
pw.print(" (");
- pw.print((float) pair.second / TrafficStats.GB_IN_BYTES);
- pw.println(" GB)");
+ pw.print(DataUnit.MEBIBYTES.toBytes(pair.second));
+ pw.println(" MiB)");
}
pw.println("Force adoptable: " + mForceAdoptable);
pw.println();
diff --git a/com/android/server/SystemConfig.java b/com/android/server/SystemConfig.java
index b7a67192..c5af8972 100644
--- a/com/android/server/SystemConfig.java
+++ b/com/android/server/SystemConfig.java
@@ -633,6 +633,11 @@ public class SystemConfig {
addFeature(PackageManager.FEATURE_SECURELY_REMOVES_USERS, 0);
}
+ // Help legacy devices that may not have updated their static config
+ if (StorageManager.hasAdoptable()) {
+ addFeature(PackageManager.FEATURE_ADOPTABLE_STORAGE, 0);
+ }
+
if (ActivityManager.isLowRamDeviceStatic()) {
addFeature(PackageManager.FEATURE_RAM_LOW, 0);
} else {
diff --git a/com/android/server/SystemServer.java b/com/android/server/SystemServer.java
index 4310a98d..94a356e6 100644
--- a/com/android/server/SystemServer.java
+++ b/com/android/server/SystemServer.java
@@ -46,10 +46,10 @@ import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
import android.os.storage.IStorageManager;
-import android.util.TimingsTraceLog;
import android.util.DisplayMetrics;
import android.util.EventLog;
import android.util.Slog;
+import android.util.TimingsTraceLog;
import android.view.WindowManager;
import com.android.internal.R;
@@ -57,20 +57,21 @@ import com.android.internal.app.ColorDisplayController;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.os.BinderInternal;
-import com.android.internal.util.EmergencyAffordanceManager;
import com.android.internal.util.ConcurrentUtils;
+import com.android.internal.util.EmergencyAffordanceManager;
import com.android.internal.widget.ILockSettings;
import com.android.server.accessibility.AccessibilityManagerService;
import com.android.server.am.ActivityManagerService;
import com.android.server.audio.AudioService;
+import com.android.server.broadcastradio.BroadcastRadioService;
import com.android.server.camera.CameraServiceProxy;
import com.android.server.car.CarServiceHelperService;
import com.android.server.clipboard.ClipboardService;
import com.android.server.connectivity.IpConnectivityMetrics;
import com.android.server.coverage.CoverageService;
import com.android.server.devicepolicy.DevicePolicyManagerService;
-import com.android.server.display.DisplayManagerService;
import com.android.server.display.ColorDisplayService;
+import com.android.server.display.DisplayManagerService;
import com.android.server.dreams.DreamManagerService;
import com.android.server.emergency.EmergencyAffordanceService;
import com.android.server.fingerprint.FingerprintService;
@@ -80,6 +81,7 @@ import com.android.server.job.JobSchedulerService;
import com.android.server.lights.LightsService;
import com.android.server.media.MediaResourceMonitorService;
import com.android.server.media.MediaRouterService;
+import com.android.server.media.MediaUpdateService;
import com.android.server.media.MediaSessionService;
import com.android.server.media.projection.MediaProjectionManagerService;
import com.android.server.net.NetworkPolicyManagerService;
@@ -91,17 +93,16 @@ import com.android.server.om.OverlayManagerService;
import com.android.server.os.DeviceIdentifiersPolicyService;
import com.android.server.os.SchedulingPolicyService;
import com.android.server.pm.BackgroundDexOptService;
+import com.android.server.pm.CrossProfileAppsService;
import com.android.server.pm.Installer;
import com.android.server.pm.LauncherAppsService;
import com.android.server.pm.OtaDexoptService;
import com.android.server.pm.PackageManagerService;
import com.android.server.pm.ShortcutService;
import com.android.server.pm.UserManagerService;
-import com.android.server.pm.crossprofile.CrossProfileAppsService;
import com.android.server.policy.PhoneWindowManager;
import com.android.server.power.PowerManagerService;
import com.android.server.power.ShutdownThread;
-import com.android.server.broadcastradio.BroadcastRadioService;
import com.android.server.restrictions.RestrictionsManagerService;
import com.android.server.security.KeyAttestationApplicationIdProviderService;
import com.android.server.security.KeyChainSystemService;
@@ -1171,6 +1172,15 @@ public final class SystemServer {
}
traceEnd();
+ traceBeginAndSlog("StartSystemUpdateManagerService");
+ try {
+ ServiceManager.addService(Context.SYSTEM_UPDATE_SERVICE,
+ new SystemUpdateManagerService(context));
+ } catch (Throwable e) {
+ reportWtf("starting SystemUpdateManagerService", e);
+ }
+ traceEnd();
+
traceBeginAndSlog("StartUpdateLockService");
try {
ServiceManager.addService(Context.UPDATE_LOCK_SERVICE,
@@ -1442,6 +1452,10 @@ public final class SystemServer {
mSystemServiceManager.startService(MediaSessionService.class);
traceEnd();
+ traceBeginAndSlog("StartMediaUpdateService");
+ mSystemServiceManager.startService(MediaUpdateService.class);
+ traceEnd();
+
if (mPackageManager.hasSystemFeature(PackageManager.FEATURE_HDMI_CEC)) {
traceBeginAndSlog("StartHdmiControlService");
mSystemServiceManager.startService(HdmiControlService.class);
diff --git a/com/android/server/SystemUpdateManagerService.java b/com/android/server/SystemUpdateManagerService.java
new file mode 100644
index 00000000..6c1ffdd3
--- /dev/null
+++ b/com/android/server/SystemUpdateManagerService.java
@@ -0,0 +1,255 @@
+/*
+ * 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 com.android.server;
+
+import static android.os.SystemUpdateManager.KEY_STATUS;
+import static android.os.SystemUpdateManager.STATUS_IDLE;
+import static android.os.SystemUpdateManager.STATUS_UNKNOWN;
+
+import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
+import static org.xmlpull.v1.XmlPullParser.END_TAG;
+import static org.xmlpull.v1.XmlPullParser.START_TAG;
+
+import android.Manifest;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.os.ISystemUpdateManager;
+import android.os.PersistableBundle;
+import android.os.SystemUpdateManager;
+import android.provider.Settings;
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.util.Xml;
+
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+public class SystemUpdateManagerService extends ISystemUpdateManager.Stub {
+
+ private static final String TAG = "SystemUpdateManagerService";
+
+ private static final int UID_UNKNOWN = -1;
+
+ private static final String INFO_FILE = "system-update-info.xml";
+ private static final int INFO_FILE_VERSION = 0;
+ private static final String TAG_INFO = "info";
+ private static final String KEY_VERSION = "version";
+ private static final String KEY_UID = "uid";
+ private static final String KEY_BOOT_COUNT = "boot-count";
+ private static final String KEY_INFO_BUNDLE = "info-bundle";
+
+ private final Context mContext;
+ private final AtomicFile mFile;
+ private final Object mLock = new Object();
+ private int mLastUid = UID_UNKNOWN;
+ private int mLastStatus = STATUS_UNKNOWN;
+
+ public SystemUpdateManagerService(Context context) {
+ mContext = context;
+ mFile = new AtomicFile(new File(Environment.getDataSystemDirectory(), INFO_FILE));
+
+ // Populate mLastUid and mLastStatus.
+ synchronized (mLock) {
+ loadSystemUpdateInfoLocked();
+ }
+ }
+
+ @Override
+ public void updateSystemUpdateInfo(PersistableBundle infoBundle) {
+ mContext.enforceCallingOrSelfPermission(Manifest.permission.RECOVERY, TAG);
+
+ int status = infoBundle.getInt(KEY_STATUS, STATUS_UNKNOWN);
+ if (status == STATUS_UNKNOWN) {
+ Slog.w(TAG, "Invalid status info. Ignored");
+ return;
+ }
+
+ // There could be multiple updater apps running on a device. But only one at most should
+ // be active (i.e. with a pending update), with the rest reporting idle status. We will
+ // only accept the reported status if any of the following conditions holds:
+ // a) none has been reported before;
+ // b) the current on-file status was last reported by the same caller;
+ // c) an active update is being reported.
+ int uid = Binder.getCallingUid();
+ if (mLastUid == UID_UNKNOWN || mLastUid == uid || status != STATUS_IDLE) {
+ synchronized (mLock) {
+ saveSystemUpdateInfoLocked(infoBundle, uid);
+ }
+ } else {
+ Slog.i(TAG, "Inactive updater reporting IDLE status. Ignored");
+ }
+ }
+
+ @Override
+ public Bundle retrieveSystemUpdateInfo() {
+ if (mContext.checkCallingOrSelfPermission(Manifest.permission.READ_SYSTEM_UPDATE_INFO)
+ == PackageManager.PERMISSION_DENIED
+ && mContext.checkCallingOrSelfPermission(Manifest.permission.RECOVERY)
+ == PackageManager.PERMISSION_DENIED) {
+ throw new SecurityException("Can't read system update info. Requiring "
+ + "READ_SYSTEM_UPDATE_INFO or RECOVERY permission.");
+ }
+
+ synchronized (mLock) {
+ return loadSystemUpdateInfoLocked();
+ }
+ }
+
+ // Reads and validates the info file. Returns the loaded info bundle on success; or a default
+ // info bundle with UNKNOWN status.
+ private Bundle loadSystemUpdateInfoLocked() {
+ PersistableBundle loadedBundle = null;
+ try (FileInputStream fis = mFile.openRead()) {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(fis, StandardCharsets.UTF_8.name());
+ loadedBundle = readInfoFileLocked(parser);
+ } catch (FileNotFoundException e) {
+ Slog.i(TAG, "No existing info file " + mFile.getBaseFile());
+ } catch (XmlPullParserException e) {
+ Slog.e(TAG, "Failed to parse the info file:", e);
+ } catch (IOException e) {
+ Slog.e(TAG, "Failed to read the info file:", e);
+ }
+
+ // Validate the loaded bundle.
+ if (loadedBundle == null) {
+ return removeInfoFileAndGetDefaultInfoBundleLocked();
+ }
+
+ int version = loadedBundle.getInt(KEY_VERSION, -1);
+ if (version == -1) {
+ Slog.w(TAG, "Invalid info file (invalid version). Ignored");
+ return removeInfoFileAndGetDefaultInfoBundleLocked();
+ }
+
+ int lastUid = loadedBundle.getInt(KEY_UID, -1);
+ if (lastUid == -1) {
+ Slog.w(TAG, "Invalid info file (invalid UID). Ignored");
+ return removeInfoFileAndGetDefaultInfoBundleLocked();
+ }
+
+ int lastBootCount = loadedBundle.getInt(KEY_BOOT_COUNT, -1);
+ if (lastBootCount == -1 || lastBootCount != getBootCount()) {
+ Slog.w(TAG, "Outdated info file. Ignored");
+ return removeInfoFileAndGetDefaultInfoBundleLocked();
+ }
+
+ PersistableBundle infoBundle = loadedBundle.getPersistableBundle(KEY_INFO_BUNDLE);
+ if (infoBundle == null) {
+ Slog.w(TAG, "Invalid info file (missing info). Ignored");
+ return removeInfoFileAndGetDefaultInfoBundleLocked();
+ }
+
+ int lastStatus = infoBundle.getInt(KEY_STATUS, STATUS_UNKNOWN);
+ if (lastStatus == STATUS_UNKNOWN) {
+ Slog.w(TAG, "Invalid info file (invalid status). Ignored");
+ return removeInfoFileAndGetDefaultInfoBundleLocked();
+ }
+
+ // Everything looks good upon reaching this point.
+ mLastStatus = lastStatus;
+ mLastUid = lastUid;
+ return new Bundle(infoBundle);
+ }
+
+ private void saveSystemUpdateInfoLocked(PersistableBundle infoBundle, int uid) {
+ // Wrap the incoming bundle with extra info (e.g. version, uid, boot count). We use nested
+ // PersistableBundle to avoid manually parsing XML attributes when loading the info back.
+ PersistableBundle outBundle = new PersistableBundle();
+ outBundle.putPersistableBundle(KEY_INFO_BUNDLE, infoBundle);
+ outBundle.putInt(KEY_VERSION, INFO_FILE_VERSION);
+ outBundle.putInt(KEY_UID, uid);
+ outBundle.putInt(KEY_BOOT_COUNT, getBootCount());
+
+ // Only update the info on success.
+ if (writeInfoFileLocked(outBundle)) {
+ mLastUid = uid;
+ mLastStatus = infoBundle.getInt(KEY_STATUS);
+ }
+ }
+
+ // Performs I/O work only, without validating the loaded info.
+ @Nullable
+ private PersistableBundle readInfoFileLocked(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ int type;
+ while ((type = parser.next()) != END_DOCUMENT) {
+ if (type == START_TAG && TAG_INFO.equals(parser.getName())) {
+ return PersistableBundle.restoreFromXml(parser);
+ }
+ }
+ return null;
+ }
+
+ private boolean writeInfoFileLocked(PersistableBundle outBundle) {
+ FileOutputStream fos = null;
+ try {
+ fos = mFile.startWrite();
+
+ XmlSerializer out = new FastXmlSerializer();
+ out.setOutput(fos, StandardCharsets.UTF_8.name());
+ out.startDocument(null, true);
+
+ out.startTag(null, TAG_INFO);
+ outBundle.saveToXml(out);
+ out.endTag(null, TAG_INFO);
+
+ out.endDocument();
+ mFile.finishWrite(fos);
+ return true;
+ } catch (IOException | XmlPullParserException e) {
+ Slog.e(TAG, "Failed to save the info file:", e);
+ if (fos != null) {
+ mFile.failWrite(fos);
+ }
+ }
+ return false;
+ }
+
+ private Bundle removeInfoFileAndGetDefaultInfoBundleLocked() {
+ if (mFile.exists()) {
+ Slog.i(TAG, "Removing info file");
+ mFile.delete();
+ }
+
+ mLastStatus = STATUS_UNKNOWN;
+ mLastUid = UID_UNKNOWN;
+ Bundle infoBundle = new Bundle();
+ infoBundle.putInt(KEY_STATUS, STATUS_UNKNOWN);
+ return infoBundle;
+ }
+
+ private int getBootCount() {
+ return Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.BOOT_COUNT, 0);
+ }
+}
diff --git a/com/android/server/TelephonyRegistry.java b/com/android/server/TelephonyRegistry.java
index 831c9cbc..6747be34 100644
--- a/com/android/server/TelephonyRegistry.java
+++ b/com/android/server/TelephonyRegistry.java
@@ -147,6 +147,8 @@ class TelephonyRegistry extends ITelephonyRegistry.Stub {
private int[] mDataActivationState;
+ private boolean[] mUserMobileDataState;
+
private SignalStrength[] mSignalStrength;
private boolean[] mMessageWaiting;
@@ -304,6 +306,7 @@ class TelephonyRegistry extends ITelephonyRegistry.Stub {
mServiceState = new ServiceState[numPhones];
mVoiceActivationState = new int[numPhones];
mDataActivationState = new int[numPhones];
+ mUserMobileDataState = new boolean[numPhones];
mSignalStrength = new SignalStrength[numPhones];
mMessageWaiting = new boolean[numPhones];
mCallForwarding = new boolean[numPhones];
@@ -320,6 +323,7 @@ class TelephonyRegistry extends ITelephonyRegistry.Stub {
mCallIncomingNumber[i] = "";
mServiceState[i] = new ServiceState();
mSignalStrength[i] = new SignalStrength();
+ mUserMobileDataState[i] = false;
mMessageWaiting[i] = false;
mCallForwarding[i] = false;
mCellLocation[i] = new Bundle();
@@ -656,6 +660,13 @@ class TelephonyRegistry extends ITelephonyRegistry.Stub {
remove(r.binder);
}
}
+ if ((events & PhoneStateListener.LISTEN_USER_MOBILE_DATA_STATE) != 0) {
+ try {
+ r.callback.onUserMobileDataStateChanged(mUserMobileDataState[phoneId]);
+ } catch (RemoteException ex) {
+ remove(r.binder);
+ }
+ }
}
}
} else {
@@ -1012,6 +1023,33 @@ class TelephonyRegistry extends ITelephonyRegistry.Stub {
}
}
+ public void notifyUserMobileDataStateChangedForPhoneId(int phoneId, int subId, boolean state) {
+ if (!checkNotifyPermission("notifyUserMobileDataStateChanged()")) {
+ return;
+ }
+ if (VDBG) {
+ log("notifyUserMobileDataStateChangedForSubscriberPhoneID: subId=" + phoneId
+ + " state=" + state);
+ }
+ synchronized (mRecords) {
+ if (validatePhoneId(phoneId)) {
+ mMessageWaiting[phoneId] = state;
+ for (Record r : mRecords) {
+ if (r.matchPhoneStateListenerEvent(
+ PhoneStateListener.LISTEN_USER_MOBILE_DATA_STATE) &&
+ idMatch(r.subId, subId, phoneId)) {
+ try {
+ r.callback.onUserMobileDataStateChanged(state);
+ } catch (RemoteException ex) {
+ mRemoveList.add(r.binder);
+ }
+ }
+ }
+ }
+ handleRemoveListLocked();
+ }
+ }
+
public void notifyCallForwardingChanged(boolean cfi) {
notifyCallForwardingChangedForSubscriber(SubscriptionManager.DEFAULT_SUBSCRIPTION_ID, cfi);
}
@@ -1374,6 +1412,7 @@ class TelephonyRegistry extends ITelephonyRegistry.Stub {
pw.println("mServiceState=" + mServiceState[i]);
pw.println("mVoiceActivationState= " + mVoiceActivationState[i]);
pw.println("mDataActivationState= " + mDataActivationState[i]);
+ pw.println("mUserMobileDataState= " + mUserMobileDataState[i]);
pw.println("mSignalStrength=" + mSignalStrength[i]);
pw.println("mMessageWaiting=" + mMessageWaiting[i]);
pw.println("mCallForwarding=" + mCallForwarding[i]);
@@ -1755,6 +1794,18 @@ class TelephonyRegistry extends ITelephonyRegistry.Stub {
}
}
+ if ((events & PhoneStateListener.LISTEN_USER_MOBILE_DATA_STATE) != 0) {
+ try {
+ if (VDBG) {
+ log("checkPossibleMissNotify: onUserMobileDataStateChanged phoneId="
+ + phoneId + " umds=" + mUserMobileDataState[phoneId]);
+ }
+ r.callback.onUserMobileDataStateChanged(mUserMobileDataState[phoneId]);
+ } catch (RemoteException ex) {
+ mRemoveList.add(r.binder);
+ }
+ }
+
if ((events & PhoneStateListener.LISTEN_MESSAGE_WAITING_INDICATOR) != 0) {
try {
if (VDBG) {
diff --git a/com/android/server/VibratorService.java b/com/android/server/VibratorService.java
index 0e51fda0..c1cda985 100644
--- a/com/android/server/VibratorService.java
+++ b/com/android/server/VibratorService.java
@@ -26,6 +26,7 @@ import android.content.res.Resources;
import android.database.ContentObserver;
import android.hardware.input.InputManager;
import android.hardware.vibrator.V1_0.Constants.EffectStrength;
+import android.icu.text.DateFormat;
import android.media.AudioManager;
import android.os.PowerManager.ServiceType;
import android.os.PowerSaveState;
@@ -62,6 +63,7 @@ import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.LinkedList;
+import java.util.Date;
public class VibratorService extends IVibratorService.Stub
implements InputManager.InputDeviceListener {
@@ -69,6 +71,8 @@ public class VibratorService extends IVibratorService.Stub
private static final boolean DEBUG = false;
private static final String SYSTEM_UI_PACKAGE = "com.android.systemui";
+ private static final long[] DOUBLE_CLICK_EFFECT_FALLBACK_TIMINGS = { 0, 30, 100, 30 };
+
private final LinkedList<VibrationInfo> mPreviousVibrations;
private final int mPreviousVibrationsLimit;
private final boolean mAllowPriorityVibrationsInLowPowerMode;
@@ -110,7 +114,12 @@ public class VibratorService extends IVibratorService.Stub
private class Vibration implements IBinder.DeathRecipient {
private final IBinder mToken;
private final VibrationEffect mEffect;
+ // Start time in CLOCK_BOOTTIME base.
private final long mStartTime;
+ // Start time in unix epoch time. Only to be used for debugging purposes and to correlate
+ // with other system events, any duration calculations should be done use mStartTime so as
+ // not to be affected by discontinuities created by RTC adjustments.
+ private final long mStartTimeDebug;
private final int mUsageHint;
private final int mUid;
private final String mOpPkg;
@@ -119,7 +128,8 @@ public class VibratorService extends IVibratorService.Stub
int usageHint, int uid, String opPkg) {
mToken = token;
mEffect = effect;
- mStartTime = SystemClock.uptimeMillis();
+ mStartTime = SystemClock.elapsedRealtime();
+ mStartTimeDebug = System.currentTimeMillis();
mUsageHint = usageHint;
mUid = uid;
mOpPkg = opPkg;
@@ -153,18 +163,22 @@ public class VibratorService extends IVibratorService.Stub
return (mUid == Process.SYSTEM_UID || mUid == 0 || SYSTEM_UI_PACKAGE.equals(mOpPkg))
&& !repeating;
}
+
+ public VibrationInfo toInfo() {
+ return new VibrationInfo(mStartTimeDebug, mEffect, mUsageHint, mUid, mOpPkg);
+ }
}
private static class VibrationInfo {
- private final long mStartTime;
+ private final long mStartTimeDebug;
private final VibrationEffect mEffect;
private final int mUsageHint;
private final int mUid;
private final String mOpPkg;
- public VibrationInfo(long startTime, VibrationEffect effect,
+ public VibrationInfo(long startTimeDebug, VibrationEffect effect,
int usageHint, int uid, String opPkg) {
- mStartTime = startTime;
+ mStartTimeDebug = startTimeDebug;
mEffect = effect;
mUsageHint = usageHint;
mUid = uid;
@@ -174,8 +188,8 @@ public class VibratorService extends IVibratorService.Stub
@Override
public String toString() {
return new StringBuilder()
- .append(", startTime: ")
- .append(mStartTime)
+ .append("startTime: ")
+ .append(DateFormat.getDateTimeInstance().format(new Date(mStartTimeDebug)))
.append(", effect: ")
.append(mEffect)
.append(", usageHint: ")
@@ -225,7 +239,7 @@ public class VibratorService extends IVibratorService.Stub
com.android.internal.R.array.config_virtualKeyVibePattern);
VibrationEffect clickEffect = createEffect(clickEffectTimings);
VibrationEffect doubleClickEffect = VibrationEffect.createWaveform(
- new long[] {0, 30, 100, 30} /*timings*/, -1);
+ DOUBLE_CLICK_EFFECT_FALLBACK_TIMINGS, -1 /*repeatIndex*/);
long[] tickEffectTimings = getLongIntArray(context.getResources(),
com.android.internal.R.array.config_clockTickVibePattern);
VibrationEffect tickEffect = createEffect(tickEffectTimings);
@@ -392,17 +406,7 @@ public class VibratorService extends IVibratorService.Stub
}
Vibration vib = new Vibration(token, effect, usageHint, uid, opPkg);
-
- // Only link against waveforms since they potentially don't have a finish if
- // they're repeating. Let other effects just play out until they're done.
- if (effect instanceof VibrationEffect.Waveform) {
- try {
- token.linkToDeath(vib, 0);
- } catch (RemoteException e) {
- return;
- }
- }
-
+ linkVibration(vib);
long ident = Binder.clearCallingIdentity();
try {
@@ -430,8 +434,7 @@ public class VibratorService extends IVibratorService.Stub
if (mPreviousVibrations.size() > mPreviousVibrationsLimit) {
mPreviousVibrations.removeFirst();
}
- mPreviousVibrations.addLast(new VibrationInfo(
- vib.mStartTime, vib.mEffect, vib.mUsageHint, vib.mUid, vib.mOpPkg));
+ mPreviousVibrations.addLast(vib.toInfo());
}
@Override // Binder call
@@ -589,10 +592,23 @@ public class VibratorService extends IVibratorService.Stub
AppOpsManager.OP_VIBRATE, mCurrentVibration.mUid,
mCurrentVibration.mOpPkg);
} catch (RemoteException e) { }
+ unlinkVibration(mCurrentVibration);
mCurrentVibration = null;
}
}
+ private void linkVibration(Vibration vib) {
+ // Only link against waveforms since they potentially don't have a finish if
+ // they're repeating. Let other effects just play out until they're done.
+ if (vib.mEffect instanceof VibrationEffect.Waveform) {
+ try {
+ vib.mToken.linkToDeath(vib, 0);
+ } catch (RemoteException e) {
+ return;
+ }
+ }
+ }
+
private void unlinkVibration(Vibration vib) {
if (vib.mEffect instanceof VibrationEffect.Waveform) {
vib.mToken.unlinkToDeath(vib, 0);
@@ -742,8 +758,7 @@ public class VibratorService extends IVibratorService.Stub
synchronized (mInputDeviceVibrators) {
VibrationEffect.Prebaked prebaked = (VibrationEffect.Prebaked) vib.mEffect;
// Input devices don't support prebaked effect, so skip trying it with them.
- final int vibratorCount = mInputDeviceVibrators.size();
- if (vibratorCount == 0) {
+ if (mInputDeviceVibrators.isEmpty()) {
long timeout = vibratorPerformEffect(prebaked.getId(), EffectStrength.MEDIUM);
if (timeout > 0) {
noteVibratorOnLocked(vib.mUid, timeout);
@@ -753,12 +768,11 @@ public class VibratorService extends IVibratorService.Stub
if (!prebaked.shouldFallback()) {
return 0;
}
- final int id = prebaked.getId();
- if (id < 0 || id >= mFallbackEffects.length || mFallbackEffects[id] == null) {
+ VibrationEffect effect = getFallbackEffect(prebaked.getId());
+ if (effect == null) {
Slog.w(TAG, "Failed to play prebaked effect, no fallback");
return 0;
}
- VibrationEffect effect = mFallbackEffects[id];
Vibration fallbackVib =
new Vibration(vib.mToken, effect, vib.mUsageHint, vib.mUid, vib.mOpPkg);
startVibrationInnerLocked(fallbackVib);
@@ -766,6 +780,13 @@ public class VibratorService extends IVibratorService.Stub
return 0;
}
+ private VibrationEffect getFallbackEffect(int effectId) {
+ if (effectId < 0 || effectId >= mFallbackEffects.length) {
+ return null;
+ }
+ return mFallbackEffects[effectId];
+ }
+
private void noteVibratorOnLocked(int uid, long millis) {
try {
mBatteryStatsService.noteVibratorOn(uid, millis);
diff --git a/com/android/server/Watchdog.java b/com/android/server/Watchdog.java
index 35f83e41..30432df4 100644
--- a/com/android/server/Watchdog.java
+++ b/com/android/server/Watchdog.java
@@ -84,6 +84,7 @@ public class Watchdog extends Thread {
"/system/bin/sdcard",
"/system/bin/surfaceflinger",
"media.extractor", // system/bin/mediaextractor
+ "media.metrics", // system/bin/mediametrics
"media.codec", // vendor/bin/hw/android.hardware.media.omx@1.0-service
"com.android.bluetooth", // Bluetooth service
};
diff --git a/com/android/server/accessibility/AccessibilityManagerService.java b/com/android/server/accessibility/AccessibilityManagerService.java
index 50b0be1a..fc6058c6 100644
--- a/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/com/android/server/accessibility/AccessibilityManagerService.java
@@ -17,6 +17,7 @@
package com.android.server.accessibility;
import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
+import static android.view.accessibility.AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS;
import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS;
@@ -762,7 +763,6 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
mPictureInPictureActionReplacingConnection = wrapper;
wrapper.linkToDeath();
}
- mSecurityPolicy.notifyWindowsChanged();
}
}
@@ -1280,7 +1280,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
int servicePackageUid = serviceInfo.applicationInfo.uid;
- if (mAppOpsManager.noteOpNoThrow(AppOpsManager.OP_BIND_ACCESSIBILITY_SERVICE,
+ if (mAppOpsManager.noteOpNoThrow(AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE,
servicePackageUid, serviceInfo.packageName) != AppOpsManager.MODE_ALLOWED) {
Slog.w(LOG_TAG, "Skipping accessibility service " + new ComponentName(
serviceInfo.packageName, serviceInfo.name).flattenToShortString()
@@ -1362,14 +1362,13 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
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);
+ // Use iterator for thread-safety
+ for (AccessibilityServiceConnection service : userState.mBoundServices) {
relevantEventTypes |= isClientInPackageWhitelist(service.getServiceInfo(), client)
? service.getRelevantEventTypes()
: 0;
}
+
relevantEventTypes |= isClientInPackageWhitelist(
mUiAutomationManager.getServiceInfo(), client)
? mUiAutomationManager.getRelevantEventTypes()
@@ -2283,6 +2282,14 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
}
+ private void sendAccessibilityEventLocked(AccessibilityEvent event, int userId) {
+ // Resync to avoid calling out with the lock held
+ event.setEventTime(SystemClock.uptimeMillis());
+ mMainHandler.obtainMessage(
+ MainHandler.MSG_SEND_ACCESSIBILITY_EVENT, userId, 0 /* unused */, event)
+ .sendToTarget();
+ }
+
/**
* AIDL-exposed method. System only.
* Inform accessibility that a fingerprint gesture was performed
@@ -2419,6 +2426,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
public static final int MSG_SEND_ACCESSIBILITY_BUTTON_TO_INPUT_FILTER = 13;
public static final int MSG_SHOW_ACCESSIBILITY_BUTTON_CHOOSER = 14;
public static final int MSG_INIT_SERVICE = 15;
+ public static final int MSG_SEND_ACCESSIBILITY_EVENT = 16;
public MainHandler(Looper looper) {
super(looper);
@@ -2519,6 +2527,12 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
(AccessibilityServiceConnection) msg.obj;
service.initializeService();
} break;
+
+ case MSG_SEND_ACCESSIBILITY_EVENT: {
+ final AccessibilityEvent event = (AccessibilityEvent) msg.obj;
+ final int userId = msg.arg1;
+ sendAccessibilityEvent(event, userId);
+ }
}
}
@@ -2533,7 +2547,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_ANNOUNCEMENT);
event.getText().add(message);
- sendAccessibilityEvent(event, mCurrentUserId);
+ sendAccessibilityEventLocked(event, mCurrentUserId);
}
}
}
@@ -2961,21 +2975,21 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
public class SecurityPolicy {
public static final int INVALID_WINDOW_ID = -1;
- private static final int RETRIEVAL_ALLOWING_EVENT_TYPES =
- AccessibilityEvent.TYPE_VIEW_CLICKED
- | AccessibilityEvent.TYPE_VIEW_FOCUSED
- | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
- | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT
- | AccessibilityEvent.TYPE_VIEW_LONG_CLICKED
- | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
- | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
- | AccessibilityEvent.TYPE_VIEW_SELECTED
- | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
- | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
- | AccessibilityEvent.TYPE_VIEW_SCROLLED
- | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
- | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
- | AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY;
+ private static final int KEEP_SOURCE_EVENT_TYPES = AccessibilityEvent.TYPE_VIEW_CLICKED
+ | AccessibilityEvent.TYPE_VIEW_FOCUSED
+ | AccessibilityEvent.TYPE_VIEW_HOVER_ENTER
+ | AccessibilityEvent.TYPE_VIEW_HOVER_EXIT
+ | AccessibilityEvent.TYPE_VIEW_LONG_CLICKED
+ | AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED
+ | AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED
+ | AccessibilityEvent.TYPE_WINDOWS_CHANGED
+ | AccessibilityEvent.TYPE_VIEW_SELECTED
+ | AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED
+ | AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
+ | AccessibilityEvent.TYPE_VIEW_SCROLLED
+ | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED
+ | AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED
+ | AccessibilityEvent.TYPE_VIEW_TEXT_TRAVERSED_AT_MOVEMENT_GRANULARITY;
// In Z order
public List<AccessibilityWindowInfo> mWindows;
@@ -3137,10 +3151,10 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
mWindows = new ArrayList<>();
}
- final int oldWindowCount = mWindows.size();
- for (int i = oldWindowCount - 1; i >= 0; i--) {
- mWindows.remove(i).recycle();
- }
+ List<AccessibilityWindowInfo> oldWindowList = new ArrayList<>(mWindows);
+ SparseArray<AccessibilityWindowInfo> oldWindowsById = mA11yWindowInfoById.clone();
+
+ mWindows.clear();
mA11yWindowInfoById.clear();
for (int i = 0; i < mWindowInfoById.size(); i++) {
@@ -3202,7 +3216,49 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
}
- notifyWindowsChanged();
+ sendEventsForChangedWindowsLocked(oldWindowList, oldWindowsById);
+
+ final int oldWindowCount = oldWindowList.size();
+ for (int i = oldWindowCount - 1; i >= 0; i--) {
+ oldWindowList.remove(i).recycle();
+ }
+ }
+
+ private void sendEventsForChangedWindowsLocked(List<AccessibilityWindowInfo> oldWindows,
+ SparseArray<AccessibilityWindowInfo> oldWindowsById) {
+ List<AccessibilityEvent> events = new ArrayList<>();
+ // Send events for all removed windows
+ final int oldWindowsCount = oldWindows.size();
+ for (int i = 0; i < oldWindowsCount; i++) {
+ final AccessibilityWindowInfo window = oldWindows.get(i);
+ if (mA11yWindowInfoById.get(window.getId()) == null) {
+ events.add(AccessibilityEvent.obtainWindowsChangedEvent(
+ window.getId(), AccessibilityEvent.WINDOWS_CHANGE_REMOVED));
+ }
+ }
+
+ // Look for other changes
+ int oldWindowIndex = 0;
+ final int newWindowCount = mWindows.size();
+ for (int i = 0; i < newWindowCount; i++) {
+ final AccessibilityWindowInfo newWindow = mWindows.get(i);
+ final AccessibilityWindowInfo oldWindow = oldWindowsById.get(newWindow.getId());
+ if (oldWindow == null) {
+ events.add(AccessibilityEvent.obtainWindowsChangedEvent(
+ newWindow.getId(), AccessibilityEvent.WINDOWS_CHANGE_ADDED));
+ } else {
+ int changes = newWindow.differenceFrom(oldWindow);
+ if (changes != 0) {
+ events.add(AccessibilityEvent.obtainWindowsChangedEvent(
+ newWindow.getId(), changes));
+ }
+ }
+ }
+
+ final int numEvents = events.size();
+ for (int i = 0; i < numEvents; i++) {
+ sendAccessibilityEventLocked(events.get(i), mCurrentUserId);
+ }
}
public boolean computePartialInteractiveRegionForWindowLocked(int windowId,
@@ -3243,7 +3299,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
}
public void updateEventSourceLocked(AccessibilityEvent event) {
- if ((event.getEventType() & RETRIEVAL_ALLOWING_EVENT_TYPES) == 0) {
+ if ((event.getEventType() & KEEP_SOURCE_EVENT_TYPES) == 0) {
event.setSource((View) null);
}
}
@@ -3357,46 +3413,55 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub
private void setActiveWindowLocked(int windowId) {
if (mActiveWindowId != windowId) {
+ sendAccessibilityEventLocked(
+ AccessibilityEvent.obtainWindowsChangedEvent(
+ mActiveWindowId, AccessibilityEvent.WINDOWS_CHANGE_ACTIVE),
+ mCurrentUserId);
+
mActiveWindowId = windowId;
if (mWindows != null) {
final int windowCount = mWindows.size();
for (int i = 0; i < windowCount; i++) {
AccessibilityWindowInfo window = mWindows.get(i);
- window.setActive(window.getId() == windowId);
+ if (window.getId() == windowId) {
+ window.setActive(true);
+ sendAccessibilityEventLocked(
+ AccessibilityEvent.obtainWindowsChangedEvent(windowId,
+ AccessibilityEvent.WINDOWS_CHANGE_ACTIVE),
+ mCurrentUserId);
+ } else {
+ window.setActive(false);
+ }
}
}
- notifyWindowsChanged();
}
}
private void setAccessibilityFocusedWindowLocked(int windowId) {
if (mAccessibilityFocusedWindowId != windowId) {
+ sendAccessibilityEventLocked(
+ AccessibilityEvent.obtainWindowsChangedEvent(
+ mAccessibilityFocusedWindowId,
+ WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED),
+ mCurrentUserId);
+
mAccessibilityFocusedWindowId = windowId;
if (mWindows != null) {
final int windowCount = mWindows.size();
for (int i = 0; i < windowCount; i++) {
AccessibilityWindowInfo window = mWindows.get(i);
- window.setAccessibilityFocused(window.getId() == windowId);
+ if (window.getId() == windowId) {
+ window.setAccessibilityFocused(true);
+ sendAccessibilityEventLocked(
+ AccessibilityEvent.obtainWindowsChangedEvent(
+ windowId, WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED),
+ mCurrentUserId);
+
+ } else {
+ window.setAccessibilityFocused(false);
+ }
}
}
-
- notifyWindowsChanged();
- }
- }
-
- public void notifyWindowsChanged() {
- if (mWindowsForAccessibilityCallback == null) {
- 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, mCurrentUserId);
- } finally {
- Binder.restoreCallingIdentity(identity);
}
}
diff --git a/com/android/server/accessibility/GestureUtils.java b/com/android/server/accessibility/GestureUtils.java
index abfdb683..d5b53bc6 100644
--- a/com/android/server/accessibility/GestureUtils.java
+++ b/com/android/server/accessibility/GestureUtils.java
@@ -40,12 +40,6 @@ final class GestureUtils {
return (deltaTime >= timeout);
}
- public static boolean isSamePointerContext(MotionEvent first, MotionEvent second) {
- return (first.getPointerIdBits() == second.getPointerIdBits()
- && first.getPointerId(first.getActionIndex())
- == second.getPointerId(second.getActionIndex()));
- }
-
/**
* Determines whether a two pointer gesture is a dragging one.
*
diff --git a/com/android/server/accessibility/GlobalActionPerformer.java b/com/android/server/accessibility/GlobalActionPerformer.java
index 3b8d4bca..672518cc 100644
--- a/com/android/server/accessibility/GlobalActionPerformer.java
+++ b/com/android/server/accessibility/GlobalActionPerformer.java
@@ -21,6 +21,8 @@ import android.app.StatusBarManager;
import android.content.Context;
import android.hardware.input.InputManager;
import android.os.Binder;
+import android.os.Handler;
+import android.os.Looper;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ServiceManager;
@@ -30,20 +32,34 @@ import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ScreenshotHelper;
import com.android.server.LocalServices;
import com.android.server.statusbar.StatusBarManagerInternal;
import com.android.server.wm.WindowManagerInternal;
+import java.util.function.Supplier;
+
/**
* Handle the back-end of AccessibilityService#performGlobalAction
*/
public class GlobalActionPerformer {
private final WindowManagerInternal mWindowManagerService;
private final Context mContext;
+ private Supplier<ScreenshotHelper> mScreenshotHelperSupplier;
public GlobalActionPerformer(Context context, WindowManagerInternal windowManagerInternal) {
mContext = context;
mWindowManagerService = windowManagerInternal;
+ mScreenshotHelperSupplier = null;
+ }
+
+ // Used to mock ScreenshotHelper
+ @VisibleForTesting
+ public GlobalActionPerformer(Context context, WindowManagerInternal windowManagerInternal,
+ Supplier<ScreenshotHelper> screenshotHelperSupplier) {
+ this(context, windowManagerInternal);
+ mScreenshotHelperSupplier = screenshotHelperSupplier;
}
public boolean performGlobalAction(int action) {
@@ -79,6 +95,9 @@ public class GlobalActionPerformer {
case AccessibilityService.GLOBAL_ACTION_LOCK_SCREEN: {
return lockScreen();
}
+ case AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT: {
+ return takeScreenshot();
+ }
}
return false;
} finally {
@@ -167,4 +186,12 @@ public class GlobalActionPerformer {
mWindowManagerService.lockNow();
return true;
}
+
+ private boolean takeScreenshot() {
+ ScreenshotHelper screenshotHelper = (mScreenshotHelperSupplier != null)
+ ? mScreenshotHelperSupplier.get() : new ScreenshotHelper(mContext);
+ screenshotHelper.takeScreenshot(android.view.WindowManager.TAKE_SCREENSHOT_FULLSCREEN,
+ true, true, new Handler(Looper.getMainLooper()));
+ return true;
+ }
}
diff --git a/com/android/server/accessibility/MagnificationGestureHandler.java b/com/android/server/accessibility/MagnificationGestureHandler.java
index 9b2b4eb7..52ab85c4 100644
--- a/com/android/server/accessibility/MagnificationGestureHandler.java
+++ b/com/android/server/accessibility/MagnificationGestureHandler.java
@@ -17,6 +17,7 @@
package com.android.server.accessibility;
import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
+import static android.view.MotionEvent.ACTION_CANCEL;
import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_MOVE;
import static android.view.MotionEvent.ACTION_POINTER_DOWN;
@@ -36,7 +37,9 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Handler;
+import android.os.Looper;
import android.os.Message;
+import android.util.Log;
import android.util.MathUtils;
import android.util.Slog;
import android.util.TypedValue;
@@ -51,6 +54,9 @@ import android.view.ViewConfiguration;
import com.android.internal.annotations.VisibleForTesting;
+import java.util.ArrayDeque;
+import java.util.Queue;
+
/**
* This class handles magnification in response to touch events.
*
@@ -107,6 +113,7 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
private static final boolean DEBUG_STATE_TRANSITIONS = false || DEBUG_ALL;
private static final boolean DEBUG_DETECTING = false || DEBUG_ALL;
private static final boolean DEBUG_PANNING_SCALING = false || DEBUG_ALL;
+ private static final boolean DEBUG_EVENT_STREAM = false || DEBUG_ALL;
private static final float MIN_SCALE = 2.0f;
private static final float MAX_SCALE = 5.0f;
@@ -138,6 +145,9 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
private PointerCoords[] mTempPointerCoords;
private PointerProperties[] mTempPointerProperties;
+ private final Queue<MotionEvent> mDebugInputEventHistory;
+ private final Queue<MotionEvent> mDebugOutputEventHistory;
+
/**
* @param context Context for resolving various magnification-related resources
* @param magnificationController the {@link MagnificationController}
@@ -153,6 +163,12 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
MagnificationController magnificationController,
boolean detectTripleTap,
boolean detectShortcutTrigger) {
+ if (DEBUG_ALL) {
+ Log.i(LOG_TAG,
+ "MagnificationGestureHandler(detectTripleTap = " + detectTripleTap
+ + ", detectShortcutTrigger = " + detectShortcutTrigger + ")");
+ }
+
mMagnificationController = magnificationController;
mDelegatingState = new DelegatingState();
@@ -170,11 +186,28 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
mScreenStateReceiver = null;
}
+ mDebugInputEventHistory = DEBUG_EVENT_STREAM ? new ArrayDeque<>() : null;
+ mDebugOutputEventHistory = DEBUG_EVENT_STREAM ? new ArrayDeque<>() : null;
+
transitionTo(mDetectingState);
}
@Override
public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
+ if (DEBUG_EVENT_STREAM) {
+ storeEventInto(mDebugInputEventHistory, event);
+ try {
+ onMotionEventInternal(event, rawEvent, policyFlags);
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Exception following input events: " + mDebugInputEventHistory, e);
+ }
+ } else {
+ onMotionEventInternal(event, rawEvent, policyFlags);
+ }
+ }
+
+ private void onMotionEventInternal(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
if (DEBUG_ALL) Slog.i(LOG_TAG, "onMotionEvent(" + event + ")");
if ((!mDetectTripleTap && !mDetectShortcutTrigger)
@@ -229,7 +262,7 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
}
void clearAndTransitionToStateDetecting() {
- mCurrentState = mDelegatingState;
+ mCurrentState = mDetectingState;
mDetectingState.clear();
mViewportDraggingState.clear();
mPanningScalingState.clear();
@@ -264,7 +297,27 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
coords, 0, 0, 1.0f, 1.0f, event.getDeviceId(), 0, event.getSource(),
event.getFlags());
}
- super.onMotionEvent(event, rawEvent, policyFlags);
+ if (DEBUG_EVENT_STREAM) {
+ storeEventInto(mDebugOutputEventHistory, event);
+ try {
+ super.onMotionEvent(event, rawEvent, policyFlags);
+ } catch (Exception e) {
+ throw new RuntimeException(
+ "Exception downstream following input events: " + mDebugInputEventHistory
+ + "\nTransformed into output events: " + mDebugOutputEventHistory,
+ e);
+ }
+ } else {
+ super.onMotionEvent(event, rawEvent, policyFlags);
+ }
+ }
+
+ private static void storeEventInto(Queue<MotionEvent> queue, MotionEvent event) {
+ queue.add(MotionEvent.obtain(event));
+ // Prune old events
+ while (!queue.isEmpty() && (event.getEventTime() - queue.peek().getEventTime() > 5000)) {
+ queue.remove().recycle();
+ }
}
private PointerCoords[] getTempPointerCoordsWithMinSize(int size) {
@@ -364,7 +417,7 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
persistScaleAndTransitionTo(mViewportDraggingState);
- } else if (action == ACTION_UP) {
+ } else if (action == ACTION_UP || action == ACTION_CANCEL) {
persistScaleAndTransitionTo(mDetectingState);
@@ -496,7 +549,9 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
}
}
break;
- case ACTION_UP: {
+
+ case ACTION_UP:
+ case ACTION_CANCEL: {
if (!mZoomedInBeforeDrag) zoomOff();
clear();
transitionTo(mDetectingState);
@@ -533,13 +588,21 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
@Override
public void onMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) {
- if (event.getActionMasked() == ACTION_UP) {
- transitionTo(mDetectingState);
- }
- if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
- mLastDelegatedDownEventTime = event.getDownTime();
+ // Ensure that the state at the end of delegation is consistent with the last delegated
+ // UP/DOWN event in queue: still delegating if pointer is down, detecting otherwise
+ switch (event.getActionMasked()) {
+ case ACTION_UP:
+ case ACTION_CANCEL: {
+ transitionTo(mDetectingState);
+ } break;
+
+ case ACTION_DOWN: {
+ transitionTo(mDelegatingState);
+ mLastDelegatedDownEventTime = event.getDownTime();
+ } break;
}
+
if (getNext() != null) {
// We cache some events to see if the user wants to trigger magnification.
// If no magnification is triggered we inject these events with adjusted
@@ -575,7 +638,7 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
@VisibleForTesting boolean mShortcutTriggered;
- Handler mHandler = new Handler(this);
+ @VisibleForTesting Handler mHandler = new Handler(this);
public DetectingState(Context context) {
mLongTapMinDelay = ViewConfiguration.getLongPressTimeout();
@@ -649,14 +712,19 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
break;
case ACTION_MOVE: {
if (isFingerDown()
- && distance(mLastDown, /* move */ event) > mSwipeMinDistance
- // For convenience, viewport dragging on 3tap&hold takes precedence
- // over insta-delegating on 3tap&swipe
- // (which is a rare combo to be used aside from magnification)
- && !isMultiTapTriggered(2 /* taps */)) {
-
- // Swipe detected - delegate skipping timeout
- transitionToDelegatingStateAndClear();
+ && distance(mLastDown, /* move */ event) > mSwipeMinDistance) {
+
+ // Swipe detected - transition immediately
+
+ // For convenience, viewport dragging takes precedence
+ // over insta-delegating on 3tap&swipe
+ // (which is a rare combo to be used aside from magnification)
+ if (isMultiTapTriggered(2 /* taps */)) {
+ transitionTo(mViewportDraggingState);
+ clear();
+ } else {
+ transitionToDelegatingStateAndClear();
+ }
}
}
break;
@@ -745,20 +813,23 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
@Override
public void clear() {
setShortcutTriggered(false);
- mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD);
- mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
+ removePendingDelayedMessages();
clearDelayedMotionEvents();
}
+ private void removePendingDelayedMessages() {
+ mHandler.removeMessages(MESSAGE_ON_TRIPLE_TAP_AND_HOLD);
+ mHandler.removeMessages(MESSAGE_TRANSITION_TO_DELEGATING_STATE);
+ }
private void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent,
int policyFlags) {
if (event.getActionMasked() == ACTION_DOWN) {
mPreLastDown = mLastDown;
- mLastDown = event;
+ mLastDown = MotionEvent.obtain(event);
} else if (event.getActionMasked() == ACTION_UP) {
mPreLastUp = mLastUp;
- mLastUp = event;
+ mLastUp = MotionEvent.obtain(event);
}
MotionEventInfo info = MotionEventInfo.obtain(event, rawEvent,
@@ -800,7 +871,7 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
void transitionToDelegatingStateAndClear() {
transitionTo(mDelegatingState);
sendDelayedMotionEvents();
- clear();
+ removePendingDelayedMessages();
}
private void onTripleTap(MotionEvent up) {
@@ -849,6 +920,7 @@ class MagnificationGestureHandler extends BaseEventStreamTransformation {
if (mShortcutTriggered == state) {
return;
}
+ if (DEBUG_DETECTING) Slog.i(LOG_TAG, "setShortcutTriggered(" + state + ")");
mShortcutTriggered = state;
mMagnificationController.setForceShowMagnifiableBounds(state);
diff --git a/com/android/server/accounts/AccountManagerService.java b/com/android/server/accounts/AccountManagerService.java
index 31aea638..8d2e3a26 100644
--- a/com/android/server/accounts/AccountManagerService.java
+++ b/com/android/server/accounts/AccountManagerService.java
@@ -2007,11 +2007,11 @@ public class AccountManagerService
getAccountRemovedReceivers(accountToRename, accounts);
accounts.accountsDb.beginTransaction();
Account renamedAccount = new Account(newName, accountToRename.type);
- if ((accounts.accountsDb.findCeAccountId(renamedAccount) >= 0)) {
- Log.e(TAG, "renameAccount failed - account with new name already exists");
- return null;
- }
try {
+ if ((accounts.accountsDb.findCeAccountId(renamedAccount) >= 0)) {
+ Log.e(TAG, "renameAccount failed - account with new name already exists");
+ return null;
+ }
final long accountId = accounts.accountsDb.findDeAccountId(accountToRename);
if (accountId >= 0) {
accounts.accountsDb.renameCeAccount(accountId, newName);
@@ -5595,24 +5595,25 @@ public class AccountManagerService
long ident = Binder.clearCallingIdentity();
try {
packages = mPackageManager.getPackagesForUid(callingUid);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- if (packages != null) {
- for (String name : packages) {
- try {
- PackageInfo packageInfo = mPackageManager.getPackageInfo(name, 0 /* flags */);
- if (packageInfo != null
- && (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
- != 0) {
- return true;
+ if (packages != null) {
+ for (String name : packages) {
+ try {
+ PackageInfo packageInfo =
+ mPackageManager.getPackageInfo(name, 0 /* flags */);
+ if (packageInfo != null
+ && (packageInfo.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM)
+ != 0) {
+ return true;
+ }
+ } catch (NameNotFoundException e) {
+ Log.w(TAG, String.format("Could not find package [%s]", name), e);
}
- } catch (NameNotFoundException e) {
- Log.w(TAG, String.format("Could not find package [%s]", name), e);
}
+ } else {
+ Log.w(TAG, "No known packages with uid " + callingUid);
}
- } else {
- Log.w(TAG, "No known packages with uid " + callingUid);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
}
return false;
}
diff --git a/com/android/server/am/ActiveInstrumentation.java b/com/android/server/am/ActiveInstrumentation.java
index 84e4ea9d..4a657334 100644
--- a/com/android/server/am/ActiveInstrumentation.java
+++ b/com/android/server/am/ActiveInstrumentation.java
@@ -22,6 +22,9 @@ import android.content.ComponentName;
import android.content.pm.ApplicationInfo;
import android.os.Bundle;
import android.util.PrintWriterPrinter;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.server.am.proto.ActiveInstrumentationProto;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -119,4 +122,26 @@ class ActiveInstrumentation {
pw.print(prefix); pw.print("mArguments=");
pw.println(mArguments);
}
+
+ void writeToProto(ProtoOutputStream proto, long fieldId) {
+ long token = proto.start(fieldId);
+ mClass.writeToProto(proto, ActiveInstrumentationProto.CLASS);
+ proto.write(ActiveInstrumentationProto.FINISHED, mFinished);
+ for (int i=0; i<mRunningProcesses.size(); i++) {
+ mRunningProcesses.get(i).writeToProto(proto,
+ ActiveInstrumentationProto.RUNNING_PROCESSES);
+ }
+ for (String p : mTargetProcesses) {
+ proto.write(ActiveInstrumentationProto.TARGET_PROCESSES, p);
+ }
+ if (mTargetInfo != null) {
+ mTargetInfo.writeToProto(proto, ActiveInstrumentationProto.TARGET_INFO);
+ }
+ proto.write(ActiveInstrumentationProto.PROFILE_FILE, mProfileFile);
+ proto.write(ActiveInstrumentationProto.WATCHER, mWatcher.toString());
+ proto.write(ActiveInstrumentationProto.UI_AUTOMATION_CONNECTION,
+ mUiAutomationConnection.toString());
+ proto.write(ActiveInstrumentationProto.ARGUMENTS, mArguments.toString());
+ proto.end(token);
+ }
}
diff --git a/com/android/server/am/ActivityDisplay.java b/com/android/server/am/ActivityDisplay.java
index af5cf1ee..aa8d56b3 100644
--- a/com/android/server/am/ActivityDisplay.java
+++ b/com/android/server/am/ActivityDisplay.java
@@ -16,6 +16,7 @@
package com.android.server.am;
+import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -50,7 +51,9 @@ import android.util.proto.ProtoOutputStream;
import android.view.Display;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wm.ConfigurationContainer;
+import com.android.server.wm.DisplayWindowController;
+import com.android.server.wm.WindowContainerListener;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -58,7 +61,8 @@ import java.util.ArrayList;
* Exactly one of these classes per Display in the system. Capable of holding zero or more
* attached {@link ActivityStack}s.
*/
-class ActivityDisplay extends ConfigurationContainer<ActivityStack> {
+class ActivityDisplay extends ConfigurationContainer<ActivityStack>
+ implements WindowContainerListener {
private static final String TAG = TAG_WITH_CLASS_NAME ? "ActivityDisplay" : TAG_AM;
private static final String TAG_STACK = TAG + POSTFIX_STACK;
@@ -100,6 +104,8 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack> {
// Used in updating the display size
private Point mTmpDisplaySize = new Point();
+ private DisplayWindowController mWindowContainerController;
+
ActivityDisplay(ActivityStackSupervisor supervisor, int displayId) {
mSupervisor = supervisor;
mDisplayId = displayId;
@@ -108,10 +114,15 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack> {
throw new IllegalStateException("Display does not exist displayId=" + displayId);
}
mDisplay = display;
+ mWindowContainerController = createWindowContainerController();
updateBounds();
}
+ protected DisplayWindowController createWindowContainerController() {
+ return new DisplayWindowController(mDisplayId, this);
+ }
+
void updateBounds() {
mDisplay.getSize(mTmpDisplaySize);
setBounds(0, 0, mTmpDisplaySize.x, mTmpDisplaySize.y);
@@ -148,7 +159,10 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack> {
private void positionChildAt(ActivityStack stack, int position) {
mStacks.remove(stack);
- mStacks.add(getTopInsertPosition(stack, position), stack);
+ final int insertPosition = getTopInsertPosition(stack, position);
+ mStacks.add(insertPosition, stack);
+ mWindowContainerController.positionChildAt(stack.getWindowContainerController(),
+ insertPosition);
}
private int getTopInsertPosition(ActivityStack stack, int candidatePosition) {
@@ -556,10 +570,10 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack> {
return stack == getTopStack();
}
- boolean isTopFullscreenStack(ActivityStack stack) {
+ boolean isTopNotPinnedStack(ActivityStack stack) {
for (int i = mStacks.size() - 1; i >= 0; --i) {
final ActivityStack current = mStacks.get(i);
- if (current.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
+ if (!current.inPinnedWindowingMode()) {
return current == stack;
}
}
@@ -651,6 +665,64 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack> {
&& (mSupervisor.mService.mRunningVoice == null);
}
+ /**
+ * @return the stack currently above the home stack. Can be null if there is no home stack, or
+ * the home stack is already on top.
+ */
+ ActivityStack getStackAboveHome() {
+ if (mHomeStack == null) {
+ // Skip if there is no home stack
+ return null;
+ }
+
+ final int stackIndex = mStacks.indexOf(mHomeStack) + 1;
+ return (stackIndex < mStacks.size()) ? mStacks.get(stackIndex) : null;
+ }
+
+ /**
+ * Adjusts the home stack behind the last visible stack in the display if necessary. Generally
+ * used in conjunction with {@link #moveHomeStackBehindStack}.
+ */
+ void moveHomeStackBehindBottomMostVisibleStack() {
+ if (mHomeStack == null) {
+ // Skip if there is no home stack
+ return;
+ }
+
+ // Move the home stack to the bottom to not affect the following visibility checks
+ positionChildAtBottom(mHomeStack);
+
+ // Find the next position where the homes stack should be placed
+ final int numStacks = mStacks.size();
+ for (int stackNdx = 0; stackNdx < numStacks; stackNdx++) {
+ final ActivityStack stack = mStacks.get(stackNdx);
+ if (stack == mHomeStack) {
+ continue;
+ }
+ final int winMode = stack.getWindowingMode();
+ final boolean isValidWindowingMode = winMode == WINDOWING_MODE_FULLSCREEN ||
+ winMode == WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
+ if (stack.shouldBeVisible(null) && isValidWindowingMode) {
+ // Move the home stack to behind this stack
+ positionChildAt(mHomeStack, Math.max(0, stackNdx - 1));
+ break;
+ }
+ }
+ }
+
+ /**
+ * Moves the home stack behind the given {@param stack} if possible. If {@param stack} is not
+ * currently in the display, then then the home stack is moved to the back. Generally used in
+ * conjunction with {@link #moveHomeStackBehindBottomMostVisibleStack}.
+ */
+ void moveHomeStackBehindStack(ActivityStack behindStack) {
+ if (behindStack == null) {
+ return;
+ }
+
+ positionChildAt(mHomeStack, Math.max(0, mStacks.indexOf(behindStack) - 1));
+ }
+
boolean isSleeping() {
return mSleeping;
}
@@ -676,6 +748,15 @@ class ActivityDisplay extends ConfigurationContainer<ActivityStack> {
}
}
+ public void dumpStacks(PrintWriter pw) {
+ for (int i = mStacks.size() - 1; i >= 0; --i) {
+ pw.print(mStacks.get(i).mStackId);
+ if (i > 0) {
+ pw.print(",");
+ }
+ }
+ }
+
public void writeToProto(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
super.writeToProto(proto, CONFIGURATION_CONTAINER, false /* trim */);
diff --git a/com/android/server/am/LaunchingActivityPositioner.java b/com/android/server/am/ActivityLaunchParamsModifier.java
index 793884d0..f44ee7a2 100644
--- a/com/android/server/am/LaunchingActivityPositioner.java
+++ b/com/android/server/am/ActivityLaunchParamsModifier.java
@@ -19,23 +19,25 @@ package com.android.server.am;
import android.app.ActivityOptions;
import android.content.pm.ActivityInfo;
import android.graphics.Rect;
-import com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner;
+
+import com.android.server.am.LaunchParamsController.LaunchParams;
+import com.android.server.am.LaunchParamsController.LaunchParamsModifier;
/**
- * An implementation of {@link LaunchingBoundsPositioner}, which applies the launch bounds specified
+ * An implementation of {@link LaunchParamsModifier}, which applies the launch bounds specified
* inside {@link ActivityOptions#getLaunchBounds()}.
*/
-public class LaunchingActivityPositioner implements LaunchingBoundsPositioner {
+public class ActivityLaunchParamsModifier implements LaunchParamsModifier {
private final ActivityStackSupervisor mSupervisor;
- LaunchingActivityPositioner(ActivityStackSupervisor activityStackSupervisor) {
+ ActivityLaunchParamsModifier(ActivityStackSupervisor activityStackSupervisor) {
mSupervisor = activityStackSupervisor;
}
@Override
- public int onCalculateBounds(TaskRecord task, ActivityInfo.WindowLayout layout,
- ActivityRecord activity, ActivityRecord source,
- ActivityOptions options, Rect current, Rect result) {
+ public int onCalculate(TaskRecord task, ActivityInfo.WindowLayout layout,
+ ActivityRecord activity, ActivityRecord source, ActivityOptions options,
+ LaunchParams currentParams, LaunchParams outParams) {
// We only care about figuring out bounds for activities.
if (activity == null) {
return RESULT_SKIP;
@@ -43,7 +45,7 @@ public class LaunchingActivityPositioner implements LaunchingBoundsPositioner {
// Activity must be resizeable in the specified task.
if (!(mSupervisor.canUseActivityOptionsLaunchBounds(options)
- && (activity.isResizeable() || (task != null && task.isResizeable())))) {
+ && (activity.isResizeable() || (task != null && task.isResizeable())))) {
return RESULT_SKIP;
}
@@ -54,7 +56,7 @@ public class LaunchingActivityPositioner implements LaunchingBoundsPositioner {
return RESULT_SKIP;
}
- result.set(bounds);
+ outParams.mBounds.set(bounds);
// When this is the most explicit position specification so we should not allow further
// modification of the position.
diff --git a/com/android/server/am/ActivityManagerConstants.java b/com/android/server/am/ActivityManagerConstants.java
index b3a596c8..0d6d2bde 100644
--- a/com/android/server/am/ActivityManagerConstants.java
+++ b/com/android/server/am/ActivityManagerConstants.java
@@ -77,8 +77,8 @@ final class ActivityManagerConstants extends ContentObserver {
private static final long DEFAULT_CONTENT_PROVIDER_RETAIN_TIME = 20*1000;
private static final long DEFAULT_GC_TIMEOUT = 5*1000;
private static final long DEFAULT_GC_MIN_INTERVAL = 60*1000;
- private static final long DEFAULT_FULL_PSS_MIN_INTERVAL = 10*60*1000;
- private static final long DEFAULT_FULL_PSS_LOWERED_INTERVAL = 2*60*1000;
+ private static final long DEFAULT_FULL_PSS_MIN_INTERVAL = 20*60*1000;
+ private static final long DEFAULT_FULL_PSS_LOWERED_INTERVAL = 5*60*1000;
private static final long DEFAULT_POWER_CHECK_INTERVAL = (DEBUG_POWER_QUICK ? 1 : 5) * 60*1000;
private static final int DEFAULT_POWER_CHECK_MAX_CPU_1 = 25;
private static final int DEFAULT_POWER_CHECK_MAX_CPU_2 = 25;
diff --git a/com/android/server/am/ActivityManagerService.java b/com/android/server/am/ActivityManagerService.java
index d92b3b86..2ccda7db 100644
--- a/com/android/server/am/ActivityManagerService.java
+++ b/com/android/server/am/ActivityManagerService.java
@@ -19,16 +19,18 @@ package com.android.server.am;
import static android.Manifest.permission.BIND_VOICE_INTERACTION;
import static android.Manifest.permission.CHANGE_CONFIGURATION;
import static android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST;
+import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
import static android.Manifest.permission.MANAGE_ACTIVITY_STACKS;
import static android.Manifest.permission.READ_FRAME_BUFFER;
import static android.Manifest.permission.REMOVE_TASKS;
+import static android.Manifest.permission.START_ACTIVITY_AS_CALLER;
import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
-import static android.app.ActivityManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
import static android.app.ActivityManager.RESIZE_MODE_PRESERVE_WINDOW;
+import static android.app.ActivityManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
import static android.app.ActivityManagerInternal.ASSIST_KEY_CONTENT;
import static android.app.ActivityManagerInternal.ASSIST_KEY_DATA;
@@ -37,6 +39,7 @@ 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_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
@@ -119,6 +122,7 @@ import static android.service.voice.VoiceInteractionSession.SHOW_SOURCE_APPLICAT
import static android.text.format.DateUtils.DAY_IN_MILLIS;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.Display.INVALID_DISPLAY;
+
import static com.android.internal.util.XmlUtils.readBooleanAttribute;
import static com.android.internal.util.XmlUtils.readIntAttribute;
import static com.android.internal.util.XmlUtils.readLongAttribute;
@@ -192,11 +196,12 @@ import static com.android.server.am.TaskRecord.INVALID_TASK_ID;
import static com.android.server.am.TaskRecord.LOCK_TASK_AUTH_DONT_LOCK;
import static com.android.server.am.TaskRecord.REPARENT_KEEP_STACK_AT_FRONT;
import static com.android.server.am.TaskRecord.REPARENT_LEAVE_STACK_IN_PLACE;
-import static com.android.server.wm.AppTransition.TRANSIT_ACTIVITY_OPEN;
-import static com.android.server.wm.AppTransition.TRANSIT_NONE;
-import static com.android.server.wm.AppTransition.TRANSIT_TASK_IN_PLACE;
-import static com.android.server.wm.AppTransition.TRANSIT_TASK_OPEN;
-import static com.android.server.wm.AppTransition.TRANSIT_TASK_TO_FRONT;
+import static android.view.WindowManager.TRANSIT_ACTIVITY_OPEN;
+import static android.view.WindowManager.TRANSIT_NONE;
+import static android.view.WindowManager.TRANSIT_TASK_IN_PLACE;
+import static android.view.WindowManager.TRANSIT_TASK_OPEN;
+import static android.view.WindowManager.TRANSIT_TASK_TO_FRONT;
+
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;
@@ -213,6 +218,7 @@ import android.app.ActivityManager.TaskSnapshot;
import android.app.ActivityManagerInternal;
import android.app.ActivityManagerInternal.ScreenObserver;
import android.app.ActivityManagerInternal.SleepToken;
+import android.app.ActivityManagerProto;
import android.app.ActivityOptions;
import android.app.ActivityThread;
import android.app.AlertDialog;
@@ -276,8 +282,8 @@ import android.content.pm.IPackageManager;
import android.content.pm.InstrumentationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
import android.content.pm.PathPermission;
import android.content.pm.PermissionInfo;
@@ -293,6 +299,7 @@ import android.database.ContentObserver;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
+import android.hardware.display.DisplayManagerInternal;
import android.location.LocationManager;
import android.media.audiofx.AudioEffect;
import android.metrics.LogMaker;
@@ -354,22 +361,25 @@ 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;
import android.util.EventLog;
import android.util.Log;
+import android.util.LongSparseArray;
import android.util.Pair;
import android.util.PrintWriterPrinter;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
+import android.util.StatsLog;
import android.util.TimeUtils;
+import android.util.TimingsTraceLog;
import android.util.Xml;
import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoUtils;
import android.view.Gravity;
+import android.view.IRecentsAnimationRunner;
import android.view.LayoutInflater;
+import android.view.RemoteAnimationDefinition;
import android.view.View;
import android.view.WindowManager;
@@ -391,12 +401,14 @@ 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.logging.MetricsLoggerWrapper;
import com.android.internal.os.ByteTransferPipe;
import com.android.internal.os.IResultReceiver;
import com.android.internal.os.ProcessCpuTracker;
import com.android.internal.os.TransferPipe;
import com.android.internal.os.Zygote;
import com.android.internal.policy.IKeyguardDismissCallback;
+import com.android.internal.policy.KeyguardDismissCallback;
import com.android.internal.telephony.TelephonyIntents;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
@@ -421,12 +433,16 @@ 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.ImportanceTokenProto;
import com.android.server.am.proto.MemInfoProto;
import com.android.server.am.proto.NeededUriGrantsProto;
+import com.android.server.am.proto.ProcessOomProto;
+import com.android.server.am.proto.ProcessToGcProto;
+import com.android.server.am.proto.ProcessesProto;
+import com.android.server.am.proto.ProcessesProto.UidObserverRegistrationProto;
import com.android.server.am.proto.StickyBroadcastProto;
import com.android.server.firewall.IntentFirewall;
import com.android.server.job.JobSchedulerInternal;
@@ -435,7 +451,14 @@ import com.android.server.pm.Installer.InstallerException;
import com.android.server.utils.PriorityDump;
import com.android.server.vr.VrManagerInternal;
import com.android.server.wm.PinnedStackWindowController;
+import com.android.server.wm.RecentsAnimationController;
import com.android.server.wm.WindowManagerService;
+
+import dalvik.system.VMRuntime;
+
+import libcore.io.IoUtils;
+import libcore.util.EmptyArray;
+
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
@@ -471,14 +494,10 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
-import dalvik.system.VMRuntime;
-
-import libcore.io.IoUtils;
-import libcore.util.EmptyArray;
-
public class ActivityManagerService extends IActivityManager.Stub
implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback {
@@ -549,6 +568,23 @@ public class ActivityManagerService extends IActivityManager.Stub
// could take much longer than usual.
static final int PROC_START_TIMEOUT_WITH_WRAPPER = 1200*1000;
+ // Permission tokens are used to temporarily granted a trusted app the ability to call
+ // #startActivityAsCaller. A client is expected to dump its token after this time has elapsed,
+ // showing any appropriate error messages to the user.
+ private static final long START_AS_CALLER_TOKEN_TIMEOUT =
+ 10 * DateUtils.MINUTE_IN_MILLIS;
+
+ // How long before the service actually expires a token. This is slightly longer than
+ // START_AS_CALLER_TOKEN_TIMEOUT, to provide a buffer so clients will rarely encounter the
+ // expiration exception.
+ private static final long START_AS_CALLER_TOKEN_TIMEOUT_IMPL =
+ START_AS_CALLER_TOKEN_TIMEOUT + 2*1000;
+
+ // How long the service will remember expired tokens, for the purpose of providing error
+ // messaging when a client uses an expired token.
+ private static final long START_AS_CALLER_TOKEN_EXPIRED_TIMEOUT =
+ START_AS_CALLER_TOKEN_TIMEOUT_IMPL + 20 * DateUtils.MINUTE_IN_MILLIS;
+
// How long we allow a receiver to run before giving up on it.
static final int BROADCAST_FG_TIMEOUT = 10*1000;
static final int BROADCAST_BG_TIMEOUT = 60*1000;
@@ -657,6 +693,13 @@ public class ActivityManagerService extends IActivityManager.Stub
final ArrayList<ActiveInstrumentation> mActiveInstrumentation = new ArrayList<>();
+ // Activity tokens of system activities that are delegating their call to
+ // #startActivityByCaller, keyed by the permissionToken granted to the delegate.
+ final HashMap<IBinder, IBinder> mStartActivitySources = new HashMap<>();
+
+ // Permission tokens that have expired, but we remember for error reporting.
+ final ArrayList<IBinder> mExpiredStartAsCallerTokens = new ArrayList<>();
+
public final IntentFirewall mIntentFirewall;
// Whether we should show our dialogs (ANR, crash, etc) or just perform their
@@ -930,6 +973,16 @@ public class ActivityManagerService extends IActivityManager.Stub
return "ImportanceToken { " + Integer.toHexString(System.identityHashCode(this))
+ " " + reason + " " + pid + " " + token + " }";
}
+
+ void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long pToken = proto.start(fieldId);
+ proto.write(ImportanceTokenProto.PID, pid);
+ if (token != null) {
+ proto.write(ImportanceTokenProto.TOKEN, token.toString());
+ }
+ proto.write(ImportanceTokenProto.REASON, reason);
+ proto.end(pToken);
+ }
}
final SparseArray<ImportanceToken> mImportantProcesses = new SparseArray<ImportanceToken>();
@@ -1308,6 +1361,14 @@ public class ActivityManagerService extends IActivityManager.Stub
duration = _duration;
tag = _tag;
}
+
+ void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(ProcessesProto.PendingTempWhitelist.TARGET_UID, targetUid);
+ proto.write(ProcessesProto.PendingTempWhitelist.DURATION_MS, duration);
+ proto.write(ProcessesProto.PendingTempWhitelist.TAG, tag);
+ proto.end(token);
+ }
}
final SparseArray<PendingTempWhitelist> mPendingTempWhitelist = new SparseArray<>();
@@ -1632,6 +1693,20 @@ public class ActivityManagerService extends IActivityManager.Stub
final SparseIntArray lastProcStates;
+ // Please keep the enum lists in sync
+ private static int[] ORIG_ENUMS = new int[]{
+ ActivityManager.UID_OBSERVER_IDLE,
+ ActivityManager.UID_OBSERVER_ACTIVE,
+ ActivityManager.UID_OBSERVER_GONE,
+ ActivityManager.UID_OBSERVER_PROCSTATE,
+ };
+ private static int[] PROTO_ENUMS = new int[]{
+ ActivityManagerProto.UID_OBSERVER_FLAG_IDLE,
+ ActivityManagerProto.UID_OBSERVER_FLAG_ACTIVE,
+ ActivityManagerProto.UID_OBSERVER_FLAG_GONE,
+ ActivityManagerProto.UID_OBSERVER_FLAG_PROCSTATE,
+ };
+
UidObserverRegistration(int _uid, String _pkg, int _which, int _cutpoint) {
uid = _uid;
pkg = _pkg;
@@ -1643,6 +1718,25 @@ public class ActivityManagerService extends IActivityManager.Stub
lastProcStates = null;
}
}
+
+ void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(UidObserverRegistrationProto.UID, uid);
+ proto.write(UidObserverRegistrationProto.PACKAGE, pkg);
+ ProtoUtils.writeBitWiseFlagsToProtoEnum(proto, UidObserverRegistrationProto.FLAGS,
+ which, ORIG_ENUMS, PROTO_ENUMS);
+ proto.write(UidObserverRegistrationProto.CUT_POINT, cutpoint);
+ if (lastProcStates != null) {
+ final int NI = lastProcStates.size();
+ for (int i=0; i<NI; i++) {
+ final long pToken = proto.start(UidObserverRegistrationProto.LAST_PROC_STATES);
+ proto.write(UidObserverRegistrationProto.ProcState.UID, lastProcStates.keyAt(i));
+ proto.write(UidObserverRegistrationProto.ProcState.STATE, lastProcStates.valueAt(i));
+ proto.end(pToken);
+ }
+ }
+ proto.end(token);
+ }
}
final List<ScreenObserver> mScreenObservers = new ArrayList<>();
@@ -1776,6 +1870,8 @@ public class ActivityManagerService extends IActivityManager.Stub
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 EXPIRE_START_AS_CALLER_TOKEN_MSG = 75;
+ static final int FORGET_START_AS_CALLER_TOKEN_MSG = 76;
static final int FIRST_ACTIVITY_STACK_MSG = 100;
static final int FIRST_BROADCAST_QUEUE_MSG = 200;
@@ -2440,6 +2536,19 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
} break;
+ case EXPIRE_START_AS_CALLER_TOKEN_MSG: {
+ synchronized (ActivityManagerService.this) {
+ final IBinder permissionToken = (IBinder)msg.obj;
+ mStartActivitySources.remove(permissionToken);
+ mExpiredStartAsCallerTokens.add(permissionToken);
+ }
+ } break;
+ case FORGET_START_AS_CALLER_TOKEN_MSG: {
+ synchronized (ActivityManagerService.this) {
+ final IBinder permissionToken = (IBinder)msg.obj;
+ mExpiredStartAsCallerTokens.remove(permissionToken);
+ }
+ } break;
}
}
};
@@ -2521,13 +2630,15 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
if (proc != null) {
+ long startTime = SystemClock.currentThreadTimeMillis();
long pss = Debug.getPss(pid, tmp, null);
+ long endTime = SystemClock.currentThreadTimeMillis();
synchronized (ActivityManagerService.this) {
if (pss != 0 && proc.thread != null && proc.setProcState == procState
&& proc.pid == pid && proc.lastPssTime == lastPssTime) {
num++;
recordPssSampleLocked(proc, procState, pss, tmp[0], tmp[1],
- SystemClock.uptimeMillis());
+ endTime-startTime, SystemClock.uptimeMillis());
}
}
}
@@ -2695,6 +2806,13 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
+ public void onBootPhase(int phase) {
+ if (phase == PHASE_SYSTEM_SERVICES_READY) {
+ mService.mBatteryStatsService.systemServicesReady();
+ }
+ }
+
+ @Override
public void onCleanupUser(int userId) {
mService.mBatteryStatsService.onCleanupUser(userId);
}
@@ -3949,6 +4067,12 @@ public class ActivityManagerService extends IActivityManager.Stub
runtimeFlags |= Zygote.ONLY_USE_SYSTEM_OAT_FILES;
}
+ if (app.info.isAllowedToUseHiddenApi()) {
+ // This app is allowed to use undocumented and private APIs. Set
+ // up its runtime with the appropriate flag.
+ runtimeFlags |= Zygote.DISABLE_HIDDEN_API_CHECKS;
+ }
+
String invokeWith = null;
if ((app.info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
// Debuggable apps may include a wrapper script with their library directory.
@@ -4693,21 +4817,60 @@ public class ActivityManagerService extends IActivityManager.Stub
.setRequestCode(requestCode)
.setStartFlags(startFlags)
.setProfilerInfo(profilerInfo)
- .setMayWait(bOptions, userId)
+ .setActivityOptions(bOptions)
+ .setMayWait(userId)
.execute();
}
+ /**
+ * Only callable from the system. This token grants a temporary permission to call
+ * #startActivityAsCallerWithToken. The token will time out after
+ * START_AS_CALLER_TOKEN_TIMEOUT if it is not used.
+ *
+ * @param delegatorToken The Binder token referencing the system Activity that wants to delegate
+ * the #startActivityAsCaller to another app. The "caller" will be the caller of this
+ * activity's token, not the delegate's caller (which is probably the delegator itself).
+ *
+ * @return Returns a token that can be given to a "delegate" app that may call
+ * #startActivityAsCaller
+ */
@Override
- public final int startActivityAsCaller(IApplicationThread caller, String callingPackage,
- Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode,
- int startFlags, ProfilerInfo profilerInfo, Bundle bOptions, boolean ignoreTargetSecurity,
- int userId) {
+ public IBinder requestStartActivityPermissionToken(IBinder delegatorToken) {
+ int callingUid = Binder.getCallingUid();
+ if (UserHandle.getAppId(callingUid) != SYSTEM_UID) {
+ throw new SecurityException("Only the system process can request a permission token, " +
+ "received request from uid: " + callingUid);
+ }
+ IBinder permissionToken = new Binder();
+ synchronized (this) {
+ mStartActivitySources.put(permissionToken, delegatorToken);
+ }
+
+ Message expireMsg = mHandler.obtainMessage(EXPIRE_START_AS_CALLER_TOKEN_MSG,
+ permissionToken);
+ mHandler.sendMessageDelayed(expireMsg, START_AS_CALLER_TOKEN_TIMEOUT_IMPL);
+
+ Message forgetMsg = mHandler.obtainMessage(FORGET_START_AS_CALLER_TOKEN_MSG,
+ permissionToken);
+ mHandler.sendMessageDelayed(forgetMsg, START_AS_CALLER_TOKEN_EXPIRED_TIMEOUT);
+ return permissionToken;
+ }
+
+ @Override
+ public final int startActivityAsCaller(IApplicationThread caller,
+ String callingPackage, Intent intent, String resolvedType, IBinder resultTo,
+ String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo,
+ Bundle bOptions, IBinder permissionToken, boolean ignoreTargetSecurity, int userId) {
// This is very dangerous -- it allows you to perform a start activity (including
- // permission grants) as any app that may launch one of your own activities. So
- // we will only allow this to be done from activities that are part of the core framework,
- // and then only when they are running as the system.
+ // permission grants) as any app that may launch one of your own activities. So we only
+ // allow this in two cases:
+ // 1) The caller is an activity that is part of the core framework, and then only when it
+ // is running as the system.
+ // 2) The caller provides a valid permissionToken. Permission tokens are one-time use and
+ // can only be requested by a system activity, which may then delegate this call to
+ // another app.
final ActivityRecord sourceRecord;
final int targetUid;
final String targetPackage;
@@ -4715,17 +4878,47 @@ public class ActivityManagerService extends IActivityManager.Stub
if (resultTo == null) {
throw new SecurityException("Must be called from an activity");
}
- sourceRecord = mStackSupervisor.isInAnyStackLocked(resultTo);
- if (sourceRecord == null) {
- throw new SecurityException("Called with bad activity token: " + resultTo);
+
+ final IBinder sourceToken;
+ if (permissionToken != null) {
+ // To even attempt to use a permissionToken, an app must also have this signature
+ // permission.
+ enforceCallingPermission(android.Manifest.permission.START_ACTIVITY_AS_CALLER,
+ "startActivityAsCaller");
+ // If called with a permissionToken, we want the sourceRecord from the delegator
+ // activity that requested this token.
+ sourceToken =
+ mStartActivitySources.remove(permissionToken);
+ if (sourceToken == null) {
+ // Invalid permissionToken, check if it recently expired.
+ if (mExpiredStartAsCallerTokens.contains(permissionToken)) {
+ throw new SecurityException("Called with expired permission token: "
+ + permissionToken);
+ } else {
+ throw new SecurityException("Called with invalid permission token: "
+ + permissionToken);
+ }
+ }
+ } else {
+ // This method was called directly by the source.
+ sourceToken = resultTo;
}
- if (!sourceRecord.info.packageName.equals("android")) {
- throw new SecurityException(
- "Must be called from an activity that is declared in the android package");
+
+ sourceRecord = mStackSupervisor.isInAnyStackLocked(sourceToken);
+ if (sourceRecord == null) {
+ throw new SecurityException("Called with bad activity token: " + sourceToken);
}
if (sourceRecord.app == null) {
throw new SecurityException("Called without a process attached to activity");
}
+
+ // Whether called directly or from a delegate, the source activity must be from the
+ // android package.
+ if (!sourceRecord.info.packageName.equals("android")) {
+ throw new SecurityException("Must be called from an activity that is " +
+ "declared in the android package");
+ }
+
if (UserHandle.getAppId(sourceRecord.app.uid) != SYSTEM_UID) {
// This is still okay, as long as this activity is running under the
// uid of the original calling activity.
@@ -4736,6 +4929,7 @@ public class ActivityManagerService extends IActivityManager.Stub
+ sourceRecord.launchedFromUid);
}
}
+
if (ignoreTargetSecurity) {
if (intent.getComponent() == null) {
throw new SecurityException(
@@ -4764,7 +4958,8 @@ public class ActivityManagerService extends IActivityManager.Stub
.setResultWho(resultWho)
.setRequestCode(requestCode)
.setStartFlags(startFlags)
- .setMayWait(bOptions, userId)
+ .setActivityOptions(bOptions)
+ .setMayWait(userId)
.setIgnoreTargetSecurity(ignoreTargetSecurity)
.execute();
} catch (SecurityException e) {
@@ -4800,7 +4995,8 @@ public class ActivityManagerService extends IActivityManager.Stub
.setResultWho(resultWho)
.setRequestCode(requestCode)
.setStartFlags(startFlags)
- .setMayWait(bOptions, userId)
+ .setActivityOptions(bOptions)
+ .setMayWait(userId)
.setProfilerInfo(profilerInfo)
.setWaitResult(res)
.execute();
@@ -4824,7 +5020,8 @@ public class ActivityManagerService extends IActivityManager.Stub
.setRequestCode(requestCode)
.setStartFlags(startFlags)
.setGlobalConfiguration(config)
- .setMayWait(bOptions, userId)
+ .setActivityOptions(bOptions)
+ .setMayWait(userId)
.execute();
}
@@ -4879,7 +5076,8 @@ public class ActivityManagerService extends IActivityManager.Stub
.setVoiceInteractor(interactor)
.setStartFlags(startFlags)
.setProfilerInfo(profilerInfo)
- .setMayWait(bOptions, userId)
+ .setActivityOptions(bOptions)
+ .setMayWait(userId)
.execute();
}
@@ -4894,27 +5092,22 @@ public class ActivityManagerService extends IActivityManager.Stub
.setCallingUid(callingUid)
.setCallingPackage(callingPackage)
.setResolvedType(resolvedType)
- .setMayWait(bOptions, userId)
+ .setActivityOptions(bOptions)
+ .setMayWait(userId)
.execute();
}
@Override
- public int startRecentsActivity(IAssistDataReceiver assistDataReceiver, Bundle options,
- Bundle activityOptions, int userId) {
- if (!mRecentTasks.isCallerRecents(Binder.getCallingUid())) {
- String msg = "Permission Denial: startRecentsActivity() from pid="
- + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid()
- + " not recent tasks package";
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
- }
-
- final int recentsUid = mRecentTasks.getRecentsComponentUid();
- final ComponentName recentsComponent = mRecentTasks.getRecentsComponent();
- final String recentsPackage = recentsComponent.getPackageName();
+ public void startRecentsActivity(Intent intent, IAssistDataReceiver assistDataReceiver,
+ IRecentsAnimationRunner recentsAnimationRunner) {
+ enforceCallerIsRecentsOrHasPermission(MANAGE_ACTIVITY_STACKS, "startRecentsActivity()");
final long origId = Binder.clearCallingIdentity();
try {
synchronized (this) {
+ final int recentsUid = mRecentTasks.getRecentsComponentUid();
+ final ComponentName recentsComponent = mRecentTasks.getRecentsComponent();
+ final String recentsPackage = recentsComponent.getPackageName();
+
// If provided, kick off the request for the assist data in the background before
// starting the activity
if (assistDataReceiver != null) {
@@ -4931,16 +5124,24 @@ public class ActivityManagerService extends IActivityManager.Stub
recentsUid, recentsPackage);
}
- final Intent intent = new Intent();
- intent.setFlags(FLAG_ACTIVITY_NEW_TASK);
- intent.setComponent(recentsComponent);
- intent.putExtras(options);
+ // Start a new recents animation
+ final RecentsAnimation anim = new RecentsAnimation(this, mStackSupervisor,
+ mActivityStartController, mWindowManager, mUserController);
+ anim.startRecentsActivity(intent, recentsAnimationRunner, recentsComponent,
+ recentsUid);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
- return mActivityStartController.obtainStarter(intent, "startRecentsActivity")
- .setCallingUid(recentsUid)
- .setCallingPackage(recentsPackage)
- .setMayWait(activityOptions, userId)
- .execute();
+ @Override
+ public void cancelRecentsAnimation() {
+ enforceCallerIsRecentsOrHasPermission(MANAGE_ACTIVITY_STACKS, "cancelRecentsAnimation()");
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ synchronized (this) {
+ mWindowManager.cancelRecentsAnimation();
}
} finally {
Binder.restoreCallingIdentity(origId);
@@ -5027,17 +5228,17 @@ public class ActivityManagerService extends IActivityManager.Stub
if (intent != null && intent.hasFileDescriptors() == true) {
throw new IllegalArgumentException("File descriptors passed in Intent");
}
- ActivityOptions options = ActivityOptions.fromBundle(bOptions);
+ SafeActivityOptions options = SafeActivityOptions.fromBundle(bOptions);
synchronized (this) {
final ActivityRecord r = ActivityRecord.isInStackLocked(callingActivity);
if (r == null) {
- ActivityOptions.abort(options);
+ SafeActivityOptions.abort(options);
return false;
}
if (r.app == null || r.app.thread == null) {
// The caller is not running... d'oh!
- ActivityOptions.abort(options);
+ SafeActivityOptions.abort(options);
return false;
}
intent = new Intent(intent);
@@ -5082,7 +5283,7 @@ public class ActivityManagerService extends IActivityManager.Stub
if (aInfo == null) {
// Nobody who is next!
- ActivityOptions.abort(options);
+ SafeActivityOptions.abort(options);
if (debug) Slog.d(TAG, "Next matching activity: nothing found");
return false;
}
@@ -5144,10 +5345,13 @@ public class ActivityManagerService extends IActivityManager.Stub
enforceCallerIsRecentsOrHasPermission(START_TASKS_FROM_RECENTS,
"startActivityFromRecents()");
+ final int callingPid = Binder.getCallingPid();
+ final int callingUid = Binder.getCallingUid();
final long origId = Binder.clearCallingIdentity();
try {
synchronized (this) {
- return mStackSupervisor.startActivityFromRecents(taskId, bOptions);
+ return mStackSupervisor.startActivityFromRecents(callingPid, callingUid, taskId,
+ SafeActivityOptions.fromBundle(bOptions));
}
} finally {
Binder.restoreCallingIdentity(origId);
@@ -5164,7 +5368,8 @@ public class ActivityManagerService extends IActivityManager.Stub
userId, false, ALLOW_FULL_ONLY, reason, null);
// TODO: Switch to user app stacks here.
int ret = mActivityStartController.startActivities(caller, -1, callingPackage,
- intents, resolvedTypes, resultTo, bOptions, userId, reason);
+ intents, resolvedTypes, resultTo, SafeActivityOptions.fromBundle(bOptions), userId,
+ reason);
return ret;
}
@@ -6233,7 +6438,7 @@ public class ActivityManagerService extends IActivityManager.Stub
// Clear its pending alarms
AlarmManagerInternal ami = LocalServices.getService(AlarmManagerInternal.class);
- ami.removeAlarmsForUid(uid);
+ ami.removeAlarmsForUid(appInfo.uid);
}
} catch (RemoteException e) {
}
@@ -6539,13 +6744,17 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
infos[i] = new Debug.MemoryInfo();
+ long startTime = SystemClock.currentThreadTimeMillis();
Debug.getMemoryInfo(pids[i], infos[i]);
+ long endTime = SystemClock.currentThreadTimeMillis();
if (proc != null) {
synchronized (this) {
if (proc.thread != null && proc.setAdj == oomAdj) {
// Record this for posterity if the process has been stable.
proc.baseProcessTracker.addPss(infos[i].getTotalPss(),
- infos[i].getTotalUss(), false, proc.pkgList);
+ infos[i].getTotalUss(), false,
+ ProcessStats.ADD_PSS_EXTERNAL_SLOW, endTime-startTime,
+ proc.pkgList);
}
}
}
@@ -6567,12 +6776,15 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
long[] tmpUss = new long[1];
+ long startTime = SystemClock.currentThreadTimeMillis();
pss[i] = Debug.getPss(pids[i], tmpUss, null);
+ long endTime = SystemClock.currentThreadTimeMillis();
if (proc != null) {
synchronized (this) {
if (proc.thread != null && proc.setAdj == oomAdj) {
// Record this for posterity if the process has been stable.
- proc.baseProcessTracker.addPss(pss[i], tmpUss[0], false, proc.pkgList);
+ proc.baseProcessTracker.addPss(pss[i], tmpUss[0], false,
+ ProcessStats.ADD_PSS_EXTERNAL, endTime-startTime, proc.pkgList);
}
}
}
@@ -7244,15 +7456,22 @@ public class ActivityManagerService extends IActivityManager.Stub
}
ProfilerInfo profilerInfo = null;
- String agent = null;
+ String preBindAgent = null;
if (mProfileApp != null && mProfileApp.equals(processName)) {
mProfileProc = app;
- profilerInfo = (mProfilerInfo != null && mProfilerInfo.profileFile != null) ?
- new ProfilerInfo(mProfilerInfo) : null;
- agent = mProfilerInfo != null ? mProfilerInfo.agent : null;
+ if (mProfilerInfo != null) {
+ // Send a profiler info object to the app if either a file is given, or
+ // an agent should be loaded at bind-time.
+ boolean needsInfo = mProfilerInfo.profileFile != null
+ || mProfilerInfo.attachAgentDuringBind;
+ profilerInfo = needsInfo ? new ProfilerInfo(mProfilerInfo) : null;
+ if (!mProfilerInfo.attachAgentDuringBind) {
+ preBindAgent = mProfilerInfo.agent;
+ }
+ }
} else if (app.instr != null && app.instr.mProfileFile != null) {
profilerInfo = new ProfilerInfo(app.instr.mProfileFile, null, 0, false, false,
- null);
+ null, false);
}
boolean enableTrackAllocation = false;
@@ -7321,8 +7540,8 @@ public class ActivityManagerService extends IActivityManager.Stub
// If we were asked to attach an agent on startup, do so now, before we're binding
// application code.
- if (agent != null) {
- thread.attachAgent(agent);
+ if (preBindAgent != null) {
+ thread.attachAgent(preBindAgent);
}
checkTime(startTime, "attachApplicationLocked: immediately before bindApplication");
@@ -7909,9 +8128,9 @@ public class ActivityManagerService extends IActivityManager.Stub
flags &= ~(PendingIntent.FLAG_NO_CREATE|PendingIntent.FLAG_CANCEL_CURRENT
|PendingIntent.FLAG_UPDATE_CURRENT);
- PendingIntentRecord.Key key = new PendingIntentRecord.Key(
- type, packageName, activity, resultWho,
- requestCode, intents, resolvedTypes, flags, bOptions, userId);
+ PendingIntentRecord.Key key = new PendingIntentRecord.Key(type, packageName, activity,
+ resultWho, requestCode, intents, resolvedTypes, flags,
+ SafeActivityOptions.fromBundle(bOptions), userId);
WeakReference<PendingIntentRecord> ref;
ref = mIntentSenderRecords.get(key);
PendingIntentRecord rec = ref != null ? ref.get() : null;
@@ -8360,8 +8579,7 @@ public class ActivityManagerService extends IActivityManager.Stub
stack.setPictureInPictureAspectRatio(aspectRatio);
stack.setPictureInPictureActions(actions);
- MetricsLogger.action(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_ENTERED,
- r.supportsEnterPipOnTaskSwitch);
+ MetricsLoggerWrapper.logPictureInPictureEnter(mContext, r.supportsEnterPipOnTaskSwitch);
logPictureInPictureArgs(params);
};
@@ -8370,22 +8588,12 @@ public class ActivityManagerService extends IActivityManager.Stub
// entering picture-in-picture (this will prompt the user to authenticate if the
// device is currently locked).
try {
- dismissKeyguard(token, new IKeyguardDismissCallback.Stub() {
- @Override
- public void onDismissError() throws RemoteException {
- // Do nothing
- }
-
+ dismissKeyguard(token, new KeyguardDismissCallback() {
@Override
public void onDismissSucceeded() throws RemoteException {
mHandler.post(enterPipRunnable);
}
-
- @Override
- public void onDismissCancelled() throws RemoteException {
- // Do nothing
- }
- });
+ }, null /* message */);
} catch (RemoteException e) {
// Local call
}
@@ -8583,6 +8791,16 @@ public class ActivityManagerService extends IActivityManager.Stub
}
return false;
}
+
+ @Override
+ public int getPackageUid(String packageName, int flags) {
+ try {
+ return mActivityManagerService.mContext.getPackageManager()
+ .getPackageUid(packageName, flags);
+ } catch (NameNotFoundException nnfe) {
+ return -1;
+ }
+ }
}
class IntentFirewallInterface implements IntentFirewall.AMSInterface {
@@ -8806,7 +9024,7 @@ public class ActivityManagerService extends IActivityManager.Stub
case AppOpsManager.MODE_ALLOWED:
// If force-background-check is enabled, restrict all apps that aren't whitelisted.
if (mForceBackgroundCheck &&
- UserHandle.isApp(uid) &&
+ !UserHandle.isCore(uid) &&
!isOnDeviceIdleWhitelistLocked(uid)) {
if (DEBUG_BACKGROUND_CHECK) {
Slog.i(TAG, "Force background check: " +
@@ -10386,10 +10604,9 @@ public class ActivityManagerService extends IActivityManager.Stub
@Override
public Bitmap getTaskDescriptionIcon(String filePath, int userId) {
- if (userId != UserHandle.getCallingUserId()) {
- enforceCallingPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
- "getTaskDescriptionIcon");
- }
+ userId = mUserController.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
+ userId, false, ALLOW_FULL_ONLY, "getTaskDescriptionIcon", null);
+
final File passedIconFile = new File(filePath);
final File legitIconFile = new File(TaskPersister.getUserImagesDir(userId),
passedIconFile.getName());
@@ -10404,9 +10621,13 @@ public class ActivityManagerService extends IActivityManager.Stub
@Override
public void startInPlaceAnimationOnFrontMostApplication(Bundle opts)
throws RemoteException {
- final ActivityOptions activityOptions = ActivityOptions.fromBundle(opts);
- if (activityOptions.getAnimationType() != ActivityOptions.ANIM_CUSTOM_IN_PLACE ||
- activityOptions.getCustomInPlaceResId() == 0) {
+ final SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(opts);
+ final ActivityOptions activityOptions = safeOptions != null
+ ? safeOptions.getOptions(mStackSupervisor)
+ : null;
+ if (activityOptions == null
+ || activityOptions.getAnimationType() != ActivityOptions.ANIM_CUSTOM_IN_PLACE
+ || activityOptions.getCustomInPlaceResId() == 0) {
throw new IllegalArgumentException("Expected in-place ActivityOption " +
"with valid animation");
}
@@ -10509,16 +10730,17 @@ public class ActivityManagerService extends IActivityManager.Stub
if (DEBUG_STACK) Slog.d(TAG_STACK, "moveTaskToFront: moving taskId=" + taskId);
synchronized(this) {
- moveTaskToFrontLocked(taskId, flags, bOptions, false /* fromRecents */);
+ moveTaskToFrontLocked(taskId, flags, SafeActivityOptions.fromBundle(bOptions),
+ false /* fromRecents */);
}
}
- void moveTaskToFrontLocked(int taskId, int flags, Bundle bOptions, boolean fromRecents) {
- ActivityOptions options = ActivityOptions.fromBundle(bOptions);
+ void moveTaskToFrontLocked(int taskId, int flags, SafeActivityOptions options,
+ boolean fromRecents) {
if (!checkAppSwitchAllowedLocked(Binder.getCallingPid(),
Binder.getCallingUid(), -1, -1, "Task to front")) {
- ActivityOptions.abort(options);
+ SafeActivityOptions.abort(options);
return;
}
final long origId = Binder.clearCallingIdentity();
@@ -10532,7 +10754,10 @@ public class ActivityManagerService extends IActivityManager.Stub
Slog.e(TAG, "moveTaskToFront: Attempt to violate Lock Task Mode");
return;
}
- mStackSupervisor.findTaskToMoveToFront(task, flags, options, "moveTaskToFront",
+ ActivityOptions realOptions = options != null
+ ? options.getOptions(mStackSupervisor)
+ : null;
+ mStackSupervisor.findTaskToMoveToFront(task, flags, realOptions, "moveTaskToFront",
false /* forceNonResizable */);
final ActivityRecord topActivity = task.getTopActivity();
@@ -10546,7 +10771,7 @@ public class ActivityManagerService extends IActivityManager.Stub
} finally {
Binder.restoreCallingIdentity(origId);
}
- ActivityOptions.abort(options);
+ SafeActivityOptions.abort(options);
}
/**
@@ -12245,6 +12470,7 @@ public class ActivityManagerService extends IActivityManager.Stub
mConstants.start(mContext.getContentResolver());
mCoreSettingsObserver = new CoreSettingsObserver(this);
mFontScaleSettingObserver = new FontScaleSettingObserver();
+ GlobalSettingsToPropertiesMapper.start(mContext.getContentResolver());
// Now that the settings provider is published we can consider sending
// in a rescue party.
@@ -13037,6 +13263,9 @@ public class ActivityManagerService extends IActivityManager.Stub
case ActivityManager.BUGREPORT_OPTION_TELEPHONY:
extraOptions = "bugreporttelephony";
break;
+ case ActivityManager.BUGREPORT_OPTION_WIFI:
+ extraOptions = "bugreportwifi";
+ break;
default:
throw new IllegalArgumentException("Provided bugreport type is not correct, value: "
+ bugreportType);
@@ -13058,9 +13287,8 @@ public class ActivityManagerService extends IActivityManager.Stub
* No new code should be calling it.
*/
@Deprecated
- @Override
- public void requestTelephonyBugReport(String shareTitle, String shareDescription) {
-
+ private void requestBugReportWithDescription(String shareTitle, String shareDescription,
+ int bugreportType) {
if (!TextUtils.isEmpty(shareTitle)) {
if (shareTitle.length() > MAX_BUGREPORT_TITLE_SIZE) {
String errorStr = "shareTitle should be less than " +
@@ -13089,9 +13317,34 @@ public class ActivityManagerService extends IActivityManager.Stub
Slog.d(TAG, "Bugreport notification title " + shareTitle
+ " description " + shareDescription);
- requestBugReport(ActivityManager.BUGREPORT_OPTION_TELEPHONY);
+ requestBugReport(bugreportType);
}
+ /**
+ * @deprecated This method is only used by a few internal components and it will soon be
+ * replaced by a proper bug report API (which will be restricted to a few, pre-defined apps).
+ * No new code should be calling it.
+ */
+ @Deprecated
+ @Override
+ public void requestTelephonyBugReport(String shareTitle, String shareDescription) {
+ requestBugReportWithDescription(shareTitle, shareDescription,
+ ActivityManager.BUGREPORT_OPTION_TELEPHONY);
+ }
+
+ /**
+ * @deprecated This method is only used by a few internal components and it will soon be
+ * replaced by a proper bug report API (which will be restricted to a few, pre-defined apps).
+ * No new code should be calling it.
+ */
+ @Deprecated
+ @Override
+ public void requestWifiBugReport(String shareTitle, String shareDescription) {
+ requestBugReportWithDescription(shareTitle, shareDescription,
+ ActivityManager.BUGREPORT_OPTION_WIFI);
+ }
+
+
public static long getInputDispatchingTimeoutLocked(ActivityRecord r) {
return r != null ? getInputDispatchingTimeoutLocked(r.app) : KEY_DISPATCHING_TIMEOUT;
}
@@ -13503,6 +13756,7 @@ public class ActivityManagerService extends IActivityManager.Stub
@Override
public boolean convertToTranslucent(IBinder token, Bundle options) {
+ SafeActivityOptions safeOptions = SafeActivityOptions.fromBundle(options);
final long origId = Binder.clearCallingIdentity();
try {
synchronized (this) {
@@ -13514,7 +13768,7 @@ public class ActivityManagerService extends IActivityManager.Stub
int index = task.mActivities.lastIndexOf(r);
if (index > 0) {
ActivityRecord under = task.mActivities.get(index - 1);
- under.returningOptions = ActivityOptions.fromBundle(options);
+ under.returningOptions = safeOptions != null ? safeOptions.getOptions(r) : null;
}
final boolean translucentChanged = r.changeWindowTranslucency(false);
if (translucentChanged) {
@@ -13659,7 +13913,8 @@ public class ActivityManagerService extends IActivityManager.Stub
* not.
*/
private void enforceSystemHasVrFeature() {
- if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_VR_MODE)) {
+ if (!mContext.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_VR_MODE_HIGH_PERFORMANCE)) {
throw new UnsupportedOperationException("VR mode not supported on this device!");
}
}
@@ -13718,9 +13973,7 @@ public class ActivityManagerService extends IActivityManager.Stub
@Override
public int setVrMode(IBinder token, boolean enabled, ComponentName packageName) {
- if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_VR_MODE)) {
- throw new UnsupportedOperationException("VR mode not supported on this device!");
- }
+ enforceSystemHasVrFeature();
final VrManagerInternal vrService = LocalServices.getService(VrManagerInternal.class);
@@ -13752,9 +14005,7 @@ public class ActivityManagerService extends IActivityManager.Stub
@Override
public boolean isVrModePackageEnabled(ComponentName packageName) {
- if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_VR_MODE)) {
- throw new UnsupportedOperationException("VR mode not supported on this device!");
- }
+ enforceSystemHasVrFeature();
final VrManagerInternal vrService = LocalServices.getService(VrManagerInternal.class);
@@ -13858,68 +14109,100 @@ public class ActivityManagerService extends IActivityManager.Stub
Context.WINDOW_SERVICE)).addView(v, lp);
}
- public void noteWakeupAlarm(IIntentSender sender, int sourceUid, String sourcePkg, String tag) {
- if (sender != null && !(sender instanceof PendingIntentRecord)) {
- return;
+ @Override
+ public void noteWakeupAlarm(IIntentSender sender, WorkSource workSource, int sourceUid,
+ String sourcePkg, String tag) {
+ if (workSource != null && workSource.isEmpty()) {
+ workSource = null;
}
- final PendingIntentRecord rec = (PendingIntentRecord)sender;
- final BatteryStatsImpl stats = mBatteryStatsService.getActiveStatistics();
- synchronized (stats) {
- if (mBatteryStatsService.isOnBattery()) {
- mBatteryStatsService.enforceCallingPermission();
- int MY_UID = Binder.getCallingUid();
- final int uid;
- if (sender == null) {
- uid = sourceUid;
- } else {
- uid = rec.uid == MY_UID ? SYSTEM_UID : rec.uid;
+
+ if (sourceUid <= 0 && workSource == null) {
+ // Try and derive a UID to attribute things to based on the caller.
+ if (sender != null) {
+ if (!(sender instanceof PendingIntentRecord)) {
+ return;
}
- BatteryStatsImpl.Uid.Pkg pkg =
- 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,
- tag);
+
+ final PendingIntentRecord rec = (PendingIntentRecord) sender;
+ final int callerUid = Binder.getCallingUid();
+ sourceUid = rec.uid == callerUid ? SYSTEM_UID : rec.uid;
+ } else {
+ // TODO(narayan): Should we throw an exception in this case ? It means that we
+ // haven't been able to derive a UID to attribute things to.
+ return;
}
}
+
+ if (DEBUG_POWER) {
+ Slog.w(TAG, "noteWakupAlarm[ sourcePkg=" + sourcePkg + ", sourceUid=" + sourceUid
+ + ", workSource=" + workSource + ", tag=" + tag + "]");
+ }
+
+ mBatteryStatsService.noteWakupAlarm(sourcePkg, sourceUid, workSource, tag);
}
- public void noteAlarmStart(IIntentSender sender, int sourceUid, String tag) {
- if (sender != null && !(sender instanceof PendingIntentRecord)) {
- return;
+ @Override
+ public void noteAlarmStart(IIntentSender sender, WorkSource workSource, int sourceUid,
+ String tag) {
+ if (workSource != null && workSource.isEmpty()) {
+ workSource = null;
}
- final PendingIntentRecord rec = (PendingIntentRecord)sender;
- final BatteryStatsImpl stats = mBatteryStatsService.getActiveStatistics();
- synchronized (stats) {
- mBatteryStatsService.enforceCallingPermission();
- int MY_UID = Binder.getCallingUid();
- final int uid;
- if (sender == null) {
- uid = sourceUid;
+
+ if (sourceUid <= 0 && workSource == null) {
+ // Try and derive a UID to attribute things to based on the caller.
+ if (sender != null) {
+ if (!(sender instanceof PendingIntentRecord)) {
+ return;
+ }
+
+ final PendingIntentRecord rec = (PendingIntentRecord) sender;
+ final int callerUid = Binder.getCallingUid();
+ sourceUid = rec.uid == callerUid ? SYSTEM_UID : rec.uid;
} else {
- uid = rec.uid == MY_UID ? SYSTEM_UID : rec.uid;
+ // TODO(narayan): Should we throw an exception in this case ? It means that we
+ // haven't been able to derive a UID to attribute things to.
+ return;
}
- mBatteryStatsService.noteAlarmStart(tag, sourceUid >= 0 ? sourceUid : uid);
}
+
+ if (DEBUG_POWER) {
+ Slog.w(TAG, "noteAlarmStart[sourceUid=" + sourceUid + ", workSource=" + workSource +
+ ", tag=" + tag + "]");
+ }
+
+ mBatteryStatsService.noteAlarmStart(tag, workSource, sourceUid);
}
- public void noteAlarmFinish(IIntentSender sender, int sourceUid, String tag) {
- if (sender != null && !(sender instanceof PendingIntentRecord)) {
- return;
+ @Override
+ public void noteAlarmFinish(IIntentSender sender, WorkSource workSource, int sourceUid,
+ String tag) {
+ if (workSource != null && workSource.isEmpty()) {
+ workSource = null;
}
- final PendingIntentRecord rec = (PendingIntentRecord)sender;
- final BatteryStatsImpl stats = mBatteryStatsService.getActiveStatistics();
- synchronized (stats) {
- mBatteryStatsService.enforceCallingPermission();
- int MY_UID = Binder.getCallingUid();
- final int uid;
- if (sender == null) {
- uid = sourceUid;
+
+ if (sourceUid <= 0 && workSource == null) {
+ // Try and derive a UID to attribute things to based on the caller.
+ if (sender != null) {
+ if (!(sender instanceof PendingIntentRecord)) {
+ return;
+ }
+
+ final PendingIntentRecord rec = (PendingIntentRecord) sender;
+ final int callerUid = Binder.getCallingUid();
+ sourceUid = rec.uid == callerUid ? SYSTEM_UID : rec.uid;
} else {
- uid = rec.uid == MY_UID ? SYSTEM_UID : rec.uid;
+ // TODO(narayan): Should we throw an exception in this case ? It means that we
+ // haven't been able to derive a UID to attribute things to.
+ return;
}
- mBatteryStatsService.noteAlarmFinish(tag, sourceUid >= 0 ? sourceUid : uid);
}
+
+ if (DEBUG_POWER) {
+ Slog.w(TAG, "noteAlarmFinish[sourceUid=" + sourceUid + ", workSource=" + workSource +
+ ", tag=" + tag + "]");
+ }
+
+ mBatteryStatsService.noteAlarmFinish(tag, workSource, sourceUid);
}
public boolean killPids(int[] pids, String pReason, boolean secure) {
@@ -14126,7 +14409,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_FOREGROUND_SERVICE
+ if (proc.setProcState >= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
&& proc.setProcState <= ActivityManager.PROCESS_STATE_SERVICE) {
if (doKilling && proc.initialIdlePss != 0
&& proc.lastPss > ((proc.initialIdlePss*3)/2)) {
@@ -14843,6 +15126,7 @@ public class ActivityManagerService extends IActivityManager.Stub
(process != null && process.info != null) ?
(process.info.isInstantApp() ? 1 : 0) : -1,
activity != null ? activity.shortComponentName : null,
+ activity != null ? activity.packageName : null,
process != null ? (process.isInterestingToUserLocked() ? 1 : 0) : -1);
// Rate-limit how often we're willing to do the heavy lifting below to
@@ -15169,7 +15453,6 @@ public class ActivityManagerService extends IActivityManager.Stub
boolean dumpVisibleStacksOnly = false;
boolean dumpFocusedStackOnly = false;
String dumpPackage = null;
- int dumpAppId = -1;
int opti = 0;
while (opti < args.length) {
@@ -15243,6 +15526,15 @@ public class ActivityManagerService extends IActivityManager.Stub
}
} else if ("service".equals(cmd)) {
mServices.writeToProto(proto);
+ } else if ("processes".equals(cmd) || "p".equals(cmd)) {
+ if (opti < args.length) {
+ dumpPackage = args[opti];
+ opti++;
+ }
+ // output proto is ProcessProto
+ synchronized (this) {
+ writeProcessesToProtoLocked(proto, dumpPackage);
+ }
} else {
// default option, dump everything, output is ActivityManagerServiceProto
synchronized (this) {
@@ -15257,6 +15549,10 @@ public class ActivityManagerService extends IActivityManager.Stub
long serviceToken = proto.start(ActivityManagerServiceProto.SERVICES);
mServices.writeToProto(proto);
proto.end(serviceToken);
+
+ long processToken = proto.start(ActivityManagerServiceProto.PROCESSES);
+ writeProcessesToProtoLocked(proto, dumpPackage);
+ proto.end(processToken);
}
}
proto.flush();
@@ -15264,16 +15560,7 @@ public class ActivityManagerService extends IActivityManager.Stub
return;
}
- if (dumpPackage != null) {
- try {
- ApplicationInfo info = mContext.getPackageManager().getApplicationInfo(
- dumpPackage, 0);
- dumpAppId = UserHandle.getAppId(info.uid);
- } catch (NameNotFoundException e) {
- e.printStackTrace();
- }
- }
-
+ int dumpAppId = getAppId(dumpPackage);
boolean more = false;
// Is the caller requesting to dump a particular piece of data?
if (opti < args.length) {
@@ -15315,33 +15602,17 @@ public class ActivityManagerService extends IActivityManager.Stub
pw.println(BinderInternal.nGetBinderProxyCount(Integer.parseInt(uid)));
}
} else if ("broadcasts".equals(cmd) || "b".equals(cmd)) {
- String[] newArgs;
- String name;
- if (opti >= args.length) {
- name = null;
- newArgs = EMPTY_STRING_ARRAY;
- } else {
+ if (opti < args.length) {
dumpPackage = args[opti];
opti++;
- newArgs = new String[args.length - opti];
- if (args.length > 2) System.arraycopy(args, opti, newArgs, 0,
- args.length - opti);
}
synchronized (this) {
dumpBroadcastsLocked(fd, pw, args, opti, true, dumpPackage);
}
} else if ("broadcast-stats".equals(cmd)) {
- String[] newArgs;
- String name;
- if (opti >= args.length) {
- name = null;
- newArgs = EMPTY_STRING_ARRAY;
- } else {
+ if (opti < args.length) {
dumpPackage = args[opti];
opti++;
- newArgs = new String[args.length - opti];
- if (args.length > 2) System.arraycopy(args, opti, newArgs, 0,
- args.length - opti);
}
synchronized (this) {
if (dumpCheckinFormat) {
@@ -15352,33 +15623,17 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
} else if ("intents".equals(cmd) || "i".equals(cmd)) {
- String[] newArgs;
- String name;
- if (opti >= args.length) {
- name = null;
- newArgs = EMPTY_STRING_ARRAY;
- } else {
+ if (opti < args.length) {
dumpPackage = args[opti];
opti++;
- newArgs = new String[args.length - opti];
- if (args.length > 2) System.arraycopy(args, opti, newArgs, 0,
- args.length - opti);
}
synchronized (this) {
dumpPendingIntentsLocked(fd, pw, args, opti, true, dumpPackage);
}
} else if ("processes".equals(cmd) || "p".equals(cmd)) {
- String[] newArgs;
- String name;
- if (opti >= args.length) {
- name = null;
- newArgs = EMPTY_STRING_ARRAY;
- } else {
+ if (opti < args.length) {
dumpPackage = args[opti];
opti++;
- newArgs = new String[args.length - opti];
- if (args.length > 2) System.arraycopy(args, opti, newArgs, 0,
- args.length - opti);
}
synchronized (this) {
dumpProcessesLocked(fd, pw, args, opti, true, dumpPackage, dumpAppId);
@@ -15799,8 +16054,21 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
+ private int getAppId(String dumpPackage) {
+ if (dumpPackage != null) {
+ try {
+ ApplicationInfo info = mContext.getPackageManager().getApplicationInfo(
+ dumpPackage, 0);
+ return UserHandle.getAppId(info.uid);
+ } catch (NameNotFoundException e) {
+ e.printStackTrace();
+ }
+ }
+ return -1;
+ }
+
boolean dumpUids(PrintWriter pw, String dumpPackage, int dumpAppId, SparseArray<UidRecord> uids,
- String header, boolean needSep) {
+ String header, boolean needSep) {
boolean printed = false;
for (int i=0; i<uids.size(); i++) {
UidRecord uidRec = uids.valueAt(i);
@@ -16018,7 +16286,7 @@ public class ActivityManagerService extends IActivityManager.Stub
"OnHold Norm", "OnHold PERS", dumpPackage);
}
- needSep = dumpProcessesToGc(fd, pw, args, opti, needSep, dumpAll, dumpPackage);
+ needSep = dumpProcessesToGc(pw, needSep, dumpPackage);
needSep = mAppErrors.dumpLocked(fd, pw, needSep, dumpPackage);
@@ -16303,8 +16571,327 @@ public class ActivityManagerService extends IActivityManager.Stub
pw.println(" mForceBackgroundCheck=" + mForceBackgroundCheck);
}
- boolean dumpProcessesToGc(FileDescriptor fd, PrintWriter pw, String[] args,
- int opti, boolean needSep, boolean dumpAll, String dumpPackage) {
+ void writeProcessesToProtoLocked(ProtoOutputStream proto, String dumpPackage) {
+ int numPers = 0;
+
+ final int NP = mProcessNames.getMap().size();
+ for (int ip=0; ip<NP; ip++) {
+ SparseArray<ProcessRecord> procs = mProcessNames.getMap().valueAt(ip);
+ final int NA = procs.size();
+ for (int ia = 0; ia<NA; ia++) {
+ ProcessRecord r = procs.valueAt(ia);
+ if (dumpPackage != null && !r.pkgList.containsKey(dumpPackage)) {
+ continue;
+ }
+ r.writeToProto(proto, ProcessesProto.PROCS);
+ if (r.persistent) {
+ numPers++;
+ }
+ }
+ }
+
+ for (int i=0; i<mIsolatedProcesses.size(); i++) {
+ ProcessRecord r = mIsolatedProcesses.valueAt(i);
+ if (dumpPackage != null && !r.pkgList.containsKey(dumpPackage)) {
+ continue;
+ }
+ r.writeToProto(proto, ProcessesProto.ISOLATED_PROCS);
+ }
+
+ for (int i=0; i<mActiveInstrumentation.size(); i++) {
+ ActiveInstrumentation ai = mActiveInstrumentation.get(i);
+ if (dumpPackage != null && !ai.mClass.getPackageName().equals(dumpPackage)
+ && !ai.mTargetInfo.packageName.equals(dumpPackage)) {
+ continue;
+ }
+ ai.writeToProto(proto, ProcessesProto.ACTIVE_INSTRUMENTATIONS);
+ }
+
+ int whichAppId = getAppId(dumpPackage);
+ for (int i=0; i<mActiveUids.size(); i++) {
+ UidRecord uidRec = mActiveUids.valueAt(i);
+ if (dumpPackage != null && UserHandle.getAppId(uidRec.uid) != whichAppId) {
+ continue;
+ }
+ uidRec.writeToProto(proto, ProcessesProto.ACTIVE_UIDS);
+ }
+
+ for (int i=0; i<mValidateUids.size(); i++) {
+ UidRecord uidRec = mValidateUids.valueAt(i);
+ if (dumpPackage != null && UserHandle.getAppId(uidRec.uid) != whichAppId) {
+ continue;
+ }
+ uidRec.writeToProto(proto, ProcessesProto.VALIDATE_UIDS);
+ }
+
+ if (mLruProcesses.size() > 0) {
+ long lruToken = proto.start(ProcessesProto.LRU_PROCS);
+ int total = mLruProcesses.size();
+ proto.write(ProcessesProto.LruProcesses.SIZE, total);
+ proto.write(ProcessesProto.LruProcesses.NON_ACT_AT, total-mLruProcessActivityStart);
+ proto.write(ProcessesProto.LruProcesses.NON_SVC_AT, total-mLruProcessServiceStart);
+ writeProcessOomListToProto(proto, ProcessesProto.LruProcesses.LIST, this,
+ mLruProcesses,false, dumpPackage);
+ proto.end(lruToken);
+ }
+
+ if (dumpPackage != null) {
+ synchronized (mPidsSelfLocked) {
+ for (int i=0; i<mPidsSelfLocked.size(); i++) {
+ ProcessRecord r = mPidsSelfLocked.valueAt(i);
+ if (!r.pkgList.containsKey(dumpPackage)) {
+ continue;
+ }
+ r.writeToProto(proto, ProcessesProto.PIDS_SELF_LOCKED);
+ }
+ }
+ }
+
+ if (mImportantProcesses.size() > 0) {
+ synchronized (mPidsSelfLocked) {
+ for (int i=0; i<mImportantProcesses.size(); i++) {
+ ImportanceToken it = mImportantProcesses.valueAt(i);
+ ProcessRecord r = mPidsSelfLocked.get(it.pid);
+ if (dumpPackage != null && (r == null
+ || !r.pkgList.containsKey(dumpPackage))) {
+ continue;
+ }
+ it.writeToProto(proto, ProcessesProto.IMPORTANT_PROCS);
+ }
+ }
+ }
+
+ for (int i=0; i<mPersistentStartingProcesses.size(); i++) {
+ ProcessRecord r = mPersistentStartingProcesses.get(i);
+ if (dumpPackage != null && !dumpPackage.equals(r.info.packageName)) {
+ continue;
+ }
+ r.writeToProto(proto, ProcessesProto.PERSISTENT_STARTING_PROCS);
+ }
+
+ for (int i=0; i<mRemovedProcesses.size(); i++) {
+ ProcessRecord r = mRemovedProcesses.get(i);
+ if (dumpPackage != null && !dumpPackage.equals(r.info.packageName)) {
+ continue;
+ }
+ r.writeToProto(proto, ProcessesProto.REMOVED_PROCS);
+ }
+
+ for (int i=0; i<mProcessesOnHold.size(); i++) {
+ ProcessRecord r = mProcessesOnHold.get(i);
+ if (dumpPackage != null && !dumpPackage.equals(r.info.packageName)) {
+ continue;
+ }
+ r.writeToProto(proto, ProcessesProto.ON_HOLD_PROCS);
+ }
+
+ writeProcessesToGcToProto(proto, ProcessesProto.GC_PROCS, dumpPackage);
+ mAppErrors.writeToProto(proto, ProcessesProto.APP_ERRORS, dumpPackage);
+
+ if (dumpPackage == null) {
+ mUserController.writeToProto(proto, ProcessesProto.USER_CONTROLLER);
+ getGlobalConfiguration().writeToProto(proto, ProcessesProto.GLOBAL_CONFIGURATION);
+ proto.write(ProcessesProto.CONFIG_WILL_CHANGE, getFocusedStack().mConfigWillChange);
+ }
+
+ if (mHomeProcess != null && (dumpPackage == null
+ || mHomeProcess.pkgList.containsKey(dumpPackage))) {
+ mHomeProcess.writeToProto(proto, ProcessesProto.HOME_PROC);
+ }
+
+ if (mPreviousProcess != null && (dumpPackage == null
+ || mPreviousProcess.pkgList.containsKey(dumpPackage))) {
+ mPreviousProcess.writeToProto(proto, ProcessesProto.PREVIOUS_PROC);
+ proto.write(ProcessesProto.PREVIOUS_PROC_VISIBLE_TIME_MS, mPreviousProcessVisibleTime);
+ }
+
+ if (mHeavyWeightProcess != null && (dumpPackage == null
+ || mHeavyWeightProcess.pkgList.containsKey(dumpPackage))) {
+ mHeavyWeightProcess.writeToProto(proto, ProcessesProto.HEAVY_WEIGHT_PROC);
+ }
+
+ for (Map.Entry<String, Integer> entry : mCompatModePackages.getPackages().entrySet()) {
+ String pkg = entry.getKey();
+ int mode = entry.getValue();
+ if (dumpPackage == null || dumpPackage.equals(pkg)) {
+ long compatToken = proto.start(ProcessesProto.SCREEN_COMPAT_PACKAGES);
+ proto.write(ProcessesProto.ScreenCompatPackage.PACKAGE, pkg);
+ proto.write(ProcessesProto.ScreenCompatPackage.MODE, mode);
+ proto.end(compatToken);
+ }
+ }
+
+ final int NI = mUidObservers.getRegisteredCallbackCount();
+ for (int i=0; i<NI; i++) {
+ final UidObserverRegistration reg = (UidObserverRegistration)
+ mUidObservers.getRegisteredCallbackCookie(i);
+ if (dumpPackage == null || dumpPackage.equals(reg.pkg)) {
+ reg.writeToProto(proto, ProcessesProto.UID_OBSERVERS);
+ }
+ }
+
+ for (int v : mDeviceIdleWhitelist) {
+ proto.write(ProcessesProto.DEVICE_IDLE_WHITELIST, v);
+ }
+
+ for (int v : mDeviceIdleTempWhitelist) {
+ proto.write(ProcessesProto.DEVICE_IDLE_TEMP_WHITELIST, v);
+ }
+
+ if (mPendingTempWhitelist.size() > 0) {
+ for (int i=0; i < mPendingTempWhitelist.size(); i++) {
+ mPendingTempWhitelist.valueAt(i).writeToProto(proto,
+ ProcessesProto.PENDING_TEMP_WHITELIST);
+ }
+ }
+
+ if (dumpPackage == null) {
+ final long sleepToken = proto.start(ProcessesProto.SLEEP_STATUS);
+ proto.write(ProcessesProto.SleepStatus.WAKEFULNESS,
+ PowerManagerInternal.wakefulnessToProtoEnum(mWakefulness));
+ for (SleepToken st : mStackSupervisor.mSleepTokens) {
+ proto.write(ProcessesProto.SleepStatus.SLEEP_TOKENS, st.toString());
+ }
+ proto.write(ProcessesProto.SleepStatus.SLEEPING, mSleeping);
+ proto.write(ProcessesProto.SleepStatus.SHUTTING_DOWN, mShuttingDown);
+ proto.write(ProcessesProto.SleepStatus.TEST_PSS_MODE, mTestPssMode);
+ proto.end(sleepToken);
+
+ if (mRunningVoice != null) {
+ final long vrToken = proto.start(ProcessesProto.RUNNING_VOICE);
+ proto.write(ProcessesProto.VoiceProto.SESSION, mRunningVoice.toString());
+ mVoiceWakeLock.writeToProto(proto, ProcessesProto.VoiceProto.WAKELOCK);
+ proto.end(vrToken);
+ }
+
+ mVrController.writeToProto(proto, ProcessesProto.VR_CONTROLLER);
+ }
+
+ if (mDebugApp != null || mOrigDebugApp != null || mDebugTransient
+ || mOrigWaitForDebugger) {
+ if (dumpPackage == null || dumpPackage.equals(mDebugApp)
+ || dumpPackage.equals(mOrigDebugApp)) {
+ final long debugAppToken = proto.start(ProcessesProto.DEBUG);
+ proto.write(ProcessesProto.DebugApp.DEBUG_APP, mDebugApp);
+ proto.write(ProcessesProto.DebugApp.ORIG_DEBUG_APP, mOrigDebugApp);
+ proto.write(ProcessesProto.DebugApp.DEBUG_TRANSIENT, mDebugTransient);
+ proto.write(ProcessesProto.DebugApp.ORIG_WAIT_FOR_DEBUGGER, mOrigWaitForDebugger);
+ proto.end(debugAppToken);
+ }
+ }
+
+ if (mCurAppTimeTracker != null) {
+ mCurAppTimeTracker.writeToProto(proto, ProcessesProto.CURRENT_TRACKER, true);
+ }
+
+ if (mMemWatchProcesses.getMap().size() > 0) {
+ final long token = proto.start(ProcessesProto.MEM_WATCH_PROCESSES);
+ ArrayMap<String, SparseArray<Pair<Long, String>>> procs = mMemWatchProcesses.getMap();
+ for (int i=0; i<procs.size(); i++) {
+ final String proc = procs.keyAt(i);
+ final SparseArray<Pair<Long, String>> uids = procs.valueAt(i);
+ final long ptoken = proto.start(ProcessesProto.MemWatchProcess.PROCS);
+ proto.write(ProcessesProto.MemWatchProcess.Process.NAME, proc);
+ for (int j=0; j<uids.size(); j++) {
+ final long utoken = proto.start(ProcessesProto.MemWatchProcess.Process.MEM_STATS);
+ Pair<Long, String> val = uids.valueAt(j);
+ proto.write(ProcessesProto.MemWatchProcess.Process.MemStats.UID, uids.keyAt(j));
+ proto.write(ProcessesProto.MemWatchProcess.Process.MemStats.SIZE,
+ DebugUtils.sizeValueToString(val.first, new StringBuilder()));
+ proto.write(ProcessesProto.MemWatchProcess.Process.MemStats.REPORT_TO, val.second);
+ proto.end(utoken);
+ }
+ proto.end(ptoken);
+ }
+
+ final long dtoken = proto.start(ProcessesProto.MemWatchProcess.DUMP);
+ proto.write(ProcessesProto.MemWatchProcess.Dump.PROC_NAME, mMemWatchDumpProcName);
+ proto.write(ProcessesProto.MemWatchProcess.Dump.FILE, mMemWatchDumpFile);
+ proto.write(ProcessesProto.MemWatchProcess.Dump.PID, mMemWatchDumpPid);
+ proto.write(ProcessesProto.MemWatchProcess.Dump.UID, mMemWatchDumpUid);
+ proto.end(dtoken);
+
+ proto.end(token);
+ }
+
+ if (mTrackAllocationApp != null) {
+ if (dumpPackage == null || dumpPackage.equals(mTrackAllocationApp)) {
+ proto.write(ProcessesProto.TRACK_ALLOCATION_APP, mTrackAllocationApp);
+ }
+ }
+
+ if (mProfileApp != null || mProfileProc != null || (mProfilerInfo != null &&
+ (mProfilerInfo.profileFile != null || mProfilerInfo.profileFd != null))) {
+ if (dumpPackage == null || dumpPackage.equals(mProfileApp)) {
+ final long token = proto.start(ProcessesProto.PROFILE);
+ proto.write(ProcessesProto.Profile.APP_NAME, mProfileApp);
+ mProfileProc.writeToProto(proto,ProcessesProto.Profile.PROC);
+ if (mProfilerInfo != null) {
+ mProfilerInfo.writeToProto(proto, ProcessesProto.Profile.INFO);
+ proto.write(ProcessesProto.Profile.TYPE, mProfileType);
+ }
+ proto.end(token);
+ }
+ }
+
+ if (dumpPackage == null || dumpPackage.equals(mNativeDebuggingApp)) {
+ proto.write(ProcessesProto.NATIVE_DEBUGGING_APP, mNativeDebuggingApp);
+ }
+
+ if (dumpPackage == null) {
+ proto.write(ProcessesProto.ALWAYS_FINISH_ACTIVITIES, mAlwaysFinishActivities);
+ if (mController != null) {
+ final long token = proto.start(ProcessesProto.CONTROLLER);
+ proto.write(ProcessesProto.Controller.CONTROLLER, mController.toString());
+ proto.write(ProcessesProto.Controller.IS_A_MONKEY, mControllerIsAMonkey);
+ proto.end(token);
+ }
+ proto.write(ProcessesProto.TOTAL_PERSISTENT_PROCS, numPers);
+ proto.write(ProcessesProto.PROCESSES_READY, mProcessesReady);
+ proto.write(ProcessesProto.SYSTEM_READY, mSystemReady);
+ proto.write(ProcessesProto.BOOTED, mBooted);
+ proto.write(ProcessesProto.FACTORY_TEST, mFactoryTest);
+ proto.write(ProcessesProto.BOOTING, mBooting);
+ proto.write(ProcessesProto.CALL_FINISH_BOOTING, mCallFinishBooting);
+ proto.write(ProcessesProto.BOOT_ANIMATION_COMPLETE, mBootAnimationComplete);
+ proto.write(ProcessesProto.LAST_POWER_CHECK_UPTIME_MS, mLastPowerCheckUptime);
+ mStackSupervisor.mGoingToSleep.writeToProto(proto, ProcessesProto.GOING_TO_SLEEP);
+ mStackSupervisor.mLaunchingActivity.writeToProto(proto, ProcessesProto.LAUNCHING_ACTIVITY);
+ proto.write(ProcessesProto.ADJ_SEQ, mAdjSeq);
+ proto.write(ProcessesProto.LRU_SEQ, mLruSeq);
+ proto.write(ProcessesProto.NUM_NON_CACHED_PROCS, mNumNonCachedProcs);
+ proto.write(ProcessesProto.NUM_SERVICE_PROCS, mNumServiceProcs);
+ proto.write(ProcessesProto.NEW_NUM_SERVICE_PROCS, mNewNumServiceProcs);
+ proto.write(ProcessesProto.ALLOW_LOWER_MEM_LEVEL, mAllowLowerMemLevel);
+ proto.write(ProcessesProto.LAST_MEMORY_LEVEL, mLastMemoryLevel);
+ proto.write(ProcessesProto.LAST_NUM_PROCESSES, mLastNumProcesses);
+ long now = SystemClock.uptimeMillis();
+ ProtoUtils.toDuration(proto, ProcessesProto.LAST_IDLE_TIME, mLastIdleTime, now);
+ proto.write(ProcessesProto.LOW_RAM_SINCE_LAST_IDLE_MS, getLowRamTimeSinceIdle(now));
+ }
+
+ }
+
+ void writeProcessesToGcToProto(ProtoOutputStream proto, long fieldId, String dumpPackage) {
+ if (mProcessesToGc.size() > 0) {
+ long now = SystemClock.uptimeMillis();
+ for (int i=0; i<mProcessesToGc.size(); i++) {
+ ProcessRecord r = mProcessesToGc.get(i);
+ if (dumpPackage != null && !dumpPackage.equals(r.info.packageName)) {
+ continue;
+ }
+ final long token = proto.start(fieldId);
+ r.writeToProto(proto, ProcessToGcProto.PROC);
+ proto.write(ProcessToGcProto.REPORT_LOW_MEMORY, r.reportLowMemory);
+ proto.write(ProcessToGcProto.NOW_UPTIME_MS, now);
+ proto.write(ProcessToGcProto.LAST_GCED_MS, r.lastRequestedGc);
+ proto.write(ProcessToGcProto.LAST_LOW_MEMORY_MS, r.lastLowMemory);
+ proto.end(token);
+ }
+ }
+ }
+
+ boolean dumpProcessesToGc(PrintWriter pw, boolean needSep, String dumpPackage) {
if (mProcessesToGc.size() > 0) {
boolean printed = false;
long now = SystemClock.uptimeMillis();
@@ -16382,7 +16969,7 @@ public class ActivityManagerService extends IActivityManager.Stub
needSep = true;
}
- dumpProcessesToGc(fd, pw, args, opti, needSep, dumpAll, null);
+ dumpProcessesToGc(pw, needSep, null);
pw.println();
pw.println(" mHomeProcess: " + mHomeProcess);
@@ -16933,11 +17520,8 @@ public class ActivityManagerService extends IActivityManager.Stub
return numPers;
}
- private static final boolean dumpProcessOomList(PrintWriter pw,
- ActivityManagerService service, List<ProcessRecord> origList,
- String prefix, String normalLabel, String persistentLabel,
- boolean inclDetails, String dumpPackage) {
-
+ private static final ArrayList<Pair<ProcessRecord, Integer>>
+ sortProcessOomList(List<ProcessRecord> origList, String dumpPackage) {
ArrayList<Pair<ProcessRecord, Integer>> list
= new ArrayList<Pair<ProcessRecord, Integer>>(origList.size());
for (int i=0; i<origList.size(); i++) {
@@ -16948,10 +17532,6 @@ public class ActivityManagerService extends IActivityManager.Stub
list.add(new Pair<ProcessRecord, Integer>(origList.get(i), i));
}
- if (list.size() <= 0) {
- return false;
- }
-
Comparator<Pair<ProcessRecord, Integer>> comparator
= new Comparator<Pair<ProcessRecord, Integer>>() {
@Override
@@ -16971,6 +17551,113 @@ public class ActivityManagerService extends IActivityManager.Stub
};
Collections.sort(list, comparator);
+ return list;
+ }
+
+ private static final boolean writeProcessOomListToProto(ProtoOutputStream proto, long fieldId,
+ ActivityManagerService service, List<ProcessRecord> origList,
+ boolean inclDetails, String dumpPackage) {
+ ArrayList<Pair<ProcessRecord, Integer>> list = sortProcessOomList(origList, dumpPackage);
+ if (list.isEmpty()) return false;
+
+ final long curUptime = SystemClock.uptimeMillis();
+
+ for (int i = list.size() - 1; i >= 0; i--) {
+ ProcessRecord r = list.get(i).first;
+ long token = proto.start(fieldId);
+ String oomAdj = ProcessList.makeOomAdjString(r.setAdj);
+ proto.write(ProcessOomProto.PERSISTENT, r.persistent);
+ proto.write(ProcessOomProto.NUM, (origList.size()-1)-list.get(i).second);
+ proto.write(ProcessOomProto.OOM_ADJ, oomAdj);
+ int schedGroup = ProcessOomProto.SCHED_GROUP_UNKNOWN;
+ switch (r.setSchedGroup) {
+ case ProcessList.SCHED_GROUP_BACKGROUND:
+ schedGroup = ProcessOomProto.SCHED_GROUP_BACKGROUND;
+ break;
+ case ProcessList.SCHED_GROUP_DEFAULT:
+ schedGroup = ProcessOomProto.SCHED_GROUP_DEFAULT;
+ break;
+ case ProcessList.SCHED_GROUP_TOP_APP:
+ schedGroup = ProcessOomProto.SCHED_GROUP_TOP_APP;
+ break;
+ case ProcessList.SCHED_GROUP_TOP_APP_BOUND:
+ schedGroup = ProcessOomProto.SCHED_GROUP_TOP_APP_BOUND;
+ break;
+ }
+ if (schedGroup != ProcessOomProto.SCHED_GROUP_UNKNOWN) {
+ proto.write(ProcessOomProto.SCHED_GROUP, schedGroup);
+ }
+ if (r.foregroundActivities) {
+ proto.write(ProcessOomProto.ACTIVITIES, true);
+ } else if (r.foregroundServices) {
+ proto.write(ProcessOomProto.SERVICES, true);
+ }
+ proto.write(ProcessOomProto.STATE, ProcessList.makeProcStateProtoEnum(r.curProcState));
+ proto.write(ProcessOomProto.TRIM_MEMORY_LEVEL, r.trimMemoryLevel);
+ r.writeToProto(proto, ProcessOomProto.PROC);
+ proto.write(ProcessOomProto.ADJ_TYPE, r.adjType);
+ if (r.adjSource != null || r.adjTarget != null) {
+ if (r.adjTarget instanceof ComponentName) {
+ ComponentName cn = (ComponentName) r.adjTarget;
+ cn.writeToProto(proto, ProcessOomProto.ADJ_TARGET_COMPONENT_NAME);
+ } else if (r.adjTarget != null) {
+ proto.write(ProcessOomProto.ADJ_TARGET_OBJECT, r.adjTarget.toString());
+ }
+ if (r.adjSource instanceof ProcessRecord) {
+ ProcessRecord p = (ProcessRecord) r.adjSource;
+ p.writeToProto(proto, ProcessOomProto.ADJ_SOURCE_PROC);
+ } else if (r.adjSource != null) {
+ proto.write(ProcessOomProto.ADJ_SOURCE_OBJECT, r.adjSource.toString());
+ }
+ }
+ if (inclDetails) {
+ long detailToken = proto.start(ProcessOomProto.DETAIL);
+ proto.write(ProcessOomProto.Detail.MAX_ADJ, r.maxAdj);
+ proto.write(ProcessOomProto.Detail.CUR_RAW_ADJ, r.curRawAdj);
+ proto.write(ProcessOomProto.Detail.SET_RAW_ADJ, r.setRawAdj);
+ proto.write(ProcessOomProto.Detail.CUR_ADJ, r.curAdj);
+ proto.write(ProcessOomProto.Detail.SET_ADJ, r.setAdj);
+ proto.write(ProcessOomProto.Detail.CURRENT_STATE,
+ ProcessList.makeProcStateProtoEnum(r.curProcState));
+ proto.write(ProcessOomProto.Detail.SET_STATE,
+ ProcessList.makeProcStateProtoEnum(r.setProcState));
+ proto.write(ProcessOomProto.Detail.LAST_PSS, DebugUtils.sizeValueToString(
+ r.lastPss*1024, new StringBuilder()));
+ proto.write(ProcessOomProto.Detail.LAST_SWAP_PSS, DebugUtils.sizeValueToString(
+ r.lastSwapPss*1024, new StringBuilder()));
+ proto.write(ProcessOomProto.Detail.LAST_CACHED_PSS, DebugUtils.sizeValueToString(
+ r.lastCachedPss*1024, new StringBuilder()));
+ proto.write(ProcessOomProto.Detail.CACHED, r.cached);
+ proto.write(ProcessOomProto.Detail.EMPTY, r.empty);
+ proto.write(ProcessOomProto.Detail.HAS_ABOVE_CLIENT, r.hasAboveClient);
+
+ if (r.setProcState >= ActivityManager.PROCESS_STATE_SERVICE) {
+ if (r.lastCpuTime != 0) {
+ long uptimeSince = curUptime - service.mLastPowerCheckUptime;
+ long timeUsed = r.curCpuTime - r.lastCpuTime;
+ long cpuTimeToken = proto.start(ProcessOomProto.Detail.SERVICE_RUN_TIME);
+ proto.write(ProcessOomProto.Detail.CpuRunTime.OVER_MS, uptimeSince);
+ proto.write(ProcessOomProto.Detail.CpuRunTime.USED_MS, timeUsed);
+ proto.write(ProcessOomProto.Detail.CpuRunTime.ULTILIZATION,
+ (100.0*timeUsed)/uptimeSince);
+ proto.end(cpuTimeToken);
+ }
+ }
+ proto.end(detailToken);
+ }
+ proto.end(token);
+ }
+
+ return true;
+ }
+
+ private static final boolean dumpProcessOomList(PrintWriter pw,
+ ActivityManagerService service, List<ProcessRecord> origList,
+ String prefix, String normalLabel, String persistentLabel,
+ boolean inclDetails, String dumpPackage) {
+
+ ArrayList<Pair<ProcessRecord, Integer>> list = sortProcessOomList(origList, dumpPackage);
+ if (list.isEmpty()) return false;
final long curUptime = SystemClock.uptimeMillis();
final long uptimeSince = curUptime - service.mLastPowerCheckUptime;
@@ -17627,11 +18314,20 @@ public class ActivityManagerService extends IActivityManager.Stub
if (mi == null) {
mi = new Debug.MemoryInfo();
}
+ final int reportType;
+ final long startTime;
+ final long endTime;
if (opts.dumpDetails || (!brief && !opts.oomOnly)) {
+ reportType = ProcessStats.ADD_PSS_EXTERNAL_SLOW;
+ startTime = SystemClock.currentThreadTimeMillis();
Debug.getMemoryInfo(pid, mi);
+ endTime = SystemClock.currentThreadTimeMillis();
hasSwapPss = mi.hasSwappedOutPss;
} else {
+ reportType = ProcessStats.ADD_PSS_EXTERNAL;
+ startTime = SystemClock.currentThreadTimeMillis();
mi.dalvikPss = (int)Debug.getPss(pid, tmpLong, null);
+ endTime = SystemClock.currentThreadTimeMillis();
mi.dalvikPrivateDirty = (int)tmpLong[0];
}
if (opts.dumpDetails) {
@@ -17674,7 +18370,8 @@ public class ActivityManagerService extends IActivityManager.Stub
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);
+ r.baseProcessTracker.addPss(myTotalPss, myTotalUss, true,
+ reportType, endTime-startTime, r.pkgList);
}
}
@@ -18117,11 +18814,20 @@ public class ActivityManagerService extends IActivityManager.Stub
if (mi == null) {
mi = new Debug.MemoryInfo();
}
+ final int reportType;
+ final long startTime;
+ final long endTime;
if (opts.dumpDetails || (!brief && !opts.oomOnly)) {
+ reportType = ProcessStats.ADD_PSS_EXTERNAL_SLOW;
+ startTime = SystemClock.currentThreadTimeMillis();
Debug.getMemoryInfo(pid, mi);
+ endTime = SystemClock.currentThreadTimeMillis();
hasSwapPss = mi.hasSwappedOutPss;
} else {
+ reportType = ProcessStats.ADD_PSS_EXTERNAL;
+ startTime = SystemClock.currentThreadTimeMillis();
mi.dalvikPss = (int) Debug.getPss(pid, tmpLong, null);
+ endTime = SystemClock.currentThreadTimeMillis();
mi.dalvikPrivateDirty = (int) tmpLong[0];
}
if (opts.dumpDetails) {
@@ -18160,7 +18866,8 @@ public class ActivityManagerService extends IActivityManager.Stub
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);
+ r.baseProcessTracker.addPss(myTotalPss, myTotalUss, true,
+ reportType, endTime-startTime, r.pkgList);
}
}
@@ -21350,7 +22057,7 @@ public class ActivityManagerService extends IActivityManager.Stub
mAppWarnings.onDensityChanged();
killAllBackgroundProcessesExcept(N,
- ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE);
}
}
@@ -21401,6 +22108,17 @@ public class ActivityManagerService extends IActivityManager.Stub
private void resizeStackWithBoundsFromWindowManager(int stackId, boolean deferResume) {
final Rect newStackBounds = new Rect();
final ActivityStack stack = mStackSupervisor.getStack(stackId);
+
+ // TODO(b/71548119): Revert CL introducing below once cause of mismatch is found.
+ if (stack == null) {
+ final StringWriter writer = new StringWriter();
+ final PrintWriter printWriter = new PrintWriter(writer);
+ mStackSupervisor.dumpDisplays(printWriter);
+ printWriter.flush();
+
+ Log.wtf(TAG, "stack not found:" + stackId + " displays:" + writer);
+ }
+
stack.getBoundsForNewConfiguration(newStackBounds);
mStackSupervisor.resizeStackLocked(
stack, !newStackBounds.isEmpty() ? newStackBounds : null /* bounds */,
@@ -22324,6 +23042,7 @@ public class ActivityManagerService extends IActivityManager.Stub
// to the top state.
switch (procState) {
case ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE:
+ case ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE:
// Something else is keeping it at this level, just leave it.
break;
case ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND:
@@ -22422,11 +23141,12 @@ public class ActivityManagerService extends IActivityManager.Stub
* Record new PSS sample for a process.
*/
void recordPssSampleLocked(ProcessRecord proc, int procState, long pss, long uss, long swapPss,
- long now) {
+ long pssDuration, long now) {
EventLogTags.writeAmPss(proc.pid, proc.uid, proc.processName, pss * 1024, uss * 1024,
swapPss * 1024);
proc.lastPssTime = now;
- proc.baseProcessTracker.addPss(pss, uss, true, proc.pkgList);
+ proc.baseProcessTracker.addPss(pss, uss, true, ProcessStats.ADD_PSS_INTERNAL,
+ pssDuration, proc.pkgList);
if (DEBUG_PSS) Slog.d(TAG_PSS,
"PSS of " + proc.toShortString() + ": " + pss + " lastPss=" + proc.lastPss
+ " state=" + ProcessList.makeProcStateString(procState));
@@ -22921,8 +23641,11 @@ public class ActivityManagerService extends IActivityManager.Stub
// the data right when a process is transitioning between process
// states, which well tend to give noisy data.
long start = SystemClock.uptimeMillis();
+ long startTime = SystemClock.currentThreadTimeMillis();
long pss = Debug.getPss(app.pid, mTmpLong, null);
- recordPssSampleLocked(app, app.curProcState, pss, mTmpLong[0], mTmpLong[1], now);
+ long endTime = SystemClock.currentThreadTimeMillis();
+ recordPssSampleLocked(app, app.curProcState, pss, endTime-startTime,
+ mTmpLong[0], mTmpLong[1], now);
mPendingPssProcesses.remove(app);
Slog.i(TAG, "Recorded pss for " + app + " state " + app.setProcState
+ " to " + app.curProcState + ": "
@@ -23157,7 +23880,7 @@ public class ActivityManagerService extends IActivityManager.Stub
// To avoid some abuse patterns, we are going to be careful about what we consider
// to be an app interaction. Being the top activity doesn't count while the display
// is sleeping, nor do short foreground services.
- if (app.curProcState <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
+ if (app.curProcState <= ActivityManager.PROCESS_STATE_TOP) {
isInteraction = true;
app.fgInteractionTime = 0;
} else if (app.curProcState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
@@ -24002,7 +24725,7 @@ public class ActivityManagerService extends IActivityManager.Stub
final int size = mActiveUids.size();
for (int i = 0; i < size; i++) {
final int uid = mActiveUids.keyAt(i);
- if (!UserHandle.isApp(uid)) {
+ if (UserHandle.isCore(uid)) {
continue;
}
final UidRecord uidRec = mActiveUids.valueAt(i);
@@ -24674,6 +25397,7 @@ public class ActivityManagerService extends IActivityManager.Stub
ActivityManagerService.this.onUserStoppedLocked(userId);
}
mBatteryStatsService.onUserRemoved(userId);
+ mUserController.onUserRemoved(userId);
}
@Override
@@ -24802,16 +25526,20 @@ public class ActivityManagerService extends IActivityManager.Stub
// "= 0" is needed because otherwise catch(RemoteException) would make it look like
// packageUid may not be initialized.
int packageUid = 0;
+ final long ident = Binder.clearCallingIdentity();
try {
packageUid = AppGlobals.getPackageManager().getPackageUid(
packageName, PackageManager.MATCH_DEBUG_TRIAGED_MISSING, userId);
} catch (RemoteException e) {
// Shouldn't happen.
+ } finally {
+ Binder.restoreCallingIdentity(ident);
}
synchronized (ActivityManagerService.this) {
return mActivityStartController.startActivitiesInPackage(packageUid, packageName,
- intents, resolvedTypes, /*resultTo*/ null, bOptions, userId);
+ intents, resolvedTypes, null /* resultTo */,
+ SafeActivityOptions.fromBundle(bOptions), userId);
}
}
@@ -25054,6 +25782,30 @@ public class ActivityManagerService extends IActivityManager.Stub
public void registerScreenObserver(ScreenObserver observer) {
mScreenObservers.add(observer);
}
+
+ @Override
+ public boolean canStartMoreUsers() {
+ return mUserController.canStartMoreUsers();
+ }
+
+ @Override
+ public void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage) {
+ mUserController.setSwitchingFromSystemUserMessage(switchingFromSystemUserMessage);
+ }
+
+ @Override
+ public void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage) {
+ mUserController.setSwitchingToSystemUserMessage(switchingToSystemUserMessage);
+ }
+
+ @Override
+ public int getMaxRunningUsers() {
+ return mUserController.mMaxRunningUsers;
+ }
+
+ public boolean isCallerRecents(int callingUid) {
+ return getRecentTasks().isCallerRecents(callingUid);
+ }
}
/**
@@ -25201,11 +25953,15 @@ public class ActivityManagerService extends IActivityManager.Stub
}
@Override
- public void dismissKeyguard(IBinder token, IKeyguardDismissCallback callback)
- throws RemoteException {
+ public void dismissKeyguard(IBinder token, IKeyguardDismissCallback callback,
+ CharSequence message) throws RemoteException {
+ if (message != null) {
+ enforceCallingPermission(permission.SHOW_KEYGUARD_MESSAGE,
+ "dismissKeyguard()");
+ }
final long callingId = Binder.clearCallingIdentity();
try {
- mKeyguardController.dismissKeyguard(token, callback);
+ mKeyguardController.dismissKeyguard(token, callback, message);
} finally {
Binder.restoreCallingIdentity(callingId);
}
@@ -25260,6 +26016,19 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
}
+ if (updateFrameworkRes) {
+ // Update system server components that need to know about changed overlays. Because the
+ // overlay is applied in ActivityThread, we need to serialize through its thread too.
+ final Executor executor = ActivityThread.currentActivityThread().getExecutor();
+ final DisplayManagerInternal display =
+ LocalServices.getService(DisplayManagerInternal.class);
+ if (display != null) {
+ executor.execute(display::onOverlayChanged);
+ }
+ if (mWindowManager != null) {
+ executor.execute(mWindowManager::onOverlayChanged);
+ }
+ }
}
/**
@@ -25350,4 +26119,23 @@ public class ActivityManagerService extends IActivityManager.Stub
}
}
}
+
+ @Override
+ public void registerRemoteAnimations(IBinder token, RemoteAnimationDefinition definition)
+ throws RemoteException {
+ enforceCallingPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS,
+ "registerRemoteAnimations");
+ synchronized (this) {
+ final ActivityRecord r = ActivityRecord.isInStackLocked(token);
+ if (r == null) {
+ return;
+ }
+ final long origId = Binder.clearCallingIdentity();
+ try {
+ r.registerRemoteAnimations(definition);
+ } finally {
+ Binder.restoreCallingIdentity(origId);
+ }
+ }
+ }
}
diff --git a/com/android/server/am/ActivityManagerShellCommand.java b/com/android/server/am/ActivityManagerShellCommand.java
index 4f60e173..1240f5e6 100644
--- a/com/android/server/am/ActivityManagerShellCommand.java
+++ b/com/android/server/am/ActivityManagerShellCommand.java
@@ -109,6 +109,7 @@ final class ActivityManagerShellCommand extends ShellCommand {
private boolean mAutoStop;
private boolean mStreaming; // Streaming the profiling output to a file.
private String mAgent; // Agent to attach on startup.
+ private boolean mAttachAgentDuringBind; // Whether agent should be attached late.
private int mDisplayId;
private int mWindowingMode;
private int mActivityType;
@@ -296,7 +297,21 @@ final class ActivityManagerShellCommand extends ShellCommand {
} else if (opt.equals("--streaming")) {
mStreaming = true;
} else if (opt.equals("--attach-agent")) {
+ if (mAgent != null) {
+ cmd.getErrPrintWriter().println(
+ "Multiple --attach-agent(-bind) not supported");
+ return false;
+ }
+ mAgent = getNextArgRequired();
+ mAttachAgentDuringBind = false;
+ } else if (opt.equals("--attach-agent-bind")) {
+ if (mAgent != null) {
+ cmd.getErrPrintWriter().println(
+ "Multiple --attach-agent(-bind) not supported");
+ return false;
+ }
mAgent = getNextArgRequired();
+ mAttachAgentDuringBind = true;
} else if (opt.equals("-R")) {
mRepeat = Integer.parseInt(getNextArgRequired());
} else if (opt.equals("-S")) {
@@ -384,7 +399,7 @@ final class ActivityManagerShellCommand extends ShellCommand {
}
}
profilerInfo = new ProfilerInfo(mProfileFile, fd, mSamplingInterval, mAutoStop,
- mStreaming, mAgent);
+ mStreaming, mAgent, mAttachAgentDuringBind);
}
pw.println("Starting: " + intent);
@@ -766,7 +781,7 @@ final class ActivityManagerShellCommand extends ShellCommand {
return -1;
}
profilerInfo = new ProfilerInfo(profileFile, fd, mSamplingInterval, false, mStreaming,
- null);
+ null, false);
}
try {
@@ -2544,6 +2559,7 @@ final class ActivityManagerShellCommand extends ShellCommand {
pw.println(" (use with --start-profiler)");
pw.println(" -P <FILE>: like above, but profiling stops when app goes idle");
pw.println(" --attach-agent <agent>: attach the given agent before binding");
+ pw.println(" --attach-agent-bind <agent>: attach the given agent during binding");
pw.println(" -R: repeat the activity launch <COUNT> times. Prior to each repeat,");
pw.println(" the top activity will be finished.");
pw.println(" -S: force stop the target app before starting the activity");
diff --git a/com/android/server/am/ActivityMetricsLogger.java b/com/android/server/am/ActivityMetricsLogger.java
index eb022b78..66f0592e 100644
--- a/com/android/server/am/ActivityMetricsLogger.java
+++ b/com/android/server/am/ActivityMetricsLogger.java
@@ -42,6 +42,7 @@ import android.os.SystemClock;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
+import android.util.StatsLog;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.os.SomeArgs;
@@ -431,6 +432,12 @@ class ActivityMetricsLogger {
builder.setType(type);
builder.addTaggedData(FIELD_CLASS_NAME, info.launchedActivity.info.name);
mMetricsLogger.write(builder);
+ StatsLog.write(
+ StatsLog.APP_START_CANCEL_CHANGED,
+ info.launchedActivity.appInfo.uid,
+ info.launchedActivity.packageName,
+ convertAppStartTransitionType(type),
+ info.launchedActivity.info.name);
}
private void logAppTransitionMultiEvents() {
@@ -450,9 +457,9 @@ class ActivityMetricsLogger {
builder.addTaggedData(APP_TRANSITION_CALLING_PACKAGE_NAME,
info.launchedActivity.launchedFromPackage);
}
- if (info.launchedActivity.info.launchToken != null) {
- builder.addTaggedData(FIELD_INSTANT_APP_LAUNCH_TOKEN,
- info.launchedActivity.info.launchToken);
+ String launchToken = info.launchedActivity.info.launchToken;
+ if (launchToken != null) {
+ builder.addTaggedData(FIELD_INSTANT_APP_LAUNCH_TOKEN, launchToken);
info.launchedActivity.info.launchToken = null;
}
builder.addTaggedData(APP_TRANSITION_IS_EPHEMERAL, isInstantApp ? 1 : 0);
@@ -470,9 +477,37 @@ class ActivityMetricsLogger {
}
builder.addTaggedData(APP_TRANSITION_WINDOWS_DRAWN_DELAY_MS, info.windowsDrawnDelayMs);
mMetricsLogger.write(builder);
+ StatsLog.write(
+ StatsLog.APP_START_CHANGED,
+ info.launchedActivity.appInfo.uid,
+ info.launchedActivity.packageName,
+ convertAppStartTransitionType(type),
+ info.launchedActivity.info.name,
+ info.launchedActivity.launchedFromPackage,
+ isInstantApp,
+ mCurrentTransitionDeviceUptime * 1000,
+ info.reason,
+ mCurrentTransitionDelayMs,
+ info.startingWindowDelayMs,
+ info.bindApplicationDelayMs,
+ info.windowsDrawnDelayMs,
+ launchToken);
}
}
+ private int convertAppStartTransitionType(int tronType) {
+ if (tronType == TYPE_TRANSITION_COLD_LAUNCH) {
+ return StatsLog.APP_START_CHANGED__TYPE__COLD;
+ }
+ if (tronType == TYPE_TRANSITION_WARM_LAUNCH) {
+ return StatsLog.APP_START_CHANGED__TYPE__WARM;
+ }
+ if (tronType == TYPE_TRANSITION_HOT_LAUNCH) {
+ return StatsLog.APP_START_CHANGED__TYPE__HOT;
+ }
+ return StatsLog.APP_START_CHANGED__TYPE__APP_START_TRANSITION_TYPE_UNKNOWN;
+ }
+
void logAppTransitionReportedDrawn(ActivityRecord r, boolean restoredFromBundle) {
final StackTransitionInfo info = mLastStackTransitionInfo.get(r.getStackId());
if (info == null) {
@@ -481,14 +516,24 @@ class ActivityMetricsLogger {
final LogMaker builder = new LogMaker(APP_TRANSITION_REPORTED_DRAWN);
builder.setPackageName(r.packageName);
builder.addTaggedData(FIELD_CLASS_NAME, r.info.name);
- builder.addTaggedData(APP_TRANSITION_REPORTED_DRAWN_MS,
- SystemClock.uptimeMillis() - mLastTransitionStartTime);
+ long startupTimeMs = SystemClock.uptimeMillis() - mLastTransitionStartTime;
+ builder.addTaggedData(APP_TRANSITION_REPORTED_DRAWN_MS, startupTimeMs);
builder.setType(restoredFromBundle
? TYPE_TRANSITION_REPORTED_DRAWN_WITH_BUNDLE
: TYPE_TRANSITION_REPORTED_DRAWN_NO_BUNDLE);
builder.addTaggedData(APP_TRANSITION_PROCESS_RUNNING,
info.currentTransitionProcessRunning ? 1 : 0);
mMetricsLogger.write(builder);
+ StatsLog.write(
+ StatsLog.APP_START_FULLY_DRAWN_CHANGED,
+ info.launchedActivity.appInfo.uid,
+ info.launchedActivity.packageName,
+ restoredFromBundle
+ ? StatsLog.APP_START_FULLY_DRAWN_CHANGED__TYPE__WITH_BUNDLE
+ : StatsLog.APP_START_FULLY_DRAWN_CHANGED__TYPE__WITHOUT_BUNDLE,
+ info.launchedActivity.info.name,
+ info.currentTransitionProcessRunning,
+ startupTimeMs);
}
private int getTransitionType(StackTransitionInfo info) {
diff --git a/com/android/server/am/ActivityRecord.java b/com/android/server/am/ActivityRecord.java
index 8eb51979..3bef8779 100644
--- a/com/android/server/am/ActivityRecord.java
+++ b/com/android/server/am/ActivityRecord.java
@@ -21,6 +21,7 @@ import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
import static android.app.ActivityManager.TaskDescription.ATTR_TASKDESCRIPTION_PREFIX;
import static android.app.ActivityOptions.ANIM_CLIP_REVEAL;
import static android.app.ActivityOptions.ANIM_CUSTOM;
+import static android.app.ActivityOptions.ANIM_REMOTE_ANIMATION;
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;
@@ -135,6 +136,7 @@ import android.app.ResultInfo;
import android.app.servertransaction.MoveToDisplayItem;
import android.app.servertransaction.MultiWindowModeChangeItem;
import android.app.servertransaction.NewIntentItem;
+import android.app.servertransaction.PauseActivityItem;
import android.app.servertransaction.PipModeChangeItem;
import android.app.servertransaction.WindowVisibilityItem;
import android.app.servertransaction.ActivityConfigurationChangeItem;
@@ -169,6 +171,7 @@ import android.util.proto.ProtoOutputStream;
import android.view.AppTransitionAnimationSpec;
import android.view.IAppTransitionAnimationSpecsFuture;
import android.view.IApplicationToken;
+import android.view.RemoteAnimationDefinition;
import android.view.WindowManager.LayoutParams;
import com.android.internal.R;
@@ -371,6 +374,10 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
}
}
+ String getLifecycleDescription(String reason) {
+ return "packageName=" + packageName + ", state=" + state + ", reason=" + reason;
+ }
+
void dump(PrintWriter pw, String prefix) {
final long now = SystemClock.uptimeMillis();
pw.print(prefix); pw.print("packageName="); pw.print(packageName);
@@ -1480,6 +1487,10 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
case ANIM_OPEN_CROSS_PROFILE_APPS:
service.mWindowManager.overridePendingAppTransitionStartCrossProfileApps();
break;
+ case ANIM_REMOTE_ANIMATION:
+ service.mWindowManager.overridePendingAppTransitionRemote(
+ pendingOptions.getRemoteAnimationAdapter());
+ break;
default:
Slog.e(TAG, "applyOptionsLocked: Unknown animationType=" + animationType);
break;
@@ -1607,6 +1618,20 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
// The activity may be waiting for stop, but that is no longer appropriate for it.
mStackSupervisor.mStoppingActivities.remove(this);
mStackSupervisor.mGoingToSleepActivities.remove(this);
+
+ // If the activity is stopped or stopping, cycle to the paused state.
+ if (state == STOPPED || state == STOPPING) {
+ // Capture reason before state change
+ final String reason = getLifecycleDescription("makeVisibleIfNeeded");
+
+ // An activity must be in the {@link PAUSING} state for the system to validate
+ // the move to {@link PAUSED}.
+ state = PAUSING;
+ service.mLifecycleManager.scheduleTransaction(app.thread, appToken,
+ PauseActivityItem.obtain(finishing, false /* userLeaving */,
+ configChangeFlags, false /* dontReport */)
+ .setDescription(reason));
+ }
} catch (Exception e) {
// Just skip on any failure; we'll make it visible when it next restarts.
Slog.w(TAG, "Exception thrown making visibile: " + intent.getComponent(), e);
@@ -2730,12 +2755,14 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
}
/**
- * @return true if the activity contains windows that have
- * {@link LayoutParams#FLAG_SHOW_WHEN_LOCKED} set or if the activity has set
- * {@link #mShowWhenLocked}.
+ * @return true if the activity windowing mode is not
+ * {@link android.app.WindowConfiguration#WINDOWING_MODE_PINNED} and activity contains
+ * windows that have {@link LayoutParams#FLAG_SHOW_WHEN_LOCKED} set or if the activity
+ * has set {@link #mShowWhenLocked}.
+ * Multi-windowing mode will be exited if true is returned.
*/
boolean canShowWhenLocked() {
- return !inMultiWindowMode() && (mShowWhenLocked
+ return !inPinnedWindowingMode() && (mShowWhenLocked
|| service.mWindowManager.containsShowWhenLockedWindow(appToken));
}
@@ -2764,6 +2791,10 @@ final class ActivityRecord extends ConfigurationContainer implements AppWindowCo
return mStackSupervisor.topRunningActivityLocked() == this;
}
+ void registerRemoteAnimations(RemoteAnimationDefinition definition) {
+ mWindowContainerController.registerRemoteAnimations(definition);
+ }
+
@Override
public String toString() {
if (stringName != null) {
diff --git a/com/android/server/am/ActivityStack.java b/com/android/server/am/ActivityStack.java
index 10c801da..172228b8 100644
--- a/com/android/server/am/ActivityStack.java
+++ b/com/android/server/am/ActivityStack.java
@@ -16,7 +16,6 @@
package com.android.server.am;
-import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SECONDARY_DISPLAY;
import static android.app.ITaskStackListener.FORCED_RESIZEABLE_REASON_SPLIT_SCREEN;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
@@ -85,14 +84,14 @@ import static com.android.server.am.proto.ActivityStackProto.FULLSCREEN;
import static com.android.server.am.proto.ActivityStackProto.ID;
import static com.android.server.am.proto.ActivityStackProto.RESUMED_ACTIVITY;
import static com.android.server.am.proto.ActivityStackProto.TASKS;
-import static com.android.server.wm.AppTransition.TRANSIT_ACTIVITY_CLOSE;
-import static com.android.server.wm.AppTransition.TRANSIT_ACTIVITY_OPEN;
-import static com.android.server.wm.AppTransition.TRANSIT_NONE;
-import static com.android.server.wm.AppTransition.TRANSIT_TASK_CLOSE;
-import static com.android.server.wm.AppTransition.TRANSIT_TASK_OPEN;
-import static com.android.server.wm.AppTransition.TRANSIT_TASK_OPEN_BEHIND;
-import static com.android.server.wm.AppTransition.TRANSIT_TASK_TO_BACK;
-import static com.android.server.wm.AppTransition.TRANSIT_TASK_TO_FRONT;
+import static android.view.WindowManager.TRANSIT_ACTIVITY_CLOSE;
+import static android.view.WindowManager.TRANSIT_ACTIVITY_OPEN;
+import static android.view.WindowManager.TRANSIT_NONE;
+import static android.view.WindowManager.TRANSIT_TASK_CLOSE;
+import static android.view.WindowManager.TRANSIT_TASK_OPEN;
+import static android.view.WindowManager.TRANSIT_TASK_OPEN_BEHIND;
+import static android.view.WindowManager.TRANSIT_TASK_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TASK_TO_FRONT;
import static java.lang.Integer.MAX_VALUE;
@@ -144,7 +143,6 @@ 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;
@@ -608,7 +606,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
true /* onTop */);
recentStack.moveToFront("setWindowingMode");
// If task moved to docked stack - show recents if needed.
- mService.mWindowManager.showRecentApps(false /* fromHome */);
+ mService.mWindowManager.showRecentApps();
}
wm.continueSurfaceLayout();
}
@@ -996,12 +994,6 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
insertTaskAtTop(task, null);
return;
}
-
- task = topTask();
- if (task != null) {
- mWindowContainerController.positionChildAtTop(task.getWindowContainerController(),
- true /* includingParents */);
- }
}
/**
@@ -1013,17 +1005,19 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
return;
}
+ /**
+ * The intent behind moving a primary split screen stack to the back is usually to hide
+ * behind the home stack. Exit split screen in this case.
+ */
+ if (getWindowingMode() == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
+ setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ }
+
getDisplay().positionChildAtBottom(this);
mStackSupervisor.setFocusStackUnchecked(reason, getDisplay().getTopStack());
if (task != null) {
insertTaskAtBottom(task);
return;
- } else {
- task = bottomTask();
- if (task != null) {
- mWindowContainerController.positionChildAtBottom(
- task.getWindowContainerController(), true /* includingParents */);
- }
}
}
@@ -1811,7 +1805,8 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
boolean behindFullscreenActivity = !stackShouldBeVisible;
boolean resumeNextActivity = mStackSupervisor.isFocusedStack(this)
&& (isInStackLocked(starting) == null);
- final boolean isTopFullscreenStack = getDisplay().isTopFullscreenStack(this);
+ final boolean isTopNotPinnedStack =
+ isAttached() && getDisplay().isTopNotPinnedStack(this);
for (int taskNdx = mTaskHistory.size() - 1; taskNdx >= 0; --taskNdx) {
final TaskRecord task = mTaskHistory.get(taskNdx);
final ArrayList<ActivityRecord> activities = task.mActivities;
@@ -1833,7 +1828,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 && isTopFullscreenStack);
+ visibleIgnoringKeyguard, isTop && isTopNotPinnedStack);
if (visibleIgnoringKeyguard) {
behindFullscreenActivity = updateBehindFullscreen(!stackShouldBeVisible,
behindFullscreenActivity, r);
@@ -2633,7 +2628,9 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
next.clearOptionsLocked();
mService.mLifecycleManager.scheduleTransaction(next.app.thread, next.appToken,
ResumeActivityItem.obtain(next.app.repProcState,
- mService.isNextTransitionForward()));
+ mService.isNextTransitionForward())
+ .setDescription(next.getLifecycleDescription(
+ "resumeTopActivityInnerLocked")));
if (DEBUG_STATES) Slog.d(TAG_STATES, "resumeTopActivityLocked: Resumed "
+ next);
@@ -3413,7 +3410,8 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
EventLogTags.writeAmStopActivity(
r.userId, System.identityHashCode(r), r.shortComponentName);
mService.mLifecycleManager.scheduleTransaction(r.app.thread, r.appToken,
- StopActivityItem.obtain(r.visible, r.configChangeFlags));
+ StopActivityItem.obtain(r.visible, r.configChangeFlags)
+ .setDescription(r.getLifecycleDescription("stopActivityLocked")));
if (shouldSleepOrShutDownActivities()) {
r.setSleeping(true);
}
@@ -4219,7 +4217,8 @@ 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,
- DestroyActivityItem.obtain(r.finishing, r.configChangeFlags));
+ DestroyActivityItem.obtain(r.finishing, r.configChangeFlags)
+ .setDescription(r.getLifecycleDescription("destroyActivityLocked")));
} catch (Exception e) {
// We can just ignore exceptions here... if the process
// has crashed, our death notification will clean things
@@ -5046,7 +5045,7 @@ class ActivityStack<T extends StackWindowController> extends ConfigurationContai
addTask(task, toTop, "createTaskRecord");
final boolean isLockscreenShown = mService.mStackSupervisor.getKeyguardController()
.isKeyguardShowing(mDisplayId != INVALID_DISPLAY ? mDisplayId : DEFAULT_DISPLAY);
- if (!mStackSupervisor.getLaunchingBoundsController()
+ if (!mStackSupervisor.getLaunchParamsController()
.layoutTask(task, info.windowLayout, activity, source, options)
&& !matchParentBounds() && task.isResizeable() && !isLockscreenShown) {
task.updateOverrideConfiguration(getOverrideBounds());
diff --git a/com/android/server/am/ActivityStackSupervisor.java b/com/android/server/am/ActivityStackSupervisor.java
index 0a42aa9c..510a3fa4 100644
--- a/com/android/server/am/ActivityStackSupervisor.java
+++ b/com/android/server/am/ActivityStackSupervisor.java
@@ -17,6 +17,7 @@
package com.android.server.am;
import static android.Manifest.permission.ACTIVITY_EMBEDDING;
+import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW;
import static android.Manifest.permission.START_ANY_ACTIVITY;
import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
@@ -93,7 +94,7 @@ import static com.android.server.am.proto.ActivityStackSupervisorProto.DISPLAYS;
import static com.android.server.am.proto.ActivityStackSupervisorProto.FOCUSED_STACK_ID;
import static com.android.server.am.proto.ActivityStackSupervisorProto.KEYGUARD_CONTROLLER;
import static com.android.server.am.proto.ActivityStackSupervisorProto.RESUMED_ACTIVITY;
-import static com.android.server.wm.AppTransition.TRANSIT_DOCK_TASK_FROM_RECENTS;
+import static android.view.WindowManager.TRANSIT_DOCK_TASK_FROM_RECENTS;
import static java.lang.Integer.MAX_VALUE;
@@ -162,10 +163,11 @@ import android.util.SparseIntArray;
import android.util.TimeUtils;
import android.util.proto.ProtoOutputStream;
import android.view.Display;
+import android.view.RemoteAnimationAdapter;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.ReferrerIntent;
-import com.android.internal.logging.MetricsLogger;
+import com.android.internal.os.logging.MetricsLoggerWrapper;
import com.android.internal.os.TransferPipe;
import com.android.internal.util.ArrayUtils;
import com.android.server.LocalServices;
@@ -295,12 +297,13 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
private RunningTasks mRunningTasks;
final ActivityStackSupervisorHandler mHandler;
+ final Looper mLooper;
/** Short cut */
WindowManagerService mWindowManager;
DisplayManager mDisplayManager;
- private LaunchingBoundsController mLaunchingBoundsController;
+ private LaunchParamsController mLaunchParamsController;
/**
* Maps the task identifier that activities are currently being started in to the userId of the
@@ -579,6 +582,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
public ActivityStackSupervisor(ActivityManagerService service, Looper looper) {
mService = service;
+ mLooper = looper;
mHandler = new ActivityStackSupervisorHandler(looper);
}
@@ -593,8 +597,8 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
mHandler.getLooper());
mKeyguardController = new KeyguardController(mService, this);
- mLaunchingBoundsController = new LaunchingBoundsController();
- mLaunchingBoundsController.registerDefaultPositioners(this);
+ mLaunchParamsController = new LaunchParamsController(mService);
+ mLaunchParamsController.registerDefaultModifiers(this);
}
@@ -1422,7 +1426,8 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
// Set desired final state.
final ActivityLifecycleItem lifecycleItem;
if (andResume) {
- lifecycleItem = ResumeActivityItem.obtain(mService.isNextTransitionForward());
+ lifecycleItem = ResumeActivityItem.obtain(mService.isNextTransitionForward())
+ .setDescription(r.getLifecycleDescription("realStartActivityLocked"));
} else {
lifecycleItem = PauseActivityItem.obtain();
}
@@ -1588,7 +1593,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
boolean checkStartAnyActivityPermission(Intent intent, ActivityInfo aInfo,
String resultWho, int requestCode, int callingPid, int callingUid,
String callingPackage, boolean ignoreTargetSecurity, ProcessRecord callerApp,
- ActivityRecord resultRecord, ActivityStack resultStack, ActivityOptions options) {
+ ActivityRecord resultRecord, ActivityStack resultStack) {
final int startAnyPerm = mService.checkPermission(START_ANY_ACTIVITY, callingPid,
callingUid);
if (startAnyPerm == PERMISSION_GRANTED) {
@@ -1642,45 +1647,6 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
Slog.w(TAG, message);
return false;
}
- if (options != null) {
- // If a launch task id is specified, then ensure that the caller is the recents
- // component or has the START_TASKS_FROM_RECENTS permission
- if (options.getLaunchTaskId() != INVALID_TASK_ID
- && !mRecentTasks.isCallerRecents(callingUid)) {
- final int startInTaskPerm = mService.checkPermission(START_TASKS_FROM_RECENTS,
- callingPid, callingUid);
- if (startInTaskPerm == PERMISSION_DENIED) {
- final String msg = "Permission Denial: starting " + intent.toString()
- + " from " + callerApp + " (pid=" + callingPid
- + ", uid=" + callingUid + ") with launchTaskId="
- + options.getLaunchTaskId();
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
- }
- }
- // Check if someone tries to launch an activity on a private display with a different
- // owner.
- final int launchDisplayId = options.getLaunchDisplayId();
- if (launchDisplayId != INVALID_DISPLAY && !isCallerAllowedToLaunchOnDisplay(callingPid,
- callingUid, launchDisplayId, aInfo)) {
- final String msg = "Permission Denial: starting " + intent.toString()
- + " from " + callerApp + " (pid=" + callingPid
- + ", uid=" + callingUid + ") with launchDisplayId="
- + launchDisplayId;
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
- }
- // Check if someone tries to launch an unwhitelisted activity into LockTask mode.
- final boolean lockTaskMode = options.getLockTaskMode();
- if (lockTaskMode && !mService.mLockTaskController.isPackageWhitelisted(
- UserHandle.getUserId(callingUid), aInfo.packageName)) {
- final String msg = "Permission Denial: starting " + intent.toString()
- + " from " + callerApp + " (pid=" + callingPid
- + ", uid=" + callingUid + ") with lockTaskMode=true";
- Slog.w(TAG, msg);
- throw new SecurityException(msg);
- }
- }
return true;
}
@@ -2151,8 +2117,8 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
}
}
- void findTaskToMoveToFront(TaskRecord task, int flags, ActivityOptions options,
- String reason, boolean forceNonResizeable) {
+ void findTaskToMoveToFront(TaskRecord task, int flags, ActivityOptions options, String reason,
+ boolean forceNonResizeable) {
final ActivityStack currentStack = task.getStack();
if (currentStack == null) {
Slog.e(TAG, "findTaskToMoveToFront: can't move task="
@@ -2220,8 +2186,8 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
|| mService.mSupportsFreeformWindowManagement;
}
- LaunchingBoundsController getLaunchingBoundsController() {
- return mLaunchingBoundsController;
+ LaunchParamsController getLaunchParamsController() {
+ return mLaunchParamsController;
}
protected <T extends ActivityStack> T getStack(int stackId) {
@@ -2605,8 +2571,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
mAllowDockedStackResize = false;
} else if (inPinnedWindowingMode && onTop) {
// Log if we are expanding the PiP to fullscreen
- MetricsLogger.action(mService.mContext,
- ACTION_PICTURE_IN_PICTURE_EXPANDED_TO_FULLSCREEN);
+ MetricsLoggerWrapper.logPictureInPictureFullScreen(mService.mContext);
}
// If we are moving from the pinned stack, then the animation takes care of updating
@@ -3116,6 +3081,10 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
// Need to make sure the pinned stack exist so we can resize it below...
stack = display.getOrCreateStack(WINDOWING_MODE_PINNED, r.getActivityType(), ON_TOP);
+ // Calculate the target bounds here before the task is reparented back into pinned windowing
+ // mode (which will reset the saved bounds)
+ final Rect destBounds = stack.getDefaultPictureInPictureBounds(aspectRatio);
+
try {
final TaskRecord task = r.getTask();
// Resize the pinned stack to match the current size of the task the activity we are
@@ -3154,11 +3123,6 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
mWindowManager.continueSurfaceLayout();
}
- // Calculate the default bounds (don't use existing stack bounds as we may have just created
- // the stack, and schedule the start of the animation into PiP (the bounds animator that
- // is triggered by this is posted on another thread)
- final Rect destBounds = stack.getDefaultPictureInPictureBounds(aspectRatio);
-
stack.animateResizePinnedStack(sourceHintBounds, destBounds, -1 /* animationDuration */,
true /* fromFullscreen */);
@@ -3738,6 +3702,15 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
}
}
+ public void dumpDisplays(PrintWriter pw) {
+ for (int i = mActivityDisplays.size() - 1; i >= 0; --i) {
+ final ActivityDisplay display = mActivityDisplays.valueAt(i);
+ pw.print("[id:" + display.mDisplayId + " stacks:");
+ display.dumpStacks(pw);
+ pw.print("]");
+ }
+ }
+
public void dump(PrintWriter pw, String prefix) {
pw.print(prefix); pw.print("mFocusedStack=" + mFocusedStack);
pw.print(" mLastFocusedStack="); pw.println(mLastFocusedStack);
@@ -4524,16 +4497,17 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
task.setTaskDockedResizing(true);
}
- int startActivityFromRecents(int taskId, Bundle bOptions) {
+ int startActivityFromRecents(int callingPid, int callingUid, int taskId,
+ SafeActivityOptions options) {
final TaskRecord task;
- final int callingUid;
final String callingPackage;
final Intent intent;
final int userId;
int activityType = ACTIVITY_TYPE_UNDEFINED;
int windowingMode = WINDOWING_MODE_UNDEFINED;
- final ActivityOptions activityOptions = (bOptions != null)
- ? new ActivityOptions(bOptions) : null;
+ final ActivityOptions activityOptions = options != null
+ ? options.getOptions(this)
+ : null;
if (activityOptions != null) {
activityType = activityOptions.getLaunchActivityType();
windowingMode = activityOptions.getLaunchWindowingMode();
@@ -4581,7 +4555,7 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
sendPowerHintForLaunchStartIfNeeded(true /* forceSend */, targetActivity);
mActivityMetricsLogger.notifyActivityLaunching();
try {
- mService.moveTaskToFrontLocked(task.taskId, 0, bOptions,
+ mService.moveTaskToFrontLocked(task.taskId, 0, options,
true /* fromRecents */);
} finally {
mActivityMetricsLogger.notifyActivityLaunched(START_TASK_TO_FRONT,
@@ -4600,13 +4574,13 @@ public class ActivityStackSupervisor extends ConfigurationContainer implements D
task.getStack());
return ActivityManager.START_TASK_TO_FRONT;
}
- callingUid = task.mCallingUid;
callingPackage = task.mCallingPackage;
intent = task.intent;
intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY);
userId = task.userId;
- int result = mService.getActivityStartController().startActivityInPackage(callingUid,
- callingPackage, intent, null, null, null, 0, 0, bOptions, userId, task,
+ int result = mService.getActivityStartController().startActivityInPackage(
+ task.mCallingUid, callingPid, callingUid, callingPackage, intent, null, null,
+ null, 0, 0, options, 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
index aed49e00..5551914f 100644
--- a/com/android/server/am/ActivityStartController.java
+++ b/com/android/server/am/ActivityStartController.java
@@ -220,43 +220,44 @@ public class ActivityStartController {
}
}
- 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) {
+ final int startActivityInPackage(int uid, int realCallingPid, int realCallingUid,
+ String callingPackage, Intent intent, String resolvedType, IBinder resultTo,
+ String resultWho, int requestCode, int startFlags, SafeActivityOptions options,
+ int userId, TaskRecord inTask, String reason) {
- userId = mService.mUserController.handleIncomingUser(Binder.getCallingPid(),
- Binder.getCallingUid(), userId, false, ALLOW_FULL_ONLY, "startActivityInPackage",
- null);
+ userId = mService.mUserController.handleIncomingUser(realCallingPid, realCallingUid, userId,
+ false, ALLOW_FULL_ONLY, "startActivityInPackage", null);
// TODO: Switch to user app stacks here.
return obtainStarter(intent, reason)
.setCallingUid(uid)
+ .setRealCallingPid(realCallingPid)
+ .setRealCallingUid(realCallingUid)
.setCallingPackage(callingPackage)
.setResolvedType(resolvedType)
.setResultTo(resultTo)
.setResultWho(resultWho)
.setRequestCode(requestCode)
.setStartFlags(startFlags)
- .setMayWait(bOptions, userId)
+ .setActivityOptions(options)
+ .setMayWait(userId)
.setInTask(inTask)
.execute();
}
final int startActivitiesInPackage(int uid, String callingPackage, Intent[] intents,
- String[] resolvedTypes, IBinder resultTo, Bundle bOptions, int userId) {
+ String[] resolvedTypes, IBinder resultTo, SafeActivityOptions options, 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;
+ return startActivities(null, uid, callingPackage, intents, resolvedTypes, resultTo, options,
+ userId, reason);
}
int startActivities(IApplicationThread caller, int callingUid, String callingPackage,
- Intent[] intents, String[] resolvedTypes, IBinder resultTo, Bundle bOptions, int userId,
- String reason) {
+ Intent[] intents, String[] resolvedTypes, IBinder resultTo, SafeActivityOptions options,
+ int userId, String reason) {
if (intents == null) {
throw new NullPointerException("intents is null");
}
@@ -312,9 +313,9 @@ public class ActivityStartController {
"FLAG_CANT_SAVE_STATE not supported here");
}
- ActivityOptions options = ActivityOptions.fromBundle(
- i == intents.length - 1 ? bOptions : null);
-
+ final SafeActivityOptions checkedOptions = i == intents.length - 1
+ ? options
+ : null;
final int res = obtainStarter(intent, reason)
.setCaller(caller)
.setResolvedType(resolvedTypes[i])
@@ -326,7 +327,7 @@ public class ActivityStartController {
.setCallingPackage(callingPackage)
.setRealCallingPid(realCallingPid)
.setRealCallingUid(realCallingUid)
- .setActivityOptions(options)
+ .setActivityOptions(checkedOptions)
.setComponentSpecified(componentSpecified)
.setOutActivity(outActivity)
.execute();
diff --git a/com/android/server/am/ActivityStartInterceptor.java b/com/android/server/am/ActivityStartInterceptor.java
index 6684f257..0480646d 100644
--- a/com/android/server/am/ActivityStartInterceptor.java
+++ b/com/android/server/am/ActivityStartInterceptor.java
@@ -30,6 +30,7 @@ import static android.content.Intent.FLAG_ACTIVITY_TASK_ON_HOME;
import static android.content.pm.ApplicationInfo.FLAG_SUSPENDED;
import android.app.ActivityOptions;
+import android.app.AppGlobals;
import android.app.KeyguardManager;
import android.app.admin.DevicePolicyManagerInternal;
import android.content.Context;
@@ -40,10 +41,12 @@ import android.content.pm.ActivityInfo;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.os.Binder;
+import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.app.HarmfulAppWarningActivity;
import com.android.internal.app.UnlaunchableAppActivity;
import com.android.server.LocalServices;
@@ -115,6 +118,15 @@ class ActivityStartInterceptor {
mCallingPackage = callingPackage;
}
+ private IntentSender createIntentSenderForOriginalIntent(int callingUid, int flags) {
+ final IIntentSender target = mService.getIntentSenderLocked(
+ INTENT_SENDER_ACTIVITY, mCallingPackage, callingUid, mUserId, null /*token*/,
+ null /*resultCode*/, 0 /*requestCode*/,
+ new Intent[] { mIntent }, new String[] { mResolvedType },
+ flags, null /*bOptions*/);
+ return new IntentSender(target);
+ }
+
/**
* Intercept the launch intent based on various signals. If an interception happened the
* internal variables get assigned and need to be read explicitly by the caller.
@@ -144,6 +156,11 @@ class ActivityStartInterceptor {
// be unlocked when profile's user is running.
return true;
}
+ if (interceptHarmfulAppIfNeeded()) {
+ // If the app has a "harmful app" warning associated with it, we should ask to uninstall
+ // before issuing the work challenge.
+ return true;
+ }
return interceptWorkProfileChallengeIfNeeded();
}
@@ -152,13 +169,10 @@ class ActivityStartInterceptor {
if (!mUserManager.isQuietModeEnabled(UserHandle.of(mUserId))) {
return false;
}
- IIntentSender target = mService.getIntentSenderLocked(
- INTENT_SENDER_ACTIVITY, mCallingPackage, mCallingUid, mUserId, null, null, 0,
- new Intent[] {mIntent}, new String[] {mResolvedType},
- FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT, null);
+ IntentSender target = createIntentSenderForOriginalIntent(mCallingUid,
+ FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT);
- mIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(mUserId,
- new IntentSender(target));
+ mIntent = UnlaunchableAppActivity.createInQuietModeDialogIntent(mUserId, target);
mCallingPid = mRealCallingPid;
mCallingUid = mRealCallingUid;
mResolvedType = null;
@@ -240,11 +254,8 @@ class ActivityStartInterceptor {
return null;
}
// TODO(b/28935539): should allow certain activities to bypass work challenge
- final IIntentSender target = mService.getIntentSenderLocked(
- INTENT_SENDER_ACTIVITY, callingPackage,
- Binder.getCallingUid(), userId, null, null, 0, new Intent[]{ intent },
- new String[]{ resolvedType },
- FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT | FLAG_IMMUTABLE, null);
+ final IntentSender target = createIntentSenderForOriginalIntent(Binder.getCallingUid(),
+ FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT | FLAG_IMMUTABLE);
final KeyguardManager km = (KeyguardManager) mServiceContext
.getSystemService(KEYGUARD_SERVICE);
final Intent newIntent = km.createConfirmDeviceCredentialIntent(null, null, userId);
@@ -254,8 +265,36 @@ class ActivityStartInterceptor {
newIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS |
FLAG_ACTIVITY_TASK_ON_HOME);
newIntent.putExtra(EXTRA_PACKAGE_NAME, aInfo.packageName);
- newIntent.putExtra(EXTRA_INTENT, new IntentSender(target));
+ newIntent.putExtra(EXTRA_INTENT, target);
return newIntent;
}
+ private boolean interceptHarmfulAppIfNeeded() {
+ CharSequence harmfulAppWarning;
+ try {
+ harmfulAppWarning = AppGlobals.getPackageManager().getHarmfulAppWarning(
+ mAInfo.packageName, mUserId);
+ } catch (RemoteException e) {
+ return false;
+ }
+
+ if (harmfulAppWarning == null) {
+ return false;
+ }
+
+ final IntentSender target = createIntentSenderForOriginalIntent(mCallingUid,
+ FLAG_CANCEL_CURRENT | FLAG_ONE_SHOT | FLAG_IMMUTABLE);
+
+ mIntent = HarmfulAppWarningActivity.createHarmfulAppWarningIntent(mServiceContext,
+ mAInfo.packageName, target, harmfulAppWarning);
+
+ mCallingPid = mRealCallingPid;
+ mCallingUid = mRealCallingUid;
+ mResolvedType = null;
+
+ mRInfo = mSupervisor.resolveIntent(mIntent, mResolvedType, mUserId);
+ mAInfo = mSupervisor.resolveActivity(mIntent, mRInfo, mStartFlags, null /*profilerInfo*/);
+ return true;
+ }
+
}
diff --git a/com/android/server/am/ActivityStarter.java b/com/android/server/am/ActivityStarter.java
index abdbfadf..8fd754af 100644
--- a/com/android/server/am/ActivityStarter.java
+++ b/com/android/server/am/ActivityStarter.java
@@ -74,6 +74,7 @@ import static com.android.server.am.TaskRecord.REPARENT_KEEP_STACK_AT_FRONT;
import static com.android.server.am.TaskRecord.REPARENT_MOVE_STACK_TO_FRONT;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityOptions;
import android.app.IApplicationThread;
@@ -96,7 +97,6 @@ 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;
@@ -109,6 +109,7 @@ 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;
+import com.android.server.am.LaunchParamsController.LaunchParams;
import com.android.server.pm.InstantAppResolver;
import java.io.PrintWriter;
@@ -144,7 +145,7 @@ class ActivityStarter {
private boolean mLaunchTaskBehind;
private int mLaunchFlags;
- private Rect mLaunchBounds = new Rect();
+ private LaunchParams mLaunchParams = new LaunchParams();
private ActivityRecord mNotTop;
private boolean mDoResume;
@@ -298,26 +299,33 @@ class ActivityStarter {
int realCallingPid;
int realCallingUid;
int startFlags;
- ActivityOptions activityOptions;
+ SafeActivityOptions activityOptions;
boolean ignoreTargetSecurity;
boolean componentSpecified;
+ boolean avoidMoveToFront;
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.
+ * {@link ActivityStarter#setMayWait(int)} is called.
* {@see ActivityStarter#startActivityMayWait}.
*/
boolean mayWait;
/**
+ * Ensure constructed request matches reset instance.
+ */
+ Request() {
+ reset();
+ }
+
+ /**
* Sets values back to the initial state, clearing any held references.
*/
void reset() {
@@ -332,8 +340,8 @@ class ActivityStarter {
resultTo = null;
resultWho = null;
requestCode = 0;
- callingPid = 0;
- callingUid = 0;
+ callingPid = DEFAULT_CALLING_PID;
+ callingUid = DEFAULT_CALLING_UID;
callingPackage = null;
realCallingPid = 0;
realCallingUid = 0;
@@ -346,10 +354,10 @@ class ActivityStarter {
reason = null;
profilerInfo = null;
globalConfig = null;
- waitOptions = null;
userId = 0;
waitResult = null;
mayWait = false;
+ avoidMoveToFront = false;
}
/**
@@ -381,10 +389,10 @@ class ActivityStarter {
reason = request.reason;
profilerInfo = request.profilerInfo;
globalConfig = request.globalConfig;
- waitOptions = request.waitOptions;
userId = request.userId;
waitResult = request.waitResult;
mayWait = request.mayWait;
+ avoidMoveToFront = request.avoidMoveToFront;
}
}
@@ -412,7 +420,7 @@ class ActivityStarter {
mLaunchFlags = starter.mLaunchFlags;
mLaunchMode = starter.mLaunchMode;
- mLaunchBounds.set(starter.mLaunchBounds);
+ mLaunchParams.set(starter.mLaunchParams);
mNotTop = starter.mNotTop;
mDoResume = starter.mDoResume;
@@ -466,7 +474,7 @@ class ActivityStarter {
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.activityOptions, mRequest.ignoreTargetSecurity, mRequest.userId,
mRequest.inTask, mRequest.reason);
} else {
return startActivity(mRequest.caller, mRequest.intent, mRequest.ephemeralIntent,
@@ -506,7 +514,7 @@ class ActivityStarter {
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid,
String callingPackage, int realCallingPid, int realCallingUid, int startFlags,
- ActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified,
+ SafeActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified,
ActivityRecord[] outActivity, TaskRecord inTask, String reason) {
if (TextUtils.isEmpty(reason)) {
@@ -548,8 +556,9 @@ class ActivityStarter {
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
IBinder resultTo, String resultWho, int requestCode, int callingPid, int callingUid,
String callingPackage, int realCallingPid, int realCallingUid, int startFlags,
- ActivityOptions options, boolean ignoreTargetSecurity, boolean componentSpecified,
- ActivityRecord[] outActivity, TaskRecord inTask) {
+ SafeActivityOptions options,
+ boolean ignoreTargetSecurity, boolean componentSpecified, ActivityRecord[] outActivity,
+ TaskRecord inTask) {
int err = ActivityManager.START_SUCCESS;
// Pull the optional Ephemeral Installer-only bundle out of the options early.
final Bundle verificationBundle
@@ -596,7 +605,7 @@ class ActivityStarter {
// Transfer the result target from the source activity to the new
// one being started, including any failures.
if (requestCode >= 0) {
- ActivityOptions.abort(options);
+ SafeActivityOptions.abort(options);
return ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT;
}
resultRecord = sourceRecord.resultTo;
@@ -684,16 +693,20 @@ class ActivityStarter {
resultStack.sendActivityResultLocked(
-1, resultRecord, resultWho, requestCode, RESULT_CANCELED, null);
}
- ActivityOptions.abort(options);
+ SafeActivityOptions.abort(options);
return err;
}
boolean abort = !mSupervisor.checkStartAnyActivityPermission(intent, aInfo, resultWho,
- requestCode, callingPid, callingUid, callingPackage, ignoreTargetSecurity, callerApp,
- resultRecord, resultStack, options);
+ requestCode, callingPid, callingUid, callingPackage, ignoreTargetSecurity,
+ callerApp, resultRecord, resultStack);
abort |= !mService.mIntentFirewall.checkStartActivity(intent, callingUid,
callingPid, resolvedType, aInfo.applicationInfo);
+ // Merge the two options bundles, while realCallerOptions takes precedence.
+ ActivityOptions checkedOptions = options != null
+ ? options.getOptions(intent, aInfo, callerApp, mSupervisor)
+ : null;
if (mService.mController != null) {
try {
// The Intent we give to the watcher has the extra data
@@ -708,7 +721,7 @@ class ActivityStarter {
mInterceptor.setStates(userId, realCallingPid, realCallingUid, startFlags, callingPackage);
if (mInterceptor.intercept(intent, rInfo, aInfo, resolvedType, inTask, callingPid,
- callingUid, options)) {
+ callingUid, checkedOptions)) {
// activity start was intercepted, e.g. because the target user is currently in quiet
// mode (turn off work) or the target application is suspended
intent = mInterceptor.mIntent;
@@ -718,7 +731,7 @@ class ActivityStarter {
inTask = mInterceptor.mInTask;
callingPid = mInterceptor.mCallingPid;
callingUid = mInterceptor.mCallingUid;
- options = mInterceptor.mActivityOptions;
+ checkedOptions = mInterceptor.mActivityOptions;
}
if (abort) {
@@ -728,7 +741,7 @@ class ActivityStarter {
}
// We pretend to the caller that it was really started, but
// they will just get a cancel result.
- ActivityOptions.abort(options);
+ ActivityOptions.abort(checkedOptions);
return START_ABORTED;
}
@@ -789,7 +802,7 @@ class ActivityStarter {
ActivityRecord r = new ActivityRecord(mService, callerApp, callingPid, callingUid,
callingPackage, intent, resolvedType, aInfo, mService.getGlobalConfiguration(),
resultRecord, resultWho, requestCode, componentSpecified, voiceSession != null,
- mSupervisor, options, sourceRecord);
+ mSupervisor, checkedOptions, sourceRecord);
if (outActivity != null) {
outActivity[0] = r;
}
@@ -801,13 +814,16 @@ class ActivityStarter {
}
final ActivityStack stack = mSupervisor.mFocusedStack;
+
+ // If we are starting an activity that is not from the same uid as the currently resumed
+ // one, check whether app switches are allowed.
if (voiceSession == null && (stack.mResumedActivity == null
- || stack.mResumedActivity.info.applicationInfo.uid != callingUid)) {
+ || stack.mResumedActivity.info.applicationInfo.uid != realCallingUid)) {
if (!mService.checkAppSwitchAllowedLocked(callingPid, callingUid,
realCallingPid, realCallingUid, "Activity start")) {
mController.addPendingActivityLaunch(new PendingActivityLaunch(r,
sourceRecord, startFlags, stack, callerApp));
- ActivityOptions.abort(options);
+ ActivityOptions.abort(checkedOptions);
return ActivityManager.START_SWITCHES_CANCELED;
}
}
@@ -826,9 +842,10 @@ class ActivityStarter {
mController.doPendingActivityLaunches(false);
return startActivity(r, sourceRecord, voiceSession, voiceInteractor, startFlags,
- true /* doResume */, options, inTask, outActivity);
+ true /* doResume */, checkedOptions, inTask, outActivity);
}
+
/**
* Creates a launch intent for the given auxiliary resolution data.
*/
@@ -893,8 +910,8 @@ class ActivityStarter {
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
IBinder resultTo, String resultWho, int requestCode, int startFlags,
ProfilerInfo profilerInfo, WaitResult outResult,
- Configuration globalConfig, Bundle bOptions, boolean ignoreTargetSecurity, int userId,
- TaskRecord inTask, String reason) {
+ Configuration globalConfig, SafeActivityOptions options, boolean ignoreTargetSecurity,
+ int userId, TaskRecord inTask, String reason) {
// Refuse possible leaked file descriptors
if (intent != null && intent.hasFileDescriptors()) {
throw new IllegalArgumentException("File descriptors passed in Intent");
@@ -946,7 +963,6 @@ class ActivityStarter {
// Collect information about the target of the Intent.
ActivityInfo aInfo = mSupervisor.resolveActivity(intent, rInfo, startFlags, profilerInfo);
- ActivityOptions options = ActivityOptions.fromBundle(bOptions);
synchronized (mService) {
final int realCallingPid = Binder.getCallingPid();
final int realCallingUid = Binder.getCallingUid();
@@ -986,7 +1002,7 @@ class ActivityStarter {
Slog.w(TAG, "Unable to find app for caller " + caller
+ " (pid=" + callingPid + ") when starting: "
+ intent.toString());
- ActivityOptions.abort(options);
+ SafeActivityOptions.abort(options);
return ActivityManager.START_PERMISSION_DENIED;
}
}
@@ -1032,12 +1048,10 @@ class ActivityStarter {
}
final ActivityRecord[] outRecord = new ActivityRecord[1];
- int res = startActivity(caller, intent, ephemeralIntent, resolvedType,
- aInfo, rInfo, voiceSession, voiceInteractor,
- resultTo, resultWho, requestCode, callingPid,
- callingUid, callingPackage, realCallingPid, realCallingUid, startFlags,
- options, ignoreTargetSecurity, componentSpecified, outRecord, inTask,
- reason);
+ int res = startActivity(caller, intent, ephemeralIntent, resolvedType, aInfo, rInfo,
+ voiceSession, voiceInteractor, resultTo, resultWho, requestCode, callingPid,
+ callingUid, callingPackage, realCallingPid, realCallingUid, startFlags, options,
+ ignoreTargetSecurity, componentSpecified, outRecord, inTask, reason);
Binder.restoreCallingIdentity(origId);
@@ -1147,6 +1161,18 @@ class ActivityStarter {
preferredLaunchDisplayId = mOptions.getLaunchDisplayId();
}
+ // windowing mode and preferred launch display values from {@link LaunchParams} take
+ // priority over those specified in {@link ActivityOptions}.
+ if (!mLaunchParams.isEmpty()) {
+ if (mLaunchParams.hasPreferredDisplay()) {
+ preferredLaunchDisplayId = mLaunchParams.mPreferredDisplayId;
+ }
+
+ if (mLaunchParams.hasWindowingMode()) {
+ preferredWindowingMode = mLaunchParams.mWindowingMode;
+ }
+ }
+
if (reusedActivity != null) {
// When the flags NEW_TASK and CLEAR_TASK are set, then the task gets reused but
// still needs to be a lock task mode violation since the task gets cleared out and
@@ -1229,7 +1255,7 @@ class ActivityStarter {
outActivity[0] = reusedActivity;
}
- return START_TASK_TO_FRONT;
+ return mMovedToFront ? START_TASK_TO_FRONT : START_DELIVERED_TO_TOP;
}
}
@@ -1371,7 +1397,7 @@ class ActivityStarter {
mLaunchFlags = 0;
mLaunchMode = INVALID_LAUNCH_MODE;
- mLaunchBounds.setEmpty();
+ mLaunchParams.reset();
mNotTop = null;
mDoResume = false;
@@ -1418,10 +1444,10 @@ class ActivityStarter {
mPreferredDisplayId = getPreferedDisplayId(mSourceRecord, mStartActivity, options);
- mLaunchBounds.setEmpty();
+ mLaunchParams.reset();
- mSupervisor.getLaunchingBoundsController().calculateBounds(inTask, null /*layout*/, r,
- sourceRecord, options, mLaunchBounds);
+ mSupervisor.getLaunchParamsController().calculate(inTask, null /*layout*/, r, sourceRecord,
+ options, mLaunchParams);
mLaunchMode = r.launchMode;
@@ -1462,19 +1488,23 @@ class ActivityStarter {
mDoResume = false;
}
- if (mOptions != null && mOptions.getLaunchTaskId() != -1
- && mOptions.getTaskOverlay()) {
- r.mTaskOverlay = true;
- if (!mOptions.canTaskOverlayResume()) {
- final TaskRecord task = mSupervisor.anyTaskForIdLocked(mOptions.getLaunchTaskId());
- final ActivityRecord top = task != null ? task.getTopActivity() : null;
- if (top != null && top.state != RESUMED) {
-
- // The caller specifies that we'd like to be avoided to be moved to the front,
- // so be it!
- mDoResume = false;
- mAvoidMoveToFront = true;
+ if (mOptions != null) {
+ if (mOptions.getLaunchTaskId() != -1 && mOptions.getTaskOverlay()) {
+ r.mTaskOverlay = true;
+ if (!mOptions.canTaskOverlayResume()) {
+ final TaskRecord task = mSupervisor.anyTaskForIdLocked(
+ mOptions.getLaunchTaskId());
+ final ActivityRecord top = task != null ? task.getTopActivity() : null;
+ if (top != null && top.state != RESUMED) {
+
+ // The caller specifies that we'd like to be avoided to be moved to the
+ // front, so be it!
+ mDoResume = false;
+ mAvoidMoveToFront = true;
+ }
}
+ } else if (mOptions.getAvoidMoveToFront()) {
+ mAvoidMoveToFront = true;
}
}
@@ -1815,7 +1845,7 @@ 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 (!mAvoidMoveToFront && !mMovedToFront && mDoResume) {
if (DEBUG_TASKS) Slog.d(TAG_TASKS, "Bring to front target: " + mTargetStack
+ " from " + intentActivity);
mTargetStack.moveToFront("intentActivityFound");
@@ -1931,7 +1961,7 @@ class ActivityStarter {
mVoiceInteractor, !mLaunchTaskBehind /* toTop */, mStartActivity, mSourceRecord,
mOptions);
addOrReparentStartingActivity(task, "setTaskFromReuseOrCreateNewTask - mReuseTask");
- updateBounds(mStartActivity.getTask(), mLaunchBounds);
+ updateBounds(mStartActivity.getTask(), mLaunchParams.mBounds);
if (DEBUG_TASKS) Slog.v(TAG_TASKS, "Starting new activity " + mStartActivity
+ " in new task " + mStartActivity.getTask());
@@ -2095,7 +2125,7 @@ class ActivityStarter {
return START_TASK_TO_FRONT;
}
- if (!mLaunchBounds.isEmpty()) {
+ if (!mLaunchParams.mBounds.isEmpty()) {
// TODO: Shouldn't we already know what stack to use by the time we get here?
ActivityStack stack = mSupervisor.getLaunchStack(null, null, mInTask, ON_TOP);
if (stack != mInTask.getStack()) {
@@ -2104,7 +2134,7 @@ class ActivityStarter {
mTargetStack = mInTask.getStack();
}
- updateBounds(mInTask, mLaunchBounds);
+ updateBounds(mInTask, mLaunchParams.mBounds);
}
mTargetStack.moveTaskToFrontLocked(
@@ -2428,11 +2458,15 @@ class ActivityStarter {
return this;
}
- ActivityStarter setActivityOptions(ActivityOptions options) {
+ ActivityStarter setActivityOptions(SafeActivityOptions options) {
mRequest.activityOptions = options;
return this;
}
+ ActivityStarter setActivityOptions(Bundle bOptions) {
+ return setActivityOptions(SafeActivityOptions.fromBundle(bOptions));
+ }
+
ActivityStarter setIgnoreTargetSecurity(boolean ignoreTargetSecurity) {
mRequest.ignoreTargetSecurity = ignoreTargetSecurity;
return this;
@@ -2468,19 +2502,13 @@ class ActivityStarter {
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) {
+ ActivityStarter setMayWait(int userId) {
mRequest.mayWait = true;
- mRequest.waitOptions = options;
mRequest.userId = userId;
return this;
diff --git a/com/android/server/am/AppErrorDialog.java b/com/android/server/am/AppErrorDialog.java
index 54122668..68c63a2d 100644
--- a/com/android/server/am/AppErrorDialog.java
+++ b/com/android/server/am/AppErrorDialog.java
@@ -38,9 +38,7 @@ final class AppErrorDialog extends BaseErrorDialog implements View.OnClickListen
private final ActivityManagerService mService;
private final AppErrorResult mResult;
private final ProcessRecord mProc;
- private final boolean mRepeating;
private final boolean mIsRestartable;
- private CharSequence mName;
static int CANT_SHOW = -1;
static int BACKGROUND_USER = -2;
@@ -53,6 +51,7 @@ final class AppErrorDialog extends BaseErrorDialog implements View.OnClickListen
static final int MUTE = 5;
static final int TIMEOUT = 6;
static final int CANCEL = 7;
+ static final int APP_INFO = 8;
// 5-minute timeout, then we automatically dismiss the crash dialog
static final long DISMISS_TIMEOUT = 1000 * 60 * 5;
@@ -64,23 +63,25 @@ final class AppErrorDialog extends BaseErrorDialog implements View.OnClickListen
mService = service;
mProc = data.proc;
mResult = data.result;
- mRepeating = data.repeating;
- mIsRestartable = data.task != null || data.isRestartableForService;
+ mIsRestartable = (data.task != null || data.isRestartableForService)
+ && Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.SHOW_RESTART_IN_CRASH_DIALOG, 0) != 0;
BidiFormatter bidi = BidiFormatter.getInstance();
+ CharSequence name;
if ((mProc.pkgList.size() == 1) &&
- (mName = context.getPackageManager().getApplicationLabel(mProc.info)) != null) {
+ (name = context.getPackageManager().getApplicationLabel(mProc.info)) != null) {
setTitle(res.getString(
- mRepeating ? com.android.internal.R.string.aerr_application_repeated
+ data.repeating ? com.android.internal.R.string.aerr_application_repeated
: com.android.internal.R.string.aerr_application,
- bidi.unicodeWrap(mName.toString()),
+ bidi.unicodeWrap(name.toString()),
bidi.unicodeWrap(mProc.info.processName)));
} else {
- mName = mProc.processName;
+ name = mProc.processName;
setTitle(res.getString(
- mRepeating ? com.android.internal.R.string.aerr_process_repeated
+ data.repeating ? com.android.internal.R.string.aerr_process_repeated
: com.android.internal.R.string.aerr_process,
- bidi.unicodeWrap(mName.toString())));
+ bidi.unicodeWrap(name.toString())));
}
setCancelable(true);
@@ -118,11 +119,14 @@ final class AppErrorDialog extends BaseErrorDialog implements View.OnClickListen
report.setOnClickListener(this);
report.setVisibility(hasReceiver ? View.VISIBLE : View.GONE);
final TextView close = findViewById(com.android.internal.R.id.aerr_close);
- close.setVisibility(mRepeating ? View.VISIBLE : View.GONE);
close.setOnClickListener(this);
+ final TextView appInfo = findViewById(com.android.internal.R.id.aerr_app_info);
+ appInfo.setOnClickListener(this);
boolean showMute = !Build.IS_USER && Settings.Global.getInt(context.getContentResolver(),
- Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
+ Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0
+ && Settings.Global.getInt(context.getContentResolver(),
+ Settings.Global.SHOW_MUTE_IN_CRASH_DIALOG, 0) != 0;
final TextView mute = findViewById(com.android.internal.R.id.aerr_mute);
mute.setOnClickListener(this);
mute.setVisibility(showMute ? View.VISIBLE : View.GONE);
@@ -183,6 +187,9 @@ final class AppErrorDialog extends BaseErrorDialog implements View.OnClickListen
case com.android.internal.R.id.aerr_close:
mHandler.obtainMessage(FORCE_QUIT).sendToTarget();
break;
+ case com.android.internal.R.id.aerr_app_info:
+ mHandler.obtainMessage(APP_INFO).sendToTarget();
+ break;
case com.android.internal.R.id.aerr_mute:
mHandler.obtainMessage(MUTE).sendToTarget();
break;
diff --git a/com/android/server/am/AppErrors.java b/com/android/server/am/AppErrors.java
index 35465a79..9776c4d2 100644
--- a/com/android/server/am/AppErrors.java
+++ b/com/android/server/am/AppErrors.java
@@ -22,6 +22,7 @@ import com.android.internal.logging.nano.MetricsProto;
import com.android.internal.os.ProcessCpuTracker;
import com.android.server.RescueParty;
import com.android.server.Watchdog;
+import com.android.server.am.proto.AppErrorsProto;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -33,6 +34,7 @@ import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
+import android.net.Uri;
import android.os.Binder;
import android.os.Message;
import android.os.Process;
@@ -48,6 +50,7 @@ import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
import java.io.File;
import java.io.FileDescriptor;
@@ -103,8 +106,76 @@ class AppErrors {
mContext = context;
}
- boolean dumpLocked(FileDescriptor fd, PrintWriter pw, boolean needSep,
- String dumpPackage) {
+ void writeToProto(ProtoOutputStream proto, long fieldId, String dumpPackage) {
+ if (mProcessCrashTimes.getMap().isEmpty() && mBadProcesses.getMap().isEmpty()) {
+ return;
+ }
+
+ final long token = proto.start(fieldId);
+ final long now = SystemClock.uptimeMillis();
+ proto.write(AppErrorsProto.NOW_UPTIME_MS, now);
+
+ if (!mProcessCrashTimes.getMap().isEmpty()) {
+ final ArrayMap<String, SparseArray<Long>> pmap = mProcessCrashTimes.getMap();
+ final int procCount = pmap.size();
+ for (int ip = 0; ip < procCount; ip++) {
+ final long ctoken = proto.start(AppErrorsProto.PROCESS_CRASH_TIMES);
+ final String pname = pmap.keyAt(ip);
+ final SparseArray<Long> uids = pmap.valueAt(ip);
+ final int uidCount = uids.size();
+
+ proto.write(AppErrorsProto.ProcessCrashTime.PROCESS_NAME, pname);
+ for (int i = 0; i < uidCount; i++) {
+ final int puid = uids.keyAt(i);
+ final ProcessRecord r = mService.mProcessNames.get(pname, puid);
+ if (dumpPackage != null && (r == null || !r.pkgList.containsKey(dumpPackage))) {
+ continue;
+ }
+ final long etoken = proto.start(AppErrorsProto.ProcessCrashTime.ENTRIES);
+ proto.write(AppErrorsProto.ProcessCrashTime.Entry.UID, puid);
+ proto.write(AppErrorsProto.ProcessCrashTime.Entry.LAST_CRASHED_AT_MS,
+ uids.valueAt(i));
+ proto.end(etoken);
+ }
+ proto.end(ctoken);
+ }
+
+ }
+
+ if (!mBadProcesses.getMap().isEmpty()) {
+ final ArrayMap<String, SparseArray<BadProcessInfo>> pmap = mBadProcesses.getMap();
+ final int processCount = pmap.size();
+ for (int ip = 0; ip < processCount; ip++) {
+ final long btoken = proto.start(AppErrorsProto.BAD_PROCESSES);
+ final String pname = pmap.keyAt(ip);
+ final SparseArray<BadProcessInfo> uids = pmap.valueAt(ip);
+ final int uidCount = uids.size();
+
+ proto.write(AppErrorsProto.BadProcess.PROCESS_NAME, pname);
+ for (int i = 0; i < uidCount; i++) {
+ final int puid = uids.keyAt(i);
+ final ProcessRecord r = mService.mProcessNames.get(pname, puid);
+ if (dumpPackage != null && (r == null
+ || !r.pkgList.containsKey(dumpPackage))) {
+ continue;
+ }
+ final BadProcessInfo info = uids.valueAt(i);
+ final long etoken = proto.start(AppErrorsProto.BadProcess.ENTRIES);
+ proto.write(AppErrorsProto.BadProcess.Entry.UID, puid);
+ proto.write(AppErrorsProto.BadProcess.Entry.CRASHED_AT_MS, info.time);
+ proto.write(AppErrorsProto.BadProcess.Entry.SHORT_MSG, info.shortMsg);
+ proto.write(AppErrorsProto.BadProcess.Entry.LONG_MSG, info.longMsg);
+ proto.write(AppErrorsProto.BadProcess.Entry.STACK, info.stack);
+ proto.end(etoken);
+ }
+ proto.end(btoken);
+ }
+ }
+
+ proto.end(token);
+ }
+
+ boolean dumpLocked(FileDescriptor fd, PrintWriter pw, boolean needSep, String dumpPackage) {
if (!mProcessCrashTimes.getMap().isEmpty()) {
boolean printed = false;
final long now = SystemClock.uptimeMillis();
@@ -408,9 +479,11 @@ class AppErrors {
final Set<String> cats = task.intent.getCategories();
if (cats != null && cats.contains(Intent.CATEGORY_LAUNCHER)) {
mService.getActivityStartController().startActivityInPackage(
- task.mCallingUid, task.mCallingPackage, task.intent, null, null,
- null, 0, 0, ActivityOptions.makeBasic().toBundle(), task.userId,
- null, "AppErrors");
+ task.mCallingUid, callingPid, callingUid, task.mCallingPackage,
+ task.intent, null, null, null, 0, 0,
+ new SafeActivityOptions(ActivityOptions.makeBasic()),
+ task.userId, null,
+ "AppErrors");
}
}
}
@@ -428,6 +501,11 @@ class AppErrors {
Binder.restoreCallingIdentity(orig);
}
}
+ if (res == AppErrorDialog.APP_INFO) {
+ appErrorIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
+ appErrorIntent.setData(Uri.parse("package:" + r.info.packageName));
+ appErrorIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ }
if (res == AppErrorDialog.FORCE_QUIT_AND_REPORT) {
appErrorIntent = createAppErrorIntentLocked(r, timeMillis, crashInfo);
}
@@ -738,9 +816,18 @@ class AppErrors {
}
return;
}
+ final boolean showFirstCrash = Settings.Global.getInt(
+ mContext.getContentResolver(),
+ Settings.Global.SHOW_FIRST_CRASH_DIALOG, 0) != 0;
+ final boolean showFirstCrashDevOption = Settings.Secure.getIntForUser(
+ mContext.getContentResolver(),
+ Settings.Secure.SHOW_FIRST_CRASH_DIALOG_DEV_OPTION,
+ 0,
+ mService.mUserController.getCurrentUserId()) != 0;
final boolean crashSilenced = mAppsNotReportingCrashes != null &&
mAppsNotReportingCrashes.contains(proc.info.packageName);
- if ((mService.canShowErrorDialogs() || showBackground) && !crashSilenced) {
+ if ((mService.canShowErrorDialogs() || showBackground) && !crashSilenced
+ && (showFirstCrash || showFirstCrashDevOption || data.repeating)) {
proc.crashDialog = new AppErrorDialog(mContext, mService, data);
} else {
// The device is asleep, so just pretend that the user
diff --git a/com/android/server/am/AppTaskImpl.java b/com/android/server/am/AppTaskImpl.java
index f821f6bd..5f5a504b 100644
--- a/com/android/server/am/AppTaskImpl.java
+++ b/com/android/server/am/AppTaskImpl.java
@@ -20,6 +20,7 @@ import static com.android.server.am.ActivityStackSupervisor.MATCH_TASK_IN_STACKS
import static com.android.server.am.ActivityStackSupervisor.REMOVE_FROM_RECENTS;
import android.app.ActivityManager;
+import android.app.ActivityOptions;
import android.app.IAppTask;
import android.app.IApplicationThread;
import android.content.Intent;
@@ -93,10 +94,13 @@ class AppTaskImpl extends IAppTask.Stub {
public void moveToFront() {
checkCaller();
// Will bring task to front if it already has a root activity.
+ final int callingPid = Binder.getCallingPid();
+ final int callingUid = Binder.getCallingUid();
final long origId = Binder.clearCallingIdentity();
try {
synchronized (this) {
- mService.mStackSupervisor.startActivityFromRecents(mTaskId, null);
+ mService.mStackSupervisor.startActivityFromRecents(callingPid, callingUid, mTaskId,
+ null);
}
} finally {
Binder.restoreCallingIdentity(origId);
@@ -127,7 +131,8 @@ class AppTaskImpl extends IAppTask.Stub {
.setCaller(appThread)
.setCallingPackage(callingPackage)
.setResolvedType(resolvedType)
- .setMayWait(bOptions, callingUser)
+ .setActivityOptions(bOptions)
+ .setMayWait(callingUser)
.setInTask(tr)
.execute();
}
diff --git a/com/android/server/am/AppTimeTracker.java b/com/android/server/am/AppTimeTracker.java
index 910f33dc..d96364ad 100644
--- a/com/android/server/am/AppTimeTracker.java
+++ b/com/android/server/am/AppTimeTracker.java
@@ -25,6 +25,10 @@ import android.os.SystemClock;
import android.util.ArrayMap;
import android.util.MutableLong;
import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoUtils;
+
+import com.android.server.am.proto.AppTimeTrackerProto;
import java.io.PrintWriter;
@@ -119,4 +123,22 @@ public class AppTimeTracker {
pw.print(prefix); pw.print("mStartedPackage="); pw.println(mStartedPackage);
}
}
+
+ void writeToProto(ProtoOutputStream proto, long fieldId, boolean details) {
+ final long token = proto.start(fieldId);
+ proto.write(AppTimeTrackerProto.RECEIVER, mReceiver.toString());
+ proto.write(AppTimeTrackerProto.TOTAL_DURATION_MS, mTotalTime);
+ for (int i=0; i<mPackageTimes.size(); i++) {
+ final long ptoken = proto.start(AppTimeTrackerProto.PACKAGE_TIMES);
+ proto.write(AppTimeTrackerProto.PackageTime.PACKAGE, mPackageTimes.keyAt(i));
+ proto.write(AppTimeTrackerProto.PackageTime.DURATION_MS, mPackageTimes.valueAt(i).value);
+ proto.end(ptoken);
+ }
+ if (details && mStartedTime != 0) {
+ ProtoUtils.toDuration(proto, AppTimeTrackerProto.STARTED_TIME,
+ mStartedTime, SystemClock.elapsedRealtime());
+ proto.write(AppTimeTrackerProto.STARTED_PACKAGE, mStartedPackage);
+ }
+ proto.end(token);
+ }
}
diff --git a/com/android/server/am/BatteryExternalStatsWorker.java b/com/android/server/am/BatteryExternalStatsWorker.java
index 45824309..927b72ce 100644
--- a/com/android/server/am/BatteryExternalStatsWorker.java
+++ b/com/android/server/am/BatteryExternalStatsWorker.java
@@ -35,6 +35,7 @@ import android.util.TimeUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.BatteryStatsImpl;
+import com.android.internal.util.function.pooled.PooledLambda;
import libcore.util.EmptyArray;
@@ -98,7 +99,7 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
// Keep the last WiFi stats so we can compute a delta.
@GuardedBy("mWorkerLock")
private WifiActivityEnergyInfo mLastInfo =
- new WifiActivityEnergyInfo(0, 0, 0, new long[]{0}, 0, 0, 0);
+ new WifiActivityEnergyInfo(0, 0, 0, new long[]{0}, 0, 0, 0, 0);
BatteryExternalStatsWorker(Context context, BatteryStatsImpl stats) {
mContext = context;
@@ -117,49 +118,45 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
}
@Override
- public Future<?> scheduleReadProcStateCpuTimes() {
+ public synchronized Future<?> scheduleCpuSyncDueToSettingChange() {
+ return scheduleSyncLocked("setting-change", UPDATE_CPU);
+ }
+
+ @Override
+ public Future<?> scheduleReadProcStateCpuTimes(boolean onBattery, boolean onBatteryScreenOff) {
synchronized (mStats) {
- if (!mStats.mPerProcStateCpuTimesAvailable) {
+ if (!mStats.trackPerProcStateCpuTimes()) {
return null;
}
}
synchronized (BatteryExternalStatsWorker.this) {
if (!mExecutorService.isShutdown()) {
- return mExecutorService.submit(mReadProcStateCpuTimesTask);
+ return mExecutorService.submit(PooledLambda.obtainRunnable(
+ BatteryStatsImpl::updateProcStateCpuTimes,
+ mStats, onBattery, onBatteryScreenOff).recycleOnUse());
}
}
return null;
}
@Override
- public Future<?> scheduleCopyFromAllUidsCpuTimes() {
+ public Future<?> scheduleCopyFromAllUidsCpuTimes(
+ boolean onBattery, boolean onBatteryScreenOff) {
synchronized (mStats) {
- if (!mStats.mPerProcStateCpuTimesAvailable) {
+ if (!mStats.trackPerProcStateCpuTimes()) {
return null;
}
}
synchronized (BatteryExternalStatsWorker.this) {
if (!mExecutorService.isShutdown()) {
- return mExecutorService.submit(mCopyFromAllUidsCpuTimesTask);
+ return mExecutorService.submit(PooledLambda.obtainRunnable(
+ BatteryStatsImpl::copyFromAllUidsCpuTimes,
+ mStats, onBattery, onBatteryScreenOff).recycleOnUse());
}
}
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"));
@@ -377,6 +374,7 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
private WifiActivityEnergyInfo extractDeltaLocked(WifiActivityEnergyInfo latest) {
final long timePeriodMs = latest.mTimestamp - mLastInfo.mTimestamp;
+ final long lastScanMs = mLastInfo.mControllerScanTimeMs;
final long lastIdleMs = mLastInfo.mControllerIdleTimeMs;
final long lastTxMs = mLastInfo.mControllerTxTimeMs;
final long lastRxMs = mLastInfo.mControllerRxTimeMs;
@@ -391,14 +389,16 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
final long txTimeMs = latest.mControllerTxTimeMs - lastTxMs;
final long rxTimeMs = latest.mControllerRxTimeMs - lastRxMs;
final long idleTimeMs = latest.mControllerIdleTimeMs - lastIdleMs;
+ final long scanTimeMs = latest.mControllerScanTimeMs - lastScanMs;
- if (txTimeMs < 0 || rxTimeMs < 0) {
+ if (txTimeMs < 0 || rxTimeMs < 0 || scanTimeMs < 0) {
// The stats were reset by the WiFi system (which is why our delta is negative).
// Returns the unaltered stats.
delta.mControllerEnergyUsed = latest.mControllerEnergyUsed;
delta.mControllerRxTimeMs = latest.mControllerRxTimeMs;
delta.mControllerTxTimeMs = latest.mControllerTxTimeMs;
delta.mControllerIdleTimeMs = latest.mControllerIdleTimeMs;
+ delta.mControllerScanTimeMs = latest.mControllerScanTimeMs;
Slog.v(TAG, "WiFi energy data was reset, new WiFi energy data is " + delta);
} else {
final long totalActiveTimeMs = txTimeMs + rxTimeMs;
@@ -436,6 +436,7 @@ class BatteryExternalStatsWorker implements BatteryStatsImpl.ExternalStatsSync {
// These times seem to be the most reliable.
delta.mControllerTxTimeMs = txTimeMs;
delta.mControllerRxTimeMs = rxTimeMs;
+ delta.mControllerScanTimeMs = scanTimeMs;
// WiFi calculates the idle time as a difference from the on time and the various
// Rx + Tx times. There seems to be some missing time there because this sometimes
// becomes negative. Just cap it at 0 and ensure that it is less than the expected idle
diff --git a/com/android/server/am/BatteryStatsService.java b/com/android/server/am/BatteryStatsService.java
index 35318f65..04b49ba3 100644
--- a/com/android/server/am/BatteryStatsService.java
+++ b/com/android/server/am/BatteryStatsService.java
@@ -38,7 +38,10 @@ import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManagerInternal;
import android.os.WorkSource;
+import android.os.WorkSource.WorkChain;
import android.os.connectivity.CellularBatteryStats;
+import android.os.connectivity.WifiBatteryStats;
+import android.os.connectivity.GpsBatteryStats;
import android.os.health.HealthStatsParceler;
import android.os.health.HealthStatsWriter;
import android.os.health.UidHealthStats;
@@ -66,6 +69,7 @@ import java.nio.CharBuffer;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
@@ -183,6 +187,10 @@ public final class BatteryStatsService extends IBatteryStats.Stub
ServiceManager.addService(BatteryStats.SERVICE_NAME, asBinder());
}
+ public void systemServicesReady() {
+ mStats.systemServicesReady(mContext);
+ }
+
private final class LocalService extends BatteryStatsInternal {
@Override
public String[] getWifiIfaces() {
@@ -446,17 +454,24 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
}
- public void noteAlarmStart(String name, int uid) {
+ public void noteWakupAlarm(String name, int uid, WorkSource workSource, String tag) {
+ enforceCallingPermission();
+ synchronized (mStats) {
+ mStats.noteWakupAlarmLocked(name, uid, workSource, tag);
+ }
+ }
+
+ public void noteAlarmStart(String name, WorkSource workSource, int uid) {
enforceCallingPermission();
synchronized (mStats) {
- mStats.noteAlarmStartLocked(name, uid);
+ mStats.noteAlarmStartLocked(name, workSource, uid);
}
}
- public void noteAlarmFinish(String name, int uid) {
+ public void noteAlarmFinish(String name, WorkSource workSource, int uid) {
enforceCallingPermission();
synchronized (mStats) {
- mStats.noteAlarmFinishLocked(name, uid);
+ mStats.noteAlarmFinishLocked(name, workSource, uid);
}
}
@@ -505,6 +520,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
}
+ @Override
public void noteLongPartialWakelockStart(String name, String historyName, int uid) {
enforceCallingPermission();
synchronized (mStats) {
@@ -512,6 +528,16 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
}
+ @Override
+ public void noteLongPartialWakelockStartFromSource(String name, String historyName,
+ WorkSource workSource) {
+ enforceCallingPermission();
+ synchronized (mStats) {
+ mStats.noteLongPartialWakelockStartFromSource(name, historyName, workSource);
+ }
+ }
+
+ @Override
public void noteLongPartialWakelockFinish(String name, String historyName, int uid) {
enforceCallingPermission();
synchronized (mStats) {
@@ -519,6 +545,15 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
}
+ @Override
+ public void noteLongPartialWakelockFinishFromSource(String name, String historyName,
+ WorkSource workSource) {
+ enforceCallingPermission();
+ synchronized (mStats) {
+ mStats.noteLongPartialWakelockFinishFromSource(name, historyName, workSource);
+ }
+ }
+
public void noteStartSensor(int uid, int sensor) {
enforceCallingPermission();
synchronized (mStats) {
@@ -561,6 +596,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
}
+ public void noteGpsSignalQuality(int signalLevel) {
+ synchronized (mStats) {
+ mStats.noteGpsSignalQualityLocked(signalLevel);
+ }
+ }
+
public void noteScreenState(int state) {
enforceCallingPermission();
if (DBG) Slog.d(TAG, "begin noteScreenState");
@@ -900,21 +941,6 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
}
- public void noteWifiMulticastEnabledFromSource(WorkSource ws) {
- enforceCallingPermission();
- synchronized (mStats) {
- mStats.noteWifiMulticastEnabledFromSourceLocked(ws);
- }
- }
-
- @Override
- public void noteWifiMulticastDisabledFromSource(WorkSource ws) {
- enforceCallingPermission();
- synchronized (mStats) {
- mStats.noteWifiMulticastDisabledFromSourceLocked(ws);
- }
- }
-
@Override
public void noteNetworkInterfaceType(String iface, int networkType) {
enforceCallingPermission();
@@ -1156,6 +1182,7 @@ public final class BatteryStatsService extends IBatteryStats.Stub
pw.println(" --write: force write current collected stats to disk.");
pw.println(" --new-daily: immediately create and write new daily stats record.");
pw.println(" --read-daily: read-load last written daily stats.");
+ pw.println(" --settings: dump the settings key/values related to batterystats");
pw.println(" <package.name>: optional name of package to filter output by.");
pw.println(" -h: print this help text.");
pw.println("Battery stats (batterystats) commands:");
@@ -1168,6 +1195,12 @@ public final class BatteryStatsService extends IBatteryStats.Stub
pw.println(" pretend-screen-off: pretend the screen is off, even if screen state changes");
}
+ private void dumpSettings(PrintWriter pw) {
+ synchronized (mStats) {
+ mStats.dumpConstantsLocked(pw);
+ }
+ }
+
private int doEnableOrDisable(PrintWriter pw, int i, String[] args, boolean enable) {
i++;
if (i >= args.length) {
@@ -1278,6 +1311,9 @@ public final class BatteryStatsService extends IBatteryStats.Stub
} else if ("-h".equals(arg)) {
dumpHelp(pw);
return;
+ } else if ("--settings".equals(arg)) {
+ dumpSettings(pw);
+ return;
} else if ("-a".equals(arg)) {
flags |= BatteryStats.DUMP_VERBOSE;
} else if (arg.length() > 0 && arg.charAt(0) == '-'){
@@ -1420,6 +1456,26 @@ public final class BatteryStatsService extends IBatteryStats.Stub
}
/**
+ * Gets a snapshot of Wifi stats
+ * @hide
+ */
+ public WifiBatteryStats getWifiBatteryStats() {
+ synchronized (mStats) {
+ return mStats.getWifiBatteryStats();
+ }
+ }
+
+ /**
+ * Gets a snapshot of Gps stats
+ * @hide
+ */
+ public GpsBatteryStats getGpsBatteryStats() {
+ synchronized (mStats) {
+ return mStats.getGpsBatteryStats();
+ }
+ }
+
+ /**
* Gets a snapshot of the system health for a particular uid.
*/
@Override
diff --git a/com/android/server/am/ClientLifecycleManager.java b/com/android/server/am/ClientLifecycleManager.java
index 1e708098..ae8d9fc1 100644
--- a/com/android/server/am/ClientLifecycleManager.java
+++ b/com/android/server/am/ClientLifecycleManager.java
@@ -43,8 +43,9 @@ class ClientLifecycleManager {
* @see ClientTransaction
*/
void scheduleTransaction(ClientTransaction transaction) throws RemoteException {
+ final IApplicationThread client = transaction.getClient();
transaction.schedule();
- if (!(transaction.getClient() instanceof Binder)) {
+ if (!(client 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.
diff --git a/com/android/server/am/ConnectionRecord.java b/com/android/server/am/ConnectionRecord.java
index 6df283ca..d320fb1d 100644
--- a/com/android/server/am/ConnectionRecord.java
+++ b/com/android/server/am/ConnectionRecord.java
@@ -20,6 +20,7 @@ import android.app.IServiceConnection;
import android.app.PendingIntent;
import android.content.Context;
import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoUtils;
import com.android.server.am.proto.ConnectionRecordProto;
@@ -37,7 +38,43 @@ final class ConnectionRecord {
final PendingIntent clientIntent; // How to launch the client.
String stringName; // Caching of toString.
boolean serviceDead; // Well is it?
-
+
+ // Please keep the following two enum list synced.
+ private static int[] BIND_ORIG_ENUMS = new int[] {
+ Context.BIND_AUTO_CREATE,
+ Context.BIND_DEBUG_UNBIND,
+ Context.BIND_NOT_FOREGROUND,
+ Context.BIND_IMPORTANT_BACKGROUND,
+ Context.BIND_ABOVE_CLIENT,
+ Context.BIND_ALLOW_OOM_MANAGEMENT,
+ Context.BIND_WAIVE_PRIORITY,
+ Context.BIND_IMPORTANT,
+ Context.BIND_ADJUST_WITH_ACTIVITY,
+ Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
+ Context.BIND_FOREGROUND_SERVICE,
+ Context.BIND_TREAT_LIKE_ACTIVITY,
+ Context.BIND_VISIBLE,
+ Context.BIND_SHOWING_UI,
+ Context.BIND_NOT_VISIBLE,
+ };
+ private static int[] BIND_PROTO_ENUMS = new int[] {
+ ConnectionRecordProto.AUTO_CREATE,
+ ConnectionRecordProto.DEBUG_UNBIND,
+ ConnectionRecordProto.NOT_FG,
+ ConnectionRecordProto.IMPORTANT_BG,
+ ConnectionRecordProto.ABOVE_CLIENT,
+ ConnectionRecordProto.ALLOW_OOM_MANAGEMENT,
+ ConnectionRecordProto.WAIVE_PRIORITY,
+ ConnectionRecordProto.IMPORTANT,
+ ConnectionRecordProto.ADJUST_WITH_ACTIVITY,
+ ConnectionRecordProto.FG_SERVICE_WHILE_AWAKE,
+ ConnectionRecordProto.FG_SERVICE,
+ ConnectionRecordProto.TREAT_LIKE_ACTIVITY,
+ ConnectionRecordProto.VISIBLE,
+ ConnectionRecordProto.SHOWING_UI,
+ ConnectionRecordProto.NOT_VISIBLE,
+ };
+
void dump(PrintWriter pw, String prefix) {
pw.println(prefix + "binding=" + binding);
if (activity != null) {
@@ -46,7 +83,7 @@ final class ConnectionRecord {
pw.println(prefix + "conn=" + conn.asBinder()
+ " flags=0x" + Integer.toHexString(flags));
}
-
+
ConnectionRecord(AppBindRecord _binding, ActivityRecord _activity,
IServiceConnection _conn, int _flags,
int _clientLabel, PendingIntent _clientIntent) {
@@ -131,51 +168,8 @@ final class ConnectionRecord {
if (binding.client != null) {
proto.write(ConnectionRecordProto.USER_ID, binding.client.userId);
}
- if ((flags&Context.BIND_AUTO_CREATE) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.AUTO_CREATE);
- }
- if ((flags&Context.BIND_DEBUG_UNBIND) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.DEBUG_UNBIND);
- }
- if ((flags&Context.BIND_NOT_FOREGROUND) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.NOT_FG);
- }
- if ((flags&Context.BIND_IMPORTANT_BACKGROUND) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.IMPORTANT_BG);
- }
- if ((flags&Context.BIND_ABOVE_CLIENT) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.ABOVE_CLIENT);
- }
- if ((flags&Context.BIND_ALLOW_OOM_MANAGEMENT) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.ALLOW_OOM_MANAGEMENT);
- }
- if ((flags&Context.BIND_WAIVE_PRIORITY) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.WAIVE_PRIORITY);
- }
- if ((flags&Context.BIND_IMPORTANT) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.IMPORTANT);
- }
- if ((flags&Context.BIND_ADJUST_WITH_ACTIVITY) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.ADJUST_WITH_ACTIVITY);
- }
- if ((flags&Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.FG_SERVICE_WHILE_WAKE);
- }
- if ((flags&Context.BIND_FOREGROUND_SERVICE) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.FG_SERVICE);
- }
- if ((flags&Context.BIND_TREAT_LIKE_ACTIVITY) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.TREAT_LIKE_ACTIVITY);
- }
- if ((flags&Context.BIND_VISIBLE) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.VISIBLE);
- }
- if ((flags&Context.BIND_SHOWING_UI) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.SHOWING_UI);
- }
- if ((flags&Context.BIND_NOT_VISIBLE) != 0) {
- proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.NOT_VISIBLE);
- }
+ ProtoUtils.writeBitWiseFlagsToProtoEnum(proto, ConnectionRecordProto.FLAGS,
+ flags, BIND_ORIG_ENUMS, BIND_PROTO_ENUMS);
if (serviceDead) {
proto.write(ConnectionRecordProto.FLAGS, ConnectionRecordProto.DEAD);
}
diff --git a/com/android/server/am/GlobalSettingsToPropertiesMapper.java b/com/android/server/am/GlobalSettingsToPropertiesMapper.java
new file mode 100644
index 00000000..d6c6f962
--- /dev/null
+++ b/com/android/server/am/GlobalSettingsToPropertiesMapper.java
@@ -0,0 +1,115 @@
+/*
+ * 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 com.android.server.am;
+
+import android.content.ContentResolver;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.SystemProperties;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Slog;
+import android.view.ThreadedRenderer;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+/**
+ * Maps global system settings to system properties.
+ * <p>The properties are dynamically updated when settings change.
+ */
+class GlobalSettingsToPropertiesMapper {
+
+ private static final String TAG = "GlobalSettingsToPropertiesMapper";
+
+ private static final String[][] sGlobalSettingsMapping = new String[][] {
+ // List mapping entries in the following format:
+ // {Settings.Global.SETTING_NAME, "system_property_name"},
+ {Settings.Global.SYS_VDSO, "sys.vdso"},
+ {Settings.Global.FPS_DEVISOR, ThreadedRenderer.DEBUG_FPS_DIVISOR},
+ };
+
+
+ private final ContentResolver mContentResolver;
+ private final String[][] mGlobalSettingsMapping;
+
+ @VisibleForTesting
+ GlobalSettingsToPropertiesMapper(ContentResolver contentResolver,
+ String[][] globalSettingsMapping) {
+ mContentResolver = contentResolver;
+ mGlobalSettingsMapping = globalSettingsMapping;
+ }
+
+ void updatePropertiesFromGlobalSettings() {
+ for (String[] entry : mGlobalSettingsMapping) {
+ final String settingName = entry[0];
+ final String propName = entry[1];
+ Uri settingUri = Settings.Global.getUriFor(settingName);
+ Preconditions.checkNotNull(settingUri, "Setting " + settingName + " not found");
+ ContentObserver co = new ContentObserver(null) {
+ @Override
+ public void onChange(boolean selfChange) {
+ updatePropertyFromSetting(settingName, propName);
+ }
+ };
+ updatePropertyFromSetting(settingName, propName);
+ mContentResolver.registerContentObserver(settingUri, false, co);
+ }
+ }
+
+ public static void start(ContentResolver contentResolver) {
+ new GlobalSettingsToPropertiesMapper(contentResolver, sGlobalSettingsMapping)
+ .updatePropertiesFromGlobalSettings();
+ }
+
+ private String getGlobalSetting(String name) {
+ return Settings.Global.getString(mContentResolver, name);
+ }
+
+ private void setProperty(String key, String value) {
+ // Check if need to clear the property
+ if (value == null) {
+ // It's impossible to remove system property, therefore we check previous value to
+ // avoid setting an empty string if the property wasn't set.
+ if (TextUtils.isEmpty(systemPropertiesGet(key))) {
+ return;
+ }
+ value = "";
+ }
+ try {
+ systemPropertiesSet(key, value);
+ } catch (IllegalArgumentException e) {
+ Slog.e(TAG, "Unable to set property " + key + " value '" + value + "'", e);
+ }
+ }
+
+ @VisibleForTesting
+ protected String systemPropertiesGet(String key) {
+ return SystemProperties.get(key);
+ }
+
+ @VisibleForTesting
+ protected void systemPropertiesSet(String key, String value) {
+ SystemProperties.set(key, value);
+ }
+
+ @VisibleForTesting
+ void updatePropertyFromSetting(String settingName, String propName) {
+ String settingValue = getGlobalSetting(settingName);
+ setProperty(propName, settingValue);
+ }
+}
diff --git a/com/android/server/am/KeyguardController.java b/com/android/server/am/KeyguardController.java
index 35f4f253..79f3fe3f 100644
--- a/com/android/server/am/KeyguardController.java
+++ b/com/android/server/am/KeyguardController.java
@@ -27,13 +27,13 @@ import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NA
import static com.android.server.am.ActivityStackSupervisor.PRESERVE_WINDOWS;
import static com.android.server.am.proto.KeyguardControllerProto.KEYGUARD_OCCLUDED;
import static com.android.server.am.proto.KeyguardControllerProto.KEYGUARD_SHOWING;
-import static com.android.server.wm.AppTransition.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION;
-import static com.android.server.wm.AppTransition.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE;
-import static com.android.server.wm.AppTransition.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER;
-import static com.android.server.wm.AppTransition.TRANSIT_KEYGUARD_GOING_AWAY;
-import static com.android.server.wm.AppTransition.TRANSIT_KEYGUARD_OCCLUDE;
-import static com.android.server.wm.AppTransition.TRANSIT_KEYGUARD_UNOCCLUDE;
-import static com.android.server.wm.AppTransition.TRANSIT_UNSET;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
+import static android.view.WindowManager.TRANSIT_UNSET;
import android.app.ActivityManagerInternal.SleepToken;
import android.os.IBinder;
@@ -150,7 +150,7 @@ class KeyguardController {
}
}
- void dismissKeyguard(IBinder token, IKeyguardDismissCallback callback) {
+ void dismissKeyguard(IBinder token, IKeyguardDismissCallback callback, CharSequence message) {
final ActivityRecord activityRecord = ActivityRecord.forTokenLocked(token);
if (activityRecord == null || !activityRecord.visibleIgnoringKeyguard) {
failCallback(callback);
@@ -164,7 +164,7 @@ class KeyguardController {
mStackSupervisor.wakeUp("dismissKeyguard");
}
- mWindowManager.dismissKeyguard(callback);
+ mWindowManager.dismissKeyguard(callback, message);
}
private void setKeyguardGoingAway(boolean keyguardGoingAway) {
@@ -304,7 +304,7 @@ class KeyguardController {
// insecure case, we actually show it on top of the lockscreen. See #canShowWhileOccluded.
if (!mOccluded && mDismissingKeyguardActivity != null
&& mWindowManager.isKeyguardSecure()) {
- mWindowManager.dismissKeyguard(null /* callback */);
+ mWindowManager.dismissKeyguard(null /* callback */, null /* message */);
mDismissalRequested = true;
// If we are about to unocclude the Keyguard, but we can dismiss it without security,
diff --git a/com/android/server/am/LaunchParamsController.java b/com/android/server/am/LaunchParamsController.java
new file mode 100644
index 00000000..7ab7f987
--- /dev/null
+++ b/com/android/server/am/LaunchParamsController.java
@@ -0,0 +1,256 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 android.annotation.IntDef;
+import android.app.ActivityOptions;
+import android.content.pm.ActivityInfo.WindowLayout;
+import android.graphics.Rect;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
+import static android.view.Display.INVALID_DISPLAY;
+
+import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE;
+import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_DONE;
+import static com.android.server.am.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
+
+/**
+ * {@link LaunchParamsController} calculates the {@link LaunchParams} by coordinating between
+ * registered {@link LaunchParamsModifier}s.
+ */
+class LaunchParamsController {
+ private final ActivityManagerService mService;
+ private final List<LaunchParamsModifier> mModifiers = new ArrayList<>();
+
+ // Temporary {@link LaunchParams} for internal calculations. This is kept separate from
+ // {@code mTmpCurrent} and {@code mTmpResult} to prevent clobbering values.
+ private final LaunchParams mTmpParams = new LaunchParams();
+
+ private final LaunchParams mTmpCurrent = new LaunchParams();
+ private final LaunchParams mTmpResult = new LaunchParams();
+
+ LaunchParamsController(ActivityManagerService service) {
+ mService = service;
+ }
+
+ /**
+ * Creates a {@link LaunchParamsController} with default registered
+ * {@link LaunchParamsModifier}s.
+ */
+ void registerDefaultModifiers(ActivityStackSupervisor supervisor) {
+ // {@link TaskLaunchParamsModifier} handles window layout preferences.
+ registerModifier(new TaskLaunchParamsModifier());
+
+ // {@link ActivityLaunchParamsModifier} is the most specific modifier and thus should be
+ // registered last (applied first) out of the defaults.
+ registerModifier(new ActivityLaunchParamsModifier(supervisor));
+ }
+
+ /**
+ * Returns the {@link LaunchParams} calculated by the registered modifiers
+ * @param task The {@link TaskRecord} currently being positioned.
+ * @param layout The specified {@link WindowLayout}.
+ * @param activity The {@link ActivityRecord} currently being positioned.
+ * @param source The {@link ActivityRecord} from which activity was started from.
+ * @param options The {@link ActivityOptions} specified for the activity.
+ * @param result The resulting params.
+ */
+ void calculate(TaskRecord task, WindowLayout layout, ActivityRecord activity,
+ ActivityRecord source, ActivityOptions options, LaunchParams result) {
+ result.reset();
+
+ // We start at the last registered {@link LaunchParamsModifier} as this represents
+ // The modifier closest to the product level. Moving back through the list moves closer to
+ // the platform logic.
+ for (int i = mModifiers.size() - 1; i >= 0; --i) {
+ mTmpCurrent.set(result);
+ mTmpResult.reset();
+ final LaunchParamsModifier modifier = mModifiers.get(i);
+
+ switch(modifier.onCalculate(task, layout, activity, source, options, mTmpCurrent,
+ mTmpResult)) {
+ case RESULT_SKIP:
+ // Do not apply any results when we are told to skip
+ continue;
+ case RESULT_DONE:
+ // Set result and return immediately.
+ result.set(mTmpResult);
+ return;
+ case RESULT_CONTINUE:
+ // Set result and continue
+ result.set(mTmpResult);
+ break;
+ }
+ }
+ }
+
+ /**
+ * A convenience method for laying out a task.
+ * @return {@code true} if bounds were set on the task. {@code false} otherwise.
+ */
+ boolean layoutTask(TaskRecord task, WindowLayout layout) {
+ return layoutTask(task, layout, null /*activity*/, null /*source*/, null /*options*/);
+ }
+
+ boolean layoutTask(TaskRecord task, WindowLayout layout, ActivityRecord activity,
+ ActivityRecord source, ActivityOptions options) {
+ calculate(task, layout, activity, source, options, mTmpParams);
+
+ // No changes, return.
+ if (mTmpParams.isEmpty()) {
+ return false;
+ }
+
+ mService.mWindowManager.deferSurfaceLayout();
+
+ try {
+ if (mTmpParams.hasPreferredDisplay()
+ && mTmpParams.mPreferredDisplayId != task.getStack().getDisplay().mDisplayId) {
+ mService.moveStackToDisplay(task.getStackId(), mTmpParams.mPreferredDisplayId);
+ }
+
+ if (mTmpParams.hasWindowingMode()
+ && mTmpParams.mWindowingMode != task.getStack().getWindowingMode()) {
+ task.getStack().setWindowingMode(mTmpParams.mWindowingMode);
+ }
+
+ if (!mTmpParams.mBounds.isEmpty()) {
+ task.updateOverrideConfiguration(mTmpParams.mBounds);
+ return true;
+ } else {
+ return false;
+ }
+ } finally {
+ mService.mWindowManager.continueSurfaceLayout();
+ }
+ }
+
+ /**
+ * Adds a modifier to participate in future bounds calculation. Note that the last registered
+ * {@link LaunchParamsModifier} will be the first to calculate the bounds.
+ */
+ void registerModifier(LaunchParamsModifier modifier) {
+ if (mModifiers.contains(modifier)) {
+ return;
+ }
+
+ mModifiers.add(modifier);
+ }
+
+ /**
+ * A container for holding launch related fields.
+ */
+ static class LaunchParams {
+ /** The bounds within the parent container. */
+ final Rect mBounds = new Rect();
+
+ /** The id of the display the {@link TaskRecord} would prefer to be on. */
+ int mPreferredDisplayId;
+
+ /** The windowing mode to be in. */
+ int mWindowingMode;
+
+ /** Sets values back to default. {@link #isEmpty} will return {@code true} once called. */
+ void reset() {
+ mBounds.setEmpty();
+ mPreferredDisplayId = INVALID_DISPLAY;
+ mWindowingMode = WINDOWING_MODE_UNDEFINED;
+ }
+
+ /** Copies the values set on the passed in {@link LaunchParams}. */
+ void set(LaunchParams params) {
+ mBounds.set(params.mBounds);
+ mPreferredDisplayId = params.mPreferredDisplayId;
+ mWindowingMode = params.mWindowingMode;
+ }
+
+ /** Returns {@code true} if no values have been explicitly set. */
+ boolean isEmpty() {
+ return mBounds.isEmpty() && mPreferredDisplayId == INVALID_DISPLAY
+ && mWindowingMode == WINDOWING_MODE_UNDEFINED;
+ }
+
+ boolean hasWindowingMode() {
+ return mWindowingMode != WINDOWING_MODE_UNDEFINED;
+ }
+
+ boolean hasPreferredDisplay() {
+ return mPreferredDisplayId != INVALID_DISPLAY;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ LaunchParams that = (LaunchParams) o;
+
+ if (mPreferredDisplayId != that.mPreferredDisplayId) return false;
+ if (mWindowingMode != that.mWindowingMode) return false;
+ return mBounds != null ? mBounds.equals(that.mBounds) : that.mBounds == null;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = mBounds != null ? mBounds.hashCode() : 0;
+ result = 31 * result + mPreferredDisplayId;
+ result = 31 * result + mWindowingMode;
+ return result;
+ }
+ }
+
+ /**
+ * An interface implemented by those wanting to participate in bounds calculation.
+ */
+ interface LaunchParamsModifier {
+ @Retention(RetentionPolicy.SOURCE)
+ @IntDef({RESULT_SKIP, RESULT_DONE, RESULT_CONTINUE})
+ @interface Result {}
+
+ // Returned when the modifier does not want to influence the bounds calculation
+ int RESULT_SKIP = 0;
+ // Returned when the modifier has changed the bounds and would like its results to be the
+ // final bounds applied.
+ int RESULT_DONE = 1;
+ // Returned when the modifier has changed the bounds but is okay with other modifiers
+ // influencing the bounds.
+ int RESULT_CONTINUE = 2;
+
+ /**
+ * Called when asked to calculate {@link LaunchParams}.
+ * @param task The {@link TaskRecord} currently being positioned.
+ * @param layout The specified {@link WindowLayout}.
+ * @param activity The {@link ActivityRecord} currently being positioned.
+ * @param source The {@link ActivityRecord} activity was started from.
+ * @param options The {@link ActivityOptions} specified for the activity.
+ * @param currentParams The current {@link LaunchParams}. This can differ from the initial
+ * params as it represents the modified params up to this point.
+ * @param outParams The resulting {@link LaunchParams} after all calculations.
+ * @return A {@link Result} representing the result of the
+ * {@link LaunchParams} calculation.
+ */
+ @Result
+ int onCalculate(TaskRecord task, WindowLayout layout, ActivityRecord activity,
+ ActivityRecord source, ActivityOptions options, LaunchParams currentParams,
+ LaunchParams outParams);
+ }
+}
diff --git a/com/android/server/am/LaunchingBoundsController.java b/com/android/server/am/LaunchingBoundsController.java
deleted file mode 100644
index 5aa7f58f..00000000
--- a/com/android/server/am/LaunchingBoundsController.java
+++ /dev/null
@@ -1,167 +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.am;
-
-import android.annotation.IntDef;
-import android.app.ActivityOptions;
-import android.content.pm.ActivityInfo.WindowLayout;
-import android.graphics.Rect;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.List;
-
-import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_CONTINUE;
-import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_DONE;
-import static com.android.server.am.LaunchingBoundsController.LaunchingBoundsPositioner.RESULT_SKIP;
-
-/**
- * {@link LaunchingBoundsController} calculates the launch bounds by coordinating between registered
- * {@link LaunchingBoundsPositioner}.
- */
-class LaunchingBoundsController {
- private final List<LaunchingBoundsPositioner> mPositioners = new ArrayList<>();
-
- // Temporary {@link Rect} for calculations. This is kept separate from {@code mTmpCurrent} and
- // {@code mTmpResult} to prevent clobbering values.
- private final Rect mTmpRect = new Rect();
-
- private final Rect mTmpCurrent = new Rect();
- private final Rect mTmpResult = new Rect();
-
- /**
- * Creates a {@link LaunchingBoundsController} with default registered
- * {@link LaunchingBoundsPositioner}s.
- */
- void registerDefaultPositioners(ActivityStackSupervisor supervisor) {
- // {@link LaunchingTaskPositioner} handles window layout preferences.
- registerPositioner(new LaunchingTaskPositioner());
-
- // {@link LaunchingActivityPositioner} is the most specific positioner and thus should be
- // registered last (applied first) out of the defaults.
- registerPositioner(new LaunchingActivityPositioner(supervisor));
- }
-
- /**
- * Returns the position calculated by the registered positioners
- * @param task The {@link TaskRecord} currently being positioned.
- * @param layout The specified {@link WindowLayout}.
- * @param activity The {@link ActivityRecord} currently being positioned.
- * @param source The {@link ActivityRecord} from which activity was started from.
- * @param options The {@link ActivityOptions} specified for the activity.
- * @param result The resulting bounds. If no bounds are set, {@link Rect#isEmpty()} will be
- * {@code true}.
- */
- void calculateBounds(TaskRecord task, WindowLayout layout, ActivityRecord activity,
- ActivityRecord source, ActivityOptions options, Rect result) {
- result.setEmpty();
-
- // We start at the last registered {@link LaunchingBoundsPositioner} as this represents
- // The positioner closest to the product level. Moving back through the list moves closer to
- // the platform logic.
- for (int i = mPositioners.size() - 1; i >= 0; --i) {
- mTmpResult.setEmpty();
- mTmpCurrent.set(result);
- final LaunchingBoundsPositioner positioner = mPositioners.get(i);
-
- switch(positioner.onCalculateBounds(task, layout, activity, source, options,
- mTmpCurrent, mTmpResult)) {
- case RESULT_SKIP:
- // Do not apply any results when we are told to skip
- continue;
- case RESULT_DONE:
- // Set result and return immediately.
- result.set(mTmpResult);
- return;
- case RESULT_CONTINUE:
- // Set result and continue
- result.set(mTmpResult);
- break;
- }
- }
- }
-
- /**
- * A convenience method for laying out a task.
- * @return {@code true} if bounds were set on the task. {@code false} otherwise.
- */
- boolean layoutTask(TaskRecord task, WindowLayout layout) {
- return layoutTask(task, layout, null /*activity*/, null /*source*/, null /*options*/);
- }
-
- boolean layoutTask(TaskRecord task, WindowLayout layout, ActivityRecord activity,
- ActivityRecord source, ActivityOptions options) {
- calculateBounds(task, layout, activity, source, options, mTmpRect);
-
- if (mTmpRect.isEmpty()) {
- return false;
- }
-
- task.updateOverrideConfiguration(mTmpRect);
-
- return true;
- }
-
- /**
- * Adds a positioner to participate in future bounds calculation. Note that the last registered
- * {@link LaunchingBoundsPositioner} will be the first to calculate the bounds.
- */
- void registerPositioner(LaunchingBoundsPositioner positioner) {
- if (mPositioners.contains(positioner)) {
- return;
- }
-
- mPositioners.add(positioner);
- }
-
- /**
- * An interface implemented by those wanting to participate in bounds calculation.
- */
- interface LaunchingBoundsPositioner {
- @Retention(RetentionPolicy.SOURCE)
- @IntDef({RESULT_SKIP, RESULT_DONE, RESULT_CONTINUE})
- @interface Result {}
-
- // Returned when the positioner does not want to influence the bounds calculation
- int RESULT_SKIP = 0;
- // Returned when the positioner has changed the bounds and would like its results to be the
- // final bounds applied.
- int RESULT_DONE = 1;
- // Returned when the positioner has changed the bounds but is okay with other positioners
- // influencing the bounds.
- int RESULT_CONTINUE = 2;
-
- /**
- * Called when asked to calculate bounds.
- * @param task The {@link TaskRecord} currently being positioned.
- * @param layout The specified {@link WindowLayout}.
- * @param activity The {@link ActivityRecord} currently being positioned.
- * @param source The {@link ActivityRecord} activity was started from.
- * @param options The {@link ActivityOptions} specified for the activity.
- * @param current The current bounds. This can differ from the initial bounds as it
- * represents the modified bounds up to this point.
- * @param result The {@link Rect} which the positioner should return its modified bounds.
- * Any merging of the current bounds should be already applied to this
- * value as well before returning.
- * @return A {@link Result} representing the result of the bounds calculation.
- */
- @Result
- int onCalculateBounds(TaskRecord task, WindowLayout layout, ActivityRecord activity,
- ActivityRecord source, ActivityOptions options, Rect current, Rect result);
- }
-}
diff --git a/com/android/server/am/LockTaskController.java b/com/android/server/am/LockTaskController.java
index ba3e25ae..21f9135b 100644
--- a/com/android/server/am/LockTaskController.java
+++ b/com/android/server/am/LockTaskController.java
@@ -752,7 +752,7 @@ public class LockTaskController {
USER_CURRENT) != 0;
if (shouldLockKeyguard) {
mWindowManager.lockNow(null);
- mWindowManager.dismissKeyguard(null /* callback */);
+ mWindowManager.dismissKeyguard(null /* callback */, null /* message */);
getLockPatternUtils().requireCredentialEntry(USER_ALL);
}
} catch (Settings.SettingNotFoundException e) {
diff --git a/com/android/server/am/PendingIntentRecord.java b/com/android/server/am/PendingIntentRecord.java
index c26e7703..8e9d85d6 100644
--- a/com/android/server/am/PendingIntentRecord.java
+++ b/com/android/server/am/PendingIntentRecord.java
@@ -16,10 +16,12 @@
package com.android.server.am;
+import static android.app.ActivityManager.START_SUCCESS;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_AM;
import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NAME;
import android.app.ActivityManager;
+import android.app.ActivityOptions;
import android.content.IIntentSender;
import android.content.IIntentReceiver;
import android.app.PendingIntent;
@@ -65,7 +67,7 @@ final class PendingIntentRecord extends IIntentSender.Stub {
final int requestCode;
final Intent requestIntent;
final String requestResolvedType;
- final Bundle options;
+ final SafeActivityOptions options;
Intent[] allIntents;
String[] allResolvedTypes;
final int flags;
@@ -75,7 +77,7 @@ final class PendingIntentRecord extends IIntentSender.Stub {
private static final int ODD_PRIME_NUMBER = 37;
Key(int _t, String _p, ActivityRecord _a, String _w,
- int _r, Intent[] _i, String[] _it, int _f, Bundle _o, int _userId) {
+ int _r, Intent[] _i, String[] _it, int _f, SafeActivityOptions _o, int _userId) {
type = _t;
packageName = _p;
activity = _a;
@@ -310,17 +312,16 @@ final class PendingIntentRecord extends IIntentSender.Stub {
if (userId == UserHandle.USER_CURRENT) {
userId = owner.mUserController.getCurrentOrTargetUserId();
}
- int res = 0;
+ int res = START_SUCCESS;
switch (key.type) {
case ActivityManager.INTENT_SENDER_ACTIVITY:
- if (options == null) {
- options = key.options;
- } else if (key.options != null) {
- Bundle opts = new Bundle(key.options);
- opts.putAll(options);
- options = opts;
- }
try {
+ SafeActivityOptions mergedOptions = key.options;
+ if (mergedOptions == null) {
+ mergedOptions = SafeActivityOptions.fromBundle(options);
+ } else {
+ mergedOptions.setCallerOptions(ActivityOptions.fromBundle(options));
+ }
if (key.allIntents != null && key.allIntents.length > 1) {
Intent[] allIntents = new Intent[key.allIntents.length];
String[] allResolvedTypes = new String[key.allIntents.length];
@@ -332,14 +333,14 @@ final class PendingIntentRecord extends IIntentSender.Stub {
}
allIntents[allIntents.length-1] = finalIntent;
allResolvedTypes[allResolvedTypes.length-1] = resolvedType;
- owner.getActivityStartController().startActivitiesInPackage(uid,
- key.packageName, allIntents, allResolvedTypes, resultTo,
- options, userId);
+ res = owner.getActivityStartController().startActivitiesInPackage(
+ uid, key.packageName, allIntents, allResolvedTypes,
+ resultTo, mergedOptions, userId);
} else {
- owner.getActivityStartController().startActivityInPackage(uid,
- key.packageName, finalIntent, resolvedType, resultTo,
- resultWho, requestCode, 0, options, userId, null,
- "PendingIntentRecord");
+ res = owner.getActivityStartController().startActivityInPackage(uid,
+ callingPid, callingUid, key.packageName, finalIntent,
+ resolvedType, resultTo, resultWho, requestCode, 0,
+ mergedOptions, 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 ab5d64c4..29bfebe6 100644
--- a/com/android/server/am/ProcessList.java
+++ b/com/android/server/am/ProcessList.java
@@ -24,6 +24,7 @@ import java.io.OutputStream;
import java.nio.ByteBuffer;
import android.app.ActivityManager;
+import android.app.ActivityManagerProto;
import android.os.Build;
import android.os.SystemClock;
import com.android.internal.util.MemInfoReader;
@@ -358,12 +359,12 @@ public final class ProcessList {
case ActivityManager.PROCESS_STATE_TOP:
procState = "TOP ";
break;
- case ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE:
- procState = "BFGS";
- break;
case ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE:
procState = "FGS ";
break;
+ case ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE:
+ procState = "BFGS";
+ break;
case ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND:
procState = "IMPF";
break;
@@ -416,6 +417,53 @@ public final class ProcessList {
return procState;
}
+ public static int makeProcStateProtoEnum(int curProcState) {
+ switch (curProcState) {
+ case ActivityManager.PROCESS_STATE_PERSISTENT:
+ return ActivityManagerProto.PROCESS_STATE_PERSISTENT;
+ case ActivityManager.PROCESS_STATE_PERSISTENT_UI:
+ return ActivityManagerProto.PROCESS_STATE_PERSISTENT_UI;
+ case ActivityManager.PROCESS_STATE_TOP:
+ return ActivityManagerProto.PROCESS_STATE_TOP;
+ case ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE:
+ return ActivityManagerProto.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+ case ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE:
+ return ActivityManagerProto.PROCESS_STATE_FOREGROUND_SERVICE;
+ case ActivityManager.PROCESS_STATE_TOP_SLEEPING:
+ return ActivityManagerProto.PROCESS_STATE_TOP_SLEEPING;
+ case ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND:
+ return ActivityManagerProto.PROCESS_STATE_IMPORTANT_FOREGROUND;
+ case ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND:
+ return ActivityManagerProto.PROCESS_STATE_IMPORTANT_BACKGROUND;
+ case ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND:
+ return ActivityManagerProto.PROCESS_STATE_TRANSIENT_BACKGROUND;
+ case ActivityManager.PROCESS_STATE_BACKUP:
+ return ActivityManagerProto.PROCESS_STATE_BACKUP;
+ case ActivityManager.PROCESS_STATE_HEAVY_WEIGHT:
+ return ActivityManagerProto.PROCESS_STATE_HEAVY_WEIGHT;
+ case ActivityManager.PROCESS_STATE_SERVICE:
+ return ActivityManagerProto.PROCESS_STATE_SERVICE;
+ case ActivityManager.PROCESS_STATE_RECEIVER:
+ return ActivityManagerProto.PROCESS_STATE_RECEIVER;
+ case ActivityManager.PROCESS_STATE_HOME:
+ return ActivityManagerProto.PROCESS_STATE_HOME;
+ case ActivityManager.PROCESS_STATE_LAST_ACTIVITY:
+ return ActivityManagerProto.PROCESS_STATE_LAST_ACTIVITY;
+ case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY:
+ return ActivityManagerProto.PROCESS_STATE_CACHED_ACTIVITY;
+ case ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT:
+ return ActivityManagerProto.PROCESS_STATE_CACHED_ACTIVITY_CLIENT;
+ case ActivityManager.PROCESS_STATE_CACHED_RECENT:
+ return ActivityManagerProto.PROCESS_STATE_CACHED_RECENT;
+ case ActivityManager.PROCESS_STATE_CACHED_EMPTY:
+ return ActivityManagerProto.PROCESS_STATE_CACHED_EMPTY;
+ case ActivityManager.PROCESS_STATE_NONEXISTENT:
+ return ActivityManagerProto.PROCESS_STATE_NONEXISTENT;
+ default:
+ return -1;
+ }
+ }
+
public static void appendRamKb(StringBuilder sb, long ramKb) {
for (int j=0, fact=10; j<6; j++, fact*=10) {
if (ramKb < fact) {
@@ -432,13 +480,13 @@ public final class ProcessList {
public static final int PSS_MIN_TIME_FROM_STATE_CHANGE = 15*1000;
// The maximum amount of time we want to go between PSS collections.
- public static final int PSS_MAX_INTERVAL = 30*60*1000;
+ public static final int PSS_MAX_INTERVAL = 40*60*1000;
// The minimum amount of time between successive PSS requests for *all* processes.
- public static final int PSS_ALL_INTERVAL = 10*60*1000;
+ public static final int PSS_ALL_INTERVAL = 20*60*1000;
- // The minimum amount of time between successive PSS requests for a process.
- private static final int PSS_SHORT_INTERVAL = 2*60*1000;
+ // The amount of time until PSS when a persistent process first appears.
+ private static final int PSS_FIRST_PERSISTENT_INTERVAL = 30*1000;
// The amount of time until PSS when a process first becomes top.
private static final int PSS_FIRST_TOP_INTERVAL = 10*1000;
@@ -449,6 +497,9 @@ public final class ProcessList {
// The amount of time until PSS when a process first becomes cached.
private static final int PSS_FIRST_CACHED_INTERVAL = 30*1000;
+ // The amount of time until PSS when the top process stays in the same state.
+ private static final int PSS_SAME_TOP_INTERVAL = 5*60*1000;
+
// The amount of time until PSS when an important process stays in the same state.
private static final int PSS_SAME_IMPORTANT_INTERVAL = 15*60*1000;
@@ -458,6 +509,18 @@ public final class ProcessList {
// The amount of time until PSS when a cached process stays in the same state.
private static final int PSS_SAME_CACHED_INTERVAL = 30*60*1000;
+ // The amount of time until PSS when a persistent process first appears.
+ private static final int PSS_FIRST_ASLEEP_PERSISTENT_INTERVAL = 1*60*1000;
+
+ // The amount of time until PSS when a process first becomes top.
+ private static final int PSS_FIRST_ASLEEP_TOP_INTERVAL = 20*1000;
+
+ // The amount of time until PSS when a process first goes into the background.
+ private static final int PSS_FIRST_ASLEEP_BACKGROUND_INTERVAL = 30*1000;
+
+ // The amount of time until PSS when a process first becomes cached.
+ private static final int PSS_FIRST_ASLEEP_CACHED_INTERVAL = 1*60*1000;
+
// The minimum time interval after a state change it is safe to collect PSS.
public static final int PSS_TEST_MIN_TIME_FROM_STATE_CHANGE = 10*1000;
@@ -483,8 +546,8 @@ public final class ProcessList {
PROC_MEM_PERSISTENT, // ActivityManager.PROCESS_STATE_PERSISTENT
PROC_MEM_PERSISTENT, // ActivityManager.PROCESS_STATE_PERSISTENT_UI
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_IMPORTANT, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
PROC_MEM_IMPORTANT, // ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND
@@ -502,11 +565,11 @@ public final class ProcessList {
};
private static final long[] sFirstAwakePssTimes = new long[] {
- PSS_SHORT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT
- PSS_SHORT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT_UI
+ PSS_FIRST_PERSISTENT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT
+ PSS_FIRST_PERSISTENT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT_UI
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_BOUND_FOREGROUND_SERVICE
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
@@ -526,9 +589,54 @@ public final class ProcessList {
private static final long[] sSameAwakePssTimes = new long[] {
PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT
PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT_UI
- PSS_SHORT_INTERVAL, // ActivityManager.PROCESS_STATE_TOP
+ PSS_SAME_TOP_INTERVAL, // ActivityManager.PROCESS_STATE_TOP
+ PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
+ 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_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
+ PSS_SAME_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT
+ PSS_SAME_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_CACHED_RECENT
+ PSS_SAME_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_CACHED_EMPTY
+ };
+
+ private static final long[] sFirstAsleepPssTimes = new long[] {
+ PSS_FIRST_ASLEEP_PERSISTENT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT
+ PSS_FIRST_ASLEEP_PERSISTENT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT_UI
+ PSS_FIRST_ASLEEP_TOP_INTERVAL, // ActivityManager.PROCESS_STATE_TOP
+ PSS_FIRST_ASLEEP_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
+ PSS_FIRST_ASLEEP_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
+ PSS_FIRST_ASLEEP_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_FOREGROUND
+ PSS_FIRST_ASLEEP_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND
+ PSS_FIRST_ASLEEP_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_TRANSIENT_BACKGROUND
+ PSS_FIRST_ASLEEP_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_BACKUP
+ PSS_FIRST_ASLEEP_BACKGROUND_INTERVAL, // ActivityManager.PROCESS_STATE_SERVICE
+ PSS_FIRST_ASLEEP_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_RECEIVER
+ PSS_FIRST_ASLEEP_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_TOP_SLEEPING
+ PSS_FIRST_ASLEEP_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_HEAVY_WEIGHT
+ PSS_FIRST_ASLEEP_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_HOME
+ PSS_FIRST_ASLEEP_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_LAST_ACTIVITY
+ PSS_FIRST_ASLEEP_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY
+ PSS_FIRST_ASLEEP_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_CACHED_ACTIVITY_CLIENT
+ PSS_FIRST_ASLEEP_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_CACHED_RECENT
+ PSS_FIRST_ASLEEP_CACHED_INTERVAL, // ActivityManager.PROCESS_STATE_CACHED_EMPTY
+ };
+
+ private static final long[] sSameAsleepPssTimes = new long[] {
+ PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT
+ PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_PERSISTENT_UI
+ PSS_SAME_TOP_INTERVAL, // ActivityManager.PROCESS_STATE_TOP
PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
+ PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
+ PSS_SAME_IMPORTANT_INTERVAL, // ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
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
@@ -549,8 +657,8 @@ public final class ProcessList {
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_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_BOUND_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
@@ -571,8 +679,8 @@ public final class ProcessList {
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_BOUND_FOREGROUND_SERVICE
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
@@ -604,8 +712,8 @@ public final class ProcessList {
? sTestFirstPssTimes
: sTestSamePssTimes)
: (first
- ? sFirstAwakePssTimes
- : sSameAwakePssTimes);
+ ? (sleeping ? sFirstAsleepPssTimes : sFirstAwakePssTimes)
+ : (sleeping ? sSameAsleepPssTimes : sSameAwakePssTimes));
return now + table[procState];
}
diff --git a/com/android/server/am/ProcessRecord.java b/com/android/server/am/ProcessRecord.java
index a1e59472..03e140de 100644
--- a/com/android/server/am/ProcessRecord.java
+++ b/com/android/server/am/ProcessRecord.java
@@ -679,6 +679,7 @@ final class ProcessRecord {
proto.write(ProcessRecordProto.ISOLATED_APP_ID, UserHandle.getAppId(uid));
}
}
+ proto.write(ProcessRecordProto.PERSISTENT, persistent);
proto.end(token);
}
diff --git a/com/android/server/am/RecentsAnimation.java b/com/android/server/am/RecentsAnimation.java
new file mode 100644
index 00000000..fe576fda
--- /dev/null
+++ b/com/android/server/am/RecentsAnimation.java
@@ -0,0 +1,159 @@
+/*
+ * 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 com.android.server.am;
+
+import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION;
+import static android.view.WindowManager.TRANSIT_NONE;
+import static com.android.server.am.ActivityStackSupervisor.PRESERVE_WINDOWS;
+
+import android.app.ActivityOptions;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.os.Handler;
+import android.view.IRecentsAnimationRunner;
+import com.android.server.wm.RecentsAnimationController.RecentsAnimationCallbacks;
+import com.android.server.wm.WindowManagerService;
+
+/**
+ * Manages the recents animation, including the reordering of the stacks for the transition and
+ * cleanup. See {@link com.android.server.wm.RecentsAnimationController}.
+ */
+class RecentsAnimation implements RecentsAnimationCallbacks {
+ private static final String TAG = RecentsAnimation.class.getSimpleName();
+
+ private static final int RECENTS_ANIMATION_TIMEOUT = 10 * 1000;
+
+ private final ActivityManagerService mService;
+ private final ActivityStackSupervisor mStackSupervisor;
+ private final ActivityStartController mActivityStartController;
+ private final WindowManagerService mWindowManager;
+ private final UserController mUserController;
+ private final Handler mHandler;
+
+ private final Runnable mCancelAnimationRunnable;
+
+ // The stack to restore the home stack behind when the animation is finished
+ private ActivityStack mRestoreHomeBehindStack;
+
+ RecentsAnimation(ActivityManagerService am, ActivityStackSupervisor stackSupervisor,
+ ActivityStartController activityStartController, WindowManagerService wm,
+ UserController userController) {
+ mService = am;
+ mStackSupervisor = stackSupervisor;
+ mActivityStartController = activityStartController;
+ mHandler = new Handler(mStackSupervisor.mLooper);
+ mWindowManager = wm;
+ mUserController = userController;
+ mCancelAnimationRunnable = () -> {
+ // The caller has not finished the animation in a predefined amount of time, so
+ // force-cancel the animation
+ mWindowManager.cancelRecentsAnimation();
+ };
+ }
+
+ void startRecentsActivity(Intent intent, IRecentsAnimationRunner recentsAnimationRunner,
+ ComponentName recentsComponent, int recentsUid) {
+
+ // Cancel the previous recents animation if necessary
+ mWindowManager.cancelRecentsAnimation();
+
+ final boolean hasExistingHomeActivity = mStackSupervisor.getHomeActivity() != null;
+ if (!hasExistingHomeActivity) {
+ // No home activity
+ final ActivityOptions opts = ActivityOptions.makeBasic();
+ opts.setLaunchActivityType(ACTIVITY_TYPE_HOME);
+ opts.setAvoidMoveToFront();
+ intent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NO_ANIMATION);
+
+ mActivityStartController.obtainStarter(intent, "startRecentsActivity_noHomeActivity")
+ .setCallingUid(recentsUid)
+ .setCallingPackage(recentsComponent.getPackageName())
+ .setActivityOptions(SafeActivityOptions.fromBundle(opts.toBundle()))
+ .setMayWait(mUserController.getCurrentUserId())
+ .execute();
+ mWindowManager.prepareAppTransition(TRANSIT_NONE, false);
+
+ // TODO: Maybe wait for app to draw in this particular case?
+ }
+
+ final ActivityRecord homeActivity = mStackSupervisor.getHomeActivity();
+ final ActivityDisplay display = homeActivity.getDisplay();
+
+ // Save the initial position of the home activity stack to be restored to after the
+ // animation completes
+ mRestoreHomeBehindStack = hasExistingHomeActivity
+ ? display.getStackAboveHome()
+ : null;
+
+ // Move the home activity into place for the animation
+ display.moveHomeStackBehindBottomMostVisibleStack();
+
+ // Mark the home activity as launch-behind to bump its visibility for the
+ // duration of the gesture that is driven by the recents component
+ homeActivity.mLaunchTaskBehind = true;
+
+ // Fetch all the surface controls and pass them to the client to get the animation
+ // started
+ mWindowManager.initializeRecentsAnimation(recentsAnimationRunner, this, display.mDisplayId);
+
+ // If we updated the launch-behind state, update the visibility of the activities after we
+ // fetch the visible tasks to be controlled by the animation
+ mStackSupervisor.ensureActivitiesVisibleLocked(null, 0, PRESERVE_WINDOWS);
+
+ // Post a timeout for the animation
+ mHandler.postDelayed(mCancelAnimationRunnable, RECENTS_ANIMATION_TIMEOUT);
+ }
+
+ @Override
+ public void onAnimationFinished(boolean moveHomeToTop) {
+ mHandler.removeCallbacks(mCancelAnimationRunnable);
+ synchronized (mService) {
+ if (mWindowManager.getRecentsAnimationController() == null) return;
+
+ mWindowManager.inSurfaceTransaction(() -> {
+ mWindowManager.cleanupRecentsAnimation();
+
+ // Move the home stack to the front
+ final ActivityRecord homeActivity = mStackSupervisor.getHomeActivity();
+ if (homeActivity == null) {
+ return;
+ }
+
+ // Restore the launched-behind state
+ homeActivity.mLaunchTaskBehind = false;
+
+ if (moveHomeToTop) {
+ // Bring the home stack to the front
+ final ActivityStack homeStack = homeActivity.getStack();
+ homeStack.mNoAnimActivities.add(homeActivity);
+ homeStack.moveToFront("RecentsAnimation.onAnimationFinished()");
+ } else {
+ // Restore the home stack to its previous position
+ final ActivityDisplay display = homeActivity.getDisplay();
+ display.moveHomeStackBehindStack(mRestoreHomeBehindStack);
+ }
+
+ mWindowManager.prepareAppTransition(TRANSIT_NONE, false);
+ mStackSupervisor.ensureActivitiesVisibleLocked(null, 0, false);
+ mStackSupervisor.resumeFocusedStackTopActivityLocked();
+ });
+ }
+ }
+}
diff --git a/com/android/server/am/SafeActivityOptions.java b/com/android/server/am/SafeActivityOptions.java
new file mode 100644
index 00000000..d08111ec
--- /dev/null
+++ b/com/android/server/am/SafeActivityOptions.java
@@ -0,0 +1,232 @@
+/*
+ * 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 com.android.server.am;
+
+import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
+import static android.Manifest.permission.START_TASKS_FROM_RECENTS;
+import static android.content.pm.PackageManager.PERMISSION_DENIED;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.view.Display.INVALID_DISPLAY;
+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.TaskRecord.INVALID_TASK_ID;
+
+import android.annotation.Nullable;
+import android.app.ActivityOptions;
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Slog;
+import android.view.RemoteAnimationAdapter;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Wraps {@link ActivityOptions}, records binder identity, and checks permission when retrieving
+ * the inner options. Also supports having two set of options: Once from the original caller, and
+ * once from the caller that is overriding it, which happens when sending a {@link PendingIntent}.
+ */
+class SafeActivityOptions {
+
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "SafeActivityOptions" : TAG_AM;
+
+ private final int mOriginalCallingPid;
+ private final int mOriginalCallingUid;
+ private int mRealCallingPid;
+ private int mRealCallingUid;
+ private final @Nullable ActivityOptions mOriginalOptions;
+ private @Nullable ActivityOptions mCallerOptions;
+
+ /**
+ * Constructs a new instance from a bundle and records {@link Binder#getCallingPid}/
+ * {@link Binder#getCallingUid}. Thus, calling identity MUST NOT be cleared when constructing
+ * this object.
+ *
+ * @param bOptions The {@link ActivityOptions} as {@link Bundle}.
+ */
+ static SafeActivityOptions fromBundle(Bundle bOptions) {
+ return bOptions != null
+ ? new SafeActivityOptions(ActivityOptions.fromBundle(bOptions))
+ : null;
+ }
+
+ /**
+ * Constructs a new instance and records {@link Binder#getCallingPid}/
+ * {@link Binder#getCallingUid}. Thus, calling identity MUST NOT be cleared when constructing
+ * this object.
+ *
+ * @param options The options to wrap.
+ */
+ SafeActivityOptions(@Nullable ActivityOptions options) {
+ mOriginalCallingPid = Binder.getCallingPid();
+ mOriginalCallingUid = Binder.getCallingUid();
+ mOriginalOptions = options;
+ }
+
+ /**
+ * Overrides options with options from a caller and records {@link Binder#getCallingPid}/
+ * {@link Binder#getCallingUid}. Thus, calling identity MUST NOT be cleared when calling this
+ * method.
+ */
+ void setCallerOptions(@Nullable ActivityOptions options) {
+ mRealCallingPid = Binder.getCallingPid();
+ mRealCallingUid = Binder.getCallingUid();
+ mCallerOptions = options;
+ }
+
+ /**
+ * Performs permission check and retrieves the options.
+ *
+ * @param r The record of the being started activity.
+ */
+ ActivityOptions getOptions(ActivityRecord r) throws SecurityException {
+ return getOptions(r.intent, r.info, r.app, r.mStackSupervisor);
+ }
+
+ /**
+ * Performs permission check and retrieves the options when options are not being used to launch
+ * a specific activity (i.e. a task is moved to front).
+ */
+ ActivityOptions getOptions(ActivityStackSupervisor supervisor) throws SecurityException {
+ return getOptions(null, null, null, supervisor);
+ }
+
+ /**
+ * Performs permission check and retrieves the options.
+ *
+ * @param intent The intent that is being launched.
+ * @param aInfo The info of the activity being launched.
+ * @param callerApp The record of the caller.
+ */
+ ActivityOptions getOptions(@Nullable Intent intent, @Nullable ActivityInfo aInfo,
+ @Nullable ProcessRecord callerApp,
+ ActivityStackSupervisor supervisor) throws SecurityException {
+ if (mOriginalOptions != null) {
+ checkPermissions(intent, aInfo, callerApp, supervisor, mOriginalOptions,
+ mOriginalCallingPid, mOriginalCallingUid);
+ }
+ if (mCallerOptions != null) {
+ checkPermissions(intent, aInfo, callerApp, supervisor, mCallerOptions,
+ mRealCallingPid, mRealCallingUid);
+ }
+ return mergeActivityOptions(mOriginalOptions, mCallerOptions);
+ }
+
+ /**
+ * @see ActivityOptions#popAppVerificationBundle
+ */
+ Bundle popAppVerificationBundle() {
+ return mOriginalOptions != null ? mOriginalOptions.popAppVerificationBundle() : null;
+ }
+
+ private void abort() {
+ if (mOriginalOptions != null) {
+ ActivityOptions.abort(mOriginalOptions);
+ }
+ if (mCallerOptions != null) {
+ ActivityOptions.abort(mCallerOptions);
+ }
+ }
+
+ static void abort(@Nullable SafeActivityOptions options) {
+ if (options != null) {
+ options.abort();
+ }
+ }
+
+ /**
+ * Merges two activity options into one, with {@code options2} taking precedence in case of a
+ * conflict.
+ */
+ @VisibleForTesting
+ @Nullable ActivityOptions mergeActivityOptions(@Nullable ActivityOptions options1,
+ @Nullable ActivityOptions options2) {
+ if (options1 == null) {
+ return options2;
+ }
+ if (options2 == null) {
+ return options1;
+ }
+ final Bundle b1 = options1.toBundle();
+ final Bundle b2 = options2.toBundle();
+ b1.putAll(b2);
+ return ActivityOptions.fromBundle(b1);
+ }
+
+ private void checkPermissions(@Nullable Intent intent, @Nullable ActivityInfo aInfo,
+ @Nullable ProcessRecord callerApp, ActivityStackSupervisor supervisor,
+ ActivityOptions options, int callingPid, int callingUid) {
+ // If a launch task id is specified, then ensure that the caller is the recents
+ // component or has the START_TASKS_FROM_RECENTS permission
+ if (options.getLaunchTaskId() != INVALID_TASK_ID
+ && !supervisor.mRecentTasks.isCallerRecents(callingUid)) {
+ final int startInTaskPerm = supervisor.mService.checkPermission(
+ START_TASKS_FROM_RECENTS, callingPid, callingUid);
+ if (startInTaskPerm == PERMISSION_DENIED) {
+ final String msg = "Permission Denial: starting " + getIntentString(intent)
+ + " from " + callerApp + " (pid=" + callingPid
+ + ", uid=" + callingUid + ") with launchTaskId="
+ + options.getLaunchTaskId();
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+ }
+ // Check if someone tries to launch an activity on a private display with a different
+ // owner.
+ final int launchDisplayId = options.getLaunchDisplayId();
+ if (aInfo != null && launchDisplayId != INVALID_DISPLAY
+ && !supervisor.isCallerAllowedToLaunchOnDisplay(callingPid, callingUid,
+ launchDisplayId, aInfo)) {
+ final String msg = "Permission Denial: starting " + getIntentString(intent)
+ + " from " + callerApp + " (pid=" + callingPid
+ + ", uid=" + callingUid + ") with launchDisplayId="
+ + launchDisplayId;
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+ // Check if someone tries to launch an unwhitelisted activity into LockTask mode.
+ final boolean lockTaskMode = options.getLockTaskMode();
+ if (aInfo != null && lockTaskMode
+ && !supervisor.mService.mLockTaskController.isPackageWhitelisted(
+ UserHandle.getUserId(callingUid), aInfo.packageName)) {
+ final String msg = "Permission Denial: starting " + getIntentString(intent)
+ + " from " + callerApp + " (pid=" + callingPid
+ + ", uid=" + callingUid + ") with lockTaskMode=true";
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+
+ // Check permission for remote animations
+ final RemoteAnimationAdapter adapter = options.getRemoteAnimationAdapter();
+ if (adapter != null && supervisor.mService.checkPermission(
+ CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS, callingPid, callingUid)
+ != PERMISSION_GRANTED) {
+ final String msg = "Permission Denial: starting " + getIntentString(intent)
+ + " from " + callerApp + " (pid=" + callingPid
+ + ", uid=" + callingUid + ") with remoteAnimationAdapter";
+ Slog.w(TAG, msg);
+ throw new SecurityException(msg);
+ }
+ }
+
+ private String getIntentString(Intent intent) {
+ return intent != null ? intent.toString() : "(no intent)";
+ }
+}
diff --git a/com/android/server/am/LaunchingTaskPositioner.java b/com/android/server/am/TaskLaunchParamsModifier.java
index d89568e2..92f1cc34 100644
--- a/com/android/server/am/LaunchingTaskPositioner.java
+++ b/com/android/server/am/TaskLaunchParamsModifier.java
@@ -21,26 +21,27 @@ import static com.android.server.am.ActivityManagerDebugConfig.TAG_WITH_CLASS_NA
import android.app.ActivityOptions;
import android.content.pm.ActivityInfo;
-import android.graphics.Point;
import android.graphics.Rect;
import android.util.Slog;
import android.view.Gravity;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.am.LaunchParamsController.LaunchParams;
+import com.android.server.am.LaunchParamsController.LaunchParamsModifier;
import java.util.ArrayList;
/**
* Determines where a launching task should be positioned and sized on the display.
*
- * The positioner is fairly simple. For the new task it tries default position based on the gravity
+ * The modifier is fairly simple. For the new task it tries default position based on the gravity
* and compares corners of the task with corners of existing tasks. If some two pairs of corners are
* sufficiently close enough, it shifts the bounds of the new task and tries again. When it exhausts
* all possible shifts, it gives up and puts the task in the original position.
*
* Note that the only gravities of concern are the corners and the center.
*/
-class LaunchingTaskPositioner implements LaunchingBoundsController.LaunchingBoundsPositioner {
- private static final String TAG = TAG_WITH_CLASS_NAME ? "LaunchingTaskPositioner" : TAG_AM;
+class TaskLaunchParamsModifier implements LaunchParamsModifier {
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "TaskLaunchParamsModifier" : TAG_AM;
// Determines how close window frames/corners have to be to call them colliding.
private static final int BOUNDS_CONFLICT_MIN_DISTANCE = 4;
@@ -70,17 +71,15 @@ class LaunchingTaskPositioner implements LaunchingBoundsController.LaunchingBoun
private final Rect mTmpProposal = new Rect();
private final Rect mTmpOriginal = new Rect();
- private final Point mDisplaySize = new Point();
-
/**
* Tries to set task's bound in a way that it won't collide with any other task. By colliding
* we mean that two tasks have left-top corner very close to each other, so one might get
* obfuscated by the other one.
*/
@Override
- public int onCalculateBounds(TaskRecord task, ActivityInfo.WindowLayout layout,
- ActivityRecord activity, ActivityRecord source,
- ActivityOptions options, Rect current, Rect result) {
+ public int onCalculate(TaskRecord task, ActivityInfo.WindowLayout layout,
+ ActivityRecord activity, ActivityRecord source, ActivityOptions options,
+ LaunchParams currentParams, LaunchParams outParams) {
// We can only apply positioning if we're in a freeform stack.
if (task == null || task.getStack() == null || !task.inFreeformWindowingMode()) {
return RESULT_SKIP;
@@ -90,9 +89,11 @@ class LaunchingTaskPositioner implements LaunchingBoundsController.LaunchingBoun
mAvailableRect.set(task.getParent().getBounds());
+ final Rect resultBounds = outParams.mBounds;
+
if (layout == null) {
positionCenter(tasks, mAvailableRect, getFreeformWidth(mAvailableRect),
- getFreeformHeight(mAvailableRect), result);
+ getFreeformHeight(mAvailableRect), resultBounds);
return RESULT_CONTINUE;
}
@@ -102,22 +103,22 @@ class LaunchingTaskPositioner implements LaunchingBoundsController.LaunchingBoun
int horizontalGravity = layout.gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
if (verticalGravity == Gravity.TOP) {
if (horizontalGravity == Gravity.RIGHT) {
- positionTopRight(tasks, mAvailableRect, width, height, result);
+ positionTopRight(tasks, mAvailableRect, width, height, resultBounds);
} else {
- positionTopLeft(tasks, mAvailableRect, width, height, result);
+ positionTopLeft(tasks, mAvailableRect, width, height, resultBounds);
}
} else if (verticalGravity == Gravity.BOTTOM) {
if (horizontalGravity == Gravity.RIGHT) {
- positionBottomRight(tasks, mAvailableRect, width, height, result);
+ positionBottomRight(tasks, mAvailableRect, width, height, resultBounds);
} else {
- positionBottomLeft(tasks, mAvailableRect, width, height, result);
+ positionBottomLeft(tasks, mAvailableRect, width, height, resultBounds);
}
} else {
// Some fancy gravity setting that we don't support yet. We just put the activity in the
// center.
Slog.w(TAG, "Received unsupported gravity: " + layout.gravity
+ ", positioning in the center instead.");
- positionCenter(tasks, mAvailableRect, width, height, result);
+ positionCenter(tasks, mAvailableRect, width, height, resultBounds);
}
return RESULT_CONTINUE;
diff --git a/com/android/server/am/TaskRecord.java b/com/android/server/am/TaskRecord.java
index 4aef95d2..809f19f6 100644
--- a/com/android/server/am/TaskRecord.java
+++ b/com/android/server/am/TaskRecord.java
@@ -714,7 +714,7 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
} else if (toStackWindowingMode == WINDOWING_MODE_FREEFORM) {
Rect bounds = getLaunchBounds();
if (bounds == null) {
- mService.mStackSupervisor.getLaunchingBoundsController().layoutTask(this, null);
+ mService.mStackSupervisor.getLaunchParamsController().layoutTask(this, null);
bounds = configBounds;
}
kept = resize(bounds, RESIZE_MODE_FORCED, !mightReplaceWindow, deferResume);
@@ -749,12 +749,7 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
supervisor.handleNonResizableTaskIfNeeded(this, preferredStack.getWindowingMode(),
DEFAULT_DISPLAY, toStack);
- boolean successful = (preferredStack == toStack);
- if (successful && toStack.getWindowingMode() == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
- // If task moved to docked stack - show recents if needed.
- mService.mWindowManager.showRecentApps(false /* fromHome */);
- }
- return successful;
+ return (preferredStack == toStack);
}
/**
@@ -1838,7 +1833,7 @@ class TaskRecord extends ConfigurationContainer implements TaskWindowContainerLi
if (mLastNonFullscreenBounds != null) {
updateOverrideConfiguration(mLastNonFullscreenBounds);
} else {
- mService.mStackSupervisor.getLaunchingBoundsController().layoutTask(this, null);
+ mService.mStackSupervisor.getLaunchParamsController().layoutTask(this, null);
}
} else {
updateOverrideConfiguration(inStack.getOverrideBounds());
diff --git a/com/android/server/am/UidRecord.java b/com/android/server/am/UidRecord.java
index 8efcb4f2..3886e5a9 100644
--- a/com/android/server/am/UidRecord.java
+++ b/com/android/server/am/UidRecord.java
@@ -18,13 +18,17 @@ package com.android.server.am;
import android.Manifest;
import android.app.ActivityManager;
+import android.app.ActivityManagerProto;
import android.content.pm.PackageManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.am.proto.UidRecordProto;
/**
* Overall information about a uid that has actively running processes.
@@ -86,6 +90,22 @@ public final class UidRecord {
static final int CHANGE_CACHED = 1<<3;
static final int CHANGE_UNCACHED = 1<<4;
+ // Keep the enum lists in sync
+ private static int[] ORIG_ENUMS = new int[] {
+ CHANGE_GONE,
+ CHANGE_IDLE,
+ CHANGE_ACTIVE,
+ CHANGE_CACHED,
+ CHANGE_UNCACHED,
+ };
+ private static int[] PROTO_ENUMS = new int[] {
+ UidRecordProto.CHANGE_GONE,
+ UidRecordProto.CHANGE_IDLE,
+ UidRecordProto.CHANGE_ACTIVE,
+ UidRecordProto.CHANGE_CACHED,
+ UidRecordProto.CHANGE_UNCACHED,
+ };
+
static final class ChangeItem {
UidRecord uidRecord;
int uid;
@@ -125,6 +145,34 @@ public final class UidRecord {
}
}
+
+ void writeToProto(ProtoOutputStream proto, long fieldId) {
+ long token = proto.start(fieldId);
+ proto.write(UidRecordProto.HEX_HASH, Integer.toHexString(System.identityHashCode(this)));
+ proto.write(UidRecordProto.UID, uid);
+ proto.write(UidRecordProto.CURRENT, ProcessList.makeProcStateProtoEnum(curProcState));
+ proto.write(UidRecordProto.EPHEMERAL, ephemeral);
+ proto.write(UidRecordProto.FG_SERVICES, foregroundServices);
+ proto.write(UidRecordProto.WHILELIST, curWhitelist);
+ ProtoUtils.toDuration(proto, UidRecordProto.LAST_BACKGROUND_TIME,
+ lastBackgroundTime, SystemClock.elapsedRealtime());
+ proto.write(UidRecordProto.IDLE, idle);
+ if (lastReportedChange != 0) {
+ ProtoUtils.writeBitWiseFlagsToProtoEnum(proto, UidRecordProto.LAST_REPORTED_CHANGES,
+ lastReportedChange, ORIG_ENUMS, PROTO_ENUMS);
+ }
+ proto.write(UidRecordProto.NUM_PROCS, numProcs);
+
+ long seqToken = proto.start(UidRecordProto.NETWORK_STATE_UPDATE);
+ proto.write(UidRecordProto.ProcStateSequence.CURURENT, curProcStateSeq);
+ proto.write(UidRecordProto.ProcStateSequence.LAST_NETWORK_UPDATED,
+ lastNetworkUpdatedProcStateSeq);
+ proto.write(UidRecordProto.ProcStateSequence.LAST_DISPATCHED, lastDispatchedProcStateSeq);
+ proto.end(seqToken);
+
+ proto.end(token);
+ }
+
public String toString() {
StringBuilder sb = new StringBuilder(128);
sb.append("UidRecord{");
diff --git a/com/android/server/am/UserController.java b/com/android/server/am/UserController.java
index 34621e03..7b0c714b 100644
--- a/com/android/server/am/UserController.java
+++ b/com/android/server/am/UserController.java
@@ -83,6 +83,7 @@ import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.TimingsTraceLog;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
@@ -94,6 +95,7 @@ import com.android.internal.widget.LockPatternUtils;
import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.SystemServiceManager;
+import com.android.server.am.proto.UserControllerProto;
import com.android.server.pm.UserManagerService;
import com.android.server.wm.WindowManagerService;
@@ -101,6 +103,7 @@ import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -216,6 +219,18 @@ class UserController implements Handler.Callback {
private volatile ArraySet<String> mCurWaitingUserSwitchCallbacks;
/**
+ * Messages for for switching from {@link android.os.UserHandle#SYSTEM}.
+ */
+ @GuardedBy("mLock")
+ private String mSwitchingFromSystemUserMessage;
+
+ /**
+ * Messages for for switching to {@link android.os.UserHandle#SYSTEM}.
+ */
+ @GuardedBy("mLock")
+ private String mSwitchingToSystemUserMessage;
+
+ /**
* Callbacks that are still active after {@link #USER_SWITCH_TIMEOUT_MS}
*/
@GuardedBy("mLock")
@@ -249,39 +264,51 @@ class UserController implements Handler.Callback {
}
}
- void stopRunningUsersLU(int maxRunningUsers) {
- int currentlyRunning = mUserLru.size();
- int i = 0;
- while (currentlyRunning > maxRunningUsers && i < mUserLru.size()) {
- Integer oldUserId = mUserLru.get(i);
- UserState oldUss = mStartedUsers.get(oldUserId);
- if (oldUss == null) {
+ List<Integer> getRunningUsersLU() {
+ ArrayList<Integer> runningUsers = new ArrayList<>();
+ for (Integer userId : mUserLru) {
+ UserState uss = mStartedUsers.get(userId);
+ if (uss == null) {
// Shouldn't happen, but be sane if it does.
- mUserLru.remove(i);
- currentlyRunning--;
continue;
}
- if (oldUss.state == UserState.STATE_STOPPING
- || oldUss.state == UserState.STATE_SHUTDOWN) {
+ if (uss.state == UserState.STATE_STOPPING
+ || uss.state == UserState.STATE_SHUTDOWN) {
// This user is already stopping, doesn't count.
- currentlyRunning--;
- i++;
continue;
}
- if (oldUserId == UserHandle.USER_SYSTEM || oldUserId == mCurrentUserId) {
- // Owner/System user and current user can't be stopped. We count it as running
- // when it is not a pure system user.
- if (UserInfo.isSystemOnly(oldUserId)) {
- currentlyRunning--;
+ if (userId == UserHandle.USER_SYSTEM) {
+ // We only count system user as running when it is not a pure system user.
+ if (UserInfo.isSystemOnly(userId)) {
+ continue;
}
- i++;
+ }
+ runningUsers.add(userId);
+ }
+ return runningUsers;
+ }
+
+ void stopRunningUsersLU(int maxRunningUsers) {
+ List<Integer> currentlyRunning = getRunningUsersLU();
+ Iterator<Integer> iterator = currentlyRunning.iterator();
+ while (currentlyRunning.size() > maxRunningUsers && iterator.hasNext()) {
+ Integer userId = iterator.next();
+ if (userId == UserHandle.USER_SYSTEM || userId == mCurrentUserId) {
+ // Owner/System user and current user can't be stopped
continue;
}
- // This is a user to be stopped.
- if (stopUsersLU(oldUserId, false, null) == USER_OP_SUCCESS) {
- currentlyRunning--;
+ if (stopUsersLU(userId, false, null) == USER_OP_SUCCESS) {
+ iterator.remove();
}
- i++;
+ }
+ }
+
+ /**
+ * Returns if more users can be started without stopping currently running users.
+ */
+ boolean canStartMoreUsers() {
+ synchronized (mLock) {
+ return getRunningUsersLU().size() < mMaxRunningUsers;
}
}
@@ -768,34 +795,25 @@ class UserController implements Handler.Callback {
/**
* Stops the guest or ephemeral user if it has gone to the background.
*/
- private void stopGuestOrEphemeralUserIfBackground() {
- IntArray userIds = new IntArray();
- synchronized (mLock) {
- final int num = mUserLru.size();
- for (int i = 0; i < num; i++) {
- Integer oldUserId = mUserLru.get(i);
- UserState oldUss = mStartedUsers.get(oldUserId);
- if (oldUserId == UserHandle.USER_SYSTEM || oldUserId == mCurrentUserId
- || oldUss.state == UserState.STATE_STOPPING
- || oldUss.state == UserState.STATE_SHUTDOWN) {
- continue;
- }
- userIds.add(oldUserId);
+ private void stopGuestOrEphemeralUserIfBackground(int oldUserId) {
+ if (DEBUG_MU) Slog.i(TAG, "Stop guest or ephemeral user if background: " + oldUserId);
+ synchronized(mLock) {
+ UserState oldUss = mStartedUsers.get(oldUserId);
+ if (oldUserId == UserHandle.USER_SYSTEM || oldUserId == mCurrentUserId || oldUss == null
+ || oldUss.state == UserState.STATE_STOPPING
+ || oldUss.state == UserState.STATE_SHUTDOWN) {
+ return;
}
}
- final int userIdsSize = userIds.size();
- for (int i = 0; i < userIdsSize; i++) {
- int oldUserId = userIds.get(i);
- UserInfo userInfo = getUserInfo(oldUserId);
- if (userInfo.isEphemeral()) {
- LocalServices.getService(UserManagerInternal.class).onEphemeralUserStop(oldUserId);
- }
- if (userInfo.isGuest() || userInfo.isEphemeral()) {
- // This is a user to be stopped.
- synchronized (mLock) {
- stopUsersLU(oldUserId, true, null);
- }
- break;
+
+ UserInfo userInfo = getUserInfo(oldUserId);
+ if (userInfo.isEphemeral()) {
+ LocalServices.getService(UserManagerInternal.class).onEphemeralUserStop(oldUserId);
+ }
+ if (userInfo.isGuest() || userInfo.isEphemeral()) {
+ // This is a user to be stopped.
+ synchronized (mLock) {
+ stopUsersLU(oldUserId, true, null);
}
}
}
@@ -1187,7 +1205,8 @@ class UserController implements Handler.Callback {
private void showUserSwitchDialog(Pair<UserInfo, UserInfo> fromToUserPair) {
// The dialog will show and then initiate the user switch by calling startUserInForeground
- mInjector.showUserSwitchingDialog(fromToUserPair.first, fromToUserPair.second);
+ mInjector.showUserSwitchingDialog(fromToUserPair.first, fromToUserPair.second,
+ getSwitchingFromSystemUserMessage(), getSwitchingToSystemUserMessage());
}
private void dispatchForegroundProfileChanged(int userId) {
@@ -1333,7 +1352,7 @@ class UserController implements Handler.Callback {
mHandler.removeMessages(REPORT_USER_SWITCH_COMPLETE_MSG);
mHandler.sendMessage(mHandler.obtainMessage(REPORT_USER_SWITCH_COMPLETE_MSG,
newUserId, 0));
- stopGuestOrEphemeralUserIfBackground();
+ stopGuestOrEphemeralUserIfBackground(oldUserId);
stopBackgroundUsersIfEnforced(oldUserId);
}
@@ -1414,7 +1433,13 @@ class UserController implements Handler.Callback {
if (callingUid != 0 && callingUid != SYSTEM_UID) {
final boolean allow;
- if (mInjector.checkComponentPermission(INTERACT_ACROSS_USERS_FULL, callingPid,
+ if (mInjector.isCallerRecents(callingUid)
+ && callingUserId == getCurrentUserId()
+ && isSameProfileGroup(callingUserId, targetUserId)) {
+ // If the caller is Recents and it is running in the current user, we then allow it
+ // to access its profiles.
+ allow = true;
+ } else if (mInjector.checkComponentPermission(INTERACT_ACROSS_USERS_FULL, callingPid,
callingUid, -1, true) == PackageManager.PERMISSION_GRANTED) {
// If the caller has this permission, they always pass go. And collect $200.
allow = true;
@@ -1762,6 +1787,20 @@ class UserController implements Handler.Callback {
}
}
+ void onUserRemoved(int userId) {
+ synchronized (mLock) {
+ int size = mUserProfileGroupIds.size();
+ for (int i = size - 1; i >= 0; i--) {
+ if (mUserProfileGroupIds.keyAt(i) == userId
+ || mUserProfileGroupIds.valueAt(i) == userId) {
+ mUserProfileGroupIds.removeAt(i);
+
+ }
+ }
+ mCurrentProfileIds = ArrayUtils.removeInt(mCurrentProfileIds, userId);
+ }
+ }
+
/**
* Returns whether the given user requires credential entry at this time. This is used to
* intercept activity launches for work apps when the Work Challenge is present.
@@ -1783,6 +1822,60 @@ class UserController implements Handler.Callback {
return mLockPatternUtils.isLockScreenDisabled(userId);
}
+ void setSwitchingFromSystemUserMessage(String switchingFromSystemUserMessage) {
+ synchronized (mLock) {
+ mSwitchingFromSystemUserMessage = switchingFromSystemUserMessage;
+ }
+ }
+
+ void setSwitchingToSystemUserMessage(String switchingToSystemUserMessage) {
+ synchronized (mLock) {
+ mSwitchingToSystemUserMessage = switchingToSystemUserMessage;
+ }
+ }
+
+ private String getSwitchingFromSystemUserMessage() {
+ synchronized (mLock) {
+ return mSwitchingFromSystemUserMessage;
+ }
+ }
+
+ private String getSwitchingToSystemUserMessage() {
+ synchronized (mLock) {
+ return mSwitchingToSystemUserMessage;
+ }
+ }
+
+ void writeToProto(ProtoOutputStream proto, long fieldId) {
+ synchronized (mLock) {
+ long token = proto.start(fieldId);
+ for (int i = 0; i < mStartedUsers.size(); i++) {
+ UserState uss = mStartedUsers.valueAt(i);
+ final long uToken = proto.start(UserControllerProto.STARTED_USERS);
+ proto.write(UserControllerProto.User.ID, uss.mHandle.getIdentifier());
+ uss.writeToProto(proto, UserControllerProto.User.STATE);
+ proto.end(uToken);
+ }
+ for (int i = 0; i < mStartedUserArray.length; i++) {
+ proto.write(UserControllerProto.STARTED_USER_ARRAY, mStartedUserArray[i]);
+ }
+ for (int i = 0; i < mUserLru.size(); i++) {
+ proto.write(UserControllerProto.USER_LRU, mUserLru.get(i));
+ }
+ if (mUserProfileGroupIds.size() > 0) {
+ for (int i = 0; i < mUserProfileGroupIds.size(); i++) {
+ final long uToken = proto.start(UserControllerProto.USER_PROFILE_GROUP_IDS);
+ proto.write(UserControllerProto.UserProfile.USER,
+ mUserProfileGroupIds.keyAt(i));
+ proto.write(UserControllerProto.UserProfile.PROFILE,
+ mUserProfileGroupIds.valueAt(i));
+ proto.end(uToken);
+ }
+ }
+ proto.end(token);
+ }
+ }
+
void dump(PrintWriter pw, boolean dumpAll) {
synchronized (mLock) {
pw.println(" mStartedUsers:");
@@ -1807,10 +1900,6 @@ class UserController implements Handler.Callback {
pw.print(mUserLru.get(i));
}
pw.println("]");
- if (dumpAll) {
- pw.print(" mStartedUserArray: ");
- pw.println(Arrays.toString(mStartedUserArray));
- }
if (mUserProfileGroupIds.size() > 0) {
pw.println(" mUserProfileGroupIds:");
for (int i=0; i< mUserProfileGroupIds.size(); i++) {
@@ -2063,9 +2152,11 @@ class UserController implements Handler.Callback {
mService.installEncryptionUnawareProviders(userId);
}
- void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser) {
+ void showUserSwitchingDialog(UserInfo fromUser, UserInfo toUser,
+ String switchingFromSystemUserMessage, String switchingToSystemUserMessage) {
Dialog d = new UserSwitchingDialog(mService, mService.mContext, fromUser, toUser,
- true /* above system */);
+ true /* above system */, switchingFromSystemUserMessage,
+ switchingToSystemUserMessage);
d.show();
}
@@ -2092,5 +2183,9 @@ class UserController implements Handler.Callback {
mService.mLockTaskController.clearLockedTasks(reason);
}
}
+
+ protected boolean isCallerRecents(int callingUid) {
+ return mService.getRecentTasks().isCallerRecents(callingUid);
+ }
}
}
diff --git a/com/android/server/am/UserState.java b/com/android/server/am/UserState.java
index d36d9cbe..00597e24 100644
--- a/com/android/server/am/UserState.java
+++ b/com/android/server/am/UserState.java
@@ -24,8 +24,10 @@ import android.os.Trace;
import android.os.UserHandle;
import android.util.ArrayMap;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.util.ProgressReporter;
+import com.android.server.am.proto.UserStateProto;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -112,10 +114,29 @@ public final class UserState {
}
}
+ public static int stateToProtoEnum(int state) {
+ switch (state) {
+ case STATE_BOOTING: return UserStateProto.STATE_BOOTING;
+ case STATE_RUNNING_LOCKED: return UserStateProto.STATE_RUNNING_LOCKED;
+ case STATE_RUNNING_UNLOCKING: return UserStateProto.STATE_RUNNING_UNLOCKING;
+ case STATE_RUNNING_UNLOCKED: return UserStateProto.STATE_RUNNING_UNLOCKED;
+ case STATE_STOPPING: return UserStateProto.STATE_STOPPING;
+ case STATE_SHUTDOWN: return UserStateProto.STATE_SHUTDOWN;
+ default: return state;
+ }
+ }
+
void dump(String prefix, PrintWriter pw) {
pw.print(prefix);
pw.print("state="); pw.print(stateToString(state));
if (switching) pw.print(" SWITCHING");
pw.println();
}
+
+ void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(UserStateProto.STATE, stateToProtoEnum(state));
+ proto.write(UserStateProto.SWITCHING, switching);
+ proto.end(token);
+ }
}
diff --git a/com/android/server/am/UserSwitchingDialog.java b/com/android/server/am/UserSwitchingDialog.java
index 3e6934f6..afcba3bf 100644
--- a/com/android/server/am/UserSwitchingDialog.java
+++ b/com/android/server/am/UserSwitchingDialog.java
@@ -53,7 +53,8 @@ final class UserSwitchingDialog extends AlertDialog
private boolean mStartedUser;
public UserSwitchingDialog(ActivityManagerService service, Context context, UserInfo oldUser,
- UserInfo newUser, boolean aboveSystem) {
+ UserInfo newUser, boolean aboveSystem, String switchingFromSystemUserMessage,
+ String switchingToSystemUserMessage) {
super(context);
mService = service;
@@ -65,7 +66,7 @@ final class UserSwitchingDialog extends AlertDialog
// Custom view due to alignment and font size requirements
View view = LayoutInflater.from(getContext()).inflate(R.layout.user_switching_dialog, null);
- String viewMessage;
+ String viewMessage = null;
if (UserManager.isSplitSystemUser() && newUser.id == UserHandle.USER_SYSTEM) {
viewMessage = res.getString(R.string.user_logging_out_message, oldUser.name);
} else if (UserManager.isDeviceInDemoMode(context)) {
@@ -75,7 +76,17 @@ final class UserSwitchingDialog extends AlertDialog
viewMessage = res.getString(R.string.demo_starting_message);
}
} else {
- viewMessage = res.getString(R.string.user_switching_message, newUser.name);
+ if (oldUser.id == UserHandle.USER_SYSTEM) {
+ viewMessage = switchingFromSystemUserMessage;
+ } else if (newUser.id == UserHandle.USER_SYSTEM) {
+ viewMessage = switchingToSystemUserMessage;
+ }
+
+ // If switchingFromSystemUserMessage or switchingToSystemUserMessage is null, fallback
+ // to system message.
+ if (viewMessage == null) {
+ viewMessage = res.getString(R.string.user_switching_message, newUser.name);
+ }
}
((TextView) view.findViewById(R.id.message)).setText(viewMessage);
setView(view);
diff --git a/com/android/server/am/VrController.java b/com/android/server/am/VrController.java
index feddfe3a..d32db7ea 100644
--- a/com/android/server/am/VrController.java
+++ b/com/android/server/am/VrController.java
@@ -20,7 +20,11 @@ import android.content.ComponentName;
import android.os.Process;
import android.service.vr.IPersistentVrStateCallbacks;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+import android.util.proto.ProtoUtils;
+
import com.android.server.LocalServices;
+import com.android.server.am.proto.ProcessesProto.VrControllerProto;
import com.android.server.vr.VrManagerInternal;
/**
@@ -49,6 +53,18 @@ final class VrController {
private static final int FLAG_VR_MODE = 1;
private static final int FLAG_PERSISTENT_VR_MODE = 2;
+ // Keep the enum lists in sync
+ private static int[] ORIG_ENUMS = new int[] {
+ FLAG_NON_VR_MODE,
+ FLAG_VR_MODE,
+ FLAG_PERSISTENT_VR_MODE,
+ };
+ private static int[] PROTO_ENUMS = new int[] {
+ VrControllerProto.FLAG_NON_VR_MODE,
+ VrControllerProto.FLAG_VR_MODE,
+ VrControllerProto.FLAG_PERSISTENT_VR_MODE,
+ };
+
// Invariants maintained for mVrState
//
// Always true:
@@ -420,4 +436,12 @@ final class VrController {
public String toString() {
return String.format("[VrState=0x%x,VrRenderThreadTid=%d]", mVrState, mVrRenderThreadTid);
}
+
+ void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ ProtoUtils.writeBitWiseFlagsToProtoEnum(proto, VrControllerProto.VR_MODE,
+ mVrState, ORIG_ENUMS, PROTO_ENUMS);
+ proto.write(VrControllerProto.RENDER_THREAD_ID, mVrRenderThreadTid);
+ proto.end(token);
+ }
}
diff --git a/com/android/server/appwidget/AppWidgetServiceImpl.java b/com/android/server/appwidget/AppWidgetServiceImpl.java
index 54cf726c..85b02206 100644
--- a/com/android/server/appwidget/AppWidgetServiceImpl.java
+++ b/com/android/server/appwidget/AppWidgetServiceImpl.java
@@ -120,7 +120,6 @@ import java.io.ByteArrayOutputStream;
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.PrintWriter;
@@ -134,6 +133,7 @@ import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
@@ -1568,6 +1568,57 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
}
@Override
+ public void updateAppWidgetProviderInfo(ComponentName componentName, String metadataKey) {
+ final int userId = UserHandle.getCallingUserId();
+ if (DEBUG) {
+ Slog.i(TAG, "updateAppWidgetProvider() " + userId);
+ }
+
+ // Make sure the package runs under the caller uid.
+ mSecurityPolicy.enforceCallFromPackage(componentName.getPackageName());
+
+ synchronized (mLock) {
+ ensureGroupStateLoadedLocked(userId);
+
+ // NOTE: The lookup is enforcing security across users by making
+ // sure the caller can access only its providers.
+ ProviderId providerId = new ProviderId(Binder.getCallingUid(), componentName);
+ Provider provider = lookupProviderLocked(providerId);
+ if (provider == null) {
+ throw new IllegalArgumentException(
+ componentName + " is not a valid AppWidget provider");
+ }
+ if (Objects.equals(provider.infoTag, metadataKey)) {
+ // No change
+ return;
+ }
+
+ String keyToUse = metadataKey == null
+ ? AppWidgetManager.META_DATA_APPWIDGET_PROVIDER : metadataKey;
+ AppWidgetProviderInfo info =
+ parseAppWidgetProviderInfo(providerId, provider.info.providerInfo, keyToUse);
+ if (info == null) {
+ throw new IllegalArgumentException("Unable to parse " + keyToUse
+ + " meta-data to a valid AppWidget provider");
+ }
+
+ provider.info = info;
+ provider.infoTag = metadataKey;
+
+ // Update all widgets for this provider
+ final int N = provider.widgets.size();
+ for (int i = 0; i < N; i++) {
+ Widget widget = provider.widgets.get(i);
+ scheduleNotifyProviderChangedLocked(widget);
+ updateAppWidgetInstanceLocked(widget, widget.views, false /* isPartialUpdate */);
+ }
+
+ saveGroupStateAsync(userId);
+ scheduleNotifyGroupHostsForProvidersChangedLocked(userId);
+ }
+ }
+
+ @Override
public boolean isRequestPinAppWidgetSupported() {
return LocalServices.getService(ShortcutServiceInternal.class)
.isRequestPinItemSupported(UserHandle.getCallingUserId(),
@@ -2168,7 +2219,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
ri.activityInfo.name);
ProviderId providerId = new ProviderId(ri.activityInfo.applicationInfo.uid, componentName);
- Provider provider = parseProviderInfoXml(providerId, ri);
+ Provider provider = parseProviderInfoXml(providerId, ri, null);
if (provider != null) {
// we might have an inactive entry for this provider already due to
// a preceding restore operation. if so, fix it up in place; otherwise
@@ -2362,6 +2413,9 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
out.attribute(null, "pkg", p.info.provider.getPackageName());
out.attribute(null, "cl", p.info.provider.getClassName());
out.attribute(null, "tag", Integer.toHexString(p.tag));
+ if (!TextUtils.isEmpty(p.infoTag)) {
+ out.attribute(null, "info_tag", p.infoTag);
+ }
out.endTag(null, "p");
}
@@ -2422,17 +2476,33 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
}
@SuppressWarnings("deprecation")
- private Provider parseProviderInfoXml(ProviderId providerId, ResolveInfo ri) {
- Provider provider = null;
-
- ActivityInfo activityInfo = ri.activityInfo;
- XmlResourceParser parser = null;
- try {
- parser = activityInfo.loadXmlMetaData(mContext.getPackageManager(),
+ private Provider parseProviderInfoXml(ProviderId providerId, ResolveInfo ri,
+ Provider oldProvider) {
+ AppWidgetProviderInfo info = null;
+ if (oldProvider != null && !TextUtils.isEmpty(oldProvider.infoTag)) {
+ info = parseAppWidgetProviderInfo(providerId, ri.activityInfo, oldProvider.infoTag);
+ }
+ if (info == null) {
+ info = parseAppWidgetProviderInfo(providerId, ri.activityInfo,
AppWidgetManager.META_DATA_APPWIDGET_PROVIDER);
+ }
+ if (info == null) {
+ return null;
+ }
+
+ Provider provider = new Provider();
+ provider.id = providerId;
+ provider.info = info;
+ return provider;
+ }
+
+ private AppWidgetProviderInfo parseAppWidgetProviderInfo(
+ ProviderId providerId, ActivityInfo activityInfo, String metadataKey) {
+ try (XmlResourceParser parser =
+ activityInfo.loadXmlMetaData(mContext.getPackageManager(), metadataKey)) {
if (parser == null) {
- Slog.w(TAG, "No " + AppWidgetManager.META_DATA_APPWIDGET_PROVIDER
- + " meta-data for " + "AppWidget provider '" + providerId + '\'');
+ Slog.w(TAG, "No " + metadataKey + " meta-data for AppWidget provider '"
+ + providerId + '\'');
return null;
}
@@ -2452,9 +2522,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
return null;
}
- provider = new Provider();
- provider.id = providerId;
- AppWidgetProviderInfo info = provider.info = new AppWidgetProviderInfo();
+ AppWidgetProviderInfo info = new AppWidgetProviderInfo();
info.provider = providerId.componentName;
info.providerInfo = activityInfo;
@@ -2501,7 +2569,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
className);
}
info.label = activityInfo.loadLabel(mContext.getPackageManager()).toString();
- info.icon = ri.getIconResource();
+ info.icon = activityInfo.getIconResource();
info.previewImage = sa.getResourceId(
com.android.internal.R.styleable.AppWidgetProviderInfo_previewImage, 0);
info.autoAdvanceViewId = sa.getResourceId(
@@ -2516,6 +2584,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
com.android.internal.R.styleable.AppWidgetProviderInfo_widgetFeatures, 0);
sa.recycle();
+ return info;
} catch (IOException | PackageManager.NameNotFoundException | XmlPullParserException e) {
// Ok to catch Exception here, because anything going wrong because
// of what a client process passes to us should not be fatal for the
@@ -2523,12 +2592,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
Slog.w(TAG, "XML parsing failed for AppWidget provider "
+ providerId.componentName + " for user " + providerId.uid, e);
return null;
- } finally {
- if (parser != null) {
- parser.close();
- }
}
- return provider;
}
private int getUidForPackage(String packageName, int userId) {
@@ -2891,7 +2955,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
if (provider.getUserId() != userId) {
continue;
}
- if (provider.widgets.size() > 0) {
+ if (provider.shouldBePersisted()) {
serializeProvider(out, provider);
}
}
@@ -3000,6 +3064,15 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
final int providerTag = !TextUtils.isEmpty(tagAttribute)
? Integer.parseInt(tagAttribute, 16) : legacyProviderIndex;
provider.tag = providerTag;
+
+ provider.infoTag = parser.getAttributeValue(null, "info_tag");
+ if (!TextUtils.isEmpty(provider.infoTag) && !mSafeMode) {
+ AppWidgetProviderInfo info = parseAppWidgetProviderInfo(
+ providerId, providerInfo, provider.infoTag);
+ if (info != null) {
+ provider.info = info;
+ }
+ }
} else if ("h".equals(tag)) {
legacyHostIndex++;
Host host = new Host();
@@ -3254,7 +3327,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
providersUpdated = true;
}
} else {
- Provider parsed = parseProviderInfoXml(providerId, ri);
+ Provider parsed = parseProviderInfoXml(providerId, ri, provider);
if (parsed != null) {
keep.add(providerId);
// Use the new AppWidgetProviderInfo.
@@ -3725,6 +3798,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
AppWidgetProviderInfo info;
ArrayList<Widget> widgets = new ArrayList<>();
PendingIntent broadcast;
+ String infoTag;
boolean zombie; // if we're in safe mode, don't prune this just because nobody references it
boolean maskedByLockedProfile;
@@ -3784,6 +3858,10 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
public boolean isMaskedLocked() {
return maskedByQuietProfile || maskedByLockedProfile || maskedBySuspendedPackage;
}
+
+ public boolean shouldBePersisted() {
+ return !widgets.isEmpty() || !TextUtils.isEmpty(infoTag);
+ }
}
private static final class ProviderId {
@@ -4114,7 +4192,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku
for (int i = 0; i < N; i++) {
Provider provider = mProviders.get(i);
- if (!provider.widgets.isEmpty()
+ if (provider.shouldBePersisted()
&& (provider.isInPackageForUser(backedupPackage, userId)
|| provider.hostedByPackageForUser(backedupPackage, userId))) {
provider.tag = index;
diff --git a/com/android/server/audio/AudioService.java b/com/android/server/audio/AudioService.java
index 799f2a92..bedf0431 100644
--- a/com/android/server/audio/AudioService.java
+++ b/com/android/server/audio/AudioService.java
@@ -63,6 +63,7 @@ import android.hardware.usb.UsbManager;
import android.media.AudioAttributes;
import android.media.AudioDevicePort;
import android.media.AudioFocusInfo;
+import android.media.AudioFocusRequest;
import android.media.AudioSystem;
import android.media.AudioFormat;
import android.media.AudioManager;
@@ -137,7 +138,6 @@ import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
@@ -770,7 +770,7 @@ public class AudioService extends IAudioService.Stub
// Register for device connection intent broadcasts.
IntentFilter intentFilter =
new IntentFilter(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED);
- intentFilter.addAction(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED);
+ intentFilter.addAction(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED);
intentFilter.addAction(Intent.ACTION_DOCK_EVENT);
intentFilter.addAction(Intent.ACTION_SCREEN_ON);
intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
@@ -1047,9 +1047,11 @@ public class AudioService extends IAudioService.Stub
private void checkMuteAffectedStreams() {
// any stream with a min level > 0 is not muteable by definition
+ // STREAM_VOICE_CALL can be muted by applications that has the the MODIFY_PHONE_STATE permission.
for (int i = 0; i < mStreamStates.length; i++) {
final VolumeStreamState vss = mStreamStates[i];
- if (vss.mIndexMin > 0) {
+ if (vss.mIndexMin > 0 &&
+ vss.mStreamType != AudioSystem.STREAM_VOICE_CALL) {
mMuteAffectedStreams &= ~(1 << vss.mStreamType);
}
}
@@ -1412,6 +1414,18 @@ public class AudioService extends IAudioService.Stub
return;
}
+ // If adjust is mute and the stream is STREAM_VOICE_CALL, make sure
+ // that the calling app have the MODIFY_PHONE_STATE permission.
+ if (isMuteAdjust &&
+ streamType == AudioSystem.STREAM_VOICE_CALL &&
+ mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.MODIFY_PHONE_STATE)
+ != PackageManager.PERMISSION_GRANTED) {
+ Log.w(TAG, "MODIFY_PHONE_STATE Permission Denial: adjustStreamVolume from pid="
+ + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
+ return;
+ }
+
// use stream type alias here so that streams with same alias have the same behavior,
// including with regard to silent mode control (e.g the use of STREAM_RING below and in
// checkForRingerModeChange() in place of STREAM_RING or STREAM_NOTIFICATION)
@@ -1712,6 +1726,15 @@ public class AudioService extends IAudioService.Stub
+ " CHANGE_ACCESSIBILITY_VOLUME callingPackage=" + callingPackage);
return;
}
+ if ((streamType == AudioManager.STREAM_VOICE_CALL) &&
+ (index == 0) &&
+ (mContext.checkCallingOrSelfPermission(
+ android.Manifest.permission.MODIFY_PHONE_STATE)
+ != PackageManager.PERMISSION_GRANTED)) {
+ Log.w(TAG, "Trying to call setStreamVolume() for STREAM_VOICE_CALL and index 0 without"
+ + " MODIFY_PHONE_STATE callingPackage=" + callingPackage);
+ return;
+ }
mVolumeLogger.log(new VolumeEvent(VolumeEvent.VOL_SET_STREAM_VOL, streamType,
index/*val1*/, flags/*val2*/, callingPackage));
setStreamVolume(streamType, index, flags, callingPackage, callingPackage,
@@ -2943,14 +2966,28 @@ public class AudioService extends IAudioService.Stub
}
public void setBluetoothScoOnInt(boolean on, String eventSource) {
+ if (DEBUG_DEVICES) {
+ Log.d(TAG, "setBluetoothScoOnInt: " + on + " " + eventSource);
+ }
if (on) {
// do not accept SCO ON if SCO audio is not connected
- synchronized(mScoClients) {
- if ((mBluetoothHeadset != null) &&
- (mBluetoothHeadset.getAudioState(mBluetoothHeadsetDevice)
- != BluetoothHeadset.STATE_AUDIO_CONNECTED)) {
- mForcedUseForCommExt = AudioSystem.FORCE_BT_SCO;
- return;
+ synchronized (mScoClients) {
+ if (mBluetoothHeadset != null) {
+ if (mBluetoothHeadsetDevice == null) {
+ BluetoothDevice activeDevice = mBluetoothHeadset.getActiveDevice();
+ if (activeDevice != null) {
+ // setBtScoActiveDevice() might trigger resetBluetoothSco() which
+ // will call setBluetoothScoOnInt(false, "resetBluetoothSco")
+ setBtScoActiveDevice(activeDevice);
+ }
+ }
+ if (mBluetoothHeadset.getAudioState(mBluetoothHeadsetDevice)
+ != BluetoothHeadset.STATE_AUDIO_CONNECTED) {
+ mForcedUseForCommExt = AudioSystem.FORCE_BT_SCO;
+ Log.w(TAG, "setBluetoothScoOnInt(true) failed because "
+ + mBluetoothHeadsetDevice + " is not in audio connected mode");
+ return;
+ }
}
}
mForcedUseForComm = AudioSystem.FORCE_BT_SCO;
@@ -3338,24 +3375,23 @@ public class AudioService extends IAudioService.Stub
}
}
- void setBtScoDeviceConnectionState(BluetoothDevice btDevice, int state) {
+ private boolean handleBtScoActiveDeviceChange(BluetoothDevice btDevice, boolean isActive) {
if (btDevice == null) {
- return;
+ return true;
}
-
String address = btDevice.getAddress();
BluetoothClass btClass = btDevice.getBluetoothClass();
int outDevice = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO;
int inDevice = AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET;
if (btClass != null) {
switch (btClass.getDeviceClass()) {
- case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
- case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE:
- outDevice = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET;
- break;
- case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO:
- outDevice = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT;
- break;
+ case BluetoothClass.Device.AUDIO_VIDEO_WEARABLE_HEADSET:
+ case BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE:
+ outDevice = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_HEADSET;
+ break;
+ case BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO:
+ outDevice = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO_CARKIT;
+ break;
}
}
@@ -3363,34 +3399,33 @@ public class AudioService extends IAudioService.Stub
address = "";
}
- boolean connected = (state == BluetoothProfile.STATE_CONNECTED);
-
String btDeviceName = btDevice.getName();
- boolean success =
- handleDeviceConnection(connected, outDevice, address, btDeviceName) &&
- handleDeviceConnection(connected, inDevice, address, btDeviceName);
-
- if (!success) {
- return;
- }
+ boolean result = handleDeviceConnection(isActive, outDevice, address, btDeviceName);
+ // handleDeviceConnection() && result to make sure the method get executed
+ result = handleDeviceConnection(isActive, inDevice, address, btDeviceName) && result;
+ return result;
+ }
- /* When one BT headset is disconnected while another BT headset
- * is connected, don't mess with the headset device.
- */
- if ((state == BluetoothProfile.STATE_DISCONNECTED ||
- state == BluetoothProfile.STATE_DISCONNECTING) &&
- mBluetoothHeadset != null &&
- mBluetoothHeadset.getAudioState(btDevice) == BluetoothHeadset.STATE_AUDIO_CONNECTED) {
- Log.w(TAG, "SCO connected through another device, returning");
- return;
+ void setBtScoActiveDevice(BluetoothDevice btDevice) {
+ if (DEBUG_DEVICES) {
+ Log.d(TAG, "setBtScoActiveDevice(" + btDevice + ")");
}
-
synchronized (mScoClients) {
- if (connected) {
+ final BluetoothDevice previousActiveDevice = mBluetoothHeadsetDevice;
+ if (!Objects.equals(btDevice, previousActiveDevice)) {
+ if (!handleBtScoActiveDeviceChange(previousActiveDevice, false)) {
+ Log.w(TAG, "setBtScoActiveDevice() failed to remove previous device "
+ + previousActiveDevice);
+ }
+ if (!handleBtScoActiveDeviceChange(btDevice, true)) {
+ Log.e(TAG, "setBtScoActiveDevice() failed to add new device " + btDevice);
+ // set mBluetoothHeadsetDevice to null when failing to add new device
+ btDevice = null;
+ }
mBluetoothHeadsetDevice = btDevice;
- } else {
- mBluetoothHeadsetDevice = null;
- resetBluetoothSco();
+ if (mBluetoothHeadsetDevice == null) {
+ resetBluetoothSco();
+ }
}
}
}
@@ -3445,12 +3480,7 @@ public class AudioService extends IAudioService.Stub
// Discard timeout message
mAudioHandler.removeMessages(MSG_BT_HEADSET_CNCT_FAILED);
mBluetoothHeadset = (BluetoothHeadset) proxy;
- deviceList = mBluetoothHeadset.getConnectedDevices();
- if (deviceList.size() > 0) {
- mBluetoothHeadsetDevice = deviceList.get(0);
- } else {
- mBluetoothHeadsetDevice = null;
- }
+ setBtScoActiveDevice(mBluetoothHeadset.getActiveDevice());
// Refresh SCO audio state
checkScoAudioState();
// Continue pending action if any
@@ -3571,10 +3601,7 @@ public class AudioService extends IAudioService.Stub
void disconnectHeadset() {
synchronized (mScoClients) {
- if (mBluetoothHeadsetDevice != null) {
- setBtScoDeviceConnectionState(mBluetoothHeadsetDevice,
- BluetoothProfile.STATE_DISCONNECTED);
- }
+ setBtScoActiveDevice(null);
mBluetoothHeadset = null;
}
}
@@ -4132,22 +4159,30 @@ public class AudioService extends IAudioService.Stub
public int setBluetoothA2dpDeviceConnectionState(BluetoothDevice device, int state, int profile)
{
+ return setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(
+ device, state, profile, false /* suppressNoisyIntent */);
+ }
+
+ public int setBluetoothA2dpDeviceConnectionStateSuppressNoisyIntent(BluetoothDevice device,
+ int state, int profile, boolean suppressNoisyIntent)
+ {
if (mAudioHandler.hasMessages(MSG_SET_A2DP_SINK_CONNECTION_STATE, device)) {
return 0;
}
return setBluetoothA2dpDeviceConnectionStateInt(
- device, state, profile, AudioSystem.DEVICE_NONE);
+ device, state, profile, suppressNoisyIntent, AudioSystem.DEVICE_NONE);
}
public int setBluetoothA2dpDeviceConnectionStateInt(
- BluetoothDevice device, int state, int profile, int musicDevice)
+ BluetoothDevice device, int state, int profile, boolean suppressNoisyIntent,
+ int musicDevice)
{
int delay;
if (profile != BluetoothProfile.A2DP && profile != BluetoothProfile.A2DP_SINK) {
throw new IllegalArgumentException("invalid profile " + profile);
}
synchronized (mConnectedDevices) {
- if (profile == BluetoothProfile.A2DP) {
+ if (profile == BluetoothProfile.A2DP && !suppressNoisyIntent) {
int intState = (state == BluetoothA2dp.STATE_CONNECTED) ? 1 : 0;
delay = checkSendBecomingNoisyIntent(AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP,
intState, musicDevice);
@@ -4503,27 +4538,30 @@ public class AudioService extends IAudioService.Stub
if (mStreamType == srcStream.mStreamType) {
return;
}
- synchronized (VolumeStreamState.class) {
- int srcStreamType = srcStream.getStreamType();
- // apply default device volume from source stream to all devices first in case
- // some devices are present in this stream state but not in source stream state
- int index = srcStream.getIndex(AudioSystem.DEVICE_OUT_DEFAULT);
- index = rescaleIndex(index, srcStreamType, mStreamType);
- for (int i = 0; i < mIndexMap.size(); i++) {
- mIndexMap.put(mIndexMap.keyAt(i), index);
- }
- // Now apply actual volume for devices in source stream state
- SparseIntArray srcMap = srcStream.mIndexMap;
- for (int i = 0; i < srcMap.size(); i++) {
- int device = srcMap.keyAt(i);
- index = srcMap.valueAt(i);
+ synchronized (mSettingsLock) {
+ synchronized (VolumeStreamState.class) {
+ int srcStreamType = srcStream.getStreamType();
+ // apply default device volume from source stream to all devices first in case
+ // some devices are present in this stream state but not in source stream state
+ int index = srcStream.getIndex(AudioSystem.DEVICE_OUT_DEFAULT);
index = rescaleIndex(index, srcStreamType, mStreamType);
-
- setIndex(index, device, caller);
+ for (int i = 0; i < mIndexMap.size(); i++) {
+ mIndexMap.put(mIndexMap.keyAt(i), index);
+ }
+ // Now apply actual volume for devices in source stream state
+ SparseIntArray srcMap = srcStream.mIndexMap;
+ for (int i = 0; i < srcMap.size(); i++) {
+ int device = srcMap.keyAt(i);
+ index = srcMap.valueAt(i);
+ index = rescaleIndex(index, srcStreamType, mStreamType);
+
+ setIndex(index, device, caller);
+ }
}
}
}
+ @GuardedBy("mSettingsLock")
public void setAllIndexesToMax() {
synchronized (VolumeStreamState.class) {
for (int i = 0; i < mIndexMap.size(); i++) {
@@ -5397,7 +5435,7 @@ public class AudioService extends IAudioService.Stub
// consistent with audio policy manager state
setBluetoothA2dpDeviceConnectionStateInt(
btDevice, BluetoothA2dp.STATE_DISCONNECTED, BluetoothProfile.A2DP,
- musicDevice);
+ false /* suppressNoisyIntent */, musicDevice);
}
}
}
@@ -5753,11 +5791,9 @@ public class AudioService extends IAudioService.Stub
AudioSystem.setForceUse(AudioSystem.FOR_DOCK, config);
}
mDockState = dockState;
- } else if (action.equals(BluetoothHeadset.ACTION_CONNECTION_STATE_CHANGED)) {
- state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE,
- BluetoothProfile.STATE_DISCONNECTED);
+ } else if (action.equals(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) {
BluetoothDevice btDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
- setBtScoDeviceConnectionState(btDevice, state);
+ setBtScoActiveDevice(btDevice);
} else if (action.equals(BluetoothHeadset.ACTION_AUDIO_STATE_CHANGED)) {
boolean broadcast = false;
int scoAudioState = AudioManager.SCO_AUDIO_STATE_ERROR;
@@ -5969,6 +6005,44 @@ public class AudioService extends IAudioService.Stub
//==========================================================================================
// Audio Focus
//==========================================================================================
+ /**
+ * Returns whether a focus request is eligible to force ducking.
+ * Will return true if:
+ * - the AudioAttributes have a usage of USAGE_ASSISTANCE_ACCESSIBILITY,
+ * - the focus request is AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK,
+ * - the associated Bundle has KEY_ACCESSIBILITY_FORCE_FOCUS_DUCKING set to true,
+ * - the uid of the requester is a known accessibility service or root.
+ * @param aa AudioAttributes of the focus request
+ * @param uid uid of the focus requester
+ * @return true if ducking is to be forced
+ */
+ private boolean forceFocusDuckingForAccessibility(@Nullable AudioAttributes aa,
+ int request, int uid) {
+ if (aa == null || aa.getUsage() != AudioAttributes.USAGE_ASSISTANCE_ACCESSIBILITY
+ || request != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK) {
+ return false;
+ }
+ final Bundle extraInfo = aa.getBundle();
+ if (extraInfo == null ||
+ !extraInfo.getBoolean(AudioFocusRequest.KEY_ACCESSIBILITY_FORCE_FOCUS_DUCKING)) {
+ return false;
+ }
+ if (uid == 0) {
+ return true;
+ }
+ synchronized (mAccessibilityServiceUidsLock) {
+ if (mAccessibilityServiceUids != null) {
+ int callingUid = Binder.getCallingUid();
+ for (int i = 0; i < mAccessibilityServiceUids.length; i++) {
+ if (mAccessibilityServiceUids[i] == callingUid) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
public int requestAudioFocus(AudioAttributes aa, int durationHint, IBinder cb,
IAudioFocusDispatcher fd, String clientId, String callingPackageName, int flags,
IAudioPolicyCallback pcb, int sdk) {
@@ -5992,7 +6066,8 @@ public class AudioService extends IAudioService.Stub
}
return mMediaFocusControl.requestAudioFocus(aa, durationHint, cb, fd,
- clientId, callingPackageName, flags, sdk);
+ clientId, callingPackageName, flags, sdk,
+ forceFocusDuckingForAccessibility(aa, durationHint, Binder.getCallingUid()));
}
public int abandonAudioFocus(IAudioFocusDispatcher fd, String clientId, AudioAttributes aa,
@@ -6560,7 +6635,19 @@ public class AudioService extends IAudioService.Stub
// Inform AudioFlinger of our device's low RAM attribute
private static void readAndSetLowRamDevice()
{
- int status = AudioSystem.setLowRamDevice(ActivityManager.isLowRamDeviceStatic());
+ boolean isLowRamDevice = ActivityManager.isLowRamDeviceStatic();
+ long totalMemory = 1024 * 1024 * 1024; // 1GB is the default if ActivityManager fails.
+
+ try {
+ final ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
+ ActivityManager.getService().getMemoryInfo(info);
+ totalMemory = info.totalMem;
+ } catch (RemoteException e) {
+ Log.w(TAG, "Cannot obtain MemoryInfo from ActivityManager, assume low memory device");
+ isLowRamDevice = true;
+ }
+
+ final int status = AudioSystem.setLowRamDevice(isLowRamDevice, totalMemory);
if (status != 0) {
Log.w(TAG, "AudioFlinger informed of device's low RAM attribute; status " + status);
}
diff --git a/com/android/server/audio/FocusRequester.java b/com/android/server/audio/FocusRequester.java
index c298fe70..f2ef02fb 100644
--- a/com/android/server/audio/FocusRequester.java
+++ b/com/android/server/audio/FocusRequester.java
@@ -25,6 +25,7 @@ import android.media.IAudioFocusDispatcher;
import android.os.IBinder;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
import com.android.server.audio.MediaFocusControl.AudioFocusDeathHandler;
import java.io.PrintWriter;
@@ -300,16 +301,20 @@ public class FocusRequester {
}
/**
- * Called synchronized on MediaFocusControl.mAudioFocusLock
+ * Handle the loss of focus resulting from a given focus gain.
+ * @param focusGain the focus gain from which the loss of focus is resulting
+ * @param frWinner the new focus owner
+ * @return true if the focus loss is definitive, false otherwise.
*/
- void handleExternalFocusGain(int focusGain, final FocusRequester fr) {
- int focusLoss = focusLossForGainRequest(focusGain);
- handleFocusLoss(focusLoss, fr);
+ @GuardedBy("MediaFocusControl.mAudioFocusLock")
+ boolean handleFocusLossFromGain(int focusGain, final FocusRequester frWinner, boolean forceDuck)
+ {
+ final int focusLoss = focusLossForGainRequest(focusGain);
+ handleFocusLoss(focusLoss, frWinner, forceDuck);
+ return (focusLoss == AudioManager.AUDIOFOCUS_LOSS);
}
- /**
- * Called synchronized on MediaFocusControl.mAudioFocusLock
- */
+ @GuardedBy("MediaFocusControl.mAudioFocusLock")
void handleFocusGain(int focusGain) {
try {
mFocusLossReceived = AudioManager.AUDIOFOCUS_NONE;
@@ -331,19 +336,16 @@ public class FocusRequester {
}
}
- /**
- * Called synchronized on MediaFocusControl.mAudioFocusLock
- */
+ @GuardedBy("MediaFocusControl.mAudioFocusLock")
void handleFocusGainFromRequest(int focusRequestResult) {
if (focusRequestResult == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
mFocusController.unduckPlayers(this);
}
}
- /**
- * Called synchronized on MediaFocusControl.mAudioFocusLock
- */
- void handleFocusLoss(int focusLoss, @Nullable final FocusRequester fr) {
+ @GuardedBy("MediaFocusControl.mAudioFocusLock")
+ void handleFocusLoss(int focusLoss, @Nullable final FocusRequester frWinner, boolean forceDuck)
+ {
try {
if (focusLoss != mFocusLossReceived) {
mFocusLossReceived = focusLoss;
@@ -371,22 +373,23 @@ public class FocusRequester {
boolean handled = false;
if (focusLoss == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK
&& MediaFocusControl.ENFORCE_DUCKING
- && fr != null) {
+ && frWinner != null) {
// candidate for enforcement by the framework
- if (fr.mCallingUid != this.mCallingUid) {
- if ((mGrantFlags
- & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0) {
+ if (frWinner.mCallingUid != this.mCallingUid) {
+ if (!forceDuck && ((mGrantFlags
+ & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) != 0)) {
// the focus loser declared it would pause instead of duck, let it
// handle it (the framework doesn't pause for apps)
handled = false;
Log.v(TAG, "not ducking uid " + this.mCallingUid + " - flags");
- } else if (MediaFocusControl.ENFORCE_DUCKING_FOR_NEW &&
- this.getSdkTarget() <= MediaFocusControl.DUCKING_IN_APP_SDK_LEVEL) {
+ } else if (!forceDuck && (MediaFocusControl.ENFORCE_DUCKING_FOR_NEW &&
+ this.getSdkTarget() <= MediaFocusControl.DUCKING_IN_APP_SDK_LEVEL))
+ {
// legacy behavior, apps used to be notified when they should be ducking
handled = false;
Log.v(TAG, "not ducking uid " + this.mCallingUid + " - old SDK");
} else {
- handled = mFocusController.duckPlayers(fr, this);
+ handled = mFocusController.duckPlayers(frWinner, this, forceDuck);
}
} // else: the focus change is within the same app, so let the dispatching
// happen as if the framework was not involved.
diff --git a/com/android/server/audio/MediaFocusControl.java b/com/android/server/audio/MediaFocusControl.java
index c5f563c7..9ddc52a1 100644
--- a/com/android/server/audio/MediaFocusControl.java
+++ b/com/android/server/audio/MediaFocusControl.java
@@ -32,11 +32,15 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
+import com.android.internal.annotations.GuardedBy;
+
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Map.Entry;
import java.util.Set;
import java.util.Stack;
@@ -97,8 +101,8 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
//=================================================================
// PlayerFocusEnforcer implementation
@Override
- public boolean duckPlayers(FocusRequester winner, FocusRequester loser) {
- return mFocusEnforcer.duckPlayers(winner, loser);
+ public boolean duckPlayers(FocusRequester winner, FocusRequester loser, boolean forceDuck) {
+ return mFocusEnforcer.duckPlayers(winner, loser, forceDuck);
}
@Override
@@ -140,15 +144,14 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
if (!mFocusStack.empty()) {
// notify the current focus owner it lost focus after removing it from stack
final FocusRequester exFocusOwner = mFocusStack.pop();
- exFocusOwner.handleFocusLoss(AudioManager.AUDIOFOCUS_LOSS, null);
+ exFocusOwner.handleFocusLoss(AudioManager.AUDIOFOCUS_LOSS, null,
+ false /*forceDuck*/);
exFocusOwner.release();
}
}
}
- /**
- * Called synchronized on mAudioFocusLock
- */
+ @GuardedBy("mAudioFocusLock")
private void notifyTopOfAudioFocusStack() {
// notify the top of the stack it gained focus
if (!mFocusStack.empty()) {
@@ -160,14 +163,25 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
/**
* Focus is requested, propagate the associated loss throughout the stack.
+ * Will also remove entries in the stack that have just received a definitive loss of focus.
* @param focusGain the new focus gain that will later be added at the top of the stack
*/
- private void propagateFocusLossFromGain_syncAf(int focusGain, final FocusRequester fr) {
+ @GuardedBy("mAudioFocusLock")
+ private void propagateFocusLossFromGain_syncAf(int focusGain, final FocusRequester fr,
+ boolean forceDuck) {
+ final List<String> clientsToRemove = new LinkedList<String>();
// going through the audio focus stack to signal new focus, traversing order doesn't
// matter as all entries respond to the same external focus gain
- Iterator<FocusRequester> stackIterator = mFocusStack.iterator();
- while(stackIterator.hasNext()) {
- stackIterator.next().handleExternalFocusGain(focusGain, fr);
+ for (FocusRequester focusLoser : mFocusStack) {
+ final boolean isDefinitiveLoss =
+ focusLoser.handleFocusLossFromGain(focusGain, fr, forceDuck);
+ if (isDefinitiveLoss) {
+ clientsToRemove.add(focusLoser.getClientId());
+ }
+ }
+ for (String clientToRemove : clientsToRemove) {
+ removeFocusStackEntry(clientToRemove, false /*signal*/,
+ true /*notifyFocusFollowers*/);
}
}
@@ -198,13 +212,12 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
}
/**
- * Helper function:
- * Called synchronized on mAudioFocusLock
* Remove a focus listener from the focus stack.
* @param clientToRemove the focus listener
* @param signal if true and the listener was at the top of the focus stack, i.e. it was holding
* focus, notify the next item in the stack it gained focus.
*/
+ @GuardedBy("mAudioFocusLock")
private void removeFocusStackEntry(String clientToRemove, boolean signal,
boolean notifyFocusFollowers) {
// is the current top of the focus stack abandoning focus? (because of request, not death)
@@ -242,10 +255,9 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
}
/**
- * Helper function:
- * Called synchronized on mAudioFocusLock
* Remove focus listeners from the focus stack for a particular client when it has died.
*/
+ @GuardedBy("mAudioFocusLock")
private void removeFocusStackEntryOnDeath(IBinder cb) {
// is the owner of the audio focus part of the client to remove?
boolean isTopOfStackForClientToRemove = !mFocusStack.isEmpty() &&
@@ -271,10 +283,10 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
/**
* Helper function for external focus policy:
- * Called synchronized on mAudioFocusLock
* Remove focus listeners from the list of potential focus owners for a particular client when
* it has died.
*/
+ @GuardedBy("mAudioFocusLock")
private void removeFocusEntryForExtPolicy(IBinder cb) {
if (mFocusOwnersForFocusPolicy.isEmpty()) {
return;
@@ -324,6 +336,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
* @return {@link AudioManager#AUDIOFOCUS_REQUEST_GRANTED} or
* {@link AudioManager#AUDIOFOCUS_REQUEST_DELAYED}
*/
+ @GuardedBy("mAudioFocusLock")
private int pushBelowLockedFocusOwners(FocusRequester nfr) {
int lastLockedFocusOwnerIndex = mFocusStack.size();
for (int index = mFocusStack.size()-1; index >= 0; index--) {
@@ -336,7 +349,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
Log.e(TAG, "No exclusive focus owner found in propagateFocusLossFromGain_syncAf()",
new Exception());
// no exclusive owner, push at top of stack, focus is granted, propagate change
- propagateFocusLossFromGain_syncAf(nfr.getGainRequest(), nfr);
+ propagateFocusLossFromGain_syncAf(nfr.getGainRequest(), nfr, false /*forceDuck*/);
mFocusStack.push(nfr);
return AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
} else {
@@ -653,7 +666,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
/** @see AudioManager#requestAudioFocus(AudioManager.OnAudioFocusChangeListener, int, int, int) */
protected int requestAudioFocus(AudioAttributes aa, int focusChangeHint, IBinder cb,
IAudioFocusDispatcher fd, String clientId, String callingPackageName, int flags,
- int sdk) {
+ int sdk, boolean forceDuck) {
mEventLogger.log((new AudioEventLogger.StringEvent(
"requestAudioFocus() from uid/pid " + Binder.getCallingUid()
+ "/" + Binder.getCallingPid()
@@ -766,7 +779,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer {
} else {
// propagate the focus change through the stack
if (!mFocusStack.empty()) {
- propagateFocusLossFromGain_syncAf(focusChangeHint, nfr);
+ propagateFocusLossFromGain_syncAf(focusChangeHint, nfr, forceDuck);
}
// push focus requester at the top of the audio focus stack
diff --git a/com/android/server/audio/PlaybackActivityMonitor.java b/com/android/server/audio/PlaybackActivityMonitor.java
index 49431733..ff864536 100644
--- a/com/android/server/audio/PlaybackActivityMonitor.java
+++ b/com/android/server/audio/PlaybackActivityMonitor.java
@@ -421,7 +421,7 @@ public final class PlaybackActivityMonitor
private final DuckingManager mDuckingManager = new DuckingManager();
@Override
- public boolean duckPlayers(FocusRequester winner, FocusRequester loser) {
+ public boolean duckPlayers(FocusRequester winner, FocusRequester loser, boolean forceDuck) {
if (DEBUG) {
Log.v(TAG, String.format("duckPlayers: uids winner=%d loser=%d",
winner.getClientUid(), loser.getClientUid()));
@@ -441,8 +441,8 @@ public final class PlaybackActivityMonitor
&& loser.hasSameUid(apc.getClientUid())
&& apc.getPlayerState() == AudioPlaybackConfiguration.PLAYER_STATE_STARTED)
{
- if (apc.getAudioAttributes().getContentType() ==
- AudioAttributes.CONTENT_TYPE_SPEECH) {
+ if (!forceDuck && (apc.getAudioAttributes().getContentType() ==
+ AudioAttributes.CONTENT_TYPE_SPEECH)) {
// the player is speaking, ducking will make the speech unintelligible
// so let the app handle it instead
Log.v(TAG, "not ducking player " + apc.getPlayerInterfaceId()
diff --git a/com/android/server/audio/PlayerFocusEnforcer.java b/com/android/server/audio/PlayerFocusEnforcer.java
index 0733eca9..3c834daf 100644
--- a/com/android/server/audio/PlayerFocusEnforcer.java
+++ b/com/android/server/audio/PlayerFocusEnforcer.java
@@ -25,7 +25,7 @@ public interface PlayerFocusEnforcer {
* @param loser
* @return
*/
- public boolean duckPlayers(FocusRequester winner, FocusRequester loser);
+ public boolean duckPlayers(FocusRequester winner, FocusRequester loser, boolean forceDuck);
public void unduckPlayers(FocusRequester winner);
diff --git a/com/android/server/autofill/AutofillManagerService.java b/com/android/server/autofill/AutofillManagerService.java
index e1cb154c..0e2ca14c 100644
--- a/com/android/server/autofill/AutofillManagerService.java
+++ b/com/android/server/autofill/AutofillManagerService.java
@@ -44,6 +44,7 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
+import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
@@ -78,6 +79,7 @@ import com.android.server.autofill.ui.AutoFillUI;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -117,6 +119,7 @@ public final class AutofillManagerService extends SystemService {
private final LocalLog mRequestsHistory = new LocalLog(20);
private final LocalLog mUiLatencyHistory = new LocalLog(20);
+ private final LocalLog mWtfHistory = new LocalLog(50);
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@Override
@@ -308,7 +311,8 @@ public final class AutofillManagerService extends SystemService {
AutofillManagerServiceImpl service = mServicesCache.get(resolvedUserId);
if (service == null) {
service = new AutofillManagerServiceImpl(mContext, mLock, mRequestsHistory,
- mUiLatencyHistory, resolvedUserId, mUi, mDisabledUsers.get(resolvedUserId));
+ mUiLatencyHistory, mWtfHistory, resolvedUserId, mUi,
+ mDisabledUsers.get(resolvedUserId));
mServicesCache.put(userId, service);
}
return service;
@@ -443,6 +447,18 @@ public final class AutofillManagerService extends SystemService {
}
}
+ // Called by Shell command.
+ public void getScore(@Nullable String algorithmName, @NonNull String value1,
+ @NonNull String value2, @NonNull RemoteCallback callback) {
+ mContext.enforceCallingPermission(MANAGE_AUTO_FILL, TAG);
+
+ final FieldClassificationStrategy strategy =
+ new FieldClassificationStrategy(mContext, UserHandle.USER_CURRENT);
+
+ strategy.getScores(callback, algorithmName, null,
+ Arrays.asList(AutofillValue.forText(value1)), new String[] { value2 });
+ }
+
private void setDebugLocked(boolean debug) {
com.android.server.autofill.Helper.sDebug = debug;
android.view.autofill.Helper.sDebug = debug;
@@ -479,7 +495,7 @@ public final class AutofillManagerService extends SystemService {
if (service != null) {
service.destroySessionsLocked();
service.updateLocked(disabled);
- if (!service.isEnabled()) {
+ if (!service.isEnabledLocked()) {
removeCachedServiceLocked(userId);
}
}
@@ -518,6 +534,8 @@ public final class AutofillManagerService extends SystemService {
final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
if (service != null) {
service.removeClientLocked(client);
+ } else if (sVerbose) {
+ Slog.v(TAG, "removeClient(): no service for " + userId);
}
}
}
@@ -574,6 +592,8 @@ public final class AutofillManagerService extends SystemService {
final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
if (service != null) {
return service.getFillEventHistory(getCallingUid());
+ } else if (sVerbose) {
+ Slog.v(TAG, "getFillEventHistory(): no service for " + userId);
}
}
@@ -588,6 +608,8 @@ public final class AutofillManagerService extends SystemService {
final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
if (service != null) {
return service.getUserData(getCallingUid());
+ } else if (sVerbose) {
+ Slog.v(TAG, "getUserData(): no service for " + userId);
}
}
@@ -602,6 +624,8 @@ public final class AutofillManagerService extends SystemService {
final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
if (service != null) {
service.setUserData(getCallingUid(), userData);
+ } else if (sVerbose) {
+ Slog.v(TAG, "setUserData(): no service for " + userId);
}
}
}
@@ -614,6 +638,8 @@ public final class AutofillManagerService extends SystemService {
final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
if (service != null) {
return service.isFieldClassificationEnabled(getCallingUid());
+ } else if (sVerbose) {
+ Slog.v(TAG, "isFieldClassificationEnabled(): no service for " + userId);
}
}
@@ -621,6 +647,40 @@ public final class AutofillManagerService extends SystemService {
}
@Override
+ public String getDefaultFieldClassificationAlgorithm() throws RemoteException {
+ final int userId = UserHandle.getCallingUserId();
+
+ synchronized (mLock) {
+ final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
+ if (service != null) {
+ return service.getDefaultFieldClassificationAlgorithm(getCallingUid());
+ } else {
+ if (sVerbose) {
+ Slog.v(TAG, "getDefaultFcAlgorithm(): no service for " + userId);
+ }
+ return null;
+ }
+ }
+ }
+
+ @Override
+ public String[] getAvailableFieldClassificationAlgorithms() throws RemoteException {
+ final int userId = UserHandle.getCallingUserId();
+
+ synchronized (mLock) {
+ final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
+ if (service != null) {
+ return service.getAvailableFieldClassificationAlgorithms(getCallingUid());
+ } else {
+ if (sVerbose) {
+ Slog.v(TAG, "getAvailableFcAlgorithms(): no service for " + userId);
+ }
+ return null;
+ }
+ }
+ }
+
+ @Override
public ComponentName getAutofillServiceComponentName() throws RemoteException {
final int userId = UserHandle.getCallingUserId();
@@ -628,6 +688,8 @@ public final class AutofillManagerService extends SystemService {
final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
if (service != null) {
return service.getServiceComponentName();
+ } else if (sVerbose) {
+ Slog.v(TAG, "getAutofillServiceComponentName(): no service for " + userId);
}
}
@@ -637,15 +699,17 @@ public final class AutofillManagerService extends SystemService {
@Override
public boolean restoreSession(int sessionId, IBinder activityToken, IBinder appCallback)
throws RemoteException {
+ final int userId = UserHandle.getCallingUserId();
activityToken = Preconditions.checkNotNull(activityToken, "activityToken");
appCallback = Preconditions.checkNotNull(appCallback, "appCallback");
synchronized (mLock) {
- final AutofillManagerServiceImpl service = mServicesCache.get(
- UserHandle.getCallingUserId());
+ final AutofillManagerServiceImpl service = mServicesCache.get(userId);
if (service != null) {
return service.restoreSession(sessionId, getCallingUid(), activityToken,
appCallback);
+ } else if (sVerbose) {
+ Slog.v(TAG, "restoreSession(): no service for " + userId);
}
}
@@ -660,6 +724,8 @@ public final class AutofillManagerService extends SystemService {
if (service != null) {
service.updateSessionLocked(sessionId, getCallingUid(), autoFillId, bounds,
value, action, flags);
+ } else if (sVerbose) {
+ Slog.v(TAG, "updateSession(): no service for " + userId);
}
}
}
@@ -675,6 +741,8 @@ public final class AutofillManagerService extends SystemService {
if (service != null) {
restart = service.updateSessionLocked(sessionId, getCallingUid(), autoFillId,
bounds, value, action, flags);
+ } else if (sVerbose) {
+ Slog.v(TAG, "updateOrRestartSession(): no service for " + userId);
}
}
if (restart) {
@@ -692,6 +760,8 @@ public final class AutofillManagerService extends SystemService {
final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
if (service != null) {
service.finishSessionLocked(sessionId, getCallingUid());
+ } else if (sVerbose) {
+ Slog.v(TAG, "finishSession(): no service for " + userId);
}
}
}
@@ -702,6 +772,8 @@ public final class AutofillManagerService extends SystemService {
final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
if (service != null) {
service.cancelSessionLocked(sessionId, getCallingUid());
+ } else if (sVerbose) {
+ Slog.v(TAG, "cancelSession(): no service for " + userId);
}
}
}
@@ -712,6 +784,8 @@ public final class AutofillManagerService extends SystemService {
final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
if (service != null) {
service.disableOwnedAutofillServicesLocked(Binder.getCallingUid());
+ } else if (sVerbose) {
+ Slog.v(TAG, "cancelSession(): no service for " + userId);
}
}
}
@@ -727,8 +801,12 @@ public final class AutofillManagerService extends SystemService {
public boolean isServiceEnabled(int userId, String packageName) {
synchronized (mLock) {
final AutofillManagerServiceImpl service = peekServiceForUserLocked(userId);
- if (service == null) return false;
- return Objects.equals(packageName, service.getServicePackageName());
+ if (service != null) {
+ return Objects.equals(packageName, service.getServicePackageName());
+ } else if (sVerbose) {
+ Slog.v(TAG, "isServiceEnabled(): no service for " + userId);
+ }
+ return false;
}
}
@@ -802,10 +880,12 @@ public final class AutofillManagerService extends SystemService {
mUi.dump(pw);
}
if (showHistory) {
- pw.println("Requests history:");
+ pw.println(); pw.println("Requests history:"); pw.println();
mRequestsHistory.reverseDump(fd, pw, args);
- pw.println("UI latency history:");
+ pw.println(); pw.println("UI latency history:"); pw.println();
mUiLatencyHistory.reverseDump(fd, pw, args);
+ pw.println(); pw.println("WTF history:"); pw.println();
+ mWtfHistory.reverseDump(fd, pw, args);
}
} finally {
setDebugLocked(oldDebug);
diff --git a/com/android/server/autofill/AutofillManagerServiceImpl.java b/com/android/server/autofill/AutofillManagerServiceImpl.java
index 4cdfd625..07b0b77a 100644
--- a/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -105,33 +105,42 @@ final class AutofillManagerServiceImpl {
private final AutoFillUI mUi;
private final MetricsLogger mMetricsLogger = new MetricsLogger();
+ @GuardedBy("mLock")
private RemoteCallbackList<IAutoFillManagerClient> mClients;
+
+ @GuardedBy("mLock")
private AutofillServiceInfo mInfo;
private static final Random sRandom = new Random();
private final LocalLog mRequestsHistory;
private final LocalLog mUiLatencyHistory;
+ private final LocalLog mWtfHistory;
+ private final FieldClassificationStrategy mFieldClassificationStrategy;
/**
* Apps disabled by the service; key is package name, value is when they will be enabled again.
*/
+ @GuardedBy("mLock")
private ArrayMap<String, Long> mDisabledApps;
/**
* Activities disabled by the service; key is component name, value is when they will be enabled
* again.
*/
+ @GuardedBy("mLock")
private ArrayMap<ComponentName, Long> mDisabledActivities;
/**
* Whether service was disabled for user due to {@link UserManager} restrictions.
*/
+ @GuardedBy("mLock")
private boolean mDisabled;
/**
* Data used for field classification.
*/
+ @GuardedBy("mLock")
private UserData mUserData;
/**
@@ -170,13 +179,16 @@ final class AutofillManagerServiceImpl {
private long mLastPrune = 0;
AutofillManagerServiceImpl(Context context, Object lock, LocalLog requestsHistory,
- LocalLog uiLatencyHistory, int userId, AutoFillUI ui, boolean disabled) {
+ LocalLog uiLatencyHistory, LocalLog wtfHistory, int userId, AutoFillUI ui,
+ boolean disabled) {
mContext = context;
mLock = lock;
mRequestsHistory = requestsHistory;
mUiLatencyHistory = uiLatencyHistory;
+ mWtfHistory = wtfHistory;
mUserId = userId;
mUi = ui;
+ mFieldClassificationStrategy = new FieldClassificationStrategy(context, userId);
updateLocked(disabled);
}
@@ -235,7 +247,7 @@ final class AutofillManagerServiceImpl {
}
void updateLocked(boolean disabled) {
- final boolean wasEnabled = isEnabled();
+ final boolean wasEnabled = isEnabledLocked();
if (sVerbose) {
Slog.v(TAG, "updateLocked(u=" + mUserId + "): wasEnabled=" + wasEnabled
+ ", mSetupComplete= " + mSetupComplete
@@ -274,7 +286,7 @@ final class AutofillManagerServiceImpl {
Slog.e(TAG, "Bad AutofillServiceInfo for '" + componentName + "': " + e);
mInfo = null;
}
- final boolean isEnabled = isEnabled();
+ final boolean isEnabled = isEnabledLocked();
if (wasEnabled != isEnabled) {
if (!isEnabled) {
final int sessionCount = mSessions.size();
@@ -292,7 +304,7 @@ final class AutofillManagerServiceImpl {
mClients = new RemoteCallbackList<>();
}
mClients.register(client);
- return isEnabled();
+ return isEnabledLocked();
}
void removeClientLocked(IAutoFillManagerClient client) {
@@ -302,7 +314,7 @@ final class AutofillManagerServiceImpl {
}
void setAuthenticationResultLocked(Bundle data, int sessionId, int authenticationId, int uid) {
- if (!isEnabled()) {
+ if (!isEnabledLocked()) {
return;
}
final Session session = mSessions.get(sessionId);
@@ -312,7 +324,7 @@ final class AutofillManagerServiceImpl {
}
void setHasCallback(int sessionId, int uid, boolean hasIt) {
- if (!isEnabled()) {
+ if (!isEnabledLocked()) {
return;
}
final Session session = mSessions.get(sessionId);
@@ -327,7 +339,7 @@ final class AutofillManagerServiceImpl {
@NonNull IBinder appCallbackToken, @NonNull AutofillId autofillId,
@NonNull Rect virtualBounds, @Nullable AutofillValue value, boolean hasCallback,
int flags, @NonNull ComponentName componentName) {
- if (!isEnabled()) {
+ if (!isEnabledLocked()) {
return 0;
}
@@ -388,7 +400,7 @@ final class AutofillManagerServiceImpl {
}
void finishSessionLocked(int sessionId, int uid) {
- if (!isEnabled()) {
+ if (!isEnabledLocked()) {
return;
}
@@ -411,7 +423,7 @@ final class AutofillManagerServiceImpl {
}
void cancelSessionLocked(int sessionId, int uid) {
- if (!isEnabled()) {
+ if (!isEnabledLocked()) {
return;
}
@@ -474,8 +486,8 @@ final class AutofillManagerServiceImpl {
assertCallerLocked(componentName);
final Session newSession = new Session(this, mUi, mContext, mHandlerCaller, mUserId, mLock,
- sessionId, uid, activityToken, appCallbackToken, hasCallback,
- mUiLatencyHistory, mInfo.getServiceInfo().getComponentName(), componentName, flags);
+ sessionId, uid, activityToken, appCallbackToken, hasCallback, mUiLatencyHistory,
+ mWtfHistory, mInfo.getServiceInfo().getComponentName(), componentName, flags);
mSessions.put(newSession.id, newSession);
return newSession;
@@ -720,10 +732,33 @@ final class AutofillManagerServiceImpl {
@Nullable ArrayList<String> changedDatasetIds,
@Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
@Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
+ @NonNull String appPackageName) {
+ logContextCommittedLocked(sessionId, clientState, selectedDatasets, ignoredDatasets,
+ changedFieldIds, changedDatasetIds, manuallyFilledFieldIds,
+ manuallyFilledDatasetIds, null, null, appPackageName);
+ }
+
+ 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 ArrayList<AutofillId> detectedFieldIdsList,
@Nullable ArrayList<FieldClassification> detectedFieldClassificationsList,
@NonNull String appPackageName) {
if (isValidEventLocked("logDatasetNotSelected()", sessionId)) {
+ if (sVerbose) {
+ Slog.v(TAG, "logContextCommitted() with FieldClassification: id=" + sessionId
+ + ", selectedDatasets=" + selectedDatasets
+ + ", ignoredDatasetIds=" + ignoredDatasets
+ + ", changedAutofillIds=" + changedFieldIds
+ + ", changedDatasetIds=" + changedDatasetIds
+ + ", manuallyFilledFieldIds=" + manuallyFilledFieldIds
+ + ", detectedFieldIds=" + detectedFieldIdsList
+ + ", detectedFieldClassifications=" + detectedFieldClassificationsList);
+ }
AutofillId[] detectedFieldsIds = null;
FieldClassification[] detectedFieldClassifications = null;
if (detectedFieldIdsList != null) {
@@ -799,14 +834,15 @@ final class AutofillManagerServiceImpl {
// 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));
+ if (!isCalledByServiceLocked("setUserData", callingUid)) {
+ return;
}
+ mUserData = userData;
+ // Log it
+ int numberFields = mUserData == null ? 0: mUserData.getRemoteIds().length;
+ mMetricsLogger.write(Helper.newLogMaker(MetricsEvent.AUTOFILL_USERDATA_UPDATED,
+ getServicePackageName(), null)
+ .setCounterValue(numberFields));
}
}
@@ -917,6 +953,9 @@ final class AutofillManagerServiceImpl {
pw.println();
mUserData.dump(prefix2, pw);
}
+
+ pw.print(prefix); pw.println("Field Classification strategy: ");
+ mFieldClassificationStrategy.dump(prefix2, pw);
}
void destroySessionsLocked() {
@@ -964,11 +1003,13 @@ final class AutofillManagerServiceImpl {
final IAutoFillManagerClient client = clients.getBroadcastItem(i);
try {
final boolean resetSession;
+ final boolean isEnabled;
synchronized (mLock) {
resetSession = resetClient || isClientSessionDestroyedLocked(client);
+ isEnabled = isEnabledLocked();
}
int flags = 0;
- if (isEnabled()) {
+ if (isEnabled) {
flags |= AutofillManager.SET_STATE_FLAG_ENABLED;
}
if (resetSession) {
@@ -1004,7 +1045,7 @@ final class AutofillManagerServiceImpl {
return true;
}
- boolean isEnabled() {
+ boolean isEnabledLocked() {
return mSetupComplete && mInfo != null && !mDisabled;
}
@@ -1093,9 +1134,9 @@ final class AutofillManagerServiceImpl {
}
// Called by AutofillManager, checks UID.
- boolean isFieldClassificationEnabled(int uid) {
+ boolean isFieldClassificationEnabled(int callingUid) {
synchronized (mLock) {
- if (!isCalledByServiceLocked("isFieldClassificationEnabled", uid)) {
+ if (!isCalledByServiceLocked("isFieldClassificationEnabled", callingUid)) {
return false;
}
return isFieldClassificationEnabledLocked();
@@ -1110,6 +1151,28 @@ final class AutofillManagerServiceImpl {
mUserId) == 1;
}
+ FieldClassificationStrategy getFieldClassificationStrategy() {
+ return mFieldClassificationStrategy;
+ }
+
+ String[] getAvailableFieldClassificationAlgorithms(int callingUid) {
+ synchronized (mLock) {
+ if (!isCalledByServiceLocked("getFCAlgorithms()", callingUid)) {
+ return null;
+ }
+ }
+ return mFieldClassificationStrategy.getAvailableAlgorithms();
+ }
+
+ String getDefaultFieldClassificationAlgorithm(int callingUid) {
+ synchronized (mLock) {
+ if (!isCalledByServiceLocked("getDefaultFCAlgorithm()", callingUid)) {
+ return null;
+ }
+ }
+ return mFieldClassificationStrategy.getDefaultAlgorithm();
+ }
+
@Override
public String toString() {
return "AutofillManagerServiceImpl: [userId=" + mUserId
diff --git a/com/android/server/autofill/AutofillManagerServiceShellCommand.java b/com/android/server/autofill/AutofillManagerServiceShellCommand.java
index f3de557e..4d69ef95 100644
--- a/com/android/server/autofill/AutofillManagerServiceShellCommand.java
+++ b/com/android/server/autofill/AutofillManagerServiceShellCommand.java
@@ -16,11 +16,15 @@
package com.android.server.autofill;
+import static android.service.autofill.AutofillFieldClassificationService.EXTRA_SCORES;
+
import static com.android.server.autofill.AutofillManagerService.RECEIVER_BUNDLE_EXTRA_SESSIONS;
import android.os.Bundle;
+import android.os.RemoteCallback;
import android.os.ShellCommand;
import android.os.UserHandle;
+import android.service.autofill.AutofillFieldClassificationService.Scores;
import android.view.autofill.AutofillManager;
import com.android.internal.os.IResultReceiver;
@@ -80,13 +84,16 @@ public final class AutofillManagerServiceShellCommand extends ShellCommand {
pw.println(" Sets the maximum number of partitions per session.");
pw.println("");
pw.println(" list sessions [--user USER_ID]");
- pw.println(" List all pending sessions.");
+ pw.println(" Lists all pending sessions.");
pw.println("");
pw.println(" destroy sessions [--user USER_ID]");
- pw.println(" Destroy all pending sessions.");
+ pw.println(" Destroys all pending sessions.");
pw.println("");
pw.println(" reset");
- pw.println(" Reset all pending sessions and cached service connections.");
+ pw.println(" Resets all pending sessions and cached service connections.");
+ pw.println("");
+ pw.println(" get fc_score [--algorithm ALGORITHM] value1 value2");
+ pw.println(" Gets the field classification score for 2 fields.");
pw.println("");
}
}
@@ -98,6 +105,8 @@ public final class AutofillManagerServiceShellCommand extends ShellCommand {
return getLogLevel(pw);
case "max_partitions":
return getMaxPartitions(pw);
+ case "fc_score":
+ return getFieldClassificationScore(pw);
default:
pw.println("Invalid set: " + what);
return -1;
@@ -164,6 +173,32 @@ public final class AutofillManagerServiceShellCommand extends ShellCommand {
return 0;
}
+ private int getFieldClassificationScore(PrintWriter pw) {
+ final String nextArg = getNextArgRequired();
+ final String algorithm, value1;
+ if ("--algorithm".equals(nextArg)) {
+ algorithm = getNextArgRequired();
+ value1 = getNextArgRequired();
+ } else {
+ algorithm = null;
+ value1 = nextArg;
+ }
+ final String value2 = getNextArgRequired();
+
+ final CountDownLatch latch = new CountDownLatch(1);
+ mService.getScore(algorithm, value1, value2, new RemoteCallback((result) -> {
+ final Scores scores = result.getParcelable(EXTRA_SCORES);
+ if (scores == null) {
+ pw.println("no score");
+ } else {
+ pw.println(scores.scores[0][0]);
+ }
+ latch.countDown();
+ }));
+
+ return waitForLatch(pw, latch);
+ }
+
private int requestDestroy(PrintWriter pw) {
if (!isNextArgSessions(pw)) {
return -1;
@@ -210,19 +245,13 @@ public final class AutofillManagerServiceShellCommand extends ShellCommand {
return true;
}
- private boolean isNextArgLogLevel(PrintWriter pw, String cmd) {
- final String type = getNextArgRequired();
- if (!type.equals("log_level")) {
- pw.println("Error: invalid " + cmd + " type: " + type);
- return false;
- }
- return true;
- }
-
private int requestSessionCommon(PrintWriter pw, CountDownLatch latch,
Runnable command) {
command.run();
+ return waitForLatch(pw, latch);
+ }
+ private int waitForLatch(PrintWriter pw, CountDownLatch latch) {
try {
final boolean received = latch.await(5, TimeUnit.SECONDS);
if (!received) {
diff --git a/com/android/server/autofill/FieldClassificationStrategy.java b/com/android/server/autofill/FieldClassificationStrategy.java
new file mode 100644
index 00000000..da522010
--- /dev/null
+++ b/com/android/server/autofill/FieldClassificationStrategy.java
@@ -0,0 +1,289 @@
+/*
+ * 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 com.android.server.autofill;
+
+import static android.view.autofill.AutofillManager.FC_SERVICE_TIMEOUT;
+
+import static com.android.server.autofill.Helper.sDebug;
+import static com.android.server.autofill.Helper.sVerbose;
+import static android.service.autofill.AutofillFieldClassificationService.SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS;
+import static android.service.autofill.AutofillFieldClassificationService.SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM;
+
+import android.Manifest;
+import android.annotation.MainThread;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+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.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.content.res.Resources;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.os.RemoteCallback;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.autofill.AutofillFieldClassificationService;
+import android.service.autofill.IAutofillFieldClassificationService;
+import android.util.Log;
+import android.util.Slog;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Strategy used to bridge the field classification algorithms provided by a service in an external
+ * package.
+ */
+//TODO(b/70291841): add unit tests ?
+final class FieldClassificationStrategy {
+
+ private static final String TAG = "FieldClassificationStrategy";
+
+ private final Context mContext;
+ private final Object mLock = new Object();
+ private final int mUserId;
+
+ @GuardedBy("mLock")
+ private ServiceConnection mServiceConnection;
+
+ @GuardedBy("mLock")
+ private IAutofillFieldClassificationService mRemoteService;
+
+ @GuardedBy("mLock")
+ private ArrayList<Command> mQueuedCommands;
+
+ public FieldClassificationStrategy(Context context, int userId) {
+ mContext = context;
+ mUserId = userId;
+ }
+
+ @Nullable
+ private ServiceInfo getServiceInfo() {
+ final String packageName =
+ mContext.getPackageManager().getServicesSystemSharedLibraryPackageName();
+ if (packageName == null) {
+ Slog.w(TAG, "no external services package!");
+ return null;
+ }
+
+ final Intent intent = new Intent(AutofillFieldClassificationService.SERVICE_INTERFACE);
+ intent.setPackage(packageName);
+ final ResolveInfo resolveInfo = mContext.getPackageManager().resolveService(intent,
+ PackageManager.GET_SERVICES | PackageManager.GET_META_DATA);
+ if (resolveInfo == null || resolveInfo.serviceInfo == null) {
+ Slog.w(TAG, "No valid components found.");
+ return null;
+ }
+ return resolveInfo.serviceInfo;
+ }
+
+ @Nullable
+ private ComponentName getServiceComponentName() {
+ final ServiceInfo serviceInfo = getServiceInfo();
+ if (serviceInfo == null) return null;
+
+ final ComponentName name = new ComponentName(serviceInfo.packageName, serviceInfo.name);
+ if (!Manifest.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE
+ .equals(serviceInfo.permission)) {
+ Slog.w(TAG, name.flattenToShortString() + " does not require permission "
+ + Manifest.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE);
+ return null;
+ }
+
+ if (sVerbose) Slog.v(TAG, "getServiceComponentName(): " + name);
+ return name;
+ }
+
+ /**
+ * Run a command, starting the service connection if necessary.
+ */
+ private void connectAndRun(@NonNull Command command) {
+ synchronized (mLock) {
+ if (mRemoteService != null) {
+ try {
+ if (sVerbose) Slog.v(TAG, "running command right away");
+ command.run(mRemoteService);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "exception calling service: " + e);
+ }
+ return;
+ } else {
+ if (sDebug) Slog.d(TAG, "service is null; queuing command");
+ if (mQueuedCommands == null) {
+ mQueuedCommands = new ArrayList<>(1);
+ }
+ mQueuedCommands.add(command);
+ // If we're already connected, don't create a new connection, just leave - the
+ // command will be run when the service connects
+ if (mServiceConnection != null) return;
+ }
+
+ if (sVerbose) Slog.v(TAG, "creating connection");
+
+ // Create the connection
+ mServiceConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (sVerbose) Slog.v(TAG, "onServiceConnected(): " + name);
+ synchronized (mLock) {
+ mRemoteService = IAutofillFieldClassificationService.Stub
+ .asInterface(service);
+ if (mQueuedCommands != null) {
+ final int size = mQueuedCommands.size();
+ if (sDebug) Slog.d(TAG, "running " + size + " queued commands");
+ for (int i = 0; i < size; i++) {
+ final Command queuedCommand = mQueuedCommands.get(i);
+ try {
+ if (sVerbose) Slog.v(TAG, "running queued command #" + i);
+ queuedCommand.run(mRemoteService);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "exception calling " + name + ": " + e);
+ }
+ }
+ mQueuedCommands = null;
+ } else if (sDebug) Slog.d(TAG, "no queued commands");
+ }
+ }
+
+ @Override
+ @MainThread
+ public void onServiceDisconnected(ComponentName name) {
+ if (sVerbose) Slog.v(TAG, "onServiceDisconnected(): " + name);
+ synchronized (mLock) {
+ mRemoteService = null;
+ }
+ }
+
+ @Override
+ public void onBindingDied(ComponentName name) {
+ if (sVerbose) Slog.v(TAG, "onBindingDied(): " + name);
+ synchronized (mLock) {
+ mRemoteService = null;
+ }
+ }
+
+ @Override
+ public void onNullBinding(ComponentName name) {
+ if (sVerbose) Slog.v(TAG, "onNullBinding(): " + name);
+ synchronized (mLock) {
+ mRemoteService = null;
+ }
+ }
+ };
+
+ final ComponentName component = getServiceComponentName();
+ if (sVerbose) Slog.v(TAG, "binding to: " + component);
+ if (component != null) {
+ final Intent intent = new Intent();
+ intent.setComponent(component);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ mContext.bindServiceAsUser(intent, mServiceConnection, Context.BIND_AUTO_CREATE,
+ UserHandle.of(mUserId));
+ if (sVerbose) Slog.v(TAG, "bound");
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ }
+ }
+
+ /**
+ * Gets the name of all available algorithms.
+ */
+ @Nullable
+ String[] getAvailableAlgorithms() {
+ return getMetadataValue(SERVICE_META_DATA_KEY_AVAILABLE_ALGORITHMS,
+ (res, id) -> res.getStringArray(id));
+ }
+
+ /**
+ * Gets the default algorithm that's used when an algorithm is not specified or is invalid.
+ */
+ @Nullable
+ String getDefaultAlgorithm() {
+ return getMetadataValue(SERVICE_META_DATA_KEY_DEFAULT_ALGORITHM, (res, id) -> res.getString(id));
+ }
+
+ @Nullable
+ private <T> T getMetadataValue(String field, MetadataParser<T> parser) {
+ final ServiceInfo serviceInfo = getServiceInfo();
+ if (serviceInfo == null) return null;
+
+ final PackageManager pm = mContext.getPackageManager();
+
+ final Resources res;
+ try {
+ res = pm.getResourcesForApplication(serviceInfo.applicationInfo);
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "Error getting application resources for " + serviceInfo, e);
+ return null;
+ }
+
+ final int resourceId = serviceInfo.metaData.getInt(field);
+ return parser.get(res, resourceId);
+ }
+
+ //TODO(b/70291841): rename this method (and all others in the chain) to something like
+ // calculateScores() ?
+ void getScores(RemoteCallback callback, @Nullable String algorithmName,
+ @Nullable Bundle algorithmArgs, @NonNull List<AutofillValue> actualValues,
+ @NonNull String[] userDataValues) {
+ connectAndRun((service) -> service.getScores(callback, algorithmName,
+ algorithmArgs, actualValues, userDataValues));
+ }
+
+ void dump(String prefix, PrintWriter pw) {
+ final ComponentName impl = getServiceComponentName();
+ pw.print(prefix); pw.print("User ID: "); pw.println(mUserId);
+ pw.print(prefix); pw.print("Queued commands: ");
+ if (mQueuedCommands == null) {
+ pw.println("N/A");
+ } else {
+ pw.println(mQueuedCommands.size());
+ }
+ pw.print(prefix); pw.print("Implementation: ");
+ if (impl == null) {
+ pw.println("N/A");
+ return;
+ }
+ pw.println(impl.flattenToShortString());
+
+ pw.print(prefix); pw.print("Available algorithms: ");
+ pw.println(Arrays.toString(getAvailableAlgorithms()));
+ pw.print(prefix); pw.print("Default algorithm: "); pw.println(getDefaultAlgorithm());
+ }
+
+ private static interface Command {
+ void run(IAutofillFieldClassificationService service) throws RemoteException;
+ }
+
+ private static interface MetadataParser<T> {
+ T get(Resources res, int resId);
+ }
+}
diff --git a/com/android/server/autofill/Session.java b/com/android/server/autofill/Session.java
index 01f90840..6b44fa5e 100644
--- a/com/android/server/autofill/Session.java
+++ b/com/android/server/autofill/Session.java
@@ -18,6 +18,7 @@ package com.android.server.autofill;
import static android.app.ActivityManagerInternal.ASSIST_KEY_RECEIVER_EXTRAS;
import static android.app.ActivityManagerInternal.ASSIST_KEY_STRUCTURE;
+import static android.service.autofill.AutofillFieldClassificationService.EXTRA_SCORES;
import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST;
import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
import static android.view.autofill.AutofillManager.ACTION_START_SESSION;
@@ -29,7 +30,6 @@ import static com.android.server.autofill.Helper.sDebug;
import static com.android.server.autofill.Helper.sPartitionMaxCount;
import static com.android.server.autofill.Helper.sVerbose;
import static com.android.server.autofill.Helper.toArray;
-import static com.android.server.autofill.ViewState.STATE_AUTOFILLED;
import static com.android.server.autofill.ViewState.STATE_RESTARTED_SESSION;
import android.annotation.NonNull;
@@ -51,10 +51,13 @@ import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcelable;
+import android.os.RemoteCallback;
import android.os.RemoteException;
import android.os.SystemClock;
+import android.service.autofill.AutofillFieldClassificationService.Scores;
import android.service.autofill.AutofillService;
import android.service.autofill.Dataset;
+import android.service.autofill.FieldClassification;
import android.service.autofill.FieldClassification.Match;
import android.service.autofill.FillContext;
import android.service.autofill.FillRequest;
@@ -65,7 +68,6 @@ 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;
@@ -90,6 +92,7 @@ import com.android.server.autofill.ui.PendingUi;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -207,6 +210,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
@GuardedBy("mLock")
private final LocalLog mUiLatencyHistory;
+ @GuardedBy("mLock")
+ private final LocalLog mWtfHistory;
+
/**
* Receiver of assist data from the app's {@link Activity}.
*/
@@ -238,7 +244,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
// ONE_WAY warning because system_service could block on app calls. We need to
// change AssistStructure so it provides a "one-way" writeToParcel() method that
// sends all the data
- structure.ensureData();
+ try {
+ structure.ensureData();
+ } catch (RuntimeException e) {
+ wtf(e, "Exception lazy loading assist structure for %s: %s",
+ structure.getActivityComponent(), e);
+ return;
+ }
// Sanitize structure before it's sent to service.
final ComponentName componentNameFromApp = structure.getActivityComponent();
@@ -444,6 +456,7 @@ 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 LocalLog wtfHistory,
@NonNull ComponentName serviceComponentName, @NonNull ComponentName componentName,
int flags) {
id = sessionId;
@@ -458,6 +471,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
mActivityToken = activityToken;
mHasCallback = hasCallback;
mUiLatencyHistory = uiLatencyHistory;
+ mWtfHistory = wtfHistory;
mComponentName = componentName;
mClient = IAutoFillManagerClient.Stub.asInterface(client);
@@ -970,18 +984,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
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 {
- detectedFieldIds = null;
- detectedFieldClassifications = null;
- }
-
for (int i = 0; i < mViewStates.size(); i++) {
final ViewState viewState = mViewStates.valueAt(i);
final int state = viewState.getState();
@@ -1086,31 +1088,14 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
} // for j
}
- // Sets field classification score for field
- if (userData!= null) {
- setScore(detectedFieldIds, detectedFieldClassifications, userData,
- viewState.id, currentValue);
- }
} // else
} // else
}
- if (sVerbose) {
- Slog.v(TAG, "logContextCommitted(): id=" + id
- + ", selectedDatasetids=" + mSelectedDatasetIds
- + ", ignoredDatasetIds=" + ignoredDatasets
- + ", changedAutofillIds=" + changedFieldIds
- + ", changedDatasetIds=" + changedDatasetIds
- + ", manuallyFilledIds=" + manuallyFilledIds
- + ", detectedFieldIds=" + detectedFieldIds
- + ", detectedFieldClassifications=" + detectedFieldClassifications
- );
- }
-
ArrayList<AutofillId> manuallyFilledFieldIds = null;
ArrayList<ArrayList<String>> manuallyFilledDatasetIds = null;
- // Must "flatten" the map to the parcellable collection primitives
+ // Must "flatten" the map to the parcelable collection primitives
if (manuallyFilledIds != null) {
final int size = manuallyFilledIds.size();
manuallyFilledFieldIds = new ArrayList<>(size);
@@ -1123,20 +1108,32 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
}
- mService.logContextCommittedLocked(id, mClientState, mSelectedDatasetIds, ignoredDatasets,
- changedFieldIds, changedDatasetIds,
- manuallyFilledFieldIds, manuallyFilledDatasetIds,
- detectedFieldIds, detectedFieldClassifications, mComponentName.getPackageName());
+ // Sets field classification scores
+ final FieldClassificationStrategy fcStrategy = mService.getFieldClassificationStrategy();
+ if (userData != null && fcStrategy != null) {
+ logFieldClassificationScoreLocked(fcStrategy, ignoredDatasets, changedFieldIds,
+ changedDatasetIds, manuallyFilledFieldIds, manuallyFilledDatasetIds,
+ userData, mViewStates.values());
+ } else {
+ mService.logContextCommittedLocked(id, mClientState, mSelectedDatasetIds,
+ ignoredDatasets, changedFieldIds, changedDatasetIds,
+ manuallyFilledFieldIds, manuallyFilledDatasetIds,
+ 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) {
+ private void logFieldClassificationScoreLocked(
+ @NonNull FieldClassificationStrategy fcStrategy,
+ @NonNull ArraySet<String> ignoredDatasets,
+ @NonNull ArrayList<AutofillId> changedFieldIds,
+ @NonNull ArrayList<String> changedDatasetIds,
+ @NonNull ArrayList<AutofillId> manuallyFilledFieldIds,
+ @NonNull ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
+ @NonNull UserData userData, @NonNull Collection<ViewState> viewStates) {
final String[] userValues = userData.getValues();
final String[] remoteIds = userData.getRemoteIds();
@@ -1150,26 +1147,80 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
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);
+ final int maxFieldsSize = UserData.getMaxFieldClassificationIdsSize();
+
+ final ArrayList<AutofillId> detectedFieldIds = new ArrayList<>(maxFieldsSize);
+ final ArrayList<FieldClassification> detectedFieldClassifications = new ArrayList<>(
+ maxFieldsSize);
+
+ final String algorithm = userData.getFieldClassificationAlgorithm();
+ final Bundle algorithmArgs = userData.getAlgorithmArgs();
+ final int viewsSize = viewStates.size();
+
+ // First, we get all scores.
+ final AutofillId[] fieldIds = new AutofillId[viewsSize];
+ final ArrayList<AutofillValue> currentValues = new ArrayList<>(viewsSize);
+ int k = 0;
+ for (ViewState viewState : viewStates) {
+ currentValues.add(viewState.getCurrentValue());
+ fieldIds[k++] = viewState.id;
+ }
+
+ // Then use the results, asynchronously
+ final RemoteCallback callback = new RemoteCallback((result) -> {
+ if (result == null) {
+ if (sDebug) Slog.d(TAG, "setFieldClassificationScore(): no results");
+ mService.logContextCommittedLocked(id, mClientState, mSelectedDatasetIds,
+ ignoredDatasets, changedFieldIds, changedDatasetIds,
+ manuallyFilledFieldIds, manuallyFilledDatasetIds,
+ mComponentName.getPackageName());
+ return;
+ }
+ final Scores scores = result.getParcelable(EXTRA_SCORES);
+ if (scores == null) {
+ Slog.w(TAG, "No field classification score on " + result);
+ return;
+ }
+ int i = 0, j = 0;
+ try {
+ for (i = 0; i < viewsSize; i++) {
+ final AutofillId fieldId = fieldIds[i];
+
+ ArrayList<Match> matches = null;
+ for (j = 0; j < userValues.length; j++) {
+ String remoteId = remoteIds[j];
+ final float score = scores.scores[i][j];
+ if (score > 0) {
+ if (sVerbose) {
+ Slog.v(TAG, "adding score " + score + " at index " + j + " 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 " + j + " and id " + fieldId);
+ }
+ }
+ if (matches != null) {
+ detectedFieldIds.add(fieldId);
+ detectedFieldClassifications.add(new FieldClassification(matches));
+ }
}
- matches.add(new Match(remoteId, score));
+ } catch (ArrayIndexOutOfBoundsException e) {
+ wtf(e, "Error accessing FC score at [%d, %d] (%s): %s", i, j, scores, e);
+ return;
}
- 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));
- }
+
+ mService.logContextCommittedLocked(id, mClientState, mSelectedDatasetIds,
+ ignoredDatasets, changedFieldIds, changedDatasetIds, manuallyFilledFieldIds,
+ manuallyFilledDatasetIds, detectedFieldIds, detectedFieldClassifications,
+ mComponentName.getPackageName());
+ });
+
+ fcStrategy.getScores(callback, algorithm, algorithmArgs, currentValues, userValues);
}
/**
@@ -1809,7 +1860,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
mUiLatencyHistory.log(historyLog.toString());
final LogMaker metricsLog = newLogMaker(MetricsEvent.AUTOFILL_UI_LATENCY)
- .setCounterValue((int) duration);
+ .addTaggedData(MetricsEvent.FIELD_AUTOFILL_DURATION, duration);
mMetricsLogger.write(metricsLog);
}
}
@@ -2108,9 +2159,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
final Intent fillInIntent = new Intent();
final FillContext context = getFillContextByRequestIdLocked(requestId);
+
if (context == null) {
- Slog.wtf(TAG, "createAuthFillInIntentLocked(): no FillContext. requestId=" + requestId
- + "; mContexts= " + mContexts);
+ wtf(null, "createAuthFillInIntentLocked(): no FillContext. requestId=%d; mContexts=%s",
+ requestId, mContexts);
return null;
}
fillInIntent.putExtra(AutofillManager.EXTRA_ASSIST_STRUCTURE, context.getStructure());
@@ -2375,4 +2427,15 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
private void writeLog(int category) {
mMetricsLogger.write(newLogMaker(category));
}
+
+ private void wtf(@Nullable Exception e, String fmt, Object...args) {
+ final String message = String.format(fmt, args);
+ mWtfHistory.log(message);
+
+ if (e != null) {
+ Slog.wtf(TAG, message, e);
+ } else {
+ Slog.wtf(TAG, message);
+ }
+ }
}
diff --git a/com/android/server/autofill/ui/SaveUi.java b/com/android/server/autofill/ui/SaveUi.java
index 307f74d3..f96fa7c2 100644
--- a/com/android/server/autofill/ui/SaveUi.java
+++ b/com/android/server/autofill/ui/SaveUi.java
@@ -231,8 +231,7 @@ final class SaveUi {
final Window window = mDialog.getWindow();
window.setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY);
- window.addFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
- | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
+ window.addFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
| WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH);
window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS);
diff --git a/com/android/server/backup/BackupManagerConstants.java b/com/android/server/backup/BackupManagerConstants.java
index 537592e3..1a54e957 100644
--- a/com/android/server/backup/BackupManagerConstants.java
+++ b/com/android/server/backup/BackupManagerConstants.java
@@ -85,9 +85,6 @@ class BackupManagerConstants extends ContentObserver {
super(handler);
mResolver = resolver;
updateSettings();
- }
-
- public void start() {
mResolver.registerContentObserver(
Settings.Secure.getUriFor(Settings.Secure.BACKUP_MANAGER_CONSTANTS), false, this);
}
@@ -121,7 +118,7 @@ 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(
+ String backupFinishedNotificationReceivers = mParser.getString(
BACKUP_FINISHED_NOTIFICATION_RECEIVERS,
DEFAULT_BACKUP_FINISHED_NOTIFICATION_RECEIVERS);
if (backupFinishedNotificationReceivers.isEmpty()) {
@@ -190,6 +187,9 @@ class BackupManagerConstants extends ContentObserver {
return mFullBackupRequiredNetworkType;
}
+ /**
+ * Returns an array of package names that should be notified whenever a backup finishes.
+ */
public synchronized String[] getBackupFinishedNotificationReceivers() {
if (RefactoredBackupManagerService.DEBUG_SCHEDULING) {
Slog.v(TAG, "getBackupFinishedNotificationReceivers(...) returns "
diff --git a/com/android/server/backup/BackupManagerConstantsTest.java b/com/android/server/backup/BackupManagerConstantsTest.java
index c20c3767..c397f23b 100644
--- a/com/android/server/backup/BackupManagerConstantsTest.java
+++ b/com/android/server/backup/BackupManagerConstantsTest.java
@@ -16,19 +16,17 @@
package com.android.server.backup;
+import static com.google.common.truth.Truth.assertThat;
+
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;
@@ -36,19 +34,9 @@ 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})
+@Config(manifest = Config.NONE, sdk = 26)
+@SystemLoaderClasses({BackupManagerConstants.class})
@Presubmit
public class BackupManagerConstantsTest {
private static final String PACKAGE_NAME = "some.package.name";
@@ -59,21 +47,16 @@ public class BackupManagerConstantsTest {
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);
+ 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);
@@ -93,17 +76,20 @@ public class BackupManagerConstantsTest {
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 String recieversSetting =
+ "backup_finished_notification_receivers="
+ + PACKAGE_NAME
+ + ':'
+ + ANOTHER_PACKAGE_NAME;
+ Settings.Secure.putString(
+ context.getContentResolver(),
+ Settings.Secure.BACKUP_MANAGER_CONSTANTS,
+ recieversSetting);
final BackupManagerConstants constants =
new BackupManagerConstants(handler, context.getContentResolver());
- constants.start();
- assertThat(constants.getBackupFinishedNotificationReceivers()).isEqualTo(new String[] {
- PACKAGE_NAME,
- ANOTHER_PACKAGE_NAME});
+ assertThat(constants.getBackupFinishedNotificationReceivers())
+ .isEqualTo(new String[] {PACKAGE_NAME, ANOTHER_PACKAGE_NAME});
}
}
diff --git a/com/android/server/backup/BackupManagerServiceInterface.java b/com/android/server/backup/BackupManagerServiceInterface.java
index 86462d85..7b021c64 100644
--- a/com/android/server/backup/BackupManagerServiceInterface.java
+++ b/com/android/server/backup/BackupManagerServiceInterface.java
@@ -186,6 +186,8 @@ public interface BackupManagerServiceInterface {
boolean isAppEligibleForBackup(String packageName);
+ String[] filterAppsEligibleForBackup(String[] packages);
+
void dump(FileDescriptor fd, PrintWriter pw, String[] args);
IBackupManager getBackupManagerBinder();
diff --git a/com/android/server/backup/BackupManagerServiceTest.java b/com/android/server/backup/BackupManagerServiceTest.java
new file mode 100644
index 00000000..e072800e
--- /dev/null
+++ b/com/android/server/backup/BackupManagerServiceTest.java
@@ -0,0 +1,679 @@
+/*
+ * 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 com.android.server.backup;
+
+import static com.android.server.backup.testing.TransportData.backupTransport;
+import static com.android.server.backup.testing.TransportData.d2dTransport;
+import static com.android.server.backup.testing.TransportData.localTransport;
+import static com.android.server.backup.testing.TransportTestUtils.setUpCurrentTransport;
+import static com.android.server.backup.testing.TransportTestUtils.setUpTransports;
+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;
+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 static org.robolectric.Shadows.shadowOf;
+import static org.testng.Assert.expectThrows;
+
+import android.app.backup.BackupManager;
+import android.app.backup.ISelectBackupTransportCallback;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.os.HandlerThread;
+import android.platform.test.annotations.Presubmit;
+import android.provider.Settings;
+import com.android.server.backup.testing.ShadowAppBackupUtils;
+import com.android.server.backup.testing.ShadowBackupPolicyEnforcer;
+import com.android.server.backup.testing.TransportData;
+import com.android.server.backup.testing.TransportTestUtils.TransportMock;
+import com.android.server.backup.transport.TransportNotRegisteredException;
+import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.SystemLoaderClasses;
+import java.io.File;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.junit.After;
+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 org.robolectric.shadows.ShadowContextWrapper;
+import org.robolectric.shadows.ShadowLog;
+import org.robolectric.shadows.ShadowLooper;
+import org.robolectric.shadows.ShadowPackageManager;
+import org.robolectric.shadows.ShadowSettings;
+import org.robolectric.shadows.ShadowSystemClock;
+
+@RunWith(FrameworkRobolectricTestRunner.class)
+@Config(
+ manifest = Config.NONE,
+ sdk = 26,
+ shadows = {ShadowAppBackupUtils.class, ShadowBackupPolicyEnforcer.class}
+)
+@SystemLoaderClasses({RefactoredBackupManagerService.class, TransportManager.class})
+@Presubmit
+public class BackupManagerServiceTest {
+ private static final String TAG = "BMSTest";
+
+ @Mock private TransportManager mTransportManager;
+ private HandlerThread mBackupThread;
+ private ShadowLooper mShadowBackupLooper;
+ private File mBaseStateDir;
+ private File mDataDir;
+ private ShadowContextWrapper mShadowContext;
+ private Context mContext;
+ private TransportData mTransport;
+ private String mTransportName;
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+
+ mTransport = backupTransport();
+ mTransportName = mTransport.transportName;
+
+ mBackupThread = new HandlerThread("backup-test");
+ mBackupThread.setUncaughtExceptionHandler(
+ (t, e) -> ShadowLog.e(TAG, "Uncaught exception in test thread " + t.getName(), e));
+ mBackupThread.start();
+ mShadowBackupLooper = shadowOf(mBackupThread.getLooper());
+
+ ContextWrapper context = RuntimeEnvironment.application;
+ mContext = context;
+ mShadowContext = shadowOf(context);
+
+ File cacheDir = mContext.getCacheDir();
+ mBaseStateDir = new File(cacheDir, "base_state_dir");
+ mDataDir = new File(cacheDir, "data_dir");
+
+ ShadowBackupPolicyEnforcer.setMandatoryBackupTransport(null);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ mBackupThread.quit();
+ ShadowAppBackupUtils.reset();
+ ShadowBackupPolicyEnforcer.setMandatoryBackupTransport(null);
+ }
+
+ /* Tests for destination string */
+
+ @Test
+ public void testDestinationString() throws Exception {
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ when(mTransportManager.getTransportCurrentDestinationString(eq(mTransportName)))
+ .thenReturn("destinationString");
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ String destination = backupManagerService.getDestinationString(mTransportName);
+
+ assertThat(destination).isEqualTo("destinationString");
+ }
+
+ @Test
+ public void testDestinationString_whenTransportNotRegistered() throws Exception {
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ when(mTransportManager.getTransportCurrentDestinationString(eq(mTransportName)))
+ .thenThrow(TransportNotRegisteredException.class);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ String destination = backupManagerService.getDestinationString(mTransportName);
+
+ assertThat(destination).isNull();
+ }
+
+ @Test
+ public void testDestinationString_withoutPermission() throws Exception {
+ mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+ when(mTransportManager.getTransportCurrentDestinationString(eq(mTransportName)))
+ .thenThrow(TransportNotRegisteredException.class);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ expectThrows(
+ SecurityException.class,
+ () -> backupManagerService.getDestinationString(mTransportName));
+ }
+
+ /* Tests for app eligibility */
+
+ @Test
+ public void testIsAppEligibleForBackup_whenAppEligible() throws Exception {
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ TransportMock transportMock = setUpCurrentTransport(mTransportManager, backupTransport());
+ ShadowAppBackupUtils.sAppIsRunningAndEligibleForBackupWithTransport = p -> true;
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ boolean result = backupManagerService.isAppEligibleForBackup("app.package");
+
+ assertThat(result).isTrue();
+
+ verify(mTransportManager)
+ .disposeOfTransportClient(eq(transportMock.transportClient), any());
+ }
+
+ @Test
+ public void testIsAppEligibleForBackup_whenAppNotEligible() throws Exception {
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ setUpCurrentTransport(mTransportManager, mTransport);
+ ShadowAppBackupUtils.sAppIsRunningAndEligibleForBackupWithTransport = p -> false;
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ boolean result = backupManagerService.isAppEligibleForBackup("app.package");
+
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void testIsAppEligibleForBackup_withoutPermission() throws Exception {
+ mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+ setUpCurrentTransport(mTransportManager, mTransport);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ expectThrows(
+ SecurityException.class,
+ () -> backupManagerService.isAppEligibleForBackup("app.package"));
+ }
+
+ @Test
+ public void testFilterAppsEligibleForBackup() throws Exception {
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ TransportMock transportMock = setUpCurrentTransport(mTransportManager, mTransport);
+ Map<String, Boolean> packagesMap = new HashMap<>();
+ packagesMap.put("package.a", true);
+ packagesMap.put("package.b", false);
+ ShadowAppBackupUtils.sAppIsRunningAndEligibleForBackupWithTransport = packagesMap::get;
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+ String[] packages = packagesMap.keySet().toArray(new String[packagesMap.size()]);
+
+ String[] filtered = backupManagerService.filterAppsEligibleForBackup(packages);
+
+ assertThat(filtered).asList().containsExactly("package.a");
+ verify(mTransportManager)
+ .disposeOfTransportClient(eq(transportMock.transportClient), any());
+ }
+
+ @Test
+ public void testFilterAppsEligibleForBackup_whenNoneIsEligible() throws Exception {
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ ShadowAppBackupUtils.sAppIsRunningAndEligibleForBackupWithTransport = p -> false;
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ String[] filtered =
+ backupManagerService.filterAppsEligibleForBackup(
+ new String[] {"package.a", "package.b"});
+
+ assertThat(filtered).isEmpty();
+ }
+
+ @Test
+ public void testFilterAppsEligibleForBackup_withoutPermission() throws Exception {
+ mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+ setUpCurrentTransport(mTransportManager, mTransport);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ expectThrows(
+ SecurityException.class,
+ () ->
+ backupManagerService.filterAppsEligibleForBackup(
+ new String[] {"package.a", "package.b"}));
+ }
+
+ /* Tests for select transport */
+
+ private ComponentName mNewTransportComponent;
+ private TransportData mNewTransport;
+ private TransportMock mNewTransportMock;
+ private ComponentName mOldTransportComponent;
+ private TransportData mOldTransport;
+ private TransportMock mOldTransportMock;
+
+ private void setUpForSelectTransport() throws Exception {
+ mNewTransport = backupTransport();
+ mNewTransportComponent = mNewTransport.getTransportComponent();
+ mOldTransport = d2dTransport();
+ mOldTransportComponent = mOldTransport.getTransportComponent();
+ List<TransportMock> transportMocks =
+ setUpTransports(mTransportManager, mNewTransport, mOldTransport, localTransport());
+ mNewTransportMock = transportMocks.get(0);
+ mOldTransportMock = transportMocks.get(1);
+ when(mTransportManager.selectTransport(eq(mNewTransport.transportName)))
+ .thenReturn(mOldTransport.transportName);
+ }
+
+ @Test
+ public void testSelectBackupTransport() throws Exception {
+ setUpForSelectTransport();
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ String oldTransport =
+ backupManagerService.selectBackupTransport(mNewTransport.transportName);
+
+ assertThat(getSettingsTransport()).isEqualTo(mNewTransport.transportName);
+ assertThat(oldTransport).isEqualTo(mOldTransport.transportName);
+ verify(mTransportManager)
+ .disposeOfTransportClient(eq(mNewTransportMock.transportClient), any());
+ }
+
+ @Test
+ public void testSelectBackupTransport_withoutPermission() throws Exception {
+ setUpForSelectTransport();
+ mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ expectThrows(
+ SecurityException.class,
+ () -> backupManagerService.selectBackupTransport(mNewTransport.transportName));
+ }
+
+ @Test
+ public void testSelectBackupTransportAsync() throws Exception {
+ setUpForSelectTransport();
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ when(mTransportManager.registerAndSelectTransport(eq(mNewTransportComponent)))
+ .thenReturn(BackupManager.SUCCESS);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+ ISelectBackupTransportCallback callback = mock(ISelectBackupTransportCallback.class);
+
+ backupManagerService.selectBackupTransportAsync(mNewTransportComponent, callback);
+
+ mShadowBackupLooper.runToEndOfTasks();
+ assertThat(getSettingsTransport()).isEqualTo(mNewTransport.transportName);
+ verify(callback).onSuccess(eq(mNewTransport.transportName));
+ verify(mTransportManager)
+ .disposeOfTransportClient(eq(mNewTransportMock.transportClient), any());
+ }
+
+ @Test
+ public void testSelectBackupTransportAsync_whenMandatoryTransport() throws Exception {
+ setUpForSelectTransport();
+ ShadowBackupPolicyEnforcer.setMandatoryBackupTransport(mNewTransportComponent);
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ when(mTransportManager.registerAndSelectTransport(eq(mNewTransportComponent)))
+ .thenReturn(BackupManager.SUCCESS);
+ ISelectBackupTransportCallback callback = mock(ISelectBackupTransportCallback.class);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ backupManagerService.selectBackupTransportAsync(mNewTransportComponent, callback);
+
+ mShadowBackupLooper.runToEndOfTasks();
+ assertThat(getSettingsTransport()).isEqualTo(mNewTransport.transportName);
+ verify(callback).onSuccess(eq(mNewTransport.transportName));
+ verify(mTransportManager)
+ .disposeOfTransportClient(eq(mNewTransportMock.transportClient), any());
+ }
+
+ @Test
+ public void testSelectBackupTransportAsync_whenOtherThanMandatoryTransport() throws Exception {
+ setUpForSelectTransport();
+ ShadowBackupPolicyEnforcer.setMandatoryBackupTransport(mOldTransportComponent);
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ when(mTransportManager.registerAndSelectTransport(eq(mNewTransportComponent)))
+ .thenReturn(BackupManager.SUCCESS);
+ ISelectBackupTransportCallback callback = mock(ISelectBackupTransportCallback.class);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ backupManagerService.selectBackupTransportAsync(mNewTransportComponent, callback);
+
+ mShadowBackupLooper.runToEndOfTasks();
+ assertThat(getSettingsTransport()).isNotEqualTo(mNewTransport.transportName);
+ verify(callback).onFailure(eq(BackupManager.ERROR_BACKUP_NOT_ALLOWED));
+ }
+
+ @Test
+ public void testSelectBackupTransportAsync_whenRegistrationFails() throws Exception {
+ setUpForSelectTransport();
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ when(mTransportManager.registerAndSelectTransport(eq(mNewTransportComponent)))
+ .thenReturn(BackupManager.ERROR_TRANSPORT_UNAVAILABLE);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+ ISelectBackupTransportCallback callback = mock(ISelectBackupTransportCallback.class);
+
+ backupManagerService.selectBackupTransportAsync(mNewTransportComponent, callback);
+
+ mShadowBackupLooper.runToEndOfTasks();
+ assertThat(getSettingsTransport()).isNotEqualTo(mNewTransport.transportName);
+ verify(callback).onFailure(anyInt());
+ }
+
+ @Test
+ public void testSelectBackupTransportAsync_whenTransportGetsUnregistered() throws Exception {
+ setUpTransports(mTransportManager, mTransport.unregistered());
+ ComponentName newTransportComponent = mTransport.getTransportComponent();
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ when(mTransportManager.registerAndSelectTransport(eq(newTransportComponent)))
+ .thenReturn(BackupManager.SUCCESS);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+ ISelectBackupTransportCallback callback = mock(ISelectBackupTransportCallback.class);
+
+ backupManagerService.selectBackupTransportAsync(newTransportComponent, callback);
+
+ mShadowBackupLooper.runToEndOfTasks();
+ assertThat(getSettingsTransport()).isNotEqualTo(mTransportName);
+ verify(callback).onFailure(anyInt());
+ }
+
+ @Test
+ public void testSelectBackupTransportAsync_withoutPermission() throws Exception {
+ setUpForSelectTransport();
+ mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+ ComponentName newTransportComponent = mNewTransport.getTransportComponent();
+
+ expectThrows(
+ SecurityException.class,
+ () ->
+ backupManagerService.selectBackupTransportAsync(
+ newTransportComponent, mock(ISelectBackupTransportCallback.class)));
+ }
+
+ private String getSettingsTransport() {
+ return ShadowSettings.ShadowSecure.getString(
+ mContext.getContentResolver(), Settings.Secure.BACKUP_TRANSPORT);
+ }
+
+ /* Tests for updating transport attributes */
+
+ private static final int PACKAGE_UID = 10;
+ private ComponentName mTransportComponent;
+ private int mTransportUid;
+
+ private void setUpForUpdateTransportAttributes() throws Exception {
+ mTransportComponent = mTransport.getTransportComponent();
+ String transportPackage = mTransportComponent.getPackageName();
+
+ ShadowPackageManager shadowPackageManager = shadowOf(mContext.getPackageManager());
+ shadowPackageManager.addPackage(transportPackage);
+ shadowPackageManager.setPackagesForUid(PACKAGE_UID, transportPackage);
+
+ mTransportUid = mContext.getPackageManager().getPackageUid(transportPackage, 0);
+ }
+
+ @Test
+ public void
+ testUpdateTransportAttributes_whenTransportUidEqualsToCallingUid_callsThroughToTransportManager()
+ throws Exception {
+ setUpForUpdateTransportAttributes();
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ Intent configurationIntent = new Intent();
+ Intent dataManagementIntent = new Intent();
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ backupManagerService.updateTransportAttributes(
+ mTransportUid,
+ mTransportComponent,
+ mTransportName,
+ configurationIntent,
+ "currentDestinationString",
+ dataManagementIntent,
+ "dataManagementLabel");
+
+ verify(mTransportManager)
+ .updateTransportAttributes(
+ eq(mTransportComponent),
+ eq(mTransportName),
+ eq(configurationIntent),
+ eq("currentDestinationString"),
+ eq(dataManagementIntent),
+ eq("dataManagementLabel"));
+ }
+
+ @Test
+ public void testUpdateTransportAttributes_whenTransportUidNotEqualToCallingUid_throwsException()
+ throws Exception {
+ setUpForUpdateTransportAttributes();
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ expectThrows(
+ SecurityException.class,
+ () ->
+ backupManagerService.updateTransportAttributes(
+ mTransportUid + 1,
+ mTransportComponent,
+ mTransportName,
+ new Intent(),
+ "currentDestinationString",
+ new Intent(),
+ "dataManagementLabel"));
+ }
+
+ @Test
+ public void testUpdateTransportAttributes_whenTransportComponentNull_throwsException()
+ throws Exception {
+ setUpForUpdateTransportAttributes();
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ expectThrows(
+ RuntimeException.class,
+ () ->
+ backupManagerService.updateTransportAttributes(
+ mTransportUid,
+ null,
+ mTransportName,
+ new Intent(),
+ "currentDestinationString",
+ new Intent(),
+ "dataManagementLabel"));
+ }
+
+ @Test
+ public void testUpdateTransportAttributes_whenNameNull_throwsException() throws Exception {
+ setUpForUpdateTransportAttributes();
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ expectThrows(
+ RuntimeException.class,
+ () ->
+ backupManagerService.updateTransportAttributes(
+ mTransportUid,
+ mTransportComponent,
+ null,
+ new Intent(),
+ "currentDestinationString",
+ new Intent(),
+ "dataManagementLabel"));
+ }
+
+ @Test
+ public void testUpdateTransportAttributes_whenCurrentDestinationStringNull_throwsException()
+ throws Exception {
+ setUpForUpdateTransportAttributes();
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ expectThrows(
+ RuntimeException.class,
+ () ->
+ backupManagerService.updateTransportAttributes(
+ mTransportUid,
+ mTransportComponent,
+ mTransportName,
+ new Intent(),
+ null,
+ new Intent(),
+ "dataManagementLabel"));
+ }
+
+ @Test
+ public void
+ testUpdateTransportAttributes_whenDataManagementArgumentsNullityDontMatch_throwsException()
+ throws Exception {
+ setUpForUpdateTransportAttributes();
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ expectThrows(
+ RuntimeException.class,
+ () ->
+ backupManagerService.updateTransportAttributes(
+ mTransportUid,
+ mTransportComponent,
+ mTransportName,
+ new Intent(),
+ "currentDestinationString",
+ null,
+ "dataManagementLabel"));
+
+ expectThrows(
+ RuntimeException.class,
+ () ->
+ backupManagerService.updateTransportAttributes(
+ mTransportUid,
+ mTransportComponent,
+ mTransportName,
+ new Intent(),
+ "currentDestinationString",
+ new Intent(),
+ null));
+ }
+
+ @Test
+ public void testUpdateTransportAttributes_whenPermissionGranted_callsThroughToTransportManager()
+ throws Exception {
+ setUpForUpdateTransportAttributes();
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+ Intent configurationIntent = new Intent();
+ Intent dataManagementIntent = new Intent();
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ backupManagerService.updateTransportAttributes(
+ mTransportUid,
+ mTransportComponent,
+ mTransportName,
+ configurationIntent,
+ "currentDestinationString",
+ dataManagementIntent,
+ "dataManagementLabel");
+
+ verify(mTransportManager)
+ .updateTransportAttributes(
+ eq(mTransportComponent),
+ eq(mTransportName),
+ eq(configurationIntent),
+ eq("currentDestinationString"),
+ eq(dataManagementIntent),
+ eq("dataManagementLabel"));
+ }
+
+ @Test
+ public void testUpdateTransportAttributes_whenPermissionDenied_throwsSecurityException()
+ throws Exception {
+ setUpForUpdateTransportAttributes();
+ mShadowContext.denyPermissions(android.Manifest.permission.BACKUP);
+ RefactoredBackupManagerService backupManagerService =
+ createInitializedBackupManagerService();
+
+ expectThrows(
+ SecurityException.class,
+ () ->
+ backupManagerService.updateTransportAttributes(
+ mTransportUid,
+ mTransportComponent,
+ mTransportName,
+ new Intent(),
+ "currentDestinationString",
+ new Intent(),
+ "dataManagementLabel"));
+ }
+
+ /* Miscellaneous tests */
+
+ @Test
+ public void testConstructor_postRegisterTransports() {
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+
+ createBackupManagerService();
+
+ mShadowBackupLooper.runToEndOfTasks();
+ verify(mTransportManager).registerTransports();
+ }
+
+ @Test
+ public void testConstructor_doesNotRegisterTransportsSynchronously() {
+ mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+
+ createBackupManagerService();
+
+ // Operations posted to mBackupThread only run with mShadowBackupLooper.runToEndOfTasks()
+ verify(mTransportManager, never()).registerTransports();
+ }
+
+ private RefactoredBackupManagerService createBackupManagerService() {
+ return new RefactoredBackupManagerService(
+ mContext,
+ new Trampoline(mContext),
+ mBackupThread,
+ mBaseStateDir,
+ mDataDir,
+ mTransportManager);
+ }
+
+ private RefactoredBackupManagerService createInitializedBackupManagerService() {
+ RefactoredBackupManagerService backupManagerService =
+ new RefactoredBackupManagerService(
+ mContext,
+ new Trampoline(mContext),
+ mBackupThread,
+ mBaseStateDir,
+ mDataDir,
+ mTransportManager);
+ mShadowBackupLooper.runToEndOfTasks();
+ // Handler instances have their own clock, so advancing looper (with runToEndOfTasks())
+ // above does NOT advance the handlers' clock, hence whenever a handler post messages with
+ // specific time to the looper the time of those messages will be before the looper's time.
+ // To fix this we advance SystemClock as well since that is from where the handlers read
+ // time.
+ ShadowSystemClock.setCurrentTimeMillis(mShadowBackupLooper.getScheduler().getCurrentTime());
+ return backupManagerService;
+ }
+}
diff --git a/com/android/server/backup/BackupPolicyEnforcer.java b/com/android/server/backup/BackupPolicyEnforcer.java
new file mode 100644
index 00000000..158084a4
--- /dev/null
+++ b/com/android/server/backup/BackupPolicyEnforcer.java
@@ -0,0 +1,25 @@
+package com.android.server.backup;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * A helper class to decouple this service from {@link DevicePolicyManager} in order to improve
+ * testability.
+ */
+@VisibleForTesting
+public class BackupPolicyEnforcer {
+ private DevicePolicyManager mDevicePolicyManager;
+
+ public BackupPolicyEnforcer(Context context) {
+ mDevicePolicyManager =
+ (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
+ }
+
+ public ComponentName getMandatoryBackupTransport() {
+ return mDevicePolicyManager.getMandatoryBackupTransport();
+ }
+}
diff --git a/com/android/server/backup/PackageManagerBackupAgent.java b/com/android/server/backup/PackageManagerBackupAgent.java
index 2d2993dc..3cf374fa 100644
--- a/com/android/server/backup/PackageManagerBackupAgent.java
+++ b/com/android/server/backup/PackageManagerBackupAgent.java
@@ -70,12 +70,28 @@ public class PackageManagerBackupAgent extends BackupAgent {
private static final String DEFAULT_HOME_KEY = "@home@";
// Sentinel: start of state file, followed by a version number
+ // Note that STATE_FILE_VERSION=2 is tied to UNDEFINED_ANCESTRAL_RECORD_VERSION=-1 *as well as*
+ // ANCESTRAL_RECORD_VERSION=1 (introduced Android P).
+ // Should the ANCESTRAL_RECORD_VERSION be bumped up in the future, STATE_FILE_VERSION will also
+ // need bumping up, assuming more data needs saving to the state file.
private static final String STATE_FILE_HEADER = "=state=";
private static final int STATE_FILE_VERSION = 2;
- // Current version of the saved ancestral-dataset file format
+ // key under which we store the saved ancestral-dataset format (starting from Android P)
+ // IMPORTANT: this key needs to come first in the restore data stream (to find out
+ // whether this version of Android knows how to restore the incoming data set), so it needs
+ // to be always the first one in alphabetical order of all the keys
+ private static final String ANCESTRAL_RECORD_KEY = "@ancestral_record@";
+
+ // Current version of the saved ancestral-dataset format
+ // Note that this constant was not used until Android P, and started being used
+ // to version @pm@ data for forwards-compatibility.
private static final int ANCESTRAL_RECORD_VERSION = 1;
+ // Undefined version of the saved ancestral-dataset file format means that the restore data
+ // is coming from pre-Android P device.
+ private static final int UNDEFINED_ANCESTRAL_RECORD_VERSION = -1;
+
private List<PackageInfo> mAllPackages;
private PackageManager mPackageManager;
// version & signature info of each app in a restore set
@@ -175,9 +191,8 @@ public class PackageManagerBackupAgent extends BackupAgent {
// additional involvement by the transport to obtain.
return mRestoredSignatures.keySet();
}
-
- // The backed up data is the signature block for each app, keyed by
- // the package name.
+
+ // The backed up data is the signature block for each app, keyed by the package name.
public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) {
if (DEBUG) Slog.v(TAG, "onBackup()");
@@ -196,6 +211,22 @@ public class PackageManagerBackupAgent extends BackupAgent {
mExisting.clear();
}
+ /*
+ * Ancestral record version:
+ *
+ * int ancestralRecordVersion -- the version of the format in which this backup set is
+ * produced
+ */
+ try {
+ if (DEBUG) Slog.v(TAG, "Storing ancestral record version key");
+ outputBufferStream.writeInt(ANCESTRAL_RECORD_VERSION);
+ writeEntity(data, ANCESTRAL_RECORD_KEY, outputBuffer.toByteArray());
+ } catch (IOException e) {
+ // Real error writing data
+ Slog.e(TAG, "Unable to write package backup data file!");
+ return;
+ }
+
long homeVersion = 0;
ArrayList<byte[]> homeSigHashes = null;
PackageInfo homeInfo = null;
@@ -230,6 +261,7 @@ public class PackageManagerBackupAgent extends BackupAgent {
Slog.i(TAG, "Home preference changed; backing up new state " + home);
}
if (home != null) {
+ outputBuffer.reset();
outputBufferStream.writeUTF(home.flattenToString());
outputBufferStream.writeLong(homeVersion);
outputBufferStream.writeUTF(homeInstaller != null ? homeInstaller : "" );
@@ -244,8 +276,8 @@ public class PackageManagerBackupAgent extends BackupAgent {
* Global metadata:
*
* int SDKversion -- the SDK version of the OS itself on the device
- * that produced this backup set. Used to reject
- * backups from later OSes onto earlier ones.
+ * that produced this backup set. Before Android P it was used to
+ * reject backups from later OSes onto earlier ones.
* String incremental -- the incremental release name of the OS stored in
* the backup set.
*/
@@ -354,7 +386,7 @@ public class PackageManagerBackupAgent extends BackupAgent {
// Finally, write the new state blob -- just the list of all apps we handled
writeStateFile(mAllPackages, home, homeVersion, homeSigHashes, newState);
}
-
+
private static void writeEntity(BackupDataOutput data, String key, byte[] bytes)
throws IOException {
data.writeEntityHeader(key, bytes.length);
@@ -366,83 +398,57 @@ public class PackageManagerBackupAgent extends BackupAgent {
// image. We'll use those later to determine what we can legitimately restore.
public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)
throws IOException {
- List<ApplicationInfo> restoredApps = new ArrayList<ApplicationInfo>();
- HashMap<String, Metadata> sigMap = new HashMap<String, Metadata>();
if (DEBUG) Slog.v(TAG, "onRestore()");
- int storedSystemVersion = -1;
- while (data.readNextHeader()) {
+ // we expect the ANCESTRAL_RECORD_KEY ("@ancestral_record@") to always come first in the
+ // restore set - based on that value we use different mechanisms to consume the data;
+ // if the ANCESTRAL_RECORD_KEY is missing in the restore set, it means that the data is
+ // is coming from a pre-Android P device, and we consume the header data in the legacy way
+ // TODO: add a CTS test to verify that backups of PMBA generated on Android P+ always
+ // contain the ANCESTRAL_RECORD_KEY, and it's always the first key
+ int ancestralRecordVersion = getAncestralRecordVersionValue(data);
+
+ RestoreDataConsumer consumer = getRestoreDataConsumer(ancestralRecordVersion);
+ if (consumer == null) {
+ Slog.w(TAG, "Ancestral restore set version is unknown"
+ + " to this Android version; not restoring");
+ return;
+ } else {
+ consumer.consumeRestoreData(data);
+ }
+ }
+
+ private int getAncestralRecordVersionValue(BackupDataInput data) throws IOException {
+ int ancestralRecordVersionValue = UNDEFINED_ANCESTRAL_RECORD_VERSION;
+ if (data.readNextHeader()) {
String key = data.getKey();
int dataSize = data.getDataSize();
if (DEBUG) Slog.v(TAG, " got key=" + key + " dataSize=" + dataSize);
- // generic setup to parse any entity data
- byte[] inputBytes = new byte[dataSize];
- data.readEntityData(inputBytes, 0, dataSize);
- ByteArrayInputStream inputBuffer = new ByteArrayInputStream(inputBytes);
- DataInputStream inputBufferStream = new DataInputStream(inputBuffer);
-
- if (key.equals(GLOBAL_METADATA_KEY)) {
- int storedSdkVersion = inputBufferStream.readInt();
- if (DEBUG) Slog.v(TAG, " storedSystemVersion = " + storedSystemVersion);
- if (storedSystemVersion > Build.VERSION.SDK_INT) {
- // returning before setting the sig map means we rejected the restore set
- Slog.w(TAG, "Restore set was from a later version of Android; not restoring");
- return;
- }
- mStoredSdkVersion = storedSdkVersion;
- mStoredIncrementalVersion = inputBufferStream.readUTF();
- mHasMetadata = true;
- if (DEBUG) {
- Slog.i(TAG, "Restore set version " + storedSystemVersion
- + " is compatible with OS version " + Build.VERSION.SDK_INT
- + " (" + mStoredIncrementalVersion + " vs "
- + Build.VERSION.INCREMENTAL + ")");
- }
- } else if (key.equals(DEFAULT_HOME_KEY)) {
- String cn = inputBufferStream.readUTF();
- mRestoredHome = ComponentName.unflattenFromString(cn);
- mRestoredHomeVersion = inputBufferStream.readLong();
- mRestoredHomeInstaller = inputBufferStream.readUTF();
- mRestoredHomeSigHashes = readSignatureHashArray(inputBufferStream);
- if (DEBUG) {
- Slog.i(TAG, " read preferred home app " + mRestoredHome
- + " version=" + mRestoredHomeVersion
- + " installer=" + mRestoredHomeInstaller
- + " sig=" + mRestoredHomeSigHashes);
- }
- } else {
- // it's a file metadata record
- 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
- + " dataSize=" + dataSize
- + " versionCode=" + versionCode + " sigs=" + sigs);
- }
-
- if (sigs == null || sigs.size() == 0) {
- Slog.w(TAG, "Not restoring package " + key
- + " since it appears to have no signatures.");
- continue;
- }
+ if (ANCESTRAL_RECORD_KEY.equals(key)) {
+ // generic setup to parse any entity data
+ byte[] inputBytes = new byte[dataSize];
+ data.readEntityData(inputBytes, 0, dataSize);
+ ByteArrayInputStream inputBuffer = new ByteArrayInputStream(inputBytes);
+ DataInputStream inputBufferStream = new DataInputStream(inputBuffer);
- ApplicationInfo app = new ApplicationInfo();
- app.packageName = key;
- restoredApps.add(app);
- sigMap.put(key, new Metadata(versionCode, sigs));
+ ancestralRecordVersionValue = inputBufferStream.readInt();
}
}
+ return ancestralRecordVersionValue;
+ }
- // On successful completion, cache the signature map for the Backup Manager to use
- mRestoredSignatures = sigMap;
+ private RestoreDataConsumer getRestoreDataConsumer(int ancestralRecordVersion) {
+ switch (ancestralRecordVersion) {
+ case UNDEFINED_ANCESTRAL_RECORD_VERSION:
+ return new LegacyRestoreDataConsumer();
+ case 1:
+ return new AncestralVersion1RestoreDataConsumer();
+ default:
+ Slog.e(TAG, "Unrecognized ANCESTRAL_RECORD_VERSION: " + ancestralRecordVersion);
+ return null;
+ }
}
private static void writeSignatureHashArray(DataOutputStream out, ArrayList<byte[]> hashes)
@@ -639,4 +645,173 @@ public class PackageManagerBackupAgent extends BackupAgent {
Slog.e(TAG, "Unable to write package manager state file!");
}
}
+
+ interface RestoreDataConsumer {
+ void consumeRestoreData(BackupDataInput data) throws IOException;
+ }
+
+ private class LegacyRestoreDataConsumer implements RestoreDataConsumer {
+
+ public void consumeRestoreData(BackupDataInput data) throws IOException {
+ List<ApplicationInfo> restoredApps = new ArrayList<ApplicationInfo>();
+ HashMap<String, Metadata> sigMap = new HashMap<String, Metadata>();
+ int storedSystemVersion = -1;
+
+ if (DEBUG) Slog.i(TAG, "Using LegacyRestoreDataConsumer");
+ // we already have the first header read and "cached", since ANCESTRAL_RECORD_KEY
+ // was missing
+ while (true) {
+ String key = data.getKey();
+ int dataSize = data.getDataSize();
+
+ if (DEBUG) Slog.v(TAG, " got key=" + key + " dataSize=" + dataSize);
+
+ // generic setup to parse any entity data
+ byte[] inputBytes = new byte[dataSize];
+ data.readEntityData(inputBytes, 0, dataSize);
+ ByteArrayInputStream inputBuffer = new ByteArrayInputStream(inputBytes);
+ DataInputStream inputBufferStream = new DataInputStream(inputBuffer);
+
+ if (key.equals(GLOBAL_METADATA_KEY)) {
+ int storedSdkVersion = inputBufferStream.readInt();
+ if (DEBUG) Slog.v(TAG, " storedSystemVersion = " + storedSystemVersion);
+ mStoredSdkVersion = storedSdkVersion;
+ mStoredIncrementalVersion = inputBufferStream.readUTF();
+ mHasMetadata = true;
+ if (DEBUG) {
+ Slog.i(TAG, "Restore set version " + storedSystemVersion
+ + " is compatible with OS version " + Build.VERSION.SDK_INT
+ + " (" + mStoredIncrementalVersion + " vs "
+ + Build.VERSION.INCREMENTAL + ")");
+ }
+ } else if (key.equals(DEFAULT_HOME_KEY)) {
+ String cn = inputBufferStream.readUTF();
+ mRestoredHome = ComponentName.unflattenFromString(cn);
+ mRestoredHomeVersion = inputBufferStream.readLong();
+ mRestoredHomeInstaller = inputBufferStream.readUTF();
+ mRestoredHomeSigHashes = readSignatureHashArray(inputBufferStream);
+ if (DEBUG) {
+ Slog.i(TAG, " read preferred home app " + mRestoredHome
+ + " version=" + mRestoredHomeVersion
+ + " installer=" + mRestoredHomeInstaller
+ + " sig=" + mRestoredHomeSigHashes);
+ }
+ } else {
+ // it's a file metadata record
+ 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
+ + " dataSize=" + dataSize
+ + " versionCode=" + versionCode + " sigs=" + sigs);
+ }
+
+ if (sigs == null || sigs.size() == 0) {
+ Slog.w(TAG, "Not restoring package " + key
+ + " since it appears to have no signatures.");
+ continue;
+ }
+
+ ApplicationInfo app = new ApplicationInfo();
+ app.packageName = key;
+ restoredApps.add(app);
+ sigMap.put(key, new Metadata(versionCode, sigs));
+ }
+
+ boolean readNextHeader = data.readNextHeader();
+ if (!readNextHeader) {
+ if (DEBUG) Slog.v(TAG, "LegacyRestoreDataConsumer:"
+ + " we're done reading all the headers");
+ break;
+ }
+ }
+
+ // On successful completion, cache the signature map for the Backup Manager to use
+ mRestoredSignatures = sigMap;
+ }
+ }
+
+ private class AncestralVersion1RestoreDataConsumer implements RestoreDataConsumer {
+
+ public void consumeRestoreData(BackupDataInput data) throws IOException {
+ List<ApplicationInfo> restoredApps = new ArrayList<ApplicationInfo>();
+ HashMap<String, Metadata> sigMap = new HashMap<String, Metadata>();
+ int storedSystemVersion = -1;
+
+ if (DEBUG) Slog.i(TAG, "Using AncestralVersion1RestoreDataConsumer");
+ while (data.readNextHeader()) {
+ String key = data.getKey();
+ int dataSize = data.getDataSize();
+
+ if (DEBUG) Slog.v(TAG, " got key=" + key + " dataSize=" + dataSize);
+
+ // generic setup to parse any entity data
+ byte[] inputBytes = new byte[dataSize];
+ data.readEntityData(inputBytes, 0, dataSize);
+ ByteArrayInputStream inputBuffer = new ByteArrayInputStream(inputBytes);
+ DataInputStream inputBufferStream = new DataInputStream(inputBuffer);
+
+ if (key.equals(GLOBAL_METADATA_KEY)) {
+ int storedSdkVersion = inputBufferStream.readInt();
+ if (DEBUG) Slog.v(TAG, " storedSystemVersion = " + storedSystemVersion);
+ mStoredSdkVersion = storedSdkVersion;
+ mStoredIncrementalVersion = inputBufferStream.readUTF();
+ mHasMetadata = true;
+ if (DEBUG) {
+ Slog.i(TAG, "Restore set version " + storedSystemVersion
+ + " is compatible with OS version " + Build.VERSION.SDK_INT
+ + " (" + mStoredIncrementalVersion + " vs "
+ + Build.VERSION.INCREMENTAL + ")");
+ }
+ } else if (key.equals(DEFAULT_HOME_KEY)) {
+ String cn = inputBufferStream.readUTF();
+ mRestoredHome = ComponentName.unflattenFromString(cn);
+ mRestoredHomeVersion = inputBufferStream.readLong();
+ mRestoredHomeInstaller = inputBufferStream.readUTF();
+ mRestoredHomeSigHashes = readSignatureHashArray(inputBufferStream);
+ if (DEBUG) {
+ Slog.i(TAG, " read preferred home app " + mRestoredHome
+ + " version=" + mRestoredHomeVersion
+ + " installer=" + mRestoredHomeInstaller
+ + " sig=" + mRestoredHomeSigHashes);
+ }
+ } else {
+ // it's a file metadata record
+ 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
+ + " dataSize=" + dataSize
+ + " versionCode=" + versionCode + " sigs=" + sigs);
+ }
+
+ if (sigs == null || sigs.size() == 0) {
+ Slog.w(TAG, "Not restoring package " + key
+ + " since it appears to have no signatures.");
+ continue;
+ }
+
+ ApplicationInfo app = new ApplicationInfo();
+ app.packageName = key;
+ restoredApps.add(app);
+ sigMap.put(key, new Metadata(versionCode, sigs));
+ }
+ }
+
+ // On successful completion, cache the signature map for the Backup Manager to use
+ mRestoredSignatures = sigMap;
+ }
+ }
}
diff --git a/com/android/server/backup/RefactoredBackupManagerService.java b/com/android/server/backup/RefactoredBackupManagerService.java
index 3a374598..465bb099 100644
--- a/com/android/server/backup/RefactoredBackupManagerService.java
+++ b/com/android/server/backup/RefactoredBackupManagerService.java
@@ -38,6 +38,7 @@ import android.app.AppGlobals;
import android.app.IActivityManager;
import android.app.IBackupAgent;
import android.app.PendingIntent;
+import android.app.admin.DevicePolicyManager;
import android.app.backup.BackupManager;
import android.app.backup.BackupManagerMonitor;
import android.app.backup.FullBackup;
@@ -148,6 +149,7 @@ import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedList;
import java.util.List;
import java.util.Queue;
import java.util.Random;
@@ -206,6 +208,10 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
public static final String BACKUP_FINISHED_ACTION = "android.intent.action.BACKUP_FINISHED";
public static final String BACKUP_FINISHED_PACKAGE_EXTRA = "packageName";
+ // Time delay for initialization operations that can be delayed so as not to consume too much CPU
+ // on bring-up and increase time-to-UI.
+ private static final long INITIALIZATION_DELAY_MILLIS = 3000;
+
// Timeout interval for deciding that a bind or clear-data has taken too long
private static final long TIMEOUT_INTERVAL = 10 * 1000;
@@ -281,6 +287,9 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
private final BackupPasswordManager mBackupPasswordManager;
+ // Time when we post the transport registration operation
+ private final long mRegisterTransportsRequestedTime;
+
@GuardedBy("mPendingRestores")
private boolean mIsRestoreInProgress;
@GuardedBy("mPendingRestores")
@@ -673,6 +682,8 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
@GuardedBy("mQueueLock")
private ArrayList<FullBackupEntry> mFullBackupQueue;
+ private BackupPolicyEnforcer mBackupPolicyEnforcer;
+
// 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.
@@ -734,6 +745,9 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
// Set up our transport options and initialize the default transport
SystemConfig systemConfig = SystemConfig.getInstance();
Set<ComponentName> transportWhitelist = systemConfig.getBackupTransportWhitelist();
+ if (transportWhitelist == null) {
+ transportWhitelist = Collections.emptySet();
+ }
String transport =
Settings.Secure.getString(
@@ -748,8 +762,7 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
new TransportManager(
context,
transportWhitelist,
- transport,
- backupThread.getLooper());
+ transport);
// If encrypted file systems is enabled or disabled, this call will return the
// correct directory.
@@ -851,15 +864,19 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
}
mTransportManager = transportManager;
- mTransportManager.setTransportBoundListener(mTransportBoundListener);
- mTransportManager.registerAllTransports();
+ mTransportManager.setOnTransportRegisteredListener(this::onTransportRegistered);
+ mRegisterTransportsRequestedTime = SystemClock.elapsedRealtime();
+ mBackupHandler.postDelayed(
+ mTransportManager::registerTransports, INITIALIZATION_DELAY_MILLIS);
- // Now that we know about valid backup participants, parse any
- // leftover journal files into the pending backup set
- mBackupHandler.post(this::parseLeftoverJournals);
+ // Now that we know about valid backup participants, parse any leftover journal files into
+ // the pending backup set
+ mBackupHandler.postDelayed(this::parseLeftoverJournals, INITIALIZATION_DELAY_MILLIS);
// Power management
mWakelock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*");
+
+ mBackupPolicyEnforcer = new BackupPolicyEnforcer(context);
}
private void initPackageTracking() {
@@ -1150,39 +1167,28 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
}
}
- 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();
+ private void onTransportRegistered(String transportName, String transportDirName) {
+ if (DEBUG) {
+ long timeMs = SystemClock.elapsedRealtime() - mRegisterTransportsRequestedTime;
+ Slog.d(TAG, "Transport " + transportName + " registered " + timeMs
+ + "ms after first request (delay = " + INITIALIZATION_DELAY_MILLIS + "ms)");
+ }
- File initSentinel = new File(stateDir, INIT_SENTINEL_FILE_NAME);
- if (initSentinel.exists()) {
- synchronized (mQueueLock) {
- mPendingInits.add(name);
+ File stateDir = new File(mBaseStateDir, transportDirName);
+ stateDir.mkdirs();
- // 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;
- }
- }
- };
+ File initSentinel = new File(stateDir, INIT_SENTINEL_FILE_NAME);
+ if (initSentinel.exists()) {
+ synchronized (mQueueLock) {
+ mPendingInits.add(transportName);
+
+ // 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);
+ }
+ }
+ }
// ----- Track installation/removal of packages -----
private BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -1424,6 +1430,8 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
final Intent notification = new Intent();
notification.setAction(BACKUP_FINISHED_ACTION);
notification.setPackage(receiver);
+ notification.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES |
+ Intent.FLAG_RECEIVER_FOREGROUND);
notification.putExtra(BACKUP_FINISHED_PACKAGE_EXTRA, packageName);
mContext.sendBroadcastAsUser(notification, UserHandle.OWNER);
}
@@ -1603,9 +1611,15 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
return BackupManager.ERROR_BACKUP_NOT_ALLOWED;
}
- TransportClient transportClient =
- mTransportManager.getCurrentTransportClient("BMS.requestBackup()");
- if (transportClient == null) {
+ final TransportClient transportClient;
+ final String transportDirName;
+ try {
+ transportDirName =
+ mTransportManager.getTransportDirName(
+ mTransportManager.getCurrentTransportName());
+ transportClient =
+ mTransportManager.getCurrentTransportClientOrThrow("BMS.requestBackup()");
+ } catch (TransportNotRegisteredException e) {
BackupObserverUtils.sendBackupFinished(observer, BackupManager.ERROR_TRANSPORT_ABORTED);
monitor = BackupManagerMonitorUtils.monitorEvent(monitor,
BackupManagerMonitor.LOG_EVENT_ID_TRANSPORT_IS_NULL,
@@ -1650,12 +1664,11 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
+ " k/v backups");
}
- String dirName = transportClient.getTransportDirName();
boolean nonIncrementalBackup = (flags & BackupManager.FLAG_NON_INCREMENTAL_BACKUP) != 0;
Message msg = mBackupHandler.obtainMessage(MSG_REQUEST_BACKUP);
- msg.obj = new BackupParams(transportClient, dirName, kvBackupList, fullBackupList, observer,
- monitor, listener, true, nonIncrementalBackup);
+ msg.obj = new BackupParams(transportClient, transportDirName, kvBackupList, fullBackupList,
+ observer, monitor, listener, true, nonIncrementalBackup);
mBackupHandler.sendMessage(msg);
return BackupManager.SUCCESS;
}
@@ -2766,6 +2779,10 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
public void setBackupEnabled(boolean enable) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
"setBackupEnabled");
+ if (!enable && mBackupPolicyEnforcer.getMandatoryBackupTransport() != null) {
+ Slog.w(TAG, "Cannot disable backups when the mandatory backups policy is active.");
+ return;
+ }
Slog.i(TAG, "Backup enabled => " + enable);
@@ -2883,14 +2900,14 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
"listAllTransports");
- return mTransportManager.getBoundTransportNames();
+ return mTransportManager.getRegisteredTransportNames();
}
@Override
public ComponentName[] listAllTransportComponents() {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
"listAllTransportComponents");
- return mTransportManager.getAllTransportComponents();
+ return mTransportManager.getRegisteredTransportComponents();
}
@Override
@@ -2993,63 +3010,115 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
}
}
- // Select which transport to use for the next backup operation.
+ /** Selects transport {@code transportName} and returns previous selected transport. */
@Override
- public String selectBackupTransport(String transport) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "selectBackupTransport");
+ @Deprecated
+ @Nullable
+ public String selectBackupTransport(String transportName) {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.BACKUP, "selectBackupTransport");
+
+ if (!isAllowedByMandatoryBackupTransportPolicy(transportName)) {
+ // Don't change the transport if it is not allowed.
+ Slog.w(TAG, "Failed to select transport - disallowed by device owner policy.");
+ return mTransportManager.getCurrentTransportName();
+ }
final long oldId = Binder.clearCallingIdentity();
try {
- String prevTransport = mTransportManager.selectTransport(transport);
- updateStateForTransport(transport);
- Slog.v(TAG, "selectBackupTransport() set " + mTransportManager.getCurrentTransportName()
- + " returning " + prevTransport);
- return prevTransport;
+ String previousTransportName = mTransportManager.selectTransport(transportName);
+ updateStateForTransport(transportName);
+ Slog.v(TAG, "selectBackupTransport(transport = " + transportName
+ + "): previous transport = " + previousTransportName);
+ return previousTransportName;
} finally {
Binder.restoreCallingIdentity(oldId);
}
}
@Override
- public void selectBackupTransportAsync(final ComponentName transport,
- final ISelectBackupTransportCallback listener) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "selectBackupTransportAsync");
-
+ public void selectBackupTransportAsync(
+ ComponentName transportComponent, @Nullable ISelectBackupTransportCallback listener) {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.BACKUP, "selectBackupTransportAsync");
+ if (!isAllowedByMandatoryBackupTransportPolicy(transportComponent)) {
+ try {
+ if (listener != null) {
+ Slog.w(TAG, "Failed to select transport - disallowed by device owner policy.");
+ listener.onFailure(BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "ISelectBackupTransportCallback listener not available");
+ }
+ return;
+ }
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.
+ try {
+ String transportString = transportComponent.flattenToShortString();
+ Slog.v(TAG, "selectBackupTransportAsync(transport = " + transportString + ")");
+ mBackupHandler.post(
+ () -> {
+ String transportName = null;
+ int result =
+ mTransportManager.registerAndSelectTransport(transportComponent);
+ if (result == BackupManager.SUCCESS) {
+ try {
+ transportName =
+ mTransportManager.getTransportName(transportComponent);
+ updateStateForTransport(transportName);
+ } catch (TransportNotRegisteredException e) {
+ Slog.e(TAG, "Transport got unregistered");
+ result = BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
+ }
}
- }
- @Override
- public void onFailure(int reason) {
- Slog.v(TAG,
- "Failed to select transport: " + transport.flattenToShortString());
try {
- listener.onFailure(reason);
+ if (listener != null) {
+ if (transportName != null) {
+ listener.onSuccess(transportName);
+ } else {
+ listener.onFailure(result);
+ }
+ }
} catch (RemoteException e) {
- // Nothing to do here.
+ Slog.e(TAG, "ISelectBackupTransportCallback listener not available");
}
- }
- });
+ });
+ } finally {
+ Binder.restoreCallingIdentity(oldId);
+ }
+ }
+
+ /**
+ * Returns if the specified transport can be set as the current transport without violating the
+ * mandatory backup transport policy.
+ */
+ private boolean isAllowedByMandatoryBackupTransportPolicy(String transportName) {
+ ComponentName mandatoryBackupTransport = mBackupPolicyEnforcer.getMandatoryBackupTransport();
+ if (mandatoryBackupTransport == null) {
+ return true;
+ }
+ final String mandatoryBackupTransportName;
+ try {
+ mandatoryBackupTransportName =
+ mTransportManager.getTransportName(mandatoryBackupTransport);
+ } catch (TransportNotRegisteredException e) {
+ Slog.e(TAG, "mandatory backup transport not registered!");
+ return false;
+ }
+ return TextUtils.equals(mandatoryBackupTransportName, transportName);
+ }
- Binder.restoreCallingIdentity(oldId);
+ /**
+ * Returns if the specified transport can be set as the current transport without violating the
+ * mandatory backup transport policy.
+ */
+ private boolean isAllowedByMandatoryBackupTransportPolicy(ComponentName transport) {
+ ComponentName mandatoryBackupTransport = mBackupPolicyEnforcer.getMandatoryBackupTransport();
+ if (mandatoryBackupTransport == null) {
+ return true;
+ }
+ return mandatoryBackupTransport.equals(transport);
}
private void updateStateForTransport(String newTransportName) {
@@ -3058,18 +3127,24 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
Settings.Secure.BACKUP_TRANSPORT, newTransportName);
// And update our current-dataset bookkeeping
- IBackupTransport transport = mTransportManager.getTransportBinder(newTransportName);
- if (transport != null) {
+ String callerLogString = "BMS.updateStateForTransport()";
+ TransportClient transportClient =
+ mTransportManager.getTransportClient(newTransportName, callerLogString);
+ if (transportClient != null) {
try {
+ IBackupTransport transport = transportClient.connectOrThrow(callerLogString);
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;
+ Slog.w(TAG, "Transport " + newTransportName + " not available: current token = 0");
}
+ mTransportManager.disposeOfTransportClient(transportClient, callerLogString);
} 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.
+ Slog.w(TAG, "Transport " + newTransportName + " not registered: current token = 0");
+ // The named transport isn't registered, so we can't know what its current dataset token
+ // is. Reset as above.
mCurrentToken = 0;
}
}
@@ -3093,29 +3168,30 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
}
}
- // 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!
+ /**
+ * Supply the current destination string for the given transport. If the name is not one of the
+ * registered transports the method will return null.
+ *
+ * <p>This string is used VERBATIM as the summary text of the relevant Settings item.
+ *
+ * @param transportName The name of the registered transport.
+ * @return The current destination string or null if the transport is not registered.
+ */
@Override
public String getDestinationString(String transportName) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "getDestinationString");
+ 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());
+ try {
+ String string = mTransportManager.getTransportCurrentDestinationString(transportName);
+ if (MORE_DEBUG) {
+ Slog.d(TAG, "getDestinationString() returning " + string);
}
+ return string;
+ } catch (TransportNotRegisteredException e) {
+ Slog.e(TAG, "Unable to get destination string from transport: " + e.getMessage());
+ return null;
}
-
- return null;
}
// Supply the manage-data intent for the given transport.
@@ -3385,30 +3461,50 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
@Override
public boolean isAppEligibleForBackup(String packageName) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
- "isAppEligibleForBackup");
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.BACKUP, "isAppEligibleForBackup");
+
+ long oldToken = Binder.clearCallingIdentity();
try {
- PackageInfo packageInfo = mPackageManager.getPackageInfo(packageName,
- PackageManager.GET_SIGNATURES);
- if (!AppBackupUtils.appIsEligibleForBackup(packageInfo.applicationInfo,
- mPackageManager) ||
- AppBackupUtils.appIsStopped(packageInfo.applicationInfo) ||
- AppBackupUtils.appIsDisabled(packageInfo.applicationInfo, mPackageManager)) {
- return false;
+ String callerLogString = "BMS.isAppEligibleForBackup";
+ TransportClient transportClient =
+ mTransportManager.getCurrentTransportClient(callerLogString);
+ boolean eligible =
+ AppBackupUtils.appIsRunningAndEligibleForBackupWithTransport(
+ transportClient, packageName, mPackageManager);
+ if (transportClient != null) {
+ mTransportManager.disposeOfTransportClient(transportClient, callerLogString);
}
- IBackupTransport transport = mTransportManager.getCurrentTransportBinder();
- if (transport != null) {
- try {
- return transport.isAppEligibleForBackup(packageInfo,
- AppBackupUtils.appGetsFullBackup(packageInfo));
- } catch (Exception e) {
- Slog.e(TAG, "Unable to ask about eligibility: " + e.getMessage());
+ return eligible;
+ } finally {
+ Binder.restoreCallingIdentity(oldToken);
+ }
+ }
+
+ @Override
+ public String[] filterAppsEligibleForBackup(String[] packages) {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.BACKUP, "filterAppsEligibleForBackup");
+
+ long oldToken = Binder.clearCallingIdentity();
+ try {
+ String callerLogString = "BMS.filterAppsEligibleForBackup";
+ TransportClient transportClient =
+ mTransportManager.getCurrentTransportClient(callerLogString);
+ List<String> eligibleApps = new LinkedList<>();
+ for (String packageName : packages) {
+ if (AppBackupUtils
+ .appIsRunningAndEligibleForBackupWithTransport(
+ transportClient, packageName, mPackageManager)) {
+ eligibleApps.add(packageName);
}
}
- // If transport is not present we couldn't tell that the package is not eligible.
- return true;
- } catch (NameNotFoundException e) {
- return false;
+ if (transportClient != null) {
+ mTransportManager.disposeOfTransportClient(transportClient, callerLogString);
+ }
+ return eligibleApps.toArray(new String[eligibleApps.size()]);
+ } finally {
+ Binder.restoreCallingIdentity(oldToken);
}
}
@@ -3428,6 +3524,9 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
} else if ("agents".startsWith(arg)) {
dumpAgents(pw);
return;
+ } else if ("transportclients".equals(arg.toLowerCase())) {
+ mTransportManager.dump(pw);
+ return;
}
}
}
@@ -3473,10 +3572,10 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
pw.println((t.equals(mTransportManager.getCurrentTransportName()) ? " * "
: " ") + t);
try {
- IBackupTransport transport = mTransportManager.getTransportBinder(t);
File dir = new File(mBaseStateDir,
mTransportManager.getTransportDirName(t));
- pw.println(" destination: " + transport.currentDestinationString());
+ pw.println(" destination: "
+ + mTransportManager.getTransportCurrentDestinationString(t));
pw.println(" intent: "
+ mTransportManager.getTransportConfigurationIntent(t));
for (File f : dir.listFiles()) {
@@ -3490,6 +3589,8 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter
}
}
+ mTransportManager.dump(pw);
+
pw.println("Pending init: " + mPendingInits.size());
for (String s : mPendingInits) {
pw.println(" " + s);
diff --git a/com/android/server/backup/Trampoline.java b/com/android/server/backup/Trampoline.java
index 94a26275..540f5a15 100644
--- a/com/android/server/backup/Trampoline.java
+++ b/com/android/server/backup/Trampoline.java
@@ -177,12 +177,15 @@ public class Trampoline extends IBackupManager.Stub {
}
}
+ // IBackupManager binder API
+
/**
* Querying activity state of backup service. Calling this method before initialize yields
* undefined result.
* @param userHandle The user in which the activity state of backup service is queried.
* @return true if the service is active.
*/
+ @Override
public boolean isBackupServiceActive(final int userHandle) {
// TODO: http://b/22388012
if (userHandle == UserHandle.USER_SYSTEM) {
@@ -193,7 +196,6 @@ public class Trampoline extends IBackupManager.Stub {
return false;
}
- // IBackupManager binder API
@Override
public void dataChanged(String packageName) throws RemoteException {
BackupManagerServiceInterface svc = mService;
@@ -452,6 +454,12 @@ public class Trampoline extends IBackupManager.Stub {
}
@Override
+ public String[] filterAppsEligibleForBackup(String[] packages) {
+ BackupManagerServiceInterface svc = mService;
+ return (svc != null) ? svc.filterAppsEligibleForBackup(packages) : null;
+ }
+
+ @Override
public int requestBackup(String[] packages, IBackupObserver observer,
IBackupManagerMonitor monitor, int flags) throws RemoteException {
BackupManagerServiceInterface svc = mService;
diff --git a/com/android/server/backup/TransportManager.java b/com/android/server/backup/TransportManager.java
index fbdb1832..7e179e5d 100644
--- a/com/android/server/backup/TransportManager.java
+++ b/com/android/server/backup/TransportManager.java
@@ -16,230 +16,200 @@
package com.android.server.backup;
-import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE;
-
import android.annotation.Nullable;
+import android.annotation.WorkerThread;
import android.app.backup.BackupManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
-import android.content.ServiceConnection;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.content.pm.ServiceInfo;
-import android.os.Handler;
-import android.os.IBinder;
-import android.os.Looper;
-import android.os.Message;
import android.os.RemoteException;
import android.os.UserHandle;
-import android.provider.Settings;
import android.util.ArrayMap;
import android.util.ArraySet;
-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.backup.IBackupTransport;
-import com.android.server.EventLogTags;
+import com.android.internal.util.Preconditions;
+import com.android.server.backup.transport.OnTransportRegisteredListener;
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.TransportNotAvailableException;
import com.android.server.backup.transport.TransportNotRegisteredException;
-import java.util.ArrayList;
-import java.util.Iterator;
+import java.io.PrintWriter;
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.
- */
+/** Handles in-memory bookkeeping of all BackupTransport objects. */
public class TransportManager {
-
private static final String TAG = "BackupTransportManager";
@VisibleForTesting
public static final String SERVICE_ACTION_TRANSPORT_HOST = "android.backup.TRANSPORT_HOST";
- private static final long REBINDING_TIMEOUT_UNPROVISIONED_MS = 30 * 1000; // 30 sec
- private static final long REBINDING_TIMEOUT_PROVISIONED_MS = 5 * 60 * 1000; // 5 mins
- private static final int REBINDING_TIMEOUT_MSG = 1;
-
private final Intent mTransportServiceIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST);
private final Context mContext;
private final PackageManager mPackageManager;
private final Set<ComponentName> mTransportWhitelist;
- private final Handler mHandler;
private final TransportClientManager mTransportClientManager;
+ private OnTransportRegisteredListener mOnTransportRegisteredListener = (c, n) -> {};
/**
- * This listener is called after we bind to any transport. If it returns true, this is a valid
- * transport.
+ * Lock for registered transports and currently selected transport.
+ *
+ * <p><b>Warning:</b> No calls to {@link IBackupTransport} or calls that result in transport
+ * code being executed such as {@link TransportClient#connect(String)}} and its variants should
+ * be made with this lock held, risk of deadlock.
*/
- private TransportBoundListener mTransportBoundListener;
-
private final Object mTransportLock = new Object();
- /**
- * We have detected these transports on the device. Unless in exceptional cases, we are also
- * bound to all of these.
- */
- @GuardedBy("mTransportLock")
- private final Map<ComponentName, TransportConnection> mValidTransports = new ArrayMap<>();
-
- /** We are currently bound to these transports. */
- @GuardedBy("mTransportLock")
- private final Map<String, ComponentName> mBoundTransports = new ArrayMap<>();
-
- /** @see #getEligibleTransportComponents() */
- @GuardedBy("mTransportLock")
- private final Set<ComponentName> mEligibleTransports = new ArraySet<>();
-
/** @see #getRegisteredTransportNames() */
@GuardedBy("mTransportLock")
private final Map<ComponentName, TransportDescription> mRegisteredTransportsDescriptionMap =
new ArrayMap<>();
@GuardedBy("mTransportLock")
+ @Nullable
private volatile String mCurrentTransportName;
-
- /**
- * Callback interface for {@link #ensureTransportReady(ComponentName, TransportReadyCallback)}.
- */
- public interface TransportReadyCallback {
-
- /**
- * Will be called when the transport is ready.
- */
- void onSuccess(String transportName);
-
- /**
- * Will be called when it's not possible to make transport ready.
- */
- void onFailure(int reason);
- }
-
- 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 selectedTransport) {
+ this(context, whitelist, selectedTransport, new TransportClientManager(context));
}
+ @VisibleForTesting
TransportManager(
Context context,
Set<ComponentName> whitelist,
- String defaultTransport,
- Looper looper) {
+ String selectedTransport,
+ TransportClientManager transportClientManager) {
mContext = context;
mPackageManager = context.getPackageManager();
- if (whitelist != null) {
- mTransportWhitelist = whitelist;
- } else {
- mTransportWhitelist = new ArraySet<>();
- }
- mCurrentTransportName = defaultTransport;
- mHandler = new RebindOnTimeoutHandler(looper);
- mTransportClientManager = new TransportClientManager(context);
+ mTransportWhitelist = Preconditions.checkNotNull(whitelist);
+ mCurrentTransportName = selectedTransport;
+ mTransportClientManager = transportClientManager;
}
- public void setTransportBoundListener(TransportBoundListener transportBoundListener) {
- mTransportBoundListener = transportBoundListener;
+ /* Sets a listener to be called whenever a transport is registered. */
+ public void setOnTransportRegisteredListener(OnTransportRegisteredListener listener) {
+ mOnTransportRegisteredListener = listener;
}
+ @WorkerThread
void onPackageAdded(String packageName) {
- // New package added. Bind to all transports it contains.
+ registerTransportsFromPackage(packageName, transportComponent -> true);
+ }
+
+ void onPackageRemoved(String packageName) {
synchronized (mTransportLock) {
- log_verbose("Package added. Binding to all transports. " + packageName);
- bindToAllInternal(packageName, null /* all components */);
+ mRegisteredTransportsDescriptionMap.keySet().removeIf(fromPackageFilter(packageName));
}
}
- void onPackageRemoved(String packageName) {
- // Package removed. Remove all its transports from our list. These transports have already
- // been removed from mBoundTransports because onServiceDisconnected would already been
- // called on TransportConnection objects.
+ @WorkerThread
+ void onPackageChanged(String packageName, String... components) {
+ // Unfortunately this can't be atomic because we risk a deadlock if
+ // registerTransportsFromPackage() is put inside the synchronized block
+ Set<ComponentName> transportComponents = new ArraySet<>(components.length);
+ for (String componentName : components) {
+ transportComponents.add(new ComponentName(packageName, componentName));
+ }
synchronized (mTransportLock) {
- Iterator<Map.Entry<ComponentName, TransportConnection>> iter =
- mValidTransports.entrySet().iterator();
- while (iter.hasNext()) {
- Map.Entry<ComponentName, TransportConnection> validTransport = iter.next();
- ComponentName componentName = validTransport.getKey();
- if (componentName.getPackageName().equals(packageName)) {
- TransportConnection transportConnection = validTransport.getValue();
- iter.remove();
- if (transportConnection != null) {
- mContext.unbindService(transportConnection);
- log_verbose("Package removed, removing transport: "
- + componentName.flattenToShortString());
- }
- }
- }
- removeTransportsIfLocked(
- componentName -> packageName.equals(componentName.getPackageName()));
+ mRegisteredTransportsDescriptionMap.keySet().removeIf(transportComponents::contains);
+ }
+ registerTransportsFromPackage(packageName, transportComponents::contains);
+ }
+
+ /**
+ * Returns the {@link ComponentName}s of the registered transports.
+ *
+ * <p>A *registered* transport is a transport that satisfies intent with action
+ * android.backup.TRANSPORT_HOST, returns true for {@link #isTransportTrusted(ComponentName)}
+ * and that we have successfully connected to once.
+ */
+ ComponentName[] getRegisteredTransportComponents() {
+ synchronized (mTransportLock) {
+ return mRegisteredTransportsDescriptionMap
+ .keySet()
+ .toArray(new ComponentName[mRegisteredTransportsDescriptionMap.size()]);
}
}
- void onPackageChanged(String packageName, String[] components) {
+ /**
+ * Returns the names of the registered transports.
+ *
+ * @see #getRegisteredTransportComponents()
+ */
+ String[] getRegisteredTransportNames() {
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);
- log_verbose("Package changed. Removing transport: " +
- componentName.flattenToShortString());
- }
+ String[] transportNames = new String[mRegisteredTransportsDescriptionMap.size()];
+ int i = 0;
+ for (TransportDescription description : mRegisteredTransportsDescriptionMap.values()) {
+ transportNames[i] = description.name;
+ i++;
}
- removeTransportsIfLocked(transportsToBeRemoved::contains);
- bindToAllInternal(packageName, components);
+ return transportNames;
}
}
- @GuardedBy("mTransportLock")
- private void removeTransportsIfLocked(Predicate<ComponentName> filter) {
- mEligibleTransports.removeIf(filter);
- mRegisteredTransportsDescriptionMap.keySet().removeIf(filter);
+ /** Returns a set with the whitelisted transports. */
+ Set<ComponentName> getTransportWhitelist() {
+ return mTransportWhitelist;
}
- public IBackupTransport getTransportBinder(String transportName) {
+ @Nullable
+ String getCurrentTransportName() {
+ return mCurrentTransportName;
+ }
+
+ /**
+ * Returns the transport name associated with {@code transportComponent}.
+ *
+ * @throws TransportNotRegisteredException if the transport is not registered.
+ */
+ public String getTransportName(ComponentName transportComponent)
+ throws TransportNotRegisteredException {
synchronized (mTransportLock) {
- ComponentName component = mBoundTransports.get(transportName);
- if (component == null) {
- Slog.w(TAG, "Transport " + transportName + " not bound.");
- return null;
- }
- TransportConnection conn = mValidTransports.get(component);
- if (conn == null) {
- Slog.w(TAG, "Transport " + transportName + " not valid.");
- return null;
- }
- return conn.getBinder();
+ return getRegisteredTransportDescriptionOrThrowLocked(transportComponent).name;
}
}
- public IBackupTransport getCurrentTransportBinder() {
- return getTransportBinder(mCurrentTransportName);
+ /**
+ * Retrieves the transport dir name of {@code transportComponent}.
+ *
+ * @throws TransportNotRegisteredException if the transport is not registered.
+ */
+ public String getTransportDirName(ComponentName transportComponent)
+ throws TransportNotRegisteredException {
+ synchronized (mTransportLock) {
+ return getRegisteredTransportDescriptionOrThrowLocked(transportComponent)
+ .transportDirName;
+ }
}
/**
- * Retrieve the configuration intent of {@code transportName}.
+ * Retrieves 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;
+ }
+ }
+
+ /**
+ * Retrieves the configuration intent of {@code transportName}.
+ *
* @throws TransportNotRegisteredException if the transport is not registered.
*/
@Nullable
@@ -252,84 +222,117 @@ public class TransportManager {
}
/**
- * Retrieve the data management intent of {@code transportName}.
+ * Retrieves the current destination string of {@code transportName}.
+ *
* @throws TransportNotRegisteredException if the transport is not registered.
*/
- @Nullable
- public Intent getTransportDataManagementIntent(String transportName)
+ public String getTransportCurrentDestinationString(String transportName)
throws TransportNotRegisteredException {
synchronized (mTransportLock) {
return getRegisteredTransportDescriptionOrThrowLocked(transportName)
- .dataManagementIntent;
+ .currentDestinationString;
}
}
/**
- * Retrieve the data management label of {@code transportName}.
+ * Retrieves the data management intent of {@code transportName}.
+ *
* @throws TransportNotRegisteredException if the transport is not registered.
*/
@Nullable
- public String getTransportDataManagementLabel(String transportName)
+ public Intent getTransportDataManagementIntent(String transportName)
throws TransportNotRegisteredException {
synchronized (mTransportLock) {
return getRegisteredTransportDescriptionOrThrowLocked(transportName)
- .dataManagementLabel;
+ .dataManagementIntent;
}
}
/**
- * Retrieve the transport dir name of {@code transportName}.
+ * Retrieves the data management label of {@code transportName}.
+ *
* @throws TransportNotRegisteredException if the transport is not registered.
*/
- public String getTransportDirName(String transportName)
+ @Nullable
+ public String getTransportDataManagementLabel(String transportName)
throws TransportNotRegisteredException {
synchronized (mTransportLock) {
return getRegisteredTransportDescriptionOrThrowLocked(transportName)
- .transportDirName;
+ .dataManagementLabel;
+ }
+ }
+
+ /* Returns true if the transport identified by {@code transportName} is registered. */
+ public boolean isTransportRegistered(String transportName) {
+ synchronized (mTransportLock) {
+ return getRegisteredTransportEntryLocked(transportName) != null;
}
}
/**
* 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}.
+ * while {@code transportConsumer} is being executed. Don't do heavy operations in {@code
+ * transportConsumer}.
+ *
+ * <p><b>Warning:</b> Do NOT make any calls to {@link IBackupTransport} or call any variants of
+ * {@link TransportClient#connect(String)} here, otherwise you risk deadlock.
*/
public void forEachRegisteredTransport(Consumer<String> transportConsumer) {
synchronized (mTransportLock) {
- for (TransportDescription transportDescription
- : mRegisteredTransportsDescriptionMap.values()) {
+ for (TransportDescription transportDescription :
+ mRegisteredTransportsDescriptionMap.values()) {
transportConsumer.accept(transportDescription.name);
}
}
}
- public String getTransportName(IBackupTransport binder) {
- synchronized (mTransportLock) {
- for (TransportConnection conn : mValidTransports.values()) {
- if (conn.getBinder() == binder) {
- return conn.getName();
- }
- }
- }
- return null;
- }
-
/**
- * Returns the transport name associated with {@param transportComponent} or {@code null} if not
- * found.
+ * Updates given values for the transport already registered and identified with {@param
+ * transportComponent}. If the transport is not registered it will log and return.
*/
- @Nullable
- public String getTransportName(ComponentName transportComponent) {
+ 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, "Trying to find name of unregistered transport " + transportComponent);
- return null;
+ Slog.e(TAG, "Transport " + name + " not registered tried to change description");
+ return;
}
- return description.name;
+ description.name = name;
+ description.configurationIntent = configurationIntent;
+ description.currentDestinationString = currentDestinationString;
+ description.dataManagementIntent = dataManagementIntent;
+ description.dataManagementLabel = dataManagementLabel;
+ Slog.d(TAG, "Transport " + name + " updated its attributes");
+ }
+ }
+
+ @GuardedBy("mTransportLock")
+ private TransportDescription getRegisteredTransportDescriptionOrThrowLocked(
+ ComponentName transportComponent) throws TransportNotRegisteredException {
+ TransportDescription description =
+ mRegisteredTransportsDescriptionMap.get(transportComponent);
+ if (description == null) {
+ throw new TransportNotRegisteredException(transportComponent);
}
+ return description;
+ }
+
+ @GuardedBy("mTransportLock")
+ private TransportDescription getRegisteredTransportDescriptionOrThrowLocked(
+ String transportName) throws TransportNotRegisteredException {
+ TransportDescription description = getRegisteredTransportDescriptionLocked(transportName);
+ if (description == null) {
+ throw new TransportNotRegisteredException(transportName);
+ }
+ return description;
}
@GuardedBy("mTransportLock")
@@ -349,22 +352,11 @@ public class TransportManager {
}
@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()) {
+ for (Map.Entry<ComponentName, TransportDescription> entry :
+ mRegisteredTransportsDescriptionMap.entrySet()) {
TransportDescription description = entry.getValue();
if (transportName.equals(description.name)) {
return entry;
@@ -373,37 +365,77 @@ public class TransportManager {
return null;
}
+ /**
+ * Returns a {@link TransportClient} for {@code transportName} or {@code null} if not
+ * registered.
+ *
+ * @param transportName The name of the transport.
+ * @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 registered.
+ */
@Nullable
public TransportClient getTransportClient(String transportName, String caller) {
+ try {
+ return getTransportClientOrThrow(transportName, caller);
+ } catch (TransportNotRegisteredException e) {
+ Slog.w(TAG, "Transport " + transportName + " not registered");
+ return null;
+ }
+ }
+
+ /**
+ * Returns a {@link TransportClient} for {@code transportName} or throws if not registered.
+ *
+ * @param transportName The name of the transport.
+ * @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}.
+ * @throws TransportNotRegisteredException if the transport is not registered.
+ */
+ public TransportClient getTransportClientOrThrow(String transportName, String caller)
+ throws TransportNotRegisteredException {
synchronized (mTransportLock) {
ComponentName component = getRegisteredTransportComponentLocked(transportName);
if (component == null) {
- Slog.w(TAG, "Transport " + transportName + " not registered");
- return null;
+ throw new TransportNotRegisteredException(transportName);
}
- TransportDescription description = mRegisteredTransportsDescriptionMap.get(component);
- return mTransportClientManager.getTransportClient(
- component, description.transportDirName, caller);
+ return mTransportClientManager.getTransportClient(component, caller);
}
}
- public boolean isTransportRegistered(String transportName) {
+ /**
+ * Returns a {@link TransportClient} for the current transport or {@code null} if not
+ * registered.
+ *
+ * @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 registered.
+ */
+ @Nullable
+ public TransportClient getCurrentTransportClient(String caller) {
synchronized (mTransportLock) {
- return getRegisteredTransportEntryLocked(transportName) != null;
+ return getTransportClient(mCurrentTransportName, caller);
}
}
/**
- * Returns a {@link TransportClient} for the current transport or null if not found.
+ * Returns a {@link TransportClient} for the current transport or throws if not registered.
*
* @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.
+ * @return A {@link TransportClient}.
+ * @throws TransportNotRegisteredException if the transport is not registered.
*/
- @Nullable
- public TransportClient getCurrentTransportClient(String caller) {
- return getTransportClient(mCurrentTransportName, caller);
+ public TransportClient getCurrentTransportClientOrThrow(String caller)
+ throws TransportNotRegisteredException {
+ synchronized (mTransportLock) {
+ return getTransportClientOrThrow(mCurrentTransportName, caller);
+ }
}
/**
@@ -418,154 +450,94 @@ public class TransportManager {
mTransportClientManager.disposeOfTransportClient(transportClient, caller);
}
- String[] getBoundTransportNames() {
- synchronized (mTransportLock) {
- return mBoundTransports.keySet().toArray(new String[mBoundTransports.size()]);
- }
- }
-
- ComponentName[] getAllTransportComponents() {
- synchronized (mTransportLock) {
- return mValidTransports.keySet().toArray(new ComponentName[mValidTransports.size()]);
- }
- }
-
/**
- * 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.
+ * Sets {@code transportName} as selected transport and returns previously selected transport
+ * name. If there was no previous transport it returns null.
+ *
+ * <p>You should NOT call this method in new code. This won't make any checks against {@code
+ * transportName}, putting any operation at risk of a {@link TransportNotRegisteredException} or
+ * another error at the time it's being executed.
+ *
+ * <p>{@link Deprecated} as public, this method can be used as private.
*/
- ComponentName[] getEligibleTransportComponents() {
+ @Deprecated
+ @Nullable
+ String selectTransport(String transportName) {
synchronized (mTransportLock) {
- return mEligibleTransports.toArray(new ComponentName[mEligibleTransports.size()]);
+ String prevTransport = mCurrentTransportName;
+ mCurrentTransportName = transportName;
+ return prevTransport;
}
}
- 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.
+ * Tries to register the transport if not registered. If successful also selects the transport.
+ *
+ * @param transportComponent Host of the transport.
+ * @return One of {@link BackupManager#SUCCESS}, {@link BackupManager#ERROR_TRANSPORT_INVALID}
+ * or {@link BackupManager#ERROR_TRANSPORT_UNAVAILABLE}.
*/
- String[] getRegisteredTransportNames() {
+ @WorkerThread
+ public int registerAndSelectTransport(ComponentName transportComponent) {
+ // If it's already registered we select and return
synchronized (mTransportLock) {
- return mRegisteredTransportsDescriptionMap.values().stream()
- .map(transportDescription -> transportDescription.name)
- .toArray(String[]::new);
+ try {
+ selectTransport(getTransportName(transportComponent));
+ return BackupManager.SUCCESS;
+ } catch (TransportNotRegisteredException e) {
+ // Fall through and release lock
+ }
}
- }
- /**
- * 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) {
+ // We can't call registerTransport() with the transport lock held
+ int result = registerTransport(transportComponent);
+ if (result != BackupManager.SUCCESS) {
+ return result;
+ }
synchronized (mTransportLock) {
- TransportDescription description =
- mRegisteredTransportsDescriptionMap.get(transportComponent);
- if (description == null) {
- Slog.e(TAG, "Transport " + name + " not registered tried to change description");
- return;
+ try {
+ selectTransport(getTransportName(transportComponent));
+ return BackupManager.SUCCESS;
+ } catch (TransportNotRegisteredException e) {
+ Slog.wtf(TAG, "Transport got unregistered");
+ return BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
}
- 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;
- mCurrentTransportName = transport;
- return prevTransport;
- }
+ @WorkerThread
+ public void registerTransports() {
+ registerTransportsForIntent(mTransportServiceIntent, transportComponent -> true);
}
- void ensureTransportReady(ComponentName transportComponent,
- TransportReadyCallback listener) {
- synchronized (mTransportLock) {
- TransportConnection conn = mValidTransports.get(transportComponent);
- if (conn == null) {
- listener.onFailure(BackupManager.ERROR_TRANSPORT_UNAVAILABLE);
- return;
- }
- // Transport can be unbound if the process hosting it crashed.
- conn.bindIfUnbound();
- conn.addListener(listener);
+ @WorkerThread
+ private void registerTransportsFromPackage(
+ String packageName, Predicate<ComponentName> transportComponentFilter) {
+ try {
+ mPackageManager.getPackageInfo(packageName, 0);
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.e(TAG, "Trying to register transports from package not found " + packageName);
+ return;
}
- }
-
- // 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 */);
+ registerTransportsForIntent(
+ new Intent(mTransportServiceIntent).setPackage(packageName),
+ transportComponentFilter.and(fromPackageFilter(packageName)));
}
- /**
- * Bind to all transports belonging to the given package and the given component list.
- * null acts a wildcard.
- *
- * If packageName is null, bind to all transports in all packages.
- * If components is null, bind to all transports in the given package.
- */
- private void bindToAllInternal(String packageName, String[] components) {
- PackageInfo pkgInfo = null;
- if (packageName != null) {
- try {
- pkgInfo = mPackageManager.getPackageInfo(packageName, 0);
- } catch (PackageManager.NameNotFoundException e) {
- Slog.w(TAG, "Package not found: " + packageName);
- return;
- }
+ @WorkerThread
+ private void registerTransportsForIntent(
+ Intent intent, Predicate<ComponentName> transportComponentFilter) {
+ List<ResolveInfo> hosts =
+ mPackageManager.queryIntentServicesAsUser(intent, 0, UserHandle.USER_SYSTEM);
+ if (hosts == null) {
+ return;
}
-
- Intent intent = new Intent(mTransportServiceIntent);
- if (packageName != null) {
- intent.setPackage(packageName);
- }
-
- List<ResolveInfo> hosts = mPackageManager.queryIntentServicesAsUser(
- intent, 0, UserHandle.USER_SYSTEM);
- if (hosts != null) {
- for (ResolveInfo host : hosts) {
- final ComponentName infoComponentName = getComponentName(host.serviceInfo);
- boolean shouldBind = false;
- if (components != null && packageName != null) {
- for (String component : components) {
- ComponentName cn = new ComponentName(pkgInfo.packageName, component);
- if (infoComponentName.equals(cn)) {
- shouldBind = true;
- break;
- }
- }
- } else {
- shouldBind = true;
- }
- if (shouldBind && isTransportTrusted(infoComponentName)) {
- tryBindTransport(infoComponentName);
- }
+ for (ResolveInfo host : hosts) {
+ ComponentName transportComponent = host.serviceInfo.getComponentName();
+ if (transportComponentFilter.test(transportComponent)
+ && isTransportTrusted(transportComponent)) {
+ registerTransport(transportComponent);
}
}
}
@@ -591,253 +563,83 @@ public class TransportManager {
return true;
}
- private void tryBindTransport(ComponentName transportComponentName) {
- 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);
+ /**
+ * Tries to register transport represented by {@code transportComponent}.
+ *
+ * <p><b>Warning:</b> Don't call this with the transport lock held.
+ *
+ * @param transportComponent Host of the transport that we want to register.
+ * @return One of {@link BackupManager#SUCCESS}, {@link BackupManager#ERROR_TRANSPORT_INVALID}
+ * or {@link BackupManager#ERROR_TRANSPORT_UNAVAILABLE}.
+ */
+ @WorkerThread
+ private int registerTransport(ComponentName transportComponent) {
+ checkCanUseTransport();
+
+ if (!isTransportTrusted(transportComponent)) {
+ return BackupManager.ERROR_TRANSPORT_INVALID;
}
- if (bindToTransport(transportComponentName, connection)) {
- synchronized (mTransportLock) {
- mValidTransports.put(transportComponentName, connection);
- }
- } else {
- Slog.w(TAG, "Couldn't bind to transport " + transportComponentName);
+
+ String transportString = transportComponent.flattenToShortString();
+ String callerLogString = "TransportManager.registerTransport()";
+ TransportClient transportClient =
+ mTransportClientManager.getTransportClient(transportComponent, callerLogString);
+ final IBackupTransport transport;
+ try {
+ transport = transportClient.connectOrThrow(callerLogString);
+ } catch (TransportNotAvailableException e) {
+ Slog.e(TAG, "Couldn't connect to transport " + transportString + " for registration");
+ mTransportClientManager.disposeOfTransportClient(transportClient, callerLogString);
+ return BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
+ }
+
+ int result;
+ try {
+ String transportName = transport.name();
+ String transportDirName = transport.transportDirName();
+ registerTransport(transportComponent, transport);
+ // If registerTransport() hasn't thrown...
+ Slog.d(TAG, "Transport " + transportString + " registered");
+ mOnTransportRegisteredListener.onTransportRegistered(transportName, transportDirName);
+ result = BackupManager.SUCCESS;
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Transport " + transportString + " died while registering");
+ result = BackupManager.ERROR_TRANSPORT_UNAVAILABLE;
}
- }
- private boolean bindToTransport(ComponentName componentName, ServiceConnection connection) {
- Intent intent = new Intent(mTransportServiceIntent)
- .setComponent(componentName);
- return mContext.bindServiceAsUser(intent, connection, Context.BIND_AUTO_CREATE,
- createSystemUserHandle());
+ mTransportClientManager.disposeOfTransportClient(transportClient, callerLogString);
+ return result;
}
/** If {@link RemoteException} is thrown the transport is guaranteed to not be registered. */
private void registerTransport(ComponentName transportComponent, IBackupTransport transport)
throws RemoteException {
+ checkCanUseTransport();
+
+ TransportDescription description =
+ new TransportDescription(
+ transport.name(),
+ transport.transportDirName(),
+ transport.configurationIntent(),
+ transport.currentDestinationString(),
+ transport.dataManagementIntent(),
+ transport.dataManagementLabel());
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 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;
-
- private final ComponentName mTransportComponent;
-
- private TransportConnection(ComponentName transportComponent) {
- mTransportComponent = transportComponent;
- }
-
- @Override
- public void onServiceConnected(ComponentName component, IBinder binder) {
- synchronized (mTransportLock) {
- mBinder = IBackupTransport.Stub.asInterface(binder);
- boolean success = false;
-
- EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE,
- component.flattenToShortString(), 1);
-
- try {
- mTransportName = mBinder.name();
- // BackupManager requests some fields from the transport. If they are
- // invalid, throw away this transport.
- 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);
- } finally {
- // we need to intern() the String of the component, so that we can use it with
- // Handler's removeMessages(), which uses == operator to compare the tokens
- String componentShortString = component.flattenToShortString().intern();
- if (success) {
- Slog.d(TAG, "Bound to transport: " + componentShortString);
- mBoundTransports.put(mTransportName, component);
- for (TransportReadyCallback listener : mListeners) {
- listener.onSuccess(mTransportName);
- }
- // cancel rebinding on timeout for this component as we've already connected
- mHandler.removeMessages(REBINDING_TIMEOUT_MSG, componentShortString);
- } else {
- Slog.w(TAG, "Bound to transport " + componentShortString +
- " but it is invalid");
- EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE,
- componentShortString, 0);
- mContext.unbindService(this);
- mValidTransports.remove(component);
- mEligibleTransports.remove(component);
- mBinder = null;
- for (TransportReadyCallback listener : mListeners) {
- listener.onFailure(BackupManager.ERROR_TRANSPORT_INVALID);
- }
- }
- mListeners.clear();
- }
- }
- }
-
- @Override
- public void onServiceDisconnected(ComponentName component) {
- synchronized (mTransportLock) {
- mBinder = null;
- mBoundTransports.remove(mTransportName);
- }
- String componentShortString = component.flattenToShortString();
- EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, componentShortString, 0);
- Slog.w(TAG, "Disconnected from transport " + componentShortString);
- scheduleRebindTimeout(component);
- }
-
- /**
- * We'll attempt to explicitly rebind to a transport if it hasn't happened automatically
- * for a few minutes after the binding went away.
- */
- private void scheduleRebindTimeout(ComponentName component) {
- // we need to intern() the String of the component, so that we can use it with Handler's
- // removeMessages(), which uses == operator to compare the tokens
- final String componentShortString = component.flattenToShortString().intern();
- final long rebindTimeout = getRebindTimeout();
- mHandler.removeMessages(REBINDING_TIMEOUT_MSG, componentShortString);
- Message msg = mHandler.obtainMessage(REBINDING_TIMEOUT_MSG);
- msg.obj = componentShortString;
- mHandler.sendMessageDelayed(msg, rebindTimeout);
- Slog.d(TAG, "Scheduled explicit rebinding for " + componentShortString + " in "
- + rebindTimeout + "ms");
- }
-
- // Intentionally not synchronized -- the variable is volatile and changes to its value
- // are inside synchronized blocks, providing a memory sync barrier; and this method
- // does not touch any other state protected by that lock.
- private IBackupTransport getBinder() {
- return mBinder;
- }
-
- // Intentionally not synchronized; same as getBinder()
- private String getName() {
- return mTransportName;
- }
-
- // Intentionally not synchronized; same as getBinder()
- private void bindIfUnbound() {
- if (mBinder == null) {
- Slog.d(TAG,
- "Rebinding to transport " + mTransportComponent.flattenToShortString());
- bindToTransport(mTransportComponent, this);
- }
- }
-
- private void addListener(TransportReadyCallback listener) {
- synchronized (mTransportLock) {
- if (mBinder == null) {
- // We are waiting for bind to complete. If mBinder is set to null after the bind
- // is complete due to transport being invalid, we won't find 'this' connection
- // object in mValidTransports list and this function can't be called.
- mListeners.add(listener);
- } else {
- listener.onSuccess(mTransportName);
- }
- }
- }
-
- private long getRebindTimeout() {
- final boolean isDeviceProvisioned = Settings.Global.getInt(
- mContext.getContentResolver(),
- Settings.Global.DEVICE_PROVISIONED, 0) != 0;
- return isDeviceProvisioned
- ? REBINDING_TIMEOUT_PROVISIONED_MS
- : REBINDING_TIMEOUT_UNPROVISIONED_MS;
- }
- }
-
- public interface TransportBoundListener {
- /** Should return true if this is a valid transport. */
- boolean onTransportBound(IBackupTransport binder);
- }
-
- private class RebindOnTimeoutHandler extends Handler {
-
- RebindOnTimeoutHandler(Looper looper) {
- super(looper);
- }
-
- @Override
- public void handleMessage(Message msg) {
- if (msg.what == REBINDING_TIMEOUT_MSG) {
- String componentShortString = (String) msg.obj;
- ComponentName transportComponent =
- ComponentName.unflattenFromString(componentShortString);
- synchronized (mTransportLock) {
- if (mBoundTransports.containsValue(transportComponent)) {
- Slog.d(TAG, "Explicit rebinding timeout passed, but already bound to "
- + componentShortString + " so not attempting to rebind");
- return;
- }
- Slog.d(TAG, "Explicit rebinding timeout passed, attempting rebinding to: "
- + componentShortString);
- // unbind the existing (broken) connection
- TransportConnection conn = mValidTransports.get(transportComponent);
- if (conn != null) {
- mContext.unbindService(conn);
- Slog.d(TAG, "Unbinding the existing (broken) connection to transport: "
- + componentShortString);
- }
- }
- // rebind to transport
- tryBindTransport(transportComponent);
- } else {
- Slog.e(TAG, "Unknown message sent to RebindOnTimeoutHandler, msg.what: "
- + msg.what);
- }
- }
- }
-
- private static void log_verbose(String message) {
- if (Log.isLoggable(TAG, Log.VERBOSE)) {
- Slog.v(TAG, message);
- }
+ private void checkCanUseTransport() {
+ Preconditions.checkState(
+ !Thread.holdsLock(mTransportLock), "Can't call transport with transport lock held");
}
- // These only exists to make it testable with Robolectric, which is not updated to API level 24
- // yet.
- // TODO: Get rid of this once Robolectric is updated.
- private static ComponentName getComponentName(ServiceInfo serviceInfo) {
- return new ComponentName(serviceInfo.packageName, serviceInfo.name);
+ public void dump(PrintWriter pw) {
+ mTransportClientManager.dump(pw);
}
- // These only exists to make it testable with Robolectric, which is not updated to API level 24
- // yet.
- // TODO: Get rid of this once Robolectric is updated.
- public static UserHandle createSystemUserHandle() {
- return new UserHandle(UserHandle.USER_SYSTEM);
+ private static Predicate<ComponentName> fromPackageFilter(String packageName) {
+ return transportComponent -> packageName.equals(transportComponent.getPackageName());
}
private static class TransportDescription {
diff --git a/com/android/server/backup/TransportManagerTest.java b/com/android/server/backup/TransportManagerTest.java
index 82830fe5..068fe813 100644
--- a/com/android/server/backup/TransportManagerTest.java
+++ b/com/android/server/backup/TransportManagerTest.java
@@ -16,9 +16,21 @@
package com.android.server.backup;
+import static com.android.server.backup.testing.TransportData.genericTransport;
+import static com.android.server.backup.testing.TransportTestUtils.mockTransport;
+import static com.android.server.backup.testing.TransportTestUtils.setUpTransportsForTransportManager;
import static com.google.common.truth.Truth.assertThat;
-
-import static org.mockito.Mockito.mock;
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toList;
+import static java.util.stream.Collectors.toSet;
+import static java.util.stream.Stream.concat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.robolectric.shadow.api.Shadow.extract;
import static org.testng.Assert.expectThrows;
@@ -26,103 +38,70 @@ import static org.testng.Assert.expectThrows;
import android.annotation.Nullable;
import android.app.backup.BackupManager;
import android.content.ComponentName;
+import android.content.Context;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
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.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.testing.TransportData;
+import com.android.server.backup.testing.TransportTestUtils.TransportMock;
+import com.android.server.backup.transport.OnTransportRegisteredListener;
import com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.transport.TransportClientManager;
import com.android.server.backup.transport.TransportNotRegisteredException;
import com.android.server.testing.FrameworkRobolectricTestRunner;
import com.android.server.testing.SystemLoaderClasses;
-
+import com.android.server.testing.shadows.FrameworkShadowContextImpl;
+import com.android.server.testing.shadows.FrameworkShadowPackageManager;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.stream.Stream;
import org.junit.After;
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 org.robolectric.shadows.ShadowLog;
-import org.robolectric.shadows.ShadowLooper;
import org.robolectric.shadows.ShadowPackageManager;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-
@RunWith(FrameworkRobolectricTestRunner.class)
@Config(
- manifest = Config.NONE,
- sdk = 26,
- shadows = {
- ShadowContextImplForBackup.class,
- ShadowBackupTransportStub.class,
- ShadowPackageManagerForBackup.class
- }
+ manifest = Config.NONE,
+ sdk = 26,
+ shadows = {FrameworkShadowPackageManager.class, FrameworkShadowContextImpl.class}
)
@SystemLoaderClasses({TransportManager.class})
@Presubmit
public class TransportManagerTest {
- private static final String PACKAGE_NAME = "some.package.name";
- private static final String ANOTHER_PACKAGE_NAME = "another.package.name";
+ private static final String PACKAGE_A = "some.package.a";
+ private static final String PACKAGE_B = "some.package.b";
- private TransportInfo mTransport1;
- private TransportInfo mTransport2;
+ @Mock private OnTransportRegisteredListener mListener;
+ @Mock private TransportClientManager mTransportClientManager;
+ private TransportData mTransportA1;
+ private TransportData mTransportA2;
+ private TransportData mTransportB1;
- private ShadowPackageManager mPackageManagerShadow;
-
- private final TransportBoundListenerStub mTransportBoundListenerStub =
- new TransportBoundListenerStub(true);
-
- private final TransportReadyCallbackStub mTransportReadyCallbackStub =
- new TransportReadyCallbackStub();
+ private ShadowPackageManager mShadowPackageManager;
+ private Context mContext;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
- ShadowLog.stream = System.out;
-
- mPackageManagerShadow =
- (ShadowPackageManagerForBackup)
+ mShadowPackageManager =
+ (FrameworkShadowPackageManager)
extract(RuntimeEnvironment.application.getPackageManager());
+ mContext = RuntimeEnvironment.application.getApplicationContext();
- 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.binderInterface);
- ShadowBackupTransportStub.sBinderTransportMap.put(
- mTransport2.binder, mTransport2.binderInterface);
+ mTransportA1 = genericTransport(PACKAGE_A, "TransportFoo");
+ mTransportA2 = genericTransport(PACKAGE_A, "TransportBar");
+ mTransportB1 = genericTransport(PACKAGE_B, "TransportBaz");
}
@After
@@ -131,613 +110,518 @@ public class TransportManagerTest {
}
@Test
- public void onPackageAdded_bindsToAllTransports() throws Exception {
- setUpPackageWithTransports(PACKAGE_NAME, Arrays.asList(mTransport1, mTransport2),
- ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
-
- TransportManager transportManager = new TransportManager(
- RuntimeEnvironment.application.getApplicationContext(),
- new HashSet<>(Arrays.asList(
- mTransport1.componentName, mTransport2.componentName)),
- null /* defaultTransport */,
- mTransportBoundListenerStub,
- ShadowLooper.getMainLooper());
- transportManager.onPackageAdded(PACKAGE_NAME);
-
- assertThat(transportManager.getAllTransportComponents()).asList().containsExactlyElementsIn(
- Arrays.asList(mTransport1.componentName, mTransport2.componentName));
- assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
- Arrays.asList(mTransport1.name, mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
- .isTrue();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
- .isTrue();
- }
-
- @Test
- public void onPackageAdded_oneTransportUnavailable_bindsToOnlyOneTransport() throws Exception {
- setUpPackageWithTransports(PACKAGE_NAME, Arrays.asList(mTransport1, mTransport2),
- ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
-
- ShadowContextImplForBackup.sUnbindableComponents.add(mTransport1.componentName);
-
- TransportManager transportManager = new TransportManager(
- RuntimeEnvironment.application.getApplicationContext(),
- new HashSet<>(Arrays.asList(
- mTransport1.componentName, mTransport2.componentName)),
- null /* defaultTransport */,
- mTransportBoundListenerStub,
- ShadowLooper.getMainLooper());
- transportManager.onPackageAdded(PACKAGE_NAME);
-
- assertThat(transportManager.getAllTransportComponents()).asList().containsExactlyElementsIn(
- Collections.singleton(mTransport2.componentName));
- assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
- Collections.singleton(mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
- .isFalse();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
- .isTrue();
- }
-
- @Test
- public void onPackageAdded_whitelistIsNull_doesNotBindToTransports() throws Exception {
- setUpPackageWithTransports(PACKAGE_NAME, Arrays.asList(mTransport1, mTransport2),
- ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ public void testRegisterTransports() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpPackage(PACKAGE_B, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportA2, mTransportB1);
+ TransportManager transportManager =
+ createTransportManager(mTransportA1, mTransportA2, mTransportB1);
- TransportManager transportManager = new TransportManager(
- RuntimeEnvironment.application.getApplicationContext(),
- null /* whitelist */,
- null /* defaultTransport */,
- mTransportBoundListenerStub,
- ShadowLooper.getMainLooper());
- transportManager.onPackageAdded(PACKAGE_NAME);
+ transportManager.registerTransports();
- assertThat(transportManager.getAllTransportComponents()).isEmpty();
- assertThat(transportManager.getBoundTransportNames()).isEmpty();
- assertThat(mTransportBoundListenerStub.isCalled()).isFalse();
- }
+ assertRegisteredTransports(
+ transportManager, asList(mTransportA1, mTransportA2, mTransportB1));
- @Test
- public void onPackageAdded_onlyOneTransportWhitelisted_onlyConnectsToWhitelistedTransport()
- throws Exception {
- setUpPackageWithTransports(PACKAGE_NAME, Arrays.asList(mTransport1, mTransport2),
- ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
-
- TransportManager transportManager = new TransportManager(
- RuntimeEnvironment.application.getApplicationContext(),
- new HashSet<>(Collections.singleton(mTransport2.componentName)),
- null /* defaultTransport */,
- mTransportBoundListenerStub,
- ShadowLooper.getMainLooper());
- transportManager.onPackageAdded(PACKAGE_NAME);
-
- assertThat(transportManager.getAllTransportComponents()).asList().containsExactlyElementsIn(
- Collections.singleton(mTransport2.componentName));
- assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
- Collections.singleton(mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
- .isFalse();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
- .isTrue();
+ verify(mListener)
+ .onTransportRegistered(mTransportA1.transportName, mTransportA1.transportDirName);
+ verify(mListener)
+ .onTransportRegistered(mTransportA2.transportName, mTransportA2.transportDirName);
+ verify(mListener)
+ .onTransportRegistered(mTransportB1.transportName, mTransportB1.transportDirName);
}
@Test
- public void onPackageAdded_appIsNotPrivileged_doesNotBindToTransports() throws Exception {
- setUpPackageWithTransports(PACKAGE_NAME, Arrays.asList(mTransport1, mTransport2), 0);
+ public void
+ testRegisterTransports_whenOneTransportUnavailable_doesNotRegisterUnavailableTransport()
+ throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ TransportData transport1 = mTransportA1.unavailable();
+ TransportData transport2 = mTransportA2;
+ setUpTransports(transport1, transport2);
+ TransportManager transportManager = createTransportManager(transport1, transport2);
- TransportManager transportManager = new TransportManager(
- RuntimeEnvironment.application.getApplicationContext(),
- new HashSet<>(Arrays.asList(
- mTransport1.componentName, mTransport2.componentName)),
- null /* defaultTransport */,
- mTransportBoundListenerStub,
- ShadowLooper.getMainLooper());
- transportManager.onPackageAdded(PACKAGE_NAME);
+ transportManager.registerTransports();
- assertThat(transportManager.getAllTransportComponents()).isEmpty();
- assertThat(transportManager.getBoundTransportNames()).isEmpty();
- assertThat(mTransportBoundListenerStub.isCalled()).isFalse();
+ assertRegisteredTransports(transportManager, singletonList(transport2));
+ verify(mListener, never())
+ .onTransportRegistered(transport1.transportName, transport1.transportDirName);
+ verify(mListener)
+ .onTransportRegistered(transport2.transportName, transport2.transportDirName);
}
@Test
- public void onPackageRemoved_transportsUnbound() throws Exception {
- TransportManager transportManager = createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ public void testRegisterTransports_whenWhitelistIsEmpty_doesNotRegisterTransports()
+ throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportA2);
+ TransportManager transportManager = createTransportManager(null);
- transportManager.onPackageRemoved(PACKAGE_NAME);
+ transportManager.registerTransports();
- assertThat(transportManager.getAllTransportComponents()).isEmpty();
- assertThat(transportManager.getBoundTransportNames()).isEmpty();
+ assertRegisteredTransports(transportManager, emptyList());
+ verify(mListener, never()).onTransportRegistered(any(), any());
}
@Test
- public void onPackageRemoved_incorrectPackageName_nothingHappens() throws Exception {
- TransportManager transportManager = createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ public void
+ testRegisterTransports_whenOnlyOneTransportWhitelisted_onlyRegistersWhitelistedTransport()
+ throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportA2);
+ TransportManager transportManager = createTransportManager(null, mTransportA1);
- transportManager.onPackageRemoved(ANOTHER_PACKAGE_NAME);
+ transportManager.registerTransports();
- assertThat(transportManager.getAllTransportComponents()).asList().containsExactlyElementsIn(
- Arrays.asList(mTransport1.componentName, mTransport2.componentName));
- assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
- Arrays.asList(mTransport1.name, mTransport2.name));
+ assertRegisteredTransports(transportManager, singletonList(mTransportA1));
+ verify(mListener)
+ .onTransportRegistered(mTransportA1.transportName, mTransportA1.transportDirName);
+ verify(mListener, never())
+ .onTransportRegistered(mTransportA2.transportName, mTransportA2.transportDirName);
}
@Test
- public void onPackageChanged_oneComponentChanged_onlyOneTransportRebound() throws Exception {
- TransportManager transportManager = createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ public void testRegisterTransports_whenAppIsNotPrivileged_doesNotRegisterTransports()
+ throws Exception {
+ // Note ApplicationInfo.PRIVATE_FLAG_PRIVILEGED is missing from flags
+ setUpPackage(PACKAGE_A, 0);
+ setUpTransports(mTransportA1, mTransportA2);
+ TransportManager transportManager =
+ createTransportManager(null, mTransportA1, mTransportA2);
- transportManager.onPackageChanged(PACKAGE_NAME, new String[]{mTransport2.name});
+ transportManager.registerTransports();
- assertThat(transportManager.getAllTransportComponents()).asList().containsExactlyElementsIn(
- Arrays.asList(mTransport1.componentName, mTransport2.componentName));
- assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
- Arrays.asList(mTransport1.name, mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
- .isFalse();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
- .isTrue();
+ assertRegisteredTransports(transportManager, emptyList());
+ verify(mListener, never()).onTransportRegistered(any(), any());
}
@Test
- public void onPackageChanged_nothingChanged_noTransportsRebound() throws Exception {
- TransportManager transportManager = createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ public void testOnPackageAdded_registerTransports() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1);
+ TransportManager transportManager = createTransportManager(mTransportA1);
- transportManager.onPackageChanged(PACKAGE_NAME, new String[0]);
+ transportManager.onPackageAdded(PACKAGE_A);
- assertThat(transportManager.getAllTransportComponents()).asList().containsExactlyElementsIn(
- Arrays.asList(mTransport1.componentName, mTransport2.componentName));
- assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
- Arrays.asList(mTransport1.name, mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
- .isFalse();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
- .isFalse();
+ assertRegisteredTransports(transportManager, asList(mTransportA1));
+ verify(mListener)
+ .onTransportRegistered(mTransportA1.transportName, mTransportA1.transportDirName);
}
@Test
- public void onPackageChanged_unexpectedComponentChanged_noTransportsRebound() throws Exception {
- TransportManager transportManager = createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ public void testOnPackageRemoved_unregisterTransports() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpPackage(PACKAGE_B, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportB1);
+ TransportManager transportManager = createTransportManager(mTransportA1, mTransportB1);
+ transportManager.registerTransports();
- transportManager.onPackageChanged(PACKAGE_NAME, new String[]{"unexpected.component"});
+ transportManager.onPackageRemoved(PACKAGE_A);
- assertThat(transportManager.getAllTransportComponents()).asList().containsExactlyElementsIn(
- Arrays.asList(mTransport1.componentName, mTransport2.componentName));
- assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
- Arrays.asList(mTransport1.name, mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
- .isFalse();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
- .isFalse();
+ assertRegisteredTransports(transportManager, singletonList(mTransportB1));
}
@Test
- public void onPackageChanged_transportsRebound() throws Exception {
- TransportManager transportManager = createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
-
- transportManager.onPackageChanged(PACKAGE_NAME, new String[]{mTransport2.name});
-
- assertThat(transportManager.getAllTransportComponents()).asList().containsExactlyElementsIn(
- Arrays.asList(mTransport1.componentName, mTransport2.componentName));
- assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
- Arrays.asList(mTransport1.name, mTransport2.name));
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport1.binderInterface))
- .isFalse();
- assertThat(mTransportBoundListenerStub.isCalledForTransport(mTransport2.binderInterface))
- .isTrue();
- }
+ public void testOnPackageRemoved_whenUnknownPackage_nothingHappens() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1);
+ TransportManager transportManager = createTransportManager(mTransportA1);
+ transportManager.registerTransports();
- @Test
- public void getTransportBinder_returnsCorrectBinder() throws Exception {
- TransportManager transportManager = createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ transportManager.onPackageRemoved(PACKAGE_A + "unknown");
- assertThat(transportManager.getTransportBinder(mTransport1.name)).isEqualTo(
- mTransport1.binderInterface);
- assertThat(transportManager.getTransportBinder(mTransport2.name)).isEqualTo(
- mTransport2.binderInterface);
+ assertRegisteredTransports(transportManager, singletonList(mTransportA1));
}
@Test
- public void getTransportBinder_incorrectTransportName_returnsNull() throws Exception {
- TransportManager transportManager = createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
-
- assertThat(transportManager.getTransportBinder("incorrect.transport")).isNull();
- }
+ public void testOnPackageChanged_whenOneComponentChanged_onlyOneTransportReRegistered()
+ throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportA2);
+ TransportManager transportManager = createTransportManager(mTransportA1, mTransportA2);
+ transportManager.registerTransports();
+ // Reset listener to verify calls after registerTransports() above
+ reset(mListener);
- @Test
- public void getTransportBinder_oneTransportUnavailable_returnsCorrectBinder() throws Exception {
- TransportManager transportManager =
- createTransportManagerAndSetUpTransports(Collections.singletonList(mTransport2),
- Collections.singletonList(mTransport1), mTransport1.name);
+ transportManager.onPackageChanged(
+ PACKAGE_A, mTransportA1.getTransportComponent().getClassName());
- assertThat(transportManager.getTransportBinder(mTransport1.name)).isNull();
- assertThat(transportManager.getTransportBinder(mTransport2.name)).isEqualTo(
- mTransport2.binderInterface);
+ assertRegisteredTransports(transportManager, asList(mTransportA1, mTransportA2));
+ verify(mListener)
+ .onTransportRegistered(mTransportA1.transportName, mTransportA1.transportDirName);
+ verify(mListener, never())
+ .onTransportRegistered(mTransportA2.transportName, mTransportA2.transportDirName);
}
@Test
- public void getCurrentTransport_selectTransportNotCalled_returnsDefaultTransport()
+ public void testOnPackageChanged_whenNoComponentsChanged_doesNotRegisterTransports()
throws Exception {
- TransportManager transportManager = createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1);
+ TransportManager transportManager = createTransportManager(mTransportA1);
+ transportManager.registerTransports();
+ reset(mListener);
+
+ transportManager.onPackageChanged(PACKAGE_A);
- assertThat(transportManager.getCurrentTransportName()).isEqualTo(mTransport1.name);
+ assertRegisteredTransports(transportManager, singletonList(mTransportA1));
+ verify(mListener, never()).onTransportRegistered(any(), any());
}
@Test
- public void getCurrentTransport_selectTransportCalled_returnsCorrectTransport()
+ public void testOnPackageChanged_whenUnknownComponentChanged_noTransportsRegistered()
throws Exception {
- TransportManager transportManager = createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
-
- assertThat(transportManager.getCurrentTransportName()).isEqualTo(mTransport1.name);
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1);
+ TransportManager transportManager = createTransportManager(mTransportA1);
+ transportManager.registerTransports();
+ reset(mListener);
- transportManager.selectTransport(mTransport2.name);
+ transportManager.onPackageChanged(PACKAGE_A, PACKAGE_A + ".UnknownComponent");
- assertThat(transportManager.getCurrentTransportName()).isEqualTo(mTransport2.name);
+ assertRegisteredTransports(transportManager, singletonList(mTransportA1));
+ verify(mListener, never()).onTransportRegistered(any(), any());
}
@Test
- public void getCurrentTransportBinder_returnsCorrectBinder() throws Exception {
- TransportManager transportManager = createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ public void testOnPackageChanged_reRegisterTransports() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportA2);
+ TransportManager transportManager = createTransportManager(mTransportA1, mTransportA2);
+ transportManager.registerTransports();
+ reset(mListener);
- assertThat(transportManager.getCurrentTransportBinder())
- .isEqualTo(mTransport1.binderInterface);
- }
-
- @Test
- public void getCurrentTransportBinder_transportNotBound_returnsNull() throws Exception {
- TransportManager transportManager =
- createTransportManagerAndSetUpTransports(Collections.singletonList(mTransport2),
- Collections.singletonList(mTransport1), mTransport2.name);
-
- transportManager.selectTransport(mTransport1.name);
+ transportManager.onPackageChanged(
+ PACKAGE_A,
+ mTransportA1.getTransportComponent().getClassName(),
+ mTransportA2.getTransportComponent().getClassName());
- assertThat(transportManager.getCurrentTransportBinder()).isNull();
+ assertRegisteredTransports(transportManager, asList(mTransportA1, mTransportA2));
+ verify(mListener)
+ .onTransportRegistered(mTransportA1.transportName, mTransportA1.transportDirName);
+ verify(mListener)
+ .onTransportRegistered(mTransportA2.transportName, mTransportA2.transportDirName);
}
@Test
- public void getTransportName_returnsCorrectTransportName() throws Exception {
- TransportManager transportManager = createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
-
- assertThat(transportManager.getTransportName(mTransport1.binderInterface))
- .isEqualTo(mTransport1.name);
- assertThat(transportManager.getTransportName(mTransport2.binderInterface))
- .isEqualTo(mTransport2.name);
- }
+ public void testRegisterAndSelectTransport_whenTransportRegistered() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1);
+ TransportManager transportManager = createTransportManager(null, mTransportA1);
+ transportManager.registerTransports();
+ ComponentName transportComponent = mTransportA1.getTransportComponent();
- @Test
- public void getTransportName_transportNotBound_returnsNull() throws Exception {
- TransportManager transportManager =
- createTransportManagerAndSetUpTransports(Collections.singletonList(mTransport2),
- Collections.singletonList(mTransport1), mTransport1.name);
+ int result = transportManager.registerAndSelectTransport(transportComponent);
- assertThat(transportManager.getTransportName(mTransport1.binderInterface)).isNull();
- assertThat(transportManager.getTransportName(mTransport2.binderInterface))
- .isEqualTo(mTransport2.name);
+ assertThat(result).isEqualTo(BackupManager.SUCCESS);
+ assertThat(transportManager.getRegisteredTransportComponents())
+ .asList()
+ .contains(transportComponent);
+ assertThat(transportManager.getCurrentTransportName())
+ .isEqualTo(mTransportA1.transportName);
}
@Test
- public void getTransportWhitelist_returnsCorrectWhiteList() throws Exception {
- TransportManager transportManager = new TransportManager(
- RuntimeEnvironment.application.getApplicationContext(),
- new HashSet<>(Arrays.asList(mTransport1.componentName, mTransport2.componentName)),
- mTransport1.name,
- mTransportBoundListenerStub,
- ShadowLooper.getMainLooper());
+ public void testRegisterAndSelectTransport_whenTransportNotRegistered() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1);
+ TransportManager transportManager = createTransportManager(null, mTransportA1);
+ ComponentName transportComponent = mTransportA1.getTransportComponent();
- assertThat(transportManager.getTransportWhitelist()).containsExactlyElementsIn(
- Arrays.asList(mTransport1.componentName, mTransport2.componentName));
- }
-
- @Test
- public void getTransportWhitelist_whiteListIsNull_returnsEmptyArray() throws Exception {
- TransportManager transportManager = new TransportManager(
- RuntimeEnvironment.application.getApplicationContext(),
- null /* whitelist */,
- mTransport1.name,
- mTransportBoundListenerStub,
- ShadowLooper.getMainLooper());
+ int result = transportManager.registerAndSelectTransport(transportComponent);
- assertThat(transportManager.getTransportWhitelist()).isEmpty();
+ assertThat(result).isEqualTo(BackupManager.SUCCESS);
+ assertThat(transportManager.getRegisteredTransportComponents())
+ .asList()
+ .contains(transportComponent);
+ assertThat(transportManager.getTransportDirName(mTransportA1.transportName))
+ .isEqualTo(mTransportA1.transportDirName);
+ assertThat(transportManager.getCurrentTransportName())
+ .isEqualTo(mTransportA1.transportName);
}
@Test
- public void selectTransport_setsTransportCorrectlyAndReturnsPreviousTransport()
+ public void testGetCurrentTransportName_whenSelectTransportNotCalled_returnsDefaultTransport()
throws Exception {
- TransportManager transportManager = new TransportManager(
- RuntimeEnvironment.application.getApplicationContext(),
- null /* whitelist */,
- mTransport1.name,
- mTransportBoundListenerStub,
- ShadowLooper.getMainLooper());
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportA2);
+ TransportManager transportManager = createTransportManager(mTransportA1, mTransportA2);
+ transportManager.registerTransports();
- assertThat(transportManager.selectTransport(mTransport2.name)).isEqualTo(mTransport1.name);
- assertThat(transportManager.selectTransport(mTransport1.name)).isEqualTo(mTransport2.name);
+ String currentTransportName = transportManager.getCurrentTransportName();
+
+ assertThat(currentTransportName).isEqualTo(mTransportA1.transportName);
}
@Test
- public void ensureTransportReady_transportNotYetBound_callsListenerOnFailure()
+ public void testGetCurrentTransport_whenSelectTransportCalled_returnsSelectedTransport()
throws Exception {
- setUpPackageWithTransports(PACKAGE_NAME, Arrays.asList(mTransport1, mTransport2),
- ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
-
- TransportManager transportManager = new TransportManager(
- RuntimeEnvironment.application.getApplicationContext(),
- new HashSet<>(Arrays.asList(mTransport1.componentName, mTransport2.componentName)),
- mTransport1.name,
- mTransportBoundListenerStub,
- ShadowLooper.getMainLooper());
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportA2);
+ TransportManager transportManager = createTransportManager(mTransportA1, mTransportA2);
+ transportManager.registerTransports();
+ transportManager.selectTransport(mTransportA2.transportName);
- transportManager.ensureTransportReady(mTransport1.componentName,
- mTransportReadyCallbackStub);
+ String currentTransportName = transportManager.getCurrentTransportName();
- assertThat(mTransportReadyCallbackStub.getSuccessCalls()).isEmpty();
- assertThat(mTransportReadyCallbackStub.getFailureCalls()).containsExactlyElementsIn(
- Collections.singleton(
- BackupManager.ERROR_TRANSPORT_UNAVAILABLE));
+ assertThat(currentTransportName).isEqualTo(mTransportA2.transportName);
}
@Test
- public void ensureTransportReady_transportCannotBeBound_callsListenerOnFailure()
- throws Exception {
- TransportManager transportManager =
- createTransportManagerAndSetUpTransports(Collections.singletonList(mTransport2),
- Collections.singletonList(mTransport1), mTransport1.name);
+ public void testGetTransportWhitelist() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportA2);
+ TransportManager transportManager = createTransportManager(mTransportA1, mTransportA2);
- transportManager.ensureTransportReady(mTransport1.componentName,
- mTransportReadyCallbackStub);
+ Set<ComponentName> transportWhitelist = transportManager.getTransportWhitelist();
- assertThat(mTransportReadyCallbackStub.getSuccessCalls()).isEmpty();
- assertThat(mTransportReadyCallbackStub.getFailureCalls()).containsExactlyElementsIn(
- Collections.singleton(
- BackupManager.ERROR_TRANSPORT_UNAVAILABLE));
+ assertThat(transportWhitelist)
+ .containsExactlyElementsIn(
+ asList(
+ mTransportA1.getTransportComponent(),
+ mTransportA2.getTransportComponent()));
}
@Test
- public void ensureTransportReady_transportsAlreadyBound_callsListenerOnSuccess()
- throws Exception {
+ public void testSelectTransport() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportA2);
TransportManager transportManager =
- createTransportManagerAndSetUpTransports(Collections.singletonList(mTransport2),
- Collections.singletonList(mTransport1), mTransport1.name);
+ createTransportManager(null, mTransportA1, mTransportA2);
- transportManager.ensureTransportReady(mTransport2.componentName,
- mTransportReadyCallbackStub);
+ String transport1 = transportManager.selectTransport(mTransportA1.transportName);
+ String transport2 = transportManager.selectTransport(mTransportA2.transportName);
- assertThat(mTransportReadyCallbackStub.getSuccessCalls()).containsExactlyElementsIn(
- Collections.singleton(mTransport2.name));
- assertThat(mTransportReadyCallbackStub.getFailureCalls()).isEmpty();
+ assertThat(transport1).isNull();
+ assertThat(transport2).isEqualTo(mTransportA1.transportName);
}
@Test
- public void getTransportClient_forRegisteredTransport_returnCorrectly() throws Exception {
- TransportManager transportManager =
- createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ public void testGetTransportClient_forRegisteredTransport() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportA2);
+ TransportManager transportManager = createTransportManager(mTransportA1, mTransportA2);
+ transportManager.registerTransports();
TransportClient transportClient =
- transportManager.getTransportClient(mTransport1.name, "caller");
+ transportManager.getTransportClient(mTransportA1.transportName, "caller");
- assertThat(transportClient.getTransportComponent()).isEqualTo(mTransport1.componentName);
+ assertThat(transportClient.getTransportComponent())
+ .isEqualTo(mTransportA1.getTransportComponent());
}
@Test
- public void getTransportClient_forOldNameOfTransportThatChangedName_returnsNull()
+ public void testGetTransportClient_forOldNameOfTransportThatChangedName_returnsNull()
throws Exception {
- TransportManager transportManager =
- createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportA2);
+ TransportManager transportManager = createTransportManager(mTransportA1, mTransportA2);
+ transportManager.registerTransports();
transportManager.updateTransportAttributes(
- mTransport1.componentName, "newName", null, "destinationString", null, null);
+ mTransportA1.getTransportComponent(),
+ "newName",
+ null,
+ "destinationString",
+ null,
+ null);
TransportClient transportClient =
- transportManager.getTransportClient(mTransport1.name, "caller");
+ transportManager.getTransportClient(mTransportA1.transportName, "caller");
assertThat(transportClient).isNull();
}
@Test
- public void getTransportClient_forNewNameOfTransportThatChangedName_returnsCorrectly()
+ public void testGetTransportClient_forNewNameOfTransportThatChangedName_returnsCorrectly()
throws Exception {
- TransportManager transportManager =
- createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportA2);
+ TransportManager transportManager = createTransportManager(mTransportA1, mTransportA2);
+ transportManager.registerTransports();
transportManager.updateTransportAttributes(
- mTransport1.componentName, "newName", null, "destinationString", null, null);
+ mTransportA1.getTransportComponent(),
+ "newName",
+ null,
+ "destinationString",
+ null,
+ null);
- TransportClient transportClient =
- transportManager.getTransportClient("newName", "caller");
+ TransportClient transportClient = transportManager.getTransportClient("newName", "caller");
- assertThat(transportClient.getTransportComponent()).isEqualTo(mTransport1.componentName);
+ assertThat(transportClient.getTransportComponent())
+ .isEqualTo(mTransportA1.getTransportComponent());
}
@Test
- public void getTransportName_forTransportThatChangedName_returnsNewName()
- throws Exception {
- TransportManager transportManager =
- createTransportManagerAndSetUpTransports(
- Arrays.asList(mTransport1, mTransport2), mTransport1.name);
+ public void testGetTransportName_forTransportThatChangedName_returnsNewName() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1, mTransportA2);
+ TransportManager transportManager = createTransportManager(mTransportA1, mTransportA2);
+ transportManager.registerTransports();
transportManager.updateTransportAttributes(
- mTransport1.componentName, "newName", null, "destinationString", null, null);
+ mTransportA1.getTransportComponent(),
+ "newName",
+ null,
+ "destinationString",
+ null,
+ null);
- String transportName = transportManager.getTransportName(mTransport1.componentName);
+ String transportName =
+ transportManager.getTransportName(mTransportA1.getTransportComponent());
assertThat(transportName).isEqualTo("newName");
}
@Test
- public void isTransportRegistered_returnsCorrectly() throws Exception {
- TransportManager transportManager =
- createTransportManagerAndSetUpTransports(
- Collections.singletonList(mTransport1),
- Collections.singletonList(mTransport2),
- mTransport1.name);
+ public void testIsTransportRegistered() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1);
+ TransportManager transportManager = createTransportManager(mTransportA1, mTransportA2);
+ transportManager.registerTransports();
- assertThat(transportManager.isTransportRegistered(mTransport1.name)).isTrue();
- assertThat(transportManager.isTransportRegistered(mTransport2.name)).isFalse();
+ boolean isTransportA1Registered =
+ transportManager.isTransportRegistered(mTransportA1.transportName);
+ boolean isTransportA2Registered =
+ transportManager.isTransportRegistered(mTransportA2.transportName);
+
+ assertThat(isTransportA1Registered).isTrue();
+ assertThat(isTransportA2Registered).isFalse();
}
@Test
- public void getTransportAttributes_forRegisteredTransport_returnsCorrectValues()
+ public void testGetTransportAttributes_forRegisteredTransport_returnsCorrectValues()
throws Exception {
- TransportManager transportManager =
- createTransportManagerAndSetUpTransports(
- Collections.singletonList(mTransport1),
- mTransport1.name);
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1);
+ TransportManager transportManager = createTransportManager(mTransportA1);
+ transportManager.registerTransports();
+
+ Intent configurationIntent =
+ transportManager.getTransportConfigurationIntent(mTransportA1.transportName);
+ Intent dataManagementIntent =
+ transportManager.getTransportDataManagementIntent(mTransportA1.transportName);
+ String dataManagementLabel =
+ transportManager.getTransportDataManagementLabel(mTransportA1.transportName);
+ String transportDirName = transportManager.getTransportDirName(mTransportA1.transportName);
- 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());
+ assertThat(configurationIntent).isEqualTo(mTransportA1.configurationIntent);
+ assertThat(dataManagementIntent).isEqualTo(mTransportA1.dataManagementIntent);
+ assertThat(dataManagementLabel).isEqualTo(mTransportA1.dataManagementLabel);
+ assertThat(transportDirName).isEqualTo(mTransportA1.transportDirName);
}
@Test
- public void getTransportAttributes_forUnregisteredTransport_throws()
- throws Exception {
- TransportManager transportManager =
- createTransportManagerAndSetUpTransports(
- Collections.singletonList(mTransport1),
- Collections.singletonList(mTransport2),
- mTransport1.name);
+ public void testGetTransportAttributes_forUnregisteredTransport_throws() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpTransports(mTransportA1);
+ TransportManager transportManager = createTransportManager(mTransportA1);
+ transportManager.registerTransports();
expectThrows(
TransportNotRegisteredException.class,
- () -> transportManager.getTransportConfigurationIntent(mTransport2.name));
+ () -> transportManager.getTransportConfigurationIntent(mTransportA2.transportName));
expectThrows(
TransportNotRegisteredException.class,
- () -> transportManager.getTransportDataManagementIntent(
- mTransport2.name));
+ () ->
+ transportManager.getTransportDataManagementIntent(
+ mTransportA2.transportName));
expectThrows(
TransportNotRegisteredException.class,
- () -> transportManager.getTransportDataManagementLabel(mTransport2.name));
+ () -> transportManager.getTransportDataManagementLabel(mTransportA2.transportName));
expectThrows(
TransportNotRegisteredException.class,
- () -> transportManager.getTransportDirName(mTransport2.name));
+ () -> transportManager.getTransportDirName(mTransportA2.transportName));
}
- private void setUpPackageWithTransports(String packageName, List<TransportInfo> transports,
- int flags) throws Exception {
- PackageInfo packageInfo = new PackageInfo();
- packageInfo.packageName = packageName;
- packageInfo.applicationInfo = new ApplicationInfo();
- packageInfo.applicationInfo.privateFlags = flags;
-
- mPackageManagerShadow.addPackage(packageInfo);
-
- List<ResolveInfo> transportsInfo = new ArrayList<>();
- for (TransportInfo transport : transports) {
- ResolveInfo info = new ResolveInfo();
- info.serviceInfo = new ServiceInfo();
- info.serviceInfo.packageName = packageName;
- info.serviceInfo.name = transport.name;
- transportsInfo.add(info);
- }
-
- Intent intent = new Intent(TransportManager.SERVICE_ACTION_TRANSPORT_HOST);
- intent.setPackage(packageName);
+ @Test
+ public void testGetRegisteredTransportNames() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpPackage(PACKAGE_B, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ TransportData[] transportsData = {mTransportA1, mTransportA2, mTransportB1};
+ setUpTransports(transportsData);
+ TransportManager transportManager =
+ createTransportManager(mTransportA1, mTransportA2, mTransportB1);
+ transportManager.registerTransports();
- mPackageManagerShadow.addResolveInfoForIntent(intent, transportsInfo);
- }
+ String[] transportNames = transportManager.getRegisteredTransportNames();
- private TransportManager createTransportManagerAndSetUpTransports(
- List<TransportInfo> availableTransports, String defaultTransportName) throws Exception {
- return createTransportManagerAndSetUpTransports(availableTransports,
- Collections.<TransportInfo>emptyList(), defaultTransportName);
+ assertThat(transportNames)
+ .asList()
+ .containsExactlyElementsIn(
+ Stream.of(transportsData)
+ .map(transportData -> transportData.transportName)
+ .collect(toList()));
}
- private TransportManager createTransportManagerAndSetUpTransports(
- List<TransportInfo> availableTransports, List<TransportInfo> unavailableTransports,
- String defaultTransportName)
- throws Exception {
- List<String> availableTransportsNames = new ArrayList<>();
- List<ComponentName> availableTransportsComponentNames = new ArrayList<>();
- for (TransportInfo transport : availableTransports) {
- availableTransportsNames.add(transport.name);
- availableTransportsComponentNames.add(transport.componentName);
- }
-
- List<ComponentName> allTransportsComponentNames = new ArrayList<>();
- allTransportsComponentNames.addAll(availableTransportsComponentNames);
- for (TransportInfo transport : unavailableTransports) {
- allTransportsComponentNames.add(transport.componentName);
- }
-
- for (TransportInfo transport : unavailableTransports) {
- ShadowContextImplForBackup.sUnbindableComponents.add(transport.componentName);
- }
-
- setUpPackageWithTransports(PACKAGE_NAME, Arrays.asList(mTransport1, mTransport2),
- ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
-
- TransportManager transportManager = new TransportManager(
- RuntimeEnvironment.application.getApplicationContext(),
- new HashSet<>(allTransportsComponentNames),
- defaultTransportName,
- mTransportBoundListenerStub,
- ShadowLooper.getMainLooper());
- transportManager.onPackageAdded(PACKAGE_NAME);
-
- assertThat(transportManager.getAllTransportComponents()).asList().containsExactlyElementsIn(
- availableTransportsComponentNames);
- assertThat(transportManager.getBoundTransportNames()).asList().containsExactlyElementsIn(
- availableTransportsNames);
- for (TransportInfo transport : availableTransports) {
- assertThat(mTransportBoundListenerStub.isCalledForTransport(transport.binderInterface))
- .isTrue();
- }
- for (TransportInfo transport : unavailableTransports) {
- assertThat(mTransportBoundListenerStub.isCalledForTransport(transport.binderInterface))
- .isFalse();
+ @Test
+ public void testGetRegisteredTransportComponents() throws Exception {
+ setUpPackage(PACKAGE_A, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ setUpPackage(PACKAGE_B, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
+ TransportData[] transportsData = {mTransportA1, mTransportA2, mTransportB1};
+ setUpTransports(transportsData);
+ TransportManager transportManager =
+ createTransportManager(mTransportA1, mTransportA2, mTransportB1);
+ transportManager.registerTransports();
+
+ ComponentName[] transportNames = transportManager.getRegisteredTransportComponents();
+
+ assertThat(transportNames)
+ .asList()
+ .containsExactlyElementsIn(
+ Stream.of(transportsData)
+ .map(TransportData::getTransportComponent)
+ .collect(toList()));
+ }
+
+ private List<TransportMock> setUpTransports(TransportData... transports) throws Exception {
+ setUpTransportsForTransportManager(mShadowPackageManager, transports);
+ List<TransportMock> transportMocks = new ArrayList<>(transports.length);
+ for (TransportData transport : transports) {
+ TransportMock transportMock = mockTransport(transport);
+ when(mTransportClientManager.getTransportClient(
+ eq(transport.getTransportComponent()), any()))
+ .thenReturn(transportMock.transportClient);
+ transportMocks.add(transportMock);
}
+ return transportMocks;
+ }
- mTransportBoundListenerStub.resetState();
+ private void setUpPackage(String packageName, int flags) {
+ PackageInfo packageInfo = new PackageInfo();
+ packageInfo.packageName = packageName;
+ packageInfo.applicationInfo = new ApplicationInfo();
+ packageInfo.applicationInfo.privateFlags = flags;
+ mShadowPackageManager.addPackage(packageInfo);
+ }
+ private TransportManager createTransportManager(
+ @Nullable TransportData selectedTransport, TransportData... transports) {
+ Set<ComponentName> whitelist =
+ concat(Stream.of(selectedTransport), Stream.of(transports))
+ .filter(Objects::nonNull)
+ .map(TransportData::getTransportComponent)
+ .collect(toSet());
+ TransportManager transportManager =
+ new TransportManager(
+ mContext,
+ whitelist,
+ selectedTransport != null ? selectedTransport.transportName : null,
+ mTransportClientManager);
+ transportManager.setOnTransportRegisteredListener(mListener);
return transportManager;
}
- private static class TransportInfo {
- public final String packageName;
- public final String name;
- public final ComponentName componentName;
- public final IBackupTransport binderInterface;
- public final IBinder binder;
-
- 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.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;
- }
+ private void assertRegisteredTransports(
+ TransportManager transportManager, List<TransportData> transports) {
+ assertThat(transportManager.getRegisteredTransportComponents())
+ .asList()
+ .containsExactlyElementsIn(
+ transports
+ .stream()
+ .map(TransportData::getTransportComponent)
+ .collect(toList()));
+ assertThat(transportManager.getRegisteredTransportNames())
+ .asList()
+ .containsExactlyElementsIn(
+ transports.stream().map(t -> t.transportName).collect(toList()));
}
-
}
diff --git a/com/android/server/backup/internal/BackupState.java b/com/android/server/backup/internal/BackupState.java
index 4d42c240..937b1676 100644
--- a/com/android/server/backup/internal/BackupState.java
+++ b/com/android/server/backup/internal/BackupState.java
@@ -5,6 +5,7 @@ package com.android.server.backup.internal;
*/
enum BackupState {
INITIAL,
+ BACKUP_PM,
RUNNING_QUEUE,
FINAL
}
diff --git a/com/android/server/backup/internal/PerformBackupTask.java b/com/android/server/backup/internal/PerformBackupTask.java
index a002334d..99ffa12e 100644
--- a/com/android/server/backup/internal/PerformBackupTask.java
+++ b/com/android/server/backup/internal/PerformBackupTask.java
@@ -114,14 +114,14 @@ public class PerformBackupTask implements BackupRestoreTask {
private RefactoredBackupManagerService backupManagerService;
private final Object mCancelLock = new Object();
- ArrayList<BackupRequest> mQueue;
- ArrayList<BackupRequest> mOriginalQueue;
- File mStateDir;
- @Nullable DataChangedJournal mJournal;
- BackupState mCurrentState;
- List<String> mPendingFullBackups;
- IBackupObserver mObserver;
- IBackupManagerMonitor mMonitor;
+ private ArrayList<BackupRequest> mQueue;
+ private ArrayList<BackupRequest> mOriginalQueue;
+ private File mStateDir;
+ @Nullable private DataChangedJournal mJournal;
+ private BackupState mCurrentState;
+ private List<String> mPendingFullBackups;
+ private IBackupObserver mObserver;
+ private IBackupManagerMonitor mMonitor;
private final TransportClient mTransportClient;
private final OnTaskFinishedListener mListener;
@@ -130,18 +130,18 @@ public class PerformBackupTask implements BackupRestoreTask {
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 IBackupAgent mAgentBinder;
+ private PackageInfo mCurrentPackage;
+ private File mSavedStateName;
+ private File mBackupDataName;
+ private File mNewStateName;
+ private ParcelFileDescriptor mSavedState;
+ private ParcelFileDescriptor mBackupData;
+ private ParcelFileDescriptor mNewState;
+ private int mStatus;
+ private boolean mFinished;
+ private final boolean mUserInitiated;
+ private final boolean mNonIncremental;
private volatile boolean mCancelAll;
@@ -224,6 +224,10 @@ public class PerformBackupTask implements BackupRestoreTask {
beginBackup();
break;
+ case BACKUP_PM:
+ backupPm();
+ break;
+
case RUNNING_QUEUE:
invokeNextAgent();
break;
@@ -239,9 +243,8 @@ public class PerformBackupTask implements BackupRestoreTask {
}
}
- // We're starting a backup pass. Initialize the transport and send
- // the PM metadata blob if we haven't already.
- void beginBackup() {
+ // We're starting a backup pass. Initialize the transport if we haven't already.
+ private void beginBackup() {
if (DEBUG_BACKUP_TRACE) {
backupManagerService.clearBackupTrace();
StringBuilder b = new StringBuilder(256);
@@ -320,56 +323,80 @@ public class PerformBackupTask implements BackupRestoreTask {
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.
+ // As the package manager is running here in the system process we can just set up
+ // its agent directly. Thus 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 = backupManagerService.makeMetadataAgent();
- mStatus = invokeAgentForBackup(
- PACKAGE_MANAGER_SENTINEL,
- IBackupAgent.Stub.asInterface(pmAgent.onBind()));
- backupManagerService.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.
- backupManagerService.getBackupHandler().removeMessages(
- MSG_BACKUP_OPERATION_TIMEOUT);
+ executeNextState(BackupState.BACKUP_PM);
}
}
-
- 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 during init", e);
+ backupManagerService.addBackupTrace("Exception in backup thread during init: " + e);
+ mStatus = BackupTransport.TRANSPORT_ERROR;
+ } finally {
+ // If we've succeeded so far, we will move to the BACKUP_PM state. If something has gone
+ // wrong then that won't have happen so cleanup.
+ backupManagerService.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.
+ backupManagerService.resetBackupState(mStateDir); // Just to make sure.
+ // In case of any other error, it's backup transport error.
+ BackupObserverUtils.sendBackupFinished(mObserver,
+ BackupManager.ERROR_TRANSPORT_ABORTED);
+ executeNextState(BackupState.FINAL);
}
+ }
+ }
+
+ private void backupPm() {
+ try {
+ // 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.
+ PackageManagerBackupAgent pmAgent = backupManagerService.makeMetadataAgent();
+ mStatus = invokeAgentForBackup(
+ PACKAGE_MANAGER_SENTINEL,
+ IBackupAgent.Stub.asInterface(pmAgent.onBind()));
+ backupManagerService.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.
+ backupManagerService.getBackupHandler().removeMessages(
+ MSG_BACKUP_OPERATION_TIMEOUT);
} catch (Exception e) {
- Slog.e(TAG, "Error in backup thread", e);
- backupManagerService.addBackupTrace("Exception in backup thread: " + e);
+ Slog.e(TAG, "Error in backup thread during pm", e);
+ backupManagerService.addBackupTrace("Exception in backup thread during pm: " + 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.
- backupManagerService.addBackupTrace("exiting prelim: " + mStatus);
+ backupManagerService.addBackupTrace("exiting backupPm: " + mStatus);
if (mStatus != BackupTransport.TRANSPORT_OK) {
// if things went wrong at this point, we need to
// restage everything and try again later.
backupManagerService.resetBackupState(mStateDir); // Just to make sure.
- // In case of any other error, it's backup transport error.
BackupObserverUtils.sendBackupFinished(mObserver,
- BackupManager.ERROR_TRANSPORT_ABORTED);
+ invokeAgentToObserverError(mStatus));
executeNextState(BackupState.FINAL);
}
}
}
+ private int invokeAgentToObserverError(int error) {
+ if (error == BackupTransport.AGENT_ERROR) {
+ return BackupManager.ERROR_AGENT_FAILURE;
+ } else {
+ return BackupManager.ERROR_TRANSPORT_ABORTED;
+ }
+ }
+
// 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() {
+ private void invokeNextAgent() {
mStatus = BackupTransport.TRANSPORT_OK;
backupManagerService.addBackupTrace("invoke q=" + mQueue.size());
@@ -511,7 +538,7 @@ public class PerformBackupTask implements BackupRestoreTask {
}
}
- void finalizeBackup() {
+ private void finalizeBackup() {
backupManagerService.addBackupTrace("finishing");
// Mark packages that we didn't backup (because backup was cancelled, etc.) as needing
@@ -560,16 +587,9 @@ public class PerformBackupTask implements BackupRestoreTask {
}
backupManagerService.addBackupTrace("init required; rerunning");
try {
- final String name = backupManagerService.getTransportManager()
+ String name = backupManagerService.getTransportManager()
.getTransportName(mTransportClient.getTransportComponent());
- if (name != null) {
- backupManagerService.getPendingInits().add(name);
- } else {
- if (DEBUG) {
- Slog.w(TAG, "Couldn't find name of transport "
- + mTransportClient.getTransportComponent() + " for init");
- }
- }
+ backupManagerService.getPendingInits().add(name);
} 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
@@ -624,14 +644,14 @@ public class PerformBackupTask implements BackupRestoreTask {
}
// Remove the PM metadata state. This will generate an init on the next pass.
- void clearMetadata() {
+ private 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) {
+ private int invokeAgentForBackup(String packageName, IBackupAgent agent) {
if (DEBUG) {
Slog.d(TAG, "invokeAgentForBackup on " + packageName);
}
@@ -718,7 +738,7 @@ public class PerformBackupTask implements BackupRestoreTask {
return BackupTransport.TRANSPORT_OK;
}
- public void failAgent(IBackupAgent agent, String message) {
+ private void failAgent(IBackupAgent agent, String message) {
try {
agent.fail(message);
} catch (Exception e) {
@@ -910,14 +930,36 @@ public class PerformBackupTask implements BackupRestoreTask {
TransportUtils.checkTransportNotNull(transport);
size = mBackupDataName.length();
if (size > 0) {
+ boolean isNonIncremental = mSavedStateName.length() == 0;
if (mStatus == BackupTransport.TRANSPORT_OK) {
backupData = ParcelFileDescriptor.open(mBackupDataName,
ParcelFileDescriptor.MODE_READ_ONLY);
backupManagerService.addBackupTrace("sending data to transport");
- int flags = mUserInitiated ? BackupTransport.FLAG_USER_INITIATED : 0;
+
+ int userInitiatedFlag =
+ mUserInitiated ? BackupTransport.FLAG_USER_INITIATED : 0;
+ int incrementalFlag =
+ isNonIncremental
+ ? BackupTransport.FLAG_NON_INCREMENTAL
+ : BackupTransport.FLAG_INCREMENTAL;
+ int flags = userInitiatedFlag | incrementalFlag;
+
mStatus = transport.performBackup(mCurrentPackage, backupData, flags);
}
+ if (isNonIncremental
+ && mStatus == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) {
+ // TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED is only valid if the backup was
+ // incremental, as if the backup is non-incremental there is no state to
+ // clear. This avoids us ending up in a retry loop if the transport always
+ // returns this code.
+ Slog.w(TAG,
+ "Transport requested non-incremental but already the case, error");
+ backupManagerService.addBackupTrace(
+ "Transport requested non-incremental but already the case, error");
+ mStatus = BackupTransport.TRANSPORT_ERROR;
+ }
+
// 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
@@ -965,6 +1007,31 @@ public class PerformBackupTask implements BackupRestoreTask {
BackupObserverUtils.sendBackupOnPackageResult(mObserver, pkgName,
BackupManager.ERROR_TRANSPORT_QUOTA_EXCEEDED);
EventLog.writeEvent(EventLogTags.BACKUP_QUOTA_EXCEEDED, pkgName);
+
+ } else if (mStatus == BackupTransport.TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED) {
+ Slog.i(TAG, "Transport lost data, retrying package");
+ backupManagerService.addBackupTrace(
+ "Transport lost data, retrying package:" + pkgName);
+ BackupManagerMonitorUtils.monitorEvent(
+ mMonitor,
+ BackupManagerMonitor
+ .LOG_EVENT_ID_TRANSPORT_NON_INCREMENTAL_BACKUP_REQUIRED,
+ mCurrentPackage,
+ BackupManagerMonitor.LOG_EVENT_CATEGORY_TRANSPORT,
+ /*extras=*/ null);
+
+ mBackupDataName.delete();
+ mSavedStateName.delete();
+ mNewStateName.delete();
+
+ // Immediately retry the package by adding it back to the front of the queue.
+ // We cannot add @pm@ to the queue because we back it up separately at the start
+ // of the backup pass in state BACKUP_PM. Instead we retry this state (see
+ // below).
+ if (!PACKAGE_MANAGER_SENTINEL.equals(pkgName)) {
+ mQueue.add(0, new BackupRequest(pkgName));
+ }
+
} else {
// Actual transport-level failure to communicate the data to the backend
BackupObserverUtils.sendBackupOnPackageResult(mObserver, pkgName,
@@ -990,6 +1057,17 @@ public class PerformBackupTask implements BackupRestoreTask {
// 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_NON_INCREMENTAL_BACKUP_REQUIRED) {
+ // We want to immediately retry the current package.
+ if (PACKAGE_MANAGER_SENTINEL.equals(pkgName)) {
+ nextState = BackupState.BACKUP_PM;
+ } else {
+ // This is an ordinary package so we will have added it back into the queue
+ // above. Thus, we proceed processing the queue.
+ nextState = BackupState.RUNNING_QUEUE;
+ }
+
} else if (mStatus == BackupTransport.TRANSPORT_QUOTA_EXCEEDED) {
if (MORE_DEBUG) {
Slog.d(TAG, "Package " + mCurrentPackage.packageName +
@@ -1058,7 +1136,7 @@ public class PerformBackupTask implements BackupRestoreTask {
}
}
- void revertAndEndBackup() {
+ private void revertAndEndBackup() {
if (MORE_DEBUG) {
Slog.i(TAG, "Reverting backup queue - restaging everything");
}
@@ -1084,14 +1162,14 @@ public class PerformBackupTask implements BackupRestoreTask {
}
- void errorCleanup() {
+ private void errorCleanup() {
mBackupDataName.delete();
mNewStateName.delete();
clearAgentState();
}
// Cleanup common to both success and failure cases
- void clearAgentState() {
+ private void clearAgentState() {
try {
if (mSavedState != null) mSavedState.close();
} catch (IOException e) {
@@ -1122,7 +1200,7 @@ public class PerformBackupTask implements BackupRestoreTask {
}
}
- void executeNextState(BackupState nextState) {
+ private void executeNextState(BackupState nextState) {
if (MORE_DEBUG) {
Slog.i(TAG, " => executing next step on "
+ this + " nextState=" + nextState);
diff --git a/com/android/server/backup/internal/PerformClearTask.java b/com/android/server/backup/internal/PerformClearTask.java
index 84ca59b5..140d7286 100644
--- a/com/android/server/backup/internal/PerformClearTask.java
+++ b/com/android/server/backup/internal/PerformClearTask.java
@@ -23,12 +23,14 @@ 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.transport.TransportClient;
import java.io.File;
public class PerformClearTask implements Runnable {
private final RefactoredBackupManagerService mBackupManagerService;
+ private final TransportManager mTransportManager;
private final TransportClient mTransportClient;
private final PackageInfo mPackage;
private final OnTaskFinishedListener mListener;
@@ -37,6 +39,7 @@ public class PerformClearTask implements Runnable {
TransportClient transportClient, PackageInfo packageInfo,
OnTaskFinishedListener listener) {
mBackupManagerService = backupManagerService;
+ mTransportManager = backupManagerService.getTransportManager();
mTransportClient = transportClient;
mPackage = packageInfo;
mListener = listener;
@@ -47,8 +50,9 @@ public class PerformClearTask implements Runnable {
IBackupTransport transport = null;
try {
// Clear the on-device backup state to ensure a full backup next time
- File stateDir = new File(mBackupManagerService.getBaseStateDir(),
- mTransportClient.getTransportDirName());
+ String transportDirName =
+ mTransportManager.getTransportDirName(mTransportClient.getTransportComponent());
+ File stateDir = new File(mBackupManagerService.getBaseStateDir(), transportDirName);
File stateFile = new File(stateDir, mPackage.packageName);
stateFile.delete();
diff --git a/com/android/server/backup/internal/PerformInitializeTask.java b/com/android/server/backup/internal/PerformInitializeTask.java
index c6246981..2f2af98e 100644
--- a/com/android/server/backup/internal/PerformInitializeTask.java
+++ b/com/android/server/backup/internal/PerformInitializeTask.java
@@ -122,7 +122,9 @@ public class PerformInitializeTask implements Runnable {
transportClientsToDisposeOf.add(transportClient);
Slog.i(TAG, "Initializing (wiping) backup transport storage: " + transportName);
- String transportDirName = transportClient.getTransportDirName();
+ String transportDirName =
+ mTransportManager.getTransportDirName(
+ transportClient.getTransportComponent());
EventLog.writeEvent(EventLogTags.BACKUP_START, transportDirName);
long startRealtime = SystemClock.elapsedRealtime();
diff --git a/com/android/server/backup/internal/PerformInitializeTaskTest.java b/com/android/server/backup/internal/PerformInitializeTaskTest.java
index 73f1c2fb..ace0441c 100644
--- a/com/android/server/backup/internal/PerformInitializeTaskTest.java
+++ b/com/android/server/backup/internal/PerformInitializeTaskTest.java
@@ -19,18 +19,21 @@ 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.android.server.backup.testing.TransportData.backupTransport;
+import static com.android.server.backup.testing.TransportData.d2dTransport;
+import static com.android.server.backup.testing.TransportData.localTransport;
+import static com.android.server.backup.testing.TransportTestUtils.setUpTransports;
+
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;
@@ -41,8 +44,10 @@ 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.testing.TransportTestUtils;
+import com.android.server.backup.testing.TransportData;
+import com.android.server.backup.testing.TransportTestUtils.TransportMock;
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;
@@ -56,34 +61,33 @@ import org.robolectric.annotation.Config;
import java.io.File;
import java.util.Arrays;
+import java.util.Iterator;
import java.util.List;
+import java.util.stream.Stream;
@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 IBackupTransport mTransportBinder;
@Mock private IBackupObserver mObserver;
@Mock private AlarmManager mAlarmManager;
@Mock private PendingIntent mRunInitIntent;
private File mBaseStateDir;
+ private TransportData mTransport;
+ private String mTransportName;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
+ mTransport = backupTransport();
+ mTransportName = mTransport.transportName;
+
Application context = RuntimeEnvironment.application;
mBaseStateDir = new File(context.getCacheDir(), "base_state_dir");
assertThat(mBaseStateDir.mkdir()).isTrue();
@@ -94,76 +98,76 @@ public class PerformInitializeTaskTest {
@Test
public void testRun_callsTransportCorrectly() throws Exception {
- setUpTransport(TRANSPORT_NAME);
- configureTransport(mTransport, TRANSPORT_OK, TRANSPORT_OK);
- PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+ setUpTransport(mTransport);
+ configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_OK);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
performInitializeTask.run();
- verify(mTransport).initializeDevice();
- verify(mTransport).finishBackup();
+ verify(mTransportBinder).initializeDevice();
+ verify(mTransportBinder).finishBackup();
}
@Test
public void testRun_callsBackupManagerCorrectly() throws Exception {
- setUpTransport(TRANSPORT_NAME);
- configureTransport(mTransport, TRANSPORT_OK, TRANSPORT_OK);
- PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+ setUpTransport(mTransport);
+ configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_OK);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
performInitializeTask.run();
verify(mBackupManagerService)
- .recordInitPending(false, TRANSPORT_NAME, dirName(TRANSPORT_NAME));
+ .recordInitPending(false, mTransportName, mTransport.transportDirName);
verify(mBackupManagerService)
- .resetBackupState(eq(new File(mBaseStateDir, dirName(TRANSPORT_NAME))));
+ .resetBackupState(eq(new File(mBaseStateDir, mTransport.transportDirName)));
}
@Test
public void testRun_callsObserverAndListenerCorrectly() throws Exception {
- setUpTransport(TRANSPORT_NAME);
- configureTransport(mTransport, TRANSPORT_OK, TRANSPORT_OK);
- PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+ setUpTransport(mTransport);
+ configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_OK);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
performInitializeTask.run();
- verify(mObserver).onResult(eq(TRANSPORT_NAME), eq(TRANSPORT_OK));
+ verify(mObserver).onResult(eq(mTransportName), 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);
+ setUpTransport(mTransport);
+ configureTransport(mTransportBinder, TRANSPORT_ERROR, 0);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
performInitializeTask.run();
- verify(mTransport).initializeDevice();
- verify(mTransport, never()).finishBackup();
+ verify(mTransportBinder).initializeDevice();
+ verify(mTransportBinder, never()).finishBackup();
verify(mBackupManagerService)
- .recordInitPending(true, TRANSPORT_NAME, dirName(TRANSPORT_NAME));
+ .recordInitPending(true, mTransportName, mTransport.transportDirName);
}
@Test
public void testRun_whenInitializeDeviceFails_callsObserverAndListenerCorrectly()
throws Exception {
- setUpTransport(TRANSPORT_NAME);
- configureTransport(mTransport, TRANSPORT_ERROR, 0);
- PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+ setUpTransport(mTransport);
+ configureTransport(mTransportBinder, TRANSPORT_ERROR, 0);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
performInitializeTask.run();
- verify(mObserver).onResult(eq(TRANSPORT_NAME), eq(TRANSPORT_ERROR));
+ verify(mObserver).onResult(eq(mTransportName), 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);
+ setUpTransport(mTransport);
+ configureTransport(mTransportBinder, TRANSPORT_ERROR, 0);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
performInitializeTask.run();
@@ -172,36 +176,36 @@ public class PerformInitializeTaskTest {
@Test
public void testRun_whenFinishBackupFails() throws Exception {
- setUpTransport(TRANSPORT_NAME);
- configureTransport(mTransport, TRANSPORT_OK, TRANSPORT_ERROR);
- PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+ setUpTransport(mTransport);
+ configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_ERROR);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
performInitializeTask.run();
- verify(mTransport).initializeDevice();
- verify(mTransport).finishBackup();
+ verify(mTransportBinder).initializeDevice();
+ verify(mTransportBinder).finishBackup();
verify(mBackupManagerService)
- .recordInitPending(true, TRANSPORT_NAME, dirName(TRANSPORT_NAME));
+ .recordInitPending(true, mTransportName, mTransport.transportDirName);
}
@Test
public void testRun_whenFinishBackupFails_callsObserverAndListenerCorrectly() throws Exception {
- setUpTransport(TRANSPORT_NAME);
- configureTransport(mTransport, TRANSPORT_OK, TRANSPORT_ERROR);
- PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+ setUpTransport(mTransport);
+ configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_ERROR);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
performInitializeTask.run();
- verify(mObserver).onResult(eq(TRANSPORT_NAME), eq(TRANSPORT_ERROR));
+ verify(mObserver).onResult(eq(mTransportName), 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);
+ setUpTransport(mTransport);
+ configureTransport(mTransportBinder, TRANSPORT_OK, TRANSPORT_ERROR);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
performInitializeTask.run();
@@ -210,57 +214,76 @@ public class PerformInitializeTaskTest {
@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);
+ TransportData transport1 = backupTransport();
+ TransportData transport2 = d2dTransport();
+ List<TransportMock> transportMocks =
+ setUpTransports(mTransportManager, transport1, transport2);
+ configureTransport(transportMocks.get(0).transport, TRANSPORT_ERROR, 0);
+ configureTransport(transportMocks.get(1).transport, TRANSPORT_OK, TRANSPORT_OK);
PerformInitializeTask performInitializeTask =
- createPerformInitializeTask(TRANSPORT_NAMES[0], TRANSPORT_NAMES[1]);
+ createPerformInitializeTask(transport1.transportName, transport2.transportName);
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(transportMocks.get(1).transport).initializeDevice();
+ verify(mObserver).onResult(eq(transport1.transportName), eq(TRANSPORT_ERROR));
+ verify(mObserver).onResult(eq(transport2.transportName), 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);
+ List<TransportMock> transportMocks =
+ setUpTransports(
+ mTransportManager, backupTransport(), d2dTransport(), localTransport());
+ configureTransport(transportMocks.get(0).transport, TRANSPORT_OK, TRANSPORT_OK);
+ configureTransport(transportMocks.get(1).transport, TRANSPORT_OK, TRANSPORT_OK);
+ configureTransport(transportMocks.get(2).transport, TRANSPORT_OK, TRANSPORT_OK);
+ String[] transportNames =
+ Stream.of(new TransportData[] {backupTransport(), d2dTransport(), localTransport()})
+ .map(t -> t.transportName)
+ .toArray(String[]::new);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(transportNames);
performInitializeTask.run();
- for (TransportData transport : transports) {
+ Iterator<TransportData> transportsIterator =
+ Arrays.asList(
+ new TransportData[] {
+ backupTransport(), d2dTransport(), localTransport()
+ })
+ .iterator();
+ for (TransportMock transportMock : transportMocks) {
+ TransportData transport = transportsIterator.next();
verify(mTransportManager).getTransportClient(eq(transport.transportName), any());
verify(mTransportManager)
- .disposeOfTransportClient(eq(transport.transportClientMock), any());
+ .disposeOfTransportClient(eq(transportMock.transportClient), 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);
+ TransportData transport1 = backupTransport();
+ TransportData transport2 = d2dTransport();
+ List<TransportMock> transportMocks =
+ setUpTransports(mTransportManager, transport1, transport2);
+ configureTransport(transportMocks.get(0).transport, TRANSPORT_ERROR, 0);
+ configureTransport(transportMocks.get(1).transport, TRANSPORT_OK, TRANSPORT_OK);
PerformInitializeTask performInitializeTask =
- createPerformInitializeTask(TRANSPORT_NAMES[0], TRANSPORT_NAMES[1]);
+ createPerformInitializeTask(transport1.transportName, transport2.transportName);
performInitializeTask.run();
verify(mTransportManager)
- .disposeOfTransportClient(eq(transports.get(0).transportClientMock), any());
+ .disposeOfTransportClient(eq(transportMocks.get(0).transportClient), any());
verify(mTransportManager)
- .disposeOfTransportClient(eq(transports.get(1).transportClientMock), any());
+ .disposeOfTransportClient(eq(transportMocks.get(1).transportClient), any());
}
@Test
public void testRun_whenTransportNotRegistered() throws Exception {
- setUpTransport(new TransportData(TRANSPORT_NAME, null, null));
- PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+ setUpTransports(mTransportManager, mTransport.unregistered());
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
performInitializeTask.run();
@@ -271,15 +294,15 @@ public class PerformInitializeTaskTest {
@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;
+ TransportData transport1 = backupTransport().unregistered();
+ TransportData transport2 = d2dTransport();
+ List<TransportMock> transportMocks =
+ setUpTransports(mTransportManager, transport1, transport2);
+ String registeredTransportName = transport2.transportName;
+ IBackupTransport registeredTransport = transportMocks.get(1).transport;
+ TransportClient registeredTransportClient = transportMocks.get(1).transportClient;
PerformInitializeTask performInitializeTask =
- createPerformInitializeTask(TRANSPORT_NAMES[0], TRANSPORT_NAMES[1]);
+ createPerformInitializeTask(transport1.transportName, transport2.transportName);
performInitializeTask.run();
@@ -290,23 +313,24 @@ public class PerformInitializeTaskTest {
@Test
public void testRun_whenTransportNotAvailable() throws Exception {
- TransportClient transportClient = mock(TransportClient.class);
- setUpTransport(new TransportData(TRANSPORT_NAME, null, transportClient));
- PerformInitializeTask performInitializeTask = createPerformInitializeTask(TRANSPORT_NAME);
+ TransportMock transportMock = setUpTransport(mTransport.unavailable());
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
performInitializeTask.run();
- verify(mTransportManager).disposeOfTransportClient(eq(transportClient), any());
+ verify(mTransportManager)
+ .disposeOfTransportClient(eq(transportMock.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);
+ TransportMock transportMock = setUpTransport(mTransport);
+ IBackupTransport transport = transportMock.transport;
+ TransportClient transportClient = transportMock.transportClient;
+ when(transport.initializeDevice()).thenThrow(DeadObjectException.class);
+ PerformInitializeTask performInitializeTask = createPerformInitializeTask(mTransportName);
performInitializeTask.run();
@@ -332,80 +356,10 @@ public class PerformInitializeTaskTest {
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));
- }
+ private TransportMock setUpTransport(TransportData transport) throws Exception {
+ TransportMock transportMock =
+ TransportTestUtils.setUpTransport(mTransportManager, transport);
+ mTransportBinder = transportMock.transport;
+ return transportMock;
}
}
diff --git a/com/android/server/backup/restore/FullRestoreEngine.java b/com/android/server/backup/restore/FullRestoreEngine.java
index 7efe5cad..2c8b5b4c 100644
--- a/com/android/server/backup/restore/FullRestoreEngine.java
+++ b/com/android/server/backup/restore/FullRestoreEngine.java
@@ -66,7 +66,6 @@ public class FullRestoreEngine extends RestoreEngine {
// Task in charge of monitoring timeouts
private final BackupRestoreTask mMonitorTask;
- private final RestoreInstallObserver mInstallObserver = new RestoreInstallObserver();
private final RestoreDeleteObserver mDeleteObserver = new RestoreDeleteObserver();
// Dedicated observer, if any
@@ -249,13 +248,12 @@ public class FullRestoreEngine extends RestoreEngine {
Slog.d(TAG, "APK file; installing");
}
// Try to install the app.
- String installerName = mPackageInstallers.get(pkg);
+ String installerPackageName = mPackageInstallers.get(pkg);
boolean isSuccessfullyInstalled = RestoreUtils.installApk(
- instream, mBackupManagerService.getPackageManager(),
- mInstallObserver, mDeleteObserver, mManifestSignatures,
- mPackagePolicies, info, installerName,
- bytesReadListener, mBackupManagerService.getDataDir()
- );
+ instream, mBackupManagerService.getContext(),
+ mDeleteObserver, mManifestSignatures,
+ mPackagePolicies, info, installerPackageName,
+ bytesReadListener);
// good to go; promote to ACCEPT
mPackagePolicies.put(pkg, isSuccessfullyInstalled
? RestorePolicy.ACCEPT
diff --git a/com/android/server/backup/restore/PerformAdbRestoreTask.java b/com/android/server/backup/restore/PerformAdbRestoreTask.java
index 3dc242f1..54c27460 100644
--- a/com/android/server/backup/restore/PerformAdbRestoreTask.java
+++ b/com/android/server/backup/restore/PerformAdbRestoreTask.java
@@ -88,7 +88,6 @@ public class PerformAdbRestoreTask implements Runnable {
private final String mDecryptPassword;
private final AtomicBoolean mLatchObject;
private final PackageManagerBackupAgent mPackageManagerBackupAgent;
- private final RestoreInstallObserver mInstallObserver = new RestoreInstallObserver();
private final RestoreDeleteObserver mDeleteObserver = new RestoreDeleteObserver();
private IFullBackupRestoreObserver mObserver;
@@ -513,13 +512,11 @@ public class PerformAdbRestoreTask implements Runnable {
Slog.d(TAG, "APK file; installing");
}
// Try to install the app.
- String installerName = mPackageInstallers.get(pkg);
- boolean isSuccessfullyInstalled = RestoreUtils.installApk(
- instream, mBackupManagerService.getPackageManager(),
- mInstallObserver, mDeleteObserver, mManifestSignatures,
- mPackagePolicies, info, installerName,
- bytesReadListener, mBackupManagerService.getDataDir()
- );
+ String installerPackageName = mPackageInstallers.get(pkg);
+ boolean isSuccessfullyInstalled = RestoreUtils.installApk(instream,
+ mBackupManagerService.getContext(),
+ mDeleteObserver, mManifestSignatures, mPackagePolicies,
+ info, installerPackageName, bytesReadListener);
// good to go; promote to ACCEPT
mPackagePolicies.put(pkg, isSuccessfullyInstalled
? RestorePolicy.ACCEPT
diff --git a/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
index 86866dca..88f9eade 100644
--- a/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
+++ b/com/android/server/backup/restore/PerformUnifiedRestoreTask.java
@@ -61,6 +61,7 @@ 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.TransportManager;
import com.android.server.backup.internal.OnTaskFinishedListener;
import com.android.server.backup.transport.TransportClient;
import com.android.server.backup.utils.AppBackupUtils;
@@ -78,6 +79,7 @@ import java.util.List;
public class PerformUnifiedRestoreTask implements BackupRestoreTask {
private RefactoredBackupManagerService backupManagerService;
+ private final TransportManager mTransportManager;
// Transport client we're working with to do the restore
private final TransportClient mTransportClient;
@@ -164,6 +166,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
int pmToken, boolean isFullSystemRestore, String[] filterSet,
OnTaskFinishedListener listener) {
this.backupManagerService = backupManagerService;
+ mTransportManager = backupManagerService.getTransportManager();
mEphemeralOpToken = backupManagerService.generateRandomIntegerToken();
mState = UnifiedRestoreState.INITIAL;
mStartRealtime = SystemClock.elapsedRealtime();
@@ -349,8 +352,9 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask {
}
try {
- String transportDir = mTransportClient.getTransportDirName();
- mStateDir = new File(backupManagerService.getBaseStateDir(), transportDir);
+ String transportDirName =
+ mTransportManager.getTransportDirName(mTransportClient.getTransportComponent());
+ mStateDir = new File(backupManagerService.getBaseStateDir(), transportDirName);
// Fetch the current metadata from the dataset first
PackageInfo pmPackage = new PackageInfo();
diff --git a/com/android/server/backup/restore/RestoreInstallObserver.java b/com/android/server/backup/restore/RestoreInstallObserver.java
deleted file mode 100644
index 3d506f13..00000000
--- a/com/android/server/backup/restore/RestoreInstallObserver.java
+++ /dev/null
@@ -1,89 +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.restore;
-
-import android.app.PackageInstallObserver;
-import android.os.Bundle;
-
-import com.android.internal.annotations.GuardedBy;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-
-/**
- * Synchronous implementation of PackageInstallObserver.
- *
- * Allows the caller to synchronously wait for package install event.
- */
-public class RestoreInstallObserver extends PackageInstallObserver {
-
- @GuardedBy("mDone")
- private final AtomicBoolean mDone = new AtomicBoolean();
-
- private String mPackageName;
- private int mResult;
-
- public RestoreInstallObserver() {
- }
-
- /**
- * Resets the observer to prepare for another installation.
- */
- public void reset() {
- synchronized (mDone) {
- mDone.set(false);
- }
- }
-
- /**
- * Synchronously waits for completion.
- */
- public void waitForCompletion() {
- synchronized (mDone) {
- while (mDone.get() == false) {
- try {
- mDone.wait();
- } catch (InterruptedException e) {
- }
- }
- }
- }
-
- /**
- * Returns result code.
- */
- public int getResult() {
- return mResult;
- }
-
- /**
- * Returns installed package name.
- */
- public String getPackageName() {
- return mPackageName;
- }
-
- @Override
- public void onPackageInstalled(String packageName, int returnCode,
- String msg, Bundle extras) {
- synchronized (mDone) {
- mResult = returnCode;
- mPackageName = packageName;
- mDone.set(true);
- mDone.notifyAll();
- }
- }
-}
diff --git a/com/android/server/backup/testing/ShadowAppBackupUtils.java b/com/android/server/backup/testing/ShadowAppBackupUtils.java
new file mode 100644
index 00000000..73cb4c0b
--- /dev/null
+++ b/com/android/server/backup/testing/ShadowAppBackupUtils.java
@@ -0,0 +1,46 @@
+/*
+ * 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 com.android.server.backup.testing;
+
+import android.annotation.Nullable;
+import android.content.pm.PackageManager;
+
+import com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.utils.AppBackupUtils;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.util.function.Function;
+
+@Implements(AppBackupUtils.class)
+public class ShadowAppBackupUtils {
+ public static Function<String, Boolean> sAppIsRunningAndEligibleForBackupWithTransport;
+ static {
+ reset();
+ }
+
+ @Implementation
+ public static boolean appIsRunningAndEligibleForBackupWithTransport(
+ @Nullable TransportClient transportClient, String packageName, PackageManager pm) {
+ return sAppIsRunningAndEligibleForBackupWithTransport.apply(packageName);
+ }
+
+ public static void reset() {
+ sAppIsRunningAndEligibleForBackupWithTransport = p -> true;
+ }
+}
diff --git a/com/android/server/backup/testing/ShadowBackupPolicyEnforcer.java b/com/android/server/backup/testing/ShadowBackupPolicyEnforcer.java
new file mode 100644
index 00000000..88b30da4
--- /dev/null
+++ b/com/android/server/backup/testing/ShadowBackupPolicyEnforcer.java
@@ -0,0 +1,24 @@
+package com.android.server.backup.testing;
+
+import android.content.ComponentName;
+
+import com.android.server.backup.BackupPolicyEnforcer;
+import com.android.server.backup.RefactoredBackupManagerService;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+@Implements(BackupPolicyEnforcer.class)
+public class ShadowBackupPolicyEnforcer {
+
+ private static ComponentName sMandatoryBackupTransport;
+
+ public static void setMandatoryBackupTransport(ComponentName backupTransportComponent) {
+ sMandatoryBackupTransport = backupTransportComponent;
+ }
+
+ @Implementation
+ public ComponentName getMandatoryBackupTransport() {
+ return sMandatoryBackupTransport;
+ }
+}
diff --git a/com/android/server/backup/testing/TestUtils.java b/com/android/server/backup/testing/TestUtils.java
new file mode 100644
index 00000000..1be298d2
--- /dev/null
+++ b/com/android/server/backup/testing/TestUtils.java
@@ -0,0 +1,68 @@
+/*
+ * 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 com.android.server.backup.testing;
+
+import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
+
+import java.util.concurrent.Callable;
+
+public class TestUtils {
+ /**
+ * Calls {@link Runnable#run()} and returns if no exception is thrown. Otherwise, if the
+ * exception is unchecked, rethrow it; if it's checked wrap in a {@link RuntimeException} and
+ * throw.
+ *
+ * <p><b>Warning:</b>DON'T use this outside tests. A wrapped checked exception is just a failure
+ * in a test.
+ */
+ public static void uncheck(ThrowingRunnable runnable) {
+ try {
+ runnable.runOrThrow();
+ } catch (Exception e) {
+ throw wrapIfChecked(e);
+ }
+ }
+
+ /**
+ * Calls {@link Callable#call()} and returns the value if no exception is thrown. Otherwise, if
+ * the exception is unchecked, rethrow it; if it's checked wrap in a {@link RuntimeException}
+ * and throw.
+ *
+ * <p><b>Warning:</b>DON'T use this outside tests. A wrapped checked exception is just a failure
+ * in a test.
+ */
+ public static <T> T uncheck(Callable<T> callable) {
+ try {
+ return callable.call();
+ } catch (Exception e) {
+ throw wrapIfChecked(e);
+ }
+ }
+
+ /**
+ * Wrap {@code e} in a {@link RuntimeException} only if it's not one already, in which case it's
+ * returned.
+ */
+ public static RuntimeException wrapIfChecked(Exception e) {
+ if (e instanceof RuntimeException) {
+ return (RuntimeException) e;
+ }
+ return new RuntimeException(e);
+ }
+
+ private TestUtils() {}
+}
diff --git a/com/android/server/backup/testing/TransportBoundListenerStub.java b/com/android/server/backup/testing/TransportBoundListenerStub.java
deleted file mode 100644
index 84ac2c21..00000000
--- a/com/android/server/backup/testing/TransportBoundListenerStub.java
+++ /dev/null
@@ -1,64 +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 com.android.internal.backup.IBackupTransport;
-import com.android.server.backup.TransportManager;
-
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Stub implementation of TransportBoundListener, which returns given result and can tell whether
- * it was called for given transport.
- */
-public class TransportBoundListenerStub implements
- TransportManager.TransportBoundListener {
- private boolean mAlwaysReturnSuccess;
- private Set<IBackupTransport> mTransportsCalledFor = new HashSet<>();
-
- public TransportBoundListenerStub(boolean alwaysReturnSuccess) {
- this.mAlwaysReturnSuccess = alwaysReturnSuccess;
- }
-
- @Override
- public boolean onTransportBound(IBackupTransport binder) {
- mTransportsCalledFor.add(binder);
- return mAlwaysReturnSuccess;
- }
-
- /**
- * Returns whether the listener was called for the specified transport at least once.
- */
- public boolean isCalledForTransport(IBackupTransport binder) {
- return mTransportsCalledFor.contains(binder);
- }
-
- /**
- * Returns whether the listener was called at least once.
- */
- public boolean isCalled() {
- return !mTransportsCalledFor.isEmpty();
- }
-
- /**
- * Resets listener calls.
- */
- public void resetState() {
- mTransportsCalledFor.clear();
- }
-}
diff --git a/com/android/server/backup/testing/TransportData.java b/com/android/server/backup/testing/TransportData.java
new file mode 100644
index 00000000..9feaa8ef
--- /dev/null
+++ b/com/android/server/backup/testing/TransportData.java
@@ -0,0 +1,149 @@
+/*
+ * 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 com.android.server.backup.testing;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Intent;
+
+public class TransportData {
+ // No constants since new Intent() can't be called in static context because of Robolectric
+ public static TransportData backupTransport() {
+ return new TransportData(
+ "com.google.android.gms/.backup.BackupTransportService",
+ "com.google.android.gms/.backup.BackupTransportService",
+ "com.google.android.gms.backup.BackupTransportService",
+ new Intent(),
+ "user@gmail.com",
+ new Intent(),
+ "Google Account");
+ }
+
+ public static TransportData d2dTransport() {
+ return new TransportData(
+ "com.google.android.gms/.backup.migrate.service.D2dTransport",
+ "com.google.android.gms/.backup.component.D2dTransportService",
+ "d2dMigrateTransport",
+ null,
+ "Moving data to new device",
+ null,
+ "");
+ }
+
+ public static TransportData localTransport() {
+ return new TransportData(
+ "android/com.android.internal.backup.LocalTransport",
+ "android/com.android.internal.backup.LocalTransportService",
+ "com.android.internal.backup.LocalTransport",
+ null,
+ "Backing up to debug-only private cache",
+ null,
+ "");
+ }
+
+ public static TransportData genericTransport(String packageName, String className) {
+ return new TransportData(
+ packageName + "/." + className,
+ packageName + "/." + className + "Service",
+ packageName + "." + className,
+ new Intent(),
+ "currentDestinationString",
+ new Intent(),
+ "dataManagementLabel");
+ }
+
+ @TransportTestUtils.TransportStatus
+ public int transportStatus;
+ public final String transportName;
+ private final String transportComponentShort;
+ @Nullable
+ public String transportDirName;
+ @Nullable public Intent configurationIntent;
+ @Nullable public String currentDestinationString;
+ @Nullable public Intent dataManagementIntent;
+ @Nullable public String dataManagementLabel;
+
+ private TransportData(
+ @TransportTestUtils.TransportStatus int transportStatus,
+ String transportName,
+ String transportComponentShort,
+ String transportDirName,
+ Intent configurationIntent,
+ String currentDestinationString,
+ Intent dataManagementIntent,
+ String dataManagementLabel) {
+ this.transportStatus = transportStatus;
+ this.transportName = transportName;
+ this.transportComponentShort = transportComponentShort;
+ this.transportDirName = transportDirName;
+ this.configurationIntent = configurationIntent;
+ this.currentDestinationString = currentDestinationString;
+ this.dataManagementIntent = dataManagementIntent;
+ this.dataManagementLabel = dataManagementLabel;
+ }
+
+ public TransportData(
+ String transportName,
+ String transportComponentShort,
+ String transportDirName,
+ Intent configurationIntent,
+ String currentDestinationString,
+ Intent dataManagementIntent,
+ String dataManagementLabel) {
+ this(
+ TransportTestUtils.TransportStatus.REGISTERED_AVAILABLE,
+ transportName,
+ transportComponentShort,
+ transportDirName,
+ configurationIntent,
+ currentDestinationString,
+ dataManagementIntent,
+ dataManagementLabel);
+ }
+
+ /**
+ * Not field because otherwise we'd have to call ComponentName::new in static context and
+ * Robolectric does not like this.
+ */
+ public ComponentName getTransportComponent() {
+ return ComponentName.unflattenFromString(transportComponentShort);
+ }
+
+ public TransportData unavailable() {
+ return new TransportData(
+ TransportTestUtils.TransportStatus.REGISTERED_UNAVAILABLE,
+ transportName,
+ transportComponentShort,
+ transportDirName,
+ configurationIntent,
+ currentDestinationString,
+ dataManagementIntent,
+ dataManagementLabel);
+ }
+
+ public TransportData unregistered() {
+ return new TransportData(
+ TransportTestUtils.TransportStatus.UNREGISTERED,
+ transportName,
+ transportComponentShort,
+ transportDirName,
+ configurationIntent,
+ currentDestinationString,
+ dataManagementIntent,
+ dataManagementLabel);
+ }
+}
diff --git a/com/android/server/backup/testing/TransportReadyCallbackStub.java b/com/android/server/backup/testing/TransportReadyCallbackStub.java
deleted file mode 100644
index bbe7eba1..00000000
--- a/com/android/server/backup/testing/TransportReadyCallbackStub.java
+++ /dev/null
@@ -1,55 +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 com.android.server.backup.TransportManager;
-
-import java.util.HashSet;
-import java.util.Set;
-
-/**
- * Stub implementation of TransportReadyCallback, which can tell which calls were made.
- */
-public class TransportReadyCallbackStub implements
- TransportManager.TransportReadyCallback {
- private final Set<String> mSuccessCalls = new HashSet<>();
- private final Set<Integer> mFailureCalls = new HashSet<>();
-
- @Override
- public void onSuccess(String transportName) {
- mSuccessCalls.add(transportName);
- }
-
- @Override
- public void onFailure(int reason) {
- mFailureCalls.add(reason);
- }
-
- /**
- * Returns set of transport names for which {@link #onSuccess(String)} was called.
- */
- public Set<String> getSuccessCalls() {
- return mSuccessCalls;
- }
-
- /**
- * Returns set of reasons for which {@link #onFailure(int)} } was called.
- */
- public Set<Integer> getFailureCalls() {
- return mFailureCalls;
- }
-}
diff --git a/com/android/server/backup/testing/TransportTestUtils.java b/com/android/server/backup/testing/TransportTestUtils.java
new file mode 100644
index 00000000..e1dc7b5e
--- /dev/null
+++ b/com/android/server/backup/testing/TransportTestUtils.java
@@ -0,0 +1,201 @@
+/*
+ * 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 com.android.server.backup.testing;
+
+import static com.android.server.backup.testing.TestUtils.uncheck;
+
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import static java.util.stream.Collectors.toList;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
+import android.os.RemoteException;
+import android.support.annotation.IntDef;
+
+import com.android.internal.backup.IBackupTransport;
+import com.android.server.backup.TransportManager;
+import com.android.server.backup.transport.TransportClient;
+import com.android.server.backup.transport.TransportNotAvailableException;
+import com.android.server.backup.transport.TransportNotRegisteredException;
+
+import org.robolectric.shadows.ShadowPackageManager;
+
+import java.util.List;
+import java.util.stream.Stream;
+
+public class TransportTestUtils {
+ /**
+ * Differently from {@link #setUpTransports(TransportManager, TransportData...)}, which
+ * configures {@link TransportManager}, this is meant to mock the environment for a real
+ * TransportManager.
+ */
+ public static void setUpTransportsForTransportManager(
+ ShadowPackageManager shadowPackageManager, TransportData... transports)
+ throws Exception {
+ for (TransportData transport : transports) {
+ ComponentName transportComponent = transport.getTransportComponent();
+ String packageName = transportComponent.getPackageName();
+ ResolveInfo resolveInfo = resolveInfo(transportComponent);
+ shadowPackageManager.addResolveInfoForIntent(transportIntent(), resolveInfo);
+ shadowPackageManager.addResolveInfoForIntent(
+ transportIntent().setPackage(packageName), resolveInfo);
+ }
+ }
+
+ private static Intent transportIntent() {
+ return new Intent(TransportManager.SERVICE_ACTION_TRANSPORT_HOST);
+ }
+
+ private static ResolveInfo resolveInfo(ComponentName transportComponent) {
+ ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.serviceInfo = new ServiceInfo();
+ resolveInfo.serviceInfo.packageName = transportComponent.getPackageName();
+ resolveInfo.serviceInfo.name = transportComponent.getClassName();
+ return resolveInfo;
+ }
+
+ /** {@code transportName} has to be in the {@link ComponentName} format (with '/') */
+ public static TransportMock setUpCurrentTransport(
+ TransportManager transportManager, TransportData transport) throws Exception {
+ TransportMock transportMock = setUpTransports(transportManager, transport).get(0);
+ if (transportMock.transportClient != null) {
+ when(transportManager.getCurrentTransportClient(any()))
+ .thenReturn(transportMock.transportClient);
+ }
+ return transportMock;
+ }
+
+ /** @see #setUpTransport(TransportManager, TransportData) */
+ public static List<TransportMock> setUpTransports(
+ TransportManager transportManager, TransportData... transports) throws Exception {
+ return Stream.of(transports)
+ .map(transport -> uncheck(() -> setUpTransport(transportManager, transport)))
+ .collect(toList());
+ }
+
+ public static TransportMock setUpTransport(
+ TransportManager transportManager, TransportData transport) throws Exception {
+ int status = transport.transportStatus;
+ String transportName = transport.transportName;
+ ComponentName transportComponent = transport.getTransportComponent();
+ String transportDirName = transport.transportDirName;
+
+ TransportMock transportMock = mockTransport(transport);
+ if (status == TransportStatus.REGISTERED_AVAILABLE
+ || status == TransportStatus.REGISTERED_UNAVAILABLE) {
+ // Transport registered
+ when(transportManager.getTransportClient(eq(transportName), any()))
+ .thenReturn(transportMock.transportClient);
+ when(transportManager.getTransportClientOrThrow(eq(transportName), any()))
+ .thenReturn(transportMock.transportClient);
+ when(transportManager.getTransportName(transportComponent)).thenReturn(transportName);
+ when(transportManager.getTransportDirName(eq(transportName)))
+ .thenReturn(transportDirName);
+ when(transportManager.getTransportDirName(eq(transportComponent)))
+ .thenReturn(transportDirName);
+ // TODO: Mock rest of description methods
+ } else {
+ // Transport not registered
+ when(transportManager.getTransportClient(eq(transportName), any())).thenReturn(null);
+ when(transportManager.getTransportClientOrThrow(eq(transportName), any()))
+ .thenThrow(TransportNotRegisteredException.class);
+ when(transportManager.getTransportName(transportComponent))
+ .thenThrow(TransportNotRegisteredException.class);
+ when(transportManager.getTransportDirName(eq(transportName)))
+ .thenThrow(TransportNotRegisteredException.class);
+ when(transportManager.getTransportDirName(eq(transportComponent)))
+ .thenThrow(TransportNotRegisteredException.class);
+ }
+ return transportMock;
+ }
+
+ public static TransportMock mockTransport(TransportData transport) throws Exception {
+ final TransportClient transportClientMock;
+ int status = transport.transportStatus;
+ ComponentName transportComponent = transport.getTransportComponent();
+ if (status == TransportStatus.REGISTERED_AVAILABLE
+ || status == TransportStatus.REGISTERED_UNAVAILABLE) {
+ // Transport registered
+ transportClientMock = mock(TransportClient.class);
+ when(transportClientMock.getTransportComponent()).thenReturn(transportComponent);
+ if (status == TransportStatus.REGISTERED_AVAILABLE) {
+ // Transport registered and available
+ IBackupTransport transportMock = mockTransportBinder(transport);
+ when(transportClientMock.connectOrThrow(any())).thenReturn(transportMock);
+
+ return new TransportMock(transportClientMock, transportMock);
+ } else {
+ // Transport registered but unavailable
+ when(transportClientMock.connectOrThrow(any()))
+ .thenThrow(TransportNotAvailableException.class);
+
+ return new TransportMock(transportClientMock, null);
+ }
+ } else {
+ // Transport not registered
+ return new TransportMock(null, null);
+ }
+ }
+
+ private static IBackupTransport mockTransportBinder(TransportData transport) throws Exception {
+ IBackupTransport transportBinder = mock(IBackupTransport.class);
+ try {
+ when(transportBinder.name()).thenReturn(transport.transportName);
+ when(transportBinder.transportDirName()).thenReturn(transport.transportDirName);
+ when(transportBinder.configurationIntent()).thenReturn(transport.configurationIntent);
+ when(transportBinder.currentDestinationString())
+ .thenReturn(transport.currentDestinationString);
+ when(transportBinder.dataManagementIntent()).thenReturn(transport.dataManagementIntent);
+ when(transportBinder.dataManagementLabel()).thenReturn(transport.dataManagementLabel);
+ } catch (RemoteException e) {
+ fail("RemoteException?");
+ }
+ return transportBinder;
+ }
+
+ public static class TransportMock {
+ @Nullable public final TransportClient transportClient;
+ @Nullable public final IBackupTransport transport;
+
+ private TransportMock(
+ @Nullable TransportClient transportClient, @Nullable IBackupTransport transport) {
+ this.transportClient = transportClient;
+ this.transport = transport;
+ }
+ }
+
+ @IntDef({
+ TransportStatus.REGISTERED_AVAILABLE,
+ TransportStatus.REGISTERED_UNAVAILABLE,
+ TransportStatus.UNREGISTERED
+ })
+ public @interface TransportStatus {
+ int REGISTERED_AVAILABLE = 0;
+ int REGISTERED_UNAVAILABLE = 1;
+ int UNREGISTERED = 2;
+ }
+
+ private TransportTestUtils() {}
+}
diff --git a/com/android/server/backup/transport/OnTransportRegisteredListener.java b/com/android/server/backup/transport/OnTransportRegisteredListener.java
new file mode 100644
index 00000000..391ec2d7
--- /dev/null
+++ b/com/android/server/backup/transport/OnTransportRegisteredListener.java
@@ -0,0 +1,33 @@
+/*
+ * 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 com.android.server.backup.transport;
+
+import com.android.server.backup.TransportManager;
+
+/**
+ * Listener called when a transport is registered with the {@link TransportManager}. Can be set
+ * using {@link TransportManager#setOnTransportRegisteredListener(OnTransportRegisteredListener)}.
+ */
+@FunctionalInterface
+public interface OnTransportRegisteredListener {
+ /**
+ * Called when a transport is successfully registered.
+ * @param transportName The name of the transport.
+ * @param transportDirName The dir name of the transport.
+ */
+ public void onTransportRegistered(String transportName, String transportDirName);
+}
diff --git a/com/android/server/backup/transport/TransportClient.java b/com/android/server/backup/transport/TransportClient.java
index 2c7a0ebd..7b2e3df6 100644
--- a/com/android/server/backup/transport/TransportClient.java
+++ b/com/android/server/backup/transport/TransportClient.java
@@ -16,6 +16,8 @@
package com.android.server.backup.transport;
+import static com.android.server.backup.transport.TransportUtils.formatMessage;
+
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.annotation.WorkerThread;
@@ -28,17 +30,23 @@ import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.UserHandle;
+import android.text.format.DateFormat;
import android.util.ArrayMap;
+import android.util.EventLog;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.backup.IBackupTransport;
import com.android.internal.util.Preconditions;
+import com.android.server.EventLogTags;
import com.android.server.backup.TransportManager;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@@ -63,15 +71,19 @@ import java.util.concurrent.ExecutionException;
*/
public class TransportClient {
private static final String TAG = "TransportClient";
+ private static final int LOG_BUFFER_SIZE = 5;
private final Context mContext;
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();
+ private final Object mLogBufferLock = new Object();
+
+ @GuardedBy("mLogBufferLock")
+ private final List<String> mLogBuffer = new LinkedList<>();
@GuardedBy("mStateLock")
private final Map<TransportConnectionListener, String> mListeners = new ArrayMap<>();
@@ -87,13 +99,11 @@ public class TransportClient {
Context context,
Intent bindIntent,
ComponentName transportComponent,
- String transportDirName,
String identifier) {
this(
context,
bindIntent,
transportComponent,
- transportDirName,
identifier,
new Handler(Looper.getMainLooper()));
}
@@ -103,29 +113,23 @@ 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;
// For logging
String classNameForLog = mTransportComponent.getShortClassName().replaceFirst(".*\\.", "");
- mPrefixForLog = classNameForLog + "#" + mIdentifier + ": ";
+ mPrefixForLog = classNameForLog + "#" + mIdentifier + ":";
}
public ComponentName getTransportComponent() {
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.
@@ -236,7 +240,7 @@ public class TransportClient {
switch (mState) {
case State.UNUSABLE:
- log(Log.DEBUG, caller, "Async connect: UNUSABLE client");
+ log(Log.WARN, caller, "Async connect: UNUSABLE client");
notifyListener(listener, null, caller);
break;
case State.IDLE:
@@ -245,7 +249,7 @@ public class TransportClient {
mBindIntent,
mConnection,
Context.BIND_AUTO_CREATE,
- TransportManager.createSystemUserHandle());
+ UserHandle.SYSTEM);
if (hasBound) {
// We don't need to set a time-out because we are guaranteed to get a call
// back in ServiceConnection, either an onServiceConnected() or
@@ -331,14 +335,14 @@ public class TransportClient {
IBackupTransport transport = mTransport;
if (transport != null) {
- log(Log.DEBUG, caller, "Sync connect: reusing transport");
+ log(Log.INFO, caller, "Sync connect: reusing transport");
return transport;
}
// If it's already UNUSABLE we return straight away, no need to go to main-thread
synchronized (mStateLock) {
if (mState == State.UNUSABLE) {
- log(Log.DEBUG, caller, "Sync connect: UNUSABLE client");
+ log(Log.WARN, caller, "Sync connect: UNUSABLE client");
return null;
}
}
@@ -410,13 +414,16 @@ public class TransportClient {
}
private void notifyListener(
- TransportConnectionListener listener, IBackupTransport transport, String caller) {
- log(Log.VERBOSE, caller, "Notifying listener of transport = " + transport);
+ TransportConnectionListener listener,
+ @Nullable IBackupTransport transport,
+ String caller) {
+ String transportString = (transport != null) ? "IBackupTransport" : "null";
+ log(Log.INFO, "Notifying [" + caller + "] transport = " + transportString);
mListenerHandler.post(() -> listener.onTransportConnectionResult(transport, this));
}
@GuardedBy("mStateLock")
- private void notifyListenersAndClearLocked(IBackupTransport transport) {
+ private void notifyListenersAndClearLocked(@Nullable IBackupTransport transport) {
for (Map.Entry<TransportConnectionListener, String> entry : mListeners.entrySet()) {
TransportConnectionListener listener = entry.getKey();
String caller = entry.getValue();
@@ -428,10 +435,45 @@ public class TransportClient {
@GuardedBy("mStateLock")
private void setStateLocked(@State int state, @Nullable IBackupTransport transport) {
log(Log.VERBOSE, "State: " + stateToString(mState) + " => " + stateToString(state));
+ onStateTransition(mState, state);
mState = state;
mTransport = transport;
}
+ private void onStateTransition(int oldState, int newState) {
+ String transport = mTransportComponent.flattenToShortString();
+ int bound = transitionThroughState(oldState, newState, State.BOUND_AND_CONNECTING);
+ int connected = transitionThroughState(oldState, newState, State.CONNECTED);
+ if (bound != Transition.NO_TRANSITION) {
+ int value = (bound == Transition.UP) ? 1 : 0; // 1 is bound, 0 is not bound
+ EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, transport, value);
+ }
+ if (connected != Transition.NO_TRANSITION) {
+ int value = (connected == Transition.UP) ? 1 : 0; // 1 is connected, 0 is not connected
+ EventLog.writeEvent(EventLogTags.BACKUP_TRANSPORT_CONNECTION, transport, value);
+ }
+ }
+
+ /**
+ * Returns:
+ *
+ * <ul>
+ * <li>{@link Transition#UP}, if oldState < stateReference <= newState
+ * <li>{@link Transition#DOWN}, if oldState >= stateReference > newState
+ * <li>{@link Transition#NO_TRANSITION}, otherwise
+ */
+ @Transition
+ private int transitionThroughState(
+ @State int oldState, @State int newState, @State int stateReference) {
+ if (oldState < stateReference && stateReference <= newState) {
+ return Transition.UP;
+ }
+ if (oldState >= stateReference && stateReference > newState) {
+ return Transition.DOWN;
+ }
+ return Transition.NO_TRANSITION;
+ }
+
@GuardedBy("mStateLock")
private void checkStateIntegrityLocked() {
switch (mState) {
@@ -481,13 +523,38 @@ public class TransportClient {
}
private void log(int priority, String message) {
- TransportUtils.log(priority, TAG, message);
+ TransportUtils.log(priority, TAG, formatMessage(mPrefixForLog, null, message));
+ saveLogEntry(formatMessage(null, null, message));
}
- private void log(int priority, String caller, String msg) {
- TransportUtils.log(priority, TAG, mPrefixForLog, caller, msg);
- // TODO(brufino): Log in internal list for dump
- // CharSequence time = DateFormat.format("yyyy-MM-dd HH:mm:ss", System.currentTimeMillis());
+ private void log(int priority, String caller, String message) {
+ TransportUtils.log(priority, TAG, formatMessage(mPrefixForLog, caller, message));
+ saveLogEntry(formatMessage(null, caller, message));
+ }
+
+ private void saveLogEntry(String message) {
+ CharSequence time = DateFormat.format("yyyy-MM-dd HH:mm:ss", System.currentTimeMillis());
+ message = time + " " + message;
+ synchronized (mLogBufferLock) {
+ if (mLogBuffer.size() == LOG_BUFFER_SIZE) {
+ mLogBuffer.remove(mLogBuffer.size() - 1);
+ }
+ mLogBuffer.add(0, message);
+ }
+ }
+
+ List<String> getLogBuffer() {
+ synchronized (mLogBufferLock) {
+ return Collections.unmodifiableList(mLogBuffer);
+ }
+ }
+
+ @IntDef({Transition.DOWN, Transition.NO_TRANSITION, Transition.UP})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface Transition {
+ int DOWN = -1;
+ int NO_TRANSITION = 0;
+ int UP = 1;
}
@IntDef({State.UNUSABLE, State.IDLE, State.BOUND_AND_CONNECTING, State.CONNECTED})
diff --git a/com/android/server/backup/transport/TransportClientManager.java b/com/android/server/backup/transport/TransportClientManager.java
index bb550f6e..1132bce6 100644
--- a/com/android/server/backup/transport/TransportClientManager.java
+++ b/com/android/server/backup/transport/TransportClientManager.java
@@ -17,20 +17,20 @@
package com.android.server.backup.transport;
import static com.android.server.backup.TransportManager.SERVICE_ACTION_TRANSPORT_HOST;
+import static com.android.server.backup.transport.TransportUtils.formatMessage;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.util.Log;
-
-import com.android.internal.backup.IBackupTransport;
import com.android.server.backup.TransportManager;
+import java.io.PrintWriter;
+import java.util.Map;
+import java.util.WeakHashMap;
/**
* Manages the creation and disposal of {@link TransportClient}s. The only class that should use
* this is {@link TransportManager}, all the other usages should go to {@link TransportManager}.
- *
- * <p>TODO(brufino): Implement pool of TransportClients
*/
public class TransportClientManager {
private static final String TAG = "TransportClientManager";
@@ -38,6 +38,7 @@ public class TransportClientManager {
private final Context mContext;
private final Object mTransportClientsLock = new Object();
private int mTransportClientsCreated = 0;
+ private Map<TransportClient, String> mTransportClientsCallerMap = new WeakHashMap<>();
public TransportClientManager(Context context) {
mContext = context;
@@ -48,17 +49,12 @@ 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 transportDirName,
- String caller) {
+ public TransportClient getTransportClient(ComponentName transportComponent, String caller) {
Intent bindIntent =
new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(transportComponent);
synchronized (mTransportClientsLock) {
@@ -67,10 +63,11 @@ public class TransportClientManager {
mContext,
bindIntent,
transportComponent,
- transportDirName,
Integer.toString(mTransportClientsCreated));
+ mTransportClientsCallerMap.put(transportClient, caller);
mTransportClientsCreated++;
- TransportUtils.log(Log.DEBUG, TAG, caller, "Retrieving " + transportClient);
+ TransportUtils.log(
+ Log.DEBUG, TAG, formatMessage(null, caller, "Retrieving " + transportClient));
return transportClient;
}
}
@@ -84,7 +81,25 @@ public class TransportClientManager {
* details.
*/
public void disposeOfTransportClient(TransportClient transportClient, String caller) {
- TransportUtils.log(Log.DEBUG, TAG, caller, "Disposing of " + transportClient);
transportClient.unbind(caller);
+ synchronized (mTransportClientsLock) {
+ TransportUtils.log(
+ Log.DEBUG, TAG, formatMessage(null, caller, "Disposing of " + transportClient));
+ mTransportClientsCallerMap.remove(transportClient);
+ }
+ }
+
+ public void dump(PrintWriter pw) {
+ pw.println("Transport clients created: " + mTransportClientsCreated);
+ synchronized (mTransportClientsLock) {
+ pw.println("Current transport clients: " + mTransportClientsCallerMap.size());
+ for (TransportClient transportClient : mTransportClientsCallerMap.keySet()) {
+ String caller = mTransportClientsCallerMap.get(transportClient);
+ pw.println(" " + transportClient + " [" + caller + "]");
+ for (String logEntry : transportClient.getLogBuffer()) {
+ pw.println(" " + logEntry);
+ }
+ }
+ }
}
}
diff --git a/com/android/server/backup/transport/TransportClientTest.java b/com/android/server/backup/transport/TransportClientTest.java
index 4462d2a0..10442b7e 100644
--- a/com/android/server/backup/transport/TransportClientTest.java
+++ b/com/android/server/backup/transport/TransportClientTest.java
@@ -17,9 +17,7 @@
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,14 +34,13 @@ import android.os.Handler;
import android.os.Looper;
import android.os.UserHandle;
import android.platform.test.annotations.Presubmit;
-
import com.android.internal.backup.IBackupTransport;
+import com.android.server.EventLogTags;
import com.android.server.backup.TransportManager;
import com.android.server.testing.FrameworkRobolectricTestRunner;
+import com.android.server.testing.ShadowEventLog;
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;
@@ -53,7 +50,7 @@ import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLooper;
@RunWith(FrameworkRobolectricTestRunner.class)
-@Config(manifest = Config.NONE, sdk = 26)
+@Config(manifest = Config.NONE, sdk = 26, shadows = {ShadowEventLog.class})
@SystemLoaderClasses({TransportManager.class, TransportClient.class})
@Presubmit
public class TransportClientTest {
@@ -65,7 +62,7 @@ public class TransportClientTest {
@Mock private IBackupTransport.Stub mIBackupTransport;
private TransportClient mTransportClient;
private ComponentName mTransportComponent;
- private String mTransportDirName;
+ private String mTransportString;
private Intent mBindIntent;
private ShadowLooper mShadowLooper;
@@ -77,16 +74,11 @@ public class TransportClientTest {
mShadowLooper = shadowOf(mainLooper);
mTransportComponent =
new ComponentName(PACKAGE_NAME, PACKAGE_NAME + ".transport.Transport");
- mTransportDirName = mTransportComponent.toString();
+ mTransportString = mTransportComponent.flattenToShortString();
mBindIntent = new Intent(SERVICE_ACTION_TRANSPORT_HOST).setComponent(mTransportComponent);
mTransportClient =
new TransportClient(
- mContext,
- mBindIntent,
- mTransportComponent,
- mTransportDirName,
- "1",
- new Handler(mainLooper));
+ mContext, mBindIntent, mTransportComponent, "1", new Handler(mainLooper));
when(mContext.bindServiceAsUser(
eq(mBindIntent),
@@ -97,11 +89,6 @@ public class TransportClientTest {
}
@Test
- public void testGetTransportDirName_returnsTransportDirName() {
- assertThat(mTransportClient.getTransportDirName()).isEqualTo(mTransportDirName);
- }
-
- @Test
public void testGetTransportComponent_returnsTransportComponent() {
assertThat(mTransportClient.getTransportComponent()).isEqualTo(mTransportComponent);
}
@@ -178,7 +165,7 @@ public class TransportClientTest {
}
@Test
- public void testConnectAsync_whenFrameworkDoesntBind_releasesConnection() throws Exception {
+ public void testConnectAsync_whenFrameworkDoesNotBind_releasesConnection() throws Exception {
when(mContext.bindServiceAsUser(
eq(mBindIntent),
any(ServiceConnection.class),
@@ -222,32 +209,110 @@ public class TransportClientTest {
.onTransportConnectionResult(isNull(), eq(mTransportClient));
}
- // TODO(b/69153972): Support SDK 26 API (ServiceConnection.inBindingDied) for transport tests
- /*@Test
+ @Test
public void testConnectAsync_callsListenerIfBindingDies() throws Exception {
- mTransportClient.connectAsync(mTransportListener, "caller");
+ mTransportClient.connectAsync(mTransportConnectionListener, "caller");
ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
connection.onBindingDied(mTransportComponent);
mShadowLooper.runToEndOfTasks();
- verify(mTransportListener).onTransportBound(isNull(), eq(mTransportClient));
+ verify(mTransportConnectionListener)
+ .onTransportConnectionResult(isNull(), eq(mTransportClient));
}
@Test
public void testConnectAsync_whenPendingConnection_callsListenersIfBindingDies()
throws Exception {
- mTransportClient.connectAsync(mTransportListener, "caller1");
+ mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
- mTransportClient.connectAsync(mTransportListener2, "caller2");
+ mTransportClient.connectAsync(mTransportConnectionListener2, "caller2");
connection.onBindingDied(mTransportComponent);
mShadowLooper.runToEndOfTasks();
- verify(mTransportListener).onTransportBound(isNull(), eq(mTransportClient));
- verify(mTransportListener2).onTransportBound(isNull(), eq(mTransportClient));
- }*/
+ verify(mTransportConnectionListener)
+ .onTransportConnectionResult(isNull(), eq(mTransportClient));
+ verify(mTransportConnectionListener2)
+ .onTransportConnectionResult(isNull(), eq(mTransportClient));
+ }
+
+ @Test
+ public void testConnectAsync_beforeFrameworkCall_logsBoundTransition() {
+ ShadowEventLog.clearEvents();
+
+ mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+
+ assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 1);
+ }
+
+ @Test
+ public void testConnectAsync_afterOnServiceConnected_logsBoundAndConnectedTransitions() {
+ ShadowEventLog.clearEvents();
+
+ mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+ ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+ connection.onServiceConnected(mTransportComponent, mIBackupTransport);
+
+ assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 1);
+ assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 1);
+ }
+
+ @Test
+ public void testConnectAsync_afterOnBindingDied_logsBoundAndUnboundTransitions() {
+ ShadowEventLog.clearEvents();
+
+ mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+ ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+ connection.onBindingDied(mTransportComponent);
+
+ assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 1);
+ assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0);
+ }
+
+ @Test
+ public void testUnbind_whenConnected_logsDisconnectedAndUnboundTransitions() {
+ mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+ ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+ connection.onServiceConnected(mTransportComponent, mIBackupTransport);
+ ShadowEventLog.clearEvents();
+
+ mTransportClient.unbind("caller1");
+
+ assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 0);
+ assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0);
+ }
+
+ @Test
+ public void testOnServiceDisconnected_whenConnected_logsDisconnectedAndUnboundTransitions() {
+ mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+ ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+ connection.onServiceConnected(mTransportComponent, mIBackupTransport);
+ ShadowEventLog.clearEvents();
+
+ connection.onServiceDisconnected(mTransportComponent);
+
+ assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 0);
+ assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0);
+ }
+
+ @Test
+ public void testOnBindingDied_whenConnected_logsDisconnectedAndUnboundTransitions() {
+ mTransportClient.connectAsync(mTransportConnectionListener, "caller1");
+ ServiceConnection connection = verifyBindServiceAsUserAndCaptureServiceConnection(mContext);
+ connection.onServiceConnected(mTransportComponent, mIBackupTransport);
+ ShadowEventLog.clearEvents();
+
+ connection.onBindingDied(mTransportComponent);
+
+ assertEventLogged(EventLogTags.BACKUP_TRANSPORT_CONNECTION, mTransportString, 0);
+ assertEventLogged(EventLogTags.BACKUP_TRANSPORT_LIFECYCLE, mTransportString, 0);
+ }
+
+ private void assertEventLogged(int tag, Object... values) {
+ assertThat(ShadowEventLog.hasEvent(tag, values)).isTrue();
+ }
private ServiceConnection verifyBindServiceAsUserAndCaptureServiceConnection(Context context) {
ArgumentCaptor<ServiceConnection> connectionCaptor =
diff --git a/com/android/server/backup/transport/TransportNotRegisteredException.java b/com/android/server/backup/transport/TransportNotRegisteredException.java
index 26bf92cb..02766dee 100644
--- a/com/android/server/backup/transport/TransportNotRegisteredException.java
+++ b/com/android/server/backup/transport/TransportNotRegisteredException.java
@@ -16,6 +16,7 @@
package com.android.server.backup.transport;
+import android.content.ComponentName;
import android.util.AndroidException;
import com.android.server.backup.TransportManager;
@@ -32,4 +33,8 @@ public class TransportNotRegisteredException extends AndroidException {
public TransportNotRegisteredException(String transportName) {
super("Transport " + transportName + " not registered");
}
+
+ public TransportNotRegisteredException(ComponentName transportComponent) {
+ super("Transport for host " + transportComponent + " not registered");
+ }
}
diff --git a/com/android/server/backup/transport/TransportUtils.java b/com/android/server/backup/transport/TransportUtils.java
index 92bba9bf..56b2d44e 100644
--- a/com/android/server/backup/transport/TransportUtils.java
+++ b/com/android/server/backup/transport/TransportUtils.java
@@ -41,21 +41,20 @@ public class TransportUtils {
}
static void log(int priority, String tag, String message) {
- log(priority, tag, null, message);
- }
-
- static void log(int priority, String tag, @Nullable String caller, String message) {
- log(priority, tag, "", caller, message);
+ if (Log.isLoggable(tag, priority)) {
+ Slog.println(priority, tag, message);
+ }
}
- static void log(
- int priority, String tag, String prefix, @Nullable String caller, String message) {
- if (Log.isLoggable(tag, priority)) {
- if (caller != null) {
- prefix += "[" + caller + "] ";
- }
- Slog.println(priority, tag, prefix + message);
+ static String formatMessage(@Nullable String prefix, @Nullable String caller, String message) {
+ StringBuilder string = new StringBuilder();
+ if (prefix != null) {
+ string.append(prefix).append(" ");
+ }
+ if (caller != null) {
+ string.append("[").append(caller).append("] ");
}
+ return string.append(message).toString();
}
private TransportUtils() {}
diff --git a/com/android/server/backup/utils/AppBackupUtils.java b/com/android/server/backup/utils/AppBackupUtils.java
index d7cac777..dbf1a826 100644
--- a/com/android/server/backup/utils/AppBackupUtils.java
+++ b/com/android/server/backup/utils/AppBackupUtils.java
@@ -20,6 +20,8 @@ import static com.android.server.backup.RefactoredBackupManagerService.MORE_DEBU
import static com.android.server.backup.RefactoredBackupManagerService.SHARED_BACKUP_AGENT_PACKAGE;
import static com.android.server.backup.RefactoredBackupManagerService.TAG;
+import android.annotation.Nullable;
+import android.app.backup.BackupTransport;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
@@ -27,7 +29,9 @@ import android.content.pm.Signature;
import android.os.Process;
import android.util.Slog;
+import com.android.internal.backup.IBackupTransport;
import com.android.internal.util.ArrayUtils;
+import com.android.server.backup.transport.TransportClient;
/**
* Utility methods wrapping operations on ApplicationInfo and PackageInfo.
@@ -71,6 +75,44 @@ public class AppBackupUtils {
return !appIsDisabled(app, pm);
}
+ /**
+ * Returns whether an app is eligible for backup at runtime. That is, the app has to:
+ * <ol>
+ * <li>Return true for {@link #appIsEligibleForBackup(ApplicationInfo, PackageManager)}
+ * <li>Return false for {@link #appIsStopped(ApplicationInfo)}
+ * <li>Return false for {@link #appIsDisabled(ApplicationInfo, PackageManager)}
+ * <li>Be eligible for the transport via
+ * {@link BackupTransport#isAppEligibleForBackup(PackageInfo, boolean)}
+ * </ol>
+ */
+ public static boolean appIsRunningAndEligibleForBackupWithTransport(
+ @Nullable TransportClient transportClient, String packageName, PackageManager pm) {
+ try {
+ PackageInfo packageInfo = pm.getPackageInfo(packageName, PackageManager.GET_SIGNATURES);
+ ApplicationInfo applicationInfo = packageInfo.applicationInfo;
+ if (!appIsEligibleForBackup(applicationInfo, pm)
+ || appIsStopped(applicationInfo)
+ || appIsDisabled(applicationInfo, pm)) {
+ return false;
+ }
+ if (transportClient != null) {
+ try {
+ IBackupTransport transport =
+ transportClient.connectOrThrow(
+ "AppBackupUtils.appIsEligibleForBackupAtRuntime");
+ return transport.isAppEligibleForBackup(
+ packageInfo, AppBackupUtils.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 (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+
/** Avoid backups of 'disabled' apps. */
public static boolean appIsDisabled(ApplicationInfo app, PackageManager pm) {
switch (pm.getApplicationEnabledSetting(app.packageName)) {
diff --git a/com/android/server/backup/utils/RestoreUtils.java b/com/android/server/backup/utils/RestoreUtils.java
index ee4201e1..632f5b53 100644
--- a/com/android/server/backup/utils/RestoreUtils.java
+++ b/com/android/server/backup/utils/RestoreUtils.java
@@ -19,23 +19,31 @@ package com.android.server.backup.utils;
import static com.android.server.backup.RefactoredBackupManagerService.DEBUG;
import static com.android.server.backup.RefactoredBackupManagerService.TAG;
+import android.content.Context;
+import android.content.IIntentReceiver;
+import android.content.IIntentSender;
+import android.content.Intent;
+import android.content.IntentSender;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
+import android.content.pm.PackageInstaller;
+import android.content.pm.PackageInstaller.Session;
+import android.content.pm.PackageInstaller.SessionParams;
import android.content.pm.PackageManager;
import android.content.pm.Signature;
-import android.net.Uri;
+import android.os.Bundle;
+import android.os.IBinder;
import android.os.Process;
import android.util.Slog;
+import com.android.internal.annotations.GuardedBy;
import com.android.server.backup.FileMetadata;
import com.android.server.backup.restore.RestoreDeleteObserver;
-import com.android.server.backup.restore.RestoreInstallObserver;
import com.android.server.backup.restore.RestorePolicy;
-import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.io.OutputStream;
import java.util.HashMap;
/**
@@ -47,62 +55,73 @@ public class RestoreUtils {
* Reads apk contents from input stream and installs the apk.
*
* @param instream - input stream to read apk data from.
- * @param packageManager - {@link PackageManager} instance.
- * @param installObserver - {@link RestoreInstallObserver} instance.
+ * @param context - installing context
* @param deleteObserver - {@link RestoreDeleteObserver} instance.
* @param manifestSignatures - manifest signatures.
* @param packagePolicies - package policies.
* @param info - backup file info.
- * @param installerPackage - installer package.
+ * @param installerPackageName - package name of installer.
* @param bytesReadListener - listener to be called for counting bytes read.
- * @param dataDir - directory where to create apk file.
* @return true if apk was successfully read and installed and false otherwise.
*/
// TODO: Refactor to get rid of unneeded params.
- public static boolean installApk(InputStream instream, PackageManager packageManager,
- RestoreInstallObserver installObserver, RestoreDeleteObserver deleteObserver,
+ public static boolean installApk(InputStream instream, Context context,
+ RestoreDeleteObserver deleteObserver,
HashMap<String, Signature[]> manifestSignatures,
HashMap<String, RestorePolicy> packagePolicies,
FileMetadata info,
- String installerPackage, BytesReadListener bytesReadListener,
- File dataDir) {
+ String installerPackageName, BytesReadListener bytesReadListener) {
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(dataDir, 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) {
- bytesReadListener.onBytesRead(didRead);
+ LocalIntentReceiver receiver = new LocalIntentReceiver();
+ PackageManager packageManager = context.getPackageManager();
+ PackageInstaller installer = packageManager.getPackageInstaller();
+
+ SessionParams params = new SessionParams(SessionParams.MODE_FULL_INSTALL);
+ params.setInstallerPackageName(installerPackageName);
+ int sessionId = installer.createSession(params);
+ try {
+ try (Session session = installer.openSession(sessionId)) {
+ try (OutputStream apkStream = session.openWrite(info.packageName, 0,
+ info.size)) {
+ 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) {
+ bytesReadListener.onBytesRead(didRead);
+ }
+ apkStream.write(buffer, 0, didRead);
+ size -= didRead;
+ }
+ }
+
+ // Installation is current disabled
+ session.abandon();
+ // session.commit(receiver.getIntentSender());
}
- apkStream.write(buffer, 0, didRead);
- size -= didRead;
+ } catch (Exception t) {
+ installer.abandonSession(sessionId);
+
+ throw t;
}
- apkStream.close();
- // make sure the installer can read it
- apkFile.setReadable(true, false);
+ // Installation is current disabled
+ Intent result = null;
+ // Intent result = receiver.getResult();
- // Now install it
- Uri packageUri = Uri.fromFile(apkFile);
- installObserver.reset();
- // TODO: PackageManager.installPackage() is deprecated, refactor.
- packageManager.installPackage(packageUri, installObserver,
- PackageManager.INSTALL_REPLACE_EXISTING | PackageManager.INSTALL_FROM_ADB,
- installerPackage);
- installObserver.waitForCompletion();
+ // Installation is current disabled
+ int status = PackageInstaller.STATUS_FAILURE;
+ // int status = result.getIntExtra(PackageInstaller.EXTRA_STATUS,
+ // PackageInstaller.STATUS_FAILURE);
- if (installObserver.getResult() != PackageManager.INSTALL_SUCCEEDED) {
+ if (status != PackageInstaller.STATUS_SUCCESS) {
// 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.
@@ -112,10 +131,12 @@ public class RestoreUtils {
} else {
// Okay, the install succeeded. Make sure it was the right app.
boolean uninstall = false;
- if (!installObserver.getPackageName().equals(info.packageName)) {
+ final String installedPackageName = result.getStringExtra(
+ PackageInstaller.EXTRA_PACKAGE_NAME);
+ if (!installedPackageName.equals(info.packageName)) {
Slog.w(TAG, "Restore stream claimed to include apk for "
+ info.packageName + " but apk was really "
- + installObserver.getPackageName());
+ + installedPackageName);
// delete the package we just put in place; it might be fraudulent
okay = false;
uninstall = true;
@@ -161,7 +182,7 @@ public class RestoreUtils {
if (uninstall) {
deleteObserver.reset();
packageManager.deletePackage(
- installObserver.getPackageName(),
+ installedPackageName,
deleteObserver, 0);
deleteObserver.waitForCompletion();
}
@@ -169,10 +190,44 @@ public class RestoreUtils {
} catch (IOException e) {
Slog.e(TAG, "Unable to transcribe restored apk for install");
okay = false;
- } finally {
- apkFile.delete();
}
return okay;
}
+
+ private static class LocalIntentReceiver {
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private Intent mResult = null;
+
+ private IIntentSender.Stub mLocalSender = new IIntentSender.Stub() {
+ @Override
+ public void send(int code, Intent intent, String resolvedType, IBinder whitelistToken,
+ IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) {
+ synchronized (mLock) {
+ mResult = intent;
+ mLock.notifyAll();
+ }
+ }
+ };
+
+ public IntentSender getIntentSender() {
+ return new IntentSender((IIntentSender) mLocalSender);
+ }
+
+ public Intent getResult() {
+ synchronized (mLock) {
+ while (mResult == null) {
+ try {
+ mLock.wait();
+ } catch (InterruptedException e) {
+ // ignored
+ }
+ }
+
+ return mResult;
+ }
+ }
+ }
}
diff --git a/com/android/server/broadcastradio/BroadcastRadioService.java b/com/android/server/broadcastradio/BroadcastRadioService.java
index 30641440..4289a25e 100644
--- a/com/android/server/broadcastradio/BroadcastRadioService.java
+++ b/com/android/server/broadcastradio/BroadcastRadioService.java
@@ -20,19 +20,29 @@ import android.annotation.NonNull;
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
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 android.os.RemoteException;
+import android.util.Slog;
+import com.android.internal.util.Preconditions;
import com.android.server.SystemService;
+import com.android.server.broadcastradio.hal2.AnnouncementAggregator;
+import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.OptionalInt;
public class BroadcastRadioService extends SystemService {
+ private static final String TAG = "BcRadioSrv";
+ private static final boolean DEBUG = false;
+
private final ServiceImpl mServiceImpl = new ServiceImpl();
private final com.android.server.broadcastradio.hal1.BroadcastRadioService mHal1 =
@@ -83,18 +93,39 @@ public class BroadcastRadioService extends SystemService {
@Override
public ITuner openTuner(int moduleId, RadioManager.BandConfig bandConfig,
- boolean withAudio, ITunerCallback callback) {
+ boolean withAudio, ITunerCallback callback) throws RemoteException {
+ if (DEBUG) Slog.i(TAG, "Opening module " + moduleId);
enforcePolicyAccess();
if (callback == null) {
throw new IllegalArgumentException("Callback must not be empty");
}
synchronized (mLock) {
if (mHal2.hasModule(moduleId)) {
- throw new RuntimeException("Not implemented");
+ return mHal2.openSession(moduleId, bandConfig, withAudio, callback);
} else {
return mHal1.openTuner(moduleId, bandConfig, withAudio, callback);
}
}
}
+
+ @Override
+ public ICloseHandle addAnnouncementListener(int[] enabledTypes,
+ IAnnouncementListener listener) {
+ if (DEBUG) {
+ Slog.i(TAG, "Adding announcement listener for " + Arrays.toString(enabledTypes));
+ }
+ Objects.requireNonNull(enabledTypes);
+ Objects.requireNonNull(listener);
+ enforcePolicyAccess();
+
+ synchronized (mLock) {
+ if (!mHal2.hasAnyModules()) {
+ Slog.i(TAG, "There are no HAL 2.x modules registered");
+ return new AnnouncementAggregator(listener);
+ }
+
+ return mHal2.addAnnouncementListener(enabledTypes, listener);
+ }
+ }
}
}
diff --git a/com/android/server/broadcastradio/hal1/Tuner.java b/com/android/server/broadcastradio/hal1/Tuner.java
index cce534d3..f9b35f53 100644
--- a/com/android/server/broadcastradio/hal1/Tuner.java
+++ b/com/android/server/broadcastradio/hal1/Tuner.java
@@ -21,6 +21,7 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.hardware.radio.ITuner;
import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.ProgramList;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
import android.os.IBinder;
@@ -249,8 +250,7 @@ class Tuner extends ITuner.Stub {
}
}
- @Override
- public List<RadioManager.ProgramInfo> getProgramList(Map vendorFilter) {
+ List<RadioManager.ProgramInfo> getProgramList(Map vendorFilter) {
Map<String, String> sFilter = vendorFilter;
synchronized (mLock) {
checkNotClosedLocked();
@@ -263,19 +263,41 @@ class Tuner extends ITuner.Stub {
}
@Override
- public boolean isAnalogForced() {
- synchronized (mLock) {
- checkNotClosedLocked();
- return nativeIsAnalogForced(mNativeContext);
+ public void startProgramListUpdates(ProgramList.Filter filter) {
+ mTunerCallback.startProgramListUpdates(filter);
+ }
+
+ @Override
+ public void stopProgramListUpdates() {
+ mTunerCallback.stopProgramListUpdates();
+ }
+
+ @Override
+ public boolean isConfigFlagSupported(int flag) {
+ return flag == RadioManager.CONFIG_FORCE_ANALOG;
+ }
+
+ @Override
+ public boolean isConfigFlagSet(int flag) {
+ if (flag == RadioManager.CONFIG_FORCE_ANALOG) {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ return nativeIsAnalogForced(mNativeContext);
+ }
}
+ throw new UnsupportedOperationException("Not supported by HAL 1.x");
}
@Override
- public void setAnalogForced(boolean isForced) {
- synchronized (mLock) {
- checkNotClosedLocked();
- nativeSetAnalogForced(mNativeContext, isForced);
+ public void setConfigFlag(int flag, boolean value) {
+ if (flag == RadioManager.CONFIG_FORCE_ANALOG) {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ nativeSetAnalogForced(mNativeContext, value);
+ return;
+ }
}
+ throw new UnsupportedOperationException("Not supported by HAL 1.x");
}
@Override
diff --git a/com/android/server/broadcastradio/hal1/TunerCallback.java b/com/android/server/broadcastradio/hal1/TunerCallback.java
index 673ff88d..18f56ed5 100644
--- a/com/android/server/broadcastradio/hal1/TunerCallback.java
+++ b/com/android/server/broadcastradio/hal1/TunerCallback.java
@@ -19,6 +19,7 @@ package com.android.server.broadcastradio.hal1;
import android.annotation.NonNull;
import android.hardware.radio.ITuner;
import android.hardware.radio.ITunerCallback;
+import android.hardware.radio.ProgramList;
import android.hardware.radio.RadioManager;
import android.hardware.radio.RadioMetadata;
import android.hardware.radio.RadioTuner;
@@ -28,6 +29,10 @@ import android.util.Slog;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
class TunerCallback implements ITunerCallback {
private static final String TAG = "BroadcastRadioService.TunerCallback";
@@ -40,6 +45,8 @@ class TunerCallback implements ITunerCallback {
@NonNull private final Tuner mTuner;
@NonNull private final ITunerCallback mClientCallback;
+ private final AtomicReference<ProgramList.Filter> mProgramListFilter = new AtomicReference<>();
+
TunerCallback(@NonNull Tuner tuner, @NonNull ITunerCallback clientCallback, int halRev) {
mTuner = tuner;
mClientCallback = clientCallback;
@@ -78,6 +85,15 @@ class TunerCallback implements ITunerCallback {
mTuner.close();
}
+ void startProgramListUpdates(@NonNull ProgramList.Filter filter) {
+ mProgramListFilter.set(Objects.requireNonNull(filter));
+ sendProgramListUpdate();
+ }
+
+ void stopProgramListUpdates() {
+ mProgramListFilter.set(null);
+ }
+
@Override
public void onError(int status) {
dispatch(() -> mClientCallback.onError(status));
@@ -121,6 +137,28 @@ class TunerCallback implements ITunerCallback {
@Override
public void onProgramListChanged() {
dispatch(() -> mClientCallback.onProgramListChanged());
+ sendProgramListUpdate();
+ }
+
+ private void sendProgramListUpdate() {
+ ProgramList.Filter filter = mProgramListFilter.get();
+ if (filter == null) return;
+
+ List<RadioManager.ProgramInfo> modified;
+ try {
+ modified = mTuner.getProgramList(filter.getVendorFilter());
+ } catch (IllegalStateException ex) {
+ Slog.d(TAG, "Program list not ready yet");
+ return;
+ }
+ Set<RadioManager.ProgramInfo> modifiedSet = modified.stream().collect(Collectors.toSet());
+ ProgramList.Chunk chunk = new ProgramList.Chunk(true, true, modifiedSet, null);
+ dispatch(() -> mClientCallback.onProgramListUpdated(chunk));
+ }
+
+ @Override
+ public void onProgramListUpdated(ProgramList.Chunk chunk) {
+ dispatch(() -> mClientCallback.onProgramListUpdated(chunk));
}
@Override
diff --git a/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java b/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java
new file mode 100644
index 00000000..0bbaf25a
--- /dev/null
+++ b/com/android/server/broadcastradio/hal2/AnnouncementAggregator.java
@@ -0,0 +1,128 @@
+/**
+ * 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 com.android.server.broadcastradio.hal2;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.radio.Announcement;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+public class AnnouncementAggregator extends ICloseHandle.Stub {
+ private static final String TAG = "BcRadio2Srv.AnnAggr";
+
+ private final Object mLock = new Object();
+ @NonNull private final IAnnouncementListener mListener;
+ private final IBinder.DeathRecipient mDeathRecipient = new DeathRecipient();
+
+ @GuardedBy("mLock")
+ private final Collection<ModuleWatcher> mModuleWatchers = new ArrayList<>();
+
+ @GuardedBy("mLock")
+ private boolean mIsClosed = false;
+
+ public AnnouncementAggregator(@NonNull IAnnouncementListener listener) {
+ mListener = Objects.requireNonNull(listener);
+ try {
+ listener.asBinder().linkToDeath(mDeathRecipient, 0);
+ } catch (RemoteException ex) {
+ ex.rethrowFromSystemServer();
+ }
+ }
+
+ private class ModuleWatcher extends IAnnouncementListener.Stub {
+ private @Nullable ICloseHandle mCloseHandle;
+ public @NonNull List<Announcement> currentList = new ArrayList<>();
+
+ public void onListUpdated(List<Announcement> active) {
+ currentList = Objects.requireNonNull(active);
+ AnnouncementAggregator.this.onListUpdated();
+ }
+
+ public void setCloseHandle(@NonNull ICloseHandle closeHandle) {
+ mCloseHandle = Objects.requireNonNull(closeHandle);
+ }
+
+ public void close() throws RemoteException {
+ if (mCloseHandle != null) mCloseHandle.close();
+ }
+ }
+
+ private class DeathRecipient implements IBinder.DeathRecipient {
+ public void binderDied() {
+ try {
+ close();
+ } catch (RemoteException ex) {}
+ }
+ }
+
+ private void onListUpdated() {
+ synchronized (mLock) {
+ if (mIsClosed) {
+ Slog.e(TAG, "Announcement aggregator is closed, it shouldn't receive callbacks");
+ return;
+ }
+ List<Announcement> combined = new ArrayList<>();
+ for (ModuleWatcher watcher : mModuleWatchers) {
+ combined.addAll(watcher.currentList);
+ }
+ TunerCallback.dispatch(() -> mListener.onListUpdated(combined));
+ }
+ }
+
+ public void watchModule(@NonNull RadioModule module, @NonNull int[] enabledTypes) {
+ synchronized (mLock) {
+ if (mIsClosed) throw new IllegalStateException();
+
+ ModuleWatcher watcher = new ModuleWatcher();
+ ICloseHandle closeHandle;
+ try {
+ closeHandle = module.addAnnouncementListener(enabledTypes, watcher);
+ } catch (RemoteException ex) {
+ Slog.e(TAG, "Failed to add announcement listener", ex);
+ return;
+ }
+ watcher.setCloseHandle(closeHandle);
+ mModuleWatchers.add(watcher);
+ }
+ }
+
+ @Override
+ public void close() throws RemoteException {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+ mIsClosed = true;
+
+ mListener.asBinder().unlinkToDeath(mDeathRecipient, 0);
+
+ for (ModuleWatcher watcher : mModuleWatchers) {
+ watcher.close();
+ }
+ mModuleWatchers.clear();
+ }
+ }
+}
diff --git a/com/android/server/broadcastradio/hal2/BroadcastRadioService.java b/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
index 76294774..406231ae 100644
--- a/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
+++ b/com/android/server/broadcastradio/hal2/BroadcastRadioService.java
@@ -17,6 +17,11 @@
package com.android.server.broadcastradio.hal2;
import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.hardware.radio.IAnnouncementListener;
+import android.hardware.radio.ICloseHandle;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ITunerCallback;
import android.hardware.radio.RadioManager;
import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
import android.hidl.manager.V1_0.IServiceManager;
@@ -28,6 +33,7 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.stream.Collectors;
public class BroadcastRadioService {
@@ -76,4 +82,44 @@ public class BroadcastRadioService {
public boolean hasModule(int id) {
return mModules.containsKey(id);
}
+
+ public boolean hasAnyModules() {
+ return !mModules.isEmpty();
+ }
+
+ public ITuner openSession(int moduleId, @Nullable RadioManager.BandConfig legacyConfig,
+ boolean withAudio, @NonNull ITunerCallback callback) throws RemoteException {
+ Objects.requireNonNull(callback);
+
+ if (!withAudio) {
+ throw new IllegalArgumentException("Non-audio sessions not supported with HAL 2.x");
+ }
+
+ RadioModule module = mModules.get(moduleId);
+ if (module == null) {
+ throw new IllegalArgumentException("Invalid module ID");
+ }
+
+ TunerSession session = module.openSession(callback);
+ session.setConfiguration(legacyConfig);
+ return session;
+ }
+
+ public ICloseHandle addAnnouncementListener(@NonNull int[] enabledTypes,
+ @NonNull IAnnouncementListener listener) {
+ AnnouncementAggregator aggregator = new AnnouncementAggregator(listener);
+ boolean anySupported = false;
+ for (RadioModule module : mModules.values()) {
+ try {
+ aggregator.watchModule(module, enabledTypes);
+ anySupported = true;
+ } catch (UnsupportedOperationException ex) {
+ Slog.v(TAG, "Announcements not supported for this module", ex);
+ }
+ }
+ if (!anySupported) {
+ Slog.i(TAG, "There are no HAL modules that support announcements");
+ }
+ return aggregator;
+ }
}
diff --git a/com/android/server/broadcastradio/hal2/Convert.java b/com/android/server/broadcastradio/hal2/Convert.java
index c3394e9e..7a95971c 100644
--- a/com/android/server/broadcastradio/hal2/Convert.java
+++ b/com/android/server/broadcastradio/hal2/Convert.java
@@ -18,12 +18,27 @@ package com.android.server.broadcastradio.hal2;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.hardware.broadcastradio.V2_0.AmFmBandRange;
+import android.hardware.broadcastradio.V2_0.AmFmRegionConfig;
+import android.hardware.broadcastradio.V2_0.Announcement;
+import android.hardware.broadcastradio.V2_0.IdentifierType;
+import android.hardware.broadcastradio.V2_0.ProgramFilter;
+import android.hardware.broadcastradio.V2_0.ProgramIdentifier;
+import android.hardware.broadcastradio.V2_0.ProgramInfo;
+import android.hardware.broadcastradio.V2_0.ProgramInfoFlags;
+import android.hardware.broadcastradio.V2_0.ProgramListChunk;
import android.hardware.broadcastradio.V2_0.Properties;
+import android.hardware.broadcastradio.V2_0.Result;
import android.hardware.broadcastradio.V2_0.VendorKeyValue;
+import android.hardware.radio.ProgramList;
import android.hardware.radio.ProgramSelector;
import android.hardware.radio.RadioManager;
+import android.os.ParcelableException;
import android.util.Slog;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
@@ -31,10 +46,33 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.stream.Collectors;
class Convert {
private static final String TAG = "BcRadio2Srv.convert";
+ static void throwOnError(String action, int result) {
+ switch (result) {
+ case Result.OK:
+ return;
+ case Result.UNKNOWN_ERROR:
+ throw new ParcelableException(new RuntimeException(action + ": UNKNOWN_ERROR"));
+ case Result.INTERNAL_ERROR:
+ throw new ParcelableException(new RuntimeException(action + ": INTERNAL_ERROR"));
+ case Result.INVALID_ARGUMENTS:
+ throw new IllegalArgumentException(action + ": INVALID_ARGUMENTS");
+ case Result.INVALID_STATE:
+ throw new IllegalStateException(action + ": INVALID_STATE");
+ case Result.NOT_SUPPORTED:
+ throw new UnsupportedOperationException(action + ": NOT_SUPPORTED");
+ case Result.TIMEOUT:
+ throw new ParcelableException(new RuntimeException(action + ": TIMEOUT"));
+ default:
+ throw new ParcelableException(new RuntimeException(
+ action + ": unknown error (" + result + ")"));
+ }
+ }
+
private static @NonNull Map<String, String>
vendorInfoFromHal(@Nullable List<VendorKeyValue> info) {
if (info == null) return Collections.emptyMap();
@@ -51,56 +89,100 @@ class Convert {
return map;
}
+ private static @ProgramSelector.ProgramType int identifierTypeToProgramType(
+ @ProgramSelector.IdentifierType int idType) {
+ switch (idType) {
+ case ProgramSelector.IDENTIFIER_TYPE_AMFM_FREQUENCY:
+ case ProgramSelector.IDENTIFIER_TYPE_RDS_PI:
+ // TODO(b/69958423): verify AM/FM with frequency range
+ return ProgramSelector.PROGRAM_TYPE_FM;
+ case ProgramSelector.IDENTIFIER_TYPE_HD_STATION_ID_EXT:
+ // TODO(b/69958423): verify AM/FM with frequency range
+ return ProgramSelector.PROGRAM_TYPE_FM_HD;
+ case ProgramSelector.IDENTIFIER_TYPE_DAB_SIDECC:
+ case ProgramSelector.IDENTIFIER_TYPE_DAB_ENSEMBLE:
+ case ProgramSelector.IDENTIFIER_TYPE_DAB_SCID:
+ case ProgramSelector.IDENTIFIER_TYPE_DAB_FREQUENCY:
+ return ProgramSelector.PROGRAM_TYPE_DAB;
+ case ProgramSelector.IDENTIFIER_TYPE_DRMO_SERVICE_ID:
+ case ProgramSelector.IDENTIFIER_TYPE_DRMO_FREQUENCY:
+ return ProgramSelector.PROGRAM_TYPE_DRMO;
+ case ProgramSelector.IDENTIFIER_TYPE_SXM_SERVICE_ID:
+ case ProgramSelector.IDENTIFIER_TYPE_SXM_CHANNEL:
+ return ProgramSelector.PROGRAM_TYPE_SXM;
+ }
+ if (idType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_START
+ && idType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_END) {
+ return idType;
+ }
+ return ProgramSelector.PROGRAM_TYPE_INVALID;
+ }
+
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;
+ int pType = identifierTypeToProgramType(idType);
+
+ if (pType == ProgramSelector.PROGRAM_TYPE_INVALID) continue;
+
+ pTypes.add(pType);
+ if (pType == ProgramSelector.PROGRAM_TYPE_FM) {
+ // TODO(b/69958423): verify AM/FM with region info
+ pTypes.add(ProgramSelector.PROGRAM_TYPE_AM);
}
- if (idType >= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_START
- && idType <= ProgramSelector.IDENTIFIER_TYPE_VENDOR_PRIMARY_END) {
- pTypes.add(idType);
+ if (pType == ProgramSelector.PROGRAM_TYPE_FM_HD) {
+ // TODO(b/69958423): verify AM/FM with region info
+ pTypes.add(ProgramSelector.PROGRAM_TYPE_AM_HD);
}
}
return pTypes.stream().mapToInt(Integer::intValue).toArray();
}
+ private static @NonNull RadioManager.BandDescriptor[]
+ amfmConfigToBands(@Nullable AmFmRegionConfig config) {
+ if (config == null) return new RadioManager.BandDescriptor[0];
+
+ int len = config.ranges.size();
+ List<RadioManager.BandDescriptor> bands = new ArrayList<>(len);
+
+ // Just a dummy value.
+ int region = RadioManager.REGION_ITU_1;
+
+ for (AmFmBandRange range : config.ranges) {
+ FrequencyBand bandType = Utils.getBand(range.lowerBound);
+ if (bandType == FrequencyBand.UNKNOWN) {
+ Slog.e(TAG, "Unknown frequency band at " + range.lowerBound + "kHz");
+ continue;
+ }
+ if (bandType == FrequencyBand.FM) {
+ bands.add(new RadioManager.FmBandDescriptor(region, RadioManager.BAND_FM,
+ range.lowerBound, range.upperBound, range.spacing,
+
+ // TODO(b/69958777): stereo, rds, ta, af, ea
+ true, true, true, true, true
+ ));
+ } else { // AM
+ bands.add(new RadioManager.AmBandDescriptor(region, RadioManager.BAND_AM,
+ range.lowerBound, range.upperBound, range.spacing,
+
+ // TODO(b/69958777): stereo
+ true
+ ));
+ }
+ }
+
+ return bands.toArray(new RadioManager.BandDescriptor[bands.size()]);
+ }
+
static @NonNull RadioManager.ModuleProperties
- propertiesFromHal(int id, @NonNull String serviceName, Properties prop) {
+ propertiesFromHal(int id, @NonNull String serviceName, @NonNull Properties prop,
+ @Nullable AmFmRegionConfig amfmConfig) {
+ Objects.requireNonNull(serviceName);
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);
@@ -123,10 +205,85 @@ class Convert {
1, // numAudioSources
false, // isCaptureSupported
- bands,
- true, // isBgScanSupported is deprecated
+ amfmConfigToBands(amfmConfig),
+ false, // isBgScanSupported is deprecated
supportedProgramTypes,
supportedIdentifierTypes,
- vendorInfoFromHal(prop.vendorInfo));
+ vendorInfoFromHal(prop.vendorInfo)
+ );
+ }
+
+ static @NonNull ProgramIdentifier programIdentifierToHal(
+ @NonNull ProgramSelector.Identifier id) {
+ ProgramIdentifier hwId = new ProgramIdentifier();
+ hwId.type = id.getType();
+ hwId.value = id.getValue();
+ return hwId;
+ }
+
+ static @Nullable ProgramSelector.Identifier programIdentifierFromHal(
+ @NonNull ProgramIdentifier id) {
+ if (id.type == IdentifierType.INVALID) return null;
+ return new ProgramSelector.Identifier(id.type, id.value);
+ }
+
+ static @NonNull ProgramSelector programSelectorFromHal(
+ @NonNull android.hardware.broadcastradio.V2_0.ProgramSelector sel) {
+ ProgramSelector.Identifier[] secondaryIds = sel.secondaryIds.stream().
+ map(id -> Objects.requireNonNull(programIdentifierFromHal(id))).
+ toArray(ProgramSelector.Identifier[]::new);
+
+ return new ProgramSelector(
+ identifierTypeToProgramType(sel.primaryId.type),
+ Objects.requireNonNull(programIdentifierFromHal(sel.primaryId)),
+ secondaryIds, null);
+ }
+
+ static @NonNull RadioManager.ProgramInfo programInfoFromHal(@NonNull ProgramInfo info) {
+ Collection<ProgramSelector.Identifier> relatedContent = info.relatedContent.stream().
+ map(id -> Objects.requireNonNull(programIdentifierFromHal(id))).
+ collect(Collectors.toList());
+
+ return new RadioManager.ProgramInfo(
+ programSelectorFromHal(info.selector),
+ programIdentifierFromHal(info.logicallyTunedTo),
+ programIdentifierFromHal(info.physicallyTunedTo),
+ relatedContent,
+ info.infoFlags,
+ info.signalQuality,
+ null, // TODO(b/69860743): metadata
+ vendorInfoFromHal(info.vendorInfo)
+ );
+ }
+
+ static @NonNull ProgramFilter programFilterToHal(@NonNull ProgramList.Filter filter) {
+ ProgramFilter hwFilter = new ProgramFilter();
+
+ filter.getIdentifierTypes().stream().forEachOrdered(hwFilter.identifierTypes::add);
+ filter.getIdentifiers().stream().forEachOrdered(
+ id -> hwFilter.identifiers.add(programIdentifierToHal(id)));
+ hwFilter.includeCategories = filter.areCategoriesIncluded();
+ hwFilter.excludeModifications = filter.areModificationsExcluded();
+
+ return hwFilter;
+ }
+
+ static @NonNull ProgramList.Chunk programListChunkFromHal(@NonNull ProgramListChunk chunk) {
+ Set<RadioManager.ProgramInfo> modified = chunk.modified.stream().
+ map(info -> programInfoFromHal(info)).collect(Collectors.toSet());
+ Set<ProgramSelector.Identifier> removed = chunk.removed.stream().
+ map(id -> Objects.requireNonNull(programIdentifierFromHal(id))).
+ collect(Collectors.toSet());
+
+ return new ProgramList.Chunk(chunk.purge, chunk.complete, modified, removed);
+ }
+
+ public static @NonNull android.hardware.radio.Announcement announcementFromHal(
+ @NonNull Announcement hwAnnouncement) {
+ return new android.hardware.radio.Announcement(
+ programSelectorFromHal(hwAnnouncement.selector),
+ hwAnnouncement.type,
+ vendorInfoFromHal(hwAnnouncement.vendorInfo)
+ );
}
}
diff --git a/com/android/server/broadcastradio/hal2/Mutable.java b/com/android/server/broadcastradio/hal2/Mutable.java
new file mode 100644
index 00000000..a9d80549
--- /dev/null
+++ b/com/android/server/broadcastradio/hal2/Mutable.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.broadcastradio.hal2;
+
+/**
+ * A wrapper class for mutable objects to be used in non-mutable contexts
+ * (i.e. final variables catched in lambda closures).
+ *
+ * @param <E> type of boxed value.
+ */
+final class Mutable<E> {
+ /**
+ * A mutable value.
+ */
+ public E value;
+
+ /**
+ * Initialize value with null pointer.
+ */
+ public Mutable() {
+ value = null;
+ }
+
+ /**
+ * Initialize value with specific value.
+ *
+ * @param value initial value.
+ */
+ public Mutable(E value) {
+ this.value = value;
+ }
+}
diff --git a/com/android/server/broadcastradio/hal2/RadioModule.java b/com/android/server/broadcastradio/hal2/RadioModule.java
index 34c1b0ce..4dff9e06 100644
--- a/com/android/server/broadcastradio/hal2/RadioModule.java
+++ b/com/android/server/broadcastradio/hal2/RadioModule.java
@@ -18,12 +18,25 @@ package com.android.server.broadcastradio.hal2;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.hardware.radio.ITuner;
import android.hardware.radio.RadioManager;
+import android.hardware.broadcastradio.V2_0.AmFmRegionConfig;
+import android.hardware.broadcastradio.V2_0.Announcement;
+import android.hardware.broadcastradio.V2_0.IAnnouncementListener;
import android.hardware.broadcastradio.V2_0.IBroadcastRadio;
+import android.hardware.broadcastradio.V2_0.ICloseHandle;
+import android.hardware.broadcastradio.V2_0.ITunerSession;
+import android.hardware.broadcastradio.V2_0.Result;
+import android.os.ParcelableException;
import android.os.RemoteException;
+import android.util.MutableInt;
import android.util.Slog;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.Objects;
+import java.util.stream.Collectors;
class RadioModule {
private static final String TAG = "BcRadio2Srv.module";
@@ -42,8 +55,13 @@ class RadioModule {
IBroadcastRadio service = IBroadcastRadio.getService();
if (service == null) return null;
+ Mutable<AmFmRegionConfig> amfmConfig = new Mutable<>();
+ service.getAmFmRegionConfig(false, (int result, AmFmRegionConfig config) -> {
+ if (result == Result.OK) amfmConfig.value = config;
+ });
+
RadioManager.ModuleProperties prop =
- Convert.propertiesFromHal(idx, fqName, service.getProperties());
+ Convert.propertiesFromHal(idx, fqName, service.getProperties(), amfmConfig.value);
return new RadioModule(service, prop);
} catch (RemoteException ex) {
@@ -51,4 +69,54 @@ class RadioModule {
return null;
}
}
+
+ public @NonNull TunerSession openSession(@NonNull android.hardware.radio.ITunerCallback userCb)
+ throws RemoteException {
+ TunerCallback cb = new TunerCallback(Objects.requireNonNull(userCb));
+ Mutable<ITunerSession> hwSession = new Mutable<>();
+ MutableInt halResult = new MutableInt(Result.UNKNOWN_ERROR);
+
+ mService.openSession(cb, (int result, ITunerSession session) -> {
+ hwSession.value = session;
+ halResult.value = result;
+ });
+
+ Convert.throwOnError("openSession", halResult.value);
+ Objects.requireNonNull(hwSession.value);
+
+ return new TunerSession(hwSession.value, cb);
+ }
+
+ public android.hardware.radio.ICloseHandle addAnnouncementListener(@NonNull int[] enabledTypes,
+ @NonNull android.hardware.radio.IAnnouncementListener listener) throws RemoteException {
+ ArrayList<Byte> enabledList = new ArrayList<>();
+ for (int type : enabledTypes) {
+ enabledList.add((byte)type);
+ }
+
+ MutableInt halResult = new MutableInt(Result.UNKNOWN_ERROR);
+ Mutable<ICloseHandle> hwCloseHandle = new Mutable<>();
+ IAnnouncementListener hwListener = new IAnnouncementListener.Stub() {
+ public void onListUpdated(ArrayList<Announcement> hwAnnouncements)
+ throws RemoteException {
+ listener.onListUpdated(hwAnnouncements.stream().
+ map(a -> Convert.announcementFromHal(a)).collect(Collectors.toList()));
+ }
+ };
+ mService.registerAnnouncementListener(enabledList, hwListener, (result, closeHandle) -> {
+ halResult.value = result;
+ hwCloseHandle.value = closeHandle;
+ });
+ Convert.throwOnError("addAnnouncementListener", halResult.value);
+
+ return new android.hardware.radio.ICloseHandle.Stub() {
+ public void close() {
+ try {
+ hwCloseHandle.value.close();
+ } catch (RemoteException ex) {
+ Slog.e(TAG, "Failed closing announcement listener", ex);
+ }
+ }
+ };
+ }
}
diff --git a/com/android/server/broadcastradio/hal2/TunerCallback.java b/com/android/server/broadcastradio/hal2/TunerCallback.java
new file mode 100644
index 00000000..ed2a1b3c
--- /dev/null
+++ b/com/android/server/broadcastradio/hal2/TunerCallback.java
@@ -0,0 +1,68 @@
+/**
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.broadcastradio.V2_0.ITunerCallback;
+import android.hardware.broadcastradio.V2_0.ProgramInfo;
+import android.hardware.broadcastradio.V2_0.ProgramListChunk;
+import android.hardware.broadcastradio.V2_0.ProgramSelector;
+import android.hardware.broadcastradio.V2_0.VendorKeyValue;
+import android.os.RemoteException;
+import android.util.Slog;
+
+import java.util.ArrayList;
+import java.util.Objects;
+
+class TunerCallback extends ITunerCallback.Stub {
+ private static final String TAG = "BcRadio2Srv.cb";
+
+ final android.hardware.radio.ITunerCallback mClientCb;
+
+ interface RunnableThrowingRemoteException {
+ void run() throws RemoteException;
+ }
+
+ TunerCallback(@NonNull android.hardware.radio.ITunerCallback clientCallback) {
+ mClientCb = Objects.requireNonNull(clientCallback);
+ }
+
+ static void dispatch(RunnableThrowingRemoteException func) {
+ try {
+ func.run();
+ } catch (RemoteException ex) {
+ Slog.e(TAG, "callback call failed", ex);
+ }
+ }
+
+ @Override
+ public void onTuneFailed(int result, ProgramSelector selector) {}
+
+ @Override
+ public void onCurrentProgramInfoChanged(ProgramInfo info) {}
+
+ @Override
+ public void onProgramListUpdated(ProgramListChunk chunk) {
+ dispatch(() -> mClientCb.onProgramListUpdated(Convert.programListChunkFromHal(chunk)));
+ }
+
+ @Override
+ public void onAntennaStateChange(boolean connected) {}
+
+ @Override
+ public void onParametersUpdated(ArrayList<VendorKeyValue> parameters) {}
+}
diff --git a/com/android/server/broadcastradio/hal2/TunerSession.java b/com/android/server/broadcastradio/hal2/TunerSession.java
new file mode 100644
index 00000000..1ae7d20f
--- /dev/null
+++ b/com/android/server/broadcastradio/hal2/TunerSession.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 com.android.server.broadcastradio.hal2;
+
+import android.annotation.NonNull;
+import android.graphics.Bitmap;
+import android.hardware.broadcastradio.V2_0.ConfigFlag;
+import android.hardware.broadcastradio.V2_0.ITunerSession;
+import android.hardware.broadcastradio.V2_0.Result;
+import android.hardware.radio.ITuner;
+import android.hardware.radio.ProgramList;
+import android.hardware.radio.ProgramSelector;
+import android.hardware.radio.RadioManager;
+import android.media.AudioSystem;
+import android.os.RemoteException;
+import android.util.MutableBoolean;
+import android.util.MutableInt;
+import android.util.Slog;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+class TunerSession extends ITuner.Stub {
+ private static final String TAG = "BcRadio2Srv.session";
+ private static final String kAudioDeviceName = "Radio tuner source";
+
+ private final Object mLock = new Object();
+
+ private final ITunerSession mHwSession;
+ private final TunerCallback mCallback;
+ private boolean mIsClosed = false;
+ private boolean mIsAudioConnected = false;
+ private boolean mIsMuted = false;
+
+ // necessary only for older APIs compatibility
+ private RadioManager.BandConfig mDummyConfig = null;
+
+ TunerSession(@NonNull ITunerSession hwSession, @NonNull TunerCallback callback) {
+ mHwSession = Objects.requireNonNull(hwSession);
+ mCallback = Objects.requireNonNull(callback);
+ notifyAudioServiceLocked(true);
+ }
+
+ @Override
+ public void close() {
+ synchronized (mLock) {
+ if (mIsClosed) return;
+ mIsClosed = true;
+ notifyAudioServiceLocked(false);
+ }
+ }
+
+ @Override
+ public boolean isClosed() {
+ return mIsClosed;
+ }
+
+ private void checkNotClosedLocked() {
+ if (mIsClosed) {
+ throw new IllegalStateException("Tuner is closed, no further operations are allowed");
+ }
+ }
+
+ private void notifyAudioServiceLocked(boolean connected) {
+ if (mIsAudioConnected == connected) return;
+
+ Slog.d(TAG, "Notifying AudioService about new state: " + connected);
+ int ret = AudioSystem.setDeviceConnectionState(AudioSystem.DEVICE_IN_FM_TUNER,
+ connected ? AudioSystem.DEVICE_STATE_AVAILABLE : AudioSystem.DEVICE_STATE_UNAVAILABLE,
+ null, kAudioDeviceName);
+
+ if (ret == AudioSystem.AUDIO_STATUS_OK) {
+ mIsAudioConnected = connected;
+ } else {
+ Slog.e(TAG, "Failed to notify AudioService about new state: " + connected);
+ }
+ }
+
+ @Override
+ public void setConfiguration(RadioManager.BandConfig config) {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ mDummyConfig = Objects.requireNonNull(config);
+ Slog.i(TAG, "Ignoring setConfiguration - not applicable for broadcastradio HAL 2.x");
+ TunerCallback.dispatch(() -> mCallback.mClientCb.onConfigurationChanged(config));
+ }
+ }
+
+ @Override
+ public RadioManager.BandConfig getConfiguration() {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ return mDummyConfig;
+ }
+ }
+
+ @Override
+ public void setMuted(boolean mute) {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ if (mIsMuted == mute) return;
+ mIsMuted = mute;
+ notifyAudioServiceLocked(!mute);
+ }
+ }
+
+ @Override
+ public boolean isMuted() {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ return mIsMuted;
+ }
+ }
+
+ @Override
+ public void step(boolean directionDown, boolean skipSubChannel) {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ }
+ }
+
+ @Override
+ public void scan(boolean directionDown, boolean skipSubChannel) {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ }
+ }
+
+ @Override
+ public void tune(ProgramSelector selector) {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ }
+ }
+
+ @Override
+ public void cancel() {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ }
+ }
+
+ @Override
+ public void cancelAnnouncement() {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ }
+ }
+
+ @Override
+ public RadioManager.ProgramInfo getProgramInformation() {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ return null;
+ }
+ }
+
+ @Override
+ public Bitmap getImage(int id) {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ return null;
+ }
+ }
+
+ @Override
+ public boolean startBackgroundScan() {
+ Slog.i(TAG, "Explicit background scan trigger is not supported with HAL 2.x");
+ return false;
+ }
+
+ @Override
+ public void startProgramListUpdates(ProgramList.Filter filter) throws RemoteException {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ int halResult = mHwSession.startProgramListUpdates(Convert.programFilterToHal(filter));
+ Convert.throwOnError("startProgramListUpdates", halResult);
+ }
+ }
+
+ @Override
+ public void stopProgramListUpdates() throws RemoteException {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ mHwSession.stopProgramListUpdates();
+ }
+ }
+
+ @Override
+ public boolean isConfigFlagSupported(int flag) {
+ try {
+ isConfigFlagSet(flag);
+ return true;
+ } catch (IllegalStateException ex) {
+ return true;
+ } catch (UnsupportedOperationException ex) {
+ return false;
+ }
+ }
+
+ @Override
+ public boolean isConfigFlagSet(int flag) {
+ Slog.v(TAG, "isConfigFlagSet " + ConfigFlag.toString(flag));
+ synchronized (mLock) {
+ checkNotClosedLocked();
+
+ MutableInt halResult = new MutableInt(Result.UNKNOWN_ERROR);
+ MutableBoolean flagState = new MutableBoolean(false);
+ try {
+ mHwSession.isConfigFlagSet(flag, (int result, boolean value) -> {
+ halResult.value = result;
+ flagState.value = value;
+ });
+ } catch (RemoteException ex) {
+ throw new RuntimeException("Failed to check flag " + ConfigFlag.toString(flag), ex);
+ }
+ Convert.throwOnError("isConfigFlagSet", halResult.value);
+
+ return flagState.value;
+ }
+ }
+
+ @Override
+ public void setConfigFlag(int flag, boolean value) throws RemoteException {
+ Slog.v(TAG, "setConfigFlag " + ConfigFlag.toString(flag) + " = " + value);
+ synchronized (mLock) {
+ checkNotClosedLocked();
+
+ int halResult = mHwSession.setConfigFlag(flag, value);
+ Convert.throwOnError("setConfigFlag", halResult);
+ }
+ }
+
+ @Override
+ public Map setParameters(Map parameters) {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ return null;
+ }
+ }
+
+ @Override
+ public Map getParameters(List<String> keys) {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ return null;
+ }
+ }
+
+ @Override
+ public boolean isAntennaConnected() {
+ synchronized (mLock) {
+ checkNotClosedLocked();
+ return true;
+ }
+ }
+}
diff --git a/com/android/server/broadcastradio/hal2/Utils.java b/com/android/server/broadcastradio/hal2/Utils.java
new file mode 100644
index 00000000..3520f378
--- /dev/null
+++ b/com/android/server/broadcastradio/hal2/Utils.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 com.android.server.broadcastradio.hal2;
+
+enum FrequencyBand {
+ UNKNOWN,
+ FM,
+ AM_LW,
+ AM_MW,
+ AM_SW,
+};
+
+class Utils {
+ private static final String TAG = "BcRadio2Srv.utils";
+
+ static FrequencyBand getBand(int freq) {
+ // keep in sync with hardware/interfaces/broadcastradio/common/utils2x/Utils.cpp
+ if (freq < 30) return FrequencyBand.UNKNOWN;
+ if (freq < 500) return FrequencyBand.AM_LW;
+ if (freq < 1705) return FrequencyBand.AM_MW;
+ if (freq < 30000) return FrequencyBand.AM_SW;
+ if (freq < 60000) return FrequencyBand.UNKNOWN;
+ if (freq < 110000) return FrequencyBand.FM;
+ return FrequencyBand.UNKNOWN;
+ }
+}
diff --git a/com/android/server/connectivity/ConnectivityConstants.java b/com/android/server/connectivity/ConnectivityConstants.java
new file mode 100644
index 00000000..24865bcd
--- /dev/null
+++ b/com/android/server/connectivity/ConnectivityConstants.java
@@ -0,0 +1,52 @@
+/*
+ * 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 com.android.server.connectivity;
+
+/**
+ * A class encapsulating various constants used by Connectivity.
+ * @hide
+ */
+public class ConnectivityConstants {
+ // IPC constants
+ public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
+ "android.net.conn.NETWORK_CONDITIONS_MEASURED";
+ public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
+ public static final String EXTRA_NETWORK_TYPE = "extra_network_type";
+ public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received";
+ public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal";
+ public static final String EXTRA_CELL_ID = "extra_cellid";
+ public static final String EXTRA_SSID = "extra_ssid";
+ public static final String EXTRA_BSSID = "extra_bssid";
+ /** real time since boot */
+ public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms";
+ public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms";
+
+ public static final String PERMISSION_ACCESS_NETWORK_CONDITIONS =
+ "android.permission.ACCESS_NETWORK_CONDITIONS";
+
+ // Penalty applied to scores of Networks that have not been validated.
+ public static final int UNVALIDATED_SCORE_PENALTY = 40;
+
+ // Score for explicitly connected network.
+ //
+ // This ensures that a) the explicitly selected network is never trumped by anything else, and
+ // b) the explicitly selected network is never torn down.
+ public static final int MAXIMUM_NETWORK_SCORE = 100;
+ // VPNs typically have priority over other networks. Give them a score that will
+ // let them win every single time.
+ public static final int VPN_DEFAULT_SCORE = 101;
+}
diff --git a/com/android/server/connectivity/DnsManager.java b/com/android/server/connectivity/DnsManager.java
new file mode 100644
index 00000000..a1c54bd4
--- /dev/null
+++ b/com/android/server/connectivity/DnsManager.java
@@ -0,0 +1,323 @@
+/*
+ * 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 com.android.server.connectivity;
+
+import static android.net.ConnectivityManager.PRIVATE_DNS_DEFAULT_MODE;
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OFF;
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.net.ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+import static android.provider.Settings.Global.DNS_RESOLVER_MIN_SAMPLES;
+import static android.provider.Settings.Global.DNS_RESOLVER_MAX_SAMPLES;
+import static android.provider.Settings.Global.DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS;
+import static android.provider.Settings.Global.DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT;
+import static android.provider.Settings.Global.PRIVATE_DNS_MODE;
+import static android.provider.Settings.Global.PRIVATE_DNS_SPECIFIER;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.net.LinkProperties;
+import android.net.Network;
+import android.net.NetworkUtils;
+import android.net.Uri;
+import android.os.Binder;
+import android.os.INetworkManagementService;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.system.GaiException;
+import android.system.OsConstants;
+import android.system.StructAddrinfo;
+import android.text.TextUtils;
+import android.util.Slog;
+
+import com.android.server.connectivity.MockableSystemProperties;
+
+import libcore.io.Libcore;
+
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.StringJoiner;
+
+
+/**
+ * Encapsulate the management of DNS settings for networks.
+ *
+ * This class it NOT designed for concurrent access. Furthermore, all non-static
+ * methods MUST be called from ConnectivityService's thread.
+ *
+ * @hide
+ */
+public class DnsManager {
+ private static final String TAG = DnsManager.class.getSimpleName();
+
+ /* Defaults for resolver parameters. */
+ private static final int DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS = 1800;
+ private static final int DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT = 25;
+ private static final int DNS_RESOLVER_DEFAULT_MIN_SAMPLES = 8;
+ private static final int DNS_RESOLVER_DEFAULT_MAX_SAMPLES = 64;
+
+ public static class PrivateDnsConfig {
+ public final boolean useTls;
+ public final String hostname;
+ public final InetAddress[] ips;
+
+ public PrivateDnsConfig() {
+ this(false);
+ }
+
+ public PrivateDnsConfig(boolean useTls) {
+ this.useTls = useTls;
+ this.hostname = "";
+ this.ips = new InetAddress[0];
+ }
+
+ public PrivateDnsConfig(String hostname, InetAddress[] ips) {
+ this.useTls = !TextUtils.isEmpty(hostname);
+ this.hostname = useTls ? hostname : "";
+ this.ips = (ips != null) ? ips : new InetAddress[0];
+ }
+
+ public PrivateDnsConfig(PrivateDnsConfig cfg) {
+ useTls = cfg.useTls;
+ hostname = cfg.hostname;
+ ips = cfg.ips;
+ }
+
+ public boolean inStrictMode() {
+ return useTls && !TextUtils.isEmpty(hostname);
+ }
+
+ public String toString() {
+ return PrivateDnsConfig.class.getSimpleName() +
+ "{" + useTls + ":" + hostname + "/" + Arrays.toString(ips) + "}";
+ }
+ }
+
+ public static PrivateDnsConfig getPrivateDnsConfig(ContentResolver cr) {
+ final String mode = getPrivateDnsMode(cr);
+
+ final boolean useTls = !TextUtils.isEmpty(mode) && !PRIVATE_DNS_MODE_OFF.equals(mode);
+
+ if (PRIVATE_DNS_MODE_PROVIDER_HOSTNAME.equals(mode)) {
+ final String specifier = getStringSetting(cr, PRIVATE_DNS_SPECIFIER);
+ return new PrivateDnsConfig(specifier, null);
+ }
+
+ return new PrivateDnsConfig(useTls);
+ }
+
+ public static PrivateDnsConfig tryBlockingResolveOf(Network network, String name) {
+ final StructAddrinfo hints = new StructAddrinfo();
+ // Unnecessary, but expressly no AI_ADDRCONFIG.
+ hints.ai_flags = 0;
+ // Fetch all IP addresses at once to minimize re-resolution.
+ hints.ai_family = OsConstants.AF_UNSPEC;
+ hints.ai_socktype = OsConstants.SOCK_DGRAM;
+
+ try {
+ final InetAddress[] ips = Libcore.os.android_getaddrinfo(name, hints, network.netId);
+ if (ips != null && ips.length > 0) {
+ return new PrivateDnsConfig(name, ips);
+ }
+ } catch (GaiException ignored) {}
+
+ return null;
+ }
+
+ public static Uri[] getPrivateDnsSettingsUris() {
+ final Uri[] uris = new Uri[2];
+ uris[0] = Settings.Global.getUriFor(PRIVATE_DNS_MODE);
+ uris[1] = Settings.Global.getUriFor(PRIVATE_DNS_SPECIFIER);
+ return uris;
+ }
+
+ private final Context mContext;
+ private final ContentResolver mContentResolver;
+ private final INetworkManagementService mNMS;
+ private final MockableSystemProperties mSystemProperties;
+ private final Map<Integer, PrivateDnsConfig> mPrivateDnsMap;
+
+ private int mNumDnsEntries;
+ private int mSampleValidity;
+ private int mSuccessThreshold;
+ private int mMinSamples;
+ private int mMaxSamples;
+ private String mPrivateDnsMode;
+ private String mPrivateDnsSpecifier;
+
+ public DnsManager(Context ctx, INetworkManagementService nms, MockableSystemProperties sp) {
+ mContext = ctx;
+ mContentResolver = mContext.getContentResolver();
+ mNMS = nms;
+ mSystemProperties = sp;
+ mPrivateDnsMap = new HashMap<>();
+
+ // TODO: Create and register ContentObservers to track every setting
+ // used herein, posting messages to respond to changes.
+ }
+
+ public PrivateDnsConfig getPrivateDnsConfig() {
+ return getPrivateDnsConfig(mContentResolver);
+ }
+
+ public void removeNetwork(Network network) {
+ mPrivateDnsMap.remove(network.netId);
+ }
+
+ public PrivateDnsConfig updatePrivateDns(Network network, PrivateDnsConfig cfg) {
+ Slog.w(TAG, "updatePrivateDns(" + network + ", " + cfg + ")");
+ return (cfg != null)
+ ? mPrivateDnsMap.put(network.netId, cfg)
+ : mPrivateDnsMap.remove(network);
+ }
+
+ public void setDnsConfigurationForNetwork(
+ int netId, LinkProperties lp, boolean isDefaultNetwork) {
+ // We only use the PrivateDnsConfig data pushed to this class instance
+ // from ConnectivityService because it works in coordination with
+ // NetworkMonitor to decide which networks need validation and runs the
+ // blocking calls to resolve Private DNS strict mode hostnames.
+ //
+ // At this time we do attempt to enable Private DNS on non-Internet
+ // networks like IMS.
+ final PrivateDnsConfig privateDnsCfg = mPrivateDnsMap.get(netId);
+
+ final boolean useTls = (privateDnsCfg != null) && privateDnsCfg.useTls;
+ final boolean strictMode = (privateDnsCfg != null) && privateDnsCfg.inStrictMode();
+ final String tlsHostname = strictMode ? privateDnsCfg.hostname : "";
+
+ final String[] serverStrs = NetworkUtils.makeStrings(
+ strictMode ? Arrays.stream(privateDnsCfg.ips)
+ .filter((ip) -> lp.isReachable(ip))
+ .collect(Collectors.toList())
+ : lp.getDnsServers());
+ final String[] domainStrs = getDomainStrings(lp.getDomains());
+
+ updateParametersSettings();
+ final int[] params = { mSampleValidity, mSuccessThreshold, mMinSamples, mMaxSamples };
+
+ Slog.d(TAG, String.format("setDnsConfigurationForNetwork(%d, %s, %s, %s, %s, %s)",
+ netId, Arrays.toString(serverStrs), Arrays.toString(domainStrs),
+ Arrays.toString(params), useTls, tlsHostname));
+ try {
+ mNMS.setDnsConfigurationForNetwork(
+ netId, serverStrs, domainStrs, params, useTls, tlsHostname);
+ } catch (Exception e) {
+ Slog.e(TAG, "Error setting DNS configuration: " + e);
+ return;
+ }
+
+ // TODO: netd should listen on [::1]:53 and proxy queries to the current
+ // default network, and we should just set net.dns1 to ::1, not least
+ // because applications attempting to use net.dns resolvers will bypass
+ // the privacy protections of things like DNS-over-TLS.
+ if (isDefaultNetwork) setDefaultDnsSystemProperties(lp.getDnsServers());
+ flushVmDnsCache();
+ }
+
+ public void setDefaultDnsSystemProperties(Collection<InetAddress> dnses) {
+ int last = 0;
+ for (InetAddress dns : dnses) {
+ ++last;
+ setNetDnsProperty(last, dns.getHostAddress());
+ }
+ for (int i = last + 1; i <= mNumDnsEntries; ++i) {
+ setNetDnsProperty(i, "");
+ }
+ mNumDnsEntries = last;
+ }
+
+ private void flushVmDnsCache() {
+ /*
+ * Tell the VMs to toss their DNS caches
+ */
+ final Intent intent = new Intent(Intent.ACTION_CLEAR_DNS_CACHE);
+ intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING);
+ /*
+ * Connectivity events can happen before boot has completed ...
+ */
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ final long ident = Binder.clearCallingIdentity();
+ try {
+ mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+
+ private void updateParametersSettings() {
+ mSampleValidity = getIntSetting(
+ DNS_RESOLVER_SAMPLE_VALIDITY_SECONDS,
+ DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS);
+ if (mSampleValidity < 0 || mSampleValidity > 65535) {
+ Slog.w(TAG, "Invalid sampleValidity=" + mSampleValidity + ", using default=" +
+ DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS);
+ mSampleValidity = DNS_RESOLVER_DEFAULT_SAMPLE_VALIDITY_SECONDS;
+ }
+
+ mSuccessThreshold = getIntSetting(
+ DNS_RESOLVER_SUCCESS_THRESHOLD_PERCENT,
+ DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT);
+ if (mSuccessThreshold < 0 || mSuccessThreshold > 100) {
+ Slog.w(TAG, "Invalid successThreshold=" + mSuccessThreshold + ", using default=" +
+ DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT);
+ mSuccessThreshold = DNS_RESOLVER_DEFAULT_SUCCESS_THRESHOLD_PERCENT;
+ }
+
+ mMinSamples = getIntSetting(DNS_RESOLVER_MIN_SAMPLES, DNS_RESOLVER_DEFAULT_MIN_SAMPLES);
+ mMaxSamples = getIntSetting(DNS_RESOLVER_MAX_SAMPLES, DNS_RESOLVER_DEFAULT_MAX_SAMPLES);
+ if (mMinSamples < 0 || mMinSamples > mMaxSamples || mMaxSamples > 64) {
+ Slog.w(TAG, "Invalid sample count (min, max)=(" + mMinSamples + ", " + mMaxSamples +
+ "), using default=(" + DNS_RESOLVER_DEFAULT_MIN_SAMPLES + ", " +
+ DNS_RESOLVER_DEFAULT_MAX_SAMPLES + ")");
+ mMinSamples = DNS_RESOLVER_DEFAULT_MIN_SAMPLES;
+ mMaxSamples = DNS_RESOLVER_DEFAULT_MAX_SAMPLES;
+ }
+ }
+
+ private int getIntSetting(String which, int dflt) {
+ return Settings.Global.getInt(mContentResolver, which, dflt);
+ }
+
+ private void setNetDnsProperty(int which, String value) {
+ final String key = "net.dns" + which;
+ // Log and forget errors setting unsupported properties.
+ try {
+ mSystemProperties.set(key, value);
+ } catch (Exception e) {
+ Slog.e(TAG, "Error setting unsupported net.dns property: ", e);
+ }
+ }
+
+ private static String getPrivateDnsMode(ContentResolver cr) {
+ final String mode = getStringSetting(cr, PRIVATE_DNS_MODE);
+ return !TextUtils.isEmpty(mode) ? mode : PRIVATE_DNS_DEFAULT_MODE;
+ }
+
+ private static String getStringSetting(ContentResolver cr, String which) {
+ return Settings.Global.getString(cr, which);
+ }
+
+ private static String[] getDomainStrings(String domains) {
+ return (TextUtils.isEmpty(domains)) ? new String[0] : domains.split(" ");
+ }
+}
diff --git a/com/android/server/connectivity/KeepaliveTracker.java b/com/android/server/connectivity/KeepaliveTracker.java
index 9e1f6b85..d24f9c98 100644
--- a/com/android/server/connectivity/KeepaliveTracker.java
+++ b/com/android/server/connectivity/KeepaliveTracker.java
@@ -18,10 +18,10 @@ package com.android.server.connectivity;
import com.android.internal.util.HexDump;
import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.connectivity.KeepalivePacketData;
import com.android.server.connectivity.NetworkAgentInfo;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.PacketKeepalive;
+import android.net.KeepalivePacketData;
import android.net.LinkAddress;
import android.net.NetworkAgent;
import android.net.NetworkUtils;
@@ -129,7 +129,7 @@ public class KeepaliveTracker {
.append("->")
.append(IpUtils.addressAndPortToString(mPacket.dstAddress, mPacket.dstPort))
.append(" interval=" + mInterval)
- .append(" data=" + HexDump.toHexString(mPacket.data))
+ .append(" packetData=" + HexDump.toHexString(mPacket.getPacket()))
.append(" uid=").append(mUid).append(" pid=").append(mPid)
.append(" ]")
.toString();
@@ -172,7 +172,7 @@ public class KeepaliveTracker {
}
private int checkInterval() {
- return mInterval >= 20 ? SUCCESS : ERROR_INVALID_INTERVAL;
+ return mInterval >= 10 ? SUCCESS : ERROR_INVALID_INTERVAL;
}
private int isValid() {
diff --git a/com/android/server/connectivity/MultipathPolicyTracker.java b/com/android/server/connectivity/MultipathPolicyTracker.java
new file mode 100644
index 00000000..9e0a230b
--- /dev/null
+++ b/com/android/server/connectivity/MultipathPolicyTracker.java
@@ -0,0 +1,361 @@
+/*
+ * 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 com.android.server.connectivity;
+
+import android.app.usage.NetworkStatsManager;
+import android.app.usage.NetworkStatsManager.UsageCallback;
+import android.content.Context;
+import android.net.INetworkStatsService;
+import android.net.INetworkPolicyManager;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkPolicyManager;
+import android.net.NetworkRequest;
+import android.net.NetworkStats;
+import android.net.NetworkTemplate;
+import android.net.StringNetworkSpecifier;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.telephony.TelephonyManager;
+import android.util.DebugUtils;
+import android.util.Slog;
+
+import java.util.Calendar;
+import java.util.concurrent.ConcurrentHashMap;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.LocalServices;
+import com.android.server.net.NetworkPolicyManagerInternal;
+
+import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_HANDOVER;
+import static android.net.ConnectivityManager.MULTIPATH_PREFERENCE_RELIABILITY;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.provider.Settings.Global.NETWORK_AVOID_BAD_WIFI;
+import static android.provider.Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE;
+import static com.android.server.net.NetworkPolicyManagerInternal.QUOTA_TYPE_MULTIPATH;
+
+/**
+ * Manages multipath data budgets.
+ *
+ * Informs the return value of ConnectivityManager#getMultipathPreference() based on:
+ * - The user's data plan, as returned by getSubscriptionOpportunisticQuota().
+ * - The amount of data usage that occurs on mobile networks while they are not the system default
+ * network (i.e., when the app explicitly selected such networks).
+ *
+ * Currently, quota is determined on a daily basis, from midnight to midnight local time.
+ *
+ * @hide
+ */
+public class MultipathPolicyTracker {
+ private static String TAG = MultipathPolicyTracker.class.getSimpleName();
+
+ private static final boolean DBG = false;
+
+ private final Context mContext;
+ private final Handler mHandler;
+
+ private ConnectivityManager mCM;
+ private NetworkStatsManager mStatsManager;
+ private NetworkPolicyManager mNPM;
+ private TelephonyManager mTelephonyManager;
+ private INetworkStatsService mStatsService;
+
+ private NetworkCallback mMobileNetworkCallback;
+ private NetworkPolicyManager.Listener mPolicyListener;
+
+ // STOPSHIP: replace this with a configurable mechanism.
+ private static final long DEFAULT_DAILY_MULTIPATH_QUOTA = 2_500_000;
+
+ private volatile int mMeteredMultipathPreference;
+
+ public MultipathPolicyTracker(Context ctx, Handler handler) {
+ mContext = ctx;
+ mHandler = handler;
+ // Because we are initialized by the ConnectivityService constructor, we can't touch any
+ // connectivity APIs. Service initialization is done in start().
+ }
+
+ public void start() {
+ mCM = (ConnectivityManager) mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mNPM = (NetworkPolicyManager) mContext.getSystemService(Context.NETWORK_POLICY_SERVICE);
+ mStatsManager = (NetworkStatsManager) mContext.getSystemService(
+ Context.NETWORK_STATS_SERVICE);
+ mStatsService = INetworkStatsService.Stub.asInterface(
+ ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
+
+ registerTrackMobileCallback();
+ registerNetworkPolicyListener();
+ }
+
+ public void shutdown() {
+ maybeUnregisterTrackMobileCallback();
+ unregisterNetworkPolicyListener();
+ for (MultipathTracker t : mMultipathTrackers.values()) {
+ t.shutdown();
+ }
+ mMultipathTrackers.clear();
+ }
+
+ // Called on an arbitrary binder thread.
+ public Integer getMultipathPreference(Network network) {
+ MultipathTracker t = mMultipathTrackers.get(network);
+ if (t != null) {
+ return t.getMultipathPreference();
+ }
+ return null;
+ }
+
+ // Track information on mobile networks as they come and go.
+ class MultipathTracker {
+ final Network network;
+ final int subId;
+ final String subscriberId;
+
+ private long mQuota;
+ /** Current multipath budget. Nonzero iff we have budget and a UsageCallback is armed. */
+ private long mMultipathBudget;
+ private final NetworkTemplate mNetworkTemplate;
+ private final UsageCallback mUsageCallback;
+
+ public MultipathTracker(Network network, NetworkCapabilities nc) {
+ this.network = network;
+ try {
+ subId = Integer.parseInt(
+ ((StringNetworkSpecifier) nc.getNetworkSpecifier()).toString());
+ } catch (ClassCastException | NullPointerException | NumberFormatException e) {
+ throw new IllegalStateException(String.format(
+ "Can't get subId from mobile network %s (%s): %s",
+ network, nc, e.getMessage()));
+ }
+
+ TelephonyManager tele = (TelephonyManager) mContext.getSystemService(
+ Context.TELEPHONY_SERVICE);
+ if (tele == null) {
+ throw new IllegalStateException(String.format("Missing TelephonyManager"));
+ }
+ tele = tele.createForSubscriptionId(subId);
+ if (tele == null) {
+ throw new IllegalStateException(String.format(
+ "Can't get TelephonyManager for subId %d", subId));
+ }
+
+ subscriberId = tele.getSubscriberId();
+ mNetworkTemplate = new NetworkTemplate(
+ NetworkTemplate.MATCH_MOBILE_ALL, subscriberId, new String[] { subscriberId },
+ null, NetworkStats.METERED_ALL, NetworkStats.ROAMING_ALL,
+ NetworkStats.DEFAULT_NETWORK_NO);
+ mUsageCallback = new UsageCallback() {
+ @Override
+ public void onThresholdReached(int networkType, String subscriberId) {
+ if (DBG) Slog.d(TAG, "onThresholdReached for network " + network);
+ mMultipathBudget = 0;
+ updateMultipathBudget();
+ }
+ };
+
+ updateMultipathBudget();
+ }
+
+ private long getDailyNonDefaultDataUsage() {
+ Calendar start = Calendar.getInstance();
+ Calendar end = (Calendar) start.clone();
+ start.set(Calendar.HOUR_OF_DAY, 0);
+ start.set(Calendar.MINUTE, 0);
+ start.set(Calendar.SECOND, 0);
+ start.set(Calendar.MILLISECOND, 0);
+
+ long bytes;
+ try {
+ // TODO: Consider using NetworkStatsManager.getSummaryForDevice instead.
+ bytes = mStatsService.getNetworkTotalBytes(mNetworkTemplate,
+ start.getTimeInMillis(), end.getTimeInMillis());
+ if (DBG) Slog.w(TAG, "Non-default data usage: " + bytes);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Can't fetch daily data usage: " + e);
+ bytes = -1;
+ } catch (IllegalStateException e) {
+ // Bandwidth control disabled?
+ bytes = -1;
+ }
+ return bytes;
+ }
+
+ void updateMultipathBudget() {
+ NetworkPolicyManagerInternal npms = LocalServices.getService(
+ NetworkPolicyManagerInternal.class);
+ long quota = npms.getSubscriptionOpportunisticQuota(this.network, QUOTA_TYPE_MULTIPATH);
+ if (DBG) Slog.d(TAG, "Opportunistic quota from data plan: " + quota + " bytes");
+
+ if (quota == 0) {
+ // STOPSHIP: replace this with a configurable mechanism.
+ quota = DEFAULT_DAILY_MULTIPATH_QUOTA;
+ if (DBG) Slog.d(TAG, "Setting quota: " + quota + " bytes");
+ }
+
+ if (haveMultipathBudget() && quota == mQuota) {
+ // If we already have a usage callback pending , there's no need to re-register it
+ // if the quota hasn't changed. The callback will simply fire as expected when the
+ // budget is spent. Also: if we re-register the callback when we're below the
+ // UsageCallback's minimum value of 2MB, we'll overshoot the budget.
+ if (DBG) Slog.d(TAG, "Quota still " + quota + ", not updating.");
+ return;
+ }
+ mQuota = quota;
+
+ long usage = getDailyNonDefaultDataUsage();
+ long budget = Math.max(0, quota - usage);
+ if (budget > 0) {
+ if (DBG) Slog.d(TAG, "Setting callback for " + budget +
+ " bytes on network " + network);
+ registerUsageCallback(budget);
+ } else {
+ maybeUnregisterUsageCallback();
+ }
+ }
+
+ public int getMultipathPreference() {
+ if (haveMultipathBudget()) {
+ return MULTIPATH_PREFERENCE_HANDOVER | MULTIPATH_PREFERENCE_RELIABILITY;
+ }
+ return 0;
+ }
+
+ // For debugging only.
+ public long getQuota() {
+ return mQuota;
+ }
+
+ // For debugging only.
+ public long getMultipathBudget() {
+ return mMultipathBudget;
+ }
+
+ private boolean haveMultipathBudget() {
+ return mMultipathBudget > 0;
+ }
+
+ private void registerUsageCallback(long budget) {
+ maybeUnregisterUsageCallback();
+ mStatsManager.registerUsageCallback(mNetworkTemplate, TYPE_MOBILE, budget,
+ mUsageCallback, mHandler);
+ mMultipathBudget = budget;
+ }
+
+ private void maybeUnregisterUsageCallback() {
+ if (haveMultipathBudget()) {
+ if (DBG) Slog.d(TAG, "Unregistering callback, budget was " + mMultipathBudget);
+ mStatsManager.unregisterUsageCallback(mUsageCallback);
+ mMultipathBudget = 0;
+ }
+ }
+
+ void shutdown() {
+ maybeUnregisterUsageCallback();
+ }
+ }
+
+ // Only ever updated on the handler thread. Accessed from other binder threads to retrieve
+ // the tracker for a specific network.
+ private final ConcurrentHashMap <Network, MultipathTracker> mMultipathTrackers =
+ new ConcurrentHashMap<>();
+
+ // TODO: this races with app code that might respond to onAvailable() by immediately calling
+ // getMultipathPreference. Fix this by adding to ConnectivityService the ability to directly
+ // invoke NetworkCallbacks on tightly-coupled classes such as this one which run on its
+ // handler thread.
+ private void registerTrackMobileCallback() {
+ final NetworkRequest request = new NetworkRequest.Builder()
+ .addCapability(NET_CAPABILITY_INTERNET)
+ .addTransportType(TRANSPORT_CELLULAR)
+ .build();
+ mMobileNetworkCallback = new ConnectivityManager.NetworkCallback() {
+ @Override
+ public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
+ MultipathTracker existing = mMultipathTrackers.get(network);
+ if (existing != null) {
+ existing.updateMultipathBudget();
+ return;
+ }
+
+ try {
+ mMultipathTrackers.put(network, new MultipathTracker(network, nc));
+ } catch (IllegalStateException e) {
+ Slog.e(TAG, "Can't track mobile network " + network + ": " + e.getMessage());
+ }
+ if (DBG) Slog.d(TAG, "Tracking mobile network " + network);
+ }
+
+ @Override
+ public void onLost(Network network) {
+ MultipathTracker existing = mMultipathTrackers.get(network);
+ if (existing != null) {
+ existing.shutdown();
+ mMultipathTrackers.remove(network);
+ }
+ if (DBG) Slog.d(TAG, "No longer tracking mobile network " + network);
+ }
+ };
+
+ mCM.registerNetworkCallback(request, mMobileNetworkCallback, mHandler);
+ }
+
+ private void maybeUnregisterTrackMobileCallback() {
+ if (mMobileNetworkCallback != null) {
+ mCM.unregisterNetworkCallback(mMobileNetworkCallback);
+ }
+ mMobileNetworkCallback = null;
+ }
+
+ private void registerNetworkPolicyListener() {
+ mPolicyListener = new NetworkPolicyManager.Listener() {
+ @Override
+ public void onMeteredIfacesChanged(String[] meteredIfaces) {
+ // Dispatched every time opportunistic quota is recalculated.
+ mHandler.post(() -> {
+ for (MultipathTracker t : mMultipathTrackers.values()) {
+ t.updateMultipathBudget();
+ }
+ });
+ }
+ };
+ mNPM.registerListener(mPolicyListener);
+ }
+
+ private void unregisterNetworkPolicyListener() {
+ mNPM.unregisterListener(mPolicyListener);
+ }
+
+ public void dump(IndentingPrintWriter pw) {
+ // Do not use in production. Access to class data is only safe on the handler thrad.
+ pw.println("MultipathPolicyTracker:");
+ pw.increaseIndent();
+ for (MultipathTracker t : mMultipathTrackers.values()) {
+ pw.println(String.format("Network %s: quota %d, budget %d. Preference: %s",
+ t.network, t.getQuota(), t.getMultipathBudget(),
+ DebugUtils.flagsToString(ConnectivityManager.class, "MULTIPATH_PREFERENCE_",
+ t.getMultipathPreference())));
+ }
+ pw.decreaseIndent();
+ }
+}
diff --git a/com/android/server/connectivity/NetworkAgentInfo.java b/com/android/server/connectivity/NetworkAgentInfo.java
index a4d72420..85b70ca0 100644
--- a/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/com/android/server/connectivity/NetworkAgentInfo.java
@@ -223,14 +223,6 @@ public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> {
// This represents the last score received from the NetworkAgent.
private int currentScore;
- // Penalty applied to scores of Networks that have not been validated.
- private static final int UNVALIDATED_SCORE_PENALTY = 40;
-
- // Score for explicitly connected network.
- //
- // This ensures that a) the explicitly selected network is never trumped by anything else, and
- // b) the explicitly selected network is never torn down.
- private static final int MAXIMUM_NETWORK_SCORE = 100;
// The list of NetworkRequests being satisfied by this Network.
private final SparseArray<NetworkRequest> mNetworkRequests = new SparseArray<>();
@@ -428,12 +420,12 @@ public class NetworkAgentInfo implements Comparable<NetworkAgentInfo> {
// down an explicitly selected network before the user gets a chance to prefer it when
// a higher-scoring network (e.g., Ethernet) is available.
if (networkMisc.explicitlySelected && (networkMisc.acceptUnvalidated || pretendValidated)) {
- return MAXIMUM_NETWORK_SCORE;
+ return ConnectivityConstants.MAXIMUM_NETWORK_SCORE;
}
int score = currentScore;
if (!lastValidated && !pretendValidated && !ignoreWifiUnvalidationPenalty()) {
- score -= UNVALIDATED_SCORE_PENALTY;
+ score -= ConnectivityConstants.UNVALIDATED_SCORE_PENALTY;
}
if (score < 0) score = 0;
return score;
diff --git a/com/android/server/connectivity/NetworkDiagnostics.java b/com/android/server/connectivity/NetworkDiagnostics.java
index 85d1d1ef..c471f0ca 100644
--- a/com/android/server/connectivity/NetworkDiagnostics.java
+++ b/com/android/server/connectivity/NetworkDiagnostics.java
@@ -24,6 +24,7 @@ import android.net.Network;
import android.net.NetworkUtils;
import android.net.RouteInfo;
import android.net.TrafficStats;
+import android.net.util.NetworkConstants;
import android.os.SystemClock;
import android.system.ErrnoException;
import android.system.Os;
@@ -421,8 +422,6 @@ public class NetworkDiagnostics {
private class IcmpCheck extends SimpleSocketCheck implements Runnable {
private static final int TIMEOUT_SEND = 100;
private static final int TIMEOUT_RECV = 300;
- private static final int ICMPV4_ECHO_REQUEST = 8;
- private static final int ICMPV6_ECHO_REQUEST = 128;
private static final int PACKET_BUFSIZE = 512;
private final int mProtocol;
private final int mIcmpType;
@@ -432,11 +431,11 @@ public class NetworkDiagnostics {
if (mAddressFamily == AF_INET6) {
mProtocol = IPPROTO_ICMPV6;
- mIcmpType = ICMPV6_ECHO_REQUEST;
+ mIcmpType = NetworkConstants.ICMPV6_ECHO_REQUEST_TYPE;
mMeasurement.description = "ICMPv6";
} else {
mProtocol = IPPROTO_ICMP;
- mIcmpType = ICMPV4_ECHO_REQUEST;
+ mIcmpType = NetworkConstants.ICMPV4_ECHO_REQUEST_TYPE;
mMeasurement.description = "ICMPv4";
}
@@ -504,7 +503,6 @@ public class NetworkDiagnostics {
private class DnsUdpCheck extends SimpleSocketCheck implements Runnable {
private static final int TIMEOUT_SEND = 100;
private static final int TIMEOUT_RECV = 500;
- private static final int DNS_SERVER_PORT = 53;
private static final int RR_TYPE_A = 1;
private static final int RR_TYPE_AAAA = 28;
private static final int PACKET_BUFSIZE = 512;
@@ -546,7 +544,8 @@ public class NetworkDiagnostics {
}
try {
- setupSocket(SOCK_DGRAM, IPPROTO_UDP, TIMEOUT_SEND, TIMEOUT_RECV, DNS_SERVER_PORT);
+ setupSocket(SOCK_DGRAM, IPPROTO_UDP, TIMEOUT_SEND, TIMEOUT_RECV,
+ NetworkConstants.DNS_SERVER_PORT);
} catch (ErrnoException | IOException e) {
mMeasurement.recordFailure(e.toString());
return;
diff --git a/com/android/server/connectivity/NetworkMonitor.java b/com/android/server/connectivity/NetworkMonitor.java
index 76840302..8a2e71c1 100644
--- a/com/android/server/connectivity/NetworkMonitor.java
+++ b/com/android/server/connectivity/NetworkMonitor.java
@@ -29,6 +29,7 @@ import android.net.CaptivePortal;
import android.net.ConnectivityManager;
import android.net.ICaptivePortal;
import android.net.Network;
+import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.ProxyInfo;
import android.net.TrafficStats;
@@ -121,22 +122,6 @@ public class NetworkMonitor extends StateMachine {
}
}
- public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
- "android.net.conn.NETWORK_CONDITIONS_MEASURED";
- public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
- public static final String EXTRA_NETWORK_TYPE = "extra_network_type";
- public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received";
- public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal";
- public static final String EXTRA_CELL_ID = "extra_cellid";
- public static final String EXTRA_SSID = "extra_ssid";
- public static final String EXTRA_BSSID = "extra_bssid";
- /** real time since boot */
- public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms";
- public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms";
-
- private static final String PERMISSION_ACCESS_NETWORK_CONDITIONS =
- "android.permission.ACCESS_NETWORK_CONDITIONS";
-
// After a network has been tested this result can be sent with EVENT_NETWORK_TESTED.
// The network should be used as a default internet connection. It was found to be:
// 1. a functioning network providing internet access, or
@@ -215,6 +200,15 @@ public class NetworkMonitor extends StateMachine {
*/
private static final int CMD_CAPTIVE_PORTAL_RECHECK = BASE + 12;
+ /**
+ * ConnectivityService notifies NetworkMonitor of settings changes to
+ * Private DNS. If a DNS resolution is required, e.g. for DNS-over-TLS in
+ * strict mode, then an event is sent back to ConnectivityService with the
+ * result of the resolution attempt.
+ */
+ private static final int CMD_PRIVATE_DNS_SETTINGS_CHANGED = BASE + 13;
+ public static final int EVENT_PRIVATE_DNS_CONFIG_RESOLVED = BASE + 14;
+
// Start mReevaluateDelayMs at this value and double.
private static final int INITIAL_REEVALUATE_DELAY_MS = 1000;
private static final int MAX_REEVALUATE_DELAY_MS = 10*60*1000;
@@ -230,6 +224,12 @@ public class NetworkMonitor extends StateMachine {
private static final int NUM_VALIDATION_LOG_LINES = 20;
+ public static boolean isValidationRequired(
+ NetworkCapabilities dfltNetCap, NetworkCapabilities nc) {
+ // TODO: Consider requiring validation for DUN networks.
+ return dfltNetCap.satisfiedByNetworkCapabilities(nc);
+ }
+
private final Context mContext;
private final Handler mConnectivityServiceHandler;
private final NetworkAgentInfo mNetworkAgentInfo;
@@ -261,6 +261,8 @@ public class NetworkMonitor extends StateMachine {
public boolean systemReady = false;
+ private DnsManager.PrivateDnsConfig mPrivateDnsCfg = null;
+
private final State mDefaultState = new DefaultState();
private final State mValidatedState = new ValidatedState();
private final State mMaybeNotifyState = new MaybeNotifyState();
@@ -342,6 +344,11 @@ public class NetworkMonitor extends StateMachine {
return 0 == mValidations ? ValidationStage.FIRST_VALIDATION : ValidationStage.REVALIDATION;
}
+ private boolean isValidationRequired() {
+ return isValidationRequired(
+ mDefaultRequest.networkCapabilities, mNetworkAgentInfo.networkCapabilities);
+ }
+
// DefaultState is the parent of all States. It exists only to handle CMD_* messages but
// does not entail any real state (hence no enter() or exit() routines).
private class DefaultState extends State {
@@ -405,6 +412,18 @@ public class NetworkMonitor extends StateMachine {
break;
}
return HANDLED;
+ case CMD_PRIVATE_DNS_SETTINGS_CHANGED:
+ if (isValidationRequired()) {
+ // This performs a blocking DNS resolution of the
+ // strict mode hostname, if required.
+ resolvePrivateDnsConfig((DnsManager.PrivateDnsConfig) message.obj);
+ if ((mPrivateDnsCfg != null) && mPrivateDnsCfg.inStrictMode()) {
+ mConnectivityServiceHandler.sendMessage(obtainMessage(
+ EVENT_PRIVATE_DNS_CONFIG_RESOLVED, 0, mNetId,
+ new DnsManager.PrivateDnsConfig(mPrivateDnsCfg)));
+ }
+ }
+ return HANDLED;
default:
return HANDLED;
}
@@ -421,7 +440,7 @@ public class NetworkMonitor extends StateMachine {
maybeLogEvaluationResult(
networkEventType(validationStage(), EvaluationResult.VALIDATED));
mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
- NETWORK_TEST_RESULT_VALID, mNetId, null));
+ NETWORK_TEST_RESULT_VALID, mNetId, mPrivateDnsCfg));
mValidations++;
}
@@ -567,9 +586,9 @@ public class NetworkMonitor extends StateMachine {
// the network so don't bother validating here. Furthermore sending HTTP
// packets over the network may be undesirable, for example an extremely
// expensive metered network, or unwanted leaking of the User Agent string.
- if (!mDefaultRequest.networkCapabilities.satisfiedByNetworkCapabilities(
- mNetworkAgentInfo.networkCapabilities)) {
+ if (!isValidationRequired()) {
validationLog("Network would not satisfy default request, not validating");
+ mPrivateDnsCfg = null;
transitionTo(mValidatedState);
return HANDLED;
}
@@ -582,6 +601,7 @@ public class NetworkMonitor extends StateMachine {
// if this is found to cause problems.
CaptivePortalProbeResult probeResult = isCaptivePortal();
if (probeResult.isSuccessful()) {
+ resolvePrivateDnsConfig();
transitionTo(mValidatedState);
} else if (probeResult.isPortal()) {
mConnectivityServiceHandler.sendMessage(obtainMessage(EVENT_NETWORK_TESTED,
@@ -1045,6 +1065,44 @@ public class NetworkMonitor extends StateMachine {
return null;
}
+ public void notifyPrivateDnsSettingsChanged(DnsManager.PrivateDnsConfig newCfg) {
+ // Cancel any outstanding resolutions.
+ removeMessages(CMD_PRIVATE_DNS_SETTINGS_CHANGED);
+ // Send the update to the proper thread.
+ sendMessage(CMD_PRIVATE_DNS_SETTINGS_CHANGED, newCfg);
+ }
+
+ private void resolvePrivateDnsConfig() {
+ resolvePrivateDnsConfig(DnsManager.getPrivateDnsConfig(mContext.getContentResolver()));
+ }
+
+ private void resolvePrivateDnsConfig(DnsManager.PrivateDnsConfig cfg) {
+ // Nothing to do.
+ if (cfg == null) {
+ mPrivateDnsCfg = null;
+ return;
+ }
+
+ // No DNS resolution required.
+ if (!cfg.inStrictMode()) {
+ mPrivateDnsCfg = cfg;
+ return;
+ }
+
+ if ((mPrivateDnsCfg != null) && mPrivateDnsCfg.inStrictMode() &&
+ (mPrivateDnsCfg.ips.length > 0) && mPrivateDnsCfg.hostname.equals(cfg.hostname)) {
+ // We have already resolved this strict mode hostname. Assume that
+ // Private DNS services won't be changing serving IP addresses very
+ // frequently and save ourselves one re-resolve.
+ return;
+ }
+
+ mPrivateDnsCfg = cfg;
+ final DnsManager.PrivateDnsConfig resolvedCfg = DnsManager.tryBlockingResolveOf(
+ mNetwork, mPrivateDnsCfg.hostname);
+ if (resolvedCfg != null) mPrivateDnsCfg = resolvedCfg;
+ }
+
/**
* @param responseReceived - whether or not we received a valid HTTP response to our request.
* If false, isCaptivePortal and responseTimestampMs are ignored
@@ -1062,7 +1120,8 @@ public class NetworkMonitor extends StateMachine {
return;
}
- Intent latencyBroadcast = new Intent(ACTION_NETWORK_CONDITIONS_MEASURED);
+ Intent latencyBroadcast =
+ new Intent(ConnectivityConstants.ACTION_NETWORK_CONDITIONS_MEASURED);
switch (mNetworkAgentInfo.networkInfo.getType()) {
case ConnectivityManager.TYPE_WIFI:
WifiInfo currentWifiInfo = mWifiManager.getConnectionInfo();
@@ -1074,15 +1133,18 @@ public class NetworkMonitor extends StateMachine {
// not change it here as it would become impossible to tell whether the SSID is
// simply being surrounded by quotes due to the API, or whether those quotes
// are actually part of the SSID.
- latencyBroadcast.putExtra(EXTRA_SSID, currentWifiInfo.getSSID());
- latencyBroadcast.putExtra(EXTRA_BSSID, currentWifiInfo.getBSSID());
+ latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_SSID,
+ currentWifiInfo.getSSID());
+ latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_BSSID,
+ currentWifiInfo.getBSSID());
} else {
if (VDBG) logw("network info is TYPE_WIFI but no ConnectionInfo found");
return;
}
break;
case ConnectivityManager.TYPE_MOBILE:
- latencyBroadcast.putExtra(EXTRA_NETWORK_TYPE, mTelephonyManager.getNetworkType());
+ latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_NETWORK_TYPE,
+ mTelephonyManager.getNetworkType());
List<CellInfo> info = mTelephonyManager.getAllCellInfo();
if (info == null) return;
int numRegisteredCellInfo = 0;
@@ -1096,16 +1158,16 @@ public class NetworkMonitor extends StateMachine {
}
if (cellInfo instanceof CellInfoCdma) {
CellIdentityCdma cellId = ((CellInfoCdma) cellInfo).getCellIdentity();
- latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
+ latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CELL_ID, cellId);
} else if (cellInfo instanceof CellInfoGsm) {
CellIdentityGsm cellId = ((CellInfoGsm) cellInfo).getCellIdentity();
- latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
+ latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CELL_ID, cellId);
} else if (cellInfo instanceof CellInfoLte) {
CellIdentityLte cellId = ((CellInfoLte) cellInfo).getCellIdentity();
- latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
+ latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CELL_ID, cellId);
} else if (cellInfo instanceof CellInfoWcdma) {
CellIdentityWcdma cellId = ((CellInfoWcdma) cellInfo).getCellIdentity();
- latencyBroadcast.putExtra(EXTRA_CELL_ID, cellId);
+ latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CELL_ID, cellId);
} else {
if (VDBG) logw("Registered cellinfo is unrecognized");
return;
@@ -1116,16 +1178,21 @@ public class NetworkMonitor extends StateMachine {
default:
return;
}
- latencyBroadcast.putExtra(EXTRA_CONNECTIVITY_TYPE, mNetworkAgentInfo.networkInfo.getType());
- latencyBroadcast.putExtra(EXTRA_RESPONSE_RECEIVED, responseReceived);
- latencyBroadcast.putExtra(EXTRA_REQUEST_TIMESTAMP_MS, requestTimestampMs);
+ latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_CONNECTIVITY_TYPE,
+ mNetworkAgentInfo.networkInfo.getType());
+ latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_RESPONSE_RECEIVED,
+ responseReceived);
+ latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_REQUEST_TIMESTAMP_MS,
+ requestTimestampMs);
if (responseReceived) {
- latencyBroadcast.putExtra(EXTRA_IS_CAPTIVE_PORTAL, isCaptivePortal);
- latencyBroadcast.putExtra(EXTRA_RESPONSE_TIMESTAMP_MS, responseTimestampMs);
+ latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_IS_CAPTIVE_PORTAL,
+ isCaptivePortal);
+ latencyBroadcast.putExtra(ConnectivityConstants.EXTRA_RESPONSE_TIMESTAMP_MS,
+ responseTimestampMs);
}
mContext.sendBroadcastAsUser(latencyBroadcast, UserHandle.CURRENT,
- PERMISSION_ACCESS_NETWORK_CONDITIONS);
+ ConnectivityConstants.PERMISSION_ACCESS_NETWORK_CONDITIONS);
}
private void logNetworkEvent(int evtype) {
diff --git a/com/android/server/connectivity/PacManager.java b/com/android/server/connectivity/PacManager.java
index d56fb1ab..3a27fcb3 100644
--- a/com/android/server/connectivity/PacManager.java
+++ b/com/android/server/connectivity/PacManager.java
@@ -54,12 +54,12 @@ import java.net.URLConnection;
* @hide
*/
public class PacManager {
- public static final String PAC_PACKAGE = "com.android.pacprocessor";
- public static final String PAC_SERVICE = "com.android.pacprocessor.PacService";
- public static final String PAC_SERVICE_NAME = "com.android.net.IProxyService";
+ private static final String PAC_PACKAGE = "com.android.pacprocessor";
+ private static final String PAC_SERVICE = "com.android.pacprocessor.PacService";
+ private static final String PAC_SERVICE_NAME = "com.android.net.IProxyService";
- public static final String PROXY_PACKAGE = "com.android.proxyhandler";
- public static final String PROXY_SERVICE = "com.android.proxyhandler.ProxyService";
+ private static final String PROXY_PACKAGE = "com.android.proxyhandler";
+ private static final String PROXY_SERVICE = "com.android.proxyhandler.ProxyService";
private static final String TAG = "PacManager";
@@ -71,8 +71,6 @@ public class PacManager {
private static final int DELAY_LONG = 4;
private static final long MAX_PAC_SIZE = 20 * 1000 * 1000;
- /** Keep these values up-to-date with ProxyService.java */
- public static final String KEY_PROXY = "keyProxy";
private String mCurrentPac;
@GuardedBy("mProxyLock")
private volatile Uri mPacUrl = Uri.EMPTY;
diff --git a/com/android/server/connectivity/Tethering.java b/com/android/server/connectivity/Tethering.java
index 59870cb9..be6c4a12 100644
--- a/com/android/server/connectivity/Tethering.java
+++ b/com/android/server/connectivity/Tethering.java
@@ -19,7 +19,6 @@ package com.android.server.connectivity;
import static android.hardware.usb.UsbManager.USB_CONFIGURED;
import static android.hardware.usb.UsbManager.USB_CONNECTED;
import static android.hardware.usb.UsbManager.USB_FUNCTION_RNDIS;
-import static android.net.ConnectivityManager.getNetworkTypeName;
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_MODE;
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_STATE;
@@ -43,7 +42,6 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.hardware.usb.UsbManager;
import android.net.ConnectivityManager;
@@ -53,9 +51,7 @@ import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.Network;
-import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
-import android.net.NetworkRequest;
import android.net.NetworkState;
import android.net.NetworkUtils;
import android.net.RouteInfo;
@@ -79,7 +75,6 @@ import android.os.UserManagerInternal;
import android.os.UserManagerInternal.UserRestrictionsListener;
import android.provider.Settings;
import android.telephony.CarrierConfigManager;
-import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
@@ -88,8 +83,6 @@ import android.util.SparseArray;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.notification.SystemNotificationChannels;
-import com.android.internal.telephony.IccCardConstants;
-import com.android.internal.telephony.TelephonyIntents;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.MessageUtils;
@@ -114,12 +107,8 @@ import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.HashMap;
import java.util.HashSet;
-import java.util.Iterator;
-import java.util.Map;
import java.util.Set;
-import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -197,8 +186,6 @@ public class Tethering extends BaseNetworkObserver {
private int mLastNotificationId;
private boolean mRndisEnabled; // track the RNDIS function enabled state
- private boolean mUsbTetherRequested; // true if USB tethering should be started
- // when RNDIS is enabled
// True iff. WiFi tethering should be started when soft AP is ready.
private boolean mWifiTetherRequested;
@@ -519,6 +506,7 @@ public class Tethering extends BaseNetworkObserver {
Intent intent = new Intent(Settings.ACTION_TETHER_PROVISIONING);
intent.putExtra(ConnectivityManager.EXTRA_ADD_TETHER_TYPE, type);
intent.putExtra(ConnectivityManager.EXTRA_PROVISION_CALLBACK, receiver);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
final long ident = Binder.clearCallingIdentity();
try {
mContext.startActivityAsUser(intent, UserHandle.CURRENT);
@@ -891,33 +879,18 @@ public class Tethering extends BaseNetworkObserver {
//
// For more explanation, see b/62552150 .
synchronized (Tethering.this.mPublicSync) {
- // Always record the state of RNDIS.
- // TODO: consider:
- // final boolean disconnected = !usbConnected;
- // if (disconnected) {
- // mRndisEnabled = false;
- // mUsbTetherRequested = false;
- // return;
- // }
- // final boolean configured = usbConnected && usbConfigured;
- // mRndisEnabled = configured ? rndisEnabled : false;
- // if (!configured) return;
- mRndisEnabled = rndisEnabled;
-
- if (usbConnected && !usbConfigured) {
- // Nothing to do here (only CONNECTED, not yet CONFIGURED).
- return;
- }
-
- // start tethering if we have a request pending
- if (usbConfigured && mRndisEnabled && mUsbTetherRequested) {
+ if (!usbConnected && mRndisEnabled) {
+ // Turn off tethering if it was enabled and there is a disconnect.
+ tetherMatchingInterfaces(
+ IControlsTethering.STATE_AVAILABLE,
+ ConnectivityManager.TETHERING_USB);
+ } else if (usbConfigured && rndisEnabled) {
+ // Tether if rndis is enabled and usb is configured.
tetherMatchingInterfaces(
IControlsTethering.STATE_TETHERED,
ConnectivityManager.TETHERING_USB);
}
-
- // TODO: Figure out how to remove the need for this variable.
- mUsbTetherRequested = false;
+ mRndisEnabled = usbConfigured && rndisEnabled;
}
}
@@ -1121,34 +1094,8 @@ public class Tethering extends BaseNetworkObserver {
public int setUsbTethering(boolean enable) {
if (VDBG) Log.d(TAG, "setUsbTethering(" + enable + ")");
UsbManager usbManager = (UsbManager) mContext.getSystemService(Context.USB_SERVICE);
-
synchronized (mPublicSync) {
- if (enable) {
- if (mRndisEnabled) {
- final long ident = Binder.clearCallingIdentity();
- try {
- tetherMatchingInterfaces(IControlsTethering.STATE_TETHERED,
- ConnectivityManager.TETHERING_USB);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- } else {
- mUsbTetherRequested = true;
- usbManager.setCurrentFunction(UsbManager.USB_FUNCTION_RNDIS, false);
- }
- } else {
- final long ident = Binder.clearCallingIdentity();
- try {
- tetherMatchingInterfaces(IControlsTethering.STATE_AVAILABLE,
- ConnectivityManager.TETHERING_USB);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- if (mRndisEnabled) {
- usbManager.setCurrentFunction(null, false);
- }
- mUsbTetherRequested = false;
- }
+ usbManager.setCurrentFunction(enable ? UsbManager.USB_FUNCTION_RNDIS : null, false);
}
return ConnectivityManager.TETHER_ERROR_NO_ERROR;
}
@@ -1205,7 +1152,7 @@ public class Tethering extends BaseNetworkObserver {
if (!mForwardedDownstreams.isEmpty()) return true;
synchronized (mPublicSync) {
- return mUsbTetherRequested || mWifiTetherRequested;
+ return mWifiTetherRequested;
}
}
diff --git a/com/android/server/connectivity/Vpn.java b/com/android/server/connectivity/Vpn.java
index 7715727f..bb46d5e2 100644
--- a/com/android/server/connectivity/Vpn.java
+++ b/com/android/server/connectivity/Vpn.java
@@ -18,6 +18,7 @@ package com.android.server.connectivity;
import static android.Manifest.permission.BIND_VPN_SERVICE;
import static android.net.ConnectivityManager.NETID_UNSET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
import static android.net.RouteInfo.RTN_THROW;
@@ -128,7 +129,7 @@ public class Vpn {
// Length of time (in milliseconds) that an app hosting an always-on VPN is placed on
// the device idle whitelist during service launch and VPN bootstrap.
- private static final long VPN_LAUNCH_IDLE_WHITELIST_DURATION = 60 * 1000;
+ private static final long VPN_LAUNCH_IDLE_WHITELIST_DURATION_MS = 60 * 1000;
// TODO: create separate trackers for each unique VPN to support
// automated reconnection
@@ -163,19 +164,6 @@ public class Vpn {
private boolean mLockdown = false;
/**
- * List of UIDs that are set to use this VPN by default. Normally, every UID in the user is
- * added to this set but that can be changed by adding allowed or disallowed applications. It
- * is non-null iff the VPN is connected.
- *
- * Unless the VPN has set allowBypass=true, these UIDs are forced into the VPN.
- *
- * @see VpnService.Builder#addAllowedApplication(String)
- * @see VpnService.Builder#addDisallowedApplication(String)
- */
- @GuardedBy("this")
- private Set<UidRange> mVpnUsers = null;
-
- /**
* List of UIDs for which networking should be blocked until VPN is ready, during brief periods
* when VPN is not running. For example, during system startup or after a crash.
* @see mLockdown
@@ -183,10 +171,10 @@ public class Vpn {
@GuardedBy("this")
private Set<UidRange> mBlockedUsers = new ArraySet<>();
- // Handle of user initiating VPN.
+ // Handle of the user initiating VPN.
private final int mUserHandle;
- // Listen to package remove and change event in this user
+ // Listen to package removal and change events (update/uninstall) for this user
private final BroadcastReceiver mPackageIntentReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
@@ -197,14 +185,14 @@ public class Vpn {
}
synchronized (Vpn.this) {
- // Avoid race that always-on package has been unset
+ // Avoid race where always-on package has been unset
if (!packageName.equals(getAlwaysOnPackage())) {
return;
}
final String action = intent.getAction();
- Log.i(TAG, "Received broadcast " + action + " for always-on package " + packageName
- + " in user " + mUserHandle);
+ Log.i(TAG, "Received broadcast " + action + " for always-on VPN package "
+ + packageName + " in user " + mUserHandle);
switch(action) {
case Intent.ACTION_PACKAGE_REPLACED:
@@ -248,7 +236,8 @@ public class Vpn {
Log.wtf(TAG, "Problem registering observer", e);
}
- mNetworkInfo = new NetworkInfo(ConnectivityManager.TYPE_VPN, 0, NETWORKTYPE, "");
+ mNetworkInfo = new NetworkInfo(ConnectivityManager.TYPE_VPN, 0 /* subtype */, NETWORKTYPE,
+ "" /* subtypeName */);
mNetworkCapabilities = new NetworkCapabilities();
mNetworkCapabilities.addTransportType(NetworkCapabilities.TRANSPORT_VPN);
mNetworkCapabilities.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN);
@@ -258,7 +247,7 @@ public class Vpn {
}
/**
- * Set if this object is responsible for watching for {@link NetworkInfo}
+ * Set whether this object is responsible for watching for {@link NetworkInfo}
* teardown. When {@code false}, teardown is handled externally by someone
* else.
*/
@@ -297,14 +286,17 @@ public class Vpn {
int upKbps = NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED;
boolean metered = false;
boolean roaming = false;
+ boolean congested = false;
if (ArrayUtils.isEmpty(underlyingNetworks)) {
// No idea what the underlying networks are; assume sane defaults
metered = true;
roaming = false;
+ congested = false;
} else {
for (Network underlying : underlyingNetworks) {
final NetworkCapabilities underlyingCaps = cm.getNetworkCapabilities(underlying);
+ if (underlyingCaps == null) continue;
for (int underlyingType : underlyingCaps.getTransportTypes()) {
transportTypes = ArrayUtils.appendInt(transportTypes, underlyingType);
}
@@ -317,22 +309,16 @@ public class Vpn {
underlyingCaps.getLinkUpstreamBandwidthKbps());
metered |= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_METERED);
roaming |= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_ROAMING);
+ congested |= !underlyingCaps.hasCapability(NET_CAPABILITY_NOT_CONGESTED);
}
}
caps.setTransportTypes(transportTypes);
caps.setLinkDownstreamBandwidthKbps(downKbps);
caps.setLinkUpstreamBandwidthKbps(upKbps);
- if (metered) {
- caps.removeCapability(NET_CAPABILITY_NOT_METERED);
- } else {
- caps.addCapability(NET_CAPABILITY_NOT_METERED);
- }
- if (roaming) {
- caps.removeCapability(NET_CAPABILITY_NOT_ROAMING);
- } else {
- caps.addCapability(NET_CAPABILITY_NOT_ROAMING);
- }
+ caps.setCapability(NET_CAPABILITY_NOT_METERED, !metered);
+ caps.setCapability(NET_CAPABILITY_NOT_ROAMING, !roaming);
+ caps.setCapability(NET_CAPABILITY_NOT_CONGESTED, !congested);
}
/**
@@ -480,7 +466,6 @@ public class Vpn {
}
private void unregisterPackageChangeReceiverLocked() {
- // register previous intent filter
if (mIsPackageIntentReceiverRegistered) {
mContext.unregisterReceiver(mPackageIntentReceiver);
mIsPackageIntentReceiverRegistered = false;
@@ -581,7 +566,7 @@ public class Vpn {
DeviceIdleController.LocalService idleController =
LocalServices.getService(DeviceIdleController.LocalService.class);
idleController.addPowerSaveTempWhitelistApp(Process.myUid(), alwaysOnPackage,
- VPN_LAUNCH_IDLE_WHITELIST_DURATION, mUserHandle, false, "vpn");
+ VPN_LAUNCH_IDLE_WHITELIST_DURATION_MS, mUserHandle, false, "vpn");
// Start the VPN service declared in the app's manifest.
Intent serviceIntent = new Intent(VpnConfig.SERVICE_INTERFACE);
@@ -611,9 +596,10 @@ public class Vpn {
* It uses {@link VpnConfig#LEGACY_VPN} as its package name, and
* it can be revoked by itself.
*
- * Note: when we added VPN pre-consent in http://ag/522961 the names oldPackage
- * and newPackage become misleading, because when an app is pre-consented, we
- * actually prepare oldPackage, not newPackage.
+ * Note: when we added VPN pre-consent in
+ * https://android.googlesource.com/platform/frameworks/base/+/0554260
+ * the names oldPackage and newPackage became misleading, because when
+ * an app is pre-consented, we actually prepare oldPackage, not newPackage.
*
* Their meanings actually are:
*
@@ -629,7 +615,7 @@ public class Vpn {
* @param oldPackage The package name of the old VPN application
* @param newPackage The package name of the new VPN application
*
- * @return true if the operation is succeeded.
+ * @return true if the operation succeeded.
*/
public synchronized boolean prepare(String oldPackage, String newPackage) {
if (oldPackage != null) {
@@ -638,7 +624,7 @@ public class Vpn {
return false;
}
- // Package is not same or old package was reinstalled.
+ // Package is not the same or old package was reinstalled.
if (!isCurrentPreparedPackage(oldPackage)) {
// The package doesn't match. We return false (to obtain user consent) unless the
// user has already consented to that VPN package.
@@ -689,7 +675,7 @@ public class Vpn {
agentDisconnect();
jniReset(mInterface);
mInterface = null;
- mVpnUsers = null;
+ mNetworkCapabilities.setUids(null);
}
// Revoke the connection or stop LegacyVpnRunner.
@@ -858,10 +844,14 @@ public class Vpn {
NetworkMisc networkMisc = new NetworkMisc();
networkMisc.allowBypass = mConfig.allowBypass && !mLockdown;
+ mNetworkCapabilities.setEstablishingVpnAppUid(Binder.getCallingUid());
+ mNetworkCapabilities.setUids(createUserAndRestrictedProfilesRanges(mUserHandle,
+ mConfig.allowedApplications, mConfig.disallowedApplications));
long token = Binder.clearCallingIdentity();
try {
- mNetworkAgent = new NetworkAgent(mLooper, mContext, NETWORKTYPE,
- mNetworkInfo, mNetworkCapabilities, lp, 0, networkMisc) {
+ mNetworkAgent = new NetworkAgent(mLooper, mContext, NETWORKTYPE /* logtag */,
+ mNetworkInfo, mNetworkCapabilities, lp,
+ ConnectivityConstants.VPN_DEFAULT_SCORE, networkMisc) {
@Override
public void unwanted() {
// We are user controlled, not driven by NetworkRequest.
@@ -870,11 +860,6 @@ public class Vpn {
} finally {
Binder.restoreCallingIdentity(token);
}
-
- mVpnUsers = createUserAndRestrictedProfilesRanges(mUserHandle,
- mConfig.allowedApplications, mConfig.disallowedApplications);
- mNetworkAgent.addUidRanges(mVpnUsers.toArray(new UidRange[mVpnUsers.size()]));
-
mNetworkInfo.setIsAvailable(true);
updateState(DetailedState.CONNECTED, "agentConnect");
}
@@ -935,7 +920,7 @@ public class Vpn {
}
ResolveInfo info = AppGlobals.getPackageManager().resolveService(intent,
- null, 0, mUserHandle);
+ null, 0, mUserHandle);
if (info == null) {
throw new SecurityException("Cannot find " + config.user);
}
@@ -943,7 +928,7 @@ public class Vpn {
throw new SecurityException(config.user + " does not require " + BIND_VPN_SERVICE);
}
} catch (RemoteException e) {
- throw new SecurityException("Cannot find " + config.user);
+ throw new SecurityException("Cannot find " + config.user);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -954,7 +939,7 @@ public class Vpn {
Connection oldConnection = mConnection;
NetworkAgent oldNetworkAgent = mNetworkAgent;
mNetworkAgent = null;
- Set<UidRange> oldUsers = mVpnUsers;
+ Set<UidRange> oldUsers = mNetworkCapabilities.getUids();
// Configure the interface. Abort if any of these steps fails.
ParcelFileDescriptor tun = ParcelFileDescriptor.adoptFd(jniCreate(config.mtu));
@@ -1012,7 +997,7 @@ public class Vpn {
// restore old state
mConfig = oldConfig;
mConnection = oldConnection;
- mVpnUsers = oldUsers;
+ mNetworkCapabilities.setUids(oldUsers);
mNetworkAgent = oldNetworkAgent;
mInterface = oldInterface;
throw e;
@@ -1132,10 +1117,12 @@ public class Vpn {
// Returns the subset of the full list of active UID ranges the VPN applies to (mVpnUsers) that
// apply to userHandle.
- private List<UidRange> uidRangesForUser(int userHandle) {
+ static private List<UidRange> uidRangesForUser(int userHandle, Set<UidRange> existingRanges) {
+ // UidRange#createForUser returns the entire range of UIDs available to a macro-user.
+ // This is something like 0-99999 ; {@see UserHandle#PER_USER_RANGE}
final UidRange userRange = UidRange.createForUser(userHandle);
final List<UidRange> ranges = new ArrayList<UidRange>();
- for (UidRange range : mVpnUsers) {
+ for (UidRange range : existingRanges) {
if (userRange.containsRange(range)) {
ranges.add(range);
}
@@ -1143,30 +1130,18 @@ public class Vpn {
return ranges;
}
- private void removeVpnUserLocked(int userHandle) {
- if (mVpnUsers == null) {
- throw new IllegalStateException("VPN is not active");
- }
- final List<UidRange> ranges = uidRangesForUser(userHandle);
- if (mNetworkAgent != null) {
- mNetworkAgent.removeUidRanges(ranges.toArray(new UidRange[ranges.size()]));
- }
- mVpnUsers.removeAll(ranges);
- }
-
public void onUserAdded(int userHandle) {
// If the user is restricted tie them to the parent user's VPN
UserInfo user = UserManager.get(mContext).getUserInfo(userHandle);
if (user.isRestricted() && user.restrictedProfileParentId == mUserHandle) {
synchronized(Vpn.this) {
- if (mVpnUsers != null) {
+ final Set<UidRange> existingRanges = mNetworkCapabilities.getUids();
+ if (existingRanges != null) {
try {
- addUserToRanges(mVpnUsers, userHandle, mConfig.allowedApplications,
+ addUserToRanges(existingRanges, userHandle, mConfig.allowedApplications,
mConfig.disallowedApplications);
- if (mNetworkAgent != null) {
- final List<UidRange> ranges = uidRangesForUser(userHandle);
- mNetworkAgent.addUidRanges(ranges.toArray(new UidRange[ranges.size()]));
- }
+ mNetworkCapabilities.setUids(existingRanges);
+ updateCapabilities();
} catch (Exception e) {
Log.wtf(TAG, "Failed to add restricted user to owner", e);
}
@@ -1181,9 +1156,14 @@ public class Vpn {
UserInfo user = UserManager.get(mContext).getUserInfo(userHandle);
if (user.isRestricted() && user.restrictedProfileParentId == mUserHandle) {
synchronized(Vpn.this) {
- if (mVpnUsers != null) {
+ final Set<UidRange> existingRanges = mNetworkCapabilities.getUids();
+ if (existingRanges != null) {
try {
- removeVpnUserLocked(userHandle);
+ final List<UidRange> removedRanges =
+ uidRangesForUser(userHandle, existingRanges);
+ existingRanges.removeAll(removedRanges);
+ mNetworkCapabilities.setUids(existingRanges);
+ updateCapabilities();
} catch (Exception e) {
Log.wtf(TAG, "Failed to remove restricted user to owner", e);
}
@@ -1227,15 +1207,6 @@ public class Vpn {
private void setVpnForcedLocked(boolean enforce) {
final List<String> exemptedPackages =
isNullOrLegacyVpn(mPackage) ? null : Collections.singletonList(mPackage);
- setVpnForcedWithExemptionsLocked(enforce, exemptedPackages);
- }
-
- /**
- * @see #setVpnForcedLocked
- */
- @GuardedBy("this")
- private void setVpnForcedWithExemptionsLocked(boolean enforce,
- @Nullable List<String> exemptedPackages) {
final Set<UidRange> removedRanges = new ArraySet<>(mBlockedUsers);
Set<UidRange> addedRanges = Collections.emptySet();
@@ -1315,7 +1286,7 @@ public class Vpn {
synchronized (Vpn.this) {
if (interfaze.equals(mInterface) && jniCheck(interfaze) == 0) {
mStatusIntent = null;
- mVpnUsers = null;
+ mNetworkCapabilities.setUids(null);
mConfig = null;
mInterface = null;
if (mConnection != null) {
@@ -1336,7 +1307,7 @@ public class Vpn {
}
private void enforceControlPermissionOrInternalCaller() {
- // Require caller to be either an application with CONTROL_VPN permission or a process
+ // Require the caller to be either an application with CONTROL_VPN permission or a process
// in the system server.
mContext.enforceCallingOrSelfPermission(Manifest.permission.CONTROL_VPN,
"Unauthorized Caller");
@@ -1416,7 +1387,7 @@ public class Vpn {
}
/**
- * This method should only be called by ConnectivityService. Because it doesn't
+ * This method should only be called by ConnectivityService because it doesn't
* have enough data to fill VpnInfo.primaryUnderlyingIface field.
*/
public synchronized VpnInfo getVpnInfo() {
@@ -1434,12 +1405,7 @@ public class Vpn {
if (!isRunningLocked()) {
return false;
}
- for (UidRange uidRange : mVpnUsers) {
- if (uidRange.contains(uid)) {
- return true;
- }
- }
- return false;
+ return mNetworkCapabilities.appliesToUid(uid);
}
/**
@@ -1767,7 +1733,7 @@ public class Vpn {
* Bringing up a VPN connection takes time, and that is all this thread
* does. Here we have plenty of time. The only thing we need to take
* care of is responding to interruptions as soon as possible. Otherwise
- * requests will be piled up. This can be done in a Handler as a state
+ * requests will pile up. This could be done in a Handler as a state
* machine, but it is much easier to read in the current form.
*/
private class LegacyVpnRunner extends Thread {
@@ -1780,7 +1746,7 @@ public class Vpn {
private final AtomicInteger mOuterConnection =
new AtomicInteger(ConnectivityManager.TYPE_NONE);
- private long mTimer = -1;
+ private long mBringupStartTime = -1;
/**
* Watch for the outer connection (passing in the constructor) going away.
@@ -1860,8 +1826,8 @@ public class Vpn {
synchronized (TAG) {
Log.v(TAG, "Executing");
try {
- execute();
- monitorDaemons();
+ bringup();
+ waitForDaemonsToStop();
interrupted(); // Clear interrupt flag if execute called exit.
} catch (InterruptedException e) {
} finally {
@@ -1882,30 +1848,27 @@ public class Vpn {
}
}
- private void checkpoint(boolean yield) throws InterruptedException {
+ private void checkInterruptAndDelay(boolean sleepLonger) throws InterruptedException {
long now = SystemClock.elapsedRealtime();
- if (mTimer == -1) {
- mTimer = now;
- Thread.sleep(1);
- } else if (now - mTimer <= 60000) {
- Thread.sleep(yield ? 200 : 1);
+ if (now - mBringupStartTime <= 60000) {
+ Thread.sleep(sleepLonger ? 200 : 1);
} else {
updateState(DetailedState.FAILED, "checkpoint");
- throw new IllegalStateException("Time is up");
+ throw new IllegalStateException("VPN bringup took too long");
}
}
- private void execute() {
- // Catch all exceptions so we can clean up few things.
+ private void bringup() {
+ // Catch all exceptions so we can clean up a few things.
boolean initFinished = false;
try {
// Initialize the timer.
- checkpoint(false);
+ mBringupStartTime = SystemClock.elapsedRealtime();
// Wait for the daemons to stop.
for (String daemon : mDaemons) {
while (!SystemService.isStopped(daemon)) {
- checkpoint(true);
+ checkInterruptAndDelay(true);
}
}
@@ -1942,7 +1905,7 @@ public class Vpn {
// Wait for the daemon to start.
while (!SystemService.isRunning(daemon)) {
- checkpoint(true);
+ checkInterruptAndDelay(true);
}
// Create the control socket.
@@ -1958,7 +1921,7 @@ public class Vpn {
} catch (Exception e) {
// ignore
}
- checkpoint(true);
+ checkInterruptAndDelay(true);
}
mSockets[i].setSoTimeout(500);
@@ -1972,7 +1935,7 @@ public class Vpn {
out.write(bytes.length >> 8);
out.write(bytes.length);
out.write(bytes);
- checkpoint(false);
+ checkInterruptAndDelay(false);
}
out.write(0xFF);
out.write(0xFF);
@@ -1988,7 +1951,7 @@ public class Vpn {
} catch (Exception e) {
// ignore
}
- checkpoint(true);
+ checkInterruptAndDelay(true);
}
}
@@ -2001,7 +1964,7 @@ public class Vpn {
throw new IllegalStateException(daemon + " is dead");
}
}
- checkpoint(true);
+ checkInterruptAndDelay(true);
}
// Now we are connected. Read and parse the new state.
@@ -2057,8 +2020,8 @@ public class Vpn {
// Set the start time
mConfig.startTime = SystemClock.elapsedRealtime();
- // Check if the thread is interrupted while we are waiting.
- checkpoint(false);
+ // Check if the thread was interrupted while we were waiting on the lock.
+ checkInterruptAndDelay(false);
// Check if the interface is gone while we are waiting.
if (jniCheck(mConfig.interfaze) == 0) {
@@ -2081,10 +2044,11 @@ public class Vpn {
}
/**
- * Monitor the daemons we started, moving to disconnected state if the
- * underlying services fail.
+ * Check all daemons every two seconds. Return when one of them is stopped.
+ * The caller will move to the disconnected state when this function returns,
+ * which can happen if a daemon failed or if the VPN was torn down.
*/
- private void monitorDaemons() throws InterruptedException{
+ private void waitForDaemonsToStop() throws InterruptedException {
if (!mNetworkInfo.isConnected()) {
return;
}
diff --git a/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java b/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
index 17adb1a7..2224913b 100644
--- a/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
+++ b/com/android/server/connectivity/tethering/TetherInterfaceStateMachine.java
@@ -30,6 +30,7 @@ import android.net.RouteInfo;
import android.net.ip.InterfaceController;
import android.net.ip.RouterAdvertisementDaemon;
import android.net.ip.RouterAdvertisementDaemon.RaParams;
+import android.net.util.InterfaceParams;
import android.net.util.NetdService;
import android.net.util.SharedLog;
import android.os.INetworkManagementService;
@@ -48,7 +49,6 @@ import com.android.internal.util.StateMachine;
import java.net.Inet6Address;
import java.net.InetAddress;
-import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.ArrayList;
@@ -120,8 +120,7 @@ public class TetherInterfaceStateMachine extends StateMachine {
private int mLastError;
private int mServingMode;
private String mMyUpstreamIfaceName; // may change over time
- private NetworkInterface mNetworkInterface;
- private byte[] mHwAddr;
+ private InterfaceParams mInterfaceParams;
// TODO: De-duplicate this with mLinkProperties above. Currently, these link
// properties are those selected by the IPv6TetheringCoordinator and relayed
// to us. By comparison, mLinkProperties contains the addresses and directly
@@ -247,31 +246,16 @@ public class TetherInterfaceStateMachine extends StateMachine {
}
private boolean startIPv6() {
- // TODO: Refactor for testability (perhaps passing an android.system.Os
- // instance and calling getifaddrs() directly).
- try {
- mNetworkInterface = NetworkInterface.getByName(mIfaceName);
- } catch (SocketException e) {
- mLog.e("Error looking up NetworkInterfaces: " + e);
- stopIPv6();
- return false;
- }
- if (mNetworkInterface == null) {
- mLog.e("Failed to find NetworkInterface");
- stopIPv6();
- return false;
- }
-
- try {
- mHwAddr = mNetworkInterface.getHardwareAddress();
- } catch (SocketException e) {
- mLog.e("Failed to find hardware address: " + e);
+ // TODO: Refactor for better testability. This is one of the things
+ // that prohibits unittesting IPv6 tethering setup.
+ mInterfaceParams = InterfaceParams.getByName(mIfaceName);
+ if (mInterfaceParams == null) {
+ mLog.e("Failed to find InterfaceParams");
stopIPv6();
return false;
}
- final int ifindex = mNetworkInterface.getIndex();
- mRaDaemon = new RouterAdvertisementDaemon(mIfaceName, ifindex, mHwAddr);
+ mRaDaemon = new RouterAdvertisementDaemon(mInterfaceParams);
if (!mRaDaemon.start()) {
stopIPv6();
return false;
@@ -281,8 +265,7 @@ public class TetherInterfaceStateMachine extends StateMachine {
}
private void stopIPv6() {
- mNetworkInterface = null;
- mHwAddr = null;
+ mInterfaceParams = null;
setRaParams(null);
if (mRaDaemon != null) {
diff --git a/com/android/server/connectivity/tethering/TetheringConfiguration.java b/com/android/server/connectivity/tethering/TetheringConfiguration.java
index acbc10b9..09bce7f4 100644
--- a/com/android/server/connectivity/tethering/TetheringConfiguration.java
+++ b/com/android/server/connectivity/tethering/TetheringConfiguration.java
@@ -28,6 +28,8 @@ import android.net.ConnectivityManager;
import android.telephony.TelephonyManager;
import android.net.util.SharedLog;
+import com.android.internal.annotations.VisibleForTesting;
+
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
@@ -49,6 +51,7 @@ import java.util.StringJoiner;
public class TetheringConfiguration {
private static final String TAG = TetheringConfiguration.class.getSimpleName();
+ @VisibleForTesting
public static final int DUN_NOT_REQUIRED = 0;
public static final int DUN_REQUIRED = 1;
public static final int DUN_UNSPECIFIED = 2;
diff --git a/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java b/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
index b35ed751..34132910 100644
--- a/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
+++ b/com/android/server/connectivity/tethering/UpstreamNetworkMonitor.java
@@ -24,6 +24,7 @@ import static android.net.ConnectivityManager.TYPE_MOBILE_HIPRI;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
+import android.os.Process;
import android.net.ConnectivityManager;
import android.net.ConnectivityManager.NetworkCallback;
import android.net.IpPrefix;
@@ -476,6 +477,7 @@ public class UpstreamNetworkMonitor {
ConnectivityManager.getNetworkTypeName(type));
continue;
}
+ nc.setSingleUid(Process.myUid());
for (NetworkState value : netStates) {
if (!nc.satisfiedByNetworkCapabilities(value.networkCapabilities)) {
diff --git a/com/android/server/content/SyncJobService.java b/com/android/server/content/SyncJobService.java
index 29b322ea..51499f73 100644
--- a/com/android/server/content/SyncJobService.java
+++ b/com/android/server/content/SyncJobService.java
@@ -22,9 +22,14 @@ import android.content.Intent;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseLongArray;
+
+import com.android.internal.annotations.GuardedBy;
public class SyncJobService extends JobService {
private static final String TAG = "SyncManager";
@@ -32,7 +37,17 @@ public class SyncJobService extends JobService {
public static final String EXTRA_MESSENGER = "messenger";
private Messenger mMessenger;
- private SparseArray<JobParameters> jobParamsMap = new SparseArray<JobParameters>();
+
+ private final Object mLock = new Object();
+
+ @GuardedBy("mLock")
+ private final SparseArray<JobParameters> mJobParamsMap = new SparseArray<>();
+
+ @GuardedBy("mLock")
+ private final SparseBooleanArray mStartedSyncs = new SparseBooleanArray();
+
+ @GuardedBy("mLock")
+ private final SparseLongArray mJobStartUptimes = new SparseLongArray();
private final SyncLogger mLogger = SyncLogger.getInstance();
@@ -69,8 +84,12 @@ public class SyncJobService extends JobService {
mLogger.purgeOldLogs();
boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
- synchronized (jobParamsMap) {
- jobParamsMap.put(params.getJobId(), params);
+ synchronized (mLock) {
+ final int jobId = params.getJobId();
+ mJobParamsMap.put(jobId, params);
+
+ mStartedSyncs.delete(jobId);
+ mJobStartUptimes.put(jobId, SystemClock.uptimeMillis());
}
Message m = Message.obtain();
m.what = SyncManager.SyncHandler.MESSAGE_START_SYNC;
@@ -97,8 +116,36 @@ public class SyncJobService extends JobService {
+ params.getStopReason());
}
mLogger.log("onStopJob() ", mLogger.jobParametersToString(params));
- synchronized (jobParamsMap) {
- jobParamsMap.remove(params.getJobId());
+ synchronized (mLock) {
+ final int jobId = params.getJobId();
+ mJobParamsMap.remove(jobId);
+
+ final long startUptime = mJobStartUptimes.get(jobId);
+ final long nowUptime = SystemClock.uptimeMillis();
+ final long runtime = nowUptime - startUptime;
+
+ if (startUptime == 0) {
+ wtf("Job " + jobId + " start uptime not found: "
+ + " params=" + jobParametersToString(params));
+ } else if (runtime > 60 * 1000) {
+ // WTF if startSyncH() hasn't happened, *unless* onStopJob() was called too soon.
+ // (1 minute threshold.)
+ if (!mStartedSyncs.get(jobId)) {
+ wtf("Job " + jobId + " didn't start: "
+ + " startUptime=" + startUptime
+ + " nowUptime=" + nowUptime
+ + " params=" + jobParametersToString(params));
+ }
+ } else if (runtime < 10 * 1000) {
+ // Job stopped too soon. WTF.
+ wtf("Job " + jobId + " stopped in " + runtime + " ms: "
+ + " startUptime=" + startUptime
+ + " nowUptime=" + nowUptime
+ + " params=" + jobParametersToString(params));
+ }
+
+ mStartedSyncs.delete(jobId);
+ mJobStartUptimes.delete(jobId);
}
Message m = Message.obtain();
m.what = SyncManager.SyncHandler.MESSAGE_STOP_SYNC;
@@ -117,8 +164,8 @@ public class SyncJobService extends JobService {
}
public void callJobFinished(int jobId, boolean needsReschedule, String why) {
- synchronized (jobParamsMap) {
- JobParameters params = jobParamsMap.get(jobId);
+ synchronized (mLock) {
+ JobParameters params = mJobParamsMap.get(jobId);
mLogger.log("callJobFinished()",
" jobid=", jobId,
" needsReschedule=", needsReschedule,
@@ -126,10 +173,31 @@ public class SyncJobService extends JobService {
" why=", why);
if (params != null) {
jobFinished(params, needsReschedule);
- jobParamsMap.remove(jobId);
+ mJobParamsMap.remove(jobId);
} else {
Slog.e(TAG, "Job params not found for " + String.valueOf(jobId));
}
}
}
+
+ public void markSyncStarted(int jobId) {
+ synchronized (mLock) {
+ mStartedSyncs.put(jobId, true);
+ }
+ }
+
+ public static String jobParametersToString(JobParameters params) {
+ if (params == null) {
+ return "job:null";
+ } else {
+ return "job:#" + params.getJobId() + ":"
+ + "sr=[" + params.getStopReason() + "/" + params.getDebugStopReason() + "]:"
+ + SyncOperation.maybeCreateFromJobExtras(params.getExtras());
+ }
+ }
+
+ private void wtf(String message) {
+ mLogger.log(message);
+ Slog.wtf(TAG, message);
+ }
}
diff --git a/com/android/server/content/SyncLogger.java b/com/android/server/content/SyncLogger.java
index 85037688..75c01819 100644
--- a/com/android/server/content/SyncLogger.java
+++ b/com/android/server/content/SyncLogger.java
@@ -233,12 +233,7 @@ public class SyncLogger {
@Override
public String jobParametersToString(JobParameters params) {
- if (params == null) {
- return "job:null";
- } else {
- return "job:#" + params.getJobId() + ":"
- + SyncOperation.maybeCreateFromJobExtras(params.getExtras());
- }
+ return SyncJobService.jobParametersToString(params);
}
@Override
diff --git a/com/android/server/content/SyncManager.java b/com/android/server/content/SyncManager.java
index 965159bd..ad2cf6c1 100644
--- a/com/android/server/content/SyncManager.java
+++ b/com/android/server/content/SyncManager.java
@@ -2898,6 +2898,8 @@ public class SyncManager {
final boolean isLoggable = Log.isLoggable(TAG, Log.VERBOSE);
if (isLoggable) Slog.v(TAG, op.toString());
+ mSyncJobService.markSyncStarted(op.jobId);
+
if (mStorageIsLow) {
deferSyncH(op, SYNC_DELAY_ON_LOW_STORAGE, "storage low");
return;
diff --git a/com/android/server/devicepolicy/BaseIDevicePolicyManager.java b/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
index e55d4ea3..a8e82378 100644
--- a/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
+++ b/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
@@ -19,12 +19,16 @@ import android.annotation.UserIdInt;
import android.app.admin.IDevicePolicyManager;
import android.content.ComponentName;
import android.os.PersistableBundle;
+import android.os.UserHandle;
import android.security.keymaster.KeymasterCertificateChain;
import android.security.keystore.ParcelableKeyGenParameterSpec;
+import android.telephony.data.ApnSetting;
import com.android.internal.R;
import com.android.server.SystemService;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -64,10 +68,15 @@ abstract class BaseIDevicePolicyManager extends IDevicePolicyManager.Stub {
public void setSystemSetting(ComponentName who, String setting, String value){}
- public void transferOwner(ComponentName admin, ComponentName target, PersistableBundle bundle) {}
+ public void transferOwnership(ComponentName admin, ComponentName target, PersistableBundle bundle) {}
+
+ public PersistableBundle getTransferOwnershipBundle() {
+ return null;
+ }
public boolean generateKeyPair(ComponentName who, String callerPackage, String algorithm,
- ParcelableKeyGenParameterSpec keySpec, KeymasterCertificateChain attestationChain) {
+ ParcelableKeyGenParameterSpec keySpec, int idAttestationFlags,
+ KeymasterCertificateChain attestationChain) {
return false;
}
@@ -96,4 +105,74 @@ abstract class BaseIDevicePolicyManager extends IDevicePolicyManager.Stub {
byte[] cert, byte[] chain, boolean isUserSelectable) {
return false;
}
+
+ @Override
+ public boolean startUserInBackground(ComponentName who, UserHandle userHandle) {
+ return false;
+ }
+
+ @Override
+ public void setStartUserSessionMessage(
+ ComponentName admin, CharSequence startUserSessionMessage) {}
+
+ @Override
+ public void setEndUserSessionMessage(ComponentName admin, CharSequence endUserSessionMessage) {}
+
+ @Override
+ public String getStartUserSessionMessage(ComponentName admin) {
+ return null;
+ }
+
+ @Override
+ public String getEndUserSessionMessage(ComponentName admin) {
+ return null;
+ }
+
+ @Override
+ public void setPrintingEnabled(ComponentName admin, boolean enabled) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isPrintingEnabled() {
+ return true;
+ }
+
+ @Override
+ public List<String> setMeteredDataDisabled(ComponentName admin, List<String> packageNames) {
+ return packageNames;
+ }
+
+ @Override
+ public List<String> getMeteredDataDisabled(ComponentName admin) {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public int addOverrideApn(ComponentName admin, ApnSetting apnSetting) {
+ return -1;
+ }
+
+ @Override
+ public boolean updateOverrideApn(ComponentName admin, int apnId, ApnSetting apnSetting) {
+ return false;
+ }
+
+ @Override
+ public boolean removeOverrideApn(ComponentName admin, int apnId) {
+ return false;
+ }
+
+ @Override
+ public List<ApnSetting> getOverrideApns(ComponentName admin) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void setOverrideApnsEnabled(ComponentName admin, boolean enabled) {}
+
+ @Override
+ public boolean isOverrideApnEnabled(ComponentName admin) {
+ return false;
+ }
}
diff --git a/com/android/server/devicepolicy/DevicePolicyManagerService.java b/com/android/server/devicepolicy/DevicePolicyManagerService.java
index e5351b48..99712a51 100644
--- a/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -20,6 +20,7 @@ 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.DeviceAdminReceiver.EXTRA_TRANSFER_OWNERSHIP_ADMIN_EXTRAS_BUNDLE;
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;
@@ -45,17 +46,34 @@ import static android.app.admin.DevicePolicyManager.DELEGATION_INSTALL_EXISTING_
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.ID_TYPE_BASE_INFO;
+import static android.app.admin.DevicePolicyManager.ID_TYPE_IMEI;
+import static android.app.admin.DevicePolicyManager.ID_TYPE_MEID;
+import static android.app.admin.DevicePolicyManager.ID_TYPE_SERIAL;
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;
import static android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE;
import static android.app.admin.DevicePolicyManager.WIPE_RESET_PROTECTION_DATA;
import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
+
+import static android.provider.Telephony.Carriers.DPC_URI;
+import static android.provider.Telephony.Carriers.ENFORCE_KEY;
+import static android.provider.Telephony.Carriers.ENFORCE_MANAGED_URI;
+
+import static com.android.internal.logging.nano.MetricsProto.MetricsEvent
+ .PROVISIONING_ENTRY_POINT_ADB;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker
+ .STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
+
import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.PROVISIONING_ENTRY_POINT_ADB;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW;
+
+import static com.android.server.devicepolicy.TransferOwnershipMetadataManager.ADMIN_TYPE_DEVICE_OWNER;
+import static com.android.server.devicepolicy.TransferOwnershipMetadataManager.ADMIN_TYPE_PROFILE_OWNER;
+
import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.END_TAG;
import static org.xmlpull.v1.XmlPullParser.TEXT;
@@ -70,6 +88,7 @@ import android.annotation.UserIdInt;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
+import android.app.ActivityThread;
import android.app.AlarmManager;
import android.app.AppGlobals;
import android.app.IActivityManager;
@@ -91,8 +110,10 @@ import android.app.admin.SystemUpdateInfo;
import android.app.admin.SystemUpdatePolicy;
import android.app.backup.IBackupManager;
import android.app.trust.TrustManager;
+import android.app.usage.UsageStatsManagerInternal;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
+import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -102,8 +123,8 @@ import android.content.pm.IPackageDataObserver;
import android.content.pm.IPackageManager;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManagerInternal;
import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PackageManagerInternal;
import android.content.pm.ParceledListSlice;
import android.content.pm.PermissionInfo;
import android.content.pm.ResolveInfo;
@@ -112,6 +133,7 @@ import android.content.pm.StringParceledListSlice;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.database.ContentObserver;
+import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.media.AudioManager;
@@ -145,26 +167,28 @@ import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.UserManagerInternal;
+import android.os.UserManagerInternal.UserRestrictionsListener;
import android.os.storage.StorageManager;
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.KeyStore;
import android.security.keymaster.KeymasterCertificateChain;
+import android.security.keystore.AttestationUtils;
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.telephony.data.ApnSetting;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.AtomicFile;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
@@ -192,9 +216,12 @@ import com.android.internal.util.Preconditions;
import com.android.internal.util.XmlUtils;
import com.android.internal.widget.LockPatternUtils;
import com.android.server.LocalServices;
+import com.android.server.SystemServerInitThreadPool;
import com.android.server.SystemService;
import com.android.server.devicepolicy.DevicePolicyManagerService.ActiveAdmin.TrustAgentInfo;
+import com.android.server.net.NetworkPolicyManagerInternal;
import com.android.server.pm.UserRestrictionsUtils;
+
import com.google.android.collect.Sets;
import org.xmlpull.v1.XmlPullParser;
@@ -208,7 +235,6 @@ 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;
@@ -217,7 +243,11 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Objects;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@@ -235,6 +265,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
private static final String DEVICE_POLICIES_XML = "device_policies.xml";
+ private static final String TRANSFER_OWNERSHIP_PARAMETERS_XML =
+ "transfer-ownership-parameters.xml";
+
private static final String TAG_ACCEPTED_CA_CERTIFICATES = "accepted-ca-certificate";
private static final String TAG_LOCK_TASK_COMPONENTS = "lock-task-component";
@@ -276,6 +309,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
private static final String TAG_PASSWORD_VALIDITY = "password-validity";
+ private static final String TAG_PRINTING_ENABLED = "printing-enabled";
+
private static final int REQUEST_EXPIRE_PASSWORD = 5571;
private static final long MS_PER_DAY = TimeUnit.DAYS.toMillis(1);
@@ -395,6 +430,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final IPackageManager mIPackageManager;
final UserManager mUserManager;
final UserManagerInternal mUserManagerInternal;
+ final UsageStatsManagerInternal mUsageStatsManagerInternal;
final TelephonyManager mTelephonyManager;
private final LockPatternUtils mLockPatternUtils;
private final DevicePolicyConstants mConstants;
@@ -436,6 +472,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
private SetupContentObserver mSetupContentObserver;
+ @VisibleForTesting
+ final TransferOwnershipMetadataManager mTransferOwnershipMetadataManager;
+
private final Runnable mRemoteBugreportTimeoutRunnable = new Runnable() {
@Override
public void run() {
@@ -578,6 +617,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
long mPasswordTokenHandle = 0;
+ boolean mPrintingEnabled = true;
+
public DevicePolicyData(int userHandle) {
mUserHandle = userHandle;
}
@@ -637,14 +678,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
if (Intent.ACTION_USER_ADDED.equals(action)) {
- sendUserAddedOrRemovedCommand(DeviceAdminReceiver.ACTION_USER_ADDED, userHandle);
+ sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_ADDED, userHandle);
synchronized (DevicePolicyManagerService.this) {
// It might take a while for the user to become affiliated. Make security
// and network logging unavailable in the meantime.
maybePauseDeviceWideLoggingLocked();
}
} else if (Intent.ACTION_USER_REMOVED.equals(action)) {
- sendUserAddedOrRemovedCommand(DeviceAdminReceiver.ACTION_USER_REMOVED, userHandle);
+ sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_REMOVED, userHandle);
synchronized (DevicePolicyManagerService.this) {
// Check whether the user is affiliated, *before* removing its data.
boolean isRemovedUserAffiliated = isUserAffiliatedWithDeviceLocked(userHandle);
@@ -658,12 +699,17 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
}
} else if (Intent.ACTION_USER_STARTED.equals(action)) {
+ sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_STARTED, userHandle);
synchronized (DevicePolicyManagerService.this) {
maybeSendAdminEnabledBroadcastLocked(userHandle);
// Reset the policy data
mUserData.remove(userHandle);
}
handlePackagesChanged(null /* check all admins */, userHandle);
+ } else if (Intent.ACTION_USER_STOPPED.equals(action)) {
+ sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_STOPPED, userHandle);
+ } else if (Intent.ACTION_USER_SWITCHED.equals(action)) {
+ sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_SWITCHED, userHandle);
} else if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
synchronized (DevicePolicyManagerService.this) {
maybeSendAdminEnabledBroadcastLocked(userHandle);
@@ -672,7 +718,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
handlePackagesChanged(null /* check all admins */, userHandle);
} else if (Intent.ACTION_PACKAGE_CHANGED.equals(action)
|| (Intent.ACTION_PACKAGE_ADDED.equals(action)
- && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false))) {
+ && intent.getBooleanExtra(Intent.EXTRA_REPLACING, false))) {
handlePackagesChanged(intent.getData().getSchemeSpecificPart(), userHandle);
} else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)
&& !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
@@ -682,7 +728,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
}
- private void sendUserAddedOrRemovedCommand(String action, int userHandle) {
+ private void sendDeviceOwnerUserCommand(String action, int userHandle) {
synchronized (DevicePolicyManagerService.this) {
ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
if (deviceOwner != null) {
@@ -694,6 +740,33 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
};
+ protected static class RestrictionsListener implements UserRestrictionsListener {
+ private Context mContext;
+
+ public RestrictionsListener(Context context) {
+ mContext = context;
+ }
+
+ public void onUserRestrictionsChanged(int userId, Bundle newRestrictions,
+ Bundle prevRestrictions) {
+ final boolean newlyDisallowed =
+ newRestrictions.getBoolean(UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE);
+ final boolean previouslyDisallowed =
+ prevRestrictions.getBoolean(UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE);
+ final boolean restrictionChanged = (newlyDisallowed != previouslyDisallowed);
+
+ if (restrictionChanged) {
+ // Notify ManagedProvisioning to update the built-in cross profile intent filters.
+ Intent intent = new Intent(
+ DevicePolicyManager.ACTION_DATA_SHARING_RESTRICTION_CHANGED);
+ intent.setPackage(MANAGED_PROVISIONING_PKG);
+ intent.putExtra(Intent.EXTRA_USER_ID, userId);
+ intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ mContext.sendBroadcastAsUser(intent, UserHandle.SYSTEM);
+ }
+ }
+ }
+
static class ActiveAdmin {
private static final String TAG_DISABLE_KEYGUARD_FEATURES = "disable-keyguard-features";
private static final String TAG_TEST_ONLY_ADMIN = "test-only-admin";
@@ -754,6 +827,11 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
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";
+ private static final String TAG_MANDATORY_BACKUP_TRANSPORT = "mandatory_backup_transport";
+ private static final String TAG_START_USER_SESSION_MESSAGE = "start_user_session_message";
+ private static final String TAG_END_USER_SESSION_MESSAGE = "end_user_session_message";
+ private static final String TAG_METERED_DATA_DISABLED_PACKAGES
+ = "metered_data_disabled_packages";
DeviceAdminInfo info;
@@ -820,6 +898,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
}
+ // The list of packages which are not allowed to use metered data.
+ List<String> meteredDisabledPackages;
+
final Set<String> accountTypesWithManagementDisabled = new ArraySet<>();
// The list of permitted accessibility services package namesas set by a profile
@@ -870,6 +951,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
// The blacklist data is stored in a file whose name is stored in the XML
String passwordBlacklistFile = null;
+ // The component name of the backup transport which has to be used if backups are mandatory
+ // or null if backups are not mandatory.
+ ComponentName mandatoryBackupTransport = null;
+
+ // Message for user switcher
+ String startUserSessionMessage = null;
+ String endUserSessionMessage = null;
+
ActiveAdmin(DeviceAdminInfo _info, boolean parent) {
info = _info;
isParent = parent;
@@ -1093,6 +1182,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
writePackageListToXml(out, TAG_PERMITTED_NOTIFICATION_LISTENERS,
permittedNotificationListeners);
writePackageListToXml(out, TAG_KEEP_UNINSTALLED_PACKAGES, keepUninstalledPackages);
+ writePackageListToXml(out, TAG_METERED_DATA_DISABLED_PACKAGES, meteredDisabledPackages);
if (hasUserRestrictions()) {
UserRestrictionsUtils.writeRestrictions(
out, userRestrictions, TAG_USER_RESTRICTIONS);
@@ -1133,6 +1223,21 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
out.attribute(null, ATTR_VALUE, Boolean.toString(isLogoutEnabled));
out.endTag(null, TAG_IS_LOGOUT_ENABLED);
}
+ if (mandatoryBackupTransport != null) {
+ out.startTag(null, TAG_MANDATORY_BACKUP_TRANSPORT);
+ out.attribute(null, ATTR_VALUE, mandatoryBackupTransport.flattenToString());
+ out.endTag(null, TAG_MANDATORY_BACKUP_TRANSPORT);
+ }
+ if (startUserSessionMessage != null) {
+ out.startTag(null, TAG_START_USER_SESSION_MESSAGE);
+ out.text(startUserSessionMessage);
+ out.endTag(null, TAG_START_USER_SESSION_MESSAGE);
+ }
+ if (endUserSessionMessage != null) {
+ out.startTag(null, TAG_END_USER_SESSION_MESSAGE);
+ out.text(endUserSessionMessage);
+ out.endTag(null, TAG_END_USER_SESSION_MESSAGE);
+ }
}
void writePackageListToXml(XmlSerializer out, String outerTag,
@@ -1274,6 +1379,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
permittedNotificationListeners = readPackageList(parser, tag);
} else if (TAG_KEEP_UNINSTALLED_PACKAGES.equals(tag)) {
keepUninstalledPackages = readPackageList(parser, tag);
+ } else if (TAG_METERED_DATA_DISABLED_PACKAGES.equals(tag)) {
+ meteredDisabledPackages = readPackageList(parser, tag);
} else if (TAG_USER_RESTRICTIONS.equals(tag)) {
userRestrictions = UserRestrictionsUtils.readRestrictions(parser);
} else if (TAG_DEFAULT_ENABLED_USER_RESTRICTIONS.equals(tag)) {
@@ -1311,6 +1418,23 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
} else if (TAG_IS_LOGOUT_ENABLED.equals(tag)) {
isLogoutEnabled = Boolean.parseBoolean(
parser.getAttributeValue(null, ATTR_VALUE));
+ } else if (TAG_MANDATORY_BACKUP_TRANSPORT.equals(tag)) {
+ mandatoryBackupTransport = ComponentName.unflattenFromString(
+ parser.getAttributeValue(null, ATTR_VALUE));
+ } else if (TAG_START_USER_SESSION_MESSAGE.equals(tag)) {
+ type = parser.next();
+ if (type == XmlPullParser.TEXT) {
+ startUserSessionMessage = parser.getText();
+ } else {
+ Log.w(LOG_TAG, "Missing text when loading start session message");
+ }
+ } else if (TAG_END_USER_SESSION_MESSAGE.equals(tag)) {
+ type = parser.next();
+ if (type == XmlPullParser.TEXT) {
+ endUserSessionMessage = parser.getText();
+ } else {
+ Log.w(LOG_TAG, "Missing text when loading end session message");
+ }
} else {
Slog.w(LOG_TAG, "Unknown admin tag: " + tag);
XmlUtils.skipCurrentTag(parser);
@@ -1554,6 +1678,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
removedAdmin = true;
policy.mAdminList.remove(i);
policy.mAdminMap.remove(aa.info.getComponent());
+ pushActiveAdminPackagesLocked(userHandle);
+ pushMeteredDisabledPackagesLocked(userHandle);
}
}
} catch (RemoteException re) {
@@ -1647,6 +1773,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return LocalServices.getService(PackageManagerInternal.class);
}
+ UsageStatsManagerInternal getUsageStatsManagerInternal() {
+ return LocalServices.getService(UsageStatsManagerInternal.class);
+ }
+
+ NetworkPolicyManagerInternal getNetworkPolicyManagerInternal() {
+ return LocalServices.getService(NetworkPolicyManagerInternal.class);
+ }
+
NotificationManager getNotificationManager() {
return mContext.getSystemService(NotificationManager.class);
}
@@ -1685,6 +1819,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return ActivityManager.getService();
}
+ ActivityManagerInternal getActivityManagerInternal() {
+ return LocalServices.getService(ActivityManagerInternal.class);
+ }
+
IPackageManager getIPackageManager() {
return AppGlobals.getPackageManager();
}
@@ -1896,6 +2034,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
KeyChainConnection keyChainBindAsUser(UserHandle user) throws InterruptedException {
return KeyChain.bindAsUser(mContext, user);
}
+
+ void postOnSystemServerInitThreadPool(Runnable runnable) {
+ SystemServerInitThreadPool.get().submit(runnable, LOG_TAG);
+ }
+
+ public TransferOwnershipMetadataManager newTransferOwnershipMetadataManager() {
+ return new TransferOwnershipMetadataManager();
+ }
}
/**
@@ -1917,6 +2063,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
mUserManager = Preconditions.checkNotNull(injector.getUserManager());
mUserManagerInternal = Preconditions.checkNotNull(injector.getUserManagerInternal());
+ mUsageStatsManagerInternal = Preconditions.checkNotNull(
+ injector.getUsageStatsManagerInternal());
mIPackageManager = Preconditions.checkNotNull(injector.getIPackageManager());
mTelephonyManager = Preconditions.checkNotNull(injector.getTelephonyManager());
@@ -1938,6 +2086,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
mOverlayPackagesProvider = new OverlayPackagesProvider(mContext);
+ mTransferOwnershipMetadataManager = mInjector.newTransferOwnershipMetadataManager();
+
if (!mHasFeature) {
// Skip the rest of the initialization
return;
@@ -1949,6 +2099,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
filter.addAction(Intent.ACTION_USER_ADDED);
filter.addAction(Intent.ACTION_USER_REMOVED);
filter.addAction(Intent.ACTION_USER_STARTED);
+ filter.addAction(Intent.ACTION_USER_STOPPED);
+ filter.addAction(Intent.ACTION_USER_SWITCHED);
filter.addAction(Intent.ACTION_USER_UNLOCKED);
filter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY);
mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler);
@@ -1966,6 +2118,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
LocalServices.addService(DevicePolicyManagerInternal.class, mLocalService);
mSetupContentObserver = new SetupContentObserver(mHandler);
+
+ mUserManagerInternal.addUserRestrictionsListener(new RestrictionsListener(mContext));
}
/**
@@ -2786,6 +2940,12 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
out.endTag(null, TAG_CURRENT_INPUT_METHOD_SET);
}
+ if (!policy.mPrintingEnabled) {
+ out.startTag(null, TAG_PRINTING_ENABLED);
+ out.attribute(null, ATTR_VALUE, Boolean.toString(policy.mPrintingEnabled));
+ out.endTag(null, TAG_PRINTING_ENABLED);
+ }
+
for (final String cert : policy.mOwnerInstalledCaCerts) {
out.startTag(null, TAG_OWNER_INSTALLED_CA_CERT);
out.attribute(null, ATTR_ALIAS, cert);
@@ -3004,6 +3164,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
policy.mCurrentInputMethodSet = true;
} else if (TAG_OWNER_INSTALLED_CA_CERT.equals(tag)) {
policy.mOwnerInstalledCaCerts.add(parser.getAttributeValue(null, ATTR_ALIAS));
+ } else if (TAG_PRINTING_ENABLED.equals(tag)) {
+ String enabled = parser.getAttributeValue(null, ATTR_VALUE);
+ policy.mPrintingEnabled = Boolean.toString(true).equals(enabled);
} else {
Slog.w(LOG_TAG, "Unknown tag: " + tag);
XmlUtils.skipCurrentTag(parser);
@@ -3124,6 +3287,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
switch (phase) {
case SystemService.PHASE_LOCK_SETTINGS_READY:
onLockSettingsReady();
+ loadAdminDataAsync();
break;
case SystemService.PHASE_BOOT_COMPLETED:
ensureDeviceOwnerUserStarted(); // TODO Consider better place to do this.
@@ -3152,11 +3316,42 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
synchronized (this) {
- // push the force-ephemeral-users policy to the user manager.
ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
if (deviceOwner != null) {
+ // Push the force-ephemeral-users policy to the user manager.
mUserManagerInternal.setForceEphemeralUsers(deviceOwner.forceEphemeralUsers);
+
+ // Update user switcher message to activity manager.
+ ActivityManagerInternal activityManagerInternal =
+ mInjector.getActivityManagerInternal();
+ activityManagerInternal.setSwitchingFromSystemUserMessage(
+ deviceOwner.startUserSessionMessage);
+ activityManagerInternal.setSwitchingToSystemUserMessage(
+ deviceOwner.endUserSessionMessage);
}
+
+ revertTransferOwnershipIfNecessaryLocked();
+ }
+ }
+
+ private void revertTransferOwnershipIfNecessaryLocked() {
+ if (!mTransferOwnershipMetadataManager.metadataFileExists()) {
+ return;
+ }
+ Slog.e(LOG_TAG, "Owner transfer metadata file exists! Reverting transfer.");
+ final TransferOwnershipMetadataManager.Metadata metadata =
+ mTransferOwnershipMetadataManager.loadMetadataFile();
+ // Revert transfer
+ if (metadata.adminType.equals(ADMIN_TYPE_PROFILE_OWNER)) {
+ transferProfileOwnershipLocked(metadata.targetComponent, metadata.sourceComponent,
+ metadata.userId);
+ deleteTransferOwnershipMetadataFileLocked();
+ deleteTransferOwnershipBundleLocked(metadata.userId);
+ } else if (metadata.adminType.equals(ADMIN_TYPE_DEVICE_OWNER)) {
+ transferDeviceOwnershipLocked(metadata.targetComponent, metadata.sourceComponent,
+ metadata.userId);
+ deleteTransferOwnershipMetadataFileLocked();
+ deleteTransferOwnershipBundleLocked(metadata.userId);
}
}
@@ -3351,6 +3546,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (replaceIndex == -1) {
policy.mAdminList.add(newAdmin);
enableIfNecessary(info.getPackageName(), userHandle);
+ mUsageStatsManagerInternal.onActiveAdminAdded(
+ adminReceiver.getPackageName(), userHandle);
} else {
policy.mAdminList.set(replaceIndex, newAdmin);
}
@@ -3363,9 +3560,63 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
}
+ private void loadAdminDataAsync() {
+ mInjector.postOnSystemServerInitThreadPool(() -> {
+ pushActiveAdminPackages();
+ mUsageStatsManagerInternal.onAdminDataAvailable();
+ pushAllMeteredRestrictedPackages();
+ mInjector.getNetworkPolicyManagerInternal().onAdminDataAvailable();
+ });
+ }
+
+ private void pushActiveAdminPackages() {
+ synchronized (this) {
+ final List<UserInfo> users = mUserManager.getUsers();
+ for (int i = users.size() - 1; i >= 0; --i) {
+ final int userId = users.get(i).id;
+ mUsageStatsManagerInternal.setActiveAdminApps(
+ getActiveAdminPackagesLocked(userId), userId);
+ }
+ }
+ }
+
+ private void pushAllMeteredRestrictedPackages() {
+ synchronized (this) {
+ final List<UserInfo> users = mUserManager.getUsers();
+ for (int i = users.size() - 1; i >= 0; --i) {
+ final int userId = users.get(i).id;
+ mInjector.getNetworkPolicyManagerInternal().setMeteredRestrictedPackagesAsync(
+ getMeteredDisabledPackagesLocked(userId), userId);
+ }
+ }
+ }
+
+ private void pushActiveAdminPackagesLocked(int userId) {
+ mUsageStatsManagerInternal.setActiveAdminApps(
+ getActiveAdminPackagesLocked(userId), userId);
+ }
+
+ private Set<String> getActiveAdminPackagesLocked(int userId) {
+ final DevicePolicyData policy = getUserData(userId);
+ Set<String> adminPkgs = null;
+ for (int i = policy.mAdminList.size() - 1; i >= 0; --i) {
+ final String pkgName = policy.mAdminList.get(i).info.getPackageName();
+ if (adminPkgs == null) {
+ adminPkgs = new ArraySet<>();
+ }
+ adminPkgs.add(pkgName);
+ }
+ return adminPkgs;
+ }
+
private void transferActiveAdminUncheckedLocked(ComponentName incomingReceiver,
ComponentName outgoingReceiver, int userHandle) {
final DevicePolicyData policy = getUserData(userHandle);
+ if (!policy.mAdminMap.containsKey(outgoingReceiver)
+ && policy.mAdminMap.containsKey(incomingReceiver)) {
+ // Nothing to transfer - the incoming receiver is already the active admin.
+ return;
+ }
final DeviceAdminInfo incomingDeviceInfo = findAdmin(incomingReceiver, userHandle,
/* throwForMissingPermission= */ true);
final ActiveAdmin adminToTransfer = policy.mAdminMap.get(outgoingReceiver);
@@ -3379,7 +3630,6 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
saveSettingsLocked(userHandle);
- //TODO: Make sure we revert back when we detect a failure.
sendAdminCommandLocked(adminToTransfer, DeviceAdminReceiver.ACTION_DEVICE_ADMIN_ENABLED,
null, null);
}
@@ -3481,6 +3731,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
}
+ @Override
public void forceRemoveActiveAdmin(ComponentName adminReceiver, int userHandle) {
if (!mHasFeature) {
return;
@@ -3534,7 +3785,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
private boolean isPackageTestOnly(String packageName, int userHandle) {
final ApplicationInfo ai;
try {
- ai = mIPackageManager.getApplicationInfo(packageName,
+ ai = mInjector.getIPackageManager().getApplicationInfo(packageName,
(PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE), userHandle);
} catch (RemoteException e) {
@@ -3556,7 +3807,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
private void enforceShell(String method) {
- final int callingUid = Binder.getCallingUid();
+ final int callingUid = mInjector.binderGetCallingUid();
if (callingUid != Process.SHELL_UID && callingUid != Process.ROOT_UID) {
throw new SecurityException("Non-shell user attempted to call " + method);
}
@@ -4417,6 +4668,28 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
}
+ private boolean canPOorDOCallResetPassword(ActiveAdmin admin, @UserIdInt int userId) {
+ // Only if the admins targets a pre-O SDK
+ return getTargetSdk(admin.info.getPackageName(), userId) < Build.VERSION_CODES.O;
+ }
+
+ /* PO or DO could do an untrusted reset in certain conditions. */
+ private boolean canUserHaveUntrustedCredentialReset(@UserIdInt int userId) {
+ synchronized (this) {
+ // An active DO or PO might be able to fo an untrusted credential reset
+ for (final ActiveAdmin admin : getUserData(userId).mAdminList) {
+ if (!isActiveAdminWithPolicyForUserLocked(admin,
+ DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, userId)) {
+ continue;
+ }
+ if (canPOorDOCallResetPassword(admin, userId)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ }
+
@Override
public boolean resetPassword(String passwordOrNull, int flags) throws RemoteException {
final int callingUid = mInjector.binderGetCallingUid();
@@ -4435,12 +4708,12 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
null, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, callingUid);
final boolean preN;
if (admin != null) {
- final int targetSdk = getTargetSdk(admin.info.getPackageName(), userHandle);
- if (targetSdk >= Build.VERSION_CODES.O) {
+ if (!canPOorDOCallResetPassword(admin, userHandle)) {
throw new SecurityException("resetPassword() is deprecated for DPC targeting O"
+ " or later");
}
- preN = targetSdk <= android.os.Build.VERSION_CODES.M;
+ preN = getTargetSdk(admin.info.getPackageName(),
+ userHandle) <= android.os.Build.VERSION_CODES.M;
} else {
// Otherwise, make sure the caller has any active admin with the right policy.
admin = getActiveAdminForCallerLocked(null,
@@ -5052,17 +5325,82 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
+ private void enforceIsDeviceOwnerOrCertInstallerOfDeviceOwner(
+ ComponentName who, String callerPackage, int callerUid) throws SecurityException {
+ if (who == null) {
+ if (!mOwners.hasDeviceOwner()) {
+ throw new SecurityException("Not in Device Owner mode.");
+ }
+ if (UserHandle.getUserId(callerUid) != mOwners.getDeviceOwnerUserId()) {
+ throw new SecurityException("Caller not from device owner user");
+ }
+ if (!isCallerDelegate(callerPackage, DELEGATION_CERT_INSTALL)) {
+ throw new SecurityException("Caller with uid " + mInjector.binderGetCallingUid() +
+ "has no permission to generate keys.");
+ }
+ } else {
+ // Caller provided - check it is the device owner.
+ synchronized (this) {
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ public static int[] translateIdAttestationFlags(
+ int idAttestationFlags) {
+ Map<Integer, Integer> idTypeToAttestationFlag = new HashMap();
+ idTypeToAttestationFlag.put(ID_TYPE_SERIAL, AttestationUtils.ID_TYPE_SERIAL);
+ idTypeToAttestationFlag.put(ID_TYPE_IMEI, AttestationUtils.ID_TYPE_IMEI);
+ idTypeToAttestationFlag.put(ID_TYPE_MEID, AttestationUtils.ID_TYPE_MEID);
+
+ int numFlagsSet = Integer.bitCount(idAttestationFlags);
+ // No flags are set - return null to indicate no device ID attestation information should
+ // be included in the attestation record.
+ if (numFlagsSet == 0) {
+ return null;
+ }
+
+ // If the ID_TYPE_BASE_INFO is set, make sure that a non-null array is returned, even if
+ // no other flag is set. That will lead to inclusion of general device make data in the
+ // attestation record, but no specific device identifiers.
+ if ((idAttestationFlags & ID_TYPE_BASE_INFO) != 0) {
+ numFlagsSet -= 1;
+ idAttestationFlags = idAttestationFlags & (~ID_TYPE_BASE_INFO);
+ }
+
+ int[] attestationUtilsFlags = new int[numFlagsSet];
+ int i = 0;
+ for (Integer idType: idTypeToAttestationFlag.keySet()) {
+ if ((idType & idAttestationFlags) != 0) {
+ attestationUtilsFlags[i++] = idTypeToAttestationFlag.get(idType);
+ }
+ }
+
+ return attestationUtilsFlags;
+ }
+
@Override
public boolean generateKeyPair(ComponentName who, String callerPackage, String algorithm,
ParcelableKeyGenParameterSpec parcelableKeySpec,
+ int idAttestationFlags,
KeymasterCertificateChain attestationChain) {
- enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
- DELEGATION_CERT_INSTALL);
+ // Get attestation flags, if any.
+ final int[] attestationUtilsFlags = translateIdAttestationFlags(idAttestationFlags);
+ final boolean deviceIdAttestationRequired = attestationUtilsFlags != null;
+ final int callingUid = mInjector.binderGetCallingUid();
+
+ if (deviceIdAttestationRequired && attestationUtilsFlags.length > 0) {
+ enforceIsDeviceOwnerOrCertInstallerOfDeviceOwner(who, callerPackage, callingUid);
+ } else {
+ enforceCanManageScope(who, callerPackage, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER,
+ DELEGATION_CERT_INSTALL);
+ }
final KeyGenParameterSpec keySpec = parcelableKeySpec.getSpec();
- if (TextUtils.isEmpty(keySpec.getKeystoreAlias())) {
+ final String alias = keySpec.getKeystoreAlias();
+ if (TextUtils.isEmpty(alias)) {
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) {
@@ -5070,7 +5408,10 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
- final int callingUid = mInjector.binderGetCallingUid();
+ if (deviceIdAttestationRequired && (keySpec.getAttestationChallenge() == null)) {
+ throw new IllegalArgumentException(
+ "Requested Device ID attestation but challenge is empty.");
+ }
final UserHandle userHandle = mInjector.binderGetCallingUserHandle();
final long id = mInjector.binderClearCallingIdentity();
@@ -5103,7 +5444,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final byte[] attestationChallenge = keySpec.getAttestationChallenge();
if (attestationChallenge != null) {
final boolean attestationResult = keyChain.attestKey(
- alias, attestationChallenge, attestationChain);
+ alias, attestationChallenge, attestationUtilsFlags, attestationChain);
if (!attestationResult) {
Log.e(LOG_TAG, String.format(
"Attestation for %s failed, deleting key.", alias));
@@ -6432,13 +6773,36 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
}
- synchronized void sendDeviceOwnerCommand(String action, Bundle extras) {
- Intent intent = new Intent(action);
- intent.setComponent(mOwners.getDeviceOwnerComponent());
+ void sendDeviceOwnerCommand(String action, Bundle extras) {
+ int deviceOwnerUserId;
+ ComponentName deviceOwnerComponent;
+ synchronized (this) {
+ deviceOwnerUserId = mOwners.getDeviceOwnerUserId();
+ deviceOwnerComponent = mOwners.getDeviceOwnerComponent();
+ }
+ sendActiveAdminCommand(action, extras, deviceOwnerUserId,
+ deviceOwnerComponent);
+ }
+
+ private void sendProfileOwnerCommand(String action, Bundle extras, int userHandle) {
+ sendActiveAdminCommand(action, extras, userHandle,
+ mOwners.getProfileOwnerComponent(userHandle));
+ }
+
+ private void sendActiveAdminCommand(String action, Bundle extras,
+ int userHandle, ComponentName receiverComponent) {
+ final Intent intent = new Intent(action);
+ intent.setComponent(receiverComponent);
if (extras != null) {
intent.putExtras(extras);
}
- mContext.sendBroadcastAsUser(intent, UserHandle.of(mOwners.getDeviceOwnerUserId()));
+ mContext.sendBroadcastAsUser(intent, UserHandle.of(userHandle));
+ }
+
+ private void sendOwnerChangedBroadcast(String broadcast, int userId) {
+ final Intent intent = new Intent(broadcast)
+ .addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
+ mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
}
private synchronized String getDeviceOwnerRemoteBugreportUri() {
@@ -6816,10 +7180,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
ident = mInjector.binderClearCallingIdentity();
try {
// TODO Send to system too?
- mContext.sendBroadcastAsUser(
- new Intent(DevicePolicyManager.ACTION_DEVICE_OWNER_CHANGED)
- .addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND),
- UserHandle.of(userId));
+ sendOwnerChangedBroadcast(DevicePolicyManager.ACTION_DEVICE_OWNER_CHANGED, userId);
} finally {
mInjector.binderRestoreCallingIdentity(ident);
}
@@ -6970,9 +7331,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
try {
clearDeviceOwnerLocked(admin, deviceOwnerUserId);
removeActiveAdminLocked(deviceOwnerComponent, deviceOwnerUserId);
- Intent intent = new Intent(DevicePolicyManager.ACTION_DEVICE_OWNER_CHANGED);
- intent.addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
- mContext.sendBroadcastAsUser(intent, UserHandle.of(deviceOwnerUserId));
+ sendOwnerChangedBroadcast(DevicePolicyManager.ACTION_DEVICE_OWNER_CHANGED,
+ deviceOwnerUserId);
} finally {
mInjector.binderRestoreCallingIdentity(ident);
}
@@ -6980,6 +7340,15 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
}
+ private void clearOverrideApnUnchecked() {
+ // Disable Override APNs and remove them from database.
+ setOverrideApnsEnabledUnchecked(false);
+ final List<ApnSetting> apns = getOverrideApnsUnchecked();
+ for (int i = 0; i < apns.size(); i ++) {
+ removeOverrideApnUnchecked(apns.get(i).getId());
+ }
+ }
+
private void clearDeviceOwnerLocked(ActiveAdmin admin, int userId) {
mDeviceAdminServiceController.stopServiceForOwner(userId, "clear-device-owner");
@@ -7000,6 +7369,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
systemPolicyData.mLastNetworkLogsRetrievalTime = -1;
saveSettingsLocked(UserHandle.USER_SYSTEM);
clearUserPoliciesLocked(userId);
+ clearOverrideApnUnchecked();
mOwners.clearDeviceOwner();
mOwners.writeDeviceOwner();
@@ -7009,6 +7379,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
mInjector.securityLogSetLoggingEnabledProperty(false);
mSecurityLogMonitor.stop();
setNetworkLoggingActiveInternal(false);
+ deleteTransferOwnershipBundleLocked(userId);
try {
if (mInjector.getIBackupManager() != null) {
@@ -7057,6 +7428,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
ensureUnknownSourcesRestrictionForProfileOwnerLocked(userHandle, admin,
true /* newOwner */);
}
+ sendOwnerChangedBroadcast(DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED,
+ userHandle);
} finally {
mInjector.binderRestoreCallingIdentity(id);
}
@@ -7085,6 +7458,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
try {
clearProfileOwnerLocked(admin, userId);
removeActiveAdminLocked(who, userId);
+ sendOwnerChangedBroadcast(DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED,
+ userId);
} finally {
mInjector.binderRestoreCallingIdentity(ident);
}
@@ -7107,6 +7482,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
clearUserPoliciesLocked(userId);
mOwners.removeProfileOwner(userId);
mOwners.writeProfileOwner(userId);
+ deleteTransferOwnershipBundleLocked(userId);
}
@Override
@@ -7116,13 +7492,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return;
}
- final int userId = mInjector.userHandleGetCallingUserId();
synchronized (this) {
- 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.");
- }
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
long token = mInjector.binderClearCallingIdentity();
try {
mLockPatternUtils.setDeviceOwnerInfo(info != null ? info.toString() : null);
@@ -7147,6 +7518,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
policy.mUserProvisioningState = DevicePolicyManager.STATE_USER_UNMANAGED;
policy.mAffiliationIds.clear();
policy.mLockTaskPackages.clear();
+ updateLockTaskPackagesLocked(policy.mLockTaskPackages, userId);
policy.mLockTaskFeatures = DevicePolicyManager.LOCK_TASK_FEATURE_NONE;
saveSettingsLocked(userId);
@@ -7580,6 +7952,37 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
enforceSystemUserOrPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL);
}
+ private boolean canUserUseLockTaskLocked(int userId) {
+ if (isUserAffiliatedWithDeviceLocked(userId)) {
+ return true;
+ }
+
+ // Unaffiliated profile owners are not allowed to use lock when there is a device owner.
+ if (mOwners.hasDeviceOwner()) {
+ return false;
+ }
+
+ final ComponentName profileOwner = getProfileOwner(userId);
+ if (profileOwner == null) {
+ return false;
+ }
+
+ // Managed profiles are not allowed to use lock task
+ if (isManagedProfile(userId)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void enforceCanCallLockTaskLocked(ComponentName who) {
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+ final int userId = mInjector.userHandleGetCallingUserId();
+ if (!canUserUseLockTaskLocked(userId)) {
+ throw new SecurityException("User " + userId + " is not allowed to use lock task");
+ }
+ }
+
private void ensureCallerPackage(@Nullable String packageName) {
if (packageName == null) {
Preconditions.checkState(isCallerWithSystemUid(),
@@ -8472,14 +8875,6 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Settings.Secure.USER_SETUP_COMPLETE, 1, userHandle);
}
- if ((flags & START_USER_IN_BACKGROUND) != 0) {
- try {
- mInjector.getIActivityManager().startUserInBackground(userHandle);
- } catch (RemoteException re) {
- // Does not happen, same process
- }
- }
-
return user;
} catch (Throwable re) {
mUserManager.removeUser(userHandle);
@@ -8492,6 +8887,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean removeUser(ComponentName who, UserHandle userHandle) {
Preconditions.checkNotNull(who, "ComponentName is null");
+ Preconditions.checkNotNull(userHandle, "UserHandle is null");
+
synchronized (this) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
}
@@ -8530,6 +8927,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
@Override
public boolean switchUser(ComponentName who, UserHandle userHandle) {
Preconditions.checkNotNull(who, "ComponentName is null");
+
synchronized (this) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
@@ -8550,8 +8948,40 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
@Override
+ public boolean startUserInBackground(ComponentName who, UserHandle userHandle) {
+ Preconditions.checkNotNull(who, "ComponentName is null");
+ Preconditions.checkNotNull(userHandle, "UserHandle 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 started in background");
+ return false;
+ }
+
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ if (!mInjector.getActivityManagerInternal().canStartMoreUsers()) {
+ Log.w(LOG_TAG, "Cannot start more users in background");
+ return false;
+ }
+
+ return mInjector.getIActivityManager().startUserInBackground(userId);
+ } catch (RemoteException e) {
+ // Same process, should not happen.
+ return false;
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ }
+
+ @Override
public boolean stopUser(ComponentName who, UserHandle userHandle) {
Preconditions.checkNotNull(who, "ComponentName is null");
+ Preconditions.checkNotNull(userHandle, "UserHandle is null");
synchronized (this) {
getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
@@ -9259,14 +9689,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(packages, "packages is null");
synchronized (this) {
- getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+ enforceCanCallLockTaskLocked(who);
final int userHandle = mInjector.userHandleGetCallingUserId();
- if (isUserAffiliatedWithDeviceLocked(userHandle)) {
- setLockTaskPackagesLocked(userHandle, new ArrayList<>(Arrays.asList(packages)));
- } else {
- throw new SecurityException("Admin " + who +
- " is neither the device owner or affiliated user's profile owner.");
- }
+ setLockTaskPackagesLocked(userHandle, new ArrayList<>(Arrays.asList(packages)));
}
}
@@ -9285,12 +9710,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final int userHandle = mInjector.binderGetCallingUserHandle().getIdentifier();
synchronized (this) {
- getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
- if (!isUserAffiliatedWithDeviceLocked(userHandle)) {
- throw new SecurityException("Admin " + who +
- " is neither the device owner or affiliated user's profile owner.");
- }
-
+ enforceCanCallLockTaskLocked(who);
final List<String> packages = getUserData(userHandle).mLockTaskPackages;
return packages.toArray(new String[packages.size()]);
}
@@ -9309,11 +9729,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = mInjector.userHandleGetCallingUserId();
synchronized (this) {
- getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
- if (!isUserAffiliatedWithDeviceLocked(userHandle)) {
- throw new SecurityException("Admin " + who +
- " is neither the device owner or affiliated user's profile owner.");
- }
+ enforceCanCallLockTaskLocked(who);
setLockTaskFeaturesLocked(userHandle, flags);
}
}
@@ -9330,11 +9746,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
Preconditions.checkNotNull(who, "ComponentName is null");
final int userHandle = mInjector.userHandleGetCallingUserId();
synchronized (this) {
- getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
- if (!isUserAffiliatedWithDeviceLocked(userHandle)) {
- throw new SecurityException("Admin " + who +
- " is neither the device owner or affiliated user's profile owner.");
- }
+ enforceCanCallLockTaskLocked(who);
return getUserData(userHandle).mLockTaskFeatures;
}
}
@@ -9345,7 +9757,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final List<UserInfo> userInfos = mUserManager.getUsers(/*excludeDying=*/ true);
for (int i = userInfos.size() - 1; i >= 0; i--) {
int userId = userInfos.get(i).id;
- if (isUserAffiliatedWithDeviceLocked(userId)) {
+ if (canUserUseLockTaskLocked(userId)) {
continue;
}
@@ -9589,6 +10001,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
" is neither the device owner or affiliated user's profile owner.");
}
}
+ if (isManagedProfile(userId)) {
+ throw new SecurityException("Managed profile cannot disable keyguard");
+ }
long ident = mInjector.binderClearCallingIdentity();
try {
@@ -9597,7 +10012,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
mLockPatternUtils.setLockScreenDisabled(disabled, userId);
- mInjector.getIWindowManager().dismissKeyguard(null);
+ mInjector.getIWindowManager().dismissKeyguard(null /* callback */, null /* message */);
} catch (RemoteException e) {
// Same process, does not happen.
} finally {
@@ -9615,6 +10030,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
throw new SecurityException("Admin " + who +
" is neither the device owner or affiliated user's profile owner.");
}
+ if (isManagedProfile(userId)) {
+ throw new SecurityException("Managed profile cannot disable status bar");
+ }
DevicePolicyData policy = getUserData(userId);
if (policy.mStatusBarDisabled != disabled) {
boolean isLockTaskMode = false;
@@ -9891,6 +10309,50 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
updateMaximumTimeToLockLocked(userId);
}
}
+
+ @Override
+ public boolean canUserHaveUntrustedCredentialReset(@UserIdInt int userId) {
+ return DevicePolicyManagerService.this.canUserHaveUntrustedCredentialReset(userId);
+ }
+
+ @Override
+ public CharSequence getPrintingDisabledReasonForUser(@UserIdInt int userId) {
+ synchronized (DevicePolicyManagerService.this) {
+ DevicePolicyData policy = getUserData(userId);
+ if (policy.mPrintingEnabled) {
+ Log.e(LOG_TAG, "printing is enabled");
+ return null;
+ }
+ String ownerPackage = mOwners.getProfileOwnerPackage(userId);
+ if (ownerPackage == null) {
+ ownerPackage = mOwners.getDeviceOwnerPackageName();
+ }
+ PackageManager pm = mInjector.getPackageManager();
+ PackageInfo packageInfo;
+ try {
+ packageInfo = pm.getPackageInfo(ownerPackage, 0);
+ } catch (NameNotFoundException e) {
+ Log.e(LOG_TAG, "getPackageInfo error", e);
+ return null;
+ }
+ if (packageInfo == null) {
+ Log.e(LOG_TAG, "packageInfo is inexplicably null");
+ return null;
+ }
+ ApplicationInfo appInfo = packageInfo.applicationInfo;
+ if (appInfo == null) {
+ Log.e(LOG_TAG, "appInfo is inexplicably null");
+ return null;
+ }
+ CharSequence appLabel = pm.getApplicationLabel(appInfo);
+ if (appLabel == null) {
+ Log.e(LOG_TAG, "appLabel is inexplicably null");
+ return null;
+ }
+ return ((Context) ActivityThread.currentActivityThread().getSystemUiContext())
+ .getResources().getString(R.string.printing_disabled_by, appLabel);
+ }
+ }
}
private Intent createShowAdminSupportIntent(ComponentName admin, int userId) {
@@ -9909,7 +10371,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final int userId = UserHandle.getUserId(uid);
Intent intent = null;
if (DevicePolicyManager.POLICY_DISABLE_CAMERA.equals(restriction) ||
- DevicePolicyManager.POLICY_DISABLE_SCREEN_CAPTURE.equals(restriction)) {
+ DevicePolicyManager.POLICY_DISABLE_SCREEN_CAPTURE.equals(restriction) ||
+ DevicePolicyManager.POLICY_MANDATORY_BACKUPS.equals(restriction)) {
synchronized(this) {
final DevicePolicyData policy = getUserData(userId);
final int N = policy.mAdminList.size();
@@ -9918,7 +10381,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if ((admin.disableCamera &&
DevicePolicyManager.POLICY_DISABLE_CAMERA.equals(restriction)) ||
(admin.disableScreenCapture && DevicePolicyManager
- .POLICY_DISABLE_SCREEN_CAPTURE.equals(restriction))) {
+ .POLICY_DISABLE_SCREEN_CAPTURE.equals(restriction)) ||
+ (admin.mandatoryBackupTransport != null && DevicePolicyManager
+ .POLICY_MANDATORY_BACKUPS.equals(restriction))) {
intent = createShowAdminSupportIntent(admin.info.getComponent(), userId);
break;
}
@@ -10708,6 +11173,93 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
@Override
+ public List<String> setMeteredDataDisabled(ComponentName who, List<String> packageNames) {
+ Preconditions.checkNotNull(who);
+ Preconditions.checkNotNull(packageNames);
+
+ if (!mHasFeature) {
+ return packageNames;
+ }
+ synchronized (this) {
+ final ActiveAdmin admin = getActiveAdminForCallerLocked(who,
+ DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+ final int callingUserId = mInjector.userHandleGetCallingUserId();
+ final long identity = mInjector.binderClearCallingIdentity();
+ try {
+ final List<String> excludedPkgs
+ = removeInvalidPkgsForMeteredDataRestriction(callingUserId, packageNames);
+ admin.meteredDisabledPackages = packageNames;
+ pushMeteredDisabledPackagesLocked(callingUserId);
+ saveSettingsLocked(callingUserId);
+ return excludedPkgs;
+ } finally {
+ mInjector.binderRestoreCallingIdentity(identity);
+ }
+ }
+ }
+
+ private List<String> removeInvalidPkgsForMeteredDataRestriction(
+ int userId, List<String> pkgNames) {
+ final Set<String> activeAdmins = getActiveAdminPackagesLocked(userId);
+ final List<String> excludedPkgs = new ArrayList<>();
+ for (int i = pkgNames.size() - 1; i >= 0; --i) {
+ final String pkgName = pkgNames.get(i);
+ // If the package is an active admin, don't restrict it.
+ if (activeAdmins.contains(pkgName)) {
+ excludedPkgs.add(pkgName);
+ continue;
+ }
+ // If the package doesn't exist, don't restrict it.
+ try {
+ if (!mInjector.getIPackageManager().isPackageAvailable(pkgName, userId)) {
+ excludedPkgs.add(pkgName);
+ }
+ } catch (RemoteException e) {
+ // Should not happen
+ }
+ }
+ pkgNames.removeAll(excludedPkgs);
+ return excludedPkgs;
+ }
+
+ @Override
+ public List<String> getMeteredDataDisabled(ComponentName who) {
+ Preconditions.checkNotNull(who);
+
+ if (!mHasFeature) {
+ return new ArrayList<>();
+ }
+ synchronized (this) {
+ final ActiveAdmin admin = getActiveAdminForCallerLocked(who,
+ DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+ return admin.meteredDisabledPackages == null
+ ? new ArrayList<>() : admin.meteredDisabledPackages;
+ }
+ }
+
+ private void pushMeteredDisabledPackagesLocked(int userId) {
+ mInjector.getNetworkPolicyManagerInternal().setMeteredRestrictedPackages(
+ getMeteredDisabledPackagesLocked(userId), userId);
+ }
+
+ private Set<String> getMeteredDisabledPackagesLocked(int userId) {
+ final DevicePolicyData policy = getUserData(userId);
+ final Set<String> restrictedPkgs = new ArraySet<>();
+ for (int i = policy.mAdminList.size() - 1; i >= 0; --i) {
+ final ActiveAdmin admin = policy.mAdminList.get(i);
+ if (!isActiveAdminWithPolicyForUserLocked(admin,
+ DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, userId)) {
+ // Not a profile or device owner, ignore
+ continue;
+ }
+ if (admin.meteredDisabledPackages != null) {
+ restrictedPkgs.addAll(admin.meteredDisabledPackages);
+ }
+ }
+ return restrictedPkgs;
+ }
+
+ @Override
public void setAffiliationIds(ComponentName admin, List<String> ids) {
if (!mHasFeature) {
return;
@@ -10782,10 +11334,12 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
// of a split user device.
return true;
}
+
final ComponentName profileOwner = getProfileOwner(userId);
if (profileOwner == null) {
return false;
}
+
final Set<String> userAffiliationIds = getUserData(userId).mAffiliationIds;
final Set<String> deviceAffiliationIds =
getUserData(UserHandle.USER_SYSTEM).mAffiliationIds;
@@ -11065,6 +11619,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
if (doProxyCleanup) {
resetGlobalProxyLocked(policy);
}
+ pushActiveAdminPackagesLocked(userHandle);
+ pushMeteredDisabledPackagesLocked(userHandle);
saveSettingsLocked(userHandle);
updateMaximumTimeToLockLocked(userHandle);
policy.mRemovingAdmins.remove(adminReceiver);
@@ -11131,7 +11687,12 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
Preconditions.checkNotNull(admin);
synchronized (this) {
- getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(
+ admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ if (!enabled) {
+ activeAdmin.mandatoryBackupTransport = null;
+ saveSettingsLocked(UserHandle.USER_SYSTEM);
+ }
}
final long ident = mInjector.binderClearCallingIdentity();
@@ -11166,6 +11727,50 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
}
@Override
+ public void setMandatoryBackupTransport(
+ ComponentName admin, ComponentName backupTransportComponent) {
+ if (!mHasFeature) {
+ return;
+ }
+ Preconditions.checkNotNull(admin);
+ synchronized (this) {
+ ActiveAdmin activeAdmin =
+ getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ if (!Objects.equals(backupTransportComponent, activeAdmin.mandatoryBackupTransport)) {
+ activeAdmin.mandatoryBackupTransport = backupTransportComponent;
+ saveSettingsLocked(UserHandle.USER_SYSTEM);
+ }
+ }
+ final long identity = mInjector.binderClearCallingIdentity();
+ try {
+ IBackupManager ibm = mInjector.getIBackupManager();
+ if (ibm != null && backupTransportComponent != null) {
+ if (!ibm.isBackupServiceActive(UserHandle.USER_SYSTEM)) {
+ ibm.setBackupServiceActive(UserHandle.USER_SYSTEM, true);
+ }
+ ibm.selectBackupTransportAsync(backupTransportComponent, null);
+ ibm.setBackupEnabled(true);
+ }
+ } catch (RemoteException e) {
+ throw new IllegalStateException("Failed to set mandatory backup transport.", e);
+ } finally {
+ mInjector.binderRestoreCallingIdentity(identity);
+ }
+ }
+
+ @Override
+ public ComponentName getMandatoryBackupTransport() {
+ if (!mHasFeature) {
+ return null;
+ }
+ synchronized (this) {
+ ActiveAdmin activeAdmin = getDeviceOwnerAdminLocked();
+ return activeAdmin == null ? null : activeAdmin.mandatoryBackupTransport;
+ }
+ }
+
+
+ @Override
public boolean bindDeviceAdminServiceAsUser(
@NonNull ComponentName admin, @NonNull IApplicationThread caller,
@Nullable IBinder activtiyToken, @NonNull Intent serviceIntent,
@@ -11737,15 +12342,18 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return;
}
Preconditions.checkNotNull(admin);
- getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
- if (enabled == isLogoutEnabledInternalLocked()) {
- // already in the requested state
- return;
+ synchronized (this) {
+ ActiveAdmin deviceOwner =
+ getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+
+ if (deviceOwner.isLogoutEnabled == enabled) {
+ // already in the requested state
+ return;
+ }
+ deviceOwner.isLogoutEnabled = enabled;
+ saveSettingsLocked(mInjector.userHandleGetCallingUserId());
}
- ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
- deviceOwner.isLogoutEnabled = enabled;
- saveSettingsLocked(mInjector.userHandleGetCallingUserId());
}
@Override
@@ -11754,15 +12362,11 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
return false;
}
synchronized (this) {
- return isLogoutEnabledInternalLocked();
+ ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
+ return (deviceOwner != null) && deviceOwner.isLogoutEnabled;
}
}
- private boolean isLogoutEnabledInternalLocked() {
- ActiveAdmin deviceOwner = getDeviceOwnerAdminLocked();
- return (deviceOwner != null) && deviceOwner.isLogoutEnabled;
- }
-
@Override
public List<String> getDisallowedSystemApps(ComponentName admin, int userId,
String provisioningAction) throws RemoteException {
@@ -11771,10 +12375,9 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
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) {
+ public void transferOwnership(@NonNull ComponentName admin, @NonNull ComponentName target,
+ @Nullable PersistableBundle bundle) {
if (!mHasFeature) {
return;
}
@@ -11799,31 +12402,462 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
final DeviceAdminInfo incomingDeviceInfo = findAdmin(target, callingUserId,
/* throwForMissingPermission= */ true);
checkActiveAdminPrecondition(target, incomingDeviceInfo, policy);
+ if (!incomingDeviceInfo.getActivityInfo().metaData
+ .getBoolean(DeviceAdminReceiver.SUPPORT_TRANSFER_OWNERSHIP_META_DATA, false)) {
+ throw new IllegalArgumentException("Provided target does not support "
+ + "ownership transfer.");
+ }
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);
+ synchronized (this) {
+ /*
+ * We must ensure the whole process is atomic to prevent the device from ending up
+ * in an invalid state (e.g. no active admin). This could happen if the device
+ * is rebooted or work mode is turned off mid-transfer.
+ * In order to guarantee atomicity, we:
+ *
+ * 1. Save an atomic journal file describing the transfer process
+ * 2. Perform the transfer itself
+ * 3. Delete the journal file
+ *
+ * That way if the journal file exists on device boot, we know that the transfer
+ * must be reverted back to the original administrator. This logic is implemented in
+ * revertTransferOwnershipIfNecessaryLocked.
+ * */
+ if (bundle == null) {
+ bundle = new PersistableBundle();
+ }
+ if (isProfileOwner(admin, callingUserId)) {
+ prepareTransfer(admin, target, bundle, callingUserId,
+ ADMIN_TYPE_PROFILE_OWNER);
+ transferProfileOwnershipLocked(admin, target, callingUserId);
+ sendProfileOwnerCommand(DeviceAdminReceiver.ACTION_TRANSFER_OWNERSHIP_COMPLETE,
+ getTransferOwnershipAdminExtras(bundle), callingUserId);
+ postTransfer(DevicePolicyManager.ACTION_PROFILE_OWNER_CHANGED, callingUserId);
+ if (isUserAffiliatedWithDeviceLocked(callingUserId)) {
+ notifyAffiliatedProfileTransferOwnershipComplete(callingUserId);
+ }
+ } else if (isDeviceOwner(admin, callingUserId)) {
+ prepareTransfer(admin, target, bundle, callingUserId,
+ ADMIN_TYPE_DEVICE_OWNER);
+ transferDeviceOwnershipLocked(admin, target, callingUserId);
+ sendDeviceOwnerCommand(DeviceAdminReceiver.ACTION_TRANSFER_OWNERSHIP_COMPLETE,
+ getTransferOwnershipAdminExtras(bundle));
+ postTransfer(DevicePolicyManager.ACTION_DEVICE_OWNER_CHANGED, callingUserId);
+ }
}
} finally {
mInjector.binderRestoreCallingIdentity(id);
}
}
+ private void prepareTransfer(ComponentName admin, ComponentName target,
+ PersistableBundle bundle, int callingUserId, String adminType) {
+ saveTransferOwnershipBundleLocked(bundle, callingUserId);
+ mTransferOwnershipMetadataManager.saveMetadataFile(
+ new TransferOwnershipMetadataManager.Metadata(admin, target,
+ callingUserId, adminType));
+ }
+
+ private void postTransfer(String broadcast, int callingUserId) {
+ deleteTransferOwnershipMetadataFileLocked();
+ sendOwnerChangedBroadcast(broadcast, callingUserId);
+ }
+
+ private void notifyAffiliatedProfileTransferOwnershipComplete(int callingUserId) {
+ final Bundle extras = new Bundle();
+ extras.putParcelable(Intent.EXTRA_USER, UserHandle.of(callingUserId));
+ sendDeviceOwnerCommand(
+ DeviceAdminReceiver.ACTION_AFFILIATED_PROFILE_TRANSFER_OWNERSHIP_COMPLETE, extras);
+ }
+
/**
* Transfers the profile owner for user with id profileOwnerUserId from admin to target.
*/
- private void transferProfileOwner(ComponentName admin, ComponentName target,
+ private void transferProfileOwnershipLocked(ComponentName admin, ComponentName target,
int profileOwnerUserId) {
+ 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");
+ }
+
+ /**
+ * Transfers the device owner for user with id userId from admin to target.
+ */
+ private void transferDeviceOwnershipLocked(ComponentName admin, ComponentName target, int userId) {
+ transferActiveAdminUncheckedLocked(target, admin, userId);
+ mOwners.transferDeviceOwnership(target);
+ Slog.i(LOG_TAG, "Device owner set: " + target + " on user " + userId);
+ mOwners.writeDeviceOwner();
+ mDeviceAdminServiceController.startServiceForOwner(
+ target.getPackageName(), userId, "transfer-device-owner");
+ }
+
+ private Bundle getTransferOwnershipAdminExtras(PersistableBundle bundle) {
+ Bundle extras = new Bundle();
+ if (bundle != null) {
+ extras.putParcelable(EXTRA_TRANSFER_OWNERSHIP_ADMIN_EXTRAS_BUNDLE, bundle);
+ }
+ return extras;
+ }
+
+ @Override
+ public void setStartUserSessionMessage(
+ ComponentName admin, CharSequence startUserSessionMessage) {
+ if (!mHasFeature) {
+ return;
+ }
+ Preconditions.checkNotNull(admin);
+
+ final String startUserSessionMessageString =
+ startUserSessionMessage != null ? startUserSessionMessage.toString() : null;
+
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");
+ final ActiveAdmin deviceOwner =
+ getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+
+ if (TextUtils.equals(deviceOwner.startUserSessionMessage, startUserSessionMessage)) {
+ return;
+ }
+ deviceOwner.startUserSessionMessage = startUserSessionMessageString;
+ saveSettingsLocked(mInjector.userHandleGetCallingUserId());
+ }
+
+ mInjector.getActivityManagerInternal()
+ .setSwitchingFromSystemUserMessage(startUserSessionMessageString);
+ }
+
+ @Override
+ public void setEndUserSessionMessage(ComponentName admin, CharSequence endUserSessionMessage) {
+ if (!mHasFeature) {
+ return;
+ }
+ Preconditions.checkNotNull(admin);
+
+ final String endUserSessionMessageString =
+ endUserSessionMessage != null ? endUserSessionMessage.toString() : null;
+
+ synchronized (this) {
+ final ActiveAdmin deviceOwner =
+ getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+
+ if (TextUtils.equals(deviceOwner.endUserSessionMessage, endUserSessionMessage)) {
+ return;
+ }
+ deviceOwner.endUserSessionMessage = endUserSessionMessageString;
+ saveSettingsLocked(mInjector.userHandleGetCallingUserId());
+ }
+
+ mInjector.getActivityManagerInternal()
+ .setSwitchingToSystemUserMessage(endUserSessionMessageString);
+ }
+
+ @Override
+ public String getStartUserSessionMessage(ComponentName admin) {
+ if (!mHasFeature) {
+ return null;
+ }
+ Preconditions.checkNotNull(admin);
+
+ synchronized (this) {
+ final ActiveAdmin deviceOwner =
+ getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ return deviceOwner.startUserSessionMessage;
+ }
+ }
+
+ @Override
+ public String getEndUserSessionMessage(ComponentName admin) {
+ if (!mHasFeature) {
+ return null;
+ }
+ Preconditions.checkNotNull(admin);
+
+ synchronized (this) {
+ final ActiveAdmin deviceOwner =
+ getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ return deviceOwner.endUserSessionMessage;
+ }
+ }
+
+ private boolean hasPrinting() {
+ return mInjector.getPackageManager().hasSystemFeature(PackageManager.FEATURE_PRINTING);
+ }
+
+ @Override
+ public void setPrintingEnabled(ComponentName admin, boolean enabled) {
+ if (!mHasFeature || !hasPrinting()) {
+ return;
+ }
+ Preconditions.checkNotNull(admin, "Admin cannot be null.");
+ enforceProfileOrDeviceOwner(admin);
+ synchronized (this) {
+ final int userHandle = mInjector.userHandleGetCallingUserId();
+ DevicePolicyData policy = getUserData(userHandle);
+ if (policy.mPrintingEnabled != enabled) {
+ policy.mPrintingEnabled = enabled;
+ saveSettingsLocked(userHandle);
+ }
+ }
+ }
+
+ private void deleteTransferOwnershipMetadataFileLocked() {
+ mTransferOwnershipMetadataManager.deleteMetadataFile();
+ }
+
+ @Override
+ @Nullable
+ public PersistableBundle getTransferOwnershipBundle() {
+ synchronized (this) {
+ final int callingUserId = mInjector.userHandleGetCallingUserId();
+ getActiveAdminForCallerLocked(null, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+ final File bundleFile = new File(
+ mInjector.environmentGetUserSystemDirectory(callingUserId),
+ TRANSFER_OWNERSHIP_PARAMETERS_XML);
+ if (!bundleFile.exists()) {
+ return null;
+ }
+ try (FileInputStream stream = new FileInputStream(bundleFile)) {
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(stream, null);
+ return PersistableBundle.restoreFromXml(parser);
+ } catch (IOException | XmlPullParserException | IllegalArgumentException e) {
+ Slog.e(LOG_TAG, "Caught exception while trying to load the "
+ + "owner transfer parameters from file " + bundleFile, e);
+ return null;
+ }
+ }
+ }
+
+ /**
+ * Returns whether printing is enabled for current user.
+ * @hide
+ */
+ @Override
+ public boolean isPrintingEnabled() {
+ if (!hasPrinting()) {
+ return false;
+ }
+ if (!mHasFeature) {
+ return true;
+ }
+ synchronized (this) {
+ final int userHandle = mInjector.userHandleGetCallingUserId();
+ DevicePolicyData policy = getUserData(userHandle);
+ return policy.mPrintingEnabled;
+ }
+ }
+
+ @Override
+ public int addOverrideApn(@NonNull ComponentName who, @NonNull ApnSetting apnSetting) {
+ if (!mHasFeature) {
+ return -1;
+ }
+ Preconditions.checkNotNull(who, "ComponentName is null in addOverrideApn");
+ Preconditions.checkNotNull(apnSetting, "ApnSetting is null in addOverrideApn");
+ synchronized (this) {
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ }
+
+ int operatedId = -1;
+ Uri resultUri;
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ resultUri = mContext.getContentResolver().insert(DPC_URI, apnSetting.toContentValues());
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ if (resultUri != null) {
+ try {
+ operatedId = Integer.parseInt(resultUri.getLastPathSegment());
+ } catch (NumberFormatException e) {
+ Slog.e(LOG_TAG, "Failed to parse inserted override APN id.", e);
+ }
}
+
+ return operatedId;
+ }
+
+ @Override
+ public boolean updateOverrideApn(@NonNull ComponentName who, int apnId,
+ @NonNull ApnSetting apnSetting) {
+ if (!mHasFeature) {
+ return false;
+ }
+ Preconditions.checkNotNull(who, "ComponentName is null in updateOverrideApn");
+ Preconditions.checkNotNull(apnSetting, "ApnSetting is null in updateOverrideApn");
+ synchronized (this) {
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ }
+
+ if (apnId < 0) {
+ return false;
+ }
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ return mContext.getContentResolver().update(
+ Uri.withAppendedPath(DPC_URI, Integer.toString(apnId)),
+ apnSetting.toContentValues(), null, null) > 0;
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ }
+
+ @Override
+ public boolean removeOverrideApn(@NonNull ComponentName who, int apnId) {
+ if (!mHasFeature) {
+ return false;
+ }
+ Preconditions.checkNotNull(who, "ComponentName is null in removeOverrideApn");
+ synchronized (this) {
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ }
+
+ return removeOverrideApnUnchecked(apnId);
+ }
+
+ private boolean removeOverrideApnUnchecked(int apnId) {
+ if(apnId < 0) {
+ return false;
+ }
+ int numDeleted = 0;
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ numDeleted = mContext.getContentResolver().delete(
+ Uri.withAppendedPath(DPC_URI, Integer.toString(apnId)), null, null);
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ return numDeleted > 0;
+ }
+
+ @Override
+ public List<ApnSetting> getOverrideApns(@NonNull ComponentName who) {
+ if (!mHasFeature) {
+ return Collections.emptyList();
+ }
+ Preconditions.checkNotNull(who, "ComponentName is null in getOverrideApns");
+ synchronized (this) {
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ }
+
+ return getOverrideApnsUnchecked();
+ }
+
+ private List<ApnSetting> getOverrideApnsUnchecked() {
+ final Cursor cursor;
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ cursor = mContext.getContentResolver().query(DPC_URI, null, null, null, null);
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+
+ if (cursor == null) {
+ return Collections.emptyList();
+ }
+ try {
+ List<ApnSetting> apnList = new ArrayList<ApnSetting>();
+ cursor.moveToPosition(-1);
+ while (cursor.moveToNext()) {
+ ApnSetting apn = ApnSetting.makeApnSetting(cursor);
+ apnList.add(apn);
+ }
+ return apnList;
+ } finally {
+ cursor.close();
+ }
+ }
+
+ @Override
+ public void setOverrideApnsEnabled(@NonNull ComponentName who, boolean enabled) {
+ if (!mHasFeature) {
+ return;
+ }
+ Preconditions.checkNotNull(who, "ComponentName is null in setOverrideApnEnabled");
+ synchronized (this) {
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ }
+
+ setOverrideApnsEnabledUnchecked(enabled);
+ }
+
+ private void setOverrideApnsEnabledUnchecked(boolean enabled) {
+ ContentValues value = new ContentValues();
+ value.put(ENFORCE_KEY, enabled);
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ mContext.getContentResolver().update(
+ ENFORCE_MANAGED_URI, value, null, null);
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+ }
+
+ @Override
+ public boolean isOverrideApnEnabled(@NonNull ComponentName who) {
+ if (!mHasFeature) {
+ return false;
+ }
+ Preconditions.checkNotNull(who, "ComponentName is null in isOverrideApnEnabled");
+ synchronized (this) {
+ getActiveAdminForCallerLocked(who, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+ }
+
+ Cursor enforceCursor;
+ final long id = mInjector.binderClearCallingIdentity();
+ try {
+ enforceCursor = mContext.getContentResolver().query(
+ ENFORCE_MANAGED_URI, null, null, null, null);
+ } finally {
+ mInjector.binderRestoreCallingIdentity(id);
+ }
+
+ if (enforceCursor == null) {
+ return false;
+ }
+ try {
+ if (enforceCursor.moveToFirst()) {
+ return enforceCursor.getInt(enforceCursor.getColumnIndex(ENFORCE_KEY)) == 1;
+ }
+ } catch (IllegalArgumentException e) {
+ Slog.e(LOG_TAG, "Cursor returned from ENFORCE_MANAGED_URI doesn't contain "
+ + "correct info.", e);
+ } finally {
+ enforceCursor.close();
+ }
+ return false;
+ }
+
+ @VisibleForTesting
+ void saveTransferOwnershipBundleLocked(PersistableBundle bundle, int userId) {
+ final File parametersFile = new File(
+ mInjector.environmentGetUserSystemDirectory(userId),
+ TRANSFER_OWNERSHIP_PARAMETERS_XML);
+ final AtomicFile atomicFile = new AtomicFile(parametersFile);
+ FileOutputStream stream = null;
+ try {
+ stream = atomicFile.startWrite();
+ final XmlSerializer serializer = new FastXmlSerializer();
+ serializer.setOutput(stream, StandardCharsets.UTF_8.name());
+ serializer.startDocument(null, true);
+ bundle.saveToXml(serializer);
+ atomicFile.finishWrite(stream);
+ } catch (IOException | XmlPullParserException e) {
+ Slog.e(LOG_TAG, "Caught exception while trying to save the "
+ + "owner transfer parameters to file " + parametersFile, e);
+ parametersFile.delete();
+ atomicFile.failWrite(stream);
+ }
+ }
+
+ void deleteTransferOwnershipBundleLocked(int userId) {
+ final File parametersFile = new File(mInjector.environmentGetUserSystemDirectory(userId),
+ TRANSFER_OWNERSHIP_PARAMETERS_XML);
+ parametersFile.delete();
}
}
diff --git a/com/android/server/devicepolicy/Owners.java b/com/android/server/devicepolicy/Owners.java
index 9042a8d8..d2151ed8 100644
--- a/com/android/server/devicepolicy/Owners.java
+++ b/com/android/server/devicepolicy/Owners.java
@@ -34,6 +34,7 @@ import android.util.Slog;
import android.util.SparseArray;
import android.util.Xml;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.FastXmlSerializer;
import org.xmlpull.v1.XmlPullParser;
@@ -110,13 +111,23 @@ class Owners {
private SystemUpdateInfo mSystemUpdateInfo;
private final Object mLock = new Object();
+ private final Injector mInjector;
public Owners(UserManager userManager,
UserManagerInternal userManagerInternal,
PackageManagerInternal packageManagerInternal) {
+ this(userManager, userManagerInternal, packageManagerInternal, new Injector());
+ }
+
+ @VisibleForTesting
+ Owners(UserManager userManager,
+ UserManagerInternal userManagerInternal,
+ PackageManagerInternal packageManagerInternal,
+ Injector injector) {
mUserManager = userManager;
mUserManagerInternal = userManagerInternal;
mPackageManagerInternal = packageManagerInternal;
+ mInjector = injector;
}
/**
@@ -125,7 +136,7 @@ class Owners {
void load() {
synchronized (mLock) {
// First, try to read from the legacy file.
- final File legacy = getLegacyConfigFileWithTestOverride();
+ final File legacy = getLegacyConfigFile();
final List<UserInfo> users = mUserManager.getUsers(true);
@@ -288,6 +299,17 @@ class Owners {
}
}
+ void transferDeviceOwnership(ComponentName target) {
+ synchronized (mLock) {
+ // We don't set a name because it's not used anyway.
+ // See DevicePolicyManagerService#getDeviceOwnerName
+ mDeviceOwner = new OwnerInfo(null, target,
+ mDeviceOwner.userRestrictionsMigrated, mDeviceOwner.remoteBugreportUri,
+ mDeviceOwner.remoteBugreportHash);
+ pushToPackageManagerLocked();
+ }
+ }
+
ComponentName getProfileOwnerComponent(int userId) {
synchronized (mLock) {
OwnerInfo profileOwner = mProfileOwners.get(userId);
@@ -631,7 +653,7 @@ class Owners {
private class DeviceOwnerReadWriter extends FileReadWriter {
protected DeviceOwnerReadWriter() {
- super(getDeviceOwnerFileWithTestOverride());
+ super(getDeviceOwnerFile());
}
@Override
@@ -702,7 +724,7 @@ class Owners {
private final int mUserId;
ProfileOwnerReadWriter(int userId) {
- super(getProfileOwnerFileWithTestOverride(userId));
+ super(getProfileOwnerFile(userId));
mUserId = userId;
}
@@ -859,15 +881,29 @@ class Owners {
}
}
- File getLegacyConfigFileWithTestOverride() {
- return new File(Environment.getDataSystemDirectory(), DEVICE_OWNER_XML_LEGACY);
+ @VisibleForTesting
+ File getLegacyConfigFile() {
+ return new File(mInjector.environmentGetDataSystemDirectory(), DEVICE_OWNER_XML_LEGACY);
+ }
+
+ @VisibleForTesting
+ File getDeviceOwnerFile() {
+ return new File(mInjector.environmentGetDataSystemDirectory(), DEVICE_OWNER_XML);
}
- File getDeviceOwnerFileWithTestOverride() {
- return new File(Environment.getDataSystemDirectory(), DEVICE_OWNER_XML);
+ @VisibleForTesting
+ File getProfileOwnerFile(int userId) {
+ return new File(mInjector.environmentGetUserSystemDirectory(userId), PROFILE_OWNER_XML);
}
- File getProfileOwnerFileWithTestOverride(int userId) {
- return new File(Environment.getUserSystemDirectory(userId), PROFILE_OWNER_XML);
+ @VisibleForTesting
+ public static class Injector {
+ File environmentGetDataSystemDirectory() {
+ return Environment.getDataSystemDirectory();
+ }
+
+ File environmentGetUserSystemDirectory(int userId) {
+ return Environment.getUserSystemDirectory(userId);
+ }
}
}
diff --git a/com/android/server/devicepolicy/TransferOwnershipMetadataManager.java b/com/android/server/devicepolicy/TransferOwnershipMetadataManager.java
new file mode 100644
index 00000000..1addeb6b
--- /dev/null
+++ b/com/android/server/devicepolicy/TransferOwnershipMetadataManager.java
@@ -0,0 +1,227 @@
+/*
+ * 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 com.android.server.devicepolicy;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.os.Environment;
+import android.text.TextUtils;
+import android.util.AtomicFile;
+import android.util.Slog;
+import android.util.Xml;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.FastXmlSerializer;
+import com.android.internal.util.Preconditions;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Handles reading and writing of the owner transfer metadata file.
+ *
+ * Before we perform a device or profile owner transfer, we save this xml file with information
+ * about the current admin, target admin, user id and admin type (device owner or profile owner).
+ * After {@link DevicePolicyManager#transferOwnership} completes, we delete the file. If after
+ * device boot the file is still there, this indicates that the transfer was interrupted by a
+ * reboot.
+ *
+ * Note that this class is not thread safe.
+ */
+class TransferOwnershipMetadataManager {
+ final static String ADMIN_TYPE_DEVICE_OWNER = "device-owner";
+ final static String ADMIN_TYPE_PROFILE_OWNER = "profile-owner";
+ private final static String TAG_USER_ID = "user-id";
+ private final static String TAG_SOURCE_COMPONENT = "source-component";
+ private final static String TAG_TARGET_COMPONENT = "target-component";
+ private final static String TAG_ADMIN_TYPE = "admin-type";
+ private final static String TAG = TransferOwnershipMetadataManager.class.getName();
+ public static final String OWNER_TRANSFER_METADATA_XML = "owner-transfer-metadata.xml";
+
+ private final Injector mInjector;
+
+ TransferOwnershipMetadataManager() {
+ this(new Injector());
+ }
+
+ @VisibleForTesting
+ TransferOwnershipMetadataManager(Injector injector) {
+ mInjector = injector;
+ }
+
+ boolean saveMetadataFile(Metadata params) {
+ final File transferOwnershipMetadataFile = new File(mInjector.getOwnerTransferMetadataDir(),
+ OWNER_TRANSFER_METADATA_XML);
+ final AtomicFile atomicFile = new AtomicFile(transferOwnershipMetadataFile);
+ FileOutputStream stream = null;
+ try {
+ stream = atomicFile.startWrite();
+ final XmlSerializer serializer = new FastXmlSerializer();
+ serializer.setOutput(stream, StandardCharsets.UTF_8.name());
+ serializer.startDocument(null, true);
+ insertSimpleTag(serializer, TAG_USER_ID, Integer.toString(params.userId));
+ insertSimpleTag(serializer,
+ TAG_SOURCE_COMPONENT, params.sourceComponent.flattenToString());
+ insertSimpleTag(serializer,
+ TAG_TARGET_COMPONENT, params.targetComponent.flattenToString());
+ insertSimpleTag(serializer, TAG_ADMIN_TYPE, params.adminType);
+ serializer.endDocument();
+ atomicFile.finishWrite(stream);
+ return true;
+ } catch (IOException e) {
+ Slog.e(TAG, "Caught exception while trying to save Owner Transfer "
+ + "Params to file " + transferOwnershipMetadataFile, e);
+ transferOwnershipMetadataFile.delete();
+ atomicFile.failWrite(stream);
+ }
+ return false;
+ }
+
+ private void insertSimpleTag(XmlSerializer serializer, String tagName, String value)
+ throws IOException {
+ serializer.startTag(null, tagName);
+ serializer.text(value);
+ serializer.endTag(null, tagName);
+ }
+
+ @Nullable
+ Metadata loadMetadataFile() {
+ final File transferOwnershipMetadataFile =
+ new File(mInjector.getOwnerTransferMetadataDir(), OWNER_TRANSFER_METADATA_XML);
+ if (!transferOwnershipMetadataFile.exists()) {
+ return null;
+ }
+ Slog.d(TAG, "Loading TransferOwnershipMetadataManager from "
+ + transferOwnershipMetadataFile);
+ try (FileInputStream stream = new FileInputStream(transferOwnershipMetadataFile)) {
+ final XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(stream, null);
+ return parseMetadataFile(parser);
+ } catch (IOException | XmlPullParserException | IllegalArgumentException e) {
+ Slog.e(TAG, "Caught exception while trying to load the "
+ + "owner transfer params from file " + transferOwnershipMetadataFile, e);
+ }
+ return null;
+ }
+
+ private Metadata parseMetadataFile(XmlPullParser parser)
+ throws XmlPullParserException, IOException {
+ int type;
+ final int outerDepth = parser.getDepth();
+ int userId = 0;
+ String adminComponent = null;
+ String targetComponent = null;
+ String adminType = null;
+ while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
+ && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
+ if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+ continue;
+ }
+ switch (parser.getName()) {
+ case TAG_USER_ID:
+ parser.next();
+ userId = Integer.parseInt(parser.getText());
+ break;
+ case TAG_TARGET_COMPONENT:
+ parser.next();
+ targetComponent = parser.getText();
+ break;
+ case TAG_SOURCE_COMPONENT:
+ parser.next();
+ adminComponent = parser.getText();
+ break;
+ case TAG_ADMIN_TYPE:
+ parser.next();
+ adminType = parser.getText();
+ break;
+ }
+ }
+ return new Metadata(adminComponent, targetComponent, userId, adminType);
+ }
+
+ void deleteMetadataFile() {
+ new File(mInjector.getOwnerTransferMetadataDir(), OWNER_TRANSFER_METADATA_XML).delete();
+ }
+
+ boolean metadataFileExists() {
+ return new File(mInjector.getOwnerTransferMetadataDir(),
+ OWNER_TRANSFER_METADATA_XML).exists();
+ }
+
+ static class Metadata {
+ final int userId;
+ final ComponentName sourceComponent;
+ final ComponentName targetComponent;
+ final String adminType;
+
+ Metadata(@NonNull String sourceComponent, @NonNull String targetComponent,
+ @NonNull int userId, @NonNull String adminType) {
+ this.sourceComponent = ComponentName.unflattenFromString(sourceComponent);
+ this.targetComponent = ComponentName.unflattenFromString(targetComponent);
+ Preconditions.checkNotNull(sourceComponent);
+ Preconditions.checkNotNull(targetComponent);
+ Preconditions.checkStringNotEmpty(adminType);
+ this.userId = userId;
+ this.adminType = adminType;
+ }
+
+ Metadata(@NonNull ComponentName sourceComponent, @NonNull ComponentName targetComponent,
+ @NonNull int userId, @NonNull String adminType) {
+ this(sourceComponent.flattenToString(), targetComponent.flattenToString(),
+ userId, adminType);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Metadata)) {
+ return false;
+ }
+ Metadata params = (Metadata) obj;
+
+ return userId == params.userId
+ && sourceComponent.equals(params.sourceComponent)
+ && targetComponent.equals(params.targetComponent)
+ && TextUtils.equals(adminType, params.adminType);
+ }
+
+ @Override
+ public int hashCode() {
+ int hashCode = 1;
+ hashCode = 31 * hashCode + userId;
+ hashCode = 31 * hashCode + sourceComponent.hashCode();
+ hashCode = 31 * hashCode + targetComponent.hashCode();
+ hashCode = 31 * hashCode + adminType.hashCode();
+ return hashCode;
+ }
+ }
+
+ @VisibleForTesting
+ static class Injector {
+ public File getOwnerTransferMetadataDir() {
+ return Environment.getDataSystemDirectory();
+ }
+ }
+}
diff --git a/com/android/server/display/AutomaticBrightnessController.java b/com/android/server/display/AutomaticBrightnessController.java
index 6c5bfc79..6a88b1e1 100644
--- a/com/android/server/display/AutomaticBrightnessController.java
+++ b/com/android/server/display/AutomaticBrightnessController.java
@@ -25,6 +25,7 @@ import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.hardware.display.BrightnessConfiguration;
+import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -57,8 +58,16 @@ class AutomaticBrightnessController {
// the user is satisfied with the result before storing the sample.
private static final int BRIGHTNESS_ADJUSTMENT_SAMPLE_DEBOUNCE_MILLIS = 10000;
+ // Timeout after which we remove the effects any user interactions might've had on the
+ // brightness mapping. This timeout doesn't start until we transition to a non-interactive
+ // display policy so that we don't reset while users are using their devices, but also so that
+ // we don't erroneously keep the short-term model if the device is dozing but the display is
+ // fully on.
+ private static final int SHORT_TERM_MODEL_TIMEOUT_MILLIS = 30000;
+
private static final int MSG_UPDATE_AMBIENT_LUX = 1;
private static final int MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE = 2;
+ private static final int MSG_RESET_SHORT_TERM_MODEL = 3;
// Length of the ambient light horizon used to calculate the long term estimate of ambient
// light.
@@ -173,8 +182,9 @@ class AutomaticBrightnessController {
// The last screen auto-brightness gamma. (For printing in dump() only.)
private float mLastScreenAutoBrightnessGamma = 1.0f;
- // Are we going to adjust brightness while dozing.
- private boolean mDozing;
+ // The current display policy. This is useful, for example, for knowing when we're dozing,
+ // where the light sensor may not be available.
+ private int mDisplayPolicy = DisplayPowerRequest.POLICY_OFF;
// True if we are collecting a brightness adjustment sample, along with some data
// for the initial state of the sample.
@@ -221,31 +231,85 @@ class AutomaticBrightnessController {
}
public int getAutomaticScreenBrightness() {
- if (mDozing) {
+ if (mDisplayPolicy == DisplayPowerRequest.POLICY_DOZE) {
return (int) (mScreenAutoBrightness * mDozeScaleFactor);
}
return mScreenAutoBrightness;
}
+ public float getAutomaticScreenBrightnessAdjustment() {
+ return mScreenAutoBrightnessAdjustment;
+ }
+
public void configure(boolean enable, @Nullable BrightnessConfiguration configuration,
- float adjustment, boolean dozing, boolean userInitiatedChange) {
+ float brightness, boolean userChangedBrightness, float adjustment,
+ boolean userChangedAutoBrightnessAdjustment, int displayPolicy) {
// 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 dozing = (displayPolicy == DisplayPowerRequest.POLICY_DOZE);
boolean changed = setBrightnessConfiguration(configuration);
- changed |= setLightSensorEnabled(enable && !dozing);
- if (enable && !dozing && userInitiatedChange) {
+ changed |= setDisplayPolicy(displayPolicy);
+ changed |= setScreenAutoBrightnessAdjustment(adjustment);
+ if (userChangedBrightness && enable) {
+ // Update the brightness curve with the new user control point. It's critical this
+ // happens after we update the autobrightness adjustment since it may reset it.
+ changed |= setScreenBrightnessByUser(brightness);
+ }
+ final boolean userInitiatedChange =
+ userChangedBrightness || userChangedAutoBrightnessAdjustment;
+ if (userInitiatedChange && enable && !dozing) {
prepareBrightnessAdjustmentSample();
}
- changed |= setScreenAutoBrightnessAdjustment(adjustment);
+ changed |= setLightSensorEnabled(enable && !dozing);
if (changed) {
updateAutoBrightness(false /*sendUpdate*/);
}
}
+ private boolean setDisplayPolicy(int policy) {
+ if (mDisplayPolicy == policy) {
+ return false;
+ }
+ final int oldPolicy = mDisplayPolicy;
+ mDisplayPolicy = policy;
+ if (DEBUG) {
+ Slog.d(TAG, "Display policy transitioning from " + mDisplayPolicy + " to " + policy);
+ }
+ if (!isInteractivePolicy(policy) && isInteractivePolicy(oldPolicy)) {
+ mHandler.sendEmptyMessageDelayed(MSG_RESET_SHORT_TERM_MODEL,
+ SHORT_TERM_MODEL_TIMEOUT_MILLIS);
+ } else if (isInteractivePolicy(policy) && !isInteractivePolicy(oldPolicy)) {
+ mHandler.removeMessages(MSG_RESET_SHORT_TERM_MODEL);
+ }
+ return true;
+ }
+
+ private static boolean isInteractivePolicy(int policy) {
+ return policy == DisplayPowerRequest.POLICY_BRIGHT
+ || policy == DisplayPowerRequest.POLICY_DIM
+ || policy == DisplayPowerRequest.POLICY_VR;
+ }
+
+ private boolean setScreenBrightnessByUser(float brightness) {
+ if (!mAmbientLuxValid) {
+ // If we don't have a valid ambient lux then we don't have a valid brightness anyways,
+ // and we can't use this data to add a new control point to the short-term model.
+ return false;
+ }
+ mBrightnessMapper.addUserDataPoint(mAmbientLux, brightness);
+ // Reset the brightness adjustment so that the next time we're queried for brightness we
+ // return the value the user set.
+ mScreenAutoBrightnessAdjustment = 0.0f;
+ return true;
+ }
+
+ private void resetShortTermModel() {
+ mBrightnessMapper.clearUserDataPoints();
+ }
+
public boolean setBrightnessConfiguration(BrightnessConfiguration configuration) {
return mBrightnessMapper.setBrightnessConfiguration(configuration);
}
@@ -280,7 +344,7 @@ class AutomaticBrightnessController {
pw.println(" mScreenAutoBrightnessAdjustmentMaxGamma="
+ mScreenAutoBrightnessAdjustmentMaxGamma);
pw.println(" mLastScreenAutoBrightnessGamma=" + mLastScreenAutoBrightnessGamma);
- pw.println(" mDozing=" + mDozing);
+ pw.println(" mDisplayPolicy=" + mDisplayPolicy);
pw.println();
mBrightnessMapper.dump(pw);
@@ -364,6 +428,10 @@ class AutomaticBrightnessController {
if (DEBUG) {
Slog.d(TAG, "setAmbientLux(" + lux + ")");
}
+ if (lux < 0) {
+ Slog.w(TAG, "Ambient lux was negative, ignoring and setting to 0.");
+ lux = 0;
+ }
mAmbientLux = lux;
mBrighteningLuxThreshold = mDynamicHysteresis.getBrighteningThreshold(lux);
mDarkeningLuxThreshold = mDynamicHysteresis.getDarkeningThreshold(lux);
@@ -647,6 +715,10 @@ class AutomaticBrightnessController {
case MSG_BRIGHTNESS_ADJUSTMENT_SAMPLE:
collectBrightnessAdjustmentSample();
break;
+
+ case MSG_RESET_SHORT_TERM_MODEL:
+ resetShortTermModel();
+ break;
}
}
}
diff --git a/com/android/server/display/BrightnessMappingStrategy.java b/com/android/server/display/BrightnessMappingStrategy.java
index 3b9d40fa..001d4d90 100644
--- a/com/android/server/display/BrightnessMappingStrategy.java
+++ b/com/android/server/display/BrightnessMappingStrategy.java
@@ -17,6 +17,8 @@
package com.android.server.display;
import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
import android.hardware.display.BrightnessConfiguration;
import android.os.PowerManager;
import android.util.MathUtils;
@@ -28,6 +30,7 @@ import com.android.internal.util.Preconditions;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
+import java.util.Arrays;
/**
* A utility to map from an ambient brightness to a display's "backlight" brightness based on the
@@ -40,12 +43,34 @@ public abstract class BrightnessMappingStrategy {
private static final String TAG = "BrightnessMappingStrategy";
private static final boolean DEBUG = false;
+ private static final float LUX_GRAD_SMOOTHING = 0.25f;
+ private static final float MAX_GRAD = 1.0f;
+
@Nullable
- public static BrightnessMappingStrategy create(
- float[] luxLevels, int[] brightnessLevelsBacklight, float[] brightnessLevelsNits,
- float[] nitsRange, int[] backlightRange) {
+ public static BrightnessMappingStrategy create(Resources resources) {
+ float[] luxLevels = getLuxLevels(resources.getIntArray(
+ com.android.internal.R.array.config_autoBrightnessLevels));
+ int[] brightnessLevelsBacklight = resources.getIntArray(
+ com.android.internal.R.array.config_autoBrightnessLcdBacklightValues);
+ float[] brightnessLevelsNits = getFloatArray(resources.obtainTypedArray(
+ com.android.internal.R.array.config_autoBrightnessDisplayValuesNits));
+
+ float[] nitsRange = getFloatArray(resources.obtainTypedArray(
+ com.android.internal.R.array.config_screenBrightnessNits));
+ int[] backlightRange = resources.getIntArray(
+ com.android.internal.R.array.config_screenBrightnessBacklight);
+
if (isValidMapping(nitsRange, backlightRange)
&& isValidMapping(luxLevels, brightnessLevelsNits)) {
+ int minimumBacklight = resources.getInteger(
+ com.android.internal.R.integer.config_screenBrightnessSettingMinimum);
+ int maximumBacklight = resources.getInteger(
+ com.android.internal.R.integer.config_screenBrightnessSettingMaximum);
+ if (backlightRange[0] > minimumBacklight
+ || backlightRange[backlightRange.length - 1] < maximumBacklight) {
+ Slog.w(TAG, "Screen brightness mapping does not cover whole range of available"
+ + " backlight values, autobrightness functionality may be impaired.");
+ }
BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
builder.setCurve(luxLevels, brightnessLevelsNits);
return new PhysicalMappingStrategy(builder.build(), nitsRange, backlightRange);
@@ -56,6 +81,25 @@ public abstract class BrightnessMappingStrategy {
}
}
+ 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;
+ }
+
private static boolean isValidMapping(float[] x, float[] y) {
if (x == null || y == null || x.length == 0 || y.length == 0) {
return false;
@@ -124,10 +168,38 @@ public abstract class BrightnessMappingStrategy {
* 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].
+ * @return The desired brightness of the display normalized to the range [0, 1.0].
*/
public abstract float getBrightness(float lux);
+ /**
+ * Converts the provided backlight value to nits if possible.
+ *
+ * Returns -1.0f if there's no available mapping for the backlight to nits.
+ */
+ public abstract float convertToNits(int backlight);
+
+ /**
+ * Adds a user interaction data point to the brightness mapping.
+ *
+ * This data point <b>must</b> exist on the brightness curve as a result of this call. This is
+ * so that the next time we come to query what the screen brightness should be, we get what the
+ * user requested rather than immediately changing to some other value.
+ *
+ * Currently, we only keep track of one of these at a time to constrain what can happen to the
+ * curve.
+ */
+ public abstract void addUserDataPoint(float lux, float brightness);
+
+ /**
+ * Removes any short term adjustments made to the curve from user interactions.
+ *
+ * Note that this does *not* reset the mapping to its initial state, any brightness
+ * configurations that have been applied will continue to be in effect. This solely removes the
+ * effects of user interactions on the model.
+ */
+ public abstract void clearUserDataPoints();
+
public abstract void dump(PrintWriter pw);
private static float normalizeAbsoluteBrightness(int brightness) {
@@ -136,6 +208,112 @@ public abstract class BrightnessMappingStrategy {
return (float) brightness / PowerManager.BRIGHTNESS_ON;
}
+ private static Spline createSpline(float[] x, float[] y) {
+ Spline spline = Spline.createSpline(x, y);
+ if (DEBUG) {
+ Slog.d(TAG, "Spline: " + spline);
+ for (float v = 1f; v < x[x.length - 1] * 1.25f; v *= 1.25f) {
+ Slog.d(TAG, String.format(" %7.1f: %7.1f", v, spline.interpolate(v)));
+ }
+ }
+ return spline;
+ }
+
+ private static Pair<float[], float[]> insertControlPoint(
+ float[] luxLevels, float[] brightnessLevels, float lux, float brightness) {
+ if (DEBUG) {
+ Slog.d(TAG, "Inserting new control point at (" + lux + ", " + brightness + ")");
+ }
+ final int idx = findInsertionPoint(luxLevels, lux);
+ final float[] newLuxLevels;
+ final float[] newBrightnessLevels;
+ if (idx == luxLevels.length) {
+ newLuxLevels = Arrays.copyOf(luxLevels, luxLevels.length + 1);
+ newBrightnessLevels = Arrays.copyOf(brightnessLevels, brightnessLevels.length + 1);
+ newLuxLevels[idx] = lux;
+ newBrightnessLevels[idx] = brightness;
+ } else if (luxLevels[idx] == lux) {
+ newLuxLevels = Arrays.copyOf(luxLevels, luxLevels.length);
+ newBrightnessLevels = Arrays.copyOf(brightnessLevels, brightnessLevels.length);
+ newBrightnessLevels[idx] = brightness;
+ } else {
+ newLuxLevels = Arrays.copyOf(luxLevels, luxLevels.length + 1);
+ System.arraycopy(newLuxLevels, idx, newLuxLevels, idx+1, luxLevels.length - idx);
+ newLuxLevels[idx] = lux;
+ newBrightnessLevels = Arrays.copyOf(brightnessLevels, brightnessLevels.length + 1);
+ System.arraycopy(newBrightnessLevels, idx, newBrightnessLevels, idx+1,
+ brightnessLevels.length - idx);
+ newBrightnessLevels[idx] = brightness;
+ }
+ smoothCurve(newLuxLevels, newBrightnessLevels, idx);
+ return Pair.create(newLuxLevels, newBrightnessLevels);
+ }
+
+ /**
+ * Returns the index of the first value that's less than or equal to {@code val}.
+ *
+ * This assumes that {@code arr} is sorted. If all values in {@code arr} are greater
+ * than val, then it will return the length of arr as the insertion point.
+ */
+ private static int findInsertionPoint(float[] arr, float val) {
+ for (int i = 0; i < arr.length; i++) {
+ if (val <= arr[i]) {
+ return i;
+ }
+ }
+ return arr.length;
+ }
+
+ private static void smoothCurve(float[] lux, float[] brightness, int idx) {
+ if (DEBUG) {
+ Slog.d(TAG, "smoothCurve(lux=" + Arrays.toString(lux)
+ + ", brightness=" + Arrays.toString(brightness)
+ + ", idx=" + idx + ")");
+ }
+ float prevLux = lux[idx];
+ float prevBrightness = brightness[idx];
+ // Smooth curve for data points above the newly introduced point
+ for (int i = idx+1; i < lux.length; i++) {
+ float currLux = lux[i];
+ float currBrightness = brightness[i];
+ float maxBrightness = prevBrightness * permissibleRatio(currLux, prevLux);
+ float newBrightness = MathUtils.constrain(
+ currBrightness, prevBrightness, maxBrightness);
+ if (newBrightness == currBrightness) {
+ break;
+ }
+ prevLux = currLux;
+ prevBrightness = newBrightness;
+ brightness[i] = newBrightness;
+ }
+
+ // Smooth curve for data points below the newly introduced point
+ prevLux = lux[idx];
+ prevBrightness = brightness[idx];
+ for (int i = idx-1; i >= 0; i--) {
+ float currLux = lux[i];
+ float currBrightness = brightness[i];
+ float minBrightness = prevBrightness * permissibleRatio(currLux, prevLux);
+ float newBrightness = MathUtils.constrain(
+ currBrightness, minBrightness, prevBrightness);
+ if (newBrightness == currBrightness) {
+ break;
+ }
+ prevLux = currLux;
+ prevBrightness = newBrightness;
+ brightness[i] = newBrightness;
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "Smoothed Curve: lux=" + Arrays.toString(lux)
+ + ", brightness=" + Arrays.toString(brightness));
+ }
+ }
+
+ private static float permissibleRatio(float currLux, float prevLux) {
+ return MathUtils.exp(MAX_GRAD
+ * (MathUtils.log(currLux + LUX_GRAD_SMOOTHING)
+ - MathUtils.log(prevLux + LUX_GRAD_SMOOTHING)));
+ }
/**
* A {@link BrightnessMappingStrategy} that maps from ambient room brightness directly to the
@@ -145,7 +323,14 @@ public abstract class BrightnessMappingStrategy {
* configurations that are set are just ignored.
*/
private static class SimpleMappingStrategy extends BrightnessMappingStrategy {
- private final Spline mSpline;
+ // Lux control points
+ private final float[] mLux;
+ // Brightness control points normalized to [0, 1]
+ private final float[] mBrightness;
+
+ private Spline mSpline;
+ private float mUserLux;
+ private float mUserBrightness;
public SimpleMappingStrategy(float[] lux, int[] brightness) {
Preconditions.checkArgument(lux.length != 0 && brightness.length != 0,
@@ -157,26 +342,20 @@ public abstract class BrightnessMappingStrategy {
0, Integer.MAX_VALUE, "brightness");
final int N = brightness.length;
- float[] x = new float[N];
- float[] y = new float[N];
+ mLux = new float[N];
+ mBrightness = new float[N];
for (int i = 0; i < N; i++) {
- x[i] = lux[i];
- y[i] = normalizeAbsoluteBrightness(brightness[i]);
+ mLux[i] = lux[i];
+ mBrightness[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)));
- }
- }
+ mSpline = createSpline(mLux, mBrightness);
+ mUserLux = -1;
+ mUserBrightness = -1;
}
@Override
public boolean setBrightnessConfiguration(@Nullable BrightnessConfiguration config) {
- Slog.e(TAG,
- "setBrightnessConfiguration called on device without display information.");
return false;
}
@@ -186,9 +365,36 @@ public abstract class BrightnessMappingStrategy {
}
@Override
+ public float convertToNits(int backlight) {
+ return -1.0f;
+ }
+
+ @Override
+ public void addUserDataPoint(float lux, float brightness) {
+ if (DEBUG){
+ Slog.d(TAG, "addUserDataPoint(lux=" + lux + ", brightness=" + brightness + ")");
+ }
+ Pair<float[], float[]> curve = insertControlPoint(mLux, mBrightness, lux, brightness);
+ mSpline = createSpline(curve.first, curve.second);
+ mUserLux = lux;
+ mUserBrightness = brightness;
+ }
+
+ @Override
+ public void clearUserDataPoints() {
+ if (mUserLux != -1) {
+ mSpline = createSpline(mLux, mBrightness);
+ mUserLux = -1;
+ mUserBrightness = -1;
+ }
+ }
+
+ @Override
public void dump(PrintWriter pw) {
pw.println("SimpleMappingStrategy");
pw.println(" mSpline=" + mSpline);
+ pw.println(" mUserLux=" + mUserLux);
+ pw.println(" mUserBrightness=" + mUserBrightness);
}
}
@@ -209,11 +415,18 @@ public abstract class BrightnessMappingStrategy {
// A spline mapping from nits to the corresponding backlight value, normalized to the range
// [0, 1.0].
- private final Spline mBacklightSpline;
+ private final Spline mNitsToBacklightSpline;
// The default brightness configuration.
private final BrightnessConfiguration mDefaultConfig;
+ // A spline mapping from the device's backlight value, normalized to the range [0, 1.0], to
+ // a brightness in nits.
+ private Spline mBacklightToNitsSpline;
+
+ private float mUserLux;
+ private float mUserBrightness;
+
public PhysicalMappingStrategy(BrightnessConfiguration config,
float[] nits, int[] backlight) {
Preconditions.checkArgument(nits.length != 0 && backlight.length != 0,
@@ -225,23 +438,18 @@ public abstract class BrightnessMappingStrategy {
Preconditions.checkArrayElementsInRange(backlight,
PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON, "backlight");
+ mUserLux = -1;
+ mUserBrightness = -1;
+
// Setup the backlight spline
final int N = nits.length;
- float[] x = new float[N];
- float[] y = new float[N];
+ float[] normalizedBacklight = new float[N];
for (int i = 0; i < N; i++) {
- x[i] = nits[i];
- y[i] = normalizeAbsoluteBrightness(backlight[i]);
+ normalizedBacklight[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)));
- }
- }
+ mNitsToBacklightSpline = createSpline(nits, normalizedBacklight);
+ mBacklightToNitsSpline = createSpline(normalizedBacklight, nits);
mDefaultConfig = config;
setBrightnessConfiguration(config);
@@ -253,29 +461,49 @@ public abstract class BrightnessMappingStrategy {
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)));
- }
- }
+ mBrightnessSpline = createSpline(curve.first /*lux*/, curve.second /*nits*/);
mConfig = config;
return true;
}
@Override
public float getBrightness(float lux) {
- return mBacklightSpline.interpolate(mBrightnessSpline.interpolate(lux));
+ float nits = mBrightnessSpline.interpolate(lux);
+ float backlight = mNitsToBacklightSpline.interpolate(nits);
+ return backlight;
+ }
+
+ @Override
+ public float convertToNits(int backlight) {
+ return mBacklightToNitsSpline.interpolate(normalizeAbsoluteBrightness(backlight));
+ }
+
+ @Override
+ public void addUserDataPoint(float lux, float backlight) {
+ if (DEBUG){
+ Slog.d(TAG, "addUserDataPoint(lux=" + lux + ", backlight=" + backlight + ")");
+ }
+ float brightness = mBacklightToNitsSpline.interpolate(backlight);
+ Pair<float[], float[]> defaultCurve = mConfig.getCurve();
+ Pair<float[], float[]> newCurve =
+ insertControlPoint(defaultCurve.first, defaultCurve.second, lux, brightness);
+ mBrightnessSpline = createSpline(newCurve.first, newCurve.second);
+ mUserLux = lux;
+ mUserBrightness = brightness;
+ }
+
+ @Override
+ public void clearUserDataPoints() {
+ if (mUserLux != -1) {
+ Pair<float[], float[]> defaultCurve = mConfig.getCurve();
+ mBrightnessSpline = createSpline(defaultCurve.first, defaultCurve.second);
+ mUserLux = -1;
+ mUserBrightness = -1;
+ }
}
@Override
@@ -283,7 +511,9 @@ public abstract class BrightnessMappingStrategy {
pw.println("PhysicalMappingStrategy");
pw.println(" mConfig=" + mConfig);
pw.println(" mBrightnessSpline=" + mBrightnessSpline);
- pw.println(" mBacklightSpline=" + mBacklightSpline);
+ pw.println(" mNitsToBacklightSpline=" + mNitsToBacklightSpline);
+ pw.println(" mUserLux=" + mUserLux);
+ pw.println(" mUserBrightness=" + mUserBrightness);
}
}
}
diff --git a/com/android/server/display/BrightnessTracker.java b/com/android/server/display/BrightnessTracker.java
index 42247f94..bcf8bfe6 100644
--- a/com/android/server/display/BrightnessTracker.java
+++ b/com/android/server/display/BrightnessTracker.java
@@ -24,7 +24,6 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ParceledListSlice;
-import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
@@ -34,6 +33,8 @@ import android.net.Uri;
import android.os.BatteryManager;
import android.os.Environment;
import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.SystemClock;
@@ -88,7 +89,7 @@ public class BrightnessTracker {
private static final String TAG_EVENTS = "events";
private static final String TAG_EVENT = "event";
- private static final String ATTR_BRIGHTNESS = "brightness";
+ private static final String ATTR_NITS = "nits";
private static final String ATTR_TIMESTAMP = "timestamp";
private static final String ATTR_PACKAGE_NAME = "packageName";
private static final String ATTR_USER = "user";
@@ -97,7 +98,10 @@ public class BrightnessTracker {
private static final String ATTR_BATTERY_LEVEL = "batteryLevel";
private static final String ATTR_NIGHT_MODE = "nightMode";
private static final String ATTR_COLOR_TEMPERATURE = "colorTemperature";
- private static final String ATTR_LAST_BRIGHTNESS = "lastBrightness";
+ private static final String ATTR_LAST_NITS = "lastNits";
+
+ private static final int MSG_BACKGROUND_START = 0;
+ private static final int MSG_BRIGHTNESS_CHANGED = 1;
// Lock held while accessing mEvents, is held while writing events to flash.
private final Object mEventsLock = new Object();
@@ -113,9 +117,7 @@ public class BrightnessTracker {
private final Context mContext;
private final ContentResolver mContentResolver;
private Handler mBgHandler;
- // mSettingsObserver, mBroadcastReceiver and mSensorListener should only be used on
- // the mBgHandler thread.
- private SettingsObserver mSettingsObserver;
+ // mBroadcastReceiver and mSensorListener should only be used on the mBgHandler thread.
private BroadcastReceiver mBroadcastReceiver;
private SensorListener mSensorListener;
@@ -126,9 +128,9 @@ public class BrightnessTracker {
@GuardedBy("mDataCollectionLock")
private float mLastBatteryLevel = Float.NaN;
@GuardedBy("mDataCollectionLock")
- private int mIgnoreBrightness = -1;
+ private float mLastBrightness = -1;
@GuardedBy("mDataCollectionLock")
- private int mLastBrightness = -1;
+ private boolean mStarted;
private final Injector mInjector;
@@ -144,33 +146,31 @@ public class BrightnessTracker {
}
}
- /** Start listening for brightness slider events */
- public void start() {
+ /**
+ * Start listening for brightness slider events
+ *
+ * @param initialBrightness the initial screen brightness
+ */
+ public void start(float initialBrightness) {
if (DEBUG) {
Slog.d(TAG, "Start");
}
- mBgHandler = mInjector.getBackgroundHandler();
+ mBgHandler = new TrackerHandler(mInjector.getBackgroundHandler().getLooper());
mUserManager = mContext.getSystemService(UserManager.class);
- mBgHandler.post(() -> backgroundStart());
+ mBgHandler.obtainMessage(MSG_BACKGROUND_START, (Float) initialBrightness).sendToTarget();
}
- private void backgroundStart() {
+ private void backgroundStart(float initialBrightness) {
readEvents();
- mLastBrightness = mInjector.getSystemIntForUser(mContentResolver,
- Settings.System.SCREEN_BRIGHTNESS, -1,
- UserHandle.USER_CURRENT);
-
mSensorListener = new SensorListener();
+
if (mInjector.isInteractive(mContext)) {
mInjector.registerSensorListener(mContext, mSensorListener, mBgHandler);
}
- mSettingsObserver = new SettingsObserver(mBgHandler);
- mInjector.registerBrightnessObserver(mContentResolver, mSettingsObserver);
-
final IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(Intent.ACTION_SHUTDOWN);
intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
@@ -180,6 +180,10 @@ public class BrightnessTracker {
mInjector.registerReceiver(mContext, mBroadcastReceiver, intentFilter);
mInjector.scheduleIdleJob(mContext);
+ synchronized (mDataCollectionLock) {
+ mLastBrightness = initialBrightness;
+ mStarted = true;
+ }
}
/** Stop listening for events */
@@ -188,10 +192,14 @@ public class BrightnessTracker {
if (DEBUG) {
Slog.d(TAG, "Stop");
}
+ mBgHandler.removeMessages(MSG_BACKGROUND_START);
mInjector.unregisterSensorListener(mContext, mSensorListener);
mInjector.unregisterReceiver(mContext, mBroadcastReceiver);
- mInjector.unregisterBrightnessObserver(mContext, mSettingsObserver);
mInjector.cancelIdleJob(mContext);
+
+ synchronized (mDataCollectionLock) {
+ mStarted = false;
+ }
}
/**
@@ -211,8 +219,8 @@ public class BrightnessTracker {
if (includePackage) {
out.add(events[i]);
} else {
- BrightnessChangeEvent event = new BrightnessChangeEvent((events[i]));
- event.packageName = null;
+ BrightnessChangeEvent event = new BrightnessChangeEvent((events[i]),
+ /* redactPackage */ true);
out.add(event);
}
}
@@ -220,48 +228,54 @@ public class BrightnessTracker {
return new ParceledListSlice<>(out);
}
- /** Sets brightness without logging the brightness change event */
- public void setBrightness(int brightness, int userId) {
- synchronized (mDataCollectionLock) {
- mIgnoreBrightness = brightness;
- }
- mInjector.putSystemIntForUser(mContentResolver, Settings.System.SCREEN_BRIGHTNESS,
- brightness, userId);
- }
-
public void persistEvents() {
scheduleWriteEvents();
}
- private void handleBrightnessChanged() {
+ /**
+ * Notify the BrightnessTracker that the user has changed the brightness of the display.
+ */
+ public void notifyBrightnessChanged(float brightness, boolean userInitiated) {
if (DEBUG) {
- Slog.d(TAG, "Brightness change");
+ Slog.d(TAG, String.format("notifyBrightnessChanged(brightness=%f, userInitiated=%b)",
+ brightness, userInitiated));
}
- final BrightnessChangeEvent event = new BrightnessChangeEvent();
- event.timeStamp = mInjector.currentTimeMillis();
+ Message m = mBgHandler.obtainMessage(MSG_BRIGHTNESS_CHANGED,
+ userInitiated ? 1 : 0, 0 /*unused*/, (Float) brightness);
+ m.sendToTarget();
+ }
- int brightness = mInjector.getSystemIntForUser(mContentResolver,
- Settings.System.SCREEN_BRIGHTNESS, -1,
- UserHandle.USER_CURRENT);
+ private void handleBrightnessChanged(float brightness, boolean userInitiated) {
+ BrightnessChangeEvent.Builder builder;
synchronized (mDataCollectionLock) {
- int previousBrightness = mLastBrightness;
+ if (!mStarted) {
+ // Not currently gathering brightness change information
+ return;
+ }
+
+ float previousBrightness = mLastBrightness;
mLastBrightness = brightness;
- if (brightness == -1 || brightness == mIgnoreBrightness) {
- // Notified of brightness change but no setting or self change so ignore.
- mIgnoreBrightness = -1;
+ if (!userInitiated) {
+ // We want to record what current brightness is so that we know what the user
+ // changed it from, but if it wasn't user initiated then we don't want to record it
+ // as a BrightnessChangeEvent.
return;
}
+ builder = new BrightnessChangeEvent.Builder();
+ builder.setBrightness(brightness);
+ builder.setTimeStamp(mInjector.currentTimeMillis());
+
final int readingCount = mLastSensorReadings.size();
if (readingCount == 0) {
// No sensor data so ignore this.
return;
}
- event.luxValues = new float[readingCount];
- event.luxTimestamps = new long[readingCount];
+ float[] luxValues = new float[readingCount];
+ long[] luxTimestamps = new long[readingCount];
int pos = 0;
@@ -269,33 +283,35 @@ public class BrightnessTracker {
long currentTimeMillis = mInjector.currentTimeMillis();
long elapsedTimeNanos = mInjector.elapsedRealtimeNanos();
for (LightData reading : mLastSensorReadings) {
- event.luxValues[pos] = reading.lux;
- event.luxTimestamps[pos] = currentTimeMillis -
+ luxValues[pos] = reading.lux;
+ luxTimestamps[pos] = currentTimeMillis -
TimeUnit.NANOSECONDS.toMillis(elapsedTimeNanos - reading.timestamp);
++pos;
}
+ builder.setLuxValues(luxValues);
+ builder.setLuxTimestamps(luxTimestamps);
- event.batteryLevel = mLastBatteryLevel;
- event.lastBrightness = previousBrightness;
+ builder.setBatteryLevel(mLastBatteryLevel);
+ builder.setLastBrightness(previousBrightness);
}
- event.brightness = brightness;
-
try {
final ActivityManager.StackInfo focusedStack = mInjector.getFocusedStack();
- event.userId = focusedStack.userId;
- event.packageName = focusedStack.topActivity.getPackageName();
+ builder.setUserId(focusedStack.userId);
+ builder.setPackageName(focusedStack.topActivity.getPackageName());
} catch (RemoteException e) {
// Really shouldn't be possible.
+ return;
}
- event.nightMode = mInjector.getSecureIntForUser(mContentResolver,
+ builder.setNightMode(mInjector.getSecureIntForUser(mContentResolver,
Settings.Secure.NIGHT_DISPLAY_ACTIVATED, 0, UserHandle.USER_CURRENT)
- == 1;
- event.colorTemperature = mInjector.getSecureIntForUser(mContentResolver,
+ == 1);
+ builder.setColorTemperature(mInjector.getSecureIntForUser(mContentResolver,
Settings.Secure.NIGHT_DISPLAY_COLOR_TEMPERATURE,
- 0, UserHandle.USER_CURRENT);
+ 0, UserHandle.USER_CURRENT));
+ BrightnessChangeEvent event = builder.build();
if (DEBUG) {
Slog.d(TAG, "Event " + event.brightness + " " + event.packageName);
}
@@ -386,7 +402,7 @@ public class BrightnessTracker {
if (userSerialNo != -1 && toWrite[i].timeStamp > timeCutOff) {
mEvents.append(toWrite[i]);
out.startTag(null, TAG_EVENT);
- out.attribute(null, ATTR_BRIGHTNESS, Integer.toString(toWrite[i].brightness));
+ out.attribute(null, ATTR_NITS, Float.toString(toWrite[i].brightness));
out.attribute(null, ATTR_TIMESTAMP, Long.toString(toWrite[i].timeStamp));
out.attribute(null, ATTR_PACKAGE_NAME, toWrite[i].packageName);
out.attribute(null, ATTR_USER, Integer.toString(userSerialNo));
@@ -394,8 +410,8 @@ public class BrightnessTracker {
out.attribute(null, ATTR_NIGHT_MODE, Boolean.toString(toWrite[i].nightMode));
out.attribute(null, ATTR_COLOR_TEMPERATURE, Integer.toString(
toWrite[i].colorTemperature));
- out.attribute(null, ATTR_LAST_BRIGHTNESS,
- Integer.toString(toWrite[i].lastBrightness));
+ out.attribute(null, ATTR_LAST_NITS,
+ Float.toString(toWrite[i].lastBrightness));
StringBuilder luxValues = new StringBuilder();
StringBuilder luxTimestamps = new StringBuilder();
for (int j = 0; j < toWrite[i].luxValues.length; ++j) {
@@ -444,40 +460,43 @@ public class BrightnessTracker {
}
tag = parser.getName();
if (TAG_EVENT.equals(tag)) {
- BrightnessChangeEvent event = new BrightnessChangeEvent();
+ BrightnessChangeEvent.Builder builder = new BrightnessChangeEvent.Builder();
- String brightness = parser.getAttributeValue(null, ATTR_BRIGHTNESS);
- event.brightness = Integer.parseInt(brightness);
+ String brightness = parser.getAttributeValue(null, ATTR_NITS);
+ builder.setBrightness(Float.parseFloat(brightness));
String timestamp = parser.getAttributeValue(null, ATTR_TIMESTAMP);
- event.timeStamp = Long.parseLong(timestamp);
- event.packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME);
+ builder.setTimeStamp(Long.parseLong(timestamp));
+ builder.setPackageName(parser.getAttributeValue(null, ATTR_PACKAGE_NAME));
String user = parser.getAttributeValue(null, ATTR_USER);
- event.userId = mInjector.getUserId(mUserManager, Integer.parseInt(user));
+ builder.setUserId(mInjector.getUserId(mUserManager, Integer.parseInt(user)));
String batteryLevel = parser.getAttributeValue(null, ATTR_BATTERY_LEVEL);
- event.batteryLevel = Float.parseFloat(batteryLevel);
+ builder.setBatteryLevel(Float.parseFloat(batteryLevel));
String nightMode = parser.getAttributeValue(null, ATTR_NIGHT_MODE);
- event.nightMode = Boolean.parseBoolean(nightMode);
+ builder.setNightMode(Boolean.parseBoolean(nightMode));
String colorTemperature =
parser.getAttributeValue(null, ATTR_COLOR_TEMPERATURE);
- event.colorTemperature = Integer.parseInt(colorTemperature);
- String lastBrightness = parser.getAttributeValue(null, ATTR_LAST_BRIGHTNESS);
- event.lastBrightness = Integer.parseInt(lastBrightness);
+ builder.setColorTemperature(Integer.parseInt(colorTemperature));
+ String lastBrightness = parser.getAttributeValue(null, ATTR_LAST_NITS);
+ builder.setLastBrightness(Float.parseFloat(lastBrightness));
String luxValue = parser.getAttributeValue(null, ATTR_LUX);
String luxTimestamp = parser.getAttributeValue(null, ATTR_LUX_TIMESTAMPS);
- String[] luxValues = luxValue.split(",");
- String[] luxTimestamps = luxTimestamp.split(",");
- if (luxValues.length != luxTimestamps.length) {
+ String[] luxValuesStrings = luxValue.split(",");
+ String[] luxTimestampsStrings = luxTimestamp.split(",");
+ if (luxValuesStrings.length != luxTimestampsStrings.length) {
continue;
}
- event.luxValues = new float[luxValues.length];
- event.luxTimestamps = new long[luxValues.length];
+ float[] luxValues = new float[luxValuesStrings.length];
+ long[] luxTimestamps = new long[luxValuesStrings.length];
for (int i = 0; i < luxValues.length; ++i) {
- event.luxValues[i] = Float.parseFloat(luxValues[i]);
- event.luxTimestamps[i] = Long.parseLong(luxTimestamps[i]);
+ luxValues[i] = Float.parseFloat(luxValuesStrings[i]);
+ luxTimestamps[i] = Long.parseLong(luxTimestampsStrings[i]);
}
+ builder.setLuxValues(luxValues);
+ builder.setLuxTimestamps(luxTimestamps);
+ BrightnessChangeEvent event = builder.build();
if (DEBUG) {
Slog.i(TAG, "Read event " + event.brightness
+ " " + event.packageName);
@@ -502,6 +521,7 @@ public class BrightnessTracker {
public void dump(PrintWriter pw) {
pw.println("BrightnessTracker state:");
synchronized (mDataCollectionLock) {
+ pw.println(" mStarted=" + mStarted);
pw.println(" mLastSensorReadings.size=" + mLastSensorReadings.size());
if (!mLastSensorReadings.isEmpty()) {
pw.println(" mLastSensorReadings time span "
@@ -582,22 +602,6 @@ public class BrightnessTracker {
}
}
- private final class SettingsObserver extends ContentObserver {
- public SettingsObserver(Handler handler) {
- super(handler);
- }
-
- @Override
- public void onChange(boolean selfChange, Uri uri) {
- if (DEBUG) {
- Slog.v(TAG, "settings change " + uri);
- }
- // Self change is based on observer passed to notifyObserver, SettingsProvider
- // passes null so no changes are self changes.
- handleBrightnessChanged();
- }
- }
-
private final class Receiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
@@ -623,6 +627,24 @@ public class BrightnessTracker {
}
}
+ private final class TrackerHandler extends Handler {
+ public TrackerHandler(Looper looper) {
+ super(looper, null, true /*async*/);
+ }
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_BACKGROUND_START:
+ backgroundStart((float)msg.obj /*initial brightness*/);
+ break;
+ case MSG_BRIGHTNESS_CHANGED:
+ float newBrightness = (float) msg.obj;
+ boolean userInitiatedChange = (msg.arg1 == 1);
+ handleBrightnessChanged(newBrightness, userInitiatedChange);
+ break;
+ }
+ }
+ }
+
@VisibleForTesting
static class Injector {
public void registerSensorListener(Context context,
@@ -638,18 +660,6 @@ public class BrightnessTracker {
sensorManager.unregisterListener(sensorListener);
}
- public void registerBrightnessObserver(ContentResolver resolver,
- ContentObserver settingsObserver) {
- resolver.registerContentObserver(Settings.System.getUriFor(
- Settings.System.SCREEN_BRIGHTNESS),
- false, settingsObserver, UserHandle.USER_ALL);
- }
-
- public void unregisterBrightnessObserver(Context context,
- ContentObserver settingsObserver) {
- context.getContentResolver().unregisterContentObserver(settingsObserver);
- }
-
public void registerReceiver(Context context,
BroadcastReceiver receiver, IntentFilter filter) {
context.registerReceiver(receiver, filter);
@@ -664,16 +674,6 @@ public class BrightnessTracker {
return BackgroundThread.getHandler();
}
- public int getSystemIntForUser(ContentResolver resolver, String setting, int defaultValue,
- int userId) {
- return Settings.System.getIntForUser(resolver, setting, defaultValue, userId);
- }
-
- public void putSystemIntForUser(ContentResolver resolver, String setting, int value,
- int userId) {
- Settings.System.putIntForUser(resolver, setting, value, userId);
- }
-
public int getSecureIntForUser(ContentResolver resolver, String setting, int defaultValue,
int userId) {
return Settings.Secure.getIntForUser(resolver, setting, defaultValue, userId);
diff --git a/com/android/server/display/ColorFade.java b/com/android/server/display/ColorFade.java
index 85686ae9..4f53ed49 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, mGammaLoc, mSaturationLoc;
+ private int mOpacityLoc, mGammaLoc;
private int mProgram;
// Vertex and corresponding texture coordinates.
@@ -245,7 +245,6 @@ final class ColorFade {
mOpacityLoc = GLES20.glGetUniformLocation(mProgram, "opacity");
mGammaLoc = GLES20.glGetUniformLocation(mProgram, "gamma");
- mSaturationLoc = GLES20.glGetUniformLocation(mProgram, "saturation");
mTexUnitLoc = GLES20.glGetUniformLocation(mProgram, "texUnit");
GLES20.glUseProgram(mProgram);
@@ -393,9 +392,8 @@ final class ColorFade {
double cos = Math.cos(Math.PI * one_minus_level);
double sign = cos < 0 ? -1 : 1;
float opacity = (float) -Math.pow(one_minus_level, 2) + 1;
- float saturation = (float) Math.pow(level, 4);
float gamma = (float) ((0.5d * sign * Math.pow(cos, 2) + 0.5d) * 0.9d + 0.1d);
- drawFaded(opacity, 1.f / gamma, saturation);
+ drawFaded(opacity, 1.f / gamma);
if (checkGlErrors("drawFrame")) {
return false;
}
@@ -407,10 +405,9 @@ final class ColorFade {
return showSurface(1.0f);
}
- private void drawFaded(float opacity, float gamma, float saturation) {
+ private void drawFaded(float opacity, float gamma) {
if (DEBUG) {
- Slog.d(TAG, "drawFaded: opacity=" + opacity + ", gamma=" + gamma +
- ", saturation=" + saturation);
+ Slog.d(TAG, "drawFaded: opacity=" + opacity + ", gamma=" + gamma);
}
// Use shaders
GLES20.glUseProgram(mProgram);
@@ -420,7 +417,6 @@ final class ColorFade {
GLES20.glUniformMatrix4fv(mTexMatrixLoc, 1, false, mTexMatrix, 0);
GLES20.glUniform1f(mOpacityLoc, opacity);
GLES20.glUniform1f(mGammaLoc, gamma);
- GLES20.glUniform1f(mSaturationLoc, saturation);
// Use textures
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
diff --git a/com/android/server/display/DisplayDevice.java b/com/android/server/display/DisplayDevice.java
index 839ab4d4..3a8e291f 100644
--- a/com/android/server/display/DisplayDevice.java
+++ b/com/android/server/display/DisplayDevice.java
@@ -143,6 +143,9 @@ abstract class DisplayDevice {
public void requestDisplayModesInTransactionLocked(int colorMode, int modeId) {
}
+ public void onOverlayChangedLocked() {
+ }
+
/**
* Sets the display layer stack while in a transaction.
*/
diff --git a/com/android/server/display/DisplayDeviceInfo.java b/com/android/server/display/DisplayDeviceInfo.java
index fddb81ba..6db3b44c 100644
--- a/com/android/server/display/DisplayDeviceInfo.java
+++ b/com/android/server/display/DisplayDeviceInfo.java
@@ -19,6 +19,7 @@ package com.android.server.display;
import android.hardware.display.DisplayViewport;
import android.util.DisplayMetrics;
import android.view.Display;
+import android.view.DisplayCutout;
import android.view.Surface;
import java.util.Arrays;
@@ -229,6 +230,11 @@ final class DisplayDeviceInfo {
public int flags;
/**
+ * The {@link DisplayCutout} if present or {@code null} otherwise.
+ */
+ public DisplayCutout displayCutout;
+
+ /**
* The touch attachment, per {@link DisplayViewport#touch}.
*/
public int touch;
@@ -321,6 +327,7 @@ final class DisplayDeviceInfo {
|| appVsyncOffsetNanos != other.appVsyncOffsetNanos
|| presentationDeadlineNanos != other.presentationDeadlineNanos
|| flags != other.flags
+ || !Objects.equal(displayCutout, other.displayCutout)
|| touch != other.touch
|| rotation != other.rotation
|| type != other.type
@@ -354,6 +361,7 @@ final class DisplayDeviceInfo {
appVsyncOffsetNanos = other.appVsyncOffsetNanos;
presentationDeadlineNanos = other.presentationDeadlineNanos;
flags = other.flags;
+ displayCutout = other.displayCutout;
touch = other.touch;
rotation = other.rotation;
type = other.type;
@@ -380,6 +388,9 @@ final class DisplayDeviceInfo {
sb.append(", ").append(xDpi).append(" x ").append(yDpi).append(" dpi");
sb.append(", appVsyncOff ").append(appVsyncOffsetNanos);
sb.append(", presDeadline ").append(presentationDeadlineNanos);
+ if (displayCutout != null) {
+ sb.append(", cutout ").append(displayCutout);
+ }
sb.append(", touch ").append(touchToString(touch));
sb.append(", rotation ").append(rotation);
sb.append(", type ").append(Display.typeToString(type));
diff --git a/com/android/server/display/DisplayManagerService.java b/com/android/server/display/DisplayManagerService.java
index 9b97934c..0c2ff051 100644
--- a/com/android/server/display/DisplayManagerService.java
+++ b/com/android/server/display/DisplayManagerService.java
@@ -29,6 +29,7 @@ import com.android.internal.util.IndentingPrintWriter;
import android.Manifest;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.AppOpsManager;
import android.content.Context;
@@ -270,8 +271,6 @@ public final class DisplayManagerService extends SystemService {
private final Injector mInjector;
- private final BrightnessTracker mBrightnessTracker;
-
public DisplayManagerService(Context context) {
this(context, new Injector());
}
@@ -290,7 +289,6 @@ public final class DisplayManagerService extends SystemService {
PowerManager pm = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE);
mGlobalDisplayBrightness = pm.getDefaultScreenBrightnessSetting();
- mBrightnessTracker = new BrightnessTracker(context, null);
mCurrentUserId = UserHandle.USER_SYSTEM;
}
@@ -1008,11 +1006,13 @@ public final class DisplayManagerService extends SystemService {
}
private void setBrightnessConfigurationForUserInternal(
- @NonNull BrightnessConfiguration c, @UserIdInt int userId) {
+ @NonNull BrightnessConfiguration c, @UserIdInt int userId,
+ @Nullable String packageName) {
final int userSerial = getUserManager().getUserSerialNumber(userId);
synchronized (mSyncRoot) {
try {
- mPersistentDataStore.setBrightnessConfigurationForUser(c, userSerial);
+ mPersistentDataStore.setBrightnessConfigurationForUser(c, userSerial,
+ packageName);
} finally {
mPersistentDataStore.saveIfNeeded();
}
@@ -1339,9 +1339,6 @@ public final class DisplayManagerService extends SystemService {
pw.println();
mPersistentDataStore.dump(pw);
-
- pw.println();
- mBrightnessTracker.dump(pw);
}
}
@@ -1418,10 +1415,6 @@ public final class DisplayManagerService extends SystemService {
break;
}
- case MSG_REGISTER_BRIGHTNESS_TRACKER:
- mBrightnessTracker.start();
- break;
-
case MSG_LOAD_BRIGHTNESS_CONFIGURATION:
loadBrightnessConfiguration();
break;
@@ -1833,43 +1826,62 @@ public final class DisplayManagerService extends SystemService {
final int userId = UserHandle.getUserId(callingUid);
final long token = Binder.clearCallingIdentity();
try {
- return mBrightnessTracker.getEvents(userId, hasUsageStats);
+ synchronized (mSyncRoot) {
+ return mDisplayPowerController.getBrightnessEvents(userId, hasUsageStats);
+ }
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override // Binder call
- public void setBrightness(int brightness) {
- // STOPSHIP - remove when adaptive brightness controller accepts curves.
+ public void setBrightnessConfigurationForUser(
+ BrightnessConfiguration c, @UserIdInt int userId, String packageName) {
mContext.enforceCallingOrSelfPermission(
- Manifest.permission.BRIGHTNESS_SLIDER_USAGE,
- "Permission to set brightness.");
- int userId = UserHandle.getUserId(Binder.getCallingUid());
+ 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");
+ }
+ if (packageName != null && !validatePackageName(getCallingUid(), packageName)) {
+ packageName = null;
+ }
final long token = Binder.clearCallingIdentity();
try {
- mBrightnessTracker.setBrightness(brightness, userId);
+ setBrightnessConfigurationForUserInternal(c, userId, packageName);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override // Binder call
- public void setBrightnessConfigurationForUser(
- BrightnessConfiguration c, @UserIdInt int userId) {
+ public void setTemporaryBrightness(int brightness) {
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");
+ Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS,
+ "Permission required to set the display's brightness");
+ final long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mSyncRoot) {
+ mDisplayPowerController.setTemporaryBrightness(brightness);
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
}
- Preconditions.checkNotNull(c);
+ }
+
+ @Override // Binder call
+ public void setTemporaryAutoBrightnessAdjustment(float adjustment) {
+ mContext.enforceCallingOrSelfPermission(
+ Manifest.permission.CONTROL_DISPLAY_BRIGHTNESS,
+ "Permission required to set the display's auto brightness adjustment");
final long token = Binder.clearCallingIdentity();
try {
- setBrightnessConfigurationForUserInternal(c, userId);
+ synchronized (mSyncRoot) {
+ mDisplayPowerController.setTemporaryAutoBrightnessAdjustment(adjustment);
+ }
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -2028,7 +2040,18 @@ public final class DisplayManagerService extends SystemService {
@Override
public void persistBrightnessSliderEvents() {
- mBrightnessTracker.persistEvents();
+ synchronized (mSyncRoot) {
+ mDisplayPowerController.persistBrightnessSliderEvents();
+ }
+ }
+
+ @Override
+ public void onOverlayChanged() {
+ synchronized (mSyncRoot) {
+ for (int i = 0; i < mDisplayDevices.size(); i++) {
+ mDisplayDevices.get(i).onOverlayChangedLocked();
+ }
+ }
}
}
}
diff --git a/com/android/server/display/DisplayPowerController.java b/com/android/server/display/DisplayPowerController.java
index a2d95482..80aec42b 100644
--- a/com/android/server/display/DisplayPowerController.java
+++ b/com/android/server/display/DisplayPowerController.java
@@ -24,16 +24,21 @@ import com.android.server.policy.WindowManagerPolicy;
import android.animation.Animator;
import android.animation.ObjectAnimator;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.content.Context;
+import android.content.pm.ParceledListSlice;
import android.content.res.Resources;
-import android.content.res.TypedArray;
+import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
+import android.hardware.display.BrightnessChangeEvent;
import android.hardware.display.BrightnessConfiguration;
import android.hardware.display.DisplayManagerInternal.DisplayPowerCallbacks;
import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest;
+import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
@@ -41,6 +46,8 @@ import android.os.PowerManager;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.Trace;
+import android.os.UserHandle;
+import android.provider.Settings;
import android.util.MathUtils;
import android.util.Slog;
import android.util.Spline;
@@ -96,7 +103,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
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 MSG_SET_TEMPORARY_BRIGHTNESS = 6;
+ private static final int MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT = 7;
private static final int PROXIMITY_UNKNOWN = -1;
private static final int PROXIMITY_NEGATIVE = 0;
@@ -142,6 +150,12 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
// The display blanker.
private final DisplayBlanker mBlanker;
+ // Tracker for brightness changes.
+ private final BrightnessTracker mBrightnessTracker;
+
+ // Tracker for brightness settings changes.
+ private final SettingsObserver mSettingsObserver;
+
// The proximity sensor, or null if not available or needed.
private Sensor mProximitySensor;
@@ -151,15 +165,18 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
// The dim screen brightness.
private final int mScreenBrightnessDimConfig;
- // The minimum screen brightness to use in a very dark room.
- private final int mScreenBrightnessDarkConfig;
-
// The minimum allowed brightness.
private final int mScreenBrightnessRangeMinimum;
// The maximum allowed brightness.
private final int mScreenBrightnessRangeMaximum;
+ // The default screen brightness.
+ private final int mScreenBrightnessDefault;
+
+ // The default screen brightness for VR.
+ private final int mScreenBrightnessForVrDefault;
+
// True if auto-brightness should be used.
private boolean mUseSoftwareAutoBrightnessConfig;
@@ -289,14 +306,47 @@ 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 mapper between ambient lux, display backlight values, and display brightness.
+ @Nullable
+ private BrightnessMappingStrategy mBrightnessMapper;
// The current brightness configuration.
+ @Nullable
private BrightnessConfiguration mBrightnessConfiguration;
+ // The last brightness that was set by the user and not temporary. Set to -1 when a brightness
+ // has yet to be recorded.
+ private int mLastUserSetScreenBrightness;
+
+ // The screen brightenss setting has changed but not taken effect yet. If this is different
+ // from the current screen brightness setting then this is coming from something other than us
+ // and should be considered a user interaction.
+ private int mPendingScreenBrightnessSetting;
+
+ // The last observed screen brightness setting, either set by us or by the settings app on
+ // behalf of the user.
+ private int mCurrentScreenBrightnessSetting;
+
+ // The temporary screen brightness. Typically set when a user is interacting with the
+ // brightness slider but hasn't settled on a choice yet. Set to -1 when there's no temporary
+ // brightness set.
+ private int mTemporaryScreenBrightness;
+
+ // The current screen brightness while in VR mode.
+ private int mScreenBrightnessForVr;
+
+ // The last auto brightness adjustment that was set by the user and not temporary. Set to
+ // Float.NaN when an auto-brightness adjustment hasn't been recorded yet.
+ private float mAutoBrightnessAdjustment;
+
+ // The pending auto brightness adjustment that will take effect on the next power state update.
+ private float mPendingAutoBrightnessAdjustment;
+
+ // The temporary auto brightness adjustment. Typically set when a user is interacting with the
+ // adjustment slider but hasn't settled on a choice yet. Set to Float.NaN when there's no
+ // temporary adjustment set.
+ private float mTemporaryAutoBrightnessAdjustment;
+
// Animators.
private ObjectAnimator mColorFadeOnAnimator;
private ObjectAnimator mColorFadeOffAnimator;
@@ -309,6 +359,8 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
DisplayPowerCallbacks callbacks, Handler handler,
SensorManager sensorManager, DisplayBlanker blanker) {
mHandler = new DisplayControllerHandler(handler.getLooper());
+ mBrightnessTracker = new BrightnessTracker(context, null);
+ mSettingsObserver = new SettingsObserver(mHandler);
mCallbacks = callbacks;
mBatteryStats = BatteryStatsService.getService();
@@ -327,26 +379,15 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
mScreenBrightnessDimConfig = clampAbsoluteBrightness(resources.getInteger(
com.android.internal.R.integer.config_screenBrightnessDim));
- mScreenBrightnessDarkConfig = clampAbsoluteBrightness(resources.getInteger(
- com.android.internal.R.integer.config_screenBrightnessDark));
- if (mScreenBrightnessDarkConfig > mScreenBrightnessDimConfig) {
- Slog.w(TAG, "Expected config_screenBrightnessDark ("
- + mScreenBrightnessDarkConfig + ") to be less than or equal to "
- + "config_screenBrightnessDim (" + mScreenBrightnessDimConfig + ").");
- }
- if (mScreenBrightnessDarkConfig > screenBrightnessSettingMinimum) {
- Slog.w(TAG, "Expected config_screenBrightnessDark ("
- + mScreenBrightnessDarkConfig + ") to be less than or equal to "
- + "config_screenBrightnessSettingMinimum ("
- + screenBrightnessSettingMinimum + ").");
- }
-
- int screenBrightnessRangeMinimum = Math.min(Math.min(
- screenBrightnessSettingMinimum, mScreenBrightnessDimConfig),
- mScreenBrightnessDarkConfig);
+ mScreenBrightnessRangeMinimum =
+ Math.min(screenBrightnessSettingMinimum, mScreenBrightnessDimConfig);
mScreenBrightnessRangeMaximum = clampAbsoluteBrightness(resources.getInteger(
com.android.internal.R.integer.config_screenBrightnessSettingMaximum));
+ mScreenBrightnessDefault = clampAbsoluteBrightness(resources.getInteger(
+ com.android.internal.R.integer.config_screenBrightnessSettingDefault));
+ mScreenBrightnessForVrDefault = clampAbsoluteBrightness(resources.getInteger(
+ com.android.internal.R.integer.config_screenBrightnessForVrSettingDefault));
mUseSoftwareAutoBrightnessConfig = resources.getBoolean(
com.android.internal.R.bool.config_automatic_brightness_available);
@@ -362,18 +403,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
com.android.internal.R.bool.config_skipScreenOnBrightnessRamp);
if (mUseSoftwareAutoBrightnessConfig) {
- float[] luxLevels = getLuxLevels(resources.getIntArray(
- com.android.internal.R.array.config_autoBrightnessLevels));
- int[] backlightValues = resources.getIntArray(
- com.android.internal.R.array.config_autoBrightnessLcdBacklightValues);
- 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);
@@ -413,31 +442,13 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
+ "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 "
- + "config_autoBrightnessLcdBacklightValues ("
- + bottom + ").");
- }
- 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) {
+ mBrightnessMapper = BrightnessMappingStrategy.create(resources);
+ if (mBrightnessMapper != null) {
mAutomaticBrightnessController = new AutomaticBrightnessController(this,
- handler.getLooper(), sensorManager, mapper, lightSensorWarmUpTimeConfig,
- screenBrightnessRangeMinimum, mScreenBrightnessRangeMaximum,
- dozeScaleFactor, lightSensorRate, initialLightSensorRate,
- brighteningLightDebounce, darkeningLightDebounce,
+ handler.getLooper(), sensorManager, mBrightnessMapper,
+ lightSensorWarmUpTimeConfig, mScreenBrightnessRangeMinimum,
+ mScreenBrightnessRangeMaximum, dozeScaleFactor, lightSensorRate,
+ initialLightSensorRate, brighteningLightDebounce, darkeningLightDebounce,
autoBrightnessResetAmbientLuxAfterWarmUp, ambientLightHorizon,
autoBrightnessAdjustmentMaxGamma, dynamicHysteresis);
} else {
@@ -445,9 +456,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
}
}
- mScreenBrightnessRangeMinimum = screenBrightnessRangeMinimum;
-
-
mColorFadeEnabled = !ActivityManager.isLowRamDeviceStatic();
mColorFadeFadesConfig = resources.getBoolean(
com.android.internal.R.bool.config_animateScreenLights);
@@ -466,32 +474,35 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
}
}
+ mCurrentScreenBrightnessSetting = getScreenBrightnessSetting();
+ mScreenBrightnessForVr = getScreenBrightnessForVrSetting();
+ mAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting();
+ mTemporaryScreenBrightness = -1;
+ mTemporaryAutoBrightnessAdjustment = Float.NaN;
}
- 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;
+ /**
+ * Returns true if the proximity sensor screen-off function is available.
+ */
+ public boolean isProximitySensorAvailable() {
+ return mProximitySensor != null;
}
- 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;
+ /**
+ * Get the {@link BrightnessChangeEvent}s for the specified user.
+ * @param userId userId to fetch data for
+ * @param includePackage if false will null out the package name in events
+ */
+ public ParceledListSlice<BrightnessChangeEvent> getBrightnessEvents(
+ @UserIdInt int userId, boolean includePackage) {
+ return mBrightnessTracker.getEvents(userId, includePackage);
}
/**
- * Returns true if the proximity sensor screen-off function is available.
+ * Persist the brightness slider events to disk.
*/
- public boolean isProximitySensorAvailable() {
- return mProximitySensor != null;
+ public void persistBrightnessSliderEvents() {
+ mBrightnessTracker.persistEvents();
}
/**
@@ -588,6 +599,19 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
} catch (RemoteException ex) {
// same process
}
+
+ // Initialize all of the brightness tracking state
+ final float brightness = convertToNits(mPowerState.getScreenBrightness());
+ if (brightness >= 0.0f) {
+ mBrightnessTracker.start(brightness);
+ }
+
+ mContext.getContentResolver().registerContentObserver(
+ Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS),
+ false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
+ mContext.getContentResolver().registerContentObserver(
+ Settings.System.getUriFor(Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ),
+ false /*notifyForDescendants*/, mSettingsObserver, UserHandle.USER_ALL);
}
private final Animator.AnimatorListener mAnimatorListener = new Animator.AnimatorListener() {
@@ -617,7 +641,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
// Update the power state request.
final boolean mustNotify;
boolean mustInitialize = false;
- boolean autoBrightnessAdjustmentChanged = false;
synchronized (mLock) {
mPendingUpdatePowerStateLocked = false;
@@ -632,8 +655,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
mPendingRequestChangedLocked = false;
mustInitialize = true;
} else if (mPendingRequestChangedLocked) {
- autoBrightnessAdjustmentChanged = (mPowerRequest.screenAutoBrightnessAdjustment
- != mPendingRequestLocked.screenAutoBrightnessAdjustment);
mPowerRequest.copyFrom(mPendingRequestLocked);
mWaitingForNegativeProximity |= mPendingWaitForNegativeProximityLocked;
mPendingWaitForNegativeProximityLocked = false;
@@ -722,47 +743,102 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
brightness = PowerManager.BRIGHTNESS_OFF;
}
- // Configure auto-brightness.
- boolean autoBrightnessEnabled = false;
- if (mAutomaticBrightnessController != null) {
- final boolean autoBrightnessEnabledInDoze =
- mAllowAutoBrightnessWhileDozingConfig && Display.isDozeState(state);
- autoBrightnessEnabled = mPowerRequest.useAutoBrightness
+ // Always use the VR brightness when in the VR state.
+ if (state == Display.STATE_VR) {
+ brightness = mScreenBrightnessForVr;
+ }
+
+ if (brightness < 0 && mPowerRequest.screenBrightnessOverride > 0) {
+ brightness = mPowerRequest.screenBrightnessOverride;
+ }
+
+ final boolean autoBrightnessEnabledInDoze =
+ mAllowAutoBrightnessWhileDozingConfig && Display.isDozeState(state);
+ final boolean autoBrightnessEnabled = mPowerRequest.useAutoBrightness
&& (state == Display.STATE_ON || autoBrightnessEnabledInDoze)
- && brightness < 0;
- final boolean userInitiatedChange = autoBrightnessAdjustmentChanged
- && mPowerRequest.brightnessSetByUser;
- mAutomaticBrightnessController.configure(autoBrightnessEnabled,
- mBrightnessConfiguration, mPowerRequest.screenAutoBrightnessAdjustment,
- state != Display.STATE_ON, userInitiatedChange);
+ && brightness < 0
+ && mAutomaticBrightnessController != null;
+ boolean brightnessIsTemporary = false;
+
+ final boolean userSetBrightnessChanged = updateUserSetScreenBrightness();
+ if (userSetBrightnessChanged) {
+ mTemporaryScreenBrightness = -1;
+ }
+
+ // Use the temporary screen brightness if there isn't an override, either from
+ // WindowManager or based on the display state.
+ if (mTemporaryScreenBrightness > 0) {
+ brightness = mTemporaryScreenBrightness;
+ brightnessIsTemporary = true;
+ }
+
+ final boolean autoBrightnessAdjustmentChanged = updateAutoBrightnessAdjustment();
+ if (autoBrightnessAdjustmentChanged) {
+ mTemporaryAutoBrightnessAdjustment = Float.NaN;
+ }
+
+ // Use the autobrightness adjustment override if set.
+ final float autoBrightnessAdjustment;
+ if (!Float.isNaN(mTemporaryAutoBrightnessAdjustment)) {
+ autoBrightnessAdjustment = mTemporaryAutoBrightnessAdjustment;
+ brightnessIsTemporary = true;
+ } else {
+ autoBrightnessAdjustment = mAutoBrightnessAdjustment;
}
// Apply brightness boost.
- // We do this here after configuring auto-brightness so that we don't
- // disable the light sensor during this temporary state. That way when
- // boost ends we will be able to resume normal auto-brightness behavior
- // without any delay.
+ // We do this here after deciding whether auto-brightness is enabled so that we don't
+ // disable the light sensor during this temporary state. That way when boost ends we will
+ // be able to resume normal auto-brightness behavior without any delay.
if (mPowerRequest.boostScreenBrightness
&& brightness != PowerManager.BRIGHTNESS_OFF) {
brightness = PowerManager.BRIGHTNESS_ON;
}
+ // If the brightness is already set then it's been overriden by something other than the
+ // user, or is a temporary adjustment.
+ final boolean userInitiatedChange = brightness < 0
+ && (autoBrightnessAdjustmentChanged || userSetBrightnessChanged);
+
+ // Configure auto-brightness.
+ if (mAutomaticBrightnessController != null) {
+ mAutomaticBrightnessController.configure(autoBrightnessEnabled,
+ mBrightnessConfiguration,
+ mLastUserSetScreenBrightness / (float) PowerManager.BRIGHTNESS_ON,
+ userSetBrightnessChanged, autoBrightnessAdjustment,
+ autoBrightnessAdjustmentChanged, mPowerRequest.policy);
+ }
+
// Apply auto-brightness.
boolean slowChange = false;
if (brightness < 0) {
+ float newAutoBrightnessAdjustment = autoBrightnessAdjustment;
if (autoBrightnessEnabled) {
brightness = mAutomaticBrightnessController.getAutomaticScreenBrightness();
+ newAutoBrightnessAdjustment =
+ mAutomaticBrightnessController.getAutomaticScreenBrightnessAdjustment();
}
+
if (brightness >= 0) {
// Use current auto-brightness value and slowly adjust to changes.
brightness = clampScreenBrightness(brightness);
if (mAppliedAutoBrightness && !autoBrightnessAdjustmentChanged) {
slowChange = true; // slowly adapt to auto-brightness
}
+ // Tell the rest of the system about the new brightness. Note that we do this
+ // before applying the low power or dim transformations so that the slider
+ // accurately represents the full possible range, even if they range changes what
+ // it means in absolute terms.
+ putScreenBrightnessSetting(brightness);
mAppliedAutoBrightness = true;
} else {
mAppliedAutoBrightness = false;
}
+ if (autoBrightnessAdjustment != newAutoBrightnessAdjustment) {
+ // If the autobrightness controller has decided to change the adjustment value
+ // used, make sure that's reflected in settings.
+ putAutoBrightnessAdjustmentSetting(newAutoBrightnessAdjustment);
+ }
} else {
mAppliedAutoBrightness = false;
}
@@ -773,13 +849,11 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
}
// Apply manual brightness.
- // Use the current brightness setting from the request, which is expected
- // provide a nominal default value for the case where auto-brightness
- // is not ready yet.
if (brightness < 0) {
- brightness = clampScreenBrightness(mPowerRequest.screenBrightness);
+ brightness = clampScreenBrightness(mCurrentScreenBrightnessSetting);
}
+
// Apply dimming by at least some minimum amount when user activity
// timeout is about to expire.
if (mPowerRequest.policy == DisplayPowerRequest.POLICY_DIM) {
@@ -848,12 +922,17 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
final boolean isDisplayContentVisible =
mColorFadeEnabled && mPowerState.getColorFadeLevel() == 1.0f;
if (initialRampSkip || hasBrightnessBuckets
- || wasOrWillBeInVr || !isDisplayContentVisible) {
+ || wasOrWillBeInVr || !isDisplayContentVisible || brightnessIsTemporary) {
animateScreenBrightness(brightness, 0);
} else {
animateScreenBrightness(brightness,
slowChange ? mBrightnessRampRateSlow : mBrightnessRampRateFast);
}
+
+ if (!brightnessIsTemporary) {
+ notifyBrightnessChanged(brightness, userInitiatedChange);
+ }
+
}
// Determine whether the display is ready for use in the newly requested state.
@@ -921,6 +1000,18 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
msg.sendToTarget();
}
+ public void setTemporaryBrightness(int brightness) {
+ Message msg = mHandler.obtainMessage(MSG_SET_TEMPORARY_BRIGHTNESS,
+ brightness, 0 /*unused*/);
+ msg.sendToTarget();
+ }
+
+ public void setTemporaryAutoBrightnessAdjustment(float adjustment) {
+ Message msg = mHandler.obtainMessage(MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT,
+ Float.floatToIntBits(adjustment), 0 /*unused*/);
+ msg.sendToTarget();
+ }
+
private void blockScreenOn() {
if (mPendingScreenOnUnblocker == null) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_POWER, SCREEN_ON_BLOCKED_TRACE_NAME, 0);
@@ -1312,6 +1403,90 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
mHandler.post(mOnStateChangedRunnable);
}
+ private void handleSettingsChange() {
+ mPendingScreenBrightnessSetting = getScreenBrightnessSetting();
+ mPendingAutoBrightnessAdjustment = getAutoBrightnessAdjustmentSetting();
+ // We don't bother with a pending variable for VR screen brightness since we just
+ // immediately adapt to it.
+ mScreenBrightnessForVr = getScreenBrightnessForVrSetting();
+ sendUpdatePowerState();
+ }
+
+ private float getAutoBrightnessAdjustmentSetting() {
+ final float adj = Settings.System.getFloatForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f, UserHandle.USER_CURRENT);
+ return Float.isNaN(adj) ? 0.0f : clampAutoBrightnessAdjustment(adj);
+ }
+
+ private int getScreenBrightnessSetting() {
+ final int brightness = Settings.System.getIntForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_BRIGHTNESS, mScreenBrightnessDefault,
+ UserHandle.USER_CURRENT);
+ return clampAbsoluteBrightness(brightness);
+ }
+
+ private int getScreenBrightnessForVrSetting() {
+ final int brightness = Settings.System.getIntForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_BRIGHTNESS_FOR_VR, mScreenBrightnessForVrDefault,
+ UserHandle.USER_CURRENT);
+ return clampAbsoluteBrightness(brightness);
+ }
+
+ private void putScreenBrightnessSetting(int brightness) {
+ mCurrentScreenBrightnessSetting = brightness;
+ Settings.System.putIntForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_BRIGHTNESS, brightness, UserHandle.USER_CURRENT);
+ }
+
+ private void putAutoBrightnessAdjustmentSetting(float adjustment) {
+ mAutoBrightnessAdjustment = adjustment;
+ Settings.System.putFloatForUser(mContext.getContentResolver(),
+ Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, adjustment, UserHandle.USER_CURRENT);
+ }
+
+ private boolean updateAutoBrightnessAdjustment() {
+ if (Float.isNaN(mPendingAutoBrightnessAdjustment)) {
+ return false;
+ }
+ if (mAutoBrightnessAdjustment == mPendingAutoBrightnessAdjustment) {
+ return false;
+ }
+ mAutoBrightnessAdjustment = mPendingAutoBrightnessAdjustment;
+ mPendingAutoBrightnessAdjustment = Float.NaN;
+ return true;
+ }
+
+ private boolean updateUserSetScreenBrightness() {
+ if (mPendingScreenBrightnessSetting < 0) {
+ return false;
+ }
+ if (mCurrentScreenBrightnessSetting == mPendingScreenBrightnessSetting) {
+ mPendingScreenBrightnessSetting = -1;
+ return false;
+ }
+ mLastUserSetScreenBrightness = mPendingScreenBrightnessSetting;
+ mPendingScreenBrightnessSetting = -1;
+ return true;
+ }
+
+ private void notifyBrightnessChanged(int brightness, boolean userInitiated) {
+ final float brightnessInNits = convertToNits(brightness);
+ if (brightnessInNits >= 0.0f) {
+ // We only want to track changes on devices that can actually map the display backlight
+ // values into a physical brightness unit since the value provided by the API is in
+ // nits and not using the arbitrary backlight units.
+ mBrightnessTracker.notifyBrightnessChanged(brightnessInNits, userInitiated);
+ }
+ }
+
+ private float convertToNits(int backlight) {
+ if (mBrightnessMapper != null) {
+ return mBrightnessMapper.convertToNits(backlight);
+ } else {
+ return -1.0f;
+ }
+ }
+
private final Runnable mOnStateChangedRunnable = new Runnable() {
@Override
public void run() {
@@ -1362,7 +1537,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
pw.println("Display Power Controller Configuration:");
pw.println(" mScreenBrightnessDozeConfig=" + mScreenBrightnessDozeConfig);
pw.println(" mScreenBrightnessDimConfig=" + mScreenBrightnessDimConfig);
- pw.println(" mScreenBrightnessDarkConfig=" + mScreenBrightnessDarkConfig);
pw.println(" mScreenBrightnessRangeMinimum=" + mScreenBrightnessRangeMinimum);
pw.println(" mScreenBrightnessRangeMaximum=" + mScreenBrightnessRangeMaximum);
pw.println(" mUseSoftwareAutoBrightnessConfig=" + mUseSoftwareAutoBrightnessConfig);
@@ -1383,7 +1557,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
pw.println("Display Power Controller Thread State:");
pw.println(" mPowerRequest=" + mPowerRequest);
pw.println(" mWaitingForNegativeProximity=" + mWaitingForNegativeProximity);
-
pw.println(" mProximitySensor=" + mProximitySensor);
pw.println(" mProximitySensorEnabled=" + mProximitySensorEnabled);
pw.println(" mProximityThreshold=" + mProximityThreshold);
@@ -1392,6 +1565,11 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
pw.println(" mPendingProximityDebounceTime="
+ TimeUtils.formatUptime(mPendingProximityDebounceTime));
pw.println(" mScreenOffBecauseOfProximity=" + mScreenOffBecauseOfProximity);
+ pw.println(" mLastUserSetScreenBrightness=" + mLastUserSetScreenBrightness);
+ pw.println(" mCurrentScreenBrightnessSetting=" + mCurrentScreenBrightnessSetting);
+ pw.println(" mPendingScreenBrightnessSetting=" + mPendingScreenBrightnessSetting);
+ pw.println(" mAutoBrightnessAdjustment=" + mAutoBrightnessAdjustment);
+ pw.println(" mPendingAutoBrightnessAdjustment=" + mPendingAutoBrightnessAdjustment);
pw.println(" mAppliedAutoBrightness=" + mAppliedAutoBrightness);
pw.println(" mAppliedDimming=" + mAppliedDimming);
pw.println(" mAppliedLowPower=" + mAppliedLowPower);
@@ -1420,6 +1598,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
mAutomaticBrightnessController.dump(pw);
}
+ if (mBrightnessTracker != null) {
+ pw.println();
+ mBrightnessTracker.dump(pw);
+ }
}
private static String proximityToString(int state) {
@@ -1452,6 +1634,10 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
return MathUtils.constrain(value, PowerManager.BRIGHTNESS_OFF, PowerManager.BRIGHTNESS_ON);
}
+ private static float clampAutoBrightnessAdjustment(float value) {
+ return MathUtils.constrain(value, -1.0f, 1.0f);
+ }
+
private final class DisplayControllerHandler extends Handler {
public DisplayControllerHandler(Looper looper) {
super(looper, null, true /*async*/);
@@ -1481,8 +1667,18 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
}
break;
case MSG_CONFIGURE_BRIGHTNESS:
- BrightnessConfiguration c = (BrightnessConfiguration) msg.obj;
- mBrightnessConfiguration = c != null ? c : mDefaultBrightnessConfiguration;
+ mBrightnessConfiguration = (BrightnessConfiguration)msg.obj;
+ updatePowerState();
+ break;
+
+ case MSG_SET_TEMPORARY_BRIGHTNESS:
+ // TODO: Should we have a a timeout for the temporary brightness?
+ mTemporaryScreenBrightness = msg.arg1;
+ updatePowerState();
+ break;
+
+ case MSG_SET_TEMPORARY_AUTO_BRIGHTNESS_ADJUSTMENT:
+ mTemporaryAutoBrightnessAdjustment = Float.intBitsToFloat(msg.arg1);
updatePowerState();
break;
}
@@ -1506,6 +1702,18 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call
}
};
+
+ private final class SettingsObserver extends ContentObserver {
+ public SettingsObserver(Handler handler) {
+ super(handler);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, Uri uri) {
+ handleSettingsChange();
+ }
+ }
+
private final class ScreenOnUnblocker implements WindowManagerPolicy.ScreenOnListener {
@Override
public void onScreenOn() {
diff --git a/com/android/server/display/LocalDisplayAdapter.java b/com/android/server/display/LocalDisplayAdapter.java
index eb9ff589..b7385d85 100644
--- a/com/android/server/display/LocalDisplayAdapter.java
+++ b/com/android/server/display/LocalDisplayAdapter.java
@@ -16,6 +16,7 @@
package com.android.server.display;
+import android.app.ActivityThread;
import android.content.res.Resources;
import com.android.server.LocalServices;
import com.android.server.lights.Light;
@@ -30,9 +31,12 @@ import android.os.Looper;
import android.os.PowerManager;
import android.os.SystemProperties;
import android.os.Trace;
+import android.text.TextUtils;
+import android.util.PathParser;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
+import android.view.DisplayCutout;
import android.view.DisplayEventReceiver;
import android.view.Surface;
import android.view.SurfaceControl;
@@ -389,7 +393,7 @@ final class LocalDisplayAdapter extends DisplayAdapter {
| DisplayDeviceInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS;
}
- final Resources res = getContext().getResources();
+ final Resources res = getOverlayContext().getResources();
if (mBuiltInDisplayId == SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
mInfo.name = res.getString(
com.android.internal.R.string.display_manager_built_in_display_name);
@@ -400,12 +404,14 @@ final class LocalDisplayAdapter extends DisplayAdapter {
&& SystemProperties.getBoolean(PROPERTY_EMULATOR_CIRCULAR, false))) {
mInfo.flags |= DisplayDeviceInfo.FLAG_ROUND;
}
+ mInfo.displayCutout = DisplayCutout.fromResources(res, mInfo.width);
mInfo.type = Display.TYPE_BUILT_IN;
mInfo.densityDpi = (int)(phys.density * 160 + 0.5f);
mInfo.xDpi = phys.xDpi;
mInfo.yDpi = phys.yDpi;
mInfo.touch = DisplayDeviceInfo.TOUCH_INTERNAL;
} else {
+ mInfo.displayCutout = null;
mInfo.type = Display.TYPE_HDMI;
mInfo.flags |= DisplayDeviceInfo.FLAG_PRESENTATION;
mInfo.name = getContext().getResources().getString(
@@ -585,6 +591,11 @@ final class LocalDisplayAdapter extends DisplayAdapter {
}
}
+ @Override
+ public void onOverlayChangedLocked() {
+ updateDeviceInfoLocked();
+ }
+
public boolean requestModeInTransactionLocked(int modeId) {
if (modeId == 0) {
modeId = mDefaultModeId;
@@ -673,6 +684,11 @@ final class LocalDisplayAdapter extends DisplayAdapter {
}
}
+ /** Supplies a context whose Resources apply runtime-overlays */
+ Context getOverlayContext() {
+ return ActivityThread.currentActivityThread().getSystemUiContext();
+ }
+
/**
* Keeps track of a display configuration.
*/
diff --git a/com/android/server/display/LogicalDisplay.java b/com/android/server/display/LogicalDisplay.java
index 78a54079..132f083c 100644
--- a/com/android/server/display/LogicalDisplay.java
+++ b/com/android/server/display/LogicalDisplay.java
@@ -145,6 +145,7 @@ final class LogicalDisplay {
mInfo.overscanRight = mOverrideDisplayInfo.overscanRight;
mInfo.overscanBottom = mOverrideDisplayInfo.overscanBottom;
mInfo.rotation = mOverrideDisplayInfo.rotation;
+ mInfo.displayCutout = mOverrideDisplayInfo.displayCutout;
mInfo.logicalDensityDpi = mOverrideDisplayInfo.logicalDensityDpi;
mInfo.physicalXDpi = mOverrideDisplayInfo.physicalXDpi;
mInfo.physicalYDpi = mOverrideDisplayInfo.physicalYDpi;
@@ -280,6 +281,7 @@ final class LogicalDisplay {
mBaseDisplayInfo.largestNominalAppHeight = deviceInfo.height;
mBaseDisplayInfo.ownerUid = deviceInfo.ownerUid;
mBaseDisplayInfo.ownerPackageName = deviceInfo.ownerPackageName;
+ mBaseDisplayInfo.displayCutout = deviceInfo.displayCutout;
mPrimaryDisplayDeviceInfo = deviceInfo;
mInfo = null;
diff --git a/com/android/server/display/PersistentDataStore.java b/com/android/server/display/PersistentDataStore.java
index 49b4465e..cbf46f83 100644
--- a/com/android/server/display/PersistentDataStore.java
+++ b/com/android/server/display/PersistentDataStore.java
@@ -23,6 +23,7 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
+import android.annotation.Nullable;
import android.graphics.Point;
import android.hardware.display.BrightnessConfiguration;
import android.hardware.display.WifiDisplay;
@@ -30,6 +31,8 @@ import android.util.AtomicFile;
import android.util.Slog;
import android.util.SparseArray;
import android.util.Pair;
+import android.util.SparseLongArray;
+import android.util.TimeUtils;
import android.util.Xml;
import android.view.Display;
@@ -73,8 +76,8 @@ import libcore.util.Objects;
* &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-configuration user-serial="0" package-name="com.example" timestamp="1234">
+ * &lt;brightness-curve description="some text">
* &lt;brightness-point lux="0" nits="13.25"/>
* &lt;brightness-point lux="20" nits="35.94"/>
* &lt;/brightness-curve>
@@ -110,8 +113,11 @@ final class PersistentDataStore {
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_PACKAGE_NAME = "package-name";
+ private static final String ATTR_TIME_STAMP = "timestamp";
private static final String ATTR_LUX = "lux";
private static final String ATTR_NITS = "nits";
+ private static final String ATTR_DESCRIPTION = "description";
// Remembered Wifi display devices.
private ArrayList<WifiDisplay> mRememberedWifiDisplays = new ArrayList<WifiDisplay>();
@@ -273,9 +279,11 @@ final class PersistentDataStore {
}
}
- public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userSerial) {
+ public void setBrightnessConfigurationForUser(BrightnessConfiguration c, int userSerial,
+ @Nullable String packageName) {
loadIfNeeded();
- if (mBrightnessConfigurations.setBrightnessConfigurationForUser(c, userSerial)) {
+ if (mBrightnessConfigurations.setBrightnessConfigurationForUser(c, userSerial,
+ packageName)) {
setDirty();
}
}
@@ -576,16 +584,34 @@ final class PersistentDataStore {
private static final class BrightnessConfigurations {
// Maps from a user ID to the users' given brightness configuration
private SparseArray<BrightnessConfiguration> mConfigurations;
+ // Timestamp of time the configuration was set.
+ private SparseLongArray mTimeStamps;
+ // Package that set the configuration.
+ private SparseArray<String> mPackageNames;
public BrightnessConfigurations() {
mConfigurations = new SparseArray<>();
+ mTimeStamps = new SparseLongArray();
+ mPackageNames = new SparseArray<>();
}
private boolean setBrightnessConfigurationForUser(BrightnessConfiguration c,
- int userSerial) {
+ int userSerial, String packageName) {
BrightnessConfiguration currentConfig = mConfigurations.get(userSerial);
- if (currentConfig == null || !currentConfig.equals(c)) {
- mConfigurations.put(userSerial, c);
+ if (currentConfig != c && (currentConfig == null || !currentConfig.equals(c))) {
+ if (c != null) {
+ if (packageName == null) {
+ mPackageNames.remove(userSerial);
+ } else {
+ mPackageNames.put(userSerial, packageName);
+ }
+ mTimeStamps.put(userSerial, System.currentTimeMillis());
+ mConfigurations.put(userSerial, c);
+ } else {
+ mPackageNames.remove(userSerial);
+ mTimeStamps.delete(userSerial);
+ mConfigurations.remove(userSerial);
+ }
return true;
}
return false;
@@ -604,14 +630,31 @@ final class PersistentDataStore {
userSerial = Integer.parseInt(
parser.getAttributeValue(null, ATTR_USER_SERIAL));
} catch (NumberFormatException nfe) {
- userSerial= -1;
+ userSerial = -1;
Slog.e(TAG, "Failed to read in brightness configuration", nfe);
}
+ String packageName = parser.getAttributeValue(null, ATTR_PACKAGE_NAME);
+ String timeStampString = parser.getAttributeValue(null, ATTR_TIME_STAMP);
+ long timeStamp = -1;
+ if (timeStampString != null) {
+ try {
+ timeStamp = Long.parseLong(timeStampString);
+ } catch (NumberFormatException nfe) {
+ // Ignore we will just not restore the timestamp.
+ }
+ }
+
try {
BrightnessConfiguration config = loadConfigurationFromXml(parser);
- if (userSerial>= 0 && config != null) {
+ if (userSerial >= 0 && config != null) {
mConfigurations.put(userSerial, config);
+ if (timeStamp != -1) {
+ mTimeStamps.put(userSerial, timeStamp);
+ }
+ if (packageName != null) {
+ mPackageNames.put(userSerial, packageName);
+ }
}
} catch (IllegalArgumentException iae) {
Slog.e(TAG, "Failed to load brightness configuration!", iae);
@@ -623,18 +666,24 @@ final class PersistentDataStore {
private static BrightnessConfiguration loadConfigurationFromXml(XmlPullParser parser)
throws IOException, XmlPullParserException {
final int outerDepth = parser.getDepth();
- final BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder();
+ String description = null;
+ Pair<float[], float[]> curve = null;
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*/);
+ description = parser.getAttributeValue(null, ATTR_DESCRIPTION);
+ curve = loadCurveFromXml(parser);
}
}
+ if (curve == null) {
+ return null;
+ }
+ final BrightnessConfiguration.Builder builder = new BrightnessConfiguration.Builder(
+ curve.first, curve.second);
+ builder.setDescription(description);
return builder.build();
}
- private static Pair<float[], float[]> loadCurveFromXml(XmlPullParser parser,
- BrightnessConfiguration.Builder builder)
+ private static Pair<float[], float[]> loadCurveFromXml(XmlPullParser parser)
throws IOException, XmlPullParserException {
final int outerDepth = parser.getDepth();
List<Float> luxLevels = new ArrayList<>();
@@ -666,11 +715,19 @@ final class PersistentDataStore {
public void saveToXml(XmlSerializer serializer) throws IOException {
for (int i = 0; i < mConfigurations.size(); i++) {
- final int userSerial= mConfigurations.keyAt(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));
+ String packageName = mPackageNames.get(userSerial);
+ if (packageName != null) {
+ serializer.attribute(null, ATTR_PACKAGE_NAME, packageName);
+ }
+ long timestamp = mTimeStamps.get(userSerial, -1);
+ if (timestamp != -1) {
+ serializer.attribute(null, ATTR_TIME_STAMP, Long.toString(timestamp));
+ }
saveConfigurationToXml(serializer, config);
serializer.endTag(null, TAG_BRIGHTNESS_CONFIGURATION);
}
@@ -679,6 +736,9 @@ final class PersistentDataStore {
private static void saveConfigurationToXml(XmlSerializer serializer,
BrightnessConfiguration config) throws IOException {
serializer.startTag(null, TAG_BRIGHTNESS_CURVE);
+ if (config.getDescription() != null) {
+ serializer.attribute(null, ATTR_DESCRIPTION, config.getDescription());
+ }
final Pair<float[], float[]> curve = config.getCurve();
for (int i = 0; i < curve.first.length; i++) {
serializer.startTag(null, TAG_BRIGHTNESS_POINT);
@@ -691,8 +751,16 @@ final class PersistentDataStore {
public void dump(final PrintWriter pw, final String prefix) {
for (int i = 0; i < mConfigurations.size(); i++) {
- final int userSerial= mConfigurations.keyAt(i);
+ final int userSerial = mConfigurations.keyAt(i);
+ long time = mTimeStamps.get(userSerial, -1);
+ String packageName = mPackageNames.get(userSerial);
pw.println(prefix + "User " + userSerial + ":");
+ if (time != -1) {
+ pw.println(prefix + " set at: " + TimeUtils.formatForLogging(time));
+ }
+ if (packageName != null) {
+ pw.println(prefix + " set by: " + packageName);
+ }
pw.println(prefix + " " + mConfigurations.valueAt(i));
}
}
diff --git a/com/android/server/ethernet/EthernetNetworkFactory.java b/com/android/server/ethernet/EthernetNetworkFactory.java
index 0bbe3b5e..2e5d09e2 100644
--- a/com/android/server/ethernet/EthernetNetworkFactory.java
+++ b/com/android/server/ethernet/EthernetNetworkFactory.java
@@ -429,6 +429,7 @@ class EthernetNetworkFactory {
mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
+ mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED);
mNetworkCapabilities.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
// We have no useful data on bandwidth. Say 100M up and 100M down. :-(
mNetworkCapabilities.setLinkUpstreamBandwidthKbps(100 * 1000);
diff --git a/com/android/server/fingerprint/AuthenticationClient.java b/com/android/server/fingerprint/AuthenticationClient.java
index 370e569f..d30b13c3 100644
--- a/com/android/server/fingerprint/AuthenticationClient.java
+++ b/com/android/server/fingerprint/AuthenticationClient.java
@@ -16,18 +16,22 @@
package com.android.server.fingerprint;
-import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-
import android.content.Context;
+import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprint;
import android.hardware.fingerprint.Fingerprint;
+import android.hardware.fingerprint.FingerprintDialog;
import android.hardware.fingerprint.FingerprintManager;
+import android.hardware.fingerprint.IFingerprintDialogReceiver;
import android.hardware.fingerprint.IFingerprintServiceReceiver;
+import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Slog;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.statusbar.IStatusBarService;
+
/**
* A class to keep track of the authentication state for a given client.
*/
@@ -41,11 +45,99 @@ public abstract class AuthenticationClient extends ClientMonitor {
public static final int LOCKOUT_TIMED = 1;
public static final int LOCKOUT_PERMANENT = 2;
+ // Callback mechanism received from the client
+ // (FingerprintDialog -> FingerprintManager -> FingerprintService -> AuthenticationClient)
+ private IFingerprintDialogReceiver mDialogReceiverFromClient;
+ private Bundle mBundle;
+ private IStatusBarService mStatusBarService;
+ private boolean mInLockout;
+ private final FingerprintManager mFingerprintManager;
+ protected boolean mDialogDismissed;
+
+ // Receives events from SystemUI
+ protected IFingerprintDialogReceiver mDialogReceiver = new IFingerprintDialogReceiver.Stub() {
+ @Override // binder call
+ public void onDialogDismissed(int reason) {
+ if (mBundle != null && mDialogReceiverFromClient != null) {
+ try {
+ mDialogReceiverFromClient.onDialogDismissed(reason);
+ if (reason == FingerprintDialog.DISMISSED_REASON_USER_CANCEL) {
+ onError(FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED,
+ 0 /* vendorCode */);
+ }
+ mDialogDismissed = true;
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to notify dialog dismissed", e);
+ }
+ stop(true /* initiatedByClient */);
+ }
+ }
+ };
+
public AuthenticationClient(Context context, long halDeviceId, IBinder token,
IFingerprintServiceReceiver receiver, int targetUserId, int groupId, long opId,
- boolean restricted, String owner) {
+ boolean restricted, String owner, Bundle bundle,
+ IFingerprintDialogReceiver dialogReceiver, IStatusBarService statusBarService) {
super(context, halDeviceId, token, receiver, targetUserId, groupId, restricted, owner);
mOpId = opId;
+ mBundle = bundle;
+ mDialogReceiverFromClient = dialogReceiver;
+ mStatusBarService = statusBarService;
+ mFingerprintManager = (FingerprintManager) getContext()
+ .getSystemService(Context.FINGERPRINT_SERVICE);
+ }
+
+ @Override
+ public void binderDied() {
+ super.binderDied();
+ // When the binder dies, we should stop the client. This probably belongs in
+ // ClientMonitor's binderDied(), but testing all the cases would be tricky.
+ // AuthenticationClient is the most user-visible case.
+ stop(false /* initiatedByClient */);
+ }
+
+ @Override
+ public boolean onAcquired(int acquiredInfo, int vendorCode) {
+ // If the dialog is showing, the client doesn't need to receive onAcquired messages.
+ if (mBundle != null) {
+ try {
+ if (acquiredInfo != FingerprintManager.FINGERPRINT_ACQUIRED_GOOD) {
+ mStatusBarService.onFingerprintHelp(
+ mFingerprintManager.getAcquiredString(acquiredInfo, vendorCode));
+ }
+ return false; // acquisition continues
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote exception when sending acquired message", e);
+ return true; // client failed
+ } finally {
+ // Good scans will keep the device awake
+ if (acquiredInfo == FingerprintManager.FINGERPRINT_ACQUIRED_GOOD) {
+ notifyUserActivity();
+ }
+ }
+ } else {
+ return super.onAcquired(acquiredInfo, vendorCode);
+ }
+ }
+
+ @Override
+ public boolean onError(int error, int vendorCode) {
+ if (mDialogDismissed) {
+ // If user cancels authentication, the application has already received the
+ // FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED message from onDialogDismissed()
+ // and stopped the fingerprint hardware, so there is no need to send a
+ // FingerprintManager.FINGERPRINT_ERROR_CANCELED message.
+ return true;
+ }
+ if (mBundle != null) {
+ try {
+ mStatusBarService.onFingerprintError(
+ mFingerprintManager.getErrorString(error, vendorCode));
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Remote exception when sending error", e);
+ }
+ }
+ return super.onError(error, vendorCode);
}
@Override
@@ -53,6 +145,20 @@ public abstract class AuthenticationClient extends ClientMonitor {
boolean result = false;
boolean authenticated = fingerId != 0;
+ // If the fingerprint dialog is showing, notify authentication succeeded
+ if (mBundle != null) {
+ try {
+ if (authenticated) {
+ mStatusBarService.onFingerprintAuthenticated();
+ } else {
+ mStatusBarService.onFingerprintHelp(getContext().getResources().getString(
+ com.android.internal.R.string.fingerprint_not_recognized));
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to notify Authenticated:", e);
+ }
+ }
+
IFingerprintServiceReceiver receiver = getReceiver();
if (receiver != null) {
try {
@@ -85,13 +191,24 @@ public abstract class AuthenticationClient extends ClientMonitor {
int lockoutMode = handleFailedAttempt();
if (lockoutMode != LOCKOUT_NONE) {
try {
+ mInLockout = true;
Slog.w(TAG, "Forcing lockout (fp driver code should do this!), mode(" +
lockoutMode + ")");
stop(false);
int errorCode = lockoutMode == LOCKOUT_TIMED ?
FingerprintManager.FINGERPRINT_ERROR_LOCKOUT :
FingerprintManager.FINGERPRINT_ERROR_LOCKOUT_PERMANENT;
+
+ // TODO: if the dialog is showing, this error should be delayed. On a similar
+ // note, AuthenticationClient should override onError and delay all other errors
+ // as well, if the dialog is showing
receiver.onError(getHalDeviceId(), errorCode, 0 /* vendorCode */);
+
+ // Send the lockout message to the system dialog
+ if (mBundle != null) {
+ mStatusBarService.onFingerprintError(
+ mFingerprintManager.getErrorString(errorCode, 0 /* vendorCode */));
+ }
} catch (RemoteException e) {
Slog.w(TAG, "Failed to notify lockout:", e);
}
@@ -126,6 +243,15 @@ public abstract class AuthenticationClient extends ClientMonitor {
return result;
}
if (DEBUG) Slog.w(TAG, "client " + getOwnerString() + " is authenticating...");
+
+ // If authenticating with system dialog, show the dialog
+ if (mBundle != null) {
+ try {
+ mStatusBarService.showFingerprintDialog(mBundle, mDialogReceiver);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to show fingerprint dialog", e);
+ }
+ }
} catch (RemoteException e) {
Slog.e(TAG, "startAuthentication failed", e);
return ERROR_ESRCH;
@@ -139,6 +265,7 @@ public abstract class AuthenticationClient extends ClientMonitor {
Slog.w(TAG, "stopAuthentication: already cancelled!");
return 0;
}
+
IBiometricsFingerprint daemon = getFingerprintDaemon();
if (daemon == null) {
Slog.w(TAG, "stopAuthentication: no fingerprint HAL!");
@@ -154,6 +281,18 @@ public abstract class AuthenticationClient extends ClientMonitor {
} catch (RemoteException e) {
Slog.e(TAG, "stopAuthentication failed", e);
return ERROR_ESRCH;
+ } finally {
+ // If the user already cancelled authentication (via some interaction with the
+ // dialog, we do not need to hide it since it's already hidden.
+ // If the device is in lockout, don't hide the dialog - it will automatically hide
+ // after FingerprintDialog.HIDE_DIALOG_DELAY
+ if (mBundle != null && !mDialogDismissed && !mInLockout) {
+ try {
+ mStatusBarService.hideFingerprintDialog();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Unable to hide fingerprint dialog", e);
+ }
+ }
}
mAlreadyCancelled = true;
return 0; // success
diff --git a/com/android/server/fingerprint/FingerprintService.java b/com/android/server/fingerprint/FingerprintService.java
index d0d951b8..b5f94b1c 100644
--- a/com/android/server/fingerprint/FingerprintService.java
+++ b/com/android/server/fingerprint/FingerprintService.java
@@ -40,10 +40,12 @@ import android.hardware.biometrics.fingerprint.V2_1.IBiometricsFingerprintClient
import android.hardware.fingerprint.Fingerprint;
import android.hardware.fingerprint.FingerprintManager;
import android.hardware.fingerprint.IFingerprintClientActiveCallback;
+import android.hardware.fingerprint.IFingerprintDialogReceiver;
import android.hardware.fingerprint.IFingerprintService;
import android.hardware.fingerprint.IFingerprintServiceLockoutResetCallback;
import android.hardware.fingerprint.IFingerprintServiceReceiver;
import android.os.Binder;
+import android.os.Build;
import android.os.Bundle;
import android.os.DeadObjectException;
import android.os.Environment;
@@ -55,7 +57,9 @@ import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import android.os.RemoteException;
import android.os.SELinux;
+import android.os.ServiceManager;
import android.os.SystemClock;
+import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
import android.security.KeyStore;
@@ -66,6 +70,7 @@ import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.MetricsLogger;
+import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.util.DumpUtils;
import com.android.server.SystemServerInitThreadPool;
import com.android.server.SystemService;
@@ -131,6 +136,7 @@ public class FingerprintService extends SystemService implements IHwBinder.Death
private SparseIntArray mFailedAttempts;
@GuardedBy("this")
private IBiometricsFingerprint mDaemon;
+ private IStatusBarService mStatusBarService;
private final PowerManager mPowerManager;
private final AlarmManager mAlarmManager;
private final UserManager mUserManager;
@@ -222,6 +228,8 @@ public class FingerprintService extends SystemService implements IHwBinder.Death
mUserManager = UserManager.get(mContext);
mTimedLockoutCleared = new SparseBooleanArray();
mFailedAttempts = new SparseIntArray();
+ mStatusBarService = IStatusBarService.Stub.asInterface(
+ ServiceManager.getService(Context.STATUS_BAR_SERVICE));
}
@Override
@@ -808,13 +816,14 @@ public class FingerprintService extends SystemService implements IHwBinder.Death
private void startAuthentication(IBinder token, long opId, int callingUserId, int groupId,
IFingerprintServiceReceiver receiver, int flags, boolean restricted,
- String opPackageName) {
+ String opPackageName, Bundle bundle, IFingerprintDialogReceiver dialogReceiver) {
updateActiveGroup(groupId, opPackageName);
if (DEBUG) Slog.v(TAG, "startAuthentication(" + opPackageName + ")");
AuthenticationClient client = new AuthenticationClient(getContext(), mHalDeviceId, token,
- receiver, mCurrentUserId, groupId, opId, restricted, opPackageName) {
+ receiver, mCurrentUserId, groupId, opId, restricted, opPackageName, bundle,
+ dialogReceiver, mStatusBarService) {
@Override
public int handleFailedAttempt() {
final int currentUser = ActivityManager.getCurrentUser();
@@ -1037,7 +1046,7 @@ public class FingerprintService extends SystemService implements IHwBinder.Death
final IFingerprintServiceReceiver receiver, final int flags,
final String opPackageName) {
checkPermission(MANAGE_FINGERPRINT);
- final int limit = mContext.getResources().getInteger(
+ final int limit = mContext.getResources().getInteger(
com.android.internal.R.integer.config_fingerprintMaxTemplatesPerUser);
final int enrolled = FingerprintService.this.getEnrolledFingerprints(userId).size();
@@ -1085,7 +1094,8 @@ public class FingerprintService extends SystemService implements IHwBinder.Death
@Override // Binder call
public void authenticate(final IBinder token, final long opId, final int groupId,
final IFingerprintServiceReceiver receiver, final int flags,
- final String opPackageName) {
+ final String opPackageName, final Bundle bundle,
+ final IFingerprintDialogReceiver dialogReceiver) {
final int callingUid = Binder.getCallingUid();
final int callingPid = Binder.getCallingPid();
final int callingUserId = UserHandle.getCallingUserId();
@@ -1113,7 +1123,7 @@ public class FingerprintService extends SystemService implements IHwBinder.Death
mPerformanceStats = stats;
startAuthentication(token, opId, callingUserId, groupId, receiver,
- flags, restricted, opPackageName);
+ flags, restricted, opPackageName, bundle, dialogReceiver);
}
});
}
@@ -1411,8 +1421,17 @@ public class FingerprintService extends SystemService implements IHwBinder.Death
try {
userId = getUserOrWorkProfileId(clientPackage, userId);
if (userId != mCurrentUserId) {
- final File systemDir = Environment.getUserSystemDirectory(userId);
- final File fpDir = new File(systemDir, FP_DATA_DIR);
+ File baseDir;
+ if (Build.VERSION.FIRST_SDK_INT <= Build.VERSION_CODES.O_MR1
+ && !SystemProperties.getBoolean(
+ "ro.treble.supports_vendor_data", false)) {
+ // TODO(b/72405644) remove the override when possible.
+ baseDir = Environment.getUserSystemDirectory(userId);
+ } else {
+ baseDir = Environment.getDataVendorDeDirectory(userId);
+ }
+
+ File fpDir = new File(baseDir, FP_DATA_DIR);
if (!fpDir.exists()) {
if (!fpDir.mkdir()) {
Slog.v(TAG, "Cannot make directory: " + fpDir.getAbsolutePath());
@@ -1426,6 +1445,7 @@ public class FingerprintService extends SystemService implements IHwBinder.Death
return;
}
}
+
daemon.setActiveGroup(userId, fpDir.getAbsolutePath());
mCurrentUserId = userId;
}
diff --git a/com/android/server/hdmi/DeviceDiscoveryAction.java b/com/android/server/hdmi/DeviceDiscoveryAction.java
index 97a6e850..db8dedbf 100644
--- a/com/android/server/hdmi/DeviceDiscoveryAction.java
+++ b/com/android/server/hdmi/DeviceDiscoveryAction.java
@@ -228,12 +228,20 @@ final class DeviceDiscoveryAction extends HdmiCecFeatureAction {
if (cmd.getOpcode() == Constants.MESSAGE_SET_OSD_NAME) {
handleSetOsdName(cmd);
return true;
+ } else if ((cmd.getOpcode() == Constants.MESSAGE_FEATURE_ABORT) &&
+ ((cmd.getParams()[0] & 0xFF) == Constants.MESSAGE_GIVE_OSD_NAME)) {
+ handleSetOsdName(cmd);
+ return true;
}
return false;
case STATE_WAITING_FOR_VENDOR_ID:
if (cmd.getOpcode() == Constants.MESSAGE_DEVICE_VENDOR_ID) {
handleVendorId(cmd);
return true;
+ } else if ((cmd.getOpcode() == Constants.MESSAGE_FEATURE_ABORT) &&
+ ((cmd.getParams()[0] & 0xFF) == Constants.MESSAGE_GIVE_DEVICE_VENDOR_ID)) {
+ handleVendorId(cmd);
+ return true;
}
return false;
case STATE_WAITING_FOR_DEVICE_POLLING:
@@ -281,7 +289,11 @@ final class DeviceDiscoveryAction extends HdmiCecFeatureAction {
String displayName = null;
try {
- displayName = new String(cmd.getParams(), "US-ASCII");
+ if (cmd.getOpcode() == Constants.MESSAGE_FEATURE_ABORT) {
+ displayName = HdmiUtils.getDefaultDeviceName(current.mLogicalAddress);
+ } else {
+ displayName = new String(cmd.getParams(), "US-ASCII");
+ }
} catch (UnsupportedEncodingException e) {
Slog.w(TAG, "Failed to decode display name: " + cmd.toString());
// If failed to get display name, use the default name of device.
@@ -302,9 +314,12 @@ final class DeviceDiscoveryAction extends HdmiCecFeatureAction {
return;
}
- byte[] params = cmd.getParams();
- int vendorId = HdmiUtils.threeBytesToInt(params);
- current.mVendorId = vendorId;
+ if (cmd.getOpcode() != Constants.MESSAGE_FEATURE_ABORT) {
+ byte[] params = cmd.getParams();
+ int vendorId = HdmiUtils.threeBytesToInt(params);
+ current.mVendorId = vendorId;
+ }
+
increaseProcessedDeviceCount();
checkAndProceedStage();
}
diff --git a/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
index 81bccdc7..1e09383d 100644
--- a/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
+++ b/com/android/server/hdmi/HdmiCecLocalDeviceTv.java
@@ -698,10 +698,9 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
protected boolean handleReportAudioStatus(HdmiCecMessage message) {
assertRunOnServiceThread();
- byte params[] = message.getParams();
- int mute = params[0] & 0x80;
- int volume = params[0] & 0x7F;
- setAudioStatus(mute == 0x80, volume);
+ boolean mute = HdmiUtils.isAudioStatusMute(message);
+ int volume = HdmiUtils.getAudioStatusVolume(message);
+ setAudioStatus(mute, volume);
return true;
}
@@ -1004,6 +1003,9 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
}
void setAudioStatus(boolean mute, int volume) {
+ if (!isSystemAudioActivated()) {
+ return;
+ }
synchronized (mLock) {
mSystemAudioMute = mute;
mSystemAudioVolume = volume;
@@ -1019,6 +1021,10 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
@ServiceThreadOnly
void changeVolume(int curVolume, int delta, int maxVolume) {
assertRunOnServiceThread();
+ if (getAvrDeviceInfo() == null) {
+ // On initialization process, getAvrDeviceInfo() may return null and cause exception
+ return;
+ }
if (delta == 0 || !isSystemAudioActivated()) {
return;
}
@@ -1048,6 +1054,10 @@ final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice {
@ServiceThreadOnly
void changeMute(boolean mute) {
assertRunOnServiceThread();
+ if (getAvrDeviceInfo() == null) {
+ // On initialization process, getAvrDeviceInfo() may return null and cause exception
+ return;
+ }
HdmiLogger.debug("[A]:Change mute:%b", mute);
synchronized (mLock) {
if (mSystemAudioMute == mute) {
diff --git a/com/android/server/hdmi/HdmiControlService.java b/com/android/server/hdmi/HdmiControlService.java
index 807b1b19..3d079ccb 100644
--- a/com/android/server/hdmi/HdmiControlService.java
+++ b/com/android/server/hdmi/HdmiControlService.java
@@ -989,8 +989,12 @@ public final class HdmiControlService extends SystemService {
}
// FLAG_HDMI_SYSTEM_AUDIO_VOLUME prevents audio manager from announcing
// volume change notification back to hdmi control service.
- audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume,
- AudioManager.FLAG_SHOW_UI | AudioManager.FLAG_HDMI_SYSTEM_AUDIO_VOLUME);
+ int flag = AudioManager.FLAG_HDMI_SYSTEM_AUDIO_VOLUME;
+ if (0 <= volume && volume <= 100) {
+ Slog.i(TAG, "volume: " + volume);
+ flag |= AudioManager.FLAG_SHOW_UI;
+ audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, volume, flag);
+ }
}
}
diff --git a/com/android/server/hdmi/HdmiUtils.java b/com/android/server/hdmi/HdmiUtils.java
index 8b164118..4ac3bba7 100644
--- a/com/android/server/hdmi/HdmiUtils.java
+++ b/com/android/server/hdmi/HdmiUtils.java
@@ -152,6 +152,32 @@ final class HdmiUtils {
}
/**
+ * Parse the <Report Audio Status> message and check if it is mute
+ *
+ * @param cmd the CEC message to parse
+ * @return true if the given parameter has [MUTE]
+ */
+ static boolean isAudioStatusMute(HdmiCecMessage cmd) {
+ byte params[] = cmd.getParams();
+ return (params[0] & 0x80) == 0x80;
+ }
+
+ /**
+ * Parse the <Report Audio Status> message and extract the volume
+ *
+ * @param cmd the CEC message to parse
+ * @return device's volume. Constants.UNKNOWN_VOLUME in case it is out of range
+ */
+ static int getAudioStatusVolume(HdmiCecMessage cmd) {
+ byte params[] = cmd.getParams();
+ int volume = params[0] & 0x7F;
+ if (volume < 0x00 || 0x64 < volume) {
+ volume = Constants.UNKNOWN_VOLUME;
+ }
+ return volume;
+ }
+
+ /**
* Convert integer array to list of {@link Integer}.
*
* <p>The result is immutable.
diff --git a/com/android/server/hdmi/SystemAudioStatusAction.java b/com/android/server/hdmi/SystemAudioStatusAction.java
index cab8439b..d41a36ca 100644
--- a/com/android/server/hdmi/SystemAudioStatusAction.java
+++ b/com/android/server/hdmi/SystemAudioStatusAction.java
@@ -92,8 +92,8 @@ final class SystemAudioStatusAction extends HdmiCecFeatureAction {
private void handleReportAudioStatus(HdmiCecMessage cmd) {
byte[] params = cmd.getParams();
- boolean mute = (params[0] & 0x80) == 0x80;
- int volume = params[0] & 0x7F;
+ boolean mute = HdmiUtils.isAudioStatusMute(cmd);
+ int volume = HdmiUtils.getAudioStatusVolume(cmd);
tv().setAudioStatus(mute, volume);
if (!(tv().isSystemAudioActivated() ^ mute)) {
diff --git a/com/android/server/hdmi/VolumeControlAction.java b/com/android/server/hdmi/VolumeControlAction.java
index cd38b1fb..0011387f 100644
--- a/com/android/server/hdmi/VolumeControlAction.java
+++ b/com/android/server/hdmi/VolumeControlAction.java
@@ -139,8 +139,8 @@ final class VolumeControlAction extends HdmiCecFeatureAction {
private boolean handleReportAudioStatus(HdmiCecMessage cmd) {
byte params[] = cmd.getParams();
- boolean mute = (params[0] & 0x80) == 0x80;
- int volume = params[0] & 0x7F;
+ boolean mute = HdmiUtils.isAudioStatusMute(cmd);
+ int volume = HdmiUtils.getAudioStatusVolume(cmd);
mLastAvrVolume = volume;
mLastAvrMute = mute;
if (shouldUpdateAudioVolume(mute)) {
diff --git a/com/android/server/job/GrantedUriPermissions.java b/com/android/server/job/GrantedUriPermissions.java
index c23b109b..8fecb8fa 100644
--- a/com/android/server/job/GrantedUriPermissions.java
+++ b/com/android/server/job/GrantedUriPermissions.java
@@ -25,6 +25,7 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -153,4 +154,21 @@ public final class GrantedUriPermissions {
pw.println(mUris.get(i));
}
}
+
+ public void dump(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(GrantedUriPermissionsDumpProto.FLAGS, mGrantFlags);
+ proto.write(GrantedUriPermissionsDumpProto.SOURCE_USER_ID, mSourceUserId);
+ proto.write(GrantedUriPermissionsDumpProto.TAG, mTag);
+ proto.write(GrantedUriPermissionsDumpProto.PERMISSION_OWNER, mPermissionOwner.toString());
+ for (int i = 0; i < mUris.size(); i++) {
+ Uri u = mUris.get(i);
+ if (u != null) {
+ proto.write(GrantedUriPermissionsDumpProto.URIS, u.toString());
+ }
+ }
+
+ proto.end(token);
+ }
}
diff --git a/com/android/server/job/JobPackageTracker.java b/com/android/server/job/JobPackageTracker.java
index 296743b5..8b8faa34 100644
--- a/com/android/server/job/JobPackageTracker.java
+++ b/com/android/server/job/JobPackageTracker.java
@@ -28,6 +28,7 @@ import android.util.ArrayMap;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.util.RingBufferIndices;
import com.android.server.job.controllers.JobStatus;
@@ -308,13 +309,13 @@ public final class JobPackageTracker {
}
}
- void dump(PrintWriter pw, String header, String prefix, long now, long nowEllapsed,
+ void dump(PrintWriter pw, String header, String prefix, long now, long nowElapsed,
int filterUid) {
final long period = getTotalTime(now);
pw.print(prefix); pw.print(header); pw.print(" at ");
pw.print(DateFormat.format("yyyy-MM-dd-HH-mm-ss", mStartClockTime).toString());
pw.print(" (");
- TimeUtils.formatDuration(mStartElapsedTime, nowEllapsed, pw);
+ TimeUtils.formatDuration(mStartElapsedTime, nowElapsed, pw);
pw.print(") over ");
TimeUtils.formatDuration(period, pw);
pw.println(":");
@@ -365,6 +366,73 @@ public final class JobPackageTracker {
pw.print(mMaxTotalActive); pw.print(" total, ");
pw.print(mMaxFgActive); pw.println(" foreground");
}
+
+ private void printPackageEntryState(ProtoOutputStream proto, long fieldId,
+ long duration, int count) {
+ final long token = proto.start(fieldId);
+ proto.write(DataSetProto.PackageEntryProto.State.DURATION_MS, duration);
+ proto.write(DataSetProto.PackageEntryProto.State.COUNT, count);
+ proto.end(token);
+ }
+
+ void dump(ProtoOutputStream proto, long fieldId, long now, long nowElapsed, int filterUid) {
+ final long token = proto.start(fieldId);
+ final long period = getTotalTime(now);
+
+ proto.write(DataSetProto.START_CLOCK_TIME_MS, mStartClockTime);
+ proto.write(DataSetProto.ELAPSED_TIME_MS, nowElapsed - mStartElapsedTime);
+ proto.write(DataSetProto.PERIOD_MS, period);
+
+ final int NE = mEntries.size();
+ for (int i = 0; i < NE; i++) {
+ int uid = mEntries.keyAt(i);
+ if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) {
+ continue;
+ }
+ ArrayMap<String, PackageEntry> uidMap = mEntries.valueAt(i);
+ final int NP = uidMap.size();
+ for (int j = 0; j < NP; j++) {
+ final long peToken = proto.start(DataSetProto.PACKAGE_ENTRIES);
+ PackageEntry pe = uidMap.valueAt(j);
+
+ proto.write(DataSetProto.PackageEntryProto.UID, uid);
+ proto.write(DataSetProto.PackageEntryProto.PACKAGE_NAME, uidMap.keyAt(j));
+
+ printPackageEntryState(proto, DataSetProto.PackageEntryProto.PENDING_STATE,
+ pe.getPendingTime(now), pe.pendingCount);
+ printPackageEntryState(proto, DataSetProto.PackageEntryProto.ACTIVE_STATE,
+ pe.getActiveTime(now), pe.activeCount);
+ printPackageEntryState(proto, DataSetProto.PackageEntryProto.ACTIVE_TOP_STATE,
+ pe.getActiveTopTime(now), pe.activeTopCount);
+
+ proto.write(DataSetProto.PackageEntryProto.PENDING,
+ pe.pendingNesting > 0 || pe.hadPending);
+ proto.write(DataSetProto.PackageEntryProto.ACTIVE,
+ pe.activeNesting > 0 || pe.hadActive);
+ proto.write(DataSetProto.PackageEntryProto.ACTIVE_TOP,
+ pe.activeTopNesting > 0 || pe.hadActiveTop);
+
+ for (int k = 0; k < pe.stopReasons.size(); k++) {
+ final long srcToken =
+ proto.start(DataSetProto.PackageEntryProto.STOP_REASONS);
+
+ proto.write(DataSetProto.PackageEntryProto.StopReasonCount.REASON,
+ pe.stopReasons.keyAt(k));
+ proto.write(DataSetProto.PackageEntryProto.StopReasonCount.COUNT,
+ pe.stopReasons.valueAt(k));
+
+ proto.end(srcToken);
+ }
+
+ proto.end(peToken);
+ }
+ }
+
+ proto.write(DataSetProto.MAX_CONCURRENCY, mMaxTotalActive);
+ proto.write(DataSetProto.MAX_FOREGROUND_CONCURRENCY, mMaxFgActive);
+
+ proto.end(token);
+ }
}
void rebatchIfNeeded(long now) {
@@ -450,7 +518,7 @@ public final class JobPackageTracker {
public void dump(PrintWriter pw, String prefix, int filterUid) {
final long now = sUptimeMillisClock.millis();
- final long nowEllapsed = sElapsedRealtimeClock.millis();
+ final long nowElapsed = sElapsedRealtimeClock.millis();
final DataSet total;
if (mLastDataSets[0] != null) {
total = new DataSet(mLastDataSets[0]);
@@ -461,11 +529,37 @@ public final class JobPackageTracker {
mCurDataSet.addTo(total, now);
for (int i = 1; i < mLastDataSets.length; i++) {
if (mLastDataSets[i] != null) {
- mLastDataSets[i].dump(pw, "Historical stats", prefix, now, nowEllapsed, filterUid);
+ mLastDataSets[i].dump(pw, "Historical stats", prefix, now, nowElapsed, filterUid);
pw.println();
}
}
- total.dump(pw, "Current stats", prefix, now, nowEllapsed, filterUid);
+ total.dump(pw, "Current stats", prefix, now, nowElapsed, filterUid);
+ }
+
+ public void dump(ProtoOutputStream proto, long fieldId, int filterUid) {
+ final long token = proto.start(fieldId);
+ final long now = sUptimeMillisClock.millis();
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+
+ final DataSet total;
+ if (mLastDataSets[0] != null) {
+ total = new DataSet(mLastDataSets[0]);
+ mLastDataSets[0].addTo(total, now);
+ } else {
+ total = new DataSet(mCurDataSet);
+ }
+ mCurDataSet.addTo(total, now);
+
+ for (int i = 1; i < mLastDataSets.length; i++) {
+ if (mLastDataSets[i] != null) {
+ mLastDataSets[i].dump(proto, JobPackageTrackerDumpProto.HISTORICAL_STATS,
+ now, nowElapsed, filterUid);
+ }
+ }
+ total.dump(proto, JobPackageTrackerDumpProto.CURRENT_STATS,
+ now, nowElapsed, filterUid);
+
+ proto.end(token);
}
public boolean dumpHistory(PrintWriter pw, String prefix, int filterUid) {
@@ -512,4 +606,40 @@ public final class JobPackageTracker {
}
return true;
}
+
+ public void dumpHistory(ProtoOutputStream proto, long fieldId, int filterUid) {
+ final int size = mEventIndices.size();
+ if (size == 0) {
+ return;
+ }
+ final long token = proto.start(fieldId);
+
+ final long now = sElapsedRealtimeClock.millis();
+ for (int i = 0; i < size; i++) {
+ final int index = mEventIndices.indexOf(i);
+ final int uid = mEventUids[index];
+ if (filterUid != -1 && filterUid != UserHandle.getAppId(uid)) {
+ continue;
+ }
+ final int cmd = mEventCmds[index] & EVENT_CMD_MASK;
+ if (cmd == EVENT_NULL) {
+ continue;
+ }
+ final long heToken = proto.start(JobPackageHistoryProto.HISTORY_EVENT);
+
+ proto.write(JobPackageHistoryProto.HistoryEvent.EVENT, cmd);
+ proto.write(JobPackageHistoryProto.HistoryEvent.TIME_SINCE_EVENT_MS, now - mEventTimes[index]);
+ proto.write(JobPackageHistoryProto.HistoryEvent.UID, uid);
+ proto.write(JobPackageHistoryProto.HistoryEvent.JOB_ID, mEventJobIds[index]);
+ proto.write(JobPackageHistoryProto.HistoryEvent.TAG, mEventTags[index]);
+ if (cmd == EVENT_STOP_JOB || cmd == EVENT_STOP_PERIODIC_JOB) {
+ proto.write(JobPackageHistoryProto.HistoryEvent.STOP_REASON,
+ (mEventCmds[index] & EVENT_STOP_REASON_MASK) >> EVENT_STOP_REASON_SHIFT);
+ }
+
+ proto.end(heToken);
+ }
+
+ proto.end(token);
+ }
}
diff --git a/com/android/server/job/JobSchedulerInternal.java b/com/android/server/job/JobSchedulerInternal.java
index c97eeaf3..4e749085 100644
--- a/com/android/server/job/JobSchedulerInternal.java
+++ b/com/android/server/job/JobSchedulerInternal.java
@@ -59,7 +59,6 @@ public interface JobSchedulerInternal {
/**
* Stats about the first load after boot and the most recent save.
- * STOPSHIP Remove it and the relevant code once b/64536115 is fixed.
*/
public class JobStorePersistStats {
public int countAllJobsLoaded = -1;
diff --git a/com/android/server/job/JobSchedulerService.java b/com/android/server/job/JobSchedulerService.java
index bcb57eff..5da470e6 100644
--- a/com/android/server/job/JobSchedulerService.java
+++ b/com/android/server/job/JobSchedulerService.java
@@ -21,6 +21,7 @@ import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
import android.app.Activity;
import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
import android.app.AppGlobals;
import android.app.IUidObserver;
import android.app.job.IJobScheduler;
@@ -63,7 +64,9 @@ import android.util.KeyValueListParser;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
+import android.util.StatsLog;
import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
@@ -71,9 +74,13 @@ import com.android.internal.app.procstats.ProcessStats;
import com.android.internal.os.BackgroundThread;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
+import com.android.internal.util.Preconditions;
import com.android.server.DeviceIdleController;
import com.android.server.FgThread;
+import com.android.server.ForceAppStandbyTracker;
import com.android.server.LocalServices;
+import com.android.server.job.JobSchedulerServiceDumpProto.ActiveJob;
+import com.android.server.job.JobSchedulerServiceDumpProto.PendingJob;
import com.android.server.job.JobStore.JobStatusFunctor;
import com.android.server.job.controllers.AppIdleController;
import com.android.server.job.controllers.BackgroundJobsController;
@@ -98,6 +105,7 @@ import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
+import java.util.function.Predicate;
/**
* Responsible for taking jobs representing work to be performed by a client app, and determining
@@ -171,8 +179,10 @@ public final class JobSchedulerService extends com.android.server.SystemService
final JobSchedulerStub mJobSchedulerStub;
PackageManagerInternal mLocalPM;
+ ActivityManagerInternal mActivityManagerInternal;
IBatteryStats mBatteryStats;
DeviceIdleController.LocalService mLocalDeviceIdleController;
+ final ForceAppStandbyTracker mForceAppStandbyTracker;
/**
* Set to true once we are allowed to run third party apps.
@@ -185,13 +195,18 @@ public final class JobSchedulerService extends com.android.server.SystemService
boolean mReportedActive;
/**
+ * Are we currently in device-wide standby parole?
+ */
+ volatile boolean mInParole;
+
+ /**
* Current limit on the number of concurrent JobServiceContext entries we want to
* keep actively running a job.
*/
int mMaxActiveJobs = 1;
/**
- * Which uids are currently in the foreground.
+ * A mapping of which uids are currently in the foreground to their effective priority.
*/
final SparseIntArray mUidPriorityOverride = new SparseIntArray();
@@ -466,11 +481,11 @@ public final class JobSchedulerService extends com.android.server.SystemService
DEFAULT_MAX_STANDARD_RESCHEDULE_COUNT);
MAX_WORK_RESCHEDULE_COUNT = mParser.getInt(KEY_MAX_WORK_RESCHEDULE_COUNT,
DEFAULT_MAX_WORK_RESCHEDULE_COUNT);
- MIN_LINEAR_BACKOFF_TIME = mParser.getLong(KEY_MIN_LINEAR_BACKOFF_TIME,
+ MIN_LINEAR_BACKOFF_TIME = mParser.getDurationMillis(KEY_MIN_LINEAR_BACKOFF_TIME,
DEFAULT_MIN_LINEAR_BACKOFF_TIME);
- MIN_EXP_BACKOFF_TIME = mParser.getLong(KEY_MIN_EXP_BACKOFF_TIME,
+ MIN_EXP_BACKOFF_TIME = mParser.getDurationMillis(KEY_MIN_EXP_BACKOFF_TIME,
DEFAULT_MIN_EXP_BACKOFF_TIME);
- STANDBY_HEARTBEAT_TIME = mParser.getLong(KEY_STANDBY_HEARTBEAT_TIME,
+ STANDBY_HEARTBEAT_TIME = mParser.getDurationMillis(KEY_STANDBY_HEARTBEAT_TIME,
DEFAULT_STANDBY_HEARTBEAT_TIME);
STANDBY_BEATS[1] = mParser.getInt(KEY_STANDBY_WORKING_BEATS,
DEFAULT_STANDBY_WORKING_BEATS);
@@ -549,6 +564,36 @@ public final class JobSchedulerService extends com.android.server.SystemService
}
pw.println('}');
}
+
+ void dump(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(ConstantsProto.MIN_IDLE_COUNT, MIN_IDLE_COUNT);
+ proto.write(ConstantsProto.MIN_CHARGING_COUNT, MIN_CHARGING_COUNT);
+ proto.write(ConstantsProto.MIN_BATTERY_NOT_LOW_COUNT, MIN_BATTERY_NOT_LOW_COUNT);
+ proto.write(ConstantsProto.MIN_STORAGE_NOT_LOW_COUNT, MIN_STORAGE_NOT_LOW_COUNT);
+ proto.write(ConstantsProto.MIN_CONNECTIVITY_COUNT, MIN_CONNECTIVITY_COUNT);
+ proto.write(ConstantsProto.MIN_CONTENT_COUNT, MIN_CONTENT_COUNT);
+ proto.write(ConstantsProto.MIN_READY_JOBS_COUNT, MIN_READY_JOBS_COUNT);
+ proto.write(ConstantsProto.HEAVY_USE_FACTOR, HEAVY_USE_FACTOR);
+ proto.write(ConstantsProto.MODERATE_USE_FACTOR, MODERATE_USE_FACTOR);
+ proto.write(ConstantsProto.FG_JOB_COUNT, FG_JOB_COUNT);
+ proto.write(ConstantsProto.BG_NORMAL_JOB_COUNT, BG_NORMAL_JOB_COUNT);
+ proto.write(ConstantsProto.BG_MODERATE_JOB_COUNT, BG_MODERATE_JOB_COUNT);
+ proto.write(ConstantsProto.BG_LOW_JOB_COUNT, BG_LOW_JOB_COUNT);
+ proto.write(ConstantsProto.BG_CRITICAL_JOB_COUNT, BG_CRITICAL_JOB_COUNT);
+ proto.write(ConstantsProto.MAX_STANDARD_RESCHEDULE_COUNT, MAX_STANDARD_RESCHEDULE_COUNT);
+ proto.write(ConstantsProto.MAX_WORK_RESCHEDULE_COUNT, MAX_WORK_RESCHEDULE_COUNT);
+ proto.write(ConstantsProto.MIN_LINEAR_BACKOFF_TIME_MS, MIN_LINEAR_BACKOFF_TIME);
+ proto.write(ConstantsProto.MIN_EXP_BACKOFF_TIME_MS, MIN_EXP_BACKOFF_TIME);
+ proto.write(ConstantsProto.STANDBY_HEARTBEAT_TIME_MS, STANDBY_HEARTBEAT_TIME);
+
+ for (int period : STANDBY_BEATS) {
+ proto.write(ConstantsProto.STANDBY_BEATS, period);
+ }
+
+ proto.end(token);
+ }
}
final Constants mConstants;
@@ -739,6 +784,22 @@ public final class JobSchedulerService extends com.android.server.SystemService
mStartedUsers = ArrayUtils.removeInt(mStartedUsers, userHandle);
}
+ /**
+ * Return whether an UID is in the foreground or not.
+ */
+ private boolean isUidInForeground(int uid) {
+ synchronized (mLock) {
+ if (mUidPriorityOverride.get(uid, 0) > 0) {
+ return true;
+ }
+ }
+ // Note UID observer may not be called in time, so we always check with the AM.
+ return mActivityManagerInternal.getUidProcessState(uid)
+ <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+ }
+
+ private final Predicate<Integer> mIsUidInForegroundPredicate = this::isUidInForeground;
+
public int scheduleAsPackage(JobInfo job, JobWorkItem work, int uId, String packageName,
int userId, String tag) {
try {
@@ -758,12 +819,25 @@ public final class JobSchedulerService extends com.android.server.SystemService
// Fast path: we are adding work to an existing job, and the JobInfo is not
// changing. We can just directly enqueue this work in to the job.
if (toCancel.getJob().equals(job)) {
+
toCancel.enqueueWorkLocked(ActivityManager.getService(), work);
+
+ // If any of work item is enqueued when the source is in the foreground,
+ // exempt the entire job.
+ toCancel.maybeAddForegroundExemption(mIsUidInForegroundPredicate);
+
return JobScheduler.RESULT_SUCCESS;
}
}
JobStatus jobStatus = JobStatus.createFromJobInfo(job, uId, packageName, userId, tag);
+
+ // Give exemption if the source is in the foreground just now.
+ // Note if it's a sync job, this method is called on the handler so it's not exactly
+ // the state when requestSync() was called, but that should be fine because of the
+ // 1 minute foreground grace period.
+ jobStatus.maybeAddForegroundExemption(mIsUidInForegroundPredicate);
+
if (DEBUG) Slog.d(TAG, "SCHEDULE: " + jobStatus.toShortString());
// Jobs on behalf of others don't apply to the per-app job cap
if (ENFORCE_MAX_JOBS && packageName == null) {
@@ -785,6 +859,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
jobStatus.enqueueWorkLocked(ActivityManager.getService(), work);
}
startTrackingJobLocked(jobStatus, toCancel);
+ StatsLog.write_non_chained(StatsLog.SCHEDULED_JOB_STATE_CHANGED,
+ uId, null, jobStatus.getBatteryName(), 2);
// If the job is immediately ready to run, then we can just immediately
// put it in the pending list and try to schedule it. This is especially
@@ -896,12 +972,14 @@ public final class JobSchedulerService extends com.android.server.SystemService
* @param uid Uid of the calling client.
* @param jobId Id of the job, provided at schedule-time.
*/
- public boolean cancelJob(int uid, int jobId) {
+ public boolean cancelJob(int uid, int jobId, int callingUid) {
JobStatus toCancel;
synchronized (mLock) {
toCancel = mJobs.getJobByUidAndJobId(uid, jobId);
if (toCancel != null) {
- cancelJobImplLocked(toCancel, null, "cancel() called by app");
+ cancelJobImplLocked(toCancel, null,
+ "cancel() called by app, callingUid=" + callingUid
+ + " uid=" + uid + " jobId=" + jobId);
}
return (toCancel != null);
}
@@ -927,7 +1005,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
// with just the foreground priority. This means that persistent processes
// can never be the top app priority... that is fine.
mUidPriorityOverride.put(uid, JobInfo.PRIORITY_TOP_APP);
- } else if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+ } else if (procState <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
mUidPriorityOverride.put(uid, JobInfo.PRIORITY_FOREGROUND_APP);
} else {
mUidPriorityOverride.delete(uid);
@@ -1004,6 +1082,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
super(context);
mLocalPM = LocalServices.getService(PackageManagerInternal.class);
+ mActivityManagerInternal = Preconditions.checkNotNull(
+ LocalServices.getService(ActivityManagerInternal.class));
mHandler = new JobHandler(context.getMainLooper());
mConstants = new Constants(mHandler);
@@ -1035,6 +1115,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
mDeviceIdleJobsController = DeviceIdleJobsController.get(this);
mControllers.add(mDeviceIdleJobsController);
+ mForceAppStandbyTracker = ForceAppStandbyTracker.getInstance(context);
+
// If the job store determined that it can't yet reschedule persisted jobs,
// we need to start watching the clock.
if (!mJobs.jobTimesInflatedValid()) {
@@ -1094,6 +1176,9 @@ public final class JobSchedulerService extends com.android.server.SystemService
public void onBootPhase(int phase) {
if (PHASE_SYSTEM_SERVICES_READY == phase) {
mConstants.start(getContext().getContentResolver());
+
+ mForceAppStandbyTracker.start();
+
// Register br for package removals and user removals.
final IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
@@ -1720,28 +1805,31 @@ 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
- 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) {
+ // an appropriate amount of time since the last invocation. During device-
+ // wide parole, standby bucketing is ignored.
+ if (!mInParole) {
+ 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;
- } else {
- if (DEBUG_STANDBY) {
- Slog.v(TAG, "Bucket deferred job aged into runnability at "
- + mHeartbeat + " : " + job);
}
}
}
@@ -2086,7 +2174,10 @@ public final class JobSchedulerService extends com.android.server.SystemService
@Override
public void onParoleStateChanged(boolean isParoleOn) {
- // Unused
+ if (DEBUG_STANDBY) {
+ Slog.i(TAG, "Global parole state now " + (isParoleOn ? "ON" : "OFF"));
+ }
+ mInParole = isParoleOn;
}
}
@@ -2297,7 +2388,8 @@ public final class JobSchedulerService extends com.android.server.SystemService
final int uid = Binder.getCallingUid();
long ident = Binder.clearCallingIdentity();
try {
- JobSchedulerService.this.cancelJobsForUid(uid, "cancelAll() called by app");
+ JobSchedulerService.this.cancelJobsForUid(uid,
+ "cancelAll() called by app, callingUid=" + uid);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -2309,7 +2401,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
long ident = Binder.clearCallingIdentity();
try {
- JobSchedulerService.this.cancelJob(uid, jobId);
+ JobSchedulerService.this.cancelJob(uid, jobId, uid);
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -2322,9 +2414,46 @@ public final class JobSchedulerService extends com.android.server.SystemService
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpAndUsageStatsPermission(getContext(), TAG, pw)) return;
+ int filterUid = -1;
+ boolean proto = false;
+ if (!ArrayUtils.isEmpty(args)) {
+ int opti = 0;
+ while (opti < args.length) {
+ String arg = args[opti];
+ if ("-h".equals(arg)) {
+ dumpHelp(pw);
+ return;
+ } else if ("-a".equals(arg)) {
+ // Ignore, we always dump all.
+ } else if ("--proto".equals(arg)) {
+ proto = true;
+ } else if (arg.length() > 0 && arg.charAt(0) == '-') {
+ pw.println("Unknown option: " + arg);
+ return;
+ } else {
+ break;
+ }
+ opti++;
+ }
+ if (opti < args.length) {
+ String pkg = args[opti];
+ try {
+ filterUid = getContext().getPackageManager().getPackageUid(pkg,
+ PackageManager.MATCH_ANY_USER);
+ } catch (NameNotFoundException ignored) {
+ pw.println("Invalid package: " + pkg);
+ return;
+ }
+ }
+ }
+
long identityToken = Binder.clearCallingIdentity();
try {
- JobSchedulerService.this.dumpInternal(pw, args);
+ if (proto) {
+ JobSchedulerService.this.dumpInternalProto(fd, filterUid);
+ } else {
+ JobSchedulerService.this.dumpInternal(pw, filterUid);
+ }
} finally {
Binder.restoreCallingIdentity(identityToken);
}
@@ -2385,7 +2514,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
for (int i=0; i<mActiveServices.size(); i++) {
final JobServiceContext jc = mActiveServices.get(i);
final JobStatus js = jc.getRunningJobLocked();
- if (jc.timeoutIfExecutingLocked(pkgName, userId, hasJobId, jobId)) {
+ if (jc.timeoutIfExecutingLocked(pkgName, userId, hasJobId, jobId, "shell")) {
foundSome = true;
pw.print("Timing out: ");
js.printUniqueId(pw);
@@ -2425,7 +2554,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
}
} else {
pw.println("Canceling job " + pkgName + "/#" + jobId + " in user " + userId);
- if (!cancelJob(pkgUid, jobId)) {
+ if (!cancelJob(pkgUid, jobId, Process.SHELL_UID)) {
pw.println("No matching job found.");
}
}
@@ -2589,37 +2718,24 @@ public final class JobSchedulerService extends com.android.server.SystemService
pw.println(" [package] is an optional package name to limit the output to.");
}
- void dumpInternal(final PrintWriter pw, String[] args) {
- int filterUid = -1;
- if (!ArrayUtils.isEmpty(args)) {
- int opti = 0;
- while (opti < args.length) {
- String arg = args[opti];
- if ("-h".equals(arg)) {
- dumpHelp(pw);
- return;
- } else if ("-a".equals(arg)) {
- // Ignore, we always dump all.
- } else if (arg.length() > 0 && arg.charAt(0) == '-') {
- pw.println("Unknown option: " + arg);
- return;
- } else {
- break;
- }
- opti++;
- }
- if (opti < args.length) {
- String pkg = args[opti];
- try {
- filterUid = getContext().getPackageManager().getPackageUid(pkg,
- PackageManager.MATCH_ANY_USER);
- } catch (NameNotFoundException ignored) {
- pw.println("Invalid package: " + pkg);
- return;
+ /** Sort jobs by caller UID, then by Job ID. */
+ private static void sortJobs(List<JobStatus> jobs) {
+ Collections.sort(jobs, new Comparator<JobStatus>() {
+ @Override
+ public int compare(JobStatus o1, JobStatus o2) {
+ int uid1 = o1.getUid();
+ int uid2 = o2.getUid();
+ int id1 = o1.getJobId();
+ int id2 = o2.getJobId();
+ if (uid1 != uid2) {
+ return uid1 < uid2 ? -1 : 1;
}
+ return id1 < id2 ? -1 : (id1 > id2 ? 1 : 0);
}
- }
+ });
+ }
+ void dumpInternal(final PrintWriter pw, int filterUid) {
final int filterUidFinal = UserHandle.getAppId(filterUid);
final long nowElapsed = sElapsedRealtimeClock.millis();
final long nowUptime = sUptimeMillisClock.millis();
@@ -2632,19 +2748,7 @@ public final class JobSchedulerService extends com.android.server.SystemService
pw.println(" jobs:");
if (mJobs.size() > 0) {
final List<JobStatus> jobs = mJobs.mJobSet.getAllJobs();
- Collections.sort(jobs, new Comparator<JobStatus>() {
- @Override
- public int compare(JobStatus o1, JobStatus o2) {
- int uid1 = o1.getUid();
- int uid2 = o2.getUid();
- int id1 = o1.getJobId();
- int id2 = o2.getJobId();
- if (uid1 != uid2) {
- return uid1 < uid2 ? -1 : 1;
- }
- return id1 < id2 ? -1 : (id1 > id2 ? 1 : 0);
- }
- });
+ sortJobs(jobs);
for (JobStatus job : jobs) {
pw.print(" JOB #"); job.printUniqueId(pw); pw.print(": ");
pw.println(job.toShortStringExceptUniqueId());
@@ -2781,4 +2885,144 @@ public final class JobSchedulerService extends com.android.server.SystemService
}
pw.println();
}
+
+ void dumpInternalProto(final FileDescriptor fd, int filterUid) {
+ ProtoOutputStream proto = new ProtoOutputStream(fd);
+ final int filterUidFinal = UserHandle.getAppId(filterUid);
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ final long nowUptime = sUptimeMillisClock.millis();
+
+ synchronized (mLock) {
+ mConstants.dump(proto, JobSchedulerServiceDumpProto.SETTINGS);
+ for (int u : mStartedUsers) {
+ proto.write(JobSchedulerServiceDumpProto.STARTED_USERS, u);
+ }
+ if (mJobs.size() > 0) {
+ final List<JobStatus> jobs = mJobs.mJobSet.getAllJobs();
+ sortJobs(jobs);
+ for (JobStatus job : jobs) {
+ final long rjToken = proto.start(JobSchedulerServiceDumpProto.REGISTERED_JOBS);
+ job.writeToShortProto(proto, JobSchedulerServiceDumpProto.RegisteredJob.INFO);
+
+ // Skip printing details if the caller requested a filter
+ if (!job.shouldDump(filterUidFinal)) {
+ continue;
+ }
+
+ job.dump(proto, JobSchedulerServiceDumpProto.RegisteredJob.DUMP, true, nowElapsed);
+
+ // isReadyToBeExecuted
+ proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_READY,
+ job.isReady());
+ proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_USER_STARTED,
+ ArrayUtils.contains(mStartedUsers, job.getUserId()));
+ proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_PENDING,
+ mPendingJobs.contains(job));
+ proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_JOB_CURRENTLY_ACTIVE,
+ isCurrentlyActiveLocked(job));
+ proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_UID_BACKING_UP,
+ mBackingUpUids.indexOfKey(job.getSourceUid()) >= 0);
+ boolean componentPresent = false;
+ try {
+ componentPresent = (AppGlobals.getPackageManager().getServiceInfo(
+ job.getServiceComponent(),
+ PackageManager.MATCH_DEBUG_TRIAGED_MISSING,
+ job.getUserId()) != null);
+ } catch (RemoteException e) {
+ }
+ proto.write(JobSchedulerServiceDumpProto.RegisteredJob.IS_COMPONENT_PRESENT,
+ componentPresent);
+
+ proto.end(rjToken);
+ }
+ }
+ for (StateController controller : mControllers) {
+ controller.dumpControllerStateLocked(
+ proto, JobSchedulerServiceDumpProto.CONTROLLERS, filterUidFinal);
+ }
+ for (int i=0; i< mUidPriorityOverride.size(); i++) {
+ int uid = mUidPriorityOverride.keyAt(i);
+ if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) {
+ long pToken = proto.start(JobSchedulerServiceDumpProto.PRIORITY_OVERRIDES);
+ proto.write(JobSchedulerServiceDumpProto.PriorityOverride.UID, uid);
+ proto.write(JobSchedulerServiceDumpProto.PriorityOverride.OVERRIDE_VALUE,
+ mUidPriorityOverride.valueAt(i));
+ proto.end(pToken);
+ }
+ }
+ for (int i = 0; i < mBackingUpUids.size(); i++) {
+ int uid = mBackingUpUids.keyAt(i);
+ if (filterUidFinal == -1 || filterUidFinal == UserHandle.getAppId(uid)) {
+ proto.write(JobSchedulerServiceDumpProto.BACKING_UP_UIDS, uid);
+ }
+ }
+
+ mJobPackageTracker.dump(proto, JobSchedulerServiceDumpProto.PACKAGE_TRACKER,
+ filterUidFinal);
+ mJobPackageTracker.dumpHistory(proto, JobSchedulerServiceDumpProto.HISTORY,
+ filterUidFinal);
+
+ for (JobStatus job : mPendingJobs) {
+ final long pjToken = proto.start(JobSchedulerServiceDumpProto.PENDING_JOBS);
+
+ job.writeToShortProto(proto, PendingJob.INFO);
+ job.dump(proto, PendingJob.DUMP, false, nowElapsed);
+ int priority = evaluateJobPriorityLocked(job);
+ if (priority != JobInfo.PRIORITY_DEFAULT) {
+ proto.write(PendingJob.EVALUATED_PRIORITY, priority);
+ }
+ proto.write(PendingJob.ENQUEUED_DURATION_MS, nowUptime - job.madePending);
+
+ proto.end(pjToken);
+ }
+ for (JobServiceContext jsc : mActiveServices) {
+ final long ajToken = proto.start(JobSchedulerServiceDumpProto.ACTIVE_JOBS);
+ final JobStatus job = jsc.getRunningJobLocked();
+
+ if (job == null) {
+ final long ijToken = proto.start(ActiveJob.INACTIVE);
+
+ proto.write(ActiveJob.InactiveJob.TIME_SINCE_STOPPED_MS,
+ nowElapsed - jsc.mStoppedTime);
+ if (jsc.mStoppedReason != null) {
+ proto.write(ActiveJob.InactiveJob.STOPPED_REASON,
+ jsc.mStoppedReason);
+ }
+
+ proto.end(ijToken);
+ } else {
+ final long rjToken = proto.start(ActiveJob.RUNNING);
+
+ job.writeToShortProto(proto, ActiveJob.RunningJob.INFO);
+
+ proto.write(ActiveJob.RunningJob.RUNNING_DURATION_MS,
+ nowElapsed - jsc.getExecutionStartTimeElapsed());
+ proto.write(ActiveJob.RunningJob.TIME_UNTIL_TIMEOUT_MS,
+ jsc.getTimeoutElapsed() - nowElapsed);
+
+ job.dump(proto, ActiveJob.RunningJob.DUMP, false, nowElapsed);
+
+ int priority = evaluateJobPriorityLocked(jsc.getRunningJobLocked());
+ if (priority != JobInfo.PRIORITY_DEFAULT) {
+ proto.write(ActiveJob.RunningJob.EVALUATED_PRIORITY, priority);
+ }
+
+ proto.write(ActiveJob.RunningJob.TIME_SINCE_MADE_ACTIVE_MS,
+ nowUptime - job.madeActive);
+ proto.write(ActiveJob.RunningJob.PENDING_DURATION_MS,
+ job.madeActive - job.madePending);
+
+ proto.end(rjToken);
+ }
+ proto.end(ajToken);
+ }
+ if (filterUid == -1) {
+ proto.write(JobSchedulerServiceDumpProto.IS_READY_TO_ROCK, mReadyToRock);
+ proto.write(JobSchedulerServiceDumpProto.REPORTED_ACTIVE, mReportedActive);
+ proto.write(JobSchedulerServiceDumpProto.MAX_ACTIVE_JOBS, mMaxActiveJobs);
+ }
+ }
+
+ proto.flush();
+ }
}
diff --git a/com/android/server/job/JobServiceContext.java b/com/android/server/job/JobServiceContext.java
index 6a3fd04a..83a3c199 100644
--- a/com/android/server/job/JobServiceContext.java
+++ b/com/android/server/job/JobServiceContext.java
@@ -38,12 +38,14 @@ import android.os.PowerManager;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.WorkSource;
+import android.util.EventLog;
import android.util.Slog;
import android.util.TimeUtils;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
+import com.android.server.EventLogTags;
import com.android.server.job.controllers.JobStatus;
/**
@@ -222,17 +224,20 @@ public final class JobServiceContext implements ServiceConnection {
isDeadlineExpired, triggeredUris, triggeredAuthorities, job.network);
mExecutionStartTimeElapsed = sElapsedRealtimeClock.millis();
- if (DEBUG_STANDBY) {
- final long whenDeferred = job.getWhenStandbyDeferred();
- if (whenDeferred > 0) {
+ final long whenDeferred = job.getWhenStandbyDeferred();
+ if (whenDeferred > 0) {
+ final long deferral = mExecutionStartTimeElapsed - whenDeferred;
+ EventLog.writeEvent(EventLogTags.JOB_DEFERRED_EXECUTION, deferral);
+ if (DEBUG_STANDBY) {
StringBuilder sb = new StringBuilder(128);
sb.append("Starting job deferred for standby by ");
- TimeUtils.formatDuration(mExecutionStartTimeElapsed - whenDeferred, sb);
- sb.append(" : ");
+ TimeUtils.formatDuration(deferral, sb);
+ sb.append(" ms : ");
sb.append(job.toShortString());
Slog.v(TAG, sb.toString());
}
}
+
// Once we'e begun executing a job, we by definition no longer care whether
// it was inflated from disk with not-yet-coherent delay/deadline bounds.
job.clearPersistedUtcTimes();
@@ -307,13 +312,14 @@ public final class JobServiceContext implements ServiceConnection {
return mTimeoutElapsed;
}
- boolean timeoutIfExecutingLocked(String pkgName, int userId, boolean matchJobId, int jobId) {
+ boolean timeoutIfExecutingLocked(String pkgName, int userId, boolean matchJobId, int jobId,
+ String reason) {
final JobStatus executing = getRunningJobLocked();
if (executing != null && (userId == UserHandle.USER_ALL || userId == executing.getUserId())
&& (pkgName == null || pkgName.equals(executing.getSourcePackageName()))
&& (!matchJobId || jobId == executing.getJobId())) {
if (mVerb == VERB_EXECUTING) {
- mParams.setStopReason(JobParameters.REASON_TIMEOUT);
+ mParams.setStopReason(JobParameters.REASON_TIMEOUT, reason);
sendStopMessageLocked("force timeout from shell");
return true;
}
@@ -532,7 +538,7 @@ public final class JobServiceContext implements ServiceConnection {
}
return;
}
- mParams.setStopReason(arg1);
+ mParams.setStopReason(arg1, debugReason);
if (arg1 == JobParameters.REASON_PREEMPT) {
mPreferredUid = mRunningJob != null ? mRunningJob.getUid() :
NO_PREFERRED_UID;
@@ -682,7 +688,7 @@ public final class JobServiceContext implements ServiceConnection {
// Not an error - client ran out of time.
Slog.i(TAG, "Client timed out while executing (no jobFinished received), " +
"sending onStop: " + getRunningJobNameLocked());
- mParams.setStopReason(JobParameters.REASON_TIMEOUT);
+ mParams.setStopReason(JobParameters.REASON_TIMEOUT, "client timed out");
sendStopMessageLocked("timeout while executing");
break;
default:
diff --git a/com/android/server/job/JobStore.java b/com/android/server/job/JobStore.java
index 36cacd7a..a24a4ac3 100644
--- a/com/android/server/job/JobStore.java
+++ b/com/android/server/job/JobStore.java
@@ -72,6 +72,9 @@ import java.util.Set;
* This is important b/c {@link com.android.server.job.JobStore.WriteJobsMapToDiskRunnable}
* and {@link com.android.server.job.JobStore.ReadJobMapFromDiskRunnable} lock on that
* object.
+ *
+ * Test:
+ * atest $ANDROID_BUILD_TOP/frameworks/base/services/tests/servicestests/src/com/android/server/job/JobStoreTest.java
*/
public final class JobStore {
private static final String TAG = "JobStore";
@@ -427,6 +430,9 @@ public final class JobStore {
out.attribute(null, "uid", Integer.toString(jobStatus.getUid()));
out.attribute(null, "priority", String.valueOf(jobStatus.getPriority()));
out.attribute(null, "flags", String.valueOf(jobStatus.getFlags()));
+ if (jobStatus.getInternalFlags() != 0) {
+ out.attribute(null, "internalFlags", String.valueOf(jobStatus.getInternalFlags()));
+ }
out.attribute(null, "lastSuccessfulRunTime",
String.valueOf(jobStatus.getLastSuccessfulRunTime()));
@@ -689,6 +695,7 @@ public final class JobStore {
int uid, sourceUserId;
long lastSuccessfulRunTime;
long lastFailedRunTime;
+ int internalFlags = 0;
// Read out job identifier attributes and priority.
try {
@@ -704,6 +711,10 @@ public final class JobStore {
if (val != null) {
jobBuilder.setFlags(Integer.parseInt(val));
}
+ val = parser.getAttributeValue(null, "internalFlags");
+ if (val != null) {
+ internalFlags = Integer.parseInt(val);
+ }
val = parser.getAttributeValue(null, "sourceUserId");
sourceUserId = val == null ? -1 : Integer.parseInt(val);
@@ -718,7 +729,6 @@ public final class JobStore {
}
String sourcePackageName = parser.getAttributeValue(null, "sourcePackageName");
-
final String sourceTag = parser.getAttributeValue(null, "sourceTag");
int eventType;
@@ -857,7 +867,7 @@ public final class JobStore {
appBucket, currentHeartbeat, sourceTag,
elapsedRuntimes.first, elapsedRuntimes.second,
lastSuccessfulRunTime, lastFailedRunTime,
- (rtcIsGood) ? null : rtcRuntimes);
+ (rtcIsGood) ? null : rtcRuntimes, internalFlags);
return js;
}
@@ -1042,13 +1052,18 @@ public final class JobStore {
final ArraySet<JobStatus> jobs = mJobs.get(uid);
final int sourceUid = job.getSourceUid();
final ArraySet<JobStatus> jobsForSourceUid = mJobsPerSourceUid.get(sourceUid);
- boolean didRemove = jobs != null && jobs.remove(job) && jobsForSourceUid.remove(job);
- if (didRemove) {
- if (jobs.size() == 0) {
- // no more jobs for this uid; let the now-empty set object be GC'd.
+ final boolean didRemove = jobs != null && jobs.remove(job);
+ final boolean sourceRemove = jobsForSourceUid != null && jobsForSourceUid.remove(job);
+ if (didRemove != sourceRemove) {
+ Slog.wtf(TAG, "Job presence mismatch; caller=" + didRemove
+ + " source=" + sourceRemove);
+ }
+ if (didRemove || sourceRemove) {
+ // no more jobs for this uid? let the now-empty set objects be GC'd.
+ if (jobs != null && jobs.size() == 0) {
mJobs.remove(uid);
}
- if (jobsForSourceUid.size() == 0) {
+ if (jobsForSourceUid != null && jobsForSourceUid.size() == 0) {
mJobsPerSourceUid.remove(sourceUid);
}
return true;
diff --git a/com/android/server/job/controllers/AppIdleController.java b/com/android/server/job/controllers/AppIdleController.java
index a7ed2f56..8d11d1ee 100644
--- a/com/android/server/job/controllers/AppIdleController.java
+++ b/com/android/server/job/controllers/AppIdleController.java
@@ -20,10 +20,12 @@ import android.app.usage.UsageStatsManagerInternal;
import android.content.Context;
import android.os.UserHandle;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobStore;
+import com.android.server.job.StateControllerProto;
import java.io.PrintWriter;
@@ -152,6 +154,38 @@ public final class AppIdleController extends StateController {
});
}
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, int filterUid) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.APP_IDLE);
+
+ proto.write(StateControllerProto.AppIdleController.IS_PAROLE_ON, mAppIdleParoleOn);
+
+ mJobSchedulerService.getJobStore().forEachJob(new JobStore.JobStatusFunctor() {
+ @Override public void process(JobStatus js) {
+ // Skip printing details if the caller requested a filter
+ if (!js.shouldDump(filterUid)) {
+ return;
+ }
+
+ final long jsToken =
+ proto.start(StateControllerProto.AppIdleController.TRACKED_JOBS);
+ js.writeToShortProto(proto, StateControllerProto.AppIdleController.TrackedJob.INFO);
+ proto.write(StateControllerProto.AppIdleController.TrackedJob.SOURCE_UID,
+ js.getSourceUid());
+ proto.write(StateControllerProto.AppIdleController.TrackedJob.SOURCE_PACKAGE_NAME,
+ js.getSourcePackageName());
+ proto.write(
+ StateControllerProto.AppIdleController.TrackedJob.ARE_CONSTRAINTS_SATISFIED,
+ (js.satisfiedConstraints & JobStatus.CONSTRAINT_APP_NOT_IDLE) != 0);
+ proto.end(jsToken);
+ }
+ });
+
+ proto.end(mToken);
+ proto.end(token);
+ }
+
void setAppIdleParoleOn(boolean isAppIdleParoleOn) {
// Flag if any app's idle state has changed
boolean changed = false;
diff --git a/com/android/server/job/controllers/BackgroundJobsController.java b/com/android/server/job/controllers/BackgroundJobsController.java
index fc4015d0..2e4567ac 100644
--- a/com/android/server/job/controllers/BackgroundJobsController.java
+++ b/com/android/server/job/controllers/BackgroundJobsController.java
@@ -17,17 +17,17 @@
package com.android.server.job.controllers;
import android.content.Context;
-import android.os.IDeviceIdleController;
-import android.os.ServiceManager;
import android.os.SystemClock;
import android.os.UserHandle;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
-import com.android.internal.util.ArrayUtils;
import com.android.server.ForceAppStandbyTracker;
import com.android.server.ForceAppStandbyTracker.Listener;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobStore;
+import com.android.server.job.StateControllerProto;
+import com.android.server.job.StateControllerProto.BackgroundJobsController.TrackedJob;
import java.io.PrintWriter;
@@ -41,7 +41,6 @@ public final class BackgroundJobsController extends StateController {
private static volatile BackgroundJobsController sController;
private final JobSchedulerService mJobSchedulerService;
- private final IDeviceIdleController mDeviceIdleController;
private final ForceAppStandbyTracker mForceAppStandbyTracker;
@@ -59,8 +58,6 @@ public final class BackgroundJobsController extends StateController {
private BackgroundJobsController(JobSchedulerService service, Context context, Object lock) {
super(service, context, lock);
mJobSchedulerService = service;
- mDeviceIdleController = IDeviceIdleController.Stub.asInterface(
- ServiceManager.getService(Context.DEVICE_IDLE_CONTROLLER));
mForceAppStandbyTracker = ForceAppStandbyTracker.getInstance(context);
@@ -90,6 +87,7 @@ public final class BackgroundJobsController extends StateController {
return;
}
final int uid = jobStatus.getSourceUid();
+ final String sourcePkg = jobStatus.getSourcePackageName();
pw.print(" #");
jobStatus.printUniqueId(pw);
pw.print(" from ");
@@ -100,11 +98,10 @@ public final class BackgroundJobsController extends StateController {
pw.print(", whitelisted");
}
pw.print(": ");
- pw.print(jobStatus.getSourcePackageName());
+ pw.print(sourcePkg);
pw.print(" [RUN_ANY_IN_BACKGROUND ");
- pw.print(mForceAppStandbyTracker.isRunAnyInBackgroundAppOpsAllowed(
- jobStatus.getSourceUid(), jobStatus.getSourcePackageName())
+ pw.print(mForceAppStandbyTracker.isRunAnyInBackgroundAppOpsAllowed(uid, sourcePkg)
? "allowed]" : "disallowed]");
if ((jobStatus.satisfiedConstraints
@@ -116,6 +113,51 @@ public final class BackgroundJobsController extends StateController {
});
}
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, int filterUid) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.BACKGROUND);
+
+ mForceAppStandbyTracker.dumpProto(proto,
+ StateControllerProto.BackgroundJobsController.FORCE_APP_STANDBY_TRACKER);
+
+ mJobSchedulerService.getJobStore().forEachJob((jobStatus) -> {
+ if (!jobStatus.shouldDump(filterUid)) {
+ return;
+ }
+ final long jsToken =
+ proto.start(StateControllerProto.BackgroundJobsController.TRACKED_JOBS);
+
+ jobStatus.writeToShortProto(proto,
+ TrackedJob.INFO);
+ final int sourceUid = jobStatus.getSourceUid();
+ proto.write(TrackedJob.SOURCE_UID, sourceUid);
+ final String sourcePkg = jobStatus.getSourcePackageName();
+ proto.write(TrackedJob.SOURCE_PACKAGE_NAME, sourcePkg);
+
+ proto.write(TrackedJob.IS_IN_FOREGROUND,
+ mForceAppStandbyTracker.isInForeground(sourceUid));
+ proto.write(TrackedJob.IS_WHITELISTED,
+ mForceAppStandbyTracker.isUidPowerSaveWhitelisted(sourceUid) ||
+ mForceAppStandbyTracker.isUidTempPowerSaveWhitelisted(sourceUid));
+
+ proto.write(
+ TrackedJob.CAN_RUN_ANY_IN_BACKGROUND,
+ mForceAppStandbyTracker.isRunAnyInBackgroundAppOpsAllowed(
+ sourceUid, sourcePkg));
+
+ proto.write(
+ TrackedJob.ARE_CONSTRAINTS_SATISFIED,
+ (jobStatus.satisfiedConstraints &
+ JobStatus.CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0);
+
+ proto.end(jsToken);
+ });
+
+ proto.end(mToken);
+ proto.end(token);
+ }
+
private void updateAllJobRestrictionsLocked() {
updateJobRestrictionsLocked(/*filterUid=*/ -1);
}
@@ -155,7 +197,9 @@ public final class BackgroundJobsController extends StateController {
final int uid = jobStatus.getSourceUid();
final String packageName = jobStatus.getSourcePackageName();
- final boolean canRun = !mForceAppStandbyTracker.areJobsRestricted(uid, packageName);
+ final boolean canRun = !mForceAppStandbyTracker.areJobsRestricted(uid, packageName,
+ (jobStatus.getInternalFlags() & JobStatus.INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION)
+ != 0);
return jobStatus.setBackgroundNotRestrictedConstraintSatisfied(canRun);
}
@@ -187,17 +231,23 @@ public final class BackgroundJobsController extends StateController {
private final Listener mForceAppStandbyListener = new Listener() {
@Override
public void updateAllJobs() {
- updateAllJobRestrictionsLocked();
+ synchronized (mLock) {
+ updateAllJobRestrictionsLocked();
+ }
}
@Override
public void updateJobsForUid(int uid) {
- updateJobRestrictionsForUidLocked(uid);
+ synchronized (mLock) {
+ updateJobRestrictionsForUidLocked(uid);
+ }
}
@Override
public void updateJobsForUidPackage(int uid, String packageName) {
- updateJobRestrictionsForUidLocked(uid);
+ synchronized (mLock) {
+ updateJobRestrictionsForUidLocked(uid);
+ }
}
};
}
diff --git a/com/android/server/job/controllers/BatteryController.java b/com/android/server/job/controllers/BatteryController.java
index 76ff8348..8d3d116e 100644
--- a/com/android/server/job/controllers/BatteryController.java
+++ b/com/android/server/job/controllers/BatteryController.java
@@ -27,11 +27,13 @@ import android.os.BatteryManagerInternal;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateChangedListener;
+import com.android.server.job.StateControllerProto;
import java.io.PrintWriter;
@@ -263,4 +265,35 @@ public final class BatteryController extends StateController {
pw.println();
}
}
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, int filterUid) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.BATTERY);
+
+ proto.write(StateControllerProto.BatteryController.IS_ON_STABLE_POWER,
+ mChargeTracker.isOnStablePower());
+ proto.write(StateControllerProto.BatteryController.IS_BATTERY_NOT_LOW,
+ mChargeTracker.isBatteryNotLow());
+
+ proto.write(StateControllerProto.BatteryController.IS_MONITORING,
+ mChargeTracker.isMonitoring());
+ proto.write(StateControllerProto.BatteryController.LAST_BROADCAST_SEQUENCE_NUMBER,
+ mChargeTracker.getSeq());
+
+ for (int i = 0; i < mTrackedTasks.size(); i++) {
+ final JobStatus js = mTrackedTasks.valueAt(i);
+ if (!js.shouldDump(filterUid)) {
+ continue;
+ }
+ final long jsToken = proto.start(StateControllerProto.BatteryController.TRACKED_JOBS);
+ js.writeToShortProto(proto, StateControllerProto.BatteryController.TrackedJob.INFO);
+ proto.write(StateControllerProto.BatteryController.TrackedJob.SOURCE_UID,
+ js.getSourceUid());
+ proto.end(jsToken);
+ }
+
+ proto.end(mToken);
+ proto.end(token);
+ }
}
diff --git a/com/android/server/job/controllers/ConnectivityController.java b/com/android/server/job/controllers/ConnectivityController.java
index da287691..373d87d9 100644
--- a/com/android/server/job/controllers/ConnectivityController.java
+++ b/com/android/server/job/controllers/ConnectivityController.java
@@ -16,6 +16,10 @@
package com.android.server.job.controllers;
+import static android.net.NetworkCapabilities.LINK_BANDWIDTH_UNSPECIFIED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+
import android.app.job.JobInfo;
import android.content.Context;
import android.net.ConnectivityManager;
@@ -25,17 +29,21 @@ import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkPolicyManager;
+import android.net.NetworkRequest;
import android.net.TrafficStats;
import android.os.Process;
import android.os.UserHandle;
import android.text.format.DateUtils;
import android.util.ArraySet;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobServiceContext;
import com.android.server.job.StateChangedListener;
+import com.android.server.job.StateControllerProto;
import java.io.PrintWriter;
@@ -59,15 +67,15 @@ public final class ConnectivityController extends StateController implements
private final ArraySet<JobStatus> mTrackedJobs = new ArraySet<>();
/** Singleton. */
- private static ConnectivityController mSingleton;
+ private static ConnectivityController sSingleton;
private static Object sCreationLock = new Object();
public static ConnectivityController get(JobSchedulerService jms) {
synchronized (sCreationLock) {
- if (mSingleton == null) {
- mSingleton = new ConnectivityController(jms, jms.getContext(), jms.getLock());
+ if (sSingleton == null) {
+ sSingleton = new ConnectivityController(jms, jms.getContext(), jms.getLock());
}
- return mSingleton;
+ return sSingleton;
}
}
@@ -102,37 +110,29 @@ public final class ConnectivityController extends StateController implements
}
/**
- * Test to see if running the given job on the given network is sane.
+ * Test to see if running the given job on the given network is insane.
* <p>
* For example, if a job is trying to send 10MB over a 128Kbps EDGE
* connection, it would take 10.4 minutes, and has no chance of succeeding
* before the job times out, so we'd be insane to try running it.
*/
- private boolean isSane(JobStatus jobStatus, NetworkCapabilities capabilities) {
+ @SuppressWarnings("unused")
+ private static boolean isInsane(JobStatus jobStatus, Network network,
+ NetworkCapabilities capabilities) {
final long estimatedBytes = jobStatus.getEstimatedNetworkBytes();
if (estimatedBytes == JobInfo.NETWORK_BYTES_UNKNOWN) {
// We don't know how large the job is; cross our fingers!
- return true;
- }
- if (capabilities == null) {
- // We don't know what the network is like; cross our fingers!
- return true;
+ return false;
}
// We don't ask developers to differentiate between upstream/downstream
// in their size estimates, so test against the slowest link direction.
- final long downstream = capabilities.getLinkDownstreamBandwidthKbps();
- final long upstream = capabilities.getLinkUpstreamBandwidthKbps();
- final long slowest;
- if (downstream > 0 && upstream > 0) {
- slowest = Math.min(downstream, upstream);
- } else if (downstream > 0) {
- slowest = downstream;
- } else if (upstream > 0) {
- slowest = upstream;
- } else {
+ final long slowest = NetworkCapabilities.minBandwidth(
+ capabilities.getLinkDownstreamBandwidthKbps(),
+ capabilities.getLinkUpstreamBandwidthKbps());
+ if (slowest == LINK_BANDWIDTH_UNSPECIFIED) {
// We don't know what the network is like; cross our fingers!
- return true;
+ return false;
}
final long estimatedMillis = ((estimatedBytes * DateUtils.SECOND_IN_MILLIS)
@@ -141,28 +141,87 @@ public final class ConnectivityController extends StateController implements
// If we'd never finish before the timeout, we'd be insane!
Slog.w(TAG, "Estimated " + estimatedBytes + " bytes over " + slowest
+ " kbps network would take " + estimatedMillis + "ms; that's insane!");
+ return true;
+ } else {
return false;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static boolean isCongestionDelayed(JobStatus jobStatus, Network network,
+ NetworkCapabilities capabilities) {
+ // If network is congested, and job is less than 50% through the
+ // developer-requested window, then we're okay delaying the job.
+ if (!capabilities.hasCapability(NET_CAPABILITY_NOT_CONGESTED)) {
+ return jobStatus.getFractionRunTime() < 0.5;
} else {
- return true;
+ return false;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ private static boolean isStrictSatisfied(JobStatus jobStatus, Network network,
+ NetworkCapabilities capabilities) {
+ return jobStatus.getJob().getRequiredNetwork().networkCapabilities
+ .satisfiedByNetworkCapabilities(capabilities);
+ }
+
+ @SuppressWarnings("unused")
+ private static boolean isRelaxedSatisfied(JobStatus jobStatus, Network network,
+ NetworkCapabilities capabilities) {
+ // Only consider doing this for prefetching jobs
+ if ((jobStatus.getJob().getFlags() & JobInfo.FLAG_IS_PREFETCH) == 0) {
+ return false;
+ }
+
+ // See if we match after relaxing any unmetered request
+ final NetworkCapabilities relaxed = new NetworkCapabilities(
+ jobStatus.getJob().getRequiredNetwork().networkCapabilities)
+ .removeCapability(NET_CAPABILITY_NOT_METERED);
+ if (relaxed.satisfiedByNetworkCapabilities(capabilities)) {
+ // TODO: treat this as "maybe" response; need to check quotas
+ return jobStatus.getFractionRunTime() > 0.5;
+ } else {
+ return false;
}
}
+ @VisibleForTesting
+ static boolean isSatisfied(JobStatus jobStatus, Network network,
+ NetworkCapabilities capabilities) {
+ // Zeroth, we gotta have a network to think about being satisfied
+ if (network == null || capabilities == null) return false;
+
+ // First, are we insane?
+ if (isInsane(jobStatus, network, capabilities)) return false;
+
+ // Second, is the network congested?
+ if (isCongestionDelayed(jobStatus, network, capabilities)) return false;
+
+ // Third, is the network a strict match?
+ if (isStrictSatisfied(jobStatus, network, capabilities)) return true;
+
+ // Third, is the network a relaxed match?
+ if (isRelaxedSatisfied(jobStatus, network, capabilities)) return true;
+
+ return false;
+ }
+
private boolean updateConstraintsSatisfied(JobStatus jobStatus) {
// TODO: consider matching against non-active networks
final int jobUid = jobStatus.getSourceUid();
final boolean ignoreBlocked = (jobStatus.getFlags() & JobInfo.FLAG_WILL_BE_FOREGROUND) != 0;
+
final Network network = mConnManager.getActiveNetworkForUid(jobUid, ignoreBlocked);
final NetworkInfo info = mConnManager.getNetworkInfoForUid(network, jobUid, ignoreBlocked);
final NetworkCapabilities capabilities = mConnManager.getNetworkCapabilities(network);
final boolean connected = (info != null) && info.isConnected();
- final boolean satisfied = jobStatus.getJob().getRequiredNetwork().networkCapabilities
- .satisfiedByNetworkCapabilities(capabilities);
- final boolean sane = isSane(jobStatus, capabilities);
+ final boolean satisfied = isSatisfied(jobStatus, network, capabilities);
final boolean changed = jobStatus
- .setConnectivityConstraintSatisfied(connected && satisfied && sane);
+ .setConnectivityConstraintSatisfied(connected && satisfied);
// Pass along the evaluated network for job to use; prevents race
// conditions as default routes change over time, and opens the door to
@@ -178,8 +237,7 @@ public final class ConnectivityController extends StateController implements
if (DEBUG) {
Slog.i(TAG, "Connectivity " + (changed ? "CHANGED" : "unchanged")
+ " for " + jobStatus + ": connected=" + connected
- + " satisfied=" + satisfied
- + " sane=" + sane);
+ + " satisfied=" + satisfied);
}
return changed;
}
@@ -241,7 +299,7 @@ public final class ConnectivityController extends StateController implements
}
};
- private final INetworkPolicyListener mNetPolicyListener = new INetworkPolicyListener.Stub() {
+ private final INetworkPolicyListener mNetPolicyListener = new NetworkPolicyManager.Listener() {
@Override
public void onUidRulesChanged(int uid, int uidRules) {
if (DEBUG) {
@@ -251,11 +309,6 @@ public final class ConnectivityController extends StateController implements
}
@Override
- public void onMeteredIfacesChanged(String[] meteredIfaces) {
- // We track this via our NetworkCallback
- }
-
- @Override
public void onRestrictBackgroundChanged(boolean restrictBackground) {
if (DEBUG) {
Slog.v(TAG, "Background restriction change to " + restrictBackground);
@@ -290,4 +343,32 @@ public final class ConnectivityController extends StateController implements
}
}
}
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, int filterUid) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.CONNECTIVITY);
+
+ proto.write(StateControllerProto.ConnectivityController.IS_CONNECTED, mConnected);
+
+ for (int i = 0; i < mTrackedJobs.size(); i++) {
+ final JobStatus js = mTrackedJobs.valueAt(i);
+ if (!js.shouldDump(filterUid)) {
+ continue;
+ }
+ final long jsToken = proto.start(StateControllerProto.ConnectivityController.TRACKED_JOBS);
+ js.writeToShortProto(proto, StateControllerProto.ConnectivityController.TrackedJob.INFO);
+ proto.write(StateControllerProto.ConnectivityController.TrackedJob.SOURCE_UID,
+ js.getSourceUid());
+ NetworkRequest rn = js.getJob().getRequiredNetwork();
+ if (rn != null) {
+ rn.writeToProto(proto,
+ StateControllerProto.ConnectivityController.TrackedJob.REQUIRED_NETWORK);
+ }
+ proto.end(jsToken);
+ }
+
+ proto.end(mToken);
+ proto.end(token);
+ }
}
diff --git a/com/android/server/job/controllers/ContentObserverController.java b/com/android/server/job/controllers/ContentObserverController.java
index ff807ecc..7394e23f 100644
--- a/com/android/server/job/controllers/ContentObserverController.java
+++ b/com/android/server/job/controllers/ContentObserverController.java
@@ -28,10 +28,13 @@ import android.util.SparseArray;
import android.util.TimeUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateChangedListener;
+import com.android.server.job.StateControllerProto;
+import com.android.server.job.StateControllerProto.ContentObserverController.Observer.TriggerContentData;
import java.io.PrintWriter;
import java.util.ArrayList;
@@ -451,4 +454,103 @@ public final class ContentObserverController extends StateController {
}
}
}
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, int filterUid) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.CONTENT_OBSERVER);
+
+ for (int i = 0; i < mTrackedTasks.size(); i++) {
+ JobStatus js = mTrackedTasks.valueAt(i);
+ if (!js.shouldDump(filterUid)) {
+ continue;
+ }
+ final long jsToken =
+ proto.start(StateControllerProto.ContentObserverController.TRACKED_JOBS);
+ js.writeToShortProto(proto,
+ StateControllerProto.ContentObserverController.TrackedJob.INFO);
+ proto.write(StateControllerProto.ContentObserverController.TrackedJob.SOURCE_UID,
+ js.getSourceUid());
+ proto.end(jsToken);
+ }
+
+ final int n = mObservers.size();
+ for (int userIdx = 0; userIdx < n; userIdx++) {
+ final long oToken =
+ proto.start(StateControllerProto.ContentObserverController.OBSERVERS);
+ final int userId = mObservers.keyAt(userIdx);
+
+ proto.write(StateControllerProto.ContentObserverController.Observer.USER_ID, userId);
+
+ ArrayMap<JobInfo.TriggerContentUri, ObserverInstance> observersOfUser =
+ mObservers.get(userId);
+ int numbOfObserversPerUser = observersOfUser.size();
+ for (int observerIdx = 0 ; observerIdx < numbOfObserversPerUser; observerIdx++) {
+ ObserverInstance obs = observersOfUser.valueAt(observerIdx);
+ int m = obs.mJobs.size();
+ boolean shouldDump = false;
+ for (int j = 0; j < m; j++) {
+ JobInstance inst = obs.mJobs.valueAt(j);
+ if (inst.mJobStatus.shouldDump(filterUid)) {
+ shouldDump = true;
+ break;
+ }
+ }
+ if (!shouldDump) {
+ continue;
+ }
+ final long tToken = proto.start(
+ StateControllerProto.ContentObserverController.Observer.TRIGGERS);
+
+ JobInfo.TriggerContentUri trigger = observersOfUser.keyAt(observerIdx);
+ Uri u = trigger.getUri();
+ if (u != null) {
+ proto.write(TriggerContentData.URI, u.toString());
+ }
+ proto.write(TriggerContentData.FLAGS, trigger.getFlags());
+
+ for (int j = 0; j < m; j++) {
+ final long jToken = proto.start(TriggerContentData.JOBS);
+ JobInstance inst = obs.mJobs.valueAt(j);
+
+ inst.mJobStatus.writeToShortProto(proto, TriggerContentData.JobInstance.INFO);
+ proto.write(TriggerContentData.JobInstance.SOURCE_UID,
+ inst.mJobStatus.getSourceUid());
+
+ if (inst.mChangedAuthorities == null) {
+ proto.end(jToken);
+ continue;
+ }
+ if (inst.mTriggerPending) {
+ proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_UPDATE_DELAY_MS,
+ inst.mJobStatus.getTriggerContentUpdateDelay());
+ proto.write(TriggerContentData.JobInstance.TRIGGER_CONTENT_MAX_DELAY_MS,
+ inst.mJobStatus.getTriggerContentMaxDelay());
+ }
+ for (int k = 0; k < inst.mChangedAuthorities.size(); k++) {
+ proto.write(TriggerContentData.JobInstance.CHANGED_AUTHORITIES,
+ inst.mChangedAuthorities.valueAt(k));
+ }
+ if (inst.mChangedUris != null) {
+ for (int k = 0; k < inst.mChangedUris.size(); k++) {
+ u = inst.mChangedUris.valueAt(k);
+ if (u != null) {
+ proto.write(TriggerContentData.JobInstance.CHANGED_URIS,
+ u.toString());
+ }
+ }
+ }
+
+ proto.end(jToken);
+ }
+
+ proto.end(tToken);
+ }
+
+ proto.end(oToken);
+ }
+
+ proto.end(mToken);
+ proto.end(token);
+ }
}
diff --git a/com/android/server/job/controllers/DeviceIdleJobsController.java b/com/android/server/job/controllers/DeviceIdleJobsController.java
index b7eb9e06..0dbcbeeb 100644
--- a/com/android/server/job/controllers/DeviceIdleJobsController.java
+++ b/com/android/server/job/controllers/DeviceIdleJobsController.java
@@ -29,12 +29,15 @@ import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseBooleanArray;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.util.ArrayUtils;
import com.android.server.DeviceIdleController;
import com.android.server.LocalServices;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.JobStore;
+import com.android.server.job.StateControllerProto;
+import com.android.server.job.StateControllerProto.DeviceIdleJobsController.TrackedJob;
import java.io.PrintWriter;
import java.util.Arrays;
@@ -270,6 +273,38 @@ public final class DeviceIdleJobsController extends StateController {
});
}
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, int filterUid) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.DEVICE_IDLE);
+
+ proto.write(StateControllerProto.DeviceIdleJobsController.IS_DEVICE_IDLE_MODE,
+ mDeviceIdleMode);
+ mJobSchedulerService.getJobStore().forEachJob(new JobStore.JobStatusFunctor() {
+ @Override public void process(JobStatus jobStatus) {
+ if (!jobStatus.shouldDump(filterUid)) {
+ return;
+ }
+ final long jsToken =
+ proto.start(StateControllerProto.DeviceIdleJobsController.TRACKED_JOBS);
+
+ jobStatus.writeToShortProto(proto, TrackedJob.INFO);
+ proto.write(TrackedJob.SOURCE_UID, jobStatus.getSourceUid());
+ proto.write(TrackedJob.SOURCE_PACKAGE_NAME, jobStatus.getSourcePackageName());
+ proto.write(TrackedJob.ARE_CONSTRAINTS_SATISFIED,
+ (jobStatus.satisfiedConstraints &
+ JobStatus.CONSTRAINT_DEVICE_NOT_DOZING) != 0);
+ proto.write(TrackedJob.IS_DOZE_WHITELISTED, jobStatus.dozeWhitelisted);
+ proto.write(TrackedJob.IS_ALLOWED_IN_DOZE, mAllowInIdleJobs.contains(jobStatus));
+
+ proto.end(jsToken);
+ }
+ });
+
+ proto.end(mToken);
+ proto.end(token);
+ }
+
final class DeviceIdleUpdateFunctor implements JobStore.JobStatusFunctor {
boolean mChanged;
@@ -300,4 +335,4 @@ public final class DeviceIdleJobsController extends StateController {
}
}
}
-} \ No newline at end of file
+}
diff --git a/com/android/server/job/controllers/IdleController.java b/com/android/server/job/controllers/IdleController.java
index 7bde1744..a9bc7e0d 100644
--- a/com/android/server/job/controllers/IdleController.java
+++ b/com/android/server/job/controllers/IdleController.java
@@ -27,10 +27,12 @@ import android.content.IntentFilter;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import com.android.server.am.ActivityManagerService;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateChangedListener;
+import com.android.server.job.StateControllerProto;
import java.io.PrintWriter;
@@ -216,4 +218,27 @@ public final class IdleController extends StateController {
pw.println();
}
}
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, int filterUid) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.IDLE);
+
+ proto.write(StateControllerProto.IdleController.IS_IDLE, mIdleTracker.isIdle());
+
+ for (int i = 0; i < mTrackedTasks.size(); i++) {
+ final JobStatus js = mTrackedTasks.valueAt(i);
+ if (!js.shouldDump(filterUid)) {
+ continue;
+ }
+ final long jsToken = proto.start(StateControllerProto.IdleController.TRACKED_JOBS);
+ js.writeToShortProto(proto, StateControllerProto.IdleController.TrackedJob.INFO);
+ proto.write(StateControllerProto.IdleController.TrackedJob.SOURCE_UID,
+ js.getSourceUid());
+ proto.end(jsToken);
+ }
+
+ proto.end(mToken);
+ proto.end(token);
+ }
}
diff --git a/com/android/server/job/controllers/JobStatus.java b/com/android/server/job/controllers/JobStatus.java
index e71b8ec4..d9a5ff67 100644
--- a/com/android/server/job/controllers/JobStatus.java
+++ b/com/android/server/job/controllers/JobStatus.java
@@ -24,6 +24,7 @@ import android.app.job.JobInfo;
import android.app.job.JobWorkItem;
import android.content.ClipData;
import android.content.ComponentName;
+import android.content.pm.PackageManagerInternal;
import android.net.Network;
import android.net.Uri;
import android.os.RemoteException;
@@ -33,15 +34,19 @@ import android.util.ArraySet;
import android.util.Pair;
import android.util.Slog;
import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
import com.android.server.LocalServices;
import com.android.server.job.GrantedUriPermissions;
import com.android.server.job.JobSchedulerInternal;
import com.android.server.job.JobSchedulerService;
+import com.android.server.job.JobStatusDumpProto;
+import com.android.server.job.JobStatusShortInfoProto;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.function.Predicate;
/**
* Uniquely identifies a job internally.
@@ -93,6 +98,7 @@ public final class JobStatus {
final JobInfo job;
/** Uid of the package requesting this job. */
final int callingUid;
+ final int targetSdkVersion;
final String batteryName;
final String sourcePackageName;
@@ -179,6 +185,21 @@ public final class JobStatus {
*/
private int trackingControllers;
+ /**
+ * Flag for {@link #mInternalFlags}: this job was scheduled when the app that owns the job
+ * service (not necessarily the caller) was in the foreground and the job has no time
+ * constraints, which makes it exempted from the battery saver job restriction.
+ *
+ * @hide
+ */
+ public static final int INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION = 1 << 0;
+
+ /**
+ * Versatile, persistable flags for a job that's updated within the system server,
+ * as opposed to {@link JobInfo#flags} that's set by callers.
+ */
+ private int mInternalFlags;
+
// These are filled in by controllers when preparing for execution.
public ArraySet<Uri> changedUris;
public ArraySet<String> changedAuthorities;
@@ -240,12 +261,13 @@ public final class JobStatus {
return callingUid;
}
- private JobStatus(JobInfo job, int callingUid, String sourcePackageName,
+ private JobStatus(JobInfo job, int callingUid, int targetSdkVersion, String sourcePackageName,
int sourceUserId, int standbyBucket, long heartbeat, String tag, int numFailures,
long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
- long lastSuccessfulRunTime, long lastFailedRunTime) {
+ long lastSuccessfulRunTime, long lastFailedRunTime, int internalFlags) {
this.job = job;
this.callingUid = callingUid;
+ this.targetSdkVersion = targetSdkVersion;
this.standbyBucket = standbyBucket;
this.baseHeartbeat = heartbeat;
@@ -298,18 +320,21 @@ public final class JobStatus {
mLastSuccessfulRunTime = lastSuccessfulRunTime;
mLastFailedRunTime = lastFailedRunTime;
+ mInternalFlags = internalFlags;
+
updateEstimatedNetworkBytesLocked();
}
/** Copy constructor: used specifically when cloning JobStatus objects for persistence,
* so we preserve RTC window bounds if the source object has them. */
public JobStatus(JobStatus jobStatus) {
- this(jobStatus.getJob(), jobStatus.getUid(),
+ this(jobStatus.getJob(), jobStatus.getUid(), jobStatus.targetSdkVersion,
jobStatus.getSourcePackageName(), jobStatus.getSourceUserId(),
jobStatus.getStandbyBucket(), jobStatus.getBaseHeartbeat(),
jobStatus.getSourceTag(), jobStatus.getNumFailures(),
jobStatus.getEarliestRunTime(), jobStatus.getLatestRunTimeElapsed(),
- jobStatus.getLastSuccessfulRunTime(), jobStatus.getLastFailedRunTime());
+ jobStatus.getLastSuccessfulRunTime(), jobStatus.getLastFailedRunTime(),
+ jobStatus.getInternalFlags());
mPersistedUtcTimes = jobStatus.mPersistedUtcTimes;
if (jobStatus.mPersistedUtcTimes != null) {
if (DEBUG) {
@@ -330,12 +355,13 @@ public final class JobStatus {
int standbyBucket, long baseHeartbeat, String sourceTag,
long earliestRunTimeElapsedMillis, long latestRunTimeElapsedMillis,
long lastSuccessfulRunTime, long lastFailedRunTime,
- Pair<Long, Long> persistedExecutionTimesUTC) {
- this(job, callingUid, sourcePkgName, sourceUserId,
+ Pair<Long, Long> persistedExecutionTimesUTC,
+ int innerFlags) {
+ this(job, callingUid, resolveTargetSdkVersion(job), sourcePkgName, sourceUserId,
standbyBucket, baseHeartbeat,
sourceTag, 0,
earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
- lastSuccessfulRunTime, lastFailedRunTime);
+ lastSuccessfulRunTime, lastFailedRunTime, innerFlags);
// Only during initial inflation do we record the UTC-timebase execution bounds
// read from the persistent store. If we ever have to recreate the JobStatus on
@@ -354,12 +380,12 @@ public final class JobStatus {
long newEarliestRuntimeElapsedMillis,
long newLatestRuntimeElapsedMillis, int backoffAttempt,
long lastSuccessfulRunTime, long lastFailedRunTime) {
- this(rescheduling.job, rescheduling.getUid(),
+ this(rescheduling.job, rescheduling.getUid(), resolveTargetSdkVersion(rescheduling.job),
rescheduling.getSourcePackageName(), rescheduling.getSourceUserId(),
rescheduling.getStandbyBucket(), newBaseHeartbeat,
rescheduling.getSourceTag(), backoffAttempt, newEarliestRuntimeElapsedMillis,
newLatestRuntimeElapsedMillis,
- lastSuccessfulRunTime, lastFailedRunTime);
+ lastSuccessfulRunTime, lastFailedRunTime, rescheduling.getInternalFlags());
}
/**
@@ -389,10 +415,12 @@ public final class JobStatus {
sourceUserId, elapsedNow);
JobSchedulerInternal js = LocalServices.getService(JobSchedulerInternal.class);
long currentHeartbeat = js != null ? js.currentHeartbeat() : 0;
- return new JobStatus(job, callingUid, sourcePkg, sourceUserId,
+
+ return new JobStatus(job, callingUid, resolveTargetSdkVersion(job), sourcePkg, sourceUserId,
standbyBucket, currentHeartbeat, tag, 0,
earliestRunTimeElapsedMillis, latestRunTimeElapsedMillis,
- 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */);
+ 0 /* lastSuccessfulRunTime */, 0 /* lastFailedRunTime */,
+ /*innerFlags=*/ 0);
}
public void enqueueWorkLocked(IActivityManager am, JobWorkItem work) {
@@ -536,6 +564,10 @@ public final class JobStatus {
return job.getId();
}
+ public int getTargetSdkVersion() {
+ return targetSdkVersion;
+ }
+
public void printUniqueId(PrintWriter pw) {
UserHandle.formatUid(pw, callingUid);
pw.print("/");
@@ -613,6 +645,28 @@ public final class JobStatus {
return job.getFlags();
}
+ public int getInternalFlags() {
+ return mInternalFlags;
+ }
+
+ public void addInternalFlags(int flags) {
+ mInternalFlags |= flags;
+ }
+
+ public void maybeAddForegroundExemption(Predicate<Integer> uidForegroundChecker) {
+ // Jobs with time constraints shouldn't be exempted.
+ if (job.hasEarlyConstraint() || job.hasLateConstraint()) {
+ return;
+ }
+ // Already exempted, skip the foreground check.
+ if ((mInternalFlags & INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0) {
+ return;
+ }
+ if (uidForegroundChecker.test(getSourceUid())) {
+ addInternalFlags(INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION);
+ }
+ }
+
private void updateEstimatedNetworkBytesLocked() {
totalNetworkBytes = computeEstimatedNetworkBytesLocked();
}
@@ -710,6 +764,37 @@ public final class JobStatus {
return latestRunTimeElapsedMillis;
}
+ /**
+ * Return the fractional position of "now" within the "run time" window of
+ * this job.
+ * <p>
+ * For example, if the earliest run time was 10 minutes ago, and the latest
+ * run time is 30 minutes from now, this would return 0.25.
+ * <p>
+ * If the job has no window defined, returns 1. When only an earliest or
+ * latest time is defined, it's treated as an infinitely small window at
+ * that time.
+ */
+ public float getFractionRunTime() {
+ final long now = JobSchedulerService.sElapsedRealtimeClock.millis();
+ if (earliestRunTimeElapsedMillis == 0 && latestRunTimeElapsedMillis == Long.MAX_VALUE) {
+ return 1;
+ } else if (earliestRunTimeElapsedMillis == 0) {
+ return now >= latestRunTimeElapsedMillis ? 1 : 0;
+ } else if (latestRunTimeElapsedMillis == Long.MAX_VALUE) {
+ return now >= earliestRunTimeElapsedMillis ? 1 : 0;
+ } else {
+ if (now <= earliestRunTimeElapsedMillis) {
+ return 0;
+ } else if (now >= latestRunTimeElapsedMillis) {
+ return 1;
+ } else {
+ return (float) (now - earliestRunTimeElapsedMillis)
+ / (float) (latestRunTimeElapsedMillis - earliestRunTimeElapsedMillis);
+ }
+ }
+ }
+
public Pair<Long, Long> getPersistedUtcTimes() {
return mPersistedUtcTimes;
}
@@ -968,6 +1053,20 @@ public final class JobStatus {
return sb.toString();
}
+ /**
+ * Convenience function to dump data that identifies a job uniquely to proto. This is intended
+ * to mimic {@link #toShortString}.
+ */
+ public void writeToShortProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+
+ proto.write(JobStatusShortInfoProto.CALLING_UID, callingUid);
+ proto.write(JobStatusShortInfoProto.JOB_ID, job.getId());
+ proto.write(JobStatusShortInfoProto.BATTERY_NAME, batteryName);
+
+ proto.end(token);
+ }
+
void dumpConstraints(PrintWriter pw, int constraints) {
if ((constraints&CONSTRAINT_CHARGING) != 0) {
pw.print(" CHARGING");
@@ -999,6 +1098,48 @@ public final class JobStatus {
if ((constraints&CONSTRAINT_DEVICE_NOT_DOZING) != 0) {
pw.print(" DEVICE_NOT_DOZING");
}
+ if ((constraints&CONSTRAINT_BACKGROUND_NOT_RESTRICTED) != 0) {
+ pw.print(" BACKGROUND_NOT_RESTRICTED");
+ }
+ if (constraints != 0) {
+ pw.print(" [0x");
+ pw.print(Integer.toHexString(constraints));
+ pw.print("]");
+ }
+ }
+
+ /** Writes constraints to the given repeating proto field. */
+ void dumpConstraints(ProtoOutputStream proto, long fieldId, int constraints) {
+ if ((constraints & CONSTRAINT_CHARGING) != 0) {
+ proto.write(fieldId, JobStatusDumpProto.CONSTRAINT_CHARGING);
+ }
+ if ((constraints & CONSTRAINT_BATTERY_NOT_LOW) != 0) {
+ proto.write(fieldId, JobStatusDumpProto.CONSTRAINT_BATTERY_NOT_LOW);
+ }
+ if ((constraints & CONSTRAINT_STORAGE_NOT_LOW) != 0) {
+ proto.write(fieldId, JobStatusDumpProto.CONSTRAINT_STORAGE_NOT_LOW);
+ }
+ if ((constraints & CONSTRAINT_TIMING_DELAY) != 0) {
+ proto.write(fieldId, JobStatusDumpProto.CONSTRAINT_TIMING_DELAY);
+ }
+ if ((constraints & CONSTRAINT_DEADLINE) != 0) {
+ proto.write(fieldId, JobStatusDumpProto.CONSTRAINT_DEADLINE);
+ }
+ if ((constraints & CONSTRAINT_IDLE) != 0) {
+ proto.write(fieldId, JobStatusDumpProto.CONSTRAINT_IDLE);
+ }
+ if ((constraints & CONSTRAINT_CONNECTIVITY) != 0) {
+ proto.write(fieldId, JobStatusDumpProto.CONSTRAINT_CONNECTIVITY);
+ }
+ if ((constraints & CONSTRAINT_APP_NOT_IDLE) != 0) {
+ proto.write(fieldId, JobStatusDumpProto.CONSTRAINT_APP_NOT_IDLE);
+ }
+ if ((constraints & CONSTRAINT_CONTENT_TRIGGER) != 0) {
+ proto.write(fieldId, JobStatusDumpProto.CONSTRAINT_CONTENT_TRIGGER);
+ }
+ if ((constraints & CONSTRAINT_DEVICE_NOT_DOZING) != 0) {
+ proto.write(fieldId, JobStatusDumpProto.CONSTRAINT_DEVICE_NOT_DOZING);
+ }
}
private void dumpJobWorkItem(PrintWriter pw, String prefix, JobWorkItem work, int index) {
@@ -1011,6 +1152,22 @@ public final class JobStatus {
}
}
+ private void dumpJobWorkItem(ProtoOutputStream proto, long fieldId, JobWorkItem work) {
+ final long token = proto.start(fieldId);
+
+ proto.write(JobStatusDumpProto.JobWorkItem.WORK_ID, work.getWorkId());
+ proto.write(JobStatusDumpProto.JobWorkItem.DELIVERY_COUNT, work.getDeliveryCount());
+ if (work.getIntent() != null) {
+ work.getIntent().writeToProto(proto, JobStatusDumpProto.JobWorkItem.INTENT);
+ }
+ Object grants = work.getGrants();
+ if (grants != null) {
+ ((GrantedUriPermissions) grants).dump(proto, JobStatusDumpProto.JobWorkItem.URI_GRANTS);
+ }
+
+ proto.end(token);
+ }
+
// normalized bucket indices, not the AppStandby constants
private String bucketName(int bucket) {
switch (bucket) {
@@ -1024,6 +1181,11 @@ public final class JobStatus {
}
}
+ private static int resolveTargetSdkVersion(JobInfo job) {
+ return LocalServices.getService(PackageManagerInternal.class)
+ .getPackageTargetSdkVersion(job.getService().getPackageName());
+ }
+
// Dumpsys infrastructure
public void dump(PrintWriter pw, String prefix, boolean full, long elapsedRealtimeMillis) {
pw.print(prefix); UserHandle.formatUid(pw, callingUid);
@@ -1052,6 +1214,15 @@ public final class JobStatus {
pw.print(prefix); pw.print(" Flags: ");
pw.println(Integer.toHexString(job.getFlags()));
}
+ if (getInternalFlags() != 0) {
+ pw.print(prefix); pw.print(" Internal flags: ");
+ pw.print(Integer.toHexString(getInternalFlags()));
+
+ if ((getInternalFlags()&INTERNAL_FLAG_HAS_FOREGROUND_EXEMPTION) != 0) {
+ pw.print(" HAS_FOREGROUND_EXEMPTION");
+ }
+ pw.println();
+ }
pw.print(prefix); pw.print(" Requires: charging=");
pw.print(job.isRequireCharging()); pw.print(" batteryNotLow=");
pw.print(job.isRequireBatteryNotLow()); pw.print(" deviceIdle=");
@@ -1198,4 +1369,169 @@ public final class JobStatus {
pw.println(t.format(format));
}
}
+
+ public void dump(ProtoOutputStream proto, long fieldId, boolean full, long elapsedRealtimeMillis) {
+ final long token = proto.start(fieldId);
+
+ proto.write(JobStatusDumpProto.CALLING_UID, callingUid);
+ proto.write(JobStatusDumpProto.TAG, tag);
+ proto.write(JobStatusDumpProto.SOURCE_UID, getSourceUid());
+ proto.write(JobStatusDumpProto.SOURCE_USER_ID, getSourceUserId());
+ proto.write(JobStatusDumpProto.SOURCE_PACKAGE_NAME, getSourcePackageName());
+ proto.write(JobStatusDumpProto.INTERNAL_FLAGS, getInternalFlags());
+
+ if (full) {
+ final long jiToken = proto.start(JobStatusDumpProto.JOB_INFO);
+
+ job.getService().writeToProto(proto, JobStatusDumpProto.JobInfo.SERVICE);
+
+ proto.write(JobStatusDumpProto.JobInfo.IS_PERIODIC, job.isPeriodic());
+ proto.write(JobStatusDumpProto.JobInfo.PERIOD_INTERVAL_MS, job.getIntervalMillis());
+ proto.write(JobStatusDumpProto.JobInfo.PERIOD_FLEX_MS, job.getFlexMillis());
+
+ proto.write(JobStatusDumpProto.JobInfo.IS_PERSISTED, job.isPersisted());
+ proto.write(JobStatusDumpProto.JobInfo.PRIORITY, job.getPriority());
+ proto.write(JobStatusDumpProto.JobInfo.FLAGS, job.getFlags());
+
+ proto.write(JobStatusDumpProto.JobInfo.REQUIRES_CHARGING, job.isRequireCharging());
+ proto.write(JobStatusDumpProto.JobInfo.REQUIRES_BATTERY_NOT_LOW, job.isRequireBatteryNotLow());
+ proto.write(JobStatusDumpProto.JobInfo.REQUIRES_DEVICE_IDLE, job.isRequireDeviceIdle());
+
+ if (job.getTriggerContentUris() != null) {
+ for (int i = 0; i < job.getTriggerContentUris().length; i++) {
+ final long tcuToken = proto.start(JobStatusDumpProto.JobInfo.TRIGGER_CONTENT_URIS);
+ JobInfo.TriggerContentUri trig = job.getTriggerContentUris()[i];
+
+ proto.write(JobStatusDumpProto.JobInfo.TriggerContentUri.FLAGS, trig.getFlags());
+ Uri u = trig.getUri();
+ if (u != null) {
+ proto.write(JobStatusDumpProto.JobInfo.TriggerContentUri.URI, u.toString());
+ }
+
+ proto.end(tcuToken);
+ }
+ if (job.getTriggerContentUpdateDelay() >= 0) {
+ proto.write(JobStatusDumpProto.JobInfo.TRIGGER_CONTENT_UPDATE_DELAY_MS,
+ job.getTriggerContentUpdateDelay());
+ }
+ if (job.getTriggerContentMaxDelay() >= 0) {
+ proto.write(JobStatusDumpProto.JobInfo.TRIGGER_CONTENT_MAX_DELAY_MS,
+ job.getTriggerContentMaxDelay());
+ }
+ }
+ if (job.getExtras() != null && !job.getExtras().maybeIsEmpty()) {
+ job.getExtras().writeToProto(proto, JobStatusDumpProto.JobInfo.EXTRAS);
+ }
+ if (job.getTransientExtras() != null && !job.getTransientExtras().maybeIsEmpty()) {
+ job.getTransientExtras().writeToProto(proto, JobStatusDumpProto.JobInfo.TRANSIENT_EXTRAS);
+ }
+ if (job.getClipData() != null) {
+ job.getClipData().writeToProto(proto, JobStatusDumpProto.JobInfo.CLIP_DATA);
+ }
+ if (uriPerms != null) {
+ uriPerms.dump(proto, JobStatusDumpProto.JobInfo.GRANTED_URI_PERMISSIONS);
+ }
+ if (job.getRequiredNetwork() != null) {
+ job.getRequiredNetwork().writeToProto(proto, JobStatusDumpProto.JobInfo.REQUIRED_NETWORK);
+ }
+ if (totalNetworkBytes != JobInfo.NETWORK_BYTES_UNKNOWN) {
+ proto.write(JobStatusDumpProto.JobInfo.TOTAL_NETWORK_BYTES, totalNetworkBytes);
+ }
+ proto.write(JobStatusDumpProto.JobInfo.MIN_LATENCY_MS, job.getMinLatencyMillis());
+ proto.write(JobStatusDumpProto.JobInfo.MAX_EXECUTION_DELAY_MS, job.getMaxExecutionDelayMillis());
+
+ final long bpToken = proto.start(JobStatusDumpProto.JobInfo.BACKOFF_POLICY);
+ proto.write(JobStatusDumpProto.JobInfo.Backoff.POLICY, job.getBackoffPolicy());
+ proto.write(JobStatusDumpProto.JobInfo.Backoff.INITIAL_BACKOFF_MS,
+ job.getInitialBackoffMillis());
+ proto.end(bpToken);
+
+ proto.write(JobStatusDumpProto.JobInfo.HAS_EARLY_CONSTRAINT, job.hasEarlyConstraint());
+ proto.write(JobStatusDumpProto.JobInfo.HAS_LATE_CONSTRAINT, job.hasLateConstraint());
+
+ proto.end(jiToken);
+ }
+
+ dumpConstraints(proto, JobStatusDumpProto.REQUIRED_CONSTRAINTS, requiredConstraints);
+ if (full) {
+ dumpConstraints(proto, JobStatusDumpProto.SATISFIED_CONSTRAINTS, satisfiedConstraints);
+ dumpConstraints(proto, JobStatusDumpProto.UNSATISFIED_CONSTRAINTS,
+ (requiredConstraints & ~satisfiedConstraints));
+ proto.write(JobStatusDumpProto.IS_DOZE_WHITELISTED, dozeWhitelisted);
+ }
+
+ // Tracking controllers
+ if ((trackingControllers&TRACKING_BATTERY) != 0) {
+ proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS,
+ JobStatusDumpProto.TRACKING_BATTERY);
+ }
+ if ((trackingControllers&TRACKING_CONNECTIVITY) != 0) {
+ proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS,
+ JobStatusDumpProto.TRACKING_CONNECTIVITY);
+ }
+ if ((trackingControllers&TRACKING_CONTENT) != 0) {
+ proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS,
+ JobStatusDumpProto.TRACKING_CONTENT);
+ }
+ if ((trackingControllers&TRACKING_IDLE) != 0) {
+ proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS,
+ JobStatusDumpProto.TRACKING_IDLE);
+ }
+ if ((trackingControllers&TRACKING_STORAGE) != 0) {
+ proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS,
+ JobStatusDumpProto.TRACKING_STORAGE);
+ }
+ if ((trackingControllers&TRACKING_TIME) != 0) {
+ proto.write(JobStatusDumpProto.TRACKING_CONTROLLERS,
+ JobStatusDumpProto.TRACKING_TIME);
+ }
+
+ if (changedAuthorities != null) {
+ for (int k = 0; k < changedAuthorities.size(); k++) {
+ proto.write(JobStatusDumpProto.CHANGED_AUTHORITIES, changedAuthorities.valueAt(k));
+ }
+ }
+ if (changedUris != null) {
+ for (int i = 0; i < changedUris.size(); i++) {
+ Uri u = changedUris.valueAt(i);
+ proto.write(JobStatusDumpProto.CHANGED_URIS, u.toString());
+ }
+ }
+
+ if (network != null) {
+ network.writeToProto(proto, JobStatusDumpProto.NETWORK);
+ }
+
+ if (pendingWork != null && pendingWork.size() > 0) {
+ for (int i = 0; i < pendingWork.size(); i++) {
+ dumpJobWorkItem(proto, JobStatusDumpProto.PENDING_WORK, pendingWork.get(i));
+ }
+ }
+ if (executingWork != null && executingWork.size() > 0) {
+ for (int i = 0; i < executingWork.size(); i++) {
+ dumpJobWorkItem(proto, JobStatusDumpProto.EXECUTING_WORK, executingWork.get(i));
+ }
+ }
+
+ proto.write(JobStatusDumpProto.STANDBY_BUCKET, standbyBucket);
+ proto.write(JobStatusDumpProto.ENQUEUE_DURATION_MS, elapsedRealtimeMillis - enqueueTime);
+ if (earliestRunTimeElapsedMillis == NO_EARLIEST_RUNTIME) {
+ proto.write(JobStatusDumpProto.TIME_UNTIL_EARLIEST_RUNTIME_MS, 0);
+ } else {
+ proto.write(JobStatusDumpProto.TIME_UNTIL_EARLIEST_RUNTIME_MS,
+ earliestRunTimeElapsedMillis - elapsedRealtimeMillis);
+ }
+ if (latestRunTimeElapsedMillis == NO_LATEST_RUNTIME) {
+ proto.write(JobStatusDumpProto.TIME_UNTIL_LATEST_RUNTIME_MS, 0);
+ } else {
+ proto.write(JobStatusDumpProto.TIME_UNTIL_LATEST_RUNTIME_MS,
+ latestRunTimeElapsedMillis - elapsedRealtimeMillis);
+ }
+
+ proto.write(JobStatusDumpProto.NUM_FAILURES, numFailures);
+ proto.write(JobStatusDumpProto.LAST_SUCCESSFUL_RUN_TIME, mLastSuccessfulRunTime);
+ proto.write(JobStatusDumpProto.LAST_FAILED_RUN_TIME, mLastFailedRunTime);
+
+ proto.end(token);
+ }
}
diff --git a/com/android/server/job/controllers/StateController.java b/com/android/server/job/controllers/StateController.java
index 497faab8..d3055e6f 100644
--- a/com/android/server/job/controllers/StateController.java
+++ b/com/android/server/job/controllers/StateController.java
@@ -17,6 +17,7 @@
package com.android.server.job.controllers;
import android.content.Context;
+import android.util.proto.ProtoOutputStream;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateChangedListener;
@@ -65,4 +66,6 @@ public abstract class StateController {
}
public abstract void dumpControllerStateLocked(PrintWriter pw, int filterUid);
+ public abstract void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId,
+ int filterUid);
}
diff --git a/com/android/server/job/controllers/StorageController.java b/com/android/server/job/controllers/StorageController.java
index 84782f59..0519b635 100644
--- a/com/android/server/job/controllers/StorageController.java
+++ b/com/android/server/job/controllers/StorageController.java
@@ -25,10 +25,12 @@ import android.content.IntentFilter;
import android.os.UserHandle;
import android.util.ArraySet;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateChangedListener;
+import com.android.server.job.StateControllerProto;
import com.android.server.storage.DeviceStorageMonitorService;
import java.io.PrintWriter;
@@ -119,7 +121,7 @@ public final class StorageController extends StateController {
*/
private boolean mStorageLow;
/** Sequence number of last broadcast. */
- private int mLastBatterySeq = -1;
+ private int mLastStorageSeq = -1;
public StorageTracker() {
}
@@ -139,7 +141,7 @@ public final class StorageController extends StateController {
}
public int getSeq() {
- return mLastBatterySeq;
+ return mLastStorageSeq;
}
@Override
@@ -150,8 +152,8 @@ public final class StorageController extends StateController {
@VisibleForTesting
public void onReceiveInternal(Intent intent) {
final String action = intent.getAction();
- mLastBatterySeq = intent.getIntExtra(DeviceStorageMonitorService.EXTRA_SEQUENCE,
- mLastBatterySeq);
+ mLastStorageSeq = intent.getIntExtra(DeviceStorageMonitorService.EXTRA_SEQUENCE,
+ mLastStorageSeq);
if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(action)) {
if (DEBUG) {
Slog.d(TAG, "Available storage too low to do work. @ "
@@ -190,4 +192,30 @@ public final class StorageController extends StateController {
pw.println();
}
}
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, int filterUid) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.STORAGE);
+
+ proto.write(StateControllerProto.StorageController.IS_STORAGE_NOT_LOW,
+ mStorageTracker.isStorageNotLow());
+ proto.write(StateControllerProto.StorageController.LAST_BROADCAST_SEQUENCE_NUMBER,
+ mStorageTracker.getSeq());
+
+ for (int i = 0; i < mTrackedTasks.size(); i++) {
+ final JobStatus js = mTrackedTasks.valueAt(i);
+ if (!js.shouldDump(filterUid)) {
+ continue;
+ }
+ final long jsToken = proto.start(StateControllerProto.StorageController.TRACKED_JOBS);
+ js.writeToShortProto(proto, StateControllerProto.StorageController.TrackedJob.INFO);
+ proto.write(StateControllerProto.StorageController.TrackedJob.SOURCE_UID,
+ js.getSourceUid());
+ proto.end(jsToken);
+ }
+
+ proto.end(mToken);
+ proto.end(token);
+ }
}
diff --git a/com/android/server/job/controllers/TimeController.java b/com/android/server/job/controllers/TimeController.java
index cb9e43a1..bbee0ebd 100644
--- a/com/android/server/job/controllers/TimeController.java
+++ b/com/android/server/job/controllers/TimeController.java
@@ -25,9 +25,11 @@ import android.os.UserHandle;
import android.os.WorkSource;
import android.util.Slog;
import android.util.TimeUtils;
+import android.util.proto.ProtoOutputStream;
import com.android.server.job.JobSchedulerService;
import com.android.server.job.StateChangedListener;
+import com.android.server.job.StateControllerProto;
import java.io.PrintWriter;
import java.util.Iterator;
@@ -331,7 +333,7 @@ public final class TimeController extends StateController {
public void dumpControllerStateLocked(PrintWriter pw, int filterUid) {
final long nowElapsed = sElapsedRealtimeClock.millis();
pw.print("Alarms: now=");
- pw.print(sElapsedRealtimeClock.millis());
+ pw.print(nowElapsed);
pw.println();
pw.print("Next delay alarm in ");
TimeUtils.formatDuration(mNextDelayExpiredElapsedMillis, nowElapsed, pw);
@@ -365,4 +367,40 @@ public final class TimeController extends StateController {
pw.println();
}
}
-} \ No newline at end of file
+
+ @Override
+ public void dumpControllerStateLocked(ProtoOutputStream proto, long fieldId, int filterUid) {
+ final long token = proto.start(fieldId);
+ final long mToken = proto.start(StateControllerProto.TIME);
+
+ final long nowElapsed = sElapsedRealtimeClock.millis();
+ proto.write(StateControllerProto.TimeController.NOW_ELAPSED_REALTIME, nowElapsed);
+ proto.write(StateControllerProto.TimeController.TIME_UNTIL_NEXT_DELAY_ALARM_MS,
+ mNextDelayExpiredElapsedMillis - nowElapsed);
+ proto.write(StateControllerProto.TimeController.TIME_UNTIL_NEXT_DEADLINE_ALARM_MS,
+ mNextJobExpiredElapsedMillis - nowElapsed);
+
+ for (JobStatus ts : mTrackedJobs) {
+ if (!ts.shouldDump(filterUid)) {
+ continue;
+ }
+ final long tsToken = proto.start(StateControllerProto.TimeController.TRACKED_JOBS);
+ ts.writeToShortProto(proto, StateControllerProto.TimeController.TrackedJob.INFO);
+
+ proto.write(StateControllerProto.TimeController.TrackedJob.HAS_TIMING_DELAY_CONSTRAINT,
+ ts.hasTimingDelayConstraint());
+ proto.write(StateControllerProto.TimeController.TrackedJob.DELAY_TIME_REMAINING_MS,
+ ts.getEarliestRunTime() - nowElapsed);
+
+ proto.write(StateControllerProto.TimeController.TrackedJob.HAS_DEADLINE_CONSTRAINT,
+ ts.hasDeadlineConstraint());
+ proto.write(StateControllerProto.TimeController.TrackedJob.TIME_REMAINING_UNTIL_DEADLINE_MS,
+ ts.getLatestRunTimeElapsed() - nowElapsed);
+
+ proto.end(tsToken);
+ }
+
+ proto.end(mToken);
+ proto.end(token);
+ }
+}
diff --git a/com/android/server/location/ContextHubClientManager.java b/com/android/server/location/ContextHubClientManager.java
index 60b5b1f0..a61842b6 100644
--- a/com/android/server/location/ContextHubClientManager.java
+++ b/com/android/server/location/ContextHubClientManager.java
@@ -113,9 +113,7 @@ import java.util.function.Consumer;
NanoAppMessage clientMessage = ContextHubServiceUtil.createNanoAppMessage(message);
if (DEBUG_LOG_ENABLED) {
- String targetAudience = clientMessage.isBroadcastMessage() ? "broadcast" : "unicast";
- Log.v(TAG, "Received a " + targetAudience + " message from nanoapp 0x"
- + Long.toHexString(clientMessage.getNanoAppId()));
+ Log.v(TAG, "Received " + clientMessage);
}
if (clientMessage.isBroadcastMessage()) {
diff --git a/com/android/server/location/ContextHubServiceUtil.java b/com/android/server/location/ContextHubServiceUtil.java
index c356b639..033437a5 100644
--- a/com/android/server/location/ContextHubServiceUtil.java
+++ b/com/android/server/location/ContextHubServiceUtil.java
@@ -221,7 +221,7 @@ import java.util.ArrayList;
case Result.NOT_INIT:
return ContextHubTransaction.RESULT_FAILED_UNINITIALIZED;
case Result.TRANSACTION_PENDING:
- return ContextHubTransaction.RESULT_FAILED_PENDING;
+ return ContextHubTransaction.RESULT_FAILED_BUSY;
case Result.TRANSACTION_FAILED:
case Result.UNKNOWN_FAILURE:
default: /* fall through */
diff --git a/com/android/server/location/GnssLocationProvider.java b/com/android/server/location/GnssLocationProvider.java
index e6de07d2..48d275cc 100644
--- a/com/android/server/location/GnssLocationProvider.java
+++ b/com/android/server/location/GnssLocationProvider.java
@@ -84,8 +84,6 @@ import com.android.internal.location.GpsNetInitiatedHandler.GpsNiNotification;
import com.android.internal.location.ProviderProperties;
import com.android.internal.location.ProviderRequest;
-import com.android.server.power.BatterySaverPolicy;
-
import libcore.io.IoUtils;
import java.io.File;
@@ -579,7 +577,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
final PowerSaveState result =
mPowerManager.getPowerSaveState(ServiceType.GPS);
switch (result.gpsMode) {
- case BatterySaverPolicy.GPS_MODE_DISABLED_WHEN_SCREEN_OFF:
+ case PowerManager.LOCATION_MODE_GPS_DISABLED_WHEN_SCREEN_OFF:
// If we are in battery saver mode and the screen is off, disable GPS.
disableGps |= result.batterySaverEnabled && !mPowerManager.isInteractive();
break;
@@ -829,7 +827,7 @@ public class GnssLocationProvider implements LocationProviderInterface {
return isEnabled();
}
};
- mGnssMetrics = new GnssMetrics();
+ mGnssMetrics = new GnssMetrics(mBatteryStats);
}
/**
@@ -2630,6 +2628,10 @@ public class GnssLocationProvider implements LocationProviderInterface {
s.append(" mStarted=").append(mStarted).append('\n');
s.append(" mFixInterval=").append(mFixInterval).append('\n');
s.append(" mLowPowerMode=").append(mLowPowerMode).append('\n');
+ s.append(" mGnssMeasurementsProvider.isRegistered()=")
+ .append(mGnssMeasurementsProvider.isRegistered()).append('\n');
+ s.append(" mGnssNavigationMessageProvider.isRegistered()=")
+ .append(mGnssNavigationMessageProvider.isRegistered()).append('\n');
s.append(" mDisableGps (battery saver mode)=").append(mDisableGps).append('\n');
s.append(" mEngineCapabilities=0x").append(Integer.toHexString(mEngineCapabilities));
s.append(" ( ");
diff --git a/com/android/server/location/RemoteListenerHelper.java b/com/android/server/location/RemoteListenerHelper.java
index 58a95162..fcdb9d1a 100644
--- a/com/android/server/location/RemoteListenerHelper.java
+++ b/com/android/server/location/RemoteListenerHelper.java
@@ -46,7 +46,8 @@ abstract class RemoteListenerHelper<TListener extends IInterface> {
private final Map<IBinder, LinkedListener> mListenerMap = new HashMap<>();
- private boolean mIsRegistered; // must access only on handler thread
+ private volatile boolean mIsRegistered; // must access only on handler thread, or read-only
+
private boolean mHasIsSupported;
private boolean mIsSupported;
@@ -58,6 +59,11 @@ abstract class RemoteListenerHelper<TListener extends IInterface> {
mTag = name;
}
+ // read-only access for a dump() thread assured via volatile
+ public boolean isRegistered() {
+ return mIsRegistered;
+ }
+
public boolean addListener(@NonNull TListener listener) {
Preconditions.checkNotNull(listener, "Attempted to register a 'null' listener.");
IBinder binder = listener.asBinder();
diff --git a/com/android/server/locksettings/LockSettingsService.java b/com/android/server/locksettings/LockSettingsService.java
index 482acefa..31c20cbf 100644
--- a/com/android/server/locksettings/LockSettingsService.java
+++ b/com/android/server/locksettings/LockSettingsService.java
@@ -53,6 +53,7 @@ import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.database.ContentObserver;
import android.database.sqlite.SQLiteDatabase;
+import android.hardware.authsecret.V1_0.IAuthSecret;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
@@ -63,7 +64,6 @@ 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;
@@ -79,10 +79,9 @@ 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.security.keystore.recovery.KeyChainProtectionParams;
+import android.security.keystore.recovery.WrappedApplicationKey;
+import android.security.keystore.recovery.KeyChainSnapshot;
import android.service.gatekeeper.GateKeeperResponse;
import android.service.gatekeeper.IGateKeeperService;
import android.text.TextUtils;
@@ -90,6 +89,7 @@ import android.util.ArrayMap;
import android.util.EventLog;
import android.util.Log;
import android.util.Slog;
+import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -127,8 +127,10 @@ import java.security.SecureRandom;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.Arrays;
+import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import java.util.NoSuchElementException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -184,6 +186,7 @@ public class LockSettingsService extends ILockSettings.Stub {
private boolean mFirstCallToVold;
protected IGateKeeperService mGateKeeperService;
+ protected IAuthSecret mAuthSecretService;
/**
* The UIDs that are used for system credential storage in keystore.
@@ -614,6 +617,14 @@ public class LockSettingsService extends ILockSettings.Stub {
} catch (RemoteException e) {
Slog.e(TAG, "Failure retrieving IGateKeeperService", e);
}
+ // Find the AuthSecret HAL
+ try {
+ mAuthSecretService = IAuthSecret.getService();
+ } catch (NoSuchElementException e) {
+ Slog.i(TAG, "Device doesn't implement AuthSecret HAL");
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to get AuthSecret HAL", e);
+ }
mDeviceProvisionedObserver.onSystemReady();
// TODO: maybe skip this for split system user mode.
mStorage.prefetchUser(UserHandle.USER_SYSTEM);
@@ -912,8 +923,11 @@ public class LockSettingsService extends ILockSettings.Stub {
}
private void notifySeparateProfileChallengeChanged(int userId) {
- LocalServices.getService(DevicePolicyManagerInternal.class)
- .reportSeparateProfileChallengeChanged(userId);
+ final DevicePolicyManagerInternal dpmi = LocalServices.getService(
+ DevicePolicyManagerInternal.class);
+ if (dpmi != null) {
+ dpmi.reportSeparateProfileChallengeChanged(userId);
+ }
}
@Override
@@ -1284,6 +1298,7 @@ public class LockSettingsService extends ILockSettings.Stub {
fixateNewestUserKeyAuth(userId);
synchronizeUnifiedWorkChallengeForProfiles(userId, null);
notifyActivePasswordMetricsAvailable(null, userId);
+ mRecoverableKeyStoreManager.lockScreenSecretChanged(credentialType, credential, userId);
return;
}
if (credential == null) {
@@ -1333,6 +1348,8 @@ public class LockSettingsService extends ILockSettings.Stub {
.verifyChallenge(userId, 0, willStore.hash, credential.getBytes());
setUserKeyProtection(userId, credential, convertResponse(gkResponse));
fixateNewestUserKeyAuth(userId);
+ mRecoverableKeyStoreManager.lockScreenSecretChanged(credentialType, credential,
+ userId);
// Refresh the auth token
doVerifyCredential(credential, credentialType, true, 0, userId, null /* progressCallback */);
synchronizeUnifiedWorkChallengeForProfiles(userId, null);
@@ -1581,8 +1598,10 @@ public class LockSettingsService extends ILockSettings.Stub {
userId, progressCallback);
// The user employs synthetic password based credential.
if (response != null) {
- mRecoverableKeyStoreManager.lockScreenSecretAvailable(credentialType, credential,
- userId);
+ if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) {
+ mRecoverableKeyStoreManager.lockScreenSecretAvailable(credentialType, credential,
+ userId);
+ }
return response;
}
@@ -1868,6 +1887,7 @@ public class LockSettingsService extends ILockSettings.Stub {
mSpManager.removeUser(userId);
mStorage.removeUser(userId);
mStrongAuth.removeUser(userId);
+ cleanSpCache();
final KeyStore ks = KeyStore.getInstance();
ks.onUserRemoved(userId);
@@ -1956,81 +1976,87 @@ public class LockSettingsService extends ILockSettings.Stub {
@Override
public void initRecoveryService(@NonNull String rootCertificateAlias,
- @NonNull byte[] signedPublicKeyList, @UserIdInt int userId)
- throws RemoteException {
+ @NonNull byte[] signedPublicKeyList) throws RemoteException {
mRecoverableKeyStoreManager.initRecoveryService(rootCertificateAlias,
- signedPublicKeyList, userId);
+ signedPublicKeyList);
}
@Override
- public KeyStoreRecoveryData getRecoveryData(@NonNull byte[] account, @UserIdInt int userId)
- throws RemoteException {
- return mRecoverableKeyStoreManager.getRecoveryData(account, userId);
+ public KeyChainSnapshot getRecoveryData(@NonNull byte[] account) throws RemoteException {
+ return mRecoverableKeyStoreManager.getRecoveryData(account);
}
- public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent, int userId)
+ public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
throws RemoteException {
- mRecoverableKeyStoreManager.setSnapshotCreatedPendingIntent(intent, userId);
+ mRecoverableKeyStoreManager.setSnapshotCreatedPendingIntent(intent);
}
- public Map getRecoverySnapshotVersions(int userId) throws RemoteException {
- return mRecoverableKeyStoreManager.getRecoverySnapshotVersions(userId);
+ public Map getRecoverySnapshotVersions() throws RemoteException {
+ return mRecoverableKeyStoreManager.getRecoverySnapshotVersions();
}
@Override
- public void setServerParameters(long serverParameters, @UserIdInt int userId)
- throws RemoteException {
- mRecoverableKeyStoreManager.setServerParameters(serverParameters, userId);
+ public void setServerParams(byte[] serverParams) throws RemoteException {
+ mRecoverableKeyStoreManager.setServerParams(serverParams);
}
@Override
public void setRecoveryStatus(@NonNull String packageName, @Nullable String[] aliases,
- int status, @UserIdInt int userId) throws RemoteException {
- mRecoverableKeyStoreManager.setRecoveryStatus(packageName, aliases, status, userId);
+ int status) throws RemoteException {
+ mRecoverableKeyStoreManager.setRecoveryStatus(packageName, aliases, status);
}
- public Map getRecoveryStatus(@Nullable String packageName, int userId) throws RemoteException {
- return mRecoverableKeyStoreManager.getRecoveryStatus(packageName, userId);
+ public Map getRecoveryStatus(@Nullable String packageName) throws RemoteException {
+ return mRecoverableKeyStoreManager.getRecoveryStatus(packageName);
}
@Override
- public void setRecoverySecretTypes(@NonNull @KeyStoreRecoveryMetadata.UserSecretType
- int[] secretTypes, @UserIdInt int userId) throws RemoteException {
- mRecoverableKeyStoreManager.setRecoverySecretTypes(secretTypes, userId);
+ public void setRecoverySecretTypes(@NonNull @KeyChainProtectionParams.UserSecretType
+ int[] secretTypes) throws RemoteException {
+ mRecoverableKeyStoreManager.setRecoverySecretTypes(secretTypes);
}
@Override
- public int[] getRecoverySecretTypes(@UserIdInt int userId) throws RemoteException {
- return mRecoverableKeyStoreManager.getRecoverySecretTypes(userId);
+ public int[] getRecoverySecretTypes() throws RemoteException {
+ return mRecoverableKeyStoreManager.getRecoverySecretTypes();
}
@Override
- public int[] getPendingRecoverySecretTypes(@UserIdInt int userId) throws RemoteException {
+ public int[] getPendingRecoverySecretTypes() throws RemoteException {
throw new SecurityException("Not implemented");
}
@Override
- public void recoverySecretAvailable(@NonNull KeyStoreRecoveryMetadata recoverySecret,
- @UserIdInt int userId) throws RemoteException {
- mRecoverableKeyStoreManager.recoverySecretAvailable(recoverySecret, userId);
+ public void recoverySecretAvailable(@NonNull KeyChainProtectionParams recoverySecret)
+ throws RemoteException {
+ mRecoverableKeyStoreManager.recoverySecretAvailable(recoverySecret);
}
@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 {
+ @NonNull byte[] vaultChallenge, @NonNull List<KeyChainProtectionParams> secrets)
+ throws RemoteException {
return mRecoverableKeyStoreManager.startRecoverySession(sessionId, verifierPublicKey,
- vaultParams, vaultChallenge, secrets, userId);
+ vaultParams, vaultChallenge, secrets);
+ }
+
+ public void closeSession(@NonNull String sessionId) throws RemoteException {
+ mRecoverableKeyStoreManager.closeSession(sessionId);
}
@Override
- public Map<String, byte[]> recoverKeys(@NonNull String sessionId, @NonNull byte[] recoveryKeyBlob,
- @NonNull List<KeyEntryRecoveryData> applicationKeys, @UserIdInt int userId)
+ public Map<String, byte[]> recoverKeys(@NonNull String sessionId,
+ @NonNull byte[] recoveryKeyBlob, @NonNull List<WrappedApplicationKey> applicationKeys)
throws RemoteException {
return mRecoverableKeyStoreManager.recoverKeys(
- sessionId, recoveryKeyBlob, applicationKeys, userId);
+ sessionId, recoveryKeyBlob, applicationKeys);
+ }
+
+ @Override
+ public void removeKey(@NonNull String alias) throws RemoteException {
+ mRecoverableKeyStoreManager.removeKey(alias);
}
@Override
@@ -2104,6 +2130,77 @@ public class LockSettingsService extends ILockSettings.Stub {
}
/**
+ * A user's synthetic password does not change so it must be cached in certain circumstances to
+ * enable untrusted credential reset.
+ *
+ * Untrusted credential reset will be removed in a future version (b/68036371) at which point
+ * this cache is no longer needed as the SP will always be known when changing the user's
+ * credential.
+ */
+ @GuardedBy("mSpManager")
+ private SparseArray<AuthenticationToken> mSpCache = new SparseArray();
+
+ private void onAuthTokenKnownForUser(@UserIdInt int userId, AuthenticationToken auth) {
+ // Pass the primary user's auth secret to the HAL
+ if (mAuthSecretService != null && mUserManager.getUserInfo(userId).isPrimary()) {
+ try {
+ final byte[] rawSecret = auth.deriveVendorAuthSecret();
+ final ArrayList<Byte> secret = new ArrayList<>(rawSecret.length);
+ for (int i = 0; i < rawSecret.length; ++i) {
+ secret.add(rawSecret[i]);
+ }
+ mAuthSecretService.primaryUserCredential(secret);
+ } catch (RemoteException e) {
+ Slog.w(TAG, "Failed to pass primary user secret to AuthSecret HAL", e);
+ }
+ }
+
+ // Update the SP cache, removing the entry when allowed
+ synchronized (mSpManager) {
+ if (shouldCacheSpForUser(userId)) {
+ Slog.i(TAG, "Caching SP for user " + userId);
+ mSpCache.put(userId, auth);
+ } else {
+ Slog.i(TAG, "Not caching SP for user " + userId);
+ mSpCache.delete(userId);
+ }
+ }
+ }
+
+ /** Clean up the SP cache by removing unneeded entries. */
+ private void cleanSpCache() {
+ synchronized (mSpManager) {
+ // Preserve indicies after removal by iterating backwards
+ for (int i = mSpCache.size() - 1; i >= 0; --i) {
+ final int userId = mSpCache.keyAt(i);
+ if (!shouldCacheSpForUser(userId)) {
+ Slog.i(TAG, "Uncaching SP for user " + userId);
+ mSpCache.removeAt(i);
+ }
+ }
+ }
+ }
+
+ private boolean shouldCacheSpForUser(@UserIdInt int userId) {
+ // Before the user setup has completed, an admin could be installed that requires the SP to
+ // be cached (see below).
+ if (Settings.Secure.getIntForUser(mContext.getContentResolver(),
+ Settings.Secure.USER_SETUP_COMPLETE, 0, userId) == 0) {
+ return true;
+ }
+
+ // If the user has an admin which can perform an untrusted credential reset, the SP needs to
+ // be cached. If there isn't a DevicePolicyManager then there can't be an admin in the first
+ // place so caching is not necessary.
+ final DevicePolicyManagerInternal dpmi = LocalServices.getService(
+ DevicePolicyManagerInternal.class);
+ if (dpmi == null) {
+ return false;
+ }
+ return dpmi.canUserHaveUntrustedCredentialReset(userId);
+ }
+
+ /**
* Precondition: vold and keystore unlocked.
*
* Create new synthetic password, set up synthetic password blob protected by the supplied
@@ -2118,9 +2215,7 @@ public class LockSettingsService extends ILockSettings.Stub {
* 3. Once a user is migrated to have synthetic password, its value will never change, no matter
* whether the user changes his lockscreen PIN or clear/reset it. When the user clears its
* lockscreen PIN, we still maintain the existing synthetic password in a password blob
- * protected by a default PIN. The only exception is when the DPC performs an untrusted
- * credential change, in which case we have no way to derive the existing synthetic password
- * and has to create a new one.
+ * protected by a default PIN.
* 4. The user SID is linked with synthetic password, but its cleared/re-created when the user
* clears/re-creates his lockscreen PIN.
*
@@ -2140,13 +2235,23 @@ public class LockSettingsService extends ILockSettings.Stub {
* This is the untrusted credential reset, OR the user sets a new lockscreen password
* FOR THE FIRST TIME on a SP-enabled device. New credential and new SID will be created
*/
+ @GuardedBy("mSpManager")
@VisibleForTesting
protected AuthenticationToken initializeSyntheticPasswordLocked(byte[] credentialHash,
String credential, int credentialType, int requestedQuality,
int userId) throws RemoteException {
Slog.i(TAG, "Initialize SyntheticPassword for user: " + userId);
- AuthenticationToken auth = mSpManager.newSyntheticPasswordAndSid(getGateKeeperService(),
- credentialHash, credential, userId);
+ // Load from the cache or a make a new one
+ AuthenticationToken auth = mSpCache.get(userId);
+ if (auth != null) {
+ // If the synthetic password has been cached, we can only be in case 3., described
+ // above, for an untrusted credential reset so a new SID is still needed.
+ mSpManager.newSidForUser(getGateKeeperService(), auth, userId);
+ } else {
+ auth = mSpManager.newSyntheticPasswordAndSid(getGateKeeperService(),
+ credentialHash, credential, userId);
+ }
+ onAuthTokenKnownForUser(userId, auth);
if (auth == null) {
Slog.wtf(TAG, "initializeSyntheticPasswordLocked returns null auth token");
return null;
@@ -2261,6 +2366,8 @@ public class LockSettingsService extends ILockSettings.Stub {
trustManager.setDeviceLockedForUser(userId, false);
}
mStrongAuth.reportSuccessfulStrongAuthUnlock(userId);
+
+ onAuthTokenKnownForUser(userId, authResult.authToken);
} else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
if (response.getTimeout() > 0) {
requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, userId);
@@ -2279,6 +2386,7 @@ public class LockSettingsService extends ILockSettings.Stub {
* SID is gone. We also clear password from (software-based) keystore and vold, which will be
* added back when new password is set in future.
*/
+ @GuardedBy("mSpManager")
private long setLockCredentialWithAuthTokenLocked(String credential, int credentialType,
AuthenticationToken auth, int requestedQuality, int userId) throws RemoteException {
if (DEBUG) Slog.d(TAG, "setLockCredentialWithAuthTokenLocked: user=" + userId);
@@ -2326,6 +2434,7 @@ public class LockSettingsService extends ILockSettings.Stub {
return newHandle;
}
+ @GuardedBy("mSpManager")
private void spBasedSetLockCredentialInternalLocked(String credential, int credentialType,
String savedCredential, int requestedQuality, int userId) throws RemoteException {
if (DEBUG) Slog.d(TAG, "spBasedSetLockCredentialInternalLocked: user=" + userId);
@@ -2361,13 +2470,19 @@ public class LockSettingsService extends ILockSettings.Stub {
setLockCredentialWithAuthTokenLocked(credential, credentialType, auth, requestedQuality,
userId);
mSpManager.destroyPasswordBasedSyntheticPassword(handle, userId);
+ onAuthTokenKnownForUser(userId, auth);
} else if (response != null
- && response.getResponseCode() == VerifyCredentialResponse.RESPONSE_ERROR){
+ && response.getResponseCode() == VerifyCredentialResponse.RESPONSE_ERROR) {
// We are performing an untrusted credential change i.e. by DevicePolicyManager.
// So provision a new SP and SID. This would invalidate existing escrow tokens.
// Still support this for now but this flow will be removed in the next release.
-
Slog.w(TAG, "Untrusted credential change invoked");
+
+ if (mSpCache.get(userId) == null) {
+ throw new IllegalStateException(
+ "Untrusted credential reset not possible without cached SP");
+ }
+
initializeSyntheticPasswordLocked(null, credential, credentialType, requestedQuality,
userId);
synchronizeUnifiedWorkChallengeForProfiles(userId, null);
@@ -2478,8 +2593,9 @@ public class LockSettingsService extends ILockSettings.Stub {
private boolean setLockCredentialWithTokenInternal(String credential, int type,
long tokenHandle, byte[] token, int requestedQuality, int userId) throws RemoteException {
+ final AuthenticationResult result;
synchronized (mSpManager) {
- AuthenticationResult result = mSpManager.unwrapTokenBasedSyntheticPassword(
+ result = mSpManager.unwrapTokenBasedSyntheticPassword(
getGateKeeperService(), tokenHandle, token, userId);
if (result.authToken == null) {
Slog.w(TAG, "Invalid escrow token supplied");
@@ -2500,8 +2616,9 @@ public class LockSettingsService extends ILockSettings.Stub {
setLockCredentialWithAuthTokenLocked(credential, type, result.authToken,
requestedQuality, userId);
mSpManager.destroyPasswordBasedSyntheticPassword(oldHandle, userId);
- return true;
}
+ onAuthTokenKnownForUser(userId, result.authToken);
+ return true;
}
@Override
@@ -2521,6 +2638,7 @@ public class LockSettingsService extends ILockSettings.Stub {
}
}
unlockUser(userId, null, authResult.authToken.deriveDiskEncryptionKey());
+ onAuthTokenKnownForUser(userId, authResult.authToken);
}
@Override
@@ -2602,6 +2720,8 @@ public class LockSettingsService extends ILockSettings.Stub {
private class DeviceProvisionedObserver extends ContentObserver {
private final Uri mDeviceProvisionedUri = Settings.Global.getUriFor(
Settings.Global.DEVICE_PROVISIONED);
+ private final Uri mUserSetupCompleteUri = Settings.Secure.getUriFor(
+ Settings.Secure.USER_SETUP_COMPLETE);
private boolean mRegistered;
@@ -2619,6 +2739,8 @@ public class LockSettingsService extends ILockSettings.Stub {
reportDeviceSetupComplete();
clearFrpCredentialIfOwnerNotSecure();
}
+ } else if (mUserSetupCompleteUri.equals(uri)) {
+ cleanSpCache();
}
}
@@ -2670,6 +2792,8 @@ public class LockSettingsService extends ILockSettings.Stub {
if (register) {
mContext.getContentResolver().registerContentObserver(mDeviceProvisionedUri,
false, this);
+ mContext.getContentResolver().registerContentObserver(mUserSetupCompleteUri,
+ false, this, UserHandle.USER_ALL);
} else {
mContext.getContentResolver().unregisterContentObserver(this);
}
diff --git a/com/android/server/locksettings/LockSettingsStorage.java b/com/android/server/locksettings/LockSettingsStorage.java
index 70d60722..f62e8a9b 100644
--- a/com/android/server/locksettings/LockSettingsStorage.java
+++ b/com/android/server/locksettings/LockSettingsStorage.java
@@ -627,7 +627,12 @@ class LockSettingsStorage {
if (persistentDataBlock == null) {
return PersistentData.NONE;
}
- return PersistentData.fromBytes(persistentDataBlock.getFrpCredentialHandle());
+ try {
+ return PersistentData.fromBytes(persistentDataBlock.getFrpCredentialHandle());
+ } catch (IllegalStateException e) {
+ Slog.e(TAG, "Error reading persistent data block", e);
+ return PersistentData.NONE;
+ }
}
public static class PersistentData {
@@ -670,11 +675,11 @@ class LockSettingsStorage {
return new PersistentData(type, userId, qualityForUi, payload);
} else {
Slog.wtf(TAG, "Unknown PersistentData version code: " + version);
- return null;
+ return NONE;
}
} catch (IOException e) {
Slog.wtf(TAG, "Could not parse PersistentData", e);
- return null;
+ return NONE;
}
}
diff --git a/com/android/server/locksettings/SyntheticPasswordManager.java b/com/android/server/locksettings/SyntheticPasswordManager.java
index 7a3a746e..88b2a368 100644
--- a/com/android/server/locksettings/SyntheticPasswordManager.java
+++ b/com/android/server/locksettings/SyntheticPasswordManager.java
@@ -121,6 +121,7 @@ public class SyntheticPasswordManager {
private static final byte[] PERSONALIZATION_USER_GK_AUTH = "user-gk-authentication".getBytes();
private static final byte[] PERSONALIZATION_SP_GK_AUTH = "sp-gk-authentication".getBytes();
private static final byte[] PERSONALIZATION_FBE_KEY = "fbe-key".getBytes();
+ private static final byte[] PERSONALIZATION_AUTHSECRET_KEY = "authsecret-hal".getBytes();
private static final byte[] PERSONALIZATION_SP_SPLIT = "sp-split".getBytes();
private static final byte[] PERSONALIZATION_E0 = "e0-encryption".getBytes();
private static final byte[] PERSONALISATION_WEAVER_PASSWORD = "weaver-pwd".getBytes();
@@ -159,6 +160,11 @@ public class SyntheticPasswordManager {
syntheticPassword.getBytes());
}
+ public byte[] deriveVendorAuthSecret() {
+ return SyntheticPasswordCrypto.personalisedHash(PERSONALIZATION_AUTHSECRET_KEY,
+ syntheticPassword.getBytes());
+ }
+
private void initialize(byte[] P0, byte[] P1) {
this.P1 = P1;
this.syntheticPassword = String.valueOf(HexEncoding.encode(
diff --git a/com/android/server/locksettings/recoverablekeystore/AndroidKeyStoreFactory.java b/com/android/server/locksettings/recoverablekeystore/AndroidKeyStoreFactory.java
deleted file mode 100644
index 9a4d0511..00000000
--- a/com/android/server/locksettings/recoverablekeystore/AndroidKeyStoreFactory.java
+++ /dev/null
@@ -1,34 +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.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/KeySyncTask.java b/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
index e385833b..e1e769cd 100644
--- a/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
+++ b/com/android/server/locksettings/recoverablekeystore/KeySyncTask.java
@@ -16,17 +16,18 @@
package com.android.server.locksettings.recoverablekeystore;
-import static android.security.recoverablekeystore.KeyStoreRecoveryMetadata.TYPE_LOCKSCREEN;
+import static android.security.keystore.recovery.KeyChainProtectionParams.TYPE_LOCKSCREEN;
-import android.annotation.NonNull;
+import android.annotation.Nullable;
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.security.keystore.recovery.KeyDerivationParams;
+import android.security.keystore.recovery.KeyChainProtectionParams;
+import android.security.keystore.recovery.KeyChainSnapshot;
+import android.security.keystore.recovery.WrappedApplicationKey;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
import com.android.internal.widget.LockPatternUtils;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
@@ -35,6 +36,7 @@ import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
+import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStoreException;
import java.security.MessageDigest;
@@ -69,6 +71,7 @@ public class KeySyncTask implements Runnable {
private final int mUserId;
private final int mCredentialType;
private final String mCredential;
+ private final boolean mCredentialUpdated;
private final PlatformKeyManager.Factory mPlatformKeyManagerFactory;
private final RecoverySnapshotStorage mRecoverySnapshotStorage;
private final RecoverySnapshotListenersStorage mSnapshotListenersStorage;
@@ -80,7 +83,8 @@ public class KeySyncTask implements Runnable {
RecoverySnapshotListenersStorage recoverySnapshotListenersStorage,
int userId,
int credentialType,
- String credential
+ String credential,
+ boolean credentialUpdated
) throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException {
return new KeySyncTask(
recoverableKeyStoreDb,
@@ -89,7 +93,8 @@ public class KeySyncTask implements Runnable {
userId,
credentialType,
credential,
- () -> PlatformKeyManager.getInstance(context, recoverableKeyStoreDb, userId));
+ credentialUpdated,
+ () -> PlatformKeyManager.getInstance(context, recoverableKeyStoreDb));
}
/**
@@ -97,8 +102,9 @@ public class KeySyncTask implements Runnable {
*
* @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 credentialType The type of credential as defined in {@code LockPatternUtils}
* @param credential The credential, encoded as a {@link String}.
+ * @param credentialUpdated signals weather credentials were updated.
* @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!
@@ -111,12 +117,14 @@ public class KeySyncTask implements Runnable {
int userId,
int credentialType,
String credential,
+ boolean credentialUpdated,
PlatformKeyManager.Factory platformKeyManagerFactory) {
mSnapshotListenersStorage = recoverySnapshotListenersStorage;
mRecoverableKeyStoreDb = recoverableKeyStoreDb;
mUserId = userId;
mCredentialType = credentialType;
mCredential = credential;
+ mCredentialUpdated = credentialUpdated;
mPlatformKeyManagerFactory = platformKeyManagerFactory;
mRecoverySnapshotStorage = snapshotStorage;
}
@@ -124,36 +132,51 @@ public class KeySyncTask implements Runnable {
@Override
public void run() {
try {
- syncKeys();
+ // Only one task is active If user unlocks phone many times in a short time interval.
+ synchronized(KeySyncTask.class) {
+ 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.");
+ if (mCredentialType == LockPatternUtils.CREDENTIAL_TYPE_NONE) {
+ // Application keys for the user will not be available for sync.
+ Log.w(TAG, "Credentials are not set for user " + mUserId);
return;
}
- int recoveryAgentUid = mRecoverableKeyStoreDb.getRecoveryAgentUid(mUserId);
- if (recoveryAgentUid == -1) {
+ List<Integer> recoveryAgents = mRecoverableKeyStoreDb.getRecoveryAgents(mUserId);
+ for (int uid : recoveryAgents) {
+ syncKeysForAgent(uid);
+ }
+ if (recoveryAgents.isEmpty()) {
Log.w(TAG, "No recovery agent initialized for user " + mUserId);
+ }
+ }
+
+ private void syncKeysForAgent(int recoveryAgentUid) {
+ if (!shoudCreateSnapshot(recoveryAgentUid)) {
+ Log.d(TAG, "Key sync not needed.");
return;
}
+
if (!mSnapshotListenersStorage.hasListener(recoveryAgentUid)) {
Log.w(TAG, "No pending intent registered for recovery agent " + recoveryAgentUid);
return;
}
- PublicKey publicKey = getVaultPublicKey();
+ PublicKey publicKey = mRecoverableKeyStoreDb.getRecoveryServicePublicKey(mUserId,
+ recoveryAgentUid);
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) {
+ byte[] vaultHandle = mRecoverableKeyStoreDb.getServerParams(mUserId, recoveryAgentUid);
+ if (vaultHandle == null) {
Log.w(TAG, "No device ID set for user " + mUserId);
return;
}
@@ -163,7 +186,7 @@ public class KeySyncTask implements Runnable {
Map<String, SecretKey> rawKeys;
try {
- rawKeys = getKeysToSync();
+ rawKeys = getKeysToSync(recoveryAgentUid);
} catch (GeneralSecurityException e) {
Log.e(TAG, "Failed to load recoverable keys for sync", e);
return;
@@ -196,12 +219,25 @@ public class KeySyncTask implements Runnable {
return;
}
- // TODO: where do we get counter_id from here?
+ Long counterId;
+ // counter id is generated exactly once for each credentials value.
+ if (mCredentialUpdated) {
+ counterId = generateAndStoreCounterId(recoveryAgentUid);
+ } else {
+ counterId = mRecoverableKeyStoreDb.getCounterId(mUserId, recoveryAgentUid);
+ if (counterId == null) {
+ counterId = generateAndStoreCounterId(recoveryAgentUid);
+ }
+ }
+
+ // TODO: make sure the same counter id is used during recovery and remove temporary fix.
+ counterId = 1L;
+
byte[] vaultParams = KeySyncUtils.packVaultParams(
publicKey,
- /*counterId=*/ 1,
+ counterId,
TRUSTED_HARDWARE_MAX_ATTEMPTS,
- deviceId);
+ vaultHandle);
byte[] encryptedRecoveryKey;
try {
@@ -217,49 +253,84 @@ public class KeySyncTask implements Runnable {
Log.e(TAG,"Could not encrypt with recovery key", e);
return;
}
-
- KeyStoreRecoveryMetadata metadata = new KeyStoreRecoveryMetadata(
+ // TODO: store raw data in RecoveryServiceMetadataEntry and generate Parcelables later
+ // TODO: use Builder.
+ KeyChainProtectionParams metadata = new KeyChainProtectionParams(
/*userSecretType=*/ TYPE_LOCKSCREEN,
- /*lockScreenUiFormat=*/ mCredentialType,
- /*keyDerivationParameters=*/ KeyDerivationParameters.createSHA256Parameters(salt),
+ /*lockScreenUiFormat=*/ getUiFormat(mCredentialType, mCredential),
+ /*keyDerivationParams=*/ KeyDerivationParams.createSha256Params(salt),
/*secret=*/ new byte[0]);
- ArrayList<KeyStoreRecoveryMetadata> metadataList = new ArrayList<>();
+ ArrayList<KeyChainProtectionParams> metadataList = new ArrayList<>();
metadataList.add(metadata);
- // TODO: implement snapshot version
- mRecoverySnapshotStorage.put(mUserId, new KeyStoreRecoveryData(
- /*snapshotVersion=*/ 1,
- /*recoveryMetadata=*/ metadataList,
- /*applicationKeyBlobs=*/ createApplicationKeyEntries(encryptedApplicationKeys),
- /*encryptedRecoveryKeyblob=*/ encryptedRecoveryKey));
+ int snapshotVersion = incrementSnapshotVersion(recoveryAgentUid);
+
+ // If application keys are not updated, snapshot will not be created on next unlock.
+ mRecoverableKeyStoreDb.setShouldCreateSnapshot(mUserId, recoveryAgentUid, false);
+
+ mRecoverySnapshotStorage.put(recoveryAgentUid, new KeyChainSnapshot.Builder()
+ .setSnapshotVersion(snapshotVersion)
+ .setMaxAttempts(TRUSTED_HARDWARE_MAX_ATTEMPTS)
+ .setCounterId(counterId)
+ .setTrustedHardwarePublicKey(SecureBox.encodePublicKey(publicKey))
+ .setServerParams(vaultHandle)
+ .setKeyChainProtectionParams(metadataList)
+ .setWrappedApplicationKeys(createApplicationKeyEntries(encryptedApplicationKeys))
+ .setEncryptedRecoveryKeyBlob(encryptedRecoveryKey)
+ .build());
+
mSnapshotListenersStorage.recoverySnapshotAvailable(recoveryAgentUid);
}
- private PublicKey getVaultPublicKey() {
- return mRecoverableKeyStoreDb.getRecoveryServicePublicKey(mUserId);
+ @VisibleForTesting
+ int incrementSnapshotVersion(int recoveryAgentUid) {
+ Long snapshotVersion = mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid);
+ snapshotVersion = snapshotVersion == null ? 1 : snapshotVersion + 1;
+ mRecoverableKeyStoreDb.setSnapshotVersion(mUserId, recoveryAgentUid, snapshotVersion);
+
+ return snapshotVersion.intValue();
+ }
+
+ private long generateAndStoreCounterId(int recoveryAgentUid) {
+ long counter = new SecureRandom().nextLong();
+ mRecoverableKeyStoreDb.setCounterId(mUserId, recoveryAgentUid, counter);
+ return counter;
}
/**
* Returns all of the recoverable keys for the user.
*/
- private Map<String, SecretKey> getKeysToSync()
+ private Map<String, SecretKey> getKeysToSync(int recoveryAgentUid)
throws InsecureUserException, KeyStoreException, UnrecoverableKeyException,
- NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException {
+ NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException,
+ InvalidKeyException, InvalidAlgorithmParameterException {
PlatformKeyManager platformKeyManager = mPlatformKeyManagerFactory.newInstance();
- PlatformDecryptionKey decryptKey = platformKeyManager.getDecryptKey();
+ PlatformDecryptionKey decryptKey = platformKeyManager.getDecryptKey(mUserId);
Map<String, WrappedKey> wrappedKeys = mRecoverableKeyStoreDb.getAllKeys(
- mUserId, decryptKey.getGenerationId());
+ mUserId, recoveryAgentUid, decryptKey.getGenerationId());
return WrappedKey.unwrapKeys(decryptKey, wrappedKeys);
}
/**
* Returns {@code true} if a sync is pending.
+ * @param recoveryAgentUid uid of the recovery agent.
*/
- 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();
+ private boolean shoudCreateSnapshot(int recoveryAgentUid) {
+ int[] types = mRecoverableKeyStoreDb.getRecoverySecretTypes(mUserId, recoveryAgentUid);
+ if (!ArrayUtils.contains(types, KeyChainProtectionParams.TYPE_LOCKSCREEN)) {
+ // Only lockscreen type is supported.
+ // We will need to pass extra argument to KeySyncTask to support custom pass phrase.
+ return false;
+ }
+ if (mCredentialUpdated) {
+ // Sync credential if at least one snapshot was created.
+ if (mRecoverableKeyStoreDb.getSnapshotVersion(mUserId, recoveryAgentUid) != null) {
+ mRecoverableKeyStoreDb.setShouldCreateSnapshot(mUserId, recoveryAgentUid, true);
+ return true;
+ }
+ }
+
+ return mRecoverableKeyStoreDb.getShouldCreateSnapshot(mUserId, recoveryAgentUid);
}
/**
@@ -269,14 +340,14 @@ public class KeySyncTask implements Runnable {
* @return The format - either pattern, pin, or password.
*/
@VisibleForTesting
- @KeyStoreRecoveryMetadata.LockScreenUiFormat static int getUiFormat(
+ @KeyChainProtectionParams.LockScreenUiFormat static int getUiFormat(
int credentialType, String credential) {
if (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PATTERN) {
- return KeyStoreRecoveryMetadata.TYPE_PATTERN;
+ return KeyChainProtectionParams.UI_FORMAT_PATTERN;
} else if (isPin(credential)) {
- return KeyStoreRecoveryMetadata.TYPE_PIN;
+ return KeyChainProtectionParams.UI_FORMAT_PIN;
} else {
- return KeyStoreRecoveryMetadata.TYPE_PASSWORD;
+ return KeyChainProtectionParams.UI_FORMAT_PASSWORD;
}
}
@@ -295,7 +366,10 @@ public class KeySyncTask implements Runnable {
* Returns {@code true} if {@code credential} looks like a pin.
*/
@VisibleForTesting
- static boolean isPin(@NonNull String credential) {
+ static boolean isPin(@Nullable String credential) {
+ if (credential == null) {
+ return false;
+ }
int length = credential.length();
for (int i = 0; i < length; i++) {
if (!Character.isDigit(credential.charAt(i))) {
@@ -336,13 +410,13 @@ public class KeySyncTask implements Runnable {
return keyGenerator.generateKey();
}
- private static List<KeyEntryRecoveryData> createApplicationKeyEntries(
+ private static List<WrappedApplicationKey> createApplicationKeyEntries(
Map<String, byte[]> encryptedApplicationKeys) {
- ArrayList<KeyEntryRecoveryData> keyEntries = new ArrayList<>();
+ ArrayList<WrappedApplicationKey> keyEntries = new ArrayList<>();
for (String alias : encryptedApplicationKeys.keySet()) {
keyEntries.add(
- new KeyEntryRecoveryData(
- alias.getBytes(StandardCharsets.UTF_8),
+ new WrappedApplicationKey(
+ alias,
encryptedApplicationKeys.get(alias)));
}
return keyEntries;
diff --git a/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java b/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java
index bc080be7..89e2debe 100644
--- a/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java
+++ b/com/android/server/locksettings/recoverablekeystore/KeySyncUtils.java
@@ -37,8 +37,7 @@ import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
/**
- * Utility functions for the flow where the RecoverableKeyStoreLoader syncs keys with remote
- * storage.
+ * Utility functions for the flow where the RecoveryManager syncs keys with remote storage.
*
* @hide
*/
@@ -62,7 +61,8 @@ public class KeySyncUtils {
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;
+ private static final int VAULT_PARAMS_LENGTH_BYTES = 94;
+ private static final int VAULT_HANDLE_LENGTH_BYTES = 17;
/**
* Encrypts the recovery key using both the lock screen hash and the remote storage's public
@@ -289,17 +289,18 @@ public class KeySyncUtils {
* @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.
+ * @param vaultHandle Handle of the Vault.
* @return The binary vault params, ready for sync.
*/
public static byte[] packVaultParams(
- PublicKey thmPublicKey, long counterId, int maxAttempts, long deviceId) {
+ PublicKey thmPublicKey, long counterId, int maxAttempts, byte[] vaultHandle) {
+ // TODO: Check if vaultHandle has exactly the length of VAULT_HANDLE_LENGTH_BYTES somewhere
return ByteBuffer.allocate(VAULT_PARAMS_LENGTH_BYTES)
.order(ByteOrder.LITTLE_ENDIAN)
.put(SecureBox.encodePublicKey(thmPublicKey))
.putLong(counterId)
.putInt(maxAttempts)
- .putLong(deviceId)
+ .put(vaultHandle)
.array();
}
diff --git a/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java b/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java
index 95f5cb7a..7005de5a 100644
--- a/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java
+++ b/com/android/server/locksettings/recoverablekeystore/PlatformKeyManager.java
@@ -71,7 +71,6 @@ public class PlatformKeyManager {
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";
@@ -80,33 +79,25 @@ public class PlatformKeyManager {
* 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,
+ public static PlatformKeyManager getInstance(Context context, RecoverableKeyStoreDb database)
+ throws KeyStoreException, NoSuchAlgorithmException {
+ return new PlatformKeyManager(
+ context.getApplicationContext(),
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;
@@ -115,74 +106,93 @@ public class PlatformKeyManager {
/**
* 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).
+ * screen). Returns -1 if no key has been generated yet.
+ *
+ * @param userId The ID of the user to whose lock screen the platform key must be bound.
*
* @hide
*/
- public int getGenerationId() {
- int generationId = mDatabase.getPlatformKeyGenerationId(mUserId);
- if (generationId == -1) {
- return 1;
- }
- return generationId;
+ public int getGenerationId(int userId) {
+ return mDatabase.getPlatformKeyGenerationId(userId);
}
/**
* 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.
*
+ * @param userId The ID of the user to whose lock screen the platform key must be bound.
+ *
* @hide
*/
- public boolean isAvailable() {
- return mContext.getSystemService(KeyguardManager.class).isDeviceSecure(mUserId);
+ public boolean isAvailable(int userId) {
+ return mContext.getSystemService(KeyguardManager.class).isDeviceSecure(userId);
}
/**
* Generates a new key and increments the generation ID. Should be invoked if the platform key
* is corrupted and needs to be rotated.
*
+ * @param userId The ID of the user to whose lock screen the platform key must be bound.
* @throws NoSuchAlgorithmException if AES is unavailable - should never happen.
* @throws KeyStoreException if there is an error in AndroidKeyStore.
+ * @throws InsecureUserException if the user does not have a lock screen set.
*
* @hide
*/
- public void regenerate() throws NoSuchAlgorithmException, KeyStoreException {
- int nextId = getGenerationId() + 1;
- generateAndLoadKey(nextId);
- setGenerationId(nextId);
+ public void regenerate(int userId)
+ throws NoSuchAlgorithmException, KeyStoreException, InsecureUserException {
+ if (!isAvailable(userId)) {
+ throw new InsecureUserException(String.format(
+ Locale.US, "%d does not have a lock screen set.", userId));
+ }
+
+ int generationId = getGenerationId(userId);
+ int nextId;
+ if (generationId == -1) {
+ nextId = 1;
+ } else {
+ nextId = generationId + 1;
+ }
+ generateAndLoadKey(userId, nextId);
}
/**
* Returns the platform key used for encryption.
*
+ * @param userId The ID of the user to whose lock screen the platform key must be bound.
* @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.
+ * @throws InsecureUserException if the user does not have a lock screen set.
*
* @hide
*/
- public PlatformEncryptionKey getEncryptKey()
- throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
- int generationId = getGenerationId();
+ public PlatformEncryptionKey getEncryptKey(int userId) throws KeyStoreException,
+ UnrecoverableKeyException, NoSuchAlgorithmException, InsecureUserException {
+ init(userId);
+ int generationId = getGenerationId(userId);
AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey(
- getEncryptAlias(generationId), /*password=*/ null);
+ getEncryptAlias(userId, generationId), /*password=*/ null);
return new PlatformEncryptionKey(generationId, key);
}
/**
* Returns the platform key used for decryption. Only works after a recent screen unlock.
*
+ * @param userId The ID of the user to whose lock screen the platform key must be bound.
* @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.
+ * @throws InsecureUserException if the user does not have a lock screen set.
*
* @hide
*/
- public PlatformDecryptionKey getDecryptKey()
- throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
- int generationId = getGenerationId();
+ public PlatformDecryptionKey getDecryptKey(int userId) throws KeyStoreException,
+ UnrecoverableKeyException, NoSuchAlgorithmException, InsecureUserException {
+ init(userId);
+ int generationId = getGenerationId(userId);
AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey(
- getDecryptAlias(generationId), /*password=*/ null);
+ getDecryptAlias(userId, generationId), /*password=*/ null);
return new PlatformDecryptionKey(generationId, key);
}
@@ -190,31 +200,36 @@ public class PlatformKeyManager {
* 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.
*
+ * @param userId The ID of the user to whose lock screen the platform key must be bound.
* @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()) {
+ void init(int userId)
+ throws KeyStoreException, NoSuchAlgorithmException, InsecureUserException {
+ if (!isAvailable(userId)) {
throw new InsecureUserException(String.format(
- Locale.US, "%d does not have a lock screen set.", mUserId));
+ Locale.US, "%d does not have a lock screen set.", userId));
}
- int generationId = getGenerationId();
- if (isKeyLoaded(generationId)) {
+ int generationId = getGenerationId(userId);
+ if (isKeyLoaded(userId, generationId)) {
Log.i(TAG, String.format(
Locale.US, "Platform key generation %d exists already.", generationId));
return;
}
- if (generationId == 1) {
+ if (generationId == -1) {
Log.i(TAG, "Generating initial platform ID.");
+ generationId = 1;
} else {
Log.w(TAG, String.format(Locale.US, "Platform generation ID was %d but no "
+ "entry was present in AndroidKeyStore. Generating fresh key.", generationId));
+ // Had to generate a fresh key, bump the generation id
+ generationId++;
}
- generateAndLoadKey(generationId);
+ generateAndLoadKey(userId, generationId);
}
/**
@@ -224,11 +239,12 @@ public class PlatformKeyManager {
* <p>These IDs look as follows:
* {@code com.security.recoverablekeystore/platform/<user id>/<generation id>/encrypt}
*
+ * @param userId The ID of the user to whose lock screen the platform key must be bound.
* @param generationId The generation ID.
* @return The alias.
*/
- private String getEncryptAlias(int generationId) {
- return KEY_ALIAS_PREFIX + mUserId + "/" + generationId + "/" + ENCRYPT_KEY_ALIAS_SUFFIX;
+ private String getEncryptAlias(int userId, int generationId) {
+ return KEY_ALIAS_PREFIX + userId + "/" + generationId + "/" + ENCRYPT_KEY_ALIAS_SUFFIX;
}
/**
@@ -238,18 +254,19 @@ public class PlatformKeyManager {
* <p>These IDs look as follows:
* {@code com.security.recoverablekeystore/platform/<user id>/<generation id>/decrypt}
*
+ * @param userId The ID of the user to whose lock screen the platform key must be bound.
* @param generationId The generation ID.
* @return The alias.
*/
- private String getDecryptAlias(int generationId) {
- return KEY_ALIAS_PREFIX + mUserId + "/" + generationId + "/" + DECRYPT_KEY_ALIAS_SUFFIX;
+ private String getDecryptAlias(int userId, int generationId) {
+ return KEY_ALIAS_PREFIX + userId + "/" + generationId + "/" + DECRYPT_KEY_ALIAS_SUFFIX;
}
/**
* Sets the current generation ID to {@code generationId}.
*/
- private void setGenerationId(int generationId) {
- mDatabase.setPlatformKeyGenerationId(mUserId, generationId);
+ private void setGenerationId(int userId, int generationId) {
+ mDatabase.setPlatformKeyGenerationId(userId, generationId);
}
/**
@@ -258,9 +275,9 @@ public class PlatformKeyManager {
*
* @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));
+ private boolean isKeyLoaded(int userId, int generationId) throws KeyStoreException {
+ return mKeyStore.containsAlias(getEncryptAlias(userId, generationId))
+ && mKeyStore.containsAlias(getDecryptAlias(userId, generationId));
}
/**
@@ -271,10 +288,10 @@ public class PlatformKeyManager {
* available since API version 1.
* @throws KeyStoreException if there was an issue loading the keys into AndroidKeyStore.
*/
- private void generateAndLoadKey(int generationId)
+ private void generateAndLoadKey(int userId, int generationId)
throws NoSuchAlgorithmException, KeyStoreException {
- String encryptAlias = getEncryptAlias(generationId);
- String decryptAlias = getDecryptAlias(generationId);
+ String encryptAlias = getEncryptAlias(userId, generationId);
+ String decryptAlias = getDecryptAlias(userId, generationId);
SecretKey secretKey = generateAesKey();
mKeyStore.setEntry(
@@ -293,9 +310,11 @@ public class PlatformKeyManager {
USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
- .setBoundToSpecificSecureUserId(mUserId)
+ .setBoundToSpecificSecureUserId(userId)
.build());
+ setGenerationId(userId, generationId);
+
try {
secretKey.destroy();
} catch (DestroyFailedException e) {
diff --git a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
index 8c23d9b4..2fe3f4e9 100644
--- a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
+++ b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyGenerator.java
@@ -99,6 +99,7 @@ public class RecoverableKeyGenerator {
Locale.US, "Failed writing (%d, %s) to database.", uid, alias));
}
+ mDatabase.setShouldCreateSnapshot(userId, uid, true);
return key.getEncoded();
}
}
diff --git a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
index fe1cad4b..b6c3c669 100644
--- a/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
+++ b/com/android/server/locksettings/recoverablekeystore/RecoverableKeyStoreManager.java
@@ -16,19 +16,27 @@
package com.android.server.locksettings.recoverablekeystore;
+import static android.security.keystore.RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT;
+import static android.security.keystore.RecoveryController.ERROR_DECRYPTION_FAILED;
+import static android.security.keystore.RecoveryController.ERROR_INSECURE_USER;
+import static android.security.keystore.RecoveryController.ERROR_NO_SNAPSHOT_PENDING;
+import static android.security.keystore.RecoveryController.ERROR_SERVICE_INTERNAL_ERROR;
+import static android.security.keystore.RecoveryController.ERROR_SESSION_EXPIRED;
+
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PendingIntent;
import android.content.Context;
+import android.Manifest;
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.security.keystore.recovery.KeyChainProtectionParams;
+import android.security.keystore.recovery.KeyChainSnapshot;
+import android.security.keystore.recovery.RecoveryController;
+import android.security.keystore.recovery.WrappedApplicationKey;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
@@ -36,7 +44,6 @@ import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKe
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;
@@ -45,6 +52,7 @@ import java.security.PublicKey;
import java.security.UnrecoverableKeyException;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
@@ -55,7 +63,7 @@ import java.util.concurrent.Executors;
import javax.crypto.AEADBadTagException;
/**
- * Class with {@link RecoverableKeyStoreLoader} API implementation and internal methods to interact
+ * Class with {@link RecoveryController} API implementation and internal methods to interact
* with {@code LockSettingsService}.
*
* @hide
@@ -63,10 +71,6 @@ import javax.crypto.AEADBadTagException;
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;
@@ -76,22 +80,34 @@ public class RecoverableKeyStoreManager {
private final RecoverySnapshotListenersStorage mListenersStorage;
private final RecoverableKeyGenerator mRecoverableKeyGenerator;
private final RecoverySnapshotStorage mSnapshotStorage;
+ private final PlatformKeyManager mPlatformKeyManager;
/**
* Returns a new or existing instance.
*
* @hide
*/
- public static synchronized RecoverableKeyStoreManager getInstance(Context mContext) {
+ public static synchronized RecoverableKeyStoreManager getInstance(Context context) {
if (mInstance == null) {
- RecoverableKeyStoreDb db = RecoverableKeyStoreDb.newInstance(mContext);
+ RecoverableKeyStoreDb db = RecoverableKeyStoreDb.newInstance(context);
+ PlatformKeyManager platformKeyManager;
+ try {
+ platformKeyManager = PlatformKeyManager.getInstance(context, db);
+ } catch (NoSuchAlgorithmException e) {
+ // Impossible: all algorithms must be supported by AOSP
+ throw new RuntimeException(e);
+ } catch (KeyStoreException e) {
+ throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
+ }
+
mInstance = new RecoverableKeyStoreManager(
- mContext.getApplicationContext(),
+ context.getApplicationContext(),
db,
new RecoverySessionStorage(),
Executors.newSingleThreadExecutor(),
new RecoverySnapshotStorage(),
- new RecoverySnapshotListenersStorage());
+ new RecoverySnapshotListenersStorage(),
+ platformKeyManager);
}
return mInstance;
}
@@ -103,25 +119,30 @@ public class RecoverableKeyStoreManager {
RecoverySessionStorage recoverySessionStorage,
ExecutorService executorService,
RecoverySnapshotStorage snapshotStorage,
- RecoverySnapshotListenersStorage listenersStorage) {
+ RecoverySnapshotListenersStorage listenersStorage,
+ PlatformKeyManager platformKeyManager) {
mContext = context;
mDatabase = recoverableKeyStoreDb;
mRecoverySessionStorage = recoverySessionStorage;
mExecutorService = executorService;
mListenersStorage = listenersStorage;
mSnapshotStorage = snapshotStorage;
+ mPlatformKeyManager = platformKeyManager;
+
try {
mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mDatabase);
} catch (NoSuchAlgorithmException e) {
- // Impossible: all AOSP implementations must support AES.
- throw new RuntimeException(e);
+ Log.wtf(TAG, "AES keygen algorithm not available. AOSP must support this.", e);
+ throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
}
}
public void initRecoveryService(
- @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList, int userId)
+ @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList)
throws RemoteException {
checkRecoverKeyStorePermission();
+ int userId = UserHandle.getCallingUserId();
+ int uid = Binder.getCallingUid();
// TODO: open /system/etc/security/... cert file, and check the signature on the public keys
PublicKey publicKey;
try {
@@ -130,12 +151,16 @@ public class RecoverableKeyStoreManager {
X509EncodedKeySpec pkSpec = new X509EncodedKeySpec(signedPublicKeyList);
publicKey = kf.generatePublic(pkSpec);
} catch (NoSuchAlgorithmException e) {
- // Should never happen
- throw new RuntimeException(e);
+ Log.wtf(TAG, "EC algorithm not available. AOSP must support this.", e);
+ throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
} catch (InvalidKeySpecException e) {
- throw new RemoteException("Invalid public key for the recovery service");
+ throw new ServiceSpecificException(
+ ERROR_BAD_CERTIFICATE_FORMAT, "Not a valid X509 certificate.");
+ }
+ long updatedRows = mDatabase.setRecoveryServicePublicKey(userId, uid, publicKey);
+ if (updatedRows > 0) {
+ mDatabase.setShouldCreateSnapshot(userId, uid, true);
}
- mDatabase.setRecoveryServicePublicKey(userId, Binder.getCallingUid(), publicKey);
}
/**
@@ -144,22 +169,23 @@ public class RecoverableKeyStoreManager {
* @return recovery data
* @hide
*/
- public @NonNull KeyStoreRecoveryData getRecoveryData(@NonNull byte[] account, int userId)
+ public @NonNull
+ KeyChainSnapshot getRecoveryData(@NonNull byte[] account)
throws RemoteException {
checkRecoverKeyStorePermission();
-
- KeyStoreRecoveryData snapshot = mSnapshotStorage.get(UserHandle.getCallingUserId());
+ int uid = Binder.getCallingUid();
+ KeyChainSnapshot snapshot = mSnapshotStorage.get(uid);
if (snapshot == null) {
- throw new ServiceSpecificException(RecoverableKeyStoreLoader.NO_SNAPSHOT_PENDING_ERROR);
+ throw new ServiceSpecificException(ERROR_NO_SNAPSHOT_PENDING);
}
return snapshot;
}
- public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent, int userId)
+ public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
throws RemoteException {
checkRecoverKeyStorePermission();
- final int recoveryAgentUid = Binder.getCallingUid();
- mListenersStorage.setSnapshotListener(recoveryAgentUid, intent);
+ int uid = Binder.getCallingUid();
+ mListenersStorage.setSnapshotListener(uid, intent);
}
/**
@@ -168,15 +194,20 @@ public class RecoverableKeyStoreManager {
*
* @return Map from Recovery agent account to snapshot version.
*/
- public @NonNull Map<byte[], Integer> getRecoverySnapshotVersions(int userId)
+ public @NonNull Map<byte[], Integer> getRecoverySnapshotVersions()
throws RemoteException {
checkRecoverKeyStorePermission();
throw new UnsupportedOperationException();
}
- public void setServerParameters(long serverParameters, int userId) throws RemoteException {
+ public void setServerParams(byte[] serverParams) throws RemoteException {
checkRecoverKeyStorePermission();
- mDatabase.setServerParameters(userId, Binder.getCallingUid(), serverParameters);
+ int userId = UserHandle.getCallingUserId();
+ int uid = Binder.getCallingUid();
+ long updatedRows = mDatabase.setServerParams(userId, uid, serverParams);
+ if (updatedRows > 0) {
+ mDatabase.setShouldCreateSnapshot(userId, uid, true);
+ }
}
/**
@@ -187,7 +218,7 @@ public class RecoverableKeyStoreManager {
* @param status - new status
*/
public void setRecoveryStatus(
- @NonNull String packageName, @Nullable String[] aliases, int status, int userId)
+ @NonNull String packageName, @Nullable String[] aliases, int status)
throws RemoteException {
checkRecoverKeyStorePermission();
int uid = Binder.getCallingUid();
@@ -211,12 +242,11 @@ public class RecoverableKeyStoreManager {
*
* @return {@code Map} from KeyStore alias to recovery status.
*/
- public @NonNull Map<String, Integer> getRecoveryStatus(@Nullable String packageName, int userId)
+ public @NonNull Map<String, Integer> getRecoveryStatus(@Nullable String packageName)
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());
}
@@ -226,11 +256,15 @@ public class RecoverableKeyStoreManager {
* @hide
*/
public void setRecoverySecretTypes(
- @NonNull @KeyStoreRecoveryMetadata.UserSecretType int[] secretTypes, int userId)
+ @NonNull @KeyChainProtectionParams.UserSecretType int[] secretTypes)
throws RemoteException {
checkRecoverKeyStorePermission();
- mDatabase.setRecoverySecretTypes(UserHandle.getCallingUserId(), Binder.getCallingUid(),
- secretTypes);
+ int userId = UserHandle.getCallingUserId();
+ int uid = Binder.getCallingUid();
+ long updatedRows = mDatabase.setRecoverySecretTypes(userId, uid, secretTypes);
+ if (updatedRows > 0) {
+ mDatabase.setShouldCreateSnapshot(userId, uid, true);
+ }
}
/**
@@ -239,29 +273,29 @@ public class RecoverableKeyStoreManager {
* @return secret types
* @hide
*/
- public @NonNull int[] getRecoverySecretTypes(int userId) throws RemoteException {
+ public @NonNull int[] getRecoverySecretTypes() throws RemoteException {
checkRecoverKeyStorePermission();
return mDatabase.getRecoverySecretTypes(UserHandle.getCallingUserId(),
Binder.getCallingUid());
}
/**
- * Gets secret types RecoverableKeyStoreLoaders is waiting for to create new Recovery Data.
+ * Gets secret types RecoveryManagers is waiting for to create new Recovery Data.
*
* @return secret types
* @hide
*/
- public @NonNull int[] getPendingRecoverySecretTypes(int userId) throws RemoteException {
+ public @NonNull int[] getPendingRecoverySecretTypes() 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) {
+ @NonNull KeyChainProtectionParams recoverySecret) throws RemoteException {
+ int uid = Binder.getCallingUid();
+ if (recoverySecret.getLockScreenUiFormat() == KeyChainProtectionParams.TYPE_LOCKSCREEN) {
throw new SecurityException(
- "Caller " + callingUid + "is not allowed to set lock screen secret");
+ "Caller " + uid + " is not allowed to set lock screen secret");
}
checkRecoverKeyStorePermission();
// TODO: add hook from LockSettingsService to set lock screen secret.
@@ -285,25 +319,41 @@ public class RecoverableKeyStoreManager {
@NonNull byte[] verifierPublicKey,
@NonNull byte[] vaultParams,
@NonNull byte[] vaultChallenge,
- @NonNull List<KeyStoreRecoveryMetadata> secrets,
- int userId)
+ @NonNull List<KeyChainProtectionParams> secrets)
throws RemoteException {
checkRecoverKeyStorePermission();
+ int uid = Binder.getCallingUid();
if (secrets.size() != 1) {
- // TODO: support multiple secrets
- throw new RemoteException("Only a single KeyStoreRecoveryMetadata is supported");
+ throw new UnsupportedOperationException(
+ "Only a single KeyChainProtectionParams is supported");
+ }
+
+ PublicKey publicKey;
+ try {
+ publicKey = KeySyncUtils.deserializePublicKey(verifierPublicKey);
+ } catch (NoSuchAlgorithmException e) {
+ // Should never happen
+ throw new RuntimeException(e);
+ } catch (InvalidKeySpecException e) {
+ throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, "Not a valid X509 key");
+ }
+ // The raw public key bytes contained in vaultParams must match the ones given in
+ // verifierPublicKey; otherwise, the user secret may be decrypted by a key that is not owned
+ // by the original recovery service.
+ if (!publicKeysMatch(publicKey, vaultParams)) {
+ throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT,
+ "The public keys given in verifierPublicKey and vaultParams do not match.");
}
byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
byte[] kfHash = secrets.get(0).getSecret();
mRecoverySessionStorage.add(
- userId,
+ uid,
new RecoverySessionStorage.Entry(sessionId, kfHash, keyClaimant, vaultParams));
try {
byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(kfHash);
- PublicKey publicKey = KeySyncUtils.deserializePublicKey(verifierPublicKey);
return KeySyncUtils.encryptRecoveryClaim(
publicKey,
vaultParams,
@@ -311,18 +361,10 @@ public class RecoverableKeyStoreManager {
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);
+ Log.wtf(TAG, "SecureBox algorithm missing. AOSP must support this.", e);
+ throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
+ } catch (InvalidKeyException e) {
+ throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage());
}
}
@@ -331,27 +373,26 @@ public class RecoverableKeyStoreManager {
* service.
*
* @param sessionId The session ID used to generate the claim. See
- * {@link #startRecoverySession(String, byte[], byte[], byte[], List, int)}.
+ * {@link #startRecoverySession(String, byte[], byte[], byte[], List)}.
* @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)
+ @NonNull List<WrappedApplicationKey> applicationKeys)
throws RemoteException {
checkRecoverKeyStorePermission();
-
+ int uid = Binder.getCallingUid();
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));
+ throw new ServiceSpecificException(ERROR_SESSION_EXPIRED,
+ String.format(Locale.US,
+ "Application uid=%d does not have pending session '%s'", uid, sessionId));
}
try {
@@ -373,35 +414,47 @@ public class RecoverableKeyStoreManager {
*/
public byte[] generateAndStoreKey(@NonNull String alias) throws RemoteException {
int uid = Binder.getCallingUid();
- int userId = Binder.getCallingUserHandle().getIdentifier();
+ int userId = UserHandle.getCallingUserId();
PlatformEncryptionKey encryptionKey;
-
try {
- PlatformKeyManager platformKeyManager = PlatformKeyManager.getInstance(
- mContext, mDatabase, userId);
- encryptionKey = platformKeyManager.getEncryptKey();
+ encryptionKey = mPlatformKeyManager.getEncryptKey(userId);
} 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());
+ throw new ServiceSpecificException(ERROR_SERVICE_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());
+ } catch (KeyStoreException | InvalidKeyException | RecoverableKeyStorageException e) {
+ throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
+ }
+ }
+
+ /**
+ * Destroys the session with the given {@code sessionId}.
+ */
+ public void closeSession(@NonNull String sessionId) throws RemoteException {
+ mRecoverySessionStorage.remove(Binder.getCallingUid(), sessionId);
+ }
+
+ public void removeKey(@NonNull String alias) throws RemoteException {
+ int uid = Binder.getCallingUid();
+ int userId = UserHandle.getCallingUserId();
+
+ boolean wasRemoved = mDatabase.removeKey(uid, alias);
+ if (wasRemoved) {
+ mDatabase.setShouldCreateSnapshot(userId, uid, true);
}
}
private byte[] decryptRecoveryKey(
RecoverySessionStorage.Entry sessionEntry, byte[] encryptedClaimResponse)
- throws RemoteException {
+ throws RemoteException, ServiceSpecificException {
try {
byte[] locallyEncryptedKey = KeySyncUtils.decryptRecoveryClaimResponse(
sessionEntry.getKeyClaimant(),
@@ -409,18 +462,12 @@ public class RecoverableKeyStoreManager {
encryptedClaimResponse);
return KeySyncUtils.decryptRecoveryKey(sessionEntry.getLskfHash(), locallyEncryptedKey);
} catch (InvalidKeyException | AEADBadTagException e) {
- throw new RemoteException(
- "Failed to decrypt recovery key",
- e,
- /*enableSuppression=*/ true,
- /*writeableStackTrace=*/ true);
+ throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
+ "Failed to decrypt recovery key " + e.getMessage());
+
} 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);
+ throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
}
}
@@ -432,10 +479,10 @@ public class RecoverableKeyStoreManager {
*/
private Map<String, byte[]> recoverApplicationKeys(
@NonNull byte[] recoveryKey,
- @NonNull List<KeyEntryRecoveryData> applicationKeys) throws RemoteException {
+ @NonNull List<WrappedApplicationKey> applicationKeys) throws RemoteException {
HashMap<String, byte[]> keyMaterialByAlias = new HashMap<>();
- for (KeyEntryRecoveryData applicationKey : applicationKeys) {
- String alias = new String(applicationKey.getAlias(), StandardCharsets.UTF_8);
+ for (WrappedApplicationKey applicationKey : applicationKeys) {
+ String alias = applicationKey.getAlias();
byte[] encryptedKeyMaterial = applicationKey.getEncryptedKeyMaterial();
try {
@@ -443,18 +490,12 @@ public class RecoverableKeyStoreManager {
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);
+ Log.wtf(TAG, "Missing SecureBox algorithm. AOSP required to support this.", e);
+ throw new ServiceSpecificException(
+ ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
} catch (InvalidKeyException | AEADBadTagException e) {
- throw new RemoteException(
- "Failed to recover key with alias '" + alias + "'",
- e,
- /*enableSuppression=*/ true,
- /*writeableStackTrace=*/ true);
+ throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
+ "Failed to recover key with alias '" + alias + "': " + e.getMessage());
}
}
return keyMaterialByAlias;
@@ -480,7 +521,8 @@ public class RecoverableKeyStoreManager {
mListenersStorage,
userId,
storedHashType,
- credential));
+ credential,
+ /*credentialUpdated=*/ false));
} catch (NoSuchAlgorithmException e) {
Log.wtf(TAG, "Should never happen - algorithm unavailable for KeySync", e);
} catch (KeyStoreException e) {
@@ -490,17 +532,45 @@ public class RecoverableKeyStoreManager {
}
}
- /** This function can only be used inside LockSettingsService. */
+ /**
+ * This function can only be used inside LockSettingsService.
+ * @param storedHashType from {@code CredentialHash}
+ * @param credential - unencrypted String
+ * @param userId for the user whose lock screen credentials were changed.
+ * @hide
+ */
public void lockScreenSecretChanged(
- @KeyStoreRecoveryMetadata.LockScreenUiFormat int type,
+ int storedHashType,
@Nullable String credential,
int userId) {
- throw new UnsupportedOperationException();
+ // 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,
+ /*credentialUpdated=*/ true));
+ } 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);
+ }
}
private void checkRecoverKeyStorePermission() {
mContext.enforceCallingOrSelfPermission(
- RecoverableKeyStoreLoader.PERMISSION_RECOVER_KEYSTORE,
+ Manifest.permission.RECOVER_KEYSTORE,
"Caller " + Binder.getCallingUid() + " doesn't have RecoverKeyStore permission.");
}
+
+ private boolean publicKeysMatch(PublicKey publicKey, byte[] vaultParams) {
+ byte[] encodedPublicKey = SecureBox.encodePublicKey(publicKey);
+ return Arrays.equals(encodedPublicKey, Arrays.copyOf(vaultParams, encodedPublicKey.length));
+ }
}
diff --git a/com/android/server/locksettings/recoverablekeystore/WrappedKey.java b/com/android/server/locksettings/recoverablekeystore/WrappedKey.java
index 54aa9f08..d85e89e0 100644
--- a/com/android/server/locksettings/recoverablekeystore/WrappedKey.java
+++ b/com/android/server/locksettings/recoverablekeystore/WrappedKey.java
@@ -16,8 +16,8 @@
package com.android.server.locksettings.recoverablekeystore;
+import android.security.keystore.RecoveryController;
import android.util.Log;
-import android.security.recoverablekeystore.RecoverableKeyStoreLoader;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
@@ -97,7 +97,7 @@ public class WrappedKey {
/*nonce=*/ cipher.getIV(),
/*keyMaterial=*/ encryptedKeyMaterial,
/*platformKeyGenerationId=*/ wrappingKey.getGenerationId(),
- RecoverableKeyStoreLoader.RECOVERY_STATUS_SYNC_IN_PROGRESS);
+ RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS);
}
/**
@@ -107,14 +107,14 @@ public class WrappedKey {
* @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
+ * @see RecoveryController.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;
+ mRecoveryStatus = RecoveryController.RECOVERY_STATUS_SYNC_IN_PROGRESS;
}
/**
@@ -184,7 +184,8 @@ public class WrappedKey {
public static Map<String, SecretKey> unwrapKeys(
PlatformDecryptionKey platformKey,
Map<String, WrappedKey> wrappedKeys)
- throws NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException {
+ throws NoSuchAlgorithmException, NoSuchPaddingException, BadPlatformKeyException,
+ InvalidKeyException, InvalidAlgorithmParameterException {
HashMap<String, SecretKey> unwrappedKeys = new HashMap<>();
Cipher cipher = Cipher.getInstance(KEY_WRAP_CIPHER_ALGORITHM);
int platformKeyGenerationId = platformKey.getGenerationId();
@@ -201,20 +202,10 @@ public class WrappedKey {
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;
- }
+ cipher.init(
+ Cipher.UNWRAP_MODE,
+ platformKey.getKey(),
+ new GCMParameterSpec(GCM_TAG_LENGTH_BITS, wrappedKey.getNonce()));
SecretKey key;
try {
key = (SecretKey) cipher.unwrap(
diff --git a/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
index 838311e1..f2e71b37 100644
--- a/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
+++ b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDb.java
@@ -35,8 +35,10 @@ import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.X509EncodedKeySpec;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.StringJoiner;
@@ -147,6 +149,19 @@ public class RecoverableKeyStoreDb {
}
/**
+ * Removes key with {@code alias} for app with {@code uid}.
+ *
+ * @return {@code true} if deleted a row.
+ */
+ public boolean removeKey(int uid, String alias) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+ String selection = KeysEntry.COLUMN_NAME_UID + " = ? AND " +
+ KeysEntry.COLUMN_NAME_ALIAS + " = ?";
+ String[] selectionArgs = { Integer.toString(uid), alias };
+ return db.delete(KeysEntry.TABLE_NAME, selection, selectionArgs) > 0;
+ }
+
+ /**
* Returns all statuses for keys {@code uid} and {@code platformKeyGenerationId}.
*
* @param uid of the application
@@ -207,16 +222,19 @@ public class RecoverableKeyStoreDb {
}
/**
- * Returns all keys for the given {@code userId} and {@code platformKeyGenerationId}.
+ * Returns all keys for the given {@code userId} {@code recoveryAgentUid}
+ * and {@code platformKeyGenerationId}.
*
* @param userId User id of the profile to which all the keys are associated.
+ * @param recoveryAgentUid Uid of the recovery agent which will perform the sync
* @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) {
+ public Map<String, WrappedKey> getAllKeys(int userId, int recoveryAgentUid,
+ int platformKeyGenerationId) {
SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
String[] projection = {
KeysEntry._ID,
@@ -226,9 +244,13 @@ public class RecoverableKeyStoreDb {
KeysEntry.COLUMN_NAME_RECOVERY_STATUS};
String selection =
KeysEntry.COLUMN_NAME_USER_ID + " = ? AND "
+ + KeysEntry.COLUMN_NAME_UID + " = ? AND "
+ KeysEntry.COLUMN_NAME_GENERATION_ID + " = ?";
String[] selectionArguments = {
- Integer.toString(userId), Integer.toString(platformKeyGenerationId) };
+ Integer.toString(userId),
+ Integer.toString(recoveryAgentUid),
+ Integer.toString(platformKeyGenerationId)
+ };
try (
Cursor cursor = db.query(
@@ -314,23 +336,17 @@ public class RecoverableKeyStoreDb {
* @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);
+ return setBytes(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY,
+ publicKey.getEncoded());
}
/**
- * Returns the uid of the recovery agent for the given user, or -1 if none is set.
+ * Returns the list of recovery agents initialized for given {@code userId}
+ * @param userId The userId of the profile the application is running under.
+ * @return The list of recovery agents
+ * @hide
*/
- public int getRecoveryAgentUid(int userId) {
+ public @NonNull List<Integer> getRecoveryAgents(int userId) {
SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
String[] projection = { RecoveryServiceMetadataEntry.COLUMN_NAME_UID };
@@ -348,84 +364,49 @@ public class RecoverableKeyStoreDb {
/*orderBy=*/ null)
) {
int count = cursor.getCount();
- if (count == 0) {
- return -1;
+ ArrayList<Integer> result = new ArrayList<>(count);
+ while (cursor.moveToNext()) {
+ int uid = cursor.getInt(
+ cursor.getColumnIndexOrThrow(RecoveryServiceMetadataEntry.COLUMN_NAME_UID));
+ result.add(uid);
}
- cursor.moveToFirst();
- return cursor.getInt(
- cursor.getColumnIndexOrThrow(RecoveryServiceMetadataEntry.COLUMN_NAME_UID));
+ return result;
}
}
/**
* Returns the public key of the recovery service.
*
- * @param userId The uid of the profile the application is running under.
+ * @param userId The userId 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;
- }
+ byte[] keyBytes =
+ getBytes(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY);
+ if (keyBytes == null) {
+ return null;
+ }
+ 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}
+ * See {@code KeyChainProtectionParams}
*
- * @param userId The uid of the profile the application is running under.
+ * @param userId The userId 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.
@@ -450,7 +431,7 @@ public class RecoverableKeyStoreDb {
/**
* 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 userId The userId 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.
*
@@ -516,7 +497,7 @@ public class RecoverableKeyStoreDb {
/**
* Returns the first (and only?) public key for {@code userId}.
*
- * @param userId The uid of the profile whose keys are to be synced.
+ * @param userId The userId of the profile whose keys are to be synced.
* @return The public key, or null if none exists.
*/
@Nullable
@@ -556,20 +537,198 @@ public class RecoverableKeyStoreDb {
}
/**
+ * Updates the counterId
+ *
+ * @param userId The userId of the profile the application is running under.
+ * @param uid The uid of the application.
+ * @param counterId The counterId.
+ * @return The primary key of the inserted row, or -1 if failed.
+ *
+ * @hide
+ */
+ public long setCounterId(int userId, int uid, long counterId) {
+ return setLong(userId, uid,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_COUNTER_ID, counterId);
+ }
+
+ /**
+ * Returns the counter id.
+ *
+ * @param userId The userId of the profile the application is running under.
+ * @param uid The uid of the application who initialized the local recovery components.
+ * @return The counter id
+ *
+ * @hide
+ */
+ @Nullable
+ public Long getCounterId(int userId, int uid) {
+ return getLong(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_COUNTER_ID);
+ }
+
+
+ /**
* 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 userId The userId of the profile the application is running under.
+ * @param uid The uid of the application.
+ * @param serverParams The server parameters.
+ * @return The primary key of the inserted row, or -1 if failed.
+ *
+ * @hide
+ */
+ public long setServerParams(int userId, int uid, byte[] serverParams) {
+ return setBytes(userId, uid,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMS, serverParams);
+ }
+
+ /**
+ * Returns the server paramters that was previously set by the application who initialized the
+ * local recovery service components.
+ *
+ * @param userId The userId 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
+ */
+ @Nullable
+ public byte[] getServerParams(int userId, int uid) {
+ return getBytes(userId, uid, RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMS);
+ }
+
+ /**
+ * Updates the snapshot version.
+ *
+ * @param userId The userId of the profile the application is running under.
* @param uid The uid of the application.
- * @param serverParameters The server parameters.
+ * @param snapshotVersion The snapshot version
* @return The primary key of the inserted row, or -1 if failed.
*
* @hide
*/
- public long setServerParameters(int userId, int uid, long serverParameters) {
+ public long setSnapshotVersion(int userId, int uid, long snapshotVersion) {
+ return setLong(userId, uid,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_SNAPSHOT_VERSION, snapshotVersion);
+ }
+
+ /**
+ * Returns the snapshot version
+ *
+ * @param userId The userId 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
+ */
+ @Nullable
+ public Long getSnapshotVersion(int userId, int uid) {
+ return getLong(userId, uid,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_SNAPSHOT_VERSION);
+ }
+
+ /**
+ * Updates the snapshot version.
+ *
+ * @param userId The userId of the profile the application is running under.
+ * @param uid The uid of the application.
+ * @param pending The server parameters.
+ * @return The primary key of the inserted row, or -1 if failed.
+ *
+ * @hide
+ */
+ public long setShouldCreateSnapshot(int userId, int uid, boolean pending) {
+ return setLong(userId, uid,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_SHOULD_CREATE_SNAPSHOT, pending ? 1 : 0);
+ }
+
+ /**
+ * Returns {@code true} if new snapshot should be created.
+ * Returns {@code false} if the flag was never set.
+ *
+ * @param userId The userId of the profile the application is running under.
+ * @param uid The uid of the application who initialized the local recovery components.
+ * @return snapshot outdated flag.
+ *
+ * @hide
+ */
+ public boolean getShouldCreateSnapshot(int userId, int uid) {
+ Long res = getLong(userId, uid,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_SHOULD_CREATE_SNAPSHOT);
+ return res != null && res != 0L;
+ }
+
+
+ /**
+ * Returns given long value from the database.
+ *
+ * @param userId The userId of the profile the application is running under.
+ * @param uid The uid of the application who initialized the local recovery components.
+ * @param key from {@code RecoveryServiceMetadataEntry}
+ * @return The value that were previously set, or null if there's none.
+ *
+ * @hide
+ */
+ private Long getLong(int userId, int uid, String key) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
+
+ String[] projection = {
+ RecoveryServiceMetadataEntry._ID,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID,
+ RecoveryServiceMetadataEntry.COLUMN_NAME_UID,
+ 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 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(key);
+ if (cursor.isNull(idx)) {
+ return null;
+ } else {
+ return cursor.getLong(idx);
+ }
+ }
+ }
+
+ /**
+ * Sets a long value in the database.
+ *
+ * @param userId The userId of the profile the application is running under.
+ * @param uid The uid of the application who initialized the local recovery components.
+ * @param key defined in {@code RecoveryServiceMetadataEntry}
+ * @param value new value.
+ * @return The primary key of the inserted row, or -1 if failed.
+ *
+ * @hide
+ */
+
+ private long setLong(int userId, int uid, String key, long value) {
SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
- values.put(RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMETERS, serverParameters);
+ values.put(key, value);
String selection =
RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + " = ? AND "
+ RecoveryServiceMetadataEntry.COLUMN_NAME_UID + " = ?";
@@ -581,37 +740,37 @@ public class RecoverableKeyStoreDb {
}
/**
- * Returns the server paramters that was previously set by the application who initialized the
- * local recovery service components.
+ * Returns given binary value from the database.
*
- * @param userId The uid of the profile the application is running under.
+ * @param userId The userId 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.
+ * @param key from {@code RecoveryServiceMetadataEntry}
+ * @return The value that were previously set, or null if there's none.
*
* @hide
*/
- public Long getServerParameters(int userId, int uid) {
+ private byte[] getBytes(int userId, int uid, String key) {
SQLiteDatabase db = mKeyStoreDbHelper.getReadableDatabase();
String[] projection = {
RecoveryServiceMetadataEntry._ID,
RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID,
RecoveryServiceMetadataEntry.COLUMN_NAME_UID,
- RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMETERS};
+ 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)
+ Cursor cursor = db.query(
+ RecoveryServiceMetadataEntry.TABLE_NAME,
+ projection,
+ selection,
+ selectionArguments,
+ /*groupBy=*/ null,
+ /*having=*/ null,
+ /*orderBy=*/ null)
) {
int count = cursor.getCount();
if (count == 0) {
@@ -620,22 +779,47 @@ public class RecoverableKeyStoreDb {
if (count > 1) {
Log.wtf(TAG,
String.format(Locale.US,
- "%d deviceId entries found for userId=%d uid=%d. "
+ "%d 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);
+ int idx = cursor.getColumnIndexOrThrow(key);
if (cursor.isNull(idx)) {
return null;
} else {
- return cursor.getLong(idx);
+ return cursor.getBlob(idx);
}
}
}
/**
+ * Sets a binary value in the database.
+ *
+ * @param userId The userId of the profile the application is running under.
+ * @param uid The uid of the application who initialized the local recovery components.
+ * @param key defined in {@code RecoveryServiceMetadataEntry}
+ * @param value new value.
+ * @return The primary key of the inserted row, or -1 if failed.
+ *
+ * @hide
+ */
+
+ private long setBytes(int userId, int uid, String key, byte[] value) {
+ SQLiteDatabase db = mKeyStoreDbHelper.getWritableDatabase();
+ ContentValues values = new ContentValues();
+ values.put(key, value);
+ 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);
+ }
+
+ /**
* 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.
*/
diff --git a/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
index 8f773ddd..4ee282b6 100644
--- a/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
+++ b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbContract.java
@@ -64,7 +64,7 @@ class RecoverableKeyStoreDbContract {
static final String COLUMN_NAME_LAST_SYNCED_AT = "last_synced_at";
/**
- * Status of the key sync {@code RecoverableKeyStoreLoader#setRecoveryStatus}
+ * Status of the key sync {@code RecoveryManager#setRecoveryStatus}
*/
static final String COLUMN_NAME_RECOVERY_STATUS = "recovery_status";
}
@@ -104,6 +104,15 @@ class RecoverableKeyStoreDbContract {
static final String COLUMN_NAME_UID = "uid";
/**
+ * Version of the latest recovery snapshot
+ */
+ static final String COLUMN_NAME_SNAPSHOT_VERSION = "snapshot_version";
+ /**
+ * Flag to generate new snapshot.
+ */
+ static final String COLUMN_NAME_SHOULD_CREATE_SNAPSHOT = "should_create_snapshot";
+
+ /**
* The public key of the recovery service.
*/
static final String COLUMN_NAME_PUBLIC_KEY = "public_key";
@@ -114,8 +123,13 @@ class RecoverableKeyStoreDbContract {
static final String COLUMN_NAME_SECRET_TYPES = "secret_types";
/**
+ * Locally generated random number.
+ */
+ static final String COLUMN_NAME_COUNTER_ID = "counter_id";
+
+ /**
* The server parameters of the recovery service.
*/
- static final String COLUMN_NAME_SERVER_PARAMETERS = "server_parameters";
+ static final String COLUMN_NAME_SERVER_PARAMS = "server_params";
}
}
diff --git a/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
index 5b07f3e2..d96671c5 100644
--- a/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
+++ b/com/android/server/locksettings/recoverablekeystore/storage/RecoverableKeyStoreDbHelper.java
@@ -51,14 +51,17 @@ class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper {
+ 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 =
+ private static final String SQL_CREATE_RECOVERY_SERVICE_METADATA_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_SNAPSHOT_VERSION + " INTEGER,"
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_SHOULD_CREATE_SNAPSHOT + " INTEGER,"
+ RecoveryServiceMetadataEntry.COLUMN_NAME_PUBLIC_KEY + " BLOB,"
+ RecoveryServiceMetadataEntry.COLUMN_NAME_SECRET_TYPES + " TEXT,"
- + RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMETERS + " INTEGER,"
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_COUNTER_ID + " INTEGER,"
+ + RecoveryServiceMetadataEntry.COLUMN_NAME_SERVER_PARAMS + " BLOB,"
+ "UNIQUE("
+ RecoveryServiceMetadataEntry.COLUMN_NAME_USER_ID + ","
+ RecoveryServiceMetadataEntry.COLUMN_NAME_UID + "))";
@@ -69,7 +72,7 @@ class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper {
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 =
+ private static final String SQL_DELETE_RECOVERY_SERVICE_METADATA_ENTRY =
"DROP TABLE IF EXISTS " + RecoveryServiceMetadataEntry.TABLE_NAME;
RecoverableKeyStoreDbHelper(Context context) {
@@ -80,14 +83,14 @@ class RecoverableKeyStoreDbHelper extends SQLiteOpenHelper {
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);
+ db.execSQL(SQL_CREATE_RECOVERY_SERVICE_METADATA_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);
+ db.execSQL(SQL_DELETE_RECOVERY_SERVICE_METADATA_ENTRY);
onCreate(db);
}
}
diff --git a/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java b/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
index f7633e4c..0e66746f 100644
--- a/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
+++ b/com/android/server/locksettings/recoverablekeystore/storage/RecoverySessionStorage.java
@@ -73,6 +73,16 @@ public class RecoverySessionStorage implements Destroyable {
}
/**
+ * Deletes the session with {@code sessionId} created by app with {@code uid}.
+ */
+ public void remove(int uid, String sessionId) {
+ if (mSessionsByUid.get(uid) == null) {
+ return;
+ }
+ mSessionsByUid.get(uid).removeIf(session -> session.mSessionId.equals(sessionId));
+ }
+
+ /**
* Removes all sessions associated with the given recovery agent uid.
*
* @param uid The uid of the recovery agent whose sessions to remove.
diff --git a/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java b/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java
index d1a1629d..3f93cc6f 100644
--- a/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java
+++ b/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java
@@ -17,7 +17,7 @@
package com.android.server.locksettings.recoverablekeystore.storage;
import android.annotation.Nullable;
-import android.security.recoverablekeystore.KeyStoreRecoveryData;
+import android.security.keystore.recovery.KeyChainSnapshot;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
@@ -27,34 +27,34 @@ import com.android.internal.annotations.GuardedBy;
*
* <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.
+ * latest snapshot for each recovery agent.
*
* <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<>();
+ private final SparseArray<KeyChainSnapshot> mSnapshotByUid = new SparseArray<>();
/**
- * Sets the latest {@code snapshot} for the user {@code userId}.
+ * Sets the latest {@code snapshot} for the recovery agent {@code uid}.
*/
- public synchronized void put(int userId, KeyStoreRecoveryData snapshot) {
- mSnapshotByUserId.put(userId, snapshot);
+ public synchronized void put(int uid, KeyChainSnapshot snapshot) {
+ mSnapshotByUid.put(uid, snapshot);
}
/**
- * Returns the latest snapshot for user {@code userId}, or null if none exists.
+ * Returns the latest snapshot for the recovery agent {@code uid}, or null if none exists.
*/
@Nullable
- public synchronized KeyStoreRecoveryData get(int userId) {
- return mSnapshotByUserId.get(userId);
+ public synchronized KeyChainSnapshot get(int uid) {
+ return mSnapshotByUid.get(uid);
}
/**
- * Removes any (if any) snapshot associated with user {@code userId}.
+ * Removes any (if any) snapshot associated with recovery agent {@code uid}.
*/
- public synchronized void remove(int userId) {
- mSnapshotByUserId.remove(userId);
+ public synchronized void remove(int uid) {
+ mSnapshotByUid.remove(uid);
}
}
diff --git a/com/android/server/media/MediaSession2Record.java b/com/android/server/media/MediaSession2Record.java
new file mode 100644
index 00000000..824b1487
--- /dev/null
+++ b/com/android/server/media/MediaSession2Record.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 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 com.android.server.media;
+
+import android.annotation.CallSuper;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.media.IMediaSession2;
+import android.media.MediaController2;
+import android.media.MediaSession2;
+import android.media.SessionToken2;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Records a {@link MediaSession2} and holds {@link MediaController2}.
+ * <p>
+ * Owner of this object should handle synchronization.
+ */
+class MediaSession2Record {
+ interface SessionDestroyedListener {
+ void onSessionDestroyed(MediaSession2Record record);
+ }
+
+ private static final String TAG = "Session2Record";
+ private static final boolean DEBUG = true; // TODO(jaewan): Change
+
+ private final Context mContext;
+ private final SessionDestroyedListener mSessionDestroyedListener;
+
+ // TODO(jaewan): Replace these with the mContext.getMainExecutor()
+ private final Handler mMainHandler;
+ private final Executor mMainExecutor;
+
+ private MediaController2 mController;
+ private ControllerCallback mControllerCallback;
+
+ private int mSessionPid;
+
+ /**
+ * Constructor
+ */
+ public MediaSession2Record(@NonNull Context context,
+ @NonNull SessionDestroyedListener listener) {
+ mContext = context;
+ mSessionDestroyedListener = listener;
+
+ mMainHandler = new Handler(Looper.getMainLooper());
+ mMainExecutor = (runnable) -> {
+ mMainHandler.post(runnable);
+ };
+ }
+
+ public int getSessionPid() {
+ return mSessionPid;
+ }
+
+ public Context getContext() {
+ return mContext;
+ }
+
+ @CallSuper
+ public void onSessionDestroyed() {
+ if (mController != null) {
+ mControllerCallback.destroy();
+ mController.close();
+ mController = null;
+ }
+ mSessionPid = 0;
+ }
+
+ /**
+ * Create session token and tell server that session is now active.
+ *
+ * @param sessionPid session's pid
+ * @return a token if successfully set, {@code null} if sanity check fails.
+ */
+ // TODO(jaewan): also add uid for multiuser support
+ @CallSuper
+ public @Nullable
+ SessionToken2 createSessionToken(int sessionPid, String packageName, String id,
+ IMediaSession2 sessionBinder) {
+ if (mController != null) {
+ if (mSessionPid != sessionPid) {
+ // A package uses the same id for session across the different process.
+ return null;
+ }
+ // If a session becomes inactive and then active again very quickly, previous 'inactive'
+ // may not have delivered yet. Check if it's the case and destroy controller before
+ // creating its session record to prevents getXXTokens() API from returning duplicated
+ // tokens.
+ // TODO(jaewan): Change this. If developer is really creating two sessions with the same
+ // id, this will silently invalidate previous session and no way for
+ // developers to know that.
+ // Instead, keep the list of static session ids from our APIs.
+ // Also change Controller2Impl.onConnectionChanged / getController.
+ // Also clean up ControllerCallback#destroy().
+ if (DEBUG) {
+ Log.d(TAG, "Session is recreated almost immediately. " + this);
+ }
+ onSessionDestroyed();
+ }
+ mController = onCreateMediaController(packageName, id, sessionBinder);
+ mSessionPid = sessionPid;
+ return mController.getSessionToken();
+ }
+
+ /**
+ * Called when session becomes active and needs controller to listen session's activeness.
+ * <p>
+ * Should be overridden by subclasses to create token with its own extra information.
+ */
+ MediaController2 onCreateMediaController(
+ String packageName, String id, IMediaSession2 sessionBinder) {
+ SessionToken2 token = new SessionToken2(
+ SessionToken2.TYPE_SESSION, packageName, id, null, sessionBinder);
+ return createMediaController(token);
+ }
+
+ final MediaController2 createMediaController(SessionToken2 token) {
+ mControllerCallback = new ControllerCallback();
+ return new MediaController2(mContext, token, mControllerCallback, mMainExecutor);
+ }
+
+ /**
+ * @return controller. Note that framework can only call oneway calls.
+ */
+ public SessionToken2 getToken() {
+ return mController == null ? null : mController.getSessionToken();
+ }
+
+ @Override
+ public String toString() {
+ return getToken() == null
+ ? "Token {null}"
+ : "SessionRecord {pid=" + mSessionPid + ", " + getToken().toString() + "}";
+ }
+
+ private class ControllerCallback extends MediaController2.ControllerCallback {
+ private final AtomicBoolean mIsActive = new AtomicBoolean(true);
+
+ // This is called on the main thread with no lock. So place ensure followings.
+ // 1. Don't touch anything in the parent class that needs synchronization.
+ // All other APIs in the MediaSession2Record assumes that server would use them with
+ // the lock hold.
+ // 2. This can be called after the controller registered is released.
+ @Override
+ public void onDisconnected() {
+ if (!mIsActive.get()) {
+ return;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "onDisconnected, token=" + getToken());
+ }
+ mSessionDestroyedListener.onSessionDestroyed(MediaSession2Record.this);
+ }
+
+ // TODO(jaewan): Remove this API when we revisit createSessionToken()
+ public void destroy() {
+ mIsActive.set(false);
+ }
+ };
+}
diff --git a/com/android/server/media/MediaSessionService.java b/com/android/server/media/MediaSessionService.java
index 06f4f5e8..c7f6014f 100644
--- a/com/android/server/media/MediaSessionService.java
+++ b/com/android/server/media/MediaSessionService.java
@@ -28,13 +28,19 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.media.AudioManager;
import android.media.AudioPlaybackConfiguration;
import android.media.AudioSystem;
import android.media.IAudioService;
+import android.media.IMediaSession2;
import android.media.IRemoteVolumeController;
+import android.media.MediaLibraryService2;
+import android.media.MediaSessionService2;
+import android.media.SessionToken2;
import android.media.session.IActiveSessionsListener;
import android.media.session.ICallback;
import android.media.session.IOnMediaKeyListener;
@@ -118,6 +124,24 @@ public class MediaSessionService extends SystemService implements Monitor {
// better way to handle this.
private IRemoteVolumeController mRvc;
+ // MediaSession2 support
+ // TODO(jaewan): Support multi-user and managed profile.
+ // TODO(jaewan): Make it priority list for handling volume/media key.
+ private final List<MediaSession2Record> mSessions = new ArrayList<>();
+
+ private final MediaSession2Record.SessionDestroyedListener mSessionDestroyedListener =
+ (MediaSession2Record record) -> {
+ synchronized (mLock) {
+ if (DEBUG) {
+ Log.d(TAG, record.toString() + " becomes inactive");
+ }
+ record.onSessionDestroyed();
+ if (!(record instanceof MediaSessionService2Record)) {
+ mSessions.remove(record);
+ }
+ }
+ };
+
public MediaSessionService(Context context) {
super(context);
mSessionManagerImpl = new SessionManagerImpl();
@@ -158,6 +182,11 @@ public class MediaSessionService extends SystemService implements Monitor {
PackageManager.FEATURE_LEANBACK);
updateUser();
+
+ // TODO(jaewan): Query per users
+ // TODO(jaewan): Add listener to know changes in list of services.
+ // Refer TvInputManagerService.registerBroadcastReceivers()
+ buildMediaSessionService2List();
}
private IAudioService getAudioService() {
@@ -411,6 +440,74 @@ public class MediaSessionService extends SystemService implements Monitor {
mHandler.postSessionsChanged(session.getUserId());
}
+ private void buildMediaSessionService2List() {
+ if (DEBUG) {
+ Log.d(TAG, "buildMediaSessionService2List");
+ }
+
+ // TODO(jaewan): Query per users.
+ List<ResolveInfo> services = new ArrayList<>();
+ // If multiple actions are declared for a service, browser gets higher priority.
+ List<ResolveInfo> libraryServices = getContext().getPackageManager().queryIntentServices(
+ new Intent(MediaLibraryService2.SERVICE_INTERFACE), PackageManager.GET_META_DATA);
+ if (libraryServices != null) {
+ services.addAll(libraryServices);
+ }
+ List<ResolveInfo> sessionServices = getContext().getPackageManager().queryIntentServices(
+ new Intent(MediaSessionService2.SERVICE_INTERFACE), PackageManager.GET_META_DATA);
+ if (sessionServices != null) {
+ services.addAll(sessionServices);
+ }
+ synchronized (mLock) {
+ mSessions.clear();
+ if (services == null) {
+ return;
+ }
+ for (int i = 0; i < services.size(); i++) {
+ if (services.get(i) == null || services.get(i).serviceInfo == null) {
+ continue;
+ }
+ ServiceInfo serviceInfo = services.get(i).serviceInfo;
+ String id = (serviceInfo.metaData != null) ? serviceInfo.metaData.getString(
+ MediaSessionService2.SERVICE_META_DATA) : null;
+ // Do basic sanity check
+ // TODO(jaewan): also santity check if it's protected with the system|privileged
+ // permission
+ boolean conflict = (getSessionRecordLocked(serviceInfo.name, id) != null);
+ if (conflict) {
+ Log.w(TAG, serviceInfo.packageName + " contains multiple"
+ + " MediaSessionService2s declared in the manifest with"
+ + " the same ID=" + id + ". Ignoring "
+ + serviceInfo.packageName + "/" + serviceInfo.name);
+ } else {
+ int type = (libraryServices.contains(services.get(i)))
+ ? SessionToken2.TYPE_LIBRARY_SERVICE : SessionToken2.TYPE_SESSION_SERVICE;
+ MediaSessionService2Record record =
+ new MediaSessionService2Record(getContext(), mSessionDestroyedListener,
+ type, serviceInfo.packageName, serviceInfo.name, id);
+ mSessions.add(record);
+ }
+ }
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Found " + mSessions.size() + " session services");
+ for (int i = 0; i < mSessions.size(); i++) {
+ Log.d(TAG, " " + mSessions.get(i).getToken());
+ }
+ }
+ }
+
+ MediaSession2Record getSessionRecordLocked(String packageName, String id) {
+ for (int i = 0; i < mSessions.size(); i++) {
+ MediaSession2Record record = mSessions.get(i);
+ if (record.getToken().getPackageName().equals(packageName)
+ && record.getToken().getId().equals(id)) {
+ return record;
+ }
+ }
+ return null;
+ }
+
private void enforcePackageName(String packageName, int uid) {
if (TextUtils.isEmpty(packageName)) {
throw new IllegalArgumentException("packageName may not be empty");
@@ -1312,6 +1409,57 @@ public class MediaSessionService extends SystemService implements Monitor {
}
}
+ @Override
+ public Bundle createSessionToken(String sessionPackage, String id,
+ IMediaSession2 sessionBinder) throws RemoteException {
+ int uid = Binder.getCallingUid();
+ int pid = Binder.getCallingPid();
+
+ MediaSession2Record record;
+ SessionToken2 token;
+ // TODO(jaewan): Add sanity check for the token if calling package is from uid.
+ synchronized (mLock) {
+ record = getSessionRecordLocked(sessionPackage, id);
+ if (record == null) {
+ record = new MediaSession2Record(getContext(), mSessionDestroyedListener);
+ mSessions.add(record);
+ }
+ token = record.createSessionToken(pid, sessionPackage, id, sessionBinder);
+ if (token == null) {
+ Log.d(TAG, "failed to create session token for " + sessionPackage
+ + " from pid=" + pid + ". Previously " + record);
+ } else {
+ Log.d(TAG, "session " + token + " is created");
+ }
+ }
+ return token == null ? null : token.toBundle();
+ }
+
+ // TODO(jaewan): Protect this API with permission
+ // TODO(jaewan): Add listeners for change in operations..
+ @Override
+ public List<Bundle> getSessionTokens(boolean activeSessionOnly,
+ boolean sessionServiceOnly) throws RemoteException {
+ List<Bundle> tokens = new ArrayList<>();
+ synchronized (mLock) {
+ for (int i = 0; i < mSessions.size(); i++) {
+ MediaSession2Record record = mSessions.get(i);
+ boolean isSessionService = (record instanceof MediaSessionService2Record);
+ boolean isActive = record.getSessionPid() != 0;
+ if ((!activeSessionOnly && isSessionService)
+ || (!sessionServiceOnly && isActive)) {
+ SessionToken2 token = record.getToken();
+ if (token != null) {
+ tokens.add(token.toBundle());
+ } else {
+ Log.wtf(TAG, "Null token for record=" + record);
+ }
+ }
+ }
+ }
+ return tokens;
+ }
+
private int verifySessionsRequest(ComponentName componentName, int userId, final int pid,
final int uid) {
String packageName = null;
diff --git a/com/android/server/media/MediaSessionService2Record.java b/com/android/server/media/MediaSessionService2Record.java
new file mode 100644
index 00000000..d033f552
--- /dev/null
+++ b/com/android/server/media/MediaSessionService2Record.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 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 com.android.server.media;
+
+import android.content.Context;
+import android.media.IMediaSession2;
+import android.media.MediaController2;
+import android.media.SessionToken2;
+import android.media.MediaSessionService2;
+
+/**
+ * Records a {@link MediaSessionService2}.
+ * <p>
+ * Owner of this object should handle synchronization.
+ */
+class MediaSessionService2Record extends MediaSession2Record {
+ private static final boolean DEBUG = true; // TODO(jaewan): Modify
+ private static final String TAG = "SessionService2Record";
+
+ private final int mType;
+ private final String mServiceName;
+ private final SessionToken2 mToken;
+
+ public MediaSessionService2Record(Context context,
+ SessionDestroyedListener sessionDestroyedListener, int type,
+ String packageName, String serviceName, String id) {
+ super(context, sessionDestroyedListener);
+ mType = type;
+ mServiceName = serviceName;
+ mToken = new SessionToken2(mType, packageName, id, mServiceName, null);
+ }
+
+ /**
+ * Overriden to change behavior of
+ * {@link #createSessionToken(int, String, String, IMediaSession2)}}.
+ */
+ @Override
+ MediaController2 onCreateMediaController(
+ String packageName, String id, IMediaSession2 sessionBinder) {
+ SessionToken2 token = new SessionToken2(mType, packageName, id, mServiceName, sessionBinder);
+ return createMediaController(token);
+ }
+
+ /**
+ * @return token with no session binder information.
+ */
+ @Override
+ public SessionToken2 getToken() {
+ return mToken;
+ }
+}
diff --git a/com/android/server/media/MediaUpdateService.java b/com/android/server/media/MediaUpdateService.java
new file mode 100644
index 00000000..6921ccde
--- /dev/null
+++ b/com/android/server/media/MediaUpdateService.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 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 com.android.server.media;
+
+import android.content.Context;
+import android.content.Intent;
+import android.media.IMediaResourceMonitor;
+import android.os.Binder;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.Log;
+import android.util.Slog;
+import com.android.server.SystemService;
+
+import android.content.BroadcastReceiver;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.os.PatternMatcher;
+import android.os.ServiceManager;
+import android.media.IMediaExtractorUpdateService;
+
+import java.lang.Exception;
+
+/** This class provides a system service that manages media framework updates. */
+public class MediaUpdateService extends SystemService {
+ private static final String TAG = "MediaUpdateService";
+ private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+ private static final String MEDIA_UPDATE_PACKAGE_NAME = "com.android.media.update";
+ private static final String EXTRACTOR_UPDATE_SERVICE_NAME = "media.extractor.update";
+
+ private IMediaExtractorUpdateService mMediaExtractorUpdateService;
+
+ public MediaUpdateService(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onStart() {
+ if ("userdebug".equals(android.os.Build.TYPE) || "eng".equals(android.os.Build.TYPE)) {
+ connect();
+ registerBroadcastReceiver();
+ }
+ }
+
+ private void connect() {
+ IBinder binder = ServiceManager.getService(EXTRACTOR_UPDATE_SERVICE_NAME);
+ if (binder != null) {
+ try {
+ binder.linkToDeath(new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ Slog.w(TAG, "mediaextractor died; reconnecting");
+ mMediaExtractorUpdateService = null;
+ connect();
+ }
+ }, 0);
+ } catch (Exception e) {
+ binder = null;
+ }
+ }
+ if (binder != null) {
+ mMediaExtractorUpdateService = IMediaExtractorUpdateService.Stub.asInterface(binder);
+ packageStateChanged();
+ } else {
+ Slog.w(TAG, EXTRACTOR_UPDATE_SERVICE_NAME + " not found.");
+ }
+ }
+
+ private void registerBroadcastReceiver() {
+ BroadcastReceiver updateReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_SYSTEM)
+ != UserHandle.USER_SYSTEM) {
+ // Ignore broadcast for non system users. We don't want to update system
+ // service multiple times.
+ return;
+ }
+ switch (intent.getAction()) {
+ case Intent.ACTION_PACKAGE_REMOVED:
+ if (intent.getExtras().getBoolean(Intent.EXTRA_REPLACING)) {
+ // The existing package is updated. Will be handled with the
+ // following ACTION_PACKAGE_ADDED case.
+ return;
+ }
+ packageStateChanged();
+ break;
+ case Intent.ACTION_PACKAGE_CHANGED:
+ packageStateChanged();
+ break;
+ case Intent.ACTION_PACKAGE_ADDED:
+ packageStateChanged();
+ break;
+ }
+ }
+ };
+ 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");
+ filter.addDataSchemeSpecificPart(MEDIA_UPDATE_PACKAGE_NAME, PatternMatcher.PATTERN_LITERAL);
+
+ getContext().registerReceiverAsUser(updateReceiver, UserHandle.ALL, filter,
+ null /* broadcast permission */, null /* handler */);
+ }
+
+ private void packageStateChanged() {
+ ApplicationInfo packageInfo = null;
+ boolean pluginsAvailable = false;
+ try {
+ packageInfo = getContext().getPackageManager().getApplicationInfo(
+ MEDIA_UPDATE_PACKAGE_NAME, PackageManager.MATCH_SYSTEM_ONLY);
+ pluginsAvailable = packageInfo.enabled;
+ } catch (Exception e) {
+ Slog.v(TAG, "package '" + MEDIA_UPDATE_PACKAGE_NAME + "' not installed");
+ }
+ loadExtractorPlugins(
+ (packageInfo != null && pluginsAvailable) ? packageInfo.sourceDir : "");
+ }
+
+ private void loadExtractorPlugins(String apkPath) {
+ try {
+ if (mMediaExtractorUpdateService != null) {
+ mMediaExtractorUpdateService.loadPlugins(apkPath);
+ }
+ } catch (Exception e) {
+ Slog.w(TAG, "Error in loadPlugins", e);
+ }
+ }
+}
diff --git a/com/android/server/net/NetworkIdentitySet.java b/com/android/server/net/NetworkIdentitySet.java
index ee00fdc3..68cd5e7a 100644
--- a/com/android/server/net/NetworkIdentitySet.java
+++ b/com/android/server/net/NetworkIdentitySet.java
@@ -39,6 +39,7 @@ public class NetworkIdentitySet extends HashSet<NetworkIdentity> implements
private static final int VERSION_ADD_ROAMING = 2;
private static final int VERSION_ADD_NETWORK_ID = 3;
private static final int VERSION_ADD_METERED = 4;
+ private static final int VERSION_ADD_DEFAULT_NETWORK = 5;
public NetworkIdentitySet() {
}
@@ -76,12 +77,20 @@ public class NetworkIdentitySet extends HashSet<NetworkIdentity> implements
metered = (type == TYPE_MOBILE);
}
- add(new NetworkIdentity(type, subType, subscriberId, networkId, roaming, metered));
+ final boolean defaultNetwork;
+ if (version >= VERSION_ADD_DEFAULT_NETWORK) {
+ defaultNetwork = in.readBoolean();
+ } else {
+ defaultNetwork = true;
+ }
+
+ add(new NetworkIdentity(type, subType, subscriberId, networkId, roaming, metered,
+ defaultNetwork));
}
}
public void writeToStream(DataOutputStream out) throws IOException {
- out.writeInt(VERSION_ADD_METERED);
+ out.writeInt(VERSION_ADD_DEFAULT_NETWORK);
out.writeInt(size());
for (NetworkIdentity ident : this) {
out.writeInt(ident.getType());
@@ -90,6 +99,7 @@ public class NetworkIdentitySet extends HashSet<NetworkIdentity> implements
writeOptionalString(out, ident.getNetworkId());
out.writeBoolean(ident.getRoaming());
out.writeBoolean(ident.getMetered());
+ out.writeBoolean(ident.getDefaultNetwork());
}
}
@@ -119,6 +129,20 @@ public class NetworkIdentitySet extends HashSet<NetworkIdentity> implements
return false;
}
+ /** @return whether any {@link NetworkIdentity} in this set is considered on the default
+ network. */
+ public boolean areAllMembersOnDefaultNetwork() {
+ if (isEmpty()) {
+ return true;
+ }
+ for (NetworkIdentity ident : this) {
+ if (!ident.getDefaultNetwork()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
private static void writeOptionalString(DataOutputStream out, String value) throws IOException {
if (value != null) {
out.writeByte(1);
diff --git a/com/android/server/net/NetworkPolicyLogger.java b/com/android/server/net/NetworkPolicyLogger.java
index 2bd9cab3..b4bc7f50 100644
--- a/com/android/server/net/NetworkPolicyLogger.java
+++ b/com/android/server/net/NetworkPolicyLogger.java
@@ -37,6 +37,7 @@ import com.android.server.am.ProcessList;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
+import java.util.Set;
public class NetworkPolicyLogger {
static final String TAG = "NetworkPolicy";
@@ -62,6 +63,7 @@ public class NetworkPolicyLogger {
private static final int EVENT_TEMP_POWER_SAVE_WL_CHANGED = 10;
private static final int EVENT_UID_FIREWALL_RULE_CHANGED = 11;
private static final int EVENT_FIREWALL_CHAIN_ENABLED = 12;
+ private static final int EVENT_UPDATE_METERED_RESTRICTED_PKGS = 13;
static final int NTWK_BLOCKED_POWER = 0;
static final int NTWK_ALLOWED_NON_METERED = 1;
@@ -179,6 +181,14 @@ public class NetworkPolicyLogger {
}
}
+ void meteredRestrictedPkgsChanged(Set<Integer> restrictedUids) {
+ synchronized (mLock) {
+ final String log = "Metered restricted uids: " + restrictedUids;
+ if (LOGD) Slog.d(TAG, log);
+ mEventsBuffer.event(log);
+ }
+ }
+
void dumpLogs(IndentingPrintWriter pw) {
synchronized (mLock) {
pw.println();
diff --git a/com/android/server/net/NetworkPolicyManagerInternal.java b/com/android/server/net/NetworkPolicyManagerInternal.java
index 7934a968..64909644 100644
--- a/com/android/server/net/NetworkPolicyManagerInternal.java
+++ b/com/android/server/net/NetworkPolicyManagerInternal.java
@@ -16,6 +16,11 @@
package com.android.server.net;
+import android.net.Network;
+import android.telephony.SubscriptionPlan;
+
+import java.util.Set;
+
/**
* Network Policy Manager local system service interface.
*
@@ -47,4 +52,42 @@ public abstract class NetworkPolicyManagerInternal {
* @param added Denotes whether the {@param appId} has been added or removed from the whitelist.
*/
public abstract void onTempPowerSaveWhitelistChange(int appId, boolean added);
+
+ /**
+ * Return the active {@link SubscriptionPlan} for the given network.
+ */
+ public abstract SubscriptionPlan getSubscriptionPlan(Network network);
+
+ public static final int QUOTA_TYPE_JOBS = 1;
+ public static final int QUOTA_TYPE_MULTIPATH = 2;
+
+ /**
+ * Return the daily quota (in bytes) that can be opportunistically used on
+ * the given network to improve the end user experience. It's called
+ * "opportunistic" because it's traffic that would typically not use the
+ * given network.
+ */
+ public abstract long getSubscriptionOpportunisticQuota(Network network, int quotaType);
+
+ /**
+ * Informs that admin data is loaded and available.
+ */
+ public abstract void onAdminDataAvailable();
+
+ /**
+ * Sets a list of packages which are restricted by admin from accessing metered data.
+ *
+ * @param packageNames the list of restricted packages.
+ * @param userId the userId in which {@param packagesNames} are restricted.
+ */
+ public abstract void setMeteredRestrictedPackages(
+ Set<String> packageNames, int userId);
+
+
+ /**
+ * Similar to {@link #setMeteredRestrictedPackages(Set, int)} but updates the restricted
+ * packages list asynchronously.
+ */
+ public abstract void setMeteredRestrictedPackagesAsync(
+ Set<String> packageNames, int userId);
}
diff --git a/com/android/server/net/NetworkPolicyManagerService.java b/com/android/server/net/NetworkPolicyManagerService.java
index fdfe2418..0e54768b 100644
--- a/com/android/server/net/NetworkPolicyManagerService.java
+++ b/com/android/server/net/NetworkPolicyManagerService.java
@@ -34,6 +34,7 @@ import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED
import static android.net.ConnectivityManager.RESTRICT_BACKGROUND_STATUS_WHITELISTED;
import static android.net.ConnectivityManager.TYPE_MOBILE;
import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
import static android.net.NetworkPolicy.LIMIT_DISABLED;
import static android.net.NetworkPolicy.SNOOZE_NEVER;
import static android.net.NetworkPolicy.WARNING_DISABLED;
@@ -69,6 +70,7 @@ import static android.net.TrafficStats.MB_IN_BYTES;
import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED;
import static android.telephony.CarrierConfigManager.DATA_CYCLE_THRESHOLD_DISABLED;
import static android.telephony.CarrierConfigManager.DATA_CYCLE_USE_PLATFORM_DEFAULT;
+import static android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID;
import static android.text.format.DateUtils.DAY_IN_MILLIS;
import static com.android.internal.util.ArrayUtils.appendInt;
@@ -97,6 +99,7 @@ import static org.xmlpull.v1.XmlPullParser.START_TAG;
import android.Manifest;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
@@ -134,8 +137,10 @@ import android.net.NetworkPolicy;
import android.net.NetworkPolicyManager;
import android.net.NetworkQuotaInfo;
import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
import android.net.NetworkState;
import android.net.NetworkTemplate;
+import android.net.StringNetworkSpecifier;
import android.net.TrafficStats;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
@@ -174,6 +179,7 @@ import android.text.format.Formatter;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
+import android.util.DataUnit;
import android.util.Log;
import android.util.NtpTrustedTime;
import android.util.Pair;
@@ -182,6 +188,7 @@ import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
+import android.util.SparseLongArray;
import android.util.TrustedTime;
import android.util.Xml;
@@ -192,6 +199,7 @@ import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.ConcurrentUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FastXmlSerializer;
import com.android.internal.util.IndentingPrintWriter;
@@ -200,8 +208,10 @@ import com.android.server.EventLogTags;
import com.android.server.LocalServices;
import com.android.server.ServiceThread;
import com.android.server.SystemConfig;
+import com.android.server.SystemService;
import libcore.io.IoUtils;
+import libcore.util.EmptyArray;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlSerializer;
@@ -219,10 +229,10 @@ import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.Objects;
+import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@@ -278,6 +288,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
public static final int TYPE_LIMIT = SystemMessage.NOTE_NET_LIMIT;
@VisibleForTesting
public static final int TYPE_LIMIT_SNOOZED = SystemMessage.NOTE_NET_LIMIT_SNOOZED;
+ @VisibleForTesting
+ public static final int TYPE_RAPID = SystemMessage.NOTE_NET_RAPID;
private static final String TAG_POLICY_LIST = "policy-list";
private static final String TAG_NETWORK_POLICY = "network-policy";
@@ -323,6 +335,11 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
private static final long TIME_CACHE_MAX_AGE = DAY_IN_MILLIS;
+ /**
+ * Indicates the maximum wait time for admin data to be available;
+ */
+ private static final long WAIT_FOR_ADMIN_DATA_TIMEOUT_MS = 10_000;
+
private static final int MSG_RULES_CHANGED = 1;
private static final int MSG_METERED_IFACES_CHANGED = 2;
private static final int MSG_LIMIT_REACHED = 5;
@@ -332,6 +349,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
private static final int MSG_REMOVE_INTERFACE_QUOTA = 11;
private static final int MSG_POLICIES_CHANGED = 13;
private static final int MSG_RESET_FIREWALL_RULES_BY_UID = 15;
+ private static final int MSG_SUBSCRIPTION_OVERRIDE = 16;
+ private static final int MSG_METERED_RESTRICTED_PACKAGES_CHANGED = 17;
private static final int UID_MSG_STATE_CHANGED = 100;
private static final int UID_MSG_GONE = 101;
@@ -373,6 +392,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
private final boolean mSuppressDefaultPolicy;
+ private final CountDownLatch mAdminDataAvailableLatch = new CountDownLatch(1);
+
/** Defined network policies. */
@GuardedBy("mNetworkPoliciesSecondLock")
final ArrayMap<NetworkTemplate, NetworkPolicy> mNetworkPolicy = new ArrayMap<>();
@@ -384,6 +405,10 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
@GuardedBy("mNetworkPoliciesSecondLock")
final SparseArray<String> mSubscriptionPlansOwner = new SparseArray<>();
+ /** Map from subId to daily opportunistic quota. */
+ @GuardedBy("mNetworkPoliciesSecondLock")
+ final SparseLongArray mSubscriptionOpportunisticQuota = new SparseLongArray();
+
/** Defined UID policies. */
@GuardedBy("mUidRulesFirstLock") final SparseIntArray mUidPolicy = new SparseIntArray();
/** Currently derived rules for each UID. */
@@ -453,6 +478,17 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
@GuardedBy("mNetworkPoliciesSecondLock")
private final SparseBooleanArray mNetworkMetered = new SparseBooleanArray();
+ /** Map from netId to subId as of last update */
+ @GuardedBy("mNetworkPoliciesSecondLock")
+ private final SparseIntArray mNetIdToSubId = new SparseIntArray();
+
+ /**
+ * Indicates the uids restricted by admin from accessing metered data. It's a mapping from
+ * userId to restricted uids which belong to that user.
+ */
+ @GuardedBy("mUidRulesFirstLock")
+ private final SparseArray<Set<Integer>> mMeteredRestrictedUids = new SparseArray<>();
+
private final RemoteCallbackList<INetworkPolicyListener>
mListeners = new RemoteCallbackList<>();
@@ -653,6 +689,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
mSystemReady = true;
+ waitForAdminData();
+
// read policy from disk
readPolicyAL();
@@ -869,6 +907,9 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
// Remove any persistable state for the given user; both cleaning up after a
// USER_REMOVED, and one last sanity check during USER_ADDED
removeUserStateUL(userId, true);
+ // Removing outside removeUserStateUL since that can also be called when
+ // user resets app preferences.
+ mMeteredRestrictedUids.remove(userId);
if (action == ACTION_USER_ADDED) {
// Add apps that are whitelisted by default.
addDefaultRestrictBackgroundWhitelistUidsUL(userId);
@@ -984,6 +1025,13 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
}
};
+ @VisibleForTesting
+ public void updateNotifications() {
+ synchronized (mNetworkPoliciesSecondLock) {
+ updateNotificationsNL();
+ }
+ }
+
/**
* Check {@link NetworkPolicy} against current {@link INetworkStatsService}
* to show visible notifications as needed.
@@ -1028,6 +1076,44 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
}
}
+ // Alert the user about heavy recent data usage that might result in
+ // going over their carrier limit.
+ for (int i = 0; i < mNetIdToSubId.size(); i++) {
+ final int subId = mNetIdToSubId.valueAt(i);
+ final SubscriptionPlan plan = getPrimarySubscriptionPlanLocked(subId);
+ if (plan == null) continue;
+
+ final long limitBytes = plan.getDataLimitBytes();
+ if (limitBytes == SubscriptionPlan.BYTES_UNKNOWN) {
+ // Ignore missing limits
+ } else if (limitBytes == SubscriptionPlan.BYTES_UNLIMITED) {
+ // Unlimited data; no rapid usage alerting
+ } else {
+ // Warn if average usage over last 4 days is on track to blow
+ // pretty far past the plan limits.
+ final long recentDuration = TimeUnit.DAYS.toMillis(4);
+ final long end = RecurrenceRule.sClock.millis();
+ final long start = end - recentDuration;
+
+ final NetworkTemplate template = NetworkTemplate.buildTemplateMobileAll(
+ mContext.getSystemService(TelephonyManager.class).getSubscriberId(subId));
+ final long recentBytes = getTotalBytes(template, start, end);
+
+ final Pair<ZonedDateTime, ZonedDateTime> cycle = plan.cycleIterator().next();
+ final long cycleDuration = cycle.second.toInstant().toEpochMilli()
+ - cycle.first.toInstant().toEpochMilli();
+
+ final long projectedBytes = (recentBytes * cycleDuration) / recentDuration;
+ final long alertBytes = (limitBytes * 3) / 2;
+ if (projectedBytes > alertBytes) {
+ final NetworkPolicy policy = new NetworkPolicy(template, plan.getCycleRule(),
+ NetworkPolicy.WARNING_DISABLED, NetworkPolicy.LIMIT_DISABLED,
+ NetworkPolicy.SNOOZE_NEVER, NetworkPolicy.SNOOZE_NEVER, true, true);
+ enqueueNotification(policy, TYPE_RAPID, 0);
+ }
+ }
+ }
+
// cancel stale notifications that we didn't renew above
for (int i = beforeNotifs.size()-1; i >= 0; i--) {
final NotificationId notificationId = beforeNotifs.valueAt(i);
@@ -1049,11 +1135,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
final SubscriptionManager sub = SubscriptionManager.from(mContext);
// Mobile template is relevant when any active subscriber matches
- final int[] subIds = sub.getActiveSubscriptionIdList();
+ final int[] subIds = ArrayUtils.defeatNullable(sub.getActiveSubscriptionIdList());
for (int subId : subIds) {
final String subscriberId = tele.getSubscriberId(subId);
final NetworkIdentity probeIdent = new NetworkIdentity(TYPE_MOBILE,
- TelephonyManager.NETWORK_TYPE_UNKNOWN, subscriberId, null, false, true);
+ TelephonyManager.NETWORK_TYPE_UNKNOWN, subscriberId, null, false, true,
+ true);
if (template.matches(probeIdent)) {
return true;
}
@@ -1186,6 +1273,21 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
break;
}
+ case TYPE_RAPID: {
+ final CharSequence title = res.getText(R.string.data_usage_rapid_title);
+ body = res.getText(R.string.data_usage_rapid_body);
+
+ builder.setOngoing(true);
+ builder.setSmallIcon(R.drawable.stat_notify_error);
+ builder.setTicker(title);
+ builder.setContentTitle(title);
+ builder.setContentText(body);
+
+ final Intent intent = buildViewDataUsageIntent(res, policy.template);
+ builder.setContentIntent(PendingIntent.getActivity(
+ mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT));
+ break;
+ }
}
// TODO: move to NotificationManager once we can mock it
@@ -1239,6 +1341,11 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
}
};
+ @VisibleForTesting
+ public void updateNetworks() {
+ mConnReceiver.onReceive(null, null);
+ }
+
/**
* Update mobile policies with data cycle information from {@link CarrierConfigManager}
* if necessary.
@@ -1254,7 +1361,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
// find and update the mobile NetworkPolicy for this subscriber id
final NetworkIdentity probeIdent = new NetworkIdentity(TYPE_MOBILE,
- TelephonyManager.NETWORK_TYPE_UNKNOWN, subscriberId, null, false, true);
+ TelephonyManager.NETWORK_TYPE_UNKNOWN, subscriberId, null, false, true, true);
for (int i = mNetworkPolicy.size() - 1; i >= 0; i--) {
final NetworkTemplate template = mNetworkPolicy.keyAt(i);
if (template.matches(probeIdent)) {
@@ -1457,11 +1564,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
final SubscriptionManager sm = SubscriptionManager.from(mContext);
final TelephonyManager tm = TelephonyManager.from(mContext);
- final int[] subIds = sm.getActiveSubscriptionIdList();
+ final int[] subIds = ArrayUtils.defeatNullable(sm.getActiveSubscriptionIdList());
for (int subId : subIds) {
final String subscriberId = tm.getSubscriberId(subId);
final NetworkIdentity probeIdent = new NetworkIdentity(TYPE_MOBILE,
- TelephonyManager.NETWORK_TYPE_UNKNOWN, subscriberId, null, false, true);
+ TelephonyManager.NETWORK_TYPE_UNKNOWN, subscriberId, null, false, true,
+ true);
// Template is matched when subscriber id matches.
if (template.matches(probeIdent)) {
tm.setPolicyDataEnabled(enabled, subId);
@@ -1496,7 +1604,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
final NetworkState[] states;
try {
- states = mConnManager.getAllNetworkState();
+ states = defeatNullable(mConnManager.getAllNetworkState());
} catch (RemoteException e) {
// ignored; service lives in system_server
return;
@@ -1504,10 +1612,15 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
// First, generate identities of all connected networks so we can
// quickly compare them against all defined policies below.
+ mNetIdToSubId.clear();
final ArrayMap<NetworkState, NetworkIdentity> identified = new ArrayMap<>();
for (NetworkState state : states) {
+ if (state.network != null) {
+ mNetIdToSubId.put(state.network.netId, parseSubId(state));
+ }
if (state.networkInfo != null && state.networkInfo.isConnected()) {
- final NetworkIdentity ident = NetworkIdentity.buildNetworkIdentity(mContext, state);
+ final NetworkIdentity ident = NetworkIdentity.buildNetworkIdentity(mContext, state,
+ true);
identified.put(state, ident);
}
}
@@ -1607,6 +1720,42 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
}
mMeteredIfaces = newMeteredIfaces;
+ // Finally, calculate our opportunistic quotas
+ // TODO: add experiments support to disable or tweak ratios
+ mSubscriptionOpportunisticQuota.clear();
+ for (NetworkState state : states) {
+ if (state.network == null) continue;
+ final int subId = getSubIdLocked(state.network);
+ final SubscriptionPlan plan = getPrimarySubscriptionPlanLocked(subId);
+ if (plan == null) continue;
+
+ // By default assume we have no quota
+ long quotaBytes = 0;
+
+ final long limitBytes = plan.getDataLimitBytes();
+ if (limitBytes == SubscriptionPlan.BYTES_UNKNOWN) {
+ // Ignore missing limits
+ } else if (limitBytes == SubscriptionPlan.BYTES_UNLIMITED) {
+ // Unlimited data; let's use 20MiB/day (600MiB/month)
+ quotaBytes = DataUnit.MEBIBYTES.toBytes(20);
+ } else {
+ // Limited data; let's only use 10% of remaining budget
+ final Pair<ZonedDateTime, ZonedDateTime> cycle = plan.cycleIterator().next();
+ final long start = cycle.first.toInstant().toEpochMilli();
+ final long end = cycle.second.toInstant().toEpochMilli();
+ final long totalBytes = getTotalBytes(
+ NetworkTemplate.buildTemplateMobileAll(state.subscriberId), start, end);
+ final long remainingBytes = limitBytes - totalBytes;
+ final long remainingDays = Math.min(1, (end - RecurrenceRule.sClock.millis())
+ / TimeUnit.DAYS.toMillis(1));
+ if (remainingBytes > 0) {
+ quotaBytes = (remainingBytes / remainingDays) / 10;
+ }
+ }
+
+ mSubscriptionOpportunisticQuota.put(subId, quotaBytes);
+ }
+
final String[] meteredIfaces = mMeteredIfaces.toArray(new String[mMeteredIfaces.size()]);
mHandler.obtainMessage(MSG_METERED_IFACES_CHANGED, meteredIfaces).sendToTarget();
@@ -1624,7 +1773,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
final TelephonyManager tele = TelephonyManager.from(mContext);
final SubscriptionManager sub = SubscriptionManager.from(mContext);
- final int[] subIds = sub.getActiveSubscriptionIdList();
+ final int[] subIds = ArrayUtils.defeatNullable(sub.getActiveSubscriptionIdList());
for (int subId : subIds) {
final String subscriberId = tele.getSubscriberId(subId);
ensureActiveMobilePolicyAL(subId, subscriberId);
@@ -1642,7 +1791,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
private boolean ensureActiveMobilePolicyAL(int subId, String subscriberId) {
// Poke around to see if we already have a policy
final NetworkIdentity probeIdent = new NetworkIdentity(TYPE_MOBILE,
- TelephonyManager.NETWORK_TYPE_UNKNOWN, subscriberId, null, false, true);
+ TelephonyManager.NETWORK_TYPE_UNKNOWN, subscriberId, null, false, true, true);
for (int i = mNetworkPolicy.size() - 1; i >= 0; i--) {
final NetworkTemplate template = mNetworkPolicy.keyAt(i);
if (template.matches(probeIdent)) {
@@ -2793,12 +2942,49 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
handleNetworkPoliciesUpdateAL(true);
}
}
+
+ final Intent intent = new Intent(SubscriptionManager.ACTION_SUBSCRIPTION_PLANS_CHANGED);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ intent.putExtra(SubscriptionManager.EXTRA_SUBSCRIPTION_INDEX, subId);
+ mContext.sendBroadcast(intent, android.Manifest.permission.MANAGE_SUBSCRIPTION_PLANS);
} finally {
Binder.restoreCallingIdentity(token);
}
}
@Override
+ public String getSubscriptionPlansOwner(int subId) {
+ if (UserHandle.getCallingAppId() != android.os.Process.SYSTEM_UID) {
+ throw new SecurityException();
+ }
+
+ synchronized (mNetworkPoliciesSecondLock) {
+ return mSubscriptionPlansOwner.get(subId);
+ }
+ }
+
+ @Override
+ public void setSubscriptionOverride(int subId, int overrideMask, int overrideValue,
+ long timeoutMillis, String callingPackage) {
+ enforceSubscriptionPlanAccess(subId, Binder.getCallingUid(), callingPackage);
+
+ // We can only override when carrier told us about plans
+ synchronized (mNetworkPoliciesSecondLock) {
+ if (ArrayUtils.isEmpty(mSubscriptionPlans.get(subId))) {
+ throw new IllegalStateException(
+ "Must provide SubscriptionPlan information before overriding");
+ }
+ }
+
+ mHandler.sendMessage(mHandler.obtainMessage(MSG_SUBSCRIPTION_OVERRIDE,
+ overrideMask, overrideValue, subId));
+ if (timeoutMillis > 0) {
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SUBSCRIPTION_OVERRIDE,
+ overrideMask, 0, subId), timeoutMillis);
+ }
+ }
+
+ @Override
protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return;
@@ -2938,7 +3124,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
if (state <= ActivityManager.PROCESS_STATE_TOP) {
fout.print(" (fg)");
} else {
- fout.print(state <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE
+ fout.print(state <= ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE
? " (fg svc)" : " (bg)");
}
@@ -2963,6 +3149,15 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
}
fout.decreaseIndent();
+ fout.println("Admin restricted uids for metered data:");
+ fout.increaseIndent();
+ size = mMeteredRestrictedUids.size();
+ for (int i = 0; i < size; ++i) {
+ fout.print("u" + mMeteredRestrictedUids.keyAt(i) + ": ");
+ fout.println(mMeteredRestrictedUids.valueAt(i));
+ }
+ fout.decreaseIndent();
+
mLogger.dumpLogs(fout);
}
}
@@ -3531,6 +3726,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
final int uidPolicy = mUidPolicy.get(uid, POLICY_NONE);
final int oldUidRules = mUidRules.get(uid, RULE_NONE);
final boolean isForeground = isUidForegroundOnRestrictBackgroundUL(uid);
+ final boolean isRestrictedByAdmin = isRestrictedByAdminUL(uid);
final boolean isBlacklisted = (uidPolicy & POLICY_REJECT_METERED_BACKGROUND) != 0;
final boolean isWhitelisted = (uidPolicy & POLICY_ALLOW_METERED_BACKGROUND) != 0;
@@ -3538,7 +3734,9 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
int newRule = RULE_NONE;
// First step: define the new rule based on user restrictions and foreground state.
- if (isForeground) {
+ if (isRestrictedByAdmin) {
+ newRule = RULE_REJECT_METERED;
+ } else if (isForeground) {
if (isBlacklisted || (mRestrictBackground && !isWhitelisted)) {
newRule = RULE_TEMPORARY_ALLOW_METERED;
} else if (isWhitelisted) {
@@ -3558,6 +3756,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
+ ": isForeground=" +isForeground
+ ", isBlacklisted=" + isBlacklisted
+ ", isWhitelisted=" + isWhitelisted
+ + ", isRestrictedByAdmin=" + isRestrictedByAdmin
+ ", oldRule=" + uidRulesToString(oldRule)
+ ", newRule=" + uidRulesToString(newRule)
+ ", newUidRules=" + uidRulesToString(newUidRules)
@@ -3593,13 +3792,13 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
if (!isWhitelisted) {
setMeteredNetworkWhitelist(uid, false);
}
- if (isBlacklisted) {
+ if (isBlacklisted || isRestrictedByAdmin) {
setMeteredNetworkBlacklist(uid, true);
}
} else if (hasRule(newRule, RULE_REJECT_METERED)
|| hasRule(oldRule, RULE_REJECT_METERED)) {
// Flip state because app was explicitly added or removed to blacklist.
- setMeteredNetworkBlacklist(uid, isBlacklisted);
+ setMeteredNetworkBlacklist(uid, (isBlacklisted || isRestrictedByAdmin));
if (hasRule(oldRule, RULE_REJECT_METERED) && isWhitelisted) {
// Since blacklist prevails over whitelist, we need to handle the special case
// where app is whitelisted and blacklisted at the same time (although such
@@ -3616,6 +3815,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
+ ": foreground=" + isForeground
+ ", whitelisted=" + isWhitelisted
+ ", blacklisted=" + isBlacklisted
+ + ", isRestrictedByAdmin=" + isRestrictedByAdmin
+ ", newRule=" + uidRulesToString(newUidRules)
+ ", oldRule=" + uidRulesToString(oldUidRules));
}
@@ -3803,6 +4003,16 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
}
}
+ private void dispatchSubscriptionOverride(INetworkPolicyListener listener, int subId,
+ int overrideMask, int overrideValue) {
+ if (listener != null) {
+ try {
+ listener.onSubscriptionOverride(subId, overrideMask, overrideValue);
+ } catch (RemoteException ignored) {
+ }
+ }
+ }
+
private final Handler.Callback mHandlerCallback = new Handler.Callback() {
@Override
public boolean handleMessage(Message msg) {
@@ -3906,6 +4116,24 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
resetUidFirewallRules(msg.arg1);
return true;
}
+ case MSG_SUBSCRIPTION_OVERRIDE: {
+ final int overrideMask = msg.arg1;
+ final int overrideValue = msg.arg2;
+ final int subId = (int) msg.obj;
+ final int length = mListeners.beginBroadcast();
+ for (int i = 0; i < length; i++) {
+ final INetworkPolicyListener listener = mListeners.getBroadcastItem(i);
+ dispatchSubscriptionOverride(listener, subId, overrideMask, overrideValue);
+ }
+ mListeners.finishBroadcast();
+ return true;
+ }
+ case MSG_METERED_RESTRICTED_PACKAGES_CHANGED: {
+ final int userId = msg.arg1;
+ final Set<String> packageNames = (Set<String>) msg.obj;
+ setMeteredRestrictedPackagesInternal(packageNames, userId);
+ return true;
+ }
default: {
return false;
}
@@ -4388,12 +4616,134 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub {
updateRulesForTempWhitelistChangeUL(appId);
}
}
+
+ @Override
+ public SubscriptionPlan getSubscriptionPlan(Network network) {
+ synchronized (mNetworkPoliciesSecondLock) {
+ final int subId = getSubIdLocked(network);
+ return getPrimarySubscriptionPlanLocked(subId);
+ }
+ }
+
+ @Override
+ public long getSubscriptionOpportunisticQuota(Network network, int quotaType) {
+ synchronized (mNetworkPoliciesSecondLock) {
+ // TODO: handle splitting quota between use-cases
+ return mSubscriptionOpportunisticQuota.get(getSubIdLocked(network));
+ }
+ }
+
+ @Override
+ public void onAdminDataAvailable() {
+ mAdminDataAvailableLatch.countDown();
+ }
+
+ @Override
+ public void setMeteredRestrictedPackages(Set<String> packageNames, int userId) {
+ setMeteredRestrictedPackagesInternal(packageNames, userId);
+ }
+
+ @Override
+ public void setMeteredRestrictedPackagesAsync(Set<String> packageNames, int userId) {
+ mHandler.obtainMessage(MSG_METERED_RESTRICTED_PACKAGES_CHANGED,
+ userId, 0, packageNames).sendToTarget();
+ }
+ }
+
+ private void setMeteredRestrictedPackagesInternal(Set<String> packageNames, int userId) {
+ synchronized (mUidRulesFirstLock) {
+ final Set<Integer> newRestrictedUids = new ArraySet<>();
+ for (String packageName : packageNames) {
+ final int uid = getUidForPackage(packageName, userId);
+ if (uid >= 0) {
+ newRestrictedUids.add(uid);
+ }
+ }
+ final Set<Integer> oldRestrictedUids = mMeteredRestrictedUids.get(userId);
+ mMeteredRestrictedUids.put(userId, newRestrictedUids);
+ handleRestrictedPackagesChangeUL(oldRestrictedUids, newRestrictedUids);
+ mLogger.meteredRestrictedPkgsChanged(newRestrictedUids);
+ }
+ }
+
+ private int getUidForPackage(String packageName, int userId) {
+ try {
+ return mContext.getPackageManager().getPackageUidAsUser(packageName,
+ PackageManager.MATCH_KNOWN_PACKAGES, userId);
+ } catch (NameNotFoundException e) {
+ return -1;
+ }
+ }
+
+ private int parseSubId(NetworkState state) {
+ // TODO: moved to using a legitimate NetworkSpecifier instead of string parsing
+ int subId = INVALID_SUBSCRIPTION_ID;
+ if (state != null && state.networkCapabilities != null
+ && state.networkCapabilities.hasTransport(TRANSPORT_CELLULAR)) {
+ NetworkSpecifier spec = state.networkCapabilities.getNetworkSpecifier();
+ if (spec instanceof StringNetworkSpecifier) {
+ try {
+ subId = Integer.parseInt(((StringNetworkSpecifier) spec).specifier);
+ } catch (NumberFormatException e) {
+ }
+ }
+ }
+ return subId;
+ }
+
+ private int getSubIdLocked(Network network) {
+ return mNetIdToSubId.get(network.netId, INVALID_SUBSCRIPTION_ID);
+ }
+
+ private SubscriptionPlan getPrimarySubscriptionPlanLocked(int subId) {
+ final SubscriptionPlan[] plans = mSubscriptionPlans.get(subId);
+ return ArrayUtils.isEmpty(plans) ? null : plans[0];
+ }
+
+ /**
+ * This will only ever be called once - during device boot.
+ */
+ private void waitForAdminData() {
+ if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)) {
+ ConcurrentUtils.waitForCountDownNoInterrupt(mAdminDataAvailableLatch,
+ WAIT_FOR_ADMIN_DATA_TIMEOUT_MS, "Wait for admin data");
+ }
+ }
+
+ private void handleRestrictedPackagesChangeUL(Set<Integer> oldRestrictedUids,
+ Set<Integer> newRestrictedUids) {
+ if (oldRestrictedUids == null) {
+ for (int uid : newRestrictedUids) {
+ updateRulesForDataUsageRestrictionsUL(uid);
+ }
+ return;
+ }
+ for (int uid : oldRestrictedUids) {
+ if (!newRestrictedUids.contains(uid)) {
+ updateRulesForDataUsageRestrictionsUL(uid);
+ }
+ }
+ for (int uid : newRestrictedUids) {
+ if (!oldRestrictedUids.contains(uid)) {
+ updateRulesForDataUsageRestrictionsUL(uid);
+ }
+ }
+ }
+
+ private boolean isRestrictedByAdminUL(int uid) {
+ final Set<Integer> restrictedUids = mMeteredRestrictedUids.get(
+ UserHandle.getUserId(uid));
+ return restrictedUids != null && restrictedUids.contains(uid);
}
private static boolean hasRule(int uidRules, int rule) {
return (uidRules & rule) != 0;
}
+ private static @NonNull NetworkState[] defeatNullable(@Nullable NetworkState[] val) {
+ return (val != null) ? val : new NetworkState[0];
+ }
+
private class NotificationId {
private final String mTag;
private final int mId;
diff --git a/com/android/server/net/NetworkStatsCollection.java b/com/android/server/net/NetworkStatsCollection.java
index 4ceb592a..961a4517 100644
--- a/com/android/server/net/NetworkStatsCollection.java
+++ b/com/android/server/net/NetworkStatsCollection.java
@@ -17,6 +17,8 @@
package com.android.server.net;
import static android.net.NetworkStats.IFACE_ALL;
+import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
import static android.net.NetworkStats.METERED_NO;
import static android.net.NetworkStats.METERED_YES;
import static android.net.NetworkStats.ROAMING_NO;
@@ -364,6 +366,8 @@ public class NetworkStatsCollection implements FileRotator.Reader {
entry.uid = key.uid;
entry.set = key.set;
entry.tag = key.tag;
+ entry.defaultNetwork = key.ident.areAllMembersOnDefaultNetwork() ?
+ DEFAULT_NETWORK_YES : DEFAULT_NETWORK_NO;
entry.metered = key.ident.isAnyMemberMetered() ? METERED_YES : METERED_NO;
entry.roaming = key.ident.isAnyMemberRoaming() ? ROAMING_YES : ROAMING_NO;
entry.rxBytes = historyEntry.rxBytes;
diff --git a/com/android/server/net/NetworkStatsService.java b/com/android/server/net/NetworkStatsService.java
index db61ef5c..bfc150e1 100644
--- a/com/android/server/net/NetworkStatsService.java
+++ b/com/android/server/net/NetworkStatsService.java
@@ -25,6 +25,7 @@ import static android.content.Intent.ACTION_USER_REMOVED;
import static android.content.Intent.EXTRA_UID;
import static android.net.ConnectivityManager.ACTION_TETHER_STATE_CHANGED;
import static android.net.ConnectivityManager.isNetworkTypeMobile;
+import static android.net.NetworkStats.DEFAULT_NETWORK_ALL;
import static android.net.NetworkStats.IFACE_ALL;
import static android.net.NetworkStats.METERED_ALL;
import static android.net.NetworkStats.ROAMING_ALL;
@@ -83,6 +84,7 @@ import android.net.INetworkManagementEventObserver;
import android.net.INetworkStatsService;
import android.net.INetworkStatsSession;
import android.net.LinkProperties;
+import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkIdentity;
import android.net.NetworkInfo;
@@ -231,14 +233,24 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
private final Object mStatsLock = new Object();
/** Set of currently active ifaces. */
+ @GuardedBy("mStatsLock")
private final ArrayMap<String, NetworkIdentitySet> mActiveIfaces = new ArrayMap<>();
+
/** Set of currently active ifaces for UID stats. */
+ @GuardedBy("mStatsLock")
private final ArrayMap<String, NetworkIdentitySet> mActiveUidIfaces = new ArrayMap<>();
+
/** Current default active iface. */
private String mActiveIface;
+
/** Set of any ifaces associated with mobile networks since boot. */
+ @GuardedBy("mStatsLock")
private String[] mMobileIfaces = new String[0];
+ /** Set of all ifaces currently used by traffic that does not explicitly specify a Network. */
+ @GuardedBy("mStatsLock")
+ private Network[] mDefaultNetworks = new Network[0];
+
private final DropBoxNonMonotonicObserver mNonMonotonicObserver =
new DropBoxNonMonotonicObserver();
@@ -666,9 +678,9 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
final NetworkStatsHistory.Entry entry = history.getValues(start, end, now, null);
final NetworkStats stats = new NetworkStats(end - start, 1);
- stats.addValues(new NetworkStats.Entry(IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE, METERED_ALL,
- ROAMING_ALL, entry.rxBytes, entry.rxPackets, entry.txBytes, entry.txPackets,
- entry.operations));
+ stats.addValues(new NetworkStats.Entry(IFACE_ALL, UID_ALL, SET_ALL, TAG_NONE,
+ METERED_ALL, ROAMING_ALL, DEFAULT_NETWORK_ALL, entry.rxBytes, entry.rxPackets,
+ entry.txBytes, entry.txPackets, entry.operations));
return stats;
}
@@ -779,13 +791,13 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
}
@Override
- public void forceUpdateIfaces() {
+ public void forceUpdateIfaces(Network[] defaultNetworks) {
mContext.enforceCallingOrSelfPermission(READ_NETWORK_USAGE_HISTORY, TAG);
assertBandwidthControlEnabled();
final long token = Binder.clearCallingIdentity();
try {
- updateIfaces();
+ updateIfaces(defaultNetworks);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -875,17 +887,21 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
@Override
public long getUidStats(int uid, int type) {
- return nativeGetUidStat(uid, type);
+ return nativeGetUidStat(uid, type, checkBpfStatsEnable());
}
@Override
public long getIfaceStats(String iface, int type) {
- return nativeGetIfaceStat(iface, type);
+ return nativeGetIfaceStat(iface, type, checkBpfStatsEnable());
}
@Override
public long getTotalStats(int type) {
- return nativeGetTotalStat(type);
+ return nativeGetTotalStat(type, checkBpfStatsEnable());
+ }
+
+ private boolean checkBpfStatsEnable() {
+ return new File("/sys/fs/bpf/traffic_uid_stats_map").exists();
}
/**
@@ -996,11 +1012,11 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
}
};
- private void updateIfaces() {
+ private void updateIfaces(Network[] defaultNetworks) {
synchronized (mStatsLock) {
mWakeLock.acquire();
try {
- updateIfacesLocked();
+ updateIfacesLocked(defaultNetworks);
} finally {
mWakeLock.release();
}
@@ -1013,7 +1029,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
* are active on a single {@code iface}, they are combined under a single
* {@link NetworkIdentitySet}.
*/
- private void updateIfacesLocked() {
+ private void updateIfacesLocked(Network[] defaultNetworks) {
if (!mSystemReady) return;
if (LOGV) Slog.v(TAG, "updateIfacesLocked()");
@@ -1040,12 +1056,18 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
// Rebuild active interfaces based on connected networks
mActiveIfaces.clear();
mActiveUidIfaces.clear();
+ if (defaultNetworks != null) {
+ // Caller is ConnectivityService. Update the list of default networks.
+ mDefaultNetworks = defaultNetworks;
+ }
final ArraySet<String> mobileIfaces = new ArraySet<>();
for (NetworkState state : states) {
if (state.networkInfo.isConnected()) {
final boolean isMobile = isNetworkTypeMobile(state.networkInfo.getType());
- final NetworkIdentity ident = NetworkIdentity.buildNetworkIdentity(mContext, state);
+ final boolean isDefault = ArrayUtils.contains(mDefaultNetworks, state.network);
+ final NetworkIdentity ident = NetworkIdentity.buildNetworkIdentity(mContext, state,
+ isDefault);
// Traffic occurring on the base interface is always counted for
// both total usage and UID details.
@@ -1065,7 +1087,8 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
// Copy the identify from IMS one but mark it as metered.
NetworkIdentity vtIdent = new NetworkIdentity(ident.getType(),
ident.getSubType(), ident.getSubscriberId(), ident.getNetworkId(),
- ident.getRoaming(), true);
+ ident.getRoaming(), true /* metered */,
+ true /* onDefaultNetwork */);
findOrCreateNetworkIdentitySet(mActiveIfaces, VT_INTERFACE).add(vtIdent);
findOrCreateNetworkIdentitySet(mActiveUidIfaces, VT_INTERFACE).add(vtIdent);
}
@@ -1511,7 +1534,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
return true;
}
case MSG_UPDATE_IFACES: {
- mService.updateIfaces();
+ mService.updateIfaces(null);
return true;
}
case MSG_REGISTER_GLOBAL_ALERT: {
@@ -1649,7 +1672,7 @@ public class NetworkStatsService extends INetworkStatsService.Stub {
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);
+ private static native long nativeGetTotalStat(int type, boolean useBpfStats);
+ private static native long nativeGetIfaceStat(String iface, int type, boolean useBpfStats);
+ private static native long nativeGetUidStat(int uid, int type, boolean useBpfStats);
}
diff --git a/com/android/server/net/watchlist/NetworkWatchlistService.java b/com/android/server/net/watchlist/NetworkWatchlistService.java
index 171703ac..7165e600 100644
--- a/com/android/server/net/watchlist/NetworkWatchlistService.java
+++ b/com/android/server/net/watchlist/NetworkWatchlistService.java
@@ -16,23 +16,22 @@
package com.android.server.net.watchlist;
+import android.annotation.Nullable;
import android.content.Context;
-import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.net.IIpConnectivityMetrics;
import android.net.INetdEventCallback;
-import android.net.NetworkWatchlistManager;
import android.net.metrics.IpConnectivityLog;
import android.os.Binder;
import android.os.Process;
-import android.os.SharedMemory;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
+import android.provider.Settings;
import android.text.TextUtils;
import android.util.Slog;
import com.android.internal.R;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.DumpUtils;
import com.android.internal.net.INetworkWatchlistManager;
@@ -40,9 +39,7 @@ import com.android.server.ServiceThread;
import com.android.server.SystemService;
import java.io.FileDescriptor;
-import java.io.IOException;
import java.io.PrintWriter;
-import java.util.List;
/**
* Implementation of network watchlist service.
@@ -52,9 +49,6 @@ public class NetworkWatchlistService extends INetworkWatchlistManager.Stub {
private static final String TAG = NetworkWatchlistService.class.getSimpleName();
static final boolean DEBUG = false;
- private static final String PROPERTY_NETWORK_WATCHLIST_ENABLED =
- "ro.network_watchlist_enabled";
-
private static final int MAX_NUM_OF_WATCHLIST_DIGESTS = 10000;
public static class Lifecycle extends SystemService {
@@ -66,8 +60,10 @@ public class NetworkWatchlistService extends INetworkWatchlistManager.Stub {
@Override
public void onStart() {
- if (!SystemProperties.getBoolean(PROPERTY_NETWORK_WATCHLIST_ENABLED, false)) {
+ if (Settings.Global.getInt(getContext().getContentResolver(),
+ Settings.Global.NETWORK_WATCHLIST_ENABLED, 0) == 0) {
// Watchlist service is disabled
+ Slog.i(TAG, "Network Watchlist service is disabled");
return;
}
mService = new NetworkWatchlistService(getContext());
@@ -76,11 +72,13 @@ public class NetworkWatchlistService extends INetworkWatchlistManager.Stub {
@Override
public void onBootPhase(int phase) {
- if (!SystemProperties.getBoolean(PROPERTY_NETWORK_WATCHLIST_ENABLED, false)) {
- // Watchlist service is disabled
- return;
- }
if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
+ if (Settings.Global.getInt(getContext().getContentResolver(),
+ Settings.Global.NETWORK_WATCHLIST_ENABLED, 0) == 0) {
+ // Watchlist service is disabled
+ Slog.i(TAG, "Network Watchlist service is disabled");
+ return;
+ }
try {
mService.initIpConnectivityMetrics();
mService.startWatchlistLogging();
@@ -92,10 +90,11 @@ public class NetworkWatchlistService extends INetworkWatchlistManager.Stub {
}
}
+ @GuardedBy("mLoggingSwitchLock")
private volatile boolean mIsLoggingEnabled = false;
private final Object mLoggingSwitchLock = new Object();
- private final WatchlistSettings mSettings;
+ private final WatchlistConfig mConfig;
private final Context mContext;
// Separate thread to handle expensive watchlist logging work.
@@ -108,7 +107,7 @@ public class NetworkWatchlistService extends INetworkWatchlistManager.Stub {
public NetworkWatchlistService(Context context) {
mContext = context;
- mSettings = WatchlistSettings.getInstance();
+ mConfig = WatchlistConfig.getInstance();
mHandlerThread = new ServiceThread(TAG, Process.THREAD_PRIORITY_BACKGROUND,
/* allowIo */ false);
mHandlerThread.start();
@@ -122,7 +121,7 @@ public class NetworkWatchlistService extends INetworkWatchlistManager.Stub {
NetworkWatchlistService(Context context, ServiceThread handlerThread,
WatchlistLoggingHandler handler, IIpConnectivityMetrics ipConnectivityMetrics) {
mContext = context;
- mSettings = WatchlistSettings.getInstance();
+ mConfig = WatchlistConfig.getInstance();
mHandlerThread = handlerThread;
mNetworkWatchlistHandler = handler;
mIpConnectivityMetrics = ipConnectivityMetrics;
@@ -212,6 +211,12 @@ public class NetworkWatchlistService extends INetworkWatchlistManager.Stub {
return stopWatchlistLoggingImpl();
}
+ @Nullable
+ @Override
+ public byte[] getWatchlistConfigHash() {
+ return mConfig.getWatchlistConfigHash();
+ }
+
private void enforceWatchlistLoggingPermission() {
final int uid = Binder.getCallingUid();
if (uid != Process.SYSTEM_UID) {
@@ -220,36 +225,11 @@ public class NetworkWatchlistService extends INetworkWatchlistManager.Stub {
}
}
- /**
- * Set a new network watchlist.
- * This method should be called by ConfigUpdater only.
- *
- * @return True if network watchlist is updated.
- */
- public boolean setNetworkSecurityWatchlist(List<byte[]> domainsCrc32Digests,
- List<byte[]> domainsSha256Digests,
- List<byte[]> ipAddressesCrc32Digests,
- List<byte[]> ipAddressesSha256Digests) {
- Slog.i(TAG, "Setting network watchlist");
- if (domainsCrc32Digests == null || domainsSha256Digests == null
- || ipAddressesCrc32Digests == null || ipAddressesSha256Digests == null) {
- Slog.e(TAG, "Parameters cannot be null");
- return false;
- }
- if (domainsCrc32Digests.size() != domainsSha256Digests.size()
- || ipAddressesCrc32Digests.size() != ipAddressesSha256Digests.size()) {
- Slog.e(TAG, "Must need to have the same number of CRC32 and SHA256 digests");
- return false;
- }
- if (domainsSha256Digests.size() + ipAddressesSha256Digests.size()
- > MAX_NUM_OF_WATCHLIST_DIGESTS) {
- Slog.e(TAG, "Total watchlist size cannot exceed " + MAX_NUM_OF_WATCHLIST_DIGESTS);
- return false;
- }
- mSettings.writeSettingsToDisk(domainsCrc32Digests, domainsSha256Digests,
- ipAddressesCrc32Digests, ipAddressesSha256Digests);
- Slog.i(TAG, "Set network watchlist: Success");
- return true;
+ @Override
+ public void reloadWatchlist() throws RemoteException {
+ enforceWatchlistLoggingPermission();
+ Slog.i(TAG, "Reloading watchlist");
+ mConfig.reloadConfig();
}
@Override
@@ -261,7 +241,7 @@ public class NetworkWatchlistService extends INetworkWatchlistManager.Stub {
@Override
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
- mSettings.dump(fd, pw, args);
+ mConfig.dump(fd, pw, args);
}
}
diff --git a/com/android/server/net/watchlist/PrivacyUtils.java b/com/android/server/net/watchlist/PrivacyUtils.java
new file mode 100644
index 00000000..c1231fa3
--- /dev/null
+++ b/com/android/server/net/watchlist/PrivacyUtils.java
@@ -0,0 +1,103 @@
+/*
+ * 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.net.watchlist;
+
+import android.privacy.DifferentialPrivacyEncoder;
+import android.privacy.internal.longitudinalreporting.LongitudinalReportingConfig;
+import android.privacy.internal.longitudinalreporting.LongitudinalReportingEncoder;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper class to apply differential privacy to watchlist reports.
+ */
+class PrivacyUtils {
+
+ private static final String TAG = "PrivacyUtils";
+
+ /**
+ * Parameters used for encoding watchlist reports.
+ * These numbers are optimal parameters for protecting privacy with good utility.
+ *
+ * TODO: Add links to explain the math behind.
+ */
+ private static final String ENCODER_ID_PREFIX = "watchlist_encoder:";
+ private static final double PROB_F = 0.469;
+ private static final double PROB_P = 0.28;
+ private static final double PROB_Q = 1.0;
+
+ private PrivacyUtils() {
+ }
+
+ /**
+ * Get insecure DP encoder.
+ * Should not apply it directly on real data as seed is not randomized.
+ */
+ @VisibleForTesting
+ static DifferentialPrivacyEncoder createInsecureDPEncoderForTest(String appDigest) {
+ final LongitudinalReportingConfig config = createLongitudinalReportingConfig(appDigest);
+ return LongitudinalReportingEncoder.createInsecureEncoderForTest(config);
+ }
+
+ /**
+ * Get secure encoder to encode watchlist.
+ *
+ * Warning: If you use the same user secret and app digest, then you will get the same
+ * PRR result.
+ */
+ @VisibleForTesting
+ static DifferentialPrivacyEncoder createSecureDPEncoder(byte[] userSecret,
+ String appDigest) {
+ final LongitudinalReportingConfig config = createLongitudinalReportingConfig(appDigest);
+ return LongitudinalReportingEncoder.createEncoder(config, userSecret);
+ }
+
+ /**
+ * Get DP config for encoding watchlist reports.
+ */
+ private static LongitudinalReportingConfig createLongitudinalReportingConfig(String appDigest) {
+ return new LongitudinalReportingConfig(ENCODER_ID_PREFIX + appDigest, PROB_F, PROB_P,
+ PROB_Q);
+ }
+
+ /**
+ * Create a map that stores appDigest, encoded_visitedWatchlist pairs.
+ */
+ @VisibleForTesting
+ static Map<String, Boolean> createDpEncodedReportMap(boolean isSecure, byte[] userSecret,
+ List<String> appDigestList, WatchlistReportDbHelper.AggregatedResult aggregatedResult) {
+ final int appDigestListSize = appDigestList.size();
+ final HashMap<String, Boolean> resultMap = new HashMap<>(appDigestListSize);
+ for (int i = 0; i < appDigestListSize; i++) {
+ final String appDigest = appDigestList.get(i);
+ // Each app needs to have different PRR result, hence we use appDigest as encoder Id.
+ final DifferentialPrivacyEncoder encoder = isSecure
+ ? createSecureDPEncoder(userSecret, appDigest)
+ : createInsecureDPEncoderForTest(appDigest);
+ final boolean visitedWatchlist = aggregatedResult.appDigestList.contains(appDigest);
+ // Get the least significant bit of first byte, and set result to True if it is 1
+ boolean encodedVisitedWatchlist = ((int) encoder.encodeBoolean(visitedWatchlist)[0]
+ & 0x1) == 0x1;
+ resultMap.put(appDigest, encodedVisitedWatchlist);
+ }
+ return resultMap;
+ }
+}
diff --git a/com/android/server/net/watchlist/ReportEncoder.java b/com/android/server/net/watchlist/ReportEncoder.java
new file mode 100644
index 00000000..5d7ff5a7
--- /dev/null
+++ b/com/android/server/net/watchlist/ReportEncoder.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 com.android.server.net.watchlist;
+
+import android.annotation.Nullable;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.HexDump;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper class to encode and generate serialized DP encoded watchlist report.
+ *
+ * <p>Serialized report data structure:
+ * [4 bytes magic number][4_bytes_report_version_code][32_bytes_watchlist_hash]
+ * [app_1_digest_byte_array][app_1_encoded_visited_cnc_byte]
+ * [app_2_digest_byte_array][app_2_encoded_visited_cnc_byte]
+ * ...
+ *
+ * Total size: 4 + 4 + 32 + (32+1)*N, where N = number of digests
+ */
+class ReportEncoder {
+
+ private static final String TAG = "ReportEncoder";
+
+ // Report header magic number
+ private static final byte[] MAGIC_NUMBER = {(byte) 0x8D, (byte) 0x37, (byte) 0x0A, (byte) 0xAC};
+ // Report version number, as file format / parameters can be changed in later version, we need
+ // to have versioning on watchlist report format
+ private static final byte[] REPORT_VERSION = {(byte) 0x00, (byte) 0x01};
+
+ private static final int WATCHLIST_HASH_SIZE = 32;
+ private static final int APP_DIGEST_SIZE = 32;
+
+ /**
+ * Apply DP on watchlist results, and generate a serialized watchlist report ready to store
+ * in DropBox.
+ */
+ static byte[] encodeWatchlistReport(WatchlistConfig config, byte[] userSecret,
+ List<String> appDigestList, WatchlistReportDbHelper.AggregatedResult aggregatedResult) {
+ Map<String, Boolean> resultMap = PrivacyUtils.createDpEncodedReportMap(
+ config.isConfigSecure(), userSecret, appDigestList, aggregatedResult);
+ return serializeReport(config, resultMap);
+ }
+
+ /**
+ * Convert DP encoded watchlist report into byte[] format.
+ * TODO: Serialize it using protobuf
+ *
+ * @param encodedReportMap DP encoded watchlist report.
+ * @return Watchlist report in byte[] format, which will be shared in Dropbox. Null if
+ * watchlist report cannot be generated.
+ */
+ @Nullable
+ @VisibleForTesting
+ static byte[] serializeReport(WatchlistConfig config,
+ Map<String, Boolean> encodedReportMap) {
+ // TODO: Handle watchlist config changed case
+ final byte[] watchlistHash = config.getWatchlistConfigHash();
+ if (watchlistHash == null) {
+ Log.e(TAG, "No watchlist hash");
+ return null;
+ }
+ if (watchlistHash.length != WATCHLIST_HASH_SIZE) {
+ Log.e(TAG, "Unexpected hash length");
+ return null;
+ }
+ final int reportMapSize = encodedReportMap.size();
+ final byte[] outputReport =
+ new byte[MAGIC_NUMBER.length + REPORT_VERSION.length + WATCHLIST_HASH_SIZE
+ + reportMapSize * (APP_DIGEST_SIZE + /* Result */ 1)];
+ final List<String> sortedKeys = new ArrayList(encodedReportMap.keySet());
+ Collections.sort(sortedKeys);
+
+ int offset = 0;
+
+ // Set magic number to report
+ System.arraycopy(MAGIC_NUMBER, 0, outputReport, offset, MAGIC_NUMBER.length);
+ offset += MAGIC_NUMBER.length;
+
+ // Set report version to report
+ System.arraycopy(REPORT_VERSION, 0, outputReport, offset, REPORT_VERSION.length);
+ offset += REPORT_VERSION.length;
+
+ // Set watchlist hash to report
+ System.arraycopy(watchlistHash, 0, outputReport, offset, watchlistHash.length);
+ offset += watchlistHash.length;
+
+ // Set app digest, encoded_isPha pair to report
+ for (int i = 0; i < reportMapSize; i++) {
+ String key = sortedKeys.get(i);
+ byte[] digest = HexDump.hexStringToByteArray(key);
+ boolean isPha = encodedReportMap.get(key);
+ System.arraycopy(digest, 0, outputReport, offset, APP_DIGEST_SIZE);
+ offset += digest.length;
+ outputReport[offset] = (byte) (isPha ? 1 : 0);
+ offset += 1;
+ }
+ if (outputReport.length != offset) {
+ Log.e(TAG, "Watchlist report size does not match! Offset: " + offset + ", report size: "
+ + outputReport.length);
+
+ }
+ return outputReport;
+ }
+}
diff --git a/com/android/server/net/watchlist/WatchlistConfig.java b/com/android/server/net/watchlist/WatchlistConfig.java
new file mode 100644
index 00000000..7387ad4f
--- /dev/null
+++ b/com/android/server/net/watchlist/WatchlistConfig.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.net.watchlist;
+
+import android.util.AtomicFile;
+import android.util.Log;
+import android.util.Slog;
+import android.util.Xml;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.HexDump;
+import com.android.internal.util.XmlUtils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.zip.CRC32;
+
+/**
+ * Class for watchlist config operations, like setting watchlist, query if a domain
+ * exists in watchlist.
+ */
+class WatchlistConfig {
+ private static final String TAG = "WatchlistConfig";
+
+ // Watchlist config that pushed by ConfigUpdater.
+ private static final String NETWORK_WATCHLIST_DB_PATH =
+ "/data/misc/network_watchlist/network_watchlist.xml";
+
+ // Hash for null / unknown config, a 32 byte array filled with content 0x00
+ private static final byte[] UNKNOWN_CONFIG_HASH = new byte[32];
+
+ private static class XmlTags {
+ private static final String WATCHLIST_CONFIG = "watchlist-config";
+ private static final String SHA256_DOMAIN = "sha256-domain";
+ private static final String CRC32_DOMAIN = "crc32-domain";
+ private static final String SHA256_IP = "sha256-ip";
+ private static final String CRC32_IP = "crc32-ip";
+ private static final String HASH = "hash";
+ }
+
+ private static class CrcShaDigests {
+ final HarmfulDigests crc32Digests;
+ final HarmfulDigests sha256Digests;
+
+ public CrcShaDigests(HarmfulDigests crc32Digests, HarmfulDigests sha256Digests) {
+ this.crc32Digests = crc32Digests;
+ this.sha256Digests = sha256Digests;
+ }
+ }
+
+ /*
+ * This is always true unless watchlist is being set by adb command, then it will be false
+ * until next reboot.
+ */
+ private boolean mIsSecureConfig = true;
+
+ private final static WatchlistConfig sInstance = new WatchlistConfig();
+ private final File mXmlFile;
+
+ private volatile CrcShaDigests mDomainDigests;
+ private volatile CrcShaDigests mIpDigests;
+
+ public static WatchlistConfig getInstance() {
+ return sInstance;
+ }
+
+ private WatchlistConfig() {
+ this(new File(NETWORK_WATCHLIST_DB_PATH));
+ }
+
+ @VisibleForTesting
+ protected WatchlistConfig(File xmlFile) {
+ mXmlFile = xmlFile;
+ reloadConfig();
+ }
+
+ /**
+ * Reload watchlist by reading config file.
+ */
+ public void reloadConfig() {
+ try (FileInputStream stream = new FileInputStream(mXmlFile)){
+ final List<byte[]> crc32DomainList = new ArrayList<>();
+ final List<byte[]> sha256DomainList = new ArrayList<>();
+ final List<byte[]> crc32IpList = new ArrayList<>();
+ final List<byte[]> sha256IpList = new ArrayList<>();
+
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(stream, StandardCharsets.UTF_8.name());
+ parser.nextTag();
+ parser.require(XmlPullParser.START_TAG, null, XmlTags.WATCHLIST_CONFIG);
+ while (parser.nextTag() == XmlPullParser.START_TAG) {
+ String tagName = parser.getName();
+ switch (tagName) {
+ case XmlTags.CRC32_DOMAIN:
+ parseHashes(parser, tagName, crc32DomainList);
+ break;
+ case XmlTags.CRC32_IP:
+ parseHashes(parser, tagName, crc32IpList);
+ break;
+ case XmlTags.SHA256_DOMAIN:
+ parseHashes(parser, tagName, sha256DomainList);
+ break;
+ case XmlTags.SHA256_IP:
+ parseHashes(parser, tagName, sha256IpList);
+ break;
+ default:
+ Log.w(TAG, "Unknown element: " + parser.getName());
+ XmlUtils.skipCurrentTag(parser);
+ }
+ }
+ parser.require(XmlPullParser.END_TAG, null, XmlTags.WATCHLIST_CONFIG);
+ mDomainDigests = new CrcShaDigests(new HarmfulDigests(crc32DomainList),
+ new HarmfulDigests(sha256DomainList));
+ mIpDigests = new CrcShaDigests(new HarmfulDigests(crc32IpList),
+ new HarmfulDigests(sha256IpList));
+ Log.i(TAG, "Reload watchlist done");
+ } catch (IllegalStateException | NullPointerException | NumberFormatException |
+ XmlPullParserException | IOException | IndexOutOfBoundsException e) {
+ Slog.e(TAG, "Failed parsing xml", e);
+ }
+ }
+
+ private void parseHashes(XmlPullParser parser, String tagName, List<byte[]> hashList)
+ throws IOException, XmlPullParserException {
+ parser.require(XmlPullParser.START_TAG, null, tagName);
+ // Get all the hashes for this tag
+ while (parser.nextTag() == XmlPullParser.START_TAG) {
+ parser.require(XmlPullParser.START_TAG, null, XmlTags.HASH);
+ byte[] hash = HexDump.hexStringToByteArray(parser.nextText());
+ parser.require(XmlPullParser.END_TAG, null, XmlTags.HASH);
+ hashList.add(hash);
+ }
+ parser.require(XmlPullParser.END_TAG, null, tagName);
+ }
+
+ public boolean containsDomain(String domain) {
+ final CrcShaDigests domainDigests = mDomainDigests;
+ if (domainDigests == null) {
+ // mDomainDigests is not initialized
+ return false;
+ }
+ // First it does a quick CRC32 check.
+ final byte[] crc32 = getCrc32(domain);
+ if (!domainDigests.crc32Digests.contains(crc32)) {
+ return false;
+ }
+ // Now we do a slow SHA256 check.
+ final byte[] sha256 = getSha256(domain);
+ return domainDigests.sha256Digests.contains(sha256);
+ }
+
+ public boolean containsIp(String ip) {
+ final CrcShaDigests ipDigests = mIpDigests;
+ if (ipDigests == null) {
+ // mIpDigests is not initialized
+ return false;
+ }
+ // First it does a quick CRC32 check.
+ final byte[] crc32 = getCrc32(ip);
+ if (!ipDigests.crc32Digests.contains(crc32)) {
+ return false;
+ }
+ // Now we do a slow SHA256 check.
+ final byte[] sha256 = getSha256(ip);
+ return ipDigests.sha256Digests.contains(sha256);
+ }
+
+
+ /** Get CRC32 of a string
+ *
+ * TODO: Review if we should use CRC32 or other algorithms
+ */
+ private byte[] getCrc32(String str) {
+ final CRC32 crc = new CRC32();
+ crc.update(str.getBytes());
+ final long tmp = crc.getValue();
+ return new byte[]{(byte) (tmp >> 24 & 255), (byte) (tmp >> 16 & 255),
+ (byte) (tmp >> 8 & 255), (byte) (tmp & 255)};
+ }
+
+ /** Get SHA256 of a string */
+ private byte[] getSha256(String str) {
+ MessageDigest messageDigest;
+ try {
+ messageDigest = MessageDigest.getInstance("SHA256");
+ } catch (NoSuchAlgorithmException e) {
+ /* can't happen */
+ return null;
+ }
+ messageDigest.update(str.getBytes());
+ return messageDigest.digest();
+ }
+
+ public boolean isConfigSecure() {
+ return mIsSecureConfig;
+ }
+
+ public byte[] getWatchlistConfigHash() {
+ if (!mXmlFile.exists()) {
+ return UNKNOWN_CONFIG_HASH;
+ }
+ try {
+ return DigestUtils.getSha256Hash(mXmlFile);
+ } catch (IOException | NoSuchAlgorithmException e) {
+ Log.e(TAG, "Unable to get watchlist config hash", e);
+ }
+ return UNKNOWN_CONFIG_HASH;
+ }
+
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("Domain CRC32 digest list:");
+ if (mDomainDigests != null) {
+ mDomainDigests.crc32Digests.dump(fd, pw, args);
+ }
+ pw.println("Domain SHA256 digest list:");
+ if (mDomainDigests != null) {
+ mDomainDigests.sha256Digests.dump(fd, pw, args);
+ }
+ pw.println("Ip CRC32 digest list:");
+ if (mIpDigests != null) {
+ mIpDigests.crc32Digests.dump(fd, pw, args);
+ }
+ pw.println("Ip SHA256 digest list:");
+ if (mIpDigests != null) {
+ mIpDigests.sha256Digests.dump(fd, pw, args);
+ }
+ }
+}
diff --git a/com/android/server/net/watchlist/WatchlistLoggingHandler.java b/com/android/server/net/watchlist/WatchlistLoggingHandler.java
index 22475584..3b6d59e1 100644
--- a/com/android/server/net/watchlist/WatchlistLoggingHandler.java
+++ b/com/android/server/net/watchlist/WatchlistLoggingHandler.java
@@ -16,8 +16,10 @@
package com.android.server.net.watchlist;
+import android.annotation.Nullable;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Bundle;
@@ -30,14 +32,19 @@ import android.provider.Settings;
import android.text.TextUtils;
import android.util.Slog;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.HexDump;
import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
@@ -57,14 +64,17 @@ class WatchlistLoggingHandler extends Handler {
private static final String DROPBOX_TAG = "network_watchlist_report";
private final Context mContext;
+ private final @Nullable DropBoxManager mDropBoxManager;
private final ContentResolver mResolver;
private final PackageManager mPm;
private final WatchlistReportDbHelper mDbHelper;
+ private final WatchlistConfig mConfig;
private final WatchlistSettings mSettings;
// A cache for uid and apk digest mapping.
// As uid won't be reused until reboot, it's safe to assume uid is unique per signature and app.
// TODO: Use more efficient data structure.
- private final HashMap<Integer, byte[]> mCachedUidDigestMap = new HashMap<>();
+ private final ConcurrentHashMap<Integer, byte[]> mCachedUidDigestMap =
+ new ConcurrentHashMap<>();
private interface WatchlistEventKeys {
String HOST = "host";
@@ -79,7 +89,9 @@ class WatchlistLoggingHandler extends Handler {
mPm = mContext.getPackageManager();
mResolver = mContext.getContentResolver();
mDbHelper = WatchlistReportDbHelper.getInstance(context);
+ mConfig = WatchlistConfig.getInstance();
mSettings = WatchlistSettings.getInstance();
+ mDropBoxManager = mContext.getSystemService(DropBoxManager.class);
}
@Override
@@ -162,69 +174,92 @@ class WatchlistLoggingHandler extends Handler {
}
private void tryAggregateRecords() {
- if (shouldReportNetworkWatchlist()) {
- Slog.i(TAG, "Start aggregating watchlist records.");
- final DropBoxManager dbox = mContext.getSystemService(DropBoxManager.class);
- if (dbox != null && !dbox.isTagEnabled(DROPBOX_TAG)) {
- final WatchlistReportDbHelper.AggregatedResult aggregatedResult =
- mDbHelper.getAggregatedRecords();
- final byte[] encodedResult = encodeAggregatedResult(aggregatedResult);
- if (encodedResult != null) {
- addEncodedReportToDropBox(encodedResult);
- }
- }
- mDbHelper.cleanup();
- Settings.Global.putLong(mResolver, Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME,
- System.currentTimeMillis());
- } else {
+ // Check if it's necessary to generate watchlist report now.
+ if (!shouldReportNetworkWatchlist()) {
Slog.i(TAG, "No need to aggregate record yet.");
+ return;
+ }
+ Slog.i(TAG, "Start aggregating watchlist records.");
+ if (mDropBoxManager != null && mDropBoxManager.isTagEnabled(DROPBOX_TAG)) {
+ Settings.Global.putLong(mResolver,
+ Settings.Global.NETWORK_WATCHLIST_LAST_REPORT_TIME,
+ System.currentTimeMillis());
+ final WatchlistReportDbHelper.AggregatedResult aggregatedResult =
+ mDbHelper.getAggregatedRecords();
+ if (aggregatedResult == null) {
+ Slog.i(TAG, "Cannot get result from database");
+ return;
+ }
+ // Get all digests for watchlist report, it should include all installed
+ // application digests and previously recorded app digests.
+ final List<String> digestsForReport = getAllDigestsForReport(aggregatedResult);
+ final byte[] secretKey = mSettings.getPrivacySecretKey();
+ final byte[] encodedResult = ReportEncoder.encodeWatchlistReport(mConfig,
+ secretKey, digestsForReport, aggregatedResult);
+ if (encodedResult != null) {
+ addEncodedReportToDropBox(encodedResult);
+ }
}
+ mDbHelper.cleanup();
}
- private byte[] encodeAggregatedResult(
- WatchlistReportDbHelper.AggregatedResult aggregatedResult) {
- // TODO: Encode results using differential privacy.
- return null;
+ /**
+ * Get all digests for watchlist report.
+ * It should include:
+ * (1) All installed app digests. We need this because we need to ensure after DP we don't know
+ * if an app is really visited C&C site.
+ * (2) App digests that previously recorded in database.
+ */
+ private List<String> getAllDigestsForReport(WatchlistReportDbHelper.AggregatedResult record) {
+ // Step 1: Get all installed application digests.
+ final List<ApplicationInfo> apps = mContext.getPackageManager().getInstalledApplications(
+ PackageManager.MATCH_ANY_USER | PackageManager.MATCH_ALL);
+ final HashSet<String> result = new HashSet<>(apps.size() + record.appDigestCNCList.size());
+ final int size = apps.size();
+ for (int i = 0; i < size; i++) {
+ byte[] digest = getDigestFromUid(apps.get(i).uid);
+ result.add(HexDump.toHexString(digest));
+ }
+ // Step 2: Add all digests from records
+ result.addAll(record.appDigestCNCList.keySet());
+ return new ArrayList<>(result);
}
private void addEncodedReportToDropBox(byte[] encodedReport) {
- final DropBoxManager dbox = mContext.getSystemService(DropBoxManager.class);
- dbox.addData(DROPBOX_TAG, encodedReport, 0);
+ mDropBoxManager.addData(DROPBOX_TAG, encodedReport, 0);
}
/**
* Get app digest from app uid.
+ * Return null if system cannot get digest from uid.
*/
+ @Nullable
private byte[] getDigestFromUid(int uid) {
- final byte[] cachedDigest = mCachedUidDigestMap.get(uid);
- if (cachedDigest != null) {
- return cachedDigest;
- }
- final String[] packageNames = mPm.getPackagesForUid(uid);
- final int userId = UserHandle.getUserId(uid);
- if (!ArrayUtils.isEmpty(packageNames)) {
- for (String packageName : packageNames) {
- try {
- final String apkPath = mPm.getPackageInfoAsUser(packageName,
- PackageManager.MATCH_DIRECT_BOOT_AWARE
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId)
- .applicationInfo.publicSourceDir;
- if (TextUtils.isEmpty(apkPath)) {
- Slog.w(TAG, "Cannot find apkPath for " + packageName);
- continue;
+ return mCachedUidDigestMap.computeIfAbsent(uid, key -> {
+ final String[] packageNames = mPm.getPackagesForUid(key);
+ final int userId = UserHandle.getUserId(uid);
+ if (!ArrayUtils.isEmpty(packageNames)) {
+ for (String packageName : packageNames) {
+ try {
+ final String apkPath = mPm.getPackageInfoAsUser(packageName,
+ PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId)
+ .applicationInfo.publicSourceDir;
+ if (TextUtils.isEmpty(apkPath)) {
+ Slog.w(TAG, "Cannot find apkPath for " + packageName);
+ continue;
+ }
+ return DigestUtils.getSha256Hash(new File(apkPath));
+ } catch (NameNotFoundException | NoSuchAlgorithmException | IOException e) {
+ Slog.e(TAG, "Should not happen", e);
+ return null;
}
- final byte[] digest = DigestUtils.getSha256Hash(new File(apkPath));
- mCachedUidDigestMap.put(uid, digest);
- return digest;
- } catch (NameNotFoundException | NoSuchAlgorithmException | IOException e) {
- Slog.e(TAG, "Should not happen", e);
- return null;
}
+ } else {
+ Slog.e(TAG, "Should not happen");
}
- } else {
- Slog.e(TAG, "Should not happen");
- }
- return null;
+ return null;
+ });
}
/**
@@ -247,7 +282,7 @@ class WatchlistLoggingHandler extends Handler {
if (ipAddr == null) {
return false;
}
- return mSettings.containsIp(ipAddr);
+ return mConfig.containsIp(ipAddr);
}
/** Search if the host is in watchlist */
@@ -255,7 +290,7 @@ class WatchlistLoggingHandler extends Handler {
if (host == null) {
return false;
}
- return mSettings.containsDomain(host);
+ return mConfig.containsDomain(host);
}
/**
diff --git a/com/android/server/net/watchlist/WatchlistReportDbHelper.java b/com/android/server/net/watchlist/WatchlistReportDbHelper.java
index f48463f5..c73b0cf1 100644
--- a/com/android/server/net/watchlist/WatchlistReportDbHelper.java
+++ b/com/android/server/net/watchlist/WatchlistReportDbHelper.java
@@ -16,15 +16,18 @@
package com.android.server.net.watchlist;
+import android.annotation.Nullable;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
+import android.os.Environment;
import android.util.Pair;
import com.android.internal.util.HexDump;
+import java.io.File;
import java.util.ArrayList;
import java.util.GregorianCalendar;
import java.util.HashMap;
@@ -74,18 +77,28 @@ class WatchlistReportDbHelper extends SQLiteOpenHelper {
*/
public static class AggregatedResult {
// A list of digests that visited c&c domain or ip before.
- Set<String> appDigestList;
+ final Set<String> appDigestList;
// The c&c domain or ip visited before.
- String cncDomainVisited;
+ @Nullable final String cncDomainVisited;
// A list of app digests and c&c domain visited.
- HashMap<String, String> appDigestCNCList;
+ final HashMap<String, String> appDigestCNCList;
+
+ public AggregatedResult(Set<String> appDigestList, String cncDomainVisited,
+ HashMap<String, String> appDigestCNCList) {
+ this.appDigestList = appDigestList;
+ this.cncDomainVisited = cncDomainVisited;
+ this.appDigestCNCList = appDigestCNCList;
+ }
+ }
+
+ static File getSystemWatchlistDbFile() {
+ return new File(Environment.getDataSystemDirectory(), NAME);
}
private WatchlistReportDbHelper(Context context) {
- super(context, WatchlistSettings.getSystemWatchlistFile(NAME).getAbsolutePath(),
- null, VERSION);
+ super(context, getSystemWatchlistDbFile().getAbsolutePath(), null, VERSION);
// Memory optimization - close idle connections after 30s of inactivity
setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
}
@@ -143,26 +156,24 @@ class WatchlistReportDbHelper extends SQLiteOpenHelper {
WhiteListReportContract.TABLE, DIGEST_DOMAIN_PROJECTION, selectStatement,
new String[]{"" + twoDaysBefore, "" + yesterday}, null, null,
null, null);
- if (c == null || c.getCount() == 0) {
+ if (c == null) {
return null;
}
- final AggregatedResult result = new AggregatedResult();
- result.cncDomainVisited = null;
- // After aggregation, each digest maximum will have only 1 record.
- result.appDigestList = new HashSet<>();
- result.appDigestCNCList = new HashMap<>();
+ final HashSet<String> appDigestList = new HashSet<>();
+ final HashMap<String, String> appDigestCNCList = new HashMap<>();
+ String cncDomainVisited = null;
while (c.moveToNext()) {
// We use hex string here as byte[] cannot be a key in HashMap.
String digestHexStr = HexDump.toHexString(c.getBlob(INDEX_DIGEST));
String cncDomain = c.getString(INDEX_CNC_DOMAIN);
- result.appDigestList.add(digestHexStr);
- if (result.cncDomainVisited != null) {
- result.cncDomainVisited = cncDomain;
+ appDigestList.add(digestHexStr);
+ if (cncDomainVisited != null) {
+ cncDomainVisited = cncDomain;
}
- result.appDigestCNCList.put(digestHexStr, cncDomain);
+ appDigestCNCList.put(digestHexStr, cncDomain);
}
- return result;
+ return new AggregatedResult(appDigestList, cncDomainVisited, appDigestCNCList);
} finally {
if (c != null) {
c.close();
diff --git a/com/android/server/net/watchlist/WatchlistSettings.java b/com/android/server/net/watchlist/WatchlistSettings.java
index c50f0d56..b78fe4d2 100644
--- a/com/android/server/net/watchlist/WatchlistSettings.java
+++ b/com/android/server/net/watchlist/WatchlistSettings.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.
@@ -19,6 +19,7 @@ package com.android.server.net.watchlist;
import android.os.Environment;
import android.util.AtomicFile;
import android.util.Log;
+import android.util.Slog;
import android.util.Xml;
import com.android.internal.annotations.VisibleForTesting;
@@ -33,252 +34,126 @@ import org.xmlpull.v1.XmlSerializer;
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.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.CRC32;
/**
- * A util class to do watchlist settings operations, like setting watchlist, query if a domain
- * exists in watchlist.
+ * Class for handling watchlist settings operations, like getting differential privacy secret key.
+ * Unlike WatchlistConfig, which will read configs that pushed from ConfigUpdater only, this class
+ * can read and write all settings for watchlist operations.
*/
class WatchlistSettings {
- private static final String TAG = "WatchlistSettings";
- // Settings xml will be stored in /data/system/network_watchlist/watchlist_settings.xml
- static final String SYSTEM_WATCHLIST_DIR = "network_watchlist";
+ private static final String TAG = "WatchlistSettings";
- private static final String WATCHLIST_XML_FILE = "watchlist_settings.xml";
+ private static final String FILE_NAME = "watchlist_settings.xml";
+ // Rappor requires min entropy input size = 48 bytes
+ private static final int SECRET_KEY_LENGTH = 48;
- private static class XmlTags {
- private static final String WATCHLIST_SETTINGS = "watchlist-settings";
- private static final String SHA256_DOMAIN = "sha256-domain";
- private static final String CRC32_DOMAIN = "crc32-domain";
- private static final String SHA256_IP = "sha256-ip";
- private static final String CRC32_IP = "crc32-ip";
- private static final String HASH = "hash";
- }
-
- private static WatchlistSettings sInstance = new WatchlistSettings();
+ private final static WatchlistSettings sInstance = new WatchlistSettings();
private final AtomicFile mXmlFile;
- private final Object mLock = new Object();
- private HarmfulDigests mCrc32DomainDigests = new HarmfulDigests(new ArrayList<>());
- private HarmfulDigests mSha256DomainDigests = new HarmfulDigests(new ArrayList<>());
- private HarmfulDigests mCrc32IpDigests = new HarmfulDigests(new ArrayList<>());
- private HarmfulDigests mSha256IpDigests = new HarmfulDigests(new ArrayList<>());
- public static synchronized WatchlistSettings getInstance() {
+ private byte[] mPrivacySecretKey = null;
+
+ public static WatchlistSettings getInstance() {
return sInstance;
}
private WatchlistSettings() {
- this(getSystemWatchlistFile(WATCHLIST_XML_FILE));
+ this(getSystemWatchlistFile());
+ }
+
+ static File getSystemWatchlistFile() {
+ return new File(Environment.getDataSystemDirectory(), FILE_NAME);
}
@VisibleForTesting
protected WatchlistSettings(File xmlFile) {
mXmlFile = new AtomicFile(xmlFile);
- readSettingsLocked();
- }
-
- static File getSystemWatchlistFile(String filename) {
- final File dataSystemDir = Environment.getDataSystemDirectory();
- final File systemWatchlistDir = new File(dataSystemDir, SYSTEM_WATCHLIST_DIR);
- systemWatchlistDir.mkdirs();
- return new File(systemWatchlistDir, filename);
+ reloadSettings();
+ if (mPrivacySecretKey == null) {
+ // Generate a new secret key and save settings
+ mPrivacySecretKey = generatePrivacySecretKey();
+ saveSettings();
+ }
}
- private void readSettingsLocked() {
- synchronized (mLock) {
- FileInputStream stream;
- try {
- stream = mXmlFile.openRead();
- } catch (FileNotFoundException e) {
- Log.i(TAG, "No watchlist settings: " + mXmlFile.getBaseFile().getAbsolutePath());
- return;
- }
-
- final List<byte[]> crc32DomainList = new ArrayList<>();
- final List<byte[]> sha256DomainList = new ArrayList<>();
- final List<byte[]> crc32IpList = new ArrayList<>();
- final List<byte[]> sha256IpList = new ArrayList<>();
-
- try {
- XmlPullParser parser = Xml.newPullParser();
- parser.setInput(stream, StandardCharsets.UTF_8.name());
- parser.nextTag();
- parser.require(XmlPullParser.START_TAG, null, XmlTags.WATCHLIST_SETTINGS);
- while (parser.nextTag() == XmlPullParser.START_TAG) {
- String tagName = parser.getName();
- switch (tagName) {
- case XmlTags.CRC32_DOMAIN:
- parseHash(parser, tagName, crc32DomainList);
- break;
- case XmlTags.CRC32_IP:
- parseHash(parser, tagName, crc32IpList);
- break;
- case XmlTags.SHA256_DOMAIN:
- parseHash(parser, tagName, sha256DomainList);
- break;
- case XmlTags.SHA256_IP:
- parseHash(parser, tagName, sha256IpList);
- break;
- default:
- Log.w(TAG, "Unknown element: " + parser.getName());
- XmlUtils.skipCurrentTag(parser);
- }
- }
- parser.require(XmlPullParser.END_TAG, null, XmlTags.WATCHLIST_SETTINGS);
- writeSettingsToMemory(crc32DomainList, sha256DomainList, crc32IpList, sha256IpList);
- } catch (IllegalStateException | NullPointerException | NumberFormatException |
- XmlPullParserException | IOException | IndexOutOfBoundsException e) {
- Log.w(TAG, "Failed parsing " + e);
- } finally {
- try {
- stream.close();
- } catch (IOException e) {
+ public void reloadSettings() {
+ try (FileInputStream stream = mXmlFile.openRead()){
+ XmlPullParser parser = Xml.newPullParser();
+ parser.setInput(stream, StandardCharsets.UTF_8.name());
+ XmlUtils.beginDocument(parser, "network-watchlist-settings");
+ final int outerDepth = parser.getDepth();
+ while (XmlUtils.nextElementWithin(parser, outerDepth)) {
+ if (parser.getName().equals("secret-key")) {
+ mPrivacySecretKey = parseSecretKey(parser);
}
}
+ Log.i(TAG, "Reload watchlist settings done");
+ } catch (IllegalStateException | NullPointerException | NumberFormatException |
+ XmlPullParserException | IOException | IndexOutOfBoundsException e) {
+ Slog.e(TAG, "Failed parsing xml", e);
}
}
- private void parseHash(XmlPullParser parser, String tagName, List<byte[]> hashSet)
+ private byte[] parseSecretKey(XmlPullParser parser)
throws IOException, XmlPullParserException {
- parser.require(XmlPullParser.START_TAG, null, tagName);
- while (parser.nextTag() == XmlPullParser.START_TAG) {
- parser.require(XmlPullParser.START_TAG, null, XmlTags.HASH);
- byte[] hash = HexDump.hexStringToByteArray(parser.nextText());
- parser.require(XmlPullParser.END_TAG, null, XmlTags.HASH);
- hashSet.add(hash);
- }
- parser.require(XmlPullParser.END_TAG, null, tagName);
- }
-
- /**
- * Write network watchlist settings to disk.
- * Adb should not use it, should use writeSettingsToMemory directly instead.
- */
- public void writeSettingsToDisk(List<byte[]> newCrc32DomainList,
- List<byte[]> newSha256DomainList,
- List<byte[]> newCrc32IpList,
- List<byte[]> newSha256IpList) {
- synchronized (mLock) {
- FileOutputStream stream;
- try {
- stream = mXmlFile.startWrite();
- } catch (IOException e) {
- Log.w(TAG, "Failed to write display settings: " + e);
- return;
- }
-
- try {
- XmlSerializer out = new FastXmlSerializer();
- out.setOutput(stream, StandardCharsets.UTF_8.name());
- out.startDocument(null, true);
- out.startTag(null, XmlTags.WATCHLIST_SETTINGS);
-
- writeHashSetToXml(out, XmlTags.SHA256_DOMAIN, newSha256DomainList);
- writeHashSetToXml(out, XmlTags.SHA256_IP, newSha256IpList);
- writeHashSetToXml(out, XmlTags.CRC32_DOMAIN, newCrc32DomainList);
- writeHashSetToXml(out, XmlTags.CRC32_IP, newCrc32IpList);
-
- out.endTag(null, XmlTags.WATCHLIST_SETTINGS);
- out.endDocument();
- mXmlFile.finishWrite(stream);
- writeSettingsToMemory(newCrc32DomainList, newSha256DomainList, newCrc32IpList,
- newSha256IpList);
- } catch (IOException e) {
- Log.w(TAG, "Failed to write display settings, restoring backup.", e);
- mXmlFile.failWrite(stream);
- }
+ parser.require(XmlPullParser.START_TAG, null, "secret-key");
+ byte[] key = HexDump.hexStringToByteArray(parser.nextText());
+ parser.require(XmlPullParser.END_TAG, null, "secret-key");
+ if (key == null || key.length != SECRET_KEY_LENGTH) {
+ Log.e(TAG, "Unable to parse secret key");
+ return null;
}
+ return key;
}
/**
- * Write network watchlist settings to memory.
+ * Get DP secret key.
+ * Make sure it is not exported or logged in anywhere.
*/
- public void writeSettingsToMemory(List<byte[]> newCrc32DomainList,
- List<byte[]> newSha256DomainList,
- List<byte[]> newCrc32IpList,
- List<byte[]> newSha256IpList) {
- synchronized (mLock) {
- mCrc32DomainDigests = new HarmfulDigests(newCrc32DomainList);
- mCrc32IpDigests = new HarmfulDigests(newCrc32IpList);
- mSha256DomainDigests = new HarmfulDigests(newSha256DomainList);
- mSha256IpDigests = new HarmfulDigests(newSha256IpList);
- }
+ synchronized byte[] getPrivacySecretKey() {
+ final byte[] key = new byte[SECRET_KEY_LENGTH];
+ System.arraycopy(mPrivacySecretKey, 0, key, 0, SECRET_KEY_LENGTH);
+ return key;
}
- private static void writeHashSetToXml(XmlSerializer out, String tagName, List<byte[]> hashSet)
- throws IOException {
- out.startTag(null, tagName);
- for (byte[] hash : hashSet) {
- out.startTag(null, XmlTags.HASH);
- out.text(HexDump.toHexString(hash));
- out.endTag(null, XmlTags.HASH);
- }
- out.endTag(null, tagName);
- }
-
- public boolean containsDomain(String domain) {
- // First it does a quick CRC32 check.
- final byte[] crc32 = getCrc32(domain);
- if (!mCrc32DomainDigests.contains(crc32)) {
- return false;
- }
- // Now we do a slow SHA256 check.
- final byte[] sha256 = getSha256(domain);
- return mSha256DomainDigests.contains(sha256);
+ private byte[] generatePrivacySecretKey() {
+ final byte[] key = new byte[SECRET_KEY_LENGTH];
+ (new SecureRandom()).nextBytes(key);
+ return key;
}
- public boolean containsIp(String ip) {
- // First it does a quick CRC32 check.
- final byte[] crc32 = getCrc32(ip);
- if (!mCrc32IpDigests.contains(crc32)) {
- return false;
+ private void saveSettings() {
+ FileOutputStream stream;
+ try {
+ stream = mXmlFile.startWrite();
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to write display settings: " + e);
+ return;
}
- // Now we do a slow SHA256 check.
- final byte[] sha256 = getSha256(ip);
- return mSha256IpDigests.contains(sha256);
- }
-
-
- /** Get CRC32 of a string */
- private byte[] getCrc32(String str) {
- final CRC32 crc = new CRC32();
- crc.update(str.getBytes());
- final long tmp = crc.getValue();
- return new byte[]{(byte)(tmp >> 24 & 255), (byte)(tmp >> 16 & 255),
- (byte)(tmp >> 8 & 255), (byte)(tmp & 255)};
- }
-
- /** Get SHA256 of a string */
- private byte[] getSha256(String str) {
- MessageDigest messageDigest;
try {
- messageDigest = MessageDigest.getInstance("SHA256");
- } catch (NoSuchAlgorithmException e) {
- /* can't happen */
- return null;
+ XmlSerializer out = new FastXmlSerializer();
+ out.setOutput(stream, StandardCharsets.UTF_8.name());
+ out.startDocument(null, true);
+ out.startTag(null, "network-watchlist-settings");
+ out.startTag(null, "secret-key");
+ out.text(HexDump.toHexString(mPrivacySecretKey));
+ out.endTag(null, "secret-key");
+ out.endTag(null, "network-watchlist-settings");
+ out.endDocument();
+ mXmlFile.finishWrite(stream);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to write display settings, restoring backup.", e);
+ mXmlFile.failWrite(stream);
}
- messageDigest.update(str.getBytes());
- return messageDigest.digest();
- }
-
- public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- pw.println("Domain CRC32 digest list:");
- mCrc32DomainDigests.dump(fd, pw, args);
- pw.println("Domain SHA256 digest list:");
- mSha256DomainDigests.dump(fd, pw, args);
- pw.println("Ip CRC32 digest list:");
- mCrc32IpDigests.dump(fd, pw, args);
- pw.println("Ip SHA256 digest list:");
- mSha256IpDigests.dump(fd, pw, args);
}
}
diff --git a/com/android/server/notification/ManagedServices.java b/com/android/server/notification/ManagedServices.java
index 7d64aed4..502760a2 100644
--- a/com/android/server/notification/ManagedServices.java
+++ b/com/android/server/notification/ManagedServices.java
@@ -71,6 +71,7 @@ import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
+import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
@@ -98,6 +99,9 @@ abstract public class ManagedServices {
static final String ATT_APPROVED_LIST = "approved";
static final String ATT_USER_ID = "user";
static final String ATT_IS_PRIMARY = "primary";
+ static final String ATT_VERSION = "version";
+
+ static final int DB_VERSION = 1;
static final int APPROVAL_BY_PACKAGE = 0;
static final int APPROVAL_BY_COMPONENT = 1;
@@ -250,24 +254,16 @@ abstract public class ManagedServices {
for (ComponentName cmpt : mEnabledServicesForCurrentProfiles) {
if (filter != null && !filter.matches(cmpt)) continue;
-
- final long cToken = proto.start(ManagedServicesProto.ENABLED);
- cmpt.toProto(proto);
- proto.end(cToken);
+ cmpt.writeToProto(proto, ManagedServicesProto.ENABLED);
}
for (ManagedServiceInfo info : mServices) {
if (filter != null && !filter.matches(info.component)) continue;
-
- final long lToken = proto.start(ManagedServicesProto.LIVE_SERVICES);
- info.toProto(proto, this);
- proto.end(lToken);
+ info.writeToProto(proto, ManagedServicesProto.LIVE_SERVICES, this);
}
for (ComponentName name : mSnoozingForCurrentProfiles) {
- final long cToken = proto.start(ManagedServicesProto.SNOOZED);
- name.toProto(proto);
- proto.end(cToken);
+ name.writeToProto(proto, ManagedServicesProto.SNOOZED);
}
}
@@ -301,6 +297,8 @@ abstract public class ManagedServices {
public void writeXml(XmlSerializer out, boolean forBackup) throws IOException {
out.startTag(null, getConfig().xmlTag);
+ out.attribute(null, ATT_VERSION, String.valueOf(DB_VERSION));
+
if (forBackup) {
trimApprovedListsAccordingToInstalledServices();
}
@@ -342,6 +340,14 @@ abstract public class ManagedServices {
public void readXml(XmlPullParser parser)
throws XmlPullParserException, IOException {
+ // upgrade xml
+ int xmlVersion = XmlUtils.readIntAttribute(parser, ATT_VERSION, 0);
+ final List<UserInfo> activeUsers = mUm.getUsers(true);
+ for (UserInfo userInfo : activeUsers) {
+ upgradeXml(xmlVersion, userInfo.getUserHandle().getIdentifier());
+ }
+
+ // read grants
int type;
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT) {
String tag = parser.getName();
@@ -352,6 +358,7 @@ abstract public class ManagedServices {
if (type == XmlPullParser.START_TAG) {
if (TAG_MANAGED_SERVICES.equals(tag)) {
Slog.i(TAG, "Read " + mConfig.caption + " permissions from xml");
+
final String approved = XmlUtils.readStringAttribute(parser, ATT_APPROVED_LIST);
final int userId = XmlUtils.readIntAttribute(parser, ATT_USER_ID, 0);
final boolean isPrimary =
@@ -366,6 +373,8 @@ abstract public class ManagedServices {
rebindServices(false);
}
+ protected void upgradeXml(final int xmlVersion, final int userId) {}
+
private void loadAllowedComponentsFromSettings() {
UserManager userManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
@@ -385,7 +394,7 @@ abstract public class ManagedServices {
Slog.d(TAG, "Done loading approved values from settings");
}
- private void addApprovedList(String approved, int userId, boolean isPrimary) {
+ protected void addApprovedList(String approved, int userId, boolean isPrimary) {
if (TextUtils.isEmpty(approved)) {
approved = "";
}
@@ -1132,14 +1141,14 @@ abstract public class ManagedServices {
.append(']').toString();
}
- public void toProto(ProtoOutputStream proto, ManagedServices host) {
- final long cToken = proto.start(ManagedServiceInfoProto.COMPONENT);
- component.toProto(proto);
- proto.end(cToken);
+ public void writeToProto(ProtoOutputStream proto, long fieldId, ManagedServices host) {
+ final long token = proto.start(fieldId);
+ component.writeToProto(proto, ManagedServiceInfoProto.COMPONENT);
proto.write(ManagedServiceInfoProto.USER_ID, userid);
proto.write(ManagedServiceInfoProto.SERVICE, service.getClass().getName());
proto.write(ManagedServiceInfoProto.IS_SYSTEM, isSystem);
proto.write(ManagedServiceInfoProto.IS_GUEST, isGuest(host));
+ proto.end(token);
}
public boolean enabledAndUserMatches(int nid) {
diff --git a/com/android/server/notification/NotificationComparator.java b/com/android/server/notification/NotificationComparator.java
index 4c009214..2584187f 100644
--- a/com/android/server/notification/NotificationComparator.java
+++ b/com/android/server/notification/NotificationComparator.java
@@ -160,7 +160,7 @@ public class NotificationComparator
}
private boolean isCall(NotificationRecord record) {
- return record.getNotification().category == Notification.CATEGORY_CALL
+ return record.isCategory(Notification.CATEGORY_CALL)
&& isDefaultPhoneApp(record.sbn.getPackageName());
}
diff --git a/com/android/server/notification/NotificationManagerService.java b/com/android/server/notification/NotificationManagerService.java
index cf014007..4c9da894 100644
--- a/com/android/server/notification/NotificationManagerService.java
+++ b/com/android/server/notification/NotificationManagerService.java
@@ -16,6 +16,7 @@
package com.android.server.notification;
+import static android.app.NotificationManager.ACTION_APP_BLOCK_STATE_CHANGED;
import static android.app.NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED;
import static android.app.NotificationManager.ACTION_NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED;
import static android.app.NotificationManager.IMPORTANCE_LOW;
@@ -137,6 +138,7 @@ import android.service.notification.NotificationRankingUpdate;
import android.service.notification.NotificationRecordProto;
import android.service.notification.NotificationServiceDumpProto;
import android.service.notification.NotificationStats;
+import android.service.notification.NotifyingApp;
import android.service.notification.SnoozeCriterion;
import android.service.notification.StatusBarNotification;
import android.service.notification.ZenModeConfig;
@@ -329,6 +331,7 @@ public class NotificationManagerService extends SystemService {
final ArrayMap<Integer, ArrayMap<String, String>> mAutobundledSummaries = new ArrayMap<>();
final ArrayList<ToastRecord> mToastQueue = new ArrayList<>();
final ArrayMap<String, NotificationRecord> mSummaryByGroupKey = new ArrayMap<>();
+ final ArrayMap<Integer, ArrayList<NotifyingApp>> mRecentApps = new ArrayMap<>();
// The last key in this list owns the hardware.
ArrayList<String> mLights = new ArrayList<>();
@@ -434,6 +437,7 @@ public class NotificationManagerService extends SystemService {
}
}
}
+
String defaultDndAccess = getContext().getResources().getString(
com.android.internal.R.string.config_defaultDndAccessPackages);
if (defaultListenerAccess != null) {
@@ -446,6 +450,29 @@ public class NotificationManagerService extends SystemService {
}
}
}
+
+ readDefaultAssistant(userId);
+ }
+
+ protected void readDefaultAssistant(int userId) {
+ String defaultAssistantAccess = getContext().getResources().getString(
+ com.android.internal.R.string.config_defaultAssistantAccessPackage);
+ if (defaultAssistantAccess != null) {
+ // Gather all notification assistant components for candidate pkg. There should
+ // only be one
+ Set<ComponentName> approvedAssistants =
+ mAssistants.queryPackageForServices(defaultAssistantAccess,
+ PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE, userId);
+ for (ComponentName cn : approvedAssistants) {
+ try {
+ getBinderService().setNotificationAssistantAccessGrantedForUser(cn,
+ userId, true);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+ }
}
void readPolicyXml(InputStream stream, boolean forRestore)
@@ -1155,6 +1182,7 @@ public class NotificationManagerService extends SystemService {
}
}
+ @VisibleForTesting
void clearNotifications() {
mEnqueuedNotifications.clear();
mNotificationList.clear();
@@ -1374,7 +1402,8 @@ public class NotificationManagerService extends SystemService {
AppGlobals.getPackageManager(), getContext().getPackageManager(),
getLocalService(LightsManager.class),
new NotificationListeners(AppGlobals.getPackageManager()),
- new NotificationAssistants(AppGlobals.getPackageManager()),
+ new NotificationAssistants(getContext(), mNotificationLock, mUserProfiles,
+ AppGlobals.getPackageManager()),
new ConditionProviders(getContext(), mUserProfiles, AppGlobals.getPackageManager()),
null, snoozeHelper, new NotificationUsageStats(getContext()),
new AtomicFile(new File(systemDir, "notification_policy.xml")),
@@ -1853,6 +1882,18 @@ public class NotificationManagerService extends SystemService {
cancelAllNotificationsInt(MY_UID, MY_PID, pkg, null, 0, 0, true,
UserHandle.getUserId(uid), REASON_PACKAGE_BANNED, null);
}
+
+ try {
+ getContext().sendBroadcastAsUser(
+ new Intent(ACTION_APP_BLOCK_STATE_CHANGED)
+ .putExtra(NotificationManager.EXTRA_BLOCKED_STATE, !enabled)
+ .addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+ .setPackage(pkg),
+ UserHandle.of(UserHandle.getUserId(uid)), null);
+ } catch (SecurityException e) {
+ Slog.w(TAG, "Can't notify app about app block change", e);
+ }
+
savePolicyFile();
}
@@ -2084,6 +2125,16 @@ public class NotificationManagerService extends SystemService {
}
@Override
+ public ParceledListSlice<NotifyingApp> getRecentNotifyingAppsForUser(int userId) {
+ checkCallerIsSystem();
+ synchronized (mNotificationLock) {
+ List<NotifyingApp> apps = new ArrayList<>(
+ mRecentApps.getOrDefault(userId, new ArrayList<>()));
+ return new ParceledListSlice<>(apps);
+ }
+ }
+
+ @Override
public void clearData(String packageName, int uid, boolean fromApp) throws RemoteException {
checkCallerIsSystem();
@@ -2917,12 +2968,34 @@ public class NotificationManagerService extends SystemService {
}
}
+ /**
+ * Sets the notification policy. Apps that target API levels below
+ * {@link android.os.Build.VERSION_CODES#P} cannot make DND silence
+ * {@link Policy#PRIORITY_CATEGORY_ALARMS} or
+ * {@link Policy#PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER}
+ */
@Override
public void setNotificationPolicy(String pkg, Policy policy) {
enforcePolicyAccess(pkg, "setNotificationPolicy");
final long identity = Binder.clearCallingIdentity();
try {
+ final ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(pkg,
+ 0, UserHandle.getUserId(MY_UID));
+
+ if (applicationInfo.targetSdkVersion <= Build.VERSION_CODES.O_MR1) {
+ Policy currPolicy = mZenModeHelper.getNotificationPolicy();
+
+ int priorityCategories = policy.priorityCategories
+ | (currPolicy.priorityCategories & Policy.PRIORITY_CATEGORY_ALARMS)
+ | (currPolicy.priorityCategories &
+ Policy.PRIORITY_CATEGORY_MEDIA_SYSTEM_OTHER);
+ policy = new Policy(priorityCategories,
+ policy.priorityCallSenders, policy.priorityMessageSenders,
+ policy.suppressedVisualEffects);
+ }
+
mZenModeHelper.setNotificationPolicy(policy);
+ } catch (RemoteException e) {
} finally {
Binder.restoreCallingIdentity(identity);
}
@@ -3322,36 +3395,28 @@ public class NotificationManagerService extends SystemService {
private void dumpProto(FileDescriptor fd, @NonNull DumpFilter filter) {
final ProtoOutputStream proto = new ProtoOutputStream(fd);
synchronized (mNotificationLock) {
- long records = proto.start(NotificationServiceDumpProto.RECORDS);
int N = mNotificationList.size();
- if (N > 0) {
- for (int i = 0; i < N; i++) {
- final NotificationRecord nr = mNotificationList.get(i);
- if (filter.filtered && !filter.matches(nr.sbn)) continue;
- nr.dump(proto, filter.redact);
- proto.write(NotificationRecordProto.STATE, NotificationRecordProto.POSTED);
- }
+ for (int i = 0; i < N; i++) {
+ final NotificationRecord nr = mNotificationList.get(i);
+ if (filter.filtered && !filter.matches(nr.sbn)) continue;
+ nr.dump(proto, NotificationServiceDumpProto.RECORDS, filter.redact,
+ NotificationRecordProto.POSTED);
}
N = mEnqueuedNotifications.size();
- if (N > 0) {
- for (int i = 0; i < N; i++) {
- final NotificationRecord nr = mEnqueuedNotifications.get(i);
- if (filter.filtered && !filter.matches(nr.sbn)) continue;
- nr.dump(proto, filter.redact);
- proto.write(NotificationRecordProto.STATE, NotificationRecordProto.ENQUEUED);
- }
+ for (int i = 0; i < N; i++) {
+ final NotificationRecord nr = mEnqueuedNotifications.get(i);
+ if (filter.filtered && !filter.matches(nr.sbn)) continue;
+ nr.dump(proto, NotificationServiceDumpProto.RECORDS, filter.redact,
+ NotificationRecordProto.ENQUEUED);
}
List<NotificationRecord> snoozed = mSnoozeHelper.getSnoozed();
N = snoozed.size();
- if (N > 0) {
- for (int i = 0; i < N; i++) {
- final NotificationRecord nr = snoozed.get(i);
- if (filter.filtered && !filter.matches(nr.sbn)) continue;
- nr.dump(proto, filter.redact);
- proto.write(NotificationRecordProto.STATE, NotificationRecordProto.SNOOZED);
- }
+ for (int i = 0; i < N; i++) {
+ final NotificationRecord nr = snoozed.get(i);
+ if (filter.filtered && !filter.matches(nr.sbn)) continue;
+ nr.dump(proto, NotificationServiceDumpProto.RECORDS, filter.redact,
+ NotificationRecordProto.SNOOZED);
}
- proto.end(records);
long zenLog = proto.start(NotificationServiceDumpProto.ZEN);
mZenModeHelper.dump(proto);
@@ -3376,9 +3441,7 @@ public class NotificationManagerService extends SystemService {
mListenersDisablingEffects.valueAt(i);
for (int j = 0; j < listeners.size(); j++) {
final ManagedServiceInfo listener = listeners.valueAt(i);
- listenersToken = proto.start(ListenersDisablingEffectsProto.LISTENERS);
- listener.toProto(proto, null);
- proto.end(listenersToken);
+ listener.writeToProto(proto, ListenersDisablingEffectsProto.LISTENERS, null);
}
proto.end(effectsToken);
@@ -4050,6 +4113,10 @@ public class NotificationManagerService extends SystemService {
mNotificationsByKey.put(n.getKey(), r);
+ if (!r.isUpdate) {
+ logRecentLocked(r);
+ }
+
// Ensure if this is a foreground service that the proper additional
// flags are set.
if ((notification.flags & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
@@ -4107,6 +4174,38 @@ public class NotificationManagerService extends SystemService {
}
/**
+ * Keeps the last 5 packages that have notified, by user.
+ */
+ @GuardedBy("mNotificationLock")
+ @VisibleForTesting
+ protected void logRecentLocked(NotificationRecord r) {
+ if (r.isUpdate) {
+ return;
+ }
+ ArrayList<NotifyingApp> recentAppsForUser =
+ mRecentApps.getOrDefault(r.getUser().getIdentifier(), new ArrayList<>(6));
+ NotifyingApp na = new NotifyingApp()
+ .setPackage(r.sbn.getPackageName())
+ .setUid(r.sbn.getUid())
+ .setLastNotified(r.sbn.getPostTime());
+ // A new notification gets an app moved to the front of the list
+ for (int i = recentAppsForUser.size() - 1; i >= 0; i--) {
+ NotifyingApp naExisting = recentAppsForUser.get(i);
+ if (na.getPackage().equals(naExisting.getPackage())
+ && na.getUid() == naExisting.getUid()) {
+ recentAppsForUser.remove(i);
+ break;
+ }
+ }
+ // time is always increasing, so always add to the front of the list
+ recentAppsForUser.add(0, na);
+ if (recentAppsForUser.size() > 5) {
+ recentAppsForUser.remove(recentAppsForUser.size() -1);
+ }
+ mRecentApps.put(r.getUser().getIdentifier(), recentAppsForUser);
+ }
+
+ /**
* Ensures that grouped notification receive their special treatment.
*
* <p>Cancels group children if the new notification causes a group to lose
@@ -5533,8 +5632,9 @@ public class NotificationManagerService extends SystemService {
public class NotificationAssistants extends ManagedServices {
static final String TAG_ENABLED_NOTIFICATION_ASSISTANTS = "enabled_assistants";
- public NotificationAssistants(IPackageManager pm) {
- super(getContext(), mNotificationLock, mUserProfiles, pm);
+ public NotificationAssistants(Context context, Object lock, UserProfiles up,
+ IPackageManager pm) {
+ super(context, lock, up, pm);
}
@Override
@@ -5639,6 +5739,14 @@ public class NotificationManagerService extends SystemService {
public boolean isEnabled() {
return !getServices().isEmpty();
}
+
+ protected void upgradeXml(final int xmlVersion, final int userId) {
+ if (xmlVersion == 0) {
+ // one time approval of the OOB assistant
+ Slog.d(TAG, "Approving default notification assistant for user " + userId);
+ readDefaultAssistant(userId);
+ }
+ }
}
public class NotificationListeners extends ManagedServices {
@@ -6052,7 +6160,7 @@ public class NotificationManagerService extends SystemService {
public static final String USAGE = "help\n"
+ "allow_listener COMPONENT [user_id]\n"
+ "disallow_listener COMPONENT [user_id]\n"
- + "set_assistant COMPONENT\n"
+ + "allow_assistant COMPONENT\n"
+ "remove_assistant COMPONENT\n"
+ "allow_dnd PACKAGE\n"
+ "disallow_dnd PACKAGE";
diff --git a/com/android/server/notification/NotificationRecord.java b/com/android/server/notification/NotificationRecord.java
index faa300f2..23b9743a 100644
--- a/com/android/server/notification/NotificationRecord.java
+++ b/com/android/server/notification/NotificationRecord.java
@@ -361,8 +361,11 @@ public final class NotificationRecord {
/** @deprecated Use {@link #getUser()} instead. */
public int getUserId() { return sbn.getUserId(); }
- void dump(ProtoOutputStream proto, boolean redact) {
+ void dump(ProtoOutputStream proto, long fieldId, boolean redact, int state) {
+ final long token = proto.start(fieldId);
+
proto.write(NotificationRecordProto.KEY, sbn.getKey());
+ proto.write(NotificationRecordProto.STATE, state);
if (getChannel() != null) {
proto.write(NotificationRecordProto.CHANNEL_ID, getChannel().getId());
}
@@ -375,8 +378,10 @@ public final class NotificationRecord {
proto.write(NotificationRecordProto.SOUND, getSound().toString());
}
if (getAudioAttributes() != null) {
- proto.write(NotificationRecordProto.SOUND_USAGE, getAudioAttributes().getUsage());
+ getAudioAttributes().writeToProto(proto, NotificationRecordProto.AUDIO_ATTRIBUTES);
}
+
+ proto.end(token);
}
String formatRemoteViews(RemoteViews rv) {
diff --git a/com/android/server/notification/RankingHelper.java b/com/android/server/notification/RankingHelper.java
index c0dccb53..b0e3820f 100644
--- a/com/android/server/notification/RankingHelper.java
+++ b/com/android/server/notification/RankingHelper.java
@@ -973,16 +973,11 @@ public class RankingHelper implements RankingConfig {
proto.write(RecordProto.VISIBILITY, r.visibility);
proto.write(RecordProto.SHOW_BADGE, r.showBadge);
- long token;
for (NotificationChannel channel : r.channels.values()) {
- token = proto.start(RecordProto.CHANNELS);
- channel.toProto(proto);
- proto.end(token);
+ channel.writeToProto(proto, RecordProto.CHANNELS);
}
for (NotificationChannelGroup group : r.groups.values()) {
- token = proto.start(RecordProto.CHANNEL_GROUPS);
- group.toProto(proto);
- proto.end(token);
+ group.writeToProto(proto, RecordProto.CHANNEL_GROUPS);
}
proto.end(fToken);
diff --git a/com/android/server/notification/ValidateNotificationPeople.java b/com/android/server/notification/ValidateNotificationPeople.java
index a30e0639..fd9ffb22 100644
--- a/com/android/server/notification/ValidateNotificationPeople.java
+++ b/com/android/server/notification/ValidateNotificationPeople.java
@@ -278,7 +278,7 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor {
// VisibleForTesting
public static String[] getExtraPeople(Bundle extras) {
- Object people = extras.get(Notification.EXTRA_PEOPLE);
+ Object people = extras.get(Notification.EXTRA_PEOPLE_LIST);
if (people instanceof String[]) {
return (String[]) people;
}
@@ -305,6 +305,16 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor {
return array;
}
+ if (arrayList.get(0) instanceof Notification.Person) {
+ ArrayList<Notification.Person> list = (ArrayList<Notification.Person>) arrayList;
+ final int N = list.size();
+ String[] array = new String[N];
+ for (int i = 0; i < N; i++) {
+ array[i] = list.get(i).resolveToLegacyUri();
+ }
+ return array;
+ }
+
return null;
}
@@ -459,7 +469,9 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor {
lookupResult = searchContacts(mContext, uri);
} else {
lookupResult = new LookupResult(); // invalid person for the cache
- Slog.w(TAG, "unsupported URI " + handle);
+ if (!"name".equals(uri.getScheme())) {
+ Slog.w(TAG, "unsupported URI " + handle);
+ }
}
if (lookupResult != null) {
synchronized (mPeopleCache) {
diff --git a/com/android/server/notification/ZenModeHelper.java b/com/android/server/notification/ZenModeHelper.java
index 700ccad8..932e4f94 100644
--- a/com/android/server/notification/ZenModeHelper.java
+++ b/com/android/server/notification/ZenModeHelper.java
@@ -565,7 +565,7 @@ public class ZenModeHelper {
proto.write(ZenModeProto.ENABLED_ACTIVE_CONDITIONS, rule.toString());
}
}
- mConfig.toNotificationPolicy().toProto(proto, ZenModeProto.POLICY);
+ mConfig.toNotificationPolicy().writeToProto(proto, ZenModeProto.POLICY);
proto.write(ZenModeProto.SUPPRESSED_EFFECTS, mSuppressedEffects);
}
}
@@ -822,21 +822,24 @@ public class ZenModeHelper {
@VisibleForTesting
protected void applyRestrictions() {
- final boolean zen = mZenMode != Global.ZEN_MODE_OFF;
+ final boolean zenPriorityOnly = mZenMode == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
+ final boolean zenSilence = mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS;
+ final boolean zenAlarmsOnly = mZenMode == Global.ZEN_MODE_ALARMS;
// notification restrictions
final boolean muteNotifications =
(mSuppressedEffects & SUPPRESSED_EFFECT_NOTIFICATIONS) != 0;
// call restrictions
- final boolean muteCalls = zen && !mConfig.allowCalls && !mConfig.allowRepeatCallers
+ final boolean muteCalls = zenAlarmsOnly
+ || (zenPriorityOnly && !mConfig.allowCalls && !mConfig.allowRepeatCallers)
|| (mSuppressedEffects & SUPPRESSED_EFFECT_CALLS) != 0;
// alarm restrictions
- final boolean muteAlarms = zen && !mConfig.allowAlarms;
+ final boolean muteAlarms = zenPriorityOnly && !mConfig.allowAlarms;
// alarm restrictions
- final boolean muteMediaAndSystemSounds = zen && !mConfig.allowMediaSystemOther;
+ final boolean muteMediaAndSystemSounds = zenPriorityOnly && !mConfig.allowMediaSystemOther;
// total silence restrictions
- final boolean muteEverything = mZenMode == Global.ZEN_MODE_NO_INTERRUPTIONS
- || areAllBehaviorSoundsMuted();
+ final boolean muteEverything = zenSilence
+ || (zenPriorityOnly && areAllBehaviorSoundsMuted());
for (int usage : AudioAttributes.SDK_USAGES) {
final int suppressionBehavior = AudioAttributes.SUPPRESSIBLE_USAGES.get(usage);
@@ -999,7 +1002,7 @@ public class ZenModeHelper {
if (isChange && policy.doNotDisturbWhenSilent) {
if (mZenMode != Global.ZEN_MODE_NO_INTERRUPTIONS
&& mZenMode != Global.ZEN_MODE_ALARMS) {
- newZen = Global.ZEN_MODE_ALARMS;
+ newZen = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
}
setPreviousRingerModeSetting(ringerModeOld);
}
@@ -1039,7 +1042,7 @@ public class ZenModeHelper {
case AudioManager.RINGER_MODE_SILENT:
if (isChange) {
if (mZenMode == Global.ZEN_MODE_OFF) {
- newZen = Global.ZEN_MODE_ALARMS;
+ newZen = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
}
ringerModeInternalOut = isVibrate ? AudioManager.RINGER_MODE_VIBRATE
: AudioManager.RINGER_MODE_SILENT;
diff --git a/com/android/server/oemlock/OemLockService.java b/com/android/server/oemlock/OemLockService.java
index 2a2ff06b..a6200bf4 100644
--- a/com/android/server/oemlock/OemLockService.java
+++ b/com/android/server/oemlock/OemLockService.java
@@ -31,10 +31,10 @@ import android.os.UserManager;
import android.os.UserManagerInternal;
import android.os.UserManagerInternal.UserRestrictionsListener;
import android.service.oemlock.IOemLockService;
-import android.service.persistentdata.PersistentDataBlockManager;
import android.util.Slog;
import com.android.server.LocalServices;
+import com.android.server.PersistentDataBlockManagerInternal;
import com.android.server.SystemService;
import com.android.server.pm.UserRestrictionsUtils;
@@ -217,13 +217,12 @@ public class OemLockService extends SystemService {
* is used to erase FRP information on a unlockable device.
*/
private void setPersistentDataBlockOemUnlockAllowedBit(boolean allowed) {
- final PersistentDataBlockManager pdbm = (PersistentDataBlockManager)
- mContext.getSystemService(Context.PERSISTENT_DATA_BLOCK_SERVICE);
+ final PersistentDataBlockManagerInternal pdbmi
+ = LocalServices.getService(PersistentDataBlockManagerInternal.class);
// if mOemLock is PersistentDataBlockLock, then the bit should have already been set
- if (pdbm != null && !(mOemLock instanceof PersistentDataBlockLock)
- && pdbm.getOemUnlockEnabled() != allowed) {
+ if (pdbmi != null && !(mOemLock instanceof PersistentDataBlockLock)) {
Slog.i(TAG, "Update OEM Unlock bit in pst partition to " + allowed);
- pdbm.setOemUnlockEnabled(allowed);
+ pdbmi.forceOemUnlockEnabled(allowed);
}
}
diff --git a/com/android/server/om/OverlayManagerServiceImpl.java b/com/android/server/om/OverlayManagerServiceImpl.java
index 253d4f5b..7600e81c 100644
--- a/com/android/server/om/OverlayManagerServiceImpl.java
+++ b/com/android/server/om/OverlayManagerServiceImpl.java
@@ -169,8 +169,9 @@ final class OverlayManagerServiceImpl {
}
final PackageInfo targetPackage = mPackageManager.getPackageInfo(packageName, userId);
- updateAllOverlaysForTarget(packageName, userId, targetPackage);
- mListener.onOverlaysChanged(packageName, userId);
+ if (updateAllOverlaysForTarget(packageName, userId, targetPackage)) {
+ mListener.onOverlaysChanged(packageName, userId);
+ }
}
void onTargetPackageChanged(@NonNull final String packageName, final int userId) {
@@ -210,7 +211,9 @@ final class OverlayManagerServiceImpl {
Slog.d(TAG, "onTargetPackageRemoved packageName=" + packageName + " userId=" + userId);
}
- updateAllOverlaysForTarget(packageName, userId, null);
+ if (updateAllOverlaysForTarget(packageName, userId, null)) {
+ mListener.onOverlaysChanged(packageName, userId);
+ }
}
/**
@@ -280,7 +283,18 @@ final class OverlayManagerServiceImpl {
}
void onOverlayPackageRemoved(@NonNull final String packageName, final int userId) {
- Slog.wtf(TAG, "onOverlayPackageRemoved called, but only pre-installed overlays supported");
+ try {
+ final OverlayInfo overlayInfo = mSettings.getOverlayInfo(packageName, userId);
+ if (mSettings.remove(packageName, userId)) {
+ removeIdmapIfPossible(overlayInfo);
+ if (overlayInfo.isEnabled()) {
+ // Only trigger updates if the overlay was enabled.
+ mListener.onOverlaysChanged(overlayInfo.targetPackageName, userId);
+ }
+ }
+ } catch (OverlayManagerSettings.BadKeyException e) {
+ Slog.e(TAG, "failed to remove overlay", e);
+ }
}
OverlayInfo getOverlayInfo(@NonNull final String packageName, final int userId) {
diff --git a/com/android/server/om/OverlayManagerSettings.java b/com/android/server/om/OverlayManagerSettings.java
index c059b378..17b38deb 100644
--- a/com/android/server/om/OverlayManagerSettings.java
+++ b/com/android/server/om/OverlayManagerSettings.java
@@ -104,7 +104,7 @@ final class OverlayManagerSettings {
return true;
}
- OverlayInfo getOverlayInfo(@NonNull final String packageName, final int userId)
+ @NonNull OverlayInfo getOverlayInfo(@NonNull final String packageName, final int userId)
throws BadKeyException {
final int idx = select(packageName, userId);
if (idx < 0) {
@@ -230,7 +230,7 @@ final class OverlayManagerSettings {
}
mItems.remove(moveIdx);
- final int newParentIdx = select(newParentPackageName, userId);
+ final int newParentIdx = select(newParentPackageName, userId) + 1;
mItems.add(newParentIdx, itemToMove);
return moveIdx != newParentIdx;
}
diff --git a/com/android/server/pm/crossprofile/CrossProfileAppsService.java b/com/android/server/pm/CrossProfileAppsService.java
index 0913269f..027a302a 100644
--- a/com/android/server/pm/crossprofile/CrossProfileAppsService.java
+++ b/com/android/server/pm/CrossProfileAppsService.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.server.pm.crossprofile;
+package com.android.server.pm;
import android.content.Context;
diff --git a/com/android/server/pm/crossprofile/CrossProfileAppsServiceImpl.java b/com/android/server/pm/CrossProfileAppsServiceImpl.java
index d6281c51..2007a0e4 100644
--- a/com/android/server/pm/crossprofile/CrossProfileAppsServiceImpl.java
+++ b/com/android/server/pm/CrossProfileAppsServiceImpl.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.server.pm.crossprofile;
+package com.android.server.pm;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
@@ -25,14 +25,12 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
+import android.content.pm.ICrossProfileApps;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ResolveInfo;
-import android.content.pm.crossprofile.ICrossProfileApps;
-import android.graphics.Rect;
import android.os.Binder;
-import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
@@ -111,6 +109,7 @@ public class CrossProfileAppsServiceImpl extends ICrossProfileApps.Stub {
final long ident = mInjector.clearCallingIdentity();
try {
+ launchIntent.setPackage(null);
launchIntent.setComponent(component);
mContext.startActivityAsUser(launchIntent,
ActivityOptions.makeOpenCrossProfileAppsAnimation().toBundle(), user);
diff --git a/com/android/server/pm/Installer.java b/com/android/server/pm/Installer.java
index be66fe22..c04cdf66 100644
--- a/com/android/server/pm/Installer.java
+++ b/com/android/server/pm/Installer.java
@@ -16,7 +16,9 @@
package com.android.server.pm;
+import android.annotation.AppIdInt;
import android.annotation.Nullable;
+import android.annotation.UserIdInt;
import android.content.Context;
import android.content.pm.PackageStats;
import android.os.Build;
@@ -33,6 +35,8 @@ import com.android.server.SystemService;
import dalvik.system.VMRuntime;
+import java.io.FileDescriptor;
+
public class Installer extends SystemService {
private static final String TAG = "Installer";
@@ -58,6 +62,9 @@ public class Installer extends SystemService {
public static final int DEXOPT_STORAGE_DE = 1 << 8;
/** Indicates that dexopt is invoked from the background service. */
public static final int DEXOPT_IDLE_BACKGROUND_JOB = 1 << 9;
+ /* Indicates that dexopt should not restrict access to private APIs.
+ * Must be kept in sync with com.android.internal.os.ZygoteInit. */
+ public static final int DEXOPT_DISABLE_HIDDEN_API_CHECKS = 1 << 10;
// NOTE: keep in sync with installd
public static final int FLAG_CLEAR_CACHE_ONLY = 1 << 8;
@@ -281,13 +288,14 @@ public class Installer extends SystemService {
public void dexopt(String apkPath, int uid, @Nullable String pkgName, String instructionSet,
int dexoptNeeded, @Nullable String outputPath, int dexFlags,
String compilerFilter, @Nullable String volumeUuid, @Nullable String sharedLibraries,
- @Nullable String seInfo, boolean downgrade)
+ @Nullable String seInfo, boolean downgrade, int targetSdkVersion)
throws InstallerException {
assertValidInstructionSet(instructionSet);
if (!checkBeforeRemote()) return;
try {
mInstalld.dexopt(apkPath, uid, pkgName, instructionSet, dexoptNeeded, outputPath,
- dexFlags, compilerFilter, volumeUuid, sharedLibraries, seInfo, downgrade);
+ dexFlags, compilerFilter, volumeUuid, sharedLibraries, seInfo, downgrade,
+ targetSdkVersion);
} catch (Exception e) {
throw InstallerException.from(e);
}
@@ -472,6 +480,16 @@ public class Installer extends SystemService {
}
}
+ public void installApkVerity(String filePath, FileDescriptor verityInput)
+ throws InstallerException {
+ if (!checkBeforeRemote()) return;
+ try {
+ mInstalld.installApkVerity(filePath, verityInput);
+ } catch (Exception e) {
+ throw InstallerException.from(e);
+ }
+ }
+
public boolean reconcileSecondaryDexFile(String apkPath, String packageName, int uid,
String[] isas, @Nullable String volumeUuid, int flags) throws InstallerException {
for (int i = 0; i < isas.length; i++) {
@@ -534,6 +552,17 @@ public class Installer extends SystemService {
}
}
+ public boolean prepareAppProfile(String pkg, @UserIdInt int userId, @AppIdInt int appId,
+ String profileName, String codePath, String dexMetadataPath) throws InstallerException {
+ if (!checkBeforeRemote()) return false;
+ try {
+ return mInstalld.prepareAppProfile(pkg, userId, appId, profileName, codePath,
+ dexMetadataPath);
+ } catch (Exception e) {
+ throw InstallerException.from(e);
+ }
+ }
+
private static void assertValidInstructionSet(String instructionSet)
throws InstallerException {
for (String abi : Build.SUPPORTED_ABIS) {
diff --git a/com/android/server/pm/InstantAppRegistry.java b/com/android/server/pm/InstantAppRegistry.java
index c964f912..af20cd77 100644
--- a/com/android/server/pm/InstantAppRegistry.java
+++ b/com/android/server/pm/InstantAppRegistry.java
@@ -49,7 +49,6 @@ import com.android.internal.os.BackgroundThread;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.XmlUtils;
-import com.android.server.pm.permission.BasePermission;
import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParser;
@@ -302,7 +301,7 @@ class InstantAppRegistry {
// into account but also allow the value from the old computation to avoid
// data loss.
final String[] signaturesSha256Digests = PackageUtils.computeSignaturesSha256Digests(
- pkg.mSignatures);
+ pkg.mSigningDetails.signatures);
final String signaturesSha256Digest = PackageUtils.computeSignaturesSha256Digest(
signaturesSha256Digests);
@@ -313,7 +312,7 @@ class InstantAppRegistry {
}
// For backwards compatibility we accept match based on first signature
- if (pkg.mSignatures.length > 1 && currentCookieFile.equals(computeInstantCookieFile(
+ if (pkg.mSigningDetails.signatures.length > 1 && currentCookieFile.equals(computeInstantCookieFile(
pkg.packageName, signaturesSha256Digests[0], userId))) {
return;
}
@@ -1176,12 +1175,13 @@ class InstantAppRegistry {
// We prefer the modern computation procedure where all certs are taken
// into account and delete the file derived via the legacy hash computation.
File newCookieFile = computeInstantCookieFile(pkg.packageName,
- PackageUtils.computeSignaturesSha256Digest(pkg.mSignatures), userId);
- if (pkg.mSignatures.length > 0) {
- File oldCookieFile = peekInstantCookieFile(pkg.packageName, userId);
- if (oldCookieFile != null && !newCookieFile.equals(oldCookieFile)) {
- oldCookieFile.delete();
- }
+ PackageUtils.computeSignaturesSha256Digest(pkg.mSigningDetails.signatures), userId);
+ if (!pkg.mSigningDetails.hasSignatures()) {
+ Slog.wtf(LOG_TAG, "Parsed Instant App contains no valid signatures!");
+ }
+ File oldCookieFile = peekInstantCookieFile(pkg.packageName, userId);
+ if (oldCookieFile != null && !newCookieFile.equals(oldCookieFile)) {
+ oldCookieFile.delete();
}
cancelPendingPersistLPw(pkg, userId);
addPendingPersistCookieLPw(userId, pkg, cookie, newCookieFile);
diff --git a/com/android/server/pm/KeySetManagerService.java b/com/android/server/pm/KeySetManagerService.java
index fca95857..93d3b775 100644
--- a/com/android/server/pm/KeySetManagerService.java
+++ b/com/android/server/pm/KeySetManagerService.java
@@ -188,7 +188,7 @@ public class KeySetManagerService {
throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
"Passed invalid package to keyset validation.");
}
- ArraySet<PublicKey> signingKeys = pkg.mSigningKeys;
+ ArraySet<PublicKey> signingKeys = pkg.mSigningDetails.publicKeys;
if (signingKeys == null || !(signingKeys.size() > 0) || signingKeys.contains(null)) {
throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
"Package has invalid signing-key-set.");
@@ -223,7 +223,7 @@ public class KeySetManagerService {
PackageSetting ps = mPackages.get(pkg.packageName);
Preconditions.checkNotNull(ps, "pkg: " + pkg.packageName
+ "does not have a corresponding entry in mPackages.");
- addSigningKeySetToPackageLPw(ps, pkg.mSigningKeys);
+ addSigningKeySetToPackageLPw(ps, pkg.mSigningDetails.publicKeys);
if (pkg.mKeySetMapping != null) {
addDefinedKeySetsToPackageLPw(ps, pkg.mKeySetMapping);
if (pkg.mUpgradeKeySets != null) {
@@ -371,7 +371,7 @@ public class KeySetManagerService {
long[] upgradeKeySets = oldPS.keySetData.getUpgradeKeySets();
for (int i = 0; i < upgradeKeySets.length; i++) {
Set<PublicKey> upgradeSet = getPublicKeysFromKeySetLPr(upgradeKeySets[i]);
- if (upgradeSet != null && newPkg.mSigningKeys.containsAll(upgradeSet)) {
+ if (upgradeSet != null && newPkg.mSigningDetails.publicKeys.containsAll(upgradeSet)) {
return true;
}
}
diff --git a/com/android/server/pm/LauncherAppsService.java b/com/android/server/pm/LauncherAppsService.java
index b06b5838..14995b38 100644
--- a/com/android/server/pm/LauncherAppsService.java
+++ b/com/android/server/pm/LauncherAppsService.java
@@ -560,7 +560,6 @@ public class LauncherAppsService extends SystemService {
private boolean startShortcutIntentsAsPublisher(@NonNull Intent[] intents,
@NonNull String publisherPackage, Bundle startActivityOptions, int userId) {
final int code;
- final long ident = injectClearCallingIdentity();
try {
code = mActivityManagerInternal.startActivitiesAsPackage(publisherPackage,
userId, intents, startActivityOptions);
@@ -575,8 +574,6 @@ public class LauncherAppsService extends SystemService {
Slog.d(TAG, "SecurityException while launching intent", e);
}
return false;
- } finally {
- injectRestoreCallingIdentity(ident);
}
}
@@ -652,6 +649,7 @@ public class LauncherAppsService extends SystemService {
activityInfo.name.equals(component.getClassName())) {
// Found an activity with category launcher that matches
// this component so ok to launch.
+ launchIntent.setPackage(null);
launchIntent.setComponent(component);
mContext.startActivityAsUser(launchIntent, opts, user);
return;
diff --git a/com/android/server/pm/OtaDexoptService.java b/com/android/server/pm/OtaDexoptService.java
index 03f662a4..03950119 100644
--- a/com/android/server/pm/OtaDexoptService.java
+++ b/com/android/server/pm/OtaDexoptService.java
@@ -260,12 +260,13 @@ public class OtaDexoptService extends IOtaDexopt.Stub {
public void dexopt(String apkPath, int uid, @Nullable String pkgName,
String instructionSet, int dexoptNeeded, @Nullable String outputPath,
int dexFlags, String compilerFilter, @Nullable String volumeUuid,
- @Nullable String sharedLibraries, @Nullable String seInfo, boolean downgrade)
+ @Nullable String sharedLibraries, @Nullable String seInfo, boolean downgrade,
+ int targetSdkVersion)
throws InstallerException {
final StringBuilder builder = new StringBuilder();
- // The version. Right now it's 3.
- builder.append("3 ");
+ // The version. Right now it's 4.
+ builder.append("4 ");
builder.append("dexopt");
@@ -281,6 +282,7 @@ public class OtaDexoptService extends IOtaDexopt.Stub {
encodeParameter(builder, sharedLibraries);
encodeParameter(builder, seInfo);
encodeParameter(builder, downgrade);
+ encodeParameter(builder, targetSdkVersion);
commands.add(builder.toString());
}
diff --git a/com/android/server/pm/PackageDexOptimizer.java b/com/android/server/pm/PackageDexOptimizer.java
index 730a9fda..6a08e1b5 100644
--- a/com/android/server/pm/PackageDexOptimizer.java
+++ b/com/android/server/pm/PackageDexOptimizer.java
@@ -55,6 +55,7 @@ import static com.android.server.pm.Installer.DEXOPT_FORCE;
import static com.android.server.pm.Installer.DEXOPT_STORAGE_CE;
import static com.android.server.pm.Installer.DEXOPT_STORAGE_DE;
import static com.android.server.pm.Installer.DEXOPT_IDLE_BACKGROUND_JOB;
+import static com.android.server.pm.Installer.DEXOPT_DISABLE_HIDDEN_API_CHECKS;
import static com.android.server.pm.InstructionSets.getAppDexInstructionSets;
import static com.android.server.pm.InstructionSets.getDexCodeInstructionSets;
@@ -274,7 +275,7 @@ public class PackageDexOptimizer {
// primary dex files.
mInstaller.dexopt(path, uid, pkg.packageName, isa, dexoptNeeded, oatDir, dexoptFlags,
compilerFilter, pkg.volumeUuid, classLoaderContext, pkg.applicationInfo.seInfo,
- false /* downgrade*/);
+ false /* downgrade*/, pkg.applicationInfo.targetSdkVersion);
if (packageStats != null) {
long endTime = System.currentTimeMillis();
@@ -395,7 +396,7 @@ public class PackageDexOptimizer {
mInstaller.dexopt(path, info.uid, info.packageName, isa, /*dexoptNeeded*/ 0,
/*oatDir*/ null, dexoptFlags,
compilerFilter, info.volumeUuid, classLoaderContext, info.seInfoUser,
- options.isDowngrade());
+ options.isDowngrade(), info.targetSdkVersion);
}
return DEX_OPT_PERFORMED;
@@ -509,12 +510,18 @@ public class PackageDexOptimizer {
boolean isProfileGuidedFilter = isProfileGuidedCompilerFilter(compilerFilter);
boolean isPublic = !info.isForwardLocked() && !isProfileGuidedFilter;
int profileFlag = isProfileGuidedFilter ? DEXOPT_PROFILE_GUIDED : 0;
+ // System apps are invoked with a runtime flag which exempts them from
+ // restrictions on hidden API usage. We dexopt with the same runtime flag
+ // otherwise offending methods would have to be re-verified at runtime
+ // and we want to avoid the performance overhead of that.
+ int hiddenApiFlag = info.isAllowedToUseHiddenApi() ? DEXOPT_DISABLE_HIDDEN_API_CHECKS : 0;
int dexFlags =
(isPublic ? DEXOPT_PUBLIC : 0)
| (debuggable ? DEXOPT_DEBUGGABLE : 0)
| profileFlag
| (options.isBootComplete() ? DEXOPT_BOOTCOMPLETE : 0)
- | (options.isDexoptIdleBackgroundJob() ? DEXOPT_IDLE_BACKGROUND_JOB : 0);
+ | (options.isDexoptIdleBackgroundJob() ? DEXOPT_IDLE_BACKGROUND_JOB : 0)
+ | hiddenApiFlag;
return adjustDexoptFlags(dexFlags);
}
@@ -629,6 +636,9 @@ public class PackageDexOptimizer {
if ((flags & DEXOPT_IDLE_BACKGROUND_JOB) == DEXOPT_IDLE_BACKGROUND_JOB) {
flagsList.add("idle_background_job");
}
+ if ((flags & DEXOPT_DISABLE_HIDDEN_API_CHECKS) == DEXOPT_DISABLE_HIDDEN_API_CHECKS) {
+ flagsList.add("disable_hidden_api_checks");
+ }
return String.join(",", flagsList);
}
diff --git a/com/android/server/pm/PackageInstallerService.java b/com/android/server/pm/PackageInstallerService.java
index 14128a78..16fae99a 100644
--- a/com/android/server/pm/PackageInstallerService.java
+++ b/com/android/server/pm/PackageInstallerService.java
@@ -415,7 +415,12 @@ public class PackageInstallerService extends IPackageInstaller.Stub {
params.installFlags |= PackageManager.INSTALL_FROM_ADB;
} else {
- mAppOps.checkPackage(callingUid, installerPackageName);
+ // Only apps with INSTALL_PACKAGES are allowed to set an installer that is not the
+ // caller.
+ if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES) !=
+ PackageManager.PERMISSION_GRANTED) {
+ mAppOps.checkPackage(callingUid, installerPackageName);
+ }
params.installFlags &= ~PackageManager.INSTALL_FROM_ADB;
params.installFlags &= ~PackageManager.INSTALL_ALL_USERS;
diff --git a/com/android/server/pm/PackageInstallerSession.java b/com/android/server/pm/PackageInstallerSession.java
index 5cf08dc4..a6ff4f7e 100644
--- a/com/android/server/pm/PackageInstallerSession.java
+++ b/com/android/server/pm/PackageInstallerSession.java
@@ -17,10 +17,12 @@
package com.android.server.pm;
import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED;
+import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_DEX_METADATA;
import static android.content.pm.PackageManager.INSTALL_FAILED_CONTAINER_ERROR;
import static android.content.pm.PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
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.PackageParser.APK_FILE_EXTENSION;
import static android.system.OsConstants.O_CREAT;
import static android.system.OsConstants.O_RDONLY;
import static android.system.OsConstants.O_WRONLY;
@@ -58,7 +60,6 @@ import android.content.pm.PackageParser;
import android.content.pm.PackageParser.ApkLite;
import android.content.pm.PackageParser.PackageLite;
import android.content.pm.PackageParser.PackageParserException;
-import android.content.pm.Signature;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Binder;
@@ -84,6 +85,7 @@ import android.util.ArraySet;
import android.util.ExceptionUtils;
import android.util.MathUtils;
import android.util.Slog;
+import android.util.apk.ApkSignatureVerifier;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.content.NativeLibraryHelper;
@@ -96,6 +98,7 @@ import com.android.server.LocalServices;
import com.android.server.pm.Installer.InstallerException;
import com.android.server.pm.PackageInstallerService.PackageInstallObserverAdapter;
+import android.content.pm.dex.DexMetadataHelper;
import libcore.io.IoUtils;
import org.xmlpull.v1.XmlPullParser;
@@ -107,7 +110,7 @@ import java.io.FileDescriptor;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -227,9 +230,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
@GuardedBy("mLock")
private long mVersionCode;
@GuardedBy("mLock")
- private Signature[] mSignatures;
- @GuardedBy("mLock")
- private Certificate[][] mCertificates;
+ private PackageParser.SigningDetails mSigningDetails;
/**
* Path to the validated base APK for this session, which may point at an
@@ -261,6 +262,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
// entries like "lost+found".
if (file.isDirectory()) return false;
if (file.getName().endsWith(REMOVE_SPLIT_MARKER_EXTENSION)) return false;
+ if (DexMetadataHelper.isDexMetadataFile(file)) return false;
return true;
}
};
@@ -342,17 +344,22 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
final boolean isSelfUpdatePermissionGranted =
(mPm.checkUidPermission(android.Manifest.permission.INSTALL_SELF_UPDATES,
mInstallerUid) == PackageManager.PERMISSION_GRANTED);
+ final boolean isUpdatePermissionGranted =
+ (mPm.checkUidPermission(android.Manifest.permission.INSTALL_PACKAGE_UPDATES,
+ mInstallerUid) == PackageManager.PERMISSION_GRANTED);
+ final int targetPackageUid = mPm.getPackageUid(mPackageName, 0, userId);
final boolean isPermissionGranted = isInstallPermissionGranted
- || (isSelfUpdatePermissionGranted
- && mPm.getPackageUid(mPackageName, 0, userId) == mInstallerUid);
+ || (isUpdatePermissionGranted && targetPackageUid != -1)
+ || (isSelfUpdatePermissionGranted && targetPackageUid == mInstallerUid);
final boolean isInstallerRoot = (mInstallerUid == Process.ROOT_UID);
+ final boolean isInstallerSystem = (mInstallerUid == Process.SYSTEM_UID);
final boolean forcePermissionPrompt =
(params.installFlags & PackageManager.INSTALL_FORCE_PERMISSION_PROMPT) != 0;
// 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
- || isInstallerDeviceOwnerOrAffiliatedProfileOwnerLocked());
+ || isInstallerSystem || isInstallerDeviceOwnerOrAffiliatedProfileOwnerLocked());
}
public PackageInstallerSession(PackageInstallerService.InternalCallback callback,
@@ -856,7 +863,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
}
Preconditions.checkNotNull(mPackageName);
- Preconditions.checkNotNull(mSignatures);
+ Preconditions.checkNotNull(mSigningDetails);
Preconditions.checkNotNull(mResolvedBaseFile);
if (needToAskForPermissionsLocked()) {
@@ -937,7 +944,16 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
mRelinquished = true;
mPm.installStage(mPackageName, stageDir, localObserver, params,
- mInstallerPackageName, mInstallerUid, user, mCertificates);
+ mInstallerPackageName, mInstallerUid, user, mSigningDetails);
+ }
+
+ private static void maybeRenameFile(File from, File to) throws PackageManagerException {
+ if (!from.equals(to)) {
+ if (!from.renameTo(to)) {
+ throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR,
+ "Could not rename file " + from + " to " + to);
+ }
+ }
}
/**
@@ -956,7 +972,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
throws PackageManagerException {
mPackageName = null;
mVersionCode = -1;
- mSignatures = null;
+ mSigningDetails = PackageParser.SigningDetails.UNKNOWN;
mResolvedBaseFile = null;
mResolvedStagedFiles.clear();
@@ -984,16 +1000,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
if (ArrayUtils.isEmpty(addedFiles) && removeSplitList.size() == 0) {
throw new PackageManagerException(INSTALL_FAILED_INVALID_APK, "No packages staged");
}
+
// Verify that all staged packages are internally consistent
final ArraySet<String> stagedSplits = new ArraySet<>();
for (File addedFile : addedFiles) {
final ApkLite apk;
try {
- int flags = PackageParser.PARSE_COLLECT_CERTIFICATES;
- if ((params.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {
- flags |= PackageParser.PARSE_IS_EPHEMERAL;
- }
- apk = PackageParser.parseApkLite(addedFile, flags);
+ apk = PackageParser.parseApkLite(
+ addedFile, PackageParser.PARSE_COLLECT_CERTIFICATES);
} catch (PackageParserException e) {
throw PackageManagerException.from(e);
}
@@ -1008,9 +1022,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
mPackageName = apk.packageName;
mVersionCode = apk.getLongVersionCode();
}
- if (mSignatures == null) {
- mSignatures = apk.signatures;
- mCertificates = apk.certificates;
+ if (mSigningDetails == PackageParser.SigningDetails.UNKNOWN) {
+ mSigningDetails = apk.signingDetails;
}
assertApkConsistentLocked(String.valueOf(addedFile), apk);
@@ -1018,9 +1031,9 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
// Take this opportunity to enforce uniform naming
final String targetName;
if (apk.splitName == null) {
- targetName = "base.apk";
+ targetName = "base" + APK_FILE_EXTENSION;
} else {
- targetName = "split_" + apk.splitName + ".apk";
+ targetName = "split_" + apk.splitName + APK_FILE_EXTENSION;
}
if (!FileUtils.isValidExtFilename(targetName)) {
throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
@@ -1028,9 +1041,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
}
final File targetFile = new File(mResolvedStageDir, targetName);
- if (!addedFile.equals(targetFile)) {
- addedFile.renameTo(targetFile);
- }
+ maybeRenameFile(addedFile, targetFile);
// Base is coming from session
if (apk.splitName == null) {
@@ -1038,6 +1049,18 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
}
mResolvedStagedFiles.add(targetFile);
+
+ final File dexMetadataFile = DexMetadataHelper.findDexMetadataForFile(addedFile);
+ if (dexMetadataFile != null) {
+ if (!FileUtils.isValidExtFilename(dexMetadataFile.getName())) {
+ throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
+ "Invalid filename: " + dexMetadataFile);
+ }
+ final File targetDexMetadataFile = new File(mResolvedStageDir,
+ DexMetadataHelper.buildDexMetadataPathForApk(targetName));
+ mResolvedStagedFiles.add(targetDexMetadataFile);
+ maybeRenameFile(dexMetadataFile, targetDexMetadataFile);
+ }
}
if (removeSplitList.size() > 0) {
@@ -1059,8 +1082,15 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
mPackageName = pkgInfo.packageName;
mVersionCode = pkgInfo.getLongVersionCode();
}
- if (mSignatures == null) {
- mSignatures = pkgInfo.signatures;
+ if (mSigningDetails == PackageParser.SigningDetails.UNKNOWN) {
+ try {
+ mSigningDetails = ApkSignatureVerifier.plsCertsNoVerifyOnlyCerts(
+ pkgInfo.applicationInfo.sourceDir,
+ PackageParser.SigningDetails.SignatureSchemeVersion.JAR);
+ } catch (PackageParserException e) {
+ throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
+ "Couldn't obtain signatures from base APK");
+ }
}
}
@@ -1095,6 +1125,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
if (mResolvedBaseFile == null) {
mResolvedBaseFile = new File(appInfo.getBaseCodePath());
mResolvedInheritedFiles.add(mResolvedBaseFile);
+ // Inherit the dex metadata if present.
+ final File baseDexMetadataFile =
+ DexMetadataHelper.findDexMetadataForFile(mResolvedBaseFile);
+ if (baseDexMetadataFile != null) {
+ mResolvedInheritedFiles.add(baseDexMetadataFile);
+ }
}
// Inherit splits if not overridden
@@ -1105,6 +1141,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
final boolean splitRemoved = removeSplitList.contains(splitName);
if (!stagedSplits.contains(splitName) && !splitRemoved) {
mResolvedInheritedFiles.add(splitFile);
+ // Inherit the dex metadata if present.
+ final File splitDexMetadataFile =
+ DexMetadataHelper.findDexMetadataForFile(splitFile);
+ if (splitDexMetadataFile != null) {
+ mResolvedInheritedFiles.add(splitDexMetadataFile);
+ }
}
}
}
@@ -1154,50 +1196,13 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
+ " version code " + apk.versionCode + " inconsistent with "
+ mVersionCode);
}
- if (!Signature.areExactMatch(mSignatures, apk.signatures)) {
+ if (!mSigningDetails.signaturesMatchExactly(apk.signingDetails)) {
throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
tag + " signatures are inconsistent");
}
}
/**
- * Calculate the final install footprint size, combining both staged and
- * existing APKs together and including unpacked native code from both.
- */
- private long calculateInstalledSize() throws PackageManagerException {
- Preconditions.checkNotNull(mResolvedBaseFile);
-
- final ApkLite baseApk;
- try {
- baseApk = PackageParser.parseApkLite(mResolvedBaseFile, 0);
- } catch (PackageParserException e) {
- throw PackageManagerException.from(e);
- }
-
- final List<String> splitPaths = new ArrayList<>();
- for (File file : mResolvedStagedFiles) {
- if (mResolvedBaseFile.equals(file)) continue;
- splitPaths.add(file.getAbsolutePath());
- }
- for (File file : mResolvedInheritedFiles) {
- if (mResolvedBaseFile.equals(file)) continue;
- splitPaths.add(file.getAbsolutePath());
- }
-
- // This is kind of hacky; we're creating a half-parsed package that is
- // straddled between the inherited and staged APKs.
- final PackageLite pkg = new PackageLite(null, baseApk, null, null, null, null,
- splitPaths.toArray(new String[splitPaths.size()]), null);
-
- try {
- return PackageHelper.calculateInstalledSize(pkg, params.abiOverride);
- } catch (IOException e) {
- throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
- "Failed to calculate install size", e);
- }
- }
-
- /**
* Determine if creating hard links between source and destination is
* possible. That is, do they all live on the same underlying device.
*/
diff --git a/com/android/server/pm/PackageManagerService.java b/com/android/server/pm/PackageManagerService.java
index 1157af41..5dfd3ae4 100644
--- a/com/android/server/pm/PackageManagerService.java
+++ b/com/android/server/pm/PackageManagerService.java
@@ -17,12 +17,15 @@
package com.android.server.pm;
import static android.Manifest.permission.DELETE_PACKAGES;
+import static android.Manifest.permission.SET_HARMFUL_APP_WARNINGS;
import static android.Manifest.permission.INSTALL_PACKAGES;
import static android.Manifest.permission.MANAGE_PROFILE_AND_DEVICE_OWNERS;
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
import static android.Manifest.permission.REQUEST_DELETE_PACKAGES;
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
import static android.Manifest.permission.WRITE_MEDIA_STORAGE;
+import static android.content.pm.PackageManager.CERT_INPUT_RAW_X509;
+import static android.content.pm.PackageManager.CERT_INPUT_SHA256;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED;
import static android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED;
@@ -47,14 +50,11 @@ 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;
import static android.content.pm.PackageManager.INSTALL_FAILED_SHARED_USER_INCOMPATIBLE;
import static android.content.pm.PackageManager.INSTALL_FAILED_TEST_ONLY;
import static android.content.pm.PackageManager.INSTALL_FAILED_UPDATE_INCOMPATIBLE;
-import static android.content.pm.PackageManager.INSTALL_FAILED_USER_RESTRICTED;
import static android.content.pm.PackageManager.INSTALL_FAILED_VERSION_DOWNGRADE;
import static android.content.pm.PackageManager.INSTALL_INTERNAL;
import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES;
@@ -108,6 +108,8 @@ import static com.android.server.pm.PackageManagerServiceUtils.dumpCriticalInfo;
import static com.android.server.pm.PackageManagerServiceUtils.getCompressedFiles;
import static com.android.server.pm.PackageManagerServiceUtils.getLastModifiedTime;
import static com.android.server.pm.PackageManagerServiceUtils.logCriticalInfo;
+import static com.android.server.pm.PackageManagerServiceUtils.signingDetailsHasCertificate;
+import static com.android.server.pm.PackageManagerServiceUtils.signingDetailsHasSha256Certificate;
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;
@@ -118,6 +120,7 @@ import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
import android.app.IActivityManager;
import android.app.ResourcesManager;
@@ -167,7 +170,6 @@ 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;
@@ -176,6 +178,7 @@ 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.PackageParser.SigningDetails.SignatureSchemeVersion;
import android.content.pm.PackageStats;
import android.content.pm.PackageUserState;
import android.content.pm.ParceledListSlice;
@@ -190,6 +193,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.DexMetadataHelper;
import android.content.pm.dex.IArtManager;
import android.content.res.Resources;
import android.database.ContentObserver;
@@ -308,6 +312,7 @@ import com.android.server.pm.permission.DefaultPermissionGrantPolicy.DefaultPerm
import com.android.server.pm.permission.PermissionManagerInternal.PermissionCallback;
import com.android.server.pm.permission.PermissionsState;
import com.android.server.pm.permission.PermissionsState.PermissionState;
+import com.android.server.security.VerityUtils;
import com.android.server.storage.DeviceStorageMonitorInternal;
import dalvik.system.CloseGuard;
@@ -337,7 +342,6 @@ import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.SecureRandom;
-import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
@@ -445,7 +449,6 @@ public class PackageManagerService extends IPackageManager.Stub
static final int SCAN_NEW_INSTALL = 1<<2;
static final int SCAN_UPDATE_TIME = 1<<3;
static final int SCAN_BOOTING = 1<<4;
- static final int SCAN_TRUSTED_OVERLAY = 1<<5;
static final int SCAN_DELETE_DATA_ON_FAILURES = 1<<6;
static final int SCAN_REQUIRE_KNOWN = 1<<7;
static final int SCAN_MOVE = 1<<8;
@@ -468,7 +471,6 @@ public class PackageManagerService extends IPackageManager.Stub
SCAN_NEW_INSTALL,
SCAN_UPDATE_TIME,
SCAN_BOOTING,
- SCAN_TRUSTED_OVERLAY,
SCAN_DELETE_DATA_ON_FAILURES,
SCAN_REQUIRE_KNOWN,
SCAN_MOVE,
@@ -771,7 +773,7 @@ public class PackageManagerService extends IPackageManager.Stub
Collection<PackageParser.Package> allPackages, String targetPackageName) {
List<PackageParser.Package> overlayPackages = null;
for (PackageParser.Package p : allPackages) {
- if (targetPackageName.equals(p.mOverlayTarget) && p.mIsStaticOverlay) {
+ if (targetPackageName.equals(p.mOverlayTarget) && p.mOverlayIsStatic) {
if (overlayPackages == null) {
overlayPackages = new ArrayList<PackageParser.Package>();
}
@@ -855,7 +857,7 @@ public class PackageManagerService extends IPackageManager.Stub
void findStaticOverlayPackages() {
synchronized (mPackages) {
for (PackageParser.Package p : mPackages.values()) {
- if (p.mIsStaticOverlay) {
+ if (p.mOverlayIsStatic) {
if (mOverlayPackages == null) {
mOverlayPackages = new ArrayList<PackageParser.Package>();
}
@@ -992,6 +994,7 @@ public class PackageManagerService extends IPackageManager.Stub
private List<String> mKeepUninstalledPackages;
private UserManagerInternal mUserManagerInternal;
+ private ActivityManagerInternal mActivityManagerInternal;
private DeviceIdleController.LocalService mDeviceIdleController;
@@ -2424,6 +2427,7 @@ public class PackageManagerService extends IPackageManager.Stub
installer, mInstallLock);
mDexManager = new DexManager(this, mPackageDexOptimizer, installer, mInstallLock,
dexManagerListener);
+ mArtManagerService = new ArtManagerService(this, installer, mInstallLock);
mMoveCallbacks = new MoveCallbacks(FgThread.get().getLooper());
mOnPermissionChangeListeners = new OnPermissionChangeListeners(
@@ -2554,7 +2558,7 @@ public class PackageManagerService extends IPackageManager.Stub
| PackageParser.PARSE_IS_SYSTEM_DIR,
scanFlags
| SCAN_AS_SYSTEM
- | SCAN_TRUSTED_OVERLAY,
+ | SCAN_AS_VENDOR,
0);
mParallelPackageParserCallback.findStaticOverlayPackages();
@@ -2697,6 +2701,12 @@ public class PackageManagerService extends IPackageManager.Stub
mSettings.getDisabledSystemPkgLPr(ps.name);
if (disabledPs.codePath == null || !disabledPs.codePath.exists()
|| disabledPs.pkg == null) {
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "Possibly deleted app: " + ps.dumpState_temp()
+ + "; path: " + (disabledPs.codePath == null ? "<<NULL>>":disabledPs.codePath)
+ + "; pkg: " + (disabledPs.pkg==null?"<<NULL>>":disabledPs.pkg.toString()));
+}
possiblyDeletedUpdatedSystemApps.add(ps.name);
}
}
@@ -2748,7 +2758,10 @@ public class PackageManagerService extends IPackageManager.Stub
for (String deletedAppName : possiblyDeletedUpdatedSystemApps) {
PackageParser.Package deletedPkg = mPackages.get(deletedAppName);
mSettings.removeDisabledSystemPackageLPw(deletedAppName);
-
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "remove update; name: " + deletedAppName + ", exists? " + (deletedPkg != null));
+}
final String msg;
if (deletedPkg == null) {
// should have found an update, but, we didn't; remove everything
@@ -3071,7 +3084,6 @@ 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) {
@@ -4566,6 +4578,14 @@ public class PackageManagerService extends IPackageManager.Stub
return mUserManagerInternal;
}
+ private ActivityManagerInternal getActivityManagerInternal() {
+ if (mActivityManagerInternal == null) {
+ mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
+ }
+ return mActivityManagerInternal;
+ }
+
+
private DeviceIdleController.LocalService getDeviceIdleController() {
if (mDeviceIdleController == null) {
mDeviceIdleController =
@@ -4726,8 +4746,12 @@ public class PackageManagerService extends IPackageManager.Stub
int filterCallingUid, int userId) {
if (!sUserManager.exists(userId)) return null;
flags = updateFlagsForComponent(flags, userId, component);
- mPermissionManager.enforceCrossUserPermission(Binder.getCallingUid(), userId,
- false /* requireFullPermission */, false /* checkShell */, "get activity info");
+
+ if (!isRecentsAccessingChildProfiles(Binder.getCallingUid(), userId)) {
+ mPermissionManager.enforceCrossUserPermission(Binder.getCallingUid(), userId,
+ false /* requireFullPermission */, false /* checkShell */, "get activity info");
+ }
+
synchronized (mPackages) {
PackageParser.Activity a = mActivities.mActivities.get(component);
@@ -4749,6 +4773,22 @@ public class PackageManagerService extends IPackageManager.Stub
return null;
}
+ private boolean isRecentsAccessingChildProfiles(int callingUid, int targetUserId) {
+ if (!getActivityManagerInternal().isCallerRecents(callingUid)) {
+ return false;
+ }
+ final long token = Binder.clearCallingIdentity();
+ try {
+ final int callingUserId = UserHandle.getUserId(callingUid);
+ if (ActivityManager.getCurrentUser() != callingUserId) {
+ return false;
+ }
+ return sUserManager.isSameProfileGroup(callingUserId, targetUserId);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
@Override
public boolean activitySupportsIntent(ComponentName component, Intent intent,
String resolvedType) {
@@ -5360,7 +5400,7 @@ public class PackageManagerService extends IPackageManager.Stub
|| filterAppAccessLPr(ps2, callingUid, callingUserId)) {
return PackageManager.SIGNATURE_UNKNOWN_PACKAGE;
}
- return compareSignatures(p1.mSignatures, p2.mSignatures);
+ return compareSignatures(p1.mSigningDetails.signatures, p2.mSigningDetails.signatures);
}
}
@@ -5382,13 +5422,13 @@ public class PackageManagerService extends IPackageManager.Stub
if (isCallerInstantApp) {
return PackageManager.SIGNATURE_UNKNOWN_PACKAGE;
}
- s1 = ((SharedUserSetting)obj).signatures.mSignatures;
+ s1 = ((SharedUserSetting)obj).signatures.mSigningDetails.signatures;
} else if (obj instanceof PackageSetting) {
final PackageSetting ps = (PackageSetting) obj;
if (filterAppAccessLPr(ps, callingUid, callingUserId)) {
return PackageManager.SIGNATURE_UNKNOWN_PACKAGE;
}
- s1 = ps.signatures.mSignatures;
+ s1 = ps.signatures.mSigningDetails.signatures;
} else {
return PackageManager.SIGNATURE_UNKNOWN_PACKAGE;
}
@@ -5401,13 +5441,13 @@ public class PackageManagerService extends IPackageManager.Stub
if (isCallerInstantApp) {
return PackageManager.SIGNATURE_UNKNOWN_PACKAGE;
}
- s2 = ((SharedUserSetting)obj).signatures.mSignatures;
+ s2 = ((SharedUserSetting)obj).signatures.mSigningDetails.signatures;
} else if (obj instanceof PackageSetting) {
final PackageSetting ps = (PackageSetting) obj;
if (filterAppAccessLPr(ps, callingUid, callingUserId)) {
return PackageManager.SIGNATURE_UNKNOWN_PACKAGE;
}
- s2 = ps.signatures.mSignatures;
+ s2 = ps.signatures.mSigningDetails.signatures;
} else {
return PackageManager.SIGNATURE_UNKNOWN_PACKAGE;
}
@@ -5418,6 +5458,73 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
+ @Override
+ public boolean hasSigningCertificate(
+ String packageName, byte[] certificate, @PackageManager.CertificateInputType int type) {
+
+ synchronized (mPackages) {
+ final PackageParser.Package p = mPackages.get(packageName);
+ if (p == null || p.mExtras == null) {
+ return false;
+ }
+ final int callingUid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(callingUid);
+ final PackageSetting ps = (PackageSetting) p.mExtras;
+ if (filterAppAccessLPr(ps, callingUid, callingUserId)) {
+ return false;
+ }
+ switch (type) {
+ case CERT_INPUT_RAW_X509:
+ return signingDetailsHasCertificate(certificate, p.mSigningDetails);
+ case CERT_INPUT_SHA256:
+ return signingDetailsHasSha256Certificate(certificate, p.mSigningDetails);
+ default:
+ return false;
+ }
+ }
+ }
+
+ @Override
+ public boolean hasUidSigningCertificate(
+ int uid, byte[] certificate, @PackageManager.CertificateInputType int type) {
+ final int callingUid = Binder.getCallingUid();
+ final int callingUserId = UserHandle.getUserId(callingUid);
+ // Map to base uids.
+ uid = UserHandle.getAppId(uid);
+ // reader
+ synchronized (mPackages) {
+ final PackageParser.SigningDetails signingDetails;
+ final Object obj = mSettings.getUserIdLPr(uid);
+ if (obj != null) {
+ if (obj instanceof SharedUserSetting) {
+ final boolean isCallerInstantApp = getInstantAppPackageName(callingUid) != null;
+ if (isCallerInstantApp) {
+ return false;
+ }
+ signingDetails = ((SharedUserSetting)obj).signatures.mSigningDetails;
+ } else if (obj instanceof PackageSetting) {
+ final PackageSetting ps = (PackageSetting) obj;
+ if (filterAppAccessLPr(ps, callingUid, callingUserId)) {
+ return false;
+ }
+ signingDetails = ps.signatures.mSigningDetails;
+ } else {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ switch (type) {
+ case CERT_INPUT_RAW_X509:
+ return signingDetailsHasCertificate(certificate, signingDetails);
+ case CERT_INPUT_SHA256:
+ return signingDetailsHasSha256Certificate(certificate, signingDetails);
+ default:
+ return false;
+ }
+ }
+ }
+
/**
* This method should typically only be used when granting or revoking
* permissions, since the app may immediately restart after this call.
@@ -8184,36 +8291,32 @@ public class PackageManagerService extends IPackageManager.Stub
}
private void collectCertificatesLI(PackageSetting ps, PackageParser.Package pkg,
- final @ParseFlags int parseFlags) throws PackageManagerException {
+ final @ParseFlags int parseFlags, boolean forceCollect) throws PackageManagerException {
// When upgrading from pre-N MR1, verify the package time stamp using the package
// directory and not the APK file.
final long lastModifiedTime = mIsPreNMR1Upgrade
? new File(pkg.codePath).lastModified() : getLastModifiedTime(pkg);
- if (ps != null
+ if (ps != null && !forceCollect
&& ps.codePathString.equals(pkg.codePath)
&& ps.timeStamp == lastModifiedTime
&& !isCompatSignatureUpdateNeeded(pkg)
&& !isRecoverSignatureUpdateNeeded(pkg)) {
- long mSigningKeySetId = ps.keySetData.getProperSigningKeySet();
- final KeySetManagerService ksms = mSettings.mKeySetManagerService;
- ArraySet<PublicKey> signingKs;
- synchronized (mPackages) {
- signingKs = ksms.getPublicKeysFromKeySetLPr(mSigningKeySetId);
- }
- if (ps.signatures.mSignatures != null
- && ps.signatures.mSignatures.length != 0
- && signingKs != null) {
- // Optimization: reuse the existing cached certificates
+ if (ps.signatures.mSigningDetails.signatures != null
+ && ps.signatures.mSigningDetails.signatures.length != 0
+ && ps.signatures.mSigningDetails.signatureSchemeVersion
+ != SignatureSchemeVersion.UNKNOWN) {
+ // Optimization: reuse the existing cached signing data
// if the package appears to be unchanged.
- pkg.mSignatures = ps.signatures.mSignatures;
- pkg.mSigningKeys = signingKs;
+ pkg.mSigningDetails =
+ new PackageParser.SigningDetails(ps.signatures.mSigningDetails);
return;
}
Slog.w(TAG, "PackageSetting for " + ps.name
+ " is missing signatures. Collecting certs again to recover them.");
} else {
- Slog.i(TAG, toString() + " changed; collecting certs");
+ Slog.i(TAG, pkg.codePath + " changed; collecting certs" +
+ (forceCollect ? " (forced)" : ""));
}
try {
@@ -8293,14 +8396,14 @@ public class PackageManagerService extends IPackageManager.Stub
}
// Scan the parent
- PackageParser.Package scannedPkg = scanPackageInternalLI(pkg, parseFlags,
+ PackageParser.Package scannedPkg = addForInitLI(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 childPackage = pkg.childPackages.get(i);
- scanPackageInternalLI(childPackage, parseFlags, scanFlags,
+ addForInitLI(childPackage, parseFlags, scanFlags,
currentTime, user);
}
@@ -8312,313 +8415,309 @@ public class PackageManagerService extends IPackageManager.Stub
return scannedPkg;
}
+ // Temporary to catch potential issues with refactoring
+ private static boolean REFACTOR_DEBUG = true;
/**
- * Scans a package and returns the newly parsed package.
- * @throws PackageManagerException on a parse error.
+ * Adds a new package to the internal data structures during platform initialization.
+ * <p>After adding, the package is known to the system and available for querying.
+ * <p>For packages located on the device ROM [eg. packages located in /system, /vendor,
+ * etc...], additional checks are performed. Basic verification [such as ensuring
+ * matching signatures, checking version codes, etc...] occurs if the package is
+ * identical to a previously known package. If the package fails a signature check,
+ * the version installed on /data will be removed. If the version of the new package
+ * is less than or equal than the version on /data, it will be ignored.
+ * <p>Regardless of the package location, the results are applied to the internal
+ * structures and the package is made available to the rest of the system.
+ * <p>NOTE: The return value should be removed. It's the passed in package object.
*/
- private PackageParser.Package scanPackageInternalLI(PackageParser.Package pkg,
+ private PackageParser.Package addForInitLI(PackageParser.Package pkg,
@ParseFlags int parseFlags, @ScanFlags int scanFlags, long currentTime,
@Nullable UserHandle user)
throws PackageManagerException {
- PackageSetting ps = null;
- PackageSetting updatedPs;
- // reader
+ final boolean scanSystemPartition = (parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) != 0;
+ final String renamedPkgName;
+ final PackageSetting disabledPkgSetting;
+ final boolean isSystemPkgUpdated;
+ final boolean pkgAlreadyExists;
+ PackageSetting pkgSetting;
+
+ // NOTE: installPackageLI() has the same code to setup the package's
+ // application info. This probably should be done lower in the call
+ // stack [such as scanPackageOnly()]. However, we verify the application
+ // info prior to that [in scanPackageNew()] and thus have to setup
+ // the application info early.
+ pkg.setApplicationVolumeUuid(pkg.volumeUuid);
+ pkg.setApplicationInfoCodePath(pkg.codePath);
+ pkg.setApplicationInfoBaseCodePath(pkg.baseCodePath);
+ pkg.setApplicationInfoSplitCodePaths(pkg.splitCodePaths);
+ pkg.setApplicationInfoResourcePath(pkg.codePath);
+ pkg.setApplicationInfoBaseResourcePath(pkg.baseCodePath);
+ pkg.setApplicationInfoSplitResourcePaths(pkg.splitCodePaths);
+
synchronized (mPackages) {
- // Look to see if we already know about this package.
- String oldName = mSettings.getRenamedPackageLPr(pkg.packageName);
- if (pkg.mOriginalPackages != null && pkg.mOriginalPackages.contains(oldName)) {
- // This package has been renamed to its original name. Let's
- // use that.
- ps = mSettings.getPackageLPr(oldName);
- }
- // If there was no original package, see one for the real package name.
- if (ps == null) {
- ps = mSettings.getPackageLPr(pkg.packageName);
- }
- // Check to see if this package could be hiding/updating a system
- // package. Must look for it either under the original or real
- // package name depending on our state.
- updatedPs = mSettings.getDisabledSystemPkgLPr(ps != null ? ps.name : pkg.packageName);
- if (DEBUG_INSTALL && updatedPs != null) Slog.d(TAG, "updatedPkg = " + updatedPs);
-
- // If this is a package we don't know about on the system partition, we
- // may need to remove disabled child packages on the system partition
- // or may need to not add child packages if the parent apk is updated
- // on the data partition and no longer defines this child package.
- if ((scanFlags & SCAN_AS_SYSTEM) != 0) {
- // If this is a parent package for an updated system app and this system
- // app got an OTA update which no longer defines some of the child packages
- // we have to prune them from the disabled system packages.
- PackageSetting disabledPs = mSettings.getDisabledSystemPkgLPr(pkg.packageName);
- if (disabledPs != null) {
+ renamedPkgName = mSettings.getRenamedPackageLPr(pkg.mRealPackage);
+ final String realPkgName = getRealPackageName(pkg, renamedPkgName);
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "Add pkg: " + pkg.packageName + (realPkgName==null?"":", realName: " + realPkgName));
+}
+ if (realPkgName != null) {
+ ensurePackageRenamed(pkg, renamedPkgName);
+ }
+ final PackageSetting originalPkgSetting = getOriginalPackageLocked(pkg, renamedPkgName);
+ final PackageSetting installedPkgSetting = mSettings.getPackageLPr(pkg.packageName);
+ pkgSetting = originalPkgSetting == null ? installedPkgSetting : originalPkgSetting;
+ pkgAlreadyExists = pkgSetting != null;
+ final String disabledPkgName = pkgAlreadyExists ? pkgSetting.name : pkg.packageName;
+ disabledPkgSetting = mSettings.getDisabledSystemPkgLPr(disabledPkgName);
+ isSystemPkgUpdated = disabledPkgSetting != null;
+
+ if (DEBUG_INSTALL && isSystemPkgUpdated) {
+ Slog.d(TAG, "updatedPkg = " + disabledPkgSetting);
+ }
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "SSP? " + scanSystemPartition
+ + ", exists? " + pkgAlreadyExists + (pkgAlreadyExists?" "+pkgSetting.toString():"")
+ + ", upgraded? " + isSystemPkgUpdated + (isSystemPkgUpdated?" "+disabledPkgSetting.toString():""));
+}
+
+ final SharedUserSetting sharedUserSetting = (pkg.mSharedUserId != null)
+ ? mSettings.getSharedUserLPw(pkg.mSharedUserId,
+ 0 /*pkgFlags*/, 0 /*pkgPrivateFlags*/, true)
+ : null;
+ if (DEBUG_PACKAGE_SCANNING
+ && (parseFlags & PackageParser.PARSE_CHATTY) != 0
+ && sharedUserSetting != null) {
+ Log.d(TAG, "Shared UserID " + pkg.mSharedUserId
+ + " (uid=" + sharedUserSetting.userId + "):"
+ + " packages=" + sharedUserSetting.packages);
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "Shared UserID " + pkg.mSharedUserId
+ + " (uid=" + sharedUserSetting.userId + "):"
+ + " packages=" + sharedUserSetting.packages);
+}
+ }
+
+ if (scanSystemPartition) {
+ // Potentially prune child packages. If the application on the /system
+ // partition has been updated via OTA, but, is still disabled by a
+ // version on /data, cycle through all of its children packages and
+ // remove children that are no longer defined.
+ if (isSystemPkgUpdated) {
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "Disable child packages");
+}
final int scannedChildCount = (pkg.childPackages != null)
? pkg.childPackages.size() : 0;
- final int disabledChildCount = disabledPs.childPackageNames != null
- ? disabledPs.childPackageNames.size() : 0;
+ final int disabledChildCount = disabledPkgSetting.childPackageNames != null
+ ? disabledPkgSetting.childPackageNames.size() : 0;
for (int i = 0; i < disabledChildCount; i++) {
- String disabledChildPackageName = disabledPs.childPackageNames.get(i);
+ String disabledChildPackageName =
+ disabledPkgSetting.childPackageNames.get(i);
boolean disabledPackageAvailable = false;
for (int j = 0; j < scannedChildCount; j++) {
PackageParser.Package childPkg = pkg.childPackages.get(j);
if (childPkg.packageName.equals(disabledChildPackageName)) {
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "Ignore " + disabledChildPackageName);
+}
disabledPackageAvailable = true;
break;
}
- }
- if (!disabledPackageAvailable) {
- mSettings.removeDisabledSystemPackageLPw(disabledChildPackageName);
- }
- }
- }
- }
- }
-
- final boolean isUpdatedPkg = updatedPs != null;
- final boolean isUpdatedSystemPkg = isUpdatedPkg && (scanFlags & SCAN_AS_SYSTEM) != 0;
- boolean isUpdatedPkgBetter = false;
- // First check if this is a system package that may involve an update
- if (isUpdatedSystemPkg) {
- // If new package is not located in "/system/priv-app" (e.g. due to an OTA),
- // it needs to drop FLAG_PRIVILEGED.
- if (locationIsPrivileged(pkg.codePath)) {
- updatedPs.pkgPrivateFlags |= ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
- } else {
- updatedPs.pkgPrivateFlags &= ~ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
- }
- // If new package is not located in "/oem" (e.g. due to an OTA),
- // it needs to drop FLAG_OEM.
- if (locationIsOem(pkg.codePath)) {
- updatedPs.pkgPrivateFlags |= ApplicationInfo.PRIVATE_FLAG_OEM;
- } 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.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.getLongVersionCode());
- if (!updatedPs.codePathString.equals(pkg.codePath)) {
- Slog.w(PackageManagerService.TAG, "Code path for hidden system pkg "
- + ps.name + " changing from " + updatedPs.codePathString
- + " to " + pkg.codePath);
- final File codePath = new File(pkg.codePath);
- updatedPs.codePath = codePath;
- updatedPs.codePathString = pkg.codePath;
- updatedPs.resourcePath = codePath;
- updatedPs.resourcePathString = pkg.codePath;
- }
- updatedPs.pkg = pkg;
- updatedPs.versionCode = pkg.getLongVersionCode();
-
- // Update the disabled system child packages to point to the package too.
- final int childCount = updatedPs.childPackageNames != null
- ? updatedPs.childPackageNames.size() : 0;
- for (int i = 0; i < childCount; i++) {
- String childPackageName = updatedPs.childPackageNames.get(i);
- PackageSetting updatedChildPkg = mSettings.getDisabledSystemPkgLPr(
- childPackageName);
- if (updatedChildPkg != null) {
- updatedChildPkg.pkg = pkg;
- updatedChildPkg.versionCode = pkg.getLongVersionCode();
+ }
+ if (!disabledPackageAvailable) {
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "Disable " + disabledChildPackageName);
+}
+ mSettings.removeDisabledSystemPackageLPw(disabledChildPackageName);
}
}
- } else {
- // The current app on the system partition is better than
- // what we have updated to on the data partition; switch
- // back to the system partition version.
- // At this point, its safely assumed that package installation for
- // apps in system partition will go through. If not there won't be a working
- // version of the app
- // writer
- synchronized (mPackages) {
- // Just remove the loaded entries from package lists.
- mPackages.remove(ps.name);
- }
-
- logCriticalInfo(Log.WARN, "Package " + ps.name + " at " + pkg.codePath
- + " reverting from " + ps.codePathString
- + ": new version " + pkg.getLongVersionCode()
- + " better than installed " + ps.versionCode);
-
- InstallArgs args = createInstallArgsForExisting(packageFlagsToInstallFlags(ps),
- ps.codePathString, ps.resourcePathString, getAppDexInstructionSets(ps));
- synchronized (mInstallLock) {
- args.cleanUpResourcesLI();
- }
- synchronized (mPackages) {
- mSettings.enableSystemPackageLPw(ps.name);
- }
- isUpdatedPkgBetter = true;
+ // we're updating the disabled package, so, scan it as the package setting
+ final ScanRequest request = new ScanRequest(pkg, sharedUserSetting,
+ disabledPkgSetting /* pkgSetting */, null /* disabledPkgSetting */,
+ null /* originalPkgSetting */, null, parseFlags, scanFlags,
+ (pkg == mPlatformPackage), user);
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "Scan disabled system package");
+Slog.e("TODD",
+ "Pre: " + request.pkgSetting.dumpState_temp());
+}
+final ScanResult result =
+ scanPackageOnlyLI(request, mFactoryTest, -1L);
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "Post: " + (result.success?result.pkgSetting.dumpState_temp():"FAILED scan"));
+}
}
}
}
- String resourcePath = null;
- String baseResourcePath = null;
- if ((parseFlags & PackageParser.PARSE_FORWARD_LOCK) != 0 && !isUpdatedPkgBetter) {
- if (ps != null && ps.resourcePathString != null) {
- resourcePath = ps.resourcePathString;
- baseResourcePath = ps.resourcePathString;
- } else {
- // Should not happen at all. Just log an error.
- Slog.e(TAG, "Resource path not set for package " + pkg.packageName);
- }
- } else {
- resourcePath = pkg.codePath;
- baseResourcePath = pkg.baseCodePath;
- }
-
- // Set application objects path explicitly.
- pkg.setApplicationVolumeUuid(pkg.volumeUuid);
- pkg.setApplicationInfoCodePath(pkg.codePath);
- pkg.setApplicationInfoBaseCodePath(pkg.baseCodePath);
- pkg.setApplicationInfoSplitCodePaths(pkg.splitCodePaths);
- pkg.setApplicationInfoResourcePath(resourcePath);
- pkg.setApplicationInfoBaseResourcePath(baseResourcePath);
- pkg.setApplicationInfoSplitResourcePaths(pkg.splitCodePaths);
+ final boolean newPkgChangedPaths =
+ pkgAlreadyExists && !pkgSetting.codePathString.equals(pkg.codePath);
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "paths changed? " + newPkgChangedPaths
+ + "; old: " + pkg.codePath
+ + ", new: " + (pkgSetting==null?"<<NULL>>":pkgSetting.codePathString));
+}
+ final boolean newPkgVersionGreater =
+ pkgAlreadyExists && pkg.getLongVersionCode() > pkgSetting.versionCode;
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "version greater? " + newPkgVersionGreater
+ + "; old: " + pkg.getLongVersionCode()
+ + ", new: " + (pkgSetting==null?"<<NULL>>":pkgSetting.versionCode));
+}
+ final boolean isSystemPkgBetter = scanSystemPartition && isSystemPkgUpdated
+ && newPkgChangedPaths && newPkgVersionGreater;
+if (REFACTOR_DEBUG) {
+ Slog.e("TODD",
+ "system better? " + isSystemPkgBetter);
+}
+ if (isSystemPkgBetter) {
+ // The version of the application on /system is greater than the version on
+ // /data. Switch back to the application on /system.
+ // It's safe to assume the application on /system will correctly scan. If not,
+ // there won't be a working copy of the application.
+ synchronized (mPackages) {
+ // just remove the loaded entries from package lists
+ mPackages.remove(pkgSetting.name);
+ }
+
+ logCriticalInfo(Log.WARN,
+ "System package updated;"
+ + " name: " + pkgSetting.name
+ + "; " + pkgSetting.versionCode + " --> " + pkg.getLongVersionCode()
+ + "; " + pkgSetting.codePathString + " --> " + pkg.codePath);
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "System package changed;"
+ + " name: " + pkgSetting.name
+ + "; " + pkgSetting.versionCode + " --> " + pkg.getLongVersionCode()
+ + "; " + pkgSetting.codePathString + " --> " + pkg.codePath);
+}
- // throw an exception if we have an update to a system application, but, it's not more
- // recent than the package we've already scanned
- if (isUpdatedSystemPkg && !isUpdatedPkgBetter) {
- // 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);
- } else {
- pkg.applicationInfo.primaryCpuAbi = updatedPs.primaryCpuAbiString;
- pkg.applicationInfo.secondaryCpuAbi = updatedPs.secondaryCpuAbiString;
+ final InstallArgs args = createInstallArgsForExisting(
+ packageFlagsToInstallFlags(pkgSetting), pkgSetting.codePathString,
+ pkgSetting.resourcePathString, getAppDexInstructionSets(pkgSetting));
+ args.cleanUpResourcesLI();
+ synchronized (mPackages) {
+ mSettings.enableSystemPackageLPw(pkgSetting.name);
}
- pkg.mExtras = updatedPs;
+ }
+ if (scanSystemPartition && isSystemPkgUpdated && !isSystemPkgBetter) {
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "THROW exception; system pkg version not good enough");
+}
+ // The version of the application on the /system partition is less than or
+ // equal to the version on the /data partition. Throw an exception and use
+ // the application already installed on the /data partition.
throw new PackageManagerException(Log.WARN, "Package " + pkg.packageName + " at "
- + pkg.codePath + " ignored: updated version " + updatedPs.versionCode
+ + pkg.codePath + " ignored: updated version " + disabledPkgSetting.versionCode
+ " better than this " + pkg.getLongVersionCode());
}
- if (isUpdatedPkg) {
- // updated system applications don't initially have the SCAN_AS_SYSTEM flag set
- scanFlags |= SCAN_AS_SYSTEM;
-
- // An updated privileged application will not have the PARSE_IS_PRIVILEGED
- // flag set initially
- if ((updatedPs.pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
- scanFlags |= SCAN_AS_PRIVILEGED;
- }
-
- // 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;
- }
+ // Verify certificates against what was last scanned. If it is an updated priv app, we will
+ // force the verification. Full apk verification will happen unless apk verity is set up for
+ // the file. In that case, only small part of the apk is verified upfront.
+ collectCertificatesLI(pkgSetting, pkg, parseFlags,
+ PackageManagerServiceUtils.isApkVerificationForced(disabledPkgSetting));
- // 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
- collectCertificatesLI(ps, pkg, parseFlags);
-
- /*
- * A new system app appeared, but we already had a non-system one of the
- * same name installed earlier.
- */
boolean shouldHideSystemApp = false;
- if (!isUpdatedPkg && ps != null
- && (parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) != 0 && !isSystemApp(ps)) {
- /*
- * Check to make sure the signatures match first. If they don't,
- * wipe the installed application and its data.
- */
- if (compareSignatures(ps.signatures.mSignatures, pkg.mSignatures)
- != PackageManager.SIGNATURE_MATCH) {
- logCriticalInfo(Log.WARN, "Package " + ps.name + " appeared on system, but"
- + " signatures don't match existing userdata copy; removing");
+ // A new application appeared on /system, but, we already have a copy of
+ // the application installed on /data.
+ if (scanSystemPartition && !isSystemPkgUpdated && pkgAlreadyExists
+ && !pkgSetting.isSystem()) {
+ // if the signatures don't match, wipe the installed application and its data
+ if (compareSignatures(pkgSetting.signatures.mSigningDetails.signatures,
+ pkg.mSigningDetails.signatures)
+ != PackageManager.SIGNATURE_MATCH) {
+ logCriticalInfo(Log.WARN,
+ "System package signature mismatch;"
+ + " name: " + pkgSetting.name);
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "System package signature mismatch;"
+ + " name: " + pkgSetting.name);
+}
try (PackageFreezer freezer = freezePackage(pkg.packageName,
"scanPackageInternalLI")) {
deletePackageLIF(pkg.packageName, null, true, null, 0, null, false, null);
}
- ps = null;
- } else {
- /*
- * If the newly-added system app is an older version than the
- * already installed version, hide it. It will be scanned later
- * and re-added like an update.
- */
- if (pkg.getLongVersionCode() <= ps.versionCode) {
- shouldHideSystemApp = true;
- logCriticalInfo(Log.INFO, "Package " + ps.name + " appeared at " + pkg.codePath
- + " but new version " + pkg.getLongVersionCode()
- + " better than installed " + ps.versionCode + "; hiding system");
- } else {
- /*
- * The newly found system app is a newer version that the
- * one previously installed. Simply remove the
- * already-installed application and replace it with our own
- * while keeping the application data.
- */
- logCriticalInfo(Log.WARN, "Package " + ps.name + " at " + pkg.codePath
- + " reverting from " + ps.codePathString + ": new version "
- + pkg.getLongVersionCode() + " better than installed "
- + ps.versionCode);
- InstallArgs args = createInstallArgsForExisting(packageFlagsToInstallFlags(ps),
- ps.codePathString, ps.resourcePathString, getAppDexInstructionSets(ps));
- synchronized (mInstallLock) {
- args.cleanUpResourcesLI();
- }
+ pkgSetting = null;
+ } else if (newPkgVersionGreater) {
+ // The application on /system is newer than the application on /data.
+ // Simply remove the application on /data [keeping application data]
+ // and replace it with the version on /system.
+ logCriticalInfo(Log.WARN,
+ "System package enabled;"
+ + " name: " + pkgSetting.name
+ + "; " + pkgSetting.versionCode + " --> " + pkg.getLongVersionCode()
+ + "; " + pkgSetting.codePathString + " --> " + pkg.codePath);
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "System package enabled;"
+ + " name: " + pkgSetting.name
+ + "; " + pkgSetting.versionCode + " --> " + pkg.getLongVersionCode()
+ + "; " + pkgSetting.codePathString + " --> " + pkg.codePath);
+}
+ InstallArgs args = createInstallArgsForExisting(
+ packageFlagsToInstallFlags(pkgSetting), pkgSetting.codePathString,
+ pkgSetting.resourcePathString, getAppDexInstructionSets(pkgSetting));
+ synchronized (mInstallLock) {
+ args.cleanUpResourcesLI();
}
+ } else {
+ // The application on /system is older than the application on /data. Hide
+ // the application on /system and the version on /data will be scanned later
+ // and re-added like an update.
+ shouldHideSystemApp = true;
+ logCriticalInfo(Log.INFO,
+ "System package disabled;"
+ + " name: " + pkgSetting.name
+ + "; old: " + pkgSetting.codePathString + " @ " + pkgSetting.versionCode
+ + "; new: " + pkg.codePath + " @ " + pkg.codePath);
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "System package disabled;"
+ + " name: " + pkgSetting.name
+ + "; old: " + pkgSetting.codePathString + " @ " + pkgSetting.versionCode
+ + "; new: " + pkg.codePath + " @ " + pkg.codePath);
+}
}
}
- // The apk is forward locked (not public) if its code and resources
- // are kept in different files. (except for app in either system or
- // vendor path).
- // TODO grab this value from PackageSettings
- if ((parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) == 0) {
- if (ps != null && !ps.codePath.equals(ps.resourcePath)) {
- parseFlags |= PackageParser.PARSE_FORWARD_LOCK;
- }
- }
-
- final int userId = ((user == null) ? 0 : user.getIdentifier());
- if (ps != null && ps.getInstantApp(userId)) {
- scanFlags |= SCAN_AS_INSTANT_APP;
- }
- if (ps != null && ps.getVirtulalPreload(userId)) {
- scanFlags |= SCAN_AS_VIRTUAL_PRELOAD;
- }
-
- // Note that we invoke the following method only if we are about to unpack an application
- PackageParser.Package scannedPkg = scanPackageNewLI(pkg, parseFlags, scanFlags
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "Scan package");
+Slog.e("TODD",
+ "Pre: " + (pkgSetting==null?"<<NONE>>":pkgSetting.dumpState_temp()));
+}
+ final PackageParser.Package scannedPkg = scanPackageNewLI(pkg, parseFlags, scanFlags
| SCAN_UPDATE_SIGNATURE, currentTime, user);
+if (REFACTOR_DEBUG) {
+pkgSetting = mSettings.getPackageLPr(pkg.packageName);
+Slog.e("TODD",
+ "Post: " + (pkgSetting==null?"<<NONE>>":pkgSetting.dumpState_temp()));
+}
- /*
- * If the system app should be overridden by a previously installed
- * data, hide the system app now and let the /data/app scan pick it up
- * again.
- */
if (shouldHideSystemApp) {
+if (REFACTOR_DEBUG) {
+Slog.e("TODD",
+ "Disable package: " + pkg.packageName);
+}
synchronized (mPackages) {
mSettings.disableSystemPackageLPw(pkg.packageName, true);
}
}
-
return scannedPkg;
}
@@ -9518,9 +9617,10 @@ public class PackageManagerService extends IPackageManager.Stub
final String[] expectedCertDigests = requiredCertDigests[i];
// For apps targeting O MR1 we require explicit enumeration of all certs.
final String[] libCertDigests = (targetSdk > Build.VERSION_CODES.O)
- ? PackageUtils.computeSignaturesSha256Digests(libPkg.mSignatures)
+ ? PackageUtils.computeSignaturesSha256Digests(
+ libPkg.mSigningDetails.signatures)
: PackageUtils.computeSignaturesSha256Digests(
- new Signature[]{libPkg.mSignatures[0]});
+ new Signature[]{libPkg.mSigningDetails.signatures[0]});
// Take a shortcut if sizes don't match. Note that if an app doesn't
// target O we don't parse the "additional-certificate" tags similarly
@@ -9717,9 +9817,77 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
+ /**
+ * Returns the actual scan flags depending upon the state of the other settings.
+ * <p>Updated system applications will not have the following flags set
+ * by default and need to be adjusted after the fact:
+ * <ul>
+ * <li>{@link #SCAN_AS_SYSTEM}</li>
+ * <li>{@link #SCAN_AS_PRIVILEGED}</li>
+ * <li>{@link #SCAN_AS_OEM}</li>
+ * <li>{@link #SCAN_AS_VENDOR}</li>
+ * <li>{@link #SCAN_AS_INSTANT_APP}</li>
+ * <li>{@link #SCAN_AS_VIRTUAL_PRELOAD}</li>
+ * </ul>
+ */
+ private @ScanFlags int adjustScanFlags(@ScanFlags int scanFlags,
+ PackageSetting pkgSetting, PackageSetting disabledPkgSetting, UserHandle user,
+ PackageParser.Package pkg) {
+ if (disabledPkgSetting != null) {
+ // updated system application, must at least have SCAN_AS_SYSTEM
+ scanFlags |= SCAN_AS_SYSTEM;
+ if ((disabledPkgSetting.pkgPrivateFlags
+ & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0) {
+ scanFlags |= SCAN_AS_PRIVILEGED;
+ }
+ if ((disabledPkgSetting.pkgPrivateFlags
+ & ApplicationInfo.PRIVATE_FLAG_OEM) != 0) {
+ scanFlags |= SCAN_AS_OEM;
+ }
+ if ((disabledPkgSetting.pkgPrivateFlags
+ & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0) {
+ scanFlags |= SCAN_AS_VENDOR;
+ }
+ }
+ if (pkgSetting != null) {
+ final int userId = ((user == null) ? 0 : user.getIdentifier());
+ if (pkgSetting.getInstantApp(userId)) {
+ scanFlags |= SCAN_AS_INSTANT_APP;
+ }
+ if (pkgSetting.getVirtulalPreload(userId)) {
+ scanFlags |= SCAN_AS_VIRTUAL_PRELOAD;
+ }
+ }
+
+ // Scan as privileged apps that share a user with a priv-app.
+ if (((scanFlags & SCAN_AS_PRIVILEGED) == 0) && !pkg.isPrivileged()
+ && (pkg.mSharedUserId != null)) {
+ SharedUserSetting sharedUserSetting = null;
+ try {
+ sharedUserSetting = mSettings.getSharedUserLPw(pkg.mSharedUserId, 0, 0, false);
+ } catch (PackageManagerException ignore) {}
+ if (sharedUserSetting != null && sharedUserSetting.isPrivileged()) {
+ // Exempt SharedUsers signed with the platform key.
+ // TODO(b/72378145) Fix this exemption. Force signature apps
+ // to whitelist their privileged permissions just like other
+ // priv-apps.
+ synchronized (mPackages) {
+ PackageSetting platformPkgSetting = mSettings.mPackages.get("android");
+ if (!pkg.packageName.equals("android")
+ && (compareSignatures(platformPkgSetting.signatures.mSigningDetails.signatures,
+ pkg.mSigningDetails.signatures) != PackageManager.SIGNATURE_MATCH)) {
+ scanFlags |= SCAN_AS_PRIVILEGED;
+ }
+ }
+ }
+ }
+
+ return scanFlags;
+ }
+
@GuardedBy("mInstallLock")
private PackageParser.Package scanPackageNewLI(@NonNull PackageParser.Package pkg,
- final @ParseFlags int parseFlags, final @ScanFlags int scanFlags, long currentTime,
+ final @ParseFlags int parseFlags, @ScanFlags int scanFlags, long currentTime,
@Nullable UserHandle user) throws PackageManagerException {
final String renamedPkgName = mSettings.getRenamedPackageLPr(pkg.mRealPackage);
@@ -9737,6 +9905,7 @@ public class PackageManagerService extends IPackageManager.Stub
+ " was transferred to another, but its .apk remains");
}
+ scanFlags = adjustScanFlags(scanFlags, pkgSetting, disabledPkgSetting, user, pkg);
synchronized (mPackages) {
applyPolicy(pkg, parseFlags, scanFlags);
assertPackageIsValid(pkg, parseFlags, scanFlags);
@@ -9791,6 +9960,7 @@ public class PackageManagerService extends IPackageManager.Stub
final @ScanFlags int scanFlags = request.scanFlags;
final PackageSetting oldPkgSetting = request.oldPkgSetting;
final PackageSetting originalPkgSetting = request.originalPkgSetting;
+ final PackageSetting disabledPkgSetting = request.disabledPkgSetting;
final UserHandle user = request.user;
final String realPkgName = request.realPkgName;
final PackageSetting pkgSetting = result.pkgSetting;
@@ -9856,14 +10026,14 @@ public class PackageManagerService extends IPackageManager.Stub
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;
+ pkgSetting.signatures.mSigningDetails = pkg.mSigningDetails;
} 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;
+ pkgSetting.signatures.mSigningDetails = pkg.mSigningDetails;
String msg = "System package " + pkg.packageName
+ " signature changed; retaining data.";
reportSettingsProblem(Log.WARN, msg);
@@ -9873,8 +10043,8 @@ public class PackageManagerService extends IPackageManager.Stub
try {
final boolean compareCompat = isCompatSignatureUpdateNeeded(pkg);
final boolean compareRecover = isRecoverSignatureUpdateNeeded(pkg);
- final boolean compatMatch = verifySignatures(signatureCheckPs, pkg.mSignatures,
- compareCompat, compareRecover);
+ final boolean compatMatch = verifySignatures(signatureCheckPs, disabledPkgSetting,
+ pkg.mSigningDetails, compareCompat, compareRecover);
// The new KeySets will be re-added later in the scanning process.
if (compatMatch) {
synchronized (mPackages) {
@@ -9883,22 +10053,23 @@ public class PackageManagerService extends IPackageManager.Stub
}
// We just determined the app is signed correctly, so bring
// over the latest parsed certs.
- pkgSetting.signatures.mSignatures = pkg.mSignatures;
+ pkgSetting.signatures.mSigningDetails = pkg.mSigningDetails;
} 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;
+ pkgSetting.signatures.mSigningDetails = pkg.mSigningDetails;
// 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) {
+ if (compareSignatures(
+ signatureCheckPs.sharedUser.signatures.mSigningDetails.signatures,
+ pkg.mSigningDetails.signatures) != PackageManager.SIGNATURE_MATCH) {
throw new PackageManagerException(
INSTALL_PARSE_FAILED_INCONSISTENT_CERTIFICATES,
"Signature mismatch for shared user: "
@@ -9963,10 +10134,16 @@ public class PackageManagerService extends IPackageManager.Stub
*/
private static @Nullable String getRealPackageName(@NonNull PackageParser.Package pkg,
@Nullable String renamedPkgName) {
- if (pkg.mOriginalPackages == null || !pkg.mOriginalPackages.contains(renamedPkgName)) {
- return null;
+ if (isPackageRenamed(pkg, renamedPkgName)) {
+ return pkg.mRealPackage;
}
- return pkg.mRealPackage;
+ return null;
+ }
+
+ /** Returns {@code true} if the package has been renamed. Otherwise, {@code false}. */
+ private static boolean isPackageRenamed(@NonNull PackageParser.Package pkg,
+ @Nullable String renamedPkgName) {
+ return pkg.mOriginalPackages != null && pkg.mOriginalPackages.contains(renamedPkgName);
}
/**
@@ -9979,7 +10156,7 @@ public class PackageManagerService extends IPackageManager.Stub
@GuardedBy("mPackages")
private @Nullable PackageSetting getOriginalPackageLocked(@NonNull PackageParser.Package pkg,
@Nullable String renamedPkgName) {
- if (pkg.mOriginalPackages == null || pkg.mOriginalPackages.contains(renamedPkgName)) {
+ if (!isPackageRenamed(pkg, renamedPkgName)) {
return null;
}
for (int i = pkg.mOriginalPackages.size() - 1; i >= 0; --i) {
@@ -10102,7 +10279,6 @@ public class PackageManagerService extends IPackageManager.Stub
usesStaticLibraries = new String[pkg.usesStaticLibraries.size()];
pkg.usesStaticLibraries.toArray(usesStaticLibraries);
}
-
final boolean createNewPackage = (pkgSetting == null);
if (createNewPackage) {
final String parentPackageName = (pkg.parentPackage != null)
@@ -10126,17 +10302,17 @@ public class PackageManagerService extends IPackageManager.Stub
// 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,
+ destCodeFile, destResourceFile, 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.
+ // This is the initial transition from the original package, so,
+ // fix up the new package's name now. We must do this after looking
+ // up the package under its new name, so getPackageLP takes care of
+ // fiddling things correctly.
pkg.setPackageName(originalPkgSetting.name);
// File a report about this.
@@ -10223,7 +10399,7 @@ public class PackageManagerService extends IPackageManager.Stub
// would've already compiled the app without taking the package setting into
// account.
if ((scanFlags & SCAN_NO_DEX) == 0 && (scanFlags & SCAN_NEW_INSTALL) != 0) {
- if (cpuAbiOverride == null && pkgSetting.cpuAbiOverrideString != null) {
+ if (cpuAbiOverride == null && pkg.packageName != null) {
Slog.w(TAG, "Ignoring persisted ABI override " + cpuAbiOverride +
" for package " + pkg.packageName);
}
@@ -10238,7 +10414,7 @@ public class PackageManagerService extends IPackageManager.Stub
pkg.cpuAbiOverride = cpuAbiOverride;
if (DEBUG_ABI_SELECTION) {
- Slog.d(TAG, "Resolved nativeLibraryRoot for " + pkg.applicationInfo.packageName
+ Slog.d(TAG, "Resolved nativeLibraryRoot for " + pkg.packageName
+ " to root=" + pkg.applicationInfo.nativeLibraryRootDir + ", isa="
+ pkg.applicationInfo.nativeLibraryRootRequiresIsa);
}
@@ -10293,6 +10469,22 @@ public class PackageManagerService extends IPackageManager.Stub
}
pkgSetting.setTimeStamp(scanFileTime);
+ pkgSetting.pkg = pkg;
+ pkgSetting.pkgFlags = pkg.applicationInfo.flags;
+ if (pkg.getLongVersionCode() != pkgSetting.versionCode) {
+ pkgSetting.versionCode = pkg.getLongVersionCode();
+ }
+ // Update volume if needed
+ final String volumeUuid = pkg.applicationInfo.volumeUuid;
+ if (!Objects.equals(volumeUuid, pkgSetting.volumeUuid)) {
+ Slog.i(PackageManagerService.TAG,
+ "Update" + (pkgSetting.isSystem() ? " system" : "")
+ + " package " + pkg.packageName
+ + " volume from " + pkgSetting.volumeUuid
+ + " to " + volumeUuid);
+ pkgSetting.volumeUuid = volumeUuid;
+ }
+
return new ScanResult(true, pkgSetting, changedAbiCodePath);
}
@@ -10421,7 +10613,6 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
}
- pkg.mTrustedOverlay = (scanFlags & SCAN_TRUSTED_OVERLAY) != 0;
if ((scanFlags & SCAN_AS_PRIVILEGED) != 0) {
pkg.applicationInfo.privateFlags |= ApplicationInfo.PRIVATE_FLAG_PRIVILEGED;
@@ -10443,6 +10634,14 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
+ private static @NonNull <T> T assertNotNull(@Nullable T object, String message)
+ throws PackageManagerException {
+ if (object == null) {
+ throw new PackageManagerException(INSTALL_FAILED_INTERNAL_ERROR, message);
+ }
+ return object;
+ }
+
/**
* Asserts the parsed package is valid according to the given policy. If the
* package is invalid, for whatever reason, throws {@link PackageManagerException}.
@@ -10693,6 +10892,73 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
}
+
+ // Verify that packages sharing a user with a privileged app are marked as privileged.
+ if (!pkg.isPrivileged() && (pkg.mSharedUserId != null)) {
+ SharedUserSetting sharedUserSetting = null;
+ try {
+ sharedUserSetting = mSettings.getSharedUserLPw(pkg.mSharedUserId, 0, 0, false);
+ } catch (PackageManagerException ignore) {}
+ if (sharedUserSetting != null && sharedUserSetting.isPrivileged()) {
+ // Exempt SharedUsers signed with the platform key.
+ PackageSetting platformPkgSetting = mSettings.mPackages.get("android");
+ if ((platformPkgSetting.signatures.mSigningDetails
+ != PackageParser.SigningDetails.UNKNOWN)
+ && (compareSignatures(
+ platformPkgSetting.signatures.mSigningDetails.signatures,
+ pkg.mSigningDetails.signatures)
+ != PackageManager.SIGNATURE_MATCH)) {
+ throw new PackageManagerException("Apps that share a user with a " +
+ "privileged app must themselves be marked as privileged. " +
+ pkg.packageName + " shares privileged user " +
+ pkg.mSharedUserId + ".");
+ }
+ }
+ }
+
+ // Apply policies specific for runtime resource overlays (RROs).
+ if (pkg.mOverlayTarget != null) {
+ // System overlays have some restrictions on their use of the 'static' state.
+ if ((scanFlags & SCAN_AS_SYSTEM) != 0) {
+ // We are scanning a system overlay. This can be the first scan of the
+ // system/vendor/oem partition, or an update to the system overlay.
+ if ((parseFlags & PackageParser.PARSE_IS_SYSTEM_DIR) == 0) {
+ // This must be an update to a system overlay.
+ final PackageSetting previousPkg = assertNotNull(
+ mSettings.getPackageLPr(pkg.packageName),
+ "previous package state not present");
+
+ // Static overlays cannot be updated.
+ if (previousPkg.pkg.mOverlayIsStatic) {
+ throw new PackageManagerException("Overlay " + pkg.packageName +
+ " is static and cannot be upgraded.");
+ // Non-static overlays cannot be converted to static overlays.
+ } else if (pkg.mOverlayIsStatic) {
+ throw new PackageManagerException("Overlay " + pkg.packageName +
+ " cannot be upgraded into a static overlay.");
+ }
+ }
+ } else {
+ // The overlay is a non-system overlay. Non-system overlays cannot be static.
+ if (pkg.mOverlayIsStatic) {
+ throw new PackageManagerException("Overlay " + pkg.packageName +
+ " is static but not pre-installed.");
+ }
+
+ // The only case where we allow installation of a non-system overlay is when
+ // its signature is signed with the platform certificate.
+ PackageSetting platformPkgSetting = mSettings.getPackageLPr("android");
+ if ((platformPkgSetting.signatures.mSigningDetails
+ != PackageParser.SigningDetails.UNKNOWN)
+ && (compareSignatures(
+ platformPkgSetting.signatures.mSigningDetails.signatures,
+ pkg.mSigningDetails.signatures)
+ != PackageManager.SIGNATURE_MATCH)) {
+ throw new PackageManagerException("Overlay " + pkg.packageName +
+ " must be signed with the platform certificate.");
+ }
+ }
+ }
}
}
@@ -13141,80 +13407,6 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
- @Override
- public void installPackageAsUser(String originPath, IPackageInstallObserver2 observer,
- int installFlags, String installerPackageName, int userId) {
- mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INSTALL_PACKAGES, null);
-
- final int callingUid = Binder.getCallingUid();
- mPermissionManager.enforceCrossUserPermission(callingUid, userId,
- true /* requireFullPermission */, true /* checkShell */, "installPackageAsUser");
-
- if (isUserRestricted(userId, UserManager.DISALLOW_INSTALL_APPS)) {
- try {
- if (observer != null) {
- observer.onPackageInstalled("", INSTALL_FAILED_USER_RESTRICTED, null, null);
- }
- } catch (RemoteException re) {
- }
- return;
- }
-
- if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) {
- installFlags |= PackageManager.INSTALL_FROM_ADB;
-
- } else {
- // Caller holds INSTALL_PACKAGES permission, so we're less strict
- // about installerPackageName.
-
- installFlags &= ~PackageManager.INSTALL_FROM_ADB;
- installFlags &= ~PackageManager.INSTALL_ALL_USERS;
- }
-
- UserHandle user;
- if ((installFlags & PackageManager.INSTALL_ALL_USERS) != 0) {
- user = UserHandle.ALL;
- } else {
- user = new UserHandle(userId);
- }
-
- // Only system components can circumvent runtime permissions when installing.
- if ((installFlags & PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS) != 0
- && mContext.checkCallingOrSelfPermission(Manifest.permission
- .INSTALL_GRANT_RUNTIME_PERMISSIONS) == PackageManager.PERMISSION_DENIED) {
- throw new SecurityException("You need the "
- + "android.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS permission "
- + "to use the PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS flag");
- }
-
- if ((installFlags & PackageManager.INSTALL_FORWARD_LOCK) != 0
- || (installFlags & PackageManager.INSTALL_EXTERNAL) != 0) {
- throw new IllegalArgumentException(
- "New installs into ASEC containers no longer supported");
- }
-
- final File originFile = new File(originPath);
- final OriginInfo origin = OriginInfo.fromUntrustedFile(originFile);
-
- final Message msg = mHandler.obtainMessage(INIT_COPY);
- final VerificationInfo verificationInfo = new VerificationInfo(
- null /*originatingUri*/, null /*referrer*/, -1 /*originatingUid*/, callingUid);
- final InstallParams params = new InstallParams(origin, null /*moveInfo*/, observer,
- installFlags, installerPackageName, null /*volumeUuid*/, verificationInfo, user,
- null /*packageAbiOverride*/, null /*grantedPermissions*/,
- null /*certificates*/, PackageManager.INSTALL_REASON_UNKNOWN);
- params.setTraceMethod("installAsUser").setTraceCookie(System.identityHashCode(params));
- msg.obj = params;
-
- Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, "installAsUser",
- System.identityHashCode(msg.obj));
- Trace.asyncTraceBegin(TRACE_TAG_PACKAGE_MANAGER, "queueInstall",
- System.identityHashCode(msg.obj));
-
- mHandler.sendMessage(msg);
- }
-
-
/**
* Ensure that the install reason matches what we know about the package installer (e.g. whether
* it is acting on behalf on an enterprise or the user).
@@ -13278,7 +13470,7 @@ public class PackageManagerService extends IPackageManager.Stub
void installStage(String packageName, File stagedDir,
IPackageInstallObserver2 observer, PackageInstaller.SessionParams sessionParams,
String installerPackageName, int installerUid, UserHandle user,
- Certificate[][] certificates) {
+ PackageParser.SigningDetails signingDetails) {
if (DEBUG_EPHEMERAL) {
if ((sessionParams.installFlags & PackageManager.INSTALL_INSTANT_APP) != 0) {
Slog.d(TAG, "Ephemeral install of " + packageName);
@@ -13296,7 +13488,7 @@ public class PackageManagerService extends IPackageManager.Stub
final InstallParams params = new InstallParams(origin, null, observer,
sessionParams.installFlags, installerPackageName, sessionParams.volumeUuid,
verificationInfo, user, sessionParams.abiOverride,
- sessionParams.grantedRuntimePermissions, certificates, installReason);
+ sessionParams.grantedRuntimePermissions, signingDetails, installReason);
params.setTraceMethod("installStage").setTraceCookie(System.identityHashCode(params));
msg.obj = params;
@@ -13900,7 +14092,7 @@ public class PackageManagerService extends IPackageManager.Stub
final PackageParser.Package pkg = mPackages.get(verifierInfo.packageName);
if (pkg == null) {
return -1;
- } else if (pkg.mSignatures.length != 1) {
+ } else if (pkg.mSigningDetails.signatures.length != 1) {
Slog.i(TAG, "Verifier package " + verifierInfo.packageName
+ " has more than one signature; ignoring");
return -1;
@@ -13914,7 +14106,7 @@ public class PackageManagerService extends IPackageManager.Stub
final byte[] expectedPublicKey;
try {
- final Signature verifierSig = pkg.mSignatures[0];
+ final Signature verifierSig = pkg.mSigningDetails.signatures[0];
final PublicKey publicKey = verifierSig.getPublicKey();
expectedPublicKey = publicKey.getEncoded();
} catch (CertificateException e) {
@@ -14201,9 +14393,10 @@ public class PackageManagerService extends IPackageManager.Stub
Object obj = mSettings.getUserIdLPr(callingUid);
if (obj != null) {
if (obj instanceof SharedUserSetting) {
- callerSignature = ((SharedUserSetting)obj).signatures.mSignatures;
+ callerSignature =
+ ((SharedUserSetting)obj).signatures.mSigningDetails.signatures;
} else if (obj instanceof PackageSetting) {
- callerSignature = ((PackageSetting)obj).signatures.mSignatures;
+ callerSignature = ((PackageSetting)obj).signatures.mSigningDetails.signatures;
} else {
throw new SecurityException("Bad object " + obj + " for uid " + callingUid);
}
@@ -14215,7 +14408,7 @@ public class PackageManagerService extends IPackageManager.Stub
// not signed with the same cert as the caller.
if (installerPackageSetting != null) {
if (compareSignatures(callerSignature,
- installerPackageSetting.signatures.mSignatures)
+ installerPackageSetting.signatures.mSigningDetails.signatures)
!= PackageManager.SIGNATURE_MATCH) {
throw new SecurityException(
"Caller does not have same cert as new installer package "
@@ -14232,7 +14425,7 @@ public class PackageManagerService extends IPackageManager.Stub
// okay to change it.
if (setting != null) {
if (compareSignatures(callerSignature,
- setting.signatures.mSignatures)
+ setting.signatures.mSigningDetails.signatures)
!= PackageManager.SIGNATURE_MATCH) {
throw new SecurityException(
"Caller does not have same cert as old installer package "
@@ -14606,13 +14799,13 @@ public class PackageManagerService extends IPackageManager.Stub
final String packageAbiOverride;
final String[] grantedRuntimePermissions;
final VerificationInfo verificationInfo;
- final Certificate[][] certificates;
+ final PackageParser.SigningDetails signingDetails;
final int installReason;
InstallParams(OriginInfo origin, MoveInfo move, IPackageInstallObserver2 observer,
int installFlags, String installerPackageName, String volumeUuid,
VerificationInfo verificationInfo, UserHandle user, String packageAbiOverride,
- String[] grantedPermissions, Certificate[][] certificates, int installReason) {
+ String[] grantedPermissions, PackageParser.SigningDetails signingDetails, int installReason) {
super(user);
this.origin = origin;
this.move = move;
@@ -14623,7 +14816,7 @@ public class PackageManagerService extends IPackageManager.Stub
this.verificationInfo = verificationInfo;
this.packageAbiOverride = packageAbiOverride;
this.grantedRuntimePermissions = grantedPermissions;
- this.certificates = certificates;
+ this.signingDetails = signingDetails;
this.installReason = installReason;
}
@@ -15054,7 +15247,7 @@ public class PackageManagerService extends IPackageManager.Stub
/** If non-null, drop an async trace when the install completes */
final String traceMethod;
final int traceCookie;
- final Certificate[][] certificates;
+ final PackageParser.SigningDetails signingDetails;
final int installReason;
// The list of instruction sets supported by this app. This is currently
@@ -15066,7 +15259,7 @@ public class PackageManagerService extends IPackageManager.Stub
int installFlags, String installerPackageName, String volumeUuid,
UserHandle user, String[] instructionSets,
String abiOverride, String[] installGrantPermissions,
- String traceMethod, int traceCookie, Certificate[][] certificates,
+ String traceMethod, int traceCookie, PackageParser.SigningDetails signingDetails,
int installReason) {
this.origin = origin;
this.move = move;
@@ -15080,7 +15273,7 @@ public class PackageManagerService extends IPackageManager.Stub
this.installGrantPermissions = installGrantPermissions;
this.traceMethod = traceMethod;
this.traceCookie = traceCookie;
- this.certificates = certificates;
+ this.signingDetails = signingDetails;
this.installReason = installReason;
}
@@ -15176,7 +15369,7 @@ public class PackageManagerService extends IPackageManager.Stub
params.installerPackageName, params.volumeUuid,
params.getUser(), null /*instructionSets*/, params.packageAbiOverride,
params.grantedRuntimePermissions,
- params.traceMethod, params.traceCookie, params.certificates,
+ params.traceMethod, params.traceCookie, params.signingDetails,
params.installReason);
if (isFwdLocked()) {
throw new IllegalArgumentException("Forward locking only supported in ASEC");
@@ -15186,7 +15379,7 @@ public class PackageManagerService extends IPackageManager.Stub
/** Existing install */
FileInstallArgs(String codePath, String resourcePath, String[] instructionSets) {
super(OriginInfo.fromNothing(), null, null, 0, null, null, null, instructionSets,
- null, null, null, 0, null /*certificates*/,
+ null, null, null, 0, PackageParser.SigningDetails.UNKNOWN,
PackageManager.INSTALL_REASON_UNKNOWN);
this.codeFile = (codePath != null) ? new File(codePath) : null;
this.resourceFile = (resourcePath != null) ? new File(resourcePath) : null;
@@ -15407,7 +15600,7 @@ public class PackageManagerService extends IPackageManager.Stub
params.installerPackageName, params.volumeUuid,
params.getUser(), null /* instruction sets */, params.packageAbiOverride,
params.grantedRuntimePermissions,
- params.traceMethod, params.traceCookie, params.certificates,
+ params.traceMethod, params.traceCookie, params.signingDetails,
params.installReason);
}
@@ -15746,7 +15939,8 @@ public class PackageManagerService extends IPackageManager.Stub
}
} else {
// default to original signature matching
- if (compareSignatures(oldPackage.mSignatures, pkg.mSignatures)
+ if (compareSignatures(oldPackage.mSigningDetails.signatures,
+ pkg.mSigningDetails.signatures)
!= PackageManager.SIGNATURE_MATCH) {
res.setError(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
"New package has a different signature: " + pkgName);
@@ -16458,7 +16652,6 @@ public class PackageManagerService extends IPackageManager.Stub
| PackageParser.PARSE_ENFORCE_CODE
| (forwardLocked ? PackageParser.PARSE_FORWARD_LOCK : 0)
| (onExternal ? PackageParser.PARSE_EXTERNAL_STORAGE : 0)
- | (instantApp ? PackageParser.PARSE_IS_EPHEMERAL : 0)
| (forceSdk ? PackageParser.PARSE_FORCE_SDK : 0);
PackageParser pp = new PackageParser();
pp.setSeparateProcesses(mSeparateProcesses);
@@ -16469,6 +16662,7 @@ public class PackageManagerService extends IPackageManager.Stub
final PackageParser.Package pkg;
try {
pkg = pp.parsePackage(tmpPackageFile, parseFlags);
+ DexMetadataHelper.validatePackageDexMetadata(pkg);
} catch (PackageParserException e) {
res.setError("Failed parse during installPackageLI", e);
return;
@@ -16476,29 +16670,29 @@ 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");
- res.setError(INSTALL_FAILED_SANDBOX_VERSION_DOWNGRADE,
- "Instant app package must target O");
- return;
- }
- if (instantApp && pkg.applicationInfo.targetSandboxVersion != 2) {
- Slog.w(TAG, "Instant app package " + pkg.packageName
- + " does not target targetSandboxVersion 2");
- res.setError(INSTALL_FAILED_SANDBOX_VERSION_DOWNGRADE,
- "Instant app package must use targetSanboxVersion 2");
- return;
+ // Instant apps have several additional install-time checks.
+ if (instantApp) {
+ if (pkg.applicationInfo.targetSdkVersion < Build.VERSION_CODES.O) {
+ Slog.w(TAG,
+ "Instant app package " + pkg.packageName + " does not target at least O");
+ res.setError(INSTALL_FAILED_INSTANT_APP_INVALID,
+ "Instant app package must target at least O");
+ return;
+ }
+ if (pkg.applicationInfo.targetSandboxVersion != 2) {
+ Slog.w(TAG, "Instant app package " + pkg.packageName
+ + " does not target targetSandboxVersion 2");
+ res.setError(INSTALL_FAILED_INSTANT_APP_INVALID,
+ "Instant app package must use targetSandboxVersion 2");
+ return;
+ }
+ if (pkg.mSharedUserId != null) {
+ Slog.w(TAG, "Instant app package " + pkg.packageName
+ + " may not declare sharedUserId.");
+ res.setError(INSTALL_FAILED_INSTANT_APP_INVALID,
+ "Instant app package may not declare a sharedUserId");
+ return;
+ }
}
if (pkg.applicationInfo.isStaticSharedLibrary()) {
@@ -16558,14 +16752,8 @@ public class PackageManagerService extends IPackageManager.Stub
try {
// either use what we've been given or parse directly from the APK
- if (args.certificates != null) {
- try {
- PackageParser.populateCertificates(pkg, args.certificates);
- } catch (PackageParserException e) {
- // there was something wrong with the certificates we were given;
- // try to pull them from the APK
- PackageParser.collectCertificates(pkg, parseFlags);
- }
+ if (args.signingDetails != PackageParser.SigningDetails.UNKNOWN) {
+ pkg.setSigningDetails(args.signingDetails);
} else {
PackageParser.collectCertificates(pkg, parseFlags);
}
@@ -16574,6 +16762,15 @@ public class PackageManagerService extends IPackageManager.Stub
return;
}
+ if (instantApp && pkg.mSigningDetails.signatureSchemeVersion
+ < SignatureSchemeVersion.SIGNING_BLOCK_V2) {
+ Slog.w(TAG, "Instant app package " + pkg.packageName
+ + " is not signed with at least APK Signature Scheme v2");
+ res.setError(INSTALL_FAILED_INSTANT_APP_INVALID,
+ "Instant app package must be signed with APK Signature Scheme v2 or greater");
+ return;
+ }
+
// Get rid of all references to package scan path via parser.
pp = null;
String oldCodePath = null;
@@ -16683,8 +16880,10 @@ public class PackageManagerService extends IPackageManager.Stub
try {
final boolean compareCompat = isCompatSignatureUpdateNeeded(pkg);
final boolean compareRecover = isRecoverSignatureUpdateNeeded(pkg);
+ // We don't care about disabledPkgSetting on install for now.
final boolean compatMatch = verifySignatures(
- signatureCheckPs, pkg.mSignatures, compareCompat, compareRecover);
+ signatureCheckPs, null, pkg.mSigningDetails, compareCompat,
+ compareRecover);
// The new KeySets will be re-added later in the scanning process.
if (compatMatch) {
synchronized (mPackages) {
@@ -16734,8 +16933,9 @@ public class PackageManagerService extends IPackageManager.Stub
sourcePackageSetting, scanFlags))) {
sigsOk = ksms.checkUpgradeKeySetLocked(sourcePackageSetting, pkg);
} else {
- sigsOk = compareSignatures(sourcePackageSetting.signatures.mSignatures,
- pkg.mSignatures) == PackageManager.SIGNATURE_MATCH;
+ sigsOk = compareSignatures(
+ sourcePackageSetting.signatures.mSigningDetails.signatures,
+ pkg.mSigningDetails.signatures) == PackageManager.SIGNATURE_MATCH;
}
if (!sigsOk) {
// If the owning package is the system itself, we log but allow
@@ -16835,6 +17035,43 @@ public class PackageManagerService extends IPackageManager.Stub
return;
}
+ if (PackageManagerServiceUtils.isApkVerityEnabled()) {
+ String apkPath = null;
+ synchronized (mPackages) {
+ // Note that if the attacker managed to skip verify setup, for example by tampering
+ // with the package settings, upon reboot we will do full apk verification when
+ // verity is not detected.
+ final PackageSetting ps = mSettings.mPackages.get(pkgName);
+ if (ps != null && ps.isPrivileged()) {
+ apkPath = pkg.baseCodePath;
+ }
+ }
+
+ if (apkPath != null) {
+ final VerityUtils.SetupResult result =
+ VerityUtils.generateApkVeritySetupData(apkPath);
+ if (result.isOk()) {
+ if (Build.IS_DEBUGGABLE) Slog.i(TAG, "Enabling apk verity to " + apkPath);
+ FileDescriptor fd = result.getUnownedFileDescriptor();
+ try {
+ mInstaller.installApkVerity(apkPath, fd);
+ } catch (InstallerException e) {
+ res.setError(INSTALL_FAILED_INTERNAL_ERROR,
+ "Failed to set up verity: " + e);
+ return;
+ } finally {
+ IoUtils.closeQuietly(fd);
+ }
+ } else if (result.isFailed()) {
+ res.setError(INSTALL_FAILED_INTERNAL_ERROR, "Failed to generate verity");
+ return;
+ } else {
+ // Do nothing if verity is skipped. Will fall back to full apk verification on
+ // reboot.
+ }
+ }
+ }
+
if (!instantApp) {
startIntentFilterVerifications(args.user.getIdentifier(), replace, pkg);
} else {
@@ -16867,6 +17104,11 @@ public class PackageManagerService extends IPackageManager.Stub
}
}
+ // Prepare the application profiles for the new code paths.
+ // This needs to be done before invoking dexopt so that any install-time profile
+ // can be used for optimizations.
+ mArtManagerService.prepareAppProfiles(pkg, resolveUserIds(args.user.getIdentifier()));
+
// Check whether we need to dexopt the app.
//
// NOTE: it is IMPORTANT to call dexopt:
@@ -17011,7 +17253,8 @@ public class PackageManagerService extends IPackageManager.Stub
for (ActivityIntentInfo filter : a.intents) {
if (filter.needsVerification() && needsNetworkVerificationLPr(filter)) {
if (DEBUG_DOMAIN_VERIFICATION) {
- Slog.d(TAG, "Intent filter needs verification, so processing all filters");
+ Slog.d(TAG,
+ "Intent filter needs verification, so processing all filters");
}
needToVerify = true;
break;
@@ -18341,7 +18584,8 @@ public class PackageManagerService extends IPackageManager.Stub
null /*enabledComponents*/,
null /*disabledComponents*/,
ps.readUserState(nextUserId).domainVerificationStatus,
- 0, PackageManager.INSTALL_REASON_UNKNOWN);
+ 0, PackageManager.INSTALL_REASON_UNKNOWN,
+ null /*harmfulAppWarning*/);
}
mSettings.writeKernelMappingLPr(ps);
}
@@ -18864,6 +19108,14 @@ public class PackageManagerService extends IPackageManager.Stub
return Build.VERSION_CODES.CUR_DEVELOPMENT;
}
+ private int getPackageTargetSdkVersionLockedLPr(String packageName) {
+ final PackageParser.Package p = mPackages.get(packageName);
+ if (p != null) {
+ return p.applicationInfo.targetSdkVersion;
+ }
+ return Build.VERSION_CODES.CUR_DEVELOPMENT;
+ }
+
@Override
public void addPreferredActivity(IntentFilter filter, int match,
ComponentName[] set, ComponentName activity, int userId) {
@@ -21871,6 +22123,8 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
Slog.e(TAG, "Failed to create app data for " + packageName + ": " + e);
}
}
+ // Prepare the application profiles.
+ mArtManagerService.prepareAppProfiles(pkg, userId);
if ((flags & StorageManager.FLAG_STORAGE_CE) != 0 && ceDataInode != -1) {
// TODO: mark this structure as dirty so we persist it!
@@ -22319,8 +22573,8 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
final OriginInfo origin = OriginInfo.fromExistingFile(codeFile);
final InstallParams params = new InstallParams(origin, move, installObserver, installFlags,
installerPackageName, volumeUuid, null /*verificationInfo*/, user,
- packageAbiOverride, null /*grantedPermissions*/, null /*certificates*/,
- PackageManager.INSTALL_REASON_UNKNOWN);
+ packageAbiOverride, null /*grantedPermissions*/,
+ PackageParser.SigningDetails.UNKNOWN, PackageManager.INSTALL_REASON_UNKNOWN);
params.setTraceMethod("movePackage").setTraceCookie(System.identityHashCode(params));
msg.obj = params;
@@ -23049,6 +23303,11 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
}
@Override
+ public void setUseOpenWifiAppPackagesProvider(PackagesProvider provider) {
+ mDefaultPermissionPolicy.setUseOpenWifiAppPackagesProvider(provider);
+ }
+
+ @Override
public void setSyncAdapterPackagesprovider(SyncAdapterPackagesProvider provider) {
mDefaultPermissionPolicy.setSyncAdapterPackagesProvider(provider);
}
@@ -23073,6 +23332,12 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
}
@Override
+ public void grantDefaultPermissionsToDefaultUseOpenWifiApp(String packageName, int userId) {
+ mDefaultPermissionPolicy.grantDefaultPermissionsToDefaultUseOpenWifiApp(
+ packageName, userId);
+ }
+
+ @Override
public void setKeepUninstalledPackages(final List<String> packageList) {
Preconditions.checkNotNull(packageList);
List<String> removedFromList = null;
@@ -23372,6 +23637,13 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
}
@Override
+ public int getPackageTargetSdkVersion(String packageName) {
+ synchronized (mPackages) {
+ return getPackageTargetSdkVersionLockedLPr(packageName);
+ }
+ }
+
+ @Override
public boolean canAccessInstantApps(int callingUid, int userId) {
return PackageManagerService.this.canViewInstantApps(callingUid, userId);
}
@@ -23642,6 +23914,47 @@ Slog.v(TAG, ":: stepped forward, applying functor at tag " + parser.getName());
}
return unusedPackages;
}
+
+ @Override
+ public void setHarmfulAppWarning(@NonNull String packageName, @Nullable CharSequence warning,
+ int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int callingAppId = UserHandle.getAppId(callingUid);
+
+ mPermissionManager.enforceCrossUserPermission(callingUid, userId,
+ true /*requireFullPermission*/, true /*checkShell*/, "setHarmfulAppInfo");
+
+ if (callingAppId != Process.SYSTEM_UID && callingAppId != Process.ROOT_UID &&
+ checkUidPermission(SET_HARMFUL_APP_WARNINGS, callingUid) != PERMISSION_GRANTED) {
+ throw new SecurityException("Caller must have the "
+ + SET_HARMFUL_APP_WARNINGS + " permission.");
+ }
+
+ synchronized(mPackages) {
+ mSettings.setHarmfulAppWarningLPw(packageName, warning, userId);
+ scheduleWritePackageRestrictionsLocked(userId);
+ }
+ }
+
+ @Nullable
+ @Override
+ public CharSequence getHarmfulAppWarning(@NonNull String packageName, int userId) {
+ final int callingUid = Binder.getCallingUid();
+ final int callingAppId = UserHandle.getAppId(callingUid);
+
+ mPermissionManager.enforceCrossUserPermission(callingUid, userId,
+ true /*requireFullPermission*/, true /*checkShell*/, "getHarmfulAppInfo");
+
+ if (callingAppId != Process.SYSTEM_UID && callingAppId != Process.ROOT_UID &&
+ checkUidPermission(SET_HARMFUL_APP_WARNINGS, callingUid) != PERMISSION_GRANTED) {
+ throw new SecurityException("Caller must have the "
+ + SET_HARMFUL_APP_WARNINGS + " permission.");
+ }
+
+ synchronized(mPackages) {
+ return mSettings.getHarmfulAppWarningLPr(packageName, userId);
+ }
+ }
}
interface PackageSender {
diff --git a/com/android/server/pm/PackageManagerServiceUtils.java b/com/android/server/pm/PackageManagerServiceUtils.java
index 20ec9b5e..bf3eb8ea 100644
--- a/com/android/server/pm/PackageManagerServiceUtils.java
+++ b/com/android/server/pm/PackageManagerServiceUtils.java
@@ -33,10 +33,12 @@ import com.android.server.pm.dex.DexManager;
import com.android.server.pm.dex.PackageDexUsage;
import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.AppGlobals;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.PackageParser;
+import android.content.pm.PackageParser.PackageParserException;
import android.content.pm.ResolveInfo;
import android.content.pm.Signature;
import android.os.Build;
@@ -45,12 +47,14 @@ import android.os.Environment;
import android.os.FileUtils;
import android.os.Process;
import android.os.RemoteException;
+import android.os.SystemProperties;
import android.os.UserHandle;
import android.service.pm.PackageServiceDumpProto;
import android.system.ErrnoException;
import android.system.Os;
import android.util.ArraySet;
import android.util.Log;
+import android.util.PackageUtils;
import android.util.Slog;
import android.util.jar.StrictJarFile;
import android.util.proto.ProtoOutputStream;
@@ -71,6 +75,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.text.SimpleDateFormat;
@@ -504,13 +510,13 @@ public class PackageManagerServiceUtils {
* system upgrade) and {@code scannedSigs} will be in the newer format.
*/
private static boolean matchSignaturesCompat(String packageName,
- PackageSignatures packageSignatures, Signature[] parsedSignatures) {
+ PackageSignatures packageSignatures, PackageParser.SigningDetails parsedSignatures) {
ArraySet<Signature> existingSet = new ArraySet<Signature>();
- for (Signature sig : packageSignatures.mSignatures) {
+ for (Signature sig : packageSignatures.mSigningDetails.signatures) {
existingSet.add(sig);
}
ArraySet<Signature> scannedCompatSet = new ArraySet<Signature>();
- for (Signature sig : parsedSignatures) {
+ for (Signature sig : parsedSignatures.signatures) {
try {
Signature[] chainSignatures = sig.getChainSignatures();
for (Signature chainSig : chainSignatures) {
@@ -523,7 +529,7 @@ public class PackageManagerServiceUtils {
// make sure the expanded scanned set contains all signatures in the existing one
if (scannedCompatSet.equals(existingSet)) {
// migrate the old signatures to the new scheme
- packageSignatures.assignSignatures(parsedSignatures);
+ packageSignatures.mSigningDetails = parsedSignatures;
return true;
}
return false;
@@ -547,27 +553,135 @@ public class PackageManagerServiceUtils {
}
/**
+ * Make sure the updated priv app is signed with the same key as the original APK file on the
+ * /system partition.
+ *
+ * <p>The rationale is that {@code disabledPkg} is a PackageSetting backed by xml files in /data
+ * and is not tamperproof.
+ */
+ private static boolean matchSignatureInSystem(PackageSetting pkgSetting,
+ PackageSetting disabledPkgSetting) {
+ try {
+ PackageParser.collectCertificates(disabledPkgSetting.pkg,
+ PackageParser.PARSE_IS_SYSTEM_DIR);
+ if (compareSignatures(pkgSetting.signatures.mSigningDetails.signatures,
+ disabledPkgSetting.signatures.mSigningDetails.signatures)
+ != PackageManager.SIGNATURE_MATCH) {
+ logCriticalInfo(Log.ERROR, "Updated system app mismatches cert on /system: " +
+ pkgSetting.name);
+ return false;
+ }
+ } catch (PackageParserException e) {
+ logCriticalInfo(Log.ERROR, "Failed to collect cert for " + pkgSetting.name + ": " +
+ e.getMessage());
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Checks the signing certificates to see if the provided certificate is a member. Invalid for
+ * {@code SigningDetails} with multiple signing certificates.
+ * @param certificate certificate to check for membership
+ * @param signingDetails signing certificates record whose members are to be searched
+ * @return true if {@code certificate} is in {@code signingDetails}
+ */
+ public static boolean signingDetailsHasCertificate(
+ byte[] certificate, PackageParser.SigningDetails signingDetails) {
+ if (signingDetails == PackageParser.SigningDetails.UNKNOWN) {
+ return false;
+ }
+ Signature signature = new Signature(certificate);
+ if (signingDetails.hasPastSigningCertificates()) {
+ for (int i = 0; i < signingDetails.pastSigningCertificates.length; i++) {
+ if (signingDetails.pastSigningCertificates[i].equals(signature)) {
+ return true;
+ }
+ }
+ } else {
+ // no signing history, just check the current signer
+ if (signingDetails.signatures.length == 1
+ && signingDetails.signatures[0].equals(signature)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Checks the signing certificates to see if the provided certificate is a member. Invalid for
+ * {@code SigningDetails} with multiple signing certificaes.
+ * @param sha256Certificate certificate to check for membership
+ * @param signingDetails signing certificates record whose members are to be searched
+ * @return true if {@code certificate} is in {@code signingDetails}
+ */
+ public static boolean signingDetailsHasSha256Certificate(
+ byte[] sha256Certificate, PackageParser.SigningDetails signingDetails ) {
+ if (signingDetails == PackageParser.SigningDetails.UNKNOWN) {
+ return false;
+ }
+ if (signingDetails.hasPastSigningCertificates()) {
+ for (int i = 0; i < signingDetails.pastSigningCertificates.length; i++) {
+ byte[] digest = PackageUtils.computeSha256DigestBytes(
+ signingDetails.pastSigningCertificates[i].toByteArray());
+ if (Arrays.equals(sha256Certificate, digest)) {
+ return true;
+ }
+ }
+ } else {
+ // no signing history, just check the current signer
+ if (signingDetails.signatures.length == 1) {
+ byte[] digest = PackageUtils.computeSha256DigestBytes(
+ signingDetails.signatures[0].toByteArray());
+ if (Arrays.equals(sha256Certificate, digest)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /** Returns true if APK Verity is enabled. */
+ static boolean isApkVerityEnabled() {
+ return SystemProperties.getInt("ro.apk_verity.mode", 0) != 0;
+ }
+
+ /** Returns true to force apk verification if the updated package (in /data) is a priv app. */
+ static boolean isApkVerificationForced(@Nullable PackageSetting disabledPs) {
+ return disabledPs != null && disabledPs.isPrivileged() && isApkVerityEnabled();
+ }
+
+ /**
* Verifies that signatures match.
* @returns {@code true} if the compat signatures were matched; otherwise, {@code false}.
* @throws PackageManagerException if the signatures did not match.
*/
public static boolean verifySignatures(PackageSetting pkgSetting,
- Signature[] parsedSignatures, boolean compareCompat, boolean compareRecover)
+ PackageSetting disabledPkgSetting, PackageParser.SigningDetails parsedSignatures,
+ boolean compareCompat, boolean compareRecover)
throws PackageManagerException {
final String packageName = pkgSetting.name;
boolean compatMatch = false;
- if (pkgSetting.signatures.mSignatures != null) {
+ if (pkgSetting.signatures.mSigningDetails.signatures != null) {
// Already existing package. Make sure signatures match
- boolean match = compareSignatures(pkgSetting.signatures.mSignatures, parsedSignatures)
+ boolean match = compareSignatures(pkgSetting.signatures.mSigningDetails.signatures,
+ parsedSignatures.signatures)
== PackageManager.SIGNATURE_MATCH;
if (!match && compareCompat) {
- match = matchSignaturesCompat(packageName, pkgSetting.signatures, parsedSignatures);
+ match = matchSignaturesCompat(packageName, pkgSetting.signatures,
+ parsedSignatures);
compatMatch = match;
}
if (!match && compareRecover) {
match = matchSignaturesRecover(
- packageName, pkgSetting.signatures.mSignatures, parsedSignatures);
+ packageName, pkgSetting.signatures.mSigningDetails.signatures,
+ parsedSignatures.signatures);
+ }
+
+ if (!match && isApkVerificationForced(disabledPkgSetting)) {
+ match = matchSignatureInSystem(pkgSetting, disabledPkgSetting);
}
+
if (!match) {
throw new PackageManagerException(INSTALL_FAILED_UPDATE_INCOMPATIBLE,
"Package " + packageName +
@@ -575,17 +689,21 @@ public class PackageManagerServiceUtils {
}
}
// Check for shared user signatures
- if (pkgSetting.sharedUser != null && pkgSetting.sharedUser.signatures.mSignatures != null) {
+ if (pkgSetting.sharedUser != null
+ && pkgSetting.sharedUser.signatures.mSigningDetails.signatures != null) {
// Already existing package. Make sure signatures match
- boolean match = compareSignatures(pkgSetting.sharedUser.signatures.mSignatures,
- parsedSignatures) == PackageManager.SIGNATURE_MATCH;
+ boolean match =
+ compareSignatures(
+ pkgSetting.sharedUser.signatures.mSigningDetails.signatures,
+ parsedSignatures.signatures) == PackageManager.SIGNATURE_MATCH;
if (!match && compareCompat) {
match = matchSignaturesCompat(
packageName, pkgSetting.sharedUser.signatures, parsedSignatures);
}
if (!match && compareRecover) {
- match = matchSignaturesRecover(
- packageName, pkgSetting.sharedUser.signatures.mSignatures, parsedSignatures);
+ match = matchSignaturesRecover(packageName,
+ pkgSetting.sharedUser.signatures.mSigningDetails.signatures,
+ parsedSignatures.signatures);
compatMatch |= match;
}
if (!match) {
diff --git a/com/android/server/pm/PackageManagerShellCommand.java b/com/android/server/pm/PackageManagerShellCommand.java
index 2d82c469..47cd8132 100644
--- a/com/android/server/pm/PackageManagerShellCommand.java
+++ b/com/android/server/pm/PackageManagerShellCommand.java
@@ -18,6 +18,7 @@ package com.android.server.pm;
import android.accounts.IAccountManager;
import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
import android.content.ComponentName;
import android.content.Context;
import android.content.IIntentReceiver;
@@ -46,6 +47,7 @@ import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.UserInfo;
import android.content.pm.VersionedPackage;
+import android.content.pm.dex.DexMetadataHelper;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.net.Uri;
@@ -72,13 +74,13 @@ import android.util.PrintWriterPrinter;
import com.android.internal.content.PackageHelper;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.SizedInputStream;
+import com.android.server.LocalServices;
import com.android.server.SystemConfig;
import dalvik.system.DexFile;
import libcore.io.IoUtils;
-import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
@@ -222,6 +224,8 @@ class PackageManagerShellCommand extends ShellCommand {
return runSetUserRestriction();
case "get-max-users":
return runGetMaxUsers();
+ case "get-max-running-users":
+ return runGetMaxRunningUsers();
case "set-home-activity":
return runSetHomeActivity();
case "set-installer":
@@ -230,6 +234,10 @@ class PackageManagerShellCommand extends ShellCommand {
return runGetInstantAppResolver();
case "has-feature":
return runHasFeature();
+ case "set-harmful-app-warning":
+ return runSetHarmfulAppWarning();
+ case "get-harmful-app-warning":
+ return runGetHarmfulAppWarning();
default: {
String nextArg = getNextArg();
if (nextArg == null) {
@@ -1277,7 +1285,7 @@ class PackageManagerShellCommand extends ShellCommand {
return runRemoveSplit(packageName, splitName);
}
- userId = translateUserId(userId, "runUninstall");
+ userId = translateUserId(userId, true /*allowAll*/, "runUninstall");
if (userId == UserHandle.USER_ALL) {
userId = UserHandle.USER_SYSTEM;
flags |= PackageManager.DELETE_ALL_USERS;
@@ -1881,6 +1889,14 @@ class PackageManagerShellCommand extends ShellCommand {
return 0;
}
+ public int runGetMaxRunningUsers() {
+ ActivityManagerInternal activityManagerInternal =
+ LocalServices.getService(ActivityManagerInternal.class);
+ getOutPrintWriter().println("Maximum supported running users: "
+ + activityManagerInternal.getMaxRunningUsers());
+ return 0;
+ }
+
private static class InstallParams {
SessionParams sessionParams;
String installerPackageName;
@@ -2088,6 +2104,54 @@ class PackageManagerShellCommand extends ShellCommand {
return 0;
}
+ private int runSetHarmfulAppWarning() 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;
+ }
+ }
+
+ userId = translateUserId(userId, false /*allowAll*/, "runSetHarmfulAppWarning");
+
+ final String packageName = getNextArgRequired();
+ final String warning = getNextArg();
+
+ mInterface.setHarmfulAppWarning(packageName, warning, userId);
+
+ return 0;
+ }
+
+ private int runGetHarmfulAppWarning() 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;
+ }
+ }
+
+ userId = translateUserId(userId, false /*allowAll*/, "runGetHarmfulAppWarning");
+
+ final String packageName = getNextArgRequired();
+ final CharSequence warning = mInterface.getHarmfulAppWarning(packageName, userId);
+ if (!TextUtils.isEmpty(warning)) {
+ getOutPrintWriter().println(warning);
+ return 0;
+ } else {
+ return 1;
+ }
+ }
+
private static String checkAbiArgument(String abi) {
if (TextUtils.isEmpty(abi)) {
throw new IllegalArgumentException("Missing ABI argument");
@@ -2107,14 +2171,14 @@ class PackageManagerShellCommand extends ShellCommand {
throw new IllegalArgumentException("ABI " + abi + " not supported on this device");
}
- private int translateUserId(int userId, String logContext) {
+ private int translateUserId(int userId, boolean allowAll, String logContext) {
return ActivityManager.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(),
- userId, true, true, logContext, "pm command");
+ userId, allowAll, true, logContext, "pm command");
}
private int doCreateSession(SessionParams params, String installerPackageName, int userId)
throws RemoteException {
- userId = translateUserId(userId, "runInstallCreate");
+ userId = translateUserId(userId, true /*allowAll*/, "runInstallCreate");
if (userId == UserHandle.USER_ALL) {
userId = UserHandle.USER_SYSTEM;
params.installFlags |= PackageManager.INSTALL_ALL_USERS;
@@ -2221,6 +2285,14 @@ class PackageManagerShellCommand extends ShellCommand {
session = new PackageInstaller.Session(
mInterface.getPackageInstaller().openSession(sessionId));
+ // Sanity check that all .dm files match an apk.
+ // (The installer does not support standalone .dm files and will not process them.)
+ try {
+ DexMetadataHelper.validateDexPaths(session.getNames());
+ } catch (IllegalStateException | IOException e) {
+ pw.println("Warning [Could not validate the dex paths: " + e.getMessage() + "]");
+ }
+
final LocalIntentReceiver receiver = new LocalIntentReceiver();
session.commit(receiver.getIntentSender());
@@ -2582,6 +2654,8 @@ class PackageManagerShellCommand extends ShellCommand {
pw.println("");
pw.println(" get-max-users");
pw.println("");
+ pw.println(" get-max-running-users");
+ pw.println("");
pw.println(" compile [-m MODE | -r REASON] [-f] [-c] [--split SPLIT_NAME]");
pw.println(" [--reset] [--check-prof (true | false)] (-a | TARGET-PACKAGE)");
pw.println(" Trigger compilation of TARGET-PACKAGE or all packages if \"-a\". Options are:");
@@ -2634,6 +2708,12 @@ class PackageManagerShellCommand extends ShellCommand {
pw.println("");
pw.println(" get-instantapp-resolver");
pw.println(" Return the name of the component that is the current instant app installer.");
+ pw.println("");
+ pw.println(" set-harmful-app-warning [--user <USER_ID>] <PACKAGE> [<WARNING>]");
+ pw.println(" Mark the app as harmful with the given warning message.");
+ pw.println("");
+ pw.println(" get-harmful-app-warning [--user <USER_ID>] <PACKAGE>");
+ pw.println(" Return the harmful app warning message for the given app, if present");
pw.println();
Intent.printIntentArgsHelp(pw , "");
}
diff --git a/com/android/server/pm/PackageSetting.java b/com/android/server/pm/PackageSetting.java
index 2b91b7d3..2a2430c0 100644
--- a/com/android/server/pm/PackageSetting.java
+++ b/com/android/server/pm/PackageSetting.java
@@ -97,6 +97,35 @@ public final class PackageSetting extends PackageSettingBase {
+ " " + name + "/" + appId + "}";
}
+ // Temporary to catch potential issues with refactoring
+ public String dumpState_temp() {
+ String flags = "";
+ flags += ((pkgFlags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 ? "U" : "");
+ flags += ((pkgFlags & ApplicationInfo.FLAG_SYSTEM) != 0 ? "S" : "");
+ if ("".equals(flags)) {
+ flags = "-";
+ }
+ String privFlags = "";
+ privFlags += ((pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0 ? "P" : "");
+ privFlags += ((pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_OEM) != 0 ? "O" : "");
+ privFlags += ((pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_VENDOR) != 0 ? "V" : "");
+ if ("".equals(privFlags)) {
+ privFlags = "-";
+ }
+ return "PackageSetting{"
+ + Integer.toHexString(System.identityHashCode(this))
+ + " " + name + (realName == null ? "" : "("+realName+")") + "/" + appId + (sharedUser==null?"":" u:" + sharedUser.name+"("+sharedUserId+")")
+ + ", ver:" + versionCode
+ + ", path: " + codePath
+ + ", pABI: " + primaryCpuAbiString
+ + ", sABI: " + secondaryCpuAbiString
+ + ", oABI: " + cpuAbiOverrideString
+ + ", flags: " + flags
+ + ", privFlags: " + privFlags
+ + ", pkg: " + (pkg == null ? "<<NULL>>" : pkg.dumpState_temp())
+ + "}";
+ }
+
public void copyFrom(PackageSetting orig) {
super.copyFrom(orig);
doCopy(orig);
diff --git a/com/android/server/pm/PackageSettingBase.java b/com/android/server/pm/PackageSettingBase.java
index 809e16cb..18356c57 100644
--- a/com/android/server/pm/PackageSettingBase.java
+++ b/com/android/server/pm/PackageSettingBase.java
@@ -233,7 +233,7 @@ public abstract class PackageSettingBase extends SettingBase {
}
public Signature[] getSignatures() {
- return signatures.mSignatures;
+ return signatures.mSigningDetails.signatures;
}
/**
@@ -437,7 +437,8 @@ public abstract class PackageSettingBase extends SettingBase {
boolean notLaunched, boolean hidden, boolean suspended, boolean instantApp,
boolean virtualPreload, String lastDisableAppCaller,
ArraySet<String> enabledComponents, ArraySet<String> disabledComponents,
- int domainVerifState, int linkGeneration, int installReason) {
+ int domainVerifState, int linkGeneration, int installReason,
+ String harmfulAppWarning) {
PackageUserState state = modifyUserState(userId);
state.ceDataInode = ceDataInode;
state.enabled = enabled;
@@ -454,6 +455,7 @@ public abstract class PackageSettingBase extends SettingBase {
state.installReason = installReason;
state.instantApp = instantApp;
state.virtualPreload = virtualPreload;
+ state.harmfulAppWarning = harmfulAppWarning;
}
ArraySet<String> getEnabledComponents(int userId) {
@@ -620,4 +622,14 @@ public abstract class PackageSettingBase extends SettingBase {
proto.end(userToken);
}
}
+
+ void setHarmfulAppWarning(int userId, String harmfulAppWarning) {
+ PackageUserState userState = modifyUserState(userId);
+ userState.harmfulAppWarning = harmfulAppWarning;
+ }
+
+ String getHarmfulAppWarning(int userId) {
+ PackageUserState userState = readUserState(userId);
+ return userState.harmfulAppWarning;
+ }
}
diff --git a/com/android/server/pm/PackageSignatures.java b/com/android/server/pm/PackageSignatures.java
index f5c81e4c..d471fc83 100644
--- a/com/android/server/pm/PackageSignatures.java
+++ b/com/android/server/pm/PackageSignatures.java
@@ -22,77 +22,148 @@ import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlSerializer;
+import android.annotation.NonNull;
+import android.content.pm.PackageParser;
+import android.content.pm.PackageParser.SigningDetails.SignatureSchemeVersion;
import android.content.pm.Signature;
import android.util.Log;
import java.io.IOException;
+import java.security.cert.CertificateException;
import java.util.ArrayList;
class PackageSignatures {
- Signature[] mSignatures;
+
+ @NonNull PackageParser.SigningDetails mSigningDetails;
PackageSignatures(PackageSignatures orig) {
- if (orig != null && orig.mSignatures != null) {
- mSignatures = orig.mSignatures.clone();
+ if (orig != null && orig.mSigningDetails != PackageParser.SigningDetails.UNKNOWN) {
+ mSigningDetails = new PackageParser.SigningDetails(orig.mSigningDetails);
+ } else {
+ mSigningDetails = PackageParser.SigningDetails.UNKNOWN;
}
}
- PackageSignatures(Signature[] sigs) {
- assignSignatures(sigs);
+ PackageSignatures(PackageParser.SigningDetails signingDetails) {
+ mSigningDetails = signingDetails;
}
PackageSignatures() {
+ mSigningDetails = PackageParser.SigningDetails.UNKNOWN;
}
void writeXml(XmlSerializer serializer, String tagName,
- ArrayList<Signature> pastSignatures) throws IOException {
- if (mSignatures == null) {
+ ArrayList<Signature> writtenSignatures) throws IOException {
+ if (mSigningDetails.signatures == null) {
return;
}
serializer.startTag(null, tagName);
- serializer.attribute(null, "count",
- Integer.toString(mSignatures.length));
- for (int i=0; i<mSignatures.length; i++) {
+ serializer.attribute(null, "count", Integer.toString(mSigningDetails.signatures.length));
+ serializer.attribute(null, "schemeVersion",
+ Integer.toString(mSigningDetails.signatureSchemeVersion));
+ writeCertsListXml(serializer, writtenSignatures, mSigningDetails.signatures, null);
+
+ // if we have past signer certificate information, write it out
+ if (mSigningDetails.pastSigningCertificates != null) {
+ serializer.startTag(null, "pastSigs");
+ serializer.attribute(null, "count",
+ Integer.toString(mSigningDetails.pastSigningCertificates.length));
+ writeCertsListXml(
+ serializer, writtenSignatures, mSigningDetails.pastSigningCertificates,
+ mSigningDetails.pastSigningCertificatesFlags);
+ serializer.endTag(null, "pastSigs");
+ }
+ serializer.endTag(null, tagName);
+ }
+
+ private void writeCertsListXml(XmlSerializer serializer, ArrayList<Signature> writtenSignatures,
+ Signature[] signatures, int[] flags) throws IOException {
+ for (int i=0; i<signatures.length; i++) {
serializer.startTag(null, "cert");
- final Signature sig = mSignatures[i];
+ final Signature sig = signatures[i];
final int sigHash = sig.hashCode();
- final int numPast = pastSignatures.size();
+ final int numWritten = writtenSignatures.size();
int j;
- for (j=0; j<numPast; j++) {
- Signature pastSig = pastSignatures.get(j);
- if (pastSig.hashCode() == sigHash && pastSig.equals(sig)) {
+ for (j=0; j<numWritten; j++) {
+ Signature writtenSig = writtenSignatures.get(j);
+ if (writtenSig.hashCode() == sigHash && writtenSig.equals(sig)) {
serializer.attribute(null, "index", Integer.toString(j));
break;
}
}
- if (j >= numPast) {
- pastSignatures.add(sig);
- serializer.attribute(null, "index", Integer.toString(numPast));
+ if (j >= numWritten) {
+ writtenSignatures.add(sig);
+ serializer.attribute(null, "index", Integer.toString(numWritten));
serializer.attribute(null, "key", sig.toCharsString());
}
+ if (flags != null) {
+ serializer.attribute(null, "flags", Integer.toString(flags[i]));
+ }
serializer.endTag(null, "cert");
}
- serializer.endTag(null, tagName);
}
- void readXml(XmlPullParser parser, ArrayList<Signature> pastSignatures)
+ void readXml(XmlPullParser parser, ArrayList<Signature> readSignatures)
throws IOException, XmlPullParserException {
+ PackageParser.SigningDetails.Builder builder =
+ new PackageParser.SigningDetails.Builder();
+
String countStr = parser.getAttributeValue(null, "count");
if (countStr == null) {
PackageManagerService.reportSettingsProblem(Log.WARN,
- "Error in package manager settings: <signatures> has"
+ "Error in package manager settings: <sigs> has"
+ " no count at " + parser.getPositionDescription());
XmlUtils.skipCurrentTag(parser);
}
final int count = Integer.parseInt(countStr);
- mSignatures = new Signature[count];
+
+ String schemeVersionStr = parser.getAttributeValue(null, "schemeVersion");
+ int signatureSchemeVersion;
+ if (schemeVersionStr == null) {
+ PackageManagerService.reportSettingsProblem(Log.WARN,
+ "Error in package manager settings: <sigs> has no schemeVersion at "
+ + parser.getPositionDescription());
+ signatureSchemeVersion = SignatureSchemeVersion.UNKNOWN;
+ } else {
+ signatureSchemeVersion = Integer.parseInt(schemeVersionStr);
+ }
+ builder.setSignatureSchemeVersion(signatureSchemeVersion);
+ Signature[] signatures = new Signature[count];
+ int pos = readCertsListXml(parser, readSignatures, signatures, null, builder);
+ builder.setSignatures(signatures);
+ if (pos < count) {
+ // Should never happen -- there is an error in the written
+ // settings -- but if it does we don't want to generate
+ // a bad array.
+ Signature[] newSigs = new Signature[pos];
+ System.arraycopy(signatures, 0, newSigs, 0, pos);
+ builder = builder.setSignatures(newSigs);
+ PackageManagerService.reportSettingsProblem(Log.WARN,
+ "Error in package manager settings: <sigs> count does not match number of "
+ + " <cert> entries" + parser.getPositionDescription());
+ }
+
+ try {
+ mSigningDetails = builder.build();
+ } catch (CertificateException e) {
+ PackageManagerService.reportSettingsProblem(Log.WARN,
+ "Error in package manager settings: <sigs> "
+ + "unable to convert certificate(s) to public key(s).");
+ mSigningDetails = PackageParser.SigningDetails.UNKNOWN;
+ }
+ }
+
+ private int readCertsListXml(XmlPullParser parser, ArrayList<Signature> readSignatures,
+ Signature[] signatures, int[] flags, PackageParser.SigningDetails.Builder builder)
+ throws IOException, XmlPullParserException {
+ int count = signatures.length;
int pos = 0;
int outerDepth = parser.getDepth();
int type;
while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
- && (type != XmlPullParser.END_TAG
- || parser.getDepth() > outerDepth)) {
+ && (type != XmlPullParser.END_TAG
+ || parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG
|| type == XmlPullParser.TEXT) {
continue;
@@ -107,82 +178,128 @@ class PackageSignatures {
int idx = Integer.parseInt(index);
String key = parser.getAttributeValue(null, "key");
if (key == null) {
- if (idx >= 0 && idx < pastSignatures.size()) {
- Signature sig = pastSignatures.get(idx);
+ if (idx >= 0 && idx < readSignatures.size()) {
+ Signature sig = readSignatures.get(idx);
if (sig != null) {
- mSignatures[pos] = pastSignatures.get(idx);
+ signatures[pos] = readSignatures.get(idx);
pos++;
} else {
PackageManagerService.reportSettingsProblem(Log.WARN,
"Error in package manager settings: <cert> "
- + "index " + index + " is not defined at "
- + parser.getPositionDescription());
+ + "index " + index + " is not defined at "
+ + parser.getPositionDescription());
}
} else {
PackageManagerService.reportSettingsProblem(Log.WARN,
"Error in package manager settings: <cert> "
- + "index " + index + " is out of bounds at "
- + parser.getPositionDescription());
+ + "index " + index + " is out of bounds at "
+ + parser.getPositionDescription());
}
} else {
- while (pastSignatures.size() <= idx) {
- pastSignatures.add(null);
+ while (readSignatures.size() <= idx) {
+ readSignatures.add(null);
}
Signature sig = new Signature(key);
- pastSignatures.set(idx, sig);
- mSignatures[pos] = sig;
+ readSignatures.set(idx, sig);
+ signatures[pos] = sig;
pos++;
}
} catch (NumberFormatException e) {
PackageManagerService.reportSettingsProblem(Log.WARN,
"Error in package manager settings: <cert> "
- + "index " + index + " is not a number at "
- + parser.getPositionDescription());
+ + "index " + index + " is not a number at "
+ + parser.getPositionDescription());
} catch (IllegalArgumentException e) {
PackageManagerService.reportSettingsProblem(Log.WARN,
"Error in package manager settings: <cert> "
- + "index " + index + " has an invalid signature at "
- + parser.getPositionDescription() + ": "
- + e.getMessage());
+ + "index " + index + " has an invalid signature at "
+ + parser.getPositionDescription() + ": "
+ + e.getMessage());
+ }
+
+ if (flags != null) {
+ String flagsStr = parser.getAttributeValue(null, "flags");
+ if (flagsStr != null) {
+ try {
+ flags[pos] = Integer.parseInt(flagsStr);
+ } catch (NumberFormatException e) {
+ PackageManagerService.reportSettingsProblem(Log.WARN,
+ "Error in package manager settings: <cert> "
+ + "flags " + flagsStr + " is not a number at "
+ + parser.getPositionDescription());
+ }
+ } else {
+ PackageManagerService.reportSettingsProblem(Log.WARN,
+ "Error in package manager settings: <cert> has no"
+ + " flags at " + parser.getPositionDescription());
+ }
}
} else {
PackageManagerService.reportSettingsProblem(Log.WARN,
"Error in package manager settings: <cert> has"
- + " no index at " + parser.getPositionDescription());
+ + " no index at " + parser.getPositionDescription());
}
} else {
PackageManagerService.reportSettingsProblem(Log.WARN,
"Error in package manager settings: too "
- + "many <cert> tags, expected " + count
- + " at " + parser.getPositionDescription());
+ + "many <cert> tags, expected " + count
+ + " at " + parser.getPositionDescription());
+ }
+ } else if (tagName.equals("pastSigs")) {
+ if (flags == null) {
+ // we haven't encountered pastSigs yet, go ahead
+ String countStr = parser.getAttributeValue(null, "count");
+ if (countStr == null) {
+ PackageManagerService.reportSettingsProblem(Log.WARN,
+ "Error in package manager settings: <pastSigs> has"
+ + " no count at " + parser.getPositionDescription());
+ XmlUtils.skipCurrentTag(parser);
+ }
+ try {
+ final int pastSigsCount = Integer.parseInt(countStr);
+ Signature[] pastSignatures = new Signature[pastSigsCount];
+ int[] pastSignaturesFlags = new int[pastSigsCount];
+ int pastSigsPos = readCertsListXml(parser, readSignatures, pastSignatures,
+ pastSignaturesFlags, builder);
+ builder = builder
+ .setPastSigningCertificates(pastSignatures)
+ .setPastSigningCertificatesFlags(pastSignaturesFlags);
+
+ if (pastSigsPos < pastSigsCount) {
+ // Should never happen -- there is an error in the written
+ // settings -- but if it does we don't want to generate
+ // a bad array.
+ Signature[] newSigs = new Signature[pastSigsPos];
+ System.arraycopy(pastSignatures, 0, newSigs, 0, pastSigsPos);
+ int[] newFlags = new int[pastSigsPos];
+ System.arraycopy(pastSignaturesFlags, 0, newFlags, 0, pastSigsPos);
+ builder = builder
+ .setPastSigningCertificates(newSigs)
+ .setPastSigningCertificatesFlags(newFlags);
+ PackageManagerService.reportSettingsProblem(Log.WARN,
+ "Error in package manager settings: <pastSigs> count does not "
+ + "match number of <cert> entries "
+ + parser.getPositionDescription());
+ }
+ } catch (NumberFormatException e) {
+ PackageManagerService.reportSettingsProblem(Log.WARN,
+ "Error in package manager settings: <pastSigs> "
+ + "count " + countStr + " is not a number at "
+ + parser.getPositionDescription());
+ }
+ } else {
+ PackageManagerService.reportSettingsProblem(Log.WARN,
+ "<pastSigs> encountered multiple times under the same <sigs> at "
+ + parser.getPositionDescription());
}
} else {
PackageManagerService.reportSettingsProblem(Log.WARN,
- "Unknown element under <cert>: "
- + parser.getName());
+ "Unknown element under <sigs>: "
+ + parser.getName());
}
XmlUtils.skipCurrentTag(parser);
}
-
- if (pos < count) {
- // Should never happen -- there is an error in the written
- // settings -- but if it does we don't want to generate
- // a bad array.
- Signature[] newSigs = new Signature[pos];
- System.arraycopy(mSignatures, 0, newSigs, 0, pos);
- mSignatures = newSigs;
- }
- }
-
- void assignSignatures(Signature[] sigs) {
- if (sigs == null) {
- mSignatures = null;
- return;
- }
- mSignatures = new Signature[sigs.length];
- for (int i=0; i<sigs.length; i++) {
- mSignatures[i] = sigs[i];
- }
+ return pos;
}
@Override
@@ -190,15 +307,27 @@ class PackageSignatures {
StringBuffer buf = new StringBuffer(128);
buf.append("PackageSignatures{");
buf.append(Integer.toHexString(System.identityHashCode(this)));
- buf.append(" [");
- if (mSignatures != null) {
- for (int i=0; i<mSignatures.length; i++) {
+ buf.append(" version:");
+ buf.append(mSigningDetails.signatureSchemeVersion);
+ buf.append(", signatures:[");
+ if (mSigningDetails.signatures != null) {
+ for (int i = 0; i < mSigningDetails.signatures.length; i++) {
if (i > 0) buf.append(", ");
buf.append(Integer.toHexString(
- mSignatures[i].hashCode()));
+ mSigningDetails.signatures[i].hashCode()));
}
}
buf.append("]}");
+ buf.append(", past signatures:[");
+ if (mSigningDetails.pastSigningCertificates != null) {
+ for (int i = 0; i < mSigningDetails.pastSigningCertificates.length; i++) {
+ if (i > 0) buf.append(", ");
+ buf.append(Integer.toHexString(
+ mSigningDetails.pastSigningCertificates[i].hashCode()));
+ buf.append(" flags: ");
+ buf.append(Integer.toHexString(mSigningDetails.pastSigningCertificatesFlags[i]));
+ }
+ }
return buf.toString();
}
} \ No newline at end of file
diff --git a/com/android/server/pm/SELinuxMMAC.java b/com/android/server/pm/SELinuxMMAC.java
index fbf3d824..2552643a 100644
--- a/com/android/server/pm/SELinuxMMAC.java
+++ b/com/android/server/pm/SELinuxMMAC.java
@@ -17,9 +17,8 @@
package com.android.server.pm;
import android.content.pm.PackageParser;
-import android.content.pm.PackageUserState;
-import android.content.pm.SELinuxUtil;
import android.content.pm.Signature;
+import android.content.pm.PackageParser.SigningDetails;
import android.os.Environment;
import android.util.Slog;
import android.util.Xml;
@@ -453,7 +452,8 @@ final class Policy {
public String getMatchedSeInfo(PackageParser.Package pkg) {
// Check for exact signature matches across all certs.
Signature[] certs = mCerts.toArray(new Signature[0]);
- if (!Signature.areExactMatch(certs, pkg.mSignatures)) {
+ if (pkg.mSigningDetails != SigningDetails.UNKNOWN
+ && !Signature.areExactMatch(certs, pkg.mSigningDetails.signatures)) {
return null;
}
diff --git a/com/android/server/pm/Settings.java b/com/android/server/pm/Settings.java
index 648f847a..8ce412e5 100644
--- a/com/android/server/pm/Settings.java
+++ b/com/android/server/pm/Settings.java
@@ -227,6 +227,7 @@ public final class Settings {
private static final String ATTR_INSTALL_REASON = "install-reason";
private static final String ATTR_INSTANT_APP = "instant-app";
private static final String ATTR_VIRTUAL_PRELOAD = "virtual-preload";
+ private static final String ATTR_HARMFUL_APP_WARNING = "harmful-app-warning";
private static final String ATTR_PACKAGE_NAME = "packageName";
private static final String ATTR_FINGERPRINT = "fingerprint";
@@ -742,7 +743,8 @@ public final class Settings {
null /*enabledComponents*/,
null /*disabledComponents*/,
INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED,
- 0, PackageManager.INSTALL_REASON_UNKNOWN);
+ 0, PackageManager.INSTALL_REASON_UNKNOWN,
+ null /*harmfulAppWarning*/);
}
}
}
@@ -783,11 +785,12 @@ public final class Settings {
*/
static void updatePackageSetting(@NonNull PackageSetting pkgSetting,
@Nullable PackageSetting disabledPkg, @Nullable SharedUserSetting sharedUser,
- @NonNull File codePath, @Nullable String legacyNativeLibraryPath,
- @Nullable String primaryCpuAbi, @Nullable String secondaryCpuAbi,
- int pkgFlags, int pkgPrivateFlags, @Nullable List<String> childPkgNames,
- @NonNull UserManagerService userManager, @Nullable String[] usesStaticLibraries,
- @Nullable long[] usesStaticLibrariesVersions) throws PackageManagerException {
+ @NonNull File codePath, File resourcePath,
+ @Nullable String legacyNativeLibraryPath, @Nullable String primaryCpuAbi,
+ @Nullable String secondaryCpuAbi, int pkgFlags, int pkgPrivateFlags,
+ @Nullable List<String> childPkgNames, @NonNull UserManagerService userManager,
+ @Nullable String[] usesStaticLibraries, @Nullable long[] usesStaticLibrariesVersions)
+ throws PackageManagerException {
final String pkgName = pkgSetting.name;
if (pkgSetting.sharedUser != sharedUser) {
PackageManagerService.reportSettingsProblem(Log.WARN,
@@ -799,29 +802,19 @@ public final class Settings {
}
if (!pkgSetting.codePath.equals(codePath)) {
- // Check to see if its a disabled system app
- if ((pkgSetting.pkgFlags & ApplicationInfo.FLAG_SYSTEM) != 0) {
- // This is an updated system app with versions in both system
- // and data partition. Just let the most recent version
- // take precedence.
- Slog.w(PackageManagerService.TAG,
- "Trying to update system app code path from "
- + pkgSetting.codePathString + " to " + codePath.toString());
- } else {
- // Just a change in the code path is not an issue, but
- // let's log a message about it.
- Slog.i(PackageManagerService.TAG,
- "Package " + pkgName + " codePath changed from "
- + pkgSetting.codePath + " to " + codePath
- + "; Retaining data and using new");
-
- // The owner user's installed flag is set false
- // when the application was installed by other user
- // and the installed flag is not updated
- // when the application is appended as system app later.
- if ((pkgFlags & ApplicationInfo.FLAG_SYSTEM) != 0
- && disabledPkg == null) {
- List<UserInfo> allUserInfos = getAllUsers(userManager);
+ final boolean isSystem = pkgSetting.isSystem();
+ Slog.i(PackageManagerService.TAG,
+ "Update" + (isSystem ? " system" : "")
+ + " package " + pkgName
+ + " code path from " + pkgSetting.codePathString
+ + " to " + codePath.toString()
+ + "; Retain data and using new");
+ if (!isSystem) {
+ // The package isn't considered as installed if the application was
+ // first installed by another user. Update the installed flag when the
+ // application ever becomes part of the system.
+ if ((pkgFlags & ApplicationInfo.FLAG_SYSTEM) != 0 && disabledPkg == null) {
+ final List<UserInfo> allUserInfos = getAllUsers(userManager);
if (allUserInfos != null) {
for (UserInfo userInfo : allUserInfos) {
pkgSetting.setInstalled(true, userInfo.id);
@@ -829,14 +822,24 @@ public final class Settings {
}
}
- /*
- * Since we've changed paths, we need to prefer the new
- * native library path over the one stored in the
- * package settings since we might have moved from
- * internal to external storage or vice versa.
- */
+ // Since we've changed paths, prefer the new native library path over
+ // the one stored in the package settings since we might have moved from
+ // internal to external storage or vice versa.
pkgSetting.legacyNativeLibraryPathString = legacyNativeLibraryPath;
}
+ pkgSetting.codePath = codePath;
+ pkgSetting.codePathString = codePath.toString();
+ }
+ if (!pkgSetting.resourcePath.equals(resourcePath)) {
+ final boolean isSystem = pkgSetting.isSystem();
+ Slog.i(PackageManagerService.TAG,
+ "Update" + (isSystem ? " system" : "")
+ + " package " + pkgName
+ + " resource path from " + pkgSetting.resourcePathString
+ + " to " + resourcePath.toString()
+ + "; Retain data and using new");
+ pkgSetting.resourcePath = resourcePath;
+ pkgSetting.resourcePathString = resourcePath.toString();
}
// If what we are scanning is a system (and possibly privileged) package,
// then make it so, regardless of whether it was previously installed only
@@ -853,13 +856,14 @@ public final class Settings {
if (childPkgNames != null) {
pkgSetting.childPackageNames = new ArrayList<>(childPkgNames);
}
- if (usesStaticLibraries != null) {
- pkgSetting.usesStaticLibraries = Arrays.copyOf(usesStaticLibraries,
- usesStaticLibraries.length);
- }
- if (usesStaticLibrariesVersions != null) {
- pkgSetting.usesStaticLibrariesVersions = Arrays.copyOf(usesStaticLibrariesVersions,
- usesStaticLibrariesVersions.length);
+ // Update static shared library dependencies if needed
+ if (usesStaticLibraries != null && usesStaticLibrariesVersions != null
+ && usesStaticLibraries.length == usesStaticLibrariesVersions.length) {
+ pkgSetting.usesStaticLibraries = usesStaticLibraries;
+ pkgSetting.usesStaticLibrariesVersions = usesStaticLibrariesVersions;
+ } else {
+ pkgSetting.usesStaticLibraries = null;
+ pkgSetting.usesStaticLibrariesVersions = null;
}
}
@@ -912,69 +916,17 @@ public final class Settings {
userId);
}
+ // TODO: Move to scanPackageOnlyLI() after verifying signatures are setup correctly
+ // by that time.
void insertPackageSettingLPw(PackageSetting p, PackageParser.Package pkg) {
- p.pkg = pkg;
- // pkg.mSetEnabled = p.getEnabled(userId);
- // pkg.mSetStopped = p.getStopped(userId);
- final String volumeUuid = pkg.applicationInfo.volumeUuid;
- final String codePath = pkg.applicationInfo.getCodePath();
- final String resourcePath = pkg.applicationInfo.getResourcePath();
- final String legacyNativeLibraryPath = pkg.applicationInfo.nativeLibraryRootDir;
- // Update volume if needed
- if (!Objects.equals(volumeUuid, p.volumeUuid)) {
- Slog.w(PackageManagerService.TAG, "Volume for " + p.pkg.packageName +
- " changing from " + p.volumeUuid + " to " + volumeUuid);
- p.volumeUuid = volumeUuid;
- }
- // Update code path if needed
- if (!Objects.equals(codePath, p.codePathString)) {
- Slog.w(PackageManagerService.TAG, "Code path for " + p.pkg.packageName +
- " changing from " + p.codePathString + " to " + codePath);
- p.codePath = new File(codePath);
- p.codePathString = codePath;
- }
- //Update resource path if needed
- if (!Objects.equals(resourcePath, p.resourcePathString)) {
- Slog.w(PackageManagerService.TAG, "Resource path for " + p.pkg.packageName +
- " changing from " + p.resourcePathString + " to " + resourcePath);
- p.resourcePath = new File(resourcePath);
- p.resourcePathString = resourcePath;
- }
- // Update the native library paths if needed
- if (!Objects.equals(legacyNativeLibraryPath, p.legacyNativeLibraryPathString)) {
- p.legacyNativeLibraryPathString = legacyNativeLibraryPath;
- }
-
- // Update the required Cpu Abi
- p.primaryCpuAbiString = pkg.applicationInfo.primaryCpuAbi;
- p.secondaryCpuAbiString = pkg.applicationInfo.secondaryCpuAbi;
- p.cpuAbiOverrideString = pkg.cpuAbiOverride;
- // Update version code if needed
- if (pkg.getLongVersionCode() != p.versionCode) {
- p.versionCode = pkg.getLongVersionCode();
- }
// Update signatures if needed.
- if (p.signatures.mSignatures == null) {
- p.signatures.assignSignatures(pkg.mSignatures);
- }
- // Update flags if needed.
- if (pkg.applicationInfo.flags != p.pkgFlags) {
- p.pkgFlags = pkg.applicationInfo.flags;
+ if (p.signatures.mSigningDetails.signatures == null) {
+ p.signatures.mSigningDetails = pkg.mSigningDetails;
}
// If this app defines a shared user id initialize
// the shared user signatures as well.
- if (p.sharedUser != null && p.sharedUser.signatures.mSignatures == null) {
- p.sharedUser.signatures.assignSignatures(pkg.mSignatures);
- }
- // Update static shared library dependencies if needed
- if (pkg.usesStaticLibraries != null && pkg.usesStaticLibrariesVersions != null
- && pkg.usesStaticLibraries.size() == pkg.usesStaticLibrariesVersions.length) {
- p.usesStaticLibraries = new String[pkg.usesStaticLibraries.size()];
- pkg.usesStaticLibraries.toArray(p.usesStaticLibraries);
- p.usesStaticLibrariesVersions = pkg.usesStaticLibrariesVersions;
- } else {
- p.usesStaticLibraries = null;
- p.usesStaticLibrariesVersions = null;
+ if (p.sharedUser != null && p.sharedUser.signatures.mSigningDetails.signatures == null) {
+ p.sharedUser.signatures.mSigningDetails = pkg.mSigningDetails;
}
addPackageSettingLPw(p, p.sharedUser);
}
@@ -1680,7 +1632,8 @@ public final class Settings {
null /*enabledComponents*/,
null /*disabledComponents*/,
INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED,
- 0, PackageManager.INSTALL_REASON_UNKNOWN);
+ 0, PackageManager.INSTALL_REASON_UNKNOWN,
+ null /*harmfulAppWarning*/);
}
return;
}
@@ -1755,7 +1708,8 @@ public final class Settings {
COMPONENT_ENABLED_STATE_DEFAULT);
final String enabledCaller = parser.getAttributeValue(null,
ATTR_ENABLED_CALLER);
-
+ final String harmfulAppWarning =
+ parser.getAttributeValue(null, ATTR_HARMFUL_APP_WARNING);
final int verifState = XmlUtils.readIntAttribute(parser,
ATTR_DOMAIN_VERIFICATON_STATE,
PackageManager.INTENT_FILTER_DOMAIN_VERIFICATION_STATUS_UNDEFINED);
@@ -1792,7 +1746,7 @@ public final class Settings {
ps.setUserState(userId, ceDataInode, enabled, installed, stopped, notLaunched,
hidden, suspended, instantApp, virtualPreload, enabledCaller,
enabledComponents, disabledComponents, verifState, linkGeneration,
- installReason);
+ installReason, harmfulAppWarning);
} else if (tagName.equals("preferred-activities")) {
readPreferredActivitiesLPw(parser, userId);
} else if (tagName.equals(TAG_PERSISTENT_PREFERRED_ACTIVITIES)) {
@@ -2125,6 +2079,10 @@ public final class Settings {
serializer.attribute(null, ATTR_INSTALL_REASON,
Integer.toString(ustate.installReason));
}
+ if (ustate.harmfulAppWarning != null) {
+ serializer.attribute(null, ATTR_HARMFUL_APP_WARNING,
+ ustate.harmfulAppWarning);
+ }
if (!ArrayUtils.isEmpty(ustate.enabledComponents)) {
serializer.startTag(null, TAG_ENABLED_COMPONENTS);
for (final String name : ustate.enabledComponents) {
@@ -3122,7 +3080,7 @@ public final class Settings {
ATTR_VOLUME_UUID);
final VersionInfo ver = findOrCreateVersion(volumeUuid);
ver.sdkVersion = XmlUtils.readIntAttribute(parser, ATTR_SDK_VERSION);
- ver.databaseVersion = XmlUtils.readIntAttribute(parser, ATTR_SDK_VERSION);
+ ver.databaseVersion = XmlUtils.readIntAttribute(parser, ATTR_DATABASE_VERSION);
ver.fingerprint = XmlUtils.readStringAttribute(parser, ATTR_FINGERPRINT);
} else {
Slog.w(PackageManagerService.TAG, "Unknown element under <packages>: "
@@ -4347,6 +4305,22 @@ public final class Settings {
return false;
}
+ void setHarmfulAppWarningLPw(String packageName, CharSequence warning, int userId) {
+ final PackageSetting pkgSetting = mPackages.get(packageName);
+ if (pkgSetting == null) {
+ throw new IllegalArgumentException("Unknown package: " + packageName);
+ }
+ pkgSetting.setHarmfulAppWarning(userId, warning == null ? null : warning.toString());
+ }
+
+ String getHarmfulAppWarningLPr(String packageName, int userId) {
+ final PackageSetting pkgSetting = mPackages.get(packageName);
+ if (pkgSetting == null) {
+ throw new IllegalArgumentException("Unknown package: " + packageName);
+ }
+ return pkgSetting.getHarmfulAppWarning(userId);
+ }
+
private static List<UserInfo> getAllUsers(UserManagerService userManager) {
long id = Binder.clearCallingIdentity();
try {
@@ -4493,11 +4467,14 @@ public final class Settings {
pw.print(ps.getNotLaunched(user.id) ? "l" : "L");
pw.print(ps.getInstantApp(user.id) ? "IA" : "ia");
pw.print(ps.getVirtulalPreload(user.id) ? "VPI" : "vpi");
+ String harmfulAppWarning = ps.getHarmfulAppWarning(user.id);
+ pw.print(harmfulAppWarning != null ? "HA" : "ha");
pw.print(",");
pw.print(ps.getEnabled(user.id));
String lastDisabledAppCaller = ps.getLastDisabledAppCaller(user.id);
pw.print(",");
pw.print(lastDisabledAppCaller != null ? lastDisabledAppCaller : "?");
+ pw.print(",");
pw.println();
}
return;
@@ -4565,10 +4542,8 @@ public final class Settings {
}
pw.print(prefix); pw.print(" versionName="); pw.println(ps.pkg.mVersionName);
pw.print(prefix); pw.print(" splits="); dumpSplitNames(pw, ps.pkg); pw.println();
- final int apkSigningVersion = PackageParser.getApkSigningVersion(ps.pkg);
- if (apkSigningVersion != PackageParser.APK_SIGNING_UNKNOWN) {
- pw.print(prefix); pw.print(" apkSigningVersion="); pw.println(apkSigningVersion);
- }
+ final int apkSigningVersion = ps.pkg.mSigningDetails.signatureSchemeVersion;
+ pw.print(prefix); pw.print(" apkSigningVersion="); pw.println(apkSigningVersion);
pw.print(prefix); pw.print(" applicationInfo=");
pw.println(ps.pkg.applicationInfo.toString());
pw.print(prefix); pw.print(" flags="); printFlags(pw, ps.pkg.applicationInfo.flags,
@@ -4772,6 +4747,12 @@ public final class Settings {
.getRuntimePermissionStates(user.id), dumpAll);
}
+ String harmfulAppWarning = ps.getHarmfulAppWarning(user.id);
+ if (harmfulAppWarning != null) {
+ pw.print(prefix); pw.print(" harmfulAppWarning: ");
+ pw.println(harmfulAppWarning);
+ }
+
if (permissionNames == null) {
ArraySet<String> cmp = ps.getDisabledComponents(user.id);
if (cmp != null && cmp.size() > 0) {
diff --git a/com/android/server/pm/SharedUserSetting.java b/com/android/server/pm/SharedUserSetting.java
index 877da144..24461318 100644
--- a/com/android/server/pm/SharedUserSetting.java
+++ b/com/android/server/pm/SharedUserSetting.java
@@ -17,6 +17,7 @@
package com.android.server.pm;
import android.annotation.Nullable;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageParser;
import android.service.pm.PackageServiceDumpProto;
import android.util.ArraySet;
@@ -102,4 +103,8 @@ public final class SharedUserSetting extends SettingBase {
}
return pkgList;
}
+
+ public boolean isPrivileged() {
+ return (this.pkgPrivateFlags & ApplicationInfo.PRIVATE_FLAG_PRIVILEGED) != 0;
+ }
}
diff --git a/com/android/server/pm/ShortcutPackage.java b/com/android/server/pm/ShortcutPackage.java
index 7bab3180..ebf6672c 100644
--- a/com/android/server/pm/ShortcutPackage.java
+++ b/com/android/server/pm/ShortcutPackage.java
@@ -459,8 +459,7 @@ class ShortcutPackage extends ShortcutPackageItem {
}
// Then, for the pinned set for each launcher, set the pin flag one by one.
- mShortcutUser.mService.getUserShortcutsLocked(getPackageUserId())
- .forAllLaunchers(launcherShortcuts -> {
+ mShortcutUser.forAllLaunchers(launcherShortcuts -> {
final ArraySet<String> pinned = launcherShortcuts.getPinnedShortcutIds(
getPackageName(), getPackageUserId());
diff --git a/com/android/server/pm/ShortcutService.java b/com/android/server/pm/ShortcutService.java
index 065eafd9..d2bc6d24 100644
--- a/com/android/server/pm/ShortcutService.java
+++ b/com/android/server/pm/ShortcutService.java
@@ -396,7 +396,7 @@ public class ShortcutService extends IShortcutService.Stub {
private final long[] mDurationStats = new long[Stats.COUNT];
private static final int PROCESS_STATE_FOREGROUND_THRESHOLD =
- ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+ ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
static final int OPERATION_SET = 0;
static final int OPERATION_ADD = 1;
diff --git a/com/android/server/pm/UserManagerService.java b/com/android/server/pm/UserManagerService.java
index 768eb8f3..92fd9041 100644
--- a/com/android/server/pm/UserManagerService.java
+++ b/com/android/server/pm/UserManagerService.java
@@ -27,7 +27,6 @@ import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityManagerNative;
-import android.app.AppOpsManager;
import android.app.IActivityManager;
import android.app.IStopUserCallback;
import android.app.KeyguardManager;
@@ -64,6 +63,7 @@ import android.os.SELinux;
import android.os.ServiceManager;
import android.os.ShellCallback;
import android.os.ShellCommand;
+import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.UserHandle;
import android.os.UserManager;
@@ -80,6 +80,7 @@ import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
+import android.util.SparseLongArray;
import android.util.TimeUtils;
import android.util.Xml;
@@ -242,8 +243,7 @@ public class UserManagerService extends IUserManager.Stub {
private static final IBinder mUserRestriconToken = new Binder();
/**
- * User-related information that is used for persisting to flash. Only UserInfo is
- * directly exposed to other system apps.
+ * Internal non-parcelable wrapper for UserInfo that is not exposed to other system apps.
*/
@VisibleForTesting
static class UserData {
@@ -261,6 +261,12 @@ public class UserManagerService extends IUserManager.Stub {
// Whether to perist the seed account information to be available after a boot
boolean persistSeedData;
+ /** Elapsed realtime since boot when the user started. */
+ long startRealtime;
+
+ /** Elapsed realtime since boot when the user was unlocked. */
+ long unlockRealtime;
+
void clearSeedAccountData() {
seedAccountName = null;
seedAccountType = null;
@@ -388,7 +394,7 @@ public class UserManagerService extends IUserManager.Stub {
/**
* Start an {@link IntentSender} when user is unlocked after disabling quiet mode.
*
- * @see {@link #trySetQuietModeEnabled(String, boolean, int, IntentSender)}
+ * @see {@link #requestQuietModeEnabled(String, boolean, int, IntentSender)}
*/
private class DisableQuietModeUserUnlockedCallback extends IProgressListener.Stub {
private final IntentSender mTarget;
@@ -454,6 +460,37 @@ public class UserManagerService extends IUserManager.Stub {
mUms.cleanupPartialUsers();
}
}
+
+ @Override
+ public void onStartUser(int userHandle) {
+ synchronized (mUms.mUsersLock) {
+ final UserData user = mUms.getUserDataLU(userHandle);
+ if (user != null) {
+ user.startRealtime = SystemClock.elapsedRealtime();
+ }
+ }
+ }
+
+ @Override
+ public void onUnlockUser(int userHandle) {
+ synchronized (mUms.mUsersLock) {
+ final UserData user = mUms.getUserDataLU(userHandle);
+ if (user != null) {
+ user.unlockRealtime = SystemClock.elapsedRealtime();
+ }
+ }
+ }
+
+ @Override
+ public void onStopUser(int userHandle) {
+ synchronized (mUms.mUsersLock) {
+ final UserData user = mUms.getUserDataLU(userHandle);
+ if (user != null) {
+ user.startRealtime = 0;
+ user.unlockRealtime = 0;
+ }
+ }
+ }
}
// TODO b/28848102 Add support for test dependencies injection
@@ -786,7 +823,7 @@ public class UserManagerService extends IUserManager.Stub {
}
@Override
- public boolean trySetQuietModeEnabled(@NonNull String callingPackage, boolean enableQuietMode,
+ public boolean requestQuietModeEnabled(@NonNull String callingPackage, boolean enableQuietMode,
int userHandle, @Nullable IntentSender target) {
Preconditions.checkNotNull(callingPackage);
@@ -795,12 +832,7 @@ public class UserManagerService extends IUserManager.Stub {
"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");
- }
-
+ ensureCanModifyQuietMode(callingPackage, Binder.getCallingUid(), target != null);
final long identity = Binder.clearCallingIdentity();
try {
if (enableQuietMode) {
@@ -824,35 +856,44 @@ public class UserManagerService extends IUserManager.Stub {
}
/**
- * An app can modify quiet mode if the caller meets one of the condition:
+ * The caller can modify quiet mode if it meets one of these conditions:
* <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>
+ * <p>
+ * If caller wants to start an intent after disabling the quiet mode, it must has
+ * {@link Manifest.permission#MANAGE_USERS}.
*/
- private boolean isAllowedToSetWorkMode(String callingPackage, int callingUid) {
+ private void ensureCanModifyQuietMode(String callingPackage, int callingUid,
+ boolean startIntent) {
if (hasManageUsersPermission()) {
- return true;
+ return;
+ }
+ if (startIntent) {
+ throw new SecurityException("MANAGE_USERS permission is required to start intent "
+ + "after disabling quiet mode.");
}
-
final boolean hasModifyQuietModePermission = ActivityManager.checkComponentPermission(
Manifest.permission.MODIFY_QUIET_MODE,
callingUid, -1, true) == PackageManager.PERMISSION_GRANTED;
if (hasModifyQuietModePermission) {
- return true;
+ return;
}
+ verifyCallingPackage(callingPackage, callingUid);
final ShortcutServiceInternal shortcutInternal =
LocalServices.getService(ShortcutServiceInternal.class);
if (shortcutInternal != null) {
boolean isForegroundLauncher =
shortcutInternal.isForegroundDefaultLauncher(callingPackage, callingUid);
if (isForegroundLauncher) {
- return true;
+ return;
}
}
- return false;
+ throw new SecurityException("Can't modify quiet mode, caller is neither foreground "
+ + "default launcher nor has MANAGE_USERS/MODIFY_QUIET_MODE permission");
}
private void setQuietModeEnabled(
@@ -1054,6 +1095,29 @@ public class UserManagerService extends IUserManager.Stub {
return mLocalService.isUserRunning(userId);
}
+ @Override
+ public long getUserStartRealtime() {
+ final int userId = UserHandle.getUserId(Binder.getCallingUid());
+ synchronized (mUsersLock) {
+ final UserData user = getUserDataLU(userId);
+ if (user != null) {
+ return user.startRealtime;
+ }
+ return 0;
+ }
+ }
+
+ @Override
+ public long getUserUnlockRealtime() {
+ synchronized (mUsersLock) {
+ final UserData user = getUserDataLU(UserHandle.getUserId(Binder.getCallingUid()));
+ if (user != null) {
+ return user.unlockRealtime;
+ }
+ return 0;
+ }
+ }
+
private void checkManageOrInteractPermIfCallerInOtherProfileGroup(int userId, String name) {
int callingUserId = UserHandle.getCallingUserId();
if (callingUserId == userId || isSameProfileGroupNoChecks(callingUserId, userId) ||
@@ -3481,6 +3545,7 @@ public class UserManagerService extends IUserManager.Stub {
if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return;
long now = System.currentTimeMillis();
+ final long nowRealtime = SystemClock.elapsedRealtime();
StringBuilder sb = new StringBuilder();
synchronized (mPackagesLock) {
synchronized (mUsersLock) {
@@ -3508,25 +3573,20 @@ public class UserManagerService extends IUserManager.Stub {
}
pw.println(UserState.stateToString(state));
pw.print(" Created: ");
- if (userInfo.creationTime == 0) {
- pw.println("<unknown>");
- } else {
- sb.setLength(0);
- TimeUtils.formatDuration(now - userInfo.creationTime, sb);
- sb.append(" ago");
- pw.println(sb);
- }
+ dumpTimeAgo(pw, sb, now, userInfo.creationTime);
+
pw.print(" Last logged in: ");
- if (userInfo.lastLoggedInTime == 0) {
- pw.println("<unknown>");
- } else {
- sb.setLength(0);
- TimeUtils.formatDuration(now - userInfo.lastLoggedInTime, sb);
- sb.append(" ago");
- pw.println(sb);
- }
+ dumpTimeAgo(pw, sb, now, userInfo.lastLoggedInTime);
+
pw.print(" Last logged in fingerprint: ");
pw.println(userInfo.lastLoggedInFingerprint);
+
+ pw.print(" Start time: ");
+ dumpTimeAgo(pw, sb, nowRealtime, userData.startRealtime);
+
+ pw.print(" Unlock time: ");
+ dumpTimeAgo(pw, sb, nowRealtime, userData.unlockRealtime);
+
pw.print(" Has profile owner: ");
pw.println(mIsUserManaged.get(userId));
pw.println(" Restrictions:");
@@ -3590,6 +3650,17 @@ public class UserManagerService extends IUserManager.Stub {
}
}
+ private static void dumpTimeAgo(PrintWriter pw, StringBuilder sb, long nowTime, long time) {
+ if (time == 0) {
+ pw.println("<unknown>");
+ } else {
+ sb.setLength(0);
+ TimeUtils.formatDuration(nowTime - time, sb);
+ sb.append(" ago");
+ pw.println(sb);
+ }
+ }
+
final class MainHandler extends Handler {
@Override
@@ -3932,4 +4003,16 @@ public class UserManagerService extends IUserManager.Stub {
return false;
}
}
+
+ /**
+ * Check if the calling package name matches with the calling UID, throw
+ * {@link SecurityException} if not.
+ */
+ private void verifyCallingPackage(String callingPackage, int callingUid) {
+ int packageUid = mPm.getPackageUid(callingPackage, 0, UserHandle.getUserId(callingUid));
+ if (packageUid != callingUid) {
+ throw new SecurityException("Specified package " + callingPackage
+ + " does not match the calling uid " + callingUid);
+ }
+ }
}
diff --git a/com/android/server/pm/UserRestrictionsUtils.java b/com/android/server/pm/UserRestrictionsUtils.java
index 92408432..a42fcbdd 100644
--- a/com/android/server/pm/UserRestrictionsUtils.java
+++ b/com/android/server/pm/UserRestrictionsUtils.java
@@ -25,12 +25,14 @@ import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.ContentResolver;
import android.content.Context;
+import android.content.Intent;
import android.os.Binder;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.UserManagerInternal;
+import android.provider.Settings;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.util.Log;
@@ -116,7 +118,11 @@ public class UserRestrictionsUtils {
UserManager.DISALLOW_USER_SWITCH,
UserManager.DISALLOW_UNIFIED_PASSWORD,
UserManager.DISALLOW_CONFIG_LOCATION_MODE,
- UserManager.DISALLOW_AIRPLANE_MODE
+ UserManager.DISALLOW_AIRPLANE_MODE,
+ UserManager.DISALLOW_CONFIG_BRIGHTNESS,
+ UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE,
+ UserManager.DISALLOW_AMBIENT_DISPLAY,
+ UserManager.DISALLOW_CONFIG_SCREEN_TIMEOUT
});
/**
@@ -460,7 +466,8 @@ public class UserRestrictionsUtils {
// DISALLOW_DATA_ROAMING user restriction is set.
// Multi sim device.
- SubscriptionManager subscriptionManager = new SubscriptionManager(context);
+ SubscriptionManager subscriptionManager = context
+ .getSystemService(SubscriptionManager.class);
final List<SubscriptionInfo> subscriptionInfoList =
subscriptionManager.getActiveSubscriptionInfoList();
if (subscriptionInfoList != null) {
@@ -538,6 +545,41 @@ public class UserRestrictionsUtils {
android.provider.Settings.Global.SAFE_BOOT_DISALLOWED,
newValue ? 1 : 0);
break;
+ case UserManager.DISALLOW_AIRPLANE_MODE:
+ if (newValue) {
+ final boolean airplaneMode = Settings.Global.getInt(
+ context.getContentResolver(),
+ Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
+ if (airplaneMode) {
+ android.provider.Settings.Global.putInt(
+ context.getContentResolver(),
+ android.provider.Settings.Global.AIRPLANE_MODE_ON, 0);
+ // Post the intent.
+ Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ intent.putExtra("state", 0);
+ context.sendBroadcastAsUser(intent, UserHandle.ALL);
+ }
+ }
+ break;
+ case UserManager.DISALLOW_AMBIENT_DISPLAY:
+ if (newValue) {
+ android.provider.Settings.Secure.putString(
+ context.getContentResolver(),
+ Settings.Secure.DOZE_ENABLED, "0");
+ android.provider.Settings.Secure.putString(
+ context.getContentResolver(),
+ Settings.Secure.DOZE_ALWAYS_ON, "0");
+ android.provider.Settings.Secure.putString(
+ context.getContentResolver(),
+ Settings.Secure.DOZE_PULSE_ON_PICK_UP, "0");
+ android.provider.Settings.Secure.putString(
+ context.getContentResolver(),
+ Settings.Secure.DOZE_PULSE_ON_LONG_PRESS, "0");
+ android.provider.Settings.Secure.putString(
+ context.getContentResolver(),
+ Settings.Secure.DOZE_PULSE_ON_DOUBLE_TAP, "0");
+ }
+ break;
}
} finally {
Binder.restoreCallingIdentity(id);
diff --git a/com/android/server/pm/dex/ArtManagerService.java b/com/android/server/pm/dex/ArtManagerService.java
index 2dbb34d8..81786890 100644
--- a/com/android/server/pm/dex/ArtManagerService.java
+++ b/com/android/server/pm/dex/ArtManagerService.java
@@ -17,9 +17,13 @@
package com.android.server.pm.dex;
import android.Manifest;
+import android.annotation.UserIdInt;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
+import android.content.pm.PackageParser;
import android.content.pm.dex.ArtManager;
+import android.content.pm.dex.DexMetadataHelper;
import android.os.Binder;
import android.os.Environment;
import android.os.Handler;
@@ -29,10 +33,12 @@ import android.content.pm.IPackageManager;
import android.content.pm.dex.ISnapshotRuntimeProfileCallback;
import android.os.SystemProperties;
import android.os.UserHandle;
+import android.util.ArrayMap;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.BackgroundThread;
+import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
import com.android.server.pm.Installer;
import com.android.server.pm.Installer.InstallerException;
@@ -230,4 +236,69 @@ public class ArtManagerService extends android.content.pm.dex.IArtManager.Stub {
// Should not happen.
}
}
+
+ /**
+ * Prepare the application profiles.
+ * For all code paths:
+ * - create the current primary profile to save time at app startup time.
+ * - copy the profiles from the associated dex metadata file to the reference profile.
+ */
+ public void prepareAppProfiles(PackageParser.Package pkg, @UserIdInt int user) {
+ final int appId = UserHandle.getAppId(pkg.applicationInfo.uid);
+ if (user < 0) {
+ Slog.wtf(TAG, "Invalid user id: " + user);
+ return;
+ }
+ if (appId < 0) {
+ Slog.wtf(TAG, "Invalid app id: " + appId);
+ return;
+ }
+ try {
+ ArrayMap<String, String> codePathsProfileNames = getPackageProfileNames(pkg);
+ for (int i = codePathsProfileNames.size() - 1; i >= 0; i--) {
+ String codePath = codePathsProfileNames.keyAt(i);
+ String profileName = codePathsProfileNames.valueAt(i);
+ File dexMetadata = DexMetadataHelper.findDexMetadataForFile(new File(codePath));
+ String dexMetadataPath = dexMetadata == null ? null : dexMetadata.getAbsolutePath();
+ synchronized (mInstaller) {
+ boolean result = mInstaller.prepareAppProfile(pkg.packageName, user, appId,
+ profileName, codePath, dexMetadataPath);
+ if (!result) {
+ Slog.e(TAG, "Failed to prepare profile for " +
+ pkg.packageName + ":" + codePath);
+ }
+ }
+ }
+ } catch (InstallerException e) {
+ Slog.e(TAG, "Failed to prepare profile for " + pkg.packageName, e);
+ }
+ }
+
+ /**
+ * Prepares the app profiles for a set of users. {@see ArtManagerService#prepareAppProfiles}.
+ */
+ public void prepareAppProfiles(PackageParser.Package pkg, int[] user) {
+ for (int i = 0; i < user.length; i++) {
+ prepareAppProfiles(pkg, user[i]);
+ }
+ }
+
+ /**
+ * Build the profiles names for all the package code paths (excluding resource only paths).
+ * Return the map [code path -> profile name].
+ */
+ private ArrayMap<String, String> getPackageProfileNames(PackageParser.Package pkg) {
+ ArrayMap<String, String> result = new ArrayMap<>();
+ if ((pkg.applicationInfo.flags & ApplicationInfo.FLAG_HAS_CODE) != 0) {
+ result.put(pkg.baseCodePath, ArtManager.getProfileName(null));
+ }
+ if (!ArrayUtils.isEmpty(pkg.splitCodePaths)) {
+ for (int i = 0; i < pkg.splitCodePaths.length; i++) {
+ if ((pkg.splitFlags[i] & ApplicationInfo.FLAG_HAS_CODE) != 0) {
+ result.put(pkg.splitCodePaths[i], ArtManager.getProfileName(pkg.splitNames[i]));
+ }
+ }
+ }
+ return result;
+ }
}
diff --git a/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
index d38dc9ae..6e07eaac 100644
--- a/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
+++ b/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java
@@ -27,7 +27,6 @@ import android.app.admin.DevicePolicyManager;
import android.companion.CompanionDeviceManager;
import android.content.Context;
import android.content.Intent;
-import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageList;
@@ -62,8 +61,6 @@ import android.util.Slog;
import android.util.Xml;
import com.android.internal.util.XmlUtils;
import com.android.server.LocalServices;
-import com.android.server.pm.PackageManagerService;
-import com.android.server.pm.PackageSetting;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
@@ -135,6 +132,11 @@ public final class DefaultPermissionGrantPolicy {
LOCATION_PERMISSIONS.add(Manifest.permission.ACCESS_COARSE_LOCATION);
}
+ private static final Set<String> COARSE_LOCATION_PERMISSIONS = new ArraySet<>();
+ static {
+ COARSE_LOCATION_PERMISSIONS.add(Manifest.permission.ACCESS_COARSE_LOCATION);
+ }
+
private static final Set<String> CALENDAR_PERMISSIONS = new ArraySet<>();
static {
CALENDAR_PERMISSIONS.add(Manifest.permission.READ_CALENDAR);
@@ -183,6 +185,7 @@ public final class DefaultPermissionGrantPolicy {
private PackagesProvider mSmsAppPackagesProvider;
private PackagesProvider mDialerAppPackagesProvider;
private PackagesProvider mSimCallManagerPackagesProvider;
+ private PackagesProvider mUseOpenWifiAppPackagesProvider;
private SyncAdapterPackagesProvider mSyncAdapterPackagesProvider;
private ArrayMap<String, List<DefaultPermissionGrant>> mGrantExceptions;
@@ -247,6 +250,12 @@ public final class DefaultPermissionGrantPolicy {
}
}
+ public void setUseOpenWifiAppPackagesProvider(PackagesProvider provider) {
+ synchronized (mLock) {
+ mUseOpenWifiAppPackagesProvider = provider;
+ }
+ }
+
public void setSyncAdapterPackagesProvider(SyncAdapterPackagesProvider provider) {
synchronized (mLock) {
mSyncAdapterPackagesProvider = provider;
@@ -320,6 +329,7 @@ public final class DefaultPermissionGrantPolicy {
final PackagesProvider smsAppPackagesProvider;
final PackagesProvider dialerAppPackagesProvider;
final PackagesProvider simCallManagerPackagesProvider;
+ final PackagesProvider useOpenWifiAppPackagesProvider;
final SyncAdapterPackagesProvider syncAdapterPackagesProvider;
synchronized (mLock) {
@@ -328,6 +338,7 @@ public final class DefaultPermissionGrantPolicy {
smsAppPackagesProvider = mSmsAppPackagesProvider;
dialerAppPackagesProvider = mDialerAppPackagesProvider;
simCallManagerPackagesProvider = mSimCallManagerPackagesProvider;
+ useOpenWifiAppPackagesProvider = mUseOpenWifiAppPackagesProvider;
syncAdapterPackagesProvider = mSyncAdapterPackagesProvider;
}
@@ -341,6 +352,8 @@ public final class DefaultPermissionGrantPolicy {
? dialerAppPackagesProvider.getPackages(userId) : null;
String[] simCallManagerPackageNames = (simCallManagerPackagesProvider != null)
? simCallManagerPackagesProvider.getPackages(userId) : null;
+ String[] useOpenWifiAppPackageNames = (useOpenWifiAppPackagesProvider != null)
+ ? useOpenWifiAppPackagesProvider.getPackages(userId) : null;
String[] contactsSyncAdapterPackages = (syncAdapterPackagesProvider != null) ?
syncAdapterPackagesProvider.getPackages(ContactsContract.AUTHORITY, userId) : null;
String[] calendarSyncAdapterPackages = (syncAdapterPackagesProvider != null) ?
@@ -458,6 +471,18 @@ public final class DefaultPermissionGrantPolicy {
}
}
+ // Use Open Wifi
+ if (useOpenWifiAppPackageNames != null) {
+ for (String useOpenWifiPackageName : useOpenWifiAppPackageNames) {
+ PackageParser.Package useOpenWifiPackage =
+ getSystemPackage(useOpenWifiPackageName);
+ if (useOpenWifiPackage != null) {
+ grantDefaultPermissionsToDefaultSystemUseOpenWifiApp(useOpenWifiPackage,
+ userId);
+ }
+ }
+ }
+
// SMS
if (smsAppPackageNames == null) {
Intent smsIntent = new Intent(Intent.ACTION_MAIN);
@@ -827,6 +852,13 @@ public final class DefaultPermissionGrantPolicy {
}
}
+ private void grantDefaultPermissionsToDefaultSystemUseOpenWifiApp(
+ PackageParser.Package useOpenWifiPackage, int userId) {
+ if (doesPackageSupportRuntimePermissions(useOpenWifiPackage)) {
+ grantRuntimePermissions(useOpenWifiPackage, COARSE_LOCATION_PERMISSIONS, userId);
+ }
+ }
+
public void grantDefaultPermissionsToDefaultSmsApp(String packageName, int userId) {
Log.i(TAG, "Granting permissions to default sms app for user:" + userId);
if (packageName == null) {
@@ -859,6 +891,19 @@ public final class DefaultPermissionGrantPolicy {
}
}
+ public void grantDefaultPermissionsToDefaultUseOpenWifiApp(String packageName, int userId) {
+ Log.i(TAG, "Granting permissions to default Use Open WiFi app for user:" + userId);
+ if (packageName == null) {
+ return;
+ }
+ PackageParser.Package useOpenWifiPackage = getPackage(packageName);
+ if (useOpenWifiPackage != null
+ && doesPackageSupportRuntimePermissions(useOpenWifiPackage)) {
+ grantRuntimePermissions(
+ useOpenWifiPackage, COARSE_LOCATION_PERMISSIONS, false, true, userId);
+ }
+ }
+
private void grantDefaultPermissionsToDefaultSimCallManager(
PackageParser.Package simCallManagerPackage, int userId) {
Log.i(TAG, "Granting permissions to sim call manager for user:" + userId);
@@ -1014,7 +1059,7 @@ public final class DefaultPermissionGrantPolicy {
}
private void grantRuntimePermissions(PackageParser.Package pkg, Set<String> permissions,
- boolean systemFixed, boolean isDefaultPhoneOrSms, int userId) {
+ boolean systemFixed, boolean ignoreSystemPackage, int userId) {
if (pkg.requestedPermissions.isEmpty()) {
return;
}
@@ -1022,13 +1067,13 @@ public final class DefaultPermissionGrantPolicy {
List<String> requestedPermissions = pkg.requestedPermissions;
Set<String> grantablePermissions = null;
- // If this is the default Phone or SMS app we grant permissions regardless
- // whether the version on the system image declares the permission as used since
- // selecting the app as the default Phone or SMS the user makes a deliberate
+ // In some cases, like for the Phone or SMS app, we grant permissions regardless
+ // of if the version on the system image declares the permission as used since
+ // selecting the app as the default for that function the user makes a deliberate
// choice to grant this app the permissions needed to function. For all other
// apps, (default grants on first boot and user creation) we don't grant default
// permissions if the version on the system image does not declare them.
- if (!isDefaultPhoneOrSms && pkg.isUpdatedSystemApp()) {
+ if (!ignoreSystemPackage && pkg.isUpdatedSystemApp()) {
final PackageParser.Package disabledPkg =
mServiceInternal.getDisabledPackage(pkg.packageName);
if (disabledPkg != null) {
@@ -1062,7 +1107,7 @@ public final class DefaultPermissionGrantPolicy {
// Unless the caller wants to override user choices. The override is
// to make sure we can grant the needed permission to the default
// sms and phone apps after the user chooses this in the UI.
- if (flags == 0 || isDefaultPhoneOrSms) {
+ if (flags == 0 || ignoreSystemPackage) {
// Never clobber policy or system.
final int fixedFlags = PackageManager.FLAG_PERMISSION_SYSTEM_FIXED
| PackageManager.FLAG_PERMISSION_POLICY_FIXED;
@@ -1121,7 +1166,8 @@ public final class DefaultPermissionGrantPolicy {
final String systemPackageName = mServiceInternal.getKnownPackageName(
PackageManagerInternal.PACKAGE_SYSTEM, UserHandle.USER_SYSTEM);
final PackageParser.Package systemPackage = getPackage(systemPackageName);
- return compareSignatures(systemPackage.mSignatures, pkg.mSignatures)
+ return compareSignatures(systemPackage.mSigningDetails.signatures,
+ pkg.mSigningDetails.signatures)
== PackageManager.SIGNATURE_MATCH;
}
diff --git a/com/android/server/pm/permission/PermissionManagerService.java b/com/android/server/pm/permission/PermissionManagerService.java
index 90ac4ab7..786b9988 100644
--- a/com/android/server/pm/permission/PermissionManagerService.java
+++ b/com/android/server/pm/permission/PermissionManagerService.java
@@ -29,7 +29,6 @@ import static android.os.Trace.TRACE_TAG_PACKAGE_MANAGER;
import android.Manifest;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
@@ -56,21 +55,17 @@ import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
-import com.android.internal.R;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.os.RoSystemProperties;
import com.android.internal.util.ArrayUtils;
-import com.android.server.FgThread;
import com.android.server.LocalServices;
import com.android.server.ServiceThread;
import com.android.server.SystemConfig;
import com.android.server.Watchdog;
-import com.android.server.pm.PackageManagerService;
import com.android.server.pm.PackageManagerServiceUtils;
import com.android.server.pm.PackageSetting;
-import com.android.server.pm.ProcessLoggingHandler;
import com.android.server.pm.SharedUserSetting;
import com.android.server.pm.UserManagerService;
import com.android.server.pm.permission.DefaultPermissionGrantPolicy.DefaultPermissionGrantedCallback;
@@ -1015,10 +1010,10 @@ Slog.e(TAG, "TODD: Packages: " + Arrays.toString(packages));
final PackageParser.Package systemPackage =
mPackageManagerInt.getPackage(systemPackageName);
boolean allowed = (PackageManagerServiceUtils.compareSignatures(
- bp.getSourceSignatures(), pkg.mSignatures)
+ bp.getSourceSignatures(), pkg.mSigningDetails.signatures)
== PackageManager.SIGNATURE_MATCH)
|| (PackageManagerServiceUtils.compareSignatures(
- systemPackage.mSignatures, pkg.mSignatures)
+ systemPackage.mSigningDetails.signatures, pkg.mSigningDetails.signatures)
== PackageManager.SIGNATURE_MATCH);
if (!allowed && (privilegedPermission || oemPermission)) {
if (pkg.isSystem()) {
diff --git a/com/android/server/policy/BarController.java b/com/android/server/policy/BarController.java
index 10d9565c..c906705a 100644
--- a/com/android/server/policy/BarController.java
+++ b/com/android/server/policy/BarController.java
@@ -16,13 +16,16 @@
package com.android.server.policy;
+import static com.android.server.wm.proto.BarControllerProto.STATE;
+import static com.android.server.wm.proto.BarControllerProto.TRANSIENT_STATE;
+
import android.app.StatusBarManager;
import android.os.Handler;
import android.os.Message;
import android.os.SystemClock;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import android.view.View;
-import android.view.ViewGroup;
import android.view.WindowManager;
import com.android.server.LocalServices;
@@ -311,6 +314,13 @@ public class BarController {
throw new IllegalArgumentException("Unknown state " + state);
}
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(STATE, mState);
+ proto.write(TRANSIENT_STATE, mTransientBarState);
+ proto.end(token);
+ }
+
public void dump(PrintWriter pw, String prefix) {
if (mWin != null) {
pw.print(prefix); pw.println(mTag);
diff --git a/com/android/server/policy/PhoneWindowManager.java b/com/android/server/policy/PhoneWindowManager.java
index 076c0e4d..0f394a4e 100644
--- a/com/android/server/policy/PhoneWindowManager.java
+++ b/com/android/server/policy/PhoneWindowManager.java
@@ -48,7 +48,6 @@ 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;
@@ -65,6 +64,8 @@ import static android.view.WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW;
import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
import static android.view.WindowManager.LayoutParams.LAST_SYSTEM_WINDOW;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT;
import static android.view.WindowManager.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_ACQUIRES_SLEEP_TOKEN;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND;
@@ -127,6 +128,26 @@ import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.C
import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.LID_ABSENT;
import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.LID_CLOSED;
import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.LID_OPEN;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.FOCUSED_APP_TOKEN;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.FOCUSED_WINDOW;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.FORCE_STATUS_BAR;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.FORCE_STATUS_BAR_FROM_KEYGUARD;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.KEYGUARD_DELEGATE;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.KEYGUARD_DRAW_COMPLETE;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.KEYGUARD_OCCLUDED;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.KEYGUARD_OCCLUDED_CHANGED;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.KEYGUARD_OCCLUDED_PENDING;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.LAST_SYSTEM_UI_FLAGS;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.NAVIGATION_BAR;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.ORIENTATION;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.ORIENTATION_LISTENER;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.ROTATION;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.ROTATION_MODE;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.SCREEN_ON_FULLY;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.STATUS_BAR;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.TOP_FULLSCREEN_OPAQUE_OR_DIMMING_WINDOW;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.TOP_FULLSCREEN_OPAQUE_WINDOW;
+import static com.android.server.wm.proto.WindowManagerPolicyProto.WINDOW_MANAGER_DRAW_COMPLETE;
import android.annotation.Nullable;
import android.app.ActivityManager;
@@ -141,12 +162,10 @@ import android.app.StatusBarManager;
import android.app.UiModeManager;
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.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@@ -172,7 +191,6 @@ import android.media.AudioSystem;
import android.media.IAudioService;
import android.media.session.MediaSessionLegacyHelper;
import android.os.Binder;
-import android.os.Build;
import android.os.Bundle;
import android.os.FactoryTest;
import android.os.Handler;
@@ -180,7 +198,6 @@ import android.os.IBinder;
import android.os.IDeviceIdleController;
import android.os.Looper;
import android.os.Message;
-import android.os.Messenger;
import android.os.PowerManager;
import android.os.PowerManagerInternal;
import android.os.Process;
@@ -247,9 +264,11 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.policy.IKeyguardDismissCallback;
import com.android.internal.policy.IShortcutService;
+import com.android.internal.policy.KeyguardDismissCallback;
import com.android.internal.policy.PhoneWindow;
import com.android.internal.statusbar.IStatusBarService;
import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.ScreenshotHelper;
import com.android.internal.util.ScreenShapeHelper;
import com.android.internal.widget.PointerLocationView;
import com.android.server.GestureLauncherService;
@@ -441,6 +460,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
AccessibilityManager mAccessibilityManager;
BurnInProtectionHelper mBurnInProtectionHelper;
AppOpsManager mAppOpsManager;
+ private ScreenshotHelper mScreenshotHelper;
private boolean mHasFeatureWatch;
private boolean mHasFeatureLeanback;
@@ -469,6 +489,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
WindowState mNavigationBar = null;
boolean mHasNavigationBar = false;
boolean mNavigationBarCanMove = false; // can the navigation bar ever move to the side?
+ @NavigationBarPosition
int mNavigationBarPosition = NAV_BAR_BOTTOM;
int[] mNavigationBarHeightForRotationDefault = new int[4];
int[] mNavigationBarWidthForRotationDefault = new int[4];
@@ -601,8 +622,6 @@ 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.
@@ -698,6 +717,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// Behavior of Back button while in-call and screen on
int mIncallBackBehavior;
+ // Behavior of rotation suggestions. (See Settings.Secure.SHOW_ROTATION_SUGGESTION)
+ int mShowRotationSuggestions;
+
Display mDisplay;
int mLandscapeRotation = 0; // default landscape rotation
@@ -823,7 +845,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
dispatchMediaKeyRepeatWithWakeLock((KeyEvent)msg.obj);
break;
case MSG_DISPATCH_SHOW_RECENTS:
- showRecentApps(false, msg.arg1 != 0);
+ showRecentApps(false);
break;
case MSG_DISPATCH_SHOW_GLOBAL_ACTIONS:
showGlobalActionsInternal();
@@ -952,11 +974,11 @@ public class PhoneWindowManager implements WindowManagerPolicy {
resolver.registerContentObserver(Settings.Secure.getUriFor(
Settings.Secure.IMMERSIVE_MODE_CONFIRMATIONS), false, this,
UserHandle.USER_ALL);
- resolver.registerContentObserver(Settings.Global.getUriFor(
- Settings.Global.POLICY_CONTROL), false, this,
+ resolver.registerContentObserver(Settings.Secure.getUriFor(
+ Settings.Secure.SHOW_ROTATION_SUGGESTIONS), false, this,
UserHandle.USER_ALL);
resolver.registerContentObserver(Settings.Global.getUriFor(
- Settings.Global.EMULATE_DISPLAY_CUTOUT), false, this,
+ Settings.Global.POLICY_CONTROL), false, this,
UserHandle.USER_ALL);
updateSettings();
}
@@ -985,23 +1007,43 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
class MyOrientationListener extends WindowOrientationListener {
- private final Runnable mUpdateRotationRunnable = new Runnable() {
+
+ private SparseArray<Runnable> mRunnableCache;
+
+ MyOrientationListener(Context context, Handler handler) {
+ super(context, handler);
+ mRunnableCache = new SparseArray<>(5);
+ }
+
+ private class UpdateRunnable implements Runnable {
+ private final int mRotation;
+ UpdateRunnable(int rotation) {
+ mRotation = rotation;
+ }
+
@Override
public void run() {
// send interaction hint to improve redraw performance
mPowerManagerInternal.powerHint(PowerHint.INTERACTION, 0);
- updateRotation(false);
+ if (isRotationChoiceEnabled()) {
+ final boolean isValid = isValidRotationChoice(mCurrentAppOrientation,
+ mRotation);
+ sendProposedRotationChangeToStatusBarInternal(mRotation, isValid);
+ } else {
+ updateRotation(false);
+ }
}
- };
-
- MyOrientationListener(Context context, Handler handler) {
- super(context, handler);
}
@Override
public void onProposedRotationChanged(int rotation) {
if (localLOGV) Slog.v(TAG, "onProposedRotationChanged, rotation=" + rotation);
- mHandler.post(mUpdateRotationRunnable);
+ Runnable r = mRunnableCache.get(rotation, null);
+ if (r == null){
+ r = new UpdateRunnable(rotation);
+ mRunnableCache.put(rotation, r);
+ }
+ mHandler.post(r);
}
}
MyOrientationListener mOrientationListener;
@@ -1106,7 +1148,11 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// orientation for a little bit, which can cause orientation
// changes to lag, so we'd like to keep it always on. (It will
// still be turned off when the screen is off.)
- return false;
+
+ // When locked we can provide rotation suggestions users can approve to change the
+ // current screen rotation. To do this the sensor needs to be running.
+ return mSupportAutoRotation &&
+ mShowRotationSuggestions == Settings.Secure.SHOW_ROTATION_SUGGESTIONS_ENABLED;
}
return mSupportAutoRotation;
}
@@ -1665,7 +1711,9 @@ public class PhoneWindowManager implements WindowManagerPolicy {
@Override
public void run() {
- takeScreenshot(mScreenshotType);
+ mScreenshotHelper.takeScreenshot(mScreenshotType,
+ mStatusBar != null && mStatusBar.isVisibleLw(),
+ mNavigationBar != null && mNavigationBar.isVisibleLw(), mHandler);
}
}
@@ -2143,6 +2191,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mWindowManagerFuncs.notifyKeyguardTrustedChanged();
}
});
+ mScreenshotHelper = new ScreenshotHelper(mContext);
}
/**
@@ -2255,9 +2304,13 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// http://developer.android.com/guide/practices/screens_support.html#range
// For car, ignore the dp limitation. It's physically impossible to rotate the car's screen
// so if the orientation is forced, we need to respect that no matter what.
- boolean isCar = mContext.getPackageManager().hasSystemFeature(
+ final boolean isCar = mContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_AUTOMOTIVE);
- mForceDefaultOrientation = ((longSizeDp >= 960 && shortSizeDp >= 720) || isCar) &&
+ // For TV, it's usually 960dp x 540dp, ignore the size limitation.
+ // so if the orientation is forced, we need to respect that no matter what.
+ final boolean isTv = mContext.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_LEANBACK);
+ mForceDefaultOrientation = ((longSizeDp >= 960 && shortSizeDp >= 720) || isCar || isTv) &&
res.getBoolean(com.android.internal.R.bool.config_forceDefaultOrientation) &&
// For debug purposes the next line turns this feature off with:
// $ adb shell setprop config.override_forced_orient true
@@ -2295,6 +2348,16 @@ public class PhoneWindowManager implements WindowManagerPolicy {
Settings.Secure.INCALL_BACK_BUTTON_BEHAVIOR_DEFAULT,
UserHandle.USER_CURRENT);
+ // Configure rotation suggestions.
+ int showRotationSuggestions = Settings.Secure.getIntForUser(resolver,
+ Settings.Secure.SHOW_ROTATION_SUGGESTIONS,
+ Settings.Secure.SHOW_ROTATION_SUGGESTIONS_DEFAULT,
+ UserHandle.USER_CURRENT);
+ if (mShowRotationSuggestions != showRotationSuggestions) {
+ mShowRotationSuggestions = showRotationSuggestions;
+ updateOrientationListenerLp(); // Enable, disable the orientation listener
+ }
+
// Configure wake gesture.
boolean wakeGestureEnabledSetting = Settings.Secure.getIntForUser(resolver,
Settings.Secure.WAKE_GESTURE_ENABLED, 0,
@@ -2344,10 +2407,6 @@ 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);
@@ -2683,6 +2742,11 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
@Override
+ public void onOverlayChangedLw() {
+ onConfigurationChanged();
+ }
+
+ @Override
public void onConfigurationChanged() {
// TODO(multi-display): Define policy for secondary displays.
Context uiContext = getSystemUiContext();
@@ -3750,7 +3814,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
final int shiftlessModifiers = event.getModifiers() & ~KeyEvent.META_SHIFT_MASK;
if (KeyEvent.metaStateHasModifiers(shiftlessModifiers, KeyEvent.META_ALT_ON)) {
mRecentAppsHeldModifiers = shiftlessModifiers;
- showRecentApps(true, false);
+ showRecentApps(true);
return -1;
}
}
@@ -4097,16 +4161,16 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
@Override
- public void showRecentApps(boolean fromHome) {
+ public void showRecentApps() {
mHandler.removeMessages(MSG_DISPATCH_SHOW_RECENTS);
- mHandler.obtainMessage(MSG_DISPATCH_SHOW_RECENTS, fromHome ? 1 : 0, 0).sendToTarget();
+ mHandler.obtainMessage(MSG_DISPATCH_SHOW_RECENTS).sendToTarget();
}
- private void showRecentApps(boolean triggeredFromAltTab, boolean fromHome) {
+ private void showRecentApps(boolean triggeredFromAltTab) {
mPreloadedRecentApps = false; // preloading no longer needs to be canceled
StatusBarManagerInternal statusbar = getStatusBarManagerInternal();
if (statusbar != null) {
- statusbar.showRecentApps(triggeredFromAltTab, fromHome);
+ statusbar.showRecentApps(triggeredFromAltTab);
}
}
@@ -4145,20 +4209,23 @@ public class PhoneWindowManager implements WindowManagerPolicy {
if (isKeyguardShowingAndNotOccluded()) {
// don't launch home if keyguard showing
return;
- }
-
- if (!mKeyguardOccluded && mKeyguardDelegate.isInputRestricted()) {
+ } else if (mKeyguardOccluded && mKeyguardDelegate.isShowing()) {
+ mKeyguardDelegate.dismiss(new KeyguardDismissCallback() {
+ @Override
+ public void onDismissSucceeded() throws RemoteException {
+ mHandler.post(() -> {
+ startDockOrHome(true /*fromHomeKey*/, awakenFromDreams);
+ });
+ }
+ }, null /* message */);
+ return;
+ } else if (!mKeyguardOccluded && mKeyguardDelegate.isInputRestricted()) {
// when in keyguard restricted mode, must first verify unlock
// before launching home
mKeyguardDelegate.verifyUnlock(new OnKeyguardExitResult() {
@Override
public void onKeyguardExitResult(boolean success) {
if (success) {
- try {
- ActivityManager.getService().stopAppSwitches();
- } catch (RemoteException e) {
- }
- sendCloseSystemWindows(SYSTEM_DIALOG_REASON_HOME_KEY);
startDockOrHome(true /*fromHomeKey*/, awakenFromDreams);
}
}
@@ -4168,11 +4235,11 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
// no keyguard stuff to worry about, just launch home!
- try {
- ActivityManager.getService().stopAppSwitches();
- } catch (RemoteException e) {
- }
if (mRecentsVisible) {
+ try {
+ ActivityManager.getService().stopAppSwitches();
+ } catch (RemoteException e) {}
+
// Hide Recents and notify it to launch Home
if (awakenFromDreams) {
awakenDreams();
@@ -4180,7 +4247,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
hideRecentApps(false, true);
} else {
// Otherwise, just launch Home
- sendCloseSystemWindows(SYSTEM_DIALOG_REASON_HOME_KEY);
startDockOrHome(true /*fromHomeKey*/, awakenFromDreams);
}
}
@@ -4286,6 +4352,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
DisplayFrames displayFrames, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, DisplayCutout.ParcelableWrapper outDisplayCutout) {
final int fl = PolicyControl.getWindowFlags(null, attrs);
+ final int pfl = attrs.privateFlags;
final int sysuiVis = PolicyControl.getSystemUiVisibility(null, attrs);
final int systemUiVisibility = (sysuiVis | attrs.subtreeSystemUiVisibility);
final int displayRotation = displayFrames.mRotation;
@@ -4308,8 +4375,12 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
- if ((fl & (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR))
- == (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR)) {
+ final boolean layoutInScreenAndInsetDecor =
+ (fl & (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR))
+ == (FLAG_LAYOUT_IN_SCREEN | FLAG_LAYOUT_INSET_DECOR);
+ final boolean screenDecor = (pfl & PRIVATE_FLAG_IS_SCREEN_DECOR) != 0;
+
+ if (layoutInScreenAndInsetDecor && !screenDecor) {
Rect frame;
int availRight, availBottom;
if (canHideNavigationBar() &&
@@ -4382,7 +4453,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
/** {@inheritDoc} */
@Override
public void beginLayoutLw(DisplayFrames displayFrames, int uiMode) {
- displayFrames.onBeginLayout(mEmulateDisplayCutout, mStatusBarHeight);
+ displayFrames.onBeginLayout();
// TODO(multi-display): This doesn't seem right...Maybe only apply to default display?
mSystemGestures.screenWidth = displayFrames.mUnrestricted.width();
mSystemGestures.screenHeight = displayFrames.mUnrestricted.height();
@@ -4429,7 +4500,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mHandler.obtainMessage(MSG_DISPOSE_INPUT_CONSUMER, mInputConsumer));
mInputConsumer = null;
}
- } else if (mInputConsumer == null) {
+ } else if (mInputConsumer == null && mStatusBar != null && canHideNavigationBar()) {
mInputConsumer = mWindowManagerFuncs.createInputConsumer(mHandler.getLooper(),
INPUT_CONSUMER_NAVIGATION,
(channel, looper) -> new HideNavInputEventReceiver(channel, looper));
@@ -4564,6 +4635,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
dockFrame.top = displayFrames.mStable.top;
displayFrames.mContent.set(dockFrame);
displayFrames.mVoiceContent.set(dockFrame);
+ displayFrames.mCurrent.set(dockFrame);
if (DEBUG_LAYOUT) Slog.v(TAG, "Status bar: " + String.format(
"dock=%s content=%s cur=%s", dockFrame.toString(),
@@ -4684,6 +4756,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
return mNavigationBarController.checkHiddenLw();
}
+ @NavigationBarPosition
private int navigationBarPosition(int displayWidth, int displayHeight, int displayRotation) {
if (mNavigationBarCanMove && displayWidth > displayHeight) {
if (displayRotation == Surface.ROTATION_270) {
@@ -4801,7 +4874,6 @@ 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);
@@ -4823,12 +4895,10 @@ 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;
+ || (requestedSysUiFl & View.SYSTEM_UI_FLAG_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);
@@ -4988,9 +5058,6 @@ 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);
@@ -5076,9 +5143,6 @@ 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);
@@ -5153,15 +5217,18 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
}
- // Ensure that windows that did not request to be laid out in the cutout don't get laid
- // out there.
- if (!layoutInCutout) {
+ final int cutoutMode = attrs.layoutInDisplayCutoutMode;
+ // Ensure that windows with a DEFAULT or NEVER display cutout mode are laid out in
+ // the cutout safe zone.
+ if (cutoutMode != LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) {
final Rect displayCutoutSafeExceptMaybeTop = mTmpRect;
displayCutoutSafeExceptMaybeTop.set(displayFrames.mDisplayCutoutSafe);
- if (layoutInScreen && layoutInsetDecor) {
+ if (layoutInScreen && layoutInsetDecor && !requestedFullscreen
+ && cutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT) {
// 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.
+ // LAYOUT_IN_SCREEN | LAYOUT_INSET_DECOR but not FULLSCREEN
+ // 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);
@@ -5590,11 +5657,7 @@ public class PhoneWindowManager implements WindowManagerPolicy {
@Override
public boolean allowAppAnimationsLw() {
- if (mShowingDream) {
- // If keyguard or dreams is currently visible, no reason to animate behind it.
- return false;
- }
- return true;
+ return !mShowingDream;
}
@Override
@@ -5706,100 +5769,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
setHdmiPlugged(!mHdmiPlugged);
}
- final Object mScreenshotLock = new Object();
- ServiceConnection mScreenshotConnection = null;
-
- final Runnable mScreenshotTimeout = new Runnable() {
- @Override public void run() {
- synchronized (mScreenshotLock) {
- if (mScreenshotConnection != null) {
- mContext.unbindService(mScreenshotConnection);
- mScreenshotConnection = null;
- notifyScreenshotError();
- }
- }
- }
- };
-
- // Assume this is called from the Handler thread.
- private void takeScreenshot(final int screenshotType) {
- synchronized (mScreenshotLock) {
- if (mScreenshotConnection != null) {
- return;
- }
- final ComponentName serviceComponent = new ComponentName(SYSUI_PACKAGE,
- SYSUI_SCREENSHOT_SERVICE);
- final Intent serviceIntent = new Intent();
- serviceIntent.setComponent(serviceComponent);
- ServiceConnection conn = new ServiceConnection() {
- @Override
- public void onServiceConnected(ComponentName name, IBinder service) {
- synchronized (mScreenshotLock) {
- if (mScreenshotConnection != this) {
- return;
- }
- Messenger messenger = new Messenger(service);
- Message msg = Message.obtain(null, screenshotType);
- final ServiceConnection myConn = this;
- Handler h = new Handler(mHandler.getLooper()) {
- @Override
- public void handleMessage(Message msg) {
- synchronized (mScreenshotLock) {
- if (mScreenshotConnection == myConn) {
- mContext.unbindService(mScreenshotConnection);
- mScreenshotConnection = null;
- mHandler.removeCallbacks(mScreenshotTimeout);
- }
- }
- }
- };
- msg.replyTo = new Messenger(h);
- msg.arg1 = msg.arg2 = 0;
- if (mStatusBar != null && mStatusBar.isVisibleLw())
- msg.arg1 = 1;
- if (mNavigationBar != null && mNavigationBar.isVisibleLw())
- msg.arg2 = 1;
- try {
- messenger.send(msg);
- } catch (RemoteException e) {
- }
- }
- }
-
- @Override
- public void onServiceDisconnected(ComponentName name) {
- synchronized (mScreenshotLock) {
- if (mScreenshotConnection != null) {
- mContext.unbindService(mScreenshotConnection);
- mScreenshotConnection = null;
- mHandler.removeCallbacks(mScreenshotTimeout);
- notifyScreenshotError();
- }
- }
- }
- };
- if (mContext.bindServiceAsUser(serviceIntent, conn,
- Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE,
- UserHandle.CURRENT)) {
- mScreenshotConnection = conn;
- mHandler.postDelayed(mScreenshotTimeout, 10000);
- }
- }
- }
-
- /**
- * Notifies the screenshot service to show an error.
- */
- private void notifyScreenshotError() {
- // If the service process is killed, then ask it to clean up after itself
- final ComponentName errorComponent = new ComponentName(SYSUI_PACKAGE,
- SYSUI_SCREENSHOT_ERROR_RECEIVER);
- Intent errorIntent = new Intent(Intent.ACTION_USER_PRESENT);
- errorIntent.setComponent(errorComponent);
- errorIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT |
- Intent.FLAG_RECEIVER_FOREGROUND);
- mContext.sendBroadcastAsUser(errorIntent, UserHandle.CURRENT);
- }
/** {@inheritDoc} */
@Override
@@ -6224,6 +6193,16 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
/**
+ * Notify the StatusBar that system rotation suggestion has changed.
+ */
+ private void sendProposedRotationChangeToStatusBarInternal(int rotation, boolean isValid) {
+ StatusBarManagerInternal statusBar = getStatusBarManagerInternal();
+ if (statusBar != null) {
+ statusBar.onProposedRotationChanged(rotation, isValid);
+ }
+ }
+
+ /**
* Returns true if the key can have global actions attached to it.
* We reserve all power management keys for the system since they require
* very careful handling.
@@ -6878,12 +6857,12 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
@Override
- public void dismissKeyguardLw(IKeyguardDismissCallback callback) {
+ public void dismissKeyguardLw(IKeyguardDismissCallback callback, CharSequence message) {
if (mKeyguardDelegate != null && mKeyguardDelegate.isShowing()) {
if (DEBUG_KEYGUARD) Slog.d(TAG, "PWM.dismissKeyguardLw");
// ask the keyguard to prompt the user to authenticate if necessary
- mKeyguardDelegate.dismiss(callback);
+ mKeyguardDelegate.dismiss(callback, message);
} else if (callback != null) {
try {
callback.onDismissError();
@@ -7165,6 +7144,92 @@ public class PhoneWindowManager implements WindowManagerPolicy {
mOrientationListener.setCurrentRotation(rotation);
}
+ public boolean isRotationChoiceEnabled() {
+ // Rotation choice is only shown when the user is in locked mode.
+ if (mUserRotationMode != WindowManagerPolicy.USER_ROTATION_LOCKED) return false;
+
+ // We should only enable rotation choice if the rotation isn't forced by the lid, dock,
+ // demo, hdmi, vr, etc mode
+
+ // Determine if the rotation is currently forced
+ if (mForceDefaultOrientation) {
+ return false; // Rotation is forced to default orientation
+
+ } else if (mLidState == LID_OPEN && mLidOpenRotation >= 0) {
+ return false; // Rotation is forced mLidOpenRotation
+
+ } else if (mDockMode == Intent.EXTRA_DOCK_STATE_CAR && !mCarDockEnablesAccelerometer) {
+ return false; // Rotation forced to mCarDockRotation
+
+ } else if ((mDockMode == Intent.EXTRA_DOCK_STATE_DESK
+ || mDockMode == Intent.EXTRA_DOCK_STATE_LE_DESK
+ || mDockMode == Intent.EXTRA_DOCK_STATE_HE_DESK)
+ && !mDeskDockEnablesAccelerometer) {
+ return false; // Rotation forced to mDeskDockRotation
+
+ } else if (mHdmiPlugged && mDemoHdmiRotationLock) {
+ return false; // Rotation forced to mDemoHdmiRotation
+
+ } else if (mHdmiPlugged && mDockMode == Intent.EXTRA_DOCK_STATE_UNDOCKED
+ && mUndockedHdmiRotation >= 0) {
+ return false; // Rotation forced to mUndockedHdmiRotation
+
+ } else if (mDemoRotationLock) {
+ return false; // Rotation forced to mDemoRotation
+
+ } else if (mPersistentVrModeEnabled) {
+ return false; // Rotation forced to mPortraitRotation
+
+ } else if (!mSupportAutoRotation) {
+ return false;
+ }
+
+ // Rotation isn't forced, enable choice
+ return true;
+ }
+
+ public boolean isValidRotationChoice(int orientation, final int preferredRotation) {
+ // Determine if the given app orientation can be chosen and, if so, if it is compatible
+ // with the provided rotation choice
+
+ switch (orientation) {
+ case ActivityInfo.SCREEN_ORIENTATION_PORTRAIT:
+ case ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE:
+ case ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT:
+ case ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE:
+ case ActivityInfo.SCREEN_ORIENTATION_LOCKED:
+ return false; // Forced into a particular rotation, no user choice
+
+ case ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE:
+ case ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT:
+ case ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR:
+ case ActivityInfo.SCREEN_ORIENTATION_SENSOR:
+ return false; // Sensor overrides user choice
+
+ case ActivityInfo.SCREEN_ORIENTATION_NOSENSOR:
+ // TODO Can sensor be used to indirectly determine the orientation?
+ return false;
+
+ case ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE:
+ // If the user has locked sensor-based rotation, this behaves the same as landscape
+ return false; // User has locked the rotation, will behave as LANDSCAPE
+ case ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT:
+ // If the user has locked sensor-based rotation, this behaves the same as portrait
+ return false; // User has locked the rotation, will behave as PORTRAIT
+ case ActivityInfo.SCREEN_ORIENTATION_USER:
+ // Works with any rotation except upside down
+ return (preferredRotation >= 0) && (preferredRotation != mUpsideDownRotation);
+ case ActivityInfo.SCREEN_ORIENTATION_FULL_USER:
+ // Works with any of the 4 rotations
+ return preferredRotation >= 0;
+
+ default:
+ // TODO: how to handle SCREEN_ORIENTATION_BEHIND, UNSET?
+ // For UNSPECIFIED use preferred orientation matching SCREEN_ORIENTATION_USER
+ return (preferredRotation >= 0) && (preferredRotation != mUpsideDownRotation);
+ }
+ }
+
private boolean isLandscapeOrSeascape(int rotation) {
return rotation == mLandscapeRotation || rotation == mSeascapeRotation;
}
@@ -7575,6 +7640,11 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
void startDockOrHome(boolean fromHomeKey, boolean awakenFromDreams) {
+ try {
+ ActivityManager.getService().stopAppSwitches();
+ } catch (RemoteException e) {}
+ sendCloseSystemWindows(SYSTEM_DIALOG_REASON_HOME_KEY);
+
if (awakenFromDreams) {
awakenDreams();
}
@@ -7614,11 +7684,6 @@ public class PhoneWindowManager implements WindowManagerPolicy {
}
if (false) {
// This code always brings home to the front.
- try {
- ActivityManager.getService().stopAppSwitches();
- } catch (RemoteException e) {
- }
- sendCloseSystemWindows();
startDockOrHome(false /*fromHomeKey*/, true /* awakenFromDreams */);
} else {
// This code brings home to the front or, if it is already
@@ -7842,8 +7907,11 @@ public class PhoneWindowManager implements WindowManagerPolicy {
// If the top fullscreen-or-dimming window is also the top fullscreen, respect
// its light flag.
vis &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
- vis |= PolicyControl.getSystemUiVisibility(statusColorWin, null)
- & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+ if (!statusColorWin.isLetterboxedForDisplayCutoutLw()) {
+ // Only allow white status bar if the window was not letterboxed.
+ vis |= PolicyControl.getSystemUiVisibility(statusColorWin, null)
+ & View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+ }
} else if (statusColorWin != null && statusColorWin.isDimming()) {
// Otherwise if it's dimming, clear the light flag.
vis &= ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
@@ -7851,26 +7919,60 @@ public class PhoneWindowManager implements WindowManagerPolicy {
return vis;
}
- private int updateLightNavigationBarLw(int vis, WindowState opaque,
- WindowState opaqueOrDimming) {
- final WindowState imeWin = mWindowManagerFuncs.getInputMethodWindowLw();
-
- final WindowState navColorWin;
- if (imeWin != null && imeWin.isVisibleLw() && mNavigationBarPosition == NAV_BAR_BOTTOM) {
- navColorWin = imeWin;
+ @VisibleForTesting
+ @Nullable
+ static WindowState chooseNavigationColorWindowLw(WindowState opaque,
+ WindowState opaqueOrDimming, WindowState imeWindow,
+ @NavigationBarPosition int navBarPosition) {
+ // If the IME window is visible and FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS is set, then IME
+ // window can be navigation color window.
+ final boolean imeWindowCanNavColorWindow = imeWindow != null
+ && imeWindow.isVisibleLw()
+ && navBarPosition == NAV_BAR_BOTTOM
+ && (PolicyControl.getWindowFlags(imeWindow, null)
+ & WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
+
+ if (opaque != null && opaqueOrDimming == opaque) {
+ // If the top fullscreen-or-dimming window is also the top fullscreen, respect it
+ // unless IME window is also eligible, since currently the IME window is always show
+ // above the opaque fullscreen app window, regardless of the IME target window.
+ // TODO(b/31559891): Maybe we need to revisit this condition once b/31559891 is fixed.
+ return imeWindowCanNavColorWindow ? imeWindow : opaque;
+ }
+
+ if (opaqueOrDimming == null || !opaqueOrDimming.isDimming()) {
+ // No dimming window is involved. Determine the result only with the IME window.
+ return imeWindowCanNavColorWindow ? imeWindow : null;
+ }
+
+ if (!imeWindowCanNavColorWindow) {
+ // No IME window is involved. Determine the result only with opaqueOrDimming.
+ return opaqueOrDimming;
+ }
+
+ // The IME window and the dimming window are competing. Check if the dimming window can be
+ // IME target or not.
+ if (LayoutParams.mayUseInputMethod(PolicyControl.getWindowFlags(opaqueOrDimming, null))) {
+ // The IME window is above the dimming window.
+ return imeWindow;
} else {
- navColorWin = opaqueOrDimming;
+ // The dimming window is above the IME window.
+ return opaqueOrDimming;
}
+ }
+
+ @VisibleForTesting
+ static int updateLightNavigationBarLw(int vis, WindowState opaque, WindowState opaqueOrDimming,
+ WindowState imeWindow, WindowState navColorWin) {
if (navColorWin != null) {
- if (navColorWin == opaque) {
- // If the top fullscreen-or-dimming window is also the top fullscreen, respect
- // its light flag.
+ if (navColorWin == imeWindow || navColorWin == opaque) {
+ // Respect the light flag.
vis &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
vis |= PolicyControl.getSystemUiVisibility(navColorWin, null)
& View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
- } else if (navColorWin.isDimming() || navColorWin == imeWin) {
- // Otherwise if it's dimming or it's the IME window, clear the light flag.
+ } else if (navColorWin == opaqueOrDimming && navColorWin.isDimming()) {
+ // Clear the light flag for dimming window.
vis &= ~View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR;
}
}
@@ -8008,8 +8110,12 @@ public class PhoneWindowManager implements WindowManagerPolicy {
vis = mNavigationBarController.updateVisibilityLw(transientNavBarAllowed, oldVis, vis);
+ final WindowState navColorWin = chooseNavigationColorWindowLw(
+ mTopFullscreenOpaqueWindowState, mTopFullscreenOpaqueOrDimmingWindowState,
+ mWindowManagerFuncs.getInputMethodWindowLw(), mNavigationBarPosition);
vis = updateLightNavigationBarLw(vis, mTopFullscreenOpaqueWindowState,
- mTopFullscreenOpaqueOrDimmingWindowState);
+ mTopFullscreenOpaqueOrDimmingWindowState,
+ mWindowManagerFuncs.getInputMethodWindowLw(), navColorWin);
return vis;
}
@@ -8177,6 +8283,40 @@ public class PhoneWindowManager implements WindowManagerPolicy {
@Override
public void writeToProto(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
+ proto.write(LAST_SYSTEM_UI_FLAGS, mLastSystemUiFlags);
+ proto.write(ROTATION_MODE, mUserRotationMode);
+ proto.write(ROTATION, mUserRotation);
+ proto.write(ORIENTATION, mCurrentAppOrientation);
+ proto.write(SCREEN_ON_FULLY, mScreenOnFully);
+ proto.write(KEYGUARD_DRAW_COMPLETE, mKeyguardDrawComplete);
+ proto.write(WINDOW_MANAGER_DRAW_COMPLETE, mWindowManagerDrawComplete);
+ if (mFocusedApp != null) {
+ proto.write(FOCUSED_APP_TOKEN, mFocusedApp.toString());
+ }
+ if (mFocusedWindow != null) {
+ mFocusedWindow.writeIdentifierToProto(proto, FOCUSED_WINDOW);
+ }
+ if (mTopFullscreenOpaqueWindowState != null) {
+ mTopFullscreenOpaqueWindowState.writeIdentifierToProto(
+ proto, TOP_FULLSCREEN_OPAQUE_WINDOW);
+ }
+ if (mTopFullscreenOpaqueOrDimmingWindowState != null) {
+ mTopFullscreenOpaqueOrDimmingWindowState.writeIdentifierToProto(
+ proto, TOP_FULLSCREEN_OPAQUE_OR_DIMMING_WINDOW);
+ }
+ proto.write(KEYGUARD_OCCLUDED, mKeyguardOccluded);
+ proto.write(KEYGUARD_OCCLUDED_CHANGED, mKeyguardOccludedChanged);
+ proto.write(KEYGUARD_OCCLUDED_PENDING, mPendingKeyguardOccluded);
+ proto.write(FORCE_STATUS_BAR, mForceStatusBar);
+ proto.write(FORCE_STATUS_BAR_FROM_KEYGUARD, mForceStatusBarFromKeyguard);
+ mStatusBarController.writeToProto(proto, STATUS_BAR);
+ mNavigationBarController.writeToProto(proto, NAVIGATION_BAR);
+ if (mOrientationListener != null) {
+ mOrientationListener.writeToProto(proto, ORIENTATION_LISTENER);
+ }
+ if (mKeyguardDelegate != null) {
+ mKeyguardDelegate.writeToProto(proto, KEYGUARD_DELEGATE);
+ }
proto.end(token);
}
diff --git a/com/android/server/policy/WindowManagerPolicy.java b/com/android/server/policy/WindowManagerPolicy.java
index cfe40887..e9c4c5c8 100644
--- a/com/android/server/policy/WindowManagerPolicy.java
+++ b/com/android/server/policy/WindowManagerPolicy.java
@@ -61,6 +61,8 @@ import static android.view.WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
import static android.view.WindowManager.LayoutParams.isSystemAlertWindowType;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
import android.Manifest;
import android.annotation.IntDef;
import android.annotation.Nullable;
@@ -136,10 +138,9 @@ import java.lang.annotation.RetentionPolicy;
* </dl>
*/
public interface WindowManagerPolicy extends WindowManagerPolicyConstants {
- // Navigation bar position values
- int NAV_BAR_LEFT = 1 << 0;
- int NAV_BAR_RIGHT = 1 << 1;
- int NAV_BAR_BOTTOM = 1 << 2;
+ @Retention(SOURCE)
+ @IntDef({NAV_BAR_LEFT, NAV_BAR_RIGHT, NAV_BAR_BOTTOM})
+ @interface NavigationBarPosition {}
/**
* Pass this event to the user / app. To be returned from
@@ -170,6 +171,11 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants {
void onKeyguardOccludedChangedLw(boolean occluded);
/**
+ * Called when the resource overlays change.
+ */
+ default void onOverlayChangedLw() {}
+
+ /**
* Interface to the Window Manager state associated with a particular
* window. You can hold on to an instance of this interface from the call
* to prepareAddWindow() until removeWindow().
@@ -440,6 +446,13 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants {
*/
public boolean isDimming();
+ /**
+ * Returns true if the window is letterboxed for the display cutout.
+ */
+ default boolean isLetterboxedForDisplayCutoutLw() {
+ return false;
+ }
+
/** @return the current windowing mode of this window. */
int getWindowingMode();
@@ -468,6 +481,11 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants {
* visible. That is, they have the permission {@link Manifest.permission#DEVICE_POWER}.
*/
boolean canAcquireSleepToken();
+
+ /**
+ * Writes {@link com.android.server.wm.proto.IdentifierProto} to stream.
+ */
+ void writeIdentifierToProto(ProtoOutputStream proto, long fieldId);
}
/**
@@ -1188,13 +1206,12 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants {
/**
* Return true if it is okay to perform animations for an app transition
- * that is about to occur. You may return false for this if, for example,
- * the lock screen is currently displayed so the switch should happen
+ * that is about to occur. You may return false for this if, for example,
+ * the dream window is currently displayed so the switch should happen
* immediately.
*/
public boolean allowAppAnimationsLw();
-
/**
* A new window has been focused.
*/
@@ -1374,8 +1391,10 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants {
* Ask the policy to dismiss the keyguard, if it is currently shown.
*
* @param callback Callback to be informed about the result.
+ * @param message A message that should be displayed in the keyguard.
*/
- public void dismissKeyguardLw(@Nullable IKeyguardDismissCallback callback);
+ public void dismissKeyguardLw(@Nullable IKeyguardDismissCallback callback,
+ CharSequence message);
/**
* Ask the policy whether the Keyguard has drawn. If the Keyguard is disabled, this method
@@ -1552,7 +1571,7 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants {
* Show the recents task list app.
* @hide
*/
- public void showRecentApps(boolean fromHome);
+ public void showRecentApps();
/**
* Show the global actions dialog.
@@ -1637,6 +1656,7 @@ public interface WindowManagerPolicy extends WindowManagerPolicyConstants {
* @see #NAV_BAR_RIGHT
* @see #NAV_BAR_BOTTOM
*/
+ @NavigationBarPosition
int getNavBarPosition();
/**
diff --git a/com/android/server/policy/WindowOrientationListener.java b/com/android/server/policy/WindowOrientationListener.java
index 169fd278..48a196df 100644
--- a/com/android/server/policy/WindowOrientationListener.java
+++ b/com/android/server/policy/WindowOrientationListener.java
@@ -16,6 +16,9 @@
package com.android.server.policy;
+import static com.android.server.wm.proto.WindowOrientationListenerProto.ENABLED;
+import static com.android.server.wm.proto.WindowOrientationListenerProto.ROTATION;
+
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
@@ -24,12 +27,11 @@ import android.hardware.SensorManager;
import android.os.Handler;
import android.os.SystemClock;
import android.os.SystemProperties;
-import android.text.TextUtils;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import android.view.Surface;
import java.io.PrintWriter;
-import java.util.Arrays;
import java.util.List;
/**
@@ -65,7 +67,7 @@ public abstract class WindowOrientationListener {
/**
* Creates a new WindowOrientationListener.
- *
+ *
* @param context for the WindowOrientationListener.
* @param handler Provides the Looper for receiving sensor updates.
*/
@@ -75,12 +77,12 @@ public abstract class WindowOrientationListener {
/**
* Creates a new WindowOrientationListener.
- *
+ *
* @param context for the WindowOrientationListener.
* @param handler Provides the Looper for receiving sensor updates.
* @param rate at which sensor events are processed (see also
* {@link android.hardware.SensorManager SensorManager}). Use the default
- * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL
+ * value of {@link android.hardware.SensorManager#SENSOR_DELAY_NORMAL
* SENSOR_DELAY_NORMAL} for simple screen orientation change detection.
*
* This constructor is private since no one uses it.
@@ -89,7 +91,28 @@ public abstract class WindowOrientationListener {
mHandler = handler;
mSensorManager = (SensorManager)context.getSystemService(Context.SENSOR_SERVICE);
mRate = rate;
- mSensor = mSensorManager.getDefaultSensor(Sensor.TYPE_DEVICE_ORIENTATION);
+ List<Sensor> l = mSensorManager.getSensorList(Sensor.TYPE_DEVICE_ORIENTATION);
+ Sensor wakeUpDeviceOrientationSensor = null;
+ Sensor nonWakeUpDeviceOrientationSensor = null;
+ /**
+ * Prefer the wakeup form of the sensor if implemented.
+ * It's OK to look for just two types of this sensor and use
+ * the last found. Typical devices will only have one sensor of
+ * this type.
+ */
+ for (Sensor s : l) {
+ if (s.isWakeUpSensor()) {
+ wakeUpDeviceOrientationSensor = s;
+ } else {
+ nonWakeUpDeviceOrientationSensor = s;
+ }
+ }
+
+ if (wakeUpDeviceOrientationSensor != null) {
+ mSensor = wakeUpDeviceOrientationSensor;
+ } else {
+ mSensor = nonWakeUpDeviceOrientationSensor;
+ }
if (mSensor != null) {
mOrientationJudge = new OrientationSensorJudge();
@@ -232,6 +255,15 @@ public abstract class WindowOrientationListener {
*/
public abstract void onProposedRotationChanged(int rotation);
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ synchronized (mLock) {
+ proto.write(ENABLED, mEnabled);
+ proto.write(ROTATION, mCurrentRotation);
+ }
+ proto.end(token);
+ }
+
public void dump(PrintWriter pw, String prefix) {
synchronized (mLock) {
pw.println(prefix + TAG);
diff --git a/com/android/server/policy/keyguard/KeyguardServiceDelegate.java b/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
index 58002bc8..18f4a3c5 100644
--- a/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
+++ b/com/android/server/policy/keyguard/KeyguardServiceDelegate.java
@@ -1,6 +1,11 @@
package com.android.server.policy.keyguard;
import static android.view.Display.INVALID_DISPLAY;
+import static com.android.server.wm.proto.KeyguardServiceDelegateProto.INTERACTIVE_STATE;
+import static com.android.server.wm.proto.KeyguardServiceDelegateProto.OCCLUDED;
+import static com.android.server.wm.proto.KeyguardServiceDelegateProto.SCREEN_STATE;
+import static com.android.server.wm.proto.KeyguardServiceDelegateProto.SECURE;
+import static com.android.server.wm.proto.KeyguardServiceDelegateProto.SHOWING;
import android.app.ActivityManager;
import android.content.ComponentName;
@@ -15,6 +20,7 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import android.view.WindowManagerPolicyConstants;
import com.android.internal.policy.IKeyguardDismissCallback;
@@ -257,9 +263,9 @@ public class KeyguardServiceDelegate {
mKeyguardState.occluded = isOccluded;
}
- public void dismiss(IKeyguardDismissCallback callback) {
+ public void dismiss(IKeyguardDismissCallback callback, CharSequence message) {
if (mKeyguardService != null) {
- mKeyguardService.dismiss(callback);
+ mKeyguardService.dismiss(callback, message);
}
}
@@ -406,6 +412,16 @@ public class KeyguardServiceDelegate {
}
}
+ public void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(SHOWING, mKeyguardState.showing);
+ proto.write(OCCLUDED, mKeyguardState.occluded);
+ proto.write(SECURE, mKeyguardState.secure);
+ proto.write(SCREEN_STATE, mKeyguardState.screenState);
+ proto.write(INTERACTIVE_STATE, mKeyguardState.interactiveState);
+ proto.end(token);
+ }
+
public void dump(String prefix, PrintWriter pw) {
pw.println(prefix + TAG);
prefix += " ";
diff --git a/com/android/server/policy/keyguard/KeyguardServiceWrapper.java b/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
index 952e0b01..4e848686 100644
--- a/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
+++ b/com/android/server/policy/keyguard/KeyguardServiceWrapper.java
@@ -74,9 +74,9 @@ public class KeyguardServiceWrapper implements IKeyguardService {
}
@Override // Binder interface
- public void dismiss(IKeyguardDismissCallback callback) {
+ public void dismiss(IKeyguardDismissCallback callback, CharSequence message) {
try {
- mService.dismiss(callback);
+ mService.dismiss(callback, message);
} catch (RemoteException e) {
Slog.w(TAG , "Remote Exception", e);
}
diff --git a/com/android/server/power/BatterySaverPolicy.java b/com/android/server/power/BatterySaverPolicy.java
index 6f005a35..a538967e 100644
--- a/com/android/server/power/BatterySaverPolicy.java
+++ b/com/android/server/power/BatterySaverPolicy.java
@@ -20,6 +20,7 @@ import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.Handler;
+import android.os.PowerManager;
import android.os.PowerManager.ServiceType;
import android.os.PowerSaveState;
import android.provider.Settings;
@@ -49,21 +50,6 @@ 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. */
- 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.
- */
- 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";
@@ -354,7 +340,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_ALL_DISABLED_WHEN_SCREEN_OFF);
+ PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF);
mGpsMode = parser.getInt(KEY_GPS_MODE, defaultGpsMode);
// Non-device-specific parameters.
diff --git a/com/android/server/power/Notifier.java b/com/android/server/power/Notifier.java
index e5a23ea3..a5362709 100644
--- a/com/android/server/power/Notifier.java
+++ b/com/android/server/power/Notifier.java
@@ -19,15 +19,6 @@ package com.android.server.power;
import android.annotation.UserIdInt;
import android.app.ActivityManagerInternal;
import android.app.AppOpsManager;
-
-import com.android.internal.app.IAppOpsService;
-import com.android.internal.app.IBatteryStats;
-import com.android.internal.logging.MetricsLogger;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-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;
@@ -54,6 +45,15 @@ import android.util.EventLog;
import android.util.Slog;
import android.view.inputmethod.InputMethodManagerInternal;
+import com.android.internal.app.IAppOpsService;
+import com.android.internal.app.IBatteryStats;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.server.EventLogTags;
+import com.android.server.LocalServices;
+import com.android.server.statusbar.StatusBarManagerInternal;
+import com.android.server.policy.WindowManagerPolicy;
+
/**
* Sends broadcasts about important power state changes.
* <p>
@@ -96,6 +96,7 @@ final class Notifier {
private final ActivityManagerInternal mActivityManagerInternal;
private final InputManagerInternal mInputManagerInternal;
private final InputMethodManagerInternal mInputMethodManagerInternal;
+ private final StatusBarManagerInternal mStatusBarManagerInternal;
private final TrustManager mTrustManager;
private final NotifierHandler mHandler;
@@ -142,6 +143,7 @@ final class Notifier {
mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
mInputMethodManagerInternal = LocalServices.getService(InputMethodManagerInternal.class);
+ mStatusBarManagerInternal = LocalServices.getService(StatusBarManagerInternal.class);
mTrustManager = mContext.getSystemService(TrustManager.class);
mHandler = new NotifierHandler(looper);
@@ -209,10 +211,7 @@ final class Notifier {
try {
if (workSource != null) {
- final int N = workSource.size();
- for (int i=0; i<N; i++) {
- mBatteryStats.noteLongPartialWakelockStart(tag, historyTag, workSource.get(i));
- }
+ mBatteryStats.noteLongPartialWakelockStartFromSource(tag, historyTag, workSource);
} else {
mBatteryStats.noteLongPartialWakelockStart(tag, historyTag, ownerUid);
}
@@ -230,10 +229,7 @@ final class Notifier {
try {
if (workSource != null) {
- final int N = workSource.size();
- for (int i=0; i<N; i++) {
- mBatteryStats.noteLongPartialWakelockFinish(tag, historyTag, workSource.get(i));
- }
+ mBatteryStats.noteLongPartialWakelockFinishFromSource(tag, historyTag, workSource);
} else {
mBatteryStats.noteLongPartialWakelockFinish(tag, historyTag, ownerUid);
}
@@ -551,9 +547,19 @@ final class Notifier {
}
/**
- * Called when wireless charging has started so as to provide user feedback.
+ * 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);
+ }
+
+ /**
+ * Called when wireless charging has started so as to provide user feedback (sound and visual).
*/
- public void onWirelessChargingStarted() {
+ public void onWirelessChargingStarted(int batteryLevel) {
if (DEBUG) {
Slog.d(TAG, "onWirelessChargingStarted");
}
@@ -561,16 +567,7 @@ final class Notifier {
mSuspendBlocker.acquire();
Message msg = mHandler.obtainMessage(MSG_WIRELESS_CHARGING_STARTED);
msg.setAsynchronous(true);
- 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;
+ msg.arg1 = batteryLevel;
mHandler.sendMessage(msg);
}
@@ -721,7 +718,11 @@ final class Notifier {
}
}
}
+ }
+ private void showWirelessChargingStarted(int batteryLevel) {
+ playWirelessChargingStartedSound();
+ mStatusBarManagerInternal.showChargingAnimation(batteryLevel);
mSuspendBlocker.release();
}
@@ -744,7 +745,7 @@ final class Notifier {
sendNextBroadcast();
break;
case MSG_WIRELESS_CHARGING_STARTED:
- playWirelessChargingStartedSound();
+ showWirelessChargingStarted(msg.arg1);
break;
case MSG_SCREEN_BRIGHTNESS_BOOST_CHANGED:
sendBrightnessBoostChangedBroadcast();
diff --git a/com/android/server/power/PowerManagerService.java b/com/android/server/power/PowerManagerService.java
index 02c8f681..cf361667 100644
--- a/com/android/server/power/PowerManagerService.java
+++ b/com/android/server/power/PowerManagerService.java
@@ -16,6 +16,11 @@
package com.android.server.power;
+import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP;
+import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE;
+import static android.os.PowerManagerInternal.WAKEFULNESS_DOZING;
+import static android.os.PowerManagerInternal.WAKEFULNESS_DREAMING;
+
import android.annotation.IntDef;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
@@ -57,6 +62,7 @@ import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.WorkSource;
+import android.os.WorkSource.WorkChain;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.service.dreams.DreamManagerInternal;
@@ -64,6 +70,7 @@ import android.service.vr.IVrManager;
import android.service.vr.IVrStateCallbacks;
import android.util.EventLog;
import android.util.KeyValueListParser;
+import android.util.MathUtils;
import android.util.PrintWriterPrinter;
import android.util.Slog;
import android.util.SparseArray;
@@ -102,11 +109,6 @@ import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
-import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP;
-import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE;
-import static android.os.PowerManagerInternal.WAKEFULNESS_DOZING;
-import static android.os.PowerManagerInternal.WAKEFULNESS_DREAMING;
-
/**
* The power manager service is responsible for coordinating power management
* functions on the device.
@@ -118,6 +120,9 @@ public final class PowerManagerService extends SystemService
private static final boolean DEBUG = false;
private static final boolean DEBUG_SPEW = DEBUG && true;
+ // if DEBUG_WIRELESS=true, plays wireless charging animation w/ sound on every plug + unplug
+ private static final boolean DEBUG_WIRELESS = false;
+
// Message: Sent when a user activity timeout occurs to update the power state.
private static final int MSG_USER_ACTIVITY_TIMEOUT = 1;
// Message: Sent when the device enters or exits a dreaming or dozing state.
@@ -454,19 +459,11 @@ public final class PowerManagerService extends SystemService
private int mScreenBrightnessSettingMinimum;
private int mScreenBrightnessSettingMaximum;
private int mScreenBrightnessSettingDefault;
- private int mScreenBrightnessForVrSettingDefault;
// The screen brightness setting, from 0 to 255.
// Use -1 if no value has been set.
private int mScreenBrightnessSetting;
- // The screen brightness setting, from 0 to 255, to be used while in VR Mode.
- private int mScreenBrightnessForVrSetting;
-
- // The screen auto-brightness adjustment setting, from -1 to 1.
- // Use 0 if there is no adjustment.
- private float mScreenAutoBrightnessAdjustmentSetting;
-
// The screen brightness mode.
// One of the Settings.System.SCREEN_BRIGHTNESS_MODE_* constants.
private int mScreenBrightnessModeSetting;
@@ -489,17 +486,6 @@ public final class PowerManagerService extends SystemService
// Use -1 to disable.
private long mUserActivityTimeoutOverrideFromWindowManager = -1;
- // The screen brightness setting override from the settings application
- // to temporarily adjust the brightness until next updated,
- // Use -1 to disable.
- private int mTemporaryScreenBrightnessSettingOverride = -1;
-
- // The screen brightness adjustment setting override from the settings
- // application to temporarily adjust the auto-brightness adjustment factor
- // until next updated, in the range -1..1.
- // Use NaN to disable.
- private float mTemporaryScreenAutoBrightnessAdjustmentSettingOverride = Float.NaN;
-
// The screen state to use while dozing.
private int mDozeScreenStateOverrideFromDreamManager = Display.STATE_UNKNOWN;
@@ -770,7 +756,6 @@ public final class PowerManagerService extends SystemService
mScreenBrightnessSettingMinimum = pm.getMinimumScreenBrightnessSetting();
mScreenBrightnessSettingMaximum = pm.getMaximumScreenBrightnessSetting();
mScreenBrightnessSettingDefault = pm.getDefaultScreenBrightnessSetting();
- mScreenBrightnessForVrSettingDefault = pm.getDefaultScreenBrightnessForVrSetting();
SensorManager sensorManager = new SystemSensorManager(mContext, mHandler.getLooper());
@@ -833,12 +818,6 @@ public final class PowerManagerService extends SystemService
Settings.Global.STAY_ON_WHILE_PLUGGED_IN),
false, mSettingsObserver, UserHandle.USER_ALL);
resolver.registerContentObserver(Settings.System.getUriFor(
- Settings.System.SCREEN_BRIGHTNESS),
- false, mSettingsObserver, UserHandle.USER_ALL);
- resolver.registerContentObserver(Settings.System.getUriFor(
- Settings.System.SCREEN_BRIGHTNESS_FOR_VR),
- false, mSettingsObserver, UserHandle.USER_ALL);
- resolver.registerContentObserver(Settings.System.getUriFor(
Settings.System.SCREEN_BRIGHTNESS_MODE),
false, mSettingsObserver, UserHandle.USER_ALL);
resolver.registerContentObserver(Settings.System.getUriFor(
@@ -974,29 +953,6 @@ public final class PowerManagerService extends SystemService
SystemProperties.set(SYSTEM_PROPERTY_RETAIL_DEMO_ENABLED, retailDemoValue);
}
- final int oldScreenBrightnessSetting = getCurrentBrightnessSettingLocked();
-
- mScreenBrightnessForVrSetting = Settings.System.getIntForUser(resolver,
- Settings.System.SCREEN_BRIGHTNESS_FOR_VR, mScreenBrightnessForVrSettingDefault,
- UserHandle.USER_CURRENT);
-
- mScreenBrightnessSetting = Settings.System.getIntForUser(resolver,
- Settings.System.SCREEN_BRIGHTNESS, mScreenBrightnessSettingDefault,
- UserHandle.USER_CURRENT);
-
- if (oldScreenBrightnessSetting != getCurrentBrightnessSettingLocked()) {
- mTemporaryScreenBrightnessSettingOverride = -1;
- }
-
- final float oldScreenAutoBrightnessAdjustmentSetting =
- mScreenAutoBrightnessAdjustmentSetting;
- mScreenAutoBrightnessAdjustmentSetting = Settings.System.getFloatForUser(resolver,
- Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0.0f,
- UserHandle.USER_CURRENT);
- if (oldScreenAutoBrightnessAdjustmentSetting != mScreenAutoBrightnessAdjustmentSetting) {
- mTemporaryScreenAutoBrightnessAdjustmentSettingOverride = Float.NaN;
- }
-
mScreenBrightnessModeSetting = Settings.System.getIntForUser(resolver,
Settings.System.SCREEN_BRIGHTNESS_MODE,
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL, UserHandle.USER_CURRENT);
@@ -1015,10 +971,6 @@ public final class PowerManagerService extends SystemService
mDirty |= DIRTY_SETTINGS;
}
- private int getCurrentBrightnessSettingLocked() {
- return mIsVrModeEnabled ? mScreenBrightnessForVrSetting : mScreenBrightnessSetting;
- }
-
private void postAfterBootCompleted(Runnable r) {
if (mBootCompleted) {
BackgroundThread.getHandler().post(r);
@@ -1793,8 +1745,8 @@ public final class PowerManagerService extends SystemService
// Tell the notifier whether wireless charging has started so that
// it can provide feedback to the user.
- if (dockedOnWirelessCharger) {
- mNotifier.onWirelessChargingStarted();
+ if (dockedOnWirelessCharger || DEBUG_WIRELESS) {
+ mNotifier.onWirelessChargingStarted(mBatteryLevel);
}
}
@@ -1976,6 +1928,16 @@ public final class PowerManagerService extends SystemService
return true;
}
}
+
+ final ArrayList<WorkChain> workChains = wakeLock.mWorkSource.getWorkChains();
+ if (workChains != null) {
+ for (int k = 0; k < workChains.size(); k++) {
+ final int uid = workChains.get(k).getAttributionUid();
+ if (userId == UserHandle.getUserId(uid)) {
+ return true;
+ }
+ }
+ }
}
return userId == UserHandle.getUserId(wakeLock.mOwnerUid);
}
@@ -2436,49 +2398,24 @@ public final class PowerManagerService extends SystemService
mDisplayPowerRequest.policy = getDesiredScreenPolicyLocked();
// Determine appropriate screen brightness and auto-brightness adjustments.
- boolean brightnessSetByUser = true;
- int screenBrightness = mScreenBrightnessSettingDefault;
- float screenAutoBrightnessAdjustment = 0.0f;
- boolean autoBrightness = (mScreenBrightnessModeSetting ==
- Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+ final boolean autoBrightness;
+ final int screenBrightnessOverride;
if (!mBootCompleted) {
// Keep the brightness steady during boot. This requires the
// bootloader brightness and the default brightness to be identical.
autoBrightness = false;
- brightnessSetByUser = false;
- } else if (mIsVrModeEnabled) {
- screenBrightness = mScreenBrightnessForVrSetting;
- autoBrightness = false;
+ screenBrightnessOverride = mScreenBrightnessSettingDefault;
} else if (isValidBrightness(mScreenBrightnessOverrideFromWindowManager)) {
- screenBrightness = mScreenBrightnessOverrideFromWindowManager;
autoBrightness = false;
- brightnessSetByUser = false;
- } else if (isValidBrightness(mTemporaryScreenBrightnessSettingOverride)) {
- screenBrightness = mTemporaryScreenBrightnessSettingOverride;
- } else if (isValidBrightness(mScreenBrightnessSetting)) {
- screenBrightness = mScreenBrightnessSetting;
- }
- if (autoBrightness) {
- screenBrightness = mScreenBrightnessSettingDefault;
- if (isValidAutoBrightnessAdjustment(
- mTemporaryScreenAutoBrightnessAdjustmentSettingOverride)) {
- screenAutoBrightnessAdjustment =
- mTemporaryScreenAutoBrightnessAdjustmentSettingOverride;
- } else if (isValidAutoBrightnessAdjustment(
- mScreenAutoBrightnessAdjustmentSetting)) {
- screenAutoBrightnessAdjustment = mScreenAutoBrightnessAdjustmentSetting;
- }
+ screenBrightnessOverride = mScreenBrightnessOverrideFromWindowManager;
+ } else {
+ autoBrightness = (mScreenBrightnessModeSetting ==
+ Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC);
+ screenBrightnessOverride = -1;
}
- screenBrightness = Math.max(Math.min(screenBrightness,
- mScreenBrightnessSettingMaximum), mScreenBrightnessSettingMinimum);
- screenAutoBrightnessAdjustment = Math.max(Math.min(
- screenAutoBrightnessAdjustment, 1.0f), -1.0f);
// Update display power request.
- mDisplayPowerRequest.screenBrightness = screenBrightness;
- mDisplayPowerRequest.screenAutoBrightnessAdjustment =
- screenAutoBrightnessAdjustment;
- mDisplayPowerRequest.brightnessSetByUser = brightnessSetByUser;
+ mDisplayPowerRequest.screenBrightnessOverride = screenBrightnessOverride;
mDisplayPowerRequest.useAutoBrightness = autoBrightness;
mDisplayPowerRequest.useProximitySensor = shouldUseProximitySensorLocked();
mDisplayPowerRequest.boostScreenBrightness = shouldBoostScreenBrightness();
@@ -2516,6 +2453,8 @@ public final class PowerManagerService extends SystemService
+ ", mWakeLockSummary=0x" + Integer.toHexString(mWakeLockSummary)
+ ", mUserActivitySummary=0x" + Integer.toHexString(mUserActivitySummary)
+ ", mBootCompleted=" + mBootCompleted
+ + ", screenBrightnessOverride=" + screenBrightnessOverride
+ + ", useAutoBrightness=" + autoBrightness
+ ", mScreenBrightnessBoostInProgress=" + mScreenBrightnessBoostInProgress
+ ", mIsVrModeEnabled= " + mIsVrModeEnabled
+ ", sQuiescent=" + sQuiescent);
@@ -2555,11 +2494,6 @@ public final class PowerManagerService extends SystemService
return value >= 0 && value <= 255;
}
- private static boolean isValidAutoBrightnessAdjustment(float value) {
- // Handles NaN by always returning false.
- return value >= -1.0f && value <= 1.0f;
- }
-
@VisibleForTesting
int getDesiredScreenPolicyLocked() {
if (mWakefulness == WAKEFULNESS_ASLEEP || sQuiescent) {
@@ -3125,7 +3059,8 @@ public final class PowerManagerService extends SystemService
if (Arrays.binarySearch(mDeviceIdleWhitelist, appid) < 0 &&
Arrays.binarySearch(mDeviceIdleTempWhitelist, appid) < 0 &&
state.mProcState != ActivityManager.PROCESS_STATE_NONEXISTENT &&
- state.mProcState > ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+ state.mProcState >
+ ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE) {
disabled = true;
}
}
@@ -3228,28 +3163,6 @@ public final class PowerManagerService extends SystemService
}
}
- private void setTemporaryScreenBrightnessSettingOverrideInternal(int brightness) {
- synchronized (mLock) {
- if (mTemporaryScreenBrightnessSettingOverride != brightness) {
- mTemporaryScreenBrightnessSettingOverride = brightness;
- mDirty |= DIRTY_SETTINGS;
- updatePowerStateLocked();
- }
- }
- }
-
- private void setTemporaryScreenAutoBrightnessAdjustmentSettingOverrideInternal(float adj) {
- synchronized (mLock) {
- // Note: This condition handles NaN because NaN is not equal to any other
- // value, including itself.
- if (mTemporaryScreenAutoBrightnessAdjustmentSettingOverride != adj) {
- mTemporaryScreenAutoBrightnessAdjustmentSettingOverride = adj;
- mDirty |= DIRTY_SETTINGS;
- updatePowerStateLocked();
- }
- }
- }
-
private void setDozeOverrideFromDreamManagerInternal(
int screenState, int screenBrightness) {
synchronized (mLock) {
@@ -3459,8 +3372,6 @@ public final class PowerManagerService extends SystemService
+ isMaximumScreenOffTimeoutFromDeviceAdminEnforcedLocked() + ")");
pw.println(" mStayOnWhilePluggedInSetting=" + mStayOnWhilePluggedInSetting);
pw.println(" mScreenBrightnessSetting=" + mScreenBrightnessSetting);
- pw.println(" mScreenAutoBrightnessAdjustmentSetting="
- + mScreenAutoBrightnessAdjustmentSetting);
pw.println(" mScreenBrightnessModeSetting=" + mScreenBrightnessModeSetting);
pw.println(" mScreenBrightnessOverrideFromWindowManager="
+ mScreenBrightnessOverrideFromWindowManager);
@@ -3468,10 +3379,6 @@ public final class PowerManagerService extends SystemService
+ mUserActivityTimeoutOverrideFromWindowManager);
pw.println(" mUserInactiveOverrideFromWindowManager="
+ mUserInactiveOverrideFromWindowManager);
- pw.println(" mTemporaryScreenBrightnessSettingOverride="
- + mTemporaryScreenBrightnessSettingOverride);
- pw.println(" mTemporaryScreenAutoBrightnessAdjustmentSettingOverride="
- + mTemporaryScreenAutoBrightnessAdjustmentSettingOverride);
pw.println(" mDozeScreenStateOverrideFromDreamManager="
+ mDozeScreenStateOverrideFromDreamManager);
pw.println(" mDozeScreenBrightnessOverrideFromDreamManager="
@@ -3479,9 +3386,6 @@ public final class PowerManagerService extends SystemService
pw.println(" mScreenBrightnessSettingMinimum=" + mScreenBrightnessSettingMinimum);
pw.println(" mScreenBrightnessSettingMaximum=" + mScreenBrightnessSettingMaximum);
pw.println(" mScreenBrightnessSettingDefault=" + mScreenBrightnessSettingDefault);
- pw.println(" mScreenBrightnessForVrSettingDefault="
- + mScreenBrightnessForVrSettingDefault);
- pw.println(" mScreenBrightnessForVrSetting=" + mScreenBrightnessForVrSetting);
pw.println(" mDoubleTapWakeEnabled=" + mDoubleTapWakeEnabled);
pw.println(" mIsVrModeEnabled=" + mIsVrModeEnabled);
pw.println(" mForegroundProfile=" + mForegroundProfile);
@@ -3793,13 +3697,6 @@ public final class PowerManagerService extends SystemService
proto.end(stayOnWhilePluggedInToken);
proto.write(
- PowerServiceSettingsAndConfigurationDumpProto.SCREEN_BRIGHTNESS_SETTING,
- mScreenBrightnessSetting);
- proto.write(
- PowerServiceSettingsAndConfigurationDumpProto
- .SCREEN_AUTO_BRIGHTNESS_ADJUSTMENT_SETTING,
- mScreenAutoBrightnessAdjustmentSetting);
- proto.write(
PowerServiceSettingsAndConfigurationDumpProto.SCREEN_BRIGHTNESS_MODE_SETTING,
mScreenBrightnessModeSetting);
proto.write(
@@ -3816,14 +3713,6 @@ public final class PowerManagerService extends SystemService
mUserInactiveOverrideFromWindowManager);
proto.write(
PowerServiceSettingsAndConfigurationDumpProto
- .TEMPORARY_SCREEN_BRIGHTNESS_SETTING_OVERRIDE,
- mTemporaryScreenBrightnessSettingOverride);
- proto.write(
- PowerServiceSettingsAndConfigurationDumpProto
- .TEMPORARY_SCREEN_AUTO_BRIGHTNESS_ADJUSTMENT_SETTING_OVERRIDE,
- mTemporaryScreenAutoBrightnessAdjustmentSettingOverride);
- proto.write(
- PowerServiceSettingsAndConfigurationDumpProto
.DOZE_SCREEN_STATE_OVERRIDE_FROM_DREAM_MANAGER,
mDozeScreenStateOverrideFromDreamManager);
proto.write(
@@ -3847,16 +3736,9 @@ public final class PowerManagerService extends SystemService
PowerServiceSettingsAndConfigurationDumpProto.ScreenBrightnessSettingLimitsProto
.SETTING_DEFAULT,
mScreenBrightnessSettingDefault);
- proto.write(
- PowerServiceSettingsAndConfigurationDumpProto.ScreenBrightnessSettingLimitsProto
- .SETTING_FOR_VR_DEFAULT,
- mScreenBrightnessForVrSettingDefault);
proto.end(screenBrightnessSettingLimitsToken);
proto.write(
- PowerServiceSettingsAndConfigurationDumpProto.SCREEN_BRIGHTNESS_FOR_VR_SETTING,
- mScreenBrightnessForVrSetting);
- proto.write(
PowerServiceSettingsAndConfigurationDumpProto.IS_DOUBLE_TAP_WAKE_ENABLED,
mDoubleTapWakeEnabled);
proto.write(
@@ -4690,56 +4572,6 @@ public final class PowerManagerService extends SystemService
}
/**
- * Used by the settings application and brightness control widgets to
- * temporarily override the current screen brightness setting so that the
- * user can observe the effect of an intended settings change without applying
- * it immediately.
- *
- * The override will be canceled when the setting value is next updated.
- *
- * @param brightness The overridden brightness.
- *
- * @see android.provider.Settings.System#SCREEN_BRIGHTNESS
- */
- @Override // Binder call
- public void setTemporaryScreenBrightnessSettingOverride(int brightness) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.DEVICE_POWER, null);
-
- final long ident = Binder.clearCallingIdentity();
- try {
- setTemporaryScreenBrightnessSettingOverrideInternal(brightness);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- }
-
- /**
- * Used by the settings application and brightness control widgets to
- * temporarily override the current screen auto-brightness adjustment setting so that the
- * user can observe the effect of an intended settings change without applying
- * it immediately.
- *
- * The override will be canceled when the setting value is next updated.
- *
- * @param adj The overridden brightness, or Float.NaN to disable the override.
- *
- * @see android.provider.Settings.System#SCREEN_AUTO_BRIGHTNESS_ADJ
- */
- @Override // Binder call
- public void setTemporaryScreenAutoBrightnessAdjustmentSettingOverride(float adj) {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.DEVICE_POWER, null);
-
- final long ident = Binder.clearCallingIdentity();
- try {
- setTemporaryScreenAutoBrightnessAdjustmentSettingOverrideInternal(adj);
- } finally {
- Binder.restoreCallingIdentity(ident);
- }
- }
-
- /**
* Used by the phone application to make the attention LED flash when ringing.
*/
@Override // Binder call
diff --git a/com/android/server/power/ShutdownThread.java b/com/android/server/power/ShutdownThread.java
index 6fb345bc..b986e046 100644
--- a/com/android/server/power/ShutdownThread.java
+++ b/com/android/server/power/ShutdownThread.java
@@ -21,8 +21,6 @@ import android.app.AlertDialog;
import android.app.Dialog;
import android.app.IActivityManager;
import android.app.ProgressDialog;
-import android.bluetooth.BluetoothAdapter;
-import android.bluetooth.IBluetoothManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
@@ -114,7 +112,6 @@ public final class ShutdownThread extends Thread {
private static String METRIC_AM = "shutdown_activity_manager";
private static String METRIC_PM = "shutdown_package_manager";
private static String METRIC_RADIOS = "shutdown_radios";
- private static String METRIC_BT = "shutdown_bt";
private static String METRIC_RADIO = "shutdown_radio";
private final Object mActionDoneSync = new Object();
@@ -408,7 +405,7 @@ public final class ShutdownThread extends Thread {
/**
* Makes sure we handle the shutdown gracefully.
- * Shuts off power regardless of radio and bluetooth state if the alloted time has passed.
+ * Shuts off power regardless of radio state if the allotted time has passed.
*/
public void run() {
TimingsTraceLog shutdownTimingLog = newTimingsLog();
@@ -572,27 +569,10 @@ public final class ShutdownThread extends Thread {
Thread t = new Thread() {
public void run() {
TimingsTraceLog shutdownTimingsTraceLog = newTimingsLog();
- boolean bluetoothReadyForShutdown;
boolean radioOff;
final ITelephony phone =
ITelephony.Stub.asInterface(ServiceManager.checkService("phone"));
- final IBluetoothManager bluetooth =
- IBluetoothManager.Stub.asInterface(ServiceManager.checkService(
- BluetoothAdapter.BLUETOOTH_MANAGER_SERVICE));
-
- try {
- bluetoothReadyForShutdown = bluetooth == null ||
- bluetooth.getState() == BluetoothAdapter.STATE_OFF;
- if (!bluetoothReadyForShutdown) {
- Log.w(TAG, "Disabling Bluetooth...");
- metricStarted(METRIC_BT);
- bluetooth.disable(mContext.getPackageName(), false); // disable but don't persist new state
- }
- } catch (RemoteException ex) {
- Log.e(TAG, "RemoteException during bluetooth shutdown", ex);
- bluetoothReadyForShutdown = true;
- }
try {
radioOff = phone == null || !phone.needMobileRadioShutdown();
@@ -606,7 +586,7 @@ public final class ShutdownThread extends Thread {
radioOff = true;
}
- Log.i(TAG, "Waiting for Bluetooth and Radio...");
+ Log.i(TAG, "Waiting for Radio...");
long delay = endTime - SystemClock.elapsedRealtime();
while (delay > 0) {
@@ -617,25 +597,6 @@ public final class ShutdownThread extends Thread {
sInstance.setRebootProgress(status, null);
}
- if (!bluetoothReadyForShutdown) {
- try {
- // BLE only mode can happen when BT is turned off
- // We will continue shutting down in such case
- bluetoothReadyForShutdown =
- bluetooth.getState() == BluetoothAdapter.STATE_OFF ||
- bluetooth.getState() == BluetoothAdapter.STATE_BLE_TURNING_OFF ||
- bluetooth.getState() == BluetoothAdapter.STATE_BLE_ON;
- } catch (RemoteException ex) {
- Log.e(TAG, "RemoteException during bluetooth shutdown", ex);
- bluetoothReadyForShutdown = true;
- }
- if (bluetoothReadyForShutdown) {
- Log.i(TAG, "Bluetooth turned off.");
- metricEnded(METRIC_BT);
- shutdownTimingsTraceLog
- .logDuration("ShutdownBt", TRON_METRICS.get(METRIC_BT));
- }
- }
if (!radioOff) {
try {
radioOff = !phone.needMobileRadioShutdown();
@@ -651,8 +612,8 @@ public final class ShutdownThread extends Thread {
}
}
- if (radioOff && bluetoothReadyForShutdown) {
- Log.i(TAG, "Radio and Bluetooth shutdown complete.");
+ if (radioOff) {
+ Log.i(TAG, "Radio shutdown complete.");
done[0] = true;
break;
}
@@ -668,7 +629,7 @@ public final class ShutdownThread extends Thread {
} catch (InterruptedException ex) {
}
if (!done[0]) {
- Log.w(TAG, "Timed out waiting for Radio and Bluetooth shutdown.");
+ Log.w(TAG, "Timed out waiting for Radio shutdown.");
}
}
diff --git a/com/android/server/power/batterysaver/BatterySaverLocationPlugin.java b/com/android/server/power/batterysaver/BatterySaverLocationPlugin.java
index 0af19b6f..bd8baeb8 100644
--- a/com/android/server/power/batterysaver/BatterySaverLocationPlugin.java
+++ b/com/android/server/power/batterysaver/BatterySaverLocationPlugin.java
@@ -16,11 +16,11 @@
package com.android.server.power.batterysaver;
import android.content.Context;
+import android.os.PowerManager;
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 {
@@ -53,7 +53,7 @@ public class BatterySaverLocationPlugin implements Plugin {
private void updateLocationState(BatterySaverController caller) {
final boolean kill =
(caller.getBatterySaverPolicy().getGpsMode()
- == BatterySaverPolicy.GPS_MODE_ALL_DISABLED_WHEN_SCREEN_OFF) &&
+ == PowerManager.LOCATION_MODE_ALL_DISABLED_WHEN_SCREEN_OFF) &&
caller.isEnabled() && !caller.isInteractive();
if (DEBUG) {
diff --git a/com/android/server/print/PrintManagerService.java b/com/android/server/print/PrintManagerService.java
index 71ba685b..d6cc8051 100644
--- a/com/android/server/print/PrintManagerService.java
+++ b/com/android/server/print/PrintManagerService.java
@@ -21,6 +21,8 @@ import static android.content.pm.PackageManager.MATCH_DEBUG_TRIAGED_MISSING;
import android.annotation.NonNull;
import android.app.ActivityManager;
+import android.app.admin.DevicePolicyManager;
+import android.app.admin.DevicePolicyManagerInternal;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -32,6 +34,7 @@ import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
+import android.os.Looper;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -54,11 +57,15 @@ import android.service.print.PrintServiceDumpProto;
import android.util.Log;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
+import android.widget.Toast;
import com.android.internal.content.PackageMonitor;
import com.android.internal.os.BackgroundThread;
+import com.android.internal.print.DualDumpOutputStream;
import com.android.internal.util.DumpUtils;
+import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
+import com.android.server.LocalServices;
import com.android.server.SystemService;
import java.io.FileDescriptor;
@@ -108,9 +115,12 @@ public final class PrintManagerService extends SystemService {
private final SparseArray<UserState> mUserStates = new SparseArray<>();
+ private final DevicePolicyManager mDpm;
+
PrintManagerImpl(Context context) {
mContext = context;
mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+ mDpm = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
registerContentObservers();
registerBroadcastReceivers();
}
@@ -118,8 +128,35 @@ public final class PrintManagerService extends SystemService {
@Override
public Bundle print(String printJobName, IPrintDocumentAdapter adapter,
PrintAttributes attributes, String packageName, int appId, int userId) {
- printJobName = Preconditions.checkStringNotEmpty(printJobName);
adapter = Preconditions.checkNotNull(adapter);
+ if (!isPrintingEnabled()) {
+ CharSequence disabledMessage = null;
+ DevicePolicyManagerInternal dpmi =
+ LocalServices.getService(DevicePolicyManagerInternal.class);
+ final int callingUserId = UserHandle.getCallingUserId();
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ disabledMessage = dpmi.getPrintingDisabledReasonForUser(callingUserId);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ if (disabledMessage != null) {
+ Toast.makeText(mContext, Looper.getMainLooper(), disabledMessage,
+ Toast.LENGTH_LONG).show();
+ }
+ try {
+ adapter.start();
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error calling IPrintDocumentAdapter.start()");
+ }
+ try {
+ adapter.finish();
+ } catch (RemoteException re) {
+ Log.e(LOG_TAG, "Error calling IPrintDocumentAdapter.finish()");
+ }
+ return null;
+ }
+ printJobName = Preconditions.checkStringNotEmpty(printJobName);
packageName = Preconditions.checkStringNotEmpty(packageName);
final int resolvedUserId = resolveCallingUserEnforcingPermissions(userId);
@@ -238,7 +275,8 @@ public final class PrintManagerService extends SystemService {
@Override
public void restartPrintJob(PrintJobId printJobId, int appId, int userId) {
- if (printJobId == null) {
+ if (printJobId == null || !isPrintingEnabled()) {
+ // if printing is disabled the state just remains "failed".
return;
}
@@ -670,37 +708,33 @@ public final class PrintManagerService extends SystemService {
final long identity = Binder.clearCallingIdentity();
try {
if (dumpAsProto) {
- dump(new ProtoOutputStream(fd), userStatesToDump);
+ dump(new DualDumpOutputStream(new ProtoOutputStream(fd), null),
+ userStatesToDump);
} else {
- dump(fd, pw, userStatesToDump);
+ pw.println("PRINT MANAGER STATE (dumpsys print)");
+
+ dump(new DualDumpOutputStream(null, new IndentingPrintWriter(pw, " ")),
+ userStatesToDump);
}
} finally {
Binder.restoreCallingIdentity(identity);
}
}
- private void dump(@NonNull ProtoOutputStream proto,
- @NonNull ArrayList<UserState> userStatesToDump) {
- final int userStateCount = userStatesToDump.size();
- for (int i = 0; i < userStateCount; i++) {
- long token = proto.start(PrintServiceDumpProto.USER_STATES);
- userStatesToDump.get(i).dump(proto);
- proto.end(token);
- }
-
- proto.flush();
+ private boolean isPrintingEnabled() {
+ return mDpm == null || mDpm.isPrintingEnabled();
}
- private void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw,
+ private void dump(@NonNull DualDumpOutputStream dumpStream,
@NonNull ArrayList<UserState> userStatesToDump) {
- pw = Preconditions.checkNotNull(pw);
-
- pw.println("PRINT MANAGER STATE (dumpsys print)");
final int userStateCount = userStatesToDump.size();
for (int i = 0; i < userStateCount; i++) {
- userStatesToDump.get(i).dump(fd, pw, "");
- pw.println();
+ long token = dumpStream.start("user_states", PrintServiceDumpProto.USER_STATES);
+ userStatesToDump.get(i).dump(dumpStream);
+ dumpStream.end(token);
}
+
+ dumpStream.flush();
}
private void registerContentObservers() {
diff --git a/com/android/server/print/RemotePrintService.java b/com/android/server/print/RemotePrintService.java
index 13462cd3..80b97cf9 100644
--- a/com/android/server/print/RemotePrintService.java
+++ b/com/android/server/print/RemotePrintService.java
@@ -47,11 +47,10 @@ import android.printservice.IPrintService;
import android.printservice.IPrintServiceClient;
import android.service.print.ActivePrintServiceProto;
import android.util.Slog;
-import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.print.DualDumpOutputStream;
-import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@@ -532,49 +531,30 @@ final class RemotePrintService implements DeathRecipient {
}
}
- public void dump(@NonNull ProtoOutputStream proto) {
- writeComponentName(proto, ActivePrintServiceProto.COMPONENT_NAME, mComponentName);
+ public void dump(@NonNull DualDumpOutputStream proto) {
+ writeComponentName(proto, "component_name", ActivePrintServiceProto.COMPONENT_NAME,
+ mComponentName);
- proto.write(ActivePrintServiceProto.IS_DESTROYED, mDestroyed);
- proto.write(ActivePrintServiceProto.IS_BOUND, isBound());
- proto.write(ActivePrintServiceProto.HAS_DISCOVERY_SESSION, mHasPrinterDiscoverySession);
- proto.write(ActivePrintServiceProto.HAS_ACTIVE_PRINT_JOBS, mHasActivePrintJobs);
- proto.write(ActivePrintServiceProto.IS_DISCOVERING_PRINTERS,
+ proto.write("is_destroyed", ActivePrintServiceProto.IS_DESTROYED, mDestroyed);
+ proto.write("is_bound", ActivePrintServiceProto.IS_BOUND, isBound());
+ proto.write("has_discovery_session", ActivePrintServiceProto.HAS_DISCOVERY_SESSION,
+ mHasPrinterDiscoverySession);
+ proto.write("has_active_print_jobs", ActivePrintServiceProto.HAS_ACTIVE_PRINT_JOBS,
+ mHasActivePrintJobs);
+ proto.write("is_discovering_printers", ActivePrintServiceProto.IS_DISCOVERING_PRINTERS,
mDiscoveryPriorityList != null);
synchronized (mLock) {
if (mTrackedPrinterList != null) {
int numTrackedPrinters = mTrackedPrinterList.size();
for (int i = 0; i < numTrackedPrinters; i++) {
- writePrinterId(proto, ActivePrintServiceProto.TRACKED_PRINTERS,
- mTrackedPrinterList.get(i));
+ writePrinterId(proto, "tracked_printers",
+ ActivePrintServiceProto.TRACKED_PRINTERS, mTrackedPrinterList.get(i));
}
}
}
}
- public void dump(PrintWriter pw, String prefix) {
- String tab = " ";
- pw.append(prefix).append("service:").println();
- pw.append(prefix).append(tab).append("componentName=")
- .append(mComponentName.flattenToString()).println();
- pw.append(prefix).append(tab).append("destroyed=")
- .append(String.valueOf(mDestroyed)).println();
- pw.append(prefix).append(tab).append("bound=")
- .append(String.valueOf(isBound())).println();
- pw.append(prefix).append(tab).append("hasDicoverySession=")
- .append(String.valueOf(mHasPrinterDiscoverySession)).println();
- pw.append(prefix).append(tab).append("hasActivePrintJobs=")
- .append(String.valueOf(mHasActivePrintJobs)).println();
- pw.append(prefix).append(tab).append("isDiscoveringPrinters=")
- .append(String.valueOf(mDiscoveryPriorityList != null)).println();
-
- synchronized (mLock) {
- pw.append(prefix).append(tab).append("trackedPrinters=").append(
- (mTrackedPrinterList != null) ? mTrackedPrinterList.toString() : "null");
- }
- }
-
private boolean isBound() {
return mPrintService != null;
}
diff --git a/com/android/server/print/RemotePrintSpooler.java b/com/android/server/print/RemotePrintSpooler.java
index f654fcb6..a69baa11 100644
--- a/com/android/server/print/RemotePrintSpooler.java
+++ b/com/android/server/print/RemotePrintSpooler.java
@@ -43,16 +43,14 @@ import android.printservice.PrintService;
import android.service.print.PrintSpoolerStateProto;
import android.util.Slog;
import android.util.TimedRemoteCaller;
-import android.util.proto.ProtoOutputStream;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.os.TransferPipe;
+import com.android.internal.print.DualDumpOutputStream;
import libcore.io.IoUtils;
-import java.io.FileDescriptor;
import java.io.IOException;
-import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.List;
import java.util.concurrent.TimeoutException;
@@ -558,37 +556,25 @@ final class RemotePrintSpooler {
}
}
- public void dump(@NonNull ProtoOutputStream proto) {
+ public void dump(@NonNull DualDumpOutputStream dumpStream) {
synchronized (mLock) {
- proto.write(PrintSpoolerStateProto.IS_DESTROYED, mDestroyed);
- proto.write(PrintSpoolerStateProto.IS_BOUND, mRemoteInstance != null);
+ dumpStream.write("is_destroyed", PrintSpoolerStateProto.IS_DESTROYED, mDestroyed);
+ dumpStream.write("is_bound", PrintSpoolerStateProto.IS_BOUND, mRemoteInstance != null);
}
try {
- proto.write(PrintSpoolerStateProto.INTERNAL_STATE,
- TransferPipe.dumpAsync(getRemoteInstanceLazy().asBinder(), "--proto"));
+ if (dumpStream.isProto()) {
+ dumpStream.write(null, PrintSpoolerStateProto.INTERNAL_STATE,
+ TransferPipe.dumpAsync(getRemoteInstanceLazy().asBinder(), "--proto"));
+ } else {
+ dumpStream.writeNested("internal_state", TransferPipe.dumpAsync(
+ getRemoteInstanceLazy().asBinder()));
+ }
} catch (IOException | TimeoutException | RemoteException | InterruptedException e) {
Slog.e(LOG_TAG, "Failed to dump remote instance", e);
}
}
- public void dump(FileDescriptor fd, PrintWriter pw, String prefix) {
- synchronized (mLock) {
- pw.append(prefix).append("destroyed=")
- .append(String.valueOf(mDestroyed)).println();
- pw.append(prefix).append("bound=")
- .append((mRemoteInstance != null) ? "true" : "false").println();
-
- pw.flush();
- try {
- TransferPipe.dumpAsync(getRemoteInstanceLazy().asBinder(), fd,
- new String[] { prefix });
- } catch (IOException | TimeoutException | RemoteException | InterruptedException e) {
- pw.println("Failed to dump remote instance: " + e);
- }
- }
- }
-
private void onAllPrintJobsHandled() {
synchronized (mLock) {
throwIfDestroyedLocked();
diff --git a/com/android/server/print/UserState.java b/com/android/server/print/UserState.java
index 364bbc03..e2808e82 100644
--- a/com/android/server/print/UserState.java
+++ b/com/android/server/print/UserState.java
@@ -76,19 +76,17 @@ import android.util.ArraySet;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;
-import android.util.proto.ProtoOutputStream;
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.print.DualDumpOutputStream;
import com.android.server.print.RemotePrintService.PrintServiceCallbacks;
import com.android.server.print.RemotePrintServiceRecommendationService
.RemotePrintServiceRecommendationServiceCallbacks;
import com.android.server.print.RemotePrintSpooler.PrintSpoolerCallbacks;
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
@@ -817,112 +815,63 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
mDestroyed = true;
}
- public void dump(@NonNull ProtoOutputStream proto) {
+ public void dump(@NonNull DualDumpOutputStream dumpStream) {
synchronized (mLock) {
- proto.write(PrintUserStateProto.USER_ID, mUserId);
+ dumpStream.write("user_id", PrintUserStateProto.USER_ID, mUserId);
final int installedServiceCount = mInstalledServices.size();
for (int i = 0; i < installedServiceCount; i++) {
- long token = proto.start(PrintUserStateProto.INSTALLED_SERVICES);
+ long token = dumpStream.start("installed_services",
+ PrintUserStateProto.INSTALLED_SERVICES);
PrintServiceInfo installedService = mInstalledServices.get(i);
ResolveInfo resolveInfo = installedService.getResolveInfo();
- writeComponentName(proto, InstalledPrintServiceProto.COMPONENT_NAME,
+ writeComponentName(dumpStream, "component_name",
+ InstalledPrintServiceProto.COMPONENT_NAME,
new ComponentName(resolveInfo.serviceInfo.packageName,
resolveInfo.serviceInfo.name));
- writeStringIfNotNull(proto, InstalledPrintServiceProto.SETTINGS_ACTIVITY,
+ writeStringIfNotNull(dumpStream, "settings_activity",
+ InstalledPrintServiceProto.SETTINGS_ACTIVITY,
installedService.getSettingsActivityName());
- writeStringIfNotNull(proto, InstalledPrintServiceProto.ADD_PRINTERS_ACTIVITY,
+ writeStringIfNotNull(dumpStream, "add_printers_activity",
+ InstalledPrintServiceProto.ADD_PRINTERS_ACTIVITY,
installedService.getAddPrintersActivityName());
- writeStringIfNotNull(proto, InstalledPrintServiceProto.ADVANCED_OPTIONS_ACTIVITY,
+ writeStringIfNotNull(dumpStream, "advanced_options_activity",
+ InstalledPrintServiceProto.ADVANCED_OPTIONS_ACTIVITY,
installedService.getAdvancedOptionsActivityName());
- proto.end(token);
+ dumpStream.end(token);
}
for (ComponentName disabledService : mDisabledServices) {
- writeComponentName(proto, PrintUserStateProto.DISABLED_SERVICES, disabledService);
+ writeComponentName(dumpStream, "disabled_services",
+ PrintUserStateProto.DISABLED_SERVICES, disabledService);
}
final int activeServiceCount = mActiveServices.size();
for (int i = 0; i < activeServiceCount; i++) {
- long token = proto.start(PrintUserStateProto.ACTIVE_SERVICES);
- mActiveServices.valueAt(i).dump(proto);
- proto.end(token);
+ long token = dumpStream.start("actives_services",
+ PrintUserStateProto.ACTIVE_SERVICES);
+ mActiveServices.valueAt(i).dump(dumpStream);
+ dumpStream.end(token);
}
- mPrintJobForAppCache.dumpLocked(proto);
+ mPrintJobForAppCache.dumpLocked(dumpStream);
if (mPrinterDiscoverySession != null) {
- long token = proto.start(PrintUserStateProto.DISCOVERY_SESSIONS);
- mPrinterDiscoverySession.dumpLocked(proto);
- proto.end(token);
+ long token = dumpStream.start("discovery_service",
+ PrintUserStateProto.DISCOVERY_SESSIONS);
+ mPrinterDiscoverySession.dumpLocked(dumpStream);
+ dumpStream.end(token);
}
}
- long token = proto.start(PrintUserStateProto.PRINT_SPOOLER_STATE);
- mSpooler.dump(proto);
- proto.end(token);
- }
-
- public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String prefix) {
- pw.append(prefix).append("user state ").append(String.valueOf(mUserId)).append(":");
- pw.println();
-
- String tab = " ";
-
- synchronized (mLock) {
- pw.append(prefix).append(tab).append("installed services:").println();
- final int installedServiceCount = mInstalledServices.size();
- for (int i = 0; i < installedServiceCount; i++) {
- PrintServiceInfo installedService = mInstalledServices.get(i);
- String installedServicePrefix = prefix + tab + tab;
- pw.append(installedServicePrefix).append("service:").println();
- ResolveInfo resolveInfo = installedService.getResolveInfo();
- ComponentName componentName = new ComponentName(
- resolveInfo.serviceInfo.packageName,
- resolveInfo.serviceInfo.name);
- pw.append(installedServicePrefix).append(tab).append("componentName=")
- .append(componentName.flattenToString()).println();
- pw.append(installedServicePrefix).append(tab).append("settingsActivity=")
- .append(installedService.getSettingsActivityName()).println();
- pw.append(installedServicePrefix).append(tab).append("addPrintersActivity=")
- .append(installedService.getAddPrintersActivityName()).println();
- pw.append(installedServicePrefix).append(tab).append("avancedOptionsActivity=")
- .append(installedService.getAdvancedOptionsActivityName()).println();
- }
-
- pw.append(prefix).append(tab).append("disabled services:").println();
- for (ComponentName disabledService : mDisabledServices) {
- String disabledServicePrefix = prefix + tab + tab;
- pw.append(disabledServicePrefix).append("service:").println();
- pw.append(disabledServicePrefix).append(tab).append("componentName=")
- .append(disabledService.flattenToString());
- pw.println();
- }
-
- pw.append(prefix).append(tab).append("active services:").println();
- final int activeServiceCount = mActiveServices.size();
- for (int i = 0; i < activeServiceCount; i++) {
- RemotePrintService activeService = mActiveServices.valueAt(i);
- activeService.dump(pw, prefix + tab + tab);
- pw.println();
- }
-
- pw.append(prefix).append(tab).append("cached print jobs:").println();
- mPrintJobForAppCache.dumpLocked(pw, prefix + tab + tab);
-
- pw.append(prefix).append(tab).append("discovery mediator:").println();
- if (mPrinterDiscoverySession != null) {
- mPrinterDiscoverySession.dumpLocked(pw, prefix + tab + tab);
- }
- }
-
- pw.append(prefix).append(tab).append("print spooler:").println();
- mSpooler.dump(fd, pw, prefix + tab + tab);
- pw.println();
+ long token = dumpStream.start("print_spooler_state",
+ PrintUserStateProto.PRINT_SPOOLER_STATE);
+ mSpooler.dump(dumpStream);
+ dumpStream.end(token);
}
private void readConfigurationLocked() {
@@ -1650,15 +1599,17 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
}
}
- public void dumpLocked(@NonNull ProtoOutputStream proto) {
- proto.write(PrinterDiscoverySessionProto.IS_DESTROYED, mDestroyed);
- proto.write(PrinterDiscoverySessionProto.IS_PRINTER_DISCOVERY_IN_PROGRESS,
+ public void dumpLocked(@NonNull DualDumpOutputStream dumpStream) {
+ dumpStream.write("is_destroyed", PrinterDiscoverySessionProto.IS_DESTROYED, mDestroyed);
+ dumpStream.write("is_printer_discovery_in_progress",
+ PrinterDiscoverySessionProto.IS_PRINTER_DISCOVERY_IN_PROGRESS,
!mStartedPrinterDiscoveryTokens.isEmpty());
final int observerCount = mDiscoveryObservers.beginBroadcast();
for (int i = 0; i < observerCount; i++) {
IPrinterDiscoveryObserver observer = mDiscoveryObservers.getBroadcastItem(i);
- proto.write(PrinterDiscoverySessionProto.PRINTER_DISCOVERY_OBSERVERS,
+ dumpStream.write("printer_discovery_observers",
+ PrinterDiscoverySessionProto.PRINTER_DISCOVERY_OBSERVERS,
observer.toString());
}
mDiscoveryObservers.finishBroadcast();
@@ -1666,61 +1617,22 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
final int tokenCount = this.mStartedPrinterDiscoveryTokens.size();
for (int i = 0; i < tokenCount; i++) {
IBinder token = mStartedPrinterDiscoveryTokens.get(i);
- proto.write(PrinterDiscoverySessionProto.DISCOVERY_REQUESTS, token.toString());
+ dumpStream.write("discovery_requests",
+ PrinterDiscoverySessionProto.DISCOVERY_REQUESTS, token.toString());
}
final int trackedPrinters = mStateTrackedPrinters.size();
for (int i = 0; i < trackedPrinters; i++) {
PrinterId printer = mStateTrackedPrinters.get(i);
- writePrinterId(proto, PrinterDiscoverySessionProto.TRACKED_PRINTER_REQUESTS,
- printer);
+ writePrinterId(dumpStream, "tracked_printer_requests",
+ PrinterDiscoverySessionProto.TRACKED_PRINTER_REQUESTS, printer);
}
final int printerCount = mPrinters.size();
for (int i = 0; i < printerCount; i++) {
PrinterInfo printer = mPrinters.valueAt(i);
- writePrinterInfo(mContext, proto, PrinterDiscoverySessionProto.PRINTER, printer);
- }
- }
-
- public void dumpLocked(PrintWriter pw, String prefix) {
- pw.append(prefix).append("destroyed=")
- .append(String.valueOf(mDestroyed)).println();
-
- pw.append(prefix).append("printDiscoveryInProgress=")
- .append(String.valueOf(!mStartedPrinterDiscoveryTokens.isEmpty())).println();
-
- String tab = " ";
-
- pw.append(prefix).append(tab).append("printer discovery observers:").println();
- final int observerCount = mDiscoveryObservers.beginBroadcast();
- for (int i = 0; i < observerCount; i++) {
- IPrinterDiscoveryObserver observer = mDiscoveryObservers.getBroadcastItem(i);
- pw.append(prefix).append(prefix).append(observer.toString());
- pw.println();
- }
- mDiscoveryObservers.finishBroadcast();
-
- pw.append(prefix).append(tab).append("start discovery requests:").println();
- final int tokenCount = this.mStartedPrinterDiscoveryTokens.size();
- for (int i = 0; i < tokenCount; i++) {
- IBinder token = mStartedPrinterDiscoveryTokens.get(i);
- pw.append(prefix).append(tab).append(tab).append(token.toString()).println();
- }
-
- pw.append(prefix).append(tab).append("tracked printer requests:").println();
- final int trackedPrinters = mStateTrackedPrinters.size();
- for (int i = 0; i < trackedPrinters; i++) {
- PrinterId printer = mStateTrackedPrinters.get(i);
- pw.append(prefix).append(tab).append(tab).append(printer.toString()).println();
- }
-
- pw.append(prefix).append(tab).append("printers:").println();
- final int pritnerCount = mPrinters.size();
- for (int i = 0; i < pritnerCount; i++) {
- PrinterInfo printer = mPrinters.valueAt(i);
- pw.append(prefix).append(tab).append(tab).append(
- printer.toString()).println();
+ writePrinterInfo(mContext, dumpStream, "printer",
+ PrinterDiscoverySessionProto.PRINTER, printer);
}
}
@@ -1933,36 +1845,22 @@ final class UserState implements PrintSpoolerCallbacks, PrintServiceCallbacks,
}
}
- public void dumpLocked(PrintWriter pw, String prefix) {
- String tab = " ";
- final int bucketCount = mPrintJobsForRunningApp.size();
- for (int i = 0; i < bucketCount; i++) {
- final int appId = mPrintJobsForRunningApp.keyAt(i);
- pw.append(prefix).append("appId=" + appId).append(':').println();
- List<PrintJobInfo> bucket = mPrintJobsForRunningApp.valueAt(i);
- final int printJobCount = bucket.size();
- for (int j = 0; j < printJobCount; j++) {
- PrintJobInfo printJob = bucket.get(j);
- pw.append(prefix).append(tab).append(printJob.toString()).println();
- }
- }
- }
-
- public void dumpLocked(@NonNull ProtoOutputStream proto) {
+ public void dumpLocked(@NonNull DualDumpOutputStream dumpStream) {
final int bucketCount = mPrintJobsForRunningApp.size();
for (int i = 0; i < bucketCount; i++) {
final int appId = mPrintJobsForRunningApp.keyAt(i);
List<PrintJobInfo> bucket = mPrintJobsForRunningApp.valueAt(i);
final int printJobCount = bucket.size();
for (int j = 0; j < printJobCount; j++) {
- long token = proto.start(PrintUserStateProto.CACHED_PRINT_JOBS);
+ long token = dumpStream.start("cached_print_jobs",
+ PrintUserStateProto.CACHED_PRINT_JOBS);
- proto.write(CachedPrintJobProto.APP_ID, appId);
+ dumpStream.write("app_id", CachedPrintJobProto.APP_ID, appId);
- writePrintJobInfo(mContext, proto, CachedPrintJobProto.PRINT_JOB,
- bucket.get(j));
+ writePrintJobInfo(mContext, dumpStream, "print_job",
+ CachedPrintJobProto.PRINT_JOB, bucket.get(j));
- proto.end(token);
+ dumpStream.end(token);
}
}
}
diff --git a/com/android/server/security/VerityUtils.java b/com/android/server/security/VerityUtils.java
new file mode 100644
index 00000000..3908df46
--- /dev/null
+++ b/com/android/server/security/VerityUtils.java
@@ -0,0 +1,184 @@
+/*
+ * 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 com.android.server.security;
+
+import static android.system.OsConstants.PROT_READ;
+import static android.system.OsConstants.PROT_WRITE;
+
+import android.annotation.NonNull;
+import android.os.SharedMemory;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.apk.ApkSignatureVerifier;
+import android.util.apk.ByteBufferFactory;
+import android.util.apk.SignatureNotFoundException;
+import android.util.Slog;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.security.DigestException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+
+/** Provides fsverity related operations. */
+abstract public class VerityUtils {
+ private static final String TAG = "VerityUtils";
+
+ private static final boolean DEBUG = false;
+
+ /**
+ * Generates Merkle tree and fsverity metadata.
+ *
+ * @return {@code SetupResult} that contains the {@code EsetupResultCode}, and when success, the
+ * {@code FileDescriptor} to read all the data from.
+ */
+ public static SetupResult generateApkVeritySetupData(@NonNull String apkPath) {
+ if (DEBUG) Slog.d(TAG, "Trying to install apk verity to " + apkPath);
+ SharedMemory shm = null;
+ try {
+ byte[] signedRootHash = ApkSignatureVerifier.getVerityRootHash(apkPath);
+ if (signedRootHash == null) {
+ if (DEBUG) {
+ Slog.d(TAG, "Skip verity tree generation since there is no root hash");
+ }
+ return SetupResult.skipped();
+ }
+
+ shm = generateApkVerityIntoSharedMemory(apkPath, signedRootHash);
+ FileDescriptor rfd = shm.getFileDescriptor();
+ if (rfd == null || !rfd.valid()) {
+ return SetupResult.failed();
+ }
+ return SetupResult.ok(Os.dup(rfd));
+ } catch (IOException | SecurityException | DigestException | NoSuchAlgorithmException |
+ SignatureNotFoundException | ErrnoException e) {
+ Slog.e(TAG, "Failed to set up apk verity: ", e);
+ return SetupResult.failed();
+ } finally {
+ if (shm != null) {
+ shm.close();
+ }
+ }
+ }
+
+ /**
+ * Returns a {@code SharedMemory} that contains Merkle tree and fsverity headers for the given
+ * apk, in the form that can immediately be used for fsverity setup.
+ */
+ private static SharedMemory generateApkVerityIntoSharedMemory(
+ String apkPath, byte[] expectedRootHash)
+ throws IOException, SecurityException, DigestException, NoSuchAlgorithmException,
+ SignatureNotFoundException {
+ TrackedShmBufferFactory shmBufferFactory = new TrackedShmBufferFactory();
+ byte[] generatedRootHash = ApkSignatureVerifier.generateApkVerity(apkPath,
+ shmBufferFactory);
+ // We only generate Merkle tree once here, so it's important to make sure the root hash
+ // matches the signed one in the apk.
+ if (!Arrays.equals(expectedRootHash, generatedRootHash)) {
+ throw new SecurityException("Locally generated verity root hash does not match");
+ }
+
+ SharedMemory shm = shmBufferFactory.releaseSharedMemory();
+ if (shm == null) {
+ throw new IllegalStateException("Failed to generate verity tree into shared memory");
+ }
+ if (!shm.setProtect(PROT_READ)) {
+ throw new SecurityException("Failed to set up shared memory correctly");
+ }
+ return shm;
+ }
+
+ public static class SetupResult {
+ /** Result code if verity is set up correctly. */
+ private static final int RESULT_OK = 1;
+
+ /** Result code if the apk does not contain a verity root hash. */
+ private static final int RESULT_SKIPPED = 2;
+
+ /** Result code if the setup failed. */
+ private static final int RESULT_FAILED = 3;
+
+ private final int mCode;
+ private final FileDescriptor mFileDescriptor;
+
+ public static SetupResult ok(@NonNull FileDescriptor fileDescriptor) {
+ return new SetupResult(RESULT_OK, fileDescriptor);
+ }
+
+ public static SetupResult skipped() {
+ return new SetupResult(RESULT_SKIPPED, null);
+ }
+
+ public static SetupResult failed() {
+ return new SetupResult(RESULT_FAILED, null);
+ }
+
+ private SetupResult(int code, FileDescriptor fileDescriptor) {
+ this.mCode = code;
+ this.mFileDescriptor = fileDescriptor;
+ }
+
+ public boolean isFailed() {
+ return mCode == RESULT_FAILED;
+ }
+
+ public boolean isOk() {
+ return mCode == RESULT_OK;
+ }
+
+ public @NonNull FileDescriptor getUnownedFileDescriptor() {
+ return mFileDescriptor;
+ }
+ }
+
+ /** A {@code ByteBufferFactory} that creates a shared memory backed {@code ByteBuffer}. */
+ private static class TrackedShmBufferFactory implements ByteBufferFactory {
+ private SharedMemory mShm;
+ private ByteBuffer mBuffer;
+
+ @Override
+ public ByteBuffer create(int capacity) throws SecurityException {
+ try {
+ if (DEBUG) Slog.d(TAG, "Creating shared memory for apk verity");
+ // NB: This method is supposed to be called once according to the contract with
+ // ApkSignatureSchemeV2Verifier.
+ if (mBuffer != null) {
+ throw new IllegalStateException("Multiple instantiation from this factory");
+ }
+ mShm = SharedMemory.create("apkverity", capacity);
+ if (!mShm.setProtect(PROT_READ | PROT_WRITE)) {
+ throw new SecurityException("Failed to set protection");
+ }
+ mBuffer = mShm.mapReadWrite();
+ return mBuffer;
+ } catch (ErrnoException e) {
+ throw new SecurityException("Failed to set protection", e);
+ }
+ }
+
+ public SharedMemory releaseSharedMemory() {
+ if (mBuffer != null) {
+ SharedMemory.unmap(mBuffer);
+ mBuffer = null;
+ }
+ SharedMemory tmp = mShm;
+ mShm = null;
+ return tmp;
+ }
+ }
+}
diff --git a/com/android/server/slice/PinnedSliceState.java b/com/android/server/slice/PinnedSliceState.java
index cf930f5c..5811714c 100644
--- a/com/android/server/slice/PinnedSliceState.java
+++ b/com/android/server/slice/PinnedSliceState.java
@@ -21,7 +21,10 @@ import android.app.slice.SliceSpec;
import android.content.ContentProviderClient;
import android.net.Uri;
import android.os.Bundle;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
import android.os.RemoteException;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
@@ -48,9 +51,13 @@ public class PinnedSliceState {
@GuardedBy("mLock")
private final ArraySet<String> mPinnedPkgs = new ArraySet<>();
@GuardedBy("mLock")
- private final ArraySet<ISliceListener> mListeners = new ArraySet<>();
+ private final ArrayMap<IBinder, ISliceListener> mListeners = new ArrayMap<>();
@GuardedBy("mLock")
private SliceSpec[] mSupportedSpecs = null;
+ @GuardedBy("mLock")
+ private final ArrayMap<IBinder, String> mPkgMap = new ArrayMap<>();
+
+ private final DeathRecipient mDeathRecipient = this::handleRecheckListeners;
public PinnedSliceState(SliceManagerService service, Uri uri) {
mService = service;
@@ -102,20 +109,29 @@ public class PinnedSliceState {
mService.getHandler().post(this::handleBind);
}
- public void addSliceListener(ISliceListener listener, SliceSpec[] specs) {
+ public void addSliceListener(ISliceListener listener, String pkg, SliceSpec[] specs) {
synchronized (mLock) {
- if (mListeners.add(listener) && mListeners.size() == 1) {
+ if (mListeners.size() == 0) {
mService.listen(mUri);
}
+ try {
+ listener.asBinder().linkToDeath(mDeathRecipient, 0);
+ } catch (RemoteException e) {
+ }
+ mListeners.put(listener.asBinder(), listener);
+ mPkgMap.put(listener.asBinder(), pkg);
mergeSpecs(specs);
}
}
public boolean removeSliceListener(ISliceListener listener) {
synchronized (mLock) {
- if (mListeners.remove(listener) && mListeners.size() == 0) {
+ listener.asBinder().unlinkToDeath(mDeathRecipient, 0);
+ mPkgMap.remove(listener.asBinder());
+ if (mListeners.containsKey(listener.asBinder()) && mListeners.size() == 1) {
mService.unlisten(mUri);
}
+ mListeners.remove(listener.asBinder());
}
return !isPinned();
}
@@ -154,38 +170,68 @@ public class PinnedSliceState {
return client;
}
+ private void handleRecheckListeners() {
+ if (!isPinned()) return;
+ synchronized (mLock) {
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ ISliceListener l = mListeners.valueAt(i);
+ if (!l.asBinder().isBinderAlive()) {
+ mListeners.removeAt(i);
+ }
+ }
+ if (!isPinned()) {
+ // All the listeners died, remove from pinned state.
+ mService.removePinnedSlice(mUri);
+ }
+ }
+ }
+
private void handleBind() {
- Slice s;
+ Slice cachedSlice = doBind(null);
+ synchronized (mLock) {
+ if (!isPinned()) return;
+ for (int i = mListeners.size() - 1; i >= 0; i--) {
+ ISliceListener l = mListeners.valueAt(i);
+ Slice s = cachedSlice;
+ if (s == null || s.hasHint(Slice.HINT_CALLER_NEEDED)) {
+ s = doBind(mPkgMap.get(l));
+ }
+ if (s == null) {
+ mListeners.removeAt(i);
+ continue;
+ }
+ try {
+ l.onSliceUpdated(s);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to notify slice " + mUri, e);
+ mListeners.removeAt(i);
+ continue;
+ }
+ }
+ if (!isPinned()) {
+ // All the listeners died, remove from pinned state.
+ mService.removePinnedSlice(mUri);
+ }
+ }
+ }
+
+ private Slice doBind(String overridePkg) {
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)));
+ extras.putString(SliceProvider.EXTRA_OVERRIDE_PKG, overridePkg);
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;
+ return null;
}
- if (res == null) return;
+ if (res == null) return null;
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);
- }
+ return res.getParcelable(SliceProvider.EXTRA_SLICE);
}
}
diff --git a/com/android/server/slice/SliceManagerService.java b/com/android/server/slice/SliceManagerService.java
index 2d9e772a..c1915801 100644
--- a/com/android/server/slice/SliceManagerService.java
+++ b/com/android/server/slice/SliceManagerService.java
@@ -16,27 +16,35 @@
package com.android.server.slice;
+import static android.content.ContentProvider.getUriWithoutUserId;
import static android.content.ContentProvider.getUserIdFromUri;
import static android.content.ContentProvider.maybeAddUserId;
import android.Manifest.permission;
+import android.app.ActivityManager;
import android.app.AppOpsManager;
+import android.app.ContentProviderHolder;
+import android.app.IActivityManager;
import android.app.slice.ISliceListener;
import android.app.slice.ISliceManager;
+import android.app.slice.SliceManager;
import android.app.slice.SliceSpec;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
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.IBinder;
import android.os.Looper;
import android.os.Process;
import android.os.RemoteException;
import android.util.ArrayMap;
+import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
@@ -63,6 +71,8 @@ public class SliceManagerService extends ISliceManager.Stub {
@GuardedBy("mLock")
private final ArrayMap<Uri, PinnedSliceState> mPinnedSlicesByUri = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private final ArraySet<SliceGrant> mUserGrants = new ArraySet<>();
private final Handler mHandler;
private final ContentObserver mObserver;
@@ -81,9 +91,9 @@ public class SliceManagerService extends ISliceManager.Stub {
mObserver = new ContentObserver(mHandler) {
@Override
- public void onChange(boolean selfChange, Uri uri) {
+ public void onChange(boolean selfChange, Uri uri, int userId) {
try {
- getPinnedSlice(uri).onChange();
+ getPinnedSlice(maybeAddUserId(uri, userId)).onChange();
} catch (IllegalStateException e) {
Log.e(TAG, "Received change for unpinned slice " + uri, e);
}
@@ -111,7 +121,7 @@ public class SliceManagerService extends ISliceManager.Stub {
verifyCaller(pkg);
uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier());
enforceAccess(pkg, uri);
- getOrCreatePinnedSlice(uri).addSliceListener(listener, specs);
+ getOrCreatePinnedSlice(uri).addSliceListener(listener, pkg, specs);
}
@Override
@@ -156,8 +166,45 @@ public class SliceManagerService extends ISliceManager.Stub {
return getPinnedSlice(uri).getSpecs();
}
+ @Override
+ public int checkSlicePermission(Uri uri, String pkg, int pid, int uid) throws RemoteException {
+ if (mContext.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
+ == PackageManager.PERMISSION_GRANTED) {
+ return SliceManager.PERMISSION_GRANTED;
+ }
+ if (hasFullSliceAccess(pkg, uid)) {
+ return SliceManager.PERMISSION_GRANTED;
+ }
+ synchronized (mLock) {
+ if (mUserGrants.contains(new SliceGrant(uri, pkg))) {
+ return SliceManager.PERMISSION_USER_GRANTED;
+ }
+ }
+ return SliceManager.PERMISSION_DENIED;
+ }
+
+ @Override
+ public void grantPermissionFromUser(Uri uri, String pkg, String callingPkg, boolean allSlices) {
+ verifyCaller(callingPkg);
+ getContext().enforceCallingOrSelfPermission(permission.MANAGE_SLICE_PERMISSIONS,
+ "Slice granting requires MANAGE_SLICE_PERMISSIONS");
+ if (allSlices) {
+ // TODO: Manage full access grants.
+ } else {
+ synchronized (mLock) {
+ mUserGrants.add(new SliceGrant(uri, pkg));
+ }
+ long ident = Binder.clearCallingIdentity();
+ try {
+ mContext.getContentResolver().notifyChange(uri, null);
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+
/// ----- internal code -----
- void removePinnedSlice(Uri uri) {
+ protected void removePinnedSlice(Uri uri) {
synchronized (mLock) {
mPinnedSlicesByUri.remove(uri).destroy();
}
@@ -186,7 +233,7 @@ public class SliceManagerService extends ISliceManager.Stub {
}
@VisibleForTesting
- PinnedSliceState createPinnedSlice(Uri uri) {
+ protected PinnedSliceState createPinnedSlice(Uri uri) {
return new PinnedSliceState(this, uri);
}
@@ -202,12 +249,45 @@ public class SliceManagerService extends ISliceManager.Stub {
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");
+ private void enforceAccess(String pkg, Uri uri) throws RemoteException {
int user = Binder.getCallingUserHandle().getIdentifier();
+ // Check for default launcher/assistant.
+ if (!hasFullSliceAccess(pkg, Binder.getCallingUid())) {
+ try {
+ // Also allow things with uri access.
+ getContext().enforceUriPermission(uri, Binder.getCallingPid(),
+ Binder.getCallingUid(),
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
+ "Slice binding requires permission to the Uri");
+ } catch (SecurityException e) {
+ // Last fallback (if the calling app owns the authority, then it can have access).
+ long ident = Binder.clearCallingIdentity();
+ try {
+ IBinder token = new Binder();
+ IActivityManager activityManager = ActivityManager.getService();
+ ContentProviderHolder holder = null;
+ String providerName = getUriWithoutUserId(uri).getAuthority();
+ try {
+ holder = activityManager.getContentProviderExternal(
+ providerName, getUserIdFromUri(uri, user), token);
+ if (holder == null || holder.info == null
+ || !Objects.equals(holder.info.packageName, pkg)) {
+ // No more fallbacks, no access.
+ throw e;
+ }
+ } finally {
+ if (holder != null && holder.provider != null) {
+ activityManager.removeContentProviderExternal(providerName, token);
+ }
+ }
+ } finally {
+ // I know, the double finally seems ugly, but seems safest for the identity.
+ Binder.restoreCallingIdentity(ident);
+ }
+ }
+ }
+ // Lastly check for any multi-userness. Any return statements above here will break this
+ // important check.
if (getUserIdFromUri(uri, user) != user) {
getContext().enforceCallingOrSelfPermission(permission.INTERACT_ACROSS_USERS_FULL,
"Slice interaction across users requires INTERACT_ACROSS_USERS_FULL");
@@ -230,8 +310,14 @@ public class SliceManagerService extends ISliceManager.Stub {
}
private boolean hasFullSliceAccess(String pkg, int userId) {
- return isDefaultHomeApp(pkg, userId) || isAssistant(pkg, userId)
- || isGrantedFullAccess(pkg, userId);
+ long ident = Binder.clearCallingIdentity();
+ try {
+ boolean ret = isDefaultHomeApp(pkg, userId) || isAssistant(pkg, userId)
+ || isGrantedFullAccess(pkg, userId);
+ return ret;
+ } finally {
+ Binder.restoreCallingIdentity(ident);
+ }
}
private boolean isAssistant(String pkg, int userId) {
@@ -259,13 +345,14 @@ public class SliceManagerService extends ISliceManager.Stub {
private boolean isDefaultHomeApp(String pkg, int userId) {
String defaultHome = getDefaultHome(userId);
- return Objects.equals(pkg, defaultHome);
+
+ return pkg != null && Objects.equals(pkg, defaultHome);
}
// Based on getDefaultHome in ShortcutService.
// TODO: Unify if possible
@VisibleForTesting
- String getDefaultHome(int userId) {
+ protected String getDefaultHome(int userId) {
final long token = Binder.clearCallingIdentity();
try {
final List<ResolveInfo> allHomeCandidates = new ArrayList<>();
@@ -301,7 +388,7 @@ public class SliceManagerService extends ISliceManager.Stub {
lastPriority = ri.priority;
}
}
- return detected.getPackageName();
+ return detected != null ? detected.getPackageName() : null;
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -349,4 +436,26 @@ public class SliceManagerService extends ISliceManager.Stub {
mService.onStopUser(userHandle);
}
}
+
+ private class SliceGrant {
+ private final Uri mUri;
+ private final String mPkg;
+
+ public SliceGrant(Uri uri, String pkg) {
+ mUri = uri;
+ mPkg = pkg;
+ }
+
+ @Override
+ public int hashCode() {
+ return mUri.hashCode() + mPkg.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof SliceGrant)) return false;
+ SliceGrant other = (SliceGrant) obj;
+ return Objects.equals(other.mUri, mUri) && Objects.equals(other.mPkg, mPkg);
+ }
+ }
}
diff --git a/com/android/server/stats/StatsCompanionService.java b/com/android/server/stats/StatsCompanionService.java
index b31f4b3f..baea964f 100644
--- a/com/android/server/stats/StatsCompanionService.java
+++ b/com/android/server/stats/StatsCompanionService.java
@@ -18,21 +18,23 @@ package com.android.server.stats;
import android.annotation.Nullable;
import android.app.AlarmManager;
import android.app.PendingIntent;
+import android.app.StatsManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.IntentSender;
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.StatsDimensionsValue;
import android.os.BatteryStatsInternal;
import android.os.Binder;
import android.os.Bundle;
+import android.os.Environment;
import android.os.IBinder;
import android.os.IStatsCompanionService;
import android.os.IStatsManager;
@@ -40,18 +42,22 @@ import android.os.Parcelable;
import android.os.Process;
import android.os.RemoteException;
import android.os.ServiceManager;
+import android.os.StatFs;
import android.os.StatsLogEventWrapper;
import android.os.SynchronousResultReceiver;
+import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
+import android.telephony.ModemActivityInfo;
+import android.telephony.TelephonyManager;
import android.util.Slog;
import android.util.StatsLog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.net.NetworkStatsFactory;
+import com.android.internal.os.KernelCpuSpeedReader;
import com.android.internal.os.KernelWakelockReader;
import com.android.internal.os.KernelWakelockStats;
-import com.android.internal.os.KernelCpuSpeedReader;
import com.android.internal.os.PowerProfile;
import com.android.server.LocalServices;
import com.android.server.SystemService;
@@ -77,9 +83,12 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
static final String TAG = "StatsCompanionService";
static final boolean DEBUG = true;
+
public static final String ACTION_TRIGGER_COLLECTION =
"com.android.server.stats.action.TRIGGER_COLLECTION";
+ public static final int CODE_SUBSCRIBER_BROADCAST = 1;
+
private final Context mContext;
private final AlarmManager mAlarmManager;
@GuardedBy("sStatsdLock")
@@ -96,6 +105,11 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
private final KernelCpuSpeedReader[] mKernelCpuSpeedReaders;
private IWifiManager mWifiManager = null;
private TelephonyManager mTelephony = null;
+ private final StatFs mStatFsData = new StatFs(Environment.getDataDirectory().getAbsolutePath());
+ private final StatFs mStatFsSystem =
+ new StatFs(Environment.getRootDirectory().getAbsolutePath());
+ private final StatFs mStatFsTemp =
+ new StatFs(Environment.getDownloadCacheDirectory().getAbsolutePath());
public StatsCompanionService(Context context) {
super();
@@ -143,10 +157,37 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
@Override
public void sendBroadcast(String pkg, String cls) {
+ // TODO: Use a pending intent, and enfoceCallingPermission.
mContext.sendBroadcastAsUser(new Intent(ACTION_TRIGGER_COLLECTION).setClassName(pkg, cls),
UserHandle.SYSTEM);
}
+ @Override
+ public void sendSubscriberBroadcast(IBinder intentSenderBinder, long configUid, long configKey,
+ long subscriptionId, long subscriptionRuleId,
+ StatsDimensionsValue dimensionsValue) {
+ if (DEBUG) Slog.d(TAG, "Statsd requested to sendSubscriberBroadcast.");
+ enforceCallingPermission();
+ IntentSender intentSender = new IntentSender(intentSenderBinder);
+ Intent intent = new Intent()
+ .putExtra(StatsManager.EXTRA_STATS_CONFIG_UID, configUid)
+ .putExtra(StatsManager.EXTRA_STATS_CONFIG_KEY, configKey)
+ .putExtra(StatsManager.EXTRA_STATS_SUBSCRIPTION_ID, subscriptionId)
+ .putExtra(StatsManager.EXTRA_STATS_SUBSCRIPTION_RULE_ID, subscriptionRuleId)
+ .putExtra(StatsManager.EXTRA_STATS_DIMENSIONS_VALUE, dimensionsValue);
+ try {
+ intentSender.sendIntent(mContext, CODE_SUBSCRIBER_BROADCAST, intent, null, null);
+ } catch (IntentSender.SendIntentException e) {
+ Slog.w(TAG, "Unable to send using IntentSender from uid " + configUid
+ + "; presumably it had been cancelled.");
+ if (DEBUG) {
+ Slog.d(TAG, String.format("SubscriberBroadcast params {%d %d %d %d %s}",
+ configUid, configKey, subscriptionId,
+ subscriptionRuleId, dimensionsValue));
+ }
+ }
+ }
+
private final static int[] toIntArray(List<Integer> list) {
int[] ret = new int[list.size()];
for (int i = 0; i < ret.length; i++) {
@@ -555,11 +596,11 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
case StatsLog.CPU_TIME_PER_FREQ: {
List<StatsLogEventWrapper> ret = new ArrayList();
for (int cluster = 0; cluster < mKernelCpuSpeedReaders.length; cluster++) {
- long[] clusterTimeMs = mKernelCpuSpeedReaders[cluster].readDelta();
+ long[] clusterTimeMs = mKernelCpuSpeedReaders[cluster].readAbsolute();
if (clusterTimeMs != null) {
for (int speed = clusterTimeMs.length - 1; speed >= 0; --speed) {
StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, 3);
- e.writeInt(tagId);
+ e.writeInt(cluster);
e.writeInt(speed);
e.writeLong(clusterTimeMs[speed]);
ret.add(e);
@@ -588,6 +629,7 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
e.writeLong(wifiInfo.getControllerIdleTimeMillis());
e.writeLong(wifiInfo.getControllerEnergyUsed());
ret.add(e);
+ return ret.toArray(new StatsLogEventWrapper[ret.size()]);
} catch (RemoteException e) {
Slog.e(TAG, "Pulling wifiManager for wifi controller activity energy info has error", e);
} finally {
@@ -618,9 +660,40 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
e.writeLong(modemInfo.getRxTimeMillis());
e.writeLong(modemInfo.getEnergyUsed());
ret.add(e);
+ return ret.toArray(new StatsLogEventWrapper[ret.size()]);
}
break;
}
+ case StatsLog.CPU_SUSPEND_TIME: {
+ List<StatsLogEventWrapper> ret = new ArrayList();
+ StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, 1);
+ e.writeLong(SystemClock.elapsedRealtime());
+ ret.add(e);
+ return ret.toArray(new StatsLogEventWrapper[ret.size()]);
+ }
+ case StatsLog.CPU_IDLE_TIME: {
+ List<StatsLogEventWrapper> ret = new ArrayList();
+ StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, 1);
+ e.writeLong(SystemClock.uptimeMillis());
+ ret.add(e);
+ return ret.toArray(new StatsLogEventWrapper[ret.size()]);
+ }
+ case StatsLog.DISK_SPACE: {
+ List<StatsLogEventWrapper> ret = new ArrayList();
+ StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, 3);
+ e.writeLong(mStatFsData.getAvailableBytes());
+ e.writeLong(mStatFsSystem.getAvailableBytes());
+ e.writeLong(mStatFsTemp.getAvailableBytes());
+ ret.add(e);
+ return ret.toArray(new StatsLogEventWrapper[ret.size()]);
+ }
+ case StatsLog.SYSTEM_UPTIME: {
+ List<StatsLogEventWrapper> ret = new ArrayList();
+ StatsLogEventWrapper e = new StatsLogEventWrapper(tagId, 1);
+ e.writeLong(SystemClock.uptimeMillis());
+ ret.add(e);
+ return ret.toArray(new StatsLogEventWrapper[ret.size()]);
+ }
default:
Slog.w(TAG, "No such tagId data as " + tagId);
return null;
@@ -743,9 +816,13 @@ public class StatsCompanionService extends IStatsCompanionService.Stub {
filter.addAction(Intent.ACTION_SHUTDOWN);
mContext.registerReceiverAsUser(
mShutdownEventReceiver, UserHandle.ALL, filter, null, null);
-
- // Pull the latest state of UID->app name, version mapping when statsd starts.
- informAllUidsLocked(mContext);
+ final long token = Binder.clearCallingIdentity();
+ try {
+ // Pull the latest state of UID->app name, version mapping when statsd starts.
+ informAllUidsLocked(mContext);
+ } finally {
+ restoreCallingIdentity(token);
+ }
} catch (RemoteException e) {
Slog.e(TAG, "Failed to inform statsd that statscompanion is ready", e);
forgetEverything();
diff --git a/com/android/server/statusbar/StatusBarManagerInternal.java b/com/android/server/statusbar/StatusBarManagerInternal.java
index 3792bc67..3ab771b7 100644
--- a/com/android/server/statusbar/StatusBarManagerInternal.java
+++ b/com/android/server/statusbar/StatusBarManagerInternal.java
@@ -30,13 +30,15 @@ public interface StatusBarManagerInternal {
void cancelPreloadRecentApps();
- void showRecentApps(boolean triggeredFromAltTab, boolean fromHome);
+ void showRecentApps(boolean triggeredFromAltTab);
void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey);
void dismissKeyboardShortcutsMenu();
void toggleKeyboardShortcutsMenu(int deviceId);
+ void showChargingAnimation(int batteryLevel);
+
/**
* Show picture-in-picture menu.
*/
@@ -95,7 +97,7 @@ public interface StatusBarManagerInternal {
*
* @param rotation rotation suggestion
*/
- void onProposedRotationChanged(int rotation);
+ void onProposedRotationChanged(int rotation, boolean isValid);
public interface GlobalActionsListener {
/**
diff --git a/com/android/server/statusbar/StatusBarManagerService.java b/com/android/server/statusbar/StatusBarManagerService.java
index c7c03b48..adb368b0 100644
--- a/com/android/server/statusbar/StatusBarManagerService.java
+++ b/com/android/server/statusbar/StatusBarManagerService.java
@@ -23,6 +23,7 @@ import android.app.StatusBarManager;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Rect;
+import android.hardware.fingerprint.IFingerprintDialogReceiver;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
@@ -282,10 +283,10 @@ public class StatusBarManagerService extends IStatusBarService.Stub {
}
@Override
- public void showRecentApps(boolean triggeredFromAltTab, boolean fromHome) {
+ public void showRecentApps(boolean triggeredFromAltTab) {
if (mBar != null) {
try {
- mBar.showRecentApps(triggeredFromAltTab, fromHome);
+ mBar.showRecentApps(triggeredFromAltTab);
} catch (RemoteException ex) {}
}
}
@@ -318,6 +319,16 @@ public class StatusBarManagerService extends IStatusBarService.Stub {
}
@Override
+ public void showChargingAnimation(int batteryLevel) {
+ if (mBar != null) {
+ try {
+ mBar.showChargingAnimation(batteryLevel);
+ } catch (RemoteException ex){
+ }
+ }
+ }
+
+ @Override
public void showPictureInPictureMenu() {
if (mBar != null) {
try {
@@ -408,10 +419,10 @@ public class StatusBarManagerService extends IStatusBarService.Stub {
}
@Override
- public void onProposedRotationChanged(int rotation) {
+ public void onProposedRotationChanged(int rotation, boolean isValid) {
if (mBar != null){
try {
- mBar.onProposedRotationChanged(rotation);
+ mBar.onProposedRotationChanged(rotation, isValid);
} catch (RemoteException ex) {}
}
}
@@ -514,6 +525,56 @@ public class StatusBarManagerService extends IStatusBarService.Stub {
}
@Override
+ public void showFingerprintDialog(Bundle bundle, IFingerprintDialogReceiver receiver) {
+ if (mBar != null) {
+ try {
+ mBar.showFingerprintDialog(bundle, receiver);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
+ @Override
+ public void onFingerprintAuthenticated() {
+ if (mBar != null) {
+ try {
+ mBar.onFingerprintAuthenticated();
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
+ @Override
+ public void onFingerprintHelp(String message) {
+ if (mBar != null) {
+ try {
+ mBar.onFingerprintHelp(message);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
+ @Override
+ public void onFingerprintError(String error) {
+ if (mBar != null) {
+ try {
+ mBar.onFingerprintError(error);
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
+ @Override
+ public void hideFingerprintDialog() {
+ if (mBar != null) {
+ try {
+ mBar.hideFingerprintDialog();
+ } catch (RemoteException ex) {
+ }
+ }
+ }
+
+ @Override
public void disable(int what, IBinder token, String pkg) {
disableForUser(what, token, pkg, mCurrentUserId);
}
diff --git a/com/android/server/storage/DeviceStorageMonitorService.java b/com/android/server/storage/DeviceStorageMonitorService.java
index a35383f3..f7cc4432 100644
--- a/com/android/server/storage/DeviceStorageMonitorService.java
+++ b/com/android/server/storage/DeviceStorageMonitorService.java
@@ -40,6 +40,7 @@ import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import android.text.format.DateUtils;
import android.util.ArrayMap;
+import android.util.DataUnit;
import android.util.Slog;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
@@ -80,13 +81,13 @@ public class DeviceStorageMonitorService extends SystemService {
private static final int MSG_CHECK = 1;
- private static final long DEFAULT_LOG_DELTA_BYTES = 64 * TrafficStats.MB_IN_BYTES;
+ private static final long DEFAULT_LOG_DELTA_BYTES = DataUnit.MEBIBYTES.toBytes(64);
private static final long DEFAULT_CHECK_INTERVAL = DateUtils.MINUTE_IN_MILLIS;
// com.android.internal.R.string.low_internal_storage_view_text_no_boot
// hard codes 250MB in the message as the storage space required for the
// boot image.
- private static final long BOOT_IMAGE_STORAGE_REQUIREMENT = 250 * TrafficStats.MB_IN_BYTES;
+ private static final long BOOT_IMAGE_STORAGE_REQUIREMENT = DataUnit.MEBIBYTES.toBytes(250);
private NotificationManager mNotifManager;
diff --git a/com/android/server/testing/ShadowEventLog.java b/com/android/server/testing/ShadowEventLog.java
new file mode 100644
index 00000000..b8059f4f
--- /dev/null
+++ b/com/android/server/testing/ShadowEventLog.java
@@ -0,0 +1,71 @@
+/*
+ * 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 com.android.server.testing;
+
+import android.util.EventLog;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+@Implements(EventLog.class)
+public class ShadowEventLog {
+ private final static LinkedHashSet<Entry> ENTRIES = new LinkedHashSet<>();
+
+ @Implementation
+ public static int writeEvent(int tag, Object... values) {
+ ENTRIES.add(new Entry(tag, Arrays.asList(values)));
+ // Currently we don't care about the return value, if we do, estimate it correctly
+ return 0;
+ }
+
+ public static boolean hasEvent(int tag, Object... values) {
+ return ENTRIES.contains(new Entry(tag, Arrays.asList(values)));
+ }
+
+ public static void clearEvents() {
+ ENTRIES.clear();
+ }
+
+ public static class Entry {
+ public final int tag;
+ public final List<Object> values;
+
+ public Entry(int tag, List<Object> values) {
+ this.tag = tag;
+ this.values = values;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Entry entry = (Entry) o;
+ return tag == entry.tag && values.equals(entry.values);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = tag;
+ result = 31 * result + values.hashCode();
+ return result;
+ }
+ }
+}
diff --git a/com/android/server/testing/shadows/FrameworkShadowContextImpl.java b/com/android/server/testing/shadows/FrameworkShadowContextImpl.java
new file mode 100644
index 00000000..6d220737
--- /dev/null
+++ b/com/android/server/testing/shadows/FrameworkShadowContextImpl.java
@@ -0,0 +1,37 @@
+/*
+ * 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 com.android.server.testing.shadows;
+
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.UserHandle;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowContextImpl;
+
+@Implements(className = ShadowContextImpl.CLASS_NAME, inheritImplementationMethods = true)
+public class FrameworkShadowContextImpl extends ShadowContextImpl {
+ @Implementation
+ public boolean bindServiceAsUser(
+ Intent service,
+ ServiceConnection connection,
+ int flags,
+ UserHandle user) {
+ return bindService(service, connection, flags);
+ }
+}
diff --git a/com/android/server/backup/testing/ShadowPackageManagerForBackup.java b/com/android/server/testing/shadows/FrameworkShadowPackageManager.java
index b64b59d2..5cdbe7ff 100644
--- a/com/android/server/backup/testing/ShadowPackageManagerForBackup.java
+++ b/com/android/server/testing/shadows/FrameworkShadowPackageManager.java
@@ -14,22 +14,18 @@
* limitations under the License
*/
-package com.android.server.backup.testing;
+package com.android.server.testing.shadows;
import android.app.ApplicationPackageManager;
import android.content.Intent;
import android.content.pm.ResolveInfo;
-
+import java.util.List;
import org.robolectric.annotation.Implements;
import org.robolectric.shadows.ShadowApplicationPackageManager;
-import java.util.List;
-
-/**
- * Implementation of PackageManager for Robolectric which handles queryIntentServicesAsUser().
- */
+/** Extension of ShadowApplicationPackageManager */
@Implements(value = ApplicationPackageManager.class, inheritImplementationMethods = true)
-public class ShadowPackageManagerForBackup extends ShadowApplicationPackageManager {
+public class FrameworkShadowPackageManager extends ShadowApplicationPackageManager {
@Override
public List<ResolveInfo> queryIntentServicesAsUser(Intent intent, int flags, int userId) {
return queryIntentServices(intent, flags);
diff --git a/com/android/server/timezone/RulesManagerService.java b/com/android/server/timezone/RulesManagerService.java
index 30fc63c2..be9b2047 100644
--- a/com/android/server/timezone/RulesManagerService.java
+++ b/com/android/server/timezone/RulesManagerService.java
@@ -143,6 +143,26 @@ public final class RulesManagerService extends IRulesManager.Stub {
return null;
}
+ // Determine the installed distro state. This should be possible regardless of whether
+ // there's an operation in progress.
+ DistroVersion installedDistroVersion;
+ int distroStatus = DISTRO_STATUS_UNKNOWN;
+ DistroRulesVersion installedDistroRulesVersion = null;
+ try {
+ installedDistroVersion = mInstaller.getInstalledDistroVersion();
+ if (installedDistroVersion == null) {
+ distroStatus = DISTRO_STATUS_NONE;
+ installedDistroRulesVersion = null;
+ } else {
+ distroStatus = DISTRO_STATUS_INSTALLED;
+ installedDistroRulesVersion = new DistroRulesVersion(
+ installedDistroVersion.rulesVersion,
+ installedDistroVersion.revision);
+ }
+ } catch (DistroException | IOException e) {
+ Slog.w(TAG, "Failed to read installed distro.", e);
+ }
+
boolean operationInProgress = this.mOperationInProgress.get();
// Determine the staged operation status, if possible.
@@ -168,27 +188,6 @@ public final class RulesManagerService extends IRulesManager.Stub {
Slog.w(TAG, "Failed to read staged distro.", e);
}
}
-
- // Determine the installed distro state, if possible.
- DistroVersion installedDistroVersion;
- int distroStatus = DISTRO_STATUS_UNKNOWN;
- DistroRulesVersion installedDistroRulesVersion = null;
- if (!operationInProgress) {
- try {
- installedDistroVersion = mInstaller.getInstalledDistroVersion();
- if (installedDistroVersion == null) {
- distroStatus = DISTRO_STATUS_NONE;
- installedDistroRulesVersion = null;
- } else {
- distroStatus = DISTRO_STATUS_INSTALLED;
- installedDistroRulesVersion = new DistroRulesVersion(
- installedDistroVersion.rulesVersion,
- installedDistroVersion.revision);
- }
- } catch (DistroException | IOException e) {
- Slog.w(TAG, "Failed to read installed distro.", e);
- }
- }
return new RulesState(systemRulesVersion, DISTRO_FORMAT_VERSION_SUPPORTED,
operationInProgress, stagedOperationStatus, stagedDistroRulesVersion,
distroStatus, installedDistroRulesVersion);
diff --git a/com/android/server/trust/TrustAgentWrapper.java b/com/android/server/trust/TrustAgentWrapper.java
index ca0a4505..28fee4ea 100644
--- a/com/android/server/trust/TrustAgentWrapper.java
+++ b/com/android/server/trust/TrustAgentWrapper.java
@@ -42,6 +42,9 @@ import android.service.trust.ITrustAgentServiceCallback;
import android.service.trust.TrustAgentService;
import android.util.Log;
import android.util.Slog;
+
+import com.android.internal.policy.IKeyguardDismissCallback;
+
import java.util.Collections;
import java.util.List;
@@ -67,6 +70,7 @@ public class TrustAgentWrapper {
private static final int MSG_REMOVE_ESCROW_TOKEN = 8;
private static final int MSG_ESCROW_TOKEN_STATE = 9;
private static final int MSG_UNLOCK_USER = 10;
+ private static final int MSG_SHOW_KEYGUARD_ERROR_MESSAGE = 11;
/**
* Time in uptime millis that we wait for the service connection, both when starting
@@ -81,6 +85,7 @@ public class TrustAgentWrapper {
private static final String DATA_ESCROW_TOKEN = "escrow_token";
private static final String DATA_HANDLE = "handle";
private static final String DATA_USER_ID = "user_id";
+ private static final String DATA_MESSAGE = "message";
private final TrustManagerService mTrustManagerService;
private final int mUserId;
@@ -255,6 +260,11 @@ public class TrustAgentWrapper {
mTrustManagerService.unlockUserWithToken(handle, eToken, userId);
break;
}
+ case MSG_SHOW_KEYGUARD_ERROR_MESSAGE: {
+ CharSequence message = msg.getData().getCharSequence(DATA_MESSAGE);
+ mTrustManagerService.showKeyguardErrorMessage(message);
+ break;
+ }
}
}
};
@@ -347,6 +357,14 @@ public class TrustAgentWrapper {
msg.getData().putByteArray(DATA_ESCROW_TOKEN, token);
msg.sendToTarget();
}
+
+ @Override
+ public void showKeyguardErrorMessage(CharSequence message) {
+ if (DEBUG) Slog.d(TAG, "Showing keyguard error message: " + message);
+ Message msg = mHandler.obtainMessage(MSG_SHOW_KEYGUARD_ERROR_MESSAGE);
+ msg.getData().putCharSequence(DATA_MESSAGE, message);
+ msg.sendToTarget();
+ }
};
private final ServiceConnection mConnection = new ServiceConnection() {
diff --git a/com/android/server/trust/TrustManagerService.java b/com/android/server/trust/TrustManagerService.java
index a7cd962b..44136661 100644
--- a/com/android/server/trust/TrustManagerService.java
+++ b/com/android/server/trust/TrustManagerService.java
@@ -60,6 +60,7 @@ import android.view.IWindowManager;
import android.view.WindowManagerGlobal;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.content.PackageMonitor;
+import com.android.internal.policy.IKeyguardDismissCallback;
import com.android.internal.util.DumpUtils;
import com.android.internal.widget.LockPatternUtils;
import com.android.server.SystemService;
@@ -249,6 +250,10 @@ public class TrustManagerService extends SystemService {
mLockPatternUtils.unlockUserWithToken(handle, token, userId);
}
+ void showKeyguardErrorMessage(CharSequence message) {
+ dispatchOnTrustError(message);
+ }
+
void refreshAgentList(int userIdOrAll) {
if (DEBUG) Slog.d(TAG, "refreshAgentList(" + userIdOrAll + ")");
if (!mTrustAgentsCanRun) {
@@ -769,6 +774,23 @@ public class TrustManagerService extends SystemService {
}
}
+ private void dispatchOnTrustError(CharSequence message) {
+ if (DEBUG) {
+ Log.i(TAG, "onTrustError(" + message + ")");
+ }
+ for (int i = 0; i < mTrustListeners.size(); i++) {
+ try {
+ mTrustListeners.get(i).onTrustError(message);
+ } catch (DeadObjectException e) {
+ Slog.d(TAG, "Removing dead TrustListener.");
+ mTrustListeners.remove(i);
+ i--;
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Exception while notifying TrustListener.", e);
+ }
+ }
+ }
+
// User lifecycle
@Override
diff --git a/com/android/server/updates/CarrierIdInstallReceiver.java b/com/android/server/updates/CarrierIdInstallReceiver.java
new file mode 100644
index 00000000..116fe7f4
--- /dev/null
+++ b/com/android/server/updates/CarrierIdInstallReceiver.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 com.android.server.updates;
+
+import android.content.ContentResolver;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.Telephony;
+import android.util.Log;
+
+public class CarrierIdInstallReceiver extends ConfigUpdateInstallReceiver {
+
+ public CarrierIdInstallReceiver() {
+ super("/data/misc/carrierid", "carrier_list.pb", "metadata/", "version");
+ }
+
+ @Override
+ protected void postInstall(Context context, Intent intent) {
+ ContentResolver resolver = context.getContentResolver();
+ resolver.update(Uri.withAppendedPath(Telephony.CarrierIdentification.CONTENT_URI,
+ "update_db"), new ContentValues(), null, null);
+ }
+}
diff --git a/com/android/server/updates/NetworkWatchlistInstallReceiver.java b/com/android/server/updates/NetworkWatchlistInstallReceiver.java
new file mode 100644
index 00000000..3b7ddc2e
--- /dev/null
+++ b/com/android/server/updates/NetworkWatchlistInstallReceiver.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 com.android.server.updates;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.NetworkWatchlistManager;
+import android.os.RemoteException;
+import android.util.Slog;
+
+public class NetworkWatchlistInstallReceiver extends ConfigUpdateInstallReceiver {
+
+ public NetworkWatchlistInstallReceiver() {
+ super("/data/misc/network_watchlist/", "network_watchlist.xml", "metadata/", "version");
+ }
+
+ @Override
+ protected void postInstall(Context context, Intent intent) {
+ try {
+ context.getSystemService(NetworkWatchlistManager.class).reloadWatchlist();
+ } catch (Exception e) {
+ // Network Watchlist is not available
+ Slog.wtf("NetworkWatchlistInstallReceiver", "Unable to reload watchlist");
+ }
+ }
+}
diff --git a/com/android/server/usage/AppIdleHistory.java b/com/android/server/usage/AppIdleHistory.java
index a1f18106..0cbda284 100644
--- a/com/android/server/usage/AppIdleHistory.java
+++ b/com/android/server/usage/AppIdleHistory.java
@@ -23,7 +23,6 @@ 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;
@@ -66,13 +65,7 @@ public class AppIdleHistory {
// History for all users and all packages
private SparseArray<ArrayMap<String,AppUsageHistory>> mIdleHistory = new SparseArray<>();
- private long mLastPeriod = 0;
private static final long ONE_MINUTE = 60 * 1000;
- private static final int HISTORY_SIZE = 100;
- private static final int FLAG_LAST_STATE = 2;
- private static final int FLAG_PARTIAL_ACTIVE = 1;
- private static final long PERIOD_DURATION = UsageStatsService.COMPRESS_TIME ? ONE_MINUTE
- : 60 * ONE_MINUTE;
@VisibleForTesting
static final String APP_IDLE_FILENAME = "app_idle_stats.xml";
@@ -89,6 +82,10 @@ public class AppIdleHistory {
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";
+ // The last time a job was run for this app
+ private static final String ATTR_LAST_RUN_JOB_TIME = "lastJobRunTime";
+ // The time when the forced active state can be overridden.
+ private static final String ATTR_BUCKET_TIMEOUT_TIME = "bucketTimeoutTime";
// device on time = mElapsedDuration + (timeNow - mElapsedSnapshot)
private long mElapsedSnapshot; // Elapsed time snapshot when last write of mDeviceOnDuration
@@ -103,8 +100,6 @@ public class AppIdleHistory {
private boolean mScreenOn;
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
@@ -118,6 +113,13 @@ public class AppIdleHistory {
String bucketingReason;
// In-memory only, last bucket for which the listeners were informed
int lastInformedBucket;
+ // The last time a job was run for this app, using elapsed timebase
+ long lastJobRunTime;
+ // When should the bucket state timeout, in elapsed timebase, if greater than
+ // lastUsedElapsedTime.
+ // This is used to keep the app in a high bucket regardless of other timeouts and
+ // predictions.
+ long bucketTimeoutTime;
}
AppIdleHistory(File storageDir, long elapsedRealtime) {
@@ -195,81 +197,47 @@ public class AppIdleHistory {
writeScreenOnTime();
}
- public int reportUsage(String packageName, int userId, long elapsedRealtime) {
+ /**
+ * Mark the app as used and update the bucket if necessary. If there is a timeout specified
+ * that's in the future, then the usage event is temporary and keeps the app in the specified
+ * bucket at least until the timeout is reached. This can be used to keep the app in an
+ * elevated bucket for a while until some important task gets to run.
+ * @param packageName
+ * @param userId
+ * @param bucket the bucket to set the app to
+ * @param elapsedRealtime mark as used time if non-zero
+ * @param timeout set the timeout of the specified bucket, if non-zero
+ * @return
+ */
+ public int reportUsage(String packageName, int userId, int bucket, long elapsedRealtime,
+ long timeout) {
ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
elapsedRealtime, true);
- shiftHistoryToNow(userHistory, elapsedRealtime);
-
- appUsageHistory.lastUsedElapsedTime = mElapsedDuration
- + (elapsedRealtime - mElapsedSnapshot);
- appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime);
- appUsageHistory.recent[HISTORY_SIZE - 1] = FLAG_LAST_STATE | FLAG_PARTIAL_ACTIVE;
- if (appUsageHistory.currentBucket > STANDBY_BUCKET_ACTIVE) {
- appUsageHistory.currentBucket = STANDBY_BUCKET_ACTIVE;
- if (DEBUG) {
- Slog.d(TAG, "Moved " + packageName + " to bucket=" + appUsageHistory.currentBucket
- + ", reason=" + appUsageHistory.bucketingReason);
- }
+ if (elapsedRealtime != 0) {
+ appUsageHistory.lastUsedElapsedTime = mElapsedDuration
+ + (elapsedRealtime - mElapsedSnapshot);
+ appUsageHistory.lastUsedScreenTime = getScreenOnTime(elapsedRealtime);
}
- appUsageHistory.bucketingReason = REASON_USAGE;
-
- return appUsageHistory.currentBucket;
- }
- public int reportMildUsage(String packageName, int userId, long elapsedRealtime) {
- ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
- AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
- elapsedRealtime, true);
- if (appUsageHistory.currentBucket > STANDBY_BUCKET_WORKING_SET) {
- appUsageHistory.currentBucket = STANDBY_BUCKET_WORKING_SET;
+ if (appUsageHistory.currentBucket > bucket) {
+ appUsageHistory.currentBucket = bucket;
if (DEBUG) {
- Slog.d(TAG, "Moved " + packageName + " to bucket=" + appUsageHistory.currentBucket
+ Slog.d(TAG, "Moved " + packageName + " to bucket=" + appUsageHistory
+ .currentBucket
+ ", reason=" + appUsageHistory.bucketingReason);
}
+ if (timeout > elapsedRealtime) {
+ // Convert to elapsed timebase
+ appUsageHistory.bucketTimeoutTime = mElapsedDuration + (timeout - mElapsedSnapshot);
+ }
}
- // TODO: Should this be a different reason for partial usage?
appUsageHistory.bucketingReason = REASON_USAGE;
return appUsageHistory.currentBucket;
}
- public void setIdle(String packageName, int userId, long elapsedRealtime) {
- ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
- AppUsageHistory appUsageHistory = getPackageHistory(userHistory, packageName,
- elapsedRealtime, true);
-
- shiftHistoryToNow(userHistory, elapsedRealtime);
-
- appUsageHistory.recent[HISTORY_SIZE - 1] &= ~FLAG_LAST_STATE;
- }
-
- private void shiftHistoryToNow(ArrayMap<String, AppUsageHistory> userHistory,
- long elapsedRealtime) {
- long thisPeriod = elapsedRealtime / PERIOD_DURATION;
- // Has the period switched over? Slide all users' package histories
- if (mLastPeriod != 0 && mLastPeriod < thisPeriod
- && (thisPeriod - mLastPeriod) < HISTORY_SIZE - 1) {
- int diff = (int) (thisPeriod - mLastPeriod);
- final int NUSERS = mIdleHistory.size();
- for (int u = 0; u < NUSERS; u++) {
- userHistory = mIdleHistory.valueAt(u);
- for (AppUsageHistory idleState : userHistory.values()) {
- // Shift left
- System.arraycopy(idleState.recent, diff, idleState.recent, 0,
- HISTORY_SIZE - diff);
- // Replicate last state across the diff
- for (int i = 0; i < diff; i++) {
- idleState.recent[HISTORY_SIZE - i - 1] =
- (byte) (idleState.recent[HISTORY_SIZE - diff - 1] & FLAG_LAST_STATE);
- }
- }
- }
- }
- mLastPeriod = thisPeriod;
- }
-
private ArrayMap<String, AppUsageHistory> getUserHistory(int userId) {
ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId);
if (userHistory == null) {
@@ -291,6 +259,7 @@ public class AppIdleHistory {
appUsageHistory.currentBucket = STANDBY_BUCKET_NEVER;
appUsageHistory.bucketingReason = REASON_DEFAULT;
appUsageHistory.lastInformedBucket = -1;
+ appUsageHistory.lastJobRunTime = Long.MIN_VALUE; // long long time ago
userHistory.put(packageName, appUsageHistory);
}
return appUsageHistory;
@@ -338,6 +307,38 @@ public class AppIdleHistory {
}
}
+ /**
+ * Marks the last time a job was run, with the given elapsedRealtime. The time stored is
+ * based on the elapsed timebase.
+ * @param packageName
+ * @param userId
+ * @param elapsedRealtime
+ */
+ public void setLastJobRunTime(String packageName, int userId, long elapsedRealtime) {
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory =
+ getPackageHistory(userHistory, packageName, elapsedRealtime, true);
+ appUsageHistory.lastJobRunTime = getElapsedTime(elapsedRealtime);
+ }
+
+ /**
+ * Returns the time since the last job was run for this app. This can be larger than the
+ * current elapsedRealtime, in case it happened before boot or a really large value if no jobs
+ * were ever run.
+ * @param packageName
+ * @param userId
+ * @param elapsedRealtime
+ * @return
+ */
+ public long getTimeSinceLastJobRun(String packageName, int userId, long elapsedRealtime) {
+ ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
+ AppUsageHistory appUsageHistory =
+ getPackageHistory(userHistory, packageName, elapsedRealtime, true);
+ // Don't adjust the default, else it'll wrap around to a positive value
+ if (appUsageHistory.lastJobRunTime == Long.MIN_VALUE) return Long.MAX_VALUE;
+ return getElapsedTime(elapsedRealtime) - appUsageHistory.lastJobRunTime;
+ }
+
public int getAppStandbyBucket(String packageName, int userId, long elapsedRealtime) {
ArrayMap<String, AppUsageHistory> userHistory = getUserHistory(userId);
AppUsageHistory appUsageHistory =
@@ -473,12 +474,8 @@ 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);
- }
+ appUsageHistory.lastPredictedTime = getLongValue(parser,
+ ATTR_LAST_PREDICTED_TIME, 0L);
String currentBucketString = parser.getAttributeValue(null,
ATTR_CURRENT_BUCKET);
appUsageHistory.currentBucket = currentBucketString == null
@@ -486,6 +483,10 @@ public class AppIdleHistory {
: Integer.parseInt(currentBucketString);
appUsageHistory.bucketingReason =
parser.getAttributeValue(null, ATTR_BUCKETING_REASON);
+ appUsageHistory.lastJobRunTime = getLongValue(parser,
+ ATTR_LAST_RUN_JOB_TIME, Long.MIN_VALUE);
+ appUsageHistory.bucketTimeoutTime = getLongValue(parser,
+ ATTR_BUCKET_TIMEOUT_TIME, 0L);
if (appUsageHistory.bucketingReason == null) {
appUsageHistory.bucketingReason = REASON_DEFAULT;
}
@@ -501,6 +502,12 @@ public class AppIdleHistory {
}
}
+ private long getLongValue(XmlPullParser parser, String attrName, long defValue) {
+ String value = parser.getAttributeValue(null, attrName);
+ if (value == null) return defValue;
+ return Long.parseLong(value);
+ }
+
public void writeAppIdleTimes(int userId) {
FileOutputStream fos = null;
AtomicFile appIdleFile = new AtomicFile(getUserFile(userId));
@@ -531,6 +538,14 @@ public class AppIdleHistory {
xml.attribute(null, ATTR_CURRENT_BUCKET,
Integer.toString(history.currentBucket));
xml.attribute(null, ATTR_BUCKETING_REASON, history.bucketingReason);
+ if (history.bucketTimeoutTime > 0) {
+ xml.attribute(null, ATTR_BUCKET_TIMEOUT_TIME, Long.toString(history
+ .bucketTimeoutTime));
+ }
+ if (history.lastJobRunTime != Long.MIN_VALUE) {
+ xml.attribute(null, ATTR_LAST_RUN_JOB_TIME, Long.toString(history
+ .lastJobRunTime));
+ }
xml.endTag(null, TAG_PACKAGE);
}
@@ -565,6 +580,10 @@ public class AppIdleHistory {
TimeUtils.formatDuration(screenOnTime - appUsageHistory.lastUsedScreenTime, idpw);
idpw.print(" lastPredictedTime=");
TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastPredictedTime, idpw);
+ idpw.print(" bucketTimeoutTime=");
+ TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.bucketTimeoutTime, idpw);
+ idpw.print(" lastJobRunTime=");
+ TimeUtils.formatDuration(totalElapsedTime - appUsageHistory.lastJobRunTime, idpw);
idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n"));
idpw.print(" bucket=" + appUsageHistory.currentBucket
+ " reason=" + appUsageHistory.bucketingReason);
@@ -579,21 +598,4 @@ public class AppIdleHistory {
idpw.println();
idpw.decreaseIndent();
}
-
- public void dumpHistory(IndentingPrintWriter idpw, int userId) {
- ArrayMap<String, AppUsageHistory> userHistory = mIdleHistory.get(userId);
- final long elapsedRealtime = SystemClock.elapsedRealtime();
- if (userHistory == null) return;
- final int P = userHistory.size();
- for (int p = 0; p < P; p++) {
- final String packageName = userHistory.keyAt(p);
- final byte[] history = userHistory.valueAt(p).recent;
- for (int i = 0; i < HISTORY_SIZE; i++) {
- idpw.print(history[i] == 0 ? '.' : 'A');
- }
- idpw.print(" idle=" + (isIdle(packageName, userId, elapsedRealtime) ? "y" : "n"));
- idpw.print(" " + packageName);
- idpw.println();
- }
- }
}
diff --git a/com/android/server/usage/AppStandbyController.java b/com/android/server/usage/AppStandbyController.java
index 9b588fa3..6782188c 100644
--- a/com/android/server/usage/AppStandbyController.java
+++ b/com/android/server/usage/AppStandbyController.java
@@ -33,10 +33,8 @@ 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.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;
@@ -67,25 +65,33 @@ import android.os.SystemClock;
import android.os.UserHandle;
import android.provider.Settings;
import android.telephony.TelephonyManager;
+import android.util.ArraySet;
import android.util.KeyValueListParser;
import android.util.Slog;
+import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.TimeUtils;
import android.view.Display;
import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IBatteryStats;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.ConcurrentUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.LocalServices;
import java.io.File;
import java.io.PrintWriter;
+import java.time.Duration;
+import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
/**
* Manages the standby state of an app, listening to various events.
@@ -124,6 +130,11 @@ public class AppStandbyController {
// Expiration time for predicted bucket
private static final long PREDICTION_TIMEOUT = 12 * ONE_HOUR;
+ /**
+ * Indicates the maximum wait time for admin data to be available;
+ */
+ private static final long WAIT_FOR_ADMIN_DATA_TIMEOUT_MS = 10_000;
+
// To name the lock for stack traces
static class Lock {}
@@ -146,6 +157,11 @@ public class AppStandbyController {
@GuardedBy("mAppIdleLock")
private List<String> mCarrierPrivilegedApps;
+ @GuardedBy("mActiveAdminApps")
+ private final SparseArray<Set<String>> mActiveAdminApps = new SparseArray<>();
+
+ private final CountDownLatch mAdminDataAvailableLatch = new CountDownLatch(1);
+
// Messages for the handler
static final int MSG_INFORM_LISTENERS = 3;
static final int MSG_FORCE_IDLE_STATE = 4;
@@ -263,8 +279,9 @@ public class AppStandbyController {
}
if (!packageName.equals(providerPkgName)) {
synchronized (mAppIdleLock) {
- int newBucket = mAppIdleHistory.reportMildUsage(packageName, userId,
- elapsedRealtime);
+ int newBucket = mAppIdleHistory.reportUsage(packageName, userId,
+ STANDBY_BUCKET_ACTIVE, elapsedRealtime,
+ elapsedRealtime + 2 * ONE_HOUR);
maybeInformListeners(packageName, userId, elapsedRealtime,
newBucket);
}
@@ -406,8 +423,10 @@ public class AppStandbyController {
AppIdleHistory.AppUsageHistory app =
mAppIdleHistory.getAppUsageHistory(packageName,
userId, elapsedRealtime);
- // If the bucket was forced by the developer, leave it alone
- if (REASON_FORCED.equals(app.bucketingReason)) {
+ // If the bucket was forced by the developer or the app is within the
+ // temporary active period, leave it alone.
+ if (REASON_FORCED.equals(app.bucketingReason)
+ || !hasBucketTimeoutPassed(app, elapsedRealtime)) {
continue;
}
boolean predictionLate = false;
@@ -449,6 +468,11 @@ public class AppStandbyController {
- app.lastPredictedTime > PREDICTION_TIMEOUT;
}
+ private boolean hasBucketTimeoutPassed(AppIdleHistory.AppUsageHistory app,
+ long elapsedRealtime) {
+ return app.bucketTimeoutTime < mAppIdleHistory.getElapsedTime(elapsedRealtime);
+ }
+
private void maybeInformListeners(String packageName, int userId,
long elapsedRealtime, int bucket) {
synchronized (mAppIdleLock) {
@@ -544,11 +568,13 @@ public class AppStandbyController {
final int newBucket;
if (event.mEventType == UsageEvents.Event.NOTIFICATION_SEEN) {
- newBucket = mAppIdleHistory.reportMildUsage(event.mPackage, userId,
- elapsedRealtime);
+ newBucket = mAppIdleHistory.reportUsage(event.mPackage, userId,
+ STANDBY_BUCKET_WORKING_SET,
+ elapsedRealtime, elapsedRealtime + 2 * ONE_HOUR);
} else {
newBucket = mAppIdleHistory.reportUsage(event.mPackage, userId,
- elapsedRealtime);
+ STANDBY_BUCKET_ACTIVE,
+ elapsedRealtime, elapsedRealtime + 2 * ONE_HOUR);
}
maybeInformListeners(event.mPackage, userId, elapsedRealtime,
@@ -592,9 +618,25 @@ public class AppStandbyController {
}
}
+ public void setLastJobRunTime(String packageName, int userId, long elapsedRealtime) {
+ synchronized (mAppIdleLock) {
+ mAppIdleHistory.setLastJobRunTime(packageName, userId, elapsedRealtime);
+ }
+ }
+
+ public long getTimeSinceLastJobRun(String packageName, int userId) {
+ final long elapsedRealtime = mInjector.elapsedRealtime();
+ synchronized (mAppIdleLock) {
+ return mAppIdleHistory.getTimeSinceLastJobRun(packageName, userId, elapsedRealtime);
+ }
+ }
+
public void onUserRemoved(int userId) {
synchronized (mAppIdleLock) {
mAppIdleHistory.onUserRemoved(userId);
+ synchronized (mActiveAdminApps) {
+ mActiveAdminApps.remove(userId);
+ }
}
}
@@ -805,16 +847,27 @@ public class AppStandbyController {
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
+
+ // Don't allow prediction to change from/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;
+
+ // If the bucket is required to stay in a higher state for a specified duration, don't
+ // override unless the duration has passed
+ if (predicted && app.currentBucket < newBucket
+ && !hasBucketTimeoutPassed(app, elapsedRealtime)) {
+ return;
+ }
+
mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime, newBucket,
reason);
}
@@ -822,10 +875,53 @@ public class AppStandbyController {
newBucket);
}
- private boolean isActiveDeviceAdmin(String packageName, int userId) {
- DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class);
- if (dpm == null) return false;
- return dpm.packageHasActiveAdmins(packageName, userId);
+ @VisibleForTesting
+ boolean isActiveDeviceAdmin(String packageName, int userId) {
+ synchronized (mActiveAdminApps) {
+ final Set<String> adminPkgs = mActiveAdminApps.get(userId);
+ return adminPkgs != null && adminPkgs.contains(packageName);
+ }
+ }
+
+ public void addActiveDeviceAdmin(String adminPkg, int userId) {
+ synchronized (mActiveAdminApps) {
+ Set<String> adminPkgs = mActiveAdminApps.get(userId);
+ if (adminPkgs == null) {
+ adminPkgs = new ArraySet<>();
+ mActiveAdminApps.put(userId, adminPkgs);
+ }
+ adminPkgs.add(adminPkg);
+ }
+ }
+
+ public void setActiveAdminApps(Set<String> adminPkgs, int userId) {
+ synchronized (mActiveAdminApps) {
+ if (adminPkgs == null) {
+ mActiveAdminApps.remove(userId);
+ } else {
+ mActiveAdminApps.put(userId, adminPkgs);
+ }
+ }
+ }
+
+ public void onAdminDataAvailable() {
+ mAdminDataAvailableLatch.countDown();
+ }
+
+ /**
+ * This will only ever be called once - during device boot.
+ */
+ private void waitForAdminData() {
+ if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_DEVICE_ADMIN)) {
+ ConcurrentUtils.waitForCountDownNoInterrupt(mAdminDataAvailableLatch,
+ WAIT_FOR_ADMIN_DATA_TIMEOUT_MS, "Wait for admin data");
+ }
+ }
+
+ Set<String> getActiveAdminAppsForTest(int userId) {
+ synchronized (mActiveAdminApps) {
+ return mActiveAdminApps.get(userId);
+ }
}
/**
@@ -947,7 +1043,10 @@ public class AppStandbyController {
final PackageInfo pi = packages.get(i);
String packageName = pi.packageName;
if (pi.applicationInfo != null && pi.applicationInfo.isSystemApp()) {
- mAppIdleHistory.reportUsage(packageName, userId, elapsedRealtime);
+ // Mark app as used for 4 hours. After that it can timeout to whatever the
+ // past usage pattern was.
+ mAppIdleHistory.reportUsage(packageName, userId, STANDBY_BUCKET_ACTIVE, 0,
+ elapsedRealtime + 4 * ONE_HOUR);
if (isAppSpecial(packageName, UserHandle.getAppId(pi.applicationInfo.uid),
userId)) {
mAppIdleHistory.setAppStandbyBucket(packageName, userId, elapsedRealtime,
@@ -967,12 +1066,6 @@ public class AppStandbyController {
.sendToTarget();
}
- void dumpHistory(IndentingPrintWriter idpw, int userId) {
- synchronized (mAppIdleLock) {
- mAppIdleHistory.dumpHistory(idpw, userId);
- }
- }
-
void dumpUser(IndentingPrintWriter idpw, int userId, String pkg) {
synchronized (mAppIdleLock) {
mAppIdleHistory.dump(idpw, userId, pkg);
@@ -1154,6 +1247,7 @@ public class AppStandbyController {
case MSG_ONE_TIME_CHECK_IDLE_STATES:
mHandler.removeMessages(MSG_ONE_TIME_CHECK_IDLE_STATES);
+ waitForAdminData();
checkIdleStates(UserHandle.USER_ALL);
break;
@@ -1278,10 +1372,10 @@ public class AppStandbyController {
synchronized (mAppIdleLock) {
// Default: 24 hours between paroles
- mAppIdleParoleIntervalMillis = mParser.getLong(KEY_PAROLE_INTERVAL,
+ mAppIdleParoleIntervalMillis = mParser.getDurationMillis(KEY_PAROLE_INTERVAL,
COMPRESS_TIME ? ONE_MINUTE * 10 : 24 * 60 * ONE_MINUTE);
- mAppIdleParoleDurationMillis = mParser.getLong(KEY_PAROLE_DURATION,
+ mAppIdleParoleDurationMillis = mParser.getDurationMillis(KEY_PAROLE_DURATION,
COMPRESS_TIME ? ONE_MINUTE : 10 * ONE_MINUTE); // 10 minutes
String screenThresholdsValue = mParser.getString(KEY_SCREEN_TIME_THRESHOLDS, null);
@@ -1308,7 +1402,15 @@ public class AppStandbyController {
if (thresholds.length == THRESHOLD_BUCKETS.length) {
long[] array = new long[THRESHOLD_BUCKETS.length];
for (int i = 0; i < THRESHOLD_BUCKETS.length; i++) {
- array[i] = Long.parseLong(thresholds[i]);
+ try {
+ if (thresholds[i].startsWith("P") || thresholds[i].startsWith("p")) {
+ array[i] = Duration.parse(thresholds[i]).toMillis();
+ } else {
+ array[i] = Long.parseLong(thresholds[i]);
+ }
+ } catch (NumberFormatException|DateTimeParseException e) {
+ return defaults;
+ }
}
return array;
} else {
diff --git a/com/android/server/usage/IntervalStats.java b/com/android/server/usage/IntervalStats.java
index cb32d1fb..4d458b02 100644
--- a/com/android/server/usage/IntervalStats.java
+++ b/com/android/server/usage/IntervalStats.java
@@ -92,6 +92,17 @@ class IntervalStats {
return false;
}
+ /**
+ * Returns whether the event type is one caused by user visible
+ * interaction. Excludes those that are internally generated.
+ * @param eventType
+ * @return
+ */
+ private boolean isUserVisibleEvent(int eventType) {
+ return eventType != UsageEvents.Event.SYSTEM_INTERACTION
+ && eventType != UsageEvents.Event.STANDBY_BUCKET_CHANGED;
+ }
+
void update(String packageName, long timeStamp, int eventType) {
UsageStats usageStats = getOrCreateUsageStats(packageName);
@@ -109,7 +120,7 @@ class IntervalStats {
usageStats.mLastEvent = eventType;
}
- if (eventType != UsageEvents.Event.SYSTEM_INTERACTION) {
+ if (isUserVisibleEvent(eventType)) {
usageStats.mLastTimeUsed = timeStamp;
}
usageStats.mEndTimeStamp = timeStamp;
diff --git a/com/android/server/usage/StorageStatsService.java b/com/android/server/usage/StorageStatsService.java
index 82f80012..2fec20ab 100644
--- a/com/android/server/usage/StorageStatsService.java
+++ b/com/android/server/usage/StorageStatsService.java
@@ -49,6 +49,7 @@ import android.os.storage.VolumeInfo;
import android.provider.Settings;
import android.text.format.DateUtils;
import android.util.ArrayMap;
+import android.util.DataUnit;
import android.util.Slog;
import android.util.SparseLongArray;
@@ -73,7 +74,7 @@ public class StorageStatsService extends IStorageStatsManager.Stub {
private static final String PROP_VERIFY_STORAGE = "fw.verify_storage";
private static final long DELAY_IN_MILLIS = 30 * DateUtils.SECOND_IN_MILLIS;
- private static final long DEFAULT_QUOTA = 64 * TrafficStats.MB_IN_BYTES;
+ private static final long DEFAULT_QUOTA = DataUnit.MEBIBYTES.toBytes(64);
public static class Lifecycle extends SystemService {
private StorageStatsService mService;
@@ -167,8 +168,11 @@ public class StorageStatsService extends IStorageStatsManager.Stub {
public boolean isReservedSupported(String volumeUuid, String callingPackage) {
enforcePermission(Binder.getCallingUid(), callingPackage);
- // TODO: implement as part of b/62024591
- return false;
+ if (volumeUuid == StorageManager.UUID_PRIVATE_INTERNAL) {
+ return SystemProperties.getBoolean(StorageManager.PROP_HAS_RESERVED, false);
+ } else {
+ return false;
+ }
}
@Override
diff --git a/com/android/server/usage/UsageStatsService.java b/com/android/server/usage/UsageStatsService.java
index cdce4487..096fdcc2 100644
--- a/com/android/server/usage/UsageStatsService.java
+++ b/com/android/server/usage/UsageStatsService.java
@@ -68,10 +68,9 @@ 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;
+import java.util.Set;
/**
* A service that collects, aggregates, and persists application usage data.
@@ -116,6 +115,26 @@ public class UsageStatsService extends SystemService implements
AppStandbyController mAppStandby;
+ private UsageStatsManagerInternal.AppIdleStateChangeListener mStandbyChangeListener =
+ new UsageStatsManagerInternal.AppIdleStateChangeListener() {
+ @Override
+ public void onAppIdleStateChanged(String packageName, int userId, boolean idle,
+ int bucket) {
+ Event event = new Event();
+ event.mEventType = Event.STANDBY_BUCKET_CHANGED;
+ event.mBucket = bucket;
+ event.mPackage = packageName;
+ // This will later be converted to system time.
+ event.mTimeStamp = SystemClock.elapsedRealtime();
+ mHandler.obtainMessage(MSG_REPORT_EVENT, userId, 0, event).sendToTarget();
+ }
+
+ @Override
+ public void onParoleStateChanged(boolean isParoleOn) {
+
+ }
+ };
+
public UsageStatsService(Context context) {
super(context);
}
@@ -130,6 +149,7 @@ public class UsageStatsService extends SystemService implements
mAppStandby = new AppStandbyController(getContext(), BackgroundThread.get().getLooper());
+ mAppStandby.addListener(mStandbyChangeListener);
File systemDataDir = new File(Environment.getDataDirectory(), "system");
mUsageStatsDir = new File(systemDataDir, "usagestats");
mUsageStatsDir.mkdirs();
@@ -478,7 +498,6 @@ public class UsageStatsService extends SystemService implements
IndentingPrintWriter idpw = new IndentingPrintWriter(pw, " ");
boolean checkin = false;
- boolean history = false;
String pkg = null;
if (args != null) {
@@ -486,11 +505,6 @@ public class UsageStatsService extends SystemService implements
String arg = args[i];
if ("--checkin".equals(arg)) {
checkin = true;
- } else if ("--history".equals(arg)) {
- history = true;
- } else if ("history".equals(arg)) {
- history = true;
- break;
} else if ("flush".equals(arg)) {
flushToDiskLocked();
pw.println("Flushed stats to disk");
@@ -514,9 +528,6 @@ public class UsageStatsService extends SystemService implements
} else {
mUserState.valueAt(i).dump(idpw, pkg);
idpw.println();
- if (history) {
- mAppStandby.dumpHistory(idpw, userId);
- }
}
mAppStandby.dumpUser(idpw, userId, pkg);
idpw.decreaseIndent();
@@ -1021,5 +1032,29 @@ public class UsageStatsService extends SystemService implements
return UsageStatsService.this.queryUsageStats(
userId, intervalType, beginTime, endTime, obfuscateInstantApps);
}
+
+ @Override
+ public void setLastJobRunTime(String packageName, int userId, long elapsedRealtime) {
+ mAppStandby.setLastJobRunTime(packageName, userId, elapsedRealtime);
+ }
+
+ @Override
+ public long getTimeSinceLastJobRun(String packageName, int userId) {
+ return mAppStandby.getTimeSinceLastJobRun(packageName, userId);
+ }
+
+ public void onActiveAdminAdded(String packageName, int userId) {
+ mAppStandby.addActiveDeviceAdmin(packageName, userId);
+ }
+
+ @Override
+ public void setActiveAdminApps(Set<String> packageNames, int userId) {
+ mAppStandby.setActiveAdminApps(packageNames, userId);
+ }
+
+ @Override
+ public void onAdminDataAvailable() {
+ mAppStandby.onAdminDataAvailable();
+ }
}
}
diff --git a/com/android/server/usage/UsageStatsXmlV1.java b/com/android/server/usage/UsageStatsXmlV1.java
index cc53a9cc..d1ed5992 100644
--- a/com/android/server/usage/UsageStatsXmlV1.java
+++ b/com/android/server/usage/UsageStatsXmlV1.java
@@ -64,6 +64,7 @@ final class UsageStatsXmlV1 {
private static final String LAST_EVENT_ATTR = "lastEvent";
private static final String TYPE_ATTR = "type";
private static final String SHORTCUT_ID_ATTR = "shortcutId";
+ private static final String STANDBY_BUCKET_ATTR = "standbyBucket";
// Time attributes stored as an offset of the beginTime.
private static final String LAST_TIME_ACTIVE_ATTR = "lastTimeActive";
@@ -173,6 +174,9 @@ final class UsageStatsXmlV1 {
final String id = XmlUtils.readStringAttribute(parser, SHORTCUT_ID_ATTR);
event.mShortcutId = (id != null) ? id.intern() : null;
break;
+ case UsageEvents.Event.STANDBY_BUCKET_CHANGED:
+ event.mBucket = XmlUtils.readIntAttribute(parser, STANDBY_BUCKET_ATTR, 0);
+ break;
}
if (statsOut.events == null) {
@@ -276,6 +280,10 @@ final class UsageStatsXmlV1 {
XmlUtils.writeStringAttribute(xml, SHORTCUT_ID_ATTR, event.mShortcutId);
}
break;
+ case UsageEvents.Event.STANDBY_BUCKET_CHANGED:
+ if (event.mBucket != 0) {
+ XmlUtils.writeIntAttribute(xml, STANDBY_BUCKET_ATTR, event.mBucket);
+ }
}
xml.endTag(null, EVENT_TAG);
diff --git a/com/android/server/usage/UserUsageStatsService.java b/com/android/server/usage/UserUsageStatsService.java
index f02221cb..ec12da23 100644
--- a/com/android/server/usage/UserUsageStatsService.java
+++ b/com/android/server/usage/UserUsageStatsService.java
@@ -599,6 +599,9 @@ class UserUsageStatsService {
if (event.mShortcutId != null) {
pw.printPair("shortcutId", event.mShortcutId);
}
+ if (event.mEventType == UsageEvents.Event.STANDBY_BUCKET_CHANGED) {
+ pw.printPair("standbyBucket", event.mBucket);
+ }
pw.printHexPair("flags", event.mFlags);
pw.println();
}
@@ -645,6 +648,8 @@ class UserUsageStatsService {
return "CHOOSER_ACTION";
case UsageEvents.Event.NOTIFICATION_SEEN:
return "NOTIFICATION_SEEN";
+ case UsageEvents.Event.STANDBY_BUCKET_CHANGED:
+ return "STANDBY_BUCKET_CHANGED";
default:
return "UNKNOWN";
}
diff --git a/com/android/server/usb/UsbDeviceManager.java b/com/android/server/usb/UsbDeviceManager.java
index 1b057f9b..4a7072d0 100644
--- a/com/android/server/usb/UsbDeviceManager.java
+++ b/com/android/server/usb/UsbDeviceManager.java
@@ -16,6 +16,9 @@
package com.android.server.usb;
+import android.app.ActivityManager;
+import android.app.ActivityManagerInternal;
+import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
@@ -26,6 +29,7 @@ import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.ContentObserver;
@@ -38,6 +42,7 @@ import android.hardware.usb.UsbManager;
import android.hardware.usb.UsbPort;
import android.hardware.usb.UsbPortStatus;
import android.os.BatteryManager;
+import android.os.Environment;
import android.os.FileUtils;
import android.os.Handler;
import android.os.Looper;
@@ -60,6 +65,7 @@ import com.android.internal.notification.SystemNotificationChannels;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.IndentingPrintWriter;
import com.android.server.FgThread;
+import com.android.server.LocalServices;
import java.io.File;
import java.io.FileNotFoundException;
@@ -75,7 +81,7 @@ import java.util.Set;
/**
* UsbDeviceManager manages USB state in device mode.
*/
-public class UsbDeviceManager {
+public class UsbDeviceManager implements ActivityManagerInternal.ScreenObserver {
private static final String TAG = "UsbDeviceManager";
private static final boolean DEBUG = false;
@@ -97,6 +103,12 @@ public class UsbDeviceManager {
private static final String USB_STATE_PROPERTY = "sys.usb.state";
/**
+ * The SharedPreference setting per user that stores the screen unlocked functions between
+ * sessions.
+ */
+ private static final String UNLOCKED_CONFIG_PREF = "usb-screen-unlocked-config-%d";
+
+ /**
* ro.bootmode value when phone boots into usual Android.
*/
private static final String NORMAL_BOOT = "normal";
@@ -128,6 +140,8 @@ public class UsbDeviceManager {
private static final int MSG_UPDATE_CHARGING_STATE = 9;
private static final int MSG_UPDATE_HOST_STATE = 10;
private static final int MSG_LOCALE_CHANGED = 11;
+ private static final int MSG_SET_SCREEN_UNLOCKED_FUNCTIONS = 12;
+ private static final int MSG_UPDATE_SCREEN_LOCK = 13;
private static final int AUDIO_MODE_SOURCE = 1;
@@ -169,6 +183,7 @@ public class UsbDeviceManager {
private Intent mBroadcastedIntent;
private boolean mPendingBootBroadcast;
private static Set<Integer> sBlackListedInterfaces;
+ private SharedPreferences mSettings;
static {
sBlackListedInterfaces = new HashSet<>();
@@ -217,6 +232,31 @@ public class UsbDeviceManager {
}
};
+ @Override
+ public void onKeyguardStateChanged(boolean isShowing) {
+ int userHandle = ActivityManager.getCurrentUser();
+ boolean secure = mContext.getSystemService(KeyguardManager.class)
+ .isDeviceSecure(userHandle);
+ boolean unlocking = mContext.getSystemService(UserManager.class)
+ .isUserUnlockingOrUnlocked(userHandle);
+ if (DEBUG) {
+ Slog.v(TAG, "onKeyguardStateChanged: isShowing:" + isShowing + " secure:" + secure
+ + " unlocking:" + unlocking + " user:" + userHandle);
+ }
+ // We are unlocked when the keyguard is down or non-secure, and user storage is unlocked.
+ mHandler.sendMessage(MSG_UPDATE_SCREEN_LOCK, (isShowing && secure) || !unlocking);
+ }
+
+ @Override
+ public void onAwakeStateChanged(boolean isAwake) {
+ // ignore
+ }
+
+ /** Called when a user is unlocked. */
+ public void onUnlockUser(int userHandle) {
+ onKeyguardStateChanged(false);
+ }
+
public UsbDeviceManager(Context context, UsbAlsaManager alsaManager,
UsbSettingsManager settingsManager) {
mContext = context;
@@ -303,6 +343,8 @@ public class UsbDeviceManager {
public void systemReady() {
if (DEBUG) Slog.d(TAG, "systemReady");
+ LocalServices.getService(ActivityManagerInternal.class).registerScreenObserver(this);
+
mNotificationManager = (NotificationManager)
mContext.getSystemService(Context.NOTIFICATION_SERVICE);
@@ -407,6 +449,14 @@ public class UsbDeviceManager {
return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_LEANBACK);
}
+ private SharedPreferences getPinnedSharedPrefs(Context context) {
+ final File prefsFile = new File(new File(
+ Environment.getDataUserCePackageDirectory(StorageManager.UUID_PRIVATE_INTERNAL,
+ context.getUserId(), context.getPackageName()), "shared_prefs"),
+ UsbDeviceManager.class.getSimpleName() + ".xml");
+ return context.getSharedPreferences(prefsFile, Context.MODE_PRIVATE);
+ }
+
private final class UsbHandler extends Handler {
// current USB state
@@ -423,11 +473,13 @@ public class UsbDeviceManager {
private UsbAccessory mCurrentAccessory;
private int mUsbNotificationId;
private boolean mAdbNotificationShown;
- private int mCurrentUser = UserHandle.USER_NULL;
+ private int mCurrentUser;
private boolean mUsbCharging;
private String mCurrentOemFunctions;
private boolean mHideUsbNotification;
private boolean mSupportsAllCombinations;
+ private String mScreenUnlockedFunctions = UsbManager.USB_FUNCTION_NONE;
+ private boolean mScreenLocked;
public UsbHandler(Looper looper) {
super(looper);
@@ -449,6 +501,9 @@ public class UsbDeviceManager {
SystemProperties.get(USB_STATE_PROPERTY));
}
+ mCurrentUser = ActivityManager.getCurrentUser();
+ mScreenLocked = true;
+
/*
* Use the normal bootmode persistent prop to maintain state of adb across
* all boot modes.
@@ -653,7 +708,7 @@ public class UsbDeviceManager {
private boolean trySetEnabledFunctions(String functions, boolean forceRestart) {
if (functions == null || applyAdbFunction(functions)
.equals(UsbManager.USB_FUNCTION_NONE)) {
- functions = getDefaultFunctions();
+ functions = getChargingFunctions();
}
functions = applyAdbFunction(functions);
@@ -876,6 +931,14 @@ public class UsbDeviceManager {
mMidiEnabled && mConfigured, mMidiCard, mMidiDevice);
}
+ private void setScreenUnlockedFunctions() {
+ setEnabledFunctions(mScreenUnlockedFunctions, false,
+ UsbManager.containsFunction(mScreenUnlockedFunctions,
+ UsbManager.USB_FUNCTION_MTP)
+ || UsbManager.containsFunction(mScreenUnlockedFunctions,
+ UsbManager.USB_FUNCTION_PTP));
+ }
+
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
@@ -895,7 +958,13 @@ public class UsbDeviceManager {
if (mBootCompleted) {
if (!mConnected && !hasMessages(MSG_ACCESSORY_MODE_ENTER_TIMEOUT)) {
// restore defaults when USB is disconnected
- setEnabledFunctions(null, !mAdbEnabled, false);
+ if (!mScreenLocked
+ && !UsbManager.USB_FUNCTION_NONE.equals(
+ mScreenUnlockedFunctions)) {
+ setScreenUnlockedFunctions();
+ } else {
+ setEnabledFunctions(null, !mAdbEnabled, false);
+ }
}
updateUsbFunctions();
} else {
@@ -978,6 +1047,47 @@ public class UsbDeviceManager {
String functions = (String) msg.obj;
setEnabledFunctions(functions, false, msg.arg1 == 1);
break;
+ case MSG_SET_SCREEN_UNLOCKED_FUNCTIONS:
+ mScreenUnlockedFunctions = (String) msg.obj;
+ SharedPreferences.Editor editor = mSettings.edit();
+ editor.putString(String.format(Locale.ENGLISH, UNLOCKED_CONFIG_PREF,
+ mCurrentUser), mScreenUnlockedFunctions);
+ editor.commit();
+ if (!mScreenLocked && !UsbManager.USB_FUNCTION_NONE.equals(
+ mScreenUnlockedFunctions)) {
+ // If the screen is unlocked, also set current functions.
+ setScreenUnlockedFunctions();
+ }
+ break;
+ case MSG_UPDATE_SCREEN_LOCK:
+ if (msg.arg1 == 1 == mScreenLocked) {
+ break;
+ }
+ mScreenLocked = msg.arg1 == 1;
+ if (mSettings == null && !mScreenLocked) {
+ // Shared preferences aren't accessible until the user has been unlocked.
+ mSettings = getPinnedSharedPrefs(mContext);
+ mScreenUnlockedFunctions = mSettings.getString(
+ String.format(Locale.ENGLISH, UNLOCKED_CONFIG_PREF, mCurrentUser),
+ UsbManager.USB_FUNCTION_NONE);
+ }
+ if (!mBootCompleted) {
+ break;
+ }
+ if (mScreenLocked) {
+ if (!mConnected) {
+ setEnabledFunctions(null, false, false);
+ }
+ } else {
+ if (!UsbManager.USB_FUNCTION_NONE.equals(mScreenUnlockedFunctions)
+ && (UsbManager.USB_FUNCTION_ADB.equals(mCurrentFunctions)
+ || (UsbManager.USB_FUNCTION_MTP.equals(mCurrentFunctions)
+ && !mUsbDataUnlocked))) {
+ // Set the screen unlocked functions if current function is charging.
+ setScreenUnlockedFunctions();
+ }
+ }
+ break;
case MSG_UPDATE_USER_RESTRICTIONS:
// Restart the USB stack if USB transfer is enabled but no longer allowed.
final boolean forceRestart = mUsbDataUnlocked
@@ -1001,7 +1111,13 @@ public class UsbDeviceManager {
updateUsbStateBroadcastIfNeeded(false);
mPendingBootBroadcast = false;
}
- setEnabledFunctions(null, false, false);
+
+ if (!mScreenLocked
+ && !UsbManager.USB_FUNCTION_NONE.equals(mScreenUnlockedFunctions)) {
+ setScreenUnlockedFunctions();
+ } else {
+ setEnabledFunctions(null, false, false);
+ }
if (mCurrentAccessory != null) {
getCurrentSettings().accessoryAttached(mCurrentAccessory);
}
@@ -1011,16 +1127,15 @@ public class UsbDeviceManager {
break;
case MSG_USER_SWITCHED: {
if (mCurrentUser != msg.arg1) {
- // Restart the USB stack and re-apply user restrictions for MTP or PTP.
- if (mUsbDataUnlocked
- && isUsbDataTransferActive()
- && mCurrentUser != UserHandle.USER_NULL) {
- Slog.v(TAG, "Current user switched to " + msg.arg1
- + "; resetting USB host stack for MTP or PTP");
- // avoid leaking sensitive data from previous user
- setEnabledFunctions(null, true, false);
+ if (DEBUG) {
+ Slog.v(TAG, "Current user switched to " + msg.arg1);
}
mCurrentUser = msg.arg1;
+ mScreenLocked = true;
+ mScreenUnlockedFunctions = mSettings.getString(
+ String.format(Locale.ENGLISH, UNLOCKED_CONFIG_PREF, mCurrentUser),
+ UsbManager.USB_FUNCTION_NONE);
+ setEnabledFunctions(null, false, false);
}
break;
}
@@ -1072,20 +1187,12 @@ public class UsbDeviceManager {
titleRes = com.android.internal.R.string.usb_unsupported_audio_accessory_title;
id = SystemMessage.NOTE_USB_AUDIO_ACCESSORY_NOT_SUPPORTED;
} else if (mConnected) {
- if (!mUsbDataUnlocked) {
- if (mSourcePower) {
- titleRes = com.android.internal.R.string.usb_supplying_notification_title;
- id = SystemMessage.NOTE_USB_SUPPLYING;
- } else {
- titleRes = com.android.internal.R.string.usb_charging_notification_title;
- id = SystemMessage.NOTE_USB_CHARGING;
- }
- } else if (UsbManager.containsFunction(mCurrentFunctions,
- UsbManager.USB_FUNCTION_MTP)) {
+ if (UsbManager.containsFunction(mCurrentFunctions,
+ UsbManager.USB_FUNCTION_MTP) && mUsbDataUnlocked) {
titleRes = com.android.internal.R.string.usb_mtp_notification_title;
id = SystemMessage.NOTE_USB_MTP;
} else if (UsbManager.containsFunction(mCurrentFunctions,
- UsbManager.USB_FUNCTION_PTP)) {
+ UsbManager.USB_FUNCTION_PTP) && mUsbDataUnlocked) {
titleRes = com.android.internal.R.string.usb_ptp_notification_title;
id = SystemMessage.NOTE_USB_PTP;
} else if (UsbManager.containsFunction(mCurrentFunctions,
@@ -1236,7 +1343,7 @@ public class UsbDeviceManager {
}
}
- private String getDefaultFunctions() {
+ private String getChargingFunctions() {
String func = SystemProperties.get(getPersistProp(true),
UsbManager.USB_FUNCTION_NONE);
// if ADB is enabled, reset functions to ADB
@@ -1253,6 +1360,8 @@ public class UsbDeviceManager {
pw.println(" mCurrentFunctions: " + mCurrentFunctions);
pw.println(" mCurrentOemFunctions: " + mCurrentOemFunctions);
pw.println(" mCurrentFunctionsApplied: " + mCurrentFunctionsApplied);
+ pw.println(" mScreenUnlockedFunctions: " + mScreenUnlockedFunctions);
+ pw.println(" mScreenLocked: " + mScreenLocked);
pw.println(" mConnected: " + mConnected);
pw.println(" mConfigured: " + mConfigured);
pw.println(" mUsbDataUnlocked: " + mUsbDataUnlocked);
@@ -1309,6 +1418,17 @@ public class UsbDeviceManager {
mHandler.sendMessage(MSG_SET_CURRENT_FUNCTIONS, functions, usbDataUnlocked);
}
+ /**
+ * Sets the functions which are set when the screen is unlocked.
+ * @param functions Functions to set.
+ */
+ public void setScreenUnlockedFunctions(String functions) {
+ if (DEBUG) {
+ Slog.d(TAG, "setScreenUnlockedFunctions(" + functions + ")");
+ }
+ mHandler.sendMessage(MSG_SET_SCREEN_UNLOCKED_FUNCTIONS, functions);
+ }
+
private void readOemUsbOverrideConfig() {
String[] configList = mContext.getResources().getStringArray(
com.android.internal.R.array.config_oemUsbModeOverride);
diff --git a/com/android/server/usb/UsbService.java b/com/android/server/usb/UsbService.java
index 8554cf7b..1a20819b 100644
--- a/com/android/server/usb/UsbService.java
+++ b/com/android/server/usb/UsbService.java
@@ -87,6 +87,11 @@ public class UsbService extends IUsbManager.Stub {
public void onStopUser(int userHandle) {
mUsbService.onStopUser(UserHandle.of(userHandle));
}
+
+ @Override
+ public void onUnlockUser(int userHandle) {
+ mUsbService.onUnlockUser(userHandle);
+ }
}
private static final String TAG = "UsbService";
@@ -205,6 +210,13 @@ public class UsbService extends IUsbManager.Stub {
}
}
+ /** Called when a user is unlocked. */
+ public void onUnlockUser(int user) {
+ if (mDeviceManager != null) {
+ mDeviceManager.onUnlockUser(user);
+ }
+ }
+
/* Returns a list of all currently attached USB devices (host mdoe) */
@Override
public void getDeviceList(Bundle devices) {
@@ -392,6 +404,23 @@ public class UsbService extends IUsbManager.Stub {
}
}
+ @Override
+ public void setScreenUnlockedFunctions(String function) {
+ mContext.enforceCallingOrSelfPermission(android.Manifest.permission.MANAGE_USB, null);
+
+ if (!isSupportedCurrentFunction(function)) {
+ Slog.w(TAG, "Caller of setScreenUnlockedFunctions() requested unsupported USB function:"
+ + function);
+ function = UsbManager.USB_FUNCTION_NONE;
+ }
+
+ if (mDeviceManager != null) {
+ mDeviceManager.setScreenUnlockedFunctions(function);
+ } else {
+ throw new IllegalStateException("USB device mode not supported");
+ }
+ }
+
private static boolean isSupportedCurrentFunction(String function) {
if (function == null) return true;
diff --git a/com/android/server/vr/VrManagerService.java b/com/android/server/vr/VrManagerService.java
index de723c67..d84fbc53 100644
--- a/com/android/server/vr/VrManagerService.java
+++ b/com/android/server/vr/VrManagerService.java
@@ -185,6 +185,14 @@ public class VrManagerService extends SystemService
ComponentName component = null;
synchronized (mLock) {
component = ((mCurrentVrService == null) ? null : mCurrentVrService.getComponent());
+
+ // If the VrCore main service was disconnected or the binding died we'll rebind
+ // automatically. Call focusedActivityChanged() once we rebind.
+ if (component != null && component.equals(event.component) &&
+ (event.event == LogEvent.EVENT_DISCONNECTED ||
+ event.event == LogEvent.EVENT_BINDING_DIED)) {
+ callFocusedActivityChangedLocked();
+ }
}
// If not on an AIO device and we permanently stopped trying to connect to the
@@ -980,16 +988,7 @@ public class VrManagerService extends SystemService
oldVrServicePackage, oldUserId);
if (mCurrentVrService != null && sendUpdatedCaller) {
- final ComponentName c = mCurrentVrModeComponent;
- final boolean b = running2dInVr;
- final int pid = processId;
- mCurrentVrService.sendEvent(new PendingEvent() {
- @Override
- public void runEvent(IInterface service) throws RemoteException {
- IVrListener l = (IVrListener) service;
- l.focusedActivityChanged(c, b, pid);
- }
- });
+ callFocusedActivityChangedLocked();
}
if (!nothingChanged) {
@@ -1002,6 +1001,23 @@ public class VrManagerService extends SystemService
}
}
+ private void callFocusedActivityChangedLocked() {
+ final ComponentName c = mCurrentVrModeComponent;
+ final boolean b = mRunning2dInVr;
+ final int pid = mVrAppProcessId;
+ mCurrentVrService.sendEvent(new PendingEvent() {
+ @Override
+ public void runEvent(IInterface service) throws RemoteException {
+ // Under specific (and unlikely) timing scenarios, when VrCore
+ // crashes and is rebound, focusedActivityChanged() may be
+ // called a 2nd time with the same arguments. IVrListeners
+ // should make sure to handle that scenario gracefully.
+ IVrListener l = (IVrListener) service;
+ l.focusedActivityChanged(c, b, pid);
+ }
+ });
+ }
+
private boolean isDefaultAllowed(String packageName) {
PackageManager pm = mContext.getPackageManager();
diff --git a/com/android/server/wallpaper/WallpaperManagerService.java b/com/android/server/wallpaper/WallpaperManagerService.java
index 8e916ad3..844aafb9 100644
--- a/com/android/server/wallpaper/WallpaperManagerService.java
+++ b/com/android/server/wallpaper/WallpaperManagerService.java
@@ -956,7 +956,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
if (mInfo != null && mInfo.getSupportsAmbientMode()) {
try {
- mEngine.setInAmbientMode(mInAmbientMode);
+ mEngine.setInAmbientMode(mInAmbientMode, false /* animated */);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to set ambient mode state", e);
}
@@ -1751,7 +1751,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
}
}
- public void setInAmbientMode(boolean inAmbienMode) {
+ public void setInAmbientMode(boolean inAmbienMode, boolean animated) {
final IWallpaperEngine engine;
synchronized (mLock) {
mInAmbientMode = inAmbienMode;
@@ -1766,7 +1766,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub
if (engine != null) {
try {
- engine.setInAmbientMode(inAmbienMode);
+ engine.setInAmbientMode(inAmbienMode, animated);
} catch (RemoteException e) {
// Cannot talk to wallpaper engine.
}
diff --git a/com/android/server/wifi/AvailableNetworkNotifier.java b/com/android/server/wifi/AvailableNetworkNotifier.java
new file mode 100644
index 00000000..a55d0503
--- /dev/null
+++ b/com/android/server/wifi/AvailableNetworkNotifier.java
@@ -0,0 +1,541 @@
+/*
+ * 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 com.android.server.wifi;
+
+import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_CONNECT_TO_NETWORK;
+import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_PICK_WIFI_NETWORK;
+import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE;
+import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_USER_DISMISSED_NOTIFICATION;
+import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.AVAILABLE_NETWORK_NOTIFIER_TAG;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.ArraySet;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.wifi.nano.WifiMetricsProto.ConnectToNetworkNotificationAndActionCount;
+import com.android.server.wifi.util.ScanResultUtil;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Base class for all network notifiers (e.g. OpenNetworkNotifier, CarrierNetworkNotifier).
+ *
+ * NOTE: These API's are not thread safe and should only be used from WifiStateMachine thread.
+ */
+public class AvailableNetworkNotifier {
+
+ /** Time in milliseconds to display the Connecting notification. */
+ private static final int TIME_TO_SHOW_CONNECTING_MILLIS = 10000;
+
+ /** Time in milliseconds to display the Connected notification. */
+ private static final int TIME_TO_SHOW_CONNECTED_MILLIS = 5000;
+
+ /** Time in milliseconds to display the Failed To Connect notification. */
+ private static final int TIME_TO_SHOW_FAILED_MILLIS = 5000;
+
+ /** The state of the notification */
+ @IntDef({
+ STATE_NO_NOTIFICATION,
+ STATE_SHOWING_RECOMMENDATION_NOTIFICATION,
+ STATE_CONNECTING_IN_NOTIFICATION,
+ STATE_CONNECTED_NOTIFICATION,
+ STATE_CONNECT_FAILED_NOTIFICATION
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface State {}
+
+ /** No recommendation is made and no notifications are shown. */
+ private static final int STATE_NO_NOTIFICATION = 0;
+ /** The initial notification recommending a network to connect to is shown. */
+ private static final int STATE_SHOWING_RECOMMENDATION_NOTIFICATION = 1;
+ /** The notification of status of connecting to the recommended network is shown. */
+ private static final int STATE_CONNECTING_IN_NOTIFICATION = 2;
+ /** The notification that the connection to the recommended network was successful is shown. */
+ private static final int STATE_CONNECTED_NOTIFICATION = 3;
+ /** The notification to show that connection to the recommended network failed is shown. */
+ private static final int STATE_CONNECT_FAILED_NOTIFICATION = 4;
+
+ /** Current state of the notification. */
+ @State private int mState = STATE_NO_NOTIFICATION;
+
+ /**
+ * The {@link Clock#getWallClockMillis()} must be at least this value for us
+ * to show the notification again.
+ */
+ private long mNotificationRepeatTime;
+ /**
+ * When a notification is shown, we wait this amount before possibly showing it again.
+ */
+ private final long mNotificationRepeatDelay;
+ /** Default repeat delay in seconds. */
+ @VisibleForTesting
+ static final int DEFAULT_REPEAT_DELAY_SEC = 900;
+
+ /** Whether the user has set the setting to show the 'available networks' notification. */
+ private boolean mSettingEnabled;
+ /** Whether the screen is on or not. */
+ private boolean mScreenOn;
+
+ /** List of SSIDs blacklisted from recommendation. */
+ private final Set<String> mBlacklistedSsids;
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final FrameworkFacade mFrameworkFacade;
+ private final WifiMetrics mWifiMetrics;
+ private final Clock mClock;
+ private final WifiConfigManager mConfigManager;
+ private final WifiStateMachine mWifiStateMachine;
+ private final Messenger mSrcMessenger;
+ private final ConnectToNetworkNotificationBuilder mNotificationBuilder;
+
+ private ScanResult mRecommendedNetwork;
+
+ /** Tag used for logs and metrics */
+ private final String mTag;
+ /** Identifier of the {@link SsidSetStoreData}. */
+ private final String mStoreDataIdentifier;
+ /** Identifier for the settings toggle, used for registering ContentObserver */
+ private final String mToggleSettingsName;
+
+ /** System wide identifier for notification in Notification Manager */
+ private final int mSystemMessageNotificationId;
+
+ public AvailableNetworkNotifier(
+ String tag,
+ String storeDataIdentifier,
+ String toggleSettingsName,
+ int notificationIdentifier,
+ Context context,
+ Looper looper,
+ FrameworkFacade framework,
+ Clock clock,
+ WifiMetrics wifiMetrics,
+ WifiConfigManager wifiConfigManager,
+ WifiConfigStore wifiConfigStore,
+ WifiStateMachine wifiStateMachine,
+ ConnectToNetworkNotificationBuilder connectToNetworkNotificationBuilder) {
+ mTag = tag;
+ mStoreDataIdentifier = storeDataIdentifier;
+ mToggleSettingsName = toggleSettingsName;
+ mSystemMessageNotificationId = notificationIdentifier;
+ mContext = context;
+ mHandler = new Handler(looper);
+ mFrameworkFacade = framework;
+ mWifiMetrics = wifiMetrics;
+ mClock = clock;
+ mConfigManager = wifiConfigManager;
+ mWifiStateMachine = wifiStateMachine;
+ mNotificationBuilder = connectToNetworkNotificationBuilder;
+ mScreenOn = false;
+ mSrcMessenger = new Messenger(new Handler(looper, mConnectionStateCallback));
+
+ mBlacklistedSsids = new ArraySet<>();
+ wifiConfigStore.registerStoreData(new SsidSetStoreData(mStoreDataIdentifier,
+ new AvailableNetworkNotifierStoreData()));
+
+ // Setting is in seconds
+ mNotificationRepeatDelay = mFrameworkFacade.getIntegerSetting(context,
+ Settings.Global.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY,
+ DEFAULT_REPEAT_DELAY_SEC) * 1000L;
+ NotificationEnabledSettingObserver settingObserver = new NotificationEnabledSettingObserver(
+ mHandler);
+ settingObserver.register();
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(ACTION_USER_DISMISSED_NOTIFICATION);
+ filter.addAction(ACTION_CONNECT_TO_NETWORK);
+ filter.addAction(ACTION_PICK_WIFI_NETWORK);
+ filter.addAction(ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE);
+ mContext.registerReceiver(
+ mBroadcastReceiver, filter, null /* broadcastPermission */, mHandler);
+ }
+
+ private final BroadcastReceiver mBroadcastReceiver =
+ new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (!mTag.equals(intent.getExtra(AVAILABLE_NETWORK_NOTIFIER_TAG))) {
+ return;
+ }
+ switch (intent.getAction()) {
+ case ACTION_USER_DISMISSED_NOTIFICATION:
+ handleUserDismissedAction();
+ break;
+ case ACTION_CONNECT_TO_NETWORK:
+ handleConnectToNetworkAction();
+ break;
+ case ACTION_PICK_WIFI_NETWORK:
+ handleSeeAllNetworksAction();
+ break;
+ case ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE:
+ handlePickWifiNetworkAfterConnectFailure();
+ break;
+ default:
+ Log.e(mTag, "Unknown action " + intent.getAction());
+ }
+ }
+ };
+
+ private final Handler.Callback mConnectionStateCallback = (Message msg) -> {
+ switch (msg.what) {
+ // Success here means that an attempt to connect to the network has been initiated.
+ // Successful connection updates are received via the
+ // WifiConnectivityManager#handleConnectionStateChanged() callback.
+ case WifiManager.CONNECT_NETWORK_SUCCEEDED:
+ break;
+ case WifiManager.CONNECT_NETWORK_FAILED:
+ handleConnectionAttemptFailedToSend();
+ break;
+ default:
+ Log.e("AvailableNetworkNotifier", "Unknown message " + msg.what);
+ }
+ return true;
+ };
+
+ /**
+ * Clears the pending notification. This is called by {@link WifiConnectivityManager} on stop.
+ *
+ * @param resetRepeatTime resets the time delay for repeated notification if true.
+ */
+ public void clearPendingNotification(boolean resetRepeatTime) {
+ if (resetRepeatTime) {
+ mNotificationRepeatTime = 0;
+ }
+
+ if (mState != STATE_NO_NOTIFICATION) {
+ getNotificationManager().cancel(mSystemMessageNotificationId);
+
+ if (mRecommendedNetwork != null) {
+ Log.d(mTag, "Notification with state="
+ + mState
+ + " was cleared for recommended network: "
+ + mRecommendedNetwork.SSID);
+ }
+ mState = STATE_NO_NOTIFICATION;
+ mRecommendedNetwork = null;
+ }
+ }
+
+ private boolean isControllerEnabled() {
+ return mSettingEnabled && !UserManager.get(mContext)
+ .hasUserRestriction(UserManager.DISALLOW_CONFIG_WIFI, UserHandle.CURRENT);
+ }
+
+ /**
+ * If there are available networks, attempt to post a network notification.
+ *
+ * @param availableNetworks Available networks to choose from and possibly show notification
+ */
+ public void handleScanResults(@NonNull List<ScanDetail> availableNetworks) {
+ if (!isControllerEnabled()) {
+ clearPendingNotification(true /* resetRepeatTime */);
+ return;
+ }
+ if (availableNetworks.isEmpty()) {
+ clearPendingNotification(false /* resetRepeatTime */);
+ return;
+ }
+
+ // Not enough time has passed to show a recommendation notification again
+ if (mState == STATE_NO_NOTIFICATION
+ && mClock.getWallClockMillis() < mNotificationRepeatTime) {
+ return;
+ }
+
+ // Do nothing when the screen is off and no notification is showing.
+ if (mState == STATE_NO_NOTIFICATION && !mScreenOn) {
+ return;
+ }
+
+ // Only show a new or update an existing recommendation notification.
+ if (mState == STATE_NO_NOTIFICATION
+ || mState == STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
+ ScanResult recommendation =
+ recommendNetwork(availableNetworks, new ArraySet<>(mBlacklistedSsids));
+
+ if (recommendation != null) {
+ postInitialNotification(recommendation);
+ } else {
+ clearPendingNotification(false /* resetRepeatTime */);
+ }
+ }
+ }
+
+ /**
+ * Recommends a network to connect to from a list of available networks, while ignoring the
+ * SSIDs in the blacklist.
+ */
+ public ScanResult recommendNetwork(@NonNull List<ScanDetail> networks,
+ @NonNull Set<String> blacklistedSsids) {
+ ScanResult result = null;
+ int highestRssi = Integer.MIN_VALUE;
+ for (ScanDetail scanDetail : networks) {
+ ScanResult scanResult = scanDetail.getScanResult();
+
+ if (scanResult.level > highestRssi) {
+ result = scanResult;
+ highestRssi = scanResult.level;
+ }
+ }
+
+ if (result != null && blacklistedSsids.contains(result.SSID)) {
+ result = null;
+ }
+ return result;
+ }
+
+ /** Handles screen state changes. */
+ public void handleScreenStateChanged(boolean screenOn) {
+ mScreenOn = screenOn;
+ }
+
+ /**
+ * Called by {@link WifiConnectivityManager} when Wi-Fi is connected. If the notification
+ * was in the connecting state, update the notification to show that it has connected to the
+ * recommended network.
+ */
+ public void handleWifiConnected() {
+ if (mState != STATE_CONNECTING_IN_NOTIFICATION) {
+ clearPendingNotification(true /* resetRepeatTime */);
+ return;
+ }
+
+ postNotification(mNotificationBuilder.createNetworkConnectedNotification(
+ mRecommendedNetwork));
+
+ Log.d(mTag, "User connected to recommended network: " + mRecommendedNetwork.SSID);
+ mWifiMetrics.incrementConnectToNetworkNotification(mTag,
+ ConnectToNetworkNotificationAndActionCount.NOTIFICATION_CONNECTED_TO_NETWORK);
+ mState = STATE_CONNECTED_NOTIFICATION;
+ mHandler.postDelayed(
+ () -> {
+ if (mState == STATE_CONNECTED_NOTIFICATION) {
+ clearPendingNotification(true /* resetRepeatTime */);
+ }
+ },
+ TIME_TO_SHOW_CONNECTED_MILLIS);
+ }
+
+ /**
+ * Handles when a Wi-Fi connection attempt failed.
+ */
+ public void handleConnectionFailure() {
+ if (mState != STATE_CONNECTING_IN_NOTIFICATION) {
+ return;
+ }
+ postNotification(mNotificationBuilder.createNetworkFailedNotification());
+
+ Log.d(mTag, "User failed to connect to recommended network: " + mRecommendedNetwork.SSID);
+ mWifiMetrics.incrementConnectToNetworkNotification(mTag,
+ ConnectToNetworkNotificationAndActionCount.NOTIFICATION_FAILED_TO_CONNECT);
+ mState = STATE_CONNECT_FAILED_NOTIFICATION;
+ mHandler.postDelayed(
+ () -> {
+ if (mState == STATE_CONNECT_FAILED_NOTIFICATION) {
+ clearPendingNotification(false /* resetRepeatTime */);
+ }
+ },
+ TIME_TO_SHOW_FAILED_MILLIS);
+ }
+
+ private NotificationManager getNotificationManager() {
+ return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ private void postInitialNotification(ScanResult recommendedNetwork) {
+ if (mRecommendedNetwork != null
+ && TextUtils.equals(mRecommendedNetwork.SSID, recommendedNetwork.SSID)) {
+ return;
+ }
+
+ postNotification(mNotificationBuilder.createConnectToAvailableNetworkNotification(mTag,
+ recommendedNetwork));
+
+ if (mState == STATE_NO_NOTIFICATION) {
+ mWifiMetrics.incrementConnectToNetworkNotification(mTag,
+ ConnectToNetworkNotificationAndActionCount.NOTIFICATION_RECOMMEND_NETWORK);
+ } else {
+ mWifiMetrics.incrementNumNetworkRecommendationUpdates(mTag);
+ }
+ mState = STATE_SHOWING_RECOMMENDATION_NOTIFICATION;
+ mRecommendedNetwork = recommendedNetwork;
+ mNotificationRepeatTime = mClock.getWallClockMillis() + mNotificationRepeatDelay;
+ }
+
+ private void postNotification(Notification notification) {
+ getNotificationManager().notify(mSystemMessageNotificationId, notification);
+ }
+
+ private void handleConnectToNetworkAction() {
+ mWifiMetrics.incrementConnectToNetworkNotificationAction(mTag, mState,
+ ConnectToNetworkNotificationAndActionCount.ACTION_CONNECT_TO_NETWORK);
+ if (mState != STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
+ return;
+ }
+ postNotification(mNotificationBuilder.createNetworkConnectingNotification(
+ mRecommendedNetwork));
+ mWifiMetrics.incrementConnectToNetworkNotification(mTag,
+ ConnectToNetworkNotificationAndActionCount.NOTIFICATION_CONNECTING_TO_NETWORK);
+
+ Log.d(mTag,
+ "User initiated connection to recommended network: " + mRecommendedNetwork.SSID);
+ WifiConfiguration network = createRecommendedNetworkConfig(mRecommendedNetwork);
+ Message msg = Message.obtain();
+ msg.what = WifiManager.CONNECT_NETWORK;
+ msg.arg1 = WifiConfiguration.INVALID_NETWORK_ID;
+ msg.obj = network;
+ msg.replyTo = mSrcMessenger;
+ mWifiStateMachine.sendMessage(msg);
+
+ mState = STATE_CONNECTING_IN_NOTIFICATION;
+ mHandler.postDelayed(
+ () -> {
+ if (mState == STATE_CONNECTING_IN_NOTIFICATION) {
+ handleConnectionFailure();
+ }
+ },
+ TIME_TO_SHOW_CONNECTING_MILLIS);
+ }
+
+ WifiConfiguration createRecommendedNetworkConfig(ScanResult recommendedNetwork) {
+ return ScanResultUtil.createNetworkFromScanResult(recommendedNetwork);
+ }
+
+ private void handleSeeAllNetworksAction() {
+ mWifiMetrics.incrementConnectToNetworkNotificationAction(mTag, mState,
+ ConnectToNetworkNotificationAndActionCount.ACTION_PICK_WIFI_NETWORK);
+ startWifiSettings();
+ }
+
+ private void startWifiSettings() {
+ // Close notification drawer before opening the picker.
+ mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+ mContext.startActivity(
+ new Intent(Settings.ACTION_WIFI_SETTINGS)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ clearPendingNotification(false /* resetRepeatTime */);
+ }
+
+ private void handleConnectionAttemptFailedToSend() {
+ handleConnectionFailure();
+ mWifiMetrics.incrementNumNetworkConnectMessageFailedToSend(mTag);
+ }
+
+ private void handlePickWifiNetworkAfterConnectFailure() {
+ mWifiMetrics.incrementConnectToNetworkNotificationAction(mTag, mState,
+ ConnectToNetworkNotificationAndActionCount
+ .ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE);
+ startWifiSettings();
+ }
+
+ private void handleUserDismissedAction() {
+ Log.d(mTag, "User dismissed notification with state=" + mState);
+ mWifiMetrics.incrementConnectToNetworkNotificationAction(mTag, mState,
+ ConnectToNetworkNotificationAndActionCount.ACTION_USER_DISMISSED_NOTIFICATION);
+ if (mState == STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
+ // blacklist dismissed network
+ mBlacklistedSsids.add(mRecommendedNetwork.SSID);
+ mWifiMetrics.setNetworkRecommenderBlacklistSize(mTag, mBlacklistedSsids.size());
+ mConfigManager.saveToStore(false /* forceWrite */);
+ Log.d(mTag, "Network is added to the network notification blacklist: "
+ + mRecommendedNetwork.SSID);
+ }
+ resetStateAndDelayNotification();
+ }
+
+ private void resetStateAndDelayNotification() {
+ mState = STATE_NO_NOTIFICATION;
+ mNotificationRepeatTime = System.currentTimeMillis() + mNotificationRepeatDelay;
+ mRecommendedNetwork = null;
+ }
+
+ /** Dump this network notifier's state. */
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println(mTag + ": ");
+ pw.println("mSettingEnabled " + mSettingEnabled);
+ pw.println("currentTime: " + mClock.getWallClockMillis());
+ pw.println("mNotificationRepeatTime: " + mNotificationRepeatTime);
+ pw.println("mState: " + mState);
+ pw.println("mBlacklistedSsids: " + mBlacklistedSsids.toString());
+ }
+
+ private class AvailableNetworkNotifierStoreData implements SsidSetStoreData.DataSource {
+ @Override
+ public Set<String> getSsids() {
+ return new ArraySet<>(mBlacklistedSsids);
+ }
+
+ @Override
+ public void setSsids(Set<String> ssidList) {
+ mBlacklistedSsids.addAll(ssidList);
+ mWifiMetrics.setNetworkRecommenderBlacklistSize(mTag, mBlacklistedSsids.size());
+ }
+ }
+
+ private class NotificationEnabledSettingObserver extends ContentObserver {
+ NotificationEnabledSettingObserver(Handler handler) {
+ super(handler);
+ }
+
+ public void register() {
+ mFrameworkFacade.registerContentObserver(mContext,
+ Settings.Global.getUriFor(mToggleSettingsName), true, this);
+ mSettingEnabled = getValue();
+ }
+
+ @Override
+ public void onChange(boolean selfChange) {
+ super.onChange(selfChange);
+ mSettingEnabled = getValue();
+ clearPendingNotification(true /* resetRepeatTime */);
+ }
+
+ private boolean getValue() {
+ boolean enabled =
+ mFrameworkFacade.getIntegerSetting(mContext, mToggleSettingsName, 1) == 1;
+ mWifiMetrics.setIsWifiNetworksAvailableNotificationEnabled(mTag, enabled);
+ Log.d(mTag, "Settings toggle enabled=" + enabled);
+ return enabled;
+ }
+ }
+}
diff --git a/com/android/server/wifi/BaseWifiDiagnostics.java b/com/android/server/wifi/BaseWifiDiagnostics.java
index 54fff68d..52517ad9 100644
--- a/com/android/server/wifi/BaseWifiDiagnostics.java
+++ b/com/android/server/wifi/BaseWifiDiagnostics.java
@@ -51,6 +51,12 @@ public class BaseWifiDiagnostics {
pw.println("set config_wifi_enable_wifi_firmware_debugging to enable");
}
+ /**
+ * Starts taking a standard android bugreport
+ * android will prompt the user to send the bugreport when it's complete
+ */
+ public void takeBugReport() { }
+
protected synchronized void dump(PrintWriter pw) {
pw.println("Chipset information :-----------------------------------------------");
pw.println("FW Version is: " + mFirmwareVersion);
diff --git a/com/android/server/wifi/CarrierNetworkNotifier.java b/com/android/server/wifi/CarrierNetworkNotifier.java
new file mode 100644
index 00000000..958f262e
--- /dev/null
+++ b/com/android/server/wifi/CarrierNetworkNotifier.java
@@ -0,0 +1,74 @@
+/*
+ * 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 com.android.server.wifi;
+
+import android.content.Context;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiConfiguration.KeyMgmt;
+import android.net.wifi.WifiEnterpriseConfig;
+import android.net.wifi.WifiEnterpriseConfig.Eap;
+import android.os.Looper;
+import android.provider.Settings;
+
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+import com.android.server.wifi.util.ScanResultUtil;
+
+/**
+ * This class handles the "carrier wi-fi network available" notification
+ *
+ * NOTE: These API's are not thread safe and should only be used from WifiStateMachine thread.
+ */
+public class CarrierNetworkNotifier extends AvailableNetworkNotifier {
+ public static final String TAG = "WifiCarrierNetworkNotifier";
+ private static final String STORE_DATA_IDENTIFIER = "CarrierNetworkNotifierBlacklist";
+ private static final String TOGGLE_SETTINGS_NAME =
+ Settings.Global.WIFI_CARRIER_NETWORKS_AVAILABLE_NOTIFICATION_ON;
+
+ public CarrierNetworkNotifier(
+ Context context,
+ Looper looper,
+ FrameworkFacade framework,
+ Clock clock,
+ WifiMetrics wifiMetrics,
+ WifiConfigManager wifiConfigManager,
+ WifiConfigStore wifiConfigStore,
+ WifiStateMachine wifiStateMachine,
+ ConnectToNetworkNotificationBuilder connectToNetworkNotificationBuilder) {
+ super(TAG, STORE_DATA_IDENTIFIER, TOGGLE_SETTINGS_NAME,
+ SystemMessage.NOTE_CARRIER_NETWORK_AVAILABLE, context, looper, framework, clock,
+ wifiMetrics, wifiConfigManager, wifiConfigStore, wifiStateMachine,
+ connectToNetworkNotificationBuilder);
+ }
+
+ @Override
+ WifiConfiguration createRecommendedNetworkConfig(ScanResult recommendedNetwork) {
+ WifiConfiguration network = ScanResultUtil.createNetworkFromScanResult(recommendedNetwork);
+
+ int eapMethod = recommendedNetwork.carrierApEapType;
+ if (eapMethod == Eap.SIM || eapMethod == Eap.AKA || eapMethod == Eap.AKA_PRIME) {
+ network.allowedKeyManagement.set(KeyMgmt.WPA_EAP);
+ network.allowedKeyManagement.set(KeyMgmt.IEEE8021X);
+ network.enterpriseConfig = new WifiEnterpriseConfig();
+ network.enterpriseConfig.setEapMethod(recommendedNetwork.carrierApEapType);
+ network.enterpriseConfig.setIdentity("");
+ network.enterpriseConfig.setAnonymousIdentity("");
+ }
+
+ return network;
+ }
+}
diff --git a/com/android/server/wifi/ConnectToNetworkNotificationBuilder.java b/com/android/server/wifi/ConnectToNetworkNotificationBuilder.java
index 38c0ad05..d7d5a910 100644
--- a/com/android/server/wifi/ConnectToNetworkNotificationBuilder.java
+++ b/com/android/server/wifi/ConnectToNetworkNotificationBuilder.java
@@ -22,6 +22,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.net.wifi.ScanResult;
+import android.util.Log;
import com.android.internal.R;
import com.android.internal.notification.SystemNotificationChannels;
@@ -47,6 +48,10 @@ public class ConnectToNetworkNotificationBuilder {
public static final String ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE =
"com.android.server.wifi.ConnectToNetworkNotification.PICK_NETWORK_AFTER_FAILURE";
+ /** Extra data added to the Intent to specify the registering network notifier. */
+ public static final String AVAILABLE_NETWORK_NOTIFIER_TAG =
+ "com.android.server.wifi.ConnectToNetworkNotification.AVAILABLE_NETWORK_NOTIFIER_TAG";
+
private Context mContext;
private Resources mResources;
private FrameworkFacade mFrameworkFacade;
@@ -66,22 +71,37 @@ public class ConnectToNetworkNotificationBuilder {
* There are two actions - "Options" link to the Wi-Fi picker activity, and "Connect" prompts
* the connection to the recommended network.
*
+ * @param notifierTag Unique tag of calling network notifier
* @param network The network to be recommended
*/
- public Notification createConnectToNetworkNotification(ScanResult network) {
- Notification.Action connectAction = new Notification.Action.Builder(
- null /* icon */,
+ public Notification createConnectToAvailableNetworkNotification(String notifierTag,
+ ScanResult network) {
+ CharSequence title;
+ int requestCode = 0; // Makes the different kinds of notifications distinguishable
+
+ switch (notifierTag) {
+ case OpenNetworkNotifier.TAG:
+ title = mContext.getText(R.string.wifi_available_title);
+ requestCode = 1;
+ break;
+ case CarrierNetworkNotifier.TAG:
+ title = mContext.getText(R.string.wifi_available_carrier_network_title);
+ requestCode = 2;
+ break;
+ default:
+ Log.wtf("ConnectToNetworkNotificationBuilder", "Unknown network notifier."
+ + notifierTag);
+ return null;
+ }
+
+ Notification.Action connectAction = new Notification.Action.Builder(null /* icon */,
mResources.getText(R.string.wifi_available_action_connect),
- getPrivateBroadcast(ACTION_CONNECT_TO_NETWORK)).build();
- Notification.Action allNetworksAction = new Notification.Action.Builder(
- null /* icon */,
+ getPrivateBroadcast(ACTION_CONNECT_TO_NETWORK, notifierTag, requestCode)).build();
+ Notification.Action allNetworksAction = new Notification.Action.Builder(null /* icon */,
mResources.getText(R.string.wifi_available_action_all_networks),
- getPrivateBroadcast(ACTION_PICK_WIFI_NETWORK)).build();
- return createNotificationBuilder(
- mContext.getText(R.string.wifi_available_title), network.SSID)
- .addAction(connectAction)
- .addAction(allNetworksAction)
- .build();
+ getPrivateBroadcast(ACTION_PICK_WIFI_NETWORK, notifierTag, requestCode)).build();
+ return createNotificationBuilder(title, network.SSID, notifierTag, requestCode)
+ .addAction(connectAction).addAction(allNetworksAction).build();
}
/**
@@ -125,22 +145,35 @@ public class ConnectToNetworkNotificationBuilder {
private Notification.Builder createNotificationBuilder(
CharSequence title, CharSequence content) {
+ return createNotificationBuilder(title, content, null, 0);
+ }
+
+ private PendingIntent getPrivateBroadcast(String action) {
+ return getPrivateBroadcast(action, null, 0);
+ }
+
+ private Notification.Builder createNotificationBuilder(
+ CharSequence title, CharSequence content, String extraData, int requestCode) {
return mFrameworkFacade.makeNotificationBuilder(mContext,
SystemNotificationChannels.NETWORK_AVAILABLE)
.setSmallIcon(R.drawable.stat_notify_wifi_in_range)
.setTicker(title)
.setContentTitle(title)
.setContentText(content)
- .setDeleteIntent(getPrivateBroadcast(ACTION_USER_DISMISSED_NOTIFICATION))
+ .setDeleteIntent(getPrivateBroadcast(
+ ACTION_USER_DISMISSED_NOTIFICATION, extraData, requestCode))
.setShowWhen(false)
.setLocalOnly(true)
.setColor(mResources.getColor(R.color.system_notification_accent_color,
mContext.getTheme()));
}
- private PendingIntent getPrivateBroadcast(String action) {
+ private PendingIntent getPrivateBroadcast(String action, String extraData, int requestCode) {
Intent intent = new Intent(action).setPackage("android");
- return mFrameworkFacade.getBroadcast(
- mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ if (extraData != null) {
+ intent.putExtra(AVAILABLE_NETWORK_NOTIFIER_TAG, extraData);
+ }
+ return mFrameworkFacade.getBroadcast(mContext, requestCode, intent,
+ PendingIntent.FLAG_UPDATE_CURRENT);
}
}
diff --git a/com/android/server/wifi/DeletedEphemeralSsidsStoreData.java b/com/android/server/wifi/DeletedEphemeralSsidsStoreData.java
index 201a132f..a129495b 100644
--- a/com/android/server/wifi/DeletedEphemeralSsidsStoreData.java
+++ b/com/android/server/wifi/DeletedEphemeralSsidsStoreData.java
@@ -53,6 +53,10 @@ public class DeletedEphemeralSsidsStoreData implements WifiConfigStore.StoreData
@Override
public void deserializeData(XmlPullParser in, int outerTagDepth, boolean shared)
throws XmlPullParserException, IOException {
+ // Ignore empty reads.
+ if (in == null) {
+ return;
+ }
if (shared) {
throw new XmlPullParserException("Share data not supported");
}
diff --git a/com/android/server/wifi/HalDeviceManager.java b/com/android/server/wifi/HalDeviceManager.java
index facb0651..8928472f 100644
--- a/com/android/server/wifi/HalDeviceManager.java
+++ b/com/android/server/wifi/HalDeviceManager.java
@@ -35,6 +35,7 @@ import android.hardware.wifi.V1_0.WifiStatusCode;
import android.hidl.manager.V1_0.IServiceManager;
import android.hidl.manager.V1_0.IServiceNotification;
import android.os.Handler;
+import android.os.HidlSupport.Mutable;
import android.os.HwRemoteBinder;
import android.os.RemoteException;
import android.util.Log;
@@ -2054,18 +2055,6 @@ public class HalDeviceManager {
return typeResp.value;
}
- private static class Mutable<E> {
- public E value;
-
- Mutable() {
- value = null;
- }
-
- Mutable(E value) {
- this.value = value;
- }
- }
-
/**
* Dump the internal state of the class.
*/
diff --git a/com/android/server/wifi/HostapdHal.java b/com/android/server/wifi/HostapdHal.java
new file mode 100644
index 00000000..efda54f5
--- /dev/null
+++ b/com/android/server/wifi/HostapdHal.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.annotation.NonNull;
+import android.content.Context;
+import android.hardware.wifi.hostapd.V1_0.HostapdStatus;
+import android.hardware.wifi.hostapd.V1_0.HostapdStatusCode;
+import android.hardware.wifi.hostapd.V1_0.IHostapd;
+import android.hidl.manager.V1_0.IServiceManager;
+import android.hidl.manager.V1_0.IServiceNotification;
+import android.net.wifi.WifiConfiguration;
+import android.os.HwRemoteBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.server.wifi.WifiNative.HostapdDeathEventHandler;
+import com.android.server.wifi.util.NativeUtil;
+
+import javax.annotation.concurrent.ThreadSafe;
+
+/**
+ * To maintain thread-safety, the locking protocol is that every non-static method (regardless of
+ * access level) acquires mLock.
+ */
+@ThreadSafe
+public class HostapdHal {
+ private static final String TAG = "HostapdHal";
+
+ private final Object mLock = new Object();
+ private boolean mVerboseLoggingEnabled = false;
+ private final boolean mEnableAcs;
+ private final boolean mEnableIeee80211AC;
+
+ // Hostapd HAL interface objects
+ private IServiceManager mIServiceManager = null;
+ private IHostapd mIHostapd;
+ private HostapdDeathEventHandler mDeathEventHandler;
+
+ private final IServiceNotification mServiceNotificationCallback =
+ new IServiceNotification.Stub() {
+ public void onRegistration(String fqName, String name, boolean preexisting) {
+ synchronized (mLock) {
+ if (mVerboseLoggingEnabled) {
+ Log.i(TAG, "IServiceNotification.onRegistration for: " + fqName
+ + ", " + name + " preexisting=" + preexisting);
+ }
+ if (!initHostapdService()) {
+ Log.e(TAG, "initalizing IHostapd failed.");
+ hostapdServiceDiedHandler();
+ } else {
+ Log.i(TAG, "Completed initialization of IHostapd.");
+ }
+ }
+ }
+ };
+ private final HwRemoteBinder.DeathRecipient mServiceManagerDeathRecipient =
+ cookie -> {
+ synchronized (mLock) {
+ Log.w(TAG, "IServiceManager died: cookie=" + cookie);
+ hostapdServiceDiedHandler();
+ mIServiceManager = null; // Will need to register a new ServiceNotification
+ }
+ };
+ private final HwRemoteBinder.DeathRecipient mHostapdDeathRecipient =
+ cookie -> {
+ synchronized (mLock) {
+ Log.w(TAG, "IHostapd/IHostapd died: cookie=" + cookie);
+ hostapdServiceDiedHandler();
+ }
+ };
+
+
+ public HostapdHal(Context context) {
+ mEnableAcs = context.getResources().getBoolean(R.bool.config_wifi_softap_acs_supported);
+ mEnableIeee80211AC =
+ context.getResources().getBoolean(R.bool.config_wifi_softap_ieee80211ac_supported);
+ }
+
+ /**
+ * Enable/Disable verbose logging.
+ *
+ * @param enable true to enable, false to disable.
+ */
+ void enableVerboseLogging(boolean enable) {
+ synchronized (mLock) {
+ mVerboseLoggingEnabled = enable;
+ }
+ }
+
+ /**
+ * Link to death for IServiceManager object.
+ * @return true on success, false otherwise.
+ */
+ private boolean linkToServiceManagerDeath() {
+ synchronized (mLock) {
+ if (mIServiceManager == null) return false;
+ try {
+ if (!mIServiceManager.linkToDeath(mServiceManagerDeathRecipient, 0)) {
+ Log.wtf(TAG, "Error on linkToDeath on IServiceManager");
+ hostapdServiceDiedHandler();
+ mIServiceManager = null; // Will need to register a new ServiceNotification
+ return false;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "IServiceManager.linkToDeath exception", e);
+ mIServiceManager = null; // Will need to register a new ServiceNotification
+ return false;
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Registers a service notification for the IHostapd service, which triggers intialization of
+ * the IHostapd
+ * @return true if the service notification was successfully registered
+ */
+ public boolean initialize() {
+ synchronized (mLock) {
+ if (mVerboseLoggingEnabled) {
+ Log.i(TAG, "Registering IHostapd service ready callback.");
+ }
+ mIHostapd = null;
+ if (mIServiceManager != null) {
+ // Already have an IServiceManager and serviceNotification registered, don't
+ // don't register another.
+ return true;
+ }
+ try {
+ mIServiceManager = getServiceManagerMockable();
+ if (mIServiceManager == null) {
+ Log.e(TAG, "Failed to get HIDL Service Manager");
+ return false;
+ }
+ if (!linkToServiceManagerDeath()) {
+ return false;
+ }
+ /* TODO(b/33639391) : Use the new IHostapd.registerForNotifications() once it
+ exists */
+ if (!mIServiceManager.registerForNotifications(
+ IHostapd.kInterfaceName, "", mServiceNotificationCallback)) {
+ Log.e(TAG, "Failed to register for notifications to "
+ + IHostapd.kInterfaceName);
+ mIServiceManager = null; // Will need to register a new ServiceNotification
+ return false;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Exception while trying to register a listener for IHostapd service: "
+ + e);
+ hostapdServiceDiedHandler();
+ mIServiceManager = null; // Will need to register a new ServiceNotification
+ return false;
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Link to death for IHostapd object.
+ * @return true on success, false otherwise.
+ */
+ private boolean linkToHostapdDeath() {
+ synchronized (mLock) {
+ if (mIHostapd == null) return false;
+ try {
+ if (!mIHostapd.linkToDeath(mHostapdDeathRecipient, 0)) {
+ Log.wtf(TAG, "Error on linkToDeath on IHostapd");
+ hostapdServiceDiedHandler();
+ return false;
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "IHostapd.linkToDeath exception", e);
+ return false;
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Initialize the IHostapd object.
+ * @return true on success, false otherwise.
+ */
+ private boolean initHostapdService() {
+ synchronized (mLock) {
+ try {
+ mIHostapd = getHostapdMockable();
+ } catch (RemoteException e) {
+ Log.e(TAG, "IHostapd.getService exception: " + e);
+ return false;
+ }
+ if (mIHostapd == null) {
+ Log.e(TAG, "Got null IHostapd service. Stopping hostapd HIDL startup");
+ return false;
+ }
+ if (!linkToHostapdDeath()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * Add and start a new access point.
+ *
+ * @param ifaceName Name of the interface.
+ * @param config Configuration to use for the AP.
+ * @return true on success, false otherwise.
+ */
+ public boolean addAccessPoint(@NonNull String ifaceName, @NonNull WifiConfiguration config) {
+ synchronized (mLock) {
+ final String methodStr = "addAccessPoint";
+ IHostapd.IfaceParams ifaceParams = new IHostapd.IfaceParams();
+ ifaceParams.ifaceName = ifaceName;
+ ifaceParams.hwModeParams.enable80211N = true;
+ ifaceParams.hwModeParams.enable80211AC = mEnableIeee80211AC;
+ try {
+ ifaceParams.channelParams.band = getBand(config);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Unrecognized apBand " + config.apBand);
+ return false;
+ }
+ if (mEnableAcs) {
+ ifaceParams.channelParams.enableAcs = true;
+ ifaceParams.channelParams.acsShouldExcludeDfs = true;
+ } else {
+ // Downgrade IHostapd.Band.BAND_ANY to IHostapd.Band.BAND_2_4_GHZ if ACS
+ // is not supported.
+ // We should remove this workaround once channel selection is moved from
+ // ApConfigUtil to here.
+ if (ifaceParams.channelParams.band == IHostapd.Band.BAND_ANY) {
+ Log.d(TAG, "ACS is not supported on this device, using 2.4 GHz band.");
+ ifaceParams.channelParams.band = IHostapd.Band.BAND_2_4_GHZ;
+ }
+ ifaceParams.channelParams.enableAcs = false;
+ ifaceParams.channelParams.channel = config.apChannel;
+ }
+
+ IHostapd.NetworkParams nwParams = new IHostapd.NetworkParams();
+ // TODO(b/67745880) Note that config.SSID is intended to be either a
+ // hex string or "double quoted".
+ // However, it seems that whatever is handing us these configurations does not obey
+ // this convention.
+ nwParams.ssid.addAll(NativeUtil.stringToByteArrayList(config.SSID));
+ nwParams.isHidden = config.hiddenSSID;
+ nwParams.encryptionType = getEncryptionType(config);
+ nwParams.pskPassphrase = (config.preSharedKey != null) ? config.preSharedKey : "";
+ if (!checkHostapdAndLogFailure(methodStr)) return false;
+ try {
+ HostapdStatus status = mIHostapd.addAccessPoint(ifaceParams, nwParams);
+ return checkStatusAndLogFailure(status, methodStr);
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Remove a previously started access point.
+ *
+ * @param ifaceName Name of the interface.
+ * @return true on success, false otherwise.
+ */
+ public boolean removeAccessPoint(@NonNull String ifaceName) {
+ synchronized (mLock) {
+ final String methodStr = "removeAccessPoint";
+ if (!checkHostapdAndLogFailure(methodStr)) return false;
+ try {
+ HostapdStatus status = mIHostapd.removeAccessPoint(ifaceName);
+ return checkStatusAndLogFailure(status, methodStr);
+ } catch (RemoteException e) {
+ handleRemoteException(e, methodStr);
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Registers a death notification for hostapd.
+ * @return Returns true on success.
+ */
+ public boolean registerDeathHandler(@NonNull HostapdDeathEventHandler handler) {
+ if (mDeathEventHandler != null) {
+ Log.e(TAG, "Death handler already present");
+ }
+ mDeathEventHandler = handler;
+ return true;
+ }
+
+ /**
+ * Deregisters a death notification for hostapd.
+ * @return Returns true on success.
+ */
+ public boolean deregisterDeathHandler() {
+ if (mDeathEventHandler == null) {
+ Log.e(TAG, "No Death handler present");
+ }
+ mDeathEventHandler = null;
+ return true;
+ }
+
+ /**
+ * Clear internal state.
+ */
+ private void clearState() {
+ synchronized (mLock) {
+ mIHostapd = null;
+ }
+ }
+
+ /**
+ * Handle hostapd death.
+ */
+ private void hostapdServiceDiedHandler() {
+ synchronized (mLock) {
+ if (mDeathEventHandler != null) {
+ mDeathEventHandler.onDeath();
+ }
+ clearState();
+ }
+ }
+
+ /**
+ * Signals whether Initialization completed successfully.
+ */
+ public boolean isInitializationStarted() {
+ synchronized (mLock) {
+ return mIServiceManager != null;
+ }
+ }
+
+ /**
+ * Signals whether Initialization completed successfully.
+ */
+ public boolean isInitializationComplete() {
+ synchronized (mLock) {
+ return mIHostapd != null;
+ }
+ }
+
+ /**
+ * Wrapper functions to access static HAL methods, created to be mockable in unit tests
+ */
+ @VisibleForTesting
+ protected IServiceManager getServiceManagerMockable() throws RemoteException {
+ synchronized (mLock) {
+ return IServiceManager.getService();
+ }
+ }
+
+ @VisibleForTesting
+ protected IHostapd getHostapdMockable() throws RemoteException {
+ synchronized (mLock) {
+ return IHostapd.getService();
+ }
+ }
+
+ private static int getEncryptionType(WifiConfiguration localConfig) {
+ int encryptionType;
+ switch (localConfig.getAuthType()) {
+ case WifiConfiguration.KeyMgmt.NONE:
+ encryptionType = IHostapd.EncryptionType.NONE;
+ break;
+ case WifiConfiguration.KeyMgmt.WPA_PSK:
+ encryptionType = IHostapd.EncryptionType.WPA;
+ break;
+ case WifiConfiguration.KeyMgmt.WPA2_PSK:
+ encryptionType = IHostapd.EncryptionType.WPA2;
+ break;
+ default:
+ // We really shouldn't default to None, but this was how NetworkManagementService
+ // used to do this.
+ encryptionType = IHostapd.EncryptionType.NONE;
+ break;
+ }
+ return encryptionType;
+ }
+
+ private static int getBand(WifiConfiguration localConfig) {
+ int bandType;
+ switch (localConfig.apBand) {
+ case WifiConfiguration.AP_BAND_2GHZ:
+ bandType = IHostapd.Band.BAND_2_4_GHZ;
+ break;
+ case WifiConfiguration.AP_BAND_5GHZ:
+ bandType = IHostapd.Band.BAND_5_GHZ;
+ break;
+ case WifiConfiguration.AP_BAND_ANY:
+ bandType = IHostapd.Band.BAND_ANY;
+ break;
+ default:
+ throw new IllegalArgumentException();
+ }
+ return bandType;
+ }
+
+ /**
+ * Returns false if Hostapd is null, and logs failure to call methodStr
+ */
+ private boolean checkHostapdAndLogFailure(String methodStr) {
+ synchronized (mLock) {
+ if (mIHostapd == null) {
+ Log.e(TAG, "Can't call " + methodStr + ", IHostapd is null");
+ return false;
+ }
+ return true;
+ }
+ }
+
+ /**
+ * Returns true if provided status code is SUCCESS, logs debug message and returns false
+ * otherwise
+ */
+ private boolean checkStatusAndLogFailure(HostapdStatus status,
+ String methodStr) {
+ synchronized (mLock) {
+ if (status.code != HostapdStatusCode.SUCCESS) {
+ Log.e(TAG, "IHostapd." + methodStr + " failed: " + status.code
+ + ", " + status.debugMessage);
+ return false;
+ } else {
+ if (mVerboseLoggingEnabled) {
+ Log.d(TAG, "IHostapd." + methodStr + " succeeded");
+ }
+ return true;
+ }
+ }
+ }
+
+ private void handleRemoteException(RemoteException e, String methodStr) {
+ synchronized (mLock) {
+ hostapdServiceDiedHandler();
+ Log.e(TAG, "IHostapd." + methodStr + " failed with exception", e);
+ }
+ }
+
+ private static void logd(String s) {
+ Log.d(TAG, s);
+ }
+
+ private static void logi(String s) {
+ Log.i(TAG, s);
+ }
+
+ private static void loge(String s) {
+ Log.e(TAG, s);
+ }
+}
diff --git a/com/android/server/wifi/NetworkListStoreData.java b/com/android/server/wifi/NetworkListStoreData.java
index f287d4b9..47f5e8d9 100644
--- a/com/android/server/wifi/NetworkListStoreData.java
+++ b/com/android/server/wifi/NetworkListStoreData.java
@@ -83,6 +83,10 @@ public class NetworkListStoreData implements WifiConfigStore.StoreData {
@Override
public void deserializeData(XmlPullParser in, int outerTagDepth, boolean shared)
throws XmlPullParserException, IOException {
+ // Ignore empty reads.
+ if (in == null) {
+ return;
+ }
if (shared) {
mSharedConfigurations = parseNetworkList(in, outerTagDepth);
} else {
diff --git a/com/android/server/wifi/OpenNetworkNotifier.java b/com/android/server/wifi/OpenNetworkNotifier.java
index eee4ac53..c951402a 100644
--- a/com/android/server/wifi/OpenNetworkNotifier.java
+++ b/com/android/server/wifi/OpenNetworkNotifier.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 The Android Open Source Project
+ * 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.
@@ -16,127 +16,24 @@
package com.android.server.wifi;
-import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_CONNECT_TO_NETWORK;
-import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_PICK_WIFI_NETWORK;
-import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE;
-import static com.android.server.wifi.ConnectToNetworkNotificationBuilder.ACTION_USER_DISMISSED_NOTIFICATION;
-
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.content.BroadcastReceiver;
import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.database.ContentObserver;
-import android.net.wifi.ScanResult;
-import android.net.wifi.WifiConfiguration;
-import android.net.wifi.WifiManager;
-import android.os.Handler;
import android.os.Looper;
-import android.os.Message;
-import android.os.Messenger;
-import android.os.UserHandle;
-import android.os.UserManager;
import android.provider.Settings;
-import android.text.TextUtils;
-import android.util.ArraySet;
-import android.util.Log;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.server.wifi.nano.WifiMetricsProto.ConnectToNetworkNotificationAndActionCount;
-import com.android.server.wifi.util.ScanResultUtil;
-
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.List;
-import java.util.Set;
/**
- * Takes care of handling the "open wi-fi network available" notification
+ * This class handles the "open wi-fi network available" notification
*
* NOTE: These API's are not thread safe and should only be used from WifiStateMachine thread.
- * @hide
*/
-public class OpenNetworkNotifier {
-
- private static final String TAG = "OpenNetworkNotifier";
-
- /** Time in milliseconds to display the Connecting notification. */
- private static final int TIME_TO_SHOW_CONNECTING_MILLIS = 10000;
-
- /** Time in milliseconds to display the Connected notification. */
- private static final int TIME_TO_SHOW_CONNECTED_MILLIS = 5000;
-
- /** Time in milliseconds to display the Failed To Connect notification. */
- private static final int TIME_TO_SHOW_FAILED_MILLIS = 5000;
-
- /** The state of the notification */
- @IntDef({
- STATE_NO_NOTIFICATION,
- STATE_SHOWING_RECOMMENDATION_NOTIFICATION,
- STATE_CONNECTING_IN_NOTIFICATION,
- STATE_CONNECTED_NOTIFICATION,
- STATE_CONNECT_FAILED_NOTIFICATION
- })
- @Retention(RetentionPolicy.SOURCE)
- private @interface State {}
-
- /** No recommendation is made and no notifications are shown. */
- private static final int STATE_NO_NOTIFICATION = 0;
- /** The initial notification recommending an open network to connect to is shown. */
- private static final int STATE_SHOWING_RECOMMENDATION_NOTIFICATION = 1;
- /** The notification of status of connecting to the recommended network is shown. */
- private static final int STATE_CONNECTING_IN_NOTIFICATION = 2;
- /** The notification that the connection to the recommended network was successful is shown. */
- private static final int STATE_CONNECTED_NOTIFICATION = 3;
- /** The notification to show that connection to the recommended network failed is shown. */
- private static final int STATE_CONNECT_FAILED_NOTIFICATION = 4;
-
- /** Current state of the notification. */
- @State private int mState = STATE_NO_NOTIFICATION;
-
- /** Identifier of the {@link SsidSetStoreData}. */
+public class OpenNetworkNotifier extends AvailableNetworkNotifier {
+ public static final String TAG = "WifiOpenNetworkNotifier";
private static final String STORE_DATA_IDENTIFIER = "OpenNetworkNotifierBlacklist";
- /**
- * The {@link Clock#getWallClockMillis()} must be at least this value for us
- * to show the notification again.
- */
- private long mNotificationRepeatTime;
- /**
- * When a notification is shown, we wait this amount before possibly showing it again.
- */
- private final long mNotificationRepeatDelay;
- /** Default repeat delay in seconds. */
- @VisibleForTesting
- static final int DEFAULT_REPEAT_DELAY_SEC = 900;
-
- /** Whether the user has set the setting to show the 'available networks' notification. */
- private boolean mSettingEnabled;
- /** Whether the screen is on or not. */
- private boolean mScreenOn;
-
- /** List of SSIDs blacklisted from recommendation. */
- private final Set<String> mBlacklistedSsids;
-
- private final Context mContext;
- private final Handler mHandler;
- private final FrameworkFacade mFrameworkFacade;
- private final WifiMetrics mWifiMetrics;
- private final Clock mClock;
- private final WifiConfigManager mConfigManager;
- private final WifiStateMachine mWifiStateMachine;
- private final Messenger mSrcMessenger;
- private final OpenNetworkRecommender mOpenNetworkRecommender;
- private final ConnectToNetworkNotificationBuilder mNotificationBuilder;
-
- private ScanResult mRecommendedNetwork;
+ private static final String TOGGLE_SETTINGS_NAME =
+ Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON;
- OpenNetworkNotifier(
+ public OpenNetworkNotifier(
Context context,
Looper looper,
FrameworkFacade framework,
@@ -145,354 +42,10 @@ public class OpenNetworkNotifier {
WifiConfigManager wifiConfigManager,
WifiConfigStore wifiConfigStore,
WifiStateMachine wifiStateMachine,
- OpenNetworkRecommender openNetworkRecommender,
ConnectToNetworkNotificationBuilder connectToNetworkNotificationBuilder) {
- mContext = context;
- mHandler = new Handler(looper);
- mFrameworkFacade = framework;
- mWifiMetrics = wifiMetrics;
- mClock = clock;
- mConfigManager = wifiConfigManager;
- mWifiStateMachine = wifiStateMachine;
- mOpenNetworkRecommender = openNetworkRecommender;
- mNotificationBuilder = connectToNetworkNotificationBuilder;
- mScreenOn = false;
- mSrcMessenger = new Messenger(new Handler(looper, mConnectionStateCallback));
-
- mBlacklistedSsids = new ArraySet<>();
- wifiConfigStore.registerStoreData(new SsidSetStoreData(
- STORE_DATA_IDENTIFIER, new OpenNetworkNotifierStoreData()));
-
- // Setting is in seconds
- mNotificationRepeatDelay = mFrameworkFacade.getIntegerSetting(context,
- Settings.Global.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY,
- DEFAULT_REPEAT_DELAY_SEC) * 1000L;
- NotificationEnabledSettingObserver settingObserver = new NotificationEnabledSettingObserver(
- mHandler);
- settingObserver.register();
-
- IntentFilter filter = new IntentFilter();
- filter.addAction(ACTION_USER_DISMISSED_NOTIFICATION);
- filter.addAction(ACTION_CONNECT_TO_NETWORK);
- filter.addAction(ACTION_PICK_WIFI_NETWORK);
- filter.addAction(ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE);
- mContext.registerReceiver(
- mBroadcastReceiver, filter, null /* broadcastPermission */, mHandler);
- }
-
- private final BroadcastReceiver mBroadcastReceiver =
- new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- switch (intent.getAction()) {
- case ACTION_USER_DISMISSED_NOTIFICATION:
- handleUserDismissedAction();
- break;
- case ACTION_CONNECT_TO_NETWORK:
- handleConnectToNetworkAction();
- break;
- case ACTION_PICK_WIFI_NETWORK:
- handleSeeAllNetworksAction();
- break;
- case ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE:
- handlePickWifiNetworkAfterConnectFailure();
- break;
- default:
- Log.e(TAG, "Unknown action " + intent.getAction());
- }
- }
- };
-
- private final Handler.Callback mConnectionStateCallback = (Message msg) -> {
- switch (msg.what) {
- // Success here means that an attempt to connect to the network has been initiated.
- // Successful connection updates are received via the
- // WifiConnectivityManager#handleConnectionStateChanged() callback.
- case WifiManager.CONNECT_NETWORK_SUCCEEDED:
- break;
- case WifiManager.CONNECT_NETWORK_FAILED:
- handleConnectionAttemptFailedToSend();
- break;
- default:
- Log.e(TAG, "Unknown message " + msg.what);
- }
- return true;
- };
-
- /**
- * Clears the pending notification. This is called by {@link WifiConnectivityManager} on stop.
- *
- * @param resetRepeatTime resets the time delay for repeated notification if true.
- */
- public void clearPendingNotification(boolean resetRepeatTime) {
- if (resetRepeatTime) {
- mNotificationRepeatTime = 0;
- }
-
- if (mState != STATE_NO_NOTIFICATION) {
- getNotificationManager().cancel(SystemMessage.NOTE_NETWORK_AVAILABLE);
-
- if (mRecommendedNetwork != null) {
- Log.d(TAG, "Notification with state="
- + mState
- + " was cleared for recommended network: "
- + mRecommendedNetwork.SSID);
- }
- mState = STATE_NO_NOTIFICATION;
- mRecommendedNetwork = null;
- }
- }
-
- private boolean isControllerEnabled() {
- return mSettingEnabled && !UserManager.get(mContext)
- .hasUserRestriction(UserManager.DISALLOW_CONFIG_WIFI, UserHandle.CURRENT);
- }
-
- /**
- * If there are open networks, attempt to post an open network notification.
- *
- * @param availableNetworks Available networks from
- * {@link WifiNetworkSelector.NetworkEvaluator#getFilteredScanDetailsForOpenUnsavedNetworks()}.
- */
- public void handleScanResults(@NonNull List<ScanDetail> availableNetworks) {
- if (!isControllerEnabled()) {
- clearPendingNotification(true /* resetRepeatTime */);
- return;
- }
- if (availableNetworks.isEmpty()) {
- clearPendingNotification(false /* resetRepeatTime */);
- return;
- }
-
- // Not enough time has passed to show a recommendation notification again
- if (mState == STATE_NO_NOTIFICATION
- && mClock.getWallClockMillis() < mNotificationRepeatTime) {
- return;
- }
-
- // Do nothing when the screen is off and no notification is showing.
- if (mState == STATE_NO_NOTIFICATION && !mScreenOn) {
- return;
- }
-
- // Only show a new or update an existing recommendation notification.
- if (mState == STATE_NO_NOTIFICATION
- || mState == STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
- ScanResult recommendation = mOpenNetworkRecommender.recommendNetwork(
- availableNetworks, new ArraySet<>(mBlacklistedSsids));
-
- if (recommendation != null) {
- postInitialNotification(recommendation);
- } else {
- clearPendingNotification(false /* resetRepeatTime */);
- }
- }
- }
-
- /** Handles screen state changes. */
- public void handleScreenStateChanged(boolean screenOn) {
- mScreenOn = screenOn;
- }
-
- /**
- * Called by {@link WifiConnectivityManager} when Wi-Fi is connected. If the notification
- * was in the connecting state, update the notification to show that it has connected to the
- * recommended network.
- */
- public void handleWifiConnected() {
- if (mState != STATE_CONNECTING_IN_NOTIFICATION) {
- clearPendingNotification(true /* resetRepeatTime */);
- return;
- }
-
- postNotification(mNotificationBuilder.createNetworkConnectedNotification(
- mRecommendedNetwork));
-
- Log.d(TAG, "User connected to recommended network: " + mRecommendedNetwork.SSID);
- mWifiMetrics.incrementConnectToNetworkNotification(
- ConnectToNetworkNotificationAndActionCount.NOTIFICATION_CONNECTED_TO_NETWORK);
- mState = STATE_CONNECTED_NOTIFICATION;
- mHandler.postDelayed(
- () -> {
- if (mState == STATE_CONNECTED_NOTIFICATION) {
- clearPendingNotification(true /* resetRepeatTime */);
- }
- },
- TIME_TO_SHOW_CONNECTED_MILLIS);
- }
-
- /**
- * Handles when a Wi-Fi connection attempt failed.
- */
- public void handleConnectionFailure() {
- if (mState != STATE_CONNECTING_IN_NOTIFICATION) {
- return;
- }
- postNotification(mNotificationBuilder.createNetworkFailedNotification());
-
- Log.d(TAG, "User failed to connect to recommended network: " + mRecommendedNetwork.SSID);
- mWifiMetrics.incrementConnectToNetworkNotification(
- ConnectToNetworkNotificationAndActionCount.NOTIFICATION_FAILED_TO_CONNECT);
- mState = STATE_CONNECT_FAILED_NOTIFICATION;
- mHandler.postDelayed(
- () -> {
- if (mState == STATE_CONNECT_FAILED_NOTIFICATION) {
- clearPendingNotification(false /* resetRepeatTime */);
- }
- },
- TIME_TO_SHOW_FAILED_MILLIS);
- }
-
- private NotificationManager getNotificationManager() {
- return (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);
- }
-
- private void postInitialNotification(ScanResult recommendedNetwork) {
- if (mRecommendedNetwork != null
- && TextUtils.equals(mRecommendedNetwork.SSID, recommendedNetwork.SSID)) {
- return;
- }
- postNotification(mNotificationBuilder.createConnectToNetworkNotification(
- recommendedNetwork));
- if (mState == STATE_NO_NOTIFICATION) {
- mWifiMetrics.incrementConnectToNetworkNotification(
- ConnectToNetworkNotificationAndActionCount.NOTIFICATION_RECOMMEND_NETWORK);
- } else {
- mWifiMetrics.incrementNumOpenNetworkRecommendationUpdates();
- }
- mState = STATE_SHOWING_RECOMMENDATION_NOTIFICATION;
- mRecommendedNetwork = recommendedNetwork;
- mNotificationRepeatTime = mClock.getWallClockMillis() + mNotificationRepeatDelay;
- }
-
- private void postNotification(Notification notification) {
- getNotificationManager().notify(SystemMessage.NOTE_NETWORK_AVAILABLE, notification);
- }
-
- private void handleConnectToNetworkAction() {
- mWifiMetrics.incrementConnectToNetworkNotificationAction(mState,
- ConnectToNetworkNotificationAndActionCount.ACTION_CONNECT_TO_NETWORK);
- if (mState != STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
- return;
- }
- postNotification(mNotificationBuilder.createNetworkConnectingNotification(
- mRecommendedNetwork));
- mWifiMetrics.incrementConnectToNetworkNotification(
- ConnectToNetworkNotificationAndActionCount.NOTIFICATION_CONNECTING_TO_NETWORK);
-
- Log.d(TAG, "User initiated connection to recommended network: " + mRecommendedNetwork.SSID);
- WifiConfiguration network = ScanResultUtil.createNetworkFromScanResult(mRecommendedNetwork);
- Message msg = Message.obtain();
- msg.what = WifiManager.CONNECT_NETWORK;
- msg.arg1 = WifiConfiguration.INVALID_NETWORK_ID;
- msg.obj = network;
- msg.replyTo = mSrcMessenger;
- mWifiStateMachine.sendMessage(msg);
-
- mState = STATE_CONNECTING_IN_NOTIFICATION;
- mHandler.postDelayed(
- () -> {
- if (mState == STATE_CONNECTING_IN_NOTIFICATION) {
- handleConnectionFailure();
- }
- },
- TIME_TO_SHOW_CONNECTING_MILLIS);
- }
-
- private void handleSeeAllNetworksAction() {
- mWifiMetrics.incrementConnectToNetworkNotificationAction(mState,
- ConnectToNetworkNotificationAndActionCount.ACTION_PICK_WIFI_NETWORK);
- startWifiSettings();
- }
-
- private void startWifiSettings() {
- // Close notification drawer before opening the picker.
- mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
- mContext.startActivity(
- new Intent(Settings.ACTION_WIFI_SETTINGS)
- .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
- clearPendingNotification(false /* resetRepeatTime */);
- }
-
- private void handleConnectionAttemptFailedToSend() {
- handleConnectionFailure();
- mWifiMetrics.incrementNumOpenNetworkConnectMessageFailedToSend();
- }
-
- private void handlePickWifiNetworkAfterConnectFailure() {
- mWifiMetrics.incrementConnectToNetworkNotificationAction(mState,
- ConnectToNetworkNotificationAndActionCount
- .ACTION_PICK_WIFI_NETWORK_AFTER_CONNECT_FAILURE);
- startWifiSettings();
- }
-
- private void handleUserDismissedAction() {
- Log.d(TAG, "User dismissed notification with state=" + mState);
- mWifiMetrics.incrementConnectToNetworkNotificationAction(mState,
- ConnectToNetworkNotificationAndActionCount.ACTION_USER_DISMISSED_NOTIFICATION);
- if (mState == STATE_SHOWING_RECOMMENDATION_NOTIFICATION) {
- // blacklist dismissed network
- mBlacklistedSsids.add(mRecommendedNetwork.SSID);
- mWifiMetrics.setOpenNetworkRecommenderBlacklistSize(mBlacklistedSsids.size());
- mConfigManager.saveToStore(false /* forceWrite */);
- Log.d(TAG, "Network is added to the open network notification blacklist: "
- + mRecommendedNetwork.SSID);
- }
- resetStateAndDelayNotification();
- }
-
- private void resetStateAndDelayNotification() {
- mState = STATE_NO_NOTIFICATION;
- mNotificationRepeatTime = System.currentTimeMillis() + mNotificationRepeatDelay;
- mRecommendedNetwork = null;
- }
-
- /** Dump ONA controller state. */
- public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- pw.println("OpenNetworkNotifier: ");
- pw.println("mSettingEnabled " + mSettingEnabled);
- pw.println("currentTime: " + mClock.getWallClockMillis());
- pw.println("mNotificationRepeatTime: " + mNotificationRepeatTime);
- pw.println("mState: " + mState);
- pw.println("mBlacklistedSsids: " + mBlacklistedSsids.toString());
- }
-
- private class OpenNetworkNotifierStoreData implements SsidSetStoreData.DataSource {
- @Override
- public Set<String> getSsids() {
- return new ArraySet<>(mBlacklistedSsids);
- }
-
- @Override
- public void setSsids(Set<String> ssidList) {
- mBlacklistedSsids.addAll(ssidList);
- mWifiMetrics.setOpenNetworkRecommenderBlacklistSize(mBlacklistedSsids.size());
- }
- }
-
- private class NotificationEnabledSettingObserver extends ContentObserver {
- NotificationEnabledSettingObserver(Handler handler) {
- super(handler);
- }
-
- public void register() {
- mFrameworkFacade.registerContentObserver(mContext, Settings.Global.getUriFor(
- Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON), true, this);
- mSettingEnabled = getValue();
- }
-
- @Override
- public void onChange(boolean selfChange) {
- super.onChange(selfChange);
- mSettingEnabled = getValue();
- clearPendingNotification(true /* resetRepeatTime */);
- }
-
- private boolean getValue() {
- boolean enabled = mFrameworkFacade.getIntegerSetting(mContext,
- Settings.Global.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, 1) == 1;
- mWifiMetrics.setIsWifiNetworksAvailableNotificationEnabled(enabled);
- return enabled;
- }
+ super(TAG, STORE_DATA_IDENTIFIER, TOGGLE_SETTINGS_NAME,
+ SystemMessage.NOTE_NETWORK_AVAILABLE, context, looper, framework, clock,
+ wifiMetrics, wifiConfigManager, wifiConfigStore, wifiStateMachine,
+ connectToNetworkNotificationBuilder);
}
}
diff --git a/com/android/server/wifi/OpenNetworkRecommender.java b/com/android/server/wifi/OpenNetworkRecommender.java
deleted file mode 100644
index 5ceeddde..00000000
--- a/com/android/server/wifi/OpenNetworkRecommender.java
+++ /dev/null
@@ -1,58 +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.wifi;
-
-import android.annotation.NonNull;
-import android.net.wifi.ScanResult;
-
-import java.util.List;
-import java.util.Set;
-
-/**
- * Helps recommend the best available network for {@link OpenNetworkNotifier}.
- *
- * NOTE: These API's are not thread safe and should only be used from WifiStateMachine thread.
- * @hide
- */
-public class OpenNetworkRecommender {
-
- /**
- * Recommends the network with the best signal strength.
- *
- * @param networks List of scan details to pick a recommendation. This list should not be null
- * or empty.
- * @param blacklistedSsids The list of SSIDs that should not be recommended.
- */
- public ScanResult recommendNetwork(@NonNull List<ScanDetail> networks,
- @NonNull Set<String> blacklistedSsids) {
- ScanResult result = null;
- int highestRssi = Integer.MIN_VALUE;
- for (ScanDetail scanDetail : networks) {
- ScanResult scanResult = scanDetail.getScanResult();
-
- if (scanResult.level > highestRssi) {
- result = scanResult;
- highestRssi = scanResult.level;
- }
- }
-
- if (result != null && blacklistedSsids.contains(result.SSID)) {
- result = null;
- }
- return result;
- }
-}
diff --git a/com/android/server/wifi/PropertyService.java b/com/android/server/wifi/PropertyService.java
index c998ff95..ea7fa63b 100644
--- a/com/android/server/wifi/PropertyService.java
+++ b/com/android/server/wifi/PropertyService.java
@@ -43,4 +43,9 @@ public interface PropertyService {
*/
boolean getBoolean(String key, boolean defaultValue);
+ /**
+ * Get the current value of |key|.
+ * @return value of |key|, if key exists; |defaultValue| otherwise
+ */
+ String getString(String key, String defaultValue);
}
diff --git a/com/android/server/wifi/RttService.java b/com/android/server/wifi/RttService.java
index bd273672..00847c84 100644
--- a/com/android/server/wifi/RttService.java
+++ b/com/android/server/wifi/RttService.java
@@ -479,7 +479,9 @@ public final class RttService extends SystemService {
if (mResponderConfig != null) {
// TODO: remove once mac address is added when enabling responder.
- mResponderConfig.macAddress = mWifiNative.getMacAddress();
+ mResponderConfig.macAddress =
+ mWifiNative.getMacAddress(
+ mWifiNative.getClientInterfaceName());
ci.addResponderRequest(key);
ci.reportResponderEnableSucceed(key, mResponderConfig);
transitionTo(mResponderEnabledState);
diff --git a/com/android/server/wifi/ScanDetailCache.java b/com/android/server/wifi/ScanDetailCache.java
index abb6ad8b..b7f54116 100644
--- a/com/android/server/wifi/ScanDetailCache.java
+++ b/com/android/server/wifi/ScanDetailCache.java
@@ -150,12 +150,6 @@ public class ScanDetailCache {
public int compare(Object o1, Object o2) {
ScanResult a = ((ScanDetail) o1).getScanResult();
ScanResult b = ((ScanDetail) o2).getScanResult();
- if (a.numIpConfigFailures > b.numIpConfigFailures) {
- return 1;
- }
- if (a.numIpConfigFailures < b.numIpConfigFailures) {
- return -1;
- }
if (a.seen > b.seen) {
return -1;
}
@@ -273,10 +267,6 @@ public class ScanDetailCache {
sbuf.append(String.format(",%4d.%02d.%02d.%02d.%03dms", ageDay,
ageHour, ageMin, ageSec, ageMilli));
}
- if (result.numIpConfigFailures > 0) {
- sbuf.append(",ipfail=");
- sbuf.append(result.numIpConfigFailures);
- }
sbuf.append("} ");
}
sbuf.append('\n');
diff --git a/com/android/server/wifi/ScanOnlyModeManager.java b/com/android/server/wifi/ScanOnlyModeManager.java
index 1edb857f..78919e5b 100644
--- a/com/android/server/wifi/ScanOnlyModeManager.java
+++ b/com/android/server/wifi/ScanOnlyModeManager.java
@@ -16,27 +16,251 @@
package com.android.server.wifi;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.WifiManager;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+import com.android.server.wifi.WifiNative.InterfaceCallback;
+
/**
* Manager WiFi in Scan Only Mode - no network connections.
*/
public class ScanOnlyModeManager implements ActiveModeManager {
+ private final ScanOnlyModeStateMachine mStateMachine;
+
private static final String TAG = "ScanOnlyModeManager";
- ScanOnlyModeManager() {
+ private final Context mContext;
+ private final WifiNative mWifiNative;
+
+ private final WifiMetrics mWifiMetrics;
+ private final Listener mListener;
+ private final ScanRequestProxy mScanRequestProxy;
+ private final WakeupController mWakeupController;
+
+ private String mClientInterfaceName;
+
+
+ ScanOnlyModeManager(Context context, @NonNull Looper looper, WifiNative wifiNative,
+ Listener listener, WifiMetrics wifiMetrics, ScanRequestProxy scanRequestProxy,
+ WakeupController wakeupController) {
+ mContext = context;
+ mWifiNative = wifiNative;
+ mListener = listener;
+ mWifiMetrics = wifiMetrics;
+ mScanRequestProxy = scanRequestProxy;
+ mWakeupController = wakeupController;
+ mStateMachine = new ScanOnlyModeStateMachine(looper);
}
/**
* Start scan only mode.
*/
public void start() {
-
+ mStateMachine.sendMessage(ScanOnlyModeStateMachine.CMD_START);
}
/**
* Cancel any pending scans and stop scan mode.
*/
public void stop() {
+ mStateMachine.sendMessage(ScanOnlyModeStateMachine.CMD_STOP);
+ }
+
+ /**
+ * Listener for ScanOnlyMode state changes.
+ */
+ public interface Listener {
+ /**
+ * Invoke when wifi state changes.
+ * @param state new wifi state
+ */
+ void onStateChanged(int state);
+ }
+
+ /**
+ * Update Wifi state.
+ * @param state new Wifi state
+ */
+ private void updateWifiState(int state) {
+ if (mListener != null) {
+ mListener.onStateChanged(state);
+ }
+ }
+
+ private class ScanOnlyModeStateMachine extends StateMachine {
+ // Commands for the state machine.
+ public static final int CMD_START = 0;
+ public static final int CMD_STOP = 1;
+ public static final int CMD_WIFINATIVE_FAILURE = 2;
+ public static final int CMD_INTERFACE_STATUS_CHANGED = 3;
+ public static final int CMD_INTERFACE_DESTROYED = 4;
+
+ private final State mIdleState = new IdleState();
+ private final State mStartedState = new StartedState();
+
+ private final WifiNative.StatusListener mWifiNativeStatusListener = (boolean isReady) -> {
+ if (!isReady) {
+ sendMessage(CMD_WIFINATIVE_FAILURE);
+ }
+ };
+
+ private final InterfaceCallback mWifiNativeInterfaceCallback = new InterfaceCallback() {
+ @Override
+ public void onDestroyed(String ifaceName) {
+ sendMessage(CMD_INTERFACE_DESTROYED);
+ }
+
+ @Override
+ public void onUp(String ifaceName) {
+ sendMessage(CMD_INTERFACE_STATUS_CHANGED, 1);
+ }
+
+ @Override
+ public void onDown(String ifaceName) {
+ sendMessage(CMD_INTERFACE_STATUS_CHANGED, 0);
+ }
+ };
+ private boolean mIfaceIsUp = false;
+
+ ScanOnlyModeStateMachine(Looper looper) {
+ super(TAG, looper);
+
+ addState(mIdleState);
+ addState(mStartedState);
+
+ setInitialState(mIdleState);
+ start();
+ }
+
+ private class IdleState extends State {
+
+ @Override
+ public void enter() {
+ Log.d(TAG, "entering IdleState");
+ mWifiNative.registerStatusListener(mWifiNativeStatusListener);
+ mClientInterfaceName = null;
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ switch (message.what) {
+ case CMD_START:
+ mClientInterfaceName = mWifiNative.setupInterfaceForClientMode(
+ mWifiNativeInterfaceCallback);
+ if (TextUtils.isEmpty(mClientInterfaceName)) {
+ Log.e(TAG, "Failed to create ClientInterface. Sit in Idle");
+ sendScanAvailableBroadcast(false);
+ updateWifiState(WifiManager.WIFI_STATE_UNKNOWN);
+ break;
+ }
+ transitionTo(mStartedState);
+ break;
+ case CMD_STOP:
+ // This should be safe to ignore.
+ Log.d(TAG, "received CMD_STOP when idle, ignoring");
+ break;
+ default:
+ Log.d(TAG, "received an invalid message: " + message);
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+ }
+
+ private class StartedState extends State {
+
+ private void onUpChanged(boolean isUp) {
+ if (isUp == mIfaceIsUp) {
+ return; // no change
+ }
+ mIfaceIsUp = isUp;
+ if (isUp) {
+ Log.d(TAG, "Wifi is ready to use for scanning");
+ mWakeupController.start();
+ sendScanAvailableBroadcast(true);
+ updateWifiState(WifiManager.WIFI_STATE_ENABLED);
+ } else {
+ // if the interface goes down we should exit and go back to idle state.
+ Log.d(TAG, "interface down - stop scan mode");
+ mStateMachine.sendMessage(CMD_STOP);
+ }
+ }
+
+ @Override
+ public void enter() {
+ Log.d(TAG, "entering StartedState");
+ mIfaceIsUp = false;
+ onUpChanged(mWifiNative.isInterfaceUp(mClientInterfaceName));
+
+ if (mIfaceIsUp) {
+ // we already received the interface up notification when we were setting up
+ sendScanAvailableBroadcast(true);
+ updateWifiState(WifiManager.WIFI_STATE_ENABLED);
+ }
+ mScanRequestProxy.enableScanningForHiddenNetworks(false);
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ switch(message.what) {
+ case CMD_START:
+ // Already started, ignore this command.
+ break;
+ case CMD_STOP:
+ Log.d(TAG, "Stopping scan mode.");
+ mWifiNative.teardownInterface(mClientInterfaceName);
+ transitionTo(mIdleState);
+ break;
+ case CMD_INTERFACE_STATUS_CHANGED:
+ boolean isUp = message.arg1 == 1;
+ onUpChanged(isUp);
+ break;
+ case CMD_WIFINATIVE_FAILURE:
+ case CMD_INTERFACE_DESTROYED:
+ Log.d(TAG, "interface failure! restart services?");
+ updateWifiState(WifiManager.WIFI_STATE_UNKNOWN);
+ transitionTo(mIdleState);
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
+ }
+
+ /**
+ * Clean up state, unregister listeners and send broadcast to tell WifiScanner
+ * that wifi is disabled.
+ */
+ @Override
+ public void exit() {
+ mWakeupController.stop();
+ // let WifiScanner know that wifi is down.
+ sendScanAvailableBroadcast(false);
+ updateWifiState(WifiManager.WIFI_STATE_DISABLED);
+ mScanRequestProxy.clearScanResults();
+ }
+ }
+ private void sendScanAvailableBroadcast(boolean available) {
+ Log.d(TAG, "sending scan available broadcast: " + available);
+ final Intent intent = new Intent(WifiManager.WIFI_SCAN_AVAILABLE);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ if (available) {
+ intent.putExtra(WifiManager.EXTRA_SCAN_AVAILABLE, WifiManager.WIFI_STATE_ENABLED);
+ } else {
+ intent.putExtra(WifiManager.EXTRA_SCAN_AVAILABLE, WifiManager.WIFI_STATE_DISABLED);
+ }
+ mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
}
}
diff --git a/com/android/server/wifi/ScanRequestProxy.java b/com/android/server/wifi/ScanRequestProxy.java
new file mode 100644
index 00000000..588d0dc5
--- /dev/null
+++ b/com/android/server/wifi/ScanRequestProxy.java
@@ -0,0 +1,222 @@
+/*
+ * 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 com.android.server.wifi;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiManager;
+import android.net.wifi.WifiScanner;
+import android.os.Binder;
+import android.os.UserHandle;
+import android.os.WorkSource;
+import android.util.Log;
+
+import com.android.server.wifi.util.WifiPermissionsUtil;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.annotation.concurrent.NotThreadSafe;
+
+/**
+ * This class manages all scan requests originating from external apps using the
+ * {@link WifiManager#startScan()}.
+ *
+ * This class is responsible for:
+ * a) Forwarding scan requests from {@link WifiManager#startScan()} to
+ * {@link WifiScanner#startScan(WifiScanner.ScanSettings, WifiScanner.ScanListener)}.
+ * Will essentially proxy scan requests from WifiService to WifiScanningService.
+ * b) Cache the results of these scan requests and return them when
+ * {@link WifiManager#getScanResults()} is invoked.
+ * c) Will send out the {@link WifiManager#SCAN_RESULTS_AVAILABLE_ACTION} broadcast when new
+ * scan results are available.
+ * Note: This class is not thread-safe. It needs to be invoked from WifiStateMachine thread only.
+ * TODO (b/68987915): Port over scan throttling logic from WifiService for all apps.
+ * TODO: Port over idle mode handling from WifiService.
+ */
+@NotThreadSafe
+public class ScanRequestProxy {
+ private static final String TAG = "WifiScanRequestProxy";
+
+ private final Context mContext;
+ private final WifiInjector mWifiInjector;
+ private final WifiConfigManager mWifiConfigManager;
+ private final WifiPermissionsUtil mWifiPermissionsUtil;
+ private WifiScanner mWifiScanner;
+
+ // Verbose logging flag.
+ private boolean mVerboseLoggingEnabled = false;
+ // Flag to decide if we need to scan for hidden networks or not.
+ private boolean mScanningForHiddenNetworksEnabled = false;
+ // Scan results cached from the last full single scan request.
+ private final List<ScanResult> mLastScanResults = new ArrayList<>();
+ // Common scan listener for scan requests.
+ private final WifiScanner.ScanListener mScanListener = new WifiScanner.ScanListener() {
+ @Override
+ public void onSuccess() {
+ // Scan request succeeded, wait for results to report to external clients.
+ if (mVerboseLoggingEnabled) {
+ Log.d(TAG, "Scan request succeeded");
+ }
+ }
+
+ @Override
+ public void onFailure(int reason, String description) {
+ Log.e(TAG, "Scan failure received");
+ sendScanResultBroadcast(false);
+ }
+
+ @Override
+ public void onResults(WifiScanner.ScanData[] scanDatas) {
+ if (mVerboseLoggingEnabled) {
+ Log.d(TAG, "Scan results received");
+ }
+ // For single scans, the array size should always be 1.
+ if (scanDatas.length != 1) {
+ Log.e(TAG, "Found more than 1 batch of scan results, Ignoring...");
+ sendScanResultBroadcast(false);
+ }
+ WifiScanner.ScanData scanData = scanDatas[0];
+ ScanResult[] scanResults = scanData.getResults();
+ if (mVerboseLoggingEnabled) {
+ Log.d(TAG, "Received " + scanResults.length + " scan results");
+ }
+ // Store the last scan results & send out the scan completion broadcast.
+ mLastScanResults.clear();
+ mLastScanResults.addAll(Arrays.asList(scanResults));
+ sendScanResultBroadcast(true);
+ }
+
+ @Override
+ public void onFullResult(ScanResult fullScanResult) {
+ // Ignore for single scans.
+ }
+
+ @Override
+ public void onPeriodChanged(int periodInMs) {
+ // Ignore for single scans.
+ }
+ };
+
+ ScanRequestProxy(Context context, WifiInjector wifiInjector, WifiConfigManager configManager,
+ WifiPermissionsUtil wifiPermissionUtil) {
+ mContext = context;
+ mWifiInjector = wifiInjector;
+ mWifiConfigManager = configManager;
+ mWifiPermissionsUtil = wifiPermissionUtil;
+ }
+
+ /**
+ * Enable verbose logging.
+ */
+ public void enableVerboseLogging(int verbose) {
+ mVerboseLoggingEnabled = (verbose > 0);
+ }
+
+ /**
+ * Enable/disable scanning for hidden networks.
+ * @param enable true to enable, false to disable.
+ */
+ public void enableScanningForHiddenNetworks(boolean enable) {
+ if (mVerboseLoggingEnabled) {
+ Log.d(TAG, "Scanning for hidden networks is " + (enable ? "enabled" : "disabled"));
+ }
+ mScanningForHiddenNetworksEnabled = enable;
+ }
+
+ /**
+ * Helper method to populate WifiScanner handle. This is done lazily because
+ * WifiScanningService is started after WifiService.
+ */
+ private boolean retrieveWifiScannerIfNecessary() {
+ if (mWifiScanner == null) {
+ mWifiScanner = mWifiInjector.getWifiScanner();
+ }
+ return mWifiScanner != null;
+ }
+
+ /**
+ * Helper method to send the scan request status broadcast.
+ */
+ private void sendScanResultBroadcast(boolean scanSucceeded) {
+ // clear calling identity to send broadcast
+ long callingIdentity = Binder.clearCallingIdentity();
+ try {
+ Intent intent = new Intent(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ intent.putExtra(WifiManager.EXTRA_RESULTS_UPDATED, scanSucceeded);
+ mContext.sendBroadcastAsUser(intent, UserHandle.ALL);
+ } finally {
+ // restore calling identity
+ Binder.restoreCallingIdentity(callingIdentity);
+ }
+ }
+
+ /**
+ * Initiate a wifi scan.
+ *
+ * @param callingUid The uid initiating the wifi scan. Blame will be given to this uid.
+ * @return true if the scan request was placed, false otherwise.
+ */
+ public boolean startScan(int callingUid) {
+ if (!retrieveWifiScannerIfNecessary()) {
+ Log.e(TAG, "Failed to retrieve wifiscanner");
+ sendScanResultBroadcast(false);
+ return false;
+ }
+ // Create a worksource using the caller's UID.
+ WorkSource workSource = new WorkSource(callingUid);
+
+ // Create the scan settings.
+ WifiScanner.ScanSettings settings = new WifiScanner.ScanSettings();
+ // Scan requests from apps with network settings will be of high accuracy type.
+ if (mWifiPermissionsUtil.checkNetworkSettingsPermission(callingUid)) {
+ settings.type = WifiScanner.TYPE_HIGH_ACCURACY;
+ }
+ // always do full scans
+ settings.band = WifiScanner.WIFI_BAND_BOTH_WITH_DFS;
+ settings.reportEvents = WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN
+ | WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT;
+ if (mScanningForHiddenNetworksEnabled) {
+ // retrieve the list of hidden network SSIDs to scan for, if enabled.
+ List<WifiScanner.ScanSettings.HiddenNetwork> hiddenNetworkList =
+ mWifiConfigManager.retrieveHiddenNetworkList();
+ settings.hiddenNetworks = hiddenNetworkList.toArray(
+ new WifiScanner.ScanSettings.HiddenNetwork[hiddenNetworkList.size()]);
+ }
+ mWifiScanner.startScan(settings, mScanListener, workSource);
+ return true;
+ }
+
+ /**
+ * Return the results of the most recent access point scan, in the form of
+ * a list of {@link ScanResult} objects.
+ * @return the list of results
+ */
+ public List<ScanResult> getScanResults() {
+ return mLastScanResults;
+ }
+
+ /**
+ * Clear the stored scan results.
+ */
+ public void clearScanResults() {
+ mLastScanResults.clear();
+ }
+}
diff --git a/com/android/server/wifi/SoftApManager.java b/com/android/server/wifi/SoftApManager.java
index 6d5fd278..f687780c 100644
--- a/com/android/server/wifi/SoftApManager.java
+++ b/com/android/server/wifi/SoftApManager.java
@@ -24,29 +24,25 @@ 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.InterfaceCallback;
import com.android.server.wifi.WifiNative.SoftApListener;
+import com.android.server.wifi.WifiNative.StatusListener;
import com.android.server.wifi.util.ApConfigUtil;
import java.util.Locale;
@@ -73,12 +69,10 @@ public class SoftApManager implements ActiveModeManager {
private final SoftApStateMachine mStateMachine;
- private final Listener mListener;
+ private final WifiManager.SoftApCallback mCallback;
- private IApInterface mApInterface;
private String mApInterfaceName;
- private final INetworkManagementService mNwService;
private final WifiApConfigStore mWifiApConfigStore;
private final WifiMetrics mWifiMetrics;
@@ -97,25 +91,12 @@ public class SoftApManager implements ActiveModeManager {
}
};
- /**
- * Listener for soft AP state changes.
- */
- public interface Listener {
- /**
- * Invoke when AP state changed.
- * @param state new AP state
- * @param failureReason reason when in failed state
- */
- void onStateChanged(int state, int failureReason);
- }
-
public SoftApManager(Context context,
Looper looper,
FrameworkFacade framework,
WifiNative wifiNative,
String countryCode,
- Listener listener,
- INetworkManagementService nms,
+ WifiManager.SoftApCallback callback,
WifiApConfigStore wifiApConfigStore,
@NonNull SoftApModeConfiguration apConfig,
WifiMetrics wifiMetrics) {
@@ -123,8 +104,7 @@ public class SoftApManager implements ActiveModeManager {
mFrameworkFacade = framework;
mWifiNative = wifiNative;
mCountryCode = countryCode;
- mListener = listener;
- mNwService = nms;
+ mCallback = callback;
mWifiApConfigStore = wifiApConfigStore;
mMode = apConfig.getTargetMode();
WifiConfiguration config = apConfig.getWifiConfiguration();
@@ -158,8 +138,10 @@ public class SoftApManager implements ActiveModeManager {
* @param reason Failure reason if the new AP state is in failure state
*/
private void updateApState(int newState, int currentState, int reason) {
- if (mListener != null) {
- mListener.onStateChanged(newState, reason);
+ if (mCallback != null) {
+ mCallback.onStateChanged(newState, reason);
+ } else {
+ Log.wtf(TAG, "SoftApCallback is null. Dropping StateChanged event.");
}
//send the AP state change broadcast
@@ -178,17 +160,6 @@ 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
@@ -215,7 +186,8 @@ public class SoftApManager implements ActiveModeManager {
if (mCountryCode != null) {
// Country code is mandatory for 5GHz band, return an error if failed to set
// country code when AP is configured for 5GHz band.
- if (!mWifiNative.setCountryCodeHal(mCountryCode.toUpperCase(Locale.ROOT))
+ if (!mWifiNative.setCountryCodeHal(
+ mApInterfaceName, mCountryCode.toUpperCase(Locale.ROOT))
&& config.apBand == WifiConfiguration.AP_BAND_5GHZ) {
Log.e(TAG, "Failed to set country code, required for setting up "
+ "soft ap in 5GHz");
@@ -225,7 +197,7 @@ public class SoftApManager implements ActiveModeManager {
if (localConfig.hiddenSSID) {
Log.d(TAG, "SoftAP is a hidden network");
}
- if (!mWifiNative.startSoftAp(localConfig, mSoftApListener)) {
+ if (!mWifiNative.startSoftAp(mApInterfaceName, localConfig, mSoftApListener)) {
Log.e(TAG, "Soft AP start failed");
return ERROR_GENERIC;
}
@@ -238,9 +210,7 @@ public class SoftApManager implements ActiveModeManager {
* Teardown soft AP and teardown the interface.
*/
private void stopSoftAp() {
- if (!mWifiNative.stopSoftAp()) {
- Log.e(TAG, "Soft AP stop failed");
- }
+ mWifiNative.teardownInterface(mApInterfaceName);
Log.d(TAG, "Soft AP is stopped");
}
@@ -248,36 +218,38 @@ 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_WIFICOND_BINDER_DEATH = 2;
+ public static final int CMD_WIFINATIVE_FAILURE = 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;
+ public static final int CMD_INTERFACE_DESTROYED = 7;
private final State mIdleState = new IdleState();
private final State mStartedState = new StartedState();
- private final WifiNative.WificondDeathEventHandler mWificondDeathRecipient = () -> {
- sendMessage(CMD_WIFICOND_BINDER_DEATH);
+ private final StatusListener mWifiNativeStatusListener = (boolean isReady) -> {
+ if (!isReady) {
+ sendMessage(CMD_WIFINATIVE_FAILURE);
+ }
};
- private NetworkObserver mNetworkObserver;
-
- private class NetworkObserver extends BaseNetworkObserver {
- private final String mIfaceName;
+ private final InterfaceCallback mWifiNativeInterfaceCallback = new InterfaceCallback() {
+ @Override
+ public void onDestroyed(String ifaceName) {
+ sendMessage(CMD_INTERFACE_DESTROYED);
+ }
- NetworkObserver(String ifaceName) {
- mIfaceName = ifaceName;
+ @Override
+ public void onUp(String ifaceName) {
+ sendMessage(CMD_INTERFACE_STATUS_CHANGED, 1);
}
@Override
- public void interfaceLinkStateChanged(String iface, boolean up) {
- if (mIfaceName.equals(iface)) {
- SoftApStateMachine.this.sendMessage(
- CMD_INTERFACE_STATUS_CHANGED, up ? 1 : 0, 0, this);
- }
+ public void onDown(String ifaceName) {
+ sendMessage(CMD_INTERFACE_STATUS_CHANGED, 0);
}
- }
+ };
SoftApStateMachine(Looper looper) {
super(TAG, looper);
@@ -292,97 +264,40 @@ public class SoftApManager implements ActiveModeManager {
private class IdleState extends State {
@Override
public void enter() {
- mWifiNative.deregisterWificondDeathHandler();
- unregisterObserver();
+ mWifiNative.registerStatusListener(mWifiNativeStatusListener);
+ mApInterfaceName = null;
}
@Override
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.
+ mApInterfaceName = mWifiNative.setupInterfaceForSoftApMode(
+ mWifiNativeInterfaceCallback);
if (TextUtils.isEmpty(mApInterfaceName)) {
- Log.e(TAG, "Not starting softap mode without an interface name.");
+ Log.e(TAG, "setup failure when creating ap interface.");
updateApState(WifiManager.WIFI_AP_STATE_FAILED,
- WifiManager.WIFI_AP_STATE_DISABLED,
- WifiManager.SAP_START_FAILURE_GENERAL);
+ WifiManager.WIFI_AP_STATE_DISABLED,
+ WifiManager.SAP_START_FAILURE_GENERAL);
mWifiMetrics.incrementSoftApStartResult(
false, WifiManager.SAP_START_FAILURE_GENERAL);
break;
}
updateApState(WifiManager.WIFI_AP_STATE_ENABLING,
WifiManager.WIFI_AP_STATE_DISABLED, 0);
- if (!mWifiNative.registerWificondDeathHandler(mWificondDeathRecipient)) {
- updateApState(WifiManager.WIFI_AP_STATE_FAILED,
- WifiManager.WIFI_AP_STATE_ENABLING,
- WifiManager.SAP_START_FAILURE_GENERAL);
- mWifiMetrics.incrementSoftApStartResult(
- false, WifiManager.SAP_START_FAILURE_GENERAL);
- break;
- }
- try {
- mNetworkObserver = new NetworkObserver(mApInterfaceName);
- mNwService.registerObserver(mNetworkObserver);
- } catch (RemoteException e) {
- mWifiNative.deregisterWificondDeathHandler();
- unregisterObserver();
- updateApState(WifiManager.WIFI_AP_STATE_FAILED,
- WifiManager.WIFI_AP_STATE_ENABLING,
- WifiManager.SAP_START_FAILURE_GENERAL);
- mWifiMetrics.incrementSoftApStartResult(
- false, WifiManager.SAP_START_FAILURE_GENERAL);
- break;
- }
-
int result = startSoftAp((WifiConfiguration) message.obj);
if (result != SUCCESS) {
int failureReason = WifiManager.SAP_START_FAILURE_GENERAL;
if (result == ERROR_NO_CHANNEL) {
failureReason = WifiManager.SAP_START_FAILURE_NO_CHANNEL;
}
- mWifiNative.deregisterWificondDeathHandler();
- unregisterObserver();
updateApState(WifiManager.WIFI_AP_STATE_FAILED,
WifiManager.WIFI_AP_STATE_ENABLING,
failureReason);
+ stopSoftAp();
mWifiMetrics.incrementSoftApStartResult(false, failureReason);
break;
}
-
transitionTo(mStartedState);
break;
default:
@@ -392,16 +307,6 @@ public class SoftApManager implements ActiveModeManager {
return HANDLED;
}
-
- private void unregisterObserver() {
- if (mNetworkObserver == null) {
- return;
- }
- try {
- mNwService.unregisterObserver(mNetworkObserver);
- } catch (RemoteException e) { }
- mNetworkObserver = null;
- }
}
private class StartedState extends State {
@@ -482,7 +387,11 @@ public class SoftApManager implements ActiveModeManager {
mNumAssociatedStations = numStations;
Log.d(TAG, "Number of associated stations changed: " + mNumAssociatedStations);
- // TODO:(b/63906412) send it up to settings.
+ if (mCallback != null) {
+ mCallback.onNumClientsChanged(mNumAssociatedStations);
+ } else {
+ Log.e(TAG, "SoftApCallback is null. Dropping NumClientsChanged event.");
+ }
mWifiMetrics.addSoftApNumAssociatedStationsChangedEvent(mNumAssociatedStations,
mMode);
@@ -504,7 +413,7 @@ public class SoftApManager implements ActiveModeManager {
WifiManager.WIFI_AP_STATE_ENABLING, 0);
mWifiMetrics.incrementSoftApStartResult(true, 0);
} else {
- // TODO: handle the case where the interface was up, but goes down
+ // TODO(b/72223325): handle the case where the interface was up, but goes down
}
mWifiMetrics.addSoftApUpChangedEvent(isUp, mMode);
}
@@ -512,14 +421,7 @@ public class SoftApManager implements ActiveModeManager {
@Override
public void enter() {
mIfaceIsUp = false;
- InterfaceConfiguration config = null;
- try {
- config = mNwService.getInterfaceConfig(mApInterfaceName);
- } catch (RemoteException e) {
- }
- if (config != null) {
- onUpChanged(config.isUp());
- }
+ onUpChanged(mWifiNative.isInterfaceUp(mApInterfaceName));
mTimeoutDelay = getConfigSoftApTimeoutDelay();
Handler handler = mStateMachine.getHandler();
@@ -544,6 +446,9 @@ public class SoftApManager implements ActiveModeManager {
Log.d(TAG, "Resetting num stations on stop");
mNumAssociatedStations = 0;
cancelTimeoutMessage();
+ // Need this here since we are exiting |Started| state and won't handle any
+ // future CMD_INTERFACE_STATUS_CHANGED events after this point
+ mWifiMetrics.addSoftApUpChangedEvent(false, mMode);
}
@Override
@@ -571,10 +476,6 @@ public class SoftApManager implements ActiveModeManager {
}
break;
case CMD_INTERFACE_STATUS_CHANGED:
- if (message.obj != mNetworkObserver) {
- // This is from some time before the most recent configuration.
- break;
- }
boolean isUp = message.arg1 == 1;
onUpChanged(isUp);
break;
@@ -592,24 +493,20 @@ public class SoftApManager implements ActiveModeManager {
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);
stopSoftAp();
- if (message.what == CMD_WIFICOND_BINDER_DEATH) {
- updateApState(WifiManager.WIFI_AP_STATE_FAILED,
- WifiManager.WIFI_AP_STATE_DISABLING,
- WifiManager.SAP_START_FAILURE_GENERAL);
- } else {
- updateApState(WifiManager.WIFI_AP_STATE_DISABLED,
- WifiManager.WIFI_AP_STATE_DISABLING, 0);
- }
+ updateApState(WifiManager.WIFI_AP_STATE_DISABLED,
+ WifiManager.WIFI_AP_STATE_DISABLING, 0);
+ transitionTo(mIdleState);
+ break;
+ case CMD_WIFINATIVE_FAILURE:
+ case CMD_INTERFACE_DESTROYED:
+ updateApState(WifiManager.WIFI_AP_STATE_FAILED,
+ WifiManager.WIFI_AP_STATE_ENABLED,
+ WifiManager.SAP_START_FAILURE_GENERAL);
transitionTo(mIdleState);
-
- // Need this here since we are exiting |Started| state and won't handle any
- // future CMD_INTERFACE_STATUS_CHANGED events after this point
- mWifiMetrics.addSoftApUpChangedEvent(false, mMode);
break;
default:
return NOT_HANDLED;
diff --git a/com/android/server/wifi/SsidSetStoreData.java b/com/android/server/wifi/SsidSetStoreData.java
index daed26a6..474740db 100644
--- a/com/android/server/wifi/SsidSetStoreData.java
+++ b/com/android/server/wifi/SsidSetStoreData.java
@@ -91,6 +91,10 @@ public class SsidSetStoreData implements WifiConfigStore.StoreData {
@Override
public void deserializeData(XmlPullParser in, int outerTagDepth, boolean shared)
throws XmlPullParserException, IOException {
+ // Ignore empty reads.
+ if (in == null) {
+ return;
+ }
if (shared) {
throw new XmlPullParserException("Share data not supported");
}
diff --git a/com/android/server/wifi/SupplicantStaIfaceHal.java b/com/android/server/wifi/SupplicantStaIfaceHal.java
index 3429e3d5..7c388f89 100644
--- a/com/android/server/wifi/SupplicantStaIfaceHal.java
+++ b/com/android/server/wifi/SupplicantStaIfaceHal.java
@@ -46,6 +46,7 @@ import android.net.wifi.SupplicantState;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiSsid;
+import android.os.HidlSupport.Mutable;
import android.os.HwRemoteBinder;
import android.os.RemoteException;
import android.text.TextUtils;
@@ -451,7 +452,6 @@ public class SupplicantStaIfaceHal {
public boolean registerDeathHandler(@NonNull SupplicantDeathEventHandler handler) {
if (mDeathEventHandler != null) {
Log.e(TAG, "Death handler already present");
- return false;
}
mDeathEventHandler = handler;
return true;
@@ -464,7 +464,6 @@ public class SupplicantStaIfaceHal {
public boolean deregisterDeathHandler() {
if (mDeathEventHandler == null) {
Log.e(TAG, "No Death handler present");
- return false;
}
mDeathEventHandler = null;
return true;
@@ -2266,18 +2265,6 @@ public class SupplicantStaIfaceHal {
}
}
- private static class Mutable<E> {
- public E value;
-
- Mutable() {
- value = null;
- }
-
- Mutable(E value) {
- this.value = value;
- }
- }
-
private class SupplicantStaIfaceHalCallback extends ISupplicantStaIfaceCallback.Stub {
private String mIfaceName;
private boolean mStateIsFourway = false; // Used to help check for PSK password mismatch
diff --git a/com/android/server/wifi/SupplicantStaNetworkHal.java b/com/android/server/wifi/SupplicantStaNetworkHal.java
index b3598979..0524a803 100644
--- a/com/android/server/wifi/SupplicantStaNetworkHal.java
+++ b/com/android/server/wifi/SupplicantStaNetworkHal.java
@@ -22,6 +22,7 @@ import android.hardware.wifi.supplicant.V1_0.SupplicantStatus;
import android.hardware.wifi.supplicant.V1_0.SupplicantStatusCode;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiEnterpriseConfig;
+import android.os.HidlSupport.Mutable;
import android.os.RemoteException;
import android.text.TextUtils;
import android.util.Log;
@@ -2510,18 +2511,6 @@ public class SupplicantStaNetworkHal {
}
}
- private static class Mutable<E> {
- public E value;
-
- Mutable() {
- value = null;
- }
-
- Mutable(E value) {
- this.value = value;
- }
- }
-
private class SupplicantStaNetworkHalCallback extends ISupplicantStaNetworkCallback.Stub {
/**
* Current configured network's framework network id.
diff --git a/com/android/server/wifi/SystemPropertyService.java b/com/android/server/wifi/SystemPropertyService.java
index 4c7ffb6d..3da531d5 100644
--- a/com/android/server/wifi/SystemPropertyService.java
+++ b/com/android/server/wifi/SystemPropertyService.java
@@ -32,4 +32,9 @@ class SystemPropertyService implements PropertyService {
public boolean getBoolean(String key, boolean defaultValue) {
return android.os.SystemProperties.getBoolean(key, defaultValue);
}
+
+ @Override
+ public String getString(String key, String defaultValue) {
+ return android.os.SystemProperties.get(key, defaultValue);
+ }
}
diff --git a/com/android/server/wifi/WakeupConfigStoreData.java b/com/android/server/wifi/WakeupConfigStoreData.java
index 57751177..0f33aae0 100644
--- a/com/android/server/wifi/WakeupConfigStoreData.java
+++ b/com/android/server/wifi/WakeupConfigStoreData.java
@@ -35,12 +35,15 @@ import java.util.Set;
public class WakeupConfigStoreData implements StoreData {
private static final String TAG = "WakeupConfigStoreData";
+ private static final String XML_TAG_FEATURE_STATE_SECTION = "FeatureState";
private static final String XML_TAG_IS_ACTIVE = "IsActive";
+ private static final String XML_TAG_IS_ONBOARDED = "IsOnboarded";
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<Boolean> mIsOnboardedDataSource;
private final DataSource<Set<ScanResultMatchInfo>> mNetworkDataSource;
/**
@@ -70,8 +73,10 @@ public class WakeupConfigStoreData implements StoreData {
*/
public WakeupConfigStoreData(
DataSource<Boolean> isActiveDataSource,
+ DataSource<Boolean> isOnboardedDataSource,
DataSource<Set<ScanResultMatchInfo>> networkDataSource) {
mIsActiveDataSource = isActiveDataSource;
+ mIsOnboardedDataSource = isOnboardedDataSource;
mNetworkDataSource = networkDataSource;
}
@@ -82,7 +87,7 @@ public class WakeupConfigStoreData implements StoreData {
throw new XmlPullParserException("Share data not supported");
}
- XmlUtil.writeNextValue(out, XML_TAG_IS_ACTIVE, mIsActiveDataSource.getData());
+ writeFeatureState(out);
for (ScanResultMatchInfo scanResultMatchInfo : mNetworkDataSource.getData()) {
writeNetwork(out, scanResultMatchInfo);
@@ -90,10 +95,27 @@ public class WakeupConfigStoreData implements StoreData {
}
/**
+ * Writes the current state of Wifi Wake to an XML output stream.
+ *
+ * @param out XML output stream
+ * @throws XmlPullParserException
+ * @throws IOException
+ */
+ private void writeFeatureState(XmlSerializer out)
+ throws IOException, XmlPullParserException {
+ XmlUtil.writeNextSectionStart(out, XML_TAG_FEATURE_STATE_SECTION);
+
+ XmlUtil.writeNextValue(out, XML_TAG_IS_ACTIVE, mIsActiveDataSource.getData());
+ XmlUtil.writeNextValue(out, XML_TAG_IS_ONBOARDED, mIsOnboardedDataSource.getData());
+
+ XmlUtil.writeNextSectionEnd(out, XML_TAG_FEATURE_STATE_SECTION);
+ }
+
+ /**
* Writes a {@link ScanResultMatchInfo} to an XML output stream.
*
* @param out XML output stream
- * @param scanResultMatchInfo The ScanResultMatchInfo to serizialize
+ * @param scanResultMatchInfo The ScanResultMatchInfo to serialize
* @throws XmlPullParserException
* @throws IOException
*/
@@ -110,22 +132,67 @@ public class WakeupConfigStoreData implements StoreData {
@Override
public void deserializeData(XmlPullParser in, int outerTagDepth, boolean shared)
throws XmlPullParserException, IOException {
+ // Ignore empty reads.
+ if (in == null) {
+ return;
+ }
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));
+
+ String[] headerName = new String[1];
+ while (XmlUtil.gotoNextSectionOrEnd(in, headerName, outerTagDepth)) {
+ switch (headerName[0]) {
+ case XML_TAG_FEATURE_STATE_SECTION:
+ parseFeatureState(in, outerTagDepth + 1);
+ break;
+ case XML_TAG_NETWORK_SECTION:
+ networks.add(parseNetwork(in, outerTagDepth + 1));
+ break;
+ }
}
mNetworkDataSource.setData(networks);
}
/**
+ * Parses the state of Wifi Wake from an XML input stream and sets the respective data sources.
+ *
+ * @param in XML input stream
+ * @param outerTagDepth XML tag depth of the containing section
+ * @throws IOException
+ * @throws XmlPullParserException
+ */
+ private void parseFeatureState(XmlPullParser in, int outerTagDepth)
+ throws IOException, XmlPullParserException {
+ boolean isActive = false;
+ boolean isOnboarded = false;
+
+ 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_IS_ACTIVE:
+ isActive = (Boolean) value;
+ break;
+ case XML_TAG_IS_ONBOARDED:
+ isOnboarded = (Boolean) value;
+ break;
+ default:
+ throw new XmlPullParserException("Unknown value found: " + valueName[0]);
+ }
+ }
+
+ mIsActiveDataSource.setData(isActive);
+ mIsOnboardedDataSource.setData(isOnboarded);
+ }
+
+ /**
* Parses a {@link ScanResultMatchInfo} from an XML input stream.
*
* @param in XML input stream
@@ -164,6 +231,7 @@ public class WakeupConfigStoreData implements StoreData {
if (!shared) {
mNetworkDataSource.setData(Collections.emptySet());
mIsActiveDataSource.setData(false);
+ mIsOnboardedDataSource.setData(false);
}
}
diff --git a/com/android/server/wifi/WakeupController.java b/com/android/server/wifi/WakeupController.java
index 4e84c4ae..c1dff8de 100644
--- a/com/android/server/wifi/WakeupController.java
+++ b/com/android/server/wifi/WakeupController.java
@@ -16,6 +16,8 @@
package com.android.server.wifi;
+import static com.android.server.wifi.WifiController.CMD_WIFI_TOGGLED;
+
import android.content.Context;
import android.database.ContentObserver;
import android.net.wifi.ScanResult;
@@ -24,12 +26,15 @@ import android.net.wifi.WifiScanner;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
+import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -44,7 +49,7 @@ public class WakeupController {
private static final String TAG = "WakeupController";
- // TODO(b/69624403) propagate this to Settings
+ // TODO(b/69624403) flip to true when feature is complete
private static final boolean USE_PLATFORM_WIFI_WAKE = false;
private final Context mContext;
@@ -52,6 +57,8 @@ public class WakeupController {
private final FrameworkFacade mFrameworkFacade;
private final ContentObserver mContentObserver;
private final WakeupLock mWakeupLock;
+ private final WakeupEvaluator mWakeupEvaluator;
+ private final WakeupOnboarding mWakeupOnboarding;
private final WifiConfigManager mWifiConfigManager;
private final WifiInjector mWifiInjector;
@@ -63,7 +70,9 @@ public class WakeupController {
@Override
public void onResults(WifiScanner.ScanData[] results) {
- // TODO(easchwar) handle scan results
+ if (results.length == 1 && results[0].isAllChannelsScanned()) {
+ handleScanResults(Arrays.asList(results[0].getResults()));
+ }
}
@Override
@@ -92,6 +101,8 @@ public class WakeupController {
Context context,
Looper looper,
WakeupLock wakeupLock,
+ WakeupEvaluator wakeupEvaluator,
+ WakeupOnboarding wakeupOnboarding,
WifiConfigManager wifiConfigManager,
WifiConfigStore wifiConfigStore,
WifiInjector wifiInjector,
@@ -99,6 +110,8 @@ public class WakeupController {
mContext = context;
mHandler = new Handler(looper);
mWakeupLock = wakeupLock;
+ mWakeupEvaluator = wakeupEvaluator;
+ mWakeupOnboarding = wakeupOnboarding;
mWifiConfigManager = wifiConfigManager;
mFrameworkFacade = frameworkFacade;
mWifiInjector = wifiInjector;
@@ -115,13 +128,16 @@ public class WakeupController {
// 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());
+ WakeupConfigStoreData wakeupConfigStoreData = new WakeupConfigStoreData(
+ new IsActiveDataSource(),
+ mWakeupOnboarding.getDataSource(),
+ mWakeupLock.getDataSource());
wifiConfigStore.registerStoreData(wakeupConfigStoreData);
}
private void setActive(boolean isActive) {
if (mIsActive != isActive) {
+ Log.d(TAG, "Setting active to " + isActive);
mIsActive = isActive;
mWifiConfigManager.saveToStore(false /* forceWrite */);
}
@@ -135,6 +151,7 @@ public class WakeupController {
* it performs its initialization steps and sets {@link #mIsActive} to true.
*/
public void start() {
+ Log.d(TAG, "start()");
mWifiInjector.getWifiScanner().registerScanListener(mScanListener);
// If already active, we don't want to re-initialize the lock, so return early.
@@ -143,7 +160,8 @@ public class WakeupController {
}
setActive(true);
- if (mWifiWakeupEnabled) {
+ if (isEnabled()) {
+ mWakeupOnboarding.maybeShowNotification();
mWakeupLock.initialize(getMostRecentSavedScanResults());
}
}
@@ -155,11 +173,14 @@ public class WakeupController {
* WifiScanner.
*/
public void stop() {
+ Log.d(TAG, "stop()");
mWifiInjector.getWifiScanner().deregisterScanListener(mScanListener);
+ mWakeupOnboarding.onStop();
}
/** Resets the WakeupController, setting {@link #mIsActive} to false. */
public void reset() {
+ Log.d(TAG, "reset()");
setActive(false);
}
@@ -202,11 +223,71 @@ public class WakeupController {
}
/**
- * Whether the feature is enabled in settings.
+ * Handles incoming scan results.
+ *
+ * <p>The controller updates the WakeupLock with the incoming scan results. If WakeupLock is
+ * empty, it evaluates scan results for a match with saved networks. If a match exists, it
+ * enables wifi.
+ *
+ * @param scanResults The scan results with which to update the controller
+ */
+ private void handleScanResults(Collection<ScanResult> scanResults) {
+ if (!isEnabled()) {
+ return;
+ }
+
+ // need to show notification here in case user enables Wifi Wake when Wifi is off
+ mWakeupOnboarding.maybeShowNotification();
+ if (!mWakeupOnboarding.isOnboarded()) {
+ return;
+ }
+
+ // only update the wakeup lock if it's not already empty
+ if (!mWakeupLock.isEmpty()) {
+ Set<ScanResultMatchInfo> networks = new ArraySet<>();
+ for (ScanResult scanResult : scanResults) {
+ networks.add(ScanResultMatchInfo.fromScanResult(scanResult));
+ }
+ mWakeupLock.update(networks);
+
+ // if wakeup lock is still not empty, return
+ if (!mWakeupLock.isEmpty()) {
+ return;
+ }
+
+ Log.d(TAG, "WakeupLock emptied");
+ }
+
+ ScanResult network =
+ mWakeupEvaluator.findViableNetwork(scanResults, getGoodSavedNetworks());
+
+ if (network != null) {
+ Log.d(TAG, "Found viable network: " + network.SSID);
+ onNetworkFound(network);
+ }
+ }
+
+ private void onNetworkFound(ScanResult scanResult) {
+ if (isEnabled() && mIsActive && USE_PLATFORM_WIFI_WAKE) {
+ Log.d(TAG, "Enabling wifi for network: " + scanResult.SSID);
+ enableWifi();
+ }
+ }
+
+ /**
+ * Enables wifi.
*
- * <p>Note: This method is only used to determine whether or not to actually enable wifi. All
- * other aspects of the WakeupController lifecycle operate normally irrespective of this.
+ * <p>This method ignores all checks and assumes that {@link WifiStateMachine} is currently
+ * in ScanModeState.
*/
+ private void enableWifi() {
+ // TODO(b/72180295): ensure that there is no race condition with WifiServiceImpl here
+ if (mWifiInjector.getWifiSettingsStore().handleWifiToggled(true /* wifiEnabled */)) {
+ mWifiInjector.getWifiController().sendMessage(CMD_WIFI_TOGGLED);
+ }
+ }
+
+ /** Whether the feature is enabled in settings. */
@VisibleForTesting
boolean isEnabled() {
return mWifiWakeupEnabled;
diff --git a/com/android/server/wifi/WakeupEvaluator.java b/com/android/server/wifi/WakeupEvaluator.java
new file mode 100644
index 00000000..df9c43df
--- /dev/null
+++ b/com/android/server/wifi/WakeupEvaluator.java
@@ -0,0 +1,89 @@
+/*
+ * 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.content.Context;
+import android.net.wifi.ScanResult;
+
+import com.android.internal.R;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Collection;
+
+/**
+ * Evaluates ScanResults for Wifi Wake.
+ */
+public class WakeupEvaluator {
+
+ private final int mThresholdMinimumRssi24;
+ private final int mThresholdMinimumRssi5;
+
+ /**
+ * Constructs a {@link WakeupEvaluator} using the given context.
+ */
+ public static WakeupEvaluator fromContext(Context context) {
+ int minimumRssi24 = context.getResources().getInteger(
+ R.integer.config_wifi_framework_wifi_score_entry_rssi_threshold_24GHz);
+ int minimumRssi5 = context.getResources().getInteger(
+ R.integer.config_wifi_framework_wifi_score_entry_rssi_threshold_5GHz);
+ return new WakeupEvaluator(minimumRssi24, minimumRssi5);
+ }
+
+ @VisibleForTesting
+ WakeupEvaluator(int minimumRssi24, int minimumRssi5) {
+ mThresholdMinimumRssi24 = minimumRssi24;
+ mThresholdMinimumRssi5 = minimumRssi5;
+ }
+
+ /**
+ * Searches ScanResults to find a connectable network.
+ *
+ * <p>This method searches the given ScanResults for one that is present in the given
+ * ScanResultMatchInfos and has a sufficiently high RSSI. If there is no such ScanResult, it
+ * returns null. If there are multiple, it returns the one with the highest RSSI.
+ *
+ * @param scanResults ScanResults to search
+ * @param savedNetworks Network list to compare against
+ * @return The {@link ScanResult} representing an in-range connectable network, or {@code null}
+ * signifying there is no viable network
+ */
+ public ScanResult findViableNetwork(Collection<ScanResult> scanResults,
+ Collection<ScanResultMatchInfo> savedNetworks) {
+ ScanResult selectedScanResult = null;
+
+ for (ScanResult scanResult : scanResults) {
+ if (isBelowThreshold(scanResult)) {
+ continue;
+ }
+ if (savedNetworks.contains(ScanResultMatchInfo.fromScanResult(scanResult))) {
+ if (selectedScanResult == null || selectedScanResult.level < scanResult.level) {
+ selectedScanResult = scanResult;
+ }
+ }
+ }
+
+ return selectedScanResult;
+ }
+
+ /**
+ * Returns whether the given ScanResult's signal strength is below the selection threshold.
+ */
+ public boolean isBelowThreshold(ScanResult scanResult) {
+ return ((scanResult.is24GHz() && scanResult.level < mThresholdMinimumRssi24)
+ || (scanResult.is5GHz() && scanResult.level < mThresholdMinimumRssi5));
+ }
+}
diff --git a/com/android/server/wifi/WakeupNotificationFactory.java b/com/android/server/wifi/WakeupNotificationFactory.java
new file mode 100644
index 00000000..42ae4670
--- /dev/null
+++ b/com/android/server/wifi/WakeupNotificationFactory.java
@@ -0,0 +1,83 @@
+/*
+ * 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 com.android.server.wifi;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+
+import com.android.internal.R;
+import com.android.internal.notification.SystemNotificationChannels;
+
+
+/** Factory for Wifi Wake notifications. */
+public class WakeupNotificationFactory {
+
+ public static final String ACTION_DISMISS_NOTIFICATION =
+ "com.android.server.wifi.wakeup.DISMISS_NOTIFICATION";
+ public static final String ACTION_OPEN_WIFI_PREFERENCES =
+ "com.android.server.wifi.wakeup.OPEN_WIFI_PREFERENCES";
+ public static final String ACTION_OPEN_WIFI_SETTINGS =
+ "com.android.server.wifi.wakeup.OPEN_WIFI_SETTINGS";
+ public static final String ACTION_TURN_OFF_WIFI_WAKE =
+ "com.android.server.wifi.wakeup.TURN_OFF_WIFI_WAKE";
+
+ private final Context mContext;
+ private final FrameworkFacade mFrameworkFacade;
+
+ WakeupNotificationFactory(Context context, FrameworkFacade frameworkFacade) {
+ mContext = context;
+ mFrameworkFacade = frameworkFacade;
+ }
+
+ /**
+ * Creates a Wifi Wake onboarding notification.
+ */
+ public Notification createOnboardingNotification() {
+ CharSequence title = mContext.getText(R.string.wifi_wakeup_onboarding_title);
+ CharSequence content = mContext.getText(R.string.wifi_wakeup_onboarding_subtext);
+ CharSequence disableText = mContext.getText(R.string.wifi_wakeup_onboarding_action_disable);
+ int color = mContext.getResources()
+ .getColor(R.color.system_notification_accent_color, mContext.getTheme());
+
+ final Notification.Action disableAction = new Notification.Action.Builder(
+ null /* icon */, disableText, getPrivateBroadcast(ACTION_TURN_OFF_WIFI_WAKE))
+ .build();
+
+ return mFrameworkFacade.makeNotificationBuilder(mContext,
+ SystemNotificationChannels.NETWORK_STATUS)
+ .setSmallIcon(R.drawable.ic_wifi_settings)
+ .setTicker(title)
+ .setContentTitle(title)
+ .setContentText(content)
+ .setContentIntent(getPrivateBroadcast(ACTION_OPEN_WIFI_PREFERENCES))
+ .setDeleteIntent(getPrivateBroadcast(ACTION_DISMISS_NOTIFICATION))
+ .addAction(disableAction)
+ .setShowWhen(false)
+ .setLocalOnly(true)
+ .setColor(color)
+ .build();
+ }
+
+
+ private PendingIntent getPrivateBroadcast(String action) {
+ Intent intent = new Intent(action).setPackage("android");
+ return mFrameworkFacade.getBroadcast(
+ mContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ }
+}
diff --git a/com/android/server/wifi/WakeupOnboarding.java b/com/android/server/wifi/WakeupOnboarding.java
new file mode 100644
index 00000000..d4caa0fd
--- /dev/null
+++ b/com/android/server/wifi/WakeupOnboarding.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 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 com.android.server.wifi;
+
+import static com.android.server.wifi.WakeupNotificationFactory.ACTION_DISMISS_NOTIFICATION;
+import static com.android.server.wifi.WakeupNotificationFactory.ACTION_OPEN_WIFI_PREFERENCES;
+import static com.android.server.wifi.WakeupNotificationFactory.ACTION_TURN_OFF_WIFI_WAKE;
+
+import android.app.NotificationManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings;
+import android.util.Log;
+
+import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
+
+/**
+ * Manages the WiFi Wake onboarding notification.
+ *
+ * <p>If a user disables wifi with Wifi Wake enabled, this notification is shown to explain that
+ * wifi may turn back on automatically. Wifi will not automatically turn back on until after the
+ * user interacts with the onboarding notification in some way (e.g. dismiss, tap).
+ */
+public class WakeupOnboarding {
+
+ private static final String TAG = "WakeupOnboarding";
+
+ private final Context mContext;
+ private final WakeupNotificationFactory mWakeupNotificationFactory;
+ private NotificationManager mNotificationManager;
+ private final Handler mHandler;
+ private final WifiConfigManager mWifiConfigManager;
+ private final IntentFilter mIntentFilter;
+ private final FrameworkFacade mFrameworkFacade;
+
+ private boolean mIsOnboarded;
+ private boolean mIsNotificationShowing;
+
+ private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ switch (intent.getAction()) {
+ case ACTION_TURN_OFF_WIFI_WAKE:
+ mFrameworkFacade.setIntegerSetting(mContext,
+ Settings.Global.WIFI_WAKEUP_ENABLED, 0);
+ dismissNotification(true /* shouldOnboard */);
+ break;
+ case ACTION_OPEN_WIFI_PREFERENCES:
+ // Close notification drawer before opening preferences.
+ mContext.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+ mContext.startActivity(new Intent(Settings.ACTION_WIFI_IP_SETTINGS)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ dismissNotification(true /* shouldOnboard */);
+ break;
+ case ACTION_DISMISS_NOTIFICATION:
+ dismissNotification(true /* shouldOnboard */);
+ break;
+ default:
+ Log.e(TAG, "Unknown action " + intent.getAction());
+ }
+ }
+ };
+
+ public WakeupOnboarding(
+ Context context,
+ WifiConfigManager wifiConfigManager,
+ Looper looper,
+ FrameworkFacade frameworkFacade,
+ WakeupNotificationFactory wakeupNotificationFactory) {
+ mContext = context;
+ mWifiConfigManager = wifiConfigManager;
+ mHandler = new Handler(looper);
+ mFrameworkFacade = frameworkFacade;
+ mWakeupNotificationFactory = wakeupNotificationFactory;
+
+ mIntentFilter = new IntentFilter();
+ mIntentFilter.addAction(ACTION_TURN_OFF_WIFI_WAKE);
+ mIntentFilter.addAction(ACTION_DISMISS_NOTIFICATION);
+ mIntentFilter.addAction(ACTION_OPEN_WIFI_PREFERENCES);
+ }
+
+ /** Returns whether the user is onboarded. */
+ public boolean isOnboarded() {
+ return mIsOnboarded;
+ }
+
+ /** Shows the onboarding notification if applicable. */
+ public void maybeShowNotification() {
+ if (isOnboarded() || mIsNotificationShowing) {
+ return;
+ }
+
+ Log.d(TAG, "Showing onboarding notification.");
+
+ mContext.registerReceiver(mBroadcastReceiver, mIntentFilter,
+ null /* broadcastPermission */, mHandler);
+ getNotificationManager().notify(SystemMessage.NOTE_WIFI_WAKE_ONBOARD,
+ mWakeupNotificationFactory.createOnboardingNotification());
+ mIsNotificationShowing = true;
+ }
+
+ /** Handles onboarding cleanup on stop. */
+ public void onStop() {
+ dismissNotification(false /* shouldOnboard */);
+ }
+
+ private void dismissNotification(boolean shouldOnboard) {
+ if (!mIsNotificationShowing) {
+ return;
+ }
+
+ if (shouldOnboard) {
+ setOnboarded();
+ }
+
+ mContext.unregisterReceiver(mBroadcastReceiver);
+ getNotificationManager().cancel(SystemMessage.NOTE_WIFI_WAKE_ONBOARD);
+ mIsNotificationShowing = false;
+ }
+
+ private void setOnboarded() {
+ Log.d(TAG, "Setting user as onboarded.");
+ mIsOnboarded = true;
+ mWifiConfigManager.saveToStore(false /* forceWrite */);
+ }
+
+ private NotificationManager getNotificationManager() {
+ if (mNotificationManager == null) {
+ mNotificationManager = (NotificationManager)
+ mContext.getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+ return mNotificationManager;
+ }
+
+ /** Returns the {@link WakeupConfigStoreData.DataSource} for the {@link WifiConfigStore}. */
+ public WakeupConfigStoreData.DataSource<Boolean> getDataSource() {
+ return new OnboardingDataSource();
+ }
+
+ private class OnboardingDataSource implements WakeupConfigStoreData.DataSource<Boolean> {
+
+ @Override
+ public Boolean getData() {
+ return mIsOnboarded;
+ }
+
+ @Override
+ public void setData(Boolean data) {
+ mIsOnboarded = data;
+ }
+ }
+}
diff --git a/com/android/server/wifi/WifiApConfigStore.java b/com/android/server/wifi/WifiApConfigStore.java
index 98a59329..19cd55a1 100644
--- a/com/android/server/wifi/WifiApConfigStore.java
+++ b/com/android/server/wifi/WifiApConfigStore.java
@@ -207,6 +207,7 @@ public class WifiApConfigStore {
*/
private WifiConfiguration getDefaultApConfiguration() {
WifiConfiguration config = new WifiConfiguration();
+ config.apBand = WifiConfiguration.AP_BAND_ANY;
config.SSID = mContext.getResources().getString(
R.string.wifi_tether_configure_ssid_default) + "_" + getRandomIntForDefaultSsid();
config.allowedKeyManagement.set(KeyMgmt.WPA2_PSK);
@@ -227,6 +228,8 @@ public class WifiApConfigStore {
*/
public static WifiConfiguration generateLocalOnlyHotspotConfig(Context context) {
WifiConfiguration config = new WifiConfiguration();
+ // For local only hotspot we only use 2.4Ghz band.
+ config.apBand = WifiConfiguration.AP_BAND_2GHZ;
config.SSID = context.getResources().getString(
R.string.wifi_localhotspot_configure_ssid_default) + "_"
+ getRandomIntForDefaultSsid();
diff --git a/com/android/server/wifi/WifiBackupDataParser.java b/com/android/server/wifi/WifiBackupDataParser.java
new file mode 100644
index 00000000..7699c559
--- /dev/null
+++ b/com/android/server/wifi/WifiBackupDataParser.java
@@ -0,0 +1,45 @@
+/*
+ * 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 com.android.server.wifi;
+
+import android.net.wifi.WifiConfiguration;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * Interface describing parser of WiFi backup data for each major version.
+ * Note that implementations of this interface should be returned
+ * from {@link WifiBackupRestore#getWifiBackupDataParser()} method based on major version they
+ * belong to.
+ */
+interface WifiBackupDataParser {
+
+ /**
+ * Parses the list of configurations from the provided XML stream.
+ *
+ * @param in XmlPullParser instance pointing to the XML stream.
+ * @param outerTagDepth depth of the outer tag in the XML document.
+ * @param minorVersion minor version number parsed from incoming data.
+ * @return List<WifiConfiguration> object if parsing is successful, null otherwise.
+ */
+ List<WifiConfiguration> parseNetworkConfigurationsFromXml(XmlPullParser in, int outerTagDepth,
+ int minorVersion) throws XmlPullParserException, IOException;
+}
diff --git a/com/android/server/wifi/WifiBackupDataV1Parser.java b/com/android/server/wifi/WifiBackupDataV1Parser.java
new file mode 100644
index 00000000..57573213
--- /dev/null
+++ b/com/android/server/wifi/WifiBackupDataV1Parser.java
@@ -0,0 +1,533 @@
+/*
+ * 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 com.android.server.wifi;
+
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.IpAssignment;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.LinkAddress;
+import android.net.NetworkUtils;
+import android.net.ProxyInfo;
+import android.net.RouteInfo;
+import android.net.StaticIpConfiguration;
+import android.net.wifi.WifiConfiguration;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.server.wifi.util.XmlUtil;
+import com.android.server.wifi.util.XmlUtil.IpConfigurationXmlUtil;
+import com.android.server.wifi.util.XmlUtil.WifiConfigurationXmlUtil;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Parser for major version 1 of WiFi backup data.
+ * Contains whitelists of tags for WifiConfiguration and IpConfiguration sections for each of
+ * the minor versions.
+ *
+ * Overall structure of the major version 1 XML schema:
+ * <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
+ * <WifiConfigStore>
+ * <float name="Version" value="1.0" />
+ * <NetworkList>
+ * <Network>
+ * <WifiConfiguration>
+ * <string name="ConfigKey">value</string>
+ * <string name="SSID">value</string>
+ * <string name="BSSID" />value</string>
+ * <string name="PreSharedKey" />value</string>
+ * <string-array name="WEPKeys" num="4">
+ * <item value="WifiConfigStoreWep1" />
+ * <item value="WifiConfigStoreWep2" />
+ * <item value="WifiConfigStoreWep3" />
+ * <item value="WifiConfigStoreWep3" />
+ * </string-array>
+ * ... (other supported tag names in minor version 1: "WEPTxKeyIndex", "HiddenSSID",
+ * "RequirePMF", "AllowedKeyMgmt", "AllowedProtocols", "AllowedAuthAlgos",
+ * "AllowedGroupCiphers", "AllowedPairwiseCiphers", "Shared")
+ * </WifiConfiguration>
+ * <IpConfiguration>
+ * <string name="IpAssignment">value</string>
+ * <string name="ProxySettings">value</string>
+ * ... (other supported tag names in minor version 1: "LinkAddress", "LinkPrefixLength",
+ * "GatewayAddress", "DNSServers", "ProxyHost", "ProxyPort", "ProxyPac",
+ * "ProxyExclusionList")
+ * </IpConfiguration>
+ * </Network>
+ * <Network>
+ * ... (format as above)
+ * </Network>
+ * </NetworkList>
+ * </WifiConfigStore>
+ */
+class WifiBackupDataV1Parser implements WifiBackupDataParser {
+
+ private static final String TAG = "WifiBackupDataV1Parser";
+
+ private static final int HIGHEST_SUPPORTED_MINOR_VERSION = 0;
+
+ // List of tags supported for <WifiConfiguration> section in minor version 0
+ private static final Set<String> WIFI_CONFIGURATION_MINOR_V0_SUPPORTED_TAGS =
+ new HashSet<String>(Arrays.asList(new String[] {
+ WifiConfigurationXmlUtil.XML_TAG_CONFIG_KEY,
+ WifiConfigurationXmlUtil.XML_TAG_SSID,
+ WifiConfigurationXmlUtil.XML_TAG_BSSID,
+ WifiConfigurationXmlUtil.XML_TAG_PRE_SHARED_KEY,
+ WifiConfigurationXmlUtil.XML_TAG_WEP_KEYS,
+ WifiConfigurationXmlUtil.XML_TAG_WEP_TX_KEY_INDEX,
+ WifiConfigurationXmlUtil.XML_TAG_HIDDEN_SSID,
+ WifiConfigurationXmlUtil.XML_TAG_REQUIRE_PMF,
+ WifiConfigurationXmlUtil.XML_TAG_ALLOWED_KEY_MGMT,
+ WifiConfigurationXmlUtil.XML_TAG_ALLOWED_PROTOCOLS,
+ WifiConfigurationXmlUtil.XML_TAG_ALLOWED_AUTH_ALGOS,
+ WifiConfigurationXmlUtil.XML_TAG_ALLOWED_GROUP_CIPHERS,
+ WifiConfigurationXmlUtil.XML_TAG_ALLOWED_PAIRWISE_CIPHERS,
+ WifiConfigurationXmlUtil.XML_TAG_SHARED,
+ }));
+
+ // List of tags supported for <IpConfiguration> section in minor version 0
+ private static final Set<String> IP_CONFIGURATION_MINOR_V0_SUPPORTED_TAGS =
+ new HashSet<String>(Arrays.asList(new String[] {
+ IpConfigurationXmlUtil.XML_TAG_IP_ASSIGNMENT,
+ IpConfigurationXmlUtil.XML_TAG_LINK_ADDRESS,
+ IpConfigurationXmlUtil.XML_TAG_LINK_PREFIX_LENGTH,
+ IpConfigurationXmlUtil.XML_TAG_GATEWAY_ADDRESS,
+ IpConfigurationXmlUtil.XML_TAG_DNS_SERVER_ADDRESSES,
+ IpConfigurationXmlUtil.XML_TAG_PROXY_SETTINGS,
+ IpConfigurationXmlUtil.XML_TAG_PROXY_HOST,
+ IpConfigurationXmlUtil.XML_TAG_PROXY_PORT,
+ IpConfigurationXmlUtil.XML_TAG_PROXY_EXCLUSION_LIST,
+ IpConfigurationXmlUtil.XML_TAG_PROXY_PAC_FILE,
+ }));
+
+ public List<WifiConfiguration> parseNetworkConfigurationsFromXml(XmlPullParser in,
+ int outerTagDepth, int minorVersion) throws XmlPullParserException, IOException {
+ // clamp down the minorVersion to the highest one that this parser version supports
+ if (minorVersion > HIGHEST_SUPPORTED_MINOR_VERSION) {
+ minorVersion = HIGHEST_SUPPORTED_MINOR_VERSION;
+ }
+ // Find the configuration list section.
+ XmlUtil.gotoNextSectionWithName(in, WifiBackupRestore.XML_TAG_SECTION_HEADER_NETWORK_LIST,
+ outerTagDepth);
+ // Find all the configurations within the configuration list section.
+ int networkListTagDepth = outerTagDepth + 1;
+ List<WifiConfiguration> configurations = new ArrayList<>();
+ while (XmlUtil.gotoNextSectionWithNameOrEnd(
+ in, WifiBackupRestore.XML_TAG_SECTION_HEADER_NETWORK, networkListTagDepth)) {
+ WifiConfiguration configuration =
+ parseNetworkConfigurationFromXml(in, minorVersion, networkListTagDepth);
+ if (configuration != null) {
+ Log.v(TAG, "Parsed Configuration: " + configuration.configKey());
+ configurations.add(configuration);
+ }
+ }
+ return configurations;
+ }
+
+ /**
+ * Parses the configuration data elements from the provided XML stream to a Configuration.
+ *
+ * @param in XmlPullParser instance pointing to the XML stream.
+ * @param minorVersion minor version number parsed from incoming data.
+ * @param outerTagDepth depth of the outer tag in the XML document.
+ * @return WifiConfiguration object if parsing is successful, null otherwise.
+ */
+ private WifiConfiguration parseNetworkConfigurationFromXml(XmlPullParser in, int minorVersion,
+ int outerTagDepth) throws XmlPullParserException, IOException {
+ WifiConfiguration configuration = null;
+ int networkTagDepth = outerTagDepth + 1;
+ // Retrieve WifiConfiguration object first.
+ XmlUtil.gotoNextSectionWithName(
+ in, WifiBackupRestore.XML_TAG_SECTION_HEADER_WIFI_CONFIGURATION,
+ networkTagDepth);
+ int configTagDepth = networkTagDepth + 1;
+ configuration = parseWifiConfigurationFromXmlAndValidateConfigKey(in, configTagDepth,
+ minorVersion);
+ if (configuration == null) {
+ return null;
+ }
+ // Now retrieve any IP configuration info.
+ XmlUtil.gotoNextSectionWithName(
+ in, WifiBackupRestore.XML_TAG_SECTION_HEADER_IP_CONFIGURATION, networkTagDepth);
+ IpConfiguration ipConfiguration = parseIpConfigurationFromXml(in, configTagDepth,
+ minorVersion);
+ configuration.setIpConfiguration(ipConfiguration);
+ return configuration;
+ }
+
+ /**
+ * Helper method to parse the WifiConfiguration object and validate the configKey parsed.
+ */
+ private WifiConfiguration parseWifiConfigurationFromXmlAndValidateConfigKey(XmlPullParser in,
+ int outerTagDepth, int minorVersion) throws XmlPullParserException, IOException {
+ Pair<String, WifiConfiguration> parsedConfig =
+ parseWifiConfigurationFromXml(in, outerTagDepth, minorVersion);
+ if (parsedConfig == null || parsedConfig.first == null || parsedConfig.second == null) {
+ return null;
+ }
+ String configKeyParsed = parsedConfig.first;
+ WifiConfiguration configuration = parsedConfig.second;
+ String configKeyCalculated = configuration.configKey();
+ if (!configKeyParsed.equals(configKeyCalculated)) {
+ String configKeyMismatchLog =
+ "Configuration key does not match. Retrieved: " + configKeyParsed
+ + ", Calculated: " + configKeyCalculated;
+ if (configuration.shared) {
+ Log.e(TAG, configKeyMismatchLog);
+ return null;
+ } else {
+ // ConfigKey mismatches are expected for private networks because the
+ // UID is not preserved across backup/restore.
+ Log.w(TAG, configKeyMismatchLog);
+ }
+ }
+ return configuration;
+ }
+
+ /**
+ * Parses the configuration data elements from the provided XML stream to a
+ * WifiConfiguration object.
+ * Looping through the tags makes it easy to add elements in the future minor versions if
+ * needed. Unsupported elements will be ignored.
+ *
+ * @param in XmlPullParser instance pointing to the XML stream.
+ * @param outerTagDepth depth of the outer tag in the XML document.
+ * @param minorVersion minor version number parsed from incoming data.
+ * @return Pair<Config key, WifiConfiguration object> if parsing is successful, null otherwise.
+ */
+ private static Pair<String, WifiConfiguration> parseWifiConfigurationFromXml(XmlPullParser in,
+ int outerTagDepth, int minorVersion) throws XmlPullParserException, IOException {
+ WifiConfiguration configuration = new WifiConfiguration();
+ String configKeyInData = null;
+ Set<String> supportedTags = getSupportedWifiConfigurationTags(minorVersion);
+
+ // Loop through and parse out all the elements from the stream within this section.
+ while (!XmlUtil.isNextSectionEnd(in, outerTagDepth)) {
+ String[] valueName = new String[1];
+ Object value = XmlUtil.readCurrentValue(in, valueName);
+ String tagName = valueName[0];
+ if (tagName == null) {
+ throw new XmlPullParserException("Missing value name");
+ }
+
+ // ignore the tags that are not supported up until the current minor version
+ if (!supportedTags.contains(tagName)) {
+ Log.w(TAG, "Unsupported tag + \"" + tagName + "\" found in <WifiConfiguration>"
+ + " section, ignoring.");
+ continue;
+ }
+
+ // note: the below switch case list should contain all tags supported up until the
+ // highest minor version supported by this parser
+ switch (tagName) {
+ case WifiConfigurationXmlUtil.XML_TAG_CONFIG_KEY:
+ configKeyInData = (String) value;
+ break;
+ case WifiConfigurationXmlUtil.XML_TAG_SSID:
+ configuration.SSID = (String) value;
+ break;
+ case WifiConfigurationXmlUtil.XML_TAG_BSSID:
+ configuration.BSSID = (String) value;
+ break;
+ case WifiConfigurationXmlUtil.XML_TAG_PRE_SHARED_KEY:
+ configuration.preSharedKey = (String) value;
+ break;
+ case WifiConfigurationXmlUtil.XML_TAG_WEP_KEYS:
+ populateWepKeysFromXmlValue(value, configuration.wepKeys);
+ break;
+ case WifiConfigurationXmlUtil.XML_TAG_WEP_TX_KEY_INDEX:
+ configuration.wepTxKeyIndex = (int) value;
+ break;
+ case WifiConfigurationXmlUtil.XML_TAG_HIDDEN_SSID:
+ configuration.hiddenSSID = (boolean) value;
+ break;
+ case WifiConfigurationXmlUtil.XML_TAG_REQUIRE_PMF:
+ configuration.requirePMF = (boolean) value;
+ break;
+ case WifiConfigurationXmlUtil.XML_TAG_ALLOWED_KEY_MGMT:
+ byte[] allowedKeyMgmt = (byte[]) value;
+ configuration.allowedKeyManagement = BitSet.valueOf(allowedKeyMgmt);
+ break;
+ case WifiConfigurationXmlUtil.XML_TAG_ALLOWED_PROTOCOLS:
+ byte[] allowedProtocols = (byte[]) value;
+ configuration.allowedProtocols = BitSet.valueOf(allowedProtocols);
+ break;
+ case WifiConfigurationXmlUtil.XML_TAG_ALLOWED_AUTH_ALGOS:
+ byte[] allowedAuthAlgorithms = (byte[]) value;
+ configuration.allowedAuthAlgorithms = BitSet.valueOf(allowedAuthAlgorithms);
+ break;
+ case WifiConfigurationXmlUtil.XML_TAG_ALLOWED_GROUP_CIPHERS:
+ byte[] allowedGroupCiphers = (byte[]) value;
+ configuration.allowedGroupCiphers = BitSet.valueOf(allowedGroupCiphers);
+ break;
+ case WifiConfigurationXmlUtil.XML_TAG_ALLOWED_PAIRWISE_CIPHERS:
+ byte[] allowedPairwiseCiphers = (byte[]) value;
+ configuration.allowedPairwiseCiphers =
+ BitSet.valueOf(allowedPairwiseCiphers);
+ break;
+ case WifiConfigurationXmlUtil.XML_TAG_SHARED:
+ configuration.shared = (boolean) value;
+ break;
+ default:
+ // should never happen, since other tags are filtered out earlier
+ throw new XmlPullParserException(
+ "Unknown value name found: " + valueName[0]);
+ }
+ }
+ return Pair.create(configKeyInData, configuration);
+ }
+
+ /**
+ * Returns a set of supported tags of <WifiConfiguration> element for all minor versions of
+ * this major version up to and including the specified minorVersion (only adding tags is
+ * supported in minor versions, removal or changing the meaning of tags requires bumping
+ * the major version and reseting the minor to 0).
+ *
+ * @param minorVersion minor version number parsed from incoming data.
+ */
+ private static Set<String> getSupportedWifiConfigurationTags(int minorVersion) {
+ switch (minorVersion) {
+ case 0: return WIFI_CONFIGURATION_MINOR_V0_SUPPORTED_TAGS;
+ default:
+ Log.e(TAG, "Invalid minorVersion: " + minorVersion);
+ return Collections.<String>emptySet();
+ }
+ }
+
+ /**
+ * Populate wepKeys array elements only if they were non-empty in the backup data.
+ *
+ * @throws XmlPullParserException if parsing errors occur.
+ */
+ private static void populateWepKeysFromXmlValue(Object value, String[] wepKeys)
+ throws XmlPullParserException, IOException {
+ String[] wepKeysInData = (String[]) value;
+ if (wepKeysInData == null) {
+ return;
+ }
+ if (wepKeysInData.length != wepKeys.length) {
+ throw new XmlPullParserException(
+ "Invalid Wep Keys length: " + wepKeysInData.length);
+ }
+ for (int i = 0; i < wepKeys.length; i++) {
+ if (wepKeysInData[i].isEmpty()) {
+ wepKeys[i] = null;
+ } else {
+ wepKeys[i] = wepKeysInData[i];
+ }
+ }
+ }
+
+ /**
+ * Parses the IP configuration data elements from the provided XML stream to an
+ * IpConfiguration object.
+ *
+ * @param in XmlPullParser instance pointing to the XML stream.
+ * @param outerTagDepth depth of the outer tag in the XML document.
+ * @param minorVersion minor version number parsed from incoming data.
+ * @return IpConfiguration object if parsing is successful, null otherwise.
+ */
+ private static IpConfiguration parseIpConfigurationFromXml(XmlPullParser in,
+ int outerTagDepth, int minorVersion) throws XmlPullParserException, IOException {
+ // First parse *all* of the tags in <IpConfiguration> section
+ Set<String> supportedTags = getSupportedIpConfigurationTags(minorVersion);
+
+ String ipAssignmentString = null;
+ String linkAddressString = null;
+ Integer linkPrefixLength = null;
+ String gatewayAddressString = null;
+ String[] dnsServerAddressesString = null;
+ String proxySettingsString = null;
+ String proxyHost = null;
+ int proxyPort = -1;
+ String proxyExclusionList = null;
+ String proxyPacFile = null;
+
+ // Loop through and parse out all the elements from the stream within this section.
+ while (!XmlUtil.isNextSectionEnd(in, outerTagDepth)) {
+ String[] valueName = new String[1];
+ Object value = XmlUtil.readCurrentValue(in, valueName);
+ String tagName = valueName[0];
+ if (tagName == null) {
+ throw new XmlPullParserException("Missing value name");
+ }
+
+ // ignore the tags that are not supported up until the current minor version
+ if (!supportedTags.contains(tagName)) {
+ Log.w(TAG, "Unsupported tag + \"" + tagName + "\" found in <IpConfiguration>"
+ + " section, ignoring.");
+ continue;
+ }
+
+ // note: the below switch case list should contain all tags supported up until the
+ // highest minor version supported by this parser
+ // should any tags be added in next minor versions, conditional processing of them
+ // also needs to be added in the below code (processing into IpConfiguration object)
+ switch (tagName) {
+ case IpConfigurationXmlUtil.XML_TAG_IP_ASSIGNMENT:
+ ipAssignmentString = (String) value;
+ break;
+ case IpConfigurationXmlUtil.XML_TAG_LINK_ADDRESS:
+ linkAddressString = (String) value;
+ break;
+ case IpConfigurationXmlUtil.XML_TAG_LINK_PREFIX_LENGTH:
+ linkPrefixLength = (Integer) value;
+ break;
+ case IpConfigurationXmlUtil.XML_TAG_GATEWAY_ADDRESS:
+ gatewayAddressString = (String) value;
+ break;
+ case IpConfigurationXmlUtil.XML_TAG_DNS_SERVER_ADDRESSES:
+ dnsServerAddressesString = (String[]) value;
+ break;
+ case IpConfigurationXmlUtil.XML_TAG_PROXY_SETTINGS:
+ proxySettingsString = (String) value;
+ break;
+ case IpConfigurationXmlUtil.XML_TAG_PROXY_HOST:
+ proxyHost = (String) value;
+ break;
+ case IpConfigurationXmlUtil.XML_TAG_PROXY_PORT:
+ proxyPort = (int) value;
+ break;
+ case IpConfigurationXmlUtil.XML_TAG_PROXY_EXCLUSION_LIST:
+ proxyExclusionList = (String) value;
+ break;
+ case IpConfigurationXmlUtil.XML_TAG_PROXY_PAC_FILE:
+ proxyPacFile = (String) value;
+ break;
+ default:
+ // should never happen, since other tags are filtered out earlier
+ throw new XmlPullParserException(
+ "Unknown value name found: " + valueName[0]);
+ }
+ }
+
+ // Now process the values into IpConfiguration object
+ IpConfiguration ipConfiguration = new IpConfiguration();
+ if (ipAssignmentString == null) {
+ throw new XmlPullParserException("IpAssignment was missing in IpConfiguration section");
+ }
+ IpAssignment ipAssignment = IpAssignment.valueOf(ipAssignmentString);
+ ipConfiguration.setIpAssignment(ipAssignment);
+ switch (ipAssignment) {
+ case STATIC:
+ StaticIpConfiguration staticIpConfiguration = new StaticIpConfiguration();
+ if (linkAddressString != null && linkPrefixLength != null) {
+ LinkAddress linkAddress = new LinkAddress(
+ NetworkUtils.numericToInetAddress(linkAddressString), linkPrefixLength);
+ if (linkAddress.getAddress() instanceof Inet4Address) {
+ staticIpConfiguration.ipAddress = linkAddress;
+ } else {
+ Log.w(TAG, "Non-IPv4 address: " + linkAddress);
+ }
+ }
+ if (gatewayAddressString != null) {
+ LinkAddress dest = null;
+ InetAddress gateway = NetworkUtils.numericToInetAddress(gatewayAddressString);
+ RouteInfo route = new RouteInfo(dest, gateway);
+ if (route.isIPv4Default()) {
+ staticIpConfiguration.gateway = gateway;
+ } else {
+ Log.w(TAG, "Non-IPv4 default route: " + route);
+ }
+ }
+ if (dnsServerAddressesString != null) {
+ for (String dnsServerAddressString : dnsServerAddressesString) {
+ InetAddress dnsServerAddress =
+ NetworkUtils.numericToInetAddress(dnsServerAddressString);
+ staticIpConfiguration.dnsServers.add(dnsServerAddress);
+ }
+ }
+ ipConfiguration.setStaticIpConfiguration(staticIpConfiguration);
+ break;
+ case DHCP:
+ case UNASSIGNED:
+ break;
+ default:
+ throw new XmlPullParserException("Unknown ip assignment type: " + ipAssignment);
+ }
+
+ // Process the proxy settings next
+ if (proxySettingsString == null) {
+ throw new XmlPullParserException("ProxySettings was missing in"
+ + " IpConfiguration section");
+ }
+ ProxySettings proxySettings = ProxySettings.valueOf(proxySettingsString);
+ ipConfiguration.setProxySettings(proxySettings);
+ switch (proxySettings) {
+ case STATIC:
+ if (proxyHost == null) {
+ throw new XmlPullParserException("ProxyHost was missing in"
+ + " IpConfiguration section");
+ }
+ if (proxyPort == -1) {
+ throw new XmlPullParserException("ProxyPort was missing in"
+ + " IpConfiguration section");
+ }
+ if (proxyExclusionList == null) {
+ throw new XmlPullParserException("ProxyExclusionList was missing in"
+ + " IpConfiguration section");
+ }
+ ipConfiguration.setHttpProxy(
+ new ProxyInfo(proxyHost, proxyPort, proxyExclusionList));
+ break;
+ case PAC:
+ if (proxyPacFile == null) {
+ throw new XmlPullParserException("ProxyPac was missing in"
+ + " IpConfiguration section");
+ }
+ ipConfiguration.setHttpProxy(new ProxyInfo(proxyPacFile));
+ break;
+ case NONE:
+ case UNASSIGNED:
+ break;
+ default:
+ throw new XmlPullParserException(
+ "Unknown proxy settings type: " + proxySettings);
+ }
+
+ return ipConfiguration;
+ }
+
+ /**
+ * Returns a set of supported tags of <IpConfiguration> element for all minor versions of
+ * this major version up to and including the specified minorVersion (only adding tags is
+ * supported in minor versions, removal or changing the meaning of tags requires bumping
+ * the major version and reseting the minor to 0).
+ *
+ * @param minorVersion minor version number parsed from incoming data.
+ */
+ private static Set<String> getSupportedIpConfigurationTags(int minorVersion) {
+ switch (minorVersion) {
+ case 0: return IP_CONFIGURATION_MINOR_V0_SUPPORTED_TAGS;
+ default:
+ Log.e(TAG, "Invalid minorVersion: " + minorVersion);
+ return Collections.<String>emptySet();
+ }
+ }
+}
diff --git a/com/android/server/wifi/WifiBackupRestore.java b/com/android/server/wifi/WifiBackupRestore.java
index ae5e411f..6b854b11 100644
--- a/com/android/server/wifi/WifiBackupRestore.java
+++ b/com/android/server/wifi/WifiBackupRestore.java
@@ -21,7 +21,6 @@ import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiEnterpriseConfig;
import android.os.Process;
import android.util.Log;
-import android.util.Pair;
import android.util.SparseArray;
import android.util.Xml;
@@ -62,9 +61,27 @@ public class WifiBackupRestore {
private static final String TAG = "WifiBackupRestore";
/**
- * Current backup data version. This will be incremented for any additions.
+ * Current backup data version.
+ * Note: before Android P this used to be an {@code int}, however support for minor versions
+ * has been added in Android P. Currently this field is a {@code float} representing
+ * "majorVersion.minorVersion" of the backed up data. MinorVersion starts with 0 and should
+ * be incremented when necessary. MajorVersion starts with 1 and bumping it up requires
+ * also resetting minorVersion to 0.
+ *
+ * MajorVersion will be incremented for modifications of the XML schema, excluding additive
+ * modifications in <WifiConfiguration> and/or <IpConfiguration> tags.
+ * Should the major version be bumped up, a new {@link WifiBackupDataParser} parser needs to
+ * be added and returned from {@link getWifiBackupDataParser()}.
+ * Note that bumping up the major version will result in inability to restore the backup
+ * set to those lower versions of SDK_INT that don't support the version.
+ *
+ * MinorVersion will only be incremented for addition of <WifiConfiguration> and/or
+ * <IpConfiguration> tags. Any other modifications to the schema should result in bumping up
+ * the major version and resetting the minor version to 0.
+ * Note that bumping up only the minor version will still allow restoring the backup set to
+ * lower versions of SDK_INT.
*/
- private static final int CURRENT_BACKUP_DATA_VERSION = 1;
+ private static final float CURRENT_BACKUP_DATA_VERSION = 1.0f;
/** This list of older versions will be used to restore data from older backups. */
/**
@@ -77,10 +94,11 @@ public class WifiBackupRestore {
*/
private static final String XML_TAG_DOCUMENT_HEADER = "WifiBackupData";
private static final String XML_TAG_VERSION = "Version";
- private static final String XML_TAG_SECTION_HEADER_NETWORK_LIST = "NetworkList";
- private static final String XML_TAG_SECTION_HEADER_NETWORK = "Network";
- private static final String XML_TAG_SECTION_HEADER_WIFI_CONFIGURATION = "WifiConfiguration";
- private static final String XML_TAG_SECTION_HEADER_IP_CONFIGURATION = "IpConfiguration";
+
+ static final String XML_TAG_SECTION_HEADER_NETWORK_LIST = "NetworkList";
+ static final String XML_TAG_SECTION_HEADER_NETWORK = "Network";
+ static final String XML_TAG_SECTION_HEADER_WIFI_CONFIGURATION = "WifiConfiguration";
+ static final String XML_TAG_SECTION_HEADER_IP_CONFIGURATION = "IpConfiguration";
/**
* Regex to mask out passwords in backup data dump.
@@ -207,7 +225,6 @@ public class WifiBackupRestore {
Log.e(TAG, "Invalid backup data received");
return null;
}
-
try {
if (mVerboseLoggingEnabled) {
mDebugLastBackupDataRestored = data;
@@ -221,13 +238,37 @@ public class WifiBackupRestore {
XmlUtil.gotoDocumentStart(in, XML_TAG_DOCUMENT_HEADER);
int rootTagDepth = in.getDepth();
- int version = (int) XmlUtil.readNextValueWithName(in, XML_TAG_VERSION);
- if (version < INITIAL_BACKUP_DATA_VERSION || version > CURRENT_BACKUP_DATA_VERSION) {
- Log.e(TAG, "Invalid version of data: " + version);
- return null;
+ int majorVersion = -1;
+ int minorVersion = -1;
+ try {
+ float version = (float) XmlUtil.readNextValueWithName(in, XML_TAG_VERSION);
+
+ // parse out major and minor versions
+ String versionStr = new Float(version).toString();
+ int separatorPos = versionStr.indexOf('.');
+ if (separatorPos == -1) {
+ majorVersion = Integer.parseInt(versionStr);
+ minorVersion = 0;
+ } else {
+ majorVersion = Integer.parseInt(versionStr.substring(0, separatorPos));
+ minorVersion = Integer.parseInt(versionStr.substring(separatorPos + 1));
+ }
+ } catch (ClassCastException cce) {
+ // Integer cannot be cast to Float for data coming from before Android P
+ majorVersion = 1;
+ minorVersion = 0;
}
+ Log.d(TAG, "Version of backup data - major: " + majorVersion
+ + "; minor: " + minorVersion);
- return parseNetworkConfigurationsFromXml(in, rootTagDepth, version);
+ WifiBackupDataParser parser = getWifiBackupDataParser(majorVersion);
+ if (parser == null) {
+ Log.w(TAG, "Major version of backup data is unknown to this Android"
+ + " version; not restoring");
+ return null;
+ } else {
+ return parser.parseNetworkConfigurationsFromXml(in, rootTagDepth, minorVersion);
+ }
} catch (XmlPullParserException | IOException | ClassCastException
| IllegalArgumentException e) {
Log.e(TAG, "Error parsing the backup data: " + e);
@@ -235,96 +276,14 @@ public class WifiBackupRestore {
return null;
}
- /**
- * Parses the list of configurations from the provided XML stream.
- *
- * @param in XmlPullParser instance pointing to the XML stream.
- * @param outerTagDepth depth of the outer tag in the XML document.
- * @param dataVersion version number parsed from incoming data.
- * @return List<WifiConfiguration> object if parsing is successful, null otherwise.
- */
- private List<WifiConfiguration> parseNetworkConfigurationsFromXml(
- XmlPullParser in, int outerTagDepth, int dataVersion)
- throws XmlPullParserException, IOException {
- // Find the configuration list section.
- XmlUtil.gotoNextSectionWithName(in, XML_TAG_SECTION_HEADER_NETWORK_LIST, outerTagDepth);
- // Find all the configurations within the configuration list section.
- int networkListTagDepth = outerTagDepth + 1;
- List<WifiConfiguration> configurations = new ArrayList<>();
- while (XmlUtil.gotoNextSectionWithNameOrEnd(
- in, XML_TAG_SECTION_HEADER_NETWORK, networkListTagDepth)) {
- WifiConfiguration configuration =
- parseNetworkConfigurationFromXml(in, dataVersion, networkListTagDepth);
- if (configuration != null) {
- Log.v(TAG, "Parsed Configuration: " + configuration.configKey());
- configurations.add(configuration);
- }
- }
- return configurations;
- }
-
- /**
- * Helper method to parse the WifiConfiguration object and validate the configKey parsed.
- */
- private WifiConfiguration parseWifiConfigurationFromXmlAndValidateConfigKey(
- XmlPullParser in, int outerTagDepth)
- throws XmlPullParserException, IOException {
- Pair<String, WifiConfiguration> parsedConfig =
- WifiConfigurationXmlUtil.parseFromXml(in, outerTagDepth);
- if (parsedConfig == null || parsedConfig.first == null || parsedConfig.second == null) {
- return null;
- }
- String configKeyParsed = parsedConfig.first;
- WifiConfiguration configuration = parsedConfig.second;
- String configKeyCalculated = configuration.configKey();
- if (!configKeyParsed.equals(configKeyCalculated)) {
- String configKeyMismatchLog =
- "Configuration key does not match. Retrieved: " + configKeyParsed
- + ", Calculated: " + configKeyCalculated;
- if (configuration.shared) {
- Log.e(TAG, configKeyMismatchLog);
- return null;
- } else {
- // ConfigKey mismatches are expected for private networks because the
- // UID is not preserved across backup/restore.
- Log.w(TAG, configKeyMismatchLog);
- }
- }
- return configuration;
- }
-
- /**
- * Parses the configuration data elements from the provided XML stream to a Configuration.
- *
- * @param in XmlPullParser instance pointing to the XML stream.
- * @param outerTagDepth depth of the outer tag in the XML document.
- * @param dataVersion version number parsed from incoming data.
- * @return WifiConfiguration object if parsing is successful, null otherwise.
- */
- private WifiConfiguration parseNetworkConfigurationFromXml(XmlPullParser in, int dataVersion,
- int outerTagDepth)
- throws XmlPullParserException, IOException {
- // Any version migration needs to be handled here in future.
- if (dataVersion == INITIAL_BACKUP_DATA_VERSION) {
- WifiConfiguration configuration = null;
- int networkTagDepth = outerTagDepth + 1;
- // Retrieve WifiConfiguration object first.
- XmlUtil.gotoNextSectionWithName(
- in, XML_TAG_SECTION_HEADER_WIFI_CONFIGURATION, networkTagDepth);
- int configTagDepth = networkTagDepth + 1;
- configuration = parseWifiConfigurationFromXmlAndValidateConfigKey(in, configTagDepth);
- if (configuration == null) {
+ private WifiBackupDataParser getWifiBackupDataParser(int majorVersion) {
+ switch (majorVersion) {
+ case INITIAL_BACKUP_DATA_VERSION:
+ return new WifiBackupDataV1Parser();
+ default:
+ Log.e(TAG, "Unrecognized majorVersion of backup data: " + majorVersion);
return null;
- }
- // Now retrieve any IP configuration info.
- XmlUtil.gotoNextSectionWithName(
- in, XML_TAG_SECTION_HEADER_IP_CONFIGURATION, networkTagDepth);
- IpConfiguration ipConfiguration =
- IpConfigurationXmlUtil.parseFromXml(in, configTagDepth);
- configuration.setIpConfiguration(ipConfiguration);
- return configuration;
}
- return null;
}
/**
diff --git a/com/android/server/wifi/WifiConfigManager.java b/com/android/server/wifi/WifiConfigManager.java
index ebd18cd4..e443cd42 100644
--- a/com/android/server/wifi/WifiConfigManager.java
+++ b/com/android/server/wifi/WifiConfigManager.java
@@ -47,6 +47,7 @@ import android.util.Log;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.LocalServices;
+import com.android.server.wifi.WifiConfigStoreLegacy.WifiConfigStoreDataLegacy;
import com.android.server.wifi.hotspot2.PasspointManager;
import com.android.server.wifi.util.TelephonyUtil;
import com.android.server.wifi.util.WifiPermissionsUtil;
@@ -241,6 +242,7 @@ public class WifiConfigManager {
private final TelephonyManager mTelephonyManager;
private final WifiKeyStore mWifiKeyStore;
private final WifiConfigStore mWifiConfigStore;
+ private final WifiConfigStoreLegacy mWifiConfigStoreLegacy;
private final WifiPermissionsUtil mWifiPermissionsUtil;
private final WifiPermissionsWrapper mWifiPermissionsWrapper;
/**
@@ -334,7 +336,7 @@ public class WifiConfigManager {
WifiConfigManager(
Context context, Clock clock, UserManager userManager,
TelephonyManager telephonyManager, WifiKeyStore wifiKeyStore,
- WifiConfigStore wifiConfigStore,
+ WifiConfigStore wifiConfigStore, WifiConfigStoreLegacy wifiConfigStoreLegacy,
WifiPermissionsUtil wifiPermissionsUtil,
WifiPermissionsWrapper wifiPermissionsWrapper,
NetworkListStoreData networkListStoreData,
@@ -346,6 +348,7 @@ public class WifiConfigManager {
mTelephonyManager = telephonyManager;
mWifiKeyStore = wifiKeyStore;
mWifiConfigStore = wifiConfigStore;
+ mWifiConfigStoreLegacy = wifiConfigStoreLegacy;
mWifiPermissionsUtil = wifiPermissionsUtil;
mWifiPermissionsWrapper = wifiPermissionsWrapper;
@@ -1904,13 +1907,6 @@ public class WifiConfigManager {
}
// Adding a new BSSID
- ScanResult result = scanDetailCache.getScanResult(scanResult.BSSID);
- if (result != null) {
- // transfer the black list status
- scanResult.blackListTimestamp = result.blackListTimestamp;
- scanResult.numIpConfigFailures = result.numIpConfigFailures;
- scanResult.numConnection = result.numConnection;
- }
if (config.ephemeral) {
// For an ephemeral Wi-Fi config, the ScanResult should be considered
// untrusted.
@@ -2416,7 +2412,8 @@ public class WifiConfigManager {
if (TelephonyUtil.isSimConfig(config)) {
String currentIdentity = null;
if (simPresent) {
- currentIdentity = TelephonyUtil.getSimIdentity(mTelephonyManager, config);
+ currentIdentity = TelephonyUtil.getSimIdentity(mTelephonyManager,
+ new TelephonyUtil(), config);
}
// Update the loaded config
config.enterpriseConfig.setIdentity(currentIdentity);
@@ -2681,6 +2678,46 @@ public class WifiConfigManager {
}
/**
+ * Migrate data from legacy store files. The function performs the following operations:
+ * 1. Check if the legacy store files are present and the new store files are absent on device.
+ * 2. Read all the data from the store files.
+ * 3. Save it to the new store files.
+ * 4. Delete the legacy store file.
+ *
+ * @return true if migration was successful or not needed (fresh install), false if it failed.
+ */
+ public boolean migrateFromLegacyStore() {
+ if (!mWifiConfigStoreLegacy.areStoresPresent()) {
+ Log.d(TAG, "Legacy store files not found. No migration needed!");
+ return true;
+ }
+ if (mWifiConfigStore.areStoresPresent()) {
+ Log.d(TAG, "New store files found. No migration needed!"
+ + " Remove legacy store files");
+ mWifiConfigStoreLegacy.removeStores();
+ return true;
+ }
+ WifiConfigStoreDataLegacy storeData = mWifiConfigStoreLegacy.read();
+ Log.d(TAG, "Reading from legacy store completed");
+ loadInternalData(storeData.getConfigurations(), new ArrayList<WifiConfiguration>(),
+ storeData.getDeletedEphemeralSSIDs());
+
+ // Setup user store for the current user in case it have not setup yet, so that data
+ // owned by the current user will be backed to the user store.
+ if (mDeferredUserUnlockRead) {
+ mWifiConfigStore.setUserStore(WifiConfigStore.createUserFile(mCurrentUserId));
+ mDeferredUserUnlockRead = false;
+ }
+
+ if (!saveToStore(true)) {
+ return false;
+ }
+ mWifiConfigStoreLegacy.removeStores();
+ Log.d(TAG, "Migration from legacy store completed");
+ return true;
+ }
+
+ /**
* Read the config store and load the in-memory lists from the store data retrieved and sends
* out the networks changed broadcast.
*
@@ -2694,7 +2731,10 @@ public class WifiConfigManager {
public boolean loadFromStore() {
if (!mWifiConfigStore.areStoresPresent()) {
Log.d(TAG, "New store files not found. No saved networks loaded!");
- mPendingStoreRead = false;
+ if (!mWifiConfigStoreLegacy.areStoresPresent()) {
+ // No legacy store files either, so reset the pending store read flag.
+ mPendingStoreRead = false;
+ }
return true;
}
// If the user unlock comes in before we load from store, which means the user store have
@@ -2755,6 +2795,10 @@ public class WifiConfigManager {
* @return Whether the write was successful or not, this is applicable only for force writes.
*/
public boolean saveToStore(boolean forceWrite) {
+ if (mPendingStoreRead) {
+ Log.e(TAG, "Cannot save to store before store is read!");
+ return false;
+ }
ArrayList<WifiConfiguration> sharedConfigurations = new ArrayList<>();
ArrayList<WifiConfiguration> userConfigurations = new ArrayList<>();
// List of network IDs for legacy Passpoint configuration to be removed.
diff --git a/com/android/server/wifi/WifiConfigStore.java b/com/android/server/wifi/WifiConfigStore.java
index 659192be..17a6670f 100644
--- a/com/android/server/wifi/WifiConfigStore.java
+++ b/com/android/server/wifi/WifiConfigStore.java
@@ -16,6 +16,7 @@
package com.android.server.wifi;
+import android.annotation.Nullable;
import android.app.AlarmManager;
import android.content.Context;
import android.os.Environment;
@@ -41,8 +42,11 @@ import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
+import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
+import java.util.Set;
/**
* This class provides the API's to save/load/modify network configurations from a persistent
@@ -341,7 +345,9 @@ public class WifiConfigStore {
public void read() throws XmlPullParserException, IOException {
// Reset both share and user store data.
resetStoreData(true);
- resetStoreData(false);
+ if (mUserStore != null) {
+ resetStoreData(false);
+ }
long readStartTime = mClock.getElapsedSinceBootMillis();
byte[] sharedDataBytes = mSharedStore.readRawData();
@@ -352,7 +358,9 @@ public class WifiConfigStore {
long readTime = mClock.getElapsedSinceBootMillis() - readStartTime;
Log.d(TAG, "Reading from stores completed in " + readTime + " ms.");
deserializeData(sharedDataBytes, true);
- deserializeData(userDataBytes, false);
+ if (mUserStore != null) {
+ deserializeData(userDataBytes, false);
+ }
}
/**
@@ -390,6 +398,14 @@ public class WifiConfigStore {
}
}
+ // Inform all the provided store data clients that there is nothing in the store for them.
+ private void indicateNoDataForStoreDatas(Collection<StoreData> storeDataSet, boolean shareData)
+ throws XmlPullParserException, IOException {
+ for (StoreData storeData : storeDataSet) {
+ storeData.deserializeData(null, 0, shareData);
+ }
+ }
+
/**
* Deserialize share data or user data into store data.
*
@@ -401,6 +417,7 @@ public class WifiConfigStore {
private void deserializeData(byte[] dataBytes, boolean shareData)
throws XmlPullParserException, IOException {
if (dataBytes == null) {
+ indicateNoDataForStoreDatas(mStoreDataList.values(), shareData);
return;
}
final XmlPullParser in = Xml.newPullParser();
@@ -412,13 +429,20 @@ public class WifiConfigStore {
parseDocumentStartAndVersionFromXml(in);
String[] headerName = new String[1];
+ Set<StoreData> storeDatasInvoked = new HashSet<>();
while (XmlUtil.gotoNextSectionOrEnd(in, headerName, rootTagDepth)) {
StoreData storeData = mStoreDataList.get(headerName[0]);
if (storeData == null) {
throw new XmlPullParserException("Unknown store data: " + headerName[0]);
}
storeData.deserializeData(in, rootTagDepth + 1, shareData);
+ storeDatasInvoked.add(storeData);
}
+ // Inform all the other registered store data clients that there is nothing in the store
+ // for them.
+ Set<StoreData> storeDatasNotInvoked = new HashSet<>(mStoreDataList.values());
+ storeDatasNotInvoked.removeAll(storeDatasInvoked);
+ indicateNoDataForStoreDatas(storeDatasNotInvoked, shareData);
}
/**
@@ -536,6 +560,13 @@ public class WifiConfigStore {
* Interface to be implemented by a module that contained data in the config store file.
*
* The module will be responsible for serializing/deserializing their own data.
+ * Whenever {@link WifiConfigStore#read()} is invoked, all registered StoreData instances will
+ * be notified that a read was performed via {@link StoreData#deserializeData(
+ * XmlPullParser, int, boolean)} regardless of whether there is any data for them or not in the
+ * store file.
+ *
+ * Note: StoreData clients that need a config store read to kick-off operations should wait
+ * for the {@link StoreData#deserializeData(XmlPullParser, int, boolean)} invocation.
*/
public interface StoreData {
/**
@@ -555,12 +586,16 @@ public class WifiConfigStore {
* the shared configuration data will be overwritten by the parsed data. Otherwise,
* the user configuration will be overwritten by the parsed data.
*
- * @param in The input stream to read the data from
+ * @param in The input stream to read the data from. This could be null if there is
+ * nothing in the store.
* @param outerTagDepth The depth of the outer tag in the XML document
* @Param shared Flag indicating if the input stream is backed by a share store or an
* user store
+ * Note: This will be invoked every time a store file is read. For example: clients
+ * will get 2 invocations on bootup, one for shared store file (shared=True) &
+ * one for user store file (shared=False).
*/
- void deserializeData(XmlPullParser in, int outerTagDepth, boolean shared)
+ void deserializeData(@Nullable XmlPullParser in, int outerTagDepth, boolean shared)
throws XmlPullParserException, IOException;
/**
diff --git a/com/android/server/wifi/WifiConfigStoreLegacy.java b/com/android/server/wifi/WifiConfigStoreLegacy.java
new file mode 100644
index 00000000..184ee2f6
--- /dev/null
+++ b/com/android/server/wifi/WifiConfigStoreLegacy.java
@@ -0,0 +1,355 @@
+/*
+ * 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.net.IpConfiguration;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiEnterpriseConfig;
+import android.os.Environment;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.server.net.IpConfigStore;
+import com.android.server.wifi.hotspot2.LegacyPasspointConfig;
+import com.android.server.wifi.hotspot2.LegacyPasspointConfigParser;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class provides the API's to load network configurations from legacy store
+ * mechanism (Pre O release).
+ * This class loads network configurations from:
+ * 1. /data/misc/wifi/networkHistory.txt
+ * 2. /data/misc/wifi/wpa_supplicant.conf
+ * 3. /data/misc/wifi/ipconfig.txt
+ * 4. /data/misc/wifi/PerProviderSubscription.conf
+ *
+ * The order of invocation of the public methods during migration is the following:
+ * 1. Check if legacy stores are present using {@link #areStoresPresent()}.
+ * 2. Load all the store data using {@link #read()}
+ * 3. Write the store data to the new store.
+ * 4. Remove all the legacy stores using {@link #removeStores()}
+ *
+ * NOTE: This class should only be used from WifiConfigManager and is not thread-safe!
+ *
+ * TODO(b/31065385): Passpoint config store data migration & deletion.
+ */
+public class WifiConfigStoreLegacy {
+ /**
+ * Log tag.
+ */
+ private static final String TAG = "WifiConfigStoreLegacy";
+ /**
+ * NetworkHistory config store file path.
+ */
+ private static final File NETWORK_HISTORY_FILE =
+ new File(WifiNetworkHistory.NETWORK_HISTORY_CONFIG_FILE);
+ /**
+ * Passpoint config store file path.
+ */
+ private static final File PPS_FILE =
+ new File(Environment.getDataMiscDirectory(), "wifi/PerProviderSubscription.conf");
+ /**
+ * IpConfig config store file path.
+ */
+ private static final File IP_CONFIG_FILE =
+ new File(Environment.getDataMiscDirectory(), "wifi/ipconfig.txt");
+ /**
+ * List of external dependencies for WifiConfigManager.
+ */
+ private final WifiNetworkHistory mWifiNetworkHistory;
+ private final WifiNative mWifiNative;
+ private final IpConfigStore mIpconfigStore;
+
+ private final LegacyPasspointConfigParser mPasspointConfigParser;
+
+ WifiConfigStoreLegacy(WifiNetworkHistory wifiNetworkHistory,
+ WifiNative wifiNative, IpConfigStore ipConfigStore,
+ LegacyPasspointConfigParser passpointConfigParser) {
+ mWifiNetworkHistory = wifiNetworkHistory;
+ mWifiNative = wifiNative;
+ mIpconfigStore = ipConfigStore;
+ mPasspointConfigParser = passpointConfigParser;
+ }
+
+ /**
+ * Helper function to lookup the WifiConfiguration object from configKey to WifiConfiguration
+ * object map using the hashcode of the configKey.
+ *
+ * @param configurationMap Map of configKey to WifiConfiguration object.
+ * @param hashCode hash code of the configKey to match.
+ * @return
+ */
+ private static WifiConfiguration lookupWifiConfigurationUsingConfigKeyHash(
+ Map<String, WifiConfiguration> configurationMap, int hashCode) {
+ for (Map.Entry<String, WifiConfiguration> entry : configurationMap.entrySet()) {
+ if (entry.getKey().hashCode() == hashCode) {
+ return entry.getValue();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Helper function to load {@link IpConfiguration} data from the ip config store file and
+ * populate the provided configuration map.
+ *
+ * @param configurationMap Map of configKey to WifiConfiguration object.
+ */
+ private void loadFromIpConfigStore(Map<String, WifiConfiguration> configurationMap) {
+ // This is a map of the hash code of the network's configKey to the corresponding
+ // IpConfiguration.
+ SparseArray<IpConfiguration> ipConfigurations =
+ mIpconfigStore.readIpAndProxyConfigurations(IP_CONFIG_FILE.getAbsolutePath());
+ if (ipConfigurations == null || ipConfigurations.size() == 0) {
+ Log.w(TAG, "No ip configurations found in ipconfig store");
+ return;
+ }
+ for (int i = 0; i < ipConfigurations.size(); i++) {
+ int id = ipConfigurations.keyAt(i);
+ WifiConfiguration config =
+ lookupWifiConfigurationUsingConfigKeyHash(configurationMap, id);
+ // This is the only place the map is looked up through a (dangerous) hash-value!
+ if (config == null || config.ephemeral) {
+ Log.w(TAG, "configuration found for missing network, nid=" + id
+ + ", ignored, networks.size=" + Integer.toString(ipConfigurations.size()));
+ } else {
+ config.setIpConfiguration(ipConfigurations.valueAt(i));
+ }
+ }
+ }
+
+ /**
+ * Helper function to load {@link WifiConfiguration} data from networkHistory file and populate
+ * the provided configuration map and deleted ephemeral ssid list.
+ *
+ * @param configurationMap Map of configKey to WifiConfiguration object.
+ * @param deletedEphemeralSSIDs Map of configKey to WifiConfiguration object.
+ */
+ private void loadFromNetworkHistory(
+ Map<String, WifiConfiguration> configurationMap, Set<String> deletedEphemeralSSIDs) {
+ // TODO: Need to revisit the scan detail cache persistance. We're not doing it in the new
+ // config store, so ignore it here as well.
+ Map<Integer, ScanDetailCache> scanDetailCaches = new HashMap<>();
+ mWifiNetworkHistory.readNetworkHistory(
+ configurationMap, scanDetailCaches, deletedEphemeralSSIDs);
+ }
+
+ /**
+ * Helper function to load {@link WifiConfiguration} data from wpa_supplicant and populate
+ * the provided configuration map and network extras.
+ *
+ * This method needs to manually parse the wpa_supplicant.conf file to retrieve some of the
+ * password fields like psk, wep_keys. password, etc.
+ *
+ * @param configurationMap Map of configKey to WifiConfiguration object.
+ * @param networkExtras Map of network extras parsed from wpa_supplicant.
+ */
+ private void loadFromWpaSupplicant(
+ Map<String, WifiConfiguration> configurationMap,
+ SparseArray<Map<String, String>> networkExtras) {
+ if (!mWifiNative.migrateNetworksFromSupplicant(mWifiNative.getClientInterfaceName(),
+ configurationMap, networkExtras)) {
+ Log.wtf(TAG, "Failed to load wifi configurations from wpa_supplicant");
+ return;
+ }
+ if (configurationMap.isEmpty()) {
+ Log.w(TAG, "No wifi configurations found in wpa_supplicant");
+ return;
+ }
+ }
+
+ /**
+ * Helper function to update {@link WifiConfiguration} that represents a Passpoint
+ * configuration.
+ *
+ * This method will manually parse PerProviderSubscription.conf file to retrieve missing
+ * fields: provider friendly name, roaming consortium OIs, realm, IMSI.
+ *
+ * @param configurationMap Map of configKey to WifiConfiguration object.
+ * @param networkExtras Map of network extras parsed from wpa_supplicant.
+ */
+ private void loadFromPasspointConfigStore(
+ Map<String, WifiConfiguration> configurationMap,
+ SparseArray<Map<String, String>> networkExtras) {
+ Map<String, LegacyPasspointConfig> passpointConfigMap = null;
+ try {
+ passpointConfigMap = mPasspointConfigParser.parseConfig(PPS_FILE.getAbsolutePath());
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to read/parse Passpoint config file: " + e.getMessage());
+ }
+
+ List<String> entriesToBeRemoved = new ArrayList<>();
+ for (Map.Entry<String, WifiConfiguration> entry : configurationMap.entrySet()) {
+ WifiConfiguration wifiConfig = entry.getValue();
+ // Ignore non-Enterprise network since enterprise configuration is required for
+ // Passpoint.
+ if (wifiConfig.enterpriseConfig == null || wifiConfig.enterpriseConfig.getEapMethod()
+ == WifiEnterpriseConfig.Eap.NONE) {
+ continue;
+ }
+ // Ignore configuration without FQDN.
+ Map<String, String> extras = networkExtras.get(wifiConfig.networkId);
+ if (extras == null || !extras.containsKey(SupplicantStaNetworkHal.ID_STRING_KEY_FQDN)) {
+ continue;
+ }
+ String fqdn = networkExtras.get(wifiConfig.networkId).get(
+ SupplicantStaNetworkHal.ID_STRING_KEY_FQDN);
+
+ // Remove the configuration if failed to find the matching configuration in the
+ // Passpoint configuration file.
+ if (passpointConfigMap == null || !passpointConfigMap.containsKey(fqdn)) {
+ entriesToBeRemoved.add(entry.getKey());
+ continue;
+ }
+
+ // Update the missing Passpoint configuration fields to this WifiConfiguration.
+ LegacyPasspointConfig passpointConfig = passpointConfigMap.get(fqdn);
+ wifiConfig.isLegacyPasspointConfig = true;
+ wifiConfig.FQDN = fqdn;
+ wifiConfig.providerFriendlyName = passpointConfig.mFriendlyName;
+ if (passpointConfig.mRoamingConsortiumOis != null) {
+ wifiConfig.roamingConsortiumIds = Arrays.copyOf(
+ passpointConfig.mRoamingConsortiumOis,
+ passpointConfig.mRoamingConsortiumOis.length);
+ }
+ if (passpointConfig.mImsi != null) {
+ wifiConfig.enterpriseConfig.setPlmn(passpointConfig.mImsi);
+ }
+ if (passpointConfig.mRealm != null) {
+ wifiConfig.enterpriseConfig.setRealm(passpointConfig.mRealm);
+ }
+ }
+
+ // Remove any incomplete Passpoint configurations. Should never happen, in case it does
+ // remove them to avoid maintaining any invalid Passpoint configurations.
+ for (String key : entriesToBeRemoved) {
+ Log.w(TAG, "Remove incomplete Passpoint configuration: " + key);
+ configurationMap.remove(key);
+ }
+ }
+
+ /**
+ * Helper function to load from the different legacy stores:
+ * 1. Read the network configurations from wpa_supplicant using {@link WifiNative}.
+ * 2. Read the network configurations from networkHistory.txt using {@link WifiNetworkHistory}.
+ * 3. Read the Ip configurations from ipconfig.txt using {@link IpConfigStore}.
+ * 4. Read all the passpoint info from PerProviderSubscription.conf using
+ * {@link LegacyPasspointConfigParser}.
+ */
+ public WifiConfigStoreDataLegacy read() {
+ final Map<String, WifiConfiguration> configurationMap = new HashMap<>();
+ final SparseArray<Map<String, String>> networkExtras = new SparseArray<>();
+ final Set<String> deletedEphemeralSSIDs = new HashSet<>();
+
+ loadFromWpaSupplicant(configurationMap, networkExtras);
+ loadFromNetworkHistory(configurationMap, deletedEphemeralSSIDs);
+ loadFromIpConfigStore(configurationMap);
+ loadFromPasspointConfigStore(configurationMap, networkExtras);
+
+ // Now create config store data instance to be returned.
+ return new WifiConfigStoreDataLegacy(
+ new ArrayList<>(configurationMap.values()), deletedEphemeralSSIDs);
+ }
+
+ /**
+ * Function to check if the legacy store files are present and hence load from those stores and
+ * then delete them.
+ *
+ * @return true if legacy store files are present, false otherwise.
+ */
+ public boolean areStoresPresent() {
+ // We may have to keep the wpa_supplicant.conf file around. So, just use networkhistory.txt
+ // as a check to see if we have not yet migrated or not. This should be the last file
+ // that is deleted after migration.
+ File file = new File(WifiNetworkHistory.NETWORK_HISTORY_CONFIG_FILE);
+ return file.exists();
+ }
+
+ /**
+ * Method to remove all the legacy store files. This should only be invoked once all
+ * the data has been migrated to the new store file.
+ * 1. Removes all networks from wpa_supplicant and saves it to wpa_supplicant.conf
+ * 2. Deletes ipconfig.txt
+ * 3. Deletes networkHistory.txt
+ *
+ * @return true if all the store files were deleted successfully, false otherwise.
+ */
+ public boolean removeStores() {
+ // TODO(b/29352330): Delete wpa_supplicant.conf file instead.
+ // First remove all networks from wpa_supplicant and save configuration.
+ if (!mWifiNative.removeAllNetworks(mWifiNative.getClientInterfaceName())) {
+ Log.e(TAG, "Removing networks from wpa_supplicant failed");
+ }
+
+ // Now remove the ipconfig.txt file.
+ if (!IP_CONFIG_FILE.delete()) {
+ Log.e(TAG, "Removing ipconfig.txt failed");
+ }
+
+ // Now finally remove network history.txt
+ if (!NETWORK_HISTORY_FILE.delete()) {
+ Log.e(TAG, "Removing networkHistory.txt failed");
+ }
+
+ if (!PPS_FILE.delete()) {
+ Log.e(TAG, "Removing PerProviderSubscription.conf failed");
+ }
+
+ Log.i(TAG, "All legacy stores removed!");
+ return true;
+ }
+
+ /**
+ * Interface used to set a masked value in the provided configuration. The masked value is
+ * retrieved by parsing the wpa_supplicant.conf file.
+ */
+ private interface MaskedWpaSupplicantFieldSetter {
+ void setValue(WifiConfiguration config, String value);
+ }
+
+ /**
+ * Class used to encapsulate all the store data retrieved from the legacy (Pre O) store files.
+ */
+ public static class WifiConfigStoreDataLegacy {
+ private List<WifiConfiguration> mConfigurations;
+ private Set<String> mDeletedEphemeralSSIDs;
+ // private List<HomeSP> mHomeSps;
+
+ WifiConfigStoreDataLegacy(List<WifiConfiguration> configurations,
+ Set<String> deletedEphemeralSSIDs) {
+ mConfigurations = configurations;
+ mDeletedEphemeralSSIDs = deletedEphemeralSSIDs;
+ }
+
+ public List<WifiConfiguration> getConfigurations() {
+ return mConfigurations;
+ }
+
+ public Set<String> getDeletedEphemeralSSIDs() {
+ return mDeletedEphemeralSSIDs;
+ }
+ }
+}
diff --git a/com/android/server/wifi/WifiConfigurationUtil.java b/com/android/server/wifi/WifiConfigurationUtil.java
index ea58549c..fc298e7e 100644
--- a/com/android/server/wifi/WifiConfigurationUtil.java
+++ b/com/android/server/wifi/WifiConfigurationUtil.java
@@ -199,6 +199,10 @@ public class WifiConfigurationUtil {
newEnterpriseConfig.getAnonymousIdentity())) {
return true;
}
+ if (!TextUtils.equals(existingEnterpriseConfig.getPassword(),
+ newEnterpriseConfig.getPassword())) {
+ return true;
+ }
X509Certificate[] existingCaCerts = existingEnterpriseConfig.getCaCertificates();
X509Certificate[] newCaCerts = newEnterpriseConfig.getCaCertificates();
if (!Arrays.equals(existingCaCerts, newCaCerts)) {
diff --git a/com/android/server/wifi/WifiConnectivityHelper.java b/com/android/server/wifi/WifiConnectivityHelper.java
index 4aac3116..b2347d7f 100644
--- a/com/android/server/wifi/WifiConnectivityHelper.java
+++ b/com/android/server/wifi/WifiConnectivityHelper.java
@@ -59,7 +59,7 @@ public class WifiConnectivityHelper {
mMaxNumBlacklistBssid = INVALID_LIST_SIZE;
mMaxNumWhitelistSsid = INVALID_LIST_SIZE;
- int fwFeatureSet = mWifiNative.getSupportedFeatureSet();
+ int fwFeatureSet = mWifiNative.getSupportedFeatureSet(mWifiNative.getClientInterfaceName());
Log.d(TAG, "Firmware supported feature set: " + Integer.toHexString(fwFeatureSet));
if ((fwFeatureSet & WIFI_FEATURE_CONTROL_ROAMING) == 0) {
@@ -68,7 +68,7 @@ public class WifiConnectivityHelper {
}
WifiNative.RoamingCapabilities roamingCap = new WifiNative.RoamingCapabilities();
- if (mWifiNative.getRoamingCapabilities(roamingCap)) {
+ if (mWifiNative.getRoamingCapabilities(mWifiNative.getClientInterfaceName(), roamingCap)) {
if (roamingCap.maxBlacklistSize < 0 || roamingCap.maxWhitelistSize < 0) {
Log.e(TAG, "Invalid firmware roaming capabilities: max num blacklist bssid="
+ roamingCap.maxBlacklistSize + " max num whitelist ssid="
@@ -159,7 +159,7 @@ public class WifiConnectivityHelper {
roamConfig.blacklistBssids = blacklistBssids;
roamConfig.whitelistSsids = whitelistSsids;
- return mWifiNative.configureRoaming(roamConfig);
+ return mWifiNative.configureRoaming(mWifiNative.getClientInterfaceName(), roamConfig);
}
/**
@@ -169,6 +169,6 @@ public class WifiConnectivityHelper {
* @param networkId network id of the network to be removed from supplicant.
*/
public void removeNetworkIfCurrent(int networkId) {
- mWifiNative.removeNetworkIfCurrent(networkId);
+ mWifiNative.removeNetworkIfCurrent(mWifiNative.getClientInterfaceName(), networkId);
}
}
diff --git a/com/android/server/wifi/WifiConnectivityManager.java b/com/android/server/wifi/WifiConnectivityManager.java
index c67e7c64..e3da50c1 100644
--- a/com/android/server/wifi/WifiConnectivityManager.java
+++ b/com/android/server/wifi/WifiConnectivityManager.java
@@ -37,6 +37,7 @@ import android.util.Log;
import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
import com.android.server.wifi.hotspot2.PasspointNetworkEvaluator;
import com.android.server.wifi.util.ScanResultUtil;
@@ -136,6 +137,8 @@ public class WifiConnectivityManager {
private final WifiNetworkSelector mNetworkSelector;
private final WifiLastResortWatchdog mWifiLastResortWatchdog;
private final OpenNetworkNotifier mOpenNetworkNotifier;
+ private final CarrierNetworkNotifier mCarrierNetworkNotifier;
+ private final CarrierNetworkConfig mCarrierNetworkConfig;
private final WifiMetrics mWifiMetrics;
private final AlarmManager mAlarmManager;
private final Handler mEventHandler;
@@ -274,6 +277,8 @@ public class WifiConnectivityManager {
if (mWifiState == WIFI_STATE_DISCONNECTED) {
mOpenNetworkNotifier.handleScanResults(
mNetworkSelector.getFilteredScanDetailsForOpenUnsavedNetworks());
+ mCarrierNetworkNotifier.handleScanResults(mNetworkSelector
+ .getFilteredScanDetailsForCarrierUnsavedNetworks(mCarrierNetworkConfig));
}
return false;
}
@@ -286,9 +291,11 @@ public class WifiConnectivityManager {
// other modules.
private class AllSingleScanListener implements WifiScanner.ScanListener {
private List<ScanDetail> mScanDetails = new ArrayList<ScanDetail>();
+ private int mNumScanResultsIgnoredDueToSingleRadioChain = 0;
public void clearScanDetails() {
mScanDetails.clear();
+ mNumScanResultsIgnoredDueToSingleRadioChain = 0;
}
@Override
@@ -327,6 +334,10 @@ public class WifiConnectivityManager {
mWifiMetrics.incrementAvailableNetworksHistograms(mScanDetails,
results[0].isAllChannelsScanned());
}
+ if (mNumScanResultsIgnoredDueToSingleRadioChain > 0) {
+ Log.i(TAG, "Number of scan results ignored due to single radio chain scan: "
+ + mNumScanResultsIgnoredDueToSingleRadioChain);
+ }
boolean wasConnectAttempted = handleScanResults(mScanDetails, "AllSingleScanListener");
clearScanDetails();
@@ -354,6 +365,14 @@ public class WifiConnectivityManager {
+ " capabilities " + fullScanResult.capabilities);
}
+ // When the scan result has radio chain info, ensure we throw away scan results
+ // not received with both radio chains.
+ if (ArrayUtils.size(fullScanResult.radioChainInfos) == 1) {
+ // Keep track of the number of dropped scan results for logging.
+ mNumScanResultsIgnoredDueToSingleRadioChain++;
+ return;
+ }
+
mScanDetails.add(ScanResultUtil.toScanDetail(fullScanResult));
}
}
@@ -543,10 +562,10 @@ public class WifiConnectivityManager {
WifiConnectivityManager(Context context, WifiStateMachine stateMachine,
WifiScanner scanner, WifiConfigManager configManager, WifiInfo wifiInfo,
WifiNetworkSelector networkSelector, WifiConnectivityHelper connectivityHelper,
- WifiLastResortWatchdog wifiLastResortWatchdog,
- OpenNetworkNotifier openNetworkNotifier, WifiMetrics wifiMetrics,
- Looper looper, Clock clock, LocalLog localLog, boolean enable,
- FrameworkFacade frameworkFacade,
+ WifiLastResortWatchdog wifiLastResortWatchdog, OpenNetworkNotifier openNetworkNotifier,
+ CarrierNetworkNotifier carrierNetworkNotifier,
+ CarrierNetworkConfig carrierNetworkConfig, WifiMetrics wifiMetrics, Looper looper,
+ Clock clock, LocalLog localLog, boolean enable, FrameworkFacade frameworkFacade,
SavedNetworkEvaluator savedNetworkEvaluator,
ScoredNetworkEvaluator scoredNetworkEvaluator,
PasspointNetworkEvaluator passpointNetworkEvaluator) {
@@ -559,6 +578,8 @@ public class WifiConnectivityManager {
mLocalLog = localLog;
mWifiLastResortWatchdog = wifiLastResortWatchdog;
mOpenNetworkNotifier = openNetworkNotifier;
+ mCarrierNetworkNotifier = carrierNetworkNotifier;
+ mCarrierNetworkConfig = carrierNetworkConfig;
mWifiMetrics = wifiMetrics;
mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
mEventHandler = new Handler(looper);
@@ -879,6 +900,7 @@ public class WifiConnectivityManager {
isFullBandScan = true;
}
}
+ settings.type = WifiScanner.TYPE_HIGH_ACCURACY; // always do high accuracy scans.
settings.band = getScanBand(isFullBandScan);
settings.reportEvents = WifiScanner.REPORT_EVENT_FULL_SCAN_RESULT
| WifiScanner.REPORT_EVENT_AFTER_EACH_SCAN;
@@ -1060,6 +1082,7 @@ public class WifiConnectivityManager {
mScreenOn = screenOn;
mOpenNetworkNotifier.handleScreenStateChanged(screenOn);
+ mCarrierNetworkNotifier.handleScreenStateChanged(screenOn);
startConnectivityScan(SCAN_ON_SCHEDULE);
}
@@ -1090,6 +1113,7 @@ public class WifiConnectivityManager {
if (mWifiState == WIFI_STATE_CONNECTED) {
mOpenNetworkNotifier.handleWifiConnected();
+ mCarrierNetworkNotifier.handleWifiConnected();
}
// Reset BSSID of last connection attempt and kick off
@@ -1111,6 +1135,7 @@ public class WifiConnectivityManager {
public void handleConnectionAttemptEnded(int failureCode) {
if (failureCode != WifiMetrics.ConnectionEvent.FAILURE_NONE) {
mOpenNetworkNotifier.handleConnectionFailure();
+ mCarrierNetworkNotifier.handleConnectionFailure();
}
}
@@ -1337,6 +1362,7 @@ public class WifiConnectivityManager {
clearBssidBlacklist();
resetLastPeriodicSingleScanTimeStamp();
mOpenNetworkNotifier.clearPendingNotification(true /* resetRepeatDelay */);
+ mCarrierNetworkNotifier.clearPendingNotification(true /* resetRepeatDelay */);
mLastConnectionAttemptBssid = null;
mWaitForFullBandScanResults = false;
}
@@ -1397,5 +1423,6 @@ public class WifiConnectivityManager {
mLocalLog.dump(fd, pw, args);
pw.println("WifiConnectivityManager - Log End ----");
mOpenNetworkNotifier.dump(fd, pw, args);
+ mCarrierNetworkNotifier.dump(fd, pw, args);
}
}
diff --git a/com/android/server/wifi/WifiController.java b/com/android/server/wifi/WifiController.java
index dc985955..9c90941f 100644
--- a/com/android/server/wifi/WifiController.java
+++ b/com/android/server/wifi/WifiController.java
@@ -16,36 +16,23 @@
package com.android.server.wifi;
-import static android.net.wifi.WifiManager.WIFI_MODE_FULL;
-import static android.net.wifi.WifiManager.WIFI_MODE_FULL_HIGH_PERF;
-import static android.net.wifi.WifiManager.WIFI_MODE_NO_LOCKS_HELD;
-import static android.net.wifi.WifiManager.WIFI_MODE_SCAN_ONLY;
-
-import android.app.AlarmManager;
-import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
-import android.database.ContentObserver;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.wifi.WifiManager;
-import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.os.WorkSource;
import android.provider.Settings;
-import android.util.Slog;
import com.android.internal.util.Protocol;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
-import java.io.FileDescriptor;
-import java.io.PrintWriter;
-
/**
* WifiController is the class used to manage on/off state of WifiStateMachine for various operating
* modes (normal, airplane, wifi hotspot, etc.).
@@ -54,27 +41,8 @@ public class WifiController extends StateMachine {
private static final String TAG = "WifiController";
private static final boolean DBG = false;
private Context mContext;
- private boolean mScreenOff;
- private boolean mDeviceIdle;
- private int mPluggedType;
- private int mStayAwakeConditions;
- private long mIdleMillis;
- private int mSleepPolicy;
private boolean mFirstUserSignOnSeen = false;
- private AlarmManager mAlarmManager;
- private PendingIntent mIdleIntent;
- private static final int IDLE_REQUEST = 0;
-
- /**
- * See {@link Settings.Global#WIFI_IDLE_MS}. This is the default value if a
- * Settings.Global value is not present. This timeout value is chosen as
- * the approximate point at which the battery drain caused by Wi-Fi
- * being enabled but not active exceeds the battery drain caused by
- * re-establishing a connection to the mobile data network.
- */
- private static final long DEFAULT_IDLE_MS = 15 * 60 * 1000; /* 15 minutes */
-
/**
* See {@link Settings.Global#WIFI_REENABLE_DELAY_MS}. This is the default value if a
* Settings.Global value is not present. This is the minimum time after wifi is disabled
@@ -83,19 +51,15 @@ public class WifiController extends StateMachine {
private static final long DEFAULT_REENABLE_DELAY_MS = 500;
// finding that delayed messages can sometimes be delivered earlier than expected
- // probably rounding errors.. add a margin to prevent problems
+ // probably rounding errors. add a margin to prevent problems
private static final long DEFER_MARGIN_MS = 5;
NetworkInfo mNetworkInfo = new NetworkInfo(ConnectivityManager.TYPE_WIFI, 0, "WIFI", "");
- private static final String ACTION_DEVICE_IDLE =
- "com.android.server.WifiManager.action.DEVICE_IDLE";
-
/* References to values tracked in WifiService */
private final WifiStateMachine mWifiStateMachine;
private final WifiStateMachinePrime mWifiStateMachinePrime;
private final WifiSettingsStore mSettingsStore;
- private final WifiLockManager mWifiLockManager;
/**
* Temporary for computing UIDS that are responsible for starting WIFI.
@@ -110,11 +74,6 @@ public class WifiController extends StateMachine {
private static final int BASE = Protocol.BASE_WIFI_CONTROLLER;
static final int CMD_EMERGENCY_MODE_CHANGED = BASE + 1;
- static final int CMD_SCREEN_ON = BASE + 2;
- static final int CMD_SCREEN_OFF = BASE + 3;
- static final int CMD_BATTERY_CHANGED = BASE + 4;
- static final int CMD_DEVICE_IDLE = BASE + 5;
- static final int CMD_LOCKS_CHANGED = BASE + 6;
static final int CMD_SCAN_ALWAYS_MODE_CHANGED = BASE + 7;
static final int CMD_WIFI_TOGGLED = BASE + 8;
static final int CMD_AIRPLANE_TOGGLED = BASE + 9;
@@ -136,37 +95,21 @@ public class WifiController extends StateMachine {
private StaDisabledWithScanState mStaDisabledWithScanState = new StaDisabledWithScanState();
private ApEnabledState mApEnabledState = new ApEnabledState();
private DeviceActiveState mDeviceActiveState = new DeviceActiveState();
- private DeviceInactiveState mDeviceInactiveState = new DeviceInactiveState();
- private ScanOnlyLockHeldState mScanOnlyLockHeldState = new ScanOnlyLockHeldState();
- private FullLockHeldState mFullLockHeldState = new FullLockHeldState();
- private FullHighPerfLockHeldState mFullHighPerfLockHeldState = new FullHighPerfLockHeldState();
- private NoLockHeldState mNoLockHeldState = new NoLockHeldState();
private EcmState mEcmState = new EcmState();
WifiController(Context context, WifiStateMachine wsm, WifiSettingsStore wss,
- WifiLockManager wifiLockManager, Looper looper, FrameworkFacade f,
- WifiStateMachinePrime wsmp) {
+ Looper looper, FrameworkFacade f, WifiStateMachinePrime wsmp) {
super(TAG, looper);
mFacade = f;
mContext = context;
mWifiStateMachine = wsm;
mWifiStateMachinePrime = wsmp;
mSettingsStore = wss;
- mWifiLockManager = wifiLockManager;
-
- mAlarmManager = (AlarmManager)mContext.getSystemService(Context.ALARM_SERVICE);
- Intent idleIntent = new Intent(ACTION_DEVICE_IDLE, null);
- mIdleIntent = mFacade.getBroadcast(mContext, IDLE_REQUEST, idleIntent, 0);
addState(mDefaultState);
addState(mApStaDisabledState, mDefaultState);
addState(mStaEnabledState, mDefaultState);
addState(mDeviceActiveState, mStaEnabledState);
- addState(mDeviceInactiveState, mStaEnabledState);
- addState(mScanOnlyLockHeldState, mDeviceInactiveState);
- addState(mFullLockHeldState, mDeviceInactiveState);
- addState(mFullHighPerfLockHeldState, mDeviceInactiveState);
- addState(mNoLockHeldState, mDeviceInactiveState);
addState(mStaDisabledWithScanState, mDefaultState);
addState(mApEnabledState, mDefaultState);
addState(mEcmState, mDefaultState);
@@ -189,7 +132,6 @@ public class WifiController extends StateMachine {
setLogOnlyTransitions(false);
IntentFilter filter = new IntentFilter();
- filter.addAction(ACTION_DEVICE_IDLE);
filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
filter.addAction(WifiManager.WIFI_AP_STATE_CHANGED_ACTION);
filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
@@ -198,9 +140,7 @@ public class WifiController extends StateMachine {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
- if (action.equals(ACTION_DEVICE_IDLE)) {
- sendMessage(CMD_DEVICE_IDLE);
- } else if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
+ if (action.equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
mNetworkInfo = (NetworkInfo) intent.getParcelableExtra(
WifiManager.EXTRA_NETWORK_INFO);
} else if (action.equals(WifiManager.WIFI_AP_STATE_CHANGED_ACTION)) {
@@ -226,127 +166,16 @@ public class WifiController extends StateMachine {
},
new IntentFilter(filter));
- initializeAndRegisterForSettingsChange(looper);
- }
-
- private void initializeAndRegisterForSettingsChange(Looper looper) {
- Handler handler = new Handler(looper);
- readStayAwakeConditions();
- registerForStayAwakeModeChange(handler);
- readWifiIdleTime();
- registerForWifiIdleTimeChange(handler);
- readWifiSleepPolicy();
- registerForWifiSleepPolicyChange(handler);
readWifiReEnableDelay();
}
- private void readStayAwakeConditions() {
- mStayAwakeConditions = mFacade.getIntegerSetting(mContext,
- Settings.Global.STAY_ON_WHILE_PLUGGED_IN, 0);
- }
-
- private void readWifiIdleTime() {
- mIdleMillis = mFacade.getLongSetting(mContext,
- Settings.Global.WIFI_IDLE_MS, DEFAULT_IDLE_MS);
- }
-
- private void readWifiSleepPolicy() {
- // This should always set to default value because the settings menu to toggle this
- // has been removed now.
- mSleepPolicy = Settings.Global.WIFI_SLEEP_POLICY_NEVER;
- }
-
private void readWifiReEnableDelay() {
mReEnableDelayMillis = mFacade.getLongSetting(mContext,
Settings.Global.WIFI_REENABLE_DELAY_MS, DEFAULT_REENABLE_DELAY_MS);
}
- /**
- * Observes settings changes to scan always mode.
- */
- private void registerForStayAwakeModeChange(Handler handler) {
- ContentObserver contentObserver = new ContentObserver(handler) {
- @Override
- public void onChange(boolean selfChange) {
- readStayAwakeConditions();
- }
- };
-
- mFacade.registerContentObserver(mContext,
- Settings.Global.getUriFor(Settings.Global.STAY_ON_WHILE_PLUGGED_IN), false,
- contentObserver);
- }
-
- /**
- * Observes settings changes to wifi idle time.
- */
- private void registerForWifiIdleTimeChange(Handler handler) {
- ContentObserver contentObserver = new ContentObserver(handler) {
- @Override
- public void onChange(boolean selfChange) {
- readWifiIdleTime();
- }
- };
-
- mFacade.registerContentObserver(mContext,
- Settings.Global.getUriFor(Settings.Global.WIFI_IDLE_MS), false, contentObserver);
- }
-
- /**
- * Observes changes to wifi sleep policy
- */
- private void registerForWifiSleepPolicyChange(Handler handler) {
- ContentObserver contentObserver = new ContentObserver(handler) {
- @Override
- public void onChange(boolean selfChange) {
- readWifiSleepPolicy();
- }
- };
- mFacade.registerContentObserver(mContext,
- Settings.Global.getUriFor(Settings.Global.WIFI_SLEEP_POLICY), false,
- contentObserver);
- }
-
- /**
- * Determines whether the Wi-Fi chipset should stay awake or be put to
- * sleep. Looks at the setting for the sleep policy and the current
- * conditions.
- *
- * @see #shouldDeviceStayAwake(int)
- */
- private boolean shouldWifiStayAwake(int pluggedType) {
- if (mSleepPolicy == Settings.Global.WIFI_SLEEP_POLICY_NEVER) {
- // Never sleep
- return true;
- } else if ((mSleepPolicy == Settings.Global.WIFI_SLEEP_POLICY_NEVER_WHILE_PLUGGED) &&
- (pluggedType != 0)) {
- // Never sleep while plugged, and we're plugged
- return true;
- } else {
- // Default
- return shouldDeviceStayAwake(pluggedType);
- }
- }
-
- /**
- * Determine whether the bit value corresponding to {@code pluggedType} is set in
- * the bit string mStayAwakeConditions. This determines whether the device should
- * stay awake based on the current plugged type.
- *
- * @param pluggedType the type of plug (USB, AC, or none) for which the check is
- * being made
- * @return {@code true} if {@code pluggedType} indicates that the device is
- * supposed to stay awake, {@code false} otherwise.
- */
- private boolean shouldDeviceStayAwake(int pluggedType) {
- return (mStayAwakeConditions & pluggedType) != 0;
- }
-
private void updateBatteryWorkSource() {
mTmpWorkSource.clear();
- if (mDeviceIdle) {
- mTmpWorkSource.add(mWifiLockManager.createMergedWorkSource());
- }
mWifiStateMachine.updateBatteryWorkSource(mTmpWorkSource);
}
@@ -354,58 +183,8 @@ public class WifiController extends StateMachine {
@Override
public boolean processMessage(Message msg) {
switch (msg.what) {
- case CMD_SCREEN_ON:
- mAlarmManager.cancel(mIdleIntent);
- mScreenOff = false;
- mDeviceIdle = false;
- updateBatteryWorkSource();
- break;
- case CMD_SCREEN_OFF:
- mScreenOff = true;
- /*
- * Set a timer to put Wi-Fi to sleep, but only if the screen is off
- * AND the "stay on while plugged in" setting doesn't match the
- * current power conditions (i.e, not plugged in, plugged in to USB,
- * or plugged in to AC).
- */
- if (!shouldWifiStayAwake(mPluggedType)) {
- //Delayed shutdown if wifi is connected
- if (mNetworkInfo.getDetailedState() ==
- NetworkInfo.DetailedState.CONNECTED) {
- if (DBG) Slog.d(TAG, "set idle timer: " + mIdleMillis + " ms");
- mAlarmManager.set(AlarmManager.RTC_WAKEUP,
- System.currentTimeMillis() + mIdleMillis, mIdleIntent);
- } else {
- sendMessage(CMD_DEVICE_IDLE);
- }
- }
- break;
- case CMD_DEVICE_IDLE:
- mDeviceIdle = true;
- updateBatteryWorkSource();
- break;
- case CMD_BATTERY_CHANGED:
- /*
- * Set a timer to put Wi-Fi to sleep, but only if the screen is off
- * AND we are transitioning from a state in which the device was supposed
- * to stay awake to a state in which it is not supposed to stay awake.
- * If "stay awake" state is not changing, we do nothing, to avoid resetting
- * the already-set timer.
- */
- int pluggedType = msg.arg1;
- if (DBG) Slog.d(TAG, "battery changed pluggedType: " + pluggedType);
- if (mScreenOff && shouldWifiStayAwake(mPluggedType) &&
- !shouldWifiStayAwake(pluggedType)) {
- long triggerTime = System.currentTimeMillis() + mIdleMillis;
- if (DBG) Slog.d(TAG, "set idle timer for " + mIdleMillis + "ms");
- mAlarmManager.set(AlarmManager.RTC_WAKEUP, triggerTime, mIdleIntent);
- }
-
- mPluggedType = pluggedType;
- break;
case CMD_SET_AP:
case CMD_SCAN_ALWAYS_MODE_CHANGED:
- case CMD_LOCKS_CHANGED:
case CMD_WIFI_TOGGLED:
case CMD_AIRPLANE_TOGGLED:
case CMD_EMERGENCY_MODE_CHANGED:
@@ -437,7 +216,7 @@ public class WifiController extends StateMachine {
@Override
public void enter() {
- mWifiStateMachine.setSupplicantRunning(false);
+ mWifiStateMachine.setOperationalMode(WifiStateMachine.DISABLED_MODE);
mWifiStateMachinePrime.disableWifi();
// Supplicant can't restart right away, so note the time we switched off
mDisabledTimestamp = SystemClock.elapsedRealtime();
@@ -459,16 +238,12 @@ public class WifiController extends StateMachine {
mHaveDeferredEnable = !mHaveDeferredEnable;
break;
}
- if (mDeviceIdle == false) {
- // wifi is toggled, we need to explicitly tell WifiStateMachine that we
- // are headed to connect mode before going to the DeviceActiveState
- // since that will start supplicant and WifiStateMachine may not know
- // what state to head to (it might go to scan mode).
- mWifiStateMachine.setOperationalMode(WifiStateMachine.CONNECT_MODE);
- transitionTo(mDeviceActiveState);
- } else {
- checkLocksAndTransitionWhenDeviceIdle();
- }
+ // wifi is toggled, we need to explicitly tell WifiStateMachine that we
+ // are headed to connect mode before going to the DeviceActiveState
+ // since that will start supplicant and WifiStateMachine may not know
+ // what state to head to (it might go to scan mode).
+ mWifiStateMachine.setOperationalMode(WifiStateMachine.CONNECT_MODE);
+ transitionTo(mDeviceActiveState);
} else if (mSettingsStore.isScanAlwaysAvailable()) {
transitionTo(mStaDisabledWithScanState);
}
@@ -476,7 +251,9 @@ public class WifiController extends StateMachine {
case CMD_SCAN_ALWAYS_MODE_CHANGED:
if (mSettingsStore.isScanAlwaysAvailable()) {
transitionTo(mStaDisabledWithScanState);
+ break;
}
+ mWifiStateMachine.setOperationalMode(WifiStateMachine.DISABLED_MODE);
break;
case CMD_SET_AP:
if (msg.arg1 == 1) {
@@ -528,8 +305,9 @@ public class WifiController extends StateMachine {
class StaEnabledState extends State {
@Override
public void enter() {
- mWifiStateMachine.setSupplicantRunning(true);
+ log("StaEnabledState.enter()");
}
+
@Override
public boolean processMessage(Message msg) {
switch (msg.what) {
@@ -594,15 +372,17 @@ public class WifiController extends StateMachine {
@Override
public void enter() {
- // need to set the mode before starting supplicant because WSM will assume we are going
- // in to client mode
+ // first send the message to WSM to trigger the transition and act as a shadow
mWifiStateMachine.setOperationalMode(WifiStateMachine.SCAN_ONLY_WITH_WIFI_OFF_MODE);
- mWifiStateMachine.setSupplicantRunning(true);
+
+ // now trigger the actual mode switch in WifiStateMachinePrime
+ mWifiStateMachinePrime.enterScanOnlyMode();
+
+ // TODO b/71559473: remove the defered enable after mode management changes are complete
// Supplicant can't restart right away, so not the time we switched off
mDisabledTimestamp = SystemClock.elapsedRealtime();
mDeferredEnableSerialNumber++;
mHaveDeferredEnable = false;
- mWifiStateMachine.clearANQPCache();
}
@Override
@@ -618,11 +398,9 @@ public class WifiController extends StateMachine {
mHaveDeferredEnable = !mHaveDeferredEnable;
break;
}
- if (mDeviceIdle == false) {
- transitionTo(mDeviceActiveState);
- } else {
- checkLocksAndTransitionWhenDeviceIdle();
- }
+ // transition from scan mode to initial state in WifiStateMachine
+ mWifiStateMachine.setOperationalMode(WifiStateMachine.DISABLED_MODE);
+ transitionTo(mDeviceActiveState);
}
break;
case CMD_AIRPLANE_TOGGLED:
@@ -633,6 +411,7 @@ public class WifiController extends StateMachine {
break;
case CMD_SCAN_ALWAYS_MODE_CHANGED:
if (! mSettingsStore.isScanAlwaysAvailable()) {
+ log("StaDisabledWithScanState: scan no longer available");
transitionTo(mApStaDisabledState);
}
break;
@@ -744,13 +523,7 @@ public class WifiController extends StateMachine {
*/
mPendingState = getNextWifiState();
}
- if (mPendingState == mDeviceActiveState && mDeviceIdle) {
- checkLocksAndTransitionWhenDeviceIdle();
- } else {
- // go ahead and transition because we are not idle or we are not going
- // to the active state.
- transitionTo(mPendingState);
- }
+ transitionTo(mPendingState);
break;
case CMD_EMERGENCY_CALL_STATE_CHANGED:
case CMD_EMERGENCY_MODE_CHANGED:
@@ -779,7 +552,8 @@ public class WifiController extends StateMachine {
private int mEcmEntryCount;
@Override
public void enter() {
- mWifiStateMachine.setSupplicantRunning(false);
+ mWifiStateMachine.setOperationalMode(WifiStateMachine.DISABLED_MODE);
+ mWifiStateMachinePrime.disableWifi();
mWifiStateMachine.clearANQPCache();
mEcmEntryCount = 1;
}
@@ -822,11 +596,7 @@ public class WifiController extends StateMachine {
if (exitEcm) {
if (mSettingsStore.isWifiToggleEnabled()) {
- if (mDeviceIdle == false) {
- transitionTo(mDeviceActiveState);
- } else {
- checkLocksAndTransitionWhenDeviceIdle();
- }
+ transitionTo(mDeviceActiveState);
} else if (mSettingsStore.isScanAlwaysAvailable()) {
transitionTo(mStaDisabledWithScanState);
} else {
@@ -841,15 +611,13 @@ public class WifiController extends StateMachine {
@Override
public void enter() {
mWifiStateMachine.setOperationalMode(WifiStateMachine.CONNECT_MODE);
+ mWifiStateMachinePrime.enterClientMode();
mWifiStateMachine.setHighPerfModeEnabled(false);
}
@Override
public boolean processMessage(Message msg) {
- if (msg.what == CMD_DEVICE_IDLE) {
- checkLocksAndTransitionWhenDeviceIdle();
- // We let default state handle the rest of work
- } else if (msg.what == CMD_USER_PRESENT) {
+ if (msg.what == CMD_USER_PRESENT) {
// TLS networks can't connect until user unlocks keystore. KeyStore
// unlocks when the user punches PIN after the reboot. So use this
// trigger to get those networks connected.
@@ -859,6 +627,9 @@ public class WifiController extends StateMachine {
mFirstUserSignOnSeen = true;
return HANDLED;
} else if (msg.what == CMD_RESTART_WIFI) {
+ mWifiStateMachine.getHandler().post(() -> {
+ mWifiStateMachine.takeBugReport();
+ });
deferMessage(obtainMessage(CMD_RESTART_WIFI_CONTINUE));
transitionTo(mApStaDisabledState);
return HANDLED;
@@ -866,89 +637,4 @@ public class WifiController extends StateMachine {
return NOT_HANDLED;
}
}
-
- /* Parent: StaEnabledState */
- class DeviceInactiveState extends State {
- @Override
- public boolean processMessage(Message msg) {
- switch (msg.what) {
- case CMD_LOCKS_CHANGED:
- checkLocksAndTransitionWhenDeviceIdle();
- updateBatteryWorkSource();
- return HANDLED;
- case CMD_SCREEN_ON:
- transitionTo(mDeviceActiveState);
- // More work in default state
- return NOT_HANDLED;
- default:
- return NOT_HANDLED;
- }
- }
- }
-
- /* Parent: DeviceInactiveState. Device is inactive, but an app is holding a scan only lock. */
- class ScanOnlyLockHeldState extends State {
- @Override
- public void enter() {
- mWifiStateMachine.setOperationalMode(WifiStateMachine.SCAN_ONLY_MODE);
- }
- }
-
- /* Parent: DeviceInactiveState. Device is inactive, but an app is holding a full lock. */
- class FullLockHeldState extends State {
- @Override
- public void enter() {
- mWifiStateMachine.setOperationalMode(WifiStateMachine.CONNECT_MODE);
- mWifiStateMachine.setHighPerfModeEnabled(false);
- }
- }
-
- /* Parent: DeviceInactiveState. Device is inactive, but an app is holding a high perf lock. */
- class FullHighPerfLockHeldState extends State {
- @Override
- public void enter() {
- mWifiStateMachine.setOperationalMode(WifiStateMachine.CONNECT_MODE);
- mWifiStateMachine.setHighPerfModeEnabled(true);
- }
- }
-
- /* Parent: DeviceInactiveState. Device is inactive and no app is holding a wifi lock. */
- class NoLockHeldState extends State {
- @Override
- public void enter() {
- mWifiStateMachine.setOperationalMode(WifiStateMachine.DISABLED_MODE);
- }
- }
-
- private void checkLocksAndTransitionWhenDeviceIdle() {
- switch (mWifiLockManager.getStrongestLockMode()) {
- case WIFI_MODE_NO_LOCKS_HELD:
- if (mSettingsStore.isScanAlwaysAvailable()) {
- transitionTo(mScanOnlyLockHeldState);
- } else {
- transitionTo(mNoLockHeldState);
- }
- break;
- case WIFI_MODE_FULL:
- transitionTo(mFullLockHeldState);
- break;
- case WIFI_MODE_FULL_HIGH_PERF:
- transitionTo(mFullHighPerfLockHeldState);
- break;
- case WIFI_MODE_SCAN_ONLY:
- transitionTo(mScanOnlyLockHeldState);
- break;
- }
- }
-
- @Override
- public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- super.dump(fd, pw, args);
-
- pw.println("mScreenOff " + mScreenOff);
- pw.println("mDeviceIdle " + mDeviceIdle);
- pw.println("mPluggedType " + mPluggedType);
- pw.println("mIdleMillis " + mIdleMillis);
- pw.println("mSleepPolicy " + mSleepPolicy);
- }
}
diff --git a/com/android/server/wifi/WifiCountryCode.java b/com/android/server/wifi/WifiCountryCode.java
index a9cbed60..6f61d474 100644
--- a/com/android/server/wifi/WifiCountryCode.java
+++ b/com/android/server/wifi/WifiCountryCode.java
@@ -213,7 +213,7 @@ public class WifiCountryCode {
}
private boolean setCountryCodeNative(String country) {
- if (mWifiNative.setCountryCode(country)) {
+ if (mWifiNative.setCountryCode(mWifiNative.getClientInterfaceName(), country)) {
Log.d(TAG, "Succeeded to set country code to: " + country);
mCurrentCountryCode = country;
return true;
diff --git a/com/android/server/wifi/WifiDiagnostics.java b/com/android/server/wifi/WifiDiagnostics.java
index 30294bda..df905d31 100644
--- a/com/android/server/wifi/WifiDiagnostics.java
+++ b/com/android/server/wifi/WifiDiagnostics.java
@@ -16,6 +16,7 @@
package com.android.server.wifi;
+import android.app.ActivityManager;
import android.content.Context;
import android.util.Base64;
@@ -108,6 +109,7 @@ class WifiDiagnostics extends BaseWifiDiagnostics {
private final LastMileLogger mLastMileLogger;
private final Runtime mJavaRuntime;
private int mMaxRingBufferSizeBytes;
+ private WifiInjector mWifiInjector;
public WifiDiagnostics(Context context, WifiInjector wifiInjector,
WifiStateMachine wifiStateMachine, WifiNative wifiNative,
@@ -125,6 +127,7 @@ class WifiDiagnostics extends BaseWifiDiagnostics {
mLog = wifiInjector.makeLog(TAG);
mLastMileLogger = lastMileLogger;
mJavaRuntime = wifiInjector.getJavaRuntime();
+ mWifiInjector = wifiInjector;
}
@Override
@@ -158,7 +161,7 @@ class WifiDiagnostics extends BaseWifiDiagnostics {
startLoggingAllExceptPerPacketBuffers();
}
- if (!mWifiNative.startPktFateMonitoring()) {
+ if (!mWifiNative.startPktFateMonitoring(mWifiNative.getClientInterfaceName())) {
mLog.wC("Failed to start packet fate monitoring");
}
}
@@ -245,6 +248,23 @@ class WifiDiagnostics extends BaseWifiDiagnostics {
pw.println("--------------------------------------------------------------------");
}
+ @Override
+ /**
+ * Initiates a system-level bugreport, in a non-blocking fashion.
+ */
+ public void takeBugReport() {
+ if (mBuildProperties.isUserBuild()) {
+ return;
+ }
+
+ try {
+ mWifiInjector.getActivityManagerService().requestBugReport(
+ ActivityManager.BUGREPORT_OPTION_WIFI);
+ } catch (Exception e) { // diagnostics should never crash system_server
+ mLog.err("error taking bugreport: %").c(e.getClass().getName()).flush();
+ }
+ }
+
/* private methods and data */
class BugReport {
long systemTimeMs;
@@ -622,7 +642,7 @@ class WifiDiagnostics extends BaseWifiDiagnostics {
ArrayList<WifiNative.FateReport> mergedFates = new ArrayList<WifiNative.FateReport>();
WifiNative.TxFateReport[] txFates =
new WifiNative.TxFateReport[WifiLoggerHal.MAX_FATE_LOG_LEN];
- if (mWifiNative.getTxPktFates(txFates)) {
+ if (mWifiNative.getTxPktFates(mWifiNative.getClientInterfaceName(), txFates)) {
for (int i = 0; i < txFates.length && txFates[i] != null; i++) {
mergedFates.add(txFates[i]);
}
@@ -630,7 +650,7 @@ class WifiDiagnostics extends BaseWifiDiagnostics {
WifiNative.RxFateReport[] rxFates =
new WifiNative.RxFateReport[WifiLoggerHal.MAX_FATE_LOG_LEN];
- if (mWifiNative.getRxPktFates(rxFates)) {
+ if (mWifiNative.getRxPktFates(mWifiNative.getClientInterfaceName(), rxFates)) {
for (int i = 0; i < rxFates.length && rxFates[i] != null; i++) {
mergedFates.add(rxFates[i]);
}
diff --git a/com/android/server/wifi/WifiInjector.java b/com/android/server/wifi/WifiInjector.java
index c2806d88..2a556757 100644
--- a/com/android/server/wifi/WifiInjector.java
+++ b/com/android/server/wifi/WifiInjector.java
@@ -24,9 +24,11 @@ import android.net.NetworkScoreManager;
import android.net.wifi.IWifiScanner;
import android.net.wifi.IWificond;
import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
import android.net.wifi.WifiNetworkScoreCache;
import android.net.wifi.WifiScanner;
import android.os.BatteryStats;
+import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.INetworkManagementService;
@@ -40,10 +42,13 @@ import android.util.LocalLog;
import com.android.internal.R;
import com.android.internal.app.IBatteryStats;
+import com.android.internal.os.PowerProfile;
+import com.android.server.am.ActivityManagerService;
import com.android.server.am.BatteryStatsService;
import com.android.server.net.DelayedDiskWrite;
import com.android.server.net.IpConfigStore;
import com.android.server.wifi.aware.WifiAwareMetrics;
+import com.android.server.wifi.hotspot2.LegacyPasspointConfigParser;
import com.android.server.wifi.hotspot2.PasspointManager;
import com.android.server.wifi.hotspot2.PasspointNetworkEvaluator;
import com.android.server.wifi.hotspot2.PasspointObjectFactory;
@@ -81,11 +86,14 @@ public class WifiInjector {
private final WifiP2pMonitor mWifiP2pMonitor;
private final SupplicantStaIfaceHal mSupplicantStaIfaceHal;
private final SupplicantP2pIfaceHal mSupplicantP2pIfaceHal;
+ private final HostapdHal mHostapdHal;
private final WifiVendorHal mWifiVendorHal;
private final WifiStateMachine mWifiStateMachine;
private final WifiStateMachinePrime mWifiStateMachinePrime;
private final WifiSettingsStore mSettingsStore;
private final OpenNetworkNotifier mOpenNetworkNotifier;
+ private final CarrierNetworkNotifier mCarrierNetworkNotifier;
+ private final CarrierNetworkConfig mCarrierNetworkConfig;
private final WifiLockManager mLockManager;
private final WifiController mWifiController;
private final WificondControl mWificondControl;
@@ -99,7 +107,9 @@ public class WifiInjector {
private final WifiMulticastLockManager mWifiMulticastLockManager;
private final WifiConfigStore mWifiConfigStore;
private final WifiKeyStore mWifiKeyStore;
+ private final WifiNetworkHistory mWifiNetworkHistory;
private final IpConfigStore mIpConfigStore;
+ private final WifiConfigStoreLegacy mWifiConfigStoreLegacy;
private final WifiConfigManager mWifiConfigManager;
private final WifiConnectivityHelper mWifiConnectivityHelper;
private final LocalLog mConnectivityLocalLog;
@@ -123,6 +133,7 @@ public class WifiInjector {
private final SelfRecovery mSelfRecovery;
private final WakeupController mWakeupController;
private final INetworkManagementService mNwManagementService;
+ private final ScanRequestProxy mScanRequestProxy;
private final boolean mUseRealLogger;
@@ -149,11 +160,12 @@ public class WifiInjector {
mNetworkScoreManager.registerNetworkScoreCache(NetworkKey.TYPE_WIFI,
mWifiNetworkScoreCache, NetworkScoreManager.CACHE_FILTER_NONE);
mWifiPermissionsUtil = new WifiPermissionsUtil(mWifiPermissionsWrapper, mContext,
- mSettingsStore, UserManager.get(mContext), mNetworkScoreManager, this);
+ mSettingsStore, UserManager.get(mContext), this);
mWifiBackupRestore = new WifiBackupRestore(mWifiPermissionsUtil);
mBatteryStats = IBatteryStats.Stub.asInterface(mFrameworkFacade.getService(
BatteryStats.SERVICE_NAME));
mWifiStateTracker = new WifiStateTracker(mBatteryStats);
+ mCarrierNetworkConfig = new CarrierNetworkConfig(mContext);
// Now create and start handler threads
mWifiServiceHandlerThread = new HandlerThread("WifiService");
mWifiServiceHandlerThread.start();
@@ -168,20 +180,21 @@ public class WifiInjector {
mWifiVendorHal =
new WifiVendorHal(mHalDeviceManager, mWifiStateMachineHandlerThread.getLooper());
mSupplicantStaIfaceHal = new SupplicantStaIfaceHal(mContext, mWifiMonitor);
- mWificondControl = new WificondControl(this, mWifiMonitor,
- new CarrierNetworkConfig(mContext));
+ mHostapdHal = new HostapdHal(mContext);
+ mWificondControl = new WificondControl(this, mWifiMonitor, mCarrierNetworkConfig);
mNwManagementService = INetworkManagementService.Stub.asInterface(
ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE));
- mWifiNative = new WifiNative(SystemProperties.get("wifi.interface", "wlan0"),
- mWifiVendorHal, mSupplicantStaIfaceHal, mWificondControl, mNwManagementService);
+ mWifiNative = new WifiNative(
+ mWifiVendorHal, mSupplicantStaIfaceHal, mHostapdHal, mWificondControl,
+ mNwManagementService, mPropertyService, mWifiMetrics);
mWifiP2pMonitor = new WifiP2pMonitor(this);
mSupplicantP2pIfaceHal = new SupplicantP2pIfaceHal(mWifiP2pMonitor);
mWifiP2pNative = new WifiP2pNative(SystemProperties.get("wifi.direct.interface", "p2p0"),
mSupplicantP2pIfaceHal);
// Now get instances of all the objects that depend on the HandlerThreads
- mTrafficPoller = new WifiTrafficPoller(mContext, mWifiServiceHandlerThread.getLooper(),
- mWifiNative.getInterfaceName());
+ mTrafficPoller = new WifiTrafficPoller(mContext, mWifiServiceHandlerThread.getLooper(),
+ mWifiNative);
mCountryCode = new WifiCountryCode(mWifiNative,
SystemProperties.get(BOOT_DEFAULT_WIFI_COUNTRY_CODE),
mContext.getResources()
@@ -196,11 +209,15 @@ public class WifiInjector {
WifiConfigStore.createSharedFile());
// Legacy config store
DelayedDiskWrite writer = new DelayedDiskWrite();
+ mWifiNetworkHistory = new WifiNetworkHistory(mContext, writer);
mIpConfigStore = new IpConfigStore(writer);
+ mWifiConfigStoreLegacy = new WifiConfigStoreLegacy(
+ mWifiNetworkHistory, mWifiNative, mIpConfigStore,
+ new LegacyPasspointConfigParser());
// Config Manager
mWifiConfigManager = new WifiConfigManager(mContext, mClock,
UserManager.get(mContext), TelephonyManager.from(mContext),
- mWifiKeyStore, mWifiConfigStore, mWifiPermissionsUtil,
+ mWifiKeyStore, mWifiConfigStore, mWifiConfigStoreLegacy, mWifiPermissionsUtil,
mWifiPermissionsWrapper, new NetworkListStoreData(mContext),
new DeletedEphemeralSsidsStoreData());
mWifiMetrics.setWifiConfigManager(mWifiConfigManager);
@@ -221,6 +238,8 @@ public class WifiInjector {
mPasspointNetworkEvaluator = new PasspointNetworkEvaluator(
mPasspointManager, mWifiConfigManager, mConnectivityLocalLog);
mWifiMetrics.setPasspointManager(mPasspointManager);
+ mScanRequestProxy = new ScanRequestProxy(mContext, this, mWifiConfigManager,
+ mWifiPermissionsUtil);
// mWifiStateMachine has an implicit dependency on mJavaRuntime due to WifiDiagnostics.
mJavaRuntime = Runtime.getRuntime();
mWifiStateMachine = new WifiStateMachine(mContext, mFrameworkFacade,
@@ -228,22 +247,29 @@ public class WifiInjector {
this, mBackupManagerProxy, mCountryCode, mWifiNative,
new WrongPasswordNotifier(mContext, mFrameworkFacade));
IBinder b = mFrameworkFacade.getService(Context.NETWORKMANAGEMENT_SERVICE);
- INetworkManagementService networkManagementService =
- INetworkManagementService.Stub.asInterface(b);
mWifiStateMachinePrime = new WifiStateMachinePrime(this, wifiStateMachineLooper,
- mWifiNative, networkManagementService);
+ mWifiNative);
mOpenNetworkNotifier = new OpenNetworkNotifier(mContext,
mWifiStateMachineHandlerThread.getLooper(), mFrameworkFacade, mClock, mWifiMetrics,
mWifiConfigManager, mWifiConfigStore, mWifiStateMachine,
- new OpenNetworkRecommender(),
new ConnectToNetworkNotificationBuilder(mContext, mFrameworkFacade));
+ mCarrierNetworkNotifier = new CarrierNetworkNotifier(mContext,
+ mWifiStateMachineHandlerThread.getLooper(), mFrameworkFacade, mClock, mWifiMetrics,
+ mWifiConfigManager, mWifiConfigStore, mWifiStateMachine,
+ new ConnectToNetworkNotificationBuilder(mContext, mFrameworkFacade));
+
+ WakeupNotificationFactory wakeupNotificationFactory =
+ new WakeupNotificationFactory(mContext, mFrameworkFacade);
+ WakeupOnboarding wakeupOnboarding = new WakeupOnboarding(mContext, mWifiConfigManager,
+ mWifiStateMachineHandlerThread.getLooper(), mFrameworkFacade,
+ wakeupNotificationFactory);
mWakeupController = new WakeupController(mContext,
mWifiStateMachineHandlerThread.getLooper(), new WakeupLock(mWifiConfigManager),
- mWifiConfigManager, mWifiConfigStore, this, mFrameworkFacade);
+ WakeupEvaluator.fromContext(mContext), wakeupOnboarding, mWifiConfigManager,
+ mWifiConfigStore, this, mFrameworkFacade);
mLockManager = new WifiLockManager(mContext, BatteryStatsService.getService());
mWifiController = new WifiController(mContext, mWifiStateMachine, mSettingsStore,
- mLockManager, mWifiServiceHandlerThread.getLooper(), mFrameworkFacade,
- mWifiStateMachinePrime);
+ mWifiServiceHandlerThread.getLooper(), mFrameworkFacade, mWifiStateMachinePrime);
mSelfRecovery = new SelfRecovery(mWifiController, mClock);
mWifiLastResortWatchdog = new WifiLastResortWatchdog(mSelfRecovery, mWifiMetrics);
mWifiMulticastLockManager = new WifiMulticastLockManager(mWifiStateMachine,
@@ -272,6 +298,7 @@ public class WifiInjector {
mWifiLastResortWatchdog.enableVerboseLogging(verbose);
mWifiBackupRestore.enableVerboseLogging(verbose);
mHalDeviceManager.enableVerboseLogging(verbose);
+ mScanRequestProxy.enableVerboseLogging(verbose);
LogcatLog.enableVerboseLogging(verbose);
}
@@ -319,6 +346,10 @@ public class WifiInjector {
return mWifiStateMachine;
}
+ public Handler getWifiStateMachineHandler() {
+ return mWifiStateMachine.getHandler();
+ }
+
public WifiStateMachinePrime getWifiStateMachinePrime() {
return mWifiStateMachinePrime;
}
@@ -392,18 +423,26 @@ public class WifiInjector {
/**
* Create a SoftApManager.
- * @param nmService NetworkManagementService allowing SoftApManager to listen for interface
- * changes
* @param listener listener for SoftApManager
* @param config SoftApModeConfiguration object holding the config and mode
* @return an instance of SoftApManager
*/
- public SoftApManager makeSoftApManager(INetworkManagementService nmService,
- SoftApManager.Listener listener,
+ public SoftApManager makeSoftApManager(WifiManager.SoftApCallback callback,
@NonNull SoftApModeConfiguration config) {
return new SoftApManager(mContext, mWifiStateMachineHandlerThread.getLooper(),
- mFrameworkFacade, mWifiNative, mCountryCode.getCountryCode(), listener,
- nmService, mWifiApConfigStore, config, mWifiMetrics);
+ mFrameworkFacade, mWifiNative, mCountryCode.getCountryCode(), callback,
+ mWifiApConfigStore, config, mWifiMetrics);
+ }
+
+ /**
+ * Create a ScanOnlyModeManager
+ *
+ * @param listener listener for ScanOnlyModeManager state changes
+ * @return a new instance of ScanOnlyModeManager
+ */
+ public ScanOnlyModeManager makeScanOnlyModeManager(ScanOnlyModeManager.Listener listener) {
+ return new ScanOnlyModeManager(mContext, mWifiStateMachineHandlerThread.getLooper(),
+ mWifiNative, listener, mWifiMetrics, mScanRequestProxy, mWakeupController);
}
/**
@@ -451,10 +490,10 @@ public class WifiInjector {
boolean hasConnectionRequests) {
return new WifiConnectivityManager(mContext, mWifiStateMachine, getWifiScanner(),
mWifiConfigManager, wifiInfo, mWifiNetworkSelector, mWifiConnectivityHelper,
- mWifiLastResortWatchdog, mOpenNetworkNotifier, mWifiMetrics,
- mWifiStateMachineHandlerThread.getLooper(), mClock, mConnectivityLocalLog,
- hasConnectionRequests, mFrameworkFacade, mSavedNetworkEvaluator,
- mScoredNetworkEvaluator, mPasspointNetworkEvaluator);
+ mWifiLastResortWatchdog, mOpenNetworkNotifier, mCarrierNetworkNotifier,
+ mCarrierNetworkConfig, mWifiMetrics, mWifiStateMachineHandlerThread.getLooper(),
+ mClock, mConnectivityLocalLog, hasConnectionRequests, mFrameworkFacade,
+ mSavedNetworkEvaluator, mScoredNetworkEvaluator, mPasspointNetworkEvaluator);
}
public WifiPermissionsUtil getWifiPermissionsUtil() {
@@ -521,4 +560,16 @@ public class WifiInjector {
public SelfRecovery getSelfRecovery() {
return mSelfRecovery;
}
+
+ public PowerProfile getPowerProfile() {
+ return new PowerProfile(mContext, false);
+ }
+
+ public ScanRequestProxy getScanRequestProxy() {
+ return mScanRequestProxy;
+ }
+
+ public ActivityManagerService getActivityManagerService() {
+ return (ActivityManagerService) ActivityManager.getService();
+ }
}
diff --git a/com/android/server/wifi/WifiLockManager.java b/com/android/server/wifi/WifiLockManager.java
index fe193f09..b1b3f112 100644
--- a/com/android/server/wifi/WifiLockManager.java
+++ b/com/android/server/wifi/WifiLockManager.java
@@ -71,7 +71,7 @@ public class WifiLockManager {
if (!isValidLockMode(lockMode)) {
throw new IllegalArgumentException("lockMode =" + lockMode);
}
- if (ws == null || ws.size() == 0) {
+ if (ws == null || ws.isEmpty()) {
ws = new WorkSource(Binder.getCallingUid());
} else {
mContext.enforceCallingOrSelfPermission(
@@ -144,18 +144,27 @@ public class WifiLockManager {
}
WorkSource newWorkSource;
- if (ws == null || ws.size() == 0) {
+ if (ws == null || ws.isEmpty()) {
newWorkSource = new WorkSource(Binder.getCallingUid());
} else {
// Make a copy of the WorkSource before adding it to the WakeLock
newWorkSource = new WorkSource(ws);
}
+ if (mVerboseLoggingEnabled) {
+ Slog.d(TAG, "updateWifiLockWakeSource: " + wl + ", newWorkSource=" + newWorkSource);
+ }
+
long ident = Binder.clearCallingIdentity();
try {
+ // Log the acquire before the release to avoid "holes" in the collected data due to
+ // an acquire event immediately after a release in the case where newWorkSource and
+ // wl.mWorkSource share one or more attribution UIDs. BatteryStats can correctly match
+ // "nested" acquire / release pairs.
+ mBatteryStats.noteFullWifiLockAcquiredFromSource(newWorkSource);
mBatteryStats.noteFullWifiLockReleasedFromSource(wl.mWorkSource);
+
wl.mWorkSource = newWorkSource;
- mBatteryStats.noteFullWifiLockAcquiredFromSource(wl.mWorkSource);
} catch (RemoteException e) {
} finally {
Binder.restoreCallingIdentity(ident);
@@ -323,7 +332,8 @@ public class WifiLockManager {
}
public String toString() {
- return "WifiLock{" + this.mTag + " type=" + this.mMode + " uid=" + mUid + "}";
+ return "WifiLock{" + this.mTag + " type=" + this.mMode + " uid=" + mUid
+ + " workSource=" + mWorkSource + "}";
}
}
}
diff --git a/com/android/server/wifi/WifiMetrics.java b/com/android/server/wifi/WifiMetrics.java
index 4e277a1d..643b2989 100644
--- a/com/android/server/wifi/WifiMetrics.java
+++ b/com/android/server/wifi/WifiMetrics.java
@@ -1273,6 +1273,26 @@ public class WifiMetrics {
}
/**
+ * Increment number of times the supplicant crashed.
+ */
+ public void incrementNumSupplicantCrashes() {
+ synchronized (mLock) {
+ // TODO(b/71720421): Add metrics for supplicant crashes.
+ mWifiLogProto.numHalCrashes++;
+ }
+ }
+
+ /**
+ * Increment number of times the hostapd crashed.
+ */
+ public void incrementNumHostapdCrashes() {
+ synchronized (mLock) {
+ // TODO(b/71720421): Add metrics for hostapd crashes.
+ mWifiLogProto.numHalCrashes++;
+ }
+ }
+
+ /**
* Increment number of times the wifi on failed due to an error in HAL.
*/
public void incrementNumWifiOnFailureDueToHal() {
@@ -1291,6 +1311,16 @@ public class WifiMetrics {
}
/**
+ * Increment number of times the wifi on failed due to an error in wificond.
+ */
+ public void incrementNumWifiOnFailureDueToSupplicant() {
+ synchronized (mLock) {
+ // TODO(b/71720421): Add metrics for supplicant failure during startup.
+ mWifiLogProto.numWifiOnFailureDueToHal++;
+ }
+ }
+
+ /**
* Increment number of times Passpoint provider being installed.
*/
public void incrementNumPasspointProviderInstallation() {
@@ -1457,8 +1487,12 @@ public class WifiMetrics {
}
}
+ /**
+ * TODO: (b/72443859) Use notifierTag param to separate metrics for OpenNetworkNotifier and
+ * CarrierNetworkNotifier, for this method and all other related metrics.
+ */
/** Increments the occurence of a "Connect to Network" notification. */
- public void incrementConnectToNetworkNotification(int notificationType) {
+ public void incrementConnectToNetworkNotification(String notifierTag, int notificationType) {
synchronized (mLock) {
int count = mConnectToNetworkNotificationCount.get(notificationType);
mConnectToNetworkNotificationCount.put(notificationType, count + 1);
@@ -1466,7 +1500,8 @@ public class WifiMetrics {
}
/** Increments the occurence of an "Connect to Network" notification user action. */
- public void incrementConnectToNetworkNotificationAction(int notificationType, int actionType) {
+ public void incrementConnectToNetworkNotificationAction(String notifierTag,
+ int notificationType, int actionType) {
synchronized (mLock) {
int key = notificationType * CONNECT_TO_NETWORK_NOTIFICATION_ACTION_KEY_MULTIPLIER
+ actionType;
@@ -1479,28 +1514,28 @@ public class WifiMetrics {
* Sets the number of SSIDs blacklisted from recommendation by the open network notification
* recommender.
*/
- public void setOpenNetworkRecommenderBlacklistSize(int size) {
+ public void setNetworkRecommenderBlacklistSize(String notifierTag, int size) {
synchronized (mLock) {
mOpenNetworkRecommenderBlacklistSize = size;
}
}
/** Sets if the available network notification feature is enabled. */
- public void setIsWifiNetworksAvailableNotificationEnabled(boolean enabled) {
+ public void setIsWifiNetworksAvailableNotificationEnabled(String notifierTag, boolean enabled) {
synchronized (mLock) {
mIsWifiNetworksAvailableNotificationOn = enabled;
}
}
/** Increments the occurence of connection attempts that were initiated unsuccessfully */
- public void incrementNumOpenNetworkRecommendationUpdates() {
+ public void incrementNumNetworkRecommendationUpdates(String notifierTag) {
synchronized (mLock) {
mNumOpenNetworkRecommendationUpdates++;
}
}
/** Increments the occurence of connection attempts that were initiated unsuccessfully */
- public void incrementNumOpenNetworkConnectMessageFailedToSend() {
+ public void incrementNumNetworkConnectMessageFailedToSend(String notifierTag) {
synchronized (mLock) {
mNumOpenNetworkConnectMessageFailedToSend++;
}
diff --git a/com/android/server/wifi/WifiMonitor.java b/com/android/server/wifi/WifiMonitor.java
index b2fc56e2..673d08e6 100644
--- a/com/android/server/wifi/WifiMonitor.java
+++ b/com/android/server/wifi/WifiMonitor.java
@@ -164,56 +164,20 @@ public class WifiMonitor {
}
/**
- * Wait for wpa_supplicant's control interface to be ready.
- *
- * TODO: Add unit tests for these once we remove the legacy code.
- */
- private boolean ensureConnectedLocked() {
- if (mConnected) {
- return true;
- }
- if (mVerboseLoggingEnabled) Log.d(TAG, "connecting to supplicant");
- int connectTries = 0;
- while (true) {
- mConnected = mWifiInjector.getWifiNative().connectToSupplicant();
- if (mConnected) {
- return true;
- }
- if (connectTries++ < 50) {
- try {
- Thread.sleep(100);
- } catch (InterruptedException ignore) {
- }
- } else {
- return false;
- }
- }
- }
-
- /**
* Start Monitoring for wpa_supplicant events.
*
* @param iface Name of iface.
- * TODO: Add unit tests for these once we remove the legacy code.
*/
- public synchronized void startMonitoring(String iface, boolean isStaIface) {
- if (ensureConnectedLocked()) {
- setMonitoring(iface, true);
- broadcastSupplicantConnectionEvent(iface);
- } else {
- boolean originalMonitoring = isMonitoring(iface);
- setMonitoring(iface, true);
- broadcastSupplicantDisconnectionEvent(iface);
- setMonitoring(iface, originalMonitoring);
- Log.e(TAG, "startMonitoring(" + iface + ") failed!");
- }
+ public synchronized void startMonitoring(String iface) {
+ if (mVerboseLoggingEnabled) Log.d(TAG, "startMonitoring(" + iface + ")");
+ setMonitoring(iface, true);
+ broadcastSupplicantConnectionEvent(iface);
}
/**
* Stop Monitoring for wpa_supplicant events.
*
* @param iface Name of iface.
- * TODO: Add unit tests for these once we remove the legacy code.
*/
public synchronized void stopMonitoring(String iface) {
if (mVerboseLoggingEnabled) Log.d(TAG, "stopMonitoring(" + iface + ")");
diff --git a/com/android/server/wifi/WifiNative.java b/com/android/server/wifi/WifiNative.java
index c22b0a51..cbd35c83 100644
--- a/com/android/server/wifi/WifiNative.java
+++ b/com/android/server/wifi/WifiNative.java
@@ -20,9 +20,8 @@ import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.net.InterfaceConfiguration;
+import android.net.MacAddress;
import android.net.apf.ApfCapabilities;
-import android.net.wifi.IApInterface;
-import android.net.wifi.IClientInterface;
import android.net.wifi.RttManager;
import android.net.wifi.RttManager.ResponderConfig;
import android.net.wifi.ScanResult;
@@ -34,14 +33,13 @@ import android.os.RemoteException;
import android.os.SystemClock;
import android.text.TextUtils;
import android.util.Log;
-import android.util.Pair;
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 com.android.server.wifi.util.NativeUtil;
import java.io.PrintWriter;
import java.io.StringWriter;
@@ -70,28 +68,28 @@ import java.util.TimeZone;
* {@hide}
*/
public class WifiNative {
- private final String mTAG;
- private final String mInterfaceName;
+ private static final String TAG = "WifiNative";
private final SupplicantStaIfaceHal mSupplicantStaIfaceHal;
+ private final HostapdHal mHostapdHal;
private final WifiVendorHal mWifiVendorHal;
private final WificondControl mWificondControl;
private final INetworkManagementService mNwManagementService;
+ private final PropertyService mPropertyService;
+ private final WifiMetrics mWifiMetrics;
// 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,
- INetworkManagementService nwService) {
- mTAG = "WifiNative-" + interfaceName;
- mInterfaceName = interfaceName;
+ public WifiNative(WifiVendorHal vendorHal,
+ SupplicantStaIfaceHal staIfaceHal, HostapdHal hostapdHal,
+ WificondControl condControl, INetworkManagementService nwService,
+ PropertyService propertyService, WifiMetrics wifiMetrics) {
mWifiVendorHal = vendorHal;
mSupplicantStaIfaceHal = staIfaceHal;
+ mHostapdHal = hostapdHal;
mWificondControl = condControl;
mNwManagementService = nwService;
- }
-
- public String getInterfaceName() {
- return mInterfaceName;
+ mPropertyService = propertyService;
+ mWifiMetrics = wifiMetrics;
}
/**
@@ -103,75 +101,9 @@ public class WifiNative {
mWifiVendorHal.enableVerboseLogging(verbose > 0);
}
- /********************************************************
- * Native Initialization/Deinitialization
- ********************************************************/
- public static final int SETUP_SUCCESS = 0;
- public static final int SETUP_FAILURE_HAL = 1;
- public static final int SETUP_FAILURE_WIFICOND = 2;
-
- /**
- * Setup wifi native for Client mode operations.
- *
- * 1. Starts the Wifi HAL and configures it in client/STA mode.
- * 2. Setup Wificond to operate in client mode and retrieve the handle to use for client
- * operations.
- *
- * @return Pair of <Integer, IClientInterface> to indicate the status and the associated wificond
- * client interface binder handler (will be null on failure).
- */
- public Pair<Integer, IClientInterface> setupForClientMode(@NonNull String ifaceName) {
- if (!startHalIfNecessary(true)) {
- Log.e(mTAG, "Failed to start HAL for client mode");
- return Pair.create(SETUP_FAILURE_HAL, null);
- }
- IClientInterface iClientInterface = mWificondControl.setupInterfaceForClientMode(ifaceName);
- if (iClientInterface == null) {
- return Pair.create(SETUP_FAILURE_WIFICOND, null);
- }
- return Pair.create(SETUP_SUCCESS, iClientInterface);
- }
-
- /**
- * Setup wifi native for AP mode operations.
- *
- * 1. Starts the Wifi HAL and configures it in AP mode.
- * 2. Setup Wificond to operate in AP mode and retrieve the handle to use for ap operations.
- *
- * @return Pair of <Integer, IApInterface> to indicate the status and the associated wificond
- * AP interface binder handler (will be null on failure).
- */
- public Pair<Integer, IApInterface> setupForSoftApMode(@NonNull String ifaceName) {
- if (!startHalIfNecessary(false)) {
- Log.e(mTAG, "Failed to start HAL for AP mode");
- return Pair.create(SETUP_FAILURE_HAL, null);
- }
- IApInterface iApInterface = mWificondControl.setupInterfaceForSoftApMode(ifaceName);
- if (iApInterface == null) {
- return Pair.create(SETUP_FAILURE_WIFICOND, null);
- }
- return Pair.create(SETUP_SUCCESS, iApInterface);
- }
-
- /**
- * Teardown all mode configurations in wifi native.
- *
- * 1. Stops the Wifi HAL.
- * 2. Tears down all the interfaces from Wificond.
- */
- public void tearDown() {
- stopHalIfNecessary();
- if (!mWificondControl.tearDownInterfaces()) {
- // TODO(b/34859006): Handle failures.
- Log.e(mTAG, "Failed to teardown interfaces from Wificond");
- }
- }
-
- /**
- * 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.
- */
+ /********************************************************
+ * Interface management related methods.
+ ********************************************************/
/**
* Meta-info about every iface that is active.
*/
@@ -258,6 +190,16 @@ public class WifiNative {
return false;
}
+ /** Checks if there are any iface of the given type active. */
+ private Iface findAnyIfaceOfType(@Iface.IfaceType int type) {
+ for (Iface iface : mIfaces.values()) {
+ if (iface.type == type) {
+ return iface;
+ }
+ }
+ return null;
+ }
+
/** Checks if there are any STA iface active. */
private boolean hasAnyStaIface() {
return hasAnyIfaceOfType(Iface.IFACE_TYPE_STA);
@@ -267,6 +209,40 @@ public class WifiNative {
private boolean hasAnyApIface() {
return hasAnyIfaceOfType(Iface.IFACE_TYPE_AP);
}
+
+ private String findAnyStaIfaceName() {
+ Iface iface = findAnyIfaceOfType(Iface.IFACE_TYPE_STA);
+ if (iface == null) {
+ return null;
+ }
+ return iface.name;
+ }
+
+ private String findAnyApIfaceName() {
+ Iface iface = findAnyIfaceOfType(Iface.IFACE_TYPE_AP);
+ if (iface == null) {
+ return null;
+ }
+ return iface.name;
+ }
+
+ /** Removes the existing iface that does not match the provided id. */
+ public Iface removeExistingIface(int newIfaceId) {
+ Iface removedIface = null;
+ // The number of ifaces in the database could be 1 existing & 1 new at the max.
+ if (mIfaces.size() > 2) {
+ Log.wtf(TAG, "More than 1 existing interface found");
+ }
+ Iterator<Map.Entry<Integer, Iface>> iter = mIfaces.entrySet().iterator();
+ while (iter.hasNext()) {
+ Map.Entry<Integer, Iface> entry = iter.next();
+ if (entry.getKey() != newIfaceId) {
+ removedIface = entry.getValue();
+ iter.remove();
+ }
+ }
+ return removedIface;
+ }
}
private Object mLock = new Object();
@@ -277,9 +253,13 @@ public class WifiNative {
private boolean startHal() {
synchronized (mLock) {
if (!mIfaceMgr.hasAnyIface()) {
- if (!mWifiVendorHal.startVendorHal()) {
- Log.e(mTAG, "Failed to start vendor HAL");
- return false;
+ if (mWifiVendorHal.isVendorHalSupported()) {
+ if (!mWifiVendorHal.startVendorHal()) {
+ Log.e(TAG, "Failed to start vendor HAL");
+ return false;
+ }
+ } else {
+ Log.i(TAG, "Vendor Hal not supported, ignoring start.");
}
}
return true;
@@ -291,9 +271,13 @@ public class WifiNative {
synchronized (mLock) {
if (!mIfaceMgr.hasAnyIface()) {
if (!mWificondControl.tearDownInterfaces()) {
- Log.e(mTAG, "Failed to teardown ifaces from wificond");
+ Log.e(TAG, "Failed to teardown ifaces from wificond");
+ }
+ if (mWifiVendorHal.isVendorHalSupported()) {
+ mWifiVendorHal.stopVendorHal();
+ } else {
+ Log.i(TAG, "Vendor Hal not supported, ignoring stop.");
}
- mWifiVendorHal.stopVendorHal();
}
}
}
@@ -332,15 +316,16 @@ public class WifiNative {
synchronized (mLock) {
if (!mIfaceMgr.hasAnyStaIface()) {
if (!mWificondControl.enableSupplicant()) {
- Log.e(mTAG, "Failed to enable supplicant");
+ Log.e(TAG, "Failed to enable supplicant");
return false;
}
if (!waitForSupplicantConnection()) {
- Log.e(mTAG, "Failed to connect to supplicant");
+ Log.e(TAG, "Failed to connect to supplicant");
return false;
}
- if (!mSupplicantStaIfaceHal.registerDeathHandler(new DeathHandlerInternal())) {
- Log.e(mTAG, "Failed to register supplicant death handler");
+ if (!mSupplicantStaIfaceHal.registerDeathHandler(
+ new SupplicantDeathHandlerInternal())) {
+ Log.e(TAG, "Failed to register supplicant death handler");
return false;
}
}
@@ -353,10 +338,10 @@ public class WifiNative {
synchronized (mLock) {
if (!mIfaceMgr.hasAnyStaIface()) {
if (!mSupplicantStaIfaceHal.deregisterDeathHandler()) {
- Log.e(mTAG, "Failed to deregister supplicant death handler");
+ Log.e(TAG, "Failed to deregister supplicant death handler");
}
if (!mWificondControl.disableSupplicant()) {
- Log.e(mTAG, "Failed to disable supplicant");
+ Log.e(TAG, "Failed to disable supplicant");
}
}
}
@@ -386,13 +371,13 @@ public class WifiNative {
private void onClientInterfaceDestroyed(@NonNull Iface iface) {
synchronized (mLock) {
if (!unregisterNetworkObserver(iface.networkObserver)) {
- Log.e(mTAG, "Failed to unregister network observer for iface=" + iface.name);
+ Log.e(TAG, "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);
+ Log.e(TAG, "Failed to teardown iface in supplicant=" + iface.name);
}
if (!mWificondControl.tearDownClientInterface(iface.name)) {
- Log.e(mTAG, "Failed to teardown iface in wificond=" + iface.name);
+ Log.e(TAG, "Failed to teardown iface in wificond=" + iface.name);
}
stopSupplicantIfNecessary();
stopHalAndWificondIfNecessary();
@@ -403,13 +388,20 @@ public class WifiNative {
private void onSoftApInterfaceDestroyed(@NonNull Iface iface) {
synchronized (mLock) {
if (!unregisterNetworkObserver(iface.networkObserver)) {
- Log.e(mTAG, "Failed to unregister network observer for iface=" + iface.name);
+ Log.e(TAG, "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 (!mHostapdHal.removeAccessPoint(iface.name)) {
+ Log.e(TAG, "Failed to remove access point on iface=" + iface.name);
+ }
+ if (!mHostapdHal.deregisterDeathHandler()) {
+ Log.e(TAG, "Failed to deregister supplicant death handler");
+ }
+ // TODO(b/71513606): Move this to a global operation.
+ if (!mWificondControl.stopHostapd(iface.name)) {
+ Log.e(TAG, "Failed to stop hostapd on iface=" + iface.name);
}
if (!mWificondControl.tearDownSoftApInterface(iface.name)) {
- Log.e(mTAG, "Failed to teardown iface in wificond=" + iface.name);
+ Log.e(TAG, "Failed to teardown iface in wificond=" + iface.name);
}
stopHalAndWificondIfNecessary();
}
@@ -445,39 +437,89 @@ public class WifiNative {
synchronized (mLock) {
final Iface iface = mIfaceMgr.removeIface(mInterfaceId);
if (iface == null) {
- Log.e(mTAG, "Received iface destroyed notification on an invalid iface="
+ Log.e(TAG, "Received iface destroyed notification on an invalid iface="
+ ifaceName);
return;
}
onInterfaceDestroyed(iface);
- Log.i(mTAG, "Successfully torn down iface=" + ifaceName);
+ Log.i(TAG, "Successfully torn down iface=" + ifaceName);
+ }
+ }
+ }
+
+ /** Helper method invoked to cleanup state after one of the native daemon's death. */
+ private void onNativeDaemonDeath() {
+ synchronized (mLock) {
+ Log.i(TAG, "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(TAG, "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);
}
}
}
/**
- * Common death handler for any of the lower layer daemons.
+ * Death handler for the Vendor HAL daemon.
*/
- private class DeathHandlerInternal implements VendorHalDeathEventHandler,
- SupplicantDeathEventHandler, WificondDeathEventHandler {
+ private class VendorHalDeathHandlerInternal implements VendorHalDeathEventHandler {
@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);
- }
+ Log.i(TAG, "Vendor HAL died. Cleaning up internal state.");
+ onNativeDaemonDeath();
+ mWifiMetrics.incrementNumHalCrashes();
+ }
+ }
+ }
+
+ /**
+ * Death handler for the wificond daemon.
+ */
+ private class WificondDeathHandlerInternal implements WificondDeathEventHandler {
+ @Override
+ public void onDeath() {
+ synchronized (mLock) {
+ Log.i(TAG, "wificond died. Cleaning up internal state.");
+ onNativeDaemonDeath();
+ mWifiMetrics.incrementNumWificondCrashes();
+ }
+ }
+ }
+
+ /**
+ * Death handler for the supplicant daemon.
+ */
+ private class SupplicantDeathHandlerInternal implements SupplicantDeathEventHandler {
+ @Override
+ public void onDeath() {
+ synchronized (mLock) {
+ Log.i(TAG, "wpa_supplicant died. Cleaning up internal state.");
+ onNativeDaemonDeath();
+ mWifiMetrics.incrementNumSupplicantCrashes();
+ }
+ }
+ }
+
+ /**
+ * Death handler for the hostapd daemon.
+ */
+ private class HostapdDeathHandlerInternal implements HostapdDeathEventHandler {
+ @Override
+ public void onDeath() {
+ synchronized (mLock) {
+ Log.i(TAG, "hostapd died. Cleaning up internal state.");
+ onNativeDaemonDeath();
+ mWifiMetrics.incrementNumHostapdCrashes();
}
}
}
@@ -496,13 +538,14 @@ public class WifiNative {
@Override
public void interfaceLinkStateChanged(String ifaceName, boolean isUp) {
synchronized (mLock) {
- Log.i(mTAG, "Interface link state changed=" + ifaceName + ", isUp=" + isUp);
+ Log.i(TAG, "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="
+ Log.e(TAG, "Received iface up/down notification on an invalid iface="
+ ifaceName);
return;
}
+
if (isUp) {
iface.externalListener.onUp(ifaceName);
} else {
@@ -512,6 +555,93 @@ public class WifiNative {
}
}
+ // For devices that don't support the vendor HAL, we will not support any concurrency.
+ // So simulate the HalDeviceManager behavior by triggering the destroy listener for
+ // any active interface.
+ private String handleIfaceCreationWhenVendorHalNotSupported(@NonNull Iface newIface) {
+ Iface existingIface = mIfaceMgr.removeExistingIface(newIface.id);
+ if (existingIface != null) {
+ onInterfaceDestroyed(existingIface);
+ Log.i(TAG, "Successfully torn down iface=" + existingIface.name);
+ }
+ // Return the interface name directly from the system property.
+ return mPropertyService.getString("wifi.interface", "wlan0");
+ }
+
+ /**
+ * Helper function to handle creation of STA iface.
+ * For devices which do not the support the HAL, this will bypass HalDeviceManager &
+ * teardown any existing iface.
+ */
+ private String createStaIface(@NonNull Iface iface) {
+ synchronized (mLock) {
+ if (mWifiVendorHal.isVendorHalSupported()) {
+ return mWifiVendorHal.createStaIface(
+ new InterfaceDestoyedListenerInternal(iface.id));
+ } else {
+ Log.i(TAG, "Vendor Hal not supported, ignoring createStaIface.");
+ return handleIfaceCreationWhenVendorHalNotSupported(iface);
+ }
+ }
+ }
+
+ /**
+ * Helper function to handle creation of AP iface.
+ * For devices which do not the support the HAL, this will bypass HalDeviceManager &
+ * teardown any existing iface.
+ */
+ private String createApIface(@NonNull Iface iface) {
+ synchronized (mLock) {
+ if (mWifiVendorHal.isVendorHalSupported()) {
+ return mWifiVendorHal.createApIface(
+ new InterfaceDestoyedListenerInternal(iface.id));
+ } else {
+ Log.i(TAG, "Vendor Hal not supported, ignoring createApIface.");
+ return handleIfaceCreationWhenVendorHalNotSupported(iface);
+ }
+ }
+ }
+
+ // For devices that don't support the vendor HAL, we will not support any concurrency.
+ // So simulate the HalDeviceManager behavior by triggering the destroy listener for
+ // the interface.
+ private boolean handleIfaceRemovalWhenVendorHalNotSupported(@NonNull Iface iface) {
+ mIfaceMgr.removeIface(iface.id);
+ onInterfaceDestroyed(iface);
+ Log.i(TAG, "Successfully torn down iface=" + iface.name);
+ return true;
+ }
+
+ /**
+ * Helper function to handle removal of STA iface.
+ * For devices which do not the support the HAL, this will bypass HalDeviceManager &
+ * teardown any existing iface.
+ */
+ private boolean removeStaIface(@NonNull Iface iface) {
+ synchronized (mLock) {
+ if (mWifiVendorHal.isVendorHalSupported()) {
+ return mWifiVendorHal.removeStaIface(iface.name);
+ } else {
+ Log.i(TAG, "Vendor Hal not supported, ignoring removeStaIface.");
+ return handleIfaceRemovalWhenVendorHalNotSupported(iface);
+ }
+ }
+ }
+
+ /**
+ * Helper function to handle removal of STA iface.
+ */
+ private boolean removeApIface(@NonNull Iface iface) {
+ synchronized (mLock) {
+ if (mWifiVendorHal.isVendorHalSupported()) {
+ return mWifiVendorHal.removeApIface(iface.name);
+ } else {
+ Log.i(TAG, "Vendor Hal not supported, ignoring removeApIface.");
+ return handleIfaceRemovalWhenVendorHalNotSupported(iface);
+ }
+ }
+ }
+
/**
* Initialize the native modules.
*
@@ -519,12 +649,12 @@ public class WifiNative {
*/
public boolean initialize() {
synchronized (mLock) {
- if (!mWifiVendorHal.initialize(new DeathHandlerInternal())) {
- Log.e(mTAG, "Failed to initialize vendor HAL");
+ if (!mWifiVendorHal.initialize(new VendorHalDeathHandlerInternal())) {
+ Log.e(TAG, "Failed to initialize vendor HAL");
return false;
}
- if (!mWificondControl.registerDeathHandler(new DeathHandlerInternal())) {
- Log.e(mTAG, "Failed to initialize wificond");
+ if (!mWificondControl.registerDeathHandler(new WificondDeathHandlerInternal())) {
+ Log.e(TAG, "Failed to initialize wificond");
return false;
}
return true;
@@ -580,6 +710,29 @@ public class WifiNative {
void onDown(String ifaceName);
}
+ private void initializeNwParamsForClientInterface(@NonNull String ifaceName) {
+ try {
+ // A runtime crash or shutting down AP mode can leave
+ // IP addresses configured, and this affects
+ // connectivity when supplicant starts up.
+ // Ensure we have no IP addresses before a supplicant start.
+ mNwManagementService.clearInterfaceAddresses(ifaceName);
+
+ // Set privacy extensions
+ mNwManagementService.setInterfaceIpv6PrivacyExtensions(ifaceName, true);
+
+ // IPv6 is enabled only as long as access point is connected since:
+ // - IPv6 addresses and routes stick around after disconnection
+ // - kernel is unaware when connected and fails to start IPv6 negotiation
+ // - kernel can start autoconfiguration when 802.1x is not complete
+ mNwManagementService.disableIpv6(ifaceName);
+ } catch (RemoteException re) {
+ Log.e(TAG, "Unable to change interface settings: " + re);
+ } catch (IllegalStateException ie) {
+ Log.e(TAG, "Unable to change interface settings: " + ie);
+ }
+ }
+
/**
* Setup an interface for Client mode operations.
*
@@ -592,43 +745,48 @@ public class WifiNative {
public String setupInterfaceForClientMode(@NonNull InterfaceCallback interfaceCallback) {
synchronized (mLock) {
if (!startHal()) {
- Log.e(mTAG, "Failed to start Hal");
+ Log.e(TAG, "Failed to start Hal");
+ mWifiMetrics.incrementNumWifiOnFailureDueToHal();
return null;
}
if (!startSupplicant()) {
- Log.e(mTAG, "Failed to start supplicant");
+ Log.e(TAG, "Failed to start supplicant");
+ mWifiMetrics.incrementNumWifiOnFailureDueToSupplicant();
return null;
}
Iface iface = mIfaceMgr.allocateIface(Iface.IFACE_TYPE_STA);
if (iface == null) {
- Log.e(mTAG, "Failed to allocate new STA iface");
+ Log.e(TAG, "Failed to allocate new STA iface");
return null;
}
iface.externalListener = interfaceCallback;
- iface.name =
- mWifiVendorHal.createStaIface(new InterfaceDestoyedListenerInternal(iface.id));
+ iface.name = createStaIface(iface);
if (TextUtils.isEmpty(iface.name)) {
- Log.e(mTAG, "Failed to create iface in vendor HAL");
+ Log.e(TAG, "Failed to create iface in vendor HAL");
mIfaceMgr.removeIface(iface.id);
+ mWifiMetrics.incrementNumWifiOnFailureDueToHal();
return null;
}
if (mWificondControl.setupInterfaceForClientMode(iface.name) == null) {
- Log.e(mTAG, "Failed to setup iface in wificond=" + iface.name);
+ Log.e(TAG, "Failed to setup iface in wificond=" + iface.name);
teardownInterface(iface.name);
+ mWifiMetrics.incrementNumWifiOnFailureDueToWificond();
return null;
}
if (!mSupplicantStaIfaceHal.setupIface(iface.name)) {
- Log.e(mTAG, "Failed to setup iface in supplicant=" + iface.name);
+ Log.e(TAG, "Failed to setup iface in supplicant=" + iface.name);
teardownInterface(iface.name);
+ mWifiMetrics.incrementNumWifiOnFailureDueToSupplicant();
return null;
}
iface.networkObserver = new NetworkObserverInternal(iface.id);
if (!registerNetworkObserver(iface.networkObserver)) {
- Log.e(mTAG, "Failed to register network observer for iface=" + iface.name);
+ Log.e(TAG, "Failed to register network observer for iface=" + iface.name);
teardownInterface(iface.name);
return null;
}
- Log.i(mTAG, "Successfully setup iface=" + iface.name);
+ initializeNwParamsForClientInterface(iface.name);
+ Log.i(TAG, "Successfully setup iface=" + iface.name);
return iface.name;
}
}
@@ -645,34 +803,38 @@ public class WifiNative {
public String setupInterfaceForSoftApMode(@NonNull InterfaceCallback interfaceCallback) {
synchronized (mLock) {
if (!startHal()) {
- Log.e(mTAG, "Failed to start Hal");
+ Log.e(TAG, "Failed to start Hal");
+ mWifiMetrics.incrementNumWifiOnFailureDueToHal();
return null;
}
Iface iface = mIfaceMgr.allocateIface(Iface.IFACE_TYPE_AP);
if (iface == null) {
- Log.e(mTAG, "Failed to allocate new AP iface");
+ Log.e(TAG, "Failed to allocate new AP iface");
return null;
}
iface.externalListener = interfaceCallback;
- iface.name =
- mWifiVendorHal.createApIface(new InterfaceDestoyedListenerInternal(iface.id));
+ iface.name = createApIface(iface);
if (TextUtils.isEmpty(iface.name)) {
- Log.e(mTAG, "Failed to create iface in vendor HAL");
+ Log.e(TAG, "Failed to create iface in vendor HAL");
mIfaceMgr.removeIface(iface.id);
+ // TODO(b/68716726): Separate SoftAp metrics
+ mWifiMetrics.incrementNumWifiOnFailureDueToHal();
return null;
}
if (mWificondControl.setupInterfaceForSoftApMode(iface.name) == null) {
- Log.e(mTAG, "Failed to setup iface in wificond=" + iface.name);
+ Log.e(TAG, "Failed to setup iface in wificond=" + iface.name);
teardownInterface(iface.name);
+ // TODO(b/68716726): Separate SoftAp metrics
+ mWifiMetrics.incrementNumWifiOnFailureDueToWificond();
return null;
}
iface.networkObserver = new NetworkObserverInternal(iface.id);
if (!registerNetworkObserver(iface.networkObserver)) {
- Log.e(mTAG, "Failed to register network observer for iface=" + iface.name);
+ Log.e(TAG, "Failed to register network observer for iface=" + iface.name);
teardownInterface(iface.name);
return null;
}
- Log.i(mTAG, "Successfully setup iface=" + iface.name);
+ Log.i(TAG, "Successfully setup iface=" + iface.name);
return iface.name;
}
}
@@ -687,7 +849,7 @@ public class WifiNative {
synchronized (mLock) {
final Iface iface = mIfaceMgr.getIface(ifaceName);
if (iface == null) {
- Log.e(mTAG, "Trying to get iface state on invalid iface=" + ifaceName);
+ Log.e(TAG, "Trying to get iface state on invalid iface=" + ifaceName);
return false;
}
InterfaceConfiguration config = null;
@@ -707,6 +869,7 @@ public class WifiNative {
*
* This method tears down the associated interface from all the native daemons
* (wificond, wpa_supplicant & vendor HAL).
+ * Also, brings down the HAL, supplicant or hostapd as necessary.
*
* @param ifaceName Name of the interface.
*/
@@ -714,27 +877,82 @@ public class WifiNative {
synchronized (mLock) {
final Iface iface = mIfaceMgr.getIface(ifaceName);
if (iface == null) {
- Log.e(mTAG, "Trying to teardown an invalid iface=" + ifaceName);
+ Log.e(TAG, "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);
+ if (!removeStaIface(iface)) {
+ Log.e(TAG, "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);
+ if (!removeApIface(iface)) {
+ Log.e(TAG, "Failed to remove iface in vendor HAL=" + ifaceName);
return;
}
}
- Log.i(mTAG, "Successfully initiated teardown for iface=" + ifaceName);
+ Log.i(TAG, "Successfully initiated teardown for iface=" + ifaceName);
+ }
+ }
+
+ /**
+ * Teardown all the active interfaces.
+ *
+ * This method tears down the associated interfaces from all the native daemons
+ * (wificond, wpa_supplicant & vendor HAL).
+ * Also, brings down the HAL, supplicant or hostapd as necessary.
+ */
+ public void teardownAllInterfaces() {
+ synchronized (mLock) {
+ Iterator<Integer> ifaceIdIter = mIfaceMgr.getIfaceIdIter();
+ while (ifaceIdIter.hasNext()) {
+ Iface iface = mIfaceMgr.getIface(ifaceIdIter.next());
+ teardownInterface(iface.name);
+ }
+ Log.i(TAG, "Successfully initiated teardown for all ifaces");
}
}
+ /**
+ * Get name of the client interface.
+ *
+ * This is mainly used by external modules that needs to perform some
+ * client operations on the STA interface.
+ *
+ * TODO(b/70932231): This may need to be reworked once we start supporting STA + STA.
+ *
+ * @return Interface name of any active client interface, null if no active client interface
+ * exist.
+ * Return Values for the different scenarios are listed below:
+ * a) When there are no client interfaces, returns null.
+ * b) when there is 1 client interface, returns the name of that interface.
+ * c) When there are 2 or more client interface, returns the name of any client interface.
+ */
+ public String getClientInterfaceName() {
+ return mIfaceMgr.findAnyStaIfaceName();
+ }
+
+ /**
+ * Get name of the softap interface.
+ *
+ * This is mainly used by external modules that needs to perform some
+ * operations on the AP interface.
+ *
+ * TODO(b/70932231): This may need to be reworked once we start supporting AP + AP.
+ *
+ * @return Interface name of any active softap interface, null if no active softap interface
+ * exist.
+ * Return Values for the different scenarios are listed below:
+ * a) When there are no softap interfaces, returns null.
+ * b) when there is 1 softap interface, returns the name of that interface.
+ * c) When there are 2 or more softap interface, returns the name of any softap interface.
+ */
+ public String getSoftApInterfaceName() {
+ return mIfaceMgr.findAnyApIfaceName();
+ }
+
/********************************************************
* Wificond operations
********************************************************/
@@ -771,53 +989,24 @@ public class WifiNative {
}
/**
- * 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.
+ * Request signal polling to wificond.
+ *
+ * @param ifaceName Name of the interface.
+ * Returns an SignalPollResult object.
+ * Returns null on failure.
*/
- public boolean deregisterWificondDeathHandler() {
- return mWificondControl.deregisterDeathHandler();
- }
-
- /**
- * Disable wpa_supplicant via wificond.
- * @return Returns true on success.
- */
- public boolean disableSupplicant() {
- return mWificondControl.disableSupplicant();
- }
-
- /**
- * Enable wpa_supplicant via wificond.
- * @return Returns true on success.
- */
- public boolean enableSupplicant() {
- return mWificondControl.enableSupplicant();
- }
-
- /**
- * Request signal polling to wificond.
- * Returns an SignalPollResult object.
- * Returns null on failure.
- */
- public SignalPollResult signalPoll() {
- return mWificondControl.signalPoll(mInterfaceName);
+ public SignalPollResult signalPoll(@NonNull String ifaceName) {
+ return mWificondControl.signalPoll(ifaceName);
}
/**
* Fetch TX packet counters on current connection from wificond.
- * Returns an TxPacketCounters object.
- * Returns null on failure.
- */
- public TxPacketCounters getTxPacketCounters() {
- return mWificondControl.getTxPacketCounters(mInterfaceName);
+ * @param ifaceName Name of the interface.
+ * Returns an TxPacketCounters object.
+ * Returns null on failure.
+ */
+ public TxPacketCounters getTxPacketCounters(@NonNull String ifaceName) {
+ return mWificondControl.getTxPacketCounters(ifaceName);
}
/**
@@ -838,48 +1027,57 @@ public class WifiNative {
/**
* Start a scan using wificond for the given parameters.
+ * @param ifaceName Name of the interface.
+ * @param scanType Type of scan to perform. One of {@link ScanSettings#SCAN_TYPE_LOW_LATENCY},
+ * {@link ScanSettings#SCAN_TYPE_LOW_POWER} or {@link ScanSettings#SCAN_TYPE_HIGH_ACCURACY}.
* @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) {
- return mWificondControl.scan(mInterfaceName, freqs, hiddenNetworkSSIDs);
+ public boolean scan(
+ @NonNull String ifaceName, int scanType, Set<Integer> freqs,
+ Set<String> hiddenNetworkSSIDs) {
+ return mWificondControl.scan(ifaceName, scanType, freqs, hiddenNetworkSSIDs);
}
/**
* 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() {
+ public ArrayList<ScanDetail> getScanResults(@NonNull String ifaceName) {
return mWificondControl.getScanResults(
- mInterfaceName, WificondControl.SCAN_TYPE_SINGLE_SCAN);
+ ifaceName, WificondControl.SCAN_TYPE_SINGLE_SCAN);
}
/**
* 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> getPnoScanResults() {
- return mWificondControl.getScanResults(mInterfaceName, WificondControl.SCAN_TYPE_PNO_SCAN);
+ public ArrayList<ScanDetail> getPnoScanResults(@NonNull String ifaceName) {
+ return mWificondControl.getScanResults(ifaceName, WificondControl.SCAN_TYPE_PNO_SCAN);
}
/**
* Start PNO scan.
+ * @param ifaceName Name of the interface.
* @param pnoSettings Pno scan configuration.
* @return true on success.
*/
- public boolean startPnoScan(PnoSettings pnoSettings) {
- return mWificondControl.startPnoScan(mInterfaceName, pnoSettings);
+ public boolean startPnoScan(@NonNull String ifaceName, PnoSettings pnoSettings) {
+ return mWificondControl.startPnoScan(ifaceName, pnoSettings);
}
/**
* Stop PNO scan.
+ * @param ifaceName Name of the interface.
* @return true on success.
*/
- public boolean stopPnoScan() {
- return mWificondControl.stopPnoScan(mInterfaceName);
+ public boolean stopPnoScan(@NonNull String ifaceName) {
+ return mWificondControl.stopPnoScan(ifaceName);
}
/**
@@ -892,73 +1090,114 @@ public class WifiNative {
void onNumAssociatedStationsChanged(int numStations);
}
+ private static final int CONNECT_TO_HOSTAPD_RETRY_INTERVAL_MS = 100;
+ private static final int CONNECT_TO_HOSTAPD_RETRY_TIMES = 50;
+ /**
+ * This method is called to wait for establishing connection to hostapd.
+ *
+ * @return true if connection is established, false otherwise.
+ */
+ private boolean waitForHostapdConnection() {
+ // Start initialization if not already started.
+ if (!mHostapdHal.isInitializationStarted()
+ && !mHostapdHal.initialize()) {
+ return false;
+ }
+ boolean connected = false;
+ int connectTries = 0;
+ while (!connected && connectTries++ < CONNECT_TO_HOSTAPD_RETRY_TIMES) {
+ // Check if the initialization is complete.
+ connected = mHostapdHal.isInitializationComplete();
+ if (connected) {
+ break;
+ }
+ try {
+ Thread.sleep(CONNECT_TO_HOSTAPD_RETRY_INTERVAL_MS);
+ } catch (InterruptedException ignore) {
+ }
+ }
+ return connected;
+ }
+
/**
* 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) {
- return mWificondControl.startSoftAp(mInterfaceName, config, listener);
+ public boolean startSoftAp(
+ @NonNull String ifaceName, WifiConfiguration config, SoftApListener listener) {
+ if (!mWificondControl.startHostapd(ifaceName, listener)) {
+ Log.e(TAG, "Failed to start hostapd");
+ return false;
+ }
+ if (!waitForHostapdConnection()) {
+ Log.e(TAG, "Failed to establish connection to hostapd");
+ return false;
+ }
+ if (!mHostapdHal.registerDeathHandler(new HostapdDeathHandlerInternal())) {
+ Log.e(TAG, "Failed to register hostapd death handler");
+ return false;
+ }
+ if (!mHostapdHal.addAccessPoint(ifaceName, config)) {
+ Log.e(TAG, "Failed to add acccess point");
+ return false;
+ }
+ return true;
}
/**
* Stop the ongoing Soft AP operation.
*
+ * @param ifaceName Name of the interface.
* @return true on success, false otherwise.
*/
- public boolean stopSoftAp() {
- return mWificondControl.stopSoftAp(mInterfaceName);
+ public boolean stopSoftAp(@NonNull String ifaceName) {
+ if (!mHostapdHal.removeAccessPoint(ifaceName)) {
+ Log.e(TAG, "Failed to remove access point");
+ }
+ return mWificondControl.stopHostapd(ifaceName);
+ }
+
+ /**
+ * Set MAC address of the given interface
+ * @param interfaceName Name of the interface
+ * @param mac Mac address to change into
+ * @return true on success
+ */
+ public boolean setMacAddress(String interfaceName, MacAddress mac) {
+ // TODO(b/72459123): Suppress interface down/up events from this call
+ return mWificondControl.setMacAddress(interfaceName, mac);
}
/********************************************************
- * Supplicant operations
+ * Hostapd operations
********************************************************/
/**
- * Callback to notify supplicant death.
+ * Callback to notify hostapd death.
*/
- public interface SupplicantDeathEventHandler {
+ public interface HostapdDeathEventHandler {
/**
* 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.
- */
- public boolean connectToSupplicant() {
- // Start initialization if not already started.
- if (!mSupplicantStaIfaceHal.isInitializationStarted()
- && !mSupplicantStaIfaceHal.initialize()) {
- return false;
- }
- // Check if the initialization is complete.
- if (!mSupplicantStaIfaceHal.isInitializationComplete()) {
- return false;
- }
- // Setup the STA iface once connection is established.
- return mSupplicantStaIfaceHal.setupIface(mInterfaceName);
- }
+ /********************************************************
+ * Supplicant operations
+ ********************************************************/
/**
- * Close supplicant connection.
+ * Callback to notify supplicant death.
*/
- public void closeSupplicantConnection() {
+ public interface SupplicantDeathEventHandler {
+ /**
+ * Invoked when the supplicant dies.
+ */
+ void onDeath();
}
/**
@@ -973,43 +1212,48 @@ public class WifiNative {
/**
* 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() {
- return mSupplicantStaIfaceHal.reconnect(mInterfaceName);
+ public boolean reconnect(@NonNull String ifaceName) {
+ return mSupplicantStaIfaceHal.reconnect(ifaceName);
}
/**
* 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() {
- return mSupplicantStaIfaceHal.reassociate(mInterfaceName);
+ public boolean reassociate(@NonNull String ifaceName) {
+ return mSupplicantStaIfaceHal.reassociate(ifaceName);
}
/**
* 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() {
- return mSupplicantStaIfaceHal.disconnect(mInterfaceName);
+ public boolean disconnect(@NonNull String ifaceName) {
+ return mSupplicantStaIfaceHal.disconnect(ifaceName);
}
/**
* 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() {
- return mSupplicantStaIfaceHal.getMacAddress(mInterfaceName);
+ public String getMacAddress(@NonNull String ifaceName) {
+ return mSupplicantStaIfaceHal.getMacAddress(ifaceName);
}
public static final int RX_FILTER_TYPE_V4_MULTICAST = 0;
public static final int RX_FILTER_TYPE_V6_MULTICAST = 1;
/**
* Start filtering out Multicast V4 packets
+ * @param ifaceName Name of the interface.
* @return {@code true} if the operation succeeded, {@code false} otherwise
*
* Multicast filtering rules work as follows:
@@ -1032,59 +1276,63 @@ public class WifiNative {
*
* The SETSUSPENDOPT driver command overrides the filtering rules
*/
- public boolean startFilteringMulticastV4Packets() {
- return mSupplicantStaIfaceHal.stopRxFilter(mInterfaceName)
+ public boolean startFilteringMulticastV4Packets(@NonNull String ifaceName) {
+ return mSupplicantStaIfaceHal.stopRxFilter(ifaceName)
&& mSupplicantStaIfaceHal.removeRxFilter(
- mInterfaceName, RX_FILTER_TYPE_V4_MULTICAST)
- && mSupplicantStaIfaceHal.startRxFilter(mInterfaceName);
+ ifaceName, RX_FILTER_TYPE_V4_MULTICAST)
+ && mSupplicantStaIfaceHal.startRxFilter(ifaceName);
}
/**
* Stop filtering out Multicast V4 packets.
+ * @param ifaceName Name of the interface.
* @return {@code true} if the operation succeeded, {@code false} otherwise
*/
- public boolean stopFilteringMulticastV4Packets() {
- return mSupplicantStaIfaceHal.stopRxFilter(mInterfaceName)
+ public boolean stopFilteringMulticastV4Packets(@NonNull String ifaceName) {
+ return mSupplicantStaIfaceHal.stopRxFilter(ifaceName)
&& mSupplicantStaIfaceHal.addRxFilter(
- mInterfaceName, RX_FILTER_TYPE_V4_MULTICAST)
- && mSupplicantStaIfaceHal.startRxFilter(mInterfaceName);
+ ifaceName, RX_FILTER_TYPE_V4_MULTICAST)
+ && mSupplicantStaIfaceHal.startRxFilter(ifaceName);
}
/**
* Start filtering out Multicast V6 packets
+ * @param ifaceName Name of the interface.
* @return {@code true} if the operation succeeded, {@code false} otherwise
*/
- public boolean startFilteringMulticastV6Packets() {
- return mSupplicantStaIfaceHal.stopRxFilter(mInterfaceName)
+ public boolean startFilteringMulticastV6Packets(@NonNull String ifaceName) {
+ return mSupplicantStaIfaceHal.stopRxFilter(ifaceName)
&& mSupplicantStaIfaceHal.removeRxFilter(
- mInterfaceName, RX_FILTER_TYPE_V6_MULTICAST)
- && mSupplicantStaIfaceHal.startRxFilter(mInterfaceName);
+ ifaceName, RX_FILTER_TYPE_V6_MULTICAST)
+ && mSupplicantStaIfaceHal.startRxFilter(ifaceName);
}
/**
* Stop filtering out Multicast V6 packets.
+ * @param ifaceName Name of the interface.
* @return {@code true} if the operation succeeded, {@code false} otherwise
*/
- public boolean stopFilteringMulticastV6Packets() {
- return mSupplicantStaIfaceHal.stopRxFilter(mInterfaceName)
+ public boolean stopFilteringMulticastV6Packets(@NonNull String ifaceName) {
+ return mSupplicantStaIfaceHal.stopRxFilter(ifaceName)
&& mSupplicantStaIfaceHal.addRxFilter(
- mInterfaceName, RX_FILTER_TYPE_V6_MULTICAST)
- && mSupplicantStaIfaceHal.startRxFilter(mInterfaceName);
+ ifaceName, RX_FILTER_TYPE_V6_MULTICAST)
+ && mSupplicantStaIfaceHal.startRxFilter(ifaceName);
}
public static final int BLUETOOTH_COEXISTENCE_MODE_ENABLED = 0;
public static final int BLUETOOTH_COEXISTENCE_MODE_DISABLED = 1;
public static final int BLUETOOTH_COEXISTENCE_MODE_SENSE = 2;
/**
- * Sets the bluetooth coexistence mode.
- *
- * @param mode One of {@link #BLUETOOTH_COEXISTENCE_MODE_DISABLED},
- * {@link #BLUETOOTH_COEXISTENCE_MODE_ENABLED}, or
- * {@link #BLUETOOTH_COEXISTENCE_MODE_SENSE}.
- * @return Whether the mode was successfully set.
- */
- public boolean setBluetoothCoexistenceMode(int mode) {
- return mSupplicantStaIfaceHal.setBtCoexistenceMode(mInterfaceName, mode);
+ * Sets the bluetooth coexistence mode.
+ *
+ * @param ifaceName Name of the interface.
+ * @param mode One of {@link #BLUETOOTH_COEXISTENCE_MODE_DISABLED},
+ * {@link #BLUETOOTH_COEXISTENCE_MODE_ENABLED}, or
+ * {@link #BLUETOOTH_COEXISTENCE_MODE_SENSE}.
+ * @return Whether the mode was successfully set.
+ */
+ public boolean setBluetoothCoexistenceMode(@NonNull String ifaceName, int mode) {
+ return mSupplicantStaIfaceHal.setBtCoexistenceMode(ifaceName, mode);
}
/**
@@ -1092,87 +1340,96 @@ public class WifiNative {
* some of the low-level scan parameters used by the driver are changed to
* reduce interference with A2DP streaming.
*
+ * @param ifaceName Name of the interface.
* @param setCoexScanMode whether to enable or disable this mode
* @return {@code true} if the command succeeded, {@code false} otherwise.
*/
- public boolean setBluetoothCoexistenceScanMode(boolean setCoexScanMode) {
+ public boolean setBluetoothCoexistenceScanMode(
+ @NonNull String ifaceName, boolean setCoexScanMode) {
return mSupplicantStaIfaceHal.setBtCoexistenceScanModeEnabled(
- mInterfaceName, setCoexScanMode);
+ ifaceName, setCoexScanMode);
}
/**
* Enable or disable suspend mode optimizations.
*
+ * @param ifaceName Name of the interface.
* @param enabled true to enable, false otherwise.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setSuspendOptimizations(boolean enabled) {
- return mSupplicantStaIfaceHal.setSuspendModeEnabled(mInterfaceName, enabled);
+ public boolean setSuspendOptimizations(@NonNull String ifaceName, boolean enabled) {
+ return mSupplicantStaIfaceHal.setSuspendModeEnabled(ifaceName, enabled);
}
/**
* Set country code.
*
+ * @param ifaceName Name of the interface.
* @param countryCode 2 byte ASCII string. For ex: US, CA.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setCountryCode(String countryCode) {
- return mSupplicantStaIfaceHal.setCountryCode(mInterfaceName, countryCode);
+ public boolean setCountryCode(@NonNull String ifaceName, String countryCode) {
+ return mSupplicantStaIfaceHal.setCountryCode(ifaceName, countryCode);
}
/**
* Initiate TDLS discover and setup or teardown with the specified peer.
*
+ * @param ifaceName Name of the interface.
* @param macAddr MAC Address of the peer.
* @param enable true to start discovery and setup, false to teardown.
*/
- public void startTdls(String macAddr, boolean enable) {
+ public void startTdls(@NonNull String ifaceName, String macAddr, boolean enable) {
if (enable) {
- mSupplicantStaIfaceHal.initiateTdlsDiscover(mInterfaceName, macAddr);
- mSupplicantStaIfaceHal.initiateTdlsSetup(mInterfaceName, macAddr);
+ mSupplicantStaIfaceHal.initiateTdlsDiscover(ifaceName, macAddr);
+ mSupplicantStaIfaceHal.initiateTdlsSetup(ifaceName, macAddr);
} else {
- mSupplicantStaIfaceHal.initiateTdlsTeardown(mInterfaceName, macAddr);
+ mSupplicantStaIfaceHal.initiateTdlsTeardown(ifaceName, macAddr);
}
}
/**
* Start WPS pin display operation with the specified peer.
*
+ * @param ifaceName Name of the interface.
* @param bssid BSSID of the peer.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean startWpsPbc(String bssid) {
- return mSupplicantStaIfaceHal.startWpsPbc(mInterfaceName, bssid);
+ public boolean startWpsPbc(@NonNull String ifaceName, String bssid) {
+ return mSupplicantStaIfaceHal.startWpsPbc(ifaceName, bssid);
}
/**
* 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) {
- return mSupplicantStaIfaceHal.startWpsPinKeypad(mInterfaceName, pin);
+ public boolean startWpsPinKeypad(@NonNull String ifaceName, String pin) {
+ return mSupplicantStaIfaceHal.startWpsPinKeypad(ifaceName, pin);
}
/**
* Start WPS pin display operation with the specified peer.
*
+ * @param ifaceName Name of the interface.
* @param bssid BSSID of the peer.
* @return new pin generated on success, null otherwise.
*/
- public String startWpsPinDisplay(String bssid) {
- return mSupplicantStaIfaceHal.startWpsPinDisplay(mInterfaceName, bssid);
+ public String startWpsPinDisplay(@NonNull String ifaceName, String bssid) {
+ return mSupplicantStaIfaceHal.startWpsPinDisplay(ifaceName, bssid);
}
/**
* Sets whether to use external sim for SIM/USIM processing.
*
+ * @param ifaceName Name of the interface.
* @param external true to enable, false otherwise.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setExternalSim(boolean external) {
- return mSupplicantStaIfaceHal.setExternalSim(mInterfaceName, external);
+ public boolean setExternalSim(@NonNull String ifaceName, boolean external) {
+ return mSupplicantStaIfaceHal.setExternalSim(ifaceName, external);
}
/**
@@ -1185,20 +1442,22 @@ public class WifiNative {
/**
* Send the sim auth response for the currently configured network.
*
+ * @param ifaceName Name of the interface.
* @param type |GSM-AUTH|, |UMTS-AUTH| or |UMTS-AUTS|.
* @param response Response params.
* @return true if succeeds, false otherwise.
*/
- public boolean simAuthResponse(int id, String type, String response) {
+ public boolean simAuthResponse(
+ @NonNull String ifaceName, int id, String type, String response) {
if (SIM_AUTH_RESP_TYPE_GSM_AUTH.equals(type)) {
return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimGsmAuthResponse(
- mInterfaceName, response);
+ ifaceName, response);
} else if (SIM_AUTH_RESP_TYPE_UMTS_AUTH.equals(type)) {
return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimUmtsAuthResponse(
- mInterfaceName, response);
+ ifaceName, response);
} else if (SIM_AUTH_RESP_TYPE_UMTS_AUTS.equals(type)) {
return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimUmtsAutsResponse(
- mInterfaceName, response);
+ ifaceName, response);
} else {
return false;
}
@@ -1207,79 +1466,87 @@ public class WifiNative {
/**
* 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 simAuthFailedResponse(int id) {
- return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimGsmAuthFailure(mInterfaceName);
+ public boolean simAuthFailedResponse(@NonNull String ifaceName, int id) {
+ return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimGsmAuthFailure(ifaceName);
}
/**
* 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 umtsAuthFailedResponse(int id) {
- return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimUmtsAuthFailure(mInterfaceName);
+ public boolean umtsAuthFailedResponse(@NonNull String ifaceName, int id) {
+ return mSupplicantStaIfaceHal.sendCurrentNetworkEapSimUmtsAuthFailure(ifaceName);
}
/**
* Send the eap identity response for the currently configured network.
*
+ * @param ifaceName Name of the interface.
* @param response String to send.
* @return true if succeeds, false otherwise.
*/
- public boolean simIdentityResponse(int id, String response) {
+ public boolean simIdentityResponse(@NonNull String ifaceName, int id, String response) {
return mSupplicantStaIfaceHal.sendCurrentNetworkEapIdentityResponse(
- mInterfaceName, response);
+ ifaceName, response);
}
/**
* This get anonymous identity from supplicant and returns it as a string.
*
+ * @param ifaceName Name of the interface.
* @return anonymous identity string if succeeds, null otherwise.
*/
- public String getEapAnonymousIdentity() {
- return mSupplicantStaIfaceHal.getCurrentNetworkEapAnonymousIdentity(mInterfaceName);
+ public String getEapAnonymousIdentity(@NonNull String ifaceName) {
+ return mSupplicantStaIfaceHal.getCurrentNetworkEapAnonymousIdentity(ifaceName);
}
/**
* Start WPS pin registrar operation with the specified peer and pin.
*
+ * @param ifaceName Name of the interface.
* @param bssid BSSID of the peer.
* @param pin Pin to be used.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean startWpsRegistrar(String bssid, String pin) {
- return mSupplicantStaIfaceHal.startWpsRegistrar(mInterfaceName, bssid, pin);
+ public boolean startWpsRegistrar(@NonNull String ifaceName, String bssid, String pin) {
+ return mSupplicantStaIfaceHal.startWpsRegistrar(ifaceName, bssid, pin);
}
/**
* Cancels any ongoing WPS requests.
*
+ * @param ifaceName Name of the interface.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean cancelWps() {
- return mSupplicantStaIfaceHal.cancelWps(mInterfaceName);
+ public boolean cancelWps(@NonNull String ifaceName) {
+ return mSupplicantStaIfaceHal.cancelWps(ifaceName);
}
/**
* 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 setDeviceName(String name) {
- return mSupplicantStaIfaceHal.setWpsDeviceName(mInterfaceName, name);
+ public boolean setDeviceName(@NonNull String ifaceName, String name) {
+ return mSupplicantStaIfaceHal.setWpsDeviceName(ifaceName, name);
}
/**
* Set WPS device type.
*
+ * @param ifaceName Name of the interface.
* @param type Type specified as a string. Used format: <categ>-<OUI>-<subcateg>
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setDeviceType(String type) {
- return mSupplicantStaIfaceHal.setWpsDeviceType(mInterfaceName, type);
+ public boolean setDeviceType(@NonNull String ifaceName, String type) {
+ return mSupplicantStaIfaceHal.setWpsDeviceType(ifaceName, type);
}
/**
@@ -1288,57 +1555,62 @@ public class WifiNative {
* @param cfg List of config methods.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setConfigMethods(String cfg) {
- return mSupplicantStaIfaceHal.setWpsConfigMethods(mInterfaceName, cfg);
+ public boolean setConfigMethods(@NonNull String ifaceName, String cfg) {
+ return mSupplicantStaIfaceHal.setWpsConfigMethods(ifaceName, cfg);
}
/**
* Set WPS manufacturer.
*
+ * @param ifaceName Name of the interface.
* @param value String to be set.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setManufacturer(String value) {
- return mSupplicantStaIfaceHal.setWpsManufacturer(mInterfaceName, value);
+ public boolean setManufacturer(@NonNull String ifaceName, String value) {
+ return mSupplicantStaIfaceHal.setWpsManufacturer(ifaceName, value);
}
/**
* Set WPS model name.
*
+ * @param ifaceName Name of the interface.
* @param value String to be set.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setModelName(String value) {
- return mSupplicantStaIfaceHal.setWpsModelName(mInterfaceName, value);
+ public boolean setModelName(@NonNull String ifaceName, String value) {
+ return mSupplicantStaIfaceHal.setWpsModelName(ifaceName, value);
}
/**
* Set WPS model number.
*
+ * @param ifaceName Name of the interface.
* @param value String to be set.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setModelNumber(String value) {
- return mSupplicantStaIfaceHal.setWpsModelNumber(mInterfaceName, value);
+ public boolean setModelNumber(@NonNull String ifaceName, String value) {
+ return mSupplicantStaIfaceHal.setWpsModelNumber(ifaceName, value);
}
/**
* Set WPS serial number.
*
+ * @param ifaceName Name of the interface.
* @param value String to be set.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean setSerialNumber(String value) {
- return mSupplicantStaIfaceHal.setWpsSerialNumber(mInterfaceName, value);
+ public boolean setSerialNumber(@NonNull String ifaceName, String value) {
+ return mSupplicantStaIfaceHal.setWpsSerialNumber(ifaceName, value);
}
/**
* Enable or disable power save mode.
*
+ * @param ifaceName Name of the interface.
* @param enabled true to enable, false to disable.
*/
- public void setPowerSave(boolean enabled) {
- mSupplicantStaIfaceHal.setPowerSave(mInterfaceName, enabled);
+ public void setPowerSave(@NonNull String ifaceName, boolean enabled) {
+ mSupplicantStaIfaceHal.setPowerSave(ifaceName, enabled);
}
/**
@@ -1355,24 +1627,27 @@ public class WifiNative {
/**
* Enable/Disable auto reconnect functionality in wpa_supplicant.
*
+ * @param ifaceName Name of the interface.
* @param enable true to enable auto reconnecting, false to disable.
* @return true if request is sent successfully, false otherwise.
*/
- public boolean enableStaAutoReconnect(boolean enable) {
- return mSupplicantStaIfaceHal.enableAutoReconnect(mInterfaceName, enable);
+ public boolean enableStaAutoReconnect(@NonNull String ifaceName, boolean enable) {
+ return mSupplicantStaIfaceHal.enableAutoReconnect(ifaceName, enable);
}
/**
* Migrate 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 Max priority of all the configs.
*/
- public boolean migrateNetworksFromSupplicant(Map<String, WifiConfiguration> configs,
- SparseArray<Map<String, String>> networkExtras) {
- return mSupplicantStaIfaceHal.loadNetworks(mInterfaceName, configs, networkExtras);
+ public boolean migrateNetworksFromSupplicant(
+ @NonNull String ifaceName, Map<String, WifiConfiguration> configs,
+ SparseArray<Map<String, String>> networkExtras) {
+ return mSupplicantStaIfaceHal.loadNetworks(ifaceName, configs, networkExtras);
}
/**
@@ -1385,13 +1660,14 @@ public class WifiNative {
* 5. Select the new network in wpa_supplicant.
* 6. Triggers reconnect command to wpa_supplicant.
*
+ * @param ifaceName Name of the interface.
* @param configuration WifiConfiguration parameters for the provided network.
* @return {@code true} if it succeeds, {@code false} otherwise
*/
- public boolean connectToNetwork(WifiConfiguration configuration) {
+ public boolean connectToNetwork(@NonNull String ifaceName, WifiConfiguration configuration) {
// Abort ongoing scan before connect() to unblock connection request.
- mWificondControl.abortScan(mInterfaceName);
- return mSupplicantStaIfaceHal.connectToNetwork(mInterfaceName, configuration);
+ mWificondControl.abortScan(ifaceName);
+ return mSupplicantStaIfaceHal.connectToNetwork(ifaceName, configuration);
}
/**
@@ -1404,56 +1680,51 @@ public class WifiNative {
* 3. Set the new bssid for the network in wpa_supplicant.
* 4. Triggers reassociate command to wpa_supplicant.
*
+ * @param ifaceName Name of the interface.
* @param configuration WifiConfiguration parameters for the provided network.
* @return {@code true} if it succeeds, {@code false} otherwise
*/
- public boolean roamToNetwork(WifiConfiguration configuration) {
+ public boolean roamToNetwork(@NonNull String ifaceName, WifiConfiguration configuration) {
// Abort ongoing scan before connect() to unblock roaming request.
- mWificondControl.abortScan(mInterfaceName);
- return mSupplicantStaIfaceHal.roamToNetwork(mInterfaceName, configuration);
- }
-
- /**
- * Get the framework network ID corresponding to the provided supplicant network ID for the
- * network configured in wpa_supplicant.
- *
- * @param supplicantNetworkId network ID in wpa_supplicant for the network.
- * @return Corresponding framework network ID if found, -1 if network not found.
- */
- public int getFrameworkNetworkId(int supplicantNetworkId) {
- return supplicantNetworkId;
+ mWificondControl.abortScan(ifaceName);
+ return mSupplicantStaIfaceHal.roamToNetwork(ifaceName, configuration);
}
/**
* Remove all the networks.
*
+ * @param ifaceName Name of the interface.
* @return {@code true} if it succeeds, {@code false} otherwise
*/
- public boolean removeAllNetworks() {
- return mSupplicantStaIfaceHal.removeAllNetworks(mInterfaceName);
+ public boolean removeAllNetworks(@NonNull String ifaceName) {
+ return mSupplicantStaIfaceHal.removeAllNetworks(ifaceName);
}
/**
* Set the BSSID for the currently configured network in wpa_supplicant.
*
+ * @param ifaceName Name of the interface.
* @return true if successful, false otherwise.
*/
- public boolean setConfiguredNetworkBSSID(String bssid) {
- return mSupplicantStaIfaceHal.setCurrentNetworkBssid(mInterfaceName, bssid);
+ public boolean setConfiguredNetworkBSSID(@NonNull String ifaceName, String bssid) {
+ return mSupplicantStaIfaceHal.setCurrentNetworkBssid(ifaceName, bssid);
}
/**
* Initiate ANQP query.
*
+ * @param ifaceName Name of the interface.
* @param bssid BSSID of the AP to be queried
* @param anqpIds Set of anqp IDs.
* @param hs20Subtypes Set of HS20 subtypes.
* @return true on success, false otherwise.
*/
- public boolean requestAnqp(String bssid, Set<Integer> anqpIds, Set<Integer> hs20Subtypes) {
+ public boolean requestAnqp(
+ @NonNull String ifaceName, String bssid, Set<Integer> anqpIds,
+ Set<Integer> hs20Subtypes) {
if (bssid == null || ((anqpIds == null || anqpIds.isEmpty())
&& (hs20Subtypes == null || hs20Subtypes.isEmpty()))) {
- Log.e(mTAG, "Invalid arguments for ANQP request.");
+ Log.e(TAG, "Invalid arguments for ANQP request.");
return false;
}
ArrayList<Short> anqpIdList = new ArrayList<>();
@@ -1463,39 +1734,43 @@ public class WifiNative {
ArrayList<Integer> hs20SubtypeList = new ArrayList<>();
hs20SubtypeList.addAll(hs20Subtypes);
return mSupplicantStaIfaceHal.initiateAnqpQuery(
- mInterfaceName, bssid, anqpIdList, hs20SubtypeList);
+ ifaceName, bssid, anqpIdList, hs20SubtypeList);
}
/**
* Request a passpoint icon file |filename| from the specified AP |bssid|.
+ *
+ * @param ifaceName Name of the interface.
* @param bssid BSSID of the AP
* @param fileName name of the icon file
* @return true if request is sent successfully, false otherwise
*/
- public boolean requestIcon(String bssid, String fileName) {
+ public boolean requestIcon(@NonNull String ifaceName, String bssid, String fileName) {
if (bssid == null || fileName == null) {
- Log.e(mTAG, "Invalid arguments for Icon request.");
+ Log.e(TAG, "Invalid arguments for Icon request.");
return false;
}
- return mSupplicantStaIfaceHal.initiateHs20IconQuery(mInterfaceName, bssid, fileName);
+ return mSupplicantStaIfaceHal.initiateHs20IconQuery(ifaceName, bssid, fileName);
}
/**
* 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() {
- return mSupplicantStaIfaceHal.getCurrentNetworkWpsNfcConfigurationToken(mInterfaceName);
+ public String getCurrentNetworkWpsNfcConfigurationToken(@NonNull String ifaceName) {
+ return mSupplicantStaIfaceHal.getCurrentNetworkWpsNfcConfigurationToken(ifaceName);
}
/** 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) {
- mSupplicantStaIfaceHal.removeNetworkIfCurrent(mInterfaceName, networkId);
+ public void removeNetworkIfCurrent(@NonNull String ifaceName, int networkId) {
+ mSupplicantStaIfaceHal.removeNetworkIfCurrent(ifaceName, networkId);
}
/********************************************************
@@ -1512,42 +1787,6 @@ public class WifiNative {
}
/**
- * Initializes the vendor HAL. This is just used to initialize the {@link HalDeviceManager}.
- */
- public boolean initializeVendorHal(VendorHalDeathEventHandler handler) {
- return mWifiVendorHal.initialize(handler);
- }
-
- /**
- * Bring up the Vendor HAL and configure for STA mode or AP mode, if vendor HAL is supported.
- *
- * @param isStaMode true to start HAL in STA mode, false to start in AP mode.
- * @return false if the HAL start fails, true if successful or if vendor HAL not supported.
- */
- private boolean startHalIfNecessary(boolean isStaMode) {
- if (!mWifiVendorHal.isVendorHalSupported()) {
- Log.i(mTAG, "Vendor HAL not supported, Ignore start...");
- return true;
- }
- if (isStaMode) {
- return mWifiVendorHal.startVendorHalSta();
- } else {
- return mWifiVendorHal.startVendorHalAp();
- }
- }
-
- /**
- * Stops the HAL, if vendor HAL is supported.
- */
- private void stopHalIfNecessary() {
- if (!mWifiVendorHal.isVendorHalSupported()) {
- Log.i(mTAG, "Vendor HAL not supported, Ignore stop...");
- return;
- }
- mWifiVendorHal.stopVendorHal();
- }
-
- /**
* Tests whether the HAL is running or not
*/
public boolean isHalStarted() {
@@ -1566,11 +1805,13 @@ public class WifiNative {
/**
* 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(ScanCapabilities capabilities) {
- return mWifiVendorHal.getBgScanCapabilities(mInterfaceName, capabilities);
+ public boolean getBgScanCapabilities(
+ @NonNull String ifaceName, ScanCapabilities capabilities) {
+ return mWifiVendorHal.getBgScanCapabilities(ifaceName, capabilities);
}
public static class ChannelSettings {
@@ -1613,7 +1854,16 @@ public class WifiNative {
}
}
+ public static final int SCAN_TYPE_LOW_LATENCY = 0;
+ public static final int SCAN_TYPE_LOW_POWER = 1;
+ public static final int SCAN_TYPE_HIGH_ACCURACY = 2;
+
public static class ScanSettings {
+ /**
+ * Type of scan to perform. One of {@link ScanSettings#SCAN_TYPE_LOW_LATENCY},
+ * {@link ScanSettings#SCAN_TYPE_LOW_POWER} or {@link ScanSettings#SCAN_TYPE_HIGH_ACCURACY}.
+ */
+ public int scanType;
public int base_period_ms;
public int max_ap_per_scan;
public int report_threshold_percent;
@@ -1715,53 +1965,64 @@ public class WifiNative {
* Starts a background scan.
* 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(ScanSettings settings, ScanEventHandler eventHandler) {
- return mWifiVendorHal.startBgScan(mInterfaceName, settings, eventHandler);
+ public boolean startBgScan(
+ @NonNull String ifaceName, ScanSettings settings, ScanEventHandler eventHandler) {
+ return mWifiVendorHal.startBgScan(ifaceName, settings, eventHandler);
}
/**
* Stops any ongoing backgound scan
+ * @param ifaceName Name of the interface.
*/
- public void stopBgScan() {
- mWifiVendorHal.stopBgScan(mInterfaceName);
+ public void stopBgScan(@NonNull String ifaceName) {
+ mWifiVendorHal.stopBgScan(ifaceName);
}
/**
* Pauses an ongoing backgound scan
+ * @param ifaceName Name of the interface.
*/
- public void pauseBgScan() {
- mWifiVendorHal.pauseBgScan(mInterfaceName);
+ public void pauseBgScan(@NonNull String ifaceName) {
+ mWifiVendorHal.pauseBgScan(ifaceName);
}
/**
* Restarts a paused scan
+ * @param ifaceName Name of the interface.
*/
- public void restartBgScan() {
- mWifiVendorHal.restartBgScan(mInterfaceName);
+ public void restartBgScan(@NonNull String ifaceName) {
+ mWifiVendorHal.restartBgScan(ifaceName);
}
/**
* Gets the latest scan results received.
+ * @param ifaceName Name of the interface.
*/
- public WifiScanner.ScanData[] getBgScanResults() {
- return mWifiVendorHal.getBgScanResults(mInterfaceName);
+ public WifiScanner.ScanData[] getBgScanResults(@NonNull String ifaceName) {
+ return mWifiVendorHal.getBgScanResults(ifaceName);
}
- public WifiLinkLayerStats getWifiLinkLayerStats() {
- return mWifiVendorHal.getWifiLinkLayerStats(mInterfaceName);
+ /**
+ * Gets the latest link layer stats
+ * @param ifaceName Name of the interface.
+ */
+ public WifiLinkLayerStats getWifiLinkLayerStats(@NonNull String ifaceName) {
+ return mWifiVendorHal.getWifiLinkLayerStats(ifaceName);
}
/**
* Get the supported features
*
+ * @param ifaceName Name of the interface.
* @return bitmask defined by WifiManager.WIFI_FEATURE_*
*/
- public int getSupportedFeatureSet() {
- return mWifiVendorHal.getSupportedFeatureSet(mInterfaceName);
+ public int getSupportedFeatureSet(@NonNull String ifaceName) {
+ return mWifiVendorHal.getSupportedFeatureSet(ifaceName);
}
public static interface RttEventHandler {
@@ -1814,11 +2075,12 @@ public class WifiNative {
* 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 OUI to set.
* @return true for success
*/
- public boolean setScanningMacOui(byte[] oui) {
- return mWifiVendorHal.setScanningMacOui(mInterfaceName, oui);
+ public boolean setScanningMacOui(@NonNull String ifaceName, byte[] oui) {
+ return mWifiVendorHal.setScanningMacOui(ifaceName, oui);
}
/**
@@ -1830,29 +2092,31 @@ public class WifiNative {
/**
* Get the APF (Android Packet Filter) capabilities of the device
+ * @param ifaceName Name of the interface.
*/
- public ApfCapabilities getApfCapabilities() {
- return mWifiVendorHal.getApfCapabilities(mInterfaceName);
+ public ApfCapabilities getApfCapabilities(@NonNull String ifaceName) {
+ return mWifiVendorHal.getApfCapabilities(ifaceName);
}
/**
* 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) {
- return mWifiVendorHal.installPacketFilter(mInterfaceName, filter);
+ public boolean installPacketFilter(@NonNull String ifaceName, byte[] filter) {
+ return mWifiVendorHal.installPacketFilter(ifaceName, filter);
}
/**
* 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) {
- return mWifiVendorHal.setCountryCodeHal(mInterfaceName, countryCode);
+ public boolean setCountryCodeHal(@NonNull String ifaceName, String countryCode) {
+ return mWifiVendorHal.setCountryCodeHal(ifaceName, countryCode);
}
//---------------------------------------------------------------------------------
@@ -2189,56 +2453,58 @@ public class WifiNative {
/**
* Ask the HAL to enable packet fate monitoring. Fails unless HAL is started.
*
+ * @param ifaceName Name of the interface.
* @return true for success, false otherwise.
*/
- public boolean startPktFateMonitoring() {
- return mWifiVendorHal.startPktFateMonitoring(mInterfaceName);
+ public boolean startPktFateMonitoring(@NonNull String ifaceName) {
+ return mWifiVendorHal.startPktFateMonitoring(ifaceName);
}
/**
* Fetch the most recent TX packet fates from the HAL. Fails unless HAL is started.
*
+ * @param ifaceName Name of the interface.
* @return true for success, false otherwise.
*/
- public boolean getTxPktFates(TxFateReport[] reportBufs) {
- return mWifiVendorHal.getTxPktFates(mInterfaceName, reportBufs);
+ public boolean getTxPktFates(@NonNull String ifaceName, TxFateReport[] reportBufs) {
+ return mWifiVendorHal.getTxPktFates(ifaceName, reportBufs);
}
/**
* Fetch the most recent RX packet fates from the HAL. Fails unless HAL is started.
+ * @param ifaceName Name of the interface.
*/
- public boolean getRxPktFates(RxFateReport[] reportBufs) {
- return mWifiVendorHal.getRxPktFates(mInterfaceName, reportBufs);
+ public boolean getRxPktFates(@NonNull String ifaceName, RxFateReport[] reportBufs) {
+ return mWifiVendorHal.getRxPktFates(ifaceName, reportBufs);
}
/**
* Start sending the specified keep alive packets periodically.
*
+ * @param ifaceName Name of the interface.
* @param slot Integer used to identify each request.
- * @param keepAlivePacket Raw packet contents to send.
+ * @param dstMac Destination MAC Address
+ * @param packet Raw packet contents to send.
+ * @param protocol The ethernet protocol type
* @param period Period to use for sending these packets.
* @return 0 for success, -1 for error
*/
- public int startSendingOffloadedPacket(int slot, KeepalivePacketData keepAlivePacket,
- int period) {
- String[] macAddrStr = getMacAddress().split(":");
- byte[] srcMac = new byte[6];
- for (int i = 0; i < 6; i++) {
- Integer hexVal = Integer.parseInt(macAddrStr[i], 16);
- srcMac[i] = hexVal.byteValue();
- }
- return mWifiVendorHal.startSendingOffloadedPacket(mInterfaceName,
- slot, srcMac, keepAlivePacket, period);
+ public int startSendingOffloadedPacket(@NonNull String ifaceName, int slot,
+ byte[] dstMac, byte[] packet, int protocol, int period) {
+ byte[] srcMac = NativeUtil.macAddressToByteArray(getMacAddress(ifaceName));
+ return mWifiVendorHal.startSendingOffloadedPacket(
+ ifaceName, slot, srcMac, dstMac, packet, protocol, period);
}
/**
* 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) {
- return mWifiVendorHal.stopSendingOffloadedPacket(mInterfaceName, slot);
+ public int stopSendingOffloadedPacket(@NonNull String ifaceName, int slot) {
+ return mWifiVendorHal.stopSendingOffloadedPacket(ifaceName, slot);
}
public static interface WifiRssiEventHandler {
@@ -2248,19 +2514,27 @@ public class WifiNative {
/**
* 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,
- WifiRssiEventHandler rssiEventHandler) {
+ public int startRssiMonitoring(
+ @NonNull String ifaceName, byte maxRssi, byte minRssi,
+ WifiRssiEventHandler rssiEventHandler) {
return mWifiVendorHal.startRssiMonitoring(
- mInterfaceName, maxRssi, minRssi, rssiEventHandler);
+ ifaceName, maxRssi, minRssi, rssiEventHandler);
}
- public int stopRssiMonitoring() {
- return mWifiVendorHal.stopRssiMonitoring(mInterfaceName);
+ /**
+ * Stop RSSI monitoring on the currently connected access point.
+ *
+ * @param ifaceName Name of the interface.
+ * @return 0 for success, -1 for failure
+ */
+ public int stopRssiMonitoring(@NonNull String ifaceName) {
+ return mWifiVendorHal.stopRssiMonitoring(ifaceName);
}
/**
@@ -2275,11 +2549,12 @@ public class WifiNative {
/**
* 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 otherwise.
*/
- public boolean configureNeighborDiscoveryOffload(boolean enabled) {
- return mWifiVendorHal.configureNeighborDiscoveryOffload(mInterfaceName, enabled);
+ public boolean configureNeighborDiscoveryOffload(@NonNull String ifaceName, boolean enabled) {
+ return mWifiVendorHal.configureNeighborDiscoveryOffload(ifaceName, enabled);
}
// Firmware roaming control.
@@ -2294,10 +2569,12 @@ public class WifiNative {
/**
* Query the firmware roaming capabilities.
+ * @param ifaceName Name of the interface.
* @return true for success, false otherwise.
*/
- public boolean getRoamingCapabilities(RoamingCapabilities capabilities) {
- return mWifiVendorHal.getRoamingCapabilities(mInterfaceName, capabilities);
+ public boolean getRoamingCapabilities(
+ @NonNull String ifaceName, RoamingCapabilities capabilities) {
+ return mWifiVendorHal.getRoamingCapabilities(ifaceName, capabilities);
}
/**
@@ -2309,10 +2586,11 @@ public class WifiNative {
/**
* Enable/disable firmware roaming.
*
+ * @param ifaceName Name of the interface.
* @return error code returned from HAL.
*/
- public int enableFirmwareRoaming(int state) {
- return mWifiVendorHal.enableFirmwareRoaming(mInterfaceName, state);
+ public int enableFirmwareRoaming(@NonNull String ifaceName, int state) {
+ return mWifiVendorHal.enableFirmwareRoaming(ifaceName, state);
}
/**
@@ -2325,19 +2603,21 @@ public class WifiNative {
/**
* Set firmware roaming configurations.
+ * @param ifaceName Name of the interface.
*/
- public boolean configureRoaming(RoamingConfig config) {
- Log.d(mTAG, "configureRoaming ");
- return mWifiVendorHal.configureRoaming(mInterfaceName, config);
+ public boolean configureRoaming(@NonNull String ifaceName, RoamingConfig config) {
+ Log.d(TAG, "configureRoaming ");
+ return mWifiVendorHal.configureRoaming(ifaceName, config);
}
/**
* Reset firmware roaming configuration.
+ * @param ifaceName Name of the interface.
*/
- public boolean resetRoamingConfiguration() {
+ public boolean resetRoamingConfiguration(@NonNull String ifaceName) {
// Pass in an empty RoamingConfig object which translates to zero size
// blacklist and whitelist to reset the firmware roaming configuration.
- return mWifiVendorHal.configureRoaming(mInterfaceName, new RoamingConfig());
+ return mWifiVendorHal.configureRoaming(ifaceName, new RoamingConfig());
}
/**
diff --git a/com/android/server/wifi/WifiNetworkHistory.java b/com/android/server/wifi/WifiNetworkHistory.java
new file mode 100644
index 00000000..282f6057
--- /dev/null
+++ b/com/android/server/wifi/WifiNetworkHistory.java
@@ -0,0 +1,630 @@
+/*
+ * 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.content.Context;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiConfiguration.NetworkSelectionStatus;
+import android.net.wifi.WifiSsid;
+import android.os.Environment;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.server.net.DelayedDiskWrite;
+
+import java.io.BufferedInputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.text.DateFormat;
+import java.util.BitSet;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * Provides an API to read and write the network history from WifiConfigurations to file
+ * This is largely separate and extra to the supplicant config file.
+ */
+public class WifiNetworkHistory {
+ public static final String TAG = "WifiNetworkHistory";
+ private static final boolean DBG = true;
+ private static final boolean VDBG = true;
+ static final String NETWORK_HISTORY_CONFIG_FILE = Environment.getDataDirectory()
+ + "/misc/wifi/networkHistory.txt";
+ /* Network History Keys */
+ private static final String SSID_KEY = "SSID";
+ static final String CONFIG_KEY = "CONFIG";
+ private static final String CONFIG_BSSID_KEY = "CONFIG_BSSID";
+ private static final String CHOICE_KEY = "CHOICE";
+ private static final String CHOICE_TIME_KEY = "CHOICE_TIME";
+ private static final String LINK_KEY = "LINK";
+ private static final String BSSID_KEY = "BSSID";
+ private static final String BSSID_KEY_END = "/BSSID";
+ private static final String RSSI_KEY = "RSSI";
+ private static final String FREQ_KEY = "FREQ";
+ private static final String DATE_KEY = "DATE";
+ private static final String MILLI_KEY = "MILLI";
+ private static final String NETWORK_ID_KEY = "ID";
+ private static final String PRIORITY_KEY = "PRIORITY";
+ private static final String DEFAULT_GW_KEY = "DEFAULT_GW";
+ private static final String AUTH_KEY = "AUTH";
+ private static final String BSSID_STATUS_KEY = "BSSID_STATUS";
+ private static final String SELF_ADDED_KEY = "SELF_ADDED";
+ private static final String DID_SELF_ADD_KEY = "DID_SELF_ADD";
+ private static final String PEER_CONFIGURATION_KEY = "PEER_CONFIGURATION";
+ static final String CREATOR_UID_KEY = "CREATOR_UID_KEY";
+ private static final String CONNECT_UID_KEY = "CONNECT_UID_KEY";
+ private static final String UPDATE_UID_KEY = "UPDATE_UID";
+ private static final String FQDN_KEY = "FQDN";
+ private static final String SCORER_OVERRIDE_KEY = "SCORER_OVERRIDE";
+ private static final String SCORER_OVERRIDE_AND_SWITCH_KEY = "SCORER_OVERRIDE_AND_SWITCH";
+ private static final String VALIDATED_INTERNET_ACCESS_KEY = "VALIDATED_INTERNET_ACCESS";
+ private static final String NO_INTERNET_ACCESS_REPORTS_KEY = "NO_INTERNET_ACCESS_REPORTS";
+ private static final String NO_INTERNET_ACCESS_EXPECTED_KEY = "NO_INTERNET_ACCESS_EXPECTED";
+ private static final String EPHEMERAL_KEY = "EPHEMERAL";
+ private static final String USE_EXTERNAL_SCORES_KEY = "USE_EXTERNAL_SCORES";
+ private static final String METERED_HINT_KEY = "METERED_HINT";
+ private static final String METERED_OVERRIDE_KEY = "METERED_OVERRIDE";
+ private static final String NUM_ASSOCIATION_KEY = "NUM_ASSOCIATION";
+ private static final String DELETED_EPHEMERAL_KEY = "DELETED_EPHEMERAL";
+ private static final String CREATOR_NAME_KEY = "CREATOR_NAME";
+ private static final String UPDATE_NAME_KEY = "UPDATE_NAME";
+ private static final String USER_APPROVED_KEY = "USER_APPROVED";
+ private static final String CREATION_TIME_KEY = "CREATION_TIME";
+ private static final String UPDATE_TIME_KEY = "UPDATE_TIME";
+ static final String SHARED_KEY = "SHARED";
+ private static final String NETWORK_SELECTION_STATUS_KEY = "NETWORK_SELECTION_STATUS";
+ private static final String NETWORK_SELECTION_DISABLE_REASON_KEY =
+ "NETWORK_SELECTION_DISABLE_REASON";
+ private static final String HAS_EVER_CONNECTED_KEY = "HAS_EVER_CONNECTED";
+
+ private static final String SEPARATOR = ": ";
+ private static final String NL = "\n";
+
+ protected final DelayedDiskWrite mWriter;
+ Context mContext;
+ /*
+ * Lost config list, whenever we read a config from networkHistory.txt that was not in
+ * wpa_supplicant.conf
+ */
+ HashSet<String> mLostConfigsDbg = new HashSet<String>();
+
+ public WifiNetworkHistory(Context c, DelayedDiskWrite writer) {
+ mContext = c;
+ mWriter = writer;
+ }
+
+ /**
+ * Write network history to file, for configured networks
+ *
+ * @param networks List of ConfiguredNetworks to write to NetworkHistory
+ */
+ public void writeKnownNetworkHistory(final List<WifiConfiguration> networks,
+ final ConcurrentHashMap<Integer, ScanDetailCache> scanDetailCaches,
+ final Set<String> deletedEphemeralSSIDs) {
+
+ /* Make a copy */
+ //final List<WifiConfiguration> networks = new ArrayList<WifiConfiguration>();
+
+ //for (WifiConfiguration config : mConfiguredNetworks.valuesForAllUsers()) {
+ // networks.add(new WifiConfiguration(config));
+ //}
+
+ mWriter.write(NETWORK_HISTORY_CONFIG_FILE, new DelayedDiskWrite.Writer() {
+ public void onWriteCalled(DataOutputStream out) throws IOException {
+ for (WifiConfiguration config : networks) {
+ //loge("onWriteCalled write SSID: " + config.SSID);
+ /* if (config.getLinkProperties() != null)
+ loge(" lp " + config.getLinkProperties().toString());
+ else
+ loge("attempt config w/o lp");
+ */
+ NetworkSelectionStatus status = config.getNetworkSelectionStatus();
+ if (VDBG) {
+ int numlink = 0;
+ if (config.linkedConfigurations != null) {
+ numlink = config.linkedConfigurations.size();
+ }
+ String disableTime;
+ if (config.getNetworkSelectionStatus().isNetworkEnabled()) {
+ disableTime = "";
+ } else {
+ disableTime = "Disable time: " + DateFormat.getInstance().format(
+ config.getNetworkSelectionStatus().getDisableTime());
+ }
+ logd("saving network history: " + config.configKey() + " gw: "
+ + config.defaultGwMacAddress + " Network Selection-status: "
+ + status.getNetworkStatusString()
+ + disableTime + " ephemeral=" + config.ephemeral
+ + " choice:" + status.getConnectChoice()
+ + " link:" + numlink
+ + " status:" + config.status
+ + " nid:" + config.networkId
+ + " hasEverConnected: " + status.getHasEverConnected());
+ }
+
+ if (!isValid(config)) {
+ continue;
+ }
+
+ if (config.SSID == null) {
+ if (VDBG) {
+ logv("writeKnownNetworkHistory trying to write config with null SSID");
+ }
+ continue;
+ }
+ if (VDBG) {
+ logv("writeKnownNetworkHistory write config " + config.configKey());
+ }
+ out.writeUTF(CONFIG_KEY + SEPARATOR + config.configKey() + NL);
+
+ if (config.SSID != null) {
+ out.writeUTF(SSID_KEY + SEPARATOR + config.SSID + NL);
+ }
+ if (config.BSSID != null) {
+ out.writeUTF(CONFIG_BSSID_KEY + SEPARATOR + config.BSSID + NL);
+ } else {
+ out.writeUTF(CONFIG_BSSID_KEY + SEPARATOR + "null" + NL);
+ }
+ if (config.FQDN != null) {
+ out.writeUTF(FQDN_KEY + SEPARATOR + config.FQDN + NL);
+ }
+
+ out.writeUTF(PRIORITY_KEY + SEPARATOR + Integer.toString(config.priority) + NL);
+ out.writeUTF(NETWORK_ID_KEY + SEPARATOR
+ + Integer.toString(config.networkId) + NL);
+ out.writeUTF(SELF_ADDED_KEY + SEPARATOR
+ + Boolean.toString(config.selfAdded) + NL);
+ out.writeUTF(DID_SELF_ADD_KEY + SEPARATOR
+ + Boolean.toString(config.didSelfAdd) + NL);
+ out.writeUTF(NO_INTERNET_ACCESS_REPORTS_KEY + SEPARATOR
+ + Integer.toString(config.numNoInternetAccessReports) + NL);
+ out.writeUTF(VALIDATED_INTERNET_ACCESS_KEY + SEPARATOR
+ + Boolean.toString(config.validatedInternetAccess) + NL);
+ out.writeUTF(NO_INTERNET_ACCESS_EXPECTED_KEY + SEPARATOR +
+ Boolean.toString(config.noInternetAccessExpected) + NL);
+ out.writeUTF(EPHEMERAL_KEY + SEPARATOR
+ + Boolean.toString(config.ephemeral) + NL);
+ out.writeUTF(METERED_HINT_KEY + SEPARATOR
+ + Boolean.toString(config.meteredHint) + NL);
+ out.writeUTF(METERED_OVERRIDE_KEY + SEPARATOR
+ + Integer.toString(config.meteredOverride) + NL);
+ out.writeUTF(USE_EXTERNAL_SCORES_KEY + SEPARATOR
+ + Boolean.toString(config.useExternalScores) + NL);
+ if (config.creationTime != null) {
+ out.writeUTF(CREATION_TIME_KEY + SEPARATOR + config.creationTime + NL);
+ }
+ if (config.updateTime != null) {
+ out.writeUTF(UPDATE_TIME_KEY + SEPARATOR + config.updateTime + NL);
+ }
+ if (config.peerWifiConfiguration != null) {
+ out.writeUTF(PEER_CONFIGURATION_KEY + SEPARATOR
+ + config.peerWifiConfiguration + NL);
+ }
+ out.writeUTF(SCORER_OVERRIDE_KEY + SEPARATOR
+ + Integer.toString(config.numScorerOverride) + NL);
+ out.writeUTF(SCORER_OVERRIDE_AND_SWITCH_KEY + SEPARATOR
+ + Integer.toString(config.numScorerOverrideAndSwitchedNetwork) + NL);
+ out.writeUTF(NUM_ASSOCIATION_KEY + SEPARATOR
+ + Integer.toString(config.numAssociation) + NL);
+ out.writeUTF(CREATOR_UID_KEY + SEPARATOR
+ + Integer.toString(config.creatorUid) + NL);
+ out.writeUTF(CONNECT_UID_KEY + SEPARATOR
+ + Integer.toString(config.lastConnectUid) + NL);
+ out.writeUTF(UPDATE_UID_KEY + SEPARATOR
+ + Integer.toString(config.lastUpdateUid) + NL);
+ out.writeUTF(CREATOR_NAME_KEY + SEPARATOR
+ + config.creatorName + NL);
+ out.writeUTF(UPDATE_NAME_KEY + SEPARATOR
+ + config.lastUpdateName + NL);
+ out.writeUTF(USER_APPROVED_KEY + SEPARATOR
+ + Integer.toString(config.userApproved) + NL);
+ out.writeUTF(SHARED_KEY + SEPARATOR + Boolean.toString(config.shared) + NL);
+ String allowedKeyManagementString =
+ makeString(config.allowedKeyManagement,
+ WifiConfiguration.KeyMgmt.strings);
+ out.writeUTF(AUTH_KEY + SEPARATOR
+ + allowedKeyManagementString + NL);
+ out.writeUTF(NETWORK_SELECTION_STATUS_KEY + SEPARATOR
+ + status.getNetworkSelectionStatus() + NL);
+ out.writeUTF(NETWORK_SELECTION_DISABLE_REASON_KEY + SEPARATOR
+ + status.getNetworkSelectionDisableReason() + NL);
+
+ if (status.getConnectChoice() != null) {
+ out.writeUTF(CHOICE_KEY + SEPARATOR + status.getConnectChoice() + NL);
+ out.writeUTF(CHOICE_TIME_KEY + SEPARATOR
+ + status.getConnectChoiceTimestamp() + NL);
+ }
+
+ if (config.linkedConfigurations != null) {
+ log("writeKnownNetworkHistory write linked "
+ + config.linkedConfigurations.size());
+
+ for (String key : config.linkedConfigurations.keySet()) {
+ out.writeUTF(LINK_KEY + SEPARATOR + key + NL);
+ }
+ }
+
+ String macAddress = config.defaultGwMacAddress;
+ if (macAddress != null) {
+ out.writeUTF(DEFAULT_GW_KEY + SEPARATOR + macAddress + NL);
+ }
+
+ if (getScanDetailCache(config, scanDetailCaches) != null) {
+ for (ScanDetail scanDetail : getScanDetailCache(config,
+ scanDetailCaches).values()) {
+ ScanResult result = scanDetail.getScanResult();
+ out.writeUTF(BSSID_KEY + SEPARATOR
+ + result.BSSID + NL);
+ out.writeUTF(FREQ_KEY + SEPARATOR
+ + Integer.toString(result.frequency) + NL);
+
+ out.writeUTF(RSSI_KEY + SEPARATOR
+ + Integer.toString(result.level) + NL);
+
+ out.writeUTF(BSSID_KEY_END + NL);
+ }
+ }
+ out.writeUTF(HAS_EVER_CONNECTED_KEY + SEPARATOR
+ + Boolean.toString(status.getHasEverConnected()) + NL);
+ out.writeUTF(NL);
+ // Add extra blank lines for clarity
+ out.writeUTF(NL);
+ out.writeUTF(NL);
+ }
+ if (deletedEphemeralSSIDs != null && deletedEphemeralSSIDs.size() > 0) {
+ for (String ssid : deletedEphemeralSSIDs) {
+ out.writeUTF(DELETED_EPHEMERAL_KEY);
+ out.writeUTF(ssid);
+ out.writeUTF(NL);
+ }
+ }
+ }
+ });
+ }
+
+ /**
+ * Adds information stored in networkHistory.txt to the given configs. The configs are provided
+ * as a mapping from configKey to WifiConfiguration, because the WifiConfigurations themselves
+ * do not contain sufficient information to compute their configKeys until after the information
+ * that is stored in networkHistory.txt has been added to them.
+ *
+ * @param configs mapping from configKey to a WifiConfiguration that contains the information
+ * information read from wpa_supplicant.conf
+ */
+ public void readNetworkHistory(Map<String, WifiConfiguration> configs,
+ Map<Integer, ScanDetailCache> scanDetailCaches,
+ Set<String> deletedEphemeralSSIDs) {
+
+ try (DataInputStream in =
+ new DataInputStream(new BufferedInputStream(
+ new FileInputStream(NETWORK_HISTORY_CONFIG_FILE)))) {
+
+ String bssid = null;
+ String ssid = null;
+
+ int freq = 0;
+ int status = 0;
+ long seen = 0;
+ int rssi = WifiConfiguration.INVALID_RSSI;
+ String caps = null;
+
+ WifiConfiguration config = null;
+ while (true) {
+ String line = in.readUTF();
+ if (line == null) {
+ break;
+ }
+ int colon = line.indexOf(':');
+ if (colon < 0) {
+ continue;
+ }
+
+ String key = line.substring(0, colon).trim();
+ String value = line.substring(colon + 1).trim();
+
+ if (key.equals(CONFIG_KEY)) {
+ config = configs.get(value);
+
+ // skip reading that configuration data
+ // since we don't have a corresponding network ID
+ if (config == null) {
+ Log.e(TAG, "readNetworkHistory didnt find netid for hash="
+ + Integer.toString(value.hashCode())
+ + " key: " + value);
+ mLostConfigsDbg.add(value);
+ continue;
+ } else {
+ // After an upgrade count old connections as owned by system
+ if (config.creatorName == null || config.lastUpdateName == null) {
+ config.creatorName =
+ mContext.getPackageManager().getNameForUid(Process.SYSTEM_UID);
+ config.lastUpdateName = config.creatorName;
+
+ if (DBG) {
+ Log.w(TAG, "Upgrading network " + config.networkId
+ + " to " + config.creatorName);
+ }
+ }
+ }
+ } else if (config != null) {
+ NetworkSelectionStatus networkStatus = config.getNetworkSelectionStatus();
+ switch (key) {
+ case SSID_KEY:
+ if (config.isPasspoint()) {
+ break;
+ }
+ ssid = value;
+ if (config.SSID != null && !config.SSID.equals(ssid)) {
+ loge("Error parsing network history file, mismatched SSIDs");
+ config = null; //error
+ ssid = null;
+ } else {
+ config.SSID = ssid;
+ }
+ break;
+ case CONFIG_BSSID_KEY:
+ config.BSSID = value.equals("null") ? null : value;
+ break;
+ case FQDN_KEY:
+ // Check for literal 'null' to be backwards compatible.
+ config.FQDN = value.equals("null") ? null : value;
+ break;
+ case DEFAULT_GW_KEY:
+ config.defaultGwMacAddress = value;
+ break;
+ case SELF_ADDED_KEY:
+ config.selfAdded = Boolean.parseBoolean(value);
+ break;
+ case DID_SELF_ADD_KEY:
+ config.didSelfAdd = Boolean.parseBoolean(value);
+ break;
+ case NO_INTERNET_ACCESS_REPORTS_KEY:
+ config.numNoInternetAccessReports = Integer.parseInt(value);
+ break;
+ case VALIDATED_INTERNET_ACCESS_KEY:
+ config.validatedInternetAccess = Boolean.parseBoolean(value);
+ break;
+ case NO_INTERNET_ACCESS_EXPECTED_KEY:
+ config.noInternetAccessExpected = Boolean.parseBoolean(value);
+ break;
+ case CREATION_TIME_KEY:
+ config.creationTime = value;
+ break;
+ case UPDATE_TIME_KEY:
+ config.updateTime = value;
+ break;
+ case EPHEMERAL_KEY:
+ config.ephemeral = Boolean.parseBoolean(value);
+ break;
+ case METERED_HINT_KEY:
+ config.meteredHint = Boolean.parseBoolean(value);
+ break;
+ case METERED_OVERRIDE_KEY:
+ config.meteredOverride = Integer.parseInt(value);
+ break;
+ case USE_EXTERNAL_SCORES_KEY:
+ config.useExternalScores = Boolean.parseBoolean(value);
+ break;
+ case CREATOR_UID_KEY:
+ config.creatorUid = Integer.parseInt(value);
+ break;
+ case SCORER_OVERRIDE_KEY:
+ config.numScorerOverride = Integer.parseInt(value);
+ break;
+ case SCORER_OVERRIDE_AND_SWITCH_KEY:
+ config.numScorerOverrideAndSwitchedNetwork = Integer.parseInt(value);
+ break;
+ case NUM_ASSOCIATION_KEY:
+ config.numAssociation = Integer.parseInt(value);
+ break;
+ case CONNECT_UID_KEY:
+ config.lastConnectUid = Integer.parseInt(value);
+ break;
+ case UPDATE_UID_KEY:
+ config.lastUpdateUid = Integer.parseInt(value);
+ break;
+ case PEER_CONFIGURATION_KEY:
+ config.peerWifiConfiguration = value;
+ break;
+ case NETWORK_SELECTION_STATUS_KEY:
+ int networkStatusValue = Integer.parseInt(value);
+ // Reset temporarily disabled network status
+ if (networkStatusValue ==
+ NetworkSelectionStatus.NETWORK_SELECTION_TEMPORARY_DISABLED) {
+ networkStatusValue =
+ NetworkSelectionStatus.NETWORK_SELECTION_ENABLED;
+ }
+ networkStatus.setNetworkSelectionStatus(networkStatusValue);
+ break;
+ case NETWORK_SELECTION_DISABLE_REASON_KEY:
+ networkStatus.setNetworkSelectionDisableReason(Integer.parseInt(value));
+ break;
+ case CHOICE_KEY:
+ networkStatus.setConnectChoice(value);
+ break;
+ case CHOICE_TIME_KEY:
+ networkStatus.setConnectChoiceTimestamp(Long.parseLong(value));
+ break;
+ case LINK_KEY:
+ if (config.linkedConfigurations == null) {
+ config.linkedConfigurations = new HashMap<>();
+ } else {
+ config.linkedConfigurations.put(value, -1);
+ }
+ break;
+ case BSSID_KEY:
+ status = 0;
+ ssid = null;
+ bssid = null;
+ freq = 0;
+ seen = 0;
+ rssi = WifiConfiguration.INVALID_RSSI;
+ caps = "";
+ break;
+ case RSSI_KEY:
+ rssi = Integer.parseInt(value);
+ break;
+ case FREQ_KEY:
+ freq = Integer.parseInt(value);
+ break;
+ case DATE_KEY:
+ /*
+ * when reading the configuration from file we don't update the date
+ * so as to avoid reading back stale or non-sensical data that would
+ * depend on network time.
+ * The date of a WifiConfiguration should only come from actual scan
+ * result.
+ *
+ String s = key.replace(FREQ_KEY, "");
+ seen = Integer.getInteger(s);
+ */
+ break;
+ case BSSID_KEY_END:
+ if ((bssid != null) && (ssid != null)) {
+ if (getScanDetailCache(config, scanDetailCaches) != null) {
+ WifiSsid wssid = WifiSsid.createFromAsciiEncoded(ssid);
+ ScanDetail scanDetail = new ScanDetail(wssid, bssid,
+ caps, rssi, freq, (long) 0, seen);
+ getScanDetailCache(config, scanDetailCaches).put(scanDetail);
+ }
+ }
+ break;
+ case DELETED_EPHEMERAL_KEY:
+ if (!TextUtils.isEmpty(value)) {
+ deletedEphemeralSSIDs.add(value);
+ }
+ break;
+ case CREATOR_NAME_KEY:
+ config.creatorName = value;
+ break;
+ case UPDATE_NAME_KEY:
+ config.lastUpdateName = value;
+ break;
+ case USER_APPROVED_KEY:
+ config.userApproved = Integer.parseInt(value);
+ break;
+ case SHARED_KEY:
+ config.shared = Boolean.parseBoolean(value);
+ break;
+ case HAS_EVER_CONNECTED_KEY:
+ networkStatus.setHasEverConnected(Boolean.parseBoolean(value));
+ break;
+ }
+ }
+ }
+ } catch (EOFException e) {
+ // do nothing
+ } catch (FileNotFoundException e) {
+ Log.i(TAG, "readNetworkHistory: no config file, " + e);
+ } catch (NumberFormatException e) {
+ Log.e(TAG, "readNetworkHistory: failed to parse, " + e, e);
+ } catch (IOException e) {
+ Log.e(TAG, "readNetworkHistory: failed to read, " + e, e);
+ }
+ }
+
+ /**
+ * Ported this out of WifiServiceImpl, I have no idea what it's doing
+ * <TODO> figure out what/why this is doing
+ * <TODO> Port it into WifiConfiguration, then remove all the silly business from ServiceImpl
+ */
+ public boolean isValid(WifiConfiguration config) {
+ if (config.allowedKeyManagement == null) {
+ return false;
+ }
+ if (config.allowedKeyManagement.cardinality() > 1) {
+ if (config.allowedKeyManagement.cardinality() != 2) {
+ return false;
+ }
+ if (!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_EAP)) {
+ return false;
+ }
+ if ((!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.IEEE8021X))
+ && (!config.allowedKeyManagement.get(WifiConfiguration.KeyMgmt.WPA_PSK))) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private static String makeString(BitSet set, String[] strings) {
+ StringBuffer buf = new StringBuffer();
+ int nextSetBit = -1;
+
+ /* Make sure all set bits are in [0, strings.length) to avoid
+ * going out of bounds on strings. (Shouldn't happen, but...) */
+ set = set.get(0, strings.length);
+
+ while ((nextSetBit = set.nextSetBit(nextSetBit + 1)) != -1) {
+ buf.append(strings[nextSetBit].replace('_', '-')).append(' ');
+ }
+
+ // remove trailing space
+ if (set.cardinality() > 0) {
+ buf.setLength(buf.length() - 1);
+ }
+
+ return buf.toString();
+ }
+
+ protected void logv(String s) {
+ Log.v(TAG, s);
+ }
+ protected void logd(String s) {
+ Log.d(TAG, s);
+ }
+ protected void log(String s) {
+ Log.d(TAG, s);
+ }
+ protected void loge(String s) {
+ loge(s, false);
+ }
+ protected void loge(String s, boolean stack) {
+ if (stack) {
+ Log.e(TAG, s + " stack:" + Thread.currentThread().getStackTrace()[2].getMethodName()
+ + " - " + Thread.currentThread().getStackTrace()[3].getMethodName()
+ + " - " + Thread.currentThread().getStackTrace()[4].getMethodName()
+ + " - " + Thread.currentThread().getStackTrace()[5].getMethodName());
+ } else {
+ Log.e(TAG, s);
+ }
+ }
+
+ private ScanDetailCache getScanDetailCache(WifiConfiguration config,
+ Map<Integer, ScanDetailCache> scanDetailCaches) {
+ if (config == null || scanDetailCaches == null) return null;
+ ScanDetailCache cache = scanDetailCaches.get(config.networkId);
+ if (cache == null && config.networkId != WifiConfiguration.INVALID_NETWORK_ID) {
+ cache =
+ new ScanDetailCache(
+ config, WifiConfigManager.SCAN_CACHE_ENTRIES_MAX_SIZE,
+ WifiConfigManager.SCAN_CACHE_ENTRIES_TRIM_SIZE);
+ scanDetailCaches.put(config.networkId, cache);
+ }
+ return cache;
+ }
+}
diff --git a/com/android/server/wifi/WifiNetworkSelector.java b/com/android/server/wifi/WifiNetworkSelector.java
index 46ded1cc..dc854097 100644
--- a/com/android/server/wifi/WifiNetworkSelector.java
+++ b/com/android/server/wifi/WifiNetworkSelector.java
@@ -360,6 +360,36 @@ public class WifiNetworkSelector {
}
/**
+ * This returns a list of ScanDetails that were filtered in the process of network selection.
+ * The list is further filtered for only carrier unsaved networks with EAP encryption.
+ *
+ * @param carrierConfig CarrierNetworkConfig used to filter carrier networks
+ * @return the list of ScanDetails for carrier unsaved networks that do not have invalid SSIDS,
+ * blacklisted BSSIDS, or low signal strength, and with EAP encryption. This will return an
+ * empty list when there are no such networks, or when network selection has not been run.
+ */
+ public List<ScanDetail> getFilteredScanDetailsForCarrierUnsavedNetworks(
+ CarrierNetworkConfig carrierConfig) {
+ List<ScanDetail> carrierUnsavedNetworks = new ArrayList<>();
+ for (ScanDetail scanDetail : mFilteredNetworks) {
+ ScanResult scanResult = scanDetail.getScanResult();
+
+ if (!ScanResultUtil.isScanResultForEapNetwork(scanResult)
+ || !carrierConfig.isCarrierNetwork(scanResult.SSID)) {
+ continue;
+ }
+
+ // Skip saved networks
+ if (mWifiConfigManager.getConfiguredNetworkForScanDetailAndCache(scanDetail) != null) {
+ continue;
+ }
+
+ carrierUnsavedNetworks.add(scanDetail);
+ }
+ return carrierUnsavedNetworks;
+ }
+
+ /**
* @return the list of ScanDetails scored as potential candidates by the last run of
* selectNetwork, this will be empty if Network selector determined no selection was
* needed on last run. This includes scan details of sufficient signal strength, and
diff --git a/com/android/server/wifi/WifiServiceImpl.java b/com/android/server/wifi/WifiServiceImpl.java
index ce49e559..d4bd12e5 100644
--- a/com/android/server/wifi/WifiServiceImpl.java
+++ b/com/android/server/wifi/WifiServiceImpl.java
@@ -16,6 +16,7 @@
package com.android.server.wifi;
+import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.net.wifi.WifiManager.EXTRA_PREVIOUS_WIFI_AP_STATE;
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_FAILURE_REASON;
import static android.net.wifi.WifiManager.EXTRA_WIFI_AP_INTERFACE_NAME;
@@ -30,18 +31,15 @@ import static android.net.wifi.WifiManager.WIFI_AP_STATE_FAILED;
import static com.android.server.wifi.LocalOnlyHotspotRequestInfo.HOTSPOT_NO_ERROR;
import static com.android.server.wifi.WifiController.CMD_AIRPLANE_TOGGLED;
-import static com.android.server.wifi.WifiController.CMD_BATTERY_CHANGED;
import static com.android.server.wifi.WifiController.CMD_EMERGENCY_CALL_STATE_CHANGED;
import static com.android.server.wifi.WifiController.CMD_EMERGENCY_MODE_CHANGED;
-import static com.android.server.wifi.WifiController.CMD_LOCKS_CHANGED;
import static com.android.server.wifi.WifiController.CMD_SCAN_ALWAYS_MODE_CHANGED;
-import static com.android.server.wifi.WifiController.CMD_SCREEN_OFF;
-import static com.android.server.wifi.WifiController.CMD_SCREEN_ON;
import static com.android.server.wifi.WifiController.CMD_SET_AP;
import static com.android.server.wifi.WifiController.CMD_USER_PRESENT;
import static com.android.server.wifi.WifiController.CMD_WIFI_TOGGLED;
import android.Manifest;
+import android.annotation.CheckResult;
import android.app.ActivityManager;
import android.app.ActivityManager.RunningAppProcessInfo;
import android.app.AppOpsManager;
@@ -62,16 +60,15 @@ import android.net.NetworkUtils;
import android.net.StaticIpConfiguration;
import android.net.Uri;
import android.net.ip.IpClient;
+import android.net.wifi.ISoftApCallback;
import android.net.wifi.IWifiManager;
import android.net.wifi.ScanResult;
import android.net.wifi.ScanSettings;
import android.net.wifi.WifiActivityEnergyInfo;
import android.net.wifi.WifiConfiguration;
-import android.net.wifi.WifiConnectionStatistics;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiManager.LocalOnlyHotspotCallback;
-import android.net.wifi.WifiScanner;
import android.net.wifi.hotspot2.IProvisioningCallback;
import android.net.wifi.hotspot2.OsuProvider;
import android.net.wifi.hotspot2.PasspointConfiguration;
@@ -102,6 +99,7 @@ import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.PowerProfile;
import com.android.internal.telephony.IccCardConstants;
import com.android.internal.telephony.PhoneConstants;
import com.android.internal.telephony.TelephonyIntents;
@@ -128,6 +126,7 @@ import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
@@ -150,6 +149,7 @@ public class WifiServiceImpl extends IWifiManager.Stub {
RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
final WifiStateMachine mWifiStateMachine;
+ final ScanRequestProxy mScanRequestProxy;
private final Context mContext;
private final FrameworkFacade mFacade;
@@ -179,7 +179,6 @@ public class WifiServiceImpl extends IWifiManager.Stub {
// Map of package name of background scan apps and last scan timestamp.
private final ArrayMap<String, Long> mLastScanTimestamps;
- private WifiScanner mWifiScanner;
private WifiLog mLog;
/**
@@ -199,6 +198,11 @@ public class WifiServiceImpl extends IWifiManager.Stub {
@GuardedBy("mLocalOnlyHotspotRequests")
private final ConcurrentHashMap<String, Integer> mIfaceIpModes;
+ /* Limit on number of registered soft AP callbacks to track and prevent potential memory leak */
+ private static final int NUM_SOFT_AP_CALLBACKS_WARN_LIMIT = 10;
+ private static final int NUM_SOFT_AP_CALLBACKS_WTF_LIMIT = 20;
+ private final HashMap<Integer, ISoftApCallback> mRegisteredSoftApCallbacks;
+
/**
* One of: {@link WifiManager#WIFI_AP_STATE_DISABLED},
* {@link WifiManager#WIFI_AP_STATE_DISABLING},
@@ -208,8 +212,17 @@ public class WifiServiceImpl extends IWifiManager.Stub {
*
* Access/maintenance MUST be done on the wifi service thread
*/
+ // TODO: (b/71714381) Remove mWifiApState and broadcast mechanism, keep mSoftApState as the only
+ // field to store soft AP state. Then rename mSoftApState and mSoftApNumClients to
+ // mWifiApState and mWifiApNumClients, to match the constants (i.e. WIFI_AP_STATE_*)
private int mWifiApState = WifiManager.WIFI_AP_STATE_DISABLED;
+ private int mSoftApState = WifiManager.WIFI_AP_STATE_DISABLED;
+ private int mSoftApNumClients = 0;
+ /**
+ * Power profile
+ */
+ PowerProfile mPowerProfile;
/**
* Callback for use with LocalOnlyHotspot to unregister requesting applications upon death.
@@ -440,6 +453,7 @@ public class WifiServiceImpl extends IWifiManager.Stub {
mCountryCode = mWifiInjector.getWifiCountryCode();
mWifiStateMachine = mWifiInjector.getWifiStateMachine();
mWifiStateMachine.enableRssiPolling(true);
+ mScanRequestProxy = mWifiInjector.getScanRequestProxy();
mSettingsStore = mWifiInjector.getWifiSettingsStore();
mPowerManager = mContext.getSystemService(PowerManager.class);
mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
@@ -464,6 +478,10 @@ public class WifiServiceImpl extends IWifiManager.Stub {
mIfaceIpModes = new ConcurrentHashMap<>();
mLocalOnlyHotspotRequests = new HashMap<>();
enableVerboseLoggingInternal(getVerboseLoggingLevel());
+ mRegisteredSoftApCallbacks = new HashMap<>();
+
+ mWifiInjector.getWifiStateMachinePrime().registerSoftApCallback(new SoftApCallbackImpl());
+ mPowerProfile = mWifiInjector.getPowerProfile();
}
/**
@@ -591,12 +609,16 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* @param settings If null, use default parameter, i.e. full scan.
* @param workSource If null, all blame is given to the calling uid.
* @param packageName Package name of the app that requests wifi scan.
+ * TODO(b/68388459): Remove |settings| & |worksource|
*/
@Override
public void startScan(ScanSettings settings, WorkSource workSource, String packageName) {
- enforceChangePermission();
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return;
+ }
- mLog.info("startScan uid=%").c(Binder.getCallingUid()).flush();
+ int callingUid = Binder.getCallingUid();
+ mLog.info("startScan uid=%").c(callingUid).flush();
// Check and throttle background apps for wifi scan.
if (isRequestFromBackground(packageName)) {
long lastScanMs = mLastScanTimestamps.getOrDefault(packageName, 0L);
@@ -610,9 +632,6 @@ public class WifiServiceImpl extends IWifiManager.Stub {
mLastScanTimestamps.put(packageName, elapsedRealtime);
}
synchronized (this) {
- if (mWifiScanner == null) {
- mWifiScanner = mWifiInjector.getWifiScanner();
- }
if (mInIdleMode) {
// Need to send an immediate scan result broadcast in case the
// caller is waiting for a result ..
@@ -626,24 +645,15 @@ public class WifiServiceImpl extends IWifiManager.Stub {
return;
}
}
- if (settings != null) {
- settings = new ScanSettings(settings);
- if (!settings.isValid()) {
- Slog.e(TAG, "invalid scan setting");
- return;
+ boolean success = mWifiInjector.getWifiStateMachineHandler().runWithScissors(() -> {
+ if (!mScanRequestProxy.startScan(callingUid)) {
+ Log.e(TAG, "Failed to start scan");
}
+ }, 0);
+ if (!success) {
+ Log.e(TAG, "Failed to post runnable to start scan");
+ sendFailedScanBroadcast();
}
- if (workSource != null) {
- enforceWorkSourcePermission();
- // WifiManager currently doesn't use names, so need to clear names out of the
- // supplied WorkSource to allow future WorkSource combining.
- workSource.clearNames();
- }
- if (workSource == null && Binder.getCallingUid() >= 0) {
- workSource = new WorkSource(Binder.getCallingUid());
- }
- mWifiStateMachine.startScan(Binder.getCallingUid(), scanRequestCounter++,
- settings, workSource);
}
// Send a failed scan broadcast to indicate the current scan request failed.
@@ -713,8 +723,9 @@ public class WifiServiceImpl extends IWifiManager.Stub {
}
if (doScan) {
// Someone requested a scan while we were idle; do a full scan now.
- // The package name doesn't matter as the request comes from System UID.
- startScan(null, null, "");
+ // A security check of the caller's identity was made when the request arrived via
+ // Binder.
+ startScan(null, null, mContext.getOpPackageName());
}
}
@@ -738,9 +749,21 @@ public class WifiServiceImpl extends IWifiManager.Stub {
"WifiService");
}
- private void enforceChangePermission() {
+ /**
+ * Checks whether the caller can change the wifi state.
+ * Possible results:
+ * 1. Operation is allowed. No exception thrown, and AppOpsManager.MODE_ALLOWED returned.
+ * 2. Operation is not allowed, and caller must be told about this. SecurityException is thrown.
+ * 3. Operation is not allowed, and caller must not be told about this (i.e. must silently
+ * ignore the operation). No exception is thrown, and AppOpsManager.MODE_IGNORED returned.
+ */
+ @CheckResult
+ private int enforceChangePermission(String callingPackage) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.CHANGE_WIFI_STATE,
"WifiService");
+
+ return mAppOps.noteOp(
+ AppOpsManager.OPSTR_CHANGE_WIFI_STATE, Binder.getCallingUid(), callingPackage);
}
private void enforceLocationHardwarePermission() {
@@ -784,7 +807,10 @@ public class WifiServiceImpl extends IWifiManager.Stub {
@Override
public synchronized boolean setWifiEnabled(String packageName, boolean enable)
throws RemoteException {
- enforceChangePermission();
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return false;
+ }
+
Slog.d(TAG, "setWifiEnabled: " + enable + " pid=" + Binder.getCallingPid()
+ ", uid=" + Binder.getCallingUid() + ", package=" + packageName);
mLog.info("setWifiEnabled package=% uid=% enable=%").c(packageName)
@@ -1038,6 +1064,141 @@ public class WifiServiceImpl extends IWifiManager.Stub {
}
/**
+ * Callback to use with WifiStateMachine to receive events from WifiStateMachine
+ *
+ * @hide
+ */
+ private final class SoftApCallbackImpl implements WifiManager.SoftApCallback {
+ /**
+ * Called when soft AP state changes.
+ *
+ * @param state new new AP state. One of {@link #WIFI_AP_STATE_DISABLED},
+ * {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED},
+ * {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
+ * @param failureReason reason when in failed state. One of
+ * {@link #SAP_START_FAILURE_GENERAL}, {@link #SAP_START_FAILURE_NO_CHANNEL}
+ */
+ @Override
+ public void onStateChanged(int state, int failureReason) {
+ mSoftApState = state;
+ mSoftApNumClients = 0;
+
+ Iterator<ISoftApCallback> iterator = mRegisteredSoftApCallbacks.values().iterator();
+ while (iterator.hasNext()) {
+ ISoftApCallback callback = iterator.next();
+ try {
+ callback.onStateChanged(state, failureReason);
+ } catch (RemoteException e) {
+ Log.e(TAG, "onStateChanged: remote exception -- " + e);
+ iterator.remove();
+ }
+ }
+ }
+
+ /**
+ * Called when number of connected clients to soft AP changes.
+ *
+ * @param numClients number of connected clients to soft AP
+ */
+ @Override
+ public void onNumClientsChanged(int numClients) {
+ mSoftApNumClients = numClients;
+
+ Iterator<ISoftApCallback> iterator = mRegisteredSoftApCallbacks.values().iterator();
+ while (iterator.hasNext()) {
+ ISoftApCallback callback = iterator.next();
+ try {
+ callback.onNumClientsChanged(numClients);
+ } catch (RemoteException e) {
+ Log.e(TAG, "onNumClientsChanged: remote exception -- " + e);
+ iterator.remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * see {@link android.net.wifi.WifiManager#registerSoftApCallback(SoftApCallback, Handler)}
+ *
+ * @param binder IBinder instance to allow cleanup if the app dies
+ * @param callback Soft AP callback to register
+ * @param callbackIdentifier Unique ID of the registering callback. This ID will be used to
+ * unregister the callback. See {@link unregisterSoftApCallback(int)}
+ *
+ * @throws SecurityException if the caller does not have permission to register a callback
+ * @throws RemoteException if remote exception happens
+ * @throws IllegalArgumentException if the arguments are null or invalid
+ */
+ @Override
+ public void registerSoftApCallback(IBinder binder, ISoftApCallback callback,
+ int callbackIdentifier) {
+ // verify arguments
+ if (binder == null) {
+ throw new IllegalArgumentException("Binder must not be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("Callback must not be null");
+ }
+
+ enforceNetworkSettingsPermission();
+
+ // register for binder death
+ IBinder.DeathRecipient dr = new IBinder.DeathRecipient() {
+ @Override
+ public void binderDied() {
+ binder.unlinkToDeath(this, 0);
+ mClientHandler.post(() -> {
+ mRegisteredSoftApCallbacks.remove(callbackIdentifier);
+ });
+ }
+ };
+ try {
+ binder.linkToDeath(dr, 0);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error on linkToDeath - " + e);
+ return;
+ }
+
+ // post operation to handler thread
+ mClientHandler.post(() -> {
+ mRegisteredSoftApCallbacks.put(callbackIdentifier, callback);
+
+ if (mRegisteredSoftApCallbacks.size() > NUM_SOFT_AP_CALLBACKS_WTF_LIMIT) {
+ Log.wtf(TAG, "Too many soft AP callbacks: " + mRegisteredSoftApCallbacks.size());
+ } else if (mRegisteredSoftApCallbacks.size() > NUM_SOFT_AP_CALLBACKS_WARN_LIMIT) {
+ Log.w(TAG, "Too many soft AP callbacks: " + mRegisteredSoftApCallbacks.size());
+ }
+
+ // Update the client about the current state immediately after registering the callback
+ try {
+ callback.onStateChanged(mSoftApState, 0);
+ callback.onNumClientsChanged(mSoftApNumClients);
+ } catch (RemoteException e) {
+ Log.e(TAG, "registerSoftApCallback: remote exception -- " + e);
+ }
+
+ });
+ }
+
+ /**
+ * see {@link android.net.wifi.WifiManager#unregisterSoftApCallback(SoftApCallback)}
+ *
+ * @param callbackIdentifier Unique ID of the callback to be unregistered.
+ *
+ * @throws SecurityException if the caller does not have permission to register a callback
+ */
+ @Override
+ public void unregisterSoftApCallback(int callbackIdentifier) {
+
+ enforceNetworkSettingsPermission();
+
+ // post operation to handler thread
+ mClientHandler.post(() -> {
+ mRegisteredSoftApCallbacks.remove(callbackIdentifier);
+ });
+ }
+
+ /**
* Private method to handle SoftAp state changes
*
* <p> MUST be called from the WifiStateMachine thread.
@@ -1188,7 +1349,9 @@ public class WifiServiceImpl extends IWifiManager.Stub {
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
- enforceChangePermission();
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return LocalOnlyHotspotCallback.ERROR_GENERIC;
+ }
enforceLocationPermission(packageName, uid);
// also need to verify that Locations services are enabled.
if (mSettingsStore.getLocationModeSetting(mContext) == Settings.Secure.LOCATION_MODE_OFF) {
@@ -1261,8 +1424,10 @@ public class WifiServiceImpl extends IWifiManager.Stub {
*/
@Override
public void stopLocalOnlyHotspot() {
- // first check if the caller has permission to stop a local only hotspot
- enforceChangePermission();
+ // don't do a permission check here. if the app has their permission to change the wifi
+ // state revoked, we still want them to be able to stop a previously created hotspot
+ // (otherwise it could cost the user money). When the app created the hotspot, its
+ // permission was checked.
final int uid = Binder.getCallingUid();
final int pid = Binder.getCallingPid();
@@ -1320,8 +1485,6 @@ public class WifiServiceImpl extends IWifiManager.Stub {
*/
@Override
public void startWatchLocalOnlyHotspot(Messenger messenger, IBinder binder) {
- final String packageName = mContext.getOpPackageName();
-
// NETWORK_SETTINGS is a signature only permission.
enforceNetworkSettingsPermission();
@@ -1364,8 +1527,10 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* @throws SecurityException if the caller does not have permission to write the sotap config
*/
@Override
- public void setWifiApConfiguration(WifiConfiguration wifiConfig) {
- enforceChangePermission();
+ public void setWifiApConfiguration(WifiConfiguration wifiConfig, String packageName) {
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return;
+ }
int uid = Binder.getCallingUid();
// only allow Settings UI to write the stored SoftApConfig
if (!mWifiPermissionsUtil.checkConfigOverridePermission(uid)) {
@@ -1397,8 +1562,10 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* see {@link android.net.wifi.WifiManager#disconnect()}
*/
@Override
- public void disconnect() {
- enforceChangePermission();
+ public void disconnect(String packageName) {
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return;
+ }
mLog.info("disconnect uid=%").c(Binder.getCallingUid()).flush();
mWifiStateMachine.disconnectCommand();
}
@@ -1407,8 +1574,10 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* see {@link android.net.wifi.WifiManager#reconnect()}
*/
@Override
- public void reconnect() {
- enforceChangePermission();
+ public void reconnect(String packageName) {
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return;
+ }
mLog.info("reconnect uid=%").c(Binder.getCallingUid()).flush();
mWifiStateMachine.reconnectCommand(new WorkSource(Binder.getCallingUid()));
}
@@ -1417,8 +1586,10 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* see {@link android.net.wifi.WifiManager#reassociate()}
*/
@Override
- public void reassociate() {
- enforceChangePermission();
+ public void reassociate(String packageName) {
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return;
+ }
mLog.info("reassociate uid=%").c(Binder.getCallingUid()).flush();
mWifiStateMachine.reassociateCommand();
}
@@ -1461,16 +1632,14 @@ public class WifiServiceImpl extends IWifiManager.Stub {
if (mWifiStateMachineChannel != null) {
stats = mWifiStateMachine.syncGetLinkLayerStats(mWifiStateMachineChannel);
if (stats != null) {
- final long rxIdleCurrent = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_wifi_idle_receive_cur_ma);
- final long rxCurrent = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_wifi_active_rx_cur_ma);
- final long txCurrent = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_wifi_tx_cur_ma);
- final double voltage = mContext.getResources().getInteger(
- com.android.internal.R.integer.config_wifi_operating_voltage_mv)
- / 1000.0;
-
+ final double rxIdleCurrent = mPowerProfile.getAveragePower(
+ PowerProfile.POWER_WIFI_CONTROLLER_IDLE);
+ final double rxCurrent = mPowerProfile.getAveragePower(
+ PowerProfile.POWER_WIFI_CONTROLLER_RX);
+ final double txCurrent = mPowerProfile.getAveragePower(
+ PowerProfile.POWER_WIFI_CONTROLLER_TX);
+ final double voltage = mPowerProfile.getAveragePower(
+ PowerProfile.POWER_WIFI_CONTROLLER_OPERATING_VOLTAGE) / 1000.0;
final long rxIdleTime = stats.on_time - stats.tx_time - stats.rx_time;
final long[] txTimePerLevel;
if (stats.tx_time_per_level != null) {
@@ -1487,7 +1656,7 @@ public class WifiServiceImpl extends IWifiManager.Stub {
stats.rx_time * rxCurrent +
rxIdleTime * rxIdleCurrent) * voltage);
if (VDBG || rxIdleTime < 0 || stats.on_time < 0 || stats.tx_time < 0 ||
- stats.rx_time < 0 || energyUsed < 0) {
+ stats.rx_time < 0 || stats.on_time_scan < 0 || energyUsed < 0) {
StringBuilder sb = new StringBuilder();
sb.append(" rxIdleCur=" + rxIdleCurrent);
sb.append(" rxCur=" + rxCurrent);
@@ -1498,6 +1667,7 @@ public class WifiServiceImpl extends IWifiManager.Stub {
sb.append(" tx_time_per_level=" + Arrays.toString(txTimePerLevel));
sb.append(" rx_time=" + stats.rx_time);
sb.append(" rxIdleTime=" + rxIdleTime);
+ sb.append(" scan_time=" + stats.on_time_scan);
sb.append(" energy=" + energyUsed);
Log.d(TAG, " reportActivityInfo: " + sb.toString());
}
@@ -1505,7 +1675,7 @@ public class WifiServiceImpl extends IWifiManager.Stub {
// Convert the LinkLayerStats into EnergyActivity
energyInfo = new WifiActivityEnergyInfo(mClock.getElapsedSinceBootMillis(),
WifiActivityEnergyInfo.STACK_STATE_STATE_IDLE, stats.tx_time,
- txTimePerLevel, stats.rx_time, rxIdleTime, energyUsed);
+ txTimePerLevel, stats.rx_time, stats.on_time_scan, rxIdleTime, energyUsed);
}
if (energyInfo != null && energyInfo.isValid()) {
return energyInfo;
@@ -1619,8 +1789,10 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* network if the operation succeeds, or {@code -1} if it fails
*/
@Override
- public int addOrUpdateNetwork(WifiConfiguration config) {
- enforceChangePermission();
+ public int addOrUpdateNetwork(WifiConfiguration config, String packageName) {
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return -1;
+ }
mLog.info("addOrUpdateNetwork uid=%").c(Binder.getCallingUid()).flush();
// Previously, this API is overloaded for installing Passpoint profiles. Now
@@ -1639,7 +1811,7 @@ public class WifiServiceImpl extends IWifiManager.Stub {
config.enterpriseConfig.getClientCertificateChain());
passpointConfig.getCredential().setClientPrivateKey(
config.enterpriseConfig.getClientPrivateKey());
- if (!addOrUpdatePasspointConfiguration(passpointConfig)) {
+ if (!addOrUpdatePasspointConfiguration(passpointConfig, packageName)) {
Slog.e(TAG, "Failed to add Passpoint profile");
return -1;
}
@@ -1690,8 +1862,10 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* @return {@code true} if the operation succeeded
*/
@Override
- public boolean removeNetwork(int netId) {
- enforceChangePermission();
+ public boolean removeNetwork(int netId, String packageName) {
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return false;
+ }
mLog.info("removeNetwork uid=%").c(Binder.getCallingUid()).flush();
// TODO Add private logging for netId b/33807876
if (mWifiStateMachineChannel != null) {
@@ -1710,8 +1884,10 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* @return {@code true} if the operation succeeded
*/
@Override
- public boolean enableNetwork(int netId, boolean disableOthers) {
- enforceChangePermission();
+ public boolean enableNetwork(int netId, boolean disableOthers, String packageName) {
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return false;
+ }
// TODO b/33807876 Log netId
mLog.info("enableNetwork uid=% disableOthers=%")
.c(Binder.getCallingUid())
@@ -1733,8 +1909,10 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* @return {@code true} if the operation succeeded
*/
@Override
- public boolean disableNetwork(int netId) {
- enforceChangePermission();
+ public boolean disableNetwork(int netId, String packageName) {
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return false;
+ }
// TODO b/33807876 Log netId
mLog.info("disableNetwork uid=%").c(Binder.getCallingUid()).flush();
@@ -1779,14 +1957,19 @@ public class WifiServiceImpl extends IWifiManager.Stub {
int uid = Binder.getCallingUid();
long ident = Binder.clearCallingIdentity();
try {
+ // TODO: Remove the bypass for apps targeting older SDK's (< M).
if (!mWifiPermissionsUtil.canAccessScanResults(callingPackage,
uid, Build.VERSION_CODES.M)) {
return new ArrayList<ScanResult>();
}
- if (mWifiScanner == null) {
- mWifiScanner = mWifiInjector.getWifiScanner();
+ final List<ScanResult> scanResults = new ArrayList<>();
+ boolean success = mWifiInjector.getWifiStateMachineHandler().runWithScissors(() -> {
+ scanResults.addAll(mScanRequestProxy.getScanResults());
+ }, 0);
+ if (!success) {
+ Log.e(TAG, "Failed to post runnable to fetch scan results");
}
- return mWifiScanner.getSingleScanResults();
+ return scanResults;
} finally {
Binder.restoreCallingIdentity(ident);
}
@@ -1799,8 +1982,11 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* @return true on success or false on failure
*/
@Override
- public boolean addOrUpdatePasspointConfiguration(PasspointConfiguration config) {
- enforceChangePermission();
+ public boolean addOrUpdatePasspointConfiguration(
+ PasspointConfiguration config, String packageName) {
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return false;
+ }
mLog.info("addorUpdatePasspointConfiguration uid=%").c(Binder.getCallingUid()).flush();
if (!mContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_WIFI_PASSPOINT)) {
@@ -1817,8 +2003,10 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* @return true on success or false on failure
*/
@Override
- public boolean removePasspointConfiguration(String fqdn) {
- enforceChangePermission();
+ public boolean removePasspointConfiguration(String fqdn, String packageName) {
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return false;
+ }
mLog.info("removePasspointConfiguration uid=%").c(Binder.getCallingUid()).flush();
if (!mContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_WIFI_PASSPOINT)) {
@@ -1890,8 +2078,10 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* TODO: deprecate this
*/
@Override
- public boolean saveConfiguration() {
- enforceChangePermission();
+ public boolean saveConfiguration(String packageName) {
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return false;
+ }
mLog.info("saveConfiguration uid=%").c(Binder.getCallingUid()).flush();
if (mWifiStateMachineChannel != null) {
return mWifiStateMachine.syncSaveConfig(mWifiStateMachineChannel);
@@ -2087,9 +2277,13 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* an AsyncChannel communication with WifiService
*/
@Override
- public Messenger getWifiServiceMessenger() {
+ public Messenger getWifiServiceMessenger(String packageName) throws RemoteException {
enforceAccessPermission();
- enforceChangePermission();
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ // We don't have a good way of creating a fake Messenger, and returning null would
+ // immediately break callers.
+ throw new SecurityException("Could not create wifi service messenger");
+ }
mLog.info("getWifiServiceMessenger uid=%").c(Binder.getCallingUid()).flush();
return new Messenger(mClientHandler);
}
@@ -2098,9 +2292,11 @@ public class WifiServiceImpl extends IWifiManager.Stub {
* Disable an ephemeral network, i.e. network that is created thru a WiFi Scorer
*/
@Override
- public void disableEphemeralNetwork(String SSID) {
+ public void disableEphemeralNetwork(String SSID, String packageName) {
enforceAccessPermission();
- enforceChangePermission();
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return;
+ }
mLog.info("disableEphemeralNetwork uid=%").c(Binder.getCallingUid()).flush();
mWifiStateMachine.disableEphemeralNetwork(SSID);
}
@@ -2109,15 +2305,8 @@ public class WifiServiceImpl extends IWifiManager.Stub {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
- if (action.equals(Intent.ACTION_SCREEN_ON)) {
- mWifiController.sendMessage(CMD_SCREEN_ON);
- } else if (action.equals(Intent.ACTION_USER_PRESENT)) {
+ if (action.equals(Intent.ACTION_USER_PRESENT)) {
mWifiController.sendMessage(CMD_USER_PRESENT);
- } else if (action.equals(Intent.ACTION_SCREEN_OFF)) {
- mWifiController.sendMessage(CMD_SCREEN_OFF);
- } else if (action.equals(Intent.ACTION_BATTERY_CHANGED)) {
- int pluggedType = intent.getIntExtra("plugged", 0);
- mWifiController.sendMessage(CMD_BATTERY_CHANGED, pluggedType, 0, null);
} else if (action.equals(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)) {
int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE,
BluetoothAdapter.STATE_DISCONNECTED);
@@ -2226,10 +2415,7 @@ public class WifiServiceImpl extends IWifiManager.Stub {
private void registerForBroadcasts() {
IntentFilter intentFilter = new IntentFilter();
- intentFilter.addAction(Intent.ACTION_SCREEN_ON);
intentFilter.addAction(Intent.ACTION_USER_PRESENT);
- intentFilter.addAction(Intent.ACTION_SCREEN_OFF);
- intentFilter.addAction(Intent.ACTION_BATTERY_CHANGED);
intentFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
intentFilter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED);
intentFilter.addAction(TelephonyIntents.ACTION_EMERGENCY_CALLBACK_MODE_CHANGED);
@@ -2335,13 +2521,18 @@ public class WifiServiceImpl extends IWifiManager.Stub {
}
}
+ /**
+ * NOTE: WifiLocks do not serve a useful purpose in their current impl and will be removed
+ * (including the methods below).
+ *
+ * TODO: b/71548157
+ */
@Override
public boolean acquireWifiLock(IBinder binder, int lockMode, String tag, WorkSource ws) {
mLog.info("acquireWifiLock uid=% lockMode=%")
.c(Binder.getCallingUid())
.c(lockMode).flush();
if (mWifiLockManager.acquireWifiLock(lockMode, tag, binder, ws)) {
- mWifiController.sendMessage(CMD_LOCKS_CHANGED);
return true;
}
return false;
@@ -2357,7 +2548,6 @@ public class WifiServiceImpl extends IWifiManager.Stub {
public boolean releaseWifiLock(IBinder binder) {
mLog.info("releaseWifiLock uid=%").c(Binder.getCallingUid()).flush();
if (mWifiLockManager.releaseWifiLock(binder)) {
- mWifiController.sendMessage(CMD_LOCKS_CHANGED);
return true;
}
return false;
@@ -2435,23 +2625,12 @@ public class WifiServiceImpl extends IWifiManager.Stub {
return mWifiStateMachine.getAggressiveHandover();
}
- /* Return the Wifi Connection statistics object */
@Override
- public WifiConnectionStatistics getConnectionStatistics() {
- enforceAccessPermission();
- enforceReadCredentialPermission();
- mLog.info("getConnectionStatistics uid=%").c(Binder.getCallingUid()).flush();
- if (mWifiStateMachineChannel != null) {
- return mWifiStateMachine.syncGetConnectionStatistics(mWifiStateMachineChannel);
- } else {
- Slog.e(TAG, "mWifiStateMachineChannel is not initialized");
- return null;
- }
- }
-
- @Override
- public void factoryReset() {
+ public void factoryReset(String packageName) {
enforceConnectivityInternalPermission();
+ if (enforceChangePermission(packageName) != MODE_ALLOWED) {
+ return;
+ }
mLog.info("factoryReset uid=%").c(Binder.getCallingUid()).flush();
if (mUserManager.hasUserRestriction(UserManager.DISALLOW_NETWORK_RESET)) {
return;
@@ -2464,21 +2643,15 @@ public class WifiServiceImpl extends IWifiManager.Stub {
}
if (!mUserManager.hasUserRestriction(UserManager.DISALLOW_CONFIG_WIFI)) {
- // Enable wifi
- try {
- setWifiEnabled(mContext.getOpPackageName(), true);
- } catch (RemoteException e) {
- /* ignore - local call */
- }
// Delete all Wifi SSIDs
if (mWifiStateMachineChannel != null) {
List<WifiConfiguration> networks = mWifiStateMachine.syncGetConfiguredNetworks(
Binder.getCallingUid(), mWifiStateMachineChannel);
if (networks != null) {
for (WifiConfiguration config : networks) {
- removeNetwork(config.networkId);
+ removeNetwork(config.networkId, packageName);
}
- saveConfiguration();
+ saveConfiguration(packageName);
}
}
}
diff --git a/com/android/server/wifi/WifiStateMachine.java b/com/android/server/wifi/WifiStateMachine.java
index 47b4b151..b500f9a1 100644
--- a/com/android/server/wifi/WifiStateMachine.java
+++ b/com/android/server/wifi/WifiStateMachine.java
@@ -42,7 +42,9 @@ import android.database.ContentObserver;
import android.net.ConnectivityManager;
import android.net.DhcpResults;
import android.net.IpConfiguration;
+import android.net.KeepalivePacketData;
import android.net.LinkProperties;
+import android.net.MacAddress;
import android.net.Network;
import android.net.NetworkAgent;
import android.net.NetworkCapabilities;
@@ -57,14 +59,12 @@ import android.net.StaticIpConfiguration;
import android.net.TrafficStats;
import android.net.dhcp.DhcpClient;
import android.net.ip.IpClient;
-import android.net.wifi.IClientInterface;
import android.net.wifi.RssiPacketCountInfo;
import android.net.wifi.ScanResult;
import android.net.wifi.ScanSettings;
import android.net.wifi.SupplicantState;
import android.net.wifi.WifiChannel;
import android.net.wifi.WifiConfiguration;
-import android.net.wifi.WifiConnectionStatistics;
import android.net.wifi.WifiEnterpriseConfig;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
@@ -81,7 +81,6 @@ import android.os.BatteryStats;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
-import android.os.INetworkManagementService;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
@@ -92,6 +91,7 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.os.WorkSource;
import android.provider.Settings;
+import android.system.OsConstants;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
@@ -108,7 +108,8 @@ import com.android.internal.util.MessageUtils;
import com.android.internal.util.Protocol;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
-import com.android.server.connectivity.KeepalivePacketData;
+import com.android.server.wifi.WifiNative.InterfaceCallback;
+import com.android.server.wifi.WifiNative.StatusListener;
import com.android.server.wifi.hotspot2.AnqpEvent;
import com.android.server.wifi.hotspot2.IconEvent;
import com.android.server.wifi.hotspot2.NetworkDetail;
@@ -132,6 +133,7 @@ import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.Inet4Address;
+import java.net.Inet6Address;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Arrays;
@@ -210,10 +212,9 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
private WifiPermissionsUtil mWifiPermissionsUtil;
private WifiConfigManager mWifiConfigManager;
private WifiConnectivityManager mWifiConnectivityManager;
- private INetworkManagementService mNwService;
- private IClientInterface mClientInterface;
private ConnectivityManager mCm;
private BaseWifiDiagnostics mWifiDiagnostics;
+ private ScanRequestProxy mScanRequestProxy;
private WifiApConfigStore mWifiApConfigStore;
private final boolean mP2pSupported;
private final AtomicBoolean mP2pConnected = new AtomicBoolean(false);
@@ -241,20 +242,44 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
private boolean mScreenOn = false;
- private final String mInterfaceName;
+ private String mInterfaceName;
private int mLastSignalLevel = -1;
private String mLastBssid;
private int mLastNetworkId; // The network Id we successfully joined
private boolean mIsLinkDebouncing = false;
- private final WifiNative.WificondDeathEventHandler mWificondDeathRecipient = () -> {
- sendMessage(CMD_WIFICOND_BINDER_DEATH);
+
+ private final StatusListener mWifiNativeStatusListener = (boolean isReady) -> {
+ if (!isReady) {
+ sendMessage(CMD_WIFINATIVE_FAILURE);
+ }
};
- private final WifiNative.VendorHalDeathEventHandler mVendorHalDeathRecipient = () -> {
- sendMessage(CMD_VENDOR_HAL_HWBINDER_DEATH);
+
+ private final InterfaceCallback mWifiNativeInterfaceCallback = new InterfaceCallback() {
+ @Override
+ public void onDestroyed(String ifaceName) {
+ sendMessage(CMD_INTERFACE_DESTROYED);
+ }
+
+ @Override
+ public void onUp(String ifaceName) {
+ }
+
+ @Override
+ public void onDown(String ifaceName) {
+ }
};
private boolean mIpReachabilityDisconnectEnabled = true;
+ private WifiManager.SoftApCallback mSoftApCallback;
+
+ /**
+ * Called from WifiServiceImpl to register a callback for notifications from SoftApManager
+ */
+ public void registerSoftApCallback(WifiManager.SoftApCallback callback) {
+ mSoftApCallback = callback;
+ }
+
@Override
public void onRssiThresholdBreached(byte curRssi) {
if (mVerboseLoggingEnabled) {
@@ -300,10 +325,13 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
* In SCAN_ONLY_MODE, the STA can only scan for access points
* In SCAN_ONLY_WIFI_OFF_MODE, the STA can only scan for access points with wifi toggle being off
*/
- private int mOperationalMode = CONNECT_MODE;
+ private int mOperationalMode = DISABLED_MODE;
private boolean mIsScanOngoing = false;
private boolean mIsFullScanOngoing = false;
+ // variable indicating we are expecting a mode switch - do not attempt recovery for failures
+ private boolean mModeChange = false;
+
private final Queue<Message> mBufferedScanMsg = new LinkedList<>();
private static final int UNKNOWN_SCAN_SOURCE = -1;
private static final int ADD_OR_UPDATE_SOURCE = -3;
@@ -417,7 +445,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
logd(dbg + " clearTargetBssid " + bssid + " key=" + config.configKey());
}
mTargetRoamBSSID = bssid;
- return mWifiNative.setConfiguredNetworkBSSID(bssid);
+ return mWifiNative.setConfiguredNetworkBSSID(mInterfaceName, bssid);
}
/**
@@ -445,7 +473,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
return true;
}
- private final IpClient mIpClient;
+ private IpClient mIpClient;
// Channel for sending replies.
private AsyncChannel mReplyChannel = new AsyncChannel();
@@ -466,11 +494,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
private byte[] mRssiRanges;
- // Keep track of various statistics, for retrieval by System Apps, i.e. under @SystemApi
- // We should really persist that into the networkHistory.txt file, and read it back when
- // WifiStateMachine starts up
- private WifiConnectionStatistics mWifiConnectionStatistics = new WifiConnectionStatistics();
-
// Used to filter out requests we couldn't possibly satisfy.
private final NetworkCapabilities mNetworkCapabilitiesFilter = new NetworkCapabilities();
@@ -479,10 +502,12 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
/* The base for wifi message types */
static final int BASE = Protocol.BASE_WIFI;
- /* Start the supplicant */
+ /* Start the STA interface */
static final int CMD_START_SUPPLICANT = BASE + 11;
- /* Stop the supplicant */
+ /* Stop the STA interface */
static final int CMD_STOP_SUPPLICANT = BASE + 12;
+ /* STA interface destroyed */
+ static final int CMD_INTERFACE_DESTROYED = BASE + 13;
/* Indicates Static IP succeeded */
static final int CMD_STATIC_IP_SUCCESS = BASE + 15;
/* Indicates Static IP failed */
@@ -529,8 +554,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
static final int CMD_RECONNECT = BASE + 74;
/* Reassociate to a network */
static final int CMD_REASSOCIATE = BASE + 75;
- /* Get Connection Statistis */
- static final int CMD_GET_CONNECTION_STATISTICS = BASE + 76;
/* Controls suspend mode optimizations
*
@@ -724,11 +747,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 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;
+ /* Signals that one of the native daemons is dead. */
+ private static final int CMD_WIFINATIVE_FAILURE = BASE + 250;
/* Indicates that diagnostics should time out a connection start event. */
private static final int CMD_DIAGS_CONNECT_TIMEOUT = BASE + 252;
@@ -781,6 +801,9 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
/* Tracks if user has enabled suspend optimizations through settings */
private AtomicBoolean mUserWantsSuspendOpt = new AtomicBoolean(true);
+ /* Tracks if user has enabled Connected Mac Randomization through settings */
+ private AtomicBoolean mEnableConnectedMacRandomization = new AtomicBoolean(false);
+
/**
* Scan period for the NO_NETWORKS_PERIIDOC_SCAN_FEATURE
*/
@@ -814,13 +837,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
private State mSupplicantStartingState = new SupplicantStartingState();
/* Driver loaded and supplicant ready */
private State mSupplicantStartedState = new SupplicantStartedState();
- /* Waiting for supplicant to stop and monitor to exit */
- private State mSupplicantStoppingState = new SupplicantStoppingState();
- /* Wait until p2p is disabled
- * This is a special state which is entered right after we exit out of DriverStartedState
- * before transitioning to another state.
- */
- private State mWaitForP2pDisableState = new WaitForP2pDisableState();
/* Scan for networks, no connection will be established */
private State mScanModeState = new ScanModeState();
/* Connecting to an access point */
@@ -924,13 +940,11 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
mWrongPasswordNotifier = wrongPasswordNotifier;
// TODO refactor WifiNative use of context out into it's own class
- mInterfaceName = mWifiNative.getInterfaceName();
mNetworkInfo = new NetworkInfo(ConnectivityManager.TYPE_WIFI, 0, NETWORKTYPE, "");
mBatteryStats = IBatteryStats.Stub.asInterface(mFacade.getService(
BatteryStats.SERVICE_NAME));
mWifiStateTracker = wifiInjector.getWifiStateTracker();
IBinder b = mFacade.getService(Context.NETWORKMANAGEMENT_SERVICE);
- mNwService = INetworkManagementService.Stub.asInterface(b);
mP2pSupported = mContext.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_WIFI_DIRECT);
@@ -943,6 +957,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
mWifiMonitor = mWifiInjector.getWifiMonitor();
mWifiDiagnostics = mWifiInjector.makeWifiDiagnostics(mWifiNative);
+ mScanRequestProxy = mWifiInjector.getScanRequestProxy();
mWifiPermissionsWrapper = mWifiInjector.getWifiPermissionsWrapper();
mWifiInfo = new ExtendedWifiInfo();
@@ -957,9 +972,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
mLastNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
mLastSignalLevel = -1;
- mIpClient = mFacade.makeIpClient(mContext, mInterfaceName, new IpClientCallback());
- mIpClient.setMulticastFilter(true);
-
mNoNetworksPeriodicScan = mContext.getResources().getInteger(
R.integer.config_wifi_no_network_periodic_scan_interval);
@@ -977,10 +989,14 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
mUserWantsSuspendOpt.set(mFacade.getIntegerSetting(mContext,
Settings.Global.WIFI_SUSPEND_OPTIMIZATIONS_ENABLED, 1) == 1);
+ mEnableConnectedMacRandomization.set(mFacade.getIntegerSetting(mContext,
+ Settings.Global.WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED, 0) == 1);
+
mNetworkCapabilitiesFilter.addTransportType(NetworkCapabilities.TRANSPORT_WIFI);
mNetworkCapabilitiesFilter.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
mNetworkCapabilitiesFilter.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
mNetworkCapabilitiesFilter.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
+ mNetworkCapabilitiesFilter.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED);
mNetworkCapabilitiesFilter.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED);
mNetworkCapabilitiesFilter.setLinkUpstreamBandwidthKbps(1024 * 1024);
mNetworkCapabilitiesFilter.setLinkDownstreamBandwidthKbps(1024 * 1024);
@@ -1014,6 +1030,18 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
});
+ mContext.getContentResolver().registerContentObserver(Settings.Global.getUriFor(
+ Settings.Global.WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED), false,
+ new ContentObserver(getHandler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ mEnableConnectedMacRandomization.set(mFacade.getIntegerSetting(mContext,
+ Settings.Global.WIFI_CONNECTED_MAC_RANDOMIZATION_ENABLED, 0) == 1);
+ Log.i(TAG, "EnableConnectedMacRandomization Setting changed to "
+ + mEnableConnectedMacRandomization);
+ }
+ });
+
mContext.registerReceiver(
new BroadcastReceiver() {
@Override
@@ -1057,7 +1085,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
addState(mInitialState, mDefaultState);
addState(mSupplicantStartingState, mDefaultState);
addState(mSupplicantStartedState, mDefaultState);
- addState(mScanModeState, mSupplicantStartedState);
addState(mConnectModeState, mSupplicantStartedState);
addState(mL2ConnectedState, mConnectModeState);
addState(mObtainingIpState, mL2ConnectedState);
@@ -1066,12 +1093,11 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
addState(mDisconnectingState, mConnectModeState);
addState(mDisconnectedState, mConnectModeState);
addState(mWpsRunningState, mConnectModeState);
- addState(mWaitForP2pDisableState, mSupplicantStartedState);
- addState(mSupplicantStoppingState, mDefaultState);
+ addState(mScanModeState, mDefaultState);
addState(mSoftApState, mDefaultState);
// CHECKSTYLE:ON IndentationCheck
- setInitialState(mInitialState);
+ setInitialState(mDefaultState);
setLogRecSize(NUM_LOG_RECS_NORMAL);
setLogOnlyTransitions(false);
@@ -1083,6 +1109,15 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
// We update this field when we receive broadcasts from the system.
handleScreenStateChanged(powerManager.isInteractive());
+ final Intent intent = new Intent(WifiManager.WIFI_SCAN_AVAILABLE);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ intent.putExtra(WifiManager.EXTRA_SCAN_AVAILABLE, WIFI_STATE_DISABLED);
+ mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+
+ sendWifiScanAvailable(false);
+ }
+
+ private void registerForWifiMonitorEvents() {
mWifiMonitor.registerHandler(mInterfaceName, CMD_TARGET_BSSID, getHandler());
mWifiMonitor.registerHandler(mInterfaceName, CMD_ASSOCIATED_BSSID, getHandler());
mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.ANQP_DONE_EVENT, getHandler());
@@ -1090,7 +1125,8 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
getHandler());
mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.AUTHENTICATION_FAILURE_EVENT,
getHandler());
- mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.GAS_QUERY_DONE_EVENT, getHandler());
+ mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.GAS_QUERY_DONE_EVENT,
+ getHandler());
mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.GAS_QUERY_START_EVENT,
getHandler());
mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.HS20_REMEDIATION_EVENT,
@@ -1108,8 +1144,10 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
getHandler());
mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT,
getHandler());
- mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.SUP_REQUEST_IDENTITY, getHandler());
- mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.SUP_REQUEST_SIM_AUTH, getHandler());
+ mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.SUP_REQUEST_IDENTITY,
+ getHandler());
+ mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.SUP_REQUEST_SIM_AUTH,
+ getHandler());
mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.WPS_FAIL_EVENT, getHandler());
mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.WPS_OVERLAP_EVENT, getHandler());
mWifiMonitor.registerHandler(mInterfaceName, WifiMonitor.WPS_SUCCESS_EVENT, getHandler());
@@ -1129,11 +1167,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
mWifiMetrics.getHandler());
mWifiMonitor.registerHandler(mInterfaceName, CMD_TARGET_BSSID,
mWifiMetrics.getHandler());
-
- final Intent intent = new Intent(WifiManager.WIFI_SCAN_AVAILABLE);
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- intent.putExtra(WifiManager.EXTRA_SCAN_AVAILABLE, WIFI_STATE_DISABLED);
- mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
}
class IpClientCallback extends IpClient.Callback {
@@ -1283,21 +1316,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
ouiBytes[2] = (byte) (Integer.parseInt(ouiParts[2], 16) & 0xFF);
logd("Setting OUI to " + oui);
- return mWifiNative.setScanningMacOui(ouiBytes);
- }
-
- /**
- * Helper method to lookup the framework network ID of the network currently configured in
- * wpa_supplicant using the provided supplicant network ID. This is needed for translating the
- * networkID received from all {@link WifiMonitor} events.
- *
- * @param supplicantNetworkId Network ID of network in wpa_supplicant.
- * @return Corresponding Internal configured network ID
- * TODO(b/31080843): This is ugly! We need to hide this translation of networkId's. This will
- * be handled once we move all of this connection logic to wificond.
- */
- private int lookupFrameworkNetworkId(int supplicantNetworkId) {
- return mWifiNative.getFrameworkNetworkId(supplicantNetworkId);
+ return mWifiNative.setScanningMacOui(mInterfaceName, ouiBytes);
}
/**
@@ -1354,6 +1373,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
* workSource is specified.
* @param workSource If not null, blame is given to workSource.
* @param settings Scan settings, see {@link ScanSettings}.
+ * TODO(b/70359905): Remove this method & it's dependencies since it's not used anymore.
*/
public void startScan(int callingUid, int scanCounter,
ScanSettings settings, WorkSource workSource) {
@@ -1441,9 +1461,13 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
WifiLinkLayerStats getWifiLinkLayerStats() {
+ if (mInterfaceName == null) {
+ loge("getWifiLinkLayerStats called without an interface");
+ return null;
+ }
WifiLinkLayerStats stats = null;
if (mWifiLinkLayerStatsSupported > 0) {
- stats = mWifiNative.getWifiLinkLayerStats();
+ stats = mWifiNative.getWifiLinkLayerStats(mInterfaceName);
if (stats == null && mWifiLinkLayerStatsSupported > 0) {
mWifiLinkLayerStatsSupported -= 1;
} else if (stats != null) {
@@ -1464,8 +1488,46 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
return stats;
}
+ private byte[] getDstMacForKeepalive(KeepalivePacketData packetData)
+ throws KeepalivePacketData.InvalidPacketException {
+ try {
+ InetAddress gateway = RouteInfo.selectBestRoute(
+ mLinkProperties.getRoutes(), packetData.dstAddress).getGateway();
+ String dstMacStr = macAddressFromRoute(gateway.getHostAddress());
+ return NativeUtil.macAddressToByteArray(dstMacStr);
+ } catch (NullPointerException | IllegalArgumentException e) {
+ throw new KeepalivePacketData.InvalidPacketException(
+ ConnectivityManager.PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
+ }
+ }
+
+ private static int getEtherProtoForKeepalive(KeepalivePacketData packetData)
+ throws KeepalivePacketData.InvalidPacketException {
+ if (packetData.dstAddress instanceof Inet4Address) {
+ return OsConstants.ETH_P_IP;
+ } else if (packetData.dstAddress instanceof Inet6Address) {
+ return OsConstants.ETH_P_IPV6;
+ } else {
+ throw new KeepalivePacketData.InvalidPacketException(
+ ConnectivityManager.PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
+ }
+ }
+
int startWifiIPPacketOffload(int slot, KeepalivePacketData packetData, int intervalSeconds) {
- int ret = mWifiNative.startSendingOffloadedPacket(slot, packetData, intervalSeconds * 1000);
+ byte[] packet = null;
+ byte[] dstMac = null;
+ int proto = 0;
+
+ try {
+ packet = packetData.getPacket();
+ dstMac = getDstMacForKeepalive(packetData);
+ proto = getEtherProtoForKeepalive(packetData);
+ } catch (KeepalivePacketData.InvalidPacketException e) {
+ return e.error;
+ }
+
+ int ret = mWifiNative.startSendingOffloadedPacket(
+ mInterfaceName, slot, dstMac, packet, proto, intervalSeconds * 1000);
if (ret != 0) {
loge("startWifiIPPacketOffload(" + slot + ", " + intervalSeconds +
"): hardware error " + ret);
@@ -1476,7 +1538,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
int stopWifiIPPacketOffload(int slot) {
- int ret = mWifiNative.stopSendingOffloadedPacket(slot);
+ int ret = mWifiNative.stopSendingOffloadedPacket(mInterfaceName, slot);
if (ret != 0) {
loge("stopWifiIPPacketOffload(" + slot + "): hardware error " + ret);
return ConnectivityManager.PacketKeepalive.ERROR_HARDWARE_ERROR;
@@ -1486,11 +1548,12 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
int startRssiMonitoringOffload(byte maxRssi, byte minRssi) {
- return mWifiNative.startRssiMonitoring(maxRssi, minRssi, WifiStateMachine.this);
+ return mWifiNative.startRssiMonitoring(
+ mInterfaceName, maxRssi, minRssi, WifiStateMachine.this);
}
int stopRssiMonitoringOffload() {
- return mWifiNative.stopRssiMonitoring();
+ return mWifiNative.stopRssiMonitoring(mInterfaceName);
}
private void handleScanRequest(Message message) {
@@ -1570,6 +1633,10 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
private boolean startScanNative(final Set<Integer> freqs,
List<WifiScanner.ScanSettings.HiddenNetwork> hiddenNetworkList,
WorkSource workSource) {
+ if (mWifiScanner == null) {
+ Log.e(TAG, "startScanNative is called when mWifiScanner is null");
+ return false;
+ }
WifiScanner.ScanSettings settings = new WifiScanner.ScanSettings();
if (freqs == null) {
settings.band = WifiScanner.WIFI_BAND_BOTH_WITH_DFS;
@@ -1781,10 +1848,18 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
*/
public void setOperationalMode(int mode) {
if (mVerboseLoggingEnabled) log("setting operational mode to " + String.valueOf(mode));
+ mModeChange = true;
sendMessage(CMD_SET_OPERATIONAL_MODE, mode, 0);
}
/**
+ * Initiates a system-level bugreport, in a non-blocking fashion.
+ */
+ public void takeBugReport() {
+ mWifiDiagnostics.takeBugReport();
+ }
+
+ /**
* Allow tests to confirm the operational mode for WSM.
*/
@VisibleForTesting
@@ -2003,19 +2078,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
resultMsg.recycle();
return result;
}
- /**
- * Get connection statistics synchronously
- *
- * @param channel
- * @return
- */
-
- public WifiConnectionStatistics syncGetConnectionStatistics(AsyncChannel channel) {
- Message resultMsg = channel.sendMessageSynchronously(CMD_GET_CONNECTION_STATISTICS);
- WifiConnectionStatistics result = (WifiConnectionStatistics) resultMsg.obj;
- resultMsg.recycle();
- return result;
- }
/**
* Get adaptors synchronously
@@ -2092,7 +2154,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
* @return a hex string representation of the WPS-NFC configuration token
*/
public String syncGetCurrentNetworkWpsNfcConfigurationToken() {
- return mWifiNative.getCurrentNetworkWpsNfcConfigurationToken();
+ return mWifiNative.getCurrentNetworkWpsNfcConfigurationToken(mInterfaceName);
}
public void enableRssiPolling(boolean enabled) {
@@ -2103,14 +2165,18 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
* Start filtering Multicast v4 packets
*/
public void startFilteringMulticastPackets() {
- mIpClient.setMulticastFilter(true);
+ if (mIpClient != null) {
+ mIpClient.setMulticastFilter(true);
+ }
}
/**
* Stop filtering Multicast v4 packets
*/
public void stopFilteringMulticastPackets() {
- mIpClient.setMulticastFilter(false);
+ if (mIpClient != null) {
+ mIpClient.setMulticastFilter(false);
+ }
}
/**
@@ -2201,7 +2267,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
if (mReportedRunning) {
// If the work source has changed since last time, need
// to remove old work from battery stats.
- if (mLastRunningWifiUids.diff(mRunningWifiUids)) {
+ if (!mLastRunningWifiUids.equals(mRunningWifiUids)) {
mBatteryStats.noteWifiRunningChanged(mLastRunningWifiUids,
mRunningWifiUids);
mLastRunningWifiUids.set(mRunningWifiUids);
@@ -2227,7 +2293,9 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
public void dumpIpClient(FileDescriptor fd, PrintWriter pw, String[] args) {
- mIpClient.dump(fd, pw, args);
+ if (mIpClient != null) {
+ mIpClient.dump(fd, pw, args);
+ }
}
@Override
@@ -2853,11 +2921,11 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
+ " - " + Thread.currentThread().getStackTrace()[4].getMethodName()
+ " - " + Thread.currentThread().getStackTrace()[5].getMethodName());
}
- mWifiNative.setSuspendOptimizations(true);
+ mWifiNative.setSuspendOptimizations(mInterfaceName, true);
}
} else {
mSuspendOptNeedsDisabled |= reason;
- mWifiNative.setSuspendOptimizations(false);
+ mWifiNative.setSuspendOptimizations(mInterfaceName, false);
}
}
@@ -2871,6 +2939,17 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
if (mVerboseLoggingEnabled) log("mSuspendOptNeedsDisabled " + mSuspendOptNeedsDisabled);
}
+ private void sendWifiScanAvailable(boolean available) {
+ int state = WIFI_STATE_DISABLED;
+ if (available) {
+ state = WIFI_STATE_ENABLED;
+ }
+ final Intent intent = new Intent(WifiManager.WIFI_SCAN_AVAILABLE);
+ intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
+ intent.putExtra(WifiManager.EXTRA_SCAN_AVAILABLE, state);
+ mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
+ }
+
private void setWifiState(int wifiState) {
final int previousWifiState = mWifiState.get();
@@ -2919,7 +2998,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
mNumScanResultsKnown = 0;
mNumScanResultsReturned = 0;
- ArrayList<ScanDetail> scanResults = mWifiNative.getScanResults();
+ ArrayList<ScanDetail> scanResults = mWifiNative.getScanResults(mInterfaceName);
if (scanResults.isEmpty()) {
mScanResults = new ArrayList<>();
@@ -2960,7 +3039,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
Integer newRssi = null;
Integer newLinkSpeed = null;
Integer newFrequency = null;
- WifiNative.SignalPollResult pollResult = mWifiNative.signalPoll();
+ WifiNative.SignalPollResult pollResult = mWifiNative.signalPoll(mInterfaceName);
if (pollResult == null) {
return;
}
@@ -3006,12 +3085,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
mWifiInfo.setLinkSpeed(newLinkSpeed);
}
if (newFrequency != null && newFrequency > 0) {
- if (ScanResult.is5GHz(newFrequency)) {
- mWifiConnectionStatistics.num5GhzConnected++;
- }
- if (ScanResult.is24GHz(newFrequency)) {
- mWifiConnectionStatistics.num24GhzConnected++;
- }
mWifiInfo.setFrequency(newFrequency);
}
mWifiConfigManager.updateScanDetailCacheFromWifiInfo(mWifiInfo);
@@ -3139,19 +3212,10 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
private void sendNetworkStateChangeBroadcast(String bssid) {
Intent intent = new Intent(WifiManager.NETWORK_STATE_CHANGED_ACTION);
intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- intent.putExtra(WifiManager.EXTRA_NETWORK_INFO, new NetworkInfo(mNetworkInfo));
- intent.putExtra(WifiManager.EXTRA_LINK_PROPERTIES, new LinkProperties(mLinkProperties));
- if (bssid != null)
- intent.putExtra(WifiManager.EXTRA_BSSID, bssid);
- if (mNetworkInfo.getDetailedState() == DetailedState.VERIFYING_POOR_LINK ||
- mNetworkInfo.getDetailedState() == DetailedState.CONNECTED) {
- // We no longer report MAC address to third-parties and our code does
- // not rely on this broadcast, so just send the default MAC address.
- fetchRssiLinkSpeedAndFrequencyNative();
- WifiInfo sentWifiInfo = new WifiInfo(mWifiInfo);
- sentWifiInfo.setMacAddress(WifiInfo.DEFAULT_MAC_ADDRESS);
- intent.putExtra(WifiManager.EXTRA_WIFI_INFO, sentWifiInfo);
- }
+ NetworkInfo networkInfo = new NetworkInfo(mNetworkInfo);
+ networkInfo.setExtraInfo(null);
+ intent.putExtra(WifiManager.EXTRA_NETWORK_INFO, networkInfo);
+ //TODO(b/69974497) This should be non-sticky, but settings needs fixing first.
mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
}
@@ -3244,7 +3308,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
// Network id and SSID are only valid when we start connecting
if (SupplicantState.isConnecting(state)) {
- mWifiInfo.setNetworkId(lookupFrameworkNetworkId(stateChangeResult.networkId));
+ mWifiInfo.setNetworkId(stateChangeResult.networkId);
mWifiInfo.setBSSID(stateChangeResult.BSSID);
mWifiInfo.setSSID(stateChangeResult.wifiSsid);
} else {
@@ -3327,11 +3391,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
*/
if (killSupplicant) {
mWifiMonitor.stopAllMonitoring();
- if (!mWifiNative.disableSupplicant()) {
- loge("Failed to disable supplicant after connection loss");
- }
}
- mWifiNative.closeSupplicantConnection();
sendSupplicantConnectionChangedBroadcast(false);
setWifiState(WIFI_STATE_DISABLED);
}
@@ -3356,7 +3416,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
*/
// Disable the coexistence mode
mWifiNative.setBluetoothCoexistenceMode(
- WifiNative.BLUETOOTH_COEXISTENCE_MODE_DISABLED);
+ mInterfaceName, WifiNative.BLUETOOTH_COEXISTENCE_MODE_DISABLED);
}
// Disable power save and suspend optimizations during DHCP
@@ -3364,7 +3424,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
// power settings when we control suspend mode optimizations.
// TODO: Remove this comment when the driver is fixed.
setSuspendOptimizationsNative(SUSPEND_DUE_TO_DHCP, false);
- mWifiNative.setPowerSave(false);
+ mWifiNative.setPowerSave(mInterfaceName, false);
// Update link layer stats
getWifiLinkLayerStats();
@@ -3386,13 +3446,13 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
void handlePostDhcpSetup() {
/* Restore power save and suspend optimizations */
setSuspendOptimizationsNative(SUSPEND_DUE_TO_DHCP, true);
- mWifiNative.setPowerSave(true);
+ mWifiNative.setPowerSave(mInterfaceName, true);
p2pSendMessage(WifiP2pServiceImpl.BLOCK_DISCOVERY, WifiP2pServiceImpl.DISABLED);
// Set the coexistence mode back to its default value
mWifiNative.setBluetoothCoexistenceMode(
- WifiNative.BLUETOOTH_COEXISTENCE_MODE_SENSE);
+ mInterfaceName, WifiNative.BLUETOOTH_COEXISTENCE_MODE_SENSE);
}
private static final long DIAGS_CONNECT_TIMEOUT_MILLIS = 60 * 1000;
@@ -3493,16 +3553,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
// Tell the framework whether the newly connected network is trusted or untrusted.
updateCapabilities(c);
}
- if (c != null) {
- ScanResult result = getCurrentScanResult();
- if (result == null) {
- logd("WifiStateMachine: handleSuccessfulIpConfiguration and no scan results" +
- c.configKey());
- } else {
- // Clear the per BSSID failure count
- result.numIpConfigFailures = 0;
- }
- }
}
private void handleIPv4Failure() {
@@ -3541,7 +3591,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
/* DHCP times out after about 30 seconds, we do a
* disconnect thru supplicant, we will let autojoin retry connecting to the network
*/
- mWifiNative.disconnect();
+ mWifiNative.disconnect(mInterfaceName);
}
// TODO: De-duplicated this and handleIpConfigurationLost().
@@ -3552,7 +3602,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
// TODO: Determine whether to call some form of mWifiConfigManager.handleSSIDStateChange().
// Disconnect via supplicant, and let autojoin retry connecting to the network.
- mWifiNative.disconnect();
+ mWifiNative.disconnect(mInterfaceName);
}
/*
@@ -3748,17 +3798,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
/**
- * 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();
- }
- }
-
- /**
* Register the phone listener if we need to set/reset the power limits during voice call for
* this device.
*/
@@ -3792,6 +3831,34 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
}
+ /**
+ * Dynamically change the MAC address to use the locally randomized
+ * MAC address generated for each network.
+ * @param config WifiConfiguration with mRandomizedMacAddress to change into. If the address
+ * is masked out or not set, it will generate a new random MAC address.
+ */
+ private void configureRandomizedMacAddress(WifiConfiguration config) {
+ if (config == null) {
+ Log.e(TAG, "No config to change MAC address to");
+ return;
+ }
+ MacAddress currentMac = MacAddress.fromString(mWifiNative.getMacAddress(mInterfaceName));
+ MacAddress newMac = config.getOrCreateRandomizedMacAddress();
+
+ if (currentMac.equals(newMac)) {
+ Log.i(TAG, "No changes in MAC address");
+ } else {
+ Log.i(TAG, "ConnectedMacRandomization SSID(" + config.getPrintableSsid()
+ + "). setMacAddress(" + newMac.toString() + ") from "
+ + currentMac.toString());
+ boolean setMacSuccess =
+ mWifiNative.setMacAddress(mInterfaceName, newMac);
+ Log.i(TAG, "ConnectedMacRandomization ...setMacAddress("
+ + newMac.toString() + ") = " + setMacSuccess);
+ }
+ }
+
+
/********************************************************
* HSM states
*******************************************************/
@@ -3884,7 +3951,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
break;
case CMD_INITIALIZE:
- ok = mWifiNative.initializeVendorHal(mVendorHalDeathRecipient);
+ ok = mWifiNative.initialize();
mPasspointManager.initializeProvisioner(
mWifiInjector.getWifiServiceHandlerThread().getLooper());
replyToMessage(message, message.what, ok ? SUCCESS : FAILURE);
@@ -3908,7 +3975,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
case CMD_START_SUPPLICANT:
case CMD_STOP_SUPPLICANT:
case CMD_DRIVER_START_TIMED_OUT:
- case CMD_START_AP:
case CMD_START_AP_FAILURE:
case CMD_STOP_AP:
case CMD_AP_STOPPED:
@@ -3926,7 +3992,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
case WifiMonitor.AUTHENTICATION_FAILURE_EVENT:
case WifiMonitor.ASSOCIATION_REJECTION_EVENT:
case WifiMonitor.WPS_OVERLAP_EVENT:
- case CMD_SET_OPERATIONAL_MODE:
case CMD_RSSI_POLL:
case DhcpClient.CMD_PRE_DHCP_ACTION:
case DhcpClient.CMD_PRE_DHCP_ACTION_COMPLETE:
@@ -3949,6 +4014,28 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
case CMD_SELECT_TX_POWER_SCENARIO:
messageHandlingStatus = MESSAGE_HANDLING_STATUS_DISCARD;
break;
+ case CMD_START_AP:
+ transitionTo(mSoftApState);
+ break;
+ case CMD_SET_OPERATIONAL_MODE:
+ mOperationalMode = message.arg1;
+ // now processing the mode change - we will start setting up new state and want
+ // to know about failures
+ mModeChange = false;
+ if (mOperationalMode == DISABLED_MODE) {
+ Log.d(TAG, "set operational mode - disabled. stay in default");
+ transitionTo(mDefaultState);
+ break;
+ } else if (mOperationalMode == CONNECT_MODE) {
+ transitionTo(mInitialState);
+ } else if (mOperationalMode == SCAN_ONLY_MODE
+ || mOperationalMode == SCAN_ONLY_WITH_WIFI_OFF_MODE) {
+ transitionTo(mScanModeState);
+ } else {
+ Log.e(TAG, "set operational mode with invalid mode: " + mOperationalMode);
+ mOperationalMode = DISABLED_MODE;
+ }
+ break;
case CMD_SET_SUSPEND_OPT_ENABLED:
if (message.arg1 == 1) {
if (message.arg2 == 1) {
@@ -3986,7 +4073,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
WifiManager.BUSY);
break;
case CMD_GET_SUPPORTED_FEATURES:
- int featureSet = mWifiNative.getSupportedFeatureSet();
+ int featureSet = mWifiNative.getSupportedFeatureSet(mInterfaceName);
replyToMessage(message, message.what, featureSet);
break;
case CMD_FIRMWARE_ALERT:
@@ -4027,9 +4114,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
case CMD_IP_REACHABILITY_LOST:
messageHandlingStatus = MESSAGE_HANDLING_STATUS_DISCARD;
break;
- case CMD_GET_CONNECTION_STATISTICS:
- replyToMessage(message, message.what, mWifiConnectionStatistics);
- break;
case CMD_REMOVE_APP_CONFIGURATIONS:
deferMessage(message);
break;
@@ -4094,28 +4178,21 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
deferMessage(message);
break;
case CMD_INSTALL_PACKET_FILTER:
- mWifiNative.installPacketFilter((byte[]) message.obj);
+ mWifiNative.installPacketFilter(mInterfaceName, (byte[]) message.obj);
break;
case CMD_SET_FALLBACK_PACKET_FILTERING:
if ((boolean) message.obj) {
- mWifiNative.startFilteringMulticastV4Packets();
+ mWifiNative.startFilteringMulticastV4Packets(mInterfaceName);
} else {
- mWifiNative.stopFilteringMulticastV4Packets();
+ mWifiNative.stopFilteringMulticastV4Packets(mInterfaceName);
}
break;
- case CMD_WIFICOND_BINDER_DEATH:
- Log.e(TAG, "wificond died unexpectedly. Triggering recovery");
- mWifiMetrics.incrementNumWificondCrashes();
+ case CMD_WIFINATIVE_FAILURE:
+ Log.e(TAG, "One of the native daemons died unexpectedly. Triggering recovery");
mWifiDiagnostics.captureBugReportData(
WifiDiagnostics.REPORT_REASON_WIFICOND_CRASH);
mWifiInjector.getSelfRecovery().trigger(SelfRecovery.REASON_WIFICOND_CRASH);
break;
- case CMD_VENDOR_HAL_HWBINDER_DEATH:
- Log.e(TAG, "Vendor HAL died unexpectedly. Triggering recovery");
- mWifiMetrics.incrementNumHalCrashes();
- mWifiDiagnostics.captureBugReportData(WifiDiagnostics.REPORT_REASON_HAL_CRASH);
- mWifiInjector.getSelfRecovery().trigger(SelfRecovery.REASON_HAL_CRASH);
- break;
case CMD_DIAGS_CONNECT_TIMEOUT:
mWifiDiagnostics.reportConnectionEvent(
(Long) message.obj, BaseWifiDiagnostics.CONNECTION_EVENT_FAILED);
@@ -4137,22 +4214,30 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
class InitialState extends State {
-
private void cleanup() {
+ // tell scanning service that scans are not available - about to kill the interface and
+ // supplicant
+ sendWifiScanAvailable(false);
+
// Tearing down the client interfaces below is going to stop our supplicant.
mWifiMonitor.stopAllMonitoring();
- // stop hostapd in case it was running from SoftApMode
- mWifiNative.stopSoftAp();
-
- mWifiNative.deregisterWificondDeathHandler();
- mWifiNative.tearDown();
+ mWifiNative.registerStatusListener(mWifiNativeStatusListener);
+ // TODO: This teardown should ideally be handled in STOP_SUPPLICANT to be consistent
+ // with other mode managers. But, client mode is not yet controlled by
+ // WifiStateMachinePrime.
+ // TODO: Remove this big hammer. We cannot support concurrent interfaces with this!
+ mWifiNative.teardownAllInterfaces();
+ mInterfaceName = null;
}
@Override
public void enter() {
+ mWifiMonitor.stopAllMonitoring();
mWifiStateTracker.updateState(WifiStateTracker.INVALID);
cleanup();
+ sendMessage(CMD_START_SUPPLICANT);
+ setWifiState(WIFI_STATE_ENABLING);
}
@Override
@@ -4160,62 +4245,30 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
logStateAndMessage(message, this);
switch (message.what) {
case CMD_START_SUPPLICANT:
- Pair<Integer, IClientInterface> statusAndInterface =
- mWifiNative.setupForClientMode(mInterfaceName);
- if (statusAndInterface.first == WifiNative.SETUP_SUCCESS) {
- mClientInterface = statusAndInterface.second;
- } else {
- incrementMetricsForSetupFailure(statusAndInterface.first);
- }
- if (mClientInterface == null
- || !mWifiNative.registerWificondDeathHandler(mWificondDeathRecipient)) {
- setWifiState(WifiManager.WIFI_STATE_UNKNOWN);
- cleanup();
- break;
- }
-
- try {
- // A runtime crash or shutting down AP mode can leave
- // IP addresses configured, and this affects
- // connectivity when supplicant starts up.
- // Ensure we have no IP addresses before a supplicant start.
- mNwService.clearInterfaceAddresses(mInterfaceName);
-
- // Set privacy extensions
- mNwService.setInterfaceIpv6PrivacyExtensions(mInterfaceName, true);
-
- // IPv6 is enabled only as long as access point is connected since:
- // - IPv6 addresses and routes stick around after disconnection
- // - kernel is unaware when connected and fails to start IPv6 negotiation
- // - kernel can start autoconfiguration when 802.1x is not complete
- mNwService.disableIpv6(mInterfaceName);
- } catch (RemoteException re) {
- loge("Unable to change interface settings: " + re);
- } catch (IllegalStateException ie) {
- loge("Unable to change interface settings: " + ie);
- }
-
- if (!mWifiNative.enableSupplicant()) {
- loge("Failed to start supplicant!");
+ mInterfaceName = mWifiNative.setupInterfaceForClientMode(
+ mWifiNativeInterfaceCallback);
+ if (TextUtils.isEmpty(mInterfaceName)) {
+ Log.e(TAG, "setup failure when creating client interface.");
setWifiState(WifiManager.WIFI_STATE_UNKNOWN);
- cleanup();
+ transitionTo(mDefaultState);
break;
}
+ mIpClient = mFacade.makeIpClient(
+ mContext, mInterfaceName, new IpClientCallback());
+ mIpClient.setMulticastFilter(true);
if (mVerboseLoggingEnabled) log("Supplicant start successful");
- mWifiMonitor.startMonitoring(mInterfaceName, true);
+ registerForWifiMonitorEvents();
+ mWifiMonitor.startMonitoring(mInterfaceName);
mWifiInjector.getWifiLastResortWatchdog().clearAllFailureCounts();
setSupplicantLogLevel();
transitionTo(mSupplicantStartingState);
break;
- case CMD_START_AP:
- transitionTo(mSoftApState);
- break;
case CMD_SET_OPERATIONAL_MODE:
- mOperationalMode = message.arg1;
- if (mOperationalMode != DISABLED_MODE) {
- sendMessage(CMD_START_SUPPLICANT);
+ if (message.arg1 == CONNECT_MODE) {
+ break;
+ } else {
+ return NOT_HANDLED;
}
- break;
default:
return NOT_HANDLED;
}
@@ -4227,29 +4280,30 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
private void initializeWpsDetails() {
String detail;
detail = mPropertyService.get("ro.product.name", "");
- if (!mWifiNative.setDeviceName(detail)) {
- loge("Failed to set device name " + detail);
+ if (!mWifiNative.setDeviceName(mInterfaceName, detail)) {
+ loge("Failed to set device name " + detail);
}
detail = mPropertyService.get("ro.product.manufacturer", "");
- if (!mWifiNative.setManufacturer(detail)) {
+ if (!mWifiNative.setManufacturer(mInterfaceName, detail)) {
loge("Failed to set manufacturer " + detail);
}
detail = mPropertyService.get("ro.product.model", "");
- if (!mWifiNative.setModelName(detail)) {
+ if (!mWifiNative.setModelName(mInterfaceName, detail)) {
loge("Failed to set model name " + detail);
}
detail = mPropertyService.get("ro.product.model", "");
- if (!mWifiNative.setModelNumber(detail)) {
+ if (!mWifiNative.setModelNumber(mInterfaceName, detail)) {
loge("Failed to set model number " + detail);
}
detail = mPropertyService.get("ro.serialno", "");
- if (!mWifiNative.setSerialNumber(detail)) {
+ if (!mWifiNative.setSerialNumber(mInterfaceName, detail)) {
loge("Failed to set serial number " + detail);
}
- if (!mWifiNative.setConfigMethods("physical_display virtual_push_button")) {
+ if (!mWifiNative.setConfigMethods(
+ mInterfaceName, "physical_display virtual_push_button")) {
loge("Failed to set WPS config methods");
}
- if (!mWifiNative.setDeviceType(mPrimaryDeviceType)) {
+ if (!mWifiNative.setDeviceType(mInterfaceName, mPrimaryDeviceType)) {
loge("Failed to set primary device type " + mPrimaryDeviceType);
}
}
@@ -4270,38 +4324,22 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
mLastBssid = null;
mLastNetworkId = WifiConfiguration.INVALID_NETWORK_ID;
mLastSignalLevel = -1;
-
- mWifiInfo.setMacAddress(mWifiNative.getMacAddress());
+ mWifiInfo.setMacAddress(mWifiNative.getMacAddress(mInterfaceName));
+ // Attempt to migrate data out of legacy store.
+ if (!mWifiConfigManager.migrateFromLegacyStore()) {
+ Log.e(TAG, "Failed to migrate from legacy config store");
+ }
initializeWpsDetails();
sendSupplicantConnectionChangedBroadcast(true);
transitionTo(mSupplicantStartedState);
break;
case WifiMonitor.SUP_DISCONNECTION_EVENT:
- if (++mSupplicantRestartCount <= SUPPLICANT_RESTART_TRIES) {
- loge("Failed to setup control channel, restart supplicant");
- mWifiMonitor.stopAllMonitoring();
- mWifiNative.disableSupplicant();
- transitionTo(mInitialState);
- sendMessageDelayed(CMD_START_SUPPLICANT, SUPPLICANT_RESTART_INTERVAL_MSECS);
- } else {
- loge("Failed " + mSupplicantRestartCount +
- " times to start supplicant, unload driver");
- mSupplicantRestartCount = 0;
- setWifiState(WIFI_STATE_UNKNOWN);
- transitionTo(mInitialState);
- }
- break;
- case CMD_START_AP:
- // now go directly to softap mode since we handle teardown in WSMP
- transitionTo(mSoftApState);
+ // since control is split between WSM and WSMP - do not worry about supplicant
+ // dying if we haven't seen it up yet
break;
case CMD_START_SUPPLICANT:
case CMD_STOP_SUPPLICANT:
case CMD_STOP_AP:
- case CMD_SET_OPERATIONAL_MODE:
- messageHandlingStatus = MESSAGE_HANDLING_STATUS_DEFERRED;
- deferMessage(message);
- break;
default:
return NOT_HANDLED;
}
@@ -4316,7 +4354,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
logd("SupplicantStartedState enter");
}
- mWifiNative.setExternalSim(true);
+ mWifiNative.setExternalSim(mInterfaceName, true);
setRandomMacOui();
mCountryCode.setReadyForChange(true);
@@ -4325,7 +4363,9 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
// wifi scanning service is initialized
if (mWifiScanner == null) {
mWifiScanner = mWifiInjector.getWifiScanner();
+ }
+ if (mWifiConnectivityManager == null) {
synchronized (mWifiReqCountLock) {
mWifiConnectivityManager =
mWifiInjector.makeWifiConnectivityManager(mWifiInfo,
@@ -4343,7 +4383,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
* When this mode is on, some of the low-level scan parameters used by the
* driver are changed to reduce interference with bluetooth
*/
- mWifiNative.setBluetoothCoexistenceScanMode(mBluetoothConnectionActive);
+ mWifiNative.setBluetoothCoexistenceScanMode(mInterfaceName, mBluetoothConnectionActive);
// Check if there is a voice call on-going and set/reset the tx power limit
// appropriately.
if (mEnableVoiceCallSarTxPowerLimit) {
@@ -4363,45 +4403,24 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
// Legacy IPv6 multicast filtering blocks ICMPv6 router advertisements which breaks IPv6
// provisioning. Legacy IPv4 multicast filtering may be re-enabled later via
// IpClient.Callback.setFallbackMulticastFilter()
- mWifiNative.stopFilteringMulticastV4Packets();
- mWifiNative.stopFilteringMulticastV6Packets();
-
- if (mOperationalMode == SCAN_ONLY_MODE ||
- mOperationalMode == SCAN_ONLY_WITH_WIFI_OFF_MODE) {
- mWifiNative.disconnect();
- setWifiState(WIFI_STATE_DISABLED);
- transitionTo(mScanModeState);
- } else if (mOperationalMode == CONNECT_MODE) {
- setWifiState(WIFI_STATE_ENABLING);
- // Transitioning to Disconnected state will trigger a scan and subsequently AutoJoin
- transitionTo(mDisconnectedState);
- } else if (mOperationalMode == DISABLED_MODE) {
- transitionTo(mSupplicantStoppingState);
- }
+ mWifiNative.stopFilteringMulticastV4Packets(mInterfaceName);
+ mWifiNative.stopFilteringMulticastV6Packets(mInterfaceName);
+
+ // Transitioning to Disconnected state will trigger a scan and subsequently AutoJoin
+ transitionTo(mDisconnectedState);
// Set the right suspend mode settings
- mWifiNative.setSuspendOptimizations(mSuspendOptNeedsDisabled == 0
+ mWifiNative.setSuspendOptimizations(mInterfaceName, mSuspendOptNeedsDisabled == 0
&& mUserWantsSuspendOpt.get());
- mWifiNative.setPowerSave(true);
+ mWifiNative.setPowerSave(mInterfaceName, true);
if (mP2pSupported) {
- if (mOperationalMode == CONNECT_MODE) {
- p2pSendMessage(WifiStateMachine.CMD_ENABLE_P2P);
- } else {
- // P2P state machine starts in disabled state, and is not enabled until
- // CMD_ENABLE_P2P is sent from here; so, nothing needs to be done to
- // keep it disabled.
- }
+ p2pSendMessage(WifiStateMachine.CMD_ENABLE_P2P);
}
- final Intent intent = new Intent(WifiManager.WIFI_SCAN_AVAILABLE);
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- intent.putExtra(WifiManager.EXTRA_SCAN_AVAILABLE, WIFI_STATE_ENABLED);
- mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
-
// Disable wpa_supplicant from auto reconnecting.
- mWifiNative.enableStaAutoReconnect(false);
+ mWifiNative.enableStaAutoReconnect(mInterfaceName, false);
// STA has higher priority over P2P
mWifiNative.setConcurrencyPriority(true);
}
@@ -4411,24 +4430,18 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
logStateAndMessage(message, this);
switch(message.what) {
- case CMD_STOP_SUPPLICANT: /* Supplicant stopped by user */
- if (mP2pSupported) {
- transitionTo(mWaitForP2pDisableState);
- } else {
- transitionTo(mSupplicantStoppingState);
- }
- break;
case WifiMonitor.SUP_DISCONNECTION_EVENT: /* Supplicant connection lost */
+ // first check if we are expecting a mode switch
+ if (mModeChange) {
+ logd("expecting a mode change, do not restart supplicant");
+ return HANDLED;
+ }
loge("Connection lost, restart supplicant");
handleSupplicantConnectionLoss(true);
handleNetworkDisconnect();
mSupplicantStateTracker.sendMessage(CMD_RESET_SUPPLICANT_STATE);
- if (mP2pSupported) {
- transitionTo(mWaitForP2pDisableState);
- } else {
- transitionTo(mInitialState);
- }
- sendMessageDelayed(CMD_START_SUPPLICANT, SUPPLICANT_RESTART_INTERVAL_MSECS);
+ sendMessage(CMD_START_SUPPLICANT);
+ transitionTo(mInitialState);
break;
case CMD_START_SCAN:
// TODO: remove scan request path (b/31445200)
@@ -4445,20 +4458,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
if (mBufferedScanMsg.size() > 0)
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);
- // now go directly to softap mode since we handle teardown in WSMP
- transitionTo(mSoftApState);
- break;
- case CMD_SET_OPERATIONAL_MODE:
- mOperationalMode = message.arg1;
- if (mOperationalMode == DISABLED_MODE) {
- transitionTo(mSupplicantStoppingState);
- }
- break;
case CMD_TARGET_BSSID:
// Trying to associate to this BSSID
if (message.obj != null) {
@@ -4476,7 +4475,8 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
case CMD_BLUETOOTH_ADAPTER_STATE_CHANGE:
mBluetoothConnectionActive = (message.arg1 !=
BluetoothAdapter.STATE_DISCONNECTED);
- mWifiNative.setBluetoothCoexistenceScanMode(mBluetoothConnectionActive);
+ mWifiNative.setBluetoothCoexistenceScanMode(
+ mInterfaceName, mBluetoothConnectionActive);
break;
case CMD_SET_SUSPEND_OPT_ENABLED:
if (message.arg1 == 1) {
@@ -4499,7 +4499,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
if (message.obj != null) {
String remoteAddress = (String) message.obj;
boolean enable = (message.arg1 == 1);
- mWifiNative.startTdls(remoteAddress, enable);
+ mWifiNative.startTdls(mInterfaceName, remoteAddress, enable);
}
break;
case WifiMonitor.ANQP_DONE_EVENT:
@@ -4525,7 +4525,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
break;
case CMD_CONFIG_ND_OFFLOAD:
final boolean enabled = (message.arg1 > 0);
- mWifiNative.configureNeighborDiscoveryOffload(enabled);
+ mWifiNative.configureNeighborDiscoveryOffload(mInterfaceName, enabled);
break;
case CMD_ENABLE_WIFI_CONNECTIVITY_MANAGER:
mWifiConnectivityManager.enable(message.arg1 == 1 ? true : false);
@@ -4545,101 +4545,31 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
@Override
public void exit() {
+ setWifiState(WIFI_STATE_DISABLING);
+ // when client mode is moved to WSMP, cleanup will be done on exit. For now, cleanup is
+ // done when entering a mode.
+
+ // exiting supplicant started state is now only applicable to client mode
mWifiDiagnostics.stopLogging();
+ if (mP2pSupported) {
+ // we are not going to wait for a response - will still temporarily send the
+ // disable command until p2p can detect the interface up/down on its own.
+ p2pSendMessage(WifiStateMachine.CMD_DISABLE_P2P_REQ);
+ }
+
+ handleNetworkDisconnect();
+
mIsRunning = false;
updateBatteryWorkSource(null);
mScanResults = new ArrayList<>();
- final Intent intent = new Intent(WifiManager.WIFI_SCAN_AVAILABLE);
- intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
- intent.putExtra(WifiManager.EXTRA_SCAN_AVAILABLE, WIFI_STATE_DISABLED);
- mContext.sendStickyBroadcastAsUser(intent, UserHandle.ALL);
mBufferedScanMsg.clear();
mNetworkInfo.setIsAvailable(false);
if (mNetworkAgent != null) mNetworkAgent.sendNetworkInfo(mNetworkInfo);
mCountryCode.setReadyForChange(false);
- }
- }
-
- class SupplicantStoppingState extends State {
- @Override
- public void enter() {
- /* Send any reset commands to supplicant before shutting it down */
- handleNetworkDisconnect();
-
- String suppState = System.getProperty("init.svc.wpa_supplicant");
- if (suppState == null) suppState = "unknown";
-
- setWifiState(WIFI_STATE_DISABLING);
- mSupplicantStateTracker.sendMessage(CMD_RESET_SUPPLICANT_STATE);
- logd("SupplicantStoppingState: disableSupplicant "
- + " init.svc.wpa_supplicant=" + suppState);
- if (mWifiNative.disableSupplicant()) {
- mWifiNative.closeSupplicantConnection();
- sendSupplicantConnectionChangedBroadcast(false);
- setWifiState(WIFI_STATE_DISABLED);
- } else {
- // Failed to disable supplicant
- handleSupplicantConnectionLoss(true);
- }
- transitionTo(mInitialState);
- }
- }
-
- class WaitForP2pDisableState extends State {
- private State mTransitionToState;
- @Override
- public void enter() {
- switch (getCurrentMessage().what) {
- case WifiMonitor.SUP_DISCONNECTION_EVENT:
- mTransitionToState = mInitialState;
- break;
- case CMD_STOP_SUPPLICANT:
- default:
- mTransitionToState = mSupplicantStoppingState;
- break;
- }
- if (p2pSendMessage(WifiStateMachine.CMD_DISABLE_P2P_REQ)) {
- sendMessageDelayed(obtainMessage(CMD_DISABLE_P2P_WATCHDOG_TIMER,
- mDisableP2pWatchdogCount, 0), DISABLE_P2P_GUARD_TIMER_MSEC);
- } else {
- transitionTo(mTransitionToState);
- }
- }
- @Override
- public boolean processMessage(Message message) {
- logStateAndMessage(message, this);
-
- switch(message.what) {
- case WifiStateMachine.CMD_DISABLE_P2P_RSP:
- transitionTo(mTransitionToState);
- break;
- case WifiStateMachine.CMD_DISABLE_P2P_WATCHDOG_TIMER:
- if (mDisableP2pWatchdogCount == message.arg1) {
- logd("Timeout waiting for CMD_DISABLE_P2P_RSP");
- transitionTo(mTransitionToState);
- }
- break;
- /* Defer wifi start/shut and driver commands */
- case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT:
- case CMD_START_SUPPLICANT:
- case CMD_STOP_SUPPLICANT:
- case CMD_START_AP:
- case CMD_STOP_AP:
- case CMD_SET_OPERATIONAL_MODE:
- case CMD_START_SCAN:
- case CMD_DISCONNECT:
- case CMD_REASSOCIATE:
- case CMD_RECONNECT:
- messageHandlingStatus = MESSAGE_HANDLING_STATUS_DEFERRED;
- deferMessage(message);
- break;
- default:
- return NOT_HANDLED;
- }
- return HANDLED;
+ setWifiState(WIFI_STATE_DISABLED);
}
}
@@ -4647,15 +4577,12 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
private int mLastOperationMode;
@Override
public void enter() {
- mLastOperationMode = mOperationalMode;
+ logd("entering ScanModeState");
mWifiStateTracker.updateState(WifiStateTracker.SCAN_MODE);
- mWifiInjector.getWakeupController().start();
}
@Override
- public void exit() {
- mWifiInjector.getWakeupController().stop();
- }
+ public void exit() {}
@Override
public boolean processMessage(Message message) {
@@ -4663,6 +4590,15 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
switch(message.what) {
case CMD_SET_OPERATIONAL_MODE:
+ int operationMode = message.arg1;
+ if (operationMode == SCAN_ONLY_MODE
+ || operationMode == SCAN_ONLY_WITH_WIFI_OFF_MODE) {
+ // nothing to do, stay here...
+ return HANDLED;
+ }
+ return NOT_HANDLED;
+
+ /*
if (message.arg1 == CONNECT_MODE) {
mOperationalMode = CONNECT_MODE;
setWifiState(WIFI_STATE_ENABLING);
@@ -4672,9 +4608,11 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
// Nothing to do
break;
+ */
// Handle scan. All the connection related commands are
// handled only in ConnectModeState
case CMD_START_SCAN:
+ // TODO b/31445200: as mentioned elsewhere, this needs to be moved out of WSM
handleScanRequest(message);
break;
default:
@@ -4876,11 +4814,13 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
@Override
public void enter() {
- if (!mWifiNative.removeAllNetworks()) {
+ if (!mWifiNative.removeAllNetworks(mInterfaceName)) {
loge("Failed to remove networks on entering connect mode");
}
+ mScanRequestProxy.enableScanningForHiddenNetworks(true);
mWifiInfo.reset();
mWifiInfo.setSupplicantState(SupplicantState.DISCONNECTED);
+ sendWifiScanAvailable(true);
// Let the system know that wifi is available in client mode.
setWifiState(WIFI_STATE_ENABLED);
@@ -4911,9 +4851,12 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
// Inform metrics that Wifi is being disabled (Toggled, airplane enabled, etc)
mWifiMetrics.setWifiState(WifiMetricsProto.WifiLog.WIFI_DISABLED);
- if (!mWifiNative.removeAllNetworks()) {
+ if (!mWifiNative.removeAllNetworks(mInterfaceName)) {
loge("Failed to remove networks on exiting connect mode");
}
+ mScanRequestProxy.enableScanningForHiddenNetworks(false);
+ // Do we want to optimize when we move from client mode to scan only mode.
+ mScanRequestProxy.clearScanResults();
mWifiInfo.reset();
mWifiInfo.setSupplicantState(SupplicantState.DISCONNECTED);
setWifiState(WIFI_STATE_DISABLED);
@@ -5000,15 +4943,15 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
SupplicantState state = handleSupplicantStateChange(message);
// A driver/firmware hang can now put the interface in a down state.
// We detect the interface going down and recover from it
- if (!SupplicantState.isDriverActive(state)) {
+ if (!SupplicantState.isDriverActive(state) && !mModeChange) {
if (mNetworkInfo.getState() != NetworkInfo.State.DISCONNECTED) {
handleNetworkDisconnect();
}
log("Detected an interface down, restart driver");
// Rely on the fact that this will force us into killing supplicant and then
// restart supplicant from a clean state.
- transitionTo(mSupplicantStoppingState);
sendMessage(CMD_START_SUPPLICANT);
+ transitionTo(mInitialState);
break;
}
@@ -5037,10 +4980,10 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
if (message.arg1 == 1) {
mWifiMetrics.logStaEvent(StaEvent.TYPE_FRAMEWORK_DISCONNECT,
StaEvent.DISCONNECT_P2P_DISCONNECT_WIFI_REQUEST);
- mWifiNative.disconnect();
+ mWifiNative.disconnect(mInterfaceName);
mTemporarilyDisconnectWifi = true;
} else {
- mWifiNative.reconnect();
+ mWifiNative.reconnect(mInterfaceName);
mTemporarilyDisconnectWifi = false;
}
break;
@@ -5104,8 +5047,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
mBackupManagerProxy.notifyDataChanged();
break;
case WifiMonitor.SUP_REQUEST_IDENTITY:
- int supplicantNetworkId = message.arg2;
- netId = lookupFrameworkNetworkId(supplicantNetworkId);
+ netId = message.arg2;
boolean identitySent = false;
// For SIM & AKA/AKA' EAP method Only, get identity from ICC
if (targetWificonfiguration != null
@@ -5113,9 +5055,9 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
&& TelephonyUtil.isSimConfig(targetWificonfiguration)) {
String identity =
TelephonyUtil.getSimIdentity(getTelephonyManager(),
- targetWificonfiguration);
+ new TelephonyUtil(), targetWificonfiguration);
if (identity != null) {
- mWifiNative.simIdentityResponse(supplicantNetworkId, identity);
+ mWifiNative.simIdentityResponse(mInterfaceName, netId, identity);
identitySent = true;
} else {
Log.e(TAG, "Unable to retrieve identity from Telephony");
@@ -5134,7 +5076,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
mWifiMetrics.logStaEvent(StaEvent.TYPE_FRAMEWORK_DISCONNECT,
StaEvent.DISCONNECT_GENERIC);
- mWifiNative.disconnect();
+ mWifiNative.disconnect(mInterfaceName);
}
break;
case WifiMonitor.SUP_REQUEST_SIM_AUTH:
@@ -5173,14 +5115,14 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
break;
case CMD_REASSOCIATE:
lastConnectAttemptTimestamp = mClock.getWallClockMillis();
- mWifiNative.reassociate();
+ mWifiNative.reassociate(mInterfaceName);
break;
case CMD_RELOAD_TLS_AND_RECONNECT:
if (mWifiConfigManager.needsUnlockedKeyStore()) {
logd("Reconnecting to give a chance to un-connected TLS networks");
- mWifiNative.disconnect();
+ mWifiNative.disconnect(mInterfaceName);
lastConnectAttemptTimestamp = mClock.getWallClockMillis();
- mWifiNative.reconnect();
+ mWifiNative.reconnect(mInterfaceName);
}
break;
case CMD_START_ROAM:
@@ -5219,9 +5161,18 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
mTargetNetworkId = netId;
setTargetBssid(config, bssid);
+ if (mEnableConnectedMacRandomization.get()) {
+ configureRandomizedMacAddress(config);
+ } else {
+ Log.i(TAG, "EnableConnectedMacRandomization setting is off");
+ }
+
+ Log.i(TAG, "Connecting with "
+ + mWifiNative.getMacAddress(mInterfaceName) + " as the mac address");
+
reportConnectionAttemptStart(config, mTargetRoamBSSID,
WifiMetricsProto.ConnectionEvent.ROAM_UNRELATED);
- if (mWifiNative.connectToNetwork(config)) {
+ if (mWifiNative.connectToNetwork(mInterfaceName, config)) {
mWifiMetrics.logStaEvent(StaEvent.TYPE_CMD_START_CONNECT, config);
lastConnectAttemptTimestamp = mClock.getWallClockMillis();
targetWificonfiguration = config;
@@ -5268,7 +5219,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
*/
netId = message.arg1;
config = (WifiConfiguration) message.obj;
- mWifiConnectionStatistics.numWifiManagerJoinAttempt++;
boolean hasCredentialChanged = false;
// New network addition.
if (config != null) {
@@ -5298,7 +5248,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
result = saveNetworkConfigAndSendReply(message);
netId = result.getNetworkId();
if (result.isSuccess() && mWifiInfo.getNetworkId() == netId) {
- mWifiConnectionStatistics.numWifiManagerJoinAttempt++;
if (result.hasCredentialChanged()) {
config = (WifiConfiguration) message.obj;
// The network credentials changed and we're connected to this network,
@@ -5348,12 +5297,12 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
WpsResult wpsResult = new WpsResult();
// TODO(b/32898136): Not needed when we start deleting networks from supplicant
// on disconnect.
- if (!mWifiNative.removeAllNetworks()) {
+ if (!mWifiNative.removeAllNetworks(mInterfaceName)) {
loge("Failed to remove networks before WPS");
}
switch (wpsInfo.setup) {
case WpsInfo.PBC:
- if (mWifiNative.startWpsPbc(wpsInfo.BSSID)) {
+ if (mWifiNative.startWpsPbc(mInterfaceName, wpsInfo.BSSID)) {
wpsResult.status = WpsResult.Status.SUCCESS;
} else {
Log.e(TAG, "Failed to start WPS push button configuration");
@@ -5361,7 +5310,8 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
break;
case WpsInfo.KEYPAD:
- if (mWifiNative.startWpsRegistrar(wpsInfo.BSSID, wpsInfo.pin)) {
+ if (mWifiNative.startWpsRegistrar(
+ mInterfaceName, wpsInfo.BSSID, wpsInfo.pin)) {
wpsResult.status = WpsResult.Status.SUCCESS;
} else {
Log.e(TAG, "Failed to start WPS push button configuration");
@@ -5369,7 +5319,8 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
break;
case WpsInfo.DISPLAY:
- wpsResult.pin = mWifiNative.startWpsPinDisplay(wpsInfo.BSSID);
+ wpsResult.pin = mWifiNative.startWpsPinDisplay(
+ mInterfaceName, wpsInfo.BSSID);
if (!TextUtils.isEmpty(wpsResult.pin)) {
wpsResult.status = WpsResult.Status.SUCCESS;
} else {
@@ -5407,7 +5358,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
return NOT_HANDLED;
case WifiMonitor.NETWORK_CONNECTION_EVENT:
if (mVerboseLoggingEnabled) log("Network connection established");
- mLastNetworkId = lookupFrameworkNetworkId(message.arg1);
+ mLastNetworkId = message.arg1;
mWifiConfigManager.clearRecentFailureReason(mLastNetworkId);
mLastBssid = (String) message.obj;
reasonCode = message.arg2;
@@ -5435,7 +5386,8 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
if (config.enterpriseConfig != null
&& TelephonyUtil.isSimEapMethod(
config.enterpriseConfig.getEapMethod())) {
- String anonymousIdentity = mWifiNative.getEapAnonymousIdentity();
+ String anonymousIdentity =
+ mWifiNative.getEapAnonymousIdentity(mInterfaceName);
if (anonymousIdentity != null) {
config.enterpriseConfig.setAnonymousIdentity(anonymousIdentity);
} else {
@@ -5840,7 +5792,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
if (getCurrentWifiConfiguration() == null) {
// The current config may have been removed while we were connecting,
// trigger a disconnect to clear up state.
- mWifiNative.disconnect();
+ mWifiNative.disconnect(mInterfaceName);
transitionTo(mDisconnectingState);
} else {
sendConnectedState();
@@ -5867,25 +5819,19 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
break;
case CMD_DISCONNECT:
mWifiMetrics.logStaEvent(StaEvent.TYPE_FRAMEWORK_DISCONNECT,
- StaEvent.DISCONNECT_UNKNOWN);
- mWifiNative.disconnect();
+ StaEvent.DISCONNECT_GENERIC);
+ mWifiNative.disconnect(mInterfaceName);
transitionTo(mDisconnectingState);
break;
case WifiP2pServiceImpl.DISCONNECT_WIFI_REQUEST:
if (message.arg1 == 1) {
mWifiMetrics.logStaEvent(StaEvent.TYPE_FRAMEWORK_DISCONNECT,
StaEvent.DISCONNECT_P2P_DISCONNECT_WIFI_REQUEST);
- mWifiNative.disconnect();
+ mWifiNative.disconnect(mInterfaceName);
mTemporarilyDisconnectWifi = true;
transitionTo(mDisconnectingState);
}
break;
- case CMD_SET_OPERATIONAL_MODE:
- if (message.arg1 != CONNECT_MODE) {
- sendMessage(CMD_DISCONNECT);
- deferMessage(message);
- }
- break;
/* Ignore connection to same network */
case WifiManager.CONNECT_NETWORK:
int netId = message.arg1;
@@ -5896,7 +5842,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
return NOT_HANDLED;
case WifiMonitor.NETWORK_CONNECTION_EVENT:
mWifiInfo.setBSSID((String) message.obj);
- mLastNetworkId = lookupFrameworkNetworkId(message.arg1);
+ mLastNetworkId = message.arg1;
mWifiInfo.setNetworkId(mLastNetworkId);
if(!mLastBssid.equals(message.obj)) {
mLastBssid = (String) message.obj;
@@ -5951,7 +5897,8 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
RssiPacketCountInfo info = new RssiPacketCountInfo();
fetchRssiLinkSpeedAndFrequencyNative();
info.rssi = mWifiInfo.getRssi();
- WifiNative.TxPacketCounters counters = mWifiNative.getTxPacketCounters();
+ WifiNative.TxPacketCounters counters =
+ mWifiNative.getTxPacketCounters(mInterfaceName);
if (counters != null) {
info.txgood = counters.txSucceeded;
info.txbad = counters.txFailed;
@@ -6023,7 +5970,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
if (TelephonyUtil.isSimConfig(config)) {
mWifiMetrics.logStaEvent(StaEvent.TYPE_FRAMEWORK_DISCONNECT,
StaEvent.DISCONNECT_RESET_SIM_NETWORKS);
- mWifiNative.disconnect();
+ mWifiNative.disconnect(mInterfaceName);
transitionTo(mDisconnectingState);
}
}
@@ -6083,15 +6030,16 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
if (!isUsingStaticIp) {
prov = IpClient.buildProvisioningConfiguration()
.withPreDhcpAction()
- .withApfCapabilities(mWifiNative.getApfCapabilities())
+ .withApfCapabilities(mWifiNative.getApfCapabilities(mInterfaceName))
.withNetwork(getCurrentNetwork())
.withDisplayName(currentConfig.SSID)
+ .withRandomMacAddress()
.build();
} else {
StaticIpConfiguration staticIpConfig = currentConfig.getStaticIpConfiguration();
prov = IpClient.buildProvisioningConfiguration()
.withStaticConfiguration(staticIpConfig)
- .withApfCapabilities(mWifiNative.getApfCapabilities())
+ .withApfCapabilities(mWifiNative.getApfCapabilities(mInterfaceName))
.withNetwork(getCurrentNetwork())
.withDisplayName(currentConfig.SSID)
.build();
@@ -6214,11 +6162,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
log("Roaming and CS doesnt want the network -> ignore");
}
return HANDLED;
- case CMD_SET_OPERATIONAL_MODE:
- if (message.arg1 != CONNECT_MODE) {
- deferMessage(message);
- }
- break;
case WifiMonitor.SUPPLICANT_STATE_CHANGE_EVENT:
/**
* If we get a SUPPLICANT_STATE_CHANGE_EVENT indicating a DISCONNECT
@@ -6259,7 +6202,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
handleNetworkDisconnect();
mWifiMetrics.logStaEvent(StaEvent.TYPE_FRAMEWORK_DISCONNECT,
StaEvent.DISCONNECT_ROAM_WATCHDOG_TIMER);
- mWifiNative.disconnect();
+ mWifiNative.disconnect(mInterfaceName);
transitionTo(mDisconnectedState);
}
break;
@@ -6268,7 +6211,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
if (mVerboseLoggingEnabled) {
log("roaming and Network connection established");
}
- mLastNetworkId = lookupFrameworkNetworkId(message.arg1);
+ mLastNetworkId = message.arg1;
mLastBssid = (String) message.obj;
mWifiInfo.setBSSID(mLastBssid);
mWifiInfo.setNetworkId(mLastNetworkId);
@@ -6377,7 +6320,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
if (message.arg1 == NETWORK_STATUS_UNWANTED_DISCONNECT) {
mWifiMetrics.logStaEvent(StaEvent.TYPE_FRAMEWORK_DISCONNECT,
StaEvent.DISCONNECT_UNWANTED);
- mWifiNative.disconnect();
+ mWifiNative.disconnect(mInterfaceName);
transitionTo(mDisconnectingState);
} else if (message.arg1 == NETWORK_STATUS_UNWANTED_DISABLE_AUTOJOIN ||
message.arg1 == NETWORK_STATUS_UNWANTED_VALIDATION_FAILED) {
@@ -6416,7 +6359,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
case CMD_TEST_NETWORK_DISCONNECT:
// Force a disconnect
if (message.arg1 == testNetworkDisconnectCounter) {
- mWifiNative.disconnect();
+ mWifiNative.disconnect(mInterfaceName);
}
break;
case CMD_ASSOCIATED_BSSID:
@@ -6515,7 +6458,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
reportConnectionAttemptStart(config, mTargetRoamBSSID,
WifiMetricsProto.ConnectionEvent.ROAM_ENTERPRISE);
- if (mWifiNative.roamToNetwork(config)) {
+ if (mWifiNative.roamToNetwork(mInterfaceName, config)) {
lastConnectAttemptTimestamp = mClock.getWallClockMillis();
targetWificonfiguration = config;
mIsAutoRoaming = true;
@@ -6536,21 +6479,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
int slot = message.arg1;
int intervalSeconds = message.arg2;
KeepalivePacketData pkt = (KeepalivePacketData) message.obj;
- byte[] dstMac;
- try {
- InetAddress gateway = RouteInfo.selectBestRoute(
- mLinkProperties.getRoutes(), pkt.dstAddress).getGateway();
- String dstMacStr = macAddressFromRoute(gateway.getHostAddress());
- dstMac = NativeUtil.macAddressToByteArray(dstMacStr);
- } catch (NullPointerException | IllegalArgumentException e) {
- loge("Can't find MAC address for next hop to " + pkt.dstAddress);
- if (mNetworkAgent != null) {
- mNetworkAgent.onPacketKeepaliveEvent(slot,
- ConnectivityManager.PacketKeepalive.ERROR_INVALID_IP_ADDRESS);
- }
- break;
- }
- pkt.dstMac = dstMac;
int result = startWifiIPPacketOffload(slot, pkt, intervalSeconds);
if (mNetworkAgent != null) {
mNetworkAgent.onPacketKeepaliveEvent(slot, result);
@@ -6599,11 +6527,6 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
public boolean processMessage(Message message) {
logStateAndMessage(message, this);
switch (message.what) {
- case CMD_SET_OPERATIONAL_MODE:
- if (message.arg1 != CONNECT_MODE) {
- deferMessage(message);
- }
- break;
case CMD_START_SCAN:
deferMessage(message);
return HANDLED;
@@ -6696,23 +6619,10 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
++mPeriodicScanToken, 0), mNoNetworksPeriodicScan);
ret = NOT_HANDLED;
break;
- case CMD_SET_OPERATIONAL_MODE:
- if (message.arg1 != CONNECT_MODE) {
- mOperationalMode = message.arg1;
- if (mOperationalMode == DISABLED_MODE) {
- transitionTo(mSupplicantStoppingState);
- } else if (mOperationalMode == SCAN_ONLY_MODE
- || mOperationalMode == SCAN_ONLY_WITH_WIFI_OFF_MODE) {
- p2pSendMessage(CMD_DISABLE_P2P_REQ);
- setWifiState(WIFI_STATE_DISABLED);
- transitionTo(mScanModeState);
- }
- }
- break;
case CMD_DISCONNECT:
mWifiMetrics.logStaEvent(StaEvent.TYPE_FRAMEWORK_DISCONNECT,
- StaEvent.DISCONNECT_UNKNOWN);
- mWifiNative.disconnect();
+ StaEvent.DISCONNECT_GENERIC);
+ mWifiNative.disconnect(mInterfaceName);
break;
/* Ignore network disconnect */
case WifiMonitor.NETWORK_DISCONNECTION_EVENT:
@@ -6859,7 +6769,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
break;
case WifiManager.CANCEL_WPS:
mWifiMetrics.incrementWpsCancellationCount();
- if (mWifiNative.cancelWps()) {
+ if (mWifiNative.cancelWps(mInterfaceName)) {
replyToMessage(message, WifiManager.CANCEL_WPS_SUCCEDED);
} else {
replyToMessage(message, WifiManager.CANCEL_WPS_FAILED, WifiManager.ERROR);
@@ -6870,19 +6780,14 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
* Defer all commands that can cause connections to a different network
* or put the state machine out of connect mode
*/
- case CMD_SET_OPERATIONAL_MODE:
case WifiManager.CONNECT_NETWORK:
case CMD_ENABLE_NETWORK:
- case CMD_RECONNECT:
- log(" Ignore CMD_RECONNECT request because wps is running");
- return HANDLED;
case CMD_REASSOCIATE:
deferMessage(message);
break;
+ case CMD_RECONNECT:
case CMD_START_CONNECT:
case CMD_START_ROAM:
- messageHandlingStatus = MESSAGE_HANDLING_STATUS_DISCARD;
- return HANDLED;
case CMD_START_SCAN:
messageHandlingStatus = MESSAGE_HANDLING_STATUS_DISCARD;
return HANDLED;
@@ -6923,7 +6828,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
Map<String, WifiConfiguration> configs = new HashMap<>();
SparseArray<Map<String, String>> extras = new SparseArray<>();
int netId = WifiConfiguration.INVALID_NETWORK_ID;
- if (!mWifiNative.migrateNetworksFromSupplicant(configs, extras)) {
+ if (!mWifiNative.migrateNetworksFromSupplicant(mInterfaceName, configs, extras)) {
loge("Failed to load networks from wpa_supplicant after Wps");
return Pair.create(false, WifiConfiguration.INVALID_NETWORK_ID);
}
@@ -6956,7 +6861,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
private int mMode;
/*
- private class SoftApListener implements SoftApManager.Listener {
+ private class SoftApCallbackImpl implements WifiManager.SoftApCallback {
@Override
public void onStateChanged(int state, int reason) {
if (state == WIFI_AP_STATE_DISABLED) {
@@ -6966,6 +6871,17 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
}
setWifiApState(state, reason, mIfaceName, mMode);
+
+ if (mSoftApCallback != null) {
+ mSoftApCallback.onStateChanged(state, reason);
+ }
+ }
+
+ @Override
+ public void onNumClientsChanged(int numClients) {
+ if (mSoftApCallback != null) {
+ mSoftApCallback.onNumClientsChanged(numClients);
+ }
}
}
*/
@@ -7009,7 +6925,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
checkAndSetConnectivityInstance();
mSoftApManager = mWifiInjector.makeSoftApManager(mNwService,
- new SoftApListener(),
+ new SoftApCallbackImpl(),
config);
mSoftApManager.start();
mWifiStateTracker.updateState(WifiStateTracker.SOFT_AP);
@@ -7033,13 +6949,13 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
break;
case CMD_STOP_AP:
//mSoftApManager.stop();
- transitionTo(mInitialState);
+ transitionTo(mDefaultState);
break;
case CMD_START_AP_FAILURE:
- transitionTo(mInitialState);
+ transitionTo(mDefaultState);
break;
case CMD_AP_STOPPED:
- transitionTo(mInitialState);
+ transitionTo(mDefaultState);
break;
default:
return NOT_HANDLED;
@@ -7107,7 +7023,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
void handleGsmAuthRequest(SimAuthRequestData requestData) {
if (targetWificonfiguration == null
|| targetWificonfiguration.networkId
- == lookupFrameworkNetworkId(requestData.networkId)) {
+ == requestData.networkId) {
logd("id matches targetWifiConfiguration");
} else {
logd("id does not match targetWifiConfiguration");
@@ -7117,10 +7033,11 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
String response =
TelephonyUtil.getGsmSimAuthResponse(requestData.data, getTelephonyManager());
if (response == null) {
- mWifiNative.simAuthFailedResponse(requestData.networkId);
+ mWifiNative.simAuthFailedResponse(mInterfaceName, requestData.networkId);
} else {
logv("Supplicant Response -" + response);
- mWifiNative.simAuthResponse(requestData.networkId,
+ mWifiNative.simAuthResponse(
+ mInterfaceName, requestData.networkId,
WifiNative.SIM_AUTH_RESP_TYPE_GSM_AUTH, response);
}
}
@@ -7128,7 +7045,7 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
void handle3GAuthRequest(SimAuthRequestData requestData) {
if (targetWificonfiguration == null
|| targetWificonfiguration.networkId
- == lookupFrameworkNetworkId(requestData.networkId)) {
+ == requestData.networkId) {
logd("id matches targetWifiConfiguration");
} else {
logd("id does not match targetWifiConfiguration");
@@ -7138,9 +7055,10 @@ public class WifiStateMachine extends StateMachine implements WifiNative.WifiRss
SimAuthResponseData response =
TelephonyUtil.get3GAuthResponse(requestData, getTelephonyManager());
if (response != null) {
- mWifiNative.simAuthResponse(requestData.networkId, response.type, response.response);
+ mWifiNative.simAuthResponse(
+ mInterfaceName, requestData.networkId, response.type, response.response);
} else {
- mWifiNative.umtsAuthFailedResponse(requestData.networkId);
+ mWifiNative.umtsAuthFailedResponse(mInterfaceName, requestData.networkId);
}
}
diff --git a/com/android/server/wifi/WifiStateMachinePrime.java b/com/android/server/wifi/WifiStateMachinePrime.java
index 2d3aaba1..96f9c5c8 100644
--- a/com/android/server/wifi/WifiStateMachinePrime.java
+++ b/com/android/server/wifi/WifiStateMachinePrime.java
@@ -19,7 +19,6 @@ package com.android.server.wifi;
import android.annotation.NonNull;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
-import android.os.INetworkManagementService;
import android.os.Looper;
import android.os.Message;
import android.util.Log;
@@ -45,37 +44,47 @@ public class WifiStateMachinePrime {
private final WifiInjector mWifiInjector;
private final Looper mLooper;
private final WifiNative mWifiNative;
- private final INetworkManagementService mNMService;
private Queue<SoftApModeConfiguration> mApConfigQueue = new ConcurrentLinkedQueue<>();
- private String mInterfaceName;
-
- /* The base for wifi message types */
+ // The base for wifi message types
static final int BASE = Protocol.BASE_WIFI;
- /* Start the soft access point */
+ // The message identifiers below are mapped to those in WifiStateMachine when applicable.
+ // Start the soft access point
static final int CMD_START_AP = BASE + 21;
- /* Indicates soft ap start failed */
+ // Indicates soft ap start failed
static final int CMD_START_AP_FAILURE = BASE + 22;
- /* Stop the soft access point */
+ // Stop the soft access point
static final int CMD_STOP_AP = BASE + 23;
- /* Soft access point teardown is completed. */
+ // Soft access point teardown is completed
static final int CMD_AP_STOPPED = BASE + 24;
- WifiStateMachinePrime(WifiInjector wifiInjector,
- Looper looper,
- WifiNative wifiNative,
- INetworkManagementService nmService) {
+ // Start Scan Only mode
+ static final int CMD_START_SCAN_ONLY_MODE = BASE + 200;
+ // Indicates that start Scan only mode failed
+ static final int CMD_START_SCAN_ONLY_MODE_FAILURE = BASE + 201;
+ // CMD_STOP_SCAN_ONLY-MODE
+ static final int CMD_STOP_SCAN_ONLY_MODE = BASE + 202;
+ // ScanOnly mode teardown is complete
+ static final int CMD_SCAN_ONLY_MODE_STOPPED = BASE + 203;
+ // ScanOnly mode failed
+ static final int CMD_SCAN_ONLY_MODE_FAILED = BASE + 204;
+
+ private WifiManager.SoftApCallback mSoftApCallback;
+
+ /**
+ * Called from WifiServiceImpl to register a callback for notifications from SoftApManager
+ */
+ public void registerSoftApCallback(WifiManager.SoftApCallback callback) {
+ mSoftApCallback = callback;
+ }
+
+ WifiStateMachinePrime(WifiInjector wifiInjector, Looper looper, WifiNative wifiNative) {
mWifiInjector = wifiInjector;
mLooper = looper;
mWifiNative = wifiNative;
- mNMService = nmService;
-
- mInterfaceName = mWifiNative.getInterfaceName();
-
- // make sure we do not have leftover state in the event of a restart
- mWifiNative.tearDown();
+ mModeStateMachine = new ModeStateMachine();
}
/**
@@ -128,15 +137,6 @@ public class WifiStateMachinePrime {
}
private void changeMode(int newMode) {
- if (mModeStateMachine == null) {
- if (newMode == ModeStateMachine.CMD_DISABLE_WIFI) {
- // command is to disable wifi, but it is already disabled.
- Log.e(TAG, "Received call to disable wifi when it is already disabled.");
- return;
- }
- // state machine was not initialized yet, we must be starting up.
- mModeStateMachine = new ModeStateMachine();
- }
mModeStateMachine.sendMessage(newMode);
}
@@ -205,8 +205,8 @@ public class WifiStateMachinePrime {
}
private void cleanup() {
- mWifiNative.disableSupplicant();
- mWifiNative.tearDown();
+ // TODO: Remove this big hammer. We cannot support concurrent interfaces with this!
+ mWifiNative.teardownAllInterfaces();
}
class ClientModeState extends State {
@@ -224,13 +224,25 @@ public class WifiStateMachinePrime {
@Override
public void exit() {
- cleanup();
+ // TODO: Activate this when client mode is handled here.
+ // cleanup();
}
}
class ScanOnlyModeState extends State {
+
@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_SCAN_ONLY_MODE) {
+ Log.d(TAG, "Entering ScanOnlyMode (idle)");
+ return;
+ }
+
+ mModeStateMachine.transitionTo(mScanOnlyModeActiveState);
}
@Override
@@ -243,9 +255,9 @@ public class WifiStateMachinePrime {
@Override
public void exit() {
- // Do not tear down interfaces yet since this mode is not actively controlled or
- // used in tests at this time.
- // tearDownInterfaces();
+ // while in transition, cleanup is done on entering states. in the future, each
+ // mode will clean up their own state on exit
+ //cleanup();
}
}
@@ -348,14 +360,57 @@ public class WifiStateMachinePrime {
}
class ScanOnlyModeActiveState extends ModeActiveState {
+ private class ScanOnlyListener implements ScanOnlyModeManager.Listener {
+ @Override
+ public void onStateChanged(int state) {
+ Log.d(TAG, "State changed from scan only mode.");
+ if (state == WifiManager.WIFI_STATE_UNKNOWN) {
+ // error while setting up scan mode or an unexpected failure.
+ mModeStateMachine.sendMessage(CMD_SCAN_ONLY_MODE_FAILED);
+ } else if (state == WifiManager.WIFI_STATE_DISABLED) {
+ //scan only mode stopped
+ mModeStateMachine.sendMessage(CMD_SCAN_ONLY_MODE_STOPPED);
+ } else if (state == WifiManager.WIFI_STATE_ENABLED) {
+ // scan mode is ready to go
+ Log.d(TAG, "scan mode active");
+ } else {
+ Log.d(TAG, "unexpected state update: " + state);
+ }
+ }
+ }
+
@Override
public void enter() {
- this.mActiveModeManager = new ScanOnlyModeManager();
+ Log.d(TAG, "Entering ScanOnlyModeActiveState");
+
+ this.mActiveModeManager = mWifiInjector.makeScanOnlyModeManager(
+ new ScanOnlyListener());
+ this.mActiveModeManager.start();
+ }
+
+ @Override
+ public boolean processMessage(Message message) {
+ switch(message.what) {
+ case CMD_START_SCAN_ONLY_MODE:
+ Log.d(TAG, "Received CMD_START_SCAN_ONLY_MODE when active - drop");
+ break;
+ case CMD_SCAN_ONLY_MODE_FAILED:
+ Log.d(TAG, "ScanOnlyMode failed, return to idle state.");
+ mModeStateMachine.transitionTo(mScanOnlyModeState);
+ break;
+ case CMD_SCAN_ONLY_MODE_STOPPED:
+ Log.d(TAG, "ScanOnlyMode stopped, return to idle state.");
+ mModeStateMachine.transitionTo(mScanOnlyModeState);
+ break;
+ default:
+ return NOT_HANDLED;
+ }
+ return HANDLED;
}
}
class SoftAPModeActiveState extends ModeActiveState {
- private class SoftApListener implements SoftApManager.Listener {
+ private class SoftApCallbackImpl implements WifiManager.SoftApCallback {
@Override
public void onStateChanged(int state, int reason) {
if (state == WifiManager.WIFI_AP_STATE_DISABLED) {
@@ -363,6 +418,21 @@ public class WifiStateMachinePrime {
} else if (state == WifiManager.WIFI_AP_STATE_FAILED) {
mModeStateMachine.sendMessage(CMD_START_AP_FAILURE);
}
+
+ if (mSoftApCallback != null) {
+ mSoftApCallback.onStateChanged(state, reason);
+ } else {
+ Log.wtf(TAG, "SoftApCallback is null. Dropping StateChanged event.");
+ }
+ }
+
+ @Override
+ public void onNumClientsChanged(int numClients) {
+ if (mSoftApCallback != null) {
+ mSoftApCallback.onNumClientsChanged(numClients);
+ } else {
+ Log.d(TAG, "SoftApCallback is null. Dropping NumClientsChanged event.");
+ }
}
}
@@ -377,8 +447,8 @@ public class WifiStateMachinePrime {
} else {
config = null;
}
- this.mActiveModeManager = mWifiInjector.makeSoftApManager(mNMService,
- new SoftApListener(), softApModeConfig);
+ this.mActiveModeManager = mWifiInjector.makeSoftApManager(
+ new SoftApCallbackImpl(), softApModeConfig);
this.mActiveModeManager.start();
}
diff --git a/com/android/server/wifi/WifiTrafficPoller.java b/com/android/server/wifi/WifiTrafficPoller.java
index 2efbb2e9..cd6c72d5 100644
--- a/com/android/server/wifi/WifiTrafficPoller.java
+++ b/com/android/server/wifi/WifiTrafficPoller.java
@@ -18,6 +18,7 @@ package com.android.server.wifi;
import static android.net.NetworkInfo.DetailedState.CONNECTED;
+import android.annotation.NonNull;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -30,6 +31,7 @@ import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
+import android.text.TextUtils;
import android.util.Log;
import java.io.FileDescriptor;
@@ -68,14 +70,14 @@ public class WifiTrafficPoller {
// the first time
private AtomicBoolean mScreenOn = new AtomicBoolean(true);
private final TrafficHandler mTrafficHandler;
+ private final WifiNative mWifiNative;
private NetworkInfo mNetworkInfo;
- private final String mInterface;
private boolean mVerboseLoggingEnabled = false;
- WifiTrafficPoller(Context context, Looper looper, String iface) {
- mInterface = iface;
+ WifiTrafficPoller(Context context, Looper looper, WifiNative wifiNative) {
mTrafficHandler = new TrafficHandler(looper);
+ mWifiNative = wifiNative;
IntentFilter filter = new IntentFilter();
filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
@@ -103,11 +105,13 @@ public class WifiTrafficPoller {
}, filter);
}
- void addClient(Messenger client) {
+ /** */
+ public void addClient(Messenger client) {
Message.obtain(mTrafficHandler, ADD_CLIENT, client).sendToTarget();
}
- void removeClient(Messenger client) {
+ /** */
+ public void removeClient(Messenger client) {
Message.obtain(mTrafficHandler, REMOVE_CLIENT, client).sendToTarget();
}
@@ -125,6 +129,7 @@ public class WifiTrafficPoller {
}
public void handleMessage(Message msg) {
+ String ifaceName;
switch (msg.what) {
case ENABLE_TRAFFIC_STATS_POLL:
mEnableTrafficStatsPoll = (msg.arg1 == 1);
@@ -134,8 +139,9 @@ public class WifiTrafficPoller {
+ Integer.toString(mTrafficStatsPollToken));
}
mTrafficStatsPollToken++;
- if (mEnableTrafficStatsPoll) {
- notifyOnDataActivity();
+ ifaceName = mWifiNative.getClientInterfaceName();
+ if (mEnableTrafficStatsPoll && !TextUtils.isEmpty(ifaceName)) {
+ notifyOnDataActivity(ifaceName);
sendMessageDelayed(Message.obtain(this, TRAFFIC_STATS_POLL,
mTrafficStatsPollToken, 0), POLL_TRAFFIC_STATS_INTERVAL_MSECS);
}
@@ -148,9 +154,12 @@ public class WifiTrafficPoller {
+ " num clients " + mClients.size());
}
if (msg.arg1 == mTrafficStatsPollToken) {
- notifyOnDataActivity();
- sendMessageDelayed(Message.obtain(this, TRAFFIC_STATS_POLL,
- mTrafficStatsPollToken, 0), POLL_TRAFFIC_STATS_INTERVAL_MSECS);
+ ifaceName = mWifiNative.getClientInterfaceName();
+ if (!TextUtils.isEmpty(ifaceName)) {
+ notifyOnDataActivity(ifaceName);
+ sendMessageDelayed(Message.obtain(this, TRAFFIC_STATS_POLL,
+ mTrafficStatsPollToken, 0), POLL_TRAFFIC_STATS_INTERVAL_MSECS);
+ }
}
break;
case ADD_CLIENT:
@@ -181,13 +190,13 @@ public class WifiTrafficPoller {
msg.sendToTarget();
}
- private void notifyOnDataActivity() {
+ private void notifyOnDataActivity(@NonNull String ifaceName) {
long sent, received;
long preTxPkts = mTxPkts, preRxPkts = mRxPkts;
int dataActivity = WifiManager.DATA_ACTIVITY_NONE;
- mTxPkts = TrafficStats.getTxPackets(mInterface);
- mRxPkts = TrafficStats.getRxPackets(mInterface);
+ mTxPkts = TrafficStats.getTxPackets(ifaceName);
+ mRxPkts = TrafficStats.getRxPackets(ifaceName);
if (DBG) {
Log.d(TAG, " packet count Tx="
diff --git a/com/android/server/wifi/WifiVendorHal.java b/com/android/server/wifi/WifiVendorHal.java
index d6b568d5..2fe8eab0 100644
--- a/com/android/server/wifi/WifiVendorHal.java
+++ b/com/android/server/wifi/WifiVendorHal.java
@@ -73,7 +73,6 @@ 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;
@@ -231,7 +230,8 @@ public class WifiVendorHal {
private final HalDeviceManager mHalDeviceManager;
private final HalDeviceManagerStatusListener mHalDeviceManagerStatusCallbacks;
private final IWifiStaIfaceEventCallback mIWifiStaIfaceEventCallback;
- private final IWifiChipEventCallback mIWifiChipEventCallback;
+ private final ChipEventCallback mIWifiChipEventCallback;
+ private final ChipEventCallbackV12 mIWifiChipEventCallbackV12;
private final RttEventCallback mRttEventCallback;
// Plumbing for event handling.
@@ -250,10 +250,10 @@ public class WifiVendorHal {
mHalDeviceManagerStatusCallbacks = new HalDeviceManagerStatusListener();
mIWifiStaIfaceEventCallback = new StaIfaceEventCallback();
mIWifiChipEventCallback = new ChipEventCallback();
+ mIWifiChipEventCallbackV12 = new ChipEventCallbackV12();
mRttEventCallback = new RttEventCallback();
}
- // TODO(mplass): figure out where we need locking in hidl world. b/33383725
public static final Object sLock = new Object();
private void handleRemoteException(RemoteException e) {
@@ -540,9 +540,14 @@ public class WifiVendorHal {
private boolean registerChipCallback() {
synchronized (sLock) {
if (mIWifiChip == null) return boolResult(false);
- if (mIWifiChipEventCallback == null) return boolResult(false);
try {
- WifiStatus status = mIWifiChip.registerEventCallback(mIWifiChipEventCallback);
+ WifiStatus status;
+ android.hardware.wifi.V1_2.IWifiChip iWifiChipV12 = getWifiChipForV1_2Mockable();
+ if (iWifiChipV12 != null) {
+ status = iWifiChipV12.registerEventCallback_1_2(mIWifiChipEventCallbackV12);
+ } else {
+ status = mIWifiChip.registerEventCallback(mIWifiChipEventCallback);
+ }
return ok(status);
} catch (RemoteException e) {
handleRemoteException(e);
@@ -2186,17 +2191,18 @@ public class WifiVendorHal {
* @param ifaceName Name of the interface.
* @param slot
* @param srcMac
+ * @param dstMac
* @param keepAlivePacket
+ * @param protocol
* @param periodInMs
* @return 0 for success, -1 for error
*/
public int startSendingOffloadedPacket(
- @NonNull String ifaceName, int slot, byte[] srcMac,
- KeepalivePacketData keepAlivePacket, int periodInMs) {
+ @NonNull String ifaceName, int slot, byte[] srcMac, byte[] dstMac,
+ byte[] packet, int protocol, int periodInMs) {
enter("slot=% periodInMs=%").c(slot).c(periodInMs).flush();
- ArrayList<Byte> data = NativeUtil.byteArrayToArrayList(keepAlivePacket.data);
- short protocol = (short) (keepAlivePacket.protocol);
+ ArrayList<Byte> data = NativeUtil.byteArrayToArrayList(packet);
synchronized (sLock) {
IWifiStaIface iface = getStaIface(ifaceName);
@@ -2205,9 +2211,9 @@ public class WifiVendorHal {
WifiStatus status = iface.startSendingKeepAlivePackets(
slot,
data,
- protocol,
+ (short) protocol,
srcMac,
- keepAlivePacket.dstMac,
+ dstMac,
periodInMs);
if (!ok(status)) return -1;
return 0;
@@ -2524,6 +2530,17 @@ public class WifiVendorHal {
return android.hardware.wifi.V1_1.IWifiChip.castFrom(mIWifiChip);
}
+ /**
+ * Method to mock out the V1_2 IWifiChip retrieval in unit tests.
+ *
+ * @return 1.2 IWifiChip object if the device is running the 1.2 wifi hal service, null
+ * otherwise.
+ */
+ protected android.hardware.wifi.V1_2.IWifiChip getWifiChipForV1_2Mockable() {
+ if (mIWifiChip == null) return null;
+ return android.hardware.wifi.V1_2.IWifiChip.castFrom(mIWifiChip);
+ }
+
private int frameworkToHalTxPowerScenario(int scenario) {
switch (scenario) {
case WifiNative.TX_POWER_SCENARIO_VOICE_CALL:
@@ -2592,7 +2609,6 @@ public class WifiVendorHal {
frameworkScanResult.level = scanResult.rssi;
frameworkScanResult.frequency = scanResult.frequency;
frameworkScanResult.timestamp = scanResult.timeStampInUs;
- frameworkScanResult.bytes = hidlIeArrayToFrameworkIeBlob(scanResult.informationElements);
return frameworkScanResult;
}
@@ -2687,7 +2703,7 @@ public class WifiVendorHal {
}
/**
- * Callback for events on the STA interface.
+ * Callback for events on the chip.
*/
private class ChipEventCallback extends IWifiChipEventCallback.Stub {
@Override
@@ -2776,6 +2792,50 @@ public class WifiVendorHal {
}
/**
+ * Callback for events on the 1.2 chip.
+ */
+ private class ChipEventCallbackV12 extends
+ android.hardware.wifi.V1_2.IWifiChipEventCallback.Stub {
+ @Override
+ public void onChipReconfigured(int modeId) {
+ mIWifiChipEventCallback.onChipReconfigured(modeId);
+ }
+
+ @Override
+ public void onChipReconfigureFailure(WifiStatus status) {
+ mIWifiChipEventCallback.onChipReconfigureFailure(status);
+ }
+
+ public void onIfaceAdded(int type, String name) {
+ mIWifiChipEventCallback.onIfaceAdded(type, name);
+ }
+
+ @Override
+ public void onIfaceRemoved(int type, String name) {
+ mIWifiChipEventCallback.onIfaceRemoved(type, name);
+ }
+
+ @Override
+ public void onDebugRingBufferDataAvailable(
+ WifiDebugRingBufferStatus status, java.util.ArrayList<Byte> data) {
+ mIWifiChipEventCallback.onDebugRingBufferDataAvailable(status, data);
+ }
+
+ @Override
+ public void onDebugErrorAlert(int errorCode, java.util.ArrayList<Byte> debugData) {
+ mIWifiChipEventCallback.onDebugErrorAlert(errorCode, debugData);
+ }
+
+ @Override
+ public void onRadioModeChange(
+ ArrayList<android.hardware.wifi.V1_2.IWifiChipEventCallback.RadioModeInfo>
+ radioModeInfoList) {
+ // Need to handle this callback.
+ mVerboseLog.d("onRadioModeChange " + radioModeInfoList);
+ }
+ }
+
+ /**
* Hal Device Manager callbacks.
*/
public class HalDeviceManagerStatusListener implements HalDeviceManager.ManagerStatusListener {
diff --git a/com/android/server/wifi/WificondControl.java b/com/android/server/wifi/WificondControl.java
index 68da98fb..dabb8462 100644
--- a/com/android/server/wifi/WificondControl.java
+++ b/com/android/server/wifi/WificondControl.java
@@ -17,6 +17,7 @@
package com.android.server.wifi;
import android.annotation.NonNull;
+import android.net.MacAddress;
import android.net.wifi.IApInterface;
import android.net.wifi.IApInterfaceEventCallback;
import android.net.wifi.IClientInterface;
@@ -25,7 +26,6 @@ import android.net.wifi.IScanEvent;
import android.net.wifi.IWifiScannerImpl;
import android.net.wifi.IWificond;
import android.net.wifi.ScanResult;
-import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiScanner;
import android.net.wifi.WifiSsid;
import android.os.Binder;
@@ -43,9 +43,9 @@ import com.android.server.wifi.wificond.HiddenNetwork;
import com.android.server.wifi.wificond.NativeScanResult;
import com.android.server.wifi.wificond.PnoNetwork;
import com.android.server.wifi.wificond.PnoSettings;
+import com.android.server.wifi.wificond.RadioChainInfo;
import com.android.server.wifi.wificond.SingleScanSettings;
-import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
@@ -186,7 +186,6 @@ public class WificondControl implements IBinder.DeathRecipient {
public boolean registerDeathHandler(@NonNull WifiNative.WificondDeathEventHandler handler) {
if (mDeathEventHandler != null) {
Log.e(TAG, "Death handler already present");
- return false;
}
mDeathEventHandler = handler;
return true;
@@ -199,7 +198,6 @@ public class WificondControl implements IBinder.DeathRecipient {
public boolean deregisterDeathHandler() {
if (mDeathEventHandler == null) {
Log.e(TAG, "No Death handler present");
- return false;
}
mDeathEventHandler = null;
return true;
@@ -545,16 +543,29 @@ public class WificondControl implements IBinder.DeathRecipient {
ScanDetail scanDetail = new ScanDetail(networkDetail, wifiSsid, bssid, flags,
result.signalMbm / 100, result.frequency, result.tsf, ies, null);
+ ScanResult scanResult = scanDetail.getScanResult();
// Update carrier network info if this AP's SSID is associated with a carrier Wi-Fi
// network and it uses EAP.
if (ScanResultUtil.isScanResultForEapNetwork(scanDetail.getScanResult())
&& mCarrierNetworkConfig.isCarrierNetwork(wifiSsid.toString())) {
- scanDetail.getScanResult().isCarrierAp = true;
- scanDetail.getScanResult().carrierApEapType =
+ scanResult.isCarrierAp = true;
+ scanResult.carrierApEapType =
mCarrierNetworkConfig.getNetworkEapType(wifiSsid.toString());
- scanDetail.getScanResult().carrierName =
+ scanResult.carrierName =
mCarrierNetworkConfig.getCarrierName(wifiSsid.toString());
}
+ // Fill up the radio chain info.
+ if (result.radioChainInfos != null) {
+ scanResult.radioChainInfos =
+ new ScanResult.RadioChainInfo[result.radioChainInfos.size()];
+ int idx = 0;
+ for (RadioChainInfo nativeRadioChainInfo : result.radioChainInfos) {
+ scanResult.radioChainInfos[idx] = new ScanResult.RadioChainInfo();
+ scanResult.radioChainInfos[idx].id = nativeRadioChainInfo.chainId;
+ scanResult.radioChainInfos[idx].level = nativeRadioChainInfo.level;
+ idx++;
+ }
+ }
results.add(scanDetail);
}
} catch (RemoteException e1) {
@@ -568,13 +579,31 @@ public class WificondControl implements IBinder.DeathRecipient {
}
/**
+ * Return scan type for the parcelable {@link SingleScanSettings}
+ */
+ private static int getScanType(int scanType) {
+ switch (scanType) {
+ case WifiNative.SCAN_TYPE_LOW_LATENCY:
+ return IWifiScannerImpl.SCAN_TYPE_LOW_SPAN;
+ case WifiNative.SCAN_TYPE_LOW_POWER:
+ return IWifiScannerImpl.SCAN_TYPE_LOW_POWER;
+ case WifiNative.SCAN_TYPE_HIGH_ACCURACY:
+ return IWifiScannerImpl.SCAN_TYPE_HIGH_ACCURACY;
+ default:
+ throw new IllegalArgumentException("Invalid scan type " + scanType);
+ }
+ }
+
+ /**
* Start a scan using wificond for the given parameters.
* @param ifaceName Name of the interface.
+ * @param scanType Type of scan to perform.
* @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(@NonNull String ifaceName,
+ int scanType,
Set<Integer> freqs,
Set<String> hiddenNetworkSSIDs) {
IWifiScannerImpl scannerImpl = getScannerImpl(ifaceName);
@@ -583,6 +612,12 @@ public class WificondControl implements IBinder.DeathRecipient {
return false;
}
SingleScanSettings settings = new SingleScanSettings();
+ try {
+ settings.scanType = getScanType(scanType);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Invalid scan type ", e);
+ return false;
+ }
settings.channelSettings = new ArrayList<>();
settings.hiddenNetworks = new ArrayList<>();
@@ -736,40 +771,24 @@ public class WificondControl implements IBinder.DeathRecipient {
}
/**
- * Start Soft AP operation using the provided configuration.
+ * Start hostapd
+ * TODO(b/71513606): Move this to a global operation.
*
* @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(@NonNull String ifaceName,
- WifiConfiguration config,
+ public boolean startHostapd(@NonNull String ifaceName,
SoftApListener listener) {
IApInterface iface = getApInterface(ifaceName);
if (iface == null) {
Log.e(TAG, "No valid ap interface handler");
return false;
}
- int encryptionType = getIApInterfaceEncryptionType(config);
try {
- // TODO(b/67745880) Note that config.SSID is intended to be either a
- // hex string or "double quoted".
- // However, it seems that whatever is handing us these configurations does not obey
- // this convention.
- boolean success = iface.writeHostapdConfig(
- config.SSID.getBytes(StandardCharsets.UTF_8), config.hiddenSSID,
- config.apChannel, encryptionType,
- (config.preSharedKey != null)
- ? config.preSharedKey.getBytes(StandardCharsets.UTF_8)
- : new byte[0]);
- if (!success) {
- Log.e(TAG, "Failed to write hostapd configuration");
- return false;
- }
IApInterfaceEventCallback callback = new ApInterfaceEventCallback(listener);
mApInterfaceListeners.put(ifaceName, callback);
- success = iface.startHostapd(callback);
+ boolean success = iface.startHostapd(callback);
if (!success) {
Log.e(TAG, "Failed to start hostapd.");
return false;
@@ -782,12 +801,13 @@ public class WificondControl implements IBinder.DeathRecipient {
}
/**
- * Stop the ongoing Soft AP operation.
+ * Stop hostapd
+ * TODO(b/71513606): Move this to a global operation.
*
* @param ifaceName Name of the interface.
* @return true on success, false otherwise.
*/
- public boolean stopSoftAp(@NonNull String ifaceName) {
+ public boolean stopHostapd(@NonNull String ifaceName) {
IApInterface iface = getApInterface(ifaceName);
if (iface == null) {
Log.e(TAG, "No valid ap interface handler");
@@ -807,25 +827,27 @@ public class WificondControl implements IBinder.DeathRecipient {
return true;
}
- private static int getIApInterfaceEncryptionType(WifiConfiguration localConfig) {
- int encryptionType;
- switch (localConfig.getAuthType()) {
- case WifiConfiguration.KeyMgmt.NONE:
- encryptionType = IApInterface.ENCRYPTION_TYPE_NONE;
- break;
- case WifiConfiguration.KeyMgmt.WPA_PSK:
- encryptionType = IApInterface.ENCRYPTION_TYPE_WPA;
- break;
- case WifiConfiguration.KeyMgmt.WPA2_PSK:
- encryptionType = IApInterface.ENCRYPTION_TYPE_WPA2;
- break;
- default:
- // We really shouldn't default to None, but this was how NetworkManagementService
- // used to do this.
- encryptionType = IApInterface.ENCRYPTION_TYPE_NONE;
- break;
+ /**
+ * Set Mac address on the given interface
+ * @param interfaceName Name of the interface.
+ * @param mac Mac address to change into
+ * @return true on success, false otherwise.
+ */
+ public boolean setMacAddress(@NonNull String interfaceName, @NonNull MacAddress mac) {
+ IClientInterface mClientInterface = getClientInterface(interfaceName);
+ if (mClientInterface == null) {
+ Log.e(TAG, "No valid wificond client interface handler");
+ return false;
}
- return encryptionType;
+ byte[] macByteArray = mac.toByteArray();
+
+ try {
+ mClientInterface.setMacAddress(macByteArray);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to setMacAddress due to remote exception");
+ return false;
+ }
+ return true;
}
/**
diff --git a/com/android/server/wifi/aware/WifiAwareDataPathStateManager.java b/com/android/server/wifi/aware/WifiAwareDataPathStateManager.java
index b2635974..0d8e6438 100644
--- a/com/android/server/wifi/aware/WifiAwareDataPathStateManager.java
+++ b/com/android/server/wifi/aware/WifiAwareDataPathStateManager.java
@@ -21,10 +21,12 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.hardware.wifi.V1_0.NanDataPathChannelCfg;
import android.hardware.wifi.V1_0.NanStatusType;
+import android.hardware.wifi.V1_2.NanDataPathChannelInfo;
import android.net.ConnectivityManager;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
+import android.net.MacAddress;
import android.net.MatchAllNetworkSpecifier;
import android.net.NetworkAgent;
import android.net.NetworkCapabilities;
@@ -60,6 +62,7 @@ import java.net.SocketException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -122,6 +125,7 @@ public class WifiAwareDataPathStateManager {
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
+ .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
.addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED);
sNetworkCapabilitiesFilter.setNetworkSpecifier(new MatchAllNetworkSpecifier());
@@ -452,13 +456,15 @@ public class WifiAwareDataPathStateManager {
* rejection/failure.
* @param message The message provided by the peer as part of the data-path setup
* process.
+ * @param channelInfo Lists of channels used for this NDP.
* @return The network specifier of the data-path or a null if none/error.
*/
public WifiAwareNetworkSpecifier onDataPathConfirm(int ndpId, byte[] mac, boolean accept,
- int reason, byte[] message) {
+ int reason, byte[] message, List<NanDataPathChannelInfo> channelInfo) {
if (VDBG) {
Log.v(TAG, "onDataPathConfirm: ndpId=" + ndpId + ", mac=" + String.valueOf(
- HexEncoding.encode(mac)) + ", accept=" + accept + ", reason=" + reason);
+ HexEncoding.encode(mac)) + ", accept=" + accept + ", reason=" + reason
+ + ", channelInfo=" + channelInfo);
}
Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation> nnriE =
@@ -487,6 +493,7 @@ public class WifiAwareDataPathStateManager {
if (accept) {
nnri.state = AwareNetworkRequestInformation.STATE_CONFIRMED;
nnri.peerDataMac = mac;
+ nnri.channelInfo = channelInfo;
NetworkInfo networkInfo = new NetworkInfo(ConnectivityManager.TYPE_NONE, 0,
NETWORK_TAG, "");
@@ -572,6 +579,35 @@ public class WifiAwareDataPathStateManager {
}
/**
+ * Notification (unsolicited/asynchronous) from the firmware that the channel for the specified
+ * NDP ids has been updated.
+ */
+ public void onDataPathSchedUpdate(byte[] peerMac, List<Integer> ndpIds,
+ List<NanDataPathChannelInfo> channelInfo) {
+ if (VDBG) {
+ Log.v(TAG, "onDataPathSchedUpdate: peerMac=" + MacAddress.fromBytes(peerMac).toString()
+ + ", ndpIds=" + ndpIds + ", channelInfo=" + channelInfo);
+ }
+
+ for (int ndpId : ndpIds) {
+ Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation> nnriE =
+ getNetworkRequestByNdpId(ndpId);
+ if (nnriE == null) {
+ Log.e(TAG, "onDataPathSchedUpdate: ndpId=" + ndpId + " - not found");
+ continue;
+ }
+ if (!Arrays.equals(peerMac, nnriE.getValue().peerDiscoveryMac)) {
+ Log.e(TAG, "onDataPathSchedUpdate: ndpId=" + ndpId + ", report NMI="
+ + MacAddress.fromBytes(peerMac).toString() + " doesn't match NDP NMI="
+ + MacAddress.fromBytes(nnriE.getValue().peerDiscoveryMac).toString());
+ continue;
+ }
+
+ nnriE.getValue().channelInfo = channelInfo;
+ }
+ }
+
+ /**
* Called whenever Aware comes down. Clean up all pending and up network requeests and agents.
*/
public void onAwareDownCleanupDataPaths() {
@@ -967,6 +1003,7 @@ public class WifiAwareDataPathStateManager {
public int ndpId = 0; // 0 is never a valid ID!
public byte[] peerDataMac;
public WifiAwareNetworkSpecifier networkSpecifier;
+ public List<NanDataPathChannelInfo> channelInfo;
public long startTimestamp = 0; // request is made (initiator) / get request (responder)
public WifiAwareNetworkAgent networkAgent;
@@ -1159,7 +1196,8 @@ public class WifiAwareDataPathStateManager {
", ndpId=").append(ndpId).append(", peerDataMac=").append(
peerDataMac == null ? ""
: String.valueOf(HexEncoding.encode(peerDataMac))).append(
- ", startTimestamp=").append(startTimestamp).append(", equivalentSpecifiers=[");
+ ", startTimestamp=").append(startTimestamp).append(", channelInfo=").append(
+ channelInfo).append(", equivalentSpecifiers=[");
for (WifiAwareNetworkSpecifier ns: equivalentSpecifiers) {
sb.append(ns.toString()).append(", ");
}
diff --git a/com/android/server/wifi/aware/WifiAwareNativeApi.java b/com/android/server/wifi/aware/WifiAwareNativeApi.java
index 66bbff52..64fdfe96 100644
--- a/com/android/server/wifi/aware/WifiAwareNativeApi.java
+++ b/com/android/server/wifi/aware/WifiAwareNativeApi.java
@@ -33,6 +33,7 @@ import android.hardware.wifi.V1_0.NanTransmitFollowupRequest;
import android.hardware.wifi.V1_0.NanTxType;
import android.hardware.wifi.V1_0.WifiStatus;
import android.hardware.wifi.V1_0.WifiStatusCode;
+import android.hardware.wifi.V1_2.NanConfigRequestSupplemental;
import android.net.wifi.aware.ConfigRequest;
import android.net.wifi.aware.PublishConfig;
import android.net.wifi.aware.SubscribeConfig;
@@ -68,26 +69,54 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
onReset();
}
+ /**
+ * (HIDL) Cast the input to a 1.2 NAN interface (possibly resulting in a null).
+ *
+ * Separate function so can be mocked in unit tests.
+ */
+ public android.hardware.wifi.V1_2.IWifiNanIface mockableCastTo_1_2(IWifiNanIface iface) {
+ return android.hardware.wifi.V1_2.IWifiNanIface.castFrom(iface);
+ }
+
/*
* Parameters settable through the shell command.
- * see wifi/1.0/types.hal NanBandSpecificConfig.discoveryWindowIntervalVal for description
+ * see wifi/1.0/types.hal NanBandSpecificConfig.discoveryWindowIntervalVal and
+ * wifi/1.2/types.hal NanConfigRequestSupplemental_1_2 for description
*/
- public static final String PARAM_DW_DEFAULT_24GHZ = "dw_default_24ghz";
- public static final int PARAM_DW_DEFAULT_24GHZ_DEFAULT = -1; // Firmware default
- public static final String PARAM_DW_DEFAULT_5GHZ = "dw_default_5ghz";
- public static final int PARAM_DW_DEFAULT_5GHZ_DEFAULT = -1; // Firmware default
- public static final String PARAM_DW_ON_INACTIVE_24GHZ = "dw_on_inactive_24ghz";
- public static final int PARAM_DW_ON_INACTIVE_24GHZ_DEFAULT = 4; // 4 -> DW=8, latency=4s
- public static final String PARAM_DW_ON_INACTIVE_5GHZ = "dw_on_inactive_5ghz";
- public static final int PARAM_DW_ON_INACTIVE_5GHZ_DEFAULT = 0; // 0 = disabled
- public static final String PARAM_DW_ON_IDLE_24GHZ = "dw_on_idle_24ghz";
- public static final int PARAM_DW_ON_IDLE_24GHZ_DEFAULT = -1; // NOP (but disabling on IDLE)
- public static final String PARAM_DW_ON_IDLE_5GHZ = "dw_on_idle_5ghz";
- public static final int PARAM_DW_ON_IDLE_5GHZ_DEFAULT = -1; // NOP (but disabling on IDLE)
-
- public static final String PARAM_MAC_RANDOM_INTERVAL_SEC = "mac_random_interval_sec";
- public static final int PARAM_MAC_RANDOM_INTERVAL_SEC_DEFAULT = 1800; // 30 minutes
-
+ /* package */ static final String POWER_PARAM_DEFAULT_KEY = "default";
+ /* package */ static final String POWER_PARAM_INACTIVE_KEY = "inactive";
+ /* package */ static final String POWER_PARAM_IDLE_KEY = "idle";
+
+ /* package */ static final String PARAM_DW_24GHZ = "dw_24ghz";
+ private static final int PARAM_DW_24GHZ_DEFAULT = -1; // Firmware default
+ private static final int PARAM_DW_24GHZ_INACTIVE = 4; // 4 -> DW=8, latency=4s
+ private static final int PARAM_DW_24GHZ_IDLE = 4; // == inactive
+
+ /* package */ static final String PARAM_DW_5GHZ = "dw_5ghz";
+ private static final int PARAM_DW_5GHZ_DEFAULT = -1; // Firmware default
+ private static final int PARAM_DW_5GHZ_INACTIVE = 0; // 0 = disabled
+ private static final int PARAM_DW_5GHZ_IDLE = 0; // == inactive
+
+ /* package */ static final String PARAM_DISCOVERY_BEACON_INTERVAL_MS =
+ "disc_beacon_interval_ms";
+ private static final int PARAM_DISCOVERY_BEACON_INTERVAL_MS_DEFAULT = 0; // Firmware defaults
+ private static final int PARAM_DISCOVERY_BEACON_INTERVAL_MS_INACTIVE = 0; // Firmware defaults
+ private static final int PARAM_DISCOVERY_BEACON_INTERVAL_MS_IDLE = 0; // Firmware defaults
+
+ /* package */ static final String PARAM_NUM_SS_IN_DISCOVERY = "num_ss_in_discovery";
+ private static final int PARAM_NUM_SS_IN_DISCOVERY_DEFAULT = 0; // Firmware defaults
+ private static final int PARAM_NUM_SS_IN_DISCOVERY_INACTIVE = 0; // Firmware defaults
+ private static final int PARAM_NUM_SS_IN_DISCOVERY_IDLE = 0; // Firmware defaults
+
+ /* package */ static final String PARAM_ENABLE_DW_EARLY_TERM = "enable_dw_early_term";
+ private static final int PARAM_ENABLE_DW_EARLY_TERM_DEFAULT = 0; // boolean: 0 = false
+ private static final int PARAM_ENABLE_DW_EARLY_TERM_INACTIVE = 0; // boolean: 0 = false
+ private static final int PARAM_ENABLE_DW_EARLY_TERM_IDLE = 0; // boolean: 0 = false
+
+ /* package */ static final String PARAM_MAC_RANDOM_INTERVAL_SEC = "mac_random_interval_sec";
+ private static final int PARAM_MAC_RANDOM_INTERVAL_SEC_DEFAULT = 1800; // 30 minutes
+
+ private Map<String, Map<String, Integer>> mSettablePowerParameters = new HashMap<>();
private Map<String, Integer> mSettableParameters = new HashMap<>();
/**
@@ -122,6 +151,35 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
mSettableParameters.put(name, value);
return 0;
}
+ case "set-power": {
+ String mode = parentShell.getNextArgRequired();
+ String name = parentShell.getNextArgRequired();
+ String valueStr = parentShell.getNextArgRequired();
+
+ if (VDBG) {
+ Log.v(TAG, "onCommand: mode='" + mode + "', name='" + name + "'" + ", value='"
+ + valueStr + "'");
+ }
+
+ if (!mSettablePowerParameters.containsKey(mode)) {
+ pw.println("Unknown mode name -- '" + mode + "'");
+ return -1;
+ }
+ if (!mSettablePowerParameters.get(mode).containsKey(name)) {
+ pw.println("Unknown parameter name '" + name + "' in mode '" + mode + "'");
+ return -1;
+ }
+
+ int value;
+ try {
+ value = Integer.valueOf(valueStr);
+ } catch (NumberFormatException e) {
+ pw.println("Can't convert value to integer -- '" + valueStr + "'");
+ return -1;
+ }
+ mSettablePowerParameters.get(mode).put(name, value);
+ return 0;
+ }
case "get": {
String name = parentShell.getNextArgRequired();
if (VDBG) Log.v(TAG, "onCommand: name='" + name + "'");
@@ -133,6 +191,23 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
parentShell.getOutPrintWriter().println((int) mSettableParameters.get(name));
return 0;
}
+ case "get-power": {
+ String mode = parentShell.getNextArgRequired();
+ String name = parentShell.getNextArgRequired();
+ if (VDBG) Log.v(TAG, "onCommand: mode='" + mode + "', name='" + name + "'");
+ if (!mSettablePowerParameters.containsKey(mode)) {
+ pw.println("Unknown mode -- '" + mode + "'");
+ return -1;
+ }
+ if (!mSettablePowerParameters.get(mode).containsKey(name)) {
+ pw.println("Unknown parameter name -- '" + name + "' in mode '" + mode + "'");
+ return -1;
+ }
+
+ parentShell.getOutPrintWriter().println(
+ (int) mSettablePowerParameters.get(mode).get(name));
+ return 0;
+ }
default:
pw.println("Unknown 'wifiaware native_api <cmd>'");
}
@@ -142,12 +217,33 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
@Override
public void onReset() {
- mSettableParameters.put(PARAM_DW_DEFAULT_24GHZ, PARAM_DW_DEFAULT_24GHZ_DEFAULT);
- mSettableParameters.put(PARAM_DW_DEFAULT_5GHZ, PARAM_DW_DEFAULT_5GHZ_DEFAULT);
- mSettableParameters.put(PARAM_DW_ON_INACTIVE_24GHZ, PARAM_DW_ON_INACTIVE_24GHZ_DEFAULT);
- mSettableParameters.put(PARAM_DW_ON_INACTIVE_5GHZ, PARAM_DW_ON_INACTIVE_5GHZ_DEFAULT);
- mSettableParameters.put(PARAM_DW_ON_IDLE_24GHZ, PARAM_DW_ON_IDLE_24GHZ_DEFAULT);
- mSettableParameters.put(PARAM_DW_ON_IDLE_5GHZ, PARAM_DW_ON_IDLE_5GHZ_DEFAULT);
+ Map<String, Integer> defaultMap = new HashMap<>();
+ defaultMap.put(PARAM_DW_24GHZ, PARAM_DW_24GHZ_DEFAULT);
+ defaultMap.put(PARAM_DW_5GHZ, PARAM_DW_5GHZ_DEFAULT);
+ defaultMap.put(PARAM_DISCOVERY_BEACON_INTERVAL_MS,
+ PARAM_DISCOVERY_BEACON_INTERVAL_MS_DEFAULT);
+ defaultMap.put(PARAM_NUM_SS_IN_DISCOVERY, PARAM_NUM_SS_IN_DISCOVERY_DEFAULT);
+ defaultMap.put(PARAM_ENABLE_DW_EARLY_TERM, PARAM_ENABLE_DW_EARLY_TERM_DEFAULT);
+
+ Map<String, Integer> inactiveMap = new HashMap<>();
+ inactiveMap.put(PARAM_DW_24GHZ, PARAM_DW_24GHZ_INACTIVE);
+ inactiveMap.put(PARAM_DW_5GHZ, PARAM_DW_5GHZ_INACTIVE);
+ inactiveMap.put(PARAM_DISCOVERY_BEACON_INTERVAL_MS,
+ PARAM_DISCOVERY_BEACON_INTERVAL_MS_INACTIVE);
+ inactiveMap.put(PARAM_NUM_SS_IN_DISCOVERY, PARAM_NUM_SS_IN_DISCOVERY_INACTIVE);
+ inactiveMap.put(PARAM_ENABLE_DW_EARLY_TERM, PARAM_ENABLE_DW_EARLY_TERM_INACTIVE);
+
+ Map<String, Integer> idleMap = new HashMap<>();
+ idleMap.put(PARAM_DW_24GHZ, PARAM_DW_24GHZ_IDLE);
+ idleMap.put(PARAM_DW_5GHZ, PARAM_DW_5GHZ_IDLE);
+ idleMap.put(PARAM_DISCOVERY_BEACON_INTERVAL_MS,
+ PARAM_DISCOVERY_BEACON_INTERVAL_MS_IDLE);
+ idleMap.put(PARAM_NUM_SS_IN_DISCOVERY, PARAM_NUM_SS_IN_DISCOVERY_IDLE);
+ idleMap.put(PARAM_ENABLE_DW_EARLY_TERM, PARAM_ENABLE_DW_EARLY_TERM_IDLE);
+
+ mSettablePowerParameters.put(POWER_PARAM_DEFAULT_KEY, defaultMap);
+ mSettablePowerParameters.put(POWER_PARAM_INACTIVE_KEY, inactiveMap);
+ mSettablePowerParameters.put(POWER_PARAM_IDLE_KEY, idleMap);
mSettableParameters.put(PARAM_MAC_RANDOM_INTERVAL_SEC,
PARAM_MAC_RANDOM_INTERVAL_SEC_DEFAULT);
@@ -160,8 +256,14 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
pw.println(" " + command);
pw.println(" set <name> <value>: sets named parameter to value. Names: "
+ mSettableParameters.keySet());
+ pw.println(" set-power <mode> <name> <value>: sets named power parameter to value."
+ + " Modes: " + mSettablePowerParameters.keySet()
+ + ", Names: " + mSettablePowerParameters.get(POWER_PARAM_DEFAULT_KEY).keySet());
pw.println(" get <name>: gets named parameter value. Names: "
+ mSettableParameters.keySet());
+ pw.println(" get-power <mode> <name>: gets named parameter value."
+ + " Modes: " + mSettablePowerParameters.keySet()
+ + ", Names: " + mSettablePowerParameters.get(POWER_PARAM_DEFAULT_KEY).keySet());
}
/**
@@ -220,6 +322,15 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
Log.e(TAG, "enableAndConfigure: null interface");
return false;
}
+ android.hardware.wifi.V1_2.IWifiNanIface iface12 = mockableCastTo_1_2(iface);
+ NanConfigRequestSupplemental configSupplemental12 = new NanConfigRequestSupplemental();
+ if (iface12 != null) {
+ if (VDBG) Log.v(TAG, "HAL 1.2 detected");
+ configSupplemental12.discoveryBeaconIntervalMs = 0;
+ configSupplemental12.numberOfSpatialStreamsInDiscovery = 0;
+ configSupplemental12.enableDiscoveryWindowEarlyTermination = false;
+ configSupplemental12.enableRanging = true;
+ }
try {
WifiStatus status;
@@ -296,9 +407,14 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
req.debugConfigs.useSdfInBandVal[NanBandIndex.NAN_BAND_24GHZ] = true;
req.debugConfigs.useSdfInBandVal[NanBandIndex.NAN_BAND_5GHZ] = true;
- updateConfigForPowerSettings(req.configParams, isInteractive, isIdle);
+ updateConfigForPowerSettings(req.configParams, configSupplemental12, isInteractive,
+ isIdle);
- status = iface.enableRequest(transactionId, req);
+ if (iface12 != null) {
+ status = iface12.enableRequest_1_2(transactionId, req, configSupplemental12);
+ } else {
+ status = iface.enableRequest(transactionId, req);
+ }
} else {
NanConfigRequest req = new NanConfigRequest();
req.masterPref = (byte) configRequest.mMasterPreference;
@@ -347,9 +463,13 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
}
req.bandSpecificConfig[NanBandIndex.NAN_BAND_5GHZ] = config5;
- updateConfigForPowerSettings(req, isInteractive, isIdle);
+ updateConfigForPowerSettings(req, configSupplemental12, isInteractive, isIdle);
- status = iface.configRequest(transactionId, req);
+ if (iface12 != null) {
+ status = iface12.configRequest_1_2(transactionId, req, configSupplemental12);
+ } else {
+ status = iface.configRequest(transactionId, req);
+ }
}
if (status.code == WifiStatusCode.SUCCESS) {
return true;
@@ -908,26 +1028,27 @@ public class WifiAwareNativeApi implements WifiAwareShellCommand.DelegatedShellC
/**
* Update the NAN configuration to reflect the current power settings.
*/
- private void updateConfigForPowerSettings(NanConfigRequest req, boolean isInteractive,
+ private void updateConfigForPowerSettings(NanConfigRequest req,
+ NanConfigRequestSupplemental configSupplemental12, boolean isInteractive,
boolean isIdle) {
- if (isIdle) { // lowest power state: doze
- updateSingleConfigForPowerSettings(req.bandSpecificConfig[NanBandIndex.NAN_BAND_5GHZ],
- mSettableParameters.get(PARAM_DW_ON_IDLE_5GHZ));
- updateSingleConfigForPowerSettings(req.bandSpecificConfig[NanBandIndex.NAN_BAND_24GHZ],
- mSettableParameters.get(PARAM_DW_ON_IDLE_24GHZ));
- } else if (!isInteractive) { // intermediate power state: inactive
- updateSingleConfigForPowerSettings(req.bandSpecificConfig[NanBandIndex.NAN_BAND_5GHZ],
- mSettableParameters.get(PARAM_DW_ON_INACTIVE_5GHZ));
- updateSingleConfigForPowerSettings(req.bandSpecificConfig[NanBandIndex.NAN_BAND_24GHZ],
- mSettableParameters.get(PARAM_DW_ON_INACTIVE_24GHZ));
- } else { // the default state
- updateSingleConfigForPowerSettings(req.bandSpecificConfig[NanBandIndex.NAN_BAND_5GHZ],
- mSettableParameters.get(PARAM_DW_DEFAULT_5GHZ));
- updateSingleConfigForPowerSettings(req.bandSpecificConfig[NanBandIndex.NAN_BAND_24GHZ],
- mSettableParameters.get(PARAM_DW_DEFAULT_24GHZ));
- }
-
- // else do nothing - normal power state
+ String key = POWER_PARAM_DEFAULT_KEY;
+ if (isIdle) {
+ key = POWER_PARAM_IDLE_KEY;
+ } else if (!isInteractive) {
+ key = POWER_PARAM_INACTIVE_KEY;
+ }
+
+ updateSingleConfigForPowerSettings(req.bandSpecificConfig[NanBandIndex.NAN_BAND_5GHZ],
+ mSettablePowerParameters.get(key).get(PARAM_DW_5GHZ));
+ updateSingleConfigForPowerSettings(req.bandSpecificConfig[NanBandIndex.NAN_BAND_24GHZ],
+ mSettablePowerParameters.get(key).get(PARAM_DW_24GHZ));
+
+ configSupplemental12.discoveryBeaconIntervalMs = mSettablePowerParameters.get(key).get(
+ PARAM_DISCOVERY_BEACON_INTERVAL_MS);
+ configSupplemental12.numberOfSpatialStreamsInDiscovery = mSettablePowerParameters.get(
+ key).get(PARAM_NUM_SS_IN_DISCOVERY);
+ configSupplemental12.enableDiscoveryWindowEarlyTermination = mSettablePowerParameters.get(
+ key).get(PARAM_ENABLE_DW_EARLY_TERM) != 0;
}
private void updateSingleConfigForPowerSettings(NanBandSpecificConfig cfg, int override) {
diff --git a/com/android/server/wifi/aware/WifiAwareNativeCallback.java b/com/android/server/wifi/aware/WifiAwareNativeCallback.java
index 78f8a28b..ed06015d 100644
--- a/com/android/server/wifi/aware/WifiAwareNativeCallback.java
+++ b/com/android/server/wifi/aware/WifiAwareNativeCallback.java
@@ -16,7 +16,6 @@
package com.android.server.wifi.aware;
-import android.hardware.wifi.V1_0.IWifiNanIfaceEventCallback;
import android.hardware.wifi.V1_0.NanCapabilities;
import android.hardware.wifi.V1_0.NanClusterEventInd;
import android.hardware.wifi.V1_0.NanClusterEventType;
@@ -26,6 +25,8 @@ import android.hardware.wifi.V1_0.NanFollowupReceivedInd;
import android.hardware.wifi.V1_0.NanMatchInd;
import android.hardware.wifi.V1_0.NanStatusType;
import android.hardware.wifi.V1_0.WifiNanStatus;
+import android.hardware.wifi.V1_2.IWifiNanIfaceEventCallback;
+import android.hardware.wifi.V1_2.NanDataPathScheduleUpdateInd;
import android.os.ShellCommand;
import android.util.Log;
import android.util.SparseIntArray;
@@ -49,6 +50,8 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
private static final boolean VDBG = false;
/* package */ boolean mDbg = false;
+ /* package */ boolean mIsHal12OrLater = false;
+
private final WifiAwareStateManager mWifiAwareStateManager;
public WifiAwareNativeCallback(WifiAwareStateManager wifiAwareStateManager) {
@@ -69,6 +72,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
private static final int CB_EV_DATA_PATH_REQUEST = 8;
private static final int CB_EV_DATA_PATH_CONFIRM = 9;
private static final int CB_EV_DATA_PATH_TERMINATED = 10;
+ private static final int CB_EV_DATA_PATH_SCHED_UPDATE = 11;
private SparseIntArray mCallbackCounter = new SparseIntArray();
@@ -91,7 +95,7 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
switch (subCmd) {
case "get_cb_count": {
String option = parentShell.getNextOption();
- Log.v(TAG, "option='" + option + "'");
+ if (VDBG) Log.v(TAG, "option='" + option + "'");
boolean reset = false;
if (option != null) {
if ("--reset".equals(option)) {
@@ -480,11 +484,49 @@ public class WifiAwareNativeCallback extends IWifiNanIfaceEventCallback.Stub imp
+ ", dataPathSetupSuccess=" + event.dataPathSetupSuccess + ", reason="
+ event.status.status);
}
+ if (mIsHal12OrLater) {
+ Log.wtf(TAG, "eventDataPathConfirm should not be called by a >=1.2 HAL!");
+ }
incrementCbCount(CB_EV_DATA_PATH_CONFIRM);
mWifiAwareStateManager.onDataPathConfirmNotification(event.ndpInstanceId,
event.peerNdiMacAddr, event.dataPathSetupSuccess, event.status.status,
- convertArrayListToNativeByteArray(event.appInfo));
+ convertArrayListToNativeByteArray(event.appInfo), null);
+ }
+
+ @Override
+ public void eventDataPathConfirm_1_2(android.hardware.wifi.V1_2.NanDataPathConfirmInd event) {
+ if (mDbg) {
+ Log.v(TAG, "eventDataPathConfirm_1_2: ndpInstanceId=" + event.V1_0.ndpInstanceId
+ + ", peerNdiMacAddr=" + String.valueOf(
+ HexEncoding.encode(event.V1_0.peerNdiMacAddr)) + ", dataPathSetupSuccess="
+ + event.V1_0.dataPathSetupSuccess + ", reason=" + event.V1_0.status.status);
+ }
+ if (!mIsHal12OrLater) {
+ Log.wtf(TAG, "eventDataPathConfirm_1_2 should not be called by a <1.2 HAL!");
+ return;
+ }
+ incrementCbCount(CB_EV_DATA_PATH_CONFIRM);
+
+ mWifiAwareStateManager.onDataPathConfirmNotification(event.V1_0.ndpInstanceId,
+ event.V1_0.peerNdiMacAddr, event.V1_0.dataPathSetupSuccess,
+ event.V1_0.status.status, convertArrayListToNativeByteArray(event.V1_0.appInfo),
+ event.channelInfo);
+ }
+
+ @Override
+ public void eventDataPathScheduleUpdate(NanDataPathScheduleUpdateInd event) {
+ if (mDbg) {
+ Log.v(TAG, "eventDataPathScheduleUpdate");
+ }
+ if (!mIsHal12OrLater) {
+ Log.wtf(TAG, "eventDataPathScheduleUpdate should not be called by a <1.2 HAL!");
+ return;
+ }
+ incrementCbCount(CB_EV_DATA_PATH_SCHED_UPDATE);
+
+ mWifiAwareStateManager.onDataPathScheduleUpdateNotification(event.peerDiscoveryAddress,
+ event.ndpInstanceIds, event.channelInfo);
}
@Override
diff --git a/com/android/server/wifi/aware/WifiAwareNativeManager.java b/com/android/server/wifi/aware/WifiAwareNativeManager.java
index b9dd6808..6556bb08 100644
--- a/com/android/server/wifi/aware/WifiAwareNativeManager.java
+++ b/com/android/server/wifi/aware/WifiAwareNativeManager.java
@@ -61,6 +61,15 @@ public class WifiAwareNativeManager {
}
/**
+ * (HIDL) Cast the input to a 1.2 NAN interface (possibly resulting in a null).
+ *
+ * Separate function so can be mocked in unit tests.
+ */
+ public android.hardware.wifi.V1_2.IWifiNanIface mockableCastTo_1_2(IWifiNanIface iface) {
+ return android.hardware.wifi.V1_2.IWifiNanIface.castFrom(iface);
+ }
+
+ /**
* Initialize the class - intended for late initialization.
*
* @param handler Handler on which to execute interface available callbacks.
@@ -132,7 +141,15 @@ public class WifiAwareNativeManager {
if (mDbg) Log.v(TAG, "Obtained an IWifiNanIface");
try {
- WifiStatus status = iface.registerEventCallback(mWifiAwareNativeCallback);
+ android.hardware.wifi.V1_2.IWifiNanIface iface12 = mockableCastTo_1_2(iface);
+ WifiStatus status;
+ if (iface12 == null) {
+ mWifiAwareNativeCallback.mIsHal12OrLater = false;
+ status = iface.registerEventCallback(mWifiAwareNativeCallback);
+ } else {
+ mWifiAwareNativeCallback.mIsHal12OrLater = true;
+ status = iface12.registerEventCallback_1_2(mWifiAwareNativeCallback);
+ }
if (status.code != WifiStatusCode.SUCCESS) {
Log.e(TAG, "IWifiNanIface.registerEventCallback error: " + statusString(
status));
diff --git a/com/android/server/wifi/aware/WifiAwareStateManager.java b/com/android/server/wifi/aware/WifiAwareStateManager.java
index 5ae69021..d02d8a0e 100644
--- a/com/android/server/wifi/aware/WifiAwareStateManager.java
+++ b/com/android/server/wifi/aware/WifiAwareStateManager.java
@@ -21,6 +21,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.wifi.V1_0.NanStatusType;
+import android.hardware.wifi.V1_2.NanDataPathChannelInfo;
import android.net.wifi.aware.Characteristics;
import android.net.wifi.aware.ConfigRequest;
import android.net.wifi.aware.IWifiAwareDiscoverySessionCallback;
@@ -57,6 +58,7 @@ import org.json.JSONObject;
import java.io.FileDescriptor;
import java.io.PrintWriter;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
@@ -148,6 +150,7 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
private static final int NOTIFICATION_TYPE_ON_DATA_PATH_REQUEST = 309;
private static final int NOTIFICATION_TYPE_ON_DATA_PATH_CONFIRM = 310;
private static final int NOTIFICATION_TYPE_ON_DATA_PATH_END = 311;
+ private static final int NOTIFICATION_TYPE_ON_DATA_PATH_SCHED_UPDATE = 312;
private static final SparseArray<String> sSmToString = MessageUtils.findMessageNames(
new Class[]{WifiAwareStateManager.class},
@@ -187,6 +190,7 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
private static final String MESSAGE_BUNDLE_KEY_OOB = "out_of_band";
private static final String MESSAGE_RANGING_INDICATION = "ranging_indication";
private static final String MESSAGE_RANGE_MM = "range_mm";
+ private static final String MESSAGE_BUNDLE_KEY_NDP_IDS = "ndp_ids";
private WifiAwareNativeApi mWifiAwareNativeApi;
private WifiAwareNativeManager mWifiAwareNativeManager;
@@ -1058,7 +1062,7 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
* data-path is now up.
*/
public void onDataPathConfirmNotification(int ndpId, byte[] mac, boolean accept, int reason,
- byte[] message) {
+ byte[] message, List<NanDataPathChannelInfo> channelInfo) {
Message msg = mSm.obtainMessage(MESSAGE_TYPE_NOTIFICATION);
msg.arg1 = NOTIFICATION_TYPE_ON_DATA_PATH_CONFIRM;
msg.arg2 = ndpId;
@@ -1066,6 +1070,7 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
msg.getData().putBoolean(MESSAGE_BUNDLE_KEY_SUCCESS_FLAG, accept);
msg.getData().putInt(MESSAGE_BUNDLE_KEY_STATUS_CODE, reason);
msg.getData().putByteArray(MESSAGE_BUNDLE_KEY_MESSAGE_DATA, message);
+ msg.obj = channelInfo;
mSm.sendMessage(msg);
}
@@ -1081,6 +1086,20 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
}
/**
+ * Place a callback request on the state machine queue: schedule update for the specified
+ * data-paths.
+ */
+ public void onDataPathScheduleUpdateNotification(byte[] peerMac, ArrayList<Integer> ndpIds,
+ List<NanDataPathChannelInfo> channelInfo) {
+ Message msg = mSm.obtainMessage(MESSAGE_TYPE_NOTIFICATION);
+ msg.arg1 = NOTIFICATION_TYPE_ON_DATA_PATH_SCHED_UPDATE;
+ msg.getData().putByteArray(MESSAGE_BUNDLE_KEY_MAC_ADDRESS, peerMac);
+ msg.getData().putIntegerArrayList(MESSAGE_BUNDLE_KEY_NDP_IDS, ndpIds);
+ msg.obj = channelInfo;
+ mSm.sendMessage(msg);
+ }
+
+ /**
* State machine.
*/
@VisibleForTesting
@@ -1401,7 +1420,8 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
msg.arg2, msg.getData().getByteArray(MESSAGE_BUNDLE_KEY_MAC_ADDRESS),
msg.getData().getBoolean(MESSAGE_BUNDLE_KEY_SUCCESS_FLAG),
msg.getData().getInt(MESSAGE_BUNDLE_KEY_STATUS_CODE),
- msg.getData().getByteArray(MESSAGE_BUNDLE_KEY_MESSAGE_DATA));
+ msg.getData().getByteArray(MESSAGE_BUNDLE_KEY_MESSAGE_DATA),
+ (List<NanDataPathChannelInfo>) msg.obj);
if (networkSpecifier != null) {
WakeupMessage timeout = mDataPathConfirmTimeoutMessages.remove(
@@ -1416,6 +1436,12 @@ public class WifiAwareStateManager implements WifiAwareShellCommand.DelegatedShe
case NOTIFICATION_TYPE_ON_DATA_PATH_END:
mDataPathMgr.onDataPathEnd(msg.arg2);
break;
+ case NOTIFICATION_TYPE_ON_DATA_PATH_SCHED_UPDATE:
+ mDataPathMgr.onDataPathSchedUpdate(
+ msg.getData().getByteArray(MESSAGE_BUNDLE_KEY_MAC_ADDRESS),
+ msg.getData().getIntegerArrayList(MESSAGE_BUNDLE_KEY_NDP_IDS),
+ (List<NanDataPathChannelInfo>) msg.obj);
+ break;
default:
Log.wtf(TAG, "processNotification: this isn't a NOTIFICATION -- msg=" + msg);
return;
diff --git a/com/android/server/wifi/hotspot2/LegacyPasspointConfigParser.java b/com/android/server/wifi/hotspot2/LegacyPasspointConfigParser.java
new file mode 100644
index 00000000..31795f12
--- /dev/null
+++ b/com/android/server/wifi/hotspot2/LegacyPasspointConfigParser.java
@@ -0,0 +1,513 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.BufferedReader;
+import java.io.FileReader;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utility class for parsing legacy (N and older) Passpoint configuration file content
+ * (/data/misc/wifi/PerProviderSubscription.conf). In N and older, only Release 1 is supported.
+ *
+ * This class only retrieve the relevant Release 1 configuration fields that are not backed
+ * elsewhere. Below are relevant fields:
+ * - FQDN (used for linking with configuration data stored elsewhere)
+ * - Friendly Name
+ * - Roaming Consortium
+ * - Realm
+ * - IMSI (for SIM credential)
+ *
+ * Below is an example content of a Passpoint configuration file:
+ *
+ * tree 3:1.2(urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0)
+ * 8:MgmtTree+
+ * 17:PerProviderSubscription+
+ * 4:r1i1+
+ * 6:HomeSP+
+ * c:FriendlyName=d:Test Provider
+ * 4:FQDN=8:test.net
+ * 13:RoamingConsortiumOI=9:1234,5678
+ * .
+ * a:Credential+
+ * 10:UsernamePassword+
+ * 8:Username=4:user
+ * 8:Password=4:pass
+ *
+ * 9:EAPMethod+
+ * 7:EAPType=2:21
+ * b:InnerMethod=3:PAP
+ * .
+ * .
+ * 5:Realm=a:boingo.com
+ * .
+ * .
+ * .
+ * .
+ *
+ * Each string is prefixed with a "|StringBytesInHex|:".
+ * '+' indicates start of a new internal node.
+ * '.' indicates end of the current internal node.
+ * '=' indicates "value" of a leaf node.
+ *
+ */
+public class LegacyPasspointConfigParser {
+ private static final String TAG = "LegacyPasspointConfigParser";
+
+ private static final String TAG_MANAGEMENT_TREE = "MgmtTree";
+ private static final String TAG_PER_PROVIDER_SUBSCRIPTION = "PerProviderSubscription";
+ private static final String TAG_HOMESP = "HomeSP";
+ private static final String TAG_FQDN = "FQDN";
+ private static final String TAG_FRIENDLY_NAME = "FriendlyName";
+ private static final String TAG_ROAMING_CONSORTIUM_OI = "RoamingConsortiumOI";
+ private static final String TAG_CREDENTIAL = "Credential";
+ private static final String TAG_REALM = "Realm";
+ private static final String TAG_SIM = "SIM";
+ private static final String TAG_IMSI = "IMSI";
+
+ private static final String LONG_ARRAY_SEPARATOR = ",";
+ private static final String END_OF_INTERNAL_NODE_INDICATOR = ".";
+ private static final char START_OF_INTERNAL_NODE_INDICATOR = '+';
+ private static final char STRING_PREFIX_INDICATOR = ':';
+ private static final char STRING_VALUE_INDICATOR = '=';
+
+ /**
+ * An abstraction for a node within a tree. A node can be an internal node (contained
+ * children nodes) or a leaf node (contained a String value).
+ */
+ private abstract static class Node {
+ private final String mName;
+ Node(String name) {
+ mName = name;
+ }
+
+ /**
+ * @return the name of the node
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Applies for internal node only.
+ *
+ * @return the list of children nodes.
+ */
+ public abstract List<Node> getChildren();
+
+ /**
+ * Applies for leaf node only.
+ *
+ * @return the string value of the node
+ */
+ public abstract String getValue();
+ }
+
+ /**
+ * Class representing an internal node of a tree. It contained a list of child nodes.
+ */
+ private static class InternalNode extends Node {
+ private final List<Node> mChildren;
+ InternalNode(String name, List<Node> children) {
+ super(name);
+ mChildren = children;
+ }
+
+ @Override
+ public List<Node> getChildren() {
+ return mChildren;
+ }
+
+ @Override
+ public String getValue() {
+ return null;
+ }
+ }
+
+ /**
+ * Class representing a leaf node of a tree. It contained a String type value.
+ */
+ private static class LeafNode extends Node {
+ private final String mValue;
+ LeafNode(String name, String value) {
+ super(name);
+ mValue = value;
+ }
+
+ @Override
+ public List<Node> getChildren() {
+ return null;
+ }
+
+ @Override
+ public String getValue() {
+ return mValue;
+ }
+ }
+
+ public LegacyPasspointConfigParser() {}
+
+ /**
+ * Parse the legacy Passpoint configuration file content, only retrieve the relevant
+ * configurations that are not saved elsewhere.
+ *
+ * For both N and M, only Release 1 is supported. Most of the configurations are saved
+ * elsewhere as part of the {@link android.net.wifi.WifiConfiguration} data.
+ * The configurations needed from the legacy Passpoint configuration file are:
+ *
+ * - FQDN - needed to be able to link to the associated {@link WifiConfiguration} data
+ * - Friendly Name
+ * - Roaming Consortium OIs
+ * - Realm
+ * - IMSI (for SIM credential)
+ *
+ * Make this function non-static so that it can be mocked during unit test.
+ *
+ * @param fileName The file name of the configuration file
+ * @return Map of FQDN to {@link LegacyPasspointConfig}
+ * @throws IOException
+ */
+ public Map<String, LegacyPasspointConfig> parseConfig(String fileName)
+ throws IOException {
+ Map<String, LegacyPasspointConfig> configs = new HashMap<>();
+ BufferedReader in = new BufferedReader(new FileReader(fileName));
+ in.readLine(); // Ignore the first line which contained the header.
+
+ // Convert the configuration data to a management tree represented by a root {@link Node}.
+ Node root = buildNode(in);
+
+ if (root == null || root.getChildren() == null) {
+ Log.d(TAG, "Empty configuration data");
+ return configs;
+ }
+
+ // Verify root node name.
+ if (!TextUtils.equals(TAG_MANAGEMENT_TREE, root.getName())) {
+ throw new IOException("Unexpected root node: " + root.getName());
+ }
+
+ // Process and retrieve the configuration from each PPS (PerProviderSubscription) node.
+ List<Node> ppsNodes = root.getChildren();
+ for (Node ppsNode : ppsNodes) {
+ LegacyPasspointConfig config = processPpsNode(ppsNode);
+ configs.put(config.mFqdn, config);
+ }
+ return configs;
+ }
+
+ /**
+ * Build a {@link Node} from the current line in the buffer. A node can be an internal
+ * node (ends with '+') or a leaf node.
+ *
+ * @param in Input buffer to read data from
+ * @return {@link Node} representing the current line
+ * @throws IOException
+ */
+ private static Node buildNode(BufferedReader in) throws IOException {
+ // Read until non-empty line.
+ String currentLine = null;
+ while ((currentLine = in.readLine()) != null) {
+ if (!currentLine.isEmpty()) {
+ break;
+ }
+ }
+
+ // Return null if EOF is reached.
+ if (currentLine == null) {
+ return null;
+ }
+
+ // Remove the leading and the trailing whitespaces.
+ currentLine = currentLine.trim();
+
+ // Check for the internal node terminator.
+ if (TextUtils.equals(END_OF_INTERNAL_NODE_INDICATOR, currentLine)) {
+ return null;
+ }
+
+ // Parse the name-value of the current line. The value will be null if the current line
+ // is not a leaf node (e.g. line ends with a '+').
+ // Each line is encoded in UTF-8.
+ Pair<String, String> nameValuePair =
+ parseLine(currentLine.getBytes(StandardCharsets.UTF_8));
+ if (nameValuePair.second != null) {
+ return new LeafNode(nameValuePair.first, nameValuePair.second);
+ }
+
+ // Parse the children contained under this internal node.
+ List<Node> children = new ArrayList<>();
+ Node child = null;
+ while ((child = buildNode(in)) != null) {
+ children.add(child);
+ }
+ return new InternalNode(nameValuePair.first, children);
+ }
+
+ /**
+ * Process a PPS (PerProviderSubscription) node to retrieve Passpoint configuration data.
+ *
+ * @param ppsNode The PPS node to process
+ * @return {@link LegacyPasspointConfig}
+ * @throws IOException
+ */
+ private static LegacyPasspointConfig processPpsNode(Node ppsNode) throws IOException {
+ if (ppsNode.getChildren() == null || ppsNode.getChildren().size() != 1) {
+ throw new IOException("PerProviderSubscription node should contain "
+ + "one instance node");
+ }
+
+ if (!TextUtils.equals(TAG_PER_PROVIDER_SUBSCRIPTION, ppsNode.getName())) {
+ throw new IOException("Unexpected name for PPS node: " + ppsNode.getName());
+ }
+
+ // Retrieve the PPS instance node.
+ Node instanceNode = ppsNode.getChildren().get(0);
+ if (instanceNode.getChildren() == null) {
+ throw new IOException("PPS instance node doesn't contained any children");
+ }
+
+ // Process and retrieve the relevant configurations under the PPS instance node.
+ LegacyPasspointConfig config = new LegacyPasspointConfig();
+ for (Node node : instanceNode.getChildren()) {
+ switch (node.getName()) {
+ case TAG_HOMESP:
+ processHomeSPNode(node, config);
+ break;
+ case TAG_CREDENTIAL:
+ processCredentialNode(node, config);
+ break;
+ default:
+ Log.d(TAG, "Ignore uninterested field under PPS instance: " + node.getName());
+ break;
+ }
+ }
+ if (config.mFqdn == null) {
+ throw new IOException("PPS instance missing FQDN");
+ }
+ return config;
+ }
+
+ /**
+ * Process a HomeSP node to retrieve configuration data into the given |config|.
+ *
+ * @param homeSpNode The HomeSP node to process
+ * @param config The config object to fill in the data
+ * @throws IOException
+ */
+ private static void processHomeSPNode(Node homeSpNode, LegacyPasspointConfig config)
+ throws IOException {
+ if (homeSpNode.getChildren() == null) {
+ throw new IOException("HomeSP node should contain at least one child node");
+ }
+
+ for (Node node : homeSpNode.getChildren()) {
+ switch (node.getName()) {
+ case TAG_FQDN:
+ config.mFqdn = getValue(node);
+ break;
+ case TAG_FRIENDLY_NAME:
+ config.mFriendlyName = getValue(node);
+ break;
+ case TAG_ROAMING_CONSORTIUM_OI:
+ config.mRoamingConsortiumOis = parseLongArray(getValue(node));
+ break;
+ default:
+ Log.d(TAG, "Ignore uninterested field under HomeSP: " + node.getName());
+ break;
+ }
+ }
+ }
+
+ /**
+ * Process a Credential node to retrieve configuration data into the given |config|.
+ *
+ * @param credentialNode The Credential node to process
+ * @param config The config object to fill in the data
+ * @throws IOException
+ */
+ private static void processCredentialNode(Node credentialNode,
+ LegacyPasspointConfig config)
+ throws IOException {
+ if (credentialNode.getChildren() == null) {
+ throw new IOException("Credential node should contain at least one child node");
+ }
+
+ for (Node node : credentialNode.getChildren()) {
+ switch (node.getName()) {
+ case TAG_REALM:
+ config.mRealm = getValue(node);
+ break;
+ case TAG_SIM:
+ processSimNode(node, config);
+ break;
+ default:
+ Log.d(TAG, "Ignore uninterested field under Credential: " + node.getName());
+ break;
+ }
+ }
+ }
+
+ /**
+ * Process a SIM node to retrieve configuration data into the given |config|.
+ *
+ * @param simNode The SIM node to process
+ * @param config The config object to fill in the data
+ * @throws IOException
+ */
+ private static void processSimNode(Node simNode, LegacyPasspointConfig config)
+ throws IOException {
+ if (simNode.getChildren() == null) {
+ throw new IOException("SIM node should contain at least one child node");
+ }
+
+ for (Node node : simNode.getChildren()) {
+ switch (node.getName()) {
+ case TAG_IMSI:
+ config.mImsi = getValue(node);
+ break;
+ default:
+ Log.d(TAG, "Ignore uninterested field under SIM: " + node.getName());
+ break;
+ }
+ }
+ }
+
+ /**
+ * Parse the given line in the legacy Passpoint configuration file.
+ * A line can be in the following formats:
+ * 2:ab+ // internal node
+ * 2:ab=2:bc // leaf node
+ * . // end of internal node
+ *
+ * @param line The line to parse
+ * @return name-value pair, a value of null indicates internal node
+ * @throws IOException
+ */
+ private static Pair<String, String> parseLine(byte[] lineBytes) throws IOException {
+ Pair<String, Integer> nameIndexPair = parseString(lineBytes, 0);
+ int currentIndex = nameIndexPair.second;
+ try {
+ if (lineBytes[currentIndex] == START_OF_INTERNAL_NODE_INDICATOR) {
+ return Pair.create(nameIndexPair.first, null);
+ }
+
+ if (lineBytes[currentIndex] != STRING_VALUE_INDICATOR) {
+ throw new IOException("Invalid line - missing both node and value indicator: "
+ + new String(lineBytes, StandardCharsets.UTF_8));
+ }
+ } catch (IndexOutOfBoundsException e) {
+ throw new IOException("Invalid line - " + e.getMessage() + ": "
+ + new String(lineBytes, StandardCharsets.UTF_8));
+ }
+ Pair<String, Integer> valueIndexPair = parseString(lineBytes, currentIndex + 1);
+ return Pair.create(nameIndexPair.first, valueIndexPair.first);
+ }
+
+ /**
+ * Parse a string value in the given line from the given start index.
+ * A string value is in the following format:
+ * |HexByteLength|:|String|
+ *
+ * The length value indicates the number of UTF-8 bytes in hex for the given string.
+ *
+ * For example: 3:abc
+ *
+ * @param lineBytes The UTF-8 bytes of the line to parse
+ * @param startIndex The start index from the given line to parse from
+ * @return Pair of a string value and an index pointed to character after the string value
+ * @throws IOException
+ */
+ private static Pair<String, Integer> parseString(byte[] lineBytes, int startIndex)
+ throws IOException {
+ // Locate the index that separate length and the string value.
+ int prefixIndex = -1;
+ for (int i = startIndex; i < lineBytes.length; i++) {
+ if (lineBytes[i] == STRING_PREFIX_INDICATOR) {
+ prefixIndex = i;
+ break;
+ }
+ }
+ if (prefixIndex == -1) {
+ throw new IOException("Invalid line - missing string prefix: "
+ + new String(lineBytes, StandardCharsets.UTF_8));
+ }
+
+ try {
+ String lengthStr = new String(lineBytes, startIndex, prefixIndex - startIndex,
+ StandardCharsets.UTF_8);
+ int length = Integer.parseInt(lengthStr, 16);
+ int strStartIndex = prefixIndex + 1;
+ // The length might account for bytes for the whitespaces, since the whitespaces are
+ // already trimmed, ignore them.
+ if ((strStartIndex + length) > lineBytes.length) {
+ length = lineBytes.length - strStartIndex;
+ }
+ return Pair.create(
+ new String(lineBytes, strStartIndex, length, StandardCharsets.UTF_8),
+ strStartIndex + length);
+ } catch (NumberFormatException | IndexOutOfBoundsException e) {
+ throw new IOException("Invalid line - " + e.getMessage() + ": "
+ + new String(lineBytes, StandardCharsets.UTF_8));
+ }
+ }
+
+ /**
+ * Parse a long array from the given string.
+ *
+ * @param str The string to parse
+ * @return long[]
+ * @throws IOException
+ */
+ private static long[] parseLongArray(String str)
+ throws IOException {
+ String[] strArray = str.split(LONG_ARRAY_SEPARATOR);
+ long[] longArray = new long[strArray.length];
+ for (int i = 0; i < longArray.length; i++) {
+ try {
+ longArray[i] = Long.parseLong(strArray[i], 16);
+ } catch (NumberFormatException e) {
+ throw new IOException("Invalid long integer value: " + strArray[i]);
+ }
+ }
+ return longArray;
+ }
+
+ /**
+ * Get the String value of the given node. An IOException will be thrown if the given
+ * node doesn't contain a String value (internal node).
+ *
+ * @param node The node to get the value from
+ * @return String
+ * @throws IOException
+ */
+ private static String getValue(Node node) throws IOException {
+ if (node.getValue() == null) {
+ throw new IOException("Attempt to retreive value from non-leaf node: "
+ + node.getName());
+ }
+ return node.getValue();
+ }
+}
diff --git a/com/android/server/wifi/hotspot2/PasspointConfigStoreData.java b/com/android/server/wifi/hotspot2/PasspointConfigStoreData.java
index 553a38bf..5c46d351 100644
--- a/com/android/server/wifi/hotspot2/PasspointConfigStoreData.java
+++ b/com/android/server/wifi/hotspot2/PasspointConfigStoreData.java
@@ -125,6 +125,10 @@ public class PasspointConfigStoreData implements WifiConfigStore.StoreData {
@Override
public void deserializeData(XmlPullParser in, int outerTagDepth, boolean shared)
throws XmlPullParserException, IOException {
+ // Ignore empty reads.
+ if (in == null) {
+ return;
+ }
if (shared) {
deserializeShareData(in, outerTagDepth);
} else {
diff --git a/com/android/server/wifi/hotspot2/PasspointEventHandler.java b/com/android/server/wifi/hotspot2/PasspointEventHandler.java
index 6a7d0af0..3815c30b 100644
--- a/com/android/server/wifi/hotspot2/PasspointEventHandler.java
+++ b/com/android/server/wifi/hotspot2/PasspointEventHandler.java
@@ -81,6 +81,7 @@ public class PasspointEventHandler {
Pair<Set<Integer>, Set<Integer>> querySets = buildAnqpIdSet(elements);
if (bssid == 0 || querySets == null) return false;
if (!mSupplicantHook.requestAnqp(
+ mSupplicantHook.getClientInterfaceName(),
Utils.macToString(bssid), querySets.first, querySets.second)) {
Log.d(Utils.hs2LogTag(getClass()), "ANQP failed on " + Utils.macToString(bssid));
return false;
@@ -97,7 +98,8 @@ public class PasspointEventHandler {
*/
public boolean requestIcon(long bssid, String fileName) {
if (bssid == 0 || fileName == null) return false;
- return mSupplicantHook.requestIcon(Utils.macToString(bssid), fileName);
+ return mSupplicantHook.requestIcon(
+ mSupplicantHook.getClientInterfaceName(), Utils.macToString(bssid), fileName);
}
/**
diff --git a/com/android/server/wifi/rtt/RttNative.java b/com/android/server/wifi/rtt/RttNative.java
index d00bf75a..e7492489 100644
--- a/com/android/server/wifi/rtt/RttNative.java
+++ b/com/android/server/wifi/rtt/RttNative.java
@@ -30,6 +30,7 @@ import android.hardware.wifi.V1_0.WifiStatusCode;
import android.net.wifi.rtt.RangingRequest;
import android.net.wifi.rtt.RangingResult;
import android.net.wifi.rtt.ResponderConfig;
+import android.os.Handler;
import android.os.RemoteException;
import android.util.Log;
@@ -53,7 +54,7 @@ public class RttNative extends IWifiRttControllerEventCallback.Stub {
private Object mLock = new Object();
- private IWifiRttController mIWifiRttController;
+ private volatile IWifiRttController mIWifiRttController;
public RttNative(RttServiceImpl rttService, HalDeviceManager halDeviceManager) {
mRttService = rttService;
@@ -63,13 +64,13 @@ public class RttNative extends IWifiRttControllerEventCallback.Stub {
/**
* Initialize the object - registering with the HAL device manager.
*/
- public void start() {
+ public void start(Handler handler) {
synchronized (mLock) {
mHalDeviceManager.initialize();
mHalDeviceManager.registerStatusListener(() -> {
if (VDBG) Log.d(TAG, "hdm.onStatusChanged");
updateController();
- }, null);
+ }, handler);
updateController();
}
}
@@ -78,9 +79,7 @@ public class RttNative extends IWifiRttControllerEventCallback.Stub {
* Returns true if Wi-Fi is ready for RTT requests, false otherwise.
*/
public boolean isReady() {
- synchronized (mLock) {
- return mIWifiRttController != null;
- }
+ return mIWifiRttController != null;
}
private void updateController() {
@@ -89,24 +88,26 @@ public class RttNative extends IWifiRttControllerEventCallback.Stub {
// only care about isStarted (Wi-Fi started) not isReady - since if not
// ready then Wi-Fi will also be down.
synchronized (mLock) {
+ IWifiRttController localWifiRttController = mIWifiRttController;
if (mHalDeviceManager.isStarted()) {
- if (mIWifiRttController == null) {
- mIWifiRttController = mHalDeviceManager.createRttController();
- if (mIWifiRttController == null) {
+ if (localWifiRttController == null) {
+ localWifiRttController = mHalDeviceManager.createRttController();
+ if (localWifiRttController == null) {
Log.e(TAG, "updateController: Failed creating RTT controller - but Wifi is "
+ "started!");
} else {
try {
- mIWifiRttController.registerEventCallback(this);
+ localWifiRttController.registerEventCallback(this);
} catch (RemoteException e) {
Log.e(TAG, "updateController: exception registering callback: " + e);
- mIWifiRttController = null;
+ localWifiRttController = null;
}
}
}
} else {
- mIWifiRttController = null;
+ localWifiRttController = null;
}
+ mIWifiRttController = localWifiRttController;
if (mIWifiRttController == null) {
mRttService.disable();
@@ -225,9 +226,9 @@ public class RttNative extends IWifiRttControllerEventCallback.Stub {
config.bw = halRttChannelBandwidthFromResponderChannelWidth(responder.channelWidth);
config.preamble = halRttPreambleFromResponderPreamble(responder.preamble);
- config.mustRequestLci = false;
- config.mustRequestLcr = false;
if (config.peer == RttPeerType.NAN) {
+ config.mustRequestLci = false;
+ config.mustRequestLcr = false;
config.burstPeriod = 0;
config.numBurst = 0;
config.numFramesPerBurst = 5;
@@ -235,6 +236,8 @@ public class RttNative extends IWifiRttControllerEventCallback.Stub {
config.numRetriesPerFtmr = 3;
config.burstDuration = 15;
} else { // AP + all non-NAN requests
+ config.mustRequestLci = true;
+ config.mustRequestLcr = true;
config.burstPeriod = 0;
config.numBurst = 0;
config.numFramesPerBurst = 8;
diff --git a/com/android/server/wifi/rtt/RttServiceImpl.java b/com/android/server/wifi/rtt/RttServiceImpl.java
index 6f025c33..c709ec96 100644
--- a/com/android/server/wifi/rtt/RttServiceImpl.java
+++ b/com/android/server/wifi/rtt/RttServiceImpl.java
@@ -16,6 +16,8 @@
package com.android.server.wifi.rtt;
+import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE;
+
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -31,6 +33,8 @@ import android.net.wifi.aware.IWifiAwareMacAddressProvider;
import android.net.wifi.aware.IWifiAwareManager;
import android.net.wifi.rtt.IRttCallback;
import android.net.wifi.rtt.IWifiRttManager;
+import android.net.wifi.rtt.LocationCivic;
+import android.net.wifi.rtt.LocationConfigurationInformation;
import android.net.wifi.rtt.RangingRequest;
import android.net.wifi.rtt.RangingResult;
import android.net.wifi.rtt.RangingResultCallback;
@@ -44,6 +48,7 @@ import android.os.PowerManager;
import android.os.RemoteException;
import android.os.UserHandle;
import android.os.WorkSource;
+import android.os.WorkSource.WorkChain;
import android.provider.Settings;
import android.util.Log;
import android.util.SparseIntArray;
@@ -51,6 +56,7 @@ import android.util.SparseIntArray;
import com.android.internal.util.WakeupMessage;
import com.android.server.wifi.Clock;
import com.android.server.wifi.FrameworkFacade;
+import com.android.server.wifi.util.NativeUtil;
import com.android.server.wifi.util.WifiPermissionsUtil;
import java.io.FileDescriptor;
@@ -170,7 +176,7 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
}, intentFilter);
mRttServiceSynchronized.mHandler.post(() -> {
- rttNative.start();
+ rttNative.start(mRttServiceSynchronized.mHandler);
});
}
@@ -286,6 +292,9 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
mWifiPermissionsUtil.enforceLocationPermission(callingPackage, uid);
if (workSource != null) {
enforceLocationHardware();
+ // We only care about UIDs in the incoming worksources and not their associated
+ // tags. Clear names so that other operations involving wakesources become simpler.
+ workSource.clearNames();
}
// register for binder death
@@ -305,12 +314,12 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
binder.linkToDeath(dr, 0);
} catch (RemoteException e) {
Log.e(TAG, "Error on linkToDeath - " + e);
+ return;
}
-
mRttServiceSynchronized.mHandler.post(() -> {
WorkSource sourceToUse = workSource;
- if (workSource == null || workSource.size() == 0 || workSource.get(0) == 0) {
+ if (workSource == null || workSource.isEmpty()) {
sourceToUse = new WorkSource(uid);
}
mRttServiceSynchronized.queueRangingRequest(uid, sourceToUse, binder, dr,
@@ -322,8 +331,13 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
public void cancelRanging(WorkSource workSource) throws RemoteException {
if (VDBG) Log.v(TAG, "cancelRanging: workSource=" + workSource);
enforceLocationHardware();
+ if (workSource != null) {
+ // We only care about UIDs in the incoming worksources and not their associated
+ // tags. Clear names so that other operations involving wakesources become simpler.
+ workSource.clearNames();
+ }
- if (workSource == null || workSource.size() == 0 || workSource.get(0) == 0) {
+ if (workSource == null || workSource.isEmpty()) {
Log.e(TAG, "cancelRanging: invalid work-source -- " + workSource);
return;
}
@@ -454,13 +468,8 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
boolean match = rri.uid == uid; // original UID will never be 0
if (rri.workSource != null && workSource != null) {
- try {
- rri.workSource.remove(workSource);
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Invalid WorkSource specified in the start or cancel requests: "
- + e);
- }
- if (rri.workSource.size() == 0) {
+ rri.workSource.remove(workSource);
+ if (rri.workSource.isEmpty()) {
match = true;
}
}
@@ -557,6 +566,14 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
int uid = rri.workSource.get(i);
counts.put(uid, counts.get(uid) + 1);
}
+
+ final ArrayList<WorkChain> workChains = rri.workSource.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ final int uid = workChains.get(i).getAttributionUid();
+ counts.put(uid, counts.get(uid) + 1);
+ }
+ }
}
for (int i = 0; i < ws.size(); ++i) {
@@ -565,6 +582,16 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
}
}
+ final ArrayList<WorkChain> workChains = ws.getWorkChains();
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ final int uid = workChains.get(i).getAttributionUid();
+ if (counts.get(uid) < MAX_QUEUED_PER_UID) {
+ return false;
+ }
+ }
+ }
+
if (mDbg) {
Log.v(TAG, "isRequestorSpamming: ws=" + ws + ", someone is spamming: " + counts);
}
@@ -677,13 +704,29 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
Log.v(TAG, "preExecThrottleCheck: uid=" + ws.get(i) + " -> importance="
+ uidImportance);
}
- if (uidImportance
- <= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE) {
+ if (uidImportance <= IMPORTANCE_FOREGROUND_SERVICE) {
allUidsInBackground = false;
break;
}
}
+ final ArrayList<WorkChain> workChains = ws.getWorkChains();
+ if (allUidsInBackground && workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ final WorkChain wc = workChains.get(i);
+ int uidImportance = mActivityManager.getUidImportance(wc.getAttributionUid());
+ if (VDBG) {
+ Log.v(TAG, "preExecThrottleCheck: workChain=" + wc + " -> importance="
+ + uidImportance);
+ }
+
+ if (uidImportance <= IMPORTANCE_FOREGROUND_SERVICE) {
+ allUidsInBackground = false;
+ break;
+ }
+ }
+ }
+
// if all UIDs are in background then check timestamp since last execution and see if
// any is permitted (infrequent enough)
boolean allowExecution = false;
@@ -697,6 +740,18 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
break;
}
}
+
+ if (workChains != null & !allowExecution) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ final WorkChain wc = workChains.get(i);
+ RttRequesterInfo info = mRttRequesterInfo.get(wc.getAttributionUid());
+ if (info == null
+ || info.lastRangingExecuted < mostRecentExecutionPermitted) {
+ allowExecution = true;
+ break;
+ }
+ }
+ }
} else {
allowExecution = true;
}
@@ -711,6 +766,18 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
}
info.lastRangingExecuted = mClock.getElapsedSinceBootMillis();
}
+
+ if (workChains != null) {
+ for (int i = 0; i < workChains.size(); ++i) {
+ final WorkChain wc = workChains.get(i);
+ RttRequesterInfo info = mRttRequesterInfo.get(wc.getAttributionUid());
+ if (info == null) {
+ info = new RttRequesterInfo();
+ mRttRequesterInfo.put(wc.getAttributionUid(), info);
+ }
+ info.lastRangingExecuted = mClock.getElapsedSinceBootMillis();
+ }
+ }
}
return allowExecution;
@@ -881,27 +948,37 @@ public class RttServiceImpl extends IWifiRttManager.Stub {
if (peer.peerHandle == null) {
finalResults.add(
new RangingResult(RangingResult.STATUS_FAIL, peer.macAddress, 0, 0,
- 0, 0));
+ 0, null, null, 0));
} else {
finalResults.add(
new RangingResult(RangingResult.STATUS_FAIL, peer.peerHandle, 0, 0,
- 0, 0));
+ 0, null, null, 0));
}
} else {
int status = resultForRequest.status == RttStatus.SUCCESS
? RangingResult.STATUS_SUCCESS : RangingResult.STATUS_FAIL;
if (peer.peerHandle == null) {
- finalResults.add(
- new RangingResult(status, peer.macAddress,
- resultForRequest.distanceInMm,
- resultForRequest.distanceSdInMm,
- resultForRequest.rssi, resultForRequest.timeStampInUs));
+ finalResults.add(new RangingResult(status, peer.macAddress,
+ resultForRequest.distanceInMm, resultForRequest.distanceSdInMm,
+ resultForRequest.rssi,
+ LocationConfigurationInformation.parseInformationElement(
+ resultForRequest.lci.id, NativeUtil.byteArrayFromArrayList(
+ resultForRequest.lcr.data)),
+ LocationCivic.parseInformationElement(resultForRequest.lci.id,
+ NativeUtil.byteArrayFromArrayList(
+ resultForRequest.lci.data)),
+ resultForRequest.timeStampInUs));
} else {
- finalResults.add(
- new RangingResult(status, peer.peerHandle,
- resultForRequest.distanceInMm,
- resultForRequest.distanceSdInMm,
- resultForRequest.rssi, resultForRequest.timeStampInUs));
+ finalResults.add(new RangingResult(status, peer.peerHandle,
+ resultForRequest.distanceInMm, resultForRequest.distanceSdInMm,
+ resultForRequest.rssi,
+ LocationConfigurationInformation.parseInformationElement(
+ resultForRequest.lci.id, NativeUtil.byteArrayFromArrayList(
+ resultForRequest.lci.data)),
+ LocationCivic.parseInformationElement(resultForRequest.lci.id,
+ NativeUtil.byteArrayFromArrayList(
+ resultForRequest.lci.data)),
+ resultForRequest.timeStampInUs));
}
}
}
diff --git a/com/android/server/wifi/scanner/HalWifiScannerImpl.java b/com/android/server/wifi/scanner/HalWifiScannerImpl.java
index 1478a99c..ec362518 100644
--- a/com/android/server/wifi/scanner/HalWifiScannerImpl.java
+++ b/com/android/server/wifi/scanner/HalWifiScannerImpl.java
@@ -39,17 +39,19 @@ public class HalWifiScannerImpl extends WifiScannerImpl implements Handler.Callb
private static final String TAG = "HalWifiScannerImpl";
private static final boolean DBG = false;
+ private final String mIfaceName;
private final WifiNative mWifiNative;
private final ChannelHelper mChannelHelper;
private final WificondScannerImpl mWificondScannerDelegate;
- public HalWifiScannerImpl(Context context, WifiNative wifiNative, WifiMonitor wifiMonitor,
- Looper looper, Clock clock) {
+ public HalWifiScannerImpl(Context context, String ifaceName, WifiNative wifiNative,
+ WifiMonitor wifiMonitor, Looper looper, Clock clock) {
+ mIfaceName = ifaceName;
mWifiNative = wifiNative;
mChannelHelper = new WificondChannelHelper(wifiNative);
mWificondScannerDelegate =
- new WificondScannerImpl(context, wifiNative, wifiMonitor, mChannelHelper,
- looper, clock);
+ new WificondScannerImpl(context, mIfaceName, wifiNative, wifiMonitor,
+ mChannelHelper, looper, clock);
}
@Override
@@ -65,7 +67,8 @@ public class HalWifiScannerImpl extends WifiScannerImpl implements Handler.Callb
@Override
public boolean getScanCapabilities(WifiNative.ScanCapabilities capabilities) {
- return mWifiNative.getBgScanCapabilities(capabilities);
+ return mWifiNative.getBgScanCapabilities(
+ mWifiNative.getClientInterfaceName(), capabilities);
}
@Override
@@ -91,27 +94,28 @@ public class HalWifiScannerImpl extends WifiScannerImpl implements Handler.Callb
+ ",eventHandler=" + eventHandler);
return false;
}
- return mWifiNative.startBgScan(settings, eventHandler);
+ return mWifiNative.startBgScan(
+ mWifiNative.getClientInterfaceName(), settings, eventHandler);
}
@Override
public void stopBatchedScan() {
- mWifiNative.stopBgScan();
+ mWifiNative.stopBgScan(mWifiNative.getClientInterfaceName());
}
@Override
public void pauseBatchedScan() {
- mWifiNative.pauseBgScan();
+ mWifiNative.pauseBgScan(mWifiNative.getClientInterfaceName());
}
@Override
public void restartBatchedScan() {
- mWifiNative.restartBgScan();
+ mWifiNative.restartBgScan(mWifiNative.getClientInterfaceName());
}
@Override
public WifiScanner.ScanData[] getLatestBatchedScanResults(boolean flush) {
- return mWifiNative.getBgScanResults();
+ return mWifiNative.getBgScanResults(mWifiNative.getClientInterfaceName());
}
@Override
diff --git a/com/android/server/wifi/scanner/WifiScannerImpl.java b/com/android/server/wifi/scanner/WifiScannerImpl.java
index 75b24d88..5a54cdcc 100644
--- a/com/android/server/wifi/scanner/WifiScannerImpl.java
+++ b/com/android/server/wifi/scanner/WifiScannerImpl.java
@@ -20,6 +20,7 @@ import android.content.Context;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiScanner;
import android.os.Looper;
+import android.text.TextUtils;
import com.android.server.wifi.Clock;
import com.android.server.wifi.WifiInjector;
@@ -50,10 +51,16 @@ public abstract class WifiScannerImpl {
public WifiScannerImpl create(Context context, Looper looper, Clock clock) {
WifiNative wifiNative = WifiInjector.getInstance().getWifiNative();
WifiMonitor wifiMonitor = WifiInjector.getInstance().getWifiMonitor();
- if (wifiNative.getBgScanCapabilities(new WifiNative.ScanCapabilities())) {
- return new HalWifiScannerImpl(context, wifiNative, wifiMonitor, looper, clock);
+ String ifaceName = wifiNative.getClientInterfaceName();
+ if (TextUtils.isEmpty(ifaceName)) {
+ return null;
+ }
+ if (wifiNative.getBgScanCapabilities(
+ wifiNative.getClientInterfaceName(), new WifiNative.ScanCapabilities())) {
+ return new HalWifiScannerImpl(context, ifaceName, wifiNative, wifiMonitor,
+ looper, clock);
} else {
- return new WificondScannerImpl(context, wifiNative, wifiMonitor,
+ return new WificondScannerImpl(context, ifaceName, wifiNative, wifiMonitor,
new WificondChannelHelper(wifiNative), looper, clock);
}
}
diff --git a/com/android/server/wifi/scanner/WifiScanningServiceImpl.java b/com/android/server/wifi/scanner/WifiScanningServiceImpl.java
index 444c0e0e..00f656db 100644
--- a/com/android/server/wifi/scanner/WifiScanningServiceImpl.java
+++ b/com/android/server/wifi/scanner/WifiScanningServiceImpl.java
@@ -287,7 +287,6 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
}
public void startService() {
- mClientHandler = new ClientHandler(TAG, mLooper);
mBackgroundScanStateMachine = new WifiBackgroundScanStateMachine(mLooper);
mSingleScanStateMachine = new WifiSingleScanStateMachine(mLooper);
mPnoScanStateMachine = new WifiPnoScanStateMachine(mLooper);
@@ -314,6 +313,9 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
mBackgroundScanStateMachine.start();
mSingleScanStateMachine.start();
mPnoScanStateMachine.start();
+
+ // Create client handler only after StateMachines are ready.
+ mClientHandler = new ClientHandler(TAG, mLooper);
}
/**
@@ -325,29 +327,24 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
mClientHandler.setWifiLog(log);
}
- private static boolean isWorkSourceValid(WorkSource workSource) {
- return workSource != null && workSource.size() > 0 && workSource.get(0) >= 0;
- }
-
private WorkSource computeWorkSource(ClientInfo ci, WorkSource requestedWorkSource) {
if (requestedWorkSource != null) {
- if (isWorkSourceValid(requestedWorkSource)) {
- // Wifi currently doesn't use names, so need to clear names out of the
- // supplied WorkSource to allow future WorkSource combining.
- requestedWorkSource.clearNames();
+ requestedWorkSource.clearNames();
+
+ if (!requestedWorkSource.isEmpty()) {
return requestedWorkSource;
- } else {
- loge("Got invalid work source request: " + requestedWorkSource.toString() +
- " from " + ci);
}
}
- WorkSource callingWorkSource = new WorkSource(ci.getUid());
- if (isWorkSourceValid(callingWorkSource)) {
- return callingWorkSource;
- } else {
- loge("Client has invalid work source: " + callingWorkSource);
- return new WorkSource();
+
+ if (ci.getUid() > 0) {
+ return new WorkSource(ci.getUid());
}
+
+ // We can't construct a sensible WorkSource because the one supplied to us was empty and
+ // we don't have a valid UID for the given client.
+ loge("Unable to compute workSource for client: " + ci + ", requested: "
+ + requestedWorkSource);
+ return new WorkSource();
}
private class RequestInfo<T> {
@@ -525,6 +522,11 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
public boolean processMessage(Message msg) {
switch (msg.what) {
case CMD_DRIVER_LOADED:
+ if (mScannerImpl == null) {
+ loge("Failed to start single scan state machine because scanner impl"
+ + " is null");
+ return HANDLED;
+ }
transitionTo(mIdleState);
return HANDLED;
case CMD_DRIVER_UNLOADED:
@@ -605,7 +607,7 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
scanParams.getParcelable(WifiScanner.SCAN_PARAMS_SCAN_SETTINGS_KEY);
WorkSource workSource =
scanParams.getParcelable(WifiScanner.SCAN_PARAMS_WORK_SOURCE_KEY);
- if (validateScanRequest(ci, handler, scanSettings, workSource)) {
+ if (validateScanRequest(ci, handler, scanSettings)) {
logScanRequest("addSingleScanRequest", ci, handler, workSource,
scanSettings, null);
replySucceeded(msg);
@@ -711,8 +713,12 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
}
}
- boolean validateScanRequest(ClientInfo ci, int handler, ScanSettings settings,
- WorkSource workSource) {
+ boolean validateScanType(int type) {
+ return (type == WifiScanner.TYPE_LOW_LATENCY || type == WifiScanner.TYPE_LOW_POWER
+ || type == WifiScanner.TYPE_HIGH_ACCURACY);
+ }
+
+ boolean validateScanRequest(ClientInfo ci, int handler, ScanSettings settings) {
if (ci == null) {
Log.d(TAG, "Failing single scan request ClientInfo not found " + handler);
return false;
@@ -723,6 +729,10 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
return false;
}
}
+ if (!validateScanType(settings.type)) {
+ Log.e(TAG, "Invalid scan type " + settings.type);
+ return false;
+ }
if (mContext.checkPermission(
Manifest.permission.NETWORK_STACK, UNKNOWN_PID, ci.getUid())
== PERMISSION_DENIED) {
@@ -740,11 +750,64 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
return true;
}
+ int getNativeScanType(int type) {
+ switch(type) {
+ case WifiScanner.TYPE_LOW_LATENCY:
+ return WifiNative.SCAN_TYPE_LOW_LATENCY;
+ case WifiScanner.TYPE_LOW_POWER:
+ return WifiNative.SCAN_TYPE_LOW_POWER;
+ case WifiScanner.TYPE_HIGH_ACCURACY:
+ return WifiNative.SCAN_TYPE_HIGH_ACCURACY;
+ default:
+ // This should never happen becuase we've validated the incoming type in
+ // |validateScanType|.
+ throw new IllegalArgumentException("Invalid scan type " + type);
+ }
+ }
+
+ // We can coalesce a LOW_POWER/LOW_LATENCY scan request into an ongoing HIGH_ACCURACY
+ // scan request. But, we can't coalesce a HIGH_ACCURACY scan request into an ongoing
+ // LOW_POWER/LOW_LATENCY scan request.
+ boolean activeScanTypeSatisfies(int requestScanType) {
+ switch(mActiveScanSettings.scanType) {
+ case WifiNative.SCAN_TYPE_LOW_LATENCY:
+ case WifiNative.SCAN_TYPE_LOW_POWER:
+ return requestScanType != WifiNative.SCAN_TYPE_HIGH_ACCURACY;
+ case WifiNative.SCAN_TYPE_HIGH_ACCURACY:
+ return true;
+ default:
+ // This should never happen becuase we've validated the incoming type in
+ // |validateScanType|.
+ throw new IllegalArgumentException("Invalid scan type "
+ + mActiveScanSettings.scanType);
+ }
+ }
+
+ // If there is a HIGH_ACCURACY scan request among the requests being merged, the merged
+ // scan type should be HIGH_ACCURACY.
+ int mergeScanTypes(int existingScanType, int newScanType) {
+ switch(existingScanType) {
+ case WifiNative.SCAN_TYPE_LOW_LATENCY:
+ case WifiNative.SCAN_TYPE_LOW_POWER:
+ return newScanType;
+ case WifiNative.SCAN_TYPE_HIGH_ACCURACY:
+ return existingScanType;
+ default:
+ // This should never happen becuase we've validated the incoming type in
+ // |validateScanType|.
+ throw new IllegalArgumentException("Invalid scan type " + existingScanType);
+ }
+ }
+
boolean activeScanSatisfies(ScanSettings settings) {
if (mActiveScanSettings == null) {
return false;
}
+ if (!activeScanTypeSatisfies(getNativeScanType(settings.type))) {
+ return false;
+ }
+
// there is always one bucket for a single scan
WifiNative.BucketSettings activeBucket = mActiveScanSettings.buckets[0];
@@ -814,6 +877,8 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
ChannelCollection channels = mChannelHelper.createChannelCollection();
List<WifiNative.HiddenNetwork> hiddenNetworkList = new ArrayList<>();
for (RequestInfo<ScanSettings> entry : mPendingScans) {
+ settings.scanType =
+ mergeScanTypes(settings.scanType, getNativeScanType(entry.settings.type));
channels.addChannels(entry.settings);
if (entry.settings.hiddenNetworks != null) {
for (int i = 0; i < entry.settings.hiddenNetworks.length; i++) {
@@ -1012,10 +1077,13 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
// TODO this should be moved to a common location since it is used outside
// of this state machine. It is ok right now because the driver loaded event
// is sent to this state machine first.
+ mScannerImpl = mScannerImplFactory.create(mContext, mLooper, mClock);
if (mScannerImpl == null) {
- mScannerImpl = mScannerImplFactory.create(mContext, mLooper, mClock);
- mChannelHelper = mScannerImpl.getChannelHelper();
+ loge("Failed to start bgscan scan state machine because scanner impl"
+ + " is null");
+ return HANDLED;
}
+ mChannelHelper = mScannerImpl.getChannelHelper();
mBackgroundScheduler = new BackgroundScanScheduler(mChannelHelper);
@@ -1077,7 +1145,9 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
public void exit() {
sendBackgroundScanFailedToAllAndClear(
WifiScanner.REASON_UNSPECIFIED, "Scan was interrupted");
- mScannerImpl.cleanup();
+ if (mScannerImpl != null) {
+ mScannerImpl.cleanup();
+ }
}
@Override
@@ -1422,6 +1492,11 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
public boolean processMessage(Message msg) {
switch (msg.what) {
case CMD_DRIVER_LOADED:
+ if (mScannerImpl == null) {
+ loge("Failed to start pno scan state machine because scanner impl"
+ + " is null");
+ return HANDLED;
+ }
transitionTo(mStartedState);
break;
case CMD_DRIVER_UNLOADED:
@@ -1767,7 +1842,7 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
// This has to be implemented by subclasses to report events back to clients.
public abstract void reportEvent(int what, int arg1, int arg2, Object obj);
- // TODO(b/27903217): Blame scan on provided work source
+ // TODO(b/27903217, 71530998): This is dead code. Should this be wired up ?
private void reportBatchedScanStart() {
if (mUid == 0)
return;
@@ -1781,6 +1856,7 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
}
}
+ // TODO(b/27903217, 71530998): This is dead code. Should this be wired up ?
private void reportBatchedScanStop() {
if (mUid == 0)
return;
@@ -1807,7 +1883,8 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
return totalScanDurationPerHour / ChannelHelper.SCAN_PERIOD_PER_CHANNEL_MS;
}
- public void reportScanWorkUpdate() {
+ // TODO(b/27903217, 71530998): This is dead code. Should this be wired up ?
+ private void reportScanWorkUpdate() {
if (mScanWorkReported) {
reportBatchedScanStop();
mScanWorkReported = false;
@@ -2111,8 +2188,24 @@ public class WifiScanningServiceImpl extends IWifiScanner.Stub {
return "results=" + results.length;
}
+ static String getScanTypeString(int type) {
+ switch(type) {
+ case WifiScanner.TYPE_LOW_LATENCY:
+ return "LOW LATENCY";
+ case WifiScanner.TYPE_LOW_POWER:
+ return "LOW POWER";
+ case WifiScanner.TYPE_HIGH_ACCURACY:
+ return "HIGH ACCURACY";
+ default:
+ // This should never happen becuase we've validated the incoming type in
+ // |validateScanType|.
+ throw new IllegalArgumentException("Invalid scan type " + type);
+ }
+ }
+
static String describeTo(StringBuilder sb, ScanSettings scanSettings) {
sb.append("ScanSettings { ")
+ .append(" type:").append(getScanTypeString(scanSettings.type))
.append(" band:").append(ChannelHelper.bandToString(scanSettings.band))
.append(" period:").append(scanSettings.periodInMs)
.append(" reportEvents:").append(scanSettings.reportEvents)
diff --git a/com/android/server/wifi/scanner/WificondScannerImpl.java b/com/android/server/wifi/scanner/WificondScannerImpl.java
index 9afe061d..a2d0f435 100644
--- a/com/android/server/wifi/scanner/WificondScannerImpl.java
+++ b/com/android/server/wifi/scanner/WificondScannerImpl.java
@@ -57,6 +57,7 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
private static final int MAX_SCAN_BUCKETS = 16;
private final Context mContext;
+ private final String mIfaceName;
private final WifiNative mWifiNative;
private final AlarmManager mAlarmManager;
private final Handler mEventHandler;
@@ -66,6 +67,7 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
private final Object mSettingsLock = new Object();
private ArrayList<ScanDetail> mNativeScanResults;
+ private ArrayList<ScanDetail> mNativePnoScanResults;
private WifiScanner.ScanData mLatestSingleScanResult =
new WifiScanner.ScanData(0, 0, new ScanResult[0]);
@@ -92,10 +94,11 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
}
};
- public WificondScannerImpl(Context context, WifiNative wifiNative,
- WifiMonitor wifiMonitor, ChannelHelper channelHelper,
- Looper looper, Clock clock) {
+ public WificondScannerImpl(Context context, String ifaceName, WifiNative wifiNative,
+ WifiMonitor wifiMonitor, ChannelHelper channelHelper,
+ Looper looper, Clock clock) {
mContext = context;
+ mIfaceName = ifaceName;
mWifiNative = wifiNative;
mChannelHelper = channelHelper;
mAlarmManager = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
@@ -106,11 +109,11 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
mHwPnoScanSupported = mContext.getResources().getBoolean(
R.bool.config_wifi_background_scan_support);
- wifiMonitor.registerHandler(mWifiNative.getInterfaceName(),
+ wifiMonitor.registerHandler(mIfaceName,
WifiMonitor.SCAN_FAILED_EVENT, mEventHandler);
- wifiMonitor.registerHandler(mWifiNative.getInterfaceName(),
+ wifiMonitor.registerHandler(mIfaceName,
WifiMonitor.PNO_SCAN_RESULTS_EVENT, mEventHandler);
- wifiMonitor.registerHandler(mWifiNative.getInterfaceName(),
+ wifiMonitor.registerHandler(mIfaceName,
WifiMonitor.SCAN_RESULTS_EVENT, mEventHandler);
}
@@ -180,7 +183,9 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
Set<Integer> freqs;
if (!allFreqs.isEmpty()) {
freqs = allFreqs.getScanFreqs();
- success = mWifiNative.scan(freqs, hiddenNetworkSSIDSet);
+ success = mWifiNative.scan(
+ mWifiNative.getClientInterfaceName(), settings.scanType, freqs,
+ hiddenNetworkSSIDSet);
if (!success) {
Log.e(TAG, "Failed to start scan, freqs=" + freqs);
}
@@ -293,11 +298,12 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
// got a scan before we started scanning or after scan was canceled
return;
}
- mNativeScanResults = mWifiNative.getPnoScanResults();
+ mNativePnoScanResults =
+ mWifiNative.getPnoScanResults(mWifiNative.getClientInterfaceName());
List<ScanResult> hwPnoScanResults = new ArrayList<>();
int numFilteredScanResults = 0;
- for (int i = 0; i < mNativeScanResults.size(); ++i) {
- ScanResult result = mNativeScanResults.get(i).getScanResult();
+ for (int i = 0; i < mNativePnoScanResults.size(); ++i) {
+ ScanResult result = mNativePnoScanResults.get(i).getScanResult();
long timestamp_ms = result.timestamp / 1000; // convert us -> ms
if (timestamp_ms > mLastPnoScanSettings.startTime) {
hwPnoScanResults.add(result);
@@ -336,7 +342,7 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
return;
}
- mNativeScanResults = mWifiNative.getScanResults();
+ mNativeScanResults = mWifiNative.getScanResults(mWifiNative.getClientInterfaceName());
List<ScanResult> singleScanResults = new ArrayList<>();
int numFilteredScanResults = 0;
for (int i = 0; i < mNativeScanResults.size(); ++i) {
@@ -382,11 +388,11 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
}
private boolean startHwPnoScan(WifiNative.PnoSettings pnoSettings) {
- return mWifiNative.startPnoScan(pnoSettings);
+ return mWifiNative.startPnoScan(mWifiNative.getClientInterfaceName(), pnoSettings);
}
private void stopHwPnoScan() {
- mWifiNative.stopPnoScan();
+ mWifiNative.stopPnoScan(mWifiNative.getClientInterfaceName());
}
/**
@@ -447,11 +453,19 @@ public class WificondScannerImpl extends WifiScannerImpl implements Handler.Call
protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
synchronized (mSettingsLock) {
pw.println("Latest native scan results:");
- if (mNativeScanResults != null && mNativeScanResults.size() != 0) {
+ dumpCachedScanResult(pw, mNativeScanResults);
+ pw.println("Latest native pno scan results:");
+ dumpCachedScanResult(pw, mNativePnoScanResults);
+ }
+ }
+
+ private void dumpCachedScanResult(PrintWriter pw, ArrayList<ScanDetail> scanResults) {
+ synchronized (mSettingsLock) {
+ if (scanResults != null && scanResults.size() != 0) {
long nowMs = mClock.getElapsedSinceBootMillis();
pw.println(" BSSID Frequency RSSI Age(sec) SSID "
+ " Flags");
- for (ScanDetail scanDetail : mNativeScanResults) {
+ for (ScanDetail scanDetail : scanResults) {
ScanResult r = scanDetail.getScanResult();
long timeStampMs = r.timestamp / 1000;
String age;
diff --git a/com/android/server/wifi/util/ApConfigUtil.java b/com/android/server/wifi/util/ApConfigUtil.java
index b1a49480..dfda45b7 100644
--- a/com/android/server/wifi/util/ApConfigUtil.java
+++ b/com/android/server/wifi/util/ApConfigUtil.java
@@ -62,7 +62,7 @@ public class ApConfigUtil {
/**
* Return a channel number for AP setup based on the frequency band.
- * @param apBand 0 for 2GHz, 1 for 5GHz
+ * @param apBand one of the value of WifiConfiguration.AP_BAND_*.
* @param allowed2GChannels list of allowed 2GHz channels
* @param allowed5GFreqList list of allowed 5GHz frequencies
* @return a valid channel number on success, -1 on failure.
@@ -71,12 +71,15 @@ public class ApConfigUtil {
ArrayList<Integer> allowed2GChannels,
int[] allowed5GFreqList) {
if (apBand != WifiConfiguration.AP_BAND_2GHZ
- && apBand != WifiConfiguration.AP_BAND_5GHZ) {
+ && apBand != WifiConfiguration.AP_BAND_5GHZ
+ && apBand != WifiConfiguration.AP_BAND_ANY) {
Log.e(TAG, "Invalid band: " + apBand);
return -1;
}
- if (apBand == WifiConfiguration.AP_BAND_2GHZ) {
+ // TODO(b/72120668): Create channel selection logic for AP_BAND_ANY.
+ if (apBand == WifiConfiguration.AP_BAND_2GHZ
+ || apBand == WifiConfiguration.AP_BAND_ANY) {
/* Select a channel from 2GHz band. */
if (allowed2GChannels == null || allowed2GChannels.size() == 0) {
Log.d(TAG, "2GHz allowed channel list not specified");
diff --git a/com/android/server/wifi/util/TelephonyUtil.java b/com/android/server/wifi/util/TelephonyUtil.java
index 5c489b83..e6b39531 100644
--- a/com/android/server/wifi/util/TelephonyUtil.java
+++ b/com/android/server/wifi/util/TelephonyUtil.java
@@ -18,18 +18,35 @@ package com.android.server.wifi.util;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiEnterpriseConfig;
+import android.telephony.ImsiEncryptionInfo;
import android.telephony.TelephonyManager;
import android.util.Base64;
import android.util.Log;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.server.wifi.WifiNative;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.NoSuchPaddingException;
+
/**
* Utilities for the Wifi Service to interact with telephony.
*/
public class TelephonyUtil {
public static final String TAG = "TelephonyUtil";
+ private static final String THREE_GPP_NAI_REALM_FORMAT = "wlan.mnc%s.mcc%s.3gppnetwork.org";
+
+ // IMSI encryption method: RSA-OAEP with SHA-256 hash function
+ private static final String IMSI_CIPHER_TRANSFORMATION =
+ "RSA/ECB/OAEPwithSHA-256andMGF1Padding";
+
/**
* Get the identity for the current SIM or null if the SIM is not available
*
@@ -37,7 +54,8 @@ public class TelephonyUtil {
* @param config WifiConfiguration that indicates what sort of authentication is necessary
* @return String with the identity or none if the SIM is not available or config is invalid
*/
- public static String getSimIdentity(TelephonyManager tm, WifiConfiguration config) {
+ public static String getSimIdentity(TelephonyManager tm, TelephonyUtil telephonyUtil,
+ WifiConfiguration config) {
if (tm == null) {
Log.e(TAG, "No valid TelephonyManager");
return null;
@@ -49,18 +67,69 @@ public class TelephonyUtil {
mccMnc = tm.getSimOperator();
}
- return buildIdentity(getSimMethodForConfig(config), imsi, mccMnc);
+ ImsiEncryptionInfo imsiEncryptionInfo;
+ try {
+ imsiEncryptionInfo = tm.getCarrierInfoForImsiEncryption(TelephonyManager.KEY_TYPE_WLAN);
+ } catch (RuntimeException e) {
+ Log.e(TAG, "Failed to get imsi encryption info: " + e.getMessage());
+ return null;
+ }
+
+ return buildIdentity(telephonyUtil, getSimMethodForConfig(config), imsi, mccMnc,
+ imsiEncryptionInfo);
}
/**
- * create Permanent Identity base on IMSI,
+ * Encrypt the given data with the given public key. The encrypted data will be returned as
+ * a Base64 encoded string.
*
- * rfc4186 & rfc4187:
- * identity = usernam@realm
- * with username = prefix | IMSI
- * and realm is derived MMC/MNC tuple according 3GGP spec(TS23.003)
+ * @param key The public key to use for encryption
+ * @return Base64 encoded string, or null if encryption failed
*/
- private static String buildIdentity(int eapMethod, String imsi, String mccMnc) {
+ @VisibleForTesting
+ public String encryptDataUsingPublicKey(PublicKey key, byte[] data) {
+ try {
+ Cipher cipher = Cipher.getInstance(IMSI_CIPHER_TRANSFORMATION);
+ cipher.init(Cipher.ENCRYPT_MODE, key);
+ byte[] encryptedBytes = cipher.doFinal(data);
+ return Base64.encodeToString(encryptedBytes, 0, encryptedBytes.length, Base64.DEFAULT);
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException
+ | IllegalBlockSizeException | BadPaddingException e) {
+ Log.e(TAG, "Encryption failed: " + e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * Create an identity used for SIM-based EAP authentication. The identity will be based on
+ * the info retrieved from the SIM card, such as IMSI and IMSI encryption info. The IMSI
+ * contained in the identity will be encrypted if IMSI encryption info is provided.
+ *
+ * See rfc4186 & rfc4187 & rfc5448:
+ *
+ * Identity format:
+ * Prefix | [IMSI || Encrypted IMSI] | @realm | {, Key Identifier AVP}
+ * where "|" denotes concatenation, "||" denotes exclusive value, "{}"
+ * denotes optional value, and realm is the 3GPP network domain name derived from the given
+ * MCC/MNC according to the 3GGP spec(TS23.003).
+ *
+ * Prefix value:
+ * "\0" - Encrypted Identity
+ * "0" - EAP-AKA Identity
+ * "1" - EAP-SIM Identity
+ * "6" - EAP-AKA' Identity
+ *
+ * Encrypted IMSI:
+ * Base64{RSA_Public_Key_Encryption{IMSI}}
+ *
+ * @param eapMethod EAP authentication method: EAP-SIM, EAP-AKA, EAP-AKA'
+ * @param imsi The IMSI retrieved from the SIM
+ * @param mccMnc The MCC MNC identifier retrieved from the SIM
+ * @param imsiEncryptionInfo The IMSI encryption info retrieved from the SIM
+ */
+ private static String buildIdentity(TelephonyUtil telephonyUtil, int eapMethod,
+ String imsi, String mccMnc,
+ ImsiEncryptionInfo imsiEncryptionInfo) {
if (imsi == null || imsi.isEmpty()) {
Log.e(TAG, "No IMSI or IMSI is null");
return null;
@@ -93,7 +162,29 @@ public class TelephonyUtil {
mnc = imsi.substring(3, 6);
}
- return prefix + imsi + "@wlan.mnc" + mnc + ".mcc" + mcc + ".3gppnetwork.org";
+ String naiRealm = String.format(THREE_GPP_NAI_REALM_FORMAT, mnc, mcc);
+
+ if (imsiEncryptionInfo == null) {
+ // Non-encrypted identity.
+ return prefix + imsi + "@" + naiRealm;
+ }
+
+ // Override prefix for encrypted IMSI.
+ prefix = "\0";
+
+ // Build and return the encrypted identity.
+ String encryptedImsi = telephonyUtil.encryptDataUsingPublicKey(
+ imsiEncryptionInfo.getPublicKey(), imsi.getBytes());
+ if (encryptedImsi == null) {
+ Log.e(TAG, "Failed to encrypt IMSI");
+ return null;
+ }
+ String encryptedIdentity = prefix + encryptedImsi + "@" + naiRealm;
+ if (imsiEncryptionInfo.getKeyIdentifier() != null) {
+ // Include key identifier AVP (Attribute Value Pair).
+ encryptedIdentity = encryptedIdentity + "," + imsiEncryptionInfo.getKeyIdentifier();
+ }
+ return encryptedIdentity;
}
/**
diff --git a/com/android/server/wifi/util/WifiPermissionsUtil.java b/com/android/server/wifi/util/WifiPermissionsUtil.java
index 4ae7d132..d9737f9a 100644
--- a/com/android/server/wifi/util/WifiPermissionsUtil.java
+++ b/com/android/server/wifi/util/WifiPermissionsUtil.java
@@ -21,7 +21,6 @@ import android.app.AppOpsManager;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
-import android.net.NetworkScoreManager;
import android.os.RemoteException;
import android.os.UserManager;
import android.provider.Settings;
@@ -43,19 +42,17 @@ public class WifiPermissionsUtil {
private final AppOpsManager mAppOps;
private final UserManager mUserManager;
private final WifiSettingsStore mSettingsStore;
- private final NetworkScoreManager mNetworkScoreManager;
private WifiLog mLog;
public WifiPermissionsUtil(WifiPermissionsWrapper wifiPermissionsWrapper,
Context context, WifiSettingsStore settingsStore, UserManager userManager,
- NetworkScoreManager networkScoreManager, WifiInjector wifiInjector) {
+ WifiInjector wifiInjector) {
mWifiPermissionsWrapper = wifiPermissionsWrapper;
mContext = context;
mUserManager = userManager;
mAppOps = (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
mSettingsStore = settingsStore;
mLog = wifiInjector.makeLog(TAG);
- mNetworkScoreManager = networkScoreManager;
}
/**
@@ -132,10 +129,8 @@ public class WifiPermissionsUtil {
public boolean canAccessScanResults(String pkgName, int uid,
int minVersion) throws SecurityException {
mAppOps.checkPackage(uid, pkgName);
- // Check if the calling Uid has CAN_READ_PEER_MAC_ADDRESS
- // permission or is an Active Nw scorer.
- boolean canCallingUidAccessLocation = checkCallerHasPeersMacAddressPermission(uid)
- || isCallerActiveNwScorer(uid);
+ // Check if the calling Uid has CAN_READ_PEER_MAC_ADDRESS permission.
+ boolean canCallingUidAccessLocation = checkCallerHasPeersMacAddressPermission(uid);
// LocationAccess by App: For AppVersion older than minVersion,
// it is sufficient to check if the App is foreground.
// Otherwise, Location Mode must be enabled and caller must have
@@ -173,13 +168,6 @@ public class WifiPermissionsUtil {
}
/**
- * Returns true if the caller is an Active Network Scorer.
- */
- private boolean isCallerActiveNwScorer(int uid) {
- return mNetworkScoreManager.isCallerActiveScorer(uid);
- }
-
- /**
* Returns true if Wifi scan operation is allowed for this caller
* and package.
*/
diff --git a/com/android/server/wifi/util/XmlUtil.java b/com/android/server/wifi/util/XmlUtil.java
index f4c3ab1a..5cceca3d 100644
--- a/com/android/server/wifi/util/XmlUtil.java
+++ b/com/android/server/wifi/util/XmlUtil.java
@@ -296,7 +296,7 @@ public class XmlUtil {
}
/**
- * Utility class to serialize and deseriaize {@link WifiConfiguration} object to XML &
+ * Utility class to serialize and deserialize {@link WifiConfiguration} object to XML &
* vice versa.
* This is used by both {@link com.android.server.wifi.WifiConfigStore} &
* {@link com.android.server.wifi.WifiBackupRestore} modules.
diff --git a/com/android/server/wifi/wificond/NativeScanResult.java b/com/android/server/wifi/wificond/NativeScanResult.java
index d41237a8..d7bfed8f 100644
--- a/com/android/server/wifi/wificond/NativeScanResult.java
+++ b/com/android/server/wifi/wificond/NativeScanResult.java
@@ -19,6 +19,7 @@ package com.android.server.wifi.wificond;
import android.os.Parcel;
import android.os.Parcelable;
+import java.util.ArrayList;
import java.util.BitSet;
/**
@@ -37,6 +38,7 @@ public class NativeScanResult implements Parcelable {
public long tsf;
public BitSet capability;
public boolean associated;
+ public ArrayList<RadioChainInfo> radioChainInfos;
/** public constructor */
public NativeScanResult() { }
@@ -76,6 +78,7 @@ public class NativeScanResult implements Parcelable {
}
out.writeInt(capabilityInt);
out.writeInt(associated ? 1 : 0);
+ out.writeTypedList(radioChainInfos);
}
/** implement Parcelable interface */
@@ -98,6 +101,8 @@ public class NativeScanResult implements Parcelable {
}
}
result.associated = (in.readInt() != 0);
+ result.radioChainInfos = new ArrayList<>();
+ in.readTypedList(result.radioChainInfos, RadioChainInfo.CREATOR);
return result;
}
diff --git a/com/android/server/wifi/wificond/RadioChainInfo.java b/com/android/server/wifi/wificond/RadioChainInfo.java
new file mode 100644
index 00000000..4b5177e7
--- /dev/null
+++ b/com/android/server/wifi/wificond/RadioChainInfo.java
@@ -0,0 +1,105 @@
+/*
+ * 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.wificond;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.util.Objects;
+
+/**
+ * RadioChainInfo for wificond
+ *
+ * @hide
+ */
+public class RadioChainInfo implements Parcelable {
+ private static final String TAG = "RadioChainInfo";
+
+ public int chainId;
+ public int level;
+
+
+ /** public constructor */
+ public RadioChainInfo() { }
+
+ public RadioChainInfo(int chainId, int level) {
+ this.chainId = chainId;
+ this.level = level;
+ }
+
+ /** override comparator */
+ @Override
+ public boolean equals(Object rhs) {
+ if (this == rhs) return true;
+ if (!(rhs instanceof RadioChainInfo)) {
+ return false;
+ }
+ RadioChainInfo chainInfo = (RadioChainInfo) rhs;
+ if (chainInfo == null) {
+ return false;
+ }
+ return chainId == chainInfo.chainId && level == chainInfo.level;
+ }
+
+ /** override hash code */
+ @Override
+ public int hashCode() {
+ return Objects.hash(chainId, level);
+ }
+
+
+ /** implement Parcelable interface */
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * implement Parcelable interface
+ * |flags| is ignored.
+ */
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeInt(chainId);
+ out.writeInt(level);
+ }
+
+ /** implement Parcelable interface */
+ public static final Parcelable.Creator<RadioChainInfo> CREATOR =
+ new Parcelable.Creator<RadioChainInfo>() {
+ /**
+ * Caller is responsible for providing a valid parcel.
+ */
+ @Override
+ public RadioChainInfo createFromParcel(Parcel in) {
+ RadioChainInfo result = new RadioChainInfo();
+ result.chainId = in.readInt();
+ result.level = in.readInt();
+ if (in.dataAvail() != 0) {
+ Log.e(TAG, "Found trailing data after parcel parsing.");
+ }
+
+ return result;
+ }
+
+ @Override
+ public RadioChainInfo[] newArray(int size) {
+ return new RadioChainInfo[size];
+ }
+ };
+}
diff --git a/com/android/server/wifi/wificond/SingleScanSettings.java b/com/android/server/wifi/wificond/SingleScanSettings.java
index 02950f0d..ed1e41cd 100644
--- a/com/android/server/wifi/wificond/SingleScanSettings.java
+++ b/com/android/server/wifi/wificond/SingleScanSettings.java
@@ -16,6 +16,7 @@
package com.android.server.wifi.wificond;
+import android.net.wifi.IWifiScannerImpl;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
@@ -31,6 +32,7 @@ import java.util.Objects;
public class SingleScanSettings implements Parcelable {
private static final String TAG = "SingleScanSettings";
+ public int scanType;
public ArrayList<ChannelSettings> channelSettings;
public ArrayList<HiddenNetwork> hiddenNetworks;
@@ -48,14 +50,15 @@ public class SingleScanSettings implements Parcelable {
if (settings == null) {
return false;
}
- return channelSettings.equals(settings.channelSettings)
+ return scanType == settings.scanType
+ && channelSettings.equals(settings.channelSettings)
&& hiddenNetworks.equals(settings.hiddenNetworks);
}
/** override hash code */
@Override
public int hashCode() {
- return Objects.hash(channelSettings, hiddenNetworks);
+ return Objects.hash(scanType, channelSettings, hiddenNetworks);
}
@@ -65,12 +68,22 @@ public class SingleScanSettings implements Parcelable {
return 0;
}
+ private static boolean isValidScanType(int scanType) {
+ return scanType == IWifiScannerImpl.SCAN_TYPE_LOW_SPAN
+ || scanType == IWifiScannerImpl.SCAN_TYPE_LOW_POWER
+ || scanType == IWifiScannerImpl.SCAN_TYPE_HIGH_ACCURACY;
+ }
+
/**
* implement Parcelable interface
* |flags| is ignored.
*/
@Override
public void writeToParcel(Parcel out, int flags) {
+ if (!isValidScanType(scanType)) {
+ Log.wtf(TAG, "Invalid scan type " + scanType);
+ }
+ out.writeInt(scanType);
out.writeTypedList(channelSettings);
out.writeTypedList(hiddenNetworks);
}
@@ -84,6 +97,10 @@ public class SingleScanSettings implements Parcelable {
@Override
public SingleScanSettings createFromParcel(Parcel in) {
SingleScanSettings result = new SingleScanSettings();
+ result.scanType = in.readInt();
+ if (!isValidScanType(result.scanType)) {
+ Log.wtf(TAG, "Invalid scan type " + result.scanType);
+ }
result.channelSettings = new ArrayList<ChannelSettings>();
in.readTypedList(result.channelSettings, ChannelSettings.CREATOR);
result.hiddenNetworks = new ArrayList<HiddenNetwork>();
@@ -91,7 +108,6 @@ public class SingleScanSettings implements Parcelable {
if (in.dataAvail() != 0) {
Log.e(TAG, "Found trailing data after parcel parsing.");
}
-
return result;
}
diff --git a/com/android/server/wm/AccessibilityController.java b/com/android/server/wm/AccessibilityController.java
index 163b1600..659253f9 100644
--- a/com/android/server/wm/AccessibilityController.java
+++ b/com/android/server/wm/AccessibilityController.java
@@ -346,12 +346,12 @@ final class AccessibilityController {
final boolean magnifying = mMagnifedViewport.isMagnifyingLocked();
if (magnifying) {
switch (transition) {
- case AppTransition.TRANSIT_ACTIVITY_OPEN:
- case AppTransition.TRANSIT_TASK_OPEN:
- case AppTransition.TRANSIT_TASK_TO_FRONT:
- case AppTransition.TRANSIT_WALLPAPER_OPEN:
- case AppTransition.TRANSIT_WALLPAPER_CLOSE:
- case AppTransition.TRANSIT_WALLPAPER_INTRA_OPEN: {
+ case WindowManager.TRANSIT_ACTIVITY_OPEN:
+ case WindowManager.TRANSIT_TASK_OPEN:
+ case WindowManager.TRANSIT_TASK_TO_FRONT:
+ case WindowManager.TRANSIT_WALLPAPER_OPEN:
+ case WindowManager.TRANSIT_WALLPAPER_CLOSE:
+ case WindowManager.TRANSIT_WALLPAPER_INTRA_OPEN: {
mHandler.sendEmptyMessage(MyHandler.MESSAGE_NOTIFY_USER_CONTEXT_CHANGED);
}
}
diff --git a/com/android/server/wm/AppTransition.java b/com/android/server/wm/AppTransition.java
index 2ac75834..fc7ad09d 100644
--- a/com/android/server/wm/AppTransition.java
+++ b/com/android/server/wm/AppTransition.java
@@ -16,6 +16,28 @@
package com.android.server.wm;
+import static android.view.WindowManager.LayoutParams;
+import static android.view.WindowManager.TRANSIT_ACTIVITY_CLOSE;
+import static android.view.WindowManager.TRANSIT_ACTIVITY_OPEN;
+import static android.view.WindowManager.TRANSIT_ACTIVITY_RELAUNCH;
+import static android.view.WindowManager.TRANSIT_DOCK_TASK_FROM_RECENTS;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
+import static android.view.WindowManager.TRANSIT_NONE;
+import static android.view.WindowManager.TRANSIT_TASK_CLOSE;
+import static android.view.WindowManager.TRANSIT_TASK_OPEN;
+import static android.view.WindowManager.TRANSIT_TASK_OPEN_BEHIND;
+import static android.view.WindowManager.TRANSIT_TASK_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TASK_TO_FRONT;
+import static android.view.WindowManager.TRANSIT_UNSET;
+import static android.view.WindowManager.TRANSIT_WALLPAPER_CLOSE;
+import static android.view.WindowManager.TRANSIT_WALLPAPER_INTRA_CLOSE;
+import static android.view.WindowManager.TRANSIT_WALLPAPER_INTRA_OPEN;
+import static android.view.WindowManager.TRANSIT_WALLPAPER_OPEN;
import static com.android.internal.R.styleable.WindowAnimation_activityCloseEnterAnimation;
import static com.android.internal.R.styleable.WindowAnimation_activityCloseExitAnimation;
import static com.android.internal.R.styleable.WindowAnimation_activityOpenEnterAnimation;
@@ -49,14 +71,17 @@ import static com.android.server.wm.WindowStateAnimator.STACK_CLIP_NONE;
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.DrawableRes;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.res.Configuration;
+import android.graphics.Color;
import android.graphics.GraphicBuffer;
import android.graphics.Path;
import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
import android.os.Binder;
import android.os.Debug;
import android.os.IBinder;
@@ -70,8 +95,13 @@ import android.util.Slog;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
import android.view.AppTransitionAnimationSpec;
+import android.view.DisplayListCanvas;
import android.view.IAppTransitionAnimationSpecsFuture;
-import android.view.WindowManager;
+import android.view.RemoteAnimationAdapter;
+import android.view.RenderNode;
+import android.view.ThreadedRenderer;
+import android.view.WindowManager.TransitionFlags;
+import android.view.WindowManager.TransitionType;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
@@ -103,63 +133,6 @@ public class AppTransition implements Dump {
private static final String TAG = TAG_WITH_CLASS_NAME ? "AppTransition" : TAG_WM;
private static final int CLIP_REVEAL_TRANSLATION_Y_DP = 8;
- /** Not set up for a transition. */
- public static final int TRANSIT_UNSET = -1;
- /** No animation for transition. */
- public static final int TRANSIT_NONE = 0;
- /** A window in a new activity is being opened on top of an existing one in the same task. */
- public static final int TRANSIT_ACTIVITY_OPEN = 6;
- /** The window in the top-most activity is being closed to reveal the
- * previous activity in the same task. */
- public static final int TRANSIT_ACTIVITY_CLOSE = 7;
- /** A window in a new task is being opened on top of an existing one
- * in another activity's task. */
- public static final int TRANSIT_TASK_OPEN = 8;
- /** A window in the top-most activity is being closed to reveal the
- * previous activity in a different task. */
- public static final int TRANSIT_TASK_CLOSE = 9;
- /** A window in an existing task is being displayed on top of an existing one
- * in another activity's task. */
- public static final int TRANSIT_TASK_TO_FRONT = 10;
- /** A window in an existing task is being put below all other tasks. */
- public static final int TRANSIT_TASK_TO_BACK = 11;
- /** A window in a new activity that doesn't have a wallpaper is being opened on top of one that
- * does, effectively closing the wallpaper. */
- public static final int TRANSIT_WALLPAPER_CLOSE = 12;
- /** A window in a new activity that does have a wallpaper is being opened on one that didn't,
- * effectively opening the wallpaper. */
- public static final int TRANSIT_WALLPAPER_OPEN = 13;
- /** A window in a new activity is being opened on top of an existing one, and both are on top
- * of the wallpaper. */
- public static final int TRANSIT_WALLPAPER_INTRA_OPEN = 14;
- /** The window in the top-most activity is being closed to reveal the previous activity, and
- * both are on top of the wallpaper. */
- public static final int TRANSIT_WALLPAPER_INTRA_CLOSE = 15;
- /** A window in a new task is being opened behind an existing one in another activity's task.
- * The new window will show briefly and then be gone. */
- public static final int TRANSIT_TASK_OPEN_BEHIND = 16;
- /** A window in a task is being animated in-place. */
- public static final int TRANSIT_TASK_IN_PLACE = 17;
- /** An activity is being relaunched (e.g. due to configuration change). */
- public static final int TRANSIT_ACTIVITY_RELAUNCH = 18;
- /** A task is being docked from recents. */
- public static final int TRANSIT_DOCK_TASK_FROM_RECENTS = 19;
- /** Keyguard is going away */
- public static final int TRANSIT_KEYGUARD_GOING_AWAY = 20;
- /** Keyguard is going away with showing an activity behind that requests wallpaper */
- public static final int TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER = 21;
- /** Keyguard is being occluded */
- public static final int TRANSIT_KEYGUARD_OCCLUDE = 22;
- /** Keyguard is being unoccluded */
- public static final int TRANSIT_KEYGUARD_UNOCCLUDE = 23;
-
- /** Transition flag: Keyguard is going away, but keeping the notification shade open */
- public static final int TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE = 0x1;
- /** Transition flag: Keyguard is going away, but doesn't want an animation for it */
- public static final int TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION = 0x2;
- /** Transition flag: Keyguard is going away while it was showing the system wallpaper. */
- public static final int TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER = 0x4;
-
/** Fraction of animation at which the recents thumbnail stays completely transparent */
private static final float RECENTS_THUMBNAIL_FADEIN_FRACTION = 0.5f;
/** Fraction of animation at which the recents thumbnail becomes completely transparent */
@@ -185,8 +158,8 @@ public class AppTransition implements Dump {
private final Context mContext;
private final WindowManagerService mService;
- private int mNextAppTransition = TRANSIT_UNSET;
- private int mNextAppTransitionFlags = 0;
+ private @TransitionType int mNextAppTransition = TRANSIT_UNSET;
+ private @TransitionFlags int mNextAppTransitionFlags = 0;
private int mLastUsedAppTransition = TRANSIT_UNSET;
private String mLastOpeningApp;
private String mLastClosingApp;
@@ -207,6 +180,8 @@ public class AppTransition implements Dump {
* }.
*/
private static final int NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS = 9;
+ private static final int NEXT_TRANSIT_TYPE_REMOTE = 10;
+
private int mNextAppTransitionType = NEXT_TRANSIT_TYPE_NONE;
// These are the possible states for the enter/exit activities during a thumbnail transition
@@ -269,6 +244,8 @@ public class AppTransition implements Dump {
private final boolean mGridLayoutRecentsEnabled;
private final boolean mLowRamRecentsEnabled;
+ private RemoteAnimationController mRemoteAnimationController;
+
AppTransition(Context context, WindowManagerService service) {
mContext = context;
mService = service;
@@ -315,11 +292,11 @@ public class AppTransition implements Dump {
return mNextAppTransition != TRANSIT_UNSET;
}
- boolean isTransitionEqual(int transit) {
+ boolean isTransitionEqual(@TransitionType int transit) {
return mNextAppTransition == transit;
}
- int getAppTransition() {
+ @TransitionType int getAppTransition() {
return mNextAppTransition;
}
@@ -391,6 +368,11 @@ public class AppTransition implements Dump {
mNextAppTransitionType == NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN;
}
+
+ boolean isNextAppTransitionOpenCrossProfileApps() {
+ return mNextAppTransitionType == NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS;
+ }
+
/**
* @return true if and only if we are currently fetching app transition specs from the future
* passed into {@link #overridePendingAppTransitionMultiThumbFuture}
@@ -443,6 +425,9 @@ public class AppTransition implements Dump {
app.startDelayingAnimationStart();
}
}
+ if (mRemoteAnimationController != null) {
+ mRemoteAnimationController.goodToGo();
+ }
return redoLayout;
}
@@ -457,6 +442,7 @@ public class AppTransition implements Dump {
mNextAppTransitionType = NEXT_TRANSIT_TYPE_NONE;
mNextAppTransitionPackage = null;
mNextAppTransitionAnimationsSpecs.clear();
+ mRemoteAnimationController = null;
mNextAppTransitionAnimationsSpecsFuture = null;
mDefaultNextAppTransitionAnimationSpec = null;
mAnimationFinishedCallback = null;
@@ -465,7 +451,7 @@ public class AppTransition implements Dump {
void freeze() {
final int transit = mNextAppTransition;
- setAppTransition(AppTransition.TRANSIT_UNSET, 0 /* flags */);
+ setAppTransition(TRANSIT_UNSET, 0 /* flags */);
clear();
setReady();
notifyAppTransitionCancelledLocked(transit);
@@ -520,7 +506,7 @@ public class AppTransition implements Dump {
return redoLayout;
}
- private AttributeCache.Entry getCachedAnimations(WindowManager.LayoutParams lp) {
+ private AttributeCache.Entry getCachedAnimations(LayoutParams lp) {
if (DEBUG_ANIM) Slog.v(TAG, "Loading animations: layout params pkg="
+ (lp != null ? lp.packageName : null)
+ " resId=0x" + (lp != null ? Integer.toHexString(lp.windowAnimations) : null));
@@ -556,7 +542,7 @@ public class AppTransition implements Dump {
return null;
}
- Animation loadAnimationAttr(WindowManager.LayoutParams lp, int animAttr) {
+ Animation loadAnimationAttr(LayoutParams lp, int animAttr) {
int anim = 0;
Context context = mContext;
if (animAttr >= 0) {
@@ -572,7 +558,7 @@ public class AppTransition implements Dump {
return null;
}
- Animation loadAnimationRes(WindowManager.LayoutParams lp, int resId) {
+ Animation loadAnimationRes(LayoutParams lp, int resId) {
Context context = mContext;
if (resId >= 0) {
AttributeCache.Entry ent = getCachedAnimations(lp);
@@ -978,6 +964,43 @@ public class AppTransition implements Dump {
}
/**
+ * Creates an overlay with a background color and a thumbnail for the cross profile apps
+ * animation.
+ */
+ GraphicBuffer createCrossProfileAppsThumbnail(
+ @DrawableRes int thumbnailDrawableRes, Rect frame) {
+ final int width = frame.width();
+ final int height = frame.height();
+
+ final RenderNode node = RenderNode.create("CrossProfileAppsThumbnail", null);
+ node.setLeftTopRightBottom(0, 0, width, height);
+ node.setClipToBounds(false);
+
+ final DisplayListCanvas canvas = node.start(width, height);
+ canvas.drawColor(Color.argb(0.6f, 0, 0, 0));
+ final int thumbnailSize = mService.mContext.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.cross_profile_apps_thumbnail_size);
+ final Drawable drawable = mService.mContext.getDrawable(thumbnailDrawableRes);
+ drawable.setBounds(
+ (width - thumbnailSize) / 2,
+ (height - thumbnailSize) / 2,
+ (width + thumbnailSize) / 2,
+ (height + thumbnailSize) / 2);
+ drawable.draw(canvas);
+ node.end(canvas);
+
+ return ThreadedRenderer.createHardwareBitmap(node, width, height)
+ .createGraphicBufferHandle();
+ }
+
+ Animation createCrossProfileAppsThumbnailAnimationLocked(Rect appRect) {
+ final Animation animation = loadAnimationRes(
+ "android", com.android.internal.R.anim.cross_profile_apps_thumbnail_enter);
+ return prepareThumbnailAnimationWithDuration(animation, appRect.width(),
+ appRect.height(), 0, null);
+ }
+
+ /**
* This animation runs for the thumbnail that gets cross faded with the enter/exit activity
* when a thumbnail is specified with the pending animation override.
*/
@@ -1503,6 +1526,10 @@ public class AppTransition implements Dump {
&& mNextAppTransition != TRANSIT_KEYGUARD_GOING_AWAY;
}
+ RemoteAnimationController getRemoteAnimationController() {
+ return mRemoteAnimationController;
+ }
+
/**
*
* @param frame These are the bounds of the window when it finishes the animation. This is where
@@ -1524,7 +1551,7 @@ public class AppTransition implements Dump {
* to the recents thumbnail and hence need to account for the surface being
* bigger.
*/
- Animation loadAnimation(WindowManager.LayoutParams lp, int transit, boolean enter, int uiMode,
+ Animation loadAnimation(LayoutParams lp, int transit, boolean enter, int uiMode,
int orientation, Rect frame, Rect displayFrame, Rect insets,
@Nullable Rect surfaceInsets, @Nullable Rect stableInsets, boolean isVoiceInteraction,
boolean freeform, int taskId) {
@@ -1624,9 +1651,10 @@ public class AppTransition implements Dump {
&& (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);
+ ? com.android.internal.R.anim.task_open_enter_cross_profile_apps
+ : com.android.internal.R.anim.task_open_exit);
Slog.v(TAG,
"applyAnimation NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS:"
+ " anim=" + a + " transit=" + appTransitionToString(transit)
@@ -1739,7 +1767,7 @@ public class AppTransition implements Dump {
void overridePendingAppTransition(String packageName, int enterAnim, int exitAnim,
IRemoteCallback startedCallback) {
- if (isTransitionSet()) {
+ if (canOverridePendingAppTransition()) {
clear();
mNextAppTransitionType = NEXT_TRANSIT_TYPE_CUSTOM;
mNextAppTransitionPackage = packageName;
@@ -1747,14 +1775,12 @@ public class AppTransition implements Dump {
mNextAppTransitionExit = exitAnim;
postAnimationCallback();
mNextAppTransitionCallback = startedCallback;
- } else {
- postAnimationCallback();
}
}
void overridePendingAppTransitionScaleUp(int startX, int startY, int startWidth,
int startHeight) {
- if (isTransitionSet()) {
+ if (canOverridePendingAppTransition()) {
clear();
mNextAppTransitionType = NEXT_TRANSIT_TYPE_SCALE_UP;
putDefaultNextAppTransitionCoordinates(startX, startY, startWidth, startHeight, null);
@@ -1764,7 +1790,7 @@ public class AppTransition implements Dump {
void overridePendingAppTransitionClipReveal(int startX, int startY,
int startWidth, int startHeight) {
- if (isTransitionSet()) {
+ if (canOverridePendingAppTransition()) {
clear();
mNextAppTransitionType = NEXT_TRANSIT_TYPE_CLIP_REVEAL;
putDefaultNextAppTransitionCoordinates(startX, startY, startWidth, startHeight, null);
@@ -1774,7 +1800,7 @@ public class AppTransition implements Dump {
void overridePendingAppTransitionThumb(GraphicBuffer srcThumb, int startX, int startY,
IRemoteCallback startedCallback, boolean scaleUp) {
- if (isTransitionSet()) {
+ if (canOverridePendingAppTransition()) {
clear();
mNextAppTransitionType = scaleUp ? NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_UP
: NEXT_TRANSIT_TYPE_THUMBNAIL_SCALE_DOWN;
@@ -1782,14 +1808,12 @@ public class AppTransition implements Dump {
putDefaultNextAppTransitionCoordinates(startX, startY, 0, 0, srcThumb);
postAnimationCallback();
mNextAppTransitionCallback = startedCallback;
- } else {
- postAnimationCallback();
}
}
void overridePendingAppTransitionAspectScaledThumb(GraphicBuffer srcThumb, int startX, int startY,
int targetWidth, int targetHeight, IRemoteCallback startedCallback, boolean scaleUp) {
- if (isTransitionSet()) {
+ if (canOverridePendingAppTransition()) {
clear();
mNextAppTransitionType = scaleUp ? NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP
: NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN;
@@ -1798,15 +1822,13 @@ public class AppTransition implements Dump {
srcThumb);
postAnimationCallback();
mNextAppTransitionCallback = startedCallback;
- } else {
- postAnimationCallback();
}
}
- public void overridePendingAppTransitionMultiThumb(AppTransitionAnimationSpec[] specs,
+ void overridePendingAppTransitionMultiThumb(AppTransitionAnimationSpec[] specs,
IRemoteCallback onAnimationStartedCallback, IRemoteCallback onAnimationFinishedCallback,
boolean scaleUp) {
- if (isTransitionSet()) {
+ if (canOverridePendingAppTransition()) {
clear();
mNextAppTransitionType = scaleUp ? NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP
: NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN;
@@ -1829,15 +1851,13 @@ public class AppTransition implements Dump {
postAnimationCallback();
mNextAppTransitionCallback = onAnimationStartedCallback;
mAnimationFinishedCallback = onAnimationFinishedCallback;
- } else {
- postAnimationCallback();
}
}
void overridePendingAppTransitionMultiThumbFuture(
IAppTransitionAnimationSpecsFuture specsFuture, IRemoteCallback callback,
boolean scaleUp) {
- if (isTransitionSet()) {
+ if (canOverridePendingAppTransition()) {
clear();
mNextAppTransitionType = scaleUp ? NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP
: NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN;
@@ -1847,14 +1867,21 @@ public class AppTransition implements Dump {
}
}
- void overrideInPlaceAppTransition(String packageName, int anim) {
+ void overridePendingAppTransitionRemote(RemoteAnimationAdapter remoteAnimationAdapter) {
if (isTransitionSet()) {
clear();
+ mNextAppTransitionType = NEXT_TRANSIT_TYPE_REMOTE;
+ mRemoteAnimationController = new RemoteAnimationController(mService,
+ remoteAnimationAdapter, mService.mH);
+ }
+ }
+
+ void overrideInPlaceAppTransition(String packageName, int anim) {
+ if (canOverridePendingAppTransition()) {
+ clear();
mNextAppTransitionType = NEXT_TRANSIT_TYPE_CUSTOM_IN_PLACE;
mNextAppTransitionPackage = packageName;
mNextAppTransitionInPlace = anim;
- } else {
- postAnimationCallback();
}
}
@@ -1862,13 +1889,18 @@ public class AppTransition implements Dump {
* @see {@link #NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS}
*/
void overridePendingAppTransitionStartCrossProfileApps() {
- if (isTransitionSet()) {
+ if (canOverridePendingAppTransition()) {
clear();
mNextAppTransitionType = NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS;
postAnimationCallback();
}
}
+ private boolean canOverridePendingAppTransition() {
+ // Remote animations always take precedence
+ return isTransitionSet() && mNextAppTransitionType != NEXT_TRANSIT_TYPE_REMOTE;
+ }
+
/**
* If a future is set for the app transition specs, fetch it in another thread.
*/
@@ -2007,6 +2039,8 @@ public class AppTransition implements Dump {
return "NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_UP";
case NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN:
return "NEXT_TRANSIT_TYPE_THUMBNAIL_ASPECT_SCALE_DOWN";
+ case NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS:
+ return "NEXT_TRANSIT_TYPE_OPEN_CROSS_PROFILE_APPS";
default:
return "unknown type=" + mNextAppTransitionType;
}
@@ -2089,8 +2123,8 @@ public class AppTransition implements Dump {
* @return true if transition is not running and should not be skipped, false if transition is
* already running
*/
- boolean prepareAppTransitionLocked(int transit, boolean alwaysKeepCurrent, int flags,
- boolean forceOverride) {
+ boolean prepareAppTransitionLocked(@TransitionType int transit, boolean alwaysKeepCurrent,
+ @TransitionFlags int flags, boolean forceOverride) {
if (DEBUG_APP_TRANSITIONS) Slog.v(TAG, "Prepare app transition:"
+ " transit=" + appTransitionToString(transit)
+ " " + this
diff --git a/com/android/server/wm/AppWindowContainerController.java b/com/android/server/wm/AppWindowContainerController.java
index ae9f28bc..e9efd4ec 100644
--- a/com/android/server/wm/AppWindowContainerController.java
+++ b/com/android/server/wm/AppWindowContainerController.java
@@ -19,8 +19,8 @@ package com.android.server.wm;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
-import static com.android.server.wm.AppTransition.TRANSIT_DOCK_TASK_FROM_RECENTS;
-import static com.android.server.wm.AppTransition.TRANSIT_UNSET;
+import static android.view.WindowManager.TRANSIT_DOCK_TASK_FROM_RECENTS;
+import static android.view.WindowManager.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;
@@ -32,7 +32,6 @@ 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.Rect;
import android.os.Debug;
import android.os.Handler;
import android.os.IBinder;
@@ -40,6 +39,8 @@ import android.os.Looper;
import android.os.Message;
import android.util.Slog;
import android.view.IApplicationToken;
+import android.view.RemoteAnimationDefinition;
+import android.view.WindowManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.server.AttributeCache;
@@ -393,7 +394,7 @@ public class AppWindowContainerController
wtoken.mEnteringAnimation = false;
}
if (mService.mAppTransition.getAppTransition()
- == AppTransition.TRANSIT_TASK_OPEN_BEHIND) {
+ == WindowManager.TRANSIT_TASK_OPEN_BEHIND) {
// We're launchingBehind, add the launching activity to mOpeningApps.
final WindowState win =
mService.getDefaultDisplayContentLocked().findFocusedWindow();
@@ -695,6 +696,17 @@ public class AppWindowContainerController
}
}
+ public void registerRemoteAnimations(RemoteAnimationDefinition definition) {
+ synchronized (mWindowMap) {
+ if (mContainer == null) {
+ Slog.w(TAG_WM, "Attempted to register remote animations with non-existing app"
+ + " token: " + mToken);
+ return;
+ }
+ mContainer.registerRemoteAnimations(definition);
+ }
+ }
+
void reportStartingWindowDrawn() {
mHandler.sendMessage(mHandler.obtainMessage(H.NOTIFY_STARTING_WINDOW_DRAWN));
}
diff --git a/com/android/server/wm/AppWindowThumbnail.java b/com/android/server/wm/AppWindowThumbnail.java
index b86cd50d..db956344 100644
--- a/com/android/server/wm/AppWindowThumbnail.java
+++ b/com/android/server/wm/AppWindowThumbnail.java
@@ -20,12 +20,16 @@ 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 static com.android.server.wm.proto.AppWindowThumbnailProto.HEIGHT;
+import static com.android.server.wm.proto.AppWindowThumbnailProto.SURFACE_ANIMATOR;
+import static com.android.server.wm.proto.AppWindowThumbnailProto.WIDTH;
import android.graphics.GraphicBuffer;
import android.graphics.PixelFormat;
import android.graphics.Point;
import android.os.Binder;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import android.view.Surface;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Builder;
@@ -49,7 +53,8 @@ class AppWindowThumbnail implements Animatable {
AppWindowThumbnail(Transaction t, AppWindowToken appToken, GraphicBuffer thumbnailHeader) {
mAppToken = appToken;
- mSurfaceAnimator = new SurfaceAnimator(this, this::onAnimationFinished, appToken.mService);
+ mSurfaceAnimator = new SurfaceAnimator(this, this::onAnimationFinished,
+ appToken.mService.mAnimator::addAfterPrepareSurfacesRunnable, appToken.mService);
mWidth = thumbnailHeader.getWidth();
mHeight = thumbnailHeader.getHeight();
@@ -84,10 +89,14 @@ class AppWindowThumbnail implements Animatable {
}
void startAnimation(Transaction t, Animation anim) {
+ startAnimation(t, anim, null /* position */);
+ }
+
+ void startAnimation(Transaction t, Animation anim, Point position) {
anim.restrictDuration(MAX_ANIMATION_DURATION);
anim.scaleCurrentDuration(mAppToken.mService.getTransitionAnimationScaleLocked());
mSurfaceAnimator.startAnimation(t, new LocalAnimationAdapter(
- new WindowAnimationSpec(anim, null /* position */,
+ new WindowAnimationSpec(anim, position,
mAppToken.mService.mAppTransition.canSkipFirstFrame()),
mAppToken.mService.mSurfaceAnimationRunner), false /* hidden */);
}
@@ -109,6 +118,22 @@ class AppWindowThumbnail implements Animatable {
mSurfaceControl.destroy();
}
+ /**
+ * Write to a protocol buffer output stream. Protocol buffer message definition is at {@link
+ * com.android.server.wm.proto.AppWindowThumbnailProto}.
+ *
+ * @param proto Stream to write the AppWindowThumbnail object to.
+ * @param fieldId Field Id of the AppWindowThumbnail as defined in the parent message.
+ * @hide
+ */
+ void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(WIDTH, mWidth);
+ proto.write(HEIGHT, mHeight);
+ mSurfaceAnimator.writeToProto(proto, SURFACE_ANIMATOR);
+ proto.end(token);
+ }
+
@Override
public Transaction getPendingTransaction() {
return mAppToken.getPendingTransaction();
@@ -139,7 +164,7 @@ class AppWindowThumbnail implements Animatable {
@Override
public Builder makeAnimationLeash() {
- return mAppToken.makeSurface().setParent(mAppToken.getAppAnimationLayer());
+ return mAppToken.makeSurface();
}
@Override
@@ -148,6 +173,11 @@ class AppWindowThumbnail implements Animatable {
}
@Override
+ public SurfaceControl getAnimationLeashParent() {
+ return mAppToken.getAppAnimationLayer();
+ }
+
+ @Override
public SurfaceControl getParentSurfaceControl() {
return mAppToken.getParentSurfaceControl();
}
diff --git a/com/android/server/wm/AppWindowToken.java b/com/android/server/wm/AppWindowToken.java
index 44d7948b..ce3f512d 100644
--- a/com/android/server/wm/AppWindowToken.java
+++ b/com/android/server/wm/AppWindowToken.java
@@ -16,21 +16,25 @@
package com.android.server.wm;
+import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
+import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
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.SurfaceControl.HIDDEN;
import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+
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.AppTransition.TRANSIT_UNSET;
+import static android.view.WindowManager.TRANSIT_UNSET;
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;
@@ -48,7 +52,28 @@ import static com.android.server.wm.WindowManagerService.H.NOTIFY_ACTIVITY_DRAWN
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_NORMAL;
import static com.android.server.wm.WindowManagerService.UPDATE_FOCUS_WILL_PLACE_SURFACES;
import static com.android.server.wm.WindowManagerService.logWithStack;
+import static com.android.server.wm.proto.AppWindowTokenProto.ALL_DRAWN;
+import static com.android.server.wm.proto.AppWindowTokenProto.APP_STOPPED;
+import static com.android.server.wm.proto.AppWindowTokenProto.CLIENT_HIDDEN;
+import static com.android.server.wm.proto.AppWindowTokenProto.DEFER_HIDING_CLIENT;
+import static com.android.server.wm.proto.AppWindowTokenProto.FILLS_PARENT;
+import static com.android.server.wm.proto.AppWindowTokenProto.FROZEN_BOUNDS;
+import static com.android.server.wm.proto.AppWindowTokenProto.HIDDEN_REQUESTED;
+import static com.android.server.wm.proto.AppWindowTokenProto.HIDDEN_SET_FROM_TRANSFERRED_STARTING_WINDOW;
+import static com.android.server.wm.proto.AppWindowTokenProto.IS_REALLY_ANIMATING;
+import static com.android.server.wm.proto.AppWindowTokenProto.IS_WAITING_FOR_TRANSITION_START;
+import static com.android.server.wm.proto.AppWindowTokenProto.LAST_ALL_DRAWN;
+import static com.android.server.wm.proto.AppWindowTokenProto.LAST_SURFACE_SHOWING;
import static com.android.server.wm.proto.AppWindowTokenProto.NAME;
+import static com.android.server.wm.proto.AppWindowTokenProto.NUM_DRAWN_WINDOWS;
+import static com.android.server.wm.proto.AppWindowTokenProto.NUM_INTERESTING_WINDOWS;
+import static com.android.server.wm.proto.AppWindowTokenProto.REMOVED;
+import static com.android.server.wm.proto.AppWindowTokenProto.REPORTED_DRAWN;
+import static com.android.server.wm.proto.AppWindowTokenProto.REPORTED_VISIBLE;
+import static com.android.server.wm.proto.AppWindowTokenProto.STARTING_DISPLAYED;
+import static com.android.server.wm.proto.AppWindowTokenProto.STARTING_MOVED;
+import static com.android.server.wm.proto.AppWindowTokenProto.STARTING_WINDOW;
+import static com.android.server.wm.proto.AppWindowTokenProto.THUMBNAIL;
import static com.android.server.wm.proto.AppWindowTokenProto.WINDOW_TOKEN;
import android.annotation.CallSuper;
@@ -65,13 +90,15 @@ 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.RemoteAnimationDefinition;
import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
+import android.view.animation.Animation;
+import com.android.internal.R;
import com.android.internal.util.ToBooleanFunction;
import com.android.server.input.InputApplicationHandle;
import com.android.server.policy.WindowManagerPolicy.StartingSurface;
@@ -80,7 +107,6 @@ 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> {
}
@@ -220,9 +246,11 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
/** Whether this token should be boosted at the top of all app window tokens. */
private boolean mNeedsZBoost;
+ private Letterbox mLetterbox;
private final Point mTmpPoint = new Point();
private final Rect mTmpRect = new Rect();
+ private RemoteAnimationDefinition mRemoteAnimationDefinition;
AppWindowToken(WindowManagerService service, IApplicationToken token, boolean voiceInteraction,
DisplayContent dc, long inputDispatchingTimeoutNanos, boolean fullscreen,
@@ -351,7 +379,6 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
// Reset the state of mHiddenSetFromTransferredStartingWindow since visibility is actually
// been set by the app now.
mHiddenSetFromTransferredStartingWindow = false;
- setClientHidden(!visible);
// Allow for state changes and animation to be applied if:
// * token is transitioning visibility state
@@ -367,7 +394,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
boolean runningAppAnimation = false;
- if (transit != AppTransition.TRANSIT_UNSET) {
+ if (transit != WindowManager.TRANSIT_UNSET) {
if (applyAnimationLocked(lp, transit, visible, isVoiceInteraction)) {
delayed = runningAppAnimation = true;
}
@@ -436,6 +463,16 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
mService.mActivityManagerAppTransitionNotifier.onAppTransitionFinishedLocked(token);
}
+ // If we're becoming visible, immediately change client visibility as well although it
+ // usually gets changed in AppWindowContainerController.setVisibility already. However,
+ // there seem to be some edge cases where we change our visibility but client visibility
+ // never gets updated.
+ // If we're becoming invisible, update the client visibility if we are not running an
+ // animation. Otherwise, we'll update client visibility in onAnimationFinished.
+ if (visible || !delayed) {
+ setClientHidden(!visible);
+ }
+
// If we are hidden but there is no delay needed we immediately
// apply the Surface transaction so that the ActivityManager
// can have some guarantee on the Surface state following
@@ -484,15 +521,25 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
}
WindowState findMainWindow() {
+ return findMainWindow(true);
+ }
+
+ /**
+ * Finds the main window that either has type base application or application starting if
+ * requested.
+ *
+ * @param includeStartingApp Allow to search application-starting windows to also be returned.
+ * @return The main window of type base application or application starting if requested.
+ */
+ WindowState findMainWindow(boolean includeStartingApp) {
WindowState candidate = null;
- int j = mChildren.size();
- while (j > 0) {
- j--;
+ for (int j = mChildren.size() - 1; j >= 0; --j) {
final WindowState win = mChildren.get(j);
final int type = win.mAttrs.type;
// No need to loop through child window as base application and starting types can't be
// child windows.
- if (type == TYPE_BASE_APPLICATION || type == TYPE_APPLICATION_STARTING) {
+ if (type == TYPE_BASE_APPLICATION
+ || (includeStartingApp && type == TYPE_APPLICATION_STARTING)) {
// In cases where there are multiple windows, we prefer the non-exiting window. This
// happens for example when replacing windows during an activity relaunch. When
// constructing the animation, we want the new window, not the exiting one.
@@ -645,8 +692,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
boolean destroyedSomething = false;
// 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);
+ final ArrayList<WindowState> children = new ArrayList<>(mChildren);
for (int i = children.size() - 1; i >= 0; i--) {
final WindowState win = children.get(i);
destroyedSomething |= win.destroySurface(cleanupOnResume, mAppStopped);
@@ -654,6 +700,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
if (destroyedSomething) {
final DisplayContent dc = getDisplayContent();
dc.assignWindowLayers(true /*setLayoutNeeded*/);
+ updateLetterbox(null);
}
}
@@ -920,6 +967,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
void removeChild(WindowState child) {
super.removeChild(child);
checkKeyguardFlagsChanged();
+ updateLetterbox(child);
}
private boolean waitingForReplacement() {
@@ -1211,6 +1259,30 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
}
@Override
+ public void onConfigurationChanged(Configuration newParentConfig) {
+ final int prevWinMode = getWindowingMode();
+ super.onConfigurationChanged(newParentConfig);
+ final int winMode = getWindowingMode();
+
+ if (prevWinMode == winMode) {
+ return;
+ }
+
+ if (prevWinMode != WINDOWING_MODE_UNDEFINED && winMode == WINDOWING_MODE_PINNED) {
+ // Entering PiP from fullscreen, reset the snap fraction
+ mDisplayContent.mPinnedStackControllerLocked.resetReentrySnapFraction(this);
+ } else if (prevWinMode == WINDOWING_MODE_PINNED && winMode != WINDOWING_MODE_UNDEFINED) {
+ // Leaving PiP to fullscreen, save the snap fraction based on the pre-animation bounds
+ // for the next re-entry into PiP (assuming the activity is not hidden or destroyed)
+ final TaskStack pinnedStack = mDisplayContent.getPinnedStack();
+ if (pinnedStack != null) {
+ mDisplayContent.mPinnedStackControllerLocked.saveReentrySnapFraction(this,
+ pinnedStack.mPreAnimationBounds);
+ }
+ }
+ }
+
+ @Override
void checkAppWindowsReadyToShow() {
if (allDrawn == mLastAllDrawn) {
return;
@@ -1315,8 +1387,11 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
if (mLastTransactionSequence != mService.mTransactionSequence) {
mLastTransactionSequence = mService.mTransactionSequence;
- mNumInterestingWindows = mNumDrawnWindows = 0;
+ mNumDrawnWindows = 0;
startingDisplayed = false;
+
+ // There is the main base application window, even if it is exiting, wait for it
+ mNumInterestingWindows = findMainWindow(false /* includeStartingApp */) != null ? 1 : 0;
}
final WindowStateAnimator winAnimator = w.mWinAnimator;
@@ -1338,7 +1413,10 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
if (w != startingWindow) {
if (w.isInteresting()) {
- mNumInterestingWindows++;
+ // Add non-main window as interesting since the main app has already been added
+ if (findMainWindow(false /* includeStartingApp */) != w) {
+ mNumInterestingWindows++;
+ }
if (w.isDrawnLw()) {
mNumDrawnWindows++;
@@ -1361,6 +1439,33 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
return isInterestingAndDrawn;
}
+ void updateLetterbox(WindowState winHint) {
+ final WindowState w = findMainWindow();
+ if (w != winHint && winHint != null && w != null) {
+ return;
+ }
+ final boolean needsLetterbox = w != null && w.isLetterboxedAppWindow()
+ && fillsParent() && w.hasDrawnLw();
+ if (needsLetterbox) {
+ if (mLetterbox == null) {
+ mLetterbox = new Letterbox(() -> makeChildSurface(null));
+ }
+ mLetterbox.setDimensions(mPendingTransaction, getParent().getBounds(), w.mFrame);
+ } else if (mLetterbox != null) {
+ final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
+ // Make sure we have a transaction here, in case we're called outside of a transaction.
+ // This does not use mPendingTransaction, because SurfaceAnimator uses a
+ // global transaction in onAnimationEnd.
+ SurfaceControl.openTransaction();
+ try {
+ mLetterbox.hide(t);
+ } finally {
+ SurfaceControl.mergeToGlobalTransaction(t);
+ SurfaceControl.closeTransaction();
+ }
+ }
+ }
+
@Override
boolean forAllWindows(ToBooleanFunction<WindowState> callback, boolean traverseTopToBottom) {
// For legacy reasons we process the TaskStack.mExitingAppTokens first in DisplayContent
@@ -1512,9 +1617,8 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
}
@Override
- public SurfaceControl.Builder makeAnimationLeash() {
- return super.makeAnimationLeash()
- .setParent(getAppAnimationLayer());
+ public SurfaceControl getAnimationLeashParent() {
+ return getAppAnimationLayer();
}
boolean applyAnimationLocked(WindowManager.LayoutParams lp, int transit, boolean enter,
@@ -1533,26 +1637,37 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
// 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;
+ final AnimationAdapter adapter;
+ final TaskStack stack = getStack();
+ mTmpPoint.set(0, 0);
+ mTmpRect.setEmpty();
+ if (stack != null) {
+ stack.getRelativePosition(mTmpPoint);
+ stack.getBounds(mTmpRect);
+ mTmpRect.offsetTo(0, 0);
+ }
+ if (mService.mAppTransition.getRemoteAnimationController() != null) {
+ adapter = mService.mAppTransition.getRemoteAnimationController()
+ .createAnimationAdapter(this, mTmpPoint, mTmpRect);
+ } else {
+ final Animation a = loadAnimation(lp, transit, enter, isVoiceInteraction);
+ if (a != null) {
+ adapter = new LocalAnimationAdapter(
+ new WindowAnimationSpec(a, mTmpPoint, mTmpRect,
+ mService.mAppTransition.canSkipFirstFrame(),
+ mService.mAppTransition.getAppStackClipMode()),
+ mService.mSurfaceAnimationRunner);
+ if (a.getZAdjustment() == Animation.ZORDER_TOP) {
+ mNeedsZBoost = true;
+ }
+ mTransit = transit;
+ mTransitFlags = mService.mAppTransition.getTransitFlags();
+ } else {
+ adapter = null;
}
+ }
+ if (adapter != null) {
startAnimation(getPendingTransaction(), adapter, !isVisible());
- mTransit = transit;
- mTransitFlags = mService.mAppTransition.getTransitFlags();
}
} else {
cancelAnimation();
@@ -1587,6 +1702,8 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
// the status bar). In that case we need to use the final frame.
if (freeform) {
frame.set(win.mFrame);
+ } else if (win.isLetterboxedAppWindow()) {
+ frame.set(getTask().getBounds());
} else {
frame.set(win.mContainingFrame);
}
@@ -1673,6 +1790,7 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
"AppWindowToken");
clearThumbnail();
+ setClientHidden(isHidden());
if (mService.mInputMethodTarget != null && mService.mInputMethodTarget.mAppToken == this) {
getDisplayContent().computeImeTarget(true /* updateImeTarget */);
@@ -1748,6 +1866,37 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
mThumbnail.startAnimation(getPendingTransaction(), loadThumbnailAnimation(thumbnailHeader));
}
+ /**
+ * Attaches a surface with a thumbnail for the
+ * {@link android.app.ActivityOptions#ANIM_OPEN_CROSS_PROFILE_APPS} animation.
+ */
+ void attachCrossProfileAppsThumbnailAnimation() {
+ if (!isReallyAnimating()) {
+ return;
+ }
+ clearThumbnail();
+
+ final WindowState win = findMainWindow();
+ if (win == null) {
+ return;
+ }
+ final Rect frame = win.mFrame;
+ final int thumbnailDrawableRes = getTask().mUserId == mService.mCurrentUserId
+ ? R.drawable.ic_account_circle
+ : R.drawable.ic_corp_badge_no_background;
+ final GraphicBuffer thumbnail =
+ mService.mAppTransition
+ .createCrossProfileAppsThumbnail(thumbnailDrawableRes, frame);
+ if (thumbnail == null) {
+ return;
+ }
+ mThumbnail = new AppWindowThumbnail(getPendingTransaction(), this, thumbnail);
+ final Animation animation =
+ mService.mAppTransition.createCrossProfileAppsThumbnailAnimationLocked(win.mFrame);
+ mThumbnail.startAnimation(getPendingTransaction(), animation, new Point(frame.left,
+ frame.top));
+ }
+
private Animation loadThumbnailAnimation(GraphicBuffer thumbnailHeader) {
final DisplayInfo displayInfo = mDisplayContent.getDisplayInfo();
@@ -1772,6 +1921,14 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
mThumbnail = null;
}
+ void registerRemoteAnimations(RemoteAnimationDefinition definition) {
+ mRemoteAnimationDefinition = definition;
+ }
+
+ RemoteAnimationDefinition getRemoteAnimationDefinition() {
+ return mRemoteAnimationDefinition;
+ }
+
@Override
void dump(PrintWriter pw, String prefix, boolean dumpAll) {
super.dump(pw, prefix, dumpAll);
@@ -1837,6 +1994,11 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
@Override
void setHidden(boolean hidden) {
super.setHidden(hidden);
+
+ if (hidden) {
+ // Once the app window is hidden, reset the last saved PiP snap fraction
+ mDisplayContent.mPinnedStackControllerLocked.resetReentrySnapFraction(this);
+ }
scheduleAnimation();
}
@@ -1873,6 +2035,34 @@ class AppWindowToken extends WindowToken implements WindowManagerService.AppFree
final long token = proto.start(fieldId);
writeNameToProto(proto, NAME);
super.writeToProto(proto, WINDOW_TOKEN, trim);
+ proto.write(LAST_SURFACE_SHOWING, mLastSurfaceShowing);
+ proto.write(IS_WAITING_FOR_TRANSITION_START, isWaitingForTransitionStart());
+ proto.write(IS_REALLY_ANIMATING, isReallyAnimating());
+ if (mThumbnail != null){
+ mThumbnail.writeToProto(proto, THUMBNAIL);
+ }
+ proto.write(FILLS_PARENT, mFillsParent);
+ proto.write(APP_STOPPED, mAppStopped);
+ proto.write(HIDDEN_REQUESTED, hiddenRequested);
+ proto.write(CLIENT_HIDDEN, mClientHidden);
+ proto.write(DEFER_HIDING_CLIENT, mDeferHidingClient);
+ proto.write(REPORTED_DRAWN, reportedDrawn);
+ proto.write(REPORTED_VISIBLE, reportedVisible);
+ proto.write(NUM_INTERESTING_WINDOWS, mNumInterestingWindows);
+ proto.write(NUM_DRAWN_WINDOWS, mNumDrawnWindows);
+ proto.write(ALL_DRAWN, allDrawn);
+ proto.write(LAST_ALL_DRAWN, mLastAllDrawn);
+ proto.write(REMOVED, removed);
+ if (startingWindow != null){
+ startingWindow.writeIdentifierToProto(proto, STARTING_WINDOW);
+ }
+ proto.write(STARTING_DISPLAYED, startingDisplayed);
+ proto.write(STARTING_MOVED, startingMoved);
+ proto.write(HIDDEN_SET_FROM_TRANSFERRED_STARTING_WINDOW,
+ mHiddenSetFromTransferredStartingWindow);
+ for (Rect bounds : mFrozenBounds) {
+ bounds.writeToProto(proto, FROZEN_BOUNDS);
+ }
proto.end(token);
}
diff --git a/com/android/server/wm/DisplayContent.java b/com/android/server/wm/DisplayContent.java
index d053015c..3f49f0cd 100644
--- a/com/android/server/wm/DisplayContent.java
+++ b/com/android/server/wm/DisplayContent.java
@@ -49,6 +49,7 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_KEYGUARD;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
import static android.view.WindowManager.LayoutParams.TYPE_BOOT_PROGRESS;
+import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER;
import static android.view.WindowManager.LayoutParams.TYPE_DRAWN_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_DREAM;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
@@ -62,7 +63,8 @@ import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_A
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_CONFIG;
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_KEYGUARD_UNOCCLUDE;
+import static com.android.server.wm.utils.CoordinateTransforms.transformPhysicalToLogicalCoordinates;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ADD_REMOVE;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_BOOT;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DISPLAY;
@@ -121,6 +123,7 @@ import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.Matrix;
+import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
@@ -132,11 +135,13 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
import android.os.Trace;
+import android.util.ArraySet;
import android.util.DisplayMetrics;
import android.util.MutableBoolean;
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
import android.view.Display;
+import android.view.DisplayCutout;
import android.view.DisplayInfo;
import android.view.InputDevice;
import android.view.MagnificationSpec;
@@ -158,6 +163,7 @@ import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
+import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
@@ -187,9 +193,10 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
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", mService);
+ // window containers together and move them in-sync if/when needed. We use a subclass of
+ // WindowContainer which is omitted from screen magnification, as the IME is never magnified.
+ private final NonMagnifiableWindowContainers mImeWindowsContainers =
+ new NonMagnifiableWindowContainers("mImeWindowsContainers", mService);
private WindowState mTmpWindow;
private WindowState mTmpWindow2;
@@ -207,6 +214,9 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
int mInitialDisplayHeight = 0;
int mInitialDisplayDensity = 0;
+ DisplayCutout mInitialDisplayCutout;
+ DisplayCutout mDisplayCutoutOverride;
+
/**
* Overridden display size. Initialized with {@link #mInitialDisplayWidth}
* and {@link #mInitialDisplayHeight}, but can be set via shell command "adb shell wm size".
@@ -322,6 +332,8 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
final PinnedStackController mPinnedStackControllerLocked;
final ArrayList<WindowState> mTapExcludedWindows = new ArrayList<>();
+ /** A collection of windows that provide tap exclude regions inside of them. */
+ final ArraySet<WindowState> mTapExcludeProvidingWindows = new ArraySet<>();
private boolean mHaveBootMsg = false;
private boolean mHaveApp = false;
@@ -334,9 +346,6 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
new TaskForResizePointSearchResult();
private final ApplySurfaceChangesTransactionState mTmpApplySurfaceChangesTransactionState =
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.
@@ -653,10 +662,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
mWallpaperController.updateWallpaperVisibility();
}
- // 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);
+ w.handleWindowMovedIfNeeded(mPendingTransaction);
final WindowStateAnimator winAnimator = w.mWinAnimator;
@@ -691,37 +697,11 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
}
}
}
- final TaskStack stack = w.getStack();
- 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
- // size) of the window. setSurfaceBoundariesLocked uses mShownPosition to
- // position the surface.
- //
- // If an animation is being started, we can't call this method because the
- // animation hasn't processed its initial transformation yet, but in general
- // we do want to update the position if the window is animating. We make an exception
- // for the bounds animating state, where an application may have been waiting
- // for an exit animation to start, but instead enters PiP. We need to ensure
- // we always recompute the top-left in this case.
- 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;
if (atoken != null) {
+ atoken.updateLetterbox(w);
final boolean updateAllDrawn = atoken.updateDrawnWindowStates(w);
if (updateAllDrawn && !mTmpUpdateAllDrawn.contains(atoken)) {
mTmpUpdateAllDrawn.add(atoken);
@@ -1192,6 +1172,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
mDisplayInfo.getLogicalMetrics(mRealDisplayMetrics,
CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO, null);
}
+ mDisplayInfo.displayCutout = calculateDisplayCutoutForCurrentRotation();
mDisplayInfo.getAppMetrics(mDisplayMetrics);
if (mDisplayScalingDisabled) {
mDisplayInfo.flags |= Display.FLAG_SCALING_DISABLED;
@@ -1213,6 +1194,18 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
return mDisplayInfo;
}
+ DisplayCutout calculateDisplayCutoutForCurrentRotation() {
+ final DisplayCutout cutout = mInitialDisplayCutout;
+ if (cutout == null || cutout == DisplayCutout.NO_CUTOUT || mRotation == ROTATION_0) {
+ return cutout;
+ }
+ final Path bounds = cutout.getBounds().getBoundaryPath();
+ transformPhysicalToLogicalCoordinates(mRotation, mInitialDisplayWidth,
+ mInitialDisplayHeight, mTmpMatrix);
+ bounds.transform(mTmpMatrix);
+ return DisplayCutout.fromBounds(bounds);
+ }
+
/**
* Compute display configuration based on display properties and policy settings.
* Do not call if mDisplayReady == false.
@@ -1515,6 +1508,10 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
return mTaskStackContainers.getTopStack();
}
+ ArrayList<Task> getVisibleTasks() {
+ return mTaskStackContainers.getVisibleTasks();
+ }
+
void onStackWindowingModeChanged(TaskStack stack) {
mTaskStackContainers.onStackWindowingModeChanged(stack);
}
@@ -1675,6 +1672,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
mInitialDisplayWidth = mDisplayInfo.logicalWidth;
mInitialDisplayHeight = mDisplayInfo.logicalHeight;
mInitialDisplayDensity = mDisplayInfo.logicalDensityDpi;
+ mInitialDisplayCutout = mDisplayInfo.displayCutout;
}
/**
@@ -1689,10 +1687,12 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
final int newWidth = rotated ? mDisplayInfo.logicalHeight : mDisplayInfo.logicalWidth;
final int newHeight = rotated ? mDisplayInfo.logicalWidth : mDisplayInfo.logicalHeight;
final int newDensity = mDisplayInfo.logicalDensityDpi;
+ final DisplayCutout newCutout = mDisplayInfo.displayCutout;
final boolean displayMetricsChanged = mInitialDisplayWidth != newWidth
|| mInitialDisplayHeight != newHeight
- || mInitialDisplayDensity != mDisplayInfo.logicalDensityDpi;
+ || mInitialDisplayDensity != mDisplayInfo.logicalDensityDpi
+ || !Objects.equals(mInitialDisplayCutout, newCutout);
if (displayMetricsChanged) {
// Check if display size or density is forced.
@@ -1709,6 +1709,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
mInitialDisplayWidth = newWidth;
mInitialDisplayHeight = newHeight;
mInitialDisplayDensity = newDensity;
+ mInitialDisplayCutout = newCutout;
mService.reconfigureDisplayLocked(this);
}
}
@@ -1805,6 +1806,11 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
getParent().positionChildAt(position, this, includingParents);
}
+ void positionStackAt(int position, TaskStack child) {
+ mTaskStackContainers.positionChildAt(position, child, false /* includingParents */);
+ layoutAndAssignWindowLayersIfNeeded();
+ }
+
int taskIdFromPoint(int x, int y) {
for (int stackNdx = mTaskStackContainers.getChildCount() - 1; stackNdx >= 0; --stackNdx) {
final TaskStack stack = mTaskStackContainers.getChildAt(stackNdx);
@@ -1873,10 +1879,14 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
}
}
for (int i = mTapExcludedWindows.size() - 1; i >= 0; i--) {
- WindowState win = mTapExcludedWindows.get(i);
+ final WindowState win = mTapExcludedWindows.get(i);
win.getTouchableRegion(mTmpRegion);
mTouchExcludeRegion.op(mTmpRegion, Region.Op.UNION);
}
+ for (int i = mTapExcludeProvidingWindows.size() - 1; i >= 0; i--) {
+ final WindowState win = mTapExcludeProvidingWindows.valueAt(i);
+ win.amendTapExcludeRegion(mTouchExcludeRegion);
+ }
// TODO(multi-display): Support docked stacks on secondary displays.
if (mDisplayId == DEFAULT_DISPLAY && getSplitScreenPrimaryStack() != null) {
mDividerControllerLocked.getTouchRegion(mTmpRect);
@@ -1923,6 +1933,8 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
mService.unregisterPointerEventListener(mService.mMousePositionTracker);
}
}
+ mService.mAnimator.removeDisplayLocked(mDisplayId);
+
// The pending transaction won't be applied so we should
// just clean up any surfaces pending destruction.
onPendingTransactionApplied();
@@ -2818,6 +2830,7 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
mTmpRecoveringMemory = recoveringMemory;
forAllWindows(mApplySurfaceChangesTransaction, true /* traverseTopToBottom */);
+ prepareSurfaces();
mService.mDisplayManagerInternal.setDisplayProperties(mDisplayId,
mTmpApplySurfaceChangesTransactionState.displayHasContent,
@@ -3177,6 +3190,21 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
*/
SurfaceControl mAppAnimationLayer = null;
+ /**
+ * Given that the split-screen divider does not have an AppWindowToken, it
+ * will have to live inside of a "NonAppWindowContainer", in particular
+ * {@link DisplayContent#mAboveAppWindowsContainers}. However, in visual Z order
+ * it will need to be interleaved with some of our children, appearing on top of
+ * both docked stacks but underneath any assistant stacks.
+ *
+ * To solve this problem we have this anchor control, which will always exist so
+ * we can always assign it the correct value in our {@link #assignChildLayers}.
+ * Likewise since it always exists, {@link AboveAppWindowContainers} can always
+ * assign the divider a layer relative to it. This way we prevent linking lifecycle
+ * events between the two containers.
+ */
+ SurfaceControl mSplitScreenDividerAnchor = 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.
private TaskStack mHomeStack = null;
@@ -3236,6 +3264,16 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
return mSplitScreenPrimaryStack;
}
+ ArrayList<Task> getVisibleTasks() {
+ final ArrayList<Task> visibleTasks = new ArrayList<>();
+ forAllTasks(task -> {
+ if (task.isVisible()) {
+ visibleTasks.add(task);
+ }
+ });
+ return visibleTasks;
+ }
+
/**
* Adds the stack to this container.
* @see DisplayContent#createStack(int, boolean, StackWindowController)
@@ -3493,37 +3531,39 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
@Override
void assignChildLayers(SurfaceControl.Transaction t) {
- 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.
- 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++);
+ final int HOME_STACK_STATE = 0;
+ final int NORMAL_STACK_STATE = 1;
+ final int ALWAYS_ON_TOP_STATE = 2;
+
+ 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);
+ if (state == HOME_STACK_STATE && s.isActivityTypeHome()) {
+ s.assignLayer(t, layer++);
+ } else if (state == NORMAL_STACK_STATE && !s.isActivityTypeHome()
+ && !s.isAlwaysOnTop()) {
+ s.assignLayer(t, layer++);
+ if (s.inSplitScreenWindowingMode() && mSplitScreenDividerAnchor != null) {
+ t.setLayer(mSplitScreenDividerAnchor, layer++);
+ }
+ } else if (state == ALWAYS_ON_TOP_STATE && s.isAlwaysOnTop()) {
+ s.assignLayer(t, layer++);
+ }
}
- }
- for (int i = 0; i < mChildren.size(); i++) {
- final TaskStack s = mChildren.get(i);
- 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 == NORMAL_STACK_STATE && mAppAnimationLayer != null) {
+ t.setLayer(mAppAnimationLayer, layer++);
}
}
for (int i = 0; i < mChildren.size(); i++) {
final TaskStack s = mChildren.get(i);
- if (s.isAlwaysOnTop()) {
- s.assignLayer(t, layer++);
- }
+ s.assignChildLayers(t);
}
- // 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
@@ -3531,6 +3571,10 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
return mAppAnimationLayer;
}
+ SurfaceControl getSplitScreenDividerAnchor() {
+ return mSplitScreenDividerAnchor;
+ }
+
@Override
void onParentSet() {
super.onParentSet();
@@ -3538,11 +3582,18 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
mAppAnimationLayer = makeChildSurface(null)
.setName("animationLayer")
.build();
- getPendingTransaction().show(mAppAnimationLayer);
+ mSplitScreenDividerAnchor = makeChildSurface(null)
+ .setName("splitScreenDividerAnchor")
+ .build();
+ getPendingTransaction()
+ .show(mAppAnimationLayer)
+ .show(mSplitScreenDividerAnchor);
scheduleAnimation();
} else {
mAppAnimationLayer.destroy();
mAppAnimationLayer = null;
+ mSplitScreenDividerAnchor.destroy();
+ mSplitScreenDividerAnchor = null;
}
}
}
@@ -3557,6 +3608,12 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
&& imeContainer.getSurfaceControl() != null;
for (int j = 0; j < mChildren.size(); ++j) {
final WindowToken wt = mChildren.get(j);
+
+ // See {@link mSplitScreenDividerAnchor}
+ if (wt.windowType == TYPE_DOCK_DIVIDER) {
+ wt.assignRelativeLayer(t, mTaskStackContainers.getSplitScreenDividerAnchor(), 1);
+ continue;
+ }
wt.assignLayer(t, j);
wt.assignChildLayers(t);
@@ -3649,6 +3706,16 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
}
}
+ private class NonMagnifiableWindowContainers extends NonAppWindowContainers {
+ NonMagnifiableWindowContainers(String name, WindowManagerService service) {
+ super(name, service);
+ }
+
+ @Override
+ void applyMagnificationSpec(Transaction t, MagnificationSpec spec) {
+ }
+ };
+
SurfaceControl.Builder makeSurface(SurfaceSession s) {
return mService.makeSurfaceBuilder(s)
.setParent(mWindowingLayer);
@@ -3684,6 +3751,13 @@ class DisplayContent extends WindowContainer<DisplayContent.DisplayChildWindowCo
.setParent(mOverlayLayer);
}
+ /**
+ * Reparents the given surface to mOverlayLayer.
+ */
+ void reparentToOverlay(Transaction transaction, SurfaceControl surface) {
+ transaction.reparent(surface, mOverlayLayer.getHandle());
+ }
+
void applyMagnificationSpec(MagnificationSpec spec) {
applyMagnificationSpec(getPendingTransaction(), spec);
getPendingTransaction().apply();
diff --git a/com/android/server/wm/DisplayFrames.java b/com/android/server/wm/DisplayFrames.java
index 01557125..13d0c869 100644
--- a/com/android/server/wm/DisplayFrames.java
+++ b/com/android/server/wm/DisplayFrames.java
@@ -22,6 +22,7 @@ import static android.view.Surface.ROTATION_90;
import static com.android.server.wm.proto.DisplayFramesProto.STABLE_BOUNDS;
import android.annotation.NonNull;
+import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.proto.ProtoOutputStream;
@@ -100,9 +101,12 @@ 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 */
+ /** The display cutout used for layout (after rotation) */
@NonNull public DisplayCutout mDisplayCutout = DisplayCutout.NO_CUTOUT;
+ /** The cutout as supplied by display info */
+ @NonNull private DisplayCutout mDisplayInfoCutout = DisplayCutout.NO_CUTOUT;
+
/**
* During layout, the frame that is display-cutout safe, i.e. that does not intersect with it.
*/
@@ -126,9 +130,11 @@ public class DisplayFrames {
mRotation = info.rotation;
mDisplayInfoOverscan.set(
info.overscanLeft, info.overscanTop, info.overscanRight, info.overscanBottom);
+ mDisplayInfoCutout = info.displayCutout != null
+ ? info.displayCutout : DisplayCutout.NO_CUTOUT;
}
- public void onBeginLayout(boolean emulateDisplayCutout, int statusBarHeight) {
+ public void onBeginLayout() {
switch (mRotation) {
case ROTATION_90:
mRotatedDisplayInfoOverscan.left = mDisplayInfoOverscan.top;
@@ -166,11 +172,24 @@ public class DisplayFrames {
mStable.set(mUnrestricted);
mStableFullscreen.set(mUnrestricted);
mCurrent.set(mUnrestricted);
- mDisplayCutout = DisplayCutout.NO_CUTOUT;
+
+ mDisplayCutout = mDisplayInfoCutout.calculateRelativeTo(mOverscan);
mDisplayCutoutSafe.set(Integer.MIN_VALUE, Integer.MIN_VALUE,
Integer.MAX_VALUE, Integer.MAX_VALUE);
- if (emulateDisplayCutout) {
- setEmulatedDisplayCutout((int) (statusBarHeight * 0.8));
+ if (!mDisplayCutout.isEmpty()) {
+ final DisplayCutout c = mDisplayCutout;
+ if (c.getSafeInsetLeft() > 0) {
+ mDisplayCutoutSafe.left = mRestrictedOverscan.left + c.getSafeInsetLeft();
+ }
+ if (c.getSafeInsetTop() > 0) {
+ mDisplayCutoutSafe.top = mRestrictedOverscan.top + c.getSafeInsetTop();
+ }
+ if (c.getSafeInsetRight() > 0) {
+ mDisplayCutoutSafe.right = mRestrictedOverscan.right - c.getSafeInsetRight();
+ }
+ if (c.getSafeInsetBottom() > 0) {
+ mDisplayCutoutSafe.bottom = mRestrictedOverscan.bottom - c.getSafeInsetBottom();
+ }
}
}
@@ -178,55 +197,6 @@ public class DisplayFrames {
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);
diff --git a/com/android/server/wm/DisplayWindowController.java b/com/android/server/wm/DisplayWindowController.java
new file mode 100644
index 00000000..ad4957e4
--- /dev/null
+++ b/com/android/server/wm/DisplayWindowController.java
@@ -0,0 +1,85 @@
+/*
+ * 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 com.android.server.wm;
+
+import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STACK;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import android.content.res.Configuration;
+import android.util.Slog;
+
+/**
+ * Controller for the display container. This is created by activity manager to link activity
+ * displays to the display content they use in window manager.
+ */
+public class DisplayWindowController
+ extends WindowContainerController<DisplayContent, WindowContainerListener> {
+
+ private final int mDisplayId;
+
+ public DisplayWindowController(int displayId, WindowContainerListener listener) {
+ super(listener, WindowManagerService.getInstance());
+ mDisplayId = displayId;
+
+ synchronized (mWindowMap) {
+ // TODO: Convert to setContainer() from DisplayContent once everything is hooked up.
+ // Currently we are not setup to register for config changes.
+ mContainer = mRoot.getDisplayContentOrCreate(displayId);
+ if (mContainer == null) {
+ throw new IllegalArgumentException("Trying to add displayId=" + displayId);
+ }
+ }
+ }
+
+ @Override
+ public void removeContainer() {
+ // TODO: Pipe through from ActivityDisplay to remove the display
+ throw new UnsupportedOperationException("To be implemented");
+ }
+
+ @Override
+ public void onOverrideConfigurationChanged(Configuration overrideConfiguration) {
+ // TODO: Pipe through from ActivityDisplay to update the configuration for the display
+ throw new UnsupportedOperationException("To be implemented");
+ }
+
+ /**
+ * Positions the task stack at the given position in the task stack container.
+ */
+ public void positionChildAt(StackWindowController child, int position) {
+ synchronized (mWindowMap) {
+ if (DEBUG_STACK) Slog.i(TAG_WM, "positionTaskStackAt: positioning stack=" + child
+ + " at " + position);
+ if (mContainer == null) {
+ if (DEBUG_STACK) Slog.i(TAG_WM,
+ "positionTaskStackAt: could not find display=" + mContainer);
+ return;
+ }
+ if (child.mContainer == null) {
+ if (DEBUG_STACK) Slog.i(TAG_WM,
+ "positionTaskStackAt: could not find stack=" + this);
+ return;
+ }
+ mContainer.positionStackAt(position, child.mContainer);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "{DisplayWindowController displayId=" + mDisplayId + "}";
+ }
+}
diff --git a/com/android/server/wm/DockedStackDividerController.java b/com/android/server/wm/DockedStackDividerController.java
index 03c0768c..7ae1f24b 100644
--- a/com/android/server/wm/DockedStackDividerController.java
+++ b/com/android/server/wm/DockedStackDividerController.java
@@ -29,7 +29,7 @@ import static android.view.WindowManager.DOCKED_RIGHT;
import static android.view.WindowManager.DOCKED_TOP;
import static com.android.server.wm.AppTransition.DEFAULT_APP_TRANSITION_DURATION;
import static com.android.server.wm.AppTransition.TOUCH_RESPONSE_INTERPOLATOR;
-import static com.android.server.wm.AppTransition.TRANSIT_NONE;
+import static android.view.WindowManager.TRANSIT_NONE;
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.NOTIFY_DOCKED_STACK_MINIMIZED_CHANGED;
@@ -454,8 +454,11 @@ public class DockedStackDividerController {
inputMethodManagerInternal.hideCurrentInputMethod();
mImeHideRequested = true;
}
+
+ // If a primary stack was just created, it will not have access to display content at
+ // this point so pass it from here to get a valid dock side.
final TaskStack stack = mDisplayContent.getSplitScreenPrimaryStackIgnoringVisibility();
- mOriginalDockedSide = stack.getDockSide();
+ mOriginalDockedSide = stack.getDockSideForDisplay(mDisplayContent);
return;
}
mOriginalDockedSide = DOCKED_INVALID;
@@ -604,7 +607,7 @@ public class DockedStackDividerController {
if (wasMinimized && mMinimizedDock && containsAppInDockedStack(openingApps)
&& appTransition != TRANSIT_NONE &&
!AppTransition.isKeyguardGoingAwayTransit(appTransition)) {
- mService.showRecentApps(true /* fromHome */);
+ mService.showRecentApps();
}
}
diff --git a/com/android/server/wm/DragDropController.java b/com/android/server/wm/DragDropController.java
index 28b4c1db..d55a6492 100644
--- a/com/android/server/wm/DragDropController.java
+++ b/com/android/server/wm/DragDropController.java
@@ -18,12 +18,10 @@ package com.android.server.wm;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_DRAG;
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;
import android.annotation.NonNull;
import android.content.ClipData;
-import android.graphics.PixelFormat;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
@@ -32,8 +30,8 @@ import android.os.Message;
import android.util.Slog;
import android.view.Display;
import android.view.IWindow;
-import android.view.Surface;
import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
import android.view.SurfaceSession;
import android.view.View;
@@ -50,10 +48,9 @@ class DragDropController {
private static final long DRAG_TIMEOUT_MS = 5000;
// Messages for Handler.
- private static final int MSG_DRAG_START_TIMEOUT = 0;
- static final int MSG_DRAG_END_TIMEOUT = 1;
- static final int MSG_TEAR_DOWN_DRAG_AND_DROP_INPUT = 2;
- static final int MSG_ANIMATION_END = 3;
+ static final int MSG_DRAG_END_TIMEOUT = 0;
+ static final int MSG_TEAR_DOWN_DRAG_AND_DROP_INPUT = 1;
+ static final int MSG_ANIMATION_END = 2;
/**
* Drag state per operation.
@@ -95,87 +92,35 @@ class DragDropController {
mDragState.sendDragStartedIfNeededLocked(window);
}
- IBinder prepareDrag(SurfaceSession session, int callerPid,
- int callerUid, IWindow window, int flags, int width, int height, Surface outSurface) {
+ IBinder performDrag(SurfaceSession session, int callerPid, int callerUid, IWindow window,
+ int flags, SurfaceControl surface, int touchSource, float touchX, float touchY,
+ float thumbCenterX, float thumbCenterY, ClipData data) {
if (DEBUG_DRAG) {
- Slog.d(TAG_WM, "prepare drag surface: w=" + width + " h=" + height
- + " flags=" + Integer.toHexString(flags) + " win=" + window
- + " asbinder=" + window.asBinder());
- }
-
- if (width <= 0 || height <= 0) {
- Slog.w(TAG_WM, "width and height of drag shadow must be positive");
- return null;
- }
-
- 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;
- }
- }
-
- boolean performDrag(IWindow window, IBinder dragToken,
- int touchSource, float touchX, float touchY, float thumbCenterX, float thumbCenterY,
- ClipData data) {
- if (DEBUG_DRAG) {
- Slog.d(TAG_WM, "perform drag: win=" + window + " data=" + data);
+ Slog.d(TAG_WM, "perform drag: win=" + window + " surface=" + surface + " flags=" +
+ Integer.toHexString(flags) + " data=" + data);
}
+ final IBinder dragToken = new Binder();
final boolean callbackResult = mCallback.get().prePerformDrag(window, dragToken,
touchSource, touchX, touchY, thumbCenterX, thumbCenterY, data);
try {
synchronized (mService.mWindowMap) {
- mHandler.removeMessages(MSG_DRAG_START_TIMEOUT, window.asBinder());
try {
if (!callbackResult) {
- return false;
+ Slog.w(TAG_WM, "IDragDropCallback rejects the performDrag request");
+ return null;
}
- Preconditions.checkState(
- mDragState != null, "performDrag() without prepareDrag()");
- Preconditions.checkState(
- mDragState.mToken == dragToken,
- "performDrag() does not match prepareDrag()");
+ if (dragDropActiveLocked()) {
+ Slog.w(TAG_WM, "Drag already in progress");
+ return null;
+ }
final WindowState callingWin = mService.windowForClientLocked(
null, window, false);
if (callingWin == null) {
Slog.w(TAG_WM, "Bad requesting window " + window);
- return false; // !!! TODO: throw here?
+ return null; // !!! TODO: throw here?
}
// !!! TODO: if input is not still focused on the initiating window, fail
@@ -188,18 +133,31 @@ class DragDropController {
// !!! FIXME: put all this heavy stuff onto the mHandler looper, as well as
// the actual drag event dispatch stuff in the dragstate
+ // !!! TODO(multi-display): support other displays
+
final DisplayContent displayContent = callingWin.getDisplayContent();
if (displayContent == null) {
Slog.w(TAG_WM, "display content is null");
- return false;
+ return null;
}
+ final float alpha = (flags & View.DRAG_FLAG_OPAQUE) == 0 ?
+ DRAG_SHADOW_ALPHA_TRANSPARENT : 1;
+ final IBinder winBinder = window.asBinder();
+ IBinder token = new Binder();
+ mDragState = new DragState(mService, this, token, surface, flags, winBinder);
+ surface = null;
+ mDragState.mPid = callerPid;
+ mDragState.mUid = callerUid;
+ mDragState.mOriginalAlpha = alpha;
+ mDragState.mToken = dragToken;
+
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;
+ return null;
}
mDragState.mDisplayContent = displayContent;
@@ -213,28 +171,31 @@ class DragDropController {
// 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");
- }
+
+ final SurfaceControl.Transaction transaction =
+ callingWin.getPendingTransaction();
+ transaction.setAlpha(surfaceControl, mDragState.mOriginalAlpha);
+ transaction.setPosition(
+ surfaceControl, touchX - thumbCenterX, touchY - thumbCenterY);
+ transaction.show(surfaceControl);
+ displayContent.reparentToOverlay(transaction, surfaceControl);
+ callingWin.scheduleAnimation();
+
+ if (SHOW_LIGHT_TRANSACTIONS) {
+ Slog.i(TAG_WM, "<<< CLOSE TRANSACTION performDrag");
}
mDragState.notifyLocationLocked(touchX, touchY);
} finally {
+ if (surface != null) {
+ surface.release();
+ }
if (mDragState != null && !mDragState.isInProgress()) {
mDragState.closeLocked();
}
}
}
- return true; // success!
+ return dragToken; // success!
} finally {
mCallback.get().postPerformDrag();
}
@@ -385,21 +346,6 @@ class DragDropController {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
- case MSG_DRAG_START_TIMEOUT: {
- IBinder win = (IBinder) msg.obj;
- if (DEBUG_DRAG) {
- Slog.w(TAG_WM, "Timeout starting drag by win " + win);
- }
-
- synchronized (mService.mWindowMap) {
- // !!! TODO: ANR the app that has failed to start the drag in time
- if (mDragState != null) {
- mDragState.closeLocked();
- }
- }
- break;
- }
-
case MSG_DRAG_END_TIMEOUT: {
final IBinder win = (IBinder) msg.obj;
if (DEBUG_DRAG) {
diff --git a/com/android/server/wm/DragState.java b/com/android/server/wm/DragState.java
index b9f437af..1ac9b887 100644
--- a/com/android/server/wm/DragState.java
+++ b/com/android/server/wm/DragState.java
@@ -42,6 +42,7 @@ import android.os.ServiceManager;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.IUserManager;
+import android.os.UserManagerInternal;
import android.util.Slog;
import android.view.Display;
import android.view.DragEvent;
@@ -55,6 +56,7 @@ import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import com.android.internal.view.IDragAndDropPermissions;
+import com.android.server.LocalServices;
import com.android.server.input.InputApplicationHandle;
import com.android.server.input.InputWindowHandle;
@@ -116,10 +118,10 @@ class DragState {
private final Interpolator mCubicEaseOutInterpolator = new DecelerateInterpolator(1.5f);
private Point mDisplaySize = new Point();
- DragState(WindowManagerService service, IBinder token, SurfaceControl surface,
- int flags, IBinder localWin) {
+ DragState(WindowManagerService service, DragDropController controller, IBinder token,
+ SurfaceControl surface, int flags, IBinder localWin) {
mService = service;
- mDragDropController = service.mDragDropController;
+ mDragDropController = controller;
mToken = token;
mSurfaceControl = surface;
mFlags = flags;
@@ -318,15 +320,9 @@ class DragState {
mSourceUserId = UserHandle.getUserId(mUid);
- final IUserManager userManager =
- (IUserManager) ServiceManager.getService(Context.USER_SERVICE);
- try {
- mCrossProfileCopyAllowed = !userManager.getUserRestrictions(mSourceUserId).getBoolean(
- UserManager.DISALLOW_CROSS_PROFILE_COPY_PASTE);
- } catch (RemoteException e) {
- Slog.e(TAG_WM, "Remote Exception calling UserManager: " + e);
- mCrossProfileCopyAllowed = false;
- }
+ final UserManagerInternal userManager = LocalServices.getService(UserManagerInternal.class);
+ mCrossProfileCopyAllowed = !userManager.getUserRestriction(
+ mSourceUserId, UserManager.DISALLOW_CROSS_PROFILE_COPY_PASTE);
if (DEBUG_DRAG) {
Slog.d(TAG_WM, "broadcasting DRAG_STARTED at (" + touchX + ", " + touchY + ")");
@@ -534,7 +530,8 @@ class DragState {
final int targetUserId = UserHandle.getUserId(touchedWin.getOwningUid());
final DragAndDropPermissionsHandler dragAndDropPermissions;
- if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0) {
+ if ((mFlags & View.DRAG_FLAG_GLOBAL) != 0 && (mFlags & DRAG_FLAGS_URI_ACCESS) != 0
+ && mData != null) {
dragAndDropPermissions = new DragAndDropPermissionsHandler(
mData,
mUid,
@@ -546,7 +543,9 @@ class DragState {
dragAndDropPermissions = null;
}
if (mSourceUserId != targetUserId){
- mData.fixUris(mSourceUserId);
+ if (mData != null) {
+ mData.fixUris(mSourceUserId);
+ }
}
final int myPid = Process.myPid();
final IBinder token = touchedWin.mClient.asBinder();
diff --git a/com/android/server/wm/InputMonitor.java b/com/android/server/wm/InputMonitor.java
index 88b7a11f..281e0a84 100644
--- a/com/android/server/wm/InputMonitor.java
+++ b/com/android/server/wm/InputMonitor.java
@@ -19,6 +19,7 @@ package com.android.server.wm;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.INPUT_CONSUMER_NAVIGATION;
import static android.view.WindowManager.INPUT_CONSUMER_PIP;
+import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION;
import static android.view.WindowManager.INPUT_CONSUMER_WALLPAPER;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_DISABLE_WALLPAPER_TOUCH_EVENTS;
@@ -86,6 +87,7 @@ final class InputMonitor implements InputManagerService.WindowManagerCallbacks {
private boolean mAddInputConsumerHandle;
private boolean mAddPipInputConsumerHandle;
private boolean mAddWallpaperInputConsumerHandle;
+ private boolean mAddRecentsAnimationInputConsumerHandle;
private boolean mDisableWallpaperTouchEvents;
private final Rect mTmpRect = new Rect();
private final UpdateInputForAllWindowsConsumer mUpdateInputForAllWindowsConsumer =
@@ -612,7 +614,7 @@ final class InputMonitor implements InputManagerService.WindowManagerCallbacks {
InputConsumerImpl navInputConsumer;
InputConsumerImpl pipInputConsumer;
InputConsumerImpl wallpaperInputConsumer;
- Rect pipTouchableBounds;
+ InputConsumerImpl recentsAnimationInputConsumer;
boolean inDrag;
WallpaperController wallpaperController;
@@ -622,11 +624,13 @@ final class InputMonitor implements InputManagerService.WindowManagerCallbacks {
navInputConsumer = getInputConsumer(INPUT_CONSUMER_NAVIGATION, DEFAULT_DISPLAY);
pipInputConsumer = getInputConsumer(INPUT_CONSUMER_PIP, DEFAULT_DISPLAY);
wallpaperInputConsumer = getInputConsumer(INPUT_CONSUMER_WALLPAPER, DEFAULT_DISPLAY);
+ recentsAnimationInputConsumer = getInputConsumer(INPUT_CONSUMER_RECENTS_ANIMATION,
+ DEFAULT_DISPLAY);
mAddInputConsumerHandle = navInputConsumer != null;
mAddPipInputConsumerHandle = pipInputConsumer != null;
mAddWallpaperInputConsumerHandle = wallpaperInputConsumer != null;
+ mAddRecentsAnimationInputConsumerHandle = recentsAnimationInputConsumer != null;
mTmpRect.setEmpty();
- pipTouchableBounds = mAddPipInputConsumerHandle ? mTmpRect : null;
mDisableWallpaperTouchEvents = false;
this.inDrag = inDrag;
wallpaperController = mService.mRoot.mWallpaperController;
@@ -659,12 +663,28 @@ final class InputMonitor implements InputManagerService.WindowManagerCallbacks {
final boolean hasFocus = w == mInputFocus;
final boolean isVisible = w.isVisibleLw();
+ if (mAddRecentsAnimationInputConsumerHandle) {
+ final RecentsAnimationController recentsAnimationController =
+ mService.getRecentsAnimationController();
+ if (recentsAnimationController != null
+ && recentsAnimationController.hasInputConsumerForApp(w.mAppToken)) {
+ if (recentsAnimationController.updateInputConsumerForApp(
+ recentsAnimationInputConsumer, hasFocus)) {
+ addInputWindowHandle(recentsAnimationInputConsumer.mWindowHandle);
+ mAddRecentsAnimationInputConsumerHandle = false;
+ }
+ // Skip adding the window below regardless of whether there is an input consumer
+ // to handle it
+ return;
+ }
+ }
+
if (w.inPinnedWindowingMode()) {
if (mAddPipInputConsumerHandle
&& (inputWindowHandle.layer <= pipInputConsumer.mWindowHandle.layer)) {
// Update the bounds of the Pip input consumer to match the window bounds.
- w.getBounds(pipTouchableBounds);
- pipInputConsumer.mWindowHandle.touchableRegion.set(pipTouchableBounds);
+ w.getBounds(mTmpRect);
+ pipInputConsumer.mWindowHandle.touchableRegion.set(mTmpRect);
addInputWindowHandle(pipInputConsumer.mWindowHandle);
mAddPipInputConsumerHandle = false;
}
diff --git a/com/android/server/wm/Letterbox.java b/com/android/server/wm/Letterbox.java
new file mode 100644
index 00000000..8fa79ab5
--- /dev/null
+++ b/com/android/server/wm/Letterbox.java
@@ -0,0 +1,145 @@
+/*
+ * 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 com.android.server.wm;
+
+import static android.view.SurfaceControl.HIDDEN;
+
+import android.graphics.Rect;
+import android.view.SurfaceControl;
+
+import java.util.function.Supplier;
+
+/**
+ * Manages a set of {@link SurfaceControl}s to draw a black letterbox between an
+ * outer rect and an inner rect.
+ */
+public class Letterbox {
+
+ private static final Rect EMPTY_RECT = new Rect();
+
+ private final Supplier<SurfaceControl.Builder> mFactory;
+ private final Rect mOuter = new Rect();
+ private final Rect mInner = new Rect();
+ private final LetterboxSurface mTop = new LetterboxSurface("top");
+ private final LetterboxSurface mLeft = new LetterboxSurface("left");
+ private final LetterboxSurface mBottom = new LetterboxSurface("bottom");
+ private final LetterboxSurface mRight = new LetterboxSurface("right");
+
+ /**
+ * Constructs a Letterbox.
+ *
+ * @param surfaceControlFactory a factory for creating the managed {@link SurfaceControl}s
+ */
+ public Letterbox(Supplier<SurfaceControl.Builder> surfaceControlFactory) {
+ mFactory = surfaceControlFactory;
+ }
+
+ /**
+ * Sets the dimensions of the the letterbox, such that the area between the outer and inner
+ * frames will be covered by black color surfaces.
+ *
+ * @param t a transaction in which to set the dimensions
+ * @param outer the outer frame of the letterbox (this frame will be black, except the area
+ * that intersects with the {code inner} frame).
+ * @param inner the inner frame of the letterbox (this frame will be clear)
+ */
+ public void setDimensions(SurfaceControl.Transaction t, Rect outer, Rect inner) {
+ mOuter.set(outer);
+ mInner.set(inner);
+
+ mTop.setRect(t, outer.left, outer.top, inner.right, inner.top);
+ mLeft.setRect(t, outer.left, inner.top, inner.left, outer.bottom);
+ mBottom.setRect(t, inner.left, inner.bottom, outer.right, outer.bottom);
+ mRight.setRect(t, inner.right, outer.top, outer.right, inner.bottom);
+ }
+
+ /**
+ * Hides the letterbox.
+ *
+ * @param t a transaction in which to hide the letterbox
+ */
+ public void hide(SurfaceControl.Transaction t) {
+ setDimensions(t, EMPTY_RECT, EMPTY_RECT);
+ }
+
+ /**
+ * Destroys the managed {@link SurfaceControl}s.
+ */
+ public void destroy() {
+ mOuter.setEmpty();
+ mInner.setEmpty();
+
+ mTop.destroy();
+ mLeft.destroy();
+ mBottom.destroy();
+ mRight.destroy();
+ }
+
+ private class LetterboxSurface {
+
+ private final String mType;
+ private SurfaceControl mSurface;
+
+ private int mLastLeft = 0;
+ private int mLastTop = 0;
+ private int mLastRight = 0;
+ private int mLastBottom = 0;
+
+ public LetterboxSurface(String type) {
+ mType = type;
+ }
+
+ public void setRect(SurfaceControl.Transaction t,
+ int left, int top, int right, int bottom) {
+ if (mLastLeft == left && mLastTop == top
+ && mLastRight == right && mLastBottom == bottom) {
+ // Nothing changed.
+ return;
+ }
+
+ if (left < right && top < bottom) {
+ if (mSurface == null) {
+ createSurface();
+ }
+ t.setPosition(mSurface, left, top);
+ t.setSize(mSurface, right - left, bottom - top);
+ t.show(mSurface);
+ } else if (mSurface != null) {
+ t.hide(mSurface);
+ }
+
+ mLastLeft = left;
+ mLastTop = top;
+ mLastRight = right;
+ mLastBottom = bottom;
+ }
+
+ private void createSurface() {
+ mSurface = mFactory.get().setName("Letterbox - " + mType)
+ .setFlags(HIDDEN).setColorLayer(true).build();
+ mSurface.setLayer(-1);
+ mSurface.setColor(new float[]{0, 0, 0});
+ }
+
+ public void destroy() {
+ if (mSurface != null) {
+ mSurface.destroy();
+ mSurface = null;
+ }
+ }
+ }
+}
diff --git a/com/android/server/wm/PinnedStackController.java b/com/android/server/wm/PinnedStackController.java
index d8726bfc..62519e12 100644
--- a/com/android/server/wm/PinnedStackController.java
+++ b/com/android/server/wm/PinnedStackController.java
@@ -48,6 +48,7 @@ import com.android.internal.policy.PipSnapAlgorithm;
import com.android.server.UiThread;
import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
@@ -71,6 +72,7 @@ class PinnedStackController {
private static final String TAG = TAG_WITH_CLASS_NAME ? "PinnedStackController" : TAG_WM;
+ public static final float INVALID_SNAP_FRACTION = -1f;
private final WindowManagerService mService;
private final DisplayContent mDisplayContent;
private final Handler mHandler = UiThread.getHandler();
@@ -101,6 +103,8 @@ class PinnedStackController {
private float mDefaultAspectRatio;
private Point mScreenEdgeInsets;
private int mCurrentMinSize;
+ private float mReentrySnapFraction = INVALID_SNAP_FRACTION;
+ private WeakReference<AppWindowToken> mLastPipActivity = null;
// The aspect ratio bounds of the PIP.
private float mMinAspectRatio;
@@ -113,6 +117,7 @@ class PinnedStackController {
private final Rect mTmpAnimatingBoundsRect = new Rect();
private final Point mTmpDisplaySize = new Point();
+
/**
* The callback object passed to listeners for them to notify the controller of state changes.
*/
@@ -250,9 +255,35 @@ class PinnedStackController {
}
/**
+ * Saves the current snap fraction for re-entry of the current activity into PiP.
+ */
+ void saveReentrySnapFraction(final AppWindowToken token, final Rect stackBounds) {
+ mReentrySnapFraction = getSnapFraction(stackBounds);
+ mLastPipActivity = new WeakReference<>(token);
+ }
+
+ /**
+ * Resets the last saved snap fraction so that the default bounds will be returned.
+ */
+ void resetReentrySnapFraction(AppWindowToken token) {
+ if (mLastPipActivity != null && mLastPipActivity.get() == token) {
+ mReentrySnapFraction = INVALID_SNAP_FRACTION;
+ mLastPipActivity = null;
+ }
+ }
+
+ /**
* @return the default bounds to show the PIP when there is no active PIP.
*/
- Rect getDefaultBounds() {
+ Rect getDefaultOrLastSavedBounds() {
+ return getDefaultBounds(mReentrySnapFraction);
+ }
+
+ /**
+ * @return the default bounds to show the PIP, if a {@param snapFraction} is provided, then it
+ * will apply the default bounds to the provided snap fraction.
+ */
+ Rect getDefaultBounds(float snapFraction) {
synchronized (mService.mWindowMap) {
final Rect insetBounds = new Rect();
getInsetBounds(insetBounds);
@@ -260,8 +291,14 @@ class PinnedStackController {
final Rect defaultBounds = new Rect();
final Size size = mSnapAlgorithm.getSizeForAspectRatio(mDefaultAspectRatio,
mDefaultMinSize, mDisplayInfo.logicalWidth, mDisplayInfo.logicalHeight);
- Gravity.apply(mDefaultStackGravity, size.getWidth(), size.getHeight(), insetBounds,
- 0, mIsImeShowing ? mImeHeight : 0, defaultBounds);
+ if (snapFraction != INVALID_SNAP_FRACTION) {
+ defaultBounds.set(0, 0, size.getWidth(), size.getHeight());
+ final Rect movementBounds = getMovementBounds(defaultBounds);
+ mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction);
+ } else {
+ Gravity.apply(mDefaultStackGravity, size.getWidth(), size.getHeight(), insetBounds,
+ 0, mIsImeShowing ? mImeHeight : 0, defaultBounds);
+ }
return defaultBounds;
}
}
@@ -299,9 +336,7 @@ class PinnedStackController {
final Rect postChangeStackBounds = mTmpRect;
// Calculate the snap fraction of the current stack along the old movement bounds
- final Rect preChangeMovementBounds = getMovementBounds(postChangeStackBounds);
- final float snapFraction = mSnapAlgorithm.getSnapFraction(postChangeStackBounds,
- preChangeMovementBounds);
+ final float snapFraction = getSnapFraction(postChangeStackBounds);
mDisplayInfo.copyFrom(displayInfo);
// Calculate the stack bounds in the new orientation to the same same fraction along the
@@ -325,14 +360,19 @@ class PinnedStackController {
* Sets the Ime state and height.
*/
void setAdjustedForIme(boolean adjustedForIme, int imeHeight) {
- // Return early if there is no state change
- if (mIsImeShowing == adjustedForIme && mImeHeight == imeHeight) {
+ // Due to the order of callbacks from the system, we may receive an ime height even when
+ // {@param adjustedForIme} is false, and also a zero height when {@param adjustedForIme}
+ // is true. Instead, ensure that the ime state changes with the height and if the ime is
+ // showing, then the height is non-zero.
+ final boolean imeShowing = adjustedForIme && imeHeight > 0;
+ imeHeight = imeShowing ? imeHeight : 0;
+ if (imeShowing == mIsImeShowing && imeHeight == mImeHeight) {
return;
}
- mIsImeShowing = adjustedForIme;
+ mIsImeShowing = imeShowing;
mImeHeight = imeHeight;
- notifyImeVisibilityChanged(adjustedForIme, imeHeight);
+ notifyImeVisibilityChanged(imeShowing, imeHeight);
notifyMovementBoundsChanged(true /* fromImeAdjustment */);
}
@@ -414,7 +454,7 @@ class PinnedStackController {
try {
final Rect insetBounds = new Rect();
getInsetBounds(insetBounds);
- final Rect normalBounds = getDefaultBounds();
+ final Rect normalBounds = getDefaultBounds(INVALID_SNAP_FRACTION);
if (isValidPictureInPictureAspectRatio(mAspectRatio)) {
transformBoundsToAspectRatio(normalBounds, mAspectRatio,
false /* useCurrentMinEdgeSize */);
@@ -486,6 +526,14 @@ class PinnedStackController {
}
/**
+ * @return the default snap fraction to apply instead of the default gravity when calculating
+ * the default stack bounds when first entering PiP.
+ */
+ private float getSnapFraction(Rect stackBounds) {
+ return mSnapAlgorithm.getSnapFraction(stackBounds, getMovementBounds(stackBounds));
+ }
+
+ /**
* @return the pixels for a given dp value.
*/
private int dpToPx(float dpValue, DisplayMetrics dm) {
@@ -494,7 +542,8 @@ class PinnedStackController {
void dump(String prefix, PrintWriter pw) {
pw.println(prefix + "PinnedStackController");
- pw.print(prefix + " defaultBounds="); getDefaultBounds().printShortString(pw);
+ pw.print(prefix + " defaultBounds=");
+ getDefaultBounds(INVALID_SNAP_FRACTION).printShortString(pw);
pw.println();
mService.getStackBounds(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, mTmpRect);
pw.print(prefix + " movementBounds="); getMovementBounds(mTmpRect).printShortString(pw);
@@ -516,7 +565,7 @@ class PinnedStackController {
void writeToProto(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
- getDefaultBounds().writeToProto(proto, DEFAULT_BOUNDS);
+ getDefaultBounds(INVALID_SNAP_FRACTION).writeToProto(proto, DEFAULT_BOUNDS);
mService.getStackBounds(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD, mTmpRect);
getMovementBounds(mTmpRect).writeToProto(proto, MOVEMENT_BOUNDS);
proto.end(token);
diff --git a/com/android/server/wm/PinnedStackWindowController.java b/com/android/server/wm/PinnedStackWindowController.java
index b021a722..02fbfba9 100644
--- a/com/android/server/wm/PinnedStackWindowController.java
+++ b/com/android/server/wm/PinnedStackWindowController.java
@@ -61,7 +61,7 @@ public class PinnedStackWindowController extends StackWindowController {
displayContent.getPinnedStackController();
if (stackBounds == null) {
// Calculate the aspect ratio bounds from the default bounds
- stackBounds = pinnedStackController.getDefaultBounds();
+ stackBounds = pinnedStackController.getDefaultOrLastSavedBounds();
}
if (pinnedStackController.isValidPictureInPictureAspectRatio(aspectRatio)) {
@@ -173,7 +173,7 @@ public class PinnedStackWindowController extends StackWindowController {
* from fullscreen to non-fullscreen bounds.
*/
public boolean deferScheduleMultiWindowModeChanged() {
- synchronized(mWindowMap) {
+ synchronized (mWindowMap) {
return mContainer.deferScheduleMultiWindowModeChanged();
}
}
diff --git a/com/android/server/wm/RecentsAnimationController.java b/com/android/server/wm/RecentsAnimationController.java
new file mode 100644
index 00000000..c7d4b8ed
--- /dev/null
+++ b/com/android/server/wm/RecentsAnimationController.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
+import static android.view.RemoteAnimationTarget.MODE_CLOSING;
+import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION;
+import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.TaskSnapshot;
+import android.app.WindowConfiguration;
+import android.graphics.GraphicBuffer;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Log;
+import android.util.Slog;
+import android.view.IRecentsAnimationController;
+import android.view.IRecentsAnimationRunner;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Controls a single instance of the remote driven recents animation. In particular, this allows
+ * the calling SystemUI to animate the visible task windows as a part of the transition. The remote
+ * runner is provided an animation controller which allows it to take screenshots and to notify
+ * window manager when the animation is completed. In addition, window manager may also notify the
+ * app if it requires the animation to be canceled at any time (ie. due to timeout, etc.)
+ */
+public class RecentsAnimationController {
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "RecentsAnimationController" : TAG_WM;
+ private static final boolean DEBUG = false;
+
+ private final WindowManagerService mService;
+ private final IRecentsAnimationRunner mRunner;
+ private final RecentsAnimationCallbacks mCallbacks;
+ private final ArrayList<TaskAnimationAdapter> mPendingAnimations = new ArrayList<>();
+
+ // The recents component app token that is shown behind the visibile tasks
+ private AppWindowToken mHomeAppToken;
+
+ // We start the RecentsAnimationController in a pending-start state since we need to wait for
+ // the wallpaper/activity to draw before we can give control to the handler to start animating
+ // the visible task surfaces
+ private boolean mPendingStart = true;
+
+ // Set when the animation has been canceled
+ private boolean mCanceled = false;
+
+ // Whether or not the input consumer is enabled. The input consumer must be both registered and
+ // enabled for it to start intercepting touch events.
+ private boolean mInputConsumerEnabled;
+
+ private Rect mTmpRect = new Rect();
+
+ public interface RecentsAnimationCallbacks {
+ void onAnimationFinished(boolean moveHomeToTop);
+ }
+
+ private final IRecentsAnimationController mController =
+ new IRecentsAnimationController.Stub() {
+
+ @Override
+ public TaskSnapshot screenshotTask(int taskId) {
+ if (DEBUG) Log.d(TAG, "screenshotTask(" + taskId + "): mCanceled=" + mCanceled);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mService.getWindowManagerLock()) {
+ if (mCanceled) {
+ return null;
+ }
+ for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
+ final TaskAnimationAdapter adapter = mPendingAnimations.get(i);
+ final Task task = adapter.mTask;
+ if (task.mTaskId == taskId) {
+ // TODO: Save this screenshot as the task snapshot?
+ final Rect taskFrame = new Rect();
+ task.getBounds(taskFrame);
+ final GraphicBuffer buffer = SurfaceControl.captureLayers(
+ task.getSurfaceControl().getHandle(), taskFrame, 1f);
+ final AppWindowToken topChild = task.getTopChild();
+ final WindowState mainWindow = topChild.findMainWindow();
+ return new TaskSnapshot(buffer, topChild.getConfiguration().orientation,
+ mainWindow.mStableInsets,
+ ActivityManager.isLowRamDeviceStatic() /* reduced */,
+ 1.0f /* scale */);
+ }
+ }
+ return null;
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void finish(boolean moveHomeToTop) {
+ if (DEBUG) Log.d(TAG, "finish(" + moveHomeToTop + "): mCanceled=" + mCanceled);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mService.getWindowManagerLock()) {
+ if (mCanceled) {
+ return;
+ }
+ }
+
+ // Note, the callback will handle its own synchronization, do not lock on WM lock
+ // prior to calling the callback
+ mCallbacks.onAnimationFinished(moveHomeToTop);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+
+ @Override
+ public void setInputConsumerEnabled(boolean enabled) {
+ if (DEBUG) Log.d(TAG, "setInputConsumerEnabled(" + enabled + "): mCanceled="
+ + mCanceled);
+ long token = Binder.clearCallingIdentity();
+ try {
+ synchronized (mService.getWindowManagerLock()) {
+ if (mCanceled) {
+ return;
+ }
+
+ mInputConsumerEnabled = enabled;
+ mService.mInputMonitor.updateInputWindowsLw(true /*force*/);
+ mService.scheduleAnimationLocked();
+ }
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
+ };
+
+ /**
+ * Initializes a new RecentsAnimationController.
+ *
+ * @param remoteAnimationRunner The remote runner which should be notified when the animation is
+ * ready to start or has been canceled
+ * @param callbacks Callbacks to be made when the animation finishes
+ * @param restoreHomeBehindStackId The stack id to restore the home stack behind once the
+ * animation is complete. Will be passed to the callback.
+ */
+ RecentsAnimationController(WindowManagerService service,
+ IRecentsAnimationRunner remoteAnimationRunner, RecentsAnimationCallbacks callbacks,
+ int displayId) {
+ mService = service;
+ mRunner = remoteAnimationRunner;
+ mCallbacks = callbacks;
+
+ final DisplayContent dc = mService.mRoot.getDisplayContent(displayId);
+ final ArrayList<Task> visibleTasks = dc.getVisibleTasks();
+ if (visibleTasks.isEmpty()) {
+ cancelAnimation();
+ return;
+ }
+
+ // Make leashes for each of the visible tasks and add it to the recents animation to be
+ // started
+ final int taskCount = visibleTasks.size();
+ for (int i = 0; i < taskCount; i++) {
+ final Task task = visibleTasks.get(i);
+ final WindowConfiguration config = task.getWindowConfiguration();
+ if (config.tasksAreFloating()
+ || config.getWindowingMode() == WINDOWING_MODE_SPLIT_SCREEN_PRIMARY
+ || config.getActivityType() == ACTIVITY_TYPE_HOME) {
+ continue;
+ }
+ addAnimation(task);
+ }
+
+ // Adjust the wallpaper visibility for the showing home activity
+ final AppWindowToken recentsComponentAppToken =
+ dc.getHomeStack().getTopChild().getTopFullscreenAppToken();
+ if (recentsComponentAppToken != null) {
+ if (DEBUG) Log.d(TAG, "setHomeApp(" + recentsComponentAppToken.getName() + ")");
+ mHomeAppToken = recentsComponentAppToken;
+ final WallpaperController wc = dc.mWallpaperController;
+ if (recentsComponentAppToken.windowsCanBeWallpaperTarget()) {
+ dc.pendingLayoutChanges |= FINISH_LAYOUT_REDO_WALLPAPER;
+ dc.setLayoutNeeded();
+ }
+ }
+
+ mService.mWindowPlacerLocked.performSurfacePlacement();
+ }
+
+ private void addAnimation(Task task) {
+ if (DEBUG) Log.d(TAG, "addAnimation(" + task.getName() + ")");
+ final SurfaceAnimator anim = new SurfaceAnimator(task, null /* animationFinishedCallback */,
+ mService.mAnimator::addAfterPrepareSurfacesRunnable, mService);
+ final TaskAnimationAdapter taskAdapter = new TaskAnimationAdapter(task);
+ anim.startAnimation(task.getPendingTransaction(), taskAdapter, false /* hidden */);
+ task.commitPendingTransaction();
+ mPendingAnimations.add(taskAdapter);
+ }
+
+ void startAnimation() {
+ if (DEBUG) Log.d(TAG, "startAnimation(): mPendingStart=" + mPendingStart);
+ if (!mPendingStart) {
+ return;
+ }
+ try {
+ final RemoteAnimationTarget[] appAnimations =
+ new RemoteAnimationTarget[mPendingAnimations.size()];
+ for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
+ appAnimations[i] = mPendingAnimations.get(i).createRemoteAnimationApp();
+ }
+ mPendingStart = false;
+ mRunner.onAnimationStart(mController, appAnimations);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to start recents animation", e);
+ }
+ }
+
+ void cancelAnimation() {
+ if (DEBUG) Log.d(TAG, "cancelAnimation()");
+ if (mCanceled) {
+ // We've already canceled the animation
+ return;
+ }
+ mCanceled = true;
+ try {
+ mRunner.onAnimationCanceled();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to cancel recents animation", e);
+ }
+
+ // Clean up and return to the previous app
+ mCallbacks.onAnimationFinished(false /* moveHomeToTop */);
+ }
+
+ void cleanupAnimation() {
+ if (DEBUG) Log.d(TAG, "cleanupAnimation(): mPendingAnimations="
+ + mPendingAnimations.size());
+ for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
+ final TaskAnimationAdapter adapter = mPendingAnimations.get(i);
+ adapter.mCapturedFinishCallback.onAnimationFinished(adapter);
+ }
+ mPendingAnimations.clear();
+
+ mService.mInputMonitor.updateInputWindowsLw(true /*force*/);
+ mService.scheduleAnimationLocked();
+ mService.destroyInputConsumer(INPUT_CONSUMER_RECENTS_ANIMATION);
+ }
+
+ void checkAnimationReady(WallpaperController wallpaperController) {
+ if (mPendingStart) {
+ final boolean wallpaperReady = !isHomeAppOverWallpaper()
+ || (wallpaperController.getWallpaperTarget() != null
+ && wallpaperController.wallpaperTransitionReady());
+ if (wallpaperReady) {
+ mService.getRecentsAnimationController().startAnimation();
+ }
+ }
+ }
+
+ boolean isWallpaperVisible(WindowState w) {
+ return w != null && w.mAppToken != null && mHomeAppToken == w.mAppToken
+ && isHomeAppOverWallpaper();
+ }
+
+ boolean hasInputConsumerForApp(AppWindowToken appToken) {
+ return mInputConsumerEnabled && isAnimatingApp(appToken);
+ }
+
+ boolean updateInputConsumerForApp(InputConsumerImpl recentsAnimationInputConsumer,
+ boolean hasFocus) {
+ // Update the input consumer touchable region to match the home app main window
+ final WindowState homeAppMainWindow = mHomeAppToken != null
+ ? mHomeAppToken.findMainWindow()
+ : null;
+ if (homeAppMainWindow != null) {
+ homeAppMainWindow.getBounds(mTmpRect);
+ recentsAnimationInputConsumer.mWindowHandle.hasFocus = hasFocus;
+ recentsAnimationInputConsumer.mWindowHandle.touchableRegion.set(mTmpRect);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isHomeAppOverWallpaper() {
+ if (mHomeAppToken == null) {
+ return false;
+ }
+ return mHomeAppToken.windowsCanBeWallpaperTarget();
+ }
+
+ private boolean isAnimatingApp(AppWindowToken appToken) {
+ for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
+ final Task task = mPendingAnimations.get(i).mTask;
+ for (int j = task.getChildCount() - 1; j >= 0; j--) {
+ final AppWindowToken app = task.getChildAt(j);
+ if (app == appToken) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ private class TaskAnimationAdapter implements AnimationAdapter {
+
+ private Task mTask;
+ private SurfaceControl mCapturedLeash;
+ private OnAnimationFinishedCallback mCapturedFinishCallback;
+
+ TaskAnimationAdapter(Task task) {
+ mTask = task;
+ }
+
+ RemoteAnimationTarget createRemoteAnimationApp() {
+ // TODO: Do we need position and stack bounds?
+ return new RemoteAnimationTarget(mTask.mTaskId, MODE_CLOSING, mCapturedLeash,
+ !mTask.fillsParent(),
+ mTask.getTopVisibleAppMainWindow().mWinAnimator.mLastClipRect,
+ mTask.getPrefixOrderIndex(), new Point(), new Rect(),
+ mTask.getWindowConfiguration());
+ }
+
+ @Override
+ public boolean getDetachWallpaper() {
+ return false;
+ }
+
+ @Override
+ public int getBackgroundColor() {
+ return 0;
+ }
+
+ @Override
+ public void startAnimation(SurfaceControl animationLeash, Transaction t,
+ OnAnimationFinishedCallback finishCallback) {
+ mCapturedLeash = animationLeash;
+ mCapturedFinishCallback = finishCallback;
+ }
+
+ @Override
+ public void onAnimationCancelled(SurfaceControl animationLeash) {
+ cancelAnimation();
+ }
+
+ @Override
+ public long getDurationHint() {
+ return 0;
+ }
+
+ @Override
+ public long getStatusBarTransitionsStartTime() {
+ return SystemClock.uptimeMillis();
+ }
+ }
+
+ public void dump(PrintWriter pw, String prefix) {
+ final String innerPrefix = prefix + " ";
+ pw.print(prefix); pw.println(RecentsAnimationController.class.getSimpleName() + ":");
+ pw.print(innerPrefix); pw.println("mPendingStart=" + mPendingStart);
+ pw.print(innerPrefix); pw.println("mHomeAppToken=" + mHomeAppToken);
+ }
+}
diff --git a/com/android/server/wm/RemoteAnimationController.java b/com/android/server/wm/RemoteAnimationController.java
new file mode 100644
index 00000000..7d4eafb0
--- /dev/null
+++ b/com/android/server/wm/RemoteAnimationController.java
@@ -0,0 +1,219 @@
+/*
+ * 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 com.android.server.wm;
+
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.util.Slog;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationFinishedCallback.Stub;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback;
+
+import java.util.ArrayList;
+
+/**
+ * Helper class to run app animations in a remote process.
+ */
+class RemoteAnimationController {
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "RemoteAnimationController" : TAG_WM;
+ private static final long TIMEOUT_MS = 2000;
+
+ private final WindowManagerService mService;
+ private final RemoteAnimationAdapter mRemoteAnimationAdapter;
+ private final ArrayList<RemoteAnimationAdapterWrapper> mPendingAnimations = new ArrayList<>();
+ private final Rect mTmpRect = new Rect();
+ private final Handler mHandler;
+
+ private final IRemoteAnimationFinishedCallback mFinishedCallback = new Stub() {
+ @Override
+ public void onAnimationFinished() throws RemoteException {
+ RemoteAnimationController.this.onAnimationFinished();
+ }
+ };
+
+ private final Runnable mTimeoutRunnable = () -> {
+ onAnimationFinished();
+ invokeAnimationCancelled();
+ };
+
+ RemoteAnimationController(WindowManagerService service,
+ RemoteAnimationAdapter remoteAnimationAdapter, Handler handler) {
+ mService = service;
+ mRemoteAnimationAdapter = remoteAnimationAdapter;
+ mHandler = handler;
+ }
+
+ /**
+ * Creates an animation for each individual {@link AppWindowToken}.
+ *
+ * @param appWindowToken The app to animate.
+ * @param position The position app bounds, in screen coordinates.
+ * @param stackBounds The stack bounds of the app.
+ * @return The adapter to be run on the app.
+ */
+ AnimationAdapter createAnimationAdapter(AppWindowToken appWindowToken, Point position,
+ Rect stackBounds) {
+ final RemoteAnimationAdapterWrapper adapter = new RemoteAnimationAdapterWrapper(
+ appWindowToken, position, stackBounds);
+ mPendingAnimations.add(adapter);
+ return adapter;
+ }
+
+ /**
+ * Called when the transition is ready to be started, and all leashes have been set up.
+ */
+ void goodToGo() {
+ mHandler.postDelayed(mTimeoutRunnable, TIMEOUT_MS);
+ try {
+ mRemoteAnimationAdapter.getRunner().onAnimationStart(createAnimations(),
+ mFinishedCallback);
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to start remote animation", e);
+ onAnimationFinished();
+ }
+ }
+
+ private RemoteAnimationTarget[] createAnimations() {
+ final ArrayList<RemoteAnimationTarget> targets = new ArrayList<>();
+ for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
+ final RemoteAnimationTarget target =
+ mPendingAnimations.get(i).createRemoteAppAnimation();
+ if (target != null) {
+ targets.add(target);
+ }
+ }
+ return targets.toArray(new RemoteAnimationTarget[targets.size()]);
+ }
+
+ private void onAnimationFinished() {
+ mHandler.removeCallbacks(mTimeoutRunnable);
+ synchronized (mService.mWindowMap) {
+ mService.openSurfaceTransaction();
+ try {
+ for (int i = mPendingAnimations.size() - 1; i >= 0; i--) {
+ final RemoteAnimationAdapterWrapper adapter = mPendingAnimations.get(i);
+ adapter.mCapturedFinishCallback.onAnimationFinished(adapter);
+ }
+ } finally {
+ mService.closeSurfaceTransaction("RemoteAnimationController#finished");
+ }
+ }
+ }
+
+ private void invokeAnimationCancelled() {
+ try {
+ mRemoteAnimationAdapter.getRunner().onAnimationCancelled();
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to notify cancel", e);
+ }
+ }
+
+ private class RemoteAnimationAdapterWrapper implements AnimationAdapter {
+
+ private final AppWindowToken mAppWindowToken;
+ private SurfaceControl mCapturedLeash;
+ private OnAnimationFinishedCallback mCapturedFinishCallback;
+ private final Point mPosition = new Point();
+ private final Rect mStackBounds = new Rect();
+
+ RemoteAnimationAdapterWrapper(AppWindowToken appWindowToken, Point position,
+ Rect stackBounds) {
+ mAppWindowToken = appWindowToken;
+ mPosition.set(position.x, position.y);
+ mStackBounds.set(stackBounds);
+ }
+
+ RemoteAnimationTarget createRemoteAppAnimation() {
+ final Task task = mAppWindowToken.getTask();
+ final WindowState mainWindow = mAppWindowToken.findMainWindow();
+ if (task == null) {
+ return null;
+ }
+ if (mainWindow == null) {
+ return null;
+ }
+ return new RemoteAnimationTarget(task.mTaskId, getMode(),
+ mCapturedLeash, !mAppWindowToken.fillsParent(),
+ mainWindow.mWinAnimator.mLastClipRect,
+ mAppWindowToken.getPrefixOrderIndex(), mPosition, mStackBounds,
+ task.getWindowConfiguration());
+ }
+
+ private int getMode() {
+ if (mService.mOpeningApps.contains(mAppWindowToken)) {
+ return RemoteAnimationTarget.MODE_OPENING;
+ } else {
+ return RemoteAnimationTarget.MODE_CLOSING;
+ }
+ }
+
+ @Override
+ public boolean getDetachWallpaper() {
+ return false;
+ }
+
+ @Override
+ public int getBackgroundColor() {
+ return 0;
+ }
+
+ @Override
+ public void startAnimation(SurfaceControl animationLeash, Transaction t,
+ OnAnimationFinishedCallback finishCallback) {
+
+ // Restore z-layering, position and stack crop until client has a chance to modify it.
+ t.setLayer(animationLeash, mAppWindowToken.getPrefixOrderIndex());
+ t.setPosition(animationLeash, mPosition.x, mPosition.y);
+ mTmpRect.set(mStackBounds);
+ mTmpRect.offsetTo(0, 0);
+ t.setWindowCrop(animationLeash, mTmpRect);
+ mCapturedLeash = animationLeash;
+ mCapturedFinishCallback = finishCallback;
+ }
+
+ @Override
+ public void onAnimationCancelled(SurfaceControl animationLeash) {
+ mPendingAnimations.remove(this);
+ if (mPendingAnimations.isEmpty()) {
+ mHandler.removeCallbacks(mTimeoutRunnable);
+ invokeAnimationCancelled();
+ }
+ }
+
+ @Override
+ public long getDurationHint() {
+ return mRemoteAnimationAdapter.getDuration();
+ }
+
+ @Override
+ public long getStatusBarTransitionsStartTime() {
+ return SystemClock.uptimeMillis()
+ + mRemoteAnimationAdapter.getStatusBarTransitionDelay();
+ }
+ }
+}
diff --git a/com/android/server/wm/RemoteSurfaceTrace.java b/com/android/server/wm/RemoteSurfaceTrace.java
index d2cbf88a..33e560f3 100644
--- a/com/android/server/wm/RemoteSurfaceTrace.java
+++ b/com/android/server/wm/RemoteSurfaceTrace.java
@@ -33,7 +33,7 @@ import java.io.DataOutputStream;
// the surface control.
//
// See cts/hostsidetests/../../SurfaceTraceReceiver.java for parsing side.
-class RemoteSurfaceTrace extends SurfaceControlWithBackground {
+class RemoteSurfaceTrace extends SurfaceControl {
static final String TAG = "RemoteSurfaceTrace";
final FileDescriptor mWriteFd;
@@ -42,7 +42,7 @@ class RemoteSurfaceTrace extends SurfaceControlWithBackground {
final WindowManagerService mService;
final WindowState mWindow;
- RemoteSurfaceTrace(FileDescriptor fd, SurfaceControlWithBackground wrapped,
+ RemoteSurfaceTrace(FileDescriptor fd, SurfaceControl wrapped,
WindowState window) {
super(wrapped);
diff --git a/com/android/server/wm/RootWindowContainer.java b/com/android/server/wm/RootWindowContainer.java
index 2a77c92b..deed7f17 100644
--- a/com/android/server/wm/RootWindowContainer.java
+++ b/com/android/server/wm/RootWindowContainer.java
@@ -86,7 +86,6 @@ import static com.android.server.wm.WindowManagerService.H.WINDOW_FREEZE_TIMEOUT
import static com.android.server.wm.WindowManagerService.logSurface;
import static com.android.server.wm.WindowSurfacePlacer.SET_FORCE_HIDING_CHANGED;
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.WindowSurfacePlacer.SET_UPDATE_ROTATION;
import static com.android.server.wm.WindowSurfacePlacer.SET_WALLPAPER_ACTION_PENDING;
import static com.android.server.wm.WindowSurfacePlacer.SET_WALLPAPER_MAY_CHANGE;
@@ -624,6 +623,13 @@ class RootWindowContainer extends WindowContainer<DisplayContent> {
defaultDisplay.pendingLayoutChanges);
}
+ // Defer starting the recents animation until the wallpaper has drawn
+ final RecentsAnimationController recentsAnimationController =
+ mService.getRecentsAnimationController();
+ if (recentsAnimationController != null) {
+ recentsAnimationController.checkAnimationReady(mWallpaperController);
+ }
+
if (mWallpaperForceHidingChanged && defaultDisplay.pendingLayoutChanges == 0
&& !mService.mAppTransition.isReady()) {
// At this point, there was a window with a wallpaper that was force hiding other
@@ -968,9 +974,7 @@ class RootWindowContainer extends WindowContainer<DisplayContent> {
doRequest = true;
}
}
- if ((bulkUpdateParams & SET_TURN_ON_SCREEN) != 0) {
- mService.mTurnOnScreen = true;
- }
+
if ((bulkUpdateParams & SET_WALLPAPER_ACTION_PENDING) != 0) {
mWallpaperActionPending = true;
}
diff --git a/com/android/server/wm/Session.java b/com/android/server/wm/Session.java
index 192d6c84..04ae38ec 100644
--- a/com/android/server/wm/Session.java
+++ b/com/android/server/wm/Session.java
@@ -51,6 +51,7 @@ 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;
@@ -308,30 +309,22 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
}
/* Drag/drop */
+
@Override
- public IBinder prepareDrag(IWindow window, int flags, int width, int height,
- Surface outSurface) {
+ public IBinder performDrag(IWindow window, int flags, SurfaceControl surface, int touchSource,
+ float touchX, float touchY, float thumbCenterX, float thumbCenterY, ClipData data) {
final int callerPid = Binder.getCallingPid();
final int callerUid = Binder.getCallingUid();
final long ident = Binder.clearCallingIdentity();
try {
- return mDragDropController.prepareDrag(
- mSurfaceSession, callerPid, callerUid, window, flags, width, height,
- outSurface);
+ return mDragDropController.performDrag(mSurfaceSession, callerPid, callerUid, window,
+ flags, surface, touchSource, touchX, touchY, thumbCenterX, thumbCenterY, data);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
@Override
- public boolean performDrag(IWindow window, IBinder dragToken,
- int touchSource, float touchX, float touchY, float thumbCenterX, float thumbCenterY,
- ClipData data) {
- return mDragDropController.performDrag(window, dragToken, touchSource,
- touchX, touchY, thumbCenterX, thumbCenterY, data);
- }
-
- @Override
public void reportDropResult(IWindow window, boolean consumed) {
final long ident = Binder.clearCallingIdentity();
try {
@@ -467,6 +460,17 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {
}
}
+ @Override
+ public void updateTapExcludeRegion(IWindow window, int regionId, int left, int top, int width,
+ int height) {
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ mService.updateTapExcludeRegion(window, regionId, left, top, width, height);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ }
+
void windowAddedLocked(String packageName) {
mPackageName = packageName;
mRelayoutTag = "relayoutWindow: " + mPackageName;
diff --git a/com/android/server/wm/SurfaceAnimationRunner.java b/com/android/server/wm/SurfaceAnimationRunner.java
index 3a41eb0e..dc62cc89 100644
--- a/com/android/server/wm/SurfaceAnimationRunner.java
+++ b/com/android/server/wm/SurfaceAnimationRunner.java
@@ -67,6 +67,9 @@ class SurfaceAnimationRunner {
@VisibleForTesting
final ArrayMap<SurfaceControl, RunningAnimation> mRunningAnimations = new ArrayMap<>();
+ @GuardedBy("mLock")
+ private boolean mAnimationStartDeferred;
+
SurfaceAnimationRunner() {
this(null /* callbackProvider */, null /* animatorFactory */, new Transaction());
}
@@ -86,13 +89,41 @@ class SurfaceAnimationRunner {
: SfValueAnimator::new;
}
+ /**
+ * Defers starting of animations until {@link #continueStartingAnimations} is called. This
+ * method is NOT nestable.
+ *
+ * @see #continueStartingAnimations
+ */
+ void deferStartingAnimations() {
+ synchronized (mLock) {
+ mAnimationStartDeferred = true;
+ }
+ }
+
+ /**
+ * Continues starting of animations.
+ *
+ * @see #deferStartingAnimations
+ */
+ void continueStartingAnimations() {
+ synchronized (mLock) {
+ mAnimationStartDeferred = false;
+ if (!mPendingAnimations.isEmpty()) {
+ mChoreographer.postFrameCallback(this::startAnimations);
+ }
+ }
+ }
+
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);
+ if (!mAnimationStartDeferred) {
+ mChoreographer.postFrameCallback(this::startAnimations);
+ }
// Some animations (e.g. move animations) require the initial transform to be applied
// immediately.
@@ -185,7 +216,7 @@ class SurfaceAnimationRunner {
a.mAnimSpec.apply(t, a.mLeash, currentPlayTime);
}
- private void stepAnimation(long frameTimeNanos) {
+ private void startAnimations(long frameTimeNanos) {
synchronized (mLock) {
startPendingAnimationsLocked();
}
diff --git a/com/android/server/wm/SurfaceAnimator.java b/com/android/server/wm/SurfaceAnimator.java
index bda5bc95..0512a08c 100644
--- a/com/android/server/wm/SurfaceAnimator.java
+++ b/com/android/server/wm/SurfaceAnimator.java
@@ -19,17 +19,21 @@ 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 static com.android.server.wm.proto.SurfaceAnimatorProto.ANIMATION_ADAPTER;
+import static com.android.server.wm.proto.SurfaceAnimatorProto.ANIMATION_START_DELAYED;
+import static com.android.server.wm.proto.SurfaceAnimatorProto.LEASH;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.util.ArrayMap;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
import com.android.internal.annotations.VisibleForTesting;
import java.io.PrintWriter;
+import java.util.function.Consumer;
/**
* A class that can run animations on objects that have a set of child surfaces. We do this by
@@ -55,16 +59,21 @@ class SurfaceAnimator {
/**
* @param animatable The object to animate.
* @param animationFinishedCallback Callback to invoke when an animation has finished running.
+ * @param addAfterPrepareSurfaces Consumer that takes a runnable and executes it after preparing
+ * surfaces in WM. Can be implemented differently during testing.
*/
- SurfaceAnimator(Animatable animatable, Runnable animationFinishedCallback,
- WindowManagerService service) {
+ SurfaceAnimator(Animatable animatable, @Nullable Runnable animationFinishedCallback,
+ Consumer<Runnable> addAfterPrepareSurfaces, WindowManagerService service) {
mAnimatable = animatable;
mService = service;
mAnimationFinishedCallback = animationFinishedCallback;
- mInnerAnimationFinishedCallback = getFinishedCallback(animationFinishedCallback);
+ mInnerAnimationFinishedCallback = getFinishedCallback(animationFinishedCallback,
+ addAfterPrepareSurfaces);
}
- private OnAnimationFinishedCallback getFinishedCallback(Runnable animationFinishedCallback) {
+ private OnAnimationFinishedCallback getFinishedCallback(
+ @Nullable Runnable animationFinishedCallback,
+ Consumer<Runnable> addAfterPrepareSurfaces) {
return anim -> {
synchronized (mService.mWindowMap) {
final SurfaceAnimator target = mService.mAnimationTransferMap.remove(anim);
@@ -72,23 +81,31 @@ class SurfaceAnimator {
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();
- }
+ // TODO: This should use pendingTransaction eventually, but right now things
+ // happening on the animation finished callback are happening on the global
+ // transaction.
+ // For now we need to run this after it's guaranteed that the transaction that
+ // reparents the surface onto the leash is executed already. Otherwise this may be
+ // executed first, leading to surface loss, as the reparent operations wouldn't
+ // be in order.
+ addAfterPrepareSurfaces.accept(() -> {
+ if (anim != mAnimation) {
+ // Callback was from another animation - ignore.
+ return;
+ }
+ final Transaction t = new Transaction();
+ SurfaceControl.openTransaction();
+ try {
+ reset(t, true /* destroyLeash */);
+ if (animationFinishedCallback != null) {
+ animationFinishedCallback.run();
+ }
+ } finally {
+ SurfaceControl.mergeToGlobalTransaction(t);
+ SurfaceControl.closeTransaction();
+ }
+ });
}
};
}
@@ -213,7 +230,7 @@ class SurfaceAnimator {
return;
}
final SurfaceControl surface = mAnimatable.getSurfaceControl();
- final SurfaceControl parent = mAnimatable.getParentSurfaceControl();
+ final SurfaceControl parent = mAnimatable.getAnimationLeashParent();
if (surface == null || parent == null) {
Slog.w(TAG, "Unable to transfer animation, surface or parent is null");
cancelAnimation();
@@ -287,6 +304,7 @@ class SurfaceAnimator {
int height, boolean hidden) {
if (DEBUG_ANIM) Slog.i(TAG, "Reparenting to leash");
final SurfaceControl.Builder builder = mAnimatable.makeAnimationLeash()
+ .setParent(mAnimatable.getAnimationLeashParent())
.setName(surface + " - animation-leash")
.setSize(width, height);
final SurfaceControl leash = builder.build();
@@ -297,6 +315,24 @@ class SurfaceAnimator {
return leash;
}
+ /**
+ * Write to a protocol buffer output stream. Protocol buffer message definition is at {@link
+ * com.android.server.wm.proto.SurfaceAnimatorProto}.
+ *
+ * @param proto Stream to write the SurfaceAnimator object to.
+ * @param fieldId Field Id of the SurfaceAnimator as defined in the parent message.
+ * @hide
+ */
+ void writeToProto(ProtoOutputStream proto, long fieldId) {
+ final long token = proto.start(fieldId);
+ proto.write(ANIMATION_ADAPTER, mAnimation != null ? mAnimation.toString() : "null");
+ if (mLeash != null){
+ mLeash.writeToProto(proto, LEASH);
+ }
+ proto.write(ANIMATION_START_DELAYED, mAnimationStartDelayed);
+ proto.end(token);
+ }
+
void dump(PrintWriter pw, String prefix) {
pw.print(prefix); pw.print("mAnimation="); pw.print(mAnimation);
pw.print(" mLeash="); pw.println(mLeash);
@@ -355,6 +391,11 @@ class SurfaceAnimator {
SurfaceControl.Builder makeAnimationLeash();
/**
+ * @return The parent that should be used for the animation leash.
+ */
+ @Nullable SurfaceControl getAnimationLeashParent();
+
+ /**
* @return The surface of the object to be animated.
*/
@Nullable SurfaceControl getSurfaceControl();
diff --git a/com/android/server/wm/SurfaceControlWithBackground.java b/com/android/server/wm/SurfaceControlWithBackground.java
deleted file mode 100644
index 7c5bd43a..00000000
--- a/com/android/server/wm/SurfaceControlWithBackground.java
+++ /dev/null
@@ -1,334 +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.wm;
-
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
-import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
-
-import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_BOTTOM;
-import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_LEFT;
-import static com.android.server.policy.WindowManagerPolicy.NAV_BAR_RIGHT;
-
-import android.graphics.Rect;
-import android.graphics.Region;
-import android.os.IBinder;
-import android.os.Parcel;
-import android.view.Surface;
-import android.view.Surface.OutOfResourcesException;
-import android.view.SurfaceControl;
-
-/**
- * SurfaceControl extension that has black background behind navigation bar area for fullscreen
- * letterboxed apps.
- */
-class SurfaceControlWithBackground extends SurfaceControl {
- // SurfaceControl that holds the background.
- private SurfaceControl mBackgroundControl;
-
- // Flag that defines whether the background should be shown.
- private boolean mVisible;
-
- // Way to communicate with corresponding window.
- private WindowSurfaceController mWindowSurfaceController;
-
- // Rect to hold task bounds when computing metrics for background.
- private Rect mTmpContainerRect = new Rect();
-
- // Last metrics applied to the main SurfaceControl.
- private float mLastWidth, mLastHeight;
- private float mLastDsDx = 1, mLastDsDy = 1;
- private float mLastX, mLastY;
-
- // SurfaceFlinger doesn't support crop rectangles where width or height is non-positive.
- // If we just set an empty crop it will behave as if there is no crop at all.
- // To fix this we explicitly hide the surface and won't let it to be shown.
- private boolean mHiddenForCrop = false;
-
- public SurfaceControlWithBackground(SurfaceControlWithBackground other) {
- super(other);
- mBackgroundControl = other.mBackgroundControl;
- mVisible = other.mVisible;
- mWindowSurfaceController = other.mWindowSurfaceController;
- }
-
- public SurfaceControlWithBackground(String name, SurfaceControl.Builder b,
- int windowType, int w, int h,
- WindowSurfaceController windowSurfaceController) throws OutOfResourcesException {
- super(b.build());
-
- // We should only show background behind app windows that are letterboxed in a task.
- if ((windowType != TYPE_BASE_APPLICATION && windowType != TYPE_APPLICATION_STARTING)
- || !windowSurfaceController.mAnimator.mWin.isLetterboxedAppWindow()) {
- return;
- }
- mWindowSurfaceController = windowSurfaceController;
- mLastWidth = w;
- mLastHeight = h;
- mWindowSurfaceController.getContainerRect(mTmpContainerRect);
- mBackgroundControl = b.setName("Background for - " + name)
- .setSize(mTmpContainerRect.width(), mTmpContainerRect.height())
- .setFormat(OPAQUE)
- .setColorLayer(true)
- .build();
- }
-
- @Override
- public void setAlpha(float alpha) {
- super.setAlpha(alpha);
-
- if (mBackgroundControl == null) {
- return;
- }
- mBackgroundControl.setAlpha(alpha);
- }
-
- @Override
- public void setLayer(int zorder) {
- super.setLayer(zorder);
-
- if (mBackgroundControl == null) {
- return;
- }
- // TODO: Use setRelativeLayer(Integer.MIN_VALUE) when it's fixed.
- mBackgroundControl.setLayer(zorder - 1);
- }
-
- @Override
- public void setPosition(float x, float y) {
- super.setPosition(x, y);
-
- if (mBackgroundControl == null) {
- return;
- }
- mLastX = x;
- mLastY = y;
- updateBgPosition();
- }
-
- private void updateBgPosition() {
- mWindowSurfaceController.getContainerRect(mTmpContainerRect);
- final Rect winFrame = mWindowSurfaceController.mAnimator.mWin.mFrame;
- final float offsetX = (mTmpContainerRect.left - winFrame.left) * mLastDsDx;
- final float offsetY = (mTmpContainerRect.top - winFrame.top) * mLastDsDy;
- mBackgroundControl.setPosition(mLastX + offsetX, mLastY + offsetY);
- }
-
- @Override
- public void setSize(int w, int h) {
- super.setSize(w, h);
-
- if (mBackgroundControl == null) {
- return;
- }
- mLastWidth = w;
- mLastHeight = h;
- mWindowSurfaceController.getContainerRect(mTmpContainerRect);
- mBackgroundControl.setSize(mTmpContainerRect.width(), mTmpContainerRect.height());
- }
-
- @Override
- public void setWindowCrop(Rect crop) {
- super.setWindowCrop(crop);
-
- if (mBackgroundControl == null) {
- return;
- }
- calculateBgCrop(crop);
- mBackgroundControl.setWindowCrop(mTmpContainerRect);
- mHiddenForCrop = mTmpContainerRect.isEmpty();
- updateBackgroundVisibility();
- }
-
- @Override
- public void setFinalCrop(Rect crop) {
- super.setFinalCrop(crop);
-
- if (mBackgroundControl == null) {
- return;
- }
- mWindowSurfaceController.getContainerRect(mTmpContainerRect);
- mBackgroundControl.setFinalCrop(mTmpContainerRect);
- }
-
- /**
- * Compute background crop based on current animation progress for main surface control and
- * update {@link #mTmpContainerRect} with new values.
- */
- private void calculateBgCrop(Rect crop) {
- // Track overall progress of animation by computing cropped portion of status bar.
- final Rect contentInsets = mWindowSurfaceController.mAnimator.mWin.mContentInsets;
- float d = contentInsets.top == 0 ? 0 : (float) crop.top / contentInsets.top;
- if (d > 1.f) {
- // We're running expand animation from launcher, won't compute custom bg crop here.
- mTmpContainerRect.setEmpty();
- return;
- }
-
- // Compute new scaled width and height for background that will depend on current animation
- // progress. Those consist of current crop rect for the main surface + scaled areas outside
- // of letterboxed area.
- // TODO: Because the progress is computed with low precision we're getting smaller values
- // for background width/height then screen size at the end of the animation. Will round when
- // the value is smaller then some empiric epsilon. However, this should be fixed by
- // computing correct frames for letterboxed windows in WindowState.
- d = d < 0.025f ? 0 : d;
- mWindowSurfaceController.getContainerRect(mTmpContainerRect);
- int backgroundWidth = 0, backgroundHeight = 0;
- // Compute additional offset for the background when app window is positioned not at (0,0).
- // E.g. landscape with navigation bar on the left.
- final Rect winFrame = mWindowSurfaceController.mAnimator.mWin.mFrame;
- int offsetX = (int)((winFrame.left - mTmpContainerRect.left) * mLastDsDx),
- offsetY = (int) ((winFrame.top - mTmpContainerRect.top) * mLastDsDy);
-
- // Position and size background.
- final int bgPosition = mWindowSurfaceController.mAnimator.mService.getNavBarPosition();
-
- switch (bgPosition) {
- case NAV_BAR_LEFT:
- backgroundWidth = (int) ((mTmpContainerRect.width() - mLastWidth) * (1 - d) + 0.5);
- backgroundHeight = crop.height();
- offsetX += crop.left - backgroundWidth;
- offsetY += crop.top;
- break;
- case NAV_BAR_RIGHT:
- backgroundWidth = (int) ((mTmpContainerRect.width() - mLastWidth) * (1 - d) + 0.5);
- backgroundHeight = crop.height();
- offsetX += crop.right;
- offsetY += crop.top;
- break;
- case NAV_BAR_BOTTOM:
- backgroundWidth = crop.width();
- backgroundHeight = (int) ((mTmpContainerRect.height() - mLastHeight) * (1 - d)
- + 0.5);
- offsetX += crop.left;
- offsetY += crop.bottom;
- break;
- }
- mTmpContainerRect.set(offsetX, offsetY, offsetX + backgroundWidth,
- offsetY + backgroundHeight);
- }
-
- @Override
- public void setLayerStack(int layerStack) {
- super.setLayerStack(layerStack);
-
- if (mBackgroundControl == null) {
- return;
- }
- mBackgroundControl.setLayerStack(layerStack);
- }
-
- @Override
- public void setOpaque(boolean isOpaque) {
- super.setOpaque(isOpaque);
- updateBackgroundVisibility();
- }
-
- @Override
- public void setSecure(boolean isSecure) {
- super.setSecure(isSecure);
- }
-
- @Override
- public void setMatrix(float dsdx, float dtdx, float dtdy, float dsdy) {
- super.setMatrix(dsdx, dtdx, dtdy, dsdy);
-
- if (mBackgroundControl == null) {
- return;
- }
- mBackgroundControl.setMatrix(dsdx, dtdx, dtdy, dsdy);
- mLastDsDx = dsdx;
- mLastDsDy = dsdy;
- updateBgPosition();
- }
-
- @Override
- public void hide() {
- super.hide();
- mVisible = false;
- updateBackgroundVisibility();
- }
-
- @Override
- public void show() {
- super.show();
- mVisible = true;
- updateBackgroundVisibility();
- }
-
- @Override
- public void destroy() {
- super.destroy();
-
- if (mBackgroundControl == null) {
- return;
- }
- mBackgroundControl.destroy();
- }
-
- @Override
- public void release() {
- super.release();
-
- if (mBackgroundControl == null) {
- return;
- }
- mBackgroundControl.release();
- }
-
- @Override
- public void setTransparentRegionHint(Region region) {
- super.setTransparentRegionHint(region);
-
- if (mBackgroundControl == null) {
- return;
- }
- mBackgroundControl.setTransparentRegionHint(region);
- }
-
- @Override
- public void deferTransactionUntil(IBinder handle, long frame) {
- super.deferTransactionUntil(handle, frame);
-
- if (mBackgroundControl == null) {
- return;
- }
- mBackgroundControl.deferTransactionUntil(handle, frame);
- }
-
- @Override
- public void deferTransactionUntil(Surface barrier, long frame) {
- super.deferTransactionUntil(barrier, frame);
-
- if (mBackgroundControl == null) {
- return;
- }
- mBackgroundControl.deferTransactionUntil(barrier, frame);
- }
-
- private void updateBackgroundVisibility() {
- if (mBackgroundControl == null) {
- return;
- }
- final AppWindowToken appWindowToken = mWindowSurfaceController.mAnimator.mWin.mAppToken;
- if (!mHiddenForCrop && mVisible && appWindowToken != null && appWindowToken.fillsParent()) {
- mBackgroundControl.show();
- } else {
- mBackgroundControl.hide();
- }
- }
-}
diff --git a/com/android/server/wm/TapExcludeRegionHolder.java b/com/android/server/wm/TapExcludeRegionHolder.java
new file mode 100644
index 00000000..cbc936f2
--- /dev/null
+++ b/com/android/server/wm/TapExcludeRegionHolder.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 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 com.android.server.wm;
+
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.util.SparseArray;
+
+/**
+ * A holder that contains a collection of rectangular areas identified by int id. Each individual
+ * region can be updated separately.
+ */
+class TapExcludeRegionHolder {
+ private SparseArray<Rect> mTapExcludeRects = new SparseArray<>();
+
+ /** Update the specified region with provided position and size. */
+ void updateRegion(int regionId, int left, int top, int width, int height) {
+ if (width <= 0 || height <= 0) {
+ // A region became empty - remove it.
+ mTapExcludeRects.remove(regionId);
+ return;
+ }
+
+ Rect region = mTapExcludeRects.get(regionId);
+ if (region == null) {
+ region = new Rect();
+ }
+ region.set(left, top, left + width, top + height);
+ mTapExcludeRects.put(regionId, region);
+ }
+
+ /**
+ * Union the provided region with current region formed by this container.
+ */
+ void amendRegion(Region region, Rect boundingRegion) {
+ for (int i = mTapExcludeRects.size() - 1; i>= 0 ; --i) {
+ final Rect rect = mTapExcludeRects.valueAt(i);
+ rect.intersect(boundingRegion);
+ region.union(rect);
+ }
+ }
+}
diff --git a/com/android/server/wm/Task.java b/com/android/server/wm/Task.java
index 3c96ca17..0628436a 100644
--- a/com/android/server/wm/Task.java
+++ b/com/android/server/wm/Task.java
@@ -22,13 +22,13 @@ 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 com.android.server.EventLogTags.WM_TASK_REMOVED;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STACK;
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.TaskProto.APP_WINDOW_TOKENS;
import static com.android.server.wm.proto.TaskProto.BOUNDS;
+import static com.android.server.wm.proto.TaskProto.DEFER_REMOVAL;
import static com.android.server.wm.proto.TaskProto.FILLS_PARENT;
import static com.android.server.wm.proto.TaskProto.ID;
import static com.android.server.wm.proto.TaskProto.TEMP_INSET_BOUNDS;
@@ -670,6 +670,7 @@ class Task extends WindowContainer<AppWindowToken> {
proto.write(FILLS_PARENT, matchParentBounds());
getBounds().writeToProto(proto, BOUNDS);
mTempInsetBounds.writeToProto(proto, TEMP_INSET_BOUNDS);
+ proto.write(DEFER_REMOVAL, mDeferRemoval);
proto.end(token);
}
diff --git a/com/android/server/wm/TaskPositioner.java b/com/android/server/wm/TaskPositioner.java
index fa7ea2ff..26c87b73 100644
--- a/com/android/server/wm/TaskPositioner.java
+++ b/com/android/server/wm/TaskPositioner.java
@@ -59,6 +59,8 @@ class TaskPositioner {
private static final String TAG_LOCAL = "TaskPositioner";
private static final String TAG = TAG_WITH_CLASS_NAME ? TAG_LOCAL : TAG_WM;
+ private static Factory sFactory;
+
// The margin the pointer position has to be within the side of the screen to be
// considered at the side of the screen.
static final int SIDE_MARGIN_DIP = 100;
@@ -214,6 +216,7 @@ class TaskPositioner {
}
}
+ /** Use {@link #create(WindowManagerService)} instead **/
TaskPositioner(WindowManagerService service) {
mService = service;
}
@@ -622,4 +625,22 @@ class TaskPositioner {
public String toShortString() {
return TAG;
}
+
+ static void setFactory(Factory factory) {
+ sFactory = factory;
+ }
+
+ static TaskPositioner create(WindowManagerService service) {
+ if (sFactory == null) {
+ sFactory = new Factory() {};
+ }
+
+ return sFactory.create(service);
+ }
+
+ interface Factory {
+ default TaskPositioner create(WindowManagerService service) {
+ return new TaskPositioner(service);
+ }
+ }
}
diff --git a/com/android/server/wm/TaskPositioningController.java b/com/android/server/wm/TaskPositioningController.java
index 4dfe290c..a3f4ee80 100644
--- a/com/android/server/wm/TaskPositioningController.java
+++ b/com/android/server/wm/TaskPositioningController.java
@@ -126,7 +126,7 @@ class TaskPositioningController {
}
Display display = displayContent.getDisplay();
- mTaskPositioner = new TaskPositioner(mService);
+ mTaskPositioner = TaskPositioner.create(mService);
mTaskPositioner.register(displayContent);
mInputMonitor.updateInputWindowsLw(true /*force*/);
diff --git a/com/android/server/wm/TaskStack.java b/com/android/server/wm/TaskStack.java
index 3ffc7fae..bc0f9ad7 100644
--- a/com/android/server/wm/TaskStack.java
+++ b/com/android/server/wm/TaskStack.java
@@ -16,8 +16,8 @@
package com.android.server.wm;
-import static android.app.ActivityManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
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.WindowConfiguration.ACTIVITY_TYPE_ASSISTANT;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
@@ -31,19 +31,25 @@ 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.wm.DragResizeMode.DRAG_RESIZE_MODE_DOCKED_DIVIDER;
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.ADJUSTED_BOUNDS;
+import static com.android.server.wm.proto.StackProto.ADJUSTED_FOR_IME;
+import static com.android.server.wm.proto.StackProto.ADJUST_DIVIDER_AMOUNT;
+import static com.android.server.wm.proto.StackProto.ADJUST_IME_AMOUNT;
import static com.android.server.wm.proto.StackProto.ANIMATION_BACKGROUND_SURFACE_IS_DIMMING;
import static com.android.server.wm.proto.StackProto.BOUNDS;
+import static com.android.server.wm.proto.StackProto.DEFER_REMOVAL;
import static com.android.server.wm.proto.StackProto.FILLS_PARENT;
import static com.android.server.wm.proto.StackProto.ID;
+import static com.android.server.wm.proto.StackProto.MINIMIZE_AMOUNT;
import static com.android.server.wm.proto.StackProto.TASKS;
import static com.android.server.wm.proto.StackProto.WINDOW_CONTAINER;
import android.annotation.CallSuper;
import android.content.res.Configuration;
+import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.Region;
import android.os.RemoteException;
@@ -145,6 +151,7 @@ public class TaskStack extends WindowContainer<Task> implements
* For {@link #prepareSurfaces}.
*/
final Rect mTmpDimBoundsRect = new Rect();
+ private final Point mLastSurfaceSize = new Point();
TaskStack(WindowManagerService service, int stackId, StackWindowController controller) {
super(service);
@@ -714,10 +721,11 @@ public class TaskStack extends WindowContainer<Task> implements
@Override
public void onConfigurationChanged(Configuration newParentConfig) {
final int prevWindowingMode = getWindowingMode();
+ super.onConfigurationChanged(newParentConfig);
+
// 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) {
@@ -743,7 +751,13 @@ public class TaskStack extends WindowContainer<Task> implements
}
final Rect stackBounds = getBounds();
- transaction.setSize(mSurfaceControl, stackBounds.width(), stackBounds.height());
+ final int width = stackBounds.width();
+ final int height = stackBounds.height();
+ if (width == mLastSurfaceSize.x && height == mLastSurfaceSize.y) {
+ return;
+ }
+ transaction.setSize(mSurfaceControl, width, height);
+ mLastSurfaceSize.set(width, height);
}
@Override
@@ -1307,6 +1321,12 @@ public class TaskStack extends WindowContainer<Task> implements
proto.write(FILLS_PARENT, matchParentBounds());
getRawBounds().writeToProto(proto, BOUNDS);
proto.write(ANIMATION_BACKGROUND_SURFACE_IS_DIMMING, mAnimationBackgroundSurfaceIsShown);
+ proto.write(DEFER_REMOVAL, mDeferRemoval);
+ proto.write(MINIMIZE_AMOUNT, mMinimizeAmount);
+ proto.write(ADJUSTED_FOR_IME, mAdjustedForIme);
+ proto.write(ADJUST_IME_AMOUNT, mAdjustImeAmount);
+ proto.write(ADJUST_DIVIDER_AMOUNT, mAdjustDividerAmount);
+ mAdjustedBounds.writeToProto(proto, ADJUSTED_BOUNDS);
proto.end(token);
}
@@ -1377,15 +1397,23 @@ public class TaskStack extends WindowContainer<Task> implements
return getDockSide(getRawBounds());
}
+ int getDockSideForDisplay(DisplayContent dc) {
+ return getDockSide(dc, getRawBounds());
+ }
+
private int getDockSide(Rect bounds) {
- if (!inSplitScreenWindowingMode()) {
+ if (mDisplayContent == null) {
return DOCKED_INVALID;
}
- if (mDisplayContent == null) {
+ return getDockSide(mDisplayContent, bounds);
+ }
+
+ private int getDockSide(DisplayContent dc, Rect bounds) {
+ if (!inSplitScreenWindowingMode()) {
return DOCKED_INVALID;
}
- mDisplayContent.getBounds(mTmpRect);
- final int orientation = mDisplayContent.getConfiguration().orientation;
+ dc.getBounds(mTmpRect);
+ final int orientation = dc.getConfiguration().orientation;
return getDockSideUnchecked(bounds, mTmpRect, orientation);
}
diff --git a/com/android/server/wm/WallpaperController.java b/com/android/server/wm/WallpaperController.java
index ac0919d9..f2ad6fb7 100644
--- a/com/android/server/wm/WallpaperController.java
+++ b/com/android/server/wm/WallpaperController.java
@@ -25,7 +25,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
-import static com.android.server.wm.AppTransition.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_APP_TRANSITIONS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER_LIGHT;
@@ -149,8 +149,17 @@ class WallpaperController {
mFindResults.setUseTopWallpaperAsTarget(true);
}
+ final RecentsAnimationController recentsAnimationController =
+ mService.getRecentsAnimationController();
final boolean hasWallpaper = (w.mAttrs.flags & FLAG_SHOW_WALLPAPER) != 0;
- if (hasWallpaper && w.isOnScreen() && (mWallpaperTarget == w || w.isDrawFinishedLw())) {
+ final boolean isRecentsTransitionTarget = (recentsAnimationController != null
+ && recentsAnimationController.isWallpaperVisible(w));
+ if (isRecentsTransitionTarget) {
+ if (DEBUG_WALLPAPER) Slog.v(TAG, "Found recents animation wallpaper target: " + w);
+ mFindResults.setWallpaperTarget(w);
+ return true;
+ } else if (hasWallpaper && w.isOnScreen()
+ && (mWallpaperTarget == w || w.isDrawFinishedLw())) {
if (DEBUG_WALLPAPER) Slog.v(TAG, "Found wallpaper target: " + w);
mFindResults.setWallpaperTarget(w);
if (w == mWallpaperTarget && w.mWinAnimator.isAnimationSet()) {
@@ -199,15 +208,22 @@ class WallpaperController {
}
}
- private boolean isWallpaperVisible(WindowState wallpaperTarget) {
+ private final boolean isWallpaperVisible(WindowState wallpaperTarget) {
+ final RecentsAnimationController recentsAnimationController =
+ mService.getRecentsAnimationController();
+ boolean isAnimatingWithRecentsComponent = recentsAnimationController != null
+ && recentsAnimationController.isWallpaperVisible(wallpaperTarget);
if (DEBUG_WALLPAPER) Slog.v(TAG, "Wallpaper vis: target " + wallpaperTarget + ", obscured="
+ (wallpaperTarget != null ? Boolean.toString(wallpaperTarget.mObscured) : "??")
+ " animating=" + ((wallpaperTarget != null && wallpaperTarget.mAppToken != null)
? wallpaperTarget.mAppToken.isSelfAnimating() : null)
- + " prev=" + mPrevWallpaperTarget);
+ + " prev=" + mPrevWallpaperTarget
+ + " recentsAnimationWallpaperVisible=" + isAnimatingWithRecentsComponent);
return (wallpaperTarget != null
- && (!wallpaperTarget.mObscured || (wallpaperTarget.mAppToken != null
- && wallpaperTarget.mAppToken.isSelfAnimating())))
+ && (!wallpaperTarget.mObscured
+ || isAnimatingWithRecentsComponent
+ || (wallpaperTarget.mAppToken != null
+ && wallpaperTarget.mAppToken.isSelfAnimating())))
|| mPrevWallpaperTarget != null;
}
@@ -587,6 +603,11 @@ class WallpaperController {
mWallpaperDrawState = WALLPAPER_DRAW_TIMEOUT;
if (DEBUG_APP_TRANSITIONS || DEBUG_WALLPAPER) Slog.v(TAG,
"*** WALLPAPER DRAW TIMEOUT");
+
+ // If there was a recents animation in progress, cancel that animation
+ if (mService.getRecentsAnimationController() != null) {
+ mService.getRecentsAnimationController().cancelAnimation();
+ }
return true;
}
return false;
diff --git a/com/android/server/wm/WindowAnimationSpec.java b/com/android/server/wm/WindowAnimationSpec.java
index 98652934..0863ee9e 100644
--- a/com/android/server/wm/WindowAnimationSpec.java
+++ b/com/android/server/wm/WindowAnimationSpec.java
@@ -89,11 +89,14 @@ public class WindowAnimationSpec implements AnimationSpec {
if (mStackClipMode == STACK_CLIP_NONE) {
t.setWindowCrop(leash, tmp.transformation.getClipRect());
} else if (mStackClipMode == STACK_CLIP_AFTER_ANIM) {
- t.setFinalCrop(leash, mStackBounds);
+ mTmpRect.set(mStackBounds);
+ // Offset stack bounds to stack position so the final crop is in screen space.
+ mTmpRect.offsetTo(mPosition.x, mPosition.y);
+ t.setFinalCrop(leash, mTmpRect);
t.setWindowCrop(leash, tmp.transformation.getClipRect());
} else {
- mTmpRect.set(tmp.transformation.getClipRect());
- mTmpRect.intersect(mStackBounds);
+ mTmpRect.set(mStackBounds);
+ mTmpRect.intersect(tmp.transformation.getClipRect());
t.setWindowCrop(leash, mTmpRect);
}
}
diff --git a/com/android/server/wm/WindowAnimator.java b/com/android/server/wm/WindowAnimator.java
index 8bceb640..cec13abd 100644
--- a/com/android/server/wm/WindowAnimator.java
+++ b/com/android/server/wm/WindowAnimator.java
@@ -35,6 +35,7 @@ import com.android.server.AnimationThread;
import com.android.server.policy.WindowManagerPolicy;
import java.io.PrintWriter;
+import java.util.ArrayList;
/**
* Singleton class that carries out the animations and Surface operations in a separate task
@@ -46,7 +47,6 @@ public class WindowAnimator {
final WindowManagerService mService;
final Context mContext;
final WindowManagerPolicy mPolicy;
- private final WindowSurfacePlacer mWindowPlacerLocked;
/** Is any window animating? */
private boolean mAnimating;
@@ -73,7 +73,7 @@ public class WindowAnimator {
SparseArray<DisplayContentsAnimator> mDisplayContentsAnimators = new SparseArray<>(2);
- boolean mInitialized = false;
+ private boolean mInitialized = false;
// When set to true the animator will go over all windows after an animation frame is posted and
// check if some got replaced and can be removed.
@@ -87,11 +87,16 @@ public class WindowAnimator {
*/
private boolean mAnimationFrameCallbackScheduled;
+ /**
+ * A list of runnable that need to be run after {@link WindowContainer#prepareSurfaces} is
+ * executed and the corresponding transaction is closed and applied.
+ */
+ private final ArrayList<Runnable> mAfterPrepareSurfacesRunnables = new ArrayList<>();
+
WindowAnimator(final WindowManagerService service) {
mService = service;
mContext = service.mContext;
mPolicy = service.mPolicy;
- mWindowPlacerLocked = service.mWindowPlacerLocked;
AnimationThread.getHandler().runWithScissors(
() -> mChoreographer = Choreographer.getSfInstance(), 0 /* timeout */);
@@ -234,7 +239,7 @@ public class WindowAnimator {
}
if (hasPendingLayoutChanges || doRequest) {
- mWindowPlacerLocked.requestTraversal();
+ mService.mWindowPlacerLocked.requestTraversal();
}
final boolean rootAnimating = mService.mRoot.isSelfOrChildAnimating();
@@ -247,7 +252,7 @@ public class WindowAnimator {
Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "animating", 0);
}
if (!rootAnimating && mLastRootAnimating) {
- mWindowPlacerLocked.requestTraversal();
+ mService.mWindowPlacerLocked.requestTraversal();
mService.mTaskSnapshotController.setPersisterPaused(false);
Trace.asyncTraceEnd(Trace.TRACE_TAG_WINDOW_MANAGER, "animating", 0);
}
@@ -262,6 +267,7 @@ public class WindowAnimator {
mService.destroyPreservedSurfaceLocked();
mService.mWindowPlacerLocked.destroyPendingSurfaces();
+ executeAfterPrepareSurfacesRunnables();
if (DEBUG_WINDOW_TRACE) {
Slog.i(TAG, "!!! animate: exit mAnimating=" + mAnimating
@@ -286,9 +292,6 @@ public class WindowAnimator {
if ((bulkUpdateParams & WindowSurfacePlacer.SET_ORIENTATION_CHANGE_COMPLETE) != 0) {
builder.append(" ORIENTATION_CHANGE_COMPLETE");
}
- if ((bulkUpdateParams & WindowSurfacePlacer.SET_TURN_ON_SCREEN) != 0) {
- builder.append(" TURN_ON_SCREEN");
- }
return builder.toString();
}
@@ -425,4 +428,23 @@ public class WindowAnimator {
void orAnimating(boolean animating) {
mAnimating |= animating;
}
+
+ /**
+ * Adds a runnable to be executed after {@link WindowContainer#prepareSurfaces} is called and
+ * the corresponding transaction is closed and applied.
+ */
+ void addAfterPrepareSurfacesRunnable(Runnable r) {
+ mAfterPrepareSurfacesRunnables.add(r);
+ scheduleAnimation();
+ }
+
+ private void executeAfterPrepareSurfacesRunnables() {
+
+ // Traverse in order they were added.
+ final int size = mAfterPrepareSurfacesRunnables.size();
+ for (int i = 0; i < size; i++) {
+ mAfterPrepareSurfacesRunnables.get(i).run();
+ }
+ mAfterPrepareSurfacesRunnables.clear();
+ }
}
diff --git a/com/android/server/wm/WindowContainer.java b/com/android/server/wm/WindowContainer.java
index b251b53b..a0b59efe 100644
--- a/com/android/server/wm/WindowContainer.java
+++ b/com/android/server/wm/WindowContainer.java
@@ -19,26 +19,26 @@ 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 android.view.SurfaceControl.Transaction;
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.SURFACE_ANIMATOR;
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.Pools;
import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
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;
@@ -95,6 +95,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
protected final WindowManagerService mService;
private final Point mTmpPos = new Point();
+ protected final Point mLastSurfacePosition = new Point();
/** Total number of elements in this subtree, including our own hierarchy element. */
private int mTreeWeight = 1;
@@ -102,7 +103,8 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
WindowContainer(WindowManagerService service) {
mService = service;
mPendingTransaction = service.mTransactionFactory.make();
- mSurfaceAnimator = new SurfaceAnimator(this, this::onAnimationFinished, service);
+ mSurfaceAnimator = new SurfaceAnimator(this, this::onAnimationFinished,
+ service.mAnimator::addAfterPrepareSurfacesRunnable, service);
}
@Override
@@ -335,9 +337,9 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
/** Returns true if this window container has the input child. */
- boolean hasChild(WindowContainer child) {
+ boolean hasChild(E child) {
for (int i = mChildren.size() - 1; i >= 0; --i) {
- final WindowContainer current = mChildren.get(i);
+ final E current = mChildren.get(i);
if (current == child || current.hasChild(child)) {
return true;
}
@@ -970,6 +972,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
super.writeToProto(proto, CONFIGURATION_CONTAINER, trim);
proto.write(ORIENTATION, mOrientation);
proto.write(VISIBLE, isVisible());
+ mSurfaceAnimator.writeToProto(proto, SURFACE_ANIMATOR);
proto.end(token);
}
@@ -1088,6 +1091,11 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
return makeSurface();
}
+ @Override
+ public SurfaceControl getAnimationLeashParent() {
+ return getParentSurfaceControl();
+ }
+
/**
* @return The layer on which all app animations are happening.
*/
@@ -1172,7 +1180,12 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer<
}
getRelativePosition(mTmpPos);
+ if (mTmpPos.equals(mLastSurfacePosition)) {
+ return;
+ }
+
transaction.setPosition(mSurfaceControl, mTmpPos.x, mTmpPos.y);
+ mLastSurfacePosition.set(mTmpPos.x, mTmpPos.y);
for (int i = mChildren.size() - 1; i >= 0; i--) {
mChildren.get(i).updateSurfacePosition(transaction);
diff --git a/com/android/server/wm/WindowManagerService.java b/com/android/server/wm/WindowManagerService.java
index 0a2ffbc9..4fb23908 100644
--- a/com/android/server/wm/WindowManagerService.java
+++ b/com/android/server/wm/WindowManagerService.java
@@ -16,6 +16,7 @@
package com.android.server.wm;
+import static android.Manifest.permission.CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS;
import static android.Manifest.permission.MANAGE_APP_TOKENS;
import static android.Manifest.permission.READ_FRAME_BUFFER;
import static android.Manifest.permission.REGISTER_WINDOW_MANAGER_LISTENERS;
@@ -23,6 +24,8 @@ import static android.Manifest.permission.RESTRICTED_VR_ACCESS;
import static android.app.ActivityManager.SPLIT_SCREEN_CREATE_MODE_TOP_OR_LEFT;
import static android.app.AppOpsManager.OP_SYSTEM_ALERT_WINDOW;
import static android.app.StatusBarManager.DISABLE_MASK;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
+import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
import static android.app.admin.DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED;
import static android.content.Intent.ACTION_USER_REMOVED;
import static android.content.Intent.EXTRA_USER_HANDLE;
@@ -87,7 +90,6 @@ 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_SCREEN_ON;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_STARTING_WINDOW;
-import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_TASK_POSITIONING;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_VISIBILITY;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WALLPAPER_LIGHT;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WINDOW_MOVEMENT;
@@ -123,6 +125,7 @@ import android.app.ActivityThread;
import android.app.AppOpsManager;
import android.app.IActivityManager;
import android.app.IAssistDataReceiver;
+import android.app.WindowConfiguration;
import android.content.BroadcastReceiver;
import android.content.ContentResolver;
import android.content.Context;
@@ -196,6 +199,7 @@ import android.view.IDockedStackListener;
import android.view.IInputFilter;
import android.view.IOnKeyguardExitResult;
import android.view.IPinnedStackListener;
+import android.view.IRecentsAnimationRunner;
import android.view.IRotationWatcher;
import android.view.IWallpaperVisibilityListener;
import android.view.IWindow;
@@ -210,17 +214,18 @@ import android.view.KeyEvent;
import android.view.MagnificationSpec;
import android.view.MotionEvent;
import android.view.PointerIcon;
+import android.view.RemoteAnimationAdapter;
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;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
+import android.view.WindowManager.TransitionFlags;
+import android.view.WindowManager.TransitionType;
import android.view.WindowManagerGlobal;
import android.view.WindowManagerPolicyConstants.PointerEventListener;
-import android.view.animation.Animation;
import android.view.inputmethod.InputMethodManagerInternal;
import com.android.internal.R;
@@ -527,6 +532,7 @@ public class WindowManagerService extends IWindowManager.Stub
IInputMethodManager mInputMethodManager;
AccessibilityController mAccessibilityController;
+ private RecentsAnimationController mRecentsAnimationController;
Watermark mWatermark;
StrictModeFlash mStrictModeFlash;
@@ -928,7 +934,6 @@ public class WindowManagerService extends IWindowManager.Stub
boolean haveInputMethods, boolean showBootMsgs, boolean onlyCore,
WindowManagerPolicy policy) {
installLock(this, INDEX_WINDOW);
- mRoot = new RootWindowContainer(this);
mContext = context;
mHaveInputMethods = haveInputMethods;
mAllowBootMessages = showBootMsgs;
@@ -952,8 +957,11 @@ public class WindowManagerService extends IWindowManager.Stub
mDisplaySettings = new DisplaySettings();
mDisplaySettings.readSettingsLocked();
- mWindowPlacerLocked = new WindowSurfacePlacer(this);
mPolicy = policy;
+ mAnimator = new WindowAnimator(this);
+ mRoot = new RootWindowContainer(this);
+
+ mWindowPlacerLocked = new WindowSurfacePlacer(this);
mTaskSnapshotController = new TaskSnapshotController(this);
mWindowTracing = WindowTracing.createDefaultAndStartLooper(context);
@@ -1051,7 +1059,6 @@ public class WindowManagerService extends IWindowManager.Stub
PowerManager.SCREEN_BRIGHT_WAKE_LOCK | PowerManager.ON_AFTER_RELEASE, TAG_WM);
mHoldingScreenWakeLock.setReferenceCounted(false);
- mAnimator = new WindowAnimator(this);
mSurfaceAnimationRunner = new SurfaceAnimationRunner();
mAllowTheaterModeWakeFromLayout = context.getResources().getBoolean(
@@ -1539,7 +1546,7 @@ public class WindowManagerService extends IWindowManager.Stub
// We treat this as if this activity was opening, so we can trigger the app transition
// animation and piggy-back on existing transition animation infrastructure.
mOpeningApps.add(atoken);
- prepareAppTransition(AppTransition.TRANSIT_ACTIVITY_RELAUNCH, ALWAYS_KEEP_CURRENT);
+ prepareAppTransition(WindowManager.TRANSIT_ACTIVITY_RELAUNCH, ALWAYS_KEEP_CURRENT);
mAppTransition.overridePendingAppTransitionClipReveal(frame.left, frame.top,
frame.width(), frame.height());
executeAppTransition();
@@ -1553,7 +1560,7 @@ public class WindowManagerService extends IWindowManager.Stub
// we don't set up the transition anymore and just let it go.
if (mDisplayFrozen && !mOpeningApps.contains(atoken) && atoken.isRelaunching()) {
mOpeningApps.add(atoken);
- prepareAppTransition(AppTransition.TRANSIT_NONE, !ALWAYS_KEEP_CURRENT);
+ prepareAppTransition(WindowManager.TRANSIT_NONE, !ALWAYS_KEEP_CURRENT);
executeAppTransition();
}
}
@@ -2508,7 +2515,7 @@ public class WindowManagerService extends IWindowManager.Stub
}
@Override
- public void prepareAppTransition(int transit, boolean alwaysKeepCurrent) {
+ public void prepareAppTransition(@TransitionType int transit, boolean alwaysKeepCurrent) {
prepareAppTransition(transit, alwaysKeepCurrent, 0 /* flags */, false /* forceOverride */);
}
@@ -2521,8 +2528,8 @@ public class WindowManagerService extends IWindowManager.Stub
* AppTransition.TRANSIT_FLAG_*.
* @param forceOverride Always override the transit, not matter what was set previously.
*/
- public void prepareAppTransition(int transit, boolean alwaysKeepCurrent, int flags,
- boolean forceOverride) {
+ public void prepareAppTransition(@TransitionType int transit, boolean alwaysKeepCurrent,
+ @TransitionFlags int flags, boolean forceOverride) {
if (!checkCallingPermission(MANAGE_APP_TOKENS, "prepareAppTransition()")) {
throw new SecurityException("Requires MANAGE_APP_TOKENS permission");
}
@@ -2538,7 +2545,7 @@ public class WindowManagerService extends IWindowManager.Stub
}
@Override
- public int getPendingAppTransition() {
+ public @TransitionType int getPendingAppTransition() {
return mAppTransition.getAppTransition();
}
@@ -2623,6 +2630,18 @@ public class WindowManagerService extends IWindowManager.Stub
}
@Override
+ public void overridePendingAppTransitionRemote(RemoteAnimationAdapter remoteAnimationAdapter) {
+ if (!checkCallingPermission(CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS,
+ "overridePendingAppTransitionRemote()")) {
+ throw new SecurityException(
+ "Requires CONTROL_REMOTE_APP_TRANSITION_ANIMATIONS permission");
+ }
+ synchronized (mWindowMap) {
+ mAppTransition.overridePendingAppTransitionRemote(remoteAnimationAdapter);
+ }
+ }
+
+ @Override
public void endProlongedAnimations() {
synchronized (mWindowMap) {
for (final WindowState win : mWindowMap.values()) {
@@ -2656,6 +2675,39 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
+ public void initializeRecentsAnimation(
+ IRecentsAnimationRunner recentsAnimationRunner,
+ RecentsAnimationController.RecentsAnimationCallbacks callbacks, int displayId) {
+ synchronized (mWindowMap) {
+ cancelRecentsAnimation();
+ mRecentsAnimationController = new RecentsAnimationController(this,
+ recentsAnimationRunner, callbacks, displayId);
+ }
+ }
+
+ public RecentsAnimationController getRecentsAnimationController() {
+ return mRecentsAnimationController;
+ }
+
+ public void cancelRecentsAnimation() {
+ synchronized (mWindowMap) {
+ if (mRecentsAnimationController != null) {
+ // This call will call through to cleanupAnimation() below after the animation is
+ // canceled
+ mRecentsAnimationController.cancelAnimation();
+ }
+ }
+ }
+
+ public void cleanupRecentsAnimation() {
+ synchronized (mWindowMap) {
+ if (mRecentsAnimationController != null) {
+ mRecentsAnimationController.cleanupAnimation();
+ mRecentsAnimationController = null;
+ }
+ }
+ }
+
public void setAppFullscreen(IBinder token, boolean toOpaque) {
synchronized (mWindowMap) {
final AppWindowToken atoken = mRoot.getAppWindowToken(token);
@@ -2940,10 +2992,10 @@ public class WindowManagerService extends IWindowManager.Stub
}
@Override
- public void dismissKeyguard(IKeyguardDismissCallback callback) {
+ public void dismissKeyguard(IKeyguardDismissCallback callback, CharSequence message) {
checkCallingPermission(permission.CONTROL_KEYGUARD, "dismissKeyguard");
synchronized(mWindowMap) {
- mPolicy.dismissKeyguardLw(callback);
+ mPolicy.dismissKeyguardLw(callback, message);
}
}
@@ -5326,6 +5378,25 @@ public class WindowManagerService extends IWindowManager.Stub
reconfigureDisplayLocked(displayContent);
}
+ @Override
+ public void startWindowTrace(){
+ try {
+ mWindowTracing.startTrace(null /* printwriter */);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public void stopWindowTrace(){
+ mWindowTracing.stopTrace(null /* printwriter */);
+ }
+
+ @Override
+ public boolean isWindowTraceEnabled() {
+ return mWindowTracing.isEnabled();
+ }
+
// -------------------------------------------------------------
// Internals
// -------------------------------------------------------------
@@ -5874,6 +5945,8 @@ public class WindowManagerService extends IWindowManager.Stub
* the screen is.
* @see WindowManagerPolicy#getNavBarPosition()
*/
+ @Override
+ @WindowManagerPolicy.NavigationBarPosition
public int getNavBarPosition() {
synchronized (mWindowMap) {
// Perform layout if it was scheduled before to make sure that we get correct nav bar
@@ -5932,8 +6005,8 @@ public class WindowManagerService extends IWindowManager.Stub
mPolicy.lockNow(options);
}
- public void showRecentApps(boolean fromHome) {
- mPolicy.showRecentApps(fromHome);
+ public void showRecentApps() {
+ mPolicy.showRecentApps();
}
@Override
@@ -6292,6 +6365,10 @@ public class WindowManagerService extends IWindowManager.Stub
pw.print(" mSkipAppTransitionAnimation=");pw.println(mSkipAppTransitionAnimation);
pw.println(" mLayoutToAnim:");
mAppTransition.dump(pw, " ");
+ if (mRecentsAnimationController != null) {
+ pw.print(" mRecentsAnimationController="); pw.println(mRecentsAnimationController);
+ mRecentsAnimationController.dump(pw, " ");
+ }
}
}
@@ -6575,6 +6652,14 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
+ public void onOverlayChanged() {
+ synchronized (mWindowMap) {
+ mPolicy.onOverlayChangedLw();
+ getDefaultDisplayContentLocked().updateDisplayInfo();
+ requestTraversal();
+ }
+ }
+
public void onDisplayChanged(int displayId) {
synchronized (mWindowMap) {
final DisplayContent displayContent = mRoot.getDisplayContentOrCreate(displayId);
@@ -6879,6 +6964,23 @@ public class WindowManagerService extends IWindowManager.Stub
}
}
+ /**
+ * Update a tap exclude region with a rectangular area in the window identified by the provided
+ * id. Touches on this region will not switch focus to this window. Passing an empty rect will
+ * remove the area from the exclude region of this window.
+ */
+ void updateTapExcludeRegion(IWindow client, int regionId, int left, int top, int width,
+ int height) {
+ synchronized (mWindowMap) {
+ final WindowState callingWin = windowForClientLocked(null, client, false);
+ if (callingWin == null) {
+ Slog.w(TAG_WM, "Bad requesting window " + client);
+ return;
+ }
+ callingWin.updateTapExcludeRegion(regionId, left, top, width, height);
+ }
+ }
+
@Override
public void registerShortcutKey(long shortcutCode, IShortcutService shortcutKeyReceiver)
throws RemoteException {
diff --git a/com/android/server/wm/WindowManagerShellCommand.java b/com/android/server/wm/WindowManagerShellCommand.java
index b9dc9db7..e24c3938 100644
--- a/com/android/server/wm/WindowManagerShellCommand.java
+++ b/com/android/server/wm/WindowManagerShellCommand.java
@@ -231,7 +231,7 @@ public class WindowManagerShellCommand extends ShellCommand {
}
private int runDismissKeyguard(PrintWriter pw) throws RemoteException {
- mInterface.dismissKeyguard(null /* callback */);
+ mInterface.dismissKeyguard(null /* callback */, null /* message */);
return 0;
}
diff --git a/com/android/server/wm/WindowState.java b/com/android/server/wm/WindowState.java
index dfb385bd..477dd2bb 100644
--- a/com/android/server/wm/WindowState.java
+++ b/com/android/server/wm/WindowState.java
@@ -20,6 +20,7 @@ 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.View.SYSTEM_UI_FLAG_FULLSCREEN;
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;
@@ -30,6 +31,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCRE
import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
import static android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND;
import static android.view.WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD;
+import static android.view.WindowManager.LayoutParams.FLAG_FULLSCREEN;
import static android.view.WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
@@ -39,6 +41,8 @@ import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
import static android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON;
import static android.view.WindowManager.LayoutParams.FORMAT_CHANGED;
import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
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;
@@ -48,8 +52,8 @@ import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_WILL_NOT_REPL
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_MEDIA_OVERLAY;
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;
@@ -112,15 +116,36 @@ import static com.android.server.wm.proto.WindowStateProto.CHILD_WINDOWS;
import static com.android.server.wm.proto.WindowStateProto.CONTAINING_FRAME;
import static com.android.server.wm.proto.WindowStateProto.CONTENT_FRAME;
import static com.android.server.wm.proto.WindowStateProto.CONTENT_INSETS;
+import static com.android.server.wm.proto.WindowStateProto.CUTOUT;
+import static com.android.server.wm.proto.WindowStateProto.DECOR_FRAME;
+import static com.android.server.wm.proto.WindowStateProto.DESTROYING;
+import static com.android.server.wm.proto.WindowStateProto.DISPLAY_FRAME;
import static com.android.server.wm.proto.WindowStateProto.DISPLAY_ID;
import static com.android.server.wm.proto.WindowStateProto.FRAME;
import static com.android.server.wm.proto.WindowStateProto.GIVEN_CONTENT_INSETS;
+import static com.android.server.wm.proto.WindowStateProto.HAS_SURFACE;
import static com.android.server.wm.proto.WindowStateProto.IDENTIFIER;
+import static com.android.server.wm.proto.WindowStateProto.IS_ON_SCREEN;
+import static com.android.server.wm.proto.WindowStateProto.IS_READY_FOR_DISPLAY;
+import static com.android.server.wm.proto.WindowStateProto.IS_VISIBLE;
+import static com.android.server.wm.proto.WindowStateProto.OUTSETS;
+import static com.android.server.wm.proto.WindowStateProto.OUTSET_FRAME;
+import static com.android.server.wm.proto.WindowStateProto.OVERSCAN_FRAME;
+import static com.android.server.wm.proto.WindowStateProto.OVERSCAN_INSETS;
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.REMOVED;
+import static com.android.server.wm.proto.WindowStateProto.REMOVE_ON_EXIT;
+import static com.android.server.wm.proto.WindowStateProto.REQUESTED_HEIGHT;
+import static com.android.server.wm.proto.WindowStateProto.REQUESTED_WIDTH;
import static com.android.server.wm.proto.WindowStateProto.SHOWN_POSITION;
+import static com.android.server.wm.proto.WindowStateProto.STABLE_INSETS;
+import static com.android.server.wm.proto.WindowStateProto.STACK_ID;
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.SYSTEM_UI_VISIBILITY;
+import static com.android.server.wm.proto.WindowStateProto.VIEW_VISIBILITY;
+import static com.android.server.wm.proto.WindowStateProto.VISIBLE_FRAME;
+import static com.android.server.wm.proto.WindowStateProto.VISIBLE_INSETS;
import static com.android.server.wm.proto.WindowStateProto.WINDOW_CONTAINER;
import android.annotation.CallSuper;
@@ -142,6 +167,7 @@ import android.os.SystemClock;
import android.os.Trace;
import android.os.UserHandle;
import android.os.WorkSource;
+import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.MergedConfiguration;
import android.util.Slog;
@@ -167,6 +193,7 @@ import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.ToBooleanFunction;
import com.android.server.input.InputWindowHandle;
import com.android.server.policy.WindowManagerPolicy;
@@ -176,7 +203,6 @@ import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Comparator;
-import java.util.LinkedList;
import java.util.function.Predicate;
/** A window in the window manager. */
@@ -604,6 +630,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
private final Point mSurfacePosition = new Point();
/**
+ * A region inside of this window to be excluded from touch-related focus switches.
+ */
+ private TapExcludeRegionHolder mTapExcludeRegionHolder;
+
+ /**
* Compares two window sub-layers and returns -1 if the first is lesser than the second in terms
* of z-order and 1 otherwise.
*/
@@ -1521,10 +1552,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
@Override
public boolean canAffectSystemUiFlags() {
final boolean translucent = mAttrs.alpha == 0.0f;
+ if (translucent) {
+ return false;
+ }
if (mAppToken == null) {
final boolean shown = mWinAnimator.getShown();
final boolean exiting = mAnimatingExit || mDestroying;
- return shown && !exiting && !translucent;
+ return shown && !exiting;
} else {
return !mAppToken.isHidden();
}
@@ -1841,6 +1875,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
if (WindowManagerService.excludeWindowTypeFromTapOutTask(type)) {
dc.mTapExcludedWindows.remove(this);
}
+ if (mTapExcludeRegionHolder != null) {
+ // If a tap exclude region container was initialized for this window, then it should've
+ // also been registered in display.
+ dc.mTapExcludeProvidingWindows.remove(this);
+ }
mPolicy.removeWindowLw(this);
disposeInputChannel();
@@ -2222,12 +2261,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mWinAnimator + ": " + mPolicyVisibilityAfterAnim);
}
mPolicyVisibility = mPolicyVisibilityAfterAnim;
- setDisplayLayoutNeeded();
if (!mPolicyVisibility) {
+ mWinAnimator.hide("checkPolicyVisibilityChange");
if (mService.mCurrentFocus == this) {
if (DEBUG_FOCUS_LIGHT) Slog.i(TAG,
"setAnimationLocked: setting mFocusMayChange true");
mService.mFocusMayChange = true;
+ setDisplayLayoutNeeded();
}
// Window is no longer visible -- make sure if we were waiting
// for it to be displayed before enabling the display, that
@@ -2641,8 +2681,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
boolean destroySurface(boolean cleanupOnResume, boolean appStopped) {
boolean destroyedSomething = false;
- for (int i = mChildren.size() - 1; i >= 0; --i) {
- final WindowState c = mChildren.get(i);
+
+ // Copying to a different list as multiple children can be removed.
+ final ArrayList<WindowState> childWindows = new ArrayList<>(mChildren);
+ for (int i = childWindows.size() - 1; i >= 0; --i) {
+ final WindowState c = childWindows.get(i);
destroyedSomething |= c.destroySurface(cleanupOnResume, appStopped);
}
@@ -2948,7 +2991,33 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
/** @return true when the window is in fullscreen task, but has non-fullscreen bounds set. */
boolean isLetterboxedAppWindow() {
- return !isInMultiWindowMode() && mAppToken != null && !mAppToken.matchParentBounds();
+ return !isInMultiWindowMode() && mAppToken != null && !mAppToken.matchParentBounds()
+ || isLetterboxedForDisplayCutoutLw();
+ }
+
+ @Override
+ public boolean isLetterboxedForDisplayCutoutLw() {
+ if (mAppToken == null) {
+ // Only windows with an AppWindowToken are letterboxed.
+ return false;
+ }
+ if (getDisplayContent().getDisplayInfo().displayCutout == null) {
+ // No cutout, no letterbox.
+ return false;
+ }
+ if (mAttrs.layoutInDisplayCutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS) {
+ // Layout in cutout, no letterbox.
+ return false;
+ }
+ // TODO: handle dialogs and other non-filling windows
+ if (mAttrs.layoutInDisplayCutoutMode == LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER) {
+ // Never layout in cutout, always letterbox.
+ return true;
+ }
+ // Letterbox if any fullscreen mode is set.
+ final int fl = mAttrs.flags;
+ final int sysui = mSystemUiVisibility;
+ return (fl & FLAG_FULLSCREEN) != 0 || (sysui & (SYSTEM_UI_FLAG_FULLSCREEN)) != 0;
}
boolean isDragResizeChanged() {
@@ -3083,10 +3152,32 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
for (int i = 0; i < mChildren.size(); i++) {
mChildren.get(i).writeToProto(proto, CHILD_WINDOWS, trim);
}
+ proto.write(REQUESTED_WIDTH, mRequestedWidth);
+ proto.write(REQUESTED_HEIGHT, mRequestedHeight);
+ proto.write(VIEW_VISIBILITY, mViewVisibility);
+ proto.write(SYSTEM_UI_VISIBILITY, mSystemUiVisibility);
+ proto.write(HAS_SURFACE, mHasSurface);
+ proto.write(IS_READY_FOR_DISPLAY, isReadyForDisplay());
+ mDisplayFrame.writeToProto(proto, DISPLAY_FRAME);
+ mOverscanFrame.writeToProto(proto, OVERSCAN_FRAME);
+ mVisibleFrame.writeToProto(proto, VISIBLE_FRAME);
+ mDecorFrame.writeToProto(proto, DECOR_FRAME);
+ mOutsetFrame.writeToProto(proto, OUTSET_FRAME);
+ mOverscanInsets.writeToProto(proto, OVERSCAN_INSETS);
+ mVisibleInsets.writeToProto(proto, VISIBLE_INSETS);
+ mStableInsets.writeToProto(proto, STABLE_INSETS);
+ mOutsets.writeToProto(proto, OUTSETS);
+ mDisplayCutout.writeToProto(proto, CUTOUT);
+ proto.write(REMOVE_ON_EXIT, mRemoveOnExit);
+ proto.write(DESTROYING, mDestroying);
+ proto.write(REMOVED, mRemoved);
+ proto.write(IS_ON_SCREEN, isOnScreen());
+ proto.write(IS_VISIBLE, isVisible());
proto.end(token);
}
- void writeIdentifierToProto(ProtoOutputStream proto, long fieldId) {
+ @Override
+ public void writeIdentifierToProto(ProtoOutputStream proto, long fieldId) {
final long token = proto.start(fieldId);
proto.write(HASH_CODE, System.identityHashCode(this));
proto.write(USER_ID, UserHandle.getUserId(mOwnerUid));
@@ -3675,6 +3766,13 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
windowInfo.activityToken = mAppToken.appToken.asBinder();
}
windowInfo.title = mAttrs.accessibilityTitle;
+ // Panel windows have no public way to set the a11y title directly. Use the
+ // regular title as a fallback.
+ if (TextUtils.isEmpty(windowInfo.title)
+ && (mAttrs.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW)
+ && (mAttrs.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW)) {
+ windowInfo.title = mAttrs.getTitle();
+ }
windowInfo.accessibilityIdOfAnchor = mAttrs.accessibilityIdOfAnchor;
windowInfo.focused = isFocused();
Task task = getTask();
@@ -3865,6 +3963,22 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
return null;
}
+ /**
+ * @return True if we our one of our ancestors has {@link #mAnimatingExit} set to true, false
+ * otherwise.
+ */
+ @VisibleForTesting
+ boolean isSelfOrAncestorWindowAnimatingExit() {
+ WindowState window = this;
+ do {
+ if (window.mAnimatingExit) {
+ return true;
+ }
+ window = window.getParentWindow();
+ } while (window != null);
+ return false;
+ }
+
void onExitAnimationDone() {
if (DEBUG_ANIM) Slog.v(TAG, "onExitAnimationDone in " + this
+ ": exiting=" + mAnimatingExit + " remove=" + mRemoveOnExit
@@ -3872,8 +3986,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
if (!mChildren.isEmpty()) {
// Copying to a different list as multiple children can be removed.
- // TODO: Not sure if we really need to copy this into a different list.
- final LinkedList<WindowState> childWindows = new LinkedList(mChildren);
+ final ArrayList<WindowState> childWindows = new ArrayList<>(mChildren);
for (int i = childWindows.size() - 1; i >= 0; i--) {
childWindows.get(i).onExitAnimationDone();
}
@@ -3901,7 +4014,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
mService.mAccessibilityController.onSomeWindowResizedOrMovedLocked();
}
- if (!mAnimatingExit) {
+ if (!isSelfOrAncestorWindowAnimatingExit()) {
return;
}
@@ -4291,8 +4404,22 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
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;
+ int x = mSurfacePosition.x + mShownPosition.x;
+ int y = mSurfacePosition.y + mShownPosition.y;
+
+ // If changed, also adjust transformFrameToSurfacePosition
+ final WindowContainer parent = getParent();
+ if (isChildWindow()) {
+ final WindowState parentWindow = getParentWindow();
+ x += parentWindow.mFrame.left - parentWindow.mAttrs.surfaceInsets.left;
+ y += parentWindow.mFrame.top - parentWindow.mAttrs.surfaceInsets.top;
+ } else if (parent != null) {
+ final Rect parentBounds = parent.getBounds();
+ x += parentBounds.left;
+ y += parentBounds.top;
+ }
+ float9[Matrix.MTRANS_X] = x;
+ float9[Matrix.MTRANS_Y] = y;
float9[Matrix.MPERSP_0] = 0;
float9[Matrix.MPERSP_1] = 0;
float9[Matrix.MPERSP_2] = 1;
@@ -4417,6 +4544,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// Leash is now responsible for position, so set our position to 0.
t.setPosition(mSurfaceControl, 0, 0);
+ mLastSurfacePosition.set(0, 0);
}
@Override
@@ -4432,13 +4560,16 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
}
transformFrameToSurfacePosition(mFrame.left, mFrame.top, mSurfacePosition);
- if (!mSurfaceAnimator.hasLeash()) {
+ if (!mSurfaceAnimator.hasLeash() && !mLastSurfacePosition.equals(mSurfacePosition)) {
t.setPosition(mSurfaceControl, mSurfacePosition.x, mSurfacePosition.y);
+ mLastSurfacePosition.set(mSurfacePosition.x, mSurfacePosition.y);
}
}
private void transformFrameToSurfacePosition(int left, int top, Point outPoint) {
outPoint.set(left, top);
+
+ // If changed, also adjust getTransformationMatrix
final WindowContainer parentWindowContainer = getParent();
if (isChildWindow()) {
// TODO: This probably falls apart at some point and we should
@@ -4499,6 +4630,37 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
}
}
+ /**
+ * Update a tap exclude region with a rectangular area identified by provided id. The requested
+ * area will be clipped to the window bounds.
+ */
+ void updateTapExcludeRegion(int regionId, int left, int top, int width, int height) {
+ final DisplayContent currentDisplay = getDisplayContent();
+ if (currentDisplay == null) {
+ throw new IllegalStateException("Trying to update window not attached to any display.");
+ }
+
+ if (mTapExcludeRegionHolder == null) {
+ mTapExcludeRegionHolder = new TapExcludeRegionHolder();
+
+ // Make sure that this window is registered as one that provides a tap exclude region
+ // for its containing display.
+ currentDisplay.mTapExcludeProvidingWindows.add(this);
+ }
+
+ mTapExcludeRegionHolder.updateRegion(regionId, left, top, width, height);
+ // Trigger touch exclude region update on current display.
+ final boolean isAppFocusedOnDisplay = mService.mFocusedApp != null
+ && mService.mFocusedApp.getDisplayContent() == currentDisplay;
+ currentDisplay.setTouchExcludeRegion(isAppFocusedOnDisplay ? mService.mFocusedApp.getTask()
+ : null);
+ }
+
+ /** Union the region with current tap exclude region that this window provides. */
+ void amendTapExcludeRegion(Region region) {
+ mTapExcludeRegionHolder.amendRegion(region, getBounds());
+ }
+
private final class MoveAnimationSpec implements AnimationSpec {
private final long mDuration;
@@ -4509,7 +4671,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
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();
+ mDuration = (long)
+ (anim.computeDurationHint() * mService.getWindowAnimationScaleLocked());
mInterpolator = anim.getInterpolator();
mFrom.set(fromX, fromY);
mTo.set(toX, toY);
diff --git a/com/android/server/wm/WindowStateAnimator.java b/com/android/server/wm/WindowStateAnimator.java
index d2247ac2..ba5156bf 100644
--- a/com/android/server/wm/WindowStateAnimator.java
+++ b/com/android/server/wm/WindowStateAnimator.java
@@ -41,9 +41,10 @@ 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.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.DRAW_STATE;
import static com.android.server.wm.proto.WindowStateAnimatorProto.LAST_CLIP_RECT;
import static com.android.server.wm.proto.WindowStateAnimatorProto.SURFACE;
+import static com.android.server.wm.proto.WindowStateAnimatorProto.SYSTEM_DECOR_RECT;
import android.content.Context;
import android.graphics.Matrix;
@@ -1163,7 +1164,7 @@ class WindowStateAnimator {
// potentially occurring while turning off the screen. This would lead to the
// screen incorrectly turning back on.
if (!mService.mPowerManager.isInteractive()) {
- mAnimator.mBulkUpdateParams |= SET_TURN_ON_SCREEN;
+ mService.mTurnOnScreen = true;
}
}
}
@@ -1381,6 +1382,8 @@ class WindowStateAnimator {
if (mSurfaceController != null) {
mSurfaceController.writeToProto(proto, SURFACE);
}
+ proto.write(DRAW_STATE, mDrawState);
+ mSystemDecorRect.writeToProto(proto, SYSTEM_DECOR_RECT);
proto.end(token);
}
diff --git a/com/android/server/wm/WindowSurfaceController.java b/com/android/server/wm/WindowSurfaceController.java
index e26a362b..2f38556e 100644
--- a/com/android/server/wm/WindowSurfaceController.java
+++ b/com/android/server/wm/WindowSurfaceController.java
@@ -53,7 +53,7 @@ class WindowSurfaceController {
final WindowStateAnimator mAnimator;
- SurfaceControlWithBackground mSurfaceControl;
+ SurfaceControl mSurfaceControl;
// Should only be set from within setShown().
private boolean mSurfaceShown = false;
@@ -108,13 +108,11 @@ class WindowSurfaceController {
.setFormat(format)
.setFlags(flags)
.setMetadata(windowType, ownerUid);
- mSurfaceControl = new SurfaceControlWithBackground(
- name, b, windowType, w, h, this);
+ mSurfaceControl = b.build();
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
if (mService.mRoot.mSurfaceTraceEnabled) {
- mSurfaceControl = new RemoteSurfaceTrace(
- mService.mRoot.mSurfaceTraceFd.getFileDescriptor(), mSurfaceControl, win);
+ installRemoteTrace(mService.mRoot.mSurfaceTraceFd.getFileDescriptor());
}
}
@@ -123,7 +121,7 @@ class WindowSurfaceController {
}
void removeRemoteTrace() {
- mSurfaceControl = new SurfaceControlWithBackground(mSurfaceControl);
+ mSurfaceControl = new SurfaceControl(mSurfaceControl);
}
diff --git a/com/android/server/wm/WindowSurfacePlacer.java b/com/android/server/wm/WindowSurfacePlacer.java
index bdab9c76..7364e872 100644
--- a/com/android/server/wm/WindowSurfacePlacer.java
+++ b/com/android/server/wm/WindowSurfacePlacer.java
@@ -20,59 +20,56 @@ import static android.app.ActivityManager.StackId.INVALID_STACK_ID;
import static android.app.ActivityManagerInternal.APP_TRANSITION_SNAPSHOT;
import static android.app.ActivityManagerInternal.APP_TRANSITION_SPLASH_SCREEN;
import static android.app.ActivityManagerInternal.APP_TRANSITION_WINDOWS_DRAWN;
+
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_CONFIG;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_LAYOUT;
-import static com.android.server.wm.AppTransition.TRANSIT_ACTIVITY_CLOSE;
-import static com.android.server.wm.AppTransition.TRANSIT_ACTIVITY_OPEN;
-import static com.android.server.wm.AppTransition.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION;
-import static com.android.server.wm.AppTransition.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE;
-import static com.android.server.wm.AppTransition.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER;
-import static com.android.server.wm.AppTransition.TRANSIT_KEYGUARD_GOING_AWAY;
-import static com.android.server.wm.AppTransition.TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER;
-import static com.android.server.wm.AppTransition.TRANSIT_NONE;
-import static com.android.server.wm.AppTransition.TRANSIT_TASK_CLOSE;
-import static com.android.server.wm.AppTransition.TRANSIT_TASK_IN_PLACE;
-import static com.android.server.wm.AppTransition.TRANSIT_TASK_OPEN;
-import static com.android.server.wm.AppTransition.TRANSIT_TASK_TO_BACK;
-import static com.android.server.wm.AppTransition.TRANSIT_TASK_TO_FRONT;
-import static com.android.server.wm.AppTransition.TRANSIT_WALLPAPER_CLOSE;
-import static com.android.server.wm.AppTransition.TRANSIT_WALLPAPER_INTRA_CLOSE;
-import static com.android.server.wm.AppTransition.TRANSIT_WALLPAPER_INTRA_OPEN;
-import static com.android.server.wm.AppTransition.TRANSIT_WALLPAPER_OPEN;
+import static android.view.WindowManager.TRANSIT_ACTIVITY_CLOSE;
+import static android.view.WindowManager.TRANSIT_ACTIVITY_OPEN;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_TO_SHADE;
+import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_WITH_WALLPAPER;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
+import static android.view.WindowManager.TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER;
+import static android.view.WindowManager.TRANSIT_NONE;
+import static android.view.WindowManager.TRANSIT_TASK_CLOSE;
+import static android.view.WindowManager.TRANSIT_TASK_IN_PLACE;
+import static android.view.WindowManager.TRANSIT_TASK_OPEN;
+import static android.view.WindowManager.TRANSIT_TASK_TO_BACK;
+import static android.view.WindowManager.TRANSIT_TASK_TO_FRONT;
+import static android.view.WindowManager.TRANSIT_WALLPAPER_CLOSE;
+import static android.view.WindowManager.TRANSIT_WALLPAPER_INTRA_CLOSE;
+import static android.view.WindowManager.TRANSIT_WALLPAPER_INTRA_OPEN;
+import static android.view.WindowManager.TRANSIT_WALLPAPER_OPEN;
import static com.android.server.wm.AppTransition.isKeyguardGoingAwayTransit;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_APP_TRANSITIONS;
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_WITH_CLASS_NAME;
import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM;
import static com.android.server.wm.WindowManagerService.H.NOTIFY_APP_TRANSITION_STARTING;
import static com.android.server.wm.WindowManagerService.H.REPORT_WINDOWS_CHANGE;
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.UPDATE_FOCUS_PLACING_SURFACES;
-import android.content.res.Configuration;
-import android.graphics.GraphicBuffer;
-import android.graphics.PixelFormat;
-import android.graphics.Rect;
-import android.os.Binder;
import android.os.Debug;
import android.os.Trace;
import android.util.ArraySet;
import android.util.Slog;
import android.util.SparseIntArray;
import android.view.Display;
-import android.view.DisplayInfo;
-import android.view.Surface;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationDefinition;
import android.view.SurfaceControl;
+import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
+import android.view.WindowManager.TransitionType;
import android.view.animation.Animation;
import com.android.server.wm.WindowManagerService.H;
import java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.function.Predicate;
/**
* Positions windows and their surfaces.
@@ -94,8 +91,7 @@ class WindowSurfacePlacer {
static final int SET_WALLPAPER_MAY_CHANGE = 1 << 1;
static final int SET_FORCE_HIDING_CHANGED = 1 << 2;
static final int SET_ORIENTATION_CHANGE_COMPLETE = 1 << 3;
- static final int SET_TURN_ON_SCREEN = 1 << 4;
- static final int SET_WALLPAPER_ACTION_PENDING = 1 << 5;
+ static final int SET_WALLPAPER_ACTION_PENDING = 1 << 4;
private boolean mTraversalScheduled;
private int mDeferDepth = 0;
@@ -256,7 +252,7 @@ class WindowSurfacePlacer {
if (DEBUG_APP_TRANSITIONS) Slog.v(TAG, "**** GOOD TO GO");
int transit = mService.mAppTransition.getAppTransition();
if (mService.mSkipAppTransitionAnimation && !isKeyguardGoingAwayTransit(transit)) {
- transit = AppTransition.TRANSIT_UNSET;
+ transit = WindowManager.TRANSIT_UNSET;
}
mService.mSkipAppTransitionAnimation = false;
mService.mNoAnimationNotifyOnTransitionFinished.clear();
@@ -264,17 +260,9 @@ class WindowSurfacePlacer {
mService.mH.removeMessages(H.APP_TRANSITION_TIMEOUT);
final DisplayContent displayContent = mService.getDefaultDisplayContentLocked();
- // TODO: Don't believe this is really needed...
- //mService.mWindowsChanged = true;
mService.mRoot.mWallpaperMayChange = false;
- // The top-most window will supply the layout params, and we will determine it below.
- LayoutParams animLp = null;
- int bestAnimLayer = -1;
- boolean fullscreenAnim = false;
- boolean voiceInteraction = false;
-
int i;
for (i = 0; i < appsCount; i++) {
final AppWindowToken wtoken = mService.mOpeningApps.valueAt(i);
@@ -283,7 +271,6 @@ class WindowSurfacePlacer {
// visibility. We need to clear it *before* maybeUpdateTransitToWallpaper() as the
// transition selection depends on wallpaper target visibility.
wtoken.clearAnimatingFlags();
-
}
// Adjust wallpaper before we pull the lower/upper target, since pending changes
@@ -292,78 +279,53 @@ class WindowSurfacePlacer {
mWallpaperControllerLocked.adjustWallpaperWindowsForAppTransitionIfNeeded(displayContent,
mService.mOpeningApps);
- final WindowState wallpaperTarget = mWallpaperControllerLocked.getWallpaperTarget();
- boolean openingAppHasWallpaper = false;
- boolean closingAppHasWallpaper = false;
-
- // Do a first pass through the tokens for two things:
- // (1) Determine if both the closing and opening app token sets are wallpaper targets, in
- // which case special animations are needed (since the wallpaper needs to stay static behind
- // them).
- // (2) Find the layout params of the top-most application window in the tokens, which is
- // what will control the animation theme.
- final int closingAppsCount = mService.mClosingApps.size();
- appsCount = closingAppsCount + mService.mOpeningApps.size();
- for (i = 0; i < appsCount; i++) {
- final AppWindowToken wtoken;
- if (i < closingAppsCount) {
- wtoken = mService.mClosingApps.valueAt(i);
- if (wallpaperTarget != null && wtoken.windowsCanBeWallpaperTarget()) {
- closingAppHasWallpaper = true;
- }
- } else {
- wtoken = mService.mOpeningApps.valueAt(i - closingAppsCount);
- if (wallpaperTarget != null && wtoken.windowsCanBeWallpaperTarget()) {
- openingAppHasWallpaper = true;
- }
- }
-
- voiceInteraction |= wtoken.mVoiceInteraction;
-
- if (wtoken.fillsParent()) {
- final WindowState ws = wtoken.findMainWindow();
- if (ws != null) {
- animLp = ws.mAttrs;
- bestAnimLayer = ws.mLayer;
- fullscreenAnim = true;
- }
- } else if (!fullscreenAnim) {
- final WindowState ws = wtoken.findMainWindow();
- if (ws != null) {
- if (ws.mLayer > bestAnimLayer) {
- animLp = ws.mAttrs;
- bestAnimLayer = ws.mLayer;
- }
- }
- }
- }
+ // Determine if closing and opening app token sets are wallpaper targets, in which case
+ // special animations are needed.
+ final boolean hasWallpaperTarget = mWallpaperControllerLocked.getWallpaperTarget() != null;
+ final boolean openingAppHasWallpaper = canBeWallpaperTarget(mService.mOpeningApps)
+ && hasWallpaperTarget;
+ final boolean closingAppHasWallpaper = canBeWallpaperTarget(mService.mClosingApps)
+ && hasWallpaperTarget;
transit = maybeUpdateTransitToWallpaper(transit, openingAppHasWallpaper,
closingAppHasWallpaper);
- // If all closing windows are obscured, then there is no need to do an animation. This is
- // the case, for example, when this transition is being done behind the lock screen.
- if (!mService.mPolicy.allowAppAnimationsLw()) {
- if (DEBUG_APP_TRANSITIONS) Slog.v(TAG,
- "Animations disallowed by keyguard or dream.");
- animLp = null;
- }
-
- processApplicationsAnimatingInPlace(transit);
+ // Find the layout params of the top-most application window in the tokens, which is
+ // what will control the animation theme. If all closing windows are obscured, then there is
+ // no need to do an animation. This is the case, for example, when this transition is being
+ // done behind a dream window.
+ final AppWindowToken animLpToken = mService.mPolicy.allowAppAnimationsLw()
+ ? findAnimLayoutParamsToken(transit)
+ : null;
- mTmpLayerAndToken.token = null;
- handleClosingApps(transit, animLp, voiceInteraction, mTmpLayerAndToken);
- final AppWindowToken topClosingApp = mTmpLayerAndToken.token;
- final AppWindowToken topOpeningApp = handleOpeningApps(transit, animLp, voiceInteraction);
+ final LayoutParams animLp = getAnimLp(animLpToken);
+ overrideWithRemoteAnimationIfSet(animLpToken, transit);
- mService.mAppTransition.setLastAppTransition(transit, topOpeningApp, topClosingApp);
+ final boolean voiceInteraction = containsVoiceInteraction(mService.mOpeningApps)
+ || containsVoiceInteraction(mService.mOpeningApps);
- final int flags = mService.mAppTransition.getTransitFlags();
- int layoutRedo = mService.mAppTransition.goodToGo(transit, topOpeningApp,
- topClosingApp, mService.mOpeningApps, mService.mClosingApps);
- handleNonAppWindowsInTransition(transit, flags);
- mService.mAppTransition.postAnimationCallback();
- mService.mAppTransition.clear();
+ final int layoutRedo;
+ mService.mSurfaceAnimationRunner.deferStartingAnimations();
+ try {
+ processApplicationsAnimatingInPlace(transit);
+
+ mTmpLayerAndToken.token = null;
+ handleClosingApps(transit, animLp, voiceInteraction, mTmpLayerAndToken);
+ final AppWindowToken topClosingApp = mTmpLayerAndToken.token;
+ final AppWindowToken topOpeningApp = handleOpeningApps(transit, animLp,
+ voiceInteraction);
+
+ mService.mAppTransition.setLastAppTransition(transit, topOpeningApp, topClosingApp);
+
+ final int flags = mService.mAppTransition.getTransitFlags();
+ layoutRedo = mService.mAppTransition.goodToGo(transit, topOpeningApp,
+ topClosingApp, mService.mOpeningApps, mService.mClosingApps);
+ handleNonAppWindowsInTransition(transit, flags);
+ mService.mAppTransition.postAnimationCallback();
+ mService.mAppTransition.clear();
+ } finally {
+ mService.mSurfaceAnimationRunner.continueStartingAnimations();
+ }
mService.mTaskSnapshotController.onTransitionStarting();
@@ -390,6 +352,81 @@ class WindowSurfacePlacer {
return layoutRedo | FINISH_LAYOUT_REDO_LAYOUT | FINISH_LAYOUT_REDO_CONFIG;
}
+ private static LayoutParams getAnimLp(AppWindowToken wtoken) {
+ final WindowState mainWindow = wtoken != null ? wtoken.findMainWindow() : null;
+ return mainWindow != null ? mainWindow.mAttrs : null;
+ }
+
+ /**
+ * Overrides the pending transition with the remote animation defined for the transition in the
+ * set of defined remote animations in the app window token.
+ */
+ private void overrideWithRemoteAnimationIfSet(AppWindowToken animLpToken, int transit) {
+ if (animLpToken == null) {
+ return;
+ }
+ final RemoteAnimationDefinition definition = animLpToken.getRemoteAnimationDefinition();
+ if (definition != null) {
+ final RemoteAnimationAdapter adapter = definition.getAdapter(transit);
+ if (adapter != null) {
+ mService.mAppTransition.overridePendingAppTransitionRemote(adapter);
+ }
+ }
+ }
+
+ /**
+ * @return The window token that determines the animation theme.
+ */
+ private AppWindowToken findAnimLayoutParamsToken(@TransitionType int transit) {
+ AppWindowToken result;
+
+ // Remote animations always win, but fullscreen tokens override non-fullscreen tokens.
+ result = lookForHighestTokenWithFilter(mService.mClosingApps, mService.mOpeningApps,
+ w -> w.getRemoteAnimationDefinition() != null
+ && w.getRemoteAnimationDefinition().hasTransition(transit));
+ if (result != null) {
+ return result;
+ }
+ result = lookForHighestTokenWithFilter(mService.mClosingApps, mService.mOpeningApps,
+ w -> w.fillsParent() && w.findMainWindow() != null);
+ if (result != null) {
+ return result;
+ }
+ return lookForHighestTokenWithFilter(mService.mClosingApps, mService.mOpeningApps,
+ w -> w.findMainWindow() != null);
+ }
+
+ private AppWindowToken lookForHighestTokenWithFilter(ArraySet<AppWindowToken> array1,
+ ArraySet<AppWindowToken> array2, Predicate<AppWindowToken> filter) {
+ final int array1count = array1.size();
+ final int count = array1count + array2.size();
+ int bestPrefixOrderIndex = Integer.MIN_VALUE;
+ AppWindowToken bestToken = null;
+ for (int i = 0; i < count; i++) {
+ final AppWindowToken wtoken;
+ if (i < array1count) {
+ wtoken = array1.valueAt(i);
+ } else {
+ wtoken = array2.valueAt(i - array1count);
+ }
+ final int prefixOrderIndex = wtoken.getPrefixOrderIndex();
+ if (filter.test(wtoken) && prefixOrderIndex > bestPrefixOrderIndex) {
+ bestPrefixOrderIndex = prefixOrderIndex;
+ bestToken = wtoken;
+ }
+ }
+ return bestToken;
+ }
+
+ private boolean containsVoiceInteraction(ArraySet<AppWindowToken> apps) {
+ for (int i = apps.size() - 1; i >= 0; i--) {
+ if (apps.valueAt(i).mVoiceInteraction) {
+ return true;
+ }
+ }
+ return false;
+ }
+
private AppWindowToken handleOpeningApps(int transit, LayoutParams animLp,
boolean voiceInteraction) {
AppWindowToken topOpeningApp = null;
@@ -407,7 +444,6 @@ class WindowSurfacePlacer {
}
wtoken.updateReportedVisibilityLocked();
wtoken.waitingToShow = false;
-
if (SHOW_LIGHT_TRANSACTIONS) Slog.i(TAG,
">>> OPEN TRANSACTION handleAppTransitionReadyLocked()");
mService.openSurfaceTransaction();
@@ -428,6 +464,8 @@ class WindowSurfacePlacer {
}
if (mService.mAppTransition.isNextAppTransitionThumbnailUp()) {
wtoken.attachThumbnailAnimation();
+ } else if (mService.mAppTransition.isNextAppTransitionOpenCrossProfileApps()) {
+ wtoken.attachCrossProfileAppsThumbnailAnimation();
}
}
return topOpeningApp;
diff --git a/com/android/server/wm/WindowToken.java b/com/android/server/wm/WindowToken.java
index bad9bf53..172efdcb 100644
--- a/com/android/server/wm/WindowToken.java
+++ b/com/android/server/wm/WindowToken.java
@@ -16,11 +16,7 @@
package com.android.server.wm;
-import android.annotation.CallSuper;
-import android.util.proto.ProtoOutputStream;
-import java.util.Comparator;
import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
-
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_ADD_REMOVE;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_FOCUS;
import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_WINDOW_MOVEMENT;
@@ -28,15 +24,20 @@ 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.UPDATE_FOCUS_NORMAL;
import static com.android.server.wm.proto.WindowTokenProto.HASH_CODE;
+import static com.android.server.wm.proto.WindowTokenProto.HIDDEN;
+import static com.android.server.wm.proto.WindowTokenProto.PAUSED;
+import static com.android.server.wm.proto.WindowTokenProto.WAITING_TO_SHOW;
import static com.android.server.wm.proto.WindowTokenProto.WINDOWS;
import static com.android.server.wm.proto.WindowTokenProto.WINDOW_CONTAINER;
+import android.annotation.CallSuper;
import android.os.Debug;
import android.os.IBinder;
import android.util.Slog;
-import android.view.SurfaceControl;
+import android.util.proto.ProtoOutputStream;
import java.io.PrintWriter;
+import java.util.Comparator;
/**
* Container of a set of related windows in the window manager. Often this is an AppWindowToken,
@@ -276,6 +277,9 @@ class WindowToken extends WindowContainer<WindowState> {
final WindowState w = mChildren.get(i);
w.writeToProto(proto, WINDOWS, trim);
}
+ proto.write(HIDDEN, mHidden);
+ proto.write(WAITING_TO_SHOW, waitingToShow);
+ proto.write(PAUSED, paused);
proto.end(token);
}
diff --git a/com/android/server/wm/WindowTracing.java b/com/android/server/wm/WindowTracing.java
index c858b198..a2997815 100644
--- a/com/android/server/wm/WindowTracing.java
+++ b/com/android/server/wm/WindowTracing.java
@@ -29,6 +29,7 @@ import android.content.Context;
import android.os.ShellCommand;
import android.os.SystemClock;
import android.os.Trace;
+import android.annotation.Nullable;
import android.util.Log;
import android.util.proto.ProtoOutputStream;
@@ -62,7 +63,7 @@ class WindowTracing {
mTraceFile = file;
}
- void startTrace(PrintWriter pw) throws IOException {
+ void startTrace(@Nullable PrintWriter pw) throws IOException {
if (IS_USER){
logAndPrintln(pw, "Error: Tracing is not supported on user builds.");
return;
@@ -81,13 +82,15 @@ class WindowTracing {
}
}
- private void logAndPrintln(PrintWriter pw, String msg) {
+ private void logAndPrintln(@Nullable PrintWriter pw, String msg) {
Log.i(TAG, msg);
- pw.println(msg);
- pw.flush();
+ if (pw != null) {
+ pw.println(msg);
+ pw.flush();
+ }
}
- void stopTrace(PrintWriter pw) {
+ void stopTrace(@Nullable PrintWriter pw) {
if (IS_USER){
logAndPrintln(pw, "Error: Tracing is not supported on user builds.");
return;
diff --git a/com/android/server/wm/utils/CoordinateTransforms.java b/com/android/server/wm/utils/CoordinateTransforms.java
new file mode 100644
index 00000000..09d7b5de
--- /dev/null
+++ b/com/android/server/wm/utils/CoordinateTransforms.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm.utils;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+import static android.view.Surface.ROTATION_270;
+import static android.view.Surface.ROTATION_90;
+
+import android.annotation.Dimension;
+import android.graphics.Matrix;
+import android.view.Surface.Rotation;
+
+public class CoordinateTransforms {
+
+ private CoordinateTransforms() {
+ }
+
+ /**
+ * Sets a matrix such that given a rotation, it transforms physical display
+ * coordinates to that rotation's logical coordinates.
+ *
+ * @param rotation the rotation to which the matrix should transform
+ * @param out the matrix to be set
+ */
+ public static void transformPhysicalToLogicalCoordinates(@Rotation int rotation,
+ @Dimension int physicalWidth, @Dimension int physicalHeight, Matrix out) {
+ switch (rotation) {
+ case ROTATION_0:
+ out.reset();
+ break;
+ case ROTATION_90:
+ out.setRotate(270);
+ out.postTranslate(0, physicalWidth);
+ break;
+ case ROTATION_180:
+ out.setRotate(180);
+ out.postTranslate(physicalWidth, physicalHeight);
+ break;
+ case ROTATION_270:
+ out.setRotate(90);
+ out.postTranslate(physicalHeight, 0);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown rotation: " + rotation);
+ }
+ }
+}
diff --git a/com/android/settingslib/Utils.java b/com/android/settingslib/Utils.java
index 3c46d999..d001e66a 100644
--- a/com/android/settingslib/Utils.java
+++ b/com/android/settingslib/Utils.java
@@ -21,10 +21,9 @@ import android.os.UserHandle;
import android.os.UserManager;
import android.print.PrintManager;
import android.provider.Settings;
-
import com.android.internal.util.UserIcons;
import com.android.settingslib.drawable.UserIconDrawable;
-
+import com.android.settingslib.wrapper.LocationManagerWrapper;
import java.text.NumberFormat;
public class Utils {
@@ -45,6 +44,24 @@ public class Utils {
com.android.internal.R.drawable.ic_wifi_signal_4
};
+ public static void updateLocationEnabled(Context context, boolean enabled, int userId) {
+ Intent intent = new Intent(LocationManager.MODE_CHANGING_ACTION);
+
+ final int oldMode = Settings.Secure.getIntForUser(context.getContentResolver(),
+ Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_OFF, userId);
+ final int newMode = enabled
+ ? Settings.Secure.LOCATION_MODE_HIGH_ACCURACY
+ : Settings.Secure.LOCATION_MODE_OFF;
+ intent.putExtra(CURRENT_MODE_KEY, oldMode);
+ intent.putExtra(NEW_MODE_KEY, newMode);
+ context.sendBroadcastAsUser(
+ intent, UserHandle.of(userId), android.Manifest.permission.WRITE_SECURE_SETTINGS);
+ LocationManager locationManager =
+ (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
+ LocationManagerWrapper wrapper = new LocationManagerWrapper(locationManager);
+ wrapper.setLocationEnabledForUser(enabled, UserHandle.of(userId));
+ }
+
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);
diff --git a/com/android/settingslib/bluetooth/A2dpProfile.java b/com/android/settingslib/bluetooth/A2dpProfile.java
index 764c5922..9b69304b 100644
--- a/com/android/settingslib/bluetooth/A2dpProfile.java
+++ b/com/android/settingslib/bluetooth/A2dpProfile.java
@@ -128,14 +128,18 @@ public class A2dpProfile implements LocalBluetoothProfile {
public boolean connect(BluetoothDevice device) {
if (mService == null) return false;
- List<BluetoothDevice> sinks = getConnectedDevices();
- if (sinks != null) {
- for (BluetoothDevice sink : sinks) {
- if (sink.equals(device)) {
- Log.w(TAG, "Connecting to device " + device + " : disconnect skipped");
- continue;
+ int max_connected_devices = mLocalAdapter.getMaxConnectedAudioDevices();
+ if (max_connected_devices == 1) {
+ // Original behavior: disconnect currently connected device
+ List<BluetoothDevice> sinks = getConnectedDevices();
+ if (sinks != null) {
+ for (BluetoothDevice sink : sinks) {
+ if (sink.equals(device)) {
+ Log.w(TAG, "Connecting to device " + device + " : disconnect skipped");
+ continue;
+ }
+ mService.disconnect(sink);
}
- mService.disconnect(sink);
}
}
return mService.connect(device);
@@ -157,6 +161,16 @@ public class A2dpProfile implements LocalBluetoothProfile {
return mService.getConnectionState(device);
}
+ public boolean setActiveDevice(BluetoothDevice device) {
+ if (mService == null) return false;
+ return mService.setActiveDevice(device);
+ }
+
+ public BluetoothDevice getActiveDevice() {
+ if (mService == null) return null;
+ return mService.getActiveDevice();
+ }
+
public boolean isPreferred(BluetoothDevice device) {
if (mService == null) return false;
return mService.getPriority(device) > BluetoothProfile.PRIORITY_OFF;
@@ -180,8 +194,8 @@ public class A2dpProfile implements LocalBluetoothProfile {
boolean isA2dpPlaying() {
if (mService == null) return false;
List<BluetoothDevice> sinks = mService.getConnectedDevices();
- if (!sinks.isEmpty()) {
- if (mService.isA2dpPlaying(sinks.get(0))) {
+ for (BluetoothDevice device : sinks) {
+ if (mService.isA2dpPlaying(device)) {
return true;
}
}
diff --git a/com/android/settingslib/bluetooth/BluetoothCallback.java b/com/android/settingslib/bluetooth/BluetoothCallback.java
index 4c41b490..ac3599ca 100644
--- a/com/android/settingslib/bluetooth/BluetoothCallback.java
+++ b/com/android/settingslib/bluetooth/BluetoothCallback.java
@@ -28,4 +28,5 @@ public interface BluetoothCallback {
void onDeviceDeleted(CachedBluetoothDevice cachedDevice);
void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState);
void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state);
+ void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile);
}
diff --git a/com/android/settingslib/bluetooth/BluetoothEventManager.java b/com/android/settingslib/bluetooth/BluetoothEventManager.java
index f57d02bb..3cda9c9e 100644
--- a/com/android/settingslib/bluetooth/BluetoothEventManager.java
+++ b/com/android/settingslib/bluetooth/BluetoothEventManager.java
@@ -16,9 +16,12 @@
package com.android.settingslib.bluetooth;
+import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHeadset;
+import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -31,6 +34,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
/**
@@ -106,6 +110,12 @@ public class BluetoothEventManager {
// Dock event broadcasts
addHandler(Intent.ACTION_DOCK_EVENT, new DockEventHandler());
+ // Active device broadcasts
+ addHandler(BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED,
+ new ActiveDeviceChangedHandler());
+ addHandler(BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED,
+ new ActiveDeviceChangedHandler());
+
mContext.registerReceiver(mBroadcastReceiver, mAdapterIntentFilter, null, mReceiverHandler);
mContext.registerReceiver(mProfileBroadcastReceiver, mProfileIntentFilter, null, mReceiverHandler);
}
@@ -409,4 +419,35 @@ public class BluetoothEventManager {
return deviceAdded;
}
+
+ private class ActiveDeviceChangedHandler implements Handler {
+ @Override
+ public void onReceive(Context context, Intent intent, BluetoothDevice device) {
+ String action = intent.getAction();
+ if (action == null) {
+ Log.w(TAG, "ActiveDeviceChangedHandler: action is null");
+ return;
+ }
+ CachedBluetoothDevice activeDevice = mDeviceManager.findDevice(device);
+ int bluetoothProfile = 0;
+ if (Objects.equals(action, BluetoothA2dp.ACTION_ACTIVE_DEVICE_CHANGED)) {
+ bluetoothProfile = BluetoothProfile.A2DP;
+ } else if (Objects.equals(action, BluetoothHeadset.ACTION_ACTIVE_DEVICE_CHANGED)) {
+ bluetoothProfile = BluetoothProfile.HEADSET;
+ } else {
+ Log.w(TAG, "ActiveDeviceChangedHandler: unknown action " + action);
+ return;
+ }
+ dispatchActiveDeviceChanged(activeDevice, bluetoothProfile);
+ }
+ }
+
+ private void dispatchActiveDeviceChanged(CachedBluetoothDevice activeDevice,
+ int bluetoothProfile) {
+ synchronized (mCallbacks) {
+ for (BluetoothCallback callback : mCallbacks) {
+ callback.onActiveDeviceChanged(activeDevice, bluetoothProfile);
+ }
+ }
+ }
}
diff --git a/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index 9caff100..fb0f75b5 100644
--- a/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -105,6 +105,10 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 5000;
private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 30000;
+ // Active device state
+ private boolean mIsActiveDeviceA2dp = false;
+ private boolean mIsActiveDeviceHeadset = false;
+
/**
* Describes the current device and profile for logging.
*
@@ -156,6 +160,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
mRemovedProfiles.add(profile);
mLocalNapRoleConnected = false;
}
+ fetchActiveDevices();
}
CachedBluetoothDevice(Context context,
@@ -359,6 +364,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
fetchName();
fetchBtClass();
updateProfiles();
+ fetchActiveDevices();
migratePhonebookPermissionChoice();
migrateMessagePermissionChoice();
fetchMessageRejectionCount();
@@ -454,6 +460,33 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
return mDevice.getBondState();
}
+ /**
+ * Set the device status as active or non-active per Bluetooth profile.
+ *
+ * @param isActive true if the device is active
+ * @param bluetoothProfile the Bluetooth profile
+ */
+ public void setActiveDevice(boolean isActive, int bluetoothProfile) {
+ boolean changed = false;
+ switch (bluetoothProfile) {
+ case BluetoothProfile.A2DP:
+ changed = (mIsActiveDeviceA2dp != isActive);
+ mIsActiveDeviceA2dp = isActive;
+ break;
+ case BluetoothProfile.HEADSET:
+ changed = (mIsActiveDeviceHeadset != isActive);
+ mIsActiveDeviceHeadset = isActive;
+ break;
+ default:
+ Log.w(TAG, "setActiveDevice: unknown profile " + bluetoothProfile +
+ " isActive " + isActive);
+ break;
+ }
+ if (changed) {
+ dispatchAttributesChanged();
+ }
+ }
+
void setRssi(short rssi) {
if (mRssi != rssi) {
mRssi = rssi;
@@ -529,6 +562,17 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
return true;
}
+ private void fetchActiveDevices() {
+ A2dpProfile a2dpProfile = mProfileManager.getA2dpProfile();
+ if (a2dpProfile != null) {
+ mIsActiveDeviceA2dp = mDevice.equals(a2dpProfile.getActiveDevice());
+ }
+ HeadsetProfile headsetProfile = mProfileManager.getHeadsetProfile();
+ if (headsetProfile != null) {
+ mIsActiveDeviceHeadset = mDevice.equals(headsetProfile.getActiveDevice());
+ }
+ }
+
/**
* Refreshes the UI for the BT class, including fetching the latest value
* for the class.
@@ -896,37 +940,60 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice>
com.android.settingslib.Utils.formatPercentage(batteryLevel);
}
+ // TODO: A temporary workaround solution using string description the device is active.
+ // Issue tracked by b/72317067 .
+ // An alternative solution would be visual indication.
+ // Intentionally not adding the strings to strings.xml for now:
+ // 1) If this is just a short-term solution, no need to waste translation effort
+ // 2) The number of strings with all possible combinations becomes enormously large.
+ // If string description becomes part of the final solution, we MUST NOT
+ // concatenate the strings here: this does not translate well.
+ String activeString = null;
+ if (mIsActiveDeviceA2dp && mIsActiveDeviceHeadset) {
+ activeString = ", active";
+ } else {
+ if (mIsActiveDeviceA2dp) {
+ activeString = ", active(media)";
+ }
+ if (mIsActiveDeviceHeadset) {
+ activeString = ", active(phone)";
+ }
+ }
+ if (activeString == null) activeString = "";
+
if (profileConnected) {
if (a2dpNotConnected && hfpNotConnected) {
if (batteryLevelPercentageString != null) {
return mContext.getString(
R.string.bluetooth_connected_no_headset_no_a2dp_battery_level,
- batteryLevelPercentageString);
+ batteryLevelPercentageString) + activeString;
} else {
- return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp);
+ return mContext.getString(R.string.bluetooth_connected_no_headset_no_a2dp) +
+ activeString;
}
} else if (a2dpNotConnected) {
if (batteryLevelPercentageString != null) {
return mContext.getString(R.string.bluetooth_connected_no_a2dp_battery_level,
- batteryLevelPercentageString);
+ batteryLevelPercentageString) + activeString;
} else {
- return mContext.getString(R.string.bluetooth_connected_no_a2dp);
+ return mContext.getString(R.string.bluetooth_connected_no_a2dp) + activeString;
}
} else if (hfpNotConnected) {
if (batteryLevelPercentageString != null) {
return mContext.getString(R.string.bluetooth_connected_no_headset_battery_level,
- batteryLevelPercentageString);
+ batteryLevelPercentageString) + activeString;
} else {
- return mContext.getString(R.string.bluetooth_connected_no_headset);
+ return mContext.getString(R.string.bluetooth_connected_no_headset)
+ + activeString;
}
} else {
if (batteryLevelPercentageString != null) {
return mContext.getString(R.string.bluetooth_connected_battery_level,
- batteryLevelPercentageString);
+ batteryLevelPercentageString) + activeString;
} else {
- return mContext.getString(R.string.bluetooth_connected);
+ return mContext.getString(R.string.bluetooth_connected) + activeString;
}
}
}
diff --git a/com/android/settingslib/bluetooth/HeadsetProfile.java b/com/android/settingslib/bluetooth/HeadsetProfile.java
index d45fe1a3..ee121912 100644
--- a/com/android/settingslib/bluetooth/HeadsetProfile.java
+++ b/com/android/settingslib/bluetooth/HeadsetProfile.java
@@ -153,6 +153,16 @@ public class HeadsetProfile implements LocalBluetoothProfile {
return BluetoothProfile.STATE_DISCONNECTED;
}
+ public boolean setActiveDevice(BluetoothDevice device) {
+ if (mService == null) return false;
+ return mService.setActiveDevice(device);
+ }
+
+ public BluetoothDevice getActiveDevice() {
+ if (mService == null) return null;
+ return mService.getActiveDevice();
+ }
+
public boolean isPreferred(BluetoothDevice device) {
if (mService == null) return false;
return mService.getPriority(device) > BluetoothProfile.PRIORITY_OFF;
diff --git a/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java b/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java
index 22674cb6..cda4e454 100644
--- a/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java
+++ b/com/android/settingslib/bluetooth/LocalBluetoothAdapter.java
@@ -239,4 +239,8 @@ public class LocalBluetoothAdapter {
public BluetoothDevice getRemoteDevice(String address) {
return mAdapter.getRemoteDevice(address);
}
+
+ public int getMaxConnectedAudioDevices() {
+ return mAdapter.getMaxConnectedAudioDevices();
+ }
}
diff --git a/com/android/settingslib/core/AbstractPreferenceController.java b/com/android/settingslib/core/AbstractPreferenceController.java
index 88f0d2be..d14b53b1 100644
--- a/com/android/settingslib/core/AbstractPreferenceController.java
+++ b/com/android/settingslib/core/AbstractPreferenceController.java
@@ -69,4 +69,12 @@ public abstract class AbstractPreferenceController {
pref.setVisible(isVisible);
}
}
+
+
+ /**
+ * @return a String for the summary of the preference.
+ */
+ public String getSummary() {
+ return null;
+ }
}
diff --git a/com/android/settingslib/core/instrumentation/EventLogWriter.java b/com/android/settingslib/core/instrumentation/EventLogWriter.java
new file mode 100644
index 00000000..72273046
--- /dev/null
+++ b/com/android/settingslib/core/instrumentation/EventLogWriter.java
@@ -0,0 +1,110 @@
+/*
+ * 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.settingslib.core.instrumentation;
+
+import android.content.Context;
+import android.metrics.LogMaker;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+
+/**
+ * {@link LogWriter} that writes data to eventlog.
+ */
+public class EventLogWriter implements LogWriter {
+
+ private final MetricsLogger mMetricsLogger = new MetricsLogger();
+
+ public void visible(Context context, int source, int category) {
+ final LogMaker logMaker = new LogMaker(category)
+ .setType(MetricsProto.MetricsEvent.TYPE_OPEN)
+ .addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, source);
+ MetricsLogger.action(logMaker);
+ }
+
+ public void hidden(Context context, int category) {
+ MetricsLogger.hidden(context, category);
+ }
+
+ public void action(int category, int value, Pair<Integer, Object>... taggedData) {
+ if (taggedData == null || taggedData.length == 0) {
+ mMetricsLogger.action(category, value);
+ } else {
+ final LogMaker logMaker = new LogMaker(category)
+ .setType(MetricsProto.MetricsEvent.TYPE_ACTION)
+ .setSubtype(value);
+ for (Pair<Integer, Object> pair : taggedData) {
+ logMaker.addTaggedData(pair.first, pair.second);
+ }
+ mMetricsLogger.write(logMaker);
+ }
+ }
+
+ public void action(int category, boolean value, Pair<Integer, Object>... taggedData) {
+ action(category, value ? 1 : 0, taggedData);
+ }
+
+ public void action(Context context, int category, Pair<Integer, Object>... taggedData) {
+ action(context, category, "", taggedData);
+ }
+
+ public void actionWithSource(Context context, int source, int category) {
+ final LogMaker logMaker = new LogMaker(category)
+ .setType(MetricsProto.MetricsEvent.TYPE_ACTION);
+ if (source != MetricsProto.MetricsEvent.VIEW_UNKNOWN) {
+ logMaker.addTaggedData(MetricsProto.MetricsEvent.FIELD_CONTEXT, source);
+ }
+ MetricsLogger.action(logMaker);
+ }
+
+ /** @deprecated use {@link #action(int, int, Pair[])} */
+ @Deprecated
+ public void action(Context context, int category, int value) {
+ MetricsLogger.action(context, category, value);
+ }
+
+ /** @deprecated use {@link #action(int, boolean, Pair[])} */
+ @Deprecated
+ public void action(Context context, int category, boolean value) {
+ MetricsLogger.action(context, category, value);
+ }
+
+ public void action(Context context, int category, String pkg,
+ Pair<Integer, Object>... taggedData) {
+ if (taggedData == null || taggedData.length == 0) {
+ MetricsLogger.action(context, category, pkg);
+ } else {
+ final LogMaker logMaker = new LogMaker(category)
+ .setType(MetricsProto.MetricsEvent.TYPE_ACTION)
+ .setPackageName(pkg);
+ for (Pair<Integer, Object> pair : taggedData) {
+ logMaker.addTaggedData(pair.first, pair.second);
+ }
+ MetricsLogger.action(logMaker);
+ }
+ }
+
+ public void count(Context context, String name, int value) {
+ MetricsLogger.count(context, name, value);
+ }
+
+ public void histogram(Context context, String name, int bucket) {
+ MetricsLogger.histogram(context, name, bucket);
+ }
+}
diff --git a/android/support/design/widget/ShadowViewDelegate.java b/com/android/settingslib/core/instrumentation/Instrumentable.java
index 83a3a7a1..dbc61c26 100644
--- a/android/support/design/widget/ShadowViewDelegate.java
+++ b/com/android/settingslib/core/instrumentation/Instrumentable.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 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,13 +14,15 @@
* limitations under the License.
*/
-package android.support.design.widget;
+package com.android.settingslib.core.instrumentation;
-import android.graphics.drawable.Drawable;
+public interface Instrumentable {
-interface ShadowViewDelegate {
- float getRadius();
- void setShadowPadding(int left, int top, int right, int bottom);
- void setBackgroundDrawable(Drawable background);
- boolean isCompatPaddingEnabled();
+ int METRICS_CATEGORY_UNKNOWN = 0;
+
+ /**
+ * Instrumented name for a view as defined in
+ * {@link com.android.internal.logging.nano.MetricsProto.MetricsEvent}.
+ */
+ int getMetricsCategory();
}
diff --git a/com/android/settingslib/core/instrumentation/LogWriter.java b/com/android/settingslib/core/instrumentation/LogWriter.java
new file mode 100644
index 00000000..4b9f5727
--- /dev/null
+++ b/com/android/settingslib/core/instrumentation/LogWriter.java
@@ -0,0 +1,84 @@
+/*
+ * 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.settingslib.core.instrumentation;
+
+import android.content.Context;
+import android.util.Pair;
+
+/**
+ * Generic log writer interface.
+ */
+public interface LogWriter {
+
+ /**
+ * Logs a visibility event when view becomes visible.
+ */
+ void visible(Context context, int source, int category);
+
+ /**
+ * Logs a visibility event when view becomes hidden.
+ */
+ void hidden(Context context, int category);
+
+ /**
+ * Logs a user action.
+ */
+ void action(int category, int value, Pair<Integer, Object>... taggedData);
+
+ /**
+ * Logs a user action.
+ */
+ void action(int category, boolean value, Pair<Integer, Object>... taggedData);
+
+ /**
+ * Logs an user action.
+ */
+ void action(Context context, int category, Pair<Integer, Object>... taggedData);
+
+ /**
+ * Logs an user action.
+ */
+ void actionWithSource(Context context, int source, int category);
+
+ /**
+ * Logs an user action.
+ * @deprecated use {@link #action(int, int, Pair[])}
+ */
+ @Deprecated
+ void action(Context context, int category, int value);
+
+ /**
+ * Logs an user action.
+ * @deprecated use {@link #action(int, boolean, Pair[])}
+ */
+ @Deprecated
+ void action(Context context, int category, boolean value);
+
+ /**
+ * Logs an user action.
+ */
+ void action(Context context, int category, String pkg, Pair<Integer, Object>... taggedData);
+
+ /**
+ * Logs a count.
+ */
+ void count(Context context, String name, int value);
+
+ /**
+ * Logs a histogram event.
+ */
+ void histogram(Context context, String name, int bucket);
+}
diff --git a/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java b/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java
new file mode 100644
index 00000000..1e5b378e
--- /dev/null
+++ b/com/android/settingslib/core/instrumentation/MetricsFeatureProvider.java
@@ -0,0 +1,159 @@
+/*
+ * 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.settingslib.core.instrumentation;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * FeatureProvider for metrics.
+ */
+public class MetricsFeatureProvider {
+ private List<LogWriter> mLoggerWriters;
+
+ public MetricsFeatureProvider() {
+ mLoggerWriters = new ArrayList<>();
+ installLogWriters();
+ }
+
+ protected void installLogWriters() {
+ mLoggerWriters.add(new EventLogWriter());
+ }
+
+ public void visible(Context context, int source, int category) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.visible(context, source, category);
+ }
+ }
+
+ public void hidden(Context context, int category) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.hidden(context, category);
+ }
+ }
+
+ public void actionWithSource(Context context, int source, int category) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.actionWithSource(context, source, category);
+ }
+ }
+
+ /**
+ * Logs a user action. Includes the elapsed time since the containing
+ * fragment has been visible.
+ */
+ public void action(VisibilityLoggerMixin visibilityLogger, int category, int value) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.action(category, value,
+ sinceVisibleTaggedData(visibilityLogger.elapsedTimeSinceVisible()));
+ }
+ }
+
+ /**
+ * Logs a user action. Includes the elapsed time since the containing
+ * fragment has been visible.
+ */
+ public void action(VisibilityLoggerMixin visibilityLogger, int category, boolean value) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.action(category, value,
+ sinceVisibleTaggedData(visibilityLogger.elapsedTimeSinceVisible()));
+ }
+ }
+
+ public void action(Context context, int category, Pair<Integer, Object>... taggedData) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.action(context, category, taggedData);
+ }
+ }
+
+ /** @deprecated use {@link #action(VisibilityLoggerMixin, int, int)} */
+ @Deprecated
+ public void action(Context context, int category, int value) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.action(context, category, value);
+ }
+ }
+
+ /** @deprecated use {@link #action(VisibilityLoggerMixin, int, boolean)} */
+ @Deprecated
+ public void action(Context context, int category, boolean value) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.action(context, category, value);
+ }
+ }
+
+ public void action(Context context, int category, String pkg,
+ Pair<Integer, Object>... taggedData) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.action(context, category, pkg, taggedData);
+ }
+ }
+
+ public void count(Context context, String name, int value) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.count(context, name, value);
+ }
+ }
+
+ public void histogram(Context context, String name, int bucket) {
+ for (LogWriter writer : mLoggerWriters) {
+ writer.histogram(context, name, bucket);
+ }
+ }
+
+ public int getMetricsCategory(Object object) {
+ if (object == null || !(object instanceof Instrumentable)) {
+ return MetricsEvent.VIEW_UNKNOWN;
+ }
+ return ((Instrumentable) object).getMetricsCategory();
+ }
+
+ public void logDashboardStartIntent(Context context, Intent intent,
+ int sourceMetricsCategory) {
+ if (intent == null) {
+ return;
+ }
+ final ComponentName cn = intent.getComponent();
+ if (cn == null) {
+ final String action = intent.getAction();
+ if (TextUtils.isEmpty(action)) {
+ // Not loggable
+ return;
+ }
+ action(context, MetricsEvent.ACTION_SETTINGS_TILE_CLICK, action,
+ Pair.create(MetricsEvent.FIELD_CONTEXT, sourceMetricsCategory));
+ return;
+ } else if (TextUtils.equals(cn.getPackageName(), context.getPackageName())) {
+ // Going to a Setting internal page, skip click logging in favor of page's own
+ // visibility logging.
+ return;
+ }
+ action(context, MetricsEvent.ACTION_SETTINGS_TILE_CLICK, cn.flattenToString(),
+ Pair.create(MetricsEvent.FIELD_CONTEXT, sourceMetricsCategory));
+ }
+
+ private Pair<Integer, Object> sinceVisibleTaggedData(long timestamp) {
+ return Pair.create(MetricsEvent.NOTIFICATION_SINCE_VISIBLE_MILLIS, timestamp);
+ }
+}
diff --git a/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java b/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java
new file mode 100644
index 00000000..facce4e0
--- /dev/null
+++ b/com/android/settingslib/core/instrumentation/SharedPreferencesLogger.java
@@ -0,0 +1,259 @@
+/*
+ * 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.settingslib.core.instrumentation;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.AsyncTask;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentSkipListSet;
+
+public class SharedPreferencesLogger implements SharedPreferences {
+
+ private static final String LOG_TAG = "SharedPreferencesLogger";
+
+ private final String mTag;
+ private final Context mContext;
+ private final MetricsFeatureProvider mMetricsFeature;
+ private final Set<String> mPreferenceKeySet;
+
+ public SharedPreferencesLogger(Context context, String tag,
+ MetricsFeatureProvider metricsFeature) {
+ mContext = context;
+ mTag = tag;
+ mMetricsFeature = metricsFeature;
+ mPreferenceKeySet = new ConcurrentSkipListSet<>();
+ }
+
+ @Override
+ public Map<String, ?> getAll() {
+ return null;
+ }
+
+ @Override
+ public String getString(String key, @Nullable String defValue) {
+ return defValue;
+ }
+
+ @Override
+ public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
+ return defValues;
+ }
+
+ @Override
+ public int getInt(String key, int defValue) {
+ return defValue;
+ }
+
+ @Override
+ public long getLong(String key, long defValue) {
+ return defValue;
+ }
+
+ @Override
+ public float getFloat(String key, float defValue) {
+ return defValue;
+ }
+
+ @Override
+ public boolean getBoolean(String key, boolean defValue) {
+ return defValue;
+ }
+
+ @Override
+ public boolean contains(String key) {
+ return false;
+ }
+
+ @Override
+ public Editor edit() {
+ return new EditorLogger();
+ }
+
+ @Override
+ public void registerOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ }
+
+ @Override
+ public void unregisterOnSharedPreferenceChangeListener(
+ OnSharedPreferenceChangeListener listener) {
+ }
+
+ private void logValue(String key, Object value) {
+ logValue(key, value, false /* forceLog */);
+ }
+
+ private void logValue(String key, Object value, boolean forceLog) {
+ final String prefKey = buildPrefKey(mTag, key);
+ if (!forceLog && !mPreferenceKeySet.contains(prefKey)) {
+ // Pref key doesn't exist in set, this is initial display so we skip metrics but
+ // keeps track of this key.
+ mPreferenceKeySet.add(prefKey);
+ return;
+ }
+ // TODO: Remove count logging to save some resource.
+ mMetricsFeature.count(mContext, buildCountName(prefKey, value), 1);
+
+ final Pair<Integer, Object> valueData;
+ if (value instanceof Long) {
+ final Long longVal = (Long) value;
+ final int intVal;
+ if (longVal > Integer.MAX_VALUE) {
+ intVal = Integer.MAX_VALUE;
+ } else if (longVal < Integer.MIN_VALUE) {
+ intVal = Integer.MIN_VALUE;
+ } else {
+ intVal = longVal.intValue();
+ }
+ valueData = Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE,
+ intVal);
+ } else if (value instanceof Integer) {
+ valueData = Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE,
+ value);
+ } else if (value instanceof Boolean) {
+ valueData = Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_INT_VALUE,
+ (Boolean) value ? 1 : 0);
+ } else if (value instanceof Float) {
+ valueData = Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_FLOAT_VALUE,
+ value);
+ } else if (value instanceof String) {
+ Log.d(LOG_TAG, "Tried to log string preference " + prefKey + " = " + value);
+ valueData = null;
+ } else {
+ Log.w(LOG_TAG, "Tried to log unloggable object" + value);
+ valueData = null;
+ }
+ if (valueData != null) {
+ // Pref key exists in set, log it's change in metrics.
+ mMetricsFeature.action(mContext, MetricsEvent.ACTION_SETTINGS_PREFERENCE_CHANGE,
+ Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME, prefKey),
+ valueData);
+ }
+ }
+
+ @VisibleForTesting
+ void logPackageName(String key, String value) {
+ final String prefKey = mTag + "/" + key;
+ mMetricsFeature.action(mContext, MetricsEvent.ACTION_SETTINGS_PREFERENCE_CHANGE, value,
+ Pair.create(MetricsEvent.FIELD_SETTINGS_PREFERENCE_CHANGE_NAME, prefKey));
+ }
+
+ private void safeLogValue(String key, String value) {
+ new AsyncPackageCheck().executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, key, value);
+ }
+
+ public static String buildCountName(String prefKey, Object value) {
+ return prefKey + "|" + value;
+ }
+
+ public static String buildPrefKey(String tag, String key) {
+ return tag + "/" + key;
+ }
+
+ private class AsyncPackageCheck extends AsyncTask<String, Void, Void> {
+ @Override
+ protected Void doInBackground(String... params) {
+ String key = params[0];
+ String value = params[1];
+ PackageManager pm = mContext.getPackageManager();
+ try {
+ // Check if this might be a component.
+ ComponentName name = ComponentName.unflattenFromString(value);
+ if (value != null) {
+ value = name.getPackageName();
+ }
+ } catch (Exception e) {
+ }
+ try {
+ pm.getPackageInfo(value, PackageManager.MATCH_ANY_USER);
+ logPackageName(key, value);
+ } catch (PackageManager.NameNotFoundException e) {
+ // Clearly not a package, and it's unlikely this preference is in prefSet, so
+ // lets force log it.
+ logValue(key, value, true /* forceLog */);
+ }
+ return null;
+ }
+ }
+
+ public class EditorLogger implements Editor {
+ @Override
+ public Editor putString(String key, @Nullable String value) {
+ safeLogValue(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putStringSet(String key, @Nullable Set<String> values) {
+ safeLogValue(key, TextUtils.join(",", values));
+ return this;
+ }
+
+ @Override
+ public Editor putInt(String key, int value) {
+ logValue(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putLong(String key, long value) {
+ logValue(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putFloat(String key, float value) {
+ logValue(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor putBoolean(String key, boolean value) {
+ logValue(key, value);
+ return this;
+ }
+
+ @Override
+ public Editor remove(String key) {
+ return this;
+ }
+
+ @Override
+ public Editor clear() {
+ return this;
+ }
+
+ @Override
+ public boolean commit() {
+ return true;
+ }
+
+ @Override
+ public void apply() {
+ }
+ }
+}
diff --git a/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java b/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java
new file mode 100644
index 00000000..79838962
--- /dev/null
+++ b/com/android/settingslib/core/instrumentation/VisibilityLoggerMixin.java
@@ -0,0 +1,96 @@
+/*
+ * 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.settingslib.core.instrumentation;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+
+import android.os.SystemClock;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.settingslib.core.lifecycle.LifecycleObserver;
+import com.android.settingslib.core.lifecycle.events.OnPause;
+import com.android.settingslib.core.lifecycle.events.OnResume;
+
+import static com.android.settingslib.core.instrumentation.Instrumentable.METRICS_CATEGORY_UNKNOWN;
+
+/**
+ * Logs visibility change of a fragment.
+ */
+public class VisibilityLoggerMixin implements LifecycleObserver, OnResume, OnPause {
+
+ private static final String TAG = "VisibilityLoggerMixin";
+
+ private final int mMetricsCategory;
+
+ private MetricsFeatureProvider mMetricsFeature;
+ private int mSourceMetricsCategory = MetricsProto.MetricsEvent.VIEW_UNKNOWN;
+ private long mVisibleTimestamp;
+
+ /**
+ * The metrics category constant for logging source when a setting fragment is opened.
+ */
+ public static final String EXTRA_SOURCE_METRICS_CATEGORY = ":settings:source_metrics";
+
+ private VisibilityLoggerMixin() {
+ mMetricsCategory = METRICS_CATEGORY_UNKNOWN;
+ }
+
+ public VisibilityLoggerMixin(int metricsCategory, MetricsFeatureProvider metricsFeature) {
+ mMetricsCategory = metricsCategory;
+ mMetricsFeature = metricsFeature;
+ }
+
+ @Override
+ public void onResume() {
+ mVisibleTimestamp = SystemClock.elapsedRealtime();
+ if (mMetricsFeature != null && mMetricsCategory != METRICS_CATEGORY_UNKNOWN) {
+ mMetricsFeature.visible(null /* context */, mSourceMetricsCategory, mMetricsCategory);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ mVisibleTimestamp = 0;
+ if (mMetricsFeature != null && mMetricsCategory != METRICS_CATEGORY_UNKNOWN) {
+ mMetricsFeature.hidden(null /* context */, mMetricsCategory);
+ }
+ }
+
+ /**
+ * Sets source metrics category for this logger. Source is the caller that opened this UI.
+ */
+ public void setSourceMetricsCategory(Activity activity) {
+ if (mSourceMetricsCategory != MetricsProto.MetricsEvent.VIEW_UNKNOWN || activity == null) {
+ return;
+ }
+ final Intent intent = activity.getIntent();
+ if (intent == null) {
+ return;
+ }
+ mSourceMetricsCategory = intent.getIntExtra(EXTRA_SOURCE_METRICS_CATEGORY,
+ MetricsProto.MetricsEvent.VIEW_UNKNOWN);
+ }
+
+ /** Returns elapsed time since onResume() */
+ public long elapsedTimeSinceVisible() {
+ if (mVisibleTimestamp == 0) {
+ return 0;
+ }
+ return SystemClock.elapsedRealtime() - mVisibleTimestamp;
+ }
+}
diff --git a/com/android/systemui/volume/ZenRadioLayout.java b/com/android/settingslib/notification/ZenRadioLayout.java
index 360907b0..1140028b 100644
--- a/com/android/systemui/volume/ZenRadioLayout.java
+++ b/com/android/settingslib/notification/ZenRadioLayout.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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
@@ -12,7 +12,7 @@
* permissions and limitations under the License.
*/
-package com.android.systemui.volume;
+package com.android.settingslib.notification;
import android.content.Context;
import android.util.AttributeSet;
@@ -22,7 +22,7 @@ import android.widget.LinearLayout;
/**
* Specialized layout for zen mode that allows the radio buttons to reside within
- * a RadioGroup, but also makes sure that all the heights off the radio buttons align
+ * a RadioGroup, but also makes sure that all the heights of the radio buttons align
* with the corresponding content in the second child of this view.
*/
public class ZenRadioLayout extends LinearLayout {
diff --git a/com/android/settingslib/wifi/WifiStatusTracker.java b/com/android/settingslib/wifi/WifiStatusTracker.java
index 0d67ad03..e8f52820 100644
--- a/com/android/settingslib/wifi/WifiStatusTracker.java
+++ b/com/android/settingslib/wifi/WifiStatusTracker.java
@@ -51,10 +51,7 @@ public class WifiStatusTracker {
connected = networkInfo != null && networkInfo.isConnected();
// If Connected grab the signal strength and ssid.
if (connected) {
- // try getting it out of the intent first
- WifiInfo info = intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO) != null
- ? (WifiInfo) intent.getParcelableExtra(WifiManager.EXTRA_WIFI_INFO)
- : mWifiManager.getConnectionInfo();
+ WifiInfo info = mWifiManager.getConnectionInfo();
if (info != null) {
ssid = getSsid(info);
} else {
diff --git a/com/android/settingslib/wrapper/LocationManagerWrapper.java b/com/android/settingslib/wrapper/LocationManagerWrapper.java
new file mode 100644
index 00000000..1a268a60
--- /dev/null
+++ b/com/android/settingslib/wrapper/LocationManagerWrapper.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 com.android.settingslib.wrapper;
+
+import android.location.LocationManager;
+import android.os.UserHandle;
+
+/**
+ * This class replicates some methods of android.location.LocationManager that are new and not
+ * yet available in our current version of Robolectric. It provides a thin wrapper to call the real
+ * methods in production and a mock in tests.
+ */
+public class LocationManagerWrapper {
+
+ private LocationManager mLocationManager;
+
+ public LocationManagerWrapper(LocationManager locationManager) {
+ mLocationManager = locationManager;
+ }
+
+ /** Returns the real {@code LocationManager} object */
+ public LocationManager getLocationManager() {
+ return mLocationManager;
+ }
+
+ /** Wraps {@code LocationManager.isProviderEnabled} method */
+ public boolean isProviderEnabled(String provider) {
+ return mLocationManager.isProviderEnabled(provider);
+ }
+
+ /** Wraps {@code LocationManager.setProviderEnabledForUser} method */
+ public void setProviderEnabledForUser(String provider, boolean enabled, UserHandle userHandle) {
+ mLocationManager.setProviderEnabledForUser(provider, enabled, userHandle);
+ }
+
+ /** Wraps {@code LocationManager.isLocationEnabled} method */
+ public boolean isLocationEnabled() {
+ return mLocationManager.isLocationEnabled();
+ }
+
+ /** Wraps {@code LocationManager.isLocationEnabledForUser} method */
+ public boolean isLocationEnabledForUser(UserHandle userHandle) {
+ return mLocationManager.isLocationEnabledForUser(userHandle);
+ }
+
+ /** Wraps {@code LocationManager.setLocationEnabledForUser} method */
+ public void setLocationEnabledForUser(boolean enabled, UserHandle userHandle) {
+ mLocationManager.setLocationEnabledForUser(enabled, userHandle);
+ }
+}
diff --git a/com/android/setupwizardlib/GlifLayout.java b/com/android/setupwizardlib/GlifLayout.java
index dd0963b0..e1d9d701 100644
--- a/com/android/setupwizardlib/GlifLayout.java
+++ b/com/android/setupwizardlib/GlifLayout.java
@@ -141,6 +141,11 @@ public class GlifLayout extends TemplateLayout {
inflateFooter(footer);
}
+ final int stickyHeader = a.getResourceId(R.styleable.SuwGlifLayout_suwStickyHeader, 0);
+ if (stickyHeader != 0) {
+ inflateStickyHeader(stickyHeader);
+ }
+
mLayoutFullscreen = a.getBoolean(R.styleable.SuwGlifLayout_suwLayoutFullscreen, true);
a.recycle();
@@ -168,17 +173,31 @@ public class GlifLayout extends TemplateLayout {
/**
* Sets the footer of the layout, which is at the bottom of the content area outside the
- * scrolling container. The footer can only be inflated once per layout.
+ * scrolling container. The footer can only be inflated once per instance of this layout.
*
* @param footer The layout to be inflated as footer.
* @return The root of the inflated footer view.
*/
public View inflateFooter(@LayoutRes int footer) {
- ViewStub footerStub = (ViewStub) findManagedViewById(R.id.suw_layout_footer);
+ ViewStub footerStub = findManagedViewById(R.id.suw_layout_footer);
footerStub.setLayoutResource(footer);
return footerStub.inflate();
}
+ /**
+ * Sets the sticky header (i.e. header that doesn't scroll) of the layout, which is at the top
+ * of the content area outside of the scrolling container. The header can only be inflated once
+ * per instance of this layout.
+ *
+ * @param header The layout to be inflated as the header.
+ * @return The root of the inflated header view.
+ */
+ public View inflateStickyHeader(@LayoutRes int header) {
+ ViewStub stickyHeaderStub = findManagedViewById(R.id.suw_layout_sticky_header);
+ stickyHeaderStub.setLayoutResource(header);
+ return stickyHeaderStub.inflate();
+ }
+
public ScrollView getScrollView() {
final View view = findManagedViewById(R.id.suw_scroll_view);
return view instanceof ScrollView ? (ScrollView) view : null;
diff --git a/com/android/setupwizardlib/GlifLayoutTest.java b/com/android/setupwizardlib/GlifLayoutTest.java
index 360dfe22..e07d5faa 100644
--- a/com/android/setupwizardlib/GlifLayoutTest.java
+++ b/com/android/setupwizardlib/GlifLayoutTest.java
@@ -267,6 +267,47 @@ public class GlifLayoutTest {
assertNotNull(layout.findViewById(android.R.id.text1));
}
+ @Test
+ public void inflateStickyHeader_shouldAddViewToLayout() {
+ GlifLayout layout = new GlifLayout(mContext);
+
+ final View view = layout.inflateStickyHeader(android.R.layout.simple_list_item_1);
+ assertEquals(android.R.id.text1, view.getId());
+ assertNotNull(layout.findViewById(android.R.id.text1));
+ }
+
+ @Config(qualifiers = "sw600dp")
+ @Test
+ public void inflateStickyHeader_whenOnTablets_shouldAddViewToLayout() {
+ inflateStickyHeader_shouldAddViewToLayout();
+ }
+
+ @Test
+ public void inflateStickyHeader_whenInXml_shouldAddViewToLayout() {
+ GlifLayout layout = new GlifLayout(
+ mContext,
+ Robolectric.buildAttributeSet()
+ .addAttribute(R.attr.suwStickyHeader, "@android:layout/simple_list_item_1")
+ .build());
+
+ assertNotNull(layout.findViewById(android.R.id.text1));
+ }
+
+ @Test
+ public void inflateStickyHeader_whenOnBlankTemplate_shouldAddViewToLayout() {
+ GlifLayout layout = new GlifLayout(mContext, R.layout.suw_glif_blank_template);
+
+ final View view = layout.inflateStickyHeader(android.R.layout.simple_list_item_1);
+ assertEquals(android.R.id.text1, view.getId());
+ assertNotNull(layout.findViewById(android.R.id.text1));
+ }
+
+ @Config(qualifiers = "sw600dp")
+ @Test
+ public void inflateStickyHeader_whenOnBlankTemplateTablet_shouldAddViewToLayout() {
+ inflateStickyHeader_whenOnBlankTemplate_shouldAddViewToLayout();
+ }
+
@Config(sdk = { VERSION_CODES.M, Config.NEWEST_SDK })
@Test
public void createFromXml_shouldSetLayoutFullscreen_whenLayoutFullscreenIsNotSet() {
diff --git a/com/android/setupwizardlib/GlifRecyclerLayout.java b/com/android/setupwizardlib/GlifRecyclerLayout.java
index 75b1c7a4..b681dee7 100644
--- a/com/android/setupwizardlib/GlifRecyclerLayout.java
+++ b/com/android/setupwizardlib/GlifRecyclerLayout.java
@@ -107,10 +107,10 @@ public class GlifRecyclerLayout extends GlifLayout {
}
@Override
- public View findManagedViewById(int id) {
+ public <T extends View> T findManagedViewById(int id) {
final View header = mRecyclerMixin.getHeader();
if (header != null) {
- final View view = header.findViewById(id);
+ final T view = header.findViewById(id);
if (view != null) {
return view;
}
diff --git a/com/android/setupwizardlib/SetupWizardRecyclerLayout.java b/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
index 5ff825d8..c5b5afc5 100644
--- a/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
+++ b/com/android/setupwizardlib/SetupWizardRecyclerLayout.java
@@ -127,10 +127,10 @@ public class SetupWizardRecyclerLayout extends SetupWizardLayout {
}
@Override
- public View findManagedViewById(int id) {
+ public <T extends View> T findManagedViewById(int id) {
final View header = mRecyclerMixin.getHeader();
if (header != null) {
- final View view = header.findViewById(id);
+ final T view = header.findViewById(id);
if (view != null) {
return view;
}
diff --git a/com/android/setupwizardlib/TemplateLayout.java b/com/android/setupwizardlib/TemplateLayout.java
index 771592f4..bd430db3 100644
--- a/com/android/setupwizardlib/TemplateLayout.java
+++ b/com/android/setupwizardlib/TemplateLayout.java
@@ -103,7 +103,7 @@ public class TemplateLayout extends FrameLayout {
* by this view but not currently added to the view hierarchy. e.g. recycler view or list view
* headers that are not currently shown.
*/
- public View findManagedViewById(int id) {
+ public <T extends View> T findManagedViewById(int id) {
return findViewById(id);
}
diff --git a/com/android/setupwizardlib/items/ButtonBarItem.java b/com/android/setupwizardlib/items/ButtonBarItem.java
index 55bbe758..06ce4acc 100644
--- a/com/android/setupwizardlib/items/ButtonBarItem.java
+++ b/com/android/setupwizardlib/items/ButtonBarItem.java
@@ -83,6 +83,7 @@ public class ButtonBarItem extends AbstractItem implements ItemInflater.ItemPare
return mVisible;
}
+ @Override
public int getViewId() {
return getId();
}
diff --git a/com/android/setupwizardlib/test/GlifPatternDrawableTest.java b/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
index 99c87fb9..37ac41a9 100644
--- a/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
+++ b/com/android/setupwizardlib/test/GlifPatternDrawableTest.java
@@ -137,7 +137,7 @@ public class GlifPatternDrawableTest {
assertEquals("Matrices should match", expected, canvas.getMatrix());
}
- @SmallTest
+ @Test
public void testScaleToCanvasMaxSize() {
final Canvas canvas = new Canvas();
final Matrix expected = new Matrix(canvas.getMatrix());
diff --git a/com/android/shell/BugreportProgressService.java b/com/android/shell/BugreportProgressService.java
index a8b184c3..600f0dc3 100644
--- a/com/android/shell/BugreportProgressService.java
+++ b/com/android/shell/BugreportProgressService.java
@@ -616,7 +616,7 @@ public class BugreportProgressService extends Service {
final IWindowManager wm = IWindowManager.Stub
.asInterface(ServiceManager.getService(Context.WINDOW_SERVICE));
try {
- wm.dismissKeyguard(null);
+ wm.dismissKeyguard(null, null);
} catch (Exception e) {
// ignore it
}
@@ -1909,7 +1909,7 @@ public class BugreportProgressService extends Service {
}
final IDumpstate dumpstate = IDumpstate.Stub.asInterface(service);
try {
- token = dumpstate.setListener("Shell", this);
+ token = dumpstate.setListener("Shell", this, /* perSectionDetails= */ false);
if (token != null) {
token.asBinder().linkToDeath(this, 0);
}
@@ -1978,6 +1978,15 @@ public class BugreportProgressService extends Service {
info.realMax = maxProgress;
}
+ @Override
+ public void onSectionComplete(String title, int status, int size, int durationMs)
+ throws RemoteException {
+ if (DEBUG) {
+ Log.v(TAG, "Title: " + title + " Status: " + status + " Size: " + size
+ + " Duration: " + durationMs + "ms");
+ }
+ }
+
public void dump(String prefix, PrintWriter pw) {
pw.print(prefix); pw.print("token: "); pw.println(token);
}
diff --git a/com/android/support/mediarouter/app/MediaRouteActionProvider.java b/com/android/support/mediarouter/app/MediaRouteActionProvider.java
new file mode 100644
index 00000000..d3e8d47f
--- /dev/null
+++ b/com/android/support/mediarouter/app/MediaRouteActionProvider.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.app;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.support.v4.view.ActionProvider;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.support.mediarouter.media.MediaRouteSelector;
+import com.android.support.mediarouter.media.MediaRouter;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * The media route action provider displays a {@link MediaRouteButton media route button}
+ * in the application's {@link ActionBar} to allow the user to select routes and
+ * to control the currently selected route.
+ * <p>
+ * The application must specify the kinds of routes that the user should be allowed
+ * to select by specifying a {@link MediaRouteSelector selector} with the
+ * {@link #setRouteSelector} method.
+ * </p><p>
+ * Refer to {@link MediaRouteButton} for a description of the button that will
+ * appear in the action bar menu. Note that instead of disabling the button
+ * when no routes are available, the action provider will instead make the
+ * menu item invisible. In this way, the button will only be visible when it
+ * is possible for the user to discover and select a matching route.
+ * </p>
+ *
+ * <h3>Prerequisites</h3>
+ * <p>
+ * To use the media route action provider, the activity must be a subclass of
+ * {@link AppCompatActivity} from the <code>android.support.v7.appcompat</code>
+ * support library. Refer to support library documentation for details.
+ * </p>
+ *
+ * <h3>Example</h3>
+ * <p>
+ * </p><p>
+ * The application should define a menu resource to include the provider in the
+ * action bar options menu. Note that the support library action bar uses attributes
+ * that are defined in the application's resource namespace rather than the framework's
+ * resource namespace to configure each item.
+ * </p><pre>
+ * &lt;menu xmlns:android="http://schemas.android.com/apk/res/android"
+ * xmlns:app="http://schemas.android.com/apk/res-auto">
+ * &lt;item android:id="@+id/media_route_menu_item"
+ * android:title="@string/media_route_menu_title"
+ * app:showAsAction="always"
+ * app:actionProviderClass="android.support.v7.app.MediaRouteActionProvider"/>
+ * &lt;/menu>
+ * </pre><p>
+ * Then configure the menu and set the route selector for the chooser.
+ * </p><pre>
+ * public class MyActivity extends AppCompatActivity {
+ * private MediaRouter mRouter;
+ * private MediaRouter.Callback mCallback;
+ * private MediaRouteSelector mSelector;
+ *
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ *
+ * mRouter = Mediarouter.getInstance(this);
+ * mSelector = new MediaRouteSelector.Builder()
+ * .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
+ * .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
+ * .build();
+ * mCallback = new MyCallback();
+ * }
+ *
+ * // Add the callback on start to tell the media router what kinds of routes
+ * // the application is interested in so that it can try to discover suitable ones.
+ * public void onStart() {
+ * super.onStart();
+ *
+ * mediaRouter.addCallback(mSelector, mCallback,
+ * MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
+ *
+ * MediaRouter.RouteInfo route = mediaRouter.updateSelectedRoute(mSelector);
+ * // do something with the route...
+ * }
+ *
+ * // Remove the selector on stop to tell the media router that it no longer
+ * // needs to invest effort trying to discover routes of these kinds for now.
+ * public void onStop() {
+ * super.onStop();
+ *
+ * mediaRouter.removeCallback(mCallback);
+ * }
+ *
+ * public boolean onCreateOptionsMenu(Menu menu) {
+ * super.onCreateOptionsMenu(menu);
+ *
+ * getMenuInflater().inflate(R.menu.sample_media_router_menu, menu);
+ *
+ * MenuItem mediaRouteMenuItem = menu.findItem(R.id.media_route_menu_item);
+ * MediaRouteActionProvider mediaRouteActionProvider =
+ * (MediaRouteActionProvider)MenuItemCompat.getActionProvider(mediaRouteMenuItem);
+ * mediaRouteActionProvider.setRouteSelector(mSelector);
+ * return true;
+ * }
+ *
+ * private final class MyCallback extends MediaRouter.Callback {
+ * // Implement callback methods as needed.
+ * }
+ * }
+ * </pre>
+ *
+ * @see #setRouteSelector
+ */
+public class MediaRouteActionProvider extends ActionProvider {
+ private static final String TAG = "MediaRouteActionProvider";
+
+ private final MediaRouter mRouter;
+ private final MediaRouterCallback mCallback;
+
+ private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
+ private MediaRouteDialogFactory mDialogFactory = MediaRouteDialogFactory.getDefault();
+ private MediaRouteButton mButton;
+
+ /**
+ * Creates the action provider.
+ *
+ * @param context The context.
+ */
+ public MediaRouteActionProvider(Context context) {
+ super(context);
+
+ mRouter = MediaRouter.getInstance(context);
+ mCallback = new MediaRouterCallback(this);
+ }
+
+ /**
+ * Gets the media route selector for filtering the routes that the user can
+ * select using the media route chooser dialog.
+ *
+ * @return The selector, never null.
+ */
+ @NonNull
+ public MediaRouteSelector getRouteSelector() {
+ return mSelector;
+ }
+
+ /**
+ * Sets the media route selector for filtering the routes that the user can
+ * select using the media route chooser dialog.
+ *
+ * @param selector The selector, must not be null.
+ */
+ public void setRouteSelector(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ if (!mSelector.equals(selector)) {
+ // FIXME: We currently have no way of knowing whether the action provider
+ // is still needed by the UI. Unfortunately this means the action provider
+ // may leak callbacks until garbage collection occurs. This may result in
+ // media route providers doing more work than necessary in the short term
+ // while trying to discover routes that are no longer of interest to the
+ // application. To solve this problem, the action provider will need some
+ // indication from the framework that it is being destroyed.
+ if (!mSelector.isEmpty()) {
+ mRouter.removeCallback(mCallback);
+ }
+ if (!selector.isEmpty()) {
+ mRouter.addCallback(selector, mCallback);
+ }
+ mSelector = selector;
+ refreshRoute();
+
+ if (mButton != null) {
+ mButton.setRouteSelector(selector);
+ }
+ }
+ }
+
+ /**
+ * Gets the media route dialog factory to use when showing the route chooser
+ * or controller dialog.
+ *
+ * @return The dialog factory, never null.
+ */
+ @NonNull
+ public MediaRouteDialogFactory getDialogFactory() {
+ return mDialogFactory;
+ }
+
+ /**
+ * Sets the media route dialog factory to use when showing the route chooser
+ * or controller dialog.
+ *
+ * @param factory The dialog factory, must not be null.
+ */
+ public void setDialogFactory(@NonNull MediaRouteDialogFactory factory) {
+ if (factory == null) {
+ throw new IllegalArgumentException("factory must not be null");
+ }
+
+ if (mDialogFactory != factory) {
+ mDialogFactory = factory;
+
+ if (mButton != null) {
+ mButton.setDialogFactory(factory);
+ }
+ }
+ }
+
+ /**
+ * Gets the associated media route button, or null if it has not yet been created.
+ */
+ @Nullable
+ public MediaRouteButton getMediaRouteButton() {
+ return mButton;
+ }
+
+ /**
+ * Called when the media route button is being created.
+ * <p>
+ * Subclasses may override this method to customize the button.
+ * </p>
+ */
+ public MediaRouteButton onCreateMediaRouteButton() {
+ return new MediaRouteButton(getContext());
+ }
+
+ @Override
+ @SuppressWarnings("deprecation")
+ public View onCreateActionView() {
+ if (mButton != null) {
+ Log.e(TAG, "onCreateActionView: this ActionProvider is already associated " +
+ "with a menu item. Don't reuse MediaRouteActionProvider instances! " +
+ "Abandoning the old menu item...");
+ }
+
+ mButton = onCreateMediaRouteButton();
+ mButton.setCheatSheetEnabled(true);
+ mButton.setRouteSelector(mSelector);
+ mButton.setDialogFactory(mDialogFactory);
+ mButton.setLayoutParams(new ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.MATCH_PARENT));
+ return mButton;
+ }
+
+ @Override
+ public boolean onPerformDefaultAction() {
+ if (mButton != null) {
+ return mButton.showDialog();
+ }
+ return false;
+ }
+
+ @Override
+ public boolean overridesItemVisibility() {
+ return true;
+ }
+
+ @Override
+ public boolean isVisible() {
+ return mRouter.isRouteAvailable(mSelector,
+ MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE);
+ }
+
+ void refreshRoute() {
+ refreshVisibility();
+ }
+
+ private static final class MediaRouterCallback extends MediaRouter.Callback {
+ private final WeakReference<MediaRouteActionProvider> mProviderWeak;
+
+ public MediaRouterCallback(MediaRouteActionProvider provider) {
+ mProviderWeak = new WeakReference<MediaRouteActionProvider>(provider);
+ }
+
+ @Override
+ public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute(router);
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute(router);
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute(router);
+ }
+
+ @Override
+ public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ refreshRoute(router);
+ }
+
+ @Override
+ public void onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ refreshRoute(router);
+ }
+
+ @Override
+ public void onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ refreshRoute(router);
+ }
+
+ private void refreshRoute(MediaRouter router) {
+ MediaRouteActionProvider provider = mProviderWeak.get();
+ if (provider != null) {
+ provider.refreshRoute();
+ } else {
+ router.removeCallback(this);
+ }
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/app/MediaRouteButton.java b/com/android/support/mediarouter/app/MediaRouteButton.java
new file mode 100644
index 00000000..65fc88c0
--- /dev/null
+++ b/com/android/support/mediarouter/app/MediaRouteButton.java
@@ -0,0 +1,620 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.app;
+
+import android.annotation.NonNull;
+import android.app.Activity;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.support.v4.app.FragmentActivity;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v7.widget.TooltipCompat;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.SoundEffectConstants;
+import android.view.View;
+
+import com.android.media.update.ApiHelper;
+import com.android.media.update.R;
+import com.android.support.mediarouter.media.MediaRouteSelector;
+import com.android.support.mediarouter.media.MediaRouter;
+
+/**
+ * The media route button allows the user to select routes and to control the
+ * currently selected route.
+ * <p>
+ * The application must specify the kinds of routes that the user should be allowed
+ * to select by specifying a {@link MediaRouteSelector selector} with the
+ * {@link #setRouteSelector} method.
+ * </p><p>
+ * When the default route is selected or when the currently selected route does not
+ * match the {@link #getRouteSelector() selector}, the button will appear in
+ * an inactive state indicating that the application is not connected to a
+ * route of the kind that it wants to use. Clicking on the button opens
+ * a {@link MediaRouteChooserDialog} to allow the user to select a route.
+ * If no non-default routes match the selector and it is not possible for an active
+ * scan to discover any matching routes, then the button is disabled and cannot
+ * be clicked.
+ * </p><p>
+ * When a non-default route is selected that matches the selector, the button will
+ * appear in an active state indicating that the application is connected
+ * to a route of the kind that it wants to use. The button may also appear
+ * in an intermediary connecting state if the route is in the process of connecting
+ * to the destination but has not yet completed doing so. In either case, clicking
+ * on the button opens a {@link MediaRouteControllerDialog} to allow the user
+ * to control or disconnect from the current route.
+ * </p>
+ *
+ * <h3>Prerequisites</h3>
+ * <p>
+ * To use the media route button, the activity must be a subclass of
+ * {@link FragmentActivity} from the <code>android.support.v4</code>
+ * support library. Refer to support library documentation for details.
+ * </p>
+ *
+ * @see MediaRouteActionProvider
+ * @see #setRouteSelector
+ */
+public class MediaRouteButton extends View {
+ private static final String TAG = "MediaRouteButton";
+
+ private static final String CHOOSER_FRAGMENT_TAG =
+ "android.support.v7.mediarouter:MediaRouteChooserDialogFragment";
+ private static final String CONTROLLER_FRAGMENT_TAG =
+ "android.support.v7.mediarouter:MediaRouteControllerDialogFragment";
+
+ private final MediaRouter mRouter;
+ private final MediaRouterCallback mCallback;
+
+ private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
+ private MediaRouteDialogFactory mDialogFactory = MediaRouteDialogFactory.getDefault();
+
+ private boolean mAttachedToWindow;
+
+ private static final SparseArray<Drawable.ConstantState> sRemoteIndicatorCache =
+ new SparseArray<>(2);
+ private RemoteIndicatorLoader mRemoteIndicatorLoader;
+ private Drawable mRemoteIndicator;
+ private boolean mRemoteActive;
+ private boolean mIsConnecting;
+
+ private ColorStateList mButtonTint;
+ private int mMinWidth;
+ private int mMinHeight;
+
+ // The checked state is used when connected to a remote route.
+ private static final int[] CHECKED_STATE_SET = {
+ android.R.attr.state_checked
+ };
+
+ // The checkable state is used while connecting to a remote route.
+ private static final int[] CHECKABLE_STATE_SET = {
+ android.R.attr.state_checkable
+ };
+
+ public MediaRouteButton(Context context) {
+ this(context, null);
+ }
+
+ public MediaRouteButton(Context context, AttributeSet attrs) {
+ this(context, attrs, R.attr.mediaRouteButtonStyle);
+ }
+
+ public MediaRouteButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(MediaRouterThemeHelper.createThemedButtonContext(context), attrs, defStyleAttr);
+ context = getContext();
+
+ mRouter = MediaRouter.getInstance(context);
+ mCallback = new MediaRouterCallback();
+
+ Resources.Theme theme = ApiHelper.getLibResources().newTheme();
+ theme.applyStyle(MediaRouterThemeHelper.getRouterThemeId(context), true);
+ TypedArray a = theme.obtainStyledAttributes(attrs,
+ R.styleable.MediaRouteButton, defStyleAttr, 0);
+
+ mButtonTint = a.getColorStateList(R.styleable.MediaRouteButton_mediaRouteButtonTint);
+ mMinWidth = a.getDimensionPixelSize(
+ R.styleable.MediaRouteButton_android_minWidth, 0);
+ mMinHeight = a.getDimensionPixelSize(
+ R.styleable.MediaRouteButton_android_minHeight, 0);
+ int remoteIndicatorResId = a.getResourceId(
+ R.styleable.MediaRouteButton_externalRouteEnabledDrawable, 0);
+ a.recycle();
+
+ if (remoteIndicatorResId != 0) {
+ Drawable.ConstantState remoteIndicatorState =
+ sRemoteIndicatorCache.get(remoteIndicatorResId);
+ if (remoteIndicatorState != null) {
+ setRemoteIndicatorDrawable(remoteIndicatorState.newDrawable());
+ } else {
+ mRemoteIndicatorLoader = new RemoteIndicatorLoader(remoteIndicatorResId);
+ mRemoteIndicatorLoader.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ }
+
+ updateContentDescription();
+ setClickable(true);
+ }
+
+ /**
+ * Gets the media route selector for filtering the routes that the user can
+ * select using the media route chooser dialog.
+ *
+ * @return The selector, never null.
+ */
+ @NonNull
+ public MediaRouteSelector getRouteSelector() {
+ return mSelector;
+ }
+
+ /**
+ * Sets the media route selector for filtering the routes that the user can
+ * select using the media route chooser dialog.
+ *
+ * @param selector The selector, must not be null.
+ */
+ public void setRouteSelector(MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ if (!mSelector.equals(selector)) {
+ if (mAttachedToWindow) {
+ if (!mSelector.isEmpty()) {
+ mRouter.removeCallback(mCallback);
+ }
+ if (!selector.isEmpty()) {
+ mRouter.addCallback(selector, mCallback);
+ }
+ }
+ mSelector = selector;
+ refreshRoute();
+ }
+ }
+
+ /**
+ * Gets the media route dialog factory to use when showing the route chooser
+ * or controller dialog.
+ *
+ * @return The dialog factory, never null.
+ */
+ @NonNull
+ public MediaRouteDialogFactory getDialogFactory() {
+ return mDialogFactory;
+ }
+
+ /**
+ * Sets the media route dialog factory to use when showing the route chooser
+ * or controller dialog.
+ *
+ * @param factory The dialog factory, must not be null.
+ */
+ public void setDialogFactory(@NonNull MediaRouteDialogFactory factory) {
+ if (factory == null) {
+ throw new IllegalArgumentException("factory must not be null");
+ }
+
+ mDialogFactory = factory;
+ }
+
+ /**
+ * Show the route chooser or controller dialog.
+ * <p>
+ * If the default route is selected or if the currently selected route does
+ * not match the {@link #getRouteSelector selector}, then shows the route chooser dialog.
+ * Otherwise, shows the route controller dialog to offer the user
+ * a choice to disconnect from the route or perform other control actions
+ * such as setting the route's volume.
+ * </p><p>
+ * The application can customize the dialogs by calling {@link #setDialogFactory}
+ * to provide a customized dialog factory.
+ * </p>
+ *
+ * @return True if the dialog was actually shown.
+ *
+ * @throws IllegalStateException if the activity is not a subclass of
+ * {@link FragmentActivity}.
+ */
+ public boolean showDialog() {
+ if (!mAttachedToWindow) {
+ return false;
+ }
+
+ final FragmentManager fm = getFragmentManager();
+ if (fm == null) {
+ throw new IllegalStateException("The activity must be a subclass of FragmentActivity");
+ }
+
+ MediaRouter.RouteInfo route = mRouter.getSelectedRoute();
+ if (route.isDefaultOrBluetooth() || !route.matchesSelector(mSelector)) {
+ if (fm.findFragmentByTag(CHOOSER_FRAGMENT_TAG) != null) {
+ Log.w(TAG, "showDialog(): Route chooser dialog already showing!");
+ return false;
+ }
+ MediaRouteChooserDialogFragment f =
+ mDialogFactory.onCreateChooserDialogFragment();
+ f.setRouteSelector(mSelector);
+ f.show(fm, CHOOSER_FRAGMENT_TAG);
+ } else {
+ if (fm.findFragmentByTag(CONTROLLER_FRAGMENT_TAG) != null) {
+ Log.w(TAG, "showDialog(): Route controller dialog already showing!");
+ return false;
+ }
+ MediaRouteControllerDialogFragment f =
+ mDialogFactory.onCreateControllerDialogFragment();
+ f.show(fm, CONTROLLER_FRAGMENT_TAG);
+ }
+ return true;
+ }
+
+ private FragmentManager getFragmentManager() {
+ Activity activity = getActivity();
+ if (activity instanceof FragmentActivity) {
+ return ((FragmentActivity)activity).getSupportFragmentManager();
+ }
+ return null;
+ }
+
+ private Activity getActivity() {
+ // Gross way of unwrapping the Activity so we can get the FragmentManager
+ Context context = getContext();
+ while (context instanceof ContextWrapper) {
+ if (context instanceof Activity) {
+ return (Activity)context;
+ }
+ context = ((ContextWrapper)context).getBaseContext();
+ }
+ return null;
+ }
+
+ /**
+ * Sets whether to enable showing a toast with the content descriptor of the
+ * button when the button is long pressed.
+ */
+ void setCheatSheetEnabled(boolean enable) {
+ TooltipCompat.setTooltipText(this, enable
+ ? ApiHelper.getLibResources().getString(R.string.mr_button_content_description)
+ : null);
+ }
+
+ @Override
+ public boolean performClick() {
+ // Send the appropriate accessibility events and call listeners
+ boolean handled = super.performClick();
+ if (!handled) {
+ playSoundEffect(SoundEffectConstants.CLICK);
+ }
+ return showDialog() || handled;
+ }
+
+ @Override
+ protected int[] onCreateDrawableState(int extraSpace) {
+ final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+
+ // Technically we should be handling this more completely, but these
+ // are implementation details here. Checkable is used to express the connecting
+ // drawable state and it's mutually exclusive with check for the purposes
+ // of state selection here.
+ if (mIsConnecting) {
+ mergeDrawableStates(drawableState, CHECKABLE_STATE_SET);
+ } else if (mRemoteActive) {
+ mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+ }
+ return drawableState;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+
+ if (mRemoteIndicator != null) {
+ int[] myDrawableState = getDrawableState();
+ mRemoteIndicator.setState(myDrawableState);
+ invalidate();
+ }
+ }
+
+ /**
+ * Sets a drawable to use as the remote route indicator.
+ */
+ public void setRemoteIndicatorDrawable(Drawable d) {
+ if (mRemoteIndicatorLoader != null) {
+ mRemoteIndicatorLoader.cancel(false);
+ }
+
+ if (mRemoteIndicator != null) {
+ mRemoteIndicator.setCallback(null);
+ unscheduleDrawable(mRemoteIndicator);
+ }
+ if (d != null) {
+ if (mButtonTint != null) {
+ d = DrawableCompat.wrap(d.mutate());
+ DrawableCompat.setTintList(d, mButtonTint);
+ }
+ d.setCallback(this);
+ d.setState(getDrawableState());
+ d.setVisible(getVisibility() == VISIBLE, false);
+ }
+ mRemoteIndicator = d;
+
+ refreshDrawableState();
+ if (mAttachedToWindow && mRemoteIndicator != null
+ && mRemoteIndicator.getCurrent() instanceof AnimationDrawable) {
+ AnimationDrawable curDrawable = (AnimationDrawable) mRemoteIndicator.getCurrent();
+ if (mIsConnecting) {
+ if (!curDrawable.isRunning()) {
+ curDrawable.start();
+ }
+ } else if (mRemoteActive) {
+ if (curDrawable.isRunning()) {
+ curDrawable.stop();
+ }
+ curDrawable.selectDrawable(curDrawable.getNumberOfFrames() - 1);
+ }
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return super.verifyDrawable(who) || who == mRemoteIndicator;
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ // We can't call super to handle the background so we do it ourselves.
+ //super.jumpDrawablesToCurrentState();
+ if (getBackground() != null) {
+ DrawableCompat.jumpToCurrentState(getBackground());
+ }
+
+ // Handle our own remote indicator.
+ if (mRemoteIndicator != null) {
+ DrawableCompat.jumpToCurrentState(mRemoteIndicator);
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+
+ if (mRemoteIndicator != null) {
+ mRemoteIndicator.setVisible(getVisibility() == VISIBLE, false);
+ }
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mAttachedToWindow = true;
+ if (!mSelector.isEmpty()) {
+ mRouter.addCallback(mSelector, mCallback);
+ }
+ refreshRoute();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mAttachedToWindow = false;
+ if (!mSelector.isEmpty()) {
+ mRouter.removeCallback(mCallback);
+ }
+
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+ final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+ final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+ final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+ final int width = Math.max(mMinWidth, mRemoteIndicator != null ?
+ mRemoteIndicator.getIntrinsicWidth() + getPaddingLeft() + getPaddingRight() : 0);
+ final int height = Math.max(mMinHeight, mRemoteIndicator != null ?
+ mRemoteIndicator.getIntrinsicHeight() + getPaddingTop() + getPaddingBottom() : 0);
+
+ int measuredWidth;
+ switch (widthMode) {
+ case MeasureSpec.EXACTLY:
+ measuredWidth = widthSize;
+ break;
+ case MeasureSpec.AT_MOST:
+ measuredWidth = Math.min(widthSize, width);
+ break;
+ default:
+ case MeasureSpec.UNSPECIFIED:
+ measuredWidth = width;
+ break;
+ }
+
+ int measuredHeight;
+ switch (heightMode) {
+ case MeasureSpec.EXACTLY:
+ measuredHeight = heightSize;
+ break;
+ case MeasureSpec.AT_MOST:
+ measuredHeight = Math.min(heightSize, height);
+ break;
+ default:
+ case MeasureSpec.UNSPECIFIED:
+ measuredHeight = height;
+ break;
+ }
+
+ setMeasuredDimension(measuredWidth, measuredHeight);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (mRemoteIndicator != null) {
+ final int left = getPaddingLeft();
+ final int right = getWidth() - getPaddingRight();
+ final int top = getPaddingTop();
+ final int bottom = getHeight() - getPaddingBottom();
+
+ final int drawWidth = mRemoteIndicator.getIntrinsicWidth();
+ final int drawHeight = mRemoteIndicator.getIntrinsicHeight();
+ final int drawLeft = left + (right - left - drawWidth) / 2;
+ final int drawTop = top + (bottom - top - drawHeight) / 2;
+
+ mRemoteIndicator.setBounds(drawLeft, drawTop,
+ drawLeft + drawWidth, drawTop + drawHeight);
+ mRemoteIndicator.draw(canvas);
+ }
+ }
+
+ void refreshRoute() {
+ final MediaRouter.RouteInfo route = mRouter.getSelectedRoute();
+ final boolean isRemote = !route.isDefaultOrBluetooth() && route.matchesSelector(mSelector);
+ final boolean isConnecting = isRemote && route.isConnecting();
+ boolean needsRefresh = false;
+ if (mRemoteActive != isRemote) {
+ mRemoteActive = isRemote;
+ needsRefresh = true;
+ }
+ if (mIsConnecting != isConnecting) {
+ mIsConnecting = isConnecting;
+ needsRefresh = true;
+ }
+
+ if (needsRefresh) {
+ updateContentDescription();
+ refreshDrawableState();
+ }
+ if (mAttachedToWindow) {
+ setEnabled(mRouter.isRouteAvailable(mSelector,
+ MediaRouter.AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE));
+ }
+ if (mRemoteIndicator != null
+ && mRemoteIndicator.getCurrent() instanceof AnimationDrawable) {
+ AnimationDrawable curDrawable = (AnimationDrawable) mRemoteIndicator.getCurrent();
+ if (mAttachedToWindow) {
+ if ((needsRefresh || isConnecting) && !curDrawable.isRunning()) {
+ curDrawable.start();
+ }
+ } else if (isRemote && !isConnecting) {
+ // When the route is already connected before the view is attached, show the last
+ // frame of the connected animation immediately.
+ if (curDrawable.isRunning()) {
+ curDrawable.stop();
+ }
+ curDrawable.selectDrawable(curDrawable.getNumberOfFrames() - 1);
+ }
+ }
+ }
+
+ private void updateContentDescription() {
+ int resId;
+ if (mIsConnecting) {
+ resId = R.string.mr_cast_button_connecting;
+ } else if (mRemoteActive) {
+ resId = R.string.mr_cast_button_connected;
+ } else {
+ resId = R.string.mr_cast_button_disconnected;
+ }
+ setContentDescription(ApiHelper.getLibResources().getString(resId));
+ }
+
+ private final class MediaRouterCallback extends MediaRouter.Callback {
+ MediaRouterCallback() {
+ }
+
+ @Override
+ public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onProviderAdded(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onProviderRemoved(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ refreshRoute();
+ }
+
+ @Override
+ public void onProviderChanged(MediaRouter router, MediaRouter.ProviderInfo provider) {
+ refreshRoute();
+ }
+ }
+
+ private final class RemoteIndicatorLoader extends AsyncTask<Void, Void, Drawable> {
+ private final int mResId;
+
+ RemoteIndicatorLoader(int resId) {
+ mResId = resId;
+ }
+
+ @Override
+ protected Drawable doInBackground(Void... params) {
+ return ApiHelper.getLibResources().getDrawable(mResId);
+ }
+
+ @Override
+ protected void onPostExecute(Drawable remoteIndicator) {
+ cacheAndReset(remoteIndicator);
+ setRemoteIndicatorDrawable(remoteIndicator);
+ }
+
+ @Override
+ protected void onCancelled(Drawable remoteIndicator) {
+ cacheAndReset(remoteIndicator);
+ }
+
+ private void cacheAndReset(Drawable remoteIndicator) {
+ if (remoteIndicator != null) {
+ sRemoteIndicatorCache.put(mResId, remoteIndicator.getConstantState());
+ }
+ mRemoteIndicatorLoader = null;
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/app/MediaRouteChooserDialog.java b/com/android/support/mediarouter/app/MediaRouteChooserDialog.java
new file mode 100644
index 00000000..cc7c3d5b
--- /dev/null
+++ b/com/android/support/mediarouter/app/MediaRouteChooserDialog.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.app;
+
+import static com.android.support.mediarouter.media.MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTED;
+import static com.android.support.mediarouter.media.MediaRouter.RouteInfo.CONNECTION_STATE_CONNECTING;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.support.v7.app.AppCompatDialog;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.TextView;
+
+import com.android.media.update.R;
+import com.android.support.mediarouter.media.MediaRouteSelector;
+import com.android.support.mediarouter.media.MediaRouter;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * This class implements the route chooser dialog for {@link MediaRouter}.
+ * <p>
+ * This dialog allows the user to choose a route that matches a given selector.
+ * </p>
+ *
+ * @see MediaRouteButton
+ * @see MediaRouteActionProvider
+ */
+public class MediaRouteChooserDialog extends AppCompatDialog {
+ static final String TAG = "MediaRouteChooserDialog";
+
+ // Do not update the route list immediately to avoid unnatural dialog change.
+ private static final long UPDATE_ROUTES_DELAY_MS = 300L;
+ static final int MSG_UPDATE_ROUTES = 1;
+
+ private final MediaRouter mRouter;
+ private final MediaRouterCallback mCallback;
+
+ private TextView mTitleView;
+ private MediaRouteSelector mSelector = MediaRouteSelector.EMPTY;
+ private ArrayList<MediaRouter.RouteInfo> mRoutes;
+ private RouteAdapter mAdapter;
+ private ListView mListView;
+ private boolean mAttachedToWindow;
+ private long mLastUpdateTime;
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case MSG_UPDATE_ROUTES:
+ updateRoutes((List<MediaRouter.RouteInfo>) message.obj);
+ break;
+ }
+ }
+ };
+
+ public MediaRouteChooserDialog(Context context) {
+ this(context, 0);
+ }
+
+ public MediaRouteChooserDialog(Context context, int theme) {
+ super(context = MediaRouterThemeHelper.createThemedDialogContext(context, theme, false),
+ MediaRouterThemeHelper.createThemedDialogStyle(context));
+ context = getContext();
+
+ mRouter = MediaRouter.getInstance(context);
+ mCallback = new MediaRouterCallback();
+ }
+
+ /**
+ * Gets the media route selector for filtering the routes that the user can select.
+ *
+ * @return The selector, never null.
+ */
+ @NonNull
+ public MediaRouteSelector getRouteSelector() {
+ return mSelector;
+ }
+
+ /**
+ * Sets the media route selector for filtering the routes that the user can select.
+ *
+ * @param selector The selector, must not be null.
+ */
+ public void setRouteSelector(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ if (!mSelector.equals(selector)) {
+ mSelector = selector;
+
+ if (mAttachedToWindow) {
+ mRouter.removeCallback(mCallback);
+ mRouter.addCallback(selector, mCallback,
+ MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ }
+
+ refreshRoutes();
+ }
+ }
+
+ /**
+ * Called to filter the set of routes that should be included in the list.
+ * <p>
+ * The default implementation iterates over all routes in the provided list and
+ * removes those for which {@link #onFilterRoute} returns false.
+ * </p>
+ *
+ * @param routes The list of routes to filter in-place, never null.
+ */
+ public void onFilterRoutes(@NonNull List<MediaRouter.RouteInfo> routes) {
+ for (int i = routes.size(); i-- > 0; ) {
+ if (!onFilterRoute(routes.get(i))) {
+ routes.remove(i);
+ }
+ }
+ }
+
+ /**
+ * Returns true if the route should be included in the list.
+ * <p>
+ * The default implementation returns true for enabled non-default routes that
+ * match the selector. Subclasses can override this method to filter routes
+ * differently.
+ * </p>
+ *
+ * @param route The route to consider, never null.
+ * @return True if the route should be included in the chooser dialog.
+ */
+ public boolean onFilterRoute(@NonNull MediaRouter.RouteInfo route) {
+ return !route.isDefaultOrBluetooth() && route.isEnabled()
+ && route.matchesSelector(mSelector);
+ }
+
+ @Override
+ public void setTitle(CharSequence title) {
+ mTitleView.setText(title);
+ }
+
+ @Override
+ public void setTitle(int titleId) {
+ mTitleView.setText(titleId);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.mr_chooser_dialog);
+
+ mRoutes = new ArrayList<>();
+ mAdapter = new RouteAdapter(getContext(), mRoutes);
+ mListView = (ListView)findViewById(R.id.mr_chooser_list);
+ mListView.setAdapter(mAdapter);
+ mListView.setOnItemClickListener(mAdapter);
+ mListView.setEmptyView(findViewById(android.R.id.empty));
+ mTitleView = findViewById(R.id.mr_chooser_title);
+
+ updateLayout();
+ }
+
+ /**
+ * Sets the width of the dialog. Also called when configuration changes.
+ */
+ void updateLayout() {
+ getWindow().setLayout(MediaRouteDialogHelper.getDialogWidth(getContext()),
+ ViewGroup.LayoutParams.WRAP_CONTENT);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ mAttachedToWindow = true;
+ mRouter.addCallback(mSelector, mCallback, MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ refreshRoutes();
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mAttachedToWindow = false;
+ mRouter.removeCallback(mCallback);
+ mHandler.removeMessages(MSG_UPDATE_ROUTES);
+
+ super.onDetachedFromWindow();
+ }
+
+ /**
+ * Refreshes the list of routes that are shown in the chooser dialog.
+ */
+ public void refreshRoutes() {
+ if (mAttachedToWindow) {
+ ArrayList<MediaRouter.RouteInfo> routes = new ArrayList<>(mRouter.getRoutes());
+ onFilterRoutes(routes);
+ Collections.sort(routes, RouteComparator.sInstance);
+ if (SystemClock.uptimeMillis() - mLastUpdateTime >= UPDATE_ROUTES_DELAY_MS) {
+ updateRoutes(routes);
+ } else {
+ mHandler.removeMessages(MSG_UPDATE_ROUTES);
+ mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_UPDATE_ROUTES, routes),
+ mLastUpdateTime + UPDATE_ROUTES_DELAY_MS);
+ }
+ }
+ }
+
+ void updateRoutes(List<MediaRouter.RouteInfo> routes) {
+ mLastUpdateTime = SystemClock.uptimeMillis();
+ mRoutes.clear();
+ mRoutes.addAll(routes);
+ mAdapter.notifyDataSetChanged();
+ }
+
+ private final class RouteAdapter extends ArrayAdapter<MediaRouter.RouteInfo>
+ implements ListView.OnItemClickListener {
+ private final LayoutInflater mInflater;
+ private final Drawable mDefaultIcon;
+ private final Drawable mTvIcon;
+ private final Drawable mSpeakerIcon;
+ private final Drawable mSpeakerGroupIcon;
+
+ public RouteAdapter(Context context, List<MediaRouter.RouteInfo> routes) {
+ super(context, 0, routes);
+ mInflater = LayoutInflater.from(context);
+ TypedArray styledAttributes = getContext().obtainStyledAttributes(new int[] {
+ R.attr.mediaRouteDefaultIconDrawable,
+ R.attr.mediaRouteTvIconDrawable,
+ R.attr.mediaRouteSpeakerIconDrawable,
+ R.attr.mediaRouteSpeakerGroupIconDrawable});
+ mDefaultIcon = styledAttributes.getDrawable(0);
+ mTvIcon = styledAttributes.getDrawable(1);
+ mSpeakerIcon = styledAttributes.getDrawable(2);
+ mSpeakerGroupIcon = styledAttributes.getDrawable(3);
+ styledAttributes.recycle();
+ }
+
+ @Override
+ public boolean areAllItemsEnabled() {
+ return false;
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return getItem(position).isEnabled();
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View view = convertView;
+ if (view == null) {
+ view = mInflater.inflate(R.layout.mr_chooser_list_item, parent, false);
+ }
+
+ MediaRouter.RouteInfo route = getItem(position);
+ TextView text1 = (TextView) view.findViewById(R.id.mr_chooser_route_name);
+ TextView text2 = (TextView) view.findViewById(R.id.mr_chooser_route_desc);
+ text1.setText(route.getName());
+ String description = route.getDescription();
+ boolean isConnectedOrConnecting =
+ route.getConnectionState() == CONNECTION_STATE_CONNECTED
+ || route.getConnectionState() == CONNECTION_STATE_CONNECTING;
+ if (isConnectedOrConnecting && !TextUtils.isEmpty(description)) {
+ text1.setGravity(Gravity.BOTTOM);
+ text2.setVisibility(View.VISIBLE);
+ text2.setText(description);
+ } else {
+ text1.setGravity(Gravity.CENTER_VERTICAL);
+ text2.setVisibility(View.GONE);
+ text2.setText("");
+ }
+ view.setEnabled(route.isEnabled());
+
+ ImageView iconView = (ImageView) view.findViewById(R.id.mr_chooser_route_icon);
+ if (iconView != null) {
+ iconView.setImageDrawable(getIconDrawable(route));
+ }
+ return view;
+ }
+
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ MediaRouter.RouteInfo route = getItem(position);
+ if (route.isEnabled()) {
+ route.select();
+ dismiss();
+ }
+ }
+
+ 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 {
+ MediaRouterCallback() {
+ }
+
+ @Override
+ public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoutes();
+ }
+
+ @Override
+ public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoutes();
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
+ refreshRoutes();
+ }
+
+ @Override
+ public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
+ dismiss();
+ }
+ }
+
+ static final class RouteComparator implements Comparator<MediaRouter.RouteInfo> {
+ public static final RouteComparator sInstance = new RouteComparator();
+
+ @Override
+ public int compare(MediaRouter.RouteInfo lhs, MediaRouter.RouteInfo rhs) {
+ return lhs.getName().compareToIgnoreCase(rhs.getName());
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/app/MediaRouteChooserDialogFragment.java b/com/android/support/mediarouter/app/MediaRouteChooserDialogFragment.java
new file mode 100644
index 00000000..2f85fb35
--- /dev/null
+++ b/com/android/support/mediarouter/app/MediaRouteChooserDialogFragment.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.app;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+
+import com.android.support.mediarouter.media.MediaRouteSelector;
+
+/**
+ * Media route chooser dialog fragment.
+ * <p>
+ * Creates a {@link MediaRouteChooserDialog}. The application may subclass
+ * this dialog fragment to customize the media route chooser dialog.
+ * </p>
+ */
+public class MediaRouteChooserDialogFragment extends DialogFragment {
+ private final String ARGUMENT_SELECTOR = "selector";
+
+ private MediaRouteChooserDialog mDialog;
+ private MediaRouteSelector mSelector;
+
+ /**
+ * Creates a media route chooser dialog fragment.
+ * <p>
+ * All subclasses of this class must also possess a default constructor.
+ * </p>
+ */
+ public MediaRouteChooserDialogFragment() {
+ setCancelable(true);
+ }
+
+ /**
+ * Gets the media route selector for filtering the routes that the user can select.
+ *
+ * @return The selector, never null.
+ */
+ public MediaRouteSelector getRouteSelector() {
+ ensureRouteSelector();
+ return mSelector;
+ }
+
+ private void ensureRouteSelector() {
+ if (mSelector == null) {
+ Bundle args = getArguments();
+ if (args != null) {
+ mSelector = MediaRouteSelector.fromBundle(args.getBundle(ARGUMENT_SELECTOR));
+ }
+ if (mSelector == null) {
+ mSelector = MediaRouteSelector.EMPTY;
+ }
+ }
+ }
+
+ /**
+ * Sets the media route selector for filtering the routes that the user can select.
+ * This method must be called before the fragment is added.
+ *
+ * @param selector The selector to set.
+ */
+ public void setRouteSelector(MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ ensureRouteSelector();
+ if (!mSelector.equals(selector)) {
+ mSelector = selector;
+
+ Bundle args = getArguments();
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putBundle(ARGUMENT_SELECTOR, selector.asBundle());
+ setArguments(args);
+
+ MediaRouteChooserDialog dialog = (MediaRouteChooserDialog)getDialog();
+ if (dialog != null) {
+ dialog.setRouteSelector(selector);
+ }
+ }
+ }
+
+ /**
+ * Called when the chooser dialog is being created.
+ * <p>
+ * Subclasses may override this method to customize the dialog.
+ * </p>
+ */
+ public MediaRouteChooserDialog onCreateChooserDialog(
+ Context context, Bundle savedInstanceState) {
+ return new MediaRouteChooserDialog(context);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ mDialog = onCreateChooserDialog(getContext(), savedInstanceState);
+ mDialog.setRouteSelector(getRouteSelector());
+ return mDialog;
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (mDialog != null) {
+ mDialog.updateLayout();
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/app/MediaRouteControllerDialog.java b/com/android/support/mediarouter/app/MediaRouteControllerDialog.java
new file mode 100644
index 00000000..942797b1
--- /dev/null
+++ b/com/android/support/mediarouter/app/MediaRouteControllerDialog.java
@@ -0,0 +1,1481 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.app;
+
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PAUSE;
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY;
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_PLAY_PAUSE;
+import static android.support.v4.media.session.PlaybackStateCompat.ACTION_STOP;
+
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.SystemClock;
+import android.support.v4.media.MediaDescriptionCompat;
+import android.support.v4.media.MediaMetadataCompat;
+import android.support.v4.media.session.MediaControllerCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.media.session.PlaybackStateCompat;
+import android.support.v4.util.ObjectsCompat;
+import android.support.v4.view.accessibility.AccessibilityEventCompat;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.graphics.Palette;
+
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationSet;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+import android.view.animation.Transformation;
+import android.view.animation.TranslateAnimation;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageButton;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RelativeLayout;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import com.android.media.update.R;
+import com.android.support.mediarouter.media.MediaRouteSelector;
+import com.android.support.mediarouter.media.MediaRouter;
+import com.android.support.mediarouter.app.OverlayListView.OverlayObject;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This class implements the route controller dialog for {@link MediaRouter}.
+ * <p>
+ * This dialog allows the user to control or disconnect from the currently selected route.
+ * </p>
+ *
+ * @see MediaRouteButton
+ * @see MediaRouteActionProvider
+ */
+public class MediaRouteControllerDialog extends AlertDialog {
+ // Tags should be less than 24 characters long (see docs for android.util.Log.isLoggable())
+ static final String TAG = "MediaRouteCtrlDialog";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ // Time to wait before updating the volume when the user lets go of the seek bar
+ // to allow the route provider time to propagate the change and publish a new
+ // route descriptor.
+ static final int VOLUME_UPDATE_DELAY_MILLIS = 500;
+ static final int CONNECTION_TIMEOUT_MILLIS = (int) TimeUnit.SECONDS.toMillis(30L);
+
+ private static final int BUTTON_NEUTRAL_RES_ID = android.R.id.button3;
+ static final int BUTTON_DISCONNECT_RES_ID = android.R.id.button2;
+ static final int BUTTON_STOP_RES_ID = android.R.id.button1;
+
+ final MediaRouter mRouter;
+ private final MediaRouterCallback mCallback;
+ final MediaRouter.RouteInfo mRoute;
+
+ Context mContext;
+ private boolean mCreated;
+ private boolean mAttachedToWindow;
+
+ private int mDialogContentWidth;
+
+ private View mCustomControlView;
+
+ private Button mDisconnectButton;
+ private Button mStopCastingButton;
+ private ImageButton mPlaybackControlButton;
+ private ImageButton mCloseButton;
+ private MediaRouteExpandCollapseButton mGroupExpandCollapseButton;
+
+ private FrameLayout mExpandableAreaLayout;
+ private LinearLayout mDialogAreaLayout;
+ FrameLayout mDefaultControlLayout;
+ private FrameLayout mCustomControlLayout;
+ private ImageView mArtView;
+ private TextView mTitleView;
+ private TextView mSubtitleView;
+ private TextView mRouteNameTextView;
+
+ private boolean mVolumeControlEnabled = true;
+ // Layout for media controllers including play/pause button and the main volume slider.
+ private LinearLayout mMediaMainControlLayout;
+ private RelativeLayout mPlaybackControlLayout;
+ private LinearLayout mVolumeControlLayout;
+ private View mDividerView;
+
+ OverlayListView mVolumeGroupList;
+ VolumeGroupAdapter mVolumeGroupAdapter;
+ private List<MediaRouter.RouteInfo> mGroupMemberRoutes;
+ Set<MediaRouter.RouteInfo> mGroupMemberRoutesAdded;
+ private Set<MediaRouter.RouteInfo> mGroupMemberRoutesRemoved;
+ Set<MediaRouter.RouteInfo> mGroupMemberRoutesAnimatingWithBitmap;
+ SeekBar mVolumeSlider;
+ VolumeChangeListener mVolumeChangeListener;
+ MediaRouter.RouteInfo mRouteInVolumeSliderTouched;
+ private int mVolumeGroupListItemIconSize;
+ private int mVolumeGroupListItemHeight;
+ private int mVolumeGroupListMaxHeight;
+ private final int mVolumeGroupListPaddingTop;
+ Map<MediaRouter.RouteInfo, SeekBar> mVolumeSliderMap;
+
+ MediaControllerCompat mMediaController;
+ MediaControllerCallback mControllerCallback;
+ PlaybackStateCompat mState;
+ MediaDescriptionCompat mDescription;
+
+ FetchArtTask mFetchArtTask;
+ Bitmap mArtIconBitmap;
+ Uri mArtIconUri;
+ boolean mArtIconIsLoaded;
+ Bitmap mArtIconLoadedBitmap;
+ int mArtIconBackgroundColor;
+
+ boolean mHasPendingUpdate;
+ boolean mPendingUpdateAnimationNeeded;
+
+ boolean mIsGroupExpanded;
+ boolean mIsGroupListAnimating;
+ boolean mIsGroupListAnimationPending;
+ int mGroupListAnimationDurationMs;
+ private int mGroupListFadeInDurationMs;
+ private int mGroupListFadeOutDurationMs;
+
+ private Interpolator mInterpolator;
+ private Interpolator mLinearOutSlowInInterpolator;
+ private Interpolator mFastOutSlowInInterpolator;
+ private Interpolator mAccelerateDecelerateInterpolator;
+
+ final AccessibilityManager mAccessibilityManager;
+
+ Runnable mGroupListFadeInAnimation = new Runnable() {
+ @Override
+ public void run() {
+ startGroupListFadeInAnimation();
+ }
+ };
+
+ public MediaRouteControllerDialog(Context context) {
+ this(context, 0);
+ }
+
+ public MediaRouteControllerDialog(Context context, int theme) {
+ super(context = MediaRouterThemeHelper.createThemedDialogContext(context, theme, true),
+ MediaRouterThemeHelper.createThemedDialogStyle(context));
+ mContext = getContext();
+
+ mControllerCallback = new MediaControllerCallback();
+ mRouter = MediaRouter.getInstance(mContext);
+ mCallback = new MediaRouterCallback();
+ mRoute = mRouter.getSelectedRoute();
+ setMediaSession(mRouter.getMediaSessionToken());
+ mVolumeGroupListPaddingTop = mContext.getResources().getDimensionPixelSize(
+ R.dimen.mr_controller_volume_group_list_padding_top);
+ mAccessibilityManager =
+ (AccessibilityManager) mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mLinearOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
+ R.interpolator.mr_linear_out_slow_in);
+ mFastOutSlowInInterpolator = AnimationUtils.loadInterpolator(context,
+ R.interpolator.mr_fast_out_slow_in);
+ }
+ mAccelerateDecelerateInterpolator = new AccelerateDecelerateInterpolator();
+ }
+
+ /**
+ * Gets the route that this dialog is controlling.
+ */
+ public MediaRouter.RouteInfo getRoute() {
+ return mRoute;
+ }
+
+ private MediaRouter.RouteGroup getGroup() {
+ if (mRoute instanceof MediaRouter.RouteGroup) {
+ return (MediaRouter.RouteGroup) mRoute;
+ }
+ return null;
+ }
+
+ /**
+ * Provides the subclass an opportunity to create a view that will replace the default media
+ * controls for the currently playing content.
+ *
+ * @param savedInstanceState The dialog's saved instance state.
+ * @return The media control view, or null if none.
+ */
+ public View onCreateMediaControlView(Bundle savedInstanceState) {
+ return null;
+ }
+
+ /**
+ * Gets the media control view that was created by {@link #onCreateMediaControlView(Bundle)}.
+ *
+ * @return The media control view, or null if none.
+ */
+ public View getMediaControlView() {
+ return mCustomControlView;
+ }
+
+ /**
+ * Sets whether to enable the volume slider and volume control using the volume keys
+ * when the route supports it.
+ * <p>
+ * The default value is true.
+ * </p>
+ */
+ public void setVolumeControlEnabled(boolean enable) {
+ if (mVolumeControlEnabled != enable) {
+ mVolumeControlEnabled = enable;
+ if (mCreated) {
+ update(false);
+ }
+ }
+ }
+
+ /**
+ * Returns whether to enable the volume slider and volume control using the volume keys
+ * when the route supports it.
+ */
+ public boolean isVolumeControlEnabled() {
+ return mVolumeControlEnabled;
+ }
+
+ /**
+ * Set the session to use for metadata and transport controls. The dialog
+ * will listen to changes on this session and update the UI automatically in
+ * response to changes.
+ *
+ * @param sessionToken The token for the session to use.
+ */
+ private void setMediaSession(MediaSessionCompat.Token sessionToken) {
+ if (mMediaController != null) {
+ mMediaController.unregisterCallback(mControllerCallback);
+ mMediaController = null;
+ }
+ if (sessionToken == null) {
+ return;
+ }
+ if (!mAttachedToWindow) {
+ return;
+ }
+ try {
+ mMediaController = new MediaControllerCompat(mContext, sessionToken);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error creating media controller in setMediaSession.", e);
+ }
+ if (mMediaController != null) {
+ mMediaController.registerCallback(mControllerCallback);
+ }
+ MediaMetadataCompat metadata = mMediaController == null ? null
+ : mMediaController.getMetadata();
+ mDescription = metadata == null ? null : metadata.getDescription();
+ mState = mMediaController == null ? null : mMediaController.getPlaybackState();
+ updateArtIconIfNeeded();
+ update(false);
+ }
+
+ /**
+ * Gets the session to use for metadata and transport controls.
+ *
+ * @return The token for the session to use or null if none.
+ */
+ public MediaSessionCompat.Token getMediaSession() {
+ return mMediaController == null ? null : mMediaController.getSessionToken();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ getWindow().setBackgroundDrawableResource(android.R.color.transparent);
+ setContentView(R.layout.mr_controller_material_dialog_b);
+
+ // Remove the neutral button.
+ findViewById(BUTTON_NEUTRAL_RES_ID).setVisibility(View.GONE);
+
+ ClickListener listener = new ClickListener();
+
+ mExpandableAreaLayout = findViewById(R.id.mr_expandable_area);
+ mExpandableAreaLayout.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ dismiss();
+ }
+ });
+ mDialogAreaLayout = findViewById(R.id.mr_dialog_area);
+ mDialogAreaLayout.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ // Eat unhandled touch events.
+ }
+ });
+ int color = MediaRouterThemeHelper.getButtonTextColor(mContext);
+ mDisconnectButton = findViewById(BUTTON_DISCONNECT_RES_ID);
+ mDisconnectButton.setText(R.string.mr_controller_disconnect);
+ mDisconnectButton.setTextColor(color);
+ mDisconnectButton.setOnClickListener(listener);
+
+ mStopCastingButton = findViewById(BUTTON_STOP_RES_ID);
+ mStopCastingButton.setText(R.string.mr_controller_stop_casting);
+ mStopCastingButton.setTextColor(color);
+ mStopCastingButton.setOnClickListener(listener);
+
+ mRouteNameTextView = findViewById(R.id.mr_name);
+ mCloseButton = findViewById(R.id.mr_close);
+ mCloseButton.setOnClickListener(listener);
+ mCustomControlLayout = findViewById(R.id.mr_custom_control);
+ mDefaultControlLayout = findViewById(R.id.mr_default_control);
+
+ // Start the session activity when a content item (album art, title or subtitle) is clicked.
+ View.OnClickListener onClickListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (mMediaController != null) {
+ PendingIntent pi = mMediaController.getSessionActivity();
+ if (pi != null) {
+ try {
+ pi.send();
+ dismiss();
+ } catch (PendingIntent.CanceledException e) {
+ Log.e(TAG, pi + " was not sent, it had been canceled.");
+ }
+ }
+ }
+ }
+ };
+ mArtView = findViewById(R.id.mr_art);
+ mArtView.setOnClickListener(onClickListener);
+ findViewById(R.id.mr_control_title_container).setOnClickListener(onClickListener);
+
+ mMediaMainControlLayout = findViewById(R.id.mr_media_main_control);
+ mDividerView = findViewById(R.id.mr_control_divider);
+
+ mPlaybackControlLayout = findViewById(R.id.mr_playback_control);
+ mTitleView = findViewById(R.id.mr_control_title);
+ mSubtitleView = findViewById(R.id.mr_control_subtitle);
+ mPlaybackControlButton = findViewById(R.id.mr_control_playback_ctrl);
+ mPlaybackControlButton.setOnClickListener(listener);
+
+ mVolumeControlLayout = findViewById(R.id.mr_volume_control);
+ mVolumeControlLayout.setVisibility(View.GONE);
+ mVolumeSlider = findViewById(R.id.mr_volume_slider);
+ mVolumeSlider.setTag(mRoute);
+ mVolumeChangeListener = new VolumeChangeListener();
+ mVolumeSlider.setOnSeekBarChangeListener(mVolumeChangeListener);
+
+ mVolumeGroupList = findViewById(R.id.mr_volume_group_list);
+ mGroupMemberRoutes = new ArrayList<MediaRouter.RouteInfo>();
+ mVolumeGroupAdapter = new VolumeGroupAdapter(mVolumeGroupList.getContext(),
+ mGroupMemberRoutes);
+ mVolumeGroupList.setAdapter(mVolumeGroupAdapter);
+ mGroupMemberRoutesAnimatingWithBitmap = new HashSet<>();
+
+ MediaRouterThemeHelper.setMediaControlsBackgroundColor(mContext,
+ mMediaMainControlLayout, mVolumeGroupList, getGroup() != null);
+ MediaRouterThemeHelper.setVolumeSliderColor(mContext,
+ (MediaRouteVolumeSlider) mVolumeSlider, mMediaMainControlLayout);
+ mVolumeSliderMap = new HashMap<>();
+ mVolumeSliderMap.put(mRoute, mVolumeSlider);
+
+ mGroupExpandCollapseButton =
+ findViewById(R.id.mr_group_expand_collapse);
+ mGroupExpandCollapseButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mIsGroupExpanded = !mIsGroupExpanded;
+ if (mIsGroupExpanded) {
+ mVolumeGroupList.setVisibility(View.VISIBLE);
+ }
+ loadInterpolator();
+ updateLayoutHeight(true);
+ }
+ });
+ loadInterpolator();
+ mGroupListAnimationDurationMs = mContext.getResources().getInteger(
+ R.integer.mr_controller_volume_group_list_animation_duration_ms);
+ mGroupListFadeInDurationMs = mContext.getResources().getInteger(
+ R.integer.mr_controller_volume_group_list_fade_in_duration_ms);
+ mGroupListFadeOutDurationMs = mContext.getResources().getInteger(
+ R.integer.mr_controller_volume_group_list_fade_out_duration_ms);
+
+ mCustomControlView = onCreateMediaControlView(savedInstanceState);
+ if (mCustomControlView != null) {
+ mCustomControlLayout.addView(mCustomControlView);
+ mCustomControlLayout.setVisibility(View.VISIBLE);
+ }
+ mCreated = true;
+ updateLayout();
+ }
+
+ /**
+ * Sets the width of the dialog. Also called when configuration changes.
+ */
+ void updateLayout() {
+ int width = MediaRouteDialogHelper.getDialogWidth(mContext);
+ getWindow().setLayout(width, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+ View decorView = getWindow().getDecorView();
+ mDialogContentWidth = width - decorView.getPaddingLeft() - decorView.getPaddingRight();
+
+ Resources res = mContext.getResources();
+ mVolumeGroupListItemIconSize = res.getDimensionPixelSize(
+ R.dimen.mr_controller_volume_group_list_item_icon_size);
+ mVolumeGroupListItemHeight = res.getDimensionPixelSize(
+ R.dimen.mr_controller_volume_group_list_item_height);
+ mVolumeGroupListMaxHeight = res.getDimensionPixelSize(
+ R.dimen.mr_controller_volume_group_list_max_height);
+
+ // Fetch art icons again for layout changes to resize it accordingly
+ mArtIconBitmap = null;
+ mArtIconUri = null;
+ updateArtIconIfNeeded();
+ update(false);
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ mAttachedToWindow = true;
+
+ mRouter.addCallback(MediaRouteSelector.EMPTY, mCallback,
+ MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS);
+ setMediaSession(mRouter.getMediaSessionToken());
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ mRouter.removeCallback(mCallback);
+ setMediaSession(null);
+ mAttachedToWindow = false;
+ super.onDetachedFromWindow();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+ || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ mRoute.requestUpdateVolume(keyCode == KeyEvent.KEYCODE_VOLUME_DOWN ? -1 : 1);
+ return true;
+ }
+ return super.onKeyDown(keyCode, event);
+ }
+
+ @Override
+ public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+ || keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
+ return true;
+ }
+ return super.onKeyUp(keyCode, event);
+ }
+
+ void update(boolean animate) {
+ // Defer dialog updates if a user is adjusting a volume in the list
+ if (mRouteInVolumeSliderTouched != null) {
+ mHasPendingUpdate = true;
+ mPendingUpdateAnimationNeeded |= animate;
+ return;
+ }
+ mHasPendingUpdate = false;
+ mPendingUpdateAnimationNeeded = false;
+ if (!mRoute.isSelected() || mRoute.isDefaultOrBluetooth()) {
+ dismiss();
+ return;
+ }
+ if (!mCreated) {
+ return;
+ }
+
+ mRouteNameTextView.setText(mRoute.getName());
+ mDisconnectButton.setVisibility(mRoute.canDisconnect() ? View.VISIBLE : View.GONE);
+ if (mCustomControlView == null && mArtIconIsLoaded) {
+ if (isBitmapRecycled(mArtIconLoadedBitmap)) {
+ Log.w(TAG, "Can't set artwork image with recycled bitmap: " + mArtIconLoadedBitmap);
+ } else {
+ mArtView.setImageBitmap(mArtIconLoadedBitmap);
+ mArtView.setBackgroundColor(mArtIconBackgroundColor);
+ }
+ clearLoadedBitmap();
+ }
+ updateVolumeControlLayout();
+ updatePlaybackControlLayout();
+ updateLayoutHeight(animate);
+ }
+
+ private boolean isBitmapRecycled(Bitmap bitmap) {
+ return bitmap != null && bitmap.isRecycled();
+ }
+
+ private boolean canShowPlaybackControlLayout() {
+ return mCustomControlView == null && (mDescription != null || mState != null);
+ }
+
+ /**
+ * Returns the height of main media controller which includes playback control and master
+ * volume control.
+ */
+ private int getMainControllerHeight(boolean showPlaybackControl) {
+ int height = 0;
+ if (showPlaybackControl || mVolumeControlLayout.getVisibility() == View.VISIBLE) {
+ height += mMediaMainControlLayout.getPaddingTop()
+ + mMediaMainControlLayout.getPaddingBottom();
+ if (showPlaybackControl) {
+ height += mPlaybackControlLayout.getMeasuredHeight();
+ }
+ if (mVolumeControlLayout.getVisibility() == View.VISIBLE) {
+ height += mVolumeControlLayout.getMeasuredHeight();
+ }
+ if (showPlaybackControl && mVolumeControlLayout.getVisibility() == View.VISIBLE) {
+ height += mDividerView.getMeasuredHeight();
+ }
+ }
+ return height;
+ }
+
+ private void updateMediaControlVisibility(boolean canShowPlaybackControlLayout) {
+ // TODO: Update the top and bottom padding of the control layout according to the display
+ // height.
+ mDividerView.setVisibility((mVolumeControlLayout.getVisibility() == View.VISIBLE
+ && canShowPlaybackControlLayout) ? View.VISIBLE : View.GONE);
+ mMediaMainControlLayout.setVisibility((mVolumeControlLayout.getVisibility() == View.GONE
+ && !canShowPlaybackControlLayout) ? View.GONE : View.VISIBLE);
+ }
+
+ void updateLayoutHeight(final boolean animate) {
+ // We need to defer the update until the first layout has occurred, as we don't yet know the
+ // overall visible display size in which the window this view is attached to has been
+ // positioned in.
+ mDefaultControlLayout.requestLayout();
+ ViewTreeObserver observer = mDefaultControlLayout.getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mDefaultControlLayout.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ if (mIsGroupListAnimating) {
+ mIsGroupListAnimationPending = true;
+ } else {
+ updateLayoutHeightInternal(animate);
+ }
+ }
+ });
+ }
+
+ /**
+ * Updates the height of views and hide artwork or metadata if space is limited.
+ */
+ void updateLayoutHeightInternal(boolean animate) {
+ // Measure the size of widgets and get the height of main components.
+ int oldHeight = getLayoutHeight(mMediaMainControlLayout);
+ setLayoutHeight(mMediaMainControlLayout, ViewGroup.LayoutParams.MATCH_PARENT);
+ updateMediaControlVisibility(canShowPlaybackControlLayout());
+ View decorView = getWindow().getDecorView();
+ decorView.measure(
+ MeasureSpec.makeMeasureSpec(getWindow().getAttributes().width, MeasureSpec.EXACTLY),
+ MeasureSpec.UNSPECIFIED);
+ setLayoutHeight(mMediaMainControlLayout, oldHeight);
+ int artViewHeight = 0;
+ if (mCustomControlView == null && mArtView.getDrawable() instanceof BitmapDrawable) {
+ Bitmap art = ((BitmapDrawable) mArtView.getDrawable()).getBitmap();
+ if (art != null) {
+ artViewHeight = getDesiredArtHeight(art.getWidth(), art.getHeight());
+ mArtView.setScaleType(art.getWidth() >= art.getHeight()
+ ? ImageView.ScaleType.FIT_XY : ImageView.ScaleType.FIT_CENTER);
+ }
+ }
+ int mainControllerHeight = getMainControllerHeight(canShowPlaybackControlLayout());
+ int volumeGroupListCount = mGroupMemberRoutes.size();
+ // Scale down volume group list items in landscape mode.
+ int expandedGroupListHeight = getGroup() == null ? 0 :
+ mVolumeGroupListItemHeight * getGroup().getRoutes().size();
+ if (volumeGroupListCount > 0) {
+ expandedGroupListHeight += mVolumeGroupListPaddingTop;
+ }
+ expandedGroupListHeight = Math.min(expandedGroupListHeight, mVolumeGroupListMaxHeight);
+ int visibleGroupListHeight = mIsGroupExpanded ? expandedGroupListHeight : 0;
+
+ int desiredControlLayoutHeight =
+ Math.max(artViewHeight, visibleGroupListHeight) + mainControllerHeight;
+ Rect visibleRect = new Rect();
+ decorView.getWindowVisibleDisplayFrame(visibleRect);
+ // Height of non-control views in decor view.
+ // This includes title bar, button bar, and dialog's vertical padding which should be
+ // always shown.
+ int nonControlViewHeight = mDialogAreaLayout.getMeasuredHeight()
+ - mDefaultControlLayout.getMeasuredHeight();
+ // Maximum allowed height for controls to fit screen.
+ int maximumControlViewHeight = visibleRect.height() - nonControlViewHeight;
+
+ // Show artwork if it fits the screen.
+ if (mCustomControlView == null && artViewHeight > 0
+ && desiredControlLayoutHeight <= maximumControlViewHeight) {
+ mArtView.setVisibility(View.VISIBLE);
+ setLayoutHeight(mArtView, artViewHeight);
+ } else {
+ if (getLayoutHeight(mVolumeGroupList) + mMediaMainControlLayout.getMeasuredHeight()
+ >= mDefaultControlLayout.getMeasuredHeight()) {
+ mArtView.setVisibility(View.GONE);
+ }
+ artViewHeight = 0;
+ desiredControlLayoutHeight = visibleGroupListHeight + mainControllerHeight;
+ }
+ // Show the playback control if it fits the screen.
+ if (canShowPlaybackControlLayout()
+ && desiredControlLayoutHeight <= maximumControlViewHeight) {
+ mPlaybackControlLayout.setVisibility(View.VISIBLE);
+ } else {
+ mPlaybackControlLayout.setVisibility(View.GONE);
+ }
+ updateMediaControlVisibility(mPlaybackControlLayout.getVisibility() == View.VISIBLE);
+ mainControllerHeight = getMainControllerHeight(
+ mPlaybackControlLayout.getVisibility() == View.VISIBLE);
+ desiredControlLayoutHeight =
+ Math.max(artViewHeight, visibleGroupListHeight) + mainControllerHeight;
+
+ // Limit the volume group list height to fit the screen.
+ if (desiredControlLayoutHeight > maximumControlViewHeight) {
+ visibleGroupListHeight -= (desiredControlLayoutHeight - maximumControlViewHeight);
+ desiredControlLayoutHeight = maximumControlViewHeight;
+ }
+ // Update the layouts with the computed heights.
+ mMediaMainControlLayout.clearAnimation();
+ mVolumeGroupList.clearAnimation();
+ mDefaultControlLayout.clearAnimation();
+ if (animate) {
+ animateLayoutHeight(mMediaMainControlLayout, mainControllerHeight);
+ animateLayoutHeight(mVolumeGroupList, visibleGroupListHeight);
+ animateLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
+ } else {
+ setLayoutHeight(mMediaMainControlLayout, mainControllerHeight);
+ setLayoutHeight(mVolumeGroupList, visibleGroupListHeight);
+ setLayoutHeight(mDefaultControlLayout, desiredControlLayoutHeight);
+ }
+ // Maximize the window size with a transparent layout in advance for smooth animation.
+ setLayoutHeight(mExpandableAreaLayout, visibleRect.height());
+ rebuildVolumeGroupList(animate);
+ }
+
+ void updateVolumeGroupItemHeight(View item) {
+ LinearLayout container = (LinearLayout) item.findViewById(R.id.volume_item_container);
+ setLayoutHeight(container, mVolumeGroupListItemHeight);
+ View icon = item.findViewById(R.id.mr_volume_item_icon);
+ ViewGroup.LayoutParams lp = icon.getLayoutParams();
+ lp.width = mVolumeGroupListItemIconSize;
+ lp.height = mVolumeGroupListItemIconSize;
+ icon.setLayoutParams(lp);
+ }
+
+ private void animateLayoutHeight(final View view, int targetHeight) {
+ final int startValue = getLayoutHeight(view);
+ final int endValue = targetHeight;
+ Animation anim = new Animation() {
+ @Override
+ protected void applyTransformation(float interpolatedTime, Transformation t) {
+ int height = startValue - (int) ((startValue - endValue) * interpolatedTime);
+ setLayoutHeight(view, height);
+ }
+ };
+ anim.setDuration(mGroupListAnimationDurationMs);
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ anim.setInterpolator(mInterpolator);
+ }
+ view.startAnimation(anim);
+ }
+
+ void loadInterpolator() {
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ mInterpolator = mIsGroupExpanded ? mLinearOutSlowInInterpolator
+ : mFastOutSlowInInterpolator;
+ } else {
+ mInterpolator = mAccelerateDecelerateInterpolator;
+ }
+ }
+
+ private void updateVolumeControlLayout() {
+ if (isVolumeControlAvailable(mRoute)) {
+ if (mVolumeControlLayout.getVisibility() == View.GONE) {
+ mVolumeControlLayout.setVisibility(View.VISIBLE);
+ mVolumeSlider.setMax(mRoute.getVolumeMax());
+ mVolumeSlider.setProgress(mRoute.getVolume());
+ mGroupExpandCollapseButton.setVisibility(getGroup() == null ? View.GONE
+ : View.VISIBLE);
+ }
+ } else {
+ mVolumeControlLayout.setVisibility(View.GONE);
+ }
+ }
+
+ private void rebuildVolumeGroupList(boolean animate) {
+ List<MediaRouter.RouteInfo> routes = getGroup() == null ? null : getGroup().getRoutes();
+ if (routes == null) {
+ mGroupMemberRoutes.clear();
+ mVolumeGroupAdapter.notifyDataSetChanged();
+ } else if (MediaRouteDialogHelper.listUnorderedEquals(mGroupMemberRoutes, routes)) {
+ mVolumeGroupAdapter.notifyDataSetChanged();
+ } else {
+ HashMap<MediaRouter.RouteInfo, Rect> previousRouteBoundMap = animate
+ ? MediaRouteDialogHelper.getItemBoundMap(mVolumeGroupList, mVolumeGroupAdapter)
+ : null;
+ HashMap<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap = animate
+ ? MediaRouteDialogHelper.getItemBitmapMap(mContext, mVolumeGroupList,
+ mVolumeGroupAdapter) : null;
+ mGroupMemberRoutesAdded =
+ MediaRouteDialogHelper.getItemsAdded(mGroupMemberRoutes, routes);
+ mGroupMemberRoutesRemoved = MediaRouteDialogHelper.getItemsRemoved(mGroupMemberRoutes,
+ routes);
+ mGroupMemberRoutes.addAll(0, mGroupMemberRoutesAdded);
+ mGroupMemberRoutes.removeAll(mGroupMemberRoutesRemoved);
+ mVolumeGroupAdapter.notifyDataSetChanged();
+ if (animate && mIsGroupExpanded
+ && mGroupMemberRoutesAdded.size() + mGroupMemberRoutesRemoved.size() > 0) {
+ animateGroupListItems(previousRouteBoundMap, previousRouteBitmapMap);
+ } else {
+ mGroupMemberRoutesAdded = null;
+ mGroupMemberRoutesRemoved = null;
+ }
+ }
+ }
+
+ private void animateGroupListItems(final Map<MediaRouter.RouteInfo, Rect> previousRouteBoundMap,
+ final Map<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap) {
+ mVolumeGroupList.setEnabled(false);
+ mVolumeGroupList.requestLayout();
+ mIsGroupListAnimating = true;
+ ViewTreeObserver observer = mVolumeGroupList.getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mVolumeGroupList.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ animateGroupListItemsInternal(previousRouteBoundMap, previousRouteBitmapMap);
+ }
+ });
+ }
+
+ void animateGroupListItemsInternal(
+ Map<MediaRouter.RouteInfo, Rect> previousRouteBoundMap,
+ Map<MediaRouter.RouteInfo, BitmapDrawable> previousRouteBitmapMap) {
+ if (mGroupMemberRoutesAdded == null || mGroupMemberRoutesRemoved == null) {
+ return;
+ }
+ int groupSizeDelta = mGroupMemberRoutesAdded.size() - mGroupMemberRoutesRemoved.size();
+ boolean listenerRegistered = false;
+ Animation.AnimationListener listener = new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) {
+ mVolumeGroupList.startAnimationAll();
+ mVolumeGroupList.postDelayed(mGroupListFadeInAnimation,
+ mGroupListAnimationDurationMs);
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) { }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) { }
+ };
+
+ // Animate visible items from previous positions to current positions except routes added
+ // just before. Added routes will remain hidden until translate animation finishes.
+ int first = mVolumeGroupList.getFirstVisiblePosition();
+ for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) {
+ View view = mVolumeGroupList.getChildAt(i);
+ int position = first + i;
+ MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position);
+ Rect previousBounds = previousRouteBoundMap.get(route);
+ int currentTop = view.getTop();
+ int previousTop = previousBounds != null ? previousBounds.top
+ : (currentTop + mVolumeGroupListItemHeight * groupSizeDelta);
+ AnimationSet animSet = new AnimationSet(true);
+ if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.contains(route)) {
+ previousTop = currentTop;
+ Animation alphaAnim = new AlphaAnimation(0.0f, 0.0f);
+ alphaAnim.setDuration(mGroupListFadeInDurationMs);
+ animSet.addAnimation(alphaAnim);
+ }
+ Animation translationAnim = new TranslateAnimation(0, 0, previousTop - currentTop, 0);
+ translationAnim.setDuration(mGroupListAnimationDurationMs);
+ animSet.addAnimation(translationAnim);
+ animSet.setFillAfter(true);
+ animSet.setFillEnabled(true);
+ animSet.setInterpolator(mInterpolator);
+ if (!listenerRegistered) {
+ listenerRegistered = true;
+ animSet.setAnimationListener(listener);
+ }
+ view.clearAnimation();
+ view.startAnimation(animSet);
+ previousRouteBoundMap.remove(route);
+ previousRouteBitmapMap.remove(route);
+ }
+
+ // If a member route doesn't exist any longer, it can be either removed or moved out of the
+ // ListView layout boundary. In this case, use the previously captured bitmaps for
+ // animation.
+ for (Map.Entry<MediaRouter.RouteInfo, BitmapDrawable> item
+ : previousRouteBitmapMap.entrySet()) {
+ final MediaRouter.RouteInfo route = item.getKey();
+ final BitmapDrawable bitmap = item.getValue();
+ final Rect bounds = previousRouteBoundMap.get(route);
+ OverlayObject object = null;
+ if (mGroupMemberRoutesRemoved.contains(route)) {
+ object = new OverlayObject(bitmap, bounds).setAlphaAnimation(1.0f, 0.0f)
+ .setDuration(mGroupListFadeOutDurationMs)
+ .setInterpolator(mInterpolator);
+ } else {
+ int deltaY = groupSizeDelta * mVolumeGroupListItemHeight;
+ object = new OverlayObject(bitmap, bounds).setTranslateYAnimation(deltaY)
+ .setDuration(mGroupListAnimationDurationMs)
+ .setInterpolator(mInterpolator)
+ .setAnimationEndListener(new OverlayObject.OnAnimationEndListener() {
+ @Override
+ public void onAnimationEnd() {
+ mGroupMemberRoutesAnimatingWithBitmap.remove(route);
+ mVolumeGroupAdapter.notifyDataSetChanged();
+ }
+ });
+ mGroupMemberRoutesAnimatingWithBitmap.add(route);
+ }
+ mVolumeGroupList.addOverlayObject(object);
+ }
+ }
+
+ void startGroupListFadeInAnimation() {
+ clearGroupListAnimation(true);
+ mVolumeGroupList.requestLayout();
+ ViewTreeObserver observer = mVolumeGroupList.getViewTreeObserver();
+ observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ mVolumeGroupList.getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ startGroupListFadeInAnimationInternal();
+ }
+ });
+ }
+
+ void startGroupListFadeInAnimationInternal() {
+ if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.size() != 0) {
+ fadeInAddedRoutes();
+ } else {
+ finishAnimation(true);
+ }
+ }
+
+ void finishAnimation(boolean animate) {
+ mGroupMemberRoutesAdded = null;
+ mGroupMemberRoutesRemoved = null;
+ mIsGroupListAnimating = false;
+ if (mIsGroupListAnimationPending) {
+ mIsGroupListAnimationPending = false;
+ updateLayoutHeight(animate);
+ }
+ mVolumeGroupList.setEnabled(true);
+ }
+
+ private void fadeInAddedRoutes() {
+ Animation.AnimationListener listener = new Animation.AnimationListener() {
+ @Override
+ public void onAnimationStart(Animation animation) { }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ finishAnimation(true);
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) { }
+ };
+ boolean listenerRegistered = false;
+ int first = mVolumeGroupList.getFirstVisiblePosition();
+ for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) {
+ View view = mVolumeGroupList.getChildAt(i);
+ int position = first + i;
+ MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position);
+ if (mGroupMemberRoutesAdded.contains(route)) {
+ Animation alphaAnim = new AlphaAnimation(0.0f, 1.0f);
+ alphaAnim.setDuration(mGroupListFadeInDurationMs);
+ alphaAnim.setFillEnabled(true);
+ alphaAnim.setFillAfter(true);
+ if (!listenerRegistered) {
+ listenerRegistered = true;
+ alphaAnim.setAnimationListener(listener);
+ }
+ view.clearAnimation();
+ view.startAnimation(alphaAnim);
+ }
+ }
+ }
+
+ void clearGroupListAnimation(boolean exceptAddedRoutes) {
+ int first = mVolumeGroupList.getFirstVisiblePosition();
+ for (int i = 0; i < mVolumeGroupList.getChildCount(); ++i) {
+ View view = mVolumeGroupList.getChildAt(i);
+ int position = first + i;
+ MediaRouter.RouteInfo route = mVolumeGroupAdapter.getItem(position);
+ if (exceptAddedRoutes && mGroupMemberRoutesAdded != null
+ && mGroupMemberRoutesAdded.contains(route)) {
+ continue;
+ }
+ LinearLayout container = (LinearLayout) view.findViewById(R.id.volume_item_container);
+ container.setVisibility(View.VISIBLE);
+ AnimationSet animSet = new AnimationSet(true);
+ Animation alphaAnim = new AlphaAnimation(1.0f, 1.0f);
+ alphaAnim.setDuration(0);
+ animSet.addAnimation(alphaAnim);
+ Animation translationAnim = new TranslateAnimation(0, 0, 0, 0);
+ translationAnim.setDuration(0);
+ animSet.setFillAfter(true);
+ animSet.setFillEnabled(true);
+ view.clearAnimation();
+ view.startAnimation(animSet);
+ }
+ mVolumeGroupList.stopAnimationAll();
+ if (!exceptAddedRoutes) {
+ finishAnimation(false);
+ }
+ }
+
+ private void updatePlaybackControlLayout() {
+ if (canShowPlaybackControlLayout()) {
+ CharSequence title = mDescription == null ? null : mDescription.getTitle();
+ boolean hasTitle = !TextUtils.isEmpty(title);
+
+ CharSequence subtitle = mDescription == null ? null : mDescription.getSubtitle();
+ boolean hasSubtitle = !TextUtils.isEmpty(subtitle);
+
+ boolean showTitle = false;
+ boolean showSubtitle = false;
+ if (mRoute.getPresentationDisplayId()
+ != MediaRouter.RouteInfo.PRESENTATION_DISPLAY_ID_NONE) {
+ // The user is currently casting screen.
+ mTitleView.setText(R.string.mr_controller_casting_screen);
+ showTitle = true;
+ } else if (mState == null || mState.getState() == PlaybackStateCompat.STATE_NONE) {
+ // Show "No media selected" as we don't yet know the playback state.
+ mTitleView.setText(R.string.mr_controller_no_media_selected);
+ showTitle = true;
+ } else if (!hasTitle && !hasSubtitle) {
+ mTitleView.setText(R.string.mr_controller_no_info_available);
+ showTitle = true;
+ } else {
+ if (hasTitle) {
+ mTitleView.setText(title);
+ showTitle = true;
+ }
+ if (hasSubtitle) {
+ mSubtitleView.setText(subtitle);
+ showSubtitle = true;
+ }
+ }
+ mTitleView.setVisibility(showTitle ? View.VISIBLE : View.GONE);
+ mSubtitleView.setVisibility(showSubtitle ? View.VISIBLE : View.GONE);
+
+ if (mState != null) {
+ boolean isPlaying = mState.getState() == PlaybackStateCompat.STATE_BUFFERING
+ || mState.getState() == PlaybackStateCompat.STATE_PLAYING;
+ Context playbackControlButtonContext = mPlaybackControlButton.getContext();
+ boolean visible = true;
+ int iconDrawableAttr = 0;
+ int iconDescResId = 0;
+ if (isPlaying && isPauseActionSupported()) {
+ iconDrawableAttr = R.attr.mediaRoutePauseDrawable;
+ iconDescResId = R.string.mr_controller_pause;
+ } else if (isPlaying && isStopActionSupported()) {
+ iconDrawableAttr = R.attr.mediaRouteStopDrawable;
+ iconDescResId = R.string.mr_controller_stop;
+ } else if (!isPlaying && isPlayActionSupported()) {
+ iconDrawableAttr = R.attr.mediaRoutePlayDrawable;
+ iconDescResId = R.string.mr_controller_play;
+ } else {
+ visible = false;
+ }
+ mPlaybackControlButton.setVisibility(visible ? View.VISIBLE : View.GONE);
+ if (visible) {
+ mPlaybackControlButton.setImageResource(
+ MediaRouterThemeHelper.getThemeResource(
+ playbackControlButtonContext, iconDrawableAttr));
+ mPlaybackControlButton.setContentDescription(
+ playbackControlButtonContext.getResources()
+ .getText(iconDescResId));
+ }
+ }
+ }
+ }
+
+ private boolean isPlayActionSupported() {
+ return (mState.getActions() & (ACTION_PLAY | ACTION_PLAY_PAUSE)) != 0;
+ }
+
+ private boolean isPauseActionSupported() {
+ return (mState.getActions() & (ACTION_PAUSE | ACTION_PLAY_PAUSE)) != 0;
+ }
+
+ private boolean isStopActionSupported() {
+ return (mState.getActions() & ACTION_STOP) != 0;
+ }
+
+ boolean isVolumeControlAvailable(MediaRouter.RouteInfo route) {
+ return mVolumeControlEnabled && route.getVolumeHandling()
+ == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE;
+ }
+
+ private static int getLayoutHeight(View view) {
+ return view.getLayoutParams().height;
+ }
+
+ static void setLayoutHeight(View view, int height) {
+ ViewGroup.LayoutParams lp = view.getLayoutParams();
+ lp.height = height;
+ view.setLayoutParams(lp);
+ }
+
+ private static boolean uriEquals(Uri uri1, Uri uri2) {
+ if (uri1 != null && uri1.equals(uri2)) {
+ return true;
+ } else if (uri1 == null && uri2 == null) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Returns desired art height to fit into controller dialog.
+ */
+ int getDesiredArtHeight(int originalWidth, int originalHeight) {
+ if (originalWidth >= originalHeight) {
+ // For landscape art, fit width to dialog width.
+ return (int) ((float) mDialogContentWidth * originalHeight / originalWidth + 0.5f);
+ }
+ // For portrait art, fit height to 16:9 ratio case's height.
+ return (int) ((float) mDialogContentWidth * 9 / 16 + 0.5f);
+ }
+
+ void updateArtIconIfNeeded() {
+ if (mCustomControlView != null || !isIconChanged()) {
+ return;
+ }
+ if (mFetchArtTask != null) {
+ mFetchArtTask.cancel(true);
+ }
+ mFetchArtTask = new FetchArtTask();
+ mFetchArtTask.execute();
+ }
+
+ /**
+ * Clear the bitmap loaded by FetchArtTask. Will be called after the loaded bitmaps are applied
+ * to artwork, or no longer valid.
+ */
+ void clearLoadedBitmap() {
+ mArtIconIsLoaded = false;
+ mArtIconLoadedBitmap = null;
+ mArtIconBackgroundColor = 0;
+ }
+
+ /**
+ * Returns whether a new art image is different from an original art image. Compares
+ * Bitmap objects first, and then compares URIs only if bitmap is unchanged with
+ * a null value.
+ */
+ private boolean isIconChanged() {
+ Bitmap newBitmap = mDescription == null ? null : mDescription.getIconBitmap();
+ Uri newUri = mDescription == null ? null : mDescription.getIconUri();
+ Bitmap oldBitmap = mFetchArtTask == null ? mArtIconBitmap : mFetchArtTask.getIconBitmap();
+ Uri oldUri = mFetchArtTask == null ? mArtIconUri : mFetchArtTask.getIconUri();
+ if (oldBitmap != newBitmap) {
+ return true;
+ } else if (oldBitmap == null && !uriEquals(oldUri, newUri)) {
+ return true;
+ }
+ return false;
+ }
+
+ private final class MediaRouterCallback extends MediaRouter.Callback {
+ MediaRouterCallback() {
+ }
+
+ @Override
+ public void onRouteUnselected(MediaRouter router, MediaRouter.RouteInfo route) {
+ update(false);
+ }
+
+ @Override
+ public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo route) {
+ update(true);
+ }
+
+ @Override
+ public void onRouteVolumeChanged(MediaRouter router, MediaRouter.RouteInfo route) {
+ SeekBar volumeSlider = mVolumeSliderMap.get(route);
+ int volume = route.getVolume();
+ if (DEBUG) {
+ Log.d(TAG, "onRouteVolumeChanged(), route.getVolume:" + volume);
+ }
+ if (volumeSlider != null && mRouteInVolumeSliderTouched != route) {
+ volumeSlider.setProgress(volume);
+ }
+ }
+ }
+
+ private final class MediaControllerCallback extends MediaControllerCompat.Callback {
+ MediaControllerCallback() {
+ }
+
+ @Override
+ public void onSessionDestroyed() {
+ if (mMediaController != null) {
+ mMediaController.unregisterCallback(mControllerCallback);
+ mMediaController = null;
+ }
+ }
+
+ @Override
+ public void onPlaybackStateChanged(PlaybackStateCompat state) {
+ mState = state;
+ update(false);
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadataCompat metadata) {
+ mDescription = metadata == null ? null : metadata.getDescription();
+ updateArtIconIfNeeded();
+ update(false);
+ }
+ }
+
+ private final class ClickListener implements View.OnClickListener {
+ ClickListener() {
+ }
+
+ @Override
+ public void onClick(View v) {
+ int id = v.getId();
+ if (id == BUTTON_STOP_RES_ID || id == BUTTON_DISCONNECT_RES_ID) {
+ if (mRoute.isSelected()) {
+ mRouter.unselect(id == BUTTON_STOP_RES_ID ?
+ MediaRouter.UNSELECT_REASON_STOPPED :
+ MediaRouter.UNSELECT_REASON_DISCONNECTED);
+ }
+ dismiss();
+ } else if (id == R.id.mr_control_playback_ctrl) {
+ if (mMediaController != null && mState != null) {
+ boolean isPlaying = mState.getState() == PlaybackStateCompat.STATE_PLAYING;
+ int actionDescResId = 0;
+ if (isPlaying && isPauseActionSupported()) {
+ mMediaController.getTransportControls().pause();
+ actionDescResId = R.string.mr_controller_pause;
+ } else if (isPlaying && isStopActionSupported()) {
+ mMediaController.getTransportControls().stop();
+ actionDescResId = R.string.mr_controller_stop;
+ } else if (!isPlaying && isPlayActionSupported()){
+ mMediaController.getTransportControls().play();
+ actionDescResId = R.string.mr_controller_play;
+ }
+ // Announce the action for accessibility.
+ if (mAccessibilityManager != null && mAccessibilityManager.isEnabled()
+ && actionDescResId != 0) {
+ AccessibilityEvent event = AccessibilityEvent.obtain(
+ AccessibilityEventCompat.TYPE_ANNOUNCEMENT);
+ event.setPackageName(mContext.getPackageName());
+ event.setClassName(getClass().getName());
+ event.getText().add(mContext.getString(actionDescResId));
+ mAccessibilityManager.sendAccessibilityEvent(event);
+ }
+ }
+ } else if (id == R.id.mr_close) {
+ dismiss();
+ }
+ }
+ }
+
+ private class VolumeChangeListener implements SeekBar.OnSeekBarChangeListener {
+ private final Runnable mStopTrackingTouch = new Runnable() {
+ @Override
+ public void run() {
+ if (mRouteInVolumeSliderTouched != null) {
+ mRouteInVolumeSliderTouched = null;
+ if (mHasPendingUpdate) {
+ update(mPendingUpdateAnimationNeeded);
+ }
+ }
+ }
+ };
+
+ VolumeChangeListener() {
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+ if (mRouteInVolumeSliderTouched != null) {
+ mVolumeSlider.removeCallbacks(mStopTrackingTouch);
+ }
+ mRouteInVolumeSliderTouched = (MediaRouter.RouteInfo) seekBar.getTag();
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+ // Defer resetting mVolumeSliderTouched to allow the media route provider
+ // a little time to settle into its new state and publish the final
+ // volume update.
+ mVolumeSlider.postDelayed(mStopTrackingTouch, VOLUME_UPDATE_DELAY_MILLIS);
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser) {
+ MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) seekBar.getTag();
+ if (DEBUG) {
+ Log.d(TAG, "onProgressChanged(): calling "
+ + "MediaRouter.RouteInfo.requestSetVolume(" + progress + ")");
+ }
+ route.requestSetVolume(progress);
+ }
+ }
+ }
+
+ private class VolumeGroupAdapter extends ArrayAdapter<MediaRouter.RouteInfo> {
+ final float mDisabledAlpha;
+
+ public VolumeGroupAdapter(Context context, List<MediaRouter.RouteInfo> objects) {
+ super(context, 0, objects);
+ mDisabledAlpha = MediaRouterThemeHelper.getDisabledAlpha(context);
+ }
+
+ @Override
+ public boolean isEnabled(int position) {
+ return false;
+ }
+
+ @Override
+ public View getView(final int position, View convertView, ViewGroup parent) {
+ View v = convertView;
+ if (v == null) {
+ v = LayoutInflater.from(parent.getContext()).inflate(
+ R.layout.mr_controller_volume_item, parent, false);
+ } else {
+ updateVolumeGroupItemHeight(v);
+ }
+
+ MediaRouter.RouteInfo route = getItem(position);
+ if (route != null) {
+ boolean isEnabled = route.isEnabled();
+
+ TextView routeName = (TextView) v.findViewById(R.id.mr_name);
+ routeName.setEnabled(isEnabled);
+ routeName.setText(route.getName());
+
+ MediaRouteVolumeSlider volumeSlider =
+ (MediaRouteVolumeSlider) v.findViewById(R.id.mr_volume_slider);
+ MediaRouterThemeHelper.setVolumeSliderColor(
+ parent.getContext(), volumeSlider, mVolumeGroupList);
+ volumeSlider.setTag(route);
+ mVolumeSliderMap.put(route, volumeSlider);
+ volumeSlider.setHideThumb(!isEnabled);
+ volumeSlider.setEnabled(isEnabled);
+ if (isEnabled) {
+ if (isVolumeControlAvailable(route)) {
+ volumeSlider.setMax(route.getVolumeMax());
+ volumeSlider.setProgress(route.getVolume());
+ volumeSlider.setOnSeekBarChangeListener(mVolumeChangeListener);
+ } else {
+ volumeSlider.setMax(100);
+ volumeSlider.setProgress(100);
+ volumeSlider.setEnabled(false);
+ }
+ }
+
+ ImageView volumeItemIcon =
+ (ImageView) v.findViewById(R.id.mr_volume_item_icon);
+ volumeItemIcon.setAlpha(isEnabled ? 0xFF : (int) (0xFF * mDisabledAlpha));
+
+ // If overlay bitmap exists, real view should remain hidden until
+ // the animation ends.
+ LinearLayout container = (LinearLayout) v.findViewById(R.id.volume_item_container);
+ container.setVisibility(mGroupMemberRoutesAnimatingWithBitmap.contains(route)
+ ? View.INVISIBLE : View.VISIBLE);
+
+ // Routes which are being added will be invisible until animation ends.
+ if (mGroupMemberRoutesAdded != null && mGroupMemberRoutesAdded.contains(route)) {
+ Animation alphaAnim = new AlphaAnimation(0.0f, 0.0f);
+ alphaAnim.setDuration(0);
+ alphaAnim.setFillEnabled(true);
+ alphaAnim.setFillAfter(true);
+ v.clearAnimation();
+ v.startAnimation(alphaAnim);
+ }
+ }
+ return v;
+ }
+ }
+
+ private class FetchArtTask extends AsyncTask<Void, Void, Bitmap> {
+ // Show animation only when fetching takes a long time.
+ private static final long SHOW_ANIM_TIME_THRESHOLD_MILLIS = 120L;
+
+ private final Bitmap mIconBitmap;
+ private final Uri mIconUri;
+ private int mBackgroundColor;
+ private long mStartTimeMillis;
+
+ FetchArtTask() {
+ Bitmap bitmap = mDescription == null ? null : mDescription.getIconBitmap();
+ if (isBitmapRecycled(bitmap)) {
+ Log.w(TAG, "Can't fetch the given art bitmap because it's already recycled.");
+ bitmap = null;
+ }
+ mIconBitmap = bitmap;
+ mIconUri = mDescription == null ? null : mDescription.getIconUri();
+ }
+
+ public Bitmap getIconBitmap() {
+ return mIconBitmap;
+ }
+
+ public Uri getIconUri() {
+ return mIconUri;
+ }
+
+ @Override
+ protected void onPreExecute() {
+ mStartTimeMillis = SystemClock.uptimeMillis();
+ clearLoadedBitmap();
+ }
+
+ @Override
+ protected Bitmap doInBackground(Void... arg) {
+ Bitmap art = null;
+ if (mIconBitmap != null) {
+ art = mIconBitmap;
+ } else if (mIconUri != null) {
+ InputStream stream = null;
+ try {
+ if ((stream = openInputStreamByScheme(mIconUri)) == null) {
+ Log.w(TAG, "Unable to open: " + mIconUri);
+ return null;
+ }
+ // Query art size.
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeStream(stream, null, options);
+ if (options.outWidth == 0 || options.outHeight == 0) {
+ return null;
+ }
+ // Rewind the stream in order to restart art decoding.
+ try {
+ stream.reset();
+ } catch (IOException e) {
+ // Failed to rewind the stream, try to reopen it.
+ stream.close();
+ if ((stream = openInputStreamByScheme(mIconUri)) == null) {
+ Log.w(TAG, "Unable to open: " + mIconUri);
+ return null;
+ }
+ }
+ // Calculate required size to decode the art and possibly resize it.
+ options.inJustDecodeBounds = false;
+ int reqHeight = getDesiredArtHeight(options.outWidth, options.outHeight);
+ int ratio = options.outHeight / reqHeight;
+ options.inSampleSize = Math.max(1, Integer.highestOneBit(ratio));
+ if (isCancelled()) {
+ return null;
+ }
+ art = BitmapFactory.decodeStream(stream, null, options);
+ } catch (IOException e){
+ Log.w(TAG, "Unable to open: " + mIconUri, e);
+ } finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ } catch (IOException e) {
+ }
+ }
+ }
+ }
+ if (isBitmapRecycled(art)) {
+ Log.w(TAG, "Can't use recycled bitmap: " + art);
+ return null;
+ }
+ if (art != null && art.getWidth() < art.getHeight()) {
+ // Portrait art requires dominant color as background color.
+ Palette palette = new Palette.Builder(art).maximumColorCount(1).generate();
+ mBackgroundColor = palette.getSwatches().isEmpty()
+ ? 0 : palette.getSwatches().get(0).getRgb();
+ }
+ return art;
+ }
+
+ @Override
+ protected void onPostExecute(Bitmap art) {
+ mFetchArtTask = null;
+ if (!ObjectsCompat.equals(mArtIconBitmap, mIconBitmap)
+ || !ObjectsCompat.equals(mArtIconUri, mIconUri)) {
+ mArtIconBitmap = mIconBitmap;
+ mArtIconLoadedBitmap = art;
+ mArtIconUri = mIconUri;
+ mArtIconBackgroundColor = mBackgroundColor;
+ mArtIconIsLoaded = true;
+ long elapsedTimeMillis = SystemClock.uptimeMillis() - mStartTimeMillis;
+ // Loaded bitmap will be applied on the next update
+ update(elapsedTimeMillis > SHOW_ANIM_TIME_THRESHOLD_MILLIS);
+ }
+ }
+
+ private InputStream openInputStreamByScheme(Uri uri) throws IOException {
+ String scheme = uri.getScheme().toLowerCase();
+ InputStream stream = null;
+ if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)
+ || ContentResolver.SCHEME_CONTENT.equals(scheme)
+ || ContentResolver.SCHEME_FILE.equals(scheme)) {
+ stream = mContext.getContentResolver().openInputStream(uri);
+ } else {
+ URL url = new URL(uri.toString());
+ URLConnection conn = url.openConnection();
+ conn.setConnectTimeout(CONNECTION_TIMEOUT_MILLIS);
+ conn.setReadTimeout(CONNECTION_TIMEOUT_MILLIS);
+ stream = conn.getInputStream();
+ }
+ return (stream == null) ? null : new BufferedInputStream(stream);
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/app/MediaRouteControllerDialogFragment.java b/com/android/support/mediarouter/app/MediaRouteControllerDialogFragment.java
new file mode 100644
index 00000000..9442df7a
--- /dev/null
+++ b/com/android/support/mediarouter/app/MediaRouteControllerDialogFragment.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.app;
+
+import android.app.Dialog;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.os.Bundle;
+import android.support.v4.app.DialogFragment;
+
+/**
+ * Media route controller dialog fragment.
+ * <p>
+ * Creates a {@link MediaRouteControllerDialog}. The application may subclass
+ * this dialog fragment to customize the media route controller dialog.
+ * </p>
+ */
+public class MediaRouteControllerDialogFragment extends DialogFragment {
+ private MediaRouteControllerDialog mDialog;
+ /**
+ * Creates a media route controller dialog fragment.
+ * <p>
+ * All subclasses of this class must also possess a default constructor.
+ * </p>
+ */
+ public MediaRouteControllerDialogFragment() {
+ setCancelable(true);
+ }
+
+ /**
+ * Called when the controller dialog is being created.
+ * <p>
+ * Subclasses may override this method to customize the dialog.
+ * </p>
+ */
+ public MediaRouteControllerDialog onCreateControllerDialog(
+ Context context, Bundle savedInstanceState) {
+ return new MediaRouteControllerDialog(context);
+ }
+
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ mDialog = onCreateControllerDialog(getContext(), savedInstanceState);
+ return mDialog;
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ if (mDialog != null) {
+ mDialog.clearGroupListAnimation(false);
+ }
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ if (mDialog != null) {
+ mDialog.updateLayout();
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/app/MediaRouteDialogFactory.java b/com/android/support/mediarouter/app/MediaRouteDialogFactory.java
new file mode 100644
index 00000000..a9eaf394
--- /dev/null
+++ b/com/android/support/mediarouter/app/MediaRouteDialogFactory.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.app;
+
+import android.support.annotation.NonNull;
+
+/**
+ * The media route dialog factory is responsible for creating the media route
+ * chooser and controller dialogs as needed.
+ * <p>
+ * The application can customize the dialogs by providing a subclass of the
+ * dialog factory to the {@link MediaRouteButton} using the
+ * {@link MediaRouteButton#setDialogFactory setDialogFactory} method.
+ * </p>
+ */
+public class MediaRouteDialogFactory {
+ private static final MediaRouteDialogFactory sDefault = new MediaRouteDialogFactory();
+
+ /**
+ * Creates a default media route dialog factory.
+ */
+ public MediaRouteDialogFactory() {
+ }
+
+ /**
+ * Gets the default factory instance.
+ *
+ * @return The default media route dialog factory, never null.
+ */
+ @NonNull
+ public static MediaRouteDialogFactory getDefault() {
+ return sDefault;
+ }
+
+ /**
+ * Called when the chooser dialog is being opened and it is time to create the fragment.
+ * <p>
+ * Subclasses may override this method to create a customized fragment.
+ * </p>
+ *
+ * @return The media route chooser dialog fragment, must not be null.
+ */
+ @NonNull
+ public MediaRouteChooserDialogFragment onCreateChooserDialogFragment() {
+ return new MediaRouteChooserDialogFragment();
+ }
+
+ /**
+ * Called when the controller dialog is being opened and it is time to create the fragment.
+ * <p>
+ * Subclasses may override this method to create a customized fragment.
+ * </p>
+ *
+ * @return The media route controller dialog fragment, must not be null.
+ */
+ @NonNull
+ public MediaRouteControllerDialogFragment onCreateControllerDialogFragment() {
+ return new MediaRouteControllerDialogFragment();
+ }
+}
diff --git a/com/android/support/mediarouter/app/MediaRouteDialogHelper.java b/com/android/support/mediarouter/app/MediaRouteDialogHelper.java
new file mode 100644
index 00000000..6f75b466
--- /dev/null
+++ b/com/android/support/mediarouter/app/MediaRouteDialogHelper.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.app;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import com.android.media.update.R;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+final class MediaRouteDialogHelper {
+ /**
+ * The framework should set the dialog width properly, but somehow it doesn't work, hence
+ * duplicating a similar logic here to determine the appropriate dialog width.
+ */
+ public static int getDialogWidth(Context context) {
+ DisplayMetrics metrics = context.getResources().getDisplayMetrics();
+ boolean isPortrait = metrics.widthPixels < metrics.heightPixels;
+
+ TypedValue value = new TypedValue();
+ context.getResources().getValue(isPortrait ? R.dimen.mr_dialog_fixed_width_minor
+ : R.dimen.mr_dialog_fixed_width_major, value, true);
+ if (value.type == TypedValue.TYPE_DIMENSION) {
+ return (int) value.getDimension(metrics);
+ } else if (value.type == TypedValue.TYPE_FRACTION) {
+ return (int) value.getFraction(metrics.widthPixels, metrics.widthPixels);
+ }
+ return ViewGroup.LayoutParams.WRAP_CONTENT;
+ }
+
+ /**
+ * Compares two lists regardless of order.
+ *
+ * @param list1 A list
+ * @param list2 A list to be compared with {@code list1}
+ * @return True if two lists have exactly same items regardless of order, false otherwise.
+ */
+ public static <E> boolean listUnorderedEquals(List<E> list1, List<E> list2) {
+ HashSet<E> set1 = new HashSet<>(list1);
+ HashSet<E> set2 = new HashSet<>(list2);
+ return set1.equals(set2);
+ }
+
+ /**
+ * Compares two lists and returns a set of items which exist
+ * after-list but before-list, which means newly added items.
+ *
+ * @param before A list
+ * @param after A list to be compared with {@code before}
+ * @return A set of items which contains newly added items while
+ * comparing {@code after} to {@code before}.
+ */
+ public static <E> Set<E> getItemsAdded(List<E> before, List<E> after) {
+ HashSet<E> set = new HashSet<>(after);
+ set.removeAll(before);
+ return set;
+ }
+
+ /**
+ * Compares two lists and returns a set of items which exist
+ * before-list but after-list, which means removed items.
+ *
+ * @param before A list
+ * @param after A list to be compared with {@code before}
+ * @return A set of items which contains removed items while
+ * comparing {@code after} to {@code before}.
+ */
+ public static <E> Set<E> getItemsRemoved(List<E> before, List<E> after) {
+ HashSet<E> set = new HashSet<>(before);
+ set.removeAll(after);
+ return set;
+ }
+
+ /**
+ * Generates an item-Rect map which indicates where member
+ * items are located in the given ListView.
+ *
+ * @param listView A list view
+ * @param adapter An array adapter which contains an array of items.
+ * @return A map of items and bounds of their views located in the given list view.
+ */
+ public static <E> HashMap<E, Rect> getItemBoundMap(ListView listView,
+ ArrayAdapter<E> adapter) {
+ HashMap<E, Rect> itemBoundMap = new HashMap<>();
+ int firstVisiblePosition = listView.getFirstVisiblePosition();
+ for (int i = 0; i < listView.getChildCount(); ++i) {
+ int position = firstVisiblePosition + i;
+ E item = adapter.getItem(position);
+ View view = listView.getChildAt(i);
+ itemBoundMap.put(item,
+ new Rect(view.getLeft(), view.getTop(), view.getRight(), view.getBottom()));
+ }
+ return itemBoundMap;
+ }
+
+ /**
+ * Generates an item-BitmapDrawable map which stores snapshots
+ * of member items in the given ListView.
+ *
+ * @param context A context
+ * @param listView A list view
+ * @param adapter An array adapter which contains an array of items.
+ * @return A map of items and snapshots of their views in the given list view.
+ */
+ public static <E> HashMap<E, BitmapDrawable> getItemBitmapMap(Context context,
+ ListView listView, ArrayAdapter<E> adapter) {
+ HashMap<E, BitmapDrawable> itemBitmapMap = new HashMap<>();
+ int firstVisiblePosition = listView.getFirstVisiblePosition();
+ for (int i = 0; i < listView.getChildCount(); ++i) {
+ int position = firstVisiblePosition + i;
+ E item = adapter.getItem(position);
+ View view = listView.getChildAt(i);
+ itemBitmapMap.put(item, getViewBitmap(context, view));
+ }
+ return itemBitmapMap;
+ }
+
+ private static BitmapDrawable getViewBitmap(Context context, View view) {
+ Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
+ Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+ view.draw(canvas);
+ return new BitmapDrawable(context.getResources(), bitmap);
+ }
+}
diff --git a/com/android/support/mediarouter/app/MediaRouteDiscoveryFragment.java b/com/android/support/mediarouter/app/MediaRouteDiscoveryFragment.java
new file mode 100644
index 00000000..02ee1183
--- /dev/null
+++ b/com/android/support/mediarouter/app/MediaRouteDiscoveryFragment.java
@@ -0,0 +1,164 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.app;
+
+import android.os.Bundle;
+import android.support.v4.app.Fragment;
+
+import com.android.support.mediarouter.media.MediaRouter;
+import com.android.support.mediarouter.media.MediaRouteSelector;
+
+/**
+ * Media route discovery fragment.
+ * <p>
+ * This fragment takes care of registering a callback for media route discovery
+ * during the {@link Fragment#onStart onStart()} phase
+ * and removing it during the {@link Fragment#onStop onStop()} phase.
+ * </p><p>
+ * The application must supply a route selector to specify the kinds of routes
+ * to discover. The application may also override {@link #onCreateCallback} to
+ * provide the {@link MediaRouter} callback to register.
+ * </p><p>
+ * Note that the discovery callback makes the application be connected with all the
+ * {@link android.support.v7.media.MediaRouteProviderService media route provider services}
+ * while it is registered.
+ * </p>
+ */
+public class MediaRouteDiscoveryFragment extends Fragment {
+ private final String ARGUMENT_SELECTOR = "selector";
+
+ private MediaRouter mRouter;
+ private MediaRouteSelector mSelector;
+ private MediaRouter.Callback mCallback;
+
+ public MediaRouteDiscoveryFragment() {
+ }
+
+ /**
+ * Gets the media router instance.
+ */
+ public MediaRouter getMediaRouter() {
+ ensureRouter();
+ return mRouter;
+ }
+
+ private void ensureRouter() {
+ if (mRouter == null) {
+ mRouter = MediaRouter.getInstance(getContext());
+ }
+ }
+
+ /**
+ * Gets the media route selector for filtering the routes to be discovered.
+ *
+ * @return The selector, never null.
+ */
+ public MediaRouteSelector getRouteSelector() {
+ ensureRouteSelector();
+ return mSelector;
+ }
+
+ /**
+ * Sets the media route selector for filtering the routes to be discovered.
+ * This method must be called before the fragment is added.
+ *
+ * @param selector The selector to set.
+ */
+ public void setRouteSelector(MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ ensureRouteSelector();
+ if (!mSelector.equals(selector)) {
+ mSelector = selector;
+
+ Bundle args = getArguments();
+ if (args == null) {
+ args = new Bundle();
+ }
+ args.putBundle(ARGUMENT_SELECTOR, selector.asBundle());
+ setArguments(args);
+
+ if (mCallback != null) {
+ mRouter.removeCallback(mCallback);
+ mRouter.addCallback(mSelector, mCallback, onPrepareCallbackFlags());
+ }
+ }
+ }
+
+ private void ensureRouteSelector() {
+ if (mSelector == null) {
+ Bundle args = getArguments();
+ if (args != null) {
+ mSelector = MediaRouteSelector.fromBundle(args.getBundle(ARGUMENT_SELECTOR));
+ }
+ if (mSelector == null) {
+ mSelector = MediaRouteSelector.EMPTY;
+ }
+ }
+ }
+
+ /**
+ * Called to create the {@link android.support.v7.media.MediaRouter.Callback callback}
+ * that will be registered.
+ * <p>
+ * The default callback does nothing. The application may override this method to
+ * supply its own callback.
+ * </p>
+ *
+ * @return The new callback, or null if no callback should be registered.
+ */
+ public MediaRouter.Callback onCreateCallback() {
+ return new MediaRouter.Callback() { };
+ }
+
+ /**
+ * Called to prepare the callback flags that will be used when the
+ * {@link android.support.v7.media.MediaRouter.Callback callback} is registered.
+ * <p>
+ * The default implementation returns {@link MediaRouter#CALLBACK_FLAG_REQUEST_DISCOVERY}.
+ * </p>
+ *
+ * @return The desired callback flags.
+ */
+ public int onPrepareCallbackFlags() {
+ return MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY;
+ }
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ ensureRouteSelector();
+ ensureRouter();
+ mCallback = onCreateCallback();
+ if (mCallback != null) {
+ mRouter.addCallback(mSelector, mCallback, onPrepareCallbackFlags());
+ }
+ }
+
+ @Override
+ public void onStop() {
+ if (mCallback != null) {
+ mRouter.removeCallback(mCallback);
+ mCallback = null;
+ }
+
+ super.onStop();
+ }
+}
diff --git a/com/android/support/mediarouter/app/MediaRouteExpandCollapseButton.java b/com/android/support/mediarouter/app/MediaRouteExpandCollapseButton.java
new file mode 100644
index 00000000..392b39d9
--- /dev/null
+++ b/com/android/support/mediarouter/app/MediaRouteExpandCollapseButton.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.app;
+
+import android.content.Context;
+import android.graphics.ColorFilter;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.drawable.AnimationDrawable;
+import android.support.v4.content.ContextCompat;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.ImageButton;
+
+import com.android.media.update.R;
+
+/**
+ * Chevron/Caret button to expand/collapse group volume list with animation.
+ */
+class MediaRouteExpandCollapseButton extends ImageButton {
+ final AnimationDrawable mExpandAnimationDrawable;
+ final AnimationDrawable mCollapseAnimationDrawable;
+ final String mExpandGroupDescription;
+ final String mCollapseGroupDescription;
+ boolean mIsGroupExpanded;
+ OnClickListener mListener;
+
+ public MediaRouteExpandCollapseButton(Context context) {
+ this(context, null);
+ }
+
+ public MediaRouteExpandCollapseButton(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public MediaRouteExpandCollapseButton(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mExpandAnimationDrawable = (AnimationDrawable) ContextCompat.getDrawable(
+ context, R.drawable.mr_group_expand);
+ mCollapseAnimationDrawable = (AnimationDrawable) ContextCompat.getDrawable(
+ context, R.drawable.mr_group_collapse);
+
+ ColorFilter filter = new PorterDuffColorFilter(
+ MediaRouterThemeHelper.getControllerColor(context, defStyleAttr),
+ PorterDuff.Mode.SRC_IN);
+ mExpandAnimationDrawable.setColorFilter(filter);
+ mCollapseAnimationDrawable.setColorFilter(filter);
+
+ mExpandGroupDescription = context.getString(R.string.mr_controller_expand_group);
+ mCollapseGroupDescription = context.getString(R.string.mr_controller_collapse_group);
+
+ setImageDrawable(mExpandAnimationDrawable.getFrame(0));
+ setContentDescription(mExpandGroupDescription);
+
+ super.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View view) {
+ mIsGroupExpanded = !mIsGroupExpanded;
+ if (mIsGroupExpanded) {
+ setImageDrawable(mExpandAnimationDrawable);
+ mExpandAnimationDrawable.start();
+ setContentDescription(mCollapseGroupDescription);
+ } else {
+ setImageDrawable(mCollapseAnimationDrawable);
+ mCollapseAnimationDrawable.start();
+ setContentDescription(mExpandGroupDescription);
+ }
+ if (mListener != null) {
+ mListener.onClick(view);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void setOnClickListener(OnClickListener listener) {
+ mListener = listener;
+ }
+}
diff --git a/com/android/support/mediarouter/app/MediaRouteVolumeSlider.java b/com/android/support/mediarouter/app/MediaRouteVolumeSlider.java
new file mode 100644
index 00000000..7a34fb54
--- /dev/null
+++ b/com/android/support/mediarouter/app/MediaRouteVolumeSlider.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.app;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.support.v7.widget.AppCompatSeekBar;
+import android.util.AttributeSet;
+import android.util.Log;
+
+/**
+ * Volume slider with showing, hiding, and applying alpha supports to the thumb.
+ */
+class MediaRouteVolumeSlider extends AppCompatSeekBar {
+ private static final String TAG = "MediaRouteVolumeSlider";
+
+ private final float mDisabledAlpha;
+
+ private boolean mHideThumb;
+ private Drawable mThumb;
+ private int mColor;
+
+ public MediaRouteVolumeSlider(Context context) {
+ this(context, null);
+ }
+
+ public MediaRouteVolumeSlider(Context context, AttributeSet attrs) {
+ this(context, attrs, android.support.v7.appcompat.R.attr.seekBarStyle);
+ }
+
+ public MediaRouteVolumeSlider(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ mDisabledAlpha = MediaRouterThemeHelper.getDisabledAlpha(context);
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ int alpha = isEnabled() ? 0xFF : (int) (0xFF * mDisabledAlpha);
+
+ // The thumb drawable is a collection of drawables and its current drawables are changed per
+ // state. Apply the color filter and alpha on every state change.
+ mThumb.setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
+ mThumb.setAlpha(alpha);
+
+ getProgressDrawable().setColorFilter(mColor, PorterDuff.Mode.SRC_IN);
+ getProgressDrawable().setAlpha(alpha);
+ }
+
+ @Override
+ public void setThumb(Drawable thumb) {
+ mThumb = thumb;
+ super.setThumb(mHideThumb ? null : mThumb);
+ }
+
+ /**
+ * Sets whether to show or hide thumb.
+ */
+ public void setHideThumb(boolean hideThumb) {
+ if (mHideThumb == hideThumb) {
+ return;
+ }
+ mHideThumb = hideThumb;
+ super.setThumb(mHideThumb ? null : mThumb);
+ }
+
+ /**
+ * Sets the volume slider color. The change takes effect next time drawable state is changed.
+ * <p>
+ * The color cannot be translucent, otherwise the underlying progress bar will be seen through
+ * the thumb.
+ * </p>
+ */
+ public void setColor(int color) {
+ if (mColor == color) {
+ return;
+ }
+ if (Color.alpha(color) != 0xFF) {
+ Log.e(TAG, "Volume slider color cannot be translucent: #" + Integer.toHexString(color));
+ }
+ mColor = color;
+ }
+}
diff --git a/com/android/support/mediarouter/app/MediaRouterThemeHelper.java b/com/android/support/mediarouter/app/MediaRouterThemeHelper.java
new file mode 100644
index 00000000..7440130c
--- /dev/null
+++ b/com/android/support/mediarouter/app/MediaRouterThemeHelper.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.app;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.support.annotation.IntDef;
+import android.support.v4.graphics.ColorUtils;
+import android.util.TypedValue;
+import android.view.ContextThemeWrapper;
+import android.view.View;
+
+import com.android.media.update.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+final class MediaRouterThemeHelper {
+ private static final float MIN_CONTRAST = 3.0f;
+
+ @IntDef({COLOR_DARK_ON_LIGHT_BACKGROUND, COLOR_WHITE_ON_DARK_BACKGROUND})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface ControllerColorType {}
+
+ static final int COLOR_DARK_ON_LIGHT_BACKGROUND = 0xDE000000; /* Opacity of 87% */
+ static final int COLOR_WHITE_ON_DARK_BACKGROUND = Color.WHITE;
+
+ private MediaRouterThemeHelper() {
+ }
+
+ static Context createThemedButtonContext(Context context) {
+ // Apply base Media Router theme.
+ context = new ContextThemeWrapper(context, getRouterThemeId(context));
+
+ // Apply custom Media Router theme.
+ int style = getThemeResource(context, R.attr.mediaRouteTheme);
+ if (style != 0) {
+ context = new ContextThemeWrapper(context, style);
+ }
+
+ return context;
+ }
+
+ /*
+ * The following two methods are to be used in conjunction. They should be used to prepare
+ * the context and theme for a super class constructor (the latter method relies on the
+ * former method to properly prepare the context):
+ * super(context = createThemedDialogContext(context, theme),
+ * createThemedDialogStyle(context));
+ *
+ * It will apply theme in the following order (style lookups will be done in reverse):
+ * 1) Current theme
+ * 2) Supplied theme
+ * 3) Base Media Router theme
+ * 4) Custom Media Router theme, if provided
+ */
+ static Context createThemedDialogContext(Context context, int theme, boolean alertDialog) {
+ // 1) Current theme is already applied to the context
+
+ // 2) If no theme is supplied, look it up from the context (dialogTheme/alertDialogTheme)
+ if (theme == 0) {
+ theme = getThemeResource(context, !alertDialog
+ ? android.support.v7.appcompat.R.attr.dialogTheme
+ : android.support.v7.appcompat.R.attr.alertDialogTheme);
+ }
+ // Apply it
+ context = new ContextThemeWrapper(context, theme);
+
+ // 3) If a custom Media Router theme is provided then apply the base theme
+ if (getThemeResource(context, R.attr.mediaRouteTheme) != 0) {
+ context = new ContextThemeWrapper(context, getRouterThemeId(context));
+ }
+
+ return context;
+ }
+ // This method should be used in conjunction with the previous method.
+ static int createThemedDialogStyle(Context context) {
+ // 4) Apply the custom Media Router theme
+ int theme = getThemeResource(context, R.attr.mediaRouteTheme);
+ if (theme == 0) {
+ // 3) No custom MediaRouther theme was provided so apply the base theme instead
+ theme = getRouterThemeId(context);
+ }
+
+ return theme;
+ }
+ // END. Previous two methods should be used in conjunction.
+
+ static int getThemeResource(Context context, int attr) {
+ TypedValue value = new TypedValue();
+ return context.getTheme().resolveAttribute(attr, value, true) ? value.resourceId : 0;
+ }
+
+ static float getDisabledAlpha(Context context) {
+ TypedValue value = new TypedValue();
+ return context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, value, true)
+ ? value.getFloat() : 0.5f;
+ }
+
+ static @ControllerColorType int getControllerColor(Context context, int style) {
+ int primaryColor = getThemeColor(context, style,
+ android.support.v7.appcompat.R.attr.colorPrimary);
+ if (primaryColor == 0) {
+ primaryColor = getThemeColor(context, style, android.R.attr.colorPrimary);
+ if (primaryColor == 0) {
+ primaryColor = 0xFF000000;
+ }
+ }
+ if (ColorUtils.calculateContrast(COLOR_WHITE_ON_DARK_BACKGROUND, primaryColor)
+ >= MIN_CONTRAST) {
+ return COLOR_WHITE_ON_DARK_BACKGROUND;
+ }
+ return COLOR_DARK_ON_LIGHT_BACKGROUND;
+ }
+
+ static int getButtonTextColor(Context context) {
+ int primaryColor = getThemeColor(context, 0,
+ android.support.v7.appcompat.R.attr.colorPrimary);
+ int backgroundColor = getThemeColor(context, 0, android.R.attr.colorBackground);
+
+ if (ColorUtils.calculateContrast(primaryColor, backgroundColor) < MIN_CONTRAST) {
+ // Default to colorAccent if the contrast ratio is low.
+ return getThemeColor(context, 0, android.support.v7.appcompat.R.attr.colorAccent);
+ }
+ return primaryColor;
+ }
+
+ static void setMediaControlsBackgroundColor(
+ Context context, View mainControls, View groupControls, boolean hasGroup) {
+ int primaryColor = getThemeColor(context, 0,
+ android.support.v7.appcompat.R.attr.colorPrimary);
+ int primaryDarkColor = getThemeColor(context, 0,
+ android.support.v7.appcompat.R.attr.colorPrimaryDark);
+ if (hasGroup && getControllerColor(context, 0) == COLOR_DARK_ON_LIGHT_BACKGROUND) {
+ // Instead of showing dark controls in a possibly dark (i.e. the primary dark), model
+ // the white dialog and use the primary color for the group controls.
+ primaryDarkColor = primaryColor;
+ primaryColor = Color.WHITE;
+ }
+ mainControls.setBackgroundColor(primaryColor);
+ groupControls.setBackgroundColor(primaryDarkColor);
+ // Also store the background colors to the view tags. They are used in
+ // setVolumeSliderColor() below.
+ mainControls.setTag(primaryColor);
+ groupControls.setTag(primaryDarkColor);
+ }
+
+ static void setVolumeSliderColor(
+ Context context, MediaRouteVolumeSlider volumeSlider, View backgroundView) {
+ int controllerColor = getControllerColor(context, 0);
+ if (Color.alpha(controllerColor) != 0xFF) {
+ // Composite with the background in order not to show the underlying progress bar
+ // through the thumb.
+ int backgroundColor = (int) backgroundView.getTag();
+ controllerColor = ColorUtils.compositeColors(controllerColor, backgroundColor);
+ }
+ volumeSlider.setColor(controllerColor);
+ }
+
+ private static boolean isLightTheme(Context context) {
+ TypedValue value = new TypedValue();
+ return context.getTheme().resolveAttribute(android.support.v7.appcompat.R.attr.isLightTheme,
+ value, true) && value.data != 0;
+ }
+
+ private static int getThemeColor(Context context, int style, int attr) {
+ if (style != 0) {
+ int[] attrs = { attr };
+ TypedArray ta = context.obtainStyledAttributes(style, attrs);
+ int color = ta.getColor(0, 0);
+ ta.recycle();
+ if (color != 0) {
+ return color;
+ }
+ }
+ TypedValue value = new TypedValue();
+ context.getTheme().resolveAttribute(attr, value, true);
+ if (value.resourceId != 0) {
+ return context.getResources().getColor(value.resourceId);
+ }
+ return value.data;
+ }
+
+ static int getRouterThemeId(Context context) {
+ int themeId;
+ if (isLightTheme(context)) {
+ if (getControllerColor(context, 0) == COLOR_DARK_ON_LIGHT_BACKGROUND) {
+ themeId = R.style.Theme_MediaRouter_Light;
+ } else {
+ themeId = R.style.Theme_MediaRouter_Light_DarkControlPanel;
+ }
+ } else {
+ if (getControllerColor(context, 0) == COLOR_DARK_ON_LIGHT_BACKGROUND) {
+ themeId = R.style.Theme_MediaRouter_LightControlPanel;
+ } else {
+ themeId = R.style.Theme_MediaRouter;
+ }
+ }
+ return themeId;
+ }
+}
diff --git a/com/android/support/mediarouter/app/OverlayListView.java b/com/android/support/mediarouter/app/OverlayListView.java
new file mode 100644
index 00000000..59019ffd
--- /dev/null
+++ b/com/android/support/mediarouter/app/OverlayListView.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.app;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.BitmapDrawable;
+import android.util.AttributeSet;
+import android.view.animation.Interpolator;
+import android.widget.ListView;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/**
+ * A ListView which has an additional overlay layer. {@link BitmapDrawable}
+ * can be added to the layer and can be animated.
+ */
+final class OverlayListView extends ListView {
+ private final List<OverlayObject> mOverlayObjects = new ArrayList<>();
+
+ public OverlayListView(Context context) {
+ super(context);
+ }
+
+ public OverlayListView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public OverlayListView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ /**
+ * Adds an object to the overlay layer.
+ *
+ * @param object An object to be added.
+ */
+ public void addOverlayObject(OverlayObject object) {
+ mOverlayObjects.add(object);
+ }
+
+ /**
+ * Starts all animations of objects in the overlay layer.
+ */
+ public void startAnimationAll() {
+ for (OverlayObject object : mOverlayObjects) {
+ if (!object.isAnimationStarted()) {
+ object.startAnimation(getDrawingTime());
+ }
+ }
+ }
+
+ /**
+ * Stops all animations of objects in the overlay layer.
+ */
+ public void stopAnimationAll() {
+ for (OverlayObject object : mOverlayObjects) {
+ object.stopAnimation();
+ }
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ if (mOverlayObjects.size() > 0) {
+ Iterator<OverlayObject> it = mOverlayObjects.iterator();
+ while (it.hasNext()) {
+ OverlayObject object = it.next();
+ BitmapDrawable bitmap = object.getBitmapDrawable();
+ if (bitmap != null) {
+ bitmap.draw(canvas);
+ }
+ if (!object.update(getDrawingTime())) {
+ it.remove();
+ }
+ }
+ }
+ }
+
+ /**
+ * A class that represents an object to be shown in the overlay layer.
+ */
+ public static class OverlayObject {
+ private BitmapDrawable mBitmap;
+ private float mCurrentAlpha = 1.0f;
+ private Rect mCurrentBounds;
+ private Interpolator mInterpolator;
+ private long mDuration;
+ private Rect mStartRect;
+ private int mDeltaY;
+ private float mStartAlpha = 1.0f;
+ private float mEndAlpha = 1.0f;
+ private long mStartTime;
+ private boolean mIsAnimationStarted;
+ private boolean mIsAnimationEnded;
+ private OnAnimationEndListener mListener;
+
+ public OverlayObject(BitmapDrawable bitmap, Rect startRect) {
+ mBitmap = bitmap;
+ mStartRect = startRect;
+ mCurrentBounds = new Rect(startRect);
+ if (mBitmap != null && mCurrentBounds != null) {
+ mBitmap.setAlpha((int) (mCurrentAlpha * 255));
+ mBitmap.setBounds(mCurrentBounds);
+ }
+ }
+
+ /**
+ * Returns the bitmap that this object represents.
+ *
+ * @return BitmapDrawable that this object has.
+ */
+ public BitmapDrawable getBitmapDrawable() {
+ return mBitmap;
+ }
+
+ /**
+ * Returns the started status of the animation.
+ *
+ * @return True if the animation has started, false otherwise.
+ */
+ public boolean isAnimationStarted() {
+ return mIsAnimationStarted;
+ }
+
+ /**
+ * Sets animation for varying alpha.
+ *
+ * @param startAlpha Starting alpha value for the animation, where 1.0 means
+ * fully opaque and 0.0 means fully transparent.
+ * @param endAlpha Ending alpha value for the animation.
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setAlphaAnimation(float startAlpha, float endAlpha) {
+ mStartAlpha = startAlpha;
+ mEndAlpha = endAlpha;
+ return this;
+ }
+
+ /**
+ * Sets animation for moving objects vertically.
+ *
+ * @param deltaY Distance to move in pixels.
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setTranslateYAnimation(int deltaY) {
+ mDeltaY = deltaY;
+ return this;
+ }
+
+ /**
+ * Sets how long the animation will last.
+ *
+ * @param duration Duration in milliseconds
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setDuration(long duration) {
+ mDuration = duration;
+ return this;
+ }
+
+ /**
+ * Sets the acceleration curve for this animation.
+ *
+ * @param interpolator The interpolator which defines the acceleration curve
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setInterpolator(Interpolator interpolator) {
+ mInterpolator = interpolator;
+ return this;
+ }
+
+ /**
+ * Binds an animation end listener to the animation.
+ *
+ * @param listener the animation end listener to be notified.
+ * @return This OverlayObject to allow for chaining of calls.
+ */
+ public OverlayObject setAnimationEndListener(OnAnimationEndListener listener) {
+ mListener = listener;
+ return this;
+ }
+
+ /**
+ * Starts the animation and sets the start time.
+ *
+ * @param startTime Start time to be set in Millis
+ */
+ public void startAnimation(long startTime) {
+ mStartTime = startTime;
+ mIsAnimationStarted = true;
+ }
+
+ /**
+ * Stops the animation.
+ */
+ public void stopAnimation() {
+ mIsAnimationStarted = true;
+ mIsAnimationEnded = true;
+ if (mListener != null) {
+ mListener.onAnimationEnd();
+ }
+ }
+
+ /**
+ * Calculates and updates current bounds and alpha value.
+ *
+ * @param currentTime Current time.in millis
+ */
+ public boolean update(long currentTime) {
+ if (mIsAnimationEnded) {
+ return false;
+ }
+ float normalizedTime = (currentTime - mStartTime) / (float) mDuration;
+ normalizedTime = Math.max(0.0f, Math.min(1.0f, normalizedTime));
+ if (!mIsAnimationStarted) {
+ normalizedTime = 0.0f;
+ }
+ float interpolatedTime = (mInterpolator == null) ? normalizedTime
+ : mInterpolator.getInterpolation(normalizedTime);
+ int deltaY = (int) (mDeltaY * interpolatedTime);
+ mCurrentBounds.top = mStartRect.top + deltaY;
+ mCurrentBounds.bottom = mStartRect.bottom + deltaY;
+ mCurrentAlpha = mStartAlpha + (mEndAlpha - mStartAlpha) * interpolatedTime;
+ if (mBitmap != null && mCurrentBounds != null) {
+ mBitmap.setAlpha((int) (mCurrentAlpha * 255));
+ mBitmap.setBounds(mCurrentBounds);
+ }
+ if (mIsAnimationStarted && normalizedTime >= 1.0f) {
+ mIsAnimationEnded = true;
+ if (mListener != null) {
+ mListener.onAnimationEnd();
+ }
+ }
+ return !mIsAnimationEnded;
+ }
+
+ /**
+ * An animation listener that receives notifications when the animation ends.
+ */
+ public interface OnAnimationEndListener {
+ /**
+ * Notifies the end of the animation.
+ */
+ public void onAnimationEnd();
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/MediaControlIntent.java b/com/android/support/mediarouter/media/MediaControlIntent.java
new file mode 100644
index 00000000..1d9e7774
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaControlIntent.java
@@ -0,0 +1,1228 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.app.PendingIntent;
+import android.content.Intent;
+import android.net.Uri;
+
+/**
+ * Constants for media control intents.
+ * <p>
+ * This class declares a set of standard media control intent categories and actions that
+ * applications can use to identify the capabilities of media routes and control them.
+ * </p>
+ *
+ * <h3>Media control intent categories</h3>
+ * <p>
+ * Media control intent categories specify means by which applications can
+ * send media to the destination of a media route. Categories are sometimes referred
+ * to as describing "types" or "kinds" of routes.
+ * </p><p>
+ * For example, if a route supports the {@link #CATEGORY_REMOTE_PLAYBACK remote playback category},
+ * then an application can ask it to play media remotely by sending a
+ * {@link #ACTION_PLAY play} or {@link #ACTION_ENQUEUE enqueue} intent with the Uri of the
+ * media content to play. Such a route may then be referred to as
+ * a "remote playback route" because it supports remote playback requests. It is common
+ * for a route to support multiple categories of requests at the same time, such as
+ * live audio and live video.
+ * </p><p>
+ * The following standard route categories are defined.
+ * </p><ul>
+ * <li>{@link #CATEGORY_LIVE_AUDIO Live audio}: The route supports streaming live audio
+ * from the device to the destination. Live audio routes include local speakers
+ * and Bluetooth headsets.
+ * <li>{@link #CATEGORY_LIVE_VIDEO Live video}: The route supports streaming live video
+ * from the device to the destination. Live video routes include local displays
+ * and wireless displays that support mirroring and
+ * {@link android.app.Presentation presentations}. Live video routes typically also
+ * support live audio capabilities.
+ * <li>{@link #CATEGORY_REMOTE_PLAYBACK Remote playback}: The route supports sending
+ * remote playback requests for media content to the destination. The content to be
+ * played is identified by a Uri and mime-type.
+ * </ul><p>
+ * Media route providers may define custom media control intent categories of their own in
+ * addition to the standard ones. Custom categories can be used to provide a variety
+ * of features to applications that recognize and know how to use them. For example,
+ * a media route provider might define a custom category to indicate that its routes
+ * support a special device-specific control interface in addition to other
+ * standard features.
+ * </p><p>
+ * Applications can determine which categories a route supports by using the
+ * {@link MediaRouter.RouteInfo#supportsControlCategory MediaRouter.RouteInfo.supportsControlCategory}
+ * or {@link MediaRouter.RouteInfo#getControlFilters MediaRouter.RouteInfo.getControlFilters}
+ * methods. Applications can also specify the types of routes that they want to use by
+ * creating {@link MediaRouteSelector media route selectors} that contain the desired
+ * categories and are used to filter routes in several parts of the media router API.
+ * </p>
+ *
+ * <h3>Media control intent actions</h3>
+ * <p>
+ * Media control intent actions specify particular functions that applications
+ * can ask the destination of a media route to perform. Media route control requests
+ * take the form of intents in a similar manner to other intents used to start activities
+ * or send broadcasts. The difference is that media control intents are directed to
+ * routes rather than activity or broadcast receiver components.
+ * </p><p>
+ * Each media route control intent specifies an action, a category and some number of parameters
+ * that are supplied as extras. Applications send media control requests to routes using the
+ * {@link MediaRouter.RouteInfo#sendControlRequest MediaRouter.RouteInfo.sendControlRequest}
+ * method and receive results via a callback.
+ * </p><p>
+ * All media control intent actions are associated with the media control intent categories
+ * that support them. Thus only remote playback routes may perform remote playback actions.
+ * The documentation of each action specifies the category to which the action belongs,
+ * the parameters it requires, and the results it returns.
+ * </p>
+ *
+ * <h3>Live audio and live video routes</h3>
+ * <p>
+ * {@link #CATEGORY_LIVE_AUDIO Live audio} and {@link #CATEGORY_LIVE_VIDEO live video}
+ * routes present media using standard system interfaces such as audio streams,
+ * {@link android.app.Presentation presentations} or display mirroring. These routes are
+ * the easiest to use because applications simply render content locally on the device
+ * and the system streams it to the route destination automatically.
+ * </p><p>
+ * In most cases, applications can stream content to live audio and live video routes in
+ * the same way they would play the content locally without any modification. However,
+ * applications may also be able to take advantage of more sophisticated features such
+ * as second-screen presentation APIs that are particular to these routes.
+ * </p>
+ *
+ * <h3>Remote playback routes</h3>
+ * <p>
+ * {@link #CATEGORY_REMOTE_PLAYBACK Remote playback} routes present media remotely
+ * by playing content from a Uri.
+ * These routes destinations take responsibility for fetching and rendering content
+ * on their own. Applications do not render the content themselves; instead, applications
+ * send control requests to initiate play, pause, resume, or stop media items and receive
+ * status updates as they change state.
+ * </p>
+ *
+ * <h4>Sessions</h4>
+ * <p>
+ * Each remote media playback action is conducted within the scope of a session.
+ * Sessions are used to prevent applications from accidentally interfering with one
+ * another because at most one session can be valid at a time.
+ * </p><p>
+ * A session can be created using the {@link #ACTION_START_SESSION start session action}
+ * and terminated using the {@link #ACTION_END_SESSION end session action} when the
+ * route provides explicit session management features.
+ * </p><p>
+ * Explicit session management was added in a later revision of the protocol so not
+ * all routes support it. If the route does not support explicit session management
+ * then implicit session management may still be used. Implicit session management
+ * relies on the use of the {@link #ACTION_PLAY play} and {@link #ACTION_ENQUEUE enqueue}
+ * actions which have the side-effect of creating a new session if none is provided
+ * as argument.
+ * </p><p>
+ * When a new session is created, the previous session is invalidated and any ongoing
+ * media playback is stopped before the requested action is performed. Any attempt
+ * to use an invalidated session will result in an error. (Protocol implementations
+ * are encouraged to aggressively discard information associated with invalidated sessions
+ * since it is no longer of use.)
+ * </p><p>
+ * Each session is identified by a unique session id that may be used to control
+ * the session using actions such as pause, resume, stop and end session.
+ * </p>
+ *
+ * <h4>Media items</h4>
+ * <p>
+ * Each successful {@link #ACTION_PLAY play} or {@link #ACTION_ENQUEUE enqueue} action
+ * returns a unique media item id that an application can use to monitor and control
+ * playback. The media item id may be passed to other actions such as
+ * {@link #ACTION_SEEK seek} or {@link #ACTION_GET_STATUS get status}. It will also appear
+ * as a parameter in status update broadcasts to identify the associated playback request.
+ * </p><p>
+ * Each media item is scoped to the session in which it was created. Therefore media item
+ * ids are only ever used together with session ids. Media item ids are meaningless
+ * on their own. When the session is invalidated, all of its media items are also
+ * invalidated.
+ * </p>
+ *
+ * <h4>The playback queue</h4>
+ * <p>
+ * Each session has its own playback queue that consists of the media items that
+ * are pending, playing, buffering or paused. Items are added to the queue when
+ * a playback request is issued. Items are removed from the queue when they are no
+ * longer eligible for playback (enter terminal states).
+ * </p><p>
+ * As described in the {@link MediaItemStatus} class, media items initially
+ * start in a pending state, transition to the playing (or buffering or paused) state
+ * during playback, and end in a finished, canceled, invalidated or error state.
+ * Once the current item enters a terminal state, playback proceeds on to the
+ * next item.
+ * </p><p>
+ * The application should determine whether the route supports queuing by checking
+ * whether the {@link #ACTION_ENQUEUE} action is declared in the route's control filter
+ * using {@link MediaRouter.RouteInfo#supportsControlRequest RouteInfo.supportsControlRequest}.
+ * </p><p>
+ * If the {@link #ACTION_ENQUEUE} action is supported by the route, then the route promises
+ * to allow at least two items (possibly more) to be enqueued at a time. Enqueued items play
+ * back to back one after the other as the previous item completes. Ideally there should
+ * be no audible pause between items for standard audio content types.
+ * </p><p>
+ * If the {@link #ACTION_ENQUEUE} action is not supported by the route, then the queue
+ * effectively contains at most one item at a time. Each play action has the effect of
+ * clearing the queue and resetting its state before the next item is played.
+ * </p>
+ *
+ * <h4>Impact of pause, resume, stop and play actions on the playback queue</h4>
+ * <p>
+ * The pause, resume and stop actions affect the session's whole queue. Pause causes
+ * the playback queue to be suspended no matter which item is currently playing.
+ * Resume reverses the effects of pause. Stop clears the queue and also resets
+ * the pause flag just like resume.
+ * </p><p>
+ * As described earlier, the play action has the effect of clearing the queue
+ * and completely resetting its state (like the stop action) then enqueuing a
+ * new media item to be played immediately. Play is therefore equivalent
+ * to stop followed by an action to enqueue an item.
+ * </p><p>
+ * The play action is also special in that it can be used to create new sessions.
+ * An application with simple needs may find that it only needs to use play
+ * (and occasionally stop) to control playback.
+ * </p>
+ *
+ * <h4>Resolving conflicts between applications</h4>
+ * <p>
+ * When an application has a valid session, it is essentially in control of remote playback
+ * on the route. No other application can view or modify the remote playback state
+ * of that application's session without knowing its id.
+ * </p><p>
+ * However, other applications can perform actions that have the effect of stopping
+ * playback and invalidating the current session. When this occurs, the former application
+ * will be informed that it has lost control by way of individual media item status
+ * update broadcasts that indicate that its queued media items have become
+ * {@link MediaItemStatus#PLAYBACK_STATE_INVALIDATED invalidated}. This broadcast
+ * implies that playback was terminated abnormally by an external cause.
+ * </p><p>
+ * Applications should handle conflicts conservatively to allow other applications to
+ * smoothly assume control over the route. When a conflict occurs, the currently playing
+ * application should release its session and allow the new application to use the
+ * route until such time as the user intervenes to take over the route again and begin
+ * a new playback session.
+ * </p>
+ *
+ * <h4>Basic actions</h4>
+ * <p>
+ * The following basic actions must be supported (all or nothing) by all remote
+ * playback routes. These actions form the basis of the remote playback protocol
+ * and are required in all implementations.
+ * </p><ul>
+ * <li>{@link #ACTION_PLAY Play}: Starts playing content specified by a given Uri
+ * and returns a new media item id to describe the request. Implicitly creates a new
+ * session if no session id was specified as a parameter.
+ * <li>{@link #ACTION_SEEK Seek}: Sets the content playback position of a specific media item.
+ * <li>{@link #ACTION_GET_STATUS Get status}: Gets the status of a media item
+ * including the item's current playback position and progress.
+ * <li>{@link #ACTION_PAUSE Pause}: Pauses playback of the queue.
+ * <li>{@link #ACTION_RESUME Resume}: Resumes playback of the queue.
+ * <li>{@link #ACTION_STOP Stop}: Stops playback, clears the queue, and resets the
+ * pause state.
+ * </ul>
+ *
+ * <h4>Queue actions</h4>
+ * <p>
+ * The following queue actions must be supported (all or nothing) by remote
+ * playback routes that offer optional queuing capabilities.
+ * </p><ul>
+ * <li>{@link #ACTION_ENQUEUE Enqueue}: Enqueues content specified by a given Uri
+ * and returns a new media item id to describe the request. Implicitly creates a new
+ * session if no session id was specified as a parameter.
+ * <li>{@link #ACTION_REMOVE Remove}: Removes a specified media item from the queue.
+ * </ul>
+ *
+ * <h4>Session actions</h4>
+ * <p>
+ * The following session actions must be supported (all or nothing) by remote
+ * playback routes that offer optional session management capabilities.
+ * </p><ul>
+ * <li>{@link #ACTION_START_SESSION Start session}: Starts a new session explicitly.
+ * <li>{@link #ACTION_GET_SESSION_STATUS Get session status}: Gets the status of a session.
+ * <li>{@link #ACTION_END_SESSION End session}: Ends a session explicitly.
+ * </ul>
+ *
+ * <h4>Implementation note</h4>
+ * <p>
+ * Implementations of the remote playback protocol must implement <em>all</em> of the
+ * documented actions, parameters and results. Note that the documentation is written from
+ * the perspective of a client of the protocol. In particular, whenever a parameter
+ * is described as being "optional", it is only from the perspective of the client.
+ * Compliant media route provider implementations of this protocol must support all
+ * of the features described herein.
+ * </p>
+ */
+public final class MediaControlIntent {
+ /* Route categories. */
+
+ /**
+ * Media control category: Live audio.
+ * <p>
+ * A route that supports live audio routing will allow the media audio stream
+ * to be sent to supported destinations. This can include internal speakers or
+ * audio jacks on the device itself, A2DP devices, and more.
+ * </p><p>
+ * When a live audio route is selected, audio routing is transparent to the application.
+ * All audio played on the media stream will be routed to the selected destination.
+ * </p><p>
+ * Refer to the class documentation for details about live audio routes.
+ * </p>
+ */
+ public static final String CATEGORY_LIVE_AUDIO = "android.media.intent.category.LIVE_AUDIO";
+
+ /**
+ * Media control category: Live video.
+ * <p>
+ * A route that supports live video routing will allow a mirrored version
+ * of the device's primary display or a customized
+ * {@link android.app.Presentation Presentation} to be sent to supported
+ * destinations.
+ * </p><p>
+ * When a live video route is selected, audio and video routing is transparent
+ * to the application. By default, audio and video is routed to the selected
+ * destination. For certain live video routes, the application may also use a
+ * {@link android.app.Presentation Presentation} to replace the mirrored view
+ * on the external display with different content.
+ * </p><p>
+ * Refer to the class documentation for details about live video routes.
+ * </p>
+ *
+ * @see MediaRouter.RouteInfo#getPresentationDisplay()
+ * @see android.app.Presentation
+ */
+ public static final String CATEGORY_LIVE_VIDEO = "android.media.intent.category.LIVE_VIDEO";
+
+ /**
+ * Media control category: Remote playback.
+ * <p>
+ * A route that supports remote playback routing will allow an application to send
+ * requests to play content remotely to supported destinations.
+ * </p><p>
+ * Remote playback routes destinations operate independently of the local device.
+ * When a remote playback route is selected, the application can control the content
+ * playing on the destination by sending media control actions to the route.
+ * The application may also receive status updates from the route regarding
+ * remote playback.
+ * </p><p>
+ * Refer to the class documentation for details about remote playback routes.
+ * </p>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ */
+ public static final String CATEGORY_REMOTE_PLAYBACK =
+ "android.media.intent.category.REMOTE_PLAYBACK";
+
+ /* Remote playback actions that affect individual items. */
+
+ /**
+ * Remote playback media control action: Play media item.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes a remote playback route to start playing content with
+ * the {@link Uri} specified in the {@link Intent}'s {@link Intent#getData() data uri}.
+ * The action returns a media session id and media item id which can be used
+ * to control playback using other remote playback actions.
+ * </p><p>
+ * Once initiated, playback of the specified content will be managed independently
+ * by the destination. The application will receive status updates as the state
+ * of the media item changes.
+ * </p><p>
+ * If the data uri specifies an HTTP or HTTPS scheme, then the destination is
+ * responsible for following HTTP redirects to a reasonable depth of at least 3
+ * levels as might typically be handled by a web browser. If an HTTP error
+ * occurs, then the destination should send a {@link MediaItemStatus status update}
+ * back to the client indicating the {@link MediaItemStatus#PLAYBACK_STATE_ERROR error}
+ * {@link MediaItemStatus#getPlaybackState() playback state}.
+ * </p>
+ *
+ * <h3>One item at a time</h3>
+ * <p>
+ * Each successful play action <em>replaces</em> the previous play action.
+ * If an item is already playing, then it is canceled, the session's playback queue
+ * is cleared and the new item begins playing immediately (regardless of
+ * whether the previously playing item had been paused).
+ * </p><p>
+ * Play is therefore equivalent to {@link #ACTION_STOP stop} followed by an action
+ * to enqueue a new media item to be played immediately.
+ * </p>
+ *
+ * <h3>Sessions</h3>
+ * <p>
+ * This request has the effect of implicitly creating a media session whenever the
+ * application does not specify the {@link #EXTRA_SESSION_ID session id} parameter.
+ * Because there can only be at most one valid session at a time, creating a new session
+ * has the side-effect of invalidating any existing sessions and their media items,
+ * then handling the playback request with a new session.
+ * </p><p>
+ * If the application specifies an invalid session id, then an error is returned.
+ * When this happens, the application should assume that its session
+ * is no longer valid. To obtain a new session, the application may try again
+ * and omit the session id parameter. However, the application should
+ * only retry requests due to an explicit action performed by the user,
+ * such as the user clicking on a "play" button in the UI, since another
+ * application may be trying to take control of the route and the former
+ * application should try to stay out of its way.
+ * </p><p>
+ * For more information on sessions, queues and media items, please refer to the
+ * class documentation.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(optional)</em>: Specifies the session id of the
+ * session to which the playback request belongs. If omitted, a new session
+ * is created implicitly.
+ * <li>{@link #EXTRA_ITEM_CONTENT_POSITION} <em>(optional)</em>: Specifies the initial
+ * content playback position as a long integer number of milliseconds from
+ * the beginning of the content.
+ * <li>{@link #EXTRA_ITEM_METADATA} <em>(optional)</em>: Specifies metadata associated
+ * with the content such as the title of a song.
+ * <li>{@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER} <em>(optional)</em>: Specifies a
+ * {@link PendingIntent} for a broadcast receiver that will receive status updates
+ * about the media item.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(always returned)</em>: Specifies the session id of the
+ * session that was affected by the request. This will be a new session in
+ * the case where no session id was supplied as a parameter.
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * <li>{@link #EXTRA_ITEM_ID} <em>(always returned)</em>: Specifies an opaque string identifier
+ * to use to refer to the media item in subsequent requests such as
+ * {@link #ACTION_GET_STATUS}.
+ * <li>{@link #EXTRA_ITEM_STATUS} <em>(always returned)</em>: Specifies the initial status of
+ * the new media item.
+ * </ul>
+ *
+ * <h3>Status updates</h3>
+ * <p>
+ * If the client supplies an
+ * {@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER item status update receiver}
+ * then the media route provider is responsible for sending status updates to the receiver
+ * when significant media item state changes occur such as when playback starts or
+ * stops. The receiver will not be invoked for content playback position changes.
+ * The application may retrieve the current playback position when necessary
+ * using the {@link #ACTION_GET_STATUS} request.
+ * </p><p>
+ * Refer to {@link MediaItemStatus} for details.
+ * </p>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if a session id was provided but is unknown or
+ * no longer valid, if the item Uri or content type is not supported, or if
+ * any other arguments are invalid.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * <h3>Example</h3>
+ * <pre>
+ * MediaRouter mediaRouter = MediaRouter.getInstance(context);
+ * MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute();
+ * Intent intent = new Intent(MediaControlIntent.ACTION_PLAY);
+ * intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ * intent.setDataAndType("http://example.com/videos/movie.mp4", "video/mp4");
+ * if (route.supportsControlRequest(intent)) {
+ * MediaRouter.ControlRequestCallback callback = new MediaRouter.ControlRequestCallback() {
+ * public void onResult(Bundle data) {
+ * // The request succeeded.
+ * // Playback may be controlled using the returned session and item id.
+ * String sessionId = data.getString(MediaControlIntent.EXTRA_SESSION_ID);
+ * String itemId = data.getString(MediaControlIntent.EXTRA_ITEM_ID);
+ * MediaItemStatus status = MediaItemStatus.fromBundle(data.getBundle(
+ * MediaControlIntent.EXTRA_ITEM_STATUS));
+ * // ...
+ * }
+ *
+ * public void onError(String message, Bundle data) {
+ * // An error occurred!
+ * }
+ * };
+ * route.sendControlRequest(intent, callback);
+ * }</pre>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ * @see #ACTION_SEEK
+ * @see #ACTION_GET_STATUS
+ * @see #ACTION_PAUSE
+ * @see #ACTION_RESUME
+ * @see #ACTION_STOP
+ */
+ public static final String ACTION_PLAY = "android.media.intent.action.PLAY";
+
+ /**
+ * Remote playback media control action: Enqueue media item.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action works just like {@link #ACTION_PLAY play} except that it does
+ * not clear the queue or reset the pause state when it enqueues the
+ * new media item into the session's playback queue. This action only
+ * enqueues a media item with no other side-effects on the queue.
+ * </p><p>
+ * If the queue is currently empty and then the item will play immediately
+ * (assuming the queue is not paused). Otherwise, the item will play
+ * after all earlier items in the queue have finished or been removed.
+ * </p><p>
+ * The enqueue action can be used to create new sessions just like play.
+ * Its parameters and results are also the same. Only the queuing behavior
+ * is different.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ */
+ public static final String ACTION_ENQUEUE = "android.media.intent.action.ENQUEUE";
+
+ /**
+ * Remote playback media control action: Seek media item to a new playback position.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes a remote playback route to modify the current playback position
+ * of the specified media item.
+ * </p><p>
+ * This action only affects the playback position of the media item; not its playback state.
+ * If the playback queue is paused, then seeking sets the position but the item
+ * remains paused. Likewise if the item is playing, then seeking will cause playback
+ * to jump to the new position and continue playing from that point. If the item has
+ * not yet started playing, then the new playback position is remembered by the
+ * queue and used as the item's initial content position when playback eventually begins.
+ * </p><p>
+ * If successful, the media item's playback position is changed.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the session
+ * to which the media item belongs.
+ * <li>{@link #EXTRA_ITEM_ID} <em>(required)</em>: Specifies the media item id of
+ * the media item to seek.
+ * <li>{@link #EXTRA_ITEM_CONTENT_POSITION} <em>(required)</em>: Specifies the new
+ * content position for playback as a long integer number of milliseconds from
+ * the beginning of the content.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * <li>{@link #EXTRA_ITEM_STATUS} <em>(always returned)</em>: Specifies the new status of
+ * the media item.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id or media item id are unknown
+ * or no longer valid, if the content position is invalid, or if the media item
+ * is in a terminal state.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ */
+ public static final String ACTION_SEEK = "android.media.intent.action.SEEK";
+
+ /**
+ * Remote playback media control action: Get media item playback status
+ * and progress information.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action asks a remote playback route to provide updated playback status and progress
+ * information about the specified media item.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the session
+ * to which the media item belongs.
+ * <li>{@link #EXTRA_ITEM_ID} <em>(required)</em>: Specifies the media item id of
+ * the media item to query.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * <li>{@link #EXTRA_ITEM_STATUS} <em>(always returned)</em>: Specifies the current status of
+ * the media item.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id or media item id are unknown
+ * or no longer valid.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ * @see #EXTRA_ITEM_STATUS_UPDATE_RECEIVER
+ */
+ public static final String ACTION_GET_STATUS = "android.media.intent.action.GET_STATUS";
+
+ /**
+ * Remote playback media control action: Remove media item from session's queue.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action asks a remote playback route to remove the specified media item
+ * from the session's playback queue. If the current item is removed, then
+ * playback will proceed to the next media item (assuming the queue has not been
+ * paused).
+ * </p><p>
+ * This action does not affect the pause state of the queue. If the queue was paused
+ * then it remains paused (even if it is now empty) until a resume, stop or play
+ * action is issued that causes the pause state to be cleared.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the session
+ * to which the media item belongs.
+ * <li>{@link #EXTRA_ITEM_ID} <em>(required)</em>: Specifies the media item id of
+ * the media item to remove.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * <li>{@link #EXTRA_ITEM_STATUS} <em>(always returned)</em>: Specifies the new status of
+ * the media item.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id or media item id are unknown
+ * or no longer valid, or if the media item is in a terminal state (and therefore
+ * no longer in the queue).
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ */
+ public static final String ACTION_REMOVE = "android.media.intent.action.REMOVE";
+
+ /* Remote playback actions that affect the whole playback queue. */
+
+ /**
+ * Remote playback media control action: Pause media playback.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes the playback queue of the specified session to be paused.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the session
+ * whose playback queue is to be paused.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id is unknown or no longer valid.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ * @see #ACTION_RESUME
+ */
+ public static final String ACTION_PAUSE = "android.media.intent.action.PAUSE";
+
+ /**
+ * Remote playback media control action: Resume media playback (unpause).
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes the playback queue of the specified session to be resumed.
+ * Reverses the effects of {@link #ACTION_PAUSE}.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the session
+ * whose playback queue is to be resumed.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id is unknown or no longer valid.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ * @see #ACTION_PAUSE
+ */
+ public static final String ACTION_RESUME = "android.media.intent.action.RESUME";
+
+ /**
+ * Remote playback media control action: Stop media playback (clear queue and unpause).
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes a remote playback route to stop playback, cancel and remove
+ * all media items from the session's media item queue and, reset the queue's
+ * pause state.
+ * </p><p>
+ * If successful, the status of all media items in the queue is set to
+ * {@link MediaItemStatus#PLAYBACK_STATE_CANCELED canceled} and a status update is sent
+ * to the appropriate status update receivers indicating the new status of each item.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of
+ * the session whose playback queue is to be stopped (cleared and unpaused).
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id is unknown or no longer valid.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ */
+ public static final String ACTION_STOP = "android.media.intent.action.STOP";
+
+ /**
+ * Remote playback media control action: Start session.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes a remote playback route to invalidate the current session
+ * and start a new session. The new session initially has an empty queue.
+ * </p><p>
+ * If successful, the status of all media items in the previous session's queue is set to
+ * {@link MediaItemStatus#PLAYBACK_STATE_INVALIDATED invalidated} and a status update
+ * is sent to the appropriate status update receivers indicating the new status
+ * of each item. The previous session becomes no longer valid and the new session
+ * takes control of the route.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS_UPDATE_RECEIVER} <em>(optional)</em>: Specifies a
+ * {@link PendingIntent} for a broadcast receiver that will receive status updates
+ * about the media session.
+ * <li>{@link #EXTRA_MESSAGE_RECEIVER} <em>(optional)</em>: Specifies a
+ * {@link PendingIntent} for a broadcast receiver that will receive messages from
+ * the media session.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(always returned)</em>: Specifies the session id of the
+ * session that was started by the request. This will always be a brand new session
+ * distinct from any other previously created sessions.
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(always returned)</em>: Specifies the
+ * status of the media session.
+ * </ul>
+ *
+ * <h3>Status updates</h3>
+ * <p>
+ * If the client supplies a
+ * {@link #EXTRA_SESSION_STATUS_UPDATE_RECEIVER status update receiver}
+ * then the media route provider is responsible for sending status updates to the receiver
+ * when significant media session state changes occur such as when the session's
+ * queue is paused or resumed or when the session is terminated or invalidated.
+ * </p><p>
+ * Refer to {@link MediaSessionStatus} for details.
+ * </p>
+ *
+ * <h3>Custom messages</h3>
+ * <p>
+ * If the client supplies a {@link #EXTRA_MESSAGE_RECEIVER message receiver}
+ * then the media route provider is responsible for sending messages to the receiver
+ * when the session has any messages to send.
+ * </p><p>
+ * Refer to {@link #EXTRA_MESSAGE} for details.
+ * </p>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session could not be created.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ */
+ public static final String ACTION_START_SESSION = "android.media.intent.action.START_SESSION";
+
+ /**
+ * Remote playback media control action: Get media session status information.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action asks a remote playback route to provide updated status information
+ * about the specified media session.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the
+ * session whose status is to be retrieved.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(always returned)</em>: Specifies the
+ * current status of the media session.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id is unknown or no longer valid.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ * @see #EXTRA_SESSION_STATUS_UPDATE_RECEIVER
+ */
+ public static final String ACTION_GET_SESSION_STATUS =
+ "android.media.intent.action.GET_SESSION_STATUS";
+
+ /**
+ * Remote playback media control action: End session.
+ * <p>
+ * Used with routes that support {@link #CATEGORY_REMOTE_PLAYBACK remote playback}
+ * media control.
+ * </p><p>
+ * This action causes a remote playback route to end the specified session.
+ * The session becomes no longer valid and the route ceases to be under control
+ * of the session.
+ * </p><p>
+ * If successful, the status of the session is set to
+ * {@link MediaSessionStatus#SESSION_STATE_ENDED} and a status update is sent to
+ * the session's status update receiver.
+ * </p><p>
+ * Additionally, the status of all media items in the queue is set to
+ * {@link MediaItemStatus#PLAYBACK_STATE_CANCELED canceled} and a status update is sent
+ * to the appropriate status update receivers indicating the new status of each item.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of
+ * the session to end.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(always returned)</em>: Specifies the
+ * status of the media session.
+ * </ul>
+ *
+ * <h3>Errors</h3>
+ * <p>
+ * This action returns an error if the session id is unknown or no longer valid.
+ * In other words, it is an error to attempt to end a session other than the
+ * current session.
+ * </p><ul>
+ * <li>{@link #EXTRA_ERROR_CODE} <em>(optional)</em>: Specifies the cause of the error.
+ * </ul>
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ * @see #CATEGORY_REMOTE_PLAYBACK
+ */
+ public static final String ACTION_END_SESSION = "android.media.intent.action.END_SESSION";
+
+ /**
+ * Custom media control action: Send {@link #EXTRA_MESSAGE}.
+ * <p>
+ * This action asks a route to handle a message described by EXTRA_MESSAGE.
+ * </p>
+ *
+ * <h3>Request parameters</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of the session
+ * to which will handle this message.
+ * <li>{@link #EXTRA_MESSAGE} <em>(required)</em>: Specifies the message to send.
+ * </ul>
+ *
+ * <h3>Result data</h3>
+ * Any messages defined by each media route provider.
+ *
+ * <h3>Errors</h3>
+ * Any error messages defined by each media route provider.
+ *
+ * @see MediaRouter.RouteInfo#sendControlRequest
+ */
+ public static final String ACTION_SEND_MESSAGE = "android.media.intent.action.SEND_MESSAGE";
+
+ /* Extras and related constants. */
+
+ /**
+ * Bundle extra: Media session id.
+ * <p>
+ * An opaque unique identifier that identifies the remote playback media session.
+ * </p><p>
+ * Used with various actions to specify the id of the media session to be controlled.
+ * </p><p>
+ * Included in broadcast intents sent to
+ * {@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER item status update receivers} to identify
+ * the session to which the item in question belongs.
+ * </p><p>
+ * Included in broadcast intents sent to
+ * {@link #EXTRA_SESSION_STATUS_UPDATE_RECEIVER session status update receivers} to identify
+ * the session.
+ * </p><p>
+ * The value is a unique string value generated by the media route provider
+ * to represent one particular media session.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_SEEK
+ * @see #ACTION_GET_STATUS
+ * @see #ACTION_PAUSE
+ * @see #ACTION_RESUME
+ * @see #ACTION_STOP
+ * @see #ACTION_START_SESSION
+ * @see #ACTION_GET_SESSION_STATUS
+ * @see #ACTION_END_SESSION
+ */
+ public static final String EXTRA_SESSION_ID =
+ "android.media.intent.extra.SESSION_ID";
+
+ /**
+ * Bundle extra: Media session status.
+ * <p>
+ * Returned as a result from media session actions such as {@link #ACTION_START_SESSION},
+ * {@link #ACTION_PAUSE}, and {@link #ACTION_GET_SESSION_STATUS}
+ * to describe the status of the specified media session.
+ * </p><p>
+ * Included in broadcast intents sent to
+ * {@link #EXTRA_SESSION_STATUS_UPDATE_RECEIVER session status update receivers} to provide
+ * updated status information.
+ * </p><p>
+ * The value is a {@link android.os.Bundle} of data that can be converted into
+ * a {@link MediaSessionStatus} object using
+ * {@link MediaSessionStatus#fromBundle MediaSessionStatus.fromBundle}.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_SEEK
+ * @see #ACTION_GET_STATUS
+ * @see #ACTION_PAUSE
+ * @see #ACTION_RESUME
+ * @see #ACTION_STOP
+ * @see #ACTION_START_SESSION
+ * @see #ACTION_GET_SESSION_STATUS
+ * @see #ACTION_END_SESSION
+ */
+ public static final String EXTRA_SESSION_STATUS =
+ "android.media.intent.extra.SESSION_STATUS";
+
+ /**
+ * Bundle extra: Media session status update receiver.
+ * <p>
+ * Used with {@link #ACTION_START_SESSION} to specify a {@link PendingIntent} for a
+ * broadcast receiver that will receive status updates about the media session.
+ * </p><p>
+ * Whenever the status of the media session changes, the media route provider will
+ * send a broadcast to the pending intent with extras that identify the session
+ * id and its updated status.
+ * </p><p>
+ * The value is a {@link PendingIntent}.
+ * </p>
+ *
+ * <h3>Broadcast extras</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of
+ * the session.
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(required)</em>: Specifies the status of the
+ * session as a bundle that can be decoded into a {@link MediaSessionStatus} object.
+ * </ul>
+ *
+ * @see #ACTION_START_SESSION
+ */
+ public static final String EXTRA_SESSION_STATUS_UPDATE_RECEIVER =
+ "android.media.intent.extra.SESSION_STATUS_UPDATE_RECEIVER";
+
+ /**
+ * Bundle extra: Media message receiver.
+ * <p>
+ * Used with {@link #ACTION_START_SESSION} to specify a {@link PendingIntent} for a
+ * broadcast receiver that will receive messages from the media session.
+ * </p><p>
+ * When the media session has a message to send, the media route provider will
+ * send a broadcast to the pending intent with extras that identify the session
+ * id and its message.
+ * </p><p>
+ * The value is a {@link PendingIntent}.
+ * </p>
+ *
+ * <h3>Broadcast extras</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of
+ * the session.
+ * <li>{@link #EXTRA_MESSAGE} <em>(required)</em>: Specifies the message from
+ * the session as a bundle object.
+ * </ul>
+ *
+ * @see #ACTION_START_SESSION
+ */
+ public static final String EXTRA_MESSAGE_RECEIVER =
+ "android.media.intent.extra.MESSAGE_RECEIVER";
+
+ /**
+ * Bundle extra: Media item id.
+ * <p>
+ * An opaque unique identifier returned as a result from {@link #ACTION_PLAY} or
+ * {@link #ACTION_ENQUEUE} that represents the media item that was created by the
+ * playback request.
+ * </p><p>
+ * Used with various actions to specify the id of the media item to be controlled.
+ * </p><p>
+ * Included in broadcast intents sent to
+ * {@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER status update receivers} to identify
+ * the item in question.
+ * </p><p>
+ * The value is a unique string value generated by the media route provider
+ * to represent one particular media item.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_ENQUEUE
+ * @see #ACTION_SEEK
+ * @see #ACTION_GET_STATUS
+ */
+ public static final String EXTRA_ITEM_ID =
+ "android.media.intent.extra.ITEM_ID";
+
+ /**
+ * Bundle extra: Media item status.
+ * <p>
+ * Returned as a result from media item actions such as {@link #ACTION_PLAY},
+ * {@link #ACTION_ENQUEUE}, {@link #ACTION_SEEK}, and {@link #ACTION_GET_STATUS}
+ * to describe the status of the specified media item.
+ * </p><p>
+ * Included in broadcast intents sent to
+ * {@link #EXTRA_ITEM_STATUS_UPDATE_RECEIVER item status update receivers} to provide
+ * updated status information.
+ * </p><p>
+ * The value is a {@link android.os.Bundle} of data that can be converted into
+ * a {@link MediaItemStatus} object using
+ * {@link MediaItemStatus#fromBundle MediaItemStatus.fromBundle}.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_ENQUEUE
+ * @see #ACTION_SEEK
+ * @see #ACTION_GET_STATUS
+ */
+ public static final String EXTRA_ITEM_STATUS =
+ "android.media.intent.extra.ITEM_STATUS";
+
+ /**
+ * Long extra: Media item content position.
+ * <p>
+ * Used with {@link #ACTION_PLAY} or {@link #ACTION_ENQUEUE} to specify the
+ * starting playback position.
+ * </p><p>
+ * Used with {@link #ACTION_SEEK} to set a new playback position.
+ * </p><p>
+ * The value is a long integer number of milliseconds from the beginning of the content.
+ * <p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_ENQUEUE
+ * @see #ACTION_SEEK
+ */
+ public static final String EXTRA_ITEM_CONTENT_POSITION =
+ "android.media.intent.extra.ITEM_POSITION";
+
+ /**
+ * Bundle extra: Media item metadata.
+ * <p>
+ * Used with {@link #ACTION_PLAY} or {@link #ACTION_ENQUEUE} to specify metadata
+ * associated with the content of a media item.
+ * </p><p>
+ * The value is a {@link android.os.Bundle} of metadata key-value pairs as defined
+ * in {@link MediaItemMetadata}.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_ENQUEUE
+ */
+ public static final String EXTRA_ITEM_METADATA =
+ "android.media.intent.extra.ITEM_METADATA";
+
+ /**
+ * Bundle extra: HTTP request headers.
+ * <p>
+ * Used with {@link #ACTION_PLAY} or {@link #ACTION_ENQUEUE} to specify HTTP request
+ * headers to be included when fetching to the content indicated by the media
+ * item's data Uri.
+ * </p><p>
+ * This extra may be used to provide authentication tokens and other
+ * parameters to the server separately from the media item's data Uri.
+ * </p><p>
+ * The value is a {@link android.os.Bundle} of string based key-value pairs
+ * that describe the HTTP request headers.
+ * </p>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_ENQUEUE
+ */
+ public static final String EXTRA_ITEM_HTTP_HEADERS =
+ "android.media.intent.extra.HTTP_HEADERS";
+
+ /**
+ * Bundle extra: Media item status update receiver.
+ * <p>
+ * Used with {@link #ACTION_PLAY} or {@link #ACTION_ENQUEUE} to specify
+ * a {@link PendingIntent} for a
+ * broadcast receiver that will receive status updates about a particular
+ * media item.
+ * </p><p>
+ * Whenever the status of the media item changes, the media route provider will
+ * send a broadcast to the pending intent with extras that identify the session
+ * to which the item belongs, the session status, the item's id
+ * and the item's updated status.
+ * </p><p>
+ * The same pending intent and broadcast receiver may be shared by any number of
+ * media items since the broadcast intent includes the media session id
+ * and media item id.
+ * </p><p>
+ * The value is a {@link PendingIntent}.
+ * </p>
+ *
+ * <h3>Broadcast extras</h3>
+ * <ul>
+ * <li>{@link #EXTRA_SESSION_ID} <em>(required)</em>: Specifies the session id of
+ * the session to which the item in question belongs.
+ * <li>{@link #EXTRA_SESSION_STATUS} <em>(optional, old implementations may
+ * omit this key)</em>: Specifies the status of the media session.
+ * <li>{@link #EXTRA_ITEM_ID} <em>(required)</em>: Specifies the media item id of the
+ * media item in question.
+ * <li>{@link #EXTRA_ITEM_STATUS} <em>(required)</em>: Specifies the status of the
+ * item as a bundle that can be decoded into a {@link MediaItemStatus} object.
+ * </ul>
+ *
+ * @see #ACTION_PLAY
+ * @see #ACTION_ENQUEUE
+ */
+ public static final String EXTRA_ITEM_STATUS_UPDATE_RECEIVER =
+ "android.media.intent.extra.ITEM_STATUS_UPDATE_RECEIVER";
+
+ /**
+ * Bundle extra: Message.
+ * <p>
+ * Used with {@link #ACTION_SEND_MESSAGE}, and included in broadcast intents sent to
+ * {@link #EXTRA_MESSAGE_RECEIVER message receivers} to describe a message between a
+ * session and a media route provider.
+ * </p><p>
+ * The value is a {@link android.os.Bundle}.
+ * </p>
+ */
+ public static final String EXTRA_MESSAGE = "android.media.intent.extra.MESSAGE";
+
+ /**
+ * Integer extra: Error code.
+ * <p>
+ * Used with all media control requests to describe the cause of an error.
+ * This extra may be omitted when the error is unknown.
+ * </p><p>
+ * The value is one of: {@link #ERROR_UNKNOWN}, {@link #ERROR_UNSUPPORTED_OPERATION},
+ * {@link #ERROR_INVALID_SESSION_ID}, {@link #ERROR_INVALID_ITEM_ID}.
+ * </p>
+ */
+ public static final String EXTRA_ERROR_CODE = "android.media.intent.extra.ERROR_CODE";
+
+ /**
+ * Error code: An unknown error occurred.
+ *
+ * @see #EXTRA_ERROR_CODE
+ */
+ public static final int ERROR_UNKNOWN = 0;
+
+ /**
+ * Error code: The operation is not supported.
+ *
+ * @see #EXTRA_ERROR_CODE
+ */
+ public static final int ERROR_UNSUPPORTED_OPERATION = 1;
+
+ /**
+ * Error code: The session id specified in the request was invalid.
+ *
+ * @see #EXTRA_ERROR_CODE
+ */
+ public static final int ERROR_INVALID_SESSION_ID = 2;
+
+ /**
+ * Error code: The item id specified in the request was invalid.
+ *
+ * @see #EXTRA_ERROR_CODE
+ */
+ public static final int ERROR_INVALID_ITEM_ID = 3;
+
+ private MediaControlIntent() {
+ }
+}
diff --git a/com/android/support/mediarouter/media/MediaItemMetadata.java b/com/android/support/mediarouter/media/MediaItemMetadata.java
new file mode 100644
index 00000000..d52ddb62
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaItemMetadata.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.os.Bundle;
+
+/**
+ * Constants for specifying metadata about a media item as a {@link Bundle}.
+ * <p>
+ * This class is part of the remote playback protocol described by the
+ * {@link MediaControlIntent MediaControlIntent} class.
+ * </p><p>
+ * Media item metadata is described as a bundle of key/value pairs as defined
+ * in this class. The documentation specifies the type of value associated
+ * with each key.
+ * </p><p>
+ * An application may specify additional custom metadata keys but there is no guarantee
+ * that they will be recognized by the destination.
+ * </p>
+ */
+public final class MediaItemMetadata {
+ /*
+ * Note: MediaMetadataRetriever also defines a collection of metadata keys that can be
+ * retrieved from a content stream although the representation is somewhat different here
+ * since we are sending the data to a remote endpoint.
+ */
+
+ private MediaItemMetadata() {
+ }
+
+ /**
+ * String key: Album artist name.
+ * <p>
+ * The value is a string suitable for display.
+ * </p>
+ */
+ public static final String KEY_ALBUM_ARTIST = "android.media.metadata.ALBUM_ARTIST";
+
+ /**
+ * String key: Album title.
+ * <p>
+ * The value is a string suitable for display.
+ * </p>
+ */
+ public static final String KEY_ALBUM_TITLE = "android.media.metadata.ALBUM_TITLE";
+
+ /**
+ * String key: Artwork Uri.
+ * <p>
+ * The value is a string URI for an image file associated with the media item,
+ * such as album or cover art.
+ * </p>
+ */
+ public static final String KEY_ARTWORK_URI = "android.media.metadata.ARTWORK_URI";
+
+ /**
+ * String key: Artist name.
+ * <p>
+ * The value is a string suitable for display.
+ * </p>
+ */
+ public static final String KEY_ARTIST = "android.media.metadata.ARTIST";
+
+ /**
+ * String key: Author name.
+ * <p>
+ * The value is a string suitable for display.
+ * </p>
+ */
+ public static final String KEY_AUTHOR = "android.media.metadata.AUTHOR";
+
+ /**
+ * String key: Composer name.
+ * <p>
+ * The value is a string suitable for display.
+ * </p>
+ */
+ public static final String KEY_COMPOSER = "android.media.metadata.COMPOSER";
+
+ /**
+ * String key: Track title.
+ * <p>
+ * The value is a string suitable for display.
+ * </p>
+ */
+ public static final String KEY_TITLE = "android.media.metadata.TITLE";
+
+ /**
+ * Integer key: Year of publication.
+ * <p>
+ * The value is an integer year number.
+ * </p>
+ */
+ public static final String KEY_YEAR = "android.media.metadata.YEAR";
+
+ /**
+ * Integer key: Track number (such as a track on a CD).
+ * <p>
+ * The value is a one-based integer track number.
+ * </p>
+ */
+ public static final String KEY_TRACK_NUMBER = "android.media.metadata.TRACK_NUMBER";
+
+ /**
+ * Integer key: Disc number within a collection.
+ * <p>
+ * The value is a one-based integer disc number.
+ * </p>
+ */
+ public static final String KEY_DISC_NUMBER = "android.media.metadata.DISC_NUMBER";
+
+ /**
+ * Long key: Item playback duration in milliseconds.
+ * <p>
+ * The value is a <code>long</code> number of milliseconds.
+ * </p><p>
+ * The duration metadata is only a hint to enable a remote media player to
+ * guess the duration of the content before it actually opens the media stream.
+ * The remote media player should still determine the actual content duration from
+ * the media stream itself independent of the value that may be specified by this key.
+ * </p>
+ */
+ public static final String KEY_DURATION = "android.media.metadata.DURATION";
+}
diff --git a/com/android/support/mediarouter/media/MediaItemStatus.java b/com/android/support/mediarouter/media/MediaItemStatus.java
new file mode 100644
index 00000000..90ea2d5e
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaItemStatus.java
@@ -0,0 +1,392 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.app.PendingIntent;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.util.TimeUtils;
+
+/**
+ * Describes the playback status of a media item.
+ * <p>
+ * This class is part of the remote playback protocol described by the
+ * {@link MediaControlIntent MediaControlIntent} class.
+ * </p><p>
+ * As a media item is played, it transitions through a sequence of states including:
+ * {@link #PLAYBACK_STATE_PENDING pending}, {@link #PLAYBACK_STATE_BUFFERING buffering},
+ * {@link #PLAYBACK_STATE_PLAYING playing}, {@link #PLAYBACK_STATE_PAUSED paused},
+ * {@link #PLAYBACK_STATE_FINISHED finished}, {@link #PLAYBACK_STATE_CANCELED canceled},
+ * {@link #PLAYBACK_STATE_INVALIDATED invalidated}, and
+ * {@link #PLAYBACK_STATE_ERROR error}. Refer to the documentation of each state
+ * for an explanation of its meaning.
+ * </p><p>
+ * While the item is playing, the playback status may also include progress information
+ * about the {@link #getContentPosition content position} and
+ * {@link #getContentDuration content duration} although not all route destinations
+ * will report it.
+ * </p><p>
+ * To monitor playback status, the application should supply a {@link PendingIntent} to use as the
+ * {@link MediaControlIntent#EXTRA_ITEM_STATUS_UPDATE_RECEIVER item status update receiver}
+ * for a given {@link MediaControlIntent#ACTION_PLAY playback request}. Note that
+ * the status update receiver will only be invoked for major status changes such as a
+ * transition from playing to finished.
+ * </p><p class="note">
+ * The status update receiver will not be invoked for minor progress updates such as
+ * changes to playback position or duration. If the application wants to monitor
+ * playback progress, then it must use the
+ * {@link MediaControlIntent#ACTION_GET_STATUS get status request} to poll for changes
+ * periodically and estimate the playback position while playing. Note that there may
+ * be a significant power impact to polling so the application is advised only
+ * to poll when the screen is on and never more than about once every 5 seconds or so.
+ * </p><p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ */
+public final class MediaItemStatus {
+ static final String KEY_TIMESTAMP = "timestamp";
+ static final String KEY_PLAYBACK_STATE = "playbackState";
+ static final String KEY_CONTENT_POSITION = "contentPosition";
+ static final String KEY_CONTENT_DURATION = "contentDuration";
+ static final String KEY_EXTRAS = "extras";
+
+ final Bundle mBundle;
+
+ /**
+ * Playback state: Pending.
+ * <p>
+ * Indicates that the media item has not yet started playback but will be played eventually.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_PENDING = 0;
+
+ /**
+ * Playback state: Playing.
+ * <p>
+ * Indicates that the media item is currently playing.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_PLAYING = 1;
+
+ /**
+ * Playback state: Paused.
+ * <p>
+ * Indicates that playback of the media item has been paused. Playback can be
+ * resumed using the {@link MediaControlIntent#ACTION_RESUME resume} action.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_PAUSED = 2;
+
+ /**
+ * Playback state: Buffering or seeking to a new position.
+ * <p>
+ * Indicates that the media item has been temporarily interrupted
+ * to fetch more content. Playback will continue automatically
+ * when enough content has been buffered.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_BUFFERING = 3;
+
+ /**
+ * Playback state: Finished.
+ * <p>
+ * Indicates that the media item played to the end of the content and finished normally.
+ * </p><p>
+ * A finished media item cannot be resumed. To play the content again, the application
+ * must send a new {@link MediaControlIntent#ACTION_PLAY play} or
+ * {@link MediaControlIntent#ACTION_ENQUEUE enqueue} action.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_FINISHED = 4;
+
+ /**
+ * Playback state: Canceled.
+ * <p>
+ * Indicates that the media item was explicitly removed from the queue by the
+ * application. Items may be canceled and removed from the queue using
+ * the {@link MediaControlIntent#ACTION_REMOVE remove} or
+ * {@link MediaControlIntent#ACTION_STOP stop} action or by issuing
+ * another {@link MediaControlIntent#ACTION_PLAY play} action that has the
+ * side-effect of clearing the queue.
+ * </p><p>
+ * A canceled media item cannot be resumed. To play the content again, the
+ * application must send a new {@link MediaControlIntent#ACTION_PLAY play} or
+ * {@link MediaControlIntent#ACTION_ENQUEUE enqueue} action.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_CANCELED = 5;
+
+ /**
+ * Playback state: Invalidated.
+ * <p>
+ * Indicates that the media item was invalidated permanently and involuntarily.
+ * This state is used to indicate that the media item was invalidated and removed
+ * from the queue because the session to which it belongs was invalidated
+ * (typically by another application taking control of the route).
+ * </p><p>
+ * When invalidation occurs, the application should generally wait for the user
+ * to perform an explicit action, such as clicking on a play button in the UI,
+ * before creating a new media session to avoid unnecessarily interrupting
+ * another application that may have just started using the route.
+ * </p><p>
+ * An invalidated media item cannot be resumed. To play the content again, the application
+ * must send a new {@link MediaControlIntent#ACTION_PLAY play} or
+ * {@link MediaControlIntent#ACTION_ENQUEUE enqueue} action.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_INVALIDATED = 6;
+
+ /**
+ * Playback state: Playback halted or aborted due to an error.
+ * <p>
+ * Examples of errors are no network connectivity when attempting to retrieve content
+ * from a server, or expired user credentials when trying to play subscription-based
+ * content.
+ * </p><p>
+ * A media item in the error state cannot be resumed. To play the content again,
+ * the application must send a new {@link MediaControlIntent#ACTION_PLAY play} or
+ * {@link MediaControlIntent#ACTION_ENQUEUE enqueue} action.
+ * </p>
+ */
+ public static final int PLAYBACK_STATE_ERROR = 7;
+
+ /**
+ * Integer extra: HTTP status code.
+ * <p>
+ * Specifies the HTTP status code that was encountered when the content
+ * was requested after all redirects were followed. This key only needs to
+ * specified when the content uri uses the HTTP or HTTPS scheme and an error
+ * occurred. This key may be omitted if the content was able to be played
+ * successfully; there is no need to report a 200 (OK) status code.
+ * </p><p>
+ * The value is an integer HTTP status code, such as 401 (Unauthorized),
+ * 404 (Not Found), or 500 (Server Error), or 0 if none.
+ * </p>
+ */
+ public static final String EXTRA_HTTP_STATUS_CODE =
+ "android.media.status.extra.HTTP_STATUS_CODE";
+
+ /**
+ * Bundle extra: HTTP response headers.
+ * <p>
+ * Specifies the HTTP response headers that were returned when the content was
+ * requested from the network. The headers may include additional information
+ * about the content or any errors conditions that were encountered while
+ * trying to fetch the content.
+ * </p><p>
+ * The value is a {@link android.os.Bundle} of string based key-value pairs
+ * that describe the HTTP response headers.
+ * </p>
+ */
+ public static final String EXTRA_HTTP_RESPONSE_HEADERS =
+ "android.media.status.extra.HTTP_RESPONSE_HEADERS";
+
+ MediaItemStatus(Bundle bundle) {
+ mBundle = bundle;
+ }
+
+ /**
+ * Gets the timestamp associated with the status information in
+ * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
+ *
+ * @return The status timestamp in the {@link SystemClock#elapsedRealtime()} time base.
+ */
+ public long getTimestamp() {
+ return mBundle.getLong(KEY_TIMESTAMP);
+ }
+
+ /**
+ * Gets the playback state of the media item.
+ *
+ * @return The playback state. One of {@link #PLAYBACK_STATE_PENDING},
+ * {@link #PLAYBACK_STATE_PLAYING}, {@link #PLAYBACK_STATE_PAUSED},
+ * {@link #PLAYBACK_STATE_BUFFERING}, {@link #PLAYBACK_STATE_FINISHED},
+ * {@link #PLAYBACK_STATE_CANCELED}, {@link #PLAYBACK_STATE_INVALIDATED},
+ * or {@link #PLAYBACK_STATE_ERROR}.
+ */
+ public int getPlaybackState() {
+ return mBundle.getInt(KEY_PLAYBACK_STATE, PLAYBACK_STATE_ERROR);
+ }
+
+ /**
+ * Gets the content playback position as a long integer number of milliseconds
+ * from the beginning of the content.
+ *
+ * @return The content playback position in milliseconds, or -1 if unknown.
+ */
+ public long getContentPosition() {
+ return mBundle.getLong(KEY_CONTENT_POSITION, -1);
+ }
+
+ /**
+ * Gets the total duration of the content to be played as a long integer number of
+ * milliseconds.
+ *
+ * @return The content duration in milliseconds, or -1 if unknown.
+ */
+ public long getContentDuration() {
+ return mBundle.getLong(KEY_CONTENT_DURATION, -1);
+ }
+
+ /**
+ * Gets a bundle of extras for this status object.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Bundle getExtras() {
+ return mBundle.getBundle(KEY_EXTRAS);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaItemStatus{ ");
+ result.append("timestamp=");
+ TimeUtils.formatDuration(SystemClock.elapsedRealtime() - getTimestamp(), result);
+ result.append(" ms ago");
+ result.append(", playbackState=").append(playbackStateToString(getPlaybackState()));
+ result.append(", contentPosition=").append(getContentPosition());
+ result.append(", contentDuration=").append(getContentDuration());
+ result.append(", extras=").append(getExtras());
+ result.append(" }");
+ return result.toString();
+ }
+
+ private static String playbackStateToString(int playbackState) {
+ switch (playbackState) {
+ case PLAYBACK_STATE_PENDING:
+ return "pending";
+ case PLAYBACK_STATE_BUFFERING:
+ return "buffering";
+ case PLAYBACK_STATE_PLAYING:
+ return "playing";
+ case PLAYBACK_STATE_PAUSED:
+ return "paused";
+ case PLAYBACK_STATE_FINISHED:
+ return "finished";
+ case PLAYBACK_STATE_CANCELED:
+ return "canceled";
+ case PLAYBACK_STATE_INVALIDATED:
+ return "invalidated";
+ case PLAYBACK_STATE_ERROR:
+ return "error";
+ }
+ return Integer.toString(playbackState);
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaItemStatus fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaItemStatus(bundle) : null;
+ }
+
+ /**
+ * Builder for {@link MediaItemStatus media item status objects}.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+
+ /**
+ * Creates a media item status builder using the current time as the
+ * reference timestamp.
+ *
+ * @param playbackState The item playback state.
+ */
+ public Builder(int playbackState) {
+ mBundle = new Bundle();
+ setTimestamp(SystemClock.elapsedRealtime());
+ setPlaybackState(playbackState);
+ }
+
+ /**
+ * Creates a media item status builder whose initial contents are
+ * copied from an existing status.
+ */
+ public Builder(MediaItemStatus status) {
+ if (status == null) {
+ throw new IllegalArgumentException("status must not be null");
+ }
+
+ mBundle = new Bundle(status.mBundle);
+ }
+
+ /**
+ * Sets the timestamp associated with the status information in
+ * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
+ */
+ public Builder setTimestamp(long elapsedRealtimeTimestamp) {
+ mBundle.putLong(KEY_TIMESTAMP, elapsedRealtimeTimestamp);
+ return this;
+ }
+
+ /**
+ * Sets the playback state of the media item.
+ */
+ public Builder setPlaybackState(int playbackState) {
+ mBundle.putInt(KEY_PLAYBACK_STATE, playbackState);
+ return this;
+ }
+
+ /**
+ * Sets the content playback position as a long integer number of milliseconds
+ * from the beginning of the content.
+ */
+ public Builder setContentPosition(long positionMilliseconds) {
+ mBundle.putLong(KEY_CONTENT_POSITION, positionMilliseconds);
+ return this;
+ }
+
+ /**
+ * Sets the total duration of the content to be played as a long integer number
+ * of milliseconds.
+ */
+ public Builder setContentDuration(long durationMilliseconds) {
+ mBundle.putLong(KEY_CONTENT_DURATION, durationMilliseconds);
+ return this;
+ }
+
+ /**
+ * Sets a bundle of extras for this status object.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Builder setExtras(Bundle extras) {
+ mBundle.putBundle(KEY_EXTRAS, extras);
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaItemStatus media item status object}.
+ */
+ public MediaItemStatus build() {
+ return new MediaItemStatus(mBundle);
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/MediaRouteDescriptor.java b/com/android/support/mediarouter/media/MediaRouteDescriptor.java
new file mode 100644
index 00000000..6bc84fc7
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaRouteDescriptor.java
@@ -0,0 +1,693 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Describes the properties of a route.
+ * <p>
+ * Each route is uniquely identified by an opaque id string. This token
+ * may take any form as long as it is unique within the media route provider.
+ * </p><p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ */
+public final class MediaRouteDescriptor {
+ static final String KEY_ID = "id";
+ static final String KEY_GROUP_MEMBER_IDS = "groupMemberIds";
+ static final String KEY_NAME = "name";
+ static final String KEY_DESCRIPTION = "status";
+ static final String KEY_ICON_URI = "iconUri";
+ static final String KEY_ENABLED = "enabled";
+ static final String KEY_CONNECTING = "connecting";
+ static final String KEY_CONNECTION_STATE = "connectionState";
+ static final String KEY_CONTROL_FILTERS = "controlFilters";
+ static final String KEY_PLAYBACK_TYPE = "playbackType";
+ static final String KEY_PLAYBACK_STREAM = "playbackStream";
+ static final String KEY_DEVICE_TYPE = "deviceType";
+ static final String KEY_VOLUME = "volume";
+ static final String KEY_VOLUME_MAX = "volumeMax";
+ static final String KEY_VOLUME_HANDLING = "volumeHandling";
+ static final String KEY_PRESENTATION_DISPLAY_ID = "presentationDisplayId";
+ static final String KEY_EXTRAS = "extras";
+ static final String KEY_CAN_DISCONNECT = "canDisconnect";
+ static final String KEY_SETTINGS_INTENT = "settingsIntent";
+ static final String KEY_MIN_CLIENT_VERSION = "minClientVersion";
+ static final String KEY_MAX_CLIENT_VERSION = "maxClientVersion";
+
+ final Bundle mBundle;
+ List<IntentFilter> mControlFilters;
+
+ MediaRouteDescriptor(Bundle bundle, List<IntentFilter> controlFilters) {
+ mBundle = bundle;
+ mControlFilters = controlFilters;
+ }
+
+ /**
+ * Gets the unique id of the route.
+ * <p>
+ * The route id associated with a route descriptor functions as a stable
+ * identifier for the route and must be unique among all routes offered
+ * by the provider.
+ * </p>
+ */
+ public String getId() {
+ return mBundle.getString(KEY_ID);
+ }
+
+ /**
+ * Gets the group member ids of the route.
+ * <p>
+ * A route descriptor that has one or more group member route ids
+ * represents a route group. A member route may belong to another group.
+ * </p>
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public List<String> getGroupMemberIds() {
+ return mBundle.getStringArrayList(KEY_GROUP_MEMBER_IDS);
+ }
+
+ /**
+ * Gets the user-visible name of the route.
+ * <p>
+ * The route name identifies the destination represented by the route.
+ * It may be a user-supplied name, an alias, or device serial number.
+ * </p>
+ */
+ public String getName() {
+ return mBundle.getString(KEY_NAME);
+ }
+
+ /**
+ * Gets the user-visible description of the route.
+ * <p>
+ * The route description describes the kind of destination represented by the route.
+ * It may be a user-supplied string, a model number or brand of device.
+ * </p>
+ */
+ public String getDescription() {
+ return mBundle.getString(KEY_DESCRIPTION);
+ }
+
+ /**
+ * Gets the URI of the icon representing this route.
+ * <p>
+ * This icon will be used in picker UIs if available.
+ * </p>
+ */
+ public Uri getIconUri() {
+ String iconUri = mBundle.getString(KEY_ICON_URI);
+ return iconUri == null ? null : Uri.parse(iconUri);
+ }
+
+ /**
+ * Gets whether the route is enabled.
+ */
+ public boolean isEnabled() {
+ return mBundle.getBoolean(KEY_ENABLED, true);
+ }
+
+ /**
+ * Gets whether the route is connecting.
+ * @deprecated Use {@link #getConnectionState} instead
+ */
+ @Deprecated
+ public boolean isConnecting() {
+ return mBundle.getBoolean(KEY_CONNECTING, false);
+ }
+
+ /**
+ * Gets the connection state of the route.
+ *
+ * @return The connection state of this route:
+ * {@link MediaRouter.RouteInfo#CONNECTION_STATE_DISCONNECTED},
+ * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTING}, or
+ * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTED}.
+ */
+ public int getConnectionState() {
+ return mBundle.getInt(KEY_CONNECTION_STATE,
+ MediaRouter.RouteInfo.CONNECTION_STATE_DISCONNECTED);
+ }
+
+ /**
+ * Gets whether the route can be disconnected without stopping playback.
+ * <p>
+ * The route can normally be disconnected without stopping playback when
+ * the destination device on the route is connected to two or more source
+ * devices. The route provider should update the route immediately when the
+ * number of connected devices changes.
+ * </p><p>
+ * To specify that the route should disconnect without stopping use
+ * {@link MediaRouter#unselect(int)} with
+ * {@link MediaRouter#UNSELECT_REASON_DISCONNECTED}.
+ * </p>
+ */
+ public boolean canDisconnectAndKeepPlaying() {
+ return mBundle.getBoolean(KEY_CAN_DISCONNECT, false);
+ }
+
+ /**
+ * Gets an {@link IntentSender} for starting a settings activity for this
+ * route. The activity may have specific route settings or general settings
+ * for the connected device or route provider.
+ *
+ * @return An {@link IntentSender} to start a settings activity.
+ */
+ public IntentSender getSettingsActivity() {
+ return mBundle.getParcelable(KEY_SETTINGS_INTENT);
+ }
+
+ /**
+ * Gets the route's {@link MediaControlIntent media control intent} filters.
+ */
+ public List<IntentFilter> getControlFilters() {
+ ensureControlFilters();
+ return mControlFilters;
+ }
+
+ void ensureControlFilters() {
+ if (mControlFilters == null) {
+ mControlFilters = mBundle.<IntentFilter>getParcelableArrayList(KEY_CONTROL_FILTERS);
+ if (mControlFilters == null) {
+ mControlFilters = Collections.<IntentFilter>emptyList();
+ }
+ }
+ }
+
+ /**
+ * Gets the type of playback associated with this route.
+ *
+ * @return The type of playback associated with this route:
+ * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_LOCAL} or
+ * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_REMOTE}.
+ */
+ public int getPlaybackType() {
+ return mBundle.getInt(KEY_PLAYBACK_TYPE, MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE);
+ }
+
+ /**
+ * Gets the route's playback stream.
+ */
+ public int getPlaybackStream() {
+ return mBundle.getInt(KEY_PLAYBACK_STREAM, -1);
+ }
+
+ /**
+ * Gets the type of the receiver device associated with this route.
+ *
+ * @return The type of the receiver device associated with this route:
+ * {@link MediaRouter.RouteInfo#DEVICE_TYPE_TV} or
+ * {@link MediaRouter.RouteInfo#DEVICE_TYPE_SPEAKER}.
+ */
+ public int getDeviceType() {
+ return mBundle.getInt(KEY_DEVICE_TYPE);
+ }
+
+ /**
+ * Gets the route's current volume, or 0 if unknown.
+ */
+ public int getVolume() {
+ return mBundle.getInt(KEY_VOLUME);
+ }
+
+ /**
+ * Gets the route's maximum volume, or 0 if unknown.
+ */
+ public int getVolumeMax() {
+ return mBundle.getInt(KEY_VOLUME_MAX);
+ }
+
+ /**
+ * Gets information about how volume is handled on the route.
+ *
+ * @return How volume is handled on the route:
+ * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_FIXED} or
+ * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_VARIABLE}.
+ */
+ public int getVolumeHandling() {
+ return mBundle.getInt(KEY_VOLUME_HANDLING,
+ MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED);
+ }
+
+ /**
+ * Gets the route's presentation display id, or -1 if none.
+ */
+ public int getPresentationDisplayId() {
+ return mBundle.getInt(
+ KEY_PRESENTATION_DISPLAY_ID, MediaRouter.RouteInfo.PRESENTATION_DISPLAY_ID_NONE);
+ }
+
+ /**
+ * Gets a bundle of extras for this route descriptor.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Bundle getExtras() {
+ return mBundle.getBundle(KEY_EXTRAS);
+ }
+
+ /**
+ * Gets the minimum client version required for this route.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public int getMinClientVersion() {
+ return mBundle.getInt(KEY_MIN_CLIENT_VERSION,
+ MediaRouteProviderProtocol.CLIENT_VERSION_START);
+ }
+
+ /**
+ * Gets the maximum client version required for this route.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public int getMaxClientVersion() {
+ return mBundle.getInt(KEY_MAX_CLIENT_VERSION, Integer.MAX_VALUE);
+ }
+
+ /**
+ * Returns true if the route descriptor has all of the required fields.
+ */
+ public boolean isValid() {
+ ensureControlFilters();
+ if (TextUtils.isEmpty(getId())
+ || TextUtils.isEmpty(getName())
+ || mControlFilters.contains(null)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaRouteDescriptor{ ");
+ result.append("id=").append(getId());
+ result.append(", groupMemberIds=").append(getGroupMemberIds());
+ result.append(", name=").append(getName());
+ result.append(", description=").append(getDescription());
+ result.append(", iconUri=").append(getIconUri());
+ result.append(", isEnabled=").append(isEnabled());
+ result.append(", isConnecting=").append(isConnecting());
+ result.append(", connectionState=").append(getConnectionState());
+ result.append(", controlFilters=").append(Arrays.toString(getControlFilters().toArray()));
+ result.append(", playbackType=").append(getPlaybackType());
+ result.append(", playbackStream=").append(getPlaybackStream());
+ result.append(", deviceType=").append(getDeviceType());
+ result.append(", volume=").append(getVolume());
+ result.append(", volumeMax=").append(getVolumeMax());
+ result.append(", volumeHandling=").append(getVolumeHandling());
+ result.append(", presentationDisplayId=").append(getPresentationDisplayId());
+ result.append(", extras=").append(getExtras());
+ result.append(", isValid=").append(isValid());
+ result.append(", minClientVersion=").append(getMinClientVersion());
+ result.append(", maxClientVersion=").append(getMaxClientVersion());
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaRouteDescriptor fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaRouteDescriptor(bundle, null) : null;
+ }
+
+ /**
+ * Builder for {@link MediaRouteDescriptor media route descriptors}.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+ private ArrayList<String> mGroupMemberIds;
+ private ArrayList<IntentFilter> mControlFilters;
+
+ /**
+ * Creates a media route descriptor builder.
+ *
+ * @param id The unique id of the route.
+ * @param name The user-visible name of the route.
+ */
+ public Builder(String id, String name) {
+ mBundle = new Bundle();
+ setId(id);
+ setName(name);
+ }
+
+ /**
+ * Creates a media route descriptor builder whose initial contents are
+ * copied from an existing descriptor.
+ */
+ public Builder(MediaRouteDescriptor descriptor) {
+ if (descriptor == null) {
+ throw new IllegalArgumentException("descriptor must not be null");
+ }
+
+ mBundle = new Bundle(descriptor.mBundle);
+
+ descriptor.ensureControlFilters();
+ if (!descriptor.mControlFilters.isEmpty()) {
+ mControlFilters = new ArrayList<IntentFilter>(descriptor.mControlFilters);
+ }
+ }
+
+ /**
+ * Sets the unique id of the route.
+ * <p>
+ * The route id associated with a route descriptor functions as a stable
+ * identifier for the route and must be unique among all routes offered
+ * by the provider.
+ * </p>
+ */
+ public Builder setId(String id) {
+ mBundle.putString(KEY_ID, id);
+ return this;
+ }
+
+ /**
+ * Adds a group member id of the route.
+ * <p>
+ * A route descriptor that has one or more group member route ids
+ * represents a route group. A member route may belong to another group.
+ * </p>
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public Builder addGroupMemberId(String groupMemberId) {
+ if (TextUtils.isEmpty(groupMemberId)) {
+ throw new IllegalArgumentException("groupMemberId must not be empty");
+ }
+
+ if (mGroupMemberIds == null) {
+ mGroupMemberIds = new ArrayList<>();
+ }
+ if (!mGroupMemberIds.contains(groupMemberId)) {
+ mGroupMemberIds.add(groupMemberId);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a list of group member ids of the route.
+ * <p>
+ * A route descriptor that has one or more group member route ids
+ * represents a route group. A member route may belong to another group.
+ * </p>
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public Builder addGroupMemberIds(Collection<String> groupMemberIds) {
+ if (groupMemberIds == null) {
+ throw new IllegalArgumentException("groupMemberIds must not be null");
+ }
+
+ if (!groupMemberIds.isEmpty()) {
+ for (String groupMemberId : groupMemberIds) {
+ addGroupMemberId(groupMemberId);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets the user-visible name of the route.
+ * <p>
+ * The route name identifies the destination represented by the route.
+ * It may be a user-supplied name, an alias, or device serial number.
+ * </p>
+ */
+ public Builder setName(String name) {
+ mBundle.putString(KEY_NAME, name);
+ return this;
+ }
+
+ /**
+ * Sets the user-visible description of the route.
+ * <p>
+ * The route description describes the kind of destination represented by the route.
+ * It may be a user-supplied string, a model number or brand of device.
+ * </p>
+ */
+ public Builder setDescription(String description) {
+ mBundle.putString(KEY_DESCRIPTION, description);
+ return this;
+ }
+
+ /**
+ * Sets the URI of the icon representing this route.
+ * <p>
+ * This icon will be used in picker UIs if available.
+ * </p><p>
+ * The URI must be one of the following formats:
+ * <ul>
+ * <li>content ({@link android.content.ContentResolver#SCHEME_CONTENT})</li>
+ * <li>android.resource ({@link android.content.ContentResolver#SCHEME_ANDROID_RESOURCE})
+ * </li>
+ * <li>file ({@link android.content.ContentResolver#SCHEME_FILE})</li>
+ * </ul>
+ * </p>
+ */
+ public Builder setIconUri(Uri iconUri) {
+ if (iconUri == null) {
+ throw new IllegalArgumentException("iconUri must not be null");
+ }
+ mBundle.putString(KEY_ICON_URI, iconUri.toString());
+ return this;
+ }
+
+ /**
+ * Sets whether the route is enabled.
+ * <p>
+ * Disabled routes represent routes that a route provider knows about, such as paired
+ * Wifi Display receivers, but that are not currently available for use.
+ * </p>
+ */
+ public Builder setEnabled(boolean enabled) {
+ mBundle.putBoolean(KEY_ENABLED, enabled);
+ return this;
+ }
+
+ /**
+ * Sets whether the route is in the process of connecting and is not yet
+ * ready for use.
+ * @deprecated Use {@link #setConnectionState} instead.
+ */
+ @Deprecated
+ public Builder setConnecting(boolean connecting) {
+ mBundle.putBoolean(KEY_CONNECTING, connecting);
+ return this;
+ }
+
+ /**
+ * Sets the route's connection state.
+ *
+ * @param connectionState The connection state of the route:
+ * {@link MediaRouter.RouteInfo#CONNECTION_STATE_DISCONNECTED},
+ * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTING}, or
+ * {@link MediaRouter.RouteInfo#CONNECTION_STATE_CONNECTED}.
+ */
+ public Builder setConnectionState(int connectionState) {
+ mBundle.putInt(KEY_CONNECTION_STATE, connectionState);
+ return this;
+ }
+
+ /**
+ * Sets whether the route can be disconnected without stopping playback.
+ */
+ public Builder setCanDisconnect(boolean canDisconnect) {
+ mBundle.putBoolean(KEY_CAN_DISCONNECT, canDisconnect);
+ return this;
+ }
+
+ /**
+ * Sets an intent sender for launching the settings activity for this
+ * route.
+ */
+ public Builder setSettingsActivity(IntentSender is) {
+ mBundle.putParcelable(KEY_SETTINGS_INTENT, is);
+ return this;
+ }
+
+ /**
+ * Adds a {@link MediaControlIntent media control intent} filter for the route.
+ */
+ public Builder addControlFilter(IntentFilter filter) {
+ if (filter == null) {
+ throw new IllegalArgumentException("filter must not be null");
+ }
+
+ if (mControlFilters == null) {
+ mControlFilters = new ArrayList<IntentFilter>();
+ }
+ if (!mControlFilters.contains(filter)) {
+ mControlFilters.add(filter);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a list of {@link MediaControlIntent media control intent} filters for the route.
+ */
+ public Builder addControlFilters(Collection<IntentFilter> filters) {
+ if (filters == null) {
+ throw new IllegalArgumentException("filters must not be null");
+ }
+
+ if (!filters.isEmpty()) {
+ for (IntentFilter filter : filters) {
+ addControlFilter(filter);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets the route's playback type.
+ *
+ * @param playbackType The playback type of the route:
+ * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_LOCAL} or
+ * {@link MediaRouter.RouteInfo#PLAYBACK_TYPE_REMOTE}.
+ */
+ public Builder setPlaybackType(int playbackType) {
+ mBundle.putInt(KEY_PLAYBACK_TYPE, playbackType);
+ return this;
+ }
+
+ /**
+ * Sets the route's playback stream.
+ */
+ public Builder setPlaybackStream(int playbackStream) {
+ mBundle.putInt(KEY_PLAYBACK_STREAM, playbackStream);
+ return this;
+ }
+
+ /**
+ * Sets the route's receiver device type.
+ *
+ * @param deviceType The receive device type of the route:
+ * {@link MediaRouter.RouteInfo#DEVICE_TYPE_TV} or
+ * {@link MediaRouter.RouteInfo#DEVICE_TYPE_SPEAKER}.
+ */
+ public Builder setDeviceType(int deviceType) {
+ mBundle.putInt(KEY_DEVICE_TYPE, deviceType);
+ return this;
+ }
+
+ /**
+ * Sets the route's current volume, or 0 if unknown.
+ */
+ public Builder setVolume(int volume) {
+ mBundle.putInt(KEY_VOLUME, volume);
+ return this;
+ }
+
+ /**
+ * Sets the route's maximum volume, or 0 if unknown.
+ */
+ public Builder setVolumeMax(int volumeMax) {
+ mBundle.putInt(KEY_VOLUME_MAX, volumeMax);
+ return this;
+ }
+
+ /**
+ * Sets the route's volume handling.
+ *
+ * @param volumeHandling how volume is handled on the route:
+ * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_FIXED} or
+ * {@link MediaRouter.RouteInfo#PLAYBACK_VOLUME_VARIABLE}.
+ */
+ public Builder setVolumeHandling(int volumeHandling) {
+ mBundle.putInt(KEY_VOLUME_HANDLING, volumeHandling);
+ return this;
+ }
+
+ /**
+ * Sets the route's presentation display id, or -1 if none.
+ */
+ public Builder setPresentationDisplayId(int presentationDisplayId) {
+ mBundle.putInt(KEY_PRESENTATION_DISPLAY_ID, presentationDisplayId);
+ return this;
+ }
+
+ /**
+ * Sets a bundle of extras for this route descriptor.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Builder setExtras(Bundle extras) {
+ mBundle.putBundle(KEY_EXTRAS, extras);
+ return this;
+ }
+
+ /**
+ * Sets the route's minimum client version.
+ * A router whose version is lower than this will not be able to connect to this route.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public Builder setMinClientVersion(int minVersion) {
+ mBundle.putInt(KEY_MIN_CLIENT_VERSION, minVersion);
+ return this;
+ }
+
+ /**
+ * Sets the route's maximum client version.
+ * A router whose version is higher than this will not be able to connect to this route.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public Builder setMaxClientVersion(int maxVersion) {
+ mBundle.putInt(KEY_MAX_CLIENT_VERSION, maxVersion);
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaRouteDescriptor media route descriptor}.
+ */
+ public MediaRouteDescriptor build() {
+ if (mControlFilters != null) {
+ mBundle.putParcelableArrayList(KEY_CONTROL_FILTERS, mControlFilters);
+ }
+ if (mGroupMemberIds != null) {
+ mBundle.putStringArrayList(KEY_GROUP_MEMBER_IDS, mGroupMemberIds);
+ }
+ return new MediaRouteDescriptor(mBundle, mControlFilters);
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/MediaRouteDiscoveryRequest.java b/com/android/support/mediarouter/media/MediaRouteDiscoveryRequest.java
new file mode 100644
index 00000000..039627fc
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaRouteDiscoveryRequest.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.os.Bundle;
+
+/**
+ * Describes the kinds of routes that the media router would like to discover
+ * and whether to perform active scanning.
+ * <p>
+ * This object is immutable once created.
+ * </p>
+ */
+public final class MediaRouteDiscoveryRequest {
+ private static final String KEY_SELECTOR = "selector";
+ private static final String KEY_ACTIVE_SCAN = "activeScan";
+
+ private final Bundle mBundle;
+ private MediaRouteSelector mSelector;
+
+ /**
+ * Creates a media route discovery request.
+ *
+ * @param selector The route selector that specifies the kinds of routes to discover.
+ * @param activeScan True if active scanning should be performed.
+ */
+ public MediaRouteDiscoveryRequest(MediaRouteSelector selector, boolean activeScan) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ mBundle = new Bundle();
+ mSelector = selector;
+ mBundle.putBundle(KEY_SELECTOR, selector.asBundle());
+ mBundle.putBoolean(KEY_ACTIVE_SCAN, activeScan);
+ }
+
+ private MediaRouteDiscoveryRequest(Bundle bundle) {
+ mBundle = bundle;
+ }
+
+ /**
+ * Gets the route selector that specifies the kinds of routes to discover.
+ */
+ public MediaRouteSelector getSelector() {
+ ensureSelector();
+ return mSelector;
+ }
+
+ private void ensureSelector() {
+ if (mSelector == null) {
+ mSelector = MediaRouteSelector.fromBundle(mBundle.getBundle(KEY_SELECTOR));
+ if (mSelector == null) {
+ mSelector = MediaRouteSelector.EMPTY;
+ }
+ }
+ }
+
+ /**
+ * Returns true if active scanning should be performed.
+ *
+ * @see MediaRouter#CALLBACK_FLAG_PERFORM_ACTIVE_SCAN
+ */
+ public boolean isActiveScan() {
+ return mBundle.getBoolean(KEY_ACTIVE_SCAN);
+ }
+
+ /**
+ * Returns true if the discovery request has all of the required fields.
+ */
+ public boolean isValid() {
+ ensureSelector();
+ return mSelector.isValid();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof MediaRouteDiscoveryRequest) {
+ MediaRouteDiscoveryRequest other = (MediaRouteDiscoveryRequest)o;
+ return getSelector().equals(other.getSelector())
+ && isActiveScan() == other.isActiveScan();
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ return getSelector().hashCode() ^ (isActiveScan() ? 1 : 0);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("DiscoveryRequest{ selector=").append(getSelector());
+ result.append(", activeScan=").append(isActiveScan());
+ result.append(", isValid=").append(isValid());
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaRouteDiscoveryRequest fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaRouteDiscoveryRequest(bundle) : null;
+ }
+}
diff --git a/com/android/support/mediarouter/media/MediaRouteProvider.java b/com/android/support/mediarouter/media/MediaRouteProvider.java
new file mode 100644
index 00000000..91a2e1ac
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaRouteProvider.java
@@ -0,0 +1,447 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.Message;
+import android.support.v4.util.ObjectsCompat;
+
+import com.android.support.mediarouter.media.MediaRouter.ControlRequestCallback;
+
+/**
+ * Media route providers are used to publish additional media routes for
+ * use within an application. Media route providers may also be declared
+ * as a service to publish additional media routes to all applications
+ * in the system.
+ * <p>
+ * The purpose of a media route provider is to discover media routes that satisfy
+ * the criteria specified by the current {@link MediaRouteDiscoveryRequest} and publish a
+ * {@link MediaRouteProviderDescriptor} with information about each route by calling
+ * {@link #setDescriptor} to notify the currently registered {@link Callback}.
+ * </p><p>
+ * The provider should watch for changes to the discovery request by implementing
+ * {@link #onDiscoveryRequestChanged} and updating the set of routes that it is
+ * attempting to discover. It should also handle route control requests such
+ * as volume changes or {@link MediaControlIntent media control intents}
+ * by implementing {@link #onCreateRouteController} to return a {@link RouteController}
+ * for a particular route.
+ * </p><p>
+ * A media route provider may be used privately within the scope of a single
+ * application process by calling {@link MediaRouter#addProvider MediaRouter.addProvider}
+ * to add it to the local {@link MediaRouter}. A media route provider may also be made
+ * available globally to all applications by registering a {@link MediaRouteProviderService}
+ * in the provider's manifest. When the media route provider is registered
+ * as a service, all applications that use the media router API will be able to
+ * discover and used the provider's routes without having to install anything else.
+ * </p><p>
+ * This object must only be accessed on the main thread.
+ * </p>
+ */
+public abstract class MediaRouteProvider {
+ static final int MSG_DELIVER_DESCRIPTOR_CHANGED = 1;
+ static final int MSG_DELIVER_DISCOVERY_REQUEST_CHANGED = 2;
+
+ private final Context mContext;
+ private final ProviderMetadata mMetadata;
+ private final ProviderHandler mHandler = new ProviderHandler();
+
+ private Callback mCallback;
+
+ private MediaRouteDiscoveryRequest mDiscoveryRequest;
+ private boolean mPendingDiscoveryRequestChange;
+
+ private MediaRouteProviderDescriptor mDescriptor;
+ private boolean mPendingDescriptorChange;
+
+ /**
+ * Creates a media route provider.
+ *
+ * @param context The context.
+ */
+ public MediaRouteProvider(@NonNull Context context) {
+ this(context, null);
+ }
+
+ MediaRouteProvider(Context context, ProviderMetadata metadata) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+
+ mContext = context;
+ if (metadata == null) {
+ mMetadata = new ProviderMetadata(new ComponentName(context, getClass()));
+ } else {
+ mMetadata = metadata;
+ }
+ }
+
+ /**
+ * Gets the context of the media route provider.
+ */
+ public final Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Gets the provider's handler which is associated with the main thread.
+ */
+ public final Handler getHandler() {
+ return mHandler;
+ }
+
+ /**
+ * Gets some metadata about the provider's implementation.
+ */
+ public final ProviderMetadata getMetadata() {
+ return mMetadata;
+ }
+
+ /**
+ * Sets a callback to invoke when the provider's descriptor changes.
+ *
+ * @param callback The callback to use, or null if none.
+ */
+ public final void setCallback(@Nullable Callback callback) {
+ MediaRouter.checkCallingThread();
+ mCallback = callback;
+ }
+
+ /**
+ * Gets the current discovery request which informs the provider about the
+ * kinds of routes to discover and whether to perform active scanning.
+ *
+ * @return The current discovery request, or null if no discovery is needed at this time.
+ *
+ * @see #onDiscoveryRequestChanged
+ */
+ @Nullable
+ public final MediaRouteDiscoveryRequest getDiscoveryRequest() {
+ return mDiscoveryRequest;
+ }
+
+ /**
+ * Sets a discovery request to inform the provider about the kinds of
+ * routes that its clients would like to discover and whether to perform active scanning.
+ *
+ * @param request The discovery request, or null if no discovery is needed at this time.
+ *
+ * @see #onDiscoveryRequestChanged
+ */
+ public final void setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
+ MediaRouter.checkCallingThread();
+
+ if (ObjectsCompat.equals(mDiscoveryRequest, request)) {
+ return;
+ }
+
+ mDiscoveryRequest = request;
+ if (!mPendingDiscoveryRequestChange) {
+ mPendingDiscoveryRequestChange = true;
+ mHandler.sendEmptyMessage(MSG_DELIVER_DISCOVERY_REQUEST_CHANGED);
+ }
+ }
+
+ void deliverDiscoveryRequestChanged() {
+ mPendingDiscoveryRequestChange = false;
+ onDiscoveryRequestChanged(mDiscoveryRequest);
+ }
+
+ /**
+ * Called by the media router when the {@link MediaRouteDiscoveryRequest discovery request}
+ * has changed.
+ * <p>
+ * Whenever an applications calls {@link MediaRouter#addCallback} to register
+ * a callback, it also provides a selector to specify the kinds of routes that
+ * it is interested in. The media router combines all of these selectors together
+ * to generate a {@link MediaRouteDiscoveryRequest} and notifies each provider when a change
+ * occurs by calling {@link #setDiscoveryRequest} which posts a message to invoke
+ * this method asynchronously.
+ * </p><p>
+ * The provider should examine the {@link MediaControlIntent media control categories}
+ * in the discovery request's {@link MediaRouteSelector selector} to determine what
+ * kinds of routes it should try to discover and whether it should perform active
+ * or passive scans. In many cases, the provider may be able to save power by
+ * determining that the selector does not contain any categories that it supports
+ * and it can therefore avoid performing any scans at all.
+ * </p>
+ *
+ * @param request The new discovery request, or null if no discovery is needed at this time.
+ *
+ * @see MediaRouter#addCallback
+ */
+ public void onDiscoveryRequestChanged(@Nullable MediaRouteDiscoveryRequest request) {
+ }
+
+ /**
+ * Gets the provider's descriptor.
+ * <p>
+ * The descriptor describes the state of the media route provider and
+ * the routes that it publishes. Watch for changes to the descriptor
+ * by registering a {@link Callback callback} with {@link #setCallback}.
+ * </p>
+ *
+ * @return The media route provider descriptor, or null if none.
+ *
+ * @see Callback#onDescriptorChanged
+ */
+ @Nullable
+ public final MediaRouteProviderDescriptor getDescriptor() {
+ return mDescriptor;
+ }
+
+ /**
+ * Sets the provider's descriptor.
+ * <p>
+ * The provider must call this method to notify the currently registered
+ * {@link Callback callback} about the change to the provider's descriptor.
+ * </p>
+ *
+ * @param descriptor The updated route provider descriptor, or null if none.
+ *
+ * @see Callback#onDescriptorChanged
+ */
+ public final void setDescriptor(@Nullable MediaRouteProviderDescriptor descriptor) {
+ MediaRouter.checkCallingThread();
+
+ if (mDescriptor != descriptor) {
+ mDescriptor = descriptor;
+ if (!mPendingDescriptorChange) {
+ mPendingDescriptorChange = true;
+ mHandler.sendEmptyMessage(MSG_DELIVER_DESCRIPTOR_CHANGED);
+ }
+ }
+ }
+
+ void deliverDescriptorChanged() {
+ mPendingDescriptorChange = false;
+
+ if (mCallback != null) {
+ mCallback.onDescriptorChanged(this, mDescriptor);
+ }
+ }
+
+ /**
+ * Called by the media router to obtain a route controller for a particular route.
+ * <p>
+ * The media router will invoke the {@link RouteController#onRelease} method of the route
+ * controller when it is no longer needed to allow it to free its resources.
+ * </p>
+ *
+ * @param routeId The unique id of the route.
+ * @return The route controller. Returns null if there is no such route or if the route
+ * cannot be controlled using the route controller interface.
+ */
+ @Nullable
+ public RouteController onCreateRouteController(@NonNull String routeId) {
+ if (routeId == null) {
+ throw new IllegalArgumentException("routeId cannot be null");
+ }
+ return null;
+ }
+
+ /**
+ * Called by the media router to obtain a route controller for a particular route which is a
+ * member of {@link MediaRouter.RouteGroup}.
+ * <p>
+ * The media router will invoke the {@link RouteController#onRelease} method of the route
+ * controller when it is no longer needed to allow it to free its resources.
+ * </p>
+ *
+ * @param routeId The unique id of the member route.
+ * @param routeGroupId The unique id of the route group.
+ * @return The route controller. Returns null if there is no such route or if the route
+ * cannot be controlled using the route controller interface.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ @Nullable
+ public RouteController onCreateRouteController(@NonNull String routeId,
+ @NonNull String routeGroupId) {
+ if (routeId == null) {
+ throw new IllegalArgumentException("routeId cannot be null");
+ }
+ if (routeGroupId == null) {
+ throw new IllegalArgumentException("routeGroupId cannot be null");
+ }
+ return onCreateRouteController(routeId);
+ }
+
+ /**
+ * Describes properties of the route provider's implementation.
+ * <p>
+ * This object is immutable once created.
+ * </p>
+ */
+ public static final class ProviderMetadata {
+ private final ComponentName mComponentName;
+
+ ProviderMetadata(ComponentName componentName) {
+ if (componentName == null) {
+ throw new IllegalArgumentException("componentName must not be null");
+ }
+ mComponentName = componentName;
+ }
+
+ /**
+ * Gets the provider's package name.
+ */
+ public String getPackageName() {
+ return mComponentName.getPackageName();
+ }
+
+ /**
+ * Gets the provider's component name.
+ */
+ public ComponentName getComponentName() {
+ return mComponentName;
+ }
+
+ @Override
+ public String toString() {
+ return "ProviderMetadata{ componentName="
+ + mComponentName.flattenToShortString() + " }";
+ }
+ }
+
+ /**
+ * Provides control over a particular route.
+ * <p>
+ * The media router obtains a route controller for a route whenever it needs
+ * to control a route. When a route is selected, the media router invokes
+ * the {@link #onSelect} method of its route controller. While selected,
+ * the media router may call other methods of the route controller to
+ * request that it perform certain actions to the route. When a route is
+ * unselected, the media router invokes the {@link #onUnselect} method of its
+ * route controller. When the media route no longer needs the route controller
+ * it will invoke the {@link #onRelease} method to allow the route controller
+ * to free its resources.
+ * </p><p>
+ * There may be multiple route controllers simultaneously active for the
+ * same route. Each route controller will be released separately.
+ * </p><p>
+ * All operations on the route controller are asynchronous and
+ * results are communicated via callbacks.
+ * </p>
+ */
+ public static abstract class RouteController {
+ /**
+ * Releases the route controller, allowing it to free its resources.
+ */
+ public void onRelease() {
+ }
+
+ /**
+ * Selects the route.
+ */
+ public void onSelect() {
+ }
+
+ /**
+ * Unselects the route.
+ */
+ public void onUnselect() {
+ }
+
+ /**
+ * Unselects the route and provides a reason. The default implementation
+ * calls {@link #onUnselect()}.
+ * <p>
+ * The reason provided will be one of the following:
+ * <ul>
+ * <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li>
+ * </ul>
+ *
+ * @param reason The reason for unselecting the route.
+ */
+ public void onUnselect(int reason) {
+ onUnselect();
+ }
+
+ /**
+ * Requests to set the volume of the route.
+ *
+ * @param volume The new volume value between 0 and {@link MediaRouteDescriptor#getVolumeMax}.
+ */
+ public void onSetVolume(int volume) {
+ }
+
+ /**
+ * Requests an incremental volume update for the route.
+ *
+ * @param delta The delta to add to the current volume.
+ */
+ public void onUpdateVolume(int delta) {
+ }
+
+ /**
+ * Performs a {@link MediaControlIntent media control} request
+ * asynchronously on behalf of the route.
+ *
+ * @param intent A {@link MediaControlIntent media control intent}.
+ * @param callback A {@link ControlRequestCallback} to invoke with the result
+ * of the request, or null if no result is required.
+ * @return True if the controller intends to handle the request and will
+ * invoke the callback when finished. False if the controller will not
+ * handle the request and will not invoke the callback.
+ *
+ * @see MediaControlIntent
+ */
+ public boolean onControlRequest(Intent intent, @Nullable ControlRequestCallback callback) {
+ return false;
+ }
+ }
+
+ /**
+ * Callback which is invoked when route information becomes available or changes.
+ */
+ public static abstract class Callback {
+ /**
+ * Called when information about a route provider and its routes changes.
+ *
+ * @param provider The media route provider that changed, never null.
+ * @param descriptor The new media route provider descriptor, or null if none.
+ */
+ public void onDescriptorChanged(@NonNull MediaRouteProvider provider,
+ @Nullable MediaRouteProviderDescriptor descriptor) {
+ }
+ }
+
+ private final class ProviderHandler extends Handler {
+ ProviderHandler() {
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case MSG_DELIVER_DESCRIPTOR_CHANGED:
+ deliverDescriptorChanged();
+ break;
+ case MSG_DELIVER_DISCOVERY_REQUEST_CHANGED:
+ deliverDiscoveryRequestChanged();
+ break;
+ }
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/MediaRouteProviderDescriptor.java b/com/android/support/mediarouter/media/MediaRouteProviderDescriptor.java
new file mode 100644
index 00000000..eb1ce09e
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaRouteProviderDescriptor.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.os.Bundle;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Describes the state of a media route provider and the routes that it publishes.
+ * <p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ */
+public final class MediaRouteProviderDescriptor {
+ private static final String KEY_ROUTES = "routes";
+
+ private final Bundle mBundle;
+ private List<MediaRouteDescriptor> mRoutes;
+
+ private MediaRouteProviderDescriptor(Bundle bundle, List<MediaRouteDescriptor> routes) {
+ mBundle = bundle;
+ mRoutes = routes;
+ }
+
+ /**
+ * Gets the list of all routes that this provider has published.
+ */
+ public List<MediaRouteDescriptor> getRoutes() {
+ ensureRoutes();
+ return mRoutes;
+ }
+
+ private void ensureRoutes() {
+ if (mRoutes == null) {
+ ArrayList<Bundle> routeBundles = mBundle.<Bundle>getParcelableArrayList(KEY_ROUTES);
+ if (routeBundles == null || routeBundles.isEmpty()) {
+ mRoutes = Collections.<MediaRouteDescriptor>emptyList();
+ } else {
+ final int count = routeBundles.size();
+ mRoutes = new ArrayList<MediaRouteDescriptor>(count);
+ for (int i = 0; i < count; i++) {
+ mRoutes.add(MediaRouteDescriptor.fromBundle(routeBundles.get(i)));
+ }
+ }
+ }
+ }
+
+ /**
+ * Returns true if the route provider descriptor and all of the routes that
+ * it contains have all of the required fields.
+ * <p>
+ * This verification is deep. If the provider descriptor is known to be
+ * valid then it is not necessary to call {@link #isValid} on each of its routes.
+ * </p>
+ */
+ public boolean isValid() {
+ ensureRoutes();
+ final int routeCount = mRoutes.size();
+ for (int i = 0; i < routeCount; i++) {
+ MediaRouteDescriptor route = mRoutes.get(i);
+ if (route == null || !route.isValid()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaRouteProviderDescriptor{ ");
+ result.append("routes=").append(
+ Arrays.toString(getRoutes().toArray()));
+ result.append(", isValid=").append(isValid());
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaRouteProviderDescriptor fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaRouteProviderDescriptor(bundle, null) : null;
+ }
+
+ /**
+ * Builder for {@link MediaRouteProviderDescriptor media route provider descriptors}.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+ private ArrayList<MediaRouteDescriptor> mRoutes;
+
+ /**
+ * Creates an empty media route provider descriptor builder.
+ */
+ public Builder() {
+ mBundle = new Bundle();
+ }
+
+ /**
+ * Creates a media route provider descriptor builder whose initial contents are
+ * copied from an existing descriptor.
+ */
+ public Builder(MediaRouteProviderDescriptor descriptor) {
+ if (descriptor == null) {
+ throw new IllegalArgumentException("descriptor must not be null");
+ }
+
+ mBundle = new Bundle(descriptor.mBundle);
+
+ descriptor.ensureRoutes();
+ if (!descriptor.mRoutes.isEmpty()) {
+ mRoutes = new ArrayList<MediaRouteDescriptor>(descriptor.mRoutes);
+ }
+ }
+
+ /**
+ * Adds a route.
+ */
+ public Builder addRoute(MediaRouteDescriptor route) {
+ if (route == null) {
+ throw new IllegalArgumentException("route must not be null");
+ }
+
+ if (mRoutes == null) {
+ mRoutes = new ArrayList<MediaRouteDescriptor>();
+ } else if (mRoutes.contains(route)) {
+ throw new IllegalArgumentException("route descriptor already added");
+ }
+ mRoutes.add(route);
+ return this;
+ }
+
+ /**
+ * Adds a list of routes.
+ */
+ public Builder addRoutes(Collection<MediaRouteDescriptor> routes) {
+ if (routes == null) {
+ throw new IllegalArgumentException("routes must not be null");
+ }
+
+ if (!routes.isEmpty()) {
+ for (MediaRouteDescriptor route : routes) {
+ addRoute(route);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Sets the list of routes.
+ */
+ Builder setRoutes(Collection<MediaRouteDescriptor> routes) {
+ if (routes == null || routes.isEmpty()) {
+ mRoutes = null;
+ mBundle.remove(KEY_ROUTES);
+ } else {
+ mRoutes = new ArrayList<>(routes);
+ }
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaRouteProviderDescriptor media route provider descriptor}.
+ */
+ public MediaRouteProviderDescriptor build() {
+ if (mRoutes != null) {
+ final int count = mRoutes.size();
+ ArrayList<Bundle> routeBundles = new ArrayList<Bundle>(count);
+ for (int i = 0; i < count; i++) {
+ routeBundles.add(mRoutes.get(i).asBundle());
+ }
+ mBundle.putParcelableArrayList(KEY_ROUTES, routeBundles);
+ }
+ return new MediaRouteProviderDescriptor(mBundle, mRoutes);
+ }
+ }
+} \ No newline at end of file
diff --git a/com/android/support/mediarouter/media/MediaRouteProviderProtocol.java b/com/android/support/mediarouter/media/MediaRouteProviderProtocol.java
new file mode 100644
index 00000000..6be9343e
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaRouteProviderProtocol.java
@@ -0,0 +1,230 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.content.Intent;
+import android.os.Messenger;
+
+/**
+ * Defines the communication protocol for media route provider services.
+ */
+abstract class MediaRouteProviderProtocol {
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * Put this in your manifest.
+ */
+ public static final String SERVICE_INTERFACE =
+ "android.media.MediaRouteProviderService";
+
+ /*
+ * Messages sent from the client to the service.
+ * DO NOT RENUMBER THESE!
+ */
+
+ /** (client v1)
+ * Register client.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : client version
+ */
+ public static final int CLIENT_MSG_REGISTER = 1;
+
+ /** (client v1)
+ * Unregister client.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ */
+ public static final int CLIENT_MSG_UNREGISTER = 2;
+
+ /** (client v1)
+ * Create route controller.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ * - CLIENT_DATA_ROUTE_ID : route id string
+ */
+ public static final int CLIENT_MSG_CREATE_ROUTE_CONTROLLER = 3;
+
+ /** (client v1)
+ * Release route controller.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ */
+ public static final int CLIENT_MSG_RELEASE_ROUTE_CONTROLLER = 4;
+
+ /** (client v1)
+ * Select route.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ */
+ public static final int CLIENT_MSG_SELECT_ROUTE = 5;
+
+ /** (client v1)
+ * Unselect route.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ */
+ public static final int CLIENT_MSG_UNSELECT_ROUTE = 6;
+
+ /** (client v1)
+ * Set route volume.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ * - CLIENT_DATA_VOLUME : volume integer
+ */
+ public static final int CLIENT_MSG_SET_ROUTE_VOLUME = 7;
+
+ /** (client v1)
+ * Update route volume.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ * - CLIENT_DATA_VOLUME : volume delta integer
+ */
+ public static final int CLIENT_MSG_UPDATE_ROUTE_VOLUME = 8;
+
+ /** (client v1)
+ * Route control request.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - arg2 : route controller id
+ * - obj : media control intent
+ */
+ public static final int CLIENT_MSG_ROUTE_CONTROL_REQUEST = 9;
+
+ /** (client v1)
+ * Sets the discovery request.
+ * - replyTo : client messenger
+ * - arg1 : request id
+ * - obj : discovery request bundle, or null if none
+ */
+ public static final int CLIENT_MSG_SET_DISCOVERY_REQUEST = 10;
+
+ public static final String CLIENT_DATA_ROUTE_ID = "routeId";
+ public static final String CLIENT_DATA_ROUTE_LIBRARY_GROUP = "routeGroupId";
+ public static final String CLIENT_DATA_VOLUME = "volume";
+ public static final String CLIENT_DATA_UNSELECT_REASON = "unselectReason";
+
+ /*
+ * Messages sent from the service to the client.
+ * DO NOT RENUMBER THESE!
+ */
+
+ /** (service v1)
+ * Generic failure sent in response to any unrecognized or malformed request.
+ * - arg1 : request id
+ */
+ public static final int SERVICE_MSG_GENERIC_FAILURE = 0;
+
+ /** (service v1)
+ * Generic failure sent in response to a successful message.
+ * - arg1 : request id
+ */
+ public static final int SERVICE_MSG_GENERIC_SUCCESS = 1;
+
+ /** (service v1)
+ * Registration succeeded.
+ * - arg1 : request id
+ * - arg2 : server version
+ * - obj : route provider descriptor bundle, or null
+ */
+ public static final int SERVICE_MSG_REGISTERED = 2;
+
+ /** (service v1)
+ * Route control request success result.
+ * - arg1 : request id
+ * - obj : result data bundle, or null
+ */
+ public static final int SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED = 3;
+
+ /** (service v1)
+ * Route control request failure result.
+ * - arg1 : request id
+ * - obj : result data bundle, or null
+ * - SERVICE_DATA_ERROR: error message
+ */
+ public static final int SERVICE_MSG_CONTROL_REQUEST_FAILED = 4;
+
+ /** (service v1)
+ * Route provider descriptor changed. (unsolicited event)
+ * - arg1 : reserved (0)
+ * - obj : route provider descriptor bundle, or null
+ */
+ public static final int SERVICE_MSG_DESCRIPTOR_CHANGED = 5;
+
+ public static final String SERVICE_DATA_ERROR = "error";
+
+ /*
+ * Recognized client version numbers. (Reserved for future use.)
+ * DO NOT RENUMBER THESE!
+ */
+
+ /**
+ * The client version used from the beginning.
+ */
+ public static final int CLIENT_VERSION_1 = 1;
+
+ /**
+ * The client version used from support library v24.1.0.
+ */
+ public static final int CLIENT_VERSION_2 = 2;
+
+ /**
+ * The current client version.
+ */
+ public static final int CLIENT_VERSION_CURRENT = CLIENT_VERSION_2;
+
+ /*
+ * Recognized server version numbers. (Reserved for future use.)
+ * DO NOT RENUMBER THESE!
+ */
+
+ /**
+ * The service version used from the beginning.
+ */
+ public static final int SERVICE_VERSION_1 = 1;
+
+ /**
+ * The current service version.
+ */
+ public static final int SERVICE_VERSION_CURRENT = SERVICE_VERSION_1;
+
+ static final int CLIENT_VERSION_START = CLIENT_VERSION_1;
+
+ /**
+ * Returns true if the messenger object is valid.
+ * <p>
+ * The messenger constructor and unparceling code does not check whether the
+ * provided IBinder is a valid IMessenger object. As a result, it's possible
+ * for a peer to send an invalid IBinder that will result in crashes downstream.
+ * This method checks that the messenger is in a valid state.
+ * </p>
+ */
+ public static boolean isValidRemoteMessenger(Messenger messenger) {
+ try {
+ return messenger != null && messenger.getBinder() != null;
+ } catch (NullPointerException ex) {
+ // If the messenger was constructed with a binder interface other than
+ // IMessenger then the call to getBinder() will crash with an NPE.
+ return false;
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/MediaRouteProviderService.java b/com/android/support/mediarouter/media/MediaRouteProviderService.java
new file mode 100644
index 00000000..43cde106
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaRouteProviderService.java
@@ -0,0 +1,759 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_DATA_ROUTE_ID;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_DATA_ROUTE_LIBRARY_GROUP;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_DATA_UNSELECT_REASON;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_DATA_VOLUME;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_CREATE_ROUTE_CONTROLLER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_REGISTER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_RELEASE_ROUTE_CONTROLLER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_ROUTE_CONTROL_REQUEST;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_SELECT_ROUTE;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_SET_DISCOVERY_REQUEST;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_SET_ROUTE_VOLUME;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_UNREGISTER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_UNSELECT_ROUTE;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_UPDATE_ROUTE_VOLUME;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_VERSION_1;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_DATA_ERROR;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_CONTROL_REQUEST_FAILED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_DESCRIPTOR_CHANGED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_GENERIC_FAILURE;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_GENERIC_SUCCESS;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_REGISTERED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_VERSION_CURRENT;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.isValidRemoteMessenger;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.support.annotation.VisibleForTesting;
+import android.support.v4.util.ObjectsCompat;
+import android.util.Log;
+import android.util.SparseArray;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Base class for media route provider services.
+ * <p>
+ * A media router will bind to media route provider services when a callback is added via
+ * {@link MediaRouter#addCallback(MediaRouteSelector, MediaRouter.Callback, int)} with a discovery
+ * flag: {@link MediaRouter#CALLBACK_FLAG_REQUEST_DISCOVERY},
+ * {@link MediaRouter#CALLBACK_FLAG_FORCE_DISCOVERY}, or
+ * {@link MediaRouter#CALLBACK_FLAG_PERFORM_ACTIVE_SCAN}, and will unbind when the callback
+ * is removed via {@link MediaRouter#removeCallback(MediaRouter.Callback)}.
+ * </p><p>
+ * To implement your own media route provider service, extend this class and
+ * override the {@link #onCreateMediaRouteProvider} method to return an
+ * instance of your {@link MediaRouteProvider}.
+ * </p><p>
+ * Declare your media route provider service in your application manifest
+ * like this:
+ * </p>
+ * <pre>
+ * &lt;service android:name=".MyMediaRouteProviderService"
+ * android:label="@string/my_media_route_provider_service">
+ * &lt;intent-filter>
+ * &lt;action android:name="android.media.MediaRouteProviderService" />
+ * &lt;/intent-filter>
+ * &lt;/service>
+ * </pre>
+ */
+public abstract class MediaRouteProviderService extends Service {
+ static final String TAG = "MediaRouteProviderSrv"; // max. 23 chars
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final ArrayList<ClientRecord> mClients = new ArrayList<ClientRecord>();
+ private final ReceiveHandler mReceiveHandler;
+ private final Messenger mReceiveMessenger;
+ final PrivateHandler mPrivateHandler;
+ private final ProviderCallback mProviderCallback;
+
+ MediaRouteProvider mProvider;
+ private MediaRouteDiscoveryRequest mCompositeDiscoveryRequest;
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * Put this in your manifest.
+ */
+ public static final String SERVICE_INTERFACE = MediaRouteProviderProtocol.SERVICE_INTERFACE;
+
+ /*
+ * Private messages used internally. (Yes, you can renumber these.)
+ */
+
+ static final int PRIVATE_MSG_CLIENT_DIED = 1;
+
+ /**
+ * Creates a media route provider service.
+ */
+ public MediaRouteProviderService() {
+ mReceiveHandler = new ReceiveHandler(this);
+ mReceiveMessenger = new Messenger(mReceiveHandler);
+ mPrivateHandler = new PrivateHandler();
+ mProviderCallback = new ProviderCallback();
+ }
+
+ /**
+ * Called by the system when it is time to create the media route provider.
+ *
+ * @return The media route provider offered by this service, or null if
+ * this service has decided not to offer a media route provider.
+ */
+ public abstract MediaRouteProvider onCreateMediaRouteProvider();
+
+ /**
+ * Gets the media route provider offered by this service.
+ *
+ * @return The media route provider offered by this service, or null if
+ * it has not yet been created.
+ *
+ * @see #onCreateMediaRouteProvider()
+ */
+ public MediaRouteProvider getMediaRouteProvider() {
+ return mProvider;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ if (intent.getAction().equals(SERVICE_INTERFACE)) {
+ if (mProvider == null) {
+ MediaRouteProvider provider = onCreateMediaRouteProvider();
+ if (provider != null) {
+ String providerPackage = provider.getMetadata().getPackageName();
+ if (!providerPackage.equals(getPackageName())) {
+ throw new IllegalStateException("onCreateMediaRouteProvider() returned "
+ + "a provider whose package name does not match the package "
+ + "name of the service. A media route provider service can "
+ + "only export its own media route providers. "
+ + "Provider package name: " + providerPackage
+ + ". Service package name: " + getPackageName() + ".");
+ }
+ mProvider = provider;
+ mProvider.setCallback(mProviderCallback);
+ }
+ }
+ if (mProvider != null) {
+ return mReceiveMessenger.getBinder();
+ }
+ }
+ return null;
+ }
+
+ @Override
+ public boolean onUnbind(Intent intent) {
+ if (mProvider != null) {
+ mProvider.setCallback(null);
+ }
+ return super.onUnbind(intent);
+ }
+
+ boolean onRegisterClient(Messenger messenger, int requestId, int version) {
+ if (version >= CLIENT_VERSION_1) {
+ int index = findClient(messenger);
+ if (index < 0) {
+ ClientRecord client = new ClientRecord(messenger, version);
+ if (client.register()) {
+ mClients.add(client);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Registered, version=" + version);
+ }
+ if (requestId != 0) {
+ MediaRouteProviderDescriptor descriptor = mProvider.getDescriptor();
+ sendReply(messenger, SERVICE_MSG_REGISTERED,
+ requestId, SERVICE_VERSION_CURRENT,
+ createDescriptorBundleForClientVersion(descriptor,
+ client.mVersion), null);
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ boolean onUnregisterClient(Messenger messenger, int requestId) {
+ int index = findClient(messenger);
+ if (index >= 0) {
+ ClientRecord client = mClients.remove(index);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Unregistered");
+ }
+ client.dispose();
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ return false;
+ }
+
+ void onBinderDied(Messenger messenger) {
+ int index = findClient(messenger);
+ if (index >= 0) {
+ ClientRecord client = mClients.remove(index);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Binder died");
+ }
+ client.dispose();
+ }
+ }
+
+ boolean onCreateRouteController(Messenger messenger, int requestId,
+ int controllerId, String routeId, String routeGroupId) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ if (client.createRouteController(routeId, routeGroupId, controllerId)) {
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route controller created, controllerId=" + controllerId
+ + ", routeId=" + routeId + ", routeGroupId=" + routeGroupId);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean onReleaseRouteController(Messenger messenger, int requestId,
+ int controllerId) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ if (client.releaseRouteController(controllerId)) {
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route controller released"
+ + ", controllerId=" + controllerId);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean onSelectRoute(Messenger messenger, int requestId,
+ int controllerId) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ MediaRouteProvider.RouteController controller =
+ client.getRouteController(controllerId);
+ if (controller != null) {
+ controller.onSelect();
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route selected"
+ + ", controllerId=" + controllerId);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean onUnselectRoute(Messenger messenger, int requestId,
+ int controllerId, int reason) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ MediaRouteProvider.RouteController controller =
+ client.getRouteController(controllerId);
+ if (controller != null) {
+ controller.onUnselect(reason);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route unselected"
+ + ", controllerId=" + controllerId);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean onSetRouteVolume(Messenger messenger, int requestId,
+ int controllerId, int volume) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ MediaRouteProvider.RouteController controller =
+ client.getRouteController(controllerId);
+ if (controller != null) {
+ controller.onSetVolume(volume);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route volume changed"
+ + ", controllerId=" + controllerId + ", volume=" + volume);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean onUpdateRouteVolume(Messenger messenger, int requestId,
+ int controllerId, int delta) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ MediaRouteProvider.RouteController controller =
+ client.getRouteController(controllerId);
+ if (controller != null) {
+ controller.onUpdateVolume(delta);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route volume updated"
+ + ", controllerId=" + controllerId + ", delta=" + delta);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean onRouteControlRequest(final Messenger messenger, final int requestId,
+ final int controllerId, final Intent intent) {
+ final ClientRecord client = getClient(messenger);
+ if (client != null) {
+ MediaRouteProvider.RouteController controller =
+ client.getRouteController(controllerId);
+ if (controller != null) {
+ MediaRouter.ControlRequestCallback callback = null;
+ if (requestId != 0) {
+ callback = new MediaRouter.ControlRequestCallback() {
+ @Override
+ public void onResult(Bundle data) {
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route control request succeeded"
+ + ", controllerId=" + controllerId
+ + ", intent=" + intent
+ + ", data=" + data);
+ }
+ if (findClient(messenger) >= 0) {
+ sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED,
+ requestId, 0, data, null);
+ }
+ }
+
+ @Override
+ public void onError(String error, Bundle data) {
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route control request failed"
+ + ", controllerId=" + controllerId
+ + ", intent=" + intent
+ + ", error=" + error + ", data=" + data);
+ }
+ if (findClient(messenger) >= 0) {
+ if (error != null) {
+ Bundle bundle = new Bundle();
+ bundle.putString(SERVICE_DATA_ERROR, error);
+ sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_FAILED,
+ requestId, 0, data, bundle);
+ } else {
+ sendReply(messenger, SERVICE_MSG_CONTROL_REQUEST_FAILED,
+ requestId, 0, data, null);
+ }
+ }
+ }
+ };
+ }
+ if (controller.onControlRequest(intent, callback)) {
+ if (DEBUG) {
+ Log.d(TAG, client + ": Route control request delivered"
+ + ", controllerId=" + controllerId + ", intent=" + intent);
+ }
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ boolean onSetDiscoveryRequest(Messenger messenger, int requestId,
+ MediaRouteDiscoveryRequest request) {
+ ClientRecord client = getClient(messenger);
+ if (client != null) {
+ boolean actuallyChanged = client.setDiscoveryRequest(request);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Set discovery request, request=" + request
+ + ", actuallyChanged=" + actuallyChanged
+ + ", compositeDiscoveryRequest=" + mCompositeDiscoveryRequest);
+ }
+ sendGenericSuccess(messenger, requestId);
+ return true;
+ }
+ return false;
+ }
+
+ void sendDescriptorChanged(MediaRouteProviderDescriptor descriptor) {
+ final int count = mClients.size();
+ for (int i = 0; i < count; i++) {
+ ClientRecord client = mClients.get(i);
+ sendReply(client.mMessenger, SERVICE_MSG_DESCRIPTOR_CHANGED, 0, 0,
+ createDescriptorBundleForClientVersion(descriptor, client.mVersion), null);
+ if (DEBUG) {
+ Log.d(TAG, client + ": Sent descriptor change event, descriptor=" + descriptor);
+ }
+ }
+ }
+
+ @VisibleForTesting
+ static Bundle createDescriptorBundleForClientVersion(MediaRouteProviderDescriptor descriptor,
+ int clientVersion) {
+ if (descriptor == null) {
+ return null;
+ }
+ MediaRouteProviderDescriptor.Builder builder =
+ new MediaRouteProviderDescriptor.Builder(descriptor);
+ builder.setRoutes(null);
+ for (MediaRouteDescriptor route : descriptor.getRoutes()) {
+ if (clientVersion >= route.getMinClientVersion()
+ && clientVersion <= route.getMaxClientVersion()) {
+ builder.addRoute(route);
+ }
+ }
+ return builder.build().asBundle();
+ }
+
+ boolean updateCompositeDiscoveryRequest() {
+ MediaRouteDiscoveryRequest composite = null;
+ MediaRouteSelector.Builder selectorBuilder = null;
+ boolean activeScan = false;
+ final int count = mClients.size();
+ for (int i = 0; i < count; i++) {
+ MediaRouteDiscoveryRequest request = mClients.get(i).mDiscoveryRequest;
+ if (request != null
+ && (!request.getSelector().isEmpty() || request.isActiveScan())) {
+ activeScan |= request.isActiveScan();
+ if (composite == null) {
+ composite = request;
+ } else {
+ if (selectorBuilder == null) {
+ selectorBuilder = new MediaRouteSelector.Builder(composite.getSelector());
+ }
+ selectorBuilder.addSelector(request.getSelector());
+ }
+ }
+ }
+ if (selectorBuilder != null) {
+ composite = new MediaRouteDiscoveryRequest(selectorBuilder.build(), activeScan);
+ }
+ if (!ObjectsCompat.equals(mCompositeDiscoveryRequest, composite)) {
+ mCompositeDiscoveryRequest = composite;
+ mProvider.setDiscoveryRequest(composite);
+ return true;
+ }
+ return false;
+ }
+
+ private ClientRecord getClient(Messenger messenger) {
+ int index = findClient(messenger);
+ return index >= 0 ? mClients.get(index) : null;
+ }
+
+ int findClient(Messenger messenger) {
+ final int count = mClients.size();
+ for (int i = 0; i < count; i++) {
+ ClientRecord client = mClients.get(i);
+ if (client.hasMessenger(messenger)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ static void sendGenericFailure(Messenger messenger, int requestId) {
+ if (requestId != 0) {
+ sendReply(messenger, SERVICE_MSG_GENERIC_FAILURE, requestId, 0, null, null);
+ }
+ }
+
+ private static void sendGenericSuccess(Messenger messenger, int requestId) {
+ if (requestId != 0) {
+ sendReply(messenger, SERVICE_MSG_GENERIC_SUCCESS, requestId, 0, null, null);
+ }
+ }
+
+ static void sendReply(Messenger messenger, int what,
+ int requestId, int arg, Object obj, Bundle data) {
+ Message msg = Message.obtain();
+ msg.what = what;
+ msg.arg1 = requestId;
+ msg.arg2 = arg;
+ msg.obj = obj;
+ msg.setData(data);
+ try {
+ messenger.send(msg);
+ } catch (DeadObjectException ex) {
+ // The client died.
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Could not send message to " + getClientId(messenger), ex);
+ }
+ }
+
+ static String getClientId(Messenger messenger) {
+ return "Client connection " + messenger.getBinder().toString();
+ }
+
+ private final class PrivateHandler extends Handler {
+ PrivateHandler() {
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case PRIVATE_MSG_CLIENT_DIED:
+ onBinderDied((Messenger)msg.obj);
+ break;
+ }
+ }
+ }
+
+ private final class ProviderCallback extends MediaRouteProvider.Callback {
+ ProviderCallback() {
+ }
+
+ @Override
+ public void onDescriptorChanged(MediaRouteProvider provider,
+ MediaRouteProviderDescriptor descriptor) {
+ sendDescriptorChanged(descriptor);
+ }
+ }
+
+ private final class ClientRecord implements DeathRecipient {
+ public final Messenger mMessenger;
+ public final int mVersion;
+ public MediaRouteDiscoveryRequest mDiscoveryRequest;
+
+ private final SparseArray<MediaRouteProvider.RouteController> mControllers =
+ new SparseArray<MediaRouteProvider.RouteController>();
+
+ public ClientRecord(Messenger messenger, int version) {
+ mMessenger = messenger;
+ mVersion = version;
+ }
+
+ public boolean register() {
+ try {
+ mMessenger.getBinder().linkToDeath(this, 0);
+ return true;
+ } catch (RemoteException ex) {
+ binderDied();
+ }
+ return false;
+ }
+
+ public void dispose() {
+ int count = mControllers.size();
+ for (int i = 0; i < count; i++) {
+ mControllers.valueAt(i).onRelease();
+ }
+ mControllers.clear();
+
+ mMessenger.getBinder().unlinkToDeath(this, 0);
+
+ setDiscoveryRequest(null);
+ }
+
+ public boolean hasMessenger(Messenger other) {
+ return mMessenger.getBinder() == other.getBinder();
+ }
+
+ public boolean createRouteController(String routeId, String routeGroupId,
+ int controllerId) {
+ if (mControllers.indexOfKey(controllerId) < 0) {
+ MediaRouteProvider.RouteController controller = routeGroupId == null
+ ? mProvider.onCreateRouteController(routeId)
+ : mProvider.onCreateRouteController(routeId, routeGroupId);
+ if (controller != null) {
+ mControllers.put(controllerId, controller);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public boolean releaseRouteController(int controllerId) {
+ MediaRouteProvider.RouteController controller = mControllers.get(controllerId);
+ if (controller != null) {
+ mControllers.remove(controllerId);
+ controller.onRelease();
+ return true;
+ }
+ return false;
+ }
+
+ public MediaRouteProvider.RouteController getRouteController(int controllerId) {
+ return mControllers.get(controllerId);
+ }
+
+ public boolean setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
+ if (!ObjectsCompat.equals(mDiscoveryRequest, request)) {
+ mDiscoveryRequest = request;
+ return updateCompositeDiscoveryRequest();
+ }
+ return false;
+ }
+
+ // Runs on a binder thread.
+ @Override
+ public void binderDied() {
+ mPrivateHandler.obtainMessage(PRIVATE_MSG_CLIENT_DIED, mMessenger).sendToTarget();
+ }
+
+ @Override
+ public String toString() {
+ return getClientId(mMessenger);
+ }
+ }
+
+ /**
+ * Handler that receives messages from clients.
+ * <p>
+ * This inner class is static and only retains a weak reference to the service
+ * to prevent the service from being leaked in case one of the clients is holding an
+ * active reference to the server's messenger.
+ * </p><p>
+ * This handler should not be used to handle any messages other than those
+ * that come from the client.
+ * </p>
+ */
+ private static final class ReceiveHandler extends Handler {
+ private final WeakReference<MediaRouteProviderService> mServiceRef;
+
+ public ReceiveHandler(MediaRouteProviderService service) {
+ mServiceRef = new WeakReference<MediaRouteProviderService>(service);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final Messenger messenger = msg.replyTo;
+ if (isValidRemoteMessenger(messenger)) {
+ final int what = msg.what;
+ final int requestId = msg.arg1;
+ final int arg = msg.arg2;
+ final Object obj = msg.obj;
+ final Bundle data = msg.peekData();
+ if (!processMessage(what, messenger, requestId, arg, obj, data)) {
+ if (DEBUG) {
+ Log.d(TAG, getClientId(messenger) + ": Message failed, what=" + what
+ + ", requestId=" + requestId + ", arg=" + arg
+ + ", obj=" + obj + ", data=" + data);
+ }
+ sendGenericFailure(messenger, requestId);
+ }
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Ignoring message without valid reply messenger.");
+ }
+ }
+ }
+
+ private boolean processMessage(int what,
+ Messenger messenger, int requestId, int arg, Object obj, Bundle data) {
+ MediaRouteProviderService service = mServiceRef.get();
+ if (service != null) {
+ switch (what) {
+ case CLIENT_MSG_REGISTER:
+ return service.onRegisterClient(messenger, requestId, arg);
+
+ case CLIENT_MSG_UNREGISTER:
+ return service.onUnregisterClient(messenger, requestId);
+
+ case CLIENT_MSG_CREATE_ROUTE_CONTROLLER: {
+ String routeId = data.getString(CLIENT_DATA_ROUTE_ID);
+ String routeGroupId = data.getString(CLIENT_DATA_ROUTE_LIBRARY_GROUP);
+ if (routeId != null) {
+ return service.onCreateRouteController(
+ messenger, requestId, arg, routeId, routeGroupId);
+ }
+ break;
+ }
+
+ case CLIENT_MSG_RELEASE_ROUTE_CONTROLLER:
+ return service.onReleaseRouteController(messenger, requestId, arg);
+
+ case CLIENT_MSG_SELECT_ROUTE:
+ return service.onSelectRoute(messenger, requestId, arg);
+
+ case CLIENT_MSG_UNSELECT_ROUTE:
+ int reason = data == null ?
+ MediaRouter.UNSELECT_REASON_UNKNOWN
+ : data.getInt(CLIENT_DATA_UNSELECT_REASON,
+ MediaRouter.UNSELECT_REASON_UNKNOWN);
+ return service.onUnselectRoute(messenger, requestId, arg, reason);
+
+ case CLIENT_MSG_SET_ROUTE_VOLUME: {
+ int volume = data.getInt(CLIENT_DATA_VOLUME, -1);
+ if (volume >= 0) {
+ return service.onSetRouteVolume(
+ messenger, requestId, arg, volume);
+ }
+ break;
+ }
+
+ case CLIENT_MSG_UPDATE_ROUTE_VOLUME: {
+ int delta = data.getInt(CLIENT_DATA_VOLUME, 0);
+ if (delta != 0) {
+ return service.onUpdateRouteVolume(
+ messenger, requestId, arg, delta);
+ }
+ break;
+ }
+
+ case CLIENT_MSG_ROUTE_CONTROL_REQUEST:
+ if (obj instanceof Intent) {
+ return service.onRouteControlRequest(
+ messenger, requestId, arg, (Intent)obj);
+ }
+ break;
+
+ case CLIENT_MSG_SET_DISCOVERY_REQUEST: {
+ if (obj == null || obj instanceof Bundle) {
+ MediaRouteDiscoveryRequest request =
+ MediaRouteDiscoveryRequest.fromBundle((Bundle)obj);
+ return service.onSetDiscoveryRequest(
+ messenger, requestId,
+ request != null && request.isValid() ? request : null);
+ }
+ }
+ }
+ }
+ return false;
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/MediaRouteSelector.java b/com/android/support/mediarouter/media/MediaRouteSelector.java
new file mode 100644
index 00000000..5669b19a
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaRouteSelector.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.content.IntentFilter;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Describes the capabilities of routes that applications would like to discover and use.
+ * <p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ *
+ * <h3>Example</h3>
+ * <pre>
+ * MediaRouteSelector selectorBuilder = new MediaRouteSelector.Builder()
+ * .addControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)
+ * .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
+ * .build();
+ *
+ * MediaRouter router = MediaRouter.getInstance(context);
+ * router.addCallback(selector, callback, MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
+ * </pre>
+ */
+public final class MediaRouteSelector {
+ static final String KEY_CONTROL_CATEGORIES = "controlCategories";
+
+ private final Bundle mBundle;
+ List<String> mControlCategories;
+
+ /**
+ * An empty media route selector that will not match any routes.
+ */
+ public static final MediaRouteSelector EMPTY = new MediaRouteSelector(new Bundle(), null);
+
+ MediaRouteSelector(Bundle bundle, List<String> controlCategories) {
+ mBundle = bundle;
+ mControlCategories = controlCategories;
+ }
+
+ /**
+ * Gets the list of {@link MediaControlIntent media control categories} in the selector.
+ *
+ * @return The list of categories.
+ */
+ public List<String> getControlCategories() {
+ ensureControlCategories();
+ return mControlCategories;
+ }
+
+ void ensureControlCategories() {
+ if (mControlCategories == null) {
+ mControlCategories = mBundle.getStringArrayList(KEY_CONTROL_CATEGORIES);
+ if (mControlCategories == null || mControlCategories.isEmpty()) {
+ mControlCategories = Collections.<String>emptyList();
+ }
+ }
+ }
+
+ /**
+ * Returns true if the selector contains the specified category.
+ *
+ * @param category The category to check.
+ * @return True if the category is present.
+ */
+ public boolean hasControlCategory(String category) {
+ if (category != null) {
+ ensureControlCategories();
+ final int categoryCount = mControlCategories.size();
+ for (int i = 0; i < categoryCount; i++) {
+ if (mControlCategories.get(i).equals(category)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the selector matches at least one of the specified control filters.
+ *
+ * @param filters The list of control filters to consider.
+ * @return True if a match is found.
+ */
+ public boolean matchesControlFilters(List<IntentFilter> filters) {
+ if (filters != null) {
+ ensureControlCategories();
+ final int categoryCount = mControlCategories.size();
+ if (categoryCount != 0) {
+ final int filterCount = filters.size();
+ for (int i = 0; i < filterCount; i++) {
+ final IntentFilter filter = filters.get(i);
+ if (filter != null) {
+ for (int j = 0; j < categoryCount; j++) {
+ if (filter.hasCategory(mControlCategories.get(j))) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if this selector contains all of the capabilities described
+ * by the specified selector.
+ *
+ * @param selector The selector to be examined.
+ * @return True if this selector contains all of the capabilities described
+ * by the specified selector.
+ */
+ public boolean contains(MediaRouteSelector selector) {
+ if (selector != null) {
+ ensureControlCategories();
+ selector.ensureControlCategories();
+ return mControlCategories.containsAll(selector.mControlCategories);
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the selector does not specify any capabilities.
+ */
+ public boolean isEmpty() {
+ ensureControlCategories();
+ return mControlCategories.isEmpty();
+ }
+
+ /**
+ * Returns true if the selector has all of the required fields.
+ */
+ public boolean isValid() {
+ ensureControlCategories();
+ if (mControlCategories.contains(null)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o instanceof MediaRouteSelector) {
+ MediaRouteSelector other = (MediaRouteSelector)o;
+ ensureControlCategories();
+ other.ensureControlCategories();
+ return mControlCategories.equals(other.mControlCategories);
+ }
+ return false;
+ }
+
+ @Override
+ public int hashCode() {
+ ensureControlCategories();
+ return mControlCategories.hashCode();
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaRouteSelector{ ");
+ result.append("controlCategories=").append(
+ Arrays.toString(getControlCategories().toArray()));
+ result.append(" }");
+ return result.toString();
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaRouteSelector fromBundle(@Nullable Bundle bundle) {
+ return bundle != null ? new MediaRouteSelector(bundle, null) : null;
+ }
+
+ /**
+ * Builder for {@link MediaRouteSelector media route selectors}.
+ */
+ public static final class Builder {
+ private ArrayList<String> mControlCategories;
+
+ /**
+ * Creates an empty media route selector builder.
+ */
+ public Builder() {
+ }
+
+ /**
+ * Creates a media route selector descriptor builder whose initial contents are
+ * copied from an existing selector.
+ */
+ public Builder(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ selector.ensureControlCategories();
+ if (!selector.mControlCategories.isEmpty()) {
+ mControlCategories = new ArrayList<String>(selector.mControlCategories);
+ }
+ }
+
+ /**
+ * Adds a {@link MediaControlIntent media control category} to the builder.
+ *
+ * @param category The category to add to the set of desired capabilities, such as
+ * {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}.
+ * @return The builder instance for chaining.
+ */
+ @NonNull
+ public Builder addControlCategory(@NonNull String category) {
+ if (category == null) {
+ throw new IllegalArgumentException("category must not be null");
+ }
+
+ if (mControlCategories == null) {
+ mControlCategories = new ArrayList<String>();
+ }
+ if (!mControlCategories.contains(category)) {
+ mControlCategories.add(category);
+ }
+ return this;
+ }
+
+ /**
+ * Adds a list of {@link MediaControlIntent media control categories} to the builder.
+ *
+ * @param categories The list categories to add to the set of desired capabilities,
+ * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO}.
+ * @return The builder instance for chaining.
+ */
+ @NonNull
+ public Builder addControlCategories(@NonNull Collection<String> categories) {
+ if (categories == null) {
+ throw new IllegalArgumentException("categories must not be null");
+ }
+
+ if (!categories.isEmpty()) {
+ for (String category : categories) {
+ addControlCategory(category);
+ }
+ }
+ return this;
+ }
+
+ /**
+ * Adds the contents of an existing media route selector to the builder.
+ *
+ * @param selector The media route selector whose contents are to be added.
+ * @return The builder instance for chaining.
+ */
+ @NonNull
+ public Builder addSelector(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+
+ addControlCategories(selector.getControlCategories());
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaRouteSelector media route selector}.
+ */
+ @NonNull
+ public MediaRouteSelector build() {
+ if (mControlCategories == null) {
+ return EMPTY;
+ }
+ Bundle bundle = new Bundle();
+ bundle.putStringArrayList(KEY_CONTROL_CATEGORIES, mControlCategories);
+ return new MediaRouteSelector(bundle, mControlCategories);
+ }
+ }
+} \ No newline at end of file
diff --git a/com/android/support/mediarouter/media/MediaRouter.java b/com/android/support/mediarouter/media/MediaRouter.java
new file mode 100644
index 00000000..db0052e3
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaRouter.java
@@ -0,0 +1,2999 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.support.v4.app.ActivityManagerCompat;
+import android.support.v4.hardware.display.DisplayManagerCompat;
+import android.support.v4.media.VolumeProviderCompat;
+import android.support.v4.media.session.MediaSessionCompat;
+import android.support.v4.util.Pair;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.Display;
+
+import com.android.support.mediarouter.media.MediaRouteProvider.ProviderMetadata;
+import com.android.support.mediarouter.media.MediaRouteProvider.RouteController;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * MediaRouter allows applications to control the routing of media channels
+ * and streams from the current device to external speakers and destination devices.
+ * <p>
+ * A MediaRouter instance is retrieved through {@link #getInstance}. Applications
+ * can query the media router about the currently selected route and its capabilities
+ * to determine how to send content to the route's destination. Applications can
+ * also {@link RouteInfo#sendControlRequest send control requests} to the route
+ * to ask the route's destination to perform certain remote control functions
+ * such as playing media.
+ * </p><p>
+ * See also {@link MediaRouteProvider} for information on how an application
+ * can publish new media routes to the media router.
+ * </p><p>
+ * The media router API is not thread-safe; all interactions with it must be
+ * done from the main thread of the process.
+ * </p>
+ */
+public final class MediaRouter {
+ static final String TAG = "MediaRouter";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ /**
+ * Passed to {@link android.support.v7.media.MediaRouteProvider.RouteController#onUnselect(int)}
+ * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the reason the route
+ * was unselected is unknown.
+ */
+ public static final int UNSELECT_REASON_UNKNOWN = 0;
+ /**
+ * Passed to {@link android.support.v7.media.MediaRouteProvider.RouteController#onUnselect(int)}
+ * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the user pressed
+ * the disconnect button to disconnect and keep playing.
+ * <p>
+ *
+ * @see MediaRouteDescriptor#canDisconnectAndKeepPlaying()
+ */
+ public static final int UNSELECT_REASON_DISCONNECTED = 1;
+ /**
+ * Passed to {@link android.support.v7.media.MediaRouteProvider.RouteController#onUnselect(int)}
+ * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the user pressed
+ * the stop casting button.
+ */
+ public static final int UNSELECT_REASON_STOPPED = 2;
+ /**
+ * Passed to {@link android.support.v7.media.MediaRouteProvider.RouteController#onUnselect(int)}
+ * and {@link Callback#onRouteUnselected(MediaRouter, RouteInfo, int)} when the user selected
+ * a different route.
+ */
+ public static final int UNSELECT_REASON_ROUTE_CHANGED = 3;
+
+ // Maintains global media router state for the process.
+ // This field is initialized in MediaRouter.getInstance() before any
+ // MediaRouter objects are instantiated so it is guaranteed to be
+ // valid whenever any instance method is invoked.
+ static GlobalMediaRouter sGlobal;
+
+ // Context-bound state of the media router.
+ final Context mContext;
+ final ArrayList<CallbackRecord> mCallbackRecords = new ArrayList<CallbackRecord>();
+
+ @IntDef(flag = true,
+ value = {
+ CALLBACK_FLAG_PERFORM_ACTIVE_SCAN,
+ CALLBACK_FLAG_REQUEST_DISCOVERY,
+ CALLBACK_FLAG_UNFILTERED_EVENTS
+ }
+ )
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface CallbackFlags {}
+
+ /**
+ * Flag for {@link #addCallback}: Actively scan for routes while this callback
+ * is registered.
+ * <p>
+ * When this flag is specified, the media router will actively scan for new
+ * routes. Certain routes, such as wifi display routes, may not be discoverable
+ * except when actively scanning. This flag is typically used when the route picker
+ * dialog has been opened by the user to ensure that the route information is
+ * up to date.
+ * </p><p>
+ * Active scanning may consume a significant amount of power and may have intrusive
+ * effects on wireless connectivity. Therefore it is important that active scanning
+ * only be requested when it is actually needed to satisfy a user request to
+ * discover and select a new route.
+ * </p><p>
+ * This flag implies {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} but performing
+ * active scans is much more expensive than a normal discovery request.
+ * </p>
+ *
+ * @see #CALLBACK_FLAG_REQUEST_DISCOVERY
+ */
+ public static final int CALLBACK_FLAG_PERFORM_ACTIVE_SCAN = 1 << 0;
+
+ /**
+ * Flag for {@link #addCallback}: Do not filter route events.
+ * <p>
+ * When this flag is specified, the callback will be invoked for events that affect any
+ * route even if they do not match the callback's filter.
+ * </p>
+ */
+ public static final int CALLBACK_FLAG_UNFILTERED_EVENTS = 1 << 1;
+
+ /**
+ * Flag for {@link #addCallback}: Request passive route discovery while this
+ * callback is registered, except on {@link ActivityManager#isLowRamDevice low-RAM devices}.
+ * <p>
+ * When this flag is specified, the media router will try to discover routes.
+ * Although route discovery is intended to be efficient, checking for new routes may
+ * result in some network activity and could slowly drain the battery. Therefore
+ * applications should only specify {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} when
+ * they are running in the foreground and would like to provide the user with the
+ * option of connecting to new routes.
+ * </p><p>
+ * Applications should typically add a callback using this flag in the
+ * {@link android.app.Activity activity's} {@link android.app.Activity#onStart onStart}
+ * method and remove it in the {@link android.app.Activity#onStop onStop} method.
+ * The {@link android.support.v7.app.MediaRouteDiscoveryFragment} fragment may
+ * also be used for this purpose.
+ * </p><p class="note">
+ * On {@link ActivityManager#isLowRamDevice low-RAM devices} this flag
+ * will be ignored. Refer to
+ * {@link #addCallback(MediaRouteSelector, Callback, int) addCallback} for details.
+ * </p>
+ *
+ * @see android.support.v7.app.MediaRouteDiscoveryFragment
+ */
+ public static final int CALLBACK_FLAG_REQUEST_DISCOVERY = 1 << 2;
+
+ /**
+ * Flag for {@link #addCallback}: Request passive route discovery while this
+ * callback is registered, even on {@link ActivityManager#isLowRamDevice low-RAM devices}.
+ * <p class="note">
+ * This flag has a significant performance impact on low-RAM devices
+ * since it may cause many media route providers to be started simultaneously.
+ * It is much better to use {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} instead to avoid
+ * performing passive discovery on these devices altogether. Refer to
+ * {@link #addCallback(MediaRouteSelector, Callback, int) addCallback} for details.
+ * </p>
+ *
+ * @see android.support.v7.app.MediaRouteDiscoveryFragment
+ */
+ public static final int CALLBACK_FLAG_FORCE_DISCOVERY = 1 << 3;
+
+ /**
+ * Flag for {@link #isRouteAvailable}: Ignore the default route.
+ * <p>
+ * This flag is used to determine whether a matching non-default route is available.
+ * This constraint may be used to decide whether to offer the route chooser dialog
+ * to the user. There is no point offering the chooser if there are no
+ * non-default choices.
+ * </p>
+ */
+ public static final int AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE = 1 << 0;
+
+ /**
+ * Flag for {@link #isRouteAvailable}: Require an actual route to be matched.
+ * <p>
+ * If this flag is not set, then {@link #isRouteAvailable} will return true
+ * if it is possible to discover a matching route even if discovery is not in
+ * progress or if no matching route has yet been found. This feature is used to
+ * save resources by removing the need to perform passive route discovery on
+ * {@link ActivityManager#isLowRamDevice low-RAM devices}.
+ * </p><p>
+ * If this flag is set, then {@link #isRouteAvailable} will only return true if
+ * a matching route has actually been discovered.
+ * </p>
+ */
+ public static final int AVAILABILITY_FLAG_REQUIRE_MATCH = 1 << 1;
+
+ private MediaRouter(Context context) {
+ mContext = context;
+ }
+
+ /**
+ * Gets an instance of the media router service associated with the context.
+ * <p>
+ * The application is responsible for holding a strong reference to the returned
+ * {@link MediaRouter} instance, such as by storing the instance in a field of
+ * the {@link android.app.Activity}, to ensure that the media router remains alive
+ * as long as the application is using its features.
+ * </p><p>
+ * In other words, the support library only holds a {@link WeakReference weak reference}
+ * to each media router instance. When there are no remaining strong references to the
+ * media router instance, all of its callbacks will be removed and route discovery
+ * will no longer be performed on its behalf.
+ * </p>
+ *
+ * @return The media router instance for the context. The application must hold
+ * a strong reference to this object as long as it is in use.
+ */
+ public static MediaRouter getInstance(@NonNull Context context) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+ checkCallingThread();
+
+ if (sGlobal == null) {
+ sGlobal = new GlobalMediaRouter(context.getApplicationContext());
+ sGlobal.start();
+ }
+ return sGlobal.getRouter(context);
+ }
+
+ /**
+ * Gets information about the {@link MediaRouter.RouteInfo routes} currently known to
+ * this media router.
+ */
+ public List<RouteInfo> getRoutes() {
+ checkCallingThread();
+ return sGlobal.getRoutes();
+ }
+
+ /**
+ * Gets information about the {@link MediaRouter.ProviderInfo route providers}
+ * currently known to this media router.
+ */
+ public List<ProviderInfo> getProviders() {
+ checkCallingThread();
+ return sGlobal.getProviders();
+ }
+
+ /**
+ * Gets the default route for playing media content on the system.
+ * <p>
+ * The system always provides a default route.
+ * </p>
+ *
+ * @return The default route, which is guaranteed to never be null.
+ */
+ @NonNull
+ public RouteInfo getDefaultRoute() {
+ checkCallingThread();
+ return sGlobal.getDefaultRoute();
+ }
+
+ /**
+ * Gets a bluetooth route for playing media content on the system.
+ *
+ * @return A bluetooth route, if exist, otherwise null.
+ */
+ public RouteInfo getBluetoothRoute() {
+ checkCallingThread();
+ return sGlobal.getBluetoothRoute();
+ }
+
+ /**
+ * Gets the currently selected route.
+ * <p>
+ * The application should examine the route's
+ * {@link RouteInfo#getControlFilters media control intent filters} to assess the
+ * capabilities of the route before attempting to use it.
+ * </p>
+ *
+ * <h3>Example</h3>
+ * <pre>
+ * public boolean playMovie() {
+ * MediaRouter mediaRouter = MediaRouter.getInstance(context);
+ * MediaRouter.RouteInfo route = mediaRouter.getSelectedRoute();
+ *
+ * // First try using the remote playback interface, if supported.
+ * if (route.supportsControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)) {
+ * // The route supports remote playback.
+ * // Try to send it the Uri of the movie to play.
+ * Intent intent = new Intent(MediaControlIntent.ACTION_PLAY);
+ * intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ * intent.setDataAndType("http://example.com/videos/movie.mp4", "video/mp4");
+ * if (route.supportsControlRequest(intent)) {
+ * route.sendControlRequest(intent, null);
+ * return true; // sent the request to play the movie
+ * }
+ * }
+ *
+ * // If remote playback was not possible, then play locally.
+ * if (route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO)) {
+ * // The route supports live video streaming.
+ * // Prepare to play content locally in a window or in a presentation.
+ * return playMovieInWindow();
+ * }
+ *
+ * // Neither interface is supported, so we can't play the movie to this route.
+ * return false;
+ * }
+ * </pre>
+ *
+ * @return The selected route, which is guaranteed to never be null.
+ *
+ * @see RouteInfo#getControlFilters
+ * @see RouteInfo#supportsControlCategory
+ * @see RouteInfo#supportsControlRequest
+ */
+ @NonNull
+ public RouteInfo getSelectedRoute() {
+ checkCallingThread();
+ return sGlobal.getSelectedRoute();
+ }
+
+ /**
+ * Returns the selected route if it matches the specified selector, otherwise
+ * selects the default route and returns it. If there is one live audio route
+ * (usually Bluetooth A2DP), it will be selected instead of default route.
+ *
+ * @param selector The selector to match.
+ * @return The previously selected route if it matched the selector, otherwise the
+ * newly selected default route which is guaranteed to never be null.
+ *
+ * @see MediaRouteSelector
+ * @see RouteInfo#matchesSelector
+ */
+ @NonNull
+ public RouteInfo updateSelectedRoute(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "updateSelectedRoute: " + selector);
+ }
+ RouteInfo route = sGlobal.getSelectedRoute();
+ if (!route.isDefaultOrBluetooth() && !route.matchesSelector(selector)) {
+ route = sGlobal.chooseFallbackRoute();
+ sGlobal.selectRoute(route);
+ }
+ return route;
+ }
+
+ /**
+ * Selects the specified route.
+ *
+ * @param route The route to select.
+ */
+ public void selectRoute(@NonNull RouteInfo route) {
+ if (route == null) {
+ throw new IllegalArgumentException("route must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "selectRoute: " + route);
+ }
+ sGlobal.selectRoute(route);
+ }
+
+ /**
+ * Unselects the current round and selects the default route instead.
+ * <p>
+ * The reason given must be one of:
+ * <ul>
+ * <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li>
+ * </ul>
+ *
+ * @param reason The reason for disconnecting the current route.
+ */
+ public void unselect(int reason) {
+ if (reason < MediaRouter.UNSELECT_REASON_UNKNOWN ||
+ reason > MediaRouter.UNSELECT_REASON_ROUTE_CHANGED) {
+ throw new IllegalArgumentException("Unsupported reason to unselect route");
+ }
+ checkCallingThread();
+
+ // Choose the fallback route if it's not already selected.
+ // Otherwise, select the default route.
+ RouteInfo fallbackRoute = sGlobal.chooseFallbackRoute();
+ if (sGlobal.getSelectedRoute() != fallbackRoute) {
+ sGlobal.selectRoute(fallbackRoute, reason);
+ } else {
+ sGlobal.selectRoute(sGlobal.getDefaultRoute(), reason);
+ }
+ }
+
+ /**
+ * Returns true if there is a route that matches the specified selector.
+ * <p>
+ * This method returns true if there are any available routes that match the
+ * selector regardless of whether they are enabled or disabled. If the
+ * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} flag is specified, then
+ * the method will only consider non-default routes.
+ * </p>
+ * <p class="note">
+ * On {@link ActivityManager#isLowRamDevice low-RAM devices} this method
+ * will return true if it is possible to discover a matching route even if
+ * discovery is not in progress or if no matching route has yet been found.
+ * Use {@link #AVAILABILITY_FLAG_REQUIRE_MATCH} to require an actual match.
+ * </p>
+ *
+ * @param selector The selector to match.
+ * @param flags Flags to control the determination of whether a route may be
+ * available. May be zero or some combination of
+ * {@link #AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE} and
+ * {@link #AVAILABILITY_FLAG_REQUIRE_MATCH}.
+ * @return True if a matching route may be available.
+ */
+ public boolean isRouteAvailable(@NonNull MediaRouteSelector selector, int flags) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+ checkCallingThread();
+
+ return sGlobal.isRouteAvailable(selector, flags);
+ }
+
+ /**
+ * Registers a callback to discover routes that match the selector and to receive
+ * events when they change.
+ * <p>
+ * This is a convenience method that has the same effect as calling
+ * {@link #addCallback(MediaRouteSelector, Callback, int)} without flags.
+ * </p>
+ *
+ * @param selector A route selector that indicates the kinds of routes that the
+ * callback would like to discover.
+ * @param callback The callback to add.
+ * @see #removeCallback
+ */
+ public void addCallback(MediaRouteSelector selector, Callback callback) {
+ addCallback(selector, callback, 0);
+ }
+
+ /**
+ * Registers a callback to discover routes that match the selector and to receive
+ * events when they change.
+ * <p>
+ * The selector describes the kinds of routes that the application wants to
+ * discover. For example, if the application wants to use
+ * live audio routes then it should include the
+ * {@link MediaControlIntent#CATEGORY_LIVE_AUDIO live audio media control intent category}
+ * in its selector when it adds a callback to the media router.
+ * The selector may include any number of categories.
+ * </p><p>
+ * If the callback has already been registered, then the selector is added to
+ * the set of selectors being monitored by the callback.
+ * </p><p>
+ * By default, the callback will only be invoked for events that affect routes
+ * that match the specified selector. Event filtering may be disabled by specifying
+ * the {@link #CALLBACK_FLAG_UNFILTERED_EVENTS} flag when the callback is registered.
+ * </p><p>
+ * Applications should use the {@link #isRouteAvailable} method to determine
+ * whether is it possible to discover a route with the desired capabilities
+ * and therefore whether the media route button should be shown to the user.
+ * </p><p>
+ * The {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} flag should be used while the application
+ * is in the foreground to request that passive discovery be performed if there are
+ * sufficient resources to allow continuous passive discovery.
+ * On {@link ActivityManager#isLowRamDevice low-RAM devices} this flag will be
+ * ignored to conserve resources.
+ * </p><p>
+ * The {@link #CALLBACK_FLAG_FORCE_DISCOVERY} flag should be used when
+ * passive discovery absolutely must be performed, even on low-RAM devices.
+ * This flag has a significant performance impact on low-RAM devices
+ * since it may cause many media route providers to be started simultaneously.
+ * It is much better to use {@link #CALLBACK_FLAG_REQUEST_DISCOVERY} instead to avoid
+ * performing passive discovery on these devices altogether.
+ * </p><p>
+ * The {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} flag should be used when the
+ * media route chooser dialog is showing to confirm the presence of available
+ * routes that the user may connect to. This flag may use substantially more
+ * power.
+ * </p>
+ *
+ * <h3>Example</h3>
+ * <pre>
+ * public class MyActivity extends Activity {
+ * private MediaRouter mRouter;
+ * private MediaRouter.Callback mCallback;
+ * private MediaRouteSelector mSelector;
+ *
+ * protected void onCreate(Bundle savedInstanceState) {
+ * super.onCreate(savedInstanceState);
+ *
+ * mRouter = Mediarouter.getInstance(this);
+ * mCallback = new MyCallback();
+ * mSelector = new MediaRouteSelector.Builder()
+ * .addControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
+ * .addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
+ * .build();
+ * }
+ *
+ * // Add the callback on start to tell the media router what kinds of routes
+ * // the application is interested in so that it can try to discover suitable ones.
+ * public void onStart() {
+ * super.onStart();
+ *
+ * mediaRouter.addCallback(mSelector, mCallback,
+ * MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY);
+ *
+ * MediaRouter.RouteInfo route = mediaRouter.updateSelectedRoute(mSelector);
+ * // do something with the route...
+ * }
+ *
+ * // Remove the selector on stop to tell the media router that it no longer
+ * // needs to invest effort trying to discover routes of these kinds for now.
+ * public void onStop() {
+ * super.onStop();
+ *
+ * mediaRouter.removeCallback(mCallback);
+ * }
+ *
+ * private final class MyCallback extends MediaRouter.Callback {
+ * // Implement callback methods as needed.
+ * }
+ * }
+ * </pre>
+ *
+ * @param selector A route selector that indicates the kinds of routes that the
+ * callback would like to discover.
+ * @param callback The callback to add.
+ * @param flags Flags to control the behavior of the callback.
+ * May be zero or a combination of {@link #CALLBACK_FLAG_PERFORM_ACTIVE_SCAN} and
+ * {@link #CALLBACK_FLAG_UNFILTERED_EVENTS}.
+ * @see #removeCallback
+ */
+ public void addCallback(@NonNull MediaRouteSelector selector, @NonNull Callback callback,
+ @CallbackFlags int flags) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "addCallback: selector=" + selector
+ + ", callback=" + callback + ", flags=" + Integer.toHexString(flags));
+ }
+
+ CallbackRecord record;
+ int index = findCallbackRecord(callback);
+ if (index < 0) {
+ record = new CallbackRecord(this, callback);
+ mCallbackRecords.add(record);
+ } else {
+ record = mCallbackRecords.get(index);
+ }
+ boolean updateNeeded = false;
+ if ((flags & ~record.mFlags) != 0) {
+ record.mFlags |= flags;
+ updateNeeded = true;
+ }
+ if (!record.mSelector.contains(selector)) {
+ record.mSelector = new MediaRouteSelector.Builder(record.mSelector)
+ .addSelector(selector)
+ .build();
+ updateNeeded = true;
+ }
+ if (updateNeeded) {
+ sGlobal.updateDiscoveryRequest();
+ }
+ }
+
+ /**
+ * Removes the specified callback. It will no longer receive events about
+ * changes to media routes.
+ *
+ * @param callback The callback to remove.
+ * @see #addCallback
+ */
+ public void removeCallback(@NonNull Callback callback) {
+ if (callback == null) {
+ throw new IllegalArgumentException("callback must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "removeCallback: callback=" + callback);
+ }
+
+ int index = findCallbackRecord(callback);
+ if (index >= 0) {
+ mCallbackRecords.remove(index);
+ sGlobal.updateDiscoveryRequest();
+ }
+ }
+
+ private int findCallbackRecord(Callback callback) {
+ final int count = mCallbackRecords.size();
+ for (int i = 0; i < count; i++) {
+ if (mCallbackRecords.get(i).mCallback == callback) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Registers a media route provider within this application process.
+ * <p>
+ * The provider will be added to the list of providers that all {@link MediaRouter}
+ * instances within this process can use to discover routes.
+ * </p>
+ *
+ * @param providerInstance The media route provider instance to add.
+ *
+ * @see MediaRouteProvider
+ * @see #removeCallback
+ */
+ public void addProvider(@NonNull MediaRouteProvider providerInstance) {
+ if (providerInstance == null) {
+ throw new IllegalArgumentException("providerInstance must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "addProvider: " + providerInstance);
+ }
+ sGlobal.addProvider(providerInstance);
+ }
+
+ /**
+ * Unregisters a media route provider within this application process.
+ * <p>
+ * The provider will be removed from the list of providers that all {@link MediaRouter}
+ * instances within this process can use to discover routes.
+ * </p>
+ *
+ * @param providerInstance The media route provider instance to remove.
+ *
+ * @see MediaRouteProvider
+ * @see #addCallback
+ */
+ public void removeProvider(@NonNull MediaRouteProvider providerInstance) {
+ if (providerInstance == null) {
+ throw new IllegalArgumentException("providerInstance must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "removeProvider: " + providerInstance);
+ }
+ sGlobal.removeProvider(providerInstance);
+ }
+
+ /**
+ * Adds a remote control client to enable remote control of the volume
+ * of the selected route.
+ * <p>
+ * The remote control client must have previously been registered with
+ * the audio manager using the {@link android.media.AudioManager#registerRemoteControlClient
+ * AudioManager.registerRemoteControlClient} method.
+ * </p>
+ *
+ * @param remoteControlClient The {@link android.media.RemoteControlClient} to register.
+ */
+ public void addRemoteControlClient(@NonNull Object remoteControlClient) {
+ if (remoteControlClient == null) {
+ throw new IllegalArgumentException("remoteControlClient must not be null");
+ }
+ checkCallingThread();
+
+ if (DEBUG) {
+ Log.d(TAG, "addRemoteControlClient: " + remoteControlClient);
+ }
+ sGlobal.addRemoteControlClient(remoteControlClient);
+ }
+
+ /**
+ * Removes a remote control client.
+ *
+ * @param remoteControlClient The {@link android.media.RemoteControlClient}
+ * to unregister.
+ */
+ public void removeRemoteControlClient(@NonNull Object remoteControlClient) {
+ if (remoteControlClient == null) {
+ throw new IllegalArgumentException("remoteControlClient must not be null");
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "removeRemoteControlClient: " + remoteControlClient);
+ }
+ sGlobal.removeRemoteControlClient(remoteControlClient);
+ }
+
+ /**
+ * Sets the media session to enable remote control of the volume of the
+ * selected route. This should be used instead of
+ * {@link #addRemoteControlClient} when using media sessions. Set the
+ * session to null to clear it.
+ *
+ * @param mediaSession The {@link android.media.session.MediaSession} to
+ * use.
+ */
+ public void setMediaSession(Object mediaSession) {
+ if (DEBUG) {
+ Log.d(TAG, "addMediaSession: " + mediaSession);
+ }
+ sGlobal.setMediaSession(mediaSession);
+ }
+
+ /**
+ * Sets a compat media session to enable remote control of the volume of the
+ * selected route. This should be used instead of
+ * {@link #addRemoteControlClient} when using {@link MediaSessionCompat}.
+ * Set the session to null to clear it.
+ *
+ * @param mediaSession
+ */
+ public void setMediaSessionCompat(MediaSessionCompat mediaSession) {
+ if (DEBUG) {
+ Log.d(TAG, "addMediaSessionCompat: " + mediaSession);
+ }
+ sGlobal.setMediaSessionCompat(mediaSession);
+ }
+
+ public MediaSessionCompat.Token getMediaSessionToken() {
+ return sGlobal.getMediaSessionToken();
+ }
+
+ /**
+ * Ensures that calls into the media router are on the correct thread.
+ * It pays to be a little paranoid when global state invariants are at risk.
+ */
+ static void checkCallingThread() {
+ if (Looper.myLooper() != Looper.getMainLooper()) {
+ throw new IllegalStateException("The media router service must only be "
+ + "accessed on the application's main thread.");
+ }
+ }
+
+ static <T> boolean equal(T a, T b) {
+ return a == b || (a != null && b != null && a.equals(b));
+ }
+
+ /**
+ * Provides information about a media route.
+ * <p>
+ * Each media route has a list of {@link MediaControlIntent media control}
+ * {@link #getControlFilters intent filters} that describe the capabilities of the
+ * route and the manner in which it is used and controlled.
+ * </p>
+ */
+ public static class RouteInfo {
+ private final ProviderInfo mProvider;
+ private final String mDescriptorId;
+ private final String mUniqueId;
+ private String mName;
+ private String mDescription;
+ private Uri mIconUri;
+ private boolean mEnabled;
+ private boolean mConnecting;
+ private int mConnectionState;
+ private boolean mCanDisconnect;
+ private final ArrayList<IntentFilter> mControlFilters = new ArrayList<>();
+ private int mPlaybackType;
+ private int mPlaybackStream;
+ private int mDeviceType;
+ private int mVolumeHandling;
+ private int mVolume;
+ private int mVolumeMax;
+ private Display mPresentationDisplay;
+ private int mPresentationDisplayId = PRESENTATION_DISPLAY_ID_NONE;
+ private Bundle mExtras;
+ private IntentSender mSettingsIntent;
+ MediaRouteDescriptor mDescriptor;
+
+ @IntDef({CONNECTION_STATE_DISCONNECTED, CONNECTION_STATE_CONNECTING,
+ CONNECTION_STATE_CONNECTED})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface ConnectionState {}
+
+ /**
+ * The default connection state indicating the route is disconnected.
+ *
+ * @see #getConnectionState
+ */
+ public static final int CONNECTION_STATE_DISCONNECTED = 0;
+
+ /**
+ * A connection state indicating the route is in the process of connecting and is not yet
+ * ready for use.
+ *
+ * @see #getConnectionState
+ */
+ public static final int CONNECTION_STATE_CONNECTING = 1;
+
+ /**
+ * A connection state indicating the route is connected.
+ *
+ * @see #getConnectionState
+ */
+ public static final int CONNECTION_STATE_CONNECTED = 2;
+
+ @IntDef({PLAYBACK_TYPE_LOCAL,PLAYBACK_TYPE_REMOTE})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface PlaybackType {}
+
+ /**
+ * The default playback type, "local", indicating the presentation of the media
+ * is happening on the same device (e.g. a phone, a tablet) as where it is
+ * controlled from.
+ *
+ * @see #getPlaybackType
+ */
+ public static final int PLAYBACK_TYPE_LOCAL = 0;
+
+ /**
+ * A playback type indicating the presentation of the media is happening on
+ * a different device (i.e. the remote device) than where it is controlled from.
+ *
+ * @see #getPlaybackType
+ */
+ public static final int PLAYBACK_TYPE_REMOTE = 1;
+
+ @IntDef({DEVICE_TYPE_UNKNOWN, DEVICE_TYPE_TV, DEVICE_TYPE_SPEAKER, DEVICE_TYPE_BLUETOOTH})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface DeviceType {}
+
+ /**
+ * The default receiver device type of the route indicating the type is unknown.
+ *
+ * @see #getDeviceType
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public static final int DEVICE_TYPE_UNKNOWN = 0;
+
+ /**
+ * A receiver device type of the route indicating the presentation of the media is happening
+ * on a TV.
+ *
+ * @see #getDeviceType
+ */
+ public static final int DEVICE_TYPE_TV = 1;
+
+ /**
+ * A receiver device type of the route indicating the presentation of the media is happening
+ * on a speaker.
+ *
+ * @see #getDeviceType
+ */
+ public static final int DEVICE_TYPE_SPEAKER = 2;
+
+ /**
+ * A receiver device type of the route indicating the presentation of the media is happening
+ * on a bluetooth device such as a bluetooth speaker.
+ *
+ * @see #getDeviceType
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public static final int DEVICE_TYPE_BLUETOOTH = 3;
+
+ @IntDef({PLAYBACK_VOLUME_FIXED,PLAYBACK_VOLUME_VARIABLE})
+ @Retention(RetentionPolicy.SOURCE)
+ private @interface PlaybackVolume {}
+
+ /**
+ * Playback information indicating the playback volume is fixed, i.e. it cannot be
+ * controlled from this object. An example of fixed playback volume is a remote player,
+ * playing over HDMI where the user prefers to control the volume on the HDMI sink, rather
+ * than attenuate at the source.
+ *
+ * @see #getVolumeHandling
+ */
+ public static final int PLAYBACK_VOLUME_FIXED = 0;
+
+ /**
+ * Playback information indicating the playback volume is variable and can be controlled
+ * from this object.
+ *
+ * @see #getVolumeHandling
+ */
+ public static final int PLAYBACK_VOLUME_VARIABLE = 1;
+
+ /**
+ * The default presentation display id indicating no presentation display is associated
+ * with the route.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public static final int PRESENTATION_DISPLAY_ID_NONE = -1;
+
+ static final int CHANGE_GENERAL = 1 << 0;
+ static final int CHANGE_VOLUME = 1 << 1;
+ static final int CHANGE_PRESENTATION_DISPLAY = 1 << 2;
+
+ // Should match to SystemMediaRouteProvider.PACKAGE_NAME.
+ static final String SYSTEM_MEDIA_ROUTE_PROVIDER_PACKAGE_NAME = "android";
+
+ RouteInfo(ProviderInfo provider, String descriptorId, String uniqueId) {
+ mProvider = provider;
+ mDescriptorId = descriptorId;
+ mUniqueId = uniqueId;
+ }
+
+ /**
+ * Gets information about the provider of this media route.
+ */
+ public ProviderInfo getProvider() {
+ return mProvider;
+ }
+
+ /**
+ * Gets the unique id of the route.
+ * <p>
+ * The route unique id functions as a stable identifier by which the route is known.
+ * For example, an application can use this id as a token to remember the
+ * selected route across restarts or to communicate its identity to a service.
+ * </p>
+ *
+ * @return The unique id of the route, never null.
+ */
+ @NonNull
+ public String getId() {
+ return mUniqueId;
+ }
+
+ /**
+ * Gets the user-visible name of the route.
+ * <p>
+ * The route name identifies the destination represented by the route.
+ * It may be a user-supplied name, an alias, or device serial number.
+ * </p>
+ *
+ * @return The user-visible name of a media route. This is the string presented
+ * to users who may select this as the active route.
+ */
+ public String getName() {
+ return mName;
+ }
+
+ /**
+ * Gets the user-visible description of the route.
+ * <p>
+ * The route description describes the kind of destination represented by the route.
+ * It may be a user-supplied string, a model number or brand of device.
+ * </p>
+ *
+ * @return The description of the route, or null if none.
+ */
+ @Nullable
+ public String getDescription() {
+ return mDescription;
+ }
+
+ /**
+ * Gets the URI of the icon representing this route.
+ * <p>
+ * This icon will be used in picker UIs if available.
+ * </p>
+ *
+ * @return The URI of the icon representing this route, or null if none.
+ */
+ public Uri getIconUri() {
+ return mIconUri;
+ }
+
+ /**
+ * Returns true if this route is enabled and may be selected.
+ *
+ * @return True if this route is enabled.
+ */
+ public boolean isEnabled() {
+ return mEnabled;
+ }
+
+ /**
+ * Returns true if the route is in the process of connecting and is not
+ * yet ready for use.
+ *
+ * @return True if this route is in the process of connecting.
+ */
+ public boolean isConnecting() {
+ return mConnecting;
+ }
+
+ /**
+ * Gets the connection state of the route.
+ *
+ * @return The connection state of this route: {@link #CONNECTION_STATE_DISCONNECTED},
+ * {@link #CONNECTION_STATE_CONNECTING}, or {@link #CONNECTION_STATE_CONNECTED}.
+ */
+ @ConnectionState
+ public int getConnectionState() {
+ return mConnectionState;
+ }
+
+ /**
+ * Returns true if this route is currently selected.
+ *
+ * @return True if this route is currently selected.
+ *
+ * @see MediaRouter#getSelectedRoute
+ */
+ public boolean isSelected() {
+ checkCallingThread();
+ return sGlobal.getSelectedRoute() == this;
+ }
+
+ /**
+ * Returns true if this route is the default route.
+ *
+ * @return True if this route is the default route.
+ *
+ * @see MediaRouter#getDefaultRoute
+ */
+ public boolean isDefault() {
+ checkCallingThread();
+ return sGlobal.getDefaultRoute() == this;
+ }
+
+ /**
+ * Returns true if this route is a bluetooth route.
+ *
+ * @return True if this route is a bluetooth route.
+ *
+ * @see MediaRouter#getBluetoothRoute
+ */
+ public boolean isBluetooth() {
+ checkCallingThread();
+ return sGlobal.getBluetoothRoute() == this;
+ }
+
+ /**
+ * Returns true if this route is the default route and the device speaker.
+ *
+ * @return True if this route is the default route and the device speaker.
+ */
+ public boolean isDeviceSpeaker() {
+ int defaultAudioRouteNameResourceId = Resources.getSystem().getIdentifier(
+ "default_audio_route_name", "string", "android");
+ return isDefault()
+ && Resources.getSystem().getText(defaultAudioRouteNameResourceId).equals(mName);
+ }
+
+ /**
+ * Gets a list of {@link MediaControlIntent media control intent} filters that
+ * describe the capabilities of this route and the media control actions that
+ * it supports.
+ *
+ * @return A list of intent filters that specifies the media control intents that
+ * this route supports.
+ *
+ * @see MediaControlIntent
+ * @see #supportsControlCategory
+ * @see #supportsControlRequest
+ */
+ public List<IntentFilter> getControlFilters() {
+ return mControlFilters;
+ }
+
+ /**
+ * Returns true if the route supports at least one of the capabilities
+ * described by a media route selector.
+ *
+ * @param selector The selector that specifies the capabilities to check.
+ * @return True if the route supports at least one of the capabilities
+ * described in the media route selector.
+ */
+ public boolean matchesSelector(@NonNull MediaRouteSelector selector) {
+ if (selector == null) {
+ throw new IllegalArgumentException("selector must not be null");
+ }
+ checkCallingThread();
+ return selector.matchesControlFilters(mControlFilters);
+ }
+
+ /**
+ * Returns true if the route supports the specified
+ * {@link MediaControlIntent media control} category.
+ * <p>
+ * Media control categories describe the capabilities of this route
+ * such as whether it supports live audio streaming or remote playback.
+ * </p>
+ *
+ * @param category A {@link MediaControlIntent media control} category
+ * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO},
+ * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO},
+ * {@link MediaControlIntent#CATEGORY_REMOTE_PLAYBACK}, or a provider-defined
+ * media control category.
+ * @return True if the route supports the specified intent category.
+ *
+ * @see MediaControlIntent
+ * @see #getControlFilters
+ */
+ public boolean supportsControlCategory(@NonNull String category) {
+ if (category == null) {
+ throw new IllegalArgumentException("category must not be null");
+ }
+ checkCallingThread();
+
+ int count = mControlFilters.size();
+ for (int i = 0; i < count; i++) {
+ if (mControlFilters.get(i).hasCategory(category)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the route supports the specified
+ * {@link MediaControlIntent media control} category and action.
+ * <p>
+ * Media control actions describe specific requests that an application
+ * can ask a route to perform.
+ * </p>
+ *
+ * @param category A {@link MediaControlIntent media control} category
+ * such as {@link MediaControlIntent#CATEGORY_LIVE_AUDIO},
+ * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO},
+ * {@link MediaControlIntent#CATEGORY_REMOTE_PLAYBACK}, or a provider-defined
+ * media control category.
+ * @param action A {@link MediaControlIntent media control} action
+ * such as {@link MediaControlIntent#ACTION_PLAY}.
+ * @return True if the route supports the specified intent action.
+ *
+ * @see MediaControlIntent
+ * @see #getControlFilters
+ */
+ public boolean supportsControlAction(@NonNull String category, @NonNull String action) {
+ if (category == null) {
+ throw new IllegalArgumentException("category must not be null");
+ }
+ if (action == null) {
+ throw new IllegalArgumentException("action must not be null");
+ }
+ checkCallingThread();
+
+ int count = mControlFilters.size();
+ for (int i = 0; i < count; i++) {
+ IntentFilter filter = mControlFilters.get(i);
+ if (filter.hasCategory(category) && filter.hasAction(action)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns true if the route supports the specified
+ * {@link MediaControlIntent media control} request.
+ * <p>
+ * Media control requests are used to request the route to perform
+ * actions such as starting remote playback of a media item.
+ * </p>
+ *
+ * @param intent A {@link MediaControlIntent media control intent}.
+ * @return True if the route can handle the specified intent.
+ *
+ * @see MediaControlIntent
+ * @see #getControlFilters
+ */
+ public boolean supportsControlRequest(@NonNull Intent intent) {
+ if (intent == null) {
+ throw new IllegalArgumentException("intent must not be null");
+ }
+ checkCallingThread();
+
+ ContentResolver contentResolver = sGlobal.getContentResolver();
+ int count = mControlFilters.size();
+ for (int i = 0; i < count; i++) {
+ if (mControlFilters.get(i).match(contentResolver, intent, true, TAG) >= 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Sends a {@link MediaControlIntent media control} request to be performed
+ * asynchronously by the route's destination.
+ * <p>
+ * Media control requests are used to request the route to perform
+ * actions such as starting remote playback of a media item.
+ * </p><p>
+ * This function may only be called on a selected route. Control requests
+ * sent to unselected routes will fail.
+ * </p>
+ *
+ * @param intent A {@link MediaControlIntent media control intent}.
+ * @param callback A {@link ControlRequestCallback} to invoke with the result
+ * of the request, or null if no result is required.
+ *
+ * @see MediaControlIntent
+ */
+ public void sendControlRequest(@NonNull Intent intent,
+ @Nullable ControlRequestCallback callback) {
+ if (intent == null) {
+ throw new IllegalArgumentException("intent must not be null");
+ }
+ checkCallingThread();
+
+ sGlobal.sendControlRequest(this, intent, callback);
+ }
+
+ /**
+ * Gets the type of playback associated with this route.
+ *
+ * @return The type of playback associated with this route: {@link #PLAYBACK_TYPE_LOCAL}
+ * or {@link #PLAYBACK_TYPE_REMOTE}.
+ */
+ @PlaybackType
+ public int getPlaybackType() {
+ return mPlaybackType;
+ }
+
+ /**
+ * Gets the audio stream over which the playback associated with this route is performed.
+ *
+ * @return The stream over which the playback associated with this route is performed.
+ */
+ public int getPlaybackStream() {
+ return mPlaybackStream;
+ }
+
+ /**
+ * Gets the type of the receiver device associated with this route.
+ *
+ * @return The type of the receiver device associated with this route:
+ * {@link #DEVICE_TYPE_TV} or {@link #DEVICE_TYPE_SPEAKER}.
+ */
+ public int getDeviceType() {
+ return mDeviceType;
+ }
+
+
+ /**
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public boolean isDefaultOrBluetooth() {
+ if (isDefault() || mDeviceType == DEVICE_TYPE_BLUETOOTH) {
+ return true;
+ }
+ // This is a workaround for platform version 23 or below where the system route
+ // provider doesn't specify device type for bluetooth media routes.
+ return isSystemMediaRouteProvider(this)
+ && supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
+ && !supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+ }
+
+ /**
+ * Returns {@code true} if the route is selectable.
+ */
+ boolean isSelectable() {
+ // This tests whether the route is still valid and enabled.
+ // The route descriptor field is set to null when the route is removed.
+ return mDescriptor != null && mEnabled;
+ }
+
+ private static boolean isSystemMediaRouteProvider(MediaRouter.RouteInfo route) {
+ return TextUtils.equals(route.getProviderInstance().getMetadata().getPackageName(),
+ SYSTEM_MEDIA_ROUTE_PROVIDER_PACKAGE_NAME);
+ }
+
+ /**
+ * Gets information about how volume is handled on the route.
+ *
+ * @return How volume is handled on the route: {@link #PLAYBACK_VOLUME_FIXED}
+ * or {@link #PLAYBACK_VOLUME_VARIABLE}.
+ */
+ @PlaybackVolume
+ public int getVolumeHandling() {
+ return mVolumeHandling;
+ }
+
+ /**
+ * Gets the current volume for this route. Depending on the route, this may only
+ * be valid if the route is currently selected.
+ *
+ * @return The volume at which the playback associated with this route is performed.
+ */
+ public int getVolume() {
+ return mVolume;
+ }
+
+ /**
+ * Gets the maximum volume at which the playback associated with this route is performed.
+ *
+ * @return The maximum volume at which the playback associated with
+ * this route is performed.
+ */
+ public int getVolumeMax() {
+ return mVolumeMax;
+ }
+
+ /**
+ * Gets whether this route supports disconnecting without interrupting
+ * playback.
+ *
+ * @return True if this route can disconnect without stopping playback,
+ * false otherwise.
+ */
+ public boolean canDisconnect() {
+ return mCanDisconnect;
+ }
+
+ /**
+ * Requests a volume change for this route asynchronously.
+ * <p>
+ * This function may only be called on a selected route. It will have
+ * no effect if the route is currently unselected.
+ * </p>
+ *
+ * @param volume The new volume value between 0 and {@link #getVolumeMax}.
+ */
+ public void requestSetVolume(int volume) {
+ checkCallingThread();
+ sGlobal.requestSetVolume(this, Math.min(mVolumeMax, Math.max(0, volume)));
+ }
+
+ /**
+ * Requests an incremental volume update for this route asynchronously.
+ * <p>
+ * This function may only be called on a selected route. It will have
+ * no effect if the route is currently unselected.
+ * </p>
+ *
+ * @param delta The delta to add to the current volume.
+ */
+ public void requestUpdateVolume(int delta) {
+ checkCallingThread();
+ if (delta != 0) {
+ sGlobal.requestUpdateVolume(this, delta);
+ }
+ }
+
+ /**
+ * Gets the {@link Display} that should be used by the application to show
+ * a {@link android.app.Presentation} on an external display when this route is selected.
+ * Depending on the route, this may only be valid if the route is currently
+ * selected.
+ * <p>
+ * The preferred presentation display may change independently of the route
+ * being selected or unselected. For example, the presentation display
+ * of the default system route may change when an external HDMI display is connected
+ * or disconnected even though the route itself has not changed.
+ * </p><p>
+ * This method may return null if there is no external display associated with
+ * the route or if the display is not ready to show UI yet.
+ * </p><p>
+ * The application should listen for changes to the presentation display
+ * using the {@link Callback#onRoutePresentationDisplayChanged} callback and
+ * show or dismiss its {@link android.app.Presentation} accordingly when the display
+ * becomes available or is removed.
+ * </p><p>
+ * This method only makes sense for
+ * {@link MediaControlIntent#CATEGORY_LIVE_VIDEO live video} routes.
+ * </p>
+ *
+ * @return The preferred presentation display to use when this route is
+ * selected or null if none.
+ *
+ * @see MediaControlIntent#CATEGORY_LIVE_VIDEO
+ * @see android.app.Presentation
+ */
+ @Nullable
+ public Display getPresentationDisplay() {
+ checkCallingThread();
+ if (mPresentationDisplayId >= 0 && mPresentationDisplay == null) {
+ mPresentationDisplay = sGlobal.getDisplay(mPresentationDisplayId);
+ }
+ return mPresentationDisplay;
+ }
+
+ /**
+ * Gets the route's presentation display id, or -1 if none.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public int getPresentationDisplayId() {
+ return mPresentationDisplayId;
+ }
+
+ /**
+ * Gets a collection of extra properties about this route that were supplied
+ * by its media route provider, or null if none.
+ */
+ @Nullable
+ public Bundle getExtras() {
+ return mExtras;
+ }
+
+ /**
+ * Gets an intent sender for launching a settings activity for this
+ * route.
+ */
+ @Nullable
+ public IntentSender getSettingsIntent() {
+ return mSettingsIntent;
+ }
+
+ /**
+ * Selects this media route.
+ */
+ public void select() {
+ checkCallingThread();
+ sGlobal.selectRoute(this);
+ }
+
+ @Override
+ public String toString() {
+ return "MediaRouter.RouteInfo{ uniqueId=" + mUniqueId
+ + ", name=" + mName
+ + ", description=" + mDescription
+ + ", iconUri=" + mIconUri
+ + ", enabled=" + mEnabled
+ + ", connecting=" + mConnecting
+ + ", connectionState=" + mConnectionState
+ + ", canDisconnect=" + mCanDisconnect
+ + ", playbackType=" + mPlaybackType
+ + ", playbackStream=" + mPlaybackStream
+ + ", deviceType=" + mDeviceType
+ + ", volumeHandling=" + mVolumeHandling
+ + ", volume=" + mVolume
+ + ", volumeMax=" + mVolumeMax
+ + ", presentationDisplayId=" + mPresentationDisplayId
+ + ", extras=" + mExtras
+ + ", settingsIntent=" + mSettingsIntent
+ + ", providerPackageName=" + mProvider.getPackageName()
+ + " }";
+ }
+
+ int maybeUpdateDescriptor(MediaRouteDescriptor descriptor) {
+ int changes = 0;
+ if (mDescriptor != descriptor) {
+ changes = updateDescriptor(descriptor);
+ }
+ return changes;
+ }
+
+ int updateDescriptor(MediaRouteDescriptor descriptor) {
+ int changes = 0;
+ mDescriptor = descriptor;
+ if (descriptor != null) {
+ if (!equal(mName, descriptor.getName())) {
+ mName = descriptor.getName();
+ changes |= CHANGE_GENERAL;
+ }
+ if (!equal(mDescription, descriptor.getDescription())) {
+ mDescription = descriptor.getDescription();
+ changes |= CHANGE_GENERAL;
+ }
+ if (!equal(mIconUri, descriptor.getIconUri())) {
+ mIconUri = descriptor.getIconUri();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mEnabled != descriptor.isEnabled()) {
+ mEnabled = descriptor.isEnabled();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mConnecting != descriptor.isConnecting()) {
+ mConnecting = descriptor.isConnecting();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mConnectionState != descriptor.getConnectionState()) {
+ mConnectionState = descriptor.getConnectionState();
+ changes |= CHANGE_GENERAL;
+ }
+ if (!mControlFilters.equals(descriptor.getControlFilters())) {
+ mControlFilters.clear();
+ mControlFilters.addAll(descriptor.getControlFilters());
+ changes |= CHANGE_GENERAL;
+ }
+ if (mPlaybackType != descriptor.getPlaybackType()) {
+ mPlaybackType = descriptor.getPlaybackType();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mPlaybackStream != descriptor.getPlaybackStream()) {
+ mPlaybackStream = descriptor.getPlaybackStream();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mDeviceType != descriptor.getDeviceType()) {
+ mDeviceType = descriptor.getDeviceType();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mVolumeHandling != descriptor.getVolumeHandling()) {
+ mVolumeHandling = descriptor.getVolumeHandling();
+ changes |= CHANGE_GENERAL | CHANGE_VOLUME;
+ }
+ if (mVolume != descriptor.getVolume()) {
+ mVolume = descriptor.getVolume();
+ changes |= CHANGE_GENERAL | CHANGE_VOLUME;
+ }
+ if (mVolumeMax != descriptor.getVolumeMax()) {
+ mVolumeMax = descriptor.getVolumeMax();
+ changes |= CHANGE_GENERAL | CHANGE_VOLUME;
+ }
+ if (mPresentationDisplayId != descriptor.getPresentationDisplayId()) {
+ mPresentationDisplayId = descriptor.getPresentationDisplayId();
+ mPresentationDisplay = null;
+ changes |= CHANGE_GENERAL | CHANGE_PRESENTATION_DISPLAY;
+ }
+ if (!equal(mExtras, descriptor.getExtras())) {
+ mExtras = descriptor.getExtras();
+ changes |= CHANGE_GENERAL;
+ }
+ if (!equal(mSettingsIntent, descriptor.getSettingsActivity())) {
+ mSettingsIntent = descriptor.getSettingsActivity();
+ changes |= CHANGE_GENERAL;
+ }
+ if (mCanDisconnect != descriptor.canDisconnectAndKeepPlaying()) {
+ mCanDisconnect = descriptor.canDisconnectAndKeepPlaying();
+ changes |= CHANGE_GENERAL | CHANGE_PRESENTATION_DISPLAY;
+ }
+ }
+ return changes;
+ }
+
+ String getDescriptorId() {
+ return mDescriptorId;
+ }
+
+ /** @hide */
+ // @RestrictTo(LIBRARY_GROUP)
+ public MediaRouteProvider getProviderInstance() {
+ return mProvider.getProviderInstance();
+ }
+ }
+
+ /**
+ * Information about a route that consists of multiple other routes in a group.
+ * @hide
+ */
+ // @RestrictTo(LIBRARY_GROUP)
+ public static class RouteGroup extends RouteInfo {
+ private List<RouteInfo> mRoutes = new ArrayList<>();
+
+ RouteGroup(ProviderInfo provider, String descriptorId, String uniqueId) {
+ super(provider, descriptorId, uniqueId);
+ }
+
+ /**
+ * @return The number of routes in this group
+ */
+ public int getRouteCount() {
+ return mRoutes.size();
+ }
+
+ /**
+ * Returns the route in this group at the specified index
+ *
+ * @param index Index to fetch
+ * @return The route at index
+ */
+ public RouteInfo getRouteAt(int index) {
+ return mRoutes.get(index);
+ }
+
+ /**
+ * Returns the routes in this group
+ *
+ * @return The list of the routes in this group
+ */
+ public List<RouteInfo> getRoutes() {
+ return mRoutes;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(super.toString());
+ sb.append('[');
+ final int count = mRoutes.size();
+ for (int i = 0; i < count; i++) {
+ if (i > 0) sb.append(", ");
+ sb.append(mRoutes.get(i));
+ }
+ sb.append(']');
+ return sb.toString();
+ }
+
+ @Override
+ int maybeUpdateDescriptor(MediaRouteDescriptor descriptor) {
+ boolean changed = false;
+ if (mDescriptor != descriptor) {
+ mDescriptor = descriptor;
+ if (descriptor != null) {
+ List<String> groupMemberIds = descriptor.getGroupMemberIds();
+ List<RouteInfo> routes = new ArrayList<>();
+ changed = groupMemberIds.size() != mRoutes.size();
+ for (String groupMemberId : groupMemberIds) {
+ String uniqueId = sGlobal.getUniqueId(getProvider(), groupMemberId);
+ RouteInfo groupMember = sGlobal.getRoute(uniqueId);
+ if (groupMember != null) {
+ routes.add(groupMember);
+ if (!changed && !mRoutes.contains(groupMember)) {
+ changed = true;
+ }
+ }
+ }
+ if (changed) {
+ mRoutes = routes;
+ }
+ }
+ }
+ return (changed ? CHANGE_GENERAL : 0) | super.updateDescriptor(descriptor);
+ }
+ }
+
+ /**
+ * Provides information about a media route provider.
+ * <p>
+ * This object may be used to determine which media route provider has
+ * published a particular route.
+ * </p>
+ */
+ public static final class ProviderInfo {
+ private final MediaRouteProvider mProviderInstance;
+ private final List<RouteInfo> mRoutes = new ArrayList<>();
+
+ private final ProviderMetadata mMetadata;
+ private MediaRouteProviderDescriptor mDescriptor;
+ private Resources mResources;
+ private boolean mResourcesNotAvailable;
+
+ ProviderInfo(MediaRouteProvider provider) {
+ mProviderInstance = provider;
+ mMetadata = provider.getMetadata();
+ }
+
+ /**
+ * Gets the provider's underlying {@link MediaRouteProvider} instance.
+ */
+ public MediaRouteProvider getProviderInstance() {
+ checkCallingThread();
+ return mProviderInstance;
+ }
+
+ /**
+ * Gets the package name of the media route provider.
+ */
+ public String getPackageName() {
+ return mMetadata.getPackageName();
+ }
+
+ /**
+ * Gets the component name of the media route provider.
+ */
+ public ComponentName getComponentName() {
+ return mMetadata.getComponentName();
+ }
+
+ /**
+ * Gets the {@link MediaRouter.RouteInfo routes} published by this route provider.
+ */
+ public List<RouteInfo> getRoutes() {
+ checkCallingThread();
+ return mRoutes;
+ }
+
+ Resources getResources() {
+ if (mResources == null && !mResourcesNotAvailable) {
+ String packageName = getPackageName();
+ Context context = sGlobal.getProviderContext(packageName);
+ if (context != null) {
+ mResources = context.getResources();
+ } else {
+ Log.w(TAG, "Unable to obtain resources for route provider package: "
+ + packageName);
+ mResourcesNotAvailable = true;
+ }
+ }
+ return mResources;
+ }
+
+ boolean updateDescriptor(MediaRouteProviderDescriptor descriptor) {
+ if (mDescriptor != descriptor) {
+ mDescriptor = descriptor;
+ return true;
+ }
+ return false;
+ }
+
+ int findRouteByDescriptorId(String id) {
+ final int count = mRoutes.size();
+ for (int i = 0; i < count; i++) {
+ if (mRoutes.get(i).mDescriptorId.equals(id)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public String toString() {
+ return "MediaRouter.RouteProviderInfo{ packageName=" + getPackageName()
+ + " }";
+ }
+ }
+
+ /**
+ * Interface for receiving events about media routing changes.
+ * All methods of this interface will be called from the application's main thread.
+ * <p>
+ * A Callback will only receive events relevant to routes that the callback
+ * was registered for unless the {@link MediaRouter#CALLBACK_FLAG_UNFILTERED_EVENTS}
+ * flag was specified in {@link MediaRouter#addCallback(MediaRouteSelector, Callback, int)}.
+ * </p>
+ *
+ * @see MediaRouter#addCallback(MediaRouteSelector, Callback, int)
+ * @see MediaRouter#removeCallback(Callback)
+ */
+ public static abstract class Callback {
+ /**
+ * Called when the supplied media route becomes selected as the active route.
+ *
+ * @param router The media router reporting the event.
+ * @param route The route that has been selected.
+ */
+ public void onRouteSelected(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when the supplied media route becomes unselected as the active route.
+ * For detailed reason, override {@link #onRouteUnselected(MediaRouter, RouteInfo, int)}
+ * instead.
+ *
+ * @param router The media router reporting the event.
+ * @param route The route that has been unselected.
+ */
+ public void onRouteUnselected(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when the supplied media route becomes unselected as the active route.
+ * The default implementation calls {@link #onRouteUnselected}.
+ * <p>
+ * The reason provided will be one of the following:
+ * <ul>
+ * <li>{@link MediaRouter#UNSELECT_REASON_UNKNOWN}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_DISCONNECTED}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_STOPPED}</li>
+ * <li>{@link MediaRouter#UNSELECT_REASON_ROUTE_CHANGED}</li>
+ * </ul>
+ *
+ * @param router The media router reporting the event.
+ * @param route The route that has been unselected.
+ * @param reason The reason for unselecting the route.
+ */
+ public void onRouteUnselected(MediaRouter router, RouteInfo route, int reason) {
+ onRouteUnselected(router, route);
+ }
+
+ /**
+ * Called when a media route has been added.
+ *
+ * @param router The media router reporting the event.
+ * @param route The route that has become available for use.
+ */
+ public void onRouteAdded(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when a media route has been removed.
+ *
+ * @param router The media router reporting the event.
+ * @param route The route that has been removed from availability.
+ */
+ public void onRouteRemoved(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when a property of the indicated media route has changed.
+ *
+ * @param router The media router reporting the event.
+ * @param route The route that was changed.
+ */
+ public void onRouteChanged(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when a media route's volume changes.
+ *
+ * @param router The media router reporting the event.
+ * @param route The route whose volume changed.
+ */
+ public void onRouteVolumeChanged(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when a media route's presentation display changes.
+ * <p>
+ * This method is called whenever the route's presentation display becomes
+ * available, is removed or has changes to some of its properties (such as its size).
+ * </p>
+ *
+ * @param router The media router reporting the event.
+ * @param route The route whose presentation display changed.
+ *
+ * @see RouteInfo#getPresentationDisplay()
+ */
+ public void onRoutePresentationDisplayChanged(MediaRouter router, RouteInfo route) {
+ }
+
+ /**
+ * Called when a media route provider has been added.
+ *
+ * @param router The media router reporting the event.
+ * @param provider The provider that has become available for use.
+ */
+ public void onProviderAdded(MediaRouter router, ProviderInfo provider) {
+ }
+
+ /**
+ * Called when a media route provider has been removed.
+ *
+ * @param router The media router reporting the event.
+ * @param provider The provider that has been removed from availability.
+ */
+ public void onProviderRemoved(MediaRouter router, ProviderInfo provider) {
+ }
+
+ /**
+ * Called when a property of the indicated media route provider has changed.
+ *
+ * @param router The media router reporting the event.
+ * @param provider The provider that was changed.
+ */
+ public void onProviderChanged(MediaRouter router, ProviderInfo provider) {
+ }
+ }
+
+ /**
+ * Callback which is invoked with the result of a media control request.
+ *
+ * @see RouteInfo#sendControlRequest
+ */
+ public static abstract class ControlRequestCallback {
+ /**
+ * Called when a media control request succeeds.
+ *
+ * @param data Result data, or null if none.
+ * Contents depend on the {@link MediaControlIntent media control action}.
+ */
+ public void onResult(Bundle data) {
+ }
+
+ /**
+ * Called when a media control request fails.
+ *
+ * @param error A localized error message which may be shown to the user, or null
+ * if the cause of the error is unclear.
+ * @param data Error data, or null if none.
+ * Contents depend on the {@link MediaControlIntent media control action}.
+ */
+ public void onError(String error, Bundle data) {
+ }
+ }
+
+ private static final class CallbackRecord {
+ public final MediaRouter mRouter;
+ public final Callback mCallback;
+ public MediaRouteSelector mSelector;
+ public int mFlags;
+
+ public CallbackRecord(MediaRouter router, Callback callback) {
+ mRouter = router;
+ mCallback = callback;
+ mSelector = MediaRouteSelector.EMPTY;
+ }
+
+ public boolean filterRouteEvent(RouteInfo route) {
+ return (mFlags & CALLBACK_FLAG_UNFILTERED_EVENTS) != 0
+ || route.matchesSelector(mSelector);
+ }
+ }
+
+ /**
+ * Global state for the media router.
+ * <p>
+ * Media routes and media route providers are global to the process; their
+ * state and the bulk of the media router implementation lives here.
+ * </p>
+ */
+ private static final class GlobalMediaRouter
+ implements SystemMediaRouteProvider.SyncCallback,
+ RegisteredMediaRouteProviderWatcher.Callback {
+ final Context mApplicationContext;
+ final ArrayList<WeakReference<MediaRouter>> mRouters = new ArrayList<>();
+ private final ArrayList<RouteInfo> mRoutes = new ArrayList<>();
+ private final Map<Pair<String, String>, String> mUniqueIdMap = new HashMap<>();
+ private final ArrayList<ProviderInfo> mProviders = new ArrayList<>();
+ private final ArrayList<RemoteControlClientRecord> mRemoteControlClients =
+ new ArrayList<>();
+ final RemoteControlClientCompat.PlaybackInfo mPlaybackInfo =
+ new RemoteControlClientCompat.PlaybackInfo();
+ private final ProviderCallback mProviderCallback = new ProviderCallback();
+ final CallbackHandler mCallbackHandler = new CallbackHandler();
+ private final DisplayManagerCompat mDisplayManager;
+ final SystemMediaRouteProvider mSystemProvider;
+ private final boolean mLowRam;
+
+ private RegisteredMediaRouteProviderWatcher mRegisteredProviderWatcher;
+ private RouteInfo mDefaultRoute;
+ private RouteInfo mBluetoothRoute;
+ RouteInfo mSelectedRoute;
+ private RouteController mSelectedRouteController;
+ // A map from route descriptor ID to RouteController for the member routes in the currently
+ // selected route group.
+ private final Map<String, RouteController> mRouteControllerMap = new HashMap<>();
+ private MediaRouteDiscoveryRequest mDiscoveryRequest;
+ private MediaSessionRecord mMediaSession;
+ MediaSessionCompat mRccMediaSession;
+ private MediaSessionCompat mCompatSession;
+ private MediaSessionCompat.OnActiveChangeListener mSessionActiveListener =
+ new MediaSessionCompat.OnActiveChangeListener() {
+ @Override
+ public void onActiveChanged() {
+ if(mRccMediaSession != null) {
+ if (mRccMediaSession.isActive()) {
+ addRemoteControlClient(mRccMediaSession.getRemoteControlClient());
+ } else {
+ removeRemoteControlClient(mRccMediaSession.getRemoteControlClient());
+ }
+ }
+ }
+ };
+
+ GlobalMediaRouter(Context applicationContext) {
+ mApplicationContext = applicationContext;
+ mDisplayManager = DisplayManagerCompat.getInstance(applicationContext);
+ mLowRam = ActivityManagerCompat.isLowRamDevice(
+ (ActivityManager)applicationContext.getSystemService(
+ Context.ACTIVITY_SERVICE));
+
+ // Add the system media route provider for interoperating with
+ // the framework media router. This one is special and receives
+ // synchronization messages from the media router.
+ mSystemProvider = SystemMediaRouteProvider.obtain(applicationContext, this);
+ }
+
+ public void start() {
+ addProvider(mSystemProvider);
+
+ // Start watching for routes published by registered media route
+ // provider services.
+ mRegisteredProviderWatcher = new RegisteredMediaRouteProviderWatcher(
+ mApplicationContext, this);
+ mRegisteredProviderWatcher.start();
+ }
+
+ public MediaRouter getRouter(Context context) {
+ MediaRouter router;
+ for (int i = mRouters.size(); --i >= 0; ) {
+ router = mRouters.get(i).get();
+ if (router == null) {
+ mRouters.remove(i);
+ } else if (router.mContext == context) {
+ return router;
+ }
+ }
+ router = new MediaRouter(context);
+ mRouters.add(new WeakReference<MediaRouter>(router));
+ return router;
+ }
+
+ public ContentResolver getContentResolver() {
+ return mApplicationContext.getContentResolver();
+ }
+
+ public Context getProviderContext(String packageName) {
+ if (packageName.equals(SystemMediaRouteProvider.PACKAGE_NAME)) {
+ return mApplicationContext;
+ }
+ try {
+ return mApplicationContext.createPackageContext(
+ packageName, Context.CONTEXT_RESTRICTED);
+ } catch (NameNotFoundException ex) {
+ return null;
+ }
+ }
+
+ public Display getDisplay(int displayId) {
+ return mDisplayManager.getDisplay(displayId);
+ }
+
+ public void sendControlRequest(RouteInfo route,
+ Intent intent, ControlRequestCallback callback) {
+ if (route == mSelectedRoute && mSelectedRouteController != null) {
+ if (mSelectedRouteController.onControlRequest(intent, callback)) {
+ return;
+ }
+ }
+ if (callback != null) {
+ callback.onError(null, null);
+ }
+ }
+
+ public void requestSetVolume(RouteInfo route, int volume) {
+ if (route == mSelectedRoute && mSelectedRouteController != null) {
+ mSelectedRouteController.onSetVolume(volume);
+ } else if (!mRouteControllerMap.isEmpty()) {
+ RouteController controller = mRouteControllerMap.get(route.mDescriptorId);
+ if (controller != null) {
+ controller.onSetVolume(volume);
+ }
+ }
+ }
+
+ public void requestUpdateVolume(RouteInfo route, int delta) {
+ if (route == mSelectedRoute && mSelectedRouteController != null) {
+ mSelectedRouteController.onUpdateVolume(delta);
+ }
+ }
+
+ public RouteInfo getRoute(String uniqueId) {
+ for (RouteInfo info : mRoutes) {
+ if (info.mUniqueId.equals(uniqueId)) {
+ return info;
+ }
+ }
+ return null;
+ }
+
+ public List<RouteInfo> getRoutes() {
+ return mRoutes;
+ }
+
+ List<ProviderInfo> getProviders() {
+ return mProviders;
+ }
+
+ @NonNull RouteInfo getDefaultRoute() {
+ if (mDefaultRoute == null) {
+ // This should never happen once the media router has been fully
+ // initialized but it is good to check for the error in case there
+ // is a bug in provider initialization.
+ throw new IllegalStateException("There is no default route. "
+ + "The media router has not yet been fully initialized.");
+ }
+ return mDefaultRoute;
+ }
+
+ RouteInfo getBluetoothRoute() {
+ return mBluetoothRoute;
+ }
+
+ @NonNull RouteInfo getSelectedRoute() {
+ if (mSelectedRoute == null) {
+ // This should never happen once the media router has been fully
+ // initialized but it is good to check for the error in case there
+ // is a bug in provider initialization.
+ throw new IllegalStateException("There is no currently selected route. "
+ + "The media router has not yet been fully initialized.");
+ }
+ return mSelectedRoute;
+ }
+
+ void selectRoute(@NonNull RouteInfo route) {
+ selectRoute(route, MediaRouter.UNSELECT_REASON_ROUTE_CHANGED);
+ }
+
+ void selectRoute(@NonNull RouteInfo route, int unselectReason) {
+ if (!mRoutes.contains(route)) {
+ Log.w(TAG, "Ignoring attempt to select removed route: " + route);
+ return;
+ }
+ if (!route.mEnabled) {
+ Log.w(TAG, "Ignoring attempt to select disabled route: " + route);
+ return;
+ }
+ setSelectedRouteInternal(route, unselectReason);
+ }
+
+ public boolean isRouteAvailable(MediaRouteSelector selector, int flags) {
+ if (selector.isEmpty()) {
+ return false;
+ }
+
+ // On low-RAM devices, do not rely on actual discovery results unless asked to.
+ if ((flags & AVAILABILITY_FLAG_REQUIRE_MATCH) == 0 && mLowRam) {
+ return true;
+ }
+
+ // Check whether any existing routes match the selector.
+ final int routeCount = mRoutes.size();
+ for (int i = 0; i < routeCount; i++) {
+ RouteInfo route = mRoutes.get(i);
+ if ((flags & AVAILABILITY_FLAG_IGNORE_DEFAULT_ROUTE) != 0
+ && route.isDefaultOrBluetooth()) {
+ continue;
+ }
+ if (route.matchesSelector(selector)) {
+ return true;
+ }
+ }
+
+ // It doesn't look like we can find a matching route right now.
+ return false;
+ }
+
+ public void updateDiscoveryRequest() {
+ // Combine all of the callback selectors and active scan flags.
+ boolean discover = false;
+ boolean activeScan = false;
+ MediaRouteSelector.Builder builder = new MediaRouteSelector.Builder();
+ for (int i = mRouters.size(); --i >= 0; ) {
+ MediaRouter router = mRouters.get(i).get();
+ if (router == null) {
+ mRouters.remove(i);
+ } else {
+ final int count = router.mCallbackRecords.size();
+ for (int j = 0; j < count; j++) {
+ CallbackRecord callback = router.mCallbackRecords.get(j);
+ builder.addSelector(callback.mSelector);
+ if ((callback.mFlags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) {
+ activeScan = true;
+ discover = true; // perform active scan implies request discovery
+ }
+ if ((callback.mFlags & CALLBACK_FLAG_REQUEST_DISCOVERY) != 0) {
+ if (!mLowRam) {
+ discover = true;
+ }
+ }
+ if ((callback.mFlags & CALLBACK_FLAG_FORCE_DISCOVERY) != 0) {
+ discover = true;
+ }
+ }
+ }
+ }
+ MediaRouteSelector selector = discover ? builder.build() : MediaRouteSelector.EMPTY;
+
+ // Create a new discovery request.
+ if (mDiscoveryRequest != null
+ && mDiscoveryRequest.getSelector().equals(selector)
+ && mDiscoveryRequest.isActiveScan() == activeScan) {
+ return; // no change
+ }
+ if (selector.isEmpty() && !activeScan) {
+ // Discovery is not needed.
+ if (mDiscoveryRequest == null) {
+ return; // no change
+ }
+ mDiscoveryRequest = null;
+ } else {
+ // Discovery is needed.
+ mDiscoveryRequest = new MediaRouteDiscoveryRequest(selector, activeScan);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Updated discovery request: " + mDiscoveryRequest);
+ }
+ if (discover && !activeScan && mLowRam) {
+ Log.i(TAG, "Forcing passive route discovery on a low-RAM device, "
+ + "system performance may be affected. Please consider using "
+ + "CALLBACK_FLAG_REQUEST_DISCOVERY instead of "
+ + "CALLBACK_FLAG_FORCE_DISCOVERY.");
+ }
+
+ // Notify providers.
+ final int providerCount = mProviders.size();
+ for (int i = 0; i < providerCount; i++) {
+ mProviders.get(i).mProviderInstance.setDiscoveryRequest(mDiscoveryRequest);
+ }
+ }
+
+ @Override
+ public void addProvider(MediaRouteProvider providerInstance) {
+ int index = findProviderInfo(providerInstance);
+ if (index < 0) {
+ // 1. Add the provider to the list.
+ ProviderInfo provider = new ProviderInfo(providerInstance);
+ mProviders.add(provider);
+ if (DEBUG) {
+ Log.d(TAG, "Provider added: " + provider);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_ADDED, provider);
+ // 2. Create the provider's contents.
+ updateProviderContents(provider, providerInstance.getDescriptor());
+ // 3. Register the provider callback.
+ providerInstance.setCallback(mProviderCallback);
+ // 4. Set the discovery request.
+ providerInstance.setDiscoveryRequest(mDiscoveryRequest);
+ }
+ }
+
+ @Override
+ public void removeProvider(MediaRouteProvider providerInstance) {
+ int index = findProviderInfo(providerInstance);
+ if (index >= 0) {
+ // 1. Unregister the provider callback.
+ providerInstance.setCallback(null);
+ // 2. Clear the discovery request.
+ providerInstance.setDiscoveryRequest(null);
+ // 3. Delete the provider's contents.
+ ProviderInfo provider = mProviders.get(index);
+ updateProviderContents(provider, null);
+ // 4. Remove the provider from the list.
+ if (DEBUG) {
+ Log.d(TAG, "Provider removed: " + provider);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_REMOVED, provider);
+ mProviders.remove(index);
+ }
+ }
+
+ void updateProviderDescriptor(MediaRouteProvider providerInstance,
+ MediaRouteProviderDescriptor descriptor) {
+ int index = findProviderInfo(providerInstance);
+ if (index >= 0) {
+ // Update the provider's contents.
+ ProviderInfo provider = mProviders.get(index);
+ updateProviderContents(provider, descriptor);
+ }
+ }
+
+ private int findProviderInfo(MediaRouteProvider providerInstance) {
+ final int count = mProviders.size();
+ for (int i = 0; i < count; i++) {
+ if (mProviders.get(i).mProviderInstance == providerInstance) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void updateProviderContents(ProviderInfo provider,
+ MediaRouteProviderDescriptor providerDescriptor) {
+ if (provider.updateDescriptor(providerDescriptor)) {
+ // Update all existing routes and reorder them to match
+ // the order of their descriptors.
+ int targetIndex = 0;
+ boolean selectedRouteDescriptorChanged = false;
+ if (providerDescriptor != null) {
+ if (providerDescriptor.isValid()) {
+ final List<MediaRouteDescriptor> routeDescriptors =
+ providerDescriptor.getRoutes();
+ final int routeCount = routeDescriptors.size();
+ // Updating route group's contents requires all member routes' information.
+ // Add the groups to the lists and update them later.
+ List<Pair<RouteInfo, MediaRouteDescriptor>> addedGroups = new ArrayList<>();
+ List<Pair<RouteInfo, MediaRouteDescriptor>> updatedGroups =
+ new ArrayList<>();
+ for (int i = 0; i < routeCount; i++) {
+ final MediaRouteDescriptor routeDescriptor = routeDescriptors.get(i);
+ final String id = routeDescriptor.getId();
+ final int sourceIndex = provider.findRouteByDescriptorId(id);
+ if (sourceIndex < 0) {
+ // 1. Add the route to the list.
+ String uniqueId = assignRouteUniqueId(provider, id);
+ boolean isGroup = routeDescriptor.getGroupMemberIds() != null;
+ RouteInfo route = isGroup ? new RouteGroup(provider, id, uniqueId) :
+ new RouteInfo(provider, id, uniqueId);
+ provider.mRoutes.add(targetIndex++, route);
+ mRoutes.add(route);
+ // 2. Create the route's contents.
+ if (isGroup) {
+ addedGroups.add(new Pair<>(route, routeDescriptor));
+ } else {
+ route.maybeUpdateDescriptor(routeDescriptor);
+ // 3. Notify clients about addition.
+ if (DEBUG) {
+ Log.d(TAG, "Route added: " + route);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route);
+ }
+
+ } else if (sourceIndex < targetIndex) {
+ Log.w(TAG, "Ignoring route descriptor with duplicate id: "
+ + routeDescriptor);
+ } else {
+ // 1. Reorder the route within the list.
+ RouteInfo route = provider.mRoutes.get(sourceIndex);
+ Collections.swap(provider.mRoutes,
+ sourceIndex, targetIndex++);
+ // 2. Update the route's contents.
+ if (route instanceof RouteGroup) {
+ updatedGroups.add(new Pair<>(route, routeDescriptor));
+ } else {
+ // 3. Notify clients about changes.
+ if (updateRouteDescriptorAndNotify(route, routeDescriptor)
+ != 0) {
+ if (route == mSelectedRoute) {
+ selectedRouteDescriptorChanged = true;
+ }
+ }
+ }
+ }
+ }
+ // Update the new and/or existing groups.
+ for (Pair<RouteInfo, MediaRouteDescriptor> pair : addedGroups) {
+ RouteInfo route = pair.first;
+ route.maybeUpdateDescriptor(pair.second);
+ if (DEBUG) {
+ Log.d(TAG, "Route added: " + route);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_ROUTE_ADDED, route);
+ }
+ for (Pair<RouteInfo, MediaRouteDescriptor> pair : updatedGroups) {
+ RouteInfo route = pair.first;
+ if (updateRouteDescriptorAndNotify(route, pair.second) != 0) {
+ if (route == mSelectedRoute) {
+ selectedRouteDescriptorChanged = true;
+ }
+ }
+ }
+ } else {
+ Log.w(TAG, "Ignoring invalid provider descriptor: " + providerDescriptor);
+ }
+ }
+
+ // Dispose all remaining routes that do not have matching descriptors.
+ for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) {
+ // 1. Delete the route's contents.
+ RouteInfo route = provider.mRoutes.get(i);
+ route.maybeUpdateDescriptor(null);
+ // 2. Remove the route from the list.
+ mRoutes.remove(route);
+ }
+
+ // Update the selected route if needed.
+ updateSelectedRouteIfNeeded(selectedRouteDescriptorChanged);
+
+ // Now notify clients about routes that were removed.
+ // We do this after updating the selected route to ensure
+ // that the framework media router observes the new route
+ // selection before the removal since removing the currently
+ // selected route may have side-effects.
+ for (int i = provider.mRoutes.size() - 1; i >= targetIndex; i--) {
+ RouteInfo route = provider.mRoutes.remove(i);
+ if (DEBUG) {
+ Log.d(TAG, "Route removed: " + route);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_ROUTE_REMOVED, route);
+ }
+
+ // Notify provider changed.
+ if (DEBUG) {
+ Log.d(TAG, "Provider changed: " + provider);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_PROVIDER_CHANGED, provider);
+ }
+ }
+
+ private int updateRouteDescriptorAndNotify(RouteInfo route,
+ MediaRouteDescriptor routeDescriptor) {
+ int changes = route.maybeUpdateDescriptor(routeDescriptor);
+ if (changes != 0) {
+ if ((changes & RouteInfo.CHANGE_GENERAL) != 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Route changed: " + route);
+ }
+ mCallbackHandler.post(
+ CallbackHandler.MSG_ROUTE_CHANGED, route);
+ }
+ if ((changes & RouteInfo.CHANGE_VOLUME) != 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Route volume changed: " + route);
+ }
+ mCallbackHandler.post(
+ CallbackHandler.MSG_ROUTE_VOLUME_CHANGED, route);
+ }
+ if ((changes & RouteInfo.CHANGE_PRESENTATION_DISPLAY) != 0) {
+ if (DEBUG) {
+ Log.d(TAG, "Route presentation display changed: "
+ + route);
+ }
+ mCallbackHandler.post(CallbackHandler.
+ MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED, route);
+ }
+ }
+ return changes;
+ }
+
+ private String assignRouteUniqueId(ProviderInfo provider, String routeDescriptorId) {
+ // Although route descriptor ids are unique within a provider, it's
+ // possible for there to be two providers with the same package name.
+ // Therefore we must dedupe the composite id.
+ String componentName = provider.getComponentName().flattenToShortString();
+ String uniqueId = componentName + ":" + routeDescriptorId;
+ if (findRouteByUniqueId(uniqueId) < 0) {
+ mUniqueIdMap.put(new Pair<>(componentName, routeDescriptorId), uniqueId);
+ return uniqueId;
+ }
+ Log.w(TAG, "Either " + routeDescriptorId + " isn't unique in " + componentName
+ + " or we're trying to assign a unique ID for an already added route");
+ for (int i = 2; ; i++) {
+ String newUniqueId = String.format(Locale.US, "%s_%d", uniqueId, i);
+ if (findRouteByUniqueId(newUniqueId) < 0) {
+ mUniqueIdMap.put(new Pair<>(componentName, routeDescriptorId), newUniqueId);
+ return newUniqueId;
+ }
+ }
+ }
+
+ private int findRouteByUniqueId(String uniqueId) {
+ final int count = mRoutes.size();
+ for (int i = 0; i < count; i++) {
+ if (mRoutes.get(i).mUniqueId.equals(uniqueId)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private String getUniqueId(ProviderInfo provider, String routeDescriptorId) {
+ String componentName = provider.getComponentName().flattenToShortString();
+ return mUniqueIdMap.get(new Pair<>(componentName, routeDescriptorId));
+ }
+
+ private void updateSelectedRouteIfNeeded(boolean selectedRouteDescriptorChanged) {
+ // Update default route.
+ if (mDefaultRoute != null && !mDefaultRoute.isSelectable()) {
+ Log.i(TAG, "Clearing the default route because it "
+ + "is no longer selectable: " + mDefaultRoute);
+ mDefaultRoute = null;
+ }
+ if (mDefaultRoute == null && !mRoutes.isEmpty()) {
+ for (RouteInfo route : mRoutes) {
+ if (isSystemDefaultRoute(route) && route.isSelectable()) {
+ mDefaultRoute = route;
+ Log.i(TAG, "Found default route: " + mDefaultRoute);
+ break;
+ }
+ }
+ }
+
+ // Update bluetooth route.
+ if (mBluetoothRoute != null && !mBluetoothRoute.isSelectable()) {
+ Log.i(TAG, "Clearing the bluetooth route because it "
+ + "is no longer selectable: " + mBluetoothRoute);
+ mBluetoothRoute = null;
+ }
+ if (mBluetoothRoute == null && !mRoutes.isEmpty()) {
+ for (RouteInfo route : mRoutes) {
+ if (isSystemLiveAudioOnlyRoute(route) && route.isSelectable()) {
+ mBluetoothRoute = route;
+ Log.i(TAG, "Found bluetooth route: " + mBluetoothRoute);
+ break;
+ }
+ }
+ }
+
+ // Update selected route.
+ if (mSelectedRoute == null || !mSelectedRoute.isSelectable()) {
+ Log.i(TAG, "Unselecting the current route because it "
+ + "is no longer selectable: " + mSelectedRoute);
+ setSelectedRouteInternal(chooseFallbackRoute(),
+ MediaRouter.UNSELECT_REASON_UNKNOWN);
+ } else if (selectedRouteDescriptorChanged) {
+ // In case the selected route is a route group, select/unselect route controllers
+ // for the added/removed route members.
+ if (mSelectedRoute instanceof RouteGroup) {
+ List<RouteInfo> routes = ((RouteGroup) mSelectedRoute).getRoutes();
+ // Build a set of descriptor IDs for the new route group.
+ Set<String> idSet = new HashSet<>();
+ for (RouteInfo route : routes) {
+ idSet.add(route.mDescriptorId);
+ }
+ // Unselect route controllers for the removed routes.
+ Iterator<Map.Entry<String, RouteController>> iter =
+ mRouteControllerMap.entrySet().iterator();
+ while (iter.hasNext()) {
+ Map.Entry<String, RouteController> entry = iter.next();
+ if (!idSet.contains(entry.getKey())) {
+ RouteController controller = entry.getValue();
+ controller.onUnselect();
+ controller.onRelease();
+ iter.remove();
+ }
+ }
+ // Select route controllers for the added routes.
+ for (RouteInfo route : routes) {
+ if (!mRouteControllerMap.containsKey(route.mDescriptorId)) {
+ RouteController controller = route.getProviderInstance()
+ .onCreateRouteController(
+ route.mDescriptorId, mSelectedRoute.mDescriptorId);
+ controller.onSelect();
+ mRouteControllerMap.put(route.mDescriptorId, controller);
+ }
+ }
+ }
+ // Update the playback info because the properties of the route have changed.
+ updatePlaybackInfoFromSelectedRoute();
+ }
+ }
+
+ RouteInfo chooseFallbackRoute() {
+ // When the current route is removed or no longer selectable,
+ // we want to revert to a live audio route if there is
+ // one (usually Bluetooth A2DP). Failing that, use
+ // the default route.
+ for (RouteInfo route : mRoutes) {
+ if (route != mDefaultRoute
+ && isSystemLiveAudioOnlyRoute(route)
+ && route.isSelectable()) {
+ return route;
+ }
+ }
+ return mDefaultRoute;
+ }
+
+ private boolean isSystemLiveAudioOnlyRoute(RouteInfo route) {
+ return route.getProviderInstance() == mSystemProvider
+ && route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO)
+ && !route.supportsControlCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+ }
+
+ private boolean isSystemDefaultRoute(RouteInfo route) {
+ return route.getProviderInstance() == mSystemProvider
+ && route.mDescriptorId.equals(
+ SystemMediaRouteProvider.DEFAULT_ROUTE_ID);
+ }
+
+ private void setSelectedRouteInternal(@NonNull RouteInfo route, int unselectReason) {
+ // TODO: Remove the following logging when no longer needed.
+ if (sGlobal == null || (mBluetoothRoute != null && route.isDefault())) {
+ final StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
+ 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())
+ .append(".")
+ .append(caller.getMethodName())
+ .append(":")
+ .append(caller.getLineNumber())
+ .append(" ");
+ }
+ if (sGlobal == null) {
+ Log.w(TAG, "setSelectedRouteInternal is called while sGlobal is null: pkgName="
+ + mApplicationContext.getPackageName() + ", callers=" + sb.toString());
+ } else {
+ Log.w(TAG, "Default route is selected while a BT route is available: pkgName="
+ + mApplicationContext.getPackageName() + ", callers=" + sb.toString());
+ }
+ }
+
+ if (mSelectedRoute != route) {
+ if (mSelectedRoute != null) {
+ if (DEBUG) {
+ Log.d(TAG, "Route unselected: " + mSelectedRoute + " reason: "
+ + unselectReason);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_ROUTE_UNSELECTED, mSelectedRoute,
+ unselectReason);
+ if (mSelectedRouteController != null) {
+ mSelectedRouteController.onUnselect(unselectReason);
+ mSelectedRouteController.onRelease();
+ mSelectedRouteController = null;
+ }
+ if (!mRouteControllerMap.isEmpty()) {
+ for (RouteController controller : mRouteControllerMap.values()) {
+ controller.onUnselect(unselectReason);
+ controller.onRelease();
+ }
+ mRouteControllerMap.clear();
+ }
+ }
+
+ mSelectedRoute = route;
+ mSelectedRouteController = route.getProviderInstance().onCreateRouteController(
+ route.mDescriptorId);
+ if (mSelectedRouteController != null) {
+ mSelectedRouteController.onSelect();
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Route selected: " + mSelectedRoute);
+ }
+ mCallbackHandler.post(CallbackHandler.MSG_ROUTE_SELECTED, mSelectedRoute);
+
+ if (mSelectedRoute instanceof RouteGroup) {
+ List<RouteInfo> routes = ((RouteGroup) mSelectedRoute).getRoutes();
+ mRouteControllerMap.clear();
+ for (RouteInfo r : routes) {
+ RouteController controller =
+ r.getProviderInstance().onCreateRouteController(
+ r.mDescriptorId, mSelectedRoute.mDescriptorId);
+ controller.onSelect();
+ mRouteControllerMap.put(r.mDescriptorId, controller);
+ }
+ }
+
+ updatePlaybackInfoFromSelectedRoute();
+ }
+ }
+
+ @Override
+ public void onSystemRouteSelectedByDescriptorId(String id) {
+ // System route is selected, do not sync the route we selected before.
+ mCallbackHandler.removeMessages(CallbackHandler.MSG_ROUTE_SELECTED);
+ int providerIndex = findProviderInfo(mSystemProvider);
+ if (providerIndex >= 0) {
+ ProviderInfo provider = mProviders.get(providerIndex);
+ int routeIndex = provider.findRouteByDescriptorId(id);
+ if (routeIndex >= 0) {
+ provider.mRoutes.get(routeIndex).select();
+ }
+ }
+ }
+
+ public void addRemoteControlClient(Object rcc) {
+ int index = findRemoteControlClientRecord(rcc);
+ if (index < 0) {
+ RemoteControlClientRecord record = new RemoteControlClientRecord(rcc);
+ mRemoteControlClients.add(record);
+ }
+ }
+
+ public void removeRemoteControlClient(Object rcc) {
+ int index = findRemoteControlClientRecord(rcc);
+ if (index >= 0) {
+ RemoteControlClientRecord record = mRemoteControlClients.remove(index);
+ record.disconnect();
+ }
+ }
+
+ public void setMediaSession(Object session) {
+ setMediaSessionRecord(session != null ? new MediaSessionRecord(session) : null);
+ }
+
+ public void setMediaSessionCompat(final MediaSessionCompat session) {
+ mCompatSession = session;
+ if (android.os.Build.VERSION.SDK_INT >= 21) {
+ setMediaSessionRecord(session != null ? new MediaSessionRecord(session) : null);
+ } else if (android.os.Build.VERSION.SDK_INT >= 14) {
+ if (mRccMediaSession != null) {
+ removeRemoteControlClient(mRccMediaSession.getRemoteControlClient());
+ mRccMediaSession.removeOnActiveChangeListener(mSessionActiveListener);
+ }
+ mRccMediaSession = session;
+ if (session != null) {
+ session.addOnActiveChangeListener(mSessionActiveListener);
+ if (session.isActive()) {
+ addRemoteControlClient(session.getRemoteControlClient());
+ }
+ }
+ }
+ }
+
+ private void setMediaSessionRecord(MediaSessionRecord mediaSessionRecord) {
+ if (mMediaSession != null) {
+ mMediaSession.clearVolumeHandling();
+ }
+ mMediaSession = mediaSessionRecord;
+ if (mediaSessionRecord != null) {
+ updatePlaybackInfoFromSelectedRoute();
+ }
+ }
+
+ public MediaSessionCompat.Token getMediaSessionToken() {
+ if (mMediaSession != null) {
+ return mMediaSession.getToken();
+ } else if (mCompatSession != null) {
+ return mCompatSession.getSessionToken();
+ }
+ return null;
+ }
+
+ private int findRemoteControlClientRecord(Object rcc) {
+ final int count = mRemoteControlClients.size();
+ for (int i = 0; i < count; i++) {
+ RemoteControlClientRecord record = mRemoteControlClients.get(i);
+ if (record.getRemoteControlClient() == rcc) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void updatePlaybackInfoFromSelectedRoute() {
+ if (mSelectedRoute != null) {
+ mPlaybackInfo.volume = mSelectedRoute.getVolume();
+ mPlaybackInfo.volumeMax = mSelectedRoute.getVolumeMax();
+ mPlaybackInfo.volumeHandling = mSelectedRoute.getVolumeHandling();
+ mPlaybackInfo.playbackStream = mSelectedRoute.getPlaybackStream();
+ mPlaybackInfo.playbackType = mSelectedRoute.getPlaybackType();
+
+ final int count = mRemoteControlClients.size();
+ for (int i = 0; i < count; i++) {
+ RemoteControlClientRecord record = mRemoteControlClients.get(i);
+ record.updatePlaybackInfo();
+ }
+ if (mMediaSession != null) {
+ if (mSelectedRoute == getDefaultRoute()
+ || mSelectedRoute == getBluetoothRoute()) {
+ // Local route
+ mMediaSession.clearVolumeHandling();
+ } else {
+ @VolumeProviderCompat.ControlType int controlType =
+ VolumeProviderCompat.VOLUME_CONTROL_FIXED;
+ if (mPlaybackInfo.volumeHandling
+ == MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE) {
+ controlType = VolumeProviderCompat.VOLUME_CONTROL_ABSOLUTE;
+ }
+ mMediaSession.configureVolume(controlType, mPlaybackInfo.volumeMax,
+ mPlaybackInfo.volume);
+ }
+ }
+ } else {
+ if (mMediaSession != null) {
+ mMediaSession.clearVolumeHandling();
+ }
+ }
+ }
+
+ private final class ProviderCallback extends MediaRouteProvider.Callback {
+ ProviderCallback() {
+ }
+
+ @Override
+ public void onDescriptorChanged(MediaRouteProvider provider,
+ MediaRouteProviderDescriptor descriptor) {
+ updateProviderDescriptor(provider, descriptor);
+ }
+ }
+
+ private final class MediaSessionRecord {
+ private final MediaSessionCompat mMsCompat;
+
+ private @VolumeProviderCompat.ControlType int mControlType;
+ private int mMaxVolume;
+ private VolumeProviderCompat mVpCompat;
+
+ public MediaSessionRecord(Object mediaSession) {
+ mMsCompat = MediaSessionCompat.fromMediaSession(mApplicationContext, mediaSession);
+ }
+
+ public MediaSessionRecord(MediaSessionCompat mediaSessionCompat) {
+ mMsCompat = mediaSessionCompat;
+ }
+
+ public void configureVolume(@VolumeProviderCompat.ControlType int controlType,
+ int max, int current) {
+ if (mVpCompat != null && controlType == mControlType && max == mMaxVolume) {
+ // If we haven't changed control type or max just set the
+ // new current volume
+ mVpCompat.setCurrentVolume(current);
+ } else {
+ // Otherwise create a new provider and update
+ mVpCompat = new VolumeProviderCompat(controlType, max, current) {
+ @Override
+ public void onSetVolumeTo(final int volume) {
+ mCallbackHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mSelectedRoute != null) {
+ mSelectedRoute.requestSetVolume(volume);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void onAdjustVolume(final int direction) {
+ mCallbackHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ if (mSelectedRoute != null) {
+ mSelectedRoute.requestUpdateVolume(direction);
+ }
+ }
+ });
+ }
+ };
+ mMsCompat.setPlaybackToRemote(mVpCompat);
+ }
+ }
+
+ public void clearVolumeHandling() {
+ mMsCompat.setPlaybackToLocal(mPlaybackInfo.playbackStream);
+ mVpCompat = null;
+ }
+
+ public MediaSessionCompat.Token getToken() {
+ return mMsCompat.getSessionToken();
+ }
+ }
+
+ private final class RemoteControlClientRecord
+ implements RemoteControlClientCompat.VolumeCallback {
+ private final RemoteControlClientCompat mRccCompat;
+ private boolean mDisconnected;
+
+ public RemoteControlClientRecord(Object rcc) {
+ mRccCompat = RemoteControlClientCompat.obtain(mApplicationContext, rcc);
+ mRccCompat.setVolumeCallback(this);
+ updatePlaybackInfo();
+ }
+
+ public Object getRemoteControlClient() {
+ return mRccCompat.getRemoteControlClient();
+ }
+
+ public void disconnect() {
+ mDisconnected = true;
+ mRccCompat.setVolumeCallback(null);
+ }
+
+ public void updatePlaybackInfo() {
+ mRccCompat.setPlaybackInfo(mPlaybackInfo);
+ }
+
+ @Override
+ public void onVolumeSetRequest(int volume) {
+ if (!mDisconnected && mSelectedRoute != null) {
+ mSelectedRoute.requestSetVolume(volume);
+ }
+ }
+
+ @Override
+ public void onVolumeUpdateRequest(int direction) {
+ if (!mDisconnected && mSelectedRoute != null) {
+ mSelectedRoute.requestUpdateVolume(direction);
+ }
+ }
+ }
+
+ private final class CallbackHandler extends Handler {
+ private final ArrayList<CallbackRecord> mTempCallbackRecords =
+ new ArrayList<CallbackRecord>();
+
+ private static final int MSG_TYPE_MASK = 0xff00;
+ private static final int MSG_TYPE_ROUTE = 0x0100;
+ private static final int MSG_TYPE_PROVIDER = 0x0200;
+
+ public static final int MSG_ROUTE_ADDED = MSG_TYPE_ROUTE | 1;
+ public static final int MSG_ROUTE_REMOVED = MSG_TYPE_ROUTE | 2;
+ public static final int MSG_ROUTE_CHANGED = MSG_TYPE_ROUTE | 3;
+ public static final int MSG_ROUTE_VOLUME_CHANGED = MSG_TYPE_ROUTE | 4;
+ public static final int MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED = MSG_TYPE_ROUTE | 5;
+ public static final int MSG_ROUTE_SELECTED = MSG_TYPE_ROUTE | 6;
+ public static final int MSG_ROUTE_UNSELECTED = MSG_TYPE_ROUTE | 7;
+
+ public static final int MSG_PROVIDER_ADDED = MSG_TYPE_PROVIDER | 1;
+ public static final int MSG_PROVIDER_REMOVED = MSG_TYPE_PROVIDER | 2;
+ public static final int MSG_PROVIDER_CHANGED = MSG_TYPE_PROVIDER | 3;
+
+ CallbackHandler() {
+ }
+
+ public void post(int msg, Object obj) {
+ obtainMessage(msg, obj).sendToTarget();
+ }
+
+ public void post(int msg, Object obj, int arg) {
+ Message message = obtainMessage(msg, obj);
+ message.arg1 = arg;
+ message.sendToTarget();
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ final int what = msg.what;
+ final Object obj = msg.obj;
+ final int arg = msg.arg1;
+
+ if (what == MSG_ROUTE_CHANGED
+ && getSelectedRoute().getId().equals(((RouteInfo) obj).getId())) {
+ updateSelectedRouteIfNeeded(true);
+ }
+
+ // Synchronize state with the system media router.
+ syncWithSystemProvider(what, obj);
+
+ // Invoke all registered callbacks.
+ // Build a list of callbacks before invoking them in case callbacks
+ // are added or removed during dispatch.
+ try {
+ for (int i = mRouters.size(); --i >= 0; ) {
+ MediaRouter router = mRouters.get(i).get();
+ if (router == null) {
+ mRouters.remove(i);
+ } else {
+ mTempCallbackRecords.addAll(router.mCallbackRecords);
+ }
+ }
+
+ final int callbackCount = mTempCallbackRecords.size();
+ for (int i = 0; i < callbackCount; i++) {
+ invokeCallback(mTempCallbackRecords.get(i), what, obj, arg);
+ }
+ } finally {
+ mTempCallbackRecords.clear();
+ }
+ }
+
+ private void syncWithSystemProvider(int what, Object obj) {
+ switch (what) {
+ case MSG_ROUTE_ADDED:
+ mSystemProvider.onSyncRouteAdded((RouteInfo) obj);
+ break;
+ case MSG_ROUTE_REMOVED:
+ mSystemProvider.onSyncRouteRemoved((RouteInfo) obj);
+ break;
+ case MSG_ROUTE_CHANGED:
+ mSystemProvider.onSyncRouteChanged((RouteInfo) obj);
+ break;
+ case MSG_ROUTE_SELECTED:
+ mSystemProvider.onSyncRouteSelected((RouteInfo) obj);
+ break;
+ }
+ }
+
+ private void invokeCallback(CallbackRecord record, int what, Object obj, int arg) {
+ final MediaRouter router = record.mRouter;
+ final MediaRouter.Callback callback = record.mCallback;
+ switch (what & MSG_TYPE_MASK) {
+ case MSG_TYPE_ROUTE: {
+ final RouteInfo route = (RouteInfo)obj;
+ if (!record.filterRouteEvent(route)) {
+ break;
+ }
+ switch (what) {
+ case MSG_ROUTE_ADDED:
+ callback.onRouteAdded(router, route);
+ break;
+ case MSG_ROUTE_REMOVED:
+ callback.onRouteRemoved(router, route);
+ break;
+ case MSG_ROUTE_CHANGED:
+ callback.onRouteChanged(router, route);
+ break;
+ case MSG_ROUTE_VOLUME_CHANGED:
+ callback.onRouteVolumeChanged(router, route);
+ break;
+ case MSG_ROUTE_PRESENTATION_DISPLAY_CHANGED:
+ callback.onRoutePresentationDisplayChanged(router, route);
+ break;
+ case MSG_ROUTE_SELECTED:
+ callback.onRouteSelected(router, route);
+ break;
+ case MSG_ROUTE_UNSELECTED:
+ callback.onRouteUnselected(router, route, arg);
+ break;
+ }
+ break;
+ }
+ case MSG_TYPE_PROVIDER: {
+ final ProviderInfo provider = (ProviderInfo)obj;
+ switch (what) {
+ case MSG_PROVIDER_ADDED:
+ callback.onProviderAdded(router, provider);
+ break;
+ case MSG_PROVIDER_REMOVED:
+ callback.onProviderRemoved(router, provider);
+ break;
+ case MSG_PROVIDER_CHANGED:
+ callback.onProviderChanged(router, provider);
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/MediaRouterApi24.java b/com/android/support/mediarouter/media/MediaRouterApi24.java
new file mode 100644
index 00000000..1146af60
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaRouterApi24.java
@@ -0,0 +1,26 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+// @@RequiresApi(24)
+final class MediaRouterApi24 {
+ public static final class RouteInfo {
+ public static int getDeviceType(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getDeviceType();
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/MediaRouterJellybean.java b/com/android/support/mediarouter/media/MediaRouterJellybean.java
new file mode 100644
index 00000000..0bb59b89
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaRouterJellybean.java
@@ -0,0 +1,462 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.media.AudioManager;
+import android.os.Build;
+import android.util.Log;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+// @@RequiresApi(16)
+final class MediaRouterJellybean {
+ private static final String TAG = "MediaRouterJellybean";
+
+ // android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP = 0x80;
+ // android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP_HEADPHONES = 0x100;
+ // android.media.AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP_SPEAKER = 0x200;
+ public static final int DEVICE_OUT_BLUETOOTH = 0x80 | 0x100 | 0x200;
+
+ public static final int ROUTE_TYPE_LIVE_AUDIO = 0x1;
+ public static final int ROUTE_TYPE_LIVE_VIDEO = 0x2;
+ public static final int ROUTE_TYPE_USER = 0x00800000;
+
+ public static final int ALL_ROUTE_TYPES =
+ MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO
+ | MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO
+ | MediaRouterJellybean.ROUTE_TYPE_USER;
+
+ public static Object getMediaRouter(Context context) {
+ return context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static List getRoutes(Object routerObj) {
+ final android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+ final int count = router.getRouteCount();
+ List out = new ArrayList(count);
+ for (int i = 0; i < count; i++) {
+ out.add(router.getRouteAt(i));
+ }
+ return out;
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static List getCategories(Object routerObj) {
+ final android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+ final int count = router.getCategoryCount();
+ List out = new ArrayList(count);
+ for (int i = 0; i < count; i++) {
+ out.add(router.getCategoryAt(i));
+ }
+ return out;
+ }
+
+ public static Object getSelectedRoute(Object routerObj, int type) {
+ return ((android.media.MediaRouter)routerObj).getSelectedRoute(type);
+ }
+
+ public static void selectRoute(Object routerObj, int types, Object routeObj) {
+ ((android.media.MediaRouter)routerObj).selectRoute(types,
+ (android.media.MediaRouter.RouteInfo)routeObj);
+ }
+
+ public static void addCallback(Object routerObj, int types, Object callbackObj) {
+ ((android.media.MediaRouter)routerObj).addCallback(types,
+ (android.media.MediaRouter.Callback)callbackObj);
+ }
+
+ public static void removeCallback(Object routerObj, Object callbackObj) {
+ ((android.media.MediaRouter)routerObj).removeCallback(
+ (android.media.MediaRouter.Callback)callbackObj);
+ }
+
+ public static Object createRouteCategory(Object routerObj,
+ String name, boolean isGroupable) {
+ return ((android.media.MediaRouter)routerObj).createRouteCategory(name, isGroupable);
+ }
+
+ public static Object createUserRoute(Object routerObj, Object categoryObj) {
+ return ((android.media.MediaRouter)routerObj).createUserRoute(
+ (android.media.MediaRouter.RouteCategory)categoryObj);
+ }
+
+ public static void addUserRoute(Object routerObj, Object routeObj) {
+ ((android.media.MediaRouter)routerObj).addUserRoute(
+ (android.media.MediaRouter.UserRouteInfo)routeObj);
+ }
+
+ public static void removeUserRoute(Object routerObj, Object routeObj) {
+ ((android.media.MediaRouter)routerObj).removeUserRoute(
+ (android.media.MediaRouter.UserRouteInfo)routeObj);
+ }
+
+ public static Object createCallback(Callback callback) {
+ return new CallbackProxy<Callback>(callback);
+ }
+
+ public static Object createVolumeCallback(VolumeCallback callback) {
+ return new VolumeCallbackProxy<VolumeCallback>(callback);
+ }
+
+ static boolean checkRoutedToBluetooth(Context context) {
+ try {
+ AudioManager audioManager = (AudioManager) context.getSystemService(
+ Context.AUDIO_SERVICE);
+ Method method = audioManager.getClass().getDeclaredMethod(
+ "getDevicesForStream", int.class);
+ int device = (Integer) method.invoke(audioManager, AudioManager.STREAM_MUSIC);
+ return (device & DEVICE_OUT_BLUETOOTH) != 0;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ public static final class RouteInfo {
+ public static CharSequence getName(Object routeObj, Context context) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getName(context);
+ }
+
+ public static CharSequence getStatus(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getStatus();
+ }
+
+ public static int getSupportedTypes(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getSupportedTypes();
+ }
+
+ public static Object getCategory(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getCategory();
+ }
+
+ public static Drawable getIconDrawable(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getIconDrawable();
+ }
+
+ public static int getPlaybackType(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackType();
+ }
+
+ public static int getPlaybackStream(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getPlaybackStream();
+ }
+
+ public static int getVolume(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getVolume();
+ }
+
+ public static int getVolumeMax(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeMax();
+ }
+
+ public static int getVolumeHandling(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getVolumeHandling();
+ }
+
+ public static Object getTag(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getTag();
+ }
+
+ public static void setTag(Object routeObj, Object tag) {
+ ((android.media.MediaRouter.RouteInfo)routeObj).setTag(tag);
+ }
+
+ public static void requestSetVolume(Object routeObj, int volume) {
+ ((android.media.MediaRouter.RouteInfo)routeObj).requestSetVolume(volume);
+ }
+
+ public static void requestUpdateVolume(Object routeObj, int direction) {
+ ((android.media.MediaRouter.RouteInfo)routeObj).requestUpdateVolume(direction);
+ }
+
+ public static Object getGroup(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getGroup();
+ }
+
+ public static boolean isGroup(Object routeObj) {
+ return routeObj instanceof android.media.MediaRouter.RouteGroup;
+ }
+ }
+
+ public static final class RouteGroup {
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static List getGroupedRoutes(Object groupObj) {
+ final android.media.MediaRouter.RouteGroup group =
+ (android.media.MediaRouter.RouteGroup)groupObj;
+ final int count = group.getRouteCount();
+ List out = new ArrayList(count);
+ for (int i = 0; i < count; i++) {
+ out.add(group.getRouteAt(i));
+ }
+ return out;
+ }
+ }
+
+ public static final class UserRouteInfo {
+ public static void setName(Object routeObj, CharSequence name) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setName(name);
+ }
+
+ public static void setStatus(Object routeObj, CharSequence status) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setStatus(status);
+ }
+
+ public static void setIconDrawable(Object routeObj, Drawable icon) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setIconDrawable(icon);
+ }
+
+ public static void setPlaybackType(Object routeObj, int type) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackType(type);
+ }
+
+ public static void setPlaybackStream(Object routeObj, int stream) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setPlaybackStream(stream);
+ }
+
+ public static void setVolume(Object routeObj, int volume) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolume(volume);
+ }
+
+ public static void setVolumeMax(Object routeObj, int volumeMax) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeMax(volumeMax);
+ }
+
+ public static void setVolumeHandling(Object routeObj, int volumeHandling) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeHandling(volumeHandling);
+ }
+
+ public static void setVolumeCallback(Object routeObj, Object volumeCallbackObj) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setVolumeCallback(
+ (android.media.MediaRouter.VolumeCallback)volumeCallbackObj);
+ }
+
+ public static void setRemoteControlClient(Object routeObj, Object rccObj) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setRemoteControlClient(
+ (android.media.RemoteControlClient)rccObj);
+ }
+ }
+
+ public static final class RouteCategory {
+ public static CharSequence getName(Object categoryObj, Context context) {
+ return ((android.media.MediaRouter.RouteCategory)categoryObj).getName(context);
+ }
+
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public static List getRoutes(Object categoryObj) {
+ ArrayList out = new ArrayList();
+ ((android.media.MediaRouter.RouteCategory)categoryObj).getRoutes(out);
+ return out;
+ }
+
+ public static int getSupportedTypes(Object categoryObj) {
+ return ((android.media.MediaRouter.RouteCategory)categoryObj).getSupportedTypes();
+ }
+
+ public static boolean isGroupable(Object categoryObj) {
+ return ((android.media.MediaRouter.RouteCategory)categoryObj).isGroupable();
+ }
+ }
+
+ public static interface Callback {
+ public void onRouteSelected(int type, Object routeObj);
+ public void onRouteUnselected(int type, Object routeObj);
+ public void onRouteAdded(Object routeObj);
+ public void onRouteRemoved(Object routeObj);
+ public void onRouteChanged(Object routeObj);
+ public void onRouteGrouped(Object routeObj, Object groupObj, int index);
+ public void onRouteUngrouped(Object routeObj, Object groupObj);
+ public void onRouteVolumeChanged(Object routeObj);
+ }
+
+ public static interface VolumeCallback {
+ public void onVolumeSetRequest(Object routeObj, int volume);
+ public void onVolumeUpdateRequest(Object routeObj, int direction);
+ }
+
+ /**
+ * Workaround for limitations of selectRoute() on JB and JB MR1.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class SelectRouteWorkaround {
+ private Method mSelectRouteIntMethod;
+
+ public SelectRouteWorkaround() {
+ if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) {
+ throw new UnsupportedOperationException();
+ }
+ try {
+ mSelectRouteIntMethod = android.media.MediaRouter.class.getMethod(
+ "selectRouteInt", int.class, android.media.MediaRouter.RouteInfo.class);
+ } catch (NoSuchMethodException ex) {
+ }
+ }
+
+ public void selectRoute(Object routerObj, int types, Object routeObj) {
+ android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+ android.media.MediaRouter.RouteInfo route =
+ (android.media.MediaRouter.RouteInfo)routeObj;
+
+ int routeTypes = route.getSupportedTypes();
+ if ((routeTypes & ROUTE_TYPE_USER) == 0) {
+ // Handle non-user routes.
+ // On JB and JB MR1, the selectRoute() API only supports programmatically
+ // selecting user routes. So instead we rely on the hidden selectRouteInt()
+ // method on these versions of the platform.
+ // This limitation was removed in JB MR2.
+ if (mSelectRouteIntMethod != null) {
+ try {
+ mSelectRouteIntMethod.invoke(router, types, route);
+ return; // success!
+ } catch (IllegalAccessException ex) {
+ Log.w(TAG, "Cannot programmatically select non-user route. "
+ + "Media routing may not work.", ex);
+ } catch (InvocationTargetException ex) {
+ Log.w(TAG, "Cannot programmatically select non-user route. "
+ + "Media routing may not work.", ex);
+ }
+ } else {
+ Log.w(TAG, "Cannot programmatically select non-user route "
+ + "because the platform is missing the selectRouteInt() "
+ + "method. Media routing may not work.");
+ }
+ }
+
+ // Default handling.
+ router.selectRoute(types, route);
+ }
+ }
+
+ /**
+ * Workaround the fact that the getDefaultRoute() method does not exist in JB and JB MR1.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class GetDefaultRouteWorkaround {
+ private Method mGetSystemAudioRouteMethod;
+
+ public GetDefaultRouteWorkaround() {
+ if (Build.VERSION.SDK_INT < 16 || Build.VERSION.SDK_INT > 17) {
+ throw new UnsupportedOperationException();
+ }
+ try {
+ mGetSystemAudioRouteMethod =
+ android.media.MediaRouter.class.getMethod("getSystemAudioRoute");
+ } catch (NoSuchMethodException ex) {
+ }
+ }
+
+ public Object getDefaultRoute(Object routerObj) {
+ android.media.MediaRouter router = (android.media.MediaRouter)routerObj;
+
+ if (mGetSystemAudioRouteMethod != null) {
+ try {
+ return mGetSystemAudioRouteMethod.invoke(router);
+ } catch (IllegalAccessException ex) {
+ } catch (InvocationTargetException ex) {
+ }
+ }
+
+ // Could not find the method or it does not work.
+ // Return the first route and hope for the best.
+ return router.getRouteAt(0);
+ }
+ }
+
+ static class CallbackProxy<T extends Callback>
+ extends android.media.MediaRouter.Callback {
+ protected final T mCallback;
+
+ public CallbackProxy(T callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onRouteSelected(android.media.MediaRouter router,
+ int type, android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteSelected(type, route);
+ }
+
+ @Override
+ public void onRouteUnselected(android.media.MediaRouter router,
+ int type, android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteUnselected(type, route);
+ }
+
+ @Override
+ public void onRouteAdded(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteAdded(route);
+ }
+
+ @Override
+ public void onRouteRemoved(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteRemoved(route);
+ }
+
+ @Override
+ public void onRouteChanged(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteChanged(route);
+ }
+
+ @Override
+ public void onRouteGrouped(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route,
+ android.media.MediaRouter.RouteGroup group, int index) {
+ mCallback.onRouteGrouped(route, group, index);
+ }
+
+ @Override
+ public void onRouteUngrouped(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route,
+ android.media.MediaRouter.RouteGroup group) {
+ mCallback.onRouteUngrouped(route, group);
+ }
+
+ @Override
+ public void onRouteVolumeChanged(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRouteVolumeChanged(route);
+ }
+ }
+
+ static class VolumeCallbackProxy<T extends VolumeCallback>
+ extends android.media.MediaRouter.VolumeCallback {
+ protected final T mCallback;
+
+ public VolumeCallbackProxy(T callback) {
+ mCallback = callback;
+ }
+
+ @Override
+ public void onVolumeSetRequest(android.media.MediaRouter.RouteInfo route,
+ int volume) {
+ mCallback.onVolumeSetRequest(route, volume);
+ }
+
+ @Override
+ public void onVolumeUpdateRequest(android.media.MediaRouter.RouteInfo route,
+ int direction) {
+ mCallback.onVolumeUpdateRequest(route, direction);
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/MediaRouterJellybeanMr1.java b/com/android/support/mediarouter/media/MediaRouterJellybeanMr1.java
new file mode 100644
index 00000000..f8539bda
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaRouterJellybeanMr1.java
@@ -0,0 +1,185 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.content.Context;
+import android.hardware.display.DisplayManager;
+import android.os.Build;
+import android.os.Handler;
+import android.support.annotation.RequiresApi;
+import android.util.Log;
+import android.view.Display;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+
+// @@RequiresApi(17)
+final class MediaRouterJellybeanMr1 {
+ private static final String TAG = "MediaRouterJellybeanMr1";
+
+ public static Object createCallback(Callback callback) {
+ return new CallbackProxy<Callback>(callback);
+ }
+
+ public static final class RouteInfo {
+ public static boolean isEnabled(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).isEnabled();
+ }
+
+ public static Display getPresentationDisplay(Object routeObj) {
+ // android.media.MediaRouter.RouteInfo.getPresentationDisplay() was
+ // added in API 17. However, some factory releases of JB MR1 missed it.
+ try {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getPresentationDisplay();
+ } catch (NoSuchMethodError ex) {
+ Log.w(TAG, "Cannot get presentation display for the route.", ex);
+ }
+ return null;
+ }
+ }
+
+ public static interface Callback extends MediaRouterJellybean.Callback {
+ public void onRoutePresentationDisplayChanged(Object routeObj);
+ }
+
+ /**
+ * Workaround the fact that the version of MediaRouter.addCallback() that accepts a
+ * flag to perform an active scan does not exist in JB MR1 so we need to force
+ * wifi display scans directly through the DisplayManager.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class ActiveScanWorkaround implements Runnable {
+ // Time between wifi display scans when actively scanning in milliseconds.
+ private static final int WIFI_DISPLAY_SCAN_INTERVAL = 15000;
+
+ private final DisplayManager mDisplayManager;
+ private final Handler mHandler;
+ private Method mScanWifiDisplaysMethod;
+
+ private boolean mActivelyScanningWifiDisplays;
+
+ public ActiveScanWorkaround(Context context, Handler handler) {
+ if (Build.VERSION.SDK_INT != 17) {
+ throw new UnsupportedOperationException();
+ }
+
+ mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
+ mHandler = handler;
+ try {
+ mScanWifiDisplaysMethod = DisplayManager.class.getMethod("scanWifiDisplays");
+ } catch (NoSuchMethodException ex) {
+ }
+ }
+
+ public void setActiveScanRouteTypes(int routeTypes) {
+ // On JB MR1, there is no API to scan wifi display routes.
+ // Instead we must make a direct call into the DisplayManager to scan
+ // wifi displays on this version but only when live video routes are requested.
+ // See also the JellybeanMr2Impl implementation of this method.
+ // This was fixed in JB MR2 by adding a new overload of addCallback() to
+ // enable active scanning on request.
+ if ((routeTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
+ if (!mActivelyScanningWifiDisplays) {
+ if (mScanWifiDisplaysMethod != null) {
+ mActivelyScanningWifiDisplays = true;
+ mHandler.post(this);
+ } else {
+ Log.w(TAG, "Cannot scan for wifi displays because the "
+ + "DisplayManager.scanWifiDisplays() method is "
+ + "not available on this device.");
+ }
+ }
+ } else {
+ if (mActivelyScanningWifiDisplays) {
+ mActivelyScanningWifiDisplays = false;
+ mHandler.removeCallbacks(this);
+ }
+ }
+ }
+
+ @Override
+ public void run() {
+ if (mActivelyScanningWifiDisplays) {
+ try {
+ mScanWifiDisplaysMethod.invoke(mDisplayManager);
+ } catch (IllegalAccessException ex) {
+ Log.w(TAG, "Cannot scan for wifi displays.", ex);
+ } catch (InvocationTargetException ex) {
+ Log.w(TAG, "Cannot scan for wifi displays.", ex);
+ }
+ mHandler.postDelayed(this, WIFI_DISPLAY_SCAN_INTERVAL);
+ }
+ }
+ }
+
+ /**
+ * Workaround the fact that the isConnecting() method does not exist in JB MR1.
+ * Do not use on JB MR2 and above.
+ */
+ public static final class IsConnectingWorkaround {
+ private Method mGetStatusCodeMethod;
+ private int mStatusConnecting;
+
+ public IsConnectingWorkaround() {
+ if (Build.VERSION.SDK_INT != 17) {
+ throw new UnsupportedOperationException();
+ }
+
+ try {
+ Field statusConnectingField =
+ android.media.MediaRouter.RouteInfo.class.getField("STATUS_CONNECTING");
+ mStatusConnecting = statusConnectingField.getInt(null);
+ mGetStatusCodeMethod =
+ android.media.MediaRouter.RouteInfo.class.getMethod("getStatusCode");
+ } catch (NoSuchFieldException ex) {
+ } catch (NoSuchMethodException ex) {
+ } catch (IllegalAccessException ex) {
+ }
+ }
+
+ public boolean isConnecting(Object routeObj) {
+ android.media.MediaRouter.RouteInfo route =
+ (android.media.MediaRouter.RouteInfo)routeObj;
+
+ if (mGetStatusCodeMethod != null) {
+ try {
+ int statusCode = (Integer)mGetStatusCodeMethod.invoke(route);
+ return statusCode == mStatusConnecting;
+ } catch (IllegalAccessException ex) {
+ } catch (InvocationTargetException ex) {
+ }
+ }
+
+ // Assume not connecting.
+ return false;
+ }
+ }
+
+ static class CallbackProxy<T extends Callback>
+ extends MediaRouterJellybean.CallbackProxy<T> {
+ public CallbackProxy(T callback) {
+ super(callback);
+ }
+
+ @Override
+ public void onRoutePresentationDisplayChanged(android.media.MediaRouter router,
+ android.media.MediaRouter.RouteInfo route) {
+ mCallback.onRoutePresentationDisplayChanged(route);
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/MediaRouterJellybeanMr2.java b/com/android/support/mediarouter/media/MediaRouterJellybeanMr2.java
new file mode 100644
index 00000000..11035495
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaRouterJellybeanMr2.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+// @@RequiresApi(18)
+final class MediaRouterJellybeanMr2 {
+ public static Object getDefaultRoute(Object routerObj) {
+ return ((android.media.MediaRouter)routerObj).getDefaultRoute();
+ }
+
+ public static void addCallback(Object routerObj, int types, Object callbackObj, int flags) {
+ ((android.media.MediaRouter)routerObj).addCallback(types,
+ (android.media.MediaRouter.Callback)callbackObj, flags);
+ }
+
+ public static final class RouteInfo {
+ public static CharSequence getDescription(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).getDescription();
+ }
+
+ public static boolean isConnecting(Object routeObj) {
+ return ((android.media.MediaRouter.RouteInfo)routeObj).isConnecting();
+ }
+ }
+
+ public static final class UserRouteInfo {
+ public static void setDescription(Object routeObj, CharSequence description) {
+ ((android.media.MediaRouter.UserRouteInfo)routeObj).setDescription(description);
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/MediaSessionStatus.java b/com/android/support/mediarouter/media/MediaSessionStatus.java
new file mode 100644
index 00000000..32065966
--- /dev/null
+++ b/com/android/support/mediarouter/media/MediaSessionStatus.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.app.PendingIntent;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.support.v4.util.TimeUtils;
+
+/**
+ * Describes the playback status of a media session.
+ * <p>
+ * This class is part of the remote playback protocol described by the
+ * {@link MediaControlIntent MediaControlIntent} class.
+ * </p><p>
+ * When a media session is created, it is initially in the
+ * {@link #SESSION_STATE_ACTIVE active} state. When the media session ends
+ * normally, it transitions to the {@link #SESSION_STATE_ENDED ended} state.
+ * If the media session is invalidated due to another session forcibly taking
+ * control of the route, then it transitions to the
+ * {@link #SESSION_STATE_INVALIDATED invalidated} state.
+ * Refer to the documentation of each state for an explanation of its meaning.
+ * </p><p>
+ * To monitor session status, the application should supply a {@link PendingIntent} to use as the
+ * {@link MediaControlIntent#EXTRA_SESSION_STATUS_UPDATE_RECEIVER session status update receiver}
+ * for a given {@link MediaControlIntent#ACTION_START_SESSION session start request}.
+ * </p><p>
+ * This object is immutable once created using a {@link Builder} instance.
+ * </p>
+ */
+public final class MediaSessionStatus {
+ static final String KEY_TIMESTAMP = "timestamp";
+ static final String KEY_SESSION_STATE = "sessionState";
+ static final String KEY_QUEUE_PAUSED = "queuePaused";
+ static final String KEY_EXTRAS = "extras";
+
+ final Bundle mBundle;
+
+ /**
+ * Session state: Active.
+ * <p>
+ * Indicates that the media session is active and in control of the route.
+ * </p>
+ */
+ public static final int SESSION_STATE_ACTIVE = 0;
+
+ /**
+ * Session state: Ended.
+ * <p>
+ * Indicates that the media session was ended normally using the
+ * {@link MediaControlIntent#ACTION_END_SESSION end session} action.
+ * </p><p>
+ * A terminated media session cannot be used anymore. To play more media, the
+ * application must start a new session.
+ * </p>
+ */
+ public static final int SESSION_STATE_ENDED = 1;
+
+ /**
+ * Session state: Invalidated.
+ * <p>
+ * Indicates that the media session was invalidated involuntarily due to
+ * another session taking control of the route.
+ * </p><p>
+ * An invalidated media session cannot be used anymore. To play more media, the
+ * application must start a new session.
+ * </p>
+ */
+ public static final int SESSION_STATE_INVALIDATED = 2;
+
+ MediaSessionStatus(Bundle bundle) {
+ mBundle = bundle;
+ }
+
+ /**
+ * Gets the timestamp associated with the status information in
+ * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
+ *
+ * @return The status timestamp in the {@link SystemClock#elapsedRealtime()} time base.
+ */
+ public long getTimestamp() {
+ return mBundle.getLong(KEY_TIMESTAMP);
+ }
+
+ /**
+ * Gets the session state.
+ *
+ * @return The session state. One of {@link #SESSION_STATE_ACTIVE},
+ * {@link #SESSION_STATE_ENDED}, or {@link #SESSION_STATE_INVALIDATED}.
+ */
+ public int getSessionState() {
+ return mBundle.getInt(KEY_SESSION_STATE, SESSION_STATE_INVALIDATED);
+ }
+
+ /**
+ * Returns true if the session's queue is paused.
+ *
+ * @return True if the session's queue is paused.
+ */
+ public boolean isQueuePaused() {
+ return mBundle.getBoolean(KEY_QUEUE_PAUSED);
+ }
+
+ /**
+ * Gets a bundle of extras for this status object.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Bundle getExtras() {
+ return mBundle.getBundle(KEY_EXTRAS);
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("MediaSessionStatus{ ");
+ result.append("timestamp=");
+ TimeUtils.formatDuration(SystemClock.elapsedRealtime() - getTimestamp(), result);
+ result.append(" ms ago");
+ result.append(", sessionState=").append(sessionStateToString(getSessionState()));
+ result.append(", queuePaused=").append(isQueuePaused());
+ result.append(", extras=").append(getExtras());
+ result.append(" }");
+ return result.toString();
+ }
+
+ private static String sessionStateToString(int sessionState) {
+ switch (sessionState) {
+ case SESSION_STATE_ACTIVE:
+ return "active";
+ case SESSION_STATE_ENDED:
+ return "ended";
+ case SESSION_STATE_INVALIDATED:
+ return "invalidated";
+ }
+ return Integer.toString(sessionState);
+ }
+
+ /**
+ * Converts this object to a bundle for serialization.
+ *
+ * @return The contents of the object represented as a bundle.
+ */
+ public Bundle asBundle() {
+ return mBundle;
+ }
+
+ /**
+ * Creates an instance from a bundle.
+ *
+ * @param bundle The bundle, or null if none.
+ * @return The new instance, or null if the bundle was null.
+ */
+ public static MediaSessionStatus fromBundle(Bundle bundle) {
+ return bundle != null ? new MediaSessionStatus(bundle) : null;
+ }
+
+ /**
+ * Builder for {@link MediaSessionStatus media session status objects}.
+ */
+ public static final class Builder {
+ private final Bundle mBundle;
+
+ /**
+ * Creates a media session status builder using the current time as the
+ * reference timestamp.
+ *
+ * @param sessionState The session state.
+ */
+ public Builder(int sessionState) {
+ mBundle = new Bundle();
+ setTimestamp(SystemClock.elapsedRealtime());
+ setSessionState(sessionState);
+ }
+
+ /**
+ * Creates a media session status builder whose initial contents are
+ * copied from an existing status.
+ */
+ public Builder(MediaSessionStatus status) {
+ if (status == null) {
+ throw new IllegalArgumentException("status must not be null");
+ }
+
+ mBundle = new Bundle(status.mBundle);
+ }
+
+ /**
+ * Sets the timestamp associated with the status information in
+ * milliseconds since boot in the {@link SystemClock#elapsedRealtime} time base.
+ */
+ public Builder setTimestamp(long elapsedRealtimeTimestamp) {
+ mBundle.putLong(KEY_TIMESTAMP, elapsedRealtimeTimestamp);
+ return this;
+ }
+
+ /**
+ * Sets the session state.
+ */
+ public Builder setSessionState(int sessionState) {
+ mBundle.putInt(KEY_SESSION_STATE, sessionState);
+ return this;
+ }
+
+ /**
+ * Sets whether the queue is paused.
+ */
+ public Builder setQueuePaused(boolean queuePaused) {
+ mBundle.putBoolean(KEY_QUEUE_PAUSED, queuePaused);
+ return this;
+ }
+
+ /**
+ * Sets a bundle of extras for this status object.
+ * The extras will be ignored by the media router but they may be used
+ * by applications.
+ */
+ public Builder setExtras(Bundle extras) {
+ mBundle.putBundle(KEY_EXTRAS, extras);
+ return this;
+ }
+
+ /**
+ * Builds the {@link MediaSessionStatus media session status object}.
+ */
+ public MediaSessionStatus build() {
+ return new MediaSessionStatus(mBundle);
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/RegisteredMediaRouteProvider.java b/com/android/support/mediarouter/media/RegisteredMediaRouteProvider.java
new file mode 100644
index 00000000..98e4e283
--- /dev/null
+++ b/com/android/support/mediarouter/media/RegisteredMediaRouteProvider.java
@@ -0,0 +1,741 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_DATA_ROUTE_ID;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_DATA_ROUTE_LIBRARY_GROUP;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_DATA_UNSELECT_REASON;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_DATA_VOLUME;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_CREATE_ROUTE_CONTROLLER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_REGISTER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_RELEASE_ROUTE_CONTROLLER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_ROUTE_CONTROL_REQUEST;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_SELECT_ROUTE;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_SET_DISCOVERY_REQUEST;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_SET_ROUTE_VOLUME;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_MSG_UNREGISTER;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_UNSELECT_ROUTE;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .CLIENT_MSG_UPDATE_ROUTE_VOLUME;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.CLIENT_VERSION_CURRENT;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_DATA_ERROR;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_CONTROL_REQUEST_FAILED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_DESCRIPTOR_CHANGED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_GENERIC_FAILURE;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol
+ .SERVICE_MSG_GENERIC_SUCCESS;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_MSG_REGISTERED;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.SERVICE_VERSION_1;
+import static com.android.support.mediarouter.media.MediaRouteProviderProtocol.isValidRemoteMessenger;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Bundle;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.IBinder.DeathRecipient;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.support.mediarouter.media.MediaRouter.ControlRequestCallback;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Maintains a connection to a particular media route provider service.
+ */
+final class RegisteredMediaRouteProvider extends MediaRouteProvider
+ implements ServiceConnection {
+ static final String TAG = "MediaRouteProviderProxy"; // max. 23 chars
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final ComponentName mComponentName;
+ final PrivateHandler mPrivateHandler;
+ private final ArrayList<Controller> mControllers = new ArrayList<Controller>();
+
+ private boolean mStarted;
+ private boolean mBound;
+ private Connection mActiveConnection;
+ private boolean mConnectionReady;
+
+ public RegisteredMediaRouteProvider(Context context, ComponentName componentName) {
+ super(context, new ProviderMetadata(componentName));
+
+ mComponentName = componentName;
+ mPrivateHandler = new PrivateHandler();
+ }
+
+ @Override
+ public RouteController onCreateRouteController(@NonNull String routeId) {
+ if (routeId == null) {
+ throw new IllegalArgumentException("routeId cannot be null");
+ }
+ return createRouteController(routeId, null);
+ }
+
+ @Override
+ public RouteController onCreateRouteController(
+ @NonNull String routeId, @NonNull String routeGroupId) {
+ if (routeId == null) {
+ throw new IllegalArgumentException("routeId cannot be null");
+ }
+ if (routeGroupId == null) {
+ throw new IllegalArgumentException("routeGroupId cannot be null");
+ }
+ return createRouteController(routeId, routeGroupId);
+ }
+
+ @Override
+ public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
+ if (mConnectionReady) {
+ mActiveConnection.setDiscoveryRequest(request);
+ }
+ updateBinding();
+ }
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Connected");
+ }
+
+ if (mBound) {
+ disconnect();
+
+ Messenger messenger = (service != null ? new Messenger(service) : null);
+ if (isValidRemoteMessenger(messenger)) {
+ Connection connection = new Connection(messenger);
+ if (connection.register()) {
+ mActiveConnection = connection;
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Registration failed");
+ }
+ }
+ } else {
+ Log.e(TAG, this + ": Service returned invalid messenger binder");
+ }
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Service disconnected");
+ }
+ disconnect();
+ }
+
+ @Override
+ public String toString() {
+ return "Service connection " + mComponentName.flattenToShortString();
+ }
+
+ public boolean hasComponentName(String packageName, String className) {
+ return mComponentName.getPackageName().equals(packageName)
+ && mComponentName.getClassName().equals(className);
+ }
+
+ public void start() {
+ if (!mStarted) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Starting");
+ }
+
+ mStarted = true;
+ updateBinding();
+ }
+ }
+
+ public void stop() {
+ if (mStarted) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Stopping");
+ }
+
+ mStarted = false;
+ updateBinding();
+ }
+ }
+
+ public void rebindIfDisconnected() {
+ if (mActiveConnection == null && shouldBind()) {
+ unbind();
+ bind();
+ }
+ }
+
+ private void updateBinding() {
+ if (shouldBind()) {
+ bind();
+ } else {
+ unbind();
+ }
+ }
+
+ private boolean shouldBind() {
+ if (mStarted) {
+ // Bind whenever there is a discovery request.
+ if (getDiscoveryRequest() != null) {
+ return true;
+ }
+
+ // Bind whenever the application has an active route controller.
+ // This means that one of this provider's routes is selected.
+ if (!mControllers.isEmpty()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void bind() {
+ if (!mBound) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Binding");
+ }
+
+ Intent service = new Intent(MediaRouteProviderProtocol.SERVICE_INTERFACE);
+ service.setComponent(mComponentName);
+ try {
+ mBound = getContext().bindService(service, this, Context.BIND_AUTO_CREATE);
+ if (!mBound && DEBUG) {
+ Log.d(TAG, this + ": Bind failed");
+ }
+ } catch (SecurityException ex) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Bind failed", ex);
+ }
+ }
+ }
+ }
+
+ private void unbind() {
+ if (mBound) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Unbinding");
+ }
+
+ mBound = false;
+ disconnect();
+ getContext().unbindService(this);
+ }
+ }
+
+ private RouteController createRouteController(String routeId, String routeGroupId) {
+ MediaRouteProviderDescriptor descriptor = getDescriptor();
+ if (descriptor != null) {
+ List<MediaRouteDescriptor> routes = descriptor.getRoutes();
+ final int count = routes.size();
+ for (int i = 0; i < count; i++) {
+ final MediaRouteDescriptor route = routes.get(i);
+ if (route.getId().equals(routeId)) {
+ Controller controller = new Controller(routeId, routeGroupId);
+ mControllers.add(controller);
+ if (mConnectionReady) {
+ controller.attachConnection(mActiveConnection);
+ }
+ updateBinding();
+ return controller;
+ }
+ }
+ }
+ return null;
+ }
+
+ void onConnectionReady(Connection connection) {
+ if (mActiveConnection == connection) {
+ mConnectionReady = true;
+ attachControllersToConnection();
+
+ MediaRouteDiscoveryRequest request = getDiscoveryRequest();
+ if (request != null) {
+ mActiveConnection.setDiscoveryRequest(request);
+ }
+ }
+ }
+
+ void onConnectionDied(Connection connection) {
+ if (mActiveConnection == connection) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Service connection died");
+ }
+ disconnect();
+ }
+ }
+
+ void onConnectionError(Connection connection, String error) {
+ if (mActiveConnection == connection) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Service connection error - " + error);
+ }
+ unbind();
+ }
+ }
+
+ void onConnectionDescriptorChanged(Connection connection,
+ MediaRouteProviderDescriptor descriptor) {
+ if (mActiveConnection == connection) {
+ if (DEBUG) {
+ Log.d(TAG, this + ": Descriptor changed, descriptor=" + descriptor);
+ }
+ setDescriptor(descriptor);
+ }
+ }
+
+ private void disconnect() {
+ if (mActiveConnection != null) {
+ setDescriptor(null);
+ mConnectionReady = false;
+ detachControllersFromConnection();
+ mActiveConnection.dispose();
+ mActiveConnection = null;
+ }
+ }
+
+ void onControllerReleased(Controller controller) {
+ mControllers.remove(controller);
+ controller.detachConnection();
+ updateBinding();
+ }
+
+ private void attachControllersToConnection() {
+ int count = mControllers.size();
+ for (int i = 0; i < count; i++) {
+ mControllers.get(i).attachConnection(mActiveConnection);
+ }
+ }
+
+ private void detachControllersFromConnection() {
+ int count = mControllers.size();
+ for (int i = 0; i < count; i++) {
+ mControllers.get(i).detachConnection();
+ }
+ }
+
+ private final class Controller extends RouteController {
+ private final String mRouteId;
+ private final String mRouteGroupId;
+
+ private boolean mSelected;
+ private int mPendingSetVolume = -1;
+ private int mPendingUpdateVolumeDelta;
+
+ private Connection mConnection;
+ private int mControllerId;
+
+ public Controller(String routeId, String routeGroupId) {
+ mRouteId = routeId;
+ mRouteGroupId = routeGroupId;
+ }
+
+ public void attachConnection(Connection connection) {
+ mConnection = connection;
+ mControllerId = connection.createRouteController(mRouteId, mRouteGroupId);
+ if (mSelected) {
+ connection.selectRoute(mControllerId);
+ if (mPendingSetVolume >= 0) {
+ connection.setVolume(mControllerId, mPendingSetVolume);
+ mPendingSetVolume = -1;
+ }
+ if (mPendingUpdateVolumeDelta != 0) {
+ connection.updateVolume(mControllerId, mPendingUpdateVolumeDelta);
+ mPendingUpdateVolumeDelta = 0;
+ }
+ }
+ }
+
+ public void detachConnection() {
+ if (mConnection != null) {
+ mConnection.releaseRouteController(mControllerId);
+ mConnection = null;
+ mControllerId = 0;
+ }
+ }
+
+ @Override
+ public void onRelease() {
+ onControllerReleased(this);
+ }
+
+ @Override
+ public void onSelect() {
+ mSelected = true;
+ if (mConnection != null) {
+ mConnection.selectRoute(mControllerId);
+ }
+ }
+
+ @Override
+ public void onUnselect() {
+ onUnselect(MediaRouter.UNSELECT_REASON_UNKNOWN);
+ }
+
+ @Override
+ public void onUnselect(int reason) {
+ mSelected = false;
+ if (mConnection != null) {
+ mConnection.unselectRoute(mControllerId, reason);
+ }
+ }
+
+ @Override
+ public void onSetVolume(int volume) {
+ if (mConnection != null) {
+ mConnection.setVolume(mControllerId, volume);
+ } else {
+ mPendingSetVolume = volume;
+ mPendingUpdateVolumeDelta = 0;
+ }
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ if (mConnection != null) {
+ mConnection.updateVolume(mControllerId, delta);
+ } else {
+ mPendingUpdateVolumeDelta += delta;
+ }
+ }
+
+ @Override
+ public boolean onControlRequest(Intent intent, ControlRequestCallback callback) {
+ if (mConnection != null) {
+ return mConnection.sendControlRequest(mControllerId, intent, callback);
+ }
+ return false;
+ }
+ }
+
+ private final class Connection implements DeathRecipient {
+ private final Messenger mServiceMessenger;
+ private final ReceiveHandler mReceiveHandler;
+ private final Messenger mReceiveMessenger;
+
+ private int mNextRequestId = 1;
+ private int mNextControllerId = 1;
+ private int mServiceVersion; // non-zero when registration complete
+
+ private int mPendingRegisterRequestId;
+ private final SparseArray<ControlRequestCallback> mPendingCallbacks =
+ new SparseArray<ControlRequestCallback>();
+
+ public Connection(Messenger serviceMessenger) {
+ mServiceMessenger = serviceMessenger;
+ mReceiveHandler = new ReceiveHandler(this);
+ mReceiveMessenger = new Messenger(mReceiveHandler);
+ }
+
+ public boolean register() {
+ mPendingRegisterRequestId = mNextRequestId++;
+ if (!sendRequest(CLIENT_MSG_REGISTER,
+ mPendingRegisterRequestId,
+ CLIENT_VERSION_CURRENT, null, null)) {
+ return false;
+ }
+
+ try {
+ mServiceMessenger.getBinder().linkToDeath(this, 0);
+ return true;
+ } catch (RemoteException ex) {
+ binderDied();
+ }
+ return false;
+ }
+
+ public void dispose() {
+ sendRequest(CLIENT_MSG_UNREGISTER, 0, 0, null, null);
+ mReceiveHandler.dispose();
+ mServiceMessenger.getBinder().unlinkToDeath(this, 0);
+
+ mPrivateHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ failPendingCallbacks();
+ }
+ });
+ }
+
+ void failPendingCallbacks() {
+ int count = 0;
+ for (int i = 0; i < mPendingCallbacks.size(); i++) {
+ mPendingCallbacks.valueAt(i).onError(null, null);
+ }
+ mPendingCallbacks.clear();
+ }
+
+ public boolean onGenericFailure(int requestId) {
+ if (requestId == mPendingRegisterRequestId) {
+ mPendingRegisterRequestId = 0;
+ onConnectionError(this, "Registration failed");
+ }
+ ControlRequestCallback callback = mPendingCallbacks.get(requestId);
+ if (callback != null) {
+ mPendingCallbacks.remove(requestId);
+ callback.onError(null, null);
+ }
+ return true;
+ }
+
+ public boolean onGenericSuccess(int requestId) {
+ return true;
+ }
+
+ public boolean onRegistered(int requestId, int serviceVersion,
+ Bundle descriptorBundle) {
+ if (mServiceVersion == 0
+ && requestId == mPendingRegisterRequestId
+ && serviceVersion >= SERVICE_VERSION_1) {
+ mPendingRegisterRequestId = 0;
+ mServiceVersion = serviceVersion;
+ onConnectionDescriptorChanged(this,
+ MediaRouteProviderDescriptor.fromBundle(descriptorBundle));
+ onConnectionReady(this);
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onDescriptorChanged(Bundle descriptorBundle) {
+ if (mServiceVersion != 0) {
+ onConnectionDescriptorChanged(this,
+ MediaRouteProviderDescriptor.fromBundle(descriptorBundle));
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onControlRequestSucceeded(int requestId, Bundle data) {
+ ControlRequestCallback callback = mPendingCallbacks.get(requestId);
+ if (callback != null) {
+ mPendingCallbacks.remove(requestId);
+ callback.onResult(data);
+ return true;
+ }
+ return false;
+ }
+
+ public boolean onControlRequestFailed(int requestId, String error, Bundle data) {
+ ControlRequestCallback callback = mPendingCallbacks.get(requestId);
+ if (callback != null) {
+ mPendingCallbacks.remove(requestId);
+ callback.onError(error, data);
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void binderDied() {
+ mPrivateHandler.post(new Runnable() {
+ @Override
+ public void run() {
+ onConnectionDied(Connection.this);
+ }
+ });
+ }
+
+ public int createRouteController(String routeId, String routeGroupId) {
+ int controllerId = mNextControllerId++;
+ Bundle data = new Bundle();
+ data.putString(CLIENT_DATA_ROUTE_ID, routeId);
+ data.putString(CLIENT_DATA_ROUTE_LIBRARY_GROUP, routeGroupId);
+ sendRequest(CLIENT_MSG_CREATE_ROUTE_CONTROLLER,
+ mNextRequestId++, controllerId, null, data);
+ return controllerId;
+ }
+
+ public void releaseRouteController(int controllerId) {
+ sendRequest(CLIENT_MSG_RELEASE_ROUTE_CONTROLLER,
+ mNextRequestId++, controllerId, null, null);
+ }
+
+ public void selectRoute(int controllerId) {
+ sendRequest(CLIENT_MSG_SELECT_ROUTE,
+ mNextRequestId++, controllerId, null, null);
+ }
+
+ public void unselectRoute(int controllerId, int reason) {
+ Bundle extras = new Bundle();
+ extras.putInt(CLIENT_DATA_UNSELECT_REASON, reason);
+ sendRequest(CLIENT_MSG_UNSELECT_ROUTE,
+ mNextRequestId++, controllerId, null, extras);
+ }
+
+ public void setVolume(int controllerId, int volume) {
+ Bundle data = new Bundle();
+ data.putInt(CLIENT_DATA_VOLUME, volume);
+ sendRequest(CLIENT_MSG_SET_ROUTE_VOLUME,
+ mNextRequestId++, controllerId, null, data);
+ }
+
+ public void updateVolume(int controllerId, int delta) {
+ Bundle data = new Bundle();
+ data.putInt(CLIENT_DATA_VOLUME, delta);
+ sendRequest(CLIENT_MSG_UPDATE_ROUTE_VOLUME,
+ mNextRequestId++, controllerId, null, data);
+ }
+
+ public boolean sendControlRequest(int controllerId, Intent intent,
+ ControlRequestCallback callback) {
+ int requestId = mNextRequestId++;
+ if (sendRequest(CLIENT_MSG_ROUTE_CONTROL_REQUEST,
+ requestId, controllerId, intent, null)) {
+ if (callback != null) {
+ mPendingCallbacks.put(requestId, callback);
+ }
+ return true;
+ }
+ return false;
+ }
+
+ public void setDiscoveryRequest(MediaRouteDiscoveryRequest request) {
+ sendRequest(CLIENT_MSG_SET_DISCOVERY_REQUEST,
+ mNextRequestId++, 0, request != null ? request.asBundle() : null, null);
+ }
+
+ private boolean sendRequest(int what, int requestId, int arg, Object obj, Bundle data) {
+ Message msg = Message.obtain();
+ msg.what = what;
+ msg.arg1 = requestId;
+ msg.arg2 = arg;
+ msg.obj = obj;
+ msg.setData(data);
+ msg.replyTo = mReceiveMessenger;
+ try {
+ mServiceMessenger.send(msg);
+ return true;
+ } catch (DeadObjectException ex) {
+ // The service died.
+ } catch (RemoteException ex) {
+ if (what != CLIENT_MSG_UNREGISTER) {
+ Log.e(TAG, "Could not send message to service.", ex);
+ }
+ }
+ return false;
+ }
+ }
+
+ private static final class PrivateHandler extends Handler {
+ PrivateHandler() {
+ }
+ }
+
+ /**
+ * Handler that receives messages from the server.
+ * <p>
+ * This inner class is static and only retains a weak reference to the connection
+ * to prevent the client from being leaked in case the service is holding an
+ * active reference to the client's messenger.
+ * </p><p>
+ * This handler should not be used to handle any messages other than those
+ * that come from the service.
+ * </p>
+ */
+ private static final class ReceiveHandler extends Handler {
+ private final WeakReference<Connection> mConnectionRef;
+
+ public ReceiveHandler(Connection connection) {
+ mConnectionRef = new WeakReference<Connection>(connection);
+ }
+
+ public void dispose() {
+ mConnectionRef.clear();
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ Connection connection = mConnectionRef.get();
+ if (connection != null) {
+ final int what = msg.what;
+ final int requestId = msg.arg1;
+ final int arg = msg.arg2;
+ final Object obj = msg.obj;
+ final Bundle data = msg.peekData();
+ if (!processMessage(connection, what, requestId, arg, obj, data)) {
+ if (DEBUG) {
+ Log.d(TAG, "Unhandled message from server: " + msg);
+ }
+ }
+ }
+ }
+
+ private boolean processMessage(Connection connection,
+ int what, int requestId, int arg, Object obj, Bundle data) {
+ switch (what) {
+ case SERVICE_MSG_GENERIC_FAILURE:
+ connection.onGenericFailure(requestId);
+ return true;
+
+ case SERVICE_MSG_GENERIC_SUCCESS:
+ connection.onGenericSuccess(requestId);
+ return true;
+
+ case SERVICE_MSG_REGISTERED:
+ if (obj == null || obj instanceof Bundle) {
+ return connection.onRegistered(requestId, arg, (Bundle)obj);
+ }
+ break;
+
+ case SERVICE_MSG_DESCRIPTOR_CHANGED:
+ if (obj == null || obj instanceof Bundle) {
+ return connection.onDescriptorChanged((Bundle)obj);
+ }
+ break;
+
+ case SERVICE_MSG_CONTROL_REQUEST_SUCCEEDED:
+ if (obj == null || obj instanceof Bundle) {
+ return connection.onControlRequestSucceeded(
+ requestId, (Bundle)obj);
+ }
+ break;
+
+ case SERVICE_MSG_CONTROL_REQUEST_FAILED:
+ if (obj == null || obj instanceof Bundle) {
+ String error = (data == null ? null :
+ data.getString(SERVICE_DATA_ERROR));
+ return connection.onControlRequestFailed(
+ requestId, error, (Bundle)obj);
+ }
+ break;
+ }
+ return false;
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/RegisteredMediaRouteProviderWatcher.java b/com/android/support/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
new file mode 100644
index 00000000..ba1f647f
--- /dev/null
+++ b/com/android/support/mediarouter/media/RegisteredMediaRouteProviderWatcher.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+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.os.Handler;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/**
+ * Watches for media route provider services to be installed.
+ * Adds a provider to the media router for each registered service.
+ *
+ * @see RegisteredMediaRouteProvider
+ */
+final class RegisteredMediaRouteProviderWatcher {
+ private final Context mContext;
+ private final Callback mCallback;
+ private final Handler mHandler;
+ private final PackageManager mPackageManager;
+
+ private final ArrayList<RegisteredMediaRouteProvider> mProviders =
+ new ArrayList<RegisteredMediaRouteProvider>();
+ private boolean mRunning;
+
+ public RegisteredMediaRouteProviderWatcher(Context context, Callback callback) {
+ mContext = context;
+ mCallback = callback;
+ mHandler = new Handler();
+ mPackageManager = context.getPackageManager();
+ }
+
+ public void start() {
+ if (!mRunning) {
+ mRunning = true;
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_PACKAGE_ADDED);
+ filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
+ filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
+ filter.addAction(Intent.ACTION_PACKAGE_RESTARTED);
+ filter.addDataScheme("package");
+ mContext.registerReceiver(mScanPackagesReceiver, filter, null, mHandler);
+
+ // Scan packages.
+ // Also has the side-effect of restarting providers if needed.
+ mHandler.post(mScanPackagesRunnable);
+ }
+ }
+
+ public void stop() {
+ if (mRunning) {
+ mRunning = false;
+
+ mContext.unregisterReceiver(mScanPackagesReceiver);
+ mHandler.removeCallbacks(mScanPackagesRunnable);
+
+ // Stop all providers.
+ for (int i = mProviders.size() - 1; i >= 0; i--) {
+ mProviders.get(i).stop();
+ }
+ }
+ }
+
+ void scanPackages() {
+ if (!mRunning) {
+ return;
+ }
+
+ // Add providers for all new services.
+ // Reorder the list so that providers left at the end will be the ones to remove.
+ int targetIndex = 0;
+ Intent intent = new Intent(MediaRouteProviderService.SERVICE_INTERFACE);
+ for (ResolveInfo resolveInfo : mPackageManager.queryIntentServices(intent, 0)) {
+ ServiceInfo serviceInfo = resolveInfo.serviceInfo;
+ if (serviceInfo != null) {
+ int sourceIndex = findProvider(serviceInfo.packageName, serviceInfo.name);
+ if (sourceIndex < 0) {
+ RegisteredMediaRouteProvider provider =
+ new RegisteredMediaRouteProvider(mContext,
+ new ComponentName(serviceInfo.packageName, serviceInfo.name));
+ provider.start();
+ mProviders.add(targetIndex++, provider);
+ mCallback.addProvider(provider);
+ } else if (sourceIndex >= targetIndex) {
+ RegisteredMediaRouteProvider provider = mProviders.get(sourceIndex);
+ provider.start(); // restart the provider if needed
+ provider.rebindIfDisconnected();
+ Collections.swap(mProviders, sourceIndex, targetIndex++);
+ }
+ }
+ }
+
+ // Remove providers for missing services.
+ if (targetIndex < mProviders.size()) {
+ for (int i = mProviders.size() - 1; i >= targetIndex; i--) {
+ RegisteredMediaRouteProvider provider = mProviders.get(i);
+ mCallback.removeProvider(provider);
+ mProviders.remove(provider);
+ provider.stop();
+ }
+ }
+ }
+
+ private int findProvider(String packageName, String className) {
+ int count = mProviders.size();
+ for (int i = 0; i < count; i++) {
+ RegisteredMediaRouteProvider provider = mProviders.get(i);
+ if (provider.hasComponentName(packageName, className)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private final BroadcastReceiver mScanPackagesReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ scanPackages();
+ }
+ };
+
+ private final Runnable mScanPackagesRunnable = new Runnable() {
+ @Override
+ public void run() {
+ scanPackages();
+ }
+ };
+
+ public interface Callback {
+ void addProvider(MediaRouteProvider provider);
+ void removeProvider(MediaRouteProvider provider);
+ }
+}
diff --git a/com/android/support/mediarouter/media/RemoteControlClientCompat.java b/com/android/support/mediarouter/media/RemoteControlClientCompat.java
new file mode 100644
index 00000000..826449b2
--- /dev/null
+++ b/com/android/support/mediarouter/media/RemoteControlClientCompat.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * Provides access to features of the remote control client.
+ *
+ * Hidden for now but we might want to make this available to applications
+ * in the future.
+ */
+abstract class RemoteControlClientCompat {
+ protected final Context mContext;
+ protected final Object mRcc;
+ protected VolumeCallback mVolumeCallback;
+
+ protected RemoteControlClientCompat(Context context, Object rcc) {
+ mContext = context;
+ mRcc = rcc;
+ }
+
+ public static RemoteControlClientCompat obtain(Context context, Object rcc) {
+ if (Build.VERSION.SDK_INT >= 16) {
+ return new JellybeanImpl(context, rcc);
+ }
+ return new LegacyImpl(context, rcc);
+ }
+
+ public Object getRemoteControlClient() {
+ return mRcc;
+ }
+
+ /**
+ * Sets the current playback information.
+ * Must be called at least once to attach to the remote control client.
+ *
+ * @param info The playback information. Must not be null.
+ */
+ public void setPlaybackInfo(PlaybackInfo info) {
+ }
+
+ /**
+ * Sets a callback to receive volume change requests from the remote control client.
+ *
+ * @param callback The volume callback to use or null if none.
+ */
+ public void setVolumeCallback(VolumeCallback callback) {
+ mVolumeCallback = callback;
+ }
+
+ /**
+ * Specifies information about the playback.
+ */
+ public static final class PlaybackInfo {
+ public int volume;
+ public int volumeMax;
+ public int volumeHandling = MediaRouter.RouteInfo.PLAYBACK_VOLUME_FIXED;
+ public int playbackStream = AudioManager.STREAM_MUSIC;
+ public int playbackType = MediaRouter.RouteInfo.PLAYBACK_TYPE_REMOTE;
+ }
+
+ /**
+ * Called when volume updates are requested by the remote control client.
+ */
+ public interface VolumeCallback {
+ /**
+ * Called when the volume should be increased or decreased.
+ *
+ * @param direction An integer indicating whether the volume is to be increased
+ * (positive value) or decreased (negative value).
+ * For bundled changes, the absolute value indicates the number of changes
+ * in the same direction, e.g. +3 corresponds to three "volume up" changes.
+ */
+ public void onVolumeUpdateRequest(int direction);
+
+ /**
+ * Called when the volume for the route should be set to the given value.
+ *
+ * @param volume An integer indicating the new volume value that should be used,
+ * always between 0 and the value set by {@link PlaybackInfo#volumeMax}.
+ */
+ public void onVolumeSetRequest(int volume);
+ }
+
+ /**
+ * Legacy implementation for platform versions prior to Jellybean.
+ * Does nothing.
+ */
+ static class LegacyImpl extends RemoteControlClientCompat {
+ public LegacyImpl(Context context, Object rcc) {
+ super(context, rcc);
+ }
+ }
+
+ /**
+ * Implementation for Jellybean.
+ *
+ * The basic idea of this implementation is to attach the RCC to a UserRouteInfo
+ * in order to hook up stream metadata and volume callbacks because there is no
+ * other API available to do so in this platform version. The UserRouteInfo itself
+ * is not attached to the MediaRouter so it is transparent to the user.
+ */
+ // @@RequiresApi(16)
+ static class JellybeanImpl extends RemoteControlClientCompat {
+ private final Object mRouterObj;
+ private final Object mUserRouteCategoryObj;
+ private final Object mUserRouteObj;
+ private boolean mRegistered;
+
+ public JellybeanImpl(Context context, Object rcc) {
+ super(context, rcc);
+
+ mRouterObj = MediaRouterJellybean.getMediaRouter(context);
+ mUserRouteCategoryObj = MediaRouterJellybean.createRouteCategory(
+ mRouterObj, "", false);
+ mUserRouteObj = MediaRouterJellybean.createUserRoute(
+ mRouterObj, mUserRouteCategoryObj);
+ }
+
+ @Override
+ public void setPlaybackInfo(PlaybackInfo info) {
+ MediaRouterJellybean.UserRouteInfo.setVolume(
+ mUserRouteObj, info.volume);
+ MediaRouterJellybean.UserRouteInfo.setVolumeMax(
+ mUserRouteObj, info.volumeMax);
+ MediaRouterJellybean.UserRouteInfo.setVolumeHandling(
+ mUserRouteObj, info.volumeHandling);
+ MediaRouterJellybean.UserRouteInfo.setPlaybackStream(
+ mUserRouteObj, info.playbackStream);
+ MediaRouterJellybean.UserRouteInfo.setPlaybackType(
+ mUserRouteObj, info.playbackType);
+
+ if (!mRegistered) {
+ mRegistered = true;
+ MediaRouterJellybean.UserRouteInfo.setVolumeCallback(mUserRouteObj,
+ MediaRouterJellybean.createVolumeCallback(
+ new VolumeCallbackWrapper(this)));
+ MediaRouterJellybean.UserRouteInfo.setRemoteControlClient(mUserRouteObj, mRcc);
+ }
+ }
+
+ private static final class VolumeCallbackWrapper
+ implements MediaRouterJellybean.VolumeCallback {
+ // Unfortunately, the framework never unregisters its volume observer from
+ // the audio service so the UserRouteInfo object may leak along with
+ // any callbacks that we attach to it. Use a weak reference to prevent
+ // the volume callback from holding strong references to anything important.
+ private final WeakReference<JellybeanImpl> mImplWeak;
+
+ public VolumeCallbackWrapper(JellybeanImpl impl) {
+ mImplWeak = new WeakReference<JellybeanImpl>(impl);
+ }
+
+ @Override
+ public void onVolumeUpdateRequest(Object routeObj, int direction) {
+ JellybeanImpl impl = mImplWeak.get();
+ if (impl != null && impl.mVolumeCallback != null) {
+ impl.mVolumeCallback.onVolumeUpdateRequest(direction);
+ }
+ }
+
+ @Override
+ public void onVolumeSetRequest(Object routeObj, int volume) {
+ JellybeanImpl impl = mImplWeak.get();
+ if (impl != null && impl.mVolumeCallback != null) {
+ impl.mVolumeCallback.onVolumeSetRequest(volume);
+ }
+ }
+ }
+ }
+}
diff --git a/com/android/support/mediarouter/media/RemotePlaybackClient.java b/com/android/support/mediarouter/media/RemotePlaybackClient.java
new file mode 100644
index 00000000..f6e1497c
--- /dev/null
+++ b/com/android/support/mediarouter/media/RemotePlaybackClient.java
@@ -0,0 +1,1044 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.app.PendingIntent;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.v4.util.ObjectsCompat;
+import android.util.Log;
+
+/**
+ * A helper class for playing media on remote routes using the remote playback protocol
+ * defined by {@link MediaControlIntent}.
+ * <p>
+ * The client maintains session state and offers a simplified interface for issuing
+ * remote playback media control intents to a single route.
+ * </p>
+ */
+public class RemotePlaybackClient {
+ static final String TAG = "RemotePlaybackClient";
+ static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+ private final Context mContext;
+ private final MediaRouter.RouteInfo mRoute;
+ private final ActionReceiver mActionReceiver;
+ private final PendingIntent mItemStatusPendingIntent;
+ private final PendingIntent mSessionStatusPendingIntent;
+ private final PendingIntent mMessagePendingIntent;
+
+ private boolean mRouteSupportsRemotePlayback;
+ private boolean mRouteSupportsQueuing;
+ private boolean mRouteSupportsSessionManagement;
+ private boolean mRouteSupportsMessaging;
+
+ String mSessionId;
+ StatusCallback mStatusCallback;
+ OnMessageReceivedListener mOnMessageReceivedListener;
+
+ /**
+ * Creates a remote playback client for a route.
+ *
+ * @param route The media route.
+ */
+ public RemotePlaybackClient(Context context, MediaRouter.RouteInfo route) {
+ if (context == null) {
+ throw new IllegalArgumentException("context must not be null");
+ }
+ if (route == null) {
+ throw new IllegalArgumentException("route must not be null");
+ }
+
+ mContext = context;
+ mRoute = route;
+
+ IntentFilter actionFilter = new IntentFilter();
+ actionFilter.addAction(ActionReceiver.ACTION_ITEM_STATUS_CHANGED);
+ actionFilter.addAction(ActionReceiver.ACTION_SESSION_STATUS_CHANGED);
+ actionFilter.addAction(ActionReceiver.ACTION_MESSAGE_RECEIVED);
+ mActionReceiver = new ActionReceiver();
+ context.registerReceiver(mActionReceiver, actionFilter);
+
+ Intent itemStatusIntent = new Intent(ActionReceiver.ACTION_ITEM_STATUS_CHANGED);
+ itemStatusIntent.setPackage(context.getPackageName());
+ mItemStatusPendingIntent = PendingIntent.getBroadcast(
+ context, 0, itemStatusIntent, 0);
+
+ Intent sessionStatusIntent = new Intent(ActionReceiver.ACTION_SESSION_STATUS_CHANGED);
+ sessionStatusIntent.setPackage(context.getPackageName());
+ mSessionStatusPendingIntent = PendingIntent.getBroadcast(
+ context, 0, sessionStatusIntent, 0);
+
+ Intent messageIntent = new Intent(ActionReceiver.ACTION_MESSAGE_RECEIVED);
+ messageIntent.setPackage(context.getPackageName());
+ mMessagePendingIntent = PendingIntent.getBroadcast(
+ context, 0, messageIntent, 0);
+ detectFeatures();
+ }
+
+ /**
+ * Releases resources owned by the client.
+ */
+ public void release() {
+ mContext.unregisterReceiver(mActionReceiver);
+ }
+
+ /**
+ * Returns true if the route supports remote playback.
+ * <p>
+ * If the route does not support remote playback, then none of the functionality
+ * offered by the client will be available.
+ * </p><p>
+ * This method returns true if the route supports all of the following
+ * actions: {@link MediaControlIntent#ACTION_PLAY play},
+ * {@link MediaControlIntent#ACTION_SEEK seek},
+ * {@link MediaControlIntent#ACTION_GET_STATUS get status},
+ * {@link MediaControlIntent#ACTION_PAUSE pause},
+ * {@link MediaControlIntent#ACTION_RESUME resume},
+ * {@link MediaControlIntent#ACTION_STOP stop}.
+ * </p>
+ *
+ * @return True if remote playback is supported.
+ */
+ public boolean isRemotePlaybackSupported() {
+ return mRouteSupportsRemotePlayback;
+ }
+
+ /**
+ * Returns true if the route supports queuing features.
+ * <p>
+ * If the route does not support queuing, then at most one media item can be played
+ * at a time and the {@link #enqueue} method will not be available.
+ * </p><p>
+ * This method returns true if the route supports all of the basic remote playback
+ * actions and all of the following actions:
+ * {@link MediaControlIntent#ACTION_ENQUEUE enqueue},
+ * {@link MediaControlIntent#ACTION_REMOVE remove}.
+ * </p>
+ *
+ * @return True if queuing is supported. Implies {@link #isRemotePlaybackSupported}
+ * is also true.
+ *
+ * @see #isRemotePlaybackSupported
+ */
+ public boolean isQueuingSupported() {
+ return mRouteSupportsQueuing;
+ }
+
+ /**
+ * Returns true if the route supports session management features.
+ * <p>
+ * If the route does not support session management, then the session will
+ * not be created until the first media item is played.
+ * </p><p>
+ * This method returns true if the route supports all of the basic remote playback
+ * actions and all of the following actions:
+ * {@link MediaControlIntent#ACTION_START_SESSION start session},
+ * {@link MediaControlIntent#ACTION_GET_SESSION_STATUS get session status},
+ * {@link MediaControlIntent#ACTION_END_SESSION end session}.
+ * </p>
+ *
+ * @return True if session management is supported.
+ * Implies {@link #isRemotePlaybackSupported} is also true.
+ *
+ * @see #isRemotePlaybackSupported
+ */
+ public boolean isSessionManagementSupported() {
+ return mRouteSupportsSessionManagement;
+ }
+
+ /**
+ * Returns true if the route supports messages.
+ * <p>
+ * This method returns true if the route supports all of the basic remote playback
+ * actions and all of the following actions:
+ * {@link MediaControlIntent#ACTION_START_SESSION start session},
+ * {@link MediaControlIntent#ACTION_SEND_MESSAGE send message},
+ * {@link MediaControlIntent#ACTION_END_SESSION end session}.
+ * </p>
+ *
+ * @return True if session management is supported.
+ * Implies {@link #isRemotePlaybackSupported} is also true.
+ *
+ * @see #isRemotePlaybackSupported
+ */
+ public boolean isMessagingSupported() {
+ return mRouteSupportsMessaging;
+ }
+
+ /**
+ * Gets the current session id if there is one.
+ *
+ * @return The current session id, or null if none.
+ */
+ public String getSessionId() {
+ return mSessionId;
+ }
+
+ /**
+ * Sets the current session id.
+ * <p>
+ * It is usually not necessary to set the session id explicitly since
+ * it is created as a side-effect of other requests such as
+ * {@link #play}, {@link #enqueue}, and {@link #startSession}.
+ * </p>
+ *
+ * @param sessionId The new session id, or null if none.
+ */
+ public void setSessionId(String sessionId) {
+ if (!ObjectsCompat.equals(mSessionId, sessionId)) {
+ if (DEBUG) {
+ Log.d(TAG, "Session id is now: " + sessionId);
+ }
+ mSessionId = sessionId;
+ if (mStatusCallback != null) {
+ mStatusCallback.onSessionChanged(sessionId);
+ }
+ }
+ }
+
+ /**
+ * Returns true if the client currently has a session.
+ * <p>
+ * Equivalent to checking whether {@link #getSessionId} returns a non-null result.
+ * </p>
+ *
+ * @return True if there is a current session.
+ */
+ public boolean hasSession() {
+ return mSessionId != null;
+ }
+
+ /**
+ * Sets a callback that should receive status updates when the state of
+ * media sessions or media items created by this instance of the remote
+ * playback client changes.
+ * <p>
+ * The callback should be set before the session is created or any play
+ * commands are issued.
+ * </p>
+ *
+ * @param callback The callback to set. May be null to remove the previous callback.
+ */
+ public void setStatusCallback(StatusCallback callback) {
+ mStatusCallback = callback;
+ }
+
+ /**
+ * Sets a callback that should receive messages when a message is sent from
+ * media sessions created by this instance of the remote playback client changes.
+ * <p>
+ * The callback should be set before the session is created.
+ * </p>
+ *
+ * @param listener The callback to set. May be null to remove the previous callback.
+ */
+ public void setOnMessageReceivedListener(OnMessageReceivedListener listener) {
+ mOnMessageReceivedListener = listener;
+ }
+
+ /**
+ * Sends a request to play a media item.
+ * <p>
+ * Clears the queue and starts playing the new item immediately. If the queue
+ * was previously paused, then it is resumed as a side-effect of this request.
+ * </p><p>
+ * The request is issued in the current session. If no session is available, then
+ * one is created implicitly.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_PLAY ACTION_PLAY} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param contentUri The content Uri to play.
+ * @param mimeType The mime type of the content, or null if unknown.
+ * @param positionMillis The initial content position for the item in milliseconds,
+ * or <code>0</code> to start at the beginning.
+ * @param metadata The media item metadata bundle, or null if none.
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_PLAY} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws UnsupportedOperationException if the route does not support remote playback.
+ *
+ * @see MediaControlIntent#ACTION_PLAY
+ * @see #isRemotePlaybackSupported
+ */
+ public void play(Uri contentUri, String mimeType, Bundle metadata,
+ long positionMillis, Bundle extras, ItemActionCallback callback) {
+ playOrEnqueue(contentUri, mimeType, metadata, positionMillis,
+ extras, callback, MediaControlIntent.ACTION_PLAY);
+ }
+
+ /**
+ * Sends a request to enqueue a media item.
+ * <p>
+ * Enqueues a new item to play. If the queue was previously paused, then will
+ * remain paused.
+ * </p><p>
+ * The request is issued in the current session. If no session is available, then
+ * one is created implicitly.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_ENQUEUE ACTION_ENQUEUE} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param contentUri The content Uri to enqueue.
+ * @param mimeType The mime type of the content, or null if unknown.
+ * @param positionMillis The initial content position for the item in milliseconds,
+ * or <code>0</code> to start at the beginning.
+ * @param metadata The media item metadata bundle, or null if none.
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_ENQUEUE} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws UnsupportedOperationException if the route does not support queuing.
+ *
+ * @see MediaControlIntent#ACTION_ENQUEUE
+ * @see #isRemotePlaybackSupported
+ * @see #isQueuingSupported
+ */
+ public void enqueue(Uri contentUri, String mimeType, Bundle metadata,
+ long positionMillis, Bundle extras, ItemActionCallback callback) {
+ playOrEnqueue(contentUri, mimeType, metadata, positionMillis,
+ extras, callback, MediaControlIntent.ACTION_ENQUEUE);
+ }
+
+ private void playOrEnqueue(Uri contentUri, String mimeType, Bundle metadata,
+ long positionMillis, Bundle extras,
+ final ItemActionCallback callback, String action) {
+ if (contentUri == null) {
+ throw new IllegalArgumentException("contentUri must not be null");
+ }
+ throwIfRemotePlaybackNotSupported();
+ if (action.equals(MediaControlIntent.ACTION_ENQUEUE)) {
+ throwIfQueuingNotSupported();
+ }
+
+ Intent intent = new Intent(action);
+ intent.setDataAndType(contentUri, mimeType);
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_STATUS_UPDATE_RECEIVER,
+ mItemStatusPendingIntent);
+ if (metadata != null) {
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_METADATA, metadata);
+ }
+ if (positionMillis != 0) {
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, positionMillis);
+ }
+ performItemAction(intent, mSessionId, null, extras, callback);
+ }
+
+ /**
+ * Sends a request to seek to a new position in a media item.
+ * <p>
+ * Seeks to a new position. If the queue was previously paused then it
+ * remains paused but the item's new position is still remembered.
+ * </p><p>
+ * The request is issued in the current session.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_SEEK ACTION_SEEK} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param itemId The item id.
+ * @param positionMillis The new content position for the item in milliseconds,
+ * or <code>0</code> to start at the beginning.
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_SEEK} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ *
+ * @see MediaControlIntent#ACTION_SEEK
+ * @see #isRemotePlaybackSupported
+ */
+ public void seek(String itemId, long positionMillis, Bundle extras,
+ ItemActionCallback callback) {
+ if (itemId == null) {
+ throw new IllegalArgumentException("itemId must not be null");
+ }
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_SEEK);
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_CONTENT_POSITION, positionMillis);
+ performItemAction(intent, mSessionId, itemId, extras, callback);
+ }
+
+ /**
+ * Sends a request to get the status of a media item.
+ * <p>
+ * The request is issued in the current session.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_GET_STATUS ACTION_GET_STATUS} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param itemId The item id.
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_GET_STATUS} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ *
+ * @see MediaControlIntent#ACTION_GET_STATUS
+ * @see #isRemotePlaybackSupported
+ */
+ public void getStatus(String itemId, Bundle extras, ItemActionCallback callback) {
+ if (itemId == null) {
+ throw new IllegalArgumentException("itemId must not be null");
+ }
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_GET_STATUS);
+ performItemAction(intent, mSessionId, itemId, extras, callback);
+ }
+
+ /**
+ * Sends a request to remove a media item from the queue.
+ * <p>
+ * The request is issued in the current session.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_REMOVE ACTION_REMOVE} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param itemId The item id.
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_REMOVE} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ * @throws UnsupportedOperationException if the route does not support queuing.
+ *
+ * @see MediaControlIntent#ACTION_REMOVE
+ * @see #isRemotePlaybackSupported
+ * @see #isQueuingSupported
+ */
+ public void remove(String itemId, Bundle extras, ItemActionCallback callback) {
+ if (itemId == null) {
+ throw new IllegalArgumentException("itemId must not be null");
+ }
+ throwIfQueuingNotSupported();
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_REMOVE);
+ performItemAction(intent, mSessionId, itemId, extras, callback);
+ }
+
+ /**
+ * Sends a request to pause media playback.
+ * <p>
+ * The request is issued in the current session. If playback is already paused
+ * then the request has no effect.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_PAUSE ACTION_PAUSE} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_PAUSE} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ *
+ * @see MediaControlIntent#ACTION_PAUSE
+ * @see #isRemotePlaybackSupported
+ */
+ public void pause(Bundle extras, SessionActionCallback callback) {
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_PAUSE);
+ performSessionAction(intent, mSessionId, extras, callback);
+ }
+
+ /**
+ * Sends a request to resume (unpause) media playback.
+ * <p>
+ * The request is issued in the current session. If playback is not paused
+ * then the request has no effect.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_RESUME ACTION_RESUME} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_RESUME} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ *
+ * @see MediaControlIntent#ACTION_RESUME
+ * @see #isRemotePlaybackSupported
+ */
+ public void resume(Bundle extras, SessionActionCallback callback) {
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_RESUME);
+ performSessionAction(intent, mSessionId, extras, callback);
+ }
+
+ /**
+ * Sends a request to stop media playback and clear the media playback queue.
+ * <p>
+ * The request is issued in the current session. If the queue is already
+ * empty then the request has no effect.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_STOP ACTION_STOP} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_STOP} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ *
+ * @see MediaControlIntent#ACTION_STOP
+ * @see #isRemotePlaybackSupported
+ */
+ public void stop(Bundle extras, SessionActionCallback callback) {
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_STOP);
+ performSessionAction(intent, mSessionId, extras, callback);
+ }
+
+ /**
+ * Sends a request to start a new media playback session.
+ * <p>
+ * The application must wait for the callback to indicate that this request
+ * is complete before issuing other requests that affect the session. If this
+ * request is successful then the previous session will be invalidated.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_START_SESSION ACTION_START_SESSION}
+ * for more information about the semantics of this request.
+ * </p>
+ *
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_START_SESSION} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws UnsupportedOperationException if the route does not support session management.
+ *
+ * @see MediaControlIntent#ACTION_START_SESSION
+ * @see #isRemotePlaybackSupported
+ * @see #isSessionManagementSupported
+ */
+ public void startSession(Bundle extras, SessionActionCallback callback) {
+ throwIfSessionManagementNotSupported();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_START_SESSION);
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_STATUS_UPDATE_RECEIVER,
+ mSessionStatusPendingIntent);
+ if (mRouteSupportsMessaging) {
+ intent.putExtra(MediaControlIntent.EXTRA_MESSAGE_RECEIVER, mMessagePendingIntent);
+ }
+ performSessionAction(intent, null, extras, callback);
+ }
+
+ /**
+ * Sends a message.
+ * <p>
+ * The request is issued in the current session.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_SEND_MESSAGE} for
+ * more information about the semantics of this request.
+ * </p>
+ *
+ * @param message A bundle message denoting {@link MediaControlIntent#EXTRA_MESSAGE}.
+ * @param callback A callback to invoke when the request has been processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ * @throws UnsupportedOperationException if the route does not support messages.
+ *
+ * @see MediaControlIntent#ACTION_SEND_MESSAGE
+ * @see #isMessagingSupported
+ */
+ public void sendMessage(Bundle message, SessionActionCallback callback) {
+ throwIfNoCurrentSession();
+ throwIfMessageNotSupported();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_SEND_MESSAGE);
+ performSessionAction(intent, mSessionId, message, callback);
+ }
+
+ /**
+ * Sends a request to get the status of the media playback session.
+ * <p>
+ * The request is issued in the current session.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_GET_SESSION_STATUS
+ * ACTION_GET_SESSION_STATUS} for more information about the semantics of this request.
+ * </p>
+ *
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_GET_SESSION_STATUS} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ * @throws UnsupportedOperationException if the route does not support session management.
+ *
+ * @see MediaControlIntent#ACTION_GET_SESSION_STATUS
+ * @see #isRemotePlaybackSupported
+ * @see #isSessionManagementSupported
+ */
+ public void getSessionStatus(Bundle extras, SessionActionCallback callback) {
+ throwIfSessionManagementNotSupported();
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_GET_SESSION_STATUS);
+ performSessionAction(intent, mSessionId, extras, callback);
+ }
+
+ /**
+ * Sends a request to end the media playback session.
+ * <p>
+ * The request is issued in the current session. If this request is successful,
+ * the {@link #getSessionId session id property} will be set to null after
+ * the callback is invoked.
+ * </p><p>
+ * Please refer to {@link MediaControlIntent#ACTION_END_SESSION ACTION_END_SESSION}
+ * for more information about the semantics of this request.
+ * </p>
+ *
+ * @param extras A bundle of extra arguments to be added to the
+ * {@link MediaControlIntent#ACTION_END_SESSION} intent, or null if none.
+ * @param callback A callback to invoke when the request has been
+ * processed, or null if none.
+ *
+ * @throws IllegalStateException if there is no current session.
+ * @throws UnsupportedOperationException if the route does not support session management.
+ *
+ * @see MediaControlIntent#ACTION_END_SESSION
+ * @see #isRemotePlaybackSupported
+ * @see #isSessionManagementSupported
+ */
+ public void endSession(Bundle extras, SessionActionCallback callback) {
+ throwIfSessionManagementNotSupported();
+ throwIfNoCurrentSession();
+
+ Intent intent = new Intent(MediaControlIntent.ACTION_END_SESSION);
+ performSessionAction(intent, mSessionId, extras, callback);
+ }
+
+ private void performItemAction(final Intent intent,
+ final String sessionId, final String itemId,
+ Bundle extras, final ItemActionCallback callback) {
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ if (sessionId != null) {
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId);
+ }
+ if (itemId != null) {
+ intent.putExtra(MediaControlIntent.EXTRA_ITEM_ID, itemId);
+ }
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ logRequest(intent);
+ mRoute.sendControlRequest(intent, new MediaRouter.ControlRequestCallback() {
+ @Override
+ public void onResult(Bundle data) {
+ if (data != null) {
+ String sessionIdResult = inferMissingResult(sessionId,
+ data.getString(MediaControlIntent.EXTRA_SESSION_ID));
+ MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle(
+ data.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS));
+ String itemIdResult = inferMissingResult(itemId,
+ data.getString(MediaControlIntent.EXTRA_ITEM_ID));
+ MediaItemStatus itemStatus = MediaItemStatus.fromBundle(
+ data.getBundle(MediaControlIntent.EXTRA_ITEM_STATUS));
+ adoptSession(sessionIdResult);
+ if (sessionIdResult != null && itemIdResult != null && itemStatus != null) {
+ if (DEBUG) {
+ Log.d(TAG, "Received result from " + intent.getAction()
+ + ": data=" + bundleToString(data)
+ + ", sessionId=" + sessionIdResult
+ + ", sessionStatus=" + sessionStatus
+ + ", itemId=" + itemIdResult
+ + ", itemStatus=" + itemStatus);
+ }
+ callback.onResult(data, sessionIdResult, sessionStatus,
+ itemIdResult, itemStatus);
+ return;
+ }
+ }
+ handleInvalidResult(intent, callback, data);
+ }
+
+ @Override
+ public void onError(String error, Bundle data) {
+ handleError(intent, callback, error, data);
+ }
+ });
+ }
+
+ private void performSessionAction(final Intent intent, final String sessionId,
+ Bundle extras, final SessionActionCallback callback) {
+ intent.addCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK);
+ if (sessionId != null) {
+ intent.putExtra(MediaControlIntent.EXTRA_SESSION_ID, sessionId);
+ }
+ if (extras != null) {
+ intent.putExtras(extras);
+ }
+ logRequest(intent);
+ mRoute.sendControlRequest(intent, new MediaRouter.ControlRequestCallback() {
+ @Override
+ public void onResult(Bundle data) {
+ if (data != null) {
+ String sessionIdResult = inferMissingResult(sessionId,
+ data.getString(MediaControlIntent.EXTRA_SESSION_ID));
+ MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle(
+ data.getBundle(MediaControlIntent.EXTRA_SESSION_STATUS));
+ adoptSession(sessionIdResult);
+ if (sessionIdResult != null) {
+ if (DEBUG) {
+ Log.d(TAG, "Received result from " + intent.getAction()
+ + ": data=" + bundleToString(data)
+ + ", sessionId=" + sessionIdResult
+ + ", sessionStatus=" + sessionStatus);
+ }
+ try {
+ callback.onResult(data, sessionIdResult, sessionStatus);
+ } finally {
+ if (intent.getAction().equals(MediaControlIntent.ACTION_END_SESSION)
+ && sessionIdResult.equals(mSessionId)) {
+ setSessionId(null);
+ }
+ }
+ return;
+ }
+ }
+ handleInvalidResult(intent, callback, data);
+ }
+
+ @Override
+ public void onError(String error, Bundle data) {
+ handleError(intent, callback, error, data);
+ }
+ });
+ }
+
+ void adoptSession(String sessionId) {
+ if (sessionId != null) {
+ setSessionId(sessionId);
+ }
+ }
+
+ void handleInvalidResult(Intent intent, ActionCallback callback,
+ Bundle data) {
+ Log.w(TAG, "Received invalid result data from " + intent.getAction()
+ + ": data=" + bundleToString(data));
+ callback.onError(null, MediaControlIntent.ERROR_UNKNOWN, data);
+ }
+
+ void handleError(Intent intent, ActionCallback callback,
+ String error, Bundle data) {
+ final int code;
+ if (data != null) {
+ code = data.getInt(MediaControlIntent.EXTRA_ERROR_CODE,
+ MediaControlIntent.ERROR_UNKNOWN);
+ } else {
+ code = MediaControlIntent.ERROR_UNKNOWN;
+ }
+ if (DEBUG) {
+ Log.w(TAG, "Received error from " + intent.getAction()
+ + ": error=" + error
+ + ", code=" + code
+ + ", data=" + bundleToString(data));
+ }
+ callback.onError(error, code, data);
+ }
+
+ private void detectFeatures() {
+ mRouteSupportsRemotePlayback = routeSupportsAction(MediaControlIntent.ACTION_PLAY)
+ && routeSupportsAction(MediaControlIntent.ACTION_SEEK)
+ && routeSupportsAction(MediaControlIntent.ACTION_GET_STATUS)
+ && routeSupportsAction(MediaControlIntent.ACTION_PAUSE)
+ && routeSupportsAction(MediaControlIntent.ACTION_RESUME)
+ && routeSupportsAction(MediaControlIntent.ACTION_STOP);
+ mRouteSupportsQueuing = mRouteSupportsRemotePlayback
+ && routeSupportsAction(MediaControlIntent.ACTION_ENQUEUE)
+ && routeSupportsAction(MediaControlIntent.ACTION_REMOVE);
+ mRouteSupportsSessionManagement = mRouteSupportsRemotePlayback
+ && routeSupportsAction(MediaControlIntent.ACTION_START_SESSION)
+ && routeSupportsAction(MediaControlIntent.ACTION_GET_SESSION_STATUS)
+ && routeSupportsAction(MediaControlIntent.ACTION_END_SESSION);
+ mRouteSupportsMessaging = doesRouteSupportMessaging();
+ }
+
+ private boolean routeSupportsAction(String action) {
+ return mRoute.supportsControlAction(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK, action);
+ }
+
+ private boolean doesRouteSupportMessaging() {
+ for (IntentFilter filter : mRoute.getControlFilters()) {
+ if (filter.hasAction(MediaControlIntent.ACTION_SEND_MESSAGE)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private void throwIfRemotePlaybackNotSupported() {
+ if (!mRouteSupportsRemotePlayback) {
+ throw new UnsupportedOperationException("The route does not support remote playback.");
+ }
+ }
+
+ private void throwIfQueuingNotSupported() {
+ if (!mRouteSupportsQueuing) {
+ throw new UnsupportedOperationException("The route does not support queuing.");
+ }
+ }
+
+ private void throwIfSessionManagementNotSupported() {
+ if (!mRouteSupportsSessionManagement) {
+ throw new UnsupportedOperationException("The route does not support "
+ + "session management.");
+ }
+ }
+
+ private void throwIfMessageNotSupported() {
+ if (!mRouteSupportsMessaging) {
+ throw new UnsupportedOperationException("The route does not support message.");
+ }
+ }
+
+ private void throwIfNoCurrentSession() {
+ if (mSessionId == null) {
+ throw new IllegalStateException("There is no current session.");
+ }
+ }
+
+ static String inferMissingResult(String request, String result) {
+ if (result == null) {
+ // Result is missing.
+ return request;
+ }
+ if (request == null || request.equals(result)) {
+ // Request didn't specify a value or result matches request.
+ return result;
+ }
+ // Result conflicts with request.
+ return null;
+ }
+
+ private static void logRequest(Intent intent) {
+ if (DEBUG) {
+ Log.d(TAG, "Sending request: " + intent);
+ }
+ }
+
+ static String bundleToString(Bundle bundle) {
+ if (bundle != null) {
+ bundle.size(); // force bundle to be unparcelled
+ return bundle.toString();
+ }
+ return "null";
+ }
+
+ private final class ActionReceiver extends BroadcastReceiver {
+ public static final String ACTION_ITEM_STATUS_CHANGED =
+ "android.support.v7.media.actions.ACTION_ITEM_STATUS_CHANGED";
+ public static final String ACTION_SESSION_STATUS_CHANGED =
+ "android.support.v7.media.actions.ACTION_SESSION_STATUS_CHANGED";
+ public static final String ACTION_MESSAGE_RECEIVED =
+ "android.support.v7.media.actions.ACTION_MESSAGE_RECEIVED";
+
+ ActionReceiver() {
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String sessionId = intent.getStringExtra(MediaControlIntent.EXTRA_SESSION_ID);
+ if (sessionId == null || !sessionId.equals(mSessionId)) {
+ Log.w(TAG, "Discarding spurious status callback "
+ + "with missing or invalid session id: sessionId=" + sessionId);
+ return;
+ }
+
+ MediaSessionStatus sessionStatus = MediaSessionStatus.fromBundle(
+ intent.getBundleExtra(MediaControlIntent.EXTRA_SESSION_STATUS));
+ String action = intent.getAction();
+ if (action.equals(ACTION_ITEM_STATUS_CHANGED)) {
+ String itemId = intent.getStringExtra(MediaControlIntent.EXTRA_ITEM_ID);
+ if (itemId == null) {
+ Log.w(TAG, "Discarding spurious status callback with missing item id.");
+ return;
+ }
+
+ MediaItemStatus itemStatus = MediaItemStatus.fromBundle(
+ intent.getBundleExtra(MediaControlIntent.EXTRA_ITEM_STATUS));
+ if (itemStatus == null) {
+ Log.w(TAG, "Discarding spurious status callback with missing item status.");
+ return;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Received item status callback: sessionId=" + sessionId
+ + ", sessionStatus=" + sessionStatus
+ + ", itemId=" + itemId
+ + ", itemStatus=" + itemStatus);
+ }
+
+ if (mStatusCallback != null) {
+ mStatusCallback.onItemStatusChanged(intent.getExtras(),
+ sessionId, sessionStatus, itemId, itemStatus);
+ }
+ } else if (action.equals(ACTION_SESSION_STATUS_CHANGED)) {
+ if (sessionStatus == null) {
+ Log.w(TAG, "Discarding spurious media status callback with "
+ +"missing session status.");
+ return;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Received session status callback: sessionId=" + sessionId
+ + ", sessionStatus=" + sessionStatus);
+ }
+
+ if (mStatusCallback != null) {
+ mStatusCallback.onSessionStatusChanged(intent.getExtras(),
+ sessionId, sessionStatus);
+ }
+ } else if (action.equals(ACTION_MESSAGE_RECEIVED)) {
+ if (DEBUG) {
+ Log.d(TAG, "Received message callback: sessionId=" + sessionId);
+ }
+
+ if (mOnMessageReceivedListener != null) {
+ mOnMessageReceivedListener.onMessageReceived(sessionId,
+ intent.getBundleExtra(MediaControlIntent.EXTRA_MESSAGE));
+ }
+ }
+ }
+ }
+
+ /**
+ * A callback that will receive media status updates.
+ */
+ public static abstract class StatusCallback {
+ /**
+ * Called when the status of a media item changes.
+ *
+ * @param data The result data bundle.
+ * @param sessionId The session id.
+ * @param sessionStatus The session status, or null if unknown.
+ * @param itemId The item id.
+ * @param itemStatus The item status.
+ */
+ public void onItemStatusChanged(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus,
+ String itemId, MediaItemStatus itemStatus) {
+ }
+
+ /**
+ * Called when the status of a media session changes.
+ *
+ * @param data The result data bundle.
+ * @param sessionId The session id.
+ * @param sessionStatus The session status, or null if unknown.
+ */
+ public void onSessionStatusChanged(Bundle data,
+ String sessionId, MediaSessionStatus sessionStatus) {
+ }
+
+ /**
+ * Called when the session of the remote playback client changes.
+ *
+ * @param sessionId The new session id.
+ */
+ public void onSessionChanged(String sessionId) {
+ }
+ }
+
+ /**
+ * Base callback type for remote playback requests.
+ */
+ public static abstract class ActionCallback {
+ /**
+ * Called when a media control request fails.
+ *
+ * @param error A localized error message which may be shown to the user, or null
+ * if the cause of the error is unclear.
+ * @param code The error code, or {@link MediaControlIntent#ERROR_UNKNOWN} if unknown.
+ * @param data The error data bundle, or null if none.
+ */
+ public void onError(String error, int code, Bundle data) {
+ }
+ }
+
+ /**
+ * Callback for remote playback requests that operate on items.
+ */
+ public static abstract class ItemActionCallback extends ActionCallback {
+ /**
+ * Called when the request succeeds.
+ *
+ * @param data The result data bundle.
+ * @param sessionId The session id.
+ * @param sessionStatus The session status, or null if unknown.
+ * @param itemId The item id.
+ * @param itemStatus The item status.
+ */
+ public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus,
+ String itemId, MediaItemStatus itemStatus) {
+ }
+ }
+
+ /**
+ * Callback for remote playback requests that operate on sessions.
+ */
+ public static abstract class SessionActionCallback extends ActionCallback {
+ /**
+ * Called when the request succeeds.
+ *
+ * @param data The result data bundle.
+ * @param sessionId The session id.
+ * @param sessionStatus The session status, or null if unknown.
+ */
+ public void onResult(Bundle data, String sessionId, MediaSessionStatus sessionStatus) {
+ }
+ }
+
+ /**
+ * A callback that will receive messages from media sessions.
+ */
+ public interface OnMessageReceivedListener {
+ /**
+ * Called when a message received.
+ *
+ * @param sessionId The session id.
+ * @param message A bundle message denoting {@link MediaControlIntent#EXTRA_MESSAGE}.
+ */
+ void onMessageReceived(String sessionId, Bundle message);
+ }
+}
diff --git a/com/android/support/mediarouter/media/SystemMediaRouteProvider.java b/com/android/support/mediarouter/media/SystemMediaRouteProvider.java
new file mode 100644
index 00000000..33d92b42
--- /dev/null
+++ b/com/android/support/mediarouter/media/SystemMediaRouteProvider.java
@@ -0,0 +1,883 @@
+/*
+ * Copyright 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 com.android.support.mediarouter.media;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.media.AudioManager;
+import android.os.Build;
+import android.support.annotation.RequiresApi;
+import android.view.Display;
+
+import com.android.media.update.ApiHelper;
+import com.android.media.update.R;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Provides routes for built-in system destinations such as the local display
+ * and speaker. On Jellybean and newer platform releases, queries the framework
+ * MediaRouter for framework-provided routes and registers non-framework-provided
+ * routes as user routes.
+ */
+abstract class SystemMediaRouteProvider extends MediaRouteProvider {
+ private static final String TAG = "SystemMediaRouteProvider";
+
+ public static final String PACKAGE_NAME = "android";
+ public static final String DEFAULT_ROUTE_ID = "DEFAULT_ROUTE";
+
+ protected SystemMediaRouteProvider(Context context) {
+ super(context, new ProviderMetadata(new ComponentName(PACKAGE_NAME,
+ SystemMediaRouteProvider.class.getName())));
+ }
+
+ public static SystemMediaRouteProvider obtain(Context context, SyncCallback syncCallback) {
+ if (Build.VERSION.SDK_INT >= 24) {
+ return new Api24Impl(context, syncCallback);
+ }
+ if (Build.VERSION.SDK_INT >= 18) {
+ return new JellybeanMr2Impl(context, syncCallback);
+ }
+ if (Build.VERSION.SDK_INT >= 17) {
+ return new JellybeanMr1Impl(context, syncCallback);
+ }
+ if (Build.VERSION.SDK_INT >= 16) {
+ return new JellybeanImpl(context, syncCallback);
+ }
+ return new LegacyImpl(context);
+ }
+
+ /**
+ * Called by the media router when a route is added to synchronize state with
+ * the framework media router.
+ */
+ public void onSyncRouteAdded(MediaRouter.RouteInfo route) {
+ }
+
+ /**
+ * Called by the media router when a route is removed to synchronize state with
+ * the framework media router.
+ */
+ public void onSyncRouteRemoved(MediaRouter.RouteInfo route) {
+ }
+
+ /**
+ * Called by the media router when a route is changed to synchronize state with
+ * the framework media router.
+ */
+ public void onSyncRouteChanged(MediaRouter.RouteInfo route) {
+ }
+
+ /**
+ * Called by the media router when a route is selected to synchronize state with
+ * the framework media router.
+ */
+ public void onSyncRouteSelected(MediaRouter.RouteInfo route) {
+ }
+
+ /**
+ * Callbacks into the media router to synchronize state with the framework media router.
+ */
+ public interface SyncCallback {
+ void onSystemRouteSelectedByDescriptorId(String id);
+ }
+
+ protected Object getDefaultRoute() {
+ return null;
+ }
+
+ protected Object getSystemRoute(MediaRouter.RouteInfo route) {
+ return null;
+ }
+
+ /**
+ * Legacy implementation for platform versions prior to Jellybean.
+ */
+ static class LegacyImpl extends SystemMediaRouteProvider {
+ static final int PLAYBACK_STREAM = AudioManager.STREAM_MUSIC;
+
+ private static final ArrayList<IntentFilter> CONTROL_FILTERS;
+ static {
+ IntentFilter f = new IntentFilter();
+ f.addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+ f.addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+
+ CONTROL_FILTERS = new ArrayList<IntentFilter>();
+ CONTROL_FILTERS.add(f);
+ }
+
+ final AudioManager mAudioManager;
+ private final VolumeChangeReceiver mVolumeChangeReceiver;
+ int mLastReportedVolume = -1;
+
+ public LegacyImpl(Context context) {
+ super(context);
+ mAudioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE);
+ mVolumeChangeReceiver = new VolumeChangeReceiver();
+
+ context.registerReceiver(mVolumeChangeReceiver,
+ new IntentFilter(VolumeChangeReceiver.VOLUME_CHANGED_ACTION));
+ publishRoutes();
+ }
+
+ void publishRoutes() {
+ Resources r = getContext().getResources();
+ int maxVolume = mAudioManager.getStreamMaxVolume(PLAYBACK_STREAM);
+ mLastReportedVolume = mAudioManager.getStreamVolume(PLAYBACK_STREAM);
+ MediaRouteDescriptor defaultRoute = new MediaRouteDescriptor.Builder(
+ DEFAULT_ROUTE_ID, r.getString(R.string.mr_system_route_name))
+ .addControlFilters(CONTROL_FILTERS)
+ .setPlaybackStream(PLAYBACK_STREAM)
+ .setPlaybackType(MediaRouter.RouteInfo.PLAYBACK_TYPE_LOCAL)
+ .setVolumeHandling(MediaRouter.RouteInfo.PLAYBACK_VOLUME_VARIABLE)
+ .setVolumeMax(maxVolume)
+ .setVolume(mLastReportedVolume)
+ .build();
+
+ MediaRouteProviderDescriptor providerDescriptor =
+ new MediaRouteProviderDescriptor.Builder()
+ .addRoute(defaultRoute)
+ .build();
+ setDescriptor(providerDescriptor);
+ }
+
+ @Override
+ public RouteController onCreateRouteController(String routeId) {
+ if (routeId.equals(DEFAULT_ROUTE_ID)) {
+ return new DefaultRouteController();
+ }
+ return null;
+ }
+
+ final class DefaultRouteController extends RouteController {
+ @Override
+ public void onSetVolume(int volume) {
+ mAudioManager.setStreamVolume(PLAYBACK_STREAM, volume, 0);
+ publishRoutes();
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ int volume = mAudioManager.getStreamVolume(PLAYBACK_STREAM);
+ int maxVolume = mAudioManager.getStreamMaxVolume(PLAYBACK_STREAM);
+ int newVolume = Math.min(maxVolume, Math.max(0, volume + delta));
+ if (newVolume != volume) {
+ mAudioManager.setStreamVolume(PLAYBACK_STREAM, volume, 0);
+ }
+ publishRoutes();
+ }
+ }
+
+ final class VolumeChangeReceiver extends BroadcastReceiver {
+ // These constants come from AudioManager.
+ public static final String VOLUME_CHANGED_ACTION =
+ "android.media.VOLUME_CHANGED_ACTION";
+ public static final String EXTRA_VOLUME_STREAM_TYPE =
+ "android.media.EXTRA_VOLUME_STREAM_TYPE";
+ public static final String EXTRA_VOLUME_STREAM_VALUE =
+ "android.media.EXTRA_VOLUME_STREAM_VALUE";
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(VOLUME_CHANGED_ACTION)) {
+ final int streamType = intent.getIntExtra(EXTRA_VOLUME_STREAM_TYPE, -1);
+ if (streamType == PLAYBACK_STREAM) {
+ final int volume = intent.getIntExtra(EXTRA_VOLUME_STREAM_VALUE, -1);
+ if (volume >= 0 && volume != mLastReportedVolume) {
+ publishRoutes();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Jellybean implementation.
+ */
+ // @@RequiresApi(16)
+ static class JellybeanImpl extends SystemMediaRouteProvider
+ implements MediaRouterJellybean.Callback, MediaRouterJellybean.VolumeCallback {
+ private static final ArrayList<IntentFilter> LIVE_AUDIO_CONTROL_FILTERS;
+ static {
+ IntentFilter f = new IntentFilter();
+ f.addCategory(MediaControlIntent.CATEGORY_LIVE_AUDIO);
+
+ LIVE_AUDIO_CONTROL_FILTERS = new ArrayList<IntentFilter>();
+ LIVE_AUDIO_CONTROL_FILTERS.add(f);
+ }
+
+ private static final ArrayList<IntentFilter> LIVE_VIDEO_CONTROL_FILTERS;
+ static {
+ IntentFilter f = new IntentFilter();
+ f.addCategory(MediaControlIntent.CATEGORY_LIVE_VIDEO);
+
+ LIVE_VIDEO_CONTROL_FILTERS = new ArrayList<IntentFilter>();
+ LIVE_VIDEO_CONTROL_FILTERS.add(f);
+ }
+
+ private final SyncCallback mSyncCallback;
+
+ protected final Object mRouterObj;
+ protected final Object mCallbackObj;
+ protected final Object mVolumeCallbackObj;
+ protected final Object mUserRouteCategoryObj;
+ protected int mRouteTypes;
+ protected boolean mActiveScan;
+ protected boolean mCallbackRegistered;
+
+ // Maintains an association from framework routes to support library routes.
+ // Note that we cannot use the tag field for this because an application may
+ // have published its own user routes to the framework media router and already
+ // used the tag for its own purposes.
+ protected final ArrayList<SystemRouteRecord> mSystemRouteRecords =
+ new ArrayList<SystemRouteRecord>();
+
+ // Maintains an association from support library routes to framework routes.
+ protected final ArrayList<UserRouteRecord> mUserRouteRecords =
+ new ArrayList<UserRouteRecord>();
+
+ private MediaRouterJellybean.SelectRouteWorkaround mSelectRouteWorkaround;
+ private MediaRouterJellybean.GetDefaultRouteWorkaround mGetDefaultRouteWorkaround;
+
+ public JellybeanImpl(Context context, SyncCallback syncCallback) {
+ super(context);
+ mSyncCallback = syncCallback;
+ mRouterObj = MediaRouterJellybean.getMediaRouter(context);
+ mCallbackObj = createCallbackObj();
+ mVolumeCallbackObj = createVolumeCallbackObj();
+
+ Resources r = ApiHelper.getLibResources();
+ mUserRouteCategoryObj = MediaRouterJellybean.createRouteCategory(
+ mRouterObj, r.getString(R.string.mr_user_route_category_name), false);
+
+ updateSystemRoutes();
+ }
+
+ @Override
+ public RouteController onCreateRouteController(String routeId) {
+ int index = findSystemRouteRecordByDescriptorId(routeId);
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ return new SystemRouteController(record.mRouteObj);
+ }
+ return null;
+ }
+
+ @Override
+ public void onDiscoveryRequestChanged(MediaRouteDiscoveryRequest request) {
+ int newRouteTypes = 0;
+ boolean newActiveScan = false;
+ if (request != null) {
+ final MediaRouteSelector selector = request.getSelector();
+ final List<String> categories = selector.getControlCategories();
+ final int count = categories.size();
+ for (int i = 0; i < count; i++) {
+ String category = categories.get(i);
+ if (category.equals(MediaControlIntent.CATEGORY_LIVE_AUDIO)) {
+ newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO;
+ } else if (category.equals(MediaControlIntent.CATEGORY_LIVE_VIDEO)) {
+ newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO;
+ } else {
+ newRouteTypes |= MediaRouterJellybean.ROUTE_TYPE_USER;
+ }
+ }
+ newActiveScan = request.isActiveScan();
+ }
+
+ if (mRouteTypes != newRouteTypes || mActiveScan != newActiveScan) {
+ mRouteTypes = newRouteTypes;
+ mActiveScan = newActiveScan;
+ updateSystemRoutes();
+ }
+ }
+
+ @Override
+ public void onRouteAdded(Object routeObj) {
+ if (addSystemRouteNoPublish(routeObj)) {
+ publishRoutes();
+ }
+ }
+
+ private void updateSystemRoutes() {
+ updateCallback();
+ boolean changed = false;
+ for (Object routeObj : MediaRouterJellybean.getRoutes(mRouterObj)) {
+ changed |= addSystemRouteNoPublish(routeObj);
+ }
+ if (changed) {
+ publishRoutes();
+ }
+ }
+
+ private boolean addSystemRouteNoPublish(Object routeObj) {
+ if (getUserRouteRecord(routeObj) == null
+ && findSystemRouteRecord(routeObj) < 0) {
+ String id = assignRouteId(routeObj);
+ SystemRouteRecord record = new SystemRouteRecord(routeObj, id);
+ updateSystemRouteDescriptor(record);
+ mSystemRouteRecords.add(record);
+ return true;
+ }
+ return false;
+ }
+
+ private String assignRouteId(Object routeObj) {
+ // TODO: The framework media router should supply a unique route id that
+ // we can use here. For now we use a hash of the route name and take care
+ // to dedupe it.
+ boolean isDefault = (getDefaultRoute() == routeObj);
+ String id = isDefault ? DEFAULT_ROUTE_ID :
+ String.format(Locale.US, "ROUTE_%08x", getRouteName(routeObj).hashCode());
+ if (findSystemRouteRecordByDescriptorId(id) < 0) {
+ return id;
+ }
+ for (int i = 2; ; i++) {
+ String newId = String.format(Locale.US, "%s_%d", id, i);
+ if (findSystemRouteRecordByDescriptorId(newId) < 0) {
+ return newId;
+ }
+ }
+ }
+
+ @Override
+ public void onRouteRemoved(Object routeObj) {
+ if (getUserRouteRecord(routeObj) == null) {
+ int index = findSystemRouteRecord(routeObj);
+ if (index >= 0) {
+ mSystemRouteRecords.remove(index);
+ publishRoutes();
+ }
+ }
+ }
+
+ @Override
+ public void onRouteChanged(Object routeObj) {
+ if (getUserRouteRecord(routeObj) == null) {
+ int index = findSystemRouteRecord(routeObj);
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ updateSystemRouteDescriptor(record);
+ publishRoutes();
+ }
+ }
+ }
+
+ @Override
+ public void onRouteVolumeChanged(Object routeObj) {
+ if (getUserRouteRecord(routeObj) == null) {
+ int index = findSystemRouteRecord(routeObj);
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ int newVolume = MediaRouterJellybean.RouteInfo.getVolume(routeObj);
+ if (newVolume != record.mRouteDescriptor.getVolume()) {
+ record.mRouteDescriptor =
+ new MediaRouteDescriptor.Builder(record.mRouteDescriptor)
+ .setVolume(newVolume)
+ .build();
+ publishRoutes();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onRouteSelected(int type, Object routeObj) {
+ if (routeObj != MediaRouterJellybean.getSelectedRoute(mRouterObj,
+ MediaRouterJellybean.ALL_ROUTE_TYPES)) {
+ // The currently selected route has already changed so this callback
+ // is stale. Drop it to prevent getting into sync loops.
+ return;
+ }
+
+ UserRouteRecord userRouteRecord = getUserRouteRecord(routeObj);
+ if (userRouteRecord != null) {
+ userRouteRecord.mRoute.select();
+ } else {
+ // Select the route if it already exists in the compat media router.
+ // If not, we will select it instead when the route is added.
+ int index = findSystemRouteRecord(routeObj);
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ mSyncCallback.onSystemRouteSelectedByDescriptorId(record.mRouteDescriptorId);
+ }
+ }
+ }
+
+ @Override
+ public void onRouteUnselected(int type, Object routeObj) {
+ // Nothing to do when a route is unselected.
+ // We only need to handle when a route is selected.
+ }
+
+ @Override
+ public void onRouteGrouped(Object routeObj, Object groupObj, int index) {
+ // Route grouping is deprecated and no longer supported.
+ }
+
+ @Override
+ public void onRouteUngrouped(Object routeObj, Object groupObj) {
+ // Route grouping is deprecated and no longer supported.
+ }
+
+ @Override
+ public void onVolumeSetRequest(Object routeObj, int volume) {
+ UserRouteRecord record = getUserRouteRecord(routeObj);
+ if (record != null) {
+ record.mRoute.requestSetVolume(volume);
+ }
+ }
+
+ @Override
+ public void onVolumeUpdateRequest(Object routeObj, int direction) {
+ UserRouteRecord record = getUserRouteRecord(routeObj);
+ if (record != null) {
+ record.mRoute.requestUpdateVolume(direction);
+ }
+ }
+
+ @Override
+ public void onSyncRouteAdded(MediaRouter.RouteInfo route) {
+ if (route.getProviderInstance() != this) {
+ Object routeObj = MediaRouterJellybean.createUserRoute(
+ mRouterObj, mUserRouteCategoryObj);
+ UserRouteRecord record = new UserRouteRecord(route, routeObj);
+ MediaRouterJellybean.RouteInfo.setTag(routeObj, record);
+ MediaRouterJellybean.UserRouteInfo.setVolumeCallback(routeObj, mVolumeCallbackObj);
+ updateUserRouteProperties(record);
+ mUserRouteRecords.add(record);
+ MediaRouterJellybean.addUserRoute(mRouterObj, routeObj);
+ } else {
+ // If the newly added route is the counterpart of the currently selected
+ // route in the framework media router then ensure it is selected in
+ // the compat media router.
+ Object routeObj = MediaRouterJellybean.getSelectedRoute(
+ mRouterObj, MediaRouterJellybean.ALL_ROUTE_TYPES);
+ int index = findSystemRouteRecord(routeObj);
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ if (record.mRouteDescriptorId.equals(route.getDescriptorId())) {
+ route.select();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onSyncRouteRemoved(MediaRouter.RouteInfo route) {
+ if (route.getProviderInstance() != this) {
+ int index = findUserRouteRecord(route);
+ if (index >= 0) {
+ UserRouteRecord record = mUserRouteRecords.remove(index);
+ MediaRouterJellybean.RouteInfo.setTag(record.mRouteObj, null);
+ MediaRouterJellybean.UserRouteInfo.setVolumeCallback(record.mRouteObj, null);
+ MediaRouterJellybean.removeUserRoute(mRouterObj, record.mRouteObj);
+ }
+ }
+ }
+
+ @Override
+ public void onSyncRouteChanged(MediaRouter.RouteInfo route) {
+ if (route.getProviderInstance() != this) {
+ int index = findUserRouteRecord(route);
+ if (index >= 0) {
+ UserRouteRecord record = mUserRouteRecords.get(index);
+ updateUserRouteProperties(record);
+ }
+ }
+ }
+
+ @Override
+ public void onSyncRouteSelected(MediaRouter.RouteInfo route) {
+ if (!route.isSelected()) {
+ // The currently selected route has already changed so this callback
+ // is stale. Drop it to prevent getting into sync loops.
+ return;
+ }
+
+ if (route.getProviderInstance() != this) {
+ int index = findUserRouteRecord(route);
+ if (index >= 0) {
+ UserRouteRecord record = mUserRouteRecords.get(index);
+ selectRoute(record.mRouteObj);
+ }
+ } else {
+ int index = findSystemRouteRecordByDescriptorId(route.getDescriptorId());
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ selectRoute(record.mRouteObj);
+ }
+ }
+ }
+
+ protected void publishRoutes() {
+ MediaRouteProviderDescriptor.Builder builder =
+ new MediaRouteProviderDescriptor.Builder();
+ int count = mSystemRouteRecords.size();
+ for (int i = 0; i < count; i++) {
+ builder.addRoute(mSystemRouteRecords.get(i).mRouteDescriptor);
+ }
+
+ setDescriptor(builder.build());
+ }
+
+ protected int findSystemRouteRecord(Object routeObj) {
+ final int count = mSystemRouteRecords.size();
+ for (int i = 0; i < count; i++) {
+ if (mSystemRouteRecords.get(i).mRouteObj == routeObj) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ protected int findSystemRouteRecordByDescriptorId(String id) {
+ final int count = mSystemRouteRecords.size();
+ for (int i = 0; i < count; i++) {
+ if (mSystemRouteRecords.get(i).mRouteDescriptorId.equals(id)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ protected int findUserRouteRecord(MediaRouter.RouteInfo route) {
+ final int count = mUserRouteRecords.size();
+ for (int i = 0; i < count; i++) {
+ if (mUserRouteRecords.get(i).mRoute == route) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ protected UserRouteRecord getUserRouteRecord(Object routeObj) {
+ Object tag = MediaRouterJellybean.RouteInfo.getTag(routeObj);
+ return tag instanceof UserRouteRecord ? (UserRouteRecord)tag : null;
+ }
+
+ protected void updateSystemRouteDescriptor(SystemRouteRecord record) {
+ // We must always recreate the route descriptor when making any changes
+ // because they are intended to be immutable once published.
+ MediaRouteDescriptor.Builder builder = new MediaRouteDescriptor.Builder(
+ record.mRouteDescriptorId, getRouteName(record.mRouteObj));
+ onBuildSystemRouteDescriptor(record, builder);
+ record.mRouteDescriptor = builder.build();
+ }
+
+ protected String getRouteName(Object routeObj) {
+ // Routes should not have null names but it may happen for badly configured
+ // user routes. We tolerate this by using an empty name string here but
+ // such unnamed routes will be discarded by the media router upstream
+ // (with a log message so we can track down the problem).
+ CharSequence name = MediaRouterJellybean.RouteInfo.getName(routeObj, getContext());
+ return name != null ? name.toString() : "";
+ }
+
+ protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+ MediaRouteDescriptor.Builder builder) {
+ int supportedTypes = MediaRouterJellybean.RouteInfo.getSupportedTypes(
+ record.mRouteObj);
+ if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_AUDIO) != 0) {
+ builder.addControlFilters(LIVE_AUDIO_CONTROL_FILTERS);
+ }
+ if ((supportedTypes & MediaRouterJellybean.ROUTE_TYPE_LIVE_VIDEO) != 0) {
+ builder.addControlFilters(LIVE_VIDEO_CONTROL_FILTERS);
+ }
+
+ builder.setPlaybackType(
+ MediaRouterJellybean.RouteInfo.getPlaybackType(record.mRouteObj));
+ builder.setPlaybackStream(
+ MediaRouterJellybean.RouteInfo.getPlaybackStream(record.mRouteObj));
+ builder.setVolume(
+ MediaRouterJellybean.RouteInfo.getVolume(record.mRouteObj));
+ builder.setVolumeMax(
+ MediaRouterJellybean.RouteInfo.getVolumeMax(record.mRouteObj));
+ builder.setVolumeHandling(
+ MediaRouterJellybean.RouteInfo.getVolumeHandling(record.mRouteObj));
+ }
+
+ protected void updateUserRouteProperties(UserRouteRecord record) {
+ MediaRouterJellybean.UserRouteInfo.setName(
+ record.mRouteObj, record.mRoute.getName());
+ MediaRouterJellybean.UserRouteInfo.setPlaybackType(
+ record.mRouteObj, record.mRoute.getPlaybackType());
+ MediaRouterJellybean.UserRouteInfo.setPlaybackStream(
+ record.mRouteObj, record.mRoute.getPlaybackStream());
+ MediaRouterJellybean.UserRouteInfo.setVolume(
+ record.mRouteObj, record.mRoute.getVolume());
+ MediaRouterJellybean.UserRouteInfo.setVolumeMax(
+ record.mRouteObj, record.mRoute.getVolumeMax());
+ MediaRouterJellybean.UserRouteInfo.setVolumeHandling(
+ record.mRouteObj, record.mRoute.getVolumeHandling());
+ }
+
+ protected void updateCallback() {
+ if (mCallbackRegistered) {
+ mCallbackRegistered = false;
+ MediaRouterJellybean.removeCallback(mRouterObj, mCallbackObj);
+ }
+
+ if (mRouteTypes != 0) {
+ mCallbackRegistered = true;
+ MediaRouterJellybean.addCallback(mRouterObj, mRouteTypes, mCallbackObj);
+ }
+ }
+
+ protected Object createCallbackObj() {
+ return MediaRouterJellybean.createCallback(this);
+ }
+
+ protected Object createVolumeCallbackObj() {
+ return MediaRouterJellybean.createVolumeCallback(this);
+ }
+
+ protected void selectRoute(Object routeObj) {
+ if (mSelectRouteWorkaround == null) {
+ mSelectRouteWorkaround = new MediaRouterJellybean.SelectRouteWorkaround();
+ }
+ mSelectRouteWorkaround.selectRoute(mRouterObj,
+ MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj);
+ }
+
+ @Override
+ protected Object getDefaultRoute() {
+ if (mGetDefaultRouteWorkaround == null) {
+ mGetDefaultRouteWorkaround = new MediaRouterJellybean.GetDefaultRouteWorkaround();
+ }
+ return mGetDefaultRouteWorkaround.getDefaultRoute(mRouterObj);
+ }
+
+ @Override
+ protected Object getSystemRoute(MediaRouter.RouteInfo route) {
+ if (route == null) {
+ return null;
+ }
+ int index = findSystemRouteRecordByDescriptorId(route.getDescriptorId());
+ if (index >= 0) {
+ return mSystemRouteRecords.get(index).mRouteObj;
+ }
+ return null;
+ }
+
+ /**
+ * Represents a route that is provided by the framework media router
+ * and published by this route provider to the support library media router.
+ */
+ protected static final class SystemRouteRecord {
+ public final Object mRouteObj;
+ public final String mRouteDescriptorId;
+ public MediaRouteDescriptor mRouteDescriptor; // assigned immediately after creation
+
+ public SystemRouteRecord(Object routeObj, String id) {
+ mRouteObj = routeObj;
+ mRouteDescriptorId = id;
+ }
+ }
+
+ /**
+ * Represents a route that is provided by the support library media router
+ * and published by this route provider to the framework media router.
+ */
+ protected static final class UserRouteRecord {
+ public final MediaRouter.RouteInfo mRoute;
+ public final Object mRouteObj;
+
+ public UserRouteRecord(MediaRouter.RouteInfo route, Object routeObj) {
+ mRoute = route;
+ mRouteObj = routeObj;
+ }
+ }
+
+ protected static final class SystemRouteController extends RouteController {
+ private final Object mRouteObj;
+
+ public SystemRouteController(Object routeObj) {
+ mRouteObj = routeObj;
+ }
+
+ @Override
+ public void onSetVolume(int volume) {
+ MediaRouterJellybean.RouteInfo.requestSetVolume(mRouteObj, volume);
+ }
+
+ @Override
+ public void onUpdateVolume(int delta) {
+ MediaRouterJellybean.RouteInfo.requestUpdateVolume(mRouteObj, delta);
+ }
+ }
+ }
+
+ /**
+ * Jellybean MR1 implementation.
+ */
+ // @@RequiresApi(17)
+ private static class JellybeanMr1Impl extends JellybeanImpl
+ implements MediaRouterJellybeanMr1.Callback {
+ private MediaRouterJellybeanMr1.ActiveScanWorkaround mActiveScanWorkaround;
+ private MediaRouterJellybeanMr1.IsConnectingWorkaround mIsConnectingWorkaround;
+
+ public JellybeanMr1Impl(Context context, SyncCallback syncCallback) {
+ super(context, syncCallback);
+ }
+
+ @Override
+ public void onRoutePresentationDisplayChanged(Object routeObj) {
+ int index = findSystemRouteRecord(routeObj);
+ if (index >= 0) {
+ SystemRouteRecord record = mSystemRouteRecords.get(index);
+ Display newPresentationDisplay =
+ MediaRouterJellybeanMr1.RouteInfo.getPresentationDisplay(routeObj);
+ int newPresentationDisplayId = (newPresentationDisplay != null
+ ? newPresentationDisplay.getDisplayId() : -1);
+ if (newPresentationDisplayId
+ != record.mRouteDescriptor.getPresentationDisplayId()) {
+ record.mRouteDescriptor =
+ new MediaRouteDescriptor.Builder(record.mRouteDescriptor)
+ .setPresentationDisplayId(newPresentationDisplayId)
+ .build();
+ publishRoutes();
+ }
+ }
+ }
+
+ @Override
+ protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+ MediaRouteDescriptor.Builder builder) {
+ super.onBuildSystemRouteDescriptor(record, builder);
+
+ if (!MediaRouterJellybeanMr1.RouteInfo.isEnabled(record.mRouteObj)) {
+ builder.setEnabled(false);
+ }
+
+ if (isConnecting(record)) {
+ builder.setConnecting(true);
+ }
+
+ Display presentationDisplay =
+ MediaRouterJellybeanMr1.RouteInfo.getPresentationDisplay(record.mRouteObj);
+ if (presentationDisplay != null) {
+ builder.setPresentationDisplayId(presentationDisplay.getDisplayId());
+ }
+ }
+
+ @Override
+ protected void updateCallback() {
+ super.updateCallback();
+
+ if (mActiveScanWorkaround == null) {
+ mActiveScanWorkaround = new MediaRouterJellybeanMr1.ActiveScanWorkaround(
+ getContext(), getHandler());
+ }
+ mActiveScanWorkaround.setActiveScanRouteTypes(mActiveScan ? mRouteTypes : 0);
+ }
+
+ @Override
+ protected Object createCallbackObj() {
+ return MediaRouterJellybeanMr1.createCallback(this);
+ }
+
+ protected boolean isConnecting(SystemRouteRecord record) {
+ if (mIsConnectingWorkaround == null) {
+ mIsConnectingWorkaround = new MediaRouterJellybeanMr1.IsConnectingWorkaround();
+ }
+ return mIsConnectingWorkaround.isConnecting(record.mRouteObj);
+ }
+ }
+
+ /**
+ * Jellybean MR2 implementation.
+ */
+ // @@RequiresApi(18)
+ private static class JellybeanMr2Impl extends JellybeanMr1Impl {
+ public JellybeanMr2Impl(Context context, SyncCallback syncCallback) {
+ super(context, syncCallback);
+ }
+
+ @Override
+ protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+ MediaRouteDescriptor.Builder builder) {
+ super.onBuildSystemRouteDescriptor(record, builder);
+
+ CharSequence description =
+ MediaRouterJellybeanMr2.RouteInfo.getDescription(record.mRouteObj);
+ if (description != null) {
+ builder.setDescription(description.toString());
+ }
+ }
+
+ @Override
+ protected void selectRoute(Object routeObj) {
+ MediaRouterJellybean.selectRoute(mRouterObj,
+ MediaRouterJellybean.ALL_ROUTE_TYPES, routeObj);
+ }
+
+ @Override
+ protected Object getDefaultRoute() {
+ return MediaRouterJellybeanMr2.getDefaultRoute(mRouterObj);
+ }
+
+ @Override
+ protected void updateUserRouteProperties(UserRouteRecord record) {
+ super.updateUserRouteProperties(record);
+
+ MediaRouterJellybeanMr2.UserRouteInfo.setDescription(
+ record.mRouteObj, record.mRoute.getDescription());
+ }
+
+ @Override
+ protected void updateCallback() {
+ if (mCallbackRegistered) {
+ MediaRouterJellybean.removeCallback(mRouterObj, mCallbackObj);
+ }
+
+ mCallbackRegistered = true;
+ MediaRouterJellybeanMr2.addCallback(mRouterObj, mRouteTypes, mCallbackObj,
+ MediaRouter.CALLBACK_FLAG_UNFILTERED_EVENTS
+ | (mActiveScan ? MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN : 0));
+ }
+
+ @Override
+ protected boolean isConnecting(SystemRouteRecord record) {
+ return MediaRouterJellybeanMr2.RouteInfo.isConnecting(record.mRouteObj);
+ }
+ }
+
+ /**
+ * Api24 implementation.
+ */
+ // @@RequiresApi(24)
+ private static class Api24Impl extends JellybeanMr2Impl {
+ public Api24Impl(Context context, SyncCallback syncCallback) {
+ super(context, syncCallback);
+ }
+
+ @Override
+ protected void onBuildSystemRouteDescriptor(SystemRouteRecord record,
+ MediaRouteDescriptor.Builder builder) {
+ super.onBuildSystemRouteDescriptor(record, builder);
+
+ builder.setDeviceType(MediaRouterApi24.RouteInfo.getDeviceType(record.mRouteObj));
+ }
+ }
+}
diff --git a/com/android/systemui/BatteryMeterView.java b/com/android/systemui/BatteryMeterView.java
index 2fe66a14..8666b0c8 100644
--- a/com/android/systemui/BatteryMeterView.java
+++ b/com/android/systemui/BatteryMeterView.java
@@ -224,7 +224,6 @@ public class BatteryMeterView extends LinearLayout implements
if (mTextColor != 0) mBatteryPercentView.setTextColor(mTextColor);
updatePercentText();
addView(mBatteryPercentView,
- 0,
new ViewGroup.LayoutParams(
LayoutParams.WRAP_CONTENT,
LayoutParams.MATCH_PARENT));
diff --git a/com/android/systemui/ChargingView.java b/com/android/systemui/ChargingView.java
deleted file mode 100644
index 33f8b069..00000000
--- a/com/android/systemui/ChargingView.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.systemui;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.content.res.TypedArray;
-import android.os.UserHandle;
-import android.util.AttributeSet;
-import android.widget.ImageView;
-
-import com.android.internal.hardware.AmbientDisplayConfiguration;
-import com.android.systemui.statusbar.policy.BatteryController;
-import com.android.systemui.statusbar.policy.ConfigurationController;
-
-/**
- * A view that only shows its drawable while the phone is charging.
- *
- * Also reloads its drawable upon density changes.
- */
-public class ChargingView extends ImageView implements
- BatteryController.BatteryStateChangeCallback,
- ConfigurationController.ConfigurationListener {
-
- private static final long CHARGING_INDICATION_DELAY_MS = 1000;
-
- private final AmbientDisplayConfiguration mConfig;
- private final Runnable mClearSuppressCharging = this::clearSuppressCharging;
- private BatteryController mBatteryController;
- private int mImageResource;
- private boolean mCharging;
- private boolean mDark;
- private boolean mSuppressCharging;
-
-
- private void clearSuppressCharging() {
- mSuppressCharging = false;
- removeCallbacks(mClearSuppressCharging);
- updateVisibility();
- }
-
- public ChargingView(Context context, @Nullable AttributeSet attrs) {
- super(context, attrs);
-
- mConfig = new AmbientDisplayConfiguration(context);
-
- TypedArray a = context.obtainStyledAttributes(attrs, new int[]{android.R.attr.src});
- int srcResId = a.getResourceId(0, 0);
-
- if (srcResId != 0) {
- mImageResource = srcResId;
- }
-
- a.recycle();
-
- updateVisibility();
- }
-
- @Override
- public void onAttachedToWindow() {
- super.onAttachedToWindow();
- mBatteryController = Dependency.get(BatteryController.class);
- mBatteryController.addCallback(this);
- Dependency.get(ConfigurationController.class).addCallback(this);
- }
-
- @Override
- public void onDetachedFromWindow() {
- super.onDetachedFromWindow();
- mBatteryController.removeCallback(this);
- Dependency.get(ConfigurationController.class).removeCallback(this);
- removeCallbacks(mClearSuppressCharging);
- }
-
- @Override
- public void onBatteryLevelChanged(int level, boolean pluggedIn, boolean charging) {
- boolean startCharging = charging && !mCharging;
- if (startCharging && deviceWillWakeUpWhenPluggedIn() && mDark) {
- // We're about to wake up, and thus don't want to show the indicator just for it to be
- // hidden again.
- clearSuppressCharging();
- mSuppressCharging = true;
- postDelayed(mClearSuppressCharging, CHARGING_INDICATION_DELAY_MS);
- }
- mCharging = charging;
- updateVisibility();
- }
-
- private boolean deviceWillWakeUpWhenPluggedIn() {
- boolean plugTurnsOnScreen = getResources().getBoolean(
- com.android.internal.R.bool.config_unplugTurnsOnScreen);
- boolean aod = mConfig.alwaysOnEnabled(UserHandle.USER_CURRENT);
- return !aod && plugTurnsOnScreen;
- }
-
- @Override
- public void onDensityOrFontScaleChanged() {
- setImageResource(mImageResource);
- }
-
- public void setDark(boolean dark) {
- mDark = dark;
- if (!dark) {
- clearSuppressCharging();
- }
- updateVisibility();
- }
-
- private void updateVisibility() {
- setVisibility(mCharging && !mSuppressCharging && mDark ? VISIBLE : INVISIBLE);
- }
-}
diff --git a/com/android/systemui/Dependency.java b/com/android/systemui/Dependency.java
index e7e70afa..7403ddc4 100644
--- a/com/android/systemui/Dependency.java
+++ b/com/android/systemui/Dependency.java
@@ -40,6 +40,8 @@ import com.android.systemui.plugins.PluginDependencyProvider;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.plugins.PluginManagerImpl;
import com.android.systemui.plugins.VolumeDialogController;
+import com.android.systemui.power.EnhancedEstimates;
+import com.android.systemui.power.EnhancedEstimatesImpl;
import com.android.systemui.power.PowerNotificationWarnings;
import com.android.systemui.power.PowerUI;
import com.android.systemui.statusbar.phone.ConfigurationControllerImpl;
@@ -310,6 +312,8 @@ public class Dependency extends SystemUI {
mProviders.put(OverviewProxyService.class, () -> new OverviewProxyService(mContext));
+ mProviders.put(EnhancedEstimates.class, () -> new EnhancedEstimatesImpl());
+
// Put all dependencies above here so the factory can override them if it wants.
SystemUIFactory.getInstance().injectDependencies(mProviders, mContext);
}
diff --git a/com/android/systemui/EmulatedDisplayCutout.java b/com/android/systemui/EmulatedDisplayCutout.java
index 6aa465ce..5d2e4d09 100644
--- a/com/android/systemui/EmulatedDisplayCutout.java
+++ b/com/android/systemui/EmulatedDisplayCutout.java
@@ -16,19 +16,14 @@
package com.android.systemui;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+
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;
@@ -37,25 +32,35 @@ import android.view.ViewGroup.LayoutParams;
import android.view.WindowInsets;
import android.view.WindowManager;
-import java.util.Collections;
-import java.util.List;
+import com.android.systemui.statusbar.policy.ConfigurationController;
+import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
/**
* Emulates a display cutout by drawing its shape in an overlay as supplied by
* {@link DisplayCutout}.
*/
-public class EmulatedDisplayCutout extends SystemUI {
+public class EmulatedDisplayCutout extends SystemUI implements ConfigurationListener {
private View mOverlay;
private boolean mAttached;
private WindowManager mWindowManager;
@Override
public void start() {
+ Dependency.get(ConfigurationController.class).addCallback(this);
+
mWindowManager = mContext.getSystemService(WindowManager.class);
- mContext.getContentResolver().registerContentObserver(
- Settings.Global.getUriFor(Settings.Global.EMULATE_DISPLAY_CUTOUT),
- false, mObserver, UserHandle.USER_ALL);
- mObserver.onChange(false);
+ updateAttached();
+ }
+
+ @Override
+ public void onOverlayChanged() {
+ updateAttached();
+ }
+
+ private void updateAttached() {
+ boolean shouldAttach = mContext.getResources().getBoolean(
+ com.android.internal.R.bool.config_fillMainBuiltInDisplayCutout);
+ setAttached(shouldAttach);
}
private void setAttached(boolean attached) {
@@ -87,23 +92,12 @@ public class EmulatedDisplayCutout extends SystemUI {
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.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
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();
@@ -114,10 +108,9 @@ public class EmulatedDisplayCutout extends SystemUI {
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
+ mBounds.reset();
if (insets.getDisplayCutout() != null) {
insets.getDisplayCutout().getBounds().getBoundaryPath(mBounds);
- } else {
- mBounds.reset();
}
invalidate();
return insets.consumeDisplayCutout();
@@ -126,7 +119,7 @@ public class EmulatedDisplayCutout extends SystemUI {
@Override
protected void onDraw(Canvas canvas) {
if (!mBounds.isEmpty()) {
- mPaint.setColor(Color.DKGRAY);
+ mPaint.setColor(Color.BLACK);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawPath(mBounds, mPaint);
diff --git a/com/android/systemui/HardwareUiLayout.java b/com/android/systemui/HardwareUiLayout.java
index ca34345d..94817880 100644
--- a/com/android/systemui/HardwareUiLayout.java
+++ b/com/android/systemui/HardwareUiLayout.java
@@ -58,6 +58,7 @@ public class HardwareUiLayout extends FrameLayout implements Tunable {
private boolean mRoundedDivider;
private int mRotation = ROTATION_NONE;
private boolean mRotatedBackground;
+ private boolean mSwapOrientation = true;
public HardwareUiLayout(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -145,6 +146,10 @@ public class HardwareUiLayout extends FrameLayout implements Tunable {
updateRotation();
}
+ public void setSwapOrientation(boolean swapOrientation) {
+ mSwapOrientation = swapOrientation;
+ }
+
private void updateRotation() {
int rotation = RotationUtils.getRotation(getContext());
if (rotation != mRotation) {
@@ -173,7 +178,9 @@ public class HardwareUiLayout extends FrameLayout implements Tunable {
if (to == ROTATION_SEASCAPE) {
swapOrder(linearLayout);
}
- linearLayout.setOrientation(LinearLayout.HORIZONTAL);
+ if (mSwapOrientation) {
+ linearLayout.setOrientation(LinearLayout.HORIZONTAL);
+ }
swapDimens(this.mChild);
}
} else {
@@ -184,7 +191,9 @@ public class HardwareUiLayout extends FrameLayout implements Tunable {
if (from == ROTATION_SEASCAPE) {
swapOrder(linearLayout);
}
- linearLayout.setOrientation(LinearLayout.VERTICAL);
+ if (mSwapOrientation) {
+ linearLayout.setOrientation(LinearLayout.VERTICAL);
+ }
swapDimens(mChild);
}
}
diff --git a/com/android/systemui/ImageWallpaper.java b/com/android/systemui/ImageWallpaper.java
index 593bb508..a59c97e0 100644
--- a/com/android/systemui/ImageWallpaper.java
+++ b/com/android/systemui/ImageWallpaper.java
@@ -494,7 +494,8 @@ public class ImageWallpaper extends WallpaperService {
}
if (mBackground != null) {
RectF dest = new RectF(left, top, right, bottom);
- // add a filter bitmap?
+ Log.i(TAG, "Redrawing in rect: " + dest + " with surface size: "
+ + mLastRequestedWidth + "x" + mLastRequestedHeight);
c.drawBitmap(mBackground, null, dest, null);
}
} finally {
diff --git a/com/android/systemui/OverviewProxyService.java b/com/android/systemui/OverviewProxyService.java
index 22922e7b..b6e49ae6 100644
--- a/com/android/systemui/OverviewProxyService.java
+++ b/com/android/systemui/OverviewProxyService.java
@@ -51,7 +51,8 @@ import java.util.List;
*/
public class OverviewProxyService implements CallbackController<OverviewProxyListener>, Dumpable {
- private static final String TAG = "OverviewProxyService";
+ public static final String TAG_OPS = "OverviewProxyService";
+ public static final boolean DEBUG_OVERVIEW_PROXY = false;
private static final long BACKOFF_MILLIS = 5000;
private final Context mContext;
@@ -76,6 +77,15 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
Binder.restoreCallingIdentity(token);
}
}
+
+ public void onRecentsAnimationStarted() {
+ long token = Binder.clearCallingIdentity();
+ try {
+ notifyRecentsAnimationStarted();
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ }
};
private final BroadcastReceiver mLauncherAddedReceiver = new BroadcastReceiver() {
@@ -96,12 +106,12 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
try {
service.linkToDeath(mOverviewServiceDeathRcpt, 0);
} catch (RemoteException e) {
- Log.e(TAG, "Lost connection to launcher service", e);
+ Log.e(TAG_OPS, "Lost connection to launcher service", e);
}
try {
mOverviewProxy.onBind(mSysUiProxy);
} catch (RemoteException e) {
- Log.e(TAG, "Failed to call onBind()", e);
+ Log.e(TAG_OPS, "Failed to call onBind()", e);
}
notifyConnectionChanged();
}
@@ -146,6 +156,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
filter.addDataScheme("package");
filter.addDataSchemeSpecificPart(mLauncherComponentName.getPackageName(),
PatternMatcher.PATTERN_LITERAL);
+ filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
mContext.registerReceiver(mLauncherAddedReceiver, filter);
}
@@ -193,6 +204,10 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
return mOverviewProxy;
}
+ public ComponentName getLauncherComponent() {
+ return mLauncherComponentName;
+ }
+
private void disconnectFromLauncherService() {
if (mOverviewProxy != null) {
mOverviewProxy.asBinder().unlinkToDeath(mOverviewServiceDeathRcpt, 0);
@@ -208,9 +223,15 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
}
}
+ private void notifyRecentsAnimationStarted() {
+ for (int i = mConnectionCallbacks.size() - 1; i >= 0; --i) {
+ mConnectionCallbacks.get(i).onRecentsAnimationStarted();
+ }
+ }
+
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- pw.println(TAG + " state:");
+ pw.println(TAG_OPS + " state:");
pw.print(" mConnectionBackoffAttempts="); pw.println(mConnectionBackoffAttempts);
pw.print(" isCurrentUserSetup="); pw.println(mDeviceProvisionedController
.isCurrentUserSetup());
@@ -218,6 +239,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis
}
public interface OverviewProxyListener {
- void onConnectionChanged(boolean isConnected);
+ default void onConnectionChanged(boolean isConnected) {}
+ default void onRecentsAnimationStarted() {}
}
}
diff --git a/com/android/systemui/Prefs.java b/com/android/systemui/Prefs.java
index 4437d314..9319bc60 100644
--- a/com/android/systemui/Prefs.java
+++ b/com/android/systemui/Prefs.java
@@ -48,6 +48,8 @@ public final class Prefs {
Key.QS_WORK_ADDED,
Key.QS_NIGHTDISPLAY_ADDED,
Key.SEEN_MULTI_USER,
+ Key.NUM_APPS_LAUNCHED,
+ Key.HAS_SWIPED_UP_FOR_RECENTS,
})
public @interface Key {
@Deprecated
@@ -75,6 +77,8 @@ public final class Prefs {
@Deprecated
String QS_NIGHTDISPLAY_ADDED = "QsNightDisplayAdded";
String SEEN_MULTI_USER = "HasSeenMultiUser";
+ String NUM_APPS_LAUNCHED = "NumAppsLaunched";
+ String HAS_SWIPED_UP_FOR_RECENTS = "HasSwipedUpForRecents";
}
public static boolean getBoolean(Context context, @Key String key, boolean defaultValue) {
diff --git a/com/android/systemui/RecentsComponent.java b/com/android/systemui/RecentsComponent.java
index 880ae709..f9dbf4a1 100644
--- a/com/android/systemui/RecentsComponent.java
+++ b/com/android/systemui/RecentsComponent.java
@@ -21,7 +21,7 @@ import android.view.Display;
import android.view.View;
public interface RecentsComponent {
- void showRecentApps(boolean triggeredFromAltTab, boolean fromHome);
+ void showRecentApps(boolean triggeredFromAltTab);
void showNextAffiliatedTask();
void showPrevAffiliatedTask();
diff --git a/com/android/systemui/RoundedCorners.java b/com/android/systemui/RoundedCorners.java
index b15b79fb..c960fa12 100644
--- a/com/android/systemui/RoundedCorners.java
+++ b/com/android/systemui/RoundedCorners.java
@@ -14,6 +14,8 @@
package com.android.systemui;
+import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
+
import static com.android.systemui.tuner.TunablePadding.FLAG_START;
import static com.android.systemui.tuner.TunablePadding.FLAG_END;
@@ -163,6 +165,7 @@ public class RoundedCorners extends SystemUI implements Tunable {
| WindowManager.LayoutParams.PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY;
lp.setTitle("RoundedOverlay");
lp.gravity = Gravity.TOP;
+ lp.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
return lp;
}
diff --git a/com/android/systemui/SlicePermissionActivity.java b/com/android/systemui/SlicePermissionActivity.java
new file mode 100644
index 00000000..302face1
--- /dev/null
+++ b/com/android/systemui/SlicePermissionActivity.java
@@ -0,0 +1,88 @@
+/*
+ * 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 com.android.systemui;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.slice.SliceManager;
+import android.app.slice.SliceProvider;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.DialogInterface.OnDismissListener;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.widget.CheckBox;
+import android.widget.TextView;
+
+public class SlicePermissionActivity extends Activity implements OnClickListener,
+ OnDismissListener {
+
+ private static final String TAG = "SlicePermissionActivity";
+
+ private CheckBox mAllCheckbox;
+
+ private Uri mUri;
+ private String mCallingPkg;
+ private String mProviderPkg;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ mUri = getIntent().getParcelableExtra(SliceProvider.EXTRA_BIND_URI);
+ mCallingPkg = getIntent().getStringExtra(SliceProvider.EXTRA_PKG);
+ mProviderPkg = getIntent().getStringExtra(SliceProvider.EXTRA_PROVIDER_PKG);
+
+ try {
+ PackageManager pm = getPackageManager();
+ CharSequence app1 = pm.getApplicationInfo(mCallingPkg, 0).loadLabel(pm);
+ CharSequence app2 = pm.getApplicationInfo(mProviderPkg, 0).loadLabel(pm);
+ AlertDialog dialog = new AlertDialog.Builder(this)
+ .setTitle(getString(R.string.slice_permission_title, app1, app2))
+ .setView(R.layout.slice_permission_request)
+ .setNegativeButton(R.string.slice_permission_deny, this)
+ .setPositiveButton(R.string.slice_permission_allow, this)
+ .setOnDismissListener(this)
+ .show();
+ TextView t1 = dialog.getWindow().getDecorView().findViewById(R.id.text1);
+ t1.setText(getString(R.string.slice_permission_text_1, app2));
+ TextView t2 = dialog.getWindow().getDecorView().findViewById(R.id.text2);
+ t2.setText(getString(R.string.slice_permission_text_2, app2));
+ mAllCheckbox = dialog.getWindow().getDecorView().findViewById(
+ R.id.slice_permission_checkbox);
+ mAllCheckbox.setText(getString(R.string.slice_permission_checkbox, app1));
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Couldn't find package", e);
+ finish();
+ }
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (which == DialogInterface.BUTTON_POSITIVE) {
+ getSystemService(SliceManager.class).grantPermissionFromUser(mUri, mCallingPkg,
+ mAllCheckbox.isChecked());
+ }
+ finish();
+ }
+
+ @Override
+ public void onDismiss(DialogInterface dialog) {
+ finish();
+ }
+}
diff --git a/com/android/systemui/SwipeHelper.java b/com/android/systemui/SwipeHelper.java
index 592dda07..a64ce296 100644
--- a/com/android/systemui/SwipeHelper.java
+++ b/com/android/systemui/SwipeHelper.java
@@ -21,6 +21,7 @@ import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.annotation.NonNull;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.RectF;
@@ -316,10 +317,12 @@ public class SwipeHelper implements Gefingerpoken {
float deltaPerpendicular = perpendicularPos - mPerpendicularInitialTouchPos;
if (Math.abs(delta) > mPagingTouchSlop
&& Math.abs(delta) > Math.abs(deltaPerpendicular)) {
- mCallback.onBeginDrag(mCurrView);
- mDragging = true;
- mInitialTouchPos = getPos(ev);
- mTranslation = getTranslation(mCurrView);
+ if (mCallback.canChildBeDragged(mCurrView)) {
+ mCallback.onBeginDrag(mCurrView);
+ mDragging = true;
+ mInitialTouchPos = getPos(ev);
+ mTranslation = getTranslation(mCurrView);
+ }
cancelLongPress();
}
}
@@ -722,5 +725,10 @@ public class SwipeHelper implements Gefingerpoken {
* @return The factor the falsing threshold should be multiplied with
*/
float getFalsingThresholdFactor();
+
+ /**
+ * @return If true, the given view is draggable.
+ */
+ default boolean canChildBeDragged(@NonNull View animView) { return true; }
}
}
diff --git a/com/android/systemui/analytics/DataCollector.java b/com/android/systemui/analytics/DataCollector.java
index 931a9941..69e347c9 100644
--- a/com/android/systemui/analytics/DataCollector.java
+++ b/com/android/systemui/analytics/DataCollector.java
@@ -463,4 +463,8 @@ public class DataCollector implements SensorEventListener {
public boolean isReportingEnabled() {
return mAllowReportRejectedTouch;
}
+
+ public void onFalsingSessionStarted() {
+ sessionEntrypoint();
+ }
}
diff --git a/com/android/systemui/charging/WirelessChargingAnimation.java b/com/android/systemui/charging/WirelessChargingAnimation.java
new file mode 100644
index 00000000..348855bb
--- /dev/null
+++ b/com/android/systemui/charging/WirelessChargingAnimation.java
@@ -0,0 +1,213 @@
+/*
+ * 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 com.android.systemui.charging;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+import android.util.Slog;
+import android.view.Gravity;
+import android.view.View;
+import android.view.WindowManager;
+
+/**
+ * A WirelessChargingAnimation is a view containing view + animation for wireless charging.
+ * @hide
+ */
+public class WirelessChargingAnimation {
+
+ public static final long DURATION = 1400;
+ private static final String TAG = "WirelessChargingView";
+ private static final boolean LOCAL_LOGV = false;
+
+ private final WirelessChargingView mCurrentWirelessChargingView;
+ private static WirelessChargingView mPreviousWirelessChargingView;
+
+ /**
+ * Constructs an empty WirelessChargingAnimation object. If looper is null,
+ * Looper.myLooper() is used. Must set
+ * {@link WirelessChargingAnimation#mCurrentWirelessChargingView}
+ * before calling {@link #show} - can be done through {@link #makeWirelessChargingAnimation}.
+ * @hide
+ */
+ public WirelessChargingAnimation(@NonNull Context context, @Nullable Looper looper, int
+ batteryLevel) {
+ mCurrentWirelessChargingView = new WirelessChargingView(context, looper,
+ batteryLevel);
+ }
+
+ /**
+ * Creates a wireless charging animation object populated with next view.
+ * @hide
+ */
+ public static WirelessChargingAnimation makeWirelessChargingAnimation(@NonNull Context context,
+ @Nullable Looper looper, int batteryLevel) {
+ return new WirelessChargingAnimation(context, looper, batteryLevel);
+ }
+
+ /**
+ * Show the view for the specified duration.
+ */
+ public void show() {
+ if (mCurrentWirelessChargingView == null ||
+ mCurrentWirelessChargingView.mNextView == null) {
+ throw new RuntimeException("setView must have been called");
+ }
+
+ if (mPreviousWirelessChargingView != null) {
+ mPreviousWirelessChargingView.hide(0);
+ }
+
+ mPreviousWirelessChargingView = mCurrentWirelessChargingView;
+ mCurrentWirelessChargingView.show();
+ mCurrentWirelessChargingView.hide(DURATION);
+ }
+
+ private static class WirelessChargingView {
+ private static final int SHOW = 0;
+ private static final int HIDE = 1;
+
+ private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
+ private final Handler mHandler;
+
+ private int mGravity;
+
+ private View mView;
+ private View mNextView;
+ private WindowManager mWM;
+
+ public WirelessChargingView(Context context, @Nullable Looper looper, int batteryLevel) {
+ mNextView = new WirelessChargingLayout(context, batteryLevel);
+ mGravity = Gravity.CENTER_HORIZONTAL | Gravity.CENTER;
+
+ final WindowManager.LayoutParams params = mParams;
+ params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+ params.width = WindowManager.LayoutParams.WRAP_CONTENT;
+ params.format = PixelFormat.TRANSLUCENT;
+
+ params.type = WindowManager.LayoutParams.TYPE_SECURE_SYSTEM_OVERLAY;
+ params.setTitle("Charging Animation");
+ params.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+ | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE
+ | WindowManager.LayoutParams.FLAG_DIM_BEHIND;
+
+ params.dimAmount = .3f;
+
+ if (looper == null) {
+ // Use Looper.myLooper() if looper is not specified.
+ looper = Looper.myLooper();
+ if (looper == null) {
+ throw new RuntimeException(
+ "Can't display wireless animation on a thread that has not called "
+ + "Looper.prepare()");
+ }
+ }
+
+ mHandler = new Handler(looper, null) {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case SHOW: {
+ handleShow();
+ break;
+ }
+ case HIDE: {
+ handleHide();
+ // Don't do this in handleHide() because it is also invoked by
+ // handleShow()
+ mNextView = null;
+ break;
+ }
+ }
+ }
+ };
+ }
+
+ public void show() {
+ if (LOCAL_LOGV) Log.v(TAG, "SHOW: " + this);
+ mHandler.obtainMessage(SHOW).sendToTarget();
+ }
+
+ public void hide(long duration) {
+ if (LOCAL_LOGV) Log.v(TAG, "HIDE: " + this);
+ mHandler.sendMessageDelayed(Message.obtain(mHandler, HIDE), duration);
+ }
+
+ private void handleShow() {
+ if (LOCAL_LOGV) {
+ Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView + " mNextView="
+ + mNextView);
+ }
+
+ if (mView != mNextView) {
+ // remove the old view if necessary
+ handleHide();
+ mView = mNextView;
+ Context context = mView.getContext().getApplicationContext();
+ String packageName = mView.getContext().getOpPackageName();
+ if (context == null) {
+ context = mView.getContext();
+ }
+ mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+ // We can resolve the Gravity here by using the Locale for getting
+ // the layout direction
+ final Configuration config = mView.getContext().getResources().getConfiguration();
+ final int gravity = Gravity.getAbsoluteGravity(mGravity,
+ config.getLayoutDirection());
+ mParams.gravity = gravity;
+ if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
+ mParams.horizontalWeight = 1.0f;
+ }
+ if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
+ mParams.verticalWeight = 1.0f;
+ }
+ mParams.packageName = packageName;
+ mParams.hideTimeoutMilliseconds = DURATION;
+
+ if (mView.getParent() != null) {
+ if (LOCAL_LOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
+ mWM.removeView(mView);
+ }
+ if (LOCAL_LOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
+
+ try {
+ mWM.addView(mView, mParams);
+ } catch (WindowManager.BadTokenException e) {
+ Slog.d(TAG, "Unable to add wireless charging view. " + e);
+ }
+ }
+ }
+
+ private void handleHide() {
+ if (LOCAL_LOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
+ if (mView != null) {
+ if (mView.getParent() != null) {
+ if (LOCAL_LOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
+ mWM.removeViewImmediate(mView);
+ }
+
+ mView = null;
+ }
+ }
+ }
+}
diff --git a/com/android/systemui/charging/WirelessChargingLayout.java b/com/android/systemui/charging/WirelessChargingLayout.java
new file mode 100644
index 00000000..c78ea565
--- /dev/null
+++ b/com/android/systemui/charging/WirelessChargingLayout.java
@@ -0,0 +1,81 @@
+/*
+ * 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 com.android.systemui.charging;
+
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.systemui.R;
+
+import java.text.NumberFormat;
+
+/**
+ * @hide
+ */
+public class WirelessChargingLayout extends FrameLayout {
+ private final static int UNKNOWN_BATTERY_LEVEL = -1;
+
+ public WirelessChargingLayout(Context context) {
+ super(context);
+ init(context, null);
+ }
+
+ public WirelessChargingLayout(Context context, int batterylLevel) {
+ super(context);
+ init(context, null, batterylLevel);
+ }
+
+ public WirelessChargingLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context, attrs);
+ }
+
+ private void init(Context c, AttributeSet attrs) {
+ init(c, attrs, -1);
+ }
+
+ private void init(Context c, AttributeSet attrs, int batteryLevel) {
+ final int mBatteryLevel = batteryLevel;
+
+ inflate(c, R.layout.wireless_charging_layout, this);
+
+ // where the circle animation occurs:
+ final WirelessChargingView mChargingView = findViewById(R.id.wireless_charging_view);
+
+ // amount of battery:
+ final TextView mPercentage = findViewById(R.id.wireless_charging_percentage);
+
+ // (optional) time until full charge if available
+ final TextView mSecondaryText = findViewById(R.id.wireless_charging_secondary_text);
+
+ if (batteryLevel != UNKNOWN_BATTERY_LEVEL) {
+ mPercentage.setText(NumberFormat.getPercentInstance().format(mBatteryLevel / 100f));
+
+ ValueAnimator animator = ObjectAnimator.ofFloat(mPercentage, "textSize",
+ getContext().getResources().getFloat(R.dimen.config_batteryLevelTextSizeStart),
+ getContext().getResources().getFloat(R.dimen.config_batteryLevelTextSizeEnd));
+
+ animator.setDuration((long) getContext().getResources().getInteger(
+ R.integer.config_batteryLevelTextAnimationDuration));
+ animator.start();
+ }
+ }
+}
diff --git a/com/android/systemui/charging/WirelessChargingView.java b/com/android/systemui/charging/WirelessChargingView.java
new file mode 100644
index 00000000..f5edf521
--- /dev/null
+++ b/com/android/systemui/charging/WirelessChargingView.java
@@ -0,0 +1,163 @@
+/*
+ * 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 com.android.systemui.charging;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+import com.android.settingslib.Utils;
+import com.android.systemui.R;
+
+final class WirelessChargingView extends View {
+
+ private Interpolator mInterpolator;
+ private float mPathGone;
+ private float mInterpolatedPathGone;
+ private long mAnimationStartTime;
+ private long mStartSpinCircleAnimationTime;
+ private long mAnimationOffset = 500;
+ private long mTotalAnimationDuration = WirelessChargingAnimation.DURATION - mAnimationOffset;
+ private long mExpandingCircle = (long) (mTotalAnimationDuration * .9);
+ private long mSpinCircleAnimationTime = mTotalAnimationDuration - mExpandingCircle;
+
+ private boolean mFinishedAnimatingSpinningCircles = false;
+
+ private int mStartAngle = -90;
+ private int mNumSmallCircles = 20;
+ private int mSmallCircleRadius = 10;
+
+ private int mMainCircleStartRadius = 100;
+ private int mMainCircleEndRadius = 230;
+ private int mMainCircleCurrentRadius = mMainCircleStartRadius;
+
+ private int mCenterX;
+ private int mCenterY;
+
+ private Paint mPaint;
+ private Context mContext;
+
+ public WirelessChargingView(Context context) {
+ super(context);
+ init(context, null);
+ }
+
+ public WirelessChargingView(Context context, AttributeSet attr) {
+ super(context, attr);
+ init(context, attr);
+ }
+
+ public WirelessChargingView(Context context, AttributeSet attr, int styleAttr) {
+ super(context, attr, styleAttr);
+ init(context, attr);
+ }
+
+ public void init(Context context, AttributeSet attr) {
+ mContext = context;
+ setupPaint();
+ mInterpolator = new DecelerateInterpolator();
+ }
+
+ private void setupPaint() {
+ mPaint = new Paint();
+ mPaint.setColor(Utils.getColorAttr(mContext, R.attr.wallpaperTextColor));
+ }
+
+ @Override
+ protected void onDraw(final Canvas canvas) {
+ super.onDraw(canvas);
+
+ if (mAnimationStartTime == 0) {
+ mAnimationStartTime = System.currentTimeMillis();
+ }
+
+ updateDrawingParameters();
+ drawCircles(canvas);
+
+ if (!mFinishedAnimatingSpinningCircles) {
+ invalidate();
+ }
+ }
+
+ /**
+ * Draws a larger circle of radius {@link WirelessChargingView#mMainCircleEndRadius} composed of
+ * {@link WirelessChargingView#mNumSmallCircles} smaller circles
+ * @param canvas
+ */
+ private void drawCircles(Canvas canvas) {
+ mCenterX = canvas.getWidth() / 2;
+ mCenterY = canvas.getHeight() / 2;
+
+ // angleOffset makes small circles look like they're moving around the main circle
+ float angleOffset = mPathGone * 10;
+
+ // draws mNumSmallCircles to compose a larger, main circle
+ for (int circle = 0; circle < mNumSmallCircles; circle++) {
+ double angle = ((mStartAngle + angleOffset) * Math.PI / 180) + (circle * ((2 * Math.PI)
+ / mNumSmallCircles));
+
+ int x = (int) (mCenterX + Math.cos(angle) * (mMainCircleCurrentRadius +
+ mSmallCircleRadius));
+ int y = (int) (mCenterY + Math.sin(angle) * (mMainCircleCurrentRadius +
+ mSmallCircleRadius));
+
+ canvas.drawCircle(x, y, mSmallCircleRadius, mPaint);
+ }
+
+ if (mMainCircleCurrentRadius >= mMainCircleEndRadius && !isSpinCircleAnimationStarted()) {
+ mStartSpinCircleAnimationTime = System.currentTimeMillis();
+ }
+
+ if (isSpinAnimationFinished()) {
+ mFinishedAnimatingSpinningCircles = true;
+ }
+ }
+
+ private boolean isSpinCircleAnimationStarted() {
+ return mStartSpinCircleAnimationTime != 0;
+ }
+
+ private boolean isSpinAnimationFinished() {
+ return isSpinCircleAnimationStarted() && System.currentTimeMillis() -
+ mStartSpinCircleAnimationTime > mSpinCircleAnimationTime;
+ }
+
+ private void updateDrawingParameters() {
+ mPathGone = getPathGone(System.currentTimeMillis());
+ mInterpolatedPathGone = mInterpolator.getInterpolation(mPathGone);
+
+ if (mPathGone < 1.0f) {
+ mMainCircleCurrentRadius = mMainCircleStartRadius + (int) (mInterpolatedPathGone *
+ (mMainCircleEndRadius - mMainCircleStartRadius));
+ } else {
+ mMainCircleCurrentRadius = mMainCircleEndRadius;
+ }
+ }
+
+ /**
+ * @return decimal depicting how far along the creation of the larger circle (of circles) is
+ * For values < 1.0, the larger circle is being drawn
+ * For values > 1.0 the larger circle has been drawn and further animation can occur
+ */
+ private float getPathGone(long now) {
+ return (float) (now - mAnimationStartTime) / (mExpandingCircle);
+ }
+} \ No newline at end of file
diff --git a/com/android/systemui/chooser/ChooserActivity.java b/com/android/systemui/chooser/ChooserActivity.java
new file mode 100644
index 00000000..085ece75
--- /dev/null
+++ b/com/android/systemui/chooser/ChooserActivity.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 com.android.systemui.chooser;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.systemui.R;
+
+import java.lang.Thread;
+import java.util.ArrayList;
+
+public final class ChooserActivity extends Activity {
+
+ private static final String TAG = "ChooserActivity";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ ChooserHelper.onChoose(this);
+ finish();
+ }
+}
diff --git a/com/android/systemui/chooser/ChooserHelper.java b/com/android/systemui/chooser/ChooserHelper.java
new file mode 100644
index 00000000..ac22568f
--- /dev/null
+++ b/com/android/systemui/chooser/ChooserHelper.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 com.android.systemui.chooser;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.util.Log;
+
+import com.android.systemui.R;
+
+public class ChooserHelper {
+
+ private static final String TAG = "ChooserHelper";
+
+ static void onChoose(Activity activity) {
+ final Intent thisIntent = activity.getIntent();
+ final Bundle thisExtras = thisIntent.getExtras();
+ final Intent chosenIntent = thisIntent.getParcelableExtra(Intent.EXTRA_INTENT);
+ final Bundle options = thisIntent.getParcelableExtra(ActivityManager.EXTRA_OPTIONS);
+ final IBinder permissionToken =
+ thisExtras.getBinder(ActivityManager.EXTRA_PERMISSION_TOKEN);
+ final boolean ignoreTargetSecurity =
+ thisIntent.getBooleanExtra(ActivityManager.EXTRA_IGNORE_TARGET_SECURITY, false);
+ final int userId = thisIntent.getIntExtra(Intent.EXTRA_USER_ID, -1);
+ activity.startActivityAsCaller(
+ chosenIntent, options, permissionToken, ignoreTargetSecurity, userId);
+ }
+}
diff --git a/com/android/systemui/classifier/FalsingManager.java b/com/android/systemui/classifier/FalsingManager.java
index e4b405f5..ed659e2d 100644
--- a/com/android/systemui/classifier/FalsingManager.java
+++ b/com/android/systemui/classifier/FalsingManager.java
@@ -167,6 +167,9 @@ public class FalsingManager implements SensorEventListener {
if (mDataCollector.isEnabledFull()) {
registerSensors(COLLECTOR_SENSORS);
}
+ if (mDataCollector.isEnabled()) {
+ mDataCollector.onFalsingSessionStarted();
+ }
}
private void registerSensors(int [] sensors) {
diff --git a/com/android/systemui/doze/DozeFactory.java b/com/android/systemui/doze/DozeFactory.java
index 0f0402d0..bfb3a6ea 100644
--- a/com/android/systemui/doze/DozeFactory.java
+++ b/com/android/systemui/doze/DozeFactory.java
@@ -66,7 +66,7 @@ public class DozeFactory {
createDozeUi(context, host, wakeLock, machine, handler, alarmManager, params),
new DozeScreenState(wrappedService, handler),
createDozeScreenBrightness(context, wrappedService, sensorManager, host, handler),
- new DozeWallpaperState()
+ new DozeWallpaperState(context)
});
return machine;
diff --git a/com/android/systemui/doze/DozeUi.java b/com/android/systemui/doze/DozeUi.java
index b352ec97..75f1b501 100644
--- a/com/android/systemui/doze/DozeUi.java
+++ b/com/android/systemui/doze/DozeUi.java
@@ -16,6 +16,8 @@
package com.android.systemui.doze;
+import static com.android.systemui.doze.DozeMachine.State.DOZE_AOD_PAUSED;
+
import android.app.AlarmManager;
import android.content.Context;
import android.os.Handler;
@@ -79,6 +81,11 @@ public class DozeUi implements DozeMachine.Part {
public void transitionTo(DozeMachine.State oldState, DozeMachine.State newState) {
switch (newState) {
case DOZE_AOD:
+ if (oldState == DOZE_AOD_PAUSED) {
+ mHost.dozeTimeTick();
+ }
+ scheduleTimeTick();
+ break;
case DOZE_AOD_PAUSING:
scheduleTimeTick();
break;
diff --git a/com/android/systemui/doze/DozeWallpaperState.java b/com/android/systemui/doze/DozeWallpaperState.java
index ee41001d..5156272b 100644
--- a/com/android/systemui/doze/DozeWallpaperState.java
+++ b/com/android/systemui/doze/DozeWallpaperState.java
@@ -23,6 +23,10 @@ import android.os.ServiceManager;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
+import com.android.systemui.statusbar.phone.DozeParameters;
+import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager;
import java.io.PrintWriter;
@@ -34,18 +38,28 @@ 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 final IWallpaperManager mWallpaperManagerService;
+ private boolean mKeyguardVisible;
private boolean mIsAmbientMode;
+ private final DozeParameters mDozeParameters;
- public DozeWallpaperState() {
+ public DozeWallpaperState(Context context) {
this(IWallpaperManager.Stub.asInterface(
- ServiceManager.getService(Context.WALLPAPER_SERVICE)));
+ ServiceManager.getService(Context.WALLPAPER_SERVICE)),
+ new DozeParameters(context), KeyguardUpdateMonitor.getInstance(context));
}
@VisibleForTesting
- DozeWallpaperState(IWallpaperManager wallpaperManagerService) {
+ DozeWallpaperState(IWallpaperManager wallpaperManagerService, DozeParameters parameters,
+ KeyguardUpdateMonitor keyguardUpdateMonitor) {
mWallpaperManagerService = wallpaperManagerService;
+ mDozeParameters = parameters;
+ keyguardUpdateMonitor.registerCallback(new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onKeyguardVisibilityChanged(boolean showing) {
+ mKeyguardVisible = showing;
+ }
+ });
}
@Override
@@ -58,17 +72,25 @@ public class DozeWallpaperState implements DozeMachine.Part {
case DOZE_REQUEST_PULSE:
case DOZE_PULSING:
case DOZE_PULSE_DONE:
- isAmbientMode = true;
+ isAmbientMode = mDozeParameters.getAlwaysOn();
break;
default:
isAmbientMode = false;
}
+ final boolean animated;
+ if (isAmbientMode) {
+ animated = mDozeParameters.getCanControlScreenOffAnimation() && !mKeyguardVisible;
+ } else {
+ animated = !mDozeParameters.getDisplayNeedsBlanking();
+ }
+
if (isAmbientMode != mIsAmbientMode) {
mIsAmbientMode = isAmbientMode;
try {
- Log.i(TAG, "AoD wallpaper state changed to: " + mIsAmbientMode);
- mWallpaperManagerService.setInAmbientMode(mIsAmbientMode);
+ Log.i(TAG, "AoD wallpaper state changed to: " + mIsAmbientMode
+ + ", animated: " + animated);
+ mWallpaperManagerService.setInAmbientMode(mIsAmbientMode, animated);
} 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);
diff --git a/com/android/systemui/fingerprint/FingerprintDialogImpl.java b/com/android/systemui/fingerprint/FingerprintDialogImpl.java
new file mode 100644
index 00000000..262c71ae
--- /dev/null
+++ b/com/android/systemui/fingerprint/FingerprintDialogImpl.java
@@ -0,0 +1,220 @@
+/*
+ * 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 com.android.systemui.fingerprint;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.fingerprint.FingerprintDialog;
+import android.hardware.fingerprint.IFingerprintDialogReceiver;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.WindowManager;
+
+import com.android.internal.os.SomeArgs;
+import com.android.systemui.SystemUI;
+import com.android.systemui.statusbar.CommandQueue;
+
+public class FingerprintDialogImpl extends SystemUI implements CommandQueue.Callbacks {
+ private static final String TAG = "FingerprintDialogImpl";
+ private static final boolean DEBUG = true;
+
+ protected static final int MSG_SHOW_DIALOG = 1;
+ protected static final int MSG_FINGERPRINT_AUTHENTICATED = 2;
+ protected static final int MSG_FINGERPRINT_HELP = 3;
+ protected static final int MSG_FINGERPRINT_ERROR = 4;
+ protected static final int MSG_HIDE_DIALOG = 5;
+ protected static final int MSG_BUTTON_NEGATIVE = 6;
+ protected static final int MSG_USER_CANCELED = 7;
+ protected static final int MSG_BUTTON_POSITIVE = 8;
+ protected static final int MSG_CLEAR_MESSAGE = 9;
+
+
+ private FingerprintDialogView mDialogView;
+ private WindowManager mWindowManager;
+ private IFingerprintDialogReceiver mReceiver;
+ private boolean mDialogShowing;
+
+ private Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch(msg.what) {
+ case MSG_SHOW_DIALOG:
+ handleShowDialog((SomeArgs) msg.obj);
+ break;
+ case MSG_FINGERPRINT_AUTHENTICATED:
+ handleFingerprintAuthenticated();
+ break;
+ case MSG_FINGERPRINT_HELP:
+ handleFingerprintHelp((String) msg.obj);
+ break;
+ case MSG_FINGERPRINT_ERROR:
+ handleFingerprintError((String) msg.obj);
+ break;
+ case MSG_HIDE_DIALOG:
+ handleHideDialog((Boolean) msg.obj);
+ break;
+ case MSG_BUTTON_NEGATIVE:
+ handleButtonNegative();
+ break;
+ case MSG_USER_CANCELED:
+ handleUserCanceled();
+ break;
+ case MSG_BUTTON_POSITIVE:
+ handleButtonPositive();
+ break;
+ case MSG_CLEAR_MESSAGE:
+ handleClearMessage();
+ break;
+ }
+ }
+ };
+
+ @Override
+ public void start() {
+ if (!mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
+ return;
+ }
+ getComponent(CommandQueue.class).addCallbacks(this);
+ mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ mDialogView = new FingerprintDialogView(mContext, mHandler);
+ }
+
+ @Override
+ public void showFingerprintDialog(Bundle bundle, IFingerprintDialogReceiver receiver) {
+ if (DEBUG) Log.d(TAG, "showFingerprintDialog");
+ // Remove these messages as they are part of the previous client
+ mHandler.removeMessages(MSG_FINGERPRINT_ERROR);
+ mHandler.removeMessages(MSG_FINGERPRINT_HELP);
+ mHandler.removeMessages(MSG_FINGERPRINT_AUTHENTICATED);
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = bundle;
+ args.arg2 = receiver;
+ mHandler.obtainMessage(MSG_SHOW_DIALOG, args).sendToTarget();
+ }
+
+ @Override
+ public void onFingerprintAuthenticated() {
+ if (DEBUG) Log.d(TAG, "onFingerprintAuthenticated");
+ mHandler.obtainMessage(MSG_FINGERPRINT_AUTHENTICATED).sendToTarget();
+ }
+
+ @Override
+ public void onFingerprintHelp(String message) {
+ if (DEBUG) Log.d(TAG, "onFingerprintHelp: " + message);
+ mHandler.obtainMessage(MSG_FINGERPRINT_HELP, message).sendToTarget();
+ }
+
+ @Override
+ public void onFingerprintError(String error) {
+ if (DEBUG) Log.d(TAG, "onFingerprintError: " + error);
+ mHandler.obtainMessage(MSG_FINGERPRINT_ERROR, error).sendToTarget();
+ }
+
+ @Override
+ public void hideFingerprintDialog() {
+ if (DEBUG) Log.d(TAG, "hideFingerprintDialog");
+ mHandler.obtainMessage(MSG_HIDE_DIALOG, false /* userCanceled */).sendToTarget();
+ }
+
+ private void handleShowDialog(SomeArgs args) {
+ if (DEBUG) Log.d(TAG, "handleShowDialog");
+ if (mDialogShowing) {
+ Log.w(TAG, "Dialog already showing");
+ return;
+ }
+ mReceiver = (IFingerprintDialogReceiver) args.arg2;
+ mDialogView.setBundle((Bundle)args.arg1);
+ mWindowManager.addView(mDialogView, mDialogView.getLayoutParams());
+ mDialogShowing = true;
+ }
+
+ private void handleFingerprintAuthenticated() {
+ if (DEBUG) Log.d(TAG, "handleFingerprintAuthenticated");
+ handleHideDialog(false /* userCanceled */);
+ }
+
+ private void handleFingerprintHelp(String message) {
+ if (DEBUG) Log.d(TAG, "handleFingerprintHelp: " + message);
+ mDialogView.showHelpMessage(message);
+ }
+
+ private void handleFingerprintError(String error) {
+ if (DEBUG) Log.d(TAG, "handleFingerprintError: " + error);
+ if (!mDialogShowing) {
+ if (DEBUG) Log.d(TAG, "Dialog already dismissed");
+ return;
+ }
+ mDialogView.showErrorMessage(error);
+ }
+
+ private void handleHideDialog(boolean userCanceled) {
+ if (DEBUG) Log.d(TAG, "handleHideDialog");
+ if (!mDialogShowing) {
+ // This can happen if there's a race and we get called from both
+ // onAuthenticated and onError, etc.
+ Log.w(TAG, "Dialog already dismissed, userCanceled: " + userCanceled);
+ return;
+ }
+ if (userCanceled) {
+ try {
+ mReceiver.onDialogDismissed(FingerprintDialog.DISMISSED_REASON_USER_CANCEL);
+ } catch (RemoteException e) {
+ Log.e(TAG, "RemoteException when hiding dialog", e);
+ }
+ }
+ mReceiver = null;
+ mWindowManager.removeView(mDialogView);
+ mDialogShowing = false;
+ }
+
+ private void handleButtonNegative() {
+ if (mReceiver == null) {
+ Log.e(TAG, "Receiver is null");
+ return;
+ }
+ try {
+ mReceiver.onDialogDismissed(FingerprintDialog.DISMISSED_REASON_NEGATIVE);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Remote exception when handling negative button", e);
+ }
+ handleHideDialog(false /* userCanceled */);
+ }
+
+ private void handleButtonPositive() {
+ if (mReceiver == null) {
+ Log.e(TAG, "Receiver is null");
+ return;
+ }
+ try {
+ mReceiver.onDialogDismissed(FingerprintDialog.DISMISSED_REASON_POSITIVE);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Remote exception when handling positive button", e);
+ }
+ handleHideDialog(false /* userCanceled */);
+ }
+
+ private void handleClearMessage() {
+ mDialogView.clearMessage();
+ }
+
+ private void handleUserCanceled() {
+ handleHideDialog(true /* userCanceled */);
+ }
+}
diff --git a/com/android/systemui/fingerprint/FingerprintDialogView.java b/com/android/systemui/fingerprint/FingerprintDialogView.java
new file mode 100644
index 00000000..9779937a
--- /dev/null
+++ b/com/android/systemui/fingerprint/FingerprintDialogView.java
@@ -0,0 +1,233 @@
+/*
+ * 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 com.android.systemui.fingerprint;
+
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+import android.hardware.fingerprint.FingerprintDialog;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.animation.Interpolator;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.android.systemui.Interpolators;
+import com.android.systemui.R;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.PackageManagerWrapper;
+
+/**
+ * This class loads the view for the system-provided dialog. The view consists of:
+ * Application Icon, Title, Subtitle, Description, Fingerprint Icon, Error/Help message area,
+ * and positive/negative buttons.
+ */
+public class FingerprintDialogView extends LinearLayout {
+
+ private static final String TAG = "FingerprintDialogView";
+
+ private static final int ANIMATION_DURATION = 250; // ms
+
+ private final IBinder mWindowToken = new Binder();
+ private final ActivityManagerWrapper mActivityManagerWrapper;
+ private final PackageManagerWrapper mPackageManageWrapper;
+ private final Interpolator mLinearOutSlowIn;
+ private final Interpolator mFastOutLinearIn;
+ private final float mAnimationTranslationOffset;
+
+ private ViewGroup mLayout;
+ private final TextView mErrorText;
+ private Handler mHandler;
+ private Bundle mBundle;
+ private final LinearLayout mDialog;
+
+ public FingerprintDialogView(Context context, Handler handler) {
+ super(context);
+ mHandler = handler;
+ mActivityManagerWrapper = ActivityManagerWrapper.getInstance();
+ mPackageManageWrapper = PackageManagerWrapper.getInstance();
+ mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
+ mFastOutLinearIn = Interpolators.FAST_OUT_LINEAR_IN;
+ mAnimationTranslationOffset = getResources()
+ .getDimension(R.dimen.fingerprint_dialog_animation_translation_offset);
+
+ // Create the dialog
+ LayoutInflater factory = LayoutInflater.from(getContext());
+ mLayout = (ViewGroup) factory.inflate(R.layout.fingerprint_dialog, this, false);
+ addView(mLayout);
+
+ mDialog = mLayout.findViewById(R.id.dialog);
+
+ mErrorText = mLayout.findViewById(R.id.error);
+
+ mLayout.setOnKeyListener(new View.OnKeyListener() {
+ boolean downPressed = false;
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (keyCode != KeyEvent.KEYCODE_BACK) {
+ return false;
+ }
+ if (event.getAction() == KeyEvent.ACTION_DOWN && downPressed == false) {
+ downPressed = true;
+ } else if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ downPressed = false;
+ } else if (event.getAction() == KeyEvent.ACTION_UP && downPressed == true) {
+ downPressed = false;
+ mHandler.obtainMessage(FingerprintDialogImpl.MSG_USER_CANCELED).sendToTarget();
+ }
+ return true;
+ }
+ });
+
+ final View space = mLayout.findViewById(R.id.space);
+ final Button negative = mLayout.findViewById(R.id.button2);
+ final Button positive = mLayout.findViewById(R.id.button1);
+
+ space.setClickable(true);
+ space.setOnTouchListener((View view, MotionEvent event) -> {
+ mHandler.obtainMessage(FingerprintDialogImpl.MSG_HIDE_DIALOG, true /* userCanceled*/)
+ .sendToTarget();
+ return true;
+ });
+
+ negative.setOnClickListener((View v) -> {
+ mHandler.obtainMessage(FingerprintDialogImpl.MSG_BUTTON_NEGATIVE).sendToTarget();
+ });
+
+ positive.setOnClickListener((View v) -> {
+ mHandler.obtainMessage(FingerprintDialogImpl.MSG_BUTTON_POSITIVE).sendToTarget();
+ });
+
+ mLayout.setFocusableInTouchMode(true);
+ mLayout.requestFocus();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ final TextView title = mLayout.findViewById(R.id.title);
+ final TextView subtitle = mLayout.findViewById(R.id.subtitle);
+ final TextView description = mLayout.findViewById(R.id.description);
+ final Button negative = mLayout.findViewById(R.id.button2);
+ final ImageView image = mLayout.findViewById(R.id.icon);
+ final Button positive = mLayout.findViewById(R.id.button1);
+ final ImageView fingerprint_icon = mLayout.findViewById(R.id.fingerprint_icon);
+
+ title.setText(mBundle.getCharSequence(FingerprintDialog.KEY_TITLE));
+ title.setSelected(true);
+ subtitle.setText(mBundle.getCharSequence(FingerprintDialog.KEY_SUBTITLE));
+ description.setText(mBundle.getCharSequence(FingerprintDialog.KEY_DESCRIPTION));
+ negative.setText(mBundle.getCharSequence(FingerprintDialog.KEY_NEGATIVE_TEXT));
+ setAppIcon(image);
+
+ final CharSequence positiveText =
+ mBundle.getCharSequence(FingerprintDialog.KEY_POSITIVE_TEXT);
+ positive.setText(positiveText); // needs to be set for marquee to work
+ if (positiveText != null) {
+ positive.setVisibility(View.VISIBLE);
+ } else {
+ positive.setVisibility(View.GONE);
+ }
+
+ // Dim the background and slide the dialog up
+ mDialog.setTranslationY(mAnimationTranslationOffset);
+ mLayout.setAlpha(0f);
+ postOnAnimation(new Runnable() {
+ @Override
+ public void run() {
+ mLayout.animate()
+ .alpha(1f)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(mLinearOutSlowIn)
+ .withLayer()
+ .start();
+ mDialog.animate()
+ .translationY(0)
+ .setDuration(ANIMATION_DURATION)
+ .setInterpolator(mLinearOutSlowIn)
+ .withLayer()
+ .start();
+ }
+ });
+ }
+
+ public void setBundle(Bundle bundle) {
+ mBundle = bundle;
+ }
+
+ protected void clearMessage() {
+ mErrorText.setVisibility(View.INVISIBLE);
+ }
+
+ private void showMessage(String message) {
+ mHandler.removeMessages(FingerprintDialogImpl.MSG_CLEAR_MESSAGE);
+ mErrorText.setText(message);
+ mErrorText.setContentDescription(message);
+ mErrorText.setVisibility(View.VISIBLE);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(FingerprintDialogImpl.MSG_CLEAR_MESSAGE),
+ FingerprintDialog.HIDE_DIALOG_DELAY);
+ }
+
+ public void showHelpMessage(String message) {
+ showMessage(message);
+ }
+
+ public void showErrorMessage(String error) {
+ showMessage(error);
+ mHandler.sendMessageDelayed(mHandler.obtainMessage(FingerprintDialogImpl.MSG_HIDE_DIALOG,
+ false /* userCanceled */), FingerprintDialog.HIDE_DIALOG_DELAY);
+ }
+
+ private void setAppIcon(ImageView image) {
+ final ActivityManager.RunningTaskInfo taskInfo = mActivityManagerWrapper.getRunningTask();
+ final ComponentName cn = taskInfo.topActivity;
+ final int userId = mActivityManagerWrapper.getCurrentUserId();
+ final ActivityInfo activityInfo = mPackageManageWrapper.getActivityInfo(cn, userId);
+ image.setImageDrawable(mActivityManagerWrapper.getBadgedActivityIcon(activityInfo, userId));
+ image.setContentDescription(
+ getResources().getString(R.string.accessibility_fingerprint_dialog_app_icon)
+ + " "
+ + mActivityManagerWrapper.getBadgedActivityLabel(activityInfo, userId));
+ }
+
+ public WindowManager.LayoutParams getLayoutParams() {
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ WindowManager.LayoutParams.TYPE_STATUS_BAR_PANEL,
+ WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
+ PixelFormat.TRANSLUCENT);
+ lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
+ lp.setTitle("FingerprintDialogView");
+ lp.token = mWindowToken;
+ return lp;
+ }
+}
diff --git a/com/android/systemui/globalactions/GlobalActionsComponent.java b/com/android/systemui/globalactions/GlobalActionsComponent.java
index f06cda0f..aa085626 100644
--- a/com/android/systemui/globalactions/GlobalActionsComponent.java
+++ b/com/android/systemui/globalactions/GlobalActionsComponent.java
@@ -14,6 +14,10 @@
package com.android.systemui.globalactions;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.Dependency;
import com.android.systemui.SysUiServiceProvider;
@@ -25,10 +29,6 @@ import com.android.systemui.statusbar.CommandQueue.Callbacks;
import com.android.systemui.statusbar.policy.ExtensionController;
import com.android.systemui.statusbar.policy.ExtensionController.Extension;
-import android.content.Context;
-import android.os.RemoteException;
-import android.os.ServiceManager;
-
public class GlobalActionsComponent extends SystemUI implements Callbacks, GlobalActionsManager {
private GlobalActions mPlugin;
diff --git a/com/android/systemui/globalactions/GlobalActionsDialog.java b/com/android/systemui/globalactions/GlobalActionsDialog.java
index 5c8c3f35..0f34513b 100644
--- a/com/android/systemui/globalactions/GlobalActionsDialog.java
+++ b/com/android/systemui/globalactions/GlobalActionsDialog.java
@@ -14,17 +14,22 @@
package com.android.systemui.globalactions;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST;
+import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED;
import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN;
import android.app.ActivityManager;
import android.app.Dialog;
+import android.app.KeyguardManager;
import android.app.WallpaperManager;
import android.app.admin.DevicePolicyManager;
import android.content.BroadcastReceiver;
+import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.ServiceConnection;
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.graphics.Point;
@@ -33,7 +38,9 @@ import android.media.AudioManager;
import android.net.ConnectivityManager;
import android.os.Build;
import android.os.Handler;
+import android.os.IBinder;
import android.os.Message;
+import android.os.Messenger;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemProperties;
@@ -74,6 +81,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.telephony.TelephonyIntents;
import com.android.internal.telephony.TelephonyProperties;
import com.android.internal.util.EmergencyAffordanceManager;
+import com.android.internal.util.ScreenshotHelper;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.Dependency;
import com.android.systemui.HardwareUiLayout;
@@ -114,12 +122,15 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
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 static final String GLOBAL_ACTION_KEY_SCREENSHOT = "screenshot";
private final Context mContext;
private final GlobalActionsManager mWindowManagerFuncs;
private final AudioManager mAudioManager;
private final IDreamManager mDreamManager;
private final DevicePolicyManager mDevicePolicyManager;
+ private final LockPatternUtils mLockPatternUtils;
+ private final KeyguardManager mKeyguardManager;
private ArrayList<Action> mItems;
private ActionsDialog mDialog;
@@ -138,6 +149,7 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
private boolean mHasLogoutButton;
private final boolean mShowSilentToggle;
private final EmergencyAffordanceManager mEmergencyAffordanceManager;
+ private final ScreenshotHelper mScreenshotHelper;
/**
* @param context everything needs a context :(
@@ -150,6 +162,8 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
ServiceManager.getService(DreamService.DREAM_SERVICE));
mDevicePolicyManager = (DevicePolicyManager) mContext.getSystemService(
Context.DEVICE_POLICY_SERVICE);
+ mLockPatternUtils = new LockPatternUtils(mContext);
+ mKeyguardManager = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE);
// receive broadcasts
IntentFilter filter = new IntentFilter();
@@ -176,6 +190,7 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
R.bool.config_useFixedVolume);
mEmergencyAffordanceManager = new EmergencyAffordanceManager(context);
+ mScreenshotHelper = new ScreenshotHelper(context);
}
/**
@@ -323,7 +338,8 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
mItems.add(getSettingsAction());
} else if (GLOBAL_ACTION_KEY_LOCKDOWN.equals(actionKey)) {
if (Settings.Secure.getInt(mContext.getContentResolver(),
- Settings.Secure.LOCKDOWN_IN_POWER_MENU, 0) != 0) {
+ Settings.Secure.LOCKDOWN_IN_POWER_MENU, 0) != 0
+ && shouldDisplayLockdown()) {
mItems.add(getLockdownAction());
}
} else if (GLOBAL_ACTION_KEY_VOICEASSIST.equals(actionKey)) {
@@ -332,6 +348,8 @@ 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_SCREENSHOT.equals(actionKey)) {
+ mItems.add(new ScreenshotAction());
} else if (GLOBAL_ACTION_KEY_LOGOUT.equals(actionKey)) {
if (mDevicePolicyManager.isLogoutEnabled()
&& getCurrentUser().id != UserHandle.USER_SYSTEM) {
@@ -372,6 +390,19 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
return dialog;
}
+ private boolean shouldDisplayLockdown() {
+ int userId = getCurrentUser().id;
+ // Lockdown is meaningless without a place to go.
+ if (!mKeyguardManager.isDeviceSecure(userId)) {
+ return false;
+ }
+
+ // Only show the lockdown button if the device isn't locked down (for whatever reason).
+ int state = mLockPatternUtils.getStrongAuthForUser(userId);
+ return (state == STRONG_AUTH_NOT_REQUIRED
+ || state == SOME_AUTH_REQUIRED_AFTER_USER_REQUEST);
+ }
+
private final class PowerAction extends SinglePressAction implements LongPressAction {
private PowerAction() {
super(R.drawable.ic_lock_power_off,
@@ -437,6 +468,38 @@ class GlobalActionsDialog implements DialogInterface.OnDismissListener,
}
+ private class ScreenshotAction extends SinglePressAction {
+ public ScreenshotAction() {
+ super(R.drawable.ic_screenshot, R.string.global_action_screenshot);
+ }
+
+ @Override
+ public void onPress() {
+ // Add a little delay before executing, to give the
+ // dialog a chance to go away before it takes a
+ // screenshot.
+ // TODO: instead, omit global action dialog layer
+ mHandler.postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ mScreenshotHelper.takeScreenshot(1, true, true, mHandler);
+ MetricsLogger.action(mContext,
+ MetricsEvent.ACTION_SCREENSHOT_POWER_MENU);
+ }
+ }, 500);
+ }
+
+ @Override
+ public boolean showDuringKeyguard() {
+ return true;
+ }
+
+ @Override
+ public boolean showBeforeProvisioning() {
+ return false;
+ }
+ }
+
private class BugReportAction extends SinglePressAction implements LongPressAction {
public BugReportAction() {
diff --git a/com/android/systemui/keyboard/KeyboardUI.java b/com/android/systemui/keyboard/KeyboardUI.java
index 4b775a5a..b8411e29 100644
--- a/com/android/systemui/keyboard/KeyboardUI.java
+++ b/com/android/systemui/keyboard/KeyboardUI.java
@@ -608,6 +608,9 @@ public class KeyboardUI extends SystemUI implements InputManager.OnTabletModeCha
public void onScanningStateChanged(boolean started) { }
@Override
public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { }
+ @Override
+ public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice,
+ int bluetoothProfile) { }
}
private final class BluetoothErrorListener implements Utils.ErrorListener {
diff --git a/com/android/systemui/keyguard/KeyguardService.java b/com/android/systemui/keyguard/KeyguardService.java
index 2a5ae0d3..22b41a4f 100644
--- a/com/android/systemui/keyguard/KeyguardService.java
+++ b/com/android/systemui/keyguard/KeyguardService.java
@@ -96,9 +96,9 @@ public class KeyguardService extends Service {
}
@Override // Binder interface
- public void dismiss(IKeyguardDismissCallback callback) {
+ public void dismiss(IKeyguardDismissCallback callback, CharSequence message) {
checkPermission();
- mKeyguardViewMediator.dismiss(callback);
+ mKeyguardViewMediator.dismiss(callback, message);
}
@Override // Binder interface
diff --git a/com/android/systemui/keyguard/KeyguardSliceProvider.java b/com/android/systemui/keyguard/KeyguardSliceProvider.java
index bd46c5f8..e49e80df 100644
--- a/com/android/systemui/keyguard/KeyguardSliceProvider.java
+++ b/com/android/systemui/keyguard/KeyguardSliceProvider.java
@@ -103,11 +103,12 @@ public class KeyguardSliceProvider extends SliceProvider implements
@Override
public Slice onBindSlice(Uri sliceUri) {
- ListBuilder builder = new ListBuilder(mSliceUri)
- .addRow(new RowBuilder(mDateUri).setTitle(mLastText));
+ ListBuilder builder = new ListBuilder(getContext(), mSliceUri);
+ builder.addRow(new RowBuilder(builder, 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));
+ builder.addRow(new RowBuilder(builder, mAlarmUri)
+ .setTitle(mNextAlarm).addEndItem(icon));
}
return builder.build();
diff --git a/com/android/systemui/keyguard/KeyguardViewMediator.java b/com/android/systemui/keyguard/KeyguardViewMediator.java
index 91ae4485..8501519d 100644
--- a/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -25,7 +25,6 @@ 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.ActivityManager;
import android.app.AlarmManager;
import android.app.NotificationManager;
@@ -344,6 +343,7 @@ public class KeyguardViewMediator extends SystemUI {
private boolean mWakeAndUnlocking;
private IKeyguardDrawnCallback mDrawnCallback;
private boolean mLockWhenSimRemoved;
+ private CharSequence mCustomMessage;
KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() {
@@ -368,7 +368,7 @@ public class KeyguardViewMediator extends SystemUI {
return;
} else if (info.isGuest() || info.isDemo()) {
// If we just switched to a guest, try to dismiss keyguard.
- dismiss(null /* callback */);
+ dismiss(null /* callback */, null /* message */);
}
}
}
@@ -654,6 +654,13 @@ public class KeyguardViewMediator extends SystemUI {
}
@Override
+ public CharSequence consumeCustomMessage() {
+ final CharSequence message = mCustomMessage;
+ mCustomMessage = null;
+ return message;
+ }
+
+ @Override
public void onSecondaryDisplayShowingChanged(int displayId) {
synchronized (KeyguardViewMediator.this) {
setShowingLocked(mShowing, displayId, false);
@@ -700,6 +707,10 @@ public class KeyguardViewMediator extends SystemUI {
&& !mLockPatternUtils.isLockScreenDisabled(
KeyguardUpdateMonitor.getCurrentUser()),
mSecondaryDisplayShowing, true /* forceCallbacks */);
+ } else {
+ // The system's keyguard is disabled or missing.
+ setShowingLocked(mLockPatternUtils.isSecure(KeyguardUpdateMonitor.getCurrentUser()),
+ mSecondaryDisplayShowing, true);
}
mStatusBarKeyguardViewManager =
@@ -1321,20 +1332,22 @@ public class KeyguardViewMediator extends SystemUI {
/**
* Dismiss the keyguard through the security layers.
* @param callback Callback to be informed about the result
+ * @param message Message that should be displayed on the bouncer.
*/
- private void handleDismiss(IKeyguardDismissCallback callback) {
+ private void handleDismiss(IKeyguardDismissCallback callback, CharSequence message) {
if (mShowing) {
if (callback != null) {
mDismissCallbackRegistry.addCallback(callback);
}
+ mCustomMessage = message;
mStatusBarKeyguardViewManager.dismissAndCollapse();
} else if (callback != null) {
new DismissCallbackWrapper(callback).notifyDismissError();
}
}
- public void dismiss(IKeyguardDismissCallback callback) {
- mHandler.obtainMessage(DISMISS, callback).sendToTarget();
+ public void dismiss(IKeyguardDismissCallback callback, CharSequence message) {
+ mHandler.obtainMessage(DISMISS, new DismissMessage(callback, message)).sendToTarget();
}
/**
@@ -1551,7 +1564,8 @@ public class KeyguardViewMediator extends SystemUI {
}
break;
case DISMISS:
- handleDismiss((IKeyguardDismissCallback) msg.obj);
+ final DismissMessage message = (DismissMessage) msg.obj;
+ handleDismiss(message.getCallback(), message.getMessage());
break;
case START_KEYGUARD_EXIT_ANIM:
Trace.beginSection("KeyguardViewMediator#handleMessage START_KEYGUARD_EXIT_ANIM");
@@ -2161,4 +2175,22 @@ public class KeyguardViewMediator extends SystemUI {
}
}
}
+
+ private static class DismissMessage {
+ private final CharSequence mMessage;
+ private final IKeyguardDismissCallback mCallback;
+
+ DismissMessage(IKeyguardDismissCallback callback, CharSequence message) {
+ mCallback = callback;
+ mMessage = message;
+ }
+
+ public IKeyguardDismissCallback getCallback() {
+ return mCallback;
+ }
+
+ public CharSequence getMessage() {
+ return mMessage;
+ }
+ }
}
diff --git a/com/android/systemui/pip/phone/PipAppOpsListener.java b/com/android/systemui/pip/phone/PipAppOpsListener.java
new file mode 100644
index 00000000..f0e4ccc1
--- /dev/null
+++ b/com/android/systemui/pip/phone/PipAppOpsListener.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES 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 android.app.AppOpsManager;
+import android.app.AppOpsManager.OnOpChangedListener;
+import android.app.IActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.util.Pair;
+
+public class PipAppOpsListener {
+ private static final String TAG = PipAppOpsListener.class.getSimpleName();
+
+ private Context mContext;
+ private IActivityManager mActivityManager;
+ private AppOpsManager mAppOpsManager;
+
+ private PipMotionHelper mMotionHelper;
+
+ 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 PipAppOpsListener(Context context, IActivityManager activityManager,
+ PipMotionHelper motionHelper) {
+ mContext = context;
+ mActivityManager = activityManager;
+ mAppOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+ mMotionHelper = motionHelper;
+ }
+
+ public void onActivityPinned(String packageName) {
+ // Register for changes to the app ops setting for this package while it is in PiP
+ registerAppOpsListener(packageName);
+ }
+
+ public void onActivityUnpinned() {
+ // Unregister for changes to the previously PiP'ed package
+ unregisterAppOpsListener();
+ }
+
+ private void registerAppOpsListener(String packageName) {
+ mAppOpsManager.startWatchingMode(OP_PICTURE_IN_PICTURE, packageName,
+ mAppOpsChangedListener);
+ }
+
+ private void unregisterAppOpsListener() {
+ mAppOpsManager.stopWatchingMode(mAppOpsChangedListener);
+ }
+} \ No newline at end of file
diff --git a/com/android/systemui/pip/phone/PipManager.java b/com/android/systemui/pip/phone/PipManager.java
index dce3e243..24d0126a 100644
--- a/com/android/systemui/pip/phone/PipManager.java
+++ b/com/android/systemui/pip/phone/PipManager.java
@@ -16,12 +16,10 @@
package com.android.systemui.pip.phone;
-import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
-import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.WindowManager.INPUT_CONSUMER_PIP;
import android.app.ActivityManager;
-import android.app.ActivityManager.StackInfo;
import android.app.IActivityManager;
import android.content.ComponentName;
import android.content.Context;
@@ -43,6 +41,7 @@ import com.android.systemui.recents.events.component.ExpandPipEvent;
import com.android.systemui.recents.misc.SysUiTaskStackChangeListener;
import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.shared.system.ActivityManagerWrapper;
+import com.android.systemui.shared.system.InputConsumerController;
import java.io.PrintWriter;
@@ -65,6 +64,7 @@ public class PipManager implements BasePipManager {
private PipMenuActivityController mMenuController;
private PipMediaController mMediaController;
private PipTouchHandler mTouchHandler;
+ private PipAppOpsListener mAppOpsListener;
/**
* Handler for system task stack changes.
@@ -75,6 +75,7 @@ public class PipManager implements BasePipManager {
mTouchHandler.onActivityPinned();
mMediaController.onActivityPinned();
mMenuController.onActivityPinned();
+ mAppOpsListener.onActivityPinned(packageName);
SystemServicesProxy.getInstance(mContext).setPipVisibility(true);
}
@@ -87,6 +88,7 @@ public class PipManager implements BasePipManager {
final int userId = topActivity != null ? topPipActivityInfo.second : 0;
mMenuController.onActivityUnpinned();
mTouchHandler.onActivityUnpinned(topActivity);
+ mAppOpsListener.onActivityUnpinned();
SystemServicesProxy.getInstance(mContext).setPipVisibility(topActivity != null);
}
@@ -171,12 +173,15 @@ public class PipManager implements BasePipManager {
}
ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
- mInputConsumerController = new InputConsumerController(mWindowManager);
+ mInputConsumerController = InputConsumerController.getPipInputConsumer();
+ mInputConsumerController.registerInputConsumer();
mMediaController = new PipMediaController(context, mActivityManager);
mMenuController = new PipMenuActivityController(context, mActivityManager, mMediaController,
mInputConsumerController);
mTouchHandler = new PipTouchHandler(context, mActivityManager, mMenuController,
mInputConsumerController);
+ mAppOpsListener = new PipAppOpsListener(context, mActivityManager,
+ mTouchHandler.getMotionHelper());
EventBus.getDefault().register(this);
}
diff --git a/com/android/systemui/pip/phone/PipMenuActivity.java b/com/android/systemui/pip/phone/PipMenuActivity.java
index bfe07a98..0486a9dc 100644
--- a/com/android/systemui/pip/phone/PipMenuActivity.java
+++ b/com/android/systemui/pip/phone/PipMenuActivity.java
@@ -373,7 +373,7 @@ public class PipMenuActivity extends Activity {
if (menuState == MENU_STATE_FULL) {
mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim);
} else {
- mMenuContainerAnimator.playTogether(settingsAnim, dismissAnim);
+ mMenuContainerAnimator.playTogether(dismissAnim);
}
mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN);
mMenuContainerAnimator.setDuration(MENU_FADE_DURATION);
diff --git a/com/android/systemui/pip/phone/PipMenuActivityController.java b/com/android/systemui/pip/phone/PipMenuActivityController.java
index 9fb201b8..26fced30 100644
--- a/com/android/systemui/pip/phone/PipMenuActivityController.java
+++ b/com/android/systemui/pip/phone/PipMenuActivityController.java
@@ -23,7 +23,6 @@ import android.app.ActivityManager.StackInfo;
import android.app.ActivityOptions;
import android.app.IActivityManager;
import android.app.RemoteAction;
-import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ParceledListSlice;
@@ -43,6 +42,7 @@ import com.android.systemui.pip.phone.PipMediaController.ActionListener;
import com.android.systemui.recents.events.EventBus;
import com.android.systemui.recents.events.component.HidePipMenuEvent;
import com.android.systemui.recents.misc.ReferenceCountedTrigger;
+import com.android.systemui.shared.system.InputConsumerController;
import java.io.PrintWriter;
import java.util.ArrayList;
diff --git a/com/android/systemui/pip/phone/PipTouchHandler.java b/com/android/systemui/pip/phone/PipTouchHandler.java
index 2b48e0fb..b2535173 100644
--- a/com/android/systemui/pip/phone/PipTouchHandler.java
+++ b/com/android/systemui/pip/phone/PipTouchHandler.java
@@ -42,11 +42,11 @@ import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityWindowInfo;
-
-import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.os.logging.MetricsLoggerWrapper;
import com.android.internal.policy.PipSnapAlgorithm;
import com.android.systemui.R;
+import com.android.systemui.shared.system.InputConsumerController;
import com.android.systemui.statusbar.FlingAnimationUtils;
import java.io.PrintWriter;
@@ -63,10 +63,6 @@ public class PipTouchHandler {
// Allow the PIP to be flung from anywhere on the screen to the bottom to be dismissed.
private static final boolean ENABLE_FLING_DISMISS = false;
- // These values are used for metrics and should never change
- private static final int METRIC_VALUE_DISMISSED_BY_TAP = 0;
- private static final int METRIC_VALUE_DISMISSED_BY_DRAG = 1;
-
private static final int SHOW_DISMISS_AFFORDANCE_DELAY = 225;
// Allow dragging the PIP to a location to close it
@@ -163,8 +159,7 @@ public class PipTouchHandler {
@Override
public void onPipDismiss() {
mMotionHelper.dismissPip();
- MetricsLogger.action(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_DISMISSED,
- METRIC_VALUE_DISMISSED_BY_TAP);
+ MetricsLoggerWrapper.logPictureInPictureDismissByTap(mContext);
}
@Override
@@ -463,8 +458,7 @@ public class PipTouchHandler {
return;
}
if (mIsMinimized != isMinimized) {
- MetricsLogger.action(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_MINIMIZED,
- isMinimized);
+ MetricsLoggerWrapper.logPictureInPictureMinimize(mContext, isMinimized);
}
mIsMinimized = isMinimized;
mSnapAlgorithm.setMinimized(isMinimized);
@@ -537,8 +531,7 @@ public class PipTouchHandler {
mMenuState = menuState;
updateMovementBounds(menuState);
if (menuState != MENU_STATE_CLOSE) {
- MetricsLogger.visibility(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_MENU,
- menuState == MENU_STATE_FULL);
+ MetricsLoggerWrapper.logPictureInPictureMenuVisible(mContext, menuState == MENU_STATE_FULL);
}
}
@@ -670,9 +663,7 @@ public class PipTouchHandler {
if (mMotionHelper.shouldDismissPip() || isFlingToBot) {
mMotionHelper.animateDismiss(mMotionHelper.getBounds(), vel.x,
vel.y, mUpdateScrimListener);
- MetricsLogger.action(mContext,
- MetricsEvent.ACTION_PICTURE_IN_PICTURE_DISMISSED,
- METRIC_VALUE_DISMISSED_BY_DRAG);
+ MetricsLoggerWrapper.logPictureInPictureDismissByDrag(mContext);
return true;
}
}
diff --git a/com/android/systemui/plugins/qs/QSTile.java b/com/android/systemui/plugins/qs/QSTile.java
index c52c0aae..61f7fe8d 100644
--- a/com/android/systemui/plugins/qs/QSTile.java
+++ b/com/android/systemui/plugins/qs/QSTile.java
@@ -108,6 +108,7 @@ public interface QSTile {
public Supplier<Icon> iconSupplier;
public int state = Tile.STATE_ACTIVE;
public CharSequence label;
+ public CharSequence secondaryLabel;
public CharSequence contentDescription;
public CharSequence dualLabelContentDescription;
public boolean disabledByPolicy;
@@ -122,6 +123,7 @@ public interface QSTile {
final boolean changed = !Objects.equals(other.icon, icon)
|| !Objects.equals(other.iconSupplier, iconSupplier)
|| !Objects.equals(other.label, label)
+ || !Objects.equals(other.secondaryLabel, secondaryLabel)
|| !Objects.equals(other.contentDescription, contentDescription)
|| !Objects.equals(other.dualLabelContentDescription,
dualLabelContentDescription)
@@ -135,6 +137,7 @@ public interface QSTile {
other.icon = icon;
other.iconSupplier = iconSupplier;
other.label = label;
+ other.secondaryLabel = secondaryLabel;
other.contentDescription = contentDescription;
other.dualLabelContentDescription = dualLabelContentDescription;
other.expandedAccessibilityClassName = expandedAccessibilityClassName;
@@ -156,6 +159,7 @@ public interface QSTile {
sb.append(",icon=").append(icon);
sb.append(",iconSupplier=").append(iconSupplier);
sb.append(",label=").append(label);
+ sb.append(",secondaryLabel=").append(secondaryLabel);
sb.append(",contentDescription=").append(contentDescription);
sb.append(",dualLabelContentDescription=").append(dualLabelContentDescription);
sb.append(",expandedAccessibilityClassName=").append(expandedAccessibilityClassName);
diff --git a/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java b/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
index 56a3ee3a..e25930c1 100644
--- a/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
+++ b/com/android/systemui/plugins/statusbar/phone/NavBarButtonProvider.java
@@ -50,5 +50,7 @@ public interface NavBarButtonProvider extends Plugin {
}
void setDarkIntensity(float intensity);
+
+ void setDelayTouchFeedback(boolean shouldDelay);
}
}
diff --git a/com/android/systemui/plugins/statusbar/phone/NavGesture.java b/com/android/systemui/plugins/statusbar/phone/NavGesture.java
index 674ed5a3..6131acc9 100644
--- a/com/android/systemui/plugins/statusbar/phone/NavGesture.java
+++ b/com/android/systemui/plugins/statusbar/phone/NavGesture.java
@@ -14,6 +14,7 @@
package com.android.systemui.plugins.statusbar.phone;
+import android.graphics.Canvas;
import android.view.MotionEvent;
import com.android.systemui.plugins.Plugin;
@@ -35,6 +36,12 @@ public interface NavGesture extends Plugin {
public void setBarState(boolean vertical, boolean isRtl);
+ public void onDraw(Canvas canvas);
+
+ public void onDarkIntensityChange(float intensity);
+
+ public void onLayout(boolean changed, int left, int top, int right, int bottom);
+
public default void destroy() { }
}
diff --git a/com/android/systemui/power/EnhancedEstimates.java b/com/android/systemui/power/EnhancedEstimates.java
new file mode 100644
index 00000000..bd130f4b
--- /dev/null
+++ b/com/android/systemui/power/EnhancedEstimates.java
@@ -0,0 +1,26 @@
+package com.android.systemui.power;
+
+public interface EnhancedEstimates {
+
+ /**
+ * Returns a boolean indicating if the hybrid notification should be used.
+ */
+ boolean isHybridNotificationEnabled();
+
+ /**
+ * Returns an estimate object if the feature is enabled.
+ */
+ Estimate getEstimate();
+
+ /**
+ * Returns a long indicating the amount of time remaining in milliseconds under which we will
+ * show a regular warning to the user.
+ */
+ long getLowWarningThreshold();
+
+ /**
+ * Returns a long indicating the amount of time remaining in milliseconds under which we will
+ * show a severe warning to the user.
+ */
+ long getSevereWarningThreshold();
+}
diff --git a/com/android/systemui/power/EnhancedEstimatesImpl.java b/com/android/systemui/power/EnhancedEstimatesImpl.java
new file mode 100644
index 00000000..5686d801
--- /dev/null
+++ b/com/android/systemui/power/EnhancedEstimatesImpl.java
@@ -0,0 +1,26 @@
+package com.android.systemui.power;
+
+import android.util.Log;
+
+public class EnhancedEstimatesImpl implements EnhancedEstimates {
+
+ @Override
+ public boolean isHybridNotificationEnabled() {
+ return false;
+ }
+
+ @Override
+ public Estimate getEstimate() {
+ return null;
+ }
+
+ @Override
+ public long getLowWarningThreshold() {
+ return 0;
+ }
+
+ @Override
+ public long getSevereWarningThreshold() {
+ return 0;
+ }
+}
diff --git a/com/android/systemui/power/Estimate.java b/com/android/systemui/power/Estimate.java
new file mode 100644
index 00000000..12a8f0a4
--- /dev/null
+++ b/com/android/systemui/power/Estimate.java
@@ -0,0 +1,11 @@
+package com.android.systemui.power;
+
+public class Estimate {
+ public final long estimateMillis;
+ public final boolean isBasedOnUsage;
+
+ public Estimate(long estimateMillis, boolean isBasedOnUsage) {
+ this.estimateMillis = estimateMillis;
+ this.isBasedOnUsage = isBasedOnUsage;
+ }
+}
diff --git a/com/android/systemui/power/PowerNotificationWarnings.java b/com/android/systemui/power/PowerNotificationWarnings.java
index c29b362b..aa566947 100644
--- a/com/android/systemui/power/PowerNotificationWarnings.java
+++ b/com/android/systemui/power/PowerNotificationWarnings.java
@@ -17,40 +17,40 @@
package com.android.systemui.power;
import android.app.Notification;
-import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.DialogInterface.OnDismissListener;
import android.content.Intent;
import android.content.IntentFilter;
+import android.icu.text.MeasureFormat;
+import android.icu.text.MeasureFormat.FormatWidth;
+import android.icu.util.Measure;
+import android.icu.util.MeasureUnit;
import android.media.AudioAttributes;
-import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import android.os.PowerManager;
-import android.os.SystemClock;
import android.os.UserHandle;
-import android.provider.Settings;
import android.support.annotation.VisibleForTesting;
+import android.text.format.DateUtils;
import android.util.Slog;
import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
-import com.android.internal.notification.SystemNotificationChannels;
import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.SystemUI;
-import com.android.systemui.statusbar.phone.StatusBar;
import com.android.systemui.statusbar.phone.SystemUIDialog;
import com.android.systemui.util.NotificationChannels;
import java.io.PrintWriter;
import java.text.NumberFormat;
+import java.util.Locale;
+import java.util.concurrent.TimeUnit;
public class PowerNotificationWarnings implements PowerUI.WarningsUI {
private static final String TAG = PowerUI.TAG + ".Notification";
@@ -96,8 +96,11 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI {
private long mScreenOffTime;
private int mShowing;
- private long mBucketDroppedNegativeTimeMs;
+ private long mWarningTriggerTimeMs;
+ private Estimate mEstimate;
+ private long mLowWarningThreshold;
+ private long mSevereWarningThreshold;
private boolean mWarning;
private boolean mPlaySound;
private boolean mInvalidCharger;
@@ -130,14 +133,29 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI {
public void update(int batteryLevel, int bucket, long screenOffTime) {
mBatteryLevel = batteryLevel;
if (bucket >= 0) {
- mBucketDroppedNegativeTimeMs = 0;
+ mWarningTriggerTimeMs = 0;
} else if (bucket < mBucket) {
- mBucketDroppedNegativeTimeMs = System.currentTimeMillis();
+ mWarningTriggerTimeMs = System.currentTimeMillis();
}
mBucket = bucket;
mScreenOffTime = screenOffTime;
}
+ @Override
+ public void updateEstimate(Estimate estimate) {
+ mEstimate = estimate;
+ if (estimate.estimateMillis <= mLowWarningThreshold) {
+ mWarningTriggerTimeMs = System.currentTimeMillis();
+ }
+ }
+
+ @Override
+ public void updateThresholds(long lowThreshold, long severeThreshold) {
+ mLowWarningThreshold = lowThreshold;
+ mSevereWarningThreshold = severeThreshold;
+ }
+
+
private void updateNotification() {
if (DEBUG) Slog.d(TAG, "updateNotification mWarning=" + mWarning + " mPlaySound="
+ mPlaySound + " mInvalidCharger=" + mInvalidCharger);
@@ -171,25 +189,45 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI {
mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_BAD_CHARGER, n, UserHandle.ALL);
}
- private void showWarningNotification() {
- final int textRes = R.string.battery_low_percent_format;
- final String percentage = NumberFormat.getPercentInstance().format((double) mBatteryLevel / 100.0);
+ protected void showWarningNotification() {
+ final String percentage = NumberFormat.getPercentInstance()
+ .format((double) mBatteryLevel / 100.0);
+
+ // get standard notification copy
+ String title = mContext.getString(R.string.battery_low_title);
+ String contentText = mContext.getString(R.string.battery_low_percent_format, percentage);
+
+ // override notification copy if hybrid notification enabled
+ if (mEstimate != null) {
+ title = mContext.getString(R.string.battery_low_title_hybrid);
+ contentText = mContext.getString(
+ mEstimate.isBasedOnUsage
+ ? R.string.battery_low_percent_format_hybrid
+ : R.string.battery_low_percent_format_hybrid_short,
+ percentage,
+ getTimeRemainingFormatted());
+ }
final Notification.Builder nb =
new Notification.Builder(mContext, NotificationChannels.BATTERY)
.setSmallIcon(R.drawable.ic_power_low)
// Bump the notification when the bucket dropped.
- .setWhen(mBucketDroppedNegativeTimeMs)
+ .setWhen(mWarningTriggerTimeMs)
.setShowWhen(false)
- .setContentTitle(mContext.getString(R.string.battery_low_title))
- .setContentText(mContext.getString(textRes, percentage))
+ .setContentTitle(title)
+ .setContentText(contentText)
.setOnlyAlertOnce(true)
.setDeleteIntent(pendingBroadcast(ACTION_DISMISSED_WARNING))
- .setVisibility(Notification.VISIBILITY_PUBLIC)
- .setColor(Utils.getColorAttr(mContext, android.R.attr.colorError));
+ .setVisibility(Notification.VISIBILITY_PUBLIC);
if (hasBatterySettings()) {
nb.setContentIntent(pendingBroadcast(ACTION_SHOW_BATTERY_SETTINGS));
}
+ // Make the notification red if the percentage goes below a certain amount or the time
+ // remaining estimate is disabled
+ if (mEstimate == null || mBucket < 0
+ || mEstimate.estimateMillis < mSevereWarningThreshold) {
+ nb.setColor(Utils.getColorAttr(mContext, android.R.attr.colorError));
+ }
nb.addAction(0,
mContext.getString(R.string.battery_saver_start_action),
pendingBroadcast(ACTION_START_SAVER));
@@ -201,6 +239,23 @@ public class PowerNotificationWarnings implements PowerUI.WarningsUI {
mNoMan.notifyAsUser(TAG_BATTERY, SystemMessage.NOTE_POWER_LOW, n, UserHandle.ALL);
}
+ @VisibleForTesting
+ String getTimeRemainingFormatted() {
+ final Locale currentLocale = mContext.getResources().getConfiguration().getLocales().get(0);
+ MeasureFormat frmt = MeasureFormat.getInstance(currentLocale, FormatWidth.NARROW);
+
+ final long remainder = mEstimate.estimateMillis % DateUtils.HOUR_IN_MILLIS;
+ final long hours = TimeUnit.MILLISECONDS.toHours(
+ mEstimate.estimateMillis - remainder);
+ // round down to the nearest 15 min for now to not appear overly precise
+ final long minutes = TimeUnit.MILLISECONDS.toMinutes(
+ remainder - (remainder % TimeUnit.MINUTES.toMillis(15)));
+ final Measure hoursMeasure = new Measure(hours, MeasureUnit.HOUR);
+ final Measure minutesMeasure = new Measure(minutes, MeasureUnit.MINUTE);
+
+ return frmt.formatMeasures(hoursMeasure, minutesMeasure);
+ }
+
private PendingIntent pendingBroadcast(String action) {
return PendingIntent.getBroadcastAsUser(mContext,
0, new Intent(action), 0, UserHandle.CURRENT);
diff --git a/com/android/systemui/power/PowerUI.java b/com/android/systemui/power/PowerUI.java
index c1a36239..b43e99be 100644
--- a/com/android/systemui/power/PowerUI.java
+++ b/com/android/systemui/power/PowerUI.java
@@ -52,6 +52,7 @@ import com.android.systemui.statusbar.phone.StatusBar;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
public class PowerUI extends SystemUI {
static final String TAG = "PowerUI";
@@ -59,6 +60,7 @@ public class PowerUI extends SystemUI {
private static final long TEMPERATURE_INTERVAL = 30 * DateUtils.SECOND_IN_MILLIS;
private static final long TEMPERATURE_LOGGING_INTERVAL = DateUtils.HOUR_IN_MILLIS;
private static final int MAX_RECENT_TEMPS = 125; // TEMPERATURE_LOGGING_INTERVAL plus a buffer
+ static final long THREE_HOURS_IN_MILLIS = DateUtils.HOUR_IN_MILLIS * 3;
private final Handler mHandler = new Handler();
private final Receiver mReceiver = new Receiver();
@@ -68,9 +70,11 @@ public class PowerUI extends SystemUI {
private WarningsUI mWarnings;
private final Configuration mLastConfiguration = new Configuration();
private int mBatteryLevel = 100;
+ private long mTimeRemaining = Long.MAX_VALUE;
private int mBatteryStatus = BatteryManager.BATTERY_STATUS_UNKNOWN;
private int mPlugType = 0;
private int mInvalidCharger = 0;
+ private EnhancedEstimates mEnhancedEstimates;
private int mLowBatteryAlertCloseLevel;
private final int[] mLowBatteryReminderLevels = new int[2];
@@ -83,8 +87,8 @@ public class PowerUI extends SystemUI {
private long mNextLogTime;
private IThermalService mThermalService;
- // We create a method reference here so that we are guaranteed that we can remove a callback
// by using the same instance (method references are not guaranteed to be the same object
+ // We create a method reference here so that we are guaranteed that we can remove a callback
// each time they are created).
private final Runnable mUpdateTempCallback = this::updateTemperatureWarning;
@@ -94,6 +98,7 @@ public class PowerUI extends SystemUI {
mContext.getSystemService(Context.HARDWARE_PROPERTIES_SERVICE);
mScreenOffTime = mPowerManager.isScreenOn() ? -1 : SystemClock.elapsedRealtime();
mWarnings = Dependency.get(WarningsUI.class);
+ mEnhancedEstimates = Dependency.get(EnhancedEstimates.class);
mLastConfiguration.setTo(mContext.getResources().getConfiguration());
ContentObserver obs = new ContentObserver(mHandler) {
@@ -131,10 +136,15 @@ public class PowerUI extends SystemUI {
com.android.internal.R.integer.config_criticalBatteryWarningLevel);
final ContentResolver resolver = mContext.getContentResolver();
- int defWarnLevel = mContext.getResources().getInteger(
+ final int defWarnLevel = mContext.getResources().getInteger(
com.android.internal.R.integer.config_lowBatteryWarningLevel);
- int warnLevel = Settings.Global.getInt(resolver,
+ final int lowPowerModeTriggerLevel = Settings.Global.getInt(resolver,
Settings.Global.LOW_POWER_MODE_TRIGGER_LEVEL, defWarnLevel);
+
+ // NOTE: Keep the logic in sync with BatteryService.
+ // TODO: Propagate this value from BatteryService to system UI, really.
+ int warnLevel = Math.min(defWarnLevel, lowPowerModeTriggerLevel);
+
if (warnLevel == 0) {
warnLevel = defWarnLevel;
}
@@ -231,21 +241,9 @@ public class PowerUI extends SystemUI {
return;
}
- boolean isPowerSaver = mPowerManager.isPowerSaveMode();
- if (!plugged
- && !isPowerSaver
- && (bucket < oldBucket || oldPlugged)
- && mBatteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN
- && bucket < 0) {
-
- // only play SFX when the dialog comes up or the bucket changes
- final boolean playSound = bucket != oldBucket || oldPlugged;
- mWarnings.showLowBatteryWarning(playSound);
- } else if (isPowerSaver || plugged || (bucket > oldBucket && bucket > 0)) {
- mWarnings.dismissLowBatteryWarning();
- } else {
- mWarnings.updateLowBatteryWarning();
- }
+ // Show the correct version of low battery warning if needed
+ maybeShowBatteryWarning(plugged, oldPlugged, oldBucket, bucket);
+
} else if (Intent.ACTION_SCREEN_OFF.equals(action)) {
mScreenOffTime = SystemClock.elapsedRealtime();
} else if (Intent.ACTION_SCREEN_ON.equals(action)) {
@@ -256,7 +254,67 @@ public class PowerUI extends SystemUI {
Slog.w(TAG, "unknown intent: " + intent);
}
}
- };
+ }
+
+ protected void maybeShowBatteryWarning(boolean plugged, boolean oldPlugged, int oldBucket,
+ int bucket) {
+ boolean isPowerSaver = mPowerManager.isPowerSaveMode();
+ // only play SFX when the dialog comes up or the bucket changes
+ final boolean playSound = bucket != oldBucket || oldPlugged;
+ long oldTimeRemaining = mTimeRemaining;
+ if (mEnhancedEstimates.isHybridNotificationEnabled()) {
+ final Estimate estimate = mEnhancedEstimates.getEstimate();
+ // Turbo is not always booted once SysUI is running so we have ot make sure we actually
+ // get data back
+ if (estimate != null) {
+ mTimeRemaining = estimate.estimateMillis;
+ mWarnings.updateEstimate(estimate);
+ mWarnings.updateThresholds(mEnhancedEstimates.getLowWarningThreshold(),
+ mEnhancedEstimates.getSevereWarningThreshold());
+ }
+ }
+
+ if (shouldShowLowBatteryWarning(plugged, oldPlugged, oldBucket, bucket, oldTimeRemaining,
+ mTimeRemaining,
+ isPowerSaver, mBatteryStatus)) {
+ mWarnings.showLowBatteryWarning(playSound);
+ } else if (shouldDismissLowBatteryWarning(plugged, oldBucket, bucket, mTimeRemaining,
+ isPowerSaver)) {
+ mWarnings.dismissLowBatteryWarning();
+ } else {
+ mWarnings.updateLowBatteryWarning();
+ }
+ }
+
+ @VisibleForTesting
+ boolean shouldShowLowBatteryWarning(boolean plugged, boolean oldPlugged, int oldBucket,
+ int bucket, long oldTimeRemaining, long timeRemaining,
+ boolean isPowerSaver, int mBatteryStatus) {
+ return !plugged
+ && !isPowerSaver
+ && (((bucket < oldBucket || oldPlugged) && bucket < 0)
+ || (mEnhancedEstimates.isHybridNotificationEnabled()
+ && timeRemaining < mEnhancedEstimates.getLowWarningThreshold()
+ && isHourLess(oldTimeRemaining, timeRemaining)))
+ && mBatteryStatus != BatteryManager.BATTERY_STATUS_UNKNOWN;
+ }
+
+ private boolean isHourLess(long oldTimeRemaining, long timeRemaining) {
+ final long dif = oldTimeRemaining - timeRemaining;
+ return dif >= TimeUnit.HOURS.toMillis(1);
+ }
+
+ @VisibleForTesting
+ boolean shouldDismissLowBatteryWarning(boolean plugged, int oldBucket, int bucket,
+ long timeRemaining, boolean isPowerSaver) {
+ final boolean hybridWouldDismiss = mEnhancedEstimates.isHybridNotificationEnabled()
+ && timeRemaining > mEnhancedEstimates.getLowWarningThreshold();
+ final boolean standardWouldDismiss = (bucket > oldBucket && bucket > 0);
+ return isPowerSaver
+ || plugged
+ || (standardWouldDismiss && (!mEnhancedEstimates.isHybridNotificationEnabled()
+ || hybridWouldDismiss));
+ }
private void initTemperatureWarning() {
ContentResolver resolver = mContext.getContentResolver();
@@ -428,6 +486,8 @@ public class PowerUI extends SystemUI {
public interface WarningsUI {
void update(int batteryLevel, int bucket, long screenOffTime);
+ void updateEstimate(Estimate estimate);
+ void updateThresholds(long lowThreshold, long severeThreshold);
void dismissLowBatteryWarning();
void showLowBatteryWarning(boolean playSound);
void dismissInvalidChargerWarning();
diff --git a/com/android/systemui/qs/QSContainerImpl.java b/com/android/systemui/qs/QSContainerImpl.java
index 33b5268e..7320b861 100644
--- a/com/android/systemui/qs/QSContainerImpl.java
+++ b/com/android/systemui/qs/QSContainerImpl.java
@@ -17,13 +17,18 @@
package com.android.systemui.qs;
import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Path;
import android.graphics.Point;
import android.util.AttributeSet;
+import android.util.Log;
import android.view.View;
import android.widget.FrameLayout;
+import com.android.settingslib.Utils;
import com.android.systemui.R;
import com.android.systemui.qs.customize.QSCustomizer;
+import com.android.systemui.statusbar.ExpandableOutlineView;
/**
* Wrapper view with background which contains {@link QSPanel} and {@link BaseStatusBarHeader}
@@ -31,6 +36,7 @@ import com.android.systemui.qs.customize.QSCustomizer;
public class QSContainerImpl extends FrameLayout {
private final Point mSizePoint = new Point();
+ private final Path mClipPath = new Path();
private int mHeightOverride = -1;
protected View mQSPanel;
@@ -39,7 +45,9 @@ public class QSContainerImpl extends FrameLayout {
protected float mQsExpansion;
private QSCustomizer mQSCustomizer;
private View mQSFooter;
- private float mFullElevation;
+ private View mBackground;
+ private float mRadius;
+ private int mSideMargins;
public QSContainerImpl(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -53,10 +61,14 @@ public class QSContainerImpl extends FrameLayout {
mHeader = findViewById(R.id.header);
mQSCustomizer = findViewById(R.id.qs_customize);
mQSFooter = findViewById(R.id.qs_footer);
- mFullElevation = mQSPanel.getElevation();
+ mBackground = findViewById(R.id.quick_settings_background);
+ mRadius = getResources().getDimensionPixelSize(
+ Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius));
+ mSideMargins = getResources().getDimensionPixelSize(R.dimen.notification_side_paddings);
setClickable(true);
setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+ setMargins();
}
@Override
@@ -93,6 +105,18 @@ public class QSContainerImpl extends FrameLayout {
updateExpansion();
}
+ @Override
+ protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+ boolean ret;
+ canvas.save();
+ if (child != mQSCustomizer) {
+ canvas.clipPath(mClipPath);
+ }
+ ret = super.drawChild(canvas, child, drawingTime);
+ canvas.restore();
+ return ret;
+ }
+
/**
* Overrides the height of this view (post-layout), so that the content is clipped to that
* height and the background is set to that height.
@@ -110,6 +134,12 @@ public class QSContainerImpl extends FrameLayout {
mQSDetail.setBottom(getTop() + height);
// Pin QS Footer to the bottom of the panel.
mQSFooter.setTranslationY(height - mQSFooter.getHeight());
+ mBackground.setTop(mQSPanel.getTop());
+ mBackground.setBottom(height);
+
+ ExpandableOutlineView.getRoundedRectPath(0, 0, getWidth(), height, mRadius,
+ mRadius,
+ mClipPath);
}
protected int calculateContainerHeight() {
@@ -123,4 +153,19 @@ public class QSContainerImpl extends FrameLayout {
mQsExpansion = expansion;
updateExpansion();
}
+
+ private void setMargins() {
+ setMargins(mQSDetail);
+ setMargins(mBackground);
+ setMargins(mQSFooter);
+ setMargins(mQSPanel);
+ setMargins(mHeader);
+ setMargins(mQSCustomizer);
+ }
+
+ private void setMargins(View view) {
+ FrameLayout.LayoutParams lp = (LayoutParams) view.getLayoutParams();
+ lp.rightMargin = mSideMargins;
+ lp.leftMargin = mSideMargins;
+ }
}
diff --git a/com/android/systemui/qs/QSFooterImpl.java b/com/android/systemui/qs/QSFooterImpl.java
index 927a49cb..92475da6 100644
--- a/com/android/systemui/qs/QSFooterImpl.java
+++ b/com/android/systemui/qs/QSFooterImpl.java
@@ -61,18 +61,16 @@ import com.android.systemui.tuner.TunerService;
public class QSFooterImpl extends FrameLayout implements QSFooter,
OnClickListener, OnUserInfoChangedListener, EmergencyListener,
SignalCallback, CommandQueue.Callbacks {
- private static final float EXPAND_INDICATOR_THRESHOLD = .93f;
-
private ActivityStarter mActivityStarter;
private UserInfoController mUserInfoController;
private SettingsButton mSettingsButton;
protected View mSettingsContainer;
+ private View mCarrierText;
private boolean mQsDisabled;
private QSPanel mQsPanel;
private boolean mExpanded;
- protected ExpandableIndicator mExpandIndicator;
private boolean mListening;
@@ -100,18 +98,18 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
Dependency.get(ActivityStarter.class).postQSRunnableDismissingKeyguard(() ->
mQsPanel.showEdit(view)));
- mExpandIndicator = findViewById(R.id.expand_indicator);
mSettingsButton = findViewById(R.id.settings_button);
mSettingsContainer = findViewById(R.id.settings_button_container);
mSettingsButton.setOnClickListener(this);
+ mCarrierText = findViewById(R.id.qs_carrier_text);
+
mMultiUserSwitch = findViewById(R.id.multi_user_switch);
mMultiUserAvatar = mMultiUserSwitch.findViewById(R.id.multi_user_avatar);
// RenderThread is doing more harm than good when touching the header (to expand quick
// settings), so disable it for this view
((RippleDrawable) mSettingsButton.getBackground()).setForceSoftware(true);
- ((RippleDrawable) mExpandIndicator.getBackground()).setForceSoftware(true);
updateResources();
@@ -162,6 +160,8 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
return new TouchAnimator.Builder()
.addFloat(mEdit, "alpha", 0, 1)
.addFloat(mMultiUserSwitch, "alpha", 0, 1)
+ .addFloat(mCarrierText, "alpha", 0, 1)
+ .addFloat(mSettingsButton, "alpha", 0, 1)
.build();
}
@@ -185,8 +185,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
if (mSettingsAlpha != null) {
mSettingsAlpha.setPosition(headerExpansionFraction);
}
-
- mExpandIndicator.setExpanded(headerExpansionFraction > EXPAND_INDICATOR_THRESHOLD);
}
@Override
@@ -237,8 +235,6 @@ public class QSFooterImpl extends FrameLayout implements QSFooter,
mSettingsContainer.findViewById(R.id.tuner_icon).setVisibility(
TunerService.isTunerEnabled(mContext) ? View.VISIBLE : View.INVISIBLE);
- mExpandIndicator.setVisibility(mQsDisabled ? View.GONE : View.VISIBLE);
-
final boolean isDemo = UserManager.isDeviceInDemoMode(mContext);
diff --git a/com/android/systemui/qs/QuickStatusBarHeader.java b/com/android/systemui/qs/QuickStatusBarHeader.java
index 398592ad..17ede658 100644
--- a/com/android/systemui/qs/QuickStatusBarHeader.java
+++ b/com/android/systemui/qs/QuickStatusBarHeader.java
@@ -18,10 +18,12 @@ import static android.app.StatusBarManager.DISABLE2_QUICK_SETTINGS;
import static android.app.StatusBarManager.DISABLE_NONE;
import android.content.Context;
+import android.content.Intent;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.Rect;
+import android.provider.AlarmClock;
import android.support.annotation.VisibleForTesting;
import android.util.AttributeSet;
import android.view.View;
@@ -37,10 +39,13 @@ import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.plugins.ActivityStarter;
import com.android.systemui.qs.QSDetail.Callback;
import com.android.systemui.statusbar.CommandQueue;
-import com.android.systemui.statusbar.SignalClusterView;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager;
+import com.android.systemui.statusbar.policy.DarkIconDispatcher;
import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
-public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue.Callbacks {
+public class QuickStatusBarHeader extends RelativeLayout
+ implements CommandQueue.Callbacks, View.OnClickListener {
private ActivityStarter mActivityStarter;
@@ -52,6 +57,12 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
protected QuickQSPanel mHeaderQsPanel;
protected QSTileHost mHost;
+ private TintedIconManager mIconManager;
+ private TouchAnimator mAlphaAnimator;
+
+ private View mQuickQsStatusIcons;
+
+ private View mDate;
public QuickStatusBarHeader(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -63,21 +74,30 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
Resources res = getResources();
mHeaderQsPanel = findViewById(R.id.quick_qs_panel);
+ mDate = findViewById(R.id.date);
+ mDate.setOnClickListener(this);
+ mQuickQsStatusIcons = findViewById(R.id.quick_qs_status_icons);
+ mIconManager = new TintedIconManager(findViewById(R.id.statusIcons));
// RenderThread is doing more harm than good when touching the header (to expand quick
// settings), so disable it for this view
updateResources();
- // Set the light/dark theming on the header status UI to match the current theme.
+ Rect tintArea = new Rect(0, 0, 0, 0);
int colorForeground = Utils.getColorAttr(getContext(), android.R.attr.colorForeground);
float intensity = colorForeground == Color.WHITE ? 0 : 1;
- Rect tintArea = new Rect(0, 0, 0, 0);
+ int fillColor = fillColorForIntensity(intensity, getContext());
- applyDarkness(R.id.battery, tintArea, intensity, colorForeground);
- applyDarkness(R.id.clock, tintArea, intensity, colorForeground);
+ // Set light text on the header icons because they will always be on a black background
+ applyDarkness(R.id.clock, tintArea, 0, DarkIconDispatcher.DEFAULT_ICON_TINT);
+ applyDarkness(id.signal_cluster, tintArea, intensity, colorForeground);
+
+ // Set the correct tint for the status icons so they contrast
+ mIconManager.setTint(fillColor);
BatteryMeterView battery = findViewById(R.id.battery);
+ battery.setFillColor(Color.WHITE);
battery.setForceShowPercent(true);
mActivityStarter = Dependency.get(ActivityStarter.class);
@@ -90,6 +110,13 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
}
}
+ private int fillColorForIntensity(float intensity, Context context) {
+ if (intensity == 0) {
+ return context.getColor(R.color.light_mode_icon_color_dual_tone_fill);
+ }
+ return context.getColor(R.color.dark_mode_icon_color_dual_tone_fill);
+ }
+
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
@@ -103,6 +130,13 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
}
private void updateResources() {
+ updateAlphaAnimator();
+ }
+
+ private void updateAlphaAnimator() {
+ mAlphaAnimator = new TouchAnimator.Builder()
+ .addFloat(mQuickQsStatusIcons, "alpha", 1, 0)
+ .build();
}
public int getCollapsedHeight() {
@@ -121,6 +155,9 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
}
public void setExpansion(float headerExpansionFraction) {
+ if (mAlphaAnimator != null ) {
+ mAlphaAnimator.setPosition(headerExpansionFraction);
+ }
}
@Override
@@ -136,6 +173,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
@Override
public void onAttachedToWindow() {
SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).addCallbacks(this);
+ Dependency.get(StatusBarIconController.class).addIconGroup(mIconManager);
}
@Override
@@ -143,6 +181,7 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
public void onDetachedFromWindow() {
setListening(false);
SysUiServiceProvider.getComponent(getContext(), CommandQueue.class).removeCallbacks(this);
+ Dependency.get(StatusBarIconController.class).removeIconGroup(mIconManager);
super.onDetachedFromWindow();
}
@@ -154,6 +193,14 @@ public class QuickStatusBarHeader extends RelativeLayout implements CommandQueue
mListening = listening;
}
+ @Override
+ public void onClick(View v) {
+ if(v == mDate){
+ Dependency.get(ActivityStarter.class).postStartActivityDismissingKeyguard(new Intent(
+ AlarmClock.ACTION_SHOW_ALARMS),0);
+ }
+ }
+
public void updateEverything() {
post(() -> setClickable(false));
}
diff --git a/com/android/systemui/qs/SignalTileView.java b/com/android/systemui/qs/SignalTileView.java
index 9ee40ccf..d9583af6 100644
--- a/com/android/systemui/qs/SignalTileView.java
+++ b/com/android/systemui/qs/SignalTileView.java
@@ -41,6 +41,7 @@ public class SignalTileView extends QSIconViewImpl {
private ImageView mOut;
private int mWideOverlayIconStartPadding;
+ private int mSignalIndicatorToIconFrameSpacing;
public SignalTileView(Context context) {
super(context);
@@ -48,8 +49,13 @@ public class SignalTileView extends QSIconViewImpl {
mIn = addTrafficView(R.drawable.ic_qs_signal_in);
mOut = addTrafficView(R.drawable.ic_qs_signal_out);
+ setClipChildren(false);
+ setClipToPadding(false);
+
mWideOverlayIconStartPadding = context.getResources().getDimensionPixelSize(
R.dimen.wide_type_icon_start_padding_qs);
+ mSignalIndicatorToIconFrameSpacing = context.getResources().getDimensionPixelSize(
+ R.dimen.signal_indicator_to_icon_frame_spacing);
}
private ImageView addTrafficView(int icon) {
@@ -99,10 +105,10 @@ public class SignalTileView extends QSIconViewImpl {
boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
int left, right;
if (isRtl) {
- right = mIconFrame.getLeft();
+ right = getLeft() - mSignalIndicatorToIconFrameSpacing;
left = right - indicator.getMeasuredWidth();
} else {
- left = mIconFrame.getRight();
+ left = getRight() + mSignalIndicatorToIconFrameSpacing;
right = left + indicator.getMeasuredWidth();
}
indicator.layout(
diff --git a/com/android/systemui/qs/car/CarQSFooter.java b/com/android/systemui/qs/car/CarQSFooter.java
index 142aab26..23d3ebbb 100644
--- a/com/android/systemui/qs/car/CarQSFooter.java
+++ b/com/android/systemui/qs/car/CarQSFooter.java
@@ -47,7 +47,7 @@ public class CarQSFooter extends RelativeLayout implements QSFooter,
private MultiUserSwitch mMultiUserSwitch;
private TextView mUserName;
private ImageView mMultiUserAvatar;
- private UserGridView mUserGridView;
+ private CarQSFragment.UserSwitchCallback mUserSwitchCallback;
public CarQSFooter(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -63,15 +63,15 @@ public class CarQSFooter extends RelativeLayout implements QSFooter,
mUserInfoController = Dependency.get(UserInfoController.class);
mMultiUserSwitch.setOnClickListener(v -> {
- if (mUserGridView == null) {
+ if (mUserSwitchCallback == null) {
Log.e(TAG, "CarQSFooter not properly set up; cannot display user switcher.");
return;
}
- if (!mUserGridView.isShowing()) {
- mUserGridView.show();
+ if (!mUserSwitchCallback.isShowing()) {
+ mUserSwitchCallback.show();
} else {
- mUserGridView.hide();
+ mUserSwitchCallback.hide();
}
});
@@ -102,8 +102,8 @@ public class CarQSFooter extends RelativeLayout implements QSFooter,
}
}
- public void setUserGridView(UserGridView view) {
- mUserGridView = view;
+ public void setUserSwitchCallback(CarQSFragment.UserSwitchCallback callback) {
+ mUserSwitchCallback = callback;
}
@Override
diff --git a/com/android/systemui/qs/car/CarQSFragment.java b/com/android/systemui/qs/car/CarQSFragment.java
index 13298d37..0ee6d1fb 100644
--- a/com/android/systemui/qs/car/CarQSFragment.java
+++ b/com/android/systemui/qs/car/CarQSFragment.java
@@ -13,6 +13,12 @@
*/
package com.android.systemui.qs.car;
+import android.animation.Animator;
+import android.animation.AnimatorInflater;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
import android.app.Fragment;
import android.os.Bundle;
import android.support.annotation.Nullable;
@@ -26,18 +32,29 @@ import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.plugins.qs.QS;
import com.android.systemui.qs.QSFooter;
+import com.android.systemui.statusbar.car.PageIndicator;
import com.android.systemui.statusbar.car.UserGridView;
import com.android.systemui.statusbar.policy.UserSwitcherController;
+import java.util.ArrayList;
+import java.util.List;
+
/**
* A quick settings fragment for the car. For auto, there is no row for quick settings or ability
* to expand the quick settings panel. Instead, the only thing is that displayed is the
* status bar, and a static row with access to the user switcher and settings.
*/
public class CarQSFragment extends Fragment implements QS {
+ private ViewGroup mPanel;
private View mHeader;
+ private View mUserSwitcherContainer;
private CarQSFooter mFooter;
+ private View mFooterUserName;
+ private View mFooterExpandIcon;
private UserGridView mUserGridView;
+ private PageIndicator mPageIndicator;
+ private AnimatorSet mAnimatorSet;
+ private UserSwitchCallback mUserSwitchCallback;
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@@ -48,14 +65,26 @@ public class CarQSFragment extends Fragment implements QS {
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
+ mPanel = (ViewGroup) view;
mHeader = view.findViewById(R.id.header);
mFooter = view.findViewById(R.id.qs_footer);
+ mFooterUserName = mFooter.findViewById(R.id.user_name);
+ mFooterExpandIcon = mFooter.findViewById(R.id.user_switch_expand_icon);
+
+ mUserSwitcherContainer = view.findViewById(R.id.user_switcher_container);
+
+ updateUserSwitcherHeight(0);
mUserGridView = view.findViewById(R.id.user_grid);
mUserGridView.init(null, Dependency.get(UserSwitcherController.class),
- false /* showInitially */);
+ false /* overrideAlpha */);
- mFooter.setUserGridView(mUserGridView);
+ mPageIndicator = view.findViewById(R.id.user_switcher_page_indicator);
+ mPageIndicator.setupWithViewPager(mUserGridView);
+
+ mUserSwitchCallback = new UserSwitchCallback();
+ mFooter.setUserSwitchCallback(mUserSwitchCallback);
+ mUserGridView.setUserSwitchCallback(mUserSwitchCallback);
}
@Override
@@ -82,11 +111,13 @@ public class CarQSFragment extends Fragment implements QS {
@Override
public void setHeaderListening(boolean listening) {
mFooter.setListening(listening);
+ mUserGridView.setListening(listening);
}
@Override
public void setListening(boolean listening) {
mFooter.setListening(listening);
+ mUserGridView.setListening(listening);
}
@Override
@@ -171,4 +202,126 @@ public class CarQSFragment extends Fragment implements QS {
public void setExpandClickListener(OnClickListener onClickListener) {
// No ability to expand the quick settings.
}
+
+ public class UserSwitchCallback {
+ private boolean mShowing;
+
+ public boolean isShowing() {
+ return mShowing;
+ }
+
+ public void show() {
+ mShowing = true;
+ animateHeightChange(true /* opening */);
+ }
+
+ public void hide() {
+ mShowing = false;
+ animateHeightChange(false /* opening */);
+ }
+
+ public void resetShowing() {
+ if (mShowing) {
+ for (int i = 0; i < mUserGridView.getChildCount(); i++) {
+ ViewGroup podContainer = (ViewGroup) mUserGridView.getChildAt(i);
+ // Need to bring the last child to the front to maintain the order in the pod
+ // container. Why? ¯\_(ツ)_/¯
+ if (podContainer.getChildCount() > 0) {
+ podContainer.getChildAt(podContainer.getChildCount() - 1).bringToFront();
+ }
+ // The alpha values are default to 0, so if the pods have been refreshed, they
+ // need to be set to 1 when showing.
+ for (int j = 0; j < podContainer.getChildCount(); j++) {
+ podContainer.getChildAt(j).setAlpha(1f);
+ }
+ }
+ }
+ }
+ }
+
+ private void updateUserSwitcherHeight(int height) {
+ ViewGroup.LayoutParams layoutParams = mUserSwitcherContainer.getLayoutParams();
+ layoutParams.height = height;
+ mUserSwitcherContainer.requestLayout();
+ }
+
+ private void animateHeightChange(boolean opening) {
+ // Animation in progress; cancel it to avoid contention.
+ if (mAnimatorSet != null){
+ mAnimatorSet.cancel();
+ }
+
+ List<Animator> allAnimators = new ArrayList<>();
+ ValueAnimator heightAnimator = (ValueAnimator) AnimatorInflater.loadAnimator(getContext(),
+ opening ? R.anim.car_user_switcher_open_animation
+ : R.anim.car_user_switcher_close_animation);
+ heightAnimator.addUpdateListener(valueAnimator -> {
+ updateUserSwitcherHeight((Integer) valueAnimator.getAnimatedValue());
+ });
+ allAnimators.add(heightAnimator);
+
+ // The user grid contains pod containers that each contain a number of pods. Animate
+ // all pods to avoid any discrepancy/race conditions with possible changes during the
+ // animation.
+ int cascadeDelay = getResources().getInteger(
+ R.integer.car_user_switcher_anim_cascade_delay_ms);
+ for (int i = 0; i < mUserGridView.getChildCount(); i++) {
+ ViewGroup podContainer = (ViewGroup) mUserGridView.getChildAt(i);
+ for (int j = 0; j < podContainer.getChildCount(); j++) {
+ View pod = podContainer.getChildAt(j);
+ Animator podAnimator = AnimatorInflater.loadAnimator(getContext(),
+ opening ? R.anim.car_user_switcher_open_pod_animation
+ : R.anim.car_user_switcher_close_pod_animation);
+ // Add the cascading delay between pods
+ if (opening) {
+ podAnimator.setStartDelay(podAnimator.getStartDelay() + j * cascadeDelay);
+ }
+ podAnimator.setTarget(pod);
+ allAnimators.add(podAnimator);
+ }
+ }
+
+ Animator nameAnimator = AnimatorInflater.loadAnimator(getContext(),
+ opening ? R.anim.car_user_switcher_open_name_animation
+ : R.anim.car_user_switcher_close_name_animation);
+ nameAnimator.setTarget(mFooterUserName);
+ allAnimators.add(nameAnimator);
+
+ Animator iconAnimator = AnimatorInflater.loadAnimator(getContext(),
+ opening ? R.anim.car_user_switcher_open_icon_animation
+ : R.anim.car_user_switcher_close_icon_animation);
+ iconAnimator.setTarget(mFooterExpandIcon);
+ allAnimators.add(iconAnimator);
+
+ Animator pageAnimator = AnimatorInflater.loadAnimator(getContext(),
+ opening ? R.anim.car_user_switcher_open_pages_animation
+ : R.anim.car_user_switcher_close_pages_animation);
+ pageAnimator.setTarget(mPageIndicator);
+ allAnimators.add(pageAnimator);
+
+ mAnimatorSet = new AnimatorSet();
+ mAnimatorSet.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimatorSet = null;
+ }
+ });
+ mAnimatorSet.playTogether(allAnimators.toArray(new Animator[0]));
+
+ // Setup all values to the start values in the animations, since there are delays, but need
+ // to have all values start at the beginning.
+ setupInitialValues(mAnimatorSet);
+
+ mAnimatorSet.start();
+ }
+
+ private void setupInitialValues(Animator anim) {
+ if (anim instanceof AnimatorSet) {
+ for (Animator a : ((AnimatorSet) anim).getChildAnimations()) {
+ setupInitialValues(a);
+ }
+ } else if (anim instanceof ObjectAnimator) {
+ ((ObjectAnimator) anim).setCurrentFraction(0.0f);
+ }
+ }
}
diff --git a/com/android/systemui/qs/tileimpl/QSIconViewImpl.java b/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
index c249e377..0f83078e 100644
--- a/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
+++ b/com/android/systemui/qs/tileimpl/QSIconViewImpl.java
@@ -103,7 +103,7 @@ public class QSIconViewImpl extends QSIconView {
if (iv instanceof SlashImageView) {
((SlashImageView) iv).setAnimationEnabled(shouldAnimate);
- ((SlashImageView) iv).setState(state.slash, d);
+ ((SlashImageView) iv).setState(null, d);
} else {
iv.setImageDrawable(d);
}
diff --git a/com/android/systemui/qs/tileimpl/QSTileBaseView.java b/com/android/systemui/qs/tileimpl/QSTileBaseView.java
index 4d0e60d5..b4cfda60 100644
--- a/com/android/systemui/qs/tileimpl/QSTileBaseView.java
+++ b/com/android/systemui/qs/tileimpl/QSTileBaseView.java
@@ -13,7 +13,12 @@
*/
package com.android.systemui.qs.tileimpl;
+import static com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH;
+
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
import android.content.Context;
+import android.content.res.ColorStateList;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.RippleDrawable;
@@ -22,16 +27,21 @@ import android.os.Looper;
import android.os.Message;
import android.service.quicksettings.Tile;
import android.text.TextUtils;
+import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ImageView.ScaleType;
import android.widget.Switch;
+import com.android.settingslib.Utils;
import com.android.systemui.R;
-import com.android.systemui.plugins.qs.*;
+import com.android.systemui.plugins.qs.QSIconView;
+import com.android.systemui.plugins.qs.QSTile;
import com.android.systemui.plugins.qs.QSTile.BooleanState;
public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView {
@@ -47,6 +57,12 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView {
private boolean mCollapsedView;
private boolean mClicked;
+ private final ImageView mBg;
+ private final int mColorActive;
+ private final int mColorInactive;
+ private final int mColorDisabled;
+ private int mCircleColor;
+
public QSTileBaseView(Context context, QSIconView icon) {
this(context, icon, false);
}
@@ -60,11 +76,17 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView {
mIconFrame.setForegroundGravity(Gravity.CENTER);
int size = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size);
addView(mIconFrame, new LayoutParams(size, size));
+ mBg = new ImageView(getContext());
+ mBg.setScaleType(ScaleType.FIT_CENTER);
+ mBg.setImageResource(R.drawable.ic_qs_circle);
+ mIconFrame.addView(mBg);
mIcon = icon;
FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
params.setMargins(0, padding, 0, padding);
mIconFrame.addView(mIcon, params);
+ mIconFrame.setClipChildren(false);
+ mIconFrame.setClipToPadding(false);
mTileBackground = newTileBackground();
if (mTileBackground instanceof RippleDrawable) {
@@ -73,6 +95,11 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView {
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
setBackground(mTileBackground);
+ mColorActive = Utils.getColorAttr(context, android.R.attr.colorAccent);
+ mColorDisabled = Utils.getDisabled(context,
+ Utils.getColorAttr(context, android.R.attr.textColorTertiary));
+ mColorInactive = Utils.getColorAttr(context, android.R.attr.textColorSecondary);
+
setPadding(0, 0, 0, 0);
setClipChildren(false);
setClipToPadding(false);
@@ -80,6 +107,10 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView {
setFocusable(true);
}
+ public View getBgCicle() {
+ return mBg;
+ }
+
protected Drawable newTileBackground() {
final int[] attrs = new int[]{android.R.attr.selectableItemBackgroundBorderless};
final TypedArray ta = getContext().obtainStyledAttributes(attrs);
@@ -150,6 +181,20 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView {
}
protected void handleStateChanged(QSTile.State state) {
+ int circleColor = getCircleColor(state.state);
+ if (circleColor != mCircleColor) {
+ if (mBg.isShown()) {
+ ValueAnimator animator = ValueAnimator.ofArgb(mCircleColor, circleColor)
+ .setDuration(QS_ANIM_LENGTH);
+ animator.addUpdateListener(animation -> mBg.setImageTintList(ColorStateList.valueOf(
+ (Integer) animation.getAnimatedValue())));
+ animator.start();
+ } else {
+ QSIconViewImpl.setTint(mBg, circleColor);
+ }
+ mCircleColor = circleColor;
+ }
+
setClickable(state.state != Tile.STATE_UNAVAILABLE);
mIcon.setIcon(state);
setContentDescription(state.contentDescription);
@@ -163,6 +208,19 @@ public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView {
}
}
+ private int getCircleColor(int state) {
+ switch (state) {
+ case Tile.STATE_ACTIVE:
+ return mColorActive;
+ case Tile.STATE_INACTIVE:
+ case Tile.STATE_UNAVAILABLE:
+ return mColorDisabled;
+ default:
+ Log.e(TAG, "Invalid state " + state);
+ return 0;
+ }
+ }
+
@Override
public void setClickable(boolean clickable) {
super.setClickable(clickable);
diff --git a/com/android/systemui/qs/tileimpl/QSTileImpl.java b/com/android/systemui/qs/tileimpl/QSTileImpl.java
index 576a4474..72592829 100644
--- a/com/android/systemui/qs/tileimpl/QSTileImpl.java
+++ b/com/android/systemui/qs/tileimpl/QSTileImpl.java
@@ -373,11 +373,11 @@ public abstract class QSTileImpl<TState extends State> implements QSTile {
switch (state) {
case Tile.STATE_UNAVAILABLE:
return Utils.getDisabled(context,
- Utils.getColorAttr(context, android.R.attr.colorForeground));
+ Utils.getColorAttr(context, android.R.attr.textColorSecondary));
case Tile.STATE_INACTIVE:
- return Utils.getColorAttr(context, android.R.attr.textColorHint);
+ return Utils.getColorAttr(context, android.R.attr.textColorSecondary);
case Tile.STATE_ACTIVE:
- return Utils.getColorAttr(context, android.R.attr.textColorPrimary);
+ return Utils.getColorAttr(context, android.R.attr.colorPrimary);
default:
Log.e("QSTile", "Invalid state " + state);
return 0;
diff --git a/com/android/systemui/qs/tileimpl/QSTileView.java b/com/android/systemui/qs/tileimpl/QSTileView.java
index 263dac0f..9eb9906b 100644
--- a/com/android/systemui/qs/tileimpl/QSTileView.java
+++ b/com/android/systemui/qs/tileimpl/QSTileView.java
@@ -18,6 +18,7 @@ import android.content.Context;
import android.content.res.Configuration;
import android.service.quicksettings.Tile;
import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.view.Gravity;
import android.view.LayoutInflater;
@@ -36,8 +37,10 @@ import libcore.util.Objects;
/** View that represents a standard quick settings tile. **/
public class QSTileView extends QSTileBaseView {
+ private static final boolean DUAL_TARGET_ALLOWED = false;
private View mDivider;
protected TextView mLabel;
+ private TextView mSecondLine;
private ImageView mPadLock;
private int mState;
private ViewGroup mLabelContainer;
@@ -86,6 +89,8 @@ public class QSTileView extends QSTileBaseView {
mDivider = mLabelContainer.findViewById(R.id.underline);
mExpandIndicator = mLabelContainer.findViewById(R.id.expand_indicator);
mExpandSpace = mLabelContainer.findViewById(R.id.expand_space);
+ mSecondLine = mLabelContainer.findViewById(R.id.app_label);
+ mSecondLine.setAlpha(.6f);
addView(mLabelContainer);
}
@@ -103,14 +108,20 @@ public class QSTileView extends QSTileBaseView {
mState = state.state;
mLabel.setText(state.label);
}
- mExpandIndicator.setVisibility(state.dualTarget ? View.VISIBLE : View.GONE);
- mExpandSpace.setVisibility(state.dualTarget ? View.VISIBLE : View.GONE);
- mLabelContainer.setContentDescription(state.dualTarget ? state.dualLabelContentDescription
+ if (!Objects.equal(mSecondLine.getText(), state.secondaryLabel)) {
+ mSecondLine.setText(state.secondaryLabel);
+ mSecondLine.setVisibility(TextUtils.isEmpty(state.secondaryLabel) ? View.GONE
+ : View.VISIBLE);
+ }
+ boolean dualTarget = DUAL_TARGET_ALLOWED && state.dualTarget;
+ mExpandIndicator.setVisibility(dualTarget ? View.VISIBLE : View.GONE);
+ mExpandSpace.setVisibility(dualTarget ? View.VISIBLE : View.GONE);
+ mLabelContainer.setContentDescription(dualTarget ? state.dualLabelContentDescription
: null);
- if (state.dualTarget != mLabelContainer.isClickable()) {
- mLabelContainer.setClickable(state.dualTarget);
- mLabelContainer.setLongClickable(state.dualTarget);
- mLabelContainer.setBackground(state.dualTarget ? newTileBackground() : null);
+ if (dualTarget != mLabelContainer.isClickable()) {
+ mLabelContainer.setClickable(dualTarget);
+ mLabelContainer.setLongClickable(dualTarget);
+ mLabelContainer.setBackground(dualTarget ? newTileBackground() : null);
}
mLabel.setEnabled(!state.disabledByPolicy);
mPadLock.setVisibility(state.disabledByPolicy ? View.VISIBLE : View.GONE);
diff --git a/com/android/systemui/qs/tiles/AirplaneModeTile.java b/com/android/systemui/qs/tiles/AirplaneModeTile.java
index bef1aff5..9883da6f 100644
--- a/com/android/systemui/qs/tiles/AirplaneModeTile.java
+++ b/com/android/systemui/qs/tiles/AirplaneModeTile.java
@@ -21,6 +21,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
+import android.os.UserManager;
import android.provider.Settings;
import android.provider.Settings.Global;
import android.service.quicksettings.Tile;
@@ -82,6 +83,7 @@ public class AirplaneModeTile extends QSTileImpl<BooleanState> {
@Override
protected void handleUpdateState(BooleanState state, Object arg) {
+ checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_AIRPLANE_MODE);
final int value = arg instanceof Integer ? (Integer)arg : mSetting.getValue();
final boolean airplaneMode = value != 0;
state.value = airplaneMode;
diff --git a/com/android/systemui/qs/tiles/BluetoothTile.java b/com/android/systemui/qs/tiles/BluetoothTile.java
index 0e4a9fe3..2607ebbb 100644
--- a/com/android/systemui/qs/tiles/BluetoothTile.java
+++ b/com/android/systemui/qs/tiles/BluetoothTile.java
@@ -18,7 +18,9 @@ package com.android.systemui.qs.tiles;
import static com.android.settingslib.graph.BluetoothDeviceLayerDrawable.createLayerDrawable;
+import android.annotation.Nullable;
import android.bluetooth.BluetoothAdapter;
+import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.Context;
@@ -26,7 +28,6 @@ import android.content.Intent;
import android.graphics.drawable.Drawable;
import android.provider.Settings;
import android.service.quicksettings.Tile;
-import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Switch;
@@ -35,6 +36,7 @@ import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settingslib.Utils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.graph.BluetoothDeviceLayerDrawable;
import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.plugins.ActivityStarter;
@@ -125,50 +127,91 @@ public class BluetoothTile extends QSTileImpl<BooleanState> {
state.slash = new SlashState();
}
state.slash.isSlashed = !enabled;
+ state.label = mContext.getString(R.string.quick_settings_bluetooth_label);
+
if (enabled) {
- state.label = null;
if (connected) {
state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_connected);
- state.label = mController.getLastDeviceName();
- CachedBluetoothDevice lastDevice = mController.getLastDevice();
+ state.contentDescription = mContext.getString(
+ R.string.accessibility_bluetooth_name, state.label);
+
+ final CachedBluetoothDevice lastDevice = mController.getLastDevice();
if (lastDevice != null) {
- int batteryLevel = lastDevice.getBatteryLevel();
+ final int batteryLevel = lastDevice.getBatteryLevel();
if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
- state.icon = new BluetoothBatteryTileIcon(lastDevice,
+ state.icon = new BluetoothBatteryTileIcon(
+ batteryLevel,
mContext.getResources().getFraction(
R.fraction.bt_battery_scale_fraction, 1, 1));
}
}
- state.contentDescription = mContext.getString(
- R.string.accessibility_bluetooth_name, state.label);
+
+ state.label = mController.getLastDeviceName();
} else if (state.isTransient) {
state.icon = ResourceIcon.get(R.drawable.ic_bluetooth_transient_animation);
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_bluetooth_connecting);
- state.label = mContext.getString(R.string.quick_settings_bluetooth_label);
} else {
state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_on);
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_bluetooth_on) + ","
+ mContext.getString(R.string.accessibility_not_connected);
}
- if (TextUtils.isEmpty(state.label)) {
- state.label = mContext.getString(R.string.quick_settings_bluetooth_label);
- }
state.state = Tile.STATE_ACTIVE;
} else {
state.icon = ResourceIcon.get(R.drawable.ic_qs_bluetooth_on);
- state.label = mContext.getString(R.string.quick_settings_bluetooth_label);
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_bluetooth_off);
state.state = Tile.STATE_INACTIVE;
}
+ state.secondaryLabel = getSecondaryLabel(enabled, connected);
+
state.dualLabelContentDescription = mContext.getResources().getString(
R.string.accessibility_quick_settings_open_settings, getTileLabel());
state.expandedAccessibilityClassName = Switch.class.getName();
}
+ /**
+ * Returns the secondary label to use for the given bluetooth connection in the form of the
+ * battery level or bluetooth profile name. If the bluetooth is disabled, there's no connected
+ * devices, or we can't map the bluetooth class to a profile, this instead returns {@code null}.
+ *
+ * @param enabled whether bluetooth is enabled
+ * @param connected whether there's a device connected via bluetooth
+ */
+ @Nullable
+ private String getSecondaryLabel(boolean enabled, boolean connected) {
+ final CachedBluetoothDevice lastDevice = mController.getLastDevice();
+
+ if (enabled && connected && lastDevice != null) {
+ final int batteryLevel = lastDevice.getBatteryLevel();
+
+ if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
+ return mContext.getString(
+ R.string.quick_settings_bluetooth_secondary_label_battery_level,
+ Utils.formatPercentage(batteryLevel));
+
+ } else {
+ final BluetoothClass bluetoothClass = lastDevice.getBtClass();
+ if (bluetoothClass != null) {
+ if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_A2DP)) {
+ return mContext.getString(
+ R.string.quick_settings_bluetooth_secondary_label_audio);
+ } else if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_HEADSET)) {
+ return mContext.getString(
+ R.string.quick_settings_bluetooth_secondary_label_headset);
+ } else if (bluetoothClass.doesClassMatch(BluetoothClass.PROFILE_HID)) {
+ return mContext.getString(
+ R.string.quick_settings_bluetooth_secondary_label_input);
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
@Override
public int getMetricsCategory() {
return MetricsEvent.QS_BLUETOOTH;
@@ -212,20 +255,29 @@ public class BluetoothTile extends QSTileImpl<BooleanState> {
return new BluetoothDetailAdapter();
}
+ /**
+ * Bluetooth icon wrapper for Quick Settings with a battery indicator that reflects the
+ * connected device's battery level. This is used instead of
+ * {@link com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon} in order to use a context
+ * that reflects dark/light theme attributes.
+ */
private class BluetoothBatteryTileIcon extends Icon {
+ private int mBatteryLevel;
private float mIconScale;
- private CachedBluetoothDevice mDevice;
- BluetoothBatteryTileIcon(CachedBluetoothDevice device, float iconScale) {
+ BluetoothBatteryTileIcon(int batteryLevel, float iconScale) {
+ mBatteryLevel = batteryLevel;
mIconScale = iconScale;
- mDevice = device;
}
@Override
public Drawable getDrawable(Context context) {
// This method returns Pair<Drawable, String> while first value is the drawable
- return com.android.settingslib.bluetooth.Utils.getBtClassDrawableWithDescription(
- context, mDevice, mIconScale).first;
+ return BluetoothDeviceLayerDrawable.createLayerDrawable(
+ context,
+ R.drawable.ic_qs_bluetooth_connected,
+ mBatteryLevel,
+ mIconScale);
}
}
@@ -307,8 +359,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> {
item.iconResId = R.drawable.ic_qs_bluetooth_connected;
int batteryLevel = device.getBatteryLevel();
if (batteryLevel != BluetoothDevice.BATTERY_LEVEL_UNKNOWN) {
- item.icon = new BluetoothBatteryTileIcon(device,
- 1 /* iconScale */);
+ item.icon = new BluetoothBatteryTileIcon(batteryLevel,1 /* iconScale */);
item.line2 = mContext.getString(
R.string.quick_settings_connected_battery_level,
Utils.formatPercentage(batteryLevel));
diff --git a/com/android/systemui/qs/tiles/DndTile.java b/com/android/systemui/qs/tiles/DndTile.java
index 9e265e22..bba847c9 100644
--- a/com/android/systemui/qs/tiles/DndTile.java
+++ b/com/android/systemui/qs/tiles/DndTile.java
@@ -19,6 +19,8 @@ package com.android.systemui.qs.tiles;
import static android.provider.Settings.Global.ZEN_MODE_ALARMS;
import static android.provider.Settings.Global.ZEN_MODE_OFF;
+import android.app.AlarmManager;
+import android.app.AlarmManager.AlarmClockInfo;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -27,13 +29,14 @@ import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.net.Uri;
import android.os.UserManager;
import android.provider.Settings;
import android.provider.Settings.Global;
+import android.service.notification.ScheduleCalendar;
import android.service.notification.ZenModeConfig;
import android.service.notification.ZenModeConfig.ZenRule;
import android.service.quicksettings.Tile;
-import android.util.Log;
import android.util.Slog;
import android.view.LayoutInflater;
import android.view.View;
@@ -55,7 +58,6 @@ import com.android.systemui.plugins.qs.QSTile.BooleanState;
import com.android.systemui.qs.QSHost;
import com.android.systemui.qs.tileimpl.QSTileImpl;
import com.android.systemui.statusbar.policy.ZenModeController;
-import com.android.systemui.statusbar.policy.ZenModeController.Callback;
import com.android.systemui.volume.ZenModePanel;
/** Quick settings tile: Do not disturb **/
@@ -134,8 +136,7 @@ public class DndTile extends QSTileImpl<BooleanState> {
if (mState.value) {
mController.setZen(ZEN_MODE_OFF, null, TAG);
} else {
- int zen = Prefs.getInt(mContext, Prefs.Key.DND_FAVORITE_ZEN, Global.ZEN_MODE_ALARMS);
- mController.setZen(zen, null, TAG);
+ mController.setZen(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG);
}
}
@@ -159,9 +160,7 @@ public class DndTile extends QSTileImpl<BooleanState> {
showDetail(true);
}
});
- int zen = Prefs.getInt(mContext, Prefs.Key.DND_FAVORITE_ZEN,
- Global.ZEN_MODE_ALARMS);
- mController.setZen(zen, null, TAG);
+ mController.setZen(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG);
} else {
showDetail(true);
}
@@ -182,29 +181,27 @@ public class DndTile extends QSTileImpl<BooleanState> {
state.value = newValue;
state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
state.slash.isSlashed = !state.value;
+ state.label = getTileLabel();
+ state.secondaryLabel = getSecondaryLabel(zen != Global.ZEN_MODE_OFF);
checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_ADJUST_VOLUME);
switch (zen) {
case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on);
- state.label = mContext.getString(R.string.quick_settings_dnd_priority_label);
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_dnd_priority_on);
break;
case Global.ZEN_MODE_NO_INTERRUPTIONS:
state.icon = TOTAL_SILENCE;
- state.label = mContext.getString(R.string.quick_settings_dnd_none_label);
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_dnd_none_on);
break;
case ZEN_MODE_ALARMS:
state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on);
- state.label = mContext.getString(R.string.quick_settings_dnd_alarms_label);
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_dnd_alarms_on);
break;
default:
state.icon = ResourceIcon.get(R.drawable.ic_qs_dnd_on);
- state.label = mContext.getString(R.string.quick_settings_dnd_label);
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_dnd);
break;
@@ -217,6 +214,102 @@ public class DndTile extends QSTileImpl<BooleanState> {
state.expandedAccessibilityClassName = Switch.class.getName();
}
+ /**
+ * Returns the secondary label to use for the given instance of do not disturb.
+ * - If turned on manually and end time is known, returns end time.
+ * - If turned on by an automatic rule, returns the automatic rule name.
+ * - If on due to an app, returns the app name.
+ * - If there's a combination of rules/apps that trigger, then shows the one that will
+ * last the longest if applicable.
+ * @return null if do not disturb is off.
+ */
+ private String getSecondaryLabel(boolean zenOn) {
+ if (!zenOn) {
+ return null;
+ }
+
+ ZenModeConfig config = mController.getConfig();
+ String secondaryText = "";
+ long latestEndTime = -1;
+
+ // DND turned on by manual rule
+ if (config.manualRule != null) {
+ final Uri id = config.manualRule.conditionId;
+ if (config.manualRule.enabler != null) {
+ // app triggered manual rule
+ String appName = ZenModeConfig.getOwnerCaption(mContext, config.manualRule.enabler);
+ if (!appName.isEmpty()) {
+ secondaryText = appName;
+ }
+ } else {
+ if (id == null) {
+ // Do not disturb manually triggered to remain on forever until turned off
+ // No subtext
+ return null;
+ } else {
+ latestEndTime = ZenModeConfig.tryParseCountdownConditionId(id);
+ if (latestEndTime > 0) {
+ final CharSequence formattedTime = ZenModeConfig.getFormattedTime(mContext,
+ latestEndTime, ZenModeConfig.isToday(latestEndTime),
+ mContext.getUserId());
+ secondaryText = mContext.getString(R.string.qs_dnd_until, formattedTime);
+ }
+ }
+ }
+ }
+
+ // DND turned on by an automatic rule
+ for (ZenModeConfig.ZenRule automaticRule : config.automaticRules.values()) {
+ if (automaticRule.isAutomaticActive()) {
+ if (ZenModeConfig.isValidEventConditionId(automaticRule.conditionId) ||
+ ZenModeConfig.isValidScheduleConditionId(automaticRule.conditionId)) {
+ // set text if automatic rule end time is the latest active rule end time
+ long endTime = parseAutomaticRuleEndTime(automaticRule.conditionId);
+ if (endTime > latestEndTime) {
+ latestEndTime = endTime;
+ secondaryText = automaticRule.name;
+ }
+ } else {
+ // set text if 3rd party rule
+ return automaticRule.name;
+ }
+ }
+ }
+
+ return !secondaryText.equals("") ? secondaryText : null;
+ }
+
+ private long parseAutomaticRuleEndTime(Uri id) {
+ if (ZenModeConfig.isValidEventConditionId(id)) {
+ // cannot look up end times for events
+ return Long.MAX_VALUE;
+ }
+
+ if (ZenModeConfig.isValidScheduleConditionId(id)) {
+ ScheduleCalendar schedule = ZenModeConfig.toScheduleCalendar(id);
+ long endTimeMs = schedule.getNextChangeTime(System.currentTimeMillis());
+
+ // check if automatic rule will end on next alarm
+ if (schedule.exitAtAlarm()) {
+ long nextAlarm = getNextAlarm(mContext);
+ schedule.maybeSetNextAlarm(System.currentTimeMillis(), nextAlarm);
+ if (schedule.shouldExitForAlarm(endTimeMs)) {
+ return nextAlarm;
+ }
+ }
+
+ return endTimeMs;
+ }
+
+ return -1;
+ }
+
+ private long getNextAlarm(Context context) {
+ final AlarmManager alarms = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
+ final AlarmClockInfo info = alarms.getNextAlarmClock(mContext.getUserId());
+ return info != null ? info.getTriggerTime() : 0;
+ }
+
@Override
public int getMetricsCategory() {
return MetricsEvent.QS_DND;
@@ -313,9 +406,7 @@ public class DndTile extends QSTileImpl<BooleanState> {
mController.setZen(ZEN_MODE_OFF, null, TAG);
mAuto = false;
} else {
- int zen = Prefs.getInt(mContext, Prefs.Key.DND_FAVORITE_ZEN,
- ZEN_MODE_ALARMS);
- mController.setZen(zen, null, TAG);
+ mController.setZen(Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, TAG);
}
}
diff --git a/com/android/systemui/qs/tiles/HotspotTile.java b/com/android/systemui/qs/tiles/HotspotTile.java
index 910b6b17..e1b58fe4 100644
--- a/com/android/systemui/qs/tiles/HotspotTile.java
+++ b/com/android/systemui/qs/tiles/HotspotTile.java
@@ -16,6 +16,7 @@
package com.android.systemui.qs.tiles;
+import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
@@ -37,7 +38,7 @@ import com.android.systemui.statusbar.policy.HotspotController;
/** Quick settings tile: Hotspot **/
public class HotspotTile extends QSTileImpl<AirplaneBooleanState> {
static final Intent TETHER_SETTINGS = new Intent().setComponent(new ComponentName(
- "com.android.settings", "com.android.settings.TetherSettings"));
+ "com.android.settings", "com.android.settings.TetherSettings"));
private final Icon mEnabledStatic = ResourceIcon.get(R.drawable.ic_hotspot);
private final Icon mUnavailable = ResourceIcon.get(R.drawable.ic_hotspot_unavailable);
@@ -115,11 +116,19 @@ public class HotspotTile extends QSTileImpl<AirplaneBooleanState> {
state.label = mContext.getString(R.string.quick_settings_hotspot_label);
checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_CONFIG_TETHERING);
- if (arg instanceof Boolean) {
- state.value = (boolean) arg;
+
+ final int numConnectedDevices;
+ if (arg instanceof CallbackInfo) {
+ CallbackInfo info = (CallbackInfo) arg;
+ state.value = info.enabled;
+ numConnectedDevices = info.numConnectedDevices;
} else {
state.value = mController.isHotspotEnabled();
+ numConnectedDevices = mController.getNumConnectedDevices();
}
+
+ state.secondaryLabel = getSecondaryLabel(state.value, numConnectedDevices);
+
state.icon = mEnabledStatic;
state.isAirplaneMode = mAirplaneMode.getValue() != 0;
state.isTransient = mController.isHotspotTransient();
@@ -133,6 +142,18 @@ public class HotspotTile extends QSTileImpl<AirplaneBooleanState> {
: state.value || state.isTransient ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
}
+ @Nullable
+ private String getSecondaryLabel(boolean enabled, int numConnectedDevices) {
+ if (numConnectedDevices > 0 && enabled) {
+ return mContext.getResources().getQuantityString(
+ R.plurals.quick_settings_hotspot_num_devices,
+ numConnectedDevices,
+ numConnectedDevices);
+ }
+
+ return null;
+ }
+
@Override
public int getMetricsCategory() {
return MetricsEvent.QS_HOTSPOT;
@@ -148,9 +169,30 @@ public class HotspotTile extends QSTileImpl<AirplaneBooleanState> {
}
private final class Callback implements HotspotController.Callback {
+ final CallbackInfo mCallbackInfo = new CallbackInfo();
+
@Override
- public void onHotspotChanged(boolean enabled) {
- refreshState(enabled);
+ public void onHotspotChanged(boolean enabled, int numConnectedDevices) {
+ mCallbackInfo.enabled = enabled;
+ mCallbackInfo.numConnectedDevices = numConnectedDevices;
+ refreshState(mCallbackInfo);
}
- };
+ }
+
+ /**
+ * Holder for any hotspot state info that needs to passed from the callback to
+ * {@link #handleUpdateState(State, Object)}.
+ */
+ protected static final class CallbackInfo {
+ boolean enabled;
+ int numConnectedDevices;
+
+ @Override
+ public String toString() {
+ return new StringBuilder("CallbackInfo[")
+ .append("enabled=").append(enabled)
+ .append(",numConnectedDevices=").append(numConnectedDevices)
+ .append(']').toString();
+ }
+ }
}
diff --git a/com/android/systemui/qs/tiles/LocationTile.java b/com/android/systemui/qs/tiles/LocationTile.java
index c35f5917..8bdbf28b 100644
--- a/com/android/systemui/qs/tiles/LocationTile.java
+++ b/com/android/systemui/qs/tiles/LocationTile.java
@@ -101,6 +101,9 @@ public class LocationTile extends QSTileImpl<BooleanState> {
// state.visible = !(mKeyguard.isSecure() && mKeyguard.isShowing());
state.value = locationEnabled;
checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_SHARE_LOCATION);
+ if (state.disabledByPolicy == false) {
+ checkIfRestrictionEnforcedByAdminOnly(state, UserManager.DISALLOW_CONFIG_LOCATION_MODE);
+ }
state.icon = mIcon;
state.slash.isSlashed = !state.value;
if (locationEnabled) {
diff --git a/com/android/systemui/qs/tiles/NightDisplayTile.java b/com/android/systemui/qs/tiles/NightDisplayTile.java
index 763ffc67..ea6e174d 100644
--- a/com/android/systemui/qs/tiles/NightDisplayTile.java
+++ b/com/android/systemui/qs/tiles/NightDisplayTile.java
@@ -16,6 +16,7 @@
package com.android.systemui.qs.tiles;
+import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Intent;
import android.provider.Settings;
@@ -29,9 +30,17 @@ import com.android.systemui.qs.QSHost;
import com.android.systemui.plugins.qs.QSTile.BooleanState;
import com.android.systemui.qs.tileimpl.QSTileImpl;
+import java.time.format.DateTimeFormatter;
+
public class NightDisplayTile extends QSTileImpl<BooleanState>
implements ColorDisplayController.Callback {
+ /**
+ * Pattern for {@link java.time.format.DateTimeFormatter} used to approximate the time to the
+ * nearest hour and add on the AM/PM indicator.
+ */
+ private static final String APPROXIMATE_HOUR_DATE_TIME_PATTERN = "h a";
+
private ColorDisplayController mController;
private boolean mIsListening;
@@ -74,13 +83,49 @@ public class NightDisplayTile extends QSTileImpl<BooleanState>
@Override
protected void handleUpdateState(BooleanState state, Object arg) {
- final boolean isActivated = mController.isActivated();
- state.value = isActivated;
+ state.value = mController.isActivated();
state.label = state.contentDescription =
mContext.getString(R.string.quick_settings_night_display_label);
state.icon = ResourceIcon.get(R.drawable.ic_qs_night_display_on);
state.expandedAccessibilityClassName = Switch.class.getName();
state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
+ state.secondaryLabel = getSecondaryLabel(state.value);
+ }
+
+ /**
+ * Returns a {@link String} for the secondary label that reflects when the light will be turned
+ * on or off based on the current auto mode and night light activated status.
+ */
+ @Nullable
+ private String getSecondaryLabel(boolean isNightLightActivated) {
+ switch(mController.getAutoMode()) {
+ case ColorDisplayController.AUTO_MODE_TWILIGHT:
+ // Auto mode related to sunrise & sunset. If the light is on, it's guaranteed to be
+ // turned off at sunrise. If it's off, it's guaranteed to be turned on at sunset.
+ return isNightLightActivated
+ ? mContext.getString(
+ R.string.quick_settings_night_secondary_label_until_sunrise)
+ : mContext.getString(
+ R.string.quick_settings_night_secondary_label_on_at_sunset);
+
+ case ColorDisplayController.AUTO_MODE_CUSTOM:
+ // User-specified time, approximated to the nearest hour.
+ return isNightLightActivated
+ ? mContext.getString(
+ R.string.quick_settings_night_secondary_label_until,
+ mController.getCustomEndTime().format(
+ DateTimeFormatter.ofPattern(
+ APPROXIMATE_HOUR_DATE_TIME_PATTERN)))
+ : mContext.getString(
+ R.string.quick_settings_night_secondary_label_on_at,
+ mController.getCustomStartTime().format(
+ DateTimeFormatter.ofPattern(
+ APPROXIMATE_HOUR_DATE_TIME_PATTERN)));
+
+ default:
+ // No secondary label when auto mode is disabled.
+ return null;
+ }
}
@Override
diff --git a/com/android/systemui/qs/tiles/RotationLockTile.java b/com/android/systemui/qs/tiles/RotationLockTile.java
index 1e008944..60422ee6 100644
--- a/com/android/systemui/qs/tiles/RotationLockTile.java
+++ b/com/android/systemui/qs/tiles/RotationLockTile.java
@@ -36,20 +36,8 @@ import com.android.systemui.statusbar.policy.RotationLockController.RotationLock
/** Quick settings tile: Rotation **/
public class RotationLockTile extends QSTileImpl<BooleanState> {
- private final AnimationIcon mPortraitToAuto
- = new AnimationIcon(R.drawable.ic_portrait_to_auto_rotate_animation,
- R.drawable.ic_portrait_from_auto_rotate);
- private final AnimationIcon mAutoToPortrait
- = new AnimationIcon(R.drawable.ic_portrait_from_auto_rotate_animation,
- R.drawable.ic_portrait_to_auto_rotate);
-
- private final AnimationIcon mLandscapeToAuto
- = new AnimationIcon(R.drawable.ic_landscape_to_auto_rotate_animation,
- R.drawable.ic_landscape_from_auto_rotate);
- private final AnimationIcon mAutoToLandscape
- = new AnimationIcon(R.drawable.ic_landscape_from_auto_rotate_animation,
- R.drawable.ic_landscape_to_auto_rotate);
+ private final Icon mIcon = ResourceIcon.get(R.drawable.ic_qs_auto_rotate);
private final RotationLockController mController;
public RotationLockTile(QSHost host) {
@@ -93,19 +81,10 @@ public class RotationLockTile extends QSTileImpl<BooleanState> {
protected void handleUpdateState(BooleanState state, Object arg) {
if (mController == null) return;
final boolean rotationLocked = mController.isRotationLocked();
- // TODO: Handle accessibility rotation lock and whatnot.
state.value = !rotationLocked;
- final boolean portrait = isCurrentOrientationLockPortrait(mController, mContext);
- if (rotationLocked) {
- final int label = portrait ? R.string.quick_settings_rotation_locked_portrait_label
- : R.string.quick_settings_rotation_locked_landscape_label;
- state.label = mContext.getString(label);
- state.icon = portrait ? mAutoToPortrait : mAutoToLandscape;
- } else {
- state.label = mContext.getString(R.string.quick_settings_rotation_unlocked_label);
- state.icon = portrait ? mPortraitToAuto : mLandscapeToAuto;
- }
+ state.label = mContext.getString(R.string.quick_settings_rotation_unlocked_label);
+ state.icon = mIcon;
state.contentDescription = getAccessibilityString(rotationLocked);
state.expandedAccessibilityClassName = Switch.class.getName();
state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
@@ -134,18 +113,7 @@ public class RotationLockTile extends QSTileImpl<BooleanState> {
* @param locked Whether or not rotation is locked.
*/
private String getAccessibilityString(boolean locked) {
- if (locked) {
- return mContext.getString(R.string.accessibility_quick_settings_rotation_value,
- isCurrentOrientationLockPortrait(mController, mContext)
- ? mContext.getString(
- R.string.quick_settings_rotation_locked_portrait_label)
- : mContext.getString(
- R.string.quick_settings_rotation_locked_landscape_label))
- + "," + mContext.getString(R.string.accessibility_quick_settings_rotation);
-
- } else {
- return mContext.getString(R.string.accessibility_quick_settings_rotation);
- }
+ return mContext.getString(R.string.accessibility_quick_settings_rotation);
}
@Override
diff --git a/com/android/systemui/qs/tiles/WorkModeTile.java b/com/android/systemui/qs/tiles/WorkModeTile.java
index 5f7d6fb4..e098fd87 100644
--- a/com/android/systemui/qs/tiles/WorkModeTile.java
+++ b/com/android/systemui/qs/tiles/WorkModeTile.java
@@ -83,7 +83,7 @@ public class WorkModeTile extends QSTileImpl<BooleanState> implements
@Override
public CharSequence getTileLabel() {
- return mContext.getString(R.string.quick_settings_work_mode_label);
+ return mContext.getString(R.string.quick_settings_work_mode_on_label);
}
@Override
@@ -98,16 +98,17 @@ public class WorkModeTile extends QSTileImpl<BooleanState> implements
state.value = mProfileController.isWorkModeEnabled();
}
- state.label = mContext.getString(R.string.quick_settings_work_mode_label);
state.icon = mIcon;
if (state.value) {
state.slash.isSlashed = false;
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_work_mode_on);
+ state.label = mContext.getString(R.string.quick_settings_work_mode_on_label);
} else {
state.slash.isSlashed = true;
state.contentDescription = mContext.getString(
R.string.accessibility_quick_settings_work_mode_off);
+ state.label = mContext.getString(R.string.quick_settings_work_mode_off_label);
}
state.expandedAccessibilityClassName = Switch.class.getName();
state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
diff --git a/com/android/systemui/recents/Recents.java b/com/android/systemui/recents/Recents.java
index 5b62c7d3..1da4deb6 100644
--- a/com/android/systemui/recents/Recents.java
+++ b/com/android/systemui/recents/Recents.java
@@ -240,7 +240,7 @@ public class Recents extends SystemUI
* Shows the Recents.
*/
@Override
- public void showRecentApps(boolean triggeredFromAltTab, boolean fromHome) {
+ public void showRecentApps(boolean triggeredFromAltTab) {
// Ensure the device has been provisioned before allowing the user to interact with
// recents
if (!isUserSetup()) {
@@ -252,7 +252,7 @@ public class Recents extends SystemUI
int currentUser = sSystemServicesProxy.getCurrentUser();
if (sSystemServicesProxy.isSystemUser(currentUser)) {
mImpl.showRecents(triggeredFromAltTab, false /* draggingInRecents */,
- true /* animate */, false /* reloadTasks */, fromHome, recentsGrowTarget);
+ true /* animate */, recentsGrowTarget);
} else {
if (mSystemToUserCallbacks != null) {
IRecentsNonSystemUserCallbacks callbacks =
@@ -260,8 +260,7 @@ public class Recents extends SystemUI
if (callbacks != null) {
try {
callbacks.showRecents(triggeredFromAltTab, false /* draggingInRecents */,
- true /* animate */, false /* reloadTasks */, fromHome,
- recentsGrowTarget);
+ true /* animate */, recentsGrowTarget);
} catch (RemoteException e) {
Log.e(TAG, "Callback failed", e);
}
diff --git a/com/android/systemui/recents/RecentsActivity.java b/com/android/systemui/recents/RecentsActivity.java
index 06dfd183..b0a2fadf 100644
--- a/com/android/systemui/recents/RecentsActivity.java
+++ b/com/android/systemui/recents/RecentsActivity.java
@@ -356,15 +356,15 @@ public class RecentsActivity extends Activity implements ViewTreeObserver.OnPreD
registerReceiver(mSystemBroadcastReceiver, filter);
getWindow().addPrivateFlags(LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION);
-
- // Reload the stack view
- reloadStackView();
}
@Override
protected void onStart() {
super.onStart();
+ // Reload the stack view whenever we are made visible again
+ reloadStackView();
+
// Notify that recents is now visible
EventBus.getDefault().send(new RecentsVisibilityChangedEvent(this, true));
MetricsLogger.visible(this, MetricsEvent.OVERVIEW_ACTIVITY);
@@ -411,14 +411,6 @@ public class RecentsActivity extends Activity implements ViewTreeObserver.OnPreD
}
}
- @Override
- protected void onNewIntent(Intent intent) {
- super.onNewIntent(intent);
-
- // Reload the stack view
- reloadStackView();
- }
-
/**
* Reloads the stack views upon launching Recents.
*/
@@ -530,7 +522,11 @@ public class RecentsActivity extends Activity implements ViewTreeObserver.OnPreD
// Set the window background
mRecentsView.updateBackgroundScrim(getWindow(), isInMultiWindowMode);
- reloadTaskStack(isInMultiWindowMode, true /* sendConfigChangedEvent */);
+ // Reload the task stack view if we are still visible to pick up the change in tasks that
+ // result from entering/exiting multi-window
+ if (mIsVisible) {
+ reloadTaskStack(isInMultiWindowMode, true /* sendConfigChangedEvent */);
+ }
}
@Override
diff --git a/com/android/systemui/recents/RecentsImpl.java b/com/android/systemui/recents/RecentsImpl.java
index 8359690b..ee1b0910 100644
--- a/com/android/systemui/recents/RecentsImpl.java
+++ b/com/android/systemui/recents/RecentsImpl.java
@@ -255,7 +255,6 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener
// When this fires, then the user has not released alt-tab for at least
// FAST_ALT_TAB_DELAY_MS milliseconds
showRecents(mTriggeredFromAltTab, false /* draggingInRecents */, true /* animate */,
- false /* reloadTasks */, false /* fromHome */,
DividerView.INVALID_RECENTS_GROW_TARGET);
}
});
@@ -322,8 +321,15 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener
}
public void showRecents(boolean triggeredFromAltTab, boolean draggingInRecents,
- boolean animate, boolean launchedWhileDockingTask, boolean fromHome,
- int growTarget) {
+ boolean animate, int growTarget) {
+ final SystemServicesProxy ssp = Recents.getSystemServices();
+ final MutableBoolean isHomeStackVisible = new MutableBoolean(true);
+ final boolean isRecentsVisible = Recents.getSystemServices().isRecentsActivityVisible(
+ isHomeStackVisible);
+ final boolean fromHome = isHomeStackVisible.value;
+ final boolean launchedWhileDockingTask =
+ Recents.getSystemServices().getSplitScreenPrimaryStack() != null;
+
mTriggeredFromAltTab = triggeredFromAltTab;
mDraggingInRecents = draggingInRecents;
mLaunchedWhileDocking = launchedWhileDockingTask;
@@ -349,10 +355,8 @@ public class RecentsImpl implements ActivityOptions.OnAnimationFinishedListener
try {
// Check if the top task is in the home stack, and start the recents activity
- SystemServicesProxy ssp = Recents.getSystemServices();
- boolean forceVisible = launchedWhileDockingTask || draggingInRecents;
- MutableBoolean isHomeStackVisible = new MutableBoolean(forceVisible);
- if (forceVisible || !ssp.isRecentsActivityVisible(isHomeStackVisible)) {
+ final boolean forceVisible = launchedWhileDockingTask || draggingInRecents;
+ if (forceVisible || !isRecentsVisible) {
ActivityManager.RunningTaskInfo runningTask =
ActivityManagerWrapper.getInstance().getRunningTask();
startRecentsActivityAndDismissKeyguardIfNeeded(runningTask,
diff --git a/com/android/systemui/recents/RecentsImplProxy.java b/com/android/systemui/recents/RecentsImplProxy.java
index 9493c78f..beec4b39 100644
--- a/com/android/systemui/recents/RecentsImplProxy.java
+++ b/com/android/systemui/recents/RecentsImplProxy.java
@@ -58,15 +58,12 @@ public class RecentsImplProxy extends IRecentsNonSystemUserCallbacks.Stub {
@Override
public void showRecents(boolean triggeredFromAltTab, boolean draggingInRecents, boolean animate,
- boolean reloadTasks, boolean fromHome, int growTarget)
- throws RemoteException {
+ int growTarget) throws RemoteException {
SomeArgs args = SomeArgs.obtain();
args.argi1 = triggeredFromAltTab ? 1 : 0;
args.argi2 = draggingInRecents ? 1 : 0;
args.argi3 = animate ? 1 : 0;
- args.argi4 = reloadTasks ? 1 : 0;
- args.argi5 = fromHome ? 1 : 0;
- args.argi6 = growTarget;
+ args.argi4 = growTarget;
mHandler.sendMessage(mHandler.obtainMessage(MSG_SHOW_RECENTS, args));
}
@@ -130,7 +127,7 @@ public class RecentsImplProxy extends IRecentsNonSystemUserCallbacks.Stub {
case MSG_SHOW_RECENTS:
args = (SomeArgs) msg.obj;
mImpl.showRecents(args.argi1 != 0, args.argi2 != 0, args.argi3 != 0,
- args.argi4 != 0, args.argi5 != 0, args.argi6);
+ args.argi4);
break;
case MSG_HIDE_RECENTS:
mImpl.hideRecents(msg.arg1 != 0, msg.arg2 != 0);
diff --git a/com/android/systemui/recents/SwipeUpOnboarding.java b/com/android/systemui/recents/SwipeUpOnboarding.java
new file mode 100644
index 00000000..0494e1b0
--- /dev/null
+++ b/com/android/systemui/recents/SwipeUpOnboarding.java
@@ -0,0 +1,252 @@
+/*
+ * 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 com.android.systemui.recents;
+
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_RECENTS;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
+import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
+
+import android.annotation.TargetApi;
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.RippleDrawable;
+import android.os.Build;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.systemui.Prefs;
+import com.android.systemui.R;
+import com.android.systemui.recents.misc.SysUiTaskStackChangeListener;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
+
+/**
+ * Shows onboarding for the new recents interaction in P (codenamed quickstep).
+ */
+@TargetApi(Build.VERSION_CODES.P)
+public class SwipeUpOnboarding {
+
+ private static final String TAG = "SwipeUpOnboarding";
+ private static final boolean RESET_PREFS_FOR_DEBUG = false;
+ private static final long SHOW_DELAY_MS = 500;
+ private static final long SHOW_HIDE_DURATION_MS = 300;
+ // Don't show the onboarding until the user has launched this number of apps.
+ private static final int SHOW_ON_APP_LAUNCH = 2;
+
+ private final Context mContext;
+ private final WindowManager mWindowManager;
+ private final View mLayout;
+ private final TextView mTextView;
+ private final ImageView mDismissView;
+ private final ColorDrawable mBackgroundDrawable;
+ private final int mDarkBackgroundColor;
+ private final int mLightBackgroundColor;
+ private final int mDarkContentColor;
+ private final int mLightContentColor;
+ private final RippleDrawable mDarkRipple;
+ private final RippleDrawable mLightRipple;
+
+ private boolean mTaskListenerRegistered;
+ private ComponentName mLauncherComponent;
+ private boolean mLayoutAttachedToWindow;
+ private boolean mBackgroundIsLight;
+
+ private final SysUiTaskStackChangeListener mTaskListener = new SysUiTaskStackChangeListener() {
+ @Override
+ public void onTaskStackChanged() {
+ ActivityManager.RunningTaskInfo info = ActivityManagerWrapper.getInstance()
+ .getRunningTask(ACTIVITY_TYPE_UNDEFINED /* ignoreActivityType */);
+ int activityType = info.configuration.windowConfiguration.getActivityType();
+ int numAppsLaunched = Prefs.getInt(mContext, Prefs.Key.NUM_APPS_LAUNCHED, 0);
+ if (activityType == ACTIVITY_TYPE_STANDARD) {
+ numAppsLaunched++;
+ if (numAppsLaunched >= SHOW_ON_APP_LAUNCH) {
+ show();
+ } else {
+ Prefs.putInt(mContext, Prefs.Key.NUM_APPS_LAUNCHED, numAppsLaunched);
+ }
+ } else {
+ String runningPackage = info.topActivity.getPackageName();
+ // TODO: use callback from the overview proxy service to handle this case
+ if (runningPackage.equals(mLauncherComponent.getPackageName())
+ && activityType == ACTIVITY_TYPE_RECENTS) {
+ Prefs.putBoolean(mContext, Prefs.Key.HAS_SWIPED_UP_FOR_RECENTS, true);
+ onDisconnectedFromLauncher();
+ } else {
+ hide(false);
+ }
+ }
+ }
+ };
+
+ private final View.OnAttachStateChangeListener mOnAttachStateChangeListener
+ = new View.OnAttachStateChangeListener() {
+ @Override
+ public void onViewAttachedToWindow(View view) {
+ if (view == mLayout) {
+ mLayoutAttachedToWindow = true;
+ }
+ }
+
+ @Override
+ public void onViewDetachedFromWindow(View view) {
+ if (view == mLayout) {
+ mLayoutAttachedToWindow = false;
+ }
+ }
+ };
+
+ public SwipeUpOnboarding(Context context) {
+ mContext = context;
+ final Resources res = context.getResources();
+ mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+ mLayout = LayoutInflater.from(mContext).inflate(R.layout.recents_swipe_up_onboarding, null);
+ mTextView = (TextView) mLayout.findViewById(R.id.onboarding_text);
+ mDismissView = (ImageView) mLayout.findViewById(R.id.dismiss);
+ mDarkBackgroundColor = res.getColor(android.R.color.background_dark);
+ mLightBackgroundColor = res.getColor(android.R.color.background_light);
+ mDarkContentColor = res.getColor(R.color.primary_text_default_material_light);
+ mLightContentColor = res.getColor(R.color.primary_text_default_material_dark);
+ mDarkRipple = new RippleDrawable(res.getColorStateList(R.color.ripple_material_light),
+ null, null);
+ mLightRipple = new RippleDrawable(res.getColorStateList(R.color.ripple_material_dark),
+ null, null);
+ mBackgroundDrawable = new ColorDrawable(mDarkBackgroundColor);
+
+ mLayout.addOnAttachStateChangeListener(mOnAttachStateChangeListener);
+ mLayout.setBackground(mBackgroundDrawable);
+ mDismissView.setOnClickListener(v -> hide(true));
+
+ if (RESET_PREFS_FOR_DEBUG) {
+ Prefs.putBoolean(mContext, Prefs.Key.HAS_SWIPED_UP_FOR_RECENTS, false);
+ Prefs.putInt(mContext, Prefs.Key.NUM_APPS_LAUNCHED, 0);
+ }
+ }
+
+ public void onConnectedToLauncher(ComponentName launcherComponent) {
+ // TODO: re-enable this once we have the proper callback for when a swipe up was performed.
+ final boolean disableOnboarding = true;
+ if (disableOnboarding) {
+ return;
+ }
+ mLauncherComponent = launcherComponent;
+ boolean alreadyLearnedSwipeUpForRecents = Prefs.getBoolean(mContext,
+ Prefs.Key.HAS_SWIPED_UP_FOR_RECENTS, false);
+ if (!mTaskListenerRegistered && !alreadyLearnedSwipeUpForRecents) {
+ ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskListener);
+ mTaskListenerRegistered = true;
+ }
+ }
+
+ public void onDisconnectedFromLauncher() {
+ if (mTaskListenerRegistered) {
+ ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskListener);
+ mTaskListenerRegistered = false;
+ }
+ hide(false);
+ }
+
+ public void onConfigurationChanged(Configuration newConfiguration) {
+ if (newConfiguration.orientation != Configuration.ORIENTATION_PORTRAIT) {
+ hide(false);
+ }
+ }
+
+ public void show() {
+ // Only show in portrait.
+ int orientation = mContext.getResources().getConfiguration().orientation;
+ if (!mLayoutAttachedToWindow && orientation == Configuration.ORIENTATION_PORTRAIT) {
+ mLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ mWindowManager.addView(mLayout, getWindowLayoutParams());
+ int layoutHeight = mLayout.getHeight();
+ if (layoutHeight == 0) {
+ mLayout.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED);
+ layoutHeight = mLayout.getMeasuredHeight();
+ }
+ mLayout.setTranslationY(layoutHeight);
+ mLayout.setAlpha(0);
+ mLayout.animate()
+ .translationY(0)
+ .alpha(1f)
+ .withLayer()
+ .setStartDelay(SHOW_DELAY_MS)
+ .setDuration(SHOW_HIDE_DURATION_MS)
+ .setInterpolator(new DecelerateInterpolator())
+ .start();
+ }
+ }
+
+ public void hide(boolean animate) {
+ if (mLayoutAttachedToWindow) {
+ if (animate) {
+ mLayout.animate()
+ .translationY(mLayout.getHeight())
+ .alpha(0f)
+ .withLayer()
+ .setDuration(SHOW_HIDE_DURATION_MS)
+ .setInterpolator(new AccelerateInterpolator())
+ .withEndAction(() -> mWindowManager.removeView(mLayout))
+ .start();
+ } else {
+ mWindowManager.removeView(mLayout);
+ }
+ }
+ }
+
+ public void setContentDarkIntensity(float contentDarkIntensity) {
+ boolean backgroundIsLight = contentDarkIntensity > 0.5f;
+ if (backgroundIsLight != mBackgroundIsLight) {
+ mBackgroundIsLight = backgroundIsLight;
+ mBackgroundDrawable.setColor(mBackgroundIsLight
+ ? mLightBackgroundColor : mDarkBackgroundColor);
+ int contentColor = mBackgroundIsLight ? mDarkContentColor : mLightContentColor;
+ mTextView.setTextColor(contentColor);
+ mTextView.getCompoundDrawables()[3].setColorFilter(contentColor,
+ PorterDuff.Mode.SRC_IN);
+ mDismissView.setColorFilter(contentColor);
+ mDismissView.setBackground(mBackgroundIsLight ? mDarkRipple : mLightRipple);
+ }
+ }
+
+ private WindowManager.LayoutParams getWindowLayoutParams() {
+ int flags = WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
+ | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR
+ | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+ final WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG,
+ flags,
+ PixelFormat.TRANSLUCENT);
+ lp.privateFlags |= WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
+ lp.setTitle("SwipeUpOnboarding");
+ lp.gravity = Gravity.BOTTOM;
+ return lp;
+ }
+}
diff --git a/com/android/systemui/recents/misc/SystemServicesProxy.java b/com/android/systemui/recents/misc/SystemServicesProxy.java
index 130a5e31..613d9fbb 100644
--- a/com/android/systemui/recents/misc/SystemServicesProxy.java
+++ b/com/android/systemui/recents/misc/SystemServicesProxy.java
@@ -274,20 +274,21 @@ public class SystemServicesProxy {
return false;
}
+ public ActivityManager.StackInfo getSplitScreenPrimaryStack() {
+ try {
+ return mIam.getStackInfo(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_UNDEFINED);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
/**
* @return whether there are any docked tasks for the current user.
*/
public boolean hasDockedTask() {
if (mIam == null) return false;
- ActivityManager.StackInfo stackInfo = null;
- try {
- stackInfo =
- mIam.getStackInfo(WINDOWING_MODE_SPLIT_SCREEN_PRIMARY, ACTIVITY_TYPE_UNDEFINED);
- } catch (RemoteException e) {
- e.printStackTrace();
- }
-
+ ActivityManager.StackInfo stackInfo = getSplitScreenPrimaryStack();
if (stackInfo != null) {
int userId = getCurrentUser();
boolean hasUserTask = false;
diff --git a/com/android/systemui/recents/views/TaskStackView.java b/com/android/systemui/recents/views/TaskStackView.java
index 36c9095f..5be29008 100644
--- a/com/android/systemui/recents/views/TaskStackView.java
+++ b/com/android/systemui/recents/views/TaskStackView.java
@@ -210,7 +210,7 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
private boolean mStackActionButtonVisible;
// Percentage of last ScrollP from the min to max scrollP that lives after configuration changes
- private float mLastScrollPPercent;
+ private float mLastScrollPPercent = -1;
// We keep track of the task view focused by user interaction and draw a frame around it in the
// grid layout.
@@ -647,14 +647,12 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
* an animation provided in {@param animationOverrides}, that will be used instead.
*/
private void relayoutTaskViews(AnimationProps animation,
- ArrayMap<Task, AnimationProps> animationOverrides,
- boolean ignoreTaskOverrides) {
+ ArrayMap<Task, AnimationProps> animationOverrides, boolean ignoreTaskOverrides) {
// If we had a deferred animation, cancel that
cancelDeferredTaskViewLayoutAnimation();
// Synchronize the current set of TaskViews
- bindVisibleTaskViews(mStackScroller.getStackScroll(),
- ignoreTaskOverrides /* ignoreTaskOverrides */);
+ bindVisibleTaskViews(mStackScroller.getStackScroll(), ignoreTaskOverrides);
// Animate them to their final transforms with the given animation
List<TaskView> taskViews = getTaskViews();
@@ -2067,8 +2065,11 @@ public class TaskStackView extends FrameLayout implements TaskStack.TaskStackCal
// Update the Clear All button in case we're switching in or out of grid layout.
updateStackActionButtonVisibility();
- // Trigger a new layout and update to the initial state if necessary
- if (event.fromMultiWindow) {
+ // Trigger a new layout and update to the initial state if necessary. When entering split
+ // screen, the multi-window configuration change event can happen after the stack is already
+ // reloaded (but pending measure/layout), in this case, do not override the intiial state
+ // and just wait for the upcoming measure/layout pass.
+ if (event.fromMultiWindow && mInitialState == INITIAL_STATE_UPDATE_NONE) {
mInitialState = INITIAL_STATE_UPDATE_LAYOUT_ONLY;
requestLayout();
} else if (event.fromDeviceOrientationChange) {
diff --git a/com/android/systemui/screenshot/GlobalScreenshot.java b/com/android/systemui/screenshot/GlobalScreenshot.java
index 6db46b59..0132fa85 100644
--- a/com/android/systemui/screenshot/GlobalScreenshot.java
+++ b/com/android/systemui/screenshot/GlobalScreenshot.java
@@ -185,7 +185,7 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
// The public notification will show similar info but with the actual screenshot omitted
mPublicNotificationBuilder =
- new Notification.Builder(context, NotificationChannels.SCREENSHOTS)
+ new Notification.Builder(context, NotificationChannels.SCREENSHOTS_HEADSUP)
.setContentTitle(r.getString(R.string.screenshot_saving_title))
.setContentText(r.getString(R.string.screenshot_saving_text))
.setSmallIcon(R.drawable.stat_notify_image)
@@ -196,7 +196,8 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
com.android.internal.R.color.system_notification_accent_color));
SystemUI.overrideNotificationAppName(context, mPublicNotificationBuilder);
- mNotificationBuilder = new Notification.Builder(context, NotificationChannels.SCREENSHOTS)
+ mNotificationBuilder = new Notification.Builder(context,
+ NotificationChannels.SCREENSHOTS_HEADSUP)
.setTicker(r.getString(R.string.screenshot_saving_ticker)
+ (mTickerAddSpace ? " " : ""))
.setContentTitle(r.getString(R.string.screenshot_saving_title))
@@ -293,12 +294,13 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
sharingIntent.putExtra(Intent.EXTRA_STREAM, uri);
sharingIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
- // Create a share action for the notification. Note, we proxy the call to ShareReceiver
- // because RemoteViews currently forces an activity options on the PendingIntent being
- // launched, and since we don't want to trigger the share sheet in this case, we will
- // start the chooser activitiy directly in ShareReceiver.
+ // Create a share action for the notification. Note, we proxy the call to
+ // ScreenshotActionReceiver because RemoteViews currently forces an activity options
+ // on the PendingIntent being launched, and since we don't want to trigger the share
+ // sheet in this case, we start the chooser activity directly in
+ // ScreenshotActionReceiver.
PendingIntent shareAction = PendingIntent.getBroadcast(context, 0,
- new Intent(context, GlobalScreenshot.ShareReceiver.class)
+ new Intent(context, GlobalScreenshot.ScreenshotActionReceiver.class)
.putExtra(SHARING_INTENT, sharingIntent),
PendingIntent.FLAG_CANCEL_CURRENT);
Notification.Action.Builder shareActionBuilder = new Notification.Action.Builder(
@@ -306,15 +308,19 @@ class SaveImageInBackgroundTask extends AsyncTask<Void, Void, Void> {
r.getString(com.android.internal.R.string.share), shareAction);
mNotificationBuilder.addAction(shareActionBuilder.build());
- // Create a delete action for the notification
- PendingIntent deleteAction = PendingIntent.getBroadcast(context, 0,
- new Intent(context, GlobalScreenshot.DeleteScreenshotReceiver.class)
- .putExtra(GlobalScreenshot.SCREENSHOT_URI_ID, uri.toString()),
- PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT);
- Notification.Action.Builder deleteActionBuilder = new Notification.Action.Builder(
- R.drawable.ic_screenshot_delete,
- r.getString(com.android.internal.R.string.delete), deleteAction);
- mNotificationBuilder.addAction(deleteActionBuilder.build());
+ Intent editIntent = new Intent(Intent.ACTION_EDIT);
+ editIntent.setType("image/png");
+ editIntent.putExtra(Intent.EXTRA_STREAM, uri);
+
+ // Create a edit action for the notification the same way.
+ PendingIntent editAction = PendingIntent.getBroadcast(context, 1,
+ new Intent(context, GlobalScreenshot.ScreenshotActionReceiver.class)
+ .putExtra(SHARING_INTENT, editIntent),
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ Notification.Action.Builder editActionBuilder = new Notification.Action.Builder(
+ R.drawable.ic_screenshot_edit,
+ r.getString(com.android.internal.R.string.screenshot_edit), editAction);
+ mNotificationBuilder.addAction(editActionBuilder.build());
mParams.imageUri = uri;
mParams.image = null;
@@ -879,9 +885,9 @@ class GlobalScreenshot {
}
/**
- * Receiver to proxy the share intent.
+ * Receiver to proxy the share or edit intent.
*/
- public static class ShareReceiver extends BroadcastReceiver {
+ public static class ScreenshotActionReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
try {
@@ -903,7 +909,7 @@ class GlobalScreenshot {
}
/**
- * Removes the notification for a screenshot after a share target is chosen.
+ * Removes the notification for a screenshot after a share or edit target is chosen.
*/
public static class TargetChosenReceiver extends BroadcastReceiver {
@Override
diff --git a/com/android/systemui/screenshot/TakeScreenshotService.java b/com/android/systemui/screenshot/TakeScreenshotService.java
index f3bae20e..34b8bfe5 100644
--- a/com/android/systemui/screenshot/TakeScreenshotService.java
+++ b/com/android/systemui/screenshot/TakeScreenshotService.java
@@ -67,6 +67,8 @@ public class TakeScreenshotService extends Service {
case WindowManager.TAKE_SCREENSHOT_SELECTED_REGION:
mScreenshot.takeScreenshotPartial(finisher, msg.arg1 > 0, msg.arg2 > 0);
break;
+ default:
+ Log.d(TAG, "Invalid screenshot option: " + msg.what);
}
}
};
diff --git a/com/android/systemui/settings/BrightnessController.java b/com/android/systemui/settings/BrightnessController.java
index d3f997a0..3db30fcb 100644
--- a/com/android/systemui/settings/BrightnessController.java
+++ b/com/android/systemui/settings/BrightnessController.java
@@ -16,9 +16,11 @@
package com.android.systemui.settings;
+import android.animation.ValueAnimator;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
+import android.hardware.display.DisplayManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
@@ -45,11 +47,7 @@ public class BrightnessController implements ToggleSlider.Listener {
private static final String TAG = "StatusBar.BrightnessController";
private static final boolean SHOW_AUTOMATIC_ICON = false;
- /**
- * {@link android.provider.Settings.System#SCREEN_AUTO_BRIGHTNESS_ADJ} uses the range [-1, 1].
- * Using this factor, it is converted to [0, BRIGHTNESS_ADJ_RESOLUTION] for the SeekBar.
- */
- private static final float BRIGHTNESS_ADJ_RESOLUTION = 2048;
+ private static final int SLIDER_ANIMATION_DURATION = 3000;
private static final int MSG_UPDATE_ICON = 0;
private static final int MSG_UPDATE_SLIDER = 1;
@@ -67,7 +65,7 @@ public class BrightnessController implements ToggleSlider.Listener {
private final ImageView mIcon;
private final ToggleSlider mControl;
private final boolean mAutomaticAvailable;
- private final IPowerManager mPower;
+ private final DisplayManager mDisplayManager;
private final CurrentUserTracker mUserTracker;
private final IVrManager mVrManager;
@@ -81,6 +79,9 @@ public class BrightnessController implements ToggleSlider.Listener {
private volatile boolean mIsVrModeEnabled;
private boolean mListening;
private boolean mExternalChange;
+ private boolean mControlInitialized;
+
+ private ValueAnimator mSliderAnimator;
public interface BrightnessStateChangeCallback {
public void onBrightnessLevelChanged();
@@ -95,8 +96,6 @@ public class BrightnessController implements ToggleSlider.Listener {
Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS);
private final Uri BRIGHTNESS_FOR_VR_URI =
Settings.System.getUriFor(Settings.System.SCREEN_BRIGHTNESS_FOR_VR);
- private final Uri BRIGHTNESS_ADJ_URI =
- Settings.System.getUriFor(Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ);
public BrightnessObserver(Handler handler) {
super(handler);
@@ -114,12 +113,10 @@ public class BrightnessController implements ToggleSlider.Listener {
if (BRIGHTNESS_MODE_URI.equals(uri)) {
mBackgroundHandler.post(mUpdateModeRunnable);
mBackgroundHandler.post(mUpdateSliderRunnable);
- } else if (BRIGHTNESS_URI.equals(uri) && !mAutomatic) {
+ } else if (BRIGHTNESS_URI.equals(uri)) {
mBackgroundHandler.post(mUpdateSliderRunnable);
} else if (BRIGHTNESS_FOR_VR_URI.equals(uri)) {
mBackgroundHandler.post(mUpdateSliderRunnable);
- } else if (BRIGHTNESS_ADJ_URI.equals(uri) && mAutomatic) {
- mBackgroundHandler.post(mUpdateSliderRunnable);
} else {
mBackgroundHandler.post(mUpdateModeRunnable);
mBackgroundHandler.post(mUpdateSliderRunnable);
@@ -141,9 +138,6 @@ public class BrightnessController implements ToggleSlider.Listener {
cr.registerContentObserver(
BRIGHTNESS_FOR_VR_URI,
false, this, UserHandle.USER_ALL);
- cr.registerContentObserver(
- BRIGHTNESS_ADJ_URI,
- false, this, UserHandle.USER_ALL);
}
public void stopObserving() {
@@ -214,12 +208,6 @@ public class BrightnessController implements ToggleSlider.Listener {
mHandler.obtainMessage(MSG_UPDATE_SLIDER,
mMaximumBacklightForVr - mMinimumBacklightForVr,
value - mMinimumBacklightForVr).sendToTarget();
- } else if (mAutomatic) {
- float value = Settings.System.getFloatForUser(mContext.getContentResolver(),
- Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, 0,
- UserHandle.USER_CURRENT);
- mHandler.obtainMessage(MSG_UPDATE_SLIDER, (int) BRIGHTNESS_ADJ_RESOLUTION,
- (int) ((value + 1) * BRIGHTNESS_ADJ_RESOLUTION / 2f)).sendToTarget();
} else {
int value;
value = Settings.System.getIntForUser(mContext.getContentResolver(),
@@ -250,7 +238,7 @@ public class BrightnessController implements ToggleSlider.Listener {
break;
case MSG_UPDATE_SLIDER:
mControl.setMax(msg.arg1);
- mControl.setValue(msg.arg2);
+ animateSliderTo(msg.arg2);
break;
case MSG_SET_CHECKED:
mControl.setChecked(msg.arg1 != 0);
@@ -295,8 +283,7 @@ public class BrightnessController implements ToggleSlider.Listener {
mAutomaticAvailable = context.getResources().getBoolean(
com.android.internal.R.bool.config_automatic_brightness_available);
- mPower = IPowerManager.Stub.asInterface(ServiceManager.getService(
- Context.POWER_SERVICE));
+ mDisplayManager = context.getSystemService(DisplayManager.class);
mVrManager = IVrManager.Stub.asInterface(ServiceManager.getService(
Context.VR_SERVICE));
}
@@ -356,6 +343,10 @@ public class BrightnessController implements ToggleSlider.Listener {
updateIcon(mAutomatic);
if (mExternalChange) return;
+ if (mSliderAnimator != null) {
+ mSliderAnimator.cancel();
+ }
+
if (mIsVrModeEnabled) {
final int val = value + mMinimumBacklightForVr;
if (stopTracking) {
@@ -371,7 +362,7 @@ public class BrightnessController implements ToggleSlider.Listener {
}
});
}
- } else if (!mAutomatic) {
+ } else {
final int val = value + mMinimumBacklight;
if (stopTracking) {
MetricsLogger.action(mContext, MetricsEvent.ACTION_BRIGHTNESS, val);
@@ -386,21 +377,6 @@ public class BrightnessController implements ToggleSlider.Listener {
}
});
}
- } else {
- final float adj = value / (BRIGHTNESS_ADJ_RESOLUTION / 2f) - 1;
- if (stopTracking) {
- MetricsLogger.action(mContext, MetricsEvent.ACTION_BRIGHTNESS_AUTO, value);
- }
- setBrightnessAdj(adj);
- if (!tracking) {
- AsyncTask.execute(new Runnable() {
- public void run() {
- Settings.System.putFloatForUser(mContext.getContentResolver(),
- Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, adj,
- UserHandle.USER_CURRENT);
- }
- });
- }
}
for (BrightnessStateChangeCallback cb : mChangeCallbacks) {
@@ -415,17 +391,11 @@ public class BrightnessController implements ToggleSlider.Listener {
}
private void setBrightness(int brightness) {
- try {
- mPower.setTemporaryScreenBrightnessSettingOverride(brightness);
- } catch (RemoteException ex) {
- }
+ mDisplayManager.setTemporaryBrightness(brightness);
}
private void setBrightnessAdj(float adj) {
- try {
- mPower.setTemporaryScreenAutoBrightnessAdjustmentSettingOverride(adj);
- } catch (RemoteException ex) {
- }
+ mDisplayManager.setTemporaryAutoBrightnessAdjustment(adj);
}
private void updateIcon(boolean automatic) {
@@ -442,4 +412,23 @@ public class BrightnessController implements ToggleSlider.Listener {
mBackgroundHandler.post(mUpdateSliderRunnable);
}
}
+
+ private void animateSliderTo(int target) {
+ if (!mControlInitialized) {
+ // Don't animate the first value since it's default state isn't meaningful to users.
+ mControl.setValue(target);
+ mControlInitialized = true;
+ }
+ if (mSliderAnimator != null && mSliderAnimator.isStarted()) {
+ mSliderAnimator.cancel();
+ }
+ mSliderAnimator = ValueAnimator.ofInt(mControl.getValue(), target);
+ mSliderAnimator.addUpdateListener((ValueAnimator animation) -> {
+ mExternalChange = true;
+ mControl.setValue((int)animation.getAnimatedValue());
+ mExternalChange = false;
+ });
+ mSliderAnimator.setDuration(SLIDER_ANIMATION_DURATION);
+ mSliderAnimator.start();
+ }
}
diff --git a/com/android/systemui/settings/ToggleSlider.java b/com/android/systemui/settings/ToggleSlider.java
index 62abf3d6..135f89d3 100644
--- a/com/android/systemui/settings/ToggleSlider.java
+++ b/com/android/systemui/settings/ToggleSlider.java
@@ -28,4 +28,5 @@ public interface ToggleSlider {
default boolean isChecked() { return false; }
void setMax(int max);
void setValue(int value);
+ int getValue();
}
diff --git a/com/android/systemui/settings/ToggleSliderView.java b/com/android/systemui/settings/ToggleSliderView.java
index 5b234e9c..07b9ec27 100644
--- a/com/android/systemui/settings/ToggleSliderView.java
+++ b/com/android/systemui/settings/ToggleSliderView.java
@@ -126,6 +126,11 @@ public class ToggleSliderView extends RelativeLayout implements ToggleSlider {
}
@Override
+ public int getValue() {
+ return mSlider.getProgress();
+ }
+
+ @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (mMirror != null) {
MotionEvent copy = ev.copy();
diff --git a/com/android/systemui/shared/recents/utilities/Utilities.java b/com/android/systemui/shared/recents/utilities/Utilities.java
index a5d19639..7d159b74 100644
--- a/com/android/systemui/shared/recents/utilities/Utilities.java
+++ b/com/android/systemui/shared/recents/utilities/Utilities.java
@@ -20,6 +20,7 @@ import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.RectEvaluator;
import android.annotation.FloatRange;
+import android.annotation.Nullable;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
@@ -28,14 +29,18 @@ import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.os.Message;
import android.os.Trace;
import android.util.ArraySet;
import android.util.IntProperty;
import android.util.Property;
import android.util.TypedValue;
+import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewParent;
+import android.view.ViewRootImpl;
import android.view.ViewStub;
import java.util.ArrayList;
@@ -291,6 +296,28 @@ public class Utilities {
}
/**
+ * @return The next frame name for the specified surface or -1 if the surface is no longer
+ * valid.
+ */
+ public static long getNextFrameNumber(Surface s) {
+ return s != null && s.isValid()
+ ? s.getNextFrameNumber()
+ : -1;
+
+ }
+
+ /**
+ * @return The surface for the specified view.
+ */
+ public static @Nullable Surface getSurface(View v) {
+ ViewRootImpl viewRoot = v.getViewRootImpl();
+ if (viewRoot == null) {
+ return null;
+ }
+ return viewRoot.mSurface;
+ }
+
+ /**
* Returns a lightweight dump of a rect.
*/
public static String dumpRect(Rect r) {
@@ -299,4 +326,12 @@ public class Utilities {
}
return r.left + "," + r.top + "-" + r.right + "," + r.bottom;
}
+
+ /**
+ * Posts a runnable on a handler at the front of the queue ignoring any sync barriers.
+ */
+ public static void postAtFrontOfQueueAsynchronously(Handler h, Runnable r) {
+ Message msg = h.obtainMessage().setCallback(r);
+ h.sendMessageAtFrontOfQueue(msg);
+ }
}
diff --git a/com/android/systemui/shared/system/ActivityCompat.java b/com/android/systemui/shared/system/ActivityCompat.java
new file mode 100644
index 00000000..0d8ce58d
--- /dev/null
+++ b/com/android/systemui/shared/system/ActivityCompat.java
@@ -0,0 +1,34 @@
+/*
+ * 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 com.android.systemui.shared.system;
+
+import android.app.Activity;
+
+public class ActivityCompat {
+ private final Activity mWrapped;
+
+ public ActivityCompat(Activity activity) {
+ mWrapped = activity;
+ }
+
+ /**
+ * @see Activity#registerRemoteAnimations
+ */
+ public void registerRemoteAnimations(RemoteAnimationDefinitionCompat definition) {
+ mWrapped.registerRemoteAnimations(definition.getWrapped());
+ }
+}
diff --git a/com/android/systemui/shared/system/ActivityManagerWrapper.java b/com/android/systemui/shared/system/ActivityManagerWrapper.java
index 1c99d384..f9e1069c 100644
--- a/com/android/systemui/shared/system/ActivityManagerWrapper.java
+++ b/com/android/systemui/shared/system/ActivityManagerWrapper.java
@@ -30,7 +30,9 @@ import android.app.ActivityManager.RecentTaskInfo;
import android.app.ActivityOptions;
import android.app.AppGlobals;
import android.app.IAssistDataReceiver;
+import android.app.WindowConfiguration.ActivityType;
import android.content.Context;
+import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@@ -47,7 +49,10 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.util.IconDrawableFactory;
import android.util.Log;
+import android.view.IRecentsAnimationController;
+import android.view.IRecentsAnimationRunner;
+import android.view.RemoteAnimationTarget;
import com.android.systemui.shared.recents.model.Task;
import com.android.systemui.shared.recents.model.Task.TaskKey;
import com.android.systemui.shared.recents.model.ThumbnailData;
@@ -96,11 +101,14 @@ public class ActivityManagerWrapper {
* @return the top running task (can be {@code null}).
*/
public ActivityManager.RunningTaskInfo getRunningTask() {
+ return getRunningTask(ACTIVITY_TYPE_RECENTS /* ignoreActivityType */);
+ }
+
+ public ActivityManager.RunningTaskInfo getRunningTask(@ActivityType int ignoreActivityType) {
// Note: The set of running tasks from the system is ordered by recency
try {
List<ActivityManager.RunningTaskInfo> tasks =
- ActivityManager.getService().getFilteredTasks(1,
- ACTIVITY_TYPE_RECENTS /* ignoreActivityType */,
+ ActivityManager.getService().getFilteredTasks(1, ignoreActivityType,
WINDOWING_MODE_PINNED /* ignoreWindowingMode */);
if (tasks.isEmpty()) {
return null;
@@ -239,10 +247,9 @@ public class ActivityManagerWrapper {
/**
* Starts the recents activity. The caller should manage the thread on which this is called.
*/
- public void startRecentsActivity(AssistDataReceiverCompat assistDataReceiver, Bundle options,
- ActivityOptions opts, int userId, Consumer<Boolean> resultCallback,
+ public void startRecentsActivity(Intent intent, AssistDataReceiver assistDataReceiver,
+ RecentsAnimationListener animationHandler, Consumer<Boolean> resultCallback,
Handler resultCallbackHandler) {
- Bundle activityOptions = opts != null ? opts.toBundle() : null;
try {
IAssistDataReceiver receiver = null;
if (assistDataReceiver != null) {
@@ -255,8 +262,24 @@ public class ActivityManagerWrapper {
}
};
}
- ActivityManager.getService().startRecentsActivity(receiver, options, activityOptions,
- userId);
+ IRecentsAnimationRunner runner = null;
+ if (animationHandler != null) {
+ runner = new IRecentsAnimationRunner.Stub() {
+ public void onAnimationStart(IRecentsAnimationController controller,
+ RemoteAnimationTarget[] apps) {
+ final RecentsAnimationControllerCompat controllerCompat =
+ new RecentsAnimationControllerCompat(controller);
+ final RemoteAnimationTargetCompat[] appsCompat =
+ RemoteAnimationTargetCompat.wrap(apps);
+ animationHandler.onAnimationStart(controllerCompat, appsCompat);
+ }
+
+ public void onAnimationCanceled() {
+ animationHandler.onAnimationCanceled();
+ }
+ };
+ }
+ ActivityManager.getService().startRecentsActivity(intent, receiver, runner);
if (resultCallback != null) {
resultCallbackHandler.post(new Runnable() {
@Override
diff --git a/com/android/systemui/shared/system/ActivityOptionsCompat.java b/com/android/systemui/shared/system/ActivityOptionsCompat.java
index 705a2152..712cca67 100644
--- a/com/android/systemui/shared/system/ActivityOptionsCompat.java
+++ b/com/android/systemui/shared/system/ActivityOptionsCompat.java
@@ -38,4 +38,9 @@ public abstract class ActivityOptionsCompat {
: SPLIT_SCREEN_CREATE_MODE_BOTTOM_OR_RIGHT);
return options;
}
+
+ public static ActivityOptions makeRemoteAnimation(
+ RemoteAnimationAdapterCompat remoteAnimationAdapter) {
+ return ActivityOptions.makeRemoteAnimation(remoteAnimationAdapter.getWrapped());
+ }
}
diff --git a/com/android/systemui/shared/system/AssistDataReceiverCompat.java b/com/android/systemui/shared/system/AssistDataReceiver.java
index cd943f62..7cd6c512 100644
--- a/com/android/systemui/shared/system/AssistDataReceiverCompat.java
+++ b/com/android/systemui/shared/system/AssistDataReceiver.java
@@ -22,7 +22,7 @@ import android.os.Bundle;
/**
* Abstract class for assist data receivers.
*/
-public abstract class AssistDataReceiverCompat {
- public abstract void onHandleAssistData(Bundle resultData);
- public abstract void onHandleAssistScreenshot(Bitmap screenshot);
+public abstract class AssistDataReceiver {
+ public void onHandleAssistData(Bundle resultData) {}
+ public void onHandleAssistScreenshot(Bitmap screenshot) {}
}
diff --git a/com/android/systemui/pip/phone/InputConsumerController.java b/com/android/systemui/shared/system/InputConsumerController.java
index db4f988a..38b8ae84 100644
--- a/com/android/systemui/pip/phone/InputConsumerController.java
+++ b/com/android/systemui/shared/system/InputConsumerController.java
@@ -14,9 +14,10 @@
* limitations under the License.
*/
-package com.android.systemui.pip.phone;
+package com.android.systemui.shared.system;
import static android.view.WindowManager.INPUT_CONSUMER_PIP;
+import static android.view.WindowManager.INPUT_CONSUMER_RECENTS_ANIMATION;
import android.os.Binder;
import android.os.IBinder;
@@ -29,11 +30,12 @@ import android.view.InputChannel;
import android.view.InputEvent;
import android.view.IWindowManager;
import android.view.MotionEvent;
+import android.view.WindowManagerGlobal;
import java.io.PrintWriter;
/**
- * Manages the input consumer that allows the SystemUI to control the PiP.
+ * Manages the input consumer that allows the SystemUI to directly receive touch input.
*/
public class InputConsumerController {
@@ -55,12 +57,12 @@ public class InputConsumerController {
}
/**
- * Input handler used for the PiP input consumer. Input events are batched and consumed with the
+ * Input handler used for the input consumer. Input events are batched and consumed with the
* SurfaceFlinger vsync.
*/
- private final class PipInputEventReceiver extends BatchedInputEventReceiver {
+ private final class InputEventReceiver extends BatchedInputEventReceiver {
- public PipInputEventReceiver(InputChannel inputChannel, Looper looper) {
+ public InputEventReceiver(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper, Choreographer.getSfInstance());
}
@@ -68,7 +70,6 @@ public class InputConsumerController {
public void onInputEvent(InputEvent event, int displayId) {
boolean handled = true;
try {
- // To be implemented for input handling over Pip windows
if (mListener != null && event instanceof MotionEvent) {
MotionEvent ev = (MotionEvent) event;
handled = mListener.onTouchEvent(ev);
@@ -81,15 +82,35 @@ public class InputConsumerController {
private final IWindowManager mWindowManager;
private final IBinder mToken;
+ private final String mName;
- private PipInputEventReceiver mInputEventReceiver;
+ private InputEventReceiver mInputEventReceiver;
private TouchListener mListener;
private RegistrationListener mRegistrationListener;
- public InputConsumerController(IWindowManager windowManager) {
+ /**
+ * @param name the name corresponding to the input consumer that is defined in the system.
+ */
+ public InputConsumerController(IWindowManager windowManager, String name) {
mWindowManager = windowManager;
mToken = new Binder();
- registerInputConsumer();
+ mName = name;
+ }
+
+ /**
+ * @return A controller for the pip input consumer.
+ */
+ public static InputConsumerController getPipInputConsumer() {
+ return new InputConsumerController(WindowManagerGlobal.getWindowManagerService(),
+ INPUT_CONSUMER_PIP);
+ }
+
+ /**
+ * @return A controller for the recents animation input consumer.
+ */
+ public static InputConsumerController getRecentsAnimationInputConsumer() {
+ return new InputConsumerController(WindowManagerGlobal.getWindowManagerService(),
+ INPUT_CONSUMER_RECENTS_ANIMATION);
}
/**
@@ -125,12 +146,12 @@ public class InputConsumerController {
if (mInputEventReceiver == null) {
final InputChannel inputChannel = new InputChannel();
try {
- mWindowManager.destroyInputConsumer(INPUT_CONSUMER_PIP);
- mWindowManager.createInputConsumer(mToken, INPUT_CONSUMER_PIP, inputChannel);
+ mWindowManager.destroyInputConsumer(mName);
+ mWindowManager.createInputConsumer(mToken, mName, inputChannel);
} catch (RemoteException e) {
- Log.e(TAG, "Failed to create PIP input consumer", e);
+ Log.e(TAG, "Failed to create input consumer", e);
}
- mInputEventReceiver = new PipInputEventReceiver(inputChannel, Looper.myLooper());
+ mInputEventReceiver = new InputEventReceiver(inputChannel, Looper.myLooper());
if (mRegistrationListener != null) {
mRegistrationListener.onRegistrationChanged(true /* isRegistered */);
}
@@ -143,9 +164,9 @@ public class InputConsumerController {
public void unregisterInputConsumer() {
if (mInputEventReceiver != null) {
try {
- mWindowManager.destroyInputConsumer(INPUT_CONSUMER_PIP);
+ mWindowManager.destroyInputConsumer(mName);
} catch (RemoteException e) {
- Log.e(TAG, "Failed to destroy PIP input consumer", e);
+ Log.e(TAG, "Failed to destroy input consumer", e);
}
mInputEventReceiver.dispose();
mInputEventReceiver = null;
diff --git a/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java b/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
new file mode 100644
index 00000000..9a7abf82
--- /dev/null
+++ b/com/android/systemui/shared/system/RecentsAnimationControllerCompat.java
@@ -0,0 +1,61 @@
+/*
+ * 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 com.android.systemui.shared.system;
+
+import android.app.ActivityManager.TaskSnapshot;
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.IRecentsAnimationController;
+
+import com.android.systemui.shared.recents.model.ThumbnailData;
+
+public class RecentsAnimationControllerCompat {
+
+ private static final String TAG = RecentsAnimationControllerCompat.class.getSimpleName();
+
+ private IRecentsAnimationController mAnimationController;
+
+ public RecentsAnimationControllerCompat(IRecentsAnimationController animationController) {
+ mAnimationController = animationController;
+ }
+
+ public ThumbnailData screenshotTask(int taskId) {
+ try {
+ TaskSnapshot snapshot = mAnimationController.screenshotTask(taskId);
+ return snapshot != null ? new ThumbnailData(snapshot) : new ThumbnailData();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to screenshot task", e);
+ return new ThumbnailData();
+ }
+ }
+
+ public void setInputConsumerEnabled(boolean enabled) {
+ try {
+ mAnimationController.setInputConsumerEnabled(enabled);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to set input consumer enabled state", e);
+ }
+ }
+
+ public void finish(boolean toHome) {
+ try {
+ mAnimationController.finish(toHome);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to finish recents animation", e);
+ }
+ }
+} \ No newline at end of file
diff --git a/com/android/systemui/shared/system/RecentsAnimationListener.java b/com/android/systemui/shared/system/RecentsAnimationListener.java
new file mode 100644
index 00000000..bf6179d7
--- /dev/null
+++ b/com/android/systemui/shared/system/RecentsAnimationListener.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.systemui.shared.system;
+
+public interface RecentsAnimationListener {
+
+ /**
+ * Called when the animation into Recents can start. This call is made on the binder thread.
+ */
+ void onAnimationStart(RecentsAnimationControllerCompat controller,
+ RemoteAnimationTargetCompat[] apps);
+
+ /**
+ * Called when the animation into Recents was canceled. This call is made on the binder thread.
+ */
+ void onAnimationCanceled();
+} \ No newline at end of file
diff --git a/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java b/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
new file mode 100644
index 00000000..625b1de7
--- /dev/null
+++ b/com/android/systemui/shared/system/RemoteAnimationAdapterCompat.java
@@ -0,0 +1,71 @@
+/*
+ * 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 com.android.systemui.shared.system;
+
+import android.os.RemoteException;
+import android.util.Log;
+import android.view.IRemoteAnimationFinishedCallback;
+import android.view.IRemoteAnimationRunner;
+import android.view.RemoteAnimationAdapter;
+import android.view.RemoteAnimationTarget;
+
+/**
+ * @see RemoteAnimationAdapter
+ */
+public class RemoteAnimationAdapterCompat {
+
+ private final RemoteAnimationAdapter mWrapped;
+
+ public RemoteAnimationAdapterCompat(RemoteAnimationRunnerCompat runner, long duration,
+ long statusBarTransitionDelay) {
+ mWrapped = new RemoteAnimationAdapter(wrapRemoteAnimationRunner(runner), duration,
+ statusBarTransitionDelay);
+ }
+
+ RemoteAnimationAdapter getWrapped() {
+ return mWrapped;
+ }
+
+ private static IRemoteAnimationRunner.Stub wrapRemoteAnimationRunner(
+ RemoteAnimationRunnerCompat remoteAnimationAdapter) {
+ return new IRemoteAnimationRunner.Stub() {
+ @Override
+ public void onAnimationStart(RemoteAnimationTarget[] apps,
+ IRemoteAnimationFinishedCallback finishedCallback) throws RemoteException {
+ final RemoteAnimationTargetCompat[] appsCompat =
+ RemoteAnimationTargetCompat.wrap(apps);
+ final Runnable animationFinishedCallback = new Runnable() {
+ @Override
+ public void run() {
+ try {
+ finishedCallback.onAnimationFinished();
+ } catch (RemoteException e) {
+ Log.e("ActivityOptionsCompat", "Failed to call app controlled animation"
+ + " finished callback", e);
+ }
+ }
+ };
+ remoteAnimationAdapter.onAnimationStart(appsCompat, animationFinishedCallback);
+ }
+
+ @Override
+ public void onAnimationCancelled() throws RemoteException {
+ remoteAnimationAdapter.onAnimationCancelled();
+ }
+ };
+ }
+}
diff --git a/com/android/systemui/shared/system/RemoteAnimationDefinitionCompat.java b/com/android/systemui/shared/system/RemoteAnimationDefinitionCompat.java
new file mode 100644
index 00000000..5fff5feb
--- /dev/null
+++ b/com/android/systemui/shared/system/RemoteAnimationDefinitionCompat.java
@@ -0,0 +1,35 @@
+/*
+ * 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 com.android.systemui.shared.system;
+
+import android.view.RemoteAnimationDefinition;
+
+/**
+ * @see RemoteAnimationDefinition
+ */
+public class RemoteAnimationDefinitionCompat {
+
+ private final RemoteAnimationDefinition mWrapped = new RemoteAnimationDefinition();
+
+ public void addRemoteAnimation(int transition, RemoteAnimationAdapterCompat adapter) {
+ mWrapped.addRemoteAnimation(transition, adapter.getWrapped());
+ }
+
+ RemoteAnimationDefinition getWrapped() {
+ return mWrapped;
+ }
+}
diff --git a/android/arch/lifecycle/LifecycleFragment.java b/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
index c0da66b0..5a85df96 100644
--- a/android/arch/lifecycle/LifecycleFragment.java
+++ b/com/android/systemui/shared/system/RemoteAnimationRunnerCompat.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -11,16 +11,12 @@
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
- * limitations under the License.
+ * limitations under the License
*/
-package android.arch.lifecycle;
+package com.android.systemui.shared.system;
-import android.support.v4.app.Fragment;
-
-/**
- * @deprecated Use {@link Fragment} instead of it.
- */
-@Deprecated
-public class LifecycleFragment extends Fragment {
-}
+public interface RemoteAnimationRunnerCompat {
+ void onAnimationStart(RemoteAnimationTargetCompat[] apps, Runnable finishedCallback);
+ void onAnimationCancelled();
+} \ No newline at end of file
diff --git a/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java b/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
new file mode 100644
index 00000000..3871980a
--- /dev/null
+++ b/com/android/systemui/shared/system/RemoteAnimationTargetCompat.java
@@ -0,0 +1,59 @@
+/*
+ * 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 com.android.systemui.shared.system;
+
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.RemoteAnimationTarget;
+
+/**
+ * @see RemoteAnimationTarget
+ */
+public class RemoteAnimationTargetCompat {
+
+ public static final int MODE_OPENING = RemoteAnimationTarget.MODE_OPENING;
+ public static final int MODE_CLOSING = RemoteAnimationTarget.MODE_CLOSING;
+
+ public final int taskId;
+ public final int mode;
+ public final SurfaceControlCompat leash;
+ public final boolean isTranslucent;
+ public final Rect clipRect;
+ public final int prefixOrderIndex;
+ public final Point position;
+ public final Rect sourceContainerBounds;
+
+ public RemoteAnimationTargetCompat(RemoteAnimationTarget app) {
+ taskId = app.taskId;
+ mode = app.mode;
+ leash = new SurfaceControlCompat(app.leash);
+ isTranslucent = app.isTranslucent;
+ clipRect = app.clipRect;
+ position = app.position;
+ sourceContainerBounds = app.sourceContainerBounds;
+ prefixOrderIndex = app.prefixOrderIndex;
+ }
+
+ public static RemoteAnimationTargetCompat[] wrap(RemoteAnimationTarget[] apps) {
+ final RemoteAnimationTargetCompat[] appsCompat =
+ new RemoteAnimationTargetCompat[apps.length];
+ for (int i = 0; i < apps.length; i++) {
+ appsCompat[i] = new RemoteAnimationTargetCompat(apps[i]);
+ }
+ return appsCompat;
+ }
+} \ No newline at end of file
diff --git a/com/android/systemui/shared/system/SurfaceControlCompat.java b/com/android/systemui/shared/system/SurfaceControlCompat.java
new file mode 100644
index 00000000..cd12141c
--- /dev/null
+++ b/com/android/systemui/shared/system/SurfaceControlCompat.java
@@ -0,0 +1,27 @@
+/*
+ * 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 com.android.systemui.shared.system;
+
+import android.view.SurfaceControl;
+
+public class SurfaceControlCompat {
+ SurfaceControl mSurfaceControl;
+
+ public SurfaceControlCompat(SurfaceControl surfaceControl) {
+ mSurfaceControl = surfaceControl;
+ }
+}
diff --git a/com/android/systemui/shared/system/TransactionCompat.java b/com/android/systemui/shared/system/TransactionCompat.java
new file mode 100644
index 00000000..c82c5191
--- /dev/null
+++ b/com/android/systemui/shared/system/TransactionCompat.java
@@ -0,0 +1,108 @@
+/*
+ * 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 com.android.systemui.shared.system;
+
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.os.IBinder;
+import android.view.Surface;
+import android.view.SurfaceControl;
+import android.view.SurfaceControl.Transaction;
+
+public class TransactionCompat {
+
+ private final Transaction mTransaction;
+
+ private final float[] mTmpValues = new float[9];
+
+ public TransactionCompat() {
+ mTransaction = new Transaction();
+ }
+
+ public void apply() {
+ mTransaction.apply();
+ }
+
+ public TransactionCompat show(SurfaceControlCompat surfaceControl) {
+ mTransaction.show(surfaceControl.mSurfaceControl);
+ return this;
+ }
+
+ public TransactionCompat hide(SurfaceControlCompat surfaceControl) {
+ mTransaction.hide(surfaceControl.mSurfaceControl);
+ return this;
+ }
+
+ public TransactionCompat setPosition(SurfaceControlCompat surfaceControl, float x, float y) {
+ mTransaction.setPosition(surfaceControl.mSurfaceControl, x, y);
+ return this;
+ }
+
+ public TransactionCompat setSize(SurfaceControlCompat surfaceControl, int w, int h) {
+ mTransaction.setSize(surfaceControl.mSurfaceControl, w, h);
+ return this;
+ }
+
+ public TransactionCompat setLayer(SurfaceControlCompat surfaceControl, int z) {
+ mTransaction.setLayer(surfaceControl.mSurfaceControl, z);
+ return this;
+ }
+
+ public TransactionCompat setAlpha(SurfaceControlCompat surfaceControl, float alpha) {
+ mTransaction.setAlpha(surfaceControl.mSurfaceControl, alpha);
+ return this;
+ }
+
+ public TransactionCompat setMatrix(SurfaceControlCompat surfaceControl, float dsdx, float dtdx,
+ float dtdy, float dsdy) {
+ mTransaction.setMatrix(surfaceControl.mSurfaceControl, dsdx, dtdx, dtdy, dsdy);
+ return this;
+ }
+
+ public TransactionCompat setMatrix(SurfaceControlCompat surfaceControl, Matrix matrix) {
+ mTransaction.setMatrix(surfaceControl.mSurfaceControl, matrix, mTmpValues);
+ return this;
+ }
+
+ public TransactionCompat setWindowCrop(SurfaceControlCompat surfaceControl, Rect crop) {
+ mTransaction.setWindowCrop(surfaceControl.mSurfaceControl, crop);
+ return this;
+ }
+
+ public TransactionCompat setFinalCrop(SurfaceControlCompat surfaceControl, Rect crop) {
+ mTransaction.setFinalCrop(surfaceControl.mSurfaceControl, crop);
+ return this;
+ }
+
+ public TransactionCompat deferTransactionUntil(SurfaceControlCompat surfaceControl,
+ IBinder handle, long frameNumber) {
+ mTransaction.deferTransactionUntil(surfaceControl.mSurfaceControl, handle, frameNumber);
+ return this;
+ }
+
+ public TransactionCompat deferTransactionUntil(SurfaceControlCompat surfaceControl,
+ Surface barrier, long frameNumber) {
+ mTransaction.deferTransactionUntilSurface(surfaceControl.mSurfaceControl, barrier,
+ frameNumber);
+ return this;
+ }
+
+ public TransactionCompat setColor(SurfaceControlCompat surfaceControl, float[] color) {
+ mTransaction.setColor(surfaceControl.mSurfaceControl, color);
+ return this;
+ }
+} \ No newline at end of file
diff --git a/com/android/systemui/shared/system/WindowManagerWrapper.java b/com/android/systemui/shared/system/WindowManagerWrapper.java
index 225dbb4a..68400fc9 100644
--- a/com/android/systemui/shared/system/WindowManagerWrapper.java
+++ b/com/android/systemui/shared/system/WindowManagerWrapper.java
@@ -20,9 +20,10 @@ import static android.view.Display.DEFAULT_DISPLAY;
import android.graphics.Rect;
import android.os.Handler;
-import android.os.IRemoteCallback;
import android.os.RemoteException;
import android.util.Log;
+import android.view.RemoteAnimationAdapter;
+import android.view.WindowManager;
import android.view.WindowManagerGlobal;
import com.android.systemui.shared.recents.view.AppTransitionAnimationSpecsFuture;
@@ -32,6 +33,31 @@ public class WindowManagerWrapper {
private static final String TAG = "WindowManagerWrapper";
+ public static final int TRANSIT_UNSET = WindowManager.TRANSIT_UNSET;
+ public static final int TRANSIT_NONE = WindowManager.TRANSIT_NONE;
+ public static final int TRANSIT_ACTIVITY_OPEN = WindowManager.TRANSIT_ACTIVITY_OPEN;
+ public static final int TRANSIT_ACTIVITY_CLOSE = WindowManager.TRANSIT_ACTIVITY_CLOSE;
+ public static final int TRANSIT_TASK_OPEN = WindowManager.TRANSIT_TASK_OPEN;
+ public static final int TRANSIT_TASK_CLOSE = WindowManager.TRANSIT_TASK_CLOSE;
+ public static final int TRANSIT_TASK_TO_FRONT = WindowManager.TRANSIT_TASK_TO_FRONT;
+ public static final int TRANSIT_TASK_TO_BACK = WindowManager.TRANSIT_TASK_TO_BACK;
+ public static final int TRANSIT_WALLPAPER_CLOSE = WindowManager.TRANSIT_WALLPAPER_CLOSE;
+ public static final int TRANSIT_WALLPAPER_OPEN = WindowManager.TRANSIT_WALLPAPER_OPEN;
+ public static final int TRANSIT_WALLPAPER_INTRA_OPEN =
+ WindowManager.TRANSIT_WALLPAPER_INTRA_OPEN;
+ public static final int TRANSIT_WALLPAPER_INTRA_CLOSE =
+ WindowManager.TRANSIT_WALLPAPER_INTRA_CLOSE;
+ public static final int TRANSIT_TASK_OPEN_BEHIND = WindowManager.TRANSIT_TASK_OPEN_BEHIND;
+ public static final int TRANSIT_TASK_IN_PLACE = WindowManager.TRANSIT_TASK_IN_PLACE;
+ public static final int TRANSIT_ACTIVITY_RELAUNCH = WindowManager.TRANSIT_ACTIVITY_RELAUNCH;
+ public static final int TRANSIT_DOCK_TASK_FROM_RECENTS =
+ WindowManager.TRANSIT_DOCK_TASK_FROM_RECENTS;
+ public static final int TRANSIT_KEYGUARD_GOING_AWAY = WindowManager.TRANSIT_KEYGUARD_GOING_AWAY;
+ public static final int TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER =
+ WindowManager.TRANSIT_KEYGUARD_GOING_AWAY_ON_WALLPAPER;
+ public static final int TRANSIT_KEYGUARD_OCCLUDE = WindowManager.TRANSIT_KEYGUARD_OCCLUDE;
+ public static final int TRANSIT_KEYGUARD_UNOCCLUDE = WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE;
+
private static final WindowManagerWrapper sInstance = new WindowManagerWrapper();
public static WindowManagerWrapper getInstance() {
@@ -65,4 +91,14 @@ public class WindowManagerWrapper {
Log.w(TAG, "Failed to override pending app transition (multi-thumbnail future): ", e);
}
}
+
+ public void overridePendingAppTransitionRemote(
+ RemoteAnimationAdapterCompat remoteAnimationAdapter) {
+ try {
+ WindowManagerGlobal.getWindowManagerService().overridePendingAppTransitionRemote(
+ remoteAnimationAdapter.getWrapped());
+ } catch (RemoteException e) {
+ Log.w(TAG, "Failed to override pending app transition (remote): ", e);
+ }
+ }
}
diff --git a/com/android/systemui/statusbar/ActivatableNotificationView.java b/com/android/systemui/statusbar/ActivatableNotificationView.java
index ff0357a3..e59c703a 100644
--- a/com/android/systemui/statusbar/ActivatableNotificationView.java
+++ b/com/android/systemui/statusbar/ActivatableNotificationView.java
@@ -931,13 +931,6 @@ public abstract class ActivatableNotificationView extends ExpandableOutlineView
}
@Override
- public void setCurrentSidePaddings(float currentSidePaddings) {
- super.setCurrentSidePaddings(currentSidePaddings);
- mBackgroundNormal.setCurrentSidePaddings(currentSidePaddings);
- mBackgroundDimmed.setCurrentSidePaddings(currentSidePaddings);
- }
-
- @Override
protected boolean childNeedsClipping(View child) {
if (child instanceof NotificationBackgroundView && isClippingNeeded()) {
return true;
diff --git a/com/android/systemui/statusbar/CommandQueue.java b/com/android/systemui/statusbar/CommandQueue.java
index 8e1b1043..79e9f7b4 100644
--- a/com/android/systemui/statusbar/CommandQueue.java
+++ b/com/android/systemui/statusbar/CommandQueue.java
@@ -18,6 +18,7 @@ package com.android.systemui.statusbar;
import android.content.ComponentName;
import android.graphics.Rect;
+import android.hardware.fingerprint.IFingerprintDialogReceiver;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -83,6 +84,12 @@ public class CommandQueue extends IStatusBar.Stub {
private static final int MSG_SHOW_SHUTDOWN_UI = 36 << MSG_SHIFT;
private static final int MSG_SET_TOP_APP_HIDES_STATUS_BAR = 37 << MSG_SHIFT;
private static final int MSG_ROTATION_PROPOSAL = 38 << MSG_SHIFT;
+ private static final int MSG_FINGERPRINT_SHOW = 39 << MSG_SHIFT;
+ private static final int MSG_FINGERPRINT_AUTHENTICATED = 40 << MSG_SHIFT;
+ private static final int MSG_FINGERPRINT_HELP = 41 << MSG_SHIFT;
+ private static final int MSG_FINGERPRINT_ERROR = 42 << MSG_SHIFT;
+ private static final int MSG_FINGERPRINT_HIDE = 43 << MSG_SHIFT;
+ private static final int MSG_SHOW_CHARGING_ANIMATION = 44 << MSG_SHIFT;
public static final int FLAG_EXCLUDE_NONE = 0;
public static final int FLAG_EXCLUDE_SEARCH_PANEL = 1 << 0;
@@ -116,7 +123,7 @@ public class CommandQueue extends IStatusBar.Stub {
default void topAppWindowChanged(boolean visible) { }
default void setImeWindowStatus(IBinder token, int vis, int backDisposition,
boolean showImeSwitcher) { }
- default void showRecentApps(boolean triggeredFromAltTab, boolean fromHome) { }
+ default void showRecentApps(boolean triggeredFromAltTab) { }
default void hideRecentApps(boolean triggeredFromAltTab, boolean triggeredFromHomeKey) { }
default void toggleRecentApps() { }
default void toggleSplitScreen() { }
@@ -144,7 +151,15 @@ public class CommandQueue extends IStatusBar.Stub {
default void handleShowGlobalActionsMenu() { }
default void handleShowShutdownUi(boolean isReboot, String reason) { }
- default void onRotationProposal(int rotation) { }
+ default void showChargingAnimation(int batteryLevel) { }
+
+ default void onRotationProposal(int rotation, boolean isValid) { }
+
+ default void showFingerprintDialog(Bundle bundle, IFingerprintDialogReceiver receiver) { }
+ default void onFingerprintAuthenticated() { }
+ default void onFingerprintHelp(String message) { }
+ default void onFingerprintError(String error) { }
+ default void hideFingerprintDialog() { }
}
@VisibleForTesting
@@ -268,11 +283,11 @@ public class CommandQueue extends IStatusBar.Stub {
}
}
- public void showRecentApps(boolean triggeredFromAltTab, boolean fromHome) {
+ public void showRecentApps(boolean triggeredFromAltTab) {
synchronized (mLock) {
mHandler.removeMessages(MSG_SHOW_RECENT_APPS);
- mHandler.obtainMessage(MSG_SHOW_RECENT_APPS,
- triggeredFromAltTab ? 1 : 0, fromHome ? 1 : 0, null).sendToTarget();
+ mHandler.obtainMessage(MSG_SHOW_RECENT_APPS, triggeredFromAltTab ? 1 : 0, 0,
+ null).sendToTarget();
}
}
@@ -462,14 +477,60 @@ public class CommandQueue extends IStatusBar.Stub {
}
@Override
- public void onProposedRotationChanged(int rotation) {
+ public void showChargingAnimation(int batteryLevel) {
+ mHandler.removeMessages(MSG_SHOW_CHARGING_ANIMATION);
+ mHandler.obtainMessage(MSG_SHOW_CHARGING_ANIMATION, batteryLevel, 0)
+ .sendToTarget();
+ }
+
+ @Override
+ public void onProposedRotationChanged(int rotation, boolean isValid) {
synchronized (mLock) {
mHandler.removeMessages(MSG_ROTATION_PROPOSAL);
- mHandler.obtainMessage(MSG_ROTATION_PROPOSAL, rotation, 0,
+ mHandler.obtainMessage(MSG_ROTATION_PROPOSAL, rotation, isValid ? 1 : 0,
null).sendToTarget();
}
}
+ @Override
+ public void showFingerprintDialog(Bundle bundle, IFingerprintDialogReceiver receiver) {
+ synchronized (mLock) {
+ SomeArgs args = SomeArgs.obtain();
+ args.arg1 = bundle;
+ args.arg2 = receiver;
+ mHandler.obtainMessage(MSG_FINGERPRINT_SHOW, args)
+ .sendToTarget();
+ }
+ }
+
+ @Override
+ public void onFingerprintAuthenticated() {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_FINGERPRINT_AUTHENTICATED).sendToTarget();
+ }
+ }
+
+ @Override
+ public void onFingerprintHelp(String message) {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_FINGERPRINT_HELP, message).sendToTarget();
+ }
+ }
+
+ @Override
+ public void onFingerprintError(String error) {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_FINGERPRINT_ERROR, error).sendToTarget();
+ }
+ }
+
+ @Override
+ public void hideFingerprintDialog() {
+ synchronized (mLock) {
+ mHandler.obtainMessage(MSG_FINGERPRINT_HIDE).sendToTarget();
+ }
+ }
+
private final class H extends Handler {
private H(Looper l) {
super(l);
@@ -541,7 +602,7 @@ public class CommandQueue extends IStatusBar.Stub {
break;
case MSG_SHOW_RECENT_APPS:
for (int i = 0; i < mCallbacks.size(); i++) {
- mCallbacks.get(i).showRecentApps(msg.arg1 != 0, msg.arg2 != 0);
+ mCallbacks.get(i).showRecentApps(msg.arg1 != 0);
}
break;
case MSG_HIDE_RECENT_APPS:
@@ -668,7 +729,42 @@ public class CommandQueue extends IStatusBar.Stub {
break;
case MSG_ROTATION_PROPOSAL:
for (int i = 0; i < mCallbacks.size(); i++) {
- mCallbacks.get(i).onRotationProposal(msg.arg1);
+ mCallbacks.get(i).onRotationProposal(msg.arg1, msg.arg2 != 0);
+ }
+ break;
+ case MSG_FINGERPRINT_SHOW:
+ mHandler.removeMessages(MSG_FINGERPRINT_ERROR);
+ mHandler.removeMessages(MSG_FINGERPRINT_HELP);
+ mHandler.removeMessages(MSG_FINGERPRINT_AUTHENTICATED);
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).showFingerprintDialog(
+ (Bundle)((SomeArgs)msg.obj).arg1,
+ (IFingerprintDialogReceiver)((SomeArgs)msg.obj).arg2);
+ }
+ break;
+ case MSG_FINGERPRINT_AUTHENTICATED:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).onFingerprintAuthenticated();
+ }
+ break;
+ case MSG_FINGERPRINT_HELP:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).onFingerprintHelp((String) msg.obj);
+ }
+ break;
+ case MSG_FINGERPRINT_ERROR:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).onFingerprintError((String) msg.obj);
+ }
+ break;
+ case MSG_FINGERPRINT_HIDE:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).hideFingerprintDialog();
+ }
+ break;
+ case MSG_SHOW_CHARGING_ANIMATION:
+ for (int i = 0; i < mCallbacks.size(); i++) {
+ mCallbacks.get(i).showChargingAnimation(msg.arg1);
}
break;
}
diff --git a/com/android/systemui/statusbar/ExpandableNotificationRow.java b/com/android/systemui/statusbar/ExpandableNotificationRow.java
index f53eb489..5f4854ae 100644
--- a/com/android/systemui/statusbar/ExpandableNotificationRow.java
+++ b/com/android/systemui/statusbar/ExpandableNotificationRow.java
@@ -33,6 +33,7 @@ import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
+import android.service.notification.NotificationListenerService;
import android.service.notification.StatusBarNotification;
import android.util.AttributeSet;
import android.util.FloatProperty;
@@ -173,6 +174,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
private FalsingManager mFalsingManager;
private AboveShelfChangedListener mAboveShelfChangedListener;
private HeadsUpManager mHeadsUpManager;
+ private View mHelperButton;
private boolean mJustClicked;
private boolean mIconAnimationRunning;
@@ -387,6 +389,9 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
updateLimits();
updateIconVisibilities();
updateShelfIconColor();
+
+ showBlockingHelper(mEntry.userSentiment ==
+ NotificationListenerService.Ranking.USER_SENTIMENT_NEGATIVE);
}
@VisibleForTesting
@@ -1318,6 +1323,10 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
requestLayout();
}
+ public void showBlockingHelper(boolean show) {
+ mHelperButton.setVisibility(show ? View.VISIBLE : View.GONE);
+ }
+
@Override
protected void onFinishInflate() {
super.onFinishInflate();
@@ -1325,6 +1334,12 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
mPrivateLayout = (NotificationContentView) findViewById(R.id.expanded);
mLayouts = new NotificationContentView[] {mPrivateLayout, mPublicLayout};
+ final NotificationGutsManager gutsMan = Dependency.get(NotificationGutsManager.class);
+ mHelperButton = findViewById(R.id.helper);
+ mHelperButton.setOnClickListener(view -> {
+ doLongClickCallback();
+ });
+
for (NotificationContentView l : mLayouts) {
l.setExpandClickListener(mExpandClickListener);
l.setContainingNotification(this);
@@ -2362,16 +2377,17 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
NotificationContentView contentView = (NotificationContentView) child;
if (isClippingNeeded()) {
return true;
- } else if (!hasNoRoundingAndNoPadding() && contentView.shouldClipToSidePaddings()) {
+ } else if (!hasNoRounding()
+ && contentView.shouldClipToRounding(getCurrentTopRoundness() != 0.0f,
+ getCurrentBottomRoundness() != 0.0f)) {
return true;
}
} else if (child == mChildrenContainer) {
- if (isClippingNeeded() || ((isGroupExpanded() || isGroupExpansionChanging())
- && getClipBottomAmount() != 0.0f && getCurrentBottomRoundness() != 0.0f)) {
+ if (isClippingNeeded() || !hasNoRounding()) {
return true;
}
} else if (child instanceof NotificationGuts) {
- return !hasNoRoundingAndNoPadding();
+ return !hasNoRounding();
}
return super.childNeedsClipping(child);
}
@@ -2401,9 +2417,8 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
return super.getCustomClipPath(child);
}
- private boolean hasNoRoundingAndNoPadding() {
- return mCurrentSidePaddings == 0 && getCurrentBottomRoundness() == 0.0f
- && getCurrentTopRoundness() == 0.0f;
+ private boolean hasNoRounding() {
+ return getCurrentBottomRoundness() == 0.0f && getCurrentTopRoundness() == 0.0f;
}
public boolean isShowingAmbient() {
@@ -2418,20 +2433,6 @@ public class ExpandableNotificationRow extends ActivatableNotificationView
}
}
- @Override
- public void setCurrentSidePaddings(float currentSidePaddings) {
- if (mIsSummaryWithChildren) {
- List<ExpandableNotificationRow> notificationChildren =
- mChildrenContainer.getNotificationChildren();
- int size = notificationChildren.size();
- for (int i = 0; i < size; i++) {
- ExpandableNotificationRow row = notificationChildren.get(i);
- row.setCurrentSidePaddings(currentSidePaddings);
- }
- }
- super.setCurrentSidePaddings(currentSidePaddings);
- }
-
public static class NotificationViewState extends ExpandableViewState {
private final StackScrollState mOverallState;
diff --git a/com/android/systemui/statusbar/ExpandableOutlineView.java b/com/android/systemui/statusbar/ExpandableOutlineView.java
index db19d2f0..66b3a75f 100644
--- a/com/android/systemui/statusbar/ExpandableOutlineView.java
+++ b/com/android/systemui/statusbar/ExpandableOutlineView.java
@@ -58,6 +58,7 @@ public abstract class ExpandableOutlineView extends ExpandableView {
private static final Path EMPTY_PATH = new Path();
private final Rect mOutlineRect = new Rect();
+ private final Path mClipPath = new Path();
private boolean mCustomOutline;
private float mOutlineAlpha = -1f;
private float mOutlineRadius;
@@ -69,13 +70,14 @@ public abstract class ExpandableOutlineView extends ExpandableView {
private float mBottomRoundness;
private float mTopRoundness;
private int mBackgroundTop;
- protected int mCurrentSidePaddings;
/**
* {@code true} if the children views of the {@link ExpandableOutlineView} are translated when
* it is moved. Otherwise, the translation is set on the {@code ExpandableOutlineView} itself.
*/
protected boolean mShouldTranslateContents;
+ private boolean mClipRoundedToClipTopAmount;
+ private float mDistanceToTopRoundness = -1;
private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
@Override
@@ -83,9 +85,9 @@ public abstract class ExpandableOutlineView extends ExpandableView {
if (!mCustomOutline && mCurrentTopRoundness == 0.0f
&& mCurrentBottomRoundness == 0.0f && !mAlwaysRoundBothCorners) {
int translation = mShouldTranslateContents ? (int) getTranslation() : 0;
- int left = Math.max(translation + mCurrentSidePaddings, mCurrentSidePaddings);
+ int left = Math.max(translation, 0);
int top = mClipTopAmount + mBackgroundTop;
- int right = getWidth() - mCurrentSidePaddings + Math.min(translation, 0);
+ int right = getWidth() + Math.min(translation, 0);
int bottom = Math.max(getActualHeight() - mClipBottomAmount, top);
outline.setRect(left, top, right, bottom);
} else {
@@ -115,9 +117,9 @@ public abstract class ExpandableOutlineView extends ExpandableView {
if (!mCustomOutline) {
int translation = mShouldTranslateContents && !ignoreTranslation
? (int) getTranslation() : 0;
- left = Math.max(translation + mCurrentSidePaddings, mCurrentSidePaddings);
+ left = Math.max(translation, 0);
top = mClipTopAmount + mBackgroundTop;
- right = getWidth() - mCurrentSidePaddings + Math.min(translation, 0);
+ right = getWidth() + Math.min(translation, 0);
bottom = Math.max(getActualHeight(), top);
int intersectBottom = Math.max(getActualHeight() - mClipBottomAmount, top);
if (bottom != intersectBottom) {
@@ -135,8 +137,6 @@ public abstract class ExpandableOutlineView extends ExpandableView {
top = mOutlineRect.top;
right = mOutlineRect.right;
bottom = mOutlineRect.bottom;
- left = Math.max(mCurrentSidePaddings, left);
- right = Math.min(getWidth() - mCurrentSidePaddings, right);
}
height = bottom - top;
if (height == 0) {
@@ -162,15 +162,8 @@ public abstract class ExpandableOutlineView extends ExpandableView {
return roundedRectPath;
}
- protected Path getRoundedRectPath(int left, int top, int right, int bottom, float topRoundness,
- float bottomRoundness) {
- getRoundedRectPath(left, top, right, bottom, topRoundness, bottomRoundness,
- mTmpPath);
- return mTmpPath;
- }
-
- private void getRoundedRectPath(int left, int top, int right, int bottom, float topRoundness,
- float bottomRoundness, Path outPath) {
+ public static void getRoundedRectPath(int left, int top, int right, int bottom,
+ float topRoundness, float bottomRoundness, Path outPath) {
outPath.reset();
int width = right - left;
float topRoundnessX = topRoundness;
@@ -207,20 +200,50 @@ public abstract class ExpandableOutlineView extends ExpandableView {
@Override
protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
canvas.save();
+ Path intersectPath = null;
+ if (mClipRoundedToClipTopAmount) {
+ int left = 0;
+ int top = (int) (mClipTopAmount - mDistanceToTopRoundness);
+ int right = getWidth();
+ int bottom = (int) Math.max(getActualHeight() - mClipBottomAmount,
+ top + mOutlineRadius);
+ ExpandableOutlineView.getRoundedRectPath(left, top, right, bottom, mOutlineRadius,
+ 0.0f,
+ mClipPath);
+ intersectPath = mClipPath;
+ }
+ boolean clipped = false;
if (childNeedsClipping(child)) {
Path clipPath = getCustomClipPath(child);
if (clipPath == null) {
clipPath = getClipPath();
}
if (clipPath != null) {
+ if (intersectPath != null) {
+ clipPath.op(intersectPath, Path.Op.INTERSECT);
+ }
canvas.clipPath(clipPath);
+ clipped = true;
}
}
+ if (!clipped && intersectPath != null) {
+ canvas.clipPath(intersectPath);
+ }
boolean result = super.drawChild(canvas, child, drawingTime);
canvas.restore();
return result;
}
+ @Override
+ public void setDistanceToTopRoundness(float distanceToTopRoundness) {
+ super.setDistanceToTopRoundness(distanceToTopRoundness);
+ if (distanceToTopRoundness != mDistanceToTopRoundness) {
+ mClipRoundedToClipTopAmount = distanceToTopRoundness >= 0;
+ mDistanceToTopRoundness = distanceToTopRoundness;
+ invalidate();
+ }
+ }
+
protected boolean childNeedsClipping(View child) {
return false;
}
@@ -395,10 +418,4 @@ public abstract class ExpandableOutlineView extends ExpandableView {
public Path getCustomClipPath(View child) {
return null;
}
-
- public void setCurrentSidePaddings(float currentSidePaddings) {
- mCurrentSidePaddings = (int) currentSidePaddings;
- invalidateOutline();
- invalidate();
- }
}
diff --git a/com/android/systemui/statusbar/ExpandableView.java b/com/android/systemui/statusbar/ExpandableView.java
index f762513d..eafa825d 100644
--- a/com/android/systemui/statusbar/ExpandableView.java
+++ b/com/android/systemui/statusbar/ExpandableView.java
@@ -130,6 +130,14 @@ public abstract class ExpandableView extends FrameLayout {
}
}
+ /**
+ * Set the distance to the top roundness, from where we should start clipping a value above
+ * or equal to 0 is the effective distance, and if a value below 0 is received, there should
+ * be no clipping.
+ */
+ public void setDistanceToTopRoundness(float distanceToTopRoundness) {
+ }
+
public void setActualHeight(int actualHeight) {
setActualHeight(actualHeight, true /* notifyListeners */);
}
diff --git a/com/android/systemui/statusbar/KeyguardIndicationController.java b/com/android/systemui/statusbar/KeyguardIndicationController.java
index 569e58d7..0a12be4e 100644
--- a/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -52,6 +52,10 @@ import com.android.systemui.statusbar.policy.UserInfoController;
import com.android.systemui.util.wakelock.SettableWakeLock;
import com.android.systemui.util.wakelock.WakeLock;
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.text.NumberFormat;
+
/**
* Controls the indications and error messages shown on the Keyguard
*/
@@ -78,7 +82,7 @@ public class KeyguardIndicationController {
private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager;
private String mRestingIndication;
- private String mTransientIndication;
+ private CharSequence mTransientIndication;
private int mTransientTextColor;
private int mInitialTextColor;
private boolean mVisible;
@@ -87,6 +91,7 @@ public class KeyguardIndicationController {
private boolean mPowerCharged;
private int mChargingSpeed;
private int mChargingWattage;
+ private int mBatteryLevel;
private String mMessageToShowOnScreenOn;
private KeyguardUpdateMonitorCallback mUpdateMonitorCallback;
@@ -113,11 +118,9 @@ public class KeyguardIndicationController {
WakeLock wakeLock) {
mContext = context;
mIndicationArea = indicationArea;
- mTextView = (KeyguardIndicationTextView) indicationArea.findViewById(
- R.id.keyguard_indication_text);
+ mTextView = indicationArea.findViewById(R.id.keyguard_indication_text);
mInitialTextColor = mTextView != null ? mTextView.getCurrentTextColor() : Color.WHITE;
- mDisclosure = (KeyguardIndicationTextView) indicationArea.findViewById(
- R.id.keyguard_indication_enterprise_disclosure);
+ mDisclosure = indicationArea.findViewById(R.id.keyguard_indication_enterprise_disclosure);
mLockIcon = lockIcon;
mWakeLock = new SettableWakeLock(wakeLock);
@@ -246,14 +249,14 @@ public class KeyguardIndicationController {
/**
* Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}.
*/
- public void showTransientIndication(String transientIndication) {
+ public void showTransientIndication(CharSequence transientIndication) {
showTransientIndication(transientIndication, mInitialTextColor);
}
/**
* Shows {@param transientIndication} until it is hidden by {@link #hideTransientIndication}.
*/
- public void showTransientIndication(String transientIndication, int textColor) {
+ public void showTransientIndication(CharSequence transientIndication, int textColor) {
mTransientIndication = transientIndication;
mTransientTextColor = textColor;
mHandler.removeMessages(MSG_HIDE_TRANSIENT);
@@ -285,14 +288,18 @@ public class KeyguardIndicationController {
// Walk down a precedence-ordered list of what indication
// should be shown based on user or device state
if (mDozing) {
- // If we're dozing, never show a persistent indication.
+ mTextView.setTextColor(Color.WHITE);
if (!TextUtils.isEmpty(mTransientIndication)) {
// When dozing we ignore any text color and use white instead, because
// colors can be hard to read in low brightness.
- mTextView.setTextColor(Color.WHITE);
mTextView.switchIndication(mTransientIndication);
+ } else if (mPowerPluggedIn) {
+ String indication = computePowerIndication();
+ mTextView.switchIndication(indication);
} else {
- mTextView.switchIndication(null);
+ String percentage = NumberFormat.getPercentInstance()
+ .format(mBatteryLevel / 100f);
+ mTextView.switchIndication(percentage);
}
return;
}
@@ -409,6 +416,21 @@ public class KeyguardIndicationController {
updateDisclosure();
}
+ public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+ pw.println("KeyguardIndicationController:");
+ pw.println(" mTransientTextColor: " + Integer.toHexString(mTransientTextColor));
+ pw.println(" mInitialTextColor: " + Integer.toHexString(mInitialTextColor));
+ pw.println(" mPowerPluggedIn: " + mPowerPluggedIn);
+ pw.println(" mPowerCharged: " + mPowerCharged);
+ pw.println(" mChargingSpeed: " + mChargingSpeed);
+ pw.println(" mChargingWattage: " + mChargingWattage);
+ pw.println(" mMessageToShowOnScreenOn: " + mMessageToShowOnScreenOn);
+ pw.println(" mDozing: " + mDozing);
+ pw.println(" mBatteryLevel: " + mBatteryLevel);
+ pw.println(" mTextView.getText(): " + (mTextView == null ? null : mTextView.getText()));
+ pw.println(" computePowerIndication(): " + computePowerIndication());
+ }
+
protected class BaseKeyguardCallback extends KeyguardUpdateMonitorCallback {
public static final int HIDE_DELAY_MS = 5000;
private int mLastSuccessiveErrorMessage = -1;
@@ -422,6 +444,7 @@ public class KeyguardIndicationController {
mPowerCharged = status.isCharged();
mChargingWattage = status.maxChargingWattage;
mChargingSpeed = status.getChargingSpeed(mSlowThreshold, mFastThreshold);
+ mBatteryLevel = status.level;
updateIndication();
if (mDozing) {
if (!wasPluggedIn && mPowerPluggedIn) {
@@ -490,6 +513,12 @@ public class KeyguardIndicationController {
}
@Override
+ public void onTrustAgentErrorMessage(CharSequence message) {
+ int errorColor = Utils.getColorError(mContext);
+ showTransientIndication(message, errorColor);
+ }
+
+ @Override
public void onScreenTurnedOn() {
if (mMessageToShowOnScreenOn != null) {
int errorColor = Utils.getColorError(mContext);
diff --git a/com/android/systemui/statusbar/NotificationBackgroundView.java b/com/android/systemui/statusbar/NotificationBackgroundView.java
index 68cf51c0..45b35d01 100644
--- a/com/android/systemui/statusbar/NotificationBackgroundView.java
+++ b/com/android/systemui/statusbar/NotificationBackgroundView.java
@@ -41,7 +41,6 @@ public class NotificationBackgroundView extends View {
private int mClipBottomAmount;
private int mTintColor;
private float[] mCornerRadii = new float[8];
- private int mCurrentSidePaddings;
private boolean mBottomIsRounded;
private int mBackgroundTop;
private boolean mBottomAmountClips = true;
@@ -68,8 +67,7 @@ public class NotificationBackgroundView extends View {
if (mBottomIsRounded && mBottomAmountClips) {
bottom -= mClipBottomAmount;
}
- drawable.setBounds(mCurrentSidePaddings, mBackgroundTop,
- getWidth() - mCurrentSidePaddings, bottom);
+ drawable.setBounds(0, mBackgroundTop, getWidth(), bottom);
drawable.draw(canvas);
}
}
@@ -206,11 +204,6 @@ public class NotificationBackgroundView extends View {
}
}
- public void setCurrentSidePaddings(float currentSidePaddings) {
- mCurrentSidePaddings = (int) currentSidePaddings;
- invalidate();
- }
-
public void setBackgroundTop(int backgroundTop) {
mBackgroundTop = backgroundTop;
invalidate();
diff --git a/com/android/systemui/statusbar/NotificationContentView.java b/com/android/systemui/statusbar/NotificationContentView.java
index 39c21313..a4c17e36 100644
--- a/com/android/systemui/statusbar/NotificationContentView.java
+++ b/com/android/systemui/statusbar/NotificationContentView.java
@@ -22,6 +22,7 @@ import android.app.RemoteInput;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
+import android.provider.Settings;
import android.service.notification.StatusBarNotification;
import android.util.AttributeSet;
import android.util.Log;
@@ -31,6 +32,7 @@ import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.ImageView;
+import android.widget.LinearLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.NotificationColorUtil;
@@ -42,6 +44,7 @@ import com.android.systemui.statusbar.notification.NotificationUtils;
import com.android.systemui.statusbar.notification.NotificationViewWrapper;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.policy.RemoteInputView;
+import com.android.systemui.statusbar.policy.SmartReplyView;
/**
* A frame layout containing the actual payload of the notification, including the contracted,
@@ -72,6 +75,7 @@ public class NotificationContentView extends FrameLayout {
private RemoteInputView mExpandedRemoteInput;
private RemoteInputView mHeadsUpRemoteInput;
+ private SmartReplyView mExpandedSmartReplyView;
private NotificationViewWrapper mContractedWrapper;
private NotificationViewWrapper mExpandedWrapper;
@@ -136,7 +140,6 @@ public class NotificationContentView extends FrameLayout {
private int mClipBottomAmount;
private boolean mIsLowPriority;
private boolean mIsContentExpandable;
- private int mCustomViewSidePaddings;
public NotificationContentView(Context context, AttributeSet attrs) {
@@ -150,8 +153,6 @@ public class NotificationContentView extends FrameLayout {
R.dimen.min_notification_layout_height);
mNotificationContentMarginEnd = getResources().getDimensionPixelSize(
com.android.internal.R.dimen.notification_content_margin_end);
- mCustomViewSidePaddings = getResources().getDimensionPixelSize(
- R.dimen.notification_content_custom_view_side_padding);
}
public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight,
@@ -391,22 +392,6 @@ public class NotificationContentView extends FrameLayout {
mContractedWrapper = NotificationViewWrapper.wrap(getContext(), child,
mContainingNotification);
mContractedWrapper.setDark(mDark, false /* animate */, 0 /* delay */);
- updateMargins(child);
- }
-
- private void updateMargins(View child) {
- if (child == null) {
- return;
- }
- NotificationViewWrapper wrapper = getWrapperForView(child);
- boolean isCustomView = wrapper instanceof NotificationCustomViewWrapper;
- boolean needsMargins = isCustomView &&
- child.getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P;
- int padding = needsMargins ? mCustomViewSidePaddings : 0;
- MarginLayoutParams layoutParams = (MarginLayoutParams) child.getLayoutParams();
- layoutParams.setMarginStart(padding);
- layoutParams.setMarginEnd(padding);
- child.setLayoutParams(layoutParams);
}
private NotificationViewWrapper getWrapperForView(View child) {
@@ -456,7 +441,6 @@ public class NotificationContentView extends FrameLayout {
mExpandedChild = child;
mExpandedWrapper = NotificationViewWrapper.wrap(getContext(), child,
mContainingNotification);
- updateMargins(child);
}
public void setHeadsUpChild(View child) {
@@ -490,7 +474,6 @@ public class NotificationContentView extends FrameLayout {
mHeadsUpChild = child;
mHeadsUpWrapper = NotificationViewWrapper.wrap(getContext(), child,
mContainingNotification);
- updateMargins(child);
}
public void setAmbientChild(View child) {
@@ -1146,7 +1129,7 @@ public class NotificationContentView extends FrameLayout {
if (mAmbientChild != null) {
mAmbientWrapper.onContentUpdated(entry.row);
}
- applyRemoteInput(entry);
+ applyRemoteInputAndSmartReply(entry);
updateLegacy();
mForceSelectNextLayout = true;
setDark(mDark, false /* animate */, 0 /* delay */);
@@ -1178,20 +1161,34 @@ public class NotificationContentView extends FrameLayout {
}
}
- private void applyRemoteInput(final NotificationData.Entry entry) {
+ private void applyRemoteInputAndSmartReply(final NotificationData.Entry entry) {
if (mRemoteInputController == null) {
return;
}
+ boolean enableSmartReplies = Settings.Global.getInt(mContext.getContentResolver(),
+ Settings.Global.ENABLE_SMART_REPLIES_IN_NOTIFICATIONS, 0) != 0;
+
boolean hasRemoteInput = false;
+ RemoteInput remoteInputWithChoices = null;
+ PendingIntent pendingIntentWithChoices = null;
Notification.Action[] actions = entry.notification.getNotification().actions;
if (actions != null) {
for (Notification.Action a : actions) {
if (a.getRemoteInputs() != null) {
for (RemoteInput ri : a.getRemoteInputs()) {
- if (ri.getAllowFreeFormInput()) {
+ boolean showRemoteInputView = ri.getAllowFreeFormInput();
+ boolean showSmartReplyView = enableSmartReplies && ri.getChoices() != null
+ && ri.getChoices().length > 0;
+ if (showRemoteInputView) {
hasRemoteInput = true;
+ }
+ if (showSmartReplyView) {
+ remoteInputWithChoices = ri;
+ pendingIntentWithChoices = a.actionIntent;
+ }
+ if (showRemoteInputView || showSmartReplyView) {
break;
}
}
@@ -1199,6 +1196,11 @@ public class NotificationContentView extends FrameLayout {
}
}
+ applyRemoteInput(entry, hasRemoteInput);
+ applySmartReplyView(remoteInputWithChoices, pendingIntentWithChoices);
+ }
+
+ private void applyRemoteInput(NotificationData.Entry entry, boolean hasRemoteInput) {
View bigContentView = mExpandedChild;
if (bigContentView != null) {
mExpandedRemoteInput = applyRemoteInput(bigContentView, entry, hasRemoteInput,
@@ -1295,6 +1297,40 @@ public class NotificationContentView extends FrameLayout {
return null;
}
+ private void applySmartReplyView(RemoteInput remoteInput, PendingIntent pendingIntent) {
+ mExpandedSmartReplyView = mExpandedChild == null ?
+ null : applySmartReplyView(mExpandedChild, remoteInput, pendingIntent);
+ }
+
+ private SmartReplyView applySmartReplyView(
+ View view, RemoteInput remoteInput, PendingIntent pendingIntent) {
+ View smartReplyContainerCandidate = view.findViewById(
+ com.android.internal.R.id.smart_reply_container);
+ if (!(smartReplyContainerCandidate instanceof LinearLayout)) {
+ return null;
+ }
+ LinearLayout smartReplyContainer = (LinearLayout) smartReplyContainerCandidate;
+ if (remoteInput == null || pendingIntent == null) {
+ smartReplyContainer.setVisibility(View.GONE);
+ return null;
+ }
+ SmartReplyView smartReplyView = null;
+ if (smartReplyContainer.getChildCount() == 0) {
+ smartReplyView = SmartReplyView.inflate(mContext, smartReplyContainer);
+ smartReplyContainer.addView(smartReplyView);
+ } else if (smartReplyContainer.getChildCount() == 1) {
+ View child = smartReplyContainer.getChildAt(0);
+ if (child instanceof SmartReplyView) {
+ smartReplyView = (SmartReplyView) child;
+ }
+ }
+ if (smartReplyView != null) {
+ smartReplyView.setRepliesFromRemoteInput(remoteInput, pendingIntent);
+ smartReplyContainer.setVisibility(View.VISIBLE);
+ }
+ return smartReplyView;
+ }
+
public void closeRemoteInput() {
if (mHeadsUpRemoteInput != null) {
mHeadsUpRemoteInput.close();
@@ -1510,19 +1546,21 @@ public class NotificationContentView extends FrameLayout {
return false;
}
- public boolean shouldClipToSidePaddings() {
- boolean needsPaddings = shouldClipToSidePaddings(getVisibleType());
+ public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) {
+ boolean needsPaddings = shouldClipToRounding(getVisibleType(), topRounded, bottomRounded);
if (mUserExpanding) {
- needsPaddings |= shouldClipToSidePaddings(mTransformationStartVisibleType);
+ needsPaddings |= shouldClipToRounding(mTransformationStartVisibleType, topRounded,
+ bottomRounded);
}
return needsPaddings;
}
- private boolean shouldClipToSidePaddings(int visibleType) {
+ private boolean shouldClipToRounding(int visibleType, boolean topRounded,
+ boolean bottomRounded) {
NotificationViewWrapper visibleWrapper = getVisibleWrapper(visibleType);
if (visibleWrapper == null) {
return false;
}
- return visibleWrapper.shouldClipToSidePaddings();
+ return visibleWrapper.shouldClipToRounding(topRounded, bottomRounded);
}
}
diff --git a/com/android/systemui/statusbar/NotificationData.java b/com/android/systemui/statusbar/NotificationData.java
index d0417b59..127f3f91 100644
--- a/com/android/systemui/statusbar/NotificationData.java
+++ b/com/android/systemui/statusbar/NotificationData.java
@@ -67,6 +67,7 @@ public class NotificationData {
public static final class Entry {
private static final long LAUNCH_COOLDOWN = 2000;
+ private static final long REMOTE_INPUT_COOLDOWN = 500;
private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN;
private static final int COLOR_INVALID = 1;
public String key;
@@ -86,10 +87,14 @@ public class NotificationData {
public RemoteViews cachedAmbientContentView;
public CharSequence remoteInputText;
public List<SnoozeCriterion> snoozeCriteria;
+ public int userSentiment = Ranking.USER_SENTIMENT_NEUTRAL;
+
private int mCachedContrastColor = COLOR_INVALID;
private int mCachedContrastColorIsFor = COLOR_INVALID;
private InflationTask mRunningTask = null;
private Throwable mDebugThrowable;
+ public CharSequence remoteInputTextWhenReset;
+ public long lastRemoteInputSent = NOT_LAUNCHED_YET;
public Entry(StatusBarNotification n) {
this.key = n.getKey();
@@ -130,6 +135,10 @@ public class NotificationData {
return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN;
}
+ public boolean hasJustSentRemoteInput() {
+ return SystemClock.elapsedRealtime() < lastRemoteInputSent + REMOTE_INPUT_COOLDOWN;
+ }
+
/**
* Create the icons for a notification
* @param context the context to create the icons with
@@ -263,6 +272,11 @@ public class NotificationData {
public Throwable getDebugThrowable() {
return mDebugThrowable;
}
+
+ public void onRemoteInputInserted() {
+ lastRemoteInputSent = NOT_LAUNCHED_YET;
+ remoteInputTextWhenReset = null;
+ }
}
private final ArrayMap<String, Entry> mEntries = new ArrayMap<>();
@@ -463,6 +477,7 @@ public class NotificationData {
}
entry.channel = getChannel(entry.key);
entry.snoozeCriteria = getSnoozeCriteria(entry.key);
+ entry.userSentiment = mTmpRanking.getUserSentiment();
}
}
}
diff --git a/com/android/systemui/statusbar/NotificationEntryManager.java b/com/android/systemui/statusbar/NotificationEntryManager.java
index 6bbd09f7..7360486a 100644
--- a/com/android/systemui/statusbar/NotificationEntryManager.java
+++ b/com/android/systemui/statusbar/NotificationEntryManager.java
@@ -35,6 +35,7 @@ import android.provider.Settings;
import android.service.notification.NotificationListenerService;
import android.service.notification.NotificationStats;
import android.service.notification.StatusBarNotification;
+import android.text.TextUtils;
import android.util.ArraySet;
import android.util.EventLog;
import android.util.Log;
@@ -462,7 +463,8 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
mMediaManager.onNotificationRemoved(key);
NotificationData.Entry entry = mNotificationData.get(key);
- if (FORCE_REMOTE_INPUT_HISTORY && mRemoteInputManager.getController().isSpinning(key)
+ if (FORCE_REMOTE_INPUT_HISTORY
+ && shouldKeepForRemoteInput(entry)
&& entry.row != null && !entry.row.isDismissed()) {
StatusBarNotification sbn = entry.notification;
@@ -477,7 +479,11 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
newHistory = new CharSequence[oldHistory.length + 1];
System.arraycopy(oldHistory, 0, newHistory, 1, oldHistory.length);
}
- newHistory[0] = String.valueOf(entry.remoteInputText);
+ CharSequence remoteInputText = entry.remoteInputText;
+ if (TextUtils.isEmpty(remoteInputText)) {
+ remoteInputText = entry.remoteInputTextWhenReset;
+ }
+ newHistory[0] = String.valueOf(remoteInputText);
b.setRemoteInputHistory(newHistory);
Notification newNotification = b.build();
@@ -492,6 +498,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
sbn.getId(), sbn.getTag(), sbn.getUid(), sbn.getInitialPid(),
newNotification, sbn.getUser(), sbn.getOverrideGroupKey(), sbn.getPostTime());
boolean updated = false;
+ entry.onRemoteInputInserted();
try {
updateNotificationInternal(newSbn, null);
updated = true;
@@ -539,6 +546,19 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
mCallback.onNotificationRemoved(key, old);
}
+ private boolean shouldKeepForRemoteInput(NotificationData.Entry entry) {
+ if (entry == null) {
+ return false;
+ }
+ if (mRemoteInputManager.getController().isSpinning(entry.key)) {
+ return true;
+ }
+ if (entry.hasJustSentRemoteInput()) {
+ return true;
+ }
+ return false;
+ }
+
private StatusBarNotification removeNotificationViews(String key,
NotificationListenerService.RankingMap ranking) {
NotificationData.Entry entry = mNotificationData.remove(key, ranking);
@@ -596,8 +616,7 @@ public class NotificationEntryManager implements Dumpable, NotificationInflater.
&& entry.row.getGuts() == mGutsManager.getExposedGuts();
entry.row.onDensityOrFontScaleChanged();
if (exposedGuts) {
- mGutsManager.setExposedGuts(entry.row.getGuts());
- mGutsManager.bindGuts(entry.row);
+ mGutsManager.onDensityOrFontScaleChanged(entry.row);
}
}
}
diff --git a/com/android/systemui/statusbar/NotificationGuts.java b/com/android/systemui/statusbar/NotificationGuts.java
index c4024a57..52776d7e 100644
--- a/com/android/systemui/statusbar/NotificationGuts.java
+++ b/com/android/systemui/statusbar/NotificationGuts.java
@@ -23,6 +23,7 @@ import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Handler;
+import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewAnimationUtils;
@@ -187,6 +188,12 @@ public class NotificationGuts extends FrameLayout {
}
}
+ public void openControls(
+ int x, int y, boolean needsFalsingProtection, @Nullable Runnable onAnimationEnd) {
+ animateOpen(x, y, onAnimationEnd);
+ setExposed(true /* exposed */, needsFalsingProtection);
+ }
+
public void closeControls(boolean leavebehinds, boolean controls, int x, int y, boolean force) {
if (mGutsContent != null) {
if (mGutsContent.isLeavebehind() && leavebehinds) {
@@ -214,6 +221,27 @@ public class NotificationGuts extends FrameLayout {
}
}
+ private void animateOpen(int x, int y, @Nullable Runnable onAnimationEnd) {
+ final double horz = Math.max(getWidth() - x, x);
+ final double vert = Math.max(getHeight() - y, y);
+ final float r = (float) Math.hypot(horz, vert);
+
+ final Animator a
+ = ViewAnimationUtils.createCircularReveal(this, x, y, 0, r);
+ a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
+ a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
+ a.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ super.onAnimationEnd(animation);
+ if (onAnimationEnd != null) {
+ onAnimationEnd.run();
+ }
+ }
+ });
+ a.start();
+ }
+
private void animateClose(int x, int y) {
if (x == -1 || y == -1) {
x = (getLeft() + getRight()) / 2;
@@ -279,7 +307,7 @@ public class NotificationGuts extends FrameLayout {
}
}
- public void setExposed(boolean exposed, boolean needsFalsingProtection) {
+ private void setExposed(boolean exposed, boolean needsFalsingProtection) {
final boolean wasExposed = mExposed;
mExposed = exposed;
mNeedsFalsingProtection = needsFalsingProtection;
diff --git a/com/android/systemui/statusbar/NotificationGutsManager.java b/com/android/systemui/statusbar/NotificationGutsManager.java
index 87ad6f6b..9d8892da 100644
--- a/com/android/systemui/statusbar/NotificationGutsManager.java
+++ b/com/android/systemui/statusbar/NotificationGutsManager.java
@@ -15,8 +15,6 @@
*/
package com.android.systemui.statusbar;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
import android.app.INotificationManager;
import android.app.NotificationChannel;
import android.content.Context;
@@ -32,17 +30,14 @@ import android.util.ArraySet;
import android.util.Log;
import android.view.HapticFeedbackConstants;
import android.view.View;
-import android.view.ViewAnimationUtils;
import android.view.accessibility.AccessibilityManager;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto;
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.StackStateAnimator;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -112,6 +107,11 @@ public class NotificationGutsManager implements Dumpable {
mKeyToRemoveOnGutsClosed = keyToRemoveOnGutsClosed;
}
+ public void onDensityOrFontScaleChanged(ExpandableNotificationRow row) {
+ setExposedGuts(row.getGuts());
+ bindGuts(row);
+ }
+
private void saveAndCloseNotificationMenu(
ExpandableNotificationRow row, NotificationGuts guts, View done) {
guts.resetFalsingCheck();
@@ -229,10 +229,9 @@ public class NotificationGutsManager implements Dumpable {
}
}
try {
- info.bindNotification(pmUser, iNotificationManager, pkg, new ArrayList(channels),
- row.getEntry().channel.getImportance(), sbn, onSettingsClick,
- onAppSettingsClick, onDoneClick, mCheckSaveListener,
- mNonBlockablePkgs);
+ info.bindNotification(pmUser, iNotificationManager, pkg, row.getEntry().channel,
+ channels.size(), sbn, mCheckSaveListener, onSettingsClick,
+ onAppSettingsClick, mNonBlockablePkgs);
} catch (RemoteException e) {
Log.e(TAG, e.toString());
}
@@ -271,7 +270,7 @@ public class NotificationGutsManager implements Dumpable {
}
/**
- * Opens guts on the given ExpandableNotificationRow |v|.
+ * Opens guts on the given ExpandableNotificationRow |v|.
*
* @param v ExpandableNotificationRow to open guts on
* @param x x coordinate of origin of circular reveal
@@ -327,26 +326,15 @@ public class NotificationGutsManager implements Dumpable {
true /* removeControls */, -1 /* x */, -1 /* y */,
false /* resetMenu */);
guts.setVisibility(View.VISIBLE);
- final double horz = Math.max(guts.getWidth() - x, x);
- final double vert = Math.max(guts.getHeight() - y, y);
- final float r = (float) Math.hypot(horz, vert);
- final Animator a
- = ViewAnimationUtils.createCircularReveal(guts, x, y, 0, r);
- a.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
- a.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN);
- a.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- super.onAnimationEnd(animation);
- // Move the notification view back over the menu
- row.resetTranslation();
- }
- });
- a.start();
+
final boolean needsFalsingProtection =
(mPresenter.isPresenterLocked() &&
!mAccessibilityManager.isTouchExplorationEnabled());
- guts.setExposed(true /* exposed */, needsFalsingProtection);
+ guts.openControls(x, y, needsFalsingProtection, () -> {
+ // Move the notification view back over the menu
+ row.resetTranslation();
+ });
+
row.closeRemoteInput();
mListContainer.onHeightChanged(row, true /* needsAnimation */);
mNotificationGutsExposed = guts;
diff --git a/com/android/systemui/statusbar/NotificationHeaderUtil.java b/com/android/systemui/statusbar/NotificationHeaderUtil.java
index 43018174..11270755 100644
--- a/com/android/systemui/statusbar/NotificationHeaderUtil.java
+++ b/com/android/systemui/statusbar/NotificationHeaderUtil.java
@@ -128,6 +128,7 @@ public class NotificationHeaderUtil {
mComparators.add(HeaderProcessor.forTextView(mRow,
com.android.internal.R.id.header_text));
mDividers.add(com.android.internal.R.id.header_text_divider);
+ mDividers.add(com.android.internal.R.id.header_text_secondary_divider);
mDividers.add(com.android.internal.R.id.time_divider);
}
diff --git a/com/android/systemui/statusbar/NotificationInfo.java b/com/android/systemui/statusbar/NotificationInfo.java
index 8d1bb5fe..6279fdc4 100644
--- a/com/android/systemui/statusbar/NotificationInfo.java
+++ b/com/android/systemui/statusbar/NotificationInfo.java
@@ -18,6 +18,10 @@ package com.android.systemui.statusbar;
import static android.app.NotificationManager.IMPORTANCE_NONE;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
import android.app.INotificationManager;
import android.app.Notification;
import android.app.NotificationChannel;
@@ -38,12 +42,12 @@ import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.widget.ImageView;
import android.widget.LinearLayout;
-import android.widget.Switch;
import android.widget.TextView;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.settingslib.Utils;
+import com.android.systemui.Interpolators;
import com.android.systemui.R;
import java.lang.IllegalArgumentException;
@@ -57,25 +61,37 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
private static final String TAG = "InfoGuts";
private INotificationManager mINotificationManager;
+ private PackageManager mPm;
+
private String mPkg;
private String mAppName;
private int mAppUid;
- private List<NotificationChannel> mNotificationChannels;
+ private int mNumNotificationChannels;
private NotificationChannel mSingleNotificationChannel;
+ private int mStartingUserImportance;
+ private int mChosenImportance;
private boolean mIsSingleDefaultChannel;
+ private boolean mNonblockable;
private StatusBarNotification mSbn;
- private int mStartingUserImportance;
+ private AnimatorSet mExpandAnimation;
- private TextView mNumChannelsView;
- private View mChannelDisabledView;
- private TextView mSettingsLinkView;
- private Switch mChannelEnabledSwitch;
private CheckSaveListener mCheckSaveListener;
+ private OnSettingsClickListener mOnSettingsClickListener;
private OnAppSettingsClickListener mAppSettingsClickListener;
- private PackageManager mPm;
-
private NotificationGuts mGutsContainer;
+ private OnClickListener mOnKeepShowing = v -> {
+ closeControls(v);
+ };
+
+ private OnClickListener mOnStopNotifications = v -> {
+ swapContent(false);
+ };
+
+ private OnClickListener mOnUndo = v -> {
+ swapContent(true);
+ };
+
public NotificationInfo(Context context, AttributeSet attrs) {
super(context, attrs);
}
@@ -98,141 +114,93 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
public void bindNotification(final PackageManager pm,
final INotificationManager iNotificationManager,
final String pkg,
- final List<NotificationChannel> notificationChannels,
- int startingUserImportance,
+ final NotificationChannel notificationChannel,
+ final int numChannels,
final StatusBarNotification sbn,
- OnSettingsClickListener onSettingsClick,
- OnAppSettingsClickListener onAppSettingsClick,
- OnClickListener onDoneClick,
- CheckSaveListener checkSaveListener,
+ final CheckSaveListener checkSaveListener,
+ final OnSettingsClickListener onSettingsClick,
+ final OnAppSettingsClickListener onAppSettingsClick,
final Set<String> nonBlockablePkgs)
throws RemoteException {
mINotificationManager = iNotificationManager;
mPkg = pkg;
- mNotificationChannels = notificationChannels;
- mCheckSaveListener = checkSaveListener;
+ mNumNotificationChannels = numChannels;
mSbn = sbn;
mPm = pm;
mAppSettingsClickListener = onAppSettingsClick;
- mStartingUserImportance = startingUserImportance;
mAppName = mPkg;
- Drawable pkgicon = null;
- CharSequence channelNameText = "";
- ApplicationInfo info = null;
- try {
- info = pm.getApplicationInfo(mPkg,
- PackageManager.MATCH_UNINSTALLED_PACKAGES
- | PackageManager.MATCH_DISABLED_COMPONENTS
- | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
- | PackageManager.MATCH_DIRECT_BOOT_AWARE);
- if (info != null) {
- mAppUid = sbn.getUid();
- mAppName = String.valueOf(pm.getApplicationLabel(info));
- pkgicon = pm.getApplicationIcon(info);
- }
- } catch (PackageManager.NameNotFoundException e) {
- // app is gone, just show package name and generic icon
- pkgicon = pm.getDefaultActivityIcon();
- }
- ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(pkgicon);
+ mCheckSaveListener = checkSaveListener;
+ mOnSettingsClickListener = onSettingsClick;
+ mSingleNotificationChannel = notificationChannel;
+ mStartingUserImportance = mChosenImportance = mSingleNotificationChannel.getImportance();
- int numTotalChannels = iNotificationManager.getNumNotificationChannelsForPackage(
+ int numTotalChannels = mINotificationManager.getNumNotificationChannelsForPackage(
pkg, mAppUid, false /* includeDeleted */);
- if (mNotificationChannels.isEmpty()) {
+ if (mNumNotificationChannels == 0) {
throw new IllegalArgumentException("bindNotification requires at least one channel");
} else {
- if (mNotificationChannels.size() == 1) {
- mSingleNotificationChannel = mNotificationChannels.get(0);
- // Special behavior for the Default channel if no other channels have been defined.
- mIsSingleDefaultChannel =
- (mSingleNotificationChannel.getId()
- .equals(NotificationChannel.DEFAULT_CHANNEL_ID) &&
- numTotalChannels <= 1);
- } else {
- mSingleNotificationChannel = null;
- mIsSingleDefaultChannel = false;
- }
+ // Special behavior for the Default channel if no other channels have been defined.
+ mIsSingleDefaultChannel = mNumNotificationChannels == 1
+ && mSingleNotificationChannel.getId()
+ .equals(NotificationChannel.DEFAULT_CHANNEL_ID)
+ && numTotalChannels <= 1;
}
- boolean nonBlockable = false;
try {
final PackageInfo pkgInfo = pm.getPackageInfo(pkg, PackageManager.GET_SIGNATURES);
if (Utils.isSystemPackage(getResources(), pm, pkgInfo)) {
- final int numChannels = mNotificationChannels.size();
- for (int i = 0; i < numChannels; i++) {
- final NotificationChannel notificationChannel = mNotificationChannels.get(i);
- // If any of the system channels is not blockable, the bundle is nonblockable
- if (!notificationChannel.isBlockableSystem()) {
- nonBlockable = true;
- break;
- }
+ if (mSingleNotificationChannel != null
+ && !mSingleNotificationChannel.isBlockableSystem()) {
+ mNonblockable = true;
}
}
} catch (PackageManager.NameNotFoundException e) {
// unlikely.
}
if (nonBlockablePkgs != null) {
- nonBlockable |= nonBlockablePkgs.contains(pkg);
+ mNonblockable |= nonBlockablePkgs.contains(pkg);
}
+ mNonblockable |= (mNumNotificationChannels > 1);
- String channelsDescText;
- mNumChannelsView = findViewById(R.id.num_channels_desc);
- if (nonBlockable) {
- channelsDescText = mContext.getString(R.string.notification_unblockable_desc);
- } else if (mIsSingleDefaultChannel) {
- channelsDescText = mContext.getString(R.string.notification_default_channel_desc);
- } else {
- switch (mNotificationChannels.size()) {
- case 1:
- channelsDescText = String.format(mContext.getResources().getQuantityString(
- R.plurals.notification_num_channels_desc, numTotalChannels),
- numTotalChannels);
- break;
- case 2:
- channelsDescText = mContext.getString(
- R.string.notification_channels_list_desc_2,
- mNotificationChannels.get(0).getName(),
- mNotificationChannels.get(1).getName());
- break;
- default:
- final int numOthers = mNotificationChannels.size() - 2;
- channelsDescText = String.format(
- mContext.getResources().getQuantityString(
- R.plurals.notification_channels_list_desc_2_and_others,
- numOthers),
- mNotificationChannels.get(0).getName(),
- mNotificationChannels.get(1).getName(),
- numOthers);
+ bindHeader();
+ bindPrompt();
+ bindButtons();
+ }
+
+ private void bindHeader() throws RemoteException {
+ // Package name
+ Drawable pkgicon = null;
+ ApplicationInfo info;
+ try {
+ info = mPm.getApplicationInfo(mPkg,
+ PackageManager.MATCH_UNINSTALLED_PACKAGES
+ | PackageManager.MATCH_DISABLED_COMPONENTS
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE);
+ if (info != null) {
+ mAppUid = mSbn.getUid();
+ mAppName = String.valueOf(mPm.getApplicationLabel(info));
+ pkgicon = mPm.getApplicationIcon(info);
}
+ } catch (PackageManager.NameNotFoundException e) {
+ // app is gone, just show package name and generic icon
+ pkgicon = mPm.getDefaultActivityIcon();
}
- mNumChannelsView.setText(channelsDescText);
-
- if (mSingleNotificationChannel == null) {
- // Multiple channels don't use a channel name for the title.
- channelNameText = mContext.getString(R.string.notification_num_channels,
- mNotificationChannels.size());
- } else if (mIsSingleDefaultChannel || nonBlockable) {
- // If this is the default channel or the app is unblockable,
- // don't use our channel-specific text.
- channelNameText = mContext.getString(R.string.notification_header_default_channel);
- } else {
- channelNameText = mSingleNotificationChannel.getName();
- }
+ ((ImageView) findViewById(R.id.pkgicon)).setImageDrawable(pkgicon);
((TextView) findViewById(R.id.pkgname)).setText(mAppName);
- ((TextView) findViewById(R.id.channel_name)).setText(channelNameText);
// Set group information if this channel has an associated group.
CharSequence groupName = null;
if (mSingleNotificationChannel != null && mSingleNotificationChannel.getGroup() != null) {
final NotificationChannelGroup notificationChannelGroup =
- iNotificationManager.getNotificationChannelGroupForPackage(
- mSingleNotificationChannel.getGroup(), pkg, mAppUid);
+ mINotificationManager.getNotificationChannelGroupForPackage(
+ mSingleNotificationChannel.getGroup(), mPkg, mAppUid);
if (notificationChannelGroup != null) {
groupName = notificationChannelGroup.getName();
}
}
- TextView groupNameView = ((TextView) findViewById(R.id.group_name));
- TextView groupDividerView = ((TextView) findViewById(R.id.pkg_group_divider));
+ TextView groupNameView = findViewById(R.id.group_name);
+ TextView groupDividerView = findViewById(R.id.pkg_group_divider);
if (groupName != null) {
groupNameView.setText(groupName);
groupNameView.setVisibility(View.VISIBLE);
@@ -242,53 +210,55 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
groupDividerView.setVisibility(View.GONE);
}
- bindButtons(nonBlockable);
-
- // Top-level importance group
- mChannelDisabledView = findViewById(R.id.channel_disabled);
- updateSecondaryText();
-
// Settings button.
- final TextView settingsButton = (TextView) findViewById(R.id.more_settings);
- if (mAppUid >= 0 && onSettingsClick != null) {
+ final View settingsButton = findViewById(R.id.info);
+ if (mAppUid >= 0 && mOnSettingsClickListener != null) {
settingsButton.setVisibility(View.VISIBLE);
final int appUidF = mAppUid;
settingsButton.setOnClickListener(
(View view) -> {
- onSettingsClick.onClick(view, mSingleNotificationChannel, appUidF);
+ mOnSettingsClickListener.onClick(view,
+ mNumNotificationChannels > 1 ? null : mSingleNotificationChannel,
+ appUidF);
});
- if (numTotalChannels <= 1 || nonBlockable) {
- settingsButton.setText(R.string.notification_more_settings);
- } else {
- settingsButton.setText(R.string.notification_all_categories);
- }
} else {
settingsButton.setVisibility(View.GONE);
}
+ }
- // Done button.
- final TextView doneButton = (TextView) findViewById(R.id.done);
- doneButton.setText(R.string.notification_done);
- doneButton.setOnClickListener(onDoneClick);
+ private void bindPrompt() {
+ final TextView channelName = findViewById(R.id.channel_name);
+ final TextView blockPrompt = findViewById(R.id.block_prompt);
+ if (mNonblockable) {
+ if (mIsSingleDefaultChannel || mNumNotificationChannels > 1) {
+ channelName.setVisibility(View.GONE);
+ } else {
+ channelName.setText(mSingleNotificationChannel.getName());
+ }
- // Optional settings link
- updateAppSettingsLink();
+ blockPrompt.setText(R.string.notification_unblockable_desc);
+ } else {
+ if (mIsSingleDefaultChannel || mNumNotificationChannels > 1) {
+ channelName.setVisibility(View.GONE);
+ blockPrompt.setText(R.string.inline_keep_showing_app);
+ } else {
+ channelName.setText(mSingleNotificationChannel.getName());
+ blockPrompt.setText(R.string.inline_keep_showing);
+ }
+ }
}
private boolean hasImportanceChanged() {
- return mSingleNotificationChannel != null &&
- mChannelEnabledSwitch != null &&
- mStartingUserImportance != getSelectedImportance();
+ return mSingleNotificationChannel != null && mStartingUserImportance != mChosenImportance;
}
private void saveImportance() {
- if (!hasImportanceChanged()) {
+ if (mNonblockable || !hasImportanceChanged()) {
return;
}
- final int selectedImportance = getSelectedImportance();
MetricsLogger.action(mContext, MetricsEvent.ACTION_SAVE_IMPORTANCE,
- selectedImportance - mStartingUserImportance);
- mSingleNotificationChannel.setImportance(selectedImportance);
+ mChosenImportance - mStartingUserImportance);
+ mSingleNotificationChannel.setImportance(mChosenImportance);
mSingleNotificationChannel.lockFields(NotificationChannel.USER_LOCKED_IMPORTANCE);
try {
mINotificationManager.updateNotificationChannelForPackage(
@@ -298,30 +268,78 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
}
}
- private int getSelectedImportance() {
- if (!mChannelEnabledSwitch.isChecked()) {
- return IMPORTANCE_NONE;
+ private void bindButtons() {
+ View block = findViewById(R.id.block);
+ block.setOnClickListener(mOnStopNotifications);
+ TextView keep = findViewById(R.id.keep);
+ keep.setOnClickListener(mOnKeepShowing);
+ findViewById(R.id.undo).setOnClickListener(mOnUndo);
+
+ if (mNonblockable) {
+ keep.setText(R.string.notification_done);
+ block.setVisibility(GONE);
+ }
+
+ // app settings link
+ TextView settingsLinkView = findViewById(R.id.app_settings);
+ Intent settingsIntent = getAppSettingsIntent(mPm, mPkg, mSingleNotificationChannel,
+ mSbn.getId(), mSbn.getTag());
+ if (settingsIntent != null
+ && !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) {
+ settingsLinkView.setVisibility(View.VISIBLE);
+ settingsLinkView.setText(mContext.getString(R.string.notification_app_settings,
+ mSbn.getNotification().getSettingsText()));
+ settingsLinkView.setOnClickListener((View view) -> {
+ mAppSettingsClickListener.onClick(view, settingsIntent);
+ });
} else {
- return mStartingUserImportance;
+ settingsLinkView.setVisibility(View.GONE);
}
}
- private void bindButtons(final boolean nonBlockable) {
- // Enabled Switch
- mChannelEnabledSwitch = (Switch) findViewById(R.id.channel_enabled_switch);
- mChannelEnabledSwitch.setChecked(
- mStartingUserImportance != IMPORTANCE_NONE);
- final boolean visible = !nonBlockable && mSingleNotificationChannel != null;
- mChannelEnabledSwitch.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
-
- // Callback when checked.
- mChannelEnabledSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
- if (mGutsContainer != null) {
- mGutsContainer.resetFalsingCheck();
+ private void swapContent(boolean showPrompt) {
+ if (mExpandAnimation != null) {
+ mExpandAnimation.cancel();
+ }
+
+ if (showPrompt) {
+ mChosenImportance = mStartingUserImportance;
+ } else {
+ mChosenImportance = IMPORTANCE_NONE;
+ }
+
+ View prompt = findViewById(R.id.prompt);
+ View confirmation = findViewById(R.id.confirmation);
+ ObjectAnimator promptAnim = ObjectAnimator.ofFloat(prompt, View.ALPHA,
+ prompt.getAlpha(), showPrompt ? 1f : 0f);
+ promptAnim.setInterpolator(showPrompt ? Interpolators.ALPHA_IN : Interpolators.ALPHA_OUT);
+ ObjectAnimator confirmAnim = ObjectAnimator.ofFloat(confirmation, View.ALPHA,
+ confirmation.getAlpha(), showPrompt ? 0f : 1f);
+ confirmAnim.setInterpolator(showPrompt ? Interpolators.ALPHA_OUT : Interpolators.ALPHA_IN);
+
+ prompt.setVisibility(showPrompt ? VISIBLE : GONE);
+ confirmation.setVisibility(showPrompt ? GONE : VISIBLE);
+
+ mExpandAnimation = new AnimatorSet();
+ mExpandAnimation.playTogether(promptAnim, confirmAnim);
+ mExpandAnimation.setDuration(150);
+ mExpandAnimation.addListener(new AnimatorListenerAdapter() {
+ boolean cancelled = false;
+
+ @Override
+ public void onAnimationCancel(Animator animation) {
+ cancelled = true;
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ if (!cancelled) {
+ prompt.setVisibility(showPrompt ? VISIBLE : GONE);
+ confirmation.setVisibility(showPrompt ? GONE : VISIBLE);
+ }
}
- updateSecondaryText();
- updateAppSettingsLink();
});
+ mExpandAnimation.start();
}
@Override
@@ -339,35 +357,6 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
}
}
- private void updateSecondaryText() {
- final boolean disabled = mSingleNotificationChannel != null &&
- getSelectedImportance() == IMPORTANCE_NONE;
- if (disabled) {
- mChannelDisabledView.setVisibility(View.VISIBLE);
- mNumChannelsView.setVisibility(View.GONE);
- } else {
- mChannelDisabledView.setVisibility(View.GONE);
- mNumChannelsView.setVisibility(mIsSingleDefaultChannel ? View.INVISIBLE : View.VISIBLE);
- }
- }
-
- private void updateAppSettingsLink() {
- mSettingsLinkView = findViewById(R.id.app_settings);
- Intent settingsIntent = getAppSettingsIntent(mPm, mPkg, mSingleNotificationChannel,
- mSbn.getId(), mSbn.getTag());
- if (settingsIntent != null && getSelectedImportance() != IMPORTANCE_NONE
- && !TextUtils.isEmpty(mSbn.getNotification().getSettingsText())) {
- mSettingsLinkView.setVisibility(View.VISIBLE);
- mSettingsLinkView.setText(mContext.getString(R.string.notification_app_settings,
- mSbn.getNotification().getSettingsText()));
- mSettingsLinkView.setOnClickListener((View view) -> {
- mAppSettingsClickListener.onClick(view, settingsIntent);
- });
- } else {
- mSettingsLinkView.setVisibility(View.GONE);
- }
- }
-
private Intent getAppSettingsIntent(PackageManager pm, String packageName,
NotificationChannel channel, int id, String tag) {
Intent intent = new Intent(Intent.ACTION_MAIN)
@@ -390,6 +379,18 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
return intent;
}
+ private void closeControls(View v) {
+ int[] parentLoc = new int[2];
+ int[] targetLoc = new int[2];
+ mGutsContainer.getLocationOnScreen(parentLoc);
+ v.getLocationOnScreen(targetLoc);
+ final int centerX = v.getWidth() / 2;
+ final int centerY = v.getHeight() / 2;
+ final int x = targetLoc[0] - parentLoc[0] + centerX;
+ final int y = targetLoc[1] - parentLoc[1] + centerY;
+ mGutsContainer.closeControls(x, y, false /* save */, false /* force */);
+ }
+
@Override
public void setGutsParent(NotificationGuts guts) {
mGutsContainer = guts;
@@ -397,7 +398,7 @@ public class NotificationInfo extends LinearLayout implements NotificationGuts.G
@Override
public boolean willBeRemoved() {
- return mChannelEnabledSwitch != null && !mChannelEnabledSwitch.isChecked();
+ return hasImportanceChanged();
}
@Override
diff --git a/com/android/systemui/statusbar/NotificationMenuRow.java b/com/android/systemui/statusbar/NotificationMenuRow.java
index b2604fe0..037eeb2d 100644
--- a/com/android/systemui/statusbar/NotificationMenuRow.java
+++ b/com/android/systemui/statusbar/NotificationMenuRow.java
@@ -176,8 +176,6 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
final Resources res = mContext.getResources();
mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size);
mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height);
- mSidePadding = res.getDimensionPixelSize(R.dimen.notification_lockscreen_side_paddings);
- mIconPadding = res.getDimensionPixelSize(R.dimen.notification_menu_icon_padding);
mMenuItems.clear();
// Construct the menu items based on the notification
if (mParent != null && mParent.getStatusBarNotification() != null) {
@@ -498,8 +496,8 @@ public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnCl
final int count = mMenuContainer.getChildCount();
for (int i = 0; i < count; i++) {
final View v = mMenuContainer.getChildAt(i);
- final float left = mSidePadding + i * mHorizSpaceForIcon;
- final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1)) - mSidePadding;
+ final float left = i * mHorizSpaceForIcon;
+ final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1));
v.setX(showOnLeft ? left : right);
}
mOnLeft = showOnLeft;
diff --git a/com/android/systemui/statusbar/car/CarNavigationBarView.java b/com/android/systemui/statusbar/car/CarNavigationBarView.java
index 6cbbd6cd..e5a311d0 100644
--- a/com/android/systemui/statusbar/car/CarNavigationBarView.java
+++ b/com/android/systemui/statusbar/car/CarNavigationBarView.java
@@ -17,11 +17,15 @@
package com.android.systemui.statusbar.car;
import android.content.Context;
+import android.graphics.Canvas;
import android.util.AttributeSet;
+import android.view.MotionEvent;
import android.view.View;
import android.widget.LinearLayout;
import com.android.systemui.R;
+import com.android.systemui.plugins.statusbar.phone.NavGesture;
+import com.android.systemui.statusbar.phone.NavigationBarGestureHelper;
import com.android.systemui.statusbar.phone.NavigationBarView;
/**
@@ -72,4 +76,68 @@ class CarNavigationBarView extends NavigationBarView {
// Calling setNavigationIconHints in the base class will result in a NPE as the car
// navigation bar does not have a back button.
}
+
+ @Override
+ public void onPluginConnected(NavGesture plugin, Context context) {
+ // set to null version of the plugin ignoring incoming arg.
+ super.onPluginConnected(new NullNavGesture(), context);
+ }
+
+ @Override
+ public void onPluginDisconnected(NavGesture plugin) {
+ // reinstall the null nav gesture plugin
+ super.onPluginConnected(new NullNavGesture(), getContext());
+ }
+
+ /**
+ * Null object pattern to work around expectations of the base class.
+ * This is a temporary solution to have the car system ui working.
+ * Already underway is a refactor of they car sys ui as to not use this class
+ * hierarchy.
+ */
+ private static class NullNavGesture implements NavGesture {
+ @Override
+ public GestureHelper getGestureHelper() {
+ return new GestureHelper() {
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ return false;
+ }
+
+ @Override
+ public void setBarState(boolean vertical, boolean isRtl) {
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ }
+
+ @Override
+ public void onDarkIntensityChange(float intensity) {
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ }
+ };
+ }
+
+ @Override
+ public int getVersion() {
+ return 0;
+ }
+
+ @Override
+ public void onCreate(Context sysuiContext, Context pluginContext) {
+ }
+
+ @Override
+ public void onDestroy() {
+ }
+ }
}
diff --git a/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java b/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
index 172c62a9..3ec89138 100644
--- a/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
+++ b/com/android/systemui/statusbar/car/FullscreenUserSwitcher.java
@@ -53,7 +53,7 @@ public class FullscreenUserSwitcher {
mParent = containerStub.inflate();
mContainer = mParent.findViewById(R.id.container);
mUserGridView = mContainer.findViewById(R.id.user_grid);
- mUserGridView.init(statusBar, mUserSwitcherController, true /* showInitially */);
+ mUserGridView.init(statusBar, mUserSwitcherController, true /* overrideAlpha */);
mUserGridView.setUserSelectionListener(record -> {
if (!record.isCurrent) {
toggleSwitchInProgress(true);
diff --git a/com/android/systemui/statusbar/car/UserGridView.java b/com/android/systemui/statusbar/car/UserGridView.java
index e551801c..1bd820db 100644
--- a/com/android/systemui/statusbar/car/UserGridView.java
+++ b/com/android/systemui/statusbar/car/UserGridView.java
@@ -16,9 +16,6 @@
package com.android.systemui.statusbar.car;
-import android.animation.Animator;
-import android.animation.Animator.AnimatorListener;
-import android.animation.ValueAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
@@ -29,62 +26,110 @@ import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
-import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
-import android.widget.LinearLayout;
import android.widget.TextView;
+import com.android.systemui.Dependency;
import com.android.systemui.R;
+import com.android.systemui.qs.car.CarQSFragment;
import com.android.systemui.statusbar.phone.StatusBar;
+import com.android.systemui.statusbar.policy.UserInfoController;
import com.android.systemui.statusbar.policy.UserSwitcherController;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Vector;
+
/**
* Displays a ViewPager with icons for the users in the system to allow switching between users.
* One of the uses of this is for the lock screen in auto.
*/
-public class UserGridView extends ViewPager {
- private static final int EXPAND_ANIMATION_TIME_MS = 200;
- private static final int HIDE_ANIMATION_TIME_MS = 133;
-
+public class UserGridView extends ViewPager implements
+ UserInfoController.OnUserInfoChangedListener {
private StatusBar mStatusBar;
private UserSwitcherController mUserSwitcherController;
private Adapter mAdapter;
private UserSelectionListener mUserSelectionListener;
- private ValueAnimator mHeightAnimator;
- private int mTargetHeight;
- private int mHeightChildren;
- private boolean mShowing;
+ private UserInfoController mUserInfoController;
+ private Vector mUserContainers;
+ private int mContainerWidth;
+ private boolean mOverrideAlpha;
+ private CarQSFragment.UserSwitchCallback mUserSwitchCallback;
public UserGridView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public void init(StatusBar statusBar, UserSwitcherController userSwitcherController,
- boolean showInitially) {
+ boolean overrideAlpha) {
mStatusBar = statusBar;
mUserSwitcherController = userSwitcherController;
mAdapter = new Adapter(mUserSwitcherController);
- addOnLayoutChangeListener(mAdapter);
- setAdapter(mAdapter);
- mShowing = showInitially;
+ mUserInfoController = Dependency.get(UserInfoController.class);
+ mOverrideAlpha = overrideAlpha;
+ // Whenever the container width changes, the containers must be refreshed. Instead of
+ // doing an initial refreshContainers() to populate the containers, this listener will
+ // refresh them on layout change because that affects how the users are split into
+ // containers. Furthermore, at this point, the container width is unknown, so
+ // refreshContainers() cannot populate any containers.
+ addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ int newWidth = Math.max(left - right, right - left);
+ if (mContainerWidth != newWidth) {
+ mContainerWidth = newWidth;
+ refreshContainers();
+ }
+ });
}
- public boolean isShowing() {
- return mShowing;
+ private void refreshContainers() {
+ mUserContainers = new Vector();
+
+ Context context = getContext();
+ LayoutInflater inflater = LayoutInflater.from(context);
+
+ for (int i = 0; i < mAdapter.getCount(); i++) {
+ ViewGroup pods = (ViewGroup) inflater.inflate(
+ R.layout.car_fullscreen_user_pod_container, null);
+
+ int iconsPerPage = mAdapter.getIconsPerPage();
+ int limit = Math.min(mUserSwitcherController.getUsers().size(), (i + 1) * iconsPerPage);
+ for (int j = i * iconsPerPage; j < limit; j++) {
+ View v = mAdapter.makeUserPod(inflater, context, j, pods);
+ if (mOverrideAlpha) {
+ v.setAlpha(1f);
+ }
+ pods.addView(v);
+ // This is hacky, but the dividers on the pod container LinearLayout don't seem
+ // to work for whatever reason. Instead, set a right margin on the pod if it's not
+ // the right-most pod and there is more than one pod in the container.
+ if (i < limit - 1 && limit > 1) {
+ ViewGroup.MarginLayoutParams params =
+ (ViewGroup.MarginLayoutParams) v.getLayoutParams();
+ params.setMargins(0, 0, getResources().getDimensionPixelSize(
+ R.dimen.car_fullscreen_user_pod_margin_between), 0);
+ v.setLayoutParams(params);
+ }
+ }
+ mUserContainers.add(pods);
+ }
+
+ mAdapter = new Adapter(mUserSwitcherController);
+ setAdapter(mAdapter);
}
- public void show() {
- mShowing = true;
- animateHeightChange(getMeasuredHeight(), mHeightChildren);
+ @Override
+ public void onUserInfoChanged(String name, Drawable picture, String userAccount) {
+ refreshContainers();
}
- public void hide() {
- mShowing = false;
- animateHeightChange(getMeasuredHeight(), 0);
+ public void setUserSwitchCallback(CarQSFragment.UserSwitchCallback callback) {
+ mUserSwitchCallback = callback;
}
public void onUserSwitched(int newUserId) {
@@ -96,6 +141,14 @@ public class UserGridView extends ViewPager {
mUserSelectionListener = userSelectionListener;
}
+ public void setListening(boolean listening) {
+ if (listening) {
+ mUserInfoController.addCallback(this);
+ } else {
+ mUserInfoController.removeCallback(this);
+ }
+ }
+
void showOfflineAuthUi() {
// TODO: Show keyguard UI in-place.
mStatusBar.executeRunnableDismissingKeyguard(null, null, true, true, true);
@@ -115,13 +168,6 @@ public class UserGridView extends ViewPager {
height = Math.max(child.getMeasuredHeight(), height);
}
- mHeightChildren = height;
-
- // Override the height if it's not showing.
- if (!mShowing) {
- height = 0;
- }
-
// Respect the AT_MOST request from parent.
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST) {
height = Math.min(MeasureSpec.getSize(heightMeasureSpec), height);
@@ -132,72 +178,19 @@ public class UserGridView extends ViewPager {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
- private void animateHeightChange(int oldHeight, int newHeight) {
- // If there is no change in height or an animation is already in progress towards the
- // desired height, then there's no need to make any changes.
- if (oldHeight == newHeight || newHeight == mTargetHeight) {
- return;
- }
-
- // Animation in progress is not going towards the new target, so cancel it.
- if (mHeightAnimator != null){
- mHeightAnimator.cancel();
- }
-
- mTargetHeight = newHeight;
- mHeightAnimator = ValueAnimator.ofInt(oldHeight, mTargetHeight);
- mHeightAnimator.addUpdateListener(valueAnimator -> {
- ViewGroup.LayoutParams layoutParams = getLayoutParams();
- layoutParams.height = (Integer) valueAnimator.getAnimatedValue();
- requestLayout();
- });
- mHeightAnimator.addListener(new AnimatorListener() {
- @Override
- public void onAnimationStart(Animator animator) {}
-
- @Override
- public void onAnimationEnd(Animator animator) {
- // ValueAnimator does not guarantee that the update listener will get an update
- // to the final value, so here, the final value is set. Though the final calculated
- // height (mTargetHeight) could be set, WRAP_CONTENT is more appropriate.
- ViewGroup.LayoutParams layoutParams = getLayoutParams();
- layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
- requestLayout();
- mHeightAnimator = null;
- }
-
- @Override
- public void onAnimationCancel(Animator animator) {}
-
- @Override
- public void onAnimationRepeat(Animator animator) {}
- });
-
- mHeightAnimator.setInterpolator(new FastOutSlowInInterpolator());
- if (oldHeight < newHeight) {
- // Expanding
- mHeightAnimator.setDuration(EXPAND_ANIMATION_TIME_MS);
- } else {
- // Hiding
- mHeightAnimator.setDuration(HIDE_ANIMATION_TIME_MS);
- }
- mHeightAnimator.start();
- }
-
/**
* This is a ViewPager.PagerAdapter which deletegates the work to a
* UserSwitcherController.BaseUserAdapter. Java doesn't support multiple inheritance so we have
* to use composition instead to achieve the same goal since both the base classes are abstract
* classes and not interfaces.
*/
- private final class Adapter extends PagerAdapter implements View.OnLayoutChangeListener {
+ private final class Adapter extends PagerAdapter {
private final int mPodWidth;
private final int mPodMarginBetween;
private final int mPodImageAvatarWidth;
private final int mPodImageAvatarHeight;
private final WrappedBaseUserAdapter mUserAdapter;
- private int mContainerWidth;
public Adapter(UserSwitcherController controller) {
super();
@@ -229,30 +222,20 @@ public class UserGridView extends ViewPager {
}
@Override
- public Object instantiateItem(ViewGroup container, int position) {
- Context context = getContext();
- LayoutInflater inflater = LayoutInflater.from(context);
-
- ViewGroup pods = (ViewGroup) inflater.inflate(
- R.layout.car_fullscreen_user_pod_container, null);
+ public void finishUpdate(ViewGroup container) {
+ if (mUserSwitchCallback != null) {
+ mUserSwitchCallback.resetShowing();
+ }
+ }
- int iconsPerPage = getIconsPerPage();
- int limit = Math.min(mUserAdapter.getCount(), (position + 1) * iconsPerPage);
- for (int i = position * iconsPerPage; i < limit; i++) {
- View v = makeUserPod(inflater, context, i, pods);
- pods.addView(v);
- // This is hacky, but the dividers on the pod container LinearLayout don't seem
- // to work for whatever reason. Instead, set a right margin on the pod if it's not
- // the right-most pod and there is more than one pod in the container.
- if (i < limit - 1 && limit > 1) {
- LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
- LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
- params.setMargins(0, 0, mPodMarginBetween, 0);
- v.setLayoutParams(params);
- }
+ @Override
+ public Object instantiateItem(ViewGroup container, int position) {
+ if (position < mUserContainers.size()) {
+ container.addView((View) mUserContainers.get(position));
+ return mUserContainers.get(position);
+ } else {
+ return null;
}
- container.addView(pods);
- return pods;
}
/**
@@ -353,17 +336,10 @@ public class UserGridView extends ViewPager {
public boolean isViewFromObject(View view, Object object) {
return view == object;
}
-
- @Override
- public void onLayoutChange(View v, int left, int top, int right, int bottom,
- int oldLeft, int oldTop, int oldRight, int oldBottom) {
- mContainerWidth = Math.max(left - right, right - left);
- notifyDataSetChanged();
- }
}
private final class WrappedBaseUserAdapter extends UserSwitcherController.BaseUserAdapter {
- private Adapter mContainer;
+ private final Adapter mContainer;
public WrappedBaseUserAdapter(UserSwitcherController controller, Adapter container) {
super(controller);
diff --git a/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java b/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java
index 27defcac..113118a1 100644
--- a/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java
+++ b/com/android/systemui/statusbar/notification/MessagingLayoutTransformState.java
@@ -26,9 +26,7 @@ import com.android.internal.widget.MessagingLayout;
import com.android.internal.widget.MessagingLinearLayout;
import com.android.internal.widget.MessagingMessage;
import com.android.internal.widget.MessagingPropertyAnimator;
-import com.android.internal.widget.ViewClippingUtil;
import com.android.systemui.Interpolators;
-import com.android.systemui.statusbar.ExpandableNotificationRow;
import java.util.ArrayList;
import java.util.HashMap;
@@ -157,8 +155,8 @@ public class MessagingLayoutTransformState extends TransformState {
setClippingDeactivated(child, true);
}
appear(ownGroup.getAvatar(), transformationAmount);
- appear(ownGroup.getSender(), transformationAmount);
- setClippingDeactivated(ownGroup.getSender(), true);
+ appear(ownGroup.getSenderView(), transformationAmount);
+ setClippingDeactivated(ownGroup.getSenderView(), true);
setClippingDeactivated(ownGroup.getAvatar(), true);
}
@@ -170,7 +168,7 @@ public class MessagingLayoutTransformState extends TransformState {
} else {
relativeOffset = (1.0f - transformationAmount) * mRelativeTranslationOffset;
}
- if (ownGroup.getSender().getVisibility() != View.GONE) {
+ if (ownGroup.getSenderView().getVisibility() != View.GONE) {
relativeOffset *= 0.5f;
}
ownGroup.getMessageContainer().setTranslationY(relativeOffset);
@@ -188,8 +186,8 @@ public class MessagingLayoutTransformState extends TransformState {
setClippingDeactivated(child, true);
}
disappear(ownGroup.getAvatar(), transformationAmount);
- disappear(ownGroup.getSender(), transformationAmount);
- setClippingDeactivated(ownGroup.getSender(), true);
+ disappear(ownGroup.getSenderView(), transformationAmount);
+ setClippingDeactivated(ownGroup.getSenderView(), true);
setClippingDeactivated(ownGroup.getAvatar(), true);
}
@@ -226,7 +224,7 @@ public class MessagingLayoutTransformState extends TransformState {
private void transformGroups(MessagingGroup ownGroup, MessagingGroup otherGroup,
float transformationAmount, boolean to) {
- transformView(transformationAmount, to, ownGroup.getSender(), otherGroup.getSender(),
+ transformView(transformationAmount, to, ownGroup.getSenderView(), otherGroup.getSenderView(),
true /* sameAsAny */);
transformView(transformationAmount, to, ownGroup.getAvatar(), otherGroup.getAvatar(),
true /* sameAsAny */);
@@ -345,7 +343,7 @@ public class MessagingLayoutTransformState extends TransformState {
setVisible(child, visible, force);
}
setVisible(ownGroup.getAvatar(), visible, force);
- setVisible(ownGroup.getSender(), visible, force);
+ setVisible(ownGroup.getSenderView(), visible, force);
}
}
}
@@ -376,9 +374,9 @@ public class MessagingLayoutTransformState extends TransformState {
setClippingDeactivated(child, false);
}
resetTransformedView(ownGroup.getAvatar());
- resetTransformedView(ownGroup.getSender());
+ resetTransformedView(ownGroup.getSenderView());
setClippingDeactivated(ownGroup.getAvatar(), false);
- setClippingDeactivated(ownGroup.getSender(), false);
+ setClippingDeactivated(ownGroup.getSenderView(), false);
ownGroup.setTranslationY(0);
ownGroup.getMessageContainer().setTranslationY(0);
}
diff --git a/com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java
index 66682e4c..adc09145 100644
--- a/com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java
+++ b/com/android/systemui/statusbar/notification/NotificationCustomViewWrapper.java
@@ -21,7 +21,6 @@ import android.animation.AnimatorListenerAdapter;
import android.content.Context;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
-import android.os.Build;
import android.view.View;
import com.android.systemui.R;
@@ -38,7 +37,6 @@ public class NotificationCustomViewWrapper extends NotificationViewWrapper {
private final Paint mGreyPaint = new Paint();
private boolean mIsLegacy;
private int mLegacyColor;
- private boolean mBeforeP;
protected NotificationCustomViewWrapper(Context ctx, View view, ExpandableNotificationRow row) {
super(ctx, view, row);
@@ -119,15 +117,7 @@ public class NotificationCustomViewWrapper extends NotificationViewWrapper {
}
@Override
- public boolean shouldClipToSidePaddings() {
- // Before P we ensure that they are now drawing inside out content bounds since we inset
- // the view. If they target P, then we don't have that guarantee and we need to be safe.
- return !mBeforeP;
- }
-
- @Override
- public void onContentUpdated(ExpandableNotificationRow row) {
- super.onContentUpdated(row);
- mBeforeP = row.getEntry().targetSdk < Build.VERSION_CODES.P;
+ public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) {
+ return true;
}
}
diff --git a/com/android/systemui/statusbar/notification/NotificationMediaTemplateViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationMediaTemplateViewWrapper.java
index 060e6d65..548f006c 100644
--- a/com/android/systemui/statusbar/notification/NotificationMediaTemplateViewWrapper.java
+++ b/com/android/systemui/statusbar/notification/NotificationMediaTemplateViewWrapper.java
@@ -62,7 +62,7 @@ public class NotificationMediaTemplateViewWrapper extends NotificationTemplateVi
}
@Override
- public boolean shouldClipToSidePaddings() {
+ public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) {
return true;
}
}
diff --git a/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
index e07112f9..d463eae6 100644
--- a/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
+++ b/com/android/systemui/statusbar/notification/NotificationTemplateViewWrapper.java
@@ -266,8 +266,12 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp
}
@Override
- public boolean shouldClipToSidePaddings() {
- return mActionsContainer != null && mActionsContainer.getVisibility() != View.GONE;
+ public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) {
+ if (super.shouldClipToRounding(topRounded, bottomRounded)) {
+ return true;
+ }
+ return bottomRounded && mActionsContainer != null
+ && mActionsContainer.getVisibility() != View.GONE;
}
private void updateActionOffset() {
diff --git a/com/android/systemui/statusbar/notification/NotificationViewWrapper.java b/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
index 8a767bb7..17eb4c11 100644
--- a/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
+++ b/com/android/systemui/statusbar/notification/NotificationViewWrapper.java
@@ -195,7 +195,7 @@ public abstract class NotificationViewWrapper implements TransformableView {
return 0;
}
- public boolean shouldClipToSidePaddings() {
+ public boolean shouldClipToRounding(boolean topRounded, boolean bottomRounded) {
return false;
}
}
diff --git a/com/android/systemui/statusbar/phone/AutoTileManager.java b/com/android/systemui/statusbar/phone/AutoTileManager.java
index 149ec0b3..36f9f6b7 100644
--- a/com/android/systemui/statusbar/phone/AutoTileManager.java
+++ b/com/android/systemui/statusbar/phone/AutoTileManager.java
@@ -127,7 +127,7 @@ public class AutoTileManager {
private final HotspotController.Callback mHotspotCallback = new Callback() {
@Override
- public void onHotspotChanged(boolean enabled) {
+ public void onHotspotChanged(boolean enabled, int numDevices) {
if (mAutoTracker.isAdded(HOTSPOT)) return;
if (enabled) {
mHost.addTile(HOTSPOT);
diff --git a/com/android/systemui/statusbar/phone/ButtonDispatcher.java b/com/android/systemui/statusbar/phone/ButtonDispatcher.java
index a83e6591..7284ee8b 100644
--- a/com/android/systemui/statusbar/phone/ButtonDispatcher.java
+++ b/com/android/systemui/statusbar/phone/ButtonDispatcher.java
@@ -14,7 +14,6 @@
package com.android.systemui.statusbar.phone;
-import android.graphics.drawable.Drawable;
import android.view.View;
import com.android.systemui.plugins.statusbar.phone.NavBarButtonProvider.ButtonInterface;
@@ -35,10 +34,12 @@ public class ButtonDispatcher {
private View.OnClickListener mClickListener;
private View.OnTouchListener mTouchListener;
private View.OnLongClickListener mLongClickListener;
+ private View.OnHoverListener mOnHoverListener;
private Boolean mLongClickable;
private Integer mAlpha;
private Float mDarkIntensity;
private Integer mVisibility = -1;
+ private Boolean mDelayTouchFeedback;
private KeyButtonDrawable mImageDrawable;
private View mCurrentView;
private boolean mVertical;
@@ -56,6 +57,7 @@ public class ButtonDispatcher {
view.setOnClickListener(mClickListener);
view.setOnTouchListener(mTouchListener);
view.setOnLongClickListener(mLongClickListener);
+ view.setOnHoverListener(mOnHoverListener);
if (mLongClickable != null) {
view.setLongClickable(mLongClickable);
}
@@ -71,10 +73,10 @@ public class ButtonDispatcher {
if (mImageDrawable != null) {
((ButtonInterface) view).setImageDrawable(mImageDrawable);
}
-
- if (view instanceof ButtonInterface) {
- ((ButtonInterface) view).setVertical(mVertical);
+ if (mDelayTouchFeedback != null) {
+ ((ButtonInterface) view).setDelayTouchFeedback(mDelayTouchFeedback);
}
+ ((ButtonInterface) view).setVertical(mVertical);
}
public int getId() {
@@ -89,6 +91,10 @@ public class ButtonDispatcher {
return mAlpha != null ? mAlpha : 1;
}
+ public KeyButtonDrawable getImageDrawable() {
+ return mImageDrawable;
+ }
+
public void setImageDrawable(KeyButtonDrawable drawable) {
mImageDrawable = drawable;
final int N = mViews.size();
@@ -130,6 +136,14 @@ public class ButtonDispatcher {
}
}
+ public void setDelayTouchFeedback(boolean delay) {
+ mDelayTouchFeedback = delay;
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ ((ButtonInterface) mViews.get(i)).setDelayTouchFeedback(delay);
+ }
+ }
+
public void setOnClickListener(View.OnClickListener clickListener) {
mClickListener = clickListener;
final int N = mViews.size();
@@ -162,6 +176,22 @@ public class ButtonDispatcher {
}
}
+ public void setOnHoverListener(View.OnHoverListener hoverListener) {
+ mOnHoverListener = hoverListener;
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ mViews.get(i).setOnHoverListener(mOnHoverListener);
+ }
+ }
+
+ public void setClickable(boolean clickable) {
+ abortCurrentGesture();
+ final int N = mViews.size();
+ for (int i = 0; i < N; i++) {
+ mViews.get(i).setClickable(clickable);
+ }
+ }
+
public ArrayList<View> getViews() {
return mViews;
}
diff --git a/com/android/systemui/statusbar/phone/DozeParameters.java b/com/android/systemui/statusbar/phone/DozeParameters.java
index 6d85fb37..fb3adf45 100644
--- a/com/android/systemui/statusbar/phone/DozeParameters.java
+++ b/com/android/systemui/statusbar/phone/DozeParameters.java
@@ -25,12 +25,14 @@ import android.util.MathUtils;
import android.util.SparseBooleanArray;
import com.android.internal.hardware.AmbientDisplayConfiguration;
+import com.android.systemui.Dependency;
import com.android.systemui.R;
import com.android.systemui.doze.AlwaysOnDisplayPolicy;
+import com.android.systemui.tuner.TunerService;
import java.io.PrintWriter;
-public class DozeParameters {
+public class DozeParameters implements TunerService.Tunable {
private static final int MAX_DURATION = 60 * 1000;
public static final String DOZE_SENSORS_WAKE_UP_FULLY = "doze_sensors_wake_up_fully";
@@ -40,10 +42,15 @@ public class DozeParameters {
private static IntInOutMatcher sPickupSubtypePerformsProxMatcher;
private final AlwaysOnDisplayPolicy mAlwaysOnPolicy;
+ private boolean mDozeAlwaysOn;
+
public DozeParameters(Context context) {
mContext = context;
mAmbientDisplayConfiguration = new AmbientDisplayConfiguration(mContext);
mAlwaysOnPolicy = new AlwaysOnDisplayPolicy(context);
+
+ Dependency.get(TunerService.class).addTunable(this, Settings.Secure.DOZE_ALWAYS_ON,
+ Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED);
}
public void dump(PrintWriter pw) {
@@ -144,7 +151,7 @@ public class DozeParameters {
* @return {@code true} if enabled and available.
*/
public boolean getAlwaysOn() {
- return mAmbientDisplayConfiguration.alwaysOnEnabled(UserHandle.USER_CURRENT);
+ return mDozeAlwaysOn;
}
/**
@@ -207,6 +214,10 @@ public class DozeParameters {
return mContext.getResources().getBoolean(R.bool.doze_double_tap_reports_touch_coordinates);
}
+ @Override
+ public void onTuningChanged(String key, String newValue) {
+ mDozeAlwaysOn = mAmbientDisplayConfiguration.alwaysOnEnabled(UserHandle.USER_CURRENT);
+ }
/**
* Parses a spec of the form `1,2,3,!5,*`. The resulting object will match numbers that are
diff --git a/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java b/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
index df1ffdaf..46d98276 100644
--- a/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
+++ b/com/android/systemui/statusbar/phone/KeyguardAffordanceHelper.java
@@ -529,6 +529,13 @@ public class KeyguardAffordanceHelper {
KeyguardAffordanceView targetView = left ? mLeftIcon : mRightIcon;
KeyguardAffordanceView otherView = left ? mRightIcon : mLeftIcon;
startSwiping(targetView);
+
+ // Do not animate the circle expanding if the affordance isn't visible,
+ // otherwise the circle will be meaningless.
+ if (targetView.getVisibility() != View.VISIBLE) {
+ animate = false;
+ }
+
if (animate) {
fling(0, false, !left);
updateIcon(otherView, 0.0f, 0, true, false, true, false);
diff --git a/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java b/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
index f0588626..ca66e987 100644
--- a/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
+++ b/com/android/systemui/statusbar/phone/KeyguardBottomAreaView.java
@@ -51,6 +51,7 @@ import android.telecom.TelecomManager;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
+import android.util.MathUtils;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
@@ -166,6 +167,10 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL
private String mLeftButtonStr;
private LockscreenGestureLogger mLockscreenGestureLogger = new LockscreenGestureLogger();
private boolean mDozing;
+ private int mIndicationBottomMargin;
+ private int mIndicationBottomMarginAmbient;
+ private float mDarkAmount;
+ private int mBurnInXOffset;
public KeyguardBottomAreaView(Context context) {
this(context, null);
@@ -235,6 +240,10 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL
mEnterpriseDisclosure = findViewById(
R.id.keyguard_indication_enterprise_disclosure);
mIndicationText = findViewById(R.id.keyguard_indication_text);
+ mIndicationBottomMargin = getResources().getDimensionPixelSize(
+ R.dimen.keyguard_indication_margin_bottom);
+ mIndicationBottomMarginAmbient = getResources().getDimensionPixelSize(
+ R.dimen.keyguard_indication_margin_bottom_ambient);
updateCameraVisibility();
mUnlockMethodCache = UnlockMethodCache.getInstance(getContext());
mUnlockMethodCache.addListener(this);
@@ -303,11 +312,13 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL
@Override
protected void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
- int indicationBottomMargin = getResources().getDimensionPixelSize(
+ mIndicationBottomMargin = getResources().getDimensionPixelSize(
R.dimen.keyguard_indication_margin_bottom);
+ mIndicationBottomMarginAmbient = getResources().getDimensionPixelSize(
+ R.dimen.keyguard_indication_margin_bottom_ambient);
MarginLayoutParams mlp = (MarginLayoutParams) mIndicationArea.getLayoutParams();
- if (mlp.bottomMargin != indicationBottomMargin) {
- mlp.bottomMargin = indicationBottomMargin;
+ if (mlp.bottomMargin != mIndicationBottomMargin) {
+ mlp.bottomMargin = mIndicationBottomMargin;
mIndicationArea.setLayoutParams(mlp);
}
@@ -543,6 +554,22 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL
}
}
+ public void setDarkAmount(float darkAmount) {
+ if (darkAmount == mDarkAmount) {
+ return;
+ }
+ mDarkAmount = darkAmount;
+ // Let's randomize the bottom margin every time we wake up to avoid burn-in.
+ if (darkAmount == 0) {
+ mIndicationBottomMarginAmbient = getResources().getDimensionPixelSize(
+ R.dimen.keyguard_indication_margin_bottom_ambient)
+ + (int) (Math.random() * mIndicationText.getTextSize());
+ }
+ mIndicationArea.setAlpha(MathUtils.lerp(1f, 0.7f, darkAmount));
+ mIndicationArea.setTranslationY(MathUtils.lerp(0,
+ mIndicationBottomMargin - mIndicationBottomMarginAmbient, darkAmount));
+ }
+
private static boolean isSuccessfulLaunch(int result) {
return result == ActivityManager.START_SUCCESS
|| result == ActivityManager.START_DELIVERED_TO_TOP
@@ -687,11 +714,6 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL
if (mRightAffordanceView.getVisibility() == View.VISIBLE) {
startFinishDozeAnimationElement(mRightAffordanceView, delay);
}
- mIndicationArea.setAlpha(0f);
- mIndicationArea.animate()
- .alpha(1f)
- .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN)
- .setDuration(NotificationPanelView.DOZE_ANIMATION_DURATION);
}
private void startFinishDozeAnimationElement(View element, long delay) {
@@ -815,6 +837,22 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL
}
}
+ public void dozeTimeTick() {
+ if (mDarkAmount == 1) {
+ // Move indication every minute to avoid burn-in
+ final int dozeTranslation = mIndicationBottomMargin - mIndicationBottomMarginAmbient;
+ mIndicationArea.setTranslationY(dozeTranslation + (float) Math.random() * 5);
+ }
+ }
+
+ public void setBurnInXOffset(int burnInXOffset) {
+ if (mBurnInXOffset == burnInXOffset) {
+ return;
+ }
+ mBurnInXOffset = burnInXOffset;
+ mIndicationArea.setTranslationX(burnInXOffset);
+ }
+
private class DefaultLeftButton implements IntentButton {
private IconState mIconState = new IconState();
@@ -822,8 +860,10 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL
@Override
public IconState getIcon() {
mLeftIsVoiceAssist = canLaunchVoiceAssist();
+ final boolean showAffordance =
+ getResources().getBoolean(R.bool.config_keyguardShowLeftAffordance);
if (mLeftIsVoiceAssist) {
- mIconState.isVisible = mUserSetupComplete;
+ mIconState.isVisible = mUserSetupComplete && showAffordance;
if (mLeftAssistIcon == null) {
mIconState.drawable = mContext.getDrawable(R.drawable.ic_mic_26dp);
} else {
@@ -832,7 +872,7 @@ public class KeyguardBottomAreaView extends FrameLayout implements View.OnClickL
mIconState.contentDescription = mContext.getString(
R.string.accessibility_voice_assist_button);
} else {
- mIconState.isVisible = mUserSetupComplete && isPhoneVisible();
+ mIconState.isVisible = mUserSetupComplete && showAffordance && isPhoneVisible();
mIconState.drawable = mContext.getDrawable(R.drawable.ic_phone_24dp);
mIconState.contentDescription = mContext.getString(
R.string.accessibility_phone_button);
diff --git a/com/android/systemui/statusbar/phone/KeyguardBouncer.java b/com/android/systemui/statusbar/phone/KeyguardBouncer.java
index b71ebfdc..699e8cf1 100644
--- a/com/android/systemui/statusbar/phone/KeyguardBouncer.java
+++ b/com/android/systemui/statusbar/phone/KeyguardBouncer.java
@@ -131,6 +131,10 @@ public class KeyguardBouncer {
mRoot.setVisibility(View.VISIBLE);
mKeyguardView.onResume();
showPromptReason(mBouncerPromptReason);
+ final CharSequence customMessage = mCallback.consumeCustomMessage();
+ if (customMessage != null) {
+ mKeyguardView.showErrorMessage(customMessage);
+ }
// We might still be collapsed and the view didn't have time to layout yet or still
// be small, let's wait on the predraw to do the animation in that case.
if (mKeyguardView.getHeight() != 0 && mKeyguardView.getHeight() != mStatusBarHeight) {
diff --git a/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java b/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
index f7aa818f..389be1ad 100644
--- a/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
+++ b/com/android/systemui/statusbar/phone/KeyguardClockPositionAlgorithm.java
@@ -20,6 +20,7 @@ import static com.android.systemui.statusbar.notification.NotificationUtils.inte
import android.content.res.Resources;
import android.graphics.Path;
+import android.util.MathUtils;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.PathInterpolator;
@@ -45,8 +46,7 @@ public class KeyguardClockPositionAlgorithm {
private static final float BURN_IN_PREVENTION_PERIOD_Y = 521;
private static final float BURN_IN_PREVENTION_PERIOD_X = 83;
- private int mClockNotificationsMarginMin;
- private int mClockNotificationsMarginMax;
+ private int mClockNotificationsMargin;
private float mClockYFractionMin;
private float mClockYFractionMax;
private int mMaxKeyguardNotifications;
@@ -84,10 +84,8 @@ public class KeyguardClockPositionAlgorithm {
* Refreshes the dimension values.
*/
public void loadDimens(Resources res) {
- mClockNotificationsMarginMin = res.getDimensionPixelSize(
- R.dimen.keyguard_clock_notifications_margin_min);
- mClockNotificationsMarginMax = res.getDimensionPixelSize(
- R.dimen.keyguard_clock_notifications_margin_max);
+ mClockNotificationsMargin = res.getDimensionPixelSize(
+ R.dimen.keyguard_clock_notifications_margin);
mClockYFractionMin = res.getFraction(R.fraction.keyguard_clock_y_fraction_min, 1, 1);
mClockYFractionMax = res.getFraction(R.fraction.keyguard_clock_y_fraction_max, 1, 1);
mMoreCardNotificationAmount =
@@ -117,7 +115,7 @@ public class KeyguardClockPositionAlgorithm {
public float getMinStackScrollerPadding(int height, int keyguardStatusHeight) {
return mClockYFractionMin * height + keyguardStatusHeight / 2
- + mClockNotificationsMarginMin;
+ + mClockNotificationsMargin;
}
public void run(Result result) {
@@ -125,21 +123,15 @@ public class KeyguardClockPositionAlgorithm {
float clockAdjustment = getClockYExpansionAdjustment();
float topPaddingAdjMultiplier = getTopPaddingAdjMultiplier();
result.stackScrollerPaddingAdjustment = (int) (clockAdjustment*topPaddingAdjMultiplier);
- int clockNotificationsPadding = getClockNotificationsPadding()
+ result.clockY = y;
+ int clockNotificationsPadding = mClockNotificationsMargin
+ result.stackScrollerPaddingAdjustment;
int padding = y + clockNotificationsPadding;
- result.clockY = y;
- result.stackScrollerPadding = mKeyguardStatusHeight + padding;
- result.clockScale = getClockScale(result.stackScrollerPadding,
- result.clockY,
- y + getClockNotificationsPadding() + mKeyguardStatusHeight);
+ result.clockScale = getClockScale(mKeyguardStatusHeight + padding,
+ y, y + mClockNotificationsMargin + mKeyguardStatusHeight);
result.clockAlpha = getClockAlpha(result.clockScale);
- result.stackScrollerPadding = (int) interpolate(
- result.stackScrollerPadding,
- mClockBottom + y + mDozingStackPadding,
- mDarkAmount);
-
+ result.stackScrollerPadding = mClockBottom + y + mDozingStackPadding;
result.clockX = (int) interpolate(0, burnInPreventionOffsetX(), mDarkAmount);
}
@@ -154,22 +146,16 @@ public class KeyguardClockPositionAlgorithm {
return interpolate(progress, 1, mDarkAmount);
}
- private int getClockNotificationsPadding() {
- float t = getNotificationAmountT();
- t = Math.min(t, 1.0f);
- return (int) (t * mClockNotificationsMarginMin + (1 - t) * mClockNotificationsMarginMax);
- }
-
private float getClockYFraction() {
float t = getNotificationAmountT();
t = Math.min(t, 1.0f);
- return (1 - t) * mClockYFractionMax + t * mClockYFractionMin;
+ return MathUtils.lerp(mClockYFractionMax, mClockYFractionMin, t);
}
private int getClockY() {
- // Dark: Align the bottom edge of the clock at one third:
- // clockBottomEdge = result - mKeyguardStatusHeight / 2 + mClockBottom
- float clockYDark = (0.33f * mHeight + (float) mKeyguardStatusHeight / 2 - mClockBottom)
+ // Dark: Align the bottom edge of the clock at about half of the screen:
+ float clockYDark = (mClockYFractionMax * mHeight +
+ (float) mKeyguardStatusHeight / 2 - mClockBottom)
+ burnInPreventionOffsetY();
float clockYRegular = getClockYFraction() * mHeight;
return (int) interpolate(clockYRegular, clockYDark, mDarkAmount);
diff --git a/com/android/systemui/statusbar/phone/LockIcon.java b/com/android/systemui/statusbar/phone/LockIcon.java
index 34486dbc..264f5749 100644
--- a/com/android/systemui/statusbar/phone/LockIcon.java
+++ b/com/android/systemui/statusbar/phone/LockIcon.java
@@ -250,7 +250,7 @@ public class LockIcon extends KeyguardAffordanceView implements OnUserInfoChange
}
break;
case STATE_FACE_UNLOCK:
- iconRes = R.drawable.ic_account_circle;
+ iconRes = R.drawable.ic_face_unlock;
break;
case STATE_FINGERPRINT:
// If screen is off and device asleep, use the draw on animation so the first frame
diff --git a/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
index 7f4deb03..0f8d59b1 100644
--- a/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
+++ b/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java
@@ -61,7 +61,7 @@ public class ManagedProfileControllerImpl implements ManagedProfileController {
public void setWorkModeEnabled(boolean enableWorkMode) {
synchronized (mProfiles) {
for (UserInfo ui : mProfiles) {
- if (!mUserManager.trySetQuietModeEnabled(!enableWorkMode, UserHandle.of(ui.id))) {
+ if (!mUserManager.requestQuietModeEnabled(!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 695168e3..dc51b1c3 100644
--- a/com/android/systemui/statusbar/phone/NavigationBarFragment.java
+++ b/com/android/systemui/statusbar/phone/NavigationBarFragment.java
@@ -24,6 +24,9 @@ import static com.android.systemui.statusbar.phone.StatusBar.DEBUG_WINDOW_STATE;
import static com.android.systemui.statusbar.phone.StatusBar.dumpBarTransitions;
import android.accessibilityservice.AccessibilityServiceInfo;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManagerNative;
@@ -39,6 +42,7 @@ import android.content.res.Configuration;
import android.database.ContentObserver;
import android.graphics.PixelFormat;
import android.graphics.Rect;
+import android.graphics.drawable.AnimatedVectorDrawable;
import android.inputmethodservice.InputMethodService;
import android.os.Binder;
import android.os.Bundle;
@@ -56,6 +60,7 @@ import android.view.IRotationWatcher.Stub;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MotionEvent;
+import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
@@ -70,17 +75,22 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.LatencyTracker;
import com.android.systemui.Dependency;
import com.android.systemui.OverviewProxyService;
+import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.SysUiServiceProvider;
import com.android.systemui.assist.AssistManager;
import com.android.systemui.fragments.FragmentHostManager;
import com.android.systemui.fragments.FragmentHostManager.FragmentListener;
import com.android.systemui.recents.Recents;
+import com.android.systemui.recents.misc.SysUiTaskStackChangeListener;
+import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.stackdivider.Divider;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.CommandQueue.Callbacks;
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
+import com.android.systemui.statusbar.policy.KeyButtonDrawable;
import com.android.systemui.statusbar.policy.KeyButtonView;
+import com.android.systemui.statusbar.policy.RotationLockController;
import com.android.systemui.statusbar.stack.StackStateAnimator;
import java.io.FileDescriptor;
@@ -101,6 +111,8 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
/** Allow some time inbetween the long press for back and recents. */
private static final int LOCK_TO_APP_GESTURE_TOLERENCE = 200;
+ private static final int BUTTON_FADE_IN_OUT_DURATION_MS = 100;
+
protected NavigationBarView mNavigationBarView = null;
protected AssistManager mAssistManager;
@@ -108,6 +120,7 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
private int mNavigationIconHints = 0;
private int mNavigationBarMode;
+ private boolean mAccessibilityFeedbackEnabled;
private AccessibilityManager mAccessibilityManager;
private MagnificationContentObserver mMagnificationObserver;
private ContentResolver mContentResolver;
@@ -130,6 +143,16 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
public boolean mHomeBlockedThisTouch;
+ private int mLastRotationSuggestion;
+ private boolean mHoveringRotationSuggestion;
+ private RotationLockController mRotationLockController;
+ private TaskStackListenerImpl mTaskStackListener;
+
+ private final Runnable mRemoveRotationProposal = () -> setRotateSuggestionButtonState(false);
+ private Animator mRotateShowAnimator;
+ private Animator mRotateHideAnimator;
+
+
// ----- Fragment Lifecycle Callbacks -----
@Override
@@ -163,6 +186,12 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
+
+ mRotationLockController = Dependency.get(RotationLockController.class);
+
+ // Register the task stack listener
+ mTaskStackListener = new TaskStackListenerImpl();
+ ActivityManagerWrapper.getInstance().registerTaskStackListener(mTaskStackListener);
}
@Override
@@ -178,6 +207,9 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
+
+ // Unregister the task stack listener
+ ActivityManagerWrapper.getInstance().unregisterTaskStackListener(mTaskStackListener);
}
@Override
@@ -304,6 +336,131 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
}
}
+ @Override
+ public void onRotationProposal(final int rotation, boolean isValid) {
+ // This method will be called on rotation suggestion changes even if the proposed rotation
+ // is not valid for the top app. Use invalid rotation choices as a signal to remove the
+ // rotate button if shown.
+
+ if (!isValid) {
+ setRotateSuggestionButtonState(false);
+ return;
+ }
+
+ if (rotation == mWindowManager.getDefaultDisplay().getRotation()) {
+ // Use this as a signal to remove any current suggestions
+ getView().getHandler().removeCallbacks(mRemoveRotationProposal);
+ setRotateSuggestionButtonState(false);
+ } else {
+ mLastRotationSuggestion = rotation; // Remember rotation for click
+ setRotateSuggestionButtonState(true);
+ rescheduleRotationTimeout(false);
+ }
+ }
+
+ private void rescheduleRotationTimeout(final boolean reasonHover) {
+ // May be called due to a new rotation proposal or a change in hover state
+ if (reasonHover) {
+ // Don't reschedule if a hide animator is running
+ if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) {
+ return;
+ }
+ // Don't reschedule if not visible
+ if (mNavigationBarView.getRotateSuggestionButton().getVisibility() != View.VISIBLE) {
+ return;
+ }
+ }
+
+ Handler h = getView().getHandler();
+ h.removeCallbacks(mRemoveRotationProposal); // Stop any pending removal
+ h.postDelayed(mRemoveRotationProposal,
+ computeRotationProposalTimeout()); // Schedule timeout
+ }
+
+ private int computeRotationProposalTimeout() {
+ if (mAccessibilityFeedbackEnabled) return 20000;
+ if (mHoveringRotationSuggestion) return 16000;
+ return 6000;
+ }
+
+ public void setRotateSuggestionButtonState(final boolean visible) {
+ setRotateSuggestionButtonState(visible, false);
+ }
+
+ public void setRotateSuggestionButtonState(final boolean visible, final boolean skipAnim) {
+ ButtonDispatcher rotBtn = mNavigationBarView.getRotateSuggestionButton();
+ final boolean currentlyVisible = rotBtn.getVisibility() == View.VISIBLE;
+
+ // Rerun a show animation to indicate change but don't rerun a hide animation
+ if (!visible && !currentlyVisible) return;
+
+ View currentView = rotBtn.getCurrentView();
+ if (currentView == null) return;
+
+ KeyButtonDrawable kbd = rotBtn.getImageDrawable();
+ if (kbd == null) return;
+
+ AnimatedVectorDrawable animIcon = null;
+ if (kbd.getDrawable(0) instanceof AnimatedVectorDrawable) {
+ animIcon = (AnimatedVectorDrawable) kbd.getDrawable(0);
+ }
+
+ if (visible) { // Appear and change
+ rotBtn.setVisibility(View.VISIBLE);
+ mNavigationBarView.notifyAccessibilitySubtreeChanged();
+
+ if (skipAnim) {
+ currentView.setAlpha(1f);
+ return;
+ }
+
+ // Start a new animation if running
+ if (mRotateShowAnimator != null) mRotateShowAnimator.pause();
+ if (mRotateHideAnimator != null) mRotateHideAnimator.pause();
+
+ ObjectAnimator appearFade = ObjectAnimator.ofFloat(currentView, "alpha",
+ 0f, 1f);
+ appearFade.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS);
+ appearFade.setInterpolator(Interpolators.LINEAR);
+ mRotateShowAnimator = appearFade;
+ appearFade.start();
+
+ // Run the rotate icon's animation if it has one
+ if (animIcon != null) {
+ animIcon.reset();
+ animIcon.start();
+ }
+
+ } else { // Hide
+
+ if (skipAnim) {
+ rotBtn.setVisibility(View.INVISIBLE);
+ mNavigationBarView.notifyAccessibilitySubtreeChanged();
+ return;
+ }
+
+ // Don't start any new hide animations if one is running
+ if (mRotateHideAnimator != null && mRotateHideAnimator.isRunning()) return;
+ // Pause any active show animations but don't reset the AVD to avoid jumps
+ if (mRotateShowAnimator != null) mRotateShowAnimator.pause();
+
+ ObjectAnimator fadeOut = ObjectAnimator.ofFloat(currentView, "alpha",
+ 0f);
+ fadeOut.setDuration(BUTTON_FADE_IN_OUT_DURATION_MS);
+ fadeOut.setInterpolator(Interpolators.LINEAR);
+ fadeOut.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ rotBtn.setVisibility(View.INVISIBLE);
+ mNavigationBarView.notifyAccessibilitySubtreeChanged();
+ }
+ });
+
+ mRotateHideAnimator = fadeOut;
+ fadeOut.start();
+ }
+ }
+
// Injected from StatusBar at creation.
public void setCurrentSysuiVisibility(int systemUiVisibility) {
mSystemUiVisibility = systemUiVisibility;
@@ -406,6 +563,10 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
accessibilityButton.setOnClickListener(this::onAccessibilityClick);
accessibilityButton.setOnLongClickListener(this::onAccessibilityLongClick);
updateAccessibilityServicesState(mAccessibilityManager);
+
+ ButtonDispatcher rotateSuggestionButton = mNavigationBarView.getRotateSuggestionButton();
+ rotateSuggestionButton.setOnClickListener(this::onRotateSuggestionClick);
+ rotateSuggestionButton.setOnHoverListener(this::onRotateSuggestionHover);
}
private boolean onHomeTouch(View v, MotionEvent event) {
@@ -581,6 +742,7 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
} catch (Settings.SettingNotFoundException e) {
}
+ boolean feedbackEnabled = false;
// AccessibilityManagerService resolves services for the current user since the local
// AccessibilityManager is created from a Context with the INTERACT_ACROSS_USERS permission
final List<AccessibilityServiceInfo> services =
@@ -591,13 +753,32 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) {
requestingServices++;
}
+
+ if (info.feedbackType != 0 && info.feedbackType !=
+ AccessibilityServiceInfo.FEEDBACK_GENERIC) {
+ feedbackEnabled = true;
+ }
}
+ mAccessibilityFeedbackEnabled = feedbackEnabled;
+
final boolean showAccessibilityButton = requestingServices >= 1;
final boolean targetSelection = requestingServices >= 2;
mNavigationBarView.setAccessibilityButtonState(showAccessibilityButton, targetSelection);
}
+ private void onRotateSuggestionClick(View v) {
+ mRotationLockController.setRotationLockedAtAngle(true, mLastRotationSuggestion);
+ }
+
+ private boolean onRotateSuggestionHover(View v, MotionEvent event) {
+ final int action = event.getActionMasked();
+ mHoveringRotationSuggestion = (action == MotionEvent.ACTION_HOVER_ENTER)
+ || (action == MotionEvent.ACTION_HOVER_MOVE);
+ rescheduleRotationTimeout(true);
+ return false; // Must return false so a11y hover events are dispatched correctly.
+ }
+
// ----- Methods that StatusBar talks to (should be minimized) -----
public void setLightBarController(LightBarController lightBarController) {
@@ -645,11 +826,21 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
private final Stub mRotationWatcher = new Stub() {
@Override
- public void onRotationChanged(int rotation) throws RemoteException {
+ public void onRotationChanged(final int rotation) throws RemoteException {
// We need this to be scheduled as early as possible to beat the redrawing of
// window in response to the orientation change.
Handler h = getView().getHandler();
Message msg = Message.obtain(h, () -> {
+
+ // If the screen rotation changes while locked, potentially update lock to flow with
+ // new screen rotation and hide any showing suggestions.
+ if (mRotationLockController.isRotationLocked()) {
+ if (shouldOverrideUserLockPrefs(rotation)) {
+ mRotationLockController.setRotationLockedAtAngle(true, rotation);
+ }
+ setRotateSuggestionButtonState(false, true);
+ }
+
if (mNavigationBarView != null
&& mNavigationBarView.needsReorient(rotation)) {
repositionNavigationBar();
@@ -658,6 +849,12 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
msg.setAsynchronous(true);
h.sendMessageAtFrontOfQueue(msg);
}
+
+ private boolean shouldOverrideUserLockPrefs(final int rotation) {
+ // Only override user prefs when returning to portrait.
+ // Don't let apps that force landscape or 180 alter user lock.
+ return rotation == Surface.ROTATION_0;
+ }
};
private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
@@ -671,6 +868,31 @@ public class NavigationBarFragment extends Fragment implements Callbacks {
}
};
+ class TaskStackListenerImpl extends SysUiTaskStackChangeListener {
+ // Invalidate any rotation suggestion on task change or activity orientation change
+ // Note: all callbacks happen on main thread
+
+ @Override
+ public void onTaskStackChanged() {
+ setRotateSuggestionButtonState(false);
+ }
+
+ @Override
+ public void onTaskRemoved(int taskId) {
+ setRotateSuggestionButtonState(false);
+ }
+
+ @Override
+ public void onTaskMovedToFront(int taskId) {
+ setRotateSuggestionButtonState(false);
+ }
+
+ @Override
+ public void onActivityRequestedOrientationChanged(int taskId, int requestedOrientation) {
+ setRotateSuggestionButtonState(false);
+ }
+ }
+
public static View create(Context context, FragmentListener listener) {
WindowManager.LayoutParams lp = new WindowManager.LayoutParams(
LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT,
diff --git a/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java b/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
index bed6d821..ff923e56 100644
--- a/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
+++ b/com/android/systemui/statusbar/phone/NavigationBarGestureHelper.java
@@ -19,15 +19,14 @@ package com.android.systemui.statusbar.phone;
import android.app.ActivityManager;
import android.content.Context;
import android.content.res.Resources;
+import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.os.RemoteException;
import android.util.Log;
-import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
-import android.view.ViewConfiguration;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget;
@@ -37,18 +36,20 @@ import com.android.systemui.R;
import com.android.systemui.RecentsComponent;
import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
import com.android.systemui.shared.recents.IOverviewProxy;
+import com.android.systemui.shared.recents.utilities.Utilities;
import com.android.systemui.stackdivider.Divider;
import com.android.systemui.tuner.TunerService;
import static android.view.WindowManager.DOCKED_INVALID;
import static android.view.WindowManager.DOCKED_LEFT;
import static android.view.WindowManager.DOCKED_TOP;
+import static com.android.systemui.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
+import static com.android.systemui.OverviewProxyService.TAG_OPS;
/**
* Class to detect gestures on the navigation bar.
*/
-public class NavigationBarGestureHelper extends GestureDetector.SimpleOnGestureListener
- implements TunerService.Tunable, GestureHelper {
+public class NavigationBarGestureHelper implements TunerService.Tunable, GestureHelper {
private static final String TAG = "NavBarGestureHelper";
private static final String KEY_DOCK_WINDOW_GESTURE = "overview_nav_bar_gesture";
@@ -72,11 +73,9 @@ public class NavigationBarGestureHelper extends GestureDetector.SimpleOnGestureL
private Context mContext;
private NavigationBarView mNavigationBarView;
private boolean mIsVertical;
- private boolean mIsRTL;
- private final GestureDetector mTaskSwitcherDetector;
+ private final QuickScrubController mQuickScrubController;
private final int mScrollTouchSlop;
- private final int mMinFlingVelocity;
private final Matrix mTransformGlobalMatrix = new Matrix();
private final Matrix mTransformLocalMatrix = new Matrix();
private int mTouchDownX;
@@ -91,11 +90,9 @@ public class NavigationBarGestureHelper extends GestureDetector.SimpleOnGestureL
public NavigationBarGestureHelper(Context context) {
mContext = context;
- ViewConfiguration configuration = ViewConfiguration.get(context);
Resources r = context.getResources();
mScrollTouchSlop = r.getDimensionPixelSize(R.dimen.navigation_bar_min_swipe_distance);
- mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity();
- mTaskSwitcherDetector = new GestureDetector(context, this);
+ mQuickScrubController = new QuickScrubController(context);
Dependency.get(TunerService.class).addTunable(this, KEY_DOCK_WINDOW_GESTURE);
}
@@ -108,11 +105,12 @@ public class NavigationBarGestureHelper extends GestureDetector.SimpleOnGestureL
mRecentsComponent = recentsComponent;
mDivider = divider;
mNavigationBarView = navigationBarView;
+ mQuickScrubController.setComponents(mNavigationBarView);
}
public void setBarState(boolean isVertical, boolean isRTL) {
mIsVertical = isVertical;
- mIsRTL = isRTL;
+ mQuickScrubController.setBarState(isVertical, isRTL);
}
private boolean proxyMotionEvents(MotionEvent event) {
@@ -122,6 +120,9 @@ public class NavigationBarGestureHelper extends GestureDetector.SimpleOnGestureL
event.transform(mTransformGlobalMatrix);
try {
overviewProxy.onMotionEvent(event);
+ if (DEBUG_OVERVIEW_PROXY) {
+ Log.d(TAG_OPS, "Send MotionEvent: " + event.toString());
+ }
return true;
} catch (RemoteException e) {
Log.e(TAG, "Callback failed", e);
@@ -134,7 +135,6 @@ public class NavigationBarGestureHelper extends GestureDetector.SimpleOnGestureL
public boolean onInterceptTouchEvent(MotionEvent event) {
int action = event.getAction();
- boolean result = false;
switch (action & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN: {
mTouchDownX = (int) event.getX();
@@ -145,28 +145,26 @@ public class NavigationBarGestureHelper extends GestureDetector.SimpleOnGestureL
mNavigationBarView.transformMatrixToLocal(mTransformLocalMatrix);
break;
}
- case MotionEvent.ACTION_MOVE: {
- int x = (int) event.getX();
- int y = (int) event.getY();
- int xDiff = Math.abs(x - mTouchDownX);
- int yDiff = Math.abs(y - mTouchDownY);
- boolean exceededTouchSlop = xDiff > mScrollTouchSlop && xDiff > yDiff
- || yDiff > mScrollTouchSlop && yDiff > xDiff;
- if (exceededTouchSlop) {
- result = true;
- }
- break;
- }
- case MotionEvent.ACTION_CANCEL:
- case MotionEvent.ACTION_UP:
- break;
}
- if (!proxyMotionEvents(event)) {
- // If we move more than a fixed amount, then start capturing for the
- // task switcher detector, disabled when proxying motion events to launcher service
- mTaskSwitcherDetector.onTouchEvent(event);
+ if (!mQuickScrubController.onInterceptTouchEvent(event)) {
+ proxyMotionEvents(event);
+ return false;
+ }
+ return (mDockWindowEnabled && interceptDockWindowEvent(event));
+ }
+
+ public void onDraw(Canvas canvas) {
+ if (mOverviewEventSender.getProxy() != null) {
+ mQuickScrubController.onDraw(canvas);
}
- return result || (mDockWindowEnabled && interceptDockWindowEvent(event));
+ }
+
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ mQuickScrubController.onLayout(changed, left, top, right, bottom);
+ }
+
+ public void onDarkIntensityChange(float intensity) {
+ mQuickScrubController.onDarkIntensityChange(intensity);
}
private boolean interceptDockWindowEvent(MotionEvent event) {
@@ -306,7 +304,7 @@ public class NavigationBarGestureHelper extends GestureDetector.SimpleOnGestureL
}
public boolean onTouchEvent(MotionEvent event) {
- boolean result = proxyMotionEvents(event) || mTaskSwitcherDetector.onTouchEvent(event);
+ boolean result = mQuickScrubController.onTouchEvent(event) || proxyMotionEvents(event);
if (mDockWindowEnabled) {
result |= handleDockWindowEvent(event);
}
@@ -314,29 +312,6 @@ public class NavigationBarGestureHelper extends GestureDetector.SimpleOnGestureL
}
@Override
- public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
- float absVelX = Math.abs(velocityX);
- float absVelY = Math.abs(velocityY);
- boolean isValidFling = absVelX > mMinFlingVelocity &&
- mIsVertical ? (absVelY > absVelX) : (absVelX > absVelY);
- if (isValidFling && mRecentsComponent != null) {
- boolean showNext;
- if (!mIsRTL) {
- showNext = mIsVertical ? (velocityY < 0) : (velocityX < 0);
- } else {
- // In RTL, vertical is still the same, but horizontal is flipped
- showNext = mIsVertical ? (velocityY < 0) : (velocityX > 0);
- }
- if (showNext) {
- mRecentsComponent.showNextAffiliatedTask();
- } else {
- mRecentsComponent.showPrevAffiliatedTask();
- }
- }
- return true;
- }
-
- @Override
public void onTuningChanged(String key, String newValue) {
switch (key) {
case KEY_DOCK_WINDOW_GESTURE:
diff --git a/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java b/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
index 4e79314b..b8b309b3 100644
--- a/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
+++ b/com/android/systemui/statusbar/phone/NavigationBarInflaterView.java
@@ -63,6 +63,7 @@ public class NavigationBarInflaterView extends FrameLayout
public static final String RECENT = "recent";
public static final String NAVSPACE = "space";
public static final String CLIPBOARD = "clipboard";
+ public static final String ROTATE = "rotate";
public static final String KEY = "key";
public static final String LEFT = "left";
public static final String RIGHT = "right";
@@ -311,7 +312,7 @@ public class NavigationBarInflaterView extends FrameLayout
View v = null;
String button = extractButton(buttonSpec);
if (LEFT.equals(button)) {
- String s = Dependency.get(TunerService.class).getValue(NAV_BAR_LEFT, NAVSPACE);
+ String s = Dependency.get(TunerService.class).getValue(NAV_BAR_LEFT, ROTATE);
button = extractButton(s);
} else if (RIGHT.equals(button)) {
String s = Dependency.get(TunerService.class).getValue(NAV_BAR_RIGHT, MENU_IME);
@@ -334,6 +335,8 @@ public class NavigationBarInflaterView extends FrameLayout
v = inflater.inflate(R.layout.nav_key_space, parent, false);
} else if (CLIPBOARD.equals(button)) {
v = inflater.inflate(R.layout.clipboard, parent, false);
+ } else if (ROTATE.equals(button)) {
+ v = inflater.inflate(R.layout.rotate_suggestion, parent, false);
} else if (button.startsWith(KEY)) {
String uri = extractImage(button);
int code = extractKeycode(button);
diff --git a/com/android/systemui/statusbar/phone/NavigationBarTransitions.java b/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
index b81a3b04..e09d31ce 100644
--- a/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
+++ b/com/android/systemui/statusbar/phone/NavigationBarTransitions.java
@@ -26,7 +26,7 @@ import android.view.IWallpaperVisibilityListener;
import android.view.IWindowManager;
import android.view.MotionEvent;
import android.view.View;
-import android.view.WindowManagerGlobal;
+import android.view.View.OnLayoutChangeListener;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.Dependency;
@@ -37,10 +37,12 @@ public final class NavigationBarTransitions extends BarTransitions {
private final NavigationBarView mView;
private final IStatusBarService mBarService;
private final LightBarTransitionsController mLightTransitionsController;
+ private final boolean mAllowAutoDimWallpaperNotVisible;
private boolean mWallpaperVisible;
private boolean mLightsOut;
private boolean mAutoDim;
+ private View mNavButtons;
public NavigationBarTransitions(NavigationBarView view) {
super(view, R.drawable.nav_background);
@@ -49,6 +51,8 @@ public final class NavigationBarTransitions extends BarTransitions {
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
mLightTransitionsController = new LightBarTransitionsController(view.getContext(),
this::applyDarkIntensity);
+ mAllowAutoDimWallpaperNotVisible = view.getContext().getResources()
+ .getBoolean(R.bool.config_navigation_bar_enable_auto_dim_no_visible_wallpaper);
IWindowManager windowManagerService = Dependency.get(IWindowManager.class);
Handler handler = Handler.getMain();
@@ -64,6 +68,18 @@ public final class NavigationBarTransitions extends BarTransitions {
}, Display.DEFAULT_DISPLAY);
} catch (RemoteException e) {
}
+ mView.addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ View currentView = mView.getCurrentView();
+ if (currentView != null) {
+ mNavButtons = currentView.findViewById(R.id.nav_buttons);
+ applyLightsOut(false, true);
+ }
+ });
+ View currentView = mView.getCurrentView();
+ if (currentView != null) {
+ mNavButtons = currentView.findViewById(R.id.nav_buttons);
+ }
}
public void init() {
@@ -80,8 +96,8 @@ public final class NavigationBarTransitions extends BarTransitions {
@Override
protected boolean isLightsOut(int mode) {
- return super.isLightsOut(mode) || (mAutoDim && !mWallpaperVisible
- && mode != MODE_WARNING);
+ return super.isLightsOut(mode) || (mAllowAutoDimWallpaperNotVisible && mAutoDim
+ && !mWallpaperVisible && mode != MODE_WARNING);
}
public LightBarTransitionsController getLightTransitionsController() {
@@ -103,21 +119,20 @@ public final class NavigationBarTransitions extends BarTransitions {
if (!force && lightsOut == mLightsOut) return;
mLightsOut = lightsOut;
-
- final View navButtons = mView.getCurrentView().findViewById(R.id.nav_buttons);
+ if (mNavButtons == null) return;
// ok, everyone, stop it right there
- navButtons.animate().cancel();
+ mNavButtons.animate().cancel();
// Bump percentage by 10% if dark.
float darkBump = mLightTransitionsController.getCurrentDarkIntensity() / 10;
final float navButtonsAlpha = lightsOut ? 0.6f + darkBump : 1f;
if (!animate) {
- navButtons.setAlpha(navButtonsAlpha);
+ mNavButtons.setAlpha(navButtonsAlpha);
} else {
final int duration = lightsOut ? LIGHTS_OUT_DURATION : LIGHTS_IN_DURATION;
- navButtons.animate()
+ mNavButtons.animate()
.alpha(navButtonsAlpha)
.setDuration(duration)
.start();
@@ -136,6 +151,7 @@ public final class NavigationBarTransitions extends BarTransitions {
if (mAutoDim) {
applyLightsOut(false, true);
}
+ mView.onDarkIntensityChange(darkIntensity);
}
private final View.OnTouchListener mLightsOutListener = new View.OnTouchListener() {
diff --git a/com/android/systemui/statusbar/phone/NavigationBarView.java b/com/android/systemui/statusbar/phone/NavigationBarView.java
index 2796f0ff..445fb243 100644
--- a/com/android/systemui/statusbar/phone/NavigationBarView.java
+++ b/com/android/systemui/statusbar/phone/NavigationBarView.java
@@ -26,11 +26,13 @@ import android.app.ActivityManager;
import android.app.StatusBarManager;
import android.content.Context;
import android.content.res.Configuration;
+import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.os.Handler;
import android.os.Message;
import android.os.RemoteException;
+import android.support.annotation.ColorInt;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseArray;
@@ -55,9 +57,11 @@ import com.android.systemui.plugins.PluginListener;
import com.android.systemui.plugins.PluginManager;
import com.android.systemui.plugins.statusbar.phone.NavGesture;
import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
+import com.android.systemui.recents.SwipeUpOnboarding;
import com.android.systemui.stackdivider.Divider;
import com.android.systemui.statusbar.policy.DeadZone;
import com.android.systemui.statusbar.policy.KeyButtonDrawable;
+import com.android.systemui.statusbar.policy.TintedKeyButtonDrawable;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -94,11 +98,13 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
private KeyButtonDrawable mImeIcon;
private KeyButtonDrawable mMenuIcon;
private KeyButtonDrawable mAccessibilityIcon;
+ private KeyButtonDrawable mRotateSuggestionIcon;
private GestureHelper mGestureHelper;
private DeadZone mDeadZone;
private final NavigationBarTransitions mBarTransitions;
private final OverviewProxyService mOverviewProxyService;
+ private boolean mRecentsAnimationStarted;
// workaround for LayoutTransitions leaving the nav buttons in a weird state (bug 5549288)
final static boolean WORKAROUND_INVALID_LAYOUT = true;
@@ -120,6 +126,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
private NavigationBarInflaterView mNavigationInflaterView;
private RecentsComponent mRecentsComponent;
private Divider mDivider;
+ private SwipeUpOnboarding mSwipeUpOnboarding;
private class NavTransitionListener implements TransitionListener {
private boolean mBackTransitioning;
@@ -199,8 +206,19 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
}
}
- private final OverviewProxyListener mOverviewProxyListener =
- isConnected -> setSlippery(!isConnected);
+ private final OverviewProxyListener mOverviewProxyListener = new OverviewProxyListener() {
+ @Override
+ public void onConnectionChanged(boolean isConnected) {
+ setSlippery(!isConnected);
+ setDisabledFlags(mDisabledFlags, true);
+ setUpSwipeUpOnboarding(isConnected);
+ }
+
+ @Override
+ public void onRecentsAnimationStarted() {
+ mRecentsAnimationStarted = true;
+ }
+ };
public NavigationBarView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -227,7 +245,11 @@ 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));
+ mButtonDispatchers.put(R.id.rotate_suggestion,
+ new ButtonDispatcher(R.id.rotate_suggestion));
+
mOverviewProxyService = Dependency.get(OverviewProxyService.class);
+ mSwipeUpOnboarding = new SwipeUpOnboarding(context);
}
public BarTransitions getBarTransitions() {
@@ -257,12 +279,26 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
if (mGestureHelper.onTouchEvent(event)) {
return true;
}
- return super.onTouchEvent(event);
+ return mRecentsAnimationStarted || super.onTouchEvent(event);
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
- return mGestureHelper.onInterceptTouchEvent(event);
+ int action = event.getActionMasked();
+ if (action == MotionEvent.ACTION_DOWN) {
+ mRecentsAnimationStarted = false;
+ } else if (action == MotionEvent.ACTION_UP) {
+ // If the overview proxy service has not started the recents animation then clean up
+ // after it to ensure that the nav bar buttons still work
+ if (mOverviewProxyService.getProxy() != null && !mRecentsAnimationStarted) {
+ try {
+ ActivityManager.getService().cancelRecentsAnimation();
+ } catch (RemoteException e) {
+ Log.e(TAG, "Could not cancel recents animation");
+ }
+ }
+ }
+ return mRecentsAnimationStarted || mGestureHelper.onInterceptTouchEvent(event);
}
public void abortCurrentGesture() {
@@ -303,6 +339,10 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
return mButtonDispatchers.get(R.id.accessibility_button);
}
+ public ButtonDispatcher getRotateSuggestionButton() {
+ return mButtonDispatchers.get(R.id.rotate_suggestion);
+ }
+
public SparseArray<ButtonDispatcher> getButtonDispatchers() {
return mButtonDispatchers;
}
@@ -347,6 +387,11 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
mImeIcon = getDrawable(darkContext, lightContext,
R.drawable.ic_ime_switcher_default, R.drawable.ic_ime_switcher_default);
+ int lightColor = Utils.getColorAttr(lightContext, R.attr.singleToneColor);
+ int darkColor = Utils.getColorAttr(darkContext, R.attr.singleToneColor);
+ mRotateSuggestionIcon = getDrawable(ctx, R.drawable.ic_sysbar_rotate_button,
+ lightColor, darkColor);
+
if (ALTERNATE_CAR_MODE_UI) {
updateCarModeIcons(ctx);
}
@@ -364,6 +409,11 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
darkContext.getDrawable(darkIcon));
}
+ private KeyButtonDrawable getDrawable(Context ctx, @DrawableRes int icon,
+ @ColorInt int lightColor, @ColorInt int darkColor) {
+ return TintedKeyButtonDrawable.create(ctx.getDrawable(icon), lightColor, darkColor);
+ }
+
@Override
public void setLayoutDirection(int layoutDirection) {
// Reload all the icons
@@ -437,6 +487,8 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
setAccessibilityButtonState(mShowAccessibilityButton, mLongClickableAccessibilityButton);
getAccessibilityButton().setImageDrawable(mAccessibilityIcon);
+ getRotateSuggestionButton().setImageDrawable(mRotateSuggestionIcon);
+
setDisabledFlags(mDisabledFlags, true);
mBarTransitions.reapplyDarkIntensity();
@@ -601,6 +653,27 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
updateRotatedViews();
}
+ public void onDarkIntensityChange(float intensity) {
+ if (mGestureHelper != null) {
+ mGestureHelper.onDarkIntensityChange(intensity);
+ }
+ if (mSwipeUpOnboarding != null) {
+ mSwipeUpOnboarding.setContentDarkIntensity(intensity);
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ mGestureHelper.onDraw(canvas);
+ super.onDraw(canvas);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+ mGestureHelper.onLayout(changed, left, top, right, bottom);
+ }
+
private void updateRotatedViews() {
mRotatedViews[Surface.ROTATION_0] =
mRotatedViews[Surface.ROTATION_180] = findViewById(R.id.rot0);
@@ -697,6 +770,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
updateTaskSwitchHelper();
updateIcons(getContext(), mConfiguration, newConfig);
updateRecentsIcon();
+ mSwipeUpOnboarding.onConfigurationChanged(newConfig);
if (uiCarModeChanged || mConfiguration.densityDpi != newConfig.densityDpi
|| mConfiguration.getLayoutDirection() != newConfig.getLayoutDirection()) {
// If car mode or density changes, we need to reset the icons.
@@ -786,6 +860,7 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
Dependency.get(PluginManager.class).addPluginListener(this,
NavGesture.class, false /* Only one */);
mOverviewProxyService.addCallback(mOverviewProxyListener);
+ setUpSwipeUpOnboarding(mOverviewProxyService.getProxy() != null);
}
@Override
@@ -796,6 +871,15 @@ public class NavigationBarView extends FrameLayout implements PluginListener<Nav
mGestureHelper.destroy();
}
mOverviewProxyService.removeCallback(mOverviewProxyListener);
+ setUpSwipeUpOnboarding(false);
+ }
+
+ private void setUpSwipeUpOnboarding(boolean connectedToOverviewProxy) {
+ if (connectedToOverviewProxy) {
+ mSwipeUpOnboarding.onConnectedToLauncher(mOverviewProxyService.getLauncherComponent());
+ } else {
+ mSwipeUpOnboarding.onDisconnectedFromLauncher();
+ }
}
@Override
diff --git a/com/android/systemui/statusbar/phone/NotificationPanelView.java b/com/android/systemui/statusbar/phone/NotificationPanelView.java
index f0bd1f94..66cb59e3 100644
--- a/com/android/systemui/statusbar/phone/NotificationPanelView.java
+++ b/com/android/systemui/statusbar/phone/NotificationPanelView.java
@@ -72,6 +72,7 @@ import com.android.systemui.statusbar.policy.OnHeadsUpChangedListener;
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
import com.android.systemui.statusbar.stack.StackStateAnimator;
+import java.util.Collection;
import java.util.List;
public class NotificationPanelView extends PanelView implements
@@ -309,7 +310,7 @@ public class NotificationPanelView extends PanelView implements
mIndicationBottomPadding = getResources().getDimensionPixelSize(
R.dimen.keyguard_indication_bottom_padding);
mQsNotificationTopPadding = getResources().getDimensionPixelSize(
- R.dimen.qs_notification_keyguard_padding);
+ R.dimen.qs_notification_padding);
}
public void updateResources() {
@@ -451,7 +452,8 @@ public class NotificationPanelView extends PanelView implements
boolean animate = mNotificationStackScroller.isAddOrRemoveAnimationPending();
int stackScrollerPadding;
if (mStatusBarState != StatusBarState.KEYGUARD) {
- stackScrollerPadding = (mQs != null ? mQs.getHeader().getHeight() : 0) + mQsPeekHeight;
+ stackScrollerPadding = (mQs != null ? mQs.getHeader().getHeight() : 0) + mQsPeekHeight
+ + mQsNotificationTopPadding;
mTopPaddingAdjustment = 0;
} else {
mClockPositionAlgorithm.setup(
@@ -477,6 +479,7 @@ public class NotificationPanelView extends PanelView implements
}
mNotificationStackScroller.setIntrinsicPadding(stackScrollerPadding);
mNotificationStackScroller.setDarkShelfOffsetX(mClockPositionResult.clockX);
+ mKeyguardBottomArea.setBurnInXOffset(mClockPositionResult.clockX);
requestScrollerTopPaddingUpdate(animate);
}
@@ -1381,7 +1384,7 @@ public class NotificationPanelView extends PanelView implements
mNotificationStackScroller.getIntrinsicPadding(),
mQsMaxExpansionHeight + mQsNotificationTopPadding);
} else {
- return mQsExpansionHeight;
+ return mQsExpansionHeight + mQsNotificationTopPadding;
}
}
@@ -2607,7 +2610,8 @@ public class NotificationPanelView extends PanelView implements
private void setDarkAmount(float amount) {
mDarkAmount = amount;
- mKeyguardStatusView.setDark(mDarkAmount);
+ mKeyguardStatusView.setDarkAmount(mDarkAmount);
+ mKeyguardBottomArea.setDarkAmount(mDarkAmount);
positionClockAndNotifications();
}
@@ -2618,8 +2622,10 @@ public class NotificationPanelView extends PanelView implements
}
}
- public void setPulsing(boolean pulsing) {
- mKeyguardStatusView.setPulsing(pulsing);
+ public void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing) {
+ mKeyguardStatusView.setPulsing(pulsing != null);
+ mNotificationStackScroller.setPulsing(pulsing, mKeyguardStatusView.getLocationOnScreen()[1]
+ + mKeyguardStatusView.getClockBottom());
}
public void setAmbientIndicationBottomPadding(int ambientIndicationBottomPadding) {
@@ -2629,8 +2635,9 @@ public class NotificationPanelView extends PanelView implements
}
}
- public void refreshTime() {
+ public void dozeTimeTick() {
mKeyguardStatusView.refreshTime();
+ mKeyguardBottomArea.dozeTimeTick();
if (mDarkAmount > 0) {
positionClockAndNotifications();
}
diff --git a/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java b/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
index f41cb293..20b50182 100644
--- a/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
+++ b/com/android/systemui/statusbar/phone/PhoneStatusBarPolicy.java
@@ -39,7 +39,6 @@ import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
-import android.content.pm.UserInfo;
import android.graphics.drawable.Icon;
import android.media.AudioManager;
import android.net.Uri;
@@ -66,7 +65,6 @@ import com.android.systemui.UiOffloadThread;
import com.android.systemui.qs.tiles.DndTile;
import com.android.systemui.qs.tiles.RotationLockTile;
import com.android.systemui.recents.misc.SysUiTaskStackChangeListener;
-import com.android.systemui.recents.misc.SystemServicesProxy;
import com.android.systemui.shared.system.ActivityManagerWrapper;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.CommandQueue.Callbacks;
@@ -146,7 +144,6 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks,
private boolean mDockedStackExists;
private boolean mManagedProfileIconVisible = false;
- private boolean mManagedProfileInQuietMode = false;
private BluetoothController mBluetooth;
@@ -474,17 +471,6 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks,
}
}
- private void updateQuietState() {
- mManagedProfileInQuietMode = false;
- int currentUserId = ActivityManager.getCurrentUser();
- for (UserInfo ui : mUserManager.getEnabledProfiles(currentUserId)) {
- if (ui.isManagedProfile() && ui.isQuietModeEnabled()) {
- mManagedProfileInQuietMode = true;
- return;
- }
- }
- }
-
private void updateManagedProfile() {
// getLastResumedActivityUserId needds to acquire the AM lock, which may be contended in
// some cases. Since it doesn't really matter here whether it's updated in this frame
@@ -502,11 +488,6 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks,
mIconController.setIcon(mSlotManagedProfile,
R.drawable.stat_sys_managed_profile_status,
mContext.getString(R.string.accessibility_managed_profile));
- } else if (mManagedProfileInQuietMode) {
- showIcon = true;
- mIconController.setIcon(mSlotManagedProfile,
- R.drawable.stat_sys_managed_profile_status_off,
- mContext.getString(R.string.accessibility_managed_profile));
} else {
showIcon = false;
}
@@ -676,7 +657,6 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks,
public void onUserSwitchComplete(int newUserId) throws RemoteException {
mHandler.post(() -> {
updateAlarm();
- updateQuietState();
updateManagedProfile();
updateForegroundInstantApps();
});
@@ -685,7 +665,7 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks,
private final HotspotController.Callback mHotspotCallback = new HotspotController.Callback() {
@Override
- public void onHotspotChanged(boolean enabled) {
+ public void onHotspotChanged(boolean enabled, int numDevices) {
mIconController.setIconVisibility(mSlotHotspot, enabled);
}
};
@@ -724,7 +704,6 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks,
if (mCurrentUserSetup == userSetup) return;
mCurrentUserSetup = userSetup;
updateAlarm();
- updateQuietState();
}
@Override
@@ -793,7 +772,6 @@ public class PhoneStatusBarPolicy implements Callback, Callbacks,
} else if (action.equals(Intent.ACTION_MANAGED_PROFILE_AVAILABLE) ||
action.equals(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE) ||
action.equals(Intent.ACTION_MANAGED_PROFILE_REMOVED)) {
- updateQuietState();
updateManagedProfile();
} else if (action.equals(AudioManager.ACTION_HEADSET_PLUG)) {
updateHeadsetPlug(intent);
diff --git a/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
index 970d1de2..b1812125 100644
--- a/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
+++ b/com/android/systemui/statusbar/phone/PhoneStatusBarView.java
@@ -16,26 +16,34 @@
package com.android.systemui.statusbar.phone;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+
+import android.annotation.Nullable;
import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.Rect;
import android.util.AttributeSet;
import android.util.EventLog;
+import android.view.DisplayCutout;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
-import com.android.systemui.BatteryMeterView;
-import com.android.systemui.DejankUtils;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
import com.android.systemui.Dependency;
import com.android.systemui.EventLogTags;
import com.android.systemui.R;
import com.android.systemui.statusbar.policy.DarkIconDispatcher;
import com.android.systemui.statusbar.policy.DarkIconDispatcher.DarkReceiver;
+import com.android.systemui.util.leak.RotationUtils;
public class PhoneStatusBarView extends PanelBar {
private static final String TAG = "PhoneStatusBarView";
private static final boolean DEBUG = StatusBar.DEBUG;
private static final boolean DEBUG_GESTURES = false;
+ private static final int NO_VALUE = Integer.MIN_VALUE;
StatusBar mBar;
@@ -53,6 +61,11 @@ public class PhoneStatusBarView extends PanelBar {
}
};
private DarkReceiver mBattery;
+ private int mLastOrientation;
+ @Nullable
+ private View mCutoutSpace;
+ @Nullable
+ private DisplayCutout mDisplayCutout;
public PhoneStatusBarView(Context context, AttributeSet attrs) {
super(context, attrs);
@@ -76,6 +89,7 @@ public class PhoneStatusBarView extends PanelBar {
public void onFinishInflate() {
mBarTransitions.init();
mBattery = findViewById(R.id.battery);
+ mCutoutSpace = findViewById(R.id.cutout_space_view);
}
@Override
@@ -83,12 +97,51 @@ public class PhoneStatusBarView extends PanelBar {
super.onAttachedToWindow();
// Always have Battery meters in the status bar observe the dark/light modes.
Dependency.get(DarkIconDispatcher.class).addDarkReceiver(mBattery);
+ if (updateOrientationAndCutout(getResources().getConfiguration().orientation)) {
+ postUpdateLayoutForCutout();
+ }
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
Dependency.get(DarkIconDispatcher.class).removeDarkReceiver(mBattery);
+ mDisplayCutout = null;
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+
+ // May trigger cutout space layout-ing
+ if (updateOrientationAndCutout(newConfig.orientation)) {
+ postUpdateLayoutForCutout();
+ }
+ }
+
+ /**
+ *
+ * @param newOrientation may pass NO_VALUE for no change
+ * @return boolean indicating if we need to update the cutout location / margins
+ */
+ private boolean updateOrientationAndCutout(int newOrientation) {
+ boolean changed = false;
+ if (newOrientation != NO_VALUE) {
+ if (mLastOrientation != newOrientation) {
+ changed = true;
+ mLastOrientation = newOrientation;
+ }
+ }
+
+ if (mDisplayCutout == null) {
+ DisplayCutout cutout = getRootWindowInsets().getDisplayCutout();
+ if (cutout != null) {
+ changed = true;
+ mDisplayCutout = cutout;
+ }
+ }
+
+ return changed;
}
@Override
@@ -214,4 +267,80 @@ public class PhoneStatusBarView extends PanelBar {
R.dimen.status_bar_height);
setLayoutParams(layoutParams);
}
+
+ private void updateLayoutForCutout() {
+ updateCutoutLocation();
+ updateSafeInsets();
+ }
+
+ private void postUpdateLayoutForCutout() {
+ Runnable r = new Runnable() {
+ @Override
+ public void run() {
+ updateLayoutForCutout();
+ }
+ };
+ // Let the cutout emulation draw first
+ postDelayed(r, 0);
+ }
+
+ private void updateCutoutLocation() {
+ // Not all layouts have a cutout (e.g., Car)
+ if (mCutoutSpace == null) {
+ return;
+ }
+
+ if (mDisplayCutout == null || mDisplayCutout.isEmpty()
+ || mLastOrientation != ORIENTATION_PORTRAIT) {
+ mCutoutSpace.setVisibility(View.GONE);
+ return;
+ }
+
+ mCutoutSpace.setVisibility(View.VISIBLE);
+ LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mCutoutSpace.getLayoutParams();
+ lp.width = mDisplayCutout.getBoundingRect().width();
+ lp.height = mDisplayCutout.getBoundingRect().height();
+ }
+
+ private void updateSafeInsets() {
+ // Depending on our rotation, we may have to work around a cutout in the middle of the view,
+ // or letterboxing from the right or left sides.
+
+ FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) getLayoutParams();
+ if (mDisplayCutout == null || mDisplayCutout.isEmpty()) {
+ lp.leftMargin = 0;
+ lp.rightMargin = 0;
+ return;
+ }
+
+ int leftMargin = 0;
+ int rightMargin = 0;
+ switch (RotationUtils.getRotation(getContext())) {
+ /*
+ * Landscape: <-|
+ * Seascape: |->
+ */
+ case RotationUtils.ROTATION_LANDSCAPE:
+ leftMargin = getDisplayCutoutHeight();
+ break;
+ case RotationUtils.ROTATION_SEASCAPE:
+ rightMargin = getDisplayCutoutHeight();
+ break;
+ default:
+ break;
+ }
+
+ lp.leftMargin = leftMargin;
+ lp.rightMargin = rightMargin;
+ }
+
+ //TODO: Find a better way
+ private int getDisplayCutoutHeight() {
+ if (mDisplayCutout == null || mDisplayCutout.isEmpty()) {
+ return 0;
+ }
+
+ Rect r = mDisplayCutout.getBoundingRect();
+ return r.bottom - r.top;
+ }
}
diff --git a/com/android/systemui/statusbar/phone/QuickScrubController.java b/com/android/systemui/statusbar/phone/QuickScrubController.java
new file mode 100644
index 00000000..ee1d0887
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/QuickScrubController.java
@@ -0,0 +1,402 @@
+/*
+ * 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 com.android.systemui.statusbar.phone;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.SystemProperties;
+import android.util.Log;
+import android.util.Slog;
+import android.view.Display;
+import android.view.GestureDetector;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.WindowManager;
+import android.view.WindowManagerGlobal;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.support.annotation.DimenRes;
+import com.android.systemui.Dependency;
+import com.android.systemui.OverviewProxyService;
+import com.android.systemui.R;
+import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
+import com.android.systemui.shared.recents.IOverviewProxy;
+import com.android.systemui.shared.recents.utilities.Utilities;
+
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_LEFT;
+import static android.view.WindowManagerPolicyConstants.NAV_BAR_BOTTOM;
+import static com.android.systemui.OverviewProxyService.DEBUG_OVERVIEW_PROXY;
+import static com.android.systemui.OverviewProxyService.TAG_OPS;
+
+/**
+ * Class to detect gestures on the navigation bar and implement quick scrub and switch.
+ */
+public class QuickScrubController extends GestureDetector.SimpleOnGestureListener implements
+ GestureHelper {
+
+ private static final String TAG = "QuickScrubController";
+ private static final int QUICK_SWITCH_FLING_VELOCITY = 0;
+ private static final int ANIM_DURATION_MS = 200;
+ private static final long LONG_PRESS_DELAY_MS = 150;
+
+ /**
+ * For quick step, set a damping value to allow the button to stick closer its origin position
+ * when dragging before quick scrub is active.
+ */
+ private static final int SWITCH_STICKINESS = 4;
+
+ private NavigationBarView mNavigationBarView;
+ private GestureDetector mGestureDetector;
+
+ private boolean mDraggingActive;
+ private boolean mQuickScrubActive;
+ private float mDownOffset;
+ private float mTranslation;
+ private int mTouchDownX;
+ private int mTouchDownY;
+ private boolean mDragPositive;
+ private boolean mIsVertical;
+ private boolean mIsRTL;
+ private float mMaxTrackPaintAlpha;
+
+ private final Handler mHandler = new Handler();
+ private final Interpolator mQuickScrubEndInterpolator = new DecelerateInterpolator();
+ private final Rect mTrackRect = new Rect();
+ private final Rect mHomeButtonRect = new Rect();
+ private final Paint mTrackPaint = new Paint();
+ private final int mScrollTouchSlop;
+ private final OverviewProxyService mOverviewEventSender;
+ private final Display mDisplay;
+ private final int mTrackThickness;
+ private final int mTrackPadding;
+ private final ValueAnimator mTrackAnimator;
+ private final ValueAnimator mButtonAnimator;
+ private final AnimatorSet mQuickScrubEndAnimator;
+ private final Context mContext;
+
+ private final AnimatorUpdateListener mTrackAnimatorListener = valueAnimator -> {
+ mTrackPaint.setAlpha(Math.round((float) valueAnimator.getAnimatedValue() * 255));
+ mNavigationBarView.invalidate();
+ };
+
+ private final AnimatorUpdateListener mButtonTranslationListener = animator -> {
+ int pos = (int) animator.getAnimatedValue();
+ if (!mQuickScrubActive) {
+ pos = mDragPositive ? Math.min((int) mTranslation, pos) : Math.max((int) mTranslation, pos);
+ }
+ final View homeView = mNavigationBarView.getHomeButton().getCurrentView();
+ if (mIsVertical) {
+ homeView.setTranslationY(pos);
+ } else {
+ homeView.setTranslationX(pos);
+ }
+ };
+
+ private AnimatorListenerAdapter mQuickScrubEndListener = new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mNavigationBarView.getHomeButton().setClickable(true);
+ mQuickScrubActive = false;
+ mTranslation = 0;
+ }
+ };
+
+ private Runnable mLongPressRunnable = this::startQuickScrub;
+
+ private final GestureDetector.SimpleOnGestureListener mGestureListener =
+ new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velX, float velY) {
+ if (!isQuickScrubEnabled() || mQuickScrubActive) {
+ return false;
+ }
+ float velocityX = mIsRTL ? -velX : velX;
+ float absVelY = Math.abs(velY);
+ final boolean isValidFling = velocityX > QUICK_SWITCH_FLING_VELOCITY &&
+ mIsVertical ? (absVelY > velocityX) : (velocityX > absVelY);
+ if (isValidFling) {
+ mDraggingActive = false;
+ mButtonAnimator.setIntValues((int) mTranslation, 0);
+ mButtonAnimator.start();
+ mHandler.removeCallbacks(mLongPressRunnable);
+ try {
+ final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy();
+ overviewProxy.onQuickSwitch();
+ if (DEBUG_OVERVIEW_PROXY) {
+ Log.d(TAG_OPS, "Quick Switch");
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send start of quick switch.", e);
+ }
+ }
+ return true;
+ }
+ };
+
+ public QuickScrubController(Context context) {
+ mContext = context;
+ mScrollTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+ mDisplay = ((WindowManager) context.getSystemService(
+ Context.WINDOW_SERVICE)).getDefaultDisplay();
+ mOverviewEventSender = Dependency.get(OverviewProxyService.class);
+ mGestureDetector = new GestureDetector(mContext, mGestureListener);
+ mTrackThickness = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_thickness);
+ mTrackPadding = getDimensionPixelSize(mContext, R.dimen.nav_quick_scrub_track_edge_padding);
+
+ mTrackAnimator = ObjectAnimator.ofFloat();
+ mTrackAnimator.addUpdateListener(mTrackAnimatorListener);
+ mButtonAnimator = ObjectAnimator.ofInt();
+ mButtonAnimator.addUpdateListener(mButtonTranslationListener);
+ mQuickScrubEndAnimator = new AnimatorSet();
+ mQuickScrubEndAnimator.playTogether(mTrackAnimator, mButtonAnimator);
+ mQuickScrubEndAnimator.setDuration(ANIM_DURATION_MS);
+ mQuickScrubEndAnimator.addListener(mQuickScrubEndListener);
+ mQuickScrubEndAnimator.setInterpolator(mQuickScrubEndInterpolator);
+ }
+
+ public void setComponents(NavigationBarView navigationBarView) {
+ mNavigationBarView = navigationBarView;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ final IOverviewProxy overviewProxy = mOverviewEventSender.getProxy();
+ final ButtonDispatcher homeButton = mNavigationBarView.getHomeButton();
+ if (overviewProxy == null) {
+ homeButton.setDelayTouchFeedback(false);
+ return false;
+ }
+ mGestureDetector.onTouchEvent(event);
+ int action = event.getAction();
+ switch (action & MotionEvent.ACTION_MASK) {
+ case MotionEvent.ACTION_DOWN: {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+ if (isQuickScrubEnabled() && mHomeButtonRect.contains(x, y)) {
+ mTouchDownX = x;
+ mTouchDownY = y;
+ homeButton.setDelayTouchFeedback(true);
+ mHandler.postDelayed(mLongPressRunnable, LONG_PRESS_DELAY_MS);
+ } else {
+ homeButton.setDelayTouchFeedback(false);
+ mTouchDownX = mTouchDownY = -1;
+ }
+ break;
+ }
+ case MotionEvent.ACTION_MOVE: {
+ if (mTouchDownX != -1) {
+ int x = (int) event.getX();
+ int y = (int) event.getY();
+ int xDiff = Math.abs(x - mTouchDownX);
+ int yDiff = Math.abs(y - mTouchDownY);
+ boolean exceededTouchSlopX = xDiff > mScrollTouchSlop && xDiff > yDiff;
+ boolean exceededTouchSlopY = yDiff > mScrollTouchSlop && yDiff > xDiff;
+ boolean exceededTouchSlop, exceededPerpendicularTouchSlop;
+ int pos, touchDown, offset, trackSize;
+
+ if (mIsVertical) {
+ exceededTouchSlop = exceededTouchSlopY;
+ exceededPerpendicularTouchSlop = exceededTouchSlopX;
+ pos = y;
+ touchDown = mTouchDownY;
+ offset = pos - mTrackRect.top;
+ trackSize = mTrackRect.height();
+ } else {
+ exceededTouchSlop = exceededTouchSlopX;
+ exceededPerpendicularTouchSlop = exceededTouchSlopY;
+ pos = x;
+ touchDown = mTouchDownX;
+ offset = pos - mTrackRect.left;
+ trackSize = mTrackRect.width();
+ }
+ // Do not start scrubbing when dragging in the perpendicular direction
+ if (!mDraggingActive && exceededPerpendicularTouchSlop) {
+ mHandler.removeCallbacksAndMessages(null);
+ return false;
+ }
+ if (!mDragPositive) {
+ offset -= mIsVertical ? mTrackRect.height() : mTrackRect.width();
+ }
+
+ // Control the button movement
+ if (!mDraggingActive && exceededTouchSlop) {
+ boolean allowDrag = !mDragPositive
+ ? offset < 0 && pos < touchDown : offset >= 0 && pos > touchDown;
+ if (allowDrag) {
+ mDownOffset = offset;
+ homeButton.setClickable(false);
+ mDraggingActive = true;
+ }
+ }
+ if (mDraggingActive && (mDragPositive && offset >= 0
+ || !mDragPositive && offset <= 0)) {
+ float scrubFraction =
+ Utilities.clamp(Math.abs(offset) * 1f / trackSize, 0, 1);
+ mTranslation = !mDragPositive
+ ? Utilities.clamp(offset - mDownOffset, -trackSize, 0)
+ : Utilities.clamp(offset - mDownOffset, 0, trackSize);
+ if (mQuickScrubActive) {
+ try {
+ overviewProxy.onQuickScrubProgress(scrubFraction);
+ if (DEBUG_OVERVIEW_PROXY) {
+ Log.d(TAG_OPS, "Quick Scrub Progress:" + scrubFraction);
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send progress of quick scrub.", e);
+ }
+ } else {
+ mTranslation /= SWITCH_STICKINESS;
+ }
+ if (mIsVertical) {
+ homeButton.getCurrentView().setTranslationY(mTranslation);
+ } else {
+ homeButton.getCurrentView().setTranslationX(mTranslation);
+ }
+ }
+ }
+ break;
+ }
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_UP:
+ endQuickScrub();
+ break;
+ }
+ return mDraggingActive || mQuickScrubActive;
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ canvas.drawRect(mTrackRect, mTrackPaint);
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ final int width = right - left;
+ final int height = bottom - top;
+ final int x1, x2, y1, y2;
+ if (mIsVertical) {
+ x1 = (width - mTrackThickness) / 2;
+ x2 = x1 + mTrackThickness;
+ y1 = mDragPositive ? height / 2 : mTrackPadding;
+ y2 = y1 + height / 2 - mTrackPadding;
+ } else {
+ y1 = (height - mTrackThickness) / 2;
+ y2 = y1 + mTrackThickness;
+ x1 = mDragPositive ? width / 2 : mTrackPadding;
+ x2 = x1 + width / 2 - mTrackPadding;
+ }
+ mTrackRect.set(x1, y1, x2, y2);
+
+ // Get the touch rect of the home button location
+ View homeView = mNavigationBarView.getHomeButton().getCurrentView();
+ if (homeView != null) {
+ int[] globalHomePos = homeView.getLocationOnScreen();
+ int[] globalNavBarPos = mNavigationBarView.getLocationOnScreen();
+ int homeX = globalHomePos[0] - globalNavBarPos[0];
+ int homeY = globalHomePos[1] - globalNavBarPos[1];
+ mHomeButtonRect.set(homeX, homeY, homeX + homeView.getMeasuredWidth(),
+ homeY + homeView.getMeasuredHeight());
+ }
+ }
+
+ @Override
+ public void onDarkIntensityChange(float intensity) {
+ if (intensity == 0) {
+ mTrackPaint.setColor(mContext.getColor(R.color.quick_step_track_background_light));
+ } else if (intensity == 1) {
+ mTrackPaint.setColor(mContext.getColor(R.color.quick_step_track_background_dark));
+ }
+ mMaxTrackPaintAlpha = mTrackPaint.getAlpha() * 1f / 255;
+ mTrackPaint.setAlpha(0);
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (event.getAction() == MotionEvent.ACTION_UP) {
+ endQuickScrub();
+ }
+ return false;
+ }
+
+ @Override
+ public void setBarState(boolean isVertical, boolean isRTL) {
+ mIsVertical = isVertical;
+ mIsRTL = isRTL;
+ try {
+ int navbarPos = WindowManagerGlobal.getWindowManagerService().getNavBarPosition();
+ mDragPositive = navbarPos == NAV_BAR_LEFT || navbarPos == NAV_BAR_BOTTOM;
+ if (isRTL) {
+ mDragPositive = !mDragPositive;
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "Failed to get nav bar position.", e);
+ }
+ }
+
+ boolean isQuickScrubEnabled() {
+ return SystemProperties.getBoolean("persist.quickstep.scrub.enabled", false);
+ }
+
+ private void startQuickScrub() {
+ if (!mQuickScrubActive) {
+ mQuickScrubActive = true;
+ mTrackAnimator.setFloatValues(0, mMaxTrackPaintAlpha);
+ mTrackAnimator.start();
+ try {
+ mOverviewEventSender.getProxy().onQuickScrubStart();
+ if (DEBUG_OVERVIEW_PROXY) {
+ Log.d(TAG_OPS, "Quick Scrub Start");
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send start of quick scrub.", e);
+ }
+ }
+ }
+
+ private void endQuickScrub() {
+ mHandler.removeCallbacks(mLongPressRunnable);
+ if (mDraggingActive || mQuickScrubActive) {
+ mButtonAnimator.setIntValues((int) mTranslation, 0);
+ mTrackAnimator.setFloatValues(mTrackPaint.getAlpha() * 1f / 255, 0);
+ mQuickScrubEndAnimator.start();
+ try {
+ mOverviewEventSender.getProxy().onQuickScrubEnd();
+ if (DEBUG_OVERVIEW_PROXY) {
+ Log.d(TAG_OPS, "Quick Scrub End");
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Failed to send end of quick scrub.", e);
+ }
+ }
+ mDraggingActive = false;
+ }
+
+ private int getDimensionPixelSize(Context context, @DimenRes int resId) {
+ return context.getResources().getDimensionPixelSize(resId);
+ }
+}
diff --git a/com/android/systemui/statusbar/phone/ScrimController.java b/com/android/systemui/statusbar/phone/ScrimController.java
index 14329b56..3b394ddd 100644
--- a/com/android/systemui/statusbar/phone/ScrimController.java
+++ b/com/android/systemui/statusbar/phone/ScrimController.java
@@ -866,13 +866,13 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener,
@Override
public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
- pw.println(" ScrimController:");
- pw.print(" state:"); pw.println(mState);
- pw.print(" frontScrim:"); pw.print(" viewAlpha="); pw.print(mScrimInFront.getViewAlpha());
+ pw.println(" ScrimController: ");
+ pw.print(" state: "); pw.println(mState);
+ pw.print(" frontScrim:"); pw.print(" viewAlpha="); pw.print(mScrimInFront.getViewAlpha());
pw.print(" alpha="); pw.print(mCurrentInFrontAlpha);
pw.print(" tint=0x"); pw.println(Integer.toHexString(mScrimInFront.getTint()));
- pw.print(" backScrim:"); pw.print(" viewAlpha="); pw.print(mScrimBehind.getViewAlpha());
+ pw.print(" backScrim:"); pw.print(" viewAlpha="); pw.print(mScrimBehind.getViewAlpha());
pw.print(" alpha="); pw.print(mCurrentBehindAlpha);
pw.print(" tint=0x"); pw.println(Integer.toHexString(mScrimBehind.getTint()));
diff --git a/com/android/systemui/statusbar/phone/SettingsButton.java b/com/android/systemui/statusbar/phone/SettingsButton.java
index 6220fcbd..1130b6de 100644
--- a/com/android/systemui/statusbar/phone/SettingsButton.java
+++ b/com/android/systemui/statusbar/phone/SettingsButton.java
@@ -32,6 +32,8 @@ import com.android.systemui.Interpolators;
public class SettingsButton extends AlphaOptimizedImageButton {
+ private static final boolean TUNER_ENABLE_AVAILABLE = false;
+
private static final long LONG_PRESS_LENGTH = 1000;
private static final long ACCEL_LENGTH = 750;
private static final long FULL_SPEED_LENGTH = 375;
@@ -59,7 +61,7 @@ public class SettingsButton extends AlphaOptimizedImageButton {
public boolean onTouchEvent(MotionEvent event) {
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
- postDelayed(mLongPressCallback, LONG_PRESS_LENGTH);
+ if (TUNER_ENABLE_AVAILABLE) postDelayed(mLongPressCallback, LONG_PRESS_LENGTH);
break;
case MotionEvent.ACTION_UP:
if (mUpToSpeed) {
diff --git a/com/android/systemui/statusbar/phone/StatusBar.java b/com/android/systemui/statusbar/phone/StatusBar.java
index 2da1e4d1..d13ecaeb 100644
--- a/com/android/systemui/statusbar/phone/StatusBar.java
+++ b/com/android/systemui/statusbar/phone/StatusBar.java
@@ -85,6 +85,7 @@ import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
import android.os.Message;
import android.os.PowerManager;
import android.os.RemoteException;
@@ -138,6 +139,7 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.keyguard.ViewMediatorCallback;
import com.android.systemui.ActivityStarterDelegate;
import com.android.systemui.AutoReinflateContainer;
+import com.android.systemui.charging.WirelessChargingAnimation;
import com.android.systemui.DemoMode;
import com.android.systemui.Dependency;
import com.android.systemui.EventLogTags;
@@ -1419,7 +1421,6 @@ public class StatusBar extends SystemUI implements DemoMode,
mQSPanel.clickTile(tile);
}
-
private void updateClearAll() {
if (!mClearAllEnabled) {
return;
@@ -1596,7 +1597,7 @@ public class StatusBar extends SystemUI implements DemoMode,
final boolean hasArtwork = artworkDrawable != null;
- if ((hasArtwork || DEBUG_MEDIA_FAKE_ARTWORK)
+ if ((hasArtwork || DEBUG_MEDIA_FAKE_ARTWORK) && !mDozing
&& (mState != StatusBarState.SHADE || allowWhenShade)
&& mFingerprintUnlockController.getMode()
!= FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
@@ -1657,15 +1658,16 @@ public class StatusBar extends SystemUI implements DemoMode,
}
}
} else {
- // need to hide the album art, either because we are unlocked or because
- // the metadata isn't there to support it
+ // need to hide the album art, either because we are unlocked, on AOD
+ // or because the metadata isn't there to support it
if (mBackdrop.getVisibility() != View.GONE) {
if (DEBUG_MEDIA) {
Log.v(TAG, "DEBUG_MEDIA: Fading out album artwork");
}
+ boolean cannotAnimateDoze = mDozing && !ScrimState.AOD.getAnimateChange();
if (mFingerprintUnlockController.getMode()
== FingerprintUnlockController.MODE_WAKE_AND_UNLOCK_PULSING
- || hideBecauseOccluded) {
+ || hideBecauseOccluded || cannotAnimateDoze) {
// We are unlocking directly - no animation!
mBackdrop.setVisibility(View.GONE);
@@ -1703,7 +1705,7 @@ public class StatusBar extends SystemUI implements DemoMode,
if (mReportRejectedTouch == null) {
return;
}
- mReportRejectedTouch.setVisibility(mState == StatusBarState.KEYGUARD
+ mReportRejectedTouch.setVisibility(mState == StatusBarState.KEYGUARD && !mDozing
&& mFalsingManager.isReportingEnabled() ? View.VISIBLE : View.INVISIBLE);
}
@@ -2421,6 +2423,18 @@ public class StatusBar extends SystemUI implements DemoMode,
mask, fullscreenStackBounds, dockedStackBounds, sbModeChanged, mStatusBarMode);
}
+ @Override
+ public void showChargingAnimation(int batteryLevel) {
+ if (mDozing) {
+ // ambient
+ } else if (mKeyguardManager.isKeyguardLocked()) {
+ // lockscreen
+ } else {
+ WirelessChargingAnimation.makeWirelessChargingAnimation(mContext, null,
+ batteryLevel).show();
+ }
+ }
+
void touchAutoHide() {
// update transient bar autohide
if (mStatusBarMode == MODE_SEMI_TRANSPARENT || (mNavigationBar != null
@@ -2662,6 +2676,10 @@ public class StatusBar extends SystemUI implements DemoMode,
mFingerprintUnlockController.dump(pw);
}
+ if (mKeyguardIndicationController != null) {
+ mKeyguardIndicationController.dump(fd, pw, args);
+ }
+
if (mScrimController != null) {
mScrimController.dump(fd, pw, args);
}
@@ -4505,6 +4523,7 @@ public class StatusBar extends SystemUI implements DemoMode,
((DozeReceiver) mAmbientIndicationContainer).setDozing(mDozing);
}
updateDozingState();
+ updateReportRejectedTouchVisibility();
Trace.endSection();
}
@@ -4614,8 +4633,7 @@ public class StatusBar extends SystemUI implements DemoMode,
}
private void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing) {
- mStackScroller.setPulsing(pulsing);
- mNotificationPanel.setPulsing(pulsing != null);
+ mNotificationPanel.setPulsing(pulsing);
mVisualStabilityManager.setPulsing(pulsing != null);
mIgnoreTouchWhilePulsing = false;
}
@@ -4644,7 +4662,7 @@ public class StatusBar extends SystemUI implements DemoMode,
@Override
public void dozeTimeTick() {
- mNotificationPanel.refreshTime();
+ mNotificationPanel.dozeTimeTick();
}
@Override
@@ -4930,18 +4948,11 @@ public class StatusBar extends SystemUI implements DemoMode,
// 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();
- }
- });
+ removeNotification(parentToCancelFinal);
+ }
+ if (shouldAutoCancel(sbn)) {
+ // Automatically remove all notifications that we may have kept around longer
+ removeNotification(sbn);
}
};
@@ -4965,6 +4976,21 @@ public class StatusBar extends SystemUI implements DemoMode,
}, afterKeyguardGone);
}
+ private void removeNotification(StatusBarNotification notification) {
+ // We have to post it to the UI thread for synchronization
+ mHandler.post(() -> {
+ Runnable removeRunnable =
+ () -> mEntryManager.performRemoveNotification(notification);
+ if (isCollapsing()) {
+ // To avoid lags we're only performing the remove
+ // after the shade was collapsed
+ addPostCollapseAction(removeRunnable);
+ } else {
+ removeRunnable.run();
+ }
+ });
+ }
+
protected NotificationListener mNotificationListener;
protected void notifyUserAboutHiddenNotifications() {
diff --git a/com/android/systemui/statusbar/phone/StatusBarIconController.java b/com/android/systemui/statusbar/phone/StatusBarIconController.java
index bcda60eb..07610cef 100644
--- a/com/android/systemui/statusbar/phone/StatusBarIconController.java
+++ b/com/android/systemui/statusbar/phone/StatusBarIconController.java
@@ -62,7 +62,7 @@ public interface StatusBarIconController {
}
/**
- * Version of ViewGroup that observers state from the DarkIconDispatcher.
+ * Version of ViewGroup that observes state from the DarkIconDispatcher.
*/
public static class DarkIconManager extends IconManager {
private final DarkIconDispatcher mDarkIconDispatcher;
diff --git a/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 8504d8e5..c6673095 100644
--- a/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -175,13 +175,18 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
public void dismissWithAction(OnDismissAction r, Runnable cancelAction,
boolean afterKeyguardGone) {
+ dismissWithAction(r, cancelAction, afterKeyguardGone, null /* message */);
+ }
+
+ public void dismissWithAction(OnDismissAction r, Runnable cancelAction,
+ boolean afterKeyguardGone, String message) {
if (mShowing) {
cancelPendingWakeupAction();
// If we're dozing, this needs to be delayed until after we wake up - unless we're
// wake-and-unlocking, because there dozing will last until the end of the transition.
if (mDozing && !isWakeAndUnlocking()) {
mPendingWakeupAction = new DismissWithActionRequest(
- r, cancelAction, afterKeyguardGone);
+ r, cancelAction, afterKeyguardGone, message);
return;
}
@@ -632,7 +637,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
if (request != null) {
if (mShowing) {
dismissWithAction(request.dismissAction, request.cancelAction,
- request.afterKeyguardGone);
+ request.afterKeyguardGone, request.message);
} else if (request.dismissAction != null) {
request.dismissAction.onDismiss();
}
@@ -651,12 +656,14 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb
final OnDismissAction dismissAction;
final Runnable cancelAction;
final boolean afterKeyguardGone;
+ final String message;
DismissWithActionRequest(OnDismissAction dismissAction, Runnable cancelAction,
- boolean afterKeyguardGone) {
+ boolean afterKeyguardGone, String message) {
this.dismissAction = dismissAction;
this.cancelAction = cancelAction;
this.afterKeyguardGone = afterKeyguardGone;
+ this.message = message;
}
}
}
diff --git a/com/android/systemui/statusbar/phone/StatusIconContainer.java b/com/android/systemui/statusbar/phone/StatusIconContainer.java
new file mode 100644
index 00000000..1897171b
--- /dev/null
+++ b/com/android/systemui/statusbar/phone/StatusIconContainer.java
@@ -0,0 +1,169 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * A container for Status bar system icons. Limits the number of system icons and handles overflow
+ * similar to NotificationIconController. Can be used to layout nested StatusIconContainers
+ *
+ * Children are expected to be of type StatusBarIconView.
+ */
+package com.android.systemui.statusbar.phone;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+
+import android.view.View;
+import com.android.keyguard.AlphaOptimizedLinearLayout;
+import com.android.systemui.R;
+import com.android.systemui.statusbar.StatusBarIconView;
+import com.android.systemui.statusbar.stack.ViewState;
+
+public class StatusIconContainer extends AlphaOptimizedLinearLayout {
+
+ private static final String TAG = "StatusIconContainer";
+ private static final int MAX_ICONS = 5;
+ private static final int MAX_DOTS = 3;
+
+ public StatusIconContainer(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ float midY = getHeight() / 2.0f;
+
+ // Layout all child views so that we can move them around later
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ int width = child.getMeasuredWidth();
+ int height = child.getMeasuredHeight();
+ int top = (int) (midY - height / 2.0f);
+ child.layout(0, top, width, top + height);
+ }
+
+ resetViewStates();
+ calculateIconTranslations();
+ applyIconStates();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ final int count = getChildCount();
+ // Measure all children so that they report the correct width
+ for (int i = 0; i < count; i++) {
+ measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
+ }
+ }
+
+ @Override
+ public void onViewAdded(View child) {
+ super.onViewAdded(child);
+ ViewState vs = new ViewState();
+ child.setTag(R.id.status_bar_view_state_tag, vs);
+ }
+
+ @Override
+ public void onViewRemoved(View child) {
+ super.onViewRemoved(child);
+ child.setTag(R.id.status_bar_view_state_tag, null);
+ }
+
+ /**
+ * Layout is happening from end -> start
+ */
+ private void calculateIconTranslations() {
+ float translationX = getWidth();
+ float contentStart = getPaddingStart();
+ int childCount = getChildCount();
+ // Underflow === don't show content until that index
+ int firstUnderflowIndex = -1;
+ android.util.Log.d(TAG, "calculateIconTransitions: start=" + translationX);
+
+ //TODO: Dots
+ for (int i = childCount - 1; i >= 0; i--) {
+ View child = getChildAt(i);
+ if (!(child instanceof StatusBarIconView)) {
+ continue;
+ }
+
+ ViewState childState = getViewStateFromChild(child);
+ if (childState == null ) {
+ continue;
+ }
+
+ // Rely on StatusBarIcon for truth about visibility
+ if (!((StatusBarIconView) child).getStatusBarIcon().visible) {
+ childState.hidden = true;
+ continue;
+ }
+
+ childState.xTranslation = translationX - child.getWidth();
+
+ if (childState.xTranslation < contentStart) {
+ if (firstUnderflowIndex == -1) {
+ firstUnderflowIndex = i;
+ }
+ }
+
+ translationX -= child.getWidth();
+ }
+
+ if (firstUnderflowIndex != -1) {
+ for (int i = 0; i <= firstUnderflowIndex; i++) {
+ View child = getChildAt(i);
+ ViewState vs = getViewStateFromChild(child);
+ if (vs != null) {
+ vs.hidden = true;
+ }
+ }
+ }
+ }
+
+ private void applyIconStates() {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ ViewState vs = getViewStateFromChild(child);
+ if (vs != null) {
+ vs.applyToView(child);
+ }
+ }
+ }
+
+ private void resetViewStates() {
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ ViewState vs = getViewStateFromChild(child);
+ if (vs == null) {
+ continue;
+ }
+
+ vs.initFrom(child);
+ vs.alpha = 1.0f;
+ if (child instanceof StatusBarIconView) {
+ vs.hidden = !((StatusBarIconView)child).getStatusBarIcon().visible;
+ } else {
+ vs.hidden = false;
+ }
+ }
+ }
+
+ private static @Nullable ViewState getViewStateFromChild(View child) {
+ return (ViewState) child.getTag(R.id.status_bar_view_state_tag);
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
index 3b15c2b8..fcf084b2 100644
--- a/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
+++ b/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java
@@ -276,6 +276,9 @@ public class BluetoothControllerImpl implements BluetoothController, BluetoothCa
mHandler.sendEmptyMessage(H.MSG_STATE_CHANGED);
}
+ @Override
+ public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice, int bluetoothProfile) {}
+
private ActuallyCachedState getCachedState(CachedBluetoothDevice device) {
ActuallyCachedState state = mCachedState.get(device);
if (state == null) {
diff --git a/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java b/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java
index 29519434..2ede327a 100644
--- a/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java
+++ b/com/android/systemui/statusbar/policy/DataSaverControllerImpl.java
@@ -74,17 +74,9 @@ public class DataSaverControllerImpl implements DataSaverController {
}
}
- private final INetworkPolicyListener mPolicyListener = new INetworkPolicyListener.Stub() {
+ private final INetworkPolicyListener mPolicyListener = new NetworkPolicyManager.Listener() {
@Override
- public void onUidRulesChanged(int uid, int uidRules) throws RemoteException {
- }
-
- @Override
- public void onMeteredIfacesChanged(String[] strings) throws RemoteException {
- }
-
- @Override
- public void onRestrictBackgroundChanged(final boolean isDataSaving) throws RemoteException {
+ public void onRestrictBackgroundChanged(final boolean isDataSaving) {
mHandler.post(new Runnable() {
@Override
public void run() {
@@ -92,10 +84,6 @@ public class DataSaverControllerImpl implements DataSaverController {
}
});
}
-
- @Override
- public void onUidPoliciesChanged(int uid, int uidPolicies) throws RemoteException {
- }
};
}
diff --git a/com/android/systemui/statusbar/policy/HotspotController.java b/com/android/systemui/statusbar/policy/HotspotController.java
index 6457209a..830b50e3 100644
--- a/com/android/systemui/statusbar/policy/HotspotController.java
+++ b/com/android/systemui/statusbar/policy/HotspotController.java
@@ -26,7 +26,9 @@ public interface HotspotController extends CallbackController<Callback>, Dumpabl
void setHotspotEnabled(boolean enabled);
boolean isHotspotSupported();
- public interface Callback {
- void onHotspotChanged(boolean enabled);
+ int getNumConnectedDevices();
+
+ interface Callback {
+ void onHotspotChanged(boolean enabled, int numDevices);
}
}
diff --git a/com/android/systemui/statusbar/policy/HotspotControllerImpl.java b/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
index 1ebb986e..8792b4f3 100644
--- a/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
+++ b/com/android/systemui/statusbar/policy/HotspotControllerImpl.java
@@ -23,31 +23,35 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.wifi.WifiManager;
-import android.os.Handler;
import android.os.UserManager;
import android.util.Log;
+import com.android.systemui.Dependency;
+
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayList;
-public class HotspotControllerImpl implements HotspotController {
+public class HotspotControllerImpl implements HotspotController, WifiManager.SoftApCallback {
private static final String TAG = "HotspotController";
private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
- private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>();
- private final Receiver mReceiver = new Receiver();
+ private final ArrayList<Callback> mCallbacks = new ArrayList<>();
+ private final WifiStateReceiver mWifiStateReceiver = new WifiStateReceiver();
private final ConnectivityManager mConnectivityManager;
+ private final WifiManager mWifiManager;
private final Context mContext;
private int mHotspotState;
+ private int mNumConnectedDevices;
private boolean mWaitingForCallback;
public HotspotControllerImpl(Context context) {
mContext = context;
- mConnectivityManager = (ConnectivityManager) context.getSystemService(
- Context.CONNECTIVITY_SERVICE);
+ mConnectivityManager =
+ (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+ mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
}
@Override
@@ -84,7 +88,8 @@ public class HotspotControllerImpl implements HotspotController {
if (callback == null || mCallbacks.contains(callback)) return;
if (DEBUG) Log.d(TAG, "addCallback " + callback);
mCallbacks.add(callback);
- mReceiver.setListening(!mCallbacks.isEmpty());
+
+ updateWifiStateListeners(!mCallbacks.isEmpty());
}
}
@@ -94,7 +99,26 @@ public class HotspotControllerImpl implements HotspotController {
if (DEBUG) Log.d(TAG, "removeCallback " + callback);
synchronized (mCallbacks) {
mCallbacks.remove(callback);
- mReceiver.setListening(!mCallbacks.isEmpty());
+
+ updateWifiStateListeners(!mCallbacks.isEmpty());
+ }
+ }
+
+ /**
+ * Updates the wifi state receiver to either start or stop listening to get updates to the
+ * hotspot status. Additionally starts listening to wifi manager state to track the number of
+ * connected devices.
+ *
+ * @param shouldListen whether we should start listening to various wifi statuses
+ */
+ private void updateWifiStateListeners(boolean shouldListen) {
+ mWifiStateReceiver.setListening(shouldListen);
+ if (shouldListen) {
+ mWifiManager.registerSoftApCallback(
+ this,
+ Dependency.get(Dependency.MAIN_HANDLER));
+ } else {
+ mWifiManager.unregisterSoftApCallback(this);
}
}
@@ -116,20 +140,55 @@ public class HotspotControllerImpl implements HotspotController {
if (DEBUG) Log.d(TAG, "Starting tethering");
mConnectivityManager.startTethering(
ConnectivityManager.TETHERING_WIFI, false, callback);
- fireCallback(isHotspotEnabled());
+ fireHotspotChangedCallback(isHotspotEnabled());
} else {
mConnectivityManager.stopTethering(ConnectivityManager.TETHERING_WIFI);
}
}
- private void fireCallback(boolean isEnabled) {
+ @Override
+ public int getNumConnectedDevices() {
+ return mNumConnectedDevices;
+ }
+
+ /**
+ * Sends a hotspot changed callback with the new enabled status. Wraps
+ * {@link #fireHotspotChangedCallback(boolean, int)} and assumes that the number of devices has
+ * not changed.
+ *
+ * @param enabled whether the hotspot is enabled
+ */
+ private void fireHotspotChangedCallback(boolean enabled) {
+ fireHotspotChangedCallback(enabled, mNumConnectedDevices);
+ }
+
+ /**
+ * Sends a hotspot changed callback with the new enabled status & the number of devices
+ * connected to the hotspot. Be careful when calling over multiple threads, especially if one of
+ * them is the main thread (as it can be blocked).
+ *
+ * @param enabled whether the hotspot is enabled
+ * @param numConnectedDevices number of devices connected to the hotspot
+ */
+ private void fireHotspotChangedCallback(boolean enabled, int numConnectedDevices) {
synchronized (mCallbacks) {
for (Callback callback : mCallbacks) {
- callback.onHotspotChanged(isEnabled);
+ callback.onHotspotChanged(enabled, numConnectedDevices);
}
}
}
+ @Override
+ public void onStateChanged(int state, int failureReason) {
+ // Do nothing - we don't care about changing anything here.
+ }
+
+ @Override
+ public void onNumClientsChanged(int numConnectedDevices) {
+ mNumConnectedDevices = numConnectedDevices;
+ fireHotspotChangedCallback(isHotspotEnabled(), numConnectedDevices);
+ }
+
private final class OnStartTetheringCallback extends
ConnectivityManager.OnStartTetheringCallback {
@Override
@@ -143,12 +202,15 @@ public class HotspotControllerImpl implements HotspotController {
public void onTetheringFailed() {
if (DEBUG) Log.d(TAG, "onTetheringFailed");
mWaitingForCallback = false;
- fireCallback(isHotspotEnabled());
+ fireHotspotChangedCallback(isHotspotEnabled());
// TODO: Show error.
}
}
- private final class Receiver extends BroadcastReceiver {
+ /**
+ * Class to listen in on wifi state and update the hotspot state
+ */
+ private final class WifiStateReceiver extends BroadcastReceiver {
private boolean mRegistered;
public void setListening(boolean listening) {
@@ -170,8 +232,17 @@ public class HotspotControllerImpl implements HotspotController {
int state = intent.getIntExtra(
WifiManager.EXTRA_WIFI_AP_STATE, WifiManager.WIFI_AP_STATE_FAILED);
if (DEBUG) Log.d(TAG, "onReceive " + state);
+
+ // Update internal hotspot state for tracking before using any enabled/callback methods.
mHotspotState = state;
- fireCallback(mHotspotState == WifiManager.WIFI_AP_STATE_ENABLED);
+
+ if (!isHotspotEnabled()) {
+ // Reset num devices if the hotspot is no longer enabled so we don't get ghost
+ // counters.
+ mNumConnectedDevices = 0;
+ }
+
+ fireHotspotChangedCallback(isHotspotEnabled());
}
}
}
diff --git a/com/android/systemui/statusbar/policy/KeyButtonDrawable.java b/com/android/systemui/statusbar/policy/KeyButtonDrawable.java
index 21a96e2e..cce9d1cd 100644
--- a/com/android/systemui/statusbar/policy/KeyButtonDrawable.java
+++ b/com/android/systemui/statusbar/policy/KeyButtonDrawable.java
@@ -39,7 +39,7 @@ public class KeyButtonDrawable extends LayerDrawable {
}
}
- private KeyButtonDrawable(Drawable[] drawables) {
+ protected KeyButtonDrawable(Drawable[] drawables) {
super(drawables);
for (int i = 0; i < drawables.length; i++) {
setLayerGravity(i, Gravity.CENTER);
diff --git a/com/android/systemui/statusbar/policy/KeyButtonRipple.java b/com/android/systemui/statusbar/policy/KeyButtonRipple.java
index cc7943b8..a2bec982 100644
--- a/com/android/systemui/statusbar/policy/KeyButtonRipple.java
+++ b/com/android/systemui/statusbar/policy/KeyButtonRipple.java
@@ -26,9 +26,11 @@ import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.drawable.Drawable;
+import android.os.Handler;
import android.view.DisplayListCanvas;
import android.view.RenderNodeAnimator;
import android.view.View;
+import android.view.ViewConfiguration;
import android.view.animation.Interpolator;
import com.android.systemui.Interpolators;
@@ -56,14 +58,17 @@ public class KeyButtonRipple extends Drawable {
private float mGlowAlpha = 0f;
private float mGlowScale = 1f;
private boolean mPressed;
+ private boolean mVisible;
private boolean mDrawingHardwareGlow;
private int mMaxWidth;
private boolean mLastDark;
private boolean mDark;
+ private boolean mDelayTouchFeedback;
private final Interpolator mInterpolator = new LogInterpolator();
private boolean mSupportHardware;
private final View mTargetView;
+ private final Handler mHandler = new Handler();
private final HashSet<Animator> mRunningAnimations = new HashSet<>();
private final ArrayList<Animator> mTmpArray = new ArrayList<>();
@@ -77,6 +82,10 @@ public class KeyButtonRipple extends Drawable {
mDark = darkIntensity >= 0.5f;
}
+ public void setDelayTouchFeedback(boolean delay) {
+ mDelayTouchFeedback = delay;
+ }
+
private Paint getRipplePaint() {
if (mRipplePaint == null) {
mRipplePaint = new Paint();
@@ -211,7 +220,16 @@ public class KeyButtonRipple extends Drawable {
}
}
+ /**
+ * Abort the ripple while it is delayed and before shown used only when setShouldDelayStartTouch
+ * is enabled.
+ */
+ public void abortDelayedRipple() {
+ mHandler.removeCallbacksAndMessages(null);
+ }
+
private void cancelAnimations() {
+ mVisible = false;
mTmpArray.addAll(mRunningAnimations);
int size = mTmpArray.size();
for (int i = 0; i < size; i++) {
@@ -220,11 +238,21 @@ public class KeyButtonRipple extends Drawable {
}
mTmpArray.clear();
mRunningAnimations.clear();
+ mHandler.removeCallbacksAndMessages(null);
}
private void setPressedSoftware(boolean pressed) {
if (pressed) {
- enterSoftware();
+ if (mDelayTouchFeedback) {
+ if (mRunningAnimations.isEmpty()) {
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler.postDelayed(this::enterSoftware, ViewConfiguration.getTapTimeout());
+ } else if (mVisible) {
+ enterSoftware();
+ }
+ } else {
+ enterSoftware();
+ }
} else {
exitSoftware();
}
@@ -232,6 +260,7 @@ public class KeyButtonRipple extends Drawable {
private void enterSoftware() {
cancelAnimations();
+ mVisible = true;
mGlowAlpha = getMaxGlowAlpha();
ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale",
0f, GLOW_MAX_SCALE_FACTOR);
@@ -240,6 +269,12 @@ public class KeyButtonRipple extends Drawable {
scaleAnimator.addListener(mAnimatorListener);
scaleAnimator.start();
mRunningAnimations.add(scaleAnimator);
+
+ // With the delay, it could eventually animate the enter animation with no pressed state,
+ // then immediately show the exit animation. If this is skipped there will be no ripple.
+ if (mDelayTouchFeedback && !mPressed) {
+ exitSoftware();
+ }
}
private void exitSoftware() {
@@ -253,7 +288,16 @@ public class KeyButtonRipple extends Drawable {
private void setPressedHardware(boolean pressed) {
if (pressed) {
- enterHardware();
+ if (mDelayTouchFeedback) {
+ if (mRunningAnimations.isEmpty()) {
+ mHandler.removeCallbacksAndMessages(null);
+ mHandler.postDelayed(this::enterHardware, ViewConfiguration.getTapTimeout());
+ } else if (mVisible) {
+ enterHardware();
+ }
+ } else {
+ enterHardware();
+ }
} else {
exitHardware();
}
@@ -302,6 +346,7 @@ public class KeyButtonRipple extends Drawable {
private void enterHardware() {
cancelAnimations();
+ mVisible = true;
mDrawingHardwareGlow = true;
setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2));
final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(),
@@ -343,6 +388,12 @@ public class KeyButtonRipple extends Drawable {
mRunningAnimations.add(endAnim);
invalidateSelf();
+
+ // With the delay, it could eventually animate the enter animation with no pressed state,
+ // then immediately show the exit animation. If this is skipped there will be no ripple.
+ if (mDelayTouchFeedback && !mPressed) {
+ exitHardware();
+ }
}
private void exitHardware() {
@@ -366,6 +417,7 @@ public class KeyButtonRipple extends Drawable {
public void onAnimationEnd(Animator animation) {
mRunningAnimations.remove(animation);
if (mRunningAnimations.isEmpty() && !mPressed) {
+ mVisible = false;
mDrawingHardwareGlow = false;
invalidateSelf();
}
diff --git a/com/android/systemui/statusbar/policy/KeyButtonView.java b/com/android/systemui/statusbar/policy/KeyButtonView.java
index 05017718..077c6c38 100644
--- a/com/android/systemui/statusbar/policy/KeyButtonView.java
+++ b/com/android/systemui/statusbar/policy/KeyButtonView.java
@@ -284,6 +284,7 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
@Override
public void abortCurrentGesture() {
setPressed(false);
+ mRipple.abortDelayedRipple();
mGestureAborted = true;
}
@@ -301,6 +302,11 @@ public class KeyButtonView extends ImageView implements ButtonInterface {
}
@Override
+ public void setDelayTouchFeedback(boolean shouldDelay) {
+ mRipple.setDelayTouchFeedback(shouldDelay);
+ }
+
+ @Override
public void setVertical(boolean vertical) {
//no op
}
diff --git a/com/android/systemui/statusbar/policy/LocationControllerImpl.java b/com/android/systemui/statusbar/policy/LocationControllerImpl.java
index 4ee4ef49..0b666a61 100644
--- a/com/android/systemui/statusbar/policy/LocationControllerImpl.java
+++ b/com/android/systemui/statusbar/policy/LocationControllerImpl.java
@@ -16,11 +16,12 @@
package com.android.systemui.statusbar.policy;
+import static com.android.settingslib.Utils.updateLocationEnabled;
+
import android.app.ActivityManager;
import android.app.AppOpsManager;
import android.app.StatusBarManager;
import android.content.BroadcastReceiver;
-import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
@@ -28,19 +29,14 @@ import android.location.LocationManager;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
+import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
-import android.provider.Settings;
import android.support.annotation.VisibleForTesting;
-
-import com.android.systemui.R;
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.
*/
@@ -101,32 +97,27 @@ public class LocationControllerImpl extends BroadcastReceiver implements Locatio
* @return true if attempt to change setting was successful.
*/
public boolean setLocationEnabled(boolean enabled) {
+ // QuickSettings always runs as the owner, so specifically set the settings
+ // for the current foreground user.
int currentUserId = ActivityManager.getCurrentUser();
if (isUserLocationRestricted(currentUserId)) {
return false;
}
- 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 updateLocationMode(mContext, currentMode, mode, currentUserId);
+ updateLocationEnabled(mContext, enabled, currentUserId);
+ return true;
}
/**
- * Returns true if location isn't disabled in settings.
+ * Returns true if location is enabled in settings.
*/
public boolean isLocationEnabled() {
- ContentResolver resolver = mContext.getContentResolver();
// QuickSettings always runs as the owner, so specifically retrieve the settings
// for the current foreground user.
- int mode = Settings.Secure.getIntForUser(resolver, Settings.Secure.LOCATION_MODE,
- Settings.Secure.LOCATION_MODE_OFF, ActivityManager.getCurrentUser());
- return mode != Settings.Secure.LOCATION_MODE_OFF;
+ LocationManager locationManager =
+ (LocationManager) mContext.getSystemService(Context.LOCATION_SERVICE);
+ return locationManager.isLocationEnabledForUser(Process.myUserHandle());
}
@Override
diff --git a/com/android/systemui/statusbar/policy/RemoteInputView.java b/com/android/systemui/statusbar/policy/RemoteInputView.java
index 4fc50442..b63c1da5 100644
--- a/com/android/systemui/statusbar/policy/RemoteInputView.java
+++ b/com/android/systemui/statusbar/policy/RemoteInputView.java
@@ -27,7 +27,9 @@ import android.content.pm.ShortcutManager;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
+import android.os.SystemClock;
import android.text.Editable;
+import android.text.SpannedString;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.util.Log;
@@ -135,11 +137,13 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent,
results);
+ RemoteInput.setResultsSource(fillInIntent, RemoteInput.SOURCE_FREE_FORM_INPUT);
mEditText.setEnabled(false);
mSendButton.setVisibility(INVISIBLE);
mProgressBar.setVisibility(VISIBLE);
mEntry.remoteInputText = mEditText.getText();
+ mEntry.lastRemoteInputSent = SystemClock.elapsedRealtime();
mController.addSpinning(mEntry.key, mToken);
mController.removeRemoteInput(mEntry, mToken);
mEditText.mShowImeOnInputConnection = false;
@@ -298,6 +302,7 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene
private void reset() {
mResetting = true;
+ mEntry.remoteInputTextWhenReset = SpannedString.valueOf(mEditText.getText());
mEditText.getText().clear();
mEditText.setEnabled(true);
diff --git a/com/android/systemui/statusbar/policy/SmartReplyView.java b/com/android/systemui/statusbar/policy/SmartReplyView.java
new file mode 100644
index 00000000..2d829af9
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/SmartReplyView.java
@@ -0,0 +1,64 @@
+package com.android.systemui.statusbar.policy;
+
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.LinearLayout;
+
+import com.android.systemui.R;
+
+/** View which displays smart reply buttons in notifications. */
+public class SmartReplyView extends LinearLayout {
+
+ private static final String TAG = "SmartReplyView";
+
+ public SmartReplyView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public void setRepliesFromRemoteInput(RemoteInput remoteInput, PendingIntent pendingIntent) {
+ removeAllViews();
+ if (remoteInput != null && pendingIntent != null) {
+ CharSequence[] choices = remoteInput.getChoices();
+ if (choices != null) {
+ for (CharSequence choice : choices) {
+ Button replyButton = inflateReplyButton(
+ getContext(), this, choice, remoteInput, pendingIntent);
+ addView(replyButton);
+ }
+ }
+ }
+ }
+
+ public static SmartReplyView inflate(Context context, ViewGroup root) {
+ return (SmartReplyView)
+ LayoutInflater.from(context).inflate(R.layout.smart_reply_view, root, false);
+ }
+
+ private static Button inflateReplyButton(Context context, ViewGroup root, CharSequence choice,
+ RemoteInput remoteInput, PendingIntent pendingIntent) {
+ Button b = (Button) LayoutInflater.from(context).inflate(
+ R.layout.smart_reply_button, root, false);
+ b.setText(choice);
+ b.setOnClickListener(view -> {
+ Bundle results = new Bundle();
+ results.putString(remoteInput.getResultKey(), choice.toString());
+ Intent intent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
+ RemoteInput.addResultsToIntent(new RemoteInput[]{remoteInput}, intent, results);
+ RemoteInput.setResultsSource(intent, RemoteInput.SOURCE_CHOICE);
+ try {
+ pendingIntent.send(context, 0, intent);
+ } catch (PendingIntent.CanceledException e) {
+ Log.w(TAG, "Unable to send smart reply", e);
+ }
+ });
+ return b;
+ }
+}
diff --git a/com/android/systemui/statusbar/policy/TintedKeyButtonDrawable.java b/com/android/systemui/statusbar/policy/TintedKeyButtonDrawable.java
new file mode 100644
index 00000000..acf9c00a
--- /dev/null
+++ b/com/android/systemui/statusbar/policy/TintedKeyButtonDrawable.java
@@ -0,0 +1,57 @@
+/*
+ * 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 com.android.systemui.statusbar.policy;
+
+import android.annotation.ColorInt;
+import android.graphics.drawable.Drawable;
+
+import com.android.internal.graphics.ColorUtils;
+
+/**
+ * Drawable for {@link KeyButtonView}s which contains a single asset and colors for light and dark
+ * navigation bar mode.
+ */
+public class TintedKeyButtonDrawable extends KeyButtonDrawable {
+
+ private final int mLightColor;
+ private final int mDarkColor;
+
+ public static TintedKeyButtonDrawable create(Drawable drawable, @ColorInt int lightColor,
+ @ColorInt int darkColor) {
+ return new TintedKeyButtonDrawable(new Drawable[] { drawable }, lightColor, darkColor);
+ }
+
+ private TintedKeyButtonDrawable(Drawable[] drawables, int lightColor, int darkColor){
+ super(drawables);
+ mLightColor = lightColor;
+ mDarkColor = darkColor;
+ }
+
+ @Override
+ public void setDarkIntensity(float intensity) {
+ // Duplicate intensity scaling from KeyButtonDrawable
+ int intermediateColor = ColorUtils.compositeColors(
+ setAlphaFloat(mDarkColor, intensity),
+ setAlphaFloat(mLightColor,1f - intensity));
+ getDrawable(0).setTint(intermediateColor);
+ invalidateSelf();
+ }
+
+ private int setAlphaFloat(int color, float alpha) {
+ return ColorUtils.setAlphaComponent(color, (int) (alpha * 255f));
+ }
+}
diff --git a/com/android/systemui/statusbar/stack/AmbientState.java b/com/android/systemui/statusbar/stack/AmbientState.java
index 4d8da441..ebf4cda4 100644
--- a/com/android/systemui/statusbar/stack/AmbientState.java
+++ b/com/android/systemui/statusbar/stack/AmbientState.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.stack;
+import android.annotation.Nullable;
import android.content.Context;
import android.view.View;
@@ -236,6 +237,7 @@ public class AmbientState {
mShelf = shelf;
}
+ @Nullable
public NotificationShelf getShelf() {
return mShelf;
}
diff --git a/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java b/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
index c0241e36..4ca33cd3 100644
--- a/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
+++ b/com/android/systemui/statusbar/stack/NotificationChildrenContainer.java
@@ -128,12 +128,11 @@ public class NotificationChildrenContainer extends ViewGroup {
mDividerHeight = res.getDimensionPixelSize(
R.dimen.notification_children_container_divider_height);
mDividerAlpha = res.getFloat(R.dimen.notification_divider_alpha);
- mHeaderHeight = res.getDimensionPixelSize(
- R.dimen.notification_children_container_header_height);
mNotificationHeaderMargin = res.getDimensionPixelSize(
R.dimen.notification_children_container_margin_top);
mNotificatonTopPadding = res.getDimensionPixelSize(
R.dimen.notification_children_container_top_padding);
+ mHeaderHeight = mNotificationHeaderMargin + mNotificatonTopPadding;
mCollapsedBottompadding = res.getDimensionPixelSize(
com.android.internal.R.dimen.notification_content_margin_bottom);
mEnableShadowOnChildNotifications =
@@ -395,7 +394,7 @@ public class NotificationChildrenContainer extends ViewGroup {
}
} else if (mOverflowNumber != null) {
removeView(mOverflowNumber);
- if (isShown()) {
+ if (isShown() && isAttachedToWindow()) {
final View removedOverflowNumber = mOverflowNumber;
addTransientView(removedOverflowNumber, getTransientViewCount());
CrossFadeHelper.fadeOut(removedOverflowNumber, new Runnable() {
diff --git a/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java b/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
index 369e7ffa..af3d64be 100644
--- a/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
+++ b/com/android/systemui/statusbar/stack/NotificationStackScrollLayout.java
@@ -29,6 +29,7 @@ import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Canvas;
+import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
@@ -43,6 +44,7 @@ import android.support.annotation.VisibleForTesting;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.util.Log;
+import android.util.MathUtils;
import android.util.Pair;
import android.util.Property;
import android.view.ContextThemeWrapper;
@@ -76,7 +78,6 @@ import com.android.systemui.statusbar.ActivatableNotificationView;
import com.android.systemui.statusbar.DismissView;
import com.android.systemui.statusbar.EmptyShadeView;
import com.android.systemui.statusbar.ExpandableNotificationRow;
-import com.android.systemui.statusbar.ExpandableOutlineView;
import com.android.systemui.statusbar.ExpandableView;
import com.android.systemui.statusbar.NotificationData;
import com.android.systemui.statusbar.NotificationGuts;
@@ -86,10 +87,8 @@ import com.android.systemui.statusbar.NotificationShelf;
import com.android.systemui.statusbar.NotificationSnooze;
import com.android.systemui.statusbar.StackScrollerDecorView;
import com.android.systemui.statusbar.StatusBarState;
-import com.android.systemui.statusbar.notification.AnimatableProperty;
import com.android.systemui.statusbar.notification.FakeShadowView;
import com.android.systemui.statusbar.notification.NotificationUtils;
-import com.android.systemui.statusbar.notification.PropertyAnimator;
import com.android.systemui.statusbar.notification.VisibilityLocationProvider;
import com.android.systemui.statusbar.phone.NotificationGroupManager;
import com.android.systemui.statusbar.phone.StatusBar;
@@ -127,15 +126,6 @@ public class NotificationStackScrollLayout extends ViewGroup
* Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
*/
private static final int INVALID_POINTER = -1;
- private static final AnimatableProperty SIDE_PADDINGS = AnimatableProperty.from(
- "sidePaddings",
- NotificationStackScrollLayout::setCurrentSidePadding,
- NotificationStackScrollLayout::getCurrentSidePadding,
- R.id.side_padding_animator_tag,
- R.id.side_padding_animator_end_tag,
- R.id.side_padding_animator_start_tag);
- private static final AnimationProperties SIDE_PADDING_PROPERTIES =
- new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
private ExpandHelper mExpandHelper;
private NotificationSwipeHelper mSwipeHelper;
@@ -143,7 +133,6 @@ public class NotificationStackScrollLayout extends ViewGroup
private int mCurrentStackHeight = Integer.MAX_VALUE;
private final Paint mBackgroundPaint = new Paint();
private final Path mBackgroundPath = new Path();
- private final float[] mBackgroundRadii = new float[8];
private final boolean mShouldDrawNotificationBackground;
private float mExpandedHeight;
@@ -171,10 +160,14 @@ public class NotificationStackScrollLayout extends ViewGroup
private int mCollapsedSize;
private int mPaddingBetweenElements;
private int mIncreasedPaddingBetweenElements;
+ private int mRegularTopPadding;
+ private int mDarkTopPadding;
+ // Current padding, will be either mRegularTopPadding or mDarkTopPadding
private int mTopPadding;
+ // Distance between AOD separator and shelf
+ private int mDarkSeparatorPadding;
private int mBottomMargin;
private int mBottomInset = 0;
- private float mCurrentSidePadding;
/**
* The algorithm which calculates the properties for our children
@@ -370,17 +363,17 @@ public class NotificationStackScrollLayout extends ViewGroup
private boolean mGroupExpandedForMeasure;
private boolean mScrollable;
private View mForcedScroll;
- private float mBackgroundFadeAmount = 1.0f;
- private static final Property<NotificationStackScrollLayout, Float> BACKGROUND_FADE =
- new FloatProperty<NotificationStackScrollLayout>("backgroundFade") {
+ private float mDarkAmount = 1.0f;
+ private static final Property<NotificationStackScrollLayout, Float> DARK_AMOUNT =
+ new FloatProperty<NotificationStackScrollLayout>("darkAmount") {
@Override
public void setValue(NotificationStackScrollLayout object, float value) {
- object.setBackgroundFadeAmount(value);
+ object.setDarkAmount(value);
}
@Override
public Float get(NotificationStackScrollLayout object) {
- return object.getBackgroundFadeAmount();
+ return object.getDarkAmount();
}
};
private boolean mUsingLightTheme;
@@ -402,8 +395,11 @@ public class NotificationStackScrollLayout extends ViewGroup
private boolean mHeadsUpGoingAwayAnimationsAllowed = true;
private Runnable mAnimateScroll = this::animateScroll;
private int mCornerRadius;
- private int mLockscreenSidePaddings;
private int mSidePaddings;
+ private final int mSeparatorWidth;
+ private final int mSeparatorThickness;
+ private final Rect mTmpRect = new Rect();
+ private int mClockBottom;
public NotificationStackScrollLayout(Context context) {
this(context, null);
@@ -438,10 +434,12 @@ public class NotificationStackScrollLayout extends ViewGroup
res.getBoolean(R.bool.config_drawNotificationBackground);
mFadeNotificationsOnDismiss =
res.getBoolean(R.bool.config_fadeNotificationsOnDismiss);
+ mSeparatorWidth = res.getDimensionPixelSize(R.dimen.widget_separator_width);
+ mSeparatorThickness = res.getDimensionPixelSize(R.dimen.widget_separator_thickness);
+ mDarkSeparatorPadding = res.getDimensionPixelSize(R.dimen.widget_bottom_separator_padding);
updateWillNotDraw();
mBackgroundPaint.setAntiAlias(true);
- mBackgroundPaint.setStyle(Paint.Style.FILL);
if (DEBUG) {
mDebugPaint = new Paint();
mDebugPaint.setColor(0xffff0000);
@@ -488,9 +486,9 @@ public class NotificationStackScrollLayout extends ViewGroup
}
protected void onDraw(Canvas canvas) {
- if (mShouldDrawNotificationBackground && !mAmbientState.isDark()
- && mCurrentBounds.top < mCurrentBounds.bottom) {
- canvas.drawPath(mBackgroundPath, mBackgroundPaint);
+ if (mShouldDrawNotificationBackground
+ && (mCurrentBounds.top < mCurrentBounds.bottom || mAmbientState.isDark())) {
+ drawBackground(canvas);
}
if (DEBUG) {
@@ -503,17 +501,57 @@ public class NotificationStackScrollLayout extends ViewGroup
}
}
+ private void drawBackground(Canvas canvas) {
+ final int lockScreenLeft = mSidePaddings;
+ final int lockScreenRight = getWidth() - mSidePaddings;
+ final int lockScreenTop = mCurrentBounds.top;
+ final int lockScreenBottom = mCurrentBounds.bottom;
+ final int darkLeft = getWidth() / 2 - mSeparatorWidth / 2;
+ final int darkRight = darkLeft + mSeparatorWidth;
+ final int darkTop = (int) (mRegularTopPadding + mSeparatorThickness / 2f);
+ final int darkBottom = darkTop + mSeparatorThickness;
+
+ if (mAmbientState.hasPulsingNotifications()) {
+ // TODO draw divider between notification and shelf
+ } else if (mAmbientState.isDark()) {
+ // Only draw divider on AOD if we actually have notifications
+ if (mFirstVisibleBackgroundChild != null) {
+ canvas.drawRect(darkLeft, darkTop, darkRight, darkBottom, mBackgroundPaint);
+ }
+ setClipBounds(null);
+ } else {
+ float animProgress = Interpolators.FAST_OUT_SLOW_IN
+ .getInterpolation(mDarkAmount);
+ float sidePaddingsProgress = Interpolators.FAST_OUT_SLOW_IN
+ .getInterpolation(mDarkAmount * 2);
+ mTmpRect.set((int) MathUtils.lerp(darkLeft, lockScreenLeft, sidePaddingsProgress),
+ (int) MathUtils.lerp(darkTop, lockScreenTop, animProgress),
+ (int) MathUtils.lerp(darkRight, lockScreenRight, sidePaddingsProgress),
+ (int) MathUtils.lerp(darkBottom, lockScreenBottom, animProgress));
+ canvas.drawRoundRect(mTmpRect.left, mTmpRect.top, mTmpRect.right, mTmpRect.bottom,
+ mCornerRadius, mCornerRadius, mBackgroundPaint);
+ setClipBounds(animProgress == 1 ? null : mTmpRect);
+ }
+ }
+
private void updateBackgroundDimming() {
// No need to update the background color if it's not being drawn.
if (!mShouldDrawNotificationBackground) {
return;
}
- float alpha = BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
- alpha *= mBackgroundFadeAmount;
- // We need to manually blend in the background color
- int scrimColor = mScrimController.getBackgroundColor();
- int color = ColorUtils.blendARGB(scrimColor, mBgColor, alpha);
+ final int color;
+ if (mAmbientState.isDark()) {
+ color = Color.WHITE;
+ } else {
+ float alpha =
+ BACKGROUND_ALPHA_DIMMED + (1 - BACKGROUND_ALPHA_DIMMED) * (1.0f - mDimAmount);
+ alpha *= mDarkAmount;
+ // We need to manually blend in the background color
+ int scrimColor = mScrimController.getBackgroundColor();
+ color = ColorUtils.blendARGB(scrimColor, mBgColor, alpha);
+ }
+
if (mCachedBackgroundColor != color) {
mCachedBackgroundColor = color;
mBackgroundPaint.setColor(color);
@@ -543,8 +581,7 @@ public class NotificationStackScrollLayout extends ViewGroup
R.dimen.min_top_overscroll_to_qs);
mStatusBarHeight = res.getDimensionPixelOffset(R.dimen.status_bar_height);
mBottomMargin = res.getDimensionPixelSize(R.dimen.notification_panel_margin_bottom);
- mLockscreenSidePaddings = res.getDimensionPixelSize(
- R.dimen.notification_lockscreen_side_paddings);
+ mSidePaddings = res.getDimensionPixelSize(R.dimen.notification_side_paddings);
mMinInteractionHeight = res.getDimensionPixelSize(
R.dimen.notification_min_interaction_height);
mCornerRadius = res.getDimensionPixelSize(
@@ -575,11 +612,15 @@ public class NotificationStackScrollLayout extends ViewGroup
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ int width = MeasureSpec.getSize(widthMeasureSpec);
+ int childWidthSpec = MeasureSpec.makeMeasureSpec(width - mSidePaddings * 2,
+ MeasureSpec.getMode(widthMeasureSpec));
// We need to measure all children even the GONE ones, such that the heights are calculated
// correctly as they are used to calculate how many we can fit on the screen.
final int size = getChildCount();
for (int i = 0; i < size; i++) {
- measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
+ measureChild(getChildAt(i), childWidthSpec, heightMeasureSpec);
}
}
@@ -646,6 +687,11 @@ public class NotificationStackScrollLayout extends ViewGroup
}
private void updateAlgorithmHeightAndPadding() {
+ if (mPulsing != null) {
+ mTopPadding = mClockBottom;
+ } else {
+ mTopPadding = mAmbientState.isDark() ? mDarkTopPadding : mRegularTopPadding;
+ }
mAmbientState.setLayoutHeight(getLayoutHeight());
updateAlgorithmLayoutMinHeight();
mAmbientState.setTopPadding(mTopPadding);
@@ -675,11 +721,32 @@ public class NotificationStackScrollLayout extends ViewGroup
private void onPreDrawDuringAnimation() {
mShelf.updateAppearance();
+ updateClippingToTopRoundedCorner();
if (!mNeedsAnimation && !mChildrenUpdateRequested) {
updateBackground();
}
}
+ private void updateClippingToTopRoundedCorner() {
+ Float clipStart = (float) mTopPadding;
+ Float clipEnd = clipStart + mCornerRadius;
+ boolean first = true;
+ for (int i = 0; i < getChildCount(); i++) {
+ ExpandableView child = (ExpandableView) getChildAt(i);
+ if (child.getVisibility() == GONE) {
+ continue;
+ }
+ float start = child.getTranslationY();
+ float end = start + Math.max(child.getActualHeight() - child.getClipBottomAmount(),
+ 0);
+ boolean clip = clipStart > start && clipStart < end
+ || clipEnd >= start && clipEnd <= end;
+ clip &= !(first && mOwnScrollY == 0);
+ child.setDistanceToTopRoundness(clip ? Math.max(start - clipStart, 0) : -1);
+ first = false;
+ }
+ }
+
private void updateScrollStateForAddedChildren() {
if (mChildrenToAddAnimated.isEmpty()) {
return;
@@ -747,8 +814,9 @@ public class NotificationStackScrollLayout extends ViewGroup
}
private void setTopPadding(int topPadding, boolean animate) {
- if (mTopPadding != topPadding) {
- mTopPadding = topPadding;
+ if (mRegularTopPadding != topPadding) {
+ mRegularTopPadding = topPadding;
+ mDarkTopPadding = topPadding + mDarkSeparatorPadding;
updateAlgorithmHeightAndPadding();
updateContentHeight();
if (animate && mAnimationsEnabled && mIsExpanded) {
@@ -2253,33 +2321,11 @@ public class NotificationStackScrollLayout extends ViewGroup
}
mScrimController.setExcludedBackgroundArea(
- mFadingOut || mParentNotFullyVisible || mAmbientState.isDark() || mIsClipped ? null
+ mFadingOut || mParentNotFullyVisible || mDarkAmount != 1 || mIsClipped ? null
: mCurrentBounds);
- updateBackgroundPath();
invalidate();
}
- private void updateBackgroundPath() {
- mBackgroundPath.reset();
- float topRoundness = 0;
- if (mFirstVisibleBackgroundChild != null) {
- topRoundness = mFirstVisibleBackgroundChild.getCurrentBackgroundRadiusTop();
- }
- topRoundness = onKeyguard() ? mCornerRadius : topRoundness;
- float bottomRoundNess = mCornerRadius;
- mBackgroundRadii[0] = topRoundness;
- mBackgroundRadii[1] = topRoundness;
- mBackgroundRadii[2] = topRoundness;
- mBackgroundRadii[3] = topRoundness;
- mBackgroundRadii[4] = bottomRoundNess;
- mBackgroundRadii[5] = bottomRoundNess;
- mBackgroundRadii[6] = bottomRoundNess;
- mBackgroundRadii[7] = bottomRoundNess;
- mBackgroundPath.addRoundRect(mCurrentSidePadding, mCurrentBounds.top,
- getWidth() - mCurrentSidePadding, mCurrentBounds.bottom, mBackgroundRadii,
- Path.Direction.CCW);
- }
-
/**
* Update the background bounds to the new desired bounds
*/
@@ -2292,8 +2338,8 @@ public class NotificationStackScrollLayout extends ViewGroup
mBackgroundBounds.left = mTempInt2[0];
mBackgroundBounds.right = mTempInt2[0] + getWidth();
}
- mBackgroundBounds.left += mCurrentSidePadding;
- mBackgroundBounds.right -= mCurrentSidePadding;
+ mBackgroundBounds.left += mSidePaddings;
+ mBackgroundBounds.right -= mSidePaddings;
if (!mIsExpanded) {
mBackgroundBounds.top = 0;
mBackgroundBounds.bottom = 0;
@@ -2902,8 +2948,7 @@ public class NotificationStackScrollLayout extends ViewGroup
private void applyRoundedNess() {
if (mFirstVisibleBackgroundChild != null) {
- mFirstVisibleBackgroundChild.setTopRoundness(
- mStatusBarState == StatusBarState.KEYGUARD ? 1.0f : 0.0f,
+ mFirstVisibleBackgroundChild.setTopRoundness(1.0f,
mFirstVisibleBackgroundChild.isShown()
&& !mChildrenToAddAnimated.contains(mFirstVisibleBackgroundChild));
}
@@ -2912,7 +2957,6 @@ public class NotificationStackScrollLayout extends ViewGroup
mLastVisibleBackgroundChild.isShown()
&& !mChildrenToAddAnimated.contains(mLastVisibleBackgroundChild));
}
- updateBackgroundPath();
invalidate();
}
@@ -2922,7 +2966,6 @@ public class NotificationStackScrollLayout extends ViewGroup
generateAddAnimation(child, false /* fromMoreCard */);
updateAnimationState(child);
updateChronometerForChild(child);
- updateCurrentSidePaddings(child);
}
private void updateHideSensitiveForChild(View child) {
@@ -3021,6 +3064,7 @@ public class NotificationStackScrollLayout extends ViewGroup
mAnimationEvents.clear();
updateBackground();
updateViewShadows();
+ updateClippingToTopRoundedCorner();
} else {
applyCurrentState();
}
@@ -3714,6 +3758,7 @@ public class NotificationStackScrollLayout extends ViewGroup
setAnimationRunning(false);
updateBackground();
updateViewShadows();
+ updateClippingToTopRoundedCorner();
}
private void updateViewShadows() {
@@ -3818,9 +3863,9 @@ public class NotificationStackScrollLayout extends ViewGroup
mDarkNeedsAnimation = true;
mDarkAnimationOriginIndex = findDarkAnimationOriginIndex(touchWakeUpScreenLocation);
mNeedsAnimation = true;
- setBackgroundFadeAmount(0.0f);
+ setDarkAmount(0.0f);
} else if (!dark) {
- setBackgroundFadeAmount(1.0f);
+ setDarkAmount(1.0f);
}
requestChildrenUpdate();
if (dark) {
@@ -3840,21 +3885,21 @@ public class NotificationStackScrollLayout extends ViewGroup
* {@link #mAmbientState}'s dark mode is toggled.
*/
private void updateWillNotDraw() {
- boolean willDraw = !mAmbientState.isDark() && mShouldDrawNotificationBackground || DEBUG;
+ boolean willDraw = mShouldDrawNotificationBackground || DEBUG;
setWillNotDraw(!willDraw);
}
- private void setBackgroundFadeAmount(float fadeAmount) {
- mBackgroundFadeAmount = fadeAmount;
+ private void setDarkAmount(float darkAmount) {
+ mDarkAmount = darkAmount;
updateBackgroundDimming();
}
- public float getBackgroundFadeAmount() {
- return mBackgroundFadeAmount;
+ public float getDarkAmount() {
+ return mDarkAmount;
}
private void startBackgroundFadeIn() {
- ObjectAnimator fadeAnimator = ObjectAnimator.ofFloat(this, BACKGROUND_FADE, 0f, 1f);
+ ObjectAnimator fadeAnimator = ObjectAnimator.ofFloat(this, DARK_AMOUNT, 0f, 1f);
fadeAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_WAKEUP);
fadeAnimator.setInterpolator(Interpolators.ALPHA_IN);
fadeAnimator.start();
@@ -4284,13 +4329,15 @@ public class NotificationStackScrollLayout extends ViewGroup
return mIsExpanded;
}
- public void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing) {
+ public void setPulsing(Collection<HeadsUpManager.HeadsUpEntry> pulsing, int clockBottom) {
if (mPulsing == null && pulsing == null) {
return;
}
mPulsing = pulsing;
+ mClockBottom = clockBottom;
mAmbientState.setPulsing(pulsing);
updateNotificationAnimationStates();
+ updateAlgorithmHeightAndPadding();
updateContentHeight();
notifyHeightChangeListener(mShelf);
requestChildrenUpdate();
@@ -4382,43 +4429,6 @@ public class NotificationStackScrollLayout extends ViewGroup
public void setStatusBarState(int statusBarState) {
mStatusBarState = statusBarState;
mAmbientState.setStatusBarState(statusBarState);
- applyRoundedNess();
- updateSidePaddings();
- }
-
- private void updateSidePaddings() {
- int sidePaddings = mStatusBarState == StatusBarState.KEYGUARD ? mLockscreenSidePaddings : 0;
- if (sidePaddings != mSidePaddings) {
- boolean animate = isShown();
- mSidePaddings = sidePaddings;
- PropertyAnimator.setProperty(this, SIDE_PADDINGS, sidePaddings,
- SIDE_PADDING_PROPERTIES, animate);
- }
- }
-
- protected void setCurrentSidePadding(float sidePadding) {
- mCurrentSidePadding = sidePadding;
- updateBackground();
- applySidePaddingsToChildren();
- }
-
- private void applySidePaddingsToChildren() {
- for (int i = 0; i < getChildCount(); i++) {
- View view = getChildAt(i);
- updateCurrentSidePaddings(view);
- }
- }
-
- private void updateCurrentSidePaddings(View view) {
- if (!(view instanceof ExpandableOutlineView)) {
- return;
- }
- ExpandableOutlineView outlineView = (ExpandableOutlineView) view;
- outlineView.setCurrentSidePaddings(mCurrentSidePadding);
- }
-
- protected float getCurrentSidePadding() {
- return mCurrentSidePadding;
}
public void setExpandingVelocity(float expandingVelocity) {
diff --git a/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java b/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
index 7374f115..2ce6df27 100644
--- a/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
+++ b/com/android/systemui/statusbar/stack/StackScrollAlgorithm.java
@@ -122,7 +122,9 @@ public class StackScrollAlgorithm {
}
private void updateShelfState(StackScrollState resultState, AmbientState ambientState) {
NotificationShelf shelf = ambientState.getShelf();
- shelf.updateState(resultState, ambientState);
+ if (shelf != null) {
+ shelf.updateState(resultState, ambientState);
+ }
}
private void updateClipping(StackScrollState resultState,
@@ -495,6 +497,10 @@ public class StackScrollAlgorithm {
*/
private void clampPositionToShelf(ExpandableViewState childViewState,
AmbientState ambientState) {
+ if (ambientState.getShelf() == null) {
+ return;
+ }
+
int shelfStart = ambientState.getInnerHeight()
- ambientState.getShelf().getIntrinsicHeight();
childViewState.yTranslation = Math.min(childViewState.yTranslation, shelfStart);
@@ -556,7 +562,8 @@ public class StackScrollAlgorithm {
} else if (i == 0 && ambientState.isAboveShelf(child)) {
// In case this is a new view that has never been measured before, we don't want to
// elevate if we are currently expanded more then the notification
- int shelfHeight = ambientState.getShelf().getIntrinsicHeight();
+ int shelfHeight = ambientState.getShelf() == null ? 0 :
+ ambientState.getShelf().getIntrinsicHeight();
float shelfStart = ambientState.getInnerHeight()
- shelfHeight + ambientState.getTopPadding()
+ ambientState.getStackTranslation();
diff --git a/com/android/systemui/tuner/TunerServiceImpl.java b/com/android/systemui/tuner/TunerServiceImpl.java
index 8e584bc3..5a4478f0 100644
--- a/com/android/systemui/tuner/TunerServiceImpl.java
+++ b/com/android/systemui/tuner/TunerServiceImpl.java
@@ -58,7 +58,7 @@ public class TunerServiceImpl extends TunerService {
private static final String TUNER_VERSION = "sysui_tuner_version";
- private static final int CURRENT_TUNER_VERSION = 1;
+ private static final int CURRENT_TUNER_VERSION = 2;
private final Observer mObserver = new Observer();
// Map of Uris we listen on to their settings keys.
@@ -116,6 +116,9 @@ public class TunerServiceImpl extends TunerService {
TextUtils.join(",", iconBlacklist), mCurrentUser);
}
}
+ if (oldVersion < 2) {
+ setTunerEnabled(mContext, false);
+ }
setValue(TUNER_VERSION, newVersion);
}
diff --git a/com/android/systemui/usb/UsbConfirmActivity.java b/com/android/systemui/usb/UsbConfirmActivity.java
index e117969c..0a3e34ee 100644
--- a/com/android/systemui/usb/UsbConfirmActivity.java
+++ b/com/android/systemui/usb/UsbConfirmActivity.java
@@ -68,7 +68,6 @@ public class UsbConfirmActivity extends AlertActivity
String appName = mResolveInfo.loadLabel(packageManager).toString();
final AlertController.AlertParams ap = mAlertParams;
- ap.mIcon = mResolveInfo.loadIcon(packageManager);
ap.mTitle = appName;
if (mDevice == null) {
ap.mMessage = getString(R.string.usb_accessory_confirm_prompt, appName,
diff --git a/com/android/systemui/usb/UsbPermissionActivity.java b/com/android/systemui/usb/UsbPermissionActivity.java
index 4606aee3..238407a9 100644
--- a/com/android/systemui/usb/UsbPermissionActivity.java
+++ b/com/android/systemui/usb/UsbPermissionActivity.java
@@ -90,7 +90,6 @@ public class UsbPermissionActivity extends AlertActivity
String appName = aInfo.loadLabel(packageManager).toString();
final AlertController.AlertParams ap = mAlertParams;
- ap.mIcon = aInfo.loadIcon(packageManager);
ap.mTitle = appName;
if (mDevice == null) {
ap.mMessage = getString(R.string.usb_accessory_permission_prompt, appName,
diff --git a/com/android/systemui/util/NotificationChannels.java b/com/android/systemui/util/NotificationChannels.java
index 87bc0e67..14d5c6f5 100644
--- a/com/android/systemui/util/NotificationChannels.java
+++ b/com/android/systemui/util/NotificationChannels.java
@@ -31,7 +31,8 @@ import java.util.Arrays;
public class NotificationChannels extends SystemUI {
public static String ALERTS = "ALR";
- public static String SCREENSHOTS = "SCN";
+ public static String SCREENSHOTS_LEGACY = "SCN";
+ public static String SCREENSHOTS_HEADSUP = "SCN_HEADSUP";
public static String GENERAL = "GEN";
public static String STORAGE = "DSK";
public static String TVPIP = "TPP";
@@ -56,10 +57,6 @@ public class NotificationChannels extends SystemUI {
context.getString(R.string.notification_channel_alerts),
NotificationManager.IMPORTANCE_HIGH),
new NotificationChannel(
- SCREENSHOTS,
- context.getString(R.string.notification_channel_screenshot),
- NotificationManager.IMPORTANCE_LOW),
- new NotificationChannel(
GENERAL,
context.getString(R.string.notification_channel_general),
NotificationManager.IMPORTANCE_MIN),
@@ -69,9 +66,18 @@ public class NotificationChannels extends SystemUI {
isTv(context)
? NotificationManager.IMPORTANCE_DEFAULT
: NotificationManager.IMPORTANCE_LOW),
+ createScreenshotChannel(
+ context.getString(R.string.notification_channel_screenshot),
+ nm.getNotificationChannel(SCREENSHOTS_LEGACY)),
batteryChannel
));
+ // Delete older SS channel if present.
+ // Screenshots promoted to heads-up in P, this cleans up the lower priority channel from O.
+ // This line can be deleted in Q.
+ nm.deleteNotificationChannel(SCREENSHOTS_LEGACY);
+
+
if (isTv(context)) {
// TV specific notification channel for TV PIP controls.
// Importance should be {@link NotificationManager#IMPORTANCE_MAX} to have the highest
@@ -83,6 +89,40 @@ public class NotificationChannels extends SystemUI {
}
}
+ /**
+ * Set up screenshot channel, respecting any previously committed user settings on legacy
+ * channel.
+ * @return
+ */
+ @VisibleForTesting static NotificationChannel createScreenshotChannel(
+ String name, NotificationChannel legacySS) {
+ NotificationChannel screenshotChannel = new NotificationChannel(SCREENSHOTS_HEADSUP,
+ name, NotificationManager.IMPORTANCE_HIGH); // pop on screen
+
+ screenshotChannel.setSound(Uri.parse(""), // silent
+ new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION).build());
+
+ if (legacySS != null) {
+ // Respect any user modified fields from the old channel.
+ int userlock = legacySS.getUserLockedFields();
+ if ((userlock & NotificationChannel.USER_LOCKED_IMPORTANCE) != 0) {
+ screenshotChannel.setImportance(legacySS.getImportance());
+ }
+ if ((userlock & NotificationChannel.USER_LOCKED_SOUND) != 0) {
+ screenshotChannel.setSound(legacySS.getSound(), legacySS.getAudioAttributes());
+ }
+ if ((userlock & NotificationChannel.USER_LOCKED_VIBRATION) != 0) {
+ screenshotChannel.setVibrationPattern(legacySS.getVibrationPattern());
+ }
+ if ((userlock & NotificationChannel.USER_LOCKED_LIGHTS) != 0) {
+ screenshotChannel.setLightColor(legacySS.getLightColor());
+ }
+ // skip show_badge, irrelevant for system channel
+ }
+
+ return screenshotChannel;
+ }
+
@Override
public void start() {
createAll(mContext);
diff --git a/com/android/systemui/volume/MediaRouterWrapper.java b/com/android/systemui/volume/MediaRouterWrapper.java
new file mode 100644
index 00000000..3423452c
--- /dev/null
+++ b/com/android/systemui/volume/MediaRouterWrapper.java
@@ -0,0 +1,51 @@
+/*
+ * 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 com.android.systemui.volume;
+
+import android.support.v7.media.MediaRouteSelector;
+import android.support.v7.media.MediaRouter;
+
+import java.util.List;
+
+/**
+ * Wrapper for final class MediaRouter, for testing.
+ */
+public class MediaRouterWrapper {
+
+ private final MediaRouter mRouter;
+
+ public MediaRouterWrapper(MediaRouter router)
+ {
+ mRouter = router;
+ }
+
+ public void addCallback(MediaRouteSelector selector, MediaRouter.Callback callback, int flags) {
+ mRouter.addCallback(selector, callback, flags);
+ }
+
+ public void removeCallback(MediaRouter.Callback callback) {
+ mRouter.removeCallback(callback);
+ }
+
+ public void unselect(int reason) {
+ mRouter.unselect(reason);
+ }
+
+ public List<MediaRouter.RouteInfo> getRoutes() {
+ return mRouter.getRoutes();
+ }
+} \ No newline at end of file
diff --git a/com/android/systemui/volume/OutputChooserDialog.java b/com/android/systemui/volume/OutputChooserDialog.java
index f8843a99..e3c85030 100644
--- a/com/android/systemui/volume/OutputChooserDialog.java
+++ b/com/android/systemui/volume/OutputChooserDialog.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2015 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.
@@ -22,6 +22,7 @@ import static android.support.v7.media.MediaRouter.UNSELECT_REASON_DISCONNECTED;
import static com.android.settingslib.bluetooth.Utils.getBtClassDrawableWithDescription;
+import android.app.Dialog;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
@@ -30,6 +31,8 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.net.wifi.WifiManager;
@@ -40,53 +43,63 @@ import android.os.SystemClock;
import android.support.v7.media.MediaControlIntent;
import android.support.v7.media.MediaRouteSelector;
import android.support.v7.media.MediaRouter;
+import android.telecom.TelecomManager;
import android.util.Log;
import android.util.Pair;
+import android.view.Window;
+import android.view.WindowManager;
import com.android.settingslib.Utils;
import com.android.settingslib.bluetooth.CachedBluetoothDevice;
import com.android.systemui.Dependency;
+import com.android.systemui.HardwareUiLayout;
+import com.android.systemui.Interpolators;
import com.android.systemui.R;
-import com.android.systemui.statusbar.phone.SystemUIDialog;
+import com.android.systemui.plugins.VolumeDialogController;
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
+public class OutputChooserDialog extends Dialog
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 static final int MSG_UPDATE_ITEMS = 1;
private final Context mContext;
- private final BluetoothController mController;
- private final WifiManager mWifiManager;
+ private final BluetoothController mBluetoothController;
+ private WifiManager mWifiManager;
private OutputChooserLayout mView;
- private final MediaRouter mRouter;
+ private final MediaRouterWrapper mRouter;
private final MediaRouterCallback mRouterCallback;
private long mLastUpdateTime;
+ private boolean mIsInCall;
+ protected boolean isAttached;
private final MediaRouteSelector mRouteSelector;
private Drawable mDefaultIcon;
private Drawable mTvIcon;
private Drawable mSpeakerIcon;
private Drawable mSpeakerGroupIcon;
+ private HardwareUiLayout mHardwareLayout;
+ private final VolumeDialogController mController;
- public OutputChooserDialog(Context context) {
- super(context);
+ public OutputChooserDialog(Context context, MediaRouterWrapper router) {
+ super(context, com.android.systemui.R.style.qs_theme);
mContext = context;
- mController = Dependency.get(BluetoothController.class);
+ mBluetoothController = Dependency.get(BluetoothController.class);
mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
- mRouter = MediaRouter.getInstance(context);
+ TelecomManager tm = (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE);
+ mIsInCall = tm.isInCall();
+ mRouter = router;
mRouterCallback = new MediaRouterCallback();
mRouteSelector = new MediaRouteSelector.Builder()
.addControlCategory(MediaControlIntent.CATEGORY_REMOTE_PLAYBACK)
@@ -94,6 +107,26 @@ public class OutputChooserDialog extends SystemUIDialog
final IntentFilter filter = new IntentFilter(Intent.ACTION_CLOSE_SYSTEM_DIALOGS);
context.registerReceiver(mReceiver, filter);
+
+ mController = Dependency.get(VolumeDialogController.class);
+
+ // Window initialization
+ Window window = getWindow();
+ window.requestFeature(Window.FEATURE_NO_TITLE);
+ window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+ window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND
+ | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR);
+ window.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);
+ window.setType(WindowManager.LayoutParams.TYPE_VOLUME_OVERLAY);
+ }
+
+ protected void setIsInCall(boolean inCall) {
+ mIsInCall = inCall;
}
@Override
@@ -102,21 +135,31 @@ public class OutputChooserDialog extends SystemUIDialog
setContentView(R.layout.output_chooser);
setCanceledOnTouchOutside(true);
setOnDismissListener(this::onDismiss);
- setTitle(R.string.output_title);
mView = findViewById(R.id.output_chooser);
+ mHardwareLayout = HardwareUiLayout.get(mView);
+ mHardwareLayout.setOutsideTouchListener(view -> dismiss());
+ mHardwareLayout.setSwapOrientation(false);
mView.setCallback(this);
+ if (mIsInCall) {
+ mView.setTitle(R.string.output_calls_title);
+ } else {
+ mView.setTitle(R.string.output_title);
+ }
+
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) {
+ final boolean btOff = !mBluetoothController.isBluetoothEnabled();
+ if (wifiOff && btOff) {
mView.setEmptyState(getDisabledServicesMessage(wifiOff, btOff));
}
+ // time out after 5 seconds
+ mView.postDelayed(() -> updateItems(true), 5000);
}
protected void cleanUp() {}
@@ -131,15 +174,21 @@ public class OutputChooserDialog extends SystemUIDialog
public void onAttachedToWindow() {
super.onAttachedToWindow();
- mRouter.addCallback(mRouteSelector, mRouterCallback,
- MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
- mController.addCallback(mCallback);
+ if (!mIsInCall) {
+ mRouter.addCallback(mRouteSelector, mRouterCallback,
+ MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
+ }
+ mBluetoothController.addCallback(mCallback);
+ mController.addCallback(mControllerCallbackH, mHandler);
+ isAttached = true;
}
@Override
public void onDetachedFromWindow() {
+ isAttached = false;
mRouter.removeCallback(mRouterCallback);
- mController.removeCallback(mCallback);
+ mController.removeCallback(mControllerCallbackH);
+ mBluetoothController.removeCallback(mCallback);
super.onDetachedFromWindow();
}
@@ -150,13 +199,44 @@ public class OutputChooserDialog extends SystemUIDialog
}
@Override
+ public void show() {
+ super.show();
+ mHardwareLayout.setTranslationX(getAnimTranslation());
+ mHardwareLayout.setAlpha(0);
+ mHardwareLayout.animate()
+ .alpha(1)
+ .translationX(0)
+ .setDuration(300)
+ .setInterpolator(Interpolators.FAST_OUT_SLOW_IN)
+ .withEndAction(() -> getWindow().getDecorView().requestAccessibilityFocus())
+ .start();
+ }
+
+ @Override
+ public void dismiss() {
+ mHardwareLayout.setTranslationX(0);
+ mHardwareLayout.setAlpha(1);
+ mHardwareLayout.animate()
+ .alpha(0)
+ .translationX(getAnimTranslation())
+ .setDuration(300)
+ .withEndAction(() -> super.dismiss())
+ .setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator())
+ .start();
+ }
+
+ private float getAnimTranslation() {
+ return getContext().getResources().getDimension(
+ com.android.systemui.R.dimen.output_chooser_panel_width) / 2;
+ }
+
+ @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);
+ if (device.getMaxConnectionState() == BluetoothProfile.STATE_DISCONNECTED) {
+ mBluetoothController.connect(device);
}
} else if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER) {
final MediaRouter.RouteInfo route = (MediaRouter.RouteInfo) item.tag;
@@ -171,18 +251,16 @@ public class OutputChooserDialog extends SystemUIDialog
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);
- }
+ mBluetoothController.disconnect(device);
} else if (item.deviceType == OutputChooserLayout.Item.DEVICE_TYPE_MEDIA_ROUTER) {
mRouter.unselect(UNSELECT_REASON_DISCONNECTED);
}
}
- private void updateItems() {
+ private void updateItems(boolean timeout) {
if (SystemClock.uptimeMillis() - mLastUpdateTime < UPDATE_DELAY_MS) {
mHandler.removeMessages(MSG_UPDATE_ITEMS);
- mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_UPDATE_ITEMS),
+ mHandler.sendMessageAtTime(mHandler.obtainMessage(MSG_UPDATE_ITEMS, timeout),
mLastUpdateTime + UPDATE_DELAY_MS);
return;
}
@@ -194,14 +272,16 @@ public class OutputChooserDialog extends SystemUIDialog
addBluetoothDevices(items);
// Add remote displays
- addRemoteDisplayRoutes(items);
+ if (!mIsInCall) {
+ addRemoteDisplayRoutes(items);
+ }
- Collections.sort(items, ItemComparator.sInstance);
+ items.sort(ItemComparator.sInstance);
- if (items.size() == 0) {
+ if (items.size() == 0 && timeout) {
String emptyMessage = mContext.getString(R.string.output_none_found);
final boolean wifiOff = !mWifiManager.isWifiEnabled();
- final boolean btOff = !mController.isBluetoothEnabled();
+ final boolean btOff = !mBluetoothController.isBluetoothEnabled();
if (wifiOff || btOff) {
emptyMessage = getDisabledServicesMessage(wifiOff, btOff);
}
@@ -219,12 +299,12 @@ public class OutputChooserDialog extends SystemUIDialog
}
private void addBluetoothDevices(List<OutputChooserLayout.Item> items) {
- final Collection<CachedBluetoothDevice> devices = mController.getDevices();
+ final Collection<CachedBluetoothDevice> devices = mBluetoothController.getDevices();
if (devices != null) {
int connectedDevices = 0;
int count = 0;
for (CachedBluetoothDevice device : devices) {
- if (mController.getBondState(device) == BluetoothDevice.BOND_NONE) continue;
+ if (mBluetoothController.getBondState(device) == BluetoothDevice.BOND_NONE) continue;
final int majorClass = device.getBtClass().getMajorDeviceClass();
if (majorClass != BluetoothClass.Device.Major.AUDIO_VIDEO
&& majorClass != BluetoothClass.Device.Major.UNCATEGORIZED) {
@@ -328,22 +408,22 @@ public class OutputChooserDialog extends SystemUIDialog
private final class MediaRouterCallback extends MediaRouter.Callback {
@Override
public void onRouteAdded(MediaRouter router, MediaRouter.RouteInfo info) {
- updateItems();
+ updateItems(false);
}
@Override
public void onRouteRemoved(MediaRouter router, MediaRouter.RouteInfo info) {
- updateItems();
+ updateItems(false);
}
@Override
public void onRouteChanged(MediaRouter router, MediaRouter.RouteInfo info) {
- updateItems();
+ updateItems(false);
}
@Override
public void onRouteSelected(MediaRouter router, MediaRouter.RouteInfo route) {
- dismiss();
+ updateItems(false);
}
}
@@ -361,12 +441,12 @@ public class OutputChooserDialog extends SystemUIDialog
private final BluetoothController.Callback mCallback = new BluetoothController.Callback() {
@Override
public void onBluetoothStateChange(boolean enabled) {
- updateItems();
+ updateItems(false);
}
@Override
public void onBluetoothDevicesChanged() {
- updateItems();
+ updateItems(false);
}
};
@@ -393,9 +473,46 @@ public class OutputChooserDialog extends SystemUIDialog
public void handleMessage(Message message) {
switch (message.what) {
case MSG_UPDATE_ITEMS:
- updateItems();
+ updateItems((Boolean) message.obj);
break;
}
}
};
+
+ private final VolumeDialogController.Callbacks mControllerCallbackH
+ = new VolumeDialogController.Callbacks() {
+ @Override
+ public void onShowRequested(int reason) {
+ dismiss();
+ }
+
+ @Override
+ public void onDismissRequested(int reason) {}
+
+ @Override
+ public void onScreenOff() {
+ dismiss();
+ }
+
+ @Override
+ public void onStateChanged(VolumeDialogController.State state) {}
+
+ @Override
+ public void onLayoutDirectionChanged(int layoutDirection) {}
+
+ @Override
+ public void onConfigurationChanged() {}
+
+ @Override
+ public void onShowVibrateHint() {}
+
+ @Override
+ public void onShowSilentHint() {}
+
+ @Override
+ public void onShowSafetyWarning(int flags) {}
+
+ @Override
+ public void onAccessibilityModeChanged(Boolean showA11yStream) {}
+ };
} \ No newline at end of file
diff --git a/com/android/systemui/volume/OutputChooserLayout.java b/com/android/systemui/volume/OutputChooserLayout.java
index 22ced600..d4c6f897 100644
--- a/com/android/systemui/volume/OutputChooserLayout.java
+++ b/com/android/systemui/volume/OutputChooserLayout.java
@@ -29,8 +29,8 @@ 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.LinearLayout;
import android.widget.TextView;
import com.android.systemui.FontSizeUtils;
@@ -40,11 +40,10 @@ import com.android.systemui.qs.AutoSizingList;
/**
* Limited height list of devices.
*/
-public class OutputChooserLayout extends FrameLayout {
+public class OutputChooserLayout extends LinearLayout {
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();
@@ -55,6 +54,7 @@ public class OutputChooserLayout extends FrameLayout {
private AutoSizingList mItemList;
private View mEmpty;
private TextView mEmptyText;
+ private TextView mTitle;
private Item[] mItems;
@@ -62,8 +62,6 @@ public class OutputChooserLayout extends FrameLayout {
super(context, attrs);
mContext = context;
mTag = TAG;
- mQsDetailIconOverlaySize = (int) getResources().getDimension(
- R.dimen.qs_detail_icon_overlay_size);
}
@Override
@@ -74,7 +72,8 @@ public class OutputChooserLayout extends FrameLayout {
mItemList.setAdapter(mAdapter);
mEmpty = findViewById(android.R.id.empty);
mEmpty.setVisibility(GONE);
- mEmptyText = mEmpty.findViewById(android.R.id.title);
+ mEmptyText = mEmpty.findViewById(R.id.empty_text);
+ mTitle = findViewById(R.id.title);
}
@Override
@@ -84,17 +83,21 @@ public class OutputChooserLayout extends FrameLayout {
int count = mItemList.getChildCount();
for (int i = 0; i < count; i++) {
View item = mItemList.getChildAt(i);
- FontSizeUtils.updateFontSize(item, android.R.id.title,
+ FontSizeUtils.updateFontSize(item, R.id.empty_text,
R.dimen.qs_detail_item_primary_text_size);
FontSizeUtils.updateFontSize(item, android.R.id.summary,
R.dimen.qs_detail_item_secondary_text_size);
+ FontSizeUtils.updateFontSize(item, android.R.id.title,
+ R.dimen.qs_detail_header_text_size);
}
}
+ public void setTitle(int title) {
+ mTitle.setText(title);
+ }
+
public void setEmptyState(String text) {
- mEmpty.post(() -> {
- mEmptyText.setText(text);
- });
+ mEmptyText.setText(text);
}
@Override
@@ -176,11 +179,6 @@ public class OutputChooserLayout extends FrameLayout {
} 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);
diff --git a/com/android/systemui/volume/VolumeDialogComponent.java b/com/android/systemui/volume/VolumeDialogComponent.java
index bc98140f..efa83868 100644
--- a/com/android/systemui/volume/VolumeDialogComponent.java
+++ b/com/android/systemui/volume/VolumeDialogComponent.java
@@ -52,7 +52,7 @@ public class VolumeDialogComponent implements VolumeComponent, TunerService.Tuna
public static final String VOLUME_UP_SILENT = "sysui_volume_up_silent";
public static final String VOLUME_SILENT_DO_NOT_DISTURB = "sysui_do_not_disturb";
- public static final boolean DEFAULT_VOLUME_DOWN_TO_ENTER_SILENT = true;
+ public static final boolean DEFAULT_VOLUME_DOWN_TO_ENTER_SILENT = false;
public static final boolean DEFAULT_VOLUME_UP_TO_EXIT_SILENT = true;
public static final boolean DEFAULT_DO_NOT_DISTURB_WHEN_SILENT = true;
diff --git a/com/android/systemui/volume/VolumeDialogImpl.java b/com/android/systemui/volume/VolumeDialogImpl.java
index d7c80101..385438c6 100644
--- a/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/com/android/systemui/volume/VolumeDialogImpl.java
@@ -20,6 +20,7 @@ import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL
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_SETTINGS_CLICKED;
import static com.android.systemui.volume.Events.DISMISS_REASON_TOUCH_OUTSIDE;
import android.accessibilityservice.AccessibilityServiceInfo;
@@ -30,14 +31,13 @@ import android.app.Dialog;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.DialogInterface;
+import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
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;
@@ -45,9 +45,9 @@ import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
+import android.provider.Settings;
import android.provider.Settings.Global;
-import android.transition.AutoTransition;
-import android.transition.TransitionManager;
+import android.support.v7.media.MediaRouter;
import android.util.Log;
import android.util.Slog;
import android.util.SparseBooleanArray;
@@ -72,7 +72,6 @@ import android.widget.TextView;
import com.android.settingslib.Utils;
import com.android.systemui.Dependency;
-import com.android.systemui.Interpolators;
import com.android.systemui.R;
import com.android.systemui.plugins.VolumeDialog;
import com.android.systemui.plugins.VolumeDialogController;
@@ -104,9 +103,7 @@ public class VolumeDialogImpl implements VolumeDialog {
private CustomDialog mDialog;
private ViewGroup mDialogView;
private ViewGroup mDialogRowsView;
- private ImageButton mExpandButton;
private ImageButton mRingerIcon;
- private ImageButton mOutputChooser;
private TextView mRingerStatus;
private final List<VolumeRow> mRows = new ArrayList<>();
private ConfigurableTexts mConfigurableTexts;
@@ -120,8 +117,6 @@ public class VolumeDialogImpl implements VolumeDialog {
private final ColorStateList mInactiveSliderTint;
private boolean mShowing;
- private boolean mExpanded;
- private boolean mExpandButtonAnimationRunning;
private boolean mShowA11yStream;
private int mActiveStream;
@@ -182,11 +177,11 @@ public class VolumeDialogImpl implements VolumeDialog {
mDialog.setContentView(R.layout.volume_dialog);
mDialog.setOnShowListener(dialog -> {
- mDialogView.setTranslationY(-mDialogView.getHeight());
+ mDialogView.setTranslationX(mDialogView.getWidth() / 2);
mDialogView.setAlpha(0);
mDialogView.animate()
.alpha(1)
- .translationY(0)
+ .translationX(0)
.setDuration(300)
.setInterpolator(new SystemUIInterpolators.LogDecelerateInterpolator())
.withEndAction(() -> {
@@ -205,20 +200,10 @@ public class VolumeDialogImpl implements VolumeDialog {
VolumeUiLayout hardwareLayout = VolumeUiLayout.get(mDialogView);
hardwareLayout.setOutsideTouchListener(view -> dismiss(DISMISS_REASON_TOUCH_OUTSIDE));
- ViewGroup dialogContentView = mDialog.findViewById(R.id.volume_dialog_content);
- mDialogRowsView = dialogContentView.findViewById(R.id.volume_dialog_rows);
+ mDialogRowsView = mDialog.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, true);
@@ -239,6 +224,7 @@ public class VolumeDialogImpl implements VolumeDialog {
} else {
addExistingRows();
}
+
updateRowsH(getActiveRow());
initRingerH();
}
@@ -273,11 +259,9 @@ public class VolumeDialogImpl implements VolumeDialog {
VolumeRow row = new VolumeRow();
initRow(row, stream, iconRes, iconMuteRes, important, defaultStream);
int rowSize;
- int viewSize;
- if (mShowA11yStream && dynamic && (rowSize = mRows.size()) > 1
- && (viewSize = mDialogRowsView.getChildCount()) > 1) {
- // A11y Stream should be the last in the list
- mDialogRowsView.addView(row.view, viewSize - 2);
+ if (mShowA11yStream && dynamic && (rowSize = mRows.size()) > 1) {
+ // A11y Stream should be the first in the list, so it's shown to start of other rows
+ mDialogRowsView.addView(row.view, 0);
mRows.add(rowSize - 2, row);
} else {
mDialogRowsView.addView(row.view);
@@ -315,7 +299,6 @@ 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);
@@ -349,6 +332,9 @@ public class VolumeDialogImpl implements VolumeDialog {
row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row));
row.anim = null;
+ ImageButton outputChooser = row.view.findViewById(R.id.output_chooser);
+ outputChooser.setOnClickListener(mClickOutputChooser);
+
// forward events above the slider into the slider
row.view.setOnTouchListener(new OnTouchListener() {
private final Rect mSliderHitRect = new Rect();
@@ -413,6 +399,9 @@ public class VolumeDialogImpl implements VolumeDialog {
Events.writeEvent(mContext, Events.EVENT_ICON_CLICK, AudioManager.STREAM_RING,
mRingerIcon.getTag());
final StreamState ss = mState.states.get(AudioManager.STREAM_RING);
+ if (ss == null) {
+ return;
+ }
final boolean hasVibrator = mController.hasVibrator();
if (mState.ringerModeInternal == AudioManager.RINGER_MODE_NORMAL) {
if (hasVibrator) {
@@ -429,6 +418,13 @@ public class VolumeDialogImpl implements VolumeDialog {
}
updateRingerH();
});
+ mRingerIcon.setOnLongClickListener(v -> {
+ Intent intent = new Intent(Settings.ACTION_SOUND_SETTINGS);
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ dismissH(DISMISS_REASON_SETTINGS_CLICKED);
+ mContext.startActivity(intent);
+ return true;
+ });
updateRingerH();
}
@@ -465,7 +461,6 @@ 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;
return 3000;
}
@@ -477,13 +472,11 @@ public class VolumeDialogImpl implements VolumeDialog {
mDialogView.animate().cancel();
mShowing = false;
- updateExpandedH(false /* expanding */, true /* dismissing */);
-
- mDialogView.setTranslationY(0);
+ mDialogView.setTranslationX(0);
mDialogView.setAlpha(1);
mDialogView.animate()
.alpha(0)
- .translationY(-mDialogView.getHeight())
+ .translationX(mDialogView.getWidth() / 2)
.setDuration(250)
.setInterpolator(new SystemUIInterpolators.LogAccelerateInterpolator())
.withEndAction(() -> mHandler.postDelayed(() -> {
@@ -511,67 +504,6 @@ 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();
}
@@ -594,7 +526,7 @@ public class VolumeDialogImpl implements VolumeDialog {
return true;
}
- return row.defaultStream || isActive || (mExpanded && row.important);
+ return row.defaultStream || isActive;
}
private void updateRowsH(final VolumeRow activeRow) {
@@ -617,6 +549,9 @@ public class VolumeDialogImpl implements VolumeDialog {
protected void updateRingerH() {
if (mState != null) {
final StreamState ss = mState.states.get(AudioManager.STREAM_RING);
+ if (ss == null) {
+ return;
+ }
switch (mState.ringerModeInternal) {
case AudioManager.RINGER_MODE_VIBRATE:
mRingerStatus.setText(R.string.volume_ringer_status_vibrate);
@@ -924,7 +859,8 @@ public class VolumeDialogImpl implements VolumeDialog {
if (mOutputChooserDialog != null) {
return;
}
- mOutputChooserDialog = new OutputChooserDialog(mContext) {
+ mOutputChooserDialog = new OutputChooserDialog(mContext,
+ new MediaRouterWrapper(MediaRouter.getInstance(mContext))) {
@Override
protected void cleanUp() {
synchronized (mOutputChooserLock) {
@@ -948,16 +884,6 @@ 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) {
diff --git a/com/android/systemui/volume/VolumeUiLayout.java b/com/android/systemui/volume/VolumeUiLayout.java
index 49ac9b6b..368194e5 100644
--- a/com/android/systemui/volume/VolumeUiLayout.java
+++ b/com/android/systemui/volume/VolumeUiLayout.java
@@ -14,15 +14,38 @@
package com.android.systemui.volume;
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE;
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_NONE;
+import static com.android.systemui.util.leak.RotationUtils.ROTATION_SEASCAPE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
import android.content.Context;
+import android.content.res.Configuration;
import android.util.AttributeSet;
+import android.util.Slog;
+import android.view.Gravity;
import android.view.View;
+import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.SeekBar;
+
+import com.android.systemui.R;
+import com.android.systemui.util.leak.RotationUtils;
public class VolumeUiLayout extends FrameLayout {
+ private View mChild;
+ private int mOldHeight;
+ private boolean mAnimating;
+ private AnimatorSet mAnimation;
+ private boolean mHasOutsideTouch;
+ private int mRotation = ROTATION_NONE;
public VolumeUiLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@@ -40,11 +63,254 @@ public class VolumeUiLayout extends FrameLayout {
}
@Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (mChild == null) {
+ if (getChildCount() != 0) {
+ mChild = getChildAt(0);
+ mOldHeight = mChild.getMeasuredHeight();
+ updateRotation();
+ } else {
+ return;
+ }
+ }
+ int newHeight = mChild.getMeasuredHeight();
+ if (newHeight != mOldHeight) {
+ animateChild(mOldHeight, newHeight);
+ }
+ }
+
+ @Override
+ protected void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ updateRotation();
+ }
+
+ private void updateRotation() {
+ int rotation = RotationUtils.getRotation(getContext());
+ if (rotation != mRotation) {
+ rotate(mRotation, rotation);
+ mRotation = rotation;
+ }
+ }
+
+ private void rotate(View view, int from, int to, boolean swapDimens) {
+ if (from != ROTATION_NONE && to != ROTATION_NONE) {
+ // Rather than handling this confusing case, just do 2 rotations.
+ rotate(view, from, ROTATION_NONE, swapDimens);
+ rotate(view, ROTATION_NONE, to, swapDimens);
+ return;
+ }
+ if (from == ROTATION_LANDSCAPE || to == ROTATION_SEASCAPE) {
+ rotateRight(view);
+ } else {
+ rotateLeft(view);
+ }
+ if (to != ROTATION_NONE) {
+ if (swapDimens && view instanceof LinearLayout) {
+ LinearLayout linearLayout = (LinearLayout) view;
+ linearLayout.setOrientation(LinearLayout.HORIZONTAL);
+ swapDimens(view);
+ }
+ } else {
+ if (swapDimens && view instanceof LinearLayout) {
+ LinearLayout linearLayout = (LinearLayout) view;
+ linearLayout.setOrientation(LinearLayout.VERTICAL);
+ swapDimens(view);
+ }
+ }
+ }
+
+ private void rotate(int from, int to) {
+ View footer = mChild.findViewById(R.id.footer);
+ rotate(footer, from, to, false);
+ rotate(this, from, to, true);
+ rotate(mChild, from, to, true);
+ ViewGroup rows = mChild.findViewById(R.id.volume_dialog_rows);
+ rotate(rows, from, to, true);
+ swapOrientation((LinearLayout) rows);
+ int rowCount = rows.getChildCount();
+ for (int i = 0; i < rowCount; i++) {
+ View row = rows.getChildAt(i);
+ if (to == ROTATION_SEASCAPE) {
+ rotateSeekBars(row, to, 180);
+ } else if (to == ROTATION_LANDSCAPE) {
+ rotateSeekBars(row, to, 0);
+ } else {
+ rotateSeekBars(row, to, 270);
+ }
+ rotate(row, from, to, true);
+ }
+ }
+
+ private void swapOrientation(LinearLayout layout) {
+ if(layout.getOrientation() == LinearLayout.HORIZONTAL) {
+ layout.setOrientation(LinearLayout.VERTICAL);
+ } else {
+ layout.setOrientation(LinearLayout.HORIZONTAL);
+ }
+ }
+
+ private void swapDimens(View v) {
+ if (v == null) {
+ return;
+ }
+ ViewGroup.LayoutParams params = v.getLayoutParams();
+ int h = params.width;
+ params.width = params.height;
+ params.height = h;
+ v.setLayoutParams(params);
+ }
+
+ private void rotateSeekBars(View row, int to, int rotation) {
+ SeekBar seekbar = row.findViewById(R.id.volume_row_slider);
+ if (seekbar != null) {
+ seekbar.setRotation((float) rotation);
+ }
+
+ View parent = row.findViewById(R.id.volume_row_slider_frame);
+ swapDimens(parent);
+ ViewGroup.LayoutParams params = seekbar.getLayoutParams();
+ ViewGroup.LayoutParams parentParams = parent.getLayoutParams();
+ if (to != ROTATION_NONE) {
+ params.height = parentParams.height;
+ params.width = parentParams.width;
+ } else {
+ params.height = parentParams.width;
+ params.width = parentParams.height;
+ }
+ seekbar.setLayoutParams(params);
+ }
+
+ private int rotateGravityRight(int gravity) {
+ int retGravity = 0;
+ int layoutDirection = getLayoutDirection();
+ final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
+ final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.CENTER_HORIZONTAL:
+ retGravity |= Gravity.CENTER_VERTICAL;
+ break;
+ case Gravity.RIGHT:
+ retGravity |= Gravity.BOTTOM;
+ break;
+ case Gravity.LEFT:
+ default:
+ retGravity |= Gravity.TOP;
+ break;
+ }
+
+ switch (verticalGravity) {
+ case Gravity.CENTER_VERTICAL:
+ retGravity |= Gravity.CENTER_HORIZONTAL;
+ break;
+ case Gravity.BOTTOM:
+ retGravity |= Gravity.LEFT;
+ break;
+ case Gravity.TOP:
+ default:
+ retGravity |= Gravity.RIGHT;
+ break;
+ }
+ return retGravity;
+ }
+
+ private int rotateGravityLeft(int gravity) {
+ if (gravity == -1) {
+ gravity = Gravity.TOP | Gravity.START;
+ }
+ int retGravity = 0;
+ int layoutDirection = getLayoutDirection();
+ final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
+ final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+ switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+ case Gravity.CENTER_HORIZONTAL:
+ retGravity |= Gravity.CENTER_VERTICAL;
+ break;
+ case Gravity.RIGHT:
+ retGravity |= Gravity.TOP;
+ break;
+ case Gravity.LEFT:
+ default:
+ retGravity |= Gravity.BOTTOM;
+ break;
+ }
+
+ switch (verticalGravity) {
+ case Gravity.CENTER_VERTICAL:
+ retGravity |= Gravity.CENTER_HORIZONTAL;
+ break;
+ case Gravity.BOTTOM:
+ retGravity |= Gravity.RIGHT;
+ break;
+ case Gravity.TOP:
+ default:
+ retGravity |= Gravity.LEFT;
+ break;
+ }
+ return retGravity;
+ }
+
+ private void rotateLeft(View v) {
+ if (v.getParent() instanceof FrameLayout) {
+ LayoutParams p = (LayoutParams) v.getLayoutParams();
+ p.gravity = rotateGravityLeft(p.gravity);
+ }
+
+ v.setPadding(v.getPaddingTop(), v.getPaddingRight(), v.getPaddingBottom(),
+ v.getPaddingLeft());
+ MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams();
+ params.setMargins(params.topMargin, params.rightMargin, params.bottomMargin,
+ params.leftMargin);
+ v.setLayoutParams(params);
+ }
+
+ private void rotateRight(View v) {
+ if (v.getParent() instanceof FrameLayout) {
+ LayoutParams p = (LayoutParams) v.getLayoutParams();
+ p.gravity = rotateGravityRight(p.gravity);
+ }
+
+ v.setPadding(v.getPaddingBottom(), v.getPaddingLeft(), v.getPaddingTop(),
+ v.getPaddingRight());
+ MarginLayoutParams params = (MarginLayoutParams) v.getLayoutParams();
+ params.setMargins(params.bottomMargin, params.leftMargin, params.topMargin,
+ params.rightMargin);
+ v.setLayoutParams(params);
+ }
+
+ private void animateChild(int oldHeight, int newHeight) {
+ if (true) return;
+ if (mAnimating) {
+ mAnimation.cancel();
+ }
+ mAnimating = true;
+ mAnimation = new AnimatorSet();
+ mAnimation.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(Animator animation) {
+ mAnimating = false;
+ }
+ });
+ int fromTop = mChild.getTop();
+ int fromBottom = mChild.getBottom();
+ int toTop = fromTop - ((newHeight - oldHeight) / 2);
+ int toBottom = fromBottom + ((newHeight - oldHeight) / 2);
+ ObjectAnimator top = ObjectAnimator.ofInt(mChild, "top", fromTop, toTop);
+ mAnimation.playTogether(top,
+ ObjectAnimator.ofInt(mChild, "bottom", fromBottom, toBottom));
+ }
+
+
+ @Override
public ViewOutlineProvider getOutlineProvider() {
return super.getOutlineProvider();
}
public void setOutsideTouchListener(OnClickListener onClickListener) {
+ mHasOutsideTouch = true;
requestLayout();
setOnClickListener(onClickListener);
setClickable(true);
@@ -60,7 +326,14 @@ public class VolumeUiLayout extends FrameLayout {
}
private final ViewTreeObserver.OnComputeInternalInsetsListener mInsetsListener = inoutInfo -> {
+ if (mHasOutsideTouch || (mChild == null)) {
+ inoutInfo.setTouchableInsets(
+ ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
+ return;
+ }
inoutInfo.setTouchableInsets(
- ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME);
+ ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT);
+ inoutInfo.contentInsets.set(mChild.getLeft(), mChild.getTop(),
+ 0, getBottom() - mChild.getBottom());
};
}
diff --git a/com/android/uiautomator/testrunner/UiAutomatorTestCase.java b/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
index 7c9aeded..3d5476d0 100644
--- a/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
+++ b/com/android/uiautomator/testrunner/UiAutomatorTestCase.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2013 The Android Open Source Project
+ * Copyright (C) 2012 The Android Open 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,24 +16,55 @@
package com.android.uiautomator.testrunner;
-import android.app.Instrumentation;
+import android.content.Context;
import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.ServiceManager;
import android.os.SystemClock;
-import android.test.InstrumentationTestCase;
+import android.view.inputmethod.InputMethodInfo;
-import com.android.uiautomator.core.InstrumentationUiAutomatorBridge;
+import com.android.internal.view.IInputMethodManager;
import com.android.uiautomator.core.UiDevice;
+import junit.framework.TestCase;
+
+import java.util.List;
+
/**
- * UI Automator test case that is executed on the device.
+ * UI automation test should extend this class. This class provides access
+ * to the following:
+ * {@link UiDevice} instance
+ * {@link Bundle} for command line parameters.
+ * @since API Level 16
* @deprecated New tests should be written using UI Automator 2.0 which is available as part of the
* Android Testing Support Library.
*/
@Deprecated
-public class UiAutomatorTestCase extends InstrumentationTestCase {
+public class UiAutomatorTestCase extends TestCase {
+ private static final String DISABLE_IME = "disable_ime";
+ private static final String DUMMY_IME_PACKAGE = "com.android.testing.dummyime";
+ private UiDevice mUiDevice;
private Bundle mParams;
private IAutomationSupport mAutomationSupport;
+ private boolean mShouldDisableIme = false;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mShouldDisableIme = "true".equals(mParams.getString(DISABLE_IME));
+ if (mShouldDisableIme) {
+ setDummyIme();
+ }
+ }
+
+ @Override
+ protected void tearDown() throws Exception {
+ if (mShouldDisableIme) {
+ restoreActiveIme();
+ }
+ super.tearDown();
+ }
/**
* Get current instance of {@link UiDevice}. Works similar to calling the static
@@ -41,7 +72,7 @@ public class UiAutomatorTestCase extends InstrumentationTestCase {
* @since API Level 16
*/
public UiDevice getUiDevice() {
- return UiDevice.getInstance();
+ return mUiDevice;
}
/**
@@ -54,43 +85,34 @@ public class UiAutomatorTestCase extends InstrumentationTestCase {
return mParams;
}
- void setAutomationSupport(IAutomationSupport automationSupport) {
- mAutomationSupport = automationSupport;
- }
-
/**
* Provides support for running tests to report interim status
*
* @return IAutomationSupport
* @since API Level 16
- * @deprecated Use {@link Instrumentation#sendStatus(int, Bundle)} instead
*/
public IAutomationSupport getAutomationSupport() {
- if (mAutomationSupport == null) {
- mAutomationSupport = new InstrumentationAutomationSupport(getInstrumentation());
- }
return mAutomationSupport;
}
/**
- * Initializes this test case.
- *
- * @param params Instrumentation arguments.
+ * package private
+ * @param uiDevice
*/
- void initialize(Bundle params) {
- mParams = params;
+ void setUiDevice(UiDevice uiDevice) {
+ mUiDevice = uiDevice;
+ }
- // check if this is a monkey test mode
- String monkeyVal = mParams.getString("monkey");
- if (monkeyVal != null) {
- // only if the monkey key is specified, we alter the state of monkey
- // else we should leave things as they are.
- getInstrumentation().getUiAutomation().setRunAsMonkey(Boolean.valueOf(monkeyVal));
- }
+ /**
+ * package private
+ * @param params
+ */
+ void setParams(Bundle params) {
+ mParams = params;
+ }
- UiDevice.getInstance().initialize(new InstrumentationUiAutomatorBridge(
- getInstrumentation().getContext(),
- getInstrumentation().getUiAutomation()));
+ void setAutomationSupport(IAutomationSupport automationSupport) {
+ mAutomationSupport = automationSupport;
}
/**
@@ -101,4 +123,28 @@ public class UiAutomatorTestCase extends InstrumentationTestCase {
public void sleep(long ms) {
SystemClock.sleep(ms);
}
+
+ private void setDummyIme() throws RemoteException {
+ IInputMethodManager im = IInputMethodManager.Stub.asInterface(ServiceManager
+ .getService(Context.INPUT_METHOD_SERVICE));
+ List<InputMethodInfo> infos = im.getInputMethodList();
+ String id = null;
+ for (InputMethodInfo info : infos) {
+ if (DUMMY_IME_PACKAGE.equals(info.getComponent().getPackageName())) {
+ id = info.getId();
+ }
+ }
+ if (id == null) {
+ throw new RuntimeException(String.format(
+ "Required testing fixture missing: IME package (%s)", DUMMY_IME_PACKAGE));
+ }
+ im.setInputMethod(null, id);
+ }
+
+ private void restoreActiveIme() throws RemoteException {
+ // TODO: figure out a way to restore active IME
+ // Currently retrieving active IME requires querying secure settings provider, which is hard
+ // to do without a Context; so the caveat here is that to make the post test device usable,
+ // the active IME needs to be manually switched.
+ }
}
diff --git a/com/android/widget/MediaControlView2Impl.java b/com/android/widget/MediaControlView2Impl.java
new file mode 100644
index 00000000..bc370d8a
--- /dev/null
+++ b/com/android/widget/MediaControlView2Impl.java
@@ -0,0 +1,904 @@
+/*
+ * 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.widget;
+
+import android.content.res.Resources;
+import android.media.MediaMetadata;
+import android.media.session.MediaController;
+import android.media.session.PlaybackState;
+import android.media.update.MediaControlView2Provider;
+import android.media.update.ViewProvider;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.ImageButton;
+import android.widget.MediaControlView2;
+import android.widget.ProgressBar;
+import android.widget.SeekBar;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+import android.widget.TextView;
+
+import com.android.media.update.ApiHelper;
+import com.android.media.update.R;
+
+import java.util.Formatter;
+import java.util.Locale;
+
+public class MediaControlView2Impl implements MediaControlView2Provider {
+ private static final String TAG = "MediaControlView2";
+
+ private final MediaControlView2 mInstance;
+ private final ViewProvider mSuperProvider;
+
+ static final String COMMAND_SHOW_SUBTITLE = "showSubtitle";
+ static final String COMMAND_HIDE_SUBTITLE = "hideSubtitle";
+ static final String COMMAND_SET_FULLSCREEN = "setFullscreen";
+
+ static final String ARGUMENT_KEY_FULLSCREEN = "fullScreen";
+
+ static final String KEY_STATE_CONTAINS_SUBTITLE = "StateContainsSubtitle";
+ static final String EVENT_UPDATE_SUBTITLE_STATUS = "UpdateSubtitleStatus";
+
+ private static final int MAX_PROGRESS = 1000;
+ private static final int DEFAULT_PROGRESS_UPDATE_TIME_MS = 1000;
+ private static final int DEFAULT_TIMEOUT_MS = 2000;
+
+ private static final int REWIND_TIME_MS = 10000;
+ private static final int FORWARD_TIME_MS = 30000;
+
+ private final AccessibilityManager mAccessibilityManager;
+
+ private MediaController mController;
+ private MediaController.TransportControls mControls;
+ private PlaybackState mPlaybackState;
+ private MediaMetadata mMetadata;
+ private ProgressBar mProgress;
+ private TextView mEndTime, mCurrentTime;
+ private TextView mTitleView;
+ private int mDuration;
+ private int mPrevState;
+ private long mPlaybackActions;
+ private boolean mShowing;
+ private boolean mDragging;
+ private boolean mIsFullScreen;
+ private boolean mOverflowExpanded;
+ private boolean mIsStopped;
+ private boolean mSubtitleIsEnabled;
+ private boolean mContainsSubtitle;
+ private boolean mSeekAvailable;
+ private View.OnClickListener mNextListener, mPrevListener;
+ private ImageButton mPlayPauseButton;
+ private ImageButton mFfwdButton;
+ private ImageButton mRewButton;
+ private ImageButton mNextButton;
+ private ImageButton mPrevButton;
+
+ private ViewGroup mBasicControls;
+ private ImageButton mSubtitleButton;
+ private ImageButton mFullScreenButton;
+ private ImageButton mOverflowButtonRight;
+
+ private ViewGroup mExtraControls;
+ private ImageButton mOverflowButtonLeft;
+ private ImageButton mMuteButton;
+ private ImageButton mAspectRationButton;
+ private ImageButton mSettingsButton;
+
+ private CharSequence mPlayDescription;
+ private CharSequence mPauseDescription;
+ private CharSequence mReplayDescription;
+
+ private StringBuilder mFormatBuilder;
+ private Formatter mFormatter;
+
+ public MediaControlView2Impl(
+ MediaControlView2 instance, ViewProvider superProvider) {
+ mInstance = instance;
+ mSuperProvider = superProvider;
+ mAccessibilityManager = AccessibilityManager.getInstance(mInstance.getContext());
+
+ // Inflate MediaControlView2 from XML
+ View root = makeControllerView();
+ mInstance.addView(root);
+ }
+
+ @Override
+ public void setController_impl(MediaController controller) {
+ mController = controller;
+ if (controller != null) {
+ mControls = controller.getTransportControls();
+ // Set mMetadata and mPlaybackState to existing MediaSession variables since they may
+ // be called before the callback is called
+ mPlaybackState = mController.getPlaybackState();
+ mMetadata = mController.getMetadata();
+ updateDuration();
+ updateTitle();
+
+ mController.registerCallback(new MediaControllerCallback());
+ }
+ }
+
+ @Override
+ public void show_impl() {
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+
+ @Override
+ public void show_impl(int timeout) {
+ if (!mShowing) {
+ setProgress();
+ if (mPlayPauseButton != null) {
+ mPlayPauseButton.requestFocus();
+ }
+ disableUnsupportedButtons();
+ mInstance.setVisibility(View.VISIBLE);
+ mShowing = true;
+ }
+ // cause the progress bar to be updated even if mShowing
+ // was already true. This happens, for example, if we're
+ // paused with the progress bar showing the user hits play.
+ mInstance.post(mShowProgress);
+
+ if (timeout != 0 && !mAccessibilityManager.isTouchExplorationEnabled()) {
+ mInstance.removeCallbacks(mFadeOut);
+ mInstance.postDelayed(mFadeOut, timeout);
+ }
+ }
+
+ @Override
+ public boolean isShowing_impl() {
+ return mShowing;
+ }
+
+ @Override
+ public void hide_impl() {
+ if (mShowing) {
+ try {
+ mInstance.removeCallbacks(mShowProgress);
+ // Remove existing call to mFadeOut to avoid from being called later.
+ mInstance.removeCallbacks(mFadeOut);
+ mInstance.setVisibility(View.GONE);
+ } catch (IllegalArgumentException ex) {
+ Log.w(TAG, "already removed");
+ }
+ mShowing = false;
+ }
+ }
+
+ @Override
+ public void showSubtitle_impl() {
+ mController.sendCommand(COMMAND_SHOW_SUBTITLE, null, null);
+ }
+
+ @Override
+ public void hideSubtitle_impl() {
+ mController.sendCommand(COMMAND_HIDE_SUBTITLE, null, null);
+ }
+
+ @Override
+ public void setPrevNextListeners_impl(View.OnClickListener next, View.OnClickListener prev) {
+ mNextListener = next;
+ mPrevListener = prev;
+
+ if (mNextButton != null) {
+ mNextButton.setOnClickListener(mNextListener);
+ mNextButton.setEnabled(mNextListener != null);
+ mNextButton.setVisibility(View.VISIBLE);
+ }
+ if (mPrevButton != null) {
+ mPrevButton.setOnClickListener(mPrevListener);
+ mPrevButton.setEnabled(mPrevListener != null);
+ mPrevButton.setVisibility(View.VISIBLE);
+ }
+ }
+
+ @Override
+ public void setButtonVisibility_impl(int button, boolean visible) {
+ switch (button) {
+ case MediaControlView2.BUTTON_PLAY_PAUSE:
+ if (mPlayPauseButton != null && canPause()) {
+ mPlayPauseButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ }
+ break;
+ case MediaControlView2.BUTTON_FFWD:
+ if (mFfwdButton != null && canSeekForward()) {
+ mFfwdButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ }
+ break;
+ case MediaControlView2.BUTTON_REW:
+ if (mRewButton != null && canSeekBackward()) {
+ mRewButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ }
+ break;
+ case MediaControlView2.BUTTON_NEXT:
+ // TODO: this button is not visible unless its listener is manually set. Should this
+ // function still be provided?
+ if (mNextButton != null) {
+ mNextButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ }
+ break;
+ case MediaControlView2.BUTTON_PREV:
+ // TODO: this button is not visible unless its listener is manually set. Should this
+ // function still be provided?
+ if (mPrevButton != null) {
+ mPrevButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ }
+ break;
+ case MediaControlView2.BUTTON_SUBTITLE:
+ if (mSubtitleButton != null && mContainsSubtitle) {
+ mSubtitleButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ }
+ break;
+ case MediaControlView2.BUTTON_FULL_SCREEN:
+ if (mFullScreenButton != null) {
+ mFullScreenButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ }
+ break;
+ case MediaControlView2.BUTTON_OVERFLOW:
+ if (mOverflowButtonRight != null) {
+ mOverflowButtonRight.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ }
+ break;
+ case MediaControlView2.BUTTON_MUTE:
+ if (mMuteButton != null) {
+ mMuteButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ }
+ break;
+ case MediaControlView2.BUTTON_ASPECT_RATIO:
+ if (mAspectRationButton != null) {
+ mAspectRationButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ }
+ break;
+ case MediaControlView2.BUTTON_SETTINGS:
+ if (mSettingsButton != null) {
+ mSettingsButton.setVisibility((visible) ? View.VISIBLE : View.GONE);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ @Override
+ public void onAttachedToWindow_impl() {
+ mSuperProvider.onAttachedToWindow_impl();
+ }
+
+ @Override
+ public void onDetachedFromWindow_impl() {
+ mSuperProvider.onDetachedFromWindow_impl();
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName_impl() {
+ return MediaControlView2.class.getName();
+ }
+
+ @Override
+ public boolean onTouchEvent_impl(MotionEvent ev) {
+ return false;
+ }
+
+ // TODO: Should this function be removed?
+ @Override
+ public boolean onTrackballEvent_impl(MotionEvent ev) {
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ return false;
+ }
+
+ @Override
+ public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
+ return mSuperProvider.onKeyDown_impl(keyCode, event);
+ }
+
+ @Override
+ public void onFinishInflate_impl() {
+ mSuperProvider.onFinishInflate_impl();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent_impl(KeyEvent event) {
+ int keyCode = event.getKeyCode();
+ final boolean uniqueDown = event.getRepeatCount() == 0
+ && event.getAction() == KeyEvent.ACTION_DOWN;
+ if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
+ || keyCode == KeyEvent.KEYCODE_SPACE) {
+ if (uniqueDown) {
+ togglePausePlayState();
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ if (mPlayPauseButton != null) {
+ mPlayPauseButton.requestFocus();
+ }
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
+ if (uniqueDown && !isPlaying()) {
+ togglePausePlayState();
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+ if (uniqueDown && isPlaying()) {
+ togglePausePlayState();
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+ || keyCode == KeyEvent.KEYCODE_VOLUME_UP
+ || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
+ || keyCode == KeyEvent.KEYCODE_CAMERA) {
+ // don't show the controls for volume adjustment
+ return mSuperProvider.dispatchKeyEvent_impl(event);
+ } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
+ if (uniqueDown) {
+ mInstance.hide();
+ }
+ return true;
+ }
+
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ return mSuperProvider.dispatchKeyEvent_impl(event);
+ }
+
+ @Override
+ public void setEnabled_impl(boolean enabled) {
+ if (mPlayPauseButton != null) {
+ mPlayPauseButton.setEnabled(enabled);
+ }
+ if (mFfwdButton != null) {
+ mFfwdButton.setEnabled(enabled);
+ }
+ if (mRewButton != null) {
+ mRewButton.setEnabled(enabled);
+ }
+ if (mNextButton != null) {
+ mNextButton.setEnabled(enabled);
+ }
+ if (mPrevButton != null) {
+ mPrevButton.setEnabled(enabled);
+ }
+ if (mProgress != null) {
+ mProgress.setEnabled(enabled);
+ }
+ disableUnsupportedButtons();
+ mSuperProvider.setEnabled_impl(enabled);
+ }
+
+ ///////////////////////////////////////////////////
+ // Protected or private methods
+ ///////////////////////////////////////////////////
+
+ private boolean isPlaying() {
+ if (mPlaybackState != null) {
+ return mPlaybackState.getState() == PlaybackState.STATE_PLAYING;
+ }
+ return false;
+ }
+
+ private int getCurrentPosition() {
+ mPlaybackState = mController.getPlaybackState();
+ if (mPlaybackState != null) {
+ return (int) mPlaybackState.getPosition();
+ }
+ return 0;
+ }
+
+ private int getBufferPercentage() {
+ if (mDuration == 0) {
+ return 0;
+ }
+ mPlaybackState = mController.getPlaybackState();
+ if (mPlaybackState != null) {
+ return (int) (mPlaybackState.getBufferedPosition() * 100) / mDuration;
+ }
+ return 0;
+ }
+
+ private boolean canPause() {
+ if (mPlaybackState != null) {
+ return (mPlaybackState.getActions() & PlaybackState.ACTION_PAUSE) != 0;
+ }
+ return true;
+ }
+
+ private boolean canSeekBackward() {
+ if (mPlaybackState != null) {
+ return (mPlaybackState.getActions() & PlaybackState.ACTION_REWIND) != 0;
+ }
+ return true;
+ }
+
+ private boolean canSeekForward() {
+ if (mPlaybackState != null) {
+ return (mPlaybackState.getActions() & PlaybackState.ACTION_FAST_FORWARD) != 0;
+ }
+ return true;
+ }
+
+ /**
+ * Create the view that holds the widgets that control playback.
+ * Derived classes can override this to create their own.
+ *
+ * @return The controller view.
+ * @hide This doesn't work as advertised
+ */
+ protected View makeControllerView() {
+ View root = ApiHelper.inflateLibLayout(mInstance.getContext(), R.layout.media_controller);
+ initControllerView(root);
+ return root;
+ }
+
+ private void initControllerView(View v) {
+ Resources res = ApiHelper.getLibResources();
+ mPlayDescription = res.getText(R.string.lockscreen_play_button_content_description);
+ mPauseDescription = res.getText(R.string.lockscreen_pause_button_content_description);
+ mReplayDescription = res.getText(R.string.lockscreen_replay_button_content_description);
+
+ mPlayPauseButton = v.findViewById(R.id.pause);
+ if (mPlayPauseButton != null) {
+ mPlayPauseButton.requestFocus();
+ mPlayPauseButton.setOnClickListener(mPlayPauseListener);
+ mPlayPauseButton.setColorFilter(R.integer.gray);
+ mPlayPauseButton.setEnabled(false);
+ }
+ mFfwdButton = v.findViewById(R.id.ffwd);
+ if (mFfwdButton != null) {
+ mFfwdButton.setOnClickListener(mFfwdListener);
+ mFfwdButton.setColorFilter(R.integer.gray);
+ mFfwdButton.setEnabled(false);
+ }
+ mRewButton = v.findViewById(R.id.rew);
+ if (mRewButton != null) {
+ mRewButton.setOnClickListener(mRewListener);
+ mRewButton.setColorFilter(R.integer.gray);
+ mRewButton.setEnabled(false);
+ }
+ mNextButton = v.findViewById(R.id.next);
+ if (mNextButton != null) {
+ mNextButton.setVisibility(View.GONE);
+ }
+ mPrevButton = v.findViewById(R.id.prev);
+ if (mPrevButton != null) {
+ mPrevButton.setVisibility(View.GONE);
+ }
+
+ mBasicControls = v.findViewById(R.id.basic_controls);
+ mSubtitleButton = v.findViewById(R.id.subtitle);
+ if (mSubtitleButton != null) {
+ mSubtitleButton.setOnClickListener(mSubtitleListener);
+ mSubtitleButton.setColorFilter(R.integer.gray);
+ mSubtitleButton.setEnabled(false);
+ }
+ mFullScreenButton = v.findViewById(R.id.fullscreen);
+ if (mFullScreenButton != null) {
+ mFullScreenButton.setOnClickListener(mFullScreenListener);
+ // TODO: Show Fullscreen button when only it is possible.
+ }
+ mOverflowButtonRight = v.findViewById(R.id.overflow_right);
+ if (mOverflowButtonRight != null) {
+ mOverflowButtonRight.setOnClickListener(mOverflowRightListener);
+ }
+
+ // TODO: should these buttons be shown as default?
+ mExtraControls = v.findViewById(R.id.extra_controls);
+ mOverflowButtonLeft = v.findViewById(R.id.overflow_left);
+ if (mOverflowButtonLeft != null) {
+ mOverflowButtonLeft.setOnClickListener(mOverflowLeftListener);
+ }
+ mMuteButton = v.findViewById(R.id.mute);
+ mAspectRationButton = v.findViewById(R.id.aspect_ratio);
+ mSettingsButton = v.findViewById(R.id.settings);
+
+ mProgress = v.findViewById(R.id.mediacontroller_progress);
+ if (mProgress != null) {
+ if (mProgress instanceof SeekBar) {
+ SeekBar seeker = (SeekBar) mProgress;
+ seeker.setOnSeekBarChangeListener(mSeekListener);
+ }
+ mProgress.setMax(MAX_PROGRESS);
+ }
+
+ mTitleView = v.findViewById(R.id.title_text);
+
+ mEndTime = v.findViewById(R.id.time);
+ mCurrentTime = v.findViewById(R.id.time_current);
+ mFormatBuilder = new StringBuilder();
+ mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
+ }
+
+ /**
+ * Disable pause or seek buttons if the stream cannot be paused or seeked.
+ * This requires the control interface to be a MediaPlayerControlExt
+ */
+ private void disableUnsupportedButtons() {
+ try {
+ if (mPlayPauseButton != null && !canPause()) {
+ mPlayPauseButton.setEnabled(false);
+ }
+ if (mRewButton != null && !canSeekBackward()) {
+ mRewButton.setEnabled(false);
+ }
+ if (mFfwdButton != null && !canSeekForward()) {
+ mFfwdButton.setEnabled(false);
+ }
+ // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
+ // this scheme can break the case when applications want to allow seek through the
+ // progress bar but disable forward/backward buttons.
+ //
+ // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
+ // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
+ // shouldn't arise in existing applications.
+ if (mProgress != null && !canSeekBackward() && !canSeekForward()) {
+ mProgress.setEnabled(false);
+ }
+ } catch (IncompatibleClassChangeError ex) {
+ // We were given an old version of the interface, that doesn't have
+ // the canPause/canSeekXYZ methods. This is OK, it just means we
+ // assume the media can be paused and seeked, and so we don't disable
+ // the buttons.
+ }
+ }
+
+ private final Runnable mFadeOut = new Runnable() {
+ @Override
+ public void run() {
+ if (isPlaying()) {
+ mInstance.hide();
+ }
+ }
+ };
+
+ private final Runnable mShowProgress = new Runnable() {
+ @Override
+ public void run() {
+ int pos = setProgress();
+ if (!mDragging && mShowing && isPlaying()) {
+ mInstance.postDelayed(mShowProgress,
+ DEFAULT_PROGRESS_UPDATE_TIME_MS - (pos % DEFAULT_PROGRESS_UPDATE_TIME_MS));
+ }
+ }
+ };
+
+ private String stringForTime(int timeMs) {
+ int totalSeconds = timeMs / 1000;
+
+ int seconds = totalSeconds % 60;
+ int minutes = (totalSeconds / 60) % 60;
+ int hours = totalSeconds / 3600;
+
+ mFormatBuilder.setLength(0);
+ if (hours > 0) {
+ return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
+ } else {
+ return mFormatter.format("%02d:%02d", minutes, seconds).toString();
+ }
+ }
+
+ private int setProgress() {
+ if (mController == null || mDragging) {
+ return 0;
+ }
+ int positionOnProgressBar = 0;
+ int currentPosition = getCurrentPosition();
+ if (mDuration > 0) {
+ positionOnProgressBar = (int) (MAX_PROGRESS * (long) currentPosition / mDuration);
+ }
+ if (mProgress != null && currentPosition != mDuration) {
+ mProgress.setProgress(positionOnProgressBar);
+ mProgress.setSecondaryProgress(getBufferPercentage() * 10);
+ }
+
+ if (mEndTime != null) {
+ mEndTime.setText(stringForTime(mDuration));
+
+ }
+ if (mCurrentTime != null) {
+ mCurrentTime.setText(stringForTime(currentPosition));
+ }
+
+ return currentPosition;
+ }
+
+ private void togglePausePlayState() {
+ if (isPlaying()) {
+ mControls.pause();
+ mPlayPauseButton.setImageDrawable(
+ ApiHelper.getLibResources().getDrawable(
+ R.drawable.ic_play_circle_filled, null));
+ mPlayPauseButton.setContentDescription(mPlayDescription);
+ } else {
+ mControls.play();
+ mPlayPauseButton.setImageDrawable(
+ ApiHelper.getLibResources().getDrawable(
+ R.drawable.ic_pause_circle_filled, null));
+ mPlayPauseButton.setContentDescription(mPauseDescription);
+ }
+ }
+
+ // There are two scenarios that can trigger the seekbar listener to trigger:
+ //
+ // The first is the user using the touchpad to adjust the posititon of the
+ // seekbar's thumb. In this case onStartTrackingTouch is called followed by
+ // a number of onProgressChanged notifications, concluded by onStopTrackingTouch.
+ // We're setting the field "mDragging" to true for the duration of the dragging
+ // session to avoid jumps in the position in case of ongoing playback.
+ //
+ // The second scenario involves the user operating the scroll ball, in this
+ // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications,
+ // we will simply apply the updated position without suspending regular updates.
+ private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
+ @Override
+ public void onStartTrackingTouch(SeekBar bar) {
+ if (!mSeekAvailable) {
+ return;
+ }
+ mInstance.show(3600000);
+
+ mDragging = true;
+
+ // By removing these pending progress messages we make sure
+ // that a) we won't update the progress while the user adjusts
+ // the seekbar and b) once the user is done dragging the thumb
+ // we will post one of these messages to the queue again and
+ // this ensures that there will be exactly one message queued up.
+ mInstance.removeCallbacks(mShowProgress);
+
+ // Check if playback is currently stopped. In this case, update the pause button to
+ // show the play image instead of the replay image.
+ if (mIsStopped) {
+ mPlayPauseButton.setImageDrawable(
+ ApiHelper.getLibResources().getDrawable(
+ R.drawable.ic_play_circle_filled, null));
+ mPlayPauseButton.setContentDescription(mPlayDescription);
+ mIsStopped = false;
+ }
+ }
+
+ @Override
+ public void onProgressChanged(SeekBar bar, int progress, boolean fromUser) {
+ if (!mSeekAvailable) {
+ return;
+ }
+ if (!fromUser) {
+ // We're not interested in programmatically generated changes to
+ // the progress bar's position.
+ return;
+ }
+ if (mDuration > 0) {
+ int newPosition = (int) (((long) mDuration * progress) / MAX_PROGRESS);
+ mControls.seekTo(newPosition);
+
+ if (mCurrentTime != null) {
+ mCurrentTime.setText(stringForTime(newPosition));
+ }
+ }
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar bar) {
+ if (!mSeekAvailable) {
+ return;
+ }
+ mDragging = false;
+
+ setProgress();
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+
+ // Ensure that progress is properly updated in the future,
+ // the call to show() does not guarantee this because it is a
+ // no-op if we are already showing.
+ mInstance.post(mShowProgress);
+ }
+ };
+
+ private final View.OnClickListener mPlayPauseListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ togglePausePlayState();
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ };
+
+ private final View.OnClickListener mRewListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int pos = getCurrentPosition() - REWIND_TIME_MS;
+ mControls.seekTo(pos);
+ setProgress();
+
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ };
+
+ private final View.OnClickListener mFfwdListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ int pos = getCurrentPosition() + FORWARD_TIME_MS;
+ mControls.seekTo(pos);
+ setProgress();
+
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ };
+
+ private final View.OnClickListener mSubtitleListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (!mSubtitleIsEnabled) {
+ mSubtitleButton.setImageDrawable(
+ ApiHelper.getLibResources().getDrawable(
+ R.drawable.ic_media_subtitle_enabled, null));
+ mInstance.showSubtitle();
+ mSubtitleIsEnabled = true;
+ } else {
+ mSubtitleButton.setImageDrawable(
+ ApiHelper.getLibResources().getDrawable(
+ R.drawable.ic_media_subtitle_disabled, null));
+ mInstance.hideSubtitle();
+ mSubtitleIsEnabled = false;
+ }
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ };
+
+ private final View.OnClickListener mFullScreenListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ final boolean isEnteringFullScreen = !mIsFullScreen;
+ // TODO: Re-arrange the button layouts according to the UX.
+ if (isEnteringFullScreen) {
+ mFullScreenButton.setImageDrawable(
+ ApiHelper.getLibResources().getDrawable(
+ R.drawable.ic_fullscreen_exit, null));
+ } else {
+ mFullScreenButton.setImageDrawable(
+ ApiHelper.getLibResources().getDrawable(R.drawable.ic_fullscreen, null));
+ }
+ Bundle args = new Bundle();
+ args.putBoolean(ARGUMENT_KEY_FULLSCREEN, isEnteringFullScreen);
+ mController.sendCommand(COMMAND_SET_FULLSCREEN, args, null);
+
+ mIsFullScreen = isEnteringFullScreen;
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ };
+
+ private final View.OnClickListener mOverflowRightListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mBasicControls.setVisibility(View.GONE);
+ mExtraControls.setVisibility(View.VISIBLE);
+ mInstance.show(DEFAULT_TIMEOUT_MS);
+ }
+ };
+
+ private final View.OnClickListener mOverflowLeftListener = new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ mBasicControls.setVisibility(View.VISIBLE);
+ mExtraControls.setVisibility(View.GONE);
+ }
+ };
+
+ private void updateDuration() {
+ if (mMetadata != null) {
+ if (mMetadata.containsKey(MediaMetadata.METADATA_KEY_DURATION)) {
+ mDuration = (int) mMetadata.getLong(MediaMetadata.METADATA_KEY_DURATION);
+ // update progress bar
+ setProgress();
+ }
+ }
+ }
+
+ private void updateTitle() {
+ if (mMetadata != null) {
+ if (mMetadata.containsKey(MediaMetadata.METADATA_KEY_TITLE)) {
+ mTitleView.setText(mMetadata.getString(MediaMetadata.METADATA_KEY_TITLE));
+ }
+ }
+ }
+
+ private class MediaControllerCallback extends MediaController.Callback {
+ @Override
+ public void onPlaybackStateChanged(PlaybackState state) {
+ mPlaybackState = state;
+
+ // Update pause button depending on playback state for the following two reasons:
+ // 1) Need to handle case where app customizes playback state behavior when app
+ // activity is resumed.
+ // 2) Need to handle case where the media file reaches end of duration.
+ if (mPlaybackState.getState() != mPrevState) {
+ switch (mPlaybackState.getState()) {
+ case PlaybackState.STATE_PLAYING:
+ mPlayPauseButton.setImageDrawable(
+ ApiHelper.getLibResources().getDrawable(
+ R.drawable.ic_pause_circle_filled, null));
+ mPlayPauseButton.setContentDescription(mPauseDescription);
+ break;
+ case PlaybackState.STATE_PAUSED:
+ mPlayPauseButton.setImageDrawable(
+ ApiHelper.getLibResources().getDrawable(
+ R.drawable.ic_play_circle_filled, null));
+ mPlayPauseButton.setContentDescription(mPlayDescription);
+ break;
+ case PlaybackState.STATE_STOPPED:
+ mPlayPauseButton.setImageDrawable(
+ ApiHelper.getLibResources().getDrawable(
+ R.drawable.ic_replay, null));
+ mPlayPauseButton.setContentDescription(mReplayDescription);
+ mIsStopped = true;
+ break;
+ default:
+ break;
+ }
+ mPrevState = mPlaybackState.getState();
+ }
+
+ if (mPlaybackActions != mPlaybackState.getActions()) {
+ long newActions = mPlaybackState.getActions();
+ if ((newActions & PlaybackState.ACTION_PAUSE) != 0) {
+ mPlayPauseButton.clearColorFilter();
+ mPlayPauseButton.setEnabled(true);
+ }
+ if ((newActions & PlaybackState.ACTION_REWIND) != 0) {
+ mRewButton.clearColorFilter();
+ mRewButton.setEnabled(true);
+ }
+ if ((newActions & PlaybackState.ACTION_FAST_FORWARD) != 0) {
+ mFfwdButton.clearColorFilter();
+ mFfwdButton.setEnabled(true);
+ }
+ if ((newActions & PlaybackState.ACTION_SEEK_TO) != 0) {
+ mSeekAvailable = true;
+ } else {
+ mSeekAvailable = false;
+ }
+ mPlaybackActions = newActions;
+ }
+ }
+
+ @Override
+ public void onMetadataChanged(MediaMetadata metadata) {
+ mMetadata = metadata;
+ updateDuration();
+ updateTitle();
+ }
+
+ @Override
+ public void onSessionEvent(String event, Bundle extras) {
+ if (event.equals(EVENT_UPDATE_SUBTITLE_STATUS)) {
+ boolean newSubtitleStatus = extras.getBoolean(KEY_STATE_CONTAINS_SUBTITLE);
+ if (newSubtitleStatus != mContainsSubtitle) {
+ if (newSubtitleStatus) {
+ mSubtitleButton.clearColorFilter();
+ mSubtitleButton.setEnabled(true);
+ } else {
+ mSubtitleButton.setColorFilter(R.integer.gray);
+ mSubtitleButton.setEnabled(false);
+ }
+ mContainsSubtitle = newSubtitleStatus;
+ }
+ }
+ }
+ }
+}
diff --git a/com/android/widget/SubtitleView.java b/com/android/widget/SubtitleView.java
new file mode 100644
index 00000000..90719674
--- /dev/null
+++ b/com/android/widget/SubtitleView.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright 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 com.android.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.media.SubtitleController.Anchor;
+import android.media.SubtitleTrack.RenderingWidget;
+import android.os.Looper;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+class SubtitleView extends FrameLayout implements Anchor {
+ private static final String TAG = "SubtitleView";
+
+ private RenderingWidget mSubtitleWidget;
+ private RenderingWidget.OnChangedListener mSubtitlesChangedListener;
+
+ public SubtitleView(Context context) {
+ this(context, null);
+ }
+
+ public SubtitleView(Context context, @Nullable AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public SubtitleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public SubtitleView(
+ Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ public void setSubtitleWidget(RenderingWidget subtitleWidget) {
+ if (mSubtitleWidget == subtitleWidget) {
+ return;
+ }
+
+ final boolean attachedToWindow = isAttachedToWindow();
+ if (mSubtitleWidget != null) {
+ if (attachedToWindow) {
+ mSubtitleWidget.onDetachedFromWindow();
+ }
+
+ mSubtitleWidget.setOnChangedListener(null);
+ }
+ mSubtitleWidget = subtitleWidget;
+
+ if (subtitleWidget != null) {
+ if (mSubtitlesChangedListener == null) {
+ mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() {
+ @Override
+ public void onChanged(RenderingWidget renderingWidget) {
+ invalidate();
+ }
+ };
+ }
+
+ setWillNotDraw(false);
+ subtitleWidget.setOnChangedListener(mSubtitlesChangedListener);
+
+ if (attachedToWindow) {
+ subtitleWidget.onAttachedToWindow();
+ requestLayout();
+ }
+ } else {
+ setWillNotDraw(true);
+ }
+
+ invalidate();
+ }
+
+ @Override
+ public Looper getSubtitleLooper() {
+ return Looper.getMainLooper();
+ }
+
+ @Override
+ public void onAttachedToWindow() {
+ super.onAttachedToWindow();
+
+ if (mSubtitleWidget != null) {
+ mSubtitleWidget.onAttachedToWindow();
+ }
+ }
+
+ @Override
+ public void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+
+ if (mSubtitleWidget != null) {
+ mSubtitleWidget.onDetachedFromWindow();
+ }
+ }
+
+ @Override
+ public void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ super.onLayout(changed, left, top, right, bottom);
+
+ if (mSubtitleWidget != null) {
+ final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+ final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+ mSubtitleWidget.setSize(width, height);
+ }
+ }
+
+ @Override
+ public void draw(Canvas canvas) {
+ super.draw(canvas);
+
+ if (mSubtitleWidget != null) {
+ final int saveCount = canvas.save();
+ canvas.translate(getPaddingLeft(), getPaddingTop());
+ mSubtitleWidget.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName() {
+ return SubtitleView.class.getName();
+ }
+}
diff --git a/com/android/widget/VideoSurfaceView.java b/com/android/widget/VideoSurfaceView.java
new file mode 100644
index 00000000..85771234
--- /dev/null
+++ b/com/android/widget/VideoSurfaceView.java
@@ -0,0 +1,198 @@
+/*
+ * Copyright 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 com.android.widget;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.media.MediaPlayer;
+import android.support.annotation.NonNull;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+
+import static android.widget.VideoView2.VIEW_TYPE_SURFACEVIEW;
+
+class VideoSurfaceView extends SurfaceView implements VideoViewInterface, SurfaceHolder.Callback {
+ private static final String TAG = "VideoSurfaceView";
+ private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+ private SurfaceHolder mSurfaceHolder = null;
+ private SurfaceListener mSurfaceListener = null;
+ private MediaPlayer mMediaPlayer;
+ // A flag to indicate taking over other view should be proceed.
+ private boolean mIsTakingOverOldView;
+ private VideoViewInterface mOldView;
+
+
+ public VideoSurfaceView(Context context) {
+ this(context, null);
+ }
+
+ public VideoSurfaceView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ getHolder().addCallback(this);
+ }
+
+ ////////////////////////////////////////////////////
+ // implements VideoViewInterface
+ ////////////////////////////////////////////////////
+
+ @Override
+ public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
+ Log.d(TAG, "assignSurfaceToMediaPlayer(): mSurfaceHolder: " + mSurfaceHolder);
+ if (mp == null || !hasAvailableSurface()) {
+ return false;
+ }
+ mp.setDisplay(mSurfaceHolder);
+ return true;
+ }
+
+ @Override
+ public void setSurfaceListener(SurfaceListener l) {
+ mSurfaceListener = l;
+ }
+
+ @Override
+ public int getViewType() {
+ return VIEW_TYPE_SURFACEVIEW;
+ }
+
+ @Override
+ public void setMediaPlayer(MediaPlayer mp) {
+ mMediaPlayer = mp;
+ if (mIsTakingOverOldView) {
+ takeOver(mOldView);
+ }
+ }
+
+ @Override
+ public void takeOver(@NonNull VideoViewInterface oldView) {
+ if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
+ ((View) oldView).setVisibility(GONE);
+ mIsTakingOverOldView = false;
+ mOldView = null;
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceTakeOverDone(this);
+ }
+ } else {
+ mIsTakingOverOldView = true;
+ mOldView = oldView;
+ }
+ }
+
+ @Override
+ public boolean hasAvailableSurface() {
+ return (mSurfaceHolder != null && mSurfaceHolder.getSurface() != null);
+ }
+
+ ////////////////////////////////////////////////////
+ // implements SurfaceHolder.Callback
+ ////////////////////////////////////////////////////
+
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+ Log.d(TAG, "surfaceCreated: mSurfaceHolder: " + mSurfaceHolder + ", new holder: " + holder);
+ mSurfaceHolder = holder;
+ if (mIsTakingOverOldView) {
+ takeOver(mOldView);
+ } else {
+ assignSurfaceToMediaPlayer(mMediaPlayer);
+ }
+
+ if (mSurfaceListener != null) {
+ Rect rect = mSurfaceHolder.getSurfaceFrame();
+ mSurfaceListener.onSurfaceCreated(this, rect.width(), rect.height());
+ }
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceChanged(this, width, height);
+ }
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ // After we return from this we can't use the surface any more
+ mSurfaceHolder = null;
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceDestroyed(this);
+ }
+ }
+
+ // TODO: Investigate the way to move onMeasure() code into FrameLayout.
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int videoWidth = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoWidth();
+ int videoHeight = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoHeight();
+ if (DEBUG) {
+ Log.d(TAG, "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", "
+ + MeasureSpec.toString(heightMeasureSpec) + ")");
+ Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+ Log.i(TAG, " viewSize: " + getWidth() + "/" + getHeight());
+ Log.i(TAG, " mVideoWidth/height: " + videoWidth + ", " + videoHeight);
+ }
+
+ int width = getDefaultSize(videoWidth, widthMeasureSpec);
+ int height = getDefaultSize(videoHeight, heightMeasureSpec);
+
+ if (videoWidth > 0 && videoHeight > 0) {
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ width = widthSpecSize;
+ height = heightSpecSize;
+
+ // for compatibility, we adjust size based on aspect ratio
+ if (videoWidth * height < width * videoHeight) {
+ width = height * videoWidth / videoHeight;
+ if (DEBUG) {
+ Log.d(TAG, "image too wide, correcting. width: " + width);
+ }
+ } else if (videoWidth * height > width * videoHeight) {
+ height = width * videoHeight / videoWidth;
+ if (DEBUG) {
+ Log.d(TAG, "image too tall, correcting. height: " + height);
+ }
+ }
+ } else {
+ // no size yet, just adopt the given spec sizes
+ }
+ setMeasuredDimension(width, height);
+ if (DEBUG) {
+ Log.i(TAG, "end of onMeasure()");
+ Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ViewType: SurfaceView / Visibility: " + getVisibility()
+ + " / surfaceHolder: " + mSurfaceHolder;
+ }
+}
diff --git a/com/android/widget/VideoTextureView.java b/com/android/widget/VideoTextureView.java
new file mode 100644
index 00000000..69a4ebe7
--- /dev/null
+++ b/com/android/widget/VideoTextureView.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright 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 com.android.widget;
+
+import android.content.Context;
+import android.graphics.SurfaceTexture;
+import android.media.MediaPlayer;
+import android.support.annotation.NonNull;
+import android.support.annotation.RequiresApi;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Surface;
+import android.view.TextureView;
+import android.view.View;
+
+import static android.widget.VideoView2.VIEW_TYPE_TEXTUREVIEW;
+
+@RequiresApi(26)
+class VideoTextureView extends TextureView
+ implements VideoViewInterface, TextureView.SurfaceTextureListener {
+ private static final String TAG = "VideoTextureView";
+ private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+
+ private SurfaceTexture mSurfaceTexture;
+ private Surface mSurface;
+ private SurfaceListener mSurfaceListener;
+ private MediaPlayer mMediaPlayer;
+ // A flag to indicate taking over other view should be proceed.
+ private boolean mIsTakingOverOldView;
+ private VideoViewInterface mOldView;
+
+ public VideoTextureView(Context context) {
+ this(context, null);
+ }
+
+ public VideoTextureView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public VideoTextureView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public VideoTextureView(
+ Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ setSurfaceTextureListener(this);
+ }
+
+ ////////////////////////////////////////////////////
+ // implements VideoViewInterface
+ ////////////////////////////////////////////////////
+
+ @Override
+ public boolean assignSurfaceToMediaPlayer(MediaPlayer mp) {
+ Log.d(TAG, "assignSurfaceToMediaPlayer(): mSurfaceTexture: " + mSurfaceTexture);
+ if (mp == null || !hasAvailableSurface()) {
+ // Surface is not ready.
+ return false;
+ }
+ mp.setSurface(mSurface);
+ return true;
+ }
+
+ @Override
+ public void setSurfaceListener(SurfaceListener l) {
+ mSurfaceListener = l;
+ }
+
+ @Override
+ public int getViewType() {
+ return VIEW_TYPE_TEXTUREVIEW;
+ }
+
+ @Override
+ public void setMediaPlayer(MediaPlayer mp) {
+ mMediaPlayer = mp;
+ if (mIsTakingOverOldView) {
+ takeOver(mOldView);
+ }
+ }
+
+ @Override
+ public void takeOver(@NonNull VideoViewInterface oldView) {
+ if (assignSurfaceToMediaPlayer(mMediaPlayer)) {
+ ((View) oldView).setVisibility(GONE);
+ mIsTakingOverOldView = false;
+ mOldView = null;
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceTakeOverDone(this);
+ }
+ } else {
+ mIsTakingOverOldView = true;
+ mOldView = oldView;
+ }
+ }
+
+ @Override
+ public boolean hasAvailableSurface() {
+ return (mSurfaceTexture != null && !mSurfaceTexture.isReleased() && mSurface != null);
+ }
+
+ ////////////////////////////////////////////////////
+ // implements TextureView.SurfaceTextureListener
+ ////////////////////////////////////////////////////
+
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
+ Log.d(TAG, "onSurfaceTextureAvailable: mSurfaceTexture: " + mSurfaceTexture
+ + ", new surface: " + surfaceTexture);
+ mSurfaceTexture = surfaceTexture;
+ mSurface = new Surface(mSurfaceTexture);
+ if (mIsTakingOverOldView) {
+ takeOver(mOldView);
+ } else {
+ assignSurfaceToMediaPlayer(mMediaPlayer);
+ }
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceCreated(this, width, height);
+ }
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceChanged(this, width, height);
+ }
+ // requestLayout(); // TODO: figure out if it should be called here?
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ // no-op
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+ if (mSurfaceListener != null) {
+ mSurfaceListener.onSurfaceDestroyed(this);
+ }
+ mSurfaceTexture = null;
+ mSurface = null;
+ return true;
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ int videoWidth = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoWidth();
+ int videoHeight = (mMediaPlayer == null) ? 0 : mMediaPlayer.getVideoHeight();
+ if (DEBUG) {
+ Log.d(TAG, "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", "
+ + MeasureSpec.toString(heightMeasureSpec) + ")");
+ Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+ Log.i(TAG, " viewSize: " + getWidth() + "/" + getHeight());
+ Log.i(TAG, " mVideoWidth/height: " + videoWidth + ", " + videoHeight);
+ }
+
+ int width = getDefaultSize(videoWidth, widthMeasureSpec);
+ int height = getDefaultSize(videoHeight, heightMeasureSpec);
+
+ if (videoWidth > 0 && videoHeight > 0) {
+ int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+ int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+ width = widthSpecSize;
+ height = heightSpecSize;
+
+ // for compatibility, we adjust size based on aspect ratio
+ if (videoWidth * height < width * videoHeight) {
+ width = height * videoWidth / videoHeight;
+ if (DEBUG) {
+ Log.d(TAG, "image too wide, correcting. width: " + width);
+ }
+ } else if (videoWidth * height > width * videoHeight) {
+ height = width * videoHeight / videoWidth;
+ if (DEBUG) {
+ Log.d(TAG, "image too tall, correcting. height: " + height);
+ }
+ }
+ } else {
+ // no size yet, just adopt the given spec sizes
+ }
+ setMeasuredDimension(width, height);
+ if (DEBUG) {
+ Log.i(TAG, "end of onMeasure()");
+ Log.i(TAG, " measuredSize: " + getMeasuredWidth() + "/" + getMeasuredHeight());
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "ViewType: TextureView / Visibility: " + getVisibility()
+ + " / surfaceTexture: " + mSurfaceTexture;
+
+ }
+}
diff --git a/com/android/widget/VideoView2Impl.java b/com/android/widget/VideoView2Impl.java
new file mode 100644
index 00000000..4c312f8c
--- /dev/null
+++ b/com/android/widget/VideoView2Impl.java
@@ -0,0 +1,1022 @@
+/*
+ * Copyright 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 com.android.widget;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.media.AudioAttributes;
+import android.media.AudioFocusRequest;
+import android.media.AudioManager;
+import android.media.MediaMetadata;
+import android.media.MediaPlayer;
+import android.media.MediaPlayerBase;
+import android.media.Cea708CaptionRenderer;
+import android.media.ClosedCaptionRenderer;
+import android.media.MediaMetadata;
+import android.media.MediaPlayer;
+import android.media.Metadata;
+import android.media.PlaybackParams;
+import android.media.SubtitleController;
+import android.media.TtmlRenderer;
+import android.media.WebVttRenderer;
+import android.media.session.MediaSession;
+import android.media.session.PlaybackState;
+import android.media.update.VideoView2Provider;
+import android.media.update.ViewProvider;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ResultReceiver;
+import android.support.annotation.Nullable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout.LayoutParams;
+import android.widget.MediaControlView2;
+import android.widget.VideoView2;
+
+import com.android.media.update.ApiHelper;
+import com.android.media.update.R;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+public class VideoView2Impl implements VideoView2Provider, VideoViewInterface.SurfaceListener {
+ private static final String TAG = "VideoView2";
+ private static final boolean DEBUG = true; // STOPSHIP: Log.isLoggable(TAG, Log.DEBUG);
+
+ private final VideoView2 mInstance;
+ private final ViewProvider mSuperProvider;
+
+ private static final int STATE_ERROR = -1;
+ private static final int STATE_IDLE = 0;
+ private static final int STATE_PREPARING = 1;
+ private static final int STATE_PREPARED = 2;
+ private static final int STATE_PLAYING = 3;
+ private static final int STATE_PAUSED = 4;
+ private static final int STATE_PLAYBACK_COMPLETED = 5;
+
+ private final AudioManager mAudioManager;
+ private AudioAttributes mAudioAttributes;
+ private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
+ private int mAudioSession;
+
+ private VideoView2.OnPreparedListener mOnPreparedListener;
+ private VideoView2.OnCompletionListener mOnCompletionListener;
+ private VideoView2.OnErrorListener mOnErrorListener;
+ private VideoView2.OnInfoListener mOnInfoListener;
+ private VideoView2.OnViewTypeChangedListener mOnViewTypeChangedListener;
+ private VideoView2.OnFullScreenChangedListener mOnFullScreenChangedListener;
+
+ private VideoViewInterface mCurrentView;
+ private VideoTextureView mTextureView;
+ private VideoSurfaceView mSurfaceView;
+
+ private MediaPlayer mMediaPlayer;
+ private MediaControlView2 mMediaControlView;
+ private MediaSession mMediaSession;
+ private Metadata mMetadata;
+ private String mTitle;
+
+ private PlaybackState.Builder mStateBuilder;
+ private int mTargetState = STATE_IDLE;
+ private int mCurrentState = STATE_IDLE;
+ private int mCurrentBufferPercentage;
+ private int mSeekWhenPrepared; // recording the seek position while preparing
+
+ private int mVideoWidth;
+ private int mVideoHeight;
+
+ private boolean mCCEnabled;
+ private int mSelectedTrackIndex;
+
+ private SubtitleView mSubtitleView;
+ private float mSpeed;
+ // TODO: Remove mFallbackSpeed when integration with MediaPlayer2's new setPlaybackParams().
+ // Refer: https://docs.google.com/document/d/1nzAfns6i2hJ3RkaUre3QMT6wsDedJ5ONLiA_OOBFFX8/edit
+ private float mFallbackSpeed; // keep the original speed before 'pause' is called.
+
+ public VideoView2Impl(VideoView2 instance, ViewProvider superProvider,
+ @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+ mInstance = instance;
+ mSuperProvider = superProvider;
+
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ mSpeed = 1.0f;
+ mFallbackSpeed = mSpeed;
+
+ mAudioManager = (AudioManager) mInstance.getContext()
+ .getSystemService(Context.AUDIO_SERVICE);
+ mAudioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
+ .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build();
+ mInstance.setFocusable(true);
+ mInstance.setFocusableInTouchMode(true);
+ mInstance.requestFocus();
+
+ // TODO: try to keep a single child at a time rather than always having both.
+ mTextureView = new VideoTextureView(mInstance.getContext());
+ mSurfaceView = new VideoSurfaceView(mInstance.getContext());
+ LayoutParams params = new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ params.gravity = Gravity.CENTER;
+ mTextureView.setLayoutParams(params);
+ mSurfaceView.setLayoutParams(params);
+ mTextureView.setSurfaceListener(this);
+ mSurfaceView.setSurfaceListener(this);
+
+ // TODO: Choose TextureView when SurfaceView cannot be created.
+ // Choose surface view by default
+ mTextureView.setVisibility(View.GONE);
+ mSurfaceView.setVisibility(View.VISIBLE);
+ mInstance.addView(mTextureView);
+ mInstance.addView(mSurfaceView);
+ mCurrentView = mSurfaceView;
+
+ LayoutParams subtitleParams = new LayoutParams(LayoutParams.MATCH_PARENT,
+ LayoutParams.MATCH_PARENT);
+ mSubtitleView = new SubtitleView(mInstance.getContext());
+ mSubtitleView.setLayoutParams(subtitleParams);
+ mSubtitleView.setBackgroundColor(0);
+ mInstance.addView(mSubtitleView);
+
+ // TODO: Need a common namespace for attributes those are defined in updatable library.
+ boolean enableControlView = (attrs == null) || attrs.getAttributeBooleanValue(
+ "http://schemas.android.com/apk/com.android.media.update",
+ "enableControlView", true);
+ if (enableControlView) {
+ mMediaControlView = new MediaControlView2(mInstance.getContext());
+ }
+ }
+
+ @Override
+ public void setMediaControlView2_impl(MediaControlView2 mediaControlView) {
+ mMediaControlView = mediaControlView;
+
+ if (mInstance.isAttachedToWindow()) {
+ attachMediaControlView();
+ }
+ }
+
+ @Override
+ public MediaControlView2 getMediaControlView2_impl() {
+ return mMediaControlView;
+ }
+
+ @Override
+ public void start_impl() {
+ if (isInPlaybackState() && mCurrentView.hasAvailableSurface()) {
+ applySpeed();
+ mMediaPlayer.start();
+ mCurrentState = STATE_PLAYING;
+ updatePlaybackState();
+ }
+ mTargetState = STATE_PLAYING;
+ if (DEBUG) {
+ Log.d(TAG, "start(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ }
+
+ @Override
+ public void pause_impl() {
+ if (isInPlaybackState()) {
+ if (mMediaPlayer.isPlaying()) {
+ mMediaPlayer.pause();
+ mCurrentState = STATE_PAUSED;
+ updatePlaybackState();
+ }
+ }
+ mTargetState = STATE_PAUSED;
+ if (DEBUG) {
+ Log.d(TAG, "pause(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ }
+
+ @Override
+ public int getDuration_impl() {
+ if (isInPlaybackState()) {
+ return mMediaPlayer.getDuration();
+ }
+ return -1;
+ }
+
+ @Override
+ public int getCurrentPosition_impl() {
+ if (isInPlaybackState()) {
+ return mMediaPlayer.getCurrentPosition();
+ }
+ return 0;
+ }
+
+ @Override
+ public void seekTo_impl(int msec) {
+ if (isInPlaybackState()) {
+ mMediaPlayer.seekTo(msec);
+ mSeekWhenPrepared = 0;
+ updatePlaybackState();
+ } else {
+ mSeekWhenPrepared = msec;
+ }
+ }
+
+ @Override
+ public boolean isPlaying_impl() {
+ return (isInPlaybackState()) && mMediaPlayer.isPlaying();
+ }
+
+ @Override
+ public int getBufferPercentage_impl() {
+ return mCurrentBufferPercentage;
+ }
+
+ @Override
+ public int getAudioSessionId_impl() {
+ if (mAudioSession == 0) {
+ MediaPlayer foo = new MediaPlayer();
+ mAudioSession = foo.getAudioSessionId();
+ foo.release();
+ }
+ return mAudioSession;
+ }
+
+ @Override
+ public void showSubtitle_impl() {
+ // Retrieve all tracks that belong to the current video.
+ MediaPlayer.TrackInfo[] trackInfos = mMediaPlayer.getTrackInfo();
+
+ List<Integer> subtitleTrackIndices = new ArrayList<>();
+ for (int i = 0; i < trackInfos.length; ++i) {
+ int trackType = trackInfos[i].getTrackType();
+ if (trackType == MediaPlayer.TrackInfo.MEDIA_TRACK_TYPE_SUBTITLE) {
+ subtitleTrackIndices.add(i);
+ }
+ }
+ if (subtitleTrackIndices.size() > 0) {
+ // Select first subtitle track
+ mCCEnabled = true;
+ mSelectedTrackIndex = subtitleTrackIndices.get(0);
+ mMediaPlayer.selectTrack(mSelectedTrackIndex);
+ }
+ }
+
+ @Override
+ public void hideSubtitle_impl() {
+ if (mCCEnabled) {
+ mMediaPlayer.deselectTrack(mSelectedTrackIndex);
+ mCCEnabled = false;
+ }
+ }
+
+ @Override
+ public void setFullScreen_impl(boolean fullScreen) {
+ if (mOnFullScreenChangedListener != null) {
+ mOnFullScreenChangedListener.onFullScreenChanged(fullScreen);
+ }
+ }
+
+ @Override
+ public void setSpeed_impl(float speed) {
+ if (speed <= 0.0f) {
+ Log.e(TAG, "Unsupported speed (" + speed + ") is ignored.");
+ return;
+ }
+ mSpeed = speed;
+ if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
+ applySpeed();
+ }
+ }
+
+ @Override
+ public float getSpeed_impl() {
+ if (DEBUG) {
+ if (mMediaPlayer != null) {
+ float speed = mMediaPlayer.getPlaybackParams().getSpeed();
+ if (speed != mSpeed) {
+ Log.w(TAG, "VideoView2's speed : " + mSpeed + " is different from "
+ + "MediaPlayer's speed : " + speed);
+ }
+ }
+ }
+ return mSpeed;
+ }
+
+ @Override
+ public void setAudioFocusRequest_impl(int focusGain) {
+ if (focusGain != AudioManager.AUDIOFOCUS_NONE
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+ && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
+ throw new IllegalArgumentException("Illegal audio focus type " + focusGain);
+ }
+ mAudioFocusType = focusGain;
+ }
+
+ @Override
+ public void setAudioAttributes_impl(AudioAttributes attributes) {
+ if (attributes == null) {
+ throw new IllegalArgumentException("Illegal null AudioAttributes");
+ }
+ mAudioAttributes = attributes;
+ }
+
+ @Override
+ public void setRouteAttributes_impl(List<String> routeCategories, MediaPlayerBase player) {
+ // TODO: implement this.
+ }
+
+ @Override
+ public void setVideoPath_impl(String path) {
+ mInstance.setVideoURI(Uri.parse(path));
+ }
+
+ @Override
+ public void setVideoURI_impl(Uri uri) {
+ mInstance.setVideoURI(uri, null);
+ }
+
+ @Override
+ public void setVideoURI_impl(Uri uri, Map<String, String> headers) {
+ mSeekWhenPrepared = 0;
+ openVideo(uri, headers);
+ }
+
+ @Override
+ public void setViewType_impl(int viewType) {
+ if (viewType == mCurrentView.getViewType()) {
+ return;
+ }
+ VideoViewInterface targetView;
+ if (viewType == VideoView2.VIEW_TYPE_TEXTUREVIEW) {
+ Log.d(TAG, "switching to TextureView");
+ targetView = mTextureView;
+ } else if (viewType == VideoView2.VIEW_TYPE_SURFACEVIEW) {
+ Log.d(TAG, "switching to SurfaceView");
+ targetView = mSurfaceView;
+ } else {
+ throw new IllegalArgumentException("Unknown view type: " + viewType);
+ }
+ ((View) targetView).setVisibility(View.VISIBLE);
+ targetView.takeOver(mCurrentView);
+ mInstance.requestLayout();
+ }
+
+ @Override
+ public int getViewType_impl() {
+ return mCurrentView.getViewType();
+ }
+
+ @Override
+ public void stopPlayback_impl() {
+ resetPlayer();
+ }
+
+ @Override
+ public void setOnPreparedListener_impl(VideoView2.OnPreparedListener l) {
+ mOnPreparedListener = l;
+ }
+
+ @Override
+ public void setOnCompletionListener_impl(VideoView2.OnCompletionListener l) {
+ mOnCompletionListener = l;
+ }
+
+ @Override
+ public void setOnErrorListener_impl(VideoView2.OnErrorListener l) {
+ mOnErrorListener = l;
+ }
+
+ @Override
+ public void setOnInfoListener_impl(VideoView2.OnInfoListener l) {
+ mOnInfoListener = l;
+ }
+
+ @Override
+ public void setOnViewTypeChangedListener_impl(VideoView2.OnViewTypeChangedListener l) {
+ mOnViewTypeChangedListener = l;
+ }
+
+ @Override
+ public void setFullScreenChangedListener_impl(VideoView2.OnFullScreenChangedListener l) {
+ mOnFullScreenChangedListener = l;
+ }
+
+ @Override
+ public void onAttachedToWindow_impl() {
+ mSuperProvider.onAttachedToWindow_impl();
+
+ // Create MediaSession
+ mMediaSession = new MediaSession(mInstance.getContext(), "VideoView2MediaSession");
+ mMediaSession.setCallback(new MediaSessionCallback());
+
+ attachMediaControlView();
+ }
+
+ @Override
+ public void onDetachedFromWindow_impl() {
+ mSuperProvider.onDetachedFromWindow_impl();
+ mMediaSession.release();
+ mMediaSession = null;
+ }
+
+ @Override
+ public CharSequence getAccessibilityClassName_impl() {
+ return VideoView2.class.getName();
+ }
+
+ @Override
+ public boolean onTouchEvent_impl(MotionEvent ev) {
+ if (DEBUG) {
+ Log.d(TAG, "onTouchEvent(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ if (ev.getAction() == MotionEvent.ACTION_UP
+ && isInPlaybackState() && mMediaControlView != null) {
+ toggleMediaControlViewVisibility();
+ }
+ return mSuperProvider.onTouchEvent_impl(ev);
+ }
+
+ @Override
+ public boolean onTrackballEvent_impl(MotionEvent ev) {
+ if (ev.getAction() == MotionEvent.ACTION_UP
+ && isInPlaybackState() && mMediaControlView != null) {
+ toggleMediaControlViewVisibility();
+ }
+ return mSuperProvider.onTrackballEvent_impl(ev);
+ }
+
+ @Override
+ public boolean onKeyDown_impl(int keyCode, KeyEvent event) {
+ Log.v(TAG, "onKeyDown_impl: " + keyCode);
+ boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK
+ && keyCode != KeyEvent.KEYCODE_VOLUME_UP
+ && keyCode != KeyEvent.KEYCODE_VOLUME_DOWN
+ && keyCode != KeyEvent.KEYCODE_VOLUME_MUTE
+ && keyCode != KeyEvent.KEYCODE_MENU
+ && keyCode != KeyEvent.KEYCODE_CALL
+ && keyCode != KeyEvent.KEYCODE_ENDCALL;
+ if (isInPlaybackState() && isKeyCodeSupported && mMediaControlView != null) {
+ if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
+ if (mMediaPlayer.isPlaying()) {
+ mInstance.pause();
+ mMediaControlView.show();
+ } else {
+ mInstance.start();
+ mMediaControlView.hide();
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
+ if (!mMediaPlayer.isPlaying()) {
+ mInstance.start();
+ mMediaControlView.hide();
+ }
+ return true;
+ } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
+ || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+ if (mMediaPlayer.isPlaying()) {
+ mInstance.pause();
+ mMediaControlView.show();
+ }
+ return true;
+ } else {
+ toggleMediaControlViewVisibility();
+ }
+ }
+
+ return mSuperProvider.onKeyDown_impl(keyCode, event);
+ }
+
+ @Override
+ public void onFinishInflate_impl() {
+ mSuperProvider.onFinishInflate_impl();
+ }
+
+ @Override
+ public boolean dispatchKeyEvent_impl(KeyEvent event) {
+ return mSuperProvider.dispatchKeyEvent_impl(event);
+ }
+
+ @Override
+ public void setEnabled_impl(boolean enabled) {
+ mSuperProvider.setEnabled_impl(enabled);
+ }
+
+ ///////////////////////////////////////////////////
+ // Implements VideoViewInterface.SurfaceListener
+ ///////////////////////////////////////////////////
+
+ @Override
+ public void onSurfaceCreated(View view, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceCreated(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState + ", width/height: " + width + "/" + height
+ + ", " + view.toString());
+ }
+ if (needToStart()) {
+ mInstance.start();
+ }
+ }
+
+ @Override
+ public void onSurfaceDestroyed(View view) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceDestroyed(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState + ", " + view.toString());
+ }
+ if (mMediaControlView != null) {
+ mMediaControlView.hide();
+ }
+ }
+
+ @Override
+ public void onSurfaceChanged(View view, int width, int height) {
+ // TODO: Do we need to call requestLayout here?
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceChanged(). width/height: " + width + "/" + height
+ + ", " + view.toString());
+ }
+ }
+
+ @Override
+ public void onSurfaceTakeOverDone(VideoViewInterface view) {
+ if (DEBUG) {
+ Log.d(TAG, "onSurfaceTakeOverDone(). Now current view is: " + view);
+ }
+ mCurrentView = view;
+ if (mOnViewTypeChangedListener != null) {
+ mOnViewTypeChangedListener.onViewTypeChanged(view.getViewType());
+ }
+ if (needToStart()) {
+ mInstance.start();
+ }
+ }
+
+ ///////////////////////////////////////////////////
+ // Protected or private methods
+ ///////////////////////////////////////////////////
+
+ private void attachMediaControlView() {
+ // Get MediaController from MediaSession and set it inside MediaControlView
+ mMediaControlView.setController(mMediaSession.getController());
+
+ LayoutParams params =
+ new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+ mInstance.addView(mMediaControlView, params);
+ }
+
+ private boolean isInPlaybackState() {
+ return (mMediaPlayer != null
+ && mCurrentState != STATE_ERROR
+ && mCurrentState != STATE_IDLE
+ && mCurrentState != STATE_PREPARING);
+ }
+
+ private boolean needToStart() {
+ return (mMediaPlayer != null
+ && mCurrentState != STATE_PLAYING
+ && mTargetState == STATE_PLAYING);
+ }
+
+ // Creates a MediaPlayer instance and prepare playback.
+ private void openVideo(Uri uri, Map<String, String> headers) {
+ resetPlayer();
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ // TODO this should have a focus listener
+ AudioFocusRequest focusRequest;
+ focusRequest = new AudioFocusRequest.Builder(mAudioFocusType)
+ .setAudioAttributes(mAudioAttributes)
+ .build();
+ mAudioManager.requestAudioFocus(focusRequest);
+ }
+
+ try {
+ Log.d(TAG, "openVideo(): creating new MediaPlayer instance.");
+ mMediaPlayer = new MediaPlayer();
+ mSurfaceView.setMediaPlayer(mMediaPlayer);
+ mTextureView.setMediaPlayer(mMediaPlayer);
+ mCurrentView.assignSurfaceToMediaPlayer(mMediaPlayer);
+
+ // TODO: create SubtitleController in MediaPlayer, but we need
+ // a context for the subtitle renderers
+ final Context context = mInstance.getContext();
+ final SubtitleController controller = new SubtitleController(
+ context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer);
+ controller.registerRenderer(new WebVttRenderer(context));
+ controller.registerRenderer(new TtmlRenderer(context));
+ controller.registerRenderer(new Cea708CaptionRenderer(context));
+ controller.registerRenderer(new ClosedCaptionRenderer(context));
+ mMediaPlayer.setSubtitleAnchor(controller, (SubtitleController.Anchor) mSubtitleView);
+
+ if (mAudioSession != 0) {
+ mMediaPlayer.setAudioSessionId(mAudioSession);
+ } else {
+ mAudioSession = mMediaPlayer.getAudioSessionId();
+ }
+ mMediaPlayer.setOnPreparedListener(mPreparedListener);
+ mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
+ mMediaPlayer.setOnCompletionListener(mCompletionListener);
+ mMediaPlayer.setOnErrorListener(mErrorListener);
+ mMediaPlayer.setOnInfoListener(mInfoListener);
+ mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
+ mCurrentBufferPercentage = 0;
+ mMediaPlayer.setDataSource(mInstance.getContext(), uri, headers);
+ mMediaPlayer.setAudioAttributes(mAudioAttributes);
+ // we don't set the target state here either, but preserve the
+ // target state that was there before.
+ mCurrentState = STATE_PREPARING;
+ mMediaPlayer.prepareAsync();
+
+ // Save file name as title since the file may not have a title Metadata.
+ mTitle = uri.getPath();
+ String scheme = uri.getScheme();
+ if (scheme != null && scheme.equals("file")) {
+ mTitle = uri.getLastPathSegment();
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "openVideo(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ /*
+ for (Pair<InputStream, MediaFormat> pending: mPendingSubtitleTracks) {
+ try {
+ mMediaPlayer.addSubtitleSource(pending.first, pending.second);
+ } catch (IllegalStateException e) {
+ mInfoListener.onInfo(
+ mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0);
+ }
+ }
+ */
+ } catch (IOException | IllegalArgumentException ex) {
+ Log.w(TAG, "Unable to open content: " + uri, ex);
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ mErrorListener.onError(mMediaPlayer,
+ MediaPlayer.MEDIA_ERROR_UNKNOWN, MediaPlayer.MEDIA_ERROR_IO);
+ } finally {
+ //mPendingSubtitleTracks.clear();
+ }
+ }
+
+ /*
+ * Reset the media player in any state
+ */
+ // TODO: Figure out if the legacy code's boolean parameter: cleartargetstate is necessary.
+ private void resetPlayer() {
+ if (mMediaPlayer != null) {
+ mMediaPlayer.reset();
+ mMediaPlayer.release();
+ mMediaPlayer = null;
+ //mPendingSubtitleTracks.clear();
+ mCurrentState = STATE_IDLE;
+ mTargetState = STATE_IDLE;
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ mAudioManager.abandonAudioFocus(null);
+ }
+ }
+ mVideoWidth = 0;
+ mVideoHeight = 0;
+ }
+
+ private void updatePlaybackState() {
+ if (mStateBuilder == null) {
+ // Get the capabilities of the player for this stream
+ mMetadata = mMediaPlayer.getMetadata(MediaPlayer.METADATA_ALL,
+ MediaPlayer.BYPASS_METADATA_FILTER);
+
+ // Add Play action as default
+ long playbackActions = PlaybackState.ACTION_PLAY;
+ if (mMetadata != null) {
+ if (!mMetadata.has(Metadata.PAUSE_AVAILABLE)
+ || mMetadata.getBoolean(Metadata.PAUSE_AVAILABLE)) {
+ playbackActions |= PlaybackState.ACTION_PAUSE;
+ }
+ if (!mMetadata.has(Metadata.SEEK_BACKWARD_AVAILABLE)
+ || mMetadata.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE)) {
+ playbackActions |= PlaybackState.ACTION_REWIND;
+ }
+ if (!mMetadata.has(Metadata.SEEK_FORWARD_AVAILABLE)
+ || mMetadata.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE)) {
+ playbackActions |= PlaybackState.ACTION_FAST_FORWARD;
+ }
+ if (!mMetadata.has(Metadata.SEEK_AVAILABLE)
+ || mMetadata.getBoolean(Metadata.SEEK_AVAILABLE)) {
+ playbackActions |= PlaybackState.ACTION_SEEK_TO;
+ }
+ } else {
+ playbackActions |= (PlaybackState.ACTION_PAUSE |
+ PlaybackState.ACTION_REWIND | PlaybackState.ACTION_FAST_FORWARD |
+ PlaybackState.ACTION_SEEK_TO);
+ }
+ mStateBuilder = new PlaybackState.Builder();
+ mStateBuilder.setActions(playbackActions);
+ mStateBuilder.addCustomAction(MediaControlView2Impl.COMMAND_SHOW_SUBTITLE, null, -1);
+ mStateBuilder.addCustomAction(MediaControlView2Impl.COMMAND_HIDE_SUBTITLE, null, -1);
+ }
+ mStateBuilder.setState(getCorrespondingPlaybackState(),
+ mInstance.getCurrentPosition(), 1.0f);
+ mStateBuilder.setBufferedPosition(
+ (long) (mCurrentBufferPercentage / 100.0) * mInstance.getDuration());
+
+ // Set PlaybackState for MediaSession
+ if (mMediaSession != null) {
+ PlaybackState state = mStateBuilder.build();
+ mMediaSession.setPlaybackState(state);
+ }
+ }
+
+ private int getCorrespondingPlaybackState() {
+ switch (mCurrentState) {
+ case STATE_ERROR:
+ return PlaybackState.STATE_ERROR;
+ case STATE_IDLE:
+ return PlaybackState.STATE_NONE;
+ case STATE_PREPARING:
+ return PlaybackState.STATE_CONNECTING;
+ case STATE_PREPARED:
+ return PlaybackState.STATE_PAUSED;
+ case STATE_PLAYING:
+ return PlaybackState.STATE_PLAYING;
+ case STATE_PAUSED:
+ return PlaybackState.STATE_PAUSED;
+ case STATE_PLAYBACK_COMPLETED:
+ return PlaybackState.STATE_STOPPED;
+ default:
+ return -1;
+ }
+ }
+
+ private void toggleMediaControlViewVisibility() {
+ if (mMediaControlView.isShowing()) {
+ mMediaControlView.hide();
+ } else {
+ mMediaControlView.show();
+ }
+ }
+
+ private void applySpeed() {
+ PlaybackParams params = mMediaPlayer.getPlaybackParams().allowDefaults();
+ if (mSpeed != params.getSpeed()) {
+ try {
+ params.setSpeed(mSpeed);
+ mMediaPlayer.setPlaybackParams(params);
+ mFallbackSpeed = mSpeed;
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "PlaybackParams has unsupported value: " + e);
+ // TODO: should revise this part after integrating with MP2.
+ // If mSpeed had an illegal value for speed rate, system will determine best
+ // handling (see PlaybackParams.AUDIO_FALLBACK_MODE_DEFAULT).
+ // Note: The pre-MP2 returns 0.0f when it is paused. In this case, VideoView2 will
+ // use mFallbackSpeed instead.
+ float fallbackSpeed = mMediaPlayer.getPlaybackParams().allowDefaults().getSpeed();
+ if (fallbackSpeed > 0.0f) {
+ mFallbackSpeed = fallbackSpeed;
+ }
+ mSpeed = mFallbackSpeed;
+ }
+ }
+ }
+
+ MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
+ new MediaPlayer.OnVideoSizeChangedListener() {
+ public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
+ if (DEBUG) {
+ Log.d(TAG, "OnVideoSizeChanged(): size: " + width + "/" + height);
+ }
+ mVideoWidth = mp.getVideoWidth();
+ mVideoHeight = mp.getVideoHeight();
+ if (DEBUG) {
+ Log.d(TAG, "OnVideoSizeChanged(): mVideoSize:" + mVideoWidth + "/"
+ + mVideoHeight);
+ }
+
+ if (mVideoWidth != 0 && mVideoHeight != 0) {
+ mInstance.requestLayout();
+ }
+ }
+ };
+
+ MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
+ public void onPrepared(MediaPlayer mp) {
+ if (DEBUG) {
+ Log.d(TAG, "OnPreparedListener(). mCurrentState=" + mCurrentState
+ + ", mTargetState=" + mTargetState);
+ }
+ mCurrentState = STATE_PREPARED;
+ if (mOnPreparedListener != null) {
+ mOnPreparedListener.onPrepared();
+ }
+ if (mMediaControlView != null) {
+ mMediaControlView.setEnabled(true);
+ }
+ int videoWidth = mp.getVideoWidth();
+ int videoHeight = mp.getVideoHeight();
+
+ // mSeekWhenPrepared may be changed after seekTo() call
+ int seekToPosition = mSeekWhenPrepared;
+ if (seekToPosition != 0) {
+ mInstance.seekTo(seekToPosition);
+ }
+
+ if (videoWidth != 0 && videoHeight != 0) {
+ if (videoWidth != mVideoWidth || videoHeight != mVideoHeight) {
+ if (DEBUG) {
+ Log.i(TAG, "OnPreparedListener() : ");
+ Log.i(TAG, " video size: " + videoWidth + "/" + videoHeight);
+ Log.i(TAG, " measuredSize: " + mInstance.getMeasuredWidth() + "/"
+ + mInstance.getMeasuredHeight());
+ Log.i(TAG, " viewSize: " + mInstance.getWidth() + "/"
+ + mInstance.getHeight());
+ }
+
+ mVideoWidth = videoWidth;
+ mVideoHeight = videoHeight;
+ mInstance.requestLayout();
+ }
+
+ if (needToStart()) {
+ mInstance.start();
+ if (mMediaControlView != null) {
+ mMediaControlView.show();
+ }
+ } else if (!mInstance.isPlaying() && (seekToPosition != 0
+ || mInstance.getCurrentPosition() > 0)) {
+ if (mMediaControlView != null) {
+ // Show the media controls when we're paused into a video and
+ // make them stick.
+ mMediaControlView.show(0);
+ }
+ }
+ } else {
+ // We don't know the video size yet, but should start anyway.
+ // The video size might be reported to us later.
+ if (needToStart()) {
+ mInstance.start();
+ }
+ }
+ // Create and set playback state for MediaControlView2
+ updatePlaybackState();
+
+ // Get and set duration and title values as MediaMetadata for MediaControlView2
+ MediaMetadata.Builder builder = new MediaMetadata.Builder();
+ if (mMetadata != null && mMetadata.has(Metadata.TITLE)) {
+ mTitle = mMetadata.getString(Metadata.TITLE);
+ }
+ builder.putString(MediaMetadata.METADATA_KEY_TITLE, mTitle);
+ builder.putLong(MediaMetadata.METADATA_KEY_DURATION, mInstance.getDuration());
+
+ if (mMediaSession != null) {
+ mMediaSession.setMetadata(builder.build());
+ }
+ }
+ };
+
+ private MediaPlayer.OnCompletionListener mCompletionListener =
+ new MediaPlayer.OnCompletionListener() {
+ public void onCompletion(MediaPlayer mp) {
+ mCurrentState = STATE_PLAYBACK_COMPLETED;
+ mTargetState = STATE_PLAYBACK_COMPLETED;
+ updatePlaybackState();
+
+ if (mOnCompletionListener != null) {
+ mOnCompletionListener.onCompletion();
+ }
+ if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+ mAudioManager.abandonAudioFocus(null);
+ }
+ }
+ };
+
+ private MediaPlayer.OnInfoListener mInfoListener =
+ new MediaPlayer.OnInfoListener() {
+ public boolean onInfo(MediaPlayer mp, int what, int extra) {
+ if (mOnInfoListener != null) {
+ mOnInfoListener.onInfo(what, extra);
+ }
+ return true;
+ }
+ };
+
+ private MediaPlayer.OnErrorListener mErrorListener =
+ new MediaPlayer.OnErrorListener() {
+ public boolean onError(MediaPlayer mp, int frameworkErr, int implErr) {
+ if (DEBUG) {
+ Log.d(TAG, "Error: " + frameworkErr + "," + implErr);
+ }
+ mCurrentState = STATE_ERROR;
+ mTargetState = STATE_ERROR;
+ updatePlaybackState();
+
+ if (mMediaControlView != null) {
+ mMediaControlView.hide();
+ }
+
+ /* If an error handler has been supplied, use it and finish. */
+ if (mOnErrorListener != null) {
+ if (mOnErrorListener.onError(frameworkErr, implErr)) {
+ return true;
+ }
+ }
+
+ /* Otherwise, pop up an error dialog so the user knows that
+ * something bad has happened. Only try and pop up the dialog
+ * if we're attached to a window. When we're going away and no
+ * longer have a window, don't bother showing the user an error.
+ */
+ if (mInstance.getWindowToken() != null) {
+ int messageId;
+
+ if (frameworkErr
+ == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
+ messageId = R.string.VideoView2_error_text_invalid_progressive_playback;
+ } else {
+ messageId = R.string.VideoView2_error_text_unknown;
+ }
+
+ Resources res = ApiHelper.getLibResources();
+ new AlertDialog.Builder(mInstance.getContext())
+ .setMessage(res.getString(messageId))
+ .setPositiveButton(res.getString(R.string.VideoView2_error_button),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int whichButton) {
+ /* If we get here, there is no onError listener, so
+ * at least inform them that the video is over.
+ */
+ if (mOnCompletionListener != null) {
+ mOnCompletionListener.onCompletion();
+ }
+ }
+ })
+ .setCancelable(false)
+ .show();
+ }
+ return true;
+ }
+ };
+
+ private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
+ new MediaPlayer.OnBufferingUpdateListener() {
+ public void onBufferingUpdate(MediaPlayer mp, int percent) {
+ mCurrentBufferPercentage = percent;
+ updatePlaybackState();
+ }
+ };
+
+ private class MediaSessionCallback extends MediaSession.Callback {
+ @Override
+ public void onCommand(String command, Bundle args, ResultReceiver receiver) {
+ switch (command) {
+ case MediaControlView2Impl.COMMAND_SHOW_SUBTITLE:
+ mInstance.showSubtitle();
+ break;
+ case MediaControlView2Impl.COMMAND_HIDE_SUBTITLE:
+ mInstance.hideSubtitle();
+ break;
+ case MediaControlView2Impl.COMMAND_SET_FULLSCREEN:
+ mInstance.setFullScreen(
+ args.getBoolean(MediaControlView2Impl.ARGUMENT_KEY_FULLSCREEN));
+ break;
+ }
+ }
+
+ @Override
+ public void onPlay() {
+ mInstance.start();
+ }
+
+ @Override
+ public void onPause() {
+ mInstance.pause();
+ }
+
+ @Override
+ public void onSeekTo(long pos) {
+ mInstance.seekTo((int) pos);
+ }
+ }
+}
diff --git a/com/android/widget/VideoViewInterface.java b/com/android/widget/VideoViewInterface.java
new file mode 100644
index 00000000..2a5eb945
--- /dev/null
+++ b/com/android/widget/VideoViewInterface.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 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 com.android.widget;
+
+import android.annotation.NonNull;
+import android.media.MediaPlayer;
+import android.view.View;
+
+interface VideoViewInterface {
+ /**
+ * Assigns the view's surface to the given MediaPlayer instance.
+ *
+ * @param mp MediaPlayer
+ * @return true if the surface is successfully assigned, false if not. It will fail to assign
+ * if any of MediaPlayer or surface is unavailable.
+ */
+ boolean assignSurfaceToMediaPlayer(MediaPlayer mp);
+ void setSurfaceListener(SurfaceListener l);
+ int getViewType();
+ void setMediaPlayer(MediaPlayer mp);
+
+ /**
+ * Takes over oldView. It means that the MediaPlayer will start rendering on this view.
+ * The visibility of oldView will be set as {@link View.GONE}. If the view doesn't have a
+ * MediaPlayer instance or its surface is not available, the actual execution is deferred until
+ * a MediaPlayer instance is set by {@link #setMediaPlayer} or its surface becomes available.
+ * {@link SurfaceListener.onSurfaceTakeOverDone} will be called when the actual execution is
+ * done.
+ *
+ * @param oldView The view that MediaPlayer is currently rendering on.
+ */
+ void takeOver(@NonNull VideoViewInterface oldView);
+
+ /**
+ * Indicates if the view's surface is available.
+ *
+ * @return true if the surface is available.
+ */
+ boolean hasAvailableSurface();
+
+ /**
+ * An instance of VideoViewInterface calls these surface notification methods accordingly if
+ * a listener has been registered via {@link #setSurfaceListener(SurfaceListener)}.
+ */
+ interface SurfaceListener {
+ void onSurfaceCreated(View view, int width, int height);
+ void onSurfaceDestroyed(View view);
+ void onSurfaceChanged(View view, int width, int height);
+ void onSurfaceTakeOverDone(VideoViewInterface view);
+ }
+}
diff --git a/foo/bar/ComplexDatabase.java b/foo/bar/ComplexDatabase.java
index cfdc1101..db2b4504 100644
--- a/foo/bar/ComplexDatabase.java
+++ b/foo/bar/ComplexDatabase.java
@@ -28,7 +28,7 @@ public class ComplexDatabase_Impl extends ComplexDatabase {
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\")");
+ _db.execSQL("INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"cd8098a1e968898879c194cef2dff8f7\")");
}
public void dropAllTables(SupportSQLiteDatabase _db) {
@@ -69,7 +69,7 @@ public class ComplexDatabase_Impl extends ComplexDatabase {
+ " Found:\n" + _existingUser);
}
}
- }, "6773601c5bcf94c71ee4eb0de04f21a4");
+ }, "cd8098a1e968898879c194cef2dff8f7", "6773601c5bcf94c71ee4eb0de04f21a4");
final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
.name(configuration.name)
.callback(_openCallback)
@@ -96,4 +96,4 @@ public class ComplexDatabase_Impl extends ComplexDatabase {
}
}
}
-}
+} \ No newline at end of file
diff --git a/foo/bar/UpdateDao.java b/foo/bar/UpdateDao.java
index 1190a0df..0d252ea4 100644
--- a/foo/bar/UpdateDao.java
+++ b/foo/bar/UpdateDao.java
@@ -207,6 +207,17 @@ public class UpdateDao_Impl implements UpdateDao {
}
@Override
+ public void updateAndAge(User user) {
+ __db.beginTransaction();
+ try {
+ UpdateDao.super.updateAndAge(user);
+ __db.setTransactionSuccessful();
+ } finally {
+ __db.endTransaction();
+ }
+ }
+
+ @Override
public void ageUserByUid(String uid) {
final SupportSQLiteStatement _stmt = __preparedStmtOfAgeUserByUid.acquire();
__db.beginTransaction();
diff --git a/java/lang/Daemons.java b/java/lang/Daemons.java
index 0e2cb456..f25c78cf 100644
--- a/java/lang/Daemons.java
+++ b/java/lang/Daemons.java
@@ -400,7 +400,7 @@ public final class Daemons {
Exception syntheticException = new TimeoutException(message);
// We use the stack from where finalize() was running to show where it was stuck.
syntheticException.setStackTrace(FinalizerDaemon.INSTANCE.getStackTrace());
- Thread.UncaughtExceptionHandler h = Thread.getDefaultUncaughtExceptionHandler();
+
// Send SIGQUIT to get native stack traces.
try {
Os.kill(Os.getpid(), OsConstants.SIGQUIT);
@@ -411,15 +411,29 @@ public final class Daemons {
} catch (OutOfMemoryError ignored) {
// May occur while trying to allocate the exception.
}
- if (h == null) {
+
+ // Ideally, we'd want to do this if this Thread had no handler to dispatch to.
+ // Unfortunately, it's extremely to messy to query whether a given Thread has *some*
+ // handler to dispatch to, either via a handler set on itself, via its ThreadGroup
+ // object or via the defaultUncaughtExceptionHandler.
+ //
+ // As an approximation, we log by hand an exit if there's no pre-exception handler nor
+ // a default uncaught exception handler.
+ //
+ // Note that this condition will only ever be hit by ART host tests and standalone
+ // dalvikvm invocations. All zygote forked process *will* have a pre-handler set
+ // in RuntimeInit and they cannot subsequently override it.
+ if (Thread.getUncaughtExceptionPreHandler() == null &&
+ Thread.getDefaultUncaughtExceptionHandler() == null) {
// If we have no handler, log and exit.
System.logE(message, syntheticException);
System.exit(2);
}
+
// Otherwise call the handler to do crash reporting.
// We don't just throw because we're not the thread that
// timed out; we're the thread that detected it.
- h.uncaughtException(Thread.currentThread(), syntheticException);
+ Thread.currentThread().dispatchUncaughtException(syntheticException);
}
}
diff --git a/java/lang/Math.java b/java/lang/Math.java
index 5c5ef1cb..bb84b81a 100644
--- a/java/lang/Math.java
+++ b/java/lang/Math.java
@@ -25,7 +25,7 @@
*/
package java.lang;
-import dalvik.annotation.optimization.FastNative;
+import dalvik.annotation.optimization.CriticalNative;
import java.util.Random;
import sun.misc.FloatConsts;
@@ -107,7 +107,7 @@ import sun.misc.DoubleConsts;
public final class Math {
// Android-changed: Numerous methods in this class are re-implemented in native for performance.
- // Those methods are also annotated @FastNative.
+ // Those methods are also annotated @CriticalNative.
/**
* Don't let anyone instantiate this class.
@@ -140,7 +140,7 @@ public final class Math {
* @param a an angle, in radians.
* @return the sine of the argument.
*/
- @FastNative
+ @CriticalNative
public static native double sin(double a);
/**
@@ -154,7 +154,7 @@ public final class Math {
* @param a an angle, in radians.
* @return the cosine of the argument.
*/
- @FastNative
+ @CriticalNative
public static native double cos(double a);
/**
@@ -170,7 +170,7 @@ public final class Math {
* @param a an angle, in radians.
* @return the tangent of the argument.
*/
- @FastNative
+ @CriticalNative
public static native double tan(double a);
/**
@@ -187,7 +187,7 @@ public final class Math {
* @param a the value whose arc sine is to be returned.
* @return the arc sine of the argument.
*/
- @FastNative
+ @CriticalNative
public static native double asin(double a);
/**
@@ -202,7 +202,7 @@ public final class Math {
* @param a the value whose arc cosine is to be returned.
* @return the arc cosine of the argument.
*/
- @FastNative
+ @CriticalNative
public static native double acos(double a);
/**
@@ -218,7 +218,7 @@ public final class Math {
* @param a the value whose arc tangent is to be returned.
* @return the arc tangent of the argument.
*/
- @FastNative
+ @CriticalNative
public static native double atan(double a);
/**
@@ -267,7 +267,7 @@ public final class Math {
* @return the value <i>e</i><sup>{@code a}</sup>,
* where <i>e</i> is the base of the natural logarithms.
*/
- @FastNative
+ @CriticalNative
public static native double exp(double a);
/**
@@ -287,7 +287,7 @@ public final class Math {
* @return the value ln&nbsp;{@code a}, the natural logarithm of
* {@code a}.
*/
- @FastNative
+ @CriticalNative
public static native double log(double a);
/**
@@ -311,7 +311,7 @@ public final class Math {
* @return the base 10 logarithm of {@code a}.
* @since 1.5
*/
- @FastNative
+ @CriticalNative
public static native double log10(double a);
/**
@@ -331,7 +331,7 @@ public final class Math {
* @return the positive square root of {@code a}.
* If the argument is NaN or less than zero, the result is NaN.
*/
- @FastNative
+ @CriticalNative
public static native double sqrt(double a);
@@ -361,7 +361,7 @@ public final class Math {
* @return the cube root of {@code a}.
* @since 1.5
*/
- @FastNative
+ @CriticalNative
public static native double cbrt(double a);
/**
@@ -386,7 +386,7 @@ public final class Math {
* @return the remainder when {@code f1} is divided by
* {@code f2}.
*/
- @FastNative
+ @CriticalNative
public static native double IEEEremainder(double f1, double f2);
/**
@@ -408,7 +408,7 @@ public final class Math {
* floating-point value that is greater than or equal to
* the argument and is equal to a mathematical integer.
*/
- @FastNative
+ @CriticalNative
public static native double ceil(double a);
/**
@@ -426,7 +426,7 @@ public final class Math {
* floating-point value that less than or equal to the argument
* and is equal to a mathematical integer.
*/
- @FastNative
+ @CriticalNative
public static native double floor(double a);
/**
@@ -444,7 +444,7 @@ public final class Math {
* @return the closest floating-point value to {@code a} that is
* equal to a mathematical integer.
*/
- @FastNative
+ @CriticalNative
public static native double rint(double a);
/**
@@ -499,7 +499,7 @@ public final class Math {
* in polar coordinates that corresponds to the point
* (<i>x</i>,&nbsp;<i>y</i>) in Cartesian coordinates.
*/
- @FastNative
+ @CriticalNative
public static native double atan2(double y, double x);
/**
@@ -625,7 +625,7 @@ public final class Math {
* @param b the exponent.
* @return the value {@code a}<sup>{@code b}</sup>.
*/
- @FastNative
+ @CriticalNative
public static native double pow(double a, double b);
/**
@@ -1593,7 +1593,7 @@ public final class Math {
* @return The hyperbolic sine of {@code x}.
* @since 1.5
*/
- @FastNative
+ @CriticalNative
public static native double sinh(double x);
/**
@@ -1620,7 +1620,7 @@ public final class Math {
* @return The hyperbolic cosine of {@code x}.
* @since 1.5
*/
- @FastNative
+ @CriticalNative
public static native double cosh(double x);
/**
@@ -1659,7 +1659,7 @@ public final class Math {
* @return The hyperbolic tangent of {@code x}.
* @since 1.5
*/
- @FastNative
+ @CriticalNative
public static native double tanh(double x);
/**
@@ -1687,7 +1687,7 @@ public final class Math {
* without intermediate overflow or underflow
* @since 1.5
*/
- @FastNative
+ @CriticalNative
public static native double hypot(double x, double y);
/**
@@ -1724,7 +1724,7 @@ public final class Math {
* @return the value <i>e</i><sup>{@code x}</sup>&nbsp;-&nbsp;1.
* @since 1.5
*/
- @FastNative
+ @CriticalNative
public static native double expm1(double x);
/**
@@ -1760,7 +1760,7 @@ public final class Math {
* log of {@code x}&nbsp;+&nbsp;1
* @since 1.5
*/
- @FastNative
+ @CriticalNative
public static native double log1p(double x);
/**
diff --git a/java/lang/Runtime.java b/java/lang/Runtime.java
index 3d528147..acc75b00 100644
--- a/java/lang/Runtime.java
+++ b/java/lang/Runtime.java
@@ -909,7 +909,7 @@ public class Runtime {
if (absolutePath == null) {
throw new NullPointerException("absolutePath == null");
}
- String error = doLoad(absolutePath, loader);
+ String error = nativeLoad(absolutePath, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
@@ -923,7 +923,7 @@ public class Runtime {
if (filename == null) {
throw new NullPointerException("filename == null");
}
- String error = doLoad(filename, fromClass.getClassLoader());
+ String error = nativeLoad(filename, fromClass.getClassLoader());
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
@@ -1011,7 +1011,7 @@ public class Runtime {
throw new UnsatisfiedLinkError(loader + " couldn't find \"" +
System.mapLibraryName(libraryName) + "\"");
}
- String error = doLoad(filename, loader);
+ String error = nativeLoad(filename, loader);
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
@@ -1026,7 +1026,7 @@ public class Runtime {
candidates.add(candidate);
if (IoUtils.canOpenReadOnly(candidate)) {
- String error = doLoad(candidate, loader);
+ String error = nativeLoad(candidate, loader);
if (error == null) {
return; // We successfully loaded the library. Job done.
}
@@ -1067,42 +1067,8 @@ public class Runtime {
}
return paths;
}
- private String doLoad(String name, ClassLoader loader) {
- // Android apps are forked from the zygote, so they can't have a custom LD_LIBRARY_PATH,
- // which means that by default an app's shared library directory isn't on LD_LIBRARY_PATH.
- // The PathClassLoader set up by frameworks/base knows the appropriate path, so we can load
- // libraries with no dependencies just fine, but an app that has multiple libraries that
- // depend on each other needed to load them in most-dependent-first order.
-
- // We added API to Android's dynamic linker so we can update the library path used for
- // the currently-running process. We pull the desired path out of the ClassLoader here
- // and pass it to nativeLoad so that it can call the private dynamic linker API.
-
- // We didn't just change frameworks/base to update the LD_LIBRARY_PATH once at the
- // beginning because multiple apks can run in the same process and third party code can
- // use its own BaseDexClassLoader.
-
- // We didn't just add a dlopen_with_custom_LD_LIBRARY_PATH call because we wanted any
- // dlopen(3) calls made from a .so's JNI_OnLoad to work too.
-
- // So, find out what the native library search path is for the ClassLoader in question...
- String librarySearchPath = null;
- if (loader != null && loader instanceof BaseDexClassLoader) {
- BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader;
- librarySearchPath = dexClassLoader.getLdLibraryPath();
- }
- // nativeLoad should be synchronized so there's only one LD_LIBRARY_PATH in use regardless
- // of how many ClassLoaders are in the system, but dalvik doesn't support synchronized
- // internal natives.
- synchronized (this) {
- return nativeLoad(name, loader, librarySearchPath);
- }
- }
-
- // TODO: should be synchronized, but dalvik doesn't support synchronized internal natives.
- private static native String nativeLoad(String filename, ClassLoader loader,
- String librarySearchPath);
+ private static native String nativeLoad(String filename, ClassLoader loader);
/**
* Creates a localized version of an input stream. This method takes
diff --git a/java/lang/StringFactory.java b/java/lang/StringFactory.java
index 208a657f..1866562b 100644
--- a/java/lang/StringFactory.java
+++ b/java/lang/StringFactory.java
@@ -65,6 +65,14 @@ public final class StringFactory {
return newStringFromBytes(data, 0, data.length, Charset.forNameUEE(charsetName));
}
+ private static final int[] TABLE_UTF8_NEEDED = new int[] {
+ // 0 1 2 3 4 5 6 7 8 9 a b c d e f
+ 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0xc0 - 0xcf
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 0xd0 - 0xdf
+ 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, // 0xe0 - 0xef
+ 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0xf0 - 0xff
+ };
+
// TODO: Implement this method natively.
public static String newStringFromBytes(byte[] data, int offset, int byteCount, Charset charset) {
if ((offset | byteCount) < 0 || byteCount > data.length - offset) {
@@ -77,98 +85,137 @@ public final class StringFactory {
// We inline UTF-8, ISO-8859-1, and US-ASCII decoders for speed.
String canonicalCharsetName = charset.name();
if (canonicalCharsetName.equals("UTF-8")) {
+ /*
+ This code converts a UTF-8 byte sequence to a Java String (UTF-16).
+ It implements the W3C recommended UTF-8 decoder.
+ https://www.w3.org/TR/encoding/#utf-8-decoder
+
+ Unicode 3.2 Well-Formed UTF-8 Byte Sequences
+ Code Points First Second Third Fourth
+ U+0000..U+007F 00..7F
+ U+0080..U+07FF C2..DF 80..BF
+ U+0800..U+0FFF E0 A0..BF 80..BF
+ U+1000..U+CFFF E1..EC 80..BF 80..BF
+ U+D000..U+D7FF ED 80..9F 80..BF
+ U+E000..U+FFFF EE..EF 80..BF 80..BF
+ U+10000..U+3FFFF F0 90..BF 80..BF 80..BF
+ U+40000..U+FFFFF F1..F3 80..BF 80..BF 80..BF
+ U+100000..U+10FFFF F4 80..8F 80..BF 80..BF
+
+ Please refer to Unicode as the authority.
+ p.126 Table 3-7 in http://www.unicode.org/versions/Unicode10.0.0/ch03.pdf
+
+ Handling Malformed Input
+ The maximal subpart should be replaced by a single U+FFFD. Maximal subpart is
+ the longest code unit subsequence starting at an unconvertible offset that is either
+ 1) the initial subsequence of a well-formed code unit sequence, or
+ 2) a subsequence of length one:
+ One U+FFFD should be emitted for every sequence of bytes that is an incomplete prefix
+ of a valid sequence, and with the conversion to restart after the incomplete sequence.
+
+ For example, in byte sequence "41 C0 AF 41 F4 80 80 41", the maximal subparts are
+ "C0", "AF", and "F4 80 80". "F4 80 80" can be the initial subsequence of "F4 80 80 80",
+ but "C0" can't be the initial subsequence of any well-formed code unit sequence.
+ Thus, the output should be "A\ufffd\ufffdA\ufffdA".
+
+ Please refer to section "Best Practices for Using U+FFFD." in
+ http://www.unicode.org/versions/Unicode10.0.0/ch03.pdf
+ */
byte[] d = data;
char[] v = new char[byteCount];
int idx = offset;
int last = offset + byteCount;
int s = 0;
-outer:
+
+ int codePoint = 0;
+ int utf8BytesSeen = 0;
+ int utf8BytesNeeded = 0;
+ int lowerBound = 0x80;
+ int upperBound = 0xbf;
+
while (idx < last) {
- byte b0 = d[idx++];
- if ((b0 & 0x80) == 0) {
- // 0xxxxxxx
- // Range: U-00000000 - U-0000007F
- int val = b0 & 0xff;
- v[s++] = (char) val;
- } else if (((b0 & 0xe0) == 0xc0) || ((b0 & 0xf0) == 0xe0) ||
- ((b0 & 0xf8) == 0xf0) || ((b0 & 0xfc) == 0xf8) || ((b0 & 0xfe) == 0xfc)) {
- int utfCount = 1;
- if ((b0 & 0xf0) == 0xe0) utfCount = 2;
- else if ((b0 & 0xf8) == 0xf0) utfCount = 3;
- else if ((b0 & 0xfc) == 0xf8) utfCount = 4;
- else if ((b0 & 0xfe) == 0xfc) utfCount = 5;
-
- // 110xxxxx (10xxxxxx)+
- // Range: U-00000080 - U-000007FF (count == 1)
- // Range: U-00000800 - U-0000FFFF (count == 2)
- // Range: U-00010000 - U-001FFFFF (count == 3)
- // Range: U-00200000 - U-03FFFFFF (count == 4)
- // Range: U-04000000 - U-7FFFFFFF (count == 5)
-
- if (idx + utfCount > last) {
- v[s++] = REPLACEMENT_CHAR;
+ int b = d[idx++] & 0xff;
+ if (utf8BytesNeeded == 0) {
+ if ((b & 0x80) == 0) { // ASCII char. 0xxxxxxx
+ v[s++] = (char) b;
continue;
}
- // Extract usable bits from b0
- int val = b0 & (0x1f >> (utfCount - 1));
- for (int i = 0; i < utfCount; ++i) {
- byte b = d[idx++];
- if ((b & 0xc0) != 0x80) {
- v[s++] = REPLACEMENT_CHAR;
- idx--; // Put the input char back
- continue outer;
- }
- // Push new bits in from the right side
- val <<= 6;
- val |= b & 0x3f;
+ if ((b & 0x40) == 0) { // 10xxxxxx is illegal as first byte
+ v[s++] = REPLACEMENT_CHAR;
+ continue;
}
- // Note: Java allows overlong char
- // specifications To disallow, check that val
- // is greater than or equal to the minimum
- // value for each count:
- //
- // count min value
- // ----- ----------
- // 1 0x80
- // 2 0x800
- // 3 0x10000
- // 4 0x200000
- // 5 0x4000000
-
- // Allow surrogate values (0xD800 - 0xDFFF) to
- // be specified using 3-byte UTF values only
- if ((utfCount != 2) && (val >= 0xD800) && (val <= 0xDFFF)) {
+ // 11xxxxxx
+ int tableLookupIndex = b & 0x3f;
+ utf8BytesNeeded = TABLE_UTF8_NEEDED[tableLookupIndex];
+ if (utf8BytesNeeded == 0) {
v[s++] = REPLACEMENT_CHAR;
continue;
}
- // Reject chars greater than the Unicode maximum of U+10FFFF.
- if (val > 0x10FFFF) {
+ // utf8BytesNeeded
+ // 1: b & 0x1f
+ // 2: b & 0x0f
+ // 3: b & 0x07
+ codePoint = b & (0x3f >> utf8BytesNeeded);
+ if (b == 0xe0) {
+ lowerBound = 0xa0;
+ } else if (b == 0xed) {
+ upperBound = 0x9f;
+ } else if (b == 0xf0) {
+ lowerBound = 0x90;
+ } else if (b == 0xf4) {
+ upperBound = 0x8f;
+ }
+ } else {
+ if (b < lowerBound || b > upperBound) {
+ // The bytes seen are ill-formed. Substitute them with U+FFFD
v[s++] = REPLACEMENT_CHAR;
+ codePoint = 0;
+ utf8BytesNeeded = 0;
+ utf8BytesSeen = 0;
+ lowerBound = 0x80;
+ upperBound = 0xbf;
+ /*
+ * According to the Unicode Standard,
+ * "a UTF-8 conversion process is required to never consume well-formed
+ * subsequences as part of its error handling for ill-formed subsequences"
+ * The current byte could be part of well-formed subsequences. Reduce the
+ * index by 1 to parse it in next loop.
+ */
+ idx--;
+ continue;
+ }
+
+ lowerBound = 0x80;
+ upperBound = 0xbf;
+ codePoint = (codePoint << 6) | (b & 0x3f);
+ utf8BytesSeen++;
+ if (utf8BytesNeeded != utf8BytesSeen) {
continue;
}
// Encode chars from U+10000 up as surrogate pairs
- if (val < 0x10000) {
- v[s++] = (char) val;
+ if (codePoint < 0x10000) {
+ v[s++] = (char) codePoint;
} else {
- int x = val & 0xffff;
- int u = (val >> 16) & 0x1f;
- int w = (u - 1) & 0xffff;
- int hi = 0xd800 | (w << 6) | (x >> 10);
- int lo = 0xdc00 | (x & 0x3ff);
- v[s++] = (char) hi;
- v[s++] = (char) lo;
+ v[s++] = (char) ((codePoint >> 10) + 0xd7c0);
+ v[s++] = (char) ((codePoint & 0x3ff) + 0xdc00);
}
- } else {
- // Illegal values 0x8*, 0x9*, 0xa*, 0xb*, 0xfd-0xff
- v[s++] = REPLACEMENT_CHAR;
+
+ utf8BytesSeen = 0;
+ utf8BytesNeeded = 0;
+ codePoint = 0;
}
}
+ // The bytes seen are ill-formed. Substitute them by U+FFFD
+ if (utf8BytesNeeded != 0) {
+ v[s++] = REPLACEMENT_CHAR;
+ }
+
if (s == byteCount) {
// We guessed right, so we can use our temporary array as-is.
value = v;
diff --git a/java/lang/Thread.java b/java/lang/Thread.java
index 0419edee..d4620337 100644
--- a/java/lang/Thread.java
+++ b/java/lang/Thread.java
@@ -1941,7 +1941,7 @@ class Thread implements Runnable {
*
* @hide
*/
- // @VisibleForTesting (would be private if not for tests)
+ // @VisibleForTesting (would be package-private if not for tests)
public final void dispatchUncaughtException(Throwable e) {
Thread.UncaughtExceptionHandler initialUeh =
Thread.getUncaughtExceptionPreHandler();
diff --git a/java/lang/invoke/CallSite.java b/java/lang/invoke/CallSite.java
index 85b4bb9f..1ff1eb84 100644
--- a/java/lang/invoke/CallSite.java
+++ b/java/lang/invoke/CallSite.java
@@ -25,363 +25,15 @@
package java.lang.invoke;
-// Android-changed: Not using Empty
-//import sun.invoke.empty.Empty;
-import static java.lang.invoke.MethodHandleStatics.*;
-import static java.lang.invoke.MethodHandles.Lookup.IMPL_LOOKUP;
-
-/**
- * A {@code CallSite} is a holder for a variable {@link MethodHandle},
- * which is called its {@code target}.
- * An {@code invokedynamic} instruction linked to a {@code CallSite} delegates
- * all calls to the site's current target.
- * A {@code CallSite} may be associated with several {@code invokedynamic}
- * instructions, or it may be "free floating", associated with none.
- * In any case, it may be invoked through an associated method handle
- * called its {@linkplain #dynamicInvoker dynamic invoker}.
- * <p>
- * {@code CallSite} is an abstract class which does not allow
- * direct subclassing by users. It has three immediate,
- * concrete subclasses that may be either instantiated or subclassed.
- * <ul>
- * <li>If a mutable target is not required, an {@code invokedynamic} instruction
- * may be permanently bound by means of a {@linkplain ConstantCallSite constant call site}.
- * <li>If a mutable target is required which has volatile variable semantics,
- * because updates to the target must be immediately and reliably witnessed by other threads,
- * a {@linkplain VolatileCallSite volatile call site} may be used.
- * <li>Otherwise, if a mutable target is required,
- * a {@linkplain MutableCallSite mutable call site} may be used.
- * </ul>
- * <p>
- * A non-constant call site may be <em>relinked</em> by changing its target.
- * The new target must have the same {@linkplain MethodHandle#type() type}
- * as the previous target.
- * Thus, though a call site can be relinked to a series of
- * successive targets, it cannot change its type.
- * <p>
- * Here is a sample use of call sites and bootstrap methods which links every
- * dynamic call site to print its arguments:
-<blockquote><pre>{@code
-static void test() throws Throwable {
- // THE FOLLOWING LINE IS PSEUDOCODE FOR A JVM INSTRUCTION
- InvokeDynamic[#bootstrapDynamic].baz("baz arg", 2, 3.14);
-}
-private static void printArgs(Object... args) {
- System.out.println(java.util.Arrays.deepToString(args));
-}
-private static final MethodHandle printArgs;
-static {
- MethodHandles.Lookup lookup = MethodHandles.lookup();
- Class thisClass = lookup.lookupClass(); // (who am I?)
- printArgs = lookup.findStatic(thisClass,
- "printArgs", MethodType.methodType(void.class, Object[].class));
-}
-private static CallSite bootstrapDynamic(MethodHandles.Lookup caller, String name, MethodType type) {
- // ignore caller and name, but match the type:
- return new ConstantCallSite(printArgs.asType(type));
-}
-}</pre></blockquote>
- * @author John Rose, JSR 292 EG
- */
abstract
public class CallSite {
- // Android-changed: not used.
- // static { MethodHandleImpl.initStatics(); }
-
- // The actual payload of this call site:
- /*package-private*/
- MethodHandle target; // Note: This field is known to the JVM. Do not change.
-
- /**
- * Make a blank call site object with the given method type.
- * An initial target method is supplied which will throw
- * an {@link IllegalStateException} if called.
- * <p>
- * Before this {@code CallSite} object is returned from a bootstrap method,
- * it is usually provided with a more useful target method,
- * via a call to {@link CallSite#setTarget(MethodHandle) setTarget}.
- * @throws NullPointerException if the proposed type is null
- */
- /*package-private*/
- CallSite(MethodType type) {
- // Android-changed: No cache for these so create uninitializedCallSite target here using
- // method handle transformations to create a method handle that has the expected method
- // type but throws an IllegalStateException.
- // target = makeUninitializedCallSite(type);
- this.target = MethodHandles.throwException(type.returnType(), IllegalStateException.class);
- this.target = MethodHandles.insertArguments(
- this.target, 0, new IllegalStateException("uninitialized call site"));
- if (type.parameterCount() > 0) {
- this.target = MethodHandles.dropArguments(this.target, 0, type.ptypes());
- }
-
- // Android-changed: Using initializer method for GET_TARGET
- // rather than complex static initializer.
- initializeGetTarget();
- }
-
- /**
- * Make a call site object equipped with an initial target method handle.
- * @param target the method handle which will be the initial target of the call site
- * @throws NullPointerException if the proposed target is null
- */
- /*package-private*/
- CallSite(MethodHandle target) {
- target.type(); // null check
- this.target = target;
-
- // Android-changed: Using initializer method for GET_TARGET
- // rather than complex static initializer.
- initializeGetTarget();
- }
- /**
- * Make a call site object equipped with an initial target method handle.
- * @param targetType the desired type of the call site
- * @param createTargetHook a hook which will bind the call site to the target method handle
- * @throws WrongMethodTypeException if the hook cannot be invoked on the required arguments,
- * or if the target returned by the hook is not of the given {@code targetType}
- * @throws NullPointerException if the hook returns a null value
- * @throws ClassCastException if the hook returns something other than a {@code MethodHandle}
- * @throws Throwable anything else thrown by the hook function
- */
- /*package-private*/
- CallSite(MethodType targetType, MethodHandle createTargetHook) throws Throwable {
- this(targetType);
- ConstantCallSite selfCCS = (ConstantCallSite) this;
- MethodHandle boundTarget = (MethodHandle) createTargetHook.invokeWithArguments(selfCCS);
- checkTargetChange(this.target, boundTarget);
- this.target = boundTarget;
+ public MethodType type() { return null; }
- // Android-changed: Using initializer method for GET_TARGET
- // rather than complex static initializer.
- initializeGetTarget();
- }
-
- /**
- * Returns the type of this call site's target.
- * Although targets may change, any call site's type is permanent, and can never change to an unequal type.
- * The {@code setTarget} method enforces this invariant by refusing any new target that does
- * not have the previous target's type.
- * @return the type of the current target, which is also the type of any future target
- */
- public MethodType type() {
- // warning: do not call getTarget here, because CCS.getTarget can throw IllegalStateException
- return target.type();
- }
-
- /**
- * Returns the target method of the call site, according to the
- * behavior defined by this call site's specific class.
- * The immediate subclasses of {@code CallSite} document the
- * class-specific behaviors of this method.
- *
- * @return the current linkage state of the call site, its target method handle
- * @see ConstantCallSite
- * @see VolatileCallSite
- * @see #setTarget
- * @see ConstantCallSite#getTarget
- * @see MutableCallSite#getTarget
- * @see VolatileCallSite#getTarget
- */
public abstract MethodHandle getTarget();
- /**
- * Updates the target method of this call site, according to the
- * behavior defined by this call site's specific class.
- * The immediate subclasses of {@code CallSite} document the
- * class-specific behaviors of this method.
- * <p>
- * The type of the new target must be {@linkplain MethodType#equals equal to}
- * the type of the old target.
- *
- * @param newTarget the new target
- * @throws NullPointerException if the proposed new target is null
- * @throws WrongMethodTypeException if the proposed new target
- * has a method type that differs from the previous target
- * @see CallSite#getTarget
- * @see ConstantCallSite#setTarget
- * @see MutableCallSite#setTarget
- * @see VolatileCallSite#setTarget
- */
public abstract void setTarget(MethodHandle newTarget);
- void checkTargetChange(MethodHandle oldTarget, MethodHandle newTarget) {
- MethodType oldType = oldTarget.type();
- MethodType newType = newTarget.type(); // null check!
- if (!newType.equals(oldType))
- throw wrongTargetType(newTarget, oldType);
- }
-
- private static WrongMethodTypeException wrongTargetType(MethodHandle target, MethodType type) {
- return new WrongMethodTypeException(String.valueOf(target)+" should be of type "+type);
- }
-
- /**
- * Produces a method handle equivalent to an invokedynamic instruction
- * which has been linked to this call site.
- * <p>
- * This method is equivalent to the following code:
- * <blockquote><pre>{@code
- * MethodHandle getTarget, invoker, result;
- * getTarget = MethodHandles.publicLookup().bind(this, "getTarget", MethodType.methodType(MethodHandle.class));
- * invoker = MethodHandles.exactInvoker(this.type());
- * result = MethodHandles.foldArguments(invoker, getTarget)
- * }</pre></blockquote>
- *
- * @return a method handle which always invokes this call site's current target
- */
public abstract MethodHandle dynamicInvoker();
- /*non-public*/ MethodHandle makeDynamicInvoker() {
- // Android-changed: Use bindTo() rather than bindArgumentL() (not implemented).
- MethodHandle getTarget = GET_TARGET.bindTo(this);
- MethodHandle invoker = MethodHandles.exactInvoker(this.type());
- return MethodHandles.foldArguments(invoker, getTarget);
- }
-
- // Android-changed: no longer final. GET_TARGET assigned in initializeGetTarget().
- private static MethodHandle GET_TARGET = null;
-
- private void initializeGetTarget() {
- // Android-changed: moved from static initializer for
- // GET_TARGET to avoid issues with running early. Called from
- // constructors. CallSite creation is not performance critical.
- synchronized (CallSite.class) {
- if (GET_TARGET == null) {
- try {
- GET_TARGET = IMPL_LOOKUP.
- findVirtual(CallSite.class, "getTarget",
- MethodType.methodType(MethodHandle.class));
- } catch (ReflectiveOperationException e) {
- throw new InternalError(e);
- }
- }
- }
- }
-
- // Android-changed: not used.
- // /** This guy is rolled into the default target if a MethodType is supplied to the constructor. */
- // /*package-private*/
- // static Empty uninitializedCallSite() {
- // throw new IllegalStateException("uninitialized call site");
- // }
-
- // unsafe stuff:
- private static final long TARGET_OFFSET;
- static {
- try {
- TARGET_OFFSET = UNSAFE.objectFieldOffset(CallSite.class.getDeclaredField("target"));
- } catch (Exception ex) { throw new Error(ex); }
- }
-
- /*package-private*/
- void setTargetNormal(MethodHandle newTarget) {
- // Android-changed: Set value directly.
- // MethodHandleNatives.setCallSiteTargetNormal(this, newTarget);
- target = newTarget;
- }
- /*package-private*/
- MethodHandle getTargetVolatile() {
- return (MethodHandle) UNSAFE.getObjectVolatile(this, TARGET_OFFSET);
- }
- /*package-private*/
- void setTargetVolatile(MethodHandle newTarget) {
- // Android-changed: Set value directly.
- // MethodHandleNatives.setCallSiteTargetVolatile(this, newTarget);
- UNSAFE.putObjectVolatile(this, TARGET_OFFSET, newTarget);
- }
-
- // Android-changed: not used.
- // this implements the upcall from the JVM, MethodHandleNatives.makeDynamicCallSite:
- // static CallSite makeSite(MethodHandle bootstrapMethod,
- // // Callee information:
- // String name, MethodType type,
- // // Extra arguments for BSM, if any:
- // Object info,
- // // Caller information:
- // Class<?> callerClass) {
- // MethodHandles.Lookup caller = IMPL_LOOKUP.in(callerClass);
- // CallSite site;
- // try {
- // Object binding;
- // info = maybeReBox(info);
- // if (info == null) {
- // binding = bootstrapMethod.invoke(caller, name, type);
- // } else if (!info.getClass().isArray()) {
- // binding = bootstrapMethod.invoke(caller, name, type, info);
- // } else {
- // Object[] argv = (Object[]) info;
- // maybeReBoxElements(argv);
- // switch (argv.length) {
- // case 0:
- // binding = bootstrapMethod.invoke(caller, name, type);
- // break;
- // case 1:
- // binding = bootstrapMethod.invoke(caller, name, type,
- // argv[0]);
- // break;
- // case 2:
- // binding = bootstrapMethod.invoke(caller, name, type,
- // argv[0], argv[1]);
- // break;
- // case 3:
- // binding = bootstrapMethod.invoke(caller, name, type,
- // argv[0], argv[1], argv[2]);
- // break;
- // case 4:
- // binding = bootstrapMethod.invoke(caller, name, type,
- // argv[0], argv[1], argv[2], argv[3]);
- // break;
- // case 5:
- // binding = bootstrapMethod.invoke(caller, name, type,
- // argv[0], argv[1], argv[2], argv[3], argv[4]);
- // break;
- // case 6:
- // binding = bootstrapMethod.invoke(caller, name, type,
- // argv[0], argv[1], argv[2], argv[3], argv[4], argv[5]);
- // break;
- // default:
- // final int NON_SPREAD_ARG_COUNT = 3; // (caller, name, type)
- // if (NON_SPREAD_ARG_COUNT + argv.length > MethodType.MAX_MH_ARITY)
- // throw new BootstrapMethodError("too many bootstrap method arguments");
- // MethodType bsmType = bootstrapMethod.type();
- // MethodType invocationType = MethodType.genericMethodType(NON_SPREAD_ARG_COUNT + argv.length);
- // MethodHandle typedBSM = bootstrapMethod.asType(invocationType);
- // MethodHandle spreader = invocationType.invokers().spreadInvoker(NON_SPREAD_ARG_COUNT);
- // binding = spreader.invokeExact(typedBSM, (Object)caller, (Object)name, (Object)type, argv);
- // }
- // }
- // //System.out.println("BSM for "+name+type+" => "+binding);
- // if (binding instanceof CallSite) {
- // site = (CallSite) binding;
- // } else {
- // throw new ClassCastException("bootstrap method failed to produce a CallSite");
- // }
- // if (!site.getTarget().type().equals(type))
- // throw wrongTargetType(site.getTarget(), type);
- // } catch (Throwable ex) {
- // BootstrapMethodError bex;
- // if (ex instanceof BootstrapMethodError)
- // bex = (BootstrapMethodError) ex;
- // else
- // bex = new BootstrapMethodError("call site initialization exception", ex);
- // throw bex;
- // }
- // return site;
- // }
-
- // private static Object maybeReBox(Object x) {
- // if (x instanceof Integer) {
- // int xi = (int) x;
- // if (xi == (byte) xi)
- // x = xi; // must rebox; see JLS 5.1.7
- // }
- // return x;
- // }
- // private static void maybeReBoxElements(Object[] xa) {
- // for (int i = 0; i < xa.length; i++) {
- // xa[i] = maybeReBox(xa[i]);
- // }
- // }
}
diff --git a/java/lang/invoke/MethodHandle.java b/java/lang/invoke/MethodHandle.java
index af3db103..159f9dd7 100644
--- a/java/lang/invoke/MethodHandle.java
+++ b/java/lang/invoke/MethodHandle.java
@@ -25,1353 +25,28 @@
package java.lang.invoke;
-
-import dalvik.system.EmulatedStackFrame;
-
-import static java.lang.invoke.MethodHandleStatics.*;
-
-/**
- * A method handle is a typed, directly executable reference to an underlying method,
- * constructor, field, or similar low-level operation, with optional
- * transformations of arguments or return values.
- * These transformations are quite general, and include such patterns as
- * {@linkplain #asType conversion},
- * {@linkplain #bindTo insertion},
- * {@linkplain java.lang.invoke.MethodHandles#dropArguments deletion},
- * and {@linkplain java.lang.invoke.MethodHandles#filterArguments substitution}.
- *
- * <h1>Method handle contents</h1>
- * Method handles are dynamically and strongly typed according to their parameter and return types.
- * They are not distinguished by the name or the defining class of their underlying methods.
- * A method handle must be invoked using a symbolic type descriptor which matches
- * the method handle's own {@linkplain #type type descriptor}.
- * <p>
- * Every method handle reports its type descriptor via the {@link #type type} accessor.
- * This type descriptor is a {@link java.lang.invoke.MethodType MethodType} object,
- * whose structure is a series of classes, one of which is
- * the return type of the method (or {@code void.class} if none).
- * <p>
- * A method handle's type controls the types of invocations it accepts,
- * and the kinds of transformations that apply to it.
- * <p>
- * A method handle contains a pair of special invoker methods
- * called {@link #invokeExact invokeExact} and {@link #invoke invoke}.
- * Both invoker methods provide direct access to the method handle's
- * underlying method, constructor, field, or other operation,
- * as modified by transformations of arguments and return values.
- * Both invokers accept calls which exactly match the method handle's own type.
- * The plain, inexact invoker also accepts a range of other call types.
- * <p>
- * Method handles are immutable and have no visible state.
- * Of course, they can be bound to underlying methods or data which exhibit state.
- * With respect to the Java Memory Model, any method handle will behave
- * as if all of its (internal) fields are final variables. This means that any method
- * handle made visible to the application will always be fully formed.
- * This is true even if the method handle is published through a shared
- * variable in a data race.
- * <p>
- * Method handles cannot be subclassed by the user.
- * Implementations may (or may not) create internal subclasses of {@code MethodHandle}
- * which may be visible via the {@link java.lang.Object#getClass Object.getClass}
- * operation. The programmer should not draw conclusions about a method handle
- * from its specific class, as the method handle class hierarchy (if any)
- * may change from time to time or across implementations from different vendors.
- *
- * <h1>Method handle compilation</h1>
- * A Java method call expression naming {@code invokeExact} or {@code invoke}
- * can invoke a method handle from Java source code.
- * From the viewpoint of source code, these methods can take any arguments
- * and their result can be cast to any return type.
- * Formally this is accomplished by giving the invoker methods
- * {@code Object} return types and variable arity {@code Object} arguments,
- * but they have an additional quality called <em>signature polymorphism</em>
- * which connects this freedom of invocation directly to the JVM execution stack.
- * <p>
- * As is usual with virtual methods, source-level calls to {@code invokeExact}
- * and {@code invoke} compile to an {@code invokevirtual} instruction.
- * More unusually, the compiler must record the actual argument types,
- * and may not perform method invocation conversions on the arguments.
- * Instead, it must push them on the stack according to their own unconverted types.
- * The method handle object itself is pushed on the stack before the arguments.
- * The compiler then calls the method handle with a symbolic type descriptor which
- * describes the argument and return types.
- * <p>
- * To issue a complete symbolic type descriptor, the compiler must also determine
- * the return type. This is based on a cast on the method invocation expression,
- * if there is one, or else {@code Object} if the invocation is an expression
- * or else {@code void} if the invocation is a statement.
- * The cast may be to a primitive type (but not {@code void}).
- * <p>
- * As a corner case, an uncasted {@code null} argument is given
- * a symbolic type descriptor of {@code java.lang.Void}.
- * The ambiguity with the type {@code Void} is harmless, since there are no references of type
- * {@code Void} except the null reference.
- *
- * <h1>Method handle invocation</h1>
- * The first time a {@code invokevirtual} instruction is executed
- * it is linked, by symbolically resolving the names in the instruction
- * and verifying that the method call is statically legal.
- * This is true of calls to {@code invokeExact} and {@code invoke}.
- * In this case, the symbolic type descriptor emitted by the compiler is checked for
- * correct syntax and names it contains are resolved.
- * Thus, an {@code invokevirtual} instruction which invokes
- * a method handle will always link, as long
- * as the symbolic type descriptor is syntactically well-formed
- * and the types exist.
- * <p>
- * When the {@code invokevirtual} is executed after linking,
- * the receiving method handle's type is first checked by the JVM
- * to ensure that it matches the symbolic type descriptor.
- * If the type match fails, it means that the method which the
- * caller is invoking is not present on the individual
- * method handle being invoked.
- * <p>
- * In the case of {@code invokeExact}, the type descriptor of the invocation
- * (after resolving symbolic type names) must exactly match the method type
- * of the receiving method handle.
- * In the case of plain, inexact {@code invoke}, the resolved type descriptor
- * must be a valid argument to the receiver's {@link #asType asType} method.
- * Thus, plain {@code invoke} is more permissive than {@code invokeExact}.
- * <p>
- * After type matching, a call to {@code invokeExact} directly
- * and immediately invoke the method handle's underlying method
- * (or other behavior, as the case may be).
- * <p>
- * A call to plain {@code invoke} works the same as a call to
- * {@code invokeExact}, if the symbolic type descriptor specified by the caller
- * exactly matches the method handle's own type.
- * If there is a type mismatch, {@code invoke} attempts
- * to adjust the type of the receiving method handle,
- * as if by a call to {@link #asType asType},
- * to obtain an exactly invokable method handle {@code M2}.
- * This allows a more powerful negotiation of method type
- * between caller and callee.
- * <p>
- * (<em>Note:</em> The adjusted method handle {@code M2} is not directly observable,
- * and implementations are therefore not required to materialize it.)
- *
- * <h1>Invocation checking</h1>
- * In typical programs, method handle type matching will usually succeed.
- * But if a match fails, the JVM will throw a {@link WrongMethodTypeException},
- * either directly (in the case of {@code invokeExact}) or indirectly as if
- * by a failed call to {@code asType} (in the case of {@code invoke}).
- * <p>
- * Thus, a method type mismatch which might show up as a linkage error
- * in a statically typed program can show up as
- * a dynamic {@code WrongMethodTypeException}
- * in a program which uses method handles.
- * <p>
- * Because method types contain "live" {@code Class} objects,
- * method type matching takes into account both types names and class loaders.
- * Thus, even if a method handle {@code M} is created in one
- * class loader {@code L1} and used in another {@code L2},
- * method handle calls are type-safe, because the caller's symbolic type
- * descriptor, as resolved in {@code L2},
- * is matched against the original callee method's symbolic type descriptor,
- * as resolved in {@code L1}.
- * The resolution in {@code L1} happens when {@code M} is created
- * and its type is assigned, while the resolution in {@code L2} happens
- * when the {@code invokevirtual} instruction is linked.
- * <p>
- * Apart from the checking of type descriptors,
- * a method handle's capability to call its underlying method is unrestricted.
- * If a method handle is formed on a non-public method by a class
- * that has access to that method, the resulting handle can be used
- * in any place by any caller who receives a reference to it.
- * <p>
- * Unlike with the Core Reflection API, where access is checked every time
- * a reflective method is invoked,
- * method handle access checking is performed
- * <a href="MethodHandles.Lookup.html#access">when the method handle is created</a>.
- * In the case of {@code ldc} (see below), access checking is performed as part of linking
- * the constant pool entry underlying the constant method handle.
- * <p>
- * Thus, handles to non-public methods, or to methods in non-public classes,
- * should generally be kept secret.
- * They should not be passed to untrusted code unless their use from
- * the untrusted code would be harmless.
- *
- * <h1>Method handle creation</h1>
- * Java code can create a method handle that directly accesses
- * any method, constructor, or field that is accessible to that code.
- * This is done via a reflective, capability-based API called
- * {@link java.lang.invoke.MethodHandles.Lookup MethodHandles.Lookup}
- * For example, a static method handle can be obtained
- * from {@link java.lang.invoke.MethodHandles.Lookup#findStatic Lookup.findStatic}.
- * There are also conversion methods from Core Reflection API objects,
- * such as {@link java.lang.invoke.MethodHandles.Lookup#unreflect Lookup.unreflect}.
- * <p>
- * Like classes and strings, method handles that correspond to accessible
- * fields, methods, and constructors can also be represented directly
- * in a class file's constant pool as constants to be loaded by {@code ldc} bytecodes.
- * A new type of constant pool entry, {@code CONSTANT_MethodHandle},
- * refers directly to an associated {@code CONSTANT_Methodref},
- * {@code CONSTANT_InterfaceMethodref}, or {@code CONSTANT_Fieldref}
- * constant pool entry.
- * (For full details on method handle constants,
- * see sections 4.4.8 and 5.4.3.5 of the Java Virtual Machine Specification.)
- * <p>
- * Method handles produced by lookups or constant loads from methods or
- * constructors with the variable arity modifier bit ({@code 0x0080})
- * have a corresponding variable arity, as if they were defined with
- * the help of {@link #asVarargsCollector asVarargsCollector}.
- * <p>
- * A method reference may refer either to a static or non-static method.
- * In the non-static case, the method handle type includes an explicit
- * receiver argument, prepended before any other arguments.
- * In the method handle's type, the initial receiver argument is typed
- * according to the class under which the method was initially requested.
- * (E.g., if a non-static method handle is obtained via {@code ldc},
- * the type of the receiver is the class named in the constant pool entry.)
- * <p>
- * Method handle constants are subject to the same link-time access checks
- * their corresponding bytecode instructions, and the {@code ldc} instruction
- * will throw corresponding linkage errors if the bytecode behaviors would
- * throw such errors.
- * <p>
- * As a corollary of this, access to protected members is restricted
- * to receivers only of the accessing class, or one of its subclasses,
- * and the accessing class must in turn be a subclass (or package sibling)
- * of the protected member's defining class.
- * If a method reference refers to a protected non-static method or field
- * of a class outside the current package, the receiver argument will
- * be narrowed to the type of the accessing class.
- * <p>
- * When a method handle to a virtual method is invoked, the method is
- * always looked up in the receiver (that is, the first argument).
- * <p>
- * A non-virtual method handle to a specific virtual method implementation
- * can also be created. These do not perform virtual lookup based on
- * receiver type. Such a method handle simulates the effect of
- * an {@code invokespecial} instruction to the same method.
- *
- * <h1>Usage examples</h1>
- * Here are some examples of usage:
- * <blockquote><pre>{@code
-Object x, y; String s; int i;
-MethodType mt; MethodHandle mh;
-MethodHandles.Lookup lookup = MethodHandles.lookup();
-// mt is (char,char)String
-mt = MethodType.methodType(String.class, char.class, char.class);
-mh = lookup.findVirtual(String.class, "replace", mt);
-s = (String) mh.invokeExact("daddy",'d','n');
-// invokeExact(Ljava/lang/String;CC)Ljava/lang/String;
-assertEquals(s, "nanny");
-// weakly typed invocation (using MHs.invoke)
-s = (String) mh.invokeWithArguments("sappy", 'p', 'v');
-assertEquals(s, "savvy");
-// mt is (Object[])List
-mt = MethodType.methodType(java.util.List.class, Object[].class);
-mh = lookup.findStatic(java.util.Arrays.class, "asList", mt);
-assert(mh.isVarargsCollector());
-x = mh.invoke("one", "two");
-// invoke(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Object;
-assertEquals(x, java.util.Arrays.asList("one","two"));
-// mt is (Object,Object,Object)Object
-mt = MethodType.genericMethodType(3);
-mh = mh.asType(mt);
-x = mh.invokeExact((Object)1, (Object)2, (Object)3);
-// invokeExact(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
-assertEquals(x, java.util.Arrays.asList(1,2,3));
-// mt is ()int
-mt = MethodType.methodType(int.class);
-mh = lookup.findVirtual(java.util.List.class, "size", mt);
-i = (int) mh.invokeExact(java.util.Arrays.asList(1,2,3));
-// invokeExact(Ljava/util/List;)I
-assert(i == 3);
-mt = MethodType.methodType(void.class, String.class);
-mh = lookup.findVirtual(java.io.PrintStream.class, "println", mt);
-mh.invokeExact(System.out, "Hello, world.");
-// invokeExact(Ljava/io/PrintStream;Ljava/lang/String;)V
- * }</pre></blockquote>
- * Each of the above calls to {@code invokeExact} or plain {@code invoke}
- * generates a single invokevirtual instruction with
- * the symbolic type descriptor indicated in the following comment.
- * In these examples, the helper method {@code assertEquals} is assumed to
- * be a method which calls {@link java.util.Objects#equals(Object,Object) Objects.equals}
- * on its arguments, and asserts that the result is true.
- *
- * <h1>Exceptions</h1>
- * The methods {@code invokeExact} and {@code invoke} are declared
- * to throw {@link java.lang.Throwable Throwable},
- * which is to say that there is no static restriction on what a method handle
- * can throw. Since the JVM does not distinguish between checked
- * and unchecked exceptions (other than by their class, of course),
- * there is no particular effect on bytecode shape from ascribing
- * checked exceptions to method handle invocations. But in Java source
- * code, methods which perform method handle calls must either explicitly
- * throw {@code Throwable}, or else must catch all
- * throwables locally, rethrowing only those which are legal in the context,
- * and wrapping ones which are illegal.
- *
- * <h1><a name="sigpoly"></a>Signature polymorphism</h1>
- * The unusual compilation and linkage behavior of
- * {@code invokeExact} and plain {@code invoke}
- * is referenced by the term <em>signature polymorphism</em>.
- * As defined in the Java Language Specification,
- * a signature polymorphic method is one which can operate with
- * any of a wide range of call signatures and return types.
- * <p>
- * In source code, a call to a signature polymorphic method will
- * compile, regardless of the requested symbolic type descriptor.
- * As usual, the Java compiler emits an {@code invokevirtual}
- * instruction with the given symbolic type descriptor against the named method.
- * The unusual part is that the symbolic type descriptor is derived from
- * the actual argument and return types, not from the method declaration.
- * <p>
- * When the JVM processes bytecode containing signature polymorphic calls,
- * it will successfully link any such call, regardless of its symbolic type descriptor.
- * (In order to retain type safety, the JVM will guard such calls with suitable
- * dynamic type checks, as described elsewhere.)
- * <p>
- * Bytecode generators, including the compiler back end, are required to emit
- * untransformed symbolic type descriptors for these methods.
- * Tools which determine symbolic linkage are required to accept such
- * untransformed descriptors, without reporting linkage errors.
- *
- * <h1>Interoperation between method handles and the Core Reflection API</h1>
- * Using factory methods in the {@link java.lang.invoke.MethodHandles.Lookup Lookup} API,
- * any class member represented by a Core Reflection API object
- * can be converted to a behaviorally equivalent method handle.
- * For example, a reflective {@link java.lang.reflect.Method Method} can
- * be converted to a method handle using
- * {@link java.lang.invoke.MethodHandles.Lookup#unreflect Lookup.unreflect}.
- * The resulting method handles generally provide more direct and efficient
- * access to the underlying class members.
- * <p>
- * As a special case,
- * when the Core Reflection API is used to view the signature polymorphic
- * methods {@code invokeExact} or plain {@code invoke} in this class,
- * they appear as ordinary non-polymorphic methods.
- * Their reflective appearance, as viewed by
- * {@link java.lang.Class#getDeclaredMethod Class.getDeclaredMethod},
- * is unaffected by their special status in this API.
- * For example, {@link java.lang.reflect.Method#getModifiers Method.getModifiers}
- * will report exactly those modifier bits required for any similarly
- * declared method, including in this case {@code native} and {@code varargs} bits.
- * <p>
- * As with any reflected method, these methods (when reflected) may be
- * invoked via {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}.
- * However, such reflective calls do not result in method handle invocations.
- * Such a call, if passed the required argument
- * (a single one, of type {@code Object[]}), will ignore the argument and
- * will throw an {@code UnsupportedOperationException}.
- * <p>
- * Since {@code invokevirtual} instructions can natively
- * invoke method handles under any symbolic type descriptor, this reflective view conflicts
- * with the normal presentation of these methods via bytecodes.
- * Thus, these two native methods, when reflectively viewed by
- * {@code Class.getDeclaredMethod}, may be regarded as placeholders only.
- * <p>
- * In order to obtain an invoker method for a particular type descriptor,
- * use {@link java.lang.invoke.MethodHandles#exactInvoker MethodHandles.exactInvoker},
- * or {@link java.lang.invoke.MethodHandles#invoker MethodHandles.invoker}.
- * The {@link java.lang.invoke.MethodHandles.Lookup#findVirtual Lookup.findVirtual}
- * API is also able to return a method handle
- * to call {@code invokeExact} or plain {@code invoke},
- * for any specified type descriptor .
- *
- * <h1>Interoperation between method handles and Java generics</h1>
- * A method handle can be obtained on a method, constructor, or field
- * which is declared with Java generic types.
- * As with the Core Reflection API, the type of the method handle
- * will constructed from the erasure of the source-level type.
- * When a method handle is invoked, the types of its arguments
- * or the return value cast type may be generic types or type instances.
- * If this occurs, the compiler will replace those
- * types by their erasures when it constructs the symbolic type descriptor
- * for the {@code invokevirtual} instruction.
- * <p>
- * Method handles do not represent
- * their function-like types in terms of Java parameterized (generic) types,
- * because there are three mismatches between function-like types and parameterized
- * Java types.
- * <ul>
- * <li>Method types range over all possible arities,
- * from no arguments to up to the <a href="MethodHandle.html#maxarity">maximum number</a> of allowed arguments.
- * Generics are not variadic, and so cannot represent this.</li>
- * <li>Method types can specify arguments of primitive types,
- * which Java generic types cannot range over.</li>
- * <li>Higher order functions over method handles (combinators) are
- * often generic across a wide range of function types, including
- * those of multiple arities. It is impossible to represent such
- * genericity with a Java type parameter.</li>
- * </ul>
- *
- * <h1><a name="maxarity"></a>Arity limits</h1>
- * The JVM imposes on all methods and constructors of any kind an absolute
- * limit of 255 stacked arguments. This limit can appear more restrictive
- * in certain cases:
- * <ul>
- * <li>A {@code long} or {@code double} argument counts (for purposes of arity limits) as two argument slots.
- * <li>A non-static method consumes an extra argument for the object on which the method is called.
- * <li>A constructor consumes an extra argument for the object which is being constructed.
- * <li>Since a method handle&rsquo;s {@code invoke} method (or other signature-polymorphic method) is non-virtual,
- * it consumes an extra argument for the method handle itself, in addition to any non-virtual receiver object.
- * </ul>
- * These limits imply that certain method handles cannot be created, solely because of the JVM limit on stacked arguments.
- * For example, if a static JVM method accepts exactly 255 arguments, a method handle cannot be created for it.
- * Attempts to create method handles with impossible method types lead to an {@link IllegalArgumentException}.
- * In particular, a method handle&rsquo;s type must not have an arity of the exact maximum 255.
- *
- * @see MethodType
- * @see MethodHandles
- * @author John Rose, JSR 292 EG
- */
public abstract class MethodHandle {
- // Android-changed:
- //
- // static { MethodHandleImpl.initStatics(); }
- //
- // LambdaForm and customizationCount are currently unused in our implementation
- // and will be substituted with appropriate implementation / delegate classes.
- //
- // /*private*/ final LambdaForm form;
- // form is not private so that invokers can easily fetch it
- // /*non-public*/ byte customizationCount;
- // customizationCount should be accessible from invokers
-
-
- /**
- * Internal marker interface which distinguishes (to the Java compiler)
- * those methods which are <a href="MethodHandle.html#sigpoly">signature polymorphic</a>.
- *
- * @hide
- */
- @java.lang.annotation.Target({java.lang.annotation.ElementType.METHOD})
- @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
- public @interface PolymorphicSignature { }
-
- /**
- * The type of this method handle, this corresponds to the exact type of the method
- * being invoked.
- */
- private final MethodType type;
-
- /**
- * The nominal type of this method handle, will be non-null if a method handle declares
- * a different type from its "real" type, which is either the type of the method being invoked
- * or the type of the emulated stackframe expected by an underyling adapter.
- */
- private MethodType nominalType;
-
- /**
- * The spread invoker associated with this type with zero trailing arguments.
- * This is used to speed up invokeWithArguments.
- */
- private MethodHandle cachedSpreadInvoker;
-
- /**
- * The INVOKE* constants and SGET/SPUT and IGET/IPUT constants specify the behaviour of this
- * method handle with respect to the ArtField* or the ArtMethod* that it operates on. These
- * behaviours are equivalent to the dex bytecode behaviour on the respective method_id or
- * field_id in the equivalent instruction.
- *
- * INVOKE_TRANSFORM is a special type of handle which doesn't encode any dex bytecode behaviour,
- * instead it transforms the list of input arguments or performs other higher order operations
- * before (optionally) delegating to another method handle.
- *
- * INVOKE_CALLSITE_TRANSFORM is a variation on INVOKE_TRANSFORM where the method type of
- * a MethodHandle dynamically varies based on the callsite. This is used by
- * the VarargsCollector implementation which places any number of trailing arguments
- * into an array before invoking an arity method. The "any number of trailing arguments" means
- * it would otherwise generate WrongMethodTypeExceptions as the callsite method type and
- * VarargsCollector method type appear incompatible.
- */
-
- /** @hide */ public static final int INVOKE_VIRTUAL = 0;
- /** @hide */ public static final int INVOKE_SUPER = 1;
- /** @hide */ public static final int INVOKE_DIRECT = 2;
- /** @hide */ public static final int INVOKE_STATIC = 3;
- /** @hide */ public static final int INVOKE_INTERFACE = 4;
- /** @hide */ public static final int INVOKE_TRANSFORM = 5;
- /** @hide */ public static final int INVOKE_CALLSITE_TRANSFORM = 6;
- /** @hide */ public static final int IGET = 7;
- /** @hide */ public static final int IPUT = 8;
- /** @hide */ public static final int SGET = 9;
- /** @hide */ public static final int SPUT = 10;
-
- // The kind of this method handle (used by the runtime). This is one of the INVOKE_*
- // constants or SGET/SPUT, IGET/IPUT.
- /** @hide */ protected final int handleKind;
-
- // The ArtMethod* or ArtField* associated with this method handle (used by the runtime).
- /** @hide */ protected final long artFieldOrMethod;
-
- /** @hide */
- protected MethodHandle(long artFieldOrMethod, int handleKind, MethodType type) {
- this.artFieldOrMethod = artFieldOrMethod;
- this.handleKind = handleKind;
- this.type = type;
- }
-
- /**
- * Reports the type of this method handle.
- * Every invocation of this method handle via {@code invokeExact} must exactly match this type.
- * @return the method handle type
- */
- public MethodType type() {
- if (nominalType != null) {
- return nominalType;
- }
-
- return type;
- }
-
- /**
- * Invokes the method handle, allowing any caller type descriptor, but requiring an exact type match.
- * The symbolic type descriptor at the call site of {@code invokeExact} must
- * exactly match this method handle's {@link #type type}.
- * No conversions are allowed on arguments or return values.
- * <p>
- * When this method is observed via the Core Reflection API,
- * it will appear as a single native method, taking an object array and returning an object.
- * If this native method is invoked directly via
- * {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}, via JNI,
- * or indirectly via {@link java.lang.invoke.MethodHandles.Lookup#unreflect Lookup.unreflect},
- * it will throw an {@code UnsupportedOperationException}.
- * @param args the signature-polymorphic parameter list, statically represented using varargs
- * @return the signature-polymorphic result, statically represented using {@code Object}
- * @throws WrongMethodTypeException if the target's type is not identical with the caller's symbolic type descriptor
- * @throws Throwable anything thrown by the underlying method propagates unchanged through the method handle call
- */
- public final native @PolymorphicSignature Object invokeExact(Object... args) throws Throwable;
-
- /**
- * Invokes the method handle, allowing any caller type descriptor,
- * and optionally performing conversions on arguments and return values.
- * <p>
- * If the call site's symbolic type descriptor exactly matches this method handle's {@link #type type},
- * the call proceeds as if by {@link #invokeExact invokeExact}.
- * <p>
- * Otherwise, the call proceeds as if this method handle were first
- * adjusted by calling {@link #asType asType} to adjust this method handle
- * to the required type, and then the call proceeds as if by
- * {@link #invokeExact invokeExact} on the adjusted method handle.
- * <p>
- * There is no guarantee that the {@code asType} call is actually made.
- * If the JVM can predict the results of making the call, it may perform
- * adaptations directly on the caller's arguments,
- * and call the target method handle according to its own exact type.
- * <p>
- * The resolved type descriptor at the call site of {@code invoke} must
- * be a valid argument to the receivers {@code asType} method.
- * In particular, the caller must specify the same argument arity
- * as the callee's type,
- * if the callee is not a {@linkplain #asVarargsCollector variable arity collector}.
- * <p>
- * When this method is observed via the Core Reflection API,
- * it will appear as a single native method, taking an object array and returning an object.
- * If this native method is invoked directly via
- * {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}, via JNI,
- * or indirectly via {@link java.lang.invoke.MethodHandles.Lookup#unreflect Lookup.unreflect},
- * it will throw an {@code UnsupportedOperationException}.
- * @param args the signature-polymorphic parameter list, statically represented using varargs
- * @return the signature-polymorphic result, statically represented using {@code Object}
- * @throws WrongMethodTypeException if the target's type cannot be adjusted to the caller's symbolic type descriptor
- * @throws ClassCastException if the target's type can be adjusted to the caller, but a reference cast fails
- * @throws Throwable anything thrown by the underlying method propagates unchanged through the method handle call
- */
- public final native @PolymorphicSignature Object invoke(Object... args) throws Throwable;
-
- // Android-changed: Removed implementation details.
- //
- // /*non-public*/ final native @PolymorphicSignature Object invokeBasic(Object... args)
- // /*non-public*/ static native @PolymorphicSignature Object linkToVirtual(Object... args)
- // /*non-public*/ static native @PolymorphicSignature Object linkToStatic(Object... args)
- // /*non-public*/ static native @PolymorphicSignature Object linkToSpecial(Object... args)
- // /*non-public*/ static native @PolymorphicSignature Object linkToInterface(Object... args)
-
- /**
- * Performs a variable arity invocation, passing the arguments in the given list
- * to the method handle, as if via an inexact {@link #invoke invoke} from a call site
- * which mentions only the type {@code Object}, and whose arity is the length
- * of the argument list.
- * <p>
- * Specifically, execution proceeds as if by the following steps,
- * although the methods are not guaranteed to be called if the JVM
- * can predict their effects.
- * <ul>
- * <li>Determine the length of the argument array as {@code N}.
- * For a null reference, {@code N=0}. </li>
- * <li>Determine the general type {@code TN} of {@code N} arguments as
- * as {@code TN=MethodType.genericMethodType(N)}.</li>
- * <li>Force the original target method handle {@code MH0} to the
- * required type, as {@code MH1 = MH0.asType(TN)}. </li>
- * <li>Spread the array into {@code N} separate arguments {@code A0, ...}. </li>
- * <li>Invoke the type-adjusted method handle on the unpacked arguments:
- * MH1.invokeExact(A0, ...). </li>
- * <li>Take the return value as an {@code Object} reference. </li>
- * </ul>
- * <p>
- * Because of the action of the {@code asType} step, the following argument
- * conversions are applied as necessary:
- * <ul>
- * <li>reference casting
- * <li>unboxing
- * <li>widening primitive conversions
- * </ul>
- * <p>
- * The result returned by the call is boxed if it is a primitive,
- * or forced to null if the return type is void.
- * <p>
- * This call is equivalent to the following code:
- * <blockquote><pre>{@code
- * MethodHandle invoker = MethodHandles.spreadInvoker(this.type(), 0);
- * Object result = invoker.invokeExact(this, arguments);
- * }</pre></blockquote>
- * <p>
- * Unlike the signature polymorphic methods {@code invokeExact} and {@code invoke},
- * {@code invokeWithArguments} can be accessed normally via the Core Reflection API and JNI.
- * It can therefore be used as a bridge between native or reflective code and method handles.
- *
- * @param arguments the arguments to pass to the target
- * @return the result returned by the target
- * @throws ClassCastException if an argument cannot be converted by reference casting
- * @throws WrongMethodTypeException if the target's type cannot be adjusted to take the given number of {@code Object} arguments
- * @throws Throwable anything thrown by the target method invocation
- * @see MethodHandles#spreadInvoker
- */
- public Object invokeWithArguments(Object... arguments) throws Throwable {
- MethodHandle invoker = null;
- synchronized (this) {
- if (cachedSpreadInvoker == null) {
- cachedSpreadInvoker = MethodHandles.spreadInvoker(this.type(), 0);
- }
-
- invoker = cachedSpreadInvoker;
- }
-
- return invoker.invoke(this, arguments);
- }
-
- /**
- * Performs a variable arity invocation, passing the arguments in the given array
- * to the method handle, as if via an inexact {@link #invoke invoke} from a call site
- * which mentions only the type {@code Object}, and whose arity is the length
- * of the argument array.
- * <p>
- * This method is also equivalent to the following code:
- * <blockquote><pre>{@code
- * invokeWithArguments(arguments.toArray()
- * }</pre></blockquote>
- *
- * @param arguments the arguments to pass to the target
- * @return the result returned by the target
- * @throws NullPointerException if {@code arguments} is a null reference
- * @throws ClassCastException if an argument cannot be converted by reference casting
- * @throws WrongMethodTypeException if the target's type cannot be adjusted to take the given number of {@code Object} arguments
- * @throws Throwable anything thrown by the target method invocation
- */
- public Object invokeWithArguments(java.util.List<?> arguments) throws Throwable {
- return invokeWithArguments(arguments.toArray());
- }
-
- /**
- * Produces an adapter method handle which adapts the type of the
- * current method handle to a new type.
- * The resulting method handle is guaranteed to report a type
- * which is equal to the desired new type.
- * <p>
- * If the original type and new type are equal, returns {@code this}.
- * <p>
- * The new method handle, when invoked, will perform the following
- * steps:
- * <ul>
- * <li>Convert the incoming argument list to match the original
- * method handle's argument list.
- * <li>Invoke the original method handle on the converted argument list.
- * <li>Convert any result returned by the original method handle
- * to the return type of new method handle.
- * </ul>
- * <p>
- * This method provides the crucial behavioral difference between
- * {@link #invokeExact invokeExact} and plain, inexact {@link #invoke invoke}.
- * The two methods
- * perform the same steps when the caller's type descriptor exactly m atches
- * the callee's, but when the types differ, plain {@link #invoke invoke}
- * also calls {@code asType} (or some internal equivalent) in order
- * to match up the caller's and callee's types.
- * <p>
- * If the current method is a variable arity method handle
- * argument list conversion may involve the conversion and collection
- * of several arguments into an array, as
- * {@linkplain #asVarargsCollector described elsewhere}.
- * In every other case, all conversions are applied <em>pairwise</em>,
- * which means that each argument or return value is converted to
- * exactly one argument or return value (or no return value).
- * The applied conversions are defined by consulting the
- * the corresponding component types of the old and new
- * method handle types.
- * <p>
- * Let <em>T0</em> and <em>T1</em> be corresponding new and old parameter types,
- * or old and new return types. Specifically, for some valid index {@code i}, let
- * <em>T0</em>{@code =newType.parameterType(i)} and <em>T1</em>{@code =this.type().parameterType(i)}.
- * Or else, going the other way for return values, let
- * <em>T0</em>{@code =this.type().returnType()} and <em>T1</em>{@code =newType.returnType()}.
- * If the types are the same, the new method handle makes no change
- * to the corresponding argument or return value (if any).
- * Otherwise, one of the following conversions is applied
- * if possible:
- * <ul>
- * <li>If <em>T0</em> and <em>T1</em> are references, then a cast to <em>T1</em> is applied.
- * (The types do not need to be related in any particular way.
- * This is because a dynamic value of null can convert to any reference type.)
- * <li>If <em>T0</em> and <em>T1</em> are primitives, then a Java method invocation
- * conversion (JLS 5.3) is applied, if one exists.
- * (Specifically, <em>T0</em> must convert to <em>T1</em> by a widening primitive conversion.)
- * <li>If <em>T0</em> is a primitive and <em>T1</em> a reference,
- * a Java casting conversion (JLS 5.5) is applied if one exists.
- * (Specifically, the value is boxed from <em>T0</em> to its wrapper class,
- * which is then widened as needed to <em>T1</em>.)
- * <li>If <em>T0</em> is a reference and <em>T1</em> a primitive, an unboxing
- * conversion will be applied at runtime, possibly followed
- * by a Java method invocation conversion (JLS 5.3)
- * on the primitive value. (These are the primitive widening conversions.)
- * <em>T0</em> must be a wrapper class or a supertype of one.
- * (In the case where <em>T0</em> is Object, these are the conversions
- * allowed by {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}.)
- * The unboxing conversion must have a possibility of success, which means that
- * if <em>T0</em> is not itself a wrapper class, there must exist at least one
- * wrapper class <em>TW</em> which is a subtype of <em>T0</em> and whose unboxed
- * primitive value can be widened to <em>T1</em>.
- * <li>If the return type <em>T1</em> is marked as void, any returned value is discarded
- * <li>If the return type <em>T0</em> is void and <em>T1</em> a reference, a null value is introduced.
- * <li>If the return type <em>T0</em> is void and <em>T1</em> a primitive,
- * a zero value is introduced.
- * </ul>
- * (<em>Note:</em> Both <em>T0</em> and <em>T1</em> may be regarded as static types,
- * because neither corresponds specifically to the <em>dynamic type</em> of any
- * actual argument or return value.)
- * <p>
- * The method handle conversion cannot be made if any one of the required
- * pairwise conversions cannot be made.
- * <p>
- * At runtime, the conversions applied to reference arguments
- * or return values may require additional runtime checks which can fail.
- * An unboxing operation may fail because the original reference is null,
- * causing a {@link java.lang.NullPointerException NullPointerException}.
- * An unboxing operation or a reference cast may also fail on a reference
- * to an object of the wrong type,
- * causing a {@link java.lang.ClassCastException ClassCastException}.
- * Although an unboxing operation may accept several kinds of wrappers,
- * if none are available, a {@code ClassCastException} will be thrown.
- *
- * @param newType the expected type of the new method handle
- * @return a method handle which delegates to {@code this} after performing
- * any necessary argument conversions, and arranges for any
- * necessary return value conversions
- * @throws NullPointerException if {@code newType} is a null reference
- * @throws WrongMethodTypeException if the conversion cannot be made
- * @see MethodHandles#explicitCastArguments
- */
- public MethodHandle asType(MethodType newType) {
- // Fast path alternative to a heavyweight {@code asType} call.
- // Return 'this' if the conversion will be a no-op.
- if (newType == type) {
- return this;
- }
-
- if (!type.isConvertibleTo(newType)) {
- throw new WrongMethodTypeException("cannot convert " + this + " to " + newType);
- }
-
- MethodHandle mh = duplicate();
- mh.nominalType = newType;
- return mh;
- }
-
- /**
- * Makes an <em>array-spreading</em> method handle, which accepts a trailing array argument
- * and spreads its elements as positional arguments.
- * The new method handle adapts, as its <i>target</i>,
- * the current method handle. The type of the adapter will be
- * the same as the type of the target, except that the final
- * {@code arrayLength} parameters of the target's type are replaced
- * by a single array parameter of type {@code arrayType}.
- * <p>
- * If the array element type differs from any of the corresponding
- * argument types on the original target,
- * the original target is adapted to take the array elements directly,
- * as if by a call to {@link #asType asType}.
- * <p>
- * When called, the adapter replaces a trailing array argument
- * by the array's elements, each as its own argument to the target.
- * (The order of the arguments is preserved.)
- * They are converted pairwise by casting and/or unboxing
- * to the types of the trailing parameters of the target.
- * Finally the target is called.
- * What the target eventually returns is returned unchanged by the adapter.
- * <p>
- * Before calling the target, the adapter verifies that the array
- * contains exactly enough elements to provide a correct argument count
- * to the target method handle.
- * (The array may also be null when zero elements are required.)
- * <p>
- * If, when the adapter is called, the supplied array argument does
- * not have the correct number of elements, the adapter will throw
- * an {@link IllegalArgumentException} instead of invoking the target.
- * <p>
- * Here are some simple examples of array-spreading method handles:
- * <blockquote><pre>{@code
-MethodHandle equals = publicLookup()
- .findVirtual(String.class, "equals", methodType(boolean.class, Object.class));
-assert( (boolean) equals.invokeExact("me", (Object)"me"));
-assert(!(boolean) equals.invokeExact("me", (Object)"thee"));
-// spread both arguments from a 2-array:
-MethodHandle eq2 = equals.asSpreader(Object[].class, 2);
-assert( (boolean) eq2.invokeExact(new Object[]{ "me", "me" }));
-assert(!(boolean) eq2.invokeExact(new Object[]{ "me", "thee" }));
-// try to spread from anything but a 2-array:
-for (int n = 0; n <= 10; n++) {
- Object[] badArityArgs = (n == 2 ? null : new Object[n]);
- try { assert((boolean) eq2.invokeExact(badArityArgs) && false); }
- catch (IllegalArgumentException ex) { } // OK
-}
-// spread both arguments from a String array:
-MethodHandle eq2s = equals.asSpreader(String[].class, 2);
-assert( (boolean) eq2s.invokeExact(new String[]{ "me", "me" }));
-assert(!(boolean) eq2s.invokeExact(new String[]{ "me", "thee" }));
-// spread second arguments from a 1-array:
-MethodHandle eq1 = equals.asSpreader(Object[].class, 1);
-assert( (boolean) eq1.invokeExact("me", new Object[]{ "me" }));
-assert(!(boolean) eq1.invokeExact("me", new Object[]{ "thee" }));
-// spread no arguments from a 0-array or null:
-MethodHandle eq0 = equals.asSpreader(Object[].class, 0);
-assert( (boolean) eq0.invokeExact("me", (Object)"me", new Object[0]));
-assert(!(boolean) eq0.invokeExact("me", (Object)"thee", (Object[])null));
-// asSpreader and asCollector are approximate inverses:
-for (int n = 0; n <= 2; n++) {
- for (Class<?> a : new Class<?>[]{Object[].class, String[].class, CharSequence[].class}) {
- MethodHandle equals2 = equals.asSpreader(a, n).asCollector(a, n);
- assert( (boolean) equals2.invokeWithArguments("me", "me"));
- assert(!(boolean) equals2.invokeWithArguments("me", "thee"));
- }
-}
-MethodHandle caToString = publicLookup()
- .findStatic(Arrays.class, "toString", methodType(String.class, char[].class));
-assertEquals("[A, B, C]", (String) caToString.invokeExact("ABC".toCharArray()));
-MethodHandle caString3 = caToString.asCollector(char[].class, 3);
-assertEquals("[A, B, C]", (String) caString3.invokeExact('A', 'B', 'C'));
-MethodHandle caToString2 = caString3.asSpreader(char[].class, 2);
-assertEquals("[A, B, C]", (String) caToString2.invokeExact('A', "BC".toCharArray()));
- * }</pre></blockquote>
- * @param arrayType usually {@code Object[]}, the type of the array argument from which to extract the spread arguments
- * @param arrayLength the number of arguments to spread from an incoming array argument
- * @return a new method handle which spreads its final array argument,
- * before calling the original method handle
- * @throws NullPointerException if {@code arrayType} is a null reference
- * @throws IllegalArgumentException if {@code arrayType} is not an array type,
- * or if target does not have at least
- * {@code arrayLength} parameter types,
- * or if {@code arrayLength} is negative,
- * or if the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- * @throws WrongMethodTypeException if the implied {@code asType} call fails
- * @see #asCollector
- */
- public MethodHandle asSpreader(Class<?> arrayType, int arrayLength) {
- MethodType postSpreadType = asSpreaderChecks(arrayType, arrayLength);
-
- final int targetParamCount = postSpreadType.parameterCount();
- MethodType dropArrayArgs = postSpreadType.dropParameterTypes(
- (targetParamCount - arrayLength), targetParamCount);
- MethodType adapterType = dropArrayArgs.appendParameterTypes(arrayType);
-
- return new Transformers.Spreader(this, adapterType, arrayLength);
- }
-
- /**
- * See if {@code asSpreader} can be validly called with the given arguments.
- * Return the type of the method handle call after spreading but before conversions.
- */
- private MethodType asSpreaderChecks(Class<?> arrayType, int arrayLength) {
- spreadArrayChecks(arrayType, arrayLength);
- int nargs = type().parameterCount();
- if (nargs < arrayLength || arrayLength < 0)
- throw newIllegalArgumentException("bad spread array length");
- Class<?> arrayElement = arrayType.getComponentType();
- MethodType mtype = type();
- boolean match = true, fail = false;
- for (int i = nargs - arrayLength; i < nargs; i++) {
- Class<?> ptype = mtype.parameterType(i);
- if (ptype != arrayElement) {
- match = false;
- if (!MethodType.canConvert(arrayElement, ptype)) {
- fail = true;
- break;
- }
- }
- }
- if (match) return mtype;
- MethodType needType = mtype.asSpreaderType(arrayType, arrayLength);
- if (!fail) return needType;
- // elicit an error:
- this.asType(needType);
- throw newInternalError("should not return", null);
- }
-
- private void spreadArrayChecks(Class<?> arrayType, int arrayLength) {
- Class<?> arrayElement = arrayType.getComponentType();
- if (arrayElement == null)
- throw newIllegalArgumentException("not an array type", arrayType);
- if ((arrayLength & 0x7F) != arrayLength) {
- if ((arrayLength & 0xFF) != arrayLength)
- throw newIllegalArgumentException("array length is not legal", arrayLength);
- assert(arrayLength >= 128);
- if (arrayElement == long.class ||
- arrayElement == double.class)
- throw newIllegalArgumentException("array length is not legal for long[] or double[]", arrayLength);
- }
- }
-
- /**
- * Makes an <em>array-collecting</em> method handle, which accepts a given number of trailing
- * positional arguments and collects them into an array argument.
- * The new method handle adapts, as its <i>target</i>,
- * the current method handle. The type of the adapter will be
- * the same as the type of the target, except that a single trailing
- * parameter (usually of type {@code arrayType}) is replaced by
- * {@code arrayLength} parameters whose type is element type of {@code arrayType}.
- * <p>
- * If the array type differs from the final argument type on the original target,
- * the original target is adapted to take the array type directly,
- * as if by a call to {@link #asType asType}.
- * <p>
- * When called, the adapter replaces its trailing {@code arrayLength}
- * arguments by a single new array of type {@code arrayType}, whose elements
- * comprise (in order) the replaced arguments.
- * Finally the target is called.
- * What the target eventually returns is returned unchanged by the adapter.
- * <p>
- * (The array may also be a shared constant when {@code arrayLength} is zero.)
- * <p>
- * (<em>Note:</em> The {@code arrayType} is often identical to the last
- * parameter type of the original target.
- * It is an explicit argument for symmetry with {@code asSpreader}, and also
- * to allow the target to use a simple {@code Object} as its last parameter type.)
- * <p>
- * In order to create a collecting adapter which is not restricted to a particular
- * number of collected arguments, use {@link #asVarargsCollector asVarargsCollector} instead.
- * <p>
- * Here are some examples of array-collecting method handles:
- * <blockquote><pre>{@code
-MethodHandle deepToString = publicLookup()
- .findStatic(Arrays.class, "deepToString", methodType(String.class, Object[].class));
-assertEquals("[won]", (String) deepToString.invokeExact(new Object[]{"won"}));
-MethodHandle ts1 = deepToString.asCollector(Object[].class, 1);
-assertEquals(methodType(String.class, Object.class), ts1.type());
-//assertEquals("[won]", (String) ts1.invokeExact( new Object[]{"won"})); //FAIL
-assertEquals("[[won]]", (String) ts1.invokeExact((Object) new Object[]{"won"}));
-// arrayType can be a subtype of Object[]
-MethodHandle ts2 = deepToString.asCollector(String[].class, 2);
-assertEquals(methodType(String.class, String.class, String.class), ts2.type());
-assertEquals("[two, too]", (String) ts2.invokeExact("two", "too"));
-MethodHandle ts0 = deepToString.asCollector(Object[].class, 0);
-assertEquals("[]", (String) ts0.invokeExact());
-// collectors can be nested, Lisp-style
-MethodHandle ts22 = deepToString.asCollector(Object[].class, 3).asCollector(String[].class, 2);
-assertEquals("[A, B, [C, D]]", ((String) ts22.invokeExact((Object)'A', (Object)"B", "C", "D")));
-// arrayType can be any primitive array type
-MethodHandle bytesToString = publicLookup()
- .findStatic(Arrays.class, "toString", methodType(String.class, byte[].class))
- .asCollector(byte[].class, 3);
-assertEquals("[1, 2, 3]", (String) bytesToString.invokeExact((byte)1, (byte)2, (byte)3));
-MethodHandle longsToString = publicLookup()
- .findStatic(Arrays.class, "toString", methodType(String.class, long[].class))
- .asCollector(long[].class, 1);
-assertEquals("[123]", (String) longsToString.invokeExact((long)123));
- * }</pre></blockquote>
- * @param arrayType often {@code Object[]}, the type of the array argument which will collect the arguments
- * @param arrayLength the number of arguments to collect into a new array argument
- * @return a new method handle which collects some trailing argument
- * into an array, before calling the original method handle
- * @throws NullPointerException if {@code arrayType} is a null reference
- * @throws IllegalArgumentException if {@code arrayType} is not an array type
- * or {@code arrayType} is not assignable to this method handle's trailing parameter type,
- * or {@code arrayLength} is not a legal array size,
- * or the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- * @throws WrongMethodTypeException if the implied {@code asType} call fails
- * @see #asSpreader
- * @see #asVarargsCollector
- */
- public MethodHandle asCollector(Class<?> arrayType, int arrayLength) {
- asCollectorChecks(arrayType, arrayLength);
-
- return new Transformers.Collector(this, arrayType, arrayLength);
- }
-
- /**
- * See if {@code asCollector} can be validly called with the given arguments.
- * Return false if the last parameter is not an exact match to arrayType.
- */
- /*non-public*/ boolean asCollectorChecks(Class<?> arrayType, int arrayLength) {
- spreadArrayChecks(arrayType, arrayLength);
- int nargs = type().parameterCount();
- if (nargs != 0) {
- Class<?> lastParam = type().parameterType(nargs-1);
- if (lastParam == arrayType) return true;
- if (lastParam.isAssignableFrom(arrayType)) return false;
- }
- throw newIllegalArgumentException("array type not assignable to trailing argument", this, arrayType);
- }
-
- /**
- * Makes a <em>variable arity</em> adapter which is able to accept
- * any number of trailing positional arguments and collect them
- * into an array argument.
- * <p>
- * The type and behavior of the adapter will be the same as
- * the type and behavior of the target, except that certain
- * {@code invoke} and {@code asType} requests can lead to
- * trailing positional arguments being collected into target's
- * trailing parameter.
- * Also, the last parameter type of the adapter will be
- * {@code arrayType}, even if the target has a different
- * last parameter type.
- * <p>
- * This transformation may return {@code this} if the method handle is
- * already of variable arity and its trailing parameter type
- * is identical to {@code arrayType}.
- * <p>
- * When called with {@link #invokeExact invokeExact}, the adapter invokes
- * the target with no argument changes.
- * (<em>Note:</em> This behavior is different from a
- * {@linkplain #asCollector fixed arity collector},
- * since it accepts a whole array of indeterminate length,
- * rather than a fixed number of arguments.)
- * <p>
- * When called with plain, inexact {@link #invoke invoke}, if the caller
- * type is the same as the adapter, the adapter invokes the target as with
- * {@code invokeExact}.
- * (This is the normal behavior for {@code invoke} when types match.)
- * <p>
- * Otherwise, if the caller and adapter arity are the same, and the
- * trailing parameter type of the caller is a reference type identical to
- * or assignable to the trailing parameter type of the adapter,
- * the arguments and return values are converted pairwise,
- * as if by {@link #asType asType} on a fixed arity
- * method handle.
- * <p>
- * Otherwise, the arities differ, or the adapter's trailing parameter
- * type is not assignable from the corresponding caller type.
- * In this case, the adapter replaces all trailing arguments from
- * the original trailing argument position onward, by
- * a new array of type {@code arrayType}, whose elements
- * comprise (in order) the replaced arguments.
- * <p>
- * The caller type must provides as least enough arguments,
- * and of the correct type, to satisfy the target's requirement for
- * positional arguments before the trailing array argument.
- * Thus, the caller must supply, at a minimum, {@code N-1} arguments,
- * where {@code N} is the arity of the target.
- * Also, there must exist conversions from the incoming arguments
- * to the target's arguments.
- * As with other uses of plain {@code invoke}, if these basic
- * requirements are not fulfilled, a {@code WrongMethodTypeException}
- * may be thrown.
- * <p>
- * In all cases, what the target eventually returns is returned unchanged by the adapter.
- * <p>
- * In the final case, it is exactly as if the target method handle were
- * temporarily adapted with a {@linkplain #asCollector fixed arity collector}
- * to the arity required by the caller type.
- * (As with {@code asCollector}, if the array length is zero,
- * a shared constant may be used instead of a new array.
- * If the implied call to {@code asCollector} would throw
- * an {@code IllegalArgumentException} or {@code WrongMethodTypeException},
- * the call to the variable arity adapter must throw
- * {@code WrongMethodTypeException}.)
- * <p>
- * The behavior of {@link #asType asType} is also specialized for
- * variable arity adapters, to maintain the invariant that
- * plain, inexact {@code invoke} is always equivalent to an {@code asType}
- * call to adjust the target type, followed by {@code invokeExact}.
- * Therefore, a variable arity adapter responds
- * to an {@code asType} request by building a fixed arity collector,
- * if and only if the adapter and requested type differ either
- * in arity or trailing argument type.
- * The resulting fixed arity collector has its type further adjusted
- * (if necessary) to the requested type by pairwise conversion,
- * as if by another application of {@code asType}.
- * <p>
- * When a method handle is obtained by executing an {@code ldc} instruction
- * of a {@code CONSTANT_MethodHandle} constant, and the target method is marked
- * as a variable arity method (with the modifier bit {@code 0x0080}),
- * the method handle will accept multiple arities, as if the method handle
- * constant were created by means of a call to {@code asVarargsCollector}.
- * <p>
- * In order to create a collecting adapter which collects a predetermined
- * number of arguments, and whose type reflects this predetermined number,
- * use {@link #asCollector asCollector} instead.
- * <p>
- * No method handle transformations produce new method handles with
- * variable arity, unless they are documented as doing so.
- * Therefore, besides {@code asVarargsCollector},
- * all methods in {@code MethodHandle} and {@code MethodHandles}
- * will return a method handle with fixed arity,
- * except in the cases where they are specified to return their original
- * operand (e.g., {@code asType} of the method handle's own type).
- * <p>
- * Calling {@code asVarargsCollector} on a method handle which is already
- * of variable arity will produce a method handle with the same type and behavior.
- * It may (or may not) return the original variable arity method handle.
- * <p>
- * Here is an example, of a list-making variable arity method handle:
- * <blockquote><pre>{@code
-MethodHandle deepToString = publicLookup()
- .findStatic(Arrays.class, "deepToString", methodType(String.class, Object[].class));
-MethodHandle ts1 = deepToString.asVarargsCollector(Object[].class);
-assertEquals("[won]", (String) ts1.invokeExact( new Object[]{"won"}));
-assertEquals("[won]", (String) ts1.invoke( new Object[]{"won"}));
-assertEquals("[won]", (String) ts1.invoke( "won" ));
-assertEquals("[[won]]", (String) ts1.invoke((Object) new Object[]{"won"}));
-// findStatic of Arrays.asList(...) produces a variable arity method handle:
-MethodHandle asList = publicLookup()
- .findStatic(Arrays.class, "asList", methodType(List.class, Object[].class));
-assertEquals(methodType(List.class, Object[].class), asList.type());
-assert(asList.isVarargsCollector());
-assertEquals("[]", asList.invoke().toString());
-assertEquals("[1]", asList.invoke(1).toString());
-assertEquals("[two, too]", asList.invoke("two", "too").toString());
-String[] argv = { "three", "thee", "tee" };
-assertEquals("[three, thee, tee]", asList.invoke(argv).toString());
-assertEquals("[three, thee, tee]", asList.invoke((Object[])argv).toString());
-List ls = (List) asList.invoke((Object)argv);
-assertEquals(1, ls.size());
-assertEquals("[three, thee, tee]", Arrays.toString((Object[])ls.get(0)));
- * }</pre></blockquote>
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * These rules are designed as a dynamically-typed variation
- * of the Java rules for variable arity methods.
- * In both cases, callers to a variable arity method or method handle
- * can either pass zero or more positional arguments, or else pass
- * pre-collected arrays of any length. Users should be aware of the
- * special role of the final argument, and of the effect of a
- * type match on that final argument, which determines whether
- * or not a single trailing argument is interpreted as a whole
- * array or a single element of an array to be collected.
- * Note that the dynamic type of the trailing argument has no
- * effect on this decision, only a comparison between the symbolic
- * type descriptor of the call site and the type descriptor of the method handle.)
- *
- * @param arrayType often {@code Object[]}, the type of the array argument which will collect the arguments
- * @return a new method handle which can collect any number of trailing arguments
- * into an array, before calling the original method handle
- * @throws NullPointerException if {@code arrayType} is a null reference
- * @throws IllegalArgumentException if {@code arrayType} is not an array type
- * or {@code arrayType} is not assignable to this method handle's trailing parameter type
- * @see #asCollector
- * @see #isVarargsCollector
- * @see #asFixedArity
- */
- public MethodHandle asVarargsCollector(Class<?> arrayType) {
- arrayType.getClass(); // explicit NPE
- boolean lastMatch = asCollectorChecks(arrayType, 0);
- if (isVarargsCollector() && lastMatch)
- return this;
- return new Transformers.VarargsCollector(this);
- }
+ public MethodType type() { return null; }
- /**
- * Determines if this method handle
- * supports {@linkplain #asVarargsCollector variable arity} calls.
- * Such method handles arise from the following sources:
- * <ul>
- * <li>a call to {@linkplain #asVarargsCollector asVarargsCollector}
- * <li>a call to a {@linkplain java.lang.invoke.MethodHandles.Lookup lookup method}
- * which resolves to a variable arity Java method or constructor
- * <li>an {@code ldc} instruction of a {@code CONSTANT_MethodHandle}
- * which resolves to a variable arity Java method or constructor
- * </ul>
- * @return true if this method handle accepts more than one arity of plain, inexact {@code invoke} calls
- * @see #asVarargsCollector
- * @see #asFixedArity
- */
- public boolean isVarargsCollector() {
- return false;
- }
+ public final Object invokeExact(Object... args) throws Throwable { return null; }
- /**
- * Makes a <em>fixed arity</em> method handle which is otherwise
- * equivalent to the current method handle.
- * <p>
- * If the current method handle is not of
- * {@linkplain #asVarargsCollector variable arity},
- * the current method handle is returned.
- * This is true even if the current method handle
- * could not be a valid input to {@code asVarargsCollector}.
- * <p>
- * Otherwise, the resulting fixed-arity method handle has the same
- * type and behavior of the current method handle,
- * except that {@link #isVarargsCollector isVarargsCollector}
- * will be false.
- * The fixed-arity method handle may (or may not) be the
- * a previous argument to {@code asVarargsCollector}.
- * <p>
- * Here is an example, of a list-making variable arity method handle:
- * <blockquote><pre>{@code
-MethodHandle asListVar = publicLookup()
- .findStatic(Arrays.class, "asList", methodType(List.class, Object[].class))
- .asVarargsCollector(Object[].class);
-MethodHandle asListFix = asListVar.asFixedArity();
-assertEquals("[1]", asListVar.invoke(1).toString());
-Exception caught = null;
-try { asListFix.invoke((Object)1); }
-catch (Exception ex) { caught = ex; }
-assert(caught instanceof ClassCastException);
-assertEquals("[two, too]", asListVar.invoke("two", "too").toString());
-try { asListFix.invoke("two", "too"); }
-catch (Exception ex) { caught = ex; }
-assert(caught instanceof WrongMethodTypeException);
-Object[] argv = { "three", "thee", "tee" };
-assertEquals("[three, thee, tee]", asListVar.invoke(argv).toString());
-assertEquals("[three, thee, tee]", asListFix.invoke(argv).toString());
-assertEquals(1, ((List) asListVar.invoke((Object)argv)).size());
-assertEquals("[three, thee, tee]", asListFix.invoke((Object)argv).toString());
- * }</pre></blockquote>
- *
- * @return a new method handle which accepts only a fixed number of arguments
- * @see #asVarargsCollector
- * @see #isVarargsCollector
- */
- public MethodHandle asFixedArity() {
- // Android-changed: implementation specific.
- MethodHandle mh = this;
- if (mh.isVarargsCollector()) {
- mh = ((Transformers.VarargsCollector) mh).asFixedArity();
- }
- assert(!mh.isVarargsCollector());
- return mh;
- }
+ public final Object invoke(Object... args) throws Throwable { return null; }
- /**
- * Binds a value {@code x} to the first argument of a method handle, without invoking it.
- * The new method handle adapts, as its <i>target</i>,
- * the current method handle by binding it to the given argument.
- * The type of the bound handle will be
- * the same as the type of the target, except that a single leading
- * reference parameter will be omitted.
- * <p>
- * When called, the bound handle inserts the given value {@code x}
- * as a new leading argument to the target. The other arguments are
- * also passed unchanged.
- * What the target eventually returns is returned unchanged by the bound handle.
- * <p>
- * The reference {@code x} must be convertible to the first parameter
- * type of the target.
- * <p>
- * (<em>Note:</em> Because method handles are immutable, the target method handle
- * retains its original type and behavior.)
- * @param x the value to bind to the first argument of the target
- * @return a new method handle which prepends the given value to the incoming
- * argument list, before calling the original method handle
- * @throws IllegalArgumentException if the target does not have a
- * leading parameter type that is a reference type
- * @throws ClassCastException if {@code x} cannot be converted
- * to the leading parameter type of the target
- * @see MethodHandles#insertArguments
- */
- public MethodHandle bindTo(Object x) {
- x = type.leadingReferenceParameter().cast(x); // throw CCE if needed
+ public Object invokeWithArguments(Object... arguments) throws Throwable { return null; }
- return new Transformers.BindTo(this, x);
- }
+ public Object invokeWithArguments(java.util.List<?> arguments) throws Throwable { return null; }
- /**
- * Returns a string representation of the method handle,
- * starting with the string {@code "MethodHandle"} and
- * ending with the string representation of the method handle's type.
- * In other words, this method returns a string equal to the value of:
- * <blockquote><pre>{@code
- * "MethodHandle" + type().toString()
- * }</pre></blockquote>
- * <p>
- * (<em>Note:</em> Future releases of this API may add further information
- * to the string representation.
- * Therefore, the present syntax should not be parsed by applications.)
- *
- * @return a string representation of the method handle
- */
- @Override
- public String toString() {
- // Android-changed: Removed debugging support.
- return "MethodHandle"+type;
- }
+ public MethodHandle asType(MethodType newType) { return null; }
- /** @hide */
- public int getHandleKind() {
- return handleKind;
- }
+ public MethodHandle asCollector(Class<?> arrayType, int arrayLength) { return null; }
- /** @hide */
- protected void transform(EmulatedStackFrame arguments) throws Throwable {
- throw new AssertionError("MethodHandle.transform should never be called.");
- }
+ public MethodHandle asVarargsCollector(Class<?> arrayType) { return null; }
- /**
- * Creates a copy of this method handle, copying all relevant data.
- *
- * @hide
- */
- protected MethodHandle duplicate() {
- try {
- return (MethodHandle) this.clone();
- } catch (CloneNotSupportedException cnse) {
- throw new AssertionError("Subclass of Transformer is not cloneable");
- }
- }
+ public boolean isVarargsCollector() { return false; }
+ public MethodHandle asFixedArity() { return null; }
- /**
- * This is the entry point for all transform calls, and dispatches to the protected
- * transform method. This layer of indirection exists purely for convenience, because
- * we can invoke-direct on a fixed ArtMethod for all transform variants.
- *
- * NOTE: If this extra layer of indirection proves to be a problem, we can get rid
- * of this layer of indirection at the cost of some additional ugliness.
- */
- private void transformInternal(EmulatedStackFrame arguments) throws Throwable {
- transform(arguments);
- }
+ public MethodHandle bindTo(Object x) { return null; }
- // Android-changed: Removed implementation details :
- //
- // String standardString();
- // String debugString();
- //
- //// Implementation methods.
- //// Sub-classes can override these default implementations.
- //// All these methods assume arguments are already validated.
- //
- // Other transforms to do: convert, explicitCast, permute, drop, filter, fold, GWT, catch
- //
- // BoundMethodHandle bindArgumentL(int pos, Object value);
- // /*non-public*/ MethodHandle setVarargs(MemberName member);
- // /*non-public*/ MethodHandle viewAsType(MethodType newType, boolean strict);
- // /*non-public*/ boolean viewAsTypeChecks(MethodType newType, boolean strict);
- //
- // Decoding
- //
- // /*non-public*/ LambdaForm internalForm();
- // /*non-public*/ MemberName internalMemberName();
- // /*non-public*/ Class<?> internalCallerClass();
- // /*non-public*/ MethodHandleImpl.Intrinsic intrinsicName();
- // /*non-public*/ MethodHandle withInternalMemberName(MemberName member, boolean isInvokeSpecial);
- // /*non-public*/ boolean isInvokeSpecial();
- // /*non-public*/ Object internalValues();
- // /*non-public*/ Object internalProperties();
- //
- //// Method handle implementation methods.
- //// Sub-classes can override these default implementations.
- //// All these methods assume arguments are already validated.
- //
- // /*non-public*/ abstract MethodHandle copyWith(MethodType mt, LambdaForm lf);
- // abstract BoundMethodHandle rebind();
- // /*non-public*/ void updateForm(LambdaForm newForm);
- // /*non-public*/ void customize();
- // private static final long FORM_OFFSET;
}
diff --git a/java/lang/invoke/MethodHandles.java b/java/lang/invoke/MethodHandles.java
index a1b861d2..f27ad988 100644
--- a/java/lang/invoke/MethodHandles.java
+++ b/java/lang/invoke/MethodHandles.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2008, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2008, 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
@@ -25,3461 +25,129 @@
package java.lang.invoke;
-import java.lang.reflect.*;
-import java.nio.ByteOrder;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
import java.util.List;
-import java.util.Arrays;
-import java.util.ArrayList;
-import java.util.NoSuchElementException;
-import dalvik.system.VMStack;
-import sun.invoke.util.VerifyAccess;
-import sun.invoke.util.Wrapper;
-import static java.lang.invoke.MethodHandleStatics.*;
-
-/**
- * This class consists exclusively of static methods that operate on or return
- * method handles. They fall into several categories:
- * <ul>
- * <li>Lookup methods which help create method handles for methods and fields.
- * <li>Combinator methods, which combine or transform pre-existing method handles into new ones.
- * <li>Other factory methods to create method handles that emulate other common JVM operations or control flow patterns.
- * </ul>
- * <p>
- * @author John Rose, JSR 292 EG
- * @since 1.7
- */
public class MethodHandles {
- private MethodHandles() { } // do not instantiate
-
- // BEGIN Android-added: unsupported() helper function.
- // TODO(b/65872996): Remove when complete.
- private static void unsupported(String msg) throws UnsupportedOperationException {
- throw new UnsupportedOperationException(msg);
- }
- // END Android-added: unsupported() helper function.
-
- // Android-changed: We do not use MemberName / MethodHandleImpl.
- //
- // private static final MemberName.Factory IMPL_NAMES = MemberName.getFactory();
- // static { MethodHandleImpl.initStatics(); }
- // See IMPL_LOOKUP below.
-
- //// Method handle creation from ordinary methods.
+ public static Lookup lookup() { return null; }
- /**
- * Returns a {@link Lookup lookup object} with
- * full capabilities to emulate all supported bytecode behaviors of the caller.
- * These capabilities include <a href="MethodHandles.Lookup.html#privacc">private access</a> to the caller.
- * Factory methods on the lookup object can create
- * <a href="MethodHandleInfo.html#directmh">direct method handles</a>
- * for any member that the caller has access to via bytecodes,
- * including protected and private fields and methods.
- * This lookup object is a <em>capability</em> which may be delegated to trusted agents.
- * Do not store it in place where untrusted code can access it.
- * <p>
- * This method is caller sensitive, which means that it may return different
- * values to different callers.
- * <p>
- * For any given caller class {@code C}, the lookup object returned by this call
- * has equivalent capabilities to any lookup object
- * supplied by the JVM to the bootstrap method of an
- * <a href="package-summary.html#indyinsn">invokedynamic instruction</a>
- * executing in the same caller class {@code C}.
- * @return a lookup object for the caller of this method, with private access
- */
- // Android-changed: Remove caller sensitive.
- // @CallerSensitive
- public static Lookup lookup() {
- // Android-changed: Do not use Reflection.getCallerClass().
- return new Lookup(VMStack.getStackClass1());
- }
-
- /**
- * Returns a {@link Lookup lookup object} which is trusted minimally.
- * It can only be used to create method handles to
- * publicly accessible fields and methods.
- * <p>
- * As a matter of pure convention, the {@linkplain Lookup#lookupClass lookup class}
- * of this lookup object will be {@link java.lang.Object}.
- *
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * The lookup class can be changed to any other class {@code C} using an expression of the form
- * {@link Lookup#in publicLookup().in(C.class)}.
- * Since all classes have equal access to public names,
- * such a change would confer no new access rights.
- * A public lookup object is always subject to
- * <a href="MethodHandles.Lookup.html#secmgr">security manager checks</a>.
- * Also, it cannot access
- * <a href="MethodHandles.Lookup.html#callsens">caller sensitive methods</a>.
- * @return a lookup object which is trusted minimally
- */
- public static Lookup publicLookup() {
- return Lookup.PUBLIC_LOOKUP;
- }
+ public static Lookup publicLookup() { return null; }
- /**
- * Performs an unchecked "crack" of a
- * <a href="MethodHandleInfo.html#directmh">direct method handle</a>.
- * The result is as if the user had obtained a lookup object capable enough
- * to crack the target method handle, called
- * {@link java.lang.invoke.MethodHandles.Lookup#revealDirect Lookup.revealDirect}
- * on the target to obtain its symbolic reference, and then called
- * {@link java.lang.invoke.MethodHandleInfo#reflectAs MethodHandleInfo.reflectAs}
- * to resolve the symbolic reference to a member.
- * <p>
- * If there is a security manager, its {@code checkPermission} method
- * is called with a {@code ReflectPermission("suppressAccessChecks")} permission.
- * @param <T> the desired type of the result, either {@link Member} or a subtype
- * @param target a direct method handle to crack into symbolic reference components
- * @param expected a class object representing the desired result type {@code T}
- * @return a reference to the method, constructor, or field object
- * @exception SecurityException if the caller is not privileged to call {@code setAccessible}
- * @exception NullPointerException if either argument is {@code null}
- * @exception IllegalArgumentException if the target is not a direct method handle
- * @exception ClassCastException if the member is not of the expected type
- * @since 1.8
- */
public static <T extends Member> T
- reflectAs(Class<T> expected, MethodHandle target) {
- MethodHandleImpl directTarget = getMethodHandleImpl(target);
- // Given that this is specified to be an "unchecked" crack, we can directly allocate
- // a member from the underlying ArtField / Method and bypass all associated access checks.
- return expected.cast(directTarget.getMemberInternal());
- }
+ reflectAs(Class<T> expected, MethodHandle target) { return null; }
- /**
- * A <em>lookup object</em> is a factory for creating method handles,
- * when the creation requires access checking.
- * Method handles do not perform
- * access checks when they are called, but rather when they are created.
- * Therefore, method handle access
- * restrictions must be enforced when a method handle is created.
- * The caller class against which those restrictions are enforced
- * is known as the {@linkplain #lookupClass lookup class}.
- * <p>
- * A lookup class which needs to create method handles will call
- * {@link #lookup MethodHandles.lookup} to create a factory for itself.
- * When the {@code Lookup} factory object is created, the identity of the lookup class is
- * determined, and securely stored in the {@code Lookup} object.
- * The lookup class (or its delegates) may then use factory methods
- * on the {@code Lookup} object to create method handles for access-checked members.
- * This includes all methods, constructors, and fields which are allowed to the lookup class,
- * even private ones.
- *
- * <h1><a name="lookups"></a>Lookup Factory Methods</h1>
- * The factory methods on a {@code Lookup} object correspond to all major
- * use cases for methods, constructors, and fields.
- * Each method handle created by a factory method is the functional
- * equivalent of a particular <em>bytecode behavior</em>.
- * (Bytecode behaviors are described in section 5.4.3.5 of the Java Virtual Machine Specification.)
- * Here is a summary of the correspondence between these factory methods and
- * the behavior the resulting method handles:
- * <table border=1 cellpadding=5 summary="lookup method behaviors">
- * <tr>
- * <th><a name="equiv"></a>lookup expression</th>
- * <th>member</th>
- * <th>bytecode behavior</th>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findGetter lookup.findGetter(C.class,"f",FT.class)}</td>
- * <td>{@code FT f;}</td><td>{@code (T) this.f;}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findStaticGetter lookup.findStaticGetter(C.class,"f",FT.class)}</td>
- * <td>{@code static}<br>{@code FT f;}</td><td>{@code (T) C.f;}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findSetter lookup.findSetter(C.class,"f",FT.class)}</td>
- * <td>{@code FT f;}</td><td>{@code this.f = x;}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findStaticSetter lookup.findStaticSetter(C.class,"f",FT.class)}</td>
- * <td>{@code static}<br>{@code FT f;}</td><td>{@code C.f = arg;}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findVirtual lookup.findVirtual(C.class,"m",MT)}</td>
- * <td>{@code T m(A*);}</td><td>{@code (T) this.m(arg*);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findStatic lookup.findStatic(C.class,"m",MT)}</td>
- * <td>{@code static}<br>{@code T m(A*);}</td><td>{@code (T) C.m(arg*);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findSpecial lookup.findSpecial(C.class,"m",MT,this.class)}</td>
- * <td>{@code T m(A*);}</td><td>{@code (T) super.m(arg*);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#findConstructor lookup.findConstructor(C.class,MT)}</td>
- * <td>{@code C(A*);}</td><td>{@code new C(arg*);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflectGetter lookup.unreflectGetter(aField)}</td>
- * <td>({@code static})?<br>{@code FT f;}</td><td>{@code (FT) aField.get(thisOrNull);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflectSetter lookup.unreflectSetter(aField)}</td>
- * <td>({@code static})?<br>{@code FT f;}</td><td>{@code aField.set(thisOrNull, arg);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflect lookup.unreflect(aMethod)}</td>
- * <td>({@code static})?<br>{@code T m(A*);}</td><td>{@code (T) aMethod.invoke(thisOrNull, arg*);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflectConstructor lookup.unreflectConstructor(aConstructor)}</td>
- * <td>{@code C(A*);}</td><td>{@code (C) aConstructor.newInstance(arg*);}</td>
- * </tr>
- * <tr>
- * <td>{@link java.lang.invoke.MethodHandles.Lookup#unreflect lookup.unreflect(aMethod)}</td>
- * <td>({@code static})?<br>{@code T m(A*);}</td><td>{@code (T) aMethod.invoke(thisOrNull, arg*);}</td>
- * </tr>
- * </table>
- *
- * Here, the type {@code C} is the class or interface being searched for a member,
- * documented as a parameter named {@code refc} in the lookup methods.
- * The method type {@code MT} is composed from the return type {@code T}
- * and the sequence of argument types {@code A*}.
- * The constructor also has a sequence of argument types {@code A*} and
- * is deemed to return the newly-created object of type {@code C}.
- * Both {@code MT} and the field type {@code FT} are documented as a parameter named {@code type}.
- * The formal parameter {@code this} stands for the self-reference of type {@code C};
- * if it is present, it is always the leading argument to the method handle invocation.
- * (In the case of some {@code protected} members, {@code this} may be
- * restricted in type to the lookup class; see below.)
- * The name {@code arg} stands for all the other method handle arguments.
- * In the code examples for the Core Reflection API, the name {@code thisOrNull}
- * stands for a null reference if the accessed method or field is static,
- * and {@code this} otherwise.
- * The names {@code aMethod}, {@code aField}, and {@code aConstructor} stand
- * for reflective objects corresponding to the given members.
- * <p>
- * In cases where the given member is of variable arity (i.e., a method or constructor)
- * the returned method handle will also be of {@linkplain MethodHandle#asVarargsCollector variable arity}.
- * In all other cases, the returned method handle will be of fixed arity.
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * The equivalence between looked-up method handles and underlying
- * class members and bytecode behaviors
- * can break down in a few ways:
- * <ul style="font-size:smaller;">
- * <li>If {@code C} is not symbolically accessible from the lookup class's loader,
- * the lookup can still succeed, even when there is no equivalent
- * Java expression or bytecoded constant.
- * <li>Likewise, if {@code T} or {@code MT}
- * is not symbolically accessible from the lookup class's loader,
- * the lookup can still succeed.
- * For example, lookups for {@code MethodHandle.invokeExact} and
- * {@code MethodHandle.invoke} will always succeed, regardless of requested type.
- * <li>If there is a security manager installed, it can forbid the lookup
- * on various grounds (<a href="MethodHandles.Lookup.html#secmgr">see below</a>).
- * By contrast, the {@code ldc} instruction on a {@code CONSTANT_MethodHandle}
- * constant is not subject to security manager checks.
- * <li>If the looked-up method has a
- * <a href="MethodHandle.html#maxarity">very large arity</a>,
- * the method handle creation may fail, due to the method handle
- * type having too many parameters.
- * </ul>
- *
- * <h1><a name="access"></a>Access checking</h1>
- * Access checks are applied in the factory methods of {@code Lookup},
- * when a method handle is created.
- * This is a key difference from the Core Reflection API, since
- * {@link java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}
- * performs access checking against every caller, on every call.
- * <p>
- * All access checks start from a {@code Lookup} object, which
- * compares its recorded lookup class against all requests to
- * create method handles.
- * A single {@code Lookup} object can be used to create any number
- * of access-checked method handles, all checked against a single
- * lookup class.
- * <p>
- * A {@code Lookup} object can be shared with other trusted code,
- * such as a metaobject protocol.
- * A shared {@code Lookup} object delegates the capability
- * to create method handles on private members of the lookup class.
- * Even if privileged code uses the {@code Lookup} object,
- * the access checking is confined to the privileges of the
- * original lookup class.
- * <p>
- * A lookup can fail, because
- * the containing class is not accessible to the lookup class, or
- * because the desired class member is missing, or because the
- * desired class member is not accessible to the lookup class, or
- * because the lookup object is not trusted enough to access the member.
- * In any of these cases, a {@code ReflectiveOperationException} will be
- * thrown from the attempted lookup. The exact class will be one of
- * the following:
- * <ul>
- * <li>NoSuchMethodException &mdash; if a method is requested but does not exist
- * <li>NoSuchFieldException &mdash; if a field is requested but does not exist
- * <li>IllegalAccessException &mdash; if the member exists but an access check fails
- * </ul>
- * <p>
- * In general, the conditions under which a method handle may be
- * looked up for a method {@code M} are no more restrictive than the conditions
- * under which the lookup class could have compiled, verified, and resolved a call to {@code M}.
- * Where the JVM would raise exceptions like {@code NoSuchMethodError},
- * a method handle lookup will generally raise a corresponding
- * checked exception, such as {@code NoSuchMethodException}.
- * And the effect of invoking the method handle resulting from the lookup
- * is <a href="MethodHandles.Lookup.html#equiv">exactly equivalent</a>
- * to executing the compiled, verified, and resolved call to {@code M}.
- * The same point is true of fields and constructors.
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * Access checks only apply to named and reflected methods,
- * constructors, and fields.
- * Other method handle creation methods, such as
- * {@link MethodHandle#asType MethodHandle.asType},
- * do not require any access checks, and are used
- * independently of any {@code Lookup} object.
- * <p>
- * If the desired member is {@code protected}, the usual JVM rules apply,
- * including the requirement that the lookup class must be either be in the
- * same package as the desired member, or must inherit that member.
- * (See the Java Virtual Machine Specification, sections 4.9.2, 5.4.3.5, and 6.4.)
- * In addition, if the desired member is a non-static field or method
- * in a different package, the resulting method handle may only be applied
- * to objects of the lookup class or one of its subclasses.
- * This requirement is enforced by narrowing the type of the leading
- * {@code this} parameter from {@code C}
- * (which will necessarily be a superclass of the lookup class)
- * to the lookup class itself.
- * <p>
- * The JVM imposes a similar requirement on {@code invokespecial} instruction,
- * that the receiver argument must match both the resolved method <em>and</em>
- * the current class. Again, this requirement is enforced by narrowing the
- * type of the leading parameter to the resulting method handle.
- * (See the Java Virtual Machine Specification, section 4.10.1.9.)
- * <p>
- * The JVM represents constructors and static initializer blocks as internal methods
- * with special names ({@code "<init>"} and {@code "<clinit>"}).
- * The internal syntax of invocation instructions allows them to refer to such internal
- * methods as if they were normal methods, but the JVM bytecode verifier rejects them.
- * A lookup of such an internal method will produce a {@code NoSuchMethodException}.
- * <p>
- * In some cases, access between nested classes is obtained by the Java compiler by creating
- * an wrapper method to access a private method of another class
- * in the same top-level declaration.
- * For example, a nested class {@code C.D}
- * can access private members within other related classes such as
- * {@code C}, {@code C.D.E}, or {@code C.B},
- * but the Java compiler may need to generate wrapper methods in
- * those related classes. In such cases, a {@code Lookup} object on
- * {@code C.E} would be unable to those private members.
- * A workaround for this limitation is the {@link Lookup#in Lookup.in} method,
- * which can transform a lookup on {@code C.E} into one on any of those other
- * classes, without special elevation of privilege.
- * <p>
- * The accesses permitted to a given lookup object may be limited,
- * according to its set of {@link #lookupModes lookupModes},
- * to a subset of members normally accessible to the lookup class.
- * For example, the {@link #publicLookup publicLookup}
- * method produces a lookup object which is only allowed to access
- * public members in public classes.
- * The caller sensitive method {@link #lookup lookup}
- * produces a lookup object with full capabilities relative to
- * its caller class, to emulate all supported bytecode behaviors.
- * Also, the {@link Lookup#in Lookup.in} method may produce a lookup object
- * with fewer access modes than the original lookup object.
- *
- * <p style="font-size:smaller;">
- * <a name="privacc"></a>
- * <em>Discussion of private access:</em>
- * We say that a lookup has <em>private access</em>
- * if its {@linkplain #lookupModes lookup modes}
- * include the possibility of accessing {@code private} members.
- * As documented in the relevant methods elsewhere,
- * only lookups with private access possess the following capabilities:
- * <ul style="font-size:smaller;">
- * <li>access private fields, methods, and constructors of the lookup class
- * <li>create method handles which invoke <a href="MethodHandles.Lookup.html#callsens">caller sensitive</a> methods,
- * such as {@code Class.forName}
- * <li>create method handles which {@link Lookup#findSpecial emulate invokespecial} instructions
- * <li>avoid <a href="MethodHandles.Lookup.html#secmgr">package access checks</a>
- * for classes accessible to the lookup class
- * <li>create {@link Lookup#in delegated lookup objects} which have private access to other classes
- * within the same package member
- * </ul>
- * <p style="font-size:smaller;">
- * Each of these permissions is a consequence of the fact that a lookup object
- * with private access can be securely traced back to an originating class,
- * whose <a href="MethodHandles.Lookup.html#equiv">bytecode behaviors</a> and Java language access permissions
- * can be reliably determined and emulated by method handles.
- *
- * <h1><a name="secmgr"></a>Security manager interactions</h1>
- * Although bytecode instructions can only refer to classes in
- * a related class loader, this API can search for methods in any
- * class, as long as a reference to its {@code Class} object is
- * available. Such cross-loader references are also possible with the
- * Core Reflection API, and are impossible to bytecode instructions
- * such as {@code invokestatic} or {@code getfield}.
- * There is a {@linkplain java.lang.SecurityManager security manager API}
- * to allow applications to check such cross-loader references.
- * These checks apply to both the {@code MethodHandles.Lookup} API
- * and the Core Reflection API
- * (as found on {@link java.lang.Class Class}).
- * <p>
- * If a security manager is present, member lookups are subject to
- * additional checks.
- * From one to three calls are made to the security manager.
- * Any of these calls can refuse access by throwing a
- * {@link java.lang.SecurityException SecurityException}.
- * Define {@code smgr} as the security manager,
- * {@code lookc} as the lookup class of the current lookup object,
- * {@code refc} as the containing class in which the member
- * is being sought, and {@code defc} as the class in which the
- * member is actually defined.
- * The value {@code lookc} is defined as <em>not present</em>
- * if the current lookup object does not have
- * <a href="MethodHandles.Lookup.html#privacc">private access</a>.
- * The calls are made according to the following rules:
- * <ul>
- * <li><b>Step 1:</b>
- * If {@code lookc} is not present, or if its class loader is not
- * the same as or an ancestor of the class loader of {@code refc},
- * then {@link SecurityManager#checkPackageAccess
- * smgr.checkPackageAccess(refcPkg)} is called,
- * where {@code refcPkg} is the package of {@code refc}.
- * <li><b>Step 2:</b>
- * If the retrieved member is not public and
- * {@code lookc} is not present, then
- * {@link SecurityManager#checkPermission smgr.checkPermission}
- * with {@code RuntimePermission("accessDeclaredMembers")} is called.
- * <li><b>Step 3:</b>
- * If the retrieved member is not public,
- * and if {@code lookc} is not present,
- * and if {@code defc} and {@code refc} are different,
- * then {@link SecurityManager#checkPackageAccess
- * smgr.checkPackageAccess(defcPkg)} is called,
- * where {@code defcPkg} is the package of {@code defc}.
- * </ul>
- * Security checks are performed after other access checks have passed.
- * Therefore, the above rules presuppose a member that is public,
- * or else that is being accessed from a lookup class that has
- * rights to access the member.
- *
- * <h1><a name="callsens"></a>Caller sensitive methods</h1>
- * A small number of Java methods have a special property called caller sensitivity.
- * A <em>caller-sensitive</em> method can behave differently depending on the
- * identity of its immediate caller.
- * <p>
- * If a method handle for a caller-sensitive method is requested,
- * the general rules for <a href="MethodHandles.Lookup.html#equiv">bytecode behaviors</a> apply,
- * but they take account of the lookup class in a special way.
- * The resulting method handle behaves as if it were called
- * from an instruction contained in the lookup class,
- * so that the caller-sensitive method detects the lookup class.
- * (By contrast, the invoker of the method handle is disregarded.)
- * Thus, in the case of caller-sensitive methods,
- * different lookup classes may give rise to
- * differently behaving method handles.
- * <p>
- * In cases where the lookup object is
- * {@link #publicLookup publicLookup()},
- * or some other lookup object without
- * <a href="MethodHandles.Lookup.html#privacc">private access</a>,
- * the lookup class is disregarded.
- * In such cases, no caller-sensitive method handle can be created,
- * access is forbidden, and the lookup fails with an
- * {@code IllegalAccessException}.
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * For example, the caller-sensitive method
- * {@link java.lang.Class#forName(String) Class.forName(x)}
- * can return varying classes or throw varying exceptions,
- * depending on the class loader of the class that calls it.
- * A public lookup of {@code Class.forName} will fail, because
- * there is no reasonable way to determine its bytecode behavior.
- * <p style="font-size:smaller;">
- * If an application caches method handles for broad sharing,
- * it should use {@code publicLookup()} to create them.
- * If there is a lookup of {@code Class.forName}, it will fail,
- * and the application must take appropriate action in that case.
- * It may be that a later lookup, perhaps during the invocation of a
- * bootstrap method, can incorporate the specific identity
- * of the caller, making the method accessible.
- * <p style="font-size:smaller;">
- * The function {@code MethodHandles.lookup} is caller sensitive
- * so that there can be a secure foundation for lookups.
- * Nearly all other methods in the JSR 292 API rely on lookup
- * objects to check access requests.
- */
- // Android-changed: Change link targets from MethodHandles#[public]Lookup to
- // #[public]Lookup to work around complaints from javadoc.
public static final
class Lookup {
- /** The class on behalf of whom the lookup is being performed. */
- /* @NonNull */ private final Class<?> lookupClass;
-
- /** The allowed sorts of members which may be looked up (PUBLIC, etc.). */
- private final int allowedModes;
-
- /** A single-bit mask representing {@code public} access,
- * which may contribute to the result of {@link #lookupModes lookupModes}.
- * The value, {@code 0x01}, happens to be the same as the value of the
- * {@code public} {@linkplain java.lang.reflect.Modifier#PUBLIC modifier bit}.
- */
- public static final int PUBLIC = Modifier.PUBLIC;
-
- /** A single-bit mask representing {@code private} access,
- * which may contribute to the result of {@link #lookupModes lookupModes}.
- * The value, {@code 0x02}, happens to be the same as the value of the
- * {@code private} {@linkplain java.lang.reflect.Modifier#PRIVATE modifier bit}.
- */
- public static final int PRIVATE = Modifier.PRIVATE;
-
- /** A single-bit mask representing {@code protected} access,
- * which may contribute to the result of {@link #lookupModes lookupModes}.
- * The value, {@code 0x04}, happens to be the same as the value of the
- * {@code protected} {@linkplain java.lang.reflect.Modifier#PROTECTED modifier bit}.
- */
- public static final int PROTECTED = Modifier.PROTECTED;
-
- /** A single-bit mask representing {@code package} access (default access),
- * which may contribute to the result of {@link #lookupModes lookupModes}.
- * The value is {@code 0x08}, which does not correspond meaningfully to
- * any particular {@linkplain java.lang.reflect.Modifier modifier bit}.
- */
- public static final int PACKAGE = Modifier.STATIC;
-
- private static final int ALL_MODES = (PUBLIC | PRIVATE | PROTECTED | PACKAGE);
-
- // Android-note: Android has no notion of a trusted lookup. If required, such lookups
- // are performed by the runtime. As a result, we always use lookupClass, which will always
- // be non-null in our implementation.
- //
- // private static final int TRUSTED = -1;
-
- private static int fixmods(int mods) {
- mods &= (ALL_MODES - PACKAGE);
- return (mods != 0) ? mods : PACKAGE;
- }
-
- /** Tells which class is performing the lookup. It is this class against
- * which checks are performed for visibility and access permissions.
- * <p>
- * The class implies a maximum level of access permission,
- * but the permissions may be additionally limited by the bitmask
- * {@link #lookupModes lookupModes}, which controls whether non-public members
- * can be accessed.
- * @return the lookup class, on behalf of which this lookup object finds members
- */
- public Class<?> lookupClass() {
- return lookupClass;
- }
-
- /** Tells which access-protection classes of members this lookup object can produce.
- * The result is a bit-mask of the bits
- * {@linkplain #PUBLIC PUBLIC (0x01)},
- * {@linkplain #PRIVATE PRIVATE (0x02)},
- * {@linkplain #PROTECTED PROTECTED (0x04)},
- * and {@linkplain #PACKAGE PACKAGE (0x08)}.
- * <p>
- * A freshly-created lookup object
- * on the {@linkplain java.lang.invoke.MethodHandles#lookup() caller's class}
- * has all possible bits set, since the caller class can access all its own members.
- * A lookup object on a new lookup class
- * {@linkplain java.lang.invoke.MethodHandles.Lookup#in created from a previous lookup object}
- * may have some mode bits set to zero.
- * The purpose of this is to restrict access via the new lookup object,
- * so that it can access only names which can be reached by the original
- * lookup object, and also by the new lookup class.
- * @return the lookup modes, which limit the kinds of access performed by this lookup object
- */
- public int lookupModes() {
- return allowedModes & ALL_MODES;
- }
-
- /** Embody the current class (the lookupClass) as a lookup class
- * for method handle creation.
- * Must be called by from a method in this package,
- * which in turn is called by a method not in this package.
- */
- Lookup(Class<?> lookupClass) {
- this(lookupClass, ALL_MODES);
- // make sure we haven't accidentally picked up a privileged class:
- checkUnprivilegedlookupClass(lookupClass, ALL_MODES);
- }
-
- private Lookup(Class<?> lookupClass, int allowedModes) {
- this.lookupClass = lookupClass;
- this.allowedModes = allowedModes;
- }
-
- /**
- * Creates a lookup on the specified new lookup class.
- * The resulting object will report the specified
- * class as its own {@link #lookupClass lookupClass}.
- * <p>
- * However, the resulting {@code Lookup} object is guaranteed
- * to have no more access capabilities than the original.
- * In particular, access capabilities can be lost as follows:<ul>
- * <li>If the new lookup class differs from the old one,
- * protected members will not be accessible by virtue of inheritance.
- * (Protected members may continue to be accessible because of package sharing.)
- * <li>If the new lookup class is in a different package
- * than the old one, protected and default (package) members will not be accessible.
- * <li>If the new lookup class is not within the same package member
- * as the old one, private members will not be accessible.
- * <li>If the new lookup class is not accessible to the old lookup class,
- * then no members, not even public members, will be accessible.
- * (In all other cases, public members will continue to be accessible.)
- * </ul>
- *
- * @param requestedLookupClass the desired lookup class for the new lookup object
- * @return a lookup object which reports the desired lookup class
- * @throws NullPointerException if the argument is null
- */
- public Lookup in(Class<?> requestedLookupClass) {
- requestedLookupClass.getClass(); // null check
- // Android-changed: There's no notion of a trusted lookup.
- // if (allowedModes == TRUSTED) // IMPL_LOOKUP can make any lookup at all
- // return new Lookup(requestedLookupClass, ALL_MODES);
+ public static final int PUBLIC = 0;
- if (requestedLookupClass == this.lookupClass)
- return this; // keep same capabilities
- int newModes = (allowedModes & (ALL_MODES & ~PROTECTED));
- if ((newModes & PACKAGE) != 0
- && !VerifyAccess.isSamePackage(this.lookupClass, requestedLookupClass)) {
- newModes &= ~(PACKAGE|PRIVATE);
- }
- // Allow nestmate lookups to be created without special privilege:
- if ((newModes & PRIVATE) != 0
- && !VerifyAccess.isSamePackageMember(this.lookupClass, requestedLookupClass)) {
- newModes &= ~PRIVATE;
- }
- if ((newModes & PUBLIC) != 0
- && !VerifyAccess.isClassAccessible(requestedLookupClass, this.lookupClass, allowedModes)) {
- // The requested class it not accessible from the lookup class.
- // No permissions.
- newModes = 0;
- }
- checkUnprivilegedlookupClass(requestedLookupClass, newModes);
- return new Lookup(requestedLookupClass, newModes);
- }
+ public static final int PRIVATE = 0;
- // Make sure outer class is initialized first.
- //
- // Android-changed: Removed unnecessary reference to IMPL_NAMES.
- // static { IMPL_NAMES.getClass(); }
+ public static final int PROTECTED = 0;
- /** Version of lookup which is trusted minimally.
- * It can only be used to create method handles to
- * publicly accessible members.
- */
- static final Lookup PUBLIC_LOOKUP = new Lookup(Object.class, PUBLIC);
+ public static final int PACKAGE = 0;
- /** Package-private version of lookup which is trusted. */
- static final Lookup IMPL_LOOKUP = new Lookup(Object.class, ALL_MODES);
+ public Class<?> lookupClass() { return null; }
- private static void checkUnprivilegedlookupClass(Class<?> lookupClass, int allowedModes) {
- String name = lookupClass.getName();
- if (name.startsWith("java.lang.invoke."))
- throw newIllegalArgumentException("illegal lookupClass: "+lookupClass);
+ public int lookupModes() { return 0; }
- // For caller-sensitive MethodHandles.lookup()
- // disallow lookup more restricted packages
- //
- // Android-changed: The bootstrap classloader isn't null.
- if (allowedModes == ALL_MODES &&
- lookupClass.getClassLoader() == Object.class.getClassLoader()) {
- if (name.startsWith("java.") ||
- (name.startsWith("sun.")
- && !name.startsWith("sun.invoke.")
- && !name.equals("sun.reflect.ReflectionFactory"))) {
- throw newIllegalArgumentException("illegal lookupClass: " + lookupClass);
- }
- }
- }
+ public Lookup in(Class<?> requestedLookupClass) { return null; }
- /**
- * Displays the name of the class from which lookups are to be made.
- * (The name is the one reported by {@link java.lang.Class#getName() Class.getName}.)
- * If there are restrictions on the access permitted to this lookup,
- * this is indicated by adding a suffix to the class name, consisting
- * of a slash and a keyword. The keyword represents the strongest
- * allowed access, and is chosen as follows:
- * <ul>
- * <li>If no access is allowed, the suffix is "/noaccess".
- * <li>If only public access is allowed, the suffix is "/public".
- * <li>If only public and package access are allowed, the suffix is "/package".
- * <li>If only public, package, and private access are allowed, the suffix is "/private".
- * </ul>
- * If none of the above cases apply, it is the case that full
- * access (public, package, private, and protected) is allowed.
- * In this case, no suffix is added.
- * This is true only of an object obtained originally from
- * {@link java.lang.invoke.MethodHandles#lookup MethodHandles.lookup}.
- * Objects created by {@link java.lang.invoke.MethodHandles.Lookup#in Lookup.in}
- * always have restricted access, and will display a suffix.
- * <p>
- * (It may seem strange that protected access should be
- * stronger than private access. Viewed independently from
- * package access, protected access is the first to be lost,
- * because it requires a direct subclass relationship between
- * caller and callee.)
- * @see #in
- */
- @Override
- public String toString() {
- String cname = lookupClass.getName();
- switch (allowedModes) {
- case 0: // no privileges
- return cname + "/noaccess";
- case PUBLIC:
- return cname + "/public";
- case PUBLIC|PACKAGE:
- return cname + "/package";
- case ALL_MODES & ~PROTECTED:
- return cname + "/private";
- case ALL_MODES:
- return cname;
- // Android-changed: No support for TRUSTED callers.
- // case TRUSTED:
- // return "/trusted"; // internal only; not exported
- default: // Should not happen, but it's a bitfield...
- cname = cname + "/" + Integer.toHexString(allowedModes);
- assert(false) : cname;
- return cname;
- }
- }
-
- /**
- * Produces a method handle for a static method.
- * The type of the method handle will be that of the method.
- * (Since static methods do not take receivers, there is no
- * additional receiver argument inserted into the method handle type,
- * as there would be with {@link #findVirtual findVirtual} or {@link #findSpecial findSpecial}.)
- * The method and all its argument types must be accessible to the lookup object.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the method's variable arity modifier bit ({@code 0x0080}) is set.
- * <p>
- * If the returned method handle is invoked, the method's class will
- * be initialized, if it has not already been initialized.
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle MH_asList = publicLookup().findStatic(Arrays.class,
- "asList", methodType(List.class, Object[].class));
-assertEquals("[x, y]", MH_asList.invoke("x", "y").toString());
- * }</pre></blockquote>
- * @param refc the class from which the method is accessed
- * @param name the name of the method
- * @param type the type of the method
- * @return the desired method handle
- * @throws NoSuchMethodException if the method does not exist
- * @throws IllegalAccessException if access checking fails,
- * or if the method is not {@code static},
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
public
- MethodHandle findStatic(Class<?> refc, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException {
- Method method = refc.getDeclaredMethod(name, type.ptypes());
- final int modifiers = method.getModifiers();
- if (!Modifier.isStatic(modifiers)) {
- throw new IllegalAccessException("Method" + method + " is not static");
- }
- checkReturnType(method, type);
- checkAccess(refc, method.getDeclaringClass(), modifiers, method.getName());
- return createMethodHandle(method, MethodHandle.INVOKE_STATIC, type);
- }
-
- private MethodHandle findVirtualForMH(String name, MethodType type) {
- // these names require special lookups because of the implicit MethodType argument
- if ("invoke".equals(name))
- return invoker(type);
- if ("invokeExact".equals(name))
- return exactInvoker(type);
- return null;
- }
-
- private static MethodHandle createMethodHandle(Method method, int handleKind,
- MethodType methodType) {
- MethodHandle mh = new MethodHandleImpl(method.getArtMethod(), handleKind, methodType);
- if (method.isVarArgs()) {
- return new Transformers.VarargsCollector(mh);
- } else {
- return mh;
- }
- }
-
- /**
- * Produces a method handle for a virtual method.
- * The type of the method handle will be that of the method,
- * with the receiver type (usually {@code refc}) prepended.
- * The method and all its argument types must be accessible to the lookup object.
- * <p>
- * When called, the handle will treat the first argument as a receiver
- * and dispatch on the receiver's type to determine which method
- * implementation to enter.
- * (The dispatching action is identical with that performed by an
- * {@code invokevirtual} or {@code invokeinterface} instruction.)
- * <p>
- * The first argument will be of type {@code refc} if the lookup
- * class has full privileges to access the member. Otherwise
- * the member must be {@code protected} and the first argument
- * will be restricted in type to the lookup class.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the method's variable arity modifier bit ({@code 0x0080}) is set.
- * <p>
- * Because of the general <a href="MethodHandles.Lookup.html#equiv">equivalence</a> between {@code invokevirtual}
- * instructions and method handles produced by {@code findVirtual},
- * if the class is {@code MethodHandle} and the name string is
- * {@code invokeExact} or {@code invoke}, the resulting
- * method handle is equivalent to one produced by
- * {@link java.lang.invoke.MethodHandles#exactInvoker MethodHandles.exactInvoker} or
- * {@link java.lang.invoke.MethodHandles#invoker MethodHandles.invoker}
- * with the same {@code type} argument.
- *
- * <b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle MH_concat = publicLookup().findVirtual(String.class,
- "concat", methodType(String.class, String.class));
-MethodHandle MH_hashCode = publicLookup().findVirtual(Object.class,
- "hashCode", methodType(int.class));
-MethodHandle MH_hashCode_String = publicLookup().findVirtual(String.class,
- "hashCode", methodType(int.class));
-assertEquals("xy", (String) MH_concat.invokeExact("x", "y"));
-assertEquals("xy".hashCode(), (int) MH_hashCode.invokeExact((Object)"xy"));
-assertEquals("xy".hashCode(), (int) MH_hashCode_String.invokeExact("xy"));
-// interface method:
-MethodHandle MH_subSequence = publicLookup().findVirtual(CharSequence.class,
- "subSequence", methodType(CharSequence.class, int.class, int.class));
-assertEquals("def", MH_subSequence.invoke("abcdefghi", 3, 6).toString());
-// constructor "internal method" must be accessed differently:
-MethodType MT_newString = methodType(void.class); //()V for new String()
-try { assertEquals("impossible", lookup()
- .findVirtual(String.class, "<init>", MT_newString));
- } catch (NoSuchMethodException ex) { } // OK
-MethodHandle MH_newString = publicLookup()
- .findConstructor(String.class, MT_newString);
-assertEquals("", (String) MH_newString.invokeExact());
- * }</pre></blockquote>
- *
- * @param refc the class or interface from which the method is accessed
- * @param name the name of the method
- * @param type the type of the method, with the receiver argument omitted
- * @return the desired method handle
- * @throws NoSuchMethodException if the method does not exist
- * @throws IllegalAccessException if access checking fails,
- * or if the method is {@code static}
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle findVirtual(Class<?> refc, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException {
- // Special case : when we're looking up a virtual method on the MethodHandles class
- // itself, we can return one of our specialized invokers.
- if (refc == MethodHandle.class) {
- MethodHandle mh = findVirtualForMH(name, type);
- if (mh != null) {
- return mh;
- }
- }
- // BEGIN Android-changed: Added VarHandle case here.
- // Implementation to follow. TODO(b/65872996)
- if (refc == VarHandle.class) {
- unsupported("MethodHandles.findVirtual with refc == VarHandle.class");
- return null;
- }
- // END Android-changed: Added VarHandle handling here.
-
- Method method = refc.getInstanceMethod(name, type.ptypes());
- if (method == null) {
- // This is pretty ugly and a consequence of the MethodHandles API. We have to throw
- // an IAE and not an NSME if the method exists but is static (even though the RI's
- // IAE has a message that says "no such method"). We confine the ugliness and
- // slowness to the failure case, and allow getInstanceMethod to remain fairly
- // general.
- try {
- Method m = refc.getDeclaredMethod(name, type.ptypes());
- if (Modifier.isStatic(m.getModifiers())) {
- throw new IllegalAccessException("Method" + m + " is static");
- }
- } catch (NoSuchMethodException ignored) {
- }
-
- throw new NoSuchMethodException(name + " " + Arrays.toString(type.ptypes()));
- }
- checkReturnType(method, type);
-
- // We have a valid method, perform access checks.
- checkAccess(refc, method.getDeclaringClass(), method.getModifiers(), method.getName());
-
- // Insert the leading reference parameter.
- MethodType handleType = type.insertParameterTypes(0, refc);
- return createMethodHandle(method, MethodHandle.INVOKE_VIRTUAL, handleType);
- }
-
- /**
- * Produces a method handle which creates an object and initializes it, using
- * the constructor of the specified type.
- * The parameter types of the method handle will be those of the constructor,
- * while the return type will be a reference to the constructor's class.
- * The constructor and all its argument types must be accessible to the lookup object.
- * <p>
- * The requested type must have a return type of {@code void}.
- * (This is consistent with the JVM's treatment of constructor type descriptors.)
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the constructor's variable arity modifier bit ({@code 0x0080}) is set.
- * <p>
- * If the returned method handle is invoked, the constructor's class will
- * be initialized, if it has not already been initialized.
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle MH_newArrayList = publicLookup().findConstructor(
- ArrayList.class, methodType(void.class, Collection.class));
-Collection orig = Arrays.asList("x", "y");
-Collection copy = (ArrayList) MH_newArrayList.invokeExact(orig);
-assert(orig != copy);
-assertEquals(orig, copy);
-// a variable-arity constructor:
-MethodHandle MH_newProcessBuilder = publicLookup().findConstructor(
- ProcessBuilder.class, methodType(void.class, String[].class));
-ProcessBuilder pb = (ProcessBuilder)
- MH_newProcessBuilder.invoke("x", "y", "z");
-assertEquals("[x, y, z]", pb.command().toString());
- * }</pre></blockquote>
- * @param refc the class or interface from which the method is accessed
- * @param type the type of the method, with the receiver argument omitted, and a void return type
- * @return the desired method handle
- * @throws NoSuchMethodException if the constructor does not exist
- * @throws IllegalAccessException if access checking fails
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle findConstructor(Class<?> refc, MethodType type) throws NoSuchMethodException, IllegalAccessException {
- if (refc.isArray()) {
- throw new NoSuchMethodException("no constructor for array class: " + refc.getName());
- }
- // The queried |type| is (PT1,PT2,..)V
- Constructor constructor = refc.getDeclaredConstructor(type.ptypes());
- if (constructor == null) {
- throw new NoSuchMethodException(
- "No constructor for " + constructor.getDeclaringClass() + " matching " + type);
- }
- checkAccess(refc, constructor.getDeclaringClass(), constructor.getModifiers(),
- constructor.getName());
-
- return createMethodHandleForConstructor(constructor);
- }
-
- private MethodHandle createMethodHandleForConstructor(Constructor constructor) {
- Class<?> refc = constructor.getDeclaringClass();
- MethodType constructorType =
- MethodType.methodType(refc, constructor.getParameterTypes());
- MethodHandle mh;
- if (refc == String.class) {
- // String constructors have optimized StringFactory methods
- // that matches returned type. These factory methods combine the
- // memory allocation and initialization calls for String objects.
- mh = new MethodHandleImpl(constructor.getArtMethod(), MethodHandle.INVOKE_DIRECT,
- constructorType);
- } else {
- // Constructors for all other classes use a Construct transformer to perform
- // their memory allocation and call to <init>.
- MethodType initType = initMethodType(constructorType);
- MethodHandle initHandle = new MethodHandleImpl(
- constructor.getArtMethod(), MethodHandle.INVOKE_DIRECT, initType);
- mh = new Transformers.Construct(initHandle, constructorType);
- }
+ MethodHandle findStatic(Class<?> refc, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException { return null; }
- if (constructor.isVarArgs()) {
- mh = new Transformers.VarargsCollector(mh);
- }
- return mh;
- }
+ public MethodHandle findVirtual(Class<?> refc, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException { return null; }
- private static MethodType initMethodType(MethodType constructorType) {
- // Returns a MethodType appropriate for class <init>
- // methods. Constructor MethodTypes have the form
- // (PT1,PT2,...)C and class <init> MethodTypes have the
- // form (C,PT1,PT2,...)V.
- assert constructorType.rtype() != void.class;
+ public MethodHandle findConstructor(Class<?> refc, MethodType type) throws NoSuchMethodException, IllegalAccessException { return null; }
- // Insert constructorType C as the first parameter type in
- // the MethodType for <init>.
- Class<?> [] initPtypes = new Class<?> [constructorType.ptypes().length + 1];
- initPtypes[0] = constructorType.rtype();
- System.arraycopy(constructorType.ptypes(), 0, initPtypes, 1,
- constructorType.ptypes().length);
-
- // Set the return type for the <init> MethodType to be void.
- return MethodType.methodType(void.class, initPtypes);
- }
-
- /**
- * Produces an early-bound method handle for a virtual method.
- * It will bypass checks for overriding methods on the receiver,
- * <a href="MethodHandles.Lookup.html#equiv">as if called</a> from an {@code invokespecial}
- * instruction from within the explicitly specified {@code specialCaller}.
- * The type of the method handle will be that of the method,
- * with a suitably restricted receiver type prepended.
- * (The receiver type will be {@code specialCaller} or a subtype.)
- * The method and all its argument types must be accessible
- * to the lookup object.
- * <p>
- * Before method resolution,
- * if the explicitly specified caller class is not identical with the
- * lookup class, or if this lookup object does not have
- * <a href="MethodHandles.Lookup.html#privacc">private access</a>
- * privileges, the access fails.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the method's variable arity modifier bit ({@code 0x0080}) is set.
- * <p style="font-size:smaller;">
- * <em>(Note: JVM internal methods named {@code "<init>"} are not visible to this API,
- * even though the {@code invokespecial} instruction can refer to them
- * in special circumstances. Use {@link #findConstructor findConstructor}
- * to access instance initialization methods in a safe manner.)</em>
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-static class Listie extends ArrayList {
- public String toString() { return "[wee Listie]"; }
- static Lookup lookup() { return MethodHandles.lookup(); }
-}
-...
-// no access to constructor via invokeSpecial:
-MethodHandle MH_newListie = Listie.lookup()
- .findConstructor(Listie.class, methodType(void.class));
-Listie l = (Listie) MH_newListie.invokeExact();
-try { assertEquals("impossible", Listie.lookup().findSpecial(
- Listie.class, "<init>", methodType(void.class), Listie.class));
- } catch (NoSuchMethodException ex) { } // OK
-// access to super and self methods via invokeSpecial:
-MethodHandle MH_super = Listie.lookup().findSpecial(
- ArrayList.class, "toString" , methodType(String.class), Listie.class);
-MethodHandle MH_this = Listie.lookup().findSpecial(
- Listie.class, "toString" , methodType(String.class), Listie.class);
-MethodHandle MH_duper = Listie.lookup().findSpecial(
- Object.class, "toString" , methodType(String.class), Listie.class);
-assertEquals("[]", (String) MH_super.invokeExact(l));
-assertEquals(""+l, (String) MH_this.invokeExact(l));
-assertEquals("[]", (String) MH_duper.invokeExact(l)); // ArrayList method
-try { assertEquals("inaccessible", Listie.lookup().findSpecial(
- String.class, "toString", methodType(String.class), Listie.class));
- } catch (IllegalAccessException ex) { } // OK
-Listie subl = new Listie() { public String toString() { return "[subclass]"; } };
-assertEquals(""+l, (String) MH_this.invokeExact(subl)); // Listie method
- * }</pre></blockquote>
- *
- * @param refc the class or interface from which the method is accessed
- * @param name the name of the method (which must not be "&lt;init&gt;")
- * @param type the type of the method, with the receiver argument omitted
- * @param specialCaller the proposed calling class to perform the {@code invokespecial}
- * @return the desired method handle
- * @throws NoSuchMethodException if the method does not exist
- * @throws IllegalAccessException if access checking fails
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
public MethodHandle findSpecial(Class<?> refc, String name, MethodType type,
- Class<?> specialCaller) throws NoSuchMethodException, IllegalAccessException {
- if (specialCaller == null) {
- throw new NullPointerException("specialCaller == null");
- }
-
- if (type == null) {
- throw new NullPointerException("type == null");
- }
-
- if (name == null) {
- throw new NullPointerException("name == null");
- }
-
- if (refc == null) {
- throw new NullPointerException("ref == null");
- }
-
- // Make sure that the special caller is identical to the lookup class or that we have
- // private access.
- checkSpecialCaller(specialCaller);
-
- // Even though constructors are invoked using a "special" invoke, handles to them can't
- // be created using findSpecial. Callers must use findConstructor instead. Similarly,
- // there is no path for calling static class initializers.
- if (name.startsWith("<")) {
- throw new NoSuchMethodException(name + " is not a valid method name.");
- }
-
- Method method = refc.getDeclaredMethod(name, type.ptypes());
- checkReturnType(method, type);
- return findSpecial(method, type, refc, specialCaller);
- }
-
- private MethodHandle findSpecial(Method method, MethodType type,
- Class<?> refc, Class<?> specialCaller)
- throws IllegalAccessException {
- if (Modifier.isStatic(method.getModifiers())) {
- throw new IllegalAccessException("expected a non-static method:" + method);
- }
-
- if (Modifier.isPrivate(method.getModifiers())) {
- // Since this is a private method, we'll need to also make sure that the
- // lookup class is the same as the refering class. We've already checked that
- // the specialCaller is the same as the special lookup class, both of these must
- // be the same as the declaring class(*) in order to access the private method.
- //
- // (*) Well, this isn't true for nested classes but OpenJDK doesn't support those
- // either.
- if (refc != lookupClass()) {
- throw new IllegalAccessException("no private access for invokespecial : "
- + refc + ", from" + this);
- }
+ Class<?> specialCaller) throws NoSuchMethodException, IllegalAccessException { return null; }
- // This is a private method, so there's nothing special to do.
- MethodType handleType = type.insertParameterTypes(0, refc);
- return createMethodHandle(method, MethodHandle.INVOKE_DIRECT, handleType);
- }
+ public MethodHandle findGetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { return null; }
- // This is a public, protected or package-private method, which means we're expecting
- // invoke-super semantics. We'll have to restrict the receiver type appropriately on the
- // handle once we check that there really is a "super" relationship between them.
- if (!method.getDeclaringClass().isAssignableFrom(specialCaller)) {
- throw new IllegalAccessException(refc + "is not assignable from " + specialCaller);
- }
+ public MethodHandle findSetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { return null; }
- // Note that we restrict the receiver to "specialCaller" instances.
- MethodType handleType = type.insertParameterTypes(0, specialCaller);
- return createMethodHandle(method, MethodHandle.INVOKE_SUPER, handleType);
- }
+ public MethodHandle findStaticGetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { return null; }
- /**
- * Produces a method handle giving read access to a non-static field.
- * The type of the method handle will have a return type of the field's
- * value type.
- * The method handle's single argument will be the instance containing
- * the field.
- * Access checking is performed immediately on behalf of the lookup class.
- * @param refc the class or interface from which the method is accessed
- * @param name the field's name
- * @param type the field's type
- * @return a method handle which can load values from the field
- * @throws NoSuchFieldException if the field does not exist
- * @throws IllegalAccessException if access checking fails, or if the field is {@code static}
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle findGetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
- return findAccessor(refc, name, type, MethodHandle.IGET);
- }
+ public MethodHandle findStaticSetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException { return null; }
- private MethodHandle findAccessor(Class<?> refc, String name, Class<?> type, int kind)
- throws NoSuchFieldException, IllegalAccessException {
- final Field field = findFieldOfType(refc, name, type);
- return findAccessor(field, refc, type, kind, true /* performAccessChecks */);
- }
+ public MethodHandle bind(Object receiver, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException { return null; }
- private MethodHandle findAccessor(Field field, Class<?> refc, Class<?> type, int kind,
- boolean performAccessChecks)
- throws IllegalAccessException {
- final boolean isSetterKind = kind == MethodHandle.IPUT || kind == MethodHandle.SPUT;
- final boolean isStaticKind = kind == MethodHandle.SGET || kind == MethodHandle.SPUT;
- commonFieldChecks(field, refc, type, isStaticKind, performAccessChecks);
- if (performAccessChecks) {
- final int modifiers = field.getModifiers();
- if (isSetterKind && Modifier.isFinal(modifiers)) {
- throw new IllegalAccessException("Field " + field + " is final");
- }
- }
+ public MethodHandle unreflect(Method m) throws IllegalAccessException { return null; }
- final MethodType methodType;
- switch (kind) {
- case MethodHandle.SGET:
- methodType = MethodType.methodType(type);
- break;
- case MethodHandle.SPUT:
- methodType = MethodType.methodType(void.class, type);
- break;
- case MethodHandle.IGET:
- methodType = MethodType.methodType(type, refc);
- break;
- case MethodHandle.IPUT:
- methodType = MethodType.methodType(void.class, refc, type);
- break;
- default:
- throw new IllegalArgumentException("Invalid kind " + kind);
- }
- return new MethodHandleImpl(field.getArtField(), kind, methodType);
- }
+ public MethodHandle unreflectSpecial(Method m, Class<?> specialCaller) throws IllegalAccessException { return null; }
- /**
- * Produces a method handle giving write access to a non-static field.
- * The type of the method handle will have a void return type.
- * The method handle will take two arguments, the instance containing
- * the field, and the value to be stored.
- * The second argument will be of the field's value type.
- * Access checking is performed immediately on behalf of the lookup class.
- * @param refc the class or interface from which the method is accessed
- * @param name the field's name
- * @param type the field's type
- * @return a method handle which can store values into the field
- * @throws NoSuchFieldException if the field does not exist
- * @throws IllegalAccessException if access checking fails, or if the field is {@code static}
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle findSetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
- return findAccessor(refc, name, type, MethodHandle.IPUT);
- }
+ public MethodHandle unreflectConstructor(Constructor<?> c) throws IllegalAccessException { return null; }
- // BEGIN Android-changed: OpenJDK 9+181 VarHandle API factory method.
- /**
- * Produces a VarHandle giving access to a non-static field {@code name}
- * of type {@code type} declared in a class of type {@code recv}.
- * The VarHandle's variable type is {@code type} and it has one
- * coordinate type, {@code recv}.
- * <p>
- * Access checking is performed immediately on behalf of the lookup
- * class.
- * <p>
- * Certain access modes of the returned VarHandle are unsupported under
- * the following conditions:
- * <ul>
- * <li>if the field is declared {@code final}, then the write, atomic
- * update, numeric atomic update, and bitwise atomic update access
- * modes are unsupported.
- * <li>if the field type is anything other than {@code byte},
- * {@code short}, {@code char}, {@code int}, {@code long},
- * {@code float}, or {@code double} then numeric atomic update
- * access modes are unsupported.
- * <li>if the field type is anything other than {@code boolean},
- * {@code byte}, {@code short}, {@code char}, {@code int} or
- * {@code long} then bitwise atomic update access modes are
- * unsupported.
- * </ul>
- * <p>
- * If the field is declared {@code volatile} then the returned VarHandle
- * will override access to the field (effectively ignore the
- * {@code volatile} declaration) in accordance to its specified
- * access modes.
- * <p>
- * If the field type is {@code float} or {@code double} then numeric
- * and atomic update access modes compare values using their bitwise
- * representation (see {@link Float#floatToRawIntBits} and
- * {@link Double#doubleToRawLongBits}, respectively).
- * @apiNote
- * Bitwise comparison of {@code float} values or {@code double} values,
- * as performed by the numeric and atomic update access modes, differ
- * from the primitive {@code ==} operator and the {@link Float#equals}
- * and {@link Double#equals} methods, specifically with respect to
- * comparing NaN values or comparing {@code -0.0} with {@code +0.0}.
- * Care should be taken when performing a compare and set or a compare
- * and exchange operation with such values since the operation may
- * unexpectedly fail.
- * There are many possible NaN values that are considered to be
- * {@code NaN} in Java, although no IEEE 754 floating-point operation
- * provided by Java can distinguish between them. Operation failure can
- * occur if the expected or witness value is a NaN value and it is
- * transformed (perhaps in a platform specific manner) into another NaN
- * value, and thus has a different bitwise representation (see
- * {@link Float#intBitsToFloat} or {@link Double#longBitsToDouble} for more
- * details).
- * The values {@code -0.0} and {@code +0.0} have different bitwise
- * representations but are considered equal when using the primitive
- * {@code ==} operator. Operation failure can occur if, for example, a
- * numeric algorithm computes an expected value to be say {@code -0.0}
- * and previously computed the witness value to be say {@code +0.0}.
- * @param recv the receiver class, of type {@code R}, that declares the
- * non-static field
- * @param name the field's name
- * @param type the field's type, of type {@code T}
- * @return a VarHandle giving access to non-static fields.
- * @throws NoSuchFieldException if the field does not exist
- * @throws IllegalAccessException if access checking fails, or if the field is {@code static}
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- * @since 9
- * @hide
- */
- public VarHandle findVarHandle(Class<?> recv, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
- final Field field = findFieldOfType(recv, name, type);
- final boolean isStatic = false;
- final boolean performAccessChecks = true;
- commonFieldChecks(field, recv, type, isStatic, performAccessChecks);
- return FieldVarHandle.create(field);
- }
- // END Android-changed: OpenJDK 9+181 VarHandle API factory method.
+ public MethodHandle unreflectGetter(Field f) throws IllegalAccessException { return null; }
- // BEGIN Android-added: Common field resolution and access check methods.
- private Field findFieldOfType(final Class<?> refc, String name, Class<?> type)
- throws NoSuchFieldException {
- Field field = null;
+ public MethodHandle unreflectSetter(Field f) throws IllegalAccessException { return null; }
- // Search refc and super classes for the field.
- for (Class<?> cls = refc; cls != null; cls = cls.getSuperclass()) {
- try {
- field = cls.getDeclaredField(name);
- break;
- } catch (NoSuchFieldException e) {
- }
- }
-
- if (field == null) {
- // Force failure citing refc.
- field = refc.getDeclaredField(name);
- }
-
- final Class<?> fieldType = field.getType();
- if (fieldType != type) {
- throw new NoSuchFieldException(name);
- }
- return field;
- }
-
- private void commonFieldChecks(Field field, Class<?> refc, Class<?> type,
- boolean isStatic, boolean performAccessChecks)
- throws IllegalAccessException {
- final int modifiers = field.getModifiers();
- if (performAccessChecks) {
- checkAccess(refc, field.getDeclaringClass(), modifiers, field.getName());
- }
- if (Modifier.isStatic(modifiers) != isStatic) {
- String reason = "Field " + field + " is " +
- (isStatic ? "not " : "") + "static";
- throw new IllegalAccessException(reason);
- }
- }
- // END Android-added: Common field resolution and access check methods.
-
- /**
- * Produces a method handle giving read access to a static field.
- * The type of the method handle will have a return type of the field's
- * value type.
- * The method handle will take no arguments.
- * Access checking is performed immediately on behalf of the lookup class.
- * <p>
- * If the returned method handle is invoked, the field's class will
- * be initialized, if it has not already been initialized.
- * @param refc the class or interface from which the method is accessed
- * @param name the field's name
- * @param type the field's type
- * @return a method handle which can load values from the field
- * @throws NoSuchFieldException if the field does not exist
- * @throws IllegalAccessException if access checking fails, or if the field is not {@code static}
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle findStaticGetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
- return findAccessor(refc, name, type, MethodHandle.SGET);
- }
-
- /**
- * Produces a method handle giving write access to a static field.
- * The type of the method handle will have a void return type.
- * The method handle will take a single
- * argument, of the field's value type, the value to be stored.
- * Access checking is performed immediately on behalf of the lookup class.
- * <p>
- * If the returned method handle is invoked, the field's class will
- * be initialized, if it has not already been initialized.
- * @param refc the class or interface from which the method is accessed
- * @param name the field's name
- * @param type the field's type
- * @return a method handle which can store values into the field
- * @throws NoSuchFieldException if the field does not exist
- * @throws IllegalAccessException if access checking fails, or if the field is not {@code static}
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle findStaticSetter(Class<?> refc, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
- return findAccessor(refc, name, type, MethodHandle.SPUT);
- }
-
- // BEGIN Android-changed: OpenJDK 9+181 VarHandle API factory method.
- /**
- * Produces a VarHandle giving access to a static field {@code name} of
- * type {@code type} declared in a class of type {@code decl}.
- * The VarHandle's variable type is {@code type} and it has no
- * coordinate types.
- * <p>
- * Access checking is performed immediately on behalf of the lookup
- * class.
- * <p>
- * If the returned VarHandle is operated on, the declaring class will be
- * initialized, if it has not already been initialized.
- * <p>
- * Certain access modes of the returned VarHandle are unsupported under
- * the following conditions:
- * <ul>
- * <li>if the field is declared {@code final}, then the write, atomic
- * update, numeric atomic update, and bitwise atomic update access
- * modes are unsupported.
- * <li>if the field type is anything other than {@code byte},
- * {@code short}, {@code char}, {@code int}, {@code long},
- * {@code float}, or {@code double}, then numeric atomic update
- * access modes are unsupported.
- * <li>if the field type is anything other than {@code boolean},
- * {@code byte}, {@code short}, {@code char}, {@code int} or
- * {@code long} then bitwise atomic update access modes are
- * unsupported.
- * </ul>
- * <p>
- * If the field is declared {@code volatile} then the returned VarHandle
- * will override access to the field (effectively ignore the
- * {@code volatile} declaration) in accordance to its specified
- * access modes.
- * <p>
- * If the field type is {@code float} or {@code double} then numeric
- * and atomic update access modes compare values using their bitwise
- * representation (see {@link Float#floatToRawIntBits} and
- * {@link Double#doubleToRawLongBits}, respectively).
- * @apiNote
- * Bitwise comparison of {@code float} values or {@code double} values,
- * as performed by the numeric and atomic update access modes, differ
- * from the primitive {@code ==} operator and the {@link Float#equals}
- * and {@link Double#equals} methods, specifically with respect to
- * comparing NaN values or comparing {@code -0.0} with {@code +0.0}.
- * Care should be taken when performing a compare and set or a compare
- * and exchange operation with such values since the operation may
- * unexpectedly fail.
- * There are many possible NaN values that are considered to be
- * {@code NaN} in Java, although no IEEE 754 floating-point operation
- * provided by Java can distinguish between them. Operation failure can
- * occur if the expected or witness value is a NaN value and it is
- * transformed (perhaps in a platform specific manner) into another NaN
- * value, and thus has a different bitwise representation (see
- * {@link Float#intBitsToFloat} or {@link Double#longBitsToDouble} for more
- * details).
- * The values {@code -0.0} and {@code +0.0} have different bitwise
- * representations but are considered equal when using the primitive
- * {@code ==} operator. Operation failure can occur if, for example, a
- * numeric algorithm computes an expected value to be say {@code -0.0}
- * and previously computed the witness value to be say {@code +0.0}.
- * @param decl the class that declares the static field
- * @param name the field's name
- * @param type the field's type, of type {@code T}
- * @return a VarHandle giving access to a static field
- * @throws NoSuchFieldException if the field does not exist
- * @throws IllegalAccessException if access checking fails, or if the field is not {@code static}
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- * @since 9
- * @hide
- */
- public VarHandle findStaticVarHandle(Class<?> decl, String name, Class<?> type) throws NoSuchFieldException, IllegalAccessException {
- final Field field = findFieldOfType(decl, name, type);
- final boolean isStatic = true;
- final boolean performAccessChecks = true;
- commonFieldChecks(field, decl, type, isStatic, performAccessChecks);
- return FieldVarHandle.create(field);
- }
- // END Android-changed: OpenJDK 9+181 VarHandle API factory method.
-
- /**
- * Produces an early-bound method handle for a non-static method.
- * The receiver must have a supertype {@code defc} in which a method
- * of the given name and type is accessible to the lookup class.
- * The method and all its argument types must be accessible to the lookup object.
- * The type of the method handle will be that of the method,
- * without any insertion of an additional receiver parameter.
- * The given receiver will be bound into the method handle,
- * so that every call to the method handle will invoke the
- * requested method on the given receiver.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the method's variable arity modifier bit ({@code 0x0080}) is set
- * <em>and</em> the trailing array argument is not the only argument.
- * (If the trailing array argument is the only argument,
- * the given receiver value will be bound to it.)
- * <p>
- * This is equivalent to the following code:
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle mh0 = lookup().findVirtual(defc, name, type);
-MethodHandle mh1 = mh0.bindTo(receiver);
-MethodType mt1 = mh1.type();
-if (mh0.isVarargsCollector())
- mh1 = mh1.asVarargsCollector(mt1.parameterType(mt1.parameterCount()-1));
-return mh1;
- * }</pre></blockquote>
- * where {@code defc} is either {@code receiver.getClass()} or a super
- * type of that class, in which the requested method is accessible
- * to the lookup class.
- * (Note that {@code bindTo} does not preserve variable arity.)
- * @param receiver the object from which the method is accessed
- * @param name the name of the method
- * @param type the type of the method, with the receiver argument omitted
- * @return the desired method handle
- * @throws NoSuchMethodException if the method does not exist
- * @throws IllegalAccessException if access checking fails
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws NullPointerException if any argument is null
- * @see MethodHandle#bindTo
- * @see #findVirtual
- */
- public MethodHandle bind(Object receiver, String name, MethodType type) throws NoSuchMethodException, IllegalAccessException {
- MethodHandle handle = findVirtual(receiver.getClass(), name, type);
- MethodHandle adapter = handle.bindTo(receiver);
- MethodType adapterType = adapter.type();
- if (handle.isVarargsCollector()) {
- adapter = adapter.asVarargsCollector(
- adapterType.parameterType(adapterType.parameterCount() - 1));
- }
-
- return adapter;
- }
-
- /**
- * Makes a <a href="MethodHandleInfo.html#directmh">direct method handle</a>
- * to <i>m</i>, if the lookup class has permission.
- * If <i>m</i> is non-static, the receiver argument is treated as an initial argument.
- * If <i>m</i> is virtual, overriding is respected on every call.
- * Unlike the Core Reflection API, exceptions are <em>not</em> wrapped.
- * The type of the method handle will be that of the method,
- * with the receiver type prepended (but only if it is non-static).
- * If the method's {@code accessible} flag is not set,
- * access checking is performed immediately on behalf of the lookup class.
- * If <i>m</i> is not public, do not share the resulting handle with untrusted parties.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the method's variable arity modifier bit ({@code 0x0080}) is set.
- * <p>
- * If <i>m</i> is static, and
- * if the returned method handle is invoked, the method's class will
- * be initialized, if it has not already been initialized.
- * @param m the reflected method
- * @return a method handle which can invoke the reflected method
- * @throws IllegalAccessException if access checking fails
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @throws NullPointerException if the argument is null
- */
- public MethodHandle unreflect(Method m) throws IllegalAccessException {
- if (m == null) {
- throw new NullPointerException("m == null");
- }
-
- MethodType methodType = MethodType.methodType(m.getReturnType(),
- m.getParameterTypes());
-
- // We should only perform access checks if setAccessible hasn't been called yet.
- if (!m.isAccessible()) {
- checkAccess(m.getDeclaringClass(), m.getDeclaringClass(), m.getModifiers(),
- m.getName());
- }
-
- if (Modifier.isStatic(m.getModifiers())) {
- return createMethodHandle(m, MethodHandle.INVOKE_STATIC, methodType);
- } else {
- methodType = methodType.insertParameterTypes(0, m.getDeclaringClass());
- return createMethodHandle(m, MethodHandle.INVOKE_VIRTUAL, methodType);
- }
- }
-
- /**
- * Produces a method handle for a reflected method.
- * It will bypass checks for overriding methods on the receiver,
- * <a href="MethodHandles.Lookup.html#equiv">as if called</a> from an {@code invokespecial}
- * instruction from within the explicitly specified {@code specialCaller}.
- * The type of the method handle will be that of the method,
- * with a suitably restricted receiver type prepended.
- * (The receiver type will be {@code specialCaller} or a subtype.)
- * If the method's {@code accessible} flag is not set,
- * access checking is performed immediately on behalf of the lookup class,
- * as if {@code invokespecial} instruction were being linked.
- * <p>
- * Before method resolution,
- * if the explicitly specified caller class is not identical with the
- * lookup class, or if this lookup object does not have
- * <a href="MethodHandles.Lookup.html#privacc">private access</a>
- * privileges, the access fails.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the method's variable arity modifier bit ({@code 0x0080}) is set.
- * @param m the reflected method
- * @param specialCaller the class nominally calling the method
- * @return a method handle which can invoke the reflected method
- * @throws IllegalAccessException if access checking fails
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @throws NullPointerException if any argument is null
- */
- public MethodHandle unreflectSpecial(Method m, Class<?> specialCaller) throws IllegalAccessException {
- if (m == null) {
- throw new NullPointerException("m == null");
- }
-
- if (specialCaller == null) {
- throw new NullPointerException("specialCaller == null");
- }
-
- if (!m.isAccessible()) {
- checkSpecialCaller(specialCaller);
- }
-
- final MethodType methodType = MethodType.methodType(m.getReturnType(),
- m.getParameterTypes());
- return findSpecial(m, methodType, m.getDeclaringClass() /* refc */, specialCaller);
- }
-
- /**
- * Produces a method handle for a reflected constructor.
- * The type of the method handle will be that of the constructor,
- * with the return type changed to the declaring class.
- * The method handle will perform a {@code newInstance} operation,
- * creating a new instance of the constructor's class on the
- * arguments passed to the method handle.
- * <p>
- * If the constructor's {@code accessible} flag is not set,
- * access checking is performed immediately on behalf of the lookup class.
- * <p>
- * The returned method handle will have
- * {@linkplain MethodHandle#asVarargsCollector variable arity} if and only if
- * the constructor's variable arity modifier bit ({@code 0x0080}) is set.
- * <p>
- * If the returned method handle is invoked, the constructor's class will
- * be initialized, if it has not already been initialized.
- * @param c the reflected constructor
- * @return a method handle which can invoke the reflected constructor
- * @throws IllegalAccessException if access checking fails
- * or if the method's variable arity modifier bit
- * is set and {@code asVarargsCollector} fails
- * @throws NullPointerException if the argument is null
- */
- public MethodHandle unreflectConstructor(Constructor<?> c) throws IllegalAccessException {
- if (c == null) {
- throw new NullPointerException("c == null");
- }
-
- if (!c.isAccessible()) {
- checkAccess(c.getDeclaringClass(), c.getDeclaringClass(), c.getModifiers(),
- c.getName());
- }
-
- return createMethodHandleForConstructor(c);
- }
-
- /**
- * Produces a method handle giving read access to a reflected field.
- * The type of the method handle will have a return type of the field's
- * value type.
- * If the field is static, the method handle will take no arguments.
- * Otherwise, its single argument will be the instance containing
- * the field.
- * If the field's {@code accessible} flag is not set,
- * access checking is performed immediately on behalf of the lookup class.
- * <p>
- * If the field is static, and
- * if the returned method handle is invoked, the field's class will
- * be initialized, if it has not already been initialized.
- * @param f the reflected field
- * @return a method handle which can load values from the reflected field
- * @throws IllegalAccessException if access checking fails
- * @throws NullPointerException if the argument is null
- */
- public MethodHandle unreflectGetter(Field f) throws IllegalAccessException {
- return findAccessor(f, f.getDeclaringClass(), f.getType(),
- Modifier.isStatic(f.getModifiers()) ? MethodHandle.SGET : MethodHandle.IGET,
- !f.isAccessible() /* performAccessChecks */);
- }
-
- /**
- * Produces a method handle giving write access to a reflected field.
- * The type of the method handle will have a void return type.
- * If the field is static, the method handle will take a single
- * argument, of the field's value type, the value to be stored.
- * Otherwise, the two arguments will be the instance containing
- * the field, and the value to be stored.
- * If the field's {@code accessible} flag is not set,
- * access checking is performed immediately on behalf of the lookup class.
- * <p>
- * If the field is static, and
- * if the returned method handle is invoked, the field's class will
- * be initialized, if it has not already been initialized.
- * @param f the reflected field
- * @return a method handle which can store values into the reflected field
- * @throws IllegalAccessException if access checking fails
- * @throws NullPointerException if the argument is null
- */
- public MethodHandle unreflectSetter(Field f) throws IllegalAccessException {
- return findAccessor(f, f.getDeclaringClass(), f.getType(),
- Modifier.isStatic(f.getModifiers()) ? MethodHandle.SPUT : MethodHandle.IPUT,
- !f.isAccessible() /* performAccessChecks */);
- }
-
- // BEGIN Android-changed: OpenJDK 9+181 VarHandle API factory method.
- /**
- * Produces a VarHandle giving access to a reflected field {@code f}
- * of type {@code T} declared in a class of type {@code R}.
- * The VarHandle's variable type is {@code T}.
- * If the field is non-static the VarHandle has one coordinate type,
- * {@code R}. Otherwise, the field is static, and the VarHandle has no
- * coordinate types.
- * <p>
- * Access checking is performed immediately on behalf of the lookup
- * class, regardless of the value of the field's {@code accessible}
- * flag.
- * <p>
- * If the field is static, and if the returned VarHandle is operated
- * on, the field's declaring class will be initialized, if it has not
- * already been initialized.
- * <p>
- * Certain access modes of the returned VarHandle are unsupported under
- * the following conditions:
- * <ul>
- * <li>if the field is declared {@code final}, then the write, atomic
- * update, numeric atomic update, and bitwise atomic update access
- * modes are unsupported.
- * <li>if the field type is anything other than {@code byte},
- * {@code short}, {@code char}, {@code int}, {@code long},
- * {@code float}, or {@code double} then numeric atomic update
- * access modes are unsupported.
- * <li>if the field type is anything other than {@code boolean},
- * {@code byte}, {@code short}, {@code char}, {@code int} or
- * {@code long} then bitwise atomic update access modes are
- * unsupported.
- * </ul>
- * <p>
- * If the field is declared {@code volatile} then the returned VarHandle
- * will override access to the field (effectively ignore the
- * {@code volatile} declaration) in accordance to its specified
- * access modes.
- * <p>
- * If the field type is {@code float} or {@code double} then numeric
- * and atomic update access modes compare values using their bitwise
- * representation (see {@link Float#floatToRawIntBits} and
- * {@link Double#doubleToRawLongBits}, respectively).
- * @apiNote
- * Bitwise comparison of {@code float} values or {@code double} values,
- * as performed by the numeric and atomic update access modes, differ
- * from the primitive {@code ==} operator and the {@link Float#equals}
- * and {@link Double#equals} methods, specifically with respect to
- * comparing NaN values or comparing {@code -0.0} with {@code +0.0}.
- * Care should be taken when performing a compare and set or a compare
- * and exchange operation with such values since the operation may
- * unexpectedly fail.
- * There are many possible NaN values that are considered to be
- * {@code NaN} in Java, although no IEEE 754 floating-point operation
- * provided by Java can distinguish between them. Operation failure can
- * occur if the expected or witness value is a NaN value and it is
- * transformed (perhaps in a platform specific manner) into another NaN
- * value, and thus has a different bitwise representation (see
- * {@link Float#intBitsToFloat} or {@link Double#longBitsToDouble} for more
- * details).
- * The values {@code -0.0} and {@code +0.0} have different bitwise
- * representations but are considered equal when using the primitive
- * {@code ==} operator. Operation failure can occur if, for example, a
- * numeric algorithm computes an expected value to be say {@code -0.0}
- * and previously computed the witness value to be say {@code +0.0}.
- * @param f the reflected field, with a field of type {@code T}, and
- * a declaring class of type {@code R}
- * @return a VarHandle giving access to non-static fields or a static
- * field
- * @throws IllegalAccessException if access checking fails
- * @throws NullPointerException if the argument is null
- * @since 9
- * @hide
- */
- public VarHandle unreflectVarHandle(Field f) throws IllegalAccessException {
- final boolean isStatic = Modifier.isStatic(f.getModifiers());
- final boolean performAccessChecks = true;
- commonFieldChecks(f, f.getDeclaringClass(), f.getType(), isStatic, performAccessChecks);
- return FieldVarHandle.create(f);
- }
- // END Android-changed: OpenJDK 9+181 VarHandle API factory method.
-
- /**
- * Cracks a <a href="MethodHandleInfo.html#directmh">direct method handle</a>
- * created by this lookup object or a similar one.
- * Security and access checks are performed to ensure that this lookup object
- * is capable of reproducing the target method handle.
- * This means that the cracking may fail if target is a direct method handle
- * but was created by an unrelated lookup object.
- * This can happen if the method handle is <a href="MethodHandles.Lookup.html#callsens">caller sensitive</a>
- * and was created by a lookup object for a different class.
- * @param target a direct method handle to crack into symbolic reference components
- * @return a symbolic reference which can be used to reconstruct this method handle from this lookup object
- * @exception SecurityException if a security manager is present and it
- * <a href="MethodHandles.Lookup.html#secmgr">refuses access</a>
- * @throws IllegalArgumentException if the target is not a direct method handle or if access checking fails
- * @exception NullPointerException if the target is {@code null}
- * @see MethodHandleInfo
- * @since 1.8
- */
- public MethodHandleInfo revealDirect(MethodHandle target) {
- MethodHandleImpl directTarget = getMethodHandleImpl(target);
- MethodHandleInfo info = directTarget.reveal();
-
- try {
- checkAccess(lookupClass(), info.getDeclaringClass(), info.getModifiers(),
- info.getName());
- } catch (IllegalAccessException exception) {
- throw new IllegalArgumentException("Unable to access memeber.", exception);
- }
-
- return info;
- }
-
- private boolean hasPrivateAccess() {
- return (allowedModes & PRIVATE) != 0;
- }
-
- /** Check public/protected/private bits on the symbolic reference class and its member. */
- void checkAccess(Class<?> refc, Class<?> defc, int mods, String methName)
- throws IllegalAccessException {
- int allowedModes = this.allowedModes;
-
- if (Modifier.isProtected(mods) &&
- defc == Object.class &&
- "clone".equals(methName) &&
- refc.isArray()) {
- // The JVM does this hack also.
- // (See ClassVerifier::verify_invoke_instructions
- // and LinkResolver::check_method_accessability.)
- // Because the JVM does not allow separate methods on array types,
- // there is no separate method for int[].clone.
- // All arrays simply inherit Object.clone.
- // But for access checking logic, we make Object.clone
- // (normally protected) appear to be public.
- // Later on, when the DirectMethodHandle is created,
- // its leading argument will be restricted to the
- // requested array type.
- // N.B. The return type is not adjusted, because
- // that is *not* the bytecode behavior.
- mods ^= Modifier.PROTECTED | Modifier.PUBLIC;
- }
-
- if (Modifier.isProtected(mods) && Modifier.isConstructor(mods)) {
- // cannot "new" a protected ctor in a different package
- mods ^= Modifier.PROTECTED;
- }
-
- if (Modifier.isPublic(mods) && Modifier.isPublic(refc.getModifiers()) && allowedModes != 0)
- return; // common case
- int requestedModes = fixmods(mods); // adjust 0 => PACKAGE
- if ((requestedModes & allowedModes) != 0) {
- if (VerifyAccess.isMemberAccessible(refc, defc, mods, lookupClass(), allowedModes))
- return;
- } else {
- // Protected members can also be checked as if they were package-private.
- if ((requestedModes & PROTECTED) != 0 && (allowedModes & PACKAGE) != 0
- && VerifyAccess.isSamePackage(defc, lookupClass()))
- return;
- }
-
- throwMakeAccessException(accessFailedMessage(refc, defc, mods), this);
- }
-
- String accessFailedMessage(Class<?> refc, Class<?> defc, int mods) {
- // check the class first:
- boolean classOK = (Modifier.isPublic(defc.getModifiers()) &&
- (defc == refc ||
- Modifier.isPublic(refc.getModifiers())));
- if (!classOK && (allowedModes & PACKAGE) != 0) {
- classOK = (VerifyAccess.isClassAccessible(defc, lookupClass(), ALL_MODES) &&
- (defc == refc ||
- VerifyAccess.isClassAccessible(refc, lookupClass(), ALL_MODES)));
- }
- if (!classOK)
- return "class is not public";
- if (Modifier.isPublic(mods))
- return "access to public member failed"; // (how?)
- if (Modifier.isPrivate(mods))
- return "member is private";
- if (Modifier.isProtected(mods))
- return "member is protected";
- return "member is private to package";
- }
-
- // Android-changed: checkSpecialCaller assumes that ALLOW_NESTMATE_ACCESS = false,
- // as in upstream OpenJDK.
- //
- // private static final boolean ALLOW_NESTMATE_ACCESS = false;
-
- private void checkSpecialCaller(Class<?> specialCaller) throws IllegalAccessException {
- // Android-changed: No support for TRUSTED lookups. Also construct the
- // IllegalAccessException by hand because the upstream code implicitly assumes
- // that the lookupClass == specialCaller.
- //
- // if (allowedModes == TRUSTED) return;
- if (!hasPrivateAccess() || (specialCaller != lookupClass())) {
- throw new IllegalAccessException("no private access for invokespecial : "
- + specialCaller + ", from" + this);
- }
- }
-
- private void throwMakeAccessException(String message, Object from) throws
- IllegalAccessException{
- message = message + ": "+ toString();
- if (from != null) message += ", from " + from;
- throw new IllegalAccessException(message);
- }
-
- private void checkReturnType(Method method, MethodType methodType)
- throws NoSuchMethodException {
- if (method.getReturnType() != methodType.rtype()) {
- throw new NoSuchMethodException(method.getName() + methodType);
- }
- }
- }
-
- /**
- * "Cracks" {@code target} to reveal the underlying {@code MethodHandleImpl}.
- */
- private static MethodHandleImpl getMethodHandleImpl(MethodHandle target) {
- // Special case : We implement handles to constructors as transformers,
- // so we must extract the underlying handle from the transformer.
- if (target instanceof Transformers.Construct) {
- target = ((Transformers.Construct) target).getConstructorHandle();
- }
-
- // Special case: Var-args methods are also implemented as Transformers,
- // so we should get the underlying handle in that case as well.
- if (target instanceof Transformers.VarargsCollector) {
- target = target.asFixedArity();
- }
-
- if (target instanceof MethodHandleImpl) {
- return (MethodHandleImpl) target;
- }
-
- throw new IllegalArgumentException(target + " is not a direct handle");
- }
-
- // BEGIN Android-added: method to check if a class is an array.
- private static void checkClassIsArray(Class<?> c) {
- if (!c.isArray()) {
- throw new IllegalArgumentException("Not an array type: " + c);
- }
- }
+ public MethodHandleInfo revealDirect(MethodHandle target) { return null; }
- private static void checkTypeIsViewable(Class<?> componentType) {
- if (componentType == short.class ||
- componentType == char.class ||
- componentType == int.class ||
- componentType == long.class ||
- componentType == float.class ||
- componentType == double.class) {
- return;
- }
- throw new UnsupportedOperationException("Component type not supported: " + componentType);
}
- // END Android-added: method to check if a class is an array.
- /**
- * Produces a method handle giving read access to elements of an array.
- * The type of the method handle will have a return type of the array's
- * element type. Its first argument will be the array type,
- * and the second will be {@code int}.
- * @param arrayClass an array type
- * @return a method handle which can load values from the given array type
- * @throws NullPointerException if the argument is null
- * @throws IllegalArgumentException if arrayClass is not an array type
- */
public static
- MethodHandle arrayElementGetter(Class<?> arrayClass) throws IllegalArgumentException {
- checkClassIsArray(arrayClass);
- final Class<?> componentType = arrayClass.getComponentType();
- if (componentType.isPrimitive()) {
- try {
- return Lookup.PUBLIC_LOOKUP.findStatic(MethodHandles.class,
- "arrayElementGetter",
- MethodType.methodType(componentType, arrayClass, int.class));
- } catch (NoSuchMethodException | IllegalAccessException exception) {
- throw new AssertionError(exception);
- }
- }
+ MethodHandle arrayElementGetter(Class<?> arrayClass) throws IllegalArgumentException { return null; }
- return new Transformers.ReferenceArrayElementGetter(arrayClass);
- }
-
- /** @hide */ public static byte arrayElementGetter(byte[] array, int i) { return array[i]; }
- /** @hide */ public static boolean arrayElementGetter(boolean[] array, int i) { return array[i]; }
- /** @hide */ public static char arrayElementGetter(char[] array, int i) { return array[i]; }
- /** @hide */ public static short arrayElementGetter(short[] array, int i) { return array[i]; }
- /** @hide */ public static int arrayElementGetter(int[] array, int i) { return array[i]; }
- /** @hide */ public static long arrayElementGetter(long[] array, int i) { return array[i]; }
- /** @hide */ public static float arrayElementGetter(float[] array, int i) { return array[i]; }
- /** @hide */ public static double arrayElementGetter(double[] array, int i) { return array[i]; }
-
- /**
- * Produces a method handle giving write access to elements of an array.
- * The type of the method handle will have a void return type.
- * Its last argument will be the array's element type.
- * The first and second arguments will be the array type and int.
- * @param arrayClass the class of an array
- * @return a method handle which can store values into the array type
- * @throws NullPointerException if the argument is null
- * @throws IllegalArgumentException if arrayClass is not an array type
- */
- public static
- MethodHandle arrayElementSetter(Class<?> arrayClass) throws IllegalArgumentException {
- checkClassIsArray(arrayClass);
- final Class<?> componentType = arrayClass.getComponentType();
- if (componentType.isPrimitive()) {
- try {
- return Lookup.PUBLIC_LOOKUP.findStatic(MethodHandles.class,
- "arrayElementSetter",
- MethodType.methodType(void.class, arrayClass, int.class, componentType));
- } catch (NoSuchMethodException | IllegalAccessException exception) {
- throw new AssertionError(exception);
- }
- }
-
- return new Transformers.ReferenceArrayElementSetter(arrayClass);
- }
-
- /** @hide */
- public static void arrayElementSetter(byte[] array, int i, byte val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(boolean[] array, int i, boolean val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(char[] array, int i, char val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(short[] array, int i, short val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(int[] array, int i, int val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(long[] array, int i, long val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(float[] array, int i, float val) { array[i] = val; }
- /** @hide */
- public static void arrayElementSetter(double[] array, int i, double val) { array[i] = val; }
-
- // BEGIN Android-changed: OpenJDK 9+181 VarHandle API factory methods.
- /**
- * Produces a VarHandle giving access to elements of an array of type
- * {@code arrayClass}. The VarHandle's variable type is the component type
- * of {@code arrayClass} and the list of coordinate types is
- * {@code (arrayClass, int)}, where the {@code int} coordinate type
- * corresponds to an argument that is an index into an array.
- * <p>
- * Certain access modes of the returned VarHandle are unsupported under
- * the following conditions:
- * <ul>
- * <li>if the component type is anything other than {@code byte},
- * {@code short}, {@code char}, {@code int}, {@code long},
- * {@code float}, or {@code double} then numeric atomic update access
- * modes are unsupported.
- * <li>if the field type is anything other than {@code boolean},
- * {@code byte}, {@code short}, {@code char}, {@code int} or
- * {@code long} then bitwise atomic update access modes are
- * unsupported.
- * </ul>
- * <p>
- * If the component type is {@code float} or {@code double} then numeric
- * and atomic update access modes compare values using their bitwise
- * representation (see {@link Float#floatToRawIntBits} and
- * {@link Double#doubleToRawLongBits}, respectively).
- * @apiNote
- * Bitwise comparison of {@code float} values or {@code double} values,
- * as performed by the numeric and atomic update access modes, differ
- * from the primitive {@code ==} operator and the {@link Float#equals}
- * and {@link Double#equals} methods, specifically with respect to
- * comparing NaN values or comparing {@code -0.0} with {@code +0.0}.
- * Care should be taken when performing a compare and set or a compare
- * and exchange operation with such values since the operation may
- * unexpectedly fail.
- * There are many possible NaN values that are considered to be
- * {@code NaN} in Java, although no IEEE 754 floating-point operation
- * provided by Java can distinguish between them. Operation failure can
- * occur if the expected or witness value is a NaN value and it is
- * transformed (perhaps in a platform specific manner) into another NaN
- * value, and thus has a different bitwise representation (see
- * {@link Float#intBitsToFloat} or {@link Double#longBitsToDouble} for more
- * details).
- * The values {@code -0.0} and {@code +0.0} have different bitwise
- * representations but are considered equal when using the primitive
- * {@code ==} operator. Operation failure can occur if, for example, a
- * numeric algorithm computes an expected value to be say {@code -0.0}
- * and previously computed the witness value to be say {@code +0.0}.
- * @param arrayClass the class of an array, of type {@code T[]}
- * @return a VarHandle giving access to elements of an array
- * @throws NullPointerException if the arrayClass is null
- * @throws IllegalArgumentException if arrayClass is not an array type
- * @since 9
- * @hide
- */
- public static
- VarHandle arrayElementVarHandle(Class<?> arrayClass) throws IllegalArgumentException {
- checkClassIsArray(arrayClass);
- return ArrayElementVarHandle.create(arrayClass);
- }
-
- /**
- * Produces a VarHandle giving access to elements of a {@code byte[]} array
- * viewed as if it were a different primitive array type, such as
- * {@code int[]} or {@code long[]}.
- * The VarHandle's variable type is the component type of
- * {@code viewArrayClass} and the list of coordinate types is
- * {@code (byte[], int)}, where the {@code int} coordinate type
- * corresponds to an argument that is an index into a {@code byte[]} array.
- * The returned VarHandle accesses bytes at an index in a {@code byte[]}
- * array, composing bytes to or from a value of the component type of
- * {@code viewArrayClass} according to the given endianness.
- * <p>
- * The supported component types (variables types) are {@code short},
- * {@code char}, {@code int}, {@code long}, {@code float} and
- * {@code double}.
- * <p>
- * Access of bytes at a given index will result in an
- * {@code IndexOutOfBoundsException} if the index is less than {@code 0}
- * or greater than the {@code byte[]} array length minus the size (in bytes)
- * of {@code T}.
- * <p>
- * Access of bytes at an index may be aligned or misaligned for {@code T},
- * with respect to the underlying memory address, {@code A} say, associated
- * with the array and index.
- * If access is misaligned then access for anything other than the
- * {@code get} and {@code set} access modes will result in an
- * {@code IllegalStateException}. In such cases atomic access is only
- * guaranteed with respect to the largest power of two that divides the GCD
- * of {@code A} and the size (in bytes) of {@code T}.
- * If access is aligned then following access modes are supported and are
- * guaranteed to support atomic access:
- * <ul>
- * <li>read write access modes for all {@code T}, with the exception of
- * access modes {@code get} and {@code set} for {@code long} and
- * {@code double} on 32-bit platforms.
- * <li>atomic update access modes for {@code int}, {@code long},
- * {@code float} or {@code double}.
- * (Future major platform releases of the JDK may support additional
- * types for certain currently unsupported access modes.)
- * <li>numeric atomic update access modes for {@code int} and {@code long}.
- * (Future major platform releases of the JDK may support additional
- * numeric types for certain currently unsupported access modes.)
- * <li>bitwise atomic update access modes for {@code int} and {@code long}.
- * (Future major platform releases of the JDK may support additional
- * numeric types for certain currently unsupported access modes.)
- * </ul>
- * <p>
- * Misaligned access, and therefore atomicity guarantees, may be determined
- * for {@code byte[]} arrays without operating on a specific array. Given
- * an {@code index}, {@code T} and it's corresponding boxed type,
- * {@code T_BOX}, misalignment may be determined as follows:
- * <pre>{@code
- * int sizeOfT = T_BOX.BYTES; // size in bytes of T
- * int misalignedAtZeroIndex = ByteBuffer.wrap(new byte[0]).
- * alignmentOffset(0, sizeOfT);
- * int misalignedAtIndex = (misalignedAtZeroIndex + index) % sizeOfT;
- * boolean isMisaligned = misalignedAtIndex != 0;
- * }</pre>
- * <p>
- * If the variable type is {@code float} or {@code double} then atomic
- * update access modes compare values using their bitwise representation
- * (see {@link Float#floatToRawIntBits} and
- * {@link Double#doubleToRawLongBits}, respectively).
- * @param viewArrayClass the view array class, with a component type of
- * type {@code T}
- * @param byteOrder the endianness of the view array elements, as
- * stored in the underlying {@code byte} array
- * @return a VarHandle giving access to elements of a {@code byte[]} array
- * viewed as if elements corresponding to the components type of the view
- * array class
- * @throws NullPointerException if viewArrayClass or byteOrder is null
- * @throws IllegalArgumentException if viewArrayClass is not an array type
- * @throws UnsupportedOperationException if the component type of
- * viewArrayClass is not supported as a variable type
- * @since 9
- * @hide
- */
public static
- VarHandle byteArrayViewVarHandle(Class<?> viewArrayClass,
- ByteOrder byteOrder) throws IllegalArgumentException {
- checkClassIsArray(viewArrayClass);
- checkTypeIsViewable(viewArrayClass.getComponentType());
- return ByteArrayViewVarHandle.create(viewArrayClass, byteOrder);
- }
-
- /**
- * Produces a VarHandle giving access to elements of a {@code ByteBuffer}
- * viewed as if it were an array of elements of a different primitive
- * component type to that of {@code byte}, such as {@code int[]} or
- * {@code long[]}.
- * The VarHandle's variable type is the component type of
- * {@code viewArrayClass} and the list of coordinate types is
- * {@code (ByteBuffer, int)}, where the {@code int} coordinate type
- * corresponds to an argument that is an index into a {@code byte[]} array.
- * The returned VarHandle accesses bytes at an index in a
- * {@code ByteBuffer}, composing bytes to or from a value of the component
- * type of {@code viewArrayClass} according to the given endianness.
- * <p>
- * The supported component types (variables types) are {@code short},
- * {@code char}, {@code int}, {@code long}, {@code float} and
- * {@code double}.
- * <p>
- * Access will result in a {@code ReadOnlyBufferException} for anything
- * other than the read access modes if the {@code ByteBuffer} is read-only.
- * <p>
- * Access of bytes at a given index will result in an
- * {@code IndexOutOfBoundsException} if the index is less than {@code 0}
- * or greater than the {@code ByteBuffer} limit minus the size (in bytes) of
- * {@code T}.
- * <p>
- * Access of bytes at an index may be aligned or misaligned for {@code T},
- * with respect to the underlying memory address, {@code A} say, associated
- * with the {@code ByteBuffer} and index.
- * If access is misaligned then access for anything other than the
- * {@code get} and {@code set} access modes will result in an
- * {@code IllegalStateException}. In such cases atomic access is only
- * guaranteed with respect to the largest power of two that divides the GCD
- * of {@code A} and the size (in bytes) of {@code T}.
- * If access is aligned then following access modes are supported and are
- * guaranteed to support atomic access:
- * <ul>
- * <li>read write access modes for all {@code T}, with the exception of
- * access modes {@code get} and {@code set} for {@code long} and
- * {@code double} on 32-bit platforms.
- * <li>atomic update access modes for {@code int}, {@code long},
- * {@code float} or {@code double}.
- * (Future major platform releases of the JDK may support additional
- * types for certain currently unsupported access modes.)
- * <li>numeric atomic update access modes for {@code int} and {@code long}.
- * (Future major platform releases of the JDK may support additional
- * numeric types for certain currently unsupported access modes.)
- * <li>bitwise atomic update access modes for {@code int} and {@code long}.
- * (Future major platform releases of the JDK may support additional
- * numeric types for certain currently unsupported access modes.)
- * </ul>
- * <p>
- * Misaligned access, and therefore atomicity guarantees, may be determined
- * for a {@code ByteBuffer}, {@code bb} (direct or otherwise), an
- * {@code index}, {@code T} and it's corresponding boxed type,
- * {@code T_BOX}, as follows:
- * <pre>{@code
- * int sizeOfT = T_BOX.BYTES; // size in bytes of T
- * ByteBuffer bb = ...
- * int misalignedAtIndex = bb.alignmentOffset(index, sizeOfT);
- * boolean isMisaligned = misalignedAtIndex != 0;
- * }</pre>
- * <p>
- * If the variable type is {@code float} or {@code double} then atomic
- * update access modes compare values using their bitwise representation
- * (see {@link Float#floatToRawIntBits} and
- * {@link Double#doubleToRawLongBits}, respectively).
- * @param viewArrayClass the view array class, with a component type of
- * type {@code T}
- * @param byteOrder the endianness of the view array elements, as
- * stored in the underlying {@code ByteBuffer} (Note this overrides the
- * endianness of a {@code ByteBuffer})
- * @return a VarHandle giving access to elements of a {@code ByteBuffer}
- * viewed as if elements corresponding to the components type of the view
- * array class
- * @throws NullPointerException if viewArrayClass or byteOrder is null
- * @throws IllegalArgumentException if viewArrayClass is not an array type
- * @throws UnsupportedOperationException if the component type of
- * viewArrayClass is not supported as a variable type
- * @since 9
- * @hide
- */
- public static
- VarHandle byteBufferViewVarHandle(Class<?> viewArrayClass,
- ByteOrder byteOrder) throws IllegalArgumentException {
- checkClassIsArray(viewArrayClass);
- checkTypeIsViewable(viewArrayClass.getComponentType());
- return ByteBufferViewVarHandle.create(viewArrayClass, byteOrder);
- }
- // END Android-changed: OpenJDK 9+181 VarHandle API factory methods.
-
- /// method handle invocation (reflective style)
-
- /**
- * Produces a method handle which will invoke any method handle of the
- * given {@code type}, with a given number of trailing arguments replaced by
- * a single trailing {@code Object[]} array.
- * The resulting invoker will be a method handle with the following
- * arguments:
- * <ul>
- * <li>a single {@code MethodHandle} target
- * <li>zero or more leading values (counted by {@code leadingArgCount})
- * <li>an {@code Object[]} array containing trailing arguments
- * </ul>
- * <p>
- * The invoker will invoke its target like a call to {@link MethodHandle#invoke invoke} with
- * the indicated {@code type}.
- * That is, if the target is exactly of the given {@code type}, it will behave
- * like {@code invokeExact}; otherwise it behave as if {@link MethodHandle#asType asType}
- * is used to convert the target to the required {@code type}.
- * <p>
- * The type of the returned invoker will not be the given {@code type}, but rather
- * will have all parameters except the first {@code leadingArgCount}
- * replaced by a single array of type {@code Object[]}, which will be
- * the final parameter.
- * <p>
- * Before invoking its target, the invoker will spread the final array, apply
- * reference casts as necessary, and unbox and widen primitive arguments.
- * If, when the invoker is called, the supplied array argument does
- * not have the correct number of elements, the invoker will throw
- * an {@link IllegalArgumentException} instead of invoking the target.
- * <p>
- * This method is equivalent to the following code (though it may be more efficient):
- * <blockquote><pre>{@code
-MethodHandle invoker = MethodHandles.invoker(type);
-int spreadArgCount = type.parameterCount() - leadingArgCount;
-invoker = invoker.asSpreader(Object[].class, spreadArgCount);
-return invoker;
- * }</pre></blockquote>
- * This method throws no reflective or security exceptions.
- * @param type the desired target type
- * @param leadingArgCount number of fixed arguments, to be passed unchanged to the target
- * @return a method handle suitable for invoking any method handle of the given type
- * @throws NullPointerException if {@code type} is null
- * @throws IllegalArgumentException if {@code leadingArgCount} is not in
- * the range from 0 to {@code type.parameterCount()} inclusive,
- * or if the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- */
- static public
- MethodHandle spreadInvoker(MethodType type, int leadingArgCount) {
- if (leadingArgCount < 0 || leadingArgCount > type.parameterCount())
- throw newIllegalArgumentException("bad argument count", leadingArgCount);
-
- MethodHandle invoker = MethodHandles.invoker(type);
- int spreadArgCount = type.parameterCount() - leadingArgCount;
- invoker = invoker.asSpreader(Object[].class, spreadArgCount);
- return invoker;
- }
-
- /**
- * Produces a special <em>invoker method handle</em> which can be used to
- * invoke any method handle of the given type, as if by {@link MethodHandle#invokeExact invokeExact}.
- * The resulting invoker will have a type which is
- * exactly equal to the desired type, except that it will accept
- * an additional leading argument of type {@code MethodHandle}.
- * <p>
- * This method is equivalent to the following code (though it may be more efficient):
- * {@code publicLookup().findVirtual(MethodHandle.class, "invokeExact", type)}
- *
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * Invoker method handles can be useful when working with variable method handles
- * of unknown types.
- * For example, to emulate an {@code invokeExact} call to a variable method
- * handle {@code M}, extract its type {@code T},
- * look up the invoker method {@code X} for {@code T},
- * and call the invoker method, as {@code X.invoke(T, A...)}.
- * (It would not work to call {@code X.invokeExact}, since the type {@code T}
- * is unknown.)
- * If spreading, collecting, or other argument transformations are required,
- * they can be applied once to the invoker {@code X} and reused on many {@code M}
- * method handle values, as long as they are compatible with the type of {@code X}.
- * <p style="font-size:smaller;">
- * <em>(Note: The invoker method is not available via the Core Reflection API.
- * An attempt to call {@linkplain java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}
- * on the declared {@code invokeExact} or {@code invoke} method will raise an
- * {@link java.lang.UnsupportedOperationException UnsupportedOperationException}.)</em>
- * <p>
- * This method throws no reflective or security exceptions.
- * @param type the desired target type
- * @return a method handle suitable for invoking any method handle of the given type
- * @throws IllegalArgumentException if the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- */
- static public
- MethodHandle exactInvoker(MethodType type) {
- return new Transformers.Invoker(type, true /* isExactInvoker */);
- }
+ MethodHandle arrayElementSetter(Class<?> arrayClass) throws IllegalArgumentException { return null; }
- /**
- * Produces a special <em>invoker method handle</em> which can be used to
- * invoke any method handle compatible with the given type, as if by {@link MethodHandle#invoke invoke}.
- * The resulting invoker will have a type which is
- * exactly equal to the desired type, except that it will accept
- * an additional leading argument of type {@code MethodHandle}.
- * <p>
- * Before invoking its target, if the target differs from the expected type,
- * the invoker will apply reference casts as
- * necessary and box, unbox, or widen primitive values, as if by {@link MethodHandle#asType asType}.
- * Similarly, the return value will be converted as necessary.
- * If the target is a {@linkplain MethodHandle#asVarargsCollector variable arity method handle},
- * the required arity conversion will be made, again as if by {@link MethodHandle#asType asType}.
- * <p>
- * This method is equivalent to the following code (though it may be more efficient):
- * {@code publicLookup().findVirtual(MethodHandle.class, "invoke", type)}
- * <p style="font-size:smaller;">
- * <em>Discussion:</em>
- * A {@linkplain MethodType#genericMethodType general method type} is one which
- * mentions only {@code Object} arguments and return values.
- * An invoker for such a type is capable of calling any method handle
- * of the same arity as the general type.
- * <p style="font-size:smaller;">
- * <em>(Note: The invoker method is not available via the Core Reflection API.
- * An attempt to call {@linkplain java.lang.reflect.Method#invoke java.lang.reflect.Method.invoke}
- * on the declared {@code invokeExact} or {@code invoke} method will raise an
- * {@link java.lang.UnsupportedOperationException UnsupportedOperationException}.)</em>
- * <p>
- * This method throws no reflective or security exceptions.
- * @param type the desired target type
- * @return a method handle suitable for invoking any method handle convertible to the given type
- * @throws IllegalArgumentException if the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- */
static public
- MethodHandle invoker(MethodType type) {
- return new Transformers.Invoker(type, false /* isExactInvoker */);
- }
+ MethodHandle spreadInvoker(MethodType type, int leadingArgCount) { return null; }
- /**
- * Produces a special <em>invoker method handle</em> which can be used to
- * invoke a signature-polymorphic access mode method on any VarHandle whose
- * associated access mode type is compatible with the given type.
- * The resulting invoker will have a type which is exactly equal to the
- * desired given type, except that it will accept an additional leading
- * argument of type {@code VarHandle}.
- *
- * @param accessMode the VarHandle access mode
- * @param type the desired target type
- * @return a method handle suitable for invoking an access mode method of
- * any VarHandle whose access mode type is of the given type.
- * @since 9
- * @hide
- */
static public
- MethodHandle varHandleExactInvoker(VarHandle.AccessMode accessMode, MethodType type) {
- unsupported("MethodHandles.varHandleExactInvoker()"); // TODO(b/65872996)
- return null;
- }
+ MethodHandle exactInvoker(MethodType type) { return null; }
- /**
- * Produces a special <em>invoker method handle</em> which can be used to
- * invoke a signature-polymorphic access mode method on any VarHandle whose
- * associated access mode type is compatible with the given type.
- * The resulting invoker will have a type which is exactly equal to the
- * desired given type, except that it will accept an additional leading
- * argument of type {@code VarHandle}.
- * <p>
- * Before invoking its target, if the access mode type differs from the
- * desired given type, the invoker will apply reference casts as necessary
- * and box, unbox, or widen primitive values, as if by
- * {@link MethodHandle#asType asType}. Similarly, the return value will be
- * converted as necessary.
- * <p>
- * This method is equivalent to the following code (though it may be more
- * efficient): {@code publicLookup().findVirtual(VarHandle.class, accessMode.name(), type)}
- *
- * @param accessMode the VarHandle access mode
- * @param type the desired target type
- * @return a method handle suitable for invoking an access mode method of
- * any VarHandle whose access mode type is convertible to the given
- * type.
- * @since 9
- * @hide
- */
static public
- MethodHandle varHandleInvoker(VarHandle.AccessMode accessMode, MethodType type) {
- unsupported("MethodHandles.varHandleInvoker()"); // TODO(b/65872996)
- return null;
- }
-
- // Android-changed: Basic invokers are not supported.
- //
- // static /*non-public*/
- // MethodHandle basicInvoker(MethodType type) {
- // return type.invokers().basicInvoker();
- // }
-
- /// method handle modification (creation from other method handles)
+ MethodHandle invoker(MethodType type) { return null; }
- /**
- * Produces a method handle which adapts the type of the
- * given method handle to a new type by pairwise argument and return type conversion.
- * The original type and new type must have the same number of arguments.
- * The resulting method handle is guaranteed to report a type
- * which is equal to the desired new type.
- * <p>
- * If the original type and new type are equal, returns target.
- * <p>
- * The same conversions are allowed as for {@link MethodHandle#asType MethodHandle.asType},
- * and some additional conversions are also applied if those conversions fail.
- * Given types <em>T0</em>, <em>T1</em>, one of the following conversions is applied
- * if possible, before or instead of any conversions done by {@code asType}:
- * <ul>
- * <li>If <em>T0</em> and <em>T1</em> are references, and <em>T1</em> is an interface type,
- * then the value of type <em>T0</em> is passed as a <em>T1</em> without a cast.
- * (This treatment of interfaces follows the usage of the bytecode verifier.)
- * <li>If <em>T0</em> is boolean and <em>T1</em> is another primitive,
- * the boolean is converted to a byte value, 1 for true, 0 for false.
- * (This treatment follows the usage of the bytecode verifier.)
- * <li>If <em>T1</em> is boolean and <em>T0</em> is another primitive,
- * <em>T0</em> is converted to byte via Java casting conversion (JLS 5.5),
- * and the low order bit of the result is tested, as if by {@code (x & 1) != 0}.
- * <li>If <em>T0</em> and <em>T1</em> are primitives other than boolean,
- * then a Java casting conversion (JLS 5.5) is applied.
- * (Specifically, <em>T0</em> will convert to <em>T1</em> by
- * widening and/or narrowing.)
- * <li>If <em>T0</em> is a reference and <em>T1</em> a primitive, an unboxing
- * conversion will be applied at runtime, possibly followed
- * by a Java casting conversion (JLS 5.5) on the primitive value,
- * possibly followed by a conversion from byte to boolean by testing
- * the low-order bit.
- * <li>If <em>T0</em> is a reference and <em>T1</em> a primitive,
- * and if the reference is null at runtime, a zero value is introduced.
- * </ul>
- * @param target the method handle to invoke after arguments are retyped
- * @param newType the expected type of the new method handle
- * @return a method handle which delegates to the target after performing
- * any necessary argument conversions, and arranges for any
- * necessary return value conversions
- * @throws NullPointerException if either argument is null
- * @throws WrongMethodTypeException if the conversion cannot be made
- * @see MethodHandle#asType
- */
public static
- MethodHandle explicitCastArguments(MethodHandle target, MethodType newType) {
- explicitCastArgumentsChecks(target, newType);
- // use the asTypeCache when possible:
- MethodType oldType = target.type();
- if (oldType == newType) return target;
- if (oldType.explicitCastEquivalentToAsType(newType)) {
- return target.asFixedArity().asType(newType);
- }
+ MethodHandle explicitCastArguments(MethodHandle target, MethodType newType) { return null; }
- return new Transformers.ExplicitCastArguments(target, newType);
- }
-
- private static void explicitCastArgumentsChecks(MethodHandle target, MethodType newType) {
- if (target.type().parameterCount() != newType.parameterCount()) {
- throw new WrongMethodTypeException("cannot explicitly cast " + target + " to " + newType);
- }
- }
-
- /**
- * Produces a method handle which adapts the calling sequence of the
- * given method handle to a new type, by reordering the arguments.
- * The resulting method handle is guaranteed to report a type
- * which is equal to the desired new type.
- * <p>
- * The given array controls the reordering.
- * Call {@code #I} the number of incoming parameters (the value
- * {@code newType.parameterCount()}, and call {@code #O} the number
- * of outgoing parameters (the value {@code target.type().parameterCount()}).
- * Then the length of the reordering array must be {@code #O},
- * and each element must be a non-negative number less than {@code #I}.
- * For every {@code N} less than {@code #O}, the {@code N}-th
- * outgoing argument will be taken from the {@code I}-th incoming
- * argument, where {@code I} is {@code reorder[N]}.
- * <p>
- * No argument or return value conversions are applied.
- * The type of each incoming argument, as determined by {@code newType},
- * must be identical to the type of the corresponding outgoing parameter
- * or parameters in the target method handle.
- * The return type of {@code newType} must be identical to the return
- * type of the original target.
- * <p>
- * The reordering array need not specify an actual permutation.
- * An incoming argument will be duplicated if its index appears
- * more than once in the array, and an incoming argument will be dropped
- * if its index does not appear in the array.
- * As in the case of {@link #dropArguments(MethodHandle,int,List) dropArguments},
- * incoming arguments which are not mentioned in the reordering array
- * are may be any type, as determined only by {@code newType}.
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodType intfn1 = methodType(int.class, int.class);
-MethodType intfn2 = methodType(int.class, int.class, int.class);
-MethodHandle sub = ... (int x, int y) -> (x-y) ...;
-assert(sub.type().equals(intfn2));
-MethodHandle sub1 = permuteArguments(sub, intfn2, 0, 1);
-MethodHandle rsub = permuteArguments(sub, intfn2, 1, 0);
-assert((int)rsub.invokeExact(1, 100) == 99);
-MethodHandle add = ... (int x, int y) -> (x+y) ...;
-assert(add.type().equals(intfn2));
-MethodHandle twice = permuteArguments(add, intfn1, 0, 0);
-assert(twice.type().equals(intfn1));
-assert((int)twice.invokeExact(21) == 42);
- * }</pre></blockquote>
- * @param target the method handle to invoke after arguments are reordered
- * @param newType the expected type of the new method handle
- * @param reorder an index array which controls the reordering
- * @return a method handle which delegates to the target after it
- * drops unused arguments and moves and/or duplicates the other arguments
- * @throws NullPointerException if any argument is null
- * @throws IllegalArgumentException if the index array length is not equal to
- * the arity of the target, or if any index array element
- * not a valid index for a parameter of {@code newType},
- * or if two corresponding parameter types in
- * {@code target.type()} and {@code newType} are not identical,
- */
public static
- MethodHandle permuteArguments(MethodHandle target, MethodType newType, int... reorder) {
- reorder = reorder.clone(); // get a private copy
- MethodType oldType = target.type();
- permuteArgumentChecks(reorder, newType, oldType);
-
- return new Transformers.PermuteArguments(newType, target, reorder);
- }
-
- // Android-changed: findFirstDupOrDrop is unused and removed.
- // private static int findFirstDupOrDrop(int[] reorder, int newArity);
-
- private static boolean permuteArgumentChecks(int[] reorder, MethodType newType, MethodType oldType) {
- if (newType.returnType() != oldType.returnType())
- throw newIllegalArgumentException("return types do not match",
- oldType, newType);
- if (reorder.length == oldType.parameterCount()) {
- int limit = newType.parameterCount();
- boolean bad = false;
- for (int j = 0; j < reorder.length; j++) {
- int i = reorder[j];
- if (i < 0 || i >= limit) {
- bad = true; break;
- }
- Class<?> src = newType.parameterType(i);
- Class<?> dst = oldType.parameterType(j);
- if (src != dst)
- throw newIllegalArgumentException("parameter types do not match after reorder",
- oldType, newType);
- }
- if (!bad) return true;
- }
- throw newIllegalArgumentException("bad reorder array: "+Arrays.toString(reorder));
- }
+ MethodHandle permuteArguments(MethodHandle target, MethodType newType, int... reorder) { return null; }
- /**
- * Produces a method handle of the requested return type which returns the given
- * constant value every time it is invoked.
- * <p>
- * Before the method handle is returned, the passed-in value is converted to the requested type.
- * If the requested type is primitive, widening primitive conversions are attempted,
- * else reference conversions are attempted.
- * <p>The returned method handle is equivalent to {@code identity(type).bindTo(value)}.
- * @param type the return type of the desired method handle
- * @param value the value to return
- * @return a method handle of the given return type and no arguments, which always returns the given value
- * @throws NullPointerException if the {@code type} argument is null
- * @throws ClassCastException if the value cannot be converted to the required return type
- * @throws IllegalArgumentException if the given type is {@code void.class}
- */
public static
- MethodHandle constant(Class<?> type, Object value) {
- if (type.isPrimitive()) {
- if (type == void.class)
- throw newIllegalArgumentException("void type");
- Wrapper w = Wrapper.forPrimitiveType(type);
- value = w.convert(value, type);
- }
-
- return new Transformers.Constant(type, value);
- }
+ MethodHandle constant(Class<?> type, Object value) { return null; }
- /**
- * Produces a method handle which returns its sole argument when invoked.
- * @param type the type of the sole parameter and return value of the desired method handle
- * @return a unary method handle which accepts and returns the given type
- * @throws NullPointerException if the argument is null
- * @throws IllegalArgumentException if the given type is {@code void.class}
- */
public static
- MethodHandle identity(Class<?> type) {
- if (type == null) {
- throw new NullPointerException("type == null");
- }
+ MethodHandle identity(Class<?> type) { return null; }
- if (type.isPrimitive()) {
- try {
- return Lookup.PUBLIC_LOOKUP.findStatic(MethodHandles.class, "identity",
- MethodType.methodType(type, type));
- } catch (NoSuchMethodException | IllegalAccessException e) {
- throw new AssertionError(e);
- }
- }
-
- return new Transformers.ReferenceIdentity(type);
- }
-
- /** @hide */ public static byte identity(byte val) { return val; }
- /** @hide */ public static boolean identity(boolean val) { return val; }
- /** @hide */ public static char identity(char val) { return val; }
- /** @hide */ public static short identity(short val) { return val; }
- /** @hide */ public static int identity(int val) { return val; }
- /** @hide */ public static long identity(long val) { return val; }
- /** @hide */ public static float identity(float val) { return val; }
- /** @hide */ public static double identity(double val) { return val; }
-
- /**
- * Provides a target method handle with one or more <em>bound arguments</em>
- * in advance of the method handle's invocation.
- * The formal parameters to the target corresponding to the bound
- * arguments are called <em>bound parameters</em>.
- * Returns a new method handle which saves away the bound arguments.
- * When it is invoked, it receives arguments for any non-bound parameters,
- * binds the saved arguments to their corresponding parameters,
- * and calls the original target.
- * <p>
- * The type of the new method handle will drop the types for the bound
- * parameters from the original target type, since the new method handle
- * will no longer require those arguments to be supplied by its callers.
- * <p>
- * Each given argument object must match the corresponding bound parameter type.
- * If a bound parameter type is a primitive, the argument object
- * must be a wrapper, and will be unboxed to produce the primitive value.
- * <p>
- * The {@code pos} argument selects which parameters are to be bound.
- * It may range between zero and <i>N-L</i> (inclusively),
- * where <i>N</i> is the arity of the target method handle
- * and <i>L</i> is the length of the values array.
- * @param target the method handle to invoke after the argument is inserted
- * @param pos where to insert the argument (zero for the first)
- * @param values the series of arguments to insert
- * @return a method handle which inserts an additional argument,
- * before calling the original method handle
- * @throws NullPointerException if the target or the {@code values} array is null
- * @see MethodHandle#bindTo
- */
public static
- MethodHandle insertArguments(MethodHandle target, int pos, Object... values) {
- int insCount = values.length;
- Class<?>[] ptypes = insertArgumentsChecks(target, insCount, pos);
- if (insCount == 0) {
- return target;
- }
-
- // Throw ClassCastExceptions early if we can't cast any of the provided values
- // to the required type.
- for (int i = 0; i < insCount; i++) {
- final Class<?> ptype = ptypes[pos + i];
- if (!ptype.isPrimitive()) {
- ptypes[pos + i].cast(values[i]);
- } else {
- // Will throw a ClassCastException if something terrible happens.
- values[i] = Wrapper.forPrimitiveType(ptype).convert(values[i], ptype);
- }
- }
+ MethodHandle insertArguments(MethodHandle target, int pos, Object... values) { return null; }
- return new Transformers.InsertArguments(target, pos, values);
- }
-
- // Android-changed: insertArgumentPrimitive is unused.
- //
- // private static BoundMethodHandle insertArgumentPrimitive(BoundMethodHandle result, int pos,
- // Class<?> ptype, Object value) {
- // Wrapper w = Wrapper.forPrimitiveType(ptype);
- // // perform unboxing and/or primitive conversion
- // value = w.convert(value, ptype);
- // switch (w) {
- // case INT: return result.bindArgumentI(pos, (int)value);
- // case LONG: return result.bindArgumentJ(pos, (long)value);
- // case FLOAT: return result.bindArgumentF(pos, (float)value);
- // case DOUBLE: return result.bindArgumentD(pos, (double)value);
- // default: return result.bindArgumentI(pos, ValueConversions.widenSubword(value));
- // }
- // }
-
- private static Class<?>[] insertArgumentsChecks(MethodHandle target, int insCount, int pos) throws RuntimeException {
- MethodType oldType = target.type();
- int outargs = oldType.parameterCount();
- int inargs = outargs - insCount;
- if (inargs < 0)
- throw newIllegalArgumentException("too many values to insert");
- if (pos < 0 || pos > inargs)
- throw newIllegalArgumentException("no argument type to append");
- return oldType.ptypes();
- }
-
- /**
- * Produces a method handle which will discard some dummy arguments
- * before calling some other specified <i>target</i> method handle.
- * The type of the new method handle will be the same as the target's type,
- * except it will also include the dummy argument types,
- * at some given position.
- * <p>
- * The {@code pos} argument may range between zero and <i>N</i>,
- * where <i>N</i> is the arity of the target.
- * If {@code pos} is zero, the dummy arguments will precede
- * the target's real arguments; if {@code pos} is <i>N</i>
- * they will come after.
- * <p>
- * <b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle cat = lookup().findVirtual(String.class,
- "concat", methodType(String.class, String.class));
-assertEquals("xy", (String) cat.invokeExact("x", "y"));
-MethodType bigType = cat.type().insertParameterTypes(0, int.class, String.class);
-MethodHandle d0 = dropArguments(cat, 0, bigType.parameterList().subList(0,2));
-assertEquals(bigType, d0.type());
-assertEquals("yz", (String) d0.invokeExact(123, "x", "y", "z"));
- * }</pre></blockquote>
- * <p>
- * This method is also equivalent to the following code:
- * <blockquote><pre>
- * {@link #dropArguments(MethodHandle,int,Class...) dropArguments}{@code (target, pos, valueTypes.toArray(new Class[0]))}
- * </pre></blockquote>
- * @param target the method handle to invoke after the arguments are dropped
- * @param valueTypes the type(s) of the argument(s) to drop
- * @param pos position of first argument to drop (zero for the leftmost)
- * @return a method handle which drops arguments of the given types,
- * before calling the original method handle
- * @throws NullPointerException if the target is null,
- * or if the {@code valueTypes} list or any of its elements is null
- * @throws IllegalArgumentException if any element of {@code valueTypes} is {@code void.class},
- * or if {@code pos} is negative or greater than the arity of the target,
- * or if the new method handle's type would have too many parameters
- */
public static
- MethodHandle dropArguments(MethodHandle target, int pos, List<Class<?>> valueTypes) {
- valueTypes = copyTypes(valueTypes);
- MethodType oldType = target.type(); // get NPE
- int dropped = dropArgumentChecks(oldType, pos, valueTypes);
-
- MethodType newType = oldType.insertParameterTypes(pos, valueTypes);
- if (dropped == 0) {
- return target;
- }
-
- return new Transformers.DropArguments(newType, target, pos, valueTypes.size());
- }
-
- private static List<Class<?>> copyTypes(List<Class<?>> types) {
- Object[] a = types.toArray();
- return Arrays.asList(Arrays.copyOf(a, a.length, Class[].class));
- }
-
- private static int dropArgumentChecks(MethodType oldType, int pos, List<Class<?>> valueTypes) {
- int dropped = valueTypes.size();
- MethodType.checkSlotCount(dropped);
- int outargs = oldType.parameterCount();
- int inargs = outargs + dropped;
- if (pos < 0 || pos > outargs)
- throw newIllegalArgumentException("no argument type to remove"
- + Arrays.asList(oldType, pos, valueTypes, inargs, outargs)
- );
- return dropped;
- }
+ MethodHandle dropArguments(MethodHandle target, int pos, List<Class<?>> valueTypes) { return null; }
- /**
- * Produces a method handle which will discard some dummy arguments
- * before calling some other specified <i>target</i> method handle.
- * The type of the new method handle will be the same as the target's type,
- * except it will also include the dummy argument types,
- * at some given position.
- * <p>
- * The {@code pos} argument may range between zero and <i>N</i>,
- * where <i>N</i> is the arity of the target.
- * If {@code pos} is zero, the dummy arguments will precede
- * the target's real arguments; if {@code pos} is <i>N</i>
- * they will come after.
- * <p>
- * <b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle cat = lookup().findVirtual(String.class,
- "concat", methodType(String.class, String.class));
-assertEquals("xy", (String) cat.invokeExact("x", "y"));
-MethodHandle d0 = dropArguments(cat, 0, String.class);
-assertEquals("yz", (String) d0.invokeExact("x", "y", "z"));
-MethodHandle d1 = dropArguments(cat, 1, String.class);
-assertEquals("xz", (String) d1.invokeExact("x", "y", "z"));
-MethodHandle d2 = dropArguments(cat, 2, String.class);
-assertEquals("xy", (String) d2.invokeExact("x", "y", "z"));
-MethodHandle d12 = dropArguments(cat, 1, int.class, boolean.class);
-assertEquals("xz", (String) d12.invokeExact("x", 12, true, "z"));
- * }</pre></blockquote>
- * <p>
- * This method is also equivalent to the following code:
- * <blockquote><pre>
- * {@link #dropArguments(MethodHandle,int,List) dropArguments}{@code (target, pos, Arrays.asList(valueTypes))}
- * </pre></blockquote>
- * @param target the method handle to invoke after the arguments are dropped
- * @param valueTypes the type(s) of the argument(s) to drop
- * @param pos position of first argument to drop (zero for the leftmost)
- * @return a method handle which drops arguments of the given types,
- * before calling the original method handle
- * @throws NullPointerException if the target is null,
- * or if the {@code valueTypes} array or any of its elements is null
- * @throws IllegalArgumentException if any element of {@code valueTypes} is {@code void.class},
- * or if {@code pos} is negative or greater than the arity of the target,
- * or if the new method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- */
public static
- MethodHandle dropArguments(MethodHandle target, int pos, Class<?>... valueTypes) {
- return dropArguments(target, pos, Arrays.asList(valueTypes));
- }
+ MethodHandle dropArguments(MethodHandle target, int pos, Class<?>... valueTypes) { return null; }
- /**
- * Adapts a target method handle by pre-processing
- * one or more of its arguments, each with its own unary filter function,
- * and then calling the target with each pre-processed argument
- * replaced by the result of its corresponding filter function.
- * <p>
- * The pre-processing is performed by one or more method handles,
- * specified in the elements of the {@code filters} array.
- * The first element of the filter array corresponds to the {@code pos}
- * argument of the target, and so on in sequence.
- * <p>
- * Null arguments in the array are treated as identity functions,
- * and the corresponding arguments left unchanged.
- * (If there are no non-null elements in the array, the original target is returned.)
- * Each filter is applied to the corresponding argument of the adapter.
- * <p>
- * If a filter {@code F} applies to the {@code N}th argument of
- * the target, then {@code F} must be a method handle which
- * takes exactly one argument. The type of {@code F}'s sole argument
- * replaces the corresponding argument type of the target
- * in the resulting adapted method handle.
- * The return type of {@code F} must be identical to the corresponding
- * parameter type of the target.
- * <p>
- * It is an error if there are elements of {@code filters}
- * (null or not)
- * which do not correspond to argument positions in the target.
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle cat = lookup().findVirtual(String.class,
- "concat", methodType(String.class, String.class));
-MethodHandle upcase = lookup().findVirtual(String.class,
- "toUpperCase", methodType(String.class));
-assertEquals("xy", (String) cat.invokeExact("x", "y"));
-MethodHandle f0 = filterArguments(cat, 0, upcase);
-assertEquals("Xy", (String) f0.invokeExact("x", "y")); // Xy
-MethodHandle f1 = filterArguments(cat, 1, upcase);
-assertEquals("xY", (String) f1.invokeExact("x", "y")); // xY
-MethodHandle f2 = filterArguments(cat, 0, upcase, upcase);
-assertEquals("XY", (String) f2.invokeExact("x", "y")); // XY
- * }</pre></blockquote>
- * <p> Here is pseudocode for the resulting adapter:
- * <blockquote><pre>{@code
- * V target(P... p, A[i]... a[i], B... b);
- * A[i] filter[i](V[i]);
- * T adapter(P... p, V[i]... v[i], B... b) {
- * return target(p..., f[i](v[i])..., b...);
- * }
- * }</pre></blockquote>
- *
- * @param target the method handle to invoke after arguments are filtered
- * @param pos the position of the first argument to filter
- * @param filters method handles to call initially on filtered arguments
- * @return method handle which incorporates the specified argument filtering logic
- * @throws NullPointerException if the target is null
- * or if the {@code filters} array is null
- * @throws IllegalArgumentException if a non-null element of {@code filters}
- * does not match a corresponding argument type of target as described above,
- * or if the {@code pos+filters.length} is greater than {@code target.type().parameterCount()},
- * or if the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- */
public static
- MethodHandle filterArguments(MethodHandle target, int pos, MethodHandle... filters) {
- filterArgumentsCheckArity(target, pos, filters);
-
- for (int i = 0; i < filters.length; ++i) {
- filterArgumentChecks(target, i + pos, filters[i]);
- }
-
- return new Transformers.FilterArguments(target, pos, filters);
- }
-
- private static void filterArgumentsCheckArity(MethodHandle target, int pos, MethodHandle[] filters) {
- MethodType targetType = target.type();
- int maxPos = targetType.parameterCount();
- if (pos + filters.length > maxPos)
- throw newIllegalArgumentException("too many filters");
- }
-
- private static void filterArgumentChecks(MethodHandle target, int pos, MethodHandle filter) throws RuntimeException {
- MethodType targetType = target.type();
- MethodType filterType = filter.type();
- if (filterType.parameterCount() != 1
- || filterType.returnType() != targetType.parameterType(pos))
- throw newIllegalArgumentException("target and filter types do not match", targetType, filterType);
- }
-
- /**
- * Adapts a target method handle by pre-processing
- * a sub-sequence of its arguments with a filter (another method handle).
- * The pre-processed arguments are replaced by the result (if any) of the
- * filter function.
- * The target is then called on the modified (usually shortened) argument list.
- * <p>
- * If the filter returns a value, the target must accept that value as
- * its argument in position {@code pos}, preceded and/or followed by
- * any arguments not passed to the filter.
- * If the filter returns void, the target must accept all arguments
- * not passed to the filter.
- * No arguments are reordered, and a result returned from the filter
- * replaces (in order) the whole subsequence of arguments originally
- * passed to the adapter.
- * <p>
- * The argument types (if any) of the filter
- * replace zero or one argument types of the target, at position {@code pos},
- * in the resulting adapted method handle.
- * The return type of the filter (if any) must be identical to the
- * argument type of the target at position {@code pos}, and that target argument
- * is supplied by the return value of the filter.
- * <p>
- * In all cases, {@code pos} must be greater than or equal to zero, and
- * {@code pos} must also be less than or equal to the target's arity.
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle deepToString = publicLookup()
- .findStatic(Arrays.class, "deepToString", methodType(String.class, Object[].class));
-
-MethodHandle ts1 = deepToString.asCollector(String[].class, 1);
-assertEquals("[strange]", (String) ts1.invokeExact("strange"));
+ MethodHandle filterArguments(MethodHandle target, int pos, MethodHandle... filters) { return null; }
-MethodHandle ts2 = deepToString.asCollector(String[].class, 2);
-assertEquals("[up, down]", (String) ts2.invokeExact("up", "down"));
-
-MethodHandle ts3 = deepToString.asCollector(String[].class, 3);
-MethodHandle ts3_ts2 = collectArguments(ts3, 1, ts2);
-assertEquals("[top, [up, down], strange]",
- (String) ts3_ts2.invokeExact("top", "up", "down", "strange"));
-
-MethodHandle ts3_ts2_ts1 = collectArguments(ts3_ts2, 3, ts1);
-assertEquals("[top, [up, down], [strange]]",
- (String) ts3_ts2_ts1.invokeExact("top", "up", "down", "strange"));
-
-MethodHandle ts3_ts2_ts3 = collectArguments(ts3_ts2, 1, ts3);
-assertEquals("[top, [[up, down, strange], charm], bottom]",
- (String) ts3_ts2_ts3.invokeExact("top", "up", "down", "strange", "charm", "bottom"));
- * }</pre></blockquote>
- * <p> Here is pseudocode for the resulting adapter:
- * <blockquote><pre>{@code
- * T target(A...,V,C...);
- * V filter(B...);
- * T adapter(A... a,B... b,C... c) {
- * V v = filter(b...);
- * return target(a...,v,c...);
- * }
- * // and if the filter has no arguments:
- * T target2(A...,V,C...);
- * V filter2();
- * T adapter2(A... a,C... c) {
- * V v = filter2();
- * return target2(a...,v,c...);
- * }
- * // and if the filter has a void return:
- * T target3(A...,C...);
- * void filter3(B...);
- * void adapter3(A... a,B... b,C... c) {
- * filter3(b...);
- * return target3(a...,c...);
- * }
- * }</pre></blockquote>
- * <p>
- * A collection adapter {@code collectArguments(mh, 0, coll)} is equivalent to
- * one which first "folds" the affected arguments, and then drops them, in separate
- * steps as follows:
- * <blockquote><pre>{@code
- * mh = MethodHandles.dropArguments(mh, 1, coll.type().parameterList()); //step 2
- * mh = MethodHandles.foldArguments(mh, coll); //step 1
- * }</pre></blockquote>
- * If the target method handle consumes no arguments besides than the result
- * (if any) of the filter {@code coll}, then {@code collectArguments(mh, 0, coll)}
- * is equivalent to {@code filterReturnValue(coll, mh)}.
- * If the filter method handle {@code coll} consumes one argument and produces
- * a non-void result, then {@code collectArguments(mh, N, coll)}
- * is equivalent to {@code filterArguments(mh, N, coll)}.
- * Other equivalences are possible but would require argument permutation.
- *
- * @param target the method handle to invoke after filtering the subsequence of arguments
- * @param pos the position of the first adapter argument to pass to the filter,
- * and/or the target argument which receives the result of the filter
- * @param filter method handle to call on the subsequence of arguments
- * @return method handle which incorporates the specified argument subsequence filtering logic
- * @throws NullPointerException if either argument is null
- * @throws IllegalArgumentException if the return type of {@code filter}
- * is non-void and is not the same as the {@code pos} argument of the target,
- * or if {@code pos} is not between 0 and the target's arity, inclusive,
- * or if the resulting method handle's type would have
- * <a href="MethodHandle.html#maxarity">too many parameters</a>
- * @see MethodHandles#foldArguments
- * @see MethodHandles#filterArguments
- * @see MethodHandles#filterReturnValue
- */
public static
- MethodHandle collectArguments(MethodHandle target, int pos, MethodHandle filter) {
- MethodType newType = collectArgumentsChecks(target, pos, filter);
- return new Transformers.CollectArguments(target, filter, pos, newType);
- }
-
- private static MethodType collectArgumentsChecks(MethodHandle target, int pos, MethodHandle filter) throws RuntimeException {
- MethodType targetType = target.type();
- MethodType filterType = filter.type();
- Class<?> rtype = filterType.returnType();
- List<Class<?>> filterArgs = filterType.parameterList();
- if (rtype == void.class) {
- return targetType.insertParameterTypes(pos, filterArgs);
- }
- if (rtype != targetType.parameterType(pos)) {
- throw newIllegalArgumentException("target and filter types do not match", targetType, filterType);
- }
- return targetType.dropParameterTypes(pos, pos+1).insertParameterTypes(pos, filterArgs);
- }
+ MethodHandle collectArguments(MethodHandle target, int pos, MethodHandle filter) { return null; }
- /**
- * Adapts a target method handle by post-processing
- * its return value (if any) with a filter (another method handle).
- * The result of the filter is returned from the adapter.
- * <p>
- * If the target returns a value, the filter must accept that value as
- * its only argument.
- * If the target returns void, the filter must accept no arguments.
- * <p>
- * The return type of the filter
- * replaces the return type of the target
- * in the resulting adapted method handle.
- * The argument type of the filter (if any) must be identical to the
- * return type of the target.
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle cat = lookup().findVirtual(String.class,
- "concat", methodType(String.class, String.class));
-MethodHandle length = lookup().findVirtual(String.class,
- "length", methodType(int.class));
-System.out.println((String) cat.invokeExact("x", "y")); // xy
-MethodHandle f0 = filterReturnValue(cat, length);
-System.out.println((int) f0.invokeExact("x", "y")); // 2
- * }</pre></blockquote>
- * <p> Here is pseudocode for the resulting adapter:
- * <blockquote><pre>{@code
- * V target(A...);
- * T filter(V);
- * T adapter(A... a) {
- * V v = target(a...);
- * return filter(v);
- * }
- * // and if the target has a void return:
- * void target2(A...);
- * T filter2();
- * T adapter2(A... a) {
- * target2(a...);
- * return filter2();
- * }
- * // and if the filter has a void return:
- * V target3(A...);
- * void filter3(V);
- * void adapter3(A... a) {
- * V v = target3(a...);
- * filter3(v);
- * }
- * }</pre></blockquote>
- * @param target the method handle to invoke before filtering the return value
- * @param filter method handle to call on the return value
- * @return method handle which incorporates the specified return value filtering logic
- * @throws NullPointerException if either argument is null
- * @throws IllegalArgumentException if the argument list of {@code filter}
- * does not match the return type of target as described above
- */
public static
- MethodHandle filterReturnValue(MethodHandle target, MethodHandle filter) {
- MethodType targetType = target.type();
- MethodType filterType = filter.type();
- filterReturnValueChecks(targetType, filterType);
+ MethodHandle filterReturnValue(MethodHandle target, MethodHandle filter) { return null; }
- return new Transformers.FilterReturnValue(target, filter);
- }
-
- private static void filterReturnValueChecks(MethodType targetType, MethodType filterType) throws RuntimeException {
- Class<?> rtype = targetType.returnType();
- int filterValues = filterType.parameterCount();
- if (filterValues == 0
- ? (rtype != void.class)
- : (rtype != filterType.parameterType(0) || filterValues != 1))
- throw newIllegalArgumentException("target and filter types do not match", targetType, filterType);
- }
-
- /**
- * Adapts a target method handle by pre-processing
- * some of its arguments, and then calling the target with
- * the result of the pre-processing, inserted into the original
- * sequence of arguments.
- * <p>
- * The pre-processing is performed by {@code combiner}, a second method handle.
- * Of the arguments passed to the adapter, the first {@code N} arguments
- * are copied to the combiner, which is then called.
- * (Here, {@code N} is defined as the parameter count of the combiner.)
- * After this, control passes to the target, with any result
- * from the combiner inserted before the original {@code N} incoming
- * arguments.
- * <p>
- * If the combiner returns a value, the first parameter type of the target
- * must be identical with the return type of the combiner, and the next
- * {@code N} parameter types of the target must exactly match the parameters
- * of the combiner.
- * <p>
- * If the combiner has a void return, no result will be inserted,
- * and the first {@code N} parameter types of the target
- * must exactly match the parameters of the combiner.
- * <p>
- * The resulting adapter is the same type as the target, except that the
- * first parameter type is dropped,
- * if it corresponds to the result of the combiner.
- * <p>
- * (Note that {@link #dropArguments(MethodHandle,int,List) dropArguments} can be used to remove any arguments
- * that either the combiner or the target does not wish to receive.
- * If some of the incoming arguments are destined only for the combiner,
- * consider using {@link MethodHandle#asCollector asCollector} instead, since those
- * arguments will not need to be live on the stack on entry to the
- * target.)
- * <p><b>Example:</b>
- * <blockquote><pre>{@code
-import static java.lang.invoke.MethodHandles.*;
-import static java.lang.invoke.MethodType.*;
-...
-MethodHandle trace = publicLookup().findVirtual(java.io.PrintStream.class,
- "println", methodType(void.class, String.class))
- .bindTo(System.out);
-MethodHandle cat = lookup().findVirtual(String.class,
- "concat", methodType(String.class, String.class));
-assertEquals("boojum", (String) cat.invokeExact("boo", "jum"));
-MethodHandle catTrace = foldArguments(cat, trace);
-// also prints "boo":
-assertEquals("boojum", (String) catTrace.invokeExact("boo", "jum"));
- * }</pre></blockquote>
- * <p> Here is pseudocode for the resulting adapter:
- * <blockquote><pre>{@code
- * // there are N arguments in A...
- * T target(V, A[N]..., B...);
- * V combiner(A...);
- * T adapter(A... a, B... b) {
- * V v = combiner(a...);
- * return target(v, a..., b...);
- * }
- * // and if the combiner has a void return:
- * T target2(A[N]..., B...);
- * void combiner2(A...);
- * T adapter2(A... a, B... b) {
- * combiner2(a...);
- * return target2(a..., b...);
- * }
- * }</pre></blockquote>
- * @param target the method handle to invoke after arguments are combined
- * @param combiner method handle to call initially on the incoming arguments
- * @return method handle which incorporates the specified argument folding logic
- * @throws NullPointerException if either argument is null
- * @throws IllegalArgumentException if {@code combiner}'s return type
- * is non-void and not the same as the first argument type of
- * the target, or if the initial {@code N} argument types
- * of the target
- * (skipping one matching the {@code combiner}'s return type)
- * are not identical with the argument types of {@code combiner}
- */
public static
- MethodHandle foldArguments(MethodHandle target, MethodHandle combiner) {
- int foldPos = 0;
- MethodType targetType = target.type();
- MethodType combinerType = combiner.type();
- Class<?> rtype = foldArgumentChecks(foldPos, targetType, combinerType);
-
- return new Transformers.FoldArguments(target, combiner);
- }
-
- private static Class<?> foldArgumentChecks(int foldPos, MethodType targetType, MethodType combinerType) {
- int foldArgs = combinerType.parameterCount();
- Class<?> rtype = combinerType.returnType();
- int foldVals = rtype == void.class ? 0 : 1;
- int afterInsertPos = foldPos + foldVals;
- boolean ok = (targetType.parameterCount() >= afterInsertPos + foldArgs);
- if (ok && !(combinerType.parameterList()
- .equals(targetType.parameterList().subList(afterInsertPos,
- afterInsertPos + foldArgs))))
- ok = false;
- if (ok && foldVals != 0 && combinerType.returnType() != targetType.parameterType(0))
- ok = false;
- if (!ok)
- throw misMatchedTypes("target and combiner types", targetType, combinerType);
- return rtype;
- }
+ MethodHandle foldArguments(MethodHandle target, MethodHandle combiner) { return null; }
- /**
- * Makes a method handle which adapts a target method handle,
- * by guarding it with a test, a boolean-valued method handle.
- * If the guard fails, a fallback handle is called instead.
- * All three method handles must have the same corresponding
- * argument and return types, except that the return type
- * of the test must be boolean, and the test is allowed
- * to have fewer arguments than the other two method handles.
- * <p> Here is pseudocode for the resulting adapter:
- * <blockquote><pre>{@code
- * boolean test(A...);
- * T target(A...,B...);
- * T fallback(A...,B...);
- * T adapter(A... a,B... b) {
- * if (test(a...))
- * return target(a..., b...);
- * else
- * return fallback(a..., b...);
- * }
- * }</pre></blockquote>
- * Note that the test arguments ({@code a...} in the pseudocode) cannot
- * be modified by execution of the test, and so are passed unchanged
- * from the caller to the target or fallback as appropriate.
- * @param test method handle used for test, must return boolean
- * @param target method handle to call if test passes
- * @param fallback method handle to call if test fails
- * @return method handle which incorporates the specified if/then/else logic
- * @throws NullPointerException if any argument is null
- * @throws IllegalArgumentException if {@code test} does not return boolean,
- * or if all three method types do not match (with the return
- * type of {@code test} changed to match that of the target).
- */
public static
MethodHandle guardWithTest(MethodHandle test,
MethodHandle target,
- MethodHandle fallback) {
- MethodType gtype = test.type();
- MethodType ttype = target.type();
- MethodType ftype = fallback.type();
- if (!ttype.equals(ftype))
- throw misMatchedTypes("target and fallback types", ttype, ftype);
- if (gtype.returnType() != boolean.class)
- throw newIllegalArgumentException("guard type is not a predicate "+gtype);
- List<Class<?>> targs = ttype.parameterList();
- List<Class<?>> gargs = gtype.parameterList();
- if (!targs.equals(gargs)) {
- int gpc = gargs.size(), tpc = targs.size();
- if (gpc >= tpc || !targs.subList(0, gpc).equals(gargs))
- throw misMatchedTypes("target and test types", ttype, gtype);
- test = dropArguments(test, gpc, targs.subList(gpc, tpc));
- gtype = test.type();
- }
+ MethodHandle fallback) { return null; }
- return new Transformers.GuardWithTest(test, target, fallback);
- }
-
- static RuntimeException misMatchedTypes(String what, MethodType t1, MethodType t2) {
- return newIllegalArgumentException(what + " must match: " + t1 + " != " + t2);
- }
-
- /**
- * Makes a method handle which adapts a target method handle,
- * by running it inside an exception handler.
- * If the target returns normally, the adapter returns that value.
- * If an exception matching the specified type is thrown, the fallback
- * handle is called instead on the exception, plus the original arguments.
- * <p>
- * The target and handler must have the same corresponding
- * argument and return types, except that handler may omit trailing arguments
- * (similarly to the predicate in {@link #guardWithTest guardWithTest}).
- * Also, the handler must have an extra leading parameter of {@code exType} or a supertype.
- * <p> Here is pseudocode for the resulting adapter:
- * <blockquote><pre>{@code
- * T target(A..., B...);
- * T handler(ExType, A...);
- * T adapter(A... a, B... b) {
- * try {
- * return target(a..., b...);
- * } catch (ExType ex) {
- * return handler(ex, a...);
- * }
- * }
- * }</pre></blockquote>
- * Note that the saved arguments ({@code a...} in the pseudocode) cannot
- * be modified by execution of the target, and so are passed unchanged
- * from the caller to the handler, if the handler is invoked.
- * <p>
- * The target and handler must return the same type, even if the handler
- * always throws. (This might happen, for instance, because the handler
- * is simulating a {@code finally} clause).
- * To create such a throwing handler, compose the handler creation logic
- * with {@link #throwException throwException},
- * in order to create a method handle of the correct return type.
- * @param target method handle to call
- * @param exType the type of exception which the handler will catch
- * @param handler method handle to call if a matching exception is thrown
- * @return method handle which incorporates the specified try/catch logic
- * @throws NullPointerException if any argument is null
- * @throws IllegalArgumentException if {@code handler} does not accept
- * the given exception type, or if the method handle types do
- * not match in their return types and their
- * corresponding parameters
- */
public static
MethodHandle catchException(MethodHandle target,
Class<? extends Throwable> exType,
- MethodHandle handler) {
- MethodType ttype = target.type();
- MethodType htype = handler.type();
- if (htype.parameterCount() < 1 ||
- !htype.parameterType(0).isAssignableFrom(exType))
- throw newIllegalArgumentException("handler does not accept exception type "+exType);
- if (htype.returnType() != ttype.returnType())
- throw misMatchedTypes("target and handler return types", ttype, htype);
- List<Class<?>> targs = ttype.parameterList();
- List<Class<?>> hargs = htype.parameterList();
- hargs = hargs.subList(1, hargs.size()); // omit leading parameter from handler
- if (!targs.equals(hargs)) {
- int hpc = hargs.size(), tpc = targs.size();
- if (hpc >= tpc || !targs.subList(0, hpc).equals(hargs))
- throw misMatchedTypes("target and handler types", ttype, htype);
- }
-
- return new Transformers.CatchException(target, handler, exType);
- }
+ MethodHandle handler) { return null; }
- /**
- * Produces a method handle which will throw exceptions of the given {@code exType}.
- * The method handle will accept a single argument of {@code exType},
- * and immediately throw it as an exception.
- * The method type will nominally specify a return of {@code returnType}.
- * The return type may be anything convenient: It doesn't matter to the
- * method handle's behavior, since it will never return normally.
- * @param returnType the return type of the desired method handle
- * @param exType the parameter type of the desired method handle
- * @return method handle which can throw the given exceptions
- * @throws NullPointerException if either argument is null
- */
public static
- MethodHandle throwException(Class<?> returnType, Class<? extends Throwable> exType) {
- if (!Throwable.class.isAssignableFrom(exType))
- throw new ClassCastException(exType.getName());
-
- return new Transformers.AlwaysThrow(returnType, exType);
- }
+ MethodHandle throwException(Class<?> returnType, Class<? extends Throwable> exType) { return null; }
}
diff --git a/java/lang/invoke/MethodType.java b/java/lang/invoke/MethodType.java
index bfa7ccd5..4cb5c226 100644
--- a/java/lang/invoke/MethodType.java
+++ b/java/lang/invoke/MethodType.java
@@ -25,1227 +25,78 @@
package java.lang.invoke;
-import sun.invoke.util.Wrapper;
-import java.lang.ref.WeakReference;
-import java.lang.ref.Reference;
-import java.lang.ref.ReferenceQueue;
-import java.util.Arrays;
-import java.util.Collections;
import java.util.List;
-import java.util.Objects;
-import java.util.concurrent.ConcurrentMap;
-import java.util.concurrent.ConcurrentHashMap;
-import sun.invoke.util.BytecodeDescriptor;
-import static java.lang.invoke.MethodHandleStatics.*;
-/**
- * A method type represents the arguments and return type accepted and
- * returned by a method handle, or the arguments and return type passed
- * and expected by a method handle caller. Method types must be properly
- * matched between a method handle and all its callers,
- * and the JVM's operations enforce this matching at, specifically
- * during calls to {@link MethodHandle#invokeExact MethodHandle.invokeExact}
- * and {@link MethodHandle#invoke MethodHandle.invoke}, and during execution
- * of {@code invokedynamic} instructions.
- * <p>
- * The structure is a return type accompanied by any number of parameter types.
- * The types (primitive, {@code void}, and reference) are represented by {@link Class} objects.
- * (For ease of exposition, we treat {@code void} as if it were a type.
- * In fact, it denotes the absence of a return type.)
- * <p>
- * All instances of {@code MethodType} are immutable.
- * Two instances are completely interchangeable if they compare equal.
- * Equality depends on pairwise correspondence of the return and parameter types and on nothing else.
- * <p>
- * This type can be created only by factory methods.
- * All factory methods may cache values, though caching is not guaranteed.
- * Some factory methods are static, while others are virtual methods which
- * modify precursor method types, e.g., by changing a selected parameter.
- * <p>
- * Factory methods which operate on groups of parameter types
- * are systematically presented in two versions, so that both Java arrays and
- * Java lists can be used to work with groups of parameter types.
- * The query methods {@code parameterArray} and {@code parameterList}
- * also provide a choice between arrays and lists.
- * <p>
- * {@code MethodType} objects are sometimes derived from bytecode instructions
- * such as {@code invokedynamic}, specifically from the type descriptor strings associated
- * with the instructions in a class file's constant pool.
- * <p>
- * Like classes and strings, method types can also be represented directly
- * in a class file's constant pool as constants.
- * A method type may be loaded by an {@code ldc} instruction which refers
- * to a suitable {@code CONSTANT_MethodType} constant pool entry.
- * The entry refers to a {@code CONSTANT_Utf8} spelling for the descriptor string.
- * (For full details on method type constants,
- * see sections 4.4.8 and 5.4.3.5 of the Java Virtual Machine Specification.)
- * <p>
- * When the JVM materializes a {@code MethodType} from a descriptor string,
- * all classes named in the descriptor must be accessible, and will be loaded.
- * (But the classes need not be initialized, as is the case with a {@code CONSTANT_Class}.)
- * This loading may occur at any time before the {@code MethodType} object is first derived.
- * @author John Rose, JSR 292 EG
- */
public final
class MethodType implements java.io.Serializable {
- private static final long serialVersionUID = 292L; // {rtype, {ptype...}}
-
- // The rtype and ptypes fields define the structural identity of the method type:
- private final Class<?> rtype;
- private final Class<?>[] ptypes;
-
- // The remaining fields are caches of various sorts:
- private @Stable MethodTypeForm form; // erased form, plus cached data about primitives
- private @Stable MethodType wrapAlt; // alternative wrapped/unwrapped version
- // Android-changed: Remove adapter cache. We're not dynamically generating any
- // adapters at this point.
- // private @Stable Invokers invokers; // cache of handy higher-order adapters
- private @Stable String methodDescriptor; // cache for toMethodDescriptorString
-
- /**
- * Check the given parameters for validity and store them into the final fields.
- */
- private MethodType(Class<?> rtype, Class<?>[] ptypes, boolean trusted) {
- checkRtype(rtype);
- checkPtypes(ptypes);
- this.rtype = rtype;
- // defensively copy the array passed in by the user
- this.ptypes = trusted ? ptypes : Arrays.copyOf(ptypes, ptypes.length);
- }
-
- /**
- * Construct a temporary unchecked instance of MethodType for use only as a key to the intern table.
- * Does not check the given parameters for validity, and must be discarded after it is used as a searching key.
- * The parameters are reversed for this constructor, so that is is not accidentally used.
- */
- private MethodType(Class<?>[] ptypes, Class<?> rtype) {
- this.rtype = rtype;
- this.ptypes = ptypes;
- }
-
- /*trusted*/ MethodTypeForm form() { return form; }
- /*trusted*/ /** @hide */ public Class<?> rtype() { return rtype; }
- /*trusted*/ /** @hide */ public Class<?>[] ptypes() { return ptypes; }
- // Android-changed: Removed method setForm. It's unused in the JDK and there's no
- // good reason to allow the form to be set externally.
- //
- // void setForm(MethodTypeForm f) { form = f; }
-
- /** This number, mandated by the JVM spec as 255,
- * is the maximum number of <em>slots</em>
- * that any Java method can receive in its argument list.
- * It limits both JVM signatures and method type objects.
- * The longest possible invocation will look like
- * {@code staticMethod(arg1, arg2, ..., arg255)} or
- * {@code x.virtualMethod(arg1, arg2, ..., arg254)}.
- */
- /*non-public*/ static final int MAX_JVM_ARITY = 255; // this is mandated by the JVM spec.
-
- /** This number is the maximum arity of a method handle, 254.
- * It is derived from the absolute JVM-imposed arity by subtracting one,
- * which is the slot occupied by the method handle itself at the
- * beginning of the argument list used to invoke the method handle.
- * The longest possible invocation will look like
- * {@code mh.invoke(arg1, arg2, ..., arg254)}.
- */
- // Issue: Should we allow MH.invokeWithArguments to go to the full 255?
- /*non-public*/ static final int MAX_MH_ARITY = MAX_JVM_ARITY-1; // deduct one for mh receiver
-
- /** This number is the maximum arity of a method handle invoker, 253.
- * It is derived from the absolute JVM-imposed arity by subtracting two,
- * which are the slots occupied by invoke method handle, and the
- * target method handle, which are both at the beginning of the argument
- * list used to invoke the target method handle.
- * The longest possible invocation will look like
- * {@code invokermh.invoke(targetmh, arg1, arg2, ..., arg253)}.
- */
- /*non-public*/ static final int MAX_MH_INVOKER_ARITY = MAX_MH_ARITY-1; // deduct one more for invoker
-
- private static void checkRtype(Class<?> rtype) {
- Objects.requireNonNull(rtype);
- }
- private static void checkPtype(Class<?> ptype) {
- Objects.requireNonNull(ptype);
- if (ptype == void.class)
- throw newIllegalArgumentException("parameter type cannot be void");
- }
- /** Return number of extra slots (count of long/double args). */
- private static int checkPtypes(Class<?>[] ptypes) {
- int slots = 0;
- for (Class<?> ptype : ptypes) {
- checkPtype(ptype);
- if (ptype == double.class || ptype == long.class) {
- slots++;
- }
- }
- checkSlotCount(ptypes.length + slots);
- return slots;
- }
- static void checkSlotCount(int count) {
- assert((MAX_JVM_ARITY & (MAX_JVM_ARITY+1)) == 0);
- // MAX_JVM_ARITY must be power of 2 minus 1 for following code trick to work:
- if ((count & MAX_JVM_ARITY) != count)
- throw newIllegalArgumentException("bad parameter count "+count);
- }
- private static IndexOutOfBoundsException newIndexOutOfBoundsException(Object num) {
- if (num instanceof Integer) num = "bad index: "+num;
- return new IndexOutOfBoundsException(num.toString());
- }
-
- static final ConcurrentWeakInternSet<MethodType> internTable = new ConcurrentWeakInternSet<>();
-
- static final Class<?>[] NO_PTYPES = {};
-
- /**
- * Finds or creates an instance of the given method type.
- * @param rtype the return type
- * @param ptypes the parameter types
- * @return a method type with the given components
- * @throws NullPointerException if {@code rtype} or {@code ptypes} or any element of {@code ptypes} is null
- * @throws IllegalArgumentException if any element of {@code ptypes} is {@code void.class}
- */
public static
MethodType methodType(Class<?> rtype, Class<?>[] ptypes) {
- return makeImpl(rtype, ptypes, false);
+ return null;
}
- /**
- * Finds or creates a method type with the given components.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param rtype the return type
- * @param ptypes the parameter types
- * @return a method type with the given components
- * @throws NullPointerException if {@code rtype} or {@code ptypes} or any element of {@code ptypes} is null
- * @throws IllegalArgumentException if any element of {@code ptypes} is {@code void.class}
- */
public static
MethodType methodType(Class<?> rtype, List<Class<?>> ptypes) {
- boolean notrust = false; // random List impl. could return evil ptypes array
- return makeImpl(rtype, listToArray(ptypes), notrust);
+ return null;
}
- private static Class<?>[] listToArray(List<Class<?>> ptypes) {
- // sanity check the size before the toArray call, since size might be huge
- checkSlotCount(ptypes.size());
- return ptypes.toArray(NO_PTYPES);
- }
-
- /**
- * Finds or creates a method type with the given components.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * The leading parameter type is prepended to the remaining array.
- * @param rtype the return type
- * @param ptype0 the first parameter type
- * @param ptypes the remaining parameter types
- * @return a method type with the given components
- * @throws NullPointerException if {@code rtype} or {@code ptype0} or {@code ptypes} or any element of {@code ptypes} is null
- * @throws IllegalArgumentException if {@code ptype0} or {@code ptypes} or any element of {@code ptypes} is {@code void.class}
- */
public static
- MethodType methodType(Class<?> rtype, Class<?> ptype0, Class<?>... ptypes) {
- Class<?>[] ptypes1 = new Class<?>[1+ptypes.length];
- ptypes1[0] = ptype0;
- System.arraycopy(ptypes, 0, ptypes1, 1, ptypes.length);
- return makeImpl(rtype, ptypes1, true);
- }
+ MethodType methodType(Class<?> rtype, Class<?> ptype0, Class<?>... ptypes) { return null; }
- /**
- * Finds or creates a method type with the given components.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * The resulting method has no parameter types.
- * @param rtype the return type
- * @return a method type with the given return value
- * @throws NullPointerException if {@code rtype} is null
- */
public static
- MethodType methodType(Class<?> rtype) {
- return makeImpl(rtype, NO_PTYPES, true);
- }
+ MethodType methodType(Class<?> rtype) { return null; }
- /**
- * Finds or creates a method type with the given components.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * The resulting method has the single given parameter type.
- * @param rtype the return type
- * @param ptype0 the parameter type
- * @return a method type with the given return value and parameter type
- * @throws NullPointerException if {@code rtype} or {@code ptype0} is null
- * @throws IllegalArgumentException if {@code ptype0} is {@code void.class}
- */
public static
- MethodType methodType(Class<?> rtype, Class<?> ptype0) {
- return makeImpl(rtype, new Class<?>[]{ ptype0 }, true);
- }
+ MethodType methodType(Class<?> rtype, Class<?> ptype0) { return null; }
- /**
- * Finds or creates a method type with the given components.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * The resulting method has the same parameter types as {@code ptypes},
- * and the specified return type.
- * @param rtype the return type
- * @param ptypes the method type which supplies the parameter types
- * @return a method type with the given components
- * @throws NullPointerException if {@code rtype} or {@code ptypes} is null
- */
public static
- MethodType methodType(Class<?> rtype, MethodType ptypes) {
- return makeImpl(rtype, ptypes.ptypes, true);
- }
-
- /**
- * Sole factory method to find or create an interned method type.
- * @param rtype desired return type
- * @param ptypes desired parameter types
- * @param trusted whether the ptypes can be used without cloning
- * @return the unique method type of the desired structure
- */
- /*trusted*/ static
- MethodType makeImpl(Class<?> rtype, Class<?>[] ptypes, boolean trusted) {
- MethodType mt = internTable.get(new MethodType(ptypes, rtype));
- if (mt != null)
- return mt;
- if (ptypes.length == 0) {
- ptypes = NO_PTYPES; trusted = true;
- }
- mt = new MethodType(rtype, ptypes, trusted);
- // promote the object to the Real Thing, and reprobe
- mt.form = MethodTypeForm.findForm(mt);
- return internTable.add(mt);
- }
- private static final MethodType[] objectOnlyTypes = new MethodType[20];
+ MethodType methodType(Class<?> rtype, MethodType ptypes) { return null; }
- /**
- * Finds or creates a method type whose components are {@code Object} with an optional trailing {@code Object[]} array.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * All parameters and the return type will be {@code Object},
- * except the final array parameter if any, which will be {@code Object[]}.
- * @param objectArgCount number of parameters (excluding the final array parameter if any)
- * @param finalArray whether there will be a trailing array parameter, of type {@code Object[]}
- * @return a generally applicable method type, for all calls of the given fixed argument count and a collected array of further arguments
- * @throws IllegalArgumentException if {@code objectArgCount} is negative or greater than 255 (or 254, if {@code finalArray} is true)
- * @see #genericMethodType(int)
- */
public static
- MethodType genericMethodType(int objectArgCount, boolean finalArray) {
- MethodType mt;
- checkSlotCount(objectArgCount);
- int ivarargs = (!finalArray ? 0 : 1);
- int ootIndex = objectArgCount*2 + ivarargs;
- if (ootIndex < objectOnlyTypes.length) {
- mt = objectOnlyTypes[ootIndex];
- if (mt != null) return mt;
- }
- Class<?>[] ptypes = new Class<?>[objectArgCount + ivarargs];
- Arrays.fill(ptypes, Object.class);
- if (ivarargs != 0) ptypes[objectArgCount] = Object[].class;
- mt = makeImpl(Object.class, ptypes, true);
- if (ootIndex < objectOnlyTypes.length) {
- objectOnlyTypes[ootIndex] = mt; // cache it here also!
- }
- return mt;
- }
+ MethodType genericMethodType(int objectArgCount, boolean finalArray) { return null; }
- /**
- * Finds or creates a method type whose components are all {@code Object}.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * All parameters and the return type will be Object.
- * @param objectArgCount number of parameters
- * @return a generally applicable method type, for all calls of the given argument count
- * @throws IllegalArgumentException if {@code objectArgCount} is negative or greater than 255
- * @see #genericMethodType(int, boolean)
- */
public static
- MethodType genericMethodType(int objectArgCount) {
- return genericMethodType(objectArgCount, false);
- }
-
- /**
- * Finds or creates a method type with a single different parameter type.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param num the index (zero-based) of the parameter type to change
- * @param nptype a new parameter type to replace the old one with
- * @return the same type, except with the selected parameter changed
- * @throws IndexOutOfBoundsException if {@code num} is not a valid index into {@code parameterArray()}
- * @throws IllegalArgumentException if {@code nptype} is {@code void.class}
- * @throws NullPointerException if {@code nptype} is null
- */
- public MethodType changeParameterType(int num, Class<?> nptype) {
- if (parameterType(num) == nptype) return this;
- checkPtype(nptype);
- Class<?>[] nptypes = ptypes.clone();
- nptypes[num] = nptype;
- return makeImpl(rtype, nptypes, true);
- }
-
- /**
- * Finds or creates a method type with additional parameter types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param num the position (zero-based) of the inserted parameter type(s)
- * @param ptypesToInsert zero or more new parameter types to insert into the parameter list
- * @return the same type, except with the selected parameter(s) inserted
- * @throws IndexOutOfBoundsException if {@code num} is negative or greater than {@code parameterCount()}
- * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
- * or if the resulting method type would have more than 255 parameter slots
- * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
- */
- public MethodType insertParameterTypes(int num, Class<?>... ptypesToInsert) {
- int len = ptypes.length;
- if (num < 0 || num > len)
- throw newIndexOutOfBoundsException(num);
- int ins = checkPtypes(ptypesToInsert);
- checkSlotCount(parameterSlotCount() + ptypesToInsert.length + ins);
- int ilen = ptypesToInsert.length;
- if (ilen == 0) return this;
- Class<?>[] nptypes = Arrays.copyOfRange(ptypes, 0, len+ilen);
- System.arraycopy(nptypes, num, nptypes, num+ilen, len-num);
- System.arraycopy(ptypesToInsert, 0, nptypes, num, ilen);
- return makeImpl(rtype, nptypes, true);
- }
-
- /**
- * Finds or creates a method type with additional parameter types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param ptypesToInsert zero or more new parameter types to insert after the end of the parameter list
- * @return the same type, except with the selected parameter(s) appended
- * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
- * or if the resulting method type would have more than 255 parameter slots
- * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
- */
- public MethodType appendParameterTypes(Class<?>... ptypesToInsert) {
- return insertParameterTypes(parameterCount(), ptypesToInsert);
- }
-
- /**
- * Finds or creates a method type with additional parameter types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param num the position (zero-based) of the inserted parameter type(s)
- * @param ptypesToInsert zero or more new parameter types to insert into the parameter list
- * @return the same type, except with the selected parameter(s) inserted
- * @throws IndexOutOfBoundsException if {@code num} is negative or greater than {@code parameterCount()}
- * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
- * or if the resulting method type would have more than 255 parameter slots
- * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
- */
- public MethodType insertParameterTypes(int num, List<Class<?>> ptypesToInsert) {
- return insertParameterTypes(num, listToArray(ptypesToInsert));
- }
-
- /**
- * Finds or creates a method type with additional parameter types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param ptypesToInsert zero or more new parameter types to insert after the end of the parameter list
- * @return the same type, except with the selected parameter(s) appended
- * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
- * or if the resulting method type would have more than 255 parameter slots
- * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
- */
- public MethodType appendParameterTypes(List<Class<?>> ptypesToInsert) {
- return insertParameterTypes(parameterCount(), ptypesToInsert);
- }
-
- /**
- * Finds or creates a method type with modified parameter types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param start the position (zero-based) of the first replaced parameter type(s)
- * @param end the position (zero-based) after the last replaced parameter type(s)
- * @param ptypesToInsert zero or more new parameter types to insert into the parameter list
- * @return the same type, except with the selected parameter(s) replaced
- * @throws IndexOutOfBoundsException if {@code start} is negative or greater than {@code parameterCount()}
- * or if {@code end} is negative or greater than {@code parameterCount()}
- * or if {@code start} is greater than {@code end}
- * @throws IllegalArgumentException if any element of {@code ptypesToInsert} is {@code void.class}
- * or if the resulting method type would have more than 255 parameter slots
- * @throws NullPointerException if {@code ptypesToInsert} or any of its elements is null
- */
- /*non-public*/ MethodType replaceParameterTypes(int start, int end, Class<?>... ptypesToInsert) {
- if (start == end)
- return insertParameterTypes(start, ptypesToInsert);
- int len = ptypes.length;
- if (!(0 <= start && start <= end && end <= len))
- throw newIndexOutOfBoundsException("start="+start+" end="+end);
- int ilen = ptypesToInsert.length;
- if (ilen == 0)
- return dropParameterTypes(start, end);
- return dropParameterTypes(start, end).insertParameterTypes(start, ptypesToInsert);
- }
-
- /** Replace the last arrayLength parameter types with the component type of arrayType.
- * @param arrayType any array type
- * @param arrayLength the number of parameter types to change
- * @return the resulting type
- */
- /*non-public*/ MethodType asSpreaderType(Class<?> arrayType, int arrayLength) {
- assert(parameterCount() >= arrayLength);
- int spreadPos = ptypes.length - arrayLength;
- if (arrayLength == 0) return this; // nothing to change
- if (arrayType == Object[].class) {
- if (isGeneric()) return this; // nothing to change
- if (spreadPos == 0) {
- // no leading arguments to preserve; go generic
- MethodType res = genericMethodType(arrayLength);
- if (rtype != Object.class) {
- res = res.changeReturnType(rtype);
- }
- return res;
- }
- }
- Class<?> elemType = arrayType.getComponentType();
- assert(elemType != null);
- for (int i = spreadPos; i < ptypes.length; i++) {
- if (ptypes[i] != elemType) {
- Class<?>[] fixedPtypes = ptypes.clone();
- Arrays.fill(fixedPtypes, i, ptypes.length, elemType);
- return methodType(rtype, fixedPtypes);
- }
- }
- return this; // arguments check out; no change
- }
-
- /** Return the leading parameter type, which must exist and be a reference.
- * @return the leading parameter type, after error checks
- */
- /*non-public*/ Class<?> leadingReferenceParameter() {
- Class<?> ptype;
- if (ptypes.length == 0 ||
- (ptype = ptypes[0]).isPrimitive())
- throw newIllegalArgumentException("no leading reference parameter");
- return ptype;
- }
-
- /** Delete the last parameter type and replace it with arrayLength copies of the component type of arrayType.
- * @param arrayType any array type
- * @param arrayLength the number of parameter types to insert
- * @return the resulting type
- */
- /*non-public*/ MethodType asCollectorType(Class<?> arrayType, int arrayLength) {
- assert(parameterCount() >= 1);
- assert(lastParameterType().isAssignableFrom(arrayType));
- MethodType res;
- if (arrayType == Object[].class) {
- res = genericMethodType(arrayLength);
- if (rtype != Object.class) {
- res = res.changeReturnType(rtype);
- }
- } else {
- Class<?> elemType = arrayType.getComponentType();
- assert(elemType != null);
- res = methodType(rtype, Collections.nCopies(arrayLength, elemType));
- }
- if (ptypes.length == 1) {
- return res;
- } else {
- return res.insertParameterTypes(0, parameterList().subList(0, ptypes.length-1));
- }
- }
-
- /**
- * Finds or creates a method type with some parameter types omitted.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param start the index (zero-based) of the first parameter type to remove
- * @param end the index (greater than {@code start}) of the first parameter type after not to remove
- * @return the same type, except with the selected parameter(s) removed
- * @throws IndexOutOfBoundsException if {@code start} is negative or greater than {@code parameterCount()}
- * or if {@code end} is negative or greater than {@code parameterCount()}
- * or if {@code start} is greater than {@code end}
- */
- public MethodType dropParameterTypes(int start, int end) {
- int len = ptypes.length;
- if (!(0 <= start && start <= end && end <= len))
- throw newIndexOutOfBoundsException("start="+start+" end="+end);
- if (start == end) return this;
- Class<?>[] nptypes;
- if (start == 0) {
- if (end == len) {
- // drop all parameters
- nptypes = NO_PTYPES;
- } else {
- // drop initial parameter(s)
- nptypes = Arrays.copyOfRange(ptypes, end, len);
- }
- } else {
- if (end == len) {
- // drop trailing parameter(s)
- nptypes = Arrays.copyOfRange(ptypes, 0, start);
- } else {
- int tail = len - end;
- nptypes = Arrays.copyOfRange(ptypes, 0, start + tail);
- System.arraycopy(ptypes, end, nptypes, start, tail);
- }
- }
- return makeImpl(rtype, nptypes, true);
- }
-
- /**
- * Finds or creates a method type with a different return type.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * @param nrtype a return parameter type to replace the old one with
- * @return the same type, except with the return type change
- * @throws NullPointerException if {@code nrtype} is null
- */
- public MethodType changeReturnType(Class<?> nrtype) {
- if (returnType() == nrtype) return this;
- return makeImpl(nrtype, ptypes, true);
- }
-
- /**
- * Reports if this type contains a primitive argument or return value.
- * The return type {@code void} counts as a primitive.
- * @return true if any of the types are primitives
- */
- public boolean hasPrimitives() {
- return form.hasPrimitives();
- }
-
- /**
- * Reports if this type contains a wrapper argument or return value.
- * Wrappers are types which box primitive values, such as {@link Integer}.
- * The reference type {@code java.lang.Void} counts as a wrapper,
- * if it occurs as a return type.
- * @return true if any of the types are wrappers
- */
- public boolean hasWrappers() {
- return unwrap() != this;
- }
-
- /**
- * Erases all reference types to {@code Object}.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * All primitive types (including {@code void}) will remain unchanged.
- * @return a version of the original type with all reference types replaced
- */
- public MethodType erase() {
- return form.erasedType();
- }
-
- /**
- * Erases all reference types to {@code Object}, and all subword types to {@code int}.
- * This is the reduced type polymorphism used by private methods
- * such as {@link MethodHandle#invokeBasic invokeBasic}.
- * @return a version of the original type with all reference and subword types replaced
- */
- /*non-public*/ MethodType basicType() {
- return form.basicType();
- }
-
- /**
- * @return a version of the original type with MethodHandle prepended as the first argument
- */
- /*non-public*/ MethodType invokerType() {
- return insertParameterTypes(0, MethodHandle.class);
- }
+ MethodType genericMethodType(int objectArgCount) { return null; }
- /**
- * Converts all types, both reference and primitive, to {@code Object}.
- * Convenience method for {@link #genericMethodType(int) genericMethodType}.
- * The expression {@code type.wrap().erase()} produces the same value
- * as {@code type.generic()}.
- * @return a version of the original type with all types replaced
- */
- public MethodType generic() {
- return genericMethodType(parameterCount());
- }
+ public MethodType changeParameterType(int num, Class<?> nptype) { return null; }
- /*non-public*/ boolean isGeneric() {
- return this == erase() && !hasPrimitives();
- }
+ public MethodType insertParameterTypes(int num, Class<?>... ptypesToInsert) { return null; }
- /**
- * Converts all primitive types to their corresponding wrapper types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * All reference types (including wrapper types) will remain unchanged.
- * A {@code void} return type is changed to the type {@code java.lang.Void}.
- * The expression {@code type.wrap().erase()} produces the same value
- * as {@code type.generic()}.
- * @return a version of the original type with all primitive types replaced
- */
- public MethodType wrap() {
- return hasPrimitives() ? wrapWithPrims(this) : this;
- }
+ public MethodType appendParameterTypes(Class<?>... ptypesToInsert) { return null; }
- /**
- * Converts all wrapper types to their corresponding primitive types.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * All primitive types (including {@code void}) will remain unchanged.
- * A return type of {@code java.lang.Void} is changed to {@code void}.
- * @return a version of the original type with all wrapper types replaced
- */
- public MethodType unwrap() {
- MethodType noprims = !hasPrimitives() ? this : wrapWithPrims(this);
- return unwrapWithNoPrims(noprims);
- }
+ public MethodType insertParameterTypes(int num, List<Class<?>> ptypesToInsert) { return null; }
- private static MethodType wrapWithPrims(MethodType pt) {
- assert(pt.hasPrimitives());
- MethodType wt = pt.wrapAlt;
- if (wt == null) {
- // fill in lazily
- wt = MethodTypeForm.canonicalize(pt, MethodTypeForm.WRAP, MethodTypeForm.WRAP);
- assert(wt != null);
- pt.wrapAlt = wt;
- }
- return wt;
- }
+ public MethodType appendParameterTypes(List<Class<?>> ptypesToInsert) { return null; }
- private static MethodType unwrapWithNoPrims(MethodType wt) {
- assert(!wt.hasPrimitives());
- MethodType uwt = wt.wrapAlt;
- if (uwt == null) {
- // fill in lazily
- uwt = MethodTypeForm.canonicalize(wt, MethodTypeForm.UNWRAP, MethodTypeForm.UNWRAP);
- if (uwt == null)
- uwt = wt; // type has no wrappers or prims at all
- wt.wrapAlt = uwt;
- }
- return uwt;
- }
+ public MethodType dropParameterTypes(int start, int end) { return null; }
- /**
- * Returns the parameter type at the specified index, within this method type.
- * @param num the index (zero-based) of the desired parameter type
- * @return the selected parameter type
- * @throws IndexOutOfBoundsException if {@code num} is not a valid index into {@code parameterArray()}
- */
- public Class<?> parameterType(int num) {
- return ptypes[num];
- }
- /**
- * Returns the number of parameter types in this method type.
- * @return the number of parameter types
- */
- public int parameterCount() {
- return ptypes.length;
- }
- /**
- * Returns the return type of this method type.
- * @return the return type
- */
- public Class<?> returnType() {
- return rtype;
- }
+ public MethodType changeReturnType(Class<?> nrtype) { return null; }
- /**
- * Presents the parameter types as a list (a convenience method).
- * The list will be immutable.
- * @return the parameter types (as an immutable list)
- */
- public List<Class<?>> parameterList() {
- return Collections.unmodifiableList(Arrays.asList(ptypes.clone()));
- }
+ public boolean hasPrimitives() { return false; }
- /*non-public*/ Class<?> lastParameterType() {
- int len = ptypes.length;
- return len == 0 ? void.class : ptypes[len-1];
- }
+ public boolean hasWrappers() { return false; }
- /**
- * Presents the parameter types as an array (a convenience method).
- * Changes to the array will not result in changes to the type.
- * @return the parameter types (as a fresh copy if necessary)
- */
- public Class<?>[] parameterArray() {
- return ptypes.clone();
- }
+ public MethodType erase() { return null; }
- /**
- * Compares the specified object with this type for equality.
- * That is, it returns <tt>true</tt> if and only if the specified object
- * is also a method type with exactly the same parameters and return type.
- * @param x object to compare
- * @see Object#equals(Object)
- */
- @Override
- public boolean equals(Object x) {
- return this == x || x instanceof MethodType && equals((MethodType)x);
- }
+ public MethodType generic() { return null; }
- private boolean equals(MethodType that) {
- return this.rtype == that.rtype
- && Arrays.equals(this.ptypes, that.ptypes);
- }
+ public MethodType wrap() { return null; }
- /**
- * Returns the hash code value for this method type.
- * It is defined to be the same as the hashcode of a List
- * whose elements are the return type followed by the
- * parameter types.
- * @return the hash code value for this method type
- * @see Object#hashCode()
- * @see #equals(Object)
- * @see List#hashCode()
- */
- @Override
- public int hashCode() {
- int hashCode = 31 + rtype.hashCode();
- for (Class<?> ptype : ptypes)
- hashCode = 31*hashCode + ptype.hashCode();
- return hashCode;
- }
+ public MethodType unwrap() { return null; }
- /**
- * Returns a string representation of the method type,
- * of the form {@code "(PT0,PT1...)RT"}.
- * The string representation of a method type is a
- * parenthesis enclosed, comma separated list of type names,
- * followed immediately by the return type.
- * <p>
- * Each type is represented by its
- * {@link java.lang.Class#getSimpleName simple name}.
- */
- @Override
- public String toString() {
- StringBuilder sb = new StringBuilder();
- sb.append("(");
- for (int i = 0; i < ptypes.length; i++) {
- if (i > 0) sb.append(",");
- sb.append(ptypes[i].getSimpleName());
- }
- sb.append(")");
- sb.append(rtype.getSimpleName());
- return sb.toString();
- }
+ public Class<?> parameterType(int num) { return null; }
- /** True if the old return type can always be viewed (w/o casting) under new return type,
- * and the new parameters can be viewed (w/o casting) under the old parameter types.
- */
- // Android-changed: Removed implementation details.
- // boolean isViewableAs(MethodType newType, boolean keepInterfaces);
- // boolean parametersAreViewableAs(MethodType newType, boolean keepInterfaces);
- /*non-public*/
- boolean isConvertibleTo(MethodType newType) {
- MethodTypeForm oldForm = this.form();
- MethodTypeForm newForm = newType.form();
- if (oldForm == newForm)
- // same parameter count, same primitive/object mix
- return true;
- if (!canConvert(returnType(), newType.returnType()))
- return false;
- Class<?>[] srcTypes = newType.ptypes;
- Class<?>[] dstTypes = ptypes;
- if (srcTypes == dstTypes)
- return true;
- int argc;
- if ((argc = srcTypes.length) != dstTypes.length)
- return false;
- if (argc <= 1) {
- if (argc == 1 && !canConvert(srcTypes[0], dstTypes[0]))
- return false;
- return true;
- }
- if ((oldForm.primitiveParameterCount() == 0 && oldForm.erasedType == this) ||
- (newForm.primitiveParameterCount() == 0 && newForm.erasedType == newType)) {
- // Somewhat complicated test to avoid a loop of 2 or more trips.
- // If either type has only Object parameters, we know we can convert.
- assert(canConvertParameters(srcTypes, dstTypes));
- return true;
- }
- return canConvertParameters(srcTypes, dstTypes);
- }
-
- /** Returns true if MHs.explicitCastArguments produces the same result as MH.asType.
- * If the type conversion is impossible for either, the result should be false.
- */
- /*non-public*/
- boolean explicitCastEquivalentToAsType(MethodType newType) {
- if (this == newType) return true;
- if (!explicitCastEquivalentToAsType(rtype, newType.rtype)) {
- return false;
- }
- Class<?>[] srcTypes = newType.ptypes;
- Class<?>[] dstTypes = ptypes;
- if (dstTypes == srcTypes) {
- return true;
- }
- assert(dstTypes.length == srcTypes.length);
- for (int i = 0; i < dstTypes.length; i++) {
- if (!explicitCastEquivalentToAsType(srcTypes[i], dstTypes[i])) {
- return false;
- }
- }
- return true;
- }
-
- /** Reports true if the src can be converted to the dst, by both asType and MHs.eCE,
- * and with the same effect.
- * MHs.eCA has the following "upgrades" to MH.asType:
- * 1. interfaces are unchecked (that is, treated as if aliased to Object)
- * Therefore, {@code Object->CharSequence} is possible in both cases but has different semantics
- * 2a. the full matrix of primitive-to-primitive conversions is supported
- * Narrowing like {@code long->byte} and basic-typing like {@code boolean->int}
- * are not supported by asType, but anything supported by asType is equivalent
- * with MHs.eCE.
- * 2b. conversion of void->primitive means explicit cast has to insert zero/false/null.
- * 3a. unboxing conversions can be followed by the full matrix of primitive conversions
- * 3b. unboxing of null is permitted (creates a zero primitive value)
- * Other than interfaces, reference-to-reference conversions are the same.
- * Boxing primitives to references is the same for both operators.
- */
- private static boolean explicitCastEquivalentToAsType(Class<?> src, Class<?> dst) {
- if (src == dst || dst == Object.class || dst == void.class) {
- return true;
- } else if (src.isPrimitive() && src != void.class) {
- // Could be a prim/prim conversion, where casting is a strict superset.
- // Or a boxing conversion, which is always to an exact wrapper class.
- return canConvert(src, dst);
- } else if (dst.isPrimitive()) {
- // Unboxing behavior is different between MHs.eCA & MH.asType (see 3b).
- return false;
- } else {
- // R->R always works, but we have to avoid a check-cast to an interface.
- return !dst.isInterface() || dst.isAssignableFrom(src);
- }
- }
+ public int parameterCount() { return 0; }
- private boolean canConvertParameters(Class<?>[] srcTypes, Class<?>[] dstTypes) {
- for (int i = 0; i < srcTypes.length; i++) {
- if (!canConvert(srcTypes[i], dstTypes[i])) {
- return false;
- }
- }
- return true;
- }
+ public Class<?> returnType() { return null; }
- /*non-public*/
- static boolean canConvert(Class<?> src, Class<?> dst) {
- // short-circuit a few cases:
- if (src == dst || src == Object.class || dst == Object.class) return true;
- // the remainder of this logic is documented in MethodHandle.asType
- if (src.isPrimitive()) {
- // can force void to an explicit null, a la reflect.Method.invoke
- // can also force void to a primitive zero, by analogy
- if (src == void.class) return true; //or !dst.isPrimitive()?
- Wrapper sw = Wrapper.forPrimitiveType(src);
- if (dst.isPrimitive()) {
- // P->P must widen
- return Wrapper.forPrimitiveType(dst).isConvertibleFrom(sw);
- } else {
- // P->R must box and widen
- return dst.isAssignableFrom(sw.wrapperType());
- }
- } else if (dst.isPrimitive()) {
- // any value can be dropped
- if (dst == void.class) return true;
- Wrapper dw = Wrapper.forPrimitiveType(dst);
- // R->P must be able to unbox (from a dynamically chosen type) and widen
- // For example:
- // Byte/Number/Comparable/Object -> dw:Byte -> byte.
- // Character/Comparable/Object -> dw:Character -> char
- // Boolean/Comparable/Object -> dw:Boolean -> boolean
- // This means that dw must be cast-compatible with src.
- if (src.isAssignableFrom(dw.wrapperType())) {
- return true;
- }
- // The above does not work if the source reference is strongly typed
- // to a wrapper whose primitive must be widened. For example:
- // Byte -> unbox:byte -> short/int/long/float/double
- // Character -> unbox:char -> int/long/float/double
- if (Wrapper.isWrapperType(src) &&
- dw.isConvertibleFrom(Wrapper.forWrapperType(src))) {
- // can unbox from src and then widen to dst
- return true;
- }
- // We have already covered cases which arise due to runtime unboxing
- // of a reference type which covers several wrapper types:
- // Object -> cast:Integer -> unbox:int -> long/float/double
- // Serializable -> cast:Byte -> unbox:byte -> byte/short/int/long/float/double
- // An marginal case is Number -> dw:Character -> char, which would be OK if there were a
- // subclass of Number which wraps a value that can convert to char.
- // Since there is none, we don't need an extra check here to cover char or boolean.
- return false;
- } else {
- // R->R always works, since null is always valid dynamically
- return true;
- }
- }
+ public List<Class<?>> parameterList() { return null; }
- /** Reports the number of JVM stack slots required to invoke a method
- * of this type. Note that (for historical reasons) the JVM requires
- * a second stack slot to pass long and double arguments.
- * So this method returns {@link #parameterCount() parameterCount} plus the
- * number of long and double parameters (if any).
- * <p>
- * This method is included for the benefit of applications that must
- * generate bytecodes that process method handles and invokedynamic.
- * @return the number of JVM stack slots for this type's parameters
- */
- /*non-public*/ int parameterSlotCount() {
- return form.parameterSlotCount();
- }
+ public Class<?>[] parameterArray() { return null; }
- /// Queries which have to do with the bytecode architecture
-
- // Android-changed: These methods aren't needed on Android and are unused within the JDK.
- //
- // int parameterSlotDepth(int num);
- // int returnSlotCount();
- //
- // Android-changed: Removed cache of higher order adapters.
- //
- // Invokers invokers();
-
- /**
- * Finds or creates an instance of a method type, given the spelling of its bytecode descriptor.
- * Convenience method for {@link #methodType(java.lang.Class, java.lang.Class[]) methodType}.
- * Any class or interface name embedded in the descriptor string
- * will be resolved by calling {@link ClassLoader#loadClass(java.lang.String)}
- * on the given loader (or if it is null, on the system class loader).
- * <p>
- * Note that it is possible to encounter method types which cannot be
- * constructed by this method, because their component types are
- * not all reachable from a common class loader.
- * <p>
- * This method is included for the benefit of applications that must
- * generate bytecodes that process method handles and {@code invokedynamic}.
- * @param descriptor a bytecode-level type descriptor string "(T...)T"
- * @param loader the class loader in which to look up the types
- * @return a method type matching the bytecode-level type descriptor
- * @throws NullPointerException if the string is null
- * @throws IllegalArgumentException if the string is not well-formed
- * @throws TypeNotPresentException if a named type cannot be found
- */
public static MethodType fromMethodDescriptorString(String descriptor, ClassLoader loader)
- throws IllegalArgumentException, TypeNotPresentException
- {
- if (!descriptor.startsWith("(") || // also generates NPE if needed
- descriptor.indexOf(')') < 0 ||
- descriptor.indexOf('.') >= 0)
- throw newIllegalArgumentException("not a method descriptor: "+descriptor);
- List<Class<?>> types = BytecodeDescriptor.parseMethod(descriptor, loader);
- Class<?> rtype = types.remove(types.size() - 1);
- checkSlotCount(types.size());
- Class<?>[] ptypes = listToArray(types);
- return makeImpl(rtype, ptypes, true);
- }
-
- /**
- * Produces a bytecode descriptor representation of the method type.
- * <p>
- * Note that this is not a strict inverse of {@link #fromMethodDescriptorString fromMethodDescriptorString}.
- * Two distinct classes which share a common name but have different class loaders
- * will appear identical when viewed within descriptor strings.
- * <p>
- * This method is included for the benefit of applications that must
- * generate bytecodes that process method handles and {@code invokedynamic}.
- * {@link #fromMethodDescriptorString(java.lang.String, java.lang.ClassLoader) fromMethodDescriptorString},
- * because the latter requires a suitable class loader argument.
- * @return the bytecode type descriptor representation
- */
- public String toMethodDescriptorString() {
- String desc = methodDescriptor;
- if (desc == null) {
- desc = BytecodeDescriptor.unparse(this);
- methodDescriptor = desc;
- }
- return desc;
- }
-
- /*non-public*/ static String toFieldDescriptorString(Class<?> cls) {
- return BytecodeDescriptor.unparse(cls);
- }
+ throws IllegalArgumentException, TypeNotPresentException { return null; }
- /// Serialization.
-
- /**
- * There are no serializable fields for {@code MethodType}.
- */
- private static final java.io.ObjectStreamField[] serialPersistentFields = { };
-
- /**
- * Save the {@code MethodType} instance to a stream.
- *
- * @serialData
- * For portability, the serialized format does not refer to named fields.
- * Instead, the return type and parameter type arrays are written directly
- * from the {@code writeObject} method, using two calls to {@code s.writeObject}
- * as follows:
- * <blockquote><pre>{@code
-s.writeObject(this.returnType());
-s.writeObject(this.parameterArray());
- * }</pre></blockquote>
- * <p>
- * The deserialized field values are checked as if they were
- * provided to the factory method {@link #methodType(Class,Class[]) methodType}.
- * For example, null values, or {@code void} parameter types,
- * will lead to exceptions during deserialization.
- * @param s the stream to write the object to
- * @throws java.io.IOException if there is a problem writing the object
- */
- private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException {
- s.defaultWriteObject(); // requires serialPersistentFields to be an empty array
- s.writeObject(returnType());
- s.writeObject(parameterArray());
- }
-
- /**
- * Reconstitute the {@code MethodType} instance from a stream (that is,
- * deserialize it).
- * This instance is a scratch object with bogus final fields.
- * It provides the parameters to the factory method called by
- * {@link #readResolve readResolve}.
- * After that call it is discarded.
- * @param s the stream to read the object from
- * @throws java.io.IOException if there is a problem reading the object
- * @throws ClassNotFoundException if one of the component classes cannot be resolved
- * @see #MethodType()
- * @see #readResolve
- * @see #writeObject
- */
- private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
- s.defaultReadObject(); // requires serialPersistentFields to be an empty array
-
- Class<?> returnType = (Class<?>) s.readObject();
- Class<?>[] parameterArray = (Class<?>[]) s.readObject();
-
- // Probably this object will never escape, but let's check
- // the field values now, just to be sure.
- checkRtype(returnType);
- checkPtypes(parameterArray);
-
- parameterArray = parameterArray.clone(); // make sure it is unshared
- MethodType_init(returnType, parameterArray);
- }
-
- /**
- * For serialization only.
- * Sets the final fields to null, pending {@code Unsafe.putObject}.
- */
- private MethodType() {
- this.rtype = null;
- this.ptypes = null;
- }
- private void MethodType_init(Class<?> rtype, Class<?>[] ptypes) {
- // In order to communicate these values to readResolve, we must
- // store them into the implementation-specific final fields.
- checkRtype(rtype);
- checkPtypes(ptypes);
- UNSAFE.putObject(this, rtypeOffset, rtype);
- UNSAFE.putObject(this, ptypesOffset, ptypes);
- }
-
- // Support for resetting final fields while deserializing
- private static final long rtypeOffset, ptypesOffset;
- static {
- try {
- rtypeOffset = UNSAFE.objectFieldOffset
- (MethodType.class.getDeclaredField("rtype"));
- ptypesOffset = UNSAFE.objectFieldOffset
- (MethodType.class.getDeclaredField("ptypes"));
- } catch (Exception ex) {
- throw new Error(ex);
- }
- }
-
- /**
- * Resolves and initializes a {@code MethodType} object
- * after serialization.
- * @return the fully initialized {@code MethodType} object
- */
- private Object readResolve() {
- // Do not use a trusted path for deserialization:
- //return makeImpl(rtype, ptypes, true);
- // Verify all operands, and make sure ptypes is unshared:
- return methodType(rtype, ptypes);
- }
-
- /**
- * Simple implementation of weak concurrent intern set.
- *
- * @param <T> interned type
- */
- private static class ConcurrentWeakInternSet<T> {
-
- private final ConcurrentMap<WeakEntry<T>, WeakEntry<T>> map;
- private final ReferenceQueue<T> stale;
-
- public ConcurrentWeakInternSet() {
- this.map = new ConcurrentHashMap<>();
- this.stale = new ReferenceQueue<>();
- }
-
- /**
- * Get the existing interned element.
- * This method returns null if no element is interned.
- *
- * @param elem element to look up
- * @return the interned element
- */
- public T get(T elem) {
- if (elem == null) throw new NullPointerException();
- expungeStaleElements();
-
- WeakEntry<T> value = map.get(new WeakEntry<>(elem));
- if (value != null) {
- T res = value.get();
- if (res != null) {
- return res;
- }
- }
- return null;
- }
-
- /**
- * Interns the element.
- * Always returns non-null element, matching the one in the intern set.
- * Under the race against another add(), it can return <i>different</i>
- * element, if another thread beats us to interning it.
- *
- * @param elem element to add
- * @return element that was actually added
- */
- public T add(T elem) {
- if (elem == null) throw new NullPointerException();
-
- // Playing double race here, and so spinloop is required.
- // First race is with two concurrent updaters.
- // Second race is with GC purging weak ref under our feet.
- // Hopefully, we almost always end up with a single pass.
- T interned;
- WeakEntry<T> e = new WeakEntry<>(elem, stale);
- do {
- expungeStaleElements();
- WeakEntry<T> exist = map.putIfAbsent(e, e);
- interned = (exist == null) ? elem : exist.get();
- } while (interned == null);
- return interned;
- }
-
- private void expungeStaleElements() {
- Reference<? extends T> reference;
- while ((reference = stale.poll()) != null) {
- map.remove(reference);
- }
- }
-
- private static class WeakEntry<T> extends WeakReference<T> {
-
- public final int hashcode;
-
- public WeakEntry(T key, ReferenceQueue<T> queue) {
- super(key, queue);
- hashcode = key.hashCode();
- }
-
- public WeakEntry(T key) {
- super(key);
- hashcode = key.hashCode();
- }
-
- @Override
- public boolean equals(Object obj) {
- if (obj instanceof WeakEntry) {
- Object that = ((WeakEntry) obj).get();
- Object mine = get();
- return (that == null || mine == null) ? (this == obj) : mine.equals(that);
- }
- return false;
- }
-
- @Override
- public int hashCode() {
- return hashcode;
- }
-
- }
- }
+ public String toMethodDescriptorString() { return null; }
}
diff --git a/java/lang/invoke/VarHandle.java b/java/lang/invoke/VarHandle.java
index da56e8d4..b6a9537c 100644
--- a/java/lang/invoke/VarHandle.java
+++ b/java/lang/invoke/VarHandle.java
@@ -2289,9 +2289,6 @@ public abstract class VarHandle {
/** BitMask of all access modes. */
private final static int ALL_MODES_BIT_MASK;
- /** Indicator of machine word size. */
- private final static boolean RUNNING_ON_64BIT = VMRuntime.getRuntime().is64Bit();
-
static {
// Check we're not about to overflow the storage of the
// bitmasks here and in the accessModesBitMask field.
@@ -2368,13 +2365,11 @@ public abstract class VarHandle {
//
// The supported access modes are described in:
// @see java.lang.invoke.MethodHandles#byteArrayViewVarHandle
- int bitMask = 0;
- // Read/write access modes supported for all types except for
- // long and double on 32-bit platforms.
- if (RUNNING_ON_64BIT || (varType != long.class && varType != double.class)) {
- bitMask |= READ_ACCESS_MODES_BIT_MASK | WRITE_ACCESS_MODES_BIT_MASK;
- }
+ // Read/write access modes supported for all types including
+ // long and double on 32-bit platforms (though these accesses
+ // may not be atomic).
+ int bitMask = READ_ACCESS_MODES_BIT_MASK | WRITE_ACCESS_MODES_BIT_MASK;
// int, long, float, double support atomic update modes per documentation.
if (varType == int.class || varType == long.class ||
diff --git a/java/net/URI.java b/java/net/URI.java
index bfce7b40..cadcecab 100644
--- a/java/net/URI.java
+++ b/java/net/URI.java
@@ -194,7 +194,7 @@ import java.lang.NullPointerException; // for javadoc
* Resolving the relative URI
*
* <blockquote>
- * {@code ../../../demo/jfc/SwingSet2/src/SwingSet2.java&nbsp;&nbsp;&nbsp;&nbsp;}(2)
+ * {@code ../../../demo/jfc/SwingSet2/src/SwingSet2.java}&nbsp;&nbsp;&nbsp;&nbsp;(2)
* </blockquote>
*
* against this result yields, in turn,
@@ -308,7 +308,7 @@ import java.lang.NullPointerException; // for javadoc
*
* <li><p><a name="encode"></a> A character is <i>encoded</i> by replacing it
* with the sequence of escaped octets that represent that character in the
- * UTF-8 character set. The Euro currency symbol ({@code '&#92;u20AC'}),
+ * UTF-8 character set. The Euro currency symbol ({@code '\u005Cu20AC'}),
* for example, is encoded as {@code "%E2%82%AC"}. <i>(<b>Deviation from
* RFC&nbsp;2396</b>, which does not specify any particular character
* set.)</i> </p></li>
@@ -327,7 +327,7 @@ import java.lang.NullPointerException; // for javadoc
* decoding any encoded non-US-ASCII characters. If a <a
* href="../nio/charset/CharsetDecoder.html#ce">decoding error</a> occurs
* when decoding the escaped octets then the erroneous octets are replaced by
- * {@code '&#92;uFFFD'}, the Unicode replacement character. </p></li>
+ * {@code '\u005CuFFFD'}, the Unicode replacement character. </p></li>
*
* </ul>
*
@@ -1065,7 +1065,7 @@ public final class URI
* Constructs a URL from this URI.
*
* <p> This convenience method works as if invoking it were equivalent to
- * evaluating the expression {@code new&nbsp;URL(this.toString())} after
+ * evaluating the expression {@code new URL(this.toString())} after
* first checking that this URI is absolute. </p>
*
* @return A URL constructed from this URI
@@ -1483,7 +1483,7 @@ public final class URI
*
* <p> The ordering of URIs is defined as follows: </p>
*
- * <ul type=disc>
+ * <ul>
*
* <li><p> Two URIs with different schemes are ordered according the
* ordering of their schemes, without regard to case. </p></li>
@@ -1501,7 +1501,7 @@ public final class URI
* <li><p> Two hierarchical URIs with identical schemes are ordered
* according to the ordering of their authority components: </p>
*
- * <ul type=disc>
+ * <ul>
*
* <li><p> If both authority components are server-based then the URIs
* are ordered according to their user-information components; if these
diff --git a/java/nio/charset/CharsetDecoderICU.java b/java/nio/charset/CharsetDecoderICU.java
index 0a73c9f6..fca29002 100644
--- a/java/nio/charset/CharsetDecoderICU.java
+++ b/java/nio/charset/CharsetDecoderICU.java
@@ -51,24 +51,26 @@ final class CharsetDecoderICU extends CharsetDecoder {
// This complexity is necessary to ensure that even if the constructor, superclass
// constructor, or call to updateCallback throw, we still free the native peer.
long address = 0;
+ CharsetDecoderICU result;
try {
address = NativeConverter.openConverter(icuCanonicalName);
float averageCharsPerByte = NativeConverter.getAveCharsPerByte(address);
- CharsetDecoderICU result = new CharsetDecoderICU(cs, averageCharsPerByte, address);
- address = 0; // CharsetDecoderICU has taken ownership; its finalizer will do the free.
- result.updateCallback();
- return result;
- } finally {
+ result = new CharsetDecoderICU(cs, averageCharsPerByte, address);
+ } catch (Throwable t) {
if (address != 0) {
NativeConverter.closeConverter(address);
}
+ throw t;
}
+ // An exception in registerConverter() will deallocate address:
+ NativeConverter.registerConverter(result, address);
+ result.updateCallback();
+ return result;
}
private CharsetDecoderICU(Charset cs, float averageCharsPerByte, long address) {
super(cs, averageCharsPerByte, MAX_CHARS_PER_BYTE);
this.converterHandle = address;
- NativeConverter.registerConverter(this, converterHandle);
}
@Override protected void implReplaceWith(String newReplacement) {
diff --git a/java/nio/charset/CharsetEncoderICU.java b/java/nio/charset/CharsetEncoderICU.java
index 3d12664d..a347db8c 100644
--- a/java/nio/charset/CharsetEncoderICU.java
+++ b/java/nio/charset/CharsetEncoderICU.java
@@ -67,20 +67,23 @@ final class CharsetEncoderICU extends CharsetEncoder {
// This complexity is necessary to ensure that even if the constructor, superclass
// constructor, or call to updateCallback throw, we still free the native peer.
long address = 0;
+ CharsetEncoderICU result;
try {
address = NativeConverter.openConverter(icuCanonicalName);
float averageBytesPerChar = NativeConverter.getAveBytesPerChar(address);
float maxBytesPerChar = NativeConverter.getMaxBytesPerChar(address);
byte[] replacement = makeReplacement(icuCanonicalName, address);
- CharsetEncoderICU result = new CharsetEncoderICU(cs, averageBytesPerChar, maxBytesPerChar, replacement, address);
- address = 0; // CharsetEncoderICU has taken ownership; its finalizer will do the free.
- result.updateCallback();
- return result;
- } finally {
+ result = new CharsetEncoderICU(cs, averageBytesPerChar, maxBytesPerChar, replacement, address);
+ } catch (Throwable t) {
if (address != 0) {
NativeConverter.closeConverter(address);
}
+ throw t;
}
+ // An exception in registerConverter() will deallocate address:
+ NativeConverter.registerConverter(result, address);
+ result.updateCallback();
+ return result;
}
private static byte[] makeReplacement(String icuCanonicalName, long address) {
@@ -97,7 +100,6 @@ final class CharsetEncoderICU extends CharsetEncoder {
super(cs, averageBytesPerChar, maxBytesPerChar, replacement, true);
// Our native peer needs to know what just happened...
this.converterHandle = address;
- NativeConverter.registerConverter(this, converterHandle);
}
@Override protected void implReplaceWith(byte[] newReplacement) {
diff --git a/java/security/AlgorithmParameters.java b/java/security/AlgorithmParameters.java
index 864866ef..bca4a5cb 100644
--- a/java/security/AlgorithmParameters.java
+++ b/java/security/AlgorithmParameters.java
@@ -66,6 +66,10 @@ import sun.security.jca.Providers;
* <td>10+</td>
* </tr>
* <tr>
+ * <td>ChaCha20</td>
+ * <td>28+</td>
+ * </tr>
+ * <tr>
* <td>DES</td>
* <td>1+</td>
* </tr>
diff --git a/java/text/DateFormat.java b/java/text/DateFormat.java
index 647973e2..d7d5081b 100644
--- a/java/text/DateFormat.java
+++ b/java/text/DateFormat.java
@@ -803,12 +803,22 @@ public abstract class DateFormat extends Format {
} else {
dateStyle = -1;
}
- // Android-changed: Removed used of DateFormatProvider.
+
+ // BEGIN Android-changed: Remove use of DateFormatProvider and LocaleProviderAdapter.
+ /*
+ LocaleProviderAdapter adapter = LocaleProviderAdapter.getAdapter(DateFormatProvider.class, loc);
+ DateFormat dateFormat = get(adapter, timeStyle, dateStyle, loc);
+ if (dateFormat == null) {
+ dateFormat = get(LocaleProviderAdapter.forJRE(), timeStyle, dateStyle, loc);
+ }
+ return dateFormat;
+ */
try {
return new SimpleDateFormat(timeStyle, dateStyle, loc);
} catch (MissingResourceException e) {
return new SimpleDateFormat("M/d/yy h:mm a");
}
+ // END Android-changed: Remove use of DateFormatProvider and LocaleProviderAdapter.
}
/**
diff --git a/java/text/SimpleDateFormat.java b/java/text/SimpleDateFormat.java
index 8ebe7c54..0062e516 100644
--- a/java/text/SimpleDateFormat.java
+++ b/java/text/SimpleDateFormat.java
@@ -574,9 +574,42 @@ public class SimpleDateFormat extends DateFormat {
* class.
*/
public SimpleDateFormat() {
+ // Android-changed: Android has no LocaleProviderAdapter. Use ICU locale data.
+ // this("", Locale.getDefault(Locale.Category.FORMAT));
+ // applyPatternImpl(LocaleProviderAdapter.getResourceBundleBased().getLocaleResources(locale)
+ // .getDateTimePattern(SHORT, SHORT, calendar));
this(SHORT, SHORT, Locale.getDefault(Locale.Category.FORMAT));
}
+ // BEGIN Android-added: Ctor used by DateFormat to remove use of LocaleProviderAdapter.
+ /**
+ * Constructs a <code>SimpleDateFormat</code> using the given date and time formatting styles.
+ * @param timeStyle the given date formatting style.
+ * @param dateStyle the given time formatting style.
+ * @param locale the locale whose pattern and date format symbols should be used
+ */
+ SimpleDateFormat(int timeStyle, int dateStyle, Locale locale) {
+ this(getDateTimeFormat(timeStyle, dateStyle, locale), locale);
+ }
+
+ private static String getDateTimeFormat(int timeStyle, int dateStyle, Locale locale) {
+ LocaleData localeData = LocaleData.get(locale);
+ if ((timeStyle >= 0) && (dateStyle >= 0)) {
+ Object[] dateTimeArgs = {
+ localeData.getDateFormat(dateStyle),
+ localeData.getTimeFormat(timeStyle),
+ };
+ return MessageFormat.format("{0} {1}", dateTimeArgs);
+ } else if (timeStyle >= 0) {
+ return localeData.getTimeFormat(timeStyle);
+ } else if (dateStyle >= 0) {
+ return localeData.getDateFormat(dateStyle);
+ } else {
+ throw new IllegalArgumentException("No date or time style specified");
+ }
+ }
+ // END Android-added: Ctor used by DateFormat to remove use of LocaleProviderAdapter.
+
/**
* Constructs a <code>SimpleDateFormat</code> using the given pattern and
* the default date format symbols for the default
@@ -647,40 +680,6 @@ public class SimpleDateFormat extends DateFormat {
useDateFormatSymbols = true;
}
- /* Package-private, called by DateFormat factory methods */
- SimpleDateFormat(int timeStyle, int dateStyle, Locale loc) {
- if (loc == null) {
- throw new NullPointerException();
- }
-
- this.locale = loc;
- // initialize calendar and related fields
- initializeCalendar(loc);
-
- // BEGIN Android-changed: Use ICU for locale data.
- formatData = DateFormatSymbols.getInstanceRef(loc);
- LocaleData localeData = LocaleData.get(loc);
- if ((timeStyle >= 0) && (dateStyle >= 0)) {
- Object[] dateTimeArgs = {
- localeData.getDateFormat(dateStyle),
- localeData.getTimeFormat(timeStyle),
- };
- pattern = MessageFormat.format("{0} {1}", dateTimeArgs);
- }
- else if (timeStyle >= 0) {
- pattern = localeData.getTimeFormat(timeStyle);
- }
- else if (dateStyle >= 0) {
- pattern = localeData.getDateFormat(dateStyle);
- }
- else {
- throw new IllegalArgumentException("No date or time style specified");
- }
- // END Android-changed: Use ICU for locale data.
-
- initialize(loc);
- }
-
/* Initialize compiledPattern and numberFormat fields */
private void initialize(Locale loc) {
// Verify and compile the given pattern.
diff --git a/java/time/zone/IcuZoneRulesProvider.java b/java/time/zone/IcuZoneRulesProvider.java
index 91d3a4c1..5a4e37d8 100644
--- a/java/time/zone/IcuZoneRulesProvider.java
+++ b/java/time/zone/IcuZoneRulesProvider.java
@@ -21,9 +21,8 @@
package java.time.zone;
-import android.icu.impl.OlsonTimeZone;
-import android.icu.impl.ZoneMeta;
import android.icu.util.AnnualTimeZoneRule;
+import android.icu.util.BasicTimeZone;
import android.icu.util.DateTimeRule;
import android.icu.util.InitialTimeZoneRule;
import android.icu.util.TimeZone;
@@ -59,7 +58,7 @@ public class IcuZoneRulesProvider extends ZoneRulesProvider {
@Override
protected Set<String> provideZoneIds() {
- Set<String> zoneIds = ZoneMeta.getAvailableIDs(TimeZone.SystemTimeZoneType.ANY, null, null);
+ Set<String> zoneIds = TimeZone.getAvailableIDs(TimeZone.SystemTimeZoneType.ANY, null, null);
zoneIds = new HashSet<>(zoneIds);
// java.time assumes ZoneId that start with "GMT" fit the pattern "GMT+HH:mm:ss" which these
// do not. Since they are equivalent to GMT, just remove these aliases.
@@ -82,10 +81,10 @@ public class IcuZoneRulesProvider extends ZoneRulesProvider {
}
/*
- * This implementation is only tested with OlsonTimeZone objects and depends on
+ * This implementation is only tested with BasicTimeZone objects and depends on
* implementation details of that class:
*
- * 0. TimeZone.getFrozenTimeZone() always returns an OlsonTimeZone object.
+ * 0. TimeZone.getFrozenTimeZone() always returns a BasicTimeZone object.
* 1. The first rule is always an InitialTimeZoneRule (guaranteed by spec).
* 2. AnnualTimeZoneRules are only used as "final rules".
* 3. The final rules are either 0 or 2 AnnualTimeZoneRules
@@ -105,9 +104,9 @@ public class IcuZoneRulesProvider extends ZoneRulesProvider {
static ZoneRules generateZoneRules(String zoneId) {
TimeZone timeZone = TimeZone.getFrozenTimeZone(zoneId);
// Assumption #0
- verify(timeZone instanceof OlsonTimeZone, zoneId,
+ verify(timeZone instanceof BasicTimeZone, zoneId,
"Unexpected time zone class " + timeZone.getClass());
- OlsonTimeZone tz = (OlsonTimeZone) timeZone;
+ BasicTimeZone tz = (BasicTimeZone) timeZone;
TimeZoneRule[] rules = tz.getTimeZoneRules();
// Assumption #1
InitialTimeZoneRule initial = (InitialTimeZoneRule) rules[0];
diff --git a/java/util/TreeMap.java b/java/util/TreeMap.java
index dc55ba6d..f46fbb79 100644
--- a/java/util/TreeMap.java
+++ b/java/util/TreeMap.java
@@ -536,34 +536,8 @@ public class TreeMap<K,V>
public V put(K key, V value) {
TreeMapEntry<K,V> t = root;
if (t == null) {
- // BEGIN Android-changed: Work around buggy comparators. http://b/34084348
- // We could just call compare(key, key) for its side effect of checking the type and
- // nullness of the input key. However, several applications seem to have written comparators
- // that only expect to be called on elements that aren't equal to each other (after
- // making assumptions about the domain of the map). Clearly, such comparators are bogus
- // because get() would never work, but TreeSets are frequently used for sorting a set
- // of distinct elements.
- //
- // As a temporary work around, we perform the null & instanceof checks by hand so that
- // we can guarantee that elements are never compared against themselves.
- //
- // **** THIS CHANGE WILL BE REVERTED IN A FUTURE ANDROID RELEASE ****
- //
- // Upstream code was:
- // compare(key, key); // type (and possibly null) check
- if (comparator != null) {
- if (key == null) {
- comparator.compare(key, key);
- }
- } else {
- if (key == null) {
- throw new NullPointerException("key == null");
- } else if (!(key instanceof Comparable)) {
- throw new ClassCastException(
- "Cannot cast" + key.getClass().getName() + " to Comparable.");
- }
- }
- // END Android-changed: Work around buggy comparators. http://b/34084348
+ compare(key, key); // type (and possibly null) check
+
root = new TreeMapEntry<>(key, value, null);
size = 1;
modCount++;
diff --git a/javax/crypto/Cipher.java b/javax/crypto/Cipher.java
index 010587d4..f3da6792 100644
--- a/javax/crypto/Cipher.java
+++ b/javax/crypto/Cipher.java
@@ -174,18 +174,29 @@ import sun.security.jca.*;
* <td>26+</td>
* </tr>
* <tr>
- * <td>ARC4</td>
+ * <td rowspan="2">ARC4</td>
* <td>ECB</td>
* <td>NoPadding</td>
* <td>10+</td>
* </tr>
* <tr>
+ * <td>NONE</td>
+ * <td>NoPadding</td>
+ * <td>28+</td>
+ * </tr>
+ * <tr>
* <td>BLOWFISH</td>
* <td>CBC<br>CFB<br>CTR<br>CTS<br>ECB<br>OFB</td>
* <td>ISO10126Padding<br>NoPadding<br>PKCS5Padding</td>
* <td>10+</td>
* </tr>
* <tr>
+ * <td>ChaCha20</td>
+ * <td>NONE<br>Poly1305</td>
+ * <td>NoPadding</td>
+ * <td>28+</td>
+ * </tr>
+ * <tr>
* <td>DES</td>
* <td>CBC<br>CFB<br>CTR<br>CTS<br>ECB<br>OFB</td>
* <td>ISO10126Padding<br>NoPadding<br>PKCS5Padding</td>
diff --git a/javax/crypto/KeyGenerator.java b/javax/crypto/KeyGenerator.java
index b0977f0a..2d6f43dd 100644
--- a/javax/crypto/KeyGenerator.java
+++ b/javax/crypto/KeyGenerator.java
@@ -109,6 +109,10 @@ import sun.security.jca.GetInstance.Instance;
* <td>10+</td>
* </tr>
* <tr>
+ * <td>ChaCha20</td>
+ * <td>28+</td>
+ * </tr>
+ * <tr>
* <td>DES</td>
* <td>1+</td>
* </tr>